Skip to content

engine: close the LTM mapped-dimension and array-operand gaps (#995, #996, #997) - #1010

Open
bpowers wants to merge 8 commits into
mainfrom
ltm-burndown-995-996-997
Open

engine: close the LTM mapped-dimension and array-operand gaps (#995, #996, #997)#1010
bpowers wants to merge 8 commits into
mainfrom
ltm-burndown-995-996-997

Conversation

@bpowers

@bpowers bpowers commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes out the LTM mapped-dimension and array-operand gap cluster in four commits, each implemented and then adversarially reviewed (with mutation testing) to convergence before landing.

Fixes #995
Fixes #996
Fixes #997

1. Pin executed mapped-reference semantics; close out #996 (dc86dae)

Before touching any describer, mapped_reference_semantics_tests pins what the engine executes for a reference across mapped dimensions: 48 cells over reference spelling (iterated-dim subscript, source-own-dim subscript, bare-in-equation, stock-flow wiring) x mapping kind (positional, permuted element map, many-to-one, reverse-cardinality, shared-element-names, no-mapping controls) x both declaration directions, each asserted against the VM with the competing rules' answers shown absent. Three resolution rules fall out, not two: iterated and bare-in-equation spellings are positional; source-own-dim and stock-flow spellings resolve name-first, then through the declared element map. The stock-flow spelling and the name-first precedence were both previously undocumented. The mapped_element_correspondence Vensim claims are now quoted from the raw subscript-mapping reference page (Example 3 refuted two of the old rustdoc's claims; the review caught that).

For #996: the allocator restaging itself landed inside #994's merge, so this records the remaining evidence -- get_implicit_subscripts reachability measured and documented (exactly two production callers, both wiring), the end-to-end hazard fixture the_996_hazard_shape_compiles_and_reads_name_first pinning name-first allocation numerically with non-canonical dimension names, and the C-LEARN re-measure showing the 135 mapping-order declines resolved (residual declines are single-axis: no ordering hazard).

2. Thread the reference spelling through the LTM describers -- #997 (a0b513c)

The executed name/element-map/mapped-parent resolution becomes one function, DimensionsContext::resolve_mapped_read, called by all three compiler sites (a divergent no-name-first arm and a dead numeric fallback are gone). On top of it the correspondence API is spelling-keyed: positional_correspondence, executed_read_correspondence, and bare_reference_correspondence (the union RefShape::Bare needs, since a structural flow-to-stock edge and an in-equation bare reference resolve by different rules). Element edges emit the union -- never fewer edges than execution reads -- while the arrayed link-score retarget is gated on the union being a singleton per target element, so where the two rules disagree the edge keeps its loud skip rather than scoring a phantom from-node with the real edge's series (the review demonstrated that hazard and the gate's test pins it). Classification gains AxisRead::MappedRead for the source-own-dim spelling, consumed through the shared row derivations by the element graph, the per-element link scores, and both pin paths.

On C-LEARN the 13 remaining unprojectable-dep declines (five X Aggregated[Aggregated Regions] deps across the 3-onto-7 element map) all become scored per-element link scores: +91 vars, one scalar slot each. simulates_clearn (real Vensim ground truth) and the whole heavy tier stay green.

3. Materialize computed array operands -- #995, compiler half (295a927)

out[d] = VECTOR SORT ORDER(vals[d] * 2, 1) failed to compile in ordinary models with LTM off. The issue's suggested Pass-1 relaxation rests on a wrong diagnosis (measured: the type checker bounds an A2A elementwise Op2 as scalar, so needs_decomposition declines before the deferral it blamed is consulted); the fix is a last lowering pass, compiler/array_operand.rs, running on the fully lowered fragment where every view is concrete. It rewrites exactly the operands codegen would reject, so every previously-compiling fragment is byte-identical: zero golden churn, max temps-per-fragment unchanged. An operand containing an array-valued PREVIOUS/INIT subexpression declined loudly (materializing it would broadcast one element's previous value -- wrong numbers where HEAD failed loudly; the review caught this before it shipped). Also made loud: a static view over a temp id past the u8 TempId namespace, which previously wrapped modulo 256 and silently returned a different array from element 128 on -- a pre-existing #583 instance reproduced at the base commit. New corpus fixture test/vector_computed_operand with hand-derived expectations gates the shapes through VM, both round-trips, and wasm parity.

4. Array PREVIOUS/INIT as snapshot views -- #995, option D (1ff773b)

