1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

codegen: make var_addrs misses panic loudly and fix latent struct-field silent drop

Phase 1 of the post-PR-#31 audit. The PR #31 state-local bug had a
specific shape: analyzer allocated a slot, codegen looked it up by
VarId, silently emitted nothing on miss. Six sites in gen_op plus
the global-initializer loop and the parameter-shuffle prologue all
used the same `if let Some(&addr) = self.var_addrs.get(var) { ... }`
pattern with no else branch. Any future allocation-map desync would
slip through the same crack.

Replace every site with a new `IrCodeGen::var_addr(VarId) -> u16`
helper that panics with an explicit "compiler bug" message on miss.
An IR op referencing an unmapped VarId is not valid input — it means
the analyzer and lowerer disagreed on what to allocate, and we want
that crash to surface in CI rather than be absorbed by whatever
zero-filled RAM happened to sit at the read.

Running cargo test against the hardened lookup surfaced exactly the
bug shape the plan predicted: uninitialized struct globals (e.g.
`var p: Point` with no literal initializer) never had their flattened
field VarIds (`"p.x"`, `"p.y"`) registered in var_addrs. The IR
lowerer's `get_or_create_var("p.x")` minted a VarId, the analyzer's
`flatten_struct_fields` allocated an address for it, but IrCodeGen::new
only populated var_addrs from `ir.globals`, which doesn't contain
synthesized field entries for uninitialized structs. Every `p.x = N`
silently compiled to nothing.

Fix by exposing the IR lowerer's name→VarId map on IrProgram and
joining it with the analyzer's allocations in IrCodeGen::new. Every
allocated name that the lowerer knows about now gets a var_addrs
entry. Example ROMs are byte-identical (no example relied on the
dropped writes), but the bug was reachable — any user program with
a plain `var pos: Point` declaration and field writes would have hit
it silently.

Add `uninitialized_struct_field_store_emits_sta_to_allocated_address`
as a byte-level regression guard: compile `p.x = 123` and scan PRG
for `LDA #\$7B / STA <addr>`. Fails against the old silently-dropping
codegen.

https://claude.ai/code/session_01AoQ678uVeqpyayvWHpfDhC
This commit is contained in:
Claude 2026-04-17 23:49:29 +00:00
parent 431f144be7
commit f7012c6533
No known key found for this signature in database
5 changed files with 144 additions and 60 deletions

View file

@ -335,6 +335,49 @@ fn program_with_u16_struct_field() {
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn uninitialized_struct_field_store_emits_sta_to_allocated_address() {
// Regression guard for the silent-drop bug uncovered while
// hardening the `var_addrs` lookup in `IrCodeGen`. Before the
// fix, field `VarId`s synthesized by the IR lowerer (e.g.
// `"pos.x"`) were only registered in `var_addrs` when their
// parent struct global had a literal initializer. An
// uninitialized `var pos: Point` produced no field globals, so
// `pos.x = 100` emitted `IrOp::StoreVar(VarId(for pos.x), ...)`
// — and the codegen's `if let Some(&addr) = var_addrs.get(..)`
// guard skipped it, silently dropping the write with no
// diagnostic.
//
// We verify by compiling a program whose entire frame handler
// is a write with a distinctive immediate constant, then
// search the PRG for the corresponding `LDA #$7B ; STA zp/abs`
// pair. `123 = $7B` is chosen because it can't appear as an
// incidental constant in the runtime prelude.
let source = r#"
game "StructStore" { mapper: NROM }
struct Point { x: u8, y: u8 }
var p: Point
on frame {
p.x = 123
}
start Main
"#;
let rom_data = compile(source);
let prg = &rom_data[16..16 + 16384];
let mut found = false;
for i in 0..prg.len().saturating_sub(3) {
if prg[i] == 0xA9 && prg[i + 1] == 0x7B && (prg[i + 2] == 0x85 || prg[i + 2] == 0x8D) {
found = true;
break;
}
}
assert!(
found,
"expected an LDA #$7B / STA <addr> pair for `p.x = 123` — if this \
fails, struct-field writes are being silently dropped again"
);
}
#[test]
fn u16_struct_field_initializer_writes_both_bytes_to_rom() {
// Struct literal initializer with a u16 field > 255 — the