Skip to content

Commit a49310e

Browse files
committed
GROOVY-11970: Provide support for compound assignment operator overloading (GEP-15)
1 parent dd95105 commit a49310e

13 files changed

Lines changed: 663 additions & 18 deletions

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,28 @@
6262
String or() default Undefined.STRING;
6363
String xor() default Undefined.STRING;
6464
String compareTo() default Undefined.STRING;
65+
/** GEP-15: rename the dedicated compound-assignment method for {@code +=}. */
66+
String plusAssign() default Undefined.STRING;
67+
/** GEP-15: rename the dedicated compound-assignment method for {@code -=}. */
68+
String minusAssign() default Undefined.STRING;
69+
/** GEP-15: rename the dedicated compound-assignment method for {@code *=}. */
70+
String multiplyAssign() default Undefined.STRING;
71+
/** GEP-15: rename the dedicated compound-assignment method for {@code /=}. */
72+
String divAssign() default Undefined.STRING;
73+
/** GEP-15: rename the dedicated compound-assignment method for {@code %=}. */
74+
String remainderAssign() default Undefined.STRING;
75+
/** GEP-15: rename the dedicated compound-assignment method for {@code **=}. */
76+
String powerAssign() default Undefined.STRING;
77+
/** GEP-15: rename the dedicated compound-assignment method for {@code <<=}. */
78+
String leftShiftAssign() default Undefined.STRING;
79+
/** GEP-15: rename the dedicated compound-assignment method for {@code >>=}. */
80+
String rightShiftAssign() default Undefined.STRING;
81+
/** GEP-15: rename the dedicated compound-assignment method for {@code >>>=}. */
82+
String rightShiftUnsignedAssign() default Undefined.STRING;
83+
/** GEP-15: rename the dedicated compound-assignment method for {@code &=}. */
84+
String andAssign() default Undefined.STRING;
85+
/** GEP-15: rename the dedicated compound-assignment method for {@code |=}. */
86+
String orAssign() default Undefined.STRING;
87+
/** GEP-15: rename the dedicated compound-assignment method for {@code ^=}. */
88+
String xorAssign() default Undefined.STRING;
6589
}

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
@@ -22,6 +22,7 @@
2222
import org.codehaus.groovy.ast.ClassHelper;
2323
import org.codehaus.groovy.ast.ClassNode;
2424
import org.codehaus.groovy.ast.Variable;
25+
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
2526
import org.codehaus.groovy.ast.expr.ArrayExpression;
2627
import org.codehaus.groovy.ast.expr.BinaryExpression;
2728
import org.codehaus.groovy.ast.expr.ClassExpression;
@@ -35,6 +36,7 @@
3536
import org.codehaus.groovy.ast.expr.PostfixExpression;
3637
import org.codehaus.groovy.ast.expr.PrefixExpression;
3738
import org.codehaus.groovy.ast.expr.PropertyExpression;
39+
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
3840
import org.codehaus.groovy.ast.expr.TernaryExpression;
3941
import org.codehaus.groovy.ast.expr.TupleExpression;
4042
import org.codehaus.groovy.ast.expr.VariableExpression;
@@ -205,15 +207,15 @@ public void eval(final BinaryExpression expression) {
205207
break;
206208

207209
case BITWISE_AND_EQUAL:
208-
evaluateBinaryExpressionWithAssignment("and", expression);
210+
evaluateCompoundAssign("andAssign", "and", expression);
209211
break;
210212

211213
case BITWISE_OR:
212214
evaluateBinaryExpression("or", expression);
213215
break;
214216

215217
case BITWISE_OR_EQUAL:
216-
evaluateBinaryExpressionWithAssignment("or", expression);
218+
evaluateCompoundAssign("orAssign", "or", expression);
217219
break;
218220

219221
case BITWISE_XOR:
@@ -225,31 +227,31 @@ public void eval(final BinaryExpression expression) {
225227
break;
226228

227229
case BITWISE_XOR_EQUAL:
228-
evaluateBinaryExpressionWithAssignment("xor", expression);
230+
evaluateCompoundAssign("xorAssign", "xor", expression);
229231
break;
230232

231233
case PLUS:
232234
evaluateBinaryExpression("plus", expression);
233235
break;
234236

235237
case PLUS_EQUAL:
236-
evaluateBinaryExpressionWithAssignment("plus", expression);
238+
evaluateCompoundAssign("plusAssign", "plus", expression);
237239
break;
238240

239241
case MINUS:
240242
evaluateBinaryExpression("minus", expression);
241243
break;
242244

243245
case MINUS_EQUAL:
244-
evaluateBinaryExpressionWithAssignment("minus", expression);
246+
evaluateCompoundAssign("minusAssign", "minus", expression);
245247
break;
246248

247249
case MULTIPLY:
248250
evaluateBinaryExpression("multiply", expression);
249251
break;
250252

251253
case MULTIPLY_EQUAL:
252-
evaluateBinaryExpressionWithAssignment("multiply", expression);
254+
evaluateCompoundAssign("multiplyAssign", "multiply", expression);
253255
break;
254256

255257
case DIVIDE:
@@ -259,14 +261,15 @@ public void eval(final BinaryExpression expression) {
259261
case DIVIDE_EQUAL:
260262
//SPG don't use divide since BigInteger implements directly
261263
//and we want to dispatch through DefaultGroovyMethods to get a BigDecimal result
262-
evaluateBinaryExpressionWithAssignment("div", expression);
264+
evaluateCompoundAssign("divAssign", "div", expression);
263265
break;
264266

265267
case INTDIV:
266268
evaluateBinaryExpression("intdiv", expression);
267269
break;
268270

269271
case INTDIV_EQUAL:
272+
// GEP-15 explicitly excludes \= (no intdivAssign convention)
270273
evaluateBinaryExpressionWithAssignment("intdiv", expression);
271274
break;
272275

@@ -275,23 +278,25 @@ public void eval(final BinaryExpression expression) {
275278
break;
276279

277280
case MOD_EQUAL:
278-
evaluateBinaryExpressionWithAssignment("mod", expression);
281+
// GEP-15 maps both MOD_EQUAL and REMAINDER_EQUAL to remainderAssign for consistency
282+
// with getOperationName collapse, even though current parser only emits REMAINDER_EQUAL.
283+
evaluateCompoundAssign("remainderAssign", "mod", expression);
279284
break;
280285

281286
case REMAINDER:
282287
evaluateBinaryExpression("remainder", expression);
283288
break;
284289

285290
case REMAINDER_EQUAL:
286-
evaluateBinaryExpressionWithAssignment("remainder", expression);
291+
evaluateCompoundAssign("remainderAssign", "remainder", expression);
287292
break;
288293

289294
case POWER:
290295
evaluateBinaryExpression("power", expression);
291296
break;
292297

293298
case POWER_EQUAL:
294-
evaluateBinaryExpressionWithAssignment("power", expression);
299+
evaluateCompoundAssign("powerAssign", "power", expression);
295300
break;
296301

297302
case ELVIS_EQUAL:
@@ -303,23 +308,23 @@ public void eval(final BinaryExpression expression) {
303308
break;
304309

305310
case LEFT_SHIFT_EQUAL:
306-
evaluateBinaryExpressionWithAssignment("leftShift", expression);
311+
evaluateCompoundAssign("leftShiftAssign", "leftShift", expression);
307312
break;
308313

309314
case RIGHT_SHIFT:
310315
evaluateBinaryExpression("rightShift", expression);
311316
break;
312317

313318
case RIGHT_SHIFT_EQUAL:
314-
evaluateBinaryExpressionWithAssignment("rightShift", expression);
319+
evaluateCompoundAssign("rightShiftAssign", "rightShift", expression);
315320
break;
316321

317322
case RIGHT_SHIFT_UNSIGNED:
318323
evaluateBinaryExpression("rightShiftUnsigned", expression);
319324
break;
320325

321326
case RIGHT_SHIFT_UNSIGNED_EQUAL:
322-
evaluateBinaryExpressionWithAssignment("rightShiftUnsigned", expression);
327+
evaluateCompoundAssign("rightShiftUnsignedAssign", "rightShiftUnsigned", expression);
323328
break;
324329

325330
case KEYWORD_INSTANCEOF:
@@ -755,6 +760,44 @@ protected void evaluateBinaryExpressionWithAssignment(final String method, final
755760
controller.getCompileStack().popLHS();
756761
}
757762

763+
/**
764+
* GEP-15: dynamic-mode compound-assign codegen. Routes through
765+
* {@link ScriptBytecodeAdapter#compoundAssign(Object, Object, String, String)}
766+
* which dispatches to {@code assignName} when the receiver responds to it,
767+
* and falls back to {@code baseName} otherwise. The caller stores the helper's
768+
* return value into the LHS — for the in-place branch this is a no-op store
769+
* of the receiver back to itself; for the fallback branch it is the usual
770+
* "x = x.op(y)" assignment.
771+
*/
772+
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression expression) {
773+
Expression leftExpression = expression.getLeftExpression();
774+
if (leftExpression instanceof BinaryExpression bexp
775+
&& bexp.getOperation().getType() == LEFT_SQUARE_BRACKET) {
776+
// Subscript LHS (e.g. a[i] += b) is intentionally out of scope for GEP-15;
777+
// keep the legacy getAt/putAt-based path.
778+
evaluateArrayAssignmentWithOperator(baseName, expression, bexp);
779+
return;
780+
}
781+
782+
StaticMethodCallExpression helperCall = new StaticMethodCallExpression(
783+
ClassHelper.make(ScriptBytecodeAdapter.class),
784+
"compoundAssign",
785+
new ArgumentListExpression(new Expression[]{
786+
leftExpression,
787+
expression.getRightExpression(),
788+
new ConstantExpression(assignName),
789+
new ConstantExpression(baseName)
790+
})
791+
);
792+
helperCall.setSourcePosition(expression);
793+
helperCall.visit(controller.getAcg());
794+
795+
controller.getOperandStack().dup();
796+
controller.getCompileStack().pushLHS(true);
797+
leftExpression.visit(controller.getAcg());
798+
controller.getCompileStack().popLHS();
799+
}
800+
758801
private void evaluateInstanceof(final BinaryExpression expression) {
759802
CompileStack compileStack = controller.getCompileStack();
760803
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,4 +998,34 @@ 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+
mc.invokeMethod(receiver, assignName, arg);
1015+
return receiver;
1016+
}
1017+
}
1018+
return InvokerHelper.invokeMethod(receiver, baseName, arg);
1019+
}
1020+
1021+
/**
1022+
* GEP-15: helper for {@code @OperatorRename(plusAssign="...")} expression rewrites.
1023+
* The user's renamed name is authoritative (no fallback), and the expression value of
1024+
* {@code x op= y} is the (mutated) {@code x}, not the called method's return value.
1025+
*/
1026+
public static Object invokeRenamedCompoundAssign(final Object receiver, final Object arg,
1027+
final String name) throws Throwable {
1028+
InvokerHelper.invokeMethod(receiver, name, arg);
1029+
return receiver;
1030+
}
10011031
}

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)