mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
ir: substitute inline-asm {param} at splice time for constant args
`inline fun` worked for plain-NEScript bodies but blew up on any
function that used inline asm with `{name}` substitution. The
codegen's `substitute_asm_vars` runs against the analyzer's
per-function scope; after splicing, the scope is the *caller*,
where the inlined fun's parameters don't exist. The result was
`{dst}` left as a literal token in the spliced asm body, and
the asm parser failing with `bad number `{dst}``.
Fix: at inline-expansion time (`try_inline_call_stmt`), when the
splicer detects a `Statement::InlineAsm` in the body and the
call site passed a compile-time constant for a parameter,
pre-substitute `{param}` with `#$<value>` so the spliced body
parses as an immediate-mode operand. The substitution is done
via a new `inline_const_args_stack` parallel to the existing
`inline_subs_stack`, populated from the args' `eval_const`
results.
When *any* arg is non-constant the splicer refuses to inline an
asm-containing function and falls back to a regular `Call` op —
preserving correctness, just at the cost of the JSR/RTS apparatus
the user was hoping to avoid. The fallback is exercised by the
new `inline_fun_with_asm_falls_back_for_runtime_arg` test.
`eval_const` is also extended to consult the same const-args
stack, so a nested inline like `inline_outer(K)` →
`inner(outer_param)` correctly recognises `outer_param` as the
constant `K` and recurses the substitution. Without this, the
cascade stopped at the first level.
Two new tests in `src/ir/tests.rs` lock in the behaviour:
- `inline_fun_with_asm_param_substitutes_immediate` — verifies
`{param}` becomes `#$<value>` in the spliced body and no
Call op is left.
- `inline_fun_with_asm_falls_back_for_runtime_arg` — verifies
the fallback path emits a Call op.
The SHA-256 example doesn't itself opt into the new feature for
its primitives (full inlining would balloon ROM by 5-10 KB —
the call sites add up fast at 1500+ source-level uses); that's
left as a future opportunity. Hash output unchanged; emulator
harness 34/34; reproducibility diff clean.
https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
This commit is contained in:
parent
f2623cb62b
commit
4afd196d1e
2 changed files with 218 additions and 3 deletions
|
|
@ -921,6 +921,99 @@ fn inline_fun_with_conditional_return_compiles_as_regular_call() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_with_asm_param_substitutes_immediate() {
|
||||
// An `inline fun` whose body contains an `asm { ... }` block
|
||||
// referencing parameters via `{name}` substitution. When the
|
||||
// call site passes compile-time constants, the splicer
|
||||
// pre-substitutes each `{param}` with its `#$<value>`
|
||||
// immediate so the spliced body parses correctly. The end
|
||||
// result should contain no Call op for the inlined fun, and
|
||||
// the frame handler's instruction stream (after lowering) is
|
||||
// expected to contain an `LDA #$2A` (= 42) — the substituted
|
||||
// immediate.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var sink: u8 = 0
|
||||
inline fun stash(value: u8) {
|
||||
asm {
|
||||
LDA {value}
|
||||
STA {sink}
|
||||
}
|
||||
}
|
||||
on frame { stash(42) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let any_call = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "stash"));
|
||||
assert!(!any_call, "stash should be inlined, no Call op left");
|
||||
let asm_body = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.find_map(|op| match op {
|
||||
IrOp::InlineAsm(body) => Some(body.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("inlined asm block should be present");
|
||||
assert!(
|
||||
asm_body.contains("#$2A"),
|
||||
"asm body should contain the substituted immediate `#$2A`; got: {asm_body}"
|
||||
);
|
||||
assert!(
|
||||
!asm_body.contains("{value}"),
|
||||
"`{{value}}` should have been pre-substituted; got: {asm_body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_with_asm_falls_back_for_runtime_arg() {
|
||||
// When the call site passes a runtime value (a variable, not
|
||||
// a literal), there's no way to synthesize an immediate at
|
||||
// expansion time. The splicer should refuse to inline and
|
||||
// emit a regular Call instead — preserving correctness over
|
||||
// performance.
|
||||
let ir = lower_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var sink: u8 = 0
|
||||
var src: u8 = 7
|
||||
inline fun stash(value: u8) {
|
||||
asm {
|
||||
LDA {value}
|
||||
STA {sink}
|
||||
}
|
||||
}
|
||||
on frame { stash(src) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
let frame_fn = ir
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.name.contains("frame"))
|
||||
.expect("frame handler should exist");
|
||||
let any_call = frame_fn
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::Call(_, name, _) if name == "stash"));
|
||||
assert!(
|
||||
any_call,
|
||||
"stash should fall back to a Call op when arg is runtime"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fun_nested_inlines_substitute_correctly() {
|
||||
// Two inline functions where the outer calls the inner
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue