This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch asf-site in repository https://gitbox.apache.org/repos/asf/groovy-website.git
commit e0828fca609ba2de79856aec2079f15b32a81a82 Author: Paul King <[email protected]> AuthorDate: Sun Apr 12 16:18:54 2026 +1000 compound assignment operator overloading proposal --- site/src/site/wiki/GEP-15.adoc | 195 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/site/src/site/wiki/GEP-15.adoc b/site/src/site/wiki/GEP-15.adoc new file mode 100644 index 0000000..9bc2742 --- /dev/null +++ b/site/src/site/wiki/GEP-15.adoc @@ -0,0 +1,195 @@ += GEP-15: Compound assignment operator overloading + +:icons: font + +.Metadata +**** +[horizontal,options="compact"] +*Number*:: GEP-15 +*Title*:: Compound assignment operator overloading +*Version*:: 1 +*Type*:: Feature +*Status*:: Draft +*Leader*:: Paul King +*Created*:: 2026-04-12 +*Last modification* :: 2026-04-12 +**** + +== Abstract + +Groovy supports operator overloading: `\+` maps to `plus()`, `-` maps to `minus()`, +`<<` maps to `leftShift()`, and so on. However, compound assignment operators +(`pass:[+=]`, `-=`, `pass:[<<=]`, etc.) are always desugared to `x = x.op(y)` -- there is +no way to override `+=` independently of `pass:[+]`. This GEP proposes adding +support for dedicated compound assignment methods such as `plusAssign`, +`minusAssign`, `leftShiftAssign`, etc. + +=== Motivation + +The current desugaring of `x += y` to `x = x.plus(y)` creates a new object +and reassigns the variable. This has several drawbacks: + +* **Mutable data structures** are forced into a create-and-reassign pattern + when in-place mutation is the intended semantics. For example, a mutable + list's `+=` creates a new list rather than appending in place. +* **Final fields and variables** cannot use compound assignment at all, + even when the underlying object is mutable and supports in-place mutation. +* **Intent** is unclear: the class author cannot distinguish between + `x + y` (produce a new value) and `x += y` (mutate in place). + +Languages like Kotlin and Scala already support this distinction. +Kotlin maps `\+=` to `plusAssign()` when available, falling back to +`plus()` with reassignment. Scala allows mutable collections to define +`+=` directly. + +=== Requirements + +* Support dedicated compound assignment methods (`plusAssign`, `minusAssign`, etc.) + that are called in preference to the current `plus` + reassign pattern. +* Maintain full backward compatibility: existing code that uses `+=` with + `plus()` must continue to work identically when no `plusAssign` method exists. +* Support `+=` on `final` fields/variables when `plusAssign` is available. +* Work correctly in both `@CompileStatic` and dynamic Groovy. + +==== Non-goals + +* Changing the behavior of `++` and `--` operators (these use `next()`/`previous()` + and are conceptually different). +* Changing subscript compound assignment (`a[i] += b`), which uses the existing + `getAt`/`putAt` pattern. + +=== Operator method name mapping + +[cols="1,2,2",options="header"] +|=== +| Operator | Assign method | Fallback method + +| `+=` | `plusAssign` | `plus` +| `-=` | `minusAssign` | `minus` +| `*=` | `multiplyAssign` | `multiply` +| `/=` | `divAssign` | `div` +| `%=` | `modAssign` | `mod` +| `%%=` | `remainderAssign` | `remainder` +| `**=` | `powerAssign` | `power` +| `<\<=` | `leftShiftAssign` | `leftShift` +| `>>=` | `rightShiftAssign` | `rightShift` +| `>>>=` | `rightShiftUnsignedAssign` | `rightShiftUnsigned` +| `&=` | `andAssign` | `and` +| `\|=` | `orAssign` | `or` +| `^=` | `xorAssign` | `xor` +| `//=` | `intdivAssign` | `intdiv` +|=== + +=== Resolution algorithm + +When the compiler encounters `x += y`: + +1. Look for a method `plusAssign(y)` on the type of `x`. +2. If found, call `x.plusAssign(y)` directly. No reassignment occurs. + This works even when `x` is `final`. +3. If not found, fall back to the current behavior: `x = x.plus(y)`. + This requires `x` to be reassignable. +4. If `x` is `final` and no `plusAssign` exists, report a compile error + (in `@CompileStatic` mode). + +When both `plusAssign` and `plus` exist on the same type, `plusAssign` +takes precedence. This is the pragmatic choice for Groovy since it lacks +Kotlin's `val`/`var` distinction. If a class author defined `plusAssign`, +they intended it to be used for `+=`. + +=== Design considerations + +* **Primitives are unaffected.** `int x = 0; x += 1` continues to use + the existing fast-path for primitive arithmetic. The `plusAssign` lookup + only applies when the base operator would resolve to a method call. +* **Expression value.** When `plusAssign` is called, the expression value + of `x += y` is `x` (the mutated object), not the return value of + `plusAssign`. This differs from the current behavior where the expression + value is the result of `x.plus(y)`. +* **Extension methods.** `plusAssign` is discoverable as an extension method + (DGM or category), not just as an instance method. +* **`@OperatorRename` support.** The existing `@OperatorRename` annotation + will be extended to support renaming the assign variants, e.g. + `@OperatorRename(plusAssign="addInPlace")`. + +=== Examples + +A mutable accumulator: + +[source,groovy] +---- +class Accumulator { + int total = 0 + void plusAssign(int n) { total += n } + Accumulator plus(int n) { new Accumulator(total: total + n) } +} + +def acc = new Accumulator() +acc += 5 // calls acc.plusAssign(5), mutates in place +assert acc.total == 5 + +def acc2 = acc + 3 // calls acc.plus(3), returns new Accumulator +assert acc2.total == 8 +assert acc.total == 5 +---- + +A final field with in-place mutation: + +[source,groovy] +---- +@CompileStatic +class EventBus { + final List<String> listeners = [] + // List has leftShiftAssign via extension method or subclass +} + +def bus = new EventBus() +bus.listeners <<= "listener1" // calls listeners.leftShiftAssign("listener1") +---- + +=== Static compilation (`@CompileStatic`) + +In `@CompileStatic` mode, the type checker resolves `plusAssign` at +compile time using `findMethod()`. If found, it records the target method +on the expression via node metadata and the bytecode generator emits a +direct method call -- no dup, no store-back. + +If not found, the existing desugaring to `x = x.plus(y)` applies. + +=== Dynamic Groovy + +In dynamic Groovy, the runtime checks for `plusAssign` via the Meta-Object +Protocol (MOP). If `respondsTo(target, "plusAssign", arg)` succeeds, it is +called. Otherwise, the fallback to `plus` + reassignment applies. + +This requires a new runtime helper method (e.g., in `ScriptBytecodeAdapter`) +that encapsulates the try-assign-then-fallback logic. + +=== Breaking behavior + +1. **Existing `plusAssign` methods.** If a class already defines a method + literally named `plusAssign`, `+=` will now call it instead of desugaring + to `plus` + reassign. This is unlikely in practice but must be documented + in release notes. +2. **Expression value change.** `val = (x += y)` -- with `plusAssign`, the + captured value is `x` (the mutated object), not the result of `plus`. + This only affects code that uses compound assignment as an expression. +3. **Final fields.** In `@CompileStatic`, `final` fields with a type that + has `plusAssign` can now use `+=`. Previously this was always an error. + +== References and useful links + +* https://kotlinlang.org/docs/operator-overloading.html#augmented-assignments[Kotlin augmented assignments] +* https://docs.scala-lang.org/overviews/collections-2.13/overview.html[Scala mutable collection operators] + +=== Reference implementation + +_TBD_ + +=== JIRA issues + +_TBD_ + +== Update history + +1 (2026-04-12) Initial draft
