Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

37KB


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

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:

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:

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:

+0  class descriptor pointer (2)
+2  first declared field
... remaining fields

E05-S02 freezes the class descriptor as an 8-byte packed record:

+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:

+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:

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)

src/compiler/emit.s is the 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. Parser hooks are gated by both em_on and pass 2, so parser fixtures that only type-check leave the emitter idle.

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. Comparisons emit CMP and record the operator in em_cmp_op; em_cond_false turns those flags into the appropriate branch, including the two-branch forms for > and <=.

Control flow uses flow.s labels and fixups: if/else, while, a positive-step-1 for with a constant byte end, and return. Methods get stable entry labels 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 can run the program after class methods. em_finish checks every flow fixup and converts em_bad to E_RES_EMIT_FAILED (E227) after pass 2. A failed emit leaves only the bounded code buffer to inspect; it writes no output file.

self.field loads and stores through members.s. A concrete call loads MB_SELF, emits JSR to the method label, and preserves a byte result. The current call path accepts only zero-argument self calls; the existing members.s discard path is exercised with a zero-byte argument list.

print emits through em_pr_text, em_pr_byte, and em_pr_cr at $C0-$C5, 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. Keeping the cells in zero page also keeps their low byte away from $FF, avoiding the 6502 indirect-JMP page bug without run-time alignment. 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 a for, an if/else, and a while, then JSRs the 244 emitted bytes in place. It checks a = 2 (the loop variable one past the end value) and b = 3, with no recorded print output. The calls.n8 fixture parses a method that increments self.n, calls a forward method that increments it again, and explicitly returns; the 53 emitted bytes leave n = 2.

Word values, call arguments, arrays, method result forwarding, the frame prologue, and sizing/pass-3 addresses remain unimplemented. for currently requires a constant byte end and step 1 and leaves the loop variable one past the end value. E04-S07 owns those limits and the 255-byte code buffer.

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:

+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:

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

Powered by TurnKey Linux.