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 | |||
| E01-S05 (section 4.4, `docs/memory-map.md`). E04-S01 defines the generated | |||
| PRG/MAP container and low-memory startup; the higher generated-program map | |||
| (banking, heap, software stack) remains open until E04-S02. | |||
| PRG/MAP container and low-memory startup; E04-S02 adds the frame runtime; E05-S01 | |||
| supplies an explicit-region heap; E05-S02 freezes class descriptors and MAP | |||
| class records. Generated-program heap placement and banking remain open. | |||
| ## 1. System Boundary | |||
| @@ -72,12 +73,14 @@ src/ | |||
| interfaces.s conformance and interface slot assignment | |||
| sizing.s dry-run code sizing and address assignment | |||
| codegen.s documented 8502 instruction emission | |||
| flow.s labels, branches, calls, and frame prologue/epilogue | |||
| output.s PRG, ERR, and MAP writers | |||
| diagnostics.s stable error catalog and recovery | |||
| runtime/ | |||
| startup.s generated-program entry and initialization | |||
| frames.s software argument/local-frame support | |||
| objects.s object construction and destruction | |||
| objects.s object and descriptor accessors | |||
| construct.s object construction, deletion, and indirect call | |||
| interfaces.s interface conversion and indirect call trampoline | |||
| heap.s deterministic allocator and free list | |||
| arithmetic.s non-inline arithmetic helpers | |||
| @@ -368,9 +371,21 @@ fields in declaration order with no hidden alignment padding: | |||
| ... remaining fields | |||
| ``` | |||
| The class descriptor contains the instance size, optional destructor address, | |||
| and a directory of implemented interfaces. Its final packed format is frozen | |||
| by E05 and E06 stories. | |||
| E05-S02 freezes the class descriptor as an 8-byte packed record: | |||
| ```text | |||
| +0..1 instance size (includes the 2-byte object header) | |||
| +2..3 destructor address; 0 = none | |||
| +4 implemented-interface count | |||
| +5..6 interface-directory pointer; 0 when the count is 0 | |||
| +7 reserved flags; must be 0 | |||
| ``` | |||
| The interface directory is a count-long array of 4-byte entries: a pointer to | |||
| the interface descriptor and a pointer to its method table in slot order. | |||
| There is no parent-class pointer or inheritance slot. `src/runtime/objects.s` | |||
| provides read-only `obj_class`, `obj_size`, `obj_destroy`, and | |||
| `obj_iface_dir` accessors. | |||
| ### 7.5 Interface representation and dispatch | |||
| @@ -421,8 +436,84 @@ remainder in `ar_rem`. Division by zero sets provisional `ar_fault` to | |||
| `AR_FAULT_DIV0`, returns A=X=0 with C=1, and is mapped to the final runtime | |||
| fault namespace by E04-S05. The scalar fixture executes the emitted shapes, | |||
| checks short-circuit evaluation, and compares a reviewed golden byte stream | |||
| and da65 disassembly. Parser integration into statements and frames is | |||
| E04-S04's responsibility. | |||
| and da65 disassembly. Parser integration into statements and frames remains | |||
| the next backend step. | |||
| ### 7.7 Control flow, calls, and returns (E04-S04) | |||
| `src/compiler/flow.s` supplies the statement-level control transfer above the | |||
| scalar emitter. It keeps 16 labels and 32 fixups per routine. A label records | |||
| the byte offset where it was defined; `fl_jmp` emits an absolute `JMP` and is | |||
| also the back edge of `while` and `for` loops. `fl_branch` takes a branch | |||
| opcode and a label and always occupies five bytes: | |||
| ```text | |||
| short Bxx distance ; target within +0..+127 | |||
| NOP NOP NOP ; unused JMP slot | |||
| long Bxx'inv +3 ; inverse of the requested condition | |||
| JMP target | |||
| ``` | |||
| The form is chosen when fixups are patched. A forward branch whose target is | |||
| behind it is `FL_ERR_RANGE`; loops use `JMP`. Patching the long form inverts | |||
| the opcode, so a true condition falls through to the `JMP` and a false one | |||
| skips it. Because every conditional branch is five bytes, `cg_put`'s counting | |||
| mode produces exactly the same label offsets and routine length as emission | |||
| without writing memory, which is the sizing half of the two-pass contract. | |||
| Calls follow section 7.3: `fl_call` emits `JSR` and then `frame_discard` for | |||
| the argument bytes. `fl_call_byte` and `fl_call_word` save the result on the | |||
| hardware stack across the discard, and `frame_discard` preserves A/X. A | |||
| routine returns through `fl_ret` (no result), `fl_ret_byte` (A), or | |||
| `fl_ret_word` (A/X); the result variants save and restore the result around | |||
| `frame_leave`. `fl_prologue` emits `frame_enter` for the local byte count and | |||
| branches to the caller's error label on carry. | |||
| The x128 fixture builds a nested routine (prologue, `while`, `if`/`else`, | |||
| `for`, call with argument discard, value return), checks sizing against a | |||
| counting pass, verifies both branch encodings in memory, and executes it. | |||
| Parser-to-emitter wiring, method address assignment, and compiler pass 3 are | |||
| not part of this layer. | |||
| ### 7.8 Print and runtime faults (E04-S05) | |||
| `src/runtime/print.s` writes PETSCII through `CHROUT` under the D129 | |||
| generated-program mapping. It prints a byte, an unsigned word, a signed | |||
| integer, a boolean (`TRUE`/`FALSE`), a length-address text block, and a | |||
| carriage return. Decimal output has no leading zeros; only `pr_cr` ends a | |||
| line. The routines append nothing on their own and do not change CR, RCR, or | |||
| the P0/P1 pointers, so a runtime message cannot relocate the program. | |||
| `pr_text` uses a zero-page pointer, so it depends on the system zero page | |||
| being mapped as in D129. | |||
| `src/runtime/faults.s` maps the provisional arithmetic fault onto a stable | |||
| R-code namespace: R001 null, R002 bounds, R003 divide by zero, R004 software | |||
| stack overflow, and R005 heap exhaustion. `flt_report` writes the code and | |||
| fault address into the mailbox, sets `MB_ST_FAIL`, and prints | |||
| `RUNTIME FAULT R00n`. The code and address are written before the status so a | |||
| monitor never observes a failure with stale fields. The routine returns; | |||
| unwinding to the runtime entry remains with the frame/exception work. | |||
| ### 7.9 First source-to-code proof (E04-S05) | |||
| The `hello.n8` compile fixture closes one source-to-code path without claiming | |||
| the general backend. Parser pass 2 records a print that consists of one string | |||
| literal in the bounded `gen_text` buffer, sets `gen_print_count`, and sets | |||
| `gen_bad` for any other print shape. The fixture then emits the D128 container | |||
| by hand: a 12-byte BASIC stub section at `$1C01` and a code section at | |||
| `$1C0D`. The fixture must initialize `out_load` and `out_entry` itself; | |||
| `output.s` reads them but does not assign them. | |||
| The emitted body saves the entry CR, selects `$0E`, writes each captured | |||
| PETSCII byte with `LDA #byte` / `JSR $FFD2`, restores CR, and returns to BASIC. | |||
| It does not call `startup.s` or the scalar/control-flow emitters. A successful | |||
| compile produces a 118-byte `HELLO.PRG`; a second x128 session autostarts the | |||
| extracted PRG and the host checks for `HELLO FROM NOVA128` on the screen. | |||
| This is deliberately narrower than compiler pass 3: expressions, calls, | |||
| multiple statements per program, and address assignment for generated code | |||
| remain open. Pass-2 code growth also made some parser branches exceed ca65's | |||
| +/-128 range, so the literal path uses a near branch and `JMP` trampoline. | |||
| ## 8. Heap and Ownership | |||
| @@ -437,6 +528,93 @@ E04-S04's responsibility. | |||
| - Aliases can outlive a deletion; that is a program error. Debug poisoning may | |||
| help diagnose it but is not a correctness mechanism. | |||
| ### 8.1 Free-list heap (E05-S01) | |||
| `src/runtime/heap.s` supplies the allocator; `src/common/heap.inc` fixes the | |||
| block/API contract. The caller supplies the region, so this does not freeze the | |||
| still-open generated-program map. In the E05-S01 fixture the region is bank-0 | |||
| `$4000-$4FFF` under CR `$0E`. | |||
| Each block has a four-byte header: | |||
| ```text | |||
| +0..1 total block size; bit 15 set while allocated | |||
| +2..3 next free block while free; $A55A while allocated | |||
| +4.. payload | |||
| ``` | |||
| Blocks are two-byte aligned and the minimum block is six bytes. Free blocks | |||
| are kept in ascending address order. `heap_alloc` takes a payload byte count in | |||
| A/X, walks first-fit, splits a remainder of at least six bytes, zeroes the | |||
| payload, and returns its pointer in A/X. `heap_free` takes a payload pointer | |||
| and inserts/coalesces with adjacent free blocks. A zero pointer is a no-op. | |||
| Range, size, used-magic, and double-free checks are always on. The allocator | |||
| returns `HEAP_ERR_*` rather than printing; generated `new` maps allocation | |||
| failure to R005 when E05-S03 wires construction. The stress fixture frees in | |||
| different orders and requires the initial free capacity to return. | |||
| Heap placement for generated programs and banked heaps remain open. The | |||
| fixture keeps code below `$4000` and uses only its explicit 4K region. | |||
| ### 8.2 Class descriptors and objects (E05-S02) | |||
| `src/common/class.inc` fixes the object/descriptor ABI and `objects.s` reads it | |||
| without allocating or changing object state. The class fixture covers a class | |||
| with no fields and no destructor, and a class with an optional destructor and | |||
| one interface-directory entry. | |||
| `output.s` accepts class records through `out_class` and appends deterministic | |||
| MAP lines after the section records: | |||
| ```text | |||
| CLASS name size destroy interface-count interface-directory | |||
| ``` | |||
| All numeric fields are four hex digits except the interface count, which is | |||
| two. Empty classes and no destructor are written as `0000`; records keep the | |||
| caller's order. The class test reads the emitted MAP back and compares it with | |||
| the descriptor's actual destructor and directory addresses. | |||
| ### 8.3 Construction and deletion (E05-S03) | |||
| `src/runtime/construct.s` implements `new` and `delete` on top of the heap and | |||
| descriptor layers, so both can be tested before parser-to-codegen exists. | |||
| `obj_new` takes A/X = class descriptor plus `obj_init_ptr` chosen by the caller | |||
| (0 = no constructor). It reads the descriptor's instance size and refuses zero | |||
| with C=1/`OBJ_ERR_BAD`, allocates exactly that many bytes, and maps | |||
| `HEAP_ERR_OOM` to C=1/`OBJ_ERR_HEAP` with A/X=0. Only after a successful | |||
| allocation does it write the descriptor pointer at object+0 and call the init | |||
| entry once with A/X = self; the payload is already zeroed by the heap, so | |||
| fields start at zero. Constructor arguments are not copied: they stay on the | |||
| caller's software frame stack, init enters its normal frame and reads them from | |||
| `FP+2` right-to-left, and the caller discards them with `frame_discard` after | |||
| `obj_new` returns. A failed allocation never calls init. | |||
| `obj_delete` takes A/X = object. Null returns C=0 without touching the heap or | |||
| the destructor. Otherwise it reads the descriptor's optional destructor, calls | |||
| it once with A/X = self and no arguments, then frees the complete block with | |||
| `heap_free`, so an instance returns to the coalescing free list in one piece. | |||
| Both calls use `obj_call`, the synthetic-return trampoline. The entry address | |||
| travels in zero-page `co_vec` rather than A/X, because A/X already carry the | |||
| callee's self pointer; `obj_call` saves them, pushes a two-byte return address, | |||
| restores them, and ends with `JMP (co_vec)`. The callee's `RTS` lands on the | |||
| trampoline's own `RTS`, which returns to the original `JSR`. `co_vec` must not | |||
| straddle a page (link-time assertion) because of the 6502 indirect-JMP bug, and | |||
| every such call costs two extra hardware-stack bytes. | |||
| `construct.s` reports through `obj_error` and the carry flag rather than | |||
| printing, so generated code still owns the R005 fault report for `new`. | |||
| The fixture constructs a five-byte class whose `init(value as word, tag as | |||
| byte)` stores both fields beside a destructor, then checks the stored header, | |||
| zeroed fields, the constructor's arguments and self, that `delete null` calls | |||
| nothing, that deletion calls the destructor once with the fields intact and | |||
| restores the full free capacity, that a `$FFFF` instance fails without calling | |||
| init, and that a class without a destructor still frees completely. | |||
| ## 9. KERNAL and Disk I/O | |||
| The compiler uses documented KERNAL entry points through `diskio.s` wrappers. | |||
| @@ -57,6 +57,11 @@ documented default. `Proposed` means a later change still needs approval. | |||
| | D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | | |||
| | D129 | Generated code executes with CR `$0E` and IRQs masked, then restores the entry CR. The software stack is bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP`/`FP` use `$FB-$FE`. A frame stores the previous FP at `FP`, arguments from `FP+2`, and locals below `FP`; callers push arguments right-to-left and discard them after return. Results are A or A/X. `frame_enter` reserves the old FP and locals transactionally, refuses when the 6502 stack pointer is below `$40`, and returns C=1/`FRAME_ERR_STACK` with `SP`/`FP` restored on overflow. | Makes recursion and nested calls testable without consuming BASIC's zero page or the compiler's private stack. The $40 hardware-stack guard prevents a JAM before the software bound, and the transactional failure lets callers unwind cleanly. The 2 KB software stack, guard threshold, and mapping install are provisional until codegen and later runtime stories. Implemented by `frames.s` and `frames.inc`. | | |||
| | D130 | Scalar codegen emits documented 6502 instructions into a bounded 256-byte buffer. Values are A (one byte) or A/X low/high (two bytes); assignments use absolute stores, comparisons produce a 0/1 byte, and forward branches use checked fixups. Multiplication and signed/unsigned division call runtime helpers with operands in `ar_a`/`ar_b`; results are A/X, division remainder is `ar_rem`, and division by zero sets provisional `ar_fault=AR_FAULT_DIV0`, A=X=0, C=1. E04-S04 owns parser-to-emitter integration, control flow, and long-branch expansion. | Gives the backend a small, directly testable instruction layer and a deterministic golden stream before the control-flow shell exists. The 256-byte buffer and forward-only branches are deliberate limits; E04-S05 maps the provisional arithmetic fault to the final R-code namespace. Implemented by `codegen.s`, `codegen.inc`, `arithmetic.s`, and `arithmetic.inc`. | | |||
| | D131 | Control-flow emission (E04-S04) uses `src/compiler/flow.s`: label ids 0-15, absolute `JMP` for back edges, and a fixed five-byte conditional branch. A target within +0..+127 is patched as the direct branch plus three `NOP`s; a farther target is patched as the inverted opcode, `+3`, and `JMP target`. `cg_put` has a counting mode so a sizing pass and an emitting pass make identical label offsets and lengths. Calls emit `JSR` then `frame_discard`; `fl_call_byte`/`fl_call_word` save A (or A/X) across the discard, and `frame_discard` itself now preserves A/X. `fl_ret_byte`/`fl_ret_word` preserve results across `frame_leave`. | Gives the backend a bounded, directly testable control-flow layer before parser and pass-2 address assignment exist. The fixed five-byte branch avoids size drift between passes by construction. Parser/statement integration, `sizing.s`, and compiler pass 3 remain open. Implemented by `flow.s`, `flow.inc`, and `codegen.s`. | | |||
| | D132 | Generated-program text output calls KERNAL `CHROUT` directly under the D129 mapping (CR `$0E`, system zero page, code below `$4000`). `pr_byte`/`pr_word`/`pr_int` print minimal decimal (no leading zeros, `-` for negatives), `pr_bool` prints `TRUE`/`FALSE`, and only `pr_cr` ends a line. Runtime faults use stable R001 null, R002 bounds, R003 divide, R004 stack, and R005 heap; `flt_report` writes code and address, then `MB_ST_FAIL`, prints `RUNTIME FAULT R00n`, and returns. The print and fault routines never change CR, RCR, or P0/P1. | Implements the output and fault half of E04-S05 without a second MMU wrapper: generated code already runs in the D129 mapping, so direct KERNAL calls are safe and the fixture proves CR/RCR survive every message. The `hello.n8` path is now proven by a fixture-only pass-2 capture (architecture 7.9); fault unwinding, general parser-to-codegen integration, and compiler pass 3 remain open. Output is verified in x128's VIC-II display only. Implemented by `print.s`, `faults.s`, and the two `.inc` files. | | |||
| | D133 | The E05-S01 heap uses four-byte headers: a two-byte total size with bit 15 set while allocated, and a two-byte next pointer while free or `$A55A` while allocated. Payload follows at header+4, blocks are two-byte aligned, and the minimum block is six bytes. `heap_init` takes A/X = page-aligned base and Y = pages 1..127; `heap_alloc` takes payload bytes, zeroes the payload, returns it or `HEAP_ERR_OOM`, and splits first-fit free blocks; `heap_free` validates range/size/magic and coalesces in address order. Validation is always on. The E05-S01 fixture uses bank-0 `$4000-$4FFF` under CR `$0E`; generated-program placement and banked heaps remain open. | Makes allocation deterministic and testable before class layout chooses a production heap base. The allocator does not print or report faults itself; E05-S03 maps `HEAP_ERR_OOM` to R005 when `new` is wired. The 15-bit size limit and page-granular region avoid a separate size argument while keeping the block header small. Implemented by `heap.s`, `heap.inc`, and the heap fixture. | | |||
| | D135 | E05-S03 construction and deletion live in `src/runtime/construct.s` above the heap and descriptor layers. `obj_new` takes A/X = descriptor, reads the instance size (zero is `OBJ_ERR_BAD`), allocates it, writes the descriptor pointer at object+0, then calls the caller-selected `obj_init_ptr` entry once with A/X = self and the constructor arguments still on the software frame stack at `FP+2`; the caller discards them with `frame_discard`. Allocation failure returns C=1 with `OBJ_ERR_HEAP` and never calls `init`. `obj_delete` takes A/X = object: null is a successful no-op, otherwise the descriptor's optional destructor runs once with A/X = self and the complete block goes back through `heap_free`. Both calls use `obj_call`, which takes its entry from zero-page `co_vec`, preserves A/X for `self`, pushes one synthetic return address, and ends with `JMP (co_vec)`; `co_vec` must not straddle a page. | Gives generated `new`/`delete` a testable runtime contract without freezing heap placement or the E06 dispatch. Carrying the entry in `co_vec` instead of A/X is what lets the callee still receive `self` in A/X, and the same shape serves method-table dispatch later. `construct.s` reports `OBJ_ERR_HEAP` rather than printing; codegen still maps that to R005. Implemented by `construct.s`, the `OBJ_*` codes in `class.inc`, and the object fixture. | | |||
| | D134 | E05-S02 class descriptors are 8-byte packed records: 2-byte instance size (including the object header), 2-byte destructor address (0 = none), 1-byte interface count, 2-byte interface-directory pointer (0 when empty), and a reserved flags byte that must be 0. There is no parent-class slot. Objects begin with a 2-byte descriptor pointer and fields follow at +2. Interface-directory entries are 4 bytes: interface-descriptor pointer and method-table pointer. `objects.s` provides read-only accessors; `output.s` writes `CLASS name size destroy count ifdir` records after sections. | Makes object identity, size/deletion, and the future interface path deterministic before construction/dispatch. Flags are reserved for non-inheritance metadata; interface descriptor contents and generated `new` remain E05-S03/E06. Implemented by `class.inc`, `objects.s`, and the class fixture. | | |||
| ## Deferred | |||
| @@ -6,8 +6,10 @@ x128 3.10 | |||
| Authoritative source: `cfg/compiler.cfg`. This document explains it. | |||
| E04-S01 defines the generated PRG/MAP container below; E04-S02 adds the | |||
| provisional frame runtime. Heap placement and any future banking remain later | |||
| stories. | |||
| provisional frame runtime. E05-S01 supplies an explicit-region heap; E05-S02 | |||
| freezes class descriptors and MAP class records; E05-S03 adds construction and | |||
| deletion above both. Production heap placement and any future banking remain | |||
| later stories. | |||
| ## Generated Program Container (E04-S01) | |||
| @@ -17,7 +19,7 @@ stories. | |||
| | BASIC 7 stub | 12 bytes, `10 SYS 7181` | | |||
| | Entry | `$1C0D` | | |||
| | Sections | ascending addresses, zero-filled gaps | | |||
| | MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size` | | |||
| | MAP | `NOVA128 MAP 1`, `ENTRY hhhh`, `SECTION name start size`, `CLASS name size destroy count ifdir` | | |||
| | Startup | clears mailbox, producer `$03`, status PASS, then `RTS` | | |||
| The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses | |||
| @@ -40,6 +42,58 @@ the entry CR before printing and returning to BASIC. Code generation will | |||
| install this mapping and call `frame_init` when it starts emitting user code | |||
| (E04-S03). The 2 KB stack and `$40` hardware-stack guard are provisional. | |||
| ## Heap Fixture (E05-S01) | |||
| | Element | Value | | |||
| |---|---| | |||
| | Region supplied by caller | page-aligned base and 1..127 pages | | |||
| | Block header | 2-byte total size (bit 15 allocated), 2-byte next/magic | | |||
| | Payload | header+4; two-byte aligned; minimum block 6 bytes | | |||
| | Fixture region | bank 0 `$4000-$4FFF` under CR `$0E` | | |||
| | Fixture code | below `$4000` | | |||
| | Exhaustion | `HEAP_ERR_OOM`, mapped to R005 by the fixture | | |||
| `heap_init` accepts the region explicitly, so this does not freeze the | |||
| generated-program heap location. The block/API contract is in | |||
| `src/common/heap.inc`; D133 records the provisional decision. The allocator | |||
| does not print faults itself; E05-S03 will map `HEAP_ERR_OOM` when `new` is | |||
| wired. | |||
| ## Class Descriptors (E05-S02) | |||
| | Element | Value | | |||
| |---|---| | |||
| | Object header | descriptor pointer at +0; fields from +2 | | |||
| | Descriptor | 8 bytes: size, destructor, interface count, directory, flags | | |||
| | Instance size | includes the two-byte object header | | |||
| | Destructor | `$0000` means none | | |||
| | Interface directory | 4-byte entries: interface descriptor, method table | | |||
| | Flags | reserved, must be 0; no parent-class slot | | |||
| | MAP | `CLASS name size destroy count ifdir` after sections | | |||
| The ABI is fixed in `src/common/class.inc`; D134 records the provisional | |||
| decision. `src/runtime/objects.s` reads descriptors and objects without | |||
| allocating. `output.s` writes class MAP records in `out_class` call order; the | |||
| E05-S02 fixture checks a no-destructor/no-interface class and a destructor plus | |||
| one interface entry. Interface dispatch remains E06 work. | |||
| ## Construction and Deletion (E05-S03) | |||
| | Element | Value | | |||
| |---|---| | |||
| | New fixed addresses | none; `obj_new` uses the caller-initialized heap | | |||
| | Fixture region | bank 0 `$4000-$4FFF` under CR `$0E`, code below `$4000` | | |||
| | Module zero page | `co_vec`, `co_desc`, `co_ptr`, `co_self` (link-allocated) | | |||
| | Module BSS | `obj_init_ptr`, `obj_error`, saved object/size/destructor | | |||
| | `obj_call` stack cost | one synthetic return address, two hardware-stack bytes | | |||
| | Constructor arguments | stay on the software frame stack at `FP+2`; caller discards | | |||
| `src/runtime/construct.s` adds no fixed addresses, so the generated-program | |||
| heap location stays open. `co_vec` carries the indirect target and the module | |||
| asserts it does not end at `$xxFF`, because `JMP ($xxFF)` reads its high byte | |||
| from the wrong page. Constructor arguments are not copied again: `init` enters | |||
| its normal frame and reads them where the caller pushed them. | |||
| ## Mappings | |||
| | State | CR | RCR (low bits) | P0 | P1 | IRQs | | |||
| @@ -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 | |||
| ; machine entry at $1C0D, and contiguous sections loaded in ascending order. | |||
| ; The higher generated-program map (banking, heap, software stack) is E04-S02. | |||
| ; Frames (E04-S02), an explicit-region heap (E05-S01), and class descriptors | |||
| ; (E05-S02) extend the container without changing its load address. | |||
| .ifndef FORMATS_INC | |||
| FORMATS_INC = 1 | |||
| @@ -14,6 +15,8 @@ PRG_STUB_SIZE = 12 | |||
| OUT_MAX_SECTIONS = 8 | |||
| OUT_SECTION_NAME_MAX = 12 | |||
| OUT_RECORD_SIZE = 7 ; name ptr(2), name len(1), start(2), size(2) | |||
| OUT_MAX_CLASSES = 8 | |||
| OUT_CLASS_RECORD_SIZE = 10 ; name ptr(2), len(1), size(2), destroy(2), count(1), dir(2) | |||
| ; out_begin/out_finish/out_abort result codes (out_err). | |||
| OUT_OK = 0 | |||
| @@ -23,5 +26,6 @@ OUT_ERR_SECTIONS = 3 ; more than OUT_MAX_SECTIONS | |||
| OUT_ERR_NAME = 4 ; base or section name too long | |||
| OUT_ERR_DISK = 5 ; KERNAL or DOS failure | |||
| OUT_ERR_STATE = 6 ; byte emitted outside a section | |||
| OUT_ERR_CLASSES = 7 ; more than OUT_MAX_CLASSES | |||
| .endif | |||
| @@ -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 | |||
| ; cg_patch_here A = fixup index; target is the current end of buffer | |||
| ; cg_here returns A = current length | |||
| ; cg_set_count A != 0: count bytes only (no stores); 0: emit bytes | |||
| ; | |||
| ; Only forward relative branches within +0..+127 bytes are accepted. This is | |||
| ; enough for the scalar shapes and gives E04-S04 the checked primitive it | |||
| ; needs for a later long-branch expansion. | |||
| ; | |||
| ; Count mode (E04-S04) lets a sizing pass measure a routine without writing | |||
| ; the code buffer. cg_len advances exactly as in emit mode, so label offsets | |||
| ; and lengths match by construction. | |||
| .setcpu "6502" | |||
| .include "../common/codegen.inc" | |||
| .export cg_init, cg_put, cg_here, cg_branch_fwd, cg_patch_here | |||
| .export cg_set_count, cg_count | |||
| .export cg_lda_imm, cg_ldx_imm, cg_ora_imm, cg_lda_abs, cg_ldx_abs | |||
| .export cg_sta_abs, cg_stx_abs, cg_adc_abs, cg_sbc_abs, cg_cmp_abs | |||
| .export cg_jsr_abs, cg_clc, cg_sec, cg_rts | |||
| @@ -44,6 +50,7 @@ cg_init: | |||
| sta cg_len | |||
| sta cg_err | |||
| sta cg_fix_count | |||
| sta cg_count | |||
| clc | |||
| rts | |||
| @@ -51,13 +58,24 @@ cg_here: | |||
| lda cg_len | |||
| rts | |||
| ; A != 0: count bytes only. A = 0: emit them (the default). | |||
| cg_set_count: | |||
| sta cg_count | |||
| rts | |||
| ; A = byte to append. C=1 on overflow. | |||
| cg_put: | |||
| ldy cg_len | |||
| cpy #CG_MAX | |||
| bcs @full | |||
| pha | |||
| lda cg_count | |||
| bne @count | |||
| pla | |||
| sta cg_code,y | |||
| inc cg_len | |||
| jmp @bump | |||
| @count: pla | |||
| @bump: inc cg_len | |||
| clc | |||
| rts | |||
| @full: lda #CG_ERR_FULL | |||
| @@ -208,6 +226,7 @@ cg_patch_here: | |||
| cg_code: .res 256 | |||
| cg_len: .res 1 | |||
| cg_err: .res 1 | |||
| cg_count: .res 1 | |||
| cg_fix_count: .res 1 | |||
| cg_fixpos: .res CG_FIX_MAX | |||
| cg_addr: .res 2 | |||
| @@ -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 | |||
| ; ENTRY hhhh | |||
| ; SECTION name start size | |||
| ; CLASS name size destroy interface-count interface-directory | |||
| ; | |||
| ; API (all calls in the bank-0 compiler state): | |||
| ; out_begin A/X = source name (bank 0), Y = length. Sets out_load and | |||
| @@ -19,6 +20,9 @@ | |||
| ; out_bytes A/X = data, Y = count (0-255). | |||
| ; out_end_section | |||
| ; out_finish Writes the MAP, closes both files, and returns C=0 on success. | |||
| ; out_class A/X = class name, Y = length. Adds a deterministic CLASS | |||
| ; record from out_class_size/destroy/ifaces/ifdir. Class | |||
| ; records follow the SECTION lines in call order. | |||
| ; out_abort Closes and scratches only files created by this run. | |||
| ; | |||
| ; Every failure sets out_err and returns C=1; the caller must call out_abort. | |||
| @@ -35,6 +39,8 @@ | |||
| .export out_begin, out_startup, out_section, out_end_section | |||
| .export out_byte, out_bytes, out_finish, out_abort | |||
| .export out_class | |||
| .export out_class_size, out_class_destroy, out_class_ifaces, out_class_ifdir | |||
| .export out_load, out_entry, out_start, out_err | |||
| .segment "ZEROPAGE" | |||
| @@ -126,6 +132,24 @@ set_rec_ptr: | |||
| bne @add | |||
| @done: rts | |||
| ; out_rec_ptr = out_classes + out_index * OUT_CLASS_RECORD_SIZE. | |||
| set_class_ptr: | |||
| lda #<out_classes | |||
| sta out_rec_ptr | |||
| lda #>out_classes | |||
| sta out_rec_ptr+1 | |||
| ldx out_index | |||
| beq @done | |||
| @add: clc | |||
| lda out_rec_ptr | |||
| adc #OUT_CLASS_RECORD_SIZE | |||
| sta out_rec_ptr | |||
| bcc :+ | |||
| inc out_rec_ptr+1 | |||
| : dex | |||
| bne @add | |||
| @done: rts | |||
| ; Build and send "S0:name" on the command channel. | |||
| out_scratch: | |||
| sta out_scratch_ptr | |||
| @@ -210,6 +234,7 @@ out_begin: | |||
| lda #$00 | |||
| sta out_count | |||
| sta out_class_count | |||
| sta out_in_section | |||
| sta out_prg_created | |||
| sta out_map_created | |||
| @@ -527,6 +552,65 @@ out_bytes: | |||
| @done: clc | |||
| @out: rts | |||
| ; A/X = class name, Y = length. The descriptor values are supplied in the | |||
| ; exported out_class_* variables. C=1 sets out_err and adds no record. | |||
| out_class: | |||
| sta out_tmp | |||
| stx out_tmp+1 | |||
| sty out_tmp_len | |||
| lda out_class_count | |||
| cmp #OUT_MAX_CLASSES | |||
| bcs @too_many | |||
| lda out_tmp_len | |||
| cmp #OUT_SECTION_NAME_MAX + 1 | |||
| bcs @bad_name | |||
| lda out_class_count | |||
| sta out_index | |||
| jsr set_class_ptr | |||
| ldy #0 | |||
| lda out_tmp | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_tmp+1 | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_tmp_len | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_size | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_size+1 | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_destroy | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_destroy+1 | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_ifaces | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_ifdir | |||
| sta (out_rec_ptr),y | |||
| iny | |||
| lda out_class_ifdir+1 | |||
| sta (out_rec_ptr),y | |||
| inc out_class_count | |||
| clc | |||
| rts | |||
| @too_many: | |||
| lda #OUT_ERR_CLASSES | |||
| sta out_err | |||
| sec | |||
| rts | |||
| @bad_name: | |||
| lda #OUT_ERR_NAME | |||
| sta out_err | |||
| sec | |||
| rts | |||
| out_finish: | |||
| jsr out_end_section | |||
| jsr out_close_prg | |||
| @@ -727,6 +811,113 @@ out_write_map: | |||
| bcs @out | |||
| inc out_index | |||
| jmp @section | |||
| @done: jsr out_write_classes | |||
| bcs @out | |||
| clc | |||
| @out: rts | |||
| ; Write CLASS records after the SECTION records, in out_class call order. | |||
| out_write_classes: | |||
| lda #$00 | |||
| sta out_index | |||
| @class: | |||
| lda out_index | |||
| cmp out_class_count | |||
| bne @body | |||
| jmp @done | |||
| @body: | |||
| jsr set_class_ptr | |||
| lda #<class_prefix | |||
| 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 | |||
| @out: rts | |||
| @@ -809,6 +1000,8 @@ map_entry: .byte "ENTRY " | |||
| map_entry_len = * - map_entry | |||
| section_prefix: .byte "SECTION " | |||
| section_prefix_len = * - section_prefix | |||
| class_prefix: .byte "CLASS " | |||
| class_prefix_len = * - class_prefix | |||
| hex_digits: .byte "0123456789ABCDEF" | |||
| .segment "BSS" | |||
| @@ -835,6 +1028,7 @@ out_entry: .res 2 | |||
| out_start: .res 2 | |||
| out_current: .res 2 | |||
| out_count: .res 1 | |||
| out_class_count: .res 1 | |||
| out_index: .res 1 | |||
| out_in_section: .res 1 | |||
| out_prg_created: .res 1 | |||
| @@ -844,6 +1038,11 @@ out_prg_is_open: .res 1 | |||
| out_map_is_open: .res 1 | |||
| out_err: .res 1 | |||
| out_sections: .res OUT_MAX_SECTIONS * OUT_RECORD_SIZE | |||
| out_classes: .res OUT_MAX_CLASSES * OUT_CLASS_RECORD_SIZE | |||
| out_class_size: .res 2 | |||
| out_class_destroy: .res 2 | |||
| out_class_ifaces: .res 1 | |||
| out_class_ifdir: .res 2 | |||
| out_tmp: .res 2 | |||
| out_tmp_len: .res 1 | |||
| out_scratch_ptr: .res 2 | |||
| @@ -58,6 +58,7 @@ | |||
| .import assign_slots, check_interfaces | |||
| .export parse_file, tb_get, tb_set, mem_count, par_count | |||
| .export gen_text, gen_text_len, gen_print_count, gen_bad | |||
| .export ex_trace_on, ex_min_sp | |||
| .exportzp ex_tp | |||
| .export type_kind, type_line_lo, type_line_hi | |||
| @@ -89,6 +90,10 @@ parse_file: | |||
| stx parse_sp | |||
| jsr tt_clear ; bank-1 BSS is not zeroed at load | |||
| lda #$00 | |||
| sta gen_text_len ; first source-to-code path (E04-S05) | |||
| sta gen_print_count | |||
| sta gen_bad | |||
| sta gen_in_print | |||
| sta mem_count | |||
| sta par_count | |||
| sta mem_full | |||
| @@ -1095,6 +1100,7 @@ parse_statement: | |||
| stx stmt_sp | |||
| lda #$00 | |||
| sta ex_depth | |||
| sta gen_in_print | |||
| lda tok_col | |||
| sta stmt_col | |||
| jsr trace_line | |||
| @@ -1107,8 +1113,9 @@ parse_statement: | |||
| bne :+ | |||
| jmp @for | |||
| : cmp #KW_ELSE | |||
| beq @else | |||
| cmp #KW_RETURN | |||
| bne :+ | |||
| jmp @else | |||
| : cmp #KW_RETURN | |||
| beq @return | |||
| cmp #KW_PRINT | |||
| beq @print | |||
| @@ -1119,8 +1126,27 @@ parse_statement: | |||
| jsr value | |||
| jsr check_cond | |||
| jmp end_stmt | |||
| @print: jsr value | |||
| @print: lda pass ; pass 2 captures a string literal in print | |||
| cmp #$02 | |||
| bne @print_value | |||
| lda #$01 | |||
| sta gen_in_print | |||
| lda gen_text_len | |||
| sta gen_len_before | |||
| @print_value: | |||
| jsr value | |||
| jsr check_print | |||
| lda #$00 | |||
| sta gen_in_print | |||
| lda pass | |||
| cmp #$02 | |||
| bne @print_done | |||
| lda gen_text_len | |||
| cmp gen_len_before | |||
| bne @print_done | |||
| lda #$01 | |||
| sta gen_bad | |||
| @print_done: | |||
| jmp end_stmt | |||
| @delete: | |||
| jsr value | |||
| @@ -1528,11 +1554,15 @@ parse_postfix: | |||
| jsr trace_byte | |||
| lda tok_kind | |||
| cmp #TOK_IDENT | |||
| beq @name | |||
| cmp #KW_NEW | |||
| beq @new | |||
| cmp #KW_SELF | |||
| beq @self | |||
| bne :+ | |||
| jmp @name | |||
| : cmp #KW_NEW | |||
| bne :+ | |||
| jmp @new | |||
| : cmp #KW_SELF | |||
| bne :+ | |||
| jmp @self | |||
| : | |||
| ldx #lit_count - 1 | |||
| @lit: cmp lit_kinds,x | |||
| beq @literal | |||
| @@ -1548,7 +1578,17 @@ parse_postfix: | |||
| @early: lda #E_SYN_UNEXPECTED_EOL | |||
| jmp syn_error | |||
| @literal: | |||
| lda lit_types,x | |||
| lda gen_in_print | |||
| beq :+ | |||
| lda tok_kind | |||
| cmp #TOK_STRING | |||
| bne :+ | |||
| txa | |||
| pha | |||
| jsr gen_capture | |||
| pla | |||
| tax | |||
| : lda lit_types,x | |||
| sta sc_base | |||
| lda tok_value ; a number's value (others ignore it) | |||
| sta sc_val | |||
| @@ -1876,6 +1916,24 @@ trace_byte: | |||
| inc ex_tp+1 | |||
| @off: rts | |||
| ; Copy the current STRING token into gen_text. Called from parse_postfix | |||
| ; only inside a pass-2 print expression (E04-S05). Clobbers A/X/Y. | |||
| gen_capture: | |||
| inc gen_print_count | |||
| ldx tok_text_len | |||
| cpx #81 | |||
| bcc :+ | |||
| ldx #80 | |||
| : stx gen_text_len | |||
| ldy #$00 | |||
| @copy: cpy gen_text_len | |||
| beq @done | |||
| lda tok_text,y | |||
| sta gen_text,y | |||
| iny | |||
| bne @copy | |||
| @done: rts | |||
| ; Remember the member name token; the next token picks field or method. | |||
| pend_member: | |||
| lda tok_col | |||
| @@ -2719,6 +2777,13 @@ op_code: .res 1 | |||
| rt_col: .res 1 | |||
| val_col: .res 1 ; first column of the statement's value | |||
| ty_col: .res 1 ; first column of a signature type | |||
| ; pass-2 capture used by the hello compile fixture (E04-S05) | |||
| gen_text: .res 80 | |||
| gen_text_len: .res 1 | |||
| gen_print_count: .res 1 | |||
| gen_bad: .res 1 | |||
| gen_in_print: .res 1 | |||
| gen_len_before: .res 1 | |||
| mem_seq: .res 1 ; pass 2: next member row of cur_owner | |||
| impl_count: .res 1 | |||
| impl_full: .res 1 | |||
| @@ -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 | |||
| @done: rts | |||
| ; Pop argument bytes after a return. A/X = byte count (low/high). | |||
| ; Pop argument bytes after a return. A/X = byte count (low/high). A and X | |||
| ; are preserved so the caller can still store the callee's result. | |||
| frame_discard: | |||
| sta frame_discard_n | |||
| stx frame_discard_n+1 | |||
| clc | |||
| adc FRAME_SP | |||
| lda FRAME_SP | |||
| adc frame_discard_n | |||
| sta FRAME_SP | |||
| txa | |||
| adc FRAME_SP+1 | |||
| lda FRAME_SP+1 | |||
| adc frame_discard_n+1 | |||
| sta FRAME_SP+1 | |||
| lda frame_discard_n | |||
| ldx frame_discard_n+1 | |||
| rts | |||
| .segment "BSS" | |||
| @@ -177,4 +183,5 @@ frame_word: .res 2 | |||
| frame_locals: .res 2 | |||
| frame_saved_sp: .res 2 | |||
| frame_saved_fp: .res 2 | |||
| frame_discard_n: .res 2 | |||
| frame_error: .res 1 | |||
| @@ -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.