From 4648bc5a38147682a910f263e6e45ff80d4a877c Mon Sep 17 00:00:00 2001 From: Jeff Walden Date: Tue, 10 Feb 2015 00:32:56 -0800 Subject: [PATCH] Bug 1130811 - Examine nodes kind-wise when deciding whether a node contains a hoisted declaration. r=shu --- js/src/frontend/FoldConstants.cpp | 437 ++++++++++++++++++++++++++---- 1 file changed, 379 insertions(+), 58 deletions(-) diff --git a/js/src/frontend/FoldConstants.cpp b/js/src/frontend/FoldConstants.cpp index f125925a9c4..b4e710fad16 100644 --- a/js/src/frontend/FoldConstants.cpp +++ b/js/src/frontend/FoldConstants.cpp @@ -29,67 +29,384 @@ using JS::ToInt32; using JS::ToUint32; static bool -ContainsVarOrConst(ExclusiveContext *cx, ParseNode *pn, ParseNode **resultp) +ContainsHoistedDeclaration(ExclusiveContext *cx, ParseNode *node, bool *result); + +static bool +ListContainsHoistedDeclaration(ExclusiveContext *cx, ListNode *list, bool *result) +{ + for (ParseNode *node = list->pn_head; node; node = node->pn_next) { + if (!ContainsHoistedDeclaration(cx, node, result)) + return false; + if (*result) + return true; + } + + *result = false; + return true; +} + +// Determines whether the given ParseNode contains any declarations whose +// visibility will extend outside the node itself -- that is, whether the +// ParseNode contains any var statements. +// +// THIS IS NOT A GENERAL-PURPOSE FUNCTION. It is only written to work in the +// specific context of deciding that |node|, as one arm of a PNK_IF controlled +// by a constant condition, contains a declaration that forbids |node| being +// completely eliminated as dead. +static bool +ContainsHoistedDeclaration(ExclusiveContext *cx, ParseNode *node, bool *result) { JS_CHECK_RECURSION(cx, return false); - if (!pn) { - *resultp = nullptr; + // With a better-typed AST, we would have distinct parse node classes for + // expressions and for statements and would characterize expressions with + // ExpressionKind and statements with StatementKind. Perhaps someday. In + // the meantime we must characterize every ParseNodeKind, even the + // expression/sub-expression ones that, if we handle all statement kinds + // correctly, we'll never see. + switch (node->getKind()) { + // Base case. + case PNK_VAR: + *result = true; return true; - } - if (pn->isKind(PNK_VAR) || pn->isKind(PNK_CONST)) { - *resultp = pn; + + // Non-global lexical declarations are block-scoped (ergo not hoistable). + // (Global lexical declarations, in addition to being irrelevant here as + // ContainsHoistedDeclaration is only used on the arms of an |if| + // statement, are handled by PNK_GLOBALCONST and PNK_VAR.) + case PNK_LET: + case PNK_CONST: + *result = false; return true; - } - switch (pn->getArity()) { - case PN_LIST: - for (ParseNode *pn2 = pn->pn_head; pn2; pn2 = pn2->pn_next) { - if (!ContainsVarOrConst(cx, pn2, resultp)) - return false; - if (*resultp) + + // ContainsHoistedDeclaration is only called on nested nodes, so any + // instance of this can't be function statements at body level. In + // SpiderMonkey, a binding induced by a function statement is added when + // the function statement is evaluated. Thus any declaration introduced + // by a function statement, as observed by this function, isn't a hoisted + // declaration. + case PNK_FUNCTION: + *result = false; + return true; + + // Statements with no sub-components at all. + case PNK_NOP: // induced by function f() {} function f() {} + case PNK_DEBUGGER: + *result = false; + return true; + + // Statements containing only an expression have no declarations. + case PNK_SEMI: + case PNK_RETURN: + case PNK_THROW: + // These two aren't statements in the spec, but we sometimes insert them + // in statement lists anyway. + case PNK_YIELD_STAR: + case PNK_YIELD: + *result = false; + return true; + + // Other statements with no sub-statement components. + case PNK_BREAK: + case PNK_CONTINUE: + case PNK_IMPORT: + case PNK_IMPORT_SPEC_LIST: + case PNK_IMPORT_SPEC: + case PNK_EXPORT_FROM: + case PNK_EXPORT_SPEC_LIST: + case PNK_EXPORT_SPEC: + case PNK_EXPORT: + case PNK_EXPORT_BATCH_SPEC: + *result = false; + return true; + + // Statements possibly containing hoistable declarations only in the left + // half, in ParseNode terms -- the loop body in AST terms. + case PNK_DOWHILE: + return ContainsHoistedDeclaration(cx, node->pn_left, result); + + // Statements possibly containing hoistable declarations only in the + // right half, in ParseNode terms -- the loop body or nested statement + // (usually a block statement), in AST terms. + case PNK_WHILE: + case PNK_WITH: + return ContainsHoistedDeclaration(cx, node->pn_right, result); + + case PNK_LABEL: + return ContainsHoistedDeclaration(cx, node->pn_expr, result); + + // Statements with more complicated structures. + + // if-statement nodes may have hoisted declarations in their consequent + // and alternative components. + case PNK_IF: { + MOZ_ASSERT(node->isArity(PN_TERNARY)); + + ParseNode *consequent = node->pn_kid2; + if (!ContainsHoistedDeclaration(cx, consequent, result)) + return false; + if (*result) + return true; + + if (ParseNode *alternative = node->pn_kid3) + return ContainsHoistedDeclaration(cx, alternative, result); + + *result = false; + return true; + } + + // Legacy array and generator comprehensions use PNK_IF to represent + // conditions specified in the comprehension tail: for example, + // [x for (x in obj) if (x)]. The consequent of such PNK_IF nodes is + // either PNK_YIELD in a PNK_SEMI statement (generator comprehensions) or + // PNK_ARRAYPUSH (array comprehensions) . The first case is consistent + // with normal if-statement structure with consequent/alternative as + // statements. The second case is abnormal and requires that we not + // banish PNK_ARRAYPUSH to the unreachable list, handling it explicitly. + // + // We could require that this one weird PNK_ARRAYPUSH case be packaged in + // a PNK_SEMI, for consistency. That requires careful bytecode emitter + // adjustment that seems unwarranted for a deprecated feature. + case PNK_ARRAYPUSH: + *result = false; + return true; + + // try-statements have statements to execute, and one or both of a + // catch-list and a finally-block. + case PNK_TRY: { + MOZ_ASSERT(node->isArity(PN_TERNARY)); + MOZ_ASSERT(node->pn_kid2 || node->pn_kid3, + "must have either catch(es) or finally"); + + ParseNode *tryBlock = node->pn_kid1; + if (!ContainsHoistedDeclaration(cx, tryBlock, result)) + return false; + if (*result) + return true; + + if (ParseNode *catchList = node->pn_kid2) { + for (ParseNode *lexicalScope = catchList->pn_head; + lexicalScope; + lexicalScope = lexicalScope->pn_next) + { + MOZ_ASSERT(lexicalScope->isKind(PNK_LEXICALSCOPE)); + + ParseNode *catchNode = lexicalScope->pn_expr; + MOZ_ASSERT(catchNode->isKind(PNK_CATCH)); + + ParseNode *catchStatements = catchNode->pn_kid3; + if (!ContainsHoistedDeclaration(cx, catchStatements, result)) + return false; + if (*result) + return true; + } + } + + if (ParseNode *finallyBlock = node->pn_kid3) + return ContainsHoistedDeclaration(cx, finallyBlock, result); + + *result = false; + return true; + } + + // A switch node's left half is an expression; only its right half (a + // list of cases/defaults, or a block node) could contain hoisted + // declarations. + case PNK_SWITCH: + MOZ_ASSERT(node->isArity(PN_BINARY)); + return ContainsHoistedDeclaration(cx, node->pn_right, result); + + // A case/default node's right half is its statements. A default node's + // left half is null; a case node's left half is its expression. + case PNK_DEFAULT: + MOZ_ASSERT(!node->pn_left); + case PNK_CASE: + MOZ_ASSERT(node->isArity(PN_BINARY)); + return ContainsHoistedDeclaration(cx, node->pn_right, result); + + // PNK_SEQ has two purposes. + // + // The first is to prepend destructuring operations to the body of a + // deprecated function expression closure: irrelevant here, as this + // function doesn't recur into PNK_FUNCTION, and this method's sole + // caller acts upon statements nested in if-statements not found in + // destructuring operations. + // + // The second is to provide a place for a hoisted declaration to go, in + // the bizarre for-in/of loops that have as target a declaration with an + // assignment, e.g. |for (var i = 0 in expr)|. This case is sadly still + // relevant, so we can't banish this ParseNodeKind to the unreachable + // list and must check every list member. + case PNK_SEQ: + return ListContainsHoistedDeclaration(cx, &node->as(), result); + + case PNK_FOR: { + MOZ_ASSERT(node->isArity(PN_BINARY)); + + ParseNode *loopHead = node->pn_left; + MOZ_ASSERT(loopHead->isKind(PNK_FORHEAD) || + loopHead->isKind(PNK_FORIN) || + loopHead->isKind(PNK_FOROF)); + + if (loopHead->isKind(PNK_FORHEAD)) { + // for (init?; cond?; update?), with only init possibly containing + // a hoisted declaration. (Note: a lexical-declaration |init| is + // (at present) hoisted in SpiderMonkey parlance -- but such + // hoisting doesn't extend outside of this statement, so it is not + // hoisting in the sense meant by ContainsHoistedDeclaration.) + MOZ_ASSERT(loopHead->isArity(PN_TERNARY)); + + ParseNode *init = loopHead->pn_kid1; + if (init && init->isKind(PNK_VAR)) { + *result = true; return true; + } + } else { + MOZ_ASSERT(loopHead->isKind(PNK_FORIN) || loopHead->isKind(PNK_FOROF)); + + // for each? (target in ...), where only target may introduce + // hoisted declarations. + // + // -- or -- + // + // for (target of ...), where only target may introduce hoisted + // declarations. + // + // Either way, if |target| contains a declaration, it's either + // |loopHead|'s first kid, *or* that declaration was hoisted to + // become a child of an ancestral PNK_SEQ node. The former case we + // detect here. The latter case is handled by this method + // recurring on PNK_SEQ, above. + MOZ_ASSERT(loopHead->isArity(PN_TERNARY)); + + ParseNode *decl = loopHead->pn_kid1; + if (decl && decl->isKind(PNK_VAR)) { + *result = true; + return true; + } } - break; - case PN_TERNARY: - if (!ContainsVarOrConst(cx, pn->pn_kid1, resultp)) - return false; - if (*resultp) - return true; - if (!ContainsVarOrConst(cx, pn->pn_kid2, resultp)) - return false; - if (*resultp) - return true; - return ContainsVarOrConst(cx, pn->pn_kid3, resultp); + ParseNode *loopBody = node->pn_right; + return ContainsHoistedDeclaration(cx, loopBody, result); + } - case PN_BINARY: - case PN_BINARY_OBJ: - // Limit recursion if pn is a binary expression, which can't contain a - // var statement. - if (!pn->isOp(JSOP_NOP)) { - *resultp = nullptr; - return true; - } - if (!ContainsVarOrConst(cx, pn->pn_left, resultp)) - return false; - if (*resultp) - return true; - return ContainsVarOrConst(cx, pn->pn_right, resultp); + case PNK_LETBLOCK: { + MOZ_ASSERT(node->isArity(PN_BINARY)); + MOZ_ASSERT(node->pn_left->isKind(PNK_LET)); + MOZ_ASSERT(node->pn_right->isKind(PNK_LEXICALSCOPE)); + return ContainsHoistedDeclaration(cx, node->pn_right, result); + } - case PN_UNARY: - if (!pn->isOp(JSOP_NOP)) { - *resultp = nullptr; - return true; - } - return ContainsVarOrConst(cx, pn->pn_kid, resultp); + case PNK_LEXICALSCOPE: { + ParseNode *expr = node->pn_expr; - case PN_NAME: - return ContainsVarOrConst(cx, pn->maybeExpr(), resultp); + if (expr->isKind(PNK_FOR)) + return ContainsHoistedDeclaration(cx, expr, result); - default:; + MOZ_ASSERT(expr->isKind(PNK_STATEMENTLIST)); + return ListContainsHoistedDeclaration(cx, &node->pn_expr->as(), result); + } + + // List nodes with all non-null children. + case PNK_STATEMENTLIST: + return ListContainsHoistedDeclaration(cx, &node->as(), result); + + // Grammar sub-components that should never be reached directly by this + // method, because some parent component should have asserted itself. + case PNK_COMPUTED_NAME: + case PNK_SPREAD: + case PNK_MUTATEPROTO: + case PNK_COLON: + case PNK_SHORTHAND: + case PNK_CONDITIONAL: + case PNK_TYPEOF: + case PNK_VOID: + case PNK_NOT: + case PNK_BITNOT: + case PNK_DELETE: + case PNK_POS: + case PNK_NEG: + case PNK_PREINCREMENT: + case PNK_POSTINCREMENT: + case PNK_PREDECREMENT: + case PNK_POSTDECREMENT: + case PNK_OR: + case PNK_AND: + case PNK_BITOR: + case PNK_BITXOR: + case PNK_BITAND: + case PNK_STRICTEQ: + case PNK_EQ: + case PNK_STRICTNE: + case PNK_NE: + case PNK_LT: + case PNK_LE: + case PNK_GT: + case PNK_GE: + case PNK_INSTANCEOF: + case PNK_IN: + case PNK_LSH: + case PNK_RSH: + case PNK_URSH: + case PNK_ADD: + case PNK_SUB: + case PNK_STAR: + case PNK_DIV: + case PNK_MOD: + case PNK_ASSIGN: + case PNK_ADDASSIGN: + case PNK_SUBASSIGN: + case PNK_BITORASSIGN: + case PNK_BITXORASSIGN: + case PNK_BITANDASSIGN: + case PNK_LSHASSIGN: + case PNK_RSHASSIGN: + case PNK_URSHASSIGN: + case PNK_MULASSIGN: + case PNK_DIVASSIGN: + case PNK_MODASSIGN: + case PNK_COMMA: + case PNK_ARRAY: + case PNK_OBJECT: + case PNK_DOT: + case PNK_ELEM: + case PNK_CALL: + case PNK_NAME: + case PNK_TEMPLATE_STRING: + case PNK_TEMPLATE_STRING_LIST: + case PNK_TAGGED_TEMPLATE: + case PNK_CALLSITEOBJ: + case PNK_STRING: + case PNK_REGEXP: + case PNK_TRUE: + case PNK_FALSE: + case PNK_NULL: + case PNK_THIS: + case PNK_LETEXPR: + case PNK_ELISION: + case PNK_NUMBER: + case PNK_NEW: + case PNK_GENERATOR: + case PNK_GENEXP: + case PNK_ARRAYCOMP: + case PNK_ARGSBODY: + case PNK_CATCHLIST: + case PNK_CATCH: + case PNK_FORIN: + case PNK_FOROF: + case PNK_FORHEAD: + MOZ_CRASH("ContainsHoistedDeclaration should have indicated false on " + "some parent node without recurring to test this node"); + + case PNK_GLOBALCONST: + MOZ_CRASH("ContainsHoistedDeclaration is only called on nested nodes where " + "globalconst nodes should never have been generated"); + + case PNK_LIMIT: // invalid sentinel value + MOZ_CRASH("unexpected PNK_LIMIT in node"); } - *resultp = nullptr; - return true; + + MOZ_CRASH("invalid node kind"); } /* @@ -424,7 +741,7 @@ Fold(ExclusiveContext *cx, ParseNode **pnp, // with node indicating a different syntactic form; |delete x| is not // the same as |delete (true && x)|. See bug 888002. // - // pn is the immediate child in question. Its descendents were already + // pn is the immediate child in question. Its descendants were already // constant-folded above, so we're done. if (sc == SyntacticContext::Delete) return true; @@ -432,15 +749,19 @@ Fold(ExclusiveContext *cx, ParseNode **pnp, switch (pn->getKind()) { case PNK_IF: { - ParseNode *decl; - if (!ContainsVarOrConst(cx, pn2, &decl)) - return false; - if (decl) - break; - if (!ContainsVarOrConst(cx, pn3, &decl)) - return false; - if (decl) - break; + bool result; + if (ParseNode *consequent = pn2) { + if (!ContainsHoistedDeclaration(cx, consequent, &result)) + return false; + if (result) + break; + } + if (ParseNode *alternative = pn3) { + if (!ContainsHoistedDeclaration(cx, alternative, &result)) + return false; + if (result) + break; + } } /* FALL THROUGH */