Implement transactional frame entry/leave, right-to-left arguments, one- and two-byte returns, and guards for both the software stack and the 6502 hardware stack. Add a recursive x128 ABI fixture covering bounded success and overflow unwind. Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>main
| @@ -41,7 +41,7 @@ SEGMENTS { | |||||
| STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D | STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D | ||||
| # COMMON's load image comes next so mmu_init, which copies it in the | # COMMON's load image comes next so mmu_init, which copies it in the | ||||
| # system mapping (BASIC ROM over $4000-$7FFF), reads it below $4000. | # system mapping (BASIC ROM over $4000-$7FFF), reads it below $4000. | ||||
| COMMON: load = MAIN, run = COMMONRAM, type = rw, define = yes; | |||||
| COMMON: load = MAIN, run = COMMONRAM, type = rw, optional = yes, define = yes; | |||||
| CODE: load = MAIN, type = ro; | CODE: load = MAIN, type = ro; | ||||
| RODATA: load = MAIN, type = ro, optional = yes; | RODATA: load = MAIN, type = ro, optional = yes; | ||||
| DATA: load = MAIN, type = rw, optional = yes; | DATA: load = MAIN, type = rw, optional = yes; | ||||
| @@ -306,8 +306,10 @@ memory. | |||||
| - Load address `$1C01`; BASIC 7 stub `10 SYS 7181`; entry `$1C0D`. | - Load address `$1C01`; BASIC 7 stub `10 SYS 7181`; entry `$1C0D`. | ||||
| - Sections are contiguous and ascending; gaps are zero-filled. | - Sections are contiguous and ascending; gaps are zero-filled. | ||||
| - `NAME.MAP` records `ENTRY` and every `SECTION name start size` in hex. | - `NAME.MAP` records `ENTRY` and every `SECTION name start size` in hex. | ||||
| - The startup writes the mailbox with producer `$03`, then PASS, and returns | |||||
| to BASIC. It does not yet install a runtime mapping or frames. | |||||
| - The E04-S01 startup writes the mailbox with producer `$03`, then PASS, and | |||||
| returns to BASIC. E04-S02 supplies the frame runtime separately; code | |||||
| generation will switch to CR `$0E` and initialize frames when it emits user | |||||
| code (E04-S03). | |||||
| ### 7.1 Scalar representation | ### 7.1 Scalar representation | ||||
| @@ -326,15 +328,34 @@ memory. | |||||
| The final register-clobber table is part of story E02-S02. | The final register-clobber table is part of story E02-S02. | ||||
| ### 7.3 Frames and calls | |||||
| ### 7.3 Frames and calls (E04-S02) | |||||
| - The hardware stack carries return addresses and small runtime temporaries. | |||||
| - Arguments, locals, and expression spills use a software-managed stack/frame. | |||||
| - `self` is held in a reserved two-byte ABI pointer during an instance call. | |||||
| - Parameters are laid out in declaration order using their ABI sizes. | |||||
| - Concrete method calls are statically resolved to a direct `JSR`. | |||||
| - The runtime checks stack bounds before frame growth in debug builds and at | |||||
| defined safety points in release builds. | |||||
| Generated code runs with CR `$0E` (RAM through `$BFFF`) and IRQs masked, then | |||||
| restores the entry CR before returning to BASIC. The software frame stack is | |||||
| bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP` and `FP` use the free | |||||
| zero-page bytes `$FB-$FE`. The compiler's own stack and private zero page are | |||||
| not shared with the generated program. | |||||
| Frame layout relative to `FP`: | |||||
| ```text | |||||
| FP+0..1 previous FP (pushed by frame_enter) | |||||
| FP+2.. arguments, arg0 at the lowest address | |||||
| FP-1.. locals, allocated below FP | |||||
| ``` | |||||
| A caller pushes arguments right-to-left, executes `JSR`, and discards the | |||||
| argument bytes after the return. The callee calls `frame_enter` with the local | |||||
| byte count in A/X, uses locals below `FP`, then calls `frame_leave`. One-byte | |||||
| results use A; two-byte results use A (low) and X (high). The hardware stack | |||||
| still carries `JSR` return addresses and small temporaries, so `frame_enter` | |||||
| also refuses a frame when the 6502 stack pointer falls below `$40`. | |||||
| `frame_enter` is transactional: it checks both bounds before committing, | |||||
| restores `SP` and `FP` on failure, and returns C=1 with | |||||
| `FRAME_ERR_STACK`. The caller can unwind through `frame_leave` without a | |||||
| partial frame. `self`, parameters in declaration order, and direct `JSR` for | |||||
| concrete method calls are unchanged from the ABI baseline. | |||||
| ### 7.4 Object layout | ### 7.4 Object layout | ||||
| @@ -55,6 +55,7 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| | D126 | Arrays are fields or locals only: never parameters, results, or whole values. A bare array is E088, and a parameter or result declaring one is E094. | Keeps array storage and copying out of the ABI (D108); indexing is the only way to read or write one. Implemented by `no_array_sig` and `ty_assign`. | | | D126 | Arrays are fields or locals only: never parameters, results, or whole values. A bare array is E088, and a parameter or result declaring one is E094. | Keeps array storage and copying out of the ABI (D108); indexing is the only way to read or write one. Implemented by `no_array_sig` and `ty_assign`. | | ||||
| | D127 | `return` with a value in a program is E151. A method that declares a result must end every path in `return`; an `if` satisfies this only when both branches do, a `while` never does, and a missing return is reported at `end` as E150. | Spec 7 permits `return` but does not say what a program return means; a program has no caller or result. Deterministic and observable, and lets E04 emit a single exit path. Implemented by `close_block` and `set_ret`. | | | D127 | `return` with a value in a program is E151. A method that declares a result must end every path in `return`; an `if` satisfies this only when both branches do, a `while` never does, and a missing return is reported at `end` as E150. | Spec 7 permits `return` but does not say what a program return means; a program has no caller or result. Deterministic and observable, and lets E04 emit a single exit path. Implemented by `close_block` and `set_ret`. | | ||||
| | D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | | | D128 | The generated PRG container loads at `$1C01` with the same 12-byte BASIC 7 stub as the compiler and enters at `$1C0D`. Sections follow in ascending address order; gaps are zero-filled. `NAME.MAP` is CR-terminated text: `NOVA128 MAP 1`, `ENTRY hhhh`, then `SECTION name start size` in hex. Existing `NAME.PRG`/`NAME.MAP` is refused with E202 instead of overwritten; outputs created by the run are scratched on abort or failure. The higher generated-program map (banking, heap, software stack) remains E04-S02. | Gives E04-S01 a deterministic, testable container while preserving a previously successful build. Refusing an existing output is explicit; the user must erase it before recompiling. Implemented by `output.s`, `startup.s`, and `formats.inc`. | | ||||
| | D129 | Generated code executes with CR `$0E` and IRQs masked, then restores the entry CR. The software stack is bank 0 `$B800-$BFFF`, growing down from `$C000`; `SP`/`FP` use `$FB-$FE`. A frame stores the previous FP at `FP`, arguments from `FP+2`, and locals below `FP`; callers push arguments right-to-left and discard them after return. Results are A or A/X. `frame_enter` reserves the old FP and locals transactionally, refuses when the 6502 stack pointer is below `$40`, and returns C=1/`FRAME_ERR_STACK` with `SP`/`FP` restored on overflow. | Makes recursion and nested calls testable without consuming BASIC's zero page or the compiler's private stack. The $40 hardware-stack guard prevents a JAM before the software bound, and the transactional failure lets callers unwind cleanly. The 2 KB software stack, guard threshold, and mapping install are provisional until codegen and later runtime stories. Implemented by `frames.s` and `frames.inc`. | | |||||
| ## Deferred | ## Deferred | ||||
| @@ -5,8 +5,9 @@ Status: frozen for the compiler (D117, source cache D118); verified in VICE | |||||
| x128 3.10 | x128 3.10 | ||||
| Authoritative source: `cfg/compiler.cfg`. This document explains it. | Authoritative source: `cfg/compiler.cfg`. This document explains it. | ||||
| E04-S01 defines the generated PRG/MAP container below; the higher | |||||
| generated-program map (banking, heap, software stack) still belongs to E04-S02. | |||||
| E04-S01 defines the generated PRG/MAP container below; E04-S02 adds the | |||||
| provisional frame runtime. Heap placement and any future banking remain later | |||||
| stories. | |||||
| ## Generated Program Container (E04-S01) | ## Generated Program Container (E04-S01) | ||||
| @@ -21,9 +22,24 @@ generated-program map (banking, heap, software stack) still belongs to E04-S02. | |||||
| The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses | The fixed startup is copied from `src/runtime/startup.s`; `output.s` refuses | ||||
| existing `NAME.PRG`/`NAME.MAP` and scratches outputs it created on failure. | existing `NAME.PRG`/`NAME.MAP` and scratches outputs it created on failure. | ||||
| Test sections stay below `$4000` until E04-S02 freezes the runtime mapping. | |||||
| The generated program still uses the test mailbox below. | The generated program still uses the test mailbox below. | ||||
| ## Generated Program Frames (E04-S02) | |||||
| | Element | Value | | |||||
| |---|---| | |||||
| | Runtime mapping | CR `$0E`, IRQs masked, entry CR restored | | |||||
| | Software stack | bank 0 `$B800-$BFFF`, grows down from `$C000` | | |||||
| | `SP` / `FP` | `$FB-$FC` / `$FD-$FE` | | |||||
| | Frame | previous FP at `FP`; arguments from `FP+2`; locals below `FP` | | |||||
| | Caller | pushes arguments right-to-left, then discards them after return | | |||||
| | Overflow | `FRAME_ERR_STACK`, C=1, `SP`/`FP` restored | | |||||
| The frame fixture links code below `$B800` and runs with CR `$0E`; it restores | |||||
| the entry CR before printing and returning to BASIC. Code generation will | |||||
| install this mapping and call `frame_init` when it starts emitting user code | |||||
| (E04-S03). The 2 KB stack and `$40` hardware-stack guard are provisional. | |||||
| ## Mappings | ## Mappings | ||||
| | State | CR | RCR (low bits) | P0 | P1 | IRQs | | | State | CR | RCR (low bits) | P0 | P1 | IRQs | | ||||
| @@ -0,0 +1,102 @@ | |||||
| --- | |||||
| story: E04-S02 | |||||
| owner: e04_frames | |||||
| status: done | |||||
| started: 2026-11-09 | |||||
| depends-on: [E01-S05, E03-S04] | |||||
| --- | |||||
| # E04-S02 Claim | |||||
| ## Intended change | |||||
| Add the generated-program software frame and calling convention in | |||||
| `src/runtime/frames.s`. The frame stack grows downward from `$C000` over | |||||
| `$B800-$BFFF`; `SP` and `FP` use the free zero-page bytes `$FB-$FE`. A caller | |||||
| pushes arguments right-to-left, `JSR`s the callee, the callee enters a frame | |||||
| (old `FP`, then locals below `FP`), returns one byte in A or two in A/X, and | |||||
| the caller discards its arguments. `frame_enter` checks the stack bound and, | |||||
| on overflow, restores `SP`/`FP` and returns `FRAME_ERR_STACK` without | |||||
| corrupting the caller's frame. | |||||
| The x128 ABI fixture runs with CR `$0E` (RAM through `$BFFF`), exercises a | |||||
| nested recursive function to a bounded depth, a one-byte-return function, and | |||||
| a deliberate overflow that unwinds cleanly, then restores the entry mapping | |||||
| and reports through the mailbox. | |||||
| ## File area | |||||
| - `src/runtime/frames.s` (new), `src/common/frames.inc` (new) | |||||
| - `tests/codegen/frame-test.s` | |||||
| - `scripts/build-frame-test.ps1`, `scripts/verify-frame-test.ps1` | |||||
| - `cfg/compiler.cfg` (`COMMON` optional for runtime-only links) | |||||
| - `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md` | |||||
| - `planning/claims/E04-S02.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] Arguments and locals use the documented software frame: the recursive | |||||
| fixture copies its depth argument from `FP+2` into a local, uses locals below | |||||
| `FP`, and preserves a local sentinel across nested calls. | |||||
| - [x] Nested calls preserve required registers and frame state: 50 nested | |||||
| frames return the correct sum and restore `SP` to `$C000`; the one-byte | |||||
| helper also returns with `SP`/`FP` restored. | |||||
| - [x] One- and two-byte returns follow the ABI: `sum_bytes` returns 12 in A; | |||||
| `recurse(50)` returns 1275 ($04FB) in A/X. | |||||
| - [x] Stack overflow follows a stable runtime-fault path: the overflow run | |||||
| returns `FRAME_ERR_STACK`, restores `SP=$C000` and `FP=0`, and leaves no | |||||
| partial frame. A direct check lowers the 6502 stack pointer below `$40`, | |||||
| calls `frame_enter`, verifies the refusal, and restores the hardware stack. | |||||
| - [x] ABI tests include recursion to a bounded successful depth and overflow: | |||||
| depth 50 succeeds; depth 1000 fails through the guarded path. | |||||
| ## Coordination notes | |||||
| - E01-S05 and E03-S04 are `done`; E04-S01's container is complete. | |||||
| - D129 records the provisional frame layout, stack region, hardware guard, and | |||||
| mapping rule. | |||||
| - The test switches CR to `$0E` itself and restores the entry CR before | |||||
| returning to BASIC. Code generation will install the same mapping and call | |||||
| `frame_init` when it emits user code (E04-S03); this story provides the | |||||
| runtime and ABI evidence, not codegen integration. | |||||
| - `cfg/compiler.cfg` now marks `COMMON` optional so runtime-only fixtures can | |||||
| link without compiler startup; compiler builds still provide it. | |||||
| ## Work log | |||||
| - The first overflow run jammed: 2 KB of software frames allowed about 256 | |||||
| small frames, but the 6502 hardware stack overflowed near 128 `JSR` levels | |||||
| first. Added the `$40` hardware-stack guard and gave the overflow fixture | |||||
| 24-byte locals so the software bound is exercised before the hardware one. | |||||
| - The guard initially executed `tsx` before saving the caller's X, replacing | |||||
| the locals high byte with the stack pointer. Locals are now saved first. | |||||
| - The recursive sum initially returned 0 because each parent reused a global | |||||
| depth that deeper frames overwrote. The parent now reads its depth back from | |||||
| its frame local after the nested return. | |||||
| - A bounded-cycle screenshot confirmed the pre-fix hang was in the overflow | |||||
| recursion, not the mapping switch or output path. | |||||
| ## Completion | |||||
| - Tests run: | |||||
| - `scripts/build-frame-test.ps1` | |||||
| - `scripts/verify-frame-test.ps1 -Port 6530` | |||||
| - `scripts/verify-output-test.ps1 -Port 6530 -RunPort 6531` | |||||
| - `scripts/verify-diag-test.ps1 -Port 6530` | |||||
| - `scripts/verify-parser-test.ps1 -Port 6530` | |||||
| - `git diff --check` | |||||
| - Results: frame mailbox PASS with recursive=1275, one-byte=12, overflow=1, | |||||
| final SP=`$C000`, `frame_error`=1, final FP=0. The map switch and restore | |||||
| complete and the fixture prints/returns to BASIC. | |||||
| - Files changed: `src/runtime/frames.s`, `src/common/frames.inc`, | |||||
| `tests/codegen/frame-test.s`, `scripts/build-frame-test.ps1`, | |||||
| `scripts/verify-frame-test.ps1`, `cfg/compiler.cfg`, `docs/decisions.md`, | |||||
| `docs/architecture.md`, `docs/memory-map.md`, this claim. | |||||
| - Remaining risks: the 2 KB software stack and `$40` hardware-stack threshold | |||||
| are provisional; the 6502 stack still limits deep recursion even when frames | |||||
| are small; expression spills beyond the tested locals are not yet exercised; | |||||
| the frame stack occupies fixed bank 0 and is not banked; codegen must install | |||||
| CR `$0E`, call `frame_init`, and restore the entry CR; runtime fault codes | |||||
| are not yet mapped to the future R-code namespace (E04-S05); no ABI test | |||||
| covers four-byte interface results or `self` yet. | |||||
| @@ -0,0 +1,15 @@ | |||||
| # E04-S02: build the software-frame ABI fixture. | |||||
| [CmdletBinding()] | |||||
| param() | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| $out = Join-Path $project "build\frame-test" | |||||
| $link = Invoke-Nova128Link -Name "frame-test" -BuildDir $out ` | |||||
| -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( | |||||
| (Join-Path $project "src\runtime\frames.s"), | |||||
| (Join-Path $project "tests\codegen\frame-test.s")) | |||||
| Write-Host ("built {0}" -f $link.Program) | |||||
| @@ -0,0 +1,45 @@ | |||||
| # E04-S02: run the software-frame ABI fixture in x128 and check the mailbox. | |||||
| [CmdletBinding()] | |||||
| param( | |||||
| [int]$Port = 6530, | |||||
| [int]$TimeoutSeconds = 90 | |||||
| ) | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| & (Join-Path $PSScriptRoot "build-frame-test.ps1") | |||||
| $out = Join-Path $project "build\frame-test" | |||||
| $prg = Join-Path $out "frame-test.prg" | |||||
| $session = Start-Nova128X128 -Autostart $prg -Port $Port ` | |||||
| -Screenshot (Join-Path $out "frame-test-vicii.png") | |||||
| try { | |||||
| $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds | |||||
| } finally { | |||||
| Stop-Nova128X128 $session | |||||
| } | |||||
| Write-Host ('mailbox: ' + (($mb | ForEach-Object { $_.ToString('X2') }) -join ' ')) | |||||
| $sum = $mb[8] + 256 * $mb[9] | |||||
| $byteSum = $mb[10] | |||||
| $overflow = $mb[11] | |||||
| $sp = $mb[12] + 256 * $mb[13] | |||||
| $frameError = $mb[14] | |||||
| $fp = $mb[15] | |||||
| Write-Host ("results: recursive={0} one-byte={1} overflow={2} SP=${3:X4} error={4} FP=${5:X2}" -f ` | |||||
| $sum, $byteSum, $overflow, $sp, $frameError, $fp) | |||||
| $failures = @() | |||||
| if ($mb[2] -ne 0x03) { $failures += "producer $($mb[2]) is not a generated program" } | |||||
| if ($mb[3] -ne 0xA5) { $failures += "fixture status is not PASS" } | |||||
| if ($mb[4] -ne 0 -or $mb[5] -ne 0) { $failures += "code is $($mb[4] + 256 * $mb[5])" } | |||||
| if ($sum -ne 1275) { $failures += "recursive result is $sum, expected 1275" } | |||||
| if ($byteSum -ne 12) { $failures += "one-byte result is $byteSum, expected 12" } | |||||
| if ($overflow -ne 1) { $failures += "overflow result is $overflow, expected 1" } | |||||
| if ($sp -ne 0xC000) { $failures += ('SP after overflow is ${0:X4}, expected $C000' -f $sp) } | |||||
| if ($frameError -ne 1) { $failures += "frame error is $frameError, expected 1" } | |||||
| if ($fp -ne 0) { $failures += "FP after overflow is $fp, expected 0" } | |||||
| if ($failures) { throw ("frame verification failed:`n " + ($failures -join "`n ")) } | |||||
| Write-Host "verified 50-deep recursion, one-byte return, and transactional overflow unwind" | |||||
| @@ -0,0 +1,29 @@ | |||||
| ; frames.inc - generated-program software frame ABI (E04-S02). | |||||
| ; | |||||
| ; Frame stack: bank 0, $B800-$BFFF, growing down from $C000. Generated code | |||||
| ; runs with CR=$0E (all RAM through $BFFF) and IRQs masked; it restores the | |||||
| ; entry CR before returning to BASIC. SP/FP use the free zero-page bytes | |||||
| ; $FB-$FE, leaving the system zero page intact. | |||||
| ; | |||||
| ; Frame layout relative to FP: | |||||
| ; FP+0..1 previous FP (frame_enter) | |||||
| ; FP+2.. arguments, arg0 at the lowest address | |||||
| ; FP-1.. locals, allocated below FP | |||||
| ; | |||||
| ; Caller pushes arguments right-to-left, JSRs the callee, then discards the | |||||
| ; argument bytes. One-byte result: A. Two-byte result: A=low, X=high. | |||||
| .ifndef FRAMES_INC | |||||
| FRAMES_INC = 1 | |||||
| FRAME_SP = $FB | |||||
| FRAME_FP = $FD | |||||
| FRAME_STACK_TOP = $C000 | |||||
| FRAME_STACK_BASE = $B800 | |||||
| FRAME_HW_MIN = $40 ; refuse frames when the 6502 stack is this low | |||||
| FRAME_OK = 0 | |||||
| FRAME_ERR_STACK = 1 | |||||
| FRAME_ERR_STATE = 2 | |||||
| .endif | |||||
| @@ -0,0 +1,180 @@ | |||||
| ; frames.s - generated-program software frames (E04-S02). | |||||
| ; | |||||
| ; All routines run with CR=$0E and IRQs masked. FRAME_SP/FRAME_FP live at | |||||
| ; $FB-$FE; the frame stack occupies $B800-$BFFF and grows downward from | |||||
| ; $C000. See src/common/frames.inc for the layout and calling convention. | |||||
| ; | |||||
| ; frame_enter is transactional on failure: if the stack bound would be | |||||
| ; crossed, SP and FP are restored to their entry values and C=1 with | |||||
| ; frame_error=FRAME_ERR_STACK. No partial frame remains. | |||||
| .setcpu "6502" | |||||
| .include "../common/frames.inc" | |||||
| .export frame_init, frame_enter, frame_leave, frame_discard | |||||
| .export frame_push_byte, frame_push_word, frame_error | |||||
| .segment "CODE" | |||||
| ; Empty software stack: SP=$C000, FP=0, no error. | |||||
| frame_init: | |||||
| lda #<FRAME_STACK_TOP | |||||
| sta FRAME_SP | |||||
| lda #>FRAME_STACK_TOP | |||||
| sta FRAME_SP+1 | |||||
| lda #$00 | |||||
| sta FRAME_FP | |||||
| sta FRAME_FP+1 | |||||
| sta frame_error | |||||
| clc | |||||
| rts | |||||
| ; A/X = byte count (low/high). C=0 and SP updated on success; C=1 with | |||||
| ; FRAME_ERR_STACK and SP unchanged when the new SP would fall below the base. | |||||
| frame_reserve: | |||||
| sta frame_size | |||||
| stx frame_size+1 | |||||
| lda FRAME_SP | |||||
| sec | |||||
| sbc frame_size | |||||
| sta frame_new_sp | |||||
| lda FRAME_SP+1 | |||||
| sbc frame_size+1 | |||||
| sta frame_new_sp+1 | |||||
| lda frame_new_sp+1 | |||||
| cmp #>FRAME_STACK_BASE | |||||
| bcc @fail | |||||
| bne @ok | |||||
| lda frame_new_sp | |||||
| cmp #<FRAME_STACK_BASE | |||||
| bcc @fail | |||||
| @ok: lda frame_new_sp | |||||
| sta FRAME_SP | |||||
| lda frame_new_sp+1 | |||||
| sta FRAME_SP+1 | |||||
| clc | |||||
| rts | |||||
| @fail: lda #FRAME_ERR_STACK | |||||
| sta frame_error | |||||
| sec | |||||
| rts | |||||
| ; Push one argument byte (A). C=1 on stack overflow. | |||||
| frame_push_byte: | |||||
| sta frame_byte | |||||
| lda #1 | |||||
| ldx #0 | |||||
| jsr frame_reserve | |||||
| bcs @out | |||||
| ldy #0 | |||||
| lda frame_byte | |||||
| sta (FRAME_SP),y | |||||
| clc | |||||
| @out: rts | |||||
| ; Push one argument word: A=low, X=high. C=1 on stack overflow. | |||||
| frame_push_word: | |||||
| sta frame_word | |||||
| stx frame_word+1 | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_reserve | |||||
| bcs @out | |||||
| ldy #1 | |||||
| lda frame_word+1 | |||||
| sta (FRAME_SP),y | |||||
| dey | |||||
| lda frame_word | |||||
| sta (FRAME_SP),y | |||||
| clc | |||||
| @out: rts | |||||
| ; Enter a callee frame. Arguments are already on the stack above FP+2. | |||||
| ; A/X = local byte count (low/high) | |||||
| ; C=0: old FP pushed, FP set, locals reserved | |||||
| ; C=1: no frame was created; SP/FP are unchanged | |||||
| frame_enter: | |||||
| sta frame_locals | |||||
| stx frame_locals+1 | |||||
| tsx | |||||
| cpx #FRAME_HW_MIN | |||||
| bcs @hw_ok | |||||
| lda #FRAME_ERR_STACK | |||||
| sta frame_error | |||||
| sec | |||||
| rts | |||||
| @hw_ok: lda FRAME_SP | |||||
| sta frame_saved_sp | |||||
| lda FRAME_SP+1 | |||||
| sta frame_saved_sp+1 | |||||
| lda FRAME_FP | |||||
| sta frame_saved_fp | |||||
| lda FRAME_FP+1 | |||||
| sta frame_saved_fp+1 | |||||
| lda FRAME_FP | |||||
| ldx FRAME_FP+1 | |||||
| jsr frame_push_word | |||||
| bcs @out ; old-FP push overflow: nothing changed | |||||
| lda FRAME_SP | |||||
| sta FRAME_FP | |||||
| lda FRAME_SP+1 | |||||
| sta FRAME_FP+1 | |||||
| lda frame_locals | |||||
| ldx frame_locals+1 | |||||
| jsr frame_reserve | |||||
| bcc @ok | |||||
| lda frame_saved_sp ; locals overflow: undo the old FP push | |||||
| sta FRAME_SP | |||||
| lda frame_saved_sp+1 | |||||
| sta FRAME_SP+1 | |||||
| lda frame_saved_fp | |||||
| sta FRAME_FP | |||||
| lda frame_saved_fp+1 | |||||
| sta FRAME_FP+1 | |||||
| sec | |||||
| @out: rts | |||||
| @ok: clc | |||||
| rts | |||||
| ; Leave the current frame. SP=FP, pop the previous FP, and leave SP pointing | |||||
| ; at arg0. The caller still owns the argument bytes. | |||||
| frame_leave: | |||||
| lda FRAME_FP | |||||
| sta FRAME_SP | |||||
| lda FRAME_FP+1 | |||||
| sta FRAME_SP+1 | |||||
| ldy #0 | |||||
| lda (FRAME_SP),y | |||||
| sta FRAME_FP | |||||
| iny | |||||
| lda (FRAME_SP),y | |||||
| sta FRAME_FP+1 | |||||
| lda FRAME_SP | |||||
| clc | |||||
| adc #2 | |||||
| sta FRAME_SP | |||||
| bcc @done | |||||
| inc FRAME_SP+1 | |||||
| @done: rts | |||||
| ; Pop argument bytes after a return. A/X = byte count (low/high). | |||||
| frame_discard: | |||||
| clc | |||||
| adc FRAME_SP | |||||
| sta FRAME_SP | |||||
| txa | |||||
| adc FRAME_SP+1 | |||||
| sta FRAME_SP+1 | |||||
| rts | |||||
| .segment "BSS" | |||||
| frame_size: .res 2 | |||||
| frame_new_sp: .res 2 | |||||
| frame_byte: .res 1 | |||||
| frame_word: .res 2 | |||||
| frame_locals: .res 2 | |||||
| frame_saved_sp: .res 2 | |||||
| frame_saved_fp: .res 2 | |||||
| frame_error: .res 1 | |||||
| @@ -0,0 +1,399 @@ | |||||
| ; E04-S02: software-frame ABI test in x128. | |||||
| ; | |||||
| ; The fixture runs with CR=$0E and IRQs masked so the frame stack at | |||||
| ; $B800-$BFFF is RAM. It restores the entry CR before returning to BASIC. | |||||
| ; Checks: | |||||
| ; +0/+1 recursive two-byte result (sum 1..50 = 1275 = $04FB) | |||||
| ; +2 one-byte return (5+7 = 12) | |||||
| ; +3 overflow error (FRAME_ERR_STACK) | |||||
| ; +4/+5 SP after the overflow unwinds ($C000) | |||||
| ; +6 frame_error after overflow (FRAME_ERR_STACK) | |||||
| ; +7 FP low after unwinding (0) | |||||
| ; Failure bits: 1 recursion, 2 one-byte return, 4 overflow, 8 stack state. | |||||
| .setcpu "6502" | |||||
| .include "../../src/common/c128.inc" | |||||
| .include "../../src/common/mailbox.inc" | |||||
| .include "../../src/common/frames.inc" | |||||
| .import frame_init, frame_enter, frame_leave, frame_discard | |||||
| .import frame_push_byte, frame_push_word, frame_error | |||||
| .segment "LOADADDR" | |||||
| .word $1C01 | |||||
| .segment "EXEHDR" | |||||
| .word basic_end | |||||
| .word 10 | |||||
| .byte $9E | |||||
| .byte "7181" | |||||
| .byte $00 | |||||
| basic_end: | |||||
| .word $0000 | |||||
| .segment "MAILBOX" | |||||
| .res MB_SIZE | |||||
| .segment "STARTUP" | |||||
| .export test_entry | |||||
| test_entry: | |||||
| php | |||||
| sei | |||||
| ldx #MB_SIZE - 1 | |||||
| lda #$00 | |||||
| @clr: sta MB_BASE,x | |||||
| dex | |||||
| bpl @clr | |||||
| lda #MB_MAGIC0_VALUE | |||||
| sta MB_MAGIC0 | |||||
| lda #MB_MAGIC1_VALUE | |||||
| sta MB_MAGIC1 | |||||
| lda #MB_PROD_PROGRAM | |||||
| sta MB_PRODUCER | |||||
| lda #MB_ST_RUNNING | |||||
| sta MB_STATUS | |||||
| lda #$00 | |||||
| sta fail_mask | |||||
| sta sum_lo | |||||
| sta sum_hi | |||||
| sta byte_sum | |||||
| sta ov_error | |||||
| sta MB_CODE | |||||
| sta MB_CODE+1 | |||||
| jsr frame_init | |||||
| lda MMU_CR | |||||
| sta saved_cr | |||||
| lda #CR_RAM0_KERNAL | |||||
| sta MMU_CR | |||||
| ; ------------------------------------------------ successful recursion | |||||
| jsr frame_init | |||||
| lda #<50 | |||||
| ldx #>50 | |||||
| jsr frame_push_word | |||||
| bcs @rec_push_fail | |||||
| jsr recurse | |||||
| bcs @rec_call_fail | |||||
| sta sum_lo | |||||
| stx sum_hi | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| lda sum_lo | |||||
| cmp #$FB | |||||
| bne @rec_bad | |||||
| lda sum_hi | |||||
| cmp #$04 | |||||
| bne @rec_bad | |||||
| lda FRAME_SP | |||||
| cmp #<FRAME_STACK_TOP | |||||
| bne @rec_bad | |||||
| lda FRAME_SP+1 | |||||
| cmp #>FRAME_STACK_TOP | |||||
| bne @rec_bad | |||||
| jmp @byte_return | |||||
| @rec_push_fail: | |||||
| lda #$01 | |||||
| jsr fail | |||||
| jmp @byte_return | |||||
| @rec_call_fail: | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| lda #$01 | |||||
| jsr fail | |||||
| jmp @byte_return | |||||
| @rec_bad: | |||||
| lda #$01 | |||||
| jsr fail | |||||
| ; --------------------------------------------------- one-byte return | |||||
| @byte_return: | |||||
| jsr frame_init | |||||
| lda #7 | |||||
| jsr frame_push_byte | |||||
| bcs @byte_fail | |||||
| lda #5 | |||||
| jsr frame_push_byte | |||||
| bcs @byte_fail | |||||
| jsr sum_bytes | |||||
| bcs @byte_fail | |||||
| sta byte_sum | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| lda byte_sum | |||||
| cmp #12 | |||||
| beq @overflow | |||||
| @byte_fail: | |||||
| lda #$02 | |||||
| jsr fail | |||||
| ; ------------------------------------------------------ stack overflow | |||||
| @overflow: | |||||
| jsr frame_init | |||||
| lda #<1000 | |||||
| ldx #>1000 | |||||
| jsr frame_push_word | |||||
| bcs @ov_push_fail | |||||
| jsr recurse | |||||
| bcc @ov_no_fault | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| lda frame_error | |||||
| sta ov_error | |||||
| cmp #FRAME_ERR_STACK | |||||
| beq @ov_check_sp | |||||
| lda #$04 | |||||
| jsr fail | |||||
| jmp @ov_check_sp | |||||
| @ov_push_fail: | |||||
| lda #$04 | |||||
| jsr fail | |||||
| jmp @finish | |||||
| @ov_no_fault: | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| lda #$04 | |||||
| jsr fail | |||||
| @ov_check_sp: | |||||
| lda FRAME_SP | |||||
| cmp #<FRAME_STACK_TOP | |||||
| bne @ov_bad | |||||
| lda FRAME_SP+1 | |||||
| cmp #>FRAME_STACK_TOP | |||||
| bne @ov_bad | |||||
| lda FRAME_FP | |||||
| ora FRAME_FP+1 | |||||
| beq @finish | |||||
| @ov_bad: | |||||
| lda #$08 | |||||
| jsr fail | |||||
| ; ---------------------------------------------- hardware-stack guard | |||||
| tsx | |||||
| stx saved_hw_sp | |||||
| ldx #$20 | |||||
| txs | |||||
| lda #0 | |||||
| ldx #0 | |||||
| jsr frame_enter | |||||
| ldx saved_hw_sp | |||||
| txs | |||||
| bcc @hw_bad | |||||
| lda frame_error | |||||
| cmp #FRAME_ERR_STACK | |||||
| beq @finish | |||||
| @hw_bad: | |||||
| lda #$10 | |||||
| jsr fail | |||||
| ; ---------------------------------------------------------- report | |||||
| @finish: | |||||
| lda sum_lo | |||||
| sta MB_DATA+0 | |||||
| lda sum_hi | |||||
| sta MB_DATA+1 | |||||
| lda byte_sum | |||||
| sta MB_DATA+2 | |||||
| lda ov_error | |||||
| sta MB_DATA+3 | |||||
| lda FRAME_SP | |||||
| sta MB_DATA+4 | |||||
| lda FRAME_SP+1 | |||||
| sta MB_DATA+5 | |||||
| lda frame_error | |||||
| sta MB_DATA+6 | |||||
| lda FRAME_FP | |||||
| sta MB_DATA+7 | |||||
| lda saved_cr | |||||
| sta MMU_CR | |||||
| lda fail_mask | |||||
| sta MB_CODE | |||||
| lda #$00 | |||||
| sta MB_CODE+1 | |||||
| lda #MB_ST_FAIL | |||||
| ldx fail_mask | |||||
| bne @status | |||||
| lda #MB_ST_PASS | |||||
| @status: | |||||
| sta MB_STATUS | |||||
| ldy #$00 | |||||
| @print: lda message,y | |||||
| beq @end | |||||
| sty print_y | |||||
| jsr CHROUT | |||||
| ldy print_y | |||||
| iny | |||||
| bne @print | |||||
| @end: plp | |||||
| rts | |||||
| .segment "CODE" | |||||
| fail: | |||||
| ora fail_mask | |||||
| sta fail_mask | |||||
| rts | |||||
| ; recurse(depth) -> depth + recurse(depth-1), zero at depth 0. | |||||
| ; Arguments: depth word at FP+2. Locals: depth word, result word. | |||||
| recurse: | |||||
| lda #24 | |||||
| ldx #0 | |||||
| jsr frame_enter | |||||
| bcc @entered | |||||
| jmp @entry_err | |||||
| @entered: | |||||
| ; local +0/+1 = depth, local +2/+3 = sentinel/result | |||||
| ldy #2 | |||||
| lda (FRAME_FP),y | |||||
| ldy #0 | |||||
| sta (FRAME_SP),y | |||||
| ldy #3 | |||||
| lda (FRAME_FP),y | |||||
| ldy #1 | |||||
| sta (FRAME_SP),y | |||||
| ldy #2 | |||||
| lda #$A5 | |||||
| sta (FRAME_SP),y | |||||
| iny | |||||
| lda #$5A | |||||
| sta (FRAME_SP),y | |||||
| ldy #0 | |||||
| lda (FRAME_SP),y | |||||
| sta depth | |||||
| iny | |||||
| lda (FRAME_SP),y | |||||
| sta depth+1 | |||||
| lda depth | |||||
| ora depth+1 | |||||
| beq @zero | |||||
| lda depth | |||||
| sec | |||||
| sbc #1 | |||||
| sta calc | |||||
| lda depth+1 | |||||
| sbc #0 | |||||
| sta calc+1 | |||||
| lda calc | |||||
| ldx calc+1 | |||||
| jsr frame_push_word | |||||
| bcs @push_err | |||||
| jsr recurse | |||||
| bcs @nested_err | |||||
| sta result | |||||
| stx result+1 | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| ldy #2 | |||||
| lda (FRAME_SP),y | |||||
| cmp #$A5 | |||||
| bne @corrupt | |||||
| iny | |||||
| lda (FRAME_SP),y | |||||
| cmp #$5A | |||||
| bne @corrupt | |||||
| ldy #0 | |||||
| lda (FRAME_SP),y | |||||
| clc | |||||
| adc result | |||||
| sta result | |||||
| ldy #1 | |||||
| lda (FRAME_SP),y | |||||
| adc result+1 | |||||
| sta result+1 | |||||
| ldy #2 | |||||
| lda result | |||||
| sta (FRAME_SP),y | |||||
| ldy #3 | |||||
| lda result+1 | |||||
| sta (FRAME_SP),y | |||||
| lda result | |||||
| ldx result+1 | |||||
| jmp @leave | |||||
| @zero: lda #0 | |||||
| ldx #0 | |||||
| @leave: | |||||
| sta ret_lo | |||||
| stx ret_hi | |||||
| jsr frame_leave | |||||
| lda ret_lo | |||||
| ldx ret_hi | |||||
| clc | |||||
| rts | |||||
| @entry_err: | |||||
| lda #FRAME_ERR_STACK | |||||
| sec | |||||
| rts | |||||
| @push_err: | |||||
| jsr frame_leave | |||||
| lda #FRAME_ERR_STACK | |||||
| sec | |||||
| rts | |||||
| @nested_err: | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| jsr frame_leave | |||||
| lda #FRAME_ERR_STACK | |||||
| sec | |||||
| rts | |||||
| @corrupt: | |||||
| lda #2 | |||||
| ldx #0 | |||||
| jsr frame_discard | |||||
| jsr frame_leave | |||||
| lda #FRAME_ERR_STATE | |||||
| sec | |||||
| rts | |||||
| ; sum_bytes(a,b) -> A = a+b. Two byte arguments at FP+2 and FP+3. | |||||
| sum_bytes: | |||||
| lda #0 | |||||
| ldx #0 | |||||
| jsr frame_enter | |||||
| bcs @out | |||||
| ldy #2 | |||||
| lda (FRAME_FP),y | |||||
| iny | |||||
| clc | |||||
| adc (FRAME_FP),y | |||||
| sta sum_result | |||||
| jsr frame_leave | |||||
| lda sum_result | |||||
| clc | |||||
| rts | |||||
| @out: sec | |||||
| rts | |||||
| .segment "RODATA" | |||||
| message: .byte "NOVA128 FRAME TEST DONE", $0D, $00 | |||||
| .segment "BSS" | |||||
| fail_mask: .res 1 | |||||
| print_y: .res 1 | |||||
| saved_cr: .res 1 | |||||
| saved_hw_sp: .res 1 | |||||
| sum_lo: .res 1 | |||||
| sum_hi: .res 1 | |||||
| byte_sum: .res 1 | |||||
| ov_error: .res 1 | |||||
| depth: .res 2 | |||||
| calc: .res 2 | |||||
| result: .res 2 | |||||
| ret_lo: .res 1 | |||||
| ret_hi: .res 1 | |||||
| sum_result: .res 1 | |||||
Powered by TurnKey Linux.