mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
`const_fold_block`'s per-block dead-code pass was collecting temp usage from only the block it was folding, so a `LoadImm` whose destination is consumed by a *sibling* block (for example via the merge block's branch terminator) was incorrectly treated as dead and dropped. The `and` / `or` short-circuit lowering emits exactly that shape: the false path writes `LoadImm(result, 0)` and joins with the right path at an `and_end` / `or_end` block whose branch terminator reads `result`. After the DCE the false path's store was gone, leaving the zero-page result slot to carry whatever value the *previous* `and` / `or` evaluation had written there — stale data that bled into subsequent conditional branches. I found this while instrumenting `examples/platformer.ne` through a puppeteer-driven jsnes harness, stepping one frame at a time and snapshotting the full zero-page trace of each scenario (title-skip, hold-right, hold-left, jump-spam, coin-drift, enemy-stomp, long-run). In a clean idle run the enemy-1 stomp bounce (`rise_count = 6`, `fall_vy = 0`) fired at emulator frames 83 and 96 with `camera_x` = 61 and 74, i.e. with `e1_sx` = 39 and 26, nowhere near the intended `[72, 96)` pickup window. The trigger turned out to be the slot alias: every time `c2_sx` landed in its pickup window (so the coin-2 `and` stored 1 into ZP(130)) and the player was mid-fall at or past `player_y = 152`, the enemy-1 stomp `and` short-circuited to its false path, left ZP(130) at 1, and the stomp `if` fired on stale data. The fix is to compute function-wide source-operand usage once before folding each function's blocks and OR it into the per-block liveness check, so a LoadImm is only dropped if nobody — neither its own block nor any other block in the function — reads the temp. Added a regression test (`const_fold_preserves_loadimm_used_by_sibling_branch`) that builds the exact CFG shape the `and` lowering emits and verifies the false-path `LoadImm(result, 0)` survives optimization. Impact on the example ROMs: - `examples/platformer.nes`: enemy-1 stomp now fires only when `e1_sx ∈ [72, 96)`, as the source intends. The pixel golden is unchanged (`player_y` converges back to the ground line before frame 180), but the audio hash flips because the spurious `play hit` sfx calls during coin-2 passage are gone. Committing the new `tests/emulator/goldens/platformer.audio.hash`. - `examples/logic_ops.nes`, `examples/bitwise_ops.nes`, `examples/match_demo.nes`, `examples/mmc3_per_state_split.nes`, `examples/two_player.nes`: byte-different but observably unchanged — their pixel + audio goldens still match to the byte. They exercise `and` / `or` in the source and now compile through the corrected DCE. All other example ROMs are byte-identical to pre-fix. `cargo fmt`, `cargo clippy --all-targets`, `cargo test --release` (498 tests), and `tests/emulator/run_examples.mjs` (22/22 goldens) are clean. https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi
1 line
16 B
Text
1 line
16 B
Text
a00949db 132084
|