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

IR codegen: allocate storage for function-local variables

Function bodies can declare local variables with \`var NAME: u8 = …\`.
Previously the lowering created a VarId for them but didn't track it
on the \`IrFunction.locals\` list, so the IR codegen had no address
to map it to and \`LoadVar\` / \`StoreVar\` silently did nothing. The
generated function body read and wrote random temp slots.

Fixes:

- Lowering: replaced the per-function \`locals\` local with a
  long-lived \`current_locals\` field; \`lower_function\` resets it
  on entry and moves it into the \`IrFunction\` at exit. Each
  \`Statement::VarDecl\` inside a function body appends to
  \`current_locals\`.
- IR codegen: iterate every function's \`locals\` list. Params 0..4
  still map to \$04-\$07, and the remaining locals get addresses in
  main RAM starting at \$0300. Each function's locals are disjoint,
  so nested calls don't corrupt each other's state.
- Integration test \`program_with_function_local_variables\`
  exercises nested calls with function-local state to guard against
  regression.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 17:01:12 +00:00
parent 240da57b54
commit 34b312537d
No known key found for this signature in database
3 changed files with 69 additions and 11 deletions

View file

@ -77,14 +77,29 @@ impl<'a> IrCodeGen<'a> {
}
}
// 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.
// parameter-passing slots $04-$07 (up to 4 params). Map the
// rest of each function's locals into main RAM starting at
// `$0300` (after the OAM buffer). Locals don't overlap with
// globals (which were placed by the analyzer) and are
// disjoint across functions so nested calls don't corrupt
// each other.
let mut local_ram_next: u16 = 0x0300;
// Advance past any global addresses so we don't clobber them.
// This is conservative: scan existing var_addrs for the max.
if let Some(max_global) = var_addrs.values().copied().max() {
if max_global >= local_ram_next {
local_ram_next = max_global + 1;
}
}
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);
for (i, local) in func.locals.iter().enumerate() {
if i < func.param_count {
if i < 4 {
var_addrs.insert(local.var_id, 0x04 + i as u16);
}
} else {
var_addrs.insert(local.var_id, local_ram_next);
local_ram_next += local.size.max(1);
}
}
}

View file

@ -24,6 +24,7 @@ struct LoweringContext {
current_blocks: Vec<IrBasicBlock>,
current_ops: Vec<IrOp>,
current_label: String,
current_locals: Vec<IrLocal>,
// Loop context for break/continue
loop_stack: Vec<LoopContext>,
// State metadata captured from the AST
@ -59,6 +60,7 @@ impl LoweringContext {
current_blocks: Vec::new(),
current_ops: Vec::new(),
current_label: String::new(),
current_locals: Vec::new(),
loop_stack: Vec::new(),
state_names: Vec::new(),
start_state: String::new(),
@ -208,12 +210,12 @@ impl LoweringContext {
fn lower_function(&mut self, fun: &FunDecl) {
self.next_temp = 0;
self.current_blocks = Vec::new();
let mut locals = Vec::new();
self.current_locals = Vec::new();
// Register parameters as locals
for param in &fun.params {
let var_id = self.get_or_create_var(&param.name);
locals.push(IrLocal {
self.current_locals.push(IrLocal {
var_id,
name: param.name.clone(),
size: type_size(&param.param_type),
@ -237,7 +239,7 @@ impl LoweringContext {
self.functions.push(IrFunction {
name: fun.name.clone(),
blocks: std::mem::take(&mut self.current_blocks),
locals,
locals: std::mem::take(&mut self.current_locals),
param_count: fun.params.len(),
has_return: fun.return_type.is_some(),
source_span: fun.span,
@ -307,8 +309,18 @@ impl LoweringContext {
fn lower_statement(&mut self, stmt: &Statement) {
match stmt {
Statement::VarDecl(var) => {
let var_id = self.get_or_create_var(&var.name);
// Track every local declared inside the current
// function so the IR codegen can allocate backing
// storage (e.g. RAM) for it.
if !self.current_locals.iter().any(|l| l.var_id == var_id) {
self.current_locals.push(IrLocal {
var_id,
name: var.name.clone(),
size: type_size(&var.var_type),
});
}
if let Some(init) = &var.init {
let var_id = self.get_or_create_var(&var.name);
let val = self.lower_expr(init);
self.emit(IrOp::StoreVar(var_id, val));
}

View file

@ -178,6 +178,37 @@ fn program_with_on_scanline_per_state() {
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_function_local_variables() {
// Functions with locally-declared variables should allocate
// their own backing storage and not corrupt caller state when
// nested.
let source = r#"
game "Locals" { mapper: NROM }
var out: u8 = 0
fun double(x: u8) -> u8 {
var t: u8 = x
t = t + t
return t
}
fun double_sum(a: u8, b: u8) -> u8 {
var s1: u8 = double(a)
var s2: u8 = double(b)
return s1 + s2
}
on frame {
out = double_sum(10, 20)
wait_frame
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_for_loop() {
let source = r#"