This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch valSpike
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 478d0ee71f486f2e17a02476b06330e9c953de27
Author: Paul King <[email protected]>
AuthorDate: Sun Apr 12 20:09:01 2026 +1000

    GEP-16: `val` keyword for final declarations
---
 src/antlr/GroovyLexer.g4                           |  1 +
 src/antlr/GroovyParser.g4                          |  6 ++-
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 14 +++---
 .../java/org/codehaus/groovy/ast/ModifierNode.java |  8 ++-
 src/test-resources/core/Val_01x.groovy             | 57 ++++++++++++++++++++++
 src/test-resources/fail/Val_01x.groovy             | 20 ++++++++
 src/test-resources/fail/Val_02x.groovy             | 20 ++++++++
 src/test-resources/fail/Val_03x.groovy             | 21 ++++++++
 src/test/groovy/bugs/Groovy5358.groovy             | 18 +++----
 .../groovy/groovy/transform/stc/LambdaTest.groovy  |  2 +-
 .../groovy/parser/antlr4/GroovyParserTest.groovy   |  5 ++
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  7 +++
 .../console/ui/text/SmartDocumentFilter.java       |  3 +-
 13 files changed, 162 insertions(+), 20 deletions(-)

diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 79ddf78dbd..dcb9a8b2ff 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -479,6 +479,7 @@ THROW         : 'throw';
 THROWS        : 'throws';
 TRANSIENT     : 'transient';
 TRY           : 'try';
+VAL           : 'val';
 VAR           : 'var';
 VOID          : 'void';
 VOLATILE      : 'volatile';
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 550852286a..9da657d752 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -133,6 +133,7 @@ modifier
           |   TRANSIENT
           |   VOLATILE
           |   DEF
+          |   VAL
           |   VAR
           )
     ;
