Skip to content

GROOVY-11970: Provide support for compound assignment operator overlo…#2499

Open
paulk-asert wants to merge 1 commit intoapache:masterfrom
paulk-asert:groovy11970
Open

GROOVY-11970: Provide support for compound assignment operator overlo…#2499
paulk-asert wants to merge 1 commit intoapache:masterfrom
paulk-asert:groovy11970

Conversation

@paulk-asert
Copy link
Copy Markdown
Contributor

…ading (GEP-15)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GEP-15 support for dedicated compound-assignment operator overloads (e.g. plusAssign) across static type checking, bytecode generation, @OperatorRename, documentation, and tests.

Changes:

  • Introduces *Assign method resolution in the static type checker and propagates the resolved target to static codegen.
  • Adds dynamic-mode compound-assign dispatch via ScriptBytecodeAdapter.compoundAssign and updates bytecode generation to use it.
  • Documents the feature in the operator spec and adds a focused test suite for compound assignment behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/test/groovy/groovy/operator/CompoundAssignmentTest.groovy New tests covering static/dynamic *Assign selection, fallback behavior, final handling (static), setter skipping (static), and @OperatorRename scenarios.
src/spec/doc/core-operators.adoc Documents compound assignment operator overloading and method mapping table.
src/main/java/org/codehaus/groovy/transform/stc/StaticTypesMarker.java Adds metadata key to carry resolved *Assign target from STC into codegen.
src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java Resolves *Assign methods for compound-assign tokens and records the chosen target; skips setter-info when in-place mutation applies.
src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java Adds token→*Assign method-name mapping helper.
src/main/java/org/codehaus/groovy/transform/OperatorRenameASTTransformation.java Extends @OperatorRename to support *Assign renames with precedence over base operator renames.
src/main/java/org/codehaus/groovy/syntax/Types.java Treats REMAINDER_EQUAL as an assignment operator token.
src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java Adds dynamic dispatcher helper for *Assign vs fallback operator call.
src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesBinaryExpressionMultiTypeDispatcher.java Emits in-place *Assign calls during static compilation when STC resolved a target; maintains legacy property-set fast-path.
src/main/java/org/codehaus/groovy/classgen/asm/BinaryExpressionMultiTypeDispatcher.java Overrides compound-assign emission to retain primitive/array fast paths.
src/main/java/org/codehaus/groovy/classgen/asm/BinaryExpressionHelper.java Routes compound-assign tokens through new evaluateCompoundAssign helper-based path.
src/main/java/org/codehaus/groovy/classgen/Verifier.java Skips final-variable reassignment checks for compound assigns marked as in-place via STC metadata.
src/main/java/groovy/transform/OperatorRename.java Adds annotation members for *Assign renames.

