Skip to content

fix(pipeline): widen lower_where sig to accept any predicate expression#40

Open
cuzzo wants to merge 1 commit into
masterfrom
hotfix-pipeline-where
Open

fix(pipeline): widen lower_where sig to accept any predicate expression#40
cuzzo wants to merge 1 commit into
masterfrom
hotfix-pipeline-where

Conversation

@cuzzo
Copy link
Copy Markdown
Owner

@cuzzo cuzzo commented May 9, 2026

Summary

PipelineHost#lower_where had a Sorbet sig that constrained expr_node to AST::BinaryOp, but CLEAR's WHERE clause accepts ANY boolean expression. The narrow sig caused a runtime Sorbet TypeError for any non-BinaryOp predicate.

Found while VM-stress-testing the register backend: transpile-tests/80_concurrent_where.cht uses WHERE isPositive(_.value) (a FuncCall predicate) and tripped the sig.

Why master appeared green

The Zig backend's pipeline pipeline routes CONCURRENT operators through the legacy string-based transpile_pipeline path, which doesn't reach lower_where for these expressions. The bc emitter's bc-specific concurrent dispatch (lower_bc_concurrent_where) calls lower_where directly with the user-written predicate — that's where the sig fires. So the bug was latent on master and only surfaced via the register VM.

Fix

One line: match visit_pipeline_expr_mir's T.untyped (which the method's body delegates to). A narrower union (T.any(AST::BinaryOp, AST::FuncCall, AST::Literal)) would still reject other valid shapes (Identifier, UnaryOp, MethodCall, ...). Sibling methods lower_select and lower_take_while have no sig at all — this brings lower_where into line.

Regression test

spec/pipeline_host_lower_where_sig_spec.rb constructs a lower_where call with each of 6 valid predicate AST shapes (BinaryOp / FuncCall / Literal / Identifier / UnaryOp / MethodCall) and asserts no Parameter 'expr_node' TypeError is raised.

Without this fix, 5 of the 6 cases fail. Verified by stashing the fix and re-running the spec.

Test plan

  • bundle exec rspec spec/pipeline_host_lower_where_sig_spec.rb — 7 examples, 0 failures
  • bundle exec prspec spec/ — 4691 examples, 0 failures
  • ./clear test transpile-tests/ — 540 passed, 0 leaks
  • transpile-tests/80_concurrent_where.cht passes on the register VM (will be visible on register-machine PR rebase)

🤖 Generated with Claude Code

PipelineHost#lower_where had `expr_node: AST::BinaryOp` in its
Sorbet sig. But CLEAR's `WHERE` clause accepts ANY boolean
expression: comparisons (`x > 0`), function calls (`isPositive(x)`),
literals (`TRUE`), identifiers (`flag`), unary ops (`!cond`),
method calls (`x.isPositive()`), etc. The narrow sig caused a
runtime Sorbet TypeError on every non-BinaryOp predicate that
flowed through this method:

  Parameter 'expr_node': Expected type AST::BinaryOp, got type
  AST::FuncCall ...

The Zig backend's `lower_pipeline` didn't typically reach
`lower_where` for these expressions (its CONCURRENT path falls
back to the legacy string-based `transpile_pipeline`), which is
why master tests stayed green -- the bug only surfaced via the
register-VM emitter's bc-specific concurrent dispatch
(register_bc_emitter.rb routing CONCURRENT WHERE through
PipelineHost#lower_where with the user-written predicate).

Fix: match `visit_pipeline_expr_mir`'s `T.untyped` (the method
`lower_where`'s body delegates to). A union like
`T.any(AST::BinaryOp, AST::FuncCall)` would still reject other
valid predicate shapes; matching the sibling's precedent is the
correct width.

Sister methods `lower_select` and `lower_take_while` have no sig
at all -- they take any expression too. This brings `lower_where`
into line.

New regression spec: spec/pipeline_host_lower_where_sig_spec.rb
constructs a lower_where call with each of the 6 valid predicate
AST shapes and asserts no `Parameter 'expr_node'` TypeError is
raised. Without this fix, 5 of the 6 cases fail.

Found by VM stress-test: 80_concurrent_where (CONCURRENT WHERE
isPositive(_.value)) on the bc target.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 9, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.08%. Comparing base (08b4c61) to head (e3c1f69).
⚠️ Report is 4 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #40      +/-   ##
==========================================
- Coverage   90.09%   90.08%   -0.02%     
==========================================
  Files         185      185              
  Lines       49984    49987       +3     
  Branches    11984    11986       +2     
==========================================
- Hits        45035    45031       -4     
- Misses       4949     4956       +7     
Flag Coverage Δ
ruby 86.76% <100.00%> (+<0.01%) ⬆️
zig 95.89% <ø> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

🐰 Bencher Report

Branchhotfix-pipeline-where
Testbedubuntu-latest

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
Benchmarkleak-build-msMeasure (units) x 1e3leak-countMeasure (units)leak-run-msMeasure (units)
benchmarks/concurrent/05_backpressure/bench📈 view plot
⚠️ NO THRESHOLD
5.45 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,648.35 units
benchmarks/concurrent/10_shard_vs_locked/bench📈 view plot
⚠️ NO THRESHOLD
5.23 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
60,004.42 units
benchmarks/concurrent/16_observables/bench📈 view plot
⚠️ NO THRESHOLD
5.37 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
96.93 units
benchmarks/inter-clear/03_concurrent_mvcc_vs_rwlock/bench📈 view plot
⚠️ NO THRESHOLD
2.77 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
371.86 units
benchmarks/sequential/07_pointer_chase/bench📈 view plot
⚠️ NO THRESHOLD
1.79 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
553.17 units
benchmarks/sequential/12_weak_ref_graph/bench📈 view plot
⚠️ NO THRESHOLD
1.82 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
259.83 units
benchmarks/server/03_pathological/server📈 view plot
⚠️ NO THRESHOLD
1.94 units x 1e3📈 view plot
⚠️ NO THRESHOLD
0.00 units📈 view plot
⚠️ NO THRESHOLD
1,002.75 units
🐰 View full continuous benchmarking report in Bencher

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.

2 participants