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
The following commit(s) were added to refs/heads/asf-site by this push:
new 98e317d compound assignment operator overloading proposal (minor
tweaks plus reference spike and existing Jira)
98e317d is described below
commit 98e317d6219a21d3e3490a154faa5c6592352b78
Author: Paul King <[email protected]>
AuthorDate: Sun Apr 12 21:06:26 2026 +1000
compound assignment operator overloading proposal (minor tweaks plus
reference spike and existing Jira)
---
site/src/site/wiki/GEP-16.adoc | 392 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 392 insertions(+)
diff --git a/site/src/site/wiki/GEP-16.adoc b/site/src/site/wiki/GEP-16.adoc
new file mode 100644
index 0000000..6ffe4fa
--- /dev/null
+++ b/site/src/site/wiki/GEP-16.adoc
@@ -0,0 +1,392 @@
+= GEP-16: `val` keyword for final declarations
+
+:icons: font
+
+.Metadata
+****
+[horizontal,options="compact"]
+*Number*:: GEP-16
+*Title*:: `val` keyword for final declarations
+*Version*:: 2
+*Type*:: Feature
+*Status*:: Draft
+*Leader*:: Paul King
+*Created*:: 2026-04-12
+*Last modification* :: 2026-04-12
+****
+
+== Abstract
+
+Groovy already supports `var` as a declaration keyword (alias for `def`,
+introduced for Java 10 compatibility). This GEP proposes adding `val`
+as a declaration keyword that combines `final` with type inference --
+equivalent to `final def`, `final var`, or just `final`.
+
+=== Motivation
+
+Declaring immutable local variables in Groovy currently requires:
+
+[source,groovy]
+----
+final name = 'Groovy'
+final def name = 'Groovy'
+final var name = 'Groovy'
+final String name = 'Groovy'
+----
+
+All of these work, but none are as concise or intention-revealing as:
+
+[source,groovy]
+----
+val name = 'Groovy'
+----
+
+The `val` keyword is familiar from Kotlin and Scala, where it is the
+default and idiomatic way to declare immutable bindings. Adding `val`
+to Groovy provides:
+
+* A concise, readable way to declare final variables with inferred types
+* Familiar syntax for developers coming from Kotlin or Scala
+* A natural complement to the existing `var` keyword
+* Encouragement of immutability as a default practice
+* Improved parity for Gradle build script authors: Kotlin Gradle build scripts
+ (`build.gradle.kts`) already use `val` for local variables. Supporting `val`
+ in Groovy Gradle build scripts (`build.gradle`) makes it easier for teams
+ that mix both languages, and reduces friction when translating between them.
+
+=== Requirements
+
+* `val` declares a `final` variable with an inferred type (like `final def`)
+* `val` must still be usable as a variable name, method name, map key,
+ and property name -- mirroring `var` behavior
+* `val` must NOT be usable for method return types or type declarations
+ -- mirroring `var` restrictions
+* `final val` should be accepted (redundant but harmless)
+
+==== Non-goals
+
+* Changing the behavior of `def` or `var`
+* Making `val` the default declaration style
+* Adding immutability enforcement beyond the `final` modifier (deep
immutability
+ is the domain of `@Immutable` and related transforms)
+
+=== Design
+
+==== Keyword classification
+
+`val` is a _contextual keyword_ in the same category as `var`.
+It is a reserved token at the lexer level but is included in the
+parser's `identifier` rule, allowing it to be used as a name in
+non-declaration contexts.
+
+Groovy's keywords fall into two categories with respect to identifier usage:
+
+[cols="2,3,3",options="header"]
+|===
+| Category | Examples | In `identifier` rule?
+
+| Declaration + method keywords
+| `def`
+| No -- `def` is used for method return types, so allowing it as an identifier
+ would create unresolvable ambiguity
+
+| Declaration-only keywords
+| `var`, `val`
+| Yes -- these are forbidden on method return types, so the contexts
+ don't overlap and identifier use is unambiguous
+|===
+
+This is why `var var = 4` works but `def def = 2` does not.
+`val` follows the `var` pattern: `val val = 5` will work.
+
+==== Semantics
+
+`val` is equivalent to `final def`, `final var`, or just `final`:
+
+[source,groovy]
+----
+val x = 42 // final, type inferred as int
+val s = 'hello' // final, type inferred as String
+val list = [1,2,3] // final, type inferred as ArrayList
+----
+
+The variable cannot be reassigned:
+
+[source,groovy]
+----
+val x = 1
+x = 2 // compile error: cannot assign to final variable
+----
+
+But the object itself can be mutated (shallow finality):
+
+[source,groovy]
+----
+val list = [1, 2, 3]
+list << 4 // OK: mutates the list, doesn't reassign
+----
+
+==== Where `val` is allowed
+
+* Local variable declarations: `val x = 1`
+* Field declarations: `class C { val x = 1 }`
+* For-loop index variables: `for (val i in 0..10) {}`
+* Closure/lambda parameters: `{ val x -> x * 2 }`
+* Package names: `package foo.val.bar`
+
+==== Where `val` is NOT allowed
+
+* Method return types: `val someMethod() {}` -- parse error
+* Type declarations: `class val {}`, `interface val {}`, `@interface val {}`
-- parse error
+* Compact constructor declarations (records)
+
+These restrictions mirror `var` exactly. Note that `val` can still be used
+as a method _name_ (`def val() {}`).
+
+==== Identifier contexts that still work
+
+[source,groovy]
+----
+// As a variable name (just like var):
+val val = 5
+assert val == 5
+
+// As a map key:
+def m = [val: 42]
+
+// As a method name:
+def val() { 'hello' }
+
+// In GString interpolation:
+assert "$val" == '5'
+
+// Combined with other contextual keywords:
+var var = 4
+val val = 5
+assert "$var$val" == '45'
+----
+
+==== Interaction with `final`
+
+`final val` is redundant but accepted, just as `final def` and `final var` are:
+
+[source,groovy]
+----
+final val x = 1 // OK, same as val x = 1
+----
+
+==== Interaction with Java classes named `val`
+
+Unlike `var` (which Java prohibits as a class name since JDK 10), Java
+allows classes named `val`. This creates a potential conflict when such a
+class is used in Groovy code. The following table summarises the behavior:
+
+[cols="3,1,3",options="header"]
+|===
+| Scenario | Works? | Notes
+
+| `new val()`
+| Yes
+| `val` parsed as class name in `new` expression
+
+| `import val as MyVal` then `MyVal x = new MyVal()`
+| Yes
+| Import alias fully resolves the ambiguity
+
+| `val x = new val()`
+| Yes*
+| `val` on LHS is the _keyword_ (final + inferred type), not the class.
+ Type is inferred from RHS. Reassignment is forbidden.
+
+| `val bar()` as method return type
+| No
+| Keyword takes precedence -- use FQN or import alias
+
+| `val x` as explicit type in declaration
+| No
+| Keyword takes precedence -- use FQN or import alias
+
+| Java-defined `@val` annotation
+| Yes
+| Annotations are resolved by class name, no conflict
+|===
+
+*Workaround:* If you need to use a Java class named `val` as an explicit
+type or method return type, use its fully-qualified name or an import alias:
+
+[source,groovy]
+----
+import val as Val
+
+Val x = new Val() // explicit type via alias
+
+class Foo {
+ Val bar() { new Val() } // return type via alias
+}
+----
+
+=== Implementation
+
+NOTE: Early in parsing, `val` is simply converted to `final` with a dynamic
+type. From that point on, the compiler never sees `val` again -- it follows
+the same code paths as any `final` declaration, including type inference
+in `@CompileStatic` mode.
+
+==== Spike implementation
+
+A spike of the proposed functionality was implemented to validate the
+design and explore edge cases.
+
+*Production code* (~15 lines across 5 files):
+
+[cols="2,5",options="header"]
+|===
+| File | Change
+
+| `GroovyLexer.g4`
+| Add `VAL : 'val';` token (1 line)
+
+| `GroovyParser.g4`
+| Add `VAL` to `modifier`, `variableModifier`, `indexVariable`,
+ `identifier`, and `keywords` rules -- alongside `VAR` in each (5 lines)
+
+| `ModifierNode.java`
+| Add `VAL` to `MODIFIER_OPCODE_MAP` mapping to `ACC_FINAL`,
+ add `isVal()` method, include `VAL` in `isDef()` (3 lines + import)
+
+| `AstBuilder.java`
+| Add `VAL` to type name, method return type, and compact constructor
+ restrictions -- alongside `VAR` in each (3 lines)
+
+| `SmartDocumentFilter.java`
+| Add `VAL` to syntax highlighting keyword list (1 line + import)
+|===
+
+No changes were needed in `ModifierManager.java` -- `val` is handled
+automatically via the opcode map (`ACC_FINAL`), and `isDef()` returning
+`true` ensures the dynamic type is applied.
+
+*Test code* (4 new files + 2 existing test fixes):
+
+[cols="2,5",options="header"]
+|===
+| File | Purpose
+
+| `src/test-resources/core/Val_01x.groovy`
+| Valid usage: basic `val`, closure param, `val val`, map key,
+ type inference, shallow finality, `final val`, for loop, GString
+
+| `src/test-resources/fail/Val_01x.groovy`
+| `class val {}` -- type declaration forbidden
+
+| `src/test-resources/fail/Val_02x.groovy`
+| `val someMethod() {}` -- method return type forbidden
+
+| `src/test-resources/fail/Val_03x.groovy`
+| `val x = 1; x = 2` -- reassignment forbidden
+
+| `GroovyParserTest.groovy`
+| Wire up `Val_01x` core test
+
+| `SyntaxErrorTest.groovy`
+| Wire up `Val_01x`/`02x`/`03x` fail tests
+|===
+
+The spike confirmed that all edge cases (identifier usage, map keys,
+package names, Java class interop, import aliases, cast expressions)
+work correctly with no additional code beyond the changes listed above.
+
+=== Breaking behavior
+
+The main concern is existing code that uses `val` as a variable name.
+Since `val` will be in the `identifier` rule (like `var`), most usage
+continues to work:
+
+* `def val = 1` -- still works (declaration with `def`, identifier `val`)
+* `val = something` -- still works (assignment to variable named `val`)
+* `[val: 42]` -- still works (map key)
+* `obj.val` -- still works (property access)
+
+The disambiguation works the same way as `var`: the parser uses
+`SemanticPredicates.isInvalidLocalVariableDeclaration()` to check
+what follows. `val = 42` is an assignment; `val x = 42` is a declaration.
+
+==== Field named `val` (or `var`) before a method declaration
+
+A field declared as `def val` (or `def var`) immediately before a method
+declaration is misinterpreted by the parser as modifiers on the method.
+This is a pre-existing issue with `var` that equally applies to `val`:
+
+[source,groovy]
+----
+class Foo {
+ def val // intended as field, but parsed as
+ void doSomething() {} // modifiers for this method -> error
+}
+----
+
+*Workarounds:*
+
+[source,groovy]
+----
+class Foo {
+ def val = null // add initializer
+ def val; // add semicolon
+ String val // use explicit type instead of def
+ void doSomething() {}
+}
+----
+
+==== `val` as a cast expression
+
+A variable or field named `val` used with the `as` cast operator is
+misinterpreted. The parser sees `val` as the keyword followed by `as`:
+
+[source,groovy]
+----
+def val = 42
+val as String // error: unable to resolve class as
+----
+
+*Workarounds:*
+
+[source,groovy]
+----
+this.val as String // qualify with this
+(val) as String // parenthesise
+"$val" as String // alternative approach
+----
+
+NOTE: This also applies to `var`. It is a pre-existing issue.
+
+==== Java class named `val`
+
+A Java class named `val` cannot be used directly as a declared type or
+method return type in Groovy -- the keyword takes precedence in those
+positions. The workaround is to use a fully-qualified name or import alias.
+This matches the behavior `var` would have if Java permitted `class var`.
+
+== References and useful links
+
+* https://kotlinlang.org/docs/basic-syntax.html#variables[Kotlin val/var]
+* https://docs.scala-lang.org/tour/basics.html[Scala val/var]
+* Groovy `var` implementation: commit `f4d96d8872` (2018)
+
+=== Reference implementation
+
+https://github.com/apache/groovy/tree/valSpike
+
+=== JIRA issues
+
+* https://issues.apache.org/jira/browse/GROOVY-9308[GROOVY-9308: Support val
for final declarations]
+ -- originally raised in 2019. At that time, the `val`/`var` distinction
+ was less widely understood. Since then, Kotlin adoption has grown
+ significantly and `val`/`var` semantics are now familiar to a broad
+ audience, making the case for this feature stronger.
+
+== Update history
+
+1 (2026-04-12) Initial draft
+2 (2026-04-12) Added Java interop edge cases, spike implementation summary
+3 (2026-04-12) Added field naming edge case, Gradle motivation
+4 (2026-04-12) Added cast expression edge case, split code stats, linked spike
branch and JIRA