Bug #21925
closedPrism 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.
Updated by knu (Akinori MUSHA) 21 days ago
Created a PR: https://github.com/ruby/ruby/pull/16256
Updated by Earlopain (Earlopain _) 20 days ago
Likely a duplicate of or at the very least related to https://bugs.ruby-lang.org/issues/21674
@kddnewton (Kevin Newton) can you take this?
Updated by kddnewton (Kevin Newton) 17 days ago
Yeah, looking
Updated by kddnewton (Kevin Newton) 17 days ago
- Status changed from Open to Closed
Applied in changeset git|c7ed328cd569a258aa949f78f721195e2be15e9e.
[ruby/prism] Fix in handling
in is a unique keyword because it can be the start of a clause or
an infix keyword. We need to be explicitly sure that even though in
could close an expression context (the body of another in clause)
that we are not also parsing an inline in. The exception is the
case of a command call, which can never be the LHS of an expression,
and so we must immediately exit.
Updated by knu (Akinori MUSHA) 14 days ago
- Backport changed from 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: REQUIRED, 4.0: REQUIRED
Updated by Earlopain (Earlopain _) 14 days ago
- Assignee set to prism
- Backport changed from 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: REQUIRED, 4.0: REQUIRED to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED
Updated by k0kubun (Takashi Kokubun) 3 days ago
- Backport changed from 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE
ruby_4_0 b5a768b666f61a861449d9ee287cb0a3e05bbea8.