From 0f1532cccabdbdd6ef33f8e32d36e31d9a2e6c74 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 26 Sep 2026 22:44:40 +0200 Subject: [PATCH 1/2] parser: refuse a modifier-`rescue` in an `arg` position, and a command as a nested `!` operand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `arg` has no `modifier_rescue` alternative in parse.y v3_4_0 — only `stmt` (3184) and the assignment right-hand sides `arg_rhs` (4162), `command_rhs` and `endless_arg` do. So `f(x rescue nil)`, `[x rescue nil]`, `a[x rescue nil]`, `{k: x rescue nil}`, `case x; when y rescue nil` and, because `expr` has no such arm either, `if x rescue nil; end` are all SyntaxErrors in MRI, while `f((x rescue nil))` and `f(a = 1 rescue nil)` are not. `expr: '!' command_call` (3350) admits a paren-less command as the operand of `!`, but the operand is a `command_call`, not an `expr`, so the privilege does not nest: `!foo bar` parses and `!!foo bar` does not. `opt_rescue` (5926) and `case_body` (5397) both REQUIRE the `then` nonterminal. Without it the stray token of an `arg_value` we had already correctly refused — the `bar` of `rescue foo bar` — was read as the clause's first statement and the clause parsed anyway. Mechanism: rescueModOK inverts noRescueMod so the zero value is the refusing one, permitRescueMod is its only writer and withRescueModifier its only reader, mirroring argOnly/permitCommand/commandArgsFollow. Co-Authored-By: Claude Opus 5 --- modifier_rescue_command_test.go | 27 +++-- parser.go | 196 ++++++++++++++++++++++++-------- 2 files changed, 167 insertions(+), 56 deletions(-) diff --git a/modifier_rescue_command_test.go b/modifier_rescue_command_test.go index f47265f..2e21c26 100644 --- a/modifier_rescue_command_test.go +++ b/modifier_rescue_command_test.go @@ -3,6 +3,7 @@ package parser_test import ( "testing" + "github.com/go-ruby-parser/parser" "github.com/go-ruby-parser/parser/ast" ) @@ -156,9 +157,11 @@ func TestModifierRescueUnchangedNonCommand(t *testing.T) { }) } -// TestModifierRescueInsideDelimitedArg checks that a modifier rescue inside a -// nested delimited context (an explicit paren group, or a parenthesised argument -// list) is still consumed there — the command-call suppression must not leak in. +// TestModifierRescueInsideDelimitedArg checks where a modifier rescue nested +// inside a delimited context is consumed. A `(…)` GROUP is MRI's `compstmt`, so +// the rescue is consumed there; a `(…)` ARGUMENT LIST is not — its contents are +// `call_args`, which has no `modifier_rescue` alternative. Both directions +// measured on ruby 4.0.5 with `ruby -c` and `ruby --parser=parse.y -c`. func TestModifierRescueInsideDelimitedArg(t *testing.T) { // Explicit paren group as a command argument: the rescue binds inside the // group, so the group becomes a Begin argument of the command. @@ -171,10 +174,20 @@ func TestModifierRescueInsideDelimitedArg(t *testing.T) { } }) - // Parenthesised argument list nested inside a command call: the inner rescue - // belongs to that argument (`foo(bar rescue baz)`), while `outer` keeps both. - t.Run("paren arglist inside command", func(t *testing.T) { - const src = `outer foo(bar rescue baz)` + // A parenthesised ARGUMENT LIST is not a statement context: `f(bar rescue + // baz)` is a SyntaxError in MRI, which wants `f((bar rescue baz))`. Both of + // MRI's parsers refuse it — prism says "unexpected 'rescue' modifier; expected + // a `)` to close the arguments" — because `call_args`/`args`/`arg_value` offer + // no `modifier_rescue` arm; only `stmt` (3184) and `arg_rhs` (4162) do. This + // subtest previously asserted the opposite and pinned our own divergence. + t.Run("paren arglist inside command refuses it", func(t *testing.T) { + for _, src := range []string{`outer foo(bar rescue baz)`, `foo(bar rescue baz)`} { + if _, err := parser.Parse(src); err == nil { + t.Errorf("Parse(%q) = nil error, want a parse error (MRI 4.0.5 refuses it)", src) + } + } + // The shape MRI does accept: the argument's own parentheses make it a group. + const src = `outer foo((bar rescue baz))` outer := wantCall(t, src, parseOne(t, src), "outer", 1) foo := wantCall(t, src, outer.Args[0], "foo", 1) body, _ := beginWrapping(t, src, foo.Args[0]) diff --git a/parser.go b/parser.go index d748c5e..38ed2db 100644 --- a/parser.go +++ b/parser.go @@ -143,13 +143,31 @@ type Parser struct { // default (`def f(a = c(1), b = nil)`), so the comma separating parameters is // not mistaken for a masgn target separator (`c(1), b = nil`). noMasgn bool - // noRescueMod suppresses the trailing modifier-`rescue` while parsing the - // arguments of a paren-less command call, so the `rescue` binds to the whole - // command rather than to its last argument: `raise "x" rescue 42` is - // `(raise "x") rescue 42`, not `raise("x" rescue 42)`. It is cleared inside any - // nested delimited context (a parenthesised group, a `(…)`/`[…]` argument list, - // a `{…}` hash), where an inner modifier-`rescue` is again allowed. - noRescueMod bool + // rescueModOK marks a grammar position that HAS a `modifier_rescue` + // alternative. MRI has exactly two families of them and `arg` is not one: + // `stmt: stmt modifier_rescue after_rescue stmt` (parse.y v3_4_0 3184), which + // every `compstmt` reaches, and the assignment right-hand sides + // `arg_rhs: arg modifier_rescue after_rescue arg` (4162) with its + // `command_rhs`/`mrhs_arg`/`endless_arg` siblings (3219, 3305, 3323, 4081). + // There is NO `arg: arg modifier_rescue arg` production, so a + // modifier-`rescue` is a SyntaxError in every `arg` position — `[x rescue + // nil]`, `{k: x rescue nil}`, `f(1, x rescue nil)`, `a[x rescue nil]`, + // `case x; when y rescue nil` — and in every `expr_value` position too, since + // `expr` has no such alternative either: `if x rescue nil; end` and `while x + // rescue nil; end` are SyntaxErrors where `if (x rescue nil); end` is not. + // The assignment carve-out is what makes `[a = x rescue nil]` and + // `f(a = 1 rescue nil)` legal in the very positions that refuse the bare form. + // + // It is false through a paren-less command call's OWN argument list too, so + // the `rescue` binds to the whole command rather than to its last argument: + // `raise "x" rescue 42` is `(raise "x") rescue 42` (`stmt modifier_rescue + // stmt`), not `raise("x" rescue 42)`. + // + // The zero value is therefore the refusing one, and it is inherited by the + // whole expression descent; only the positions MRI gives a production to turn + // it on. permitRescueMod is the only writer and withRescueModifier the only + // reader. + rescueModOK bool // argOnly marks a grammar position MRI reaches through `arg_value: arg` // (parse.y v3_4_0 4133), which has NO `command` alternative: an array-literal // element, a hash key or value, a splat/double-splat/block-pass operand, any @@ -368,6 +386,16 @@ func (p *Parser) permitCommand(allowed bool) func() { return func() { p.argOnly = saved } } +// permitRescueMod records whether the position about to be parsed admits a +// trailing modifier-`rescue` (MRI's `modifier_rescue`) and returns the function +// restoring the previous answer; use it as `defer p.permitRescueMod(false)()`. +// It is the only writer of rescueModOK — see that field for the rule it carries. +func (p *Parser) permitRescueMod(allowed bool) func() { + saved := p.rescueModOK + p.rescueModOK = allowed + return func() { p.rescueModOK = saved } +} + // commandArgShape selects which token shapes may open a paren-less argument list // for the name just decided on; the spellings differ only in that. type commandArgShape int @@ -511,11 +539,13 @@ func (p *Parser) parseStatements(stop map[token.Type]bool) []ast.Node { // `[begin; foo bar; end]`, `[(foo bar)]`, `[proc do foo bar end]` all parse in // MRI while `[foo bar]` does not. defer p.permitCommand(true)() - // A nested statement body (a parenthesised group, an interpolation, a keyword - // block) is a fresh context: an inner modifier-`rescue` there is not the one - // that binds to an enclosing command call, so re-enable it. - savedRescue := p.noRescueMod - p.noRescueMod = false + // A statement body is MRI's `compstmt`, and `stmt: stmt modifier_rescue + // after_rescue stmt` (3184) is the production that gives a modifier-`rescue` + // its main home. Every nested body — a parenthesised group, an interpolation, + // a keyword block, a method or block body — is one, which is why + // `[(x rescue nil)]` and `f(begin; x rescue nil; end)` parse where + // `[x rescue nil]` and `f(x rescue nil)` do not. + defer p.permitRescueMod(true)() // A nested statement body also resets MRI's CMDARG bit-stack, so a `do` inside // it opens a block for the call it follows even when the body itself sits in a // paren-less command-argument list: `foo :a, begin; y do end; end`, @@ -534,7 +564,6 @@ func (p *Parser) parseStatements(stop map[token.Type]bool) []ast.Node { p.noDoBlock = false defer func() { p.noMasgn = savedMasgn - p.noRescueMod = savedRescue p.noDoBlock = savedDoBlock }() var body []ast.Node @@ -1132,6 +1161,11 @@ func (p *Parser) parseCond() ast.Node { // on parsing inside an `arg` position too: `[while foo bar; end]` is legal // where `[foo bar]` is not. defer p.permitCommand(true)() + // `expr_value` is an `expr`, and only `stmt` has a `modifier_rescue` + // alternative (3184), so `if x rescue nil; end` and `while x rescue nil; end` + // are SyntaxErrors in MRI while `if (x rescue nil); end` is not — the + // parenthesised group is a `compstmt`. + defer p.permitRescueMod(false)() return p.parseOneLineMatch(p.parseKeywordLogical()) } @@ -1436,6 +1470,33 @@ func (p *Parser) parseBegin() ast.Node { return node } +// clauseThen consumes MRI's `then` nonterminal — `term | keyword_then | term +// keyword_then`, where a `term` is a newline or a `;` — which both +// `opt_rescue: k_rescue exc_list exc_var then compstmt` (parse.y v3_4_0 5926) +// and `case_body: k_when case_args then compstmt` (5397) REQUIRE: `begin; x; +// rescue Foo end` and `case x; when 1 end` are SyntaxErrors in MRI. +// +// Requiring it is also what makes the `arg_value`-ness of those two lists +// observable. `exc_list: arg_value` and `case_args: arg_value` have no +// `command` alternative, so commandArgsFollow already declines to give `foo` the +// argument `bar` in `rescue foo bar` / `when foo bar` — but with the terminator +// optional the leftover `bar` was simply read as the clause's FIRST STATEMENT +// and the whole clause parsed. A refused production only shows up as an error +// if something downstream insists on what must come next. +func (p *Parser) clauseThen(clause string) { + if p.accept(token.THEN) { + return + } + if !p.is(token.NEWLINE) { + p.fail("expected a newline, %q or %q after the %s clause head, got %q (%s)", + ";", "then", clause, p.cur().Lit, p.cur().Type) + } + p.advance() + // `term keyword_then` — `rescue Foo; then y` and `when 1; then 2` are legal. + p.skipNewlines() + p.accept(token.THEN) +} + // parseRescueTail parses the `(rescue …)* [else …] [ensure …]` clauses that // follow an already-parsed body, returning a Begin. The caller consumes `end`. // It is shared by `begin … end` and method-level rescue (`def … rescue … end`). @@ -1463,7 +1524,7 @@ func (p *Parser) parseRescueTail(body []ast.Node) *ast.Begin { clause.VarTarget = p.rescueVarTarget() } } - p.accept(token.THEN) + p.clauseThen("rescue") clause.Body = p.parseStatements(beginBodyEnd) node.Rescues = append(node.Rescues, clause) } @@ -1592,9 +1653,13 @@ func (p *Parser) parseCase() ast.Node { var subject ast.Node if !p.is(token.NEWLINE) { // `k_case expr_value opt_terms case_body k_end` — the subject admits a - // command (`case foo bar` ⏎ `when 1` ⏎ `end` parses in MRI). + // command (`case foo bar` ⏎ `when 1` ⏎ `end` parses in MRI) but, being an + // `expr_value`, not a modifier-`rescue`: `case x rescue nil; when 1; end` + // is a SyntaxError where `case (x rescue nil); when 1; end` is not. restore := p.permitCommand(true) + restoreRescue := p.permitRescueMod(false) subject = p.parseExprOrAssign() + restoreRescue() restore() } p.skipNewlines() @@ -1610,7 +1675,7 @@ func (p *Parser) parseCase() ast.Node { for p.accept(token.COMMA) { clause.Conds = append(clause.Conds, p.parseSplatOrExpr()) } - p.accept(token.THEN) + p.clauseThen("when") clause.Body = p.parseStatements(caseBodyEnd) node.Whens = append(node.Whens, clause) } @@ -2355,35 +2420,35 @@ func (p *Parser) parseExprOrAssign() ast.Node { if p.is(token.IDENT) && p.peekTok().Type == token.OPASSIGN { name := p.advance().Lit op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() p.declareLocal(name) return &ast.OpAssign{Name: name, Op: op, Value: rhs} } if p.is(token.IVAR) && p.peekTok().Type == token.OPASSIGN { name := p.advance().Lit op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.IvarAssign{Name: name, Value: &ast.BinaryExpr{Op: op, Left: &ast.IvarRef{Name: name}, Right: rhs}} } // Compound assignment to a global: $name OP= expr → $name = $name OP expr. if p.is(token.GVAR) && p.peekTok().Type == token.OPASSIGN { name := p.advance().Lit op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.GVarAssign{Name: name, Value: &ast.BinaryExpr{Op: op, Left: &ast.GVarRef{Name: name}, Right: rhs}} } // Compound assignment to a class variable: @@name OP= expr. if p.is(token.CVAR) && p.peekTok().Type == token.OPASSIGN { name := p.advance().Lit op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.CVarAssign{Name: name, Value: &ast.BinaryExpr{Op: op, Left: &ast.CVarRef{Name: name}, Right: rhs}} } // Compound assignment to a constant: NAME OP= expr (`C += 1`, `C ||= default`). if p.is(token.CONST) && p.peekTok().Type == token.OPASSIGN { name := p.advance().Lit op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return constOpAssign(op, &ast.ConstRef{Name: name}, rhs, func(v ast.Node) ast.Node { return &ast.ConstAssign{Name: name, Value: v} }) @@ -2394,7 +2459,7 @@ func (p *Parser) parseExprOrAssign() ast.Node { // Compound index assignment: recv[i] OP= v → recv[i] = recv[i] OP v. if call.Name == "[]" { op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() read := &ast.Call{Recv: call.Recv, Name: "[]", Args: call.Args} newVal := &ast.BinaryExpr{Op: op, Left: read, Right: rhs} args := append(append([]ast.Node{}, call.Args...), newVal) @@ -2403,7 +2468,7 @@ func (p *Parser) parseExprOrAssign() ast.Node { // Compound attribute assignment: recv.a OP= v → recv.a = recv.a OP v. if len(call.Args) == 0 && call.Block == nil { op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() read := &ast.Call{Recv: call.Recv, Name: call.Name} newVal := &ast.BinaryExpr{Op: op, Left: read, Right: rhs} return &ast.Call{Recv: call.Recv, Name: call.Name + "=", Args: []ast.Node{newVal}} @@ -2412,7 +2477,7 @@ func (p *Parser) parseExprOrAssign() ast.Node { // Compound scope-resolved constant assignment: `A::B OP= v`. if sc, ok := left.(*ast.ScopedConst); ok { op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return constOpAssign(op, sc, rhs, func(v ast.Node) ast.Node { return &ast.ScopedConstAssign{Target: sc, Value: v} }) @@ -2466,6 +2531,12 @@ func constOpAssign(op string, ref, rhs ast.Node, assign func(ast.Node) ast.Node) // leading `*splat` — makes the RHS an implicit array: `x = 1, 2, 3` ≡ `x = [1, // 2, 3]` and `a = *list, y` ≡ `a = [*list, y]`, matching MRI. func (p *Parser) parseAssignRhs() ast.Node { + // `arg_rhs: arg modifier_rescue after_rescue arg` (parse.y v3_4_0 4162) — an + // assignment's right-hand side is the ONE `arg` position with a + // `modifier_rescue` alternative, which is why `[a = x rescue nil]` and + // `f(a = 1 rescue nil)` are legal where `[x rescue nil]` and `f(x rescue nil)` + // are not. + defer p.permitRescueMod(true)() first := p.parseRhsElem() // While masgn is suppressed (a parameter default or a keyword-argument value), // a comma ends this value rather than gathering an implicit array RHS, so @@ -2492,6 +2563,17 @@ func (p *Parser) parseAssignRhs() ast.Node { return &ast.ArrayLit{Elems: elems} } +// assignRhsValue parses a single-value assignment right-hand side — the one an +// `OP=` takes, and the one an attribute/index `=` takes when the target was +// already parsed. Like parseAssignRhs it is MRI's `arg_rhs` (parse.y v3_4_0 +// 4157-4166), so a modifier-`rescue` is available even inside an `arg` position: +// `f(a ||= 1 rescue nil)` and `f(a.b = 1 rescue nil)` parse where +// `f(a rescue nil)` does not. It takes no comma list, which `arg_rhs` has none of. +func (p *Parser) assignRhsValue() ast.Node { + defer p.permitRescueMod(true)() + return p.parseExprOrAssign() +} + // parseRhsElem parses one element of an assignment right-hand side, which may be // a `*splat`. func (p *Parser) parseRhsElem() ast.Node { @@ -2522,11 +2604,15 @@ func (p *Parser) restElem() ast.Node { // A clause `rescue` (in a begin/def body) always starts a new line, so the // preceding NEWLINE keeps it from being read as a modifier. Left-associative. func (p *Parser) withRescueModifier(node ast.Node) ast.Node { - // While parsing a paren-less command call's arguments the modifier binds to the - // whole call, not to this (last) argument, so leave the `rescue` for the - // enclosing command node to consume: `raise "x" rescue 42` == `(raise "x") - // rescue 42`. - for p.is(token.RESCUE) && !p.noRescueMod { + // It is the ONE place MRI's "which positions have a `modifier_rescue` + // alternative" question is answered, so the rule cannot drift between call + // sites (the lesson commandArgsFollow already encodes for `arg`/`command`). + // A `rescue` left unconsumed here is not silently dropped: the caller's + // terminator check reports it. That is how `f(x rescue nil)` and + // `[x rescue nil]` become parse errors, and how the modifier binds to the + // whole paren-less call rather than to its last argument — `raise "x" rescue + // 42` == `(raise "x") rescue 42`. + for p.is(token.RESCUE) && p.rescueModOK { p.advance() fallback := p.parseTernary() node = &ast.Begin{Body: []ast.Node{node}, Rescues: []ast.RescueClause{{Body: []ast.Node{fallback}}}} @@ -2723,13 +2809,13 @@ func (p *Parser) maybeInlineAssign(node ast.Node) ast.Node { if n.Name == "[]" { p.advance() n.Name = "[]=" - n.Args = append(n.Args, p.parseExprOrAssign()) + n.Args = append(n.Args, p.assignRhsValue()) return n } if len(n.Args) == 0 { p.advance() n.Name += "=" - n.Args = []ast.Node{p.parseExprOrAssign()} + n.Args = []ast.Node{p.assignRhsValue()} return n } } @@ -2757,13 +2843,13 @@ func (p *Parser) inlineOpAssign(node ast.Node) ast.Node { switch n := node.(type) { case *ast.VarRef: op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() p.declareLocal(n.Name) return &ast.OpAssign{Name: n.Name, Op: op, Value: rhs} case *ast.Call: if n.Recv == nil && len(n.Args) == 0 && n.Block == nil { // a bare local op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() p.declareLocal(n.Name) return &ast.OpAssign{Name: n.Name, Op: op, Value: rhs} } @@ -2771,7 +2857,7 @@ func (p *Parser) inlineOpAssign(node ast.Node) ast.Node { // recv[i] OP= v → recv[i] = recv[i] OP v; recv.a OP= v → recv.a = recv.a OP v. if n.Name == "[]" { op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() read := &ast.Call{Recv: n.Recv, Name: "[]", Args: n.Args} newVal := &ast.BinaryExpr{Op: op, Left: read, Right: rhs} args := append(append([]ast.Node{}, n.Args...), newVal) @@ -2779,7 +2865,7 @@ func (p *Parser) inlineOpAssign(node ast.Node) ast.Node { } if len(n.Args) == 0 { op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() read := &ast.Call{Recv: n.Recv, Name: n.Name} newVal := &ast.BinaryExpr{Op: op, Left: read, Right: rhs} return &ast.Call{Recv: n.Recv, Name: n.Name + "=", Args: []ast.Node{newVal}} @@ -2787,15 +2873,15 @@ func (p *Parser) inlineOpAssign(node ast.Node) ast.Node { } case *ast.IvarRef: op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.IvarAssign{Name: n.Name, Value: &ast.BinaryExpr{Op: op, Left: &ast.IvarRef{Name: n.Name}, Right: rhs}} case *ast.CVarRef: op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.CVarAssign{Name: n.Name, Value: &ast.BinaryExpr{Op: op, Left: &ast.CVarRef{Name: n.Name}, Right: rhs}} case *ast.GVarRef: op := p.advance().Lit - rhs := p.parseExprOrAssign() + rhs := p.assignRhsValue() return &ast.GVarAssign{Name: n.Name, Value: &ast.BinaryExpr{Op: op, Left: &ast.GVarRef{Name: n.Name}, Right: rhs}} } return nil @@ -2845,6 +2931,18 @@ func (p *Parser) parseUnary() ast.Node { return p.parseUnary() // unary plus is a no-op case token.BANG: p.advance() + // `!` is the ONE unary operator whose operand may be a paren-less command + // call, and the alternative that allows it is `expr: '!' command_call` + // (parse.y v3_4_0 3350) — a `command_call`, NOT an `expr`. The only other + // arm is `arg: '!' arg` (4009), whose operand is an `arg`. So the privilege + // does not nest: `!foo bar` parses in MRI and `!!foo bar` does not, because + // the inner `!foo bar` is not a `command_call` and the outer `!` therefore + // has to be the `arg` arm. `arg` has no `keyword_not` alternative either, + // so `!not …` falls the same way (which is where MRI's two parsers part + // company: prism accepts `!not x`, parse.y refuses it). + if p.is(token.BANG) || p.is(token.NOT) { + defer p.permitCommand(false)() + } return &ast.UnaryExpr{Op: "!", Operand: p.parseUnary()} case token.TILDE: p.advance() @@ -3854,10 +3952,10 @@ func (p *Parser) parseCommandArgs() []ast.Node { // assignment value stops at the comma. Mirrors parameter-default handling. savedMasgn := p.noMasgn p.noMasgn = true - // A trailing modifier-`rescue` binds to the whole command call, not to its last - // argument, so suppress it here and let the enclosing expression consume it. - savedRescue := p.noRescueMod - p.noRescueMod = true + // An argument is an `arg_value`, which has no `modifier_rescue` alternative, + // so a trailing modifier-`rescue` binds to the whole command call and is left + // for the enclosing `stmt` to consume: `raise "x" rescue 42`. + restoreRescue := p.permitRescueMod(false) // `command_args: call_args` and `call_args: command` (parse.y v3_4_0 4250, // 4223) give the HEAD argument of a paren-less call the right to be a // paren-less call itself — `foo bar baz` is `foo(bar(baz))` — while the later @@ -3874,7 +3972,7 @@ func (p *Parser) parseCommandArgs() []ast.Node { p.parseOneCallArg(&args, &kw) } restoreRest() - p.noRescueMod = savedRescue + restoreRescue() p.noMasgn = savedMasgn p.noDoBlock = saved if kw != nil { @@ -3903,11 +4001,12 @@ func (p *Parser) parseArrayElems() []ast.Node { func (p *Parser) parseArgList(until token.Type, headAdmitsCommand bool) []ast.Node { p.bracketDepth++ - // A parenthesised `(…)` argument list or `[…]` element list is a fresh, - // delimited context: an inner modifier-`rescue` (`foo(bar rescue baz)`) belongs - // to that argument, not to any enclosing command call, so re-enable it. - savedRescue := p.noRescueMod - p.noRescueMod = false + // Every element of the list is an `arg_value` (or, for the head, a `command`), + // and NEITHER has a `modifier_rescue` alternative: `f(bar rescue baz)`, + // `[x rescue nil]` and `a[x rescue nil]` are SyntaxErrors in MRI, which wants + // `f((bar rescue baz))`. The bracket is not a fresh statement context — only + // a `(…)` GROUP is, and that is a `compstmt` parsed elsewhere. + defer p.permitRescueMod(false)() // The comma separates arguments (or array elements), so multiple-assignment // detection must be off: `f(a = 1, b = 2, 3)` is three arguments (the first two // assignments), not `f(a = [1, b = [2, 3]])`, and `[x = 1, 2]` is a two-element @@ -3924,7 +4023,6 @@ func (p *Parser) parseArgList(until token.Type, headAdmitsCommand bool) []ast.No defer p.enterBracket()() defer func() { p.bracketDepth-- - p.noRescueMod = savedRescue p.noMasgn = savedMasgn }() var args []ast.Node From dd5462cb9c28447aee996248da500cf8d5e5493b Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 26 Sep 2026 22:53:37 +0200 Subject: [PATCH 2/2] parser: three more productions with no `command`/`rescue`/`not` arm, and the clause terminator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `defined?` unparenthesised is `arg: keyword_defined '\n'? begin_defined arg` (parse.y v3_4_0 4021) — an ARG production, so it must NOT go through commandArgsFollow, whose first act is to refuse everything in an `arg` position. `[defined? x]`, `{k: defined? x}`, `1 + defined? x` and `def m(a = defined? x); end` are legal MRI and v0.6.0 refused them. - `arg` has no `keyword_not` arm (only `expr: keyword_not expr`, 3345), so `[not x]`, `{k: not x}`, `f(1, not x)`, `[*not x]`, `f(¬ x)`, `def m(a = not x); end` and `case y; when not x; end` are SyntaxErrors in both of MRI's parsers. - `then`/`do` — `term | KEYWORD | term KEYWORD` — is REQUIRED by every clause head (`opt_rescue` 5926, `case_body` 5397, `k_if`, `k_unless`, `expr_value_do`, `p_case_body`), so `if x end`, `while x end`, `for i in a end`, `case x; when 1 end` and `begin; x; rescue Foo end` are SyntaxErrors. Requiring it is also what makes a refused `arg_value` OBSERVABLE: the stray `bar` of `rescue foo bar` was being read as the clause's first statement. - modifier-`rescue` refused in the remaining `arg_value` positions: hash literal entries, a `when`/`rescue` list, a parameter default, a keyword-argument value. Tables: 319 enumerated shapes, each judged by BOTH `ruby -c` and `ruby --parser=parse.y -c` on ruby 4.0.5. 115 accepted -> REFUSED, 7 REFUSED -> accepted (the `defined?` family), 0 regressions, 13 shapes where MRI's two parsers disagree and which are therefore asserted in neither direction. Co-Authored-By: Claude Opus 5 --- arg_position_command_test.go | 328 +++++++++++++++++++++++++++++++++++ parser.go | 119 +++++++++---- 2 files changed, 410 insertions(+), 37 deletions(-) diff --git a/arg_position_command_test.go b/arg_position_command_test.go index c78b06f..d93a442 100644 --- a/arg_position_command_test.go +++ b/arg_position_command_test.go @@ -118,6 +118,165 @@ func TestParenLessCommandCallRefusedInAnArgPosition(t *testing.T) { `{k: foo bar}`, `{k: foo.bar baz}`, `~foo bar`, + // --- v0.6.1 additions. Three further productions that have no + // `command`/`modifier_rescue`/`keyword_not` alternative, plus the clause + // terminator that makes the refusal observable. Every shape judged on ruby + // 4.0.5 by BOTH `ruby -c` and `ruby --parser=parse.y -c`, which agree on all + // of them. + // + // `arg: '!' arg` (4009) vs `expr: '!' command_call` (3350): the operand of + // `!` may be a paren-less command ONLY at the `expr` level, and a + // `command_call` is not itself a unary expression — hence `!foo bar` legal, + // `!!foo bar` not. + // + // `arg` has no `modifier_rescue` arm — only `stmt` (3184) and the assignment + // right-hand sides `arg_rhs` (4162) / `command_rhs` / `endless_arg` do — and + // `expr` has none either, which covers the `expr_value` headers. + // + // `arg` has no `keyword_not` arm: `not` is `expr: keyword_not '\n'? expr` + // (3345) only. + // + // And `then`/`do` — `term | KEYWORD | term KEYWORD` — is REQUIRED by every + // clause head (`opt_rescue` 5926, `case_body` 5397, `k_if`/`k_unless`, + // `expr_value_do`, `p_case_body`). Without it the stray token of an + // `arg_value` already refused upstream was read as the clause's first + // statement and the clause parsed anyway. + "begin\nx\nrescue foo bar\ny\nend", + "case x\nwhen foo bar\n1\nend", + `!! foo bar`, + `!!!!foo bar`, + `!!!foo bar`, + `!!Foo 1`, + `!![foo bar]`, + `!!a.b foo bar`, + `!!defined? foo bar`, + `!!foo bar, baz`, + `!!foo bar`, + `!!foo.bar baz`, + `!!not foo bar`, + `!!super 1`, + `!!x = foo bar`, + `!-foo bar`, + `!a = foo bar`, + `!~foo bar`, + `+!foo bar`, + `-!foo bar`, + `->() { x; rescue foo bar }`, + `-not x`, + `1 + [x rescue nil]`, + `1 + not x`, + `Foo(x rescue nil)`, + `[!!foo bar]`, + `[*not x]`, + `[*x rescue nil]`, + `[1, 2 rescue nil]`, + `[1, not x]`, + `[1, x rescue nil]`, + `[1].each do; x; rescue foo bar; end`, + `[1].each { x; rescue foo bar; }`, + `[[x rescue nil]]`, + `[not x]`, + `[x rescue nil]`, + `[{k: x rescue nil}]`, + `a = !!foo bar`, + `a ? not x : 1`, + `a ? x rescue nil : 1`, + `a&.b(x rescue nil)`, + `a.b(1, x rescue nil)`, + `a.b(x rescue nil)`, + `a::b(x rescue nil)`, + `a[1, not x]`, + `a[1, x rescue nil]`, + `a[x rescue nil]`, + `begin; x; rescue !foo bar; y; end`, + `begin; x; rescue *foo bar; y; end`, + `begin; x; rescue Foo => e end`, + `begin; x; rescue Foo bar; y; end`, + `begin; x; rescue Foo end`, + `begin; x; rescue Foo if c; y; end`, + `begin; x; rescue Foo rescue Bar; y; end`, + `begin; x; rescue Foo rescue nil; y; end`, + `begin; x; rescue Foo, Bar end`, + `begin; x; rescue Foo, bar baz => e; y; end`, + `begin; x; rescue Foo, bar baz; y; end`, + `begin; x; rescue a = foo bar; y; end`, + `begin; x; rescue end`, + `begin; x; rescue foo bar => e; y; end`, + `begin; x; rescue foo bar baz qux; y; end`, + `begin; x; rescue foo bar then y; end`, + `begin; x; rescue foo bar; y; end`, + `begin; y; rescue not x; z; end`, + `case x rescue nil; when 1; end`, + `case x; in 1 end`, + `case x; when 1 end`, + `case x; when 1, 2 end`, + `case x; when Foo, bar baz; 1; end`, + `case x; when foo bar; 1; end`, + `case x; when foo bar; end`, + `case x; when y rescue nil then 1; end`, + `case x; when y rescue nil; end`, + `case y; when not x; end`, + `class C; x; rescue foo bar; end`, + `def m(*a rescue nil); end`, + `def m(a = not x); end`, + `def m(a = x rescue nil); end`, + `def m(a: not x); end`, + `def m(a: x rescue nil); end`, + `def m; super(x rescue nil); end`, + `def m; x; rescue *foo bar; end`, + `def m; x; rescue Foo, bar baz; end`, + `def m; x; rescue foo bar; end`, + `def m; yield(x rescue nil); end`, + `elsif_check = 1; if a; elsif b end`, + `f(!x rescue nil)`, + `f(¬ x)`, + `f(&x rescue nil)`, + `f(**not x)`, + `f(**x rescue nil)`, + `f(*a, x rescue nil)`, + `f(*x rescue nil)`, + `f(-x rescue nil)`, + `f(1, 2 rescue nil)`, + `f(1, not x)`, + `f(1, x rescue nil)`, + `f(a: 1, b: x rescue nil)`, + `f(defined?(x) rescue nil)`, + `f(k: x rescue nil)`, + `f(x + 1 rescue nil)`, + `f(x ? 1 : 2 rescue nil)`, + `f(x rescue nil)`, + `f(x rescue nil, 1)`, + `f(x, y rescue nil)`, + `f(y, x rescue nil, 1)`, + `foo(bar rescue baz)`, + `for i in a end`, + `for i in x rescue nil; end`, + `h = {k: v rescue nil}`, + `if !!foo bar then end`, + `if !!foo bar; end`, + `if x 1 end`, + `if x end`, + `if x rescue nil; end`, + `module M; x; rescue foo bar; end`, + `not !!foo bar`, + `p !!foo bar`, + `p(x rescue nil)`, + `super(x rescue nil)`, + `unless x end`, + `unless x rescue nil; end`, + `until x end`, + `until x rescue nil; end`, + `while !!foo bar; end`, + `while [x rescue nil]; end`, + `while x end`, + `while x rescue nil; end`, + `x = [x rescue nil]`, + `{1 => not x}`, + `{1 => x rescue nil}`, + `{k: [x rescue nil]}`, + `{k: not x}`, + `{k: x rescue nil}`, + `~!foo bar`, } { _, err := Parse(src) if err == nil { @@ -234,6 +393,175 @@ func TestParenLessCommandCallStillAcceptedWhereMRIAdmitsIt(t *testing.T) { `{k: a[foo bar]}`, `{k: defined?(foo bar)}`, `{k: foo(bar baz)}`, + // --- v0.6.1 additions: the other direction. The contrasts that name each + // rule sit beside their refused twin above — `f((x rescue nil))` vs + // `f(x rescue nil)`, `f(a = 1 rescue nil)` vs `f(x rescue nil)` (the + // `arg_rhs` carve-out), `!!(foo bar)` vs `!!foo bar`, `when (y rescue nil)` + // vs `when y rescue nil`. `defined? x` in an `arg` position is here because + // `arg: keyword_defined '\n'? begin_defined arg` (4021) IS an `arg` + // production: `[defined? x]` is legal MRI and v0.6.0 refused it. + "begin\nx\nrescue Foo\ny\nend", + "case x\nwhen Foo\n1\nend", + `!!!defined? x`, + `!!!x`, + `!!(begin; foo bar; end)`, + `!!(foo bar)`, + `!!a[foo bar]`, + `!!begin; foo bar; end`, + `!!defined? x`, + `!!defined?(foo bar)`, + `!!foo(bar baz)`, + `!!foo(bar)`, + `!!if foo bar then 1 end`, + `!!lambda { foo bar }`, + `!!while foo bar; end`, + `!!x.y`, + `!!x[0]`, + `!!x`, + `!(foo bar)`, + `!Foo 1`, + `!a.b foo bar`, + `!foo bar rescue 1`, + `!foo bar, baz`, + `!foo.bar baz`, + `(not x)`, + `(x rescue nil)`, + `-(x rescue nil)`, + `-> { x rescue nil }`, + `-defined? x`, + `-x rescue nil`, + `1 + (x rescue nil)`, + `1 + defined? x`, + `1 + x rescue nil`, + `@x = y rescue nil`, + `X = y rescue nil`, + `[!x]`, + `[(x rescue nil)]`, + `[1].map { |x| x rescue nil }`, + `[a += x rescue nil]`, + `[a = 1 rescue nil, 2]`, + `[a = b = 1 rescue nil]`, + `[a = x rescue nil]`, + `[defined? foo]`, + `[defined? x]`, + `[defined?(x)]`, + `a += x rescue nil`, + `a = [1, 2] rescue nil`, + `a = b = x rescue nil`, + `a = x rescue nil`, + `a ? 1 : x rescue nil`, + `a, b = x rescue nil`, + `a.b = y rescue nil`, + `a[(x rescue nil)]`, + `a[0] = y rescue nil`, + `a[a = 1 rescue nil]`, + `a[defined? x]`, + `begin; x rescue nil; end`, + `begin; x; ensure; foo bar; end`, + `begin; x; rescue (Foo rescue nil); y; end`, + `begin; x; rescue (foo bar); y; end`, + `begin; x; rescue *A, B; y; end`, + `begin; x; rescue *Foo; y; end`, + `begin; x; rescue ::Foo; y; end`, + `begin; x; rescue => e; y; end`, + `begin; x; rescue Foo => e then y; end`, + `begin; x; rescue Foo => e; y; end`, + `begin; x; rescue Foo then y; end`, + `begin; x; rescue Foo then; y; end`, + `begin; x; rescue Foo, ::Bar; y; end`, + `begin; x; rescue Foo, Bar => e; y; end`, + `begin; x; rescue Foo, Bar then y; end`, + `begin; x; rescue Foo, Bar; y; end`, + `begin; x; rescue Foo, foo(bar baz); y; end`, + `begin; x; rescue Foo::Bar; y; end`, + `begin; x; rescue Foo; else; y; end`, + `begin; x; rescue Foo; end`, + `begin; x; rescue Foo; rescue Bar; end`, + `begin; x; rescue Foo; y; end rescue nil`, + `begin; x; rescue Foo; y; end`, + `begin; x; rescue [Foo, Bar].first; y; end`, + `begin; x; rescue a.b; y; end`, + `begin; x; rescue a::B; y; end`, + `begin; x; rescue a[foo bar]; y; end`, + `begin; x; rescue foo(bar); y; end`, + `begin; x; rescue; foo bar; end`, + `begin; x; rescue; y; end`, + `begin; y; rescue defined? x; z; end`, + `case (x rescue nil); when 1; end`, + `case x; in 1 then 2; end`, + `case x; in 1; 2; end`, + `case x; when (y rescue nil); end`, + `case x; when *a; 3; end`, + `case x; when 1 then 2; end`, + `case x; when 1 then; 2; end`, + `case x; when 1, 2; 3; end`, + `case x; when 1; 2; end`, + `case x; when 1; end`, + `case x; when Foo; end`, + `case x; when a.b; end`, + `case x; when a[0]; end`, + `case y; when defined? x; end`, + `def m = x rescue nil`, + `def m(a = (x rescue nil)); end`, + `def m(a = defined? x); end`, + `def m(a: (x rescue nil)); end`, + `def m; return x rescue nil; end`, + `def m; super x rescue nil; end`, + `def m; x rescue nil; end`, + `def m; x; ensure; y; end`, + `def m; yield x rescue nil; end`, + `f { x rescue nil }`, + `f($x = 1 rescue nil)`, + `f((x rescue nil))`, + `f(1) { x rescue nil }`, + `f(1, (x rescue nil))`, + `f(1, a = 2 rescue nil)`, + `f(1, defined? x)`, + `f(::A = 1 rescue nil)`, + `f(@x = 1 rescue nil)`, + `f(A = 1 rescue nil)`, + `f(a &&= 1 rescue nil)`, + `f(a = 1 rescue nil)`, + `f(a = b = 1 rescue nil)`, + `f(a ||= 1 rescue nil)`, + `f(a.b = 1 rescue nil)`, + `f(a::B = 1 rescue nil)`, + `f(a[0] = 1 rescue nil)`, + `f(x) rescue nil`, + `foo a = 1 rescue nil`, + `foo x rescue nil`, + `for i in (x rescue nil); end`, + `for i in a do end`, + `if (x rescue nil); end`, + `if a; elsif b; end`, + `if foo bar then 1 end`, + `if not x; end`, + `if x then 1 end`, + `if x; 1; end`, + `if x; end`, + `lambda { x rescue nil }`, + `loop do x rescue nil end`, + `not !foo bar`, + `not not foo bar`, + `not not x`, + `not x`, + `p x rescue nil`, + `return x rescue nil`, + `unless (x rescue nil); end`, + `unless not done; end`, + `until (x rescue nil); end`, + `while (x rescue nil); end`, + `while not x; end`, + `while x do end`, + `while x; end`, + `x = (not y)`, + `x = begin; y; rescue; 1; end rescue nil`, + `x = y rescue nil`, + `x rescue nil`, + `x ||= y rescue nil`, + `{k: (x rescue nil)}`, + `{k: a = x rescue nil}`, + `{k: defined? x}`, } { if _, err := Parse(src); err != nil { t.Errorf("Parse(%q) = %v, want no error (MRI 4.0.5 accepts it)", src, err) diff --git a/parser.go b/parser.go index 38ed2db..8671718 100644 --- a/parser.go +++ b/parser.go @@ -1126,6 +1126,10 @@ func (p *Parser) parseParamDefault() ast.Node { // `f_opt: f_arg_asgn f_eq arg_value` — an `arg`, so `def m(a = foo bar); end` // is a SyntaxError in MRI. defer p.permitCommand(false)() + // An `arg_value`, NOT an `arg_rhs`: a parameter default has no + // `modifier_rescue` alternative, so `def m(a = x rescue nil); end` is a + // SyntaxError where `def m(a = (x rescue nil)); end` is legal. + defer p.permitRescueMod(false)() saved := p.noMasgn p.noMasgn = true def := p.parseExprOrAssign() @@ -1141,6 +1145,10 @@ func (p *Parser) parseKwArgValue() ast.Node { // a parameter list are both `arg`, so `{k: foo bar}`, `f(k: foo bar)` and // `def m(a: foo bar); end` are SyntaxErrors in MRI. defer p.permitCommand(false)() + // An `arg_value` too, so no modifier-`rescue`: `f(k: x rescue nil)` and + // `def m(a: x rescue nil); end` are SyntaxErrors where `f(k: (x rescue nil))` + // and `f(k: a = x rescue nil)` are legal. + defer p.permitRescueMod(false)() saved := p.noMasgn p.noMasgn = true v := p.parseExprOrAssign() @@ -1172,13 +1180,13 @@ func (p *Parser) parseCond() ast.Node { func (p *Parser) parseIf() ast.Node { p.expect(token.IF) cond := p.parseCond() - p.accept(token.THEN) + p.clauseHeadEnd("if", token.THEN) then := p.parseStatements(ifBodyEnd) node := &ast.If{Cond: cond, Then: then} for p.is(token.ELSIF) { p.advance() c := p.parseCond() - p.accept(token.THEN) + p.clauseHeadEnd("elsif", token.THEN) b := p.parseStatements(ifBodyEnd) node.Elsifs = append(node.Elsifs, ast.Elsif{Cond: c, Body: b}) } @@ -1193,7 +1201,7 @@ func (p *Parser) parseIf() ast.Node { func (p *Parser) parseUnless() ast.Node { p.expect(token.UNLESS) cond := p.parseCond() - p.accept(token.THEN) + p.clauseHeadEnd("unless", token.THEN) then := p.parseStatements(ifBodyEnd) node := &ast.If{Cond: not(cond), Then: then} if p.accept(token.ELSE) { @@ -1206,7 +1214,7 @@ func (p *Parser) parseUnless() ast.Node { func (p *Parser) parseWhile() ast.Node { p.expect(token.WHILE) cond := p.parseLoopCond() - p.accept(token.DO) + p.clauseHeadEnd("while", token.DO) body := p.parseStatements(bodyEnd) p.expect(token.END) return &ast.While{Cond: cond, Body: body} @@ -1226,7 +1234,7 @@ func (p *Parser) parseLoopCond() ast.Node { func (p *Parser) parseUntil() ast.Node { p.expect(token.UNTIL) cond := p.parseLoopCond() - p.accept(token.DO) + p.clauseHeadEnd("until", token.DO) body := p.parseStatements(bodyEnd) p.expect(token.END) return &ast.While{Cond: not(cond), Body: body} @@ -1242,7 +1250,7 @@ func (p *Parser) parseFor() ast.Node { vars, target := p.parseForVar() p.expect(token.IN) iter := p.parseLoopCond() - p.accept(token.DO) + p.clauseHeadEnd("for", token.DO) body := p.parseStatements(bodyEnd) p.expect(token.END) return &ast.For{Vars: vars, Target: target, Iter: iter, Body: body} @@ -1470,31 +1478,43 @@ func (p *Parser) parseBegin() ast.Node { return node } -// clauseThen consumes MRI's `then` nonterminal — `term | keyword_then | term -// keyword_then`, where a `term` is a newline or a `;` — which both -// `opt_rescue: k_rescue exc_list exc_var then compstmt` (parse.y v3_4_0 5926) -// and `case_body: k_when case_args then compstmt` (5397) REQUIRE: `begin; x; -// rescue Foo end` and `case x; when 1 end` are SyntaxErrors in MRI. +// clauseHeadEnd consumes MRI's `then` / `do` nonterminal — each spelled +// `term | KEYWORD | term KEYWORD`, where a `term` is a newline or a `;` — which +// every clause head in the grammar REQUIRES, never optionally: +// +// opt_rescue : k_rescue exc_list exc_var then compstmt (parse.y v3_4_0 5926) +// case_body : k_when case_args then compstmt cases (5397) +// primary : k_if expr_value then compstmt if_tail k_end (4300) +// if_tail : ... | k_elsif expr_value then compstmt ... +// primary : k_unless expr_value then compstmt ... +// primary : k_while expr_value_do compstmt k_end (with `do: term | keyword_do_cond`) +// primary : k_until expr_value_do compstmt k_end +// primary : k_for for_var keyword_in expr_value_do compstmt k_end +// p_case_body: keyword_in p_top_expr then compstmt p_cases // -// Requiring it is also what makes the `arg_value`-ness of those two lists -// observable. `exc_list: arg_value` and `case_args: arg_value` have no -// `command` alternative, so commandArgsFollow already declines to give `foo` the -// argument `bar` in `rescue foo bar` / `when foo bar` — but with the terminator -// optional the leftover `bar` was simply read as the clause's FIRST STATEMENT -// and the whole clause parsed. A refused production only shows up as an error -// if something downstream insists on what must come next. -func (p *Parser) clauseThen(clause string) { - if p.accept(token.THEN) { +// so `begin; x; rescue Foo end`, `case x; when 1 end`, `if x end`, `while x end` +// and `for i in a end` are all SyntaxErrors in MRI. +// +// Requiring it is also what makes the `arg_value`/`expr_value`-ness of the head +// OBSERVABLE. `exc_list: arg_value` and `case_args: arg_value` have no `command` +// alternative, so commandArgsFollow already declines to give `foo` the argument +// `bar` in `rescue foo bar` / `when foo bar`; and a nested `!` operand is an +// `arg` too, so `!!foo bar` leaves a stray `bar` in `if !!foo bar; end`. With +// the terminator optional the leftover token was simply read as the clause's +// FIRST STATEMENT and the whole clause parsed. A refused production only +// surfaces as an error when something downstream insists on what must come next. +func (p *Parser) clauseHeadEnd(clause string, kw token.Type) { + if p.accept(kw) { return } if !p.is(token.NEWLINE) { p.fail("expected a newline, %q or %q after the %s clause head, got %q (%s)", - ";", "then", clause, p.cur().Lit, p.cur().Type) + ";", kw, clause, p.cur().Lit, p.cur().Type) } p.advance() - // `term keyword_then` — `rescue Foo; then y` and `when 1; then 2` are legal. + // `term KEYWORD` — `rescue Foo; then y`, `when 1; then 2`, `while x; do end`. p.skipNewlines() - p.accept(token.THEN) + p.accept(kw) } // parseRescueTail parses the `(rescue …)* [else …] [ensure …]` clauses that @@ -1524,7 +1544,7 @@ func (p *Parser) parseRescueTail(body []ast.Node) *ast.Begin { clause.VarTarget = p.rescueVarTarget() } } - p.clauseThen("rescue") + p.clauseHeadEnd("rescue", token.THEN) clause.Body = p.parseStatements(beginBodyEnd) node.Rescues = append(node.Rescues, clause) } @@ -1675,7 +1695,7 @@ func (p *Parser) parseCase() ast.Node { for p.accept(token.COMMA) { clause.Conds = append(clause.Conds, p.parseSplatOrExpr()) } - p.clauseThen("when") + p.clauseHeadEnd("when", token.THEN) clause.Body = p.parseStatements(caseBodyEnd) node.Whens = append(node.Whens, clause) } @@ -1700,7 +1720,7 @@ func (p *Parser) parseCaseIn(subject ast.Node) ast.Node { clause.Guard = p.parseExprOrAssign() clause.GuardNeg = true } - p.accept(token.THEN) + p.clauseHeadEnd("in", token.THEN) clause.Body = p.parseStatements(inBodyEnd) node.Clauses = append(node.Clauses, clause) } @@ -2297,6 +2317,10 @@ func (p *Parser) parseMlhsGroup() ast.Node { // in MRI. func (p *Parser) parseSplatOrExpr() ast.Node { defer p.permitCommand(false)() + // Being `arg`s, they have no `modifier_rescue` alternative either: + // `case x; when y rescue nil; end` and `begin; x; rescue Foo rescue nil; y; + // end` are SyntaxErrors where `when (y rescue nil)` is legal. + defer p.permitRescueMod(false)() if p.accept(token.STAR) { return &ast.SplatArg{Value: p.parseExprOrAssign()} } @@ -2373,10 +2397,21 @@ func isPostfixStart(tt token.Type) bool { } func (p *Parser) parseExprOrAssign() ast.Node { - // A `not EXPR` prefix in value position (assignment RHS, call argument, array - // element): `x = not true`, `foo(not flag)`. `not` binds looser than the + // A `not EXPR` prefix in value position (assignment RHS, head of a call's + // argument list): `x = not true`, `foo(not flag)`. `not` binds looser than the // binary operators, so it wraps the whole following expression. - if p.accept(token.NOT) { + // + // `arg` has NO `keyword_not` alternative: MRI spells it only + // `expr: keyword_not '\n'? expr` (parse.y v3_4_0 3345), with `endless_arg` + // (4087) and the parenthesised `primary: keyword_not '(' expr rparen` (4464) + // as the other two. So `[not x]`, `{k: not x}`, `f(1, not x)`, + // `def m(a = not x); end`, `case y; when not x; end`, `[*not x]` and + // `f(¬ x)` are SyntaxErrors in BOTH of MRI's parsers, and the same argOnly + // that refuses a `command` refuses this. (Where the position does admit an + // `expr` — an assignment RHS, the head of a paren argument list — MRI's two + // parsers disagree: prism accepts `x = not y` and `f(not x)`, parse.y refuses + // them. We keep prism's answer there and assert neither.) + if !p.argOnly && p.accept(token.NOT) { return not(p.parseExprOrAssign()) } // Multiple assignment to local targets: a, b = … / a, *b = … . Disabled while @@ -3111,6 +3146,10 @@ func (p *Parser) parseHashLiteral() ast.Node { // `command` alternative, so `{k: foo bar}`, `{1 => foo bar}` and `{**foo bar}` // are SyntaxErrors in MRI while `{k: (foo bar)}` and `{k: foo(bar baz)}` parse. defer p.permitCommand(false)() + // Being `arg`s, they have no `modifier_rescue` alternative either: + // `{k: x rescue nil}` and `{1 => x rescue nil}` are SyntaxErrors where + // `{k: (x rescue nil)}` and `{k: a = x rescue nil}` (an `arg_rhs`) are legal. + defer p.permitRescueMod(false)() h := &ast.HashLit{} p.skipNewlines() for !p.is(token.RBRACE) { @@ -3800,15 +3839,21 @@ func (p *Parser) parseIdentExpr() ast.Node { // Otherwise it is a method call on self. p.advance() + // The UNPARENTHESISED `defined?` is the other of MRI's two productions for it: + // `arg: keyword_defined '\n'? begin_defined arg` (parse.y v3_4_0 4021). It is + // an ARG production, not a `command`, so it is available in an `arg` position + // where a paren-less call is not: `[defined? x]`, `{k: defined? x}`, + // `1 + defined? x` and `def m(a = defined? x); end` are all legal MRI. It + // therefore asks the token-shape test DIRECTLY and must not go through + // commandArgsFollow, whose first act is to refuse everything in an `arg` + // position. Its operand is ONE `arg`, not a `command_args` list, so + // `defined? foo bar` is a SyntaxError where `defined? foo` and + // `defined?(foo bar)` are both legal. + if name == "defined?" && p.canStartCommandArg() { + defer p.permitCommand(false)() + return &ast.Call{Name: name, Args: []ast.Node{p.parseExprOrAssign()}} + } if p.commandArgsFollow(cmdArgPlain) { - // The UNPARENTHESISED `defined?` is the other of MRI's two productions for - // it: `arg: keyword_defined '\n'? begin_defined arg`. Its operand is ONE - // `arg`, not a `command_args` list, so `defined? foo bar` is a SyntaxError - // where `defined? foo` and `defined?(foo bar)` are both legal. - if name == "defined?" { - defer p.permitCommand(false)() - return &ast.Call{Name: name, Args: []ast.Node{p.parseExprOrAssign()}} - } call := &ast.Call{Name: name, Args: p.parseCommandArgs()} p.attachCommandBrace(&call.Block) return call