Issue #21925 has been updated by Earlopain (Earlopain _). Likely a duplicate of or at the very least related to https://bugs.ruby-lang.org/issues/21674 @kddnewton can you take this? ---------------------------------------- Bug #21925: Prism misparses standalone "in" pattern matching in "case/in" https://bugs.ruby-lang.org/issues/21925#change-116548 * Author: knu (Akinori MUSHA) * Status: Open * Target version: 4.1 * ruby -v: ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin23] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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 ```ruby 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: ```ruby 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. -- https://bugs.ruby-lang.org/