The simlin-engine implements comprehensive array support following the XMILE v1.0 specification. The implementation is complete for the core array functionality, with a few edge cases remaining as documented below.
- Array indexing is 1-based in source equations, converted to 0-based internally
- Numeric ranges like
1:5are inclusive of both start and end (selects elements 1,2,3,4,5) - Named ranges like
Cities.Boston:Cities.Seattleare inclusive - Wildcards (
*) preserve the dimension in the result - Star ranges (
*:SubDim) select elements matching a subdimension - Dimension position (
@n) references the nth dimension in subscripts - Transpose (
') reverses dimension order
Array expressions go through a multi-phase compilation pipeline:
- Parser (Expr0 -> Expr1): Captures all XMILE array syntax
- Type Checker (Expr1 -> Expr2): Computes array bounds, validates dimensions
- Pass 0 (Expr2 -> Expr2): Normalizes array expressions
- Expands bare array references to explicit subscripts
- Normalizes wildcards (e.g.,
*todim.*) - Ensures all Var references can be treated as scalars in later phases
- Expr3 lowering (Expr2 -> Expr3): Structural, no temps
- Resolves every wildcard to an explicit star range
- Gives every bare array reference its subscripts
- Compiler (Expr3 -> Expr): Creates optimized expressions
- Resolves static subscripts into
ArrayViewinstances - Generates
StaticSubscriptexpressions
- Resolves static subscripts into
- Materialization (
compiler::array_operand): the one pass that moves an array value into a temp -- the array-producing builtins, the per-element arrayed-GF applies, and the computed array operands -- so codegen only ever sees views. GeneratesAssignTemp,TempArray,TempArrayElement - Bytecode Generation: Emits VM opcodes
- View stack operations for array access
- Iteration loops for element-wise operations
- Reduction opcodes for array builtins
// Efficient array view representation (no data copying)
pub struct ArrayView {
pub dims: Vec<usize>, // Dimension sizes after slicing
pub strides: Vec<isize>, // Elements to skip per dimension
pub offset: usize, // Starting offset in underlying data
}
// Runtime view with dimension identity tracking
pub struct RuntimeView {
pub base_off: usize, // Base variable offset
pub dims: Vec<ViewDim>, // Dimension info with dim_ids
pub is_valid: bool, // For out-of-bounds detection
pub sparse: Vec<(usize, Vec<usize>)>, // Sparse iteration support
}
// Compiler expression types
pub enum Expr {
Var(usize, Loc), // Scalar variable
Subscript(usize, Vec<Expr>, Vec<usize>, Loc), // Dynamic subscript
StaticSubscript(usize, ArrayView, Loc), // Optimized static subscript
TempArray(u32, ArrayView, Loc), // Reference to temp array
TempArrayElement(u32, ArrayView, usize, Loc), // Single temp element
AssignTemp(u32, Box<Expr>, ArrayView, Loc), // Populate temp array
// ... other variants
}When combining arrays with different dimensions (broadcasting), the VM uses a two-pass algorithm:
- Pass 1 - Name Matching: Match dimensions by exact
dim_id(semantic identity) - Pass 2 - Positional Matching: For indexed dimensions only, fall back to size-based matching
Named dimensions (e.g., Cities=[Boston,Seattle]) must match by name because their elements have semantic meaning.
Two different named dimensions of the same size will NOT match.
Indexed dimensions (e.g., numeric dimensions like Periods(5)) can use positional matching when names don't match but sizes do.
- Transpose operator (
') - Dimension position (
@n) - Range subscripts: numeric (
[1:3]) and named ([Boston:LA]) - Wildcard subscripts (
[*]) - Star ranges (
[*:SubDim])
- Array bounds propagation
- Dimension compatibility validation
- Temp ID allocation for intermediate results
ArrayViewabstraction for zero-copy array operations- Static subscript optimization
- Named element resolution (case-insensitive)
- Range slicing with proper inclusive semantics
- Transpose support
- Dimension position handling
- Expression rewriting for array builtins
- All expression types:
StaticSubscript,TempArray,TempArrayElement,AssignTemp
- View stack operations:
PushStaticView(a view whose constant subscripts, ranges, star ranges and transposes codegen baked into its geometry),PushVarViewDirect(the base for a dynamic subscript),PopView - Dynamic view manipulation:
ViewSubscriptDynamic,ViewRangeDynamic - Iteration:
BeginIter,LoadIterViewAt(broadcasting a lower-dimensional source by dimension id),StoreIterElement,NextIterOrJump,EndIter - Reductions:
ArraySum,ArrayMax,ArrayMin,ArrayMean,ArrayStddev,ArraySize - Sparse (star-range) views iterate through their
RuntimeSparseMapping
SUM(array)- Sum all elementsMEAN(array)- Arithmetic meanSTDDEV(array)- Standard deviationMIN(array)- Minimum valueMAX(array)- Maximum valueSIZE(array)- Element count
- Named dimensions with element lookup
- Indexed dimensions with numeric access
- Subdimension detection and star range support
- Broadcasting between compatible dimensions
- Different-named indexed dimensions can broadcast by position
Issue: When array builtins like SUM(a[*]+h[*]) combine arrays with different dimensions (e.g., a[DimA] and h[DimC]), the expected XMILE behavior is a cross-product (3x3 = 9 elements summed), but the current implementation treats this as element-wise.
Test: simulates_sum in src/simlin-compat/tests/simulate.rs (ignored)
Example:
a[DimA] = [1, 2, 3] // DimA = {A1, A2, A3}
h[DimC] = [10, 20, 30] // DimC = {C1, C2, C3}
result = SUM(a[*] + h[*])
// Expected: 198 (sum of 3x3 cross-product: 11+21+31+12+22+32+13+23+33)
// Current: 66 (sum of 3 elements: 11+22+33)
Issue: When assigning a smaller array slice to a larger array (e.g., array[5] = source[1:3]), elements beyond the slice should be NaN or zero, not a repeat of the last value.
Tests: range_basic, range_with_expressions in src/simlin-engine/src/array_tests.rs (ignored)
Example:
source[Periods(5)] = [1, 2, 3, 4, 5]
slice[Periods(5)] = source[1:3]
// Expected: [1.0, 2.0, 3.0, NaN, NaN] or [1.0, 2.0, 3.0, 0.0, 0.0]
// Current: [1.0, 2.0, 3.0, 3.0, 3.0] (extends last element)
Issue: When iterating over mismatched-size arrays in A2A context, out-of-bounds accesses should return NaN.
Tests: out_of_bounds_iteration_returns_nan, bounds_check_in_fast_path in src/simlin-engine/src/array_tests.rs (ignored)
Notes: VM changes are in place (is_valid flag on RuntimeView), but compiler needs to generate code that properly creates mismatched-size views.
Issue: Some edge cases with transpose combined with slicing may not work correctly.
Test: transpose_and_slice in src/simlin-engine/src/array_tests.rs (ignored)
Issue: The EXCEPT builtin for Vensim compatibility requires subscript mapping information that isn't preserved in XMILE conversion.
Test: simulates_except in src/simlin-compat/tests/simulate.rs (ignored)
Note: This is a Vensim compatibility issue, not core array support.
The following test categories exercise array functionality and pass:
simulates_arrays- Basic array modelsimulates_array_sum_simple- SUM with simple expressionssimulates_array_sum_expr- SUM with complex expressionssimulates_array_multi_source- Multiple arrays in expressionssimulates_array_broadcast- Cross-dimension broadcastingdifferent_indexed_dims_same_size_broadcast- Indexed dimension broadcastingstar_range_indexed_subdimension- Star ranges with indexed subdimensionsnamed_dims_same_size_no_fallback- Named dimensions require name matching
test/arrays1/arrays.stmx- Basic array accesstest/test-models/tests/subscript_1d_arrays/- 1D array operationstest/test-models/tests/subscript_2d_arrays/- 2D array operationstest/test-models/samples/arrays/a2a/- Apply-to-all arraystest/test-models/samples/arrays/non-a2a/- Non-A2A arrays
ArrayView enables efficient array operations by adjusting iteration patterns rather than copying data.
This is crucial for large array performance.
The compiler aggressively optimizes array operations resolvable at compile time, reducing runtime overhead for common patterns.
All array operations follow XMILE v1.0 semantics:
- Element-wise arithmetic (not linear algebra)
- 1-based indexing in source, 0-based internally
- Inclusive ranges
Named dimensions preserve semantic meaning - Cities will never match Products even if both have the same size.
This prevents subtle bugs from accidental dimension mismatches.
If additional array features are needed:
- RANK builtin: Query number of dimensions
- View composition optimization: Combine multiple view operations into single view
- Contiguous detection fast path: Optimize for contiguous array access
- SIMD operations: Vectorize array operations where possible