You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15KB


type: compiler-runtime-architecture source-path: expert design plan, repository Vim notes, C128 reference architecture ingested-at: 2026-11-07 version: 0.1-draft

Nova128 Compiler and Runtime Architecture

Status: approved version 1 baseline. The compiler memory map is frozen by E01-S05 (section 4.4, docs/memory-map.md). Generated-program addresses are still deliberately absent until the E04 stories validate them.

1. System Boundary

The deliverable compiler is an 8502 program that runs on a Commodore 128 in native mode. Host-side tools may:

  • assemble and link the compiler with ca65 and ld65;
  • copy files into or out of D81 images with c1541;
  • launch and control VICE;
  • compare deterministic test outputs.

Host tools must not perform Nova128 parsing, type checking, or code generation in the release workflow. The C128 compiler performs those functions.

Generated version 1 programs target C128 native 8502 mode. They do not target C64 mode or the Z80.

2. Toolchain and Artifacts

2.1 Authoritative source

  • Compiler/runtime assembly: ASCII ca65 source under src/
  • Linker layouts: ld65 configuration under cfg/
  • Language examples: readable ASCII mirrors under examples/
  • PETSCII/D81 forms: generated under build/

2.2 Compiler artifacts

Artifact Purpose
N128C.PRG On-machine compiler executable
NAME.N8 PETSCII SEQ input source
NAME.PRG Generated application
NAME.ERR Complete stable-code diagnostics
NAME.MAP Symbols, addresses, class layouts, interface slots, and section sizes

Output replacement must be explicit. The compiler must not silently overwrite an existing PRG after a failed compilation.

3. Repository Module Plan

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: compiler tables $2000-$BFFF (40 KB).
  • Zero page: the compiler owns logical $02-$FF via P0; KERNAL calls run on the system zero page.
  • NMI: guarded by mmu_nmi_guard through $0318; RESTORE is ignored during compilation.
  • Test mailbox: 16 bytes at $1BF0, shared by platform tests, the compiler, and generated programs.

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

4.3 Display and clock

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

5. Compiler Passes

The compiler reparses a buffered/streamed source rather than retaining a large pointer-rich abstract syntax tree.

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 trade disk/source traversal time for bounded memory. The source stream should be rewindable without reopening unsafe shared emulator state.

6. Compiler Data Records

Final byte layouts are story outputs, but records require these logical fields:

  • Interned name: hash, length, original spelling location, normalized bytes
  • Symbol: name ID, kind, type ID, scope ID, flags, value/address
  • Type: kind, size, referenced class/interface/element, array length
  • Class: name ID, instance size, field range, method range, interface range
  • Field: owner class, name ID, type ID, access, byte offset
  • Method: owner, name ID, signature, access, code address, code size
  • Interface: name ID, ordered signature range
  • Implementation: class ID, interface ID, emitted table address
  • Diagnostic: code, file position, arguments

Use compact fixed-width records where practical. Every table has an explicit capacity and produces a resource diagnostic rather than corrupting adjacent memory.

7. Generated Program ABI

7.1 Scalar representation

  • Multi-byte values are little-endian.
  • boolean, byte, and char occupy one byte.
  • integer, word, concrete references, and addresses occupy two bytes.
  • Interface references occupy four bytes.

7.2 Return convention

  • One-byte result: accumulator A
  • Two-byte result: A is low byte, X is high byte
  • Four-byte interface result: runtime-defined result area referenced through a reserved ABI pointer; exact zero-page location is frozen after E01
  • No result: A and X are caller-clobbered

The final register-clobber table is part of story E02-S02.

7.3 Frames and calls

  • The hardware stack carries return addresses and small runtime temporaries.
  • Arguments, locals, and expression spills use a software-managed stack/frame.
  • self is held in a reserved two-byte ABI pointer during an instance call.
  • Parameters are laid out in declaration order using their ABI sizes.
  • Concrete method calls are statically resolved to a direct JSR.
  • The runtime checks stack bounds before frame growth in debug builds and at defined safety points in release builds.

7.4 Object layout

Every heap object begins with a two-byte class-descriptor pointer, followed by fields in declaration order with no hidden alignment padding:

+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. Numeric assignments live in src/common/errors.inc once story E02-S03 freezes the catalog.

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
  • Whether to cache the complete source in banked RAM when it fits
  • Physical-hardware acceptance requirements

Powered by TurnKey Linux.