Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

22KB


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; the higher generated-program map (banking, heap, software stack) remains open until E04-S02.

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
    output.s        PRG, ERR, and MAP writers
    diagnostics.s   stable error catalog and recovery
  runtime/
    startup.s       generated-program entry and initialization
    frames.s        software argument/local-frame support
    objects.s       object construction and destruction
    interfaces.s    interface conversion and indirect call trampoline
    heap.s          deterministic allocator and free list
    arithmetic.s    non-inline arithmetic helpers
    print.s         scalar and literal output
    faults.s        null, bounds, divide, stack, and heap failures
  common/
    c128.inc        hardware and KERNAL symbols
    formats.inc     compiler record and output constants
    tokens.inc      token numbers
    errors.inc      stable diagnostic numbers

Files are introduced by stories; this tree is a target, not permission to add empty placeholders.

4. C128 Memory Strategy

The design uses the two 64K RAM banks and a verified common-RAM window.

4.1 Compiler bank roles

  • Bank 0: resident compiler code, phase controller, lexer/parser state, disk/KERNAL wrappers, diagnostics, and small buffers.
  • Bank 1: interned names, symbols, class records, interface records, method signatures, type records, and pass workspaces.
  • Common RAM: MMU transition code, hardware stack, software-stack metadata, disk transfer buffer, and state required while either bank is selected.

No module other than mmu.s directly changes the MMU. Banked data is accessed through handles or explicit copy routines; ordinary pointers must not escape the bank in which they are valid.

4.2 Memory-map freeze gate

Story E01-S02 must prove:

  • chosen MMU configurations;
  • common-memory size and placement;
  • page-zero and page-one behavior;
  • safe execution of the bank-switch trampoline;
  • KERNAL entry and return mappings;
  • interrupt behavior while a non-default bank is active.

Only then may linker addresses and zero-page allocations become normative.

E01-S02 evidence is in docs/mmu-probe.md. In VICE x128 3.10, distinct banks, 8K bottom+top common RAM, bank-switched JSR/RTS, and full MMU restoration pass. Page pointers aimed at bank 1 while bottom common is active resolve to bank 0 at the translated page, so Nova128 keeps P0/P1 in bank 0. Non-default mappings run with IRQs masked; an IRQ path under bank 1 is not yet proven.

E01-S03 evidence is in docs/kernal-wrapper.md. A common-RAM wrapper enters the KERNAL in BASIC's mapping and restores the exact compiler MMU state. From bank 0 and bank 1 compiler mappings, CHROUT, a drive-8 status read, and a device-not-present failure all work, with A/X/Y/C/ST preserved and no channels left open.

4.4 Frozen compiler memory map (E01-S05)

The gate above is closed for the compiler. cfg/compiler.cfg is authoritative and docs/memory-map.md explains it and gives the evidence. In summary:

  • Compiler states: CR $0E (bank 0) or $4E (bank 1), 8K bottom common RAM, P0 relocated to bank 0 page $1A, IRQs masked.
  • Bank 0: COMMON $1300-$18FF (copied at init), DISKBUF $1900, private zero page $1A00, SCRATCH $1B00-$1BEF, MAILBOX $1BF0-$1BFF, MAIN $1C0D-$B7FF (STARTUP first), SWSTACK $B800-$BFFF.
  • Bank 1: source cache $2000-$6AFF (D118), compiler tables $6B00-$BFFF (21,760 bytes).
  • Zero page: the compiler owns logical $02-$FF via P0; KERNAL calls run on the system zero page.
  • NMI: guarded by mmu_nmi_guard through $0318; RESTORE is ignored during compilation.
  • Test mailbox: 16 bytes at $1BF0, shared by platform tests, the compiler, and generated programs.

The generated-program memory map remains open (E04-S01/E04-S02).

4.3 Display and clock

Compiler text uses KERNAL-compatible character output on the display already selected by the user; it does not force 80-column mode. E01-S04 verified the editor's $00D7 mode flag in x128: 40-column uses a deterministic 1 MHz fallback, while 80-column may set $D030 bit 0 for 2 MHz KERNAL output. The speed bit is cleared before returning to BASIC, and any uncertain mode falls back to 1 MHz. Direct VDC screen memory is display-only and is not compiler semantic storage. See docs/platform-display-clock.md for evidence.

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

The class descriptor contains the instance size, optional destructor address, and a directory of implemented interfaces. Its final packed format is frozen by E05 and E06 stories.

7.5 Interface representation and dispatch

An interface value is a fat reference:

+0  object pointer (2)
+2  interface method-table pointer (2)