The view records' is_temp bool becomes ViewStorage { Curr, Temp, Prev, Initial } with distinct symbolic PrevVar/InitialVar bases, and both backends read views through the base. First-step semantics are branches mirroring the scalar opcodes (a Prev view reads the fallback while use_prev_fallback is set; an Initial view reads curr during Initials) -- load-bearing on wasm, whose reset does not zero snapshot regions, pinned by a repeated-run parity test. builtins_visitor passes array-shaped PREVIOUS/INIT arguments through to lowering, where the operand position is known; the phase-3 nested-operand decline is replaced by correct materialization. Every row of #995's original table now compiles with numbers pinned against per-element capture oracles on time-varying fixtures, including the PR #1001 user shape VECTOR SELECT(sel[Row,*], PREVIOUS(matrix[Row,*]), ...). Loud declines where the view cannot be right: non-default array PREVIOUS fallback, PREVIOUS/INIT of a temp, and ALLOCATE AVAILABLE's priority-profile position (whose expander would otherwise allocate over one column -- the review measured wrong allocations and the decline restores HEAD's behavior). The dep graph classifies the new bases like their scalar twins, each arm pinned by a fixture its mutation kills. C-LEARN is byte-identical across all four LTM harnesses; the LTM freeze helpers stay (their axis-qualified rows are still load-bearing for non-prefix subdimensions), with the retire-onto-views simplification assessed as follow-on in the freeze module's doc.

Verification

