Skip to content

engine: close out the VECTOR builtin issues (#576/#578/#579)#736

Merged
bpowers merged 3 commits into
mainfrom
vector-builtins-cleanup
Jun 9, 2026
Merged

engine: close out the VECTOR builtin issues (#576/#578/#579)#736
bpowers merged 3 commits into
mainfrom
vector-builtins-cleanup

Conversation

@bpowers

@bpowers bpowers commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Closes the open VECTOR-builtin engine issues. (#585 was already fixed on main by the element-cycle-resolution work and is closed separately.)

#576 -- 2-D VECTOR SORT ORDER p becomes a hard gate

The #585 fix made VECTOR SORT ORDER rank per iterated row (innermost dim = sorted axis, 0-based), so the engine now produces p = [0,1,1,0,0,1] for p[DimA,DimB] = VECTOR SORT ORDER(o[DimA,DimB], ASCENDING), matching test/sdeverywhere/models/vector/vector.dat exactly. Real-Vensim multi-dimensional VSO semantics are independently confirmed by C-LEARN (simulates_clearn vs Ref.vdf), so the dormant fixture is no longer ambiguous. p is removed from the simulates_vector_xmile_genuine exclusion list.

#579 -- numeric end-to-end gate for full_source_len on the strict-slice path

Investigation showed the existing full-array-source out_of_bounds_element_returns_nan_* tests already catch a corrupted full_source_len (both the codegen computation and the renumber_opcode/resolve path) -- #579's "behaviorally invisible" framing only checked the three .dat simulate gates. The genuine residual gap was the strict-slice source branch (base != 0): added strict_slice_source_oob_returns_nan_{vm,monolithic} (a cross-dimension d[DimA,B1] source whose A3 row offset walks past the source storage -> NaN). Verified red under corruption at both sites, green when correct. The stale symbolic.rs comment is corrected.

#578 -- scalar-source / expression-offset VECTOR ELM MAP now compiles

y[DimA] = VECTOR ELM MAP(x[three], (DimA-1)) previously failed with NotSimulatable: "failed to compile fragments". Two coupled gaps:

  1. A fully-collapsed source subscript (x[three]) was promoted back to the whole source array, discarding the element base (only correct when the element is index 0). It now stays a scalar StaticSubscript carrying view.offset = the element's flat index.
  2. The per-element constant offset can't be a view (which the ELM MAP opcode requires); a new compile-time fold rewrites VECTOR ELM MAP(scalar_source, const_offset) to a direct full-storage read (or :NA:/NaN out of range), recursing through Op2/If so nested forms like 10 + ELM MAP(...) fold too -- but never into reducer arguments where the ELM MAP must stay an array.

Because the fold emits plain slot reads, no ELM MAP opcode is generated for this shape and the wasm backend needs no change. vector.xmile is now a fully un-narrowed genuine-Vensim gate (no exclusions).

Validation

  • Full simlin-engine lib (4352) + integration (487) suites green.
  • C-LEARN VM and wasm vs real-Vensim Ref.vdf (simulates_clearn, simulates_clearn_wasm) green.
  • New unit tests: scalar-source ELM MAP (top-level, monolithic, nested, OOB) + strict-slice full_source_len OOB.
  • Each commit passed the full pre-commit hook (fmt, clippy, rust/ts/python tests, wasm build).

bpowers added 3 commits June 8, 2026 18:32
The #585 fix made VECTOR SORT ORDER rank per iterated row (innermost dim is
the sorted axis, 0-based), so the engine now produces p = [0,1,1,0,0,1] for
the 2-D p[DimA,DimB] = VECTOR SORT ORDER(o[DimA,DimB], ASCENDING), matching
test/sdeverywhere/models/vector/vector.dat exactly. Real-Vensim multi-
dimensional VSO semantics are independently confirmed by C-LEARN
(simulates_clearn vs Ref.vdf), so the dormant fixture is no longer ambiguous.

Removes p from the simulates_vector_xmile_genuine exclusion list, leaving only
y (GitHub #578, a separate ELM MAP compile gap). Closes #576.
…-slice path

The full-array-source out_of_bounds_element_returns_nan_{vm,monolithic} tests
already catch a corrupted full_source_len (both the codegen computation and the
renumber_opcode/resolve path) -- contrary to GH #579's framing, which only
checked the three genuine-Vensim .dat simulate gates (none of which have an
out-of-range offset). The genuine residual gap was the STRICT-SLICE source
branch (base != 0, source_is_full_array == false): no end-to-end test fed an
out-of-range offset through it, so full_source_len's role there -- both the
full-array-vs-strict-slice decision and the [0, full_source_len) -> :NA: guard
-- was unverified numerically.

Adds strict_slice_source_oob_returns_nan_{vm,monolithic}: a cross-dimension
d[DimA,B1] source whose A3 row offset (5) pushes the lookup to flat 9, past d's
6-element storage, yielding NaN. Inflating full_source_len makes that read land
in-bounds (a finite value) and deflating it flips the branch to full-array
(base forced to 0); either corruption drops the expected NaN and fails the
assertion. Verified red under both corruption sites, green when correct.

Updates the symbolic.rs comment that claimed full_source_len was behaviorally
invisible end-to-end. Closes #579.
y[DimA] = VECTOR ELM MAP(x[three], (DimA-1)) -- a fully-collapsed scalar
source element reference plus a per-element arithmetic offset -- previously
failed to compile (NotSimulatable, "failed to compile fragments"). Two
coupled gaps caused it:

1. Inside an array-producing builtin a fully-collapsed source subscript like
   x[three] was promoted back to the whole source array (Wildcard), discarding
   the element's base offset. That is only correct when the element is index 0
   (the historical b[B1] case); for x[three] (base 2) the base was lost. The
   collapsed scalar view is now kept as a StaticSubscript whose view.offset is
   the element's flat index, so codegen::full_source_len recovers the full
   source length and the VM maps from the right base.

2. The per-element offset (DimA-1) lowers to a compile-time constant, which the
   array-producing ELM MAP opcode cannot consume (it needs a *view* offset). A
   new compile-time fold rewrites VECTOR ELM MAP(scalar_source, const_offset)
   into a direct read of the source variable's full storage at
   off + base + round(offset), or :NA: (NaN) when that flat index is out of
   range. The fold recurses through Op2/If so a nested form (10 + ELM MAP(...))
   folds too, but never descends into reducer arguments where the ELM MAP must
   stay an array.

Because the fold emits plain slot reads, no ELM MAP opcode is generated for
this shape and the wasm backend needs no change (confirmed by the wasm parity
hook and simulates_clearn_wasm). The genuine-Vensim vector.xmile gate is now
fully un-narrowed -- y rejoins c/f/g/p and every other variable as a hard
equality against vector.dat. Closes #578.
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.57143% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.61%. Comparing base (d42c8a7) to head (2fb17c2).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/simlin-engine/src/compiler/mod.rs 79.48% 8 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #736   +/-   ##
=======================================
  Coverage   90.60%   90.61%           
=======================================
  Files         224      224           
  Lines      136247   136278   +31     
=======================================
+ Hits       123447   123485   +38     
+ Misses      12800    12793    -7     

☔ 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.

@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review complete. Traced through the changes:

The context.rs change correctly preserves the scalar element-references view.offset (the elements flat index) instead of promoting collapsed Single-ops back to a wildcard view. The old promotion only happened to be correct when the element was index 0 (the historical b[B1] case, base 0); the new x[three] case (base 2) needs the preserved offset.

The new fold (try_fold_scalar_source_elm_map / fold_scalar_source_elm_maps) handles the case where the array-producing ELM MAP opcode cannot consume the per-element constant offset (it requires a view offset). The restriction to Op2/If recursion is the right call - folding inside an Expr::App argument (e.g. SUM(elm_map_array)) would be wrong because the reducer consumes the ELM MAP as an array.

Regression check on the legacy vector_elm_map(b[B1], a[DimA]) case: the per-element offset a[DimA] lowers to Var(a_off + i), which does not fold to a constant, so try_fold_scalar_source_elm_map returns None and the runtime opcode is used. With the new scalar source view (empty dims, view.offset = 0), RuntimeView::flat_offset(&[]) returns 0, so base_i = 0 and the existing c = [11, 12, 12] result is preserved.

Simulate.rs: removing y and p from the exclusion list is consistent with the now-fixed compile path for y and the GH-585 per-iterated-row VSO fix for p.

Notes (not findings):

  • A constant offset that folds to NaN would diverge from the VM (fold returns a slot read since NaN as i64 saturates to 0; VM returns NaN unconditionally). Realistically unreachable - per-element offsets are small integers like (DimA - 1).
  • The fold does not recurse through Op1::Not, so a hypothetical NOT(vector_elm_map(scalar, const)) would not fold. Op1::Transpose is correctly excluded (array op).
  • scalar_source_dim_offset_nested_vm and scalar_source_dim_offset_oob_returns_nan_vm have VM-only coverage; the base case has both VM and monolithic.

Verdict: Overall correctness correct. No actionable findings.

@bpowers bpowers merged commit 8306a93 into main Jun 9, 2026
13 of 15 checks passed
@bpowers bpowers deleted the vector-builtins-cleanup branch June 9, 2026 04:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant