--- type: project-decisions source-path: user conversation and repository inspection ingested-at: 2026-11-07 --- # Project Decisions This file prevents later agents from silently changing the approved product. `Accepted` means the user explicitly requested, confirmed, or approved the documented default. `Proposed` means a later change still needs approval. ## Accepted | ID | Decision | Consequence | |---|---|---| | D001 | Create an object-oriented language for the Commodore 128. | C128 behavior and limits define the platform contract. | | D002 | Write the compiler in assembly. | Compiler implementation uses documented 8502/6502 instructions and ca65 source. | | D003 | Source creation must work with the repository's new Vim clone. | The compiler accepts the editor's PETSCII SEQ output directly. | | D004 | Put project programs and supporting material under `newLang/`. | Existing `vim/` and reference assets remain external inputs. | | D005 | Plan and obtain approval before implementation. | The user approved continuation on 2026-11-07; implementation stories may now be claimed in dependency order. | | D006 | Support interfaces. | Interfaces and dynamic interface dispatch are version 1 work, not an optional extension. | | D007 | Create a specification and epics/stories suitable for other agents. | Work is divided into claimable, testable stories with durable context. | | D100 | Use `Nova128` as the language name and `.n8` as the source suffix. | Short, recognizable, and compatible with 16-character disk filenames. | | D101 | Run the compiler in C128 native 8502 mode. | Gives access to both RAM banks, the MMU, 80-column facilities, and 2 MHz operation where safe. | | D102 | Use host `ca65`/`ld65` only to build the on-machine compiler. | The resulting compiler still runs entirely on the C128. | | D103 | Keep the existing Vim binary in C64 mode for version 1. | A native C128 editor port is a separate large project. | | D104 | Exchange source through a copied D81 image. | It works with the current editor without inventing an editor API. | | D105 | Use explicit `new`/`delete` and a deterministic free-list heap. | Avoids the size and timing cost of garbage collection. | | D106 | Permit multiple interfaces but no class inheritance in version 1. | Delivers polymorphism while limiting layout and dispatch complexity. | | D107 | Use statically dispatched concrete method calls and dynamically dispatched interface calls. | Keeps ordinary calls fast while preserving interface polymorphism. | | D108 | Represent interface values as a four-byte pair: object pointer plus interface-table pointer. | Makes interface invocation constant-time without searching class metadata on every call. | | D109 | Compile one source file into one PRG in version 1. | Separate compilation and object linking can follow a proven compiler. | | D110 | Use KERNAL-compatible output so the compiler can operate on 40- or 80-column displays. | Avoids coupling the first compiler UI directly to one video chip. | | D111 | Physical C128 testing is P2 validation, not a version 1 release blocker when hardware is unavailable. | VICE x128 remains the mandatory automated acceptance environment. | | D112 | Compiler diagnostics use the active C128 display and do not force 80-column mode. | The compiler remains usable with either display; safe clock acceleration is opportunistic. | | D113 | Version 1 integer overflow always wraps in the destination representation. | Debug overflow trapping is deferred, keeping semantics identical between builds. | | D114 | Version 1 has no interface-to-class down-cast operation. | Programs express polymorphism through interfaces without runtime type reflection. | | D115 | Character literals use single quotes and contain exactly one PETSCII character or supported escape. | The `char` type has a direct, unambiguous literal form while `//` remains the comment syntax. | | D116 | Runtime faults report a stable code and the best available map-resolvable program address. | Rich stack traces are deferred; faults remain diagnosable within the runtime budget. | | D117 | Freeze the compiler memory map in `cfg/compiler.cfg`: 8K bottom common RAM, COMMON at `$1300`, private zero page at bank 0 page `$1A` via P0, bank-1 tables at `$2000-$BFFF`, IRQs masked and NMIs guarded in compiler states, test mailbox at `$1BF0`. | Internal contract backed by E01-S05 x128 evidence (`docs/memory-map.md`); no user-visible behavior except that RESTORE is ignored while compiling. Generated-program maps remain open. | | D118 | The compiler loads the whole source file once into a bank-1 cache (`$2000-$6AFF`) and replays it for every pass, instead of re-reading the disk. | Rewind is exact by construction and avoids reading a slow disk three times. Compiler tables get the remaining 21,760 bytes of bank 1 (`$6B00-$BFFF`). Internal; no user-visible behavior. | | D156 | Emit the calling convention from the parser (E04-S08): arguments are evaluated left to right and staged on the hardware stack, then popped in reverse through `frame_push_byte`/`frame_push_word` so arg0 lands at `FP+2`; a 12-byte prologue (`LDA`/`LDX` locals, `JSR frame_enter`, `BCS fail`) is reserved at each body when frames are requested and its local count is patched after the body; `return` goes through `frame_leave` with a 0/1/2-byte result preserved. Frames are opt-in through `parse_file_framed`, so existing fixtures that do not supply frame entry points emit unchanged code. | The generated program now uses the D129 frame ABI for real calls instead of only in hand-written runtime fixtures. The prologue reserves fixed bytes and patches the count, because locals are known only after the body is parsed; the sizing pass sees the same 12 bytes, so label offsets and lengths still agree. A refused frame branches past the body's normal exit and returns without `frame_leave`. Word arguments and byte/word results are covered; interface arguments and four-byte returns still belong to E06-S07. | ## Proposed | ID | Proposal | Consequence | |---|---|---| | D119 | The string and character escapes `\n` and `\r` both produce PETSCII `$0D`. | Commodore output has one line-end character, so both escapes start a new line. Implemented as `ESC_N_VALUE`/`ESC_R_VALUE` in `src/common/tokens.inc`; changing them later affects only those constants and the lexer golden files. | | D120 | A program may use at most 255 distinct identifiers (names are 1-byte IDs). | Covers well beyond a 599-line program's typical vocabulary at 5,164 bytes of bank 1. Exceeding it is a resource diagnostic; raising it means 2-byte name IDs in every later record. | | D121 | A method name may be any identifier or reserved word, as in spec 5.1's `method print()`. | Spec 5.1 and the accepted fixtures declare `method print()` although `print` is a statement keyword. The parser accepts a keyword after `method`; E03-S03 must accept the same member names after `.`. | | D122 | `init` returns no value (`as type` is E151), like `destroy`. | Spec 6.3 says `new Class(args)` yields the object, so an `init` result could never be observed. Spec 6.3 already gives `destroy` no parameters and no result (E152, E151). Implemented by E03-S02. | | D123 | Inside a method an unqualified name is a parameter or local, else a field of the enclosing class; methods are always called through an expression such as `self.tick()`. | Spec 9 lets a local "shadow a field", which implies fields are in scope unqualified, but no fixture used one. Spec 7 defines calls only as `expression.method(arguments)`, so a bare `tick()` is an unknown name. Implemented by E03-S02. | | D124 | Numeric literals are constants that adopt the type of the place or operand they meet when the value fits, and literal-only arithmetic folds at compile time. `and`/`or` take booleans; `+ - * /` take byte, word, or integer; `< <= > >=` take numbers or chars; `= <>` also take booleans and references (identity). `byte` widens to `word`/`integer`, but `word` and `integer` do not mix. | Spec 3.4 names the conversions but not literal types or the operator table. Out-of-range folded results are E084, division by zero E086, and a constant that does not fit E085. Implemented by `types.s`; changing it changes only that module and the semantic goldens. | | D125 | An array index may be an integer or a value assignable to word; a constant index is also checked against the declared length. | Spec 3.3 gives arrays but not the index type. Byte indexes widen; an out-of-range constant is E089 at compile time. Implemented by `check_index` in `parser.s`. | | D126 | Arrays are fields or locals only: never parameters, results, or whole values. A bare array is E088, and a parameter or result declaring one is E094. | Keeps array storage and copying out of the ABI (D108); indexing is the only way to read or write one. Implemented by `no_array_sig` and `ty_assign`. | | D127 | `return` with a value in a program is E151. A method that declares a result must end every path in `return`; an `if` satisfies this only when both branches do, a `while` never does, and a missing return is reported at `end` as E150. | Spec 7 permits `return` but does not say what a program return means; a program has no caller or result. Deterministic and observable, and lets E04 emit a single exit path. Implemented by `close_block` and `set_ret`. | | D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | | D129 | Generated code executes with CR `$0E` and IRQs masked, then restores the entry CR. The software stack is bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP`/`FP` use `$FB-$FE`. A frame stores the previous FP at `FP`, arguments from `FP+2`, and locals below `FP`; callers push arguments right-to-left and discard them after return. Results are A or A/X. `frame_enter` reserves the old FP and locals transactionally, refuses when the 6502 stack pointer is below `$40`, and returns C=1/`FRAME_ERR_STACK` with `SP`/`FP` restored on overflow. | Makes recursion and nested calls testable without consuming BASIC's zero page or the compiler's private stack. The $40 hardware-stack guard prevents a JAM before the software bound, and the transactional failure lets callers unwind cleanly. The 2 KB software stack, guard threshold, and mapping install are provisional until codegen and later runtime stories. Implemented by `frames.s` and `frames.inc`. | | D130 | Scalar codegen emits documented 6502 instructions into a bounded 256-byte buffer. Values are A (one byte) or A/X low/high (two bytes); assignments use absolute stores, comparisons produce a 0/1 byte, and forward branches use checked fixups. Multiplication and signed/unsigned division call runtime helpers with operands in `ar_a`/`ar_b`; results are A/X, division remainder is `ar_rem`, and division by zero sets provisional `ar_fault=AR_FAULT_DIV0`, A=X=0, C=1. E04-S04 owns parser-to-emitter integration, control flow, and long-branch expansion. | Gives the backend a small, directly testable instruction layer and a deterministic golden stream before the control-flow shell exists. The 256-byte buffer and forward-only branches are deliberate limits; E04-S05 maps the provisional arithmetic fault to the final R-code namespace. Implemented by `codegen.s`, `codegen.inc`, `arithmetic.s`, and `arithmetic.inc`. | | D131 | Control-flow emission (E04-S04) uses `src/compiler/flow.s`: label ids 0-15, absolute `JMP` for back edges, and a fixed five-byte conditional branch. A target within +0..+127 is patched as the direct branch plus three `NOP`s; a farther target is patched as the inverted opcode, `+3`, and `JMP target`. `cg_put` has a counting mode so a sizing pass and an emitting pass make identical label offsets and lengths. Calls emit `JSR` then `frame_discard`; `fl_call_byte`/`fl_call_word` save A (or A/X) across the discard, and `frame_discard` itself now preserves A/X. `fl_ret_byte`/`fl_ret_word` preserve results across `frame_leave`. | Gives the backend a bounded, directly testable control-flow layer before parser and pass-2 address assignment exist. The fixed five-byte branch avoids size drift between passes by construction. Parser/statement integration, `sizing.s`, and compiler pass 3 remain open. Implemented by `flow.s`, `flow.inc`, and `codegen.s`. | | D132 | Generated-program text output calls KERNAL `CHROUT` directly under the D129 mapping (CR `$0E`, system zero page, code below `$4000`). `pr_byte`/`pr_word`/`pr_int` print minimal decimal (no leading zeros, `-` for negatives), `pr_bool` prints `TRUE`/`FALSE`, and only `pr_cr` ends a line. Runtime faults use stable R001 null, R002 bounds, R003 divide, R004 stack, and R005 heap; `flt_report` writes code and address, then `MB_ST_FAIL`, prints `RUNTIME FAULT R00n`, and returns. The print and fault routines never change CR, RCR, or P0/P1. | Implements the output and fault half of E04-S05 without a second MMU wrapper: generated code already runs in the D129 mapping, so direct KERNAL calls are safe and the fixture proves CR/RCR survive every message. The `hello.n8` path is now proven by a fixture-only pass-2 capture (architecture 7.9); fault unwinding, general parser-to-codegen integration, and compiler pass 3 remain open. Output is verified in x128's VIC-II display only. Implemented by `print.s`, `faults.s`, and the two `.inc` files. | | D133 | The E05-S01 heap uses four-byte headers: a two-byte total size with bit 15 set while allocated, and a two-byte next pointer while free or `$A55A` while allocated. Payload follows at header+4, blocks are two-byte aligned, and the minimum block is six bytes. `heap_init` takes A/X = page-aligned base and Y = pages 1..127; `heap_alloc` takes payload bytes, zeroes the payload, returns it or `HEAP_ERR_OOM`, and splits first-fit free blocks; `heap_free` validates range/size/magic and coalesces in address order. Validation is always on. The E05-S01 fixture uses bank-0 `$4000-$4FFF` under CR `$0E`; generated-program placement and banked heaps remain open. | Makes allocation deterministic and testable before class layout chooses a production heap base. The allocator does not print or report faults itself; E05-S03 maps `HEAP_ERR_OOM` to R005 when `new` is wired. The 15-bit size limit and page-granular region avoid a separate size argument while keeping the block header small. Implemented by `heap.s`, `heap.inc`, and the heap fixture. | | D135 | E05-S03 construction and deletion live in `src/runtime/construct.s` above the heap and descriptor layers. `obj_new` takes A/X = descriptor, reads the instance size (zero is `OBJ_ERR_BAD`), allocates it, writes the descriptor pointer at object+0, then calls the caller-selected `obj_init_ptr` entry once with A/X = self and the constructor arguments still on the software frame stack at `FP+2`; the caller discards them with `frame_discard`. Allocation failure returns C=1 with `OBJ_ERR_HEAP` and never calls `init`. `obj_delete` takes A/X = object: null is a successful no-op, otherwise the descriptor's optional destructor runs once with A/X = self and the complete block goes back through `heap_free`. Both calls use `obj_call`, which takes its entry from zero-page `co_vec`, preserves A/X for `self`, pushes one synthetic return address, and ends with `JMP (co_vec)`; `co_vec` must not straddle a page. | Gives generated `new`/`delete` a testable runtime contract without freezing heap placement or the E06 dispatch. Carrying the entry in `co_vec` instead of A/X is what lets the callee still receive `self` in A/X, and the same shape serves method-table dispatch later. `construct.s` reports `OBJ_ERR_HEAP` rather than printing; codegen still maps that to R005. Implemented by `construct.s`, the `OBJ_*` codes in `class.inc`, and the object fixture. | | D134 | E05-S02 class descriptors are 8-byte packed records: 2-byte instance size (including the object header), 2-byte destructor address (0 = none), 1-byte interface count, 2-byte interface-directory pointer (0 when empty), and a reserved flags byte that must be 0. There is no parent-class slot. Objects begin with a 2-byte descriptor pointer and fields follow at +2. Interface-directory entries are 4 bytes: interface-descriptor pointer and method-table pointer. `objects.s` provides read-only accessors; `output.s` writes `CLASS name size destroy count ifdir` records after sections. | Makes object identity, size/deletion, and the future interface path deterministic before construction/dispatch. Flags are reserved for non-inheritance metadata; interface descriptor contents and generated `new` remain E05-S03/E06. Implemented by `class.inc`, `objects.s`, and the class fixture. | | D136 | E05-S04 member emission lives in `src/compiler/members.s`. A generated method body starts with `mb_self_store`, recording the incoming self in zero-page `MB_SELF` (`$F9-$FA`). `self.field` emits `LDY #offset` plus `LDA (MB_SELF),Y` or `STA (MB_SELF),Y`, with a second byte for word fields; the word store moves the high byte with `TXA` because the 6502 has no `STX (zp),Y`. `mb_point` loads any two-byte object reference into `MB_SELF` for a field or call on another receiver. A concrete call emits `mb_self_load`, `JSR target`, then `frame_discard` for the argument bytes, preserving a byte or word result. `codegen.s` gains `cg_ldy_imm`, the zero-page forms, and the indexed-indirect forms. | Gives generated methods the member half of the D129 ABI without freezing parser integration or interface dispatch. One receiver pointer keeps field access at four bytes per byte field and stays clear of `$FB-$FE` (link-time assertion). Private access never reaches this layer: the parser reports `ACC_PRIVATE_MEMBER` (E120) first. Implemented by `members.s`, `members.inc`, `codegen.s`, and the member fixture. | | D137 | E04-S06 emission is driven from parser pass 2: with `em_on` set, the parser calls `emit.s` as it type-checks each statement, and `parse_file` resets the emitter (and the code buffer) at the start of pass 2; pass 1 only assigns the same slots. Parameters take `FP+2` upward in declaration order, locals `FP-1` downward, one byte for byte-sized types and two for words. A slot is reached by emitting `EM_LP = FRAME_FP + offset` with the offset's sign byte, because `(FP),Y` cannot address below `FP`; `EM_LP` is `$F5-$F6` and the combine temp `EM_TMP` is `$F7`. Byte `+`/`-` keep the left operand on the hardware stack. Print calls go through the `em_pr_text/byte/cr` cells and three `JMP (cell)` trampolines, so a fixture can record output without CHROUT. Emitter failures set `em_bad` (`EM_ERR_SIZE`, `EM_ERR_TARGET`, `EM_ERR_OP`, `EM_ERR_STATE`) and stop emitting; nothing is written outside the code buffer. | Gives the first parser-to-codegen path a testable shape without freezing pass 3: the fixture executes the emitted bytes in place and checks locals and recorded output. The 255-byte buffer, one captured string literal, and the fixture-owned frame prologue are known limits for E04-S07, which owns sizing, section addresses, and `frame_enter`. Control flow, calls, fields, word values, arrays, and diagnostic reporting remain E04-S06 work. Implemented by `emit.s`, `emit.inc`, the `parser.s` hooks, `codegen.s`, and the statements fixture. | | D138 | E04-S06 completes the pass-2 emission path above D137. `flow.s` grows to 64 labels and 128 fixups; `if`/`else`, `while`, a constant-byte-end positive-step-1 `for`, and `return` emit through five-byte branches, `JMP` back edges, and `JSR` fixups. `fl_release` remains a successful no-op because released labels could still have unresolved fixups. Each method gets a stable entry label allocated by member row, so a call written before the method body resolves forward; `em_entry` records the program body's offset in `cg_code` so a fixture runs the program rather than a class method. `em_bad` is converted to `E_RES_EMIT_FAILED` (E227) after pass 2. The three print vectors are absolute zero-page cells at `$C0-$C5` (`em_pr_text`, `em_pr_byte`, `em_pr_cr`), so their low byte is never `$FF` and the `JMP (cell)` trampolines avoid the 6502 indirect-JMP page bug; six reserved BSS bytes preserve the existing layout of later emitter state. | Closes the E04-S06 byte-code path without changing the generated container. `for` ends one past its end value and currently requires a constant byte end and step 1; `return` is a bare `RTS` because the frame prologue is still fixture-owned. Word values, call arguments, arrays, method result forwarding, and the sizing/pass-3 address pass remain explicitly unimplemented and belong to E04-S07. Implemented by `emit.s`, `flow.s`, `parser.s`, `codegen.s`, `diagcat.inc`, the statements fixture, and the calls fixture. | | D139 | E04-S07 begins the pass-3 address path. `flow.s` resolves absolute `JMP`/`JSR` operands against `fl_code_base`, which defaults to `cg_code`; `sizing.s` computes the final code base as `PRG_ENTRY_ADDR + bytes before CODE` and installs it with `em_set_base` before pass 2. `counter.n8` aliases two local bytes, loops `i = 1 to 5`, and sums into `total`; its fixture writes `COUNTER.PRG`/`COUNTB.PRG` with STUB, START, and CODE sections. The generated wrapper selects CR `$0E`, sets `FRAME_FP` to `$BF80`, calls the code base, reads `total`, restores CR, and reports PASS. | Proves final section addresses and generated-code relocation in a real x128 run while keeping existing in-place emitter fixtures unchanged. D140 completes the count-mode and MAP work for E04-S07. Implemented by `flow.s`, `emit.s`, `sizing.s`, `counter.n8`, the counter fixture, and its scripts. | | D140 | E04-S07 completes count-mode sizing and the MAP/diagnostic evidence. `em_count` selects the count pass; `em_end_func` records each routine length in `em_routine_sizes`; `sz_capture` snapshots code length, entry, routine sizes, and all 64 label offsets; the emitting pass runs with `diag_suppress`; `sz_verify` counts every difference and raises `RES_SIZE_MISMATCH` (E228) after the pass. The counter fixture requires `sz_mismatch = 0`, 175 code bytes, `CODE 1C4F`, deterministic PRGs, and `total = 15`. `output.s` adds `METHOD name address` records after `CLASS` records, proven by `METHOD DESTROY`/`METHOD STUB` in the class MAP. `tests/parser/unresolved.n8` proves one unknown method produces exactly one `SEM_UNKNOWN_NAME` (E61), and the output suite still proves E202 in the pass-3-adjacent writer path. | Closes the E04-S07 acceptance evidence without changing ordinary in-place fixtures: `parse_file` clears `em_count`, `diag_suppress`, and the base request, while `parse_file_sized` opts into the counted path. The code buffer remains 255 bytes and `em_entry` remains an offset; general program startup and frame sizing are separate work. Implemented by `parser.s`, `emit.s`, `emit.inc`, `sizing.s`, `diagnostics.s`, `diagcat.inc`, `output.s`, `formats.inc`, the counter/class/output/parser fixtures, and their scripts. | | D141 | The MAP CLASS record carries the class descriptor's final address between the name and size: `CLASS name address size destroy interface-count interface-directory`. `out_class_addr` supplies it, `OUT_CLASS_RECORD_SIZE` grows from 10 to 12 bytes, and METHOD records remain `METHOD name address`. The class fixture exports `desc_empty`/`desc_box` and compares both descriptor addresses plus the two method addresses against the linker labels. | Makes class and method map records symmetric and directly verifiable, instead of requiring the caller to infer a descriptor address from the size/destructor fields. This is a visible MAP format change; existing SECTION, ENTRY, and method records keep their order. | | D142 | E06-S01 freezes the interface value ABI in `src/common/interfaces.inc`. A value is a four-byte fat reference, little-endian: object pointer at +0 and interface method-table pointer at +2. All four bytes zero is canonical null; a non-null object with a zero table is `IF_ERR_TABLE`. A per-class table holds two-byte method entries in the interface's declaration slot order with no header, so slot *n* sits at table+2*n. Interface parameters, locals, fields, and array elements occupy four bytes with no padding, and callers push all four bytes right-to-left so the object low byte is at `FP+2`. A four-byte result cannot use A/X: the callee writes it to fixed zero-page `IF_RESULT` (`$C6-$C9`) and the caller copies it out before another interface-returning call. `src/runtime/interfaces.s` supplies `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_ret_store`, and `if_ret_load`, with `if_error` reporting `IF_OK`, `IF_ERR_NULL`, or `IF_ERR_TABLE`. | Implements the E06-S01 freeze above the object/descriptor ABI and settles the open interface-result convention. Dispatch, table emission, and interface lifetime remain E06-S02/E06-S03/E06-S06. The x128 fixture checks layout, null, copy, slot order, the result area, and argument order; the assertion keeps `IF_RESULT` clear of the print vectors and for-end values. | | D143 | E06-S02 adds `if_call` to `src/runtime/interfaces.s` as the one interface dispatch path: A/X = interface value pointer, Y = slot. It resolves the slot through `if_slot`, stores the entry in page-safe `if_vec`, and jumps there with A/X = the object pointer as `self` after pushing one synthetic return address. Faults happen before any target executes: `IF_ERR_ENTRY` for a zero entry, `IF_ERR_NULL`, `IF_ERR_TABLE`, and `IF_ERR_STACK` when TSX is at or below `FRAME_HW_MIN`. The callee's A/X and flags come back unchanged, so a refusal is reported by `if_error` (`IF_OK` exactly when the call happened), not by C. | Gives every interface call one audited trampoline instead of duplicating synthetic-return code, and refuses invalid targets rather than jumping to address zero. The x128 fixture covers three runtime-selected targets, three-level nesting, 8- and 12-level recursion, SP/frame/argument/IRQ and I/D preservation, the three target faults, a full-stack refusal with recovery, and the page-safety assertion. | | D144 | E06-S03 adds `src/common/iftables.inc` and `src/compiler/iftables.s` as the one interface-table emitter. `it_begin` (A/X = first table address) clears the error, table count, and buffer watermark; `it_set_addr` (A/X = address, Y = member row) records a method entry; `it_class` (A = class name ID) emits one table per implemented interface in implements order, one slot per interface method in slot order, resolving each requirement to the class member row through the exported `find_method`. Table data lives in a 512-byte `it_data` buffer with at most 16 tables and 255 address rows; parallel arrays `it_class_ids`, `it_iface_ids`, `it_slots`, `it_table_addrs`, and 2-byte `it_offsets` are indexed by table number. Errors are `IT_OK`, `IT_ERR_SPACE`, `IT_ERR_TABLES`, `IT_ERR_STATE`, `IT_ERR_METHOD`, and `IT_ERR_UNSET`; a failed class restores `it_used` and leaves `it_count` unchanged, so the caller can supply the missing address and retry. `output.s` gains `out_table` and appends `TABLE class interface address slots slot0 .. slotN` records after the METHOD records, bounded by `OUT_MAX_TABLES` (16) with `OUT_ERR_TABLES` (9). | Emits the tables E06-S01 froze and `if_call` consumes, with the destination address supplied by the caller because method placement is pass 3. The x128 fixture fills the real parser tables for two interfaces and two classes and checks table contents, distinct per-class tables, one ALPHA method filling both IFOO and IBAR slot 0, both rollback paths, the three MAP records, and `it_used = 10`. Binding the tables to pass 3, interface conversion, and lifetime remain E06-S04/E06-S05/E06-S06. | | D145 | E06-S04 closes the gap between the E06-S03 tables and an interface value. `it_find` (A = class ID, X = interface ID) returns the table address the class emitted for that interface, or C=1 with `IT_ERR_IFACE`, so a conversion never guesses a table and cannot emit one the class does not implement. `src/runtime/interfaces.s` gains `if_assign` (A/X = destination, Y = 0 copies `if_src`, any other value converts `if_obj` with the compile-time `if_tab`; a zero object writes the canonical null), `if_arg_push` (pushes four bytes right-to-left so the object low byte lands at FP+2), and `if_elem_store`/`if_elem_load` (4-byte elements at `base + IF_VALUE_SIZE*index`, refusing a wrapped address with `IF_ERR_RANGE`). | Makes the story's four acceptance paths executable before codegen grows four-byte values: assignment, parameter passing, field and element storage, and returns all go through one API, while the compile-time rejection stays in `types.s` (E080). The x128 conversion fixture checks the class pair, canonical null, verbatim copy, a field, three array elements, the wrapped-address refusal, the argument layout, and the result area; the table fixture now proves `it_find` for three implemented pairs and one refusal. | | D146 | E06-S05 makes an interface call resolvable at its call site. `src/common/ifcalls.inc` fixes the call-site result codes (`IS_OK`, `IS_ERR_METHOD`), and `src/compiler/interfaces.s` gains `iface_slot` (A = interface name ID, X = method name ID -> A = the declaration slot from `C_MEM_SLOT`, or C=1 with `IS_ERR_METHOD` when the interface declares no such method, which covers a class or program ID too). At run time the slot indexes the table half of the fat reference, so the compile-time slot and the runtime table pointer together select the method; nothing at run time re-derives interface order. | The call sequence itself was already `if_call` (E06-S02) plus the frame ABI, so what was missing was a way for emission to learn the slot without re-deriving declaration order, and an end-to-end proof of the story's four behaviours. The x128 call fixture reaches two classes from one call site, two interfaces from one object by swapping only the table pointer, a nested call with a one-byte result, and a byte/word argument pair returning a word in A/X with the frame stack restored; it also checks `self` is the object, not the value record. | | D147 | E06-S06 gives interface values one lifetime operation in a separate module. `src/runtime/ifelife.s` adds `if_delete` (A/X = value pointer) so deletion does not force value and dispatch fixtures to link the heap, objects and construction. A zero pointer or a value whose object half is zero is a no-op with C=0/`IF_OK` (the table half is ignored, because deletion reaches the object). Otherwise the object half is passed to `heap_check` before anything indirect happens, and only a live block reaches `obj_delete`; the four value bytes are cleared so deleting through the same value twice is a no-op. A refused delete returns C=1 with the new `IF_ERR_FREED` (6) and calls and frees nothing. `heap_check` in `heap.s` exposes the always-on validation subset `heap_free` already performed: range, used bit, even size, and the `$A55A` used magic. | Architecture §8 says aliases may outlive a deletion and that the always-on heap validation, not poisoning, is what makes a stale pointer detectable. Checking the object half before the destructor address is read turns use-after-free into a defined refusal instead of reading a freed header, and clearing the value keeps the common single-owner path idempotent without adding a reference count. Keeping the check inside `heap.s` means the rule is written once; a switch to optional poisoning later cannot leave deletion unguarded. | | D148 | E07-S02 puts both emulator launchers on one shared implementation, `scripts/lib/Nova128Launcher.psm1`. `Enter-Nova128ImageSession` takes an exclusive `.newlang-session.lock` beside the writable image, replaces a stale lock only when its PID is gone, and refuses when the holder is alive or the lock cannot even be read; `Wait-Nova128ImageFlush` reopens the image exclusively with retries so a launcher never returns while the drive is still writing; `Invoke-Nova128Emulator` starts the emulator, waits for exit, then waits for flush and releases the lock. `scripts/launch-editor.ps1` uses `x64sc` with `../vim/vim128.prg` and copies `../vim/vim-improved.d81` to `build/newlang-edit.d81`; `scripts/launch-compiler.ps1` uses `x128` with `build/n128c.prg` and prepares the development image unless `-SkipPrepare`. Both autostart a host-side program path, so no viewer binary has to be on the disk, and both accept `-LimitCycles`, `-Warp` and `-Screenshot`. | The story's rule is that the editor and the compiler never hold the same writable disk at once, which is only reliable if both launchers implement the lock and the flush wait identically. A lock file beside the image is detectable from either script and from a test without a shared daemon; an unreadable lock counts as held because an exclusive holder denies reads too. Copying the editor disk per session keeps `../vim/vim-improved.d81` a read-only original, matching E07-S01's rule for the compiler disk. `x64sc` names the VIC-II exit screenshot `-exitscreenshot` while `x128` uses `-exitscreenshotvicii`, so the option is chosen by emulator name. `docs/editor-workflow.md` carries the user-facing order and recovery rules, and `tests/disk/launchers.tests.ps1` proves the lock, the flush wait, both autostarts and that the source disk hash is unchanged. | | D151 | The editor workflow now defaults to the C128 builds: `scripts/launch-editor.ps1` starts `x128` in 80-column mode with `../vim/vim128.prg`, injected as `vim128` into the session's copy of `../vim/vim-improved.d81`, and autostarts that disk copy. This supersedes the `x64sc` editor default in E07-S02's acceptance text; the C64 editor build and its disk remain available through explicit arguments. Because `x128`'s plain `-exitscreenshot` captures the active display (VDC in 80-column mode) while `-exitscreenshotvicii` captures the 40-column screen, the shared launcher selects the option by emulator and mode. The E07-S03 round-trip test now creates, reopens and tokenizes the source through `vim128` in `x128`, so the whole path stays on the target machine. The editor must be autostarted from the session disk (`work.d81:vim128`, with the launcher running in the image's folder), not from a host path: a host-injected program has no device number from its load, and `vim128` then reports `written` while the image never receives the file — observed while validating the round-trip, which only passed once the disk copy was autostarted. | The user asked for the C128 Vim to be the default editor everywhere. The C128 build was already present as `../vim/vim128.prg` (80-column, built by `vim/build128.bat`) but no shipped `.d81` held it; injecting it into the per-session disk copy makes the editor available to the session's own disk access without writing `../vim` or adding a new reference image. Capturing the VDC screen is required for automated checks: a VIC-II capture of an 80-column session is blank. Keeping the C64 build reachable as an argument preserves the proven fallback instead of deleting working behaviour. | | D152 | The E08-S01 harness reads its suite list from `tests/regression/suites.ps1`, which returns an array of hashtables, and gives each suite a fresh `build/-test` directory unless `-NoFresh` is passed. Results are written as JSON with per-suite status, seconds, exit code, signature, problems, log path and artifacts; artifact SHA-256 values are compared against `tests/regression/baseline.json`, which `-UpdateBaseline` rewrites. A failed suite's signature is the diagnosis line it printed (for example a fixture name and the mismatch), falling back to the structural problem such as `exit code N` or a timeout when the suite produced no output. | A `.psd1` cannot be invoked with `&` (PowerShell returns nothing), so the manifest is a script whose array literal is evaluated on read; a data file would need `Import-PowerShellDataFile` and could not express per-suite logic. Fresh per-suite directories satisfy the `E08-S01` requirement that every run creates its own D81; `-NoFresh` exists so the self-test can prove that changed artifacts are detected on a rerun. Reviewing hashes rather than regenerating them keeps `E08-S02`'s "reviewed rather than blindly regenerated" rule intact. Preferring the observed diagnosis over an exit status is what makes a failure identify the fixture, as the story requires. | | D153 | E06-S07 emits interface values without a new type category: the parser already classifies class and interface references by name (`sc_name`/`tt_name`) and `ty_assign` already accepts a class for an interface it implements, so a four-byte interface slot reaches `em_local`/`em_param` through the same size argument as any other type. Each storage or conversion site emits a pointer to the four-byte destination in `if_dst` and the operand in `if_src`, with the compile-time table from `it_find` in `if_tab`, then `JSR if_assign`; a null object is written to `if_obj` and does not need a distinct path. An interface call emits the slot address in `if_dst`, `Y` from `iface_slot`, and `JSR if_call`, and reports refusal through `if_error` rather than C. A field or array element of interface type is reached through a four-byte member load/store that copies between the value and `if_dst`/`if_src`, not through `mb_ld_field`/`mb_st_field`, which are byte/word only. Four-byte values never travel in A/X except through `IF_RESULT`, and the emitter does not add a zero-page pointer: `EM_LP` (`$F5-$F6`) addresses one value at a time, `$F2-$F4` is the only free zero page, and `$FB-$FE` is the frame ABI. | The runtime already provides every transfer (D145); the missing half is emission, so reuse keeps one audited conversion and one dispatch path instead of copies in generated code. Deriving the slot size from the parser avoids inventing parallel interface types that would then disagree with `ty_assign`. Refusing to add a fourth zero-page pointer is forced by the map: `$C0-$C5` print vectors, `$C6-$C9` `IF_RESULT`, `$D0-$E0` for-end values, `$E1-$F1` for-step values, `$F5-$F6` `EM_LP`, `$F7-$F8` `EM_TMP`, `$F9-$FA` `MB_SELF`, `$FB-$FE` frame ABI. Argument-bearing interface calls wait for E04-S07's push/discard emission, which is why that story is now a dependency. | | D154 | E07-S05 adds the on-machine driver `src/compiler/main.s`, built by `scripts/build-driver.ps1` into `build/n128c.prg` with the whole compiler, `output.s` and `src/runtime/startup.s`. The driver prompts `SOURCE:` through KERNAL `CHRIN`, accepts up to 15 characters, folds PETSCII lowercase to uppercase, loads with `src_load`, initialises `diag_init`, sets `em_on`, and plans the code base as `PRG_ENTRY_ADDR + gen_startup_size` with `sz_plan_code` before `parse_file_base`, so absolute targets match the PRG. It then writes STUB, START and CODE with `out_begin`/`out_startup`/`out_section`/`out_bytes`/`out_finish` and prints `SOURCE name`, `OUTPUT name`, `n ERRORS`. The three `em_pr_*` cells are explicitly zeroed because the generated image does not carry `src/runtime/print.s` yet: a `print` source fails with E227 instead of emitting a call into the compiler's own copy of the routine. Refusals print `OUTPUT EXISTS`, `NAME TOO LONG`, `DISK ERROR`, `LOAD FAILED`, `COMPILE FAILED, n ERRORS` or `NO CODE GENERATED`, and `out_abort` scratches only files the run created. `scripts/launch-compiler.ps1` gains `-Keys`/`-KeyDelay` so a test can type the source name, and `scripts/verify-driver-test.ps1` drives the summary, success, syntax-failure, output-collision and generated-program runs on a fresh D81. | The driver is wiring around contracts that already existed, so it adds no new compiler phase: the only new decisions are the fixed image layout (CODE follows the startup at `$1C57` for the current startup size), the name limit, and keeping print honest rather than emitting a pointer into the compiler. A clean code-only source now produces a runnable container (`COUNTER.PRG`, 263 bytes, CODE at `$1C57`, 95-byte MAP) whose generated program reports `MB_ST_PASS` in a second x128 session; a syntax error writes `BAD.ERR` (`E024`, then `E227`) and leaves an earlier `COUNTER.PRG` byte-identical, as does a refused second compile. Text `print` stays a known gap until the generated-program print runtime is emitted into the container. | | D155 | Generated programs carry their own print runtime. `src/runtime/prgprint.s` is position independent (relative branches, no self-addressed stores, only the fixed `CHROUT` address) and is copied verbatim into a `PRINT` section between `START` and `CODE`. It works in four zero-page bytes (`$C0`, `$F2-$F4`) that a generated `ENTRY` stub saves before calling the body and restores before returning to BASIC; the stub also sets the D129 frame base to `$BF80`. `em_print` now emits `JSR` to the value in `em_pr_text`/`em_pr_byte`/`em_pr_cr` instead of a compiler trampoline, and the driver sets those cells to `PRG_ENTRY_ADDR + gen_startup_size` plus each routine's offset before parsing. The code base is planned as entry plus startup plus runtime size. The one captured string literal is copied into a `DATA` section after CODE and the driver rewrites the emitted `LDA #gen_text` pair to that copy, so the PRG never references compiler memory. | Closes the image gap E07-S05 hit: `print` compiles to a call inside the PRG, `COUNTER.MAP` gains a `PRINT` section, and `hello.n8` displays its string from a standalone PRG. Emitted sizes are unchanged (the `JSR` is still three bytes), so existing lengths and in-place recorder fixtures hold; the old trampolines remain for fixtures that point the cells at their own recorders. | ## Deferred - Native C128/80-column Vim port - Vim `:make` and error-navigation commands - Class inheritance and virtual class methods - Interface inheritance and default methods - Separate compilation and relocatable Nova128 object files - Garbage collection or reference counting - Exceptions, generics, closures, reflection, and method overloading - Floating point - C64 and Z80 code-generation targets - Bank 1 heaps for generated programs ## Open Questions 1. What generated-program memory map should E04-S01/E04-S02 adopt? (The compiler map is D117.) 2. What compiler table capacities fit in the 21,760-byte bank-1 table window and 254-byte zero page (D117, D118)?