Status
FIXED in commit 70f8f749 ("engine: represent Vensim :NA: as the finite sentinel -2^109, not NaN"). Filed for the record (an earlier filing attempt failed with an HTTP 529). One follow-on remains open (see "Remaining follow-on" below); leaving close/triage to the owner.
Problem
Vensim's :NA: keyword is not IEEE NaN. Per the Vensim documentation (vensim.com/documentation/na.html, dataequations.html) and the Ventana forum (t=4707), :NA: is an ordinary finite sentinel value, -2^109 (= -6.490371073168535e32). It is used to test for the existence of data via the idiom IF THEN ELSE(X = :NA:, ...) -- a plain equality-against-the-sentinel that only works because :NA: is finite (IEEE NaN never compares equal to anything, including itself, so the existence test would silently never fire under NaN).
Simlin previously mishandled :NA: in three inconsistent ways, none of which equalled -2^109:
- Expression
:NA: -> the literal string "NAN" (in mdl/xmile_compat.rs) -> Const(f64::NAN) -> IEEE NaN.
- Data-list
:NA: (inside a data equation's value list) -> NA_VALUE = -1e38 (in mdl/parser.rs).
- Neither path used the correct
-2^109 sentinel.
Under IEEE NaN, any unguarded arithmetic touching a :NA: value is irreversibly NaN (NaN is absorbing). This was the root cause of C-LEARN's 434 entirely-NaN core series (tracked under element-cycle-resolution AC7.3): a single :NA: poisoned downstream expressions instead of producing a finite, testable sentinel.
Why it matters
- Correctness / Vensim semantic fidelity: the
X = :NA: existence-test idiom is common in Vensim data models and is structurally broken under a NaN representation -- the equality silently never matches.
- Robustness: NaN propagation poisons whole series irreversibly; the finite sentinel keeps arithmetic finite and recoverable, matching Vensim runtime behavior.
- Consistency: three different in-engine representations of the same concept (
f64::NAN, -1e38, and the intended -2^109) is a latent correctness hazard.
The fix (commit 70f8f74)
A single canonical constant pub const NA: f64 = -6.490371073168535e32; was introduced in src/simlin-engine/src/float.rs, bit-pinned to -(2.0_f64).powi(109) by the test na_is_negative_two_pow_109 (-2^109 has a zero mantissa, so the spelled-out literal is bit-identical). Both :NA: paths (expression-literal and data-list) now route to this constant. Tests assert the existence-test semantics: approx_eq(NA, NA) is true (the test fires), approx_eq(NA, 0.0) and approx_eq(NA, -2^110) are false (a contaminated/doubled value does not spuriously match), and NA.is_finite() holds.
Structurally distinct NaN-producing paths were deliberately left untouched, because they are genuinely-NaN by Vensim semantics (not the :NA: sentinel):
- out-of-bounds -> NaN for VECTOR ELM MAP and range subscripts;
- empty-reducer -> NaN.
Remaining follow-on (do NOT conflate with this fix)
Vensim saves :NA: as a literal 0.0 to the VDF (verified by a raw-byte scan of test/xmutil_test_models/Ref.vdf). So the runtime sentinel (-2^109) vs. VDF-on-disk (0.0) reconciliation needed for the 1% numeric match in the test-comparison machinery is a separate follow-on (element-cycle-resolution Phase 7 / AC8.1). By design, the engine's runtime output keeps the -2^109 sentinel and does NOT map it to 0; only the test comparator must reconcile the two representations.
Components affected
src/simlin-engine/src/float.rs (canonical NA constant)
src/simlin-engine/src/mdl/xmile_compat.rs (expression :NA: path)
src/simlin-engine/src/mdl/parser.rs (data-list :NA: path)
src/simlin-engine/src/mdl/ast.rs
src/simlin-engine/src/mdl/reader.rs
Relationship to other issues
Distinct from #584 (INITIAL()-backed module/macro output omitted from initials runlist, spurious inf) and #585 (arrayed VECTOR SORT ORDER flat indices). Those are unrelated numeric/runlist issues; this is specifically the :NA: sentinel representation.
Discovery context
Identified during the C-LEARN hero-model work (branch clearn-hero-model) while diagnosing C-LEARN's all-NaN core series. The :NA: -> NaN mishandling was the root cause.
Status
FIXED in commit
70f8f749("engine: represent Vensim :NA: as the finite sentinel -2^109, not NaN"). Filed for the record (an earlier filing attempt failed with an HTTP 529). One follow-on remains open (see "Remaining follow-on" below); leaving close/triage to the owner.Problem
Vensim's
:NA:keyword is not IEEE NaN. Per the Vensim documentation (vensim.com/documentation/na.html, dataequations.html) and the Ventana forum (t=4707),:NA:is an ordinary finite sentinel value,-2^109(=-6.490371073168535e32). It is used to test for the existence of data via the idiomIF THEN ELSE(X = :NA:, ...)-- a plain equality-against-the-sentinel that only works because:NA:is finite (IEEE NaN never compares equal to anything, including itself, so the existence test would silently never fire under NaN).Simlin previously mishandled
:NA:in three inconsistent ways, none of which equalled-2^109::NA:-> the literal string"NAN"(inmdl/xmile_compat.rs) ->Const(f64::NAN)-> IEEE NaN.:NA:(inside a data equation's value list) ->NA_VALUE = -1e38(inmdl/parser.rs).-2^109sentinel.Under IEEE NaN, any unguarded arithmetic touching a
:NA:value is irreversibly NaN (NaN is absorbing). This was the root cause of C-LEARN's 434 entirely-NaN core series (tracked under element-cycle-resolution AC7.3): a single:NA:poisoned downstream expressions instead of producing a finite, testable sentinel.Why it matters
X = :NA:existence-test idiom is common in Vensim data models and is structurally broken under a NaN representation -- the equality silently never matches.f64::NAN,-1e38, and the intended-2^109) is a latent correctness hazard.The fix (commit 70f8f74)
A single canonical constant
pub const NA: f64 = -6.490371073168535e32;was introduced insrc/simlin-engine/src/float.rs, bit-pinned to-(2.0_f64).powi(109)by the testna_is_negative_two_pow_109(-2^109has a zero mantissa, so the spelled-out literal is bit-identical). Both:NA:paths (expression-literal and data-list) now route to this constant. Tests assert the existence-test semantics:approx_eq(NA, NA)is true (the test fires),approx_eq(NA, 0.0)andapprox_eq(NA, -2^110)are false (a contaminated/doubled value does not spuriously match), andNA.is_finite()holds.Structurally distinct NaN-producing paths were deliberately left untouched, because they are genuinely-NaN by Vensim semantics (not the
:NA:sentinel):Remaining follow-on (do NOT conflate with this fix)
Vensim saves
:NA:as a literal0.0to the VDF (verified by a raw-byte scan oftest/xmutil_test_models/Ref.vdf). So the runtime sentinel (-2^109) vs. VDF-on-disk (0.0) reconciliation needed for the 1% numeric match in the test-comparison machinery is a separate follow-on (element-cycle-resolution Phase 7 / AC8.1). By design, the engine's runtime output keeps the-2^109sentinel and does NOT map it to 0; only the test comparator must reconcile the two representations.Components affected
src/simlin-engine/src/float.rs(canonicalNAconstant)src/simlin-engine/src/mdl/xmile_compat.rs(expression:NA:path)src/simlin-engine/src/mdl/parser.rs(data-list:NA:path)src/simlin-engine/src/mdl/ast.rssrc/simlin-engine/src/mdl/reader.rsRelationship to other issues
Distinct from #584 (INITIAL()-backed module/macro output omitted from initials runlist, spurious inf) and #585 (arrayed VECTOR SORT ORDER flat indices). Those are unrelated numeric/runlist issues; this is specifically the
:NA:sentinel representation.Discovery context
Identified during the C-LEARN hero-model work (branch
clearn-hero-model) while diagnosing C-LEARN's all-NaN core series. The:NA:-> NaN mishandling was the root cause.