Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions spec/pipeline_host_lower_where_sig_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require "rspec"
require "sorbet-runtime"
require_relative "../src/ast/lexer"
require_relative "../src/ast/ast"
require_relative "../src/backends/pipeline_host"

# Regression: PipelineHost#lower_where had a Sorbet sig that constrained
# `expr_node` to `AST::BinaryOp`. CLEAR's `WHERE` clause accepts ANY
# boolean expression: `WHERE x > 0` (BinaryOp), `WHERE isPositive(x)`
# (FuncCall), `WHERE TRUE` (Literal), `WHERE flag` (Identifier),
# `WHERE !blocked` (UnaryOp), etc. The narrow sig caused a Sorbet
# TypeError at runtime for any non-BinaryOp predicate that flowed
# through the bc emitter's CONCURRENT pipeline path.
#
# The Zig backend doesn't typically reach `lower_where` for the same
# expressions (its lower_pipeline path falls back to the legacy
# string-based `transpile_pipeline` for CONCURRENT operators), which
# is why master tests passed -- the bug only surfaced via the bc
# target's bc-specific concurrent dispatch.
RSpec.describe "PipelineHost#lower_where signature" do
it "accepts any expression node (not just AST::BinaryOp)" do
sig = T::Utils.signature_for_method(PipelineHost.instance_method(:lower_where))
expr_pair = sig.arg_types.find { |name, _| name == :expr_node }
expect(expr_pair).not_to be_nil
expr_type = expr_pair[1]

# The fix matches `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 (Literal, Identifier, UnaryOp,
# MethodCall, ...). Match the precedent.
expect(expr_type).to be_a(T::Types::Untyped),
"expected T.untyped to match visit_pipeline_expr_mir; got #{expr_type.inspect}"
end

# Behavioral check: calling lower_where with each valid predicate
# shape must NOT raise a Sorbet TypeError. The body fails for
# unrelated reasons (we pass stub site / no lowering setup), but a
# TypeError specifically would mean the sig rejected the input
# shape -- the regression we're guarding against.
[
[:BinaryOp, :"`x > 0` (BinaryOp)"],
[:FuncCall, :"`isPositive(_)` (FuncCall)"],
[:Literal, :"`TRUE` (Literal)"],
[:Identifier, :"`flag` (Identifier)"],
[:UnaryOp, :"`!cond` (UnaryOp)"],
[:MethodCall, :"`x.isPositive()` (MethodCall)"],
].each do |ast_class, label|
it "does not raise TypeError when expr_node is #{label}" do
tok = Lexer::Token.new(:VAR_ID, "x", 1, 1)
node = case ast_class
when :BinaryOp then AST::BinaryOp.new(tok, AST::Identifier.new(tok, "x"), :GT, AST::Literal.new(tok, :INT64, 0, :stack))
when :FuncCall then AST::FuncCall.new(tok, "isPositive", [])
when :Literal then AST::Literal.new(tok, :BOOLEAN, true, :stack)
when :Identifier then AST::Identifier.new(tok, "flag")
when :UnaryOp then AST::UnaryOp.new(tok, :NOT, AST::Identifier.new(tok, "cond"))
when :MethodCall then AST::MethodCall.new(tok, AST::Identifier.new(tok, "x"), "isPositive", [])
end

host = PipelineHost.allocate
site = PipelineHost::PipelineSite.new(list: nil, options: nil)

begin
host.send(:lower_where, site, node)
rescue TypeError => e
# If the sig rejects this shape, Sorbet raises TypeError with
# "Parameter 'expr_node': Expected type ..., got type ...".
# That's the regression. Anything else (TypeError on `site`,
# NoMethodError, etc.) is fine -- it just means the body
# tried to do real work against the stub and failed downstream.
if e.message.include?("Parameter 'expr_node'")
fail "Sorbet sig rejected #{label}: #{e.message[0, 200]}"
end
# Other TypeErrors (e.g. site validation) — body fired, sig OK.
rescue
# Body failed for unrelated reasons — that's expected with no
# full lowering setup. The sig accepted the input.
end
end
end
end
8 changes: 7 additions & 1 deletion src/backends/pipeline_host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,13 @@ def pipeline_alloc(smooth_node)
smooth_node.respond_to?(:storage) && smooth_node.storage == :heap ? :heap : :frame
end

sig { params(site: PipelineHost::PipelineSite, expr_node: AST::BinaryOp).returns(MIR::BlockExpr) }
# `expr_node` is any boolean-typed expression: BinaryOp (`x > 0`),
# FuncCall (`isPositive(x)`), Literal (`TRUE`), Identifier (`flag`),
# MethodCall, UnaryOp, etc. Match `visit_pipeline_expr_mir`'s
# `T.untyped` (line 676) since the body just delegates to it; a
# narrower union (e.g., `T.any(AST::BinaryOp, AST::FuncCall)`) would
# reject other valid predicates at runtime via Sorbet's validator.
sig { params(site: PipelineHost::PipelineSite, expr_node: T.untyped).returns(MIR::BlockExpr) }
def lower_where(site, expr_node)
list_node = site.list
smooth_node = site.options
Expand Down
Loading