Each interface defines a slot order from its source declaration. For every implemented interface, a class emits a table of two-byte method addresses in that order.

Converting a concrete reference to an interface reference:

  1. copies the object pointer;
  2. uses compile-time-known class/interface metadata to select the class's implementation table;
  3. stores the table pointer in the interface value.

An interface call:

  1. checks the object pointer for null;
  2. installs the object pointer as self;
  3. loads the two-byte target from the known interface slot;
  4. invokes the runtime's indirect-call trampoline.

The 8502 has no JSR (address) instruction, so the trampoline must establish a synthetic return path safely. It is tested independently before interface code generation relies on it.

delete through an interface uses the object pointer and its class descriptor, not the interface table, to find the optional destructor and object size.

8. Heap and Ownership

  • The generated-program heap initially resides in the same RAM bank as program data; banked heaps are deferred.
  • new requests the class instance size, zeroes the allocation, stores the descriptor pointer, and invokes init.
  • delete invokes destroy at most once for that call and returns the entire block to a coalescing free list.
  • Allocation failure and corrupt/double-free detection use runtime fault codes.
  • Reference copying does not change an ownership count.
  • Aliases can outlive a deletion; that is a program error. Debug poisoning may help diagnose it but is not a correctness mechanism.

9. KERNAL and Disk I/O

The compiler uses documented KERNAL entry points through diskio.s wrappers. Each wrapper:

  1. records compiler mapping state;
  2. installs the verified KERNAL-compatible mapping;
  3. performs the operation and reads status;
  4. restores compiler mapping state;
  5. reports errors without leaving files or channels open.

The command channel must be opened before source/output files when status is needed and closed last, matching the proven editor workflow.

10. Vim Integration

The existing Vim clone is treated as an external producer/consumer of SEQ files. Version 1 does not link the editor and compiler in memory.

The development image workflow is strictly sequential:

  1. copy ../vim/work.d81 to build/newlang-dev.d81;
  2. inject N128C.PRG, examples, and fixtures;
  3. launch x64sc and autostart VIM for editing;
  4. close the process and wait for image flush;
  5. launch x128 and autostart N128C.PRG for compilation;
  6. close the compiler session before inspecting or modifying the image.

Scripts must refuse obvious concurrent use where detectable. Tests create a fresh image per run.

11. Diagnostics and Faults

Compile-time diagnostics use stable IDs such as E014. Runtime faults use a separate stable namespace such as R003. The compile-time catalog is src/common/diagcat.inc (one line per diagnostic, numbered by category range); errors.inc turns it into E_* constants and diagnostics.s into messages. docs/diagnostics.md is generated from it (E02-S04).

diagnostics.s prints a two-line screen form (header, message clipped to 39 characters) and appends one full line per diagnostic to NAME.ERR. The ERR file is scratched when compilation starts and created only on the first diagnostic. After 20 errors the next is replaced by E221 and the caller must stop. If NAME.ERR cannot be written, E201 is shown once and reporting continues on screen.

Compiler errors include source name, line, column, and compact message. Runtime faults always include a stable fault code and the best available program address that can be resolved through the MAP file. Rich stack traces are deferred.

12. Verification Architecture

Testing is layered:

  1. Host checks for deterministic converters, image preparation, and byte-level expected files.
  2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface conformance, and diagnostics.
  3. Golden PRG bytes and disassembly for small generated routines.
  4. End-to-end x128 runs that write a result signature to the fixed test mailbox at $1BF0 (src/common/mailbox.inc, docs/memory-map.md).
  5. Monitor scripts with MMU, stack, heap, and dispatch break/watch points.
  6. Editor round-trip: Vim saves source, C128 compiler consumes it without host conversion, and Vim reopens unchanged source.
  7. Physical C128 smoke testing when hardware is available.

Each emulator test has a cycle/time limit and captures enough output to explain failure without relying only on a screenshot.

13. Principal Risks

  • Incorrect MMU assumptions corrupt compiler state or KERNAL execution.
  • Interface fat references increase local/array memory use.
  • The indirect-call trampoline can corrupt the hardware stack if designed or tested casually.
  • Repeated parsing can be slow on real disks.
  • Cross-bank raw pointers can create intermittent corruption.
  • Feature growth can exceed the on-machine compiler's memory budget.
  • Two VICE processes can overwrite the same D81 image.

The story order makes each high-risk mechanism prove itself before downstream features depend on it.

Open Questions

  • Generated-program memory map (the compiler map is frozen in section 4.4)
  • Final interface-result convention
  • Compiler table capacities and their user-visible resource limits
  • Physical-hardware acceptance requirements

Powered by TurnKey Linux.