mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
Fixes the last two deferred compiler bugs catalogued in examples/war/COMPILER_BUGS.md, finishing the bug-cleanup arc on the War branch. Bug #5 — `inline fun` inliner Previously the `inline` keyword was parsed into `FunDecl.is_inline` and then dropped on the floor: every call site emitted a regular `JSR` through the $04-$07 transport slots. Now the IR lowerer captures inline function bodies up front in `LoweringContext::capture_inline_bodies` and rewrites call sites at lowering time. Two body shapes are supported: 1. Single-return expression — the body is re-lowered in place of the `Call` op with the parameter names substituted to fresh IR temps for each argument. 2. Void multi-statement body whose every statement is one of Assign/Call/Draw/Scroll/SetPalette/LoadBackground/WaitFrame/ Play/StartMusic/StopMusic/InlineAsm/RawAsm/DebugLog/DebugAssert — the statements are spliced into the caller's block with the same parameter substitution machinery. Control-flow-heavy inline bodies (conditional early returns, loops, transitions) fall back to a regular out-of-line call with no diagnostic. That's predictable and documented in the bug-tracking doc. Nested inline expansion uses a substitution-frame stack so an inline calling another inline sees the right arguments. A codegen follow-up was needed because bug #3's scope-qualified local names broke `{result}` substitution in inline asm. The codegen now tracks `current_fn_scope_prefix` per function and the InlineAsm op tries the qualified name first before falling back to the bare name. Bug #4 — W0109 sprite-per-scanline static check Adds a new warning code W0109 and an analyzer pass `check_sprite_scanline_budget` that walks each state's `on_frame` handler, collects literal-coordinate `draw` statements (including metasprite expansion via dx/dy offsets), and iterates scanlines 0..240 to count how many 8x8 sprites overlap each line. When a scanline has > 8, the analyzer emits W0109 with labels pointing at each offending draw site plus a help message about staggering y-rows and a note explaining the hardware dropout. Non-literal coordinates are skipped (static analysis can't resolve them). Nested `if`/`while`/`for`/`loop` blocks are unioned conservatively. Tests added src/ir/tests.rs - inline_fun_expression_body_emits_no_call_at_use_site - inline_fun_void_body_statements_are_spliced - inline_fun_with_conditional_return_compiles_as_regular_call - inline_fun_nested_inlines_substitute_correctly src/analyzer/tests.rs - analyze_sprite_scanline_budget_warns_over_eight - analyze_sprite_scanline_budget_ok_when_staggered - analyze_sprite_scanline_budget_skips_dynamic_coords - analyze_sprite_scanline_budget_expands_metasprites - analyze_sprite_scanline_budget_recurses_into_if COMPILER_BUGS.md Bugs #4 and #5 marked **FIXED** in the status table, with full reproduction/root-cause/fix/regression-test write-ups updated in place. All seven catalogued bugs now have shipped fixes. Artifact churn - examples/war.nes and examples/inline_asm_demo.nes rebuild byte-shifted (different JSR targets post-inliner). - tests/emulator/goldens/war.audio.hash shifts from 143660f to 13443e28 — the inliner removes JSRs to set_phase, which nudges NMI sampling timing. No pixel diff; behavior is unchanged. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
This commit is contained in:
parent
76dd8eacb0
commit
d6cb84a5bd
10 changed files with 1046 additions and 60 deletions
154
src/ir/tests.rs
154
src/ir/tests.rs
|
|
@ -801,3 +801,157 @@ fn wide_hi_does_not_leak_between_functions() {
|
|||
"wide CmpEq16 destination aliased a source operand — wide_hi leaked between functions"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_expression_body_emits_no_call_at_use_site() {
|
||||
// Regression test for COMPILER_BUGS.md §5: `inline fun`
|
||||
// with a single-return-expression body should be spliced
|
||||
// at every call site instead of emitting a Call op. The
|
||||
// lowered frame handler should contain zero Call ops
|
||||
// targeting the inline function.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
inline fun shift_right_4(c: u8) -> u8 {
|
||||
return c >> 4
|
||||
}
|
||||
var out: u8 = 0
|
||||
on frame { out = shift_right_4(0x90) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let any_call_to_inline = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "shift_right_4"));
|
||||
assert!(
|
||||
!any_call_to_inline,
|
||||
"frame handler should not contain a Call to the inlined function; ops: {:?}",
|
||||
frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_void_body_statements_are_spliced() {
|
||||
// Void `inline fun` with a multi-statement body (no
|
||||
// control flow) should be spliced at every statement-
|
||||
// context call site. `set_phase(P_FLY_A)` should lower
|
||||
// to two StoreVar ops (phase = P_FLY_A, phase_timer = 0)
|
||||
// rather than a Call op.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
const P_WAIT: u8 = 0
|
||||
const P_FLY: u8 = 1
|
||||
var phase: u8 = 0
|
||||
var phase_timer: u8 = 0
|
||||
inline fun set_phase(p: u8) {
|
||||
phase = p
|
||||
phase_timer = 0
|
||||
}
|
||||
on frame { set_phase(P_FLY) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let any_call_to_inline = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "set_phase"));
|
||||
assert!(
|
||||
!any_call_to_inline,
|
||||
"frame handler should not contain a Call to set_phase; ops: {:?}",
|
||||
frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_with_conditional_return_compiles_as_regular_call() {
|
||||
// A conditional early-return body (wrap52-style) is too
|
||||
// complex for the simple inliner. It should gracefully
|
||||
// fall back to a regular Call op — this is the intended
|
||||
// behaviour, not a bug. The important thing is that the
|
||||
// fallback is correct, not that it's inlined.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
inline fun wrap52(v: u8) -> u8 {
|
||||
if v >= 52 { return v - 52 }
|
||||
return v
|
||||
}
|
||||
var out: u8 = 0
|
||||
on frame { out = wrap52(60) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let calls_wrap52 = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "wrap52"));
|
||||
assert!(
|
||||
calls_wrap52,
|
||||
"wrap52 has conditional early return — it should fall back to a Call op"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_nested_inlines_substitute_correctly() {
|
||||
// Two inline functions where the outer calls the inner
|
||||
// using its own parameter. Both should inline; the
|
||||
// result should have no Call ops in the frame handler
|
||||
// targeting either function.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
inline fun double(x: u8) -> u8 { return x + x }
|
||||
inline fun quad(x: u8) -> u8 { return double(double(x)) }
|
||||
var out: u8 = 0
|
||||
on frame { out = quad(5) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let any_inline_call = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "double" || name == "quad"));
|
||||
assert!(
|
||||
!any_inline_call,
|
||||
"nested inline calls should both be expanded; frame ops: {:?}",
|
||||
frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue