From e1fc99d6efdddb3ed0eef82281a8662bf3b1a344 Mon Sep 17 00:00:00 2001 From: FranklinDM Date: Sun, 24 Apr 2022 21:05:43 +0800 Subject: Issue #1658 - Part 1: Implement support for optional chaining in the JS Parser Partially based on: https://bugzilla.mozilla.org/show_bug.cgi?id=1566143 --- js/src/frontend/FoldConstants.cpp | 24 ++- js/src/frontend/FullParseHandler.h | 29 +++ js/src/frontend/ParseNode.cpp | 5 + js/src/frontend/ParseNode.h | 110 ++++++++++- js/src/frontend/Parser.cpp | 363 ++++++++++++++++++++++++++--------- js/src/frontend/Parser.h | 17 ++ js/src/frontend/SyntaxParseHandler.h | 15 +- js/src/frontend/TokenKind.h | 1 + js/src/frontend/TokenStream.cpp | 21 +- js/src/js.msg | 2 + 10 files changed, 483 insertions(+), 104 deletions(-) diff --git a/js/src/frontend/FoldConstants.cpp b/js/src/frontend/FoldConstants.cpp index c348056c45..d0e88581ea 100644 --- a/js/src/frontend/FoldConstants.cpp +++ b/js/src/frontend/FoldConstants.cpp @@ -319,6 +319,7 @@ ContainsHoistedDeclaration(ExclusiveContext* cx, ParseNode* node, bool* result) case PNK_DELETEPROP: case PNK_DELETEELEM: case PNK_DELETEEXPR: + case PNK_DELETEOPTCHAIN: case PNK_POS: case PNK_NEG: case PNK_PREINCREMENT: @@ -1304,7 +1305,7 @@ FoldElement(ExclusiveContext* cx, ParseNode** nodePtr, Parser& { ParseNode* node = *nodePtr; - MOZ_ASSERT(node->isKind(PNK_ELEM)); + MOZ_ASSERT(node->isKind(PNK_ELEM) || node->isKind(PNK_OPTELEM)); MOZ_ASSERT(node->isArity(PN_BINARY)); ParseNode*& expr = node->pn_left; @@ -1348,9 +1349,15 @@ FoldElement(ExclusiveContext* cx, ParseNode** nodePtr, Parser& // Optimization 3: We have expr["foo"] where foo is not an index. Convert // to a property access (like expr.foo) that optimizes better downstream. - ParseNode* dottedAccess = parser.handler.newPropertyAccess(expr, name, node->pn_pos.end); - if (!dottedAccess) + ParseNode* dottedAccess; + if (node->isKind(PNK_OPTELEM)) { + dottedAccess = parser.handler.newOptionalPropertyAccess(expr, name, node->pn_pos.end); + } else { + dottedAccess = parser.handler.newPropertyAccess(expr, name, node->pn_pos.end); + } + if (!dottedAccess) { return false; + } dottedAccess->setInParens(node->isInParens()); ReplaceNode(nodePtr, dottedAccess); @@ -1599,13 +1606,13 @@ static bool FoldDottedProperty(ExclusiveContext* cx, ParseNode* node, Parser& parser, bool inGenexpLambda) { - MOZ_ASSERT(node->isKind(PNK_DOT)); + MOZ_ASSERT(node->isKind(PNK_DOT) || node->isKind(PNK_OPTDOT)); MOZ_ASSERT(node->isArity(PN_NAME)); // Iterate through a long chain of dotted property accesses to find the // most-nested non-dotted property node, then fold that. ParseNode** nested = &node->pn_expr; - while ((*nested)->isKind(PNK_DOT)) { + while ((*nested)->isKind(PNK_DOT) || (*nested)->isKind(PNK_OPTDOT)) { MOZ_ASSERT((*nested)->isArity(PN_NAME)); nested = &(*nested)->pn_expr; } @@ -1632,6 +1639,9 @@ Fold(ExclusiveContext* cx, ParseNode** pnp, Parser& parser, bo JS_CHECK_RECURSION(cx, return false); ParseNode* pn = *pnp; +#ifdef DEBUG + ParseNodeKind kind = pn->getKind(); +#endif switch (pn->getKind()) { case PNK_NOP: @@ -1713,6 +1723,8 @@ Fold(ExclusiveContext* cx, ParseNode** pnp, Parser& parser, bo MOZ_ASSERT(pn->isArity(PN_BINARY)); return Fold(cx, &pn->pn_left, parser, inGenexpLambda); + case PNK_DELETEOPTCHAIN: + case PNK_OPTCHAIN: case PNK_SEMI: case PNK_THIS: MOZ_ASSERT(pn->isArity(PN_UNARY)); @@ -1806,6 +1818,7 @@ Fold(ExclusiveContext* cx, ParseNode** pnp, Parser& parser, bo case PNK_CLASS: return FoldClass(cx, pn, parser, inGenexpLambda); + case PNK_OPTELEM: case PNK_ELEM: return FoldElement(cx, pnp, parser, inGenexpLambda); @@ -1896,6 +1909,7 @@ Fold(ExclusiveContext* cx, ParseNode** pnp, Parser& parser, bo MOZ_ASSERT(pn->isArity(PN_NAME)); return Fold(cx, &pn->pn_expr, parser, inGenexpLambda); + case PNK_OPTDOT: case PNK_DOT: return FoldDottedProperty(cx, pn, parser, inGenexpLambda); diff --git a/js/src/frontend/FullParseHandler.h b/js/src/frontend/FullParseHandler.h index f346351255..a5a5bd16a0 100644 --- a/js/src/frontend/FullParseHandler.h +++ b/js/src/frontend/FullParseHandler.h @@ -213,6 +213,18 @@ class FullParseHandler if (expr->isKind(PNK_ELEM)) return newUnary(PNK_DELETEELEM, JSOP_NOP, begin, expr); + if (expr->isKind(PNK_OPTCHAIN)) { + ParseNode* kid = expr->pn_kid; + // Handle property deletion explicitly. OptionalCall is handled + // via DeleteExpr. + if (kid->isKind(PNK_DOT) || + kid->isKind(PNK_OPTDOT) || + kid->isKind(PNK_ELEM) || + kid->isKind(PNK_OPTELEM)) { + return newUnary(PNK_DELETEOPTCHAIN, JSOP_NOP, begin, kid); + } + } + return newUnary(PNK_DELETEEXPR, JSOP_NOP, begin, expr); } @@ -319,6 +331,10 @@ class FullParseHandler return newList(PNK_CALL, JSOP_CALL); } + ParseNode* newOptionalCall() { + return newList(PNK_OPTCALL, JSOP_CALL); + } + ParseNode* newTaggedTemplate() { return newList(PNK_TAGGED_TEMPLATE, JSOP_CALL); } @@ -459,6 +475,11 @@ class FullParseHandler return new_(PNK_AWAIT, JSOP_AWAIT, pos, value); } + ParseNode* newOptionalChain(uint32_t begin, ParseNode* value) { + TokenPos pos(begin, value->pn_pos.end); + return new_(PNK_OPTCHAIN, JSOP_NOP, pos, value); + } + // Statements ParseNode* newStatementList(const TokenPos& pos) { @@ -678,6 +699,14 @@ class FullParseHandler return new_(lhs, index, lhs->pn_pos.begin, end); } + ParseNode* newOptionalPropertyAccess(ParseNode* pn, PropertyName* name, uint32_t end) { + return new_(pn, name, pn->pn_pos.begin, end); + } + + ParseNode* newOptionalPropertyByValue(ParseNode* lhs, ParseNode* index, uint32_t end) { + return new_(lhs, index, lhs->pn_pos.begin, end); + } + inline MOZ_MUST_USE bool addCatchBlock(ParseNode* catchList, ParseNode* lexicalScope, ParseNode* catchName, ParseNode* catchGuard, ParseNode* catchBody); diff --git a/js/src/frontend/ParseNode.cpp b/js/src/frontend/ParseNode.cpp index 6233465630..dfbd724e2e 100644 --- a/js/src/frontend/ParseNode.cpp +++ b/js/src/frontend/ParseNode.cpp @@ -228,6 +228,8 @@ PushNodeChildren(ParseNode* pn, NodeStack* stack) return PushUnaryNodeChild(pn, stack); // Nodes with a single nullable child. + case PNK_OPTCHAIN: + case PNK_DELETEOPTCHAIN: case PNK_THIS: case PNK_SEMI: { MOZ_ASSERT(pn->isArity(PN_UNARY)); @@ -253,6 +255,7 @@ PushNodeChildren(ParseNode* pn, NodeStack* stack) case PNK_MODASSIGN: case PNK_POWASSIGN: // ...and a few others. + case PNK_OPTELEM: case PNK_ELEM: case PNK_IMPORT_SPEC: case PNK_EXPORT_SPEC: @@ -449,6 +452,7 @@ PushNodeChildren(ParseNode* pn, NodeStack* stack) case PNK_POW: case PNK_COMMA: case PNK_NEW: + case PNK_OPTCALL: case PNK_CALL: case PNK_SUPERCALL: case PNK_GENEXP: @@ -483,6 +487,7 @@ PushNodeChildren(ParseNode* pn, NodeStack* stack) } case PNK_LABEL: + case PNK_OPTDOT: case PNK_DOT: case PNK_NAME: return PushNameNodeChildren(pn, stack); diff --git a/js/src/frontend/ParseNode.h b/js/src/frontend/ParseNode.h index 8e5e74d3b7..aae2a691bb 100644 --- a/js/src/frontend/ParseNode.h +++ b/js/src/frontend/ParseNode.h @@ -34,6 +34,10 @@ class ObjectBox; F(POSTDECREMENT) \ F(DOT) \ F(ELEM) \ + F(OPTDOT) \ + F(OPTCHAIN) \ + F(OPTELEM) \ + F(OPTCALL) \ F(ARRAY) \ F(ELISION) \ F(STATEMENTLIST) \ @@ -76,6 +80,7 @@ class ObjectBox; F(DELETEPROP) \ F(DELETEELEM) \ F(DELETEEXPR) \ + F(DELETEOPTCHAIN) \ F(TRY) \ F(CATCH) \ F(CATCHLIST) \ @@ -371,6 +376,29 @@ IsTypeofKind(ParseNodeKind kind) * for a more-specific PNK_DELETE* unless constant * folding (or a similar parse tree manipulation) has * occurred + * PNK_DELETEOPTCHAIN unary pn_kid: MEMBER expr that's evaluated, then the + * overall delete evaluates to true; If constant + * folding occurs, PNK_ELEM may become PNK_DOT. + * PNK_OPTELEM does not get folded into PNK_OPTDOT. + * PNK_OPTCHAIN unary pn_kid: expression that is evaluated as a chain. + * Contains one or more optional nodes. Its first node + * is always an optional node, such as PNK_OPTELEM, + * PNK_OPTDOT, or PNK_OPTCALL. This will shortcircuit + * and return 'undefined' without evaluating the rest + * of the expression if any of the optional nodes it + * contains are nullish. An optional chain can also + * contain nodes such as PNK_DOT, PNK_ELEM, PNK_NAME, + * PNK_CALL, etc. These are evaluated normally. + * PNK_OPTDOT name pn_expr: MEMBER expr to left of . + * short circuits back to PNK_OPTCHAIN if nullish. + * pn_atom: name to right of . + * PNK_OPTELEM binary pn_left: MEMBER expr to left of [ + * short circuits back to PNK_OPTCHAIN if nullish. + * pn_right: expr between [ and ] + * PNK_OPTCALL list pn_head: list of call, arg1, arg2, ... argN + * pn_count: 1 + N (where N is number of args) + * call is a MEMBER expr naming a callable object, + * short circuits back to PNK_OPTCHAIN if nullish. * PNK_DOT name pn_expr: MEMBER expr to left of . * pn_atom: name to right of . * PNK_ELEM binary pn_left: MEMBER expr to left of [ @@ -1182,11 +1210,12 @@ class RegExpLiteral : public NullaryNode } }; -class PropertyAccess : public ParseNode +class PropertyAccessBase : public ParseNode { public: - PropertyAccess(ParseNode* lhs, PropertyName* name, uint32_t begin, uint32_t end) - : ParseNode(PNK_DOT, JSOP_NOP, PN_NAME, TokenPos(begin, end)) + PropertyAccessBase(ParseNodeKind kind, ParseNode* lhs, PropertyName* name, + uint32_t begin, uint32_t end) + : ParseNode(kind, JSOP_NOP, PN_NAME, TokenPos(begin, end)) { MOZ_ASSERT(lhs != nullptr); MOZ_ASSERT(name != nullptr); @@ -1195,7 +1224,8 @@ class PropertyAccess : public ParseNode } static bool test(const ParseNode& node) { - bool match = node.isKind(PNK_DOT); + bool match = node.isKind(PNK_DOT) || + node.isKind(PNK_OPTDOT); MOZ_ASSERT_IF(match, node.isArity(PN_NAME)); return match; } @@ -1214,18 +1244,56 @@ class PropertyAccess : public ParseNode } }; -class PropertyByValue : public ParseNode +class PropertyAccess : public PropertyAccessBase +{ + public: + PropertyAccess(ParseNode* lhs, PropertyName* name, uint32_t begin, + uint32_t end) + : PropertyAccessBase(PNK_DOT, lhs, name, begin, end) + { + MOZ_ASSERT(lhs != nullptr); + MOZ_ASSERT(name != nullptr); + } + + static bool test(const ParseNode& node) { + bool match = node.isKind(PNK_DOT); + MOZ_ASSERT_IF(match, node.isArity(PN_NAME)); + return match; + } +}; + +class OptionalPropertyAccess : public PropertyAccessBase +{ + public: + OptionalPropertyAccess(ParseNode* lhs, PropertyName* name, uint32_t begin, + uint32_t end) + : PropertyAccessBase(PNK_OPTDOT, lhs, name, begin, end) + { + MOZ_ASSERT(lhs != nullptr); + MOZ_ASSERT(name != nullptr); + } + + static bool test(const ParseNode& node) { + bool match = node.isKind(PNK_OPTDOT); + MOZ_ASSERT_IF(match, node.isArity(PN_NAME)); + return match; + } +}; + +class PropertyByValueBase : public ParseNode { public: - PropertyByValue(ParseNode* lhs, ParseNode* propExpr, uint32_t begin, uint32_t end) - : ParseNode(PNK_ELEM, JSOP_NOP, PN_BINARY, TokenPos(begin, end)) + PropertyByValueBase(ParseNodeKind kind, ParseNode* lhs, ParseNode* propExpr, + uint32_t begin, uint32_t end) + : ParseNode(kind, JSOP_NOP, PN_BINARY, TokenPos(begin, end)) { pn_u.binary.left = lhs; pn_u.binary.right = propExpr; } static bool test(const ParseNode& node) { - bool match = node.isKind(PNK_ELEM); + bool match = node.isKind(PNK_ELEM) || + node.isKind(PNK_OPTELEM); MOZ_ASSERT_IF(match, node.isArity(PN_BINARY)); return match; } @@ -1235,6 +1303,32 @@ class PropertyByValue : public ParseNode } }; +class PropertyByValue : public PropertyByValueBase { + public: + PropertyByValue(ParseNode* lhs, ParseNode* propExpr, uint32_t begin, + uint32_t end) + : PropertyByValueBase(PNK_ELEM, lhs, propExpr, begin, end) {} + + static bool test(const ParseNode& node) { + bool match = node.isKind(PNK_ELEM); + MOZ_ASSERT_IF(match, node.isArity(PN_BINARY)); + return match; + } +}; + +class OptionalPropertyByValue : public PropertyByValueBase { + public: + OptionalPropertyByValue(ParseNode* lhs, ParseNode* propExpr, + uint32_t begin, uint32_t end) + : PropertyByValueBase(PNK_OPTELEM, lhs, propExpr, begin, end) {} + + static bool test(const ParseNode& node) { + bool match = node.isKind(PNK_OPTELEM); + MOZ_ASSERT_IF(match, node.isArity(PN_BINARY)); + return match; + } +}; + /* * A CallSiteNode represents the implicit call site object argument in a TaggedTemplate. */ diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index 38bd77ead3..cced4a4ea9 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -8361,6 +8361,109 @@ Parser::unaryOpExpr(YieldHandling yieldHandling, ParseNodeKind kin return handler.newUnary(kind, op, begin, kid); } +template +typename ParseHandler::Node +Parser::optionalExpr( + YieldHandling yieldHandling, TripledotHandling tripledotHandling, + TokenKind tt, bool allowCallSyntax /* = true */, + PossibleError* possibleError /* = nullptr */, + InvokedPrediction invoked /* = PredictUninvoked */) +{ + JS_CHECK_RECURSION(context, return null()); + + uint32_t begin = pos().begin; + + Node lhs = memberExpr(yieldHandling, tripledotHandling, tt, + /* allowCallSyntax = */ true, possibleError, invoked); + + if (!lhs) { + return null(); + } + + // Note: TokenStream::None is equivalent to TokenStream::SlashIsDiv + // in current Mozilla code, see bug 1539821. + if (!tokenStream.peekToken(&tt, TokenStream::None)) { + return null(); + } + + if (tt == TOK_EOF || tt != TOK_OPTCHAIN) { + return lhs; + } + + while (true) { + if (!tokenStream.getToken(&tt)) { + return null(); + } + + if (tt == TOK_EOF) { + break; + } + + Node nextMember; + if (tt == TOK_OPTCHAIN) { + if (!tokenStream.getToken(&tt)) { + return null(); + } + if (TokenKindIsPossibleIdentifierName(tt)) { + nextMember = memberPropertyAccess(lhs, OptionalKind::Optional); + if (!nextMember) { + return null(); + } + } else if (tt == TOK_LB) { + nextMember = memberElemAccess(lhs, yieldHandling, + OptionalKind::Optional); + if (!nextMember) { + return null(); + } + } else if (tt == TOK_LP) { + nextMember = memberCall(tt, lhs, yieldHandling, possibleError, + OptionalKind::Optional); + if (!nextMember) { + return null(); + } + } else { + error(JSMSG_NAME_AFTER_DOT); + return null(); + } + } else if (tt == TOK_DOT) { + if (!tokenStream.getToken(&tt)) { + return null(); + } + if (TokenKindIsPossibleIdentifierName(tt)) { + nextMember = memberPropertyAccess(lhs); + if (!nextMember) { + return null(); + } + } else { + error(JSMSG_NAME_AFTER_DOT); + return null(); + } + } else if (tt == TOK_LB) { + nextMember = memberElemAccess(lhs, yieldHandling); + if (!nextMember) { + return null(); + } + } else if (allowCallSyntax && tt == TOK_LP) { + nextMember = memberCall(tt, lhs, yieldHandling, possibleError); + if (!nextMember) { + return null(); + } + } else if (tt == TOK_TEMPLATE_HEAD || tt == TOK_NO_SUBS_TEMPLATE) { + error(JSMSG_BAD_OPTIONAL_TEMPLATE); + return null(); + } else { + tokenStream.ungetToken(); + break; + } + + if (nextMember) { + lhs = nextMember; + } + } + + return handler.newOptionalChain(begin, lhs); +} + template typename ParseHandler::Node Parser::unaryExpr(YieldHandling yieldHandling, TripledotHandling tripledotHandling, @@ -8454,8 +8557,9 @@ Parser::unaryExpr(YieldHandling yieldHandling, TripledotHandling t MOZ_FALLTHROUGH; default: { - Node expr = memberExpr(yieldHandling, tripledotHandling, tt, /* allowCallSyntax = */ true, - possibleError, invoked); + Node expr = optionalExpr(yieldHandling, tripledotHandling, tt, + /* allowCallSyntax = */ true, + possibleError, invoked); if (!expr) return null(); @@ -8904,6 +9008,18 @@ Parser::memberExpr(YieldHandling yieldHandling, TripledotHandling handler.addList(lhs, ctorExpr); + // If we have encountered an optional chain, in the form of `new + // ClassName?.()` then we need to throw, as this is disallowed + // by the spec. + bool optionalToken; + if (!tokenStream.matchToken(&optionalToken, TOK_OPTCHAIN)) { + return null(); + } + if (optionalToken) { + errorAt(newBegin, JSMSG_BAD_NEW_OPTIONAL); + return null(); + } + bool matched; if (!tokenStream.matchToken(&matched, TOK_LP)) return null(); @@ -8941,12 +9057,7 @@ Parser::memberExpr(YieldHandling yieldHandling, TripledotHandling if (!tokenStream.getToken(&tt)) return null(); if (TokenKindIsPossibleIdentifierName(tt)) { - PropertyName* field = tokenStream.currentName(); - if (handler.isSuperBase(lhs) && !checkAndMarkSuperScope()) { - error(JSMSG_BAD_SUPERPROP, "property"); - return null(); - } - nextMember = handler.newPropertyAccess(lhs, field, pos().end); + nextMember = memberPropertyAccess(lhs); if (!nextMember) return null(); } else { @@ -8954,17 +9065,7 @@ Parser::memberExpr(YieldHandling yieldHandling, TripledotHandling return null(); } } else if (tt == TOK_LB) { - Node propExpr = expr(InAllowed, yieldHandling, TripledotProhibited); - if (!propExpr) - return null(); - - MUST_MATCH_TOKEN(TOK_RB, JSMSG_BRACKET_IN_INDEX); - - if (handler.isSuperBase(lhs) && !checkAndMarkSuperScope()) { - error(JSMSG_BAD_SUPERPROP, "member"); - return null(); - } - nextMember = handler.newPropertyByValue(lhs, propExpr, pos().end); + nextMember = memberElemAccess(lhs, yieldHandling); if (!nextMember) return null(); } else if ((allowCallSyntax && tt == TOK_LP) || @@ -9000,82 +9101,17 @@ Parser::memberExpr(YieldHandling yieldHandling, TripledotHandling if (!thisName) return null(); + // XXX This was refactored out into memberSuperCall as part of + // bug 1566143, but was not used elsewhere aside from here, so + // I opted not to move this into a separate function. However, + // I'm unsure if this will be needed eventually in the future. nextMember = handler.newSetThis(thisName, nextMember); if (!nextMember) return null(); } else { - if (options().selfHostingMode && handler.isPropertyAccess(lhs)) { - error(JSMSG_SELFHOSTED_METHOD_CALL); - return null(); - } - - nextMember = tt == TOK_LP ? handler.newCall() : handler.newTaggedTemplate(); + nextMember = memberCall(tt, lhs, yieldHandling, possibleError); if (!nextMember) return null(); - - JSOp op = JSOP_CALL; - bool maybeAsyncArrow = false; - if (PropertyName* prop = handler.maybeDottedProperty(lhs)) { - // Use the JSOP_FUN{APPLY,CALL} optimizations given the - // right syntax. - if (prop == context->names().apply) { - op = JSOP_FUNAPPLY; - if (pc->isFunctionBox()) { - pc->functionBox()->usesApply = true; - } - } else if (prop == context->names().call) { - op = JSOP_FUNCALL; - } - } else if (tt == TOK_LP) { - if (handler.isAsyncKeyword(lhs, context)) { - // |async (| can be the start of an async arrow - // function, so we need to defer reporting possible - // errors from destructuring syntax. To give better - // error messages, we only allow the AsyncArrowHead - // part of the CoverCallExpressionAndAsyncArrowHead - // syntax when the initial name is "async". - maybeAsyncArrow = true; - } else if (handler.isEvalAnyParentheses(lhs, context)) { - // Select the right EVAL op and flag pc as having a - // direct eval. - op = pc->sc()->strict() ? JSOP_STRICTEVAL : JSOP_EVAL; - pc->sc()->setBindingsAccessedDynamically(); - pc->sc()->setHasDirectEval(); - - // In non-strict mode code, direct calls to eval can - // add variables to the call object. - if (pc->isFunctionBox() && !pc->sc()->strict()) - pc->functionBox()->setHasExtensibleScope(); - - // If we're in a method, mark the method as requiring - // support for 'super', since direct eval code can use - // it. (If we're not in a method, that's fine, so - // ignore the return value.) - checkAndMarkSuperScope(); - } - } - - handler.setBeginPosition(nextMember, lhs); - handler.addList(nextMember, lhs); - - if (tt == TOK_LP) { - bool isSpread = false; - PossibleError* asyncPossibleError = maybeAsyncArrow ? possibleError : nullptr; - if (!argumentList(yieldHandling, nextMember, &isSpread, asyncPossibleError)) - return null(); - if (isSpread) { - if (op == JSOP_EVAL) - op = JSOP_SPREADEVAL; - else if (op == JSOP_STRICTEVAL) - op = JSOP_STRICTSPREADEVAL; - else - op = JSOP_SPREADCALL; - } - } else { - if (!taggedTemplate(yieldHandling, nextMember, tt)) - return null(); - } - handler.setOp(nextMember, op); } } else { tokenStream.ungetToken(); @@ -9109,6 +9145,155 @@ Parser::newName(PropertyName* name, TokenPos pos) return handler.newName(name, pos, context); } +template +typename ParseHandler::Node +Parser::memberPropertyAccess( + Node lhs, OptionalKind optionalKind /* = OptionalKind::NonOptional */) +{ + MOZ_ASSERT(TokenKindIsPossibleIdentifierName(tokenStream.currentToken().type)); + PropertyName* field = tokenStream.currentName(); + if (handler.isSuperBase(lhs) && !checkAndMarkSuperScope()) { + error(JSMSG_BAD_SUPERPROP, "property"); + return null(); + } + if (optionalKind == OptionalKind::Optional) { + return handler.newOptionalPropertyAccess(lhs, field, pos().end); + } + return handler.newPropertyAccess(lhs, field, pos().end); +} + +template +typename ParseHandler::Node +Parser::memberElemAccess( + Node lhs, YieldHandling yieldHandling, + OptionalKind optionalKind /* = OptionalKind::NonOptional */) +{ + MOZ_ASSERT(tokenStream.currentToken().type == TOK_LB); + Node propExpr = expr(InAllowed, yieldHandling, TripledotProhibited); + if (!propExpr) { + return null(); + } + + MUST_MATCH_TOKEN(TOK_RB, JSMSG_BRACKET_IN_INDEX); + + if (handler.isSuperBase(lhs) && !checkAndMarkSuperScope()) { + error(JSMSG_BAD_SUPERPROP, "member"); + return null(); + } + + if (optionalKind == OptionalKind::Optional) { + return handler.newOptionalPropertyByValue(lhs, propExpr, pos().end); + } + return handler.newPropertyByValue(lhs, propExpr, pos().end); +} + +template +typename ParseHandler::Node +Parser::memberCall( + TokenKind tt, Node lhs, YieldHandling yieldHandling, + PossibleError* possibleError /* = nullptr */, + OptionalKind optionalKind /* = OptionalKind::NonOptional */) +{ + if (options().selfHostingMode && handler.isPropertyAccess(lhs)) { + error(JSMSG_SELFHOSTED_METHOD_CALL); + return null(); + } + + MOZ_ASSERT(tt == TOK_LP || + tt == TOK_TEMPLATE_HEAD || + tt == TOK_NO_SUBS_TEMPLATE, + "Unexpected token kind for member call"); + + Node nextMember; + if (tt == TOK_LP) { + if (optionalKind == OptionalKind::Optional) { + nextMember = handler.newOptionalCall(); + } else { + nextMember = handler.newCall(); + } + } else { + nextMember = handler.newTaggedTemplate(); + } + + JSOp op = JSOP_CALL; + bool maybeAsyncArrow = false; + if (PropertyName* prop = handler.maybeDottedProperty(lhs)) { + // Use the JSOP_FUN{APPLY,CALL} optimizations given the + // right syntax. + if (prop == context->names().apply) { + op = JSOP_FUNAPPLY; + if (pc->isFunctionBox()) { + pc->functionBox()->usesApply = true; + } + } else if (prop == context->names().call) { + op = JSOP_FUNCALL; + } + } else if (tt == TOK_LP && optionalKind == OptionalKind::NonOptional) { + if (handler.isAsyncKeyword(lhs, context)) { + // |async (| can be the start of an async arrow + // function, so we need to defer reporting possible + // errors from destructuring syntax. To give better + // error messages, we only allow the AsyncArrowHead + // part of the CoverCallExpressionAndAsyncArrowHead + // syntax when the initial name is "async". + maybeAsyncArrow = true; + } else if (handler.isEvalAnyParentheses(lhs, context)) { + // Select the right EVAL op and flag pc as having a + // direct eval. + op = pc->sc()->strict() ? JSOP_STRICTEVAL : JSOP_EVAL; + pc->sc()->setBindingsAccessedDynamically(); + pc->sc()->setHasDirectEval(); + + // In non-strict mode code, direct calls to eval can + // add variables to the call object. + if (pc->isFunctionBox() && !pc->sc()->strict()) { + pc->functionBox()->setHasExtensibleScope(); + } + + // If we're in a method, mark the method as requiring + // support for 'super', since direct eval code can use + // it. (If we're not in a method, that's fine, so + // ignore the return value.) + checkAndMarkSuperScope(); + } + } + + handler.setBeginPosition(nextMember, lhs); + handler.addList(nextMember, lhs); + + if (tt == TOK_LP) { + bool isSpread = false; + PossibleError* asyncPossibleError = maybeAsyncArrow ? + possibleError : + nullptr; + + if (!argumentList(yieldHandling, nextMember, &isSpread, asyncPossibleError)) { + return null(); + } + + if (isSpread) { + if (op == JSOP_EVAL) { + op = JSOP_SPREADEVAL; + } else if (op == JSOP_STRICTEVAL) { + op = JSOP_STRICTSPREADEVAL; + } else { + op = JSOP_SPREADCALL; + } + } + } else { + if (!taggedTemplate(yieldHandling, nextMember, tt)) { + return null(); + } + if (optionalKind == OptionalKind::Optional) { + error(JSMSG_BAD_OPTIONAL_TEMPLATE); + return null(); + } + } + handler.setOp(nextMember, op); + + return nextMember; +} + template bool Parser::checkLabelOrIdentifierReference(HandlePropertyName ident, diff --git a/js/src/frontend/Parser.h b/js/src/frontend/Parser.h index 0e2f28fe88..97f6917bd6 100644 --- a/js/src/frontend/Parser.h +++ b/js/src/frontend/Parser.h @@ -1321,6 +1321,11 @@ class Parser final : public ParserBase, private JS::AutoGCRooter Node unaryExpr(YieldHandling yieldHandling, TripledotHandling tripledotHandling, PossibleError* possibleError = nullptr, InvokedPrediction invoked = PredictUninvoked); + Node optionalExpr(YieldHandling yieldHandling, + TripledotHandling tripledotHandling, TokenKind tt, + bool allowCallSyntax = true, + PossibleError* possibleError = nullptr, + InvokedPrediction invoked = PredictUninvoked); Node memberExpr(YieldHandling yieldHandling, TripledotHandling tripledotHandling, TokenKind tt, bool allowCallSyntax = true, PossibleError* possibleError = nullptr, @@ -1532,6 +1537,18 @@ class Parser final : public ParserBase, private JS::AutoGCRooter JSAtom* prefixAccessorName(PropertyType propType, HandleAtom propAtom); bool asmJS(Node list); + + enum class OptionalKind { + NonOptional = 0, + Optional, + }; + Node memberPropertyAccess( + Node lhs, OptionalKind optionalKind = OptionalKind::NonOptional); + Node memberElemAccess(Node lhs, YieldHandling yieldHandling, + OptionalKind optionalKind = OptionalKind::NonOptional); + Node memberCall(TokenKind tt, Node lhs, YieldHandling yieldHandling, + PossibleError* possibleError, + OptionalKind optionalKind = OptionalKind::NonOptional); }; template diff --git a/js/src/frontend/SyntaxParseHandler.h b/js/src/frontend/SyntaxParseHandler.h index 4fef3584c8..c55db41086 100644 --- a/js/src/frontend/SyntaxParseHandler.h +++ b/js/src/frontend/SyntaxParseHandler.h @@ -58,6 +58,7 @@ class SyntaxParseHandler // in code not actually executed (or at least not executed enough to be // noticed). NodeFunctionCall, + NodeOptionalFunctionCall, // Nodes representing *parenthesized* IsValidSimpleAssignmentTarget // nodes. We can't simply treat all such parenthesized nodes @@ -78,7 +79,9 @@ class SyntaxParseHandler NodeParenthesizedName, NodeDottedProperty, + NodeOptionalDottedProperty, NodeElement, + NodeOptionalElement, // Destructuring target patterns can't be parenthesized: |([a]) = [3];| // must be a syntax error. (We can't use NodeGeneric instead of these @@ -283,6 +286,7 @@ class SyntaxParseHandler void addArrayElement(Node literal, Node element) { } Node newCall() { return NodeFunctionCall; } + Node newOptionalCall() { return NodeOptionalFunctionCall; } Node newTaggedTemplate() { return NodeGeneric; } Node newObjectLiteral(uint32_t begin) { return NodeUnparenthesizedObject; } @@ -303,6 +307,7 @@ class SyntaxParseHandler Node newYieldExpression(uint32_t begin, Node value) { return NodeGeneric; } Node newYieldStarExpression(uint32_t begin, Node value) { return NodeGeneric; } Node newAwaitExpression(uint32_t begin, Node value) { return NodeGeneric; } + Node newOptionalChain(uint32_t begin, Node value) { return NodeGeneric; } // Statements @@ -353,8 +358,15 @@ class SyntaxParseHandler return NodeDottedProperty; } + Node newOptionalPropertyAccess(Node pn, PropertyName* name, uint32_t end) { + lastAtom = name; + return NodeOptionalDottedProperty; + } + Node newPropertyByValue(Node pn, Node kid, uint32_t end) { return NodeElement; } + Node newOptionalPropertyByValue(Node pn, Node kid, uint32_t end) { return NodeOptionalElement; } + MOZ_MUST_USE bool addCatchBlock(Node catchList, Node letBlock, Node catchName, Node catchGuard, Node catchBody) { return true; } @@ -471,7 +483,8 @@ class SyntaxParseHandler list == NodeUnparenthesizedCommaExpr || list == NodeVarDeclaration || list == NodeLexicalDeclaration || - list == NodeFunctionCall); + list == NodeFunctionCall || + list == NodeOptionalFunctionCall); } Node newAssignment(ParseNodeKind kind, Node lhs, Node rhs, JSOp op) { diff --git a/js/src/frontend/TokenKind.h b/js/src/frontend/TokenKind.h index f4e1afa328..27da8ecf3e 100644 --- a/js/src/frontend/TokenKind.h +++ b/js/src/frontend/TokenKind.h @@ -63,6 +63,7 @@ macro(DEC, "'--'") /* decrement */ \ macro(DOT, "'.'") /* member operator */ \ macro(TRIPLEDOT, "'...'") /* rest arguments and spread operator */ \ + macro(OPTCHAIN, "'?.'") \ macro(LB, "'['") \ macro(RB, "']'") \ macro(LC, "'{'") \ diff --git a/js/src/frontend/TokenStream.cpp b/js/src/frontend/TokenStream.cpp index 083bcd5045..8f9e206d9f 100644 --- a/js/src/frontend/TokenStream.cpp +++ b/js/src/frontend/TokenStream.cpp @@ -1285,7 +1285,7 @@ static const uint8_t firstCharKinds[] = { /* 30+ */ _______, _______, Space, _______, String, _______, Ident, _______, _______, String, /* 40+ */ TOK_LP, TOK_RP, _______, _______, T_COMMA,_______, _______, _______,BasePrefix, Dec, /* 50+ */ Dec, Dec, Dec, Dec, Dec, Dec, Dec, Dec, T_COLON,TOK_SEMI, -/* 60+ */ _______, _______, _______,TOK_HOOK, _______, Ident, Ident, Ident, Ident, Ident, +/* 60+ */ _______, _______, _______, _______, _______, Ident, Ident, Ident, Ident, Ident, /* 70+ */ Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, /* 80+ */ Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, Ident, /* 90+ */ Ident, TOK_LB, _______, TOK_RB, _______, Ident, Templat, Ident, Ident, Ident, @@ -1796,6 +1796,25 @@ TokenStream::getTokenInternal(TokenKind* ttp, Modifier modifier) tp->type = matchChar('=') ? TOK_BITANDASSIGN : TOK_BITAND; goto out; + case '?': + if (matchChar('.')) { + c = getCharIgnoreEOL(); + if (JS7_ISDEC(c)) { + // if the code unit is followed by a number, for example it has + // the following form `<...> ?.5 <..> then it should be treated + // as a ternary rather than as an optional chain + tp->type = TOK_HOOK; + ungetCharIgnoreEOL(c); + ungetChar('.'); + } else { + ungetCharIgnoreEOL(c); + tp->type = TOK_OPTCHAIN; + } + } else { + tp->type = TOK_HOOK; + } + goto out; + case '!': if (matchChar('=')) tp->type = matchChar('=') ? TOK_STRICTNE : TOK_NE; diff --git a/js/src/js.msg b/js/src/js.msg index b03da4153d..566b55479b 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -354,6 +354,8 @@ MSG_DEF(JSMSG_BAD_COLUMN_NUMBER, 0, JSEXN_RANGEERR, "column number out of MSG_DEF(JSMSG_COMPUTED_NAME_IN_PATTERN,0, JSEXN_SYNTAXERR, "computed property names aren't supported in this destructuring declaration") MSG_DEF(JSMSG_DEFAULT_IN_PATTERN, 0, JSEXN_SYNTAXERR, "destructuring defaults aren't supported in this destructuring declaration") MSG_DEF(JSMSG_BAD_NEWTARGET, 0, JSEXN_SYNTAXERR, "new.target only allowed within functions") +MSG_DEF(JSMSG_BAD_NEW_OPTIONAL, 0, JSEXN_SYNTAXERR, "new keyword cannot be used with an optional chain") +MSG_DEF(JSMSG_BAD_OPTIONAL_TEMPLATE, 0, JSEXN_SYNTAXERR, "tagged template cannot be used with optional chain") MSG_DEF(JSMSG_ESCAPED_KEYWORD, 0, JSEXN_SYNTAXERR, "keywords must be written literally, without embedded escapes") // asm.js -- cgit v1.2.3