Bug #21925
openPrism misparses standalone "in" pattern matching in "case/in"
Description
Prism misparses standalone in pattern matching in case/in when the left-hand side is a method call with a dot receiver
Summary¶
When a standalone expr in pattern one-line pattern match appears inside a case/in branch, and expr is a method call with a dot receiver (for example, obj.method), Prism incorrectly parses in pattern as a new branch of the outer case/in instead of treating it as a MatchPredicateNode. The parse.y parser handles this case correctly.
Reproduction¶
$ ruby -v
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin23]
It also reproduces on Ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin25].
Minimal script¶
x = "x"
case x
in Integer
nil
in String
p :before
x.itself in String
p :after
end
Command output¶
$ ruby -e '
x = "x"
case x
in Integer
nil
in String
p :before
x.itself in String
p :after
end
'
:before
$ ruby --parser=parse.y -e '
x = "x"
case x
in Integer
nil
in String
p :before
x.itself in String
p :after
end
'
:before
:after
Expected behavior (parse.y)¶
Both :before and :after are printed. x.itself in String is parsed as a standalone pattern match (predicate) inside the in String branch.
Actual behavior (Prism)¶
Only :before is printed; p :after is never executed.
Analysis¶
Prism splits x.itself in String at the in keyword, treating x.itself as the last statement of the in String branch and in String as a third branch of the outer case/in.
The following script shows how each parser interprets the case/in branches:
code = <<~RUBY
x = "x"
case x
in Integer
nil
in String
x.itself in String
p :after
end
RUBY
# Prism
require "prism"
ast = Prism.parse(code).value.statements.body[1]
puts "Prism: conditions: #{ast.conditions.length}"
ast.conditions.each_with_index do |c, i|
pat = c.pattern
name = pat.respond_to?(:name) ? pat.name : pat.class
stmts = c.statements&.body&.map { |s| s.class.name.split("::").last } || []
puts " in[#{i}]: pattern=#{name}, body=#{stmts}"
end
# parse.y (Ripper)
require "ripper"
sexp = Ripper.sexp(code)
case_node = sexp[1][1]
def count_in_branches(node)
return [] unless node.is_a?(Array) && node[0] == :in
pattern_node = node[1]
body = node[2] || []
name = pattern_node.is_a?(Array) && pattern_node[1].is_a?(Array) ? pattern_node[1][1] : pattern_node[1]
stmts = body.map { |s| s[0].to_s }
[{ pattern: name, body: stmts }] + count_in_branches(node[3])
end
branches = count_in_branches(case_node[2])
puts "\nparse.y (Ripper): conditions: #{branches.length}"
branches.each_with_index do |b, i|
puts " in[#{i}]: pattern=#{b[:pattern]}, body=#{b[:body]}"
end
Output (run with ruby --parser=parse.y so Ripper uses parse.y):
Prism: conditions: 3
in[0]: pattern=Integer, body=["NilNode"]
in[1]: pattern=String, body=["CallNode"]
in[2]: pattern=String, body=["CallNode"]
parse.y (Ripper): conditions: 2
in[0]: pattern=Integer, body=["var_ref"]
in[1]: pattern=String, body=["case", "command"]
Prism reports 3 branches, while parse.y reports 2. In the parse.y result, in[1] correctly contains both the inner expression (x.itself in String parsed as a standalone pattern match) and the command (p :after). In the Prism result, x.itself becomes the sole body of in[1], and in String is misinterpreted as a new in[2] branch of the outer case.
Consideration¶
From experimentation, the issue appears to be triggered only when all of the following are true:
- The outer
case/inhas at least twoinbranches before the affected branch. - The standalone
inexpression uses a dot-receiver method call as its left-hand side, such asobj.method in Pattern.
Conditions where this behavior is not observed:
- If the expression is a receiver-less call, such as
f(x) in Pattern, the parser keeps it as a standalone pattern match. - If the receiver expression is parenthesized, such as
(obj.method) in Pattern, the parser keeps it as a standalone pattern match. - If you assign first, such as
y = obj.method; y in Pattern, the parser keeps it as a standalone pattern match.