Fix #243 - Add "Elvis" and "Null Coalescing" operators
diff --git a/spec/src/main/asciidoc/ELSpec.adoc b/spec/src/main/asciidoc/ELSpec.adoc
index 642ce81..972152e 100644
--- a/spec/src/main/asciidoc/ELSpec.adoc
+++ b/spec/src/main/asciidoc/ELSpec.adoc
@@ -1039,6 +1039,28 @@
 
 * If `A` is `false`, evaluate and return `C`
 
+== Elvis Operator - `A ?: B`
+
+The Elivs operator returns the first operand if that operand is `true`.
+Otherwise, the second operand is returned.
+
+Coerce `A` to `boolean` (primitive):
+
+* If `A` is `true`, evaluate and return `A` (without coercion)
+
+* If `A` is `false`, evaluate and return `B`
+
+== Null coalescing Operator - `A ?? B`
+
+The Null coalescing operator returns the first operand if that operand is non-null.
+Otherwise, the second operand is returned.
+
+To evaluate `A ?? B`:
+
+* If `A` is `null`, return `B`
+
+* Otherwise return `A`
+
 === Assignment Operator - `A = B`
 
 Assign the value of `B` to `A`. `A` must be an _lvalue_, otherwise, a
@@ -1703,7 +1725,7 @@
 void Assignment() : {}
 {
         LOOKAHEAD(4) LambdaExpression() |
-        Choice() ( LOOKAHEAD(2) <ASSIGN> Assignment() #Assign(2) )*
+        Ternary() ( LOOKAHEAD(2) <ASSIGN> Assignment() #Assign(2) )*
 }
 
 /*
@@ -1712,7 +1734,7 @@
 void LambdaExpression() #LambdaExpression : {}
 {
        LambdaParameters() <ARROW>
-       (LOOKAHEAD(3) LambdaExpression() | Choice() )
+       (LOOKAHEAD(3) LambdaExpression() | Ternary() )
 }
 
 void LambdaParameters() #LambdaParameters: {}
@@ -1724,12 +1746,19 @@
 }
 
 /*
- * Choice
- * For Choice markup a ? b : c, right associative
+ * Ternary
+ * For '??' '?:' '? :' then Or
  */
-void Choice() : {}
+void Ternary() : {}
 {
-    Or() (<QUESTIONMARK> Choice() <COLON> Choice() #Choice(3))?
+    Or()
+    (
+        LOOKAHEAD(2) (<QUESTIONMARK><QUESTIONMARK> Ternary() #NullCoalescing(2))
+    |
+        LOOKAHEAD(2) (<QUESTIONMARK><COLON> Ternary() #Elvis(2))
+    |
+        (<QUESTIONMARK> Ternary() <COLON> Ternary() #Choice(3))
+    )?
 }
 
 /*
@@ -1900,7 +1929,7 @@
 {
     <LPAREN>
         LambdaParameters() <ARROW>
-        (LOOKAHEAD(3) LambdaExpression() | Choice() )
+        (LOOKAHEAD(3) LambdaExpression() | Ternary() )
     <RPAREN>
     (MethodArguments())*
 }
diff --git a/tck/src/main/java/com/sun/ts/tests/el/common/util/ExprEval.java b/tck/src/main/java/com/sun/ts/tests/el/common/util/ExprEval.java
index 8171d9a..8bfb5aa 100644
--- a/tck/src/main/java/com/sun/ts/tests/el/common/util/ExprEval.java
+++ b/tck/src/main/java/com/sun/ts/tests/el/common/util/ExprEval.java
@@ -65,6 +65,10 @@
       sandwich = "{empty A}";
     else if ("conditional".equals(operation))
       sandwich = "{A " + "?" + "B" + ":" + " C}";
+    else if ("null_coalescing".equals(operation))
+        sandwich = "{A " + "??" + "B" + "}";
+    else if ("elvis".equals(operation))
+        sandwich = "{A " + "?:" + "B" + "}";
     else // binary operation
       sandwich = "{A " + operation + " B}";
 
diff --git a/tck/src/main/java/com/sun/ts/tests/el/spec/elvisoperator/ELClientIT.java b/tck/src/main/java/com/sun/ts/tests/el/spec/elvisoperator/ELClientIT.java
new file mode 100644
index 0000000..aa1473e
--- /dev/null
+++ b/tck/src/main/java/com/sun/ts/tests/el/spec/elvisoperator/ELClientIT.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package com.sun.ts.tests.el.spec.elvisoperator;
+
+import com.sun.ts.tests.el.common.util.ExprEval;
+import com.sun.ts.tests.el.common.util.NameValuePair;
+
+import jakarta.el.ELException;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import java.lang.System.Logger;
+
+public class ELClientIT {
+
+    private static final Logger logger = System.getLogger(ELClientIT.class.getName());
+
+    private final boolean[] deferred = { true, false };
+
+    @AfterEach
+    public void cleanup() throws Exception {
+        logger.log(Logger.Level.INFO, "Cleanup method called");
+    }
+
+    @BeforeEach
+    void logStartTest(TestInfo testInfo) {
+        logger.log(Logger.Level.INFO, "STARTING TEST : " + testInfo.getDisplayName());
+    }
+
+    @AfterEach
+    void logFinishTest(TestInfo testInfo) {
+        logger.log(Logger.Level.INFO, "FINISHED TEST : " + testInfo.getDisplayName());
+    }
+
+    @Test
+    public void elEvlisNullTest() throws Exception {
+
+        boolean pass = false;
+
+        String[] symbols = { "$", "#" };
+        String expectedResult = "default";
+
+        try {
+            for (String prefix : symbols) {
+                /*
+                 * Elvis operator expects a boolean first operand so null is coerced to false. First operand is false so
+                 * the second operand is returned.
+                 */
+                String expr = prefix + "{null ?: 'default'}";
+                Object result = ExprEval.evaluateValueExpression(expr, null, String.class);
+
+                if (result == null) {
+                    logger.log(Logger.Level.TRACE, "result is null");
+                } else {
+                    logger.log(Logger.Level.TRACE, "result is " + result.toString());
+                }
+
+                pass = (ExprEval.compareClass(result, String.class) && ExprEval.compareValue(result, expectedResult));
+
+                if (!pass) {
+                    throw new Exception("TEST FAILED: pass = false");
+                }
+            }
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    @Test
+    public void elElvisEmptyStringTest() throws Exception {
+        this.testElvisOperator("", "default");
+    }
+
+    @Test
+    public void elElvisNonTrueStringTest() throws Exception {
+        this.testElvisOperator("other", "default");
+    }
+
+    @Test
+    public void elElvisFalseStringTest() throws Exception {
+        this.testElvisOperator("false", "default");
+    }
+
+    @Test
+    public void elElvisTrueStringTest() throws Exception {
+        String testVal = "true";
+        this.testElvisOperator(testVal, testVal);
+    }
+
+    @Test
+    public void elElvisLongTest() throws Exception {
+        assertThrows(ELException.class, () -> {
+            this.testElvisOperator(Long.valueOf(0), "ignored");
+        });
+    }
+
+    // ---------------------------------------------------------- private methods
+
+    // Test Evlis.
+    private void testElvisOperator(Object testVal, Object expectedResult) throws Exception {
+
+        boolean pass = false;
+
+        NameValuePair value[] = NameValuePair.buildNameValuePair(testVal, "default");
+
+        try {
+            for (boolean tf : deferred) {
+                String expr = ExprEval.buildElExpr(tf, "elvis");
+                Object result = ExprEval.evaluateValueExpression(expr, value, Object.class);
+
+                logger.log(Logger.Level.TRACE, "result is " + result.toString());
+
+                pass = (ExprEval.compareClass(result, expectedResult.getClass()) && ExprEval.compareValue(result, expectedResult));
+
+                if (!pass) {
+                    throw new Exception("TEST FAILED: pass = false");
+                }
+            }
+        } catch (ELException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+}
diff --git a/tck/src/main/java/com/sun/ts/tests/el/spec/nullcoalescingoperator/ELClientIT.java b/tck/src/main/java/com/sun/ts/tests/el/spec/nullcoalescingoperator/ELClientIT.java
new file mode 100644
index 0000000..b8b7a3b
--- /dev/null
+++ b/tck/src/main/java/com/sun/ts/tests/el/spec/nullcoalescingoperator/ELClientIT.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package com.sun.ts.tests.el.spec.nullcoalescingoperator;
+
+import com.sun.ts.tests.el.common.util.ExprEval;
+import com.sun.ts.tests.el.common.util.NameValuePair;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import java.lang.System.Logger;
+
+public class ELClientIT {
+
+    private static final Logger logger = System.getLogger(ELClientIT.class.getName());
+
+    private final boolean[] deferred = { true, false };
+
+    @AfterEach
+    public void cleanup() throws Exception {
+        logger.log(Logger.Level.INFO, "Cleanup method called");
+    }
+
+    @BeforeEach
+    void logStartTest(TestInfo testInfo) {
+        logger.log(Logger.Level.INFO, "STARTING TEST : " + testInfo.getDisplayName());
+    }
+
+    @AfterEach
+    void logFinishTest(TestInfo testInfo) {
+        logger.log(Logger.Level.INFO, "FINISHED TEST : " + testInfo.getDisplayName());
+    }
+
+    @Test
+    public void elNullCoalescingNullTest() throws Exception {
+
+        boolean pass = false;
+
+        String[] symbols = { "$", "#" };
+        String expectedResult = "default";
+
+        try {
+            for (String prefix : symbols) {
+                String expr = prefix + "{null ?? 'default'}";
+                Object result = ExprEval.evaluateValueExpression(expr, null, Object.class);
+
+                if (result == null) {
+                    logger.log(Logger.Level.TRACE, "result is null");
+                } else {
+                    logger.log(Logger.Level.TRACE, "result is " + result.toString());
+                }
+
+                pass = (ExprEval.compareClass(result, String.class) && ExprEval.compareValue(result, expectedResult));
+
+                if (!pass) {
+                    throw new Exception("TEST FAILED: pass = false");
+                }
+            }
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    @Test
+    public void elNullCoalescingStringTest() throws Exception {
+        this.testNullCoalescingOperator(null, "default");
+        String testVal = "something";
+        this.testNullCoalescingOperator(testVal, testVal);
+    }
+
+    @Test
+    public void elNullCoalescingLongTest() throws Exception {
+        Long testVal = Long.valueOf(0);
+        this.testNullCoalescingOperator(testVal, testVal);
+        testVal = Long.valueOf(1234);
+        this.testNullCoalescingOperator(testVal, testVal);
+    }
+
+    @Test
+    public void elNullCoalescingArrayTest() throws Exception {
+        String[] testVal = new String[0];
+        this.testNullCoalescingOperator(testVal, testVal);
+        testVal = new String[] { "data" };
+        this.testNullCoalescingOperator(testVal, testVal);
+    }
+
+    // ---------------------------------------------------------- private methods
+
+    // Test Null Coalescing.
+    private void testNullCoalescingOperator(Object testVal, Object expectedResult) throws Exception {
+
+        boolean pass = false;
+
+        NameValuePair value[] = NameValuePair.buildNameValuePair(testVal, "default");
+
+        try {
+            for (boolean tf : deferred) {
+                String expr = ExprEval.buildElExpr(tf, "null_coalescing");
+                Object result = ExprEval.evaluateValueExpression(expr, value, Object.class);
+
+                logger.log(Logger.Level.TRACE, "result is " + result.toString());
+
+                pass = (ExprEval.compareClass(result, expectedResult.getClass()) && ExprEval.compareValue(result, expectedResult));
+
+                if (!pass) {
+                    throw new Exception("TEST FAILED: pass = false");
+                }
+            }
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+}