Generated methods can now store and load fields through a zero-page receiver, pass self to direct JSR calls, and keep byte or word results across argument cleanup. The backlog gains the parser-to-codegen and pass-3 address assignment stories the next backend work needs. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>main
| @@ -515,6 +515,40 @@ multiple statements per program, and address assignment for generated code | |||||
| remain open. Pass-2 code growth also made some parser branches exceed ca65's | remain open. Pass-2 code growth also made some parser branches exceed ca65's | ||||
| +/-128 range, so the literal path uses a near branch and `JMP` trampoline. | +/-128 range, so the literal path uses a near branch and `JMP` trampoline. | ||||
| ### 7.10 Member emission (E05-S04) | |||||
| `src/compiler/members.s` emits the member half of a method body above the | |||||
| scalar emitter and the flow helpers. It takes field offsets from the E03-S05 | |||||
| layout unchanged and assumes the D129 call ABI, where a method receives self | |||||
| in A/X. | |||||
| A generated method body begins with `mb_self_store`, which records the | |||||
| incoming self in zero-page `MB_SELF` (`$F9-$FA`). `self.field` then needs one | |||||
| indexed-indirect access: `mb_ld_field` and `mb_st_field` emit `LDY #offset` | |||||
| and `LDA (MB_SELF),Y` or `STA (MB_SELF),Y`, adding the +1 byte for a word | |||||
| field. A word store moves the high byte out of X with `TXA` before storing, | |||||
| because the 6502 has no `STX (zp),Y`. | |||||
| `mb_point` loads a two-byte object reference into `MB_SELF`, so the same | |||||
| emission serves `x.field` and a call on a receiver other than self. A concrete | |||||
| call is `mb_self_load` (A/X = self), `JSR target`, then `frame_discard` for the | |||||
| argument bytes: the callee enters its normal frame and reads arguments at | |||||
| `FP+2`, and a byte or word result survives the discard. That is the direct-JSR | |||||
| path D107 keeps for concrete methods; interface dispatch remains E06. | |||||
| `codegen.s` gained the forms this needs: `cg_ldy_imm`, `cg_lda_zp`, | |||||
| `cg_ldx_zp`, `cg_sta_zp`, `cg_stx_zp`, and `cg_lda_zpy`, `cg_ldx_zpy`, | |||||
| `cg_sta_zpy`. `MB_SELF` sits directly below the frame ABI and `members.s` | |||||
| asserts at link time that it does not overlap `FRAME_SP` or `FRAME_FP`. | |||||
| The fixture generates a field writer and byte and word readers, runs them | |||||
| against two objects of one layout, then generates callers that push arguments | |||||
| and call real methods. It checks field offsets, independent state, the | |||||
| argument discard, and a word result at the call site. Private member access is | |||||
| rejected before code generation: the parser reports `ACC_PRIVATE_MEMBER` | |||||
| (E120) for a private field or method reached outside its class, so this layer | |||||
| never sees one. | |||||
| ## 8. Heap and Ownership | ## 8. Heap and Ownership | ||||
| - The generated-program heap initially resides in the same RAM bank as program | - The generated-program heap initially resides in the same RAM bank as program | ||||
| @@ -62,6 +62,7 @@ documented default. `Proposed` means a later change still needs approval. | |||||
| | D133 | The E05-S01 heap uses four-byte headers: a two-byte total size with bit 15 set while allocated, and a two-byte next pointer while free or `$A55A` while allocated. Payload follows at header+4, blocks are two-byte aligned, and the minimum block is six bytes. `heap_init` takes A/X = page-aligned base and Y = pages 1..127; `heap_alloc` takes payload bytes, zeroes the payload, returns it or `HEAP_ERR_OOM`, and splits first-fit free blocks; `heap_free` validates range/size/magic and coalesces in address order. Validation is always on. The E05-S01 fixture uses bank-0 `$4000-$4FFF` under CR `$0E`; generated-program placement and banked heaps remain open. | Makes allocation deterministic and testable before class layout chooses a production heap base. The allocator does not print or report faults itself; E05-S03 maps `HEAP_ERR_OOM` to R005 when `new` is wired. The 15-bit size limit and page-granular region avoid a separate size argument while keeping the block header small. Implemented by `heap.s`, `heap.inc`, and the heap fixture. | | | D133 | The E05-S01 heap uses four-byte headers: a two-byte total size with bit 15 set while allocated, and a two-byte next pointer while free or `$A55A` while allocated. Payload follows at header+4, blocks are two-byte aligned, and the minimum block is six bytes. `heap_init` takes A/X = page-aligned base and Y = pages 1..127; `heap_alloc` takes payload bytes, zeroes the payload, returns it or `HEAP_ERR_OOM`, and splits first-fit free blocks; `heap_free` validates range/size/magic and coalesces in address order. Validation is always on. The E05-S01 fixture uses bank-0 `$4000-$4FFF` under CR `$0E`; generated-program placement and banked heaps remain open. | Makes allocation deterministic and testable before class layout chooses a production heap base. The allocator does not print or report faults itself; E05-S03 maps `HEAP_ERR_OOM` to R005 when `new` is wired. The 15-bit size limit and page-granular region avoid a separate size argument while keeping the block header small. Implemented by `heap.s`, `heap.inc`, and the heap fixture. | | ||||
| | D135 | E05-S03 construction and deletion live in `src/runtime/construct.s` above the heap and descriptor layers. `obj_new` takes A/X = descriptor, reads the instance size (zero is `OBJ_ERR_BAD`), allocates it, writes the descriptor pointer at object+0, then calls the caller-selected `obj_init_ptr` entry once with A/X = self and the constructor arguments still on the software frame stack at `FP+2`; the caller discards them with `frame_discard`. Allocation failure returns C=1 with `OBJ_ERR_HEAP` and never calls `init`. `obj_delete` takes A/X = object: null is a successful no-op, otherwise the descriptor's optional destructor runs once with A/X = self and the complete block goes back through `heap_free`. Both calls use `obj_call`, which takes its entry from zero-page `co_vec`, preserves A/X for `self`, pushes one synthetic return address, and ends with `JMP (co_vec)`; `co_vec` must not straddle a page. | Gives generated `new`/`delete` a testable runtime contract without freezing heap placement or the E06 dispatch. Carrying the entry in `co_vec` instead of A/X is what lets the callee still receive `self` in A/X, and the same shape serves method-table dispatch later. `construct.s` reports `OBJ_ERR_HEAP` rather than printing; codegen still maps that to R005. Implemented by `construct.s`, the `OBJ_*` codes in `class.inc`, and the object fixture. | | | D135 | E05-S03 construction and deletion live in `src/runtime/construct.s` above the heap and descriptor layers. `obj_new` takes A/X = descriptor, reads the instance size (zero is `OBJ_ERR_BAD`), allocates it, writes the descriptor pointer at object+0, then calls the caller-selected `obj_init_ptr` entry once with A/X = self and the constructor arguments still on the software frame stack at `FP+2`; the caller discards them with `frame_discard`. Allocation failure returns C=1 with `OBJ_ERR_HEAP` and never calls `init`. `obj_delete` takes A/X = object: null is a successful no-op, otherwise the descriptor's optional destructor runs once with A/X = self and the complete block goes back through `heap_free`. Both calls use `obj_call`, which takes its entry from zero-page `co_vec`, preserves A/X for `self`, pushes one synthetic return address, and ends with `JMP (co_vec)`; `co_vec` must not straddle a page. | Gives generated `new`/`delete` a testable runtime contract without freezing heap placement or the E06 dispatch. Carrying the entry in `co_vec` instead of A/X is what lets the callee still receive `self` in A/X, and the same shape serves method-table dispatch later. `construct.s` reports `OBJ_ERR_HEAP` rather than printing; codegen still maps that to R005. Implemented by `construct.s`, the `OBJ_*` codes in `class.inc`, and the object fixture. | | ||||
| | D134 | E05-S02 class descriptors are 8-byte packed records: 2-byte instance size (including the object header), 2-byte destructor address (0 = none), 1-byte interface count, 2-byte interface-directory pointer (0 when empty), and a reserved flags byte that must be 0. There is no parent-class slot. Objects begin with a 2-byte descriptor pointer and fields follow at +2. Interface-directory entries are 4 bytes: interface-descriptor pointer and method-table pointer. `objects.s` provides read-only accessors; `output.s` writes `CLASS name size destroy count ifdir` records after sections. | Makes object identity, size/deletion, and the future interface path deterministic before construction/dispatch. Flags are reserved for non-inheritance metadata; interface descriptor contents and generated `new` remain E05-S03/E06. Implemented by `class.inc`, `objects.s`, and the class fixture. | | | D134 | E05-S02 class descriptors are 8-byte packed records: 2-byte instance size (including the object header), 2-byte destructor address (0 = none), 1-byte interface count, 2-byte interface-directory pointer (0 when empty), and a reserved flags byte that must be 0. There is no parent-class slot. Objects begin with a 2-byte descriptor pointer and fields follow at +2. Interface-directory entries are 4 bytes: interface-descriptor pointer and method-table pointer. `objects.s` provides read-only accessors; `output.s` writes `CLASS name size destroy count ifdir` records after sections. | Makes object identity, size/deletion, and the future interface path deterministic before construction/dispatch. Flags are reserved for non-inheritance metadata; interface descriptor contents and generated `new` remain E05-S03/E06. Implemented by `class.inc`, `objects.s`, and the class fixture. | | ||||
| | D136 | E05-S04 member emission lives in `src/compiler/members.s`. A generated method body starts with `mb_self_store`, recording the incoming self in zero-page `MB_SELF` (`$F9-$FA`). `self.field` emits `LDY #offset` plus `LDA (MB_SELF),Y` or `STA (MB_SELF),Y`, with a second byte for word fields; the word store moves the high byte with `TXA` because the 6502 has no `STX (zp),Y`. `mb_point` loads any two-byte object reference into `MB_SELF` for a field or call on another receiver. A concrete call emits `mb_self_load`, `JSR target`, then `frame_discard` for the argument bytes, preserving a byte or word result. `codegen.s` gains `cg_ldy_imm`, the zero-page forms, and the indexed-indirect forms. | Gives generated methods the member half of the D129 ABI without freezing parser integration or interface dispatch. One receiver pointer keeps field access at four bytes per byte field and stays clear of `$FB-$FE` (link-time assertion). Private access never reaches this layer: the parser reports `ACC_PRIVATE_MEMBER` (E120) first. Implemented by `members.s`, `members.inc`, `codegen.s`, and the member fixture. | | |||||
| ## Deferred | ## Deferred | ||||
| @@ -42,6 +42,21 @@ 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 | 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. | (E04-S03). The 2 KB stack and `$40` hardware-stack guard are provisional. | ||||
| ## Member Access (E05-S04) | |||||
| | Element | Value | | |||||
| |---|---| | |||||
| | Receiver pointer | `MB_SELF` = `$F9-$FA` in the generated program | | |||||
| | Field access | `LDY #offset` + `LDA/STA (MB_SELF),Y`; word fields add +1 byte | | |||||
| | Method entry | self in A/X, stored to `MB_SELF` by the body prologue | | |||||
| | Concrete call | `JSR` to a fixed address, then `frame_discard` for the arguments | | |||||
| | Fixture code | below `$4000` | | |||||
| `MB_SELF` occupies the two bytes directly below the frame ABI at `$FB-$FE`; | |||||
| `src/compiler/members.s` asserts at link time that the pair overlaps neither | |||||
| `FRAME_SP` nor `FRAME_FP`. Field offsets are the parser's layout offsets, with | |||||
| the first field at +2, so generated code needs no runtime translation. | |||||
| ## Heap Fixture (E05-S01) | ## Heap Fixture (E05-S01) | ||||
| | Element | Value | | | Element | Value | | ||||
| @@ -0,0 +1,107 @@ | |||||
| --- | |||||
| story: E05-S04 | |||||
| owner: e05_members | |||||
| status: done | |||||
| started: 2026-11-09 | |||||
| depends-on: [E05-S03, E03-S05] | |||||
| --- | |||||
| # E05-S04 Claim | |||||
| ## Intended change | |||||
| Add member emission above the scalar emitter and control-flow helpers: | |||||
| `src/compiler/members.s` records the incoming self in zero-page `MB_SELF` | |||||
| (`$F9-$FA`), loads and stores fields at their E03-S05 offsets through | |||||
| `LDA/STA (MB_SELF),Y`, points `MB_SELF` at any two-byte object reference, and | |||||
| emits a direct concrete call as self load, `JSR`, then `frame_discard` for the | |||||
| argument bytes. `codegen.s` gains the zero-page and indexed-indirect forms this | |||||
| needs. | |||||
| The story is limited to emission and its ABI: parser-to-codegen integration, | |||||
| `counter.n8` as a real compile, and interface dispatch remain open. | |||||
| ## File area | |||||
| - `src/compiler/members.s`, `src/common/members.inc` (new) | |||||
| - `src/compiler/codegen.s` (`cg_ldy_imm`, `cg_lda_zp`, `cg_ldx_zp`, `cg_sta_zp`, | |||||
| `cg_stx_zp`, `cg_lda_zpy`, `cg_sta_zpy`) | |||||
| - `tests/codegen/member-test.s` | |||||
| - `scripts/build-member-test.ps1`, `scripts/verify-member-test.ps1` | |||||
| - `docs/decisions.md` (D136), `docs/architecture.md` (7.10), | |||||
| `docs/memory-map.md` (Member Access) | |||||
| - `planning/claims/E05-S04.md` | |||||
| - generated files under `build/` | |||||
| ## Acceptance evidence | |||||
| - [x] `self.field` loads/stores correct offsets: the fixture generates a | |||||
| writer that stores `self.count` (offset 2) and `self.total` (offset 3..4) | |||||
| and byte/word readers, runs them on two objects, and checks the raw bytes at | |||||
| +2, +3, and +4 rather than only the returned values. | |||||
| - [x] Concrete calls use direct `JSR` and preserve the caller ABI: generated | |||||
| callers push a word and a byte argument, call a real method with self in A/X, | |||||
| and the callee reads the arguments at `FP+2..4` and stores them into the | |||||
| receiver's fields. `FRAME_SP` is identical before the pushes and after the | |||||
| emitted discard, so the caller cleaned up exactly three argument bytes. | |||||
| - [x] Private access is rejected at compile time: `tests/parser/private.n8` | |||||
| still matches `private.expect` exactly, including the `ACC_PRIVATE_MEMBER` | |||||
| (E120) rows for private fields and methods reached outside their class, so | |||||
| no private member reaches `members.s`. | |||||
| - [x] Two objects of one class retain independent state: after writing | |||||
| `count=3`, `total=$1234` to object A and `count=7`, `total=$5678` to object | |||||
| B, the emitted readers return 3/`$1234` and 7/`$5678`, and object A's bytes | |||||
| are unchanged by the writes to B. | |||||
| - [ ] `counter.n8` compiles and runs with the expected result: deferred. There | |||||
| is still no parser-to-codegen path (D131, E04-S04) and no pass-3 address | |||||
| assignment, so no source file compiles end to end. The fixture proves the | |||||
| emitted member and call shapes the parser will need; wiring them is the | |||||
| backend story that follows `sizing.s`. | |||||
| ## Work log | |||||
| - Selected E05-S04 after E05-S03; no other claim was active. | |||||
| - Added the zero-page and indexed-indirect emitter forms, then `members.s` | |||||
| with `mb_self_store`, `mb_self_load`, `mb_point`, `mb_ld_field`, | |||||
| `mb_st_field`, and `mb_call`/`mb_call_byte`/`mb_call_word`. `MB_SELF` is | |||||
| `$F9-$FA`, directly below the frame ABI, asserted at link time. | |||||
| - First run failed the field checks: word loads emitted `LDX (zp),Y`, which | |||||
| the 6502 does not have (`$B6` is `LDX zp,Y`, so it read zero page instead of | |||||
| the object). Word loads now use `PHA`, `LDA (MB_SELF),Y` for the +1 byte, | |||||
| `TAX`, then `PLA`; the invalid `cg_ldx_zpy` wrapper was removed so no later | |||||
| story can emit it by mistake. | |||||
| - The fixture also needed `set_ptr` in the ZEROPAGE segment: ca65 rejects | |||||
| `(zp),Y` for a 16-bit BSS symbol. | |||||
| - Two fixture-side defects were found while tracing the call path: `run_plain` | |||||
| was called without setting A to the code offset (it jumped into the middle of | |||||
| the snippet), and a temporary trace wrote A before `setter` saved self. Both | |||||
| were fixture bugs, not layer bugs; the traces were removed before the final | |||||
| runs. | |||||
| - Long `bne`/`bcs` paths use the project's branch-to-`JMP` macro form. | |||||
| ## Completion | |||||
| - Tests run: | |||||
| - `scripts/verify-member-test.ps1` | |||||
| - `scripts/verify-control-test.ps1` | |||||
| - `scripts/verify-scalar-test.ps1` | |||||
| - `scripts/verify-parser-test.ps1` | |||||
| - `git diff --check` | |||||
| - Results: the member fixture reports producer 3, status `$A5`, code 0, all | |||||
| four flags set, result `$5678`, and shows `MEMBER OK`; the PRG ends `$2B9D`. | |||||
| The control fixture still passes (`11 05 0A 04 5A 00 00 03`), the scalar | |||||
| golden stream is unchanged at 231 bytes, and all 41 parser fixtures pass, | |||||
| including the exact `private.n8` expectations. | |||||
| - Files changed: `src/compiler/members.s`, `src/common/members.inc`, | |||||
| `src/compiler/codegen.s`, `tests/codegen/member-test.s`, | |||||
| `scripts/build-member-test.ps1`, `scripts/verify-member-test.ps1`, | |||||
| `docs/decisions.md`, `docs/architecture.md`, `docs/memory-map.md`, this | |||||
| claim. | |||||
| - Remaining risks: no parser-to-codegen integration, so `counter.n8` cannot | |||||
| compile yet. Field access handles scalar fields only; arrays and interface | |||||
| references are E05-S05/E06. A byte field costs four bytes per access and a | |||||
| word eight, because only one zero-page receiver is cached. `MB_SELF` is | |||||
| fixed for the whole generated program, so nested expressions that need two | |||||
| receivers at once will need a spill rule when the parser drives this layer. | |||||
| The fixture runs in x128 against a fresh true-drive D81; physical C128 | |||||
| testing remains P2 validation. | |||||
| @@ -372,6 +372,44 @@ Primary files: `sizing.s`, `codegen.s`, `output.s`, `src/runtime/startup.s`, | |||||
| - Runtime messages do not corrupt the program's memory mapping. | - Runtime messages do not corrupt the program's memory mapping. | ||||
| - `hello.n8` compiles and runs end to end in x128. | - `hello.n8` compiles and runs end to end in x128. | ||||
| ### E04-S06 — Parser-to-codegen integration (P1) | |||||
| **Depends on:** E04-S04, E04-S05, E05-S04, E03-S04 | |||||
| **Acceptance criteria:** | |||||
| - Statements and expressions emit code through `codegen.s`/`flow.s` instead of | |||||
| being type-checked only: assignment, calls, `if`/`else`, `while`, `for`, | |||||
| `return`, and `print`. | |||||
| - Evaluation order and temporaries follow the D129 frame ABI; results arrive in | |||||
| A (byte) or A/X (word), and arguments are pushed right-to-left and discarded | |||||
| by the caller. | |||||
| - `self.field` and concrete method calls emit through `members.s` (D136), and a | |||||
| method body stores its incoming self before any other statement. | |||||
| - Emitter failures (buffer full, label or fixup exhaustion) become diagnostics; | |||||
| a failed emit leaves no partial output. | |||||
| - A source program using statements, expressions, and one concrete call | |||||
| generates a routine that runs in x128 and produces the expected mailbox | |||||
| signature. | |||||
| ### E04-S07 — Sizing pass and pass-3 address assignment (P1) | |||||
| **Depends on:** E04-S06 | |||||
| **Acceptance criteria:** | |||||
| - `sizing.s` measures each routine in count mode so pass 2 and pass 3 produce | |||||
| identical lengths and identical label offsets, with a mismatch counter of 0. | |||||
| - Pass 3 patches every label, branch, `JSR`, and section address; the MAP | |||||
| `ENTRY` and `SECTION` records carry the final addresses, including class | |||||
| descriptors and method entries. | |||||
| - Forward references to methods and classes declared later resolve; an | |||||
| unresolved reference is a single diagnostic rather than a bad operand. | |||||
| - Recompiling the same source produces a byte-identical PRG, and the existing | |||||
| overwrite refusal (E202) still holds. | |||||
| - `counter.n8` compiles from source and runs in x128 with the expected result, | |||||
| closing the end-to-end criteria deferred by E05-S04 and E04-S05. | |||||
| --- | --- | ||||
| ## E05 — Classes, Heap, and Concrete Methods | ## E05 — Classes, Heap, and Concrete Methods | ||||
| @@ -0,0 +1,27 @@ | |||||
| # E05-S04: build the field/self/concrete-call emitter fixture and its disk. | |||||
| [CmdletBinding()] | |||||
| param() | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| $out = Join-Path $project "build\member-test" | |||||
| New-Item -ItemType Directory -Force -Path $out | Out-Null | |||||
| $link = Invoke-Nova128Link -Name "member-test" -BuildDir $out ` | |||||
| -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( | |||||
| (Join-Path $project "src\compiler\codegen.s"), | |||||
| (Join-Path $project "src\compiler\flow.s"), | |||||
| (Join-Path $project "src\compiler\members.s"), | |||||
| (Join-Path $project "src\runtime\frames.s"), | |||||
| (Join-Path $project "src\runtime\print.s"), | |||||
| (Join-Path $project "tests\codegen\member-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 ("member-test ends at `$" + $end.ToString("X4") + "; it must stay below `$4000") | |||||
| } | |||||
| New-Nova128Disk -Path (Join-Path $out "member-test.d81") -Files @( | |||||
| @{ Name = "member-test"; Path = $link.Program; Type = "prg" }) | |||||
| Write-Host ("built {0} (end {1:X4})" -f $link.Program, $end) | |||||
| @@ -0,0 +1,49 @@ | |||||
| # E05-S04: run the member fixture and check field offsets, two-object | |||||
| # independence, the concrete-call ABI, and word results. | |||||
| [CmdletBinding()] | |||||
| param([int]$Port = 6556, [int]$TimeoutSeconds = 180) | |||||
| $ErrorActionPreference = "Stop" | |||||
| $project = Split-Path $PSScriptRoot -Parent | |||||
| Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force | |||||
| & (Join-Path $PSScriptRoot "build-member-test.ps1") | |||||
| $out = Join-Path $project "build\member-test" | |||||
| $disk = Join-Path $out "member-test.d81" | |||||
| $failures = @() | |||||
| $session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` | |||||
| -Screenshot (Join-Path $out "member-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 += "flag $i is $($mb[8 + $i]), expected 1" } | |||||
| } | |||||
| if ($mb[12] -ne 0x78) { $failures += ('result low is {0:X2}, expected 78' -f $mb[12]) } | |||||
| if ($mb[13] -ne 0x56) { $failures += ('result high is {0:X2}, expected 56' -f $mb[13]) } | |||||
| $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 "MEMBER OK")) { | |||||
| $failures += "screen does not contain 'MEMBER OK'" | |||||
| } | |||||
| if ($failures) { throw ("member 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 member/call flags set" | |||||
| Write-Host "verified field offsets, two-object independence, concrete-call ABI with argument discard, and word results" | |||||
| @@ -0,0 +1,22 @@ | |||||
| ; members.inc - self, field access, and concrete-call emission (E05-S04). | |||||
| ; | |||||
| ; Generated methods receive self in A/X (D129) and keep it in MB_SELF so | |||||
| ; `self.field` needs only one zero-page pointer. MB_SELF must stay clear of | |||||
| ; the frame ABI at $FB-$FE; members.s asserts that at link time. | |||||
| ; | |||||
| ; Field access uses the class layout from E03-S05: the instance begins with | |||||
| ; its two-byte descriptor pointer and fields follow in declaration order, so | |||||
| ; the parser-supplied offset is used unchanged. | |||||
| .ifndef MEMBERS_INC | |||||
| MEMBERS_INC = 1 | |||||
| MB_SELF = $F9 ; two-byte receiver pointer in the generated program | |||||
| MB_BYTE = 1 ; field size codes for mb_ld_field/mb_st_field | |||||
| MB_WORD = 2 | |||||
| MB_OK = 0 | |||||
| MB_ERR_SIZE = 1 ; field size is not MB_BYTE or MB_WORD | |||||
| .endif | |||||
| @@ -10,6 +10,14 @@ | |||||
| ; cg_lda_imm A = value | ; cg_lda_imm A = value | ||||
| ; cg_ldx_imm A = value | ; cg_ldx_imm A = value | ||||
| ; cg_ora_imm A = value | ; cg_ora_imm A = value | ||||
| ; cg_ldy_imm A = value | |||||
| ; cg_lda_zp A = zero-page address | |||||
| ; cg_ldx_zp A = zero-page address | |||||
| ; cg_sta_zp A = zero-page address | |||||
| ; cg_stx_zp A = zero-page address | |||||
| ; cg_lda_zpy A = zero-page pointer address, indexed by Y | |||||
| ; cg_sta_zpy A = zero-page pointer address, indexed by Y | |||||
| ; (there is no LDX (zp),Y: use LDA/TAX for a high byte) | |||||
| ; cg_lda_abs A/X = address | ; cg_lda_abs A/X = address | ||||
| ; cg_ldx_abs A/X = address | ; cg_ldx_abs A/X = address | ||||
| ; cg_sta_abs A/X = address | ; cg_sta_abs A/X = address | ||||
| @@ -38,7 +46,8 @@ | |||||
| .export cg_init, cg_put, cg_here, cg_branch_fwd, cg_patch_here | .export cg_init, cg_put, cg_here, cg_branch_fwd, cg_patch_here | ||||
| .export cg_set_count, cg_count | .export cg_set_count, cg_count | ||||
| .export cg_lda_imm, cg_ldx_imm, cg_ora_imm, cg_lda_abs, cg_ldx_abs | |||||
| .export cg_lda_imm, cg_ldx_imm, cg_ora_imm, cg_ldy_imm, cg_lda_abs, cg_ldx_abs | |||||
| .export cg_lda_zp, cg_ldx_zp, cg_sta_zp, cg_stx_zp, cg_lda_zpy, cg_sta_zpy | |||||
| .export cg_sta_abs, cg_stx_abs, cg_adc_abs, cg_sbc_abs, cg_cmp_abs | .export cg_sta_abs, cg_stx_abs, cg_adc_abs, cg_sbc_abs, cg_cmp_abs | ||||
| .export cg_jsr_abs, cg_clc, cg_sec, cg_rts | .export cg_jsr_abs, cg_clc, cg_sec, cg_rts | ||||
| .export cg_code, cg_len, cg_err | .export cg_code, cg_len, cg_err | ||||
| @@ -117,6 +126,48 @@ cg_ora_imm: | |||||
| sec | sec | ||||
| rts | rts | ||||
| cg_ldy_imm: | |||||
| pha | |||||
| lda #$A0 | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| pla | |||||
| jmp cg_put | |||||
| @out: pla | |||||
| sec | |||||
| rts | |||||
| ; Zero-page forms (E05-S04). Each wrapper supplies the opcode in Y; the | |||||
| ; shared routine stores the address before Y is reused by cg_put. | |||||
| cg_lda_zp: | |||||
| ldy #$A5 | |||||
| jmp cg_emit_zp | |||||
| cg_ldx_zp: | |||||
| ldy #$A6 | |||||
| jmp cg_emit_zp | |||||
| cg_sta_zp: | |||||
| ldy #$85 | |||||
| jmp cg_emit_zp | |||||
| cg_stx_zp: | |||||
| ldy #$86 | |||||
| jmp cg_emit_zp | |||||
| cg_lda_zpy: | |||||
| ldy #$B1 | |||||
| jmp cg_emit_zp | |||||
| cg_sta_zpy: | |||||
| ldy #$91 | |||||
| jmp cg_emit_zp | |||||
| cg_emit_zp: | |||||
| sta cg_addr | |||||
| tya | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| lda cg_addr | |||||
| jmp cg_put | |||||
| @out: sec | |||||
| rts | |||||
| ; Absolute-address forms. Each wrapper supplies the opcode in Y; the shared | ; Absolute-address forms. Each wrapper supplies the opcode in Y; the shared | ||||
| ; routine stores the address before Y is reused by cg_put. | ; routine stores the address before Y is reused by cg_put. | ||||
| cg_lda_abs: | cg_lda_abs: | ||||
| @@ -0,0 +1,227 @@ | |||||
| ; members.s - self, field access, and concrete-call emission (E05-S04). | |||||
| ; | |||||
| ; This layer emits the member half of a method body above the scalar emitter | |||||
| ; (codegen.s) and the call helper (flow.s). It assumes the class layout from | |||||
| ; E03-S05: instance fields start at +2 in declaration order, so the parser's | |||||
| ; field offset is used unchanged. | |||||
| ; | |||||
| ; API: | |||||
| ; mb_init reset mb_err | |||||
| ; mb_self_store A/X = incoming self: STA MB_SELF / STX MB_SELF+1. Emitted | |||||
| ; as the first bytes of a method body. | |||||
| ; mb_self_load load MB_SELF into A/X, so a callee receives self. | |||||
| ; mb_point A/X = address of a two-byte object reference; load that | |||||
| ; pointer into MB_SELF (the receiver of `x.field` or a call). | |||||
| ; mb_ld_field A = offset, X = MB_BYTE or MB_WORD: load the field at | |||||
| ; MB_SELF into A, or A (low) / X (high). | |||||
| ; mb_st_field A = offset, X = MB_BYTE or MB_WORD: store A, or A/X, into | |||||
| ; the field at MB_SELF. | |||||
| ; mb_call A/X = method entry, Y = argument bytes: emit self load, JSR, | |||||
| ; then discard the arguments. mb_call_byte/mb_call_word also | |||||
| ; preserve a one- or two-byte result. | |||||
| ; mb_err MB_OK or MB_ERR_SIZE. | |||||
| ; | |||||
| ; Emitter failures (buffer full, missing runtime address) come back as C=1 and | |||||
| ; are reported through cg_err/fl_err; mb_err only describes a bad field size. | |||||
| ; | |||||
| ; Field access reuses one zero-page pointer because 6502 indexed-indirect | |||||
| ; addressing needs a zero-page operand. A byte field costs four bytes | |||||
| ; (LDY #offset, LDA (MB_SELF),Y); a word adds four more for the high byte. | |||||
| .setcpu "6502" | |||||
| .include "../common/codegen.inc" | |||||
| .include "../common/members.inc" | |||||
| .include "../common/frames.inc" | |||||
| .import cg_put, cg_ldy_imm, cg_lda_zp, cg_ldx_zp, cg_sta_zp, cg_stx_zp | |||||
| .import cg_lda_zpy, cg_sta_zpy, cg_lda_abs | |||||
| .import fl_call, fl_call_byte, fl_call_word | |||||
| .export mb_init, mb_point, mb_self_load, mb_self_store | |||||
| .export mb_ld_field, mb_st_field, mb_call, mb_call_byte, mb_call_word | |||||
| .export mb_err | |||||
| .assert MB_SELF + 1 < FRAME_SP, error, "MB_SELF overlaps the frame ABI" | |||||
| .assert MB_SELF + 1 < FRAME_FP, error, "MB_SELF overlaps the frame ABI" | |||||
| .segment "CODE" | |||||
| mb_init: | |||||
| lda #MB_OK | |||||
| sta mb_err | |||||
| clc | |||||
| rts | |||||
| ; A/X = address of a two-byte object reference. | |||||
| mb_point: | |||||
| sta mb_addr | |||||
| stx mb_addr+1 | |||||
| jsr cg_lda_abs | |||||
| bcs @out | |||||
| lda #MB_SELF | |||||
| jsr cg_sta_zp | |||||
| bcs @out | |||||
| lda mb_addr | |||||
| clc | |||||
| adc #$01 | |||||
| sta mb_addr | |||||
| lda mb_addr+1 | |||||
| adc #$00 | |||||
| sta mb_addr+1 | |||||
| lda mb_addr | |||||
| ldx mb_addr+1 | |||||
| jsr cg_lda_abs | |||||
| bcs @out | |||||
| lda #MB_SELF + 1 | |||||
| jmp cg_sta_zp | |||||
| @out: sec | |||||
| rts | |||||
| ; A/X = self. Record it for later field access. | |||||
| mb_self_store: | |||||
| lda #MB_SELF | |||||
| jsr cg_sta_zp | |||||
| bcs @out | |||||
| lda #MB_SELF + 1 | |||||
| jmp cg_stx_zp | |||||
| @out: sec | |||||
| rts | |||||
| ; Load MB_SELF into A/X. | |||||
| mb_self_load: | |||||
| lda #MB_SELF | |||||
| jsr cg_lda_zp | |||||
| bcs @out | |||||
| lda #MB_SELF + 1 | |||||
| jmp cg_ldx_zp | |||||
| @out: sec | |||||
| rts | |||||
| ; A = offset, X = size. Load the field at MB_SELF. | |||||
| mb_ld_field: | |||||
| sta mb_off | |||||
| stx mb_size | |||||
| cpx #MB_WORD | |||||
| beq @ok | |||||
| cpx #MB_BYTE | |||||
| bne @bad | |||||
| @ok: lda mb_off | |||||
| jsr cg_ldy_imm | |||||
| bcs @out | |||||
| lda #MB_SELF | |||||
| jsr cg_lda_zpy | |||||
| bcs @out | |||||
| lda mb_size | |||||
| cmp #MB_WORD | |||||
| bne @done | |||||
| lda #$48 ; PHA: the low byte is already in A | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| lda mb_off | |||||
| clc | |||||
| adc #$01 | |||||
| jsr cg_ldy_imm | |||||
| bcs @out | |||||
| lda #MB_SELF | |||||
| jsr cg_lda_zpy | |||||
| bcs @out | |||||
| lda #$AA ; TAX: no LDX (zp),Y exists | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| lda #$68 ; PLA: A = low, X = high | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| @done: clc | |||||
| rts | |||||
| @bad: lda #MB_ERR_SIZE | |||||
| sta mb_err | |||||
| sec | |||||
| rts | |||||
| @out: sec | |||||
| rts | |||||
| ; A = offset, X = size. Store A, or A/X, into the field at MB_SELF. | |||||
| mb_st_field: | |||||
| sta mb_off | |||||
| stx mb_size | |||||
| cpx #MB_WORD | |||||
| beq @ok | |||||
| cpx #MB_BYTE | |||||
| bne @bad | |||||
| @ok: lda mb_off | |||||
| jsr cg_ldy_imm | |||||
| bcs @out | |||||
| lda #MB_SELF | |||||
| jsr cg_sta_zpy | |||||
| bcs @out | |||||
| lda mb_size | |||||
| cmp #MB_WORD | |||||
| bne @done | |||||
| lda mb_off | |||||
| clc | |||||
| adc #$01 | |||||
| jsr cg_ldy_imm | |||||
| bcs @out | |||||
| lda #$8A ; TXA: high byte (no STX (zp),Y exists) | |||||
| jsr cg_put | |||||
| bcs @out | |||||
| lda #MB_SELF | |||||
| jsr cg_sta_zpy | |||||
| bcs @out | |||||
| @done: clc | |||||
| rts | |||||
| @bad: lda #MB_ERR_SIZE | |||||
| sta mb_err | |||||
| sec | |||||
| rts | |||||
| @out: sec | |||||
| rts | |||||
| ; A/X = entry, Y = argument bytes. No result. | |||||
| mb_call: | |||||
| sta mb_addr | |||||
| stx mb_addr+1 | |||||
| sty mb_count | |||||
| jsr mb_self_load | |||||
| bcs @out | |||||
| lda mb_addr | |||||
| ldx mb_addr+1 | |||||
| ldy mb_count | |||||
| jmp fl_call | |||||
| @out: sec | |||||
| rts | |||||
| ; A/X = entry, Y = argument bytes. One-byte result in A. | |||||
| mb_call_byte: | |||||
| sta mb_addr | |||||
| stx mb_addr+1 | |||||
| sty mb_count | |||||
| jsr mb_self_load | |||||
| bcs @out | |||||
| lda mb_addr | |||||
| ldx mb_addr+1 | |||||
| ldy mb_count | |||||
| jmp fl_call_byte | |||||
| @out: sec | |||||
| rts | |||||
| ; A/X = entry, Y = argument bytes. Two-byte result in A/X. | |||||
| mb_call_word: | |||||
| sta mb_addr | |||||
| stx mb_addr+1 | |||||
| sty mb_count | |||||
| jsr mb_self_load | |||||
| bcs @out | |||||
| lda mb_addr | |||||
| ldx mb_addr+1 | |||||
| ldy mb_count | |||||
| jmp fl_call_word | |||||
| @out: sec | |||||
| rts | |||||
| .segment "BSS" | |||||
| mb_err: .res 1 | |||||
| mb_addr: .res 2 | |||||
| mb_off: .res 1 | |||||
| mb_size: .res 1 | |||||
| mb_count: .res 1 | |||||
| @@ -0,0 +1,599 @@ | |||||
| ; E05-S04: fields, self, and concrete calls in x128. | |||||
| ; | |||||
| ; members.s emits method-body fragments; this fixture generates several and | |||||
| ; executes them against two objects of the same class layout (descriptor word | |||||
| ; at +0, byte field at +2, word field at +3..4): | |||||
| ; | |||||
| ; 1. a field writer storing the immediates in gen_count/gen_lo/gen_hi, | |||||
| ; run on both objects, then read back through emitted byte and word | |||||
| ; loaders, which proves offsets and independent state; | |||||
| ; 2. a caller that loads a receiver reference with mb_point, pushes arguments | |||||
| ; on the frame stack, and calls a real method with self in A/X, which | |||||
| ; proves the direct-JSR call ABI and argument discard; | |||||
| ; 3. the same caller shape with a word result, stored by the call site. | |||||
| ; | |||||
| ; Mailbox data: | |||||
| ; +0 field store/load offsets verified (1) | |||||
| ; +1 the two objects kept independent state (1) | |||||
| ; +2 concrete call ABI and argument discard verified (1) | |||||
| ; +3 word result preserved across the call path (1) | |||||
| ; +4 word result low byte ($78) | |||||
| ; +5 word result high byte ($56) | |||||
| ; Failure bits: 1 emitter, 2 store/offset, 4 independence, 8 call ABI, | |||||
| ; 16 result. | |||||
| .setcpu "6502" | |||||
| .include "../../src/common/c128.inc" | |||||
| .include "../../src/common/mailbox.inc" | |||||
| .include "../../src/common/print.inc" | |||||
| .include "../../src/common/codegen.inc" | |||||
| .include "../../src/common/flow.inc" | |||||
| .include "../../src/common/frames.inc" | |||||
| .include "../../src/common/members.inc" | |||||
| .import cg_init, cg_err, cg_code, cg_rts | |||||
| .import cg_lda_imm, cg_ldx_imm, cg_jsr_abs, cg_sta_abs, cg_stx_abs | |||||
| .import fl_err, fl_enter, fl_leave, fl_discard | |||||
| .import mb_init, mb_point, mb_self_load, mb_self_store | |||||
| .import mb_ld_field, mb_st_field, mb_call, mb_call_word | |||||
| .import mb_err | |||||
| .import frame_init, frame_enter, frame_leave, frame_discard, frame_push_byte, frame_push_word | |||||
| .import pr_char, pr_text, pr_cr | |||||
| COUNT_OFF = 2 ; byte field | |||||
| TOTAL_OFF = 3 ; word field | |||||
| OBJ_SIZE = 5 | |||||
| .macro TEXT src, len | |||||
| lda #<src | |||||
| ldx #>src | |||||
| ldy #len | |||||
| jsr pr_text | |||||
| .endmacro | |||||
| ; Branch to a global label even when it is out of relative range. | |||||
| .macro ON_CARRY target | |||||
| bcc :+ | |||||
| jmp target | |||||
| : | |||||
| .endmacro | |||||
| .macro ON_NE target | |||||
| beq :+ | |||||
| jmp target | |||||
| : | |||||
| .endmacro | |||||
| .segment "LOADADDR" | |||||
| .word $1C01 | |||||
| .segment "EXEHDR" | |||||
| .word basic_end | |||||
| .word 10 | |||||
| .byte $9E | |||||
| .byte "7181" | |||||
| .byte $00 | |||||
| basic_end: | |||||
| .word $0000 | |||||
| .segment "MAILBOX" | |||||
| .res MB_SIZE | |||||
| .segment "ZEROPAGE" | |||||
| set_ptr: .res 2 ; receiver for the hand-written methods | |||||
| .segment "STARTUP" | |||||
| .export test_entry, test_end | |||||
| test_entry: | |||||
| php | |||||
| sei | |||||
| ldx #MB_SIZE - 1 | |||||
| lda #$00 | |||||
| @clr: sta MB_BASE,x | |||||
| dex | |||||
| bpl @clr | |||||
| lda #MB_MAGIC0_VALUE | |||||
| sta MB_MAGIC0 | |||||
| lda #MB_MAGIC1_VALUE | |||||
| sta MB_MAGIC1 | |||||
| lda #MB_PROD_PROGRAM | |||||
| sta MB_PRODUCER | |||||
| lda #MB_ST_RUNNING | |||||
| sta MB_STATUS | |||||
| lda #$00 | |||||
| sta MB_CODE | |||||
| sta MB_CODE+1 | |||||
| lda MMU_CR | |||||
| sta saved_cr | |||||
| lda #CR_RAM0_KERNAL | |||||
| sta MMU_CR | |||||
| lda #PR_CLEAR | |||||
| jsr pr_char | |||||
| jsr clear_state | |||||
| jsr frame_init | |||||
| jsr mb_init | |||||
| ; Runtime addresses the emitted prologue and call helpers need. | |||||
| lda #<frame_enter | |||||
| sta fl_enter | |||||
| lda #>frame_enter | |||||
| sta fl_enter+1 | |||||
| lda #<frame_leave | |||||
| sta fl_leave | |||||
| lda #>frame_leave | |||||
| sta fl_leave+1 | |||||
| lda #<frame_discard | |||||
| sta fl_discard | |||||
| lda #>frame_discard | |||||
| sta fl_discard+1 | |||||
| ; ---------------------------------------- 1. field store on both objects | |||||
| lda #3 | |||||
| sta gen_count | |||||
| lda #$34 | |||||
| sta gen_lo | |||||
| lda #$12 | |||||
| sta gen_hi | |||||
| jsr cg_init | |||||
| jsr gen_store | |||||
| ON_CARRY emitter_error | |||||
| jsr run_a | |||||
| lda obj_a+COUNT_OFF | |||||
| cmp #3 | |||||
| ON_NE store_fail | |||||
| lda obj_a+TOTAL_OFF | |||||
| cmp #$34 | |||||
| ON_NE store_fail | |||||
| lda obj_a+TOTAL_OFF+1 | |||||
| cmp #$12 | |||||
| ON_NE store_fail | |||||
| lda #7 | |||||
| sta gen_count | |||||
| lda #$78 | |||||
| sta gen_lo | |||||
| lda #$56 | |||||
| sta gen_hi | |||||
| jsr cg_init | |||||
| jsr gen_store | |||||
| ON_CARRY emitter_error | |||||
| jsr run_b | |||||
| lda obj_b+COUNT_OFF | |||||
| cmp #7 | |||||
| ON_NE store_fail | |||||
| lda obj_b+TOTAL_OFF | |||||
| cmp #$78 | |||||
| ON_NE store_fail | |||||
| lda obj_b+TOTAL_OFF+1 | |||||
| cmp #$56 | |||||
| ON_NE store_fail | |||||
| ; A must be untouched by writing B. | |||||
| lda obj_a+COUNT_OFF | |||||
| cmp #3 | |||||
| ON_NE indep_fail | |||||
| lda obj_a+TOTAL_OFF | |||||
| cmp #$34 | |||||
| ON_NE indep_fail | |||||
| lda obj_a+TOTAL_OFF+1 | |||||
| cmp #$12 | |||||
| ON_NE indep_fail | |||||
| ; ------------------------------------- field loads through the emitter | |||||
| jsr cg_init | |||||
| jsr gen_read_count | |||||
| ON_CARRY emitter_error | |||||
| jsr run_a | |||||
| sta v_count_a | |||||
| jsr run_b | |||||
| sta v_count_b | |||||
| lda v_count_a | |||||
| cmp #3 | |||||
| ON_NE store_fail | |||||
| lda v_count_b | |||||
| cmp #7 | |||||
| ON_NE indep_fail | |||||
| jsr cg_init | |||||
| jsr gen_read_total | |||||
| ON_CARRY emitter_error | |||||
| jsr run_a | |||||
| sta v_lo_a | |||||
| stx v_hi_a | |||||
| jsr run_b | |||||
| sta v_lo_b | |||||
| stx v_hi_b | |||||
| lda v_lo_a | |||||
| cmp #$34 | |||||
| ON_NE store_fail | |||||
| lda v_hi_a | |||||
| cmp #$12 | |||||
| ON_NE store_fail | |||||
| lda v_lo_b | |||||
| cmp #$78 | |||||
| ON_NE indep_fail | |||||
| lda v_hi_b | |||||
| cmp #$56 | |||||
| ON_NE indep_fail | |||||
| lda #$01 | |||||
| sta MB_DATA+0 | |||||
| sta MB_DATA+1 | |||||
| ; ------------------------------- 2. concrete call with arguments | |||||
| lda FRAME_SP | |||||
| sta sp_before | |||||
| lda FRAME_SP+1 | |||||
| sta sp_before+1 | |||||
| lda #<ref_a | |||||
| sta call_ref | |||||
| lda #>ref_a | |||||
| sta call_ref+1 | |||||
| lda #9 | |||||
| sta call_count | |||||
| lda #$CD | |||||
| sta call_lo | |||||
| lda #$AB | |||||
| sta call_hi | |||||
| jsr cg_init | |||||
| jsr gen_call_set | |||||
| ON_CARRY emitter_error | |||||
| lda #$00 | |||||
| jsr run_plain | |||||
| lda obj_a+COUNT_OFF | |||||
| cmp #9 | |||||
| ON_NE call_fail | |||||
| lda obj_a+TOTAL_OFF | |||||
| cmp #$CD | |||||
| ON_NE call_fail | |||||
| lda obj_a+TOTAL_OFF+1 | |||||
| cmp #$AB | |||||
| ON_NE call_fail | |||||
| lda FRAME_SP | |||||
| cmp sp_before | |||||
| ON_NE call_fail | |||||
| lda FRAME_SP+1 | |||||
| cmp sp_before+1 | |||||
| ON_NE call_fail | |||||
| lda obj_b+COUNT_OFF ; B was not the receiver | |||||
| cmp #7 | |||||
| ON_NE call_fail | |||||
| lda #$01 | |||||
| sta MB_DATA+2 | |||||
| ; ------------------------------------- 3. word result at the call site | |||||
| lda #<ref_b | |||||
| sta call_ref | |||||
| lda #>ref_b | |||||
| sta call_ref+1 | |||||
| jsr cg_init | |||||
| jsr gen_call_total | |||||
| ON_CARRY emitter_error | |||||
| lda #$00 | |||||
| jsr run_plain | |||||
| lda res_lo | |||||
| sta MB_DATA+4 | |||||
| lda res_hi | |||||
| sta MB_DATA+5 | |||||
| lda res_lo | |||||
| cmp #$78 | |||||
| ON_NE result_fail | |||||
| lda res_hi | |||||
| cmp #$56 | |||||
| ON_NE result_fail | |||||
| lda #$01 | |||||
| sta MB_DATA+3 | |||||
| jmp report | |||||
| emitter_error: | |||||
| lda #$01 | |||||
| jmp fail | |||||
| store_fail: | |||||
| lda #$02 | |||||
| jmp fail | |||||
| indep_fail: | |||||
| lda #$04 | |||||
| jmp fail | |||||
| call_fail: | |||||
| lda #$08 | |||||
| jmp fail | |||||
| result_fail: | |||||
| lda #$10 | |||||
| jmp fail | |||||
| fail: | |||||
| ora fail_mask | |||||
| sta fail_mask | |||||
| report: | |||||
| lda saved_cr | |||||
| sta MMU_CR | |||||
| lda fail_mask | |||||
| sta MB_CODE | |||||
| lda #$00 | |||||
| sta MB_CODE+1 | |||||
| ldx fail_mask | |||||
| bne @fail | |||||
| lda #MB_ST_PASS | |||||
| sta MB_STATUS | |||||
| TEXT msg_ok, 9 | |||||
| jmp @end | |||||
| @fail: | |||||
| lda #MB_ST_FAIL | |||||
| sta MB_STATUS | |||||
| TEXT msg_fail, 11 | |||||
| @end: jsr pr_cr | |||||
| plp | |||||
| rts | |||||
| ; ------------------------------------------------------------- state | |||||
| clear_state: | |||||
| lda #$00 | |||||
| sta fail_mask | |||||
| sta res_lo | |||||
| sta res_hi | |||||
| sta v_count_a | |||||
| sta v_count_b | |||||
| ldx #OBJ_SIZE - 1 | |||||
| @a: sta obj_a,x | |||||
| dex | |||||
| bpl @a | |||||
| ldx #OBJ_SIZE - 1 | |||||
| @b: sta obj_b,x | |||||
| dex | |||||
| bpl @b | |||||
| sta obj_a+0 ; descriptor words stay zero here: these | |||||
| sta obj_b+0 ; fixtures exercise fields, not classes | |||||
| lda #<obj_a | |||||
| sta ref_a | |||||
| lda #>obj_a | |||||
| sta ref_a+1 | |||||
| lda #<obj_b | |||||
| sta ref_b | |||||
| lda #>obj_b | |||||
| sta ref_b+1 | |||||
| rts | |||||
| ; --------------------------------------------------------- generators | |||||
| ; store self.count = gen_count; self.total = gen_hi:gen_lo | |||||
| gen_store: | |||||
| jsr mb_self_store | |||||
| ON_CARRY gen_out | |||||
| lda gen_count | |||||
| jsr cg_lda_imm | |||||
| ON_CARRY gen_out | |||||
| lda #COUNT_OFF | |||||
| ldx #MB_BYTE | |||||
| jsr mb_st_field | |||||
| ON_CARRY gen_out | |||||
| lda gen_lo | |||||
| jsr cg_lda_imm | |||||
| ON_CARRY gen_out | |||||
| lda gen_hi | |||||
| jsr cg_ldx_imm | |||||
| ON_CARRY gen_out | |||||
| lda #TOTAL_OFF | |||||
| ldx #MB_WORD | |||||
| jsr mb_st_field | |||||
| ON_CARRY gen_out | |||||
| jmp cg_rts | |||||
| gen_out: | |||||
| sec | |||||
| rts | |||||
| ; return self.count | |||||
| gen_read_count: | |||||
| jsr mb_self_store | |||||
| ON_CARRY gen_out | |||||
| lda #COUNT_OFF | |||||
| ldx #MB_BYTE | |||||
| jsr mb_ld_field | |||||
| ON_CARRY gen_out | |||||
| jmp cg_rts | |||||
| ; return self.total | |||||
| gen_read_total: | |||||
| jsr mb_self_store | |||||
| ON_CARRY gen_out | |||||
| lda #TOTAL_OFF | |||||
| ldx #MB_WORD | |||||
| jsr mb_ld_field | |||||
| ON_CARRY gen_out | |||||
| jmp cg_rts | |||||
| ; receiver = *call_ref; push(word call_hi:call_lo); push(byte call_count); | |||||
| ; call setter(3 bytes). Demonstrates the caller ABI from generated code. | |||||
| gen_call_set: | |||||
| lda call_ref | |||||
| ldx call_ref+1 | |||||
| jsr mb_point | |||||
| ON_CARRY gen_out | |||||
| lda call_lo | |||||
| jsr cg_lda_imm | |||||
| ON_CARRY gen_out | |||||
| lda call_hi | |||||
| jsr cg_ldx_imm | |||||
| ON_CARRY gen_out | |||||
| lda #<frame_push_word | |||||
| ldx #>frame_push_word | |||||
| jsr cg_jsr_abs | |||||
| ON_CARRY gen_out | |||||
| lda call_count | |||||
| jsr cg_lda_imm | |||||
| ON_CARRY gen_out | |||||
| lda #<frame_push_byte | |||||
| ldx #>frame_push_byte | |||||
| jsr cg_jsr_abs | |||||
| ON_CARRY gen_out | |||||
| lda #<setter | |||||
| ldx #>setter | |||||
| ldy #3 | |||||
| jsr mb_call | |||||
| ON_CARRY gen_out | |||||
| jmp cg_rts | |||||
| ; receiver = *call_ref; result = total(). The call site stores A/X, so the | |||||
| ; word result has to survive the call epilogue. | |||||
| gen_call_total: | |||||
| lda call_ref | |||||
| ldx call_ref+1 | |||||
| jsr mb_point | |||||
| ON_CARRY gen_out | |||||
| lda #<total_of | |||||
| ldx #>total_of | |||||
| ldy #0 | |||||
| jsr mb_call_word | |||||
| ON_CARRY gen_out | |||||
| lda #<res_lo | |||||
| ldx #>res_lo | |||||
| jsr cg_sta_abs | |||||
| ON_CARRY gen_out | |||||
| lda #<res_hi | |||||
| ldx #>res_hi | |||||
| jsr cg_stx_abs | |||||
| ON_CARRY gen_out | |||||
| jmp cg_rts | |||||
| ; ------------------------------------------------------------ methods | |||||
| ; set(count as byte, total as word): self in A/X. | |||||
| setter: | |||||
| sta cur_self | |||||
| stx cur_self+1 | |||||
| lda #$00 | |||||
| ldx #$00 | |||||
| jsr frame_enter | |||||
| bcs @fault | |||||
| lda cur_self | |||||
| sta set_ptr | |||||
| lda cur_self+1 | |||||
| sta set_ptr+1 | |||||
| ldy #2 | |||||
| lda (FRAME_FP),y | |||||
| ldy #COUNT_OFF | |||||
| sta (set_ptr),y | |||||
| ldy #3 | |||||
| lda (FRAME_FP),y | |||||
| ldy #TOTAL_OFF | |||||
| sta (set_ptr),y | |||||
| ldy #4 | |||||
| lda (FRAME_FP),y | |||||
| ldy #TOTAL_OFF+1 | |||||
| sta (set_ptr),y | |||||
| jsr frame_leave | |||||
| clc | |||||
| rts | |||||
| @fault: sec | |||||
| rts | |||||
| ; total() as word: A = low, X = high. | |||||
| total_of: | |||||
| sta cur_self | |||||
| stx cur_self+1 | |||||
| lda #$00 | |||||
| ldx #$00 | |||||
| jsr frame_enter | |||||
| bcs @fault | |||||
| lda cur_self | |||||
| sta set_ptr | |||||
| lda cur_self+1 | |||||
| sta set_ptr+1 | |||||
| ldy #TOTAL_OFF | |||||
| lda (set_ptr),y | |||||
| pha | |||||
| ldy #TOTAL_OFF+1 | |||||
| lda (set_ptr),y | |||||
| tax | |||||
| pla | |||||
| pha | |||||
| txa | |||||
| pha | |||||
| jsr frame_leave | |||||
| pla | |||||
| tax | |||||
| pla | |||||
| clc | |||||
| rts | |||||
| @fault: sec | |||||
| rts | |||||
| ; ------------------------------------------------------------- runners | |||||
| ; A = code offset; A/X = self for the snippet. | |||||
| run_self: | |||||
| clc | |||||
| adc #<cg_code | |||||
| sta @target+1 | |||||
| lda #>cg_code | |||||
| adc #$00 | |||||
| sta @target+2 | |||||
| lda run_ptr | |||||
| ldx run_ptr+1 | |||||
| @target: | |||||
| jsr $FFFF | |||||
| rts | |||||
| ; A = code offset; no receiver. | |||||
| run_plain: | |||||
| clc | |||||
| adc #<cg_code | |||||
| sta @target+1 | |||||
| lda #>cg_code | |||||
| adc #$00 | |||||
| sta @target+2 | |||||
| @target: | |||||
| jsr $FFFF | |||||
| rts | |||||
| run_a: | |||||
| lda #<obj_a | |||||
| sta run_ptr | |||||
| lda #>obj_a | |||||
| sta run_ptr+1 | |||||
| lda #$00 | |||||
| jmp run_self | |||||
| run_b: | |||||
| lda #<obj_b | |||||
| sta run_ptr | |||||
| lda #>obj_b | |||||
| sta run_ptr+1 | |||||
| lda #$00 | |||||
| jmp run_self | |||||
| .segment "RODATA" | |||||
| msg_ok: .byte "MEMBER OK" | |||||
| msg_fail: .byte "MEMBER FAIL" | |||||
| .segment "BSS" | |||||
| saved_cr: .res 1 | |||||
| fail_mask: .res 1 | |||||
| gen_count: .res 1 | |||||
| gen_lo: .res 1 | |||||
| gen_hi: .res 1 | |||||
| call_ref: .res 2 | |||||
| call_count: .res 1 | |||||
| call_lo: .res 1 | |||||
| call_hi: .res 1 | |||||
| run_ptr: .res 2 | |||||
| sp_before: .res 2 | |||||
| res_lo: .res 1 | |||||
| res_hi: .res 1 | |||||
| v_count_a: .res 1 | |||||
| v_count_b: .res 1 | |||||
| v_lo_a: .res 1 | |||||
| v_hi_a: .res 1 | |||||
| v_lo_b: .res 1 | |||||
| v_hi_b: .res 1 | |||||
| obj_a: .res OBJ_SIZE | |||||
| obj_b: .res OBJ_SIZE | |||||
| ref_a: .res 2 | |||||
| ref_b: .res 2 | |||||
| cur_self: .res 2 | |||||
| test_end: | |||||
Powered by TurnKey Linux.