Skip to content

Commit 7b26481

Browse files
committed
GROOVY-11970: Provide support for compound assignment operator overloading (GEP-15)
1 parent 9c23ae1 commit 7b26481

13 files changed

Lines changed: 655 additions & 18 deletions

src/main/java/groovy/transform/OperatorRename.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,28 @@
152152
* @return the replacement method name
153153
*/
154154
String compareTo() default Undefined.STRING;
155+
/** GEP-15: rename the dedicated compound-assignment method for {@code +=}. */
156+
String plusAssign() default Undefined.STRING;
157+
/** GEP-15: rename the dedicated compound-assignment method for {@code -=}. */
158+
String minusAssign() default Undefined.STRING;
159+
/** GEP-15: rename the dedicated compound-assignment method for {@code *=}. */
160+
String multiplyAssign() default Undefined.STRING;
161+
/** GEP-15: rename the dedicated compound-assignment method for {@code /=}. */
162+
String divAssign() default Undefined.STRING;
163+
/** GEP-15: rename the dedicated compound-assignment method for {@code %=}. */
164+
String remainderAssign() default Undefined.STRING;
165+
/** GEP-15: rename the dedicated compound-assignment method for {@code **=}. */
166+
String powerAssign() default Undefined.STRING;
167+
/** GEP-15: rename the dedicated compound-assignment method for {@code <<=}. */
168+
String leftShiftAssign() default Undefined.STRING;
169+
/** GEP-15: rename the dedicated compound-assignment method for {@code >>=}. */
170+
String rightShiftAssign() default Undefined.STRING;
171+
/** GEP-15: rename the dedicated compound-assignment method for {@code >>>=}. */
172+
String rightShiftUnsignedAssign() default Undefined.STRING;
173+
/** GEP-15: rename the dedicated compound-assignment method for {@code &=}. */
174+
String andAssign() default Undefined.STRING;
175+
/** GEP-15: rename the dedicated compound-assignment method for {@code |=}. */
176+
String orAssign() default Undefined.STRING;
177+
/** GEP-15: rename the dedicated compound-assignment method for {@code ^=}. */
178+
String xorAssign() default Undefined.STRING;
155179
}

src/main/java/org/codehaus/groovy/classgen/Verifier.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
import static org.codehaus.groovy.ast.tools.GenericsUtils.parameterizeType;
127127
import static org.codehaus.groovy.ast.tools.PropertyNodeUtils.adjustPropertyModifiersForMethod;
128128
import static org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys.STATIC_COMPILE_NODE;
129+
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.COMPOUND_ASSIGN_TARGET;
129130
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
130131

131132
/**
@@ -349,6 +350,12 @@ public void variableNotFinal(Variable var, final Expression bexp) {
349350
if (var instanceof VariableExpression) {
350351
var = ((VariableExpression) var).getAccessedVariable();
351352
}
353+
// GEP-15: a compound-assign that resolved to a *Assign method (e.g. plusAssign)
354+
// mutates the receiver in place rather than reassigning the variable, so the
355+
// final-reassignment check must not fire for it.
356+
if (bexp instanceof BinaryExpression be && be.getNodeMetaData(COMPOUND_ASSIGN_TARGET) != null) {
357+
return;
358+
}
352359
if (var instanceof VariableExpression && isFinal(var.getModifiers())) {
353360
throw new RuntimeParserException("The variable [" + var.getName() + "] is declared final but is reassigned", bexp);
354361
}

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

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.codehaus.groovy.ast.ClassNode;
2424
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
2525
import org.codehaus.groovy.ast.Variable;
26+
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
2627
import org.codehaus.groovy.ast.expr.ArrayExpression;
2728
import org.codehaus.groovy.ast.expr.BinaryExpression;
2829
import org.codehaus.groovy.ast.expr.ClassExpression;
@@ -36,6 +37,7 @@
3637
import org.codehaus.groovy.ast.expr.PostfixExpression;
3738
import org.codehaus.groovy.ast.expr.PrefixExpression;
3839
import org.codehaus.groovy.ast.expr.PropertyExpression;
40+
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
3941
import org.codehaus.groovy.ast.expr.TernaryExpression;
4042
import org.codehaus.groovy.ast.expr.TupleExpression;
4143
import org.codehaus.groovy.ast.expr.VariableExpression;
@@ -209,15 +211,15 @@ public void eval(final BinaryExpression expression) {
209211
break;
210212

211213
case BITWISE_AND_EQUAL:
212-
evaluateBinaryExpressionWithAssignment("and", expression);
214+
evaluateCompoundAssign("andAssign", "and", expression);
213215
break;
214216

215217
case BITWISE_OR:
216218
evaluateBinaryExpression("or", expression);
217219
break;
218220

219221
case BITWISE_OR_EQUAL:
220-
evaluateBinaryExpressionWithAssignment("or", expression);
222+
evaluateCompoundAssign("orAssign", "or", expression);
221223
break;
222224

223225
case BITWISE_XOR:
@@ -229,31 +231,31 @@ public void eval(final BinaryExpression expression) {
229231
break;
230232

231233
case BITWISE_XOR_EQUAL:
232-
evaluateBinaryExpressionWithAssignment("xor", expression);
234+
evaluateCompoundAssign("xorAssign", "xor", expression);
233235
break;
234236

235237
case PLUS:
236238
evaluateBinaryExpression("plus", expression);
237239
break;
238240

239241
case PLUS_EQUAL:
240-
evaluateBinaryExpressionWithAssignment("plus", expression);
242+
evaluateCompoundAssign("plusAssign", "plus", expression);
241243
break;
242244

243245
case MINUS:
244246
evaluateBinaryExpression("minus", expression);
245247
break;
246248

247249
case MINUS_EQUAL:
248-
evaluateBinaryExpressionWithAssignment("minus", expression);
250+
evaluateCompoundAssign("minusAssign", "minus", expression);
249251
break;
250252

251253
case MULTIPLY:
252254
evaluateBinaryExpression("multiply", expression);
253255
break;
254256

255257
case MULTIPLY_EQUAL:
256-
evaluateBinaryExpressionWithAssignment("multiply", expression);
258+
evaluateCompoundAssign("multiplyAssign", "multiply", expression);
257259
break;
258260

259261
case DIVIDE:
@@ -263,14 +265,15 @@ public void eval(final BinaryExpression expression) {
263265
case DIVIDE_EQUAL:
264266
//SPG don't use divide since BigInteger implements directly
265267
//and we want to dispatch through DefaultGroovyMethods to get a BigDecimal result
266-
evaluateBinaryExpressionWithAssignment("div", expression);
268+
evaluateCompoundAssign("divAssign", "div", expression);
267269
break;
268270

269271
case INTDIV:
270272
evaluateBinaryExpression("intdiv", expression);
271273
break;
272274

273275
case INTDIV_EQUAL:
276+
// GEP-15 explicitly excludes \= (no intdivAssign convention)
274277
evaluateBinaryExpressionWithAssignment("intdiv", expression);
275278
break;
276279

@@ -279,23 +282,25 @@ public void eval(final BinaryExpression expression) {
279282
break;
280283

281284
case MOD_EQUAL:
282-
evaluateBinaryExpressionWithAssignment("mod", expression);
285+
// GEP-15 maps both MOD_EQUAL and REMAINDER_EQUAL to remainderAssign for consistency
286+
// with getOperationName collapse, even though current parser only emits REMAINDER_EQUAL.
287+
evaluateCompoundAssign("remainderAssign", "mod", expression);
283288
break;
284289

285290
case REMAINDER:
286291
evaluateBinaryExpression("remainder", expression);
287292
break;
288293

289294
case REMAINDER_EQUAL:
290-
evaluateBinaryExpressionWithAssignment("remainder", expression);
295+
evaluateCompoundAssign("remainderAssign", "remainder", expression);
291296
break;
292297

293298
case POWER:
294299
evaluateBinaryExpression("power", expression);
295300
break;
296301

297302
case POWER_EQUAL:
298-
evaluateBinaryExpressionWithAssignment("power", expression);
303+
evaluateCompoundAssign("powerAssign", "power", expression);
299304
break;
300305

301306
case ELVIS_EQUAL:
@@ -307,23 +312,23 @@ public void eval(final BinaryExpression expression) {
307312
break;
308313

309314
case LEFT_SHIFT_EQUAL:
310-
evaluateBinaryExpressionWithAssignment("leftShift", expression);
315+
evaluateCompoundAssign("leftShiftAssign", "leftShift", expression);
311316
break;
312317

313318
case RIGHT_SHIFT:
314319
evaluateBinaryExpression("rightShift", expression);
315320
break;
316321

317322
case RIGHT_SHIFT_EQUAL:
318-
evaluateBinaryExpressionWithAssignment("rightShift", expression);
323+
evaluateCompoundAssign("rightShiftAssign", "rightShift", expression);
319324
break;
320325

321326
case RIGHT_SHIFT_UNSIGNED:
322327
evaluateBinaryExpression("rightShiftUnsigned", expression);
323328
break;
324329

325330
case RIGHT_SHIFT_UNSIGNED_EQUAL:
326-
evaluateBinaryExpressionWithAssignment("rightShiftUnsigned", expression);
331+
evaluateCompoundAssign("rightShiftUnsignedAssign", "rightShiftUnsigned", expression);
327332
break;
328333

329334
case KEYWORD_INSTANCEOF:
@@ -891,6 +896,44 @@ protected void evaluateBinaryExpressionWithAssignment(final String method, final
891896
controller.getCompileStack().popLHS();
892897
}
893898

899+
/**
900+
* GEP-15: dynamic-mode compound-assign codegen. Routes through
901+
* {@link ScriptBytecodeAdapter#compoundAssign(Object, Object, String, String)}
902+
* which dispatches to {@code assignName} when the receiver responds to it,
903+
* and falls back to {@code baseName} otherwise. The caller stores the helper's
904+
* return value into the LHS — for the in-place branch this is a no-op store
905+
* of the receiver back to itself; for the fallback branch it is the usual
906+
* "x = x.op(y)" assignment.
907+
*/
908+
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression expression) {
909+
Expression leftExpression = expression.getLeftExpression();
910+
if (leftExpression instanceof BinaryExpression bexp
911+
&& bexp.getOperation().getType() == LEFT_SQUARE_BRACKET) {
912+
// Subscript LHS (e.g. a[i] += b) is intentionally out of scope for GEP-15;
913+
// keep the legacy getAt/putAt-based path.
914+
evaluateArrayAssignmentWithOperator(baseName, expression, bexp);
915+
return;
916+
}
917+
918+
StaticMethodCallExpression helperCall = new StaticMethodCallExpression(
919+
ClassHelper.make(ScriptBytecodeAdapter.class),
920+
"compoundAssign",
921+
new ArgumentListExpression(new Expression[]{
922+
leftExpression,
923+
expression.getRightExpression(),
924+
new ConstantExpression(assignName),
925+
new ConstantExpression(baseName)
926+
})
927+
);
928+
helperCall.setSourcePosition(expression);
929+
helperCall.visit(controller.getAcg());
930+
931+
controller.getOperandStack().dup();
932+
controller.getCompileStack().pushLHS(true);
933+
leftExpression.visit(controller.getAcg());
934+
controller.getCompileStack().popLHS();
935+
}
936+
894937
private void evaluateInstanceof(final BinaryExpression expression) {
895938
CompileStack compileStack = controller.getCompileStack();
896939
OperandStack operandStack = controller.getOperandStack();

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,15 @@ protected void evaluateBinaryExpressionWithAssignment(final String method, final
332332
super.evaluateBinaryExpressionWithAssignment(method, binExp);
333333
}
334334

335+
@Override
336+
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression binExp) {
337+
// GEP-15: keep the primitive-int/long/float/double fast paths for int i += j etc.;
338+
// they cannot have a *Assign override anyway since primitives are excluded.
339+
if (doAssignmentToArray(binExp)) return;
340+
if (doAssignmentToLocalVariable(baseName, binExp)) return;
341+
super.evaluateCompoundAssign(assignName, baseName, binExp);
342+
}
343+
335344
private boolean doAssignmentToLocalVariable(final String method, final BinaryExpression binExp) {
336345
Expression left = binExp.getLeftExpression();
337346
if (left instanceof VariableExpression ve) {

src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesBinaryExpressionMultiTypeDispatcher.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import static org.codehaus.groovy.transform.sc.StaticCompilationVisitor.ARRAYLIST_CONSTRUCTOR;
6868
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isAssignment;
6969
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor.inferLoopElementType;
70+
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.COMPOUND_ASSIGN_TARGET;
7071
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
7172
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_TYPE;
7273
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
@@ -136,6 +137,28 @@ private static void visitInsnByType(final ClassNode top, final MethodVisitor mv,
136137

137138
@Override
138139
protected void evaluateBinaryExpressionWithAssignment(final String method, final BinaryExpression expression) {
140+
if (tryStaticCompoundAssignPaths(method, expression)) return;
141+
super.evaluateBinaryExpressionWithAssignment(method, expression);
142+
}
143+
144+
@Override
145+
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression expression) {
146+
if (tryStaticCompoundAssignPaths(baseName, expression)) return;
147+
super.evaluateCompoundAssign(assignName, baseName, expression);
148+
}
149+
150+
/**
151+
* GEP-15 + legacy setter fast-path. Returns true when codegen has been emitted
152+
* (no further dispatch required).
153+
*/
154+
private boolean tryStaticCompoundAssignPaths(final String baseName, final BinaryExpression expression) {
155+
MethodNode assignTarget = expression.getNodeMetaData(COMPOUND_ASSIGN_TARGET);
156+
if (assignTarget != null) {
157+
// GEP-15: receiver.<assignMethod>(arg); receiver remains the expression value.
158+
// The setter (for property LHS) is intentionally skipped.
159+
emitCompoundAssignCall(assignTarget, expression);
160+
return true;
161+
}
139162
Expression leftExpression = expression.getLeftExpression();
140163
if (leftExpression instanceof PropertyExpression pexp
141164
&& !(leftExpression instanceof AttributeExpression)) {
@@ -161,10 +184,31 @@ protected void evaluateBinaryExpressionWithAssignment(final String method, final
161184
pexp.isSpreadSafe(),
162185
pexp.isImplicitThis(),
163186
true)) { // TODO: GROOVY-11843
164-
return;
187+
return true;
165188
}
166189
}
167-
super.evaluateBinaryExpressionWithAssignment(method, expression);
190+
return false;
191+
}
192+
193+
private void emitCompoundAssignCall(final MethodNode target, final BinaryExpression expression) {
194+
OperandStack operandStack = controller.getOperandStack();
195+
Expression leftExpression = expression.getLeftExpression();
196+
Expression rightExpression = expression.getRightExpression();
197+
198+
leftExpression.visit(controller.getAcg());
199+
ClassNode receiverType = operandStack.getTopOperand();
200+
int slot = controller.getCompileStack().defineTemporaryVariable("$gep15recv", receiverType, true);
201+
202+
VariableSlotLoader callReceiver = new VariableSlotLoader(receiverType, slot, operandStack);
203+
MethodCallExpression call = callX(callReceiver, target.getName(), rightExpression);
204+
call.setMethodTarget(target);
205+
call.setImplicitThis(false);
206+
call.setSourcePosition(expression);
207+
call.visit(controller.getAcg());
208+
operandStack.pop(); // discard the *Assign return value
209+
210+
new VariableSlotLoader(receiverType, slot, operandStack).visit(controller.getAcg());
211+
controller.getCompileStack().removeVar(slot);
168212
}
169213

170214
@Override

src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,4 +998,24 @@ public static Object bitwiseNegate(final Object value) throws Throwable {
998998
throw unwrap(gre);
999999
}
10001000
}
1001+
1002+
/**
1003+
* GEP-15: dispatcher for compound-assignment operators in dynamic Groovy.
1004+
* If {@code receiver} responds to {@code assignName} with the supplied argument,
1005+
* invoke it and return {@code receiver} (so the caller's STORE assigns the same
1006+
* reference back, leaving the in-place mutation visible). Otherwise fall back to
1007+
* {@code baseName} and return its result for the caller to assign.
1008+
*/
1009+
public static Object compoundAssign(final Object receiver, final Object arg,
1010+
final String assignName, final String baseName) throws Throwable {
1011+
if (receiver != null) {
1012+
MetaClass mc = InvokerHelper.getMetaClass(receiver);
1013+
if (!mc.respondsTo(receiver, assignName, new Object[]{arg}).isEmpty()) {
1014+
invokeMethodN(ScriptBytecodeAdapter.class, receiver, assignName, new Object[]{arg});
1015+
return receiver;
1016+
}
1017+
}
1018+
return invokeMethodN(ScriptBytecodeAdapter.class, receiver, baseName, new Object[]{arg});
1019+
}
1020+
10011021
}

src/main/java/org/codehaus/groovy/syntax/Types.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,8 @@ public static boolean ofType(int specific, int general) {
415415
case ASSIGNMENT_OPERATOR:
416416
return specific == EQUAL || (specific >= PLUS_EQUAL && specific <= ELVIS_EQUAL) || (specific >= LOGICAL_OR_EQUAL && specific <= LOGICAL_AND_EQUAL)
417417
|| (specific >= LEFT_SHIFT_EQUAL && specific <= RIGHT_SHIFT_UNSIGNED_EQUAL)
418-
|| (specific >= BITWISE_OR_EQUAL && specific <= BITWISE_XOR_EQUAL);
418+
|| (specific >= BITWISE_OR_EQUAL && specific <= BITWISE_XOR_EQUAL)
419+
|| specific == REMAINDER_EQUAL;
419420

420421
case COMPARISON_OPERATOR:
421422
return specific >= COMPARE_NOT_EQUAL && specific <= COMPARE_TO;

0 commit comments

Comments
 (0)