6502 without the memory maze¶
The processor may see 64 KiB at a time. Your d/BASIC program is not a 64 KiB program.
This page is about running d/BASIC on the 6502, the processor in the Atari 800, the Commodore 64 and the Apple II. It explains why your program is not limited to the 64 KiB the processor can see at once, what the first 6502 profile aims for, and which habits still matter on a small machine. After reading it you will know what the runtime, the program that runs your compiled code, handles for you, and what stays your job.
The 6502 is an important home for d/BASIC, not a novelty mode. It brings a wonderfully small, understandable processor into a language with objects, widgets, large arrays, packaged resources and modern operating-system services. The trick is not pretending the processor has a wider address bus. The trick is making that address bus the runtime's problem rather than yours.
The first profile aims at 512 KiB¶
Many older BASIC environments were squeezed into 16 to 64 KiB alongside the interpreter, the screen, the operating system, the source text, the variables and the string heap. Programs learned to survive through overlays, tiny buffers, bank registers and machine-specific copy routines.
d/BASIC removes that ceiling from the language. Its first 6502 profile sets a nominal useful workspace target of 512 KiB. Later profiles can offer more without adding a dialect or requiring a rewrite. The 512 KiB figure is the first target, not a permanent language limit, not a flat pointer, and not a guarantee that any particular allocation will succeed.
Before a program runs, the 6502 runtime (the second, smaller runtime written for that processor) checks the compiled file, its static data and the program's known minimum needs. More demand can arrive later from REDIM, growing strings, new objects, resources, or work done by a provider (the part of the host that supplies files, sound, or the network). Each such request is handled as a whole: if there is no room, the runtime declines that one request and leaves the old array and everything already visible intact. A smaller machine never carries on with a half-applied allocation on the strength of a marketing number.
This distinction matters:
| Fact | Who deals with it? |
|---|---|
| the 6502 has a 16-bit address window | the runtime and its hardware layer |
| a particular byte currently lives in some bank or page | the memory provider, the part of the runtime that supplies storage |
| an array has 100,001 elements | your d/BASIC program |
| a machine cannot find room for a request | a named "out of capacity" stop, never corruption |
You still design within a real machine's published capacity. You just do it with values and bounds instead of addresses and bank numbers.
Write DIM, not a bank-switching ritual¶
DIM Samples%(0 TO 4095)
ARRAYFILL Samples%(), -1
Samples%(2048) = 42
PRINT Samples%(2048)
ERASE Samples%
END
That program contains no far pointer, no page call, no bank selector and no copy-through buffer. DIM asks for an array of a given shape. The runtime chooses where it lives, pages values in as needed, checks every access, and keeps the array valid until ERASE releases it.
Arrays, variable-length strings, objects and other dynamic values live behind managed handles. A handle is never a machine address. It can name storage in resident memory, in a banked pool, or in another tier the machine offers, while the same compiled file keeps the same meaning.
Banking is deliberately invisible¶
The toolchain and the runtime decide where code and constants go. The 6502 runtime keeps a bounded working set within reach and pages the unchanging parts of the compiled file in and out through its own private layer. On a miss, the runtime performs the bank switch and then carries on with the same bytecode operation.
None of this reaches your source:
- no bank register names;
- no overlay annotations;
- no near and far pointer types;
- no "copy this bank into conventional memory first" call; and
- no machine test around an array, an object or a procedure call.
If a bank is selected on the way into a runtime operation, the runtime restores the mapping it needs before returning. Event delivery, object identity, array bounds and error behaviour therefore never depend on which page happened to be resident.
One optional memory policy, still no banking¶
By default, a runtime may satisfy storage requests from every tier it honestly offers: fast resident memory, the ordinary program pool, and a slower media tier. Most programs should keep that default.
OPTION TIER RAM is the one memory ceiling you can see in source. It excludes the media tier when a program needs RAM-only latency. It does not select a bank, choose an address, or promise that every allocation will fit. Running out of the allowed tiers produces a clear "out of capacity" stop instead of a silent spill or corruption.
Intents are the acceleration API¶
Hidden memory management solves capacity. Intents solve the other classic 6502 problem: asking a small CPU to do every pixel, floating-point operation, digest, transfer and database step one byte at a time. An intent is a named request such as "fill this rectangle" or "play this tone" that the machine carries out however it can.
INTENT GFX.BEGIN_FRAME()
INTENT GFX.FILL_RECT(0, 0, 320, 192, 2081)
INTENT GFX.DRAW_TEXT(12, 12, "HELLO FROM d/BASIC", 3, -1, 2081)
INTENT GFX.PRESENT()
An intent describes meaning. A 6502 runtime can hand a request to the display side of the machine, a maths engine, a bulk-memory provider, a storage service, or another accelerator. A desktop runtime may do the same request through its own local route. Your d/BASIC code sees one operation, one ordering rule and one result.
Acceleration is never a hidden second program. When several correct routes exist, the runtime picks the best one available. When no honest route can satisfy an operation the program requires, the runtime stops, before or during the run, and says which operation is missing rather than pretending it succeeded.
Give the optimizer a big idea¶
A 6502 benefits most when the source says what a whole operation means:
These verbs give the compiler and the runtime one unit of meaning that can be sliced, accelerated, or done locally in a simpler way. ARRAYSWAP exchanges two names in constant work; it never walks either array. Pure floating-point expressions can likewise become one ordered MATH request instead of one crossing per operator.
The developer-friendly rule is simple: write the clearest large operation. The toolchain handles placement and keeps the number of crossings down.
What still belongs to the programmer¶
Hidden banking does not mean imaginary resources. Good 6502-friendly d/BASIC still has a shape:
- Use the smallest honest numeric type for large collections.
- State whole-array work with
ARRAYFILL,ARRAYCOPYandARRAYSWAP. - Keep resource and network operations bounded and asynchronous.
- Repaint the regions that changed instead of every unchanged control.
- Release arrays, objects, streams and sessions when you are done with them.
- Declare the capability families you require with
APP ... NEEDS, and ask about optional capabilities only when your program has two useful experiences to offer. - Test the exact compiled file on every profile you intend to ship to.
What you do not manage is just as important: bank layouts, resident trampolines, provider mailboxes, accelerator command packets, and restoring the machine's mapping state. Those are the runtime's promises.
The promise in one line¶
A learner gets DIM, arrays, classes and widgets. An experienced 6502 developer gets a real small-machine target without carrying three decades of manual memory machinery into every program. That is the point: the charm of the 6502 stays; its old ceiling on what a program could be does not.