Bug 1130811 - Examine nodes kind-wise when deciding whether a node contains a hoisted declaration. r=shu
authorJeff Walden <jwalden@mit.edu>
Tue, 10 Feb 2015 00:32:56 -0800
changeset 242203 5a850bc4ea8fe8cee9fb030373239c80f112095f
parent 242202 f28e0ce04e93863d08d6b51e7cc75553cd3b19f9
child 242204 b560b5afe0c48dfc380e4b5d55067bc9ed106df4
push id649
push userwcosta@mozilla.com
push dateWed, 11 Feb 2015 16:57:44 +0000
reviewersshu
bugs1130811
milestone38.0a1
Bug 1130811 - Examine nodes kind-wise when deciding whether a node contains a hoisted declaration. r=shu
js/src/frontend/FoldConstants.cpp
--- a/js/src/frontend/FoldConstants.cpp
+++ b/js/src/frontend/FoldConstants.cpp
@@ -24,77 +24,394 @@ using mozilla::IsNaN;
 using mozilla::IsNegative;
 using mozilla::NegativeInfinity;
 using mozilla::PositiveInfinity;
 using JS::GenericNaN;
 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;
+
+      // 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;
+
+      // 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;
-    }
-    if (pn->isKind(PNK_VAR) || pn->isKind(PNK_CONST)) {
-        *resultp = pn;
+
+      // 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;
-    }
-    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)
-                return true;
-        }
-        break;
+
+      // 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;
 
-      case PN_TERNARY:
-        if (!ContainsVarOrConst(cx, pn->pn_kid1, resultp))
+      // 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 (*resultp)
+        if (*result)
             return true;
-        if (!ContainsVarOrConst(cx, pn->pn_kid2, resultp))
-            return false;
-        if (*resultp)
-            return true;
-        return ContainsVarOrConst(cx, pn->pn_kid3, resultp);
+
+        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);
 
-      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;
+      // 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<ListNode>(), 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;
+            }
         }
-        if (!ContainsVarOrConst(cx, pn->pn_left, resultp))
-            return false;
-        if (*resultp)
-            return true;
-        return ContainsVarOrConst(cx, pn->pn_right, resultp);
+
+        ParseNode *loopBody = node->pn_right;
+        return ContainsHoistedDeclaration(cx, loopBody, result);
+      }
+
+      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 PNK_LEXICALSCOPE: {
+        ParseNode *expr = node->pn_expr;
+
+        if (expr->isKind(PNK_FOR))
+            return ContainsHoistedDeclaration(cx, expr, result);
+
+        MOZ_ASSERT(expr->isKind(PNK_STATEMENTLIST));
+        return ListContainsHoistedDeclaration(cx, &node->pn_expr->as<ListNode>(), result);
+      }
+
+      // List nodes with all non-null children.
+      case PNK_STATEMENTLIST:
+        return ListContainsHoistedDeclaration(cx, &node->as<ListNode>(), result);
 
-      case PN_UNARY:
-        if (!pn->isOp(JSOP_NOP)) {
-            *resultp = nullptr;
-            return true;
-        }
-        return ContainsVarOrConst(cx, pn->pn_kid, resultp);
+      // 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 PN_NAME:
-        return ContainsVarOrConst(cx, pn->maybeExpr(), resultp);
+      case PNK_GLOBALCONST:
+        MOZ_CRASH("ContainsHoistedDeclaration is only called on nested nodes where "
+                  "globalconst nodes should never have been generated");
 
-      default:;
+      case PNK_LIMIT: // invalid sentinel value
+        MOZ_CRASH("unexpected PNK_LIMIT in node");
     }
-    *resultp = nullptr;
-    return true;
+
+    MOZ_CRASH("invalid node kind");
 }
 
 /*
  * Fold from one constant type to another.
  * XXX handles only strings and numbers for now
  */
 static bool
 FoldType(ExclusiveContext *cx, ParseNode *pn, ParseNodeKind kind)
@@ -419,33 +736,37 @@ Fold(ExclusiveContext *cx, ParseNode **p
       case PN_NULLARY:
         break;
     }
 
     // The immediate child of a PNK_DELETE node should not be replaced
     // 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;
 
     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 */
 
       case PNK_CONDITIONAL:
         /* Reduce 'if (C) T; else E' into T for true C, E for false. */
         switch (pn1->getKind()) {
           case PNK_NUMBER:
             if (pn1->pn_dval == 0 || IsNaN(pn1->pn_dval))