Skip to content

Commit 661e6cf

Browse files
committed
GROOVY-11964: Additional multi-assignment forms
1 parent 406feaf commit 661e6cf

9 files changed

Lines changed: 2536 additions & 17 deletions

File tree

src/antlr/GroovyParser.g4

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,10 +578,15 @@ variableDeclaration[int t]
578578

579579
typeNamePairs
580580
: LPAREN typeNamePair (COMMA typeNamePair)* RPAREN
581+
| LPAREN keyedPair (COMMA keyedPair)* RPAREN
581582
;
582583

583584
typeNamePair
584-
: type? variableDeclaratorId
585+
: (DEF | VAR | type)? MUL? variableDeclaratorId
586+
;
587+
588+
keyedPair
589+
: key=identifier COLON (DEF | VAR | type)? variableDeclaratorId
585590
;
586591

587592
variableNames

src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.codehaus.groovy.ast.FieldNode;
5757
import org.codehaus.groovy.ast.GenericsType;
5858
import org.codehaus.groovy.ast.ImportNode;
59+
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
5960
import org.codehaus.groovy.ast.InnerClassNode;
6061
import org.codehaus.groovy.ast.MethodNode;
6162
import org.codehaus.groovy.ast.ModifierNode;
@@ -2205,16 +2206,56 @@ private boolean isFieldDeclaration(final ModifierManager modifierManager, final
22052206

22062207
@Override
22072208
public List<Expression> visitTypeNamePairs(final TypeNamePairsContext ctx) {
2208-
return ctx.typeNamePair().stream().map(this::visitTypeNamePair).collect(Collectors.toList());
2209+
if (asBoolean(ctx.keyedPair())) { // GEP-20 map-style: def (name: n, age: a) = person
2210+
return ctx.keyedPair().stream().map(this::visitKeyedPair).collect(Collectors.toList());
2211+
}
2212+
List<Expression> pairs = ctx.typeNamePair().stream().map(this::visitTypeNamePair).collect(Collectors.toList());
2213+
// GEP-20: at most one rest binding (*) per parens form
2214+
boolean seenRest = false;
2215+
for (Expression e : pairs) {
2216+
if (Boolean.TRUE.equals(e.getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) {
2217+
if (seenRest) {
2218+
throw createParsingFailedException("Only one rest binding (*) is allowed in a multi-assignment", e);
2219+
}
2220+
seenRest = true;
2221+
}
2222+
}
2223+
return pairs;
22092224
}
22102225

22112226
@Override
22122227
public VariableExpression visitTypeNamePair(final TypeNamePairContext ctx) {
2213-
return configureAST(
2228+
boolean isRest = asBoolean(ctx.MUL());
2229+
// GEP-20: typed rest (e.g. `def (h, List<Integer> *t) = list`) is accepted; the
2230+
// declared container type is honoured by static type checking and runtime coercion.
2231+
// `def` and `var` are also accepted in place of a type (equivalent to omitting it),
2232+
// for symmetry with switch case patterns and the bracket-form declaration grammar.
2233+
VariableExpression ve = configureAST(
22142234
new VariableExpression(
22152235
this.visitVariableDeclaratorId(ctx.variableDeclaratorId()).getName(),
2216-
this.visitType(ctx.type())),
2236+
binderType(ctx.DEF(), ctx.VAR(), ctx.type())),
22172237
ctx);
2238+
if (isRest) {
2239+
ve.putNodeMetaData(MultipleAssignmentMetadata.REST_BINDING, Boolean.TRUE);
2240+
}
2241+
return ve;
2242+
}
2243+
2244+
@Override
2245+
public VariableExpression visitKeyedPair(final KeyedPairContext ctx) {
2246+
VariableExpression ve = configureAST(
2247+
new VariableExpression(
2248+
this.visitVariableDeclaratorId(ctx.variableDeclaratorId()).getName(),
2249+
binderType(ctx.DEF(), ctx.VAR(), ctx.type())),
2250+
ctx);
2251+
ve.putNodeMetaData(MultipleAssignmentMetadata.MAP_KEY, this.visitIdentifier(ctx.key));
2252+
return ve;
2253+
}
2254+
2255+
/** GEP-20: resolve a binder's declared type — `def`/`var` produce the dynamic type, same as omitting a type. */
2256+
private ClassNode binderType(final TerminalNode defNode, final TerminalNode varNode, final TypeContext typeCtx) {
2257+
if (asBoolean(defNode) || asBoolean(varNode)) return ClassHelper.dynamicType();
2258+
return this.visitType(typeCtx);
22182259
}
22192260

22202261
@Override
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.ast;
20+
21+
/**
22+
* AST node metadata keys used by the multi-assignment destructuring pipeline
23+
* introduced in GEP-20. Each key is attached to a {@code VariableExpression}
24+
* appearing inside the {@code TupleExpression} on the LHS of a
25+
* {@code DeclarationExpression}.
26+
*/
27+
public enum MultipleAssignmentMetadata {
28+
/** Marker on the rest binder (the {@code *ident} slot). Value is {@link Boolean#TRUE}. */
29+
REST_BINDING,
30+
/** Value is the key name (String) used for a map-style {@code key: ident} binder. */
31+
MAP_KEY
32+
}

src/main/java/org/codehaus/groovy/classgen/asm/BinaryExpressionHelper.java

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.codehaus.groovy.GroovyBugError;
2222
import org.codehaus.groovy.ast.ClassHelper;
2323
import org.codehaus.groovy.ast.ClassNode;
24+
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
2425
import org.codehaus.groovy.ast.Variable;
2526
import org.codehaus.groovy.ast.expr.ArrayExpression;
2627
import org.codehaus.groovy.ast.expr.BinaryExpression;
@@ -43,6 +44,7 @@
4344
import org.codehaus.groovy.ast.tools.WideningCategories;
4445
import org.codehaus.groovy.classgen.AsmClassGenerator;
4546
import org.codehaus.groovy.classgen.BytecodeExpression;
47+
import org.codehaus.groovy.runtime.MultipleAssignmentSupport;
4648
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
4749
import org.codehaus.groovy.syntax.Token;
4850
import org.objectweb.asm.Label;
@@ -53,10 +55,12 @@
5355
import static org.codehaus.groovy.ast.tools.GeneralUtils.binX;
5456
import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
5557
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
58+
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
5659
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
5760
import static org.codehaus.groovy.ast.tools.GeneralUtils.elvisX;
5861
import static org.codehaus.groovy.ast.tools.GeneralUtils.notX;
5962
import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
63+
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
6064
import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
6165
import static org.codehaus.groovy.syntax.Types.ASSIGN;
6266
import static org.codehaus.groovy.syntax.Types.BITWISE_AND;
@@ -505,6 +509,101 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
505509
leftExpression.visit(acg);
506510
operandStack.remove(operandStack.getStackLength() - mark);
507511
} else { // multiple declaration or assignment
512+
TupleExpression tuple = (TupleExpression) leftExpression;
513+
java.util.List<Expression> elements = tuple.getExpressions();
514+
int tupleSize = elements.size();
515+
int restIndex = -1;
516+
for (int idx = 0; idx < tupleSize; idx++) {
517+
if (Boolean.TRUE.equals(elements.get(idx).getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) {
518+
restIndex = idx;
519+
break;
520+
}
521+
}
522+
boolean hasRest = (restIndex >= 0);
523+
boolean tailRest = hasRest && restIndex == tupleSize - 1;
524+
boolean isMapStyle = !elements.isEmpty()
525+
&& elements.get(0).getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY) != null;
526+
527+
// GEP-20 map-style destructuring: def (name: n, age: a) = person
528+
// Each binder is emitted as a property access on the RHS, dispatched via the MOP
529+
// (Map → key lookup, bean → getter, GroovyObject → getProperty).
530+
if (isMapStyle) {
531+
for (Expression e : elements) {
532+
String key = (String) e.getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY);
533+
// Property access is a read here; the surrounding pushLHS(true) above would
534+
// otherwise mark it as a store target.
535+
compileStack.popLHS();
536+
propX(rhsValueLoader, key).visit(acg);
537+
compileStack.pushLHS(true);
538+
assignOneMultiAssignSlot(e, defineVariable, operandStack, compileStack, acg);
539+
}
540+
compileStack.popLHS();
541+
if (returnRightValue) rhsValueLoader.visit(acg);
542+
compileStack.removeVar(rhsValueId);
543+
return;
544+
}
545+
546+
// GEP-20 degenerate case: `def (*t) = rhs` — single rest binder; equivalent to `def t = rhs`.
547+
if (tailRest && tupleSize == 1) {
548+
rhsValueLoader.visit(acg);
549+
if (defineVariable) {
550+
Variable v = (Variable) elements.get(0);
551+
operandStack.doGroovyCast(v);
552+
compileStack.defineVariable(v, true);
553+
operandStack.remove(1);
554+
} else {
555+
elements.get(0).visit(acg);
556+
}
557+
compileStack.popLHS();
558+
if (returnRightValue) rhsValueLoader.visit(acg);
559+
compileStack.removeVar(rhsValueId);
560+
return;
561+
}
562+
563+
// GEP-20 head/middle rest: def (*f, last) = list, def (l, *m, r) = list, etc.
564+
// Requires a sized, indexable RHS (Path B only — no iterator fallback).
565+
// Load-bearing ordering (GEP lines 177-186): the IntRange call for the rest slot
566+
// must be emitted BEFORE any negative-index call, so that an iterator/stream RHS
567+
// fails fast with MissingMethodException instead of hanging via materialisation.
568+
if (hasRest && !tailRest) {
569+
// 1. Emit the IntRange call for the rest slot first, via the helper that
570+
// returns an empty slice for inverted ranges (short RHS) and fails fast
571+
// for non-indexable RHS (iterator/stream/set), per GEP lines 177-186.
572+
// Number of fixed slots after the rest = tupleSize - restIndex - 1; their negative
573+
// indices span [-k, -1]; the rest slice therefore ends at -(k+1) = -(tupleSize - restIndex).
574+
// e.g. def (*f,last): -2; def (l,*m,r): -2; def (a,b,*m,y,z): -3
575+
int toIdx = -(tupleSize - restIndex);
576+
MethodCallExpression sliceCall = callX(
577+
classX(MultipleAssignmentSupport.class),
578+
"nonTailRestSlice",
579+
args(rhsValueLoader, constX(restIndex, true), constX(toIdx, true)));
580+
sliceCall.setImplicitThis(false);
581+
sliceCall.visit(acg);
582+
assignOneMultiAssignSlot(elements.get(restIndex), defineVariable, operandStack, compileStack, acg);
583+
584+
// 2. Positive-index fixed slots (before rest), left-to-right.
585+
for (int idx = 0; idx < restIndex; idx++) {
586+
MethodCallExpression call = callX(rhsValueLoader, "getAt", constX(idx, true));
587+
call.setImplicitThis(false);
588+
call.visit(acg);
589+
assignOneMultiAssignSlot(elements.get(idx), defineVariable, operandStack, compileStack, acg);
590+
}
591+
592+
// 3. Negative-index fixed slots (after rest), left-to-right.
593+
for (int idx = restIndex + 1; idx < tupleSize; idx++) {
594+
int negIdx = -(tupleSize - idx);
595+
MethodCallExpression call = callX(rhsValueLoader, "getAt", constX(negIdx, true));
596+
call.setImplicitThis(false);
597+
call.visit(acg);
598+
assignOneMultiAssignSlot(elements.get(idx), defineVariable, operandStack, compileStack, acg);
599+
}
600+
601+
compileStack.popLHS();
602+
if (returnRightValue) rhsValueLoader.visit(acg);
603+
compileStack.removeVar(rhsValueId);
604+
return;
605+
}
606+
508607
MethodCallExpression iterator = callX(rhsValueLoader, "iterator");
509608
iterator.setImplicitThis(false);
510609
iterator.visit(acg);
@@ -529,9 +628,17 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
529628
mv.visitJumpInsn(IF_ACMPEQ, useGetAt);
530629

531630
boolean first = true;
532-
for (Expression e : (TupleExpression) leftExpression) {
533-
if (first) {
534-
first = false;
631+
for (int idx = 0; idx < tupleSize; idx++) {
632+
Expression e = elements.get(idx);
633+
if (idx == restIndex) { // tail rest: dispatch Path B (slice) vs Path C (iterator) at runtime
634+
MethodCallExpression restCall = callX(
635+
classX(MultipleAssignmentSupport.class),
636+
"tailRest",
637+
args(rhsValueLoader, constX(idx, true), seq));
638+
restCall.setImplicitThis(false);
639+
restCall.visit(acg);
640+
} else if (first) {
641+
first = false; // value already on stack from next() above
535642
} else {
536643
ternaryX(hasNext, next, nullX()).visit(acg);
537644
}
@@ -553,11 +660,19 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
553660

554661
mv.visitLabel(useGetAt_noPop);
555662

556-
int i = 0;
557-
for (Expression e : (TupleExpression) leftExpression) {
558-
MethodCallExpression getAt = callX(rhsValueLoader, "getAt", constX(i++, true));
559-
getAt.setImplicitThis(false);
560-
getAt.visit(acg);
663+
for (int idx = 0; idx < tupleSize; idx++) {
664+
Expression e = elements.get(idx);
665+
MethodCallExpression call;
666+
if (idx == restIndex) { // tail rest: dispatch via helper so empty RHS / non-indexable cases are handled uniformly
667+
call = callX(
668+
classX(MultipleAssignmentSupport.class),
669+
"tailRest",
670+
args(rhsValueLoader, constX(idx, true), seq));
671+
} else {
672+
call = callX(rhsValueLoader, "getAt", constX(idx, true));
673+
}
674+
call.setImplicitThis(false);
675+
call.visit(acg);
561676

562677
if (defineVariable) {
563678
Variable v = (Variable) e;
@@ -585,6 +700,20 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
585700
compileStack.removeVar(rhsValueId);
586701
}
587702

703+
/** GEP-20: assign the single value currently on the operand stack to the given declarator slot. */
704+
private void assignOneMultiAssignSlot(final Expression e, final boolean defineVariable,
705+
final OperandStack operandStack, final CompileStack compileStack,
706+
final AsmClassGenerator acg) {
707+
if (defineVariable) {
708+
Variable v = (Variable) e;
709+
operandStack.doGroovyCast(v);
710+
compileStack.defineVariable(v, true);
711+
operandStack.remove(1);
712+
} else {
713+
e.visit(acg);
714+
}
715+
}
716+
588717
protected void evaluateCompareExpression(final MethodCaller compareMethod, final BinaryExpression expression) {
589718
Expression leftExp = expression.getLeftExpression();
590719
Expression rightExp = expression.getRightExpression();

0 commit comments

Comments
 (0)