mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
codegen: reuse analyzer's local allocations so inline asm {param} works
Fixes compiler-bugs.md #1 — the inline-asm `{name}` resolver looks parameters up in the analyzer's `VarAllocation` table (because that's the only address map it has), but `IrCodeGen::new` was minting a parallel `$0300+` range for every function-local and ignoring what the analyzer had picked. The spill prologue wrote the param to the codegen's private address, the inline asm read from the analyzer's zero-page address, and nothing ever bridged the two — `LDA {param}` would silently load whatever the RAM clear left at the stale slot (always `0`). Fix: drop the `local_ram_next` loop and just look each local up in `allocations` by the analyzer's qualified name (`__local__{scope}__{local}`). The scope string that `gen_function` already computed for `substitute_asm_vars` is now shared with the new address-seeding loop via a `scope_prefix_for_fn(&str)` helper, so the two call sites can't drift. The analyzer's layout already satisfies the "no overlapping live locals" invariant the codegen was relying on — it scopes every local under `__local__<scope>__<name>` so two functions with a parameter named `x` land in different slots. Updated `gen_function_prologue_spills_params_to_local_ram`: the regression test for the War-era param clobbering bug was asserting the spill's destination specifically had to be an absolute address at `$0300+`. That's no longer the mechanism — the spill lands in whatever slot the analyzer assigned, which is zero page when there's room. The test now asserts the destination is *any* address outside `$04-$07`, which is the actual invariant. Reverted the `LDX $04` / `LDY $05` workaround in `examples/sha256/sha_core.ne` — every primitive there now uses `{dst}` / `{src}` / `{w_ofs}` / `{h_ofs}` / `{k_ofs}` substitution as originally intended. The "Parameter convention" comment that documented the workaround is gone. Regenerated `tests/emulator/goldens/inline_asm_demo.png`: that example's `times_four(input)` was previously returning `input` verbatim because the inline asm's `LDA {result}` / `ASL A` / `ASL A` / `STA {result}` operated on a zero-page byte that was disconnected from the NEScript-level `result` variable. With the fix, `times_four` correctly returns `input * 4`, so the smiley-tracker's frame-180 position shifts by the expected `(frame_count * 4) mod 256` delta. The other 33 ROMs remain byte-identical. Verified: - `cargo clippy --all-targets -- -D warnings` clean on both rustc 1.94.1 and 1.95.0. - `cargo test --all-targets`: 616 + 3 + 75 tests pass. - `cargo fmt --check` clean. - Full emulator harness: 34/34 ROMs match goldens. - SHA-256 of "NES" still computes to `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. - `--memory-map` output now reflects what the generated code actually reads and writes (previously the codegen's $0300+ override was invisible to the dump). https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
This commit is contained in:
parent
f128170abf
commit
76d0fd0d28
6 changed files with 179 additions and 191 deletions
146
compiler-bugs.md
146
compiler-bugs.md
|
|
@ -35,10 +35,13 @@
|
|||
|
||||
## #1 — inline `asm { {param} }` resolves to an address nothing writes to
|
||||
|
||||
**Status**: WORKED-AROUND (every SHA-256 primitive in
|
||||
`examples/sha256/sha_core.ne` reads parameters straight out of the
|
||||
caller's `$04`/`$05` transport slots instead of using `{dst}` /
|
||||
`{src}`)
|
||||
**Status**: FIXED in `src/codegen/ir_codegen.rs::IrCodeGen::new`
|
||||
(the codegen now reads each function-local's address out of the
|
||||
analyzer's `VarAllocation` table instead of minting its own
|
||||
parallel `$0300+` range). The workaround in
|
||||
`examples/sha256/sha_core.ne` — reading parameters directly from
|
||||
the `$04`/`$05` transport slots — has been reverted in the same
|
||||
commit.
|
||||
**Phase**: codegen (prologue spill vs. inline-asm resolver
|
||||
disagree on local addresses)
|
||||
**Surfaced in**: `examples/sha256/sha_core.ne` — the 20-odd
|
||||
|
|
@ -206,96 +209,75 @@ So `times_four(x)` actually returns `x`, not `x * 4`. The
|
|||
committed golden for that example reflects the bug rather than
|
||||
the intended `×4` behaviour.
|
||||
|
||||
### Workaround (applied in `examples/sha256/`)
|
||||
### How it was fixed
|
||||
|
||||
Every primitive in `sha_core.ne` reads its parameters straight
|
||||
out of the transport slots `$04` / `$05` with the raw literal:
|
||||
|
||||
```ne
|
||||
fun cp_wk(dst: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04 ; == dst on entry
|
||||
LDY $05 ; == src on entry
|
||||
LDA {wk},Y
|
||||
STA {wk},X
|
||||
; ... 3 more 4-byte iterations ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works because:
|
||||
|
||||
1. The analyzer's function prologue at the AST level doesn't do
|
||||
anything with the inline-asm block's contents — it's a raw
|
||||
text token.
|
||||
2. The codegen's spill prologue copies `$04`/`$05` → the codegen
|
||||
local but **leaves the originals alone**. So the transport
|
||||
slots still hold the argument when the first instruction of
|
||||
the asm block executes.
|
||||
3. None of the primitives `JSR` from inside the `asm { ... }`
|
||||
block, so nothing else re-enters the function's body (or any
|
||||
other function) while the inline block is running, which
|
||||
would re-populate `$04`/`$05` with different arguments.
|
||||
|
||||
The file has a big comment (`── Parameter convention ──`)
|
||||
explaining exactly this. Every primitive in that file starts
|
||||
with `LDX $04` (and if it has two params, `LDY $05`) instead of
|
||||
`LDX {dst}` / `LDY {src}`.
|
||||
|
||||
### Once the compiler is fixed
|
||||
|
||||
Revert every `LDX $04` / `LDY $05` in `examples/sha256/sha_core.ne`
|
||||
back to `LDX {dst}` / `LDY {src}` / `LDX {h_ofs}` / …, and delete
|
||||
the "Parameter convention" comment. Also consider whether
|
||||
`examples/inline_asm_demo.ne` should be updated so `times_four`
|
||||
actually produces the documented `×4`, and regenerate
|
||||
`tests/emulator/goldens/inline_asm_demo.png` in the same commit —
|
||||
the current golden encodes the buggy behaviour.
|
||||
|
||||
### Guess at the fix
|
||||
|
||||
Two equivalent options, each about 10 lines of code:
|
||||
|
||||
**(a) Make the codegen use the analyzer's allocation for
|
||||
locals.** Drop the `local_ram_next` loop at the top of
|
||||
`Emitter::new` and, instead of minting new addresses, look up
|
||||
each local's analyzer key and copy its address into
|
||||
`var_addrs`:
|
||||
Option (a) from the original writeup: `IrCodeGen::new` now looks
|
||||
each function-local's address up in the analyzer's
|
||||
`VarAllocation` table instead of minting a parallel `$0300+`
|
||||
range. The codegen and the inline-asm resolver consequently
|
||||
agree on every local's address, so `{dst}` / `{src}` / … inside
|
||||
`asm { ... }` blocks resolve to the same slot the NEScript-level
|
||||
code reads and writes.
|
||||
|
||||
```rust
|
||||
// Was:
|
||||
let mut local_ram_next: u16 = 0x0300;
|
||||
// ...
|
||||
for func in &ir.functions {
|
||||
for local in &func.locals {
|
||||
let qualified = /* __local__<scope>__<local.name> */;
|
||||
if let Some(a) = allocations.iter().find(|a| a.name == qualified) {
|
||||
var_addrs.insert(local.var_id, a.address);
|
||||
var_sizes.insert(local.var_id, a.size);
|
||||
var_addrs.insert(local.var_id, local_ram_next);
|
||||
var_sizes.insert(local.var_id, local.size);
|
||||
local_ram_next += local.size.max(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Is now:
|
||||
for func in &ir.functions {
|
||||
let scope = scope_prefix_for_fn(&func.name);
|
||||
for local in &func.locals {
|
||||
let qualified = format!("__local__{scope}__{}", local.name);
|
||||
if let Some(alloc) = allocations.iter().find(|a| a.name == qualified) {
|
||||
var_addrs.insert(local.var_id, alloc.address);
|
||||
var_sizes.insert(local.var_id, alloc.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The analyzer already picks slots that are stable across
|
||||
functions (the `__local__fn__name` prefix avoids collisions and
|
||||
it allocates from zero page first, which is faster anyway), so
|
||||
the codegen's "grow linearly from $0300" policy isn't actually
|
||||
buying anything — and the comment in `ir_codegen.rs` explaining
|
||||
why it's safe to stack locals was already relying on the same
|
||||
"no recursion, bounded call depth" guarantees the analyzer
|
||||
enforces. The analyzer's allocations already satisfy them.
|
||||
The same commit:
|
||||
|
||||
**(b) Make `substitute_asm_vars` use the codegen's
|
||||
`var_addrs`.** Pass `self.var_addrs` (plus the VarId map) into
|
||||
the resolver instead of `self.allocations`. Same effect — both
|
||||
maps agree after this — and arguably more local to the bug. The
|
||||
analyzer's allocations stay as they are.
|
||||
- factors the "function name → analyzer scope prefix" mapping
|
||||
(`_frame` / `_enter` / `_exit` / `_scanline_N` / bare name)
|
||||
into a `scope_prefix_for_fn(&str) -> String` helper and
|
||||
reuses it in `gen_function` so the two sites can't drift;
|
||||
- updates `gen_function_prologue_spills_params_to_local_ram`
|
||||
(the regression test originally guarding the War-era param
|
||||
clobbering bug) to assert the spill's destination is *any*
|
||||
address outside `$04-$07`, not specifically `$0300+`. The
|
||||
invariant that matters is "separate from the transport slots",
|
||||
which holds for the analyzer's zero-page allocations too;
|
||||
- reverts the `LDX $04` / `LDY $05` workaround across every
|
||||
primitive in `examples/sha256/sha_core.ne` back to the
|
||||
intended `LDX {dst}` / `LDY {src}` substitution form, and
|
||||
drops the "Parameter convention" note from the top of the
|
||||
file;
|
||||
- regenerates `tests/emulator/goldens/inline_asm_demo.png`:
|
||||
that example's `times_four` previously returned its input
|
||||
verbatim (the inline asm operated on an unrelated zero-page
|
||||
byte that was always `0`), so the golden's smiley position
|
||||
drifted by exactly the expected `x * 4 mod 256` delta at
|
||||
frame 180.
|
||||
|
||||
Preferred: (a) — it deletes code instead of rerouting it, and
|
||||
it makes the memory map dumped by `--memory-map` truthful again
|
||||
(the codegen's override was invisible to `--memory-map`, which
|
||||
is why the discrepancy above looks puzzling without this writeup).
|
||||
Verified after the fix:
|
||||
|
||||
Once either change is in, re-run the full emulator harness. The
|
||||
`inline_asm_demo` and `sha256` goldens will need fresh captures
|
||||
because both change observable output.
|
||||
- `cargo test --all-targets` — 616 + 3 + 75 tests pass on
|
||||
both rustc 1.94.1 and 1.95.0.
|
||||
- `cargo clippy --all-targets -- -D warnings` clean on both.
|
||||
- Full emulator harness — 34/34 ROMs match their goldens
|
||||
(only `inline_asm_demo.png` changed, and the new capture
|
||||
reflects the corrected `×4` behaviour).
|
||||
- The SHA-256 example still computes `AE9145DB…4E0D` for the
|
||||
auto-demo input `"NES"`, matching `shasum` byte-for-byte,
|
||||
with the inline-asm-pretty `{dst}` / `{src}` primitives.
|
||||
|
||||
---
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -21,28 +21,21 @@
|
|||
//
|
||||
// K[i] and H_INIT[i] live in RAM as `var` arrays loaded from
|
||||
// the init_array initialiser at reset time (see constants.ne).
|
||||
//
|
||||
// ── Parameter convention ────────────────────────────────────
|
||||
//
|
||||
// NEScript passes the first two function parameters via
|
||||
// zero-page slots $04 and $05 before the JSR. The compiler's
|
||||
// standard prologue immediately spills those slots into a
|
||||
// per-function local in high RAM so nested calls don't step on
|
||||
// them — but the inline-asm `{name}` resolver looks parameters
|
||||
// up in the analyzer's allocation table, which doesn't see the
|
||||
// codegen's spill. Rather than double-copy through a global,
|
||||
// every primitive below reads its parameters straight out of
|
||||
// the transport slots with `LDX $04` / `LDY $05`. Our
|
||||
// primitives never JSR from inside the `asm` block, so the
|
||||
// transport slots are still live when we read them.
|
||||
|
||||
// ── 32-bit byte primitives ──────────────────────────────────
|
||||
//
|
||||
// Every primitive reads its destination and source offsets
|
||||
// via `{dst}` / `{src}` / `{w_ofs}` / … substitutions, which
|
||||
// resolve to the analyzer's per-function local slots. The
|
||||
// codegen's function prologue spills the `$04`/`$05` transport
|
||||
// slots into those same addresses on entry, so the values are
|
||||
// already live by the time the asm block runs.
|
||||
|
||||
// wk[dst..dst+4] = wk[src..src+4]
|
||||
fun cp_wk(dst: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {src}
|
||||
LDA {wk},Y
|
||||
STA {wk},X
|
||||
INX
|
||||
|
|
@ -63,8 +56,8 @@ fun cp_wk(dst: u8, src: u8) {
|
|||
// wk[dst..dst+4] ^= wk[src..src+4]
|
||||
fun xor_wk(dst: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {src}
|
||||
LDA {wk},X
|
||||
EOR {wk},Y
|
||||
STA {wk},X
|
||||
|
|
@ -89,8 +82,8 @@ fun xor_wk(dst: u8, src: u8) {
|
|||
// wk[dst..dst+4] &= wk[src..src+4]
|
||||
fun and_wk(dst: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {src}
|
||||
LDA {wk},X
|
||||
AND {wk},Y
|
||||
STA {wk},X
|
||||
|
|
@ -115,8 +108,8 @@ fun and_wk(dst: u8, src: u8) {
|
|||
// wk[dst..dst+4] += wk[src..src+4] (chained ADC for carry)
|
||||
fun add_wk(dst: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {src}
|
||||
CLC
|
||||
LDA {wk},X
|
||||
ADC {wk},Y
|
||||
|
|
@ -142,7 +135,7 @@ fun add_wk(dst: u8, src: u8) {
|
|||
// wk[dst..dst+4] = ~wk[dst..dst+4] (bitwise NOT, in place)
|
||||
fun not_wk(dst: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDX {dst}
|
||||
LDA {wk},X
|
||||
EOR #$FF
|
||||
STA {wk},X
|
||||
|
|
@ -170,7 +163,7 @@ fun not_wk(dst: u8) {
|
|||
// previous byte's bit 0 into the next byte's bit 7.
|
||||
fun rotr1_wk(dst: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDX {dst}
|
||||
LDA {wk},X
|
||||
LSR A
|
||||
INX
|
||||
|
|
@ -191,7 +184,7 @@ fun rotr1_wk(dst: u8) {
|
|||
// new[2] = old[3], new[3] = old[0]
|
||||
fun byte_rotr_wk(dst: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDX {dst}
|
||||
LDY {wk},X
|
||||
INX
|
||||
LDA {wk},X
|
||||
|
|
@ -233,7 +226,7 @@ fun rotr_wk(dst: u8, n: u8) {
|
|||
// becomes 0).
|
||||
fun shr1_wk(dst: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDX {dst}
|
||||
INX
|
||||
INX
|
||||
INX
|
||||
|
|
@ -251,7 +244,7 @@ fun shr1_wk(dst: u8) {
|
|||
// byte becomes 0.
|
||||
fun byte_shr_wk(dst: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDX {dst}
|
||||
INX
|
||||
LDA {wk},X
|
||||
DEX
|
||||
|
|
@ -290,8 +283,8 @@ fun shr_wk(dst: u8, n: u8) {
|
|||
// wk[dst..dst+4] = w[w_ofs..w_ofs+4]
|
||||
fun cp_w_to_wk(dst: u8, w_ofs: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {w_ofs}
|
||||
LDA {w},Y
|
||||
STA {wk},X
|
||||
INX
|
||||
|
|
@ -312,8 +305,8 @@ fun cp_w_to_wk(dst: u8, w_ofs: u8) {
|
|||
// wk[dst..dst+4] += w[w_ofs..w_ofs+4]
|
||||
fun add_w_to_wk(dst: u8, w_ofs: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {w_ofs}
|
||||
CLC
|
||||
LDA {wk},X
|
||||
ADC {w},Y
|
||||
|
|
@ -339,8 +332,8 @@ fun add_w_to_wk(dst: u8, w_ofs: u8) {
|
|||
// w[w_ofs..w_ofs+4] = wk[src..src+4]
|
||||
fun cp_wk_to_w(w_ofs: u8, src: u8) {
|
||||
asm {
|
||||
LDX $05
|
||||
LDY $04
|
||||
LDX {src}
|
||||
LDY {w_ofs}
|
||||
LDA {wk},X
|
||||
STA {w},Y
|
||||
INX
|
||||
|
|
@ -361,8 +354,8 @@ fun cp_wk_to_w(w_ofs: u8, src: u8) {
|
|||
// h_state[h_ofs..h_ofs+4] += wk[src..src+4]
|
||||
fun add_wk_to_h(h_ofs: u8, src: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {h_ofs}
|
||||
LDY {src}
|
||||
CLC
|
||||
LDA {h_state},X
|
||||
ADC {wk},Y
|
||||
|
|
@ -388,8 +381,8 @@ fun add_wk_to_h(h_ofs: u8, src: u8) {
|
|||
// wk[dst..dst+4] += _K_BYTES[k_ofs..k_ofs+4]
|
||||
fun add_k_to_wk(dst: u8, k_ofs: u8) {
|
||||
asm {
|
||||
LDX $04
|
||||
LDY $05
|
||||
LDX {dst}
|
||||
LDY {k_ofs}
|
||||
CLC
|
||||
LDA {wk},X
|
||||
ADC {_K_BYTES},Y
|
||||
|
|
|
|||
|
|
@ -230,51 +230,34 @@ impl<'a> IrCodeGen<'a> {
|
|||
}
|
||||
}
|
||||
// Map every function-local — parameters AND body-declared
|
||||
// vars — into a dedicated RAM slot at `$0300+`. Parameters
|
||||
// are still passed via the zero-page transport slots
|
||||
// vars — into the slot the analyzer already reserved for it.
|
||||
// Parameters arrive via the zero-page transport slots
|
||||
// `$04-$07` as the calling convention, but `gen_function`
|
||||
// emits a prologue at function entry that copies those
|
||||
// transport slots into these per-function RAM slots. That
|
||||
// way, when a function makes a nested call, the nested
|
||||
// call clobbers `$04-$07` (writing its own arguments into
|
||||
// them) without disturbing the caller's saved parameters.
|
||||
// transport slots into the analyzer's per-function slot so
|
||||
// nested calls don't step on the caller's parameters.
|
||||
//
|
||||
// Before this change, parameters lived in `$04-$07` for the
|
||||
// duration of the function body, so any call nested inside
|
||||
// a function's body silently corrupted the caller's
|
||||
// parameters (fixed on the War bug-cleanup branch; see
|
||||
// `git log` for the original reproduction and root cause).
|
||||
// The per-function RAM slots + prologue spill fix that
|
||||
// class of bug at the cost of 4 LDA/STA pairs per function
|
||||
// entry.
|
||||
// NEScript forbids recursion (E0402) and caps call depth
|
||||
// (E0401), so the analyzer's single-slot-per-local layout
|
||||
// can't alias even though two functions may be active on
|
||||
// the 6502 stack at once.
|
||||
//
|
||||
// Locals are laid out linearly across every function:
|
||||
// NEScript forbids recursion (E0402) and enforces a
|
||||
// bounded call depth (E0401), so lifetime overlap between
|
||||
// functions is fine and we don't need to pack them.
|
||||
let mut local_ram_next: u16 = 0x0300;
|
||||
// Advance past any RAM global so locals don't clobber them.
|
||||
// Each global occupies `[address, address + size)` — for an
|
||||
// array global at $0308 with size=4 that's $0308..$030C. We
|
||||
// must advance past the END, not the base, otherwise
|
||||
// subsequent locals overlap with the tail of the array.
|
||||
// Globals are looked up by name against the analyzer's
|
||||
// `allocations` (which has per-global sizes) rather than the
|
||||
// `var_addrs` map, which only stores base addresses.
|
||||
let max_ram_global_end = allocations
|
||||
.iter()
|
||||
.filter(|a| a.address >= 0x0100)
|
||||
.map(|a| a.address + a.size.max(1))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if max_ram_global_end > local_ram_next {
|
||||
local_ram_next = max_ram_global_end;
|
||||
}
|
||||
// Using the analyzer's addresses here (instead of minting a
|
||||
// fresh linear `$0300+` range) is critical for inline-asm
|
||||
// `{name}` substitution: `substitute_asm_vars` resolves
|
||||
// `{param}` against `allocations` (= the analyzer's table),
|
||||
// so the codegen has to agree with the analyzer on each
|
||||
// local's address or `LDA {param}` inside `asm { ... }`
|
||||
// would read a slot nothing ever writes to. See
|
||||
// `compiler-bugs.md` entry #1 for the full diagnosis.
|
||||
for func in &ir.functions {
|
||||
let scope = scope_prefix_for_fn(&func.name);
|
||||
for local in &func.locals {
|
||||
var_addrs.insert(local.var_id, local_ram_next);
|
||||
var_sizes.insert(local.var_id, local.size);
|
||||
local_ram_next += local.size.max(1);
|
||||
let qualified = format!("__local__{scope}__{}", local.name);
|
||||
if let Some(alloc) = allocations.iter().find(|a| a.name == qualified) {
|
||||
var_addrs.insert(local.var_id, alloc.address);
|
||||
var_sizes.insert(local.var_id, alloc.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
let function_names = ir.functions.iter().map(|f| f.name.clone()).collect();
|
||||
|
|
@ -852,24 +835,7 @@ impl<'a> IrCodeGen<'a> {
|
|||
// their locals. For regular user functions it's just
|
||||
// the function name. See the commentary on
|
||||
// `current_fn_scope_prefix` above.
|
||||
self.current_fn_scope_prefix = if let Some(state) = func.name.strip_suffix("_frame") {
|
||||
format!("{state}__frame")
|
||||
} else if let Some(state) = func.name.strip_suffix("_enter") {
|
||||
format!("{state}__enter")
|
||||
} else if let Some(state) = func.name.strip_suffix("_exit") {
|
||||
format!("{state}__exit")
|
||||
} else if let Some(rest) = func.name.strip_prefix("") {
|
||||
// Scanline handlers encode the line number, but
|
||||
// the analyzer's prefix is
|
||||
// `{state}__scanline_{N}` — check the split.
|
||||
if let Some((state, line)) = rest.rsplit_once("_scanline_") {
|
||||
format!("{state}__scanline_{line}")
|
||||
} else {
|
||||
rest.to_string()
|
||||
}
|
||||
} else {
|
||||
func.name.clone()
|
||||
};
|
||||
self.current_fn_scope_prefix = scope_prefix_for_fn(&func.name);
|
||||
|
||||
self.emit_label(&format!("__ir_fn_{}", func.name));
|
||||
|
||||
|
|
@ -2151,6 +2117,33 @@ enum Cmp16Kind {
|
|||
GtEq,
|
||||
}
|
||||
|
||||
/// Map an IR function name to the analyzer's scope prefix for its
|
||||
/// locals. The analyzer registers every function-local under
|
||||
/// `__local__{prefix}__{name}` — state handlers use
|
||||
/// `{state}__{frame|enter|exit}` or `{state}__scanline_{line}`,
|
||||
/// regular functions use the bare function name. Both
|
||||
/// `IrCodeGen::new` (when seeding `var_addrs`) and `gen_function`
|
||||
/// (when setting `current_fn_scope_prefix` for inline-asm
|
||||
/// substitution) have to agree on the string used here, or
|
||||
/// `{param}` references would resolve to a different address than
|
||||
/// the one generated code reads and writes.
|
||||
fn scope_prefix_for_fn(name: &str) -> String {
|
||||
if let Some(state) = name.strip_suffix("_frame") {
|
||||
format!("{state}__frame")
|
||||
} else if let Some(state) = name.strip_suffix("_enter") {
|
||||
format!("{state}__enter")
|
||||
} else if let Some(state) = name.strip_suffix("_exit") {
|
||||
format!("{state}__exit")
|
||||
} else if let Some((state, line)) = name.rsplit_once("_scanline_") {
|
||||
// Scanline handlers encode the line number in the
|
||||
// function name; the analyzer's prefix joins them with
|
||||
// a double underscore.
|
||||
format!("{state}__scanline_{line}")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace `{name}` tokens in an inline-asm body with the resolved
|
||||
/// hex address from the given resolver. Unknown names and malformed
|
||||
/// placeholders are passed through unchanged (the asm parser will
|
||||
|
|
@ -3916,9 +3909,21 @@ fn gen_function_prologue_spills_params_to_local_ram() {
|
|||
// own arguments, silently corrupting the caller's params.
|
||||
//
|
||||
// Compile a function that takes `x: u8`, calls `helper(x)`,
|
||||
// then uses `x` again. Verify the callee reads `x` from a
|
||||
// RAM slot (absolute addressing at $0300+) rather than
|
||||
// directly from `$04`.
|
||||
// then uses `x` again. Verify that immediately after the
|
||||
// `__ir_fn_caller` label, the codegen emits a spill
|
||||
// `LDA $04 / STA <slot>` where `<slot>` is the analyzer's
|
||||
// dedicated address for the param — crucially, not $04
|
||||
// itself (which nested calls would clobber) and not
|
||||
// $05/$06/$07 either.
|
||||
//
|
||||
// Earlier revisions of this test asserted `<slot>` had to
|
||||
// be an absolute address at `$0300+`, reflecting a codegen
|
||||
// that minted a fresh per-function RAM range. After
|
||||
// `compiler-bugs.md` #1 — the inline-asm `{param}`
|
||||
// resolution fix — the codegen reuses the analyzer's
|
||||
// allocation, which can land in zero page when there's
|
||||
// room. The invariant that matters is "separate from the
|
||||
// transport slots", not "must be main RAM".
|
||||
use crate::parser;
|
||||
let src = r#"
|
||||
game "Test" { mapper: NROM }
|
||||
|
|
@ -3943,14 +3948,17 @@ fn gen_function_prologue_spills_params_to_local_ram() {
|
|||
let mut codegen = IrCodeGen::new(&analysis.var_allocations, &ir);
|
||||
let insts = codegen.generate(&ir);
|
||||
|
||||
// Find the __ir_fn_caller label. Immediately after it, look
|
||||
// for the spill pattern: `LDA $04 / STA <absolute $0300+>`.
|
||||
// Walk the instructions emitted for `caller` (up until the
|
||||
// next function label) looking for the spill `LDA $04` /
|
||||
// `STA <slot>` pair. `<slot>` is accepted as either
|
||||
// `ZeroPage(addr)` or `Absolute(addr)`, as long as `addr`
|
||||
// is outside the `$04-$07` transport range.
|
||||
let caller_idx = insts
|
||||
.iter()
|
||||
.position(|i| i.mode == AM::Label("__ir_fn_caller".into()))
|
||||
.expect("caller function should be emitted");
|
||||
let mut saw_lda_zp4 = false;
|
||||
let mut saw_sta_abs = false;
|
||||
let mut saw_sta_separate = false;
|
||||
for inst in &insts[caller_idx + 1..] {
|
||||
if let AM::Label(l) = &inst.mode {
|
||||
if l.starts_with("__ir_fn_") && l != "__ir_fn_caller" {
|
||||
|
|
@ -3959,20 +3967,25 @@ fn gen_function_prologue_spills_params_to_local_ram() {
|
|||
}
|
||||
if inst.opcode == LDA && inst.mode == AM::ZeroPage(0x04) {
|
||||
saw_lda_zp4 = true;
|
||||
continue;
|
||||
}
|
||||
if saw_lda_zp4 && inst.opcode == STA {
|
||||
if let AM::Absolute(a) = inst.mode {
|
||||
if a >= 0x0300 {
|
||||
saw_sta_abs = true;
|
||||
break;
|
||||
}
|
||||
let addr: u16 = match inst.mode {
|
||||
AM::ZeroPage(a) => u16::from(a),
|
||||
AM::Absolute(a) => a,
|
||||
_ => continue,
|
||||
};
|
||||
if !(0x04..=0x07).contains(&addr) {
|
||||
saw_sta_separate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
saw_lda_zp4 && saw_sta_abs,
|
||||
"caller function should open with `LDA $04 / STA <absolute>` \
|
||||
as the param-spill prologue — the param-clobbering fix is \
|
||||
not in effect"
|
||||
saw_lda_zp4 && saw_sta_separate,
|
||||
"caller function should open with `LDA $04` followed by \
|
||||
a `STA <slot>` that spills the param out of the \
|
||||
transport slots — the param-clobbering fix is not in \
|
||||
effect"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 846 B After Width: | Height: | Size: 846 B |
Loading…
Add table
Add a link
Reference in a new issue