Every commit passed the full pre-commit gauntlet (fmt, clippy, cargo test, TS lint/build/tsc/test, wasm build, pysimlin). Heavy C-LEARN tier green at each phase (simulates_clearn, simulates_clearn_wasm, clearn_pinned_climate_loop_is_scored, discovery_clearn_matches_vm_wasm, ...). C-LEARN LTM state on the branch tip: 0 unprojectable-dep declines (was 13), 0 failing LTM fragments, 5 rank-like declines (deliberate, the #995 option-C semantic trap staying closed), 6848 LTM vars / 29808 slots.

Known residuals, disclosed in-tree rather than deferred silently

5. Review response -- the three Codex P1s, and the bug beneath one (1510a59)

All three findings were reproduced before fixing (branch also rebased onto the salsa-0.28 main).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ff773b4c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if is_snapshot_view(&operand) {
return operand;
}
let Some(source_view) = super::find_expr_array_view(&operand) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive materialized shape from all array operands

When a computed operand broadcasts a narrower left-hand array into a wider one—for example, VECTOR SORT ORDER(small[A] + wide[A,B], 1)find_expr_array_view returns the first view it encounters, so this creates a temporary shaped only [A]. A production-path reproduction returned [0,0,0,1,1,1], while commuting the addition to wide[A,B] + small[A] produced the correct [0,1,2,0,1,2]. Because addition is commutative and subset broadcasting is supported, operand order now silently changes simulation results; derive the computed expression's merged output dimensions and cover both operand orders.

AGENTS.md reference: AGENTS.md:L97-L99

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, reproduced, and fixed in 1510a59. The mechanism was worse than an [A]-shaped truncation: the narrow temp's iteration found no match for the wide array's extra axis and read NaN for every element, so the wrong order returned the sort of three NaNs. find_expr_array_view now joins every array-shaped subexpression by a maximality filter over dim names/sizes (order-independence is structural, not accumulated), the If condition is visited (it was previously skipped entirely -- a wider condition silently collapsed the IF to its ELSE arm), and incomparable shapes, axis-order ties, and repeated-dimension views decline loudly rather than guessing. Both operand orders of your reproduction now agree, pinned with numeric tests plus a three-array permutation row; instrumented blast-radius probes measured zero divergence from the old answer across the corpus and the C-LEARN heavy tier.

Comment on lines +437 to +439
Some((
corr.iter().map(|e| e.as_str().to_string()).collect(),
Some(slots),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep repeated mapped axes on the same target coordinate

When two source axes both map to the same active target dimension—for example, target[State] = matrix[Region1,Region2] with both region dimensions mapped to State—execution resolves both indices from the same current State element and reads only matrix[map1(s),map2(s)]. This arm returns each MappedRead axis as an independent list, and the generic product below combines them cartesianly, so the element graph also emits cross rows such as matrix[map1(s1),map2(s2)] that execution never reads; downstream target projection uses only the first duplicate slot coordinate, creating phantom causal edges and false candidate loops. Zip axes sharing dim, or decline this repeated projection before returning rows.

AGENTS.md reference: AGENTS.md:L97-L99

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real and fixed in 1510a59, with one correction to the report: the cross rows never became link scores (the score-name derivation is keyed by a dim-name map, so it always produced the diagonal) -- the damage was phantom element EDGES and loop candidates, 9 where execution reads 3, with names and edges describing different graphs. read_slice_row_parts now enumerates drivers: axes naming the same target dimension share one enumerated coordinate (zip, not product), preserving row order for every unrepeated slice; an all-Iterated subscript repeating a target dimension also retargets Bare -> PerElement when the target names that dimension once. The repeated-TARGET sibling (cube[D1,D1]) deliberately keeps Bare -- retargeting flips its score surface to the loud square-source skip, and expand_same_element's name-keyed target positions carry both phantom and missing edges there either way; that residual is pinned in both directions as a stated follow-up rather than absorbed here.

Comment on lines +1165 to +1166
SymStaticViewBase::PrevVar(var_ref) => (resolve_var(var_ref)?, ViewStorage::Prev),
SymStaticViewBase::InitialVar(var_ref) => (resolve_var(var_ref)?, ViewStorage::Initial),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Relocate snapshot views for nested module instances

When an array-valued PREVIOUS or INIT is compiled inside a nested module, resolve_var returns an offset relative to that module's body layout, but these new variants become absolute Prev/Initial static views. Vm::PushStaticView and wasm's PrevAbsolute/InitialAbsolute loads never add the runtime module_off, unlike scalar LoadPrev/LoadInitial, so an instance reads root or sibling snapshot slots instead of its own array; multiple instances therefore cannot produce independent results. Preserve a module-relative snapshot base through resolution, or apply module_off when the view is pushed or loaded.

AGENTS.md reference: src/simlin-engine/AGENTS.md:L31-L31

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real -- and bigger than reported. Reproducing it showed the Curr control aliasing too: PushStaticView never applied module_off for ANY region, on main before this branch, on both backends identically (the wasm lowering documented the VM verbatim, which is why the parity gate never caught it; reads from the root were correct because module_off is 0 there, which is why the existing cross-module array tests passed). So every array reducer, vector op, and arrayed GF evaluated INSIDE a module instance read the root's storage -- the snapshot views inherited the defect rather than introduced it. Fixed in 1510a59 for all regions: StaticArrayView::to_runtime_view takes the executing instance's base for Curr/Prev/Initial and not Temp (per-evaluation shared storage), the wasm ViewBase arms are renamed for their region with the absolute/relative split gone, and two-instance plus nested two-hop fixtures pin instance addressing on both backends (the nested one kills a last-hop-only mutation the one-hop fixture cannot see).

bpowers added 5 commits August 2, 2026 09:18
…questions

Before any LTM describer changes for GH #997, pin what the engine
actually executes for a reference across mapped dimensions. The new
mapped_reference_semantics_tests module enumerates reference spelling
(iterated-dim subscript, source-own-dim subscript, bare-in-equation,
stock-flow wiring) x mapping kind (positional, permuted element map,
many-to-one, reverse-cardinality, shared-element-names, no-mapping
controls) x both declaration directions, every cell asserted against the
VM with the competing rules' answers shown absent. Three resolution
rules fall out, not two: the iterated and bare-in-equation spellings are
positional (lower_pass0 rewrites a bare ref into the iterated spelling
before anything can consult a map), while the source-own-dim and
stock-flow spellings resolve name-first and only then through the
declared element map. The stock-flow spelling and the name-first
precedence were both previously undocumented; the
mapped_element_correspondence rustdoc now records them, and its Vensim
claims are quoted from the raw subscript-mapping reference page instead
of a paraphrase (Example 3 turns out to refute the old doubt about the
iterated spelling's legality, while still not settling positional vs
map-following, which stays marked unverified).

For GH #996: the allocator restaging itself landed with GH #994's merge,
so this records the remaining evidence. get_implicit_subscripts'
reachability is measured and documented (exactly two production callers,
both wiring; ordinary expressions never arrive), and the end-to-end
hazard fixture the_996_hazard_shape_compiles_and_reads_name_first pins
name-first allocation numerically on the executed path, using
non-canonical dimension names so a canonicalization slip cannot pass
vacuously. C-LEARN re-measure: the 135 mapping-order declines are gone;
the residual declines are single-axis (no ordering hazard), attributable
to the element-map decline (GH #997) and rank-like partials (GH #995).
TestProject gains named_dimension_with_mappings for the multi-target
dimension the hazard shape needs.
…ibers

An LTM describer must resolve a mapped-dimension reference the way the
executed simulation does, and the execution rule depends on the
reference site's SPELLING, which the pair-keyed
mapped_element_correspondence could not see -- so it declined every
explicit element map and class-D edges (a dep read through an
element-mapped axis) were declined loudly instead of scored (GH #997).

The executed name-then-element-map-then-mapped-parent resolution is now
one function, DimensionsContext::resolve_mapped_read, called by all
three compiler sites (get_implicit_subscript_off, build_view_from_ops,
and the dynamic IndexExpr3::Dimension arm, whose divergent no-name-first
pairing and dead numeric fallback are gone). On top of it the
correspondence API is spelling-keyed: positional_correspondence for the
iterated and bare-in-equation spellings (now returning the positional
diagonal for an element-mapped pair, which is what execution does),
executed_read_correspondence for the source-own-dim and stock-flow
spellings (name-first, any cardinality), and
bare_reference_correspondence, the union RefShape::Bare needs because a
structural flow-to-stock edge and an in-equation bare reference resolve
by different rules. Element edges emit the union (never fewer edges than
execution reads); the arrayed link-score retarget is gated on the union
being a singleton per target element (mapped_pair_projects_uniquely), so
where the two rules disagree the edge keeps its loud skip rather than
scoring a phantom from-node with the real edge's series.

Classification gains AxisRead::MappedRead / OccurrenceAxis::MappedRead
for a subscript naming a non-active dimension resolvable through exactly
one target-iterated dim (ambiguity declines to the conservative
cross-product, with an attribution control); the row derivations,
per-element link scores, and both pin paths consume it through the
shared derivations -- dep_element_pins now carries both spellings' rows
per dep, since one table serves a map-following subscripted reference
and a positional bare one. On C-LEARN the 13 remaining unprojectable-dep
declines (five X-Aggregated deps across the 3-onto-7 element map) all
become scored per-element link scores (+91 vars, one scalar slot each);
simulates_clearn and the whole heavy tier stay green, and the Phase A
semantics matrix is unmoved.
Codegen requires an array-valued operand of a vector builtin to be a
view over storage, and any computed array must have been moved into an
AssignTemp earlier in the fragment -- but nothing did that for the
apply-to-all spellings, so ordinary hand-written models like
out[d] = VECTOR SORT ORDER(vals[d] * 2, 1) failed to compile with LTM
off (the compiler half of GH #995; the LTM half landed with GH #1003's
freeze helpers). The issue's suggested Pass-1 relaxation turns out to
rest on a wrong diagnosis, established by instrumenting real lowered
fragments: the type checker bounds an A2A elementwise Op2 as SCALAR, so
needs_decomposition declines before the has_a2a defer is ever consulted,
and an Expr3-level hoist would collapse the operand per element (the
AssignTemp arm lowers without wildcard promotion). The fix is instead a
last lowering pass, compiler/array_operand.rs, run at the Var::new
chokepoint on the fully lowered fragment where every view is already
concrete: it rewrites exactly the operands codegen would reject (the
negation of walk_expr_as_view's accepting arms), so every fragment that
compiled before is byte-identical -- zero golden churn, max
temps-per-fragment unchanged at 21 across the corpus and C-LEARN. The
Rank arm in Pass 1 additionally decomposes its array argument like its
five siblings (pinned at the lowered-fragment level, since the new pass
subsumes its behavior).

An operand CONTAINING an array-valued PREVIOUS/INIT subexpression is
declined loudly rather than materialized: the frozen reference lowers
element-collapsed while its sibling supplies the array shape, so the
temp would broadcast one element's previous value across the row --
wrong numbers where HEAD failed to compile. Those rows are pinned per
view position for the follow-on snapshot-view work to flip. Also made
loud, in symbolic::resolve_static_view: a static view over a temp id
past the u8 TempId namespace, which previously wrapped modulo 256 and
returned a different array from element 128 on (a pre-existing GH #583
instance, reproduced at the base commit; the write-side truncation
coincidence is now pinned rather than assumed). New corpus fixture
test/vector_computed_operand gates the computed-operand shapes through
the VM, both round-trips, and the wasm parity hook with time-varying
hand-computed expectations.
…ot buffers

An array-valued operand must be a view over storage, and PREVIOUS/INIT
had no view to offer: LoadPrev/LoadInitial address one static slot, so
out[d] = VECTOR SORT ORDER(PREVIOUS(vals[d]), 1) -- and the real user
shape VECTOR SELECT(sel[Row,*], PREVIOUS(matrix[Row,*]), ...) -- failed
to compile. This is GH #995's option D: the view records' is_temp bool
becomes ViewStorage { Curr, Temp, Prev, Initial } (with distinct
symbolic PrevVar/InitialVar bases, so Temp-plus-snapshot is
unrepresentable and every dereference site had to say which region it
means), the three chunk regions share curr's slot numbering, and both
backends read through the base. First-step semantics are BRANCHES, not
buffer contents: a Prev view reads the scalar fallback while
use_prev_fallback is set and an Initial view reads curr during the
Initials phase, mirroring the scalar opcodes -- the wasm reset does not
zero its snapshot regions, so relying on zeroed memory would have been
silently wrong on a second run, which the repeated-run parity test
pins. A non-default array PREVIOUS fallback, PREVIOUS/INIT of a temp,
and a snapshot in ALLOCATE AVAILABLE's priority-profile position (whose
expander rebuilds the full view only from a direct variable reference,
so a snapshot there would allocate over one column) all decline loudly.

builtins_visitor now passes an array-shaped PREVIOUS/INIT argument
through to lowering instead of pinning or capturing it at parse time,
so the array-vs-element decision is made where the operand position is
known (compiler::snapshot_view_arg, one classifier shared by codegen's
consumers and the operand materializer); the C1+C2 nested-operand
decline is replaced by correct materialization, and every row of GH
per-element capture oracles on time-varying fixtures. The dep graph
classifies the new bases like their scalar twins (a Prev view is never a
same-step edge; an Initial view is an initials-phase edge), each arm
pinned by a fixture its mutation kills. C-LEARN is byte-identical on
all four LTM harnesses -- the LTM wrap already materializes its freezes
as helper variables, which stay (their axis-qualified rows are still
load-bearing for non-prefix subdimensions; retiring them onto these
views is assessed in the freeze module's doc as follow-on). New corpus
fixture test/vector_snapshot_operand gates the shapes through the VM,
both round-trips, and the wasm parity hook with hand-derived
expectations. The PartialEquationError cluster moves to a path-mounted
ltm_augment_partial_error.rs sibling, keeping ltm_augment.rs under the
per-file line cap.
…ew module_off bug beneath one

Three review findings on the array-operand and mapped-describer work,
each reproduced before fixing; the third led somewhere bigger than its
report.

A computed array operand took the FIRST subexpression's shape:
VECTOR SORT ORDER(vals[d] + matrix[e,d], 1) materialized a [d]-shaped
temp whose iteration read matrix as NaN, so commuting the addition
changed the answer. find_expr_array_view now joins every array-shaped
subexpression by a maximality filter over dim names and sizes --
order-independence is structural, not accumulated -- and the If
condition is visited (previously a wider condition silently collapsed
the IF to its ELSE arm). Incomparable shapes, axis-order ties, and a
view naming one dimension twice decline loudly; the repeated-name
decline replaces a pre-existing wrong-both-ways answer whose mechanism
(project_var_index_to_temp matching temp axes to variable axes by name,
first hit wins) is pinned by a control so a future projection fix must
restate it. The join lives in find_expr_array_view itself because the
enclosing hoisted AssignTemp's view is derived from the same function
-- a join beside it produced new wrong numbers through the temp-index
projection. Measured zero divergence from the old answer across the
corpus and heavy tier.

Two source axes projected onto one target dimension enumerated their
rows cartesianly while execution resolves both from the same active
element: 9 element edges and 9 loop candidates where execution reads 3
(link-score names were already diagonal, so names and edges described
different graphs). read_slice_row_parts now enumerates drivers -- axes
naming the same target dimension share one -- preserving row order for
every unrepeated slice, and an all-Iterated subscript repeating a
target dimension retargets from Bare to PerElement when the target
names that dimension once. The repeated-TARGET shape keeps Bare
deliberately: the retarget would flip its score surface to the loud
square-source skip, a separate decision, and expand_same_element's
name-keyed target positions carry both phantom and missing edges there
either way -- pinned in both directions as a residual with the
CLAUDE.md never-fewer-edges claim carrying the measured exception.

The reported snapshot-view module_off gap turned out to predate the
branch and cover every region: PushStaticView never applied module_off
at all, so any static view pushed while executing INSIDE a module
instance -- every array reducer, vector op, and arrayed GF in a
sub-model, Curr included -- read the root's storage, identically on
both backends (the wasm lowering documented the VM verbatim, which is
why parity never caught it; reads from the root were correct because
module_off is 0 there, which is why the cross-module array tests
passed). StaticArrayView::to_runtime_view now takes the executing
instance's base for Curr/Prev/Initial and not for Temp (temp storage is
per-evaluation and shared), the wasm ViewBase arms are named for their
region with the absolute/relative split gone, and two-instance plus
nested two-hop fixtures pin instance addressing on both backends -- the
nested one kills a last-hop-only mutation the one-hop fixture cannot
see.
@bpowers
bpowers force-pushed the ltm-burndown-995-996-997 branch from 1ff773b to 1510a59 Compare August 2, 2026 18:45
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.83408% with 110 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.02%. Comparing base (ccf7ed3) to head (77e9c70).

Files with missing lines Patch % Lines
src/simlin-engine/src/compiler/mod.rs 72.47% 30 Missing ⚠️
src/simlin-engine/src/vm.rs 86.39% 23 Missing ⚠️
...simlin-engine/src/ltm_augment_wrap_test_support.rs 71.42% 16 Missing ⚠️
src/simlin-engine/src/compiler/codegen.rs 92.06% 10 Missing ⚠️
src/simlin-engine/src/wasmgen/vector.rs 86.48% 5 Missing ⚠️
src/simlin-engine/src/compiler/symbolic.rs 87.09% 4 Missing ⚠️
src/simlin-engine/src/ltm_agg.rs 82.60% 4 Missing ⚠️
...rc/simlin-engine/src/ltm_augment_post_transform.rs 96.15% 4 Missing ⚠️
src/simlin-engine/src/db/ltm/loops.rs 96.00% 3 Missing ⚠️
src/simlin-engine/src/wasmgen/views.rs 95.58% 3 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1010      +/-   ##
==========================================
+ Coverage   91.86%   92.02%   +0.16%     
==========================================
  Files         247      249       +2     
  Lines      159059   160329    +1270     
==========================================
+ Hits       146116   147542    +1426     
+ Misses      12943    12787     -156     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bpowers

bpowers commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1510a59aa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1336 to +1337
if outer.dims == inner.dims && outer.dim_names == inner.dim_names {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Decline repeated-dimension computed operands

When a computed operand has a repeated dimension, such as VECTOR SORT ORDER(matrix[d,d] * 2, 1), this identity shortcut supplies [d,d] as the materialization shape even though downstream projection matches axes only by name and maps both temp axes to the first d coordinate. The parent revision rejected this operand, but this change now compiles it and silently returns rows like [0,0,0,1,1,1,2,2,2] instead of each row's correct [0,1,2]; reject repeated-name views even when they are the operand's sole shape until axis-identity projection is implemented.

AGENTS.md reference: src/simlin-engine/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and your parent-revision claim was the correct one -- our earlier 'pre-existing' framing had been measured against a branch commit rather than the merge base. At ccf7ed3 the computed spelling (and the PREVIOUS/INIT spellings, which turn out to share the defect) all fail to compile, so this branch had introduced them compiling to garbage. Fixed in 0e61d13: the operand materializer refuses a repeated-name view even as the sole shape, and snapshot_static_view refuses a repeated-dimension snapshot source (that route never consults the join). Review caught that placing the refusal in the shared join instead panicked the VM on reducer-nested shapes that work at the merge base (through the hoisters' silent fallback), so the placement is itself pinned by a regression test. The DIRECT spelling compiles at the merge base and stays byte-identical, pinned as a disclosed residual; vensim-probes/repeated_dimension.mdl is queued for a real Vensim DSS run to learn whether the declaration is even legal there before investing in the axis-identity projection fix.

Comment on lines +400 to +402
AxisRead::Iterated { dim, .. } | AxisRead::MappedRead { dim, .. } => {
Some(dim.as_str())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve mapped slot parts instead of reparsing CSV

When a mapped target dimension contains a quoted element whose canonical name includes a comma, this newly accepted MappedRead path passes its slot through ReadSliceRow.slot, and line 420 reparses that comma-joined string with split(','). For a one-axis element such as "a,b", the real target[a,b] coordinate is therefore interpreted as separate a and b coordinates, so the element graph can omit the actual executed edge and create an edge to a nonexistent target; carry ReadSliceRowParts::slot_parts through this projection instead of serializing and splitting it.

AGENTS.md reference: src/simlin-engine/AGENTS.md:L95-L95

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real and constructible -- canonicalize on a quoted XMILE element "a,b" preserves the comma, and a model declaring one compiles and simulates. Fixed in 0e61d13: the PerElement element-edge arm consumes ReadSliceRowParts::slot_parts directly (structured, no serialize-and-split), with the comma-joined form cfg(test)-gated at the db::ltm boundary and a pin whose comma element comes FIRST so a mis-split shifts every following coordinate (mutation: reintroducing the round-trip reds with exactly the predicted phantom target). Disclosed alongside: the link-SCORE emitters carry their own split(',') sites and the defect is live there (measured on the same comma element carried into an agg -- the two halves of one agg name different variables); that sweep is its own change, and the invariant (never serialize a coordinate tuple you will re-split) is now stated in the engine CLAUDE.md.

Comment on lines +220 to +223
// Materializing an ELM MAP *source* deliberately changes which storage
// the mapping ranges over, and the choice is this: the temp. Genuine
// Vensim maps over the source VARIABLE's full row-major storage from
// the base arg-1's element reference establishes, and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify computed VECTOR ELM MAP storage semantics

For a computed source such as VECTOR ELM MAP(matrix[1,*] * 1, offsets), this branch makes a new semantic choice: materialization confines mapping to the temporary instead of retaining the source variable's full-storage addressing. The nearby test only pins that chosen behavior against Simlin itself; it provides no official Vensim citation or genuine Vensim output for the computed-source case, so the unverified premise can make newly compiling models silently disagree with Vensim. Verify this exact case against the function reference or checked-in DSS output and cite it here; if it cannot be checked, mark the behavior unverified rather than letting it determine lowering.

AGENTS.md reference: AGENTS.md:L103-L109

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the standard, and the research split the question. The VARIABLE-source full-storage rule is now cited rather than asserted (0e61d13): the fn_vector_elm_map reference page says the function returns the value of 'the variable' offset from vec, with :NA: outside 'the range of the variable', and its multi-subscript example makes the flat whole-variable addressing explicit; the checked-in vector.dat ground truth confirms it (f[A2,B1] = 5 reads past its own B1 slice), recomputed independently. The COMPUTED-source case is genuinely outside what the page reaches -- every example there and in the corpus spells the argument as a variable reference pinned to an element -- so it is now marked UNVERIFIED at the lowering arm, in the test, and in CLAUDE.md, with the helper-equivalence argument stated as the reason for the choice rather than as evidence. A discriminating probe model (vensim-probes/elm_map_computed_source.mdl, with self-checking controls that reproduce vector.dat and per-rule prediction tables) is queued for a real Vensim DSS run; the lowering will match whatever that run shows, including refusing the shape if Vensim rejects inline expressions there.

bpowers added 2 commits August 2, 2026 13:42
… structured

Second round of PR #1010 review findings, each claim measured before
acting -- including against the true merge base, which corrected a
mis-baselined claim from the prior round: the repeated-dimension
computed shapes (VECTOR SORT ORDER(matrix[d,d] * 2, 1) and the
PREVIOUS/INIT spellings) all FAIL at ccf7ed3, so this branch had
introduced them compiling to garbage, not inherited them. They decline
loudly now, at the two positions that can produce a diagnostic: the
operand materializer refuses a repeated-name view even as the sole
shape, and snapshot_static_view refuses a repeated-dimension snapshot
source (the PREVIOUS/INIT route never consults the join). Placing the
refusal in the shared join instead panicked the VM on reducer-nested
shapes that work at the merge base (SUM(VECTOR SORT ORDER(matrix[d,d],
1)) and kin) through the hoisters' silent unwrap_or_else fallback --
caught in review, and the placement is now constrained by a regression
test rather than a comment. The DIRECT repeated-dimension spellings
compile at the merge base and stay byte-identical, pinned as a
disclosed residual stating the correct answers alongside ours (the
mechanism is project_var_index_to_temp's first-hit name matching);
vensim-probes/repeated_dimension.mdl exists to learn whether Vensim
even accepts the declaration before anyone invests in the fix.

A canonical element name can contain a comma (quoted XMILE "a,b"
canonicalizes to a,b and simulates), so the PerElement element-edge arm
now consumes the row derivation's structured slot parts instead of
re-splitting a comma-joined string that mis-parsed such a name into two
coordinates -- one real edge dropped, one minted to a nonexistent
target. The comma-joined form is cfg(test)-gated at the db::ltm
boundary; the link-score emitters' own split sites are a measured,
disclosed residual on the pin test.

VECTOR ELM MAP's full-storage variable-source rule is now CITED (the
fn_vector_elm_map reference page, quoted, plus an independent
recomputation of the checked-in vector.dat ground truth where
f[A2,B1] = 5 reads past its own B1 slice) instead of asserted. The
COMPUTED-source case is marked unverified everywhere it is decided:
every example on the page and in the corpus spells the argument as a
variable reference pinned to an element, so the documented rule does
not reach an inline expression; vensim-probes/elm_map_computed_source.mdl
carries the discriminating fixture with per-rule prediction tables and
self-checking controls, for a real Vensim DSS run to settle.
…points at

The engine CLAUDE.md's unverified-semantics note references
vensim-probes/elm_map_computed_source.mdl, which existed only untracked,
so the documentation-links check passed locally (the file is on disk)
and failed in CI (it is not in the checkout). The kit is real project
material -- two discriminating probe models plus a README with per-rule
prediction tables and self-checking controls, awaiting a real Vensim DSS
run -- so it belongs in the tree. Nothing lands in test/ until that run
produces genuine output, exactly as the kit's README states.
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code review — no blocking findings

Reviewed the four commits across the array-operand materializer, the ViewStorage refactor, the LTM describer mapped-read threading, and the module_off fix. Verified the following areas that would be the most likely sources of latent bugs, and found nothing to flag:

  • Enum exhaustiveness. All matches over ViewStorage (bytecode.rs, vm.rs::ChunkRegions::backing, wasmgen/views.rs), SymStaticViewBase (symbolic.rs::resolve_static_view, FragmentMerger::absorb, dep_graph.rs element-offset extractor + phase-classified read), AxisRead (three deliberate MappedRead => None sites in ltm_agg.rs, each documented), and BuiltinFn (array_operand.rs::materialize_view_operands) enumerate every variant without _.
  • VM/wasm parity for the module_off fix. StaticArrayView::to_runtime_view folds module_off into base_off for Curr | Prev | Initial only (Temp intentionally excluded); the wasm side sets module_relative: true for the same three via ViewDesc::element_addr_for_flat. Pinned by chunk_regions_tests::backing_routes_every_storage_arm_under_every_run_state (4×2×3 grid) plus one-hop / nested-two-hop absolute-series fixtures on both backends.
  • First-step semantics. Prev view reads fallback under use_prev_fallback in both backends; Initial view reads curr during Initials phase (VM's runtime branch mirrored by wasm's compile-time bases.initial = curr_base when step_part == Initials).
  • Loud declines (non-default array PREVIOUS fallback / PREVIOUS/INIT of a temp / ALLOCATE AVAILABLE priority-profile position) are enforced at every codegen site with unit-test pins in array_operand_materialization_tests.rs, and the materializer correctly skips these operands so they reach the decline arms.
  • Computed-operand view join. join_array_views correctly returns None for incomparable shapes and axis-order ties; find_expr_array_view visits If conditions; view_repeats_a_dimension refuses matrix[d,d]; u8 TempId overflow is caught loudly at symbolic::resolve_static_view rather than wrapping.
  • Correspondence at describer call sites. Iterated/Barepositional_correspondence; MappedRead/OwnDimSubscriptexecuted_read_correspondence; bare_reference_correspondence unions them and the singleton gate (all(|elems| elems.len() == 1)) is correctly parametrized. The repeated-projected-axes retarget is narrowed by target_iterated_dims.iter().filter(|t| *t == dim).count() == 1, so cube[D1,D1] (repeated target) correctly stays on the conservative path while matrix[D1,D1] onto target[D1] retargets to PerElement.

The disclosed residuals (nested array-producing builtin inside arithmetic, repeated-TARGET name-keyed edges, u8 TempId ceiling, ALLOCATE AVAILABLE profile look-through, freeze-helper retirement) match what the PR body calls out and are pinned in-tree.

Overall correctness verdict: correct

No blocking issues; the patch matches its stated contract and the adversarial-review + mutation-testing groundwork is visible in the test structure.

The probe .mdl files carried empty sketch sections, so they opened in
Vensim with a blank diagram. examples/layout_probe_models.rs runs
generate_best_layout over each and splices ONLY the generated sketch
block into the original file: the first attempt re-serialized the whole
project through the MDL writer, which re-spells an apply-to-all
equation per element -- an element-pinned left-hand side over a
right-hand side naming subscript ranges, which is not the shape the
probes ask Vensim about -- so the equations stay byte-identical and the
rendered output serves only as a sketch donor. The harness is checked
in rather than its output alone so the views can be regenerated if the
probes change.
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review: no blocking findings

Reviewed the four largest concerns of the PR in parallel: the new compiler/array_operand.rs materialization pass with its VM callers, the dimensions/mapping API rewrite (resolve_mapped_read + the three spelling-keyed correspondences), the wasm backend's Prev/Initial region handling, and the LTM augmentation surface consuming those correspondences.

Key claims verified:

  • Materialization is a no-op on already-compilable fragments. is_view is the exact negation of walk_expr_as_view's accepting arms; next_temp_id starts at max+1 so existing IDs are preserved; the bail-outs on find_expr_array_view == None, view_repeats_a_dimension, and empty dims all run before any temp allocation. BuiltinFn match is exhaustive against builtins.rs — a new variant is a compile error rather than a silently unconsidered position.
  • Snapshot views correctly fold module_off. to_runtime_view in bytecode.rs folds for Curr/Prev/Initial and NOT for Temp; wasmgen mirrors this via module_relative: true on the three chunk-shaped bases (views.rs + vector.rs). The Initial-during-initials compile-time branch lives in EmitCtx::region_bases and is consumed uniformly; the Prev runtime select on use_prev_fallback is emitted symmetrically in both address-load paths, load-bearing for a second run over non-cleared snapshot memory.
  • Three-rule mapped-read resolution is consistent. resolve_mapped_read (name-first → translate_via_mapping → mapped parent) is called from all three executed sites. positional_correspondence and executed_read_correspondence share the cardinality invariant that bare_reference_correspondence's union relies on. mapped_read_partner_dim correctly declines ambiguity where execution would tie-break by position.
  • VM/wasm parity for PushStaticView. VM folds module_off at push time; wasm folds at load time via module_relative. Both reach the same slot, and both had the pre-existing module_off-not-applied defect described in commit 1510a59a.
  • Loud rejections where silence would be wrong. view_repeats_a_dimension refuses in the materializer and codegen::snapshot_static_view, not in join_array_views (which has three silent-substituting consumers). symbolic::resolve_static_view fails loudly on a u32 temp id above u8::MAX (engine: TempId u8 overflow in incremental fragment assembly on large models (C-LEARN) #583's boundary). emit_unscoreable_disjoint_edge_warning accumulates a Warning and skips emission rather than producing a misleading scalarized stand-in.
  • Determinism. No new HashMap-iteration-order dependency in salsa-cached values; read_slice_row_parts's local HashMap<&str, usize> is lookup-only and output ordering rides groups: Vec iteration order.

Explicitly-disclosed residuals in the PR body (nested array-producing builtin in arithmetic operands, u8 TempId ceiling at ~128 elements #583, repeated-dimension direct read, expand_same_element's repeated-TARGET defect, emit_per_element_link_scores refusing repeated-dim targets, remaining split(',') sites) were not counted against the review.

Overall correctness verdict: correct. The change ships one new module, threads a spelling-keyed API through the LTM describers, and turns one pre-existing cross-module correctness gap (PushStaticView module_off) into pinned behavior on both backends. No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment