You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vensim legitimately defines a subscripted variable on a subset of its range and never defines the rest. The MDL reader declares such a variable over the full parent dimension(s) and the compiler fills the uncovered elements with 0. In Vensim those elements do not exist: no zero, no error, and any equation that read them would itself be a Vensim error.
Concrete shapes, all from the checked-in corpus:
h[DimA] :EXCEPT: [SubA] = 8 with DimA: A1, A2, A3, SubA: A2, A3 -- only h[A1] is defined (test/sdeverywhere/models/except/except.mdl:20).
d[SubA, C1] = 4 -- only d[A2,C1] and d[A3,C1] are defined (except.mdl:16).
except3[DimE, DimF, DimG] :EXCEPT: [E2, F2, G2] = 3 -- 7 of 8 elements defined (except.mdl:36).
d[A1] = b[B1] and e[B1] = b[B1] -- one element each (test/sdeverywhere/models/subscript/subscript.mdl:12,14).
d[D1, DimB, DimC] = GET DIRECT CONSTANTS(...) -- one element of the first axis (test/sdeverywhere/models/directconst/directconst.mdl:31).
Ground truth
The Vensim .dat exports list exactly the defined elements and nothing else:
test/sdeverywhere/models/except/except.dat: h[A1] only (line 97; no h[A2], no h[A3]); d[A2,C1] and d[A3,C1] only (lines 29, 31); seven except3[...] rows with no except3[E2,F2,G2] (lines 45-57).
test/sdeverywhere/models/subscript/subscript.dat: d[A1] (line 25) and e[B1] (line 28) only.
test/sdeverywhere/models/directconst/directconst.dat: six d[D1,...] rows only (lines 21-31).
the LHS subranges are normalized to their parent dimensions before the variable is built (comment at ~line 608: "parent_dims are already normalized to parent dimensions when equations span different subranges"), so d[SubA, C1] is declared over DimA x DimC;
:EXCEPT: handling filters the excepted keys out of the element list (~lines 466-489) but keeps the full-dimension declaration, and the has_except_default comment (~line 615-618) states the resulting artifact as intent: "excepted elements should remain at 0 (undefined) rather than receiving the default."
The compiler then materializes a slot for every declared element and leaves the uncovered ones at 0. The corpus comparison never reads those elements (they are absent from the .dat), so nothing fails. The inline fixture in src/simlin-engine/tests/integration/simulate.rs (simulates_except_basic_mdl, ~line 2550-2559) asserts h[a2] == 0 and h[a3] == 0 with the message "h[A2] should be 0 (undefined)" -- that pins a Simlin artifact, not Vensim behavior.
Affected variables in the corpus: except.mdl d, except3, except4, f, g, h, k, o, p, q, r, u, w, x, y; subscript.mdl d, e; directconst.mdl d; plus the inline simulates_except_basic_mdl fixture's h.
Relationship to the B3 diagnostic on branch ltm-fidelity-post
Since the B3 fix on that branch (element-key owner), a declared element with no equation arm and no default raises ErrorCode::MissingElementEquation naming the uncovered elements. Its severity was deliberately kept at Warning (the model still compiles) precisely because of these Vensim corpus models -- the diagnostic is correct for a hand-authored XMILE model that forgot an element, but for an MDL import it is reporting a shape the reader itself fabricated. Closed #905 asked for that diagnostic; this issue is about the reader producing the wrong shape in the first place, which the diagnostic cannot fix.
Why it matters
Correctness / fidelity: Simlin invents values (0) for elements Vensim says do not exist. Any consumer that reads the variable's full extent -- SUM(h[DimA!]), a downstream apply-to-all equation over DimA, unit inference, LTM's element graph, the layout's per-element sparklines -- sees a shape and values Vensim never had. This is the silent-wrong-number class, hidden from the corpus comparison only because the comparison never reads the fabricated cells.
Diagnostics: every partially defined Vensim import now carries a MissingElementEquation warning that the modeler cannot act on, which teaches users to ignore that warning where it is genuinely useful.
src/simlin-engine/src/datamodel.rs -- Equation::Arrayed / variable dimensions (needs to be able to declare a variable over a covered subrange, including a synthesized one)
Consumers that assume a variable spans its declared dimensions: compiler element materialization, unit inference, LTM element graph, layout/sparklines, MDL writer
src/simlin-engine/tests/integration/simulate.rs -- simulates_except_basic_mdl (the h[a2]/h[a3] == 0 assertions encode the artifact)
Proposed fix
The MDL reader (and the datamodel) should declare a partially defined variable over the subrange its arms actually cover:
Compute the covered element set per axis from the LHS arms (explicit elements, subranges, and :EXCEPT: subtractions).
If the covered set per axis is a declared subrange (e.g. SubA for d[SubA, C1]), declare the variable over that subrange.
If it is not a declared subrange (e.g. h covers {A1}; except3 covers a non-rectangular 7 of 8), synthesize a subrange dimension for the covered set (a named subrange of the parent so mapping/aggregation against the parent still resolves), and declare over it. Non-rectangular coverage like except3 needs a decision: either a sparse-element shape in the datamodel, or the smallest rectangular cover plus a per-element existence mask. State the choice and its consumers explicitly.
The uncovered elements then do not exist in Simlin either: the compiler allocates no slot, MissingElementEquation no longer fires for imports, and every consumer sees the true shape.
Update simulates_except_basic_mdl to assert h[a2] / h[a3] are absent rather than 0, and add corpus-level coverage over except, subscript, and directconst that asserts the declared shape matches the .dat row set.
This changes variable shapes across consumers, so it is its own item and not part of the LTM fidelity branch. Cost estimate: medium (reader + datamodel subrange synthesis + tests over the except/subscript/directconst corpus models).
Context
Identified during the LTM fidelity work on branch ltm-fidelity-post (2026-09-08). LTM audit report B (arrays oracle) found the spaced-subscript silent-zero bug that led here; the B3 (element-key owner) commit on that branch references this issue. Nearest existing tracking: #905 (closed; asked for the missing-element diagnostic), #350 / #858 (MDL writer side of :EXCEPT:), #356 (default-fill heuristic), #651 (apply-to-all collapse in the same reader function).
Problem
Vensim legitimately defines a subscripted variable on a subset of its range and never defines the rest. The MDL reader declares such a variable over the full parent dimension(s) and the compiler fills the uncovered elements with 0. In Vensim those elements do not exist: no zero, no error, and any equation that read them would itself be a Vensim error.
Concrete shapes, all from the checked-in corpus:
h[DimA] :EXCEPT: [SubA] = 8withDimA: A1, A2, A3,SubA: A2, A3-- onlyh[A1]is defined (test/sdeverywhere/models/except/except.mdl:20).d[SubA, C1] = 4-- onlyd[A2,C1]andd[A3,C1]are defined (except.mdl:16).except3[DimE, DimF, DimG] :EXCEPT: [E2, F2, G2] = 3-- 7 of 8 elements defined (except.mdl:36).d[A1] = b[B1]ande[B1] = b[B1]-- one element each (test/sdeverywhere/models/subscript/subscript.mdl:12,14).d[D1, DimB, DimC] = GET DIRECT CONSTANTS(...)-- one element of the first axis (test/sdeverywhere/models/directconst/directconst.mdl:31).Ground truth
The Vensim
.datexports list exactly the defined elements and nothing else:test/sdeverywhere/models/except/except.dat:h[A1]only (line 97; noh[A2], noh[A3]);d[A2,C1]andd[A3,C1]only (lines 29, 31); sevenexcept3[...]rows with noexcept3[E2,F2,G2](lines 45-57).test/sdeverywhere/models/subscript/subscript.dat:d[A1](line 25) ande[B1](line 28) only.test/sdeverywhere/models/directconst/directconst.dat: sixd[D1,...]rows only (lines 21-31).What Simlin does today
src/simlin-engine/src/mdl/convert/variables.rs(build_variable_with_elements):d[SubA, C1]is declared overDimA x DimC;:EXCEPT:handling filters the excepted keys out of the element list (~lines 466-489) but keeps the full-dimension declaration, and thehas_except_defaultcomment (~line 615-618) states the resulting artifact as intent: "excepted elements should remain at 0 (undefined) rather than receiving the default."The compiler then materializes a slot for every declared element and leaves the uncovered ones at 0. The corpus comparison never reads those elements (they are absent from the
.dat), so nothing fails. The inline fixture insrc/simlin-engine/tests/integration/simulate.rs(simulates_except_basic_mdl, ~line 2550-2559) assertsh[a2] == 0andh[a3] == 0with the message "h[A2] should be 0 (undefined)" -- that pins a Simlin artifact, not Vensim behavior.Affected variables in the corpus:
except.mdld, except3, except4, f, g, h, k, o, p, q, r, u, w, x, y;subscript.mdld, e;directconst.mdld; plus the inlinesimulates_except_basic_mdlfixture's h.Relationship to the B3 diagnostic on branch
ltm-fidelity-postSince the B3 fix on that branch (element-key owner), a declared element with no equation arm and no default raises
ErrorCode::MissingElementEquationnaming the uncovered elements. Its severity was deliberately kept at Warning (the model still compiles) precisely because of these Vensim corpus models -- the diagnostic is correct for a hand-authored XMILE model that forgot an element, but for an MDL import it is reporting a shape the reader itself fabricated. Closed #905 asked for that diagnostic; this issue is about the reader producing the wrong shape in the first place, which the diagnostic cannot fix.Why it matters
SUM(h[DimA!]), a downstream apply-to-all equation overDimA, unit inference, LTM's element graph, the layout's per-element sparklines -- sees a shape and values Vensim never had. This is the silent-wrong-number class, hidden from the corpus comparison only because the comparison never reads the fabricated cells.MissingElementEquationwarning that the modeler cannot act on, which teaches users to ignore that warning where it is genuinely useful.:EXCEPT:lists from a full-dimension declaration plus an element list; a truthful declared shape makes the faithful re-emit straightforward.Component(s) affected
src/simlin-engine/src/mdl/convert/variables.rs--build_variable_with_elements(subrange-to-parent normalization,:EXCEPT:key filtering,has_except_default)src/simlin-engine/src/datamodel.rs--Equation::Arrayed/ variabledimensions(needs to be able to declare a variable over a covered subrange, including a synthesized one)src/simlin-engine/tests/integration/simulate.rs--simulates_except_basic_mdl(theh[a2]/h[a3] == 0assertions encode the artifact)Proposed fix
The MDL reader (and the datamodel) should declare a partially defined variable over the subrange its arms actually cover:
:EXCEPT:subtractions).SubAford[SubA, C1]), declare the variable over that subrange.hcovers{A1};except3covers a non-rectangular 7 of 8), synthesize a subrange dimension for the covered set (a named subrange of the parent so mapping/aggregation against the parent still resolves), and declare over it. Non-rectangular coverage likeexcept3needs a decision: either a sparse-element shape in the datamodel, or the smallest rectangular cover plus a per-element existence mask. State the choice and its consumers explicitly.MissingElementEquationno longer fires for imports, and every consumer sees the true shape.simulates_except_basic_mdlto asserth[a2]/h[a3]are absent rather than 0, and add corpus-level coverage overexcept,subscript, anddirectconstthat asserts the declared shape matches the.datrow set.This changes variable shapes across consumers, so it is its own item and not part of the LTM fidelity branch. Cost estimate: medium (reader + datamodel subrange synthesis + tests over the except/subscript/directconst corpus models).
Context
Identified during the LTM fidelity work on branch
ltm-fidelity-post(2026-09-08). LTM audit report B (arrays oracle) found the spaced-subscript silent-zero bug that led here; the B3 (element-key owner) commit on that branch references this issue. Nearest existing tracking: #905 (closed; asked for the missing-element diagnostic), #350 / #858 (MDL writer side of:EXCEPT:), #356 (default-fill heuristic), #651 (apply-to-all collapse in the same reader function).