diff --git a/lib/Common/ConfigFlagsList.h b/lib/Common/ConfigFlagsList.h index 45339a9da1c..d431dc01832 100644 --- a/lib/Common/ConfigFlagsList.h +++ b/lib/Common/ConfigFlagsList.h @@ -673,6 +673,7 @@ PHASE(All) #define DEFAULT_CONFIG_ESArrayFindFromLast (true) #define DEFAULT_CONFIG_ESPromiseAny (true) #define DEFAULT_CONFIG_ESNullishCoalescingOperator (true) +#define DEFAULT_CONFIG_ESOptionalChaining (true) #define DEFAULT_CONFIG_ESGlobalThis (true) // Jitting generator functions is not functional on ARM @@ -1199,6 +1200,9 @@ FLAGR(Boolean, ESNumericSeparator, "Enable Numeric Separator flag", DEFAULT_CONF // ES Nullish coalescing operator support (??) FLAGR(Boolean, ESNullishCoalescingOperator, "Enable nullish coalescing operator", DEFAULT_CONFIG_ESNullishCoalescingOperator) +// ES Optional chaining operator support (?.) +FLAGR(Boolean, ESOptionalChaining, "Enable optional chaining operator", DEFAULT_CONFIG_ESOptionalChaining) + // ES Hashbang support for interpreter directive syntax FLAGR(Boolean, ESHashbang, "Enable Hashbang syntax", DEFAULT_CONFIG_ESHashbang) diff --git a/lib/Parser/Parse.cpp b/lib/Parser/Parse.cpp index 99370b420e0..255f7597e1e 100644 --- a/lib/Parser/Parse.cpp +++ b/lib/Parser/Parse.cpp @@ -309,6 +309,7 @@ LPCWSTR Parser::GetTokenString(tokens token) case tkLParen: return _u("("); case tkLBrack: return _u("["); case tkDot: return _u("."); + case tkOptChain: return _u("?."); default: return _u("unknown token"); @@ -894,13 +895,13 @@ ParseNodeUni * Parser::CreateUniNode(OpCode nop, ParseNodePtr pnode1, charcount_ } // Create ParseNodeBin -ParseNodeBin * Parser::StaticCreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ArenaAllocator* alloc, charcount_t ichMin, charcount_t ichLim) +ParseNodeBin * Parser::StaticCreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ArenaAllocator* alloc, charcount_t ichMin, charcount_t ichLim, bool isNullPropagating) { DebugOnly(VerifyNodeSize(nop, sizeof(ParseNodeBin))); - return Anew(alloc, ParseNodeBin, nop, ichMin, ichLim, pnode1, pnode2); + return Anew(alloc, ParseNodeBin, nop, ichMin, ichLim, pnode1, pnode2, isNullPropagating); } -ParseNodeBin * Parser::CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2) +ParseNodeBin * Parser::CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, bool isNullPropagating) { Assert(!this->m_deferringAST); charcount_t ichMin; @@ -937,15 +938,15 @@ ParseNodeBin * Parser::CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodeP } } - return CreateBinNode(nop, pnode1, pnode2, ichMin, ichLim); + return CreateBinNode(nop, pnode1, pnode2, ichMin, ichLim, isNullPropagating); } ParseNodeBin * Parser::CreateBinNode(OpCode nop, ParseNodePtr pnode1, - ParseNodePtr pnode2, charcount_t ichMin, charcount_t ichLim) + ParseNodePtr pnode2, charcount_t ichMin, charcount_t ichLim, bool isNullPropagating) { Assert(!this->m_deferringAST); - ParseNodeBin * pnode = StaticCreateBinNode(nop, pnode1, pnode2, &m_nodeAllocator, ichMin, ichLim); + ParseNodeBin * pnode = StaticCreateBinNode(nop, pnode1, pnode2, &m_nodeAllocator, ichMin, ichLim, isNullPropagating); AddAstSize(sizeof(ParseNodeBin)); return pnode; } @@ -3909,6 +3910,7 @@ ParseNodePtr Parser::ParsePostfixOperators( *pfIsDotOrIndex = false; } + bool isOptionalChain = false; for (;;) { uint16 spreadArgCount = 0; @@ -3918,8 +3920,14 @@ ParseNodePtr Parser::ParsePostfixOperators( { AutoMarkInParsingArgs autoMarkInParsingArgs(this); + bool isNullPropagating = tkOptChain == this->GetScanner()->GetPrevious(); if (fInNew) { + if (isNullPropagating) + { + Error(ERRInvalidOptChainInNew); + } + ParseNodePtr pnodeArgs = ParseArgList(&callOfConstants, &spreadArgCount, &count); if (buildAST) { @@ -3972,6 +3980,11 @@ ParseNodePtr Parser::ParsePostfixOperators( // Detect super() if (this->NodeIsSuperName(pnode)) { + if (isNullPropagating) + { + Error(ERRInvalidOptChainInSuper); + } + pnode = CreateSuperCallNode(pnode->AsParseNodeSpecialName(), pnodeArgs); Assert(pnode); @@ -3988,7 +4001,7 @@ ParseNodePtr Parser::ParsePostfixOperators( // Note: we used to leave it up to the byte code generator to detect eval calls // at global scope, but now it relies on the flag the parser sets, so set it here. - if (count > 0 && this->NodeIsEvalName(pnode->AsParseNodeCall()->pnodeTarget)) + if (count > 0 && this->NodeIsEvalName(pnode->AsParseNodeCall()->pnodeTarget) && !isNullPropagating) { this->MarkEvalCaller(); fCallIsEval = true; @@ -4006,6 +4019,7 @@ ParseNodePtr Parser::ParsePostfixOperators( pnode->AsParseNodeCall()->isApplyCall = false; pnode->AsParseNodeCall()->isEvalCall = fCallIsEval; pnode->AsParseNodeCall()->hasDestructuring = m_hasDestructuringPattern; + pnode->AsParseNodeCall()->isNullPropagating = isNullPropagating; Assert(!m_hasDestructuringPattern || count > 0); pnode->AsParseNodeCall()->argCount = count; pnode->ichLim = this->GetScanner()->IchLimTok(); @@ -4041,7 +4055,7 @@ ParseNodePtr Parser::ParsePostfixOperators( } if (pfCanAssign) { - *pfCanAssign = fCanAssignToCallResult && + *pfCanAssign = !isOptionalChain && fCanAssignToCallResult && (m_sourceContextInfo ? !PHASE_ON_RAW(Js::EarlyErrorOnAssignToCallPhase, m_sourceContextInfo->sourceContextId, GetCurrentFunctionNode()->functionId) : !PHASE_ON1(Js::EarlyErrorOnAssignToCallPhase)); @@ -4054,6 +4068,8 @@ ParseNodePtr Parser::ParsePostfixOperators( } case tkLBrack: { + bool isNullPropagating = tkOptChain == this->GetScanner()->GetPrevious(); + this->GetScanner()->Scan(); IdentToken tok; ParseNodePtr pnodeExpr = ParseExpr(0, FALSE, TRUE, FALSE, nullptr, nullptr, nullptr, &tok); @@ -4062,12 +4078,17 @@ ParseNodePtr Parser::ParsePostfixOperators( AnalysisAssert(pnodeExpr); if (pnode && pnode->nop == knopName && pnode->AsParseNodeName()->IsSpecialName() && pnode->AsParseNodeSpecialName()->isSuper) { + if (isNullPropagating) + { + Error(ERRInvalidOptChainInSuper); + } + pnode = CreateSuperReferenceNode(knopIndex, pnode->AsParseNodeSpecialName(), pnodeExpr); pnode->AsParseNodeSuperReference()->pnodeThis = ReferenceSpecialName(wellKnownPropertyPids._this, pnode->ichMin, pnode->ichLim, true); } else { - pnode = CreateBinNode(knopIndex, pnode, pnodeExpr); + pnode = CreateBinNode(knopIndex, pnode, pnodeExpr, isNullPropagating); } AnalysisAssert(pnode); @@ -4081,7 +4102,8 @@ ParseNodePtr Parser::ParsePostfixOperators( ChkCurTok(tkRBrack, ERRnoRbrack); if (pfCanAssign) { - *pfCanAssign = TRUE; + // optional assignment not permitted + *pfCanAssign = !isOptionalChain; } if (pfIsDotOrIndex) { @@ -4163,17 +4185,41 @@ ParseNodePtr Parser::ParsePostfixOperators( } } break; - + + case tkOptChain: case tkDot: { ParseNodePtr name = nullptr; OpCode opCode = knopDot; + // We don't use separate knops for optional-chains + // Instead mark nodes as null-propagating + bool isNullPropagating = tkOptChain == m_token.tk; + if (isNullPropagating) + { + isOptionalChain = true; + } + this->GetScanner()->Scan(); if (!m_token.IsIdentifier()) { - //allow reserved words in ES5 mode - if (!(m_token.IsReservedWord())) + if (isNullPropagating) + { + // We don't need an identifier for an Index `?.[` or Call `?.(` + switch (m_token.tk) + { + case tkLParen: + case tkLBrack: + // Continue to parse function or index (loop) + // Check previous token to check for null-propagation + continue; + + case tkStrTmplBasic: + case tkStrTmplBegin: + Error(ERRInvalidOptChainWithTaggedTemplate); + } + } + else if (!(m_token.IsReservedWord())) //allow reserved words in ES5 mode { IdentifierExpectedError(m_token); } @@ -4200,12 +4246,17 @@ ParseNodePtr Parser::ParsePostfixOperators( } if (pnode && pnode->nop == knopName && pnode->AsParseNodeName()->IsSpecialName() && pnode->AsParseNodeSpecialName()->isSuper) { + if (isNullPropagating) + { + Error(ERRInvalidOptChainInSuper); + } + pnode = CreateSuperReferenceNode(opCode, pnode->AsParseNodeSpecialName(), name); pnode->AsParseNodeSuperReference()->pnodeThis = ReferenceSpecialName(wellKnownPropertyPids._this, pnode->ichMin, pnode->ichLim, true); } else { - pnode = CreateBinNode(opCode, pnode, name); + pnode = CreateBinNode(opCode, pnode, name, isNullPropagating); } } else @@ -4216,7 +4267,8 @@ ParseNodePtr Parser::ParsePostfixOperators( if (pfCanAssign) { - *pfCanAssign = TRUE; + // optional assignment not permitted + *pfCanAssign = !isOptionalChain; } if (pfIsDotOrIndex) { @@ -4230,6 +4282,11 @@ ParseNodePtr Parser::ParsePostfixOperators( case tkStrTmplBasic: case tkStrTmplBegin: { + if (isOptionalChain) + { + Error(ERRInvalidOptChainWithTaggedTemplate); + } + ParseNode* templateNode = nullptr; if (pnode != nullptr) { @@ -4258,6 +4315,11 @@ ParseNodePtr Parser::ParsePostfixOperators( break; } default: + if (buildAST && isOptionalChain) + { + // Wrap the whole expression as an optional-chain + return CreateUniNode(knopOptChain, pnode); + } return pnode; } } diff --git a/lib/Parser/Parse.h b/lib/Parser/Parse.h index bcbc9413c13..bc4f2a5cd5d 100644 --- a/lib/Parser/Parse.h +++ b/lib/Parser/Parse.h @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #pragma once @@ -371,7 +372,7 @@ class Parser return Anew(alloc, typename OpCodeTrait::ParseNodeType, nop, ichMin, ichLim); } - static ParseNodeBin * StaticCreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ArenaAllocator* alloc, charcount_t ichMin = 0, charcount_t ichLim = 0); + static ParseNodeBin * StaticCreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ArenaAllocator* alloc, charcount_t ichMin = 0, charcount_t ichLim = 0, bool isNullPropagating = false); static ParseNodeBlock * StaticCreateBlockNode(ArenaAllocator* alloc, charcount_t ichMin = 0, charcount_t ichLim = 0, int blockId = -1, PnodeBlockType blockType = PnodeBlockType::Regular); static ParseNodeVar * StaticCreateTempNode(ParseNode* initExpr, ArenaAllocator* alloc); static ParseNodeUni * StaticCreateTempRef(ParseNode* tempNode, ArenaAllocator* alloc); @@ -379,8 +380,8 @@ class Parser private: ParseNodeUni * CreateUniNode(OpCode nop, ParseNodePtr pnodeOp); ParseNodeUni * CreateUniNode(OpCode nop, ParseNodePtr pnode1, charcount_t ichMin, charcount_t ichLim); - ParseNodeBin * CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2); - ParseNodeBin * CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, charcount_t ichMin, charcount_t ichLim); + ParseNodeBin * CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, bool isNullPropagating = false); + ParseNodeBin * CreateBinNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, charcount_t ichMin, charcount_t ichLim, bool isNullPropagating = false); ParseNodeTri * CreateTriNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ParseNodePtr pnode3); ParseNodeTri * CreateTriNode(OpCode nop, ParseNodePtr pnode1, ParseNodePtr pnode2, ParseNodePtr pnode3, charcount_t ichMin, charcount_t ichLim); ParseNodeBlock * CreateBlockNode(PnodeBlockType blockType = PnodeBlockType::Regular); diff --git a/lib/Parser/Scan.cpp b/lib/Parser/Scan.cpp index d3b2be4eb1b..168e70e181c 100644 --- a/lib/Parser/Scan.cpp +++ b/lib/Parser/Scan.cpp @@ -1781,6 +1781,17 @@ tokens Scanner::ScanCore(bool identifyKwds) token = tkCoalesce; break; } + else if (m_scriptContext->GetConfig()->IsESOptionalChainingEnabled() && this->PeekFirst(p, last) == '.') + { + // `a?.3:0` is actually a ternary operator containing the number `0.3` + bool isTernary = CharTypes::_C_DIG == this->charClassifier->GetCharType(this->PeekFirst(p + 1, last)); + if (isTernary) + break; + + p++; + token = tkOptChain; + break; + } break; case '{': Assert(chType == _C_LC); token = tkLCurly; break; diff --git a/lib/Parser/kwd-lsc.h b/lib/Parser/kwd-lsc.h index dba31b1c5d9..0b913a6d38c 100644 --- a/lib/Parser/kwd-lsc.h +++ b/lib/Parser/kwd-lsc.h @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #ifndef KEYWORD @@ -174,6 +175,7 @@ TOK_DCL(tkEllipsis , No, knopNone ,Spr, knopEllipsis ) // ... TOK_DCL(tkLParen , No, knopNone , No, knopNone ) // ( TOK_DCL(tkLBrack , No, knopNone , No, knopNone ) // [ TOK_DCL(tkDot , No, knopNone , No, knopNone ) // . +TOK_DCL(tkOptChain , No, knopNone , No, knopNone ) // ?. // String template tokens TOK_DCL(tkStrTmplBasic , No, knopNone , No, knopNone ) // `...` diff --git a/lib/Parser/perrors.h b/lib/Parser/perrors.h index c4b93869ac3..59c91b08753 100644 --- a/lib/Parser/perrors.h +++ b/lib/Parser/perrors.h @@ -120,7 +120,11 @@ LSC_ERROR_MSG(1102, ERRInvalidAsgTarget, "Invalid left-hand side in assignment." LSC_ERROR_MSG(1103, ERRMissingFrom, "Expected 'from' after import or export clause.") // 1104 ERRsyntaxEOF -// 1105-1199 available for future use + +LSC_ERROR_MSG(1105, ERRInvalidOptChainInNew, "Invalid optional chain in new expression.") +LSC_ERROR_MSG(1106, ERRInvalidOptChainInSuper, "Invalid optional chain in call to 'super'.") +LSC_ERROR_MSG(1107, ERRInvalidOptChainWithTaggedTemplate, "Invalid tagged template in optional chain.") +// 1108-1199 available for future use // Generic errors intended to be re-usable LSC_ERROR_MSG(1200, ERRKeywordAfter, "Unexpected keyword '%s' after '%s'") diff --git a/lib/Parser/ptlist.h b/lib/Parser/ptlist.h index a340d73f5bd..e59c050aa31 100644 --- a/lib/Parser/ptlist.h +++ b/lib/Parser/ptlist.h @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- /*****************************************************************************/ @@ -80,6 +81,7 @@ PTNODE(knopGe , ">=" , OP(Ge) , Bin , fnopBin|fn PTNODE(knopGt , ">" , OP(Gt) , Bin , fnopBin|fnopRel , "GreaterThanOper" ) PTNODE(knopCall , "()" , Nop , Call , fnopNone , "CallExpr" ) PTNODE(knopDot , "." , Nop , Bin , fnopBin , "DotOper" ) +PTNODE(knopOptChain , "?." , Nop , Uni , fnopUni , "OptChain" ) PTNODE(knopAsg , "=" , Nop , Bin , fnopBin|fnopAsg , "AssignmentOper" ) PTNODE(knopInstOf , "instanceof" , IsInst , Bin , fnopBin|fnopRel , "InstanceOfExpr" ) PTNODE(knopIn , "in" , IsIn , Bin , fnopBin|fnopRel , "InOper" ) diff --git a/lib/Parser/ptree.cpp b/lib/Parser/ptree.cpp index faa254f6564..f9819f21478 100644 --- a/lib/Parser/ptree.cpp +++ b/lib/Parser/ptree.cpp @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #include "ParserPch.h" @@ -301,7 +302,7 @@ ParseNodeUni::ParseNodeUni(OpCode nop, charcount_t ichMin, charcount_t ichLim, P this->pnode1 = pnode1; } -ParseNodeBin::ParseNodeBin(OpCode nop, charcount_t ichMin, charcount_t ichLim, ParseNode * pnode1, ParseNode * pnode2) +ParseNodeBin::ParseNodeBin(OpCode nop, charcount_t ichMin, charcount_t ichLim, ParseNode * pnode1, ParseNode * pnode2, bool isNullPropagating) : ParseNode(nop, ichMin, ichLim) { // Member name is either a string or a computed name @@ -313,6 +314,7 @@ ParseNodeBin::ParseNodeBin(OpCode nop, charcount_t ichMin, charcount_t ichLim, P this->pnode1 = pnode1; this->pnode2 = pnode2; + this->isNullPropagating = isNullPropagating; // Statically detect if the add is a concat if (!PHASE_OFF1(Js::ByteCodeConcatExprOptPhase)) @@ -495,6 +497,7 @@ ParseNodeCall::ParseNodeCall(OpCode nop, charcount_t ichMin, charcount_t ichLim, this->isEvalCall = false; this->isSuperCall = false; this->hasDestructuring = false; + this->isNullPropagating = false; } ParseNodeStmt::ParseNodeStmt(OpCode nop, charcount_t ichMin, charcount_t ichLim) diff --git a/lib/Parser/ptree.h b/lib/Parser/ptree.h index 82b56fef30c..5ad2be66c74 100644 --- a/lib/Parser/ptree.h +++ b/lib/Parser/ptree.h @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #pragma once @@ -277,10 +278,11 @@ class ParseNodeUni : public ParseNode class ParseNodeBin : public ParseNode { public: - ParseNodeBin(OpCode nop, charcount_t ichMin, charcount_t ichLim, ParseNode * pnode1, ParseNode * pnode2); + ParseNodeBin(OpCode nop, charcount_t ichMin, charcount_t ichLim, ParseNode * pnode1, ParseNode * pnode2, bool isNullPropagating = false); ParseNodePtr pnode1; ParseNodePtr pnode2; + bool isNullPropagating; DISABLE_SELF_CAST(ParseNodeBin); }; @@ -791,6 +793,7 @@ class ParseNodeCall : public ParseNode BYTE isEvalCall : 1; BYTE isSuperCall : 1; BYTE hasDestructuring : 1; + bool isNullPropagating; DISABLE_SELF_CAST(ParseNodeCall); }; diff --git a/lib/Runtime/Base/ThreadConfigFlagsList.h b/lib/Runtime/Base/ThreadConfigFlagsList.h index 3026920fc9f..dee273547ce 100644 --- a/lib/Runtime/Base/ThreadConfigFlagsList.h +++ b/lib/Runtime/Base/ThreadConfigFlagsList.h @@ -45,6 +45,7 @@ FLAG_RELEASE(IsESBigIntEnabled, ESBigInt) FLAG_RELEASE(IsESNumericSeparatorEnabled, ESNumericSeparator) FLAG_RELEASE(IsESHashbangEnabled, ESHashbang) FLAG_RELEASE(IsESNullishCoalescingOperatorEnabled, ESNullishCoalescingOperator) +FLAG_RELEASE(IsESOptionalChainingEnabled, ESOptionalChaining) FLAG_RELEASE(IsESExportNsAsEnabled, ESExportNsAs) FLAG_RELEASE(IsESSymbolDescriptionEnabled, ESSymbolDescription) FLAG_RELEASE(IsESPromiseAnyEnabled, ESPromiseAny) diff --git a/lib/Runtime/ByteCode/ByteCodeEmitter.cpp b/lib/Runtime/ByteCode/ByteCodeEmitter.cpp index 98533e8f0e6..45de2812a4e 100644 --- a/lib/Runtime/ByteCode/ByteCodeEmitter.cpp +++ b/lib/Runtime/ByteCode/ByteCodeEmitter.cpp @@ -8,6 +8,7 @@ #include "Language/AsmJs.h" #include "ConfigFlagsList.h" +void Emit(ParseNode *pnode, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, BOOL fReturnValue, bool isConstructorCall = false, bool isTopLevel = false); void EmitReference(ParseNode *pnode, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo); void EmitAssignment(ParseNode *asgnNode, ParseNode *lhs, Js::RegSlot rhsLocation, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo); void EmitLoad(ParseNode *rhs, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo); @@ -20,6 +21,80 @@ void EmitUseBeforeDeclaration(Symbol *sym, ByteCodeGenerator *byteCodeGenerator, void EmitUseBeforeDeclarationRuntimeError(ByteCodeGenerator *byteCodeGenerator, Js::RegSlot location); void VisitClearTmpRegs(ParseNode * pnode, ByteCodeGenerator * byteCodeGenerator, FuncInfo * funcInfo); +/// +/// This function generates the common code for null-propagation / optional-chaining. +/// If the targetObject is nullish this will short-circuit(skip) to the end of the chain-expression. +/// +/// It should be called on every ?. location. +/// A call to this function is only valid from a node-emission inside a `knopOptChain` node. +/// See EmitOptionalChain. +/// +static void EmitNullPropagation(Js::RegSlot targetObjectSlot, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, bool isNullPropagating) { + if (!isNullPropagating) + return; + + // Ensure we've setup the skipLabel in the emission of a `knopOptChain` + Assert(funcInfo->currentOptionalChainSkipLabel >= 0); + + // if (targetObject == null) goto skipLabel; + byteCodeGenerator->Writer()->BrReg2( + Js::OpCode::BrEq_A, funcInfo->currentOptionalChainSkipLabel, + targetObjectSlot, funcInfo->undefinedConstantRegister + ); +} + +/// +/// The whole optional-chain expression will be wrapped in a UniNode with `knopOptChain`. +/// Use this function to emit the whole expression. +/// +template +static void EmitOptionalChainWrapper(ParseNodeUni *pnodeOptChain, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, TEmitProc emitChainContent) { + Assert(knopOptChain == pnodeOptChain->nop); + + Js::ByteCodeLabel previousSkipLabel = funcInfo->currentOptionalChainSkipLabel; + + // Create a label that can skip the whole chain and store it in `funcInfo` + Js::ByteCodeLabel skipLabel = byteCodeGenerator->Writer()->DefineLabel(); + funcInfo->currentOptionalChainSkipLabel = skipLabel; + + // Copy values from wrapper to inner expression + ParseNodePtr innerNode = pnodeOptChain->pnode1; + innerNode->isUsed = pnodeOptChain->isUsed; + innerNode->location = pnodeOptChain->location; + + // emit chain expression + // Every `?.` node will call `EmitNullPropagation` + // `EmitNullPropagation` short-circuits to `skipLabel` in case of a nullish value + emitChainContent(innerNode); + pnodeOptChain->location = innerNode->location; + pnodeOptChain->isUsed = innerNode->isUsed; + + Js::ByteCodeLabel doneLabel = Js::Constants::NoRegister; + if (pnodeOptChain->isUsed) + { + Assert(innerNode->isUsed); + Assert(Js::Constants::NoRegister != innerNode->location); + + // Skip short-circuiting logic + doneLabel = byteCodeGenerator->Writer()->DefineLabel(); + byteCodeGenerator->Writer()->Br(doneLabel); + } + + byteCodeGenerator->Writer()->MarkLabel(skipLabel); + + if (pnodeOptChain->isUsed) + { + Assert(innerNode->isUsed); + Assert(Js::Constants::NoRegister != pnodeOptChain->location); + + // Set `undefined` on short-circuiting + byteCodeGenerator->Writer()->Reg2(Js::OpCode::Ld_A_ReuseLoc, pnodeOptChain->location, funcInfo->undefinedConstantRegister); + byteCodeGenerator->Writer()->MarkLabel(doneLabel); + } + + funcInfo->currentOptionalChainSkipLabel = previousSkipLabel; +} + bool CallTargetIsArray(ParseNode *pnode) { return pnode->nop == knopName && pnode->AsParseNodeName()->PropertyIdFromNameNode() == Js::PropertyIds::Array; @@ -251,8 +326,7 @@ bool IsArguments(ParseNode *pnode) } bool ApplyEnclosesArgs(ParseNode* fncDecl, ByteCodeGenerator* byteCodeGenerator); -void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, BOOL fReturnValue, bool isConstructorCall = false, bool isTopLevel = false); -void EmitBinaryOpnds(ParseNode* pnode1, ParseNode* pnode2, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot computedPropertyLocation = Js::Constants::NoRegister); +void EmitBinaryOpnds(ParseNode* pnode1, ParseNode* pnode2, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot computedPropertyLocation = Js::Constants::NoRegister, bool isNullPropagating = false); bool IsExpressionStatement(ParseNode* stmt, const Js::ScriptContext *const scriptContext); void EmitInvoke(Js::RegSlot location, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo); void EmitInvoke(Js::RegSlot location, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot arg1Location); @@ -7854,10 +7928,14 @@ Js::ArgSlot EmitNewObjectOfConstants( return actualArgCount; } -void EmitMethodFld(bool isRoot, bool isScoped, Js::RegSlot location, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, bool registerCacheIdForCall = true) +void EmitMethodFld(bool isRoot, bool isScoped, bool isNullPropagating, Js::RegSlot location, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, bool registerCacheIdForCall = true) { Js::OpCode opcode; - if (!isRoot) + if (isNullPropagating) + { + opcode = (!isScoped && isRoot) ? Js::OpCode::LdRootFld : Js::OpCode::LdFld; + } + else if (!isRoot) { if (callObjLocation == funcInfo->frameObjRegister) { @@ -7897,7 +7975,7 @@ void EmitMethodFld(bool isRoot, bool isScoped, Js::RegSlot location, Js::RegSlot } } -void EmitMethodFld(ParseNode *pnode, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, bool registerCacheIdForCall = true) +void EmitMethodFld(ParseNodeCall *pnodeCall, ParseNode *pnode, Js::RegSlot callObjLocation, Js::PropertyId propertyId, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo, bool registerCacheIdForCall = true) { // Load a call target of the form x.y(). (Call target may be a plain knopName if we're getting it from // the global object, etc.) @@ -7905,7 +7983,7 @@ void EmitMethodFld(ParseNode *pnode, Js::RegSlot callObjLocation, Js::PropertyId bool isScoped = (byteCodeGenerator->GetFlags() & fscrEval) != 0 || (isRoot && callObjLocation != ByteCodeGenerator::RootObjectRegister); - EmitMethodFld(isRoot, isScoped, pnode->location, callObjLocation, propertyId, byteCodeGenerator, funcInfo, registerCacheIdForCall); + EmitMethodFld(isRoot, isScoped, pnodeCall->isNullPropagating, pnode->location, callObjLocation, propertyId, byteCodeGenerator, funcInfo, registerCacheIdForCall); } // lhs.apply(this, arguments); @@ -7932,7 +8010,7 @@ void EmitApplyCall(ParseNodeCall* pnodeCall, ByteCodeGenerator* byteCodeGenerato // call for apply, we won't remove the entry for "apply" cacheId from // ByteCodeWriter::callRegToLdFldCacheIndexMap, which is contrary to our assumption that we would // have removed an entry from a map upon seeing its corresponding call. - EmitMethodFld(applyNode, funcNode->location, propertyId, byteCodeGenerator, funcInfo, false /*registerCacheIdForCall*/); + EmitMethodFld(pnodeCall, applyNode, funcNode->location, propertyId, byteCodeGenerator, funcInfo, false /*registerCacheIdForCall*/); Symbol *argSym = funcInfo->GetArgumentsSymbol(); Assert(argSym && argSym->IsArguments()); @@ -8035,6 +8113,7 @@ void EmitCallTargetNoEvalComponents( } void EmitCallTarget( + ParseNodeCall *pnodeCall, ParseNode *pnodeTarget, BOOL fSideEffectArgs, Js::RegSlot *thisLocation, @@ -8057,6 +8136,12 @@ void EmitCallTarget( switch (pnodeTarget->nop) { + case knopOptChain: { + EmitOptionalChainWrapper(pnodeTarget->AsParseNodeUni(), byteCodeGenerator, funcInfo, [&](ParseNodePtr innerNode) { + EmitCallTarget(pnodeCall, innerNode, fSideEffectArgs, thisLocation, releaseThisLocation, callObjLocation, byteCodeGenerator, funcInfo, callApplyCallSiteId); + }); + break; + } case knopDot: { ParseNodeBin * pnodeBinTarget = pnodeTarget->AsParseNodeBin(); @@ -8101,7 +8186,8 @@ void EmitCallTarget( else { *thisLocation = pnodeBinTarget->pnode1->location; - EmitMethodFld(pnodeBinTarget, protoLocation, propertyId, byteCodeGenerator, funcInfo); + EmitNullPropagation(pnodeBinTarget->pnode1->location, byteCodeGenerator, funcInfo, pnodeBinTarget->isNullPropagating); + EmitMethodFld(pnodeCall, pnodeBinTarget, protoLocation, propertyId, byteCodeGenerator, funcInfo); } break; @@ -8109,22 +8195,24 @@ void EmitCallTarget( case knopIndex: { - funcInfo->AcquireLoc(pnodeTarget); + ParseNodeBin *pnodeBinTarget = pnodeTarget->AsParseNodeBin(); + funcInfo->AcquireLoc(pnodeBinTarget); // Assign the call target operand(s), putting them into expression temps if necessary to protect // them from side-effects. - if (fSideEffectArgs || !(ParseNode::Grfnop(pnodeTarget->AsParseNodeBin()->pnode2->nop) & fnopLeaf)) + if (fSideEffectArgs || !(ParseNode::Grfnop(pnodeBinTarget->pnode2->nop) & fnopLeaf)) { // Though we're done with target evaluation after this point, still protect opnd1 from // arg or opnd2 side-effects as it's the "this" pointer. - SaveOpndValue(pnodeTarget->AsParseNodeBin()->pnode1, funcInfo); + SaveOpndValue(pnodeBinTarget->pnode1, funcInfo); } - Emit(pnodeTarget->AsParseNodeBin()->pnode1, byteCodeGenerator, funcInfo, false); - Emit(pnodeTarget->AsParseNodeBin()->pnode2, byteCodeGenerator, funcInfo, false); + Emit(pnodeBinTarget->pnode1, byteCodeGenerator, funcInfo, false); + EmitNullPropagation(pnodeBinTarget->pnode1->location, byteCodeGenerator, funcInfo, pnodeBinTarget->isNullPropagating); + Emit(pnodeBinTarget->pnode2, byteCodeGenerator, funcInfo, false); - Js::RegSlot indexLocation = pnodeTarget->AsParseNodeBin()->pnode2->location; - Js::RegSlot protoLocation = pnodeTarget->AsParseNodeBin()->pnode1->location; + Js::RegSlot indexLocation = pnodeBinTarget->pnode2->location; + Js::RegSlot protoLocation = pnodeBinTarget->pnode1->location; - if (ByteCodeGenerator::IsSuper(pnodeTarget->AsParseNodeBin()->pnode1)) + if (ByteCodeGenerator::IsSuper(pnodeBinTarget->pnode1)) { Emit(pnodeTarget->AsParseNodeSuperReference()->pnodeThis, byteCodeGenerator, funcInfo, false); protoLocation = byteCodeGenerator->EmitLdObjProto(Js::OpCode::LdHomeObjProto, protoLocation, funcInfo); @@ -8136,16 +8224,16 @@ void EmitCallTarget( } else { - *thisLocation = pnodeTarget->AsParseNodeBin()->pnode1->location; + *thisLocation = pnodeBinTarget->pnode1->location; } EmitMethodElem(pnodeTarget, protoLocation, indexLocation, byteCodeGenerator); - funcInfo->ReleaseLoc(pnodeTarget->AsParseNodeBin()->pnode2); // don't release indexLocation until after we use it. + funcInfo->ReleaseLoc(pnodeBinTarget->pnode2); // don't release indexLocation until after we use it. - if (ByteCodeGenerator::IsSuper(pnodeTarget->AsParseNodeBin()->pnode1)) + if (ByteCodeGenerator::IsSuper(pnodeBinTarget->pnode1)) { - funcInfo->ReleaseLoc(pnodeTarget->AsParseNodeBin()->pnode1); + funcInfo->ReleaseLoc(pnodeBinTarget->pnode1); } break; } @@ -8167,7 +8255,7 @@ void EmitCallTarget( { // Load the call target as a property of the instance. Js::PropertyId propertyId = pnodeNameTarget->PropertyIdFromNameNode(); - EmitMethodFld(pnodeNameTarget, *callObjLocation, propertyId, byteCodeGenerator, funcInfo); + EmitMethodFld(pnodeCall, pnodeNameTarget, *callObjLocation, propertyId, byteCodeGenerator, funcInfo); break; } } @@ -8323,7 +8411,7 @@ void EmitCallInstrNoEvalComponents( Assert(pnodeTarget->AsParseNodeBin()->pnode2->nop == knopName); Js::PropertyId propertyId = pnodeTarget->AsParseNodeBin()->pnode2->AsParseNodeName()->PropertyIdFromNameNode(); - EmitMethodFld(pnodeTarget, callObjLocation, propertyId, byteCodeGenerator, funcInfo); + EmitMethodFld(pnodeCall, pnodeTarget, callObjLocation, propertyId, byteCodeGenerator, funcInfo); EmitCallI(pnodeCall, /*fEvaluateComponents*/ FALSE, fIsEval, fHasNewTarget, actualArgCount, byteCodeGenerator, funcInfo, callSiteId, spreadIndices); } break; @@ -8347,7 +8435,7 @@ void EmitCallInstrNoEvalComponents( funcInfo->ReleaseTmpRegister(callObjLocation); Js::PropertyId propertyId = pnodeTarget->AsParseNodeName()->PropertyIdFromNameNode(); - EmitMethodFld(pnodeTarget, callObjLocation, propertyId, byteCodeGenerator, funcInfo); + EmitMethodFld(pnodeCall, pnodeTarget, callObjLocation, propertyId, byteCodeGenerator, funcInfo); EmitCallI(pnodeCall, /*fEvaluateComponents*/ FALSE, fIsEval, fHasNewTarget, actualArgCount, byteCodeGenerator, funcInfo, callSiteId, spreadIndices); break; } @@ -8572,10 +8660,12 @@ void EmitCall( } else { - EmitCallTarget(pnodeTarget, fSideEffectArgs, &thisLocation, &releaseThisLocation, &callObjLocation, byteCodeGenerator, funcInfo, &callApplyCallSiteId); + EmitCallTarget(pnodeCall, pnodeTarget, fSideEffectArgs, &thisLocation, &releaseThisLocation, &callObjLocation, byteCodeGenerator, funcInfo, &callApplyCallSiteId); } } + EmitNullPropagation(pnodeCall->pnodeTarget->location, byteCodeGenerator, funcInfo, pnodeCall->isNullPropagating); + // If we are strictly overriding the this location, ignore what the call target set this location to. if (overrideThisLocation != Js::Constants::NoRegister) { @@ -8622,7 +8712,7 @@ void EmitInvoke( ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo) { - EmitMethodFld(false, false, location, callObjLocation, propertyId, byteCodeGenerator, funcInfo); + EmitMethodFld(false, false, false, location, callObjLocation, propertyId, byteCodeGenerator, funcInfo); funcInfo->StartRecordingOutArgs(1); @@ -8642,7 +8732,7 @@ void EmitInvoke( FuncInfo* funcInfo, Js::RegSlot arg1Location) { - EmitMethodFld(false, false, location, callObjLocation, propertyId, byteCodeGenerator, funcInfo); + EmitMethodFld(false, false, false, location, callObjLocation, propertyId, byteCodeGenerator, funcInfo); funcInfo->StartRecordingOutArgs(2); @@ -10081,7 +10171,7 @@ void ByteCodeGenerator::EmitJumpCleanup(ParseNode* target, FuncInfo* funcInfo) } } -void EmitBinaryOpnds(ParseNode* pnode1, ParseNode* pnode2, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot computedPropertyLocation) +void EmitBinaryOpnds(ParseNode* pnode1, ParseNode* pnode2, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, Js::RegSlot computedPropertyLocation, bool isNullPropagating) { // If opnd2 can overwrite opnd1, make sure the value of opnd1 is stashed away. if (MayHaveSideEffectOnNode(pnode1, pnode2, byteCodeGenerator)) @@ -10096,6 +10186,7 @@ void EmitBinaryOpnds(ParseNode* pnode1, ParseNode* pnode2, ByteCodeGenerator* by byteCodeGenerator->Writer()->Reg2(Js::OpCode::Conv_Prop, computedPropertyLocation, pnode1->location); } + EmitNullPropagation(pnode1->location, byteCodeGenerator, funcInfo, isNullPropagating); Emit(pnode2, byteCodeGenerator, funcInfo, false, false, computedPropertyLocation); } @@ -11124,6 +11215,81 @@ void TrackGlobalIntAssignments(ParseNodePtr pnode, ByteCodeGenerator * byteCodeG } } +static void EmitDelete(ParseNode *pnode, ParseNode *pexpr, ByteCodeGenerator *byteCodeGenerator, FuncInfo *funcInfo) { + switch (pexpr->nop) + { + case knopOptChain: + EmitOptionalChainWrapper(pexpr->AsParseNodeUni(), byteCodeGenerator, funcInfo, [&](ParseNode *innerNode) { + EmitDelete(innerNode, innerNode, byteCodeGenerator, funcInfo); + }); + pnode->location = pexpr->location; + break; + case knopName: + { + ParseNodeName *pnodeName = pexpr->AsParseNodeName(); + if (pnodeName->IsSpecialName()) + { + funcInfo->AcquireLoc(pnode); + byteCodeGenerator->Writer()->Reg1(Js::OpCode::LdTrue, pnode->location); + } + else + { + funcInfo->AcquireLoc(pnode); + byteCodeGenerator->EmitPropDelete(pnode->location, pnodeName->sym, pnodeName->pid, funcInfo); + } + break; + } + case knopDot: + { + ParseNodeBin *pnodeDot = pexpr->AsParseNodeBin(); + ParseNode *pnode1 = pnodeDot->pnode1; + ParseNode *pnode2 = pnodeDot->pnode2; + + if (ByteCodeGenerator::IsSuper(pnode1)) + { + byteCodeGenerator->Writer()->W1(Js::OpCode::RuntimeReferenceError, SCODE_CODE(JSERR_DeletePropertyWithSuper)); + + funcInfo->AcquireLoc(pnode); + byteCodeGenerator->Writer()->Reg1(Js::OpCode::LdUndef, pnode->location); + } + else + { + Emit(pnode1, byteCodeGenerator, funcInfo, false); + EmitNullPropagation(pnode1->location, byteCodeGenerator, funcInfo, pnodeDot->isNullPropagating); + + funcInfo->ReleaseLoc(pnode1); + Js::PropertyId propertyId = pnode2->AsParseNodeName()->PropertyIdFromNameNode(); + funcInfo->AcquireLoc(pnode); + byteCodeGenerator->Writer()->Property(Js::OpCode::DeleteFld, pnode->location, pnode1->location, + funcInfo->FindOrAddReferencedPropertyId(propertyId), byteCodeGenerator->forceStrictModeForClassComputedPropertyName); + } + + break; + } + case knopIndex: + { + ParseNodeBin *pnodeIndex = pexpr->AsParseNodeBin(); + ParseNode *pnode1 = pnodeIndex->pnode1; + ParseNode *pnode2 = pnodeIndex->pnode2; + + EmitBinaryOpnds(pnode1, pnode2, byteCodeGenerator, funcInfo, Js::Constants::NoRegister, pnodeIndex->isNullPropagating); + funcInfo->ReleaseLoc(pnode2); + funcInfo->ReleaseLoc(pnode1); + funcInfo->AcquireLoc(pnode); + byteCodeGenerator->Writer()->Element(Js::OpCode::DeleteElemI_A, pnode->location, pnode1->location, pnode2->location); + break; + } + default: + { + Emit(pexpr, byteCodeGenerator, funcInfo, false); + funcInfo->ReleaseLoc(pexpr); + byteCodeGenerator->Writer()->Reg2( + Js::OpCode::Delete_A, funcInfo->AcquireLoc(pnode), pexpr->location); + break; + } + } +} + void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo, BOOL fReturnValue, bool isConstructorCall, bool isTopLevel) { if (pnode == nullptr) @@ -11485,63 +11651,7 @@ void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* func { ParseNode *pexpr = pnode->AsParseNodeUni()->pnode1; byteCodeGenerator->StartStatement(pnode); - switch (pexpr->nop) - { - case knopName: - { - ParseNodeName * pnodeName = pexpr->AsParseNodeName(); - if (pnodeName->IsSpecialName()) - { - funcInfo->AcquireLoc(pnode); - byteCodeGenerator->Writer()->Reg1(Js::OpCode::LdTrue, pnode->location); - } - else - { - funcInfo->AcquireLoc(pnode); - byteCodeGenerator->EmitPropDelete(pnode->location, pnodeName->sym, pnodeName->pid, funcInfo); - } - break; - } - case knopDot: - { - if (ByteCodeGenerator::IsSuper(pexpr->AsParseNodeBin()->pnode1)) - { - byteCodeGenerator->Writer()->W1(Js::OpCode::RuntimeReferenceError, SCODE_CODE(JSERR_DeletePropertyWithSuper)); - - funcInfo->AcquireLoc(pnode); - byteCodeGenerator->Writer()->Reg1(Js::OpCode::LdUndef, pnode->location); - } - else - { - Emit(pexpr->AsParseNodeBin()->pnode1, byteCodeGenerator, funcInfo, false); - - funcInfo->ReleaseLoc(pexpr->AsParseNodeBin()->pnode1); - Js::PropertyId propertyId = pexpr->AsParseNodeBin()->pnode2->AsParseNodeName()->PropertyIdFromNameNode(); - funcInfo->AcquireLoc(pnode); - byteCodeGenerator->Writer()->Property(Js::OpCode::DeleteFld, pnode->location, pexpr->AsParseNodeBin()->pnode1->location, - funcInfo->FindOrAddReferencedPropertyId(propertyId), byteCodeGenerator->forceStrictModeForClassComputedPropertyName); - } - - break; - } - case knopIndex: - { - EmitBinaryOpnds(pexpr->AsParseNodeBin()->pnode1, pexpr->AsParseNodeBin()->pnode2, byteCodeGenerator, funcInfo); - funcInfo->ReleaseLoc(pexpr->AsParseNodeBin()->pnode2); - funcInfo->ReleaseLoc(pexpr->AsParseNodeBin()->pnode1); - funcInfo->AcquireLoc(pnode); - byteCodeGenerator->Writer()->Element(Js::OpCode::DeleteElemI_A, pnode->location, pexpr->AsParseNodeBin()->pnode1->location, pexpr->AsParseNodeBin()->pnode2->location); - break; - } - default: - { - Emit(pexpr, byteCodeGenerator, funcInfo, false); - funcInfo->ReleaseLoc(pexpr); - byteCodeGenerator->Writer()->Reg2( - Js::OpCode::Delete_A, funcInfo->AcquireLoc(pnode), pexpr->location); - break; - } - } + EmitDelete(pnode, pexpr, byteCodeGenerator, funcInfo); byteCodeGenerator->EndStatement(pnode); break; } @@ -11583,7 +11693,10 @@ void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* func case knopIndex: { STARTSTATEMENET_IFTOPLEVEL(isTopLevel, pnode); - EmitBinaryOpnds(pnode->AsParseNodeBin()->pnode1, pnode->AsParseNodeBin()->pnode2, byteCodeGenerator, funcInfo); + EmitBinaryOpnds(pnode->AsParseNodeBin()->pnode1, pnode->AsParseNodeBin()->pnode2, byteCodeGenerator, funcInfo, + Js::Constants::NoRegister, + // EmitNullPropagation is called in EmitBinaryOpnds to short-circuit indexer content + pnode->AsParseNodeBin()->isNullPropagating); Js::RegSlot callObjLocation = pnode->AsParseNodeBin()->pnode1->location; Js::RegSlot protoLocation = callObjLocation; @@ -11605,6 +11718,13 @@ void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* func ENDSTATEMENET_IFTOPLEVEL(isTopLevel, pnode); break; } + + case knopOptChain: + EmitOptionalChainWrapper(pnode->AsParseNodeUni(), byteCodeGenerator, funcInfo, [&](ParseNodePtr innerNode) { + Emit(innerNode, byteCodeGenerator, funcInfo, false); + }); + break; + // this is MemberExpression as rvalue case knopDot: { @@ -11625,6 +11745,8 @@ void Emit(ParseNode* pnode, ByteCodeGenerator* byteCodeGenerator, FuncInfo* func Js::PropertyId propertyId = pnode->AsParseNodeBin()->pnode2->AsParseNodeName()->PropertyIdFromNameNode(); uint cacheId = funcInfo->FindOrAddInlineCacheId(protoLocation, propertyId, false, false); + EmitNullPropagation(callObjLocation, byteCodeGenerator, funcInfo, pnode->AsParseNodeBin()->isNullPropagating); + if (propertyId == Js::PropertyIds::length) { byteCodeGenerator->Writer()->PatchableProperty(Js::OpCode::LdLen_A, pnode->location, protoLocation, cacheId); diff --git a/lib/Runtime/ByteCode/ByteCodeGenerator.cpp b/lib/Runtime/ByteCode/ByteCodeGenerator.cpp index 1ed46444cf4..dc0d9a2d020 100644 --- a/lib/Runtime/ByteCode/ByteCodeGenerator.cpp +++ b/lib/Runtime/ByteCode/ByteCodeGenerator.cpp @@ -1,6 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. -// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #include "RuntimeByteCodePch.h" @@ -1224,6 +1224,10 @@ ParseNode* VisitBlock(ParseNode *pnode, ByteCodeGenerator* byteCodeGenerator, Pr } } } + if (nullptr != pnodeLastVal) + { + pnodeLastVal->isUsed = true; + } return pnodeLastVal; } @@ -4965,6 +4969,9 @@ void AssignRegisters(ParseNode *pnode, ByteCodeGenerator *byteCodeGenerator) case knopObject: byteCodeGenerator->AssignNullConstRegister(); break; + case knopOptChain: + byteCodeGenerator->AssignUndefinedConstRegister(); + break; case knopClassDecl: { FuncInfo * topFunc = byteCodeGenerator->TopFuncInfo(); diff --git a/lib/Runtime/ByteCode/FuncInfo.cpp b/lib/Runtime/ByteCode/FuncInfo.cpp index ca277e059cb..be75552b79d 100644 --- a/lib/Runtime/ByteCode/FuncInfo.cpp +++ b/lib/Runtime/ByteCode/FuncInfo.cpp @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #include "RuntimeByteCodePch.h" @@ -33,6 +34,7 @@ FuncInfo::FuncInfo( outArgsCurrentExpr(0), innerScopeCount(0), currentInnerScopeIndex((uint)-1), + currentOptionalChainSkipLabel(-1), #if DBG outArgsDepth(0), #endif diff --git a/lib/Runtime/ByteCode/FuncInfo.h b/lib/Runtime/ByteCode/FuncInfo.h index ecc63f08b18..9fe4754ead1 100644 --- a/lib/Runtime/ByteCode/FuncInfo.h +++ b/lib/Runtime/ByteCode/FuncInfo.h @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- struct InlineCacheUnit @@ -95,6 +96,7 @@ class FuncInfo Js::RegSlot outArgsCurrentExpr; // max number of out args accumulated in the current nested expression uint innerScopeCount; uint currentInnerScopeIndex; + Js::ByteCodeLabel currentOptionalChainSkipLabel; #if DBG uint32 outArgsDepth; // number of calls nested in an expression #endif diff --git a/test/es12/optional-async.js b/test/es12/optional-async.js new file mode 100644 index 00000000000..f4a11aabadb --- /dev/null +++ b/test/es12/optional-async.js @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +// @ts-check + +const simpleObj = { null: null, undefined: undefined, something: 42 }; +Object.freeze(simpleObj); + +// Short-circuiting ignores indexer expression and method args +(async () => { + simpleObj.nothing?.[await Promise.reject()]; + simpleObj.null?.[await Promise.reject()]; + simpleObj.undefined?.[await Promise.reject()]; +})().then( + () => { + console.log("pass"); + }, + (x) => console.log(x) +); diff --git a/test/es12/optional-calls.js b/test/es12/optional-calls.js new file mode 100644 index 00000000000..4b62e71b8c1 --- /dev/null +++ b/test/es12/optional-calls.js @@ -0,0 +1,170 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +// @ts-check +/// + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +const simpleObj = { "null": null, "undefined": undefined, something: 42 }; +Object.freeze(simpleObj); + +const tests = [ + { + name: "Simple method call on property", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.nothing(), TypeError); + assert.throws(() => simpleObj.null(), TypeError); + assert.throws(() => simpleObj.undefined(), TypeError); + + // With optional-chains + assert.isUndefined(simpleObj.nothing?.(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.null?.(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.undefined?.(), "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Simple method call on indexer", + body() { + // Verify normal behavior + assert.throws(() => simpleObj["nothing"](), TypeError); + assert.throws(() => simpleObj["null"](), TypeError); + assert.throws(() => simpleObj["undefined"](), TypeError); + + // With optional-chains + assert.isUndefined(simpleObj["nothing"]?.(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj["null"]?.(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj["undefined"]?.(), "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Simple method call on non-callable property", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.something(), TypeError, "Non-callable prop", "Function expected"); + + // With optional-chains + assert.throws(() => simpleObj.something?.(), TypeError, "Non-callable prop", "Function expected"); + assert.throws(() => simpleObj?.something(), TypeError, "Non-callable prop", "Function expected"); + assert.throws(() => simpleObj?.something?.(), TypeError, "Non-callable prop", "Function expected"); + } + }, + { + name: "Simple method call on non-callable indexer", + body() { + // Verify normal behavior + assert.throws(() => simpleObj["something"](), TypeError, "Non-callable prop", "Function expected"); + + // With optional-chains + assert.throws(() => simpleObj["something"]?.(), TypeError, "Non-callable prop", "Function expected"); + assert.throws(() => simpleObj?.["something"](), TypeError, "Non-callable prop", "Function expected"); + assert.throws(() => simpleObj?.["something"]?.(), TypeError, "Non-callable prop", "Function expected"); + } + }, + { + name: "Optional properties before call", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.nothing.something(), TypeError); + assert.throws(() => simpleObj.null.something(), TypeError); + assert.throws(() => simpleObj.undefined.something(), TypeError); + + // With optional-chains + assert.isUndefined(simpleObj.nothing?.something(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.null?.something(), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.undefined?.something(), "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Optional indexers before call", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.nothing["something"](), TypeError); + assert.throws(() => simpleObj.null["something"](), TypeError); + assert.throws(() => simpleObj.undefined["something"](), TypeError); + + // With optional-chains + assert.isUndefined(simpleObj.nothing?.["something"](), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.null?.["something"](), "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.undefined?.["something"](), "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Propagate 'this' correctly", + body() { + const specialObj = { + b() { return this._b; }, + _b: { c: 42 } + }; + + assert.areEqual(42, specialObj.b().c); + assert.areEqual(42, specialObj?.b().c); + assert.areEqual(42, specialObj.b?.().c); + assert.areEqual(42, specialObj?.b?.().c); + assert.areEqual(42, (specialObj?.b)().c); + assert.areEqual(42, (specialObj.b)?.().c); + assert.areEqual(42, (specialObj?.b)?.().c); + } + }, + { + name: "Optional call of root function", + body(){ + assert.areEqual(42, eval?.("42")); + + globalThis.doNotUseThisBadGlobalFunction = () => 42; + assert.areEqual(42, doNotUseThisBadGlobalFunction?.()); + assert.areEqual(42, eval("doNotUseThisBadGlobalFunction?.()")); + } + }, + { + name: "Optional call in eval (function)", + body() { + function fn() { + return 42; + } + assert.areEqual(42, eval("fn?.()")); + }, + }, + { + name: "Optional call in eval (lambda)", + body() { + const fn = () => 42; + assert.areEqual(42, eval("fn?.()")); + }, + }, + { + name: "Optional call in eval (object)", + body() { + const obj = { + fn: () => 42, + }; + assert.areEqual(42, eval("obj?.fn?.()")); + }, + }, + { + name: "Optional call in eval (undefined)", + body() { + assert.areEqual(undefined, eval("doesNotExist?.()")); + }, + }, + { + name: "Optional call in eval respects scope", + body() { + function fn() { + return 42; + } + assert.areEqual(24, eval(` + function fn(){ + return 24; + } + fn?.() + `)); + }, + } +]; + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" }); diff --git a/test/es12/optional-chaining.js b/test/es12/optional-chaining.js new file mode 100644 index 00000000000..defdbb7538f --- /dev/null +++ b/test/es12/optional-chaining.js @@ -0,0 +1,102 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +// @ts-check +/// + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +const simpleObj = { "null": null, "undefined": undefined, something: 42 }; +Object.freeze(simpleObj); + +const tests = [ + { + name: "Simple properties", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.nothing.something, TypeError); + assert.throws(() => simpleObj.null.something, TypeError); + assert.throws(() => simpleObj.undefined.something, TypeError); + + // With optional-chains + assert.isUndefined(simpleObj.nothing?.something, "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.null?.something, "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.undefined?.something, "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Simple indexers", + body() { + // Verify normal behavior + assert.throws(() => simpleObj.nothing["something"], TypeError); + assert.throws(() => simpleObj.null["something"], TypeError); + assert.throws(() => simpleObj.undefined["something"], TypeError); + + // With optional-chains + assert.isUndefined(simpleObj.nothing?.["something"], "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.null?.["something"], "OptChain should evaluated to 'undefined'"); + assert.isUndefined(simpleObj.undefined?.["something"], "OptChain should evaluated to 'undefined'"); + } + }, + { + name: "Short-circuiting ignores indexer expression and method args", + body() { + let i = 0; + + assert.isUndefined(simpleObj.nothing?.[i++]); + assert.isUndefined(simpleObj.null?.[i++]); + assert.isUndefined(simpleObj.undefined?.[i++]); + + assert.isUndefined(simpleObj.nothing?.[i++]()); + assert.isUndefined(simpleObj.null?.[i++]()); + assert.isUndefined(simpleObj.undefined?.[i++]()); + + assert.isUndefined(simpleObj.nothing?.something(i++)); + assert.isUndefined(simpleObj.null?.something(i++)); + assert.isUndefined(simpleObj.undefined?.something(i++)); + + assert.strictEqual(0, i, "Indexer may never be evaluated"); + } + }, + { + name: "Short-circuiting ignores nested properties", + body() { + assert.isUndefined(simpleObj.nothing?.a.b.c.d.e.f.g.h); + assert.isUndefined(simpleObj.null?.a.b.c.d.e.f.g.h); + assert.isUndefined(simpleObj.undefined?.a.b.c.d.e.f.g.h); + } + }, + { + name: "Short-circuiting multiple levels", + body() { + let i = 0; + const specialObj = { + get null() { + i++; + return null; + }, + get undefined() { + i++; + return undefined; + } + }; + + assert.isUndefined(specialObj?.null?.a.b.c.d?.e.f.g.h); + assert.isUndefined(specialObj?.undefined?.a.b.c.d?.e.f.g.h); + + assert.areEqual(2, i, "Properties should be called") + } + }, + // Null check + { + name: "Only check for 'null' and 'undefined'", + body() { + assert.areEqual(0, ""?.length, "Expected empty string length"); + } + } +]; + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" }); diff --git a/test/es12/optional-parsing.js b/test/es12/optional-parsing.js new file mode 100644 index 00000000000..d6803f7867a --- /dev/null +++ b/test/es12/optional-parsing.js @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Copyright (c) ChakraCore Project Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +// @ts-check +/// + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +const tests = [ + { + name: "Parse ternary correctly", + body() { + assert.areEqual(0.42, eval(`"this is not falsy"?.42 : 0`)); + } + }, + { + name: "Tagged Template in OptChain is illegal", + body() { + assert.throws(() => eval("simpleObj.undefined?.`template`"), SyntaxError, "No TaggedTemplate here", "Invalid tagged template in optional chain."); + assert.throws(() => eval(`simpleObj.undefined?. + \`template\``), SyntaxError, "No TaggedTemplate here", "Invalid tagged template in optional chain."); + } + }, + { + name: "No new in OptChain", + body() { + assert.throws(() => eval(` + class Test { } + new Test?.(); + `), SyntaxError, "'new' in OptChain is illegal", "Invalid optional chain in new expression."); + } + }, + { + name: "No super in OptChain", + body() { + assert.throws(() => eval(` + class Base { } + class Test extends Base { + constructor(){ + super?.(); + } + } + `), SyntaxError, "Super in OptChain is illegal", "Invalid use of the 'super' keyword"); + + assert.throws(() => eval(` + class Base { } + class Test extends Base { + constructor(){ + super(); + + super?.abc; + } + } + `), SyntaxError, "Super in OptChain is illegal", "Invalid use of the 'super' keyword"); + } + }, + { + name: "No assignment", + body() { + const a = {}; + assert.throws(() => eval(`a?.b++`), SyntaxError, "Assignment is illegal", "Invalid left-hand side in assignment."); + assert.throws(() => eval(`a?.b += 1`), SyntaxError, "Assignment is illegal", "Invalid left-hand side in assignment."); + assert.throws(() => eval(`a?.b = 5`), SyntaxError, "Assignment is illegal", "Invalid left-hand side in assignment."); + } + } +]; + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" }); diff --git a/test/es12/rlexe.xml b/test/es12/rlexe.xml new file mode 100644 index 00000000000..88fbcfa1230 --- /dev/null +++ b/test/es12/rlexe.xml @@ -0,0 +1,21 @@ + + + + + optional-chaining.js + -args summary -endargs + + + optional-parsing.js + -args summary -endargs + + + optional-calls.js + -args summary -endargs + + + optional-async.js + -args summary -endargs + + + diff --git a/test/rlexedirs.xml b/test/rlexedirs.xml index 5b61668f4c1..b9fc438b4a5 100644 --- a/test/rlexedirs.xml +++ b/test/rlexedirs.xml @@ -278,6 +278,11 @@ es7 + + + es12 + + switchStatement