From b6425bd57bd9fdcc95f47794220605c65e13a407 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Mon, 9 Nov 2026 13:47:13 -0500 Subject: [PATCH] Add the Nova128 runtime and codegen layer through E05-S03. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated programs now have control-flow emission, text and fault output, a free-list heap, class descriptors, and object construction/deletion. Each story lands with an x128 fixture and verifier so the runtime stays proven before the parser drives code generation. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- newLang/docs/architecture.md | 194 ++++++- newLang/docs/decisions.md | 5 + newLang/docs/memory-map.md | 60 +- newLang/planning/claims/E04-S04.md | 98 ++++ newLang/planning/claims/E04-S05.md | 120 ++++ newLang/planning/claims/E05-S01.md | 87 +++ newLang/planning/claims/E05-S02.md | 91 +++ newLang/planning/claims/E05-S03.md | 102 ++++ newLang/scripts/build-class-test.ps1 | 23 + newLang/scripts/build-control-test.ps1 | 17 + newLang/scripts/build-heap-test.ps1 | 25 + newLang/scripts/build-hello-test.ps1 | 32 + newLang/scripts/build-object-test.ps1 | 27 + newLang/scripts/build-print-test.ps1 | 24 + newLang/scripts/verify-class-test.ps1 | 76 +++ newLang/scripts/verify-control-test.ps1 | 42 ++ newLang/scripts/verify-heap-test.ps1 | 55 ++ newLang/scripts/verify-hello-test.ps1 | 78 +++ newLang/scripts/verify-object-test.ps1 | 46 ++ newLang/scripts/verify-print-test.ps1 | 67 +++ newLang/src/common/class.inc | 49 ++ newLang/src/common/faults.inc | 17 + newLang/src/common/flow.inc | 22 + newLang/src/common/formats.inc | 6 +- newLang/src/common/heap.inc | 37 ++ newLang/src/common/print.inc | 14 + newLang/src/compiler/codegen.s | 21 +- newLang/src/compiler/flow.s | 604 +++++++++++++++++++ newLang/src/compiler/output.s | 199 +++++++ newLang/src/compiler/parser.s | 83 ++- newLang/src/runtime/construct.s | 207 +++++++ newLang/src/runtime/faults.s | 87 +++ newLang/src/runtime/frames.s | 15 +- newLang/src/runtime/heap.s | 742 ++++++++++++++++++++++++ newLang/src/runtime/objects.s | 127 ++++ newLang/src/runtime/print.s | 152 +++++ newLang/tests/codegen/control-test.s | 633 ++++++++++++++++++++ newLang/tests/compiler/hello-test.s | 265 +++++++++ newLang/tests/compiler/hello.n8 | 4 + newLang/tests/runtime/class-test.s | 340 +++++++++++ newLang/tests/runtime/heap-test.s | 584 +++++++++++++++++++ newLang/tests/runtime/object-test.s | 419 +++++++++++++ newLang/tests/runtime/print-test.s | 193 ++++++ 43 files changed, 6063 insertions(+), 26 deletions(-) create mode 100644 newLang/planning/claims/E04-S04.md create mode 100644 newLang/planning/claims/E04-S05.md create mode 100644 newLang/planning/claims/E05-S01.md create mode 100644 newLang/planning/claims/E05-S02.md create mode 100644 newLang/planning/claims/E05-S03.md create mode 100644 newLang/scripts/build-class-test.ps1 create mode 100644 newLang/scripts/build-control-test.ps1 create mode 100644 newLang/scripts/build-heap-test.ps1 create mode 100644 newLang/scripts/build-hello-test.ps1 create mode 100644 newLang/scripts/build-object-test.ps1 create mode 100644 newLang/scripts/build-print-test.ps1 create mode 100644 newLang/scripts/verify-class-test.ps1 create mode 100644 newLang/scripts/verify-control-test.ps1 create mode 100644 newLang/scripts/verify-heap-test.ps1 create mode 100644 newLang/scripts/verify-hello-test.ps1 create mode 100644 newLang/scripts/verify-object-test.ps1 create mode 100644 newLang/scripts/verify-print-test.ps1 create mode 100644 newLang/src/common/class.inc create mode 100644 newLang/src/common/faults.inc create mode 100644 newLang/src/common/flow.inc create mode 100644 newLang/src/common/heap.inc create mode 100644 newLang/src/common/print.inc create mode 100644 newLang/src/compiler/flow.s create mode 100644 newLang/src/runtime/construct.s create mode 100644 newLang/src/runtime/faults.s create mode 100644 newLang/src/runtime/heap.s create mode 100644 newLang/src/runtime/objects.s create mode 100644 newLang/src/runtime/print.s create mode 100644 newLang/tests/codegen/control-test.s create mode 100644 newLang/tests/compiler/hello-test.s create mode 100644 newLang/tests/compiler/hello.n8 create mode 100644 newLang/tests/runtime/class-test.s create mode 100644 newLang/tests/runtime/heap-test.s create mode 100644 newLang/tests/runtime/object-test.s create mode 100644 newLang/tests/runtime/print-test.s diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index e4a3d9c..444461d 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -9,8 +9,9 @@ version: 0.1-draft 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. +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 @@ -72,12 +73,14 @@ src/ 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 construction and destruction + 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 @@ -368,9 +371,21 @@ fields in declaration order with no hidden alignment padding: ... 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. +E05-S02 freezes the class descriptor as an 8-byte packed record: + +```text ++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 @@ -421,8 +436,84 @@ 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 is -E04-S04's responsibility. +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: + +```text +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. ## 8. Heap and Ownership @@ -437,6 +528,93 @@ E04-S04's responsibility. - 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: + +```text ++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: + +```text +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. diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index 609adfb..e3043bd 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -57,6 +57,11 @@ documented default. `Proposed` means a later change still needs approval. | D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | | D129 | Generated code executes with CR `$0E` and IRQs masked, then restores the entry CR. The software stack is bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP`/`FP` use `$FB-$FE`. A frame stores the previous FP at `FP`, arguments from `FP+2`, and locals below `FP`; callers push arguments right-to-left and discard them after return. Results are A or A/X. `frame_enter` reserves the old FP and locals transactionally, refuses when the 6502 stack pointer is below `$40`, and returns C=1/`FRAME_ERR_STACK` with `SP`/`FP` restored on overflow. | Makes recursion and nested calls testable without consuming BASIC's zero page or the compiler's private stack. The $40 hardware-stack guard prevents a JAM before the software bound, and the transactional failure lets callers unwind cleanly. The 2 KB software stack, guard threshold, and mapping install are provisional until codegen and later runtime stories. Implemented by `frames.s` and `frames.inc`. | | D130 | Scalar codegen emits documented 6502 instructions into a bounded 256-byte buffer. Values are A (one byte) or A/X low/high (two bytes); assignments use absolute stores, comparisons produce a 0/1 byte, and forward branches use checked fixups. Multiplication and signed/unsigned division call runtime helpers with operands in `ar_a`/`ar_b`; results are A/X, division remainder is `ar_rem`, and division by zero sets provisional `ar_fault=AR_FAULT_DIV0`, A=X=0, C=1. E04-S04 owns parser-to-emitter integration, control flow, and long-branch expansion. | Gives the backend a small, directly testable instruction layer and a deterministic golden stream before the control-flow shell exists. The 256-byte buffer and forward-only branches are deliberate limits; E04-S05 maps the provisional arithmetic fault to the final R-code namespace. Implemented by `codegen.s`, `codegen.inc`, `arithmetic.s`, and `arithmetic.inc`. | +| D131 | Control-flow emission (E04-S04) uses `src/compiler/flow.s`: label ids 0-15, absolute `JMP` for back edges, and a fixed five-byte conditional branch. A target within +0..+127 is patched as the direct branch plus three `NOP`s; a farther target is patched as the inverted opcode, `+3`, and `JMP target`. `cg_put` has a counting mode so a sizing pass and an emitting pass make identical label offsets and lengths. Calls emit `JSR` then `frame_discard`; `fl_call_byte`/`fl_call_word` save A (or A/X) across the discard, and `frame_discard` itself now preserves A/X. `fl_ret_byte`/`fl_ret_word` preserve results across `frame_leave`. | Gives the backend a bounded, directly testable control-flow layer before parser and pass-2 address assignment exist. The fixed five-byte branch avoids size drift between passes by construction. Parser/statement integration, `sizing.s`, and compiler pass 3 remain open. Implemented by `flow.s`, `flow.inc`, and `codegen.s`. | +| D132 | Generated-program text output calls KERNAL `CHROUT` directly under the D129 mapping (CR `$0E`, system zero page, code below `$4000`). `pr_byte`/`pr_word`/`pr_int` print minimal decimal (no leading zeros, `-` for negatives), `pr_bool` prints `TRUE`/`FALSE`, and only `pr_cr` ends a line. Runtime faults use stable R001 null, R002 bounds, R003 divide, R004 stack, and R005 heap; `flt_report` writes code and address, then `MB_ST_FAIL`, prints `RUNTIME FAULT R00n`, and returns. The print and fault routines never change CR, RCR, or P0/P1. | Implements the output and fault half of E04-S05 without a second MMU wrapper: generated code already runs in the D129 mapping, so direct KERNAL calls are safe and the fixture proves CR/RCR survive every message. The `hello.n8` path is now proven by a fixture-only pass-2 capture (architecture 7.9); fault unwinding, general parser-to-codegen integration, and compiler pass 3 remain open. Output is verified in x128's VIC-II display only. Implemented by `print.s`, `faults.s`, and the two `.inc` files. | +| D133 | The E05-S01 heap uses four-byte headers: a two-byte total size with bit 15 set while allocated, and a two-byte next pointer while free or `$A55A` while allocated. Payload follows at header+4, blocks are two-byte aligned, and the minimum block is six bytes. `heap_init` takes A/X = page-aligned base and Y = pages 1..127; `heap_alloc` takes payload bytes, zeroes the payload, returns it or `HEAP_ERR_OOM`, and splits first-fit free blocks; `heap_free` validates range/size/magic and coalesces in address order. Validation is always on. The E05-S01 fixture uses bank-0 `$4000-$4FFF` under CR `$0E`; generated-program placement and banked heaps remain open. | Makes allocation deterministic and testable before class layout chooses a production heap base. The allocator does not print or report faults itself; E05-S03 maps `HEAP_ERR_OOM` to R005 when `new` is wired. The 15-bit size limit and page-granular region avoid a separate size argument while keeping the block header small. Implemented by `heap.s`, `heap.inc`, and the heap fixture. | +| D135 | E05-S03 construction and deletion live in `src/runtime/construct.s` above the heap and descriptor layers. `obj_new` takes A/X = descriptor, reads the instance size (zero is `OBJ_ERR_BAD`), allocates it, writes the descriptor pointer at object+0, then calls the caller-selected `obj_init_ptr` entry once with A/X = self and the constructor arguments still on the software frame stack at `FP+2`; the caller discards them with `frame_discard`. Allocation failure returns C=1 with `OBJ_ERR_HEAP` and never calls `init`. `obj_delete` takes A/X = object: null is a successful no-op, otherwise the descriptor's optional destructor runs once with A/X = self and the complete block goes back through `heap_free`. Both calls use `obj_call`, which takes its entry from zero-page `co_vec`, preserves A/X for `self`, pushes one synthetic return address, and ends with `JMP (co_vec)`; `co_vec` must not straddle a page. | Gives generated `new`/`delete` a testable runtime contract without freezing heap placement or the E06 dispatch. Carrying the entry in `co_vec` instead of A/X is what lets the callee still receive `self` in A/X, and the same shape serves method-table dispatch later. `construct.s` reports `OBJ_ERR_HEAP` rather than printing; codegen still maps that to R005. Implemented by `construct.s`, the `OBJ_*` codes in `class.inc`, and the object fixture. | +| D134 | E05-S02 class descriptors are 8-byte packed records: 2-byte instance size (including the object header), 2-byte destructor address (0 = none), 1-byte interface count, 2-byte interface-directory pointer (0 when empty), and a reserved flags byte that must be 0. There is no parent-class slot. Objects begin with a 2-byte descriptor pointer and fields follow at +2. Interface-directory entries are 4 bytes: interface-descriptor pointer and method-table pointer. `objects.s` provides read-only accessors; `output.s` writes `CLASS name size destroy count ifdir` records after sections. | Makes object identity, size/deletion, and the future interface path deterministic before construction/dispatch. Flags are reserved for non-inheritance metadata; interface descriptor contents and generated `new` remain E05-S03/E06. Implemented by `class.inc`, `objects.s`, and the class fixture. | ## Deferred diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index be06542..ce6286e 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -6,8 +6,10 @@ x128 3.10 Authoritative source: `cfg/compiler.cfg`. This document explains it. E04-S01 defines the generated PRG/MAP container below; E04-S02 adds the -provisional frame runtime. Heap placement and any future banking remain later -stories. +provisional frame runtime. E05-S01 supplies an explicit-region heap; E05-S02 +freezes class descriptors and MAP class records; E05-S03 adds construction and +deletion above both. Production heap placement and any future banking remain +later stories. ## Generated Program Container (E04-S01) @@ -17,7 +19,7 @@ stories. | BASIC 7 stub | 12 bytes, `10 SYS 7181` | | Entry | `$1C0D` | | Sections | ascending addresses, zero-filled gaps | -| MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size` | +| MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size`, `CLASS name size destroy count ifdir` | | Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses @@ -40,6 +42,58 @@ the entry CR before printing and returning to BASIC. Code generation will install this mapping and call `frame_init` when it starts emitting user code (E04-S03). The 2 KB stack and `$40` hardware-stack guard are provisional. +## Heap Fixture (E05-S01) + +| Element | Value | +|---|---| +| Region supplied by caller | page-aligned base and 1..127 pages | +| Block header | 2-byte total size (bit 15 allocated), 2-byte next/magic | +| Payload | header+4; two-byte aligned; minimum block 6 bytes | +| Fixture region | bank 0 `$4000-$4FFF` under CR `$0E` | +| Fixture code | below `$4000` | +| Exhaustion | `HEAP_ERR_OOM`, mapped to R005 by the fixture | + +`heap_init` accepts the region explicitly, so this does not freeze the +generated-program heap location. The block/API contract is in +`src/common/heap.inc`; D133 records the provisional decision. The allocator +does not print faults itself; E05-S03 will map `HEAP_ERR_OOM` when `new` is +wired. + +## Class Descriptors (E05-S02) + +| Element | Value | +|---|---| +| Object header | descriptor pointer at +0; fields from +2 | +| Descriptor | 8 bytes: size, destructor, interface count, directory, flags | +| Instance size | includes the two-byte object header | +| Destructor | `$0000` means none | +| Interface directory | 4-byte entries: interface descriptor, method table | +| Flags | reserved, must be 0; no parent-class slot | +| MAP | `CLASS name size destroy count ifdir` after sections | + +The ABI is fixed in `src/common/class.inc`; D134 records the provisional +decision. `src/runtime/objects.s` reads descriptors and objects without +allocating. `output.s` writes class MAP records in `out_class` call order; the +E05-S02 fixture checks a no-destructor/no-interface class and a destructor plus +one interface entry. Interface dispatch remains E06 work. + +## Construction and Deletion (E05-S03) + +| Element | Value | +|---|---| +| New fixed addresses | none; `obj_new` uses the caller-initialized heap | +| Fixture region | bank 0 `$4000-$4FFF` under CR `$0E`, code below `$4000` | +| Module zero page | `co_vec`, `co_desc`, `co_ptr`, `co_self` (link-allocated) | +| Module BSS | `obj_init_ptr`, `obj_error`, saved object/size/destructor | +| `obj_call` stack cost | one synthetic return address, two hardware-stack bytes | +| Constructor arguments | stay on the software frame stack at `FP+2`; caller discards | + +`src/runtime/construct.s` adds no fixed addresses, so the generated-program +heap location stays open. `co_vec` carries the indirect target and the module +asserts it does not end at `$xxFF`, because `JMP ($xxFF)` reads its high byte +from the wrong page. Constructor arguments are not copied again: `init` enters +its normal frame and reads them where the caller pushed them. + ## Mappings | State | CR | RCR (low bits) | P0 | P1 | IRQs | diff --git a/newLang/planning/claims/E04-S04.md b/newLang/planning/claims/E04-S04.md new file mode 100644 index 0000000..3c95037 --- /dev/null +++ b/newLang/planning/claims/E04-S04.md @@ -0,0 +1,98 @@ +--- +story: E04-S04 +owner: e04_control +status: done +started: 2026-11-09 +depends-on: [E04-S03, E03-S03] +--- + +# E04-S04 Claim + +## Intended change + +Add `src/compiler/flow.s`, the control-flow layer above the scalar emitter: +labels, absolute jumps for loop back edges, conditional branches with a fixed +five-byte short/long encoding, calls that discard frame arguments, and +prologue/epilogue helpers for the software-frame ABI. Add a counting mode to +`codegen.s` so one pass measures a routine without writing the buffer and the +next pass emits it; both passes must produce identical label offsets and +lengths. The x128 fixture generates a nested routine (while, if/else, for, +call, return), sizes it twice, verifies both branch encodings in memory, and +executes it. + +The story is limited to the emitter: parser-to-emitter wiring, method address +assignment (`sizing.s`), and compiler pass 3 remain open and are recorded as +remaining risk. + +## File area + +- `src/compiler/flow.s`, `src/common/flow.inc` (new) +- `src/compiler/codegen.s` (counting mode), `src/runtime/frames.s` + (`frame_discard` preserves A/X) +- `tests/codegen/control-test.s` (new) +- `scripts/build-control-test.ps1`, `scripts/verify-control-test.ps1` (new) +- `docs/decisions.md`, `docs/architecture.md` +- `planning/claims/E04-S04.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] Emits `if`, `else`, `while`, `for`, calls, and `return`: the generated + routine runs with the expected mailbox signature `A5 11 05 0A 04 5A 00 00 03` + (status PASS, return 17, a 5, if/else path 10, i 4, long-branch result 5A, + wrong path 0, zero sizing mismatches, both branch forms verified). +- [x] Pass-2 sizing matches emission: the fixture runs the same generator with + `FL_MODE_COUNT` (label offsets recorded, no stores) and `FL_MODE_EMIT`, then + compares every main-routine label offset and the total length; mismatches 0. +- [x] Long branches are correct: a conditional branch over 125 bytes of filler + is patched as the inverted `BEQ +3` / `JMP`, jumps over the wrong path + (`v_bad` stays 0), and reaches the target (`v_long` is `$5A`). +- [x] Nested-control fixture executes expected mailbox signatures: while exits + at 5, if/else takes the then path, for reaches 4 and sums 16, `add(sum)` + returns 17 through the frame ABI. + +## Coordination notes + +- E04-S03 is done; this story uses its scalar emitter and adds the control + transfer it deferred. +- D131 records the emitter contract, including the long-form opcode inversion + and the result-preserving call/return helpers. +- `frame_discard` now preserves A/X; the earlier implementation left its count + in A, which destroyed a callee's result during argument cleanup. The frame + regression still passes. +- Compiler pass 2/3 integration is not attempted here: `sizing.s`, method + addresses, and parser statement emission remain the next backend story. + +## Work log + +- Added count mode to `cg_put`; `cg_init` clears it and `fl_init` sets it. +- Added label/fixup tables (16 labels, 32 fixups), absolute JMP patching, and + five-byte conditional branches. The long form inverts the branch opcode at + patch time so a true condition falls through to the JMP. +- Added `fl_call`/`fl_call_byte`/`fl_call_word`: JSR, save the result on the + hardware stack, discard argument bytes, restore. Added `fl_ret`, + `fl_ret_byte`, and `fl_ret_word`, preserving results across `frame_leave`. +- Fixed an infinite filler loop in the fixture: the emit macros clobber X, so + the filler counter moved to BSS. +- Fixed the fixture's branch-offset capture: `cg_here` is a routine, so it must + be called (`jsr cg_here`), not read as a variable. This was why the in-memory + branch-form checks inspected the wrong byte. + +## Completion + +- Tests run: + - `scripts/verify-control-test.ps1` + - `scripts/verify-frame-test.ps1 -Port 6541` + - `scripts/verify-scalar-test.ps1 -Port 6542` +- Results: control PASS (`4E 38 03 A5 00 00 00 00 11 05 0A 04 5A 00 00 03`); + frame PASS; scalar PASS with its unchanged 231-byte golden stream. +- Files changed: `src/compiler/flow.s`, `src/common/flow.inc`, + `src/compiler/codegen.s`, `src/runtime/frames.s`, `tests/codegen/control-test.s`, + `scripts/build-control-test.ps1`, `scripts/verify-control-test.ps1`, + `docs/decisions.md`, `docs/architecture.md`, this claim. +- Remaining risks: parser statement emission and pass-2 address assignment are + not wired yet; labels (16), fixups (32), and the 255-byte code buffer bound + each routine; `fl_branch` targets must be forward (loops use `fl_jmp`); + interface dispatch calls and void calls through the control layer are not + separately exercised; the long-branch expansion adds three NOPs to short + branches. diff --git a/newLang/planning/claims/E04-S05.md b/newLang/planning/claims/E04-S05.md new file mode 100644 index 0000000..5cbeca1 --- /dev/null +++ b/newLang/planning/claims/E04-S05.md @@ -0,0 +1,120 @@ +--- +story: E04-S05 +owner: e04_runtime +status: done +started: 2026-11-09 +depends-on: [E04-S03, E01-S04] +--- + +# E04-S05 Claim + +## Intended change + +Deliver the runtime half of print and runtime faults: PETSCII text and +scalar output, stable R-codes, and a mailbox/screen fault report, with an x128 +fixture that checks the rendered screen and the mailbox. The increment also +proves one source-to-code path: `hello.n8` compiles to a PRG through a pass-2 +string capture and a fixture-only emitter, then runs in x128. General +parser-to-codegen integration, `sizing.s`, and compiler pass 3 remain separate +backend work. + +## File area + +- `src/runtime/print.s`, `src/common/print.inc` (new) +- `src/runtime/faults.s`, `src/common/faults.inc` (new) +- `src/compiler/parser.s` (pass-2 print capture) +- `tests/runtime/print-test.s` (new) +- `tests/compiler/hello.n8`, `tests/compiler/hello-test.s` (new) +- `scripts/build-print-test.ps1`, `scripts/verify-print-test.ps1` (new) +- `scripts/build-hello-test.ps1`, `scripts/verify-hello-test.ps1` (new) +- `docs/decisions.md`, `docs/architecture.md` +- `planning/claims/E04-S05.md` +- generated files under `build/print-test/` and `build/hello-test/` + +## Acceptance evidence + +- [x] Prints literals and every supported scalar on tested displays: the + fixture renders `TEXT OK` (string literal), `BYTE 0 7 255`, + `WORD 0 12345 65535`, `INT -1 -32768 32767`, `BOOL FALSE TRUE`, and + `CHAR A` in x128's VIC-II display; the host compares all twelve screen rows. +- [x] Null, bounds, divide, stack, and heap faults have stable codes: R001, + R002, R003, R004, and R005 are printed as `RUNTIME FAULT R00n`, and the + mailbox ends with code 5 and address `$1234`. +- [x] Runtime messages do not corrupt the program's memory mapping: the + fixture compares CR to `$0E` and RCR to its saved value after all print and + fault calls, and records 1 in `MB_DATA` only when both match. +- [x] `hello.n8` compiles and runs end to end in x128: the compile fixture + loads `HELLO.N8` from a D81, parses it, captures the single string literal, + and emits a 118-byte `HELLO.PRG` (`$1C01` load, `$1C0D` entry). A second + x128 session autostarts the extracted PRG and displays `HELLO FROM NOVA128` + on row 13. The emitter is fixture-only: it handles one print of one string + literal and does not call `out_startup` or the general code generator. + +## Coordination notes + +- D132 records the output formats, the R-code namespace, and the direct + `CHROUT` contract. It builds on D129 (generated code runs at CR `$0E`) and + changes no memory map. +- The runtime must not be called from a compiler state with relocated zero + page: `print.s` calls the KERNAL without a wrapper and uses `(pr_ptr),y`. +- Fault reporting returns to its caller. Unwinding a fault through frames is + deferred to the frame/exception work. +- E04-S03's `AR_FAULT_DIV0` now has a stable external code (`FLT_DIV0` = R003); + arithmetic itself is unchanged. +- The hello path uses the D128 container (stub at `$1C01`, body at `$1C0D`) + but bypasses `startup.s`; it is a bounded proof, not the general emitter. + The fixture must set `out_load` and `out_entry` before `out_begin`, because + `output.s` does not initialize them. +- `HELLO.N8` is stored with unshifted PETSCII name bytes (`$48...`), so the + build script writes the host file with a lowercase name; c1541 maps it to + those bytes. The true-drive D81 must be allowed to settle before exit or the + PRG's data sectors are not flushed to the image. + +## Work log + +- Added `print.s`: `pr_char` (preserves X/Y), `pr_cr`, `pr_text`, + minimal-decimal `pr_byte`/`pr_word`/`pr_int`, and `pr_bool`. +- Moved `pr_ptr` to the zero-page segment: indirect indexed addressing needs a + zero-page operand, which ca65 rejected from BSS. +- Added `faults.s`: mailbox code/address then `MB_ST_FAIL`, a fixed + `RUNTIME FAULT R` prefix, three-digit code, and CR. +- Added the fixture with a screen clear, all scalar formats, all five faults, + and a CR/RCR preservation check. +- Fixed the fixture's mapping constant to `CR_RAM0_KERNAL` (the c128.inc + spelling) and checked that the linked image ends below `$4000` in the build + script. +- Added D132 and architecture section 7.8. +- Added pass-2 print capture to `parser.s`: `gen_text`/`gen_text_len`, + `gen_print_count`, and `gen_bad`; a print accepts one string-literal item and + marks unsupported shapes. Parser code growth required near-branch + trampolines around the literal path for ca65's +/-128 range. +- Added `hello-test.s` and its build/verify scripts. The compile fixture sets + `out_load`/`out_entry`, emits the D128 stub and a body that saves CR, selects + `$0E`, calls `CHROUT` per captured byte, restores CR, and returns to BASIC. +- Added architecture section 7.9 and updated D132. + +## Completion + +- Tests run: + - `scripts/verify-print-test.ps1` (build plus x128 run) + - `scripts/verify-hello-test.ps1` (compile session, D81 extraction, x128 run) + - `scripts/verify-parser-test.ps1 -Port 6551` (41 fixtures) + - `git diff --check` +- Results: all twelve print/fault screen rows match; the print mailbox has + producer 3, status FA, code 5, address `$1234`, and mapping preserved. The + hello mailbox has stage 2, status A5, one 18-byte print, and no output error; + `HELLO.PRG` is 118 bytes and displays on row 13. All 41 parser fixtures pass + (lowest expression stack pointer `$A7`). +- Files changed: `src/runtime/print.s`, `src/common/print.inc`, + `src/runtime/faults.s`, `src/common/faults.inc`, `src/compiler/parser.s`, + `tests/runtime/print-test.s`, `tests/compiler/hello.n8`, + `tests/compiler/hello-test.s`, the four build/verify scripts, + `docs/decisions.md`, `docs/architecture.md`, this claim. +- Remaining risks: the hello emitter handles only one literal string print, + with no expressions, calls, or startup section; general parser-to-codegen + integration and compiler pass 3 remain open. Only the 40-column VIC-II + display is tested. The capture buffer is bounded; longer or unsupported + print shapes stop at the unsupported stage rather than truncating silently. + Print/fault routines are not reentrant and assume the system zero page; + fault unwinding and best-address selection are not implemented; generated + code and BSS must stay below `$4000` under CR `$0E`. diff --git a/newLang/planning/claims/E05-S01.md b/newLang/planning/claims/E05-S01.md new file mode 100644 index 0000000..a7a5644 --- /dev/null +++ b/newLang/planning/claims/E05-S01.md @@ -0,0 +1,87 @@ +--- +story: E05-S01 +owner: e05_heap +status: done +started: 2026-11-09 +depends-on: [E04-S05, E01-S05] +--- + +# E05-S01 Claim + +## Intended change + +Add a deterministic free-list heap for generated programs. `heap_init` accepts +an explicit page-aligned base and page count, so heap placement remains +independent of the still-open generated-program map. Blocks use a four-byte +header (total size with bit 15 allocated, and a next pointer that is a magic +value while allocated), two-byte payload alignment, first-fit allocation with +splitting, payload zeroing, address-ordered free insertion, and coalescing. +`heap_free` validates range, size, the used magic, and double-free; validation +is always on rather than a separately compiled debug build. A bounded x128 +fixture exercises allocation, zeroing, splitting, coalescing, a multi-block +stress pass, double-free and corrupt-header detection, and exhaustion without +damaging live allocations. The fixture maps exhaustion to stable R005 through +`flt_report`; wiring `new` to the heap is E05-S03. + +## File area + +- `src/runtime/heap.s`, `src/common/heap.inc` (new) +- `tests/runtime/heap-test.s` (new) +- `scripts/build-heap-test.ps1`, `scripts/verify-heap-test.ps1` (new) +- `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md` +- `planning/claims/E05-S01.md` +- generated files under `build/heap-test/` + +## Acceptance evidence + +- [x] Allocates, zeroes, frees, splits, and coalesces blocks deterministically. +- [x] Heap exhaustion faults without corrupting allocated blocks. +- [x] Debug mode detects representative double-free/corrupt-header cases. +- [x] Stress test returns the heap to its initial free capacity. + +## Coordination notes + +- E04-S05 and E01-S05 are complete; E04-S04/S05 remain uncommitted and are not + discarded or modified. +- The generated-program heap location is still open. This story supplies the + allocator and a test instance at bank-0 `$4000-$4FFF` under CR `$0E`, not a + final generated-program map. +- D133 records the provisional block/API contract. The allocator returns + `HEAP_ERR_*` on exhaustion; the test calls `flt_report` to demonstrate the + stable R005 path, while E05-S03 owns `new` integration. + +## Work log + +- Selected E05-S01 as the next dependency-complete story after E04-S05. +- Added `heap.s`/`heap.inc` with an explicit page-granular region, first-fit + splitting, payload zeroing, sorted free insertion, coalescing, and always-on + validation. +- Added the 4K x128 fixture at `$4000-$4FFF`; code ends at `$28C4`. +- First build exposed ca65 range errors on the far error paths; converted them + to inverted short branches plus `JMP`. +- The first run failed phase 2 because `heap_zero` branched to its tail path + before loading A=0, so payload bytes were not zeroed. Fixed by loading A=0 + at the tail entry, then reran successfully. + +## Completion + +- Tests run: + - `scripts/build-heap-test.ps1` (link ends at `$28C4`) + - `scripts/verify-heap-test.ps1` (x128 3.10) + - `git diff --check` +- Results: the fixture reports producer 3, status `$FA`, code 5, address + `$4000`, and all eight phase flags `01`; the screen shows + `NOVA128 HEAP TEST`, `HEAP OK`, `RUNTIME FAULT R005`. It verified allocation, + zeroing, first-fit splitting, sorted insertion, repeated coalescing, the + 16-block stress/free cycle, double-free rejection, corrupt-magic rejection, + exhaustion without damaging live patterns, and return to 4096 free bytes. +- Files changed: `src/runtime/heap.s`, `src/common/heap.inc`, + `tests/runtime/heap-test.s`, `scripts/build-heap-test.ps1`, + `scripts/verify-heap-test.ps1`, `docs/decisions.md`, `docs/architecture.md`, + `docs/memory-map.md`, this claim. +- Remaining risks: production generated-program heap placement is still open; + the canonical region is 4K and uses 15-bit block sizes; validation is always + on (no release/debug toggle); fragmentation is solved by first-fit plus + coalescing but no worst-case performance bound is measured. `HEAP_ERR_OOM` + is returned to the caller, not automatically reported; E05-S03 owns the + `new`/R005 integration. Banked heaps remain deferred. diff --git a/newLang/planning/claims/E05-S02.md b/newLang/planning/claims/E05-S02.md new file mode 100644 index 0000000..3da8176 --- /dev/null +++ b/newLang/planning/claims/E05-S02.md @@ -0,0 +1,91 @@ +--- +story: E05-S02 +owner: e05_objects +status: done +started: 2026-11-09 +depends-on: [E03-S05, E05-S01] +--- + +# E05-S02 Claim + +## Intended change + +Freeze the object/class descriptor ABI and make it visible in generated MAP +output. A class descriptor is an 8-byte packed record containing the instance +size, an optional destructor address, an interface count, a pointer to an +interface-directory array, and reserved flags that must be zero. Objects start +with a two-byte pointer to their descriptor; fields follow at offset 2. + +Add `src/runtime/objects.s` helpers to read an object's descriptor, size, +destructor, and interface directory. Extend `output.s` with `out_class` records +and deterministic `CLASS name size destroy count ifdir` MAP lines. There is no +parent-class slot and no inheritance path. + +## File area + +- `src/common/class.inc`, `src/runtime/objects.s` (new) +- `src/common/formats.inc`, `src/compiler/output.s` +- `tests/runtime/class-test.s` +- `scripts/build-class-test.ps1`, `scripts/verify-class-test.ps1` +- `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md` +- `planning/claims/E05-S02.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] Descriptor identifies instance size and optional destructor: the + `EMPTY` descriptor reports size 7 and destructor 0; `BOX` reports size 35 and + a nonzero destructor. `obj_size`/`obj_destroy` read both values from the + descriptor. +- [x] Every object begins with a valid descriptor pointer: both fixture objects + store their descriptor at +0; `obj_class` rejects a null object/pointer and + returns the descriptor for both real objects. +- [x] Descriptor/MAP output is deterministic: the fixture emits + `CLASS EMPTY 0007 0000 00 0000` and + `CLASS BOX 0023 01 ` after the section records, with actual + label addresses supplied by the linker. The verifier compares the complete + MAP and the host reruns the fixture on a freshly built disk. +- [x] Format reserves a documented path to interface-directory data without + introducing class inheritance: descriptor offset +4 is the interface count, + +5..6 points to 4-byte interface/method-table entries, and +7 flags must be + zero. There is no parent-class slot; `class.inc` records this ABI. + +## Work log + +- Selected E05-S02 after E05-S01; no other claim was active. +- Added the 8-byte descriptor and object header constants, plus read-only + descriptor accessors for size, destructor, and interface directory. +- Extended `output.s` with `out_class`, initialized class-record storage, and + appended deterministic `CLASS` MAP lines after sections. Converted the new + writer's long error branches to short branches plus `JMP` trampolines. +- Added the x128 fixture with a static EMPTY object and a BOX object whose + descriptor has a destructor and one interface-directory entry. It writes + `CLASSTEST.PRG/MAP` through the real writer. +- Corrected the fixture's mailbox producer to `MB_PROD_PROGRAM`; corrected the + verifier to use the drive's lowercase `classtest.map` name. + +## Completion + +- Tests run: + - `scripts/build-class-test.ps1` + - `scripts/verify-class-test.ps1 -Port 6552` (repeated on fresh disks) + - `scripts/verify-output-test.ps1 -Port 6530 -RunPort 6531` + - `git diff --check` +- Results: the class fixture reports producer 3, status `$A5`, code 0, and all + eight descriptor/object/MAP flags set. The screen shows + `NOVA128 CLASS TEST OK`. The complete MAP is identical to the expected + six-line record, including `CLASS EMPTY 0007 0000 00 0000` and + `CLASS BOX 0023 01 `. The existing output test still + passes with its 133-byte PRG and unchanged section MAP. +- Files changed: `src/common/class.inc`, `src/runtime/objects.s`, + `src/common/formats.inc`, `src/compiler/output.s`, + `tests/runtime/class-test.s`, `scripts/build-class-test.ps1`, + `scripts/verify-class-test.ps1`, `docs/decisions.md`, + `docs/architecture.md`, `docs/memory-map.md`, this claim. +- Remaining risks: production parser/codegen does not yet call `out_class` or + emit class descriptors; the MAP exposes descriptor records but not + field-by-field offset lines, which remain the parser layout record until + codegen pass 3. The descriptor flags, interface descriptor pointer, and + method-table pointer are reserved for later interface work. The fixture uses + static synthetic objects, so construction/deletion remains E05-S03. Tests + run against a fresh true-drive D81. diff --git a/newLang/planning/claims/E05-S03.md b/newLang/planning/claims/E05-S03.md new file mode 100644 index 0000000..90f23c6 --- /dev/null +++ b/newLang/planning/claims/E05-S03.md @@ -0,0 +1,102 @@ +--- +story: E05-S03 +owner: e05_objects +status: done +started: 2026-11-09 +depends-on: [E05-S02, E04-S02] +--- + +# E05-S03 Claim + +## Intended change + +Add the runtime construction/deletion layer above the E05-S01 heap and E05-S02 +descriptor. `obj_new` allocates the descriptor's instance size, stores the +descriptor pointer in the object header, and calls a caller-supplied `init` +entry once after a successful allocation. Constructor arguments stay on the +software frame stack, so `init` can enter its normal frame and read them. +Allocation failure returns `OBJ_ERR_HEAP` without calling `init`. + +`obj_delete` treats null as a successful no-op, reads the optional destructor +from the descriptor, calls it with the object as `self`, then returns the whole +object to the heap. A small synthetic-return indirect-call helper invokes the +runtime-selected destructor without relying on `JSR (address)`, which the 8502 +does not have. + +## File area + +- `src/runtime/construct.s` (new) +- `src/common/class.inc` (construction error codes) +- `tests/runtime/object-test.s` +- `scripts/build-object-test.ps1`, `scripts/verify-object-test.ps1` +- `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md` +- `planning/claims/E05-S03.md` +- generated files under `build/` + +## Acceptance evidence + +- [x] `new` allocates, initializes the header, then calls `init` once: the + fixture's five-byte `Res` object gets a nonzero heap pointer, `obj_class` + reads back `desc_res` from object+0, and `init_count` is 1. Both fields are + zero at init entry, so the heap's zeroing happens before the header is + written and before init runs. +- [x] Failed allocation does not call `init`: `desc_huge` (`$FFFF` bytes) makes + `obj_new` return C=1 with `obj_error = OBJ_ERR_HEAP` and leaves + `init_count` at 1. +- [x] `delete null` is a no-op: `obj_delete(0)` returns C=0 with + `obj_error = OBJ_OK` and `destroy_count` still 0. +- [x] `delete` calls `destroy` once and frees the complete object: the + destructor runs exactly once with A/X = the object, reads back tag `$2A` and + word `$1234` written by init, and `heap_free_total` returns to the initial + `$1000` once the instance is freed. A class with no destructor also returns + the full capacity. +- [x] Constructor arguments and destructor side effects have end-to-end tests: + `init(value as word, tag as byte)` reads its two arguments from `FP+2` in the + fixture and writes them into the object; the destructor's recorded self and + field values are checked after deletion. + +## Work log + +- Selected E05-S03 after E05-S02; no other claim was active. +- Added `obj_new`, `obj_delete`, and `obj_call`, plus `obj_init_ptr` and + `obj_error`, and the `OBJ_OK`/`OBJ_ERR_HEAP`/`OBJ_ERR_BAD` codes in + `class.inc`. Construction lives in its own module so the E05-S02 class test + still links `objects.s` without the heap. +- First x128 run hung: `obj_new` handed the object pointer to `obj_call` + instead of the init entry, so it jumped into the zeroed heap and executed a + BRK. `obj_call` now takes its entry from zero-page `co_vec` and preserves + A/X, which also leaves A/X free for the callee's `self`. +- Second run failed phase 3: after the null test, `obj_delete` passed `low|high` + to `obj_class`, reading the descriptor from the wrong address. Reloading the + object pointer before the accessor fixed it. +- Replaced the branches that outgrew the +/-128-byte range with the project's + short-branch plus `JMP` trampoline pattern. +- Removed the temporary mailbox breadcrumbs used to locate the two defects + before recording the final runs. + +## Completion + +- Tests run: + - `scripts/build-object-test.ps1` (PRG ends at `$28F7`, below the `$4000` + heap base) + - `scripts/verify-object-test.ps1` + - `scripts/verify-class-test.ps1` + - `scripts/verify-output-test.ps1` + - `scripts/verify-heap-test.ps1` + - `git diff --check` +- Results: the object fixture reports producer 3, status `$A5`, code 0, all + five construction/deletion flags set, and shows `OBJECT OK`. The class test + still emits `CLASS EMPTY 0007 0000 00 0000` and + `CLASS BOX 0023 2935 01 2A47`; the output test still passes with its 133-byte + PRG; the heap test still passes with its R005 exhaustion fault. +- Files changed: `src/runtime/construct.s`, `src/common/class.inc`, + `tests/runtime/object-test.s`, `scripts/build-object-test.ps1`, + `scripts/verify-object-test.ps1`, `docs/decisions.md` (D135), + `docs/architecture.md` (8.3), `docs/memory-map.md`, this claim. +- Remaining risks: no parser-to-codegen path emits `new`/`delete` yet, so the + R005 mapping for a failed `new` still belongs to codegen (D133). The heap + region stays caller-chosen. `obj_call` relies on `co_vec` not ending at + `$xxFF` (link-time assertion) and spends two hardware-stack bytes per call. + Constructor argument count is bounded only by the frame stack. The fixture + runs against a fresh true-drive D81 in x128; physical C128 testing remains + P2 validation. diff --git a/newLang/scripts/build-class-test.ps1 b/newLang/scripts/build-class-test.ps1 new file mode 100644 index 0000000..176d592 --- /dev/null +++ b/newLang/scripts/build-class-test.ps1 @@ -0,0 +1,23 @@ +# E05-S02: build the class descriptor/object fixture and its x128 disk. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force + +$out = Join-Path $project "build\class-test" +$link = Invoke-Nova128Link -Name "class-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\compiler\mmu.s"), + (Join-Path $project "src\compiler\diskio.s"), + (Join-Path $project "src\compiler\output.s"), + (Join-Path $project "src\runtime\startup.s"), + (Join-Path $project "src\runtime\objects.s"), + (Join-Path $project "tests\runtime\class-test.s")) + +New-Nova128Disk -Path (Join-Path $out "class-test.d81") -Files @( + @{ Name = "CLASSTEST"; Path = $link.Program; Type = "prg" }) + +Write-Host ("built {0}" -f $link.Program) +Write-Host ("disk {0}" -f (Join-Path $out "class-test.d81")) diff --git a/newLang/scripts/build-control-test.ps1 b/newLang/scripts/build-control-test.ps1 new file mode 100644 index 0000000..533ecb8 --- /dev/null +++ b/newLang/scripts/build-control-test.ps1 @@ -0,0 +1,17 @@ +# E04-S04: build the control-flow emitter fixture. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force + +$out = Join-Path $project "build\control-test" +$link = Invoke-Nova128Link -Name "control-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\compiler\codegen.s"), + (Join-Path $project "src\compiler\flow.s"), + (Join-Path $project "src\runtime\frames.s"), + (Join-Path $project "tests\codegen\control-test.s")) + +Write-Host ("built {0}" -f $link.Program) diff --git a/newLang/scripts/build-heap-test.ps1 b/newLang/scripts/build-heap-test.ps1 new file mode 100644 index 0000000..9106d15 --- /dev/null +++ b/newLang/scripts/build-heap-test.ps1 @@ -0,0 +1,25 @@ +# E05-S01: build the free-list heap fixture and its D81. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\heap-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$link = Invoke-Nova128Link -Name "heap-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\runtime\print.s"), + (Join-Path $project "src\runtime\faults.s"), + (Join-Path $project "src\runtime\heap.s"), + (Join-Path $project "tests\runtime\heap-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } +$end = Get-Nova128Label $link.Labels "test_end" +if ($end -ge 0x4000) { + throw ("heap-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000") +} + +New-Nova128Disk -Path (Join-Path $out "heap-test.d81") -Files @( + @{ Name = "heap-test"; Path = $link.Program; Type = "prg" }) +Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) diff --git a/newLang/scripts/build-hello-test.ps1 b/newLang/scripts/build-hello-test.ps1 new file mode 100644 index 0000000..eae2c01 --- /dev/null +++ b/newLang/scripts/build-hello-test.ps1 @@ -0,0 +1,32 @@ +# E04-S05: build the hello compiler fixture and a D81 holding HELLO.N8. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\hello-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$link = Invoke-Nova128Link -Name "hello-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\compiler\mmu.s"), + (Join-Path $project "src\compiler\diskio.s"), + (Join-Path $project "src\compiler\source.s"), + (Join-Path $project "src\compiler\lexer.s"), + (Join-Path $project "src\compiler\names.s"), + (Join-Path $project "src\compiler\diagnostics.s"), + (Join-Path $project "src\compiler\parser.s"), + (Join-Path $project "src\compiler\interfaces.s"), + (Join-Path $project "src\compiler\types.s"), + (Join-Path $project "src\compiler\output.s"), + (Join-Path $project "src\runtime\startup.s"), + (Join-Path $project "tests\compiler\hello-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } + +$pet = Join-Path $out "hello.pet" +[IO.File]::WriteAllBytes($pet, (ConvertTo-Nova128Petscii (Get-Content -Raw -LiteralPath (Join-Path $project "tests\compiler\hello.n8")))) +New-Nova128Disk -Path (Join-Path $out "hello-test.d81") -Files @( + @{ Name = "hello-test"; Path = $link.Program; Type = "prg" }, + @{ Name = "hello.n8"; Path = $pet; Type = "seq" }) # lowercase -> unshifted PETSCII +Write-Host ("built {0}" -f $link.Program) diff --git a/newLang/scripts/build-object-test.ps1 b/newLang/scripts/build-object-test.ps1 new file mode 100644 index 0000000..c18f8aa --- /dev/null +++ b/newLang/scripts/build-object-test.ps1 @@ -0,0 +1,27 @@ +# E05-S03: build the object construction/deletion fixture and its D81. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\object-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$link = Invoke-Nova128Link -Name "object-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\runtime\print.s"), + (Join-Path $project "src\runtime\heap.s"), + (Join-Path $project "src\runtime\frames.s"), + (Join-Path $project "src\runtime\objects.s"), + (Join-Path $project "src\runtime\construct.s"), + (Join-Path $project "tests\runtime\object-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } +$end = Get-Nova128Label $link.Labels "test_end" +if ($end -ge 0x4000) { + throw ("object-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000") +} + +New-Nova128Disk -Path (Join-Path $out "object-test.d81") -Files @( + @{ Name = "object-test"; Path = $link.Program; Type = "prg" }) +Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) diff --git a/newLang/scripts/build-print-test.ps1 b/newLang/scripts/build-print-test.ps1 new file mode 100644 index 0000000..60b7708 --- /dev/null +++ b/newLang/scripts/build-print-test.ps1 @@ -0,0 +1,24 @@ +# E04-S05: build the print/fault runtime test and its D81. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\print-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$link = Invoke-Nova128Link -Name "print-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\runtime\print.s"), + (Join-Path $project "src\runtime\faults.s"), + (Join-Path $project "tests\runtime\print-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } +$end = Get-Nova128Label $link.Labels "test_end" +if ($end -ge 0x4000) { + throw ("print-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000 for the CR=`$00 mapping") +} + +New-Nova128Disk -Path (Join-Path $out "print-test.d81") -Files @( + @{ Name = "print-test"; Path = $link.Program; Type = "prg" }) +Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) diff --git a/newLang/scripts/verify-class-test.ps1 b/newLang/scripts/verify-class-test.ps1 new file mode 100644 index 0000000..184b342 --- /dev/null +++ b/newLang/scripts/verify-class-test.ps1 @@ -0,0 +1,76 @@ +# E05-S02: run the class descriptor/object fixture in x128 and verify the +# descriptor ABI, object headers, and deterministic CLASS MAP records. +[CmdletBinding()] +param([int]$Port = 6552, [int]$TimeoutSeconds = 90) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-class-test.ps1") + +$out = Join-Path $project "build\class-test" +$disk = Join-Path $out "class-test.d81" +$labels = Join-Path $out "class-test.lbl" +$c1541 = Get-ViceTool "c1541" +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "class-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds + Start-Sleep -Milliseconds 500 + $screenRam = Read-Nova128Memory $session 0x0400 1000 +} finally { + Stop-Nova128X128 $session -SettleSeconds 0 +} + +if ($mb[2] -ne 0x03) { $failures += "producer $($mb[2]) expected 3" } +if ($mb[3] -ne 0xA5) { $failures += ("status {0:X2} expected A5" -f $mb[3]) } +if ($mb[4] -ne 0 -or $mb[5] -ne 0) { $failures += "code $($mb[4] + 256 * $mb[5]) expected 0" } +for ($i = 0; $i -lt 8; $i++) { + if ($mb[8 + $i] -ne 1) { $failures += "phase $i flag is $($mb[8 + $i]), expected 1" } +} + +$rows = for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + (-join $chars).TrimEnd() +} +if (-not ($rows -contains "NOVA128 CLASS TEST OK")) { + $failures += "screen does not contain 'NOVA128 CLASS TEST OK'" +} + +function Read-DiskFile([string]$Name, [string]$Dest, [bool]$Seq) { + if (Test-Path -LiteralPath $Dest) { Remove-Item -LiteralPath $Dest -Force } + $spec = if ($Seq) { "$Name,s" } else { $Name } + & $c1541 -attach $disk -read $spec $Dest 2>&1 | Out-Null + if (-not (Test-Path -LiteralPath $Dest)) { throw "could not read $Name from the test disk" } +} + +$mapPath = Join-Path $out "CLASSTEST.MAP" +Read-DiskFile "classtest.map" $mapPath $true +$mapLines = ([IO.File]::ReadAllText($mapPath)).TrimEnd("`r", "`n") -split "`r`n|`r|`n" +$destroy = Get-Nova128Label $labels "destroy_b" +$ifdir = Get-Nova128Label $labels "iface_dir" +$expectedMap = @( + "NOVA128 MAP 1", + "ENTRY 1C0D", + "SECTION STUB 1C01 000C", + "SECTION START 1C0D 004A", + ("CLASS EMPTY 0007 0000 00 0000"), + ("CLASS BOX 0023 {0:X4} 01 {1:X4}" -f $destroy, $ifdir)) +if (($mapLines -join "`n") -ne ($expectedMap -join "`n")) { + $failures += ("CLASSTEST.MAP differs:`n got: " + ($mapLines -join "`n ") + + "`n want: " + ($expectedMap -join "`n ")) +} + +if ($failures) { + throw ("class verification failed:`n " + ($failures -join "`n ")) +} +Write-Host "screen:" +$rows | Where-Object { $_ } | ForEach-Object { Write-Host " |$_" } +Write-Host ("mailbox: producer 3, PASS, all 8 descriptor/MAP flags set; destroy {0:X4}, ifdir {1:X4}" -f $destroy, $ifdir) +Write-Host "MAP:" +$expectedMap | ForEach-Object { Write-Host " |$_" } diff --git a/newLang/scripts/verify-control-test.ps1 b/newLang/scripts/verify-control-test.ps1 new file mode 100644 index 0000000..2cf238e --- /dev/null +++ b/newLang/scripts/verify-control-test.ps1 @@ -0,0 +1,42 @@ +# E04-S04: run the control-flow fixture and check sizing, branch forms, and +# the executed if/else/while/for/call/return results. +[CmdletBinding()] +param( + [int]$Port = 6530, + [int]$TimeoutSeconds = 90 +) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-control-test.ps1") + +$out = Join-Path $project "build\control-test" +$prg = Join-Path $out "control-test.prg" +$session = Start-Nova128X128 -Autostart $prg -Port $Port ` + -Screenshot (Join-Path $out "control-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds +} finally { + Stop-Nova128X128 $session +} + +Write-Host ('mailbox: ' + (($mb | ForEach-Object { $_.ToString('X2') }) -join ' ')) +Write-Host (("results: return={0} a={1} path={2} i={3} long={4:X2} bad={5} mismatches={6} forms={7:X2}" -f ` + $mb[8], $mb[9], $mb[10], $mb[11], $mb[12], $mb[13], $mb[14], $mb[15])) + +$failures = @() +if ($mb[2] -ne 0x03) { $failures += "producer $($mb[2]) is not a generated program" } +if ($mb[3] -ne 0xA5) { $failures += "fixture status is not PASS" } +if ($mb[4] -ne 0 -or $mb[5] -ne 0) { $failures += "code is $($mb[4] + 256 * $mb[5])" } +if ($mb[8] -ne 17) { $failures += "program result is $($mb[8]), expected 17" } +if ($mb[9] -ne 5) { $failures += "while/a is $($mb[9]), expected 5" } +if ($mb[10] -ne 10) { $failures += "if/else path is $($mb[10]), expected 10" } +if ($mb[11] -ne 4) { $failures += "for/i is $($mb[11]), expected 4" } +if ($mb[12] -ne 0x5A) { $failures += ('long branch result is {0:X2}, expected 5A' -f $mb[12]) } +if ($mb[13] -ne 0) { $failures += "long branch ran the wrong path" } +if ($mb[14] -ne 0) { $failures += "$($mb[14]) sizing label/length mismatches" } +if ($mb[15] -ne 3) { $failures += ('branch forms are {0:X2}, expected 03 (short and long verified)' -f $mb[15]) } + +if ($failures) { throw ("control verification failed:`n " + ($failures -join "`n ")) } +Write-Host "verified sizing pass, short/long branches, if/else, while, for, call, and return" diff --git a/newLang/scripts/verify-heap-test.ps1 b/newLang/scripts/verify-heap-test.ps1 new file mode 100644 index 0000000..aeff07f --- /dev/null +++ b/newLang/scripts/verify-heap-test.ps1 @@ -0,0 +1,55 @@ +# E05-S01: run the free-list heap fixture and check its phase flags, R005 +# exhaustion fault, and screen output. +[CmdletBinding()] +param([int]$Port = 6547, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-heap-test.ps1") + +$out = Join-Path $project "build\heap-test" +$disk = Join-Path $out "heap-test.d81" +$expected = @( + "NOVA128 HEAP TEST", + "HEAP OK", + "RUNTIME FAULT R005") +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "heap-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds + Start-Sleep -Seconds 1 + $screenRam = Read-Nova128Memory $session 0x0400 1000 +} finally { + Stop-Nova128X128 $session -SettleSeconds 0 +} + +if ($mb[2] -ne 0x03) { $failures += ("producer $($mb[2]) expected 3 (generated program)") } +if ($mb[3] -ne 0xFA) { $failures += ("status ${0:X2} expected FA (failure)" -f $mb[3]) } +if ($mb[4] -ne 5 -or $mb[5] -ne 0) { $failures += ("code {0} expected 5 (heap)" -f ($mb[4] + 256 * $mb[5])) } +$addr = $mb[6] + 256 * $mb[7] +if ($addr -ne 0x4000) { $failures += ("address ${0:X4} expected 4000" -f $addr) } +for ($i = 0; $i -lt 8; $i++) { + if ($mb[8 + $i] -ne 1) { $failures += ("heap phase {0} flag is {1}, expected 1" -f ($i + 1), $mb[8 + $i]) } +} + +$rows = for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + (-join $chars).TrimEnd() +} +for ($r = 0; $r -lt $expected.Count; $r++) { + if ($rows[$r] -cne $expected[$r]) { + $failures += "screen row ${r}: '$($rows[$r])' expected '$($expected[$r])'" + } +} + +if ($failures) { throw ("heap verification failed:`n " + ($failures -join "`n ")) } +Write-Host "screen:" +$rows[0..($expected.Count - 1)] | ForEach-Object { Write-Host " |$_" } +Write-Host ("mailbox: producer 3, status FA, code 5, address {0:X4}, flags 01 01 01 01 01 01 01 01" -f $addr) +Write-Host "verified allocation, zeroing, splitting, coalescing, stress, double-free, corrupt-header detection, and R005 exhaustion" diff --git a/newLang/scripts/verify-hello-test.ps1 b/newLang/scripts/verify-hello-test.ps1 new file mode 100644 index 0000000..b3d6851 --- /dev/null +++ b/newLang/scripts/verify-hello-test.ps1 @@ -0,0 +1,78 @@ +# E04-S05: compile HELLO.N8 in x128, extract the generated PRG, and run it in +# a second x128 session; the screen must show the source's string. +[CmdletBinding()] +param([int]$Port = 6548, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-hello-test.ps1") + +$out = Join-Path $project "build\hello-test" +$disk = Join-Path $out "hello-test.d81" +$text = "HELLO FROM NOVA128" +$failures = @() + +# ------------------------------------------------------- compile session +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "hello-compile-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds +} finally { + Stop-Nova128X128 $session +} +Write-Host ("mailbox: " + (($mb | ForEach-Object { $_.ToString('X2') }) -join ' ')) + +if ($mb[2] -ne 0x02) { $failures += ("producer $($mb[2]) expected 2 (compiler)") } +if ($mb[3] -ne 0xA5) { + $failures += ("compiler status {0:X2} failed at stage {1}" -f $mb[3], $mb[4]) +} +if ($mb[8] -ne 0) { $failures += "diagnostics present: $($mb[8]) (see HELLO.ERR)" } +if ($mb[9] -ne 1) { $failures += "captured $($mb[9]) string prints, expected 1" } +if ($mb[10] -ne 0) { $failures += "unsupported print flag set" } +if ($mb[11] -ne 18) { $failures += "captured $($mb[11]) string bytes, expected 18" } +if ($mb[12] -ne 0) { $failures += "output error $($mb[12])" } + +# ------------------------------------------------- extract the generated PRG +$c1541 = Get-ViceTool "c1541" +$prg = Join-Path $out "hello.prg" +if (Test-Path -LiteralPath $prg) { Remove-Item -LiteralPath $prg -Force } +& $c1541 -attach $disk -read "hello.prg" $prg 2>&1 | Out-Null +if (-not (Test-Path -LiteralPath $prg)) { + $failures += "HELLO.PRG was not written to the compile disk" +} else { + $bytes = [IO.File]::ReadAllBytes($prg) + if ($bytes.Length -lt 14 -or $bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { + $failures += "HELLO.PRG does not load at `$1C01" + } +} + +# ----------------------------------------------------------- run the PRG +$hitRow = -1 +if (-not $failures) { + $session = Start-Nova128X128 -Autostart $prg -Port ($Port + 1) ` + -Screenshot (Join-Path $out "hello-run-vicii.png") + try { + $deadline = [DateTime]::UtcNow.AddSeconds(60) + while ([DateTime]::UtcNow -lt $deadline) { + Start-Sleep -Seconds 1 + try { $screenRam = Read-Nova128Memory $session 0x0400 1000 } catch { continue } + for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + if ((-join $chars) -match [regex]::Escape($text)) { $hitRow = $r; break } + } + if ($hitRow -ge 0) { break } + } + } finally { + Stop-Nova128X128 $session + } + if ($hitRow -lt 0) { $failures += "the generated PRG never displayed '$text'" } +} + +if ($failures) { throw ("hello verification failed:`n " + ($failures -join "`n ")) } +Write-Host ("compile: diagnostics 0, one 18-byte print, out_err 0; HELLO.PRG {0} bytes" -f (Get-Item $prg).Length) +Write-Host ("run: '$text' displayed on screen row $hitRow") +Write-Host "verified hello.n8 compiles to a PRG and the PRG runs in x128" diff --git a/newLang/scripts/verify-object-test.ps1 b/newLang/scripts/verify-object-test.ps1 new file mode 100644 index 0000000..bb8a960 --- /dev/null +++ b/newLang/scripts/verify-object-test.ps1 @@ -0,0 +1,46 @@ +# E05-S03: run the object construction/deletion fixture and check its mailbox. +[CmdletBinding()] +param([int]$Port = 6555, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-object-test.ps1") + +$out = Join-Path $project "build\object-test" +$disk = Join-Path $out "object-test.d81" +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "object-test-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds + Start-Sleep -Milliseconds 500 + $screenRam = Read-Nova128Memory $session 0x0400 1000 +} finally { + Stop-Nova128X128 $session -SettleSeconds 0 +} + +if ($mb[2] -ne 0x03) { $failures += "producer $($mb[2]) expected 3" } +if ($mb[3] -ne 0xA5) { $failures += ("status {0:X2} expected A5" -f $mb[3]) } +if ($mb[4] -ne 0 -or $mb[5] -ne 0) { $failures += "code $($mb[4] + 256 * $mb[5]) expected 0" } +for ($i = 0; $i -lt 5; $i++) { + if ($mb[8 + $i] -ne 1) { $failures += "phase $i flag is $($mb[8 + $i]), expected 1" } +} + +$rows = for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + (-join $chars).TrimEnd() +} +if (-not ($rows -contains "OBJECT OK")) { + $failures += "screen does not contain 'OBJECT OK'" +} + +if ($failures) { throw ("object verification failed:`n " + ($failures -join "`n ")) } +Write-Host "screen:" +$rows | Where-Object { $_ } | ForEach-Object { Write-Host " |$_" } +Write-Host "mailbox: producer 3, PASS, code 0, all 5 construction/deletion flags set" +Write-Host "verified init arguments, zeroed fields, header, null deletion, destructor side effects, allocation-failure no-init, and complete free" diff --git a/newLang/scripts/verify-print-test.ps1 b/newLang/scripts/verify-print-test.ps1 new file mode 100644 index 0000000..99d5046 --- /dev/null +++ b/newLang/scripts/verify-print-test.ps1 @@ -0,0 +1,67 @@ +# E04-S05: run the print/fault runtime test in x128, then check the screen +# text and the mailbox failure fields. +[CmdletBinding()] +param([int]$Port = 6546, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-print-test.ps1") + +$out = Join-Path $project "build\print-test" +$disk = Join-Path $out "print-test.d81" +$expected = @( + "NOVA128 PRINT TEST", + "TEXT OK", + "BYTE 0 7 255", + "WORD 0 12345 65535", + "INT -1 -32768 32767", + "BOOL FALSE TRUE", + "CHAR A", + "RUNTIME FAULT R001", + "RUNTIME FAULT R002", + "RUNTIME FAULT R003", + "RUNTIME FAULT R004", + "RUNTIME FAULT R005") +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "print-test-vicii.png") +try { + $null = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds + Start-Sleep -Seconds 2 # all five fault lines are printed + $mb = Read-Nova128Memory $session 0x1BF0 16 + $screenRam = Read-Nova128Memory $session 0x0400 1000 +} finally { + Stop-Nova128X128 $session -SettleSeconds 0 +} + +# ------------------------------------------------------------- mailbox +if ($mb[2] -ne 0x03) { $failures += ("producer $($mb[2]) expected 3 (generated program)") } +if ($mb[3] -ne 0xFA) { $failures += ("status ${0:X2} expected FA (failure)" -f $mb[3]) } +if ($mb[4] -ne 5 -or $mb[5] -ne 0) { $failures += ("code {0} expected 5 (heap)" -f ($mb[4] + 256 * $mb[5])) } +$addr = $mb[6] + 256 * $mb[7] +if ($addr -ne 0x1234) { $failures += ("address ${0:X4} expected 1234" -f $addr) } +if ($mb[8] -ne 1) { $failures += "CR/RCR changed across the print and fault calls" } + +# -------------------------------------------------------------- screen +$rows = for ($r = 0; $r -lt 25; $r++) { + $chars = for ($c = 0; $c -lt 40; $c++) { + $code = $screenRam[$r * 40 + $c] + if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' } + } + (-join $chars).TrimEnd() +} +for ($r = 0; $r -lt $expected.Count; $r++) { + if ($rows[$r] -cne $expected[$r]) { + $failures += "screen row ${r}: '$($rows[$r])' expected '$($expected[$r])'" + } +} +$wide = @($rows[0..($expected.Count - 1)] | Where-Object { $_.Length -gt 39 }) +if ($wide) { $failures += "screen rows longer than 39: $($wide -join ' | ')" } + +if ($failures) { throw ("print runtime verification failed:`n " + ($failures -join "`n ")) } +Write-Host "screen:" +$rows[0..($expected.Count - 1)] | ForEach-Object { Write-Host " |$_" } +Write-Host ("mailbox: producer 3, status FA, code 5, address {0:X4}, mapping preserved" -f $addr) +Write-Host "verified print formatting, fault reporting, and mapping stability" diff --git a/newLang/src/common/class.inc b/newLang/src/common/class.inc new file mode 100644 index 0000000..bac0f67 --- /dev/null +++ b/newLang/src/common/class.inc @@ -0,0 +1,49 @@ +; class.inc - object header and class descriptor ABI (E05-S02). +; +; Every object starts with a two-byte pointer to its class descriptor. Fields +; follow at offset 2 in declaration order, with no hidden padding. The +; descriptor is packed little-endian: +; +; +0 instance size in bytes (2), including the object header +; +2 destructor address (2), 0 = no destructor +; +4 implemented-interface count (1) +; +5 interface-directory pointer (2), 0 when the count is 0 +; +7 flags (1), must be 0; no parent-class slot exists +; +; The interface directory is an array of four-byte entries: +; +; +0 interface descriptor pointer (2), defined by the interface ABI +; +2 method-table pointer (2), in interface slot order +; +; The directory reserves the path for interface dispatch and runtime identity +; without introducing class inheritance. Flag bits are reserved for future +; non-inheritance metadata and must be zero in this ABI. + +.ifndef CLASS_INC +CLASS_INC = 1 + +CLASS_DESC_SIZE = 8 +CD_INSTANCE_SIZE = 0 +CD_DESTRUCTOR = 2 +CD_IFACE_COUNT = 4 +CD_IFACE_DIR = 5 +CD_FLAGS = 7 + +CLASS_DESC_FLAG_NONE = 0 +CLASS_DESC_FLAG_MASK = 0 ; no defined flag bits yet + +OBJ_DESCRIPTOR = 0 +OBJ_FIELDS = 2 +OBJ_HEADER_SIZE = 2 +OBJ_MIN_SIZE = 2 + +CLASS_IFACE_ENTRY_SIZE = 4 +CI_INTERFACE = 0 +CI_TABLE = 2 + +; construct.s result codes (obj_error) +OBJ_OK = 0 +OBJ_ERR_HEAP = 1 +OBJ_ERR_BAD = 2 + +.endif diff --git a/newLang/src/common/faults.inc b/newLang/src/common/faults.inc new file mode 100644 index 0000000..1b396ba --- /dev/null +++ b/newLang/src/common/faults.inc @@ -0,0 +1,17 @@ +; faults.inc - stable runtime fault codes (E04-S05). +; +; The codes are the R-code suffix printed by flt_report and stored in the +; mailbox MB_CODE. They are stable: never renumber or reuse one. + +.ifndef FAULTS_INC +FAULTS_INC = 1 + +FLT_NULL = 1 ; R001 null object/interface reference +FLT_BOUNDS = 2 ; R002 array index outside the bounds +FLT_DIV0 = 3 ; R003 division by zero +FLT_STACK = 4 ; R004 software-frame stack overflow +FLT_HEAP = 5 ; R005 heap exhausted or corrupt + +FLT_CODE_MAX = 5 + +.endif diff --git a/newLang/src/common/flow.inc b/newLang/src/common/flow.inc new file mode 100644 index 0000000..f75b55a --- /dev/null +++ b/newLang/src/common/flow.inc @@ -0,0 +1,22 @@ +; flow.inc - control-flow emitter limits (E04-S04). + +.ifndef FLOW_INC +FLOW_INC = 1 + +FL_LABELS = 16 ; label ids 0..15 per generated routine +FL_FIX_MAX = 32 ; jumps and conditional branches per routine +FL_FIX_SIZE = 4 ; kind, label, operand position, spare + +FL_BRANCH_SIZE = 5 ; opcode + distance + 3-byte JMP slot + +FL_MODE_EMIT = 0 ; write bytes and patch fixups +FL_MODE_COUNT = 1 ; count bytes only (sizing pass) + +FL_OK = 0 +FL_ERR_LABEL = 1 ; fixup names a label that was never defined +FL_ERR_FULL = 2 ; fixup table full +FL_ERR_STATE = 3 ; double-defined label, missing runtime address +FL_ERR_ID = 4 ; label id out of range +FL_ERR_RANGE = 5 ; branch target is behind the branch + +.endif diff --git a/newLang/src/common/formats.inc b/newLang/src/common/formats.inc index 5d13c90..9fd7deb 100644 --- a/newLang/src/common/formats.inc +++ b/newLang/src/common/formats.inc @@ -2,7 +2,8 @@ ; ; The container is deliberately small: a C128 BASIC 7 stub at $1C01, the ; machine entry at $1C0D, and contiguous sections loaded in ascending order. -; The higher generated-program map (banking, heap, software stack) is E04-S02. +; Frames (E04-S02), an explicit-region heap (E05-S01), and class descriptors +; (E05-S02) extend the container without changing its load address. .ifndef FORMATS_INC FORMATS_INC = 1 @@ -14,6 +15,8 @@ PRG_STUB_SIZE = 12 OUT_MAX_SECTIONS = 8 OUT_SECTION_NAME_MAX = 12 OUT_RECORD_SIZE = 7 ; name ptr(2), name len(1), start(2), size(2) +OUT_MAX_CLASSES = 8 +OUT_CLASS_RECORD_SIZE = 10 ; name ptr(2), len(1), size(2), destroy(2), count(1), dir(2) ; out_begin/out_finish/out_abort result codes (out_err). OUT_OK = 0 @@ -23,5 +26,6 @@ OUT_ERR_SECTIONS = 3 ; more than OUT_MAX_SECTIONS OUT_ERR_NAME = 4 ; base or section name too long OUT_ERR_DISK = 5 ; KERNAL or DOS failure OUT_ERR_STATE = 6 ; byte emitted outside a section +OUT_ERR_CLASSES = 7 ; more than OUT_MAX_CLASSES .endif diff --git a/newLang/src/common/heap.inc b/newLang/src/common/heap.inc new file mode 100644 index 0000000..20b29e0 --- /dev/null +++ b/newLang/src/common/heap.inc @@ -0,0 +1,37 @@ +; heap.inc - generated-program free-list heap contract (E05-S01). +; +; Block header, four bytes: +; +0/+1 total block size; bit 15 set while allocated +; +2/+3 next free block pointer while free; HEAP_USED_MAGIC while allocated +; +; Blocks are two-byte aligned. Payload starts at header+4. The minimum block is +; six bytes: one four-byte header plus a two-byte payload. A heap block must be +; smaller than 32K because the used bit lives in the size word. +; +; heap_init: A/X = page-aligned base (A must be 0), Y = page count (1..127). +; heap_alloc: A/X = payload bytes; returns A/X = payload pointer, C=0, or +; A/X = 0, C=1 with heap_error. +; heap_free: A/X = payload pointer; A/X=0 is a no-op; C=1 with heap_error on +; an invalid, corrupt, or already-free block. +; +; The heap is not banked. The caller supplies a region in the generated +; program's current RAM bank; heap placement remains open until later stories. + +.ifndef HEAP_INC +HEAP_INC = 1 + +HEAP_HEADER_SIZE = 4 +HEAP_MIN_BLOCK = 6 +HEAP_SIZE_MASK = $7FFF +HEAP_USED_FLAG = $8000 +HEAP_USED_MAGIC = $A55A +HEAP_MAX_PAGES = 127 + +HEAP_OK = 0 +HEAP_ERR_OOM = 1 +HEAP_ERR_ARG = 2 +HEAP_ERR_RANGE = 3 +HEAP_ERR_DOUBLE = 4 +HEAP_ERR_CORRUPT = 5 + +.endif diff --git a/newLang/src/common/print.inc b/newLang/src/common/print.inc new file mode 100644 index 0000000..a872110 --- /dev/null +++ b/newLang/src/common/print.inc @@ -0,0 +1,14 @@ +; print.inc - PETSCII output constants for generated Nova128 programs (E04-S05). +; +; The runtime routines in src/runtime/print.s assume the D129 generated-program +; mapping (CR=$0E): KERNAL and I/O visible, standard zero page, code below +; $4000. They do not change the mapping (D132). + +.ifndef PRINT_INC +PRINT_INC = 1 + +PR_CR = $0D ; PETSCII carriage return +PR_SPACE = $20 +PR_CLEAR = $93 ; PETSCII clear screen + +.endif diff --git a/newLang/src/compiler/codegen.s b/newLang/src/compiler/codegen.s index be35e94..5f5b1fb 100644 --- a/newLang/src/compiler/codegen.s +++ b/newLang/src/compiler/codegen.s @@ -23,15 +23,21 @@ ; C=0 and A = fixup index ; cg_patch_here A = fixup index; target is the current end of buffer ; cg_here returns A = current length +; cg_set_count A != 0: count bytes only (no stores); 0: emit bytes ; ; Only forward relative branches within +0..+127 bytes are accepted. This is ; enough for the scalar shapes and gives E04-S04 the checked primitive it ; needs for a later long-branch expansion. +; +; Count mode (E04-S04) lets a sizing pass measure a routine without writing +; the code buffer. cg_len advances exactly as in emit mode, so label offsets +; and lengths match by construction. .setcpu "6502" .include "../common/codegen.inc" .export cg_init, cg_put, cg_here, cg_branch_fwd, cg_patch_here +.export cg_set_count, cg_count .export cg_lda_imm, cg_ldx_imm, cg_ora_imm, cg_lda_abs, cg_ldx_abs .export cg_sta_abs, cg_stx_abs, cg_adc_abs, cg_sbc_abs, cg_cmp_abs .export cg_jsr_abs, cg_clc, cg_sec, cg_rts @@ -44,6 +50,7 @@ cg_init: sta cg_len sta cg_err sta cg_fix_count + sta cg_count clc rts @@ -51,13 +58,24 @@ cg_here: lda cg_len rts +; A != 0: count bytes only. A = 0: emit them (the default). +cg_set_count: + sta cg_count + rts + ; A = byte to append. C=1 on overflow. cg_put: ldy cg_len cpy #CG_MAX bcs @full + pha + lda cg_count + bne @count + pla sta cg_code,y - inc cg_len + jmp @bump +@count: pla +@bump: inc cg_len clc rts @full: lda #CG_ERR_FULL @@ -208,6 +226,7 @@ cg_patch_here: cg_code: .res 256 cg_len: .res 1 cg_err: .res 1 +cg_count: .res 1 cg_fix_count: .res 1 cg_fixpos: .res CG_FIX_MAX cg_addr: .res 2 diff --git a/newLang/src/compiler/flow.s b/newLang/src/compiler/flow.s new file mode 100644 index 0000000..faeca00 --- /dev/null +++ b/newLang/src/compiler/flow.s @@ -0,0 +1,604 @@ +; flow.s - labels, branches, calls, and frame prologue/epilogue (E04-S04). +; +; flow.s builds on the scalar emitter (codegen.s). It supplies the control +; transfer that whole statements need: +; +; fl_init A = FL_MODE_EMIT or FL_MODE_COUNT +; fl_label A = label id: define it at the current position +; fl_jmp A = label id: absolute JMP (also the back edge of a loop) +; fl_branch A = opcode, X = label id: conditional branch +; fl_branch_true / fl_branch_false A = label id (BNE / BEQ) +; fl_finish resolve every fixup; in emit mode patch the bytes +; fl_get_label A = label id -> X = offset, C=0; C=1 if undefined +; fl_call A/X = target, Y = argument bytes: JSR then discard +; fl_call_byte same, with a one-byte result preserved in A +; fl_call_word same, with a two-byte result preserved in A/X +; fl_prologue A/X = local bytes, Y = error label: frame_enter + BCS +; fl_ret no result: JSR frame_leave, then RTS +; fl_ret_byte one-byte result in A, preserved across frame_leave +; fl_ret_word two-byte result in A/X, preserved across frame_leave +; fl_err FL_OK or the first FL_ERR_* code +; fl_enter, fl_leave, fl_discard runtime addresses the caller supplies +; +; Conditional branches are always five bytes: +; +; short form (target within +0..+127) +; Bxx target ; distance byte +; NOP NOP NOP ; unused JMP slot; executed only when not taken +; +; long form (target further away, or out of range of a byte) +; Bxx'inv +3 ; the inverse of the requested condition +; JMP target +; +; The length never changes, so a counting sizing pass and an emitting pass +; make identical label offsets and identical total lengths. The long form +; inverts the opcode when it patches, so a true condition falls through to the +; JMP and a false one skips it. Short branches keep the direct opcode and NOP +; out the unused JMP slot. +; +; Backward control transfer always uses fl_jmp (JMP abs). A relative branch +; whose target is not ahead is FL_ERR_RANGE rather than a wrapped distance. +; +; Calls follow the software-frame ABI (frames.inc): arguments are already on +; the software stack, fl_call emits JSR and then frame_discard for Y bytes. +; fl_prologue emits frame_enter and branches to the caller's error label with +; BCS, so a refused frame takes the same fault path as later runtime checks. + +.setcpu "6502" +.include "../common/codegen.inc" +.include "../common/flow.inc" + +.import cg_init, cg_put, cg_len, cg_code, cg_lda_imm, cg_ldx_imm +.import cg_jsr_abs, cg_rts +.import cg_set_count + +; Branch to this routine's @out label even when it is out of relative range. +.macro CHECK_OUT + bcc :+ + jmp @out +: +.endmacro + +.export fl_init, fl_label, fl_jmp, fl_branch, fl_finish, fl_get_label +.export fl_branch_true, fl_branch_false +.export fl_call, fl_call_byte, fl_call_word +.export fl_prologue, fl_ret, fl_ret_byte, fl_ret_word +.export fl_err, fl_enter, fl_leave, fl_discard + +.segment "CODE" + +; A = mode. Reset the buffer, labels, and fixups, then set count mode. +fl_init: + sta fl_mode + jsr cg_init + lda #$00 + sta fl_fix_count + sta fl_err + ldx #FL_LABELS - 1 +@clr: sta fl_defined,x + dex + bpl @clr + lda fl_mode + jsr cg_set_count + clc + rts + +; A = label id. C=1 on a bad id or a redefinition. +fl_label: + cmp #FL_LABELS + bcs @bad + tax + lda fl_defined,x + bne @dup + lda #$01 + sta fl_defined,x + lda cg_len + sta fl_labels,x + clc + rts +@dup: lda #FL_ERR_STATE + sta fl_err + sec + rts +@bad: lda #FL_ERR_ID + sta fl_err + sec + rts + +; A = label id. Emit JMP abs with a placeholder operand. +fl_jmp: + cmp #FL_LABELS + bcs @bad + sta fl_lab + lda #$4C + jsr cg_put + CHECK_OUT + lda cg_len + sta fl_pos + lda #$01 ; fixup kind: JMP + jsr fl_add_fix + CHECK_OUT + lda #$FF + jsr cg_put + CHECK_OUT + lda #$FF + jmp cg_put +@bad: lda #FL_ERR_ID + sta fl_err + sec + rts +@out: sec + rts + +; A = branch opcode, X = label id. Emit the fixed five-byte form. +fl_branch: + sta fl_op + stx fl_lab + cpx #FL_LABELS + bcs @bad + lda fl_op + jsr cg_put + CHECK_OUT + lda cg_len ; the distance byte + sta fl_pos + lda #$02 ; fixup kind: branch + jsr fl_add_fix + CHECK_OUT + lda #$00 + jsr cg_put + CHECK_OUT + lda #$4C + jsr cg_put + CHECK_OUT + lda #$FF + jsr cg_put + CHECK_OUT + lda #$FF + jmp cg_put +@bad: lda #FL_ERR_ID + sta fl_err + sec + rts +@out: sec + rts + +fl_branch_true: + sta fl_lab + lda #$D0 ; BNE + ldx fl_lab + jmp fl_branch + +fl_branch_false: + sta fl_lab + lda #$F0 ; BEQ + ldx fl_lab + jmp fl_branch + +; A = kind, fl_lab = label, fl_pos = operand position. Append a fixup. +fl_add_fix: + sta fl_kind + lda fl_fix_count + cmp #FL_FIX_MAX + bcs @full + asl a + asl a ; * FL_FIX_SIZE + tax + lda fl_kind + sta fl_fix,x + inx + lda fl_lab + sta fl_fix,x + inx + lda fl_pos + sta fl_fix,x + inc fl_fix_count + clc + rts +@full: lda #FL_ERR_FULL + sta fl_err + sec + rts + +; Resolve every fixup. Counting mode only checks that each label exists. +fl_finish: + lda #$00 + sta fl_index +@next: lda fl_index + cmp fl_fix_count + beq @ok + asl a + asl a + sta fl_recp + tay + lda fl_fix+1,y ; label id + jsr fl_resolve + CHECK_OUT + lda fl_mode + bne @valid ; counting: no bytes to patch + ldy fl_recp + lda fl_fix,y + cmp #$01 + beq @patch_jmp + lda fl_fix+1,y + tax + lda fl_labels,x + jsr fl_patch_branch + CHECK_OUT + jmp @valid +@patch_jmp: + ldy fl_recp + lda fl_fix+2,y + tay + lda fl_tlo + sta cg_code,y + iny + lda fl_thi + sta cg_code,y +@valid: inc fl_index + jmp @next +@ok: clc + rts +@out: sec + rts + +; A = label id -> fl_tlo/fl_thi = the label's absolute code address. +fl_resolve: + cmp #FL_LABELS + bcs @bad + tax + lda fl_defined,x + beq @bad + lda fl_labels,x + clc + adc #cg_code + adc #$00 + sta fl_thi + clc + rts +@bad: lda #FL_ERR_LABEL + sta fl_err + sec + rts + +; A = target offset. fl_recp = record, fl_pos = distance-byte position. +; Patch the five-byte form: short (distance + three NOPs) or long (JMP). +fl_patch_branch: + sta fl_laboff + ldy fl_recp + lda fl_fix+2,y + sta fl_pos + lda fl_laboff + cmp fl_pos + bcc @range ; target before the branch + beq @range + sec + sbc fl_pos + sec + sbc #$01 ; distance from the end of the branch + cmp #128 + bcs @long + ldy fl_pos + sta cg_code,y + iny + lda #$EA ; NOP out the unused JMP slot + sta cg_code,y + iny + sta cg_code,y + iny + sta cg_code,y + clc + rts +@long: ldy fl_pos + dey + lda cg_code,y ; invert the requested condition + jsr fl_invert_opcode + CHECK_OUT + ldy fl_pos + dey + sta cg_code,y + iny + lda #$03 ; skip the three-byte JMP + sta cg_code,y + iny + lda #$4C + sta cg_code,y + iny + lda fl_tlo + sta cg_code,y + iny + lda fl_thi + sta cg_code,y + clc + rts +@out: sec + rts +@range: lda #FL_ERR_RANGE + sta fl_err + sec + rts + +; A = branch opcode -> A = its inverse. Only the conditions flow.s emits. +fl_invert_opcode: + cmp #$F0 ; BEQ <-> BNE + beq @beq + cmp #$D0 + beq @bne + cmp #$90 ; BCC <-> BCS + beq @bcc + cmp #$B0 + beq @bcs + cmp #$10 ; BPL <-> BMI + beq @bpl + cmp #$30 + beq @bmi + cmp #$50 ; BVC <-> BVS + beq @bvc + cmp #$70 + beq @bvs + lda #FL_ERR_STATE + sta fl_err + sec + rts +@beq: lda #$D0 + clc + rts +@bne: lda #$F0 + clc + rts +@bcc: lda #$B0 + clc + rts +@bcs: lda #$90 + clc + rts +@bpl: lda #$10 + clc + rts +@bmi: lda #$30 + clc + rts +@bvc: lda #$50 + clc + rts +@bvs: lda #$70 + clc + rts + +; A = label id -> X = offset, C=0; C=1 if the label was never defined. +fl_get_label: + cmp #FL_LABELS + bcs @bad + tax + lda fl_defined,x + beq @bad + lda fl_labels,x + tax + clc + rts +@bad: lda #FL_ERR_LABEL + sta fl_err + sec + rts + +; A/X = target, Y = argument bytes. fl_call is for a callee without a +; result; fl_call_byte and fl_call_word save the result on the hardware +; stack across the argument discard and restore it afterward. +fl_call: + sta fl_addr + stx fl_addr+1 + sty fl_count + lda #$00 + sta fl_resmode + jmp fl_call_emit + +fl_call_byte: + sta fl_addr + stx fl_addr+1 + sty fl_count + lda #$01 + sta fl_resmode + jmp fl_call_emit + +fl_call_word: + sta fl_addr + stx fl_addr+1 + sty fl_count + lda #$02 + sta fl_resmode + jmp fl_call_emit + +fl_call_emit: + lda fl_addr + ldx fl_addr+1 + jsr cg_jsr_abs + CHECK_OUT + lda fl_resmode + beq @discard + cmp #$01 + beq @save_byte + lda #$48 ; PHA (word low) + jsr cg_put + CHECK_OUT + lda #$8A ; TXA + jsr cg_put + CHECK_OUT + lda #$48 ; PHA (word high) + jsr cg_put + CHECK_OUT + jmp @discard +@save_byte: + lda #$48 ; PHA (byte result) + jsr cg_put + CHECK_OUT +@discard: + lda fl_count + beq @restore + lda fl_discard + ora fl_discard+1 + beq @state + lda fl_count + jsr cg_lda_imm + CHECK_OUT + lda #$00 + jsr cg_ldx_imm + CHECK_OUT + lda fl_discard + ldx fl_discard+1 + jsr cg_jsr_abs + CHECK_OUT +@restore: + lda fl_resmode + beq @ok + cmp #$01 + beq @restore_byte + lda #$68 ; PLA -> high + jsr cg_put + CHECK_OUT + lda #$AA ; TAX + jsr cg_put + CHECK_OUT + lda #$68 ; PLA -> low + jsr cg_put + CHECK_OUT + jmp @ok +@restore_byte: + lda #$68 ; PLA + jsr cg_put + CHECK_OUT +@ok: clc + rts +@state: lda #FL_ERR_STATE + sta fl_err + sec + rts +@out: sec + rts + +; A/X = local bytes, Y = error label. frame_enter, then BCS error. +fl_prologue: + sta fl_locals + stx fl_locals+1 + sty fl_lab + lda fl_enter + ora fl_enter+1 + beq @state + lda fl_locals + jsr cg_lda_imm + CHECK_OUT + lda fl_locals+1 + jsr cg_ldx_imm + CHECK_OUT + lda fl_enter + ldx fl_enter+1 + jsr cg_jsr_abs + CHECK_OUT + lda #$B0 ; BCS: frame refused + ldx fl_lab + jmp fl_branch +@state: lda #FL_ERR_STATE + sta fl_err + sec + rts +@out: sec + rts + +fl_ret: + lda fl_leave + ora fl_leave+1 + beq @state + lda fl_leave + ldx fl_leave+1 + jsr cg_jsr_abs + CHECK_OUT + jmp cg_rts +@state: lda #FL_ERR_STATE + sta fl_err + sec + rts +@out: sec + rts + +; One-byte result in A: PHA, JSR frame_leave, PLA, RTS. +fl_ret_byte: + lda fl_leave + ora fl_leave+1 + beq @state + lda #$48 ; PHA + jsr cg_put + CHECK_OUT + lda fl_leave + ldx fl_leave+1 + jsr cg_jsr_abs + CHECK_OUT + lda #$68 ; PLA + jsr cg_put + CHECK_OUT + jmp cg_rts +@state: lda #FL_ERR_STATE + sta fl_err + sec + rts +@out: sec + rts + +; Two-byte result in A (low) and X (high): PHA, TXA, PHA, JSR frame_leave, +; PLA, TAX, PLA, RTS. +fl_ret_word: + lda fl_leave + ora fl_leave+1 + beq @state + lda #$48 ; PHA (low) + jsr cg_put + CHECK_OUT + lda #$8A ; TXA + jsr cg_put + CHECK_OUT + lda #$48 ; PHA (high) + jsr cg_put + CHECK_OUT + lda fl_leave + ldx fl_leave+1 + jsr cg_jsr_abs + CHECK_OUT + lda #$68 ; PLA -> high + jsr cg_put + CHECK_OUT + lda #$AA ; TAX + jsr cg_put + CHECK_OUT + lda #$68 ; PLA -> low + jsr cg_put + CHECK_OUT + jmp cg_rts +@state: lda #FL_ERR_STATE + sta fl_err + sec + rts +@out: sec + rts + +.segment "BSS" +fl_mode: .res 1 +fl_err: .res 1 +fl_fix_count: .res 1 +fl_index: .res 1 +fl_recp: .res 1 +fl_lab: .res 1 +fl_pos: .res 1 +fl_op: .res 1 +fl_kind: .res 1 +fl_count: .res 1 +fl_resmode: .res 1 +fl_addr: .res 2 +fl_locals: .res 2 +fl_laboff: .res 1 +fl_tlo: .res 1 +fl_thi: .res 1 +fl_labels: .res FL_LABELS +fl_defined: .res FL_LABELS +fl_fix: .res FL_FIX_MAX * FL_FIX_SIZE +fl_enter: .res 2 +fl_leave: .res 2 +fl_discard: .res 2 diff --git a/newLang/src/compiler/output.s b/newLang/src/compiler/output.s index 3aa6556..9ae5fbc 100644 --- a/newLang/src/compiler/output.s +++ b/newLang/src/compiler/output.s @@ -7,6 +7,7 @@ ; NOVA128 MAP 1 ; ENTRY hhhh ; SECTION name start size +; CLASS name size destroy interface-count interface-directory ; ; API (all calls in the bank-0 compiler state): ; out_begin A/X = source name (bank 0), Y = length. Sets out_load and @@ -19,6 +20,9 @@ ; out_bytes A/X = data, Y = count (0-255). ; out_end_section ; out_finish Writes the MAP, closes both files, and returns C=0 on success. +; out_class A/X = class name, Y = length. Adds a deterministic CLASS +; record from out_class_size/destroy/ifaces/ifdir. Class +; records follow the SECTION lines in call order. ; out_abort Closes and scratches only files created by this run. ; ; Every failure sets out_err and returns C=1; the caller must call out_abort. @@ -35,6 +39,8 @@ .export out_begin, out_startup, out_section, out_end_section .export out_byte, out_bytes, out_finish, out_abort +.export out_class +.export out_class_size, out_class_destroy, out_class_ifaces, out_class_ifdir .export out_load, out_entry, out_start, out_err .segment "ZEROPAGE" @@ -126,6 +132,24 @@ set_rec_ptr: bne @add @done: rts +; out_rec_ptr = out_classes + out_index * OUT_CLASS_RECORD_SIZE. +set_class_ptr: + lda #out_classes + sta out_rec_ptr+1 + ldx out_index + beq @done +@add: clc + lda out_rec_ptr + adc #OUT_CLASS_RECORD_SIZE + sta out_rec_ptr + bcc :+ + inc out_rec_ptr+1 +: dex + bne @add +@done: rts + ; Build and send "S0:name" on the command channel. out_scratch: sta out_scratch_ptr @@ -210,6 +234,7 @@ out_begin: lda #$00 sta out_count + sta out_class_count sta out_in_section sta out_prg_created sta out_map_created @@ -527,6 +552,65 @@ out_bytes: @done: clc @out: rts +; A/X = class name, Y = length. The descriptor values are supplied in the +; exported out_class_* variables. C=1 sets out_err and adds no record. +out_class: + sta out_tmp + stx out_tmp+1 + sty out_tmp_len + lda out_class_count + cmp #OUT_MAX_CLASSES + bcs @too_many + lda out_tmp_len + cmp #OUT_SECTION_NAME_MAX + 1 + bcs @bad_name + lda out_class_count + sta out_index + jsr set_class_ptr + ldy #0 + lda out_tmp + sta (out_rec_ptr),y + iny + lda out_tmp+1 + sta (out_rec_ptr),y + iny + lda out_tmp_len + sta (out_rec_ptr),y + iny + lda out_class_size + sta (out_rec_ptr),y + iny + lda out_class_size+1 + sta (out_rec_ptr),y + iny + lda out_class_destroy + sta (out_rec_ptr),y + iny + lda out_class_destroy+1 + sta (out_rec_ptr),y + iny + lda out_class_ifaces + sta (out_rec_ptr),y + iny + lda out_class_ifdir + sta (out_rec_ptr),y + iny + lda out_class_ifdir+1 + sta (out_rec_ptr),y + inc out_class_count + clc + rts +@too_many: + lda #OUT_ERR_CLASSES + sta out_err + sec + rts +@bad_name: + lda #OUT_ERR_NAME + sta out_err + sec + rts + out_finish: jsr out_end_section jsr out_close_prg @@ -727,6 +811,113 @@ out_write_map: bcs @out inc out_index jmp @section +@done: jsr out_write_classes + bcs @out + clc +@out: rts + +; Write CLASS records after the SECTION records, in out_class call order. +out_write_classes: + lda #$00 + sta out_index +@class: + lda out_index + cmp out_class_count + bne @body + jmp @done +@body: + jsr set_class_ptr + lda #class_prefix + ldy #class_prefix_len + jsr out_text + bcc @prefix_ok + jmp @out +@prefix_ok: + ldy #0 + lda (out_rec_ptr),y + sta out_srcp + iny + lda (out_rec_ptr),y + sta out_srcp+1 + iny + lda (out_rec_ptr),y + sta out_text_len + ldy #0 +@name: cpy out_text_len + beq @named + lda (out_srcp),y + sty out_y + jsr out_char + bcc @name_ok + jmp @out +@name_ok: + ldy out_y + iny + bne @name +@named: + lda #' ' + jsr out_char + bcc @space1 + jmp @out +@space1: + ldy #3 + lda (out_rec_ptr),y + sta out_num + iny + lda (out_rec_ptr),y + sta out_num+1 + jsr out_hex4 + bcc @size_ok + jmp @out +@size_ok: + lda #' ' + jsr out_char + bcc @space2 + jmp @out +@space2: + ldy #5 + lda (out_rec_ptr),y + sta out_num + iny + lda (out_rec_ptr),y + sta out_num+1 + jsr out_hex4 + bcc @destroy_ok + jmp @out +@destroy_ok: + lda #' ' + jsr out_char + bcc @space3 + jmp @out +@space3: + ldy #7 + lda (out_rec_ptr),y + jsr out_hex_byte + bcc @count_ok + jmp @out +@count_ok: + lda #' ' + jsr out_char + bcc @space4 + jmp @out +@space4: + ldy #8 + lda (out_rec_ptr),y + sta out_num + iny + lda (out_rec_ptr),y + sta out_num+1 + jsr out_hex4 + bcc @dir_ok + jmp @out +@dir_ok: + jsr out_cr + bcc @line_ok + jmp @out +@line_ok: + inc out_index + jmp @class @done: clc @out: rts @@ -809,6 +1000,8 @@ map_entry: .byte "ENTRY " map_entry_len = * - map_entry section_prefix: .byte "SECTION " section_prefix_len = * - section_prefix +class_prefix: .byte "CLASS " +class_prefix_len = * - class_prefix hex_digits: .byte "0123456789ABCDEF" .segment "BSS" @@ -835,6 +1028,7 @@ out_entry: .res 2 out_start: .res 2 out_current: .res 2 out_count: .res 1 +out_class_count: .res 1 out_index: .res 1 out_in_section: .res 1 out_prg_created: .res 1 @@ -844,6 +1038,11 @@ out_prg_is_open: .res 1 out_map_is_open: .res 1 out_err: .res 1 out_sections: .res OUT_MAX_SECTIONS * OUT_RECORD_SIZE +out_classes: .res OUT_MAX_CLASSES * OUT_CLASS_RECORD_SIZE +out_class_size: .res 2 +out_class_destroy: .res 2 +out_class_ifaces: .res 1 +out_class_ifdir: .res 2 out_tmp: .res 2 out_tmp_len: .res 1 out_scratch_ptr: .res 2 diff --git a/newLang/src/compiler/parser.s b/newLang/src/compiler/parser.s index 7f574b2..60978c2 100644 --- a/newLang/src/compiler/parser.s +++ b/newLang/src/compiler/parser.s @@ -58,6 +58,7 @@ .import assign_slots, check_interfaces .export parse_file, tb_get, tb_set, mem_count, par_count +.export gen_text, gen_text_len, gen_print_count, gen_bad .export ex_trace_on, ex_min_sp .exportzp ex_tp .export type_kind, type_line_lo, type_line_hi @@ -89,6 +90,10 @@ parse_file: stx parse_sp jsr tt_clear ; bank-1 BSS is not zeroed at load lda #$00 + sta gen_text_len ; first source-to-code path (E04-S05) + sta gen_print_count + sta gen_bad + sta gen_in_print sta mem_count sta par_count sta mem_full @@ -1095,6 +1100,7 @@ parse_statement: stx stmt_sp lda #$00 sta ex_depth + sta gen_in_print lda tok_col sta stmt_col jsr trace_line @@ -1107,8 +1113,9 @@ parse_statement: bne :+ jmp @for : cmp #KW_ELSE - beq @else - cmp #KW_RETURN + bne :+ + jmp @else +: cmp #KW_RETURN beq @return cmp #KW_PRINT beq @print @@ -1119,8 +1126,27 @@ parse_statement: jsr value jsr check_cond jmp end_stmt -@print: jsr value +@print: lda pass ; pass 2 captures a string literal in print + cmp #$02 + bne @print_value + lda #$01 + sta gen_in_print + lda gen_text_len + sta gen_len_before +@print_value: + jsr value jsr check_print + lda #$00 + sta gen_in_print + lda pass + cmp #$02 + bne @print_done + lda gen_text_len + cmp gen_len_before + bne @print_done + lda #$01 + sta gen_bad +@print_done: jmp end_stmt @delete: jsr value @@ -1528,11 +1554,15 @@ parse_postfix: jsr trace_byte lda tok_kind cmp #TOK_IDENT - beq @name - cmp #KW_NEW - beq @new - cmp #KW_SELF - beq @self + bne :+ + jmp @name +: cmp #KW_NEW + bne :+ + jmp @new +: cmp #KW_SELF + bne :+ + jmp @self +: ldx #lit_count - 1 @lit: cmp lit_kinds,x beq @literal @@ -1548,7 +1578,17 @@ parse_postfix: @early: lda #E_SYN_UNEXPECTED_EOL jmp syn_error @literal: - lda lit_types,x + lda gen_in_print + beq :+ + lda tok_kind + cmp #TOK_STRING + bne :+ + txa + pha + jsr gen_capture + pla + tax +: lda lit_types,x sta sc_base lda tok_value ; a number's value (others ignore it) sta sc_val @@ -1876,6 +1916,24 @@ trace_byte: inc ex_tp+1 @off: rts +; Copy the current STRING token into gen_text. Called from parse_postfix +; only inside a pass-2 print expression (E04-S05). Clobbers A/X/Y. +gen_capture: + inc gen_print_count + ldx tok_text_len + cpx #81 + bcc :+ + ldx #80 +: stx gen_text_len + ldy #$00 +@copy: cpy gen_text_len + beq @done + lda tok_text,y + sta gen_text,y + iny + bne @copy +@done: rts + ; Remember the member name token; the next token picks field or method. pend_member: lda tok_col @@ -2719,6 +2777,13 @@ op_code: .res 1 rt_col: .res 1 val_col: .res 1 ; first column of the statement's value ty_col: .res 1 ; first column of a signature type +; pass-2 capture used by the hello compile fixture (E04-S05) +gen_text: .res 80 +gen_text_len: .res 1 +gen_print_count: .res 1 +gen_bad: .res 1 +gen_in_print: .res 1 +gen_len_before: .res 1 mem_seq: .res 1 ; pass 2: next member row of cur_owner impl_count: .res 1 impl_full: .res 1 diff --git a/newLang/src/runtime/construct.s b/newLang/src/runtime/construct.s new file mode 100644 index 0000000..7d2618c --- /dev/null +++ b/newLang/src/runtime/construct.s @@ -0,0 +1,207 @@ +; construct.s - runtime object construction and deletion (E05-S03). +; +; This layer sits above the E05-S01 heap and the E05-S02 descriptor ABI. It +; deliberately does not depend on the compiler: generated code will call it +; after evaluating constructor arguments. +; +; API: +; obj_new A/X = class descriptor pointer. The caller has already +; pushed constructor arguments right-to-left on the software +; frame stack and set obj_init_ptr to the init entry (or 0). +; Allocates the descriptor's instance size, zeroes it (heap +; contract), stores the descriptor at object+0, then calls +; init with A/X = self. Returns A/X = object, C=0. On failure +; A/X=0, C=1 and obj_error is OBJ_ERR_HEAP or OBJ_ERR_BAD. +; init is never called on failure. +; obj_delete A/X = object. Null is a no-op. Reads the optional +; destructor from the descriptor, calls it once with A/X = +; self, then frees the complete object. Returns C=0 and +; A/X=0; C=1 with obj_error on a bad object or heap error. +; obj_init_ptr init entry selected by the caller; 0 = no init. +; obj_error OBJ_OK, OBJ_ERR_HEAP, or OBJ_ERR_BAD. +; +; Constructor arguments remain on the caller's frame stack. init uses the +; normal frame_enter/frame_leave contract and sees them at FP+2. The caller +; discards the argument bytes after obj_new returns. obj_delete and obj_new +; use obj_call, a synthetic-return indirect call, because the 8502 has no +; JSR (address). obj_call takes its entry from co_vec because A/X already +; carry the callee's self pointer; it preserves A/X and only needs one level +; of synthetic return, so E06-S02 can reuse it for interface dispatch. + +.setcpu "6502" +.include "../common/class.inc" + +.import heap_alloc, heap_free, obj_class +.export obj_new, obj_delete, obj_init_ptr, obj_error + +.segment "ZEROPAGE" +co_vec: .res 2 ; indirect target for obj_call +co_desc: .res 2 ; descriptor pointer +co_ptr: .res 2 ; object header writer +co_self: .res 2 ; A/X passed through obj_call + +.segment "CODE" + +; A/X = descriptor. Allocate and construct; see the API above. +obj_new: + sta co_desc + stx co_desc+1 + lda #OBJ_OK + sta obj_error + + ldy #CD_INSTANCE_SIZE + lda (co_desc),y + sta co_size + iny + lda (co_desc),y + sta co_size+1 + ora co_size + beq @bad + + lda co_size + ldx co_size+1 + jsr heap_alloc + bcs @heap_fail + sta co_obj + stx co_obj+1 + + lda co_obj ; object+0 = descriptor + sta co_ptr + lda co_obj+1 + sta co_ptr+1 + ldy #OBJ_DESCRIPTOR + lda co_desc + sta (co_ptr),y + iny + lda co_desc+1 + sta (co_ptr),y + + lda obj_init_ptr + ora obj_init_ptr+1 + beq @ok + lda obj_init_ptr + sta co_vec + lda obj_init_ptr+1 + sta co_vec+1 + lda co_obj + ldx co_obj+1 + jsr obj_call +@ok: + lda co_obj + ldx co_obj+1 + clc + rts +@heap_fail: + lda #OBJ_ERR_HEAP + sta obj_error + lda #$00 + tax + sec + rts +@bad: + lda #OBJ_ERR_BAD + sta obj_error + lda #$00 + tax + sec + rts + +; A/X = object. Optional destructor, then free. Null is successful. +obj_delete: + sta co_obj + stx co_obj+1 + lda #OBJ_OK + sta obj_error + lda co_obj + ora co_obj+1 + bne :+ + jmp @null +: + lda co_obj ; reload: the null test left A = low|high + ldx co_obj+1 + jsr obj_class + bcc :+ + jmp @bad +: + sta co_desc + stx co_desc+1 + + ldy #CD_INSTANCE_SIZE + lda (co_desc),y + sta co_size + iny + lda (co_desc),y + sta co_size+1 + ora co_size + bne :+ + jmp @bad +: + + ldy #CD_DESTRUCTOR + lda (co_desc),y + sta co_destroy + iny + lda (co_desc),y + sta co_destroy+1 + lda co_destroy + ora co_destroy+1 + bne :+ + jmp @free +: + + lda co_destroy + sta co_vec + lda co_destroy+1 + sta co_vec+1 + lda co_obj + ldx co_obj+1 + jsr obj_call + +@free: + lda co_obj + ldx co_obj+1 + jsr heap_free + bcs @heap_fail +@null: + lda #$00 + tax + clc + rts +@heap_fail: + lda #OBJ_ERR_HEAP + sta obj_error + lda #$00 + tax + sec + rts +@bad: + lda #OBJ_ERR_BAD + sta obj_error + lda #$00 + tax + sec + rts + +; Entry in co_vec. A/X pass through to the callee, so the caller passes self +; there. Push a synthetic return address, then JMP (co_vec): the callee's RTS +; returns to @ret, whose RTS returns to the original caller. co_vec must not +; straddle a page because of the 6502 JMP-indirect bug. +obj_call: + sta co_self + stx co_self+1 + lda #>(@ret - 1) + pha + lda #<(@ret - 1) + pha + lda co_self + ldx co_self+1 + jmp (co_vec) +@ret: rts +.assert $FF, error, "co_vec must not straddle a page" + +.segment "BSS" +co_obj: .res 2 +co_size: .res 2 +co_destroy: .res 2 +obj_init_ptr: .res 2 +obj_error: .res 1 diff --git a/newLang/src/runtime/faults.s b/newLang/src/runtime/faults.s new file mode 100644 index 0000000..5914ea5 --- /dev/null +++ b/newLang/src/runtime/faults.s @@ -0,0 +1,87 @@ +; faults.s - runtime fault reporting for generated programs (E04-S05). +; +; flt_report: A = fault code (FLT_*), X/Y = fault address (0 if unknown). +; Writes the mailbox as a generated program failure and prints +; +; RUNTIME FAULT R00n +; +; followed by a carriage return. The code and address are written before +; MB_STATUS, so a monitor polling the mailbox never sees FAIL with stale +; fields. The routine returns; unwinding to the runtime entry is deferred to +; the frame/exception work in E05. It requires the same mapping and zero page +; as src/runtime/print.s (D132). + +.setcpu "6502" +.include "../common/mailbox.inc" +.include "../common/faults.inc" + +.import pr_text, pr_char, pr_cr +.export flt_report, flt_code, flt_addr + +.segment "CODE" +flt_report: + sta flt_code + stx flt_addr + sty flt_addr+1 + + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda flt_code + sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda flt_addr + sta MB_ADDR + lda flt_addr+1 + sta MB_ADDR+1 + lda #MB_ST_FAIL + sta MB_STATUS + + lda #fault_prefix + ldy #fault_prefix_len + jsr pr_text + + lda flt_code ; three digits, leading zeros kept + ldx #$00 +@hundreds: + cmp #100 + bcc @h_done + sbc #100 + inx + bne @hundreds +@h_done: + pha + txa + clc + adc #$30 + jsr pr_char + pla + ldx #$00 +@tens: cmp #10 + bcc @t_done + sbc #10 + inx + bne @tens +@t_done: + pha + txa + clc + adc #$30 + jsr pr_char + pla + clc + adc #$30 + jsr pr_char + jsr pr_cr + rts + +.segment "RODATA" +fault_prefix: .byte "RUNTIME FAULT R" +fault_prefix_end: +fault_prefix_len = fault_prefix_end - fault_prefix +.assert fault_prefix_len = 15, error, "fault prefix changed" + +.segment "BSS" +flt_code: .res 1 +flt_addr: .res 2 diff --git a/newLang/src/runtime/frames.s b/newLang/src/runtime/frames.s index 5a00e33..472a09a 100644 --- a/newLang/src/runtime/frames.s +++ b/newLang/src/runtime/frames.s @@ -159,14 +159,20 @@ frame_leave: inc FRAME_SP+1 @done: rts -; Pop argument bytes after a return. A/X = byte count (low/high). +; Pop argument bytes after a return. A/X = byte count (low/high). A and X +; are preserved so the caller can still store the callee's result. frame_discard: + sta frame_discard_n + stx frame_discard_n+1 clc - adc FRAME_SP + lda FRAME_SP + adc frame_discard_n sta FRAME_SP - txa - adc FRAME_SP+1 + lda FRAME_SP+1 + adc frame_discard_n+1 sta FRAME_SP+1 + lda frame_discard_n + ldx frame_discard_n+1 rts .segment "BSS" @@ -177,4 +183,5 @@ frame_word: .res 2 frame_locals: .res 2 frame_saved_sp: .res 2 frame_saved_fp: .res 2 +frame_discard_n: .res 2 frame_error: .res 1 diff --git a/newLang/src/runtime/heap.s b/newLang/src/runtime/heap.s new file mode 100644 index 0000000..55c1b44 --- /dev/null +++ b/newLang/src/runtime/heap.s @@ -0,0 +1,742 @@ +; heap.s - deterministic generated-program free-list heap (E05-S01). +; +; The caller supplies the region; this module does not choose a generated- +; program heap address. All routines run in the generated program's current +; RAM bank. See src/common/heap.inc for the block layout and API. +; +; Allocation is first-fit in address order. A free block is split when the +; remainder is at least HEAP_MIN_BLOCK. Freeing inserts in address order and +; coalesces with the following free block(s) and then with the preceding one. +; Every boundary, size, magic, and double-free check is always on. + +.setcpu "6502" +.include "../common/heap.inc" + +.export heap_init, heap_alloc, heap_free +.export heap_base, heap_end, heap_free_head, heap_free_total, heap_error + +.segment "ZEROPAGE" +heap_ptr: .res 2 +heap_ptr2: .res 2 + +.segment "CODE" + +; A/X = base (A must be 0), Y = pages (1..127). C=1 with HEAP_ERR_ARG on a +; malformed region. On success the heap is one free block. +heap_init: + sta heap_arg + stx heap_arg+1 + sty heap_arg+2 + lda #HEAP_OK + sta heap_error + + lda heap_arg+2 + beq @bad + cmp #HEAP_MAX_PAGES+1 + bcs @bad + lda heap_arg + bne @bad + + lda heap_arg+1 + clc + adc heap_arg+2 + bcs @bad + cmp #$C1 ; heap must end at or below $C000 + bcs @bad + sta heap_end+1 + lda #$00 + sta heap_end + + lda heap_arg+1 + sta heap_base+1 + lda #$00 + sta heap_base + + lda heap_base + sta heap_free_head + lda heap_base+1 + sta heap_free_head+1 + + lda #$00 + sta heap_free_total + lda heap_arg+2 + sta heap_free_total+1 + + lda heap_base + sta heap_ptr + lda heap_base+1 + sta heap_ptr+1 + ldy #0 + lda #$00 + sta (heap_ptr),y ; size low + iny + lda heap_arg+2 + sta (heap_ptr),y ; size high = pages + iny + lda #$00 + sta (heap_ptr),y ; next low + iny + sta (heap_ptr),y ; next high + clc + rts +@bad: + lda #HEAP_ERR_ARG + sta heap_error + sec + rts + +; A/X = requested payload bytes. Returns A/X = payload, C=0; or A/X=0, C=1 +; with heap_error. The payload is zeroed. +heap_alloc: + sta heap_req + stx heap_req+1 + lda #HEAP_OK + sta heap_error + + lda heap_req + clc + adc #HEAP_HEADER_SIZE + sta heap_need + lda heap_req+1 + adc #0 + sta heap_need+1 + bcc :+ + jmp @oom +: + lda heap_need + and #1 + beq @rounded + inc heap_need + bne @rounded + inc heap_need+1 + bne :+ + jmp @oom +: +@rounded: + lda heap_need+1 + bpl :+ + jmp @oom +: + bne @min_ok + lda heap_need + cmp #HEAP_MIN_BLOCK + bcs @min_ok + lda #HEAP_MIN_BLOCK + sta heap_need + lda #0 + sta heap_need+1 +@min_ok: + lda #0 + sta heap_prev + sta heap_prev+1 + lda heap_free_head + sta heap_cur + lda heap_free_head+1 + sta heap_cur+1 + +@walk: + lda heap_cur + ora heap_cur+1 + bne :+ + jmp @oom +: + lda heap_cur + sta heap_ptr + lda heap_cur+1 + sta heap_ptr+1 + ldy #0 + lda (heap_ptr),y + sta heap_bsize + iny + lda (heap_ptr),y + and #$7F + sta heap_bsize+1 + + lda heap_bsize+1 + cmp heap_need+1 + bcc @next + bne @found + lda heap_bsize + cmp heap_need + bcc @next + +@found: + lda heap_bsize + sec + sbc heap_need + sta heap_rem + lda heap_bsize+1 + sbc heap_need+1 + sta heap_rem+1 + lda heap_rem+1 + beq :+ + jmp @split +: + lda heap_rem + cmp #HEAP_MIN_BLOCK + bcc :+ + jmp @split +: + jmp @use_whole + +@next: + lda heap_cur + sta heap_prev + lda heap_cur+1 + sta heap_prev+1 + ldy #2 + lda (heap_ptr),y + sta heap_cur + iny + lda (heap_ptr),y + sta heap_cur+1 + jmp @walk + +@use_whole: + lda heap_cur + sta heap_block + lda heap_cur+1 + sta heap_block+1 + lda heap_block + sta heap_ptr + lda heap_block+1 + sta heap_ptr+1 + ldy #2 + lda (heap_ptr),y + sta heap_next + iny + lda (heap_ptr),y + sta heap_next+1 + + lda heap_prev + ora heap_prev+1 + beq @uw_head + lda heap_prev + sta heap_ptr + lda heap_prev+1 + sta heap_ptr+1 + ldy #2 + lda heap_next + sta (heap_ptr),y + iny + lda heap_next+1 + sta (heap_ptr),y + jmp @uw_removed +@uw_head: + lda heap_next + sta heap_free_head + lda heap_next+1 + sta heap_free_head+1 +@uw_removed: + lda heap_free_total + sec + sbc heap_bsize + sta heap_free_total + lda heap_free_total+1 + sbc heap_bsize+1 + sta heap_free_total+1 + + jsr @mark_used + lda heap_bsize + sec + sbc #HEAP_HEADER_SIZE + sta heap_zero_len + lda heap_bsize+1 + sbc #0 + sta heap_zero_len+1 + jmp @zero_return + +@split: + lda heap_cur + sta heap_block + lda heap_cur+1 + sta heap_block+1 + lda heap_block + sta heap_ptr + lda heap_block+1 + sta heap_ptr+1 + ldy #2 + lda (heap_ptr),y + sta heap_next + iny + lda (heap_ptr),y + sta heap_next+1 + + lda heap_block + clc + adc heap_need + sta heap_rem_block + lda heap_block+1 + adc heap_need+1 + sta heap_rem_block+1 + + lda heap_rem_block + sta heap_ptr2 + lda heap_rem_block+1 + sta heap_ptr2+1 + ldy #0 + lda heap_rem + sta (heap_ptr2),y + iny + lda heap_rem+1 + sta (heap_ptr2),y + iny + lda heap_next + sta (heap_ptr2),y + iny + lda heap_next+1 + sta (heap_ptr2),y + + lda heap_prev + ora heap_prev+1 + beq @sp_head + lda heap_prev + sta heap_ptr + lda heap_prev+1 + sta heap_ptr+1 + ldy #2 + lda heap_rem_block + sta (heap_ptr),y + iny + lda heap_rem_block+1 + sta (heap_ptr),y + jmp @sp_linked +@sp_head: + lda heap_rem_block + sta heap_free_head + lda heap_rem_block+1 + sta heap_free_head+1 +@sp_linked: + lda heap_need + sta heap_bsize + lda heap_need+1 + sta heap_bsize+1 + jsr @mark_used + + lda heap_free_total + sec + sbc heap_need + sta heap_free_total + lda heap_free_total+1 + sbc heap_need+1 + sta heap_free_total+1 + + lda heap_need + sec + sbc #HEAP_HEADER_SIZE + sta heap_zero_len + lda heap_need+1 + sbc #0 + sta heap_zero_len+1 + jmp @zero_return + +@mark_used: + lda heap_block + sta heap_ptr + lda heap_block+1 + sta heap_ptr+1 + ldy #0 + lda heap_bsize + sta (heap_ptr),y + iny + lda heap_bsize+1 + ora #$80 + sta (heap_ptr),y + iny + lda #$5A + sta (heap_ptr),y + iny + lda #$A5 + sta (heap_ptr),y + rts + +@zero_return: + lda heap_block + clc + adc #HEAP_HEADER_SIZE + sta heap_ptr + sta heap_ptr2 + lda heap_block+1 + adc #0 + sta heap_ptr+1 + sta heap_ptr2+1 + jsr heap_zero + lda heap_ptr2 + ldx heap_ptr2+1 + clc + rts + +@oom: + lda #HEAP_ERR_OOM + sta heap_error + lda #0 + tax + sec + rts + +; Zero heap_zero_len bytes at heap_ptr. A=0 and is preserved. +heap_zero: + ldx heap_zero_len+1 + beq @tail + ldy #0 + lda #0 +@page: + sta (heap_ptr),y + iny + bne @page + inc heap_ptr+1 + dex + bne @page +@tail: + ldy #0 + lda #0 + cpy heap_zero_len + beq @done +@byte: + sta (heap_ptr),y + iny + cpy heap_zero_len + bcc @byte +@done: + rts + +; A/X = payload pointer. Null is a successful no-op. On success the block is +; free and coalesced; on failure C=1 and heap_error is set. +heap_free: + sta heap_arg + stx heap_arg+1 + lda #HEAP_OK + sta heap_error + lda heap_arg + ora heap_arg+1 + bne :+ + jmp @ok +: + lda heap_arg + and #1 + beq :+ + jmp @corrupt +: + + lda heap_arg + sec + sbc #HEAP_HEADER_SIZE + sta heap_block + lda heap_arg+1 + sbc #0 + sta heap_block+1 + bcs @header_ok + jmp @corrupt +@header_ok: + lda heap_block+1 + cmp heap_base+1 + bcs :+ + jmp @corrupt +: + bne @above_base + lda heap_block + cmp heap_base + bcs :+ + jmp @corrupt +: +@above_base: + lda heap_block+1 + cmp heap_end+1 + bcc @in_range + beq :+ + jmp @corrupt +: + lda heap_block + cmp heap_end + bcc :+ + jmp @corrupt +: +@in_range: + lda heap_block + sta heap_ptr + lda heap_block+1 + sta heap_ptr+1 + ldy #0 + lda (heap_ptr),y + sta heap_bsize + iny + lda (heap_ptr),y + sta heap_bsize+1 + + lda heap_bsize+1 + and #$80 + bne :+ + jmp @double +: + lda heap_bsize+1 + and #$7F + sta heap_bsize+1 + + lda heap_bsize+1 + bne @size_hi + lda heap_bsize + cmp #HEAP_MIN_BLOCK + bcs :+ + jmp @corrupt +: +@size_hi: + lda heap_bsize + and #1 + beq :+ + jmp @corrupt +: + + lda heap_block + clc + adc heap_bsize + sta heap_tmp + lda heap_block+1 + adc heap_bsize+1 + sta heap_tmp+1 + bcc :+ + jmp @corrupt +: + lda heap_tmp+1 + cmp heap_end+1 + bcc @size_ok + beq :+ + jmp @corrupt +: + lda heap_tmp + cmp heap_end + beq :+ + jmp @corrupt +: +@size_ok: + ldy #2 + lda (heap_ptr),y + cmp #$5A + beq :+ + jmp @corrupt +: + iny + lda (heap_ptr),y + cmp #$A5 + beq :+ + jmp @corrupt +: + + ldy #0 + lda heap_bsize + sta (heap_ptr),y + iny + lda heap_bsize+1 + sta (heap_ptr),y + iny + lda #0 + sta (heap_ptr),y + iny + sta (heap_ptr),y + + clc + lda heap_free_total + adc heap_bsize + sta heap_free_total + lda heap_free_total+1 + adc heap_bsize+1 + sta heap_free_total+1 + + lda #0 + sta heap_prev + sta heap_prev+1 + lda heap_free_head + sta heap_cur + lda heap_free_head+1 + sta heap_cur+1 + +@find: + lda heap_cur + ora heap_cur+1 + beq @place + lda heap_cur+1 + cmp heap_block+1 + bcc @advance + bne @place + lda heap_cur + cmp heap_block + bcc @advance + bne :+ + jmp @double +: + jmp @place +@advance: + lda heap_cur + sta heap_prev + lda heap_cur+1 + sta heap_prev+1 + lda heap_cur + sta heap_ptr + lda heap_cur+1 + sta heap_ptr+1 + ldy #2 + lda (heap_ptr),y + sta heap_cur + iny + lda (heap_ptr),y + sta heap_cur+1 + jmp @find + +@place: + lda heap_cur + ora heap_cur+1 + beq @merged_next +@merge_next: + lda heap_block + clc + adc heap_bsize + sta heap_tmp + lda heap_block+1 + adc heap_bsize+1 + sta heap_tmp+1 + lda heap_tmp+1 + cmp heap_cur+1 + bne @merged_next + lda heap_tmp + cmp heap_cur + bne @merged_next + + lda heap_cur + sta heap_ptr + lda heap_cur+1 + sta heap_ptr+1 + ldy #0 + lda (heap_ptr),y + clc + adc heap_bsize + sta heap_bsize + iny + lda (heap_ptr),y + adc heap_bsize+1 + sta heap_bsize+1 + ldy #2 + lda (heap_ptr),y + sta heap_next + iny + lda (heap_ptr),y + sta heap_next+1 + lda heap_next + sta heap_cur + lda heap_next+1 + sta heap_cur+1 + jmp @merge_next + +@merged_next: + lda heap_block + sta heap_ptr + lda heap_block+1 + sta heap_ptr+1 + ldy #0 + lda heap_bsize + sta (heap_ptr),y + iny + lda heap_bsize+1 + sta (heap_ptr),y + iny + lda heap_cur + sta (heap_ptr),y + iny + lda heap_cur+1 + sta (heap_ptr),y + + lda heap_prev + ora heap_prev+1 + beq @link_head + lda heap_prev + sta heap_ptr + lda heap_prev+1 + sta heap_ptr+1 + ldy #0 + lda (heap_ptr),y + sta heap_tmp + iny + lda (heap_ptr),y + sta heap_tmp+1 + + lda heap_prev + clc + adc heap_tmp + sta heap_rem + lda heap_prev+1 + adc heap_tmp+1 + sta heap_rem+1 + lda heap_rem+1 + cmp heap_block+1 + bne @link_prev_block + lda heap_rem + cmp heap_block + bne @link_prev_block + + ldy #0 + lda heap_tmp + clc + adc heap_bsize + sta (heap_ptr),y + iny + lda heap_tmp+1 + adc heap_bsize+1 + sta (heap_ptr),y + iny + lda heap_cur + sta (heap_ptr),y + iny + lda heap_cur+1 + sta (heap_ptr),y + jmp @ok +@link_prev_block: + ldy #2 + lda heap_block + sta (heap_ptr),y + iny + lda heap_block+1 + sta (heap_ptr),y + jmp @ok +@link_head: + lda heap_block + sta heap_free_head + lda heap_block+1 + sta heap_free_head+1 + jmp @ok + +@ok: + lda #0 + tax + clc + rts +@double: + lda #HEAP_ERR_DOUBLE + sta heap_error + lda #0 + tax + sec + rts +@corrupt: + lda #HEAP_ERR_CORRUPT + sta heap_error + lda #0 + tax + sec + rts + +.segment "BSS" +heap_base: .res 2 +heap_end: .res 2 +heap_free_head: .res 2 +heap_free_total: .res 2 +heap_error: .res 1 +heap_arg: .res 3 +heap_req: .res 2 +heap_need: .res 2 +heap_bsize: .res 2 +heap_rem: .res 2 +heap_rem_block: .res 2 +heap_cur: .res 2 +heap_prev: .res 2 +heap_next: .res 2 +heap_block: .res 2 +heap_tmp: .res 2 +heap_zero_len: .res 2 diff --git a/newLang/src/runtime/objects.s b/newLang/src/runtime/objects.s new file mode 100644 index 0000000..34fa122 --- /dev/null +++ b/newLang/src/runtime/objects.s @@ -0,0 +1,127 @@ +; objects.s - object and class descriptor accessors (E05-S02). +; +; Objects use the E05-S02 layout in ../common/class.inc: +; object+0 = descriptor pointer, object+2 = first field. +; The helpers are read-only and non-reentrant; they use this module's BSS. +; +; API: +; obj_class A/X = object -> A/X = class descriptor; C=1 if null or +; the object has no descriptor pointer. +; obj_size A/X = object -> A/X = instance size; C=1 if invalid or the +; descriptor's size is zero. +; obj_destroy A/X = object -> A/X = destructor address (0 = none); C=1 +; if the object/descriptor is invalid. +; obj_iface_dir A/X = object -> A/X = interface-directory pointer, Y = +; interface count; C=1 if invalid. +; +; The accessors return C=0 on success. They do not allocate, free, or call a +; destructor; construction and deletion remain E05-S03. + +.setcpu "6502" +.include "../common/class.inc" + +.export obj_class, obj_size, obj_destroy, obj_iface_dir + +.segment "ZEROPAGE" +obj_ptr: .res 2 ; object pointer +obj_desc: .res 2 ; descriptor pointer +obj_tmp: .res 2 ; returned word +obj_count: .res 1 ; interface count + +.segment "CODE" + +; A/X = object. A/X = its descriptor; C=1 if the object or pointer is null. +obj_class: + sta obj_ptr + stx obj_ptr+1 + ora obj_ptr+1 + beq @bad + ldy #OBJ_DESCRIPTOR + lda (obj_ptr),y + sta obj_desc + iny + lda (obj_ptr),y + sta obj_desc+1 + ora obj_desc + beq @bad + lda obj_desc + ldx obj_desc+1 + clc + rts +@bad: lda #$00 + tax + sec + rts + +; A/X = object. A/X = instance size; C=1 if the object/descriptor is invalid. +obj_size: + jsr obj_class + bcs @bad + sta obj_desc + stx obj_desc+1 + ldy #CD_INSTANCE_SIZE + lda (obj_desc),y + sta obj_tmp + iny + lda (obj_desc),y + sta obj_tmp+1 + ora obj_tmp + beq @bad + lda obj_tmp + ldx obj_tmp+1 + clc + rts +@bad: lda #$00 + tax + sec + rts + +; A/X = object. A/X = destructor address, zero meaning none. +obj_destroy: + jsr obj_class + bcs @bad + sta obj_desc + stx obj_desc+1 + ldy #CD_DESTRUCTOR + lda (obj_desc),y + sta obj_tmp + iny + lda (obj_desc),y + sta obj_tmp+1 + lda obj_tmp + ldx obj_tmp+1 + clc + rts +@bad: lda #$00 + tax + sec + rts + +; A/X = object. A/X = interface directory, Y = count. +obj_iface_dir: + jsr obj_class + bcs @bad + sta obj_desc + stx obj_desc+1 + ldy #CD_IFACE_COUNT + lda (obj_desc),y + sta obj_count + ldy #CD_IFACE_DIR + lda (obj_desc),y + sta obj_tmp + iny + lda (obj_desc),y + sta obj_tmp+1 + lda obj_tmp + ldx obj_tmp+1 + ldy obj_count + clc + rts +@bad: lda #$00 + tax + ldy #$00 + sec + rts + +.segment "BSS" +obj_save: .res 0 ; keeps BSS non-empty if fields are optimized diff --git a/newLang/src/runtime/print.s b/newLang/src/runtime/print.s new file mode 100644 index 0000000..1865a73 --- /dev/null +++ b/newLang/src/runtime/print.s @@ -0,0 +1,152 @@ +; print.s - PETSCII text and scalar output for generated programs (E04-S05). +; +; Contract (D132): +; - The caller runs in the D129 generated-program mapping (CR=$0E): KERNAL and +; I/O visible, system zero page mapped, code below $4000. CHROUT can then be +; called directly. The routines never change CR, RCR, or the P0/P1 pointers. +; - The routines are not reentrant: module BSS holds the working values. +; - pr_char preserves X and Y and clobbers A. +; - pr_text takes A/X = address and Y = length (0..255); clobbers A. +; - pr_byte takes A = 0..255; pr_word A = low, X = high; pr_int A/X signed; +; pr_bool treats A != 0 as TRUE. These clobber A/X/Y. +; - No routine appends a newline; call pr_cr for that. + +.setcpu "6502" +.include "../common/c128.inc" +.include "../common/print.inc" + +.export pr_char, pr_cr, pr_text, pr_byte, pr_word, pr_int, pr_bool + +.segment "ZEROPAGE" +pr_ptr: .res 2 ; indirect text pointer (must be zero page) + +.segment "CODE" + +; A = PETSCII character. CHROUT may clobber X/Y, so they are saved here to +; keep the formatting loops simple. +pr_char: + stx pr_sx + sty pr_sy + jsr CHROUT + ldx pr_sx + ldy pr_sy + rts + +pr_cr: + lda #PR_CR + jmp pr_char + +; A/X = address, Y = length. A length of 0 prints nothing. +pr_text: + sta pr_ptr + stx pr_ptr+1 + sty pr_len + lda pr_len + beq @done + ldy #$00 +@loop: lda (pr_ptr),y + jsr pr_char + iny + dec pr_len + bne @loop +@done: rts + +; A = unsigned byte, printed as 1..3 decimal digits with no leading zeros. +pr_byte: + ldx #$00 + jmp pr_word + +; A/X = unsigned word, printed as 1..5 decimal digits with no leading zeros. +pr_word: + sta pr_val + stx pr_val+1 + lda #$00 + sta pr_started + ldx #$00 ; 10000, 1000, 100, 10, 1 +@power: lda pr_pow_lo,x + sta pr_pow + lda pr_pow_hi,x + sta pr_pow+1 + lda #$00 + sta pr_digit +@sub: lda pr_val+1 ; while value >= power + cmp pr_pow+1 + bcc @emit + bne @take + lda pr_val + cmp pr_pow + bcc @emit +@take: sec + lda pr_val + sbc pr_pow + sta pr_val + lda pr_val+1 + sbc pr_pow+1 + sta pr_val+1 + inc pr_digit + jmp @sub +@emit: lda pr_digit + bne @show + lda pr_started + bne @show + cpx #$04 ; the ones digit is always printed + beq @show + jmp @next +@show: lda #$01 + sta pr_started + lda pr_digit + clc + adc #$30 + jsr pr_char +@next: inx + cpx #$05 + bne @power + rts + +; A/X = signed 16-bit integer, printed in minimal decimal form. +pr_int: + sta pr_val + stx pr_val+1 + lda pr_val+1 + bpl @positive + lda #'-' + jsr pr_char + sec + lda #$00 + sbc pr_val + sta pr_val + lda #$00 + sbc pr_val+1 + sta pr_val+1 +@positive: + lda pr_val + ldx pr_val+1 + jmp pr_word + +; A != 0 prints TRUE, A = 0 prints FALSE. +pr_bool: + cmp #$00 + beq @false + lda #pr_true_text + ldy #$04 + jmp pr_text +@false: lda #pr_false_text + ldy #$05 + jmp pr_text + +.segment "RODATA" +pr_pow_lo: .byte $10, $E8, $64, $0A, $01 +pr_pow_hi: .byte $27, $03, $00, $00, $00 +pr_true_text: .byte "TRUE" +pr_false_text: .byte "FALSE" + +.segment "BSS" +pr_len: .res 1 +pr_val: .res 2 +pr_pow: .res 2 +pr_digit: .res 1 +pr_started: .res 1 +pr_sx: .res 1 +pr_sy: .res 1 diff --git a/newLang/tests/codegen/control-test.s b/newLang/tests/codegen/control-test.s new file mode 100644 index 0000000..94326f0 --- /dev/null +++ b/newLang/tests/codegen/control-test.s @@ -0,0 +1,633 @@ +; E04-S04: control-flow emitter fixture in x128. +; +; The fixture generates the same routine twice with flow.s: first in counting +; mode (a sizing pass that writes no bytes), then in emitting mode. Both +; passes must produce identical label offsets and total lengths. The emitted +; routine is then executed and must compute: +; +; a = 0; while a < 5: a = a + 1 -> a = 5 +; if a = 5 then path = 10 else path = 20 -> path = 10 +; for i = 1 to 3: sum = sum + i -> sum = 16, i = 4 +; sum = add(sum) -> sum = 17 (call + return) +; return sum -> A = 17 +; +; A separate routine takes a long conditional branch (distance >= 128) over +; 125 bytes of filler and must reach its target. The fixture also checks the +; five-byte branch encoding: short form has the distance byte plus three NOPs, +; long form has +3 and a live JMP. +; +; Mailbox data: +; +0 program result (17) +; +1 a (5) +; +2 if/else path marker (10) +; +3 i after the for loop (4) +; +4 long-branch result (5A) +; +5 long-branch wrong-path marker (0) +; +6 label/length mismatches between the sizing and emission passes (0) +; +7 branch-form bits: 1 short form verified, 2 long form verified (3) +; Failure bits: 1 sizing, 2 program result, 4 if/else, 8 for loop, +; 16 call/return, 32 long branch, 64 branch bytes, 128 emitter error. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/codegen.inc" +.include "../../src/common/flow.inc" +.include "../../src/common/frames.inc" + +.import cg_init, cg_len, cg_err, cg_code, cg_here +.import cg_lda_imm, cg_ldx_imm, cg_lda_abs, cg_sta_abs +.import cg_adc_abs, cg_cmp_abs, cg_jsr_abs +.import cg_clc, cg_sec, cg_rts +.import fl_init, fl_label, fl_jmp, fl_branch, fl_finish, fl_get_label +.import fl_branch_true, fl_call_byte, fl_prologue, fl_ret_byte +.import fl_err, fl_enter, fl_leave, fl_discard +.import frame_init, frame_enter, frame_leave, frame_discard, frame_push_word + +; Label ids (reset by every fl_init). +L_WHILE = 0 +L_ENDWHILE = 1 +L_ELSE = 2 +L_ENDIF = 3 +L_FOR = 4 +L_ENDFOR = 5 +L_ERR = 6 +L_LONG = 7 +LABEL_IDS = 7 ; ids 0..6 belong to the main routine + +.macro EMIT_LDA addr + lda #<(addr) + ldx #>(addr) + jsr cg_lda_abs +.endmacro + +.macro EMIT_STA addr + lda #<(addr) + ldx #>(addr) + jsr cg_sta_abs +.endmacro + +.macro EMIT_ADC addr + lda #<(addr) + ldx #>(addr) + jsr cg_adc_abs +.endmacro + +.macro EMIT_CMP addr + lda #<(addr) + ldx #>(addr) + jsr cg_cmp_abs +.endmacro + +.macro EMIT_LDA_IMM value + lda #value + jsr cg_lda_imm +.endmacro + +.macro EMIT_LDX_IMM value + lda #value + jsr cg_ldx_imm +.endmacro + +.macro EMIT_JSR addr + lda #<(addr) + ldx #>(addr) + jsr cg_jsr_abs +.endmacro + +; Branch to a global error label even when it is out of relative range. +.macro ON_CARRY target + bcc :+ + jmp target +: +.endmacro + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "STARTUP" +.export test_entry +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #$00 + sta fail_mask + sta mismatch + sta form_mask + sta MB_CODE + sta MB_CODE+1 + jsr init_vars + + ; Runtime addresses for the emitted prologue/call/epilogue. + lda #frame_enter + sta fl_enter+1 + lda #frame_leave + sta fl_leave+1 + lda #frame_discard + sta fl_discard+1 + + lda MMU_CR + sta saved_cr + lda #CR_RAM0_KERNAL + sta MMU_CR + + ; ------------------------------------------- main routine: sizing pass + lda #FL_MODE_COUNT + jsr fl_init + jsr gen_main + ON_CARRY emitter_error + jsr fl_finish + ON_CARRY emitter_error + lda cg_len + sta main_size + jsr save_labels + + ; ------------------------------------------ main routine: emit pass 2 + jsr frame_init + lda #FL_MODE_EMIT + jsr fl_init + jsr gen_main + ON_CARRY emitter_error + jsr fl_finish + ON_CARRY emitter_error + lda cg_len + cmp main_size + beq @len_ok + inc mismatch + lda #$01 + jsr fail +@len_ok: + jsr compare_labels + + ; short branch: distance byte plus three NOPs + ldy pos_while + lda cg_code,y + cmp #128 + bcs @short_bad + iny + lda cg_code,y + cmp #$EA + bne @short_bad + iny + lda cg_code,y + cmp #$EA + bne @short_bad + iny + lda cg_code,y + cmp #$EA + bne @short_bad + lda #$01 + sta form_mask + jmp @run_main +@short_bad: + lda #$40 + jsr fail + + ; ------------------------------------------------ execute the routine +@run_main: + jsr frame_init + lda #$00 + jsr run_code + sta prog_result + + lda prog_result + cmp #17 + beq @check_a + lda #$02 + jsr fail +@check_a: + lda v_a + cmp #5 + beq @check_if + lda #$04 + jsr fail +@check_if: + lda v_ifpath + cmp #10 + beq @check_for + lda #$04 + jsr fail +@check_for: + lda v_i + cmp #4 + beq @check_call + lda #$08 + jsr fail +@check_call: + lda v_sum + cmp #17 + beq @long_begin + lda #$10 + jsr fail + + ; ------------------------------- long branch routine: sizing + emit +@long_begin: + lda #FL_MODE_COUNT + jsr fl_init + jsr gen_long + ON_CARRY emitter_error + jsr fl_finish + ON_CARRY emitter_error + lda cg_len + sta long_size + + lda #FL_MODE_EMIT + jsr fl_init + jsr gen_long + ON_CARRY emitter_error + jsr fl_finish + ON_CARRY emitter_error + lda cg_len + cmp long_size + beq @long_len_ok + inc mismatch + lda #$01 + jsr fail +@long_len_ok: + ; long form: +3 then JMP, and the target is at least 128 bytes away + ldy pos_long + lda cg_code,y + cmp #$03 + bne @long_bad + iny + lda cg_code,y + cmp #$4C + bne @long_bad + ldy pos_long + dey + lda cg_code,y + cmp #$F0 ; BNE was inverted to BEQ + bne @long_bad + lda #L_LONG + jsr fl_get_label + bcs @long_bad + txa + sec + sbc pos_long + sec + sbc #$01 + cmp #128 + bcc @long_bad + lda form_mask + ora #$02 + sta form_mask + jmp @run_long +@long_bad: + lda #$40 + jsr fail + +@run_long: + lda #$00 + jsr run_code + lda v_long + cmp #$5A + beq @check_bad + lda #$20 + jsr fail +@check_bad: + lda v_bad + beq report + lda #$20 + jsr fail + + jmp report + +; ---------------------------------------------------------- report +report: + lda saved_cr + sta MMU_CR + lda prog_result + sta MB_DATA+0 + lda v_a + sta MB_DATA+1 + lda v_ifpath + sta MB_DATA+2 + lda v_i + sta MB_DATA+3 + lda v_long + sta MB_DATA+4 + lda v_bad + sta MB_DATA+5 + lda mismatch + sta MB_DATA+6 + lda form_mask + sta MB_DATA+7 + lda fail_mask + sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda #MB_ST_FAIL + ldx fail_mask + bne @status + lda #MB_ST_PASS +@status: + sta MB_STATUS + ldy #$00 +@print: lda message,y + beq @end + sty print_y + jsr CHROUT + ldy print_y + iny + bne @print +@end: plp + rts + +emitter_error: + lda #$80 + jsr fail + jmp report + +; ------------------------------------------------------------- generators +; if/else/while/for, one call, and a value return. +gen_main: + lda #$00 ; prologue: no locals + ldx #$00 + ldy #L_ERR + jsr fl_prologue + ON_CARRY gen_out + + EMIT_LDA_IMM $00 ; a = 0 + EMIT_STA v_a + EMIT_LDA_IMM $00 ; sum = 0 + EMIT_STA v_sum + + lda #L_WHILE ; while a < 5 + jsr fl_label + EMIT_LDA v_a + EMIT_CMP v_five + jsr cg_here ; the exit branch's distance byte + clc + adc #$01 + sta pos_while + lda #$B0 ; BCS: a >= 5 leaves the loop + ldx #L_ENDWHILE + jsr fl_branch + ON_CARRY gen_out + EMIT_LDA v_a ; a = a + 1 + jsr cg_clc + EMIT_ADC v_one + EMIT_STA v_a + lda #L_WHILE ; back edge: absolute jump + jsr fl_jmp + ON_CARRY gen_out + lda #L_ENDWHILE + jsr fl_label + + EMIT_LDA v_a ; if a = 5 + EMIT_CMP v_five + lda #$D0 ; BNE: else + ldx #L_ELSE + jsr fl_branch + ON_CARRY gen_out + EMIT_LDA_IMM 10 + EMIT_STA v_ifpath + EMIT_STA v_sum + lda #L_ENDIF + jsr fl_jmp + ON_CARRY gen_out + lda #L_ELSE + jsr fl_label + EMIT_LDA_IMM 20 + EMIT_STA v_ifpath + EMIT_STA v_sum + lda #L_ENDIF + jsr fl_label + + EMIT_LDA_IMM 1 ; for i = 1 to 3 + EMIT_STA v_i + lda #L_FOR + jsr fl_label + EMIT_LDA v_i + EMIT_CMP v_four + lda #$B0 ; BCS: i >= 4 leaves the loop + ldx #L_ENDFOR + jsr fl_branch + ON_CARRY gen_out + EMIT_LDA v_sum ; sum = sum + i + jsr cg_clc + EMIT_ADC v_i + EMIT_STA v_sum + EMIT_LDA v_i ; i = i + 1 + jsr cg_clc + EMIT_ADC v_one + EMIT_STA v_i + lda #L_FOR + jsr fl_jmp + ON_CARRY gen_out + lda #L_ENDFOR + jsr fl_label + + EMIT_LDA v_sum ; push(sum) for add() + EMIT_LDX_IMM 0 + EMIT_JSR frame_push_word + ON_CARRY gen_out + lda #$B0 ; push refused a frame + ldx #L_ERR + jsr fl_branch + ON_CARRY gen_out + lda #add + ldy #2 + jsr fl_call_byte + ON_CARRY gen_out + EMIT_STA v_sum + + EMIT_LDA v_sum ; return sum + jsr fl_ret_byte + ON_CARRY gen_out + + lda #L_ERR ; frame or argument-push failure + jsr fl_label + lda #$00 + jsr cg_sec + jmp cg_rts + +; A long taken branch over 125 bytes of filler. +gen_long: + EMIT_LDA_IMM $00 + EMIT_STA v_bad + EMIT_LDA_IMM $01 + jsr cg_here ; remember the branch's distance byte + clc + adc #$01 + sta pos_long + lda #L_LONG + jsr fl_branch_true + ON_CARRY gen_out + EMIT_LDA_IMM $AA ; wrong path if the branch fails + EMIT_STA v_bad + lda #25 ; X is clobbered by the emit macros + sta fill_count +@fill: EMIT_LDA_IMM $11 + EMIT_STA v_filler + dec fill_count + bne @fill + lda #L_LONG + jsr fl_label + EMIT_LDA_IMM $5A + EMIT_STA v_long + jmp cg_rts + +; Shared early exit for both generators: C=1 from the failing call. +gen_out: + rts + +; ---------------------------------------------------------------- helpers +; A = code-buffer offset. Run the snippet and return its A. +run_code: + clc + adc #cg_code + adc #$00 + sta @target+2 +@target: + jsr $FFFF + rts + +save_labels: + lda #$00 + sta label_id +@loop: lda label_id + cmp #LABEL_IDS + beq @done + jsr fl_get_label + bcs @missing + ldy label_id + txa + sta saved_labels,y + inc label_id + jmp @loop +@missing: + inc mismatch + lda #$01 + jsr fail + inc label_id + jmp @loop +@done: rts + +compare_labels: + lda #$00 + sta label_id +@loop: lda label_id + cmp #LABEL_IDS + beq @done + jsr fl_get_label + bcs @bad + ldy label_id + txa + cmp saved_labels,y + beq @next +@bad: inc mismatch + lda #$01 + jsr fail +@next: inc label_id + jmp @loop +@done: rts + +init_vars: + lda #$01 + sta v_one + lda #$03 + sta v_three + lda #$04 + sta v_four + lda #$05 + sta v_five + lda #$00 + sta v_a + sta v_sum + sta v_i + sta v_ifpath + sta v_bad + sta v_long + sta v_filler + sta prog_result + rts + +; add(value as word) as byte: returns value + 1. +add: + lda #$00 + ldx #$00 + jsr frame_enter + bcs @fault + ldy #2 + lda (FRAME_FP),y + clc + adc #$01 + sta add_result + jsr frame_leave + lda add_result + clc + rts +@fault: lda #$00 + sec + rts + +.segment "CODE" +fail: + ora fail_mask + sta fail_mask + rts + +.segment "RODATA" +message: .byte "NOVA128 CONTROL TEST DONE", $0D, $00 + +.segment "BSS" +fail_mask: .res 1 +print_y: .res 1 +saved_cr: .res 1 +label_id: .res 1 +mismatch: .res 1 +form_mask: .res 1 +main_size: .res 1 +long_size: .res 1 +pos_while: .res 1 +pos_long: .res 1 +prog_result: .res 1 +v_one: .res 1 +v_three: .res 1 +v_four: .res 1 +v_five: .res 1 +v_a: .res 1 +v_sum: .res 1 +v_i: .res 1 +v_ifpath: .res 1 +v_bad: .res 1 +v_long: .res 1 +v_filler: .res 1 +add_result: .res 1 +saved_labels: .res LABEL_IDS +fill_count: .res 1 diff --git a/newLang/tests/compiler/hello-test.s b/newLang/tests/compiler/hello-test.s new file mode 100644 index 0000000..1e5aa01 --- /dev/null +++ b/newLang/tests/compiler/hello-test.s @@ -0,0 +1,265 @@ +; E04-S05: compile hello.n8 and emit a runnable PRG (x128). +; +; The fixture runs in the compiler mapping, loads HELLO.N8, parses and +; type-checks it normally, and reads the single string print captured during +; pass 2. It then writes HELLO.PRG with output.s: the standard BASIC stub at +; $1C01 and a body at $1C0D that saves CR, selects the D129 mapping ($0E), +; prints the captured bytes through CHROUT, restores CR, and returns to BASIC. +; +; This is a bounded source-to-code proof, not the general statement emitter: +; one string print is required and gen_bad rejects anything else. +; +; Mailbox data: +0 diagnostics, +1 print count, +2 gen_bad, +3 text length, +; +4 out_err. FAIL code is the stage: 1 load, 2 diagnostics, 3 unsupported +; print, 4 output. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/formats.inc" + +.import mmu_init, mmu_bank0, mmu_shutdown +.import src_load, src_dos_code, src_kernal_err +.import names_init +.import diag_init, diag_finish, diag_screen +.import parse_file, gen_text, gen_text_len, gen_print_count, gen_bad +.import out_begin, out_section, out_end_section +.import out_byte, out_bytes, out_finish, out_abort +.import out_load, out_entry, out_start, out_err +.import gen_stub + +; A conditional branch to a far error path: near branch over an absolute JMP. +.macro ON_CARRY target + bcc :+ + jmp target +: +.endmacro + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "STARTUP" +.export test_entry +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_COMPILER + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #$00 + sta MB_DATA + sta MB_DATA+1 + sta MB_DATA+2 + sta MB_DATA+3 + sta MB_DATA+4 + sta MB_DATA+5 + sta MB_DATA+6 + sta MB_DATA+7 + + jsr mmu_init + jsr mmu_bank0 + + lda #PRG_LOAD_ADDR + sta out_load+1 + lda #PRG_ENTRY_ADDR + sta out_entry+1 + + lda #name_src + ldy #name_src_len + jsr src_load + cmp #$00 + bne @fail_load + + jsr names_init + lda #name_src + ldy #name_src_len + jsr diag_init + lda #$00 ; ERR files only; keep the screen quiet + sta diag_screen + jsr parse_file + jsr diag_finish + sta diag_count + + lda diag_count + sta MB_DATA+0 + lda gen_print_count + sta MB_DATA+1 + lda gen_bad + sta MB_DATA+2 + lda gen_text_len + sta MB_DATA+3 + + lda diag_count + bne @fail_diag + lda gen_bad + bne @fail_gen + lda gen_print_count + cmp #1 + bne @fail_gen + lda gen_text_len + beq @fail_gen + + jsr emit_hello + bcc @ok + lda emit_err + sta MB_DATA+4 + lda #4 + jmp @fail +@ok: lda out_err + sta MB_DATA+4 + lda #MB_ST_PASS + sta MB_STATUS + jmp @done +@fail_load: + sta MB_DATA+5 ; src_load result + lda src_dos_code + sta MB_DATA+6 + lda src_kernal_err + sta MB_DATA+7 + lda #1 + jmp @fail +@fail_diag: + lda #2 + jmp @fail +@fail_gen: + lda #3 +@fail: sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda #MB_ST_FAIL + sta MB_STATUS +@done: jsr mmu_shutdown + plp + rts + +; Write HELLO.PRG through output.s. C=1 on any output error. +emit_hello: + lda #name_hello + ldy #name_hello_len + jsr out_begin + ON_CARRY @abort + + lda #PRG_LOAD_ADDR + sta out_start+1 + lda #sec_stub + ldy #4 + jsr out_section + ON_CARRY @abort + lda #gen_stub + ldy #PRG_STUB_SIZE + jsr out_bytes + ON_CARRY @abort + jsr out_end_section + ON_CARRY @abort + + lda #PRG_ENTRY_ADDR + sta out_start+1 + lda #sec_code + ldy #4 + jsr out_section + ON_CARRY @abort + lda #prologue + ldy #prologue_len + jsr out_bytes + ON_CARRY @abort + + ldy #$00 +@chars: cpy gen_text_len + beq @epilogue + sty temp_y + lda #$A9 ; LDA #char + jsr out_byte + ON_CARRY @abort + ldy temp_y + lda gen_text,y + jsr out_byte + ON_CARRY @abort + lda #$20 ; JSR $FFD2 (CHROUT) + jsr out_byte + ON_CARRY @abort + lda #$D2 + jsr out_byte + ON_CARRY @abort + lda #$FF + jsr out_byte + ON_CARRY @abort + ldy temp_y + iny + bne @chars + +@epilogue: + lda #epilogue + ldy #epilogue_len + jsr out_bytes + ON_CARRY @abort + jsr out_end_section + ON_CARRY @abort + jsr out_finish + ON_CARRY @abort + clc + rts +@abort: lda out_err + sta emit_err + jsr out_abort + sec + rts + +.segment "RODATA" +name_src: .byte "HELLO.N8" +name_src_len = * - name_src +name_hello: .byte "HELLO" +name_hello_len = * - name_hello +sec_stub: .byte "STUB" +sec_code: .byte "CODE" + +; LDA MMU_CR / PHA / LDA #$0E / STA MMU_CR +prologue: + .byte $AD, $00, $FF, $48, $A9, $0E, $8D, $00, $FF +prologue_len = * - prologue +; PLA / STA MMU_CR / RTS +epilogue: + .byte $68, $8D, $00, $FF, $60 +epilogue_len = * - epilogue + +.segment "BSS" +diag_count: .res 1 +temp_y: .res 1 +emit_err: .res 1 diff --git a/newLang/tests/compiler/hello.n8 b/newLang/tests/compiler/hello.n8 new file mode 100644 index 0000000..e0b7218 --- /dev/null +++ b/newLang/tests/compiler/hello.n8 @@ -0,0 +1,4 @@ +// E04-S05 end-to-end source: one string print. +program hello + print "hello from nova128" +end program diff --git a/newLang/tests/runtime/class-test.s b/newLang/tests/runtime/class-test.s new file mode 100644 index 0000000..535004c --- /dev/null +++ b/newLang/tests/runtime/class-test.s @@ -0,0 +1,340 @@ +; E05-S02: class descriptor and object-header test in x128. +; +; The fixture checks the packed descriptor ABI and its runtime accessors: +; EMPTY: size 7, no destructor, no interfaces +; BOX: size 35, destructor, one interface-directory entry +; It then writes CLASSTEST.PRG/MAP through the real output writer. MB_DATA +; flags 0..7 report descriptor, size, destructor, interface, and MAP success. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/formats.inc" +.include "../../src/common/class.inc" + +.import mmu_init, mmu_bank0, mmu_shutdown +.import out_begin, out_startup, out_finish, out_abort +.import out_load, out_entry, out_err +.import out_class, out_class_size, out_class_destroy +.import out_class_ifaces, out_class_ifdir +.import obj_class, obj_size, obj_destroy, obj_iface_dir + +.export destroy_b, iface_dir + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "STARTUP" +.export test_entry +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #$00 + sta fail_mask + sta MB_CODE + sta MB_CODE+1 + ldx #$07 +@flags: sta MB_DATA,x + dex + bpl @flags + + jsr mmu_init + jsr mmu_bank0 + + ; ------------------------------------------------ descriptor: EMPTY + lda #obj_empty + jsr obj_class + bcs @p0_fail + cmp #desc_empty + bne @p0_fail + lda #$00 ; null object and null descriptor pointer + ldx #$00 + jsr obj_class + bcc @p0_fail + lda #$01 + sta MB_DATA+0 + jmp @p1 +@p0_fail: + lda #$01 + jsr fail + +@p1: ; ------------------------------------------------------ size: EMPTY + lda #obj_empty + jsr obj_size + bcs @p1_fail + cmp #7 + bne @p1_fail + cpx #$00 + bne @p1_fail + lda #$01 + sta MB_DATA+1 + jmp @p2 +@p1_fail: + lda #$02 + jsr fail + +@p2: ; -------------------------------------------------- destructor: EMPTY + lda #obj_empty + jsr obj_destroy + bcs @p2_fail + cmp #$00 + bne @p2_fail + cpx #$00 + bne @p2_fail + lda #$01 + sta MB_DATA+2 + jmp @p3 +@p2_fail: + lda #$04 + jsr fail + +@p3: ; ------------------------------------------------- descriptor: BOX + lda #obj_box + jsr obj_class + bcs @p3_fail + cmp #desc_box + bne @p3_fail + lda #$01 + sta MB_DATA+3 + jmp @p4 +@p3_fail: + lda #$08 + jsr fail + +@p4: ; ------------------------------------------------------ size: BOX + lda #obj_box + jsr obj_size + bcs @p4_fail + cmp #35 + bne @p4_fail + cpx #$00 + bne @p4_fail + lda #$01 + sta MB_DATA+4 + jmp @p5 +@p4_fail: + lda #$10 + jsr fail + +@p5: ; ------------------------------------------------- destructor: BOX + lda #obj_box + jsr obj_destroy + bcs @p5_fail + cmp #destroy_b + bne @p5_fail + lda #$01 + sta MB_DATA+5 + jmp @p6 +@p5_fail: + lda #$20 + jsr fail + +@p6: ; ------------------------------------------------ interface directory + lda #obj_box + jsr obj_iface_dir + bcs @p6_fail + cmp #iface_dir + bne @p6_fail + cpy #1 + bne @p6_fail + lda #$01 + sta MB_DATA+6 + jmp @p7 +@p6_fail: + lda #$40 + jsr fail + +@p7: ; -------------------------------------------------------- MAP output + lda #PRG_LOAD_ADDR + sta out_load+1 + lda #PRG_ENTRY_ADDR + sta out_entry+1 + lda #name_source + ldy #name_source_len + jsr out_begin + bcs @p7_abort + jsr out_startup + bcs @p7_abort + + lda #7 ; EMPTY: size 7, no destructor/interface + sta out_class_size + lda #$00 + sta out_class_size+1 + sta out_class_destroy + sta out_class_destroy+1 + sta out_class_ifaces + sta out_class_ifdir + sta out_class_ifdir+1 + lda #name_empty + ldy #name_empty_len + jsr out_class + bcs @p7_abort + + lda #35 ; BOX: destructor and one interface + sta out_class_size + lda #$00 + sta out_class_size+1 + lda #destroy_b + sta out_class_destroy+1 + lda #1 + sta out_class_ifaces + lda #iface_dir + sta out_class_ifdir+1 + lda #name_box + ldy #name_box_len + jsr out_class + bcs @p7_abort + + jsr out_finish + bcs @p7_fail + lda #$01 + sta MB_DATA+7 + jmp @done +@p7_abort: + lda out_err + sta MB_ADDR + jsr out_abort +@p7_fail: + lda out_err + sta MB_ADDR + lda #$80 + jsr fail + +@done: + jsr mmu_shutdown + lda fail_mask + sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda #MB_ST_FAIL + ldx fail_mask + bne @status + lda #MB_ST_PASS +@status: + sta MB_STATUS + lda fail_mask + beq @ok_message + ldy #fail_message - messages + bne @print +@ok_message: + ldy #ok_message - messages +@print: lda messages,y + beq @end + sty print_y + jsr CHROUT + ldy print_y + iny + bne @print +@end: rts + +.segment "CODE" +fail: + ora fail_mask + sta fail_mask + rts + +destroy_b: + rts + +method_stub: + rts + +.segment "DATA" +desc_empty: + .word 7 + .word 0 + .byte 0 + .word 0 + .byte 0 +.assert * - desc_empty = CLASS_DESC_SIZE, error, "EMPTY descriptor size" + +desc_box: + .word 35 + .word destroy_b + .byte 1 + .word iface_dir + .byte 0 +.assert * - desc_box = CLASS_DESC_SIZE, error, "BOX descriptor size" + +iface_dir: + .word iface_stub + .word iface_table +iface_stub: + .word 0 +iface_table: + .word method_stub + +obj_empty: + .word desc_empty + .res 5 +obj_box: + .word desc_box + .res 33 +.assert obj_box - obj_empty = 7, error, "EMPTY object size" +.assert * - obj_box = 35, error, "BOX object size" + +.segment "RODATA" +name_source: .byte "CLASSTEST.N8" +name_source_len = * - name_source +name_empty: .byte "EMPTY" +name_empty_len = * - name_empty +name_box: .byte "BOX" +name_box_len = * - name_box +messages: +ok_message: .byte "NOVA128 CLASS TEST OK", $0D, $00 +fail_message: .byte "NOVA128 CLASS TEST FAIL", $0D, $00 + +.segment "BSS" +fail_mask: .res 1 +print_y: .res 1 diff --git a/newLang/tests/runtime/heap-test.s b/newLang/tests/runtime/heap-test.s new file mode 100644 index 0000000..5cc242d --- /dev/null +++ b/newLang/tests/runtime/heap-test.s @@ -0,0 +1,584 @@ +; E05-S01: free-list heap fixture in x128. +; +; Runs under CR=$0E with the heap at $4000-$4FFF (16 pages). All code and BSS +; stay below $4000. The fixture checks the allocator and records one flag per +; phase in MB_DATA; the final exhaustion check invokes the stable R005 fault +; path, so the mailbox intentionally ends FAIL with code 5. +; +; Expected screen: +; NOVA128 HEAP TEST +; HEAP OK +; RUNTIME FAULT R005 + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/print.inc" +.include "../../src/common/faults.inc" +.include "../../src/common/heap.inc" + +.import pr_char, pr_cr, pr_text +.import flt_report +.import heap_init, heap_alloc, heap_free +.import heap_base, heap_end, heap_free_head, heap_free_total, heap_error + +HEAP_TEST_BASE = $4000 +HEAP_TEST_END = $5000 +HEAP_TEST_PAGES = 16 +HEAP_TEST_SIZE = $1000 + +.macro TEXT src, len + lda #src + ldy #len + jsr pr_text +.endmacro + +.macro PHASE number + lda #number + sta MB_CODE + lda #$00 + sta MB_CODE+1 +.endmacro + +.macro FAIL_CS + bcc :+ + jmp @fail +: +.endmacro + +.macro FAIL_CC + bcs :+ + jmp @fail +: +.endmacro + +.macro FAIL_NE + beq :+ + jmp @fail +: +.endmacro + +.macro FAIL_EQ + bne :+ + jmp @fail +: +.endmacro + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "ZEROPAGE" +tptr: .res 2 + +.segment "STARTUP" +.export test_entry, test_end +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + + lda MMU_CR + sta saved_cr + lda #CR_RAM0_KERNAL + sta MMU_CR + + lda #PR_CLEAR + jsr pr_char + TEXT title, 17 + jsr pr_cr + + ; ------------------------------------------------------------- init + PHASE 1 + lda #$00 + ldx #>HEAP_TEST_BASE + ldy #HEAP_TEST_PAGES + jsr heap_init + FAIL_CS + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda heap_free_head + cmp #$00 + FAIL_NE + lda heap_free_head+1 + cmp #$40 + FAIL_NE + lda HEAP_TEST_BASE + cmp #$00 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE+2 + ora HEAP_TEST_BASE+3 + FAIL_NE + lda #$01 + sta MB_DATA+0 + + ; ------------------------------------------- one allocation and free + PHASE 2 + lda #32 + ldx #0 + jsr heap_alloc + FAIL_CS + cmp #$04 + FAIL_NE + cpx #$40 + FAIL_NE + lda heap_free_total + cmp #$DC + FAIL_NE + lda heap_free_total+1 + cmp #$0F + FAIL_NE + lda HEAP_TEST_BASE + cmp #$24 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$80 + FAIL_NE + lda HEAP_TEST_BASE+2 + cmp #$5A + FAIL_NE + lda HEAP_TEST_BASE+3 + cmp #$A5 + FAIL_NE + ldy #0 +@zero: lda HEAP_TEST_BASE+4,y + FAIL_NE + iny + cpy #32 + bne @zero + lda #$A5 + ldy #0 +@fill: sta HEAP_TEST_BASE+4,y + iny + cpy #32 + bne @fill + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CS + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE + cmp #$00 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE+2 + ora HEAP_TEST_BASE+3 + FAIL_NE + lda #$01 + sta MB_DATA+1 + + ; ------------------------------------------ split and coalesce twice + PHASE 3 + lda #16 + ldx #0 + jsr heap_alloc + FAIL_CS + cmp #$04 + FAIL_NE + cpx #$40 + FAIL_NE + lda heap_free_total + cmp #$EC + FAIL_NE + lda heap_free_total+1 + cmp #$0F + FAIL_NE + lda HEAP_TEST_BASE + cmp #$14 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$80 + FAIL_NE + lda HEAP_TEST_BASE+$14 + cmp #$EC + FAIL_NE + lda HEAP_TEST_BASE+$15 + cmp #$0F + FAIL_NE + + lda #16 + ldx #0 + jsr heap_alloc + FAIL_CS + cmp #$18 + FAIL_NE + cpx #$40 + FAIL_NE + lda heap_free_total + cmp #$D8 + FAIL_NE + lda heap_free_total+1 + cmp #$0F + FAIL_NE + lda HEAP_TEST_BASE+$28 + cmp #$D8 + FAIL_NE + lda HEAP_TEST_BASE+$29 + cmp #$0F + FAIL_NE + + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CS + lda #$18 + ldx #$40 + jsr heap_free + FAIL_CS + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE + cmp #$00 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE+2 + ora HEAP_TEST_BASE+3 + FAIL_NE + lda #$01 + sta MB_DATA+2 + + ; ---------------------------------------------------- stress pass + PHASE 4 + lda #0 + sta stress_i +@alloc: + lda #64 + ldx #0 + jsr heap_alloc + FAIL_CS + sta tptr + stx tptr+1 + ldx stress_i + lda tptr + sta ptrs,x + lda tptr+1 + sta ptrs+1,x + ldy #0 + lda tptr + sta (tptr),y + ldy #63 + lda #$A5 + sta (tptr),y + lda stress_i + clc + adc #2 + sta stress_i + cmp #32 + bne @alloc + + lda #0 + sta stress_i +@verify: + ldx stress_i + lda ptrs,x + sta tptr + lda ptrs+1,x + sta tptr+1 + ldy #0 + lda (tptr),y + cmp tptr + FAIL_NE + ldy #63 + lda (tptr),y + cmp #$A5 + FAIL_NE + lda stress_i + clc + adc #2 + sta stress_i + cmp #32 + bne @verify + + lda #0 + sta stress_i +@free_even: + ldx stress_i + lda ptrs,x + sta tptr + lda ptrs+1,x + sta tptr+1 + lda tptr + ldx tptr+1 + jsr heap_free + FAIL_CS + lda stress_i + clc + adc #4 + sta stress_i + cmp #32 + bne @free_even + + lda #30 + sta stress_i +@free_odd: + ldx stress_i + lda ptrs,x + sta tptr + lda ptrs+1,x + sta tptr+1 + lda tptr + ldx tptr+1 + jsr heap_free + FAIL_CS + lda stress_i + sec + sbc #4 + sta stress_i + bcs @free_odd + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE + cmp #$00 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+3 + + ; --------------------------------------------------- double free + PHASE 5 + lda #16 + ldx #0 + jsr heap_alloc + FAIL_CS + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CS + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CC + lda heap_error + cmp #HEAP_ERR_DOUBLE + FAIL_NE + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+4 + + ; ------------------------------------------------ corrupt header + PHASE 6 + lda #16 + ldx #0 + jsr heap_alloc + FAIL_CS + lda #$00 + sta HEAP_TEST_BASE+2 + sta HEAP_TEST_BASE+3 + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CC + lda heap_error + cmp #HEAP_ERR_CORRUPT + FAIL_NE + lda heap_free_total + cmp #$EC + FAIL_NE + lda heap_free_total+1 + cmp #$0F + FAIL_NE + lda #$5A + sta HEAP_TEST_BASE+2 + lda #$A5 + sta HEAP_TEST_BASE+3 + lda #$04 + ldx #$40 + jsr heap_free + FAIL_CS + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+5 + + ; ---------------------------------------- exhaustion and integrity + PHASE 7 + lda #0 + sta stress_i +@ex_alloc: + lda #128 + ldx #0 + jsr heap_alloc + FAIL_CS + sta tptr + stx tptr+1 + ldx stress_i + lda tptr + sta ptrs,x + lda tptr+1 + sta ptrs+1,x + ldy #0 + lda #$C3 + sta (tptr),y + ldy #127 + lda #$A5 + sta (tptr),y + lda stress_i + clc + adc #2 + sta stress_i + cmp #6 + bne @ex_alloc + lda heap_free_total + cmp #$74 + FAIL_NE + lda heap_free_total+1 + cmp #$0E + FAIL_NE + + lda #$00 + ldx #$10 + jsr heap_alloc + FAIL_CC + lda heap_error + cmp #HEAP_ERR_OOM + FAIL_NE + lda heap_free_total + cmp #$74 + FAIL_NE + lda heap_free_total+1 + cmp #$0E + FAIL_NE + + lda #0 + sta stress_i +@ex_verify: + ldx stress_i + lda ptrs,x + sta tptr + lda ptrs+1,x + sta tptr+1 + ldy #0 + lda (tptr),y + cmp #$C3 + FAIL_NE + ldy #127 + lda (tptr),y + cmp #$A5 + FAIL_NE + lda stress_i + clc + adc #2 + sta stress_i + cmp #6 + bne @ex_verify + + lda #0 + sta stress_i +@ex_free: + ldx stress_i + lda ptrs,x + sta tptr + lda ptrs+1,x + sta tptr+1 + lda tptr + ldx tptr+1 + jsr heap_free + FAIL_CS + lda stress_i + clc + adc #2 + sta stress_i + cmp #6 + bne @ex_free + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda HEAP_TEST_BASE + cmp #$00 + FAIL_NE + lda HEAP_TEST_BASE+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+6 + lda #$01 + sta MB_DATA+7 + + TEXT heap_ok, 7 + jsr pr_cr + lda #FLT_HEAP + ldx #$00 + ldy #$40 + jsr flt_report + jmp @done + +@fail: + lda #MB_ST_FAIL + sta MB_STATUS +@done: + lda saved_cr + sta MMU_CR + plp + rts + +.segment "RODATA" +title: .byte "NOVA128 HEAP TEST" +heap_ok: .byte "HEAP OK" + +.segment "BSS" +saved_cr: .res 1 +stress_i: .res 1 +ptrs: .res 32 +test_end: diff --git a/newLang/tests/runtime/object-test.s b/newLang/tests/runtime/object-test.s new file mode 100644 index 0000000..b9eb9aa --- /dev/null +++ b/newLang/tests/runtime/object-test.s @@ -0,0 +1,419 @@ +; E05-S03: construction/deletion fixture in x128. +; +; Runs under CR=$0E with a 4K heap at $4000. It checks: +; - object header and zeroed fields, then init called once with two args; +; - delete null is a no-op; +; - delete calls destroy once with self/fields intact and frees the object; +; - failed allocation does not call init; +; - a class without a destructor still frees correctly. +; +; MB_DATA flags 0..4 report the five phases. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/print.inc" +.include "../../src/common/heap.inc" +.include "../../src/common/class.inc" +.include "../../src/common/frames.inc" + +.import pr_char, pr_cr, pr_text +.import heap_init, heap_alloc, heap_free, heap_free_total, heap_error +.import frame_init, frame_enter, frame_leave, frame_push_byte, frame_push_word, frame_discard +.import obj_class +.import obj_new, obj_delete, obj_init_ptr, obj_error + +HEAP_BASE = $4000 +HEAP_PAGES = 16 + +.macro TEXT src, len + lda #src + ldy #len + jsr pr_text +.endmacro + +.macro PHASE n + lda #n + sta MB_CODE + lda #$00 + sta MB_CODE+1 +.endmacro + +.macro FAIL_CS + bcc :+ + jmp @fail +: +.endmacro + +.macro FAIL_CC + bcs :+ + jmp @fail +: +.endmacro + +.macro FAIL_NE + beq :+ + jmp @fail +: +.endmacro + +.macro FAIL_EQ + bne :+ + jmp @fail +: +.endmacro + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "ZEROPAGE" +tptr: .res 2 + +.segment "STARTUP" +.export test_entry, test_end +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + lda #$00 + sta MB_CODE + sta MB_CODE+1 + + lda MMU_CR + sta saved_cr + lda #CR_RAM0_KERNAL + sta MMU_CR + lda #PR_CLEAR + jsr pr_char + TEXT title, 18 + jsr pr_cr + + PHASE 1 + lda #$00 + ldx #>HEAP_BASE + ldy #HEAP_PAGES + jsr heap_init + FAIL_CS + jsr frame_init + lda #$00 + sta init_count + sta destroy_count + sta init_error + + ; Construct Res(value=$1234, tag=$2A). + lda #$2A + jsr frame_push_byte ; right-to-left: tag first + lda #$34 + ldx #$12 + jsr frame_push_word ; then value + lda #init_res + sta obj_init_ptr+1 + lda #desc_res + jsr obj_new + FAIL_CS + sta new_obj + stx new_obj+1 + lda #3 + ldx #$00 + jsr frame_discard + + lda init_error + FAIL_NE + lda init_count + cmp #1 + FAIL_NE + lda init_self + cmp new_obj + FAIL_NE + lda init_self+1 + cmp new_obj+1 + FAIL_NE + lda init_tag + cmp #$2A + FAIL_NE + lda init_word + cmp #$34 + FAIL_NE + lda init_word+1 + cmp #$12 + FAIL_NE + lda init_tag_before + FAIL_NE + lda init_word_before + ora init_word_before+1 + FAIL_NE + + lda new_obj + ldx new_obj+1 + jsr obj_class + FAIL_CS + cmp #desc_res + FAIL_NE + lda #$01 + sta MB_DATA+0 + + ; delete null must not call destroy. + PHASE 2 + lda #$00 + ldx #$00 + jsr obj_delete + FAIL_CS + lda obj_error + cmp #OBJ_OK + FAIL_NE + lda destroy_count + FAIL_NE + lda #$01 + sta MB_DATA+1 + + ; Deleting the object calls destroy once, with self and fields intact. + PHASE 3 + lda new_obj + ldx new_obj+1 + jsr obj_delete + FAIL_CS + lda obj_error + cmp #OBJ_OK + FAIL_NE + lda destroy_count + cmp #1 + FAIL_NE + lda destroy_self + cmp new_obj + FAIL_NE + lda destroy_self+1 + cmp new_obj+1 + FAIL_NE + lda destroy_tag + cmp #$2A + FAIL_NE + lda destroy_word + cmp #$34 + FAIL_NE + lda destroy_word+1 + cmp #$12 + FAIL_NE + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+2 + + ; Allocation failure must not call init. + PHASE 4 + lda #init_res + sta obj_init_ptr+1 + lda #desc_huge + jsr obj_new + FAIL_CC + lda obj_error + cmp #OBJ_ERR_HEAP + FAIL_NE + lda init_count + cmp #1 + FAIL_NE + lda #$01 + sta MB_DATA+3 + + ; A class with no destructor still frees its complete object. + PHASE 5 + lda #$00 + sta obj_init_ptr + sta obj_init_ptr+1 + lda #desc_plain + jsr obj_new + FAIL_CS + sta plain_obj + stx plain_obj+1 + lda plain_obj + ldx plain_obj+1 + jsr obj_delete + FAIL_CS + lda obj_error + cmp #OBJ_OK + FAIL_NE + lda destroy_count + cmp #1 + FAIL_NE + lda heap_free_total + cmp #$00 + FAIL_NE + lda heap_free_total+1 + cmp #$10 + FAIL_NE + lda #$01 + sta MB_DATA+4 + + TEXT object_ok, 9 + jsr pr_cr + lda #MB_ST_PASS + sta MB_STATUS + lda #$00 + sta MB_CODE + sta MB_CODE+1 + jmp @restore + +@fail: + lda #MB_ST_FAIL + sta MB_STATUS + TEXT object_fail, 11 + jsr pr_cr +@restore: + lda saved_cr + sta MMU_CR + plp + rts + +; init(value as word, tag as byte), arguments at FP+2..FP+4. +init_res: + sta init_self + stx init_self+1 + lda #$00 + ldx #$00 + jsr frame_enter + bcc @entered + lda #1 + sta init_error + rts +@entered: + ldy #2 + lda (FRAME_FP),y + sta init_word + iny + lda (FRAME_FP),y + sta init_word+1 + iny + lda (FRAME_FP),y + sta init_tag + + lda init_self + sta tptr + lda init_self+1 + sta tptr+1 + ldy #2 + lda (tptr),y + sta init_tag_before + ldy #3 + lda (tptr),y + sta init_word_before + iny + lda (tptr),y + sta init_word_before+1 + + ldy #2 + lda init_tag + sta (tptr),y + ldy #3 + lda init_word + sta (tptr),y + iny + lda init_word+1 + sta (tptr),y + inc init_count + jsr frame_leave + clc + rts + +; No-argument destructor; it records self and the fields left by init. +destroy_res: + sta destroy_self + stx destroy_self+1 + inc destroy_count + lda destroy_self + sta tptr + lda destroy_self+1 + sta tptr+1 + ldy #2 + lda (tptr),y + sta destroy_tag + ldy #3 + lda (tptr),y + sta destroy_word + iny + lda (tptr),y + sta destroy_word+1 + rts + +.segment "DATA" +desc_res: + .word 5 ; header + tag + word + .word destroy_res + .byte 0 + .word 0 + .byte 0 +.assert * - desc_res = CLASS_DESC_SIZE, error, "Res descriptor size" + +desc_plain: + .word 2 ; header only + .word 0 + .byte 0 + .word 0 + .byte 0 +.assert * - desc_plain = CLASS_DESC_SIZE, error, "plain descriptor size" + +desc_huge: + .word $FFFF + .word 0 + .byte 0 + .word 0 + .byte 0 +.assert * - desc_huge = CLASS_DESC_SIZE, error, "huge descriptor size" + +.segment "RODATA" +title: .byte "NOVA128 OBJECT TEST" +object_ok: .byte "OBJECT OK" +object_fail: .byte "OBJECT FAIL" + +.segment "BSS" +saved_cr: .res 1 +new_obj: .res 2 +plain_obj: .res 2 +init_count: .res 1 +init_error: .res 1 +init_self: .res 2 +init_word: .res 2 +init_tag: .res 1 +init_tag_before: .res 1 +init_word_before: .res 2 +destroy_count: .res 1 +destroy_self: .res 2 +destroy_tag: .res 1 +destroy_word: .res 2 +test_end: diff --git a/newLang/tests/runtime/print-test.s b/newLang/tests/runtime/print-test.s new file mode 100644 index 0000000..1574f9b --- /dev/null +++ b/newLang/tests/runtime/print-test.s @@ -0,0 +1,193 @@ +; E04-S05: print and fault runtime test in x128. +; +; Runs in the D129 generated-program mapping (CR=$0E), where the KERNAL and +; I/O are visible and $B800-$BFFF is RAM; all code and BSS stay below $4000. +; The screen is cleared first, then: +; +; NOVA128 PRINT TEST +; TEXT OK +; BYTE 0 7 255 +; WORD 0 12345 65535 +; INT -1 -32768 32767 +; BOOL FALSE TRUE +; CHAR A +; RUNTIME FAULT R001 .. R005 +; +; The faults run last, so the mailbox ends in MB_ST_FAIL with code 5 and +; address $1234. The host checks the screen rows and the mailbox fields. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/print.inc" +.include "../../src/common/faults.inc" + +.import pr_char, pr_cr, pr_text, pr_byte, pr_word, pr_int, pr_bool +.import flt_report + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.macro TEXT text, len + lda #text + ldy #len + jsr pr_text +.endmacro + +.macro SPACE + lda #PR_SPACE + jsr pr_char +.endmacro + +.segment "STARTUP" +.export test_entry, test_end +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_PROGRAM + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + + lda MMU_CR + sta saved_cr + lda MMU_RCR + sta saved_rcr + lda #CR_RAM0_KERNAL + sta MMU_CR + + lda #PR_CLEAR + jsr pr_char + + ; ------------------------------------------------------- print tests + TEXT title, 18 + jsr pr_cr + TEXT text_ok, 7 + jsr pr_cr + + TEXT label_byte, 5 + lda #0 + jsr pr_byte + SPACE + lda #7 + jsr pr_byte + SPACE + lda #255 + jsr pr_byte + jsr pr_cr + + TEXT label_word, 5 + lda #0 + ldx #0 + jsr pr_word + SPACE + lda #<12345 + ldx #>12345 + jsr pr_word + SPACE + lda #$FF + ldx #$FF + jsr pr_word + jsr pr_cr + + TEXT label_int, 4 + lda #$FF ; -1 + ldx #$FF + jsr pr_int + SPACE + lda #$00 ; -32768 + ldx #$80 + jsr pr_int + SPACE + lda #$FF ; 32767 + ldx #$7F + jsr pr_int + jsr pr_cr + + TEXT label_bool, 5 + lda #$00 + jsr pr_bool + SPACE + lda #$01 + jsr pr_bool + jsr pr_cr + + TEXT label_char, 5 + lda #$41 ; 'A' + jsr pr_char + jsr pr_cr + + ; ------------------------------------------------------- fault tests + lda #FLT_NULL + ldx #$11 + ldy #$22 + jsr flt_report + lda #FLT_BOUNDS + ldx #$22 + ldy #$33 + jsr flt_report + lda #FLT_DIV0 + ldx #$33 + ldy #$44 + jsr flt_report + lda #FLT_STACK + ldx #$44 + ldy #$55 + jsr flt_report + lda #FLT_HEAP + ldx #$34 + ldy #$12 + jsr flt_report + + ; The print and fault calls must not disturb the generated-program map. + lda #$00 + sta MB_DATA + lda MMU_CR + cmp #CR_RAM0_KERNAL + bne @map_done + lda MMU_RCR + cmp saved_rcr + bne @map_done + lda #$01 + sta MB_DATA +@map_done: + lda saved_cr + sta MMU_CR + plp + rts + +.segment "RODATA" +title: .byte "NOVA128 PRINT TEST" +text_ok: .byte "TEXT OK" +label_byte: .byte "BYTE " +label_word: .byte "WORD " +label_int: .byte "INT " +label_bool: .byte "BOOL " +label_char: .byte "CHAR " + +.segment "BSS" +saved_cr: .res 1 +saved_rcr: .res 1 +test_end: