[ESNext] Implement nullish coalescing
authorross.kirsling@sony.com <ross.kirsling@sony.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 25 Jul 2019 07:50:46 +0000 (07:50 +0000)
committerross.kirsling@sony.com <ross.kirsling@sony.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 25 Jul 2019 07:50:46 +0000 (07:50 +0000)
https://bugs.webkit.org/show_bug.cgi?id=200072

Reviewed by Darin Adler.

JSTests:

* stress/nullish-coalescing.js: Added.

Source/JavaScriptCore:

Implement the nullish coalescing proposal, which has now reached Stage 3 at TC39.

This introduces a ?? operator which:
  - acts like || but checks for nullishness instead of truthiness
  - has a precedence lower than || (or any other binary operator)
  - must be disambiguated with parentheses when combined with || or &&

* bytecompiler/NodesCodegen.cpp:
(JSC::CoalesceNode::emitBytecode): Added.
Bytecode must use OpIsUndefinedOrNull and not OpNeqNull because of document.all.

* parser/ASTBuilder.h:
(JSC::ASTBuilder::makeBinaryNode):
* parser/Lexer.cpp:
(JSC::Lexer<T>::lexWithoutClearingLineTerminator):
* parser/NodeConstructors.h:
(JSC::CoalesceNode::CoalesceNode): Added.
* parser/Nodes.h:
Introduce new token and AST node.

* parser/Parser.cpp:
(JSC::Parser<LexerType>::parseBinaryExpression):
Implement early error.

* parser/ParserTokens.h:
Since this patch needs to shift the value of every binary operator token anyway,
let's only bother to increment their LSBs when we actually have a precedence conflict.

* parser/ResultType.h:
(JSC::ResultType::definitelyIsNull const): Added.
(JSC::ResultType::mightBeUndefinedOrNull const): Added.
(JSC::ResultType::forCoalesce): Added.
We can do better than forLogicalOp here; let's be as accurate as possible.

* runtime/Options.h:
Add runtime feature flag.

Tools:

* Scripts/run-jsc-stress-tests:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@247819 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
JSTests/ChangeLog
JSTests/stress/nullish-coalescing.js [new file with mode: 0644]
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp
Source/JavaScriptCore/parser/ASTBuilder.h
Source/JavaScriptCore/parser/Lexer.cpp
Source/JavaScriptCore/parser/NodeConstructors.h
Source/JavaScriptCore/parser/Nodes.h
Source/JavaScriptCore/parser/Parser.cpp
Source/JavaScriptCore/parser/ParserTokens.h
Source/JavaScriptCore/parser/ResultType.h
Source/JavaScriptCore/runtime/Options.h
Tools/ChangeLog
Tools/Scripts/run-jsc-stress-tests

index e8d52e0..d88721e 100644 (file)
@@ -1,3 +1,12 @@
+2019-07-25  Ross Kirsling  <ross.kirsling@sony.com>
+
+        [ESNext] Implement nullish coalescing
+        https://bugs.webkit.org/show_bug.cgi?id=200072
+
+        Reviewed by Darin Adler.
+
+        * stress/nullish-coalescing.js: Added.
+
 2019-07-24  Alexey Shvayka  <shvaikalesh@gmail.com>
 
         Three checks are missing in Proxy internal methods
diff --git a/JSTests/stress/nullish-coalescing.js b/JSTests/stress/nullish-coalescing.js
new file mode 100644 (file)
index 0000000..5958f12
--- /dev/null
@@ -0,0 +1,85 @@
+//@ runNullishCoalescingEnabled
+
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`expected ${expected} but got ${actual}`);
+}
+
+function shouldNotThrow(script) {
+    eval(script);
+}
+
+function shouldThrowSyntaxError(script) {
+    let error;
+    try {
+        eval(script);
+    } catch (e) {
+        error = e;
+    }
+
+    if (!(error instanceof SyntaxError))
+        throw new Error('Expected SyntaxError!');
+}
+
+shouldBe(undefined ?? 3, 3);
+shouldBe(null ?? 3, 3);
+shouldBe(true ?? 3, true);
+shouldBe(false ?? 3, false);
+shouldBe(0 ?? 3, 0);
+shouldBe(1 ?? 3, 1);
+shouldBe('' ?? 3, '');
+shouldBe('hi' ?? 3, 'hi');
+shouldBe(({} ?? 3) instanceof Object, true);
+shouldBe(({ x: 'hi' } ?? 3).x, 'hi');
+shouldBe(([] ?? 3) instanceof Array, true);
+shouldBe((['hi'] ?? 3)[0], 'hi');
+shouldBe((makeMasquerader() ?? 3) == null, true);
+
+shouldBe(1 | null ?? 3, 1);
+shouldBe(1 ^ null ?? 3, 1);
+shouldBe(1 & null ?? 3, 0);
+shouldBe(3 == null ?? 3, false);
+shouldBe(3 != null ?? 3, true);
+shouldBe(3 === null ?? 3, false);
+shouldBe(3 !== null ?? 3, true);
+shouldBe(1 < null ?? 3, false);
+shouldBe(1 > null ?? 3, true);
+shouldBe(1 <= null ?? 3, false);
+shouldBe(1 >= null ?? 3, true);
+shouldBe(1 << null ?? 3, 1);
+shouldBe(1 >> null ?? 3, 1);
+shouldBe(1 >>> null ?? 3, 1);
+shouldBe(1 + null ?? 3, 1);
+shouldBe(1 - null ?? 3, 1);
+shouldBe(1 * null ?? 3, 0);
+shouldBe(1 / null ?? 3, Infinity);
+shouldBe(isNaN(1 % null ?? 3), true);
+shouldBe(1 ** null ?? 3, 1);
+
+const obj = {
+    count: 0,
+    get x() { this.count++; return 'x'; }
+};
+false ?? obj.x;
+shouldBe(obj.count, 0);
+null ?? obj.x;
+shouldBe(obj.count, 1);
+obj.x ?? obj.x;
+shouldBe(obj.count, 2);
+
+shouldThrowSyntaxError('0 || 1 ?? 2');
+shouldThrowSyntaxError('0 && 1 ?? 2');
+shouldThrowSyntaxError('0 ?? 1 || 2');
+shouldThrowSyntaxError('0 ?? 1 && 2');
+shouldNotThrow('(0 || 1) ?? 2');
+shouldNotThrow('0 || (1 ?? 2)');
+shouldNotThrow('(0 && 1) ?? 2');
+shouldNotThrow('0 && (1 ?? 2)');
+shouldNotThrow('(0 ?? 1) || 2');
+shouldNotThrow('0 ?? (1 || 2)');
+shouldNotThrow('(0 ?? 1) && 2');
+shouldNotThrow('0 ?? (1 && 2)');
+
+shouldNotThrow('0 || 1 && 2 | 3 ^ 4 & 5 == 6 != 7 === 8 !== 9 < 0 > 1 <= 2 >= 3 << 4 >> 5 >>> 6 + 7 - 8 * 9 / 0 % 1 ** 2');
+shouldThrowSyntaxError('0 || 1 && 2 | 3 ^ 4 & 5 == 6 != 7 === 8 !== 9 < 0 > 1 <= 2 >= 3 << 4 >> 5 >>> 6 + 7 - 8 * 9 / 0 % 1 ** 2 ?? 3');
+shouldThrowSyntaxError('3 ?? 2 ** 1 % 0 / 9 * 8 - 7 + 6 >>> 5 >> 4 << 3 >= 2 <= 1 > 0 < 9 !== 8 === 7 != 6 == 5 & 4 ^ 3 | 2 && 1 || 0');
index 4692a56..00d8ea9 100644 (file)
@@ -1,3 +1,47 @@
+2019-07-25  Ross Kirsling  <ross.kirsling@sony.com>
+
+        [ESNext] Implement nullish coalescing
+        https://bugs.webkit.org/show_bug.cgi?id=200072
+
+        Reviewed by Darin Adler.
+
+        Implement the nullish coalescing proposal, which has now reached Stage 3 at TC39.
+
+        This introduces a ?? operator which:
+          - acts like || but checks for nullishness instead of truthiness
+          - has a precedence lower than || (or any other binary operator)
+          - must be disambiguated with parentheses when combined with || or &&
+
+        * bytecompiler/NodesCodegen.cpp:
+        (JSC::CoalesceNode::emitBytecode): Added.
+        Bytecode must use OpIsUndefinedOrNull and not OpNeqNull because of document.all.
+
+        * parser/ASTBuilder.h:
+        (JSC::ASTBuilder::makeBinaryNode):
+        * parser/Lexer.cpp:
+        (JSC::Lexer<T>::lexWithoutClearingLineTerminator):
+        * parser/NodeConstructors.h:
+        (JSC::CoalesceNode::CoalesceNode): Added.
+        * parser/Nodes.h:
+        Introduce new token and AST node.
+
+        * parser/Parser.cpp:
+        (JSC::Parser<LexerType>::parseBinaryExpression):
+        Implement early error.
+
+        * parser/ParserTokens.h:
+        Since this patch needs to shift the value of every binary operator token anyway,
+        let's only bother to increment their LSBs when we actually have a precedence conflict.
+
+        * parser/ResultType.h:
+        (JSC::ResultType::definitelyIsNull const): Added.
+        (JSC::ResultType::mightBeUndefinedOrNull const): Added.
+        (JSC::ResultType::forCoalesce): Added.
+        We can do better than forLogicalOp here; let's be as accurate as possible.
+
+        * runtime/Options.h:
+        Add runtime feature flag.
+
 2019-07-24  Alexey Shvayka  <shvaikalesh@gmail.com>
 
         Three checks are missing in Proxy internal methods
index 7ff7b58..7cc078c 100644 (file)
@@ -2338,6 +2338,21 @@ void LogicalOpNode::emitBytecodeInConditionContext(BytecodeGenerator& generator,
     generator.emitNodeInConditionContext(m_expr2, trueTarget, falseTarget, fallThroughMode);
 }
 
+// ------------------------------ CoalesceNode ----------------------------
+
+RegisterID* CoalesceNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
+{
+    RefPtr<RegisterID> temp = generator.tempDestination(dst);
+    Ref<Label> target = generator.newLabel();
+
+    generator.emitNode(temp.get(), m_expr1);
+    generator.emitJumpIfFalse(generator.emitUnaryOp<OpIsUndefinedOrNull>(generator.newTemporary(), temp.get()), target.get());
+    generator.emitNodeInTailPosition(temp.get(), m_expr2);
+    generator.emitLabel(target.get());
+
+    return generator.move(dst, temp.get());
+}
+
 // ------------------------------ ConditionalNode ------------------------------
 
 RegisterID* ConditionalNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
index dc878c5..de5b133 100644 (file)
@@ -1393,6 +1393,9 @@ ExpressionNode* ASTBuilder::makeFunctionCallNode(const JSTokenLocation& location
 ExpressionNode* ASTBuilder::makeBinaryNode(const JSTokenLocation& location, int token, std::pair<ExpressionNode*, BinaryOpInfo> lhs, std::pair<ExpressionNode*, BinaryOpInfo> rhs)
 {
     switch (token) {
+    case COALESCE:
+        return new (m_parserArena) CoalesceNode(location, lhs.first, rhs.first);
+
     case OR:
         return new (m_parserArena) LogicalOpNode(location, lhs.first, rhs.first, OpLogicalOr);
 
index 4662c54..16bfa8a 100644 (file)
@@ -2153,8 +2153,13 @@ start:
         shift();
         break;
     case CharacterQuestion:
-        token = QUESTION;
         shift();
+        if (Options::useNullishCoalescing() && m_current == '?') {
+            shift();
+            token = COALESCE;
+            break;
+        }
+        token = QUESTION;
         break;
     case CharacterTilde:
         token = TILDE;
index a2b8c5f..d41e3ae 100644 (file)
@@ -669,6 +669,13 @@ namespace JSC {
     {
     }
 
+    inline CoalesceNode::CoalesceNode(const JSTokenLocation& location, ExpressionNode* expr1, ExpressionNode* expr2)
+        : ExpressionNode(location, ResultType::forCoalesce(expr1->resultDescriptor(), expr2->resultDescriptor()))
+        , m_expr1(expr1)
+        , m_expr2(expr2)
+    {
+    }
+
     inline ConditionalNode::ConditionalNode(const JSTokenLocation& location, ExpressionNode* logical, ExpressionNode* expr1, ExpressionNode* expr2)
         : ExpressionNode(location)
         , m_logical(logical)
index 60ba5ee..86fdaf3 100644 (file)
@@ -1305,6 +1305,17 @@ namespace JSC {
         ExpressionNode* m_expr2;
     };
 
+    class CoalesceNode final : public ExpressionNode {
+    public:
+        CoalesceNode(const JSTokenLocation&, ExpressionNode* expr1, ExpressionNode* expr2);
+
+    private:
+        RegisterID* emitBytecode(BytecodeGenerator&, RegisterID* = 0) override;
+
+        ExpressionNode* m_expr1;
+        ExpressionNode* m_expr2;
+    };
+
     // The ternary operator, "m_logical ? m_expr1 : m_expr2"
     class ConditionalNode final : public ExpressionNode {
     public:
index 502abf8..e254dc9 100644 (file)
@@ -3883,14 +3883,20 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseBinaryExpres
     int operatorStackDepth = 0;
     typename TreeBuilder::BinaryExprContext binaryExprContext(context);
     JSTokenLocation location(tokenLocation());
+    bool hasLogicalOperator = false;
+    bool hasCoalesceOperator = false;
+
     while (true) {
         JSTextPosition exprStart = tokenStartPosition();
         int initialAssignments = m_parserState.assignmentCount;
         JSTokenType leadingTokenTypeForUnaryExpression = m_token.m_type;
         TreeExpression current = parseUnaryExpression(context);
         failIfFalse(current, "Cannot parse expression");
-        
+
         context.appendBinaryExpressionInfo(operandStackDepth, current, exprStart, lastTokenEndPosition(), lastTokenEndPosition(), initialAssignments != m_parserState.assignmentCount);
+        int precedence = isBinaryOperator(m_token.m_type);
+        if (!precedence)
+            break;
 
         // 12.6 https://tc39.github.io/ecma262/#sec-exp-operator
         // ExponentiationExpresion is described as follows.
@@ -3915,9 +3921,14 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseBinaryExpres
         // But it's OK for ** because the operator "**" has the highest operator precedence in the binary operators.
         failIfTrue(match(POW) && isUnaryOpExcludingUpdateOp(leadingTokenTypeForUnaryExpression), "Ambiguous unary expression in the left hand side of the exponentiation expression; parentheses must be used to disambiguate the expression");
 
-        int precedence = isBinaryOperator(m_token.m_type);
-        if (!precedence)
-            break;
+        // Mixing ?? with || or && is currently specified as an early error.
+        // Since ?? is the lowest-precedence binary operator, it suffices to check whether these ever coexist in the operator stack.
+        if (match(AND) || match(OR))
+            hasLogicalOperator = true;
+        else if (match(COALESCE))
+            hasCoalesceOperator = true;
+        failIfTrue(hasLogicalOperator && hasCoalesceOperator, "Coalescing and logical operators used together in the same expression; parentheses must be used to disambiguate");
+
         m_parserState.nonTrivialExpressionCount++;
         m_parserState.nonLHSCount++;
         int operatorToken = m_token.m_type;
index cfee337..15ca5ea 100644 (file)
@@ -148,30 +148,31 @@ enum JSTokenType {
     TYPEOF = 6 | UnaryOpTokenFlag | KeywordTokenFlag,
     VOIDTOKEN = 7 | UnaryOpTokenFlag | KeywordTokenFlag,
     DELETETOKEN = 8 | UnaryOpTokenFlag | KeywordTokenFlag,
-    OR = 0 | BINARY_OP_PRECEDENCE(1),
-    AND = 1 | BINARY_OP_PRECEDENCE(2),
-    BITOR = 2 | BINARY_OP_PRECEDENCE(3),
-    BITXOR = 3 | BINARY_OP_PRECEDENCE(4),
-    BITAND = 4 | BINARY_OP_PRECEDENCE(5),
-    EQEQ = 5 | BINARY_OP_PRECEDENCE(6),
-    NE = 6 | BINARY_OP_PRECEDENCE(6),
-    STREQ = 7 | BINARY_OP_PRECEDENCE(6),
-    STRNEQ = 8 | BINARY_OP_PRECEDENCE(6),
-    LT = 9 | BINARY_OP_PRECEDENCE(7),
-    GT = 10 | BINARY_OP_PRECEDENCE(7),
-    LE = 11 | BINARY_OP_PRECEDENCE(7),
-    GE = 12 | BINARY_OP_PRECEDENCE(7),
-    INSTANCEOF = 13 | BINARY_OP_PRECEDENCE(7) | KeywordTokenFlag,
-    INTOKEN = 14 | IN_OP_PRECEDENCE(7) | KeywordTokenFlag,
-    LSHIFT = 15 | BINARY_OP_PRECEDENCE(8),
-    RSHIFT = 16 | BINARY_OP_PRECEDENCE(8),
-    URSHIFT = 17 | BINARY_OP_PRECEDENCE(8),
-    PLUS = 18 | BINARY_OP_PRECEDENCE(9) | UnaryOpTokenFlag,
-    MINUS = 19 | BINARY_OP_PRECEDENCE(9) | UnaryOpTokenFlag,
-    TIMES = 20 | BINARY_OP_PRECEDENCE(10),
-    DIVIDE = 21 | BINARY_OP_PRECEDENCE(10),
-    MOD = 22 | BINARY_OP_PRECEDENCE(10),
-    POW = 23 | BINARY_OP_PRECEDENCE(11) | RightAssociativeBinaryOpTokenFlag, // Make sure that POW has the highest operator precedence.
+    COALESCE = 0 | BINARY_OP_PRECEDENCE(1),
+    OR = 0 | BINARY_OP_PRECEDENCE(2),
+    AND = 0 | BINARY_OP_PRECEDENCE(3),
+    BITOR = 0 | BINARY_OP_PRECEDENCE(4),
+    BITXOR = 0 | BINARY_OP_PRECEDENCE(5),
+    BITAND = 0 | BINARY_OP_PRECEDENCE(6),
+    EQEQ = 0 | BINARY_OP_PRECEDENCE(7),
+    NE = 1 | BINARY_OP_PRECEDENCE(7),
+    STREQ = 2 | BINARY_OP_PRECEDENCE(7),
+    STRNEQ = 3 | BINARY_OP_PRECEDENCE(7),
+    LT = 0 | BINARY_OP_PRECEDENCE(8),
+    GT = 1 | BINARY_OP_PRECEDENCE(8),
+    LE = 2 | BINARY_OP_PRECEDENCE(8),
+    GE = 3 | BINARY_OP_PRECEDENCE(8),
+    INSTANCEOF = 4 | BINARY_OP_PRECEDENCE(8) | KeywordTokenFlag,
+    INTOKEN = 5 | IN_OP_PRECEDENCE(8) | KeywordTokenFlag,
+    LSHIFT = 0 | BINARY_OP_PRECEDENCE(9),
+    RSHIFT = 1 | BINARY_OP_PRECEDENCE(9),
+    URSHIFT = 2 | BINARY_OP_PRECEDENCE(9),
+    PLUS = 0 | BINARY_OP_PRECEDENCE(10) | UnaryOpTokenFlag,
+    MINUS = 1 | BINARY_OP_PRECEDENCE(10) | UnaryOpTokenFlag,
+    TIMES = 0 | BINARY_OP_PRECEDENCE(11),
+    DIVIDE = 1 | BINARY_OP_PRECEDENCE(11),
+    MOD = 2 | BINARY_OP_PRECEDENCE(11),
+    POW = 0 | BINARY_OP_PRECEDENCE(12) | RightAssociativeBinaryOpTokenFlag, // Make sure that POW has the highest operator precedence.
     ERRORTOK = 0 | ErrorTokenFlag,
     UNTERMINATED_IDENTIFIER_ESCAPE_ERRORTOK = 0 | ErrorTokenFlag | UnterminatedErrorTokenFlag,
     INVALID_IDENTIFIER_ESCAPE_ERRORTOK = 1 | ErrorTokenFlag,
index cce0f6d..f5fadde 100644 (file)
@@ -76,6 +76,16 @@ namespace JSC {
             return (m_bits & TypeBits) == TypeMaybeBigInt;
         }
 
+        constexpr bool definitelyIsNull() const
+        {
+            return (m_bits & TypeBits) == TypeMaybeNull;
+        }
+
+        constexpr bool mightBeUndefinedOrNull() const
+        {
+            return m_bits & (TypeMaybeNull | TypeMaybeOther);
+        }
+
         constexpr bool mightBeNumber() const
         {
             return m_bits & TypeMaybeNumber;
@@ -172,6 +182,15 @@ namespace JSC {
             return unknownType();
         }
 
+        static constexpr ResultType forCoalesce(ResultType op1, ResultType op2)
+        {
+            if (op1.definitelyIsNull())
+                return op2;
+            if (!op1.mightBeUndefinedOrNull())
+                return op1;
+            return unknownType();
+        }
+
         static constexpr ResultType forBitOp()
         {
             return bigIntOrInt32Type();
index ca8fa01..f5fe088 100644 (file)
@@ -494,6 +494,7 @@ constexpr bool enableWebAssemblyStreamingApi = false;
     v(bool, useWebAssemblyReferences, false, Normal, "Allow types from the wasm references spec.") \
     v(bool, useWeakRefs, false, Normal, "Expose the WeakRef constructor.") \
     v(bool, useBigInt, false, Normal, "If true, we will enable BigInt support.") \
+    v(bool, useNullishCoalescing, false, Normal, "Enable support for the ?? operator.") \
     v(bool, useArrayAllocationProfiling, true, Normal, "If true, we will use our normal array allocation profiling. If false, the allocation profile will always claim to be undecided.") \
     v(bool, forcePolyProto, false, Normal, "If true, create_this will always create an object with a poly proto structure.") \
     v(bool, forceMiniVMMode, false, Normal, "If true, it will force mini VM mode on.") \
index 10fd8c4..804e891 100644 (file)
@@ -1,3 +1,12 @@
+2019-07-25  Ross Kirsling  <ross.kirsling@sony.com>
+
+        [ESNext] Implement nullish coalescing
+        https://bugs.webkit.org/show_bug.cgi?id=200072
+
+        Reviewed by Darin Adler.
+
+        * Scripts/run-jsc-stress-tests:
+
 2019-07-24  Fujii Hironori  <Hironori.Fujii@sony.com>
 
         Add Takashi Komori and Tomoki Imai as contributors
index 9d77f58..83dc062 100755 (executable)
@@ -699,6 +699,10 @@ def runBigIntEnabled(*optionalTestSpecificOptions)
     run("big-int-enabled", "--useBigInt=true" , *(FTL_OPTIONS + optionalTestSpecificOptions))
 end
 
+def runNullishCoalescingEnabled(*optionalTestSpecificOptions)
+    run("nullish-coalescing-enabled", "--useNullishCoalescing=true" , *(FTL_OPTIONS + optionalTestSpecificOptions))
+end
+
 def runFTLNoCJIT(*optionalTestSpecificOptions)
     run("misc-ftl-no-cjit", *(FTL_OPTIONS + NO_CJIT_OPTIONS + optionalTestSpecificOptions))
 end