Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/antlr/GroovyParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,15 @@ variableDeclaration[int t]

typeNamePairs
: LPAREN typeNamePair (COMMA typeNamePair)* RPAREN
| LPAREN keyedPair (COMMA keyedPair)* RPAREN
;

typeNamePair
: type? variableDeclaratorId
: (DEF | VAR | type)? MUL? variableDeclaratorId
;

keyedPair
: key=identifier COLON (DEF | VAR | type)? variableDeclaratorId
;

variableNames
Expand Down
47 changes: 44 additions & 3 deletions src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModifierNode;
Expand Down Expand Up @@ -2205,16 +2206,56 @@ private boolean isFieldDeclaration(final ModifierManager modifierManager, final

@Override
public List<Expression> visitTypeNamePairs(final TypeNamePairsContext ctx) {
return ctx.typeNamePair().stream().map(this::visitTypeNamePair).collect(Collectors.toList());
if (asBoolean(ctx.keyedPair())) { // GEP-20 map-style: def (name: n, age: a) = person
return ctx.keyedPair().stream().map(this::visitKeyedPair).collect(Collectors.toList());
}
List<Expression> pairs = ctx.typeNamePair().stream().map(this::visitTypeNamePair).collect(Collectors.toList());
// GEP-20: at most one rest binding (*) per parens form
boolean seenRest = false;
for (Expression e : pairs) {
if (Boolean.TRUE.equals(e.getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) {
if (seenRest) {
throw createParsingFailedException("Only one rest binding (*) is allowed in a multi-assignment", e);
}
seenRest = true;
}
}
return pairs;
}

@Override
public VariableExpression visitTypeNamePair(final TypeNamePairContext ctx) {
return configureAST(
boolean isRest = asBoolean(ctx.MUL());
// GEP-20: typed rest (e.g. `def (h, List<Integer> *t) = list`) is accepted; the
// declared container type is honoured by static type checking and runtime coercion.
// `def` and `var` are also accepted in place of a type (equivalent to omitting it),
// for symmetry with switch case patterns and the bracket-form declaration grammar.
VariableExpression ve = configureAST(
new VariableExpression(
this.visitVariableDeclaratorId(ctx.variableDeclaratorId()).getName(),
this.visitType(ctx.type())),
binderType(ctx.DEF(), ctx.VAR(), ctx.type())),
ctx);
if (isRest) {
ve.putNodeMetaData(MultipleAssignmentMetadata.REST_BINDING, Boolean.TRUE);
}
return ve;
}

@Override
public VariableExpression visitKeyedPair(final KeyedPairContext ctx) {
VariableExpression ve = configureAST(
new VariableExpression(
this.visitVariableDeclaratorId(ctx.variableDeclaratorId()).getName(),
binderType(ctx.DEF(), ctx.VAR(), ctx.type())),
ctx);
ve.putNodeMetaData(MultipleAssignmentMetadata.MAP_KEY, this.visitIdentifier(ctx.key));
return ve;
}

/** GEP-20: resolve a binder's declared type — `def`/`var` produce the dynamic type, same as omitting a type. */
private ClassNode binderType(final TerminalNode defNode, final TerminalNode varNode, final TypeContext typeCtx) {
if (asBoolean(defNode) || asBoolean(varNode)) return ClassHelper.dynamicType();
return this.visitType(typeCtx);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.ast;

/**
* AST node metadata keys used by the multi-assignment destructuring pipeline
* introduced in GEP-20. Each key is attached to a {@code VariableExpression}
* appearing inside the {@code TupleExpression} on the LHS of a
* {@code DeclarationExpression}.
*/
public enum MultipleAssignmentMetadata {
/** Marker on the rest binder (the {@code *ident} slot). Value is {@link Boolean#TRUE}. */
REST_BINDING,
/** Value is the key name (String) used for a map-style {@code key: ident} binder. */
MAP_KEY
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
Expand All @@ -43,6 +44,7 @@
import org.codehaus.groovy.ast.tools.WideningCategories;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.runtime.MultipleAssignmentSupport;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.syntax.Token;
import org.objectweb.asm.Label;
Expand All @@ -53,10 +55,12 @@
import static org.codehaus.groovy.ast.tools.GeneralUtils.binX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.elvisX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.notX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
import static org.codehaus.groovy.syntax.Types.ASSIGN;
import static org.codehaus.groovy.syntax.Types.BITWISE_AND;
Expand Down Expand Up @@ -498,6 +502,101 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
leftExpression.visit(acg);
operandStack.remove(operandStack.getStackLength() - mark);
} else { // multiple declaration or assignment
TupleExpression tuple = (TupleExpression) leftExpression;
java.util.List<Expression> elements = tuple.getExpressions();
int tupleSize = elements.size();
int restIndex = -1;
for (int idx = 0; idx < tupleSize; idx++) {
if (Boolean.TRUE.equals(elements.get(idx).getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) {
restIndex = idx;
break;
}
}
boolean hasRest = (restIndex >= 0);
boolean tailRest = hasRest && restIndex == tupleSize - 1;
boolean isMapStyle = !elements.isEmpty()
&& elements.get(0).getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY) != null;

// GEP-20 map-style destructuring: def (name: n, age: a) = person
// Each binder is emitted as a property access on the RHS, dispatched via the MOP
// (Map → key lookup, bean → getter, GroovyObject → getProperty).
if (isMapStyle) {
for (Expression e : elements) {
String key = (String) e.getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY);
// Property access is a read here; the surrounding pushLHS(true) above would
// otherwise mark it as a store target.
compileStack.popLHS();
propX(rhsValueLoader, key).visit(acg);
compileStack.pushLHS(true);
assignOneMultiAssignSlot(e, defineVariable, operandStack, compileStack, acg);
}
compileStack.popLHS();
if (returnRightValue) rhsValueLoader.visit(acg);
compileStack.removeVar(rhsValueId);
return;
}

// GEP-20 degenerate case: `def (*t) = rhs` — single rest binder; equivalent to `def t = rhs`.
if (tailRest && tupleSize == 1) {
rhsValueLoader.visit(acg);
if (defineVariable) {
Variable v = (Variable) elements.get(0);
operandStack.doGroovyCast(v);
compileStack.defineVariable(v, true);
operandStack.remove(1);
} else {
elements.get(0).visit(acg);
}
compileStack.popLHS();
if (returnRightValue) rhsValueLoader.visit(acg);
compileStack.removeVar(rhsValueId);
return;
}

// GEP-20 head/middle rest: def (*f, last) = list, def (l, *m, r) = list, etc.
// Requires a sized, indexable RHS (Path B only — no iterator fallback).
// Load-bearing ordering (GEP lines 177-186): the IntRange call for the rest slot
// must be emitted BEFORE any negative-index call, so that an iterator/stream RHS
// fails fast with MissingMethodException instead of hanging via materialisation.
if (hasRest && !tailRest) {
// 1. Emit the IntRange call for the rest slot first, via the helper that
// returns an empty slice for inverted ranges (short RHS) and fails fast
// for non-indexable RHS (iterator/stream/set), per GEP lines 177-186.
// Number of fixed slots after the rest = tupleSize - restIndex - 1; their negative
// indices span [-k, -1]; the rest slice therefore ends at -(k+1) = -(tupleSize - restIndex).
// e.g. def (*f,last): -2; def (l,*m,r): -2; def (a,b,*m,y,z): -3
int toIdx = -(tupleSize - restIndex);
MethodCallExpression sliceCall = callX(
classX(MultipleAssignmentSupport.class),
"nonTailRestSlice",
args(rhsValueLoader, constX(restIndex, true), constX(toIdx, true)));
sliceCall.setImplicitThis(false);
sliceCall.visit(acg);
assignOneMultiAssignSlot(elements.get(restIndex), defineVariable, operandStack, compileStack, acg);

// 2. Positive-index fixed slots (before rest), left-to-right.
for (int idx = 0; idx < restIndex; idx++) {
MethodCallExpression call = callX(rhsValueLoader, "getAt", constX(idx, true));
call.setImplicitThis(false);
call.visit(acg);
assignOneMultiAssignSlot(elements.get(idx), defineVariable, operandStack, compileStack, acg);
}

// 3. Negative-index fixed slots (after rest), left-to-right.
for (int idx = restIndex + 1; idx < tupleSize; idx++) {
int negIdx = -(tupleSize - idx);
MethodCallExpression call = callX(rhsValueLoader, "getAt", constX(negIdx, true));
call.setImplicitThis(false);
call.visit(acg);
assignOneMultiAssignSlot(elements.get(idx), defineVariable, operandStack, compileStack, acg);
}

compileStack.popLHS();
if (returnRightValue) rhsValueLoader.visit(acg);
compileStack.removeVar(rhsValueId);
return;
}

MethodCallExpression iterator = callX(rhsValueLoader, "iterator");
iterator.setImplicitThis(false);
iterator.visit(acg);
Expand All @@ -522,9 +621,17 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
mv.visitJumpInsn(IF_ACMPEQ, useGetAt);

boolean first = true;
for (Expression e : (TupleExpression) leftExpression) {
if (first) {
first = false;
for (int idx = 0; idx < tupleSize; idx++) {
Expression e = elements.get(idx);
if (idx == restIndex) { // tail rest: dispatch Path B (slice) vs Path C (iterator) at runtime
MethodCallExpression restCall = callX(
classX(MultipleAssignmentSupport.class),
"tailRest",
args(rhsValueLoader, constX(idx, true), seq));
restCall.setImplicitThis(false);
restCall.visit(acg);
} else if (first) {
first = false; // value already on stack from next() above
} else {
ternaryX(hasNext, next, nullX()).visit(acg);
}
Expand All @@ -546,11 +653,19 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin

mv.visitLabel(useGetAt_noPop);

int i = 0;
for (Expression e : (TupleExpression) leftExpression) {
MethodCallExpression getAt = callX(rhsValueLoader, "getAt", constX(i++, true));
getAt.setImplicitThis(false);
getAt.visit(acg);
for (int idx = 0; idx < tupleSize; idx++) {
Expression e = elements.get(idx);
MethodCallExpression call;
if (idx == restIndex) { // tail rest: dispatch via helper so empty RHS / non-indexable cases are handled uniformly
call = callX(
classX(MultipleAssignmentSupport.class),
"tailRest",
args(rhsValueLoader, constX(idx, true), seq));
} else {
call = callX(rhsValueLoader, "getAt", constX(idx, true));
}
call.setImplicitThis(false);
call.visit(acg);

if (defineVariable) {
Variable v = (Variable) e;
Expand Down Expand Up @@ -578,6 +693,20 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
compileStack.removeVar(rhsValueId);
}

/** GEP-20: assign the single value currently on the operand stack to the given declarator slot. */
private void assignOneMultiAssignSlot(final Expression e, final boolean defineVariable,
final OperandStack operandStack, final CompileStack compileStack,
final AsmClassGenerator acg) {
if (defineVariable) {
Variable v = (Variable) e;
operandStack.doGroovyCast(v);
compileStack.defineVariable(v, true);
operandStack.remove(1);
} else {
e.visit(acg);
}
}

protected void evaluateCompareExpression(final MethodCaller compareMethod, final BinaryExpression expression) {
Expression leftExp = expression.getLeftExpression();
Expression rightExp = expression.getRightExpression();
Expand Down
Loading
Loading