# 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. 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` | | 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. ## 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).