1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 17:52:51 +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:
Claude 2026-04-16 16:03:10 +00:00
parent f128170abf
commit 76d0fd0d28
No known key found for this signature in database
6 changed files with 179 additions and 191 deletions

Binary file not shown.

Binary file not shown.

View file

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