Sfoglia il codice sorgente

Add member emission for fields and concrete calls (E05-S04).

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
Daniel Covington 4 giorni fa
parent
commit
f54af8654d
11 ha cambiato i file con 1171 aggiunte e 1 eliminazioni
  1. +34
    -0
      newLang/docs/architecture.md
  2. +1
    -0
      newLang/docs/decisions.md
  3. +15
    -0
      newLang/docs/memory-map.md
  4. +107
    -0
      newLang/planning/claims/E05-S04.md
  5. +38
    -0
      newLang/planning/epics-and-stories.md
  6. +27
    -0
      newLang/scripts/build-member-test.ps1
  7. +49
    -0
      newLang/scripts/verify-member-test.ps1
  8. +22
    -0
      newLang/src/common/members.inc
  9. +52
    -1
      newLang/src/compiler/codegen.s
  10. +227
    -0
      newLang/src/compiler/members.s
  11. +599
    -0
      newLang/tests/codegen/member-test.s

+ 34
- 0
newLang/docs/architecture.md Vedi File

@@ -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
+/-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

- The generated-program heap initially resides in the same RAM bank as program


+ 1
- 0
newLang/docs/decisions.md Vedi File

@@ -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. |
| 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. |
| 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



+ 15
- 0
newLang/docs/memory-map.md Vedi File

@@ -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
(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)

| Element | Value |


+ 107
- 0
newLang/planning/claims/E05-S04.md Vedi File

@@ -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.

+ 38
- 0
newLang/planning/epics-and-stories.md Vedi File

@@ -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.
- `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


+ 27
- 0
newLang/scripts/build-member-test.ps1 Vedi File

@@ -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)

+ 49
- 0
newLang/scripts/verify-member-test.ps1 Vedi File

@@ -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"

+ 22
- 0
newLang/src/common/members.inc Vedi File

@@ -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

+ 52
- 1
newLang/src/compiler/codegen.s Vedi File

@@ -10,6 +10,14 @@
; cg_lda_imm A = value
; cg_ldx_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_ldx_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_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_jsr_abs, cg_clc, cg_sec, cg_rts
.export cg_code, cg_len, cg_err
@@ -117,6 +126,48 @@ cg_ora_imm:
sec
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
; routine stores the address before Y is reused by cg_put.
cg_lda_abs:


+ 227
- 0
newLang/src/compiler/members.s Vedi File

@@ -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

+ 599
- 0
newLang/tests/codegen/member-test.s Vedi File

@@ -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:

Loading…
Annulla
Salva

Powered by TurnKey Linux.