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

IR codegen: fix function call target label + parameter passing

Two bugs in the IR codegen's function call path:

1. \`IrOp::Call\` was emitting \`JSR __fn_name\` but functions are
   defined at \`__ir_fn_name\`. The JSR went to an unresolved label
   and the generated ROM would jump to garbage.

2. Caller stored arguments to ZP \$04-\$07 but the callee's
   \`LoadVar\` for each parameter VarId resolved to a temp slot at
   \$80+ because parameter VarIds weren't in \`var_addrs\`. The
   callee never read the arguments.

Fixes:

- Register each function's first four parameter VarIds at
  \$04-\$07 in \`var_addrs\` during codegen construction, so
  \`LoadVar\` for a parameter emits \`LDA \$04\` / \`\$05\` / etc.
- Call emits \`JSR __ir_fn_{name}\` matching the function label.

Adds a unit test that exercises both sides of the contract:
the caller must STA to \$04/\$05 and JSR to \`__ir_fn_sum\`, and the
callee must LDA from \$04 to read the first parameter.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 16:33:06 +00:00
parent 08322abda4
commit 1778ae0ec8
No known key found for this signature in database

View file

@ -76,6 +76,18 @@ impl<'a> IrCodeGen<'a> {
var_addrs.insert(global.var_id, alloc.address);
}
}
// Map each function's parameter VarIds to the zero-page
// parameter-passing slots $04-$07 (up to 4 params). The
// caller writes arguments to these slots before JSR and the
// callee reads them via `LoadVar(t, VarId)` through the same
// mapping.
for func in &ir.functions {
for (i, local) in func.locals.iter().take(func.param_count).enumerate() {
if i < 4 {
var_addrs.insert(local.var_id, 0x04 + i as u16);
}
}
}
let function_names = ir.functions.iter().map(|f| f.name.clone()).collect();
Self {
instructions: Vec::new(),
@ -473,11 +485,15 @@ impl<'a> IrCodeGen<'a> {
}
}
IrOp::Call(dest, name, args) => {
for (i, arg) in args.iter().enumerate() {
// Pass up to 4 arguments via zero-page slots $04-$07.
// Arguments beyond the fourth are silently dropped
// (the analyzer has already validated arity against
// the declared signature).
for (i, arg) in args.iter().enumerate().take(4) {
self.load_temp(*arg);
self.emit(STA, AM::ZeroPage(0x04 + i as u8));
}
self.emit(JSR, AM::Label(format!("__fn_{name}")));
self.emit(JSR, AM::Label(format!("__ir_fn_{name}")));
if let Some(d) = dest {
// Return value is in A
self.store_temp(*d);
@ -881,6 +897,39 @@ mod more_tests {
assert!(writes_slot1, "second draw should use OAM slot 1");
}
#[test]
fn ir_codegen_function_call_uses_correct_label_and_params() {
let insts = lower_and_gen(
r#"
game "T" { mapper: NROM }
fun sum(a: u8, b: u8) -> u8 { return a + b }
var x: u8 = 0
on frame { x = sum(3, 4) }
start Main
"#,
);
// Caller must store arg0 to $04 and arg1 to $05.
let writes_arg0 = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(0x04));
let writes_arg1 = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(0x05));
assert!(writes_arg0, "caller should store arg0 to $04");
assert!(writes_arg1, "caller should store arg1 to $05");
// Caller must JSR to __ir_fn_sum (not __fn_sum).
let has_jsr = insts
.iter()
.any(|i| i.opcode == JSR && i.mode == AM::Label("__ir_fn_sum".into()));
assert!(has_jsr, "caller should JSR to __ir_fn_sum");
// Callee must read parameters from $04 and $05, not from
// temp slots $80+.
let has_param_read = insts
.iter()
.any(|i| i.opcode == LDA && i.mode == AM::ZeroPage(0x04));
assert!(has_param_read, "callee should read parameters from $04");
}
#[test]
fn ir_codegen_scanline_emits_mmc3_setup_and_irq_user() {
let insts = lower_and_gen(