Skip to content

engine: arrayed VECTOR SORT ORDER returns flat global indices instead of per-iterated-slice 0-based ranks #585

Description

@bpowers

Summary

For an arrayed (multi-dimensional) source, Opcode::VectorSortOrder returns global flat indices into the entire array instead of per-iterated-slice 0-based ranks. When the resulting "order" is fed to VECTOR ELM MAP, the out-of-range global indices index a single-row slice -> out-of-bounds -> NaN. This is a runtime-numeric correctness bug (the model compiles and runs to FINAL TIME).

Concrete C-LEARN manifestation

C-LEARN (test/xmutil_test_models/C-LEARN v77 for Vensim.mdl):

Target Order[COP,Target]   = VECTOR SORT ORDER(Effective Target Year[COP,Target], ASCENDING)   ; line ~19565
sorted target year[COP,Target] = VECTOR ELM MAP(Effective Target Year[COP,t1], Target Order[COP,Target])  ; line ~19486

Target has 3 elements; COP has 7 rows. Opcode::VectorSortOrder (src/simlin-engine/src/vm.rs:~2334-2377) iterates the whole input view (for i in 0..size, size = input_view.size()) and writes orig_idx -- the global flat index -- into the result. So for the 7th COP row it emits [18,19,20] instead of the per-row 0-based ranks [0,1,2].

VECTOR ELM MAP (src/simlin-engine/src/vm_vector_elm_map.rs:~107, the flat_i = base_i + offset_val path with the [0, full_len) -> NaN guard) then uses [18,19,20] to index the 7-element single-column slice -> out of bounds -> NaN. (The ELM MAP OOB->NaN behavior is the correct genuine-Vensim semantics from the Phase 5 work; the bug is the bad indices coming from VectorSortOrder, not the guard.)

Observed: target_order[cop_developing_b] = [18,19,20] (Simlin) vs [0,1,2] (Ref.vdf); sorted_target_year[cop_developing_b] = NaN vs 4000.0 (Ref.vdf). This is one of the two root causes of C-LEARN's core series being entirely NaN at AC7.3.

Root cause

Opcode::VectorSortOrder sorts and ranks over the flattened whole-array view and emits absolute flat indices. This is the multi-row / apply-to-all (A2A) case. Phase 4 (AC5) corrected the 1-based->0-based output for the single-row case but did not cover per-iterated-slice ranks for a multi-dimensional source: a sort order for [COP,Target] must produce, for each iterated COP row, a 0-based permutation within that row ([0..Target.len())), not flat indices into the whole [COP,Target] array.

Why it matters

  • Correctness: any arrayed VECTOR SORT ORDER whose result is consumed positionally (e.g. as the offset argument to VECTOR ELM MAP, the canonical Vensim idiom) produces out-of-range indices -> NaN. C-LEARN's sorted target year series is entirely NaN as a direct consequence.

Components affected

  • src/simlin-engine/src/vm.rs:~2334-2377 -- Opcode::VectorSortOrder (emits global flat indices over the whole view)
  • src/simlin-engine/src/vm_vector_elm_map.rs:~107 -- the consumer whose OOB->NaN guard (correctly) trips on the bad indices

Possible approach for resolution

VectorSortOrder must rank within the currently-iterated source slice (per-row), 0-based, producing a valid per-row index permutation rather than absolute flat indices into the whole array. The fix must keep the single-row AC5 case byte-identical (it is already genuine-Vensim 0-based for an effectively-1-D source).

Context / lineage

Freshly root-caused during Phase 6 of the element-level cycle resolution work (branch clearn-hero-model, HEAD b0910842). Being fixed now as Phase 6 Task 9; filing for traceability. Relate to the AC5 / Phase 4 VECTOR SORT ORDER work (commit a82dff29, which fixed the single-row 1-based->0-based output).

This is the Cluster B spurious-NaN bug. Relationship to existing issues:

Activity

  1. added
    bugSomething isn't working
    engineIssues with the rust-based simulation engine
    on May 19, 2026
  2. bpowers commented on Jun 8, 2026

    @bpowers
    OwnerAuthor

    Fixed by #592 (commit 81cdcb1c, the element-level cycle resolution / C-LEARN work).

    Opcode::VectorSortOrder now ranks within each currently-iterated source slice (per-row), 0-based, rather than emitting absolute flat indices over the whole flattened view. The implementation lives in the extracted src/simlin-engine/src/vm_vector_sort_order.rs: the innermost (last-declared) dimension is the sorted axis, outer dims select independent rows, and each row's result is a valid [0, inner) index permutation -- so a downstream VECTOR ELM MAP can no longer read out of bounds. The single-row case stays byte-identical to the AC5/Phase 4 0-based output (it is the degenerate row == whole-view case).

    Verified end-to-end against real Vensim: simulates_clearn matches Ref.vdf (C-LEARN's Target Order[COP,Target] / sorted target year are the exact shape this issue described, now finite instead of NaN). Unit coverage is in array_tests.rs::arrayed_vector_sort_order_per_slice_tests: multi_row_vso_ranks_within_each_row_vm/_monolithic, multi_row_vso_descending_within_each_row_vm, and vso_feeding_elm_map_single_column_source_no_oob_nan_vm (the C-LEARN VSO->ELM MAP downstream shape, asserting no OOB->NaN). The wasm backend mirrors it in wasmgen/vector.rs.

    The related #576 (dormant 2-D VSO fixture / unverified multi-dim semantics) is tracked separately.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengineIssues with the rust-based simulation engine

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions