Skip to content

engine: migrate assert_compiles to incremental path and delete#387

Merged
bpowers merged 16 commits intomainfrom
position-and-mean
Mar 8, 2026
Merged

engine: migrate assert_compiles to incremental path and delete#387
bpowers merged 16 commits intomainfrom
position-and-mean

Conversation

@bpowers
Copy link
Owner

@bpowers bpowers commented Mar 8, 2026

Summary

  • Resolve @N position syntax to concrete element offsets in scalar context on the incremental compilation path, fixing arr[@1] and mixed cases like cube[@1, *, @3]
  • Extract emit_array_reduce helper eliminating 6x duplication of the walk_expr_as_view/ArrayOp/PopView pattern across SUM, SIZE, STDDEV, MIN, MAX, MEAN
  • Fix MEAN with dynamic range subscripts (MEAN(data[start:end])) by routing single-arg MEAN through emit_array_reduce instead of a narrow is_array guard
  • Switch all 26 assert_compiles() tests to assert_compiles_incremental() and delete assert_compiles() from test_common.rs

Closes #377

Test plan

  • 2 new error tests: dimension_position_zero_is_error, dimension_position_out_of_range_is_error
  • All 9 dimension_position tests pass (7 existing + 2 new)
  • mean_with_dynamic_range passes on incremental path
  • All 2564 engine tests pass, zero regressions
  • Full pre-commit hook suite passes on all commits
  • Human test plan at docs/test-plans/2026-03-08-assert-compiles-migration.md

bpowers added 12 commits March 8, 2026 07:44
Catalogs all 26 assert_compiles() call sites, identifies the two
incremental compiler gaps (@n position syntax in scalar context, MEAN
with dynamic ranges), and designs the migration path including an
array-reduce codegen abstraction.
When the LHS is a scalar variable (no active A2A dimension and not
inside an array-reducing builtin), resolve DimPosition(@n) to a
concrete element offset in two places:

1. In the static subscript path (normalize_subscripts3 result handling),
   convert DimPosition ops to Single ops so the built view correctly
   collapses the dimension to a scalar element.

2. In the dynamic subscript path (lower_index_expr3), resolve @n to a
   1-based Expr::Const rather than returning
   ArrayReferenceNeedsExplicitSubscripts.

Also adds a fallback for mixed A2A contexts (e.g. cube[@1, *, @3])
where the active subscript doesn't match the target dimension.

Bounds checking rejects @0 and out-of-range @n with
MismatchedDimensions in both paths.

Switches dimension_position_single and dimension_position_and_wildcard
tests to use assert_compiles_incremental().
Add two tests in dimension_position_tests verifying that the
incremental compiler rejects invalid @n positions:

- dimension_position_zero_is_error: @0 is rejected (1-based, 0 is
  invalid)
- dimension_position_out_of_range_is_error: @5 is rejected for a
  size-3 dimension

Also fix normalize_subscripts3 to bail out on @0 rather than silently
treating it the same as @1 via saturating_sub.
The pos_1based == 0 condition in the static subscript path is
unreachable because normalize_subscripts3 already rejects @0, but
we keep the check as defense-in-depth. Add a comment explaining this.
Unify the repeated walk_expr_as_view + push(opcode) + push(PopView)
pattern into a single emit_array_reduce helper method. Refactor SUM,
SIZE, STDDEV, and the 1-arg paths of MIN/MAX to use it. No behavior
change -- this is a pure mechanical refactor.
…duce

The MEAN handler had an is_array guard that only matched
StaticSubscript and TempArray, missing Expr::Subscript with Range
indices. This caused MEAN with dynamic range arguments (e.g.
MEAN(data[start:end])) to fall through to the scalar mean path,
producing wrong results on the incremental compilation path.

Route all single-arg MEAN through emit_array_reduce, which calls
walk_expr_as_view and handles all array expression types including
dynamic ranges. The multi-argument scalar path remains for len > 1.
Now that MEAN routes through emit_array_reduce (which handles all
array expression types including dynamic subscript ranges), this
test passes on the incremental compilation path.
Replace all 20 assert_compiles() calls with assert_compiles_incremental()
in builtins_visitor.rs test module, migrating these tests from the
monolithic compilation path to the salsa-based incremental path.
Replace assert_compiles() with assert_compiles_incremental() in the
remaining 3 test functions: test_arrayed_1arg_previous_loadprev_per_element,
test_arrayed_2arg_previous_per_element_modules, and
test_nested_previous_does_not_create_false_cycle_via_helper_deps.
All callers have been migrated to assert_compiles_incremental(). The
compile() method is retained because it has active callers in
assert_compile_error_impl() and tests/simulate_ltm.rs.
Document @n position syntax resolution semantics in context.rs
(scalar vs A2A vs array-reducing builtin contexts) and the
emit_array_reduce helper in codegen.rs.
@claude
Copy link

claude bot commented Mar 8, 2026

Code Review

I reviewed the full diff across all 10 changed files. The changes are well-structured: the emit_array_reduce extraction cleanly eliminates 6x duplication, the @N position resolution in scalar context is correct with proper bounds checking in both the static and dynamic paths, the MEAN fix correctly aligns its single-arg behavior with the other array-reduce builtins, and the test migration is mechanical.

No bugs found.

Overall correctness: Correct. The patch is free of bugs and blocking issues. Existing code and tests will not break.

🤖 Generated with Claude Code

Copy link

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

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: 93a02c5f74

ℹ️ 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 +865 to +866
if args.len() == 1 {
// Check if the argument is an array expression
let arg = &args[0];
let is_array = matches!(
arg,
Expr::StaticSubscript(_, _, _) | Expr::TempArray(_, _, _)
);
if is_array {
// Array mean - use ArrayMean opcode
self.walk_expr_as_view(arg)?;
self.push(Opcode::ArrayMean {});
self.push(Opcode::PopView {});
return Ok(Some(()));
}
return self.emit_array_reduce(&args[0], Opcode::ArrayMean {});

Choose a reason for hiding this comment

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

P1 Badge Preserve scalar fallback for single-argument MEAN

Routing every one-argument MEAN(...) call through emit_array_reduce makes scalar inputs fail compilation, because walk_expr_as_view only accepts array-like nodes (StaticSubscript, TempArray, Var, Subscript) and rejects scalars like constants or compound scalar expressions. This is a regression from the previous behavior where MEAN(x) with a non-array argument used the scalar path and evaluated to x (sum/divide by 1), so models using forms like MEAN(42) or MEAN(a+b) now error on the incremental compiler.

Useful? React with 👍 / 👎.

@codecov
Copy link

codecov bot commented Mar 8, 2026

Codecov Report

❌ Patch coverage is 81.13208% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.87%. Comparing base (a8ea19e) to head (e93df9a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/simlin-engine/src/compiler/codegen.rs 73.68% 5 Missing ⚠️
src/simlin-engine/src/compiler/context.rs 83.87% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #387      +/-   ##
==========================================
+ Coverage   76.80%   76.87%   +0.07%     
==========================================
  Files         143      143              
  Lines       36171    36171              
==========================================
+ Hits        27781    27808      +27     
+ Misses       8390     8363      -27     

☔ View full report in Codecov by Sentry.
📢 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.

Phase files and test requirements for the three-phase migration:
P1 @n position syntax, P2 MEAN dynamic ranges, P3 bulk test switch.
@claude
Copy link

claude bot commented Mar 8, 2026

Code Review

Summary

This PR makes three substantive changes: (1) resolves @N position syntax to concrete element offsets in scalar context on the incremental compilation path, (2) extracts emit_array_reduce to deduplicate array-reduce builtin codegen, and (3) fixes MEAN with dynamic range subscripts by removing an overly narrow is_array guard. It then migrates all 26 assert_compiles() tests to assert_compiles_incremental() and removes the dead helper.

Analysis

@N position resolution (context.rs, subscript.rs): The two-site fix is correct. In normalize_subscripts3, @0 is properly rejected (returns None) instead of silently mapping to position 0 via saturating_sub. In context.rs, scalar context (active_dimension.is_none() && !preserve_wildcards_for_iteration) converts DimPosition to Single in the static path, and to a concrete 1-based Expr::Const in the dynamic fallback path. The A2A path is unchanged — DimPosition entries flow through to build_view_from_ops for dimension reordering as before.

emit_array_reduce (codegen.rs): Mechanical extraction. The push-view / emit-opcode / pop-view pattern is identical to what each builtin had inline. No behavioral change for SUM, SIZE, STDDEV, MIN, MAX.

MEAN fix (codegen.rs): The old code only checked for StaticSubscript and TempArray before routing to ArrayMean, missing Subscript (dynamic range). The new code routes all single-arg MEAN through emit_array_reduce, which correctly handles Subscript via walk_expr_as_view. For Var (scalar variable), walk_expr_as_view creates a 1-element contiguous view, so ArrayMean returns the scalar value — same as the old value / 1 fallback. This is a correct fix.

Test migration: Mechanical assert_compilesassert_compiles_incremental across 26 call sites, followed by deletion of the now-unused assert_compiles() method. New error tests for @0 and out-of-range @N provide good boundary coverage.

Findings

No bugs found. The changes are well-scoped, the emit_array_reduce extraction reduces duplication without changing behavior, the @N scalar-context resolution is properly guarded, and the MEAN fix correctly broadens the expression types accepted for array reduction.

Overall Correctness Verdict

Correct. The patch introduces no regressions — existing code paths produce identical results, the new scalar-context @N resolution is properly bounded and validated, and the MEAN dynamic-range fix aligns its behavior with the other array-reduce builtins (SUM, SIZE, STDDEV) that already handled this case.

Two bugs found during code review:

1. In A2A context, @n resolved via active subscripts even when the
   target dimension wasn't being iterated. For indexed dimensions with
   overlapping numeric ranges, this caused cross-dimension matching
   (e.g., row[Y] = matrix[@1, *] used the Y iteration value as an X
   subscript). Fix: only resolve @n through active subscripts when
   dims[i] is actually among the active iteration dimensions.

2. Single-arg MEAN unconditionally routed through emit_array_reduce,
   which calls walk_expr_as_view. That function only handles array-shaped
   expressions (StaticSubscript, TempArray, Var, Subscript) and errors
   on composite scalar expressions like Op2. Fix: match on expression
   type, routing only array forms through emit_array_reduce and
   evaluating scalar expressions directly.
@claude
Copy link

claude bot commented Mar 8, 2026

Code Review

I've reviewed the full diff across all 14 commits. The changes are well-structured and the code logic is sound.

Summary of changes reviewed:

  1. @n position syntax in scalar context (context.rs) — correctly resolves DimPosition(@N) to a concrete element offset when no A2A dimension is active, with proper bounds checking for @0 and out-of-range values
  2. normalize_subscripts3 @0 rejection (subscript.rs) — replaces saturating_sub(1) with explicit @0 bail-out, preventing silent treatment of @0 as @1
  3. emit_array_reduce extraction (codegen.rs) — clean mechanical refactor of the push-view/opcode/pop-view pattern, no behavior change
  4. MEAN dynamic range fix (codegen.rs) — routes single-arg MEAN through emit_array_reduce for array expressions, with an appropriate match guard to preserve scalar expression handling (MEAN(x + 1) goes through walk_expr, not walk_expr_as_view)
  5. A2A indexed dimension discrimination (patch 14, context.rs) — active_dims.iter().any(|ad| ad == dim) check correctly prevents false resolution when indexed dimension element names overlap across dimensions
  6. Test migration and assert_compiles() deletion — straightforward mechanical changes

Findings

No bugs found. The patch is clean. A few observations (not actionable):

  • The pos_1based == 0 check in the static path (context.rs:1238) is correctly documented as defensive/unreachable since normalize_subscripts3 already rejects @0.
  • The static path condition (active_dimension.is_none() && !preserve_wildcards_for_iteration) is slightly more conservative than the dynamic path (active_dimension.is_none() only), but this is harmless because preserve_wildcards_for_iteration is only set via with_preserved_wildcards() which preserves the caller's active_dimension — so when active_dimension is None, preserve_wildcards_for_iteration being true would only arise if explicitly constructed that way (which doesn't happen in current code).

Verdict: Correct

The patch should not break existing code or tests. All behavioral changes are covered by new and existing tests.

Exercises the Expr::Var arm in the MEAN handler which routes through
emit_array_reduce/walk_expr_as_view. Confirms the VM's ArrayMean
opcode handles a 1-element view correctly (returns the value itself).
@claude
Copy link

claude bot commented Mar 8, 2026

Code Review

Summary

This PR migrates all 26 assert_compiles() tests to assert_compiles_incremental(), fixes two incremental-compiler feature gaps (@N position syntax in scalar context and MEAN with dynamic range subscripts), extracts a shared emit_array_reduce helper, and deletes the now-unused assert_compiles() helper.

Findings

No bugs found. The changes are well-structured and thoroughly tested:

  • @N resolution in scalar context (context.rs:1226-1244): The two-tier handling (static path via normalize_subscripts3 + dynamic path via IndexExpr3::DimPosition) correctly resolves DimPosition to concrete element offsets. The @0 rejection in normalize_subscripts3 is a good improvement over the old saturating_sub(1) which silently treated @0 as @1.

  • A2A vs mixed-case discrimination (context.rs:2166-2199): Checking whether the target dimension is actively iterated (active_dims.iter().any(|ad| ad == dim)) before using active subscripts is the right approach for distinguishing pure reorders (matrix[@2, @1]) from mixed @N-with-wildcard cases (matrix[@1, *]). The removed parse::<usize> fallback was only needed for cross-dimension lookups on indexed dimensions where element names don't exist in the target dimension — a scenario that represents an invalid model configuration anyway.

  • emit_array_reduce extraction (codegen.rs:363-370): Clean mechanical extraction, behavior-preserving for SUM/SIZE/STDDEV/MIN/MAX.

  • MEAN routing (codegen.rs:864-877): Expanding the match to include Expr::Var and Expr::Subscript correctly routes dynamic-range MEAN through emit_array_reduce, while the _ fallback for scalar expressions (like MEAN(x + 1)) correctly walks the expression directly. Both new paths have dedicated test coverage.

Verdict: Correct

The patch is free of bugs. Test migration is mechanical and safe, the two feature-gap fixes are well-reasoned with good boundary-condition tests (@0, out-of-range @N, scalar MEAN), and the refactoring is behavior-preserving.


🤖 Generated with Claude Code

For named dimensions, element names are unique across dimensions, so
get_offset reliably distinguishes cross-dimension false positives. This
correctly handles subdimension cases (e.g. selected[SubRegion] =
data[@1] where SubRegion elements are a subset of Region).

For indexed dimensions, numeric element names overlap across unrelated
dimensions, so we require an exact dimension identity match to prevent
the mixed-wildcard bug (row[Y] = matrix[@1, *]).

Also adds a clarifying comment on the MEAN handler explaining why it
needs expression-type matching unlike other array-reduce builtins.
@claude
Copy link

claude bot commented Mar 8, 2026

Code Review

I reviewed all source code changes in this PR (compiler/context.rs, compiler/codegen.rs, compiler/subscript.rs, array_tests.rs, builtins_visitor.rs, db_tests.rs, db_prev_init_tests.rs, test_common.rs).

Findings

No bugs found.

Notes

  • The emit_array_reduce extraction is clean and correctly deduplicates the 6x copy-paste of the walk_expr_as_view + opcode + PopView pattern.
  • The MEAN fix properly distinguishes array-like expressions (StaticSubscript, TempArray, Var, Subscript) from scalar expressions (Op2, etc.), which is necessary because MEAN is variadic unlike the other array-reduce builtins. The two new regression tests (mean_of_scalar_expression, mean_of_scalar_var) validate both sides of this distinction.
  • The @N position syntax fix handles three cases correctly: scalar context (direct resolution), A2A context with matching dimension (reorder path), and A2A fallback for mixed cases. The indexed-dimension discrimination via allow_binding is well-motivated by the comment explaining that numeric element names overlap across unrelated indexed dimensions.
  • The @0 rejection in normalize_subscripts3 is cleaner than the old saturating_sub(1) approach that relied on overflow to eventually trigger bounds checks.
  • The dimension_position_and_wildcard_indexed_values test is a good addition that validates end-to-end correctness of the mixed @N/wildcard path with indexed dimensions.

Overall Correctness

Correct. The patch is well-structured, the logic changes are sound, and the test coverage is thorough. Existing code and tests will not break.

@bpowers
Copy link
Owner Author

bpowers commented Mar 8, 2026

Review cycle summary

Four iterations of automated review (codex + Claude) identified and fixed two genuine bugs, then refined the fix for correctness across dimension types.

Bugs fixed:

  1. Cross-dimension @n matching on indexed dimensions -- In A2A context, @N resolved via active subscripts even when the target dimension wasn't being iterated. For indexed dimensions with overlapping numeric ranges (e.g., row[Y] = matrix[@1, *] with indexed X and Y), the Y iteration value was incorrectly used as an X subscript. Fixed by checking dimension type: named dimensions use element-name matching (which handles subdimensions correctly), indexed dimensions require exact dimension identity.

  2. MEAN scalar expression regression -- Single-arg MEAN unconditionally routed through emit_array_reduce, which calls walk_expr_as_view. That function only handles array-shaped expressions and errored on composite scalar expressions like MEAN(x + 1). Fixed by matching on expression type, routing only array forms through the array-reduce path.

Both fixes include targeted regression tests written before the implementation (TDD). A third test (mean_of_scalar_var) was added to cover the MEAN(scalar_var) path through ArrayMean.

@bpowers bpowers merged commit db31ea6 into main Mar 8, 2026
12 checks passed
@bpowers bpowers deleted the position-and-mean branch March 8, 2026 20:00
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.

Incremental path: @N position syntax and MEAN with dynamic ranges unsupported

1 participant