From a8d4621e03defe86eca04fc1baee7cea8f56e0fe Mon Sep 17 00:00:00 2001 From: "satish.srinivasan" Date: Thu, 29 Aug 2024 16:25:57 +0530 Subject: [PATCH] FEAT: Implement `??` Co-authored-by: Greg Caban --- .../org/mozilla/javascript/IRFactory.java | 17 +++++++ .../java/org/mozilla/javascript/Parser.java | 11 ++++- .../java/org/mozilla/javascript/Token.java | 5 +- .../org/mozilla/javascript/TokenStream.java | 3 ++ .../tests/NullishCoalescingOpTest.java | 49 +++++++++++++++++++ tests/testsrc/test262.properties | 6 +-- 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 tests/src/test/java/org/mozilla/javascript/tests/NullishCoalescingOpTest.java diff --git a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java index 5464161e20..30cb3aab02 100644 --- a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java +++ b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java @@ -2038,6 +2038,23 @@ private static Node createBinary(int nodeType, Node left, Node right) { } break; } + + case Token.NULLISH_COALESCING: + { + // foo ?? default => + // (foo == undefined || foo == null) ? foo (left) : default (right) + + Node undefinedNode = new Name(0, "undefined"); + Node nullNode = new Node(Token.NULL); + + Node conditional = + new Node( + Token.OR, + new Node(Token.SHEQ, nullNode, left), + new Node(Token.SHEQ, undefinedNode, left)); + + return new Node(Token.HOOK, conditional, right, left); + } } return new Node(nodeType, left, right); diff --git a/rhino/src/main/java/org/mozilla/javascript/Parser.java b/rhino/src/main/java/org/mozilla/javascript/Parser.java index 1d8a75c127..3014c977dc 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Parser.java +++ b/rhino/src/main/java/org/mozilla/javascript/Parser.java @@ -2370,7 +2370,7 @@ private AstNode assignExpr() throws IOException { } private AstNode condExpr() throws IOException { - AstNode pn = orExpr(); + AstNode pn = nullishCoalescingExpr(); if (matchToken(Token.HOOK, true)) { int line = ts.lineno; int qmarkPos = ts.tokenBeg, colonPos = -1; @@ -2402,6 +2402,15 @@ private AstNode condExpr() throws IOException { return pn; } + private AstNode nullishCoalescingExpr() throws IOException { + AstNode pn = orExpr(); + if (matchToken(Token.NULLISH_COALESCING, true)) { + int opPos = ts.tokenBeg; + pn = new InfixExpression(Token.NULLISH_COALESCING, pn, nullishCoalescingExpr(), opPos); + } + return pn; + } + private AstNode orExpr() throws IOException { AstNode pn = andExpr(); if (matchToken(Token.OR, true)) { diff --git a/rhino/src/main/java/org/mozilla/javascript/Token.java b/rhino/src/main/java/org/mozilla/javascript/Token.java index eb85b876ac..0e2ccff01c 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Token.java +++ b/rhino/src/main/java/org/mozilla/javascript/Token.java @@ -227,7 +227,8 @@ public static enum CommentType { TEMPLATE_LITERAL_SUBST = 173, // template literal - substitution TAGGED_TEMPLATE_LITERAL = 174, // template literal - tagged/handler DOTDOTDOT = 175, // spread/rest ... - LAST_TOKEN = 175; + NULLISH_COALESCING = 176, // nullish coalescing (??) + LAST_TOKEN = 176; /** * Returns a name for the token. If Rhino is compiled with certain hardcoded debugging flags in @@ -462,6 +463,8 @@ public static String typeToName(int token) { return "COLON"; case OR: return "OR"; + case NULLISH_COALESCING: + return "NULLISH_COALESCING"; case AND: return "AND"; case INC: diff --git a/rhino/src/main/java/org/mozilla/javascript/TokenStream.java b/rhino/src/main/java/org/mozilla/javascript/TokenStream.java index 4c8fc39fbf..ec8ba14cb5 100644 --- a/rhino/src/main/java/org/mozilla/javascript/TokenStream.java +++ b/rhino/src/main/java/org/mozilla/javascript/TokenStream.java @@ -1146,6 +1146,9 @@ && peekChar() == '!' case ',': return Token.COMMA; case '?': + if (matchChar('?')) { + return Token.NULLISH_COALESCING; + } return Token.HOOK; case ':': if (matchChar(':')) { diff --git a/tests/src/test/java/org/mozilla/javascript/tests/NullishCoalescingOpTest.java b/tests/src/test/java/org/mozilla/javascript/tests/NullishCoalescingOpTest.java new file mode 100644 index 0000000000..6e3c4aee59 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/NullishCoalescingOpTest.java @@ -0,0 +1,49 @@ +package org.mozilla.javascript.tests; + +import org.junit.Assert; +import org.junit.Test; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; + +public class NullishCoalescingOpTest { + + @Test + public void testNullishColascingBasic() { + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + cx.setLanguageVersion(Context.VERSION_ES6); + + String script = "null ?? 'default string'"; + Assert.assertEquals( + "default string", + cx.evaluateString(scope, script, "nullish coalescing basic", 0, null)); + + String script2 = "undefined ?? 'default string'"; + Assert.assertEquals( + "default string", + cx.evaluateString(scope, script2, "nullish coalescing basic", 0, null)); + } + } + + @Test + public void testNullishColascingPrecedence() { + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + cx.setLanguageVersion(Context.VERSION_ES6); + cx.setOptimizationLevel(-1); + + String script1 = "3 == 3 ? 'yes' ?? 'default string' : 'no'"; + Assert.assertEquals( + "yes", cx.evaluateString(scope, script1, "nullish coalescing basic", 0, null)); + + String script3 = "3 || null ?? 'default string'"; + Assert.assertEquals( + 3.0, cx.evaluateString(scope, script3, "nullish coalescing basic", 0, null)); + + String script2 = "3 && null ?? 'default string'"; + Assert.assertEquals( + "default string", + cx.evaluateString(scope, script2, "nullish coalescing basic", 0, null)); + } + } +} diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index bda92a6188..7acb5fec8a 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -4616,8 +4616,7 @@ language/expressions/compound-assignment 137/454 (30.18%) language/expressions/concatenation 0/5 (0.0%) -language/expressions/conditional 3/22 (13.64%) - coalesce-expr-ternary.js +language/expressions/conditional 2/22 (9.09%) tco-cond.js {unsupported: [tail-call-optimization]} tco-pos.js {unsupported: [tail-call-optimization]} @@ -5282,7 +5281,7 @@ language/expressions/new 41/59 (69.49%) ~language/expressions/new.target -language/expressions/object 866/1169 (74.08%) +language/expressions/object 865/1169 (73.99%) dstr/async-gen-meth-ary-init-iter-close.js {unsupported: [async-iteration, async]} dstr/async-gen-meth-ary-init-iter-get-err.js {unsupported: [async-iteration]} dstr/async-gen-meth-ary-init-iter-get-err-array-prototype.js {unsupported: [async-iteration]} @@ -6090,7 +6089,6 @@ language/expressions/object 866/1169 (74.08%) cpn-obj-lit-computed-property-name-from-assignment-expression-logical-or.js cpn-obj-lit-computed-property-name-from-async-arrow-function-expression.js cpn-obj-lit-computed-property-name-from-await-expression.js {unsupported: [module, async]} - cpn-obj-lit-computed-property-name-from-expression-coalesce.js cpn-obj-lit-computed-property-name-from-yield-expression.js fn-name-accessor-get.js fn-name-accessor-set.js