Project

General

Profile

Actions

Bug #21925

closed

Prism misparses standalone "in" pattern matching in "case/in"

Bug #21925: Prism misparses standalone "in" pattern matching in "case/in"

Added by knu (Akinori MUSHA) 21 days ago. Updated 3 days ago.

Status:
Closed
Assignee:
Target version:
ruby -v:
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin23]
[ruby-core:124885]
Tags:

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:

  1. The outer case/in has at least two in branches before the affected branch.
  2. The standalone in expression uses a dot-receiver method call as its left-hand side, such as obj.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 Earlopain (Earlopain _) 21 days ago Actions #2 [ruby-core:124889]

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 Actions #3 [ruby-core:124899]

Yeah, looking

Updated by kddnewton (Kevin Newton) 17 days ago Actions #4

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

[Bug #21925]
[Bug #21674]

https://github.com/ruby/prism/commit/20374ced51

Updated by knu (Akinori MUSHA) 14 days ago Actions #5

  • 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 Actions #6

  • 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 Actions #7 [ruby-core:125025]

  • 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
Actions

Also available in: PDF Atom