Project

General

Profile

Actions

Bug #21925

open

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) about 21 hours ago. Updated about 2 hours ago.

Status:
Open
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.
Actions

Also available in: PDF Atom