1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 17:28:00 +00:00
nescript/examples/war/compare.ne

27 lines
579 B
Text
Raw Normal View History

2026-04-15 15:22:20 +00:00
// war/compare.ne — card rank/suit extraction and round resolution.
//
// Cards are packed as `(rank << 4) | suit`; the extractors are
// one shift or one mask each.
compiler: fix three scoping bugs; war: revert all local/param workarounds Three related scoping bugs from examples/war/COMPILER_BUGS.md, all fixed in one pass because they're different layer manifestations of the same "flat global namespace" problem: ## §3: function-local `var` declarations lived in one namespace `src/analyzer/mod.rs::register_var` inserted every `var` it saw — top-level, state-local, AND function-body local — into the same `self.symbols: HashMap<String, Symbol>`. Two different functions declaring `var i` collided on E0501, which is why every local in war/*.ne had a function-prefix like `dfa_card` or `dwp_px`. Fix: add a `current_scope_prefix: Option<String>` to the Analyzer, set it to `Some("<fn_name>")` when checking a function body (or `Some("Title__frame")` for state handler bodies), and have `register_var` store the declaration under an internal key `"__local__{prefix}__{name}"`. New `resolve_symbol` / `resolve_key` helpers try the scope-qualified key first and fall back to the bare key for globals / consts / enum variants / state-level vars / function names. Every existing `self.symbols.get(name)` inside body-checking code was swapped over. Two `var i` declarations inside the SAME function body still collide with E0501 — we scoped per function body, not per nested block. Per-block scoping would require live-range analysis to reuse RAM slots. ## §1b: same-named params across functions shared VarIds `src/ir/lowering.rs::get_or_create_var` looked up names in a single global `var_map`, so two functions both with a `card: u8` parameter resolved to the same `VarId`. Whichever function was lowered last won the zero-page slot mapping, silently rerouting the other function's param reads to the wrong slot. Fix: the IR lowerer now mirrors the analyzer's scope logic. `LoweringContext` gains a `current_scope_prefix` field that gets set in `lower_function` / `lower_handler`, and `get_or_create_var` uses a new `scoped_key` helper that prepends `"__local__{prefix}__"` when the qualified key exists in `var_map` or `var_types`. Each function's parameters and locals therefore get distinct VarIds, and the codegen's `var_addrs` map naturally has no collisions. ## §2: param transport slots $04-$07 clobbered across nested JSRs Parameters were passed AND kept in `$04-$07` for the lifetime of a function. Any nested call overwrote those slots with its own arguments, so the caller's params were silently corrupted as soon as it invoked anything. Every war helper that took params and called other helpers (draw_card_face, push_back_a, etc) snapshotted its params into fresh locals at the top of the body. Fix: in `codegen/ir_codegen.rs::IrCodeGen::new`, every function-local — including parameters — now gets a dedicated per-function RAM slot at `$0300+`. Parameters are still passed via the zero-page transport slots `$04-$07` as the calling convention, but `gen_function` now emits a **prologue** at every function entry: LDA $04 STA <param_0_addr> LDA $05 STA <param_1_addr> ... etc, up to 4 ... By the time the body runs, every parameter lives in the function's dedicated RAM slot, so any nested call can freely clobber $04-$07 (writing its own arguments there) without corrupting the caller's saved parameters. Costs 4 LDA/STA pairs (≈ 20 bytes of ROM, 16 cycles) at every function entry — worth it to make the calling convention sound. ## War cleanup With all three fixes in place, every workaround prefix in `examples/war/*.ne` is gone: - `card_rank(card)` instead of `card_rank(crk_c)` — bug #1b - `compare_cards(a, b)` instead of `compare_cards(cmp_a, cmp_b)` - `push_back_a(card)` instead of `push_back_a(pba_in)` — bug #1b - `var card: u8 = draw_front_a()` in bury_from_* — bug #3 - `var i: u8 = 0` freely in multiple functions — bug #3 - `fun push_back_a(card)` body no longer snapshots `card` into `pba_card` before calling wrap52 — bug #2 - `fun draw_card_face` body no longer snapshots x/y/card into locals before calling card_rank/card_suit — bug #2 - `draw_word_player` steps its own x without needing a `dwp_px` accumulator to avoid the `x + N` arg compilation quirk — that quirk was a downstream symptom of bug #2 and is also gone The source is now about 300 lines shorter and significantly more readable. ## Regression tests Seven new tests nail these bugs down: - `analyzer::tests::analyze_allows_same_local_name_in_two_functions` - `analyzer::tests::analyze_allows_same_param_name_in_two_functions` - `analyzer::tests::analyze_allows_same_local_name_in_two_state_handlers` - `analyzer::tests::analyze_still_rejects_duplicate_local_in_same_function` - `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram` Plus the four param-arity tests from the earlier E0506 fix and the wide_hi-leak regression test from the previous compiler fix. Total suite: 591 unit tests, all passing. ## Golden drift The prologue change adds a few cycles to every function entry, which shifts NMI sampling by a handful of cycles and flips the audio-hash of any example that plays sfx or music (platformer, war). `arrays_and_functions.png` also picks up a 1-pixel shift in its enemy positions due to the same timing drift. All three golden updates are pure "compiler produces different but functionally-identical output" — no game behavior changed. ## What's still open in COMPILER_BUGS.md - §4: 8-sprites-per-scanline hardware limit is invisible to user code. A static analyzer hint could help; deferred. - §5: `inline` keyword is silently declined for short functions that the optimizer's inliner doesn't recognize (it only removes empty functions). Deferred pending a real single-return-expression inlining pass. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 20:33:41 +00:00
inline fun card_rank(card: u8) -> u8 {
return card >> 4
2026-04-15 15:22:20 +00:00
}
compiler: fix three scoping bugs; war: revert all local/param workarounds Three related scoping bugs from examples/war/COMPILER_BUGS.md, all fixed in one pass because they're different layer manifestations of the same "flat global namespace" problem: ## §3: function-local `var` declarations lived in one namespace `src/analyzer/mod.rs::register_var` inserted every `var` it saw — top-level, state-local, AND function-body local — into the same `self.symbols: HashMap<String, Symbol>`. Two different functions declaring `var i` collided on E0501, which is why every local in war/*.ne had a function-prefix like `dfa_card` or `dwp_px`. Fix: add a `current_scope_prefix: Option<String>` to the Analyzer, set it to `Some("<fn_name>")` when checking a function body (or `Some("Title__frame")` for state handler bodies), and have `register_var` store the declaration under an internal key `"__local__{prefix}__{name}"`. New `resolve_symbol` / `resolve_key` helpers try the scope-qualified key first and fall back to the bare key for globals / consts / enum variants / state-level vars / function names. Every existing `self.symbols.get(name)` inside body-checking code was swapped over. Two `var i` declarations inside the SAME function body still collide with E0501 — we scoped per function body, not per nested block. Per-block scoping would require live-range analysis to reuse RAM slots. ## §1b: same-named params across functions shared VarIds `src/ir/lowering.rs::get_or_create_var` looked up names in a single global `var_map`, so two functions both with a `card: u8` parameter resolved to the same `VarId`. Whichever function was lowered last won the zero-page slot mapping, silently rerouting the other function's param reads to the wrong slot. Fix: the IR lowerer now mirrors the analyzer's scope logic. `LoweringContext` gains a `current_scope_prefix` field that gets set in `lower_function` / `lower_handler`, and `get_or_create_var` uses a new `scoped_key` helper that prepends `"__local__{prefix}__"` when the qualified key exists in `var_map` or `var_types`. Each function's parameters and locals therefore get distinct VarIds, and the codegen's `var_addrs` map naturally has no collisions. ## §2: param transport slots $04-$07 clobbered across nested JSRs Parameters were passed AND kept in `$04-$07` for the lifetime of a function. Any nested call overwrote those slots with its own arguments, so the caller's params were silently corrupted as soon as it invoked anything. Every war helper that took params and called other helpers (draw_card_face, push_back_a, etc) snapshotted its params into fresh locals at the top of the body. Fix: in `codegen/ir_codegen.rs::IrCodeGen::new`, every function-local — including parameters — now gets a dedicated per-function RAM slot at `$0300+`. Parameters are still passed via the zero-page transport slots `$04-$07` as the calling convention, but `gen_function` now emits a **prologue** at every function entry: LDA $04 STA <param_0_addr> LDA $05 STA <param_1_addr> ... etc, up to 4 ... By the time the body runs, every parameter lives in the function's dedicated RAM slot, so any nested call can freely clobber $04-$07 (writing its own arguments there) without corrupting the caller's saved parameters. Costs 4 LDA/STA pairs (≈ 20 bytes of ROM, 16 cycles) at every function entry — worth it to make the calling convention sound. ## War cleanup With all three fixes in place, every workaround prefix in `examples/war/*.ne` is gone: - `card_rank(card)` instead of `card_rank(crk_c)` — bug #1b - `compare_cards(a, b)` instead of `compare_cards(cmp_a, cmp_b)` - `push_back_a(card)` instead of `push_back_a(pba_in)` — bug #1b - `var card: u8 = draw_front_a()` in bury_from_* — bug #3 - `var i: u8 = 0` freely in multiple functions — bug #3 - `fun push_back_a(card)` body no longer snapshots `card` into `pba_card` before calling wrap52 — bug #2 - `fun draw_card_face` body no longer snapshots x/y/card into locals before calling card_rank/card_suit — bug #2 - `draw_word_player` steps its own x without needing a `dwp_px` accumulator to avoid the `x + N` arg compilation quirk — that quirk was a downstream symptom of bug #2 and is also gone The source is now about 300 lines shorter and significantly more readable. ## Regression tests Seven new tests nail these bugs down: - `analyzer::tests::analyze_allows_same_local_name_in_two_functions` - `analyzer::tests::analyze_allows_same_param_name_in_two_functions` - `analyzer::tests::analyze_allows_same_local_name_in_two_state_handlers` - `analyzer::tests::analyze_still_rejects_duplicate_local_in_same_function` - `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram` Plus the four param-arity tests from the earlier E0506 fix and the wide_hi-leak regression test from the previous compiler fix. Total suite: 591 unit tests, all passing. ## Golden drift The prologue change adds a few cycles to every function entry, which shifts NMI sampling by a handful of cycles and flips the audio-hash of any example that plays sfx or music (platformer, war). `arrays_and_functions.png` also picks up a 1-pixel shift in its enemy positions due to the same timing drift. All three golden updates are pure "compiler produces different but functionally-identical output" — no game behavior changed. ## What's still open in COMPILER_BUGS.md - §4: 8-sprites-per-scanline hardware limit is invisible to user code. A static analyzer hint could help; deferred. - §5: `inline` keyword is silently declined for short functions that the optimizer's inliner doesn't recognize (it only removes empty functions). Deferred pending a real single-return-expression inlining pass. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 20:33:41 +00:00
inline fun card_suit(card: u8) -> u8 {
return card & 0x0F
2026-04-15 15:22:20 +00:00
}
// Compare two cards by rank. Returns:
compiler: fix three scoping bugs; war: revert all local/param workarounds Three related scoping bugs from examples/war/COMPILER_BUGS.md, all fixed in one pass because they're different layer manifestations of the same "flat global namespace" problem: ## §3: function-local `var` declarations lived in one namespace `src/analyzer/mod.rs::register_var` inserted every `var` it saw — top-level, state-local, AND function-body local — into the same `self.symbols: HashMap<String, Symbol>`. Two different functions declaring `var i` collided on E0501, which is why every local in war/*.ne had a function-prefix like `dfa_card` or `dwp_px`. Fix: add a `current_scope_prefix: Option<String>` to the Analyzer, set it to `Some("<fn_name>")` when checking a function body (or `Some("Title__frame")` for state handler bodies), and have `register_var` store the declaration under an internal key `"__local__{prefix}__{name}"`. New `resolve_symbol` / `resolve_key` helpers try the scope-qualified key first and fall back to the bare key for globals / consts / enum variants / state-level vars / function names. Every existing `self.symbols.get(name)` inside body-checking code was swapped over. Two `var i` declarations inside the SAME function body still collide with E0501 — we scoped per function body, not per nested block. Per-block scoping would require live-range analysis to reuse RAM slots. ## §1b: same-named params across functions shared VarIds `src/ir/lowering.rs::get_or_create_var` looked up names in a single global `var_map`, so two functions both with a `card: u8` parameter resolved to the same `VarId`. Whichever function was lowered last won the zero-page slot mapping, silently rerouting the other function's param reads to the wrong slot. Fix: the IR lowerer now mirrors the analyzer's scope logic. `LoweringContext` gains a `current_scope_prefix` field that gets set in `lower_function` / `lower_handler`, and `get_or_create_var` uses a new `scoped_key` helper that prepends `"__local__{prefix}__"` when the qualified key exists in `var_map` or `var_types`. Each function's parameters and locals therefore get distinct VarIds, and the codegen's `var_addrs` map naturally has no collisions. ## §2: param transport slots $04-$07 clobbered across nested JSRs Parameters were passed AND kept in `$04-$07` for the lifetime of a function. Any nested call overwrote those slots with its own arguments, so the caller's params were silently corrupted as soon as it invoked anything. Every war helper that took params and called other helpers (draw_card_face, push_back_a, etc) snapshotted its params into fresh locals at the top of the body. Fix: in `codegen/ir_codegen.rs::IrCodeGen::new`, every function-local — including parameters — now gets a dedicated per-function RAM slot at `$0300+`. Parameters are still passed via the zero-page transport slots `$04-$07` as the calling convention, but `gen_function` now emits a **prologue** at every function entry: LDA $04 STA <param_0_addr> LDA $05 STA <param_1_addr> ... etc, up to 4 ... By the time the body runs, every parameter lives in the function's dedicated RAM slot, so any nested call can freely clobber $04-$07 (writing its own arguments there) without corrupting the caller's saved parameters. Costs 4 LDA/STA pairs (≈ 20 bytes of ROM, 16 cycles) at every function entry — worth it to make the calling convention sound. ## War cleanup With all three fixes in place, every workaround prefix in `examples/war/*.ne` is gone: - `card_rank(card)` instead of `card_rank(crk_c)` — bug #1b - `compare_cards(a, b)` instead of `compare_cards(cmp_a, cmp_b)` - `push_back_a(card)` instead of `push_back_a(pba_in)` — bug #1b - `var card: u8 = draw_front_a()` in bury_from_* — bug #3 - `var i: u8 = 0` freely in multiple functions — bug #3 - `fun push_back_a(card)` body no longer snapshots `card` into `pba_card` before calling wrap52 — bug #2 - `fun draw_card_face` body no longer snapshots x/y/card into locals before calling card_rank/card_suit — bug #2 - `draw_word_player` steps its own x without needing a `dwp_px` accumulator to avoid the `x + N` arg compilation quirk — that quirk was a downstream symptom of bug #2 and is also gone The source is now about 300 lines shorter and significantly more readable. ## Regression tests Seven new tests nail these bugs down: - `analyzer::tests::analyze_allows_same_local_name_in_two_functions` - `analyzer::tests::analyze_allows_same_param_name_in_two_functions` - `analyzer::tests::analyze_allows_same_local_name_in_two_state_handlers` - `analyzer::tests::analyze_still_rejects_duplicate_local_in_same_function` - `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram` Plus the four param-arity tests from the earlier E0506 fix and the wide_hi-leak regression test from the previous compiler fix. Total suite: 591 unit tests, all passing. ## Golden drift The prologue change adds a few cycles to every function entry, which shifts NMI sampling by a handful of cycles and flips the audio-hash of any example that plays sfx or music (platformer, war). `arrays_and_functions.png` also picks up a 1-pixel shift in its enemy positions due to the same timing drift. All three golden updates are pure "compiler produces different but functionally-identical output" — no game behavior changed. ## What's still open in COMPILER_BUGS.md - §4: 8-sprites-per-scanline hardware limit is invisible to user code. A static analyzer hint could help; deferred. - §5: `inline` keyword is silently declined for short functions that the optimizer's inliner doesn't recognize (it only removes empty functions). Deferred pending a real single-return-expression inlining pass. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 20:33:41 +00:00
// 1 if A wins, 2 if B wins, 0 if they tie.
fun compare_cards(a: u8, b: u8) -> u8 {
var ra: u8 = card_rank(a)
var rb: u8 = card_rank(b)
if ra > rb {
2026-04-15 15:22:20 +00:00
return 1
}
compiler: fix three scoping bugs; war: revert all local/param workarounds Three related scoping bugs from examples/war/COMPILER_BUGS.md, all fixed in one pass because they're different layer manifestations of the same "flat global namespace" problem: ## §3: function-local `var` declarations lived in one namespace `src/analyzer/mod.rs::register_var` inserted every `var` it saw — top-level, state-local, AND function-body local — into the same `self.symbols: HashMap<String, Symbol>`. Two different functions declaring `var i` collided on E0501, which is why every local in war/*.ne had a function-prefix like `dfa_card` or `dwp_px`. Fix: add a `current_scope_prefix: Option<String>` to the Analyzer, set it to `Some("<fn_name>")` when checking a function body (or `Some("Title__frame")` for state handler bodies), and have `register_var` store the declaration under an internal key `"__local__{prefix}__{name}"`. New `resolve_symbol` / `resolve_key` helpers try the scope-qualified key first and fall back to the bare key for globals / consts / enum variants / state-level vars / function names. Every existing `self.symbols.get(name)` inside body-checking code was swapped over. Two `var i` declarations inside the SAME function body still collide with E0501 — we scoped per function body, not per nested block. Per-block scoping would require live-range analysis to reuse RAM slots. ## §1b: same-named params across functions shared VarIds `src/ir/lowering.rs::get_or_create_var` looked up names in a single global `var_map`, so two functions both with a `card: u8` parameter resolved to the same `VarId`. Whichever function was lowered last won the zero-page slot mapping, silently rerouting the other function's param reads to the wrong slot. Fix: the IR lowerer now mirrors the analyzer's scope logic. `LoweringContext` gains a `current_scope_prefix` field that gets set in `lower_function` / `lower_handler`, and `get_or_create_var` uses a new `scoped_key` helper that prepends `"__local__{prefix}__"` when the qualified key exists in `var_map` or `var_types`. Each function's parameters and locals therefore get distinct VarIds, and the codegen's `var_addrs` map naturally has no collisions. ## §2: param transport slots $04-$07 clobbered across nested JSRs Parameters were passed AND kept in `$04-$07` for the lifetime of a function. Any nested call overwrote those slots with its own arguments, so the caller's params were silently corrupted as soon as it invoked anything. Every war helper that took params and called other helpers (draw_card_face, push_back_a, etc) snapshotted its params into fresh locals at the top of the body. Fix: in `codegen/ir_codegen.rs::IrCodeGen::new`, every function-local — including parameters — now gets a dedicated per-function RAM slot at `$0300+`. Parameters are still passed via the zero-page transport slots `$04-$07` as the calling convention, but `gen_function` now emits a **prologue** at every function entry: LDA $04 STA <param_0_addr> LDA $05 STA <param_1_addr> ... etc, up to 4 ... By the time the body runs, every parameter lives in the function's dedicated RAM slot, so any nested call can freely clobber $04-$07 (writing its own arguments there) without corrupting the caller's saved parameters. Costs 4 LDA/STA pairs (≈ 20 bytes of ROM, 16 cycles) at every function entry — worth it to make the calling convention sound. ## War cleanup With all three fixes in place, every workaround prefix in `examples/war/*.ne` is gone: - `card_rank(card)` instead of `card_rank(crk_c)` — bug #1b - `compare_cards(a, b)` instead of `compare_cards(cmp_a, cmp_b)` - `push_back_a(card)` instead of `push_back_a(pba_in)` — bug #1b - `var card: u8 = draw_front_a()` in bury_from_* — bug #3 - `var i: u8 = 0` freely in multiple functions — bug #3 - `fun push_back_a(card)` body no longer snapshots `card` into `pba_card` before calling wrap52 — bug #2 - `fun draw_card_face` body no longer snapshots x/y/card into locals before calling card_rank/card_suit — bug #2 - `draw_word_player` steps its own x without needing a `dwp_px` accumulator to avoid the `x + N` arg compilation quirk — that quirk was a downstream symptom of bug #2 and is also gone The source is now about 300 lines shorter and significantly more readable. ## Regression tests Seven new tests nail these bugs down: - `analyzer::tests::analyze_allows_same_local_name_in_two_functions` - `analyzer::tests::analyze_allows_same_param_name_in_two_functions` - `analyzer::tests::analyze_allows_same_local_name_in_two_state_handlers` - `analyzer::tests::analyze_still_rejects_duplicate_local_in_same_function` - `codegen::ir_codegen::gen_function_prologue_spills_params_to_local_ram` Plus the four param-arity tests from the earlier E0506 fix and the wide_hi-leak regression test from the previous compiler fix. Total suite: 591 unit tests, all passing. ## Golden drift The prologue change adds a few cycles to every function entry, which shifts NMI sampling by a handful of cycles and flips the audio-hash of any example that plays sfx or music (platformer, war). `arrays_and_functions.png` also picks up a 1-pixel shift in its enemy positions due to the same timing drift. All three golden updates are pure "compiler produces different but functionally-identical output" — no game behavior changed. ## What's still open in COMPILER_BUGS.md - §4: 8-sprites-per-scanline hardware limit is invisible to user code. A static analyzer hint could help; deferred. - §5: `inline` keyword is silently declined for short functions that the optimizer's inliner doesn't recognize (it only removes empty functions). Deferred pending a real single-return-expression inlining pass. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 20:33:41 +00:00
if rb > ra {
2026-04-15 15:22:20 +00:00
return 2
}
return 0
}