--- 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. E03-S01 implements the declaration-level part of this as `parse_file` (`parser.s`): pass 1 parses declarations and method headers, records top-level types, tracks block structure, and reports syntax, ordering, and duplicate errors; pass 2 replays the source and reports type references that name no declared interface or class. E03-S02 adds members and scopes. Pass 1 records every field and method signature (access, type, parameter types) in bank-1 member and parameter tables, rejects duplicate fields or methods per type (separate namespaces), `init`/`destroy` shapes (D122), and `var` after a method statement or inside a block. Each method signature and the program open a fresh local scope whose locals carry their declared types. Pass 2 scans statement lines token by token: an unqualified name must be a local, parameter, or (in a method) a field of the enclosing class (D123); `expr.member` follows the left side's type through locals, fields, `self`, `new`, call results, and indexing, and reports unknown members and private members used outside their class. E03-S03 replaced that scan with a real statement and expression parser that runs in both passes. `parse_statement` handles every spec 7 form (assignment, call, if/else, while, for/to/step, return, print, delete) and requires the line to end after it; a syntax error is reported in pass 1, unwinds to the statement through a saved stack pointer, and skips the rest of the line, so later lines still parse. Expressions use precedence climbing over spec 8's levels with left-associative binary operators and right-nesting unary `not` and `-`; postfix member access, calls, and indexing bind tightest. Name, member, and access lookups from E03-S02 run from the parser in pass 2. Each expression level is counted, and more than 16 is E225, which bounds hardware stack use (the deepest fixture leaves SP at `$A7`). For tests, pass 1 can record every expression in postfix order (`ex_trace_on`). Operator and E03-S04 adds `types.s`: every expression carries a type in pass 2, and `ty_assign`, `ty_binary`, and `ty_unary` apply spec 3.4 and 8 (D124-D127). Assignments, call arguments and results, returns, conditions, `for`, `print`, `delete`, indexing, and member chains are checked; a method that declares a result must end in `return` (an `if` only when both branches do), reported at `end` as E150. Numeric literals are typed by context and folded (E084/E086); arrays are never whole values (E088). E03-S05 computes each class's instance size and field offsets after the declaration pass, once every forward type reference has a kind. Fields keep declaration order after the two-byte descriptor pointer with no padding; ABI sizes are 1 for boolean/byte/char, 2 for integer/word/class reference, 4 for an interface reference, and element size times length for an array. A class whose instance would exceed 65535 bytes reports E096 at its declaration line. E03-S06 adds `interfaces.s`. After the declaration pass it numbers every interface method in source order (`C_MEM_SLOT`), giving later dispatch tables a stable slot order, and checks each class against every interface in its implements list. A satisfying method is public and has the same parameter count, parameter types, and return type; one method may satisfy identical requirements from several interfaces. Failures report at the class declaration line, column 1: E130 for a missing or private method, E131 for a signature mismatch, E132 when a same-named requirement of another implemented interface conflicts with the class method's signature, and E133 when an implements entry names a class or program rather than an interface. An implements entry that names no declared type is left to pass 2's unknown-name diagnostic. ### 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`. The compile-time catalog is `src/common/diagcat.inc` (one line per diagnostic, numbered by category range); `errors.inc` turns it into `E_*` constants and `diagnostics.s` into messages. `docs/diagnostics.md` is generated from it (E02-S04). `diagnostics.s` prints a two-line screen form (header, message clipped to 39 characters) and appends one full line per diagnostic to `NAME.ERR`. The ERR file is scratched when compilation starts and created only on the first diagnostic. After 20 errors the next is replaced by E221 and the caller must stop. If `NAME.ERR` cannot be written, E201 is shown once and reporting continues on screen. 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