[ESNext] Implement optional chaining
authorross.kirsling@sony.com <ross.kirsling@sony.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 18 Aug 2019 05:54:13 +0000 (05:54 +0000)
committerross.kirsling@sony.com <ross.kirsling@sony.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 18 Aug 2019 05:54:13 +0000 (05:54 +0000)
https://bugs.webkit.org/show_bug.cgi?id=200199

Reviewed by Yusuke Suzuki.

JSTests:

* stress/nullish-coalescing.js:
* stress/optional-chaining.js: Added.
* stress/tail-call-recognize.js:

Source/JavaScriptCore:

Implement the optional chaining proposal, which has now reached Stage 3 at TC39.

This introduces a ?. operator which:
 - guards member access when the LHS is nullish, i.e. `null?.foo` and `null?.['foo']` are undefined
 - guards function calls when the LHS is nullish, i.e. `null?.()` is undefined
 - short-circuits over a whole access/call chain, i.e. `null?.a['b'](c++)` is undefined and does not increment c

This feature can be naively viewed as a ternary in disguise, i.e. `a?.b` is like `a == null ? undefined : a.b`.
However, since we must be sure not to double-evaluate the LHS, it's actually rather akin to a try block --
namely, we have the bytecode generator keep an early-out label for use throughout the access and call chain.

(Also note that document.all behaves as an object, so "nullish" means *strictly* equal to null or undefined.)

* bytecompiler/BytecodeGenerator.cpp:
(JSC::BytecodeGenerator::pushOptionalChainTarget): Added.
(JSC::BytecodeGenerator::popOptionalChainTarget): Added.
(JSC::BytecodeGenerator::emitOptionalCheck): Added.
* bytecompiler/BytecodeGenerator.h:
Implement early-out logic.

* bytecompiler/NodesCodegen.cpp:
(JSC::BracketAccessorNode::emitBytecode):
(JSC::DotAccessorNode::emitBytecode):
(JSC::EvalFunctionCallNode::emitBytecode): Refactor so we can emitOptionalCheck in a single location.
(JSC::FunctionCallValueNode::emitBytecode):
(JSC::FunctionCallResolveNode::emitBytecode): Refactor so we can emitOptionalCheck in a single location.
(JSC::FunctionCallBracketNode::emitBytecode):
(JSC::FunctionCallDotNode::emitBytecode):
(JSC::CallFunctionCallDotNode::emitBytecode):
(JSC::ApplyFunctionCallDotNode::emitBytecode):
(JSC::DeleteBracketNode::emitBytecode):
(JSC::DeleteDotNode::emitBytecode):
(JSC::CoalesceNode::emitBytecode): Clean up.
(JSC::OptionalChainNode::emitBytecode): Added.
Implement ?. node and emit checks where needed.

* llint/LowLevelInterpreter32_64.asm:
* llint/LowLevelInterpreter64.asm:
Have OpIsUndefinedOrNull support constant registers.

* parser/ASTBuilder.h:
(JSC::ASTBuilder::createOptionalChain): Added.
(JSC::ASTBuilder::makeDeleteNode):
(JSC::ASTBuilder::makeFunctionCallNode):
* parser/Lexer.cpp:
(JSC::Lexer<T>::lexWithoutClearingLineTerminator):
* parser/NodeConstructors.h:
(JSC::OptionalChainNode::OptionalChainNode): Added.
* parser/Nodes.h:
(JSC::ExpressionNode::isOptionalChain const): Added.
(JSC::ExpressionNode::isOptionalChainBase const): Added.
(JSC::ExpressionNode::setIsOptionalChainBase): Added.
* parser/ParserTokens.h:
* parser/SyntaxChecker.h:
(JSC::SyntaxChecker::makeFunctionCallNode):
(JSC::SyntaxChecker::createOptionalChain): Added.
Introduce new token and AST node, as well as an ExpressionNode field to mark LHSes with.

* parser/Parser.cpp:
(JSC::Parser<LexerType>::parseMemberExpression):
Parse optional chains by wrapping the access/call parse loop.

* runtime/ExceptionHelpers.cpp:
(JSC::functionCallBase):
Ensure that TypeError messages don't include the '?.'.

* runtime/Options.h:
Update feature flag, as ?. and ?? are a double feature of "nullish-aware" operators.

Tools:

* Scripts/run-jsc-stress-tests:

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

21 files changed:
JSTests/ChangeLog
JSTests/stress/nullish-coalescing.js
JSTests/stress/optional-chaining.js [new file with mode: 0644]
JSTests/stress/tail-call-recognize.js
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h
Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp
Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
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/SyntaxChecker.h
Source/JavaScriptCore/runtime/ExceptionHelpers.cpp
Source/JavaScriptCore/runtime/Options.h
Tools/ChangeLog
Tools/Scripts/run-jsc-stress-tests

index c75e5a8..36e8fd4 100644 (file)
@@ -1,5 +1,16 @@
 2019-08-17  Ross Kirsling  <ross.kirsling@sony.com>
 
+        [ESNext] Implement optional chaining
+        https://bugs.webkit.org/show_bug.cgi?id=200199
+
+        Reviewed by Yusuke Suzuki.
+
+        * stress/nullish-coalescing.js:
+        * stress/optional-chaining.js: Added.
+        * stress/tail-call-recognize.js:
+
+2019-08-17  Ross Kirsling  <ross.kirsling@sony.com>
+
         [ESNext] Support hashbang.
         https://bugs.webkit.org/show_bug.cgi?id=200865
 