Comment on lines +772 to +790
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression expression) {
Expression leftExpression = expression.getLeftExpression();
if (leftExpression instanceof BinaryExpression bexp
&& bexp.getOperation().getType() == LEFT_SQUARE_BRACKET) {
// Subscript LHS (e.g. a[i] += b) is intentionally out of scope for GEP-15;
// keep the legacy getAt/putAt-based path.
evaluateArrayAssignmentWithOperator(baseName, expression, bexp);
return;
}

StaticMethodCallExpression helperCall = new StaticMethodCallExpression(
ClassHelper.make(ScriptBytecodeAdapter.class),
"compoundAssign",
new ArgumentListExpression(new Expression[]{
leftExpression,
expression.getRightExpression(),
new ConstantExpression(assignName),
new ConstantExpression(baseName)
})
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluateCompoundAssign doesn't currently account for null-safe compound assignments (expression.isSafe()), because ScriptBytecodeAdapter.compoundAssign will fall back to invoking the base operator even when the receiver is null. This breaks safe-navigation semantics (the operation should short-circuit without invoking plus/etc when safe and the LHS resolves to null). Consider either handling expression.isSafe() here by keeping the legacy safe path, or extending the helper to treat safe && receiver==null as a short-circuit.

Copilot uses AI. Check for mistakes.
Comment on lines +527 to +549
/**
* GEP-15: returns the dedicated compound-assignment method name for a compound-assign
* operator token (e.g. {@code PLUS_EQUAL} -> {@code "plusAssign"}), or {@code null} if
* the token is not one of the twelve operators in scope. {@code INTDIV_EQUAL},
* {@code MOD_EQUAL}, {@code ELVIS_EQUAL}, {@code LOGICAL_OR_EQUAL} and
* {@code LOGICAL_AND_EQUAL} are intentionally excluded.
*/
public static String getAssignOperationName(final int op) {
return switch (op) {
case PLUS_EQUAL -> "plusAssign";
case MINUS_EQUAL -> "minusAssign";
case MULTIPLY_EQUAL -> "multiplyAssign";
case DIVIDE_EQUAL -> "divAssign";
case REMAINDER_EQUAL -> "remainderAssign";
case POWER_EQUAL -> "powerAssign";
case LEFT_SHIFT_EQUAL -> "leftShiftAssign";
case RIGHT_SHIFT_EQUAL -> "rightShiftAssign";
case RIGHT_SHIFT_UNSIGNED_EQUAL -> "rightShiftUnsignedAssign";
case BITWISE_AND_EQUAL -> "andAssign";
case BITWISE_OR_EQUAL -> "orAssign";
case BITWISE_XOR_EQUAL -> "xorAssign";
default -> null;
};
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAssignOperationName only maps REMAINDER_EQUAL, but Groovy still defines/uses MOD_EQUAL (%=) as a distinct token in some parser paths. Since codegen now routes MOD_EQUAL through remainderAssign (see BinaryExpressionHelper), the STC helper should likely map MOD_EQUAL to "remainderAssign" as well; otherwise statically compiled %= may miss remainderAssign depending on which token the frontend produces.

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +185
static String getAssignOperationName(final int op) {
return switch (op) {
case PLUS_EQUAL -> "plusAssign";
case MINUS_EQUAL -> "minusAssign";
case MULTIPLY_EQUAL -> "multiplyAssign";
case DIVIDE_EQUAL -> "divAssign";
case REMAINDER_EQUAL -> "remainderAssign";
case POWER_EQUAL -> "powerAssign";
case LEFT_SHIFT_EQUAL -> "leftShiftAssign";
case RIGHT_SHIFT_EQUAL -> "rightShiftAssign";
case RIGHT_SHIFT_UNSIGNED_EQUAL -> "rightShiftUnsignedAssign";
case BITWISE_AND_EQUAL -> "andAssign";
case BITWISE_OR_EQUAL -> "orAssign";
case BITWISE_XOR_EQUAL -> "xorAssign";
default -> null;
};
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same token-mapping issue exists here: getAssignOperationName doesn't include MOD_EQUAL, so @OperatorRename(… plusAssign=…) precedence logic may not trigger for %= depending on which token (MOD_EQUAL vs REMAINDER_EQUAL) the parser emits. Consider mapping both tokens to remainderAssign for consistency with codegen and the user-facing operator table.

Copilot uses AI. Check for mistakes.
Comment thread src/spec/doc/core-operators.adoc Outdated
Comment thread src/test/groovy/groovy/operator/CompoundAssignmentTest.groovy
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 84.93151% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.1516%. Comparing base (5a41b8f) to head (a49310e).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
...ovy/transform/OperatorRenameASTTransformation.java 63.1579% 12 Missing and 2 partials ⚠️
...taticTypesBinaryExpressionMultiTypeDispatcher.java 88.8889% 3 Missing ⚠️
...us/groovy/classgen/asm/BinaryExpressionHelper.java 92.8571% 1 Missing and 1 partial ⚠️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 91.3044% 1 Missing and 1 partial ⚠️
...codehaus/groovy/runtime/ScriptBytecodeAdapter.java 87.5000% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2499        +/-   ##
==================================================
+ Coverage     67.1374%   67.1516%   +0.0142%     
- Complexity      31622      31659        +37     
==================================================
  Files            1451       1451                
  Lines          122565     122694       +129     
  Branches        22007      22020        +13     
==================================================
+ Hits            82287      82391       +104     
- Misses          33199      33222        +23     
- Partials         7079       7081         +2     
Files with missing lines Coverage Δ
...in/java/org/codehaus/groovy/classgen/Verifier.java 90.0938% <100.0000%> (+0.0207%) ⬆️
...ssgen/asm/BinaryExpressionMultiTypeDispatcher.java 86.9347% <100.0000%> (-1.7833%) ⬇️
...rc/main/java/org/codehaus/groovy/syntax/Types.java 74.0458% <ø> (ø)
...roovy/transform/stc/StaticTypeCheckingSupport.java 81.7626% <100.0000%> (+0.2255%) ⬆️
...dehaus/groovy/transform/stc/StaticTypesMarker.java 100.0000% <100.0000%> (ø)
...codehaus/groovy/runtime/ScriptBytecodeAdapter.java 65.8768% <87.5000%> (+0.4178%) ⬆️
...us/groovy/classgen/asm/BinaryExpressionHelper.java 88.2784% <92.8571%> (-1.3638%) ⬇️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 87.9145% <91.3044%> (+0.0215%) ⬆️
...taticTypesBinaryExpressionMultiTypeDispatcher.java 86.9565% <88.8889%> (+0.1918%) ⬆️
...ovy/transform/OperatorRenameASTTransformation.java 64.3564% <63.1579%> (+3.4189%) ⬆️

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

return receiver;
}
}
return InvokerHelper.invokeMethod(receiver, baseName, arg);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCB already has methods for invocation, why not use those?

public static Object invokeRenamedCompoundAssign(final Object receiver, final Object arg,
final String name) throws Throwable {
InvokerHelper.invokeMethod(receiver, name, arg);
return receiver;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use a normal method call for this then? I don't see why we need an extra method in SCB for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants