# Nova128 Compiler Memory Map Story: E01-S05 Status: frozen for the compiler (D117, source cache D118); verified in VICE x128 3.10 Authoritative source: `cfg/compiler.cfg`. This document explains it. E04-S01 defines the generated PRG/MAP container below; E04-S02 adds the provisional frame runtime. E05-S01 supplies an explicit-region heap; E05-S02 freezes class descriptors and MAP class records; E05-S03 adds construction and deletion above both. Production heap placement and any future banking remain later stories. ## Generated Program Container (E04-S01) | Element | Value | |---|---| | Load address | `$1C01` | | BASIC 7 stub | 12 bytes, `10 SYS 7181` | | Entry | `$1C0D` | | Sections | ascending addresses, zero-filled gaps | | MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size`, `CLASS name address size destroy count ifdir`, `METHOD name address` | | Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses existing `NAME.PRG`/`NAME.MAP` and scratches outputs it created on failure. The generated program still uses the test mailbox below. ## Generated Program Frames (E04-S02) | Element | Value | |---|---| | Runtime mapping | CR `$0E`, IRQs masked, entry CR restored | | Software stack | bank 0 `$B800-$BFFF`, grows down from `$C000` | | `SP` / `FP` | `$FB-$FC` / `$FD-$FE` | | Frame | previous FP at `FP`; arguments from `FP+2`; locals below `FP` | | Caller | pushes arguments right-to-left, then discards them after return | | Overflow | `FRAME_ERR_STACK`, C=1, `SP`/`FP` restored | The frame fixture links code below `$B800` and runs with CR `$0E`; it restores the entry CR before printing and returning to BASIC. Code generation will install this mapping and call `frame_init` when it starts emitting user code (E04-S03). The 2 KB stack and `$40` hardware-stack guard are provisional. ## Member Access (E05-S04) | Element | Value | |---|---| | Receiver pointer | `MB_SELF` = `$F9-$FA` in the generated program | | Field access | `LDY #offset` + `LDA/STA (MB_SELF),Y`; word fields add +1 byte | | Method entry | self in A/X, stored to `MB_SELF` by the body prologue | | Concrete call | `JSR` to a fixed address, then `frame_discard` for the arguments | | Fixture code | below `$4000` | `MB_SELF` occupies the two bytes directly below the frame ABI at `$FB-$FE`; `src/compiler/members.s` asserts at link time that the pair overlaps neither `FRAME_SP` nor `FRAME_FP`. Field offsets are the parser's layout offsets, with the first field at +2, so generated code needs no runtime translation. ## Statement Emission (E04-S06) | Element | Value | |---|---| | Slot pointer | `EM_LP` = `$F5-$F6`, set to `FP + signed offset` | | Combine temp | `EM_TMP` = `$F7` | | Parameters | `FP+2` upward, declaration order | | Locals | `FP-1` downward, declaration order | | For end values | `EM_FOR_END` = `$D0-$E0`, one byte per block depth | | Flow labels / fixups | 64 labels, 128 fixups (BSS, no fixed address) | | Method labels | 256 bytes, one entry label by member row | | Program entry | `em_entry` = offset of the program body in `cg_code` | | Code buffer | `cg_code`, 255 bytes (E04-S03) | | Final code base | `fl_code_base`, default `cg_code`; pass 3 uses `PRG_ENTRY_ADDR + pre-CODE bytes` | | Routine sizes | `em_routine_sizes`, one byte per routine, recorded by `em_end_func` | | Sizing mismatch | `sz_mismatch`; nonzero raises E228 after the emitting pass | | Print entries | `em_pr_text`/`em_pr_byte`/`em_pr_cr` cells at `$C0-$C5`, with `JMP (cell)` trampolines | `EM_LP` and `EM_TMP` sit below `MB_SELF` (`$F9-$FA`) and clear of the frame ABI at `$FB-$FE`; `emit.s` asserts at link time that `EM_TMP` does not overlap `MB_SELF`. Slot offsets are 8-bit frame displacements, so a function's locals and parameters together must stay under 127 bytes until pass-3 sizing assigns frame sizes. Control-flow labels use the `flow.s` pool; `if`/`while`/`for` labels are released only after their fixups resolve. Method entry labels are allocated by member row, so a forward self-call can target a method body parsed later. The 255-byte code buffer and the one-byte `em_entry` still bound the current emitted program. Count-mode sizing checks that the emitting pass keeps the same length and label offsets; a mismatch raises E228 instead of producing a shifted program. ## Heap Fixture (E05-S01) | Element | Value | |---|---| | Region supplied by caller | page-aligned base and 1..127 pages | | Block header | 2-byte total size (bit 15 allocated), 2-byte next/magic | | Payload | header+4; two-byte aligned; minimum block 6 bytes | | Fixture region | bank 0 `$4000-$4FFF` under CR `$0E` | | Fixture code | below `$4000` | | Exhaustion | `HEAP_ERR_OOM`, mapped to R005 by the fixture | `heap_init` accepts the region explicitly, so this does not freeze the generated-program heap location. The block/API contract is in `src/common/heap.inc`; D133 records the provisional decision. The allocator does not print faults itself; E05-S03 will map `HEAP_ERR_OOM` when `new` is wired. ## Class Descriptors (E05-S02) | Element | Value | |---|---| | Object header | descriptor pointer at +0; fields from +2 | | Descriptor | 8 bytes: size, destructor, interface count, directory, flags | | Instance size | includes the two-byte object header | | Destructor | `$0000` means none | | Interface directory | 4-byte entries: interface descriptor, method table | | Flags | reserved, must be 0; no parent-class slot | | MAP | `CLASS name address size destroy count ifdir` and `METHOD name address` after sections | The ABI is fixed in `src/common/class.inc`; D134 records the provisional decision. `src/runtime/objects.s` reads descriptors and objects without allocating. `output.s` writes class MAP records in `out_class` call order and method MAP records in `out_method` call order. The descriptor address comes from `out_class_addr`; method addresses come from `out_method_addr`. The E05-S02 fixture checks a no-destructor/no-interface class, a destructor plus one interface entry, two method addresses, and both descriptor addresses. Interface dispatch remains E06 work. ## Construction and Deletion (E05-S03) | Element | Value | |---|---| | New fixed addresses | none; `obj_new` uses the caller-initialized heap | | Fixture region | bank 0 `$4000-$4FFF` under CR `$0E`, code below `$4000` | | Module zero page | `co_vec`, `co_desc`, `co_ptr`, `co_self` (link-allocated) | | Module BSS | `obj_init_ptr`, `obj_error`, saved object/size/destructor | | `obj_call` stack cost | one synthetic return address, two hardware-stack bytes | | Constructor arguments | stay on the software frame stack at `FP+2`; caller discards | `src/runtime/construct.s` adds no fixed addresses, so the generated-program heap location stays open. `co_vec` carries the indirect target and the module asserts it does not end at `$xxFF`, because `JMP ($xxFF)` reads its high byte from the wrong page. Constructor arguments are not copied again: `init` enters its normal frame and reads them where the caller pushed them. ## Interface Values (E06-S01) | Element | Value | |---|---| | Value | 4 bytes: object pointer +0..1, method-table pointer +2..3, little-endian | | Null | four zero bytes; a non-null object with a zero table is `IF_ERR_TABLE` | | Method table | 2-byte entries in interface slot order, no header; slot *n* at table+2*n | | Interface params/locals/fields/array elements | 4 bytes, no alignment padding | | Argument order | caller pushes four bytes right-to-left, object low at `FP+2` | | Four-byte result | `IF_RESULT` = `$C6-$C9`; the caller copies it before the next call | | Module zero page | `if_obj`, `if_tab`, `if_dst`, `if_src`, `if_vec`, `if_self` (link-allocated) | | Module status | `if_error`: `IF_OK`, `IF_ERR_NULL`, `IF_ERR_TABLE`, `IF_ERR_ENTRY`, `IF_ERR_STACK` | | Dispatch | `if_call` takes A/X = value, Y = slot; a refusal leaves A/X=0, C=1 and sets `if_error` | | Dispatch floor | TSX at or below `FRAME_HW_MIN` (`$40`) is refused with `IF_ERR_STACK` | | Table emitter | `it_begin` A/X = first table address; `it_set_addr` A/X = address, Y = member row; `it_class` A = class name ID | | Table emitter state | `it_count`, `it_used` (2-byte watermark), `it_error` (`IT_OK`..`IT_ERR_UNSET`), `it_data` (512-byte buffer) | | Table records | `it_class_ids`, `it_iface_ids`, `it_slots`, `it_table_addrs`, `it_offsets`, indexed by table number | | Table result | one table per implemented interface in implements order, two bytes per slot; a failed class restores the watermark and count | | MAP table record | `TABLE class interface address slots slot0 .. slotN` | | Fixture | code below `$4000`, producer `$03`, six phase flags | `src/common/interfaces.inc` fixes the layout; `src/runtime/interfaces.s` implements `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_call`, `if_ret_store`, and `if_ret_load`. The module asserts at link time that `IF_RESULT` follows the print vectors (`$C0-$C5`) and ends below the for-end values (`$D0`), and that `if_vec` does not cross a page. Interface lifetime remains E06-S06. ## Mappings | State | CR | RCR (low bits) | P0 | P1 | IRQs | |---|---|---|---|---|---| | System | as BASIC left it (`$00`) | as BASIC left it | `$0000` | `$0001` | caller's | | Compiler bank 0 | `$0E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | | Compiler bank 1 | `$4E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | RCR bits 6–7 (VIC bank) are always carried over from the system value. `src/compiler/mmu.s` is the only module that writes MMU registers. ## Bank 0 | Range | Size | Name | Contents | |---|---|---|---| | `$0000-$12FF` | | system | BASIC 7, KERNAL, editor, screen. Off limits except `NMINV` (`$0318`), which the compiler saves and restores | | `$1300-$18FF` | 1536 | COMMON | Copied from the PRG by `mmu_init`: MMU switches, KERNAL wrapper, NMI guard, and any code that runs while bank 1 is mapped | | `$1900-$19FF` | 256 | DISKBUF | Disk transfer buffer, visible from both banks | | `$1A00-$1AFF` | 256 | private ZP | Physical page behind logical zero page in compiler states. Not a linker area: nothing else may live here | | `$1B00-$1BEF` | 240 | SCRATCH | Shared state needed from both banks | | `$1BF0-$1BFF` | 16 | MAILBOX | Fixed test mailbox (below) | | `$1C01-$1C0C` | 12 | header | BASIC 7 stub `10 SYS 7181` | | `$1C0D-$B7FF` | 39923 | MAIN | `STARTUP` (always first, entry at `$1C0D`), CODE, RODATA, DATA, COMMON load image, BSS | | `$B800-$BFFF` | 2048 | SWSTACK | Compiler software stack | | `$C000-$FFFF` | | system ROM | KERNAL/editor in both compiler states | `$1300-$1BFF` is the C128's free application area: after boot x128 shows only the power-on fill pattern there. All of `$0000-$1FFF` is bottom common RAM, so COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. ## Bank 1 | Range | Size | Name | Contents | |---|---|---|---| | `$0000-$1FFF` | | hidden | Covered by bottom common RAM in compiler states | | `$2000-$6AFF` | 19200 | SRCCACHE / `SOURCE` | Whole source file, loaded once by `src_load` (18,999-byte limit) | | `$6B00-$BFFF` | 21760 | BANK1 / `TABLES` | Names, symbols, class/interface/type records, pass workspaces | `TABLES` allocations so far (in link order; ld65 rejects overflow): | Owner | Bytes | Contents | |---|---|---| | `names.s` | 5,164 | 64 hash-chain heads + 255 name records of 20 bytes (D120) | | `parser.s` | 6,912 | 27 columns of 256 bytes (`src/common/parser.inc`): type table by name ID (kind, declaration line, member and implements ranges, instance size; E03-S01, E03-S05), 255 member rows (owner, name, flags, type, parameter range, field offset, interface slot; E03-S02, E03-S05, E03-S06) and 255 parameter-type rows | BSS is not zeroed at load: every table must be cleared by its owner before use (`names_init`, `parser.s` `tt_clear`). The member and parameter rows are bounded by their counts instead, so only the type columns are cleared. | `$C000-$FFFF` | | unmapped | System ROM is visible here instead; BASIC string space at the top of bank 1 is untouched | Bank-1 data is reached only through `mmu.s` helpers (`mmu_peek1`/`mmu_poke1` and `mmu_copy_to1`, using the zero-page pointers `mmu_ptr`/`mmu_src`), or by code in COMMON such as `src_fetch_line`. MAIN code never runs while bank 1 is mapped, because MAIN above `$1FFF` is not visible then. BASIC 7 keeps variables in bank 1 from `$0400`. The compiler is started with `RUN`, which clears variables, so overwriting `$2000-$BFFF` loses no BASIC state. ## Zero Page In compiler states P0 points at bank 0 page `$1A`, so the compiler owns the whole logical zero page `$02-$FF` (linker area `ZP`, 254 bytes). `$00/$01` are the 8502 port and are never allocated. Modules allocate zero page only through the `ZEROPAGE` segment. Current users: `mmu_ptr` and `mmu_src` (4 bytes). System zero page stays intact. KERNAL calls switch P0 back to `$0000` for the call. While P0 is relocated, the system zero page is still readable at `$1A00-$1AFF` (the MMU swaps the two pages), e.g. `ST` at `$1A90`. `mmu_init` runs in the system mapping too, before the first `mmu_bank0`, and copies the COMMON load image with ordinary loads. `cfg/compiler.cfg` therefore places that image directly after STARTUP, and `mmu.s` asserts at link time that both `mmu_init` and the image end at or below `$4000`. E03-S02 found this when the parser test's load image first crossed `$4000`: COMMON was copied from BASIC ROM and the test crashed into the monitor. KERNAL file names are not affected: `diskio.s` selects bank 0 with SETBNK, and the KERNAL reads names through an all-RAM mapping. After `mmu_shutdown` the CPU is back in BASIC's mapping, where bank-0 RAM at `$4000-$BFFF` is hidden by BASIC ROM. A program must finish reading its MAIN variables and buffers (and anything else above `$3FFF`) before it calls `mmu_shutdown`. Two test programs read wrong values this way before the rule was written down. ## Interrupts - **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via `$0314` into BASIC's handler, which uses system zero page and would corrupt the private page. `kcall` enables IRQs only for the duration of a KERNAL call, in system state. - **NMI:** cannot be masked. The ROM entry `$FF05` uses no zero page: it pushes A/X/Y/CR, sets CR=0, and jumps via `$0318`. `mmu_init` points `$0318` at `mmu_nmi_guard` (in COMMON), which reads the CIA #2 ICR, counts the event, and exits through `$FF33` (pull CR/Y/X/A, RTI). RESTORE is therefore ignored while the compiler runs. `mmu_shutdown` restores `$0318`. ## Test Mailbox Defined in `src/common/mailbox.inc`. Used by platform tests, the compiler, and generated programs, and read by the harness after the program returns. | Address | Field | Meaning | |---|---|---| | `$1BF0` | magic 0 | `$4E` (`N`) once initialized | | `$1BF1` | magic 1 | `$38` (`8`) | | `$1BF2` | producer | `$01` platform test, `$02` compiler, `$03` generated program | | `$1BF3` | status | `$01` running, `$A5` pass, `$FA` fail | | `$1BF4-$1BF5` | code | Little-endian diagnostic, fault, or failed-check code; 0 on pass | | `$1BF6-$1BF7` | address | Little-endian fault or completion address | | `$1BF8-$1BFF` | data | Eight producer-defined bytes | Protocol: 1. The producer clears all 16 bytes, writes the magic and producer, then sets status `$01` before doing any work. 2. It writes code, address, and data, then writes the final status **last**. 3. The harness polls until the magic matches and status is `$A5` or `$FA`. A status still `$01` at the timeout means the producer hung or crashed. A missing magic means it never started. ## Evidence `scripts/verify-map-skeleton.ps1` links `src/compiler/mmu.s` with `tests/platform/map-skeleton.s` using `cfg/compiler.cfg`, then: 1. Builds five variants that overflow ZP, COMMON, MAIN, SWSTACK, and TABLES by one region's worth. ld65 rejects each in the expected memory area. 2. Checks fixed placements: STARTUP and `skel_entry` at `$1C0D`, MAILBOX at `$1BF0`, COMMON within `$1300-$18FF`, TABLES within `$2000-$BFFF`. 3. Runs the skeleton in x128 and reads the mailbox: `4E 38 01 A5 00 00 … 01 4E 1A C5`. All six checks pass: | Check | What it proves | |---|---| | ZP | A store to logical ZP lands at `$1Axx`; system ZP is unchanged | | KCALL | CHROUT via `kcall_inline` from both states; private ZP and CR/P0 intact afterwards | | BANK1 | First and last TABLES bytes (`$6B00`, `$BFFF`) hold bank-1 values; bank 0 at the same addresses unchanged | | COMMON | DISKBUF/SCRATCH written in bank 0 read back in bank 1; mailbox written from bank 1 | | NMI | A CIA #2 timer NMI taken in the bank-1 state returns with CR `$4E`, P0 `$1A`, sentinel `$C5`, and the same SP | | RESTORE | After `mmu_shutdown`: CR/RCR/P0/P1/`$0318` and SP equal their entry values | Negative control: forcing P0L to `$00` in `mmu_bank0/1` makes the skeleton report FAIL with code `$000F` (ZP, KCALL, BANK1, NMI).