diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index 8c129fd..276426f 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -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 diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index f6c65ed..8011aef 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -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 diff --git a/newLang/docs/memory-map.md b/newLang/docs/memory-map.md index 07bc0e6..faa6162 100644 --- a/newLang/docs/memory-map.md +++ b/newLang/docs/memory-map.md @@ -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 | diff --git a/newLang/planning/claims/E06-S01.md b/newLang/planning/claims/E06-S01.md new file mode 100644 index 0000000..ec086c4 --- /dev/null +++ b/newLang/planning/claims/E06-S01.md @@ -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. diff --git a/newLang/scripts/build-interface-test.ps1 b/newLang/scripts/build-interface-test.ps1 new file mode 100644 index 0000000..2beaa94 --- /dev/null +++ b/newLang/scripts/build-interface-test.ps1 @@ -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) diff --git a/newLang/scripts/verify-interface-test.ps1 b/newLang/scripts/verify-interface-test.ps1 new file mode 100644 index 0000000..d1b4d7e --- /dev/null +++ b/newLang/scripts/verify-interface-test.ps1 @@ -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" diff --git a/newLang/src/common/interfaces.inc b/newLang/src/common/interfaces.inc new file mode 100644 index 0000000..883232d --- /dev/null +++ b/newLang/src/common/interfaces.inc @@ -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 diff --git a/newLang/src/runtime/interfaces.s b/newLang/src/runtime/interfaces.s new file mode 100644 index 0000000..ebfbdcd --- /dev/null +++ b/newLang/src/runtime/interfaces.s @@ -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 diff --git a/newLang/tests/runtime/interface-test.s b/newLang/tests/runtime/interface-test.s new file mode 100644 index 0000000..84e3ac9 --- /dev/null +++ b/newLang/tests/runtime/interface-test.s @@ -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 + 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+1 +.endmacro + +.macro SETDST var + lda #var + sta if_dst+1 +.endmacro + +.macro SETSRC var + 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 + jsr if_is_null + FAIL_CC + lda #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+1 + SETDST val_f + jsr if_conv + FAIL_CS + + lda #val_f + ldy #0 + jsr if_slot + FAIL_CS + cmp #$EF + FAIL_NE + cpx #$BE + FAIL_NE + + lda #val_f + ldy #1 + jsr if_slot + FAIL_CS + cmp #$FE + FAIL_NE + cpx #$CA + FAIL_NE + + lda #val_b + ldy #0 + jsr if_slot + FAIL_CC + lda if_error + cmp #IF_ERR_NULL + FAIL_NE + + lda #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 + jsr if_ret_store + FAIL_CS + POINT IF_RESULT + EXPECT $34, $12, $78, $56 + jsr cmp_val + FAIL_CS + + lda #val_g + jsr if_ret_load + FAIL_CS + POINT val_g + EXPECT $34, $12, $78, $56 + jsr cmp_val + FAIL_CS + + lda #val_b + jsr if_ret_store + FAIL_CS + lda #val_h + jsr if_ret_load + FAIL_CS + POINT val_h + EXPECT $00, $00, $00, $00 + jsr cmp_val + FAIL_CS + + lda #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: