revert: reject marginal numeric VM slot reuse

This commit is contained in:
boris
2026-09-06 05:37:20 +08:00
parent 5122c73aa8
commit 52c7831bf6
2 changed files with 100 additions and 65 deletions
+16 -55
View File
@@ -168,9 +168,10 @@ impl Program {
Instruction::Push(value) => scratch.stack.push(value), Instruction::Push(value) => scratch.stack.push(value),
Instruction::LoadVariable(index) => { Instruction::LoadVariable(index) => {
let index = usize::from(index); let index = usize::from(index);
let value = if scratch.variable_generations[index] == scratch.generation { let cached = scratch.variables[index];
scratch.variables[index] let value = match cached {
} else { Some(value) => value,
None => {
let expected_type = self.variable_types[index]; let expected_type = self.variable_types[index];
let value = resolve(index, &self.variables[index], expected_type)?; let value = resolve(index, &self.variables[index], expected_type)?;
if value.value_type() != expected_type { if value.value_type() != expected_type {
@@ -181,27 +182,22 @@ impl Program {
value.value_type() value.value_type()
))); )));
} }
scratch.variables[index] = value; scratch.variables[index] = Some(value);
scratch.variable_generations[index] = scratch.generation;
value value
}
}; };
scratch.stack.push(value); scratch.stack.push(value);
} }
Instruction::LoadLocal(index) => { Instruction::LoadLocal(index) => {
let index = usize::from(index); let index = usize::from(index);
if scratch.local_generations[index] != scratch.generation { let value = scratch.locals[index].ok_or_else(|| {
return Err(EvalError::new(format!( EvalError::new(format!("local slot {index} was not initialized"))
"local slot {index} was not initialized" })?;
)));
}
let value = scratch.locals[index];
scratch.stack.push(value); scratch.stack.push(value);
} }
Instruction::StoreLocal(index) => { Instruction::StoreLocal(index) => {
let index = usize::from(index);
let value = pop(&mut scratch.stack)?; let value = pop(&mut scratch.stack)?;
scratch.locals[index] = value; scratch.locals[usize::from(index)] = Some(value);
scratch.local_generations[index] = scratch.generation;
} }
Instruction::Unary(operator) => { Instruction::Unary(operator) => {
let value = pop(&mut scratch.stack)?; let value = pop(&mut scratch.stack)?;
@@ -264,11 +260,8 @@ impl Program {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct Scratch { pub(crate) struct Scratch {
stack: Vec<Value>, stack: Vec<Value>,
variables: Vec<Value>, variables: Vec<Option<Value>>,
variable_generations: Vec<u32>, locals: Vec<Option<Value>>,
locals: Vec<Value>,
local_generations: Vec<u32>,
generation: u32,
} }
impl Scratch { impl Scratch {
@@ -280,21 +273,10 @@ impl Scratch {
.len() .len()
.saturating_sub(self.stack.capacity()), .saturating_sub(self.stack.capacity()),
); );
self.generation = self.generation.wrapping_add(1); self.variables.clear();
if self.generation == 0 { self.variables.resize(program.variables.len(), None);
self.variable_generations.fill(0); self.locals.clear();
self.local_generations.fill(0); self.locals.resize(program.local_count, None);
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);
}
} }
} }
@@ -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] #[test]
fn matches_rhai_for_representative_numeric_boolean_corpus() { fn matches_rhai_for_representative_numeric_boolean_corpus() {
let source = r#" let source = r#"
@@ -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"
}
}