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 | |||
| # 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. | |||
| 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; | |||
| RODATA: load = MAIN, type = ro, 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`. | |||
| - Sections are contiguous and ascending; gaps are zero-filled. | |||
| - `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 | |||
| @@ -326,15 +328,34 @@ memory. | |||
| 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 | |||
| @@ -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`. | | |||
| | 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`. | | |||
| | 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 | |||
| @@ -5,8 +5,9 @@ Status: frozen for the compiler (D117, source cache D118); verified in VICE | |||
| x128 3.10 | |||
| 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) | |||
| @@ -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 | |||
| 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. | |||
| ## 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 | |||
| | 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.