@@ -174,6 +175,7 @@ variableModifier
     :   annotation
     |   m=( FINAL
           | DEF
+          | VAL
           | VAR
           // Groovy supports declaring local variables as instance/class 
fields,
           // e.g. import groovy.transform.*; @Field static List awe = [1, 2, 3]
@@ -698,7 +700,7 @@ enhancedForControl
     ;
 
 indexVariable
-    :   (BuiltInPrimitiveType | DEF | VAR)? identifier
+    :   (BuiltInPrimitiveType | DEF | VAL | VAR)? identifier
     ;
 
 originalForControl
@@ -1234,6 +1236,7 @@ identifier
     |   RECORD
     |   SEALED
     |   TRAIT
+    |   VAL
     |   VAR
     |   YIELD
     ;
@@ -1289,6 +1292,7 @@ keywords
     |   TRAIT
     |   THREADSAFE
     |   TRY
+    |   VAL
     |   VAR
     |   VOLATILE
     |   WHILE
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java 
b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 07331823f8..bd79a8966d 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -1122,8 +1122,8 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
     public ClassNode visitClassDeclaration(final ClassDeclarationContext ctx) {
         String packageName = 
Optional.ofNullable(this.moduleNode.getPackageName()).orElse("");
         String className = this.visitIdentifier(ctx.identifier());
-        if ("var".equals(className)) {
-            throw createParsingFailedException("var cannot be used for type 
declarations", ctx.identifier());
+        if ("var".equals(className) || "val".equals(className)) {
+            throw createParsingFailedException(className + " cannot be used 
for type declarations", ctx.identifier());
         }
 
         boolean isAnnotation = asBoolean(ctx.AT());
@@ -1644,8 +1644,8 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
             throw createParsingFailedException("Only record can have compact 
constructor", ctx);
         }
 
-        if (new ModifierManager(this, 
ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS)).containsAny(VAR))
 {
-            throw createParsingFailedException("var cannot be used for compact 
constructor declaration", ctx);
+        if (new ModifierManager(this, 
ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS)).containsAny(VAL,
 VAR)) {
+            throw createParsingFailedException("val/var cannot be used for 
compact constructor declaration", ctx);
         }
 
         String methodName = this.visitMethodName(ctx.methodName());
@@ -1682,8 +1682,8 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
     public MethodNode visitMethodDeclaration(final MethodDeclarationContext 
ctx) {
         ModifierManager modifierManager = createModifierManager(ctx);
 
-        if (modifierManager.containsAny(VAR)) {
-            throw createParsingFailedException("var cannot be used for method 
declarations", ctx);
+        if (modifierManager.containsAny(VAL, VAR)) {
+            throw createParsingFailedException("val/var cannot be used for 
method return types", ctx);
         }
 
         String methodName = this.visitMethodName(ctx.methodName());
@@ -4492,7 +4492,7 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
             return true;
         }
 
-        if (hasReturnType && (modifierManager.containsAny(DEF, VAR))) {
+        if (hasReturnType && (modifierManager.containsAny(DEF, VAL, VAR))) {
             return true;
         }
 
diff --git a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java 
b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
index e76e1b4849..079364c4ab 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
@@ -38,6 +38,7 @@ import static 
org.apache.groovy.parser.antlr4.GroovyParser.STATIC;
 import static org.apache.groovy.parser.antlr4.GroovyParser.STRICTFP;
 import static org.apache.groovy.parser.antlr4.GroovyParser.SYNCHRONIZED;
 import static org.apache.groovy.parser.antlr4.GroovyParser.TRANSIENT;
+import static org.apache.groovy.parser.antlr4.GroovyParser.VAL;
 import static org.apache.groovy.parser.antlr4.GroovyParser.VAR;
 import static org.apache.groovy.parser.antlr4.GroovyParser.VOLATILE;
 import static org.codehaus.groovy.runtime.DefaultGroovyMethods.asBoolean;
@@ -56,6 +57,7 @@ public class ModifierNode extends ASTNode {
     public static final Map<Integer, Integer> MODIFIER_OPCODE_MAP = Maps.of(
             ANNOTATION_TYPE, 0,
             DEF, 0,
+            VAL, Opcodes.ACC_FINAL,
             VAR, 0,
 
             NATIVE, Opcodes.ACC_NATIVE,
@@ -129,7 +131,11 @@ public class ModifierNode extends ASTNode {
     }
 
     public boolean isDef() {
-        return Objects.equals(DEF, this.type) || Objects.equals(VAR, 
this.type);
+        return Objects.equals(DEF, this.type) || Objects.equals(VAL, 
this.type) || Objects.equals(VAR, this.type);
+    }
+
+    public boolean isVal() {
+        return Objects.equals(VAL, this.type);
     }
 
     public Integer getType() {
diff --git a/src/test-resources/core/Val_01x.groovy 
b/src/test-resources/core/Val_01x.groovy
new file mode 100644
index 0000000000..e9444001cc
--- /dev/null
+++ b/src/test-resources/core/Val_01x.groovy
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+// basic val declaration
+val name = "Groovy"
+assert "Groovy" == name
+
+// val as closure parameter name
+[1, 2, 3].each { val ->
+    assert 0 < val && val < 4
+}
+
+// val as variable name (contextual keyword)
+val val = "val variable name"
+assert "val variable name" == val
+
+// val in map key
+def m = [val: 42]
+assert m.val == 42
+
+// val with different types
+val x = 42
+assert x == 42
+val s = "hello"
+assert s.class == String
+
+// shallow finality - mutation OK
+val list = [1, 2, 3]
+list << 4
+assert list == [1, 2, 3, 4]
+
+// final val is redundant but works
+final val y = 10
+assert y == 10
+
+// for loop with val
+for (val i in [1, 2, 3]) { assert i > 0 }
+
+// GString interpolation
+val g = 99
+assert "$g" == "99"
diff --git a/src/test-resources/fail/Val_01x.groovy 
b/src/test-resources/fail/Val_01x.groovy
new file mode 100644
index 0000000000..0748f66b32
--- /dev/null
+++ b/src/test-resources/fail/Val_01x.groovy
@@ -0,0 +1,20 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+class val {}
diff --git a/src/test-resources/fail/Val_02x.groovy 
b/src/test-resources/fail/Val_02x.groovy
new file mode 100644
index 0000000000..ed6e519f7d
--- /dev/null
+++ b/src/test-resources/fail/Val_02x.groovy
@@ -0,0 +1,20 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+val someMethod() {}
diff --git a/src/test-resources/fail/Val_03x.groovy 
b/src/test-resources/fail/Val_03x.groovy
new file mode 100644
index 0000000000..9751976344
--- /dev/null
+++ b/src/test-resources/fail/Val_03x.groovy
@@ -0,0 +1,21 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+val x = 1
+x = 2
diff --git a/src/test/groovy/bugs/Groovy5358.groovy 
b/src/test/groovy/bugs/Groovy5358.groovy
index d6d6f63110..9c7fcec5e8 100644
--- a/src/test/groovy/bugs/Groovy5358.groovy
+++ b/src/test/groovy/bugs/Groovy5358.groovy
@@ -71,25 +71,25 @@ final class Groovy5358 {
     void testSetPropertyOverrides() {
         assertScript '''
             class FooWorksAsMap {
-                def val
+                def stored
                 void setProperty(String name, value) {
-                    val = "OK:FooWorksAsMap.$value"
+                    stored = "OK:FooWorksAsMap.$value"
                 }
             }
             class BarWorksAsMap {
-                def val
+                def stored
             }
             @Category(BarWorksAsMap) class C {
                 void setProperty(String name, value) {
-                    setVal("OK:BarWorksAsMap.$value")
+                    setStored("OK:BarWorksAsMap.$value")
                 }
             }
             BarWorksAsMap.mixin C
             class BazWorksAsMap {
-                def val
+                def stored
             }
             BazWorksAsMap.metaClass.setProperty = { String name, value ->
-                    setVal("OK:BazWorksAsMap.$value")
+                    setStored("OK:BazWorksAsMap.$value")
             }
 
             def objects = [
@@ -99,10 +99,10 @@ final class Groovy5358 {
                 [:]
             ]
             for (def obj in objects) {
-                def which = "${obj.getClass().getSimpleName()}.val"
+                def which = "${obj.getClass().getSimpleName()}.stored"
                 try {
-                    obj.val = which.startsWith('LinkedHashMap') ? 
"OK:LinkedHashMap.bar" : 'bar'
-                    assert obj.val.startsWith('OK:') : "$which -> $obj.val"
+                    obj.stored = which.startsWith('LinkedHashMap') ? 
"OK:LinkedHashMap.bar" : 'bar'
+                    assert obj.stored.startsWith('OK:') : "$which -> 
$obj.stored"
                 } catch (any) {
                     assert false : "$which -> FAIL:$any"
                 }
diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy 
b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
index f69eb1a332..90a376842a 100644
--- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
@@ -815,7 +815,7 @@ final class LambdaTest {
                     this.val = v
                 }
                 String toString() {
-                    val as String
+                    this.val as String
                 }
                 def <Out> Value<Out> replace(Supplier<Out> supplier) {
                     new Value<>(supplier.get())
diff --git 
a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy 
b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index 1abb9bd8cb..20e16129de 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -482,6 +482,11 @@ final class GroovyParserTest {
         doRunAndTestAntlr4('core/SafeChainOperator.groovy')
     }
 
+    @Test
+    void 'groovy core - val'() {
+        doRunAndTestAntlr4('core/Val_01x.groovy')
+    }
+
     @Test
     void 'groovy core - var'() {
         doRunAndTestAntlr4('core/Var_01x.groovy')
diff --git 
a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy 
b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index 788f47e8ef..bbe0175f72 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -480,6 +480,13 @@ final class SyntaxErrorTest {
         TestUtils.doRunAndShouldFail('fail/MethodCall_01x.groovy')
     }
 
+    @Test
+    void 'groovy core - val'() {
+        TestUtils.doRunAndShouldFail('fail/Val_01x.groovy')
+        TestUtils.doRunAndShouldFail('fail/Val_02x.groovy')
+        TestUtils.doRunAndShouldFail('fail/Val_03x.groovy')
+    }
+
     @Test
     void 'groovy core - var'() {
         TestUtils.doRunAndShouldFail('fail/Var_01x.groovy')
diff --git 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
index 31a22c99f1..284d718d4f 100644
--- 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
+++ 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
@@ -110,6 +110,7 @@ import static 
org.apache.groovy.parser.antlr4.GroovyLexer.TRAIT;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.TRANSIENT;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.TRY;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.UNEXPECTED_CHAR;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.VAL;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.VAR;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.VOID;
 import static org.apache.groovy.parser.antlr4.GroovyLexer.VOLATILE;
@@ -124,7 +125,7 @@ import static 
org.apache.groovy.parser.antlr4.GroovyLexer.YIELD;
  */
 public class SmartDocumentFilter extends DocumentFilter {
     public static final List<Integer> HIGHLIGHTED_TOKEN_TYPE_LIST = 
Arrays.asList(AS, DEF, IN, TRAIT, THREADSAFE,
-            VAR, BuiltInPrimitiveType, ABSTRACT, ASSERT, BREAK, CASE, CATCH, 
CLASS, CONST, CONTINUE, DEFAULT, DO,
+            VAL, VAR, BuiltInPrimitiveType, ABSTRACT, ASSERT, BREAK, CASE, 
CATCH, CLASS, CONST, CONTINUE, DEFAULT, DO,
             ELSE, ENUM, EXTENDS, FINAL, FINALLY, FOR, IF, GOTO, IMPLEMENTS, 
IMPORT, INSTANCEOF, INTERFACE,
             NATIVE, NEW, NON_SEALED, NOT_IN, NOT_INSTANCEOF, PACKAGE, PERMITS, 
PRIVATE, PROTECTED, PUBLIC,
             RECORD, RETURN, SEALED, STATIC, STRICTFP, SUPER, SWITCH, 
SYNCHRONIZED,

Reply via email to