mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
ir: clear wide_hi between functions to fix 16-bit op aliasing
The IrLowerer's wide_hi map records "this u8 temp's high byte lives at this other temp" pairs whenever a 16-bit value is produced. Both lower_function and lower_handler reset next_temp to 0 at the start of each function, but neither cleared wide_hi — so stale (low_id -> high_id) entries from earlier functions leaked into subsequent ones. When a fresh function reused those temp IDs for unrelated u8 expressions, is_wide() returned spurious true and widen() handed back stale (lo, hi) pairs whose hi happened to coincide with the *next* temp ID fresh_temp() was about to allocate. The result was 16-bit IR ops (CmpEq16 in particular) where the destination temp aliased one of the source operand high bytes — for War this made `match phase` arms past P_WIN_B impossible to enter and the game would freeze with both face-up cards on the table forever. Fix: clear wide_hi alongside the next_temp reset in both lower_function and lower_handler. Adds a regression test (ir::tests::wide_hi_does_not_leak_between_functions) that constructs a function whose body has no u16 ops but follows a function that does, and asserts no CmpEq16 op aliases its dest with an operand high byte. Also: - Convert the war Playing state's phase machine from an if-chain to a `match`, which is what tripped this bug to the surface (it was lurking in earlier ROMs too but their layouts never produced the dest/source collision shape). - Refactor begin_draw_a/b to set fly_card / fly_face_up via globals before calling arm_fly, since arm_fly only takes 4 params (the v0.1 ABI limit, now diagnosed by E0506). - Hoist the P_RESOLVE comparison result to the global pf_result to dodge the param-clobbering issue documented in examples/war/COMPILER_BUGS.md §2. - Document the bug as item #6 in COMPILER_BUGS.md with a minimal repro and reproducer-test pointer. - Refresh the war golden + audio hash to match the new ROM. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
This commit is contained in:
parent
155a0e7096
commit
4e8e349d7c
8 changed files with 334 additions and 169 deletions
|
|
@ -395,6 +395,17 @@ impl LoweringContext {
|
|||
|
||||
fn lower_function(&mut self, fun: &FunDecl) {
|
||||
self.next_temp = 0;
|
||||
// Clear the wide-temp tracking map. `wide_hi` records "this
|
||||
// low temp has its high byte at this other temp" entries
|
||||
// produced by `make_wide`; without clearing it, the entries
|
||||
// from previous functions leak into the next function and
|
||||
// get matched against fresh temp IDs (since next_temp resets
|
||||
// to 0). That manifests as `is_wide(t)` spuriously returning
|
||||
// true and, worse, `widen(t)` returning a stale `hi` temp ID
|
||||
// that collides with a later `fresh_temp()` allocation —
|
||||
// producing 16-bit IR ops where the destination temp is
|
||||
// *also* one of the source temps. See COMPILER_BUGS.md §6.
|
||||
self.wide_hi.clear();
|
||||
self.current_blocks = Vec::new();
|
||||
self.current_locals = Vec::new();
|
||||
|
||||
|
|
@ -460,6 +471,12 @@ impl LoweringContext {
|
|||
|
||||
fn lower_handler(&mut self, name: &str, block: &Block, state: &StateDecl) {
|
||||
self.next_temp = 0;
|
||||
// Same per-function reset as `lower_function`. See the
|
||||
// commentary there and COMPILER_BUGS.md §6 for why this is
|
||||
// critical — without it, state-handler bodies pick up wide
|
||||
// temp pairs left over from the previous function and emit
|
||||
// catastrophically wrong 16-bit IR ops.
|
||||
self.wide_hi.clear();
|
||||
self.current_blocks = Vec::new();
|
||||
// Seed `current_locals` with the state's declared locals so any
|
||||
// `VarDecl` inside the handler body — tracked by
|
||||
|
|
|
|||
|
|
@ -740,3 +740,64 @@ fn lower_modulo_emits_mod_op_not_load_imm_zero() {
|
|||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_hi_does_not_leak_between_functions() {
|
||||
// Regression test for COMPILER_BUGS.md §6: the IR lowerer's
|
||||
// `wide_hi` map used to persist across function boundaries
|
||||
// even though `next_temp` resets to 0 per function. A
|
||||
// function whose body had no u16 ops would inherit stale
|
||||
// `(temp_id -> high_byte)` entries from earlier functions
|
||||
// and emit `CmpEq16` (or other 16-bit ops) where the
|
||||
// destination temp aliased one of the source temps.
|
||||
//
|
||||
// The shape that reproduces it: function A bumps a u16
|
||||
// global (creating wide entries); function B does u8 ==
|
||||
// const compares against a u8 global. Pre-fix, function B's
|
||||
// last few comparisons would lower to `CmpEq16`. Post-fix,
|
||||
// they all stay narrow.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var clock: u16 = 0
|
||||
var phase: u8 = 0
|
||||
var hits: u8 = 0
|
||||
fun bump_a() { hits += 1 }
|
||||
fun bump_b() { hits += 2 }
|
||||
fun bump_c() { hits += 3 }
|
||||
fun bump_d() { hits += 4 }
|
||||
on frame {
|
||||
clock += 1
|
||||
if phase == 0 { bump_a() }
|
||||
if phase == 1 { bump_b() }
|
||||
if phase == 2 { bump_c() }
|
||||
if phase == 3 { bump_d() }
|
||||
wait_frame
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let mut wide_eq_dest_aliases = 0;
|
||||
for op in frame_fn.blocks.iter().flat_map(|b| &b.ops) {
|
||||
if let IrOp::CmpEq16 {
|
||||
dest, b_hi, a_hi, ..
|
||||
} = op
|
||||
{
|
||||
// The dest of a 16-bit compare must never alias one
|
||||
// of its operand high bytes — that's the symptom of
|
||||
// bug #6 from war/COMPILER_BUGS.md.
|
||||
if dest == b_hi || dest == a_hi {
|
||||
wide_eq_dest_aliases += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
wide_eq_dest_aliases, 0,
|
||||
"wide CmpEq16 destination aliased a source operand — wide_hi leaked between functions"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue