--- 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`). E04-S01 defines the generated PRG/MAP container and low-memory startup; E04-S02 adds the frame runtime; E05-S01 supplies an explicit-region heap; E05-S02 freezes class descriptors and MAP class records. Generated-program heap placement and banking remain open. ## 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. E04-S01 therefore refuses an existing `NAME.PRG` or `NAME.MAP` with E202; the user erases the old output before rebuilding. Files created by the current run are scratched on abort or failure. ## 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 flow.s labels, branches, calls, and frame prologue/epilogue 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 and descriptor accessors construct.s object construction, deletion, and indirect call 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. ### 2.3 Generated program container (E04-S01) The generated PRG begins with load address `$1C01`, the 12-byte BASIC 7 stub `10 SYS 7181`, and machine entry `$1C0D`. Sections are emitted in ascending address order; a gap before a section is filled with zero bytes. The entry point and each section's start/size are recorded in `NAME.MAP` as CR-terminated text: ```text NOVA128 MAP 1 ENTRY 1C0D SECTION STUB 1C01 000C SECTION START 1C0D 004A ``` `src/runtime/startup.s` supplies the fixed stub and startup bytes. The startup initializes the test mailbox, reports producer `$03` and PASS, and returns to BASIC. The test fixture keeps its sections below `$4000`, where BASIC's mapping still exposes RAM; banking, heap, and software-stack placement are E04-S02. ## 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.0 Container and entry (E04-S01) - Load address `$1C01`; BASIC 7 stub `10 SYS 7181`; entry `$1C0D`. - Sections are contiguous and ascending; gaps are zero-filled. - `NAME.MAP` records `ENTRY` and every `SECTION name start size` in hex. - The E04-S01 startup writes the mailbox with producer `$03`, then PASS, and returns to BASIC. E04-S02 supplies the frame runtime separately; code generation will switch to CR `$0E` and initialize frames when it emits user code (E04-S03). ### 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 (E04-S02) Generated code runs with CR `$0E` (RAM through `$BFFF`) and IRQs masked, then restores the entry CR before returning to BASIC. The software frame stack is bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP` and `FP` use the free zero-page bytes `$FB-$FE`. The compiler's own stack and private zero page are not shared with the generated program. Frame layout relative to `FP`: ```text FP+0..1 previous FP (pushed by frame_enter) FP+2.. arguments, arg0 at the lowest address FP-1.. locals, allocated below FP ``` A caller pushes arguments right-to-left, executes `JSR`, and discards the argument bytes after the return. The callee calls `frame_enter` with the local byte count in A/X, uses locals below `FP`, then calls `frame_leave`. One-byte results use A; two-byte results use A (low) and X (high). The hardware stack still carries `JSR` return addresses and small temporaries, so `frame_enter` also refuses a frame when the 6502 stack pointer falls below `$40`. `frame_enter` is transactional: it checks both bounds before committing, restores `SP` and `FP` on failure, and returns C=1 with `FRAME_ERR_STACK`. The caller can unwind through `frame_leave` without a partial frame. `self`, parameters in declaration order, and direct `JSR` for concrete method calls are unchanged from the ABI baseline. ### 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 ``` E05-S02 freezes the class descriptor as an 8-byte packed record: ```text +0..1 instance size (includes the 2-byte object header) +2..3 destructor address; 0 = none +4 implemented-interface count +5..6 interface-directory pointer; 0 when the count is 0 +7 reserved flags; must be 0 ``` The interface directory is a count-long array of 4-byte entries: a pointer to the interface descriptor and a pointer to its method table in slot order. There is no parent-class pointer or inheritance slot. `src/runtime/objects.s` provides read-only `obj_class`, `obj_size`, `obj_destroy`, and `obj_iface_dir` accessors. ### 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. ### 7.6 Scalar code generation (E04-S03) `src/compiler/codegen.s` emits documented 6502 instructions into a bounded 256-byte buffer. One-byte values are in A; two-byte values are A (low) and X (high). The emitter provides immediate/absolute loads and stores, absolute `ADC`/`SBC`/`CMP` operands, `JSR`, and forward relative branches with checked fixups. Forward branches are limited to +0..+127 bytes; E04-S04 supplies the control-flow and long-branch expansion. Multiplication and signed/unsigned division use `src/runtime/arithmetic.s`. Operands are in `ar_a`/`ar_b`, the result is A/X, and division leaves its remainder in `ar_rem`. Division by zero sets provisional `ar_fault` to `AR_FAULT_DIV0`, returns A=X=0 with C=1, and is mapped to the final runtime fault namespace by E04-S05. The scalar fixture executes the emitted shapes, checks short-circuit evaluation, and compares a reviewed golden byte stream and da65 disassembly. Parser integration into statements and frames remains the next backend step. ### 7.7 Control flow, calls, and returns (E04-S04) `src/compiler/flow.s` supplies the statement-level control transfer above the scalar emitter. It keeps 16 labels and 32 fixups per routine. A label records the byte offset where it was defined; `fl_jmp` emits an absolute `JMP` and is also the back edge of `while` and `for` loops. `fl_branch` takes a branch opcode and a label and always occupies five bytes: ```text short Bxx distance ; target within +0..+127 NOP NOP NOP ; unused JMP slot long Bxx'inv +3 ; inverse of the requested condition JMP target ``` The form is chosen when fixups are patched. A forward branch whose target is behind it is `FL_ERR_RANGE`; loops use `JMP`. Patching the long form inverts the opcode, so a true condition falls through to the `JMP` and a false one skips it. Because every conditional branch is five bytes, `cg_put`'s counting mode produces exactly the same label offsets and routine length as emission without writing memory, which is the sizing half of the two-pass contract. Calls follow section 7.3: `fl_call` emits `JSR` and then `frame_discard` for the argument bytes. `fl_call_byte` and `fl_call_word` save the result on the hardware stack across the discard, and `frame_discard` preserves A/X. A routine returns through `fl_ret` (no result), `fl_ret_byte` (A), or `fl_ret_word` (A/X); the result variants save and restore the result around `frame_leave`. `fl_prologue` emits `frame_enter` for the local byte count and branches to the caller's error label on carry. The x128 fixture builds a nested routine (prologue, `while`, `if`/`else`, `for`, call with argument discard, value return), checks sizing against a counting pass, verifies both branch encodings in memory, and executes it. Parser-to-emitter wiring, method address assignment, and compiler pass 3 are not part of this layer. ### 7.8 Print and runtime faults (E04-S05) `src/runtime/print.s` writes PETSCII through `CHROUT` under the D129 generated-program mapping. It prints a byte, an unsigned word, a signed integer, a boolean (`TRUE`/`FALSE`), a length-address text block, and a carriage return. Decimal output has no leading zeros; only `pr_cr` ends a line. The routines append nothing on their own and do not change CR, RCR, or the P0/P1 pointers, so a runtime message cannot relocate the program. `pr_text` uses a zero-page pointer, so it depends on the system zero page being mapped as in D129. `src/runtime/faults.s` maps the provisional arithmetic fault onto a stable R-code namespace: R001 null, R002 bounds, R003 divide by zero, R004 software stack overflow, and R005 heap exhaustion. `flt_report` writes the code and fault address into the mailbox, sets `MB_ST_FAIL`, and prints `RUNTIME FAULT R00n`. The code and address are written before the status so a monitor never observes a failure with stale fields. The routine returns; unwinding to the runtime entry remains with the frame/exception work. ### 7.9 First source-to-code proof (E04-S05) The `hello.n8` compile fixture closes one source-to-code path without claiming the general backend. Parser pass 2 records a print that consists of one string literal in the bounded `gen_text` buffer, sets `gen_print_count`, and sets `gen_bad` for any other print shape. The fixture then emits the D128 container by hand: a 12-byte BASIC stub section at `$1C01` and a code section at `$1C0D`. The fixture must initialize `out_load` and `out_entry` itself; `output.s` reads them but does not assign them. The emitted body saves the entry CR, selects `$0E`, writes each captured PETSCII byte with `LDA #byte` / `JSR $FFD2`, restores CR, and returns to BASIC. It does not call `startup.s` or the scalar/control-flow emitters. A successful compile produces a 118-byte `HELLO.PRG`; a second x128 session autostarts the extracted PRG and the host checks for `HELLO FROM NOVA128` on the screen. This is deliberately narrower than compiler pass 3: expressions, calls, multiple statements per program, and address assignment for generated code remain open. Pass-2 code growth also made some parser branches exceed ca65's +/-128 range, so the literal path uses a near branch and `JMP` trampoline. ### 7.10 Member emission (E05-S04) `src/compiler/members.s` emits the member half of a method body above the scalar emitter and the flow helpers. It takes field offsets from the E03-S05 layout unchanged and assumes the D129 call ABI, where a method receives self in A/X. A generated method body begins with `mb_self_store`, which records the incoming self in zero-page `MB_SELF` (`$F9-$FA`). `self.field` then needs one indexed-indirect access: `mb_ld_field` and `mb_st_field` emit `LDY #offset` and `LDA (MB_SELF),Y` or `STA (MB_SELF),Y`, adding the +1 byte for a word field. A word store moves the high byte out of X with `TXA` before storing, because the 6502 has no `STX (zp),Y`. `mb_point` loads a two-byte object reference into `MB_SELF`, so the same emission serves `x.field` and a call on a receiver other than self. A concrete call is `mb_self_load` (A/X = self), `JSR target`, then `frame_discard` for the argument bytes: the callee enters its normal frame and reads arguments at `FP+2`, and a byte or word result survives the discard. That is the direct-JSR path D107 keeps for concrete methods; interface dispatch remains E06. `codegen.s` gained the forms this needs: `cg_ldy_imm`, `cg_lda_zp`, `cg_ldx_zp`, `cg_sta_zp`, `cg_stx_zp`, and `cg_lda_zpy`, `cg_ldx_zpy`, `cg_sta_zpy`. `MB_SELF` sits directly below the frame ABI and `members.s` asserts at link time that it does not overlap `FRAME_SP` or `FRAME_FP`. The fixture generates a field writer and byte and word readers, runs them against two objects of one layout, then generates callers that push arguments and call real methods. It checks field offsets, independent state, the argument discard, and a word result at the call site. Private member access is rejected before code generation: the parser reports `ACC_PRIVATE_MEMBER` (E120) for a private field or method reached outside its class, so this layer never sees one. ### 7.11 Statement emission (E04-S06, in progress) `src/compiler/emit.s` is the first emitter driven by the parser itself. Pass 2 parses statements for type checking; when `em_on` is set, the same parse also emits bytes into the shared `codegen.s` buffer. `parse_file` resets the emitter and the buffer at the start of pass 2, and pass 1 assigns the same frame slots without emitting, so the two passes agree by construction. Parameters take `FP+2` upward in declaration order and locals `FP-1` downward, with one byte for boolean/byte/char and two for integer/word. A slot is not addressed with `LDY #offset` / `(FRAME_FP),Y`: indexed-indirect addressing adds Y to the pointer, so a negative offset would address a page above `FP`. `em_point_slot` instead emits `EM_LP = FRAME_FP + offset` (low byte plus carry, high byte plus the offset's sign byte), leaves Y = 0, and the load or store uses `(EM_LP),Y`. The store keeps its value on the hardware stack across that sequence. Byte `+` and `-` push the left operand, evaluate the right, then combine through `EM_TMP`. `print` emits through `em_pr_text`, `em_pr_byte`, and `em_pr_cr`, which the caller fills; each call goes through a `JMP (cell)` trampoline, so the test records what the program asked to print instead of depending on CHROUT's mapping. A `print` that captured a string literal passes `gen_text` and its length; otherwise it passes the byte in A. The `statements.n8` fixture parses two locals, three assignments, an addition, and two prints, then JSRs the 138 emitted bytes in place. It checks `a = 10`, `b = 7`, and the recorded stream `SUM`, CR, `$FF`, `$0A`, CR. Control flow, calls, field stores, word values, arrays, the frame prologue, and reporting `em_bad` as a diagnostic remain open; E04-S07 owns sizing and pass-3 addresses. ## 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. ### 8.1 Free-list heap (E05-S01) `src/runtime/heap.s` supplies the allocator; `src/common/heap.inc` fixes the block/API contract. The caller supplies the region, so this does not freeze the still-open generated-program map. In the E05-S01 fixture the region is bank-0 `$4000-$4FFF` under CR `$0E`. Each block has a four-byte header: ```text +0..1 total block size; bit 15 set while allocated +2..3 next free block while free; $A55A while allocated +4.. payload ``` Blocks are two-byte aligned and the minimum block is six bytes. Free blocks are kept in ascending address order. `heap_alloc` takes a payload byte count in A/X, walks first-fit, splits a remainder of at least six bytes, zeroes the payload, and returns its pointer in A/X. `heap_free` takes a payload pointer and inserts/coalesces with adjacent free blocks. A zero pointer is a no-op. Range, size, used-magic, and double-free checks are always on. The allocator returns `HEAP_ERR_*` rather than printing; generated `new` maps allocation failure to R005 when E05-S03 wires construction. The stress fixture frees in different orders and requires the initial free capacity to return. Heap placement for generated programs and banked heaps remain open. The fixture keeps code below `$4000` and uses only its explicit 4K region. ### 8.2 Class descriptors and objects (E05-S02) `src/common/class.inc` fixes the object/descriptor ABI and `objects.s` reads it without allocating or changing object state. The class fixture covers a class with no fields and no destructor, and a class with an optional destructor and one interface-directory entry. `output.s` accepts class records through `out_class` and appends deterministic MAP lines after the section records: ```text CLASS name size destroy interface-count interface-directory ``` All numeric fields are four hex digits except the interface count, which is two. Empty classes and no destructor are written as `0000`; records keep the caller's order. The class test reads the emitted MAP back and compares it with the descriptor's actual destructor and directory addresses. ### 8.3 Construction and deletion (E05-S03) `src/runtime/construct.s` implements `new` and `delete` on top of the heap and descriptor layers, so both can be tested before parser-to-codegen exists. `obj_new` takes A/X = class descriptor plus `obj_init_ptr` chosen by the caller (0 = no constructor). It reads the descriptor's instance size and refuses zero with C=1/`OBJ_ERR_BAD`, allocates exactly that many bytes, and maps `HEAP_ERR_OOM` to C=1/`OBJ_ERR_HEAP` with A/X=0. Only after a successful allocation does it write the descriptor pointer at object+0 and call the init entry once with A/X = self; the payload is already zeroed by the heap, so fields start at zero. Constructor arguments are not copied: they stay on the caller's software frame stack, init enters its normal frame and reads them from `FP+2` right-to-left, and the caller discards them with `frame_discard` after `obj_new` returns. A failed allocation never calls init. `obj_delete` takes A/X = object. Null returns C=0 without touching the heap or the destructor. Otherwise it reads the descriptor's optional destructor, calls it once with A/X = self and no arguments, then frees the complete block with `heap_free`, so an instance returns to the coalescing free list in one piece. Both calls use `obj_call`, the synthetic-return trampoline. The entry address travels in zero-page `co_vec` rather than A/X, because A/X already carry the callee's self pointer; `obj_call` saves them, pushes a two-byte return address, restores them, and ends with `JMP (co_vec)`. The callee's `RTS` lands on the trampoline's own `RTS`, which returns to the original `JSR`. `co_vec` must not straddle a page (link-time assertion) because of the 6502 indirect-JMP bug, and every such call costs two extra hardware-stack bytes. `construct.s` reports through `obj_error` and the carry flag rather than printing, so generated code still owns the R005 fault report for `new`. The fixture constructs a five-byte class whose `init(value as word, tag as byte)` stores both fields beside a destructor, then checks the stored header, zeroed fields, the constructor's arguments and self, that `delete null` calls nothing, that deletion calls the destructor once with the fields intact and restores the full free capacity, that a `$FFFF` instance fails without calling init, and that a class without a destructor still frees completely. ## 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