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 <noreply@codebuff.com>main
| @@ -9,8 +9,9 @@ version: 0.1-draft | |||||
| Status: approved version 1 baseline. The compiler memory map is frozen by | 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 | 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 | ## 1. System Boundary | ||||
| @@ -72,12 +73,14 @@ src/ | |||||
| interfaces.s conformance and interface slot assignment | interfaces.s conformance and interface slot assignment | ||||
| sizing.s dry-run code sizing and address assignment | sizing.s dry-run code sizing and address assignment | ||||
| codegen.s documented 8502 instruction emission | codegen.s documented 8502 instruction emission | ||||
| flow.s labels, branches, calls, and frame prologue/epilogue | |||||
| output.s PRG, ERR, and MAP writers | output.s PRG, ERR, and MAP writers | ||||
| diagnostics.s stable error catalog and recovery | diagnostics.s stable error catalog and recovery | ||||
| runtime/ | runtime/ | ||||
| startup.s generated-program entry and initialization | startup.s generated-program entry and initialization | ||||
| frames.s software argument/local-frame support | 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 | interfaces.s interface conversion and indirect call trampoline | ||||
| heap.s deterministic allocator and free list | heap.s deterministic allocator and free list | ||||
| arithmetic.s non-inline arithmetic helpers | arithmetic.s non-inline arithmetic helpers | ||||
| @@ -368,9 +371,21 @@ fields in declaration order with no hidden alignment padding: | |||||
| ... remaining fields | ... 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 | ### 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 | `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, | fault namespace by E04-S05. The scalar fixture executes the emitted shapes, | ||||
| checks short-circuit evaluation, and compares a reviewed golden byte stream | 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 | ## 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 | - Aliases can outlive a deletion; that is a program error. Debug poisoning may | ||||
| help diagnose it but is not a correctness mechanism. | 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 | ## 9. KERNAL and Disk I/O | ||||
| The compiler uses documented KERNAL entry points through `diskio.s` wrappers. | The compiler uses documented KERNAL entry points through `diskio.s` wrappers. | ||||
| @@ -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`. | | | 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`. | | | 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`. | | | 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 | ## Deferred | ||||
| @@ -6,8 +6,10 @@ x128 3.10 | |||||
| Authoritative source: `cfg/compiler.cfg`. This document explains it. | Authoritative source: `cfg/compiler.cfg`. This document explains it. | ||||
| E04-S01 defines the generated PRG/MAP container below; E04-S02 adds the | 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) | ## Generated Program Container (E04-S01) | ||||
| @@ -17,7 +19,7 @@ stories. | |||||
| | BASIC 7 stub | 12 bytes, `10 SYS 7181` | | | BASIC 7 stub | 12 bytes, `10 SYS 7181` | | ||||
| | Entry | `$1C0D` | | | Entry | `$1C0D` | | ||||
| | Sections | ascending addresses, zero-filled gaps | | | 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` | | | Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | | ||||
| The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses | 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 | 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. | (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 | ## Mappings | ||||
| | State | CR | RCR (low bits) | P0 | P1 | IRQs | | | State | CR | RCR (low bits) | P0 | P1 | IRQs | | ||||
| @@ -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. | |||||
| @@ -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`. | |||||
| @@ -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. | |||||
| @@ -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 <destroy> 01 <dir>` 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 <destroy_b> 01 <iface_dir>`. 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. | |||||
| @@ -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. | |||||
| @@ -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")) | |||||
| @@ -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) | |||||
| @@ -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) | |||||
| @@ -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) | |||||
| @@ -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) | |||||
| @@ -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) | |||||
| @@ -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 " |$_" } | |||||
| @@ -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" | |||||
| @@ -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" | |||||
| @@ -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" | |||||
| @@ -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" | |||||
| @@ -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" | |||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -2,7 +2,8 @@ | |||||
| ; | ; | ||||
| ; The container is deliberately small: a C128 BASIC 7 stub at $1C01, the | ; The container is deliberately small: a C128 BASIC 7 stub at $1C01, the | ||||
| ; machine entry at $1C0D, and contiguous sections loaded in ascending order. | ; 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 | .ifndef FORMATS_INC | ||||
| FORMATS_INC = 1 | FORMATS_INC = 1 | ||||
| @@ -14,6 +15,8 @@ PRG_STUB_SIZE = 12 | |||||
| OUT_MAX_SECTIONS = 8 | OUT_MAX_SECTIONS = 8 | ||||
| OUT_SECTION_NAME_MAX = 12 | OUT_SECTION_NAME_MAX = 12 | ||||
| OUT_RECORD_SIZE = 7 ; name ptr(2), name len(1), start(2), size(2) | 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_begin/out_finish/out_abort result codes (out_err). | ||||
| OUT_OK = 0 | 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_NAME = 4 ; base or section name too long | ||||
| OUT_ERR_DISK = 5 ; KERNAL or DOS failure | OUT_ERR_DISK = 5 ; KERNAL or DOS failure | ||||
| OUT_ERR_STATE = 6 ; byte emitted outside a section | OUT_ERR_STATE = 6 ; byte emitted outside a section | ||||
| OUT_ERR_CLASSES = 7 ; more than OUT_MAX_CLASSES | |||||
| .endif | .endif | ||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -23,15 +23,21 @@ | |||||
| ; C=0 and A = fixup index | ; C=0 and A = fixup index | ||||
| ; cg_patch_here A = fixup index; target is the current end of buffer | ; cg_patch_here A = fixup index; target is the current end of buffer | ||||
| ; cg_here returns A = current length | ; 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 | ; 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 | ; enough for the scalar shapes and gives E04-S04 the checked primitive it | ||||
| ; needs for a later long-branch expansion. | ; 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" | .setcpu "6502" | ||||
| .include "../common/codegen.inc" | .include "../common/codegen.inc" | ||||
| .export cg_init, cg_put, cg_here, cg_branch_fwd, cg_patch_here | .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_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_sta_abs, cg_stx_abs, cg_adc_abs, cg_sbc_abs, cg_cmp_abs | ||||
| .export cg_jsr_abs, cg_clc, cg_sec, cg_rts | .export cg_jsr_abs, cg_clc, cg_sec, cg_rts | ||||
| @@ -44,6 +50,7 @@ cg_init: | |||||
| sta cg_len | sta cg_len | ||||
| sta cg_err | sta cg_err | ||||
| sta cg_fix_count | sta cg_fix_count | ||||
| sta cg_count | |||||
| clc | clc | ||||
| rts | rts | ||||
| @@ -51,13 +58,24 @@ cg_here: | |||||
| lda cg_len | lda cg_len | ||||
| rts | 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. | ; A = byte to append. C=1 on overflow. | ||||
| cg_put: | cg_put: | ||||
| ldy cg_len | ldy cg_len | ||||
| cpy #CG_MAX | cpy #CG_MAX | ||||
| bcs @full | bcs @full | ||||
| pha | |||||
| lda cg_count | |||||
| bne @count | |||||
| pla | |||||
| sta cg_code,y | sta cg_code,y | ||||
| inc cg_len | |||||
| jmp @bump | |||||
| @count: pla | |||||
| @bump: inc cg_len | |||||
| clc | clc | ||||
| rts | rts | ||||
| @full: lda #CG_ERR_FULL | @full: lda #CG_ERR_FULL | ||||
| @@ -208,6 +226,7 @@ cg_patch_here: | |||||
| cg_code: .res 256 | cg_code: .res 256 | ||||
| cg_len: .res 1 | cg_len: .res 1 | ||||
| cg_err: .res 1 | cg_err: .res 1 | ||||
| cg_count: .res 1 | |||||
| cg_fix_count: .res 1 | cg_fix_count: .res 1 | ||||
| cg_fixpos: .res CG_FIX_MAX | cg_fixpos: .res CG_FIX_MAX | ||||
| cg_addr: .res 2 | cg_addr: .res 2 | ||||
| @@ -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 | |||||
| sta fl_tlo | |||||
| lda #>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 | |||||
| @@ -7,6 +7,7 @@ | |||||
| ; NOVA128 MAP 1 | ; NOVA128 MAP 1 | ||||
| ; ENTRY hhhh | ; ENTRY hhhh | ||||
| ; SECTION name start size | ; SECTION name start size | ||||
| ; CLASS name size destroy interface-count interface-directory | |||||
| ; | ; | ||||
| ; API (all calls in the bank-0 compiler state): | ; API (all calls in the bank-0 compiler state): | ||||
| ; out_begin A/X = source name (bank 0), Y = length. Sets out_load and | ; 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_bytes A/X = data, Y = count (0-255). | ||||
| ; out_end_section | ; out_end_section | ||||
| ; out_finish Writes the MAP, closes both files, and returns C=0 on success. | ; 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. | ; 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. | ; 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_begin, out_startup, out_section, out_end_section | ||||
| .export out_byte, out_bytes, out_finish, out_abort | .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 | .export out_load, out_entry, out_start, out_err | ||||
| .segment "ZEROPAGE" | .segment "ZEROPAGE" | ||||
| @@ -126,6 +132,24 @@ set_rec_ptr: | |||||
| bne @add | bne @add | ||||
| @done: rts | @done: rts | ||||
| ; out_rec_ptr = out_classes + out_index * OUT_CLASS_RECORD_SIZE. | |||||
| set_class_ptr: | |||||
| lda #<out_classes | |||||
| sta out_rec_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. | ; Build and send "S0:name" on the command channel. | ||||
| out_scratch: | out_scratch: | ||||
| sta out_scratch_ptr | sta out_scratch_ptr | ||||
| @@ -210,6 +234,7 @@ out_begin: | |||||
| lda #$00 | lda #$00 | ||||
| sta out_count | sta out_count | ||||
| sta out_class_count | |||||
| sta out_in_section | sta out_in_section | ||||
| sta out_prg_created | sta out_prg_created | ||||
| sta out_map_created | sta out_map_created | ||||
| @@ -527,6 +552,65 @@ out_bytes: | |||||
| @done: clc | @done: clc | ||||
| @out: rts | @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: | out_finish: | ||||
| jsr out_end_section | jsr out_end_section | ||||
| jsr out_close_prg | jsr out_close_prg | ||||
| @@ -727,6 +811,113 @@ out_write_map: | |||||
| bcs @out | bcs @out | ||||
| inc out_index | inc out_index | ||||
| jmp @section | 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 | |||||
| ldx #>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 | @done: clc | ||||
| @out: rts | @out: rts | ||||
| @@ -809,6 +1000,8 @@ map_entry: .byte "ENTRY " | |||||
| map_entry_len = * - map_entry | map_entry_len = * - map_entry | ||||
| section_prefix: .byte "SECTION " | section_prefix: .byte "SECTION " | ||||
| section_prefix_len = * - section_prefix | section_prefix_len = * - section_prefix | ||||
| class_prefix: .byte "CLASS " | |||||
| class_prefix_len = * - class_prefix | |||||
| hex_digits: .byte "0123456789ABCDEF" | hex_digits: .byte "0123456789ABCDEF" | ||||
| .segment "BSS" | .segment "BSS" | ||||
| @@ -835,6 +1028,7 @@ out_entry: .res 2 | |||||
| out_start: .res 2 | out_start: .res 2 | ||||
| out_current: .res 2 | out_current: .res 2 | ||||
| out_count: .res 1 | out_count: .res 1 | ||||
| out_class_count: .res 1 | |||||
| out_index: .res 1 | out_index: .res 1 | ||||
| out_in_section: .res 1 | out_in_section: .res 1 | ||||
| out_prg_created: .res 1 | out_prg_created: .res 1 | ||||
| @@ -844,6 +1038,11 @@ out_prg_is_open: .res 1 | |||||
| out_map_is_open: .res 1 | out_map_is_open: .res 1 | ||||
| out_err: .res 1 | out_err: .res 1 | ||||
| out_sections: .res OUT_MAX_SECTIONS * OUT_RECORD_SIZE | 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: .res 2 | ||||
| out_tmp_len: .res 1 | out_tmp_len: .res 1 | ||||
| out_scratch_ptr: .res 2 | out_scratch_ptr: .res 2 | ||||
| @@ -58,6 +58,7 @@ | |||||
| .import assign_slots, check_interfaces | .import assign_slots, check_interfaces | ||||
| .export parse_file, tb_get, tb_set, mem_count, par_count | .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 | .export ex_trace_on, ex_min_sp | ||||
| .exportzp ex_tp | .exportzp ex_tp | ||||
| .export type_kind, type_line_lo, type_line_hi | .export type_kind, type_line_lo, type_line_hi | ||||
| @@ -89,6 +90,10 @@ parse_file: | |||||
| stx parse_sp | stx parse_sp | ||||
| jsr tt_clear ; bank-1 BSS is not zeroed at load | jsr tt_clear ; bank-1 BSS is not zeroed at load | ||||
| lda #$00 | 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 mem_count | ||||
| sta par_count | sta par_count | ||||
| sta mem_full | sta mem_full | ||||
| @@ -1095,6 +1100,7 @@ parse_statement: | |||||
| stx stmt_sp | stx stmt_sp | ||||
| lda #$00 | lda #$00 | ||||
| sta ex_depth | sta ex_depth | ||||
| sta gen_in_print | |||||
| lda tok_col | lda tok_col | ||||
| sta stmt_col | sta stmt_col | ||||
| jsr trace_line | jsr trace_line | ||||
| @@ -1107,8 +1113,9 @@ parse_statement: | |||||
| bne :+ | bne :+ | ||||
| jmp @for | jmp @for | ||||
| : cmp #KW_ELSE | : cmp #KW_ELSE | ||||
| beq @else | |||||
| cmp #KW_RETURN | |||||
| bne :+ | |||||
| jmp @else | |||||
| : cmp #KW_RETURN | |||||
| beq @return | beq @return | ||||
| cmp #KW_PRINT | cmp #KW_PRINT | ||||
| beq @print | beq @print | ||||
| @@ -1119,8 +1126,27 @@ parse_statement: | |||||
| jsr value | jsr value | ||||
| jsr check_cond | jsr check_cond | ||||
| jmp end_stmt | 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 | 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 | jmp end_stmt | ||||
| @delete: | @delete: | ||||
| jsr value | jsr value | ||||
| @@ -1528,11 +1554,15 @@ parse_postfix: | |||||
| jsr trace_byte | jsr trace_byte | ||||
| lda tok_kind | lda tok_kind | ||||
| cmp #TOK_IDENT | 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 | ldx #lit_count - 1 | ||||
| @lit: cmp lit_kinds,x | @lit: cmp lit_kinds,x | ||||
| beq @literal | beq @literal | ||||
| @@ -1548,7 +1578,17 @@ parse_postfix: | |||||
| @early: lda #E_SYN_UNEXPECTED_EOL | @early: lda #E_SYN_UNEXPECTED_EOL | ||||
| jmp syn_error | jmp syn_error | ||||
| @literal: | @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 | sta sc_base | ||||
| lda tok_value ; a number's value (others ignore it) | lda tok_value ; a number's value (others ignore it) | ||||
| sta sc_val | sta sc_val | ||||
| @@ -1876,6 +1916,24 @@ trace_byte: | |||||
| inc ex_tp+1 | inc ex_tp+1 | ||||
| @off: rts | @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. | ; Remember the member name token; the next token picks field or method. | ||||
| pend_member: | pend_member: | ||||
| lda tok_col | lda tok_col | ||||
| @@ -2719,6 +2777,13 @@ op_code: .res 1 | |||||
| rt_col: .res 1 | rt_col: .res 1 | ||||
| val_col: .res 1 ; first column of the statement's value | val_col: .res 1 ; first column of the statement's value | ||||
| ty_col: .res 1 ; first column of a signature type | 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 | mem_seq: .res 1 ; pass 2: next member row of cur_owner | ||||
| impl_count: .res 1 | impl_count: .res 1 | ||||
| impl_full: .res 1 | impl_full: .res 1 | ||||
| @@ -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 <co_vec <> $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 | |||||
| @@ -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 | |||||
| ldx #>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 | |||||
| @@ -159,14 +159,20 @@ frame_leave: | |||||
| inc FRAME_SP+1 | inc FRAME_SP+1 | ||||
| @done: rts | @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: | frame_discard: | ||||
| sta frame_discard_n | |||||
| stx frame_discard_n+1 | |||||
| clc | clc | ||||
| adc FRAME_SP | |||||
| lda FRAME_SP | |||||
| adc frame_discard_n | |||||
| sta FRAME_SP | sta FRAME_SP | ||||
| txa | |||||
| adc FRAME_SP+1 | |||||
| lda FRAME_SP+1 | |||||
| adc frame_discard_n+1 | |||||
| sta FRAME_SP+1 | sta FRAME_SP+1 | ||||
| lda frame_discard_n | |||||
| ldx frame_discard_n+1 | |||||
| rts | rts | ||||
| .segment "BSS" | .segment "BSS" | ||||
| @@ -177,4 +183,5 @@ frame_word: .res 2 | |||||
| frame_locals: .res 2 | frame_locals: .res 2 | ||||
| frame_saved_sp: .res 2 | frame_saved_sp: .res 2 | ||||
| frame_saved_fp: .res 2 | frame_saved_fp: .res 2 | ||||
| frame_discard_n: .res 2 | |||||
| frame_error: .res 1 | frame_error: .res 1 | ||||
| @@ -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 | |||||
| @@ -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 | |||||
| @@ -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 | |||||
| ldx #>pr_true_text | |||||
| ldy #$04 | |||||
| jmp pr_text | |||||
| @false: lda #<pr_false_text | |||||
| ldx #>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 | |||||
| @@ -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 | |||||
| lda #>frame_enter | |||||
| sta fl_enter+1 | |||||
| lda #<frame_leave | |||||
| sta fl_leave | |||||
| lda #>frame_leave | |||||
| sta fl_leave+1 | |||||
| lda #<frame_discard | |||||
| sta fl_discard | |||||
| 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 ; sum = add(sum) | |||||
| ldx #>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 | |||||
| sta @target+1 | |||||
| lda #>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 | |||||
| @@ -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 | |||||
| lda #>PRG_LOAD_ADDR | |||||
| sta out_load+1 | |||||
| lda #<PRG_ENTRY_ADDR | |||||
| sta out_entry | |||||
| lda #>PRG_ENTRY_ADDR | |||||
| sta out_entry+1 | |||||
| lda #<name_src | |||||
| ldx #>name_src | |||||
| ldy #name_src_len | |||||
| jsr src_load | |||||
| cmp #$00 | |||||
| bne @fail_load | |||||
| jsr names_init | |||||
| lda #<name_src | |||||
| ldx #>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 | |||||
| ldx #>name_hello | |||||
| ldy #name_hello_len | |||||
| jsr out_begin | |||||
| ON_CARRY @abort | |||||
| lda #<PRG_LOAD_ADDR ; BASIC stub section | |||||
| sta out_start | |||||
| lda #>PRG_LOAD_ADDR | |||||
| sta out_start+1 | |||||
| lda #<sec_stub | |||||
| ldx #>sec_stub | |||||
| ldy #4 | |||||
| jsr out_section | |||||
| ON_CARRY @abort | |||||
| lda #<gen_stub | |||||
| ldx #>gen_stub | |||||
| ldy #PRG_STUB_SIZE | |||||
| jsr out_bytes | |||||
| ON_CARRY @abort | |||||
| jsr out_end_section | |||||
| ON_CARRY @abort | |||||
| lda #<PRG_ENTRY_ADDR ; generated body | |||||
| sta out_start | |||||
| lda #>PRG_ENTRY_ADDR | |||||
| sta out_start+1 | |||||
| lda #<sec_code | |||||
| ldx #>sec_code | |||||
| ldy #4 | |||||
| jsr out_section | |||||
| ON_CARRY @abort | |||||
| lda #<prologue | |||||
| ldx #>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 | |||||
| ldx #>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 | |||||
| @@ -0,0 +1,4 @@ | |||||
| // E04-S05 end-to-end source: one string print. | |||||
| program hello | |||||
| print "hello from nova128" | |||||
| end program | |||||
| @@ -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 | |||||
| ldx #>obj_empty | |||||
| jsr obj_class | |||||
| bcs @p0_fail | |||||
| cmp #<desc_empty | |||||
| bne @p0_fail | |||||
| cpx #>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 | |||||
| ldx #>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 | |||||
| ldx #>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 | |||||
| ldx #>obj_box | |||||
| jsr obj_class | |||||
| bcs @p3_fail | |||||
| cmp #<desc_box | |||||
| bne @p3_fail | |||||
| cpx #>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 | |||||
| ldx #>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 | |||||
| ldx #>obj_box | |||||
| jsr obj_destroy | |||||
| bcs @p5_fail | |||||
| cmp #<destroy_b | |||||
| bne @p5_fail | |||||
| cpx #>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 | |||||
| ldx #>obj_box | |||||
| jsr obj_iface_dir | |||||
| bcs @p6_fail | |||||
| cmp #<iface_dir | |||||
| bne @p6_fail | |||||
| cpx #>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 | |||||
| lda #>PRG_LOAD_ADDR | |||||
| sta out_load+1 | |||||
| lda #<PRG_ENTRY_ADDR | |||||
| sta out_entry | |||||
| lda #>PRG_ENTRY_ADDR | |||||
| sta out_entry+1 | |||||
| lda #<name_source | |||||
| ldx #>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 | |||||
| ldx #>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 | |||||
| lda #>destroy_b | |||||
| sta out_class_destroy+1 | |||||
| lda #1 | |||||
| sta out_class_ifaces | |||||
| lda #<iface_dir | |||||
| sta out_class_ifdir | |||||
| lda #>iface_dir | |||||
| sta out_class_ifdir+1 | |||||
| lda #<name_box | |||||
| ldx #>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 | |||||
| @@ -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 | |||||
| ldx #>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: | |||||
| @@ -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 | |||||
| ldx #>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 | |||||
| lda #>init_res | |||||
| sta obj_init_ptr+1 | |||||
| lda #<desc_res | |||||
| ldx #>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 | |||||
| cpx #>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 | |||||
| lda #>init_res | |||||
| sta obj_init_ptr+1 | |||||
| lda #<desc_huge | |||||
| ldx #>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 | |||||
| ldx #>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: | |||||
| @@ -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 | |||||
| ldx #>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: | |||||
Powered by TurnKey Linux.