Skip to content

fix(parser): bind modifier rescue to whole paren-less command call - #15

Merged
tannevaled merged 1 commit into
mainfrom
fix/modifier-rescue-command-precedence
Jul 31, 2026
Merged

fix(parser): bind modifier rescue to whole paren-less command call#15
tannevaled merged 1 commit into
mainfrom
fix/modifier-rescue-command-precedence

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Problem

A trailing modifier rescue after a paren-less command call bound to the last argument instead of the whole call:

raise "x" rescue 42   # parsed as raise("x" rescue 42)  -> raise ran unrescued

MRI parses this as (raise "x") rescue 42 (Ripper: rescue_mod(command(raise, ["x"]), 42)), so the value is 42. The parenthesised form method(args) rescue x was already correct.

Fix

The modifier rescue has lower precedence than a command call's paren-less argument list. A new noRescueMod flag (mirroring the existing noDo / noMasgn) suppresses the modifier rescue while parsing command arguments, so the enclosing expression wraps the whole Call in a Begin — identical to the parenthesised form. The flag is cleared inside any nested delimited context (a (…) group, a (…)/[…] argument list), so an inner modifier rescue there is still consumed (foo(bar rescue baz), p ("x" rescue 42)).

Also accepts a modifier rescue after a def…end statement (def foo; end rescue nil) — MRI accepts it, the parser previously errored — by handling rescue in applyModifiers.

AST proof

raise "x" rescue 42 now yields Begin{ Body:[ Call{raise, ["x"]} ], Rescues:[{ Body:[42] }] } — the rescue wraps the Call, and each argument stays a plain literal.

Unchanged (verified)

  • modifier rescue on a plain expression / paren group ("x" rescue 42)
  • assignment RHS x = expr rescue default
  • parenthesised call method(args) rescue x

Verification

MRI ruby 4.0.x match for (raise "boom" rescue 99)=>99, (raise "x","y" rescue 7)=>7, puts "a" rescue nil (runs, no rescue), def foo; end rescue nil (accepted), assignment-RHS rescue=>9.

  • 100% statement coverage
  • go vet ./... clean
  • GOWORK=off CGO_ENABLED=0

🤖 Generated with Claude Code

A trailing modifier `rescue` after a paren-less command call bound to the
last argument instead of the whole call: `raise "x" rescue 42` parsed as
`raise("x" rescue 42)` (so the raise ran unrescued) instead of MRI's
`(raise "x") rescue 42`. Ripper confirms `rescue_mod(command(raise, ["x"]), 42)`.

The modifier `rescue` has lower precedence than a command call's paren-less
argument list. Suppress its consumption while parsing command arguments (new
noRescueMod flag, mirroring noDo/noMasgn) so the enclosing expression wraps the
whole Call in a Begin, matching the already-correct parenthesised form
`method(args) rescue x`. The flag is cleared inside any nested delimited context
(a `(…)` group, a `(…)`/`[…]` argument list) so an inner modifier rescue there
is still consumed (`foo(bar rescue baz)`, `p ("x" rescue 42)`).

Also accept a modifier `rescue` after a `def…end` statement
(`def foo; end rescue nil`), which MRI accepts but the parser rejected, by
handling `rescue` in applyModifiers (the keyword-statement modifier path).

Unchanged: modifier rescue on a plain expression, on an assignment RHS
(`x = expr rescue default`), and the parenthesised-call form.

100% statement coverage; go vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit addd08f into main Jul 31, 2026
11 checks passed
@tannevaled
tannevaled deleted the fix/modifier-rescue-command-precedence branch July 31, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant