diff --git a/crates/fidc-core/src/numeric_expr_vm.rs b/crates/fidc-core/src/numeric_expr_vm.rs index bfd4756..a854143 100644 --- a/crates/fidc-core/src/numeric_expr_vm.rs +++ b/crates/fidc-core/src/numeric_expr_vm.rs @@ -168,36 +168,40 @@ impl Program { Instruction::Push(value) => scratch.stack.push(value), Instruction::LoadVariable(index) => { let index = usize::from(index); - let cached = scratch.variables[index]; - let value = match cached { - Some(value) => value, - None => { - let expected_type = self.variable_types[index]; - let value = resolve(index, &self.variables[index], expected_type)?; - if value.value_type() != expected_type { - return Err(EvalError::new(format!( - "variable {} expected {:?}, got {:?}", - self.variables[index], - expected_type, - value.value_type() - ))); - } - scratch.variables[index] = Some(value); - value + let value = if scratch.variable_generations[index] == scratch.generation { + scratch.variables[index] + } else { + let expected_type = self.variable_types[index]; + let value = resolve(index, &self.variables[index], expected_type)?; + if value.value_type() != expected_type { + return Err(EvalError::new(format!( + "variable {} expected {:?}, got {:?}", + self.variables[index], + expected_type, + value.value_type() + ))); } + scratch.variables[index] = value; + scratch.variable_generations[index] = scratch.generation; + value }; scratch.stack.push(value); } Instruction::LoadLocal(index) => { let index = usize::from(index); - let value = scratch.locals[index].ok_or_else(|| { - EvalError::new(format!("local slot {index} was not initialized")) - })?; + if scratch.local_generations[index] != scratch.generation { + return Err(EvalError::new(format!( + "local slot {index} was not initialized" + ))); + } + let value = scratch.locals[index]; scratch.stack.push(value); } Instruction::StoreLocal(index) => { + let index = usize::from(index); let value = pop(&mut scratch.stack)?; - scratch.locals[usize::from(index)] = Some(value); + scratch.locals[index] = value; + scratch.local_generations[index] = scratch.generation; } Instruction::Unary(operator) => { let value = pop(&mut scratch.stack)?; @@ -260,8 +264,11 @@ impl Program { #[derive(Debug, Default)] pub(crate) struct Scratch { stack: Vec, - variables: Vec>, - locals: Vec>, + variables: Vec, + variable_generations: Vec, + locals: Vec, + local_generations: Vec, + generation: u32, } impl Scratch { @@ -273,10 +280,21 @@ impl Scratch { .len() .saturating_sub(self.stack.capacity()), ); - self.variables.clear(); - self.variables.resize(program.variables.len(), None); - self.locals.clear(); - self.locals.resize(program.local_count, None); + self.generation = self.generation.wrapping_add(1); + if self.generation == 0 { + self.variable_generations.fill(0); + self.local_generations.fill(0); + self.generation = 1; + } + if self.variables.len() < program.variables.len() { + self.variables + .resize(program.variables.len(), Value::Number(0.0)); + self.variable_generations.resize(program.variables.len(), 0); + } + if self.locals.len() < program.local_count { + self.locals.resize(program.local_count, Value::Number(0.0)); + self.local_generations.resize(program.local_count, 0); + } } } @@ -1362,6 +1380,27 @@ mod tests { } } + #[test] + fn scratch_generation_rollover_invalidates_cached_slots() { + let program = compile("left + right", |_| Some(ValueType::Number)).expect("compile"); + let mut scratch = Scratch::default(); + let first = program + .evaluate(&mut scratch, |_index, name, _expected| { + Ok(Value::Number(if name == "left" { 1.0 } else { 2.0 })) + }) + .expect("first evaluation"); + assert_eq!(first, Value::Number(3.0)); + + scratch.generation = u32::MAX; + let second = program + .evaluate(&mut scratch, |_index, name, _expected| { + Ok(Value::Number(if name == "left" { 10.0 } else { 20.0 })) + }) + .expect("evaluation after generation rollover"); + assert_eq!(second, Value::Number(30.0)); + assert_eq!(scratch.generation, 1); + } + #[test] fn matches_rhai_for_representative_numeric_boolean_corpus() { let source = r#"