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*&#160;:: 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

Reply via email to