From c253e454f8a398aeebfcf467eaa0789780fe5e15 Mon Sep 17 00:00:00 2001 From: Shugo Maeda Date: Sat, 4 Jul 2026 23:54:55 +0900 Subject: [PATCH] Accept a nested target as the first `for` loop index A `for` loop index may be a multiple-assignment target whose first element is itself a parenthesized (nested) target, e.g. for (a, b), c in 1..10 end CRuby's parse.y accepts this, and a plain multiple assignment with the same target (`(a, b), c = ...`) is accepted, but Prism rejected it with "unexpected write target". When parse_parentheses finishes the first `(a, b)`, it validates the multi-target it produced. In a `for` index it only allowed the target to be followed by `in`, so a following `,` (which continues the index target list) fell through to the "not a statement level" error. Allow a comma there as well. --- snapshots/for.txt | 148 +++++++++++++++++++++++++++++++----- src/prism.c | 5 +- test/prism/fixtures/for.txt | 8 ++ 3 files changed, 142 insertions(+), 19 deletions(-) diff --git a/snapshots/for.txt b/snapshots/for.txt index 5558209826..c02998aebc 100644 --- a/snapshots/for.txt +++ b/snapshots/for.txt @@ -1,10 +1,10 @@ -@ ProgramNode (location: (1,0)-(19,22)) +@ ProgramNode (location: (1,0)-(27,3)) ├── flags: ∅ -├── locals: [:i, :j, :k] +├── locals: [:i, :j, :k, :a, :b, :c, :d] └── statements: - @ StatementsNode (location: (1,0)-(19,22)) + @ StatementsNode (location: (1,0)-(27,3)) ├── flags: ∅ - └── body: (length: 6) + └── body: (length: 8) ├── @ ForNode (location: (1,0)-(3,3)) │ ├── flags: newline │ ├── index: @@ -186,34 +186,148 @@ │ ├── in_keyword_loc: (15,6)-(15,8) = "in" │ ├── do_keyword_loc: (15,15)-(15,17) = "do" │ └── end_keyword_loc: (17,0)-(17,3) = "end" - └── @ ForNode (location: (19,0)-(19,22)) + ├── @ ForNode (location: (19,0)-(19,22)) + │ ├── flags: newline + │ ├── index: + │ │ @ LocalVariableTargetNode (location: (19,4)-(19,5)) + │ │ ├── flags: ∅ + │ │ ├── name: :i + │ │ └── depth: 0 + │ ├── collection: + │ │ @ RangeNode (location: (19,9)-(19,14)) + │ │ ├── flags: static_literal + │ │ ├── left: + │ │ │ @ IntegerNode (location: (19,9)-(19,10)) + │ │ │ ├── flags: static_literal, decimal + │ │ │ └── value: 1 + │ │ ├── right: + │ │ │ @ IntegerNode (location: (19,12)-(19,14)) + │ │ │ ├── flags: static_literal, decimal + │ │ │ └── value: 10 + │ │ └── operator_loc: (19,10)-(19,12) = ".." + │ ├── statements: + │ │ @ StatementsNode (location: (19,16)-(19,17)) + │ │ ├── flags: ∅ + │ │ └── body: (length: 1) + │ │ └── @ LocalVariableReadNode (location: (19,16)-(19,17)) + │ │ ├── flags: newline + │ │ ├── name: :i + │ │ └── depth: 0 + │ ├── for_keyword_loc: (19,0)-(19,3) = "for" + │ ├── in_keyword_loc: (19,6)-(19,8) = "in" + │ ├── do_keyword_loc: ∅ + │ └── end_keyword_loc: (19,19)-(19,22) = "end" + ├── @ ForNode (location: (21,0)-(23,3)) + │ ├── flags: newline + │ ├── index: + │ │ @ MultiTargetNode (location: (21,4)-(21,13)) + │ │ ├── flags: ∅ + │ │ ├── lefts: (length: 2) + │ │ │ ├── @ MultiTargetNode (location: (21,4)-(21,10)) + │ │ │ │ ├── flags: ∅ + │ │ │ │ ├── lefts: (length: 2) + │ │ │ │ │ ├── @ LocalVariableTargetNode (location: (21,5)-(21,6)) + │ │ │ │ │ │ ├── flags: ∅ + │ │ │ │ │ │ ├── name: :a + │ │ │ │ │ │ └── depth: 0 + │ │ │ │ │ └── @ LocalVariableTargetNode (location: (21,8)-(21,9)) + │ │ │ │ │ ├── flags: ∅ + │ │ │ │ │ ├── name: :b + │ │ │ │ │ └── depth: 0 + │ │ │ │ ├── rest: ∅ + │ │ │ │ ├── rights: (length: 0) + │ │ │ │ ├── lparen_loc: (21,4)-(21,5) = "(" + │ │ │ │ └── rparen_loc: (21,9)-(21,10) = ")" + │ │ │ └── @ LocalVariableTargetNode (location: (21,12)-(21,13)) + │ │ │ ├── flags: ∅ + │ │ │ ├── name: :c + │ │ │ └── depth: 0 + │ │ ├── rest: ∅ + │ │ ├── rights: (length: 0) + │ │ ├── lparen_loc: ∅ + │ │ └── rparen_loc: ∅ + │ ├── collection: + │ │ @ RangeNode (location: (21,17)-(21,22)) + │ │ ├── flags: static_literal + │ │ ├── left: + │ │ │ @ IntegerNode (location: (21,17)-(21,18)) + │ │ │ ├── flags: static_literal, decimal + │ │ │ └── value: 1 + │ │ ├── right: + │ │ │ @ IntegerNode (location: (21,20)-(21,22)) + │ │ │ ├── flags: static_literal, decimal + │ │ │ └── value: 10 + │ │ └── operator_loc: (21,18)-(21,20) = ".." + │ ├── statements: + │ │ @ StatementsNode (location: (22,0)-(22,1)) + │ │ ├── flags: ∅ + │ │ └── body: (length: 1) + │ │ └── @ LocalVariableReadNode (location: (22,0)-(22,1)) + │ │ ├── flags: newline + │ │ ├── name: :i + │ │ └── depth: 0 + │ ├── for_keyword_loc: (21,0)-(21,3) = "for" + │ ├── in_keyword_loc: (21,14)-(21,16) = "in" + │ ├── do_keyword_loc: ∅ + │ └── end_keyword_loc: (23,0)-(23,3) = "end" + └── @ ForNode (location: (25,0)-(27,3)) ├── flags: newline ├── index: - │ @ LocalVariableTargetNode (location: (19,4)-(19,5)) + │ @ MultiTargetNode (location: (25,4)-(25,17)) │ ├── flags: ∅ - │ ├── name: :i - │ └── depth: 0 + │ ├── lefts: (length: 1) + │ │ └── @ MultiTargetNode (location: (25,4)-(25,10)) + │ │ ├── flags: ∅ + │ │ ├── lefts: (length: 2) + │ │ │ ├── @ LocalVariableTargetNode (location: (25,5)-(25,6)) + │ │ │ │ ├── flags: ∅ + │ │ │ │ ├── name: :a + │ │ │ │ └── depth: 0 + │ │ │ └── @ LocalVariableTargetNode (location: (25,8)-(25,9)) + │ │ │ ├── flags: ∅ + │ │ │ ├── name: :b + │ │ │ └── depth: 0 + │ │ ├── rest: ∅ + │ │ ├── rights: (length: 0) + │ │ ├── lparen_loc: (25,4)-(25,5) = "(" + │ │ └── rparen_loc: (25,9)-(25,10) = ")" + │ ├── rest: + │ │ @ SplatNode (location: (25,12)-(25,14)) + │ │ ├── flags: ∅ + │ │ ├── operator_loc: (25,12)-(25,13) = "*" + │ │ └── expression: + │ │ @ LocalVariableTargetNode (location: (25,13)-(25,14)) + │ │ ├── flags: ∅ + │ │ ├── name: :c + │ │ └── depth: 0 + │ ├── rights: (length: 1) + │ │ └── @ LocalVariableTargetNode (location: (25,16)-(25,17)) + │ │ ├── flags: ∅ + │ │ ├── name: :d + │ │ └── depth: 0 + │ ├── lparen_loc: ∅ + │ └── rparen_loc: ∅ ├── collection: - │ @ RangeNode (location: (19,9)-(19,14)) + │ @ RangeNode (location: (25,21)-(25,26)) │ ├── flags: static_literal │ ├── left: - │ │ @ IntegerNode (location: (19,9)-(19,10)) + │ │ @ IntegerNode (location: (25,21)-(25,22)) │ │ ├── flags: static_literal, decimal │ │ └── value: 1 │ ├── right: - │ │ @ IntegerNode (location: (19,12)-(19,14)) + │ │ @ IntegerNode (location: (25,24)-(25,26)) │ │ ├── flags: static_literal, decimal │ │ └── value: 10 - │ └── operator_loc: (19,10)-(19,12) = ".." + │ └── operator_loc: (25,22)-(25,24) = ".." ├── statements: - │ @ StatementsNode (location: (19,16)-(19,17)) + │ @ StatementsNode (location: (26,0)-(26,1)) │ ├── flags: ∅ │ └── body: (length: 1) - │ └── @ LocalVariableReadNode (location: (19,16)-(19,17)) + │ └── @ LocalVariableReadNode (location: (26,0)-(26,1)) │ ├── flags: newline │ ├── name: :i │ └── depth: 0 - ├── for_keyword_loc: (19,0)-(19,3) = "for" - ├── in_keyword_loc: (19,6)-(19,8) = "in" + ├── for_keyword_loc: (25,0)-(25,3) = "for" + ├── in_keyword_loc: (25,18)-(25,20) = "in" ├── do_keyword_loc: ∅ - └── end_keyword_loc: (19,19)-(19,22) = "end" + └── end_keyword_loc: (27,0)-(27,3) = "end" diff --git a/src/prism.c b/src/prism.c index d997c63d16..57860692cc 100644 --- a/src/prism.c +++ b/src/prism.c @@ -19097,9 +19097,10 @@ parse_parentheses(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t if (context_p(parser, PM_CONTEXT_MULTI_TARGET)) { /* All set, this is explicitly allowed by the parent context. */ - } else if (context_p(parser, PM_CONTEXT_FOR_INDEX) && match1(parser, PM_TOKEN_KEYWORD_IN)) { + } else if (context_p(parser, PM_CONTEXT_FOR_INDEX) && match2(parser, PM_TOKEN_KEYWORD_IN, PM_TOKEN_COMMA)) { /* All set, we're inside a for loop and we're parsing multiple - * targets. */ + * targets. A comma continues the index target list, as in + * `for (a, b), c in ...`. */ } else if (flags & PM_PARSE_ACCEPTS_STATEMENT) { /* The rescue-modifier value parser promotes this target on a * following `=` or comma. Reject any other binary operator that diff --git a/test/prism/fixtures/for.txt b/test/prism/fixtures/for.txt index b6eb2cb24f..99d8610f73 100644 --- a/test/prism/fixtures/for.txt +++ b/test/prism/fixtures/for.txt @@ -17,3 +17,11 @@ i end for i in 1..10; i; end + +for (a, b), c in 1..10 +i +end + +for (a, b), *c, d in 1..10 +i +end