--- type: compiler-runtime-architecture source-path: expert design plan, repository Vim notes, C128 reference architecture ingested-at: 2026-11-07 version: 0.1-draft --- # Nova128 Compiler and Runtime Architecture Status: approved version 1 baseline. The compiler memory map is frozen by E01-S05 (section 4.4, `docs/memory-map.md`). Generated-program addresses are still deliberately absent until the E04 stories validate them. ## 1. System Boundary The deliverable compiler is an 8502 program that runs on a Commodore 128 in native mode. Host-side tools may: - assemble and link the compiler with `ca65` and `ld65`; - copy files into or out of D81 images with `c1541`; - launch and control VICE; - compare deterministic test outputs. Host tools must not perform Nova128 parsing, type checking, or code generation in the release workflow. The C128 compiler performs those functions. Generated version 1 programs target C128 native 8502 mode. They do not target C64 mode or the Z80. ## 2. Toolchain and Artifacts ### 2.1 Authoritative source - Compiler/runtime assembly: ASCII ca65 source under `src/` - Linker layouts: ld65 configuration under `cfg/` - Language examples: readable ASCII mirrors under `examples/` - PETSCII/D81 forms: generated under `build/` ### 2.2 Compiler artifacts | Artifact | Purpose | |---|---| | `N128C.PRG` | On-machine compiler executable | | `NAME.N8` | PETSCII SEQ input source | | `NAME.PRG` | Generated application | | `NAME.ERR` | Complete stable-code diagnostics | | `NAME.MAP` | Symbols, addresses, class layouts, interface slots, and section sizes | Output replacement must be explicit. The compiler must not silently overwrite an existing PRG after a failed compilation. ## 3. Repository Module Plan ```text src/ compiler/ main.s command loop and phase orchestration mmu.s all MMU transitions and bank-safe copies diskio.s KERNAL file and command-channel wrappers source.s buffered source stream and positions lexer.s PETSCII normalization and tokenization parser.s recursive-descent declarations/statements expr.s precedence expression parser names.s interning and identifier comparison symbols.s scopes, symbols, and lookups types.s type representation and compatibility classes.s field/method layout and access checks interfaces.s conformance and interface slot assignment sizing.s dry-run code sizing and address assignment codegen.s documented 8502 instruction emission output.s PRG, ERR, and MAP writers diagnostics.s stable error catalog and recovery runtime/ startup.s generated-program entry and initialization frames.s software argument/local-frame support objects.s object construction and destruction interfaces.s interface conversion and indirect call trampoline heap.s deterministic allocator and free list arithmetic.s non-inline arithmetic helpers print.s scalar and literal output faults.s null, bounds, divide, stack, and heap failures common/ c128.inc hardware and KERNAL symbols formats.inc compiler record and output constants tokens.inc token numbers errors.inc stable diagnostic numbers ``` Files are introduced by stories; this tree is a target, not permission to add empty placeholders. ## 4. C128 Memory Strategy The design uses the two 64K RAM banks and a verified common-RAM window. ### 4.1 Compiler bank roles - **Bank 0:** resident compiler code, phase controller, lexer/parser state, disk/KERNAL wrappers, diagnostics, and small buffers. - **Bank 1:** interned names, symbols, class records, interface records, method signatures, type records, and pass workspaces. - **Common RAM:** MMU transition code, hardware stack, software-stack metadata, disk transfer buffer, and state required while either bank is selected. No module other than `mmu.s` directly changes the MMU. Banked data is accessed through handles or explicit copy routines; ordinary pointers must not escape the bank in which they are valid. ### 4.2 Memory-map freeze gate Story E01-S02 must prove: - chosen MMU configurations; - common-memory size and placement; - page-zero and page-one behavior; - safe execution of the bank-switch trampoline; - KERNAL entry and return mappings; - interrupt behavior while a non-default bank is active. Only then may linker addresses and zero-page allocations become normative. E01-S02 evidence is in `docs/mmu-probe.md`. In VICE x128 3.10, distinct banks, 8K bottom+top common RAM, bank-switched `JSR`/`RTS`, and full MMU restoration pass. Page pointers aimed at bank 1 while bottom common is active resolve to bank 0 at the translated page, so Nova128 keeps P0/P1 in bank 0. Non-default mappings run with IRQs masked; an IRQ path under bank 1 is not yet proven. E01-S03 evidence is in `docs/kernal-wrapper.md`. A common-RAM wrapper enters the KERNAL in BASIC's mapping and restores the exact compiler MMU state. From bank 0 and bank 1 compiler mappings, CHROUT, a drive-8 status read, and a device-not-present failure all work, with A/X/Y/C/ST preserved and no channels left open. ### 4.4 Frozen compiler memory map (E01-S05) The gate above is closed for the compiler. `cfg/compiler.cfg` is authoritative and `docs/memory-map.md` explains it and gives the evidence. In summary: - **Compiler states:** CR `$0E` (bank 0) or `$4E` (bank 1), 8K bottom common RAM, P0 relocated to bank 0 page `$1A`, IRQs masked. - **Bank 0:** COMMON `$1300-$18FF` (copied at init), DISKBUF `$1900`, private zero page `$1A00`, SCRATCH `$1B00-$1BEF`, MAILBOX `$1BF0-$1BFF`, MAIN `$1C0D-$B7FF` (STARTUP first), SWSTACK `$B800-$BFFF`. - **Bank 1:** source cache `$2000-$6AFF` (D118), compiler tables `$6B00-$BFFF` (21,760 bytes). - **Zero page:** the compiler owns logical `$02-$FF` via P0; KERNAL calls run on the system zero page. - **NMI:** guarded by `mmu_nmi_guard` through `$0318`; RESTORE is ignored during compilation. - **Test mailbox:** 16 bytes at `$1BF0`, shared by platform tests, the compiler, and generated programs. The generated-program memory map remains open (E04-S01/E04-S02). ### 4.3 Display and clock Compiler text uses KERNAL-compatible character output on the display already selected by the user; it does not force 80-column mode. E01-S04 verified the editor's `$00D7` mode flag in x128: 40-column uses a deterministic 1 MHz fallback, while 80-column may set `$D030` bit 0 for 2 MHz KERNAL output. The speed bit is cleared before returning to BASIC, and any uncertain mode falls back to 1 MHz. Direct VDC screen memory is display-only and is not compiler semantic storage. See `docs/platform-display-clock.md` for evidence. ## 5. Compiler Passes The compiler reparses a buffered/streamed source rather than retaining a large pointer-rich abstract syntax tree. ### Pass 1 — declarations and layout - tokenize the complete source; - intern names; - collect interfaces, classes, fields, methods, parameters, and program data; - validate duplicate declarations and top-level structure; - assign field offsets; - assign a stable slot order within every interface; - build class-to-interface conformance records. ### Pass 2 — semantics and sizing - parse method/program bodies; - resolve names and access control; - check types, returns, and interface conversions; - perform definite-local-initialization checks that fit the compiler budget; - dry-run code generation to determine method sizes and addresses; - calculate literal, descriptor, interface-table, and runtime sections. ### Pass 3 — emission - repeat semantic parsing deterministically; - emit documented 8502 instructions; - emit literals, class descriptors, and interface method tables; - resolve branches and addresses against Pass 2 assignments; - append/link required runtime sections; - write PRG and MAP output; - delete or invalidate partial output on failure. Repeated passes re-read the source, not the disk. `src_load` (`source.s`) reads the file once into the bank-1 cache and enforces the editor-profile limits. Each pass calls `src_rewind`, then `src_next_line` delivers one logical line at a time into a common-RAM buffer (D118, E02-S01). ## 6. Compiler Data Records Final byte layouts are story outputs, but records require these logical fields: - **Interned name:** hash, length, original spelling location, normalized bytes - **Symbol:** name ID, kind, type ID, scope ID, flags, value/address - **Type:** kind, size, referenced class/interface/element, array length - **Class:** name ID, instance size, field range, method range, interface range - **Field:** owner class, name ID, type ID, access, byte offset - **Method:** owner, name ID, signature, access, code address, code size - **Interface:** name ID, ordered signature range - **Implementation:** class ID, interface ID, emitted table address - **Diagnostic:** code, file position, arguments Use compact fixed-width records where practical. Every table has an explicit capacity and produces a resource diagnostic rather than corrupting adjacent memory. ## 7. Generated Program ABI ### 7.1 Scalar representation - Multi-byte values are little-endian. - `boolean`, `byte`, and `char` occupy one byte. - `integer`, `word`, concrete references, and addresses occupy two bytes. - Interface references occupy four bytes. ### 7.2 Return convention - One-byte result: accumulator A - Two-byte result: A is low byte, X is high byte - Four-byte interface result: runtime-defined result area referenced through a reserved ABI pointer; exact zero-page location is frozen after E01 - No result: A and X are caller-clobbered The final register-clobber table is part of story E02-S02. ### 7.3 Frames and calls - The hardware stack carries return addresses and small runtime temporaries. - Arguments, locals, and expression spills use a software-managed stack/frame. - `self` is held in a reserved two-byte ABI pointer during an instance call. - Parameters are laid out in declaration order using their ABI sizes. - Concrete method calls are statically resolved to a direct `JSR`. - The runtime checks stack bounds before frame growth in debug builds and at defined safety points in release builds. ### 7.4 Object layout Every heap object begins with a two-byte class-descriptor pointer, followed by fields in declaration order with no hidden alignment padding: ```text +0 class descriptor pointer (2) +2 first declared field ... remaining fields ``` The class descriptor contains the instance size, optional destructor address, and a directory of implemented interfaces. Its final packed format is frozen by E05 and E06 stories. ### 7.5 Interface representation and dispatch An interface value is a fat reference: ```text +0 object pointer (2) +2 interface method-table pointer (2) ``` Each interface defines a slot order from its source declaration. For every implemented interface, a class emits a table of two-byte method addresses in that order. Converting a concrete reference to an interface reference: 1. copies the object pointer; 2. uses compile-time-known class/interface metadata to select the class's implementation table; 3. stores the table pointer in the interface value. An interface call: 1. checks the object pointer for null; 2. installs the object pointer as `self`; 3. loads the two-byte target from the known interface slot; 4. invokes the runtime's indirect-call trampoline. The 8502 has no `JSR (address)` instruction, so the trampoline must establish a synthetic return path safely. It is tested independently before interface code generation relies on it. `delete` through an interface uses the object pointer and its class descriptor, not the interface table, to find the optional destructor and object size. ## 8. Heap and Ownership - The generated-program heap initially resides in the same RAM bank as program data; banked heaps are deferred. - `new` requests the class instance size, zeroes the allocation, stores the descriptor pointer, and invokes `init`. - `delete` invokes `destroy` at most once for that call and returns the entire block to a coalescing free list. - Allocation failure and corrupt/double-free detection use runtime fault codes. - Reference copying does not change an ownership count. - Aliases can outlive a deletion; that is a program error. Debug poisoning may help diagnose it but is not a correctness mechanism. ## 9. KERNAL and Disk I/O The compiler uses documented KERNAL entry points through `diskio.s` wrappers. Each wrapper: 1. records compiler mapping state; 2. installs the verified KERNAL-compatible mapping; 3. performs the operation and reads status; 4. restores compiler mapping state; 5. reports errors without leaving files or channels open. The command channel must be opened before source/output files when status is needed and closed last, matching the proven editor workflow. ## 10. Vim Integration The existing Vim clone is treated as an external producer/consumer of SEQ files. Version 1 does not link the editor and compiler in memory. The development image workflow is strictly sequential: 1. copy `../vim/work.d81` to `build/newlang-dev.d81`; 2. inject `N128C.PRG`, examples, and fixtures; 3. launch `x64sc` and autostart `VIM` for editing; 4. close the process and wait for image flush; 5. launch `x128` and autostart `N128C.PRG` for compilation; 6. close the compiler session before inspecting or modifying the image. Scripts must refuse obvious concurrent use where detectable. Tests create a fresh image per run. ## 11. Diagnostics and Faults Compile-time diagnostics use stable IDs such as `E014`. Runtime faults use a separate stable namespace such as `R003`. Numeric assignments live in `src/common/errors.inc` once story E02-S03 freezes the catalog. Compiler errors include source name, line, column, and compact message. Runtime faults always include a stable fault code and the best available program address that can be resolved through the MAP file. Rich stack traces are deferred. ## 12. Verification Architecture Testing is layered: 1. Host checks for deterministic converters, image preparation, and byte-level expected files. 2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface conformance, and diagnostics. 3. Golden PRG bytes and disassembly for small generated routines. 4. End-to-end x128 runs that write a result signature to the fixed test mailbox at `$1BF0` (`src/common/mailbox.inc`, `docs/memory-map.md`). 5. Monitor scripts with MMU, stack, heap, and dispatch break/watch points. 6. Editor round-trip: Vim saves source, C128 compiler consumes it without host conversion, and Vim reopens unchanged source. 7. Physical C128 smoke testing when hardware is available. Each emulator test has a cycle/time limit and captures enough output to explain failure without relying only on a screenshot. ## 13. Principal Risks - Incorrect MMU assumptions corrupt compiler state or KERNAL execution. - Interface fat references increase local/array memory use. - The indirect-call trampoline can corrupt the hardware stack if designed or tested casually. - Repeated parsing can be slow on real disks. - Cross-bank raw pointers can create intermittent corruption. - Feature growth can exceed the on-machine compiler's memory budget. - Two VICE processes can overwrite the same D81 image. The story order makes each high-risk mechanism prove itself before downstream features depend on it. ## Open Questions - Generated-program memory map (the compiler map is frozen in section 4.4) - Final interface-result convention - Compiler table capacities and their user-visible resource limits - Physical-hardware acceptance requirements