From 73dcf08c7a009fcd354043377b0122c93216c431 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 02:20:07 +0000 Subject: [PATCH 1/2] analyzer+ir: automatically overlay state-local variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, state-local variables (`state Foo { var x: u8 = 0 }`) were silently no-ops: the analyzer allocated a ZP slot for them, but the codegen's `var_addrs` map only covered IR globals and scope-qualified function locals — so every `LoadVar` / `StoreVar` whose `VarId` pointed at a state-local resolved to no address and emitted nothing. Existing examples compiled and matched their goldens because none of them observed the dropped writes within the 180-frame harness window. The overlay changes the analyzer's state-local pass to snapshot both the ZP and RAM cursors after the globals have been laid out, then rewind to that snapshot before each state's locals and track the running max. `ZP_CURRENT_STATE` keeps exactly one state active at runtime, so every state's locals are mutually exclusive with every other state's and can share the same bytes. The IR lowerer now pushes each state's locals into the IR globals table (with `init_value=None`) so the codegen resolves their addresses the same way it does program globals, and prepends the declared initializers to each state's `on_enter` handler (synthesizing an empty one where needed) so a freshly-entered state re-establishes its bytes before user code runs. `--memory-map` now tags each allocation with its owning state (`[@Title]`, `[@Playing]`, ...) and counts distinct bytes rather than summed allocation sizes so overlaid slots don't double-count. The `AnalysisResult.state_local_owners` map exposes the ownership to any tool that wants to group allocations the same way. Only `state_machine.ne` and `platformer.ne` declare state-level vars, so they're the only example ROMs whose bytes change. `platformer.ne`'s audio golden shifts slightly (the now-working `blink` counter in Title adds a few cycles per frame before the auto-transition to Playing, which offsets APU register writes within each frame); its video golden and every other example ROM stay byte-for-byte identical. Fixes #22. https://claude.ai/code/session_015kvJu3iEFLSRJoShPBfm3X --- CLAUDE.md | 12 ++ docs/future-work.md | 27 +++ docs/language-guide.md | 25 +++ docs/platformer.gif | Bin 456147 -> 456391 bytes examples/platformer.nes | Bin 24592 -> 24592 bytes examples/state_machine.nes | Bin 24592 -> 24592 bytes src/analyzer/mod.rs | 48 ++++- src/ir/lowering.rs | 97 +++++++++- src/main.rs | 61 ++++-- tests/emulator/goldens/platformer.audio.hash | 2 +- tests/integration_test.rs | 193 +++++++++++++++++++ 11 files changed, 446 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8fe317f..602a5ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,6 +210,18 @@ change needs a manual update + review. case — programs without palette/bg keep the old `$10` layout to preserve their goldens). User vars go at `$10+` or `$18+`; IR temps land at `$80+`. +- State-local variables (declared at `state Foo { var x }`) are + automatically **overlaid** across states. The analyzer snapshots + the ZP/RAM cursors after the globals are laid out, rewinds to the + snapshot before each state's locals, and advances to the running + max at the end. Because `ZP_CURRENT_STATE` makes at most one state + active at runtime, two states' locals can share the same bytes — + the IR lowerer re-emits each state's declared initializers at the + top of its `on_enter` handler (synthesizing one if needed) so a + freshly entered state doesn't inherit the previous state's writes. + `--memory-map` annotates each allocation with its owning state + (`[@Title]`, `[@Playing]`, ...) so the overlay shows up in the + report. - `docs/future-work.md` is the authoritative roadmap. If you finish an item, delete its section; if you add a new gap, write one. diff --git a/docs/future-work.md b/docs/future-work.md index f490170..d18733c 100644 --- a/docs/future-work.md +++ b/docs/future-work.md @@ -186,6 +186,33 @@ peephole pass mops up the most obvious waste, but a real CFG-aware allocator that holds short-lived temps in `A`/`X`/`Y` would cut a noticeable number of LDA/STA pairs. +### State-local memory overlay follow-ups + +State-local variables are now overlaid across mutually-exclusive states +(see the analyzer's per-state allocation cursor rewind and the IR +lowerer's `on_enter` initializer prologue), but a few pieces are still +missing: + +- **Same-named locals across different states.** `register_var` stores + state-locals under their bare name, so two states each declaring + `var timer: u8` collide with E0501. A per-state symbol-table scope + prefix would let each state carve its own namespace while keeping + the overlay. +- **Struct-literal and array-literal initializers on state-locals.** + The on-enter prologue lowers scalar initializers cleanly, and + struct-literal initializers fall back to per-field stores, but + array-literal initializers (`var xs: u8[4] = [1,2,3,4]`) are + skipped. A runtime `memcpy` from a ROM blob into the overlay + slot (mirroring the reset-time global path) is the natural + lowering. +- **Handler-local overlay.** Handler-local `var`s declared inside + `on_frame { ... }` are already per-handler scoped via + `current_scope_prefix`, but they get a dedicated RAM slot for the + program's lifetime. Overlaying them inside each handler's stack + frame — using a per-handler bump allocator that resets on each + call — would shave a few bytes more on programs with many deep + handlers. + ### Cross-block temp live-range analysis The slot recycler is function-local per-block. Temps that flow across block diff --git a/docs/language-guide.md b/docs/language-guide.md index c840e63..0c7c122 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -337,6 +337,31 @@ state Playing { `on frame` is syntactic sugar for a loop with an implicit `wait_frame()` at the end. A state can have any combination of `on enter`, `on exit`, and `on frame`. +### State-Local Variables and Memory Overlays + +Variables declared directly inside a `state` block (outside any handler) are **state-local**. They are visible to every handler in the state (`on enter`, `on frame`, etc.) and persist for as long as that state is active. + +Because the NES runtime keeps exactly one state active at a time, the compiler **automatically overlays state-local variables across states**. Two states' locals can share the same RAM bytes without colliding — only the currently active state reads or writes them. This makes the limited 2 KB of NES work RAM go much further on programs with many scenes or game modes. + +``` +state Title { + var blink: u8 = 0 // overlays with Playing.timer below + on enter { blink = 0 } + on frame { blink = blink + 1 } +} + +state Playing { + var timer: u8 = 0 // same byte as Title.blink — reused + var lives: u8 = 3 + on enter { timer = 0; lives = 3 } + on frame { timer = timer + 1 } +} +``` + +Every time a state is entered, its state-local variables are re-initialized from their declared initializers (`= 0`, `= 3` above) before `on enter` runs. This is what makes the overlay safe: entering Playing re-runs `timer = 0` even if the previous state wrote a different value into the shared byte. `cargo run -- build --memory-map` shows each overlaid address alongside its owning state. + +Global `var`s (declared at the top level, outside any state) are never overlaid and keep dedicated RAM slots. Variables declared inside a handler block are handler-local and live only for the handler invocation. + ### State Transitions ``` diff --git a/docs/platformer.gif b/docs/platformer.gif index 4b982f75ddaa11817e99141780a4bc6a2c09314b..2ed2b8665d07342730984112dd51c068fb8d0f63 100644 GIT binary patch delta 47300 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 Ia^gb&59#L9n*aa+ delta 29 lcmX^9R{HX5>4p}@7N!>F7M2#)Eo@6gxBLHO(S5*Z4FJAo3yc5& diff --git a/examples/platformer.nes b/examples/platformer.nes index 81966a4c6020402ca300c8a36f08056c0a536d6f..76cea08bb67c5b6dda96cc4da68ff8d0df0db5e2 100644 GIT binary patch delta 786 zcmZuvO=uHA7~PruNfV<OnWUBKD+0tv$!*$vwy1i~B$B*n)V zJR$*Dq;;Iy-%qiWGhG+;H*K%W$#_-Nztc}x8y&U)5mC)p`eu`%LevcnI4z1rL*#oXX_pV)K98YEtYj= z>Z1O|;$W^SgP?El69fhQo5jzz#FpG78+BG(T#xCJ4XT05Fl1E3GF<+BO_uXbj@TRw zOiH$Ia?Jk6WagL2Umj^9sR!CD;}>{L@@%{ib%dU%S8RAl@*qD5vb-r^kj)^%Aeb1l zG`%k+E@ey+_k)80Ribg_kgt4Y!!pr8;Y!U@D&|jNcBh2*q55Y(x1-Ng2k|#LPS42u z=(Ie7E^?$H!{>K6f99j-^2kKHquy?@xFM?`@P#EDfj|Gd4q*PN?>qcc`5Z)oZpmY~ zpQ6LcEmWtbqM^66t!Nl`>4uWW6I~h$B&P;JInn)pD!i0U+z9(fkKl=e4U=1m8D?Gkjk)08-e29&t1NY++9p2dzeBZ`i;g` X;DdzDHeQDhPs*z=h35H2q>R)bBdIWq delta 742 zcmZ{iO=uHA6vy}NW_O#~Ho;a+HBC1q+wKO6(25i(*h@fR!Gj_q9xC=Ch#-OrMyi29 zP=W~qN-nhrNBg{32vYE3lHHIF2nGdDN-r9aYKdMH6yjtiMAWXyKOhd;#?M#y2}PRG<}iaXM`xZM!4LFGt-ai?FgX}Hx889 zC_l8UI2*1IvpE$d=cCv?C^=tUapVlkG~+D77n}0(paUKnS$PJ#vh&Iep8In@Hs(Vn zJRv%eanFbB_x^#Tenb9ZBuK#6U16s0aEVwfW{0^{SDdtm4~d29gCMgCl8kEM6v!wQ zWSJ!jirffxMtcD+brx*q&8_(yu?4fsXTJ3C4zW=aOuJp86*fyo>9@e}z}S{TW^fR1 zG)0rD1*hpzVQFX=|t zvl?$TQ%;sH=sV#PeX1w7uh*DvLvtHbNgvZl$hlJdqQ4Kok>Z=4eUo~twemtL9WRDv G!1x9DCm@Ib diff --git a/examples/state_machine.nes b/examples/state_machine.nes index bc2ec804e6c30ebae434167d4edad46d5f912f6b..ca4c77536f68813c1a277a36e6e6d435596d0850 100644 GIT binary patch delta 375 zcmbPmfN=s4)i63wtkn&cILUZ{*~kCjNhTom>Yz{Wft3ucf~|rHix^h^o6ErT0thaA z2a0h%6I>avgmY!UOa`Wv3^N&-W->9YOqt2d^o$b%mkOSgo51YDeQ@GGc}u~QHXoQa zF#AXy1R1xKQB&Xp*Gk4#W(8f46(Cz0S_N00XcYoV3nQ~9+b~Mk1JyHrU|%XIk;w$s z%mm_Cyo7K#fE*zZM+m5l1;ha<6M81RRQTi;u%(+0E)|f-<@+32B47&txZ68F#tu^fE=}ib7|8| z2Bwt^GZ~p?GBGV}n90oaj1vO!d44i4lbrm?3b1ABhY;o=`+Ktw(>~kD2befnqYg0~ Ln6jDQfl&bf{Wq2f diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index bdec3cb..5c65c87 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -31,6 +31,10 @@ pub struct AnalysisResult { pub diagnostics: Vec, pub call_graph: HashMap>, pub max_depths: HashMap, + /// For each state-local variable name, the state it belongs to. + /// Consumed by the memory-map printer to group overlaid slots by + /// their owning state. Empty for programs without state-locals. + pub state_local_owners: HashMap, } /// Default call stack depth limit for the NES runtime. @@ -124,12 +128,20 @@ pub fn analyze(program: &Program) -> AnalysisResult { }; analyzer.analyze_program(program); + let mut state_local_owners = HashMap::new(); + for state in &program.states { + for var in &state.locals { + state_local_owners.insert(var.name.clone(), state.name.clone()); + } + } + AnalysisResult { symbols: analyzer.symbols, var_allocations: analyzer.var_allocations, diagnostics: analyzer.diagnostics, call_graph: analyzer.call_graph, max_depths: analyzer.max_depths, + state_local_owners, } } @@ -524,12 +536,46 @@ impl Analyzer { self.register_fun(fun); } - // Register state-local variables + // Register state-local variables with automatic memory + // overlaying. At runtime only one state is active at a time + // (a single `ZP_CURRENT_STATE` byte picks the handler), so + // every state's locals are mutually exclusive with every + // other state's — their RAM footprints can share the same + // addresses. The allocator snapshots both cursors after the + // globals have been laid out, then rewinds to that snapshot + // before each state's locals and tracks the running max. + // The overall cursor advances to the max at the end, so + // anything allocated after the state-locals (function + // parameters, function bodies' locals) picks up past every + // state's overlay window. + // + // Each state's on_enter handler re-initializes the locals + // from their declared initializers — the IR lowering moves + // those stores into the handler's prologue so a freshly + // entered state doesn't read another state's leftover + // bytes. State-locals whose name collides with a global or + // another state's local are still rejected via E0501 at + // `register_var` because the symbol table is keyed by the + // bare name. + let overlay_zp_base = self.next_zp_addr; + let overlay_ram_base = self.next_ram_addr; + let mut max_zp = overlay_zp_base; + let mut max_ram = overlay_ram_base; for state in &program.states { + self.next_zp_addr = overlay_zp_base; + self.next_ram_addr = overlay_ram_base; for var in &state.locals { self.register_var(var); } + if self.next_zp_addr > max_zp { + max_zp = self.next_zp_addr; + } + if self.next_ram_addr > max_ram { + max_ram = self.next_ram_addr; + } } + self.next_zp_addr = max_zp; + self.next_ram_addr = max_ram; // Validate state references let state_names: Vec<&str> = program.states.iter().map(|s| s.name.as_str()).collect(); diff --git a/src/ir/lowering.rs b/src/ir/lowering.rs index 48f8d46..36661ab 100644 --- a/src/ir/lowering.rs +++ b/src/ir/lowering.rs @@ -655,6 +655,35 @@ impl LoweringContext { // enforced. self.capture_inline_bodies(program); + // Register state-local variables as IR globals so the codegen + // resolves their addresses through the same `ir.globals` + // pathway it uses for program globals — the analyzer records + // them under their bare names in `var_allocations`, which + // `IrCodeGen::new` then matches against each global's + // `name` field. Without this, a `LoadVar`/`StoreVar` on a + // state-local variable resolved its `VarId` to no address + // and the codegen silently emitted nothing — the root + // cause of the "state-local variables don't actually work" + // bug that this change ships with the overlay feature. + // + // `init_value` / `init_array` are intentionally left blank: + // state-locals are re-initialized in each state's on_enter + // handler below, not at program reset. The analyzer's + // overlay allocation means one state's initial bytes would + // stomp on another state's if we emitted them at reset. + for state in &program.states { + for var in &state.locals { + let var_id = self.get_or_create_var(&var.name); + self.globals.push(IrGlobal { + var_id, + name: var.name.clone(), + size: type_size(&var.var_type), + init_value: None, + init_array: Vec::new(), + }); + } + } + // Lower user functions for fun in &program.functions { self.lower_function(fun); @@ -737,7 +766,26 @@ impl LoweringContext { // `Title::on frame` and one in `Playing::on frame` get // different VarIds. - if let Some(on_enter) = &state.on_enter { + // State-local variables with initializers need their values + // re-established every time the state is entered, because + // the analyzer overlays state-locals across mutually + // exclusive states and another state's writes can clobber + // the bytes in between. If the state already has an + // on_enter handler, `lower_handler` prepends the + // initializer stores; if not, synthesize an empty one here + // so the dispatch path still calls into the prelude. + let needs_synthetic_enter = + state.on_enter.is_none() && state.locals.iter().any(|v| v.init.is_some()); + let synthetic_enter = Block { + statements: Vec::new(), + span: state.span, + }; + let on_enter_block: Option<&Block> = state.on_enter.as_ref().or(if needs_synthetic_enter { + Some(&synthetic_enter) + } else { + None + }); + if let Some(on_enter) = on_enter_block { self.lower_handler( &format!("{}_enter", state.name), &format!("{}__enter", state.name), @@ -813,6 +861,53 @@ impl LoweringContext { let entry = self.fresh_label(&format!("{name}_entry")); self.start_block(&entry); + + // on_enter handlers carry the state-local initializer + // prologue: every `var x: u8 = expr` declared at + // `state Foo { ... }` level gets a store emitted at the + // top of on_enter so the state's locals are reset every + // time the state is entered. This is what makes the + // analyzer's overlay allocation safe — another state + // having written into these bytes no longer matters, + // because we unconditionally re-initialize them here. + // User code inside the on_enter body then runs on top. + // Locals without an initializer are left at whatever + // bytes the previous state wrote; the programmer can + // explicitly assign them if they want a fresh value. + if name.ends_with("_enter") { + for var in &state.locals { + let Some(init) = &var.init else { continue }; + let var_id = self.get_or_create_var(&var.name); + if let Expr::ArrayLiteral(_, _) = init { + // Array initializers for state-locals aren't + // supported yet — a runtime memcpy loop from a + // ROM blob would be the natural lowering. + // Programs that try this should get a diagnostic + // from the analyzer; for now, silently skip. + continue; + } + if let Expr::StructLiteral(_, fields, _) = init { + for (fname, fexpr) in fields { + let full = format!("{}.{fname}", var.name); + let fvid = self.get_or_create_var(&full); + let val = self.lower_expr(fexpr); + self.emit(IrOp::StoreVar(fvid, val)); + } + continue; + } + let val = self.lower_expr(init); + self.emit(IrOp::StoreVar(var_id, val)); + // u16-typed state-locals also need the high byte + // of the initializer stored at base+1. Mirror the + // `VarDecl` lowering in `lower_statement` so wide + // inits round-trip cleanly. + if matches!(var.var_type, NesType::U16) { + let (_, hi) = self.widen(val); + self.emit(IrOp::StoreVarHi(var_id, hi)); + } + } + } + self.lower_block(block); self.end_block(IrTerminator::Return(None)); diff --git a/src/main.rs b/src/main.rs index 962d429..4fe29f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -155,7 +155,24 @@ fn write_memory_map( backgrounds: &[BackgroundData], ) -> std::io::Result<()> { let mut allocs: Vec<_> = analysis.var_allocations.iter().collect(); - allocs.sort_by_key(|a| a.address); + // Sort by address, then by state-local owner (None before Some), + // so the memory map groups overlaid state-locals together under + // their shared base address. + allocs.sort_by(|a, b| { + a.address.cmp(&b.address).then_with(|| { + analysis + .state_local_owners + .get(&a.name) + .cmp(&analysis.state_local_owners.get(&b.name)) + }) + }); + + let fmt_tag = |name: &str| -> String { + match analysis.state_local_owners.get(name) { + Some(state) => format!("[@{state}]"), + None => "[USER] ".to_string(), + } + }; writeln!(w, "=== NEScript Memory Map ===")?; writeln!(w, "Zero Page ($00-$FF):")?; @@ -164,14 +181,16 @@ fn write_memory_map( " $00-$0F [SYSTEM] reserved (frame flag, input, state, params, scratch)" )?; for a in allocs.iter().filter(|a| a.address < 0x100) { + let tag = fmt_tag(&a.name); if a.size == 1 { - writeln!(w, " ${:04X} [USER] {} (u8)", a.address, a.name)?; + writeln!(w, " ${:04X} {} {} (u8)", a.address, tag, a.name)?; } else { writeln!( w, - " ${:04X}-${:04X} [USER] {} ({} bytes)", + " ${:04X}-${:04X} {} {} ({} bytes)", a.address, a.address + a.size - 1, + tag, a.name, a.size )?; @@ -183,14 +202,16 @@ fn write_memory_map( writeln!(w, "\nRAM ($0200-$07FF):")?; writeln!(w, " $0200-$02FF [SYSTEM] OAM shadow buffer")?; for a in &ram_allocs { + let tag = fmt_tag(&a.name); if a.size == 1 { - writeln!(w, " ${:04X} [USER] {} (u8)", a.address, a.name)?; + writeln!(w, " ${:04X} {} {} (u8)", a.address, tag, a.name)?; } else { writeln!( w, - " ${:04X}-${:04X} [USER] {} ({} bytes)", + " ${:04X}-${:04X} {} {} ({} bytes)", a.address, a.address + a.size - 1, + tag, a.name, a.size )?; @@ -198,17 +219,24 @@ fn write_memory_map( } } - // Summary line. - let zp_used: u16 = allocs - .iter() - .filter(|a| a.address < 0x80) - .map(|a| a.size) - .sum(); - let ram_used: u16 = allocs - .iter() - .filter(|a| a.address >= 0x300) - .map(|a| a.size) - .sum(); + // Summary counts distinct byte addresses in use, not the sum of + // allocation sizes, so overlaid state-locals are only counted + // once per shared byte. Non-state-local allocations and the + // per-state allocations each contribute their own bytes. + let mut zp_bytes_used: std::collections::HashSet = std::collections::HashSet::new(); + let mut ram_bytes_used: std::collections::HashSet = std::collections::HashSet::new(); + for a in &allocs { + for offset in 0..a.size { + let byte = a.address + offset; + if byte < 0x80 { + zp_bytes_used.insert(byte); + } else if byte >= 0x300 { + ram_bytes_used.insert(byte); + } + } + } + let zp_used = zp_bytes_used.len(); + let ram_used = ram_bytes_used.len(); writeln!(w)?; writeln!(w, "Zero Page: {zp_used}/128 bytes used")?; writeln!(w, "Main RAM: {ram_used}/1280 bytes used")?; @@ -513,6 +541,7 @@ mod tests { diagnostics: Vec::new(), call_graph: HashMap::new(), max_depths: HashMap::new(), + state_local_owners: HashMap::new(), } } diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index e2b0778..1acd83e 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -ea23d9c4 132084 +8f18a5d1 132084 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 6d17916..99333f0 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1120,6 +1120,199 @@ fn program_without_palette_does_not_reserve_ppu_zero_page() { ); } +#[test] +fn state_locals_overlay_at_same_base_address() { + // Two states' locals each start at the same ZP address because + // `ZP_CURRENT_STATE` makes them mutually exclusive at runtime. + // The overlay saves bytes: without it, A's two locals plus B's + // two locals would occupy four distinct slots; with it, each + // state uses the same pair of slots. + let source = r#" + game "Overlay" { mapper: NROM } + state A { + var a1: u8 = 11 + var a2: u8 = 22 + on frame { a1 = a1 + 1; a2 = a2 + 1; wait_frame } + } + state B { + var b1: u8 = 33 + var b2: u8 = 44 + on frame { b1 = b1 + 1; b2 = b2 + 1; wait_frame } + } + start A + "#; + let (program, diags) = nescript::parser::parse(source); + assert!(diags.is_empty(), "parse errors: {diags:?}"); + let program = program.expect("parse should succeed"); + let analysis = analyzer::analyze(&program); + assert!( + analysis.diagnostics.iter().all(|d| !d.is_error()), + "unexpected analysis errors: {:?}", + analysis.diagnostics + ); + let addr_of = |name: &str| -> u16 { + analysis + .var_allocations + .iter() + .find(|a| a.name == name) + .unwrap_or_else(|| panic!("var '{name}' not allocated")) + .address + }; + // First locals of each state share the overlay base. + assert_eq!(addr_of("a1"), addr_of("b1")); + // Second locals share the next overlay byte. + assert_eq!(addr_of("a2"), addr_of("b2")); + // Within a single state, sibling locals land at distinct slots. + assert_ne!(addr_of("a1"), addr_of("a2")); + // The second state's owners are recorded so tooling (memory map, + // debug symbols) can group overlaid slots by owning state. + assert_eq!( + analysis.state_local_owners.get("b1").map(String::as_str), + Some("B") + ); +} + +#[test] +fn state_local_and_global_do_not_overlay() { + // Globals sit before the state-local overlay window and keep + // their own slots even if the state-locals happen to start at + // the next address. This guards against a regression where the + // overlay cursor snapshot gets taken before globals are laid + // out, which would alias a global onto a state-local. + let source = r#" + game "NoAlias" { mapper: NROM } + var g1: u8 = 5 + var g2: u8 = 6 + state S { + var s1: u8 = 0 + on frame { s1 = s1 + 1; wait_frame } + } + start S + "#; + let (program, diags) = nescript::parser::parse(source); + assert!(diags.is_empty(), "parse errors: {diags:?}"); + let analysis = analyzer::analyze(&program.unwrap()); + let addr_of = |name: &str| { + analysis + .var_allocations + .iter() + .find(|a| a.name == name) + .unwrap_or_else(|| panic!("var '{name}' not allocated")) + .address + }; + assert_ne!(addr_of("g1"), addr_of("s1")); + assert_ne!(addr_of("g2"), addr_of("s1")); + assert!(addr_of("s1") > addr_of("g2")); +} + +#[test] +fn state_local_store_round_trips_through_zero_page() { + // Prior to the overlay work, a `StoreVar` on a state-local + // silently emitted nothing because the codegen never mapped the + // IR `VarId` to a RAM address — reads and writes inside state + // handlers got dropped and the declared initializer at + // `var counter: u8 = 7` never ran. With the fix, the on_enter + // prologue stores the initializer and the frame handler stores + // a literal value, both landing on the allocated ZP slot. + let source = r#" + game "SL" { mapper: NROM } + state Main { + var counter: u8 = 7 + on frame { + counter = 42 + wait_frame + } + } + start Main + "#; + let rom_data = compile(source); + rom::validate_ines(&rom_data).expect("valid iNES"); + // `LDA #7 / STA $10` — the on_enter prologue writes the + // state-local's declared initializer every time the state is + // entered. + let init_bytes = [0xA9u8, 0x07, 0x85, 0x10]; + assert!( + rom_data.windows(init_bytes.len()).any(|w| w == init_bytes), + "state-local initializer `= 7` should write $10 at state entry" + ); + // `LDA #42 / STA $10` — the frame handler's assignment reaches + // the same slot. Previously this was silently dropped. + let assign_bytes = [0xA9u8, 0x2A, 0x85, 0x10]; + assert!( + rom_data + .windows(assign_bytes.len()) + .any(|w| w == assign_bytes), + "frame handler assignment `counter = 42` should reach $10" + ); +} + +#[test] +fn state_local_initializer_does_not_run_at_reset() { + // With the overlay allocator, each state's `var x = expr` + // initializer runs on every state entry — not once at reset. + // Emitting the init at reset would fight the overlay: the + // last state's initializer would stomp the byte that belongs + // to the active starting state. Verify by looking at the reset + // path in the ROM — the `STA $10` happens only inside each + // state's `_enter` handler (i.e., preceded by a `JSR`), never + // in the straight-line reset prologue. + let source = r#" + game "SL" { mapper: NROM } + state First { + var x: u8 = 1 + on frame { x = x + 1; wait_frame } + } + state Second { + var x2: u8 = 2 + on frame { x2 = x2 + 1; wait_frame } + } + start First + "#; + // x and x2 overlay at $10 (in the no-global case). We can check + // the generated ROM contains both initializers and that both + // land on the same ZP address — which would be impossible if + // they ran at reset (one would overwrite the other before the + // loop ever started). + let rom_data = compile(source); + rom::validate_ines(&rom_data).expect("valid iNES"); + let init_first = [0xA9u8, 0x01, 0x85, 0x10]; // LDA #1 / STA $10 + let init_second = [0xA9u8, 0x02, 0x85, 0x10]; // LDA #2 / STA $10 + assert!( + rom_data.windows(init_first.len()).any(|w| w == init_first), + "First's initializer must survive to its on_enter" + ); + assert!( + rom_data + .windows(init_second.len()) + .any(|w| w == init_second), + "Second's initializer must survive to its on_enter" + ); +} + +#[test] +fn state_without_on_enter_gets_synthesized_one_for_initializers() { + // A state with locals that have initializers but no explicit + // on_enter still needs its initializers re-established on every + // entry. The lowering synthesizes an empty on_enter and + // prepends the init stores. + let source = r#" + game "Synth" { mapper: NROM } + state Only { + var v: u8 = 99 + on frame { v = v + 1; wait_frame } + } + start Only + "#; + let rom_data = compile(source); + rom::validate_ines(&rom_data).expect("valid iNES"); + // `LDA #99 / STA $10` + let init_bytes = [0xA9u8, 0x63, 0x85, 0x10]; + assert!( + rom_data.windows(init_bytes.len()).any(|w| w == init_bytes), + "synthesized on_enter should write $10 with the initializer" + ); +} + // ── M5 Tests ── /// Compile a source string using the mapper-aware linker. From 40dec7907a01b0856096b3fa0db8f3598a0f15ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 11:58:02 +0000 Subject: [PATCH 2/2] examples: move state-scoped globals to state-local in coin_cavern + platformer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both examples declared their gameplay variables at the top level even though every read and write happened inside one specific state. That pattern hid the overlay feature from new users and kept the state-local code path from being exercised outside the dedicated `state_machine.ne` demo (which is how the "state-locals silently drop their writes" bug survived so long). `coin_cavern.ne`: the five Playing-only physics/position/inventory vars (`player_x`, `player_y`, `player_vy`, `on_ground`, `coins_left`) move onto Playing's state block. `score` stays global because GameOver-era code could reasonably grow to read it. The `on_enter` body loses its redundant resets — the declared initializers on the state-locals re-run on every entry, so retrying after `transition Title` comes back to a fresh state. `platformer.ne`: player physics, camera, liveness, animation phase, and the autopilot budget (`player_y`, `on_ground`, `rise_count`, `fall_vy`, `camera_x`, `anim_tick`, `alive`, `auto_jumps`) all move onto Playing. `frame_tick` and `stomp_count` stay global — Title reads the former to auto-advance, GameOver reads the latter to tally coins on the death screen. The analyzer now overlays Title's `blink`, Playing's eight physics vars, and GameOver's `linger` starting at the same ZP byte (`$1A`), so the three scenes share a 9-byte window instead of each claiming their own slots. Byte-level ROM bytes for both examples shift because variable addresses moved. Video goldens stay pixel-identical (the harness doesn't see Playing in coin_cavern, and the pre-transition Title→Playing timing in platformer is preserved); the platformer audio hash needed one more refresh because the now-slightly-shorter reset prologue shifts APU writes within each frame. https://claude.ai/code/session_015kvJu3iEFLSRJoShPBfm3X --- examples/coin_cavern.ne | 20 +++++---- examples/coin_cavern.nes | Bin 24592 -> 24592 bytes examples/platformer.ne | 45 ++++++++++--------- examples/platformer.nes | Bin 24592 -> 24592 bytes tests/emulator/goldens/platformer.audio.hash | 2 +- 5 files changed, 37 insertions(+), 30 deletions(-) diff --git a/examples/coin_cavern.ne b/examples/coin_cavern.ne index 4d4a7f6..a5bb108 100644 --- a/examples/coin_cavern.ne +++ b/examples/coin_cavern.ne @@ -23,12 +23,7 @@ const COIN_X: u8 = 180 const COIN_Y: u8 = 100 // Global variables -var player_x: u8 = 40 -var player_y: u8 = 200 -var player_vy: u8 = 0 -var on_ground: u8 = 1 var score: u8 = 0 -var coins_left: u8 = 3 // Helper function: clamp a value to screen bounds fun clamp_x(val: u8) -> u8 { @@ -53,11 +48,20 @@ state Title { // Main gameplay state state Playing { + // Physics and position live with the state: they're only + // meaningful while Playing is active, and the analyzer + // overlays them with the locals of the Title and GameOver + // states so the idle scenes don't reserve bytes they never + // touch. Initializers re-run on every entry, so dying and + // retrying starts the player back on the ground. + var player_x: u8 = 40 + var player_y: u8 = 200 + var player_vy: u8 = 0 + var on_ground: u8 = 1 + var coins_left: u8 = 3 + on enter { - player_x = 40 - player_y = 200 score = 0 - coins_left = 3 } on frame { diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 4be191b6e65a5efeef17071911aa6fee3e6fa785..d9b031965dcef97986103c4bc6843e65a8decb11 100644 GIT binary patch delta 326 zcmXX>J4*vW5Z;|Vi!^$B4|1hwqZZaS!Pws)PzbTm!vCNs!ZxXLTp?H3h*NBL5WzCH z7QyYV6;h-M+HGeOg0sP4V1{qM$IiInW9nPpEAFymra~oB2~H5O>B zO+IQcu+Bj(^7rEF?0(e{M6C~$)SoIMVXV1Iv`{JS;pGd{>t_bvRP8Sz9ps1HB`C8S zQ1aiZZ?sdAUchxxTJL^tP@en{B_9})NlfTBB7Za(W!JHama(1JVn~3VQ>>4qXtH>4 zp38|_d10ahf~9>U&nb!R)n-|SvlP>f<b}*_KXjXBk08o~WAHk?87W?HX ZbOV3tjPxA z5T^+218ilnXdz5zBjQaI94?%Jd(Jm=ikcTRSA00WPKUc%nim}!rj_A3B8`pE38_Hl zzepJh$NY}t3y}@11CsT5HmNai-osR6O>J0?L0)dcVEoa)QI@te=AYd$^Lu8#?-;z% zS$q^atnpgX27Di-Yah6zRxG+1{?-$y;%m+qqWhT)i~Qa=G#BP-HSN@ diff --git a/examples/platformer.ne b/examples/platformer.ne index 0eaf76c..687cd23 100644 --- a/examples/platformer.ne +++ b/examples/platformer.ne @@ -367,21 +367,17 @@ const AUTOPILOT_JUMPS: u8 = 2 // ── Game state ────────────────────────────────────────────── -// Player physics -var player_y: u8 = 160 -var on_ground: u8 = 1 -var rise_count: u8 = 0 // frames of upward motion remaining -var fall_vy: u8 = 0 // gravity accumulator - -// World/camera -var camera_x: u8 = 0 +// `frame_tick` is shared: Title reads it to auto-advance, Playing +// reads it for animation phasing. `stomp_count` bridges +// Playing → GameOver so the death screen can tally coins. The +// rest — player physics, camera, liveness, autopilot budget — +// are only meaningful while Playing is running, so they live on +// Playing's state block and overlay with the Title / GameOver +// locals (`blink`, `linger`) at the same bytes. + +// Cross-state scratch var frame_tick: u8 = 0 // free-running frame counter -var anim_tick: u8 = 0 // visual animation phase - -// Gameplay -var alive: u8 = 1 // 0 = dying/dead, 1 = playable var stomp_count: u8 = 0 // successful enemy stomps this life -var auto_jumps: u8 = 0 // proximity pre-jumps used this life // ── Helper functions ──────────────────────────────────────── @@ -520,17 +516,24 @@ state Title { } state Playing { + // Physics, camera, liveness, and autopilot budget — all of + // this is Playing-only. Declaring them inside the state block + // lets the analyzer overlay them with Title.blink and + // GameOver.linger; each variable's initializer re-runs on + // entry, so the retry loop starts each life on the ground + // with a fresh autopilot budget without any manual reset. + var player_y: u8 = GROUND_Y + var on_ground: u8 = 1 + var rise_count: u8 = 0 + var fall_vy: u8 = 0 + var camera_x: u8 = 0 + var anim_tick: u8 = 0 + var alive: u8 = 1 + var auto_jumps: u8 = 0 + on enter { - player_y = GROUND_Y - on_ground = 1 - rise_count = 0 - fall_vy = 0 - camera_x = 0 frame_tick = 0 - anim_tick = 0 - alive = 1 stomp_count = 0 - auto_jumps = 0 start_music Theme } diff --git a/examples/platformer.nes b/examples/platformer.nes index 76cea08bb67c5b6dda96cc4da68ff8d0df0db5e2..c55cdc9c6cf477ba2c643ee7ee37603293e984ef 100644 GIT binary patch delta 1174 zcmZ`&O=uHA6yBN1W?OAlwCX19ZZ=7qQZN1<6a;fnR9w6WQV=2j;ayP={-g?X3`PdT zBGeQd_35QjyeL99No_|wc+i4iZ@Qo;tQUVk&^H@xD=NF}``)~H-+OQ7+v)_V;(Lx< zymHO z(_)llNLmD>8^5!pfZqgNTRvNt301;;1Zj0Gt{lU$)|&IHo54CH5G9P=7v2dbvRMs$JOo zs|JEn)da&Yzi0qyH~iTK_I62Lin2Is4bMI0e`tU=cV_$N&rJ5kBu*1Si^Oim9NFey z9P66@;mGlXN9!_QjxF5b1(_LGrnFvy^8VM4(erc~yJ84cT8kB-Bg zKjZm@@ha~}%C?yDVG}O%en=u^zfMWcQIcNqC6VdsTaYPJir^vT#TqkbrIZ%GFZRWl zAwDI}#W;a@L`XBY-c$N#x;yEe45V8z@s{x;J(Mc*rzU}-OrGk$PRk+|oA_$FDEQUM zi^#8QT@yEwziS1ESBlwWC&qNW_uB6o`m;e@e1-Ug8`oN$y)CI0rsfs+w<1N!T1f`Q zY0GQ7Ii-F?JNz?Z#9BpW!~<*1@|RQ68X(bPY4Rm9$(QC#q4z^mJA^|}V*bwQv}7{x-wCGneb*RvPY4CdYh{d<8gHD;Vul9%sjM}XdaI67#y7B zoiNKg3No&Yz#OCAp{jHgMX(RSruURP-u^1yMK(|4-bFMzhh6SSS6OB1ov8XQ`j4Xj z#p(jiZUM;ccIk)FQb4jv-l+8E{~dUM&xP6N%~otRojsK3w>EDDJ1Yi zyfCx7lByeDEUjbLDa+M$JIPR2&@r#0X+g>V7?aM`N>xnyDZG9*pscHkfCQ$WKxqc% z(}bUZkxlOsycZ_5$iX@-`rxV-{W7CWBsE>1n6-v0r2V`ofOX+3~0hjq8M9_Os1Xf&pu)4eZ7kEpi2-dt+EVHvwrt~V0h^-;k+guVOA+``d z6eT;mHmyiNz1jr3RF#R6&A`Azrg8JA_i6%6ocbI65$P$1yCYuPDUubl_Bo8NbB&3y z$O>^I(m9v!CFj0VZTD{5Y9B6Hd6*G5qpOyEYNC|{i=@pL g%LrfMof>}`eQS|D<4