You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
engine: reducer over a scalar-shaped expression is refused (SUM(pop * 2) in an apply-to-all body, SUM(s * 2)) where SUM(pop) * 2 compiles -- the materializer declines a 0-d operand; origin/main computes it x|D| #1051
compiles, pop[e] * 2 (single-argument MEAN has no array position, array_operand.rs comment above materialize_view_operands)
x = SUM(s) (scalar s, scalar equation)
compiles, s
x = SUM(s * 2)
refused
Exact refusal text (branch compiler-unification-v2, simlin simulate):
assembly error in model 'main': variable 'x' failed to compile: codegen rejected the lowered expressions: SimulationError{generic: an array operand here must be a variable, a subscripted array or an array temp, but it is an arithmetic or comparison expression}
error compiling model 'main': SimulationError{not_simulatable: failed to compile fragments for variables: x}
(... but it is a conditional for the IF operand.) The message names the Rust expression kind; it does not say why the operand has no array shape here (every array reference in it is pinned to the enclosing element) or what spelling would (pop[*]).
The branch turned that wrong number into a loud refusal, at Phase 6b (be7adbbf, "engine: one materialization pass over the lowered fragment"). That is the right interim state under the no-silent-wrong-numbers rule, but it is not recorded in the design plan's Phase 6b divergence list (docs/design-plans/2026-08-25-compiler-unification.md, "Phase 6b semantic divergences"): item 9 there calls itself "the one new loud refusal of a base-compiling shape in this phase", and this family is a second. The explicit spelling SUM(pop[region] * 2) and the scalar SUM(s * 2) were refused on main too, so for those two rows the refusal IS pre-existing.
Mechanism (branch)
compiler/context.rs::lower_builtin_expr3 lowers a reducer's operand (ArgKind::Array { whole: false }) so the enclosing element pins the axes it names: the bare pop is pass 0's pop[region], which under the region iteration is a StaticSubscript collapsed to one slot. pop * 2 is then an Op2 over a 0-d view.
compiler/array_operand.rs::Materializer::materialize_view_operand is the one pass that would turn a computed operand into a temp; it returns the operand unchanged when find_expr_array_view yields a dimensionless view (if source_view.dims.is_empty() { return operand; }, the guard right after the repeated-dimension refusal). Its module doc lists four declining limits and this is a fifth it does not name.
Codegen's walk_expr_as_view fallback arm (compiler/codegen.rs, the _ => arm that formats "an array operand here must be ...") then refuses, because a reducer's argument is read through emit_array_reduce as a view.
SUM(pop) compiles because a StaticSubscript IS one of the accepted view shapes, even collapsed; MEAN(pop * 2) compiles because single-argument MEAN is the n-ary mean and has no ArgKind::Array position.
On origin/main the same operand went through ast/expr3.rs::Pass1Context::maybe_decompose_array_arg_inner, which defers an operand containing an apply-to-all reference (has_a2a) to the per-element path in context.rs (with_preserved_wildcards), where the bare reference's axis is preserved as an iteration while its load stays pinned -- the x|D|.
Should these shapes compile?
Under the engine's own rule they should, to the degenerate reduce of one value. The rule -- a bare arrayed name inside a reducer in an apply-to-all body reads the enclosing element -- is pinned by the Phase 8.1 48-row table (db::lowering_scope_tests::a_helper_reads_what_the_plain_spelling_reads: SUM(pop) is pop[region], SIZE(pop) is 1) and rests on XMILE v1.0 section 3.7.1's dimension-name binding ("when all indices are dimension names, they can be omitted"; each element's equation has the name bound to its index). The spec does not address a bare name in a reducer argument separately, and what Stella computes for it is unverified (the design plan says so at Phase 8.1). Given that rule, SUM(pop * 2) is SUM over the one value pop[e] * 2, i.e. exactly what SUM(pop) * 2 computes today. Expected values for the table's model: SUM/MIN/MAX/MEAN(pop * 2) = 200 400 600; SIZE(pop * 2) = 1 1 1; STDDEV(pop * 2) = 0 0 0; SUM(pop * w) = 100 400 900; SUM(pop * w[idx]) = 100 200 300; SUM(pop + w) = 101 202 303; SUM(-pop) = -100 -200 -300; SUM(IF pop > 150 THEN pop ELSE 0) = 0 200 300; x = SUM(s * 2) = 10.
If a modeller wanted Vensim's SUM(pop[region!] * 2) (the whole array, 1200) they spell it SUM(pop[*] * 2) in Simlin, which compiles; that is a separate reading and the refusal message could point at it.
Possible approaches
Materialize a 0-d operand into a one-element temp in materialize_view_operand (ArrayView::contiguous(vec![1]) in place of the dims.is_empty() decline). Codegen already lowers an AssignTemp whose body is not an array-producing opcode to a BeginIter loop; a one-element loop evaluates the scalar body once and the reducer reads a one-element view. Keeps the single materialization owner; the per-element regime reissues one temp id, so TempId pressure does not move. Check that codegen::array_view_to_static_temp accepts an unnamed one-axis temp and that join_array_views's "a set including an unnamed axis is None" rule does not bite when the 1-element temp is joined with siblings.
Alternatively, fold the reducer at lowering when its operand is scalar (SUM/MIN/MAX/MEAN identity, SIZE 1, STDDEV 0). Smaller code, but a second statement of reducer semantics beside the VM's; (1) is preferable.
If refusing is kept, the refusal must name the shape: the operand is a scalar expression because pop reads pop[region] inside an apply-to-all body; write pop[*] (or drop the reducer). The current text cannot be acted on without reading the compiler.
Whichever is chosen: record the Phase 6b divergence (main: x|D| wrong number; branch: refusal; after this issue: the degenerate reduce) in the design plan, and update the Phase 8.1 row aggx[region] = PREVIOUS(SUM(pop * scale)) / plainx[region] = SUM(pop * scale) ("refused by codegen on both trees") in lockstep -- a helper must compile exactly as the plain spelling (GH #1035's invariant, db::lowering_scope_tests::a_helper_is_refused_where_the_plain_spelling_is).
Acceptance (TDD)
Rows derived from the enumeration reducer class {SUM, MEAN, MIN, MAX, STDDEV, SIZE} x operand form {bare pop * 2, explicit pop[region] * 2, -pop, IF, pop * w, pop * w[idx], scalar-equation s * 2}, asserting the values above on the VM plus ensure_wasm_matches.
The controls that compile today keep their values: SUM(pop), SUM(pop) * 2, SUM(pop * w[*]), SUM(pop[*] * 2), MEAN(pop * 2), the scalar-equation SUM(pop * 2) = 1200.
LTM: ltm_array_agg's bare-reducer rows (V9b-4) and ltm_augment partials over these spellings keep scoring; no failed to compile warning appears for a fragment that now compiles.
Corpus (test/) values and the C-LEARN artifact unchanged (no corpus model has the shape; the sweep should say so).
Identified during PR #1040 (branch compiler-unification-v2): the V9b report's out-of-scope discoveries and the Phase 8.1 review, which first saw plainx[region] = SUM(pop * scale) refused with the pre-Phase-9 wording Cannot push view for expression type Discriminant(10). Reproduced with simlin simulate on three binaries: origin/main at d04593e6, the branch's first commit be7adbbf (Phase 6b), and branch HEAD; a release CLI built 2026-08-07 (pre-unification main) computes the same x|D| numbers as d04593e6.
Summary
A reducer whose argument is a scalar-shaped expression is refused by codegen, while the same value spelled as a bare view compiles:
x[region],pop[region]a stock)SUM(pop)pop[e]SUM(pop) * 2pop[e] * 2SUM(pop * 2)SUM(pop[region] * 2)SUM(pop * w[idx]),SUM(pop * w),SUM(pop + w),SUM(-pop),SUM(IF pop > 150 THEN pop ELSE 0)MAX(pop * 2),MIN(pop * 2),STDDEV(pop * 2),SIZE(pop * 2)MEAN(pop * 2)pop[e] * 2(single-argumentMEANhas no array position,array_operand.rscomment abovematerialize_view_operands)x = SUM(s)(scalars, scalar equation)sx = SUM(s * 2)Exact refusal text (branch
compiler-unification-v2,simlin simulate):(
... but it is a conditionalfor theIFoperand.) The message names the Rust expression kind; it does not say why the operand has no array shape here (every array reference in it is pinned to the enclosing element) or what spelling would (pop[*]).Minimal model
Swap
x's equation for any row of the table above. For thew/idx/srows addw[region] = {1, 2, 3},idx = 1,s = 5.What each binary computes (measured,
t = 0,x[north] x[south] x[east])origin/main(d04593e6)be7adbbf, first branch commit) and branch HEADSUM(pop * 2)600 1200 1800(=pop[e] * 2 * 3)SUM(pop * w[idx])300 600 900(=pop[e] * w[idx] * 3)SUM(pop * w)300 1200 2700(=pop[e] * w[e] * 3)SUM(pop + w)303 606 909(=(pop[e] + w[e]) * 3)SUM(-pop)-300 -600 -900SUM(IF pop > 150 THEN pop ELSE 0)0 600 900SIZE(pop * 2)3 3 3(besideSIZE(pop)=1 1 1)MAX(pop * 2)/MIN(pop * 2)200 400 600(right by accident: max of three copies)STDDEV(pop * 2)0 0 0(right by accident)SUM(pop[region] * 2)Cannot push view for expression type Discriminant(10))x = SUM(s * 2)SUM(pop),SUM(pop) * 2,SUM(pop[region]),SUM(pop * w[*]),SUM(pop[region] * w[*]),SUM(pop[*] * 2),MEAN(pop * 2),x = SUM(pop * 2)(scalar equation, 1200)So two things are true at once:
origin/maincomputes a silent wrong number for every bare spelling: the correct per-element term multiplied by|region|. This is GH engine: bare arrayed ref sharing a reducer with a sliced co-source multiplies the result by the bare ref's dimension cardinality #789's mechanism (a bare arrayed reference inside a reducer re-enters the iteration its element already binds; the loads stay slot-resolved, so the result isx|D|, not a cross product). engine: bare arrayed ref sharing a reducer with a sliced co-source multiplies the result by the bare ref's dimension cardinality #789 was closed 2026-06-12 by PR engine: close ltm phase 1 residuals #795, which is LTM-side; the engine side is what theorigin/maincolumn above measures, live today.SIZE(pop * 2)= 3 besideSIZE(pop)= 1 is the cleanest witness that the extra factor is a redundant iteration, not a whole-array read.be7adbbf, "engine: one materialization pass over the lowered fragment"). That is the right interim state under the no-silent-wrong-numbers rule, but it is not recorded in the design plan's Phase 6b divergence list (docs/design-plans/2026-08-25-compiler-unification.md, "Phase 6b semantic divergences"): item 9 there calls itself "the one new loud refusal of a base-compiling shape in this phase", and this family is a second. The explicit spellingSUM(pop[region] * 2)and the scalarSUM(s * 2)were refused on main too, so for those two rows the refusal IS pre-existing.Mechanism (branch)
compiler/context.rs::lower_builtin_expr3lowers a reducer's operand (ArgKind::Array { whole: false }) so the enclosing element pins the axes it names: the barepopis pass 0'spop[region], which under theregioniteration is aStaticSubscriptcollapsed to one slot.pop * 2is then anOp2over a 0-d view.compiler/array_operand.rs::Materializer::materialize_view_operandis the one pass that would turn a computed operand into a temp; it returns the operand unchanged whenfind_expr_array_viewyields a dimensionless view (if source_view.dims.is_empty() { return operand; }, the guard right after the repeated-dimension refusal). Its module doc lists four declining limits and this is a fifth it does not name.walk_expr_as_viewfallback arm (compiler/codegen.rs, the_ =>arm that formats "an array operand here must be ...") then refuses, because a reducer's argument is read throughemit_array_reduceas a view.SUM(pop)compiles because aStaticSubscriptIS one of the accepted view shapes, even collapsed;MEAN(pop * 2)compiles because single-argumentMEANis the n-ary mean and has noArgKind::Arrayposition.On
origin/mainthe same operand went throughast/expr3.rs::Pass1Context::maybe_decompose_array_arg_inner, which defers an operand containing an apply-to-all reference (has_a2a) to the per-element path incontext.rs(with_preserved_wildcards), where the bare reference's axis is preserved as an iteration while its load stays pinned -- thex|D|.Should these shapes compile?
Under the engine's own rule they should, to the degenerate reduce of one value. The rule -- a bare arrayed name inside a reducer in an apply-to-all body reads the enclosing element -- is pinned by the Phase 8.1 48-row table (
db::lowering_scope_tests::a_helper_reads_what_the_plain_spelling_reads:SUM(pop)ispop[region],SIZE(pop)is 1) and rests on XMILE v1.0 section 3.7.1's dimension-name binding ("when all indices are dimension names, they can be omitted"; each element's equation has the name bound to its index). The spec does not address a bare name in a reducer argument separately, and what Stella computes for it is unverified (the design plan says so at Phase 8.1). Given that rule,SUM(pop * 2)isSUMover the one valuepop[e] * 2, i.e. exactly whatSUM(pop) * 2computes today. Expected values for the table's model:SUM/MIN/MAX/MEAN(pop * 2)=200 400 600;SIZE(pop * 2)=1 1 1;STDDEV(pop * 2)=0 0 0;SUM(pop * w)=100 400 900;SUM(pop * w[idx])=100 200 300;SUM(pop + w)=101 202 303;SUM(-pop)=-100 -200 -300;SUM(IF pop > 150 THEN pop ELSE 0)=0 200 300;x = SUM(s * 2)=10.If a modeller wanted Vensim's
SUM(pop[region!] * 2)(the whole array, 1200) they spell itSUM(pop[*] * 2)in Simlin, which compiles; that is a separate reading and the refusal message could point at it.Possible approaches
materialize_view_operand(ArrayView::contiguous(vec![1])in place of thedims.is_empty()decline). Codegen already lowers anAssignTempwhose body is not an array-producing opcode to aBeginIterloop; a one-element loop evaluates the scalar body once and the reducer reads a one-element view. Keeps the single materialization owner; the per-element regime reissues one temp id, soTempIdpressure does not move. Check thatcodegen::array_view_to_static_tempaccepts an unnamed one-axis temp and thatjoin_array_views's "a set including an unnamed axis isNone" rule does not bite when the 1-element temp is joined with siblings.SUM/MIN/MAX/MEANidentity,SIZE1,STDDEV0). Smaller code, but a second statement of reducer semantics beside the VM's; (1) is preferable.popreadspop[region]inside an apply-to-all body; writepop[*](or drop the reducer). The current text cannot be acted on without reading the compiler.Whichever is chosen: record the Phase 6b divergence (main:
x|D|wrong number; branch: refusal; after this issue: the degenerate reduce) in the design plan, and update the Phase 8.1 rowaggx[region] = PREVIOUS(SUM(pop * scale))/plainx[region] = SUM(pop * scale)("refused by codegen on both trees") in lockstep -- a helper must compile exactly as the plain spelling (GH #1035's invariant,db::lowering_scope_tests::a_helper_is_refused_where_the_plain_spelling_is).Acceptance (TDD)
SUM,MEAN,MIN,MAX,STDDEV,SIZE} x operand form {barepop * 2, explicitpop[region] * 2,-pop,IF,pop * w,pop * w[idx], scalar-equations * 2}, asserting the values above on the VM plusensure_wasm_matches.SUM(pop),SUM(pop) * 2,SUM(pop * w[*]),SUM(pop[*] * 2),MEAN(pop * 2), the scalar-equationSUM(pop * 2)= 1200.ltm_array_agg's bare-reducer rows (V9b-4) andltm_augmentpartials over these spellings keep scoring; nofailed to compilewarning appears for a fragment that now compiles.test/) values and the C-LEARN artifact unchanged (no corpus model has the shape; the sweep should say so).Components
src/simlin-engine/src/compiler/array_operand.rs(materialize_view_operand, module doc's "What still declines"),src/simlin-engine/src/compiler/codegen.rs(walk_expr_as_viewfallback arm,emit_array_reduce),src/simlin-engine/src/compiler/context.rs(lower_builtin_expr3),src/simlin-engine/src/compiler/mod.rs(find_expr_array_view,join_array_views).Related
x|D|numberorigin/mainstill computes for these spellings.Discovery context
Identified during PR #1040 (branch
compiler-unification-v2): the V9b report's out-of-scope discoveries and the Phase 8.1 review, which first sawplainx[region] = SUM(pop * scale)refused with the pre-Phase-9 wordingCannot push view for expression type Discriminant(10). Reproduced withsimlin simulateon three binaries:origin/mainatd04593e6, the branch's first commitbe7adbbf(Phase 6b), and branch HEAD; a release CLI built 2026-08-07 (pre-unification main) computes the samex|D|numbers asd04593e6.