iface_slot reads the declaration slot assign_slots recorded, so a call site emits an offset the class table actually has and refuses a method the interface does not declare. The x128 fixture proves one call site reaching two classes, one object reaching two interface tables, a nested call with a one-byte result, and byte/word arguments returning a word with the frame stack restored, while self stays the underlying object. Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>main
| @@ -14,7 +14,9 @@ supplies an explicit-region heap; E05-S02 freezes class descriptors and MAP | |||||
| class records; E06-S01 freezes the interface value ABI; E06-S02 adds the | class records; E06-S01 freezes the interface value ABI; E06-S02 adds the | ||||
| dispatch trampoline; E06-S03 emits one table per implemented interface; | dispatch trampoline; E06-S03 emits one table per implemented interface; | ||||
| E06-S04 converts a class reference or null into an interface value and stores | E06-S04 converts a class reference or null into an interface value and stores | ||||
| it. Generated-program heap placement and banking remain open. | |||||
| it; E06-S05 resolves the compile-time slot at a call site and dispatches | |||||
| through the runtime table. Generated-program heap placement and banking remain | |||||
| open. | |||||
| ## 1. System Boundary | ## 1. System Boundary | ||||
| @@ -490,6 +492,15 @@ that wraps past `$FFFF` with `IF_ERR_RANGE`. Assignment rules stay in | |||||
| `types.s`: a class converts only to an interface it implements and everything | `types.s`: a class converts only to an interface it implements and everything | ||||
| else is `E080`, so an invalid conversion never reaches emission. | else is `E080`, so an invalid conversion never reaches emission. | ||||
| An interface call site fixes its slot at compile time: `iface_slot` reads the | |||||
| slot `assign_slots` recorded for the interface's method row and refuses a name | |||||
| the interface does not declare (`IS_ERR_METHOD`), so emission cannot invent an | |||||
| offset. At run time `if_call` takes the table half of the value and dispatches | |||||
| slot *n* from it, which is why one call site reaches different classes and one | |||||
| object reaches different interfaces. The caller pushes arguments right-to-left | |||||
| and discards them after the return; a one-byte result comes back in A and a | |||||
| two-byte result in A/X. | |||||
| The E06-S01 fixture (`tests/runtime/interface-test.s`) verifies the | The E06-S01 fixture (`tests/runtime/interface-test.s`) verifies the | ||||
| little-endian layout, canonical null, the invalid-table refusal, four-byte | little-endian layout, canonical null, the invalid-table refusal, four-byte | ||||
| copy, slot order, the result area, and the pushed argument order in x128. | copy, slot order, the result area, and the pushed argument order in x128. | ||||
| @@ -499,7 +510,9 @@ and I/D preservation, the three target faults, and recovery after a full-stack | |||||
| refusal. The E06-S04 fixture (`tests/runtime/convert-test.s`) adds the class and | refusal. The E06-S04 fixture (`tests/runtime/convert-test.s`) adds the class and | ||||
| null conversions, verbatim interface assignment, a four-byte field, three array | null conversions, verbatim interface assignment, a four-byte field, three array | ||||
| elements, the wrapped-address refusal, the argument layout, and the result | elements, the wrapped-address refusal, the argument layout, and the result | ||||
| area. | |||||
| area. The E06-S05 fixture (`tests/runtime/ifcall-test.s`) adds one call site | |||||
| reaching two classes, one object reaching two interface tables, a nested call | |||||
| with a one-byte result, and a byte/word argument pair returning a word. | |||||
| ### 7.6 Scalar code generation (E04-S03) | ### 7.6 Scalar code generation (E04-S03) | ||||
| @@ -77,6 +77,8 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| | D145 | E06-S04 closes the gap between the E06-S03 tables and an interface value. `it_find` (A = class ID, X = interface ID) returns the table address the class emitted for that interface, or C=1 with `IT_ERR_IFACE`, so a conversion never guesses a table and cannot emit one the class does not implement. `src/runtime/interfaces.s` gains `if_assign` (A/X = destination, Y = 0 copies `if_src`, any other value converts `if_obj` with the compile-time `if_tab`; a zero object writes the canonical null), `if_arg_push` (pushes four bytes right-to-left so the object low byte lands at FP+2), and `if_elem_store`/`if_elem_load` (4-byte elements at `base + IF_VALUE_SIZE*index`, refusing a wrapped address with `IF_ERR_RANGE`). | Makes the story's four acceptance paths executable before codegen grows four-byte values: assignment, parameter passing, field and element storage, and returns all go through one API, while the compile-time rejection stays in `types.s` (E080). The x128 conversion fixture checks the class pair, canonical null, verbatim copy, a field, three array elements, the wrapped-address refusal, the argument layout, and the result area; the table fixture now proves `it_find` for three implemented pairs and one refusal. | | | D145 | E06-S04 closes the gap between the E06-S03 tables and an interface value. `it_find` (A = class ID, X = interface ID) returns the table address the class emitted for that interface, or C=1 with `IT_ERR_IFACE`, so a conversion never guesses a table and cannot emit one the class does not implement. `src/runtime/interfaces.s` gains `if_assign` (A/X = destination, Y = 0 copies `if_src`, any other value converts `if_obj` with the compile-time `if_tab`; a zero object writes the canonical null), `if_arg_push` (pushes four bytes right-to-left so the object low byte lands at FP+2), and `if_elem_store`/`if_elem_load` (4-byte elements at `base + IF_VALUE_SIZE*index`, refusing a wrapped address with `IF_ERR_RANGE`). | Makes the story's four acceptance paths executable before codegen grows four-byte values: assignment, parameter passing, field and element storage, and returns all go through one API, while the compile-time rejection stays in `types.s` (E080). The x128 conversion fixture checks the class pair, canonical null, verbatim copy, a field, three array elements, the wrapped-address refusal, the argument layout, and the result area; the table fixture now proves `it_find` for three implemented pairs and one refusal. | | ||||
| | D146 | E06-S05 makes an interface call resolvable at its call site. `src/common/ifcalls.inc` fixes the call-site result codes (`IS_OK`, `IS_ERR_METHOD`), and `src/compiler/interfaces.s` gains `iface_slot` (A = interface name ID, X = method name ID -> A = the declaration slot from `C_MEM_SLOT`, or C=1 with `IS_ERR_METHOD` when the interface declares no such method, which covers a class or program ID too). At run time the slot indexes the table half of the fat reference, so the compile-time slot and the runtime table pointer together select the method; nothing at run time re-derives interface order. | The call sequence itself was already `if_call` (E06-S02) plus the frame ABI, so what was missing was a way for emission to learn the slot without re-deriving declaration order, and an end-to-end proof of the story's four behaviours. The x128 call fixture reaches two classes from one call site, two interfaces from one object by swapping only the table pointer, a nested call with a one-byte result, and a byte/word argument pair returning a word in A/X with the frame stack restored; it also checks `self` is the object, not the value record. | | |||||
| ## Deferred | ## Deferred | ||||
| - Native C128/80-column Vim port | - Native C128/80-column Vim port | ||||
| @@ -166,6 +166,8 @@ its normal frame and reads them where the caller pushed them. | |||||
| | Conversion | `if_assign` A/X = destination, Y = 0 copies `if_src`, else converts `if_obj` + `if_tab` | | | Conversion | `if_assign` A/X = destination, Y = 0 copies `if_src`, else converts `if_obj` + `if_tab` | | ||||
| | Argument push | `if_arg_push` A/X = value; object low byte at `FP+2`; `IF_ERR_NULL` / `IF_ERR_STACK` | | | Argument push | `if_arg_push` A/X = value; object low byte at `FP+2`; `IF_ERR_NULL` / `IF_ERR_STACK` | | ||||
| | Array elements | `if_elem_store`/`if_elem_load` A/X = base, Y = index; stride `IF_VALUE_SIZE`; `IF_ERR_RANGE` on wrap | | | Array elements | `if_elem_store`/`if_elem_load` A/X = base, Y = index; stride `IF_VALUE_SIZE`; `IF_ERR_RANGE` on wrap | | ||||
| | Interface call | `iface_slot` A = interface ID, X = method ID -> A = compile-time slot; `IS_ERR_METHOD` when absent | | |||||
| | Call sequence | caller pushes arguments, `if_call` A/X = value and Y = slot, then `frame_discard` the argument bytes | | |||||
| | Fixture | code below `$4000`, producer `$03`, six phase flags | | | Fixture | code below `$4000`, producer `$03`, six phase flags | | ||||
| `src/common/interfaces.inc` fixes the layout; `src/runtime/interfaces.s` | `src/common/interfaces.inc` fixes the layout; `src/runtime/interfaces.s` | ||||
| @@ -0,0 +1,102 @@ | |||||
| --- | |||||
| story: E06-S05 | |||||
| owner: codebuff | |||||
| status: done | |||||
| started: 2026-11-10 | |||||
| completed: 2026-11-10 | |||||
| depends-on: [E06-S02, E06-S04] | |||||
| --- | |||||
| # E06-S05 Claim | |||||
| ## Intended change | |||||
| Make an interface method call resolvable from both halves it needs: the slot | |||||
| that the interface declaration fixes at compile time, and the table pointer the | |||||
| value carries at run time. `iface_slot` resolves the slot from the parser tables | |||||
| and refuses a name the interface does not declare, so emission never invents an | |||||
| offset. The call itself stays `if_call` (E06-S02); this story proves it against | |||||
| the story's four behaviours, including `self` identity, two classes per call | |||||
| site, two interfaces per object, arguments, and one-/two-byte returns. | |||||
| ## File area | |||||
| - `src/common/ifcalls.inc` (new): `IS_OK`, `IS_ERR_METHOD` | |||||
| - `src/compiler/interfaces.s`: `iface_slot`, `is_error` | |||||
| - `tests/compiler/iftable-test.s`: slot-resolution checks in phase 7 | |||||
| - `tests/runtime/ifcall-test.s` (new) | |||||
| - `scripts/build-ifcall-test.ps1`, `scripts/verify-ifcall-test.ps1` (new) | |||||
| - `docs/decisions.md` (D146), `docs/architecture.md` (status, 7.5), | |||||
| `docs/memory-map.md` (call rows) | |||||
| - `planning/claims/E06-S05.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] Call uses the compile-time interface slot and runtime table pointer: | |||||
| `iface_slot` returns 0 for IFOO/ALPHA, 1 for IFOO/BETA, and 0 for IBAR/ALPHA | |||||
| in the table fixture, so the slot is the declaration order `assign_slots` | |||||
| recorded; the call fixture then passes those slots as constants to `if_call`, | |||||
| which reads the table pointer from the value at run time. Phase 2 swaps only | |||||
| the table pointer under one slot and reaches a different method for the same | |||||
| object. | |||||
| - [x] `self` is the underlying object, not the fat-reference address: every | |||||
| fixture method compares the incoming A/X against its class object | |||||
| (`$1111`/`$2222`) while the value records live elsewhere, and any miss counts | |||||
| `self_bad`; all phases require `self_bad = 0`. | |||||
| - [x] Two classes dispatch the same interface call to different methods: phase | |||||
| 1 calls slot 0 of IFOO on object `$1111` (returns `$A0`) and on `$2222` | |||||
| (returns `$B0`), with each class's call counter at 1. | |||||
| - [x] One class implements and dispatches at least two interfaces: phase 2 calls | |||||
| slot 0 on object `$1111` through the IFOO table (`m_a0`, `$A0`) and through | |||||
| the IBAR table (`m_abar`, `$C0`). | |||||
| - [x] Arguments and one-/two-byte returns work through interface calls: phase 3 | |||||
| calls slot 1, whose method makes its own interface call and returns `$5A+1` | |||||
| in A; phase 4 pushes a byte and a word, the method reads them at `FP+2..FP+4` | |||||
| and returns `$1234+7` in A/X (`$3B`/`$12`), and the caller's frame stack | |||||
| returns to `$C000` after discarding three argument bytes. | |||||
| - [x] A call site that names a method the interface does not declare is refused | |||||
| at compile time: `iface_slot` reports `IS_ERR_METHOD` for IBAR/GAMMA and for a | |||||
| class ID, verified in the table fixture. | |||||
| ## Coordination notes | |||||
| - Depends on E06-S02 (`if_call`) and E06-S04 (values); both are complete. The | |||||
| trampoline and frame ABI were reused unchanged, so this story adds no runtime | |||||
| entry point and cannot regress dispatch behaviour. | |||||
| - `iface_slot` is the lookup emission will call; codegen still handles only | |||||
| byte scalars and zero-argument self calls, so wiring interface expressions | |||||
| into `emit.s` remains open compiler work rather than part of this story. | |||||
| - Interface lifetime, deletion through an interface, and null-call policy are | |||||
| E06-S06. | |||||
| ## Work log | |||||
| - Added `iface_slot` beside `assign_slots` so the slot's writer and reader live | |||||
| in one module; it walks the interface's signature rows and returns | |||||
| `C_MEM_SLOT` rather than recomputing the index, which keeps a call site | |||||
| correct if slot assignment ever changes. | |||||
| - The call fixture reuses the E06-S02 dispatch conventions (A/X = value | |||||
| pointer, Y = slot, results in A/X, status in `if_error`) so it tests the call | |||||
| path rather than a private copy of it. | |||||
| - First build failed the `.assert` on the IFOO table: `* - tab_a_foo` spanned | |||||
| all four tables. The assertion now subtracts the next label, and a second one | |||||
| covers the one-slot IBAR table. | |||||
| ## Completion | |||||
| - Tests run: `scripts/verify-iftable-test.ps1` (x128, fresh D81, now including | |||||
| the slot-resolution checks); `scripts/verify-ifcall-test.ps1` (x128, fresh | |||||
| D81); `scripts/verify-parser-test.ps1` (42 semantic/parser fixtures); | |||||
| `git diff --check`. | |||||
| - Results: the table fixture still reports PASS with all eight flags, | |||||
| `NOVA128 IFTABLE TEST OK`, and its four MAP records (stub addresses moved to | |||||
| `7573`/`7574`/`7576`/`7577` because `interfaces.s` grew). The call fixture | |||||
| reports PASS with all four flags and prints `IFCALL OK`; its PRG ends `$2354`. | |||||
| - Remaining risks: the trampoline still trusts the value's object and table | |||||
| pointers, so a forged value can reach any address; `if_call` uses one shared | |||||
| `if_vec`, safe only because the jump happens before a nested call can reuse | |||||
| it; and argument pushes are byte-wise, so a stack refusal mid-push leaves the | |||||
| caller's arguments to be discarded by the caller. Generated-code emission of | |||||
| interface calls and lifetime remain E06-S06 and later compiler work, and | |||||
| physical C128 testing remains P2 validation. | |||||
| @@ -0,0 +1,25 @@ | |||||
| # E06-S05: build the interface call 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\ifcall-test" | |||||
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |||||
| $link = Invoke-Nova128Link -Name "ifcall-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\ifcall-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 ("ifcall-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000") | |||||
| } | |||||
| New-Nova128Disk -Path (Join-Path $out "ifcall-test.d81") -Files @( | |||||
| @{ Name = "ifcall-test"; Path = $link.Program; Type = "prg" }) | |||||
| Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) | |||||
| @@ -0,0 +1,46 @@ | |||||
| # E06-S05: run the interface call fixture in x128 and check its mailbox. | |||||
| [CmdletBinding()] | |||||
| param([int]$Port = 6561, [int]$TimeoutSeconds = 180) | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| & (Join-Path $PSScriptRoot "build-ifcall-test.ps1") | |||||
| $out = Join-Path $project "build\ifcall-test" | |||||
| $disk = Join-Path $out "ifcall-test.d81" | |||||
| $failures = @() | |||||
| $session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` | |||||
| -Screenshot (Join-Path $out "ifcall-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 4; $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 "IFCALL OK")) { | |||||
| $failures += "screen does not contain 'IFCALL OK'" | |||||
| } | |||||
| if ($failures) { throw ("interface call 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 4 call flags set" | |||||
| Write-Host "verified two classes per call site, two interfaces per object, a nested call, and byte/word arguments with a word result" | |||||
| @@ -0,0 +1,19 @@ | |||||
| ; ifcalls.inc - interface call site constants (E06-S05). | |||||
| ; | |||||
| ; A call through an interface type knows the interface and the method at | |||||
| ; compile time, so it also knows the declaration slot: slot n is at | |||||
| ; table + IF_SLOT_SIZE*n (interfaces.inc). src/compiler/interfaces.s resolves | |||||
| ; that slot from the parser tables with iface_slot; the table pointer half of | |||||
| ; the fat reference is read at run time, so two classes answering the same | |||||
| ; slot reach different methods. | |||||
| ; | |||||
| ; A call site that cannot resolve a slot reports an error rather than emitting | |||||
| ; a call that would dispatch through whatever bytes sit at that offset. | |||||
| .ifndef IFCALLS_INC | |||||
| IFCALLS_INC = 1 | |||||
| IS_OK = 0 | |||||
| IS_ERR_METHOD = 1 ; the interface declares no such method | |||||
| .endif | |||||
| @@ -5,6 +5,11 @@ | |||||
| ; assign_slots numbers each interface method in declaration order and | ; assign_slots numbers each interface method in declaration order and | ||||
| ; stores it in C_MEM_SLOT, so later method tables and the MAP | ; stores it in C_MEM_SLOT, so later method tables and the MAP | ||||
| ; writer can use a stable slot without re-reading the source; | ; writer can use a stable slot without re-reading the source; | ||||
| ; iface_slot A = interface name ID, X = method name ID. A = that | |||||
| ; method's declaration slot, C=0; C=1 with | |||||
| ; is_error = IS_ERR_METHOD when the interface declares no | |||||
| ; such method (or the ID is not an interface). A call site | |||||
| ; uses this so the slot it emits is the one E06-S03 wrote; | |||||
| ; check_interfaces | ; check_interfaces | ||||
| ; verifies each class against every interface it implements. | ; verifies each class against every interface it implements. | ||||
| ; A satisfying method is public and has the same parameter | ; A satisfying method is public and has the same parameter | ||||
| @@ -25,13 +30,15 @@ | |||||
| .setcpu "6502" | .setcpu "6502" | ||||
| .include "../common/errors.inc" | .include "../common/errors.inc" | ||||
| .include "../common/ifcalls.inc" | |||||
| .include "../common/parser.inc" | .include "../common/parser.inc" | ||||
| .import tb_get, tb_set, names_count | .import tb_get, tb_set, names_count | ||||
| .import diag_report, diag_line, diag_col, diag_arg1, diag_arg1_len | .import diag_report, diag_line, diag_col, diag_arg1, diag_arg1_len | ||||
| .import name_get, name_out, name_out_len | .import name_get, name_out, name_out_len | ||||
| .export assign_slots, check_interfaces, find_method | |||||
| .export assign_slots, iface_slot, check_interfaces, find_method | |||||
| .export is_error | |||||
| .segment "CODE" | .segment "CODE" | ||||
| @@ -75,6 +82,47 @@ assign_slots: | |||||
| bne @next | bne @next | ||||
| rts | rts | ||||
| ; --------------------------------------------------------- call sites | |||||
| ; A = interface name ID, X = method name ID. A = declaration slot, C=0. | |||||
| iface_slot: | |||||
| sta is_iface | |||||
| stx is_name | |||||
| ldy is_iface | |||||
| lda #C_TYPE_KIND | |||||
| jsr tb_get | |||||
| cmp #TYPE_INTERFACE | |||||
| bne @no | |||||
| ldy is_iface | |||||
| lda #C_TYPE_MFIRST | |||||
| jsr tb_get | |||||
| sta is_row | |||||
| ldy is_iface | |||||
| lda #C_TYPE_MCOUNT | |||||
| jsr tb_get | |||||
| sta is_n | |||||
| @row: lda is_n | |||||
| beq @no | |||||
| ldy is_row | |||||
| lda #C_MEM_NAME | |||||
| jsr tb_get | |||||
| cmp is_name | |||||
| beq @yes | |||||
| inc is_row | |||||
| dec is_n | |||||
| bne @row | |||||
| @no: lda #IS_ERR_METHOD | |||||
| sta is_error | |||||
| lda #$00 | |||||
| sec | |||||
| rts | |||||
| @yes: lda #IS_OK | |||||
| sta is_error | |||||
| ldy is_row | |||||
| lda #C_MEM_SLOT | |||||
| jsr tb_get | |||||
| clc | |||||
| rts | |||||
| ; ----------------------------------------------------------- conformance | ; ----------------------------------------------------------- conformance | ||||
| ; For each class in declaration order, walk its implements list in source | ; For each class in declaration order, walk its implements list in source | ||||
| ; order and each interface's signatures in slot order. | ; order and each interface's signatures in slot order. | ||||
| @@ -411,6 +459,12 @@ iface_err: | |||||
| rts | rts | ||||
| .segment "BSS" | .segment "BSS" | ||||
| ; call sites | |||||
| is_iface: .res 1 | |||||
| is_name: .res 1 | |||||
| is_row: .res 1 | |||||
| is_n: .res 1 | |||||
| is_error: .res 1 | |||||
| ; slot assignment | ; slot assignment | ||||
| as_id: .res 1 | as_id: .res 1 | ||||
| as_row: .res 1 | as_row: .res 1 | ||||
| @@ -23,6 +23,7 @@ | |||||
| .include "../../src/common/formats.inc" | .include "../../src/common/formats.inc" | ||||
| .include "../../src/common/parser.inc" | .include "../../src/common/parser.inc" | ||||
| .include "../../src/common/iftables.inc" | .include "../../src/common/iftables.inc" | ||||
| .include "../../src/common/ifcalls.inc" | |||||
| .import mmu_init, mmu_bank0, mmu_shutdown | .import mmu_init, mmu_bank0, mmu_shutdown | ||||
| .import out_begin, out_startup, out_finish, out_abort | .import out_begin, out_startup, out_finish, out_abort | ||||
| @@ -33,6 +34,7 @@ | |||||
| .import it_begin, it_set_addr, it_class, it_find | .import it_begin, it_set_addr, it_class, it_find | ||||
| .import it_error, it_count, it_used, it_data | .import it_error, it_count, it_used, it_data | ||||
| .import it_class_ids, it_iface_ids, it_table_addrs, it_slots, it_offsets | .import it_class_ids, it_iface_ids, it_table_addrs, it_slots, it_offsets | ||||
| .import iface_slot, is_error | |||||
| TABLE_BASE = $B000 | TABLE_BASE = $B000 | ||||
| @@ -393,6 +395,58 @@ test_entry: | |||||
| jmp @fail | jmp @fail | ||||
| : CHK it_error, IT_ERR_IFACE | : CHK it_error, IT_ERR_IFACE | ||||
| CHK it_count, 3 | CHK it_count, 3 | ||||
| ; A call site resolves its slot from the interface declaration: | |||||
| ; IFOO ALPHA=0, BETA=1; IBAR ALPHA=0; IBAR GAMMA does not exist, and a | |||||
| ; class name is not an interface. | |||||
| STEP $34 | |||||
| lda #1 | |||||
| ldx #1 | |||||
| jsr iface_slot | |||||
| bcc :+ | |||||
| jmp @fail | |||||
| : cmp #0 | |||||
| beq :+ | |||||
| jmp @fail | |||||
| : CHK is_error, IS_OK | |||||
| STEP $35 | |||||
| lda #1 | |||||
| ldx #2 | |||||
| jsr iface_slot | |||||
| bcc :+ | |||||
| jmp @fail | |||||
| : cmp #1 | |||||
| beq :+ | |||||
| jmp @fail | |||||
| : CHK is_error, IS_OK | |||||
| STEP $36 | |||||
| lda #2 | |||||
| ldx #1 | |||||
| jsr iface_slot | |||||
| bcc :+ | |||||
| jmp @fail | |||||
| : cmp #0 | |||||
| beq :+ | |||||
| jmp @fail | |||||
| : CHK is_error, IS_OK | |||||
| STEP $37 | |||||
| lda #2 | |||||
| ldx #3 | |||||
| jsr iface_slot | |||||
| bcs :+ ; no such method: a refusal is expected | |||||
| jmp @fail | |||||
| : CHK is_error, IS_ERR_METHOD | |||||
| STEP $38 | |||||
| lda #3 | |||||
| ldx #1 | |||||
| jsr iface_slot | |||||
| bcs :+ ; a class is not an interface | |||||
| jmp @fail | |||||
| : CHK is_error, IS_ERR_METHOD | |||||
| lda #$01 | lda #$01 | ||||
| sta MB_DATA+7 | sta MB_DATA+7 | ||||
| @@ -0,0 +1,381 @@ | |||||
| ; E06-S05: interface method call fixture in x128. | |||||
| ; | |||||
| ; Runs under CR=$0E on the E06-S01 value ABI, the E06-S02 trampoline, the | |||||
| ; E06-S03 table shape, and the E06-S04 conversion API. It checks: | |||||
| ; - two classes dispatch the same interface slot to different methods, and | |||||
| ; self is the underlying object ($1111 / $2222), not the value record; | |||||
| ; - one class dispatches through two of its interface tables, so the same | |||||
| ; object reaches different methods by table pointer alone; | |||||
| ; - a callee can itself make an interface call and pass on a one-byte result; | |||||
| ; - a byte argument and a two-byte argument arrive at FP+2..FP+4, a two-byte | |||||
| ; result comes back in A/X, and the caller's frame stack is restored. | |||||
| ; | |||||
| ; The slots are the compile-time interface slots (resolved by the compiler's | |||||
| ; iface_slot in the table fixture); the table pointer is read at run time from | |||||
| ; the value, which is what lets one call site reach two classes. | |||||
| ; | |||||
| ; MB_DATA flags 0..3 report the four 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_push_word, frame_discard | |||||
| .import if_call, if_error | |||||
| .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_NE | |||||
| beq :+ | |||||
| jmp @fail | |||||
| : | |||||
| .endmacro | |||||
| ; A/X = self. A method whose self is not its class's object counts the miss | |||||
| ; and returns failure instead of reporting a result. | |||||
| .macro SELF_IS lo, hi | |||||
| cmp #lo | |||||
| bne @selfbad | |||||
| cpx #hi | |||||
| beq @selfok | |||||
| @selfbad: | |||||
| inc self_bad | |||||
| lda #$00 | |||||
| ldx #$00 | |||||
| rts | |||||
| @selfok: | |||||
| .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_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 | |||||
| sta self_bad | |||||
| sta a_calls | |||||
| sta b_calls | |||||
| sta leaf_calls | |||||
| ldx #$03 | |||||
| @flags: sta MB_DATA,x | |||||
| dex | |||||
| bpl @flags | |||||
| lda MMU_CR | |||||
| sta saved_cr | |||||
| lda #CR_RAM0_KERNAL | |||||
| sta MMU_CR | |||||
| lda #PR_CLEAR | |||||
| jsr pr_char | |||||
| TEXT title, title_len | |||||
| jsr pr_cr | |||||
| ; 1: one call site, two classes, two methods. | |||||
| PHASE 1 | |||||
| lda #<val_a_foo | |||||
| ldx #>val_a_foo | |||||
| ldy #0 | |||||
| jsr if_call | |||||
| sta result_a | |||||
| lda if_error | |||||
| FAIL_NE | |||||
| lda result_a | |||||
| cmp #$A0 | |||||
| FAIL_NE | |||||
| lda a_calls | |||||
| cmp #1 | |||||
| FAIL_NE | |||||
| lda #<val_b_foo | |||||
| ldx #>val_b_foo | |||||
| ldy #0 | |||||
| jsr if_call | |||||
| sta result_a | |||||
| lda if_error | |||||
| FAIL_NE | |||||
| lda result_a | |||||
| cmp #$B0 | |||||
| FAIL_NE | |||||
| lda b_calls | |||||
| cmp #1 | |||||
| FAIL_NE | |||||
| lda self_bad | |||||
| FAIL_NE | |||||
| lda #$01 | |||||
| sta MB_DATA+0 | |||||
| ; 2: one class, two interface tables, different methods. | |||||
| PHASE 2 | |||||
| lda #<val_a_bar | |||||
| ldx #>val_a_bar | |||||
| ldy #0 | |||||
| jsr if_call | |||||
| sta result_a | |||||
| lda if_error | |||||
| FAIL_NE | |||||
| lda result_a | |||||
| cmp #$C0 | |||||
| FAIL_NE | |||||
| lda a_calls | |||||
| cmp #2 | |||||
| FAIL_NE | |||||
| lda self_bad | |||||
| FAIL_NE | |||||
| lda #$01 | |||||
| sta MB_DATA+1 | |||||
| ; 3: an interface call inside a method, one-byte result passed on. | |||||
| PHASE 3 | |||||
| lda #<val_a_foo | |||||
| ldx #>val_a_foo | |||||
| ldy #1 | |||||
| jsr if_call | |||||
| sta result_a | |||||
| lda if_error | |||||
| FAIL_NE | |||||
| lda result_a | |||||
| cmp #$5B | |||||
| FAIL_NE | |||||
| lda a_calls | |||||
| cmp #3 | |||||
| FAIL_NE | |||||
| lda leaf_calls | |||||
| cmp #1 | |||||
| FAIL_NE | |||||
| lda self_bad | |||||
| FAIL_NE | |||||
| lda #$01 | |||||
| sta MB_DATA+2 | |||||
| ; 4: arguments and a two-byte result through an interface call. | |||||
| PHASE 4 | |||||
| jsr frame_init | |||||
| lda #$34 | |||||
| ldx #$12 | |||||
| jsr frame_push_word ; arg1 = $1234 | |||||
| FAIL_CS | |||||
| lda #$07 | |||||
| jsr frame_push_byte ; arg0 = 7 | |||||
| FAIL_CS | |||||
| lda #<val_a_foo | |||||
| ldx #>val_a_foo | |||||
| ldy #2 | |||||
| jsr if_call | |||||
| sta result_a | |||||
| stx result_x | |||||
| lda if_error | |||||
| FAIL_NE | |||||
| lda #$03 | |||||
| ldx #$00 | |||||
| jsr frame_discard | |||||
| lda result_a | |||||
| cmp #$3B ; $1234 + 7 | |||||
| FAIL_NE | |||||
| lda result_x | |||||
| cmp #$12 | |||||
| FAIL_NE | |||||
| lda FRAME_SP | |||||
| cmp #<FRAME_STACK_TOP | |||||
| FAIL_NE | |||||
| lda FRAME_SP+1 | |||||
| cmp #>FRAME_STACK_TOP | |||||
| FAIL_NE | |||||
| lda a_calls | |||||
| cmp #4 | |||||
| FAIL_NE | |||||
| lda self_bad | |||||
| FAIL_NE | |||||
| lda #$01 | |||||
| sta MB_DATA+3 | |||||
| TEXT ok_msg, ok_len | |||||
| 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 fail_msg, fail_len | |||||
| jsr pr_cr | |||||
| @restore: | |||||
| lda saved_cr | |||||
| sta MMU_CR | |||||
| plp | |||||
| rts | |||||
| .segment "CODE" | |||||
| ; IFOO slot 0 of class A: byte result, self checked. | |||||
| m_a0: | |||||
| SELF_IS $11, $11 | |||||
| inc a_calls | |||||
| lda #$A0 | |||||
| ldx #$00 | |||||
| rts | |||||
| ; IFOO slot 1 of class A: calls leaf through val_leaf and adds one. | |||||
| m_a1: | |||||
| SELF_IS $11, $11 | |||||
| inc a_calls | |||||
| lda #<val_leaf | |||||
| ldx #>val_leaf | |||||
| ldy #$00 | |||||
| jsr if_call | |||||
| sta child | |||||
| stx child_x | |||||
| lda if_error | |||||
| bne @bad | |||||
| lda child | |||||
| clc | |||||
| adc #$01 | |||||
| ldx #$00 | |||||
| rts | |||||
| @bad: inc self_bad | |||||
| lda #$00 | |||||
| ldx #$00 | |||||
| rts | |||||
| ; IFOO slot 2 of class A: (byte, word) -> word sum. | |||||
| m_addw: | |||||
| SELF_IS $11, $11 | |||||
| inc a_calls | |||||
| lda #$00 | |||||
| ldx #$00 | |||||
| jsr frame_enter | |||||
| bcs @bad | |||||
| ldy #$02 | |||||
| lda (FRAME_FP),y | |||||
| sta tmp | |||||
| iny | |||||
| lda (FRAME_FP),y | |||||
| clc | |||||
| adc tmp | |||||
| sta res_lo | |||||
| iny | |||||
| lda (FRAME_FP),y | |||||
| adc #$00 | |||||
| sta res_hi | |||||
| jsr frame_leave | |||||
| lda res_lo | |||||
| ldx res_hi | |||||
| clc | |||||
| rts | |||||
| @bad: lda #$00 | |||||
| ldx #$00 | |||||
| sec | |||||
| rts | |||||
| ; IBAR slot 0 of class A: a different method for the same object. | |||||
| m_abar: | |||||
| SELF_IS $11, $11 | |||||
| inc a_calls | |||||
| lda #$C0 | |||||
| ldx #$00 | |||||
| rts | |||||
| ; IFOO slot 0 of class B. | |||||
| m_b0: | |||||
| SELF_IS $22, $22 | |||||
| inc b_calls | |||||
| lda #$B0 | |||||
| ldx #$00 | |||||
| rts | |||||
| ; Target of the nested call. | |||||
| m_leaf: | |||||
| inc leaf_calls | |||||
| lda #$5A | |||||
| ldx #$00 | |||||
| rts | |||||
| .segment "DATA" | |||||
| ; Object pointers are only identities here; the methods compare them. | |||||
| val_a_foo: .word $1111, tab_a_foo | |||||
| val_a_bar: .word $1111, tab_a_bar | |||||
| val_b_foo: .word $2222, tab_b_foo | |||||
| val_leaf: .word $9999, tab_leaf | |||||
| tab_a_foo: .word m_a0, m_a1, m_addw | |||||
| tab_a_bar: .word m_abar | |||||
| tab_b_foo: .word m_b0 | |||||
| tab_leaf: .word m_leaf | |||||
| .assert tab_a_bar - tab_a_foo = 3 * IF_SLOT_SIZE, error, "IFOO table size" | |||||
| .assert tab_b_foo - tab_a_bar = 1 * IF_SLOT_SIZE, error, "IBAR table size" | |||||
| .segment "RODATA" | |||||
| title: .byte "NOVA128 IFCALL TEST" | |||||
| title_len = * - title | |||||
| ok_msg: .byte "IFCALL OK" | |||||
| ok_len = * - ok_msg | |||||
| fail_msg: .byte "IFCALL FAIL" | |||||
| fail_len = * - fail_msg | |||||
| .segment "BSS" | |||||
| saved_cr: .res 1 | |||||
| a_calls: .res 1 | |||||
| b_calls: .res 1 | |||||
| leaf_calls: .res 1 | |||||
| self_bad: .res 1 | |||||
| result_a: .res 1 | |||||
| result_x: .res 1 | |||||
| child: .res 1 | |||||
| child_x: .res 1 | |||||
| tmp: .res 1 | |||||
| res_lo: .res 1 | |||||
| res_hi: .res 1 | |||||
| test_end: | |||||
Powered by TurnKey Linux.