index f3ddca7..8cbad44 100644 (file)
@@ -1,4 +1,4 @@
-//@ runNullishCoalescingEnabled
+//@ runNullishAwareOperatorsEnabled
 
 function shouldBe(actual, expected) {
     if (actual !== expected)
diff --git a/JSTests/stress/optional-chaining.js b/JSTests/stress/optional-chaining.js
new file mode 100644 (file)
index 0000000..2767949
--- /dev/null
@@ -0,0 +1,162 @@
+//@ runNullishAwareOperatorsEnabled
+
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`expected ${expected} but got ${actual}`);
+}
+
+function shouldThrowSyntaxError(script) {
+    let error;
+    try {
+        eval(script);
+    } catch (e) {
+        error = e;
+    }
+
+    if (!(error instanceof SyntaxError))
+        throw new Error('Expected SyntaxError!');
+}
+
+function shouldThrowTypeError(func, messagePrefix) {
+    let error;
+    try {
+        func();
+    } catch (e) {
+        error = e;
+    }
+
+    if (!(error instanceof TypeError))
+        throw new Error('Expected TypeError!');
+
+    if (!error.message.startsWith(messagePrefix))
+        throw new Error('TypeError has wrong message!');
+}
+
+const masquerader = makeMasquerader();
+masquerader.foo = 3;
+
+function testBasicCases() {
+    shouldBe(undefined?.valueOf(), undefined);
+    shouldBe(null?.valueOf(), undefined);
+    shouldBe(true?.valueOf(), true);
+    shouldBe(false?.valueOf(), false);
+    shouldBe(0?.valueOf(), 0);
+    shouldBe(1?.valueOf(), 1);
+    shouldBe(''?.valueOf(), '');
+    shouldBe('hi'?.valueOf(), 'hi');
+    shouldBe(({})?.constructor, Object);
+    shouldBe(({ x: 'hi' })?.x, 'hi');
+    shouldBe([]?.length, 0);
+    shouldBe(['hi']?.length, 1);
+    shouldBe(masquerader?.foo, 3);
+
+    shouldBe(undefined?.['valueOf'](), undefined);
+    shouldBe(null?.['valueOf'](), undefined);
+    shouldBe(true?.['valueOf'](), true);
+    shouldBe(false?.['valueOf'](), false);
+    shouldBe(0?.['valueOf'](), 0);
+    shouldBe(1?.['valueOf'](), 1);
+    shouldBe(''?.['valueOf'](), '');
+    shouldBe('hi'?.['valueOf'](), 'hi');
+    shouldBe(({})?.['constructor'], Object);
+    shouldBe(({ x: 'hi' })?.['x'], 'hi');
+    shouldBe([]?.['length'], 0);
+    shouldBe(['hi']?.[0], 'hi');
+    shouldBe(masquerader?.['foo'], 3);
+
+    shouldBe(undefined?.(), undefined);
+    shouldBe(null?.(), undefined);
+    shouldThrowTypeError(() => true?.(), 'true is not a function');
+    shouldThrowTypeError(() => false?.(), 'false is not a function');
+    shouldThrowTypeError(() => 0?.(), '0 is not a function');
+    shouldThrowTypeError(() => 1?.(), '1 is not a function');
+    shouldThrowTypeError(() => ''?.(), '\'\' is not a function');
+    shouldThrowTypeError(() => 'hi'?.(), '\'hi\' is not a function');
+    shouldThrowTypeError(() => ({})?.(), '({}) is not a function');
+    shouldThrowTypeError(() => ({ x: 'hi' })?.(), '({ x: \'hi\' }) is not a function');
+    shouldThrowTypeError(() => []?.(), '[] is not a function');
+    shouldThrowTypeError(() => ['hi']?.(), '[\'hi\'] is not a function');
+    shouldThrowTypeError(() => masquerader?.(), 'masquerader is not a function');
+}
+noInline(testBasicCases);
+
+for (let i = 0; i < 1e5; i++)
+    testBasicCases();
+
+shouldThrowTypeError(() => ({})?.i(), '({})?.i is not a function');
+shouldBe(({}).i?.(), undefined);
+shouldBe(({})?.i?.(), undefined);
+shouldThrowTypeError(() => ({})?.['i'](), '({})?.[\'i\'] is not a function');
+shouldBe(({})['i']?.(), undefined);
+shouldBe(({})?.['i']?.(), undefined);
+
+shouldThrowTypeError(() => ({})?.a['b'], 'undefined is not an object');
+shouldBe(({})?.a?.['b'], undefined);
+shouldBe(null?.a['b']().c, undefined);
+shouldThrowTypeError(() => ({})?.['a'].b, 'undefined is not an object');
+shouldBe(({})?.['a']?.b, undefined);
+shouldBe(null?.['a'].b()['c'], undefined);
+shouldThrowTypeError(() => (() => {})?.()(), '(() => {})?.() is not a function');
+shouldBe((() => {})?.()?.(), undefined);
+shouldBe(null?.()().a['b'], undefined);
+
+const o0 = { a: { b() { return this._b.bind(this); }, _b() { return this.__b; }, __b: { c: 42 } } };
+shouldBe(o0?.a?.['b']?.()?.()?.c, 42);
+shouldBe(o0?.i?.['j']?.()?.()?.k, undefined);
+shouldBe((o0.a?._b)?.().c, 42);
+shouldBe((o0.a?._b)().c, 42);
+
+shouldBe(({ undefined: 3 })?.[null?.a], 3);
+shouldBe((() => 3)?.(null?.a), 3);
+
+const o1 = { count: 0, get x() { this.count++; return () => {}; } };
+o1.x?.y;
+shouldBe(o1.count, 1);
+o1.x?.['y'];
+shouldBe(o1.count, 2);
+o1.x?.();
+shouldBe(o1.count, 3);
+null?.(o1.x);
+shouldBe(o1.count, 3);
+
+shouldBe(delete undefined?.foo, true);
+shouldBe(delete null?.foo, true);
+shouldBe(delete undefined?.['foo'], true);
+shouldBe(delete null?.['foo'], true);
+shouldBe(delete undefined?.(), true);
+shouldBe(delete null?.(), true);
+
+const o2 = { x: 0, y: 0, z() {} };
+shouldBe(delete o2?.x, true);
+shouldBe(o2.x, undefined);
+shouldBe(delete o2?.x, true);
+shouldBe(delete o2?.['y'], true);
+shouldBe(o2.y, undefined);
+shouldBe(delete o2?.['y'], true);
+shouldBe(delete o2.z?.(), true);
+
+function greet(name) { return `hey, ${name}${this.suffix ?? '.'}`; }
+shouldBe(eval?.('greet("world")'), 'hey, world.');
+shouldBe(greet?.call({ suffix: '!' }, 'world'), 'hey, world!');
+shouldBe(greet.call?.({ suffix: '!' }, 'world'), 'hey, world!');
+shouldBe(null?.call({ suffix: '!' }, 'world'), undefined);
+shouldBe(({}).call?.({ suffix: '!' }, 'world'), undefined);
+shouldBe(greet?.apply({ suffix: '?' }, ['world']), 'hey, world?');
+shouldBe(greet.apply?.({ suffix: '?' }, ['world']), 'hey, world?');
+shouldBe(null?.apply({ suffix: '?' }, ['world']), undefined);
+shouldBe(({}).apply?.({ suffix: '?' }, ['world']), undefined);
+
+shouldThrowSyntaxError('class C {} class D extends C { foo() { return super?.bar; } }');
+shouldThrowSyntaxError('class C {} class D extends C { foo() { return super?.["bar"]; } }');
+shouldThrowSyntaxError('class C {} class D extends C { constructor() { super?.(); } }');
+
+shouldThrowSyntaxError('const o = { C: class {} }; new o?.C();')
+shouldThrowSyntaxError('const o = { C: class {} }; new o?.["C"]();')
+shouldThrowSyntaxError('class C {} new C?.();')
+shouldThrowSyntaxError('function foo() { new?.target; }');
+
+shouldThrowSyntaxError('function tag() {} tag?.``;');
+shouldThrowSyntaxError('const o = { tag() {} }; o?.tag``;');
+
+// NOT an optional chain
+shouldBe(false?.4:5, 5);
index 1681e40..9cc0e30 100644 (file)
@@ -1,3 +1,5 @@
+//@ runNullishAwareOperatorsEnabled
+
 function callerMustBeRun() {
     if (!Object.is(callerMustBeRun.caller, runTests))
         throw new Error("Wrong caller, expected run but got ", callerMustBeRun.caller);
@@ -150,6 +152,11 @@ function runTests() {
         return false || callerMustBeRun();
     })();
 
+    (function tailCallCoalesce() {
+        "use strict";
+        return false ?? callerMustBeRun();
+    })();
+
     (function memberTailCall() {
         "use strict";
         return { f: callerMustBeRun }.f();
@@ -160,6 +167,11 @@ function runTests() {
         return callerMustBeRun.bind()();
     })();
 
+    (function optionalTailCall() {
+        "use strict";
+        return callerMustBeRun?.();
+    })();
+
     // Function.prototype tests
 
     (function applyTailCall() {
index 2c7d3f2..042eb39 100644 (file)
@@ -1,5 +1,81 @@
 2019-08-17  Ross Kirsling  <ross.kirsling@sony.com>
 
+        [ESNext] Implement optional chaining
+        https://bugs.webkit.org/show_bug.cgi?id=200199
+
+        Reviewed by Yusuke Suzuki.
+
+        Implement the optional chaining proposal, which has now reached Stage 3 at TC39.
+
+        This introduces a ?. operator which:
+         - guards member access when the LHS is nullish, i.e. `null?.foo` and `null?.['foo']` are undefined
+         - guards function calls when the LHS is nullish, i.e. `null?.()` is undefined
+         - short-circuits over a whole access/call chain, i.e. `null?.a['b'](c++)` is undefined and does not increment c
+
+        This feature can be naively viewed as a ternary in disguise, i.e. `a?.b` is like `a == null ? undefined : a.b`.
+        However, since we must be sure not to double-evaluate the LHS, it's actually rather akin to a try block --
+        namely, we have the bytecode generator keep an early-out label for use throughout the access and call chain.
+
+        (Also note that document.all behaves as an object, so "nullish" means *strictly* equal to null or undefined.)
+
+        * bytecompiler/BytecodeGenerator.cpp:
+        (JSC::BytecodeGenerator::pushOptionalChainTarget): Added.
+        (JSC::BytecodeGenerator::popOptionalChainTarget): Added.
+        (JSC::BytecodeGenerator::emitOptionalCheck): Added.
+        * bytecompiler/BytecodeGenerator.h:
+        Implement early-out logic.
+
+        * bytecompiler/NodesCodegen.cpp:
+        (JSC::BracketAccessorNode::emitBytecode):
+        (JSC::DotAccessorNode::emitBytecode):
+        (JSC::EvalFunctionCallNode::emitBytecode): Refactor so we can emitOptionalCheck in a single location.
+        (JSC::FunctionCallValueNode::emitBytecode):
+        (JSC::FunctionCallResolveNode::emitBytecode): Refactor so we can emitOptionalCheck in a single location.
+        (JSC::FunctionCallBracketNode::emitBytecode):
+        (JSC::FunctionCallDotNode::emitBytecode):
+        (JSC::CallFunctionCallDotNode::emitBytecode):
+        (JSC::ApplyFunctionCallDotNode::emitBytecode):
+        (JSC::DeleteBracketNode::emitBytecode):
+        (JSC::DeleteDotNode::emitBytecode):
+        (JSC::CoalesceNode::emitBytecode): Clean up.
+        (JSC::OptionalChainNode::emitBytecode): Added.
+        Implement ?. node and emit checks where needed.
+
+        * llint/LowLevelInterpreter32_64.asm:
+        * llint/LowLevelInterpreter64.asm:
+        Have OpIsUndefinedOrNull support constant registers.
+
+        * parser/ASTBuilder.h:
+        (JSC::ASTBuilder::createOptionalChain): Added.
+        (JSC::ASTBuilder::makeDeleteNode):
+        (JSC::ASTBuilder::makeFunctionCallNode):
+        * parser/Lexer.cpp:
+        (JSC::Lexer<T>::lexWithoutClearingLineTerminator):
+        * parser/NodeConstructors.h:
+        (JSC::OptionalChainNode::OptionalChainNode): Added.
+        * parser/Nodes.h:
+        (JSC::ExpressionNode::isOptionalChain const): Added.
+        (JSC::ExpressionNode::isOptionalChainBase const): Added.
+        (JSC::ExpressionNode::setIsOptionalChainBase): Added.
+        * parser/ParserTokens.h:
+        * parser/SyntaxChecker.h:
+        (JSC::SyntaxChecker::makeFunctionCallNode):
+        (JSC::SyntaxChecker::createOptionalChain): Added.
+        Introduce new token and AST node, as well as an ExpressionNode field to mark LHSes with.
+
+        * parser/Parser.cpp:
+        (JSC::Parser<LexerType>::parseMemberExpression):
+        Parse optional chains by wrapping the access/call parse loop.
+
+        * runtime/ExceptionHelpers.cpp:
+        (JSC::functionCallBase):
+        Ensure that TypeError messages don't include the '?.'.
+
+        * runtime/Options.h:
+        Update feature flag, as ?. and ?? are a double feature of "nullish-aware" operators.
+
+2019-08-17  Ross Kirsling  <ross.kirsling@sony.com>
+
         [ESNext] Support hashbang.
         https://bugs.webkit.org/show_bug.cgi?id=200865
 
index 2c30a4d..950aaa2 100644 (file)
@@ -4964,6 +4964,30 @@ void BytecodeGenerator::emitJumpIf(RegisterID* completionTypeRegister, Completio
     emitJumpIfTrue(equivalenceResult, jumpTarget);
 }
 
+void BytecodeGenerator::pushOptionalChainTarget()
+{
+    m_optionalChainTargetStack.append(newLabel());
+}
+
+void BytecodeGenerator::popOptionalChainTarget(RegisterID* dst, bool isDelete)
+{
+    ASSERT(m_optionalChainTargetStack.size());
+
+    Ref<Label> endLabel = newLabel();
+    emitJump(endLabel.get());
+
+    emitLabel(m_optionalChainTargetStack.takeLast().get());
+    emitLoad(dst, isDelete ? jsBoolean(true) : jsUndefined());
+
+    emitLabel(endLabel.get());
+}
+
+void BytecodeGenerator::emitOptionalCheck(RegisterID* src)
+{
+    ASSERT(m_optionalChainTargetStack.size());
+    emitJumpIfTrue(emitIsUndefinedOrNull(newTemporary(), src), m_optionalChainTargetStack.last().get());
+}
+
 void ForInContext::finalize(BytecodeGenerator& generator, UnlinkedCodeBlock* codeBlock, unsigned bodyBytecodeEndOffset)
 {
     // Lexically invalidating ForInContexts is kind of weak sauce, but it only occurs if
index e97686a..3cb33c3 100644 (file)
@@ -959,6 +959,10 @@ namespace JSC {
         void pushFinallyControlFlowScope(FinallyContext&);
         void popFinallyControlFlowScope();
 
+        void pushOptionalChainTarget();
+        void popOptionalChainTarget(RegisterID* dst, bool isDelete = false);
+        void emitOptionalCheck(RegisterID* src);
+
         void pushIndexedForInScope(RegisterID* local, RegisterID* index);
         void popIndexedForInScope(RegisterID* local);
         void pushStructureForInScope(RegisterID* local, RegisterID* index, RegisterID* property, RegisterID* enumerator);
@@ -1266,6 +1270,8 @@ namespace JSC {
         Vector<TryRange> m_tryRanges;
         SegmentedVector<TryData, 8> m_tryData;
 
+        Vector<Ref<Label>> m_optionalChainTargetStack;
+
         int m_nextConstantOffset { 0 };
 
         typedef HashMap<FunctionMetadataNode*, unsigned> FunctionOffsetMap;
index 7cc078c..862f0e5 100644 (file)
@@ -744,12 +744,18 @@ RegisterID* BracketAccessorNode::emitBytecode(BytecodeGenerator& generator, Regi
     RegisterID* ret;
     RefPtr<RegisterID> finalDest = generator.finalDestination(dst);
 
-    if (isNonIndexStringElement(*m_subscript)) {
-        RefPtr<RegisterID> base = generator.emitNode(m_base);
+    bool subscriptIsNonIndexString = isNonIndexStringElement(*m_subscript);
+    RefPtr<RegisterID> base = subscriptIsNonIndexString
+        ? generator.emitNode(m_base)
+        : generator.emitNodeForLeftHandSide(m_base, m_subscriptHasAssignments, m_subscript->isPure(generator));
+
+    if (m_base->isOptionalChainBase())
+        generator.emitOptionalCheck(base.get());
+
+    if (subscriptIsNonIndexString) {
         generator.emitExpressionInfo(divot(), divotStart(), divotEnd());
         ret = generator.emitGetById(finalDest.get(), base.get(), static_cast<StringNode*>(m_subscript)->value());
     } else {
-        RefPtr<RegisterID> base = generator.emitNodeForLeftHandSide(m_base, m_subscriptHasAssignments, m_subscript->isPure(generator));
         RegisterID* property = generator.emitNodeForProperty(m_subscript);
         generator.emitExpressionInfo(divot(), divotStart(), divotEnd());
         ret = generator.emitGetByVal(finalDest.get(), base.get(), property);
@@ -763,17 +769,26 @@ RegisterID* BracketAccessorNode::emitBytecode(BytecodeGenerator& generator, Regi
 
 RegisterID* DotAccessorNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
 {
+    RefPtr<RegisterID> finalDest = generator.finalDestination(dst);
     bool baseIsSuper = m_base->isSuperNode();
-    RefPtr<RegisterID> base = baseIsSuper ? emitSuperBaseForCallee(generator) : generator.emitNode(m_base);
+
+    RefPtr<RegisterID> base;
+    if (baseIsSuper)
+        base = emitSuperBaseForCallee(generator);
+    else {
+        base = generator.emitNode(m_base);
+        if (m_base->isOptionalChainBase())
+            generator.emitOptionalCheck(base.get());
+    }
+
     generator.emitExpressionInfo(divot(), divotStart(), divotEnd());
-    RegisterID* finalDest = generator.finalDestination(dst);
     RegisterID* ret;
     if (baseIsSuper) {
         RefPtr<RegisterID> thisValue = generator.ensureThis();
-        ret = generator.emitGetById(finalDest, base.get(), thisValue.get(), m_ident);
+        ret = generator.emitGetById(finalDest.get(), base.get(), thisValue.get(), m_ident);
     } else
-        ret = generator.emitGetById(finalDest, base.get(), m_ident);
-    generator.emitProfileType(finalDest, divotStart(), divotEnd());
+        ret = generator.emitGetById(finalDest.get(), base.get(), m_ident);
+    generator.emitProfileType(finalDest.get(), divotStart(), divotEnd());
     return ret;
 }
 
@@ -848,24 +863,32 @@ RegisterID* EvalFunctionCallNode::emitBytecode(BytecodeGenerator& generator, Reg
         generator.emitLoadThisFromArrowFunctionLexicalEnvironment();
 
     Variable var = generator.variable(generator.propertyNames().eval);
-    if (RegisterID* local = var.local()) {
-        generator.emitTDZCheckIfNecessary(var, local, nullptr);
-        RefPtr<RegisterID> func = generator.move(generator.tempDestination(dst), local);
-        CallArguments callArguments(generator, m_args);
+    RefPtr<RegisterID> local = var.local();
+    RefPtr<RegisterID> func;
+    if (local) {
+        generator.emitTDZCheckIfNecessary(var, local.get(), nullptr);
+        func = generator.move(generator.tempDestination(dst), local.get());
+    } else
+        func = generator.newTemporary();
+    CallArguments callArguments(generator, m_args);
+
+    if (local)
         generator.emitLoad(callArguments.thisRegister(), jsUndefined());
-        return generator.emitCallEval(generator.finalDestination(dst, func.get()), func.get(), callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::No);
+    else {
+        JSTextPosition newDivot = divotStart() + 4;
+        generator.emitExpressionInfo(newDivot, divotStart(), newDivot);
+        generator.move(
+            callArguments.thisRegister(),
+            generator.emitResolveScope(callArguments.thisRegister(), var));
+        generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound);
+        generator.emitTDZCheckIfNecessary(var, func.get(), nullptr);
     }
 
-    RefPtr<RegisterID> func = generator.newTemporary();
-    CallArguments callArguments(generator, m_args);
-    JSTextPosition newDivot = divotStart() + 4;
-    generator.emitExpressionInfo(newDivot, divotStart(), newDivot);
-    generator.move(
-        callArguments.thisRegister(),
-        generator.emitResolveScope(callArguments.thisRegister(), var));
-    generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound);
-    generator.emitTDZCheckIfNecessary(var, func.get(), nullptr);
-    return generator.emitCallEval(generator.finalDestination(dst, func.get()), func.get(), callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::No);
+    RefPtr<RegisterID> returnValue = generator.finalDestination(dst, func.get());
+    if (isOptionalChainBase())
+        generator.emitOptionalCheck(func.get());
+
+    return generator.emitCallEval(returnValue.get(), func.get(), callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::No);
 }
 
 // ------------------------------ FunctionCallValueNode ----------------------------------
@@ -899,8 +922,12 @@ RegisterID* FunctionCallValueNode::emitBytecode(BytecodeGenerator& generator, Re
         
         return ret;
     }
+
     RefPtr<RegisterID> func = generator.emitNode(m_expr);
     RefPtr<RegisterID> returnValue = generator.finalDestination(dst, func.get());
+    if (isOptionalChainBase())
+        generator.emitOptionalCheck(func.get());
+
     CallArguments callArguments(generator, m_args);
     generator.emitLoad(callArguments.thisRegister(), jsUndefined());
     RegisterID* ret = generator.emitCallInTailPosition(returnValue.get(), func.get(), NoExpectedFunction, callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::Yes);
@@ -920,30 +947,34 @@ RegisterID* FunctionCallResolveNode::emitBytecode(BytecodeGenerator& generator,
     ExpectedFunction expectedFunction = generator.expectedFunctionForIdentifier(m_ident);
 
     Variable var = generator.variable(m_ident);
-    if (RegisterID* local = var.local()) {
-        generator.emitTDZCheckIfNecessary(var, local, nullptr);
-        RefPtr<RegisterID> func = generator.move(generator.tempDestination(dst), local);
-        RefPtr<RegisterID> returnValue = generator.finalDestination(dst, func.get());
-        CallArguments callArguments(generator, m_args);
+    RefPtr<RegisterID> local = var.local();
+    RefPtr<RegisterID> func;
+    if (local) {
+        generator.emitTDZCheckIfNecessary(var, local.get(), nullptr);
+        func = generator.move(generator.tempDestination(dst), local.get());
+    } else
+        func = generator.newTemporary();
+    CallArguments callArguments(generator, m_args);
+
+    if (local) {
         generator.emitLoad(callArguments.thisRegister(), jsUndefined());
         // This passes NoExpectedFunction because we expect that if the function is in a
         // local variable, then it's not one of our built-in constructors.
-        RegisterID* ret = generator.emitCallInTailPosition(returnValue.get(), func.get(), NoExpectedFunction, callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::Yes);
-        generator.emitProfileType(returnValue.get(), divotStart(), divotEnd());
-        return ret;
+        expectedFunction = NoExpectedFunction;
+    } else {
+        JSTextPosition newDivot = divotStart() + m_ident.length();
+        generator.emitExpressionInfo(newDivot, divotStart(), newDivot);
+        generator.move(
+            callArguments.thisRegister(),
+            generator.emitResolveScope(callArguments.thisRegister(), var));
+        generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound);
+        generator.emitTDZCheckIfNecessary(var, func.get(), nullptr);
     }
 
-    RefPtr<RegisterID> func = generator.newTemporary();
     RefPtr<RegisterID> returnValue = generator.finalDestination(dst, func.get());
-    CallArguments callArguments(generator, m_args);
+    if (isOptionalChainBase())
+        generator.emitOptionalCheck(func.get());
 
-    JSTextPosition newDivot = divotStart() + m_ident.length();
-    generator.emitExpressionInfo(newDivot, divotStart(), newDivot);
-    generator.move(
-        callArguments.thisRegister(),
-        generator.emitResolveScope(callArguments.thisRegister(), var));
-    generator.emitGetFromScope(func.get(), callArguments.thisRegister(), var, ThrowIfNotFound);
-    generator.emitTDZCheckIfNecessary(var, func.get(), nullptr);
     RegisterID* ret = generator.emitCallInTailPosition(returnValue.get(), func.get(), expectedFunction, callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::Yes);
     generator.emitProfileType(returnValue.get(), divotStart(), divotEnd());
     return ret;
@@ -1275,6 +1306,8 @@ RegisterID* BytecodeIntrinsicNode::emit_intrinsic_defineEnumerableWritableConfig
 
 RegisterID* FunctionCallBracketNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
 {
+    RefPtr<RegisterID> function = generator.tempDestination(dst);
+    RefPtr<RegisterID> returnValue = generator.finalDestination(dst, function.get());
     bool baseIsSuper = m_base->isSuperNode();
     bool subscriptIsNonIndexString = isNonIndexStringElement(*m_subscript);
 
@@ -1286,9 +1319,11 @@ RegisterID* FunctionCallBracketNode::emitBytecode(BytecodeGenerator& generator,
             base = generator.emitNode(m_base);
         else
             base = generator.emitNodeForLeftHandSide(m_base, m_subscriptHasAssignments, m_subscript->isPure(generator));
+
+        if (m_base->isOptionalChainBase())
+            generator.emitOptionalCheck(base.get());
     }
 
-    RefPtr<RegisterID> function;
     RefPtr<RegisterID> thisRegister;
     if (baseIsSuper) {
         // Note that we only need to do this once because we either have a non-TDZ this or we throw. Once we have a non-TDZ this, we can't change its value back to TDZ.
@@ -1297,19 +1332,20 @@ RegisterID* FunctionCallBracketNode::emitBytecode(BytecodeGenerator& generator,
     if (subscriptIsNonIndexString) {
         generator.emitExpressionInfo(subexpressionDivot(), subexpressionStart(), subexpressionEnd());
         if (baseIsSuper)
-            function = generator.emitGetById(generator.tempDestination(dst), base.get(), thisRegister.get(), static_cast<StringNode*>(m_subscript)->value());
+            generator.emitGetById(function.get(), base.get(), thisRegister.get(), static_cast<StringNode*>(m_subscript)->value());
         else
-            function = generator.emitGetById(generator.tempDestination(dst), base.get(), static_cast<StringNode*>(m_subscript)->value());
+            generator.emitGetById(function.get(), base.get(), static_cast<StringNode*>(m_subscript)->value());
     } else {
         RefPtr<RegisterID> property = generator.emitNodeForProperty(m_subscript);
         generator.emitExpressionInfo(subexpressionDivot(), subexpressionStart(), subexpressionEnd());
         if (baseIsSuper)
-            function = generator.emitGetByVal(generator.tempDestination(dst), base.get(), thisRegister.get(), property.get());
+            generator.emitGetByVal(function.get(), base.get(), thisRegister.get(), property.get());
         else
-            function = generator.emitGetByVal(generator.tempDestination(dst), base.get(), property.get());
+            generator.emitGetByVal(function.get(), base.get(), property.get());
     }
+    if (isOptionalChainBase())
+        generator.emitOptionalCheck(function.get());
 
-    RefPtr<RegisterID> returnValue = generator.finalDestination(dst, function.get());
     CallArguments callArguments(generator, m_args);
     if (baseIsSuper) {
         generator.emitTDZCheck(generator.thisRegister());
@@ -1331,14 +1367,21 @@ RegisterID* FunctionCallDotNode::emitBytecode(BytecodeGenerator& generator, Regi
     bool baseIsSuper = m_base->isSuperNode();
     if (baseIsSuper)
         generator.move(callArguments.thisRegister(), generator.ensureThis());
-    else
+    else {
         generator.emitNode(callArguments.thisRegister(), m_base);
+        if (m_base->isOptionalChainBase())
+            generator.emitOptionalCheck(callArguments.thisRegister());
+    }
     generator.emitExpressionInfo(subexpressionDivot(), subexpressionStart(), subexpressionEnd());
     if (baseIsSuper) {
         RefPtr<RegisterID> superBase = emitSuperBaseForCallee(generator);
         generator.emitGetById(function.get(), superBase.get(), callArguments.thisRegister(), m_ident);
     } else
         generator.emitGetById(function.get(), callArguments.thisRegister(), m_ident);
+
+    if (isOptionalChainBase())
+        generator.emitOptionalCheck(function.get());
+
     RegisterID* ret = generator.emitCallInTailPosition(returnValue.get(), function.get(), NoExpectedFunction, callArguments, divot(), divotStart(), divotEnd(), DebuggableCall::Yes);
     generator.emitProfileType(returnValue.get(), divotStart(), divotEnd());
     return ret;
@@ -1348,17 +1391,24 @@ static constexpr size_t maxDistanceToInnermostCallOrApply = 2;
 
 RegisterID* CallFunctionCallDotNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
 {
+    RefPtr<RegisterID> returnValue = generator.finalDestination(dst);
     RefPtr<RegisterID> base = generator.emitNode(m_base);
+
+    if (m_base->isOptionalChainBase())
+        generator.emitOptionalCheck(base.get());
+
     generator.emitExpressionInfo(subexpressionDivot(), subexpressionStart(), subexpressionEnd());
-    RefPtr<RegisterID> function;
-    RefPtr<RegisterID> returnValue = generator.finalDestination(dst);
 
+    RefPtr<RegisterID> function;
     auto makeFunction = [&] {
         if (m_base->isSuperNode()) {
             RefPtr<RegisterID> thisValue = generator.ensureThis();
             function = generator.emitGetById(generator.tempDestination(dst), base.get(), thisValue.get(), generator.propertyNames().builtinNames().callPublicName());
         } else
             function = generator.emitGetById(generator.tempDestination(dst), base.get(), generator.propertyNames().builtinNames().callPublicName());
+
+        if (isOptionalChainBase())
+            generator.emitOptionalCheck(function.get());
     };
 
     bool emitCallCheck = !generator.isBuiltinFunction();
@@ -1430,15 +1480,22 @@ RegisterID* ApplyFunctionCallDotNode::emitBytecode(BytecodeGenerator& generator,
     // function.apply(thisArg, [arg0, arg1, ...]) -> can be trivially coerced into function.call(thisArg, arg0, arg1, ...) and saves object allocation
     bool mayBeCall = areTrivialApplyArguments(m_args);
 
-    RefPtr<RegisterID> function;
-    RefPtr<RegisterID> base = generator.emitNode(m_base);
     RefPtr<RegisterID> returnValue = generator.finalDestination(dst);
+    RefPtr<RegisterID> base = generator.emitNode(m_base);
+
+    if (m_base->isOptionalChainBase())
+        generator.emitOptionalCheck(base.get());
+
+    RefPtr<RegisterID> function;
     auto makeFunction = [&] {
         if (m_base->isSuperNode()) {
             RefPtr<RegisterID> thisValue = generator.ensureThis();
             function = generator.emitGetById(generator.tempDestination(dst), base.get(), thisValue.get(), generator.propertyNames().builtinNames().applyPublicName());
         } else
             function = generator.emitGetById(generator.tempDestination(dst), base.get(), generator.propertyNames().builtinNames().applyPublicName());
+
+        if (isOptionalChainBase())
+            generator.emitOptionalCheck(function.get());
     };
 
     bool emitCallCheck = !generator.isBuiltinFunction();
@@ -1692,25 +1749,33 @@ RegisterID* DeleteResolveNode::emitBytecode(BytecodeGenerator& generator, Regist
 
 RegisterID* DeleteBracketNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
 {
+    RefPtr<RegisterID> finalDest = generator.finalDestination(dst);
     RefPtr<RegisterID> r0 = generator.emitNode(m_base);
-    RefPtr<RegisterID> r1 = generator.emitNode(m_subscript);
 
+    if (m_base->isOptionalChainBase())
+        generator.emitOptionalCheck(r0.get());
+
+    RefPtr<RegisterID> r1 = generator.emitNode(m_subscript);
     generator.emitExpressionInfo(divot(), divotStart(), divotEnd());
     if (m_base->isSuperNode())
         return emitThrowReferenceError(generator, "Cannot delete a super property");
-    return generator.emitDeleteByVal(generator.finalDestination(dst), r0.get(), r1.get());
+    return generator.emitDeleteByVal(finalDest.get(), r0.get(), r1.get());
 }
 
 // ------------------------------ DeleteDotNode -----------------------------------
 
 RegisterID* DeleteDotNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
 {
+    RefPtr<RegisterID> finalDest = generator.finalDestination(dst);
     RefPtr<RegisterID> r0 = generator.emitNode(m_base);
 
+    if (m_base->isOptionalChainBase())
+        generator.emitOptionalCheck(r0.get());
+
     generator.emitExpressionInfo(divot(), divotStart(), divotEnd());
     if (m_base->isSuperNode())
         return emitThrowReferenceError(generator, "Cannot delete a super property");
-    return generator.emitDeleteById(generator.finalDestination(dst), r0.get(), m_ident);
+    return generator.emitDeleteById(finalDest.get(), r0.get(), m_ident);
 }
 
 // ------------------------------ DeleteValueNode -----------------------------------
@@ -2346,13 +2411,28 @@ RegisterID* CoalesceNode::emitBytecode(BytecodeGenerator& generator, RegisterID*
     Ref<Label> target = generator.newLabel();
 
     generator.emitNode(temp.get(), m_expr1);
-    generator.emitJumpIfFalse(generator.emitUnaryOp<OpIsUndefinedOrNull>(generator.newTemporary(), temp.get()), target.get());
+    generator.emitJumpIfFalse(generator.emitIsUndefinedOrNull(generator.newTemporary(), temp.get()), target.get());
     generator.emitNodeInTailPosition(temp.get(), m_expr2);
     generator.emitLabel(target.get());
 
     return generator.move(dst, temp.get());
 }
 
+// ------------------------------ OptionalChainNode ----------------------------
+
+RegisterID* OptionalChainNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
+{
+    RefPtr<RegisterID> finalDest = generator.finalDestination(dst);
+
+    if (m_isOutermost)
+        generator.pushOptionalChainTarget();
+    generator.emitNodeInTailPosition(finalDest.get(), m_expr);
+    if (m_isOutermost)
+        generator.popOptionalChainTarget(finalDest.get(), m_isDelete);
+
+    return finalDest.get();
+}
+
 // ------------------------------ ConditionalNode ------------------------------
 
 RegisterID* ConditionalNode::emitBytecode(BytecodeGenerator& generator, RegisterID* dst)
index ac4159b..55006d4 100644 (file)
@@ -875,8 +875,7 @@ equalNullComparisonOp(op_neq_null, OpNeqNull,
 
 llintOpWithReturn(op_is_undefined_or_null, OpIsUndefinedOrNull, macro (size, get, dispatch, return)
     get(m_operand, t0)
-    assertNotConstant(size, t0)
-    loadi TagOffset[cfr, t0, 8], t1
+    loadConstantOrVariableTag(size, t0, t1)
     ori 1, t1
     cieq t1, NullTag, t1
     return(BooleanTag, t1)
index e09f898..1534ea8 100644 (file)
@@ -827,8 +827,8 @@ equalNullComparisonOp(op_neq_null, OpNeqNull,
 
 
 llintOpWithReturn(op_is_undefined_or_null, OpIsUndefinedOrNull, macro (size, get, dispatch, return)
-    get(m_operand, t0)
-    loadq [cfr, t0, 8], t0
+    get(m_operand, t1)
+    loadConstantOrVariable(size, t1, t0)
     andq ~TagBitUndefined, t0
     cqeq t0, ValueNull, t0
     orq ValueFalse, t0
index de5b133..3b751ed 100644 (file)
@@ -129,7 +129,7 @@ public:
     static const int  DontBuildStrings = 0;
 
     ExpressionNode* makeBinaryNode(const JSTokenLocation&, int token, std::pair<ExpressionNode*, BinaryOpInfo>, std::pair<ExpressionNode*, BinaryOpInfo>);
-    ExpressionNode* makeFunctionCallNode(const JSTokenLocation&, ExpressionNode* func, bool previousBaseWasSuper, ArgumentsNode* args, const JSTextPosition& divotStart, const JSTextPosition& divot, const JSTextPosition& divotEnd, size_t callOrApplyChildDepth);
+    ExpressionNode* makeFunctionCallNode(const JSTokenLocation&, ExpressionNode* func, bool previousBaseWasSuper, ArgumentsNode* args, const JSTextPosition& divotStart, const JSTextPosition& divot, const JSTextPosition& divotEnd, size_t callOrApplyChildDepth, bool isOptionalCall);
 
     JSC::SourceElements* createSourceElements() { return new (m_parserArena) JSC::SourceElements(); }
 
@@ -360,6 +360,12 @@ public:
         return node;
     }
 
+    ExpressionNode* createOptionalChain(const JSTokenLocation& location, ExpressionNode* base, ExpressionNode* expr, bool isOutermost)
+    {
+        base->setIsOptionalChainBase();
+        return new (m_parserArena) OptionalChainNode(location, expr, isOutermost);
+    }
+
     ExpressionNode* createConditionalExpr(const JSTokenLocation& location, ExpressionNode* condition, ExpressionNode* lhs, ExpressionNode* rhs)
     {
         return new (m_parserArena) ConditionalNode(location, condition, lhs, rhs);
@@ -1148,6 +1154,16 @@ ExpressionNode* ASTBuilder::makeTypeOfNode(const JSTokenLocation& location, Expr
 
 ExpressionNode* ASTBuilder::makeDeleteNode(const JSTokenLocation& location, ExpressionNode* expr, const JSTextPosition& start, const JSTextPosition& divot, const JSTextPosition& end)
 {
+    if (expr->isOptionalChain()) {
+        OptionalChainNode* optionalChain = static_cast<OptionalChainNode*>(expr);
+        if (optionalChain->expr()->isLocation()) {
+            ASSERT(!optionalChain->expr()->isResolveNode());
+            optionalChain->setExpr(makeDeleteNode(location, optionalChain->expr(), start, divot, end));
+            optionalChain->setIsDelete();
+            return optionalChain;
+        }
+    }
+
     if (!expr->isLocation())
         return new (m_parserArena) DeleteValueNode(location, expr);
     if (expr->isResolveNode()) {
@@ -1345,17 +1361,31 @@ ExpressionNode* ASTBuilder::makeBitXOrNode(const JSTokenLocation& location, Expr
     return new (m_parserArena) BitXOrNode(location, expr1, expr2, rightHasAssignments);
 }
 
-ExpressionNode* ASTBuilder::makeFunctionCallNode(const JSTokenLocation& location, ExpressionNode* func, bool previousBaseWasSuper, ArgumentsNode* args, const JSTextPosition& divotStart, const JSTextPosition& divot, const JSTextPosition& divotEnd, size_t callOrApplyChildDepth)
+ExpressionNode* ASTBuilder::makeFunctionCallNode(const JSTokenLocation& location, ExpressionNode* func, bool previousBaseWasSuper, ArgumentsNode* args, const JSTextPosition& divotStart, const JSTextPosition& divot, const JSTextPosition& divotEnd, size_t callOrApplyChildDepth, bool isOptionalCall)
 {
     ASSERT(divot.offset >= divot.lineStartOffset);
     if (func->isSuperNode())
         usesSuperCall();
 
     if (func->isBytecodeIntrinsicNode()) {
+        ASSERT(!isOptionalCall);
         BytecodeIntrinsicNode* intrinsic = static_cast<BytecodeIntrinsicNode*>(func);
         if (intrinsic->type() == BytecodeIntrinsicNode::Type::Constant)
             return new (m_parserArena) BytecodeIntrinsicNode(BytecodeIntrinsicNode::Type::Function, location, intrinsic->emitter(), intrinsic->identifier(), args, divot, divotStart, divotEnd);
     }
+
+    if (func->isOptionalChain()) {
+        OptionalChainNode* optionalChain = static_cast<OptionalChainNode*>(func);
+        if (optionalChain->expr()->isLocation()) {
+            ASSERT(!optionalChain->expr()->isResolveNode());
+            // We must take care to preserve our `this` value in cases like `a?.b?.()` and `(a?.b)()`, respectively.
+            if (isOptionalCall)
+                return makeFunctionCallNode(location, optionalChain->expr(), previousBaseWasSuper, args, divotStart, divot, divotEnd, callOrApplyChildDepth, isOptionalCall);  
+            optionalChain->setExpr(makeFunctionCallNode(location, optionalChain->expr(), previousBaseWasSuper, args, divotStart, divot, divotEnd, callOrApplyChildDepth, isOptionalCall));
+            return optionalChain;
+        }
+    }
+
     if (!func->isLocation())
         return new (m_parserArena) FunctionCallValueNode(location, func, args, divot, divotStart, divotEnd);
     if (func->isResolveNode()) {
index ea8ef7f..5bbf2c7 100644 (file)
@@ -2158,10 +2158,17 @@ start:
         break;
     case CharacterQuestion:
         shift();
-        if (Options::useNullishCoalescing() && m_current == '?') {
-            shift();
-            token = COALESCE;
-            break;
+        if (Options::useNullishAwareOperators()) {
+            if (m_current == '?') {
+                shift();
+                token = COALESCE;
+                break;
+            }
+            if (m_current == '.' && !isASCIIDigit(peek(1))) {
+                shift();
+                token = QUESTIONDOT;
+                break;
+            }
         }
         token = QUESTION;
         break;
index d41e3ae..d65b08b 100644 (file)
@@ -676,6 +676,13 @@ namespace JSC {
     {
     }
 
+    inline OptionalChainNode::OptionalChainNode(const JSTokenLocation& location, ExpressionNode* expr, bool isOutermost)
+        : ExpressionNode(location)
+        , m_expr(expr)
+        , m_isOutermost(isOutermost)
+    {
+    }
+
     inline ConditionalNode::ConditionalNode(const JSTokenLocation& location, ExpressionNode* logical, ExpressionNode* expr1, ExpressionNode* expr2)
         : ExpressionNode(location)
         , m_logical(logical)
index 86fdaf3..fa70acb 100644 (file)
@@ -205,6 +205,7 @@ namespace JSC {
         virtual bool isBytecodeIntrinsicNode() const { return false; }
         virtual bool isBinaryOpNode() const { return false; }
         virtual bool isFunctionCall() const { return false; }
+        virtual bool isOptionalChain() const { return false; }
 
         virtual void emitBytecodeInConditionContext(BytecodeGenerator&, Label&, Label&, FallThroughMode);
 
@@ -212,8 +213,12 @@ namespace JSC {
 
         ResultType resultDescriptor() const { return m_resultType; }
 
+        bool isOptionalChainBase() const { return m_isOptionalChainBase; }
+        void setIsOptionalChainBase() { m_isOptionalChainBase = true; }
+
     private:
         ResultType m_resultType;
+        bool m_isOptionalChainBase { false };
     };
 
     class StatementNode : public Node {
@@ -1310,12 +1315,31 @@ namespace JSC {
         CoalesceNode(const JSTokenLocation&, ExpressionNode* expr1, ExpressionNode* expr2);
 
     private:
-        RegisterID* emitBytecode(BytecodeGenerator&, RegisterID* = 0) override;
+        RegisterID* emitBytecode(BytecodeGenerator&, RegisterID* = nullptr) final;
 
         ExpressionNode* m_expr1;
         ExpressionNode* m_expr2;
     };
 
+    class OptionalChainNode final : public ExpressionNode {
+    public:
+        OptionalChainNode(const JSTokenLocation&, ExpressionNode*, bool);
+
+        void setExpr(ExpressionNode* expr) { m_expr = expr; }
+        ExpressionNode* expr() const { return m_expr; }
+
+        void setIsDelete() { m_isDelete = true; }
+
+    private:
+        RegisterID* emitBytecode(BytecodeGenerator&, RegisterID* = nullptr) final;
+
+        bool isOptionalChain() const final { return true; }
+
+        ExpressionNode* m_expr;
+        bool m_isOutermost;
+        bool m_isDelete { false };
+    };
+
     // The ternary operator, "m_logical ? m_expr1 : m_expr2"
     class ConditionalNode final : public ExpressionNode {
     public:
index 73d9aaf..fdc6b44 100644 (file)
@@ -4808,103 +4808,136 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseMemberExpres
     }
 
     failIfFalse(base, "Cannot parse base expression");
-    while (true) {
-        location = tokenLocation();
-        switch (m_token.m_type) {
-        case OPENBRACKET: {
-            m_parserState.nonTrivialExpressionCount++;
-            JSTextPosition expressionEnd = lastTokenEndPosition();
+
+    do {
+        TreeExpression optionalChainBase = 0;
+        JSTokenLocation optionalChainLocation;
+        JSTokenType type = m_token.m_type;
+
+        if (match(QUESTIONDOT)) {
+            semanticFailIfTrue(newCount, "Cannot call constructor in an optional chain");
+            semanticFailIfTrue(baseIsSuper, "Cannot use super as the base of an optional chain");
+            optionalChainBase = base;
+            optionalChainLocation = tokenLocation();
+
+            SavePoint savePoint = createSavePoint();
             next();
-            int nonLHSCount = m_parserState.nonLHSCount;
-            int initialAssignments = m_parserState.assignmentCount;
-            TreeExpression property = parseExpression(context);
-            failIfFalse(property, "Cannot parse subscript expression");
-            base = context.createBracketAccess(startLocation, base, property, initialAssignments != m_parserState.assignmentCount, expressionStart, expressionEnd, tokenEndPosition());
-            
-            if (UNLIKELY(baseIsSuper && currentScope()->isArrowFunction()))
-                currentFunctionScope()->setInnerArrowFunctionUsesSuperProperty();
-            
-            handleProductionOrFail(CLOSEBRACKET, "]", "end", "subscript expression");
-            m_parserState.nonLHSCount = nonLHSCount;
-            break;
+            if (match(OPENBRACKET) || match(OPENPAREN) || match(BACKQUOTE))
+                type = m_token.m_type;
+            else {
+                type = DOT;
+                restoreSavePoint(savePoint);
+            }
         }
-        case OPENPAREN: {
-            m_parserState.nonTrivialExpressionCount++;
-            int nonLHSCount = m_parserState.nonLHSCount;
-            if (newCount) {
-                newCount--;
-                JSTextPosition expressionEnd = lastTokenEndPosition();
-                TreeArguments arguments = parseArguments(context);
-                failIfFalse(arguments, "Cannot parse call arguments");
-                base = context.createNewExpr(location, base, arguments, expressionStart, expressionEnd, lastTokenEndPosition());
-            } else {
-                size_t usedVariablesSize = currentScope()->currentUsedVariablesSize();
+
+        while (true) {
+            location = tokenLocation();
+            switch (type) {
+            case OPENBRACKET: {
+                m_parserState.nonTrivialExpressionCount++;
                 JSTextPosition expressionEnd = lastTokenEndPosition();
-                Optional<CallOrApplyDepthScope> callOrApplyDepthScope;
-                recordCallOrApplyDepth<TreeBuilder>(this, *m_vm, callOrApplyDepthScope, base);
+                next();
+                int nonLHSCount = m_parserState.nonLHSCount;
+                int initialAssignments = m_parserState.assignmentCount;
+                TreeExpression property = parseExpression(context);
+                failIfFalse(property, "Cannot parse subscript expression");
+                base = context.createBracketAccess(startLocation, base, property, initialAssignments != m_parserState.assignmentCount, expressionStart, expressionEnd, tokenEndPosition());
 
-                TreeArguments arguments = parseArguments(context);
+                if (UNLIKELY(baseIsSuper && currentScope()->isArrowFunction()))
+                    currentFunctionScope()->setInnerArrowFunctionUsesSuperProperty();
 
-                if (baseIsAsyncKeyword && (!arguments || match(ARROWFUNCTION))) {
-                    currentScope()->revertToPreviousUsedVariables(usedVariablesSize);
-                    forceClassifyExpressionError(ErrorIndicatesAsyncArrowFunction);
-                    failDueToUnexpectedToken();
-                }
+                handleProductionOrFail(CLOSEBRACKET, "]", "end", "subscript expression");
+                m_parserState.nonLHSCount = nonLHSCount;
+                break;
+            }
+            case OPENPAREN: {
+                m_parserState.nonTrivialExpressionCount++;
+                int nonLHSCount = m_parserState.nonLHSCount;
+                if (newCount) {
+                    newCount--;
+                    JSTextPosition expressionEnd = lastTokenEndPosition();
+                    TreeArguments arguments = parseArguments(context);
+                    failIfFalse(arguments, "Cannot parse call arguments");
+                    base = context.createNewExpr(location, base, arguments, expressionStart, expressionEnd, lastTokenEndPosition());
+                } else {
+                    size_t usedVariablesSize = currentScope()->currentUsedVariablesSize();
+                    JSTextPosition expressionEnd = lastTokenEndPosition();
+                    Optional<CallOrApplyDepthScope> callOrApplyDepthScope;
+                    recordCallOrApplyDepth<TreeBuilder>(this, *m_vm, callOrApplyDepthScope, base);
+
+                    TreeArguments arguments = parseArguments(context);
 
-                failIfFalse(arguments, "Cannot parse call arguments");
-                if (baseIsSuper) {
-                    ScopeRef functionScope = currentFunctionScope();
-                    if (!functionScope->setHasDirectSuper()) {
-                        // It unnecessary to check of using super during reparsing one more time. Also it can lead to syntax error
-                        // in case of arrow function because during reparsing we don't know whether we currently parse the arrow function
-                        // inside of the constructor or method.
-                        if (!m_lexer->isReparsingFunction()) {
-                            ScopeRef closestOrdinaryFunctionScope = closestParentOrdinaryFunctionNonLexicalScope();
-                            ConstructorKind functionConstructorKind = !functionScope->isArrowFunction() && !closestOrdinaryFunctionScope->isEvalContext()
-                                ? functionScope->constructorKind()
-                                : closestOrdinaryFunctionScope->constructorKind();
-                            semanticFailIfTrue(functionConstructorKind == ConstructorKind::None, "super is not valid in this context");
-                            semanticFailIfTrue(functionConstructorKind != ConstructorKind::Extends, "super is not valid in this context");
+                    if (baseIsAsyncKeyword && (!arguments || match(ARROWFUNCTION))) {
+                        currentScope()->revertToPreviousUsedVariables(usedVariablesSize);
+                        forceClassifyExpressionError(ErrorIndicatesAsyncArrowFunction);
+                        failDueToUnexpectedToken();
+                    }
+
+                    failIfFalse(arguments, "Cannot parse call arguments");
+                    if (baseIsSuper) {
+                        ScopeRef functionScope = currentFunctionScope();
+                        if (!functionScope->setHasDirectSuper()) {
+                            // It unnecessary to check of using super during reparsing one more time. Also it can lead to syntax error
+                            // in case of arrow function because during reparsing we don't know whether we currently parse the arrow function
+                            // inside of the constructor or method.
+                            if (!m_lexer->isReparsingFunction()) {
+                                ScopeRef closestOrdinaryFunctionScope = closestParentOrdinaryFunctionNonLexicalScope();
+                                ConstructorKind functionConstructorKind = !functionScope->isArrowFunction() && !closestOrdinaryFunctionScope->isEvalContext()
+                                    ? functionScope->constructorKind()
+                                    : closestOrdinaryFunctionScope->constructorKind();
+                                semanticFailIfTrue(functionConstructorKind == ConstructorKind::None, "super is not valid in this context");
+                                semanticFailIfTrue(functionConstructorKind != ConstructorKind::Extends, "super is not valid in this context");
+                            }
                         }
+                        if (currentScope()->isArrowFunction())
+                            functionScope->setInnerArrowFunctionUsesSuperCall();
                     }
-                    if (currentScope()->isArrowFunction())
-                        functionScope->setInnerArrowFunctionUsesSuperCall();
+
+                    bool isOptionalCall = optionalChainLocation.endOffset == static_cast<unsigned>(expressionEnd.offset);
+                    base = context.makeFunctionCallNode(startLocation, base, previousBaseWasSuper, arguments, expressionStart,
+                        expressionEnd, lastTokenEndPosition(), callOrApplyDepthScope ? callOrApplyDepthScope->distanceToInnermostChild() : 0, isOptionalCall);
+
+                    if (isOptionalCall)
+                        optionalChainBase = base;
                 }
-                base = context.makeFunctionCallNode(startLocation, base, previousBaseWasSuper, arguments, expressionStart,
-                    expressionEnd, lastTokenEndPosition(), callOrApplyDepthScope ? callOrApplyDepthScope->distanceToInnermostChild() : 0);
+                m_parserState.nonLHSCount = nonLHSCount;
+                break;
             }
-            m_parserState.nonLHSCount = nonLHSCount;
-            break;
-        }
-        case DOT: {
-            m_parserState.nonTrivialExpressionCount++;
-            JSTextPosition expressionEnd = lastTokenEndPosition();
-            nextExpectIdentifier(LexerFlagsIgnoreReservedWords | TreeBuilder::DontBuildKeywords);
-            matchOrFail(IDENT, "Expected a property name after '.'");
-            base = context.createDotAccess(startLocation, base, m_token.m_data.ident, expressionStart, expressionEnd, tokenEndPosition());
-            if (UNLIKELY(baseIsSuper && currentScope()->isArrowFunction()))
-                currentFunctionScope()->setInnerArrowFunctionUsesSuperProperty();
-            next();
-            break;
-        }
-        case BACKQUOTE: {
-            semanticFailIfTrue(baseIsSuper, "Cannot use super as tag for tagged templates");
-            JSTextPosition expressionEnd = lastTokenEndPosition();
-            int nonLHSCount = m_parserState.nonLHSCount;
-            typename TreeBuilder::TemplateLiteral templateLiteral = parseTemplateLiteral(context, LexerType::RawStringsBuildMode::BuildRawStrings);
-            failIfFalse(templateLiteral, "Cannot parse template literal");
-            base = context.createTaggedTemplate(startLocation, base, templateLiteral, expressionStart, expressionEnd, lastTokenEndPosition());
-            m_parserState.nonLHSCount = nonLHSCount;
-            m_seenTaggedTemplate = true;
-            break;
-        }
-        default:
-            goto endMemberExpression;
+            case DOT: {
+                m_parserState.nonTrivialExpressionCount++;
+                JSTextPosition expressionEnd = lastTokenEndPosition();
+                nextExpectIdentifier(LexerFlagsIgnoreReservedWords | TreeBuilder::DontBuildKeywords);
+                matchOrFail(IDENT, "Expected a property name after ", optionalChainBase ? "'?.'" : "'.'");
+                base = context.createDotAccess(startLocation, base, m_token.m_data.ident, expressionStart, expressionEnd, tokenEndPosition());
+                if (UNLIKELY(baseIsSuper && currentScope()->isArrowFunction()))
+                    currentFunctionScope()->setInnerArrowFunctionUsesSuperProperty();
+                next();
+                break;
+            }
+            case BACKQUOTE: {
+                semanticFailIfTrue(optionalChainBase, "Cannot use tagged templates in an optional chain");
+                semanticFailIfTrue(baseIsSuper, "Cannot use super as tag for tagged templates");
+                JSTextPosition expressionEnd = lastTokenEndPosition();
+                int nonLHSCount = m_parserState.nonLHSCount;
+                typename TreeBuilder::TemplateLiteral templateLiteral = parseTemplateLiteral(context, LexerType::RawStringsBuildMode::BuildRawStrings);
+                failIfFalse(templateLiteral, "Cannot parse template literal");
+                base = context.createTaggedTemplate(startLocation, base, templateLiteral, expressionStart, expressionEnd, lastTokenEndPosition());
+                m_parserState.nonLHSCount = nonLHSCount;
+                m_seenTaggedTemplate = true;
+                break;
+            }
+            default:
+                goto endOfChain;
+            }
+            previousBaseWasSuper = baseIsSuper;
+            baseIsSuper = false;
+            type = m_token.m_type;
         }
-        previousBaseWasSuper = baseIsSuper;
-        baseIsSuper = false;
-    }
-endMemberExpression:
+endOfChain:
+        if (optionalChainBase)
+            base = context.createOptionalChain(optionalChainLocation, optionalChainBase, base, !match(QUESTIONDOT));
+    } while (match(QUESTIONDOT));
+
     semanticFailIfTrue(baseIsSuper, "super is not valid in this context");
     while (newCount--)
         base = context.createNewExpr(location, base, expressionStart, lastTokenEndPosition());
index 15ca5ea..72064b9 100644 (file)
@@ -136,6 +136,7 @@ enum JSTokenType {
     OREQUAL,
     DOTDOTDOT,
     ARROWFUNCTION,
+    QUESTIONDOT,
     LastUntaggedToken,
 
     // Begin tagged tokens
index 419958b..6ce73f7 100644 (file)
@@ -73,7 +73,7 @@ public:
         ResolveEvalExpr, ResolveExpr, IntegerExpr, DoubleExpr, StringExpr, BigIntExpr,
         ThisExpr, NullExpr, BoolExpr, RegExpExpr, ObjectLiteralExpr,
         FunctionExpr, ClassExpr, SuperExpr, ImportExpr, BracketExpr, DotExpr, CallExpr,
-        NewExpr, PreExpr, PostExpr, UnaryExpr, BinaryExpr,
+        NewExpr, PreExpr, PostExpr, UnaryExpr, BinaryExpr, OptionalChain,
         ConditionalExpr, AssignmentExpr, TypeofExpr,
         DeleteExpr, ArrayLiteralExpr, BindingDestructuring, RestParameter,
         ArrayDestructuring, ObjectDestructuring, SourceElementsResult,
@@ -147,7 +147,7 @@ public:
     static const unsigned DontBuildStrings = LexerFlagsDontBuildStrings;
 
     int createSourceElements() { return SourceElementsResult; }
-    ExpressionType makeFunctionCallNode(const JSTokenLocation&, int, bool, int, int, int, int, size_t) { return CallExpr; }
+    ExpressionType makeFunctionCallNode(const JSTokenLocation&, ExpressionType, bool, int, int, int, int, size_t, bool) { return CallExpr; }
     ExpressionType createCommaExpr(const JSTokenLocation&, ExpressionType expr) { return expr; }
     ExpressionType appendToCommaExpr(const JSTokenLocation&, ExpressionType& head, ExpressionType, ExpressionType next) { head = next; return next; }
     ExpressionType makeAssignNode(const JSTokenLocation&, ExpressionType, Operator, ExpressionType, bool, bool, int, int, int) { return AssignmentExpr; }
@@ -184,6 +184,7 @@ public:
     ExpressionType createRegExp(const JSTokenLocation&, const Identifier& pattern, const Identifier& flags, int) { return Yarr::hasError(Yarr::checkSyntax(pattern.string(), flags.string())) ? 0 : RegExpExpr; }
     ExpressionType createNewExpr(const JSTokenLocation&, ExpressionType, int, int, int, int) { return NewExpr; }
     ExpressionType createNewExpr(const JSTokenLocation&, ExpressionType, int, int) { return NewExpr; }
+    ExpressionType createOptionalChain(const JSTokenLocation&, ExpressionType, ExpressionType, bool) { return OptionalChain; }
     ExpressionType createConditionalExpr(const JSTokenLocation&, ExpressionType, ExpressionType, ExpressionType) { return ConditionalExpr; }
     ExpressionType createAssignResolve(const JSTokenLocation&, const Identifier&, ExpressionType, int, int, int, AssignmentContext) { return AssignmentExpr; }
     ExpressionType createEmptyVarExpression(const JSTokenLocation&, const Identifier&) { return AssignmentExpr; }
index 156c180..9e75892 100644 (file)
@@ -176,6 +176,10 @@ static String functionCallBase(const String& sourceText)
         return String();
     }
 
+    // Don't display the ?. of an optional call.
+    if (idx > 1 && sourceText[idx] == '.' && sourceText[idx - 1] == '?')
+        idx -= 2;
+
     return sourceText.left(idx + 1);
 }
 
index e30d2d2..7c31a62 100644 (file)
@@ -499,7 +499,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, useNullishAwareOperators, false, Normal, "Enable support for ?. and ?? operators.") \
     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 efb7322..3bfc859 100644 (file)
@@ -1,3 +1,12 @@
+2019-08-17  Ross Kirsling  <ross.kirsling@sony.com>
+
+        [ESNext] Implement optional chaining
+        https://bugs.webkit.org/show_bug.cgi?id=200199
+
+        Reviewed by Yusuke Suzuki.
+
+        * Scripts/run-jsc-stress-tests:
+
 2019-08-17  Tim Horton  <timothy_horton@apple.com>
 
         Layout tests that call resizeTo() crash when run on iOS with IOSurface support enabled
index 83dc062..0987235 100755 (executable)
@@ -699,8 +699,8 @@ 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))
+def runNullishAwareOperatorsEnabled(*optionalTestSpecificOptions)
+    run("nullish-aware-operators-enabled", "--useNullishAwareOperators=true" , *(FTL_OPTIONS + optionalTestSpecificOptions))
 end
 
 def runFTLNoCJIT(*optionalTestSpecificOptions)