diff --git a/crates/fidc-core/src/numeric_expr_vm.rs b/crates/fidc-core/src/numeric_expr_vm.rs index a854143..bfd4756 100644 --- a/crates/fidc-core/src/numeric_expr_vm.rs +++ b/crates/fidc-core/src/numeric_expr_vm.rs @@ -168,40 +168,36 @@ impl Program { Instruction::Push(value) => scratch.stack.push(value), Instruction::LoadVariable(index) => { let index = usize::from(index); - 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() - ))); + 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 } - scratch.variables[index] = value; - scratch.variable_generations[index] = scratch.generation; - value }; scratch.stack.push(value); } Instruction::LoadLocal(index) => { let index = usize::from(index); - if scratch.local_generations[index] != scratch.generation { - return Err(EvalError::new(format!( - "local slot {index} was not initialized" - ))); - } - let value = scratch.locals[index]; + let value = scratch.locals[index].ok_or_else(|| { + EvalError::new(format!("local slot {index} was not initialized")) + })?; scratch.stack.push(value); } Instruction::StoreLocal(index) => { - let index = usize::from(index); let value = pop(&mut scratch.stack)?; - scratch.locals[index] = value; - scratch.local_generations[index] = scratch.generation; + scratch.locals[usize::from(index)] = Some(value); } Instruction::Unary(operator) => { let value = pop(&mut scratch.stack)?; @@ -264,11 +260,8 @@ impl Program { #[derive(Debug, Default)] pub(crate) struct Scratch { stack: Vec, - variables: Vec, - variable_generations: Vec, - locals: Vec, - local_generations: Vec, - generation: u32, + variables: Vec>, + locals: Vec>, } impl Scratch { @@ -280,21 +273,10 @@ impl Scratch { .len() .saturating_sub(self.stack.capacity()), ); - 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); - } + self.variables.clear(); + self.variables.resize(program.variables.len(), None); + self.locals.clear(); + self.locals.resize(program.local_count, None); } } @@ -1380,27 +1362,6 @@ 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#" diff --git a/docs/evidence/numeric-vm-binding-generation-rejection-20260906.json b/docs/evidence/numeric-vm-binding-generation-rejection-20260906.json new file mode 100644 index 0000000..4d5158a --- /dev/null +++ b/docs/evidence/numeric-vm-binding-generation-rejection-20260906.json @@ -0,0 +1,74 @@ +{ + "schemaVersion": "fidc-numeric-vm-binding-generation-rejection/v1", + "measuredAt": "2026-09-06T05:36:00+08:00", + "host": "192.168.31.177", + "baseline": { + "engineCommit": "5f08978", + "primaryFiveYearHotMedianEngineSeconds": 2.629, + "primaryFiveYearTotalReturn": 0.9219861819172002, + "primaryFiveYearTradeCount": 26088, + "primaryCanonicalSha256": "b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234", + "primaryResultStoreSha256": "9ccf0c0fc6f5d72e974381ad4cd09a80241d7de99f2649f2b01736f7c80dc2c7", + "profileInstructions": 16600366059, + "profileBranches": 2592214949 + }, + "compileTimeIdentifierBinding": { + "engineCommit": "2135a5b", + "implementation": "map every numeric VM identifier to a typed runtime enum during expression plan compilation", + "primaryFiveYearHotEngineSeconds": [2.608, 2.624, 2.651, 2.619, 2.781], + "primaryFiveYearHotMedianEngineSeconds": 2.624, + "observedMedianImprovementPercent": 0.190186, + "profileInstructions": 16360894880, + "instructionReductionPercent": 1.442566, + "secondaryFiveYearResultConsistent": true, + "secondaryFiveYearPerformanceExcluded": true, + "secondaryFiveYearExclusionReason": "the symbolic campaign entered a high-memory-bandwidth phase between the primary and secondary batches", + "netCodeLinesAdded": 562, + "retained": false + }, + "generationStampedScratchSlots": { + "engineCommit": "5122c73", + "implementation": "invalidate numeric VM variable and local slots with a generation counter instead of clearing Option arrays for each evaluation", + "localReleaseBenchmark": { + "baselineVmNanosecondsPerEvaluation": 86.992, + "candidateSamples": [83.284, 94.166, 86.275, 84.138, 85.453], + "candidateMedianNanosecondsPerEvaluation": 85.453, + "componentImprovementPercent": 1.769136, + "comparisonStrength": "weak because the baseline contains one sample" + }, + "primaryFiveYearHotEngineSeconds": [2.634, 2.636, 2.608, 2.642, 2.710], + "primaryFiveYearHotMedianEngineSeconds": 2.636, + "observedMedianRegressionPercent": 0.266261, + "profileInstructions": 16521146700, + "instructionReductionPercent": 0.477215, + "netCodeLinesAdded": 39, + "retained": false + }, + "hostLoad": { + "symbolicWorkersObserved": 3, + "symbolicWorkerCpuPercentApproximate": [720, 718, 698], + "symbolicWorkerRssBytesApproximate": [54479982592, 53353455616, 53941170176], + "wallTimeComparisonsAcrossPhasesExcluded": true + }, + "testGate": { + "workspacePassedBeforeFirstCandidateRejection": 539, + "workspacePassedForGenerationCandidate": 540, + "failed": 0, + "ignoredManualBenchmarks": 8 + }, + "remoteArtifacts": [ + "/srv/fidc/canonical/run/fidc-private/evidence/goal-primary-five-year-20260906.json", + "/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-primary-five-year-20260906.json", + "/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-secondary-five-year-20260906.json", + "/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-candidate-perf-stat-20260906.csv", + "/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-primary-five-year-20260906.json", + "/srv/fidc/canonical/run/fidc-private/evidence/identifier-binding-rollback-perf-stat-20260906.csv", + "/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-primary-five-year-20260906.json", + "/srv/fidc/canonical/run/fidc-private/evidence/vm-generation-candidate-perf-stat-20260906.csv" + ], + "decision": { + "status": "rejected_and_removed", + "reason": "both candidates preserved exact business results but failed to produce a material, stable end-to-end improvement; the typed binding added disproportionate code and the generation slots slightly regressed the five-year median", + "nextTarget": "profile and specialize the numeric VM instruction dispatch or runtime helper execution without changing expression, PIT, or lazy short-circuit semantics" + } +}