Problem
LtmSyntheticVar::equation is a typed LtmEquation whose arms carry a parsed Expr0 AST (src/simlin-engine/src/db/ltm/equation.rs). Expr0::Const(String, f64, Loc) (src/simlin-engine/src/ast/expr0.rs:76) holds an f64, and LtmArm / LtmEquation get derived PartialEq and salsa::Update:
#[derive(Clone, PartialEq, salsa::Update)]
pub struct LtmArm { /* text: String, expr: Option<Expr0>, ... */ }
nan is a lexer keyword (("nan", Nan) in the KEYWORDS table, src/simlin-engine/src/lexer/mod.rs:86), so a generated LTM equation can legitimately embed f64::NAN. Since NaN != NaN, two arms built from identical text compare unequal.
Consequence: salsa cannot backdate the affected query result. link_score_equation_text_shaped re-runs and reports a changed value on every unrelated edit, so the expensive dependent compile_ltm_var_fragment (src/simlin-engine/src/db/ltm/compile.rs) re-executes even though the equation is semantically and textually identical. This is a performance/incrementality regression, not a wrong answer.
It also falsifies a claim in commit dea42188 ("engine: store LTM synthetic equations as typed ASTs"), whose message states: "Salsa incrementality is preserved: the shaped text query backdates on the PartialEq-equal typed equation, so unrelated edits reuse the expensive fragment (pinned by the existing per-link caching test)." That is true for ordinary equations and false for NaN-bearing ones -- worth noting so the next reader does not trust that sentence unconditionally.
Measured (verified by running code, not by inspection)
A throwaway probe constructing values through the public API (since removed, nothing committed):
LtmArm::new("NaN") parses successfully (expr.is_some() == true).
- Two
LtmArm::new("NaN") compare unequal (a == b is false).
- Two
LtmEquation::scalar("1 + NaN") compare unequal.
- Control: two
LtmEquation::scalar("1 + 2") compare equal.
- Critically,
<LtmEquation as salsa::Update>::maybe_update(...) returns true (CHANGED) for identical "1 + NaN" text -- so the actual backdating mechanism is affected, not merely PartialEq.
That last point matters for whoever fixes this: implementing only a manual PartialEq is insufficient, because salsa's derived Update compares fields independently.
Reachability
Narrow but legal. It needs a NaN literal in a user equation that feeds a scored LTM link. Note that Vensim's :NA: does not produce NaN in this engine -- crate::float::NA is the finite sentinel -2^109 (src/simlin-engine/src/float.rs:24, see also #586) -- so MDL imports are unaffected. A hand-written XMILE equation using NaN is the realistic path.
Given #977 (LTM link-score generation is already quadratic in an arrayed target's element count), losing fragment reuse on top of that is a real if narrow cost.
Components affected
src/simlin-engine/src/db/ltm/equation.rs (LtmArm, LtmEquation derived PartialEq / salsa::Update)
src/simlin-engine/src/db/ltm/compile.rs (link_score_equation_text_shaped, compile_ltm_var_fragment)
src/simlin-engine/src/ast/expr0.rs (Expr0::Const float equality)
Possible approaches
-
Compare on text alone for LtmArm. expr is a pure function of text (expr = Expr0::new(text), built together at construction and documented as never drifting -- see the LtmArm type docs), so text is the canonical identity and this is NaN-stable. But it requires hand-writing BOTH PartialEq and salsa::Update, and salsa::Update is an unsafe trait whose maybe_update contract, if got wrong, makes salsa MISS real changes -- stale results, a correctness bug strictly worse than the performance issue being fixed. So it deserves its own careful change with tests, not a drive-by.
-
Compare the f64 by bit pattern (to_bits) inside Expr0's equality. This fixes the problem generally for every Expr0 consumer rather than only LTM, but it changes equality semantics for +0.0/-0.0 too, so it needs its own analysis.
Relationship to #642
#642 covers the same root class (derived PartialEq over f64 defeating salsa backdating) for the compiled bytecode types (ByteCode.literals, ByteCodeContext.graphical_functions, vm::Specs). #642 argues the defect is benign there because the only consumer (compile_project_incremental) is non-tracked. That mitigation does not apply here: the consumer of the typed LtmEquation is a tracked query, so the lost backdating has an actually observable cost. The fix in approach 2 above would address both.
Severity
Low -- performance/incrementality only, narrow reachability. No wrong answers.
Context
Identified while reviewing the typed-equation storage introduced by #965 / commit dea42188. Cross-references: #642, #965, #977, #586.
Problem
LtmSyntheticVar::equationis a typedLtmEquationwhose arms carry a parsedExpr0AST (src/simlin-engine/src/db/ltm/equation.rs).Expr0::Const(String, f64, Loc)(src/simlin-engine/src/ast/expr0.rs:76) holds anf64, andLtmArm/LtmEquationget derivedPartialEqandsalsa::Update:nanis a lexer keyword (("nan", Nan)in the KEYWORDS table,src/simlin-engine/src/lexer/mod.rs:86), so a generated LTM equation can legitimately embedf64::NAN. SinceNaN != NaN, two arms built from identical text compare unequal.Consequence: salsa cannot backdate the affected query result.
link_score_equation_text_shapedre-runs and reports a changed value on every unrelated edit, so the expensive dependentcompile_ltm_var_fragment(src/simlin-engine/src/db/ltm/compile.rs) re-executes even though the equation is semantically and textually identical. This is a performance/incrementality regression, not a wrong answer.It also falsifies a claim in commit
dea42188("engine: store LTM synthetic equations as typed ASTs"), whose message states: "Salsa incrementality is preserved: the shaped text query backdates on the PartialEq-equal typed equation, so unrelated edits reuse the expensive fragment (pinned by the existing per-link caching test)." That is true for ordinary equations and false for NaN-bearing ones -- worth noting so the next reader does not trust that sentence unconditionally.Measured (verified by running code, not by inspection)
A throwaway probe constructing values through the public API (since removed, nothing committed):
LtmArm::new("NaN")parses successfully (expr.is_some() == true).LtmArm::new("NaN")compare unequal (a == bisfalse).LtmEquation::scalar("1 + NaN")compare unequal.LtmEquation::scalar("1 + 2")compare equal.<LtmEquation as salsa::Update>::maybe_update(...)returnstrue(CHANGED) for identical"1 + NaN"text -- so the actual backdating mechanism is affected, not merelyPartialEq.That last point matters for whoever fixes this: implementing only a manual
PartialEqis insufficient, because salsa's derivedUpdatecompares fields independently.Reachability
Narrow but legal. It needs a
NaNliteral in a user equation that feeds a scored LTM link. Note that Vensim's:NA:does not produce NaN in this engine --crate::float::NAis the finite sentinel-2^109(src/simlin-engine/src/float.rs:24, see also #586) -- so MDL imports are unaffected. A hand-written XMILE equation usingNaNis the realistic path.Given #977 (LTM link-score generation is already quadratic in an arrayed target's element count), losing fragment reuse on top of that is a real if narrow cost.
Components affected
src/simlin-engine/src/db/ltm/equation.rs(LtmArm,LtmEquationderivedPartialEq/salsa::Update)src/simlin-engine/src/db/ltm/compile.rs(link_score_equation_text_shaped,compile_ltm_var_fragment)src/simlin-engine/src/ast/expr0.rs(Expr0::Constfloat equality)Possible approaches
Compare on
textalone forLtmArm.expris a pure function oftext(expr = Expr0::new(text), built together at construction and documented as never drifting -- see theLtmArmtype docs), sotextis the canonical identity and this is NaN-stable. But it requires hand-writing BOTHPartialEqandsalsa::Update, andsalsa::Updateis anunsafetrait whosemaybe_updatecontract, if got wrong, makes salsa MISS real changes -- stale results, a correctness bug strictly worse than the performance issue being fixed. So it deserves its own careful change with tests, not a drive-by.Compare the
f64by bit pattern (to_bits) insideExpr0's equality. This fixes the problem generally for everyExpr0consumer rather than only LTM, but it changes equality semantics for+0.0/-0.0too, so it needs its own analysis.Relationship to #642
#642 covers the same root class (derived
PartialEqoverf64defeating salsa backdating) for the compiled bytecode types (ByteCode.literals,ByteCodeContext.graphical_functions,vm::Specs). #642 argues the defect is benign there because the only consumer (compile_project_incremental) is non-tracked. That mitigation does not apply here: the consumer of the typedLtmEquationis a tracked query, so the lost backdating has an actually observable cost. The fix in approach 2 above would address both.Severity
Low -- performance/incrementality only, narrow reachability. No wrong answers.
Context
Identified while reviewing the typed-equation storage introduced by #965 / commit
dea42188. Cross-references: #642, #965, #977, #586.