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.

20KB


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.

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 NOPs; 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.

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)?

Powered by TurnKey Linux.