Freeze the four-byte fat reference, canonical null, slot/table layout, and the IF_RESULT return area so interface dispatch and table emission can build on one documented contract. Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>main
| @@ -11,7 +11,8 @@ 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; 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. | |||
| class records; E06-S01 freezes the interface value ABI. Generated-program heap | |||
| placement and banking remain open. | |||
| ## 1. System Boundary | |||
| @@ -326,8 +327,9 @@ memory. | |||
| - One-byte result: accumulator A | |||
| - Two-byte result: A is low byte, X is high byte | |||
| - Four-byte interface result: runtime-defined result area referenced through a | |||
| reserved ABI pointer; exact zero-page location is frozen after E01 | |||
| - Four-byte interface result: fixed zero-page area `IF_RESULT` (`$C6-$C9`); the | |||
| callee writes it and the caller copies it out before another | |||
| interface-returning call (E06-S01, D142) | |||
| - No result: A and X are caller-clobbered | |||
| The final register-clobber table is part of story E02-S02. | |||
| @@ -388,40 +390,71 @@ There is no parent-class pointer or inheritance slot. `src/runtime/objects.s` | |||
| provides read-only `obj_class`, `obj_size`, `obj_destroy`, and | |||
| `obj_iface_dir` accessors. | |||
| ### 7.5 Interface representation and dispatch | |||
| ### 7.5 Interface values and dispatch (E06-S01) | |||
| An interface value is a fat reference: | |||
| E06-S01 freezes the interface value ABI in `src/common/interfaces.inc` (D142). | |||
| An interface value is a four-byte fat reference, little-endian: | |||
| ```text | |||
| +0 object pointer (2) | |||
| +2 interface method-table pointer (2) | |||
| +0..1 object pointer | |||
| +2..3 interface method-table pointer | |||
| ``` | |||
| Each interface defines a slot order from its source declaration. For every | |||
| implemented interface, a class emits a table of two-byte method addresses in | |||
| that order. | |||
| Converting a concrete reference to an interface reference: | |||
| 1. copies the object pointer; | |||
| 2. uses compile-time-known class/interface metadata to select the class's | |||
| implementation table; | |||
| 3. stores the table pointer in the interface value. | |||
| An interface call: | |||
| All four bytes zero is the canonical null. A non-null object with a zero table | |||
| pointer is not a valid value: conversion reports `IF_ERR_TABLE` and writes a | |||
| zero table half, so no invalid pair is observable. | |||
| Each interface defines a slot order from its source declaration (E03-S06). For | |||
| every implemented interface, a class emits a table of two-byte method | |||
| addresses in that order with no header, so slot `n` lives at table+2*n. | |||
| `if_slot` reads such an entry and refuses a null value with `IF_ERR_NULL` or a | |||
| zero table with `IF_ERR_TABLE`. | |||
| `src/runtime/interfaces.s` supplies the transfers every later interface path | |||
| shares. Destinations and sources travel in module zero page (`if_obj`, `if_tab`, | |||
| `if_dst`, `if_src`) because A/X carry the object or the value, and `if_error` | |||
| reports `IF_OK`, `IF_ERR_NULL`, or `IF_ERR_TABLE`: | |||
| - `if_conv` writes the object pointer and the compile-time-selected table; | |||
| - `if_copy` copies all four bytes verbatim (a zero source pointer is | |||
| `IF_ERR_NULL`); | |||
| - `if_is_null` tests the object half; | |||
| - `if_slot` loads a method entry from a slot; | |||
| - `if_ret_store` and `if_ret_load` move a value through `IF_RESULT`. | |||
| Converting a concrete reference to an interface reference therefore copies the | |||
| object pointer, selects the class's implementation table from compile-time | |||
| metadata (E06-S03 emits it), and writes the table pointer beside the object | |||
| pointer, or four zero bytes when the object is null. | |||
| Storage follows the type sizes of section 7.1: interface parameters, locals, | |||
| fields, and array elements occupy four bytes with no alignment padding, and a | |||
| caller pushes all four bytes right-to-left so the object low byte is at | |||
| `FP+2`. A four-byte result cannot travel in A/X, so the callee writes the value | |||
| to the fixed zero-page area `IF_RESULT` (`$C6-$C9`) and the caller copies it out | |||
| immediately; another interface-returning call overwrites the area. The runtime | |||
| module asserts at link time that `IF_RESULT` sits between the print vectors | |||
| (`$C0-$C5`) and the for-end values (`$D0`). | |||
| An interface call then: | |||
| 1. checks the object pointer for null; | |||
| 2. installs the object pointer as `self`; | |||
| 3. loads the two-byte target from the known interface slot; | |||
| 3. loads the two-byte target from the compile-time interface slot; | |||
| 4. invokes the runtime's indirect-call trampoline. | |||
| The 8502 has no `JSR (address)` instruction, so the trampoline must establish a | |||
| synthetic return path safely. It is tested independently before interface code | |||
| generation relies on it. | |||
| synthetic return path safely. `construct.s` already proves one synthetic-return | |||
| form for constructors; E06-S02 tests the dispatch form independently before | |||
| interface code generation relies on it. | |||
| `delete` through an interface uses the object pointer and its class descriptor, | |||
| not the interface table, to find the optional destructor and object size. | |||
| The E06-S01 fixture (`tests/runtime/interface-test.s`) verifies the | |||
| little-endian layout, canonical null, the invalid-table refusal, four-byte | |||
| copy, slot order, the result area, and the pushed argument order in x128. | |||
| ### 7.6 Scalar code generation (E04-S03) | |||
| `src/compiler/codegen.s` emits documented 6502 instructions into a bounded | |||
| @@ -826,7 +859,6 @@ features depend on it. | |||
| ## Open Questions | |||
| - Generated-program memory map (the compiler map is frozen in section 4.4) | |||
| - Final interface-result convention | |||
| - Compiler table capacities and their user-visible resource limits | |||
| - Physical-hardware acceptance requirements | |||
| @@ -70,6 +70,7 @@ documented default. `Proposed` means a later change still needs approval. | |||
| | D140 | E04-S07 completes count-mode sizing and the MAP/diagnostic evidence. `em_count` selects the count pass; `em_end_func` records each routine length in `em_routine_sizes`; `sz_capture` snapshots code length, entry, routine sizes, and all 64 label offsets; the emitting pass runs with `diag_suppress`; `sz_verify` counts every difference and raises `RES_SIZE_MISMATCH` (E228) after the pass. The counter fixture requires `sz_mismatch = 0`, 175 code bytes, `CODE 1C4F`, deterministic PRGs, and `total = 15`. `output.s` adds `METHOD name address` records after `CLASS` records, proven by `METHOD DESTROY`/`METHOD STUB` in the class MAP. `tests/parser/unresolved.n8` proves one unknown method produces exactly one `SEM_UNKNOWN_NAME` (E61), and the output suite still proves E202 in the pass-3-adjacent writer path. | Closes the E04-S07 acceptance evidence without changing ordinary in-place fixtures: `parse_file` clears `em_count`, `diag_suppress`, and the base request, while `parse_file_sized` opts into the counted path. The code buffer remains 255 bytes and `em_entry` remains an offset; general program startup and frame sizing are separate work. Implemented by `parser.s`, `emit.s`, `emit.inc`, `sizing.s`, `diagnostics.s`, `diagcat.inc`, `output.s`, `formats.inc`, the counter/class/output/parser fixtures, and their scripts. | | |||
| | D141 | The MAP CLASS record carries the class descriptor's final address between the name and size: `CLASS name address size destroy interface-count interface-directory`. `out_class_addr` supplies it, `OUT_CLASS_RECORD_SIZE` grows from 10 to 12 bytes, and METHOD records remain `METHOD name address`. The class fixture exports `desc_empty`/`desc_box` and compares both descriptor addresses plus the two method addresses against the linker labels. | Makes class and method map records symmetric and directly verifiable, instead of requiring the caller to infer a descriptor address from the size/destructor fields. This is a visible MAP format change; existing SECTION, ENTRY, and method records keep their order. | | |||
| | D142 | E06-S01 freezes the interface value ABI in `src/common/interfaces.inc`. A value is a four-byte fat reference, little-endian: object pointer at +0 and interface method-table pointer at +2. All four bytes zero is canonical null; a non-null object with a zero table is `IF_ERR_TABLE`. A per-class table holds two-byte method entries in the interface's declaration slot order with no header, so slot *n* sits at table+2*n. Interface parameters, locals, fields, and array elements occupy four bytes with no padding, and callers push all four bytes right-to-left so the object low byte is at `FP+2`. A four-byte result cannot use A/X: the callee writes it to fixed zero-page `IF_RESULT` (`$C6-$C9`) and the caller copies it out before another interface-returning call. `src/runtime/interfaces.s` supplies `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_ret_store`, and `if_ret_load`, with `if_error` reporting `IF_OK`, `IF_ERR_NULL`, or `IF_ERR_TABLE`. | Implements the E06-S01 freeze above the object/descriptor ABI and settles the open interface-result convention. Dispatch, table emission, and interface lifetime remain E06-S02/E06-S03/E06-S06. The x128 fixture checks layout, null, copy, slot order, the result area, and argument order; the assertion keeps `IF_RESULT` clear of the print vectors and for-end values. | | |||
| ## Deferred | |||
| @@ -143,6 +143,26 @@ 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. | |||
| ## Interface Values (E06-S01) | |||
| | Element | Value | | |||
| |---|---| | |||
| | Value | 4 bytes: object pointer +0..1, method-table pointer +2..3, little-endian | | |||
| | Null | four zero bytes; a non-null object with a zero table is `IF_ERR_TABLE` | | |||
| | Method table | 2-byte entries in interface slot order, no header; slot *n* at table+2*n | | |||
| | Interface params/locals/fields/array elements | 4 bytes, no alignment padding | | |||
| | Argument order | caller pushes four bytes right-to-left, object low at `FP+2` | | |||
| | Four-byte result | `IF_RESULT` = `$C6-$C9`; the caller copies it before the next call | | |||
| | Module zero page | `if_obj`, `if_tab`, `if_dst`, `if_src` (link-allocated) | | |||
| | Module status | `if_error`: `IF_OK`, `IF_ERR_NULL`, `IF_ERR_TABLE` | | |||
| | Fixture | code below `$4000`, producer `$03`, six phase flags | | |||
| `src/common/interfaces.inc` fixes the layout; `src/runtime/interfaces.s` | |||
| implements `if_conv`, `if_copy`, `if_is_null`, `if_slot`, `if_ret_store`, and | |||
| `if_ret_load`. The module asserts at link time that `IF_RESULT` follows the | |||
| print vectors (`$C0-$C5`) and ends below the for-end values (`$D0`). Interface | |||
| table emission, dispatch, and interface lifetime remain E06-S02/E06-S03/E06-S06. | |||
| ## Mappings | |||
| | State | CR | RCR (low bits) | P0 | P1 | IRQs | | |||
| @@ -0,0 +1,104 @@ | |||
| --- | |||
| story: E06-S01 | |||
| owner: codebuff | |||
| status: done | |||
| started: 2026-11-10 | |||
| completed: 2026-11-10 | |||
| depends-on: [E01-S05, E03-S06, E05-S02] | |||
| --- | |||
| # E06-S01 Claim | |||
| ## Intended change | |||
| Freeze the four-byte interface value ABI in `src/common/interfaces.inc`, add | |||
| the layout and transfer runtime in `src/runtime/interfaces.s`, and prove the | |||
| layout, canonical null, invalid conversion, four-byte copy, slot order, | |||
| four-byte result area, and pushed argument order with a new x128 fixture. | |||
| Dispatch, method-table emission, and interface lifetime remain | |||
| E06-S02/E06-S03/E06-S06. | |||
| ## File area | |||
| - `src/common/interfaces.inc`, `src/runtime/interfaces.s` (new) | |||
| - `tests/runtime/interface-test.s` (new) | |||
| - `scripts/build-interface-test.ps1`, `scripts/verify-interface-test.ps1` (new) | |||
| - `docs/decisions.md` (D142), `docs/architecture.md` (7.2, 7.5, open questions), | |||
| `docs/memory-map.md` (Interface Values) | |||
| - `planning/claims/E06-S01.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] Four-byte interface value layout is documented and tested little-endian: | |||
| `src/common/interfaces.inc` fixes `IF_OBJECT` at +0 and `IF_TABLE` at +2; | |||
| phase 1 of the fixture converts object `$1234` with table `$5678` and checks | |||
| the raw bytes `34 12 78 56` at the destination. All four bytes zero is the | |||
| canonical null: phase 2 checks that a null object writes four zeros and | |||
| ignores a non-zero table pointer. | |||
| - [x] Interface slot and per-class table layouts are frozen: slots are | |||
| two-byte entries at table+2*n in interface declaration order with no header | |||
| (`IF_SLOT_SIZE`); phase 4 reads slot 0 as `$BEEF` and slot 1 as `$CAFE` from | |||
| a data table, and refuses a null value (`IF_ERR_NULL`) and a zero table | |||
| (`IF_ERR_TABLE`). | |||
| - [x] Conversion, parameter, return, local, field, and array storage rules are | |||
| specified in architecture 7.5, `docs/memory-map.md`, and D142: four bytes | |||
| with no padding, callers push all four bytes right-to-left, the four-byte | |||
| result uses a fixed area. Phase 3 copies values without reading through a | |||
| null source pointer; phase 5 moves a value through `IF_RESULT` ($C6-$C9) and | |||
| shows a failed store leaves the previous result intact; phase 6 pushes four | |||
| bytes, reads them back at `FP+2..FP+5`, and confirms the caller discards | |||
| exactly `IF_VALUE_SIZE` bytes. | |||
| - [x] Register clobbers and the four-byte return convention are finalized: | |||
| `src/runtime/interfaces.s` states that every helper may clobber A/X/Y and | |||
| returns status in C plus `if_error`; a four-byte result travels through | |||
| `IF_RESULT`, never A/X. The module asserts at link time that `IF_RESULT` | |||
| follows the print vectors (`$C0-$C5`) and ends below the for-end values | |||
| (`$D0`). | |||
| - [x] `if_conv` cannot expose an invalid pair: a non-null object with a zero | |||
| table writes a zero table half and returns C=1/`IF_ERR_TABLE` (phase 2). | |||
| ## Coordination notes | |||
| - Dependencies E01-S05, E03-S06, and E05-S02 are complete; no other claim was | |||
| active. `src/common/class.inc` (E05-S02) already reserves the descriptor's | |||
| interface-directory path, which this ABI reuses unchanged. | |||
| - E06-S02 (trampoline) and E06-S03 (table emission) consume these constants and | |||
| helpers; no dispatch code was added here. | |||
| - The installed assembler is ca65 2.19 from | |||
| `C:\Program Files\cc65-snapshot-win32\bin`, which is not on the default | |||
| shell PATH in this session; the verification command added it to PATH. | |||
| ## Work log | |||
| - Added `interfaces.inc` and `interfaces.s`, then the fixture. The fixture's | |||
| first macro used `a` as a parameter name, which ca65 rejects because it is | |||
| the accumulator keyword; renamed the parameters. | |||
| - The fixture first used `CR_RAM0_KERNEL`; the frozen constant in | |||
| `src/common/c128.inc` is spelled `CR_RAM0_KERNAL`. Fixed in the fixture. | |||
| - ld65 warned about the module zero-page cells imported as absolute; the | |||
| fixture now uses `.importzp` for `if_obj`, `if_tab`, `if_dst`, and `if_src`. | |||
| - A first x128 session could not reach the remote monitor (port 6557) and a | |||
| second reached it but the shared `Stop-Nova128X128` `quit` handshake timed | |||
| out. A manual session read the mailbox and screen, and the committed verify | |||
| script then passed unchanged on port 6603, so both were transient | |||
| emulator/port issues rather than fixture failures. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-interface-test.ps1` (x128, fresh D81); | |||
| `git diff --check`. | |||
| - Results: PRG ends `$246A`; mailbox | |||
| `4E 38 03 A5 00 00 00 00 01 01 01 01 01 01 00 00`; screen shows | |||
| `NOVA128 INTERFACE ABI` and `INTERFACE OK`; all six phase flags are 1. | |||
| - Files changed: `src/common/interfaces.inc`, `src/runtime/interfaces.s`, | |||
| `tests/runtime/interface-test.s`, `scripts/build-interface-test.ps1`, | |||
| `scripts/verify-interface-test.ps1`, `docs/decisions.md`, | |||
| `docs/architecture.md`, `docs/memory-map.md`, this claim. | |||
| - Remaining risks: `IF_RESULT` is a single fixed area, so a caller must copy a | |||
| result before evaluating another interface-returning call; the parser/emitter | |||
| does not yet enforce that rule because interface expressions are not emitted. | |||
| `if_slot` trusts the compile-time slot index (no runtime slot count), and no | |||
| helper validates a destination pointer. Generated `new`/`delete` for | |||
| interface-typed values and interface method tables are E06-S03/E06-S06. | |||
| Physical C128 testing remains P2 validation. | |||
| @@ -0,0 +1,25 @@ | |||
| # E06-S01: build the interface value ABI 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\interface-test" | |||
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |||
| $link = Invoke-Nova128Link -Name "interface-test" -BuildDir $out ` | |||
| -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( | |||
| (Join-Path $project "src\runtime\print.s"), | |||
| (Join-Path $project "src\runtime\frames.s"), | |||
| (Join-Path $project "src\runtime\interfaces.s"), | |||
| (Join-Path $project "tests\runtime\interface-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 ("interface-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000") | |||
| } | |||
| New-Nova128Disk -Path (Join-Path $out "interface-test.d81") -Files @( | |||
| @{ Name = "interface-test"; Path = $link.Program; Type = "prg" }) | |||
| Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) | |||
| @@ -0,0 +1,46 @@ | |||
| # E06-S01: run the interface value ABI fixture and check its mailbox. | |||
| [CmdletBinding()] | |||
| param([int]$Port = 6557, [int]$TimeoutSeconds = 180) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||
| & (Join-Path $PSScriptRoot "build-interface-test.ps1") | |||
| $out = Join-Path $project "build\interface-test" | |||
| $disk = Join-Path $out "interface-test.d81" | |||
| $failures = @() | |||
| $session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` | |||
| -Screenshot (Join-Path $out "interface-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 6; $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 "INTERFACE OK")) { | |||
| $failures += "screen does not contain 'INTERFACE OK'" | |||
| } | |||
| if ($failures) { throw ("interface 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 6 interface ABI flags set" | |||
| Write-Host "verified little-endian layout, canonical null, copy, slot order, IF_RESULT, and argument order" | |||
| @@ -0,0 +1,37 @@ | |||
| ; interfaces.inc - interface value ABI (E06-S01, D142). | |||
| ; | |||
| ; An interface value is a four-byte fat reference, little-endian: | |||
| ; +0..1 object pointer | |||
| ; +2..3 interface method-table pointer | |||
| ; All four bytes zero is the canonical null. A non-null object with a zero | |||
| ; table pointer is not a valid value (IF_ERR_TABLE). | |||
| ; | |||
| ; A class emits one method table per implemented interface: two-byte method | |||
| ; entry addresses in the interface's declaration slot order (E03-S06), with | |||
| ; no table header. Slot n therefore lives at table + 2*n. | |||
| ; | |||
| ; Parameters, locals, fields, and array elements holding an interface value | |||
| ; occupy IF_VALUE_SIZE bytes with no alignment padding. A caller pushes all | |||
| ; four bytes right-to-left, so the object low byte ends at FP+2. | |||
| ; | |||
| ; A four-byte result cannot travel in A/X. The callee writes the value to the | |||
| ; fixed zero-page area IF_RESULT and the caller copies it out before making | |||
| ; another interface-returning call. IF_RESULT sits between the print vectors | |||
| ; ($C0-$C5) and the for-end values ($D0), which src/runtime/interfaces.s | |||
| ; asserts at link time. | |||
| .ifndef INTERFACES_INC | |||
| INTERFACES_INC = 1 | |||
| IF_VALUE_SIZE = 4 | |||
| IF_OBJECT = 0 ; object pointer column | |||
| IF_TABLE = 2 ; method-table pointer column | |||
| IF_SLOT_SIZE = 2 ; one method address per interface slot | |||
| IF_RESULT = $C6 ; four-byte interface result area | |||
| IF_OK = 0 | |||
| IF_ERR_NULL = 1 ; null value pointer or null object | |||
| IF_ERR_TABLE = 2 ; non-null object with no method table | |||
| .endif | |||
| @@ -0,0 +1,218 @@ | |||
| ; interfaces.s - interface value layout and transfer (E06-S01). | |||
| ; | |||
| ; Defines the frozen four-byte fat reference from ../common/interfaces.inc | |||
| ; and the small transfers every later interface path needs. It performs no | |||
| ; dispatch: E06-S02 adds the indirect-call trampoline and E06-S03 emits the | |||
| ; per-class method tables. | |||
| ; | |||
| ; API (destination/source pointers travel in module zero page because A/X | |||
| ; usually carry the object or the value): | |||
| ; if_conv if_obj + if_tab -> four bytes at if_dst. A null object | |||
| ; writes four zero bytes and ignores if_tab. A non-null object | |||
| ; with a zero table sets IF_ERR_TABLE and writes a zero table | |||
| ; half, so no invalid pair can be observed. | |||
| ; if_copy if_src -> if_dst, all four bytes verbatim. A zero source | |||
| ; pointer is IF_ERR_NULL and leaves the destination alone. | |||
| ; if_is_null A/X = value pointer -> C=1 when its object pointer is zero | |||
| ; (a zero value pointer also reports null). | |||
| ; if_slot A/X = value pointer, Y = slot -> A/X = entry address read | |||
| ; from the table at slot*2. C=1 with IF_ERR_NULL or | |||
| ; IF_ERR_TABLE when the value cannot dispatch. | |||
| ; if_ret_store A/X = value pointer -> copies the value to IF_RESULT for a | |||
| ; four-byte return. A zero pointer is IF_ERR_NULL and leaves | |||
| ; the previous result intact. | |||
| ; if_ret_load A/X = destination -> copies IF_RESULT there. | |||
| ; | |||
| ; All destinations are caller-provided; nothing here allocates or validates a | |||
| ; destination pointer. Every routine may clobber A/X/Y: values and pointers | |||
| ; travel in memory, status in C (0 = success, 1 = refusal) and if_error | |||
| ; (IF_OK on success). | |||
| .setcpu "6502" | |||
| .include "../common/interfaces.inc" | |||
| .include "../common/emit.inc" | |||
| .export if_conv, if_copy, if_is_null, if_slot, if_ret_store, if_ret_load | |||
| .export if_error, if_obj, if_tab, if_dst, if_src | |||
| .segment "ZEROPAGE" | |||
| if_obj: .res 2 ; concrete object pointer for if_conv | |||
| if_tab: .res 2 ; method table selected at compile time | |||
| if_dst: .res 2 ; destination value pointer | |||
| if_src: .res 2 ; source value pointer | |||
| .segment "CODE" | |||
| ; IF_RESULT must stay clear of the print vectors and the for-end values. | |||
| .assert IF_RESULT >= $C6, error, "IF_RESULT overlaps the print vectors" | |||
| .assert IF_RESULT + IF_VALUE_SIZE <= EM_FOR_END, error, "IF_RESULT overlaps EM_FOR_END" | |||
| ; if_obj + if_tab -> if_dst. See the API above. | |||
| if_conv: | |||
| lda #IF_OK | |||
| sta if_error | |||
| ldy #IF_OBJECT | |||
| lda if_obj | |||
| sta (if_dst),y | |||
| iny | |||
| lda if_obj+1 | |||
| sta (if_dst),y | |||
| lda if_obj | |||
| ora if_obj+1 | |||
| beq @null | |||
| lda if_tab | |||
| ora if_tab+1 | |||
| beq @no_table | |||
| ldy #IF_TABLE | |||
| lda if_tab | |||
| sta (if_dst),y | |||
| iny | |||
| lda if_tab+1 | |||
| sta (if_dst),y | |||
| clc | |||
| rts | |||
| @null: | |||
| ldy #IF_TABLE | |||
| lda #$00 | |||
| sta (if_dst),y | |||
| iny | |||
| sta (if_dst),y | |||
| clc | |||
| rts | |||
| @no_table: | |||
| ldy #IF_TABLE | |||
| lda #$00 | |||
| sta (if_dst),y | |||
| iny | |||
| sta (if_dst),y | |||
| lda #IF_ERR_TABLE | |||
| sta if_error | |||
| sec | |||
| rts | |||
| ; if_src -> if_dst, four bytes in memory order. | |||
| if_copy: | |||
| lda #IF_OK | |||
| sta if_error | |||
| lda if_src | |||
| ora if_src+1 | |||
| beq @null | |||
| ldy #$00 | |||
| @loop: lda (if_src),y | |||
| sta (if_dst),y | |||
| iny | |||
| cpy #IF_VALUE_SIZE | |||
| bne @loop | |||
| clc | |||
| rts | |||
| @null: lda #IF_ERR_NULL | |||
| sta if_error | |||
| sec | |||
| rts | |||
| ; A/X = value pointer. C=1 when the object pointer is zero. | |||
| if_is_null: | |||
| sta if_src | |||
| stx if_src+1 | |||
| lda if_src | |||
| ora if_src+1 | |||
| beq @yes | |||
| ldy #IF_OBJECT | |||
| lda (if_src),y | |||
| iny | |||
| ora (if_src),y | |||
| beq @yes | |||
| clc | |||
| rts | |||
| @yes: sec | |||
| rts | |||
| ; A/X = value pointer, Y = slot. A/X = table[slot], C=0. | |||
| if_slot: | |||
| sta if_src | |||
| stx if_src+1 | |||
| sty if_slot_i | |||
| lda #IF_OK | |||
| sta if_error | |||
| lda if_src | |||
| ora if_src+1 | |||
| beq @null | |||
| ldy #IF_OBJECT | |||
| lda (if_src),y | |||
| iny | |||
| ora (if_src),y | |||
| beq @null | |||
| ldy #IF_TABLE | |||
| lda (if_src),y | |||
| sta if_tab | |||
| iny | |||
| lda (if_src),y | |||
| sta if_tab+1 | |||
| ora if_tab | |||
| beq @no_table | |||
| lda if_slot_i | |||
| asl a | |||
| sta if_off | |||
| ldy if_off | |||
| lda (if_tab),y | |||
| sta if_entry | |||
| iny | |||
| lda (if_tab),y | |||
| sta if_entry+1 | |||
| lda if_entry | |||
| ldx if_entry+1 | |||
| clc | |||
| rts | |||
| @null: lda #IF_ERR_NULL | |||
| sta if_error | |||
| jmp @fail | |||
| @no_table: | |||
| lda #IF_ERR_TABLE | |||
| sta if_error | |||
| @fail: lda #$00 | |||
| tax | |||
| sec | |||
| rts | |||
| ; A/X = value pointer -> IF_RESULT. | |||
| if_ret_store: | |||
| sta if_src | |||
| stx if_src+1 | |||
| lda #IF_OK | |||
| sta if_error | |||
| lda if_src | |||
| ora if_src+1 | |||
| beq @null | |||
| ldy #$00 | |||
| @loop: lda (if_src),y | |||
| sta IF_RESULT,y | |||
| iny | |||
| cpy #IF_VALUE_SIZE | |||
| bne @loop | |||
| clc | |||
| rts | |||
| @null: lda #IF_ERR_NULL | |||
| sta if_error | |||
| sec | |||
| rts | |||
| ; A/X = destination pointer <- IF_RESULT. | |||
| if_ret_load: | |||
| sta if_dst | |||
| stx if_dst+1 | |||
| lda #IF_OK | |||
| sta if_error | |||
| ldy #$00 | |||
| @loop: lda IF_RESULT,y | |||
| sta (if_dst),y | |||
| iny | |||
| cpy #IF_VALUE_SIZE | |||
| bne @loop | |||
| clc | |||
| rts | |||
| .segment "BSS" | |||
| if_error: .res 1 | |||
| if_slot_i: .res 1 | |||
| if_off: .res 1 | |||
| if_entry: .res 2 | |||
| @@ -0,0 +1,494 @@ | |||
| ; E06-S01: interface value ABI fixture in x128. | |||
| ; | |||
| ; Runs under CR=$0E. It checks: | |||
| ; - the four-byte value is little-endian: object low/high then table | |||
| ; low/high; | |||
| ; - null conversion writes four zero bytes and ignores the table pointer; | |||
| ; - a non-null object with no table is IF_ERR_TABLE with a zero table half; | |||
| ; - copying preserves all four bytes and never reads through a null pointer; | |||
| ; - slot lookup reads table[slot*2] in declaration slot order; | |||
| ; - the four-byte result area IF_RESULT carries all four bytes, and a failed | |||
| ; store leaves the previous result intact; | |||
| ; - four interface bytes pushed right-to-left arrive at FP+2..FP+5. | |||
| ; | |||
| ; MB_DATA flags 0..5 report the six phases. | |||
| .setcpu "6502" | |||
| .include "../../src/common/c128.inc" | |||
| .include "../../src/common/mailbox.inc" | |||
| .include "../../src/common/print.inc" | |||
| .include "../../src/common/frames.inc" | |||
| .include "../../src/common/interfaces.inc" | |||
| .import pr_char, pr_cr, pr_text | |||
| .import frame_init, frame_enter, frame_leave, frame_push_byte, frame_discard | |||
| .import if_conv, if_copy, if_is_null, if_slot, if_ret_store, if_ret_load | |||
| .import if_error | |||
| .importzp if_obj, if_tab, if_dst, if_src | |||
| .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 POINT var | |||
| lda #<var | |||
| sta tptr | |||
| lda #>var | |||
| sta tptr+1 | |||
| .endmacro | |||
| .macro SETDST var | |||
| lda #<var | |||
| sta if_dst | |||
| lda #>var | |||
| sta if_dst+1 | |||
| .endmacro | |||
| .macro SETSRC var | |||
| lda #<var | |||
| sta if_src | |||
| lda #>var | |||
| sta if_src+1 | |||
| .endmacro | |||
| .macro EXPECT v0, v1, v2, v3 | |||
| lda #v0 | |||
| sta e0 | |||
| lda #v1 | |||
| sta e1 | |||
| lda #v2 | |||
| sta e2 | |||
| lda #v3 | |||
| sta e3 | |||
| .endmacro | |||
| .macro FAIL_CS | |||
| bcc :+ | |||
| jmp @fail | |||
| : | |||
| .endmacro | |||
| .macro FAIL_CC | |||
| bcs :+ | |||
| jmp @fail | |||
| : | |||
| .endmacro | |||
| .macro FAIL_NE | |||
| beq :+ | |||
| 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, 22 | |||
| jsr pr_cr | |||
| ; 1: little-endian layout of object and table pointers. | |||
| PHASE 1 | |||
| lda #$34 | |||
| sta if_obj | |||
| lda #$12 | |||
| sta if_obj+1 | |||
| lda #$78 | |||
| sta if_tab | |||
| lda #$56 | |||
| sta if_tab+1 | |||
| SETDST val_a | |||
| jsr if_conv | |||
| FAIL_CS | |||
| lda if_error | |||
| FAIL_NE | |||
| POINT val_a | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #$01 | |||
| sta MB_DATA+0 | |||
| ; 2: canonical null and invalid conversion. | |||
| PHASE 2 | |||
| lda #$00 | |||
| sta if_obj | |||
| sta if_obj+1 | |||
| lda #$78 | |||
| sta if_tab | |||
| lda #$56 | |||
| sta if_tab+1 | |||
| SETDST val_b | |||
| jsr if_conv | |||
| FAIL_CS | |||
| lda if_error | |||
| FAIL_NE | |||
| POINT val_b | |||
| EXPECT $00, $00, $00, $00 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #<val_b | |||
| ldx #>val_b | |||
| jsr if_is_null | |||
| FAIL_CC | |||
| lda #<val_a | |||
| ldx #>val_a | |||
| jsr if_is_null | |||
| FAIL_CS | |||
| lda #$34 | |||
| sta if_obj | |||
| lda #$12 | |||
| sta if_obj+1 | |||
| lda #$00 | |||
| sta if_tab | |||
| sta if_tab+1 | |||
| SETDST val_c | |||
| jsr if_conv | |||
| FAIL_CC | |||
| lda if_error | |||
| cmp #IF_ERR_TABLE | |||
| FAIL_NE | |||
| POINT val_c | |||
| EXPECT $34, $12, $00, $00 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #$00 | |||
| ldx #$00 | |||
| jsr if_is_null | |||
| FAIL_CC | |||
| lda #$01 | |||
| sta MB_DATA+1 | |||
| ; 3: copying preserves every byte and rejects a null source pointer. | |||
| PHASE 3 | |||
| SETSRC val_a | |||
| SETDST val_d | |||
| jsr if_copy | |||
| FAIL_CS | |||
| POINT val_d | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| POINT val_a | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| SETSRC val_b | |||
| SETDST val_e | |||
| jsr if_copy | |||
| FAIL_CS | |||
| POINT val_e | |||
| EXPECT $00, $00, $00, $00 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #$00 | |||
| sta if_src | |||
| sta if_src+1 | |||
| SETDST val_e | |||
| jsr if_copy | |||
| FAIL_CC | |||
| lda if_error | |||
| cmp #IF_ERR_NULL | |||
| FAIL_NE | |||
| POINT val_e | |||
| EXPECT $00, $00, $00, $00 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #$01 | |||
| sta MB_DATA+2 | |||
| ; 4: slot order and the two refusal cases. | |||
| PHASE 4 | |||
| lda #$34 | |||
| sta if_obj | |||
| lda #$12 | |||
| sta if_obj+1 | |||
| lda #<slot_tab | |||
| sta if_tab | |||
| lda #>slot_tab | |||
| sta if_tab+1 | |||
| SETDST val_f | |||
| jsr if_conv | |||
| FAIL_CS | |||
| lda #<val_f | |||
| ldx #>val_f | |||
| ldy #0 | |||
| jsr if_slot | |||
| FAIL_CS | |||
| cmp #$EF | |||
| FAIL_NE | |||
| cpx #$BE | |||
| FAIL_NE | |||
| lda #<val_f | |||
| ldx #>val_f | |||
| ldy #1 | |||
| jsr if_slot | |||
| FAIL_CS | |||
| cmp #$FE | |||
| FAIL_NE | |||
| cpx #$CA | |||
| FAIL_NE | |||
| lda #<val_b | |||
| ldx #>val_b | |||
| ldy #0 | |||
| jsr if_slot | |||
| FAIL_CC | |||
| lda if_error | |||
| cmp #IF_ERR_NULL | |||
| FAIL_NE | |||
| lda #<val_c | |||
| ldx #>val_c | |||
| ldy #0 | |||
| jsr if_slot | |||
| FAIL_CC | |||
| lda if_error | |||
| cmp #IF_ERR_TABLE | |||
| FAIL_NE | |||
| lda #$01 | |||
| sta MB_DATA+3 | |||
| ; 5: four-byte result area. | |||
| PHASE 5 | |||
| lda #<val_a | |||
| ldx #>val_a | |||
| jsr if_ret_store | |||
| FAIL_CS | |||
| POINT IF_RESULT | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #<val_g | |||
| ldx #>val_g | |||
| jsr if_ret_load | |||
| FAIL_CS | |||
| POINT val_g | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #<val_b | |||
| ldx #>val_b | |||
| jsr if_ret_store | |||
| FAIL_CS | |||
| lda #<val_h | |||
| ldx #>val_h | |||
| jsr if_ret_load | |||
| FAIL_CS | |||
| POINT val_h | |||
| EXPECT $00, $00, $00, $00 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #<val_a | |||
| ldx #>val_a | |||
| jsr if_ret_store | |||
| FAIL_CS | |||
| lda #$00 | |||
| ldx #$00 | |||
| jsr if_ret_store | |||
| FAIL_CC | |||
| lda if_error | |||
| cmp #IF_ERR_NULL | |||
| FAIL_NE | |||
| POINT IF_RESULT | |||
| EXPECT $34, $12, $78, $56 | |||
| jsr cmp_val | |||
| FAIL_CS | |||
| lda #$01 | |||
| sta MB_DATA+4 | |||
| ; 6: an interface argument pushed right-to-left lands object-first. | |||
| PHASE 6 | |||
| jsr frame_init | |||
| lda FRAME_SP | |||
| sta saved_sp | |||
| lda FRAME_SP+1 | |||
| sta saved_sp+1 | |||
| lda #$56 | |||
| jsr frame_push_byte | |||
| FAIL_CS | |||
| lda #$78 | |||
| jsr frame_push_byte | |||
| FAIL_CS | |||
| lda #$12 | |||
| jsr frame_push_byte | |||
| FAIL_CS | |||
| lda #$34 | |||
| jsr frame_push_byte | |||
| FAIL_CS | |||
| jsr take_iface | |||
| FAIL_CS | |||
| lda #IF_VALUE_SIZE | |||
| ldx #$00 | |||
| jsr frame_discard | |||
| lda FRAME_SP | |||
| cmp saved_sp | |||
| FAIL_NE | |||
| lda FRAME_SP+1 | |||
| cmp saved_sp+1 | |||
| FAIL_NE | |||
| lda #$01 | |||
| sta MB_DATA+5 | |||
| TEXT iface_ok, 12 | |||
| 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 iface_fail, 14 | |||
| jsr pr_cr | |||
| @restore: | |||
| lda saved_cr | |||
| sta MMU_CR | |||
| plp | |||
| rts | |||
| ; Reads the four argument bytes at FP+2..FP+5: object $1234, table $5678. | |||
| take_iface: | |||
| lda #$00 | |||
| ldx #$00 | |||
| jsr frame_enter | |||
| bcs @bad | |||
| ldy #2 | |||
| lda (FRAME_FP),y | |||
| cmp #$34 | |||
| bne @bad | |||
| iny | |||
| lda (FRAME_FP),y | |||
| cmp #$12 | |||
| bne @bad | |||
| iny | |||
| lda (FRAME_FP),y | |||
| cmp #$78 | |||
| bne @bad | |||
| iny | |||
| lda (FRAME_FP),y | |||
| cmp #$56 | |||
| bne @bad | |||
| jsr frame_leave | |||
| clc | |||
| rts | |||
| @bad: sec | |||
| rts | |||
| ; C=1 when the four bytes at tptr differ from e0..e3. | |||
| cmp_val: | |||
| ldy #$00 | |||
| lda (tptr),y | |||
| cmp e0 | |||
| bne @no | |||
| iny | |||
| lda (tptr),y | |||
| cmp e1 | |||
| bne @no | |||
| iny | |||
| lda (tptr),y | |||
| cmp e2 | |||
| bne @no | |||
| iny | |||
| lda (tptr),y | |||
| cmp e3 | |||
| bne @no | |||
| clc | |||
| rts | |||
| @no: sec | |||
| rts | |||
| .segment "DATA" | |||
| slot_tab: | |||
| .word $BEEF | |||
| .word $CAFE | |||
| .assert * - slot_tab = 2 * IF_SLOT_SIZE, error, "slot table size" | |||
| .segment "RODATA" | |||
| title: .byte "NOVA128 INTERFACE ABI" | |||
| iface_ok: .byte "INTERFACE OK" | |||
| iface_fail: .byte "INTERFACE FAIL" | |||
| .segment "BSS" | |||
| saved_cr: .res 1 | |||
| saved_sp: .res 2 | |||
| e0: .res 1 | |||
| e1: .res 1 | |||
| e2: .res 1 | |||
| e3: .res 1 | |||
| val_a: .res IF_VALUE_SIZE | |||
| val_b: .res IF_VALUE_SIZE | |||
| val_c: .res IF_VALUE_SIZE | |||
| val_d: .res IF_VALUE_SIZE | |||
| val_e: .res IF_VALUE_SIZE | |||
| val_f: .res IF_VALUE_SIZE | |||
| val_g: .res IF_VALUE_SIZE | |||
| val_h: .res IF_VALUE_SIZE | |||
| test_end: | |||
Powered by TurnKey Linux.