diff --git a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java index 9e05b5fe29..25faea2892 100644 --- a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java +++ b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java @@ -2038,6 +2038,27 @@ 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, + /* left= */ conditional, + /* mid= */ right, + /* 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..09aab64f8c 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,25 @@ 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; + AstNode rn = nullishCoalescingExpr(); + + // Cannot immediately contain, or be contained within, an && or || operation. + if (pn.getType() == Token.OR + || pn.getType() == Token.AND + || rn.getType() == Token.OR + || rn.getType() == Token.AND) { + reportError("msg.nullish.bad.token"); + } + + pn = new InfixExpression(Token.NULLISH_COALESCING, pn, rn, 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 9f26730ae4..d1493280f7 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Token.java +++ b/rhino/src/main/java/org/mozilla/javascript/Token.java @@ -229,7 +229,8 @@ public static enum CommentType { TEMPLATE_LITERAL_SUBST = 175, // template literal - substitution TAGGED_TEMPLATE_LITERAL = 176, // template literal - tagged/handler DOTDOTDOT = 177, // spread/rest ... - LAST_TOKEN = 177; + NULLISH_COALESCING = 178, // nullish coalescing (??) + LAST_TOKEN = 178; /** * Returns a name for the token. If Rhino is compiled with certain hardcoded debugging flags in @@ -468,6 +469,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 02e2f6e7dc..d84f589813 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/rhino/src/main/java/org/mozilla/javascript/ast/AstNode.java b/rhino/src/main/java/org/mozilla/javascript/ast/AstNode.java index c40dcf720c..45225ebb53 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ast/AstNode.java +++ b/rhino/src/main/java/org/mozilla/javascript/ast/AstNode.java @@ -82,6 +82,7 @@ public abstract class AstNode extends Node implements Comparable { operatorNames.put(Token.COMMA, ","); operatorNames.put(Token.COLON, ":"); operatorNames.put(Token.OR, "||"); + operatorNames.put(Token.NULLISH_COALESCING, "??"); operatorNames.put(Token.AND, "&&"); operatorNames.put(Token.INC, "++"); operatorNames.put(Token.DEC, "--"); diff --git a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties index 642491e89c..a91dcd0aec 100644 --- a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties +++ b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties @@ -122,6 +122,9 @@ msg.bad.yield =\ msg.yield.parenthesized =\ yield expression must be parenthesized. +msg.nullish.bad.token =\ + Syntax Error: Unexpected token. + # NativeGlobal msg.cant.call.indirect =\ Function "{0}" must be called directly, and not by way of a \ diff --git a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages_fr.properties b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages_fr.properties index 2082b17c72..436ebccdb0 100644 --- a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages_fr.properties +++ b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages_fr.properties @@ -87,6 +87,9 @@ msg.bad.yield =\ msg.yield.parenthesized =\ L''expression suivant ''yield'' doit \u00eatre entre parenth\u00e8ses. +msg.nullish.bad.token =\ + Erreur de syntaxe: Jeton inattendu. + # NativeGlobal msg.cant.call.indirect =\ La fonction "{0}" doit \u00EAtre appel\u00E9e directement et non par l''interm\u00E9diaire \ 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..861a5d9364 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/NullishCoalescingOpTest.java @@ -0,0 +1,59 @@ +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() { + Utils.runWithAllOptimizationLevels( + cx -> { + 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)); + return null; + }); + } + + @Test + public void testNullishColascingShortCircuit() { + String script = "0 || 0 ?? true"; + Utils.assertEvaluatorExceptionES6("Syntax Error: Unexpected token. (test#1)", script); + + String script2 = "0 && 0 ?? true"; + Utils.assertEvaluatorExceptionES6("Syntax Error: Unexpected token. (test#1)", script2); + + String script3 = "0 ?? 0 && true;"; + Utils.assertEvaluatorExceptionES6("Syntax Error: Unexpected token. (test#1)", script3); + + String script4 = "0 ?? 0 || true;"; + Utils.assertEvaluatorExceptionES6("Syntax Error: Unexpected token. (test#1)", script4); + } + + @Test + public void testNullishColascingPrecedence() { + Utils.runWithAllOptimizationLevels( + cx -> { + Scriptable scope = cx.initStandardObjects(); + cx.setLanguageVersion(Context.VERSION_ES6); + + String script1 = "3 == 3 ? 'yes' ?? 'default string' : 'no'"; + Assert.assertEquals( + "yes", + cx.evaluateString(scope, script1, "nullish coalescing basic", 0, null)); + return null; + }); + } +} diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index 734e5b37e0..c758217d8f 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -4470,7 +4470,9 @@ language/expressions/call 60/92 (65.22%) ~language/expressions/class -~language/expressions/coalesce +language/expressions/coalesce 2/24 (8.33%) + tco-pos-null.js {unsupported: [tail-call-optimization]} + tco-pos-undefined.js {unsupported: [tail-call-optimization]} language/expressions/comma 1/6 (16.67%) tco-final.js {unsupported: [tail-call-optimization]} @@ -4616,8 +4618,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]} @@ -5271,7 +5272,7 @@ language/expressions/new 41/59 (69.49%) ~language/expressions/new.target -language/expressions/object 864/1169 (73.91%) +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]} @@ -6077,7 +6078,6 @@ language/expressions/object 864/1169 (73.91%) cpn-obj-lit-computed-property-name-from-assignment-expression-coalesce.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