Bug #22310
openparse.y rejects a variable binding that precedes an unrelated alternative pattern
Description
parse.y rejects patterns in which a variable binding appears before
an alternative pattern, even when the alternative itself binds nothing.
Prism accepts the same code. Is this intended?
Reproduction¶
$ ruby --parser=parse.y -c ok.rb
Syntax OK
$ ruby --parser=parse.y -c ng.rb
ng.rb:2: alternative pattern after variable capture (SyntaxError)
$ ruby --parser=prism -c ng.rb
Syntax OK
The two patterns are semantically identical; only the order of the
array elements differs. Both files run without error under the default
parser.
Other forms rejected by parse.y (all accepted by Prism)¶
in { a: String => t, b: Integer | String } # binding, then alternative
in { a: String => t, b: { c: Integer | String } } # alternative nested deeper
in [ one, "a" | "b" => two ] # form reported in rails/bootsnap issue 539
in { access_token: String => token, expires_in: (Integer | String) => expires_in }
# form shipped in the anthropic gem
Reversing the order makes them pass:
For contrast, a binding inside the alternation is rejected by both
parsers on 4.0.4, as expected:
Why this looks unintended¶
:a | :b and Integer | String bind no variables, so the documented
restriction -- "Binding to variables currently does NOT work for
alternative patterns joined with |" -- is not violated. Nothing can be
left unbound.
The check in p_alt tests p->ctxt.capture_in_pattern, which is set by
any binding anywhere in the same in clause, not only by a binding on
the left-hand side of the |:
p_alt : p_alt[left] '|'[alt]
{
p->ctxt.in_alt_pattern = 1;
}
p_expr_basic[right]
{
if (p->ctxt.capture_in_pattern) {
yyerror1(&@alt, "alternative pattern after variable capture");
}
Prism instead visits the left-hand node when it reaches the |
(parse_pattern_alternation_error in prism.c), so sibling bindings are
not affected.
The check was introduced in
https://github.com/ruby/ruby/commit/f4b6a5191ceb0ed0cd7a3e3c8bab24cc0dd15736
([Feature #21572]). The discussion in #21572 and in the dev meeting
(https://github.com/ruby/dev-meeting-log/blob/master/2025/DevMeeting-2025-10-23.md)
only covers bindings inside an alternation, and
test/ruby/test_pattern_matching.rb only asserts the passing order
(in [ :a | :b, x]). The p_alt rule is unchanged on current master.
Impact¶
Not visible under the default parser. However,
RubyVM::InstructionSequence.compile_file was routed to parse.y until
[Bug #22023] was fixed, so bootsnap (which uses it to build its bytecode
cache) made gems fail to load on Ruby 4.0.1-4.0.3 while ruby foo.rb
worked. That path is fixed in 4.0.4 and worked around in bootsnap
1.24.2, but the parse.y check itself remains.
Prior sightings¶
The same rejection has been observed before and was each time attributed
to the compile_file routing bug ([Bug #22023]):
- https://github.com/rails/bootsnap/issues/539 (cause identified as
https://github.com/ruby/ruby/pull/16463, later backported as #22023) - https://github.com/ruby/irb/issues/1212 ("in Ruby
4.0.2parse.yhas
a bug, but the important part here is that this is usingparse.y") - https://github.com/anthropics/anthropic-sdk-ruby/pull/192, whose author
noticed that swapping the hash-pattern key order avoids the error
As far as I can find, whether the parse.y check itself is correct has not
been raised.
Environment¶
ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [arm64-darwin25] # rejects with --parser=parse.y
ruby 4.0.3 (2026-04-21 revision 85ddef263a) +PRISM [arm64-darwin25] # same; also via compile_file
ruby 3.4.9 (2026-03-11 revision 76cca827ab) +PRISM [arm64-darwin25] # parse.y accepts all of the above
Updated by Earlopain (Earlopain _) 7 days ago
In short, https://github.com/ruby/ruby/pull/14923 made the check for parse.y too strict. It used to accept it, now doesn't anymore.
As far as I can find, whether the parse.y check itself is correct has not been raised.
The syntax check was intended to mirror the runtime check, so it's clearly a parse.y bug.
Updated by Earlopain (Earlopain _) 7 days ago
- Related to Feature #21572: Make illegal variable in alternation pattern a syntax error added
Updated by yuki300 (y h) 6 days ago
Thanks, that matches what I was seeing.
I got curious about which change actually started rejecting these, so I built the two commits around https://github.com/ruby/ruby/pull/15329 and ran the ng.rb from the description through --parser=parse.y -c:
a211abbcbd (parent of PR 15329; its parse.y is identical to the PR 14923 merge) Syntax OK
dfdc5d40ec (PR 15329) alternative pattern after variable capture
So https://github.com/ruby/ruby/pull/14923 by itself was fine here. It reset capture_in_pattern on every p_expr reduction, so a binding in one array/hash element never leaked into its siblings. PR 15329 removed that reset to catch nested captures like in [a] | 1, and since then the flag just lives for the whole in clause. That's the point where in [ x, :a | :b ] started failing.
I've put up https://github.com/ruby/ruby/pull/18830 with a fix. It does roughly what Prism does: the flag stays as a cheap pre-check, and when we hit | we walk the left operand's nodes and only raise if there really is an LASGN/DASGN in there. There's also a small p_alt_begin rule to save and restore in_alt_pattern, otherwise in 1 | [ 2 | 3, a ] would slip through once the false positive is gone. With the patch, every form I tried gives the same answer under parse.y and Prism, and test_pattern_matching.rb, test_syntax.rb, test_parse.rb and test/ripper pass on current master with RUN_OPTS=--parser=parse.y. I added assertions for the order that wasn't covered; the existing suite passes on the unpatched tree, which is why nothing caught this.
One more thing I ran into while testing: **rest in a pattern never goes through error_duplicate_pattern_variable, so parse.y accepts both { a: x, **x } and 1 | { a: 1, **rest } at parse time while Prism rejects them. The duplicate case isn't new, 3.4.9's parse.y accepts it too (and Prism 3.4.9 already rejected it); the alternation case only shows up in 4.0 because that's when the parse-time check arrived. Since fixing it turns code parse.y has accepted for a long time into a SyntaxError, I kept it out of this fix and opened it separately as https://github.com/ruby/ruby/pull/18831. No ticket for that one yet; happy to file one if it should be tracked on its own.
Updated by yuki300 (y h) 6 days ago
Filed the **rest one separately as #22314 (fix in https://github.com/ruby/ruby/pull/18831).