[strong] Allow mutually recursive classes.
authormarja <marja@chromium.org>
Thu, 16 Apr 2015 14:13:03 +0000 (07:13 -0700)
committerCommit bot <commit-bot@chromium.org>
Thu, 16 Apr 2015 14:12:52 +0000 (14:12 +0000)
The previous restrictions were overshooting (didn't allow a class to refer to a
later class under any circumstances); after this CL we're undershooting (allow
referring to any class from inside a method).

Implementing the correct checks (allow referring only if the class declarations
are in a consecutive block and if there's no dependency cycle) will be
implemented as a follow up.

BUG=v8:3956
LOG=N

Review URL: https://codereview.chromium.org/1087543004

Cr-Commit-Position: refs/heads/master@{#27888}

src/ast.h
src/parser.cc
src/scopes.cc
src/variables.h
test/mjsunit/strong/declaration-after-use.js
test/mjsunit/strong/mutually-recursive-classes.js [new file with mode: 0644]

index afb0684053114f370c8442997910924e397f2bca..7040ff78350bcfdc524114752ca33e559b0f1199 100644 (file)
--- a/src/ast.h
+++ b/src/ast.h
@@ -568,14 +568,15 @@ class VariableDeclaration FINAL : public Declaration {
     return mode() == VAR ? kCreatedInitialized : kNeedsInitialization;
   }
 
+  bool is_class_declaration() const { return is_class_declaration_; }
+
  protected:
-  VariableDeclaration(Zone* zone,
-                      VariableProxy* proxy,
-                      VariableMode mode,
-                      Scope* scope,
-                      int pos)
-      : Declaration(zone, proxy, mode, scope, pos) {
-  }
+  VariableDeclaration(Zone* zone, VariableProxy* proxy, VariableMode mode,
+                      Scope* scope, int pos, bool is_class_declaration = false)
+      : Declaration(zone, proxy, mode, scope, pos),
+        is_class_declaration_(is_class_declaration) {}
+
+  bool is_class_declaration_;
 };
 
 
@@ -3215,11 +3216,11 @@ class AstNodeFactory FINAL BASE_EMBEDDED {
       : zone_(ast_value_factory->zone()),
         ast_value_factory_(ast_value_factory) {}
 
-  VariableDeclaration* NewVariableDeclaration(VariableProxy* proxy,
-                                              VariableMode mode,
-                                              Scope* scope,
-                                              int pos) {
-    return new (zone_) VariableDeclaration(zone_, proxy, mode, scope, pos);
+  VariableDeclaration* NewVariableDeclaration(
+      VariableProxy* proxy, VariableMode mode, Scope* scope, int pos,
+      bool is_class_declaration = false) {
+    return new (zone_) VariableDeclaration(zone_, proxy, mode, scope, pos,
+                                           is_class_declaration);
   }
 
   FunctionDeclaration* NewFunctionDeclaration(VariableProxy* proxy,
index 5c61ef970fe634d1afc50392a3666c8be81b13ff..5f794c23c999a740ad1e0f0c9bb2c60286bf8a6e 100644 (file)
@@ -1948,11 +1948,15 @@ Variable* Parser::Declare(Declaration* declaration, bool resolve, bool* ok) {
     var = declaration_scope->LookupLocal(name);
     if (var == NULL) {
       // Declare the name.
+      Variable::Kind kind = Variable::NORMAL;
+      if (declaration->IsFunctionDeclaration()) {
+        kind = Variable::FUNCTION;
+      } else if (declaration->IsVariableDeclaration() &&
+                 declaration->AsVariableDeclaration()->is_class_declaration()) {
+        kind = Variable::CLASS;
+      }
       var = declaration_scope->DeclareLocal(
-          name, mode, declaration->initialization(),
-          declaration->IsFunctionDeclaration() ? Variable::FUNCTION
-                                               : Variable::NORMAL,
-          kNotAssigned);
+          name, mode, declaration->initialization(), kind, kNotAssigned);
     } else if (IsLexicalVariableMode(mode) ||
                IsLexicalVariableMode(var->mode()) ||
                ((mode == CONST_LEGACY || var->mode() == CONST_LEGACY) &&
@@ -2170,8 +2174,9 @@ Statement* Parser::ParseClassDeclaration(ZoneList<const AstRawString*>* names,
 
   VariableMode mode = is_strong(language_mode()) ? CONST : LET;
   VariableProxy* proxy = NewUnresolved(name, mode);
-  Declaration* declaration =
-      factory()->NewVariableDeclaration(proxy, mode, scope_, pos);
+  const bool is_class_declaration = true;
+  Declaration* declaration = factory()->NewVariableDeclaration(
+      proxy, mode, scope_, pos, is_class_declaration);
   Declare(declaration, true, CHECK_OK);
   proxy->var()->set_initializer_position(position());
 
index b796452e00589b17b837a0eb6b5224885d9c389b..364f2f1cf7ce88ca08e207172c1cba1613dbb3f7 100644 (file)
@@ -1138,6 +1138,30 @@ bool Scope::CheckStrongModeDeclaration(VariableProxy* proxy, Variable* var) {
     scope = scope->outer_scope();
   }
 
+  // Allow references from methods to classes declared later, if we detect no
+  // problematic dependency cycles.
+
+  if (ClassVariableForMethod() && var->is_class()) {
+    // A method is referring to some other class, possibly declared
+    // later. Referring to a class declared earlier is always OK and covered by
+    // the code outside this if. Here we only need to allow special cases for
+    // referring to a class which is declared later.
+
+    // Referring to a class C declared later is OK under the following
+    // circumstances:
+
+    // 1. The class declarations are in a consecutive group with no other
+    // declarations or statements in between, and
+
+    // 2. There is no dependency cycle where the first edge is an initialization
+    // time dependency (computed property name or extends clause) from C to
+    // something that depends on this class directly or transitively.
+
+    // TODO(marja,rossberg): implement these checks. Here we undershoot the
+    // target and allow referring to any class.
+    return true;
+  }
+
   // If both the use and the declaration are inside an eval scope (possibly
   // indirectly), or one of them is, we need to check whether they are inside
   // the same eval scope or different ones.
index 545c3bd1f5bfae92e9adbe9c013e4dd65f2331fa..de7f39045af934d8a7d13320f58cbc8cb88588a2 100644 (file)
@@ -18,7 +18,7 @@ namespace internal {
 
 class Variable: public ZoneObject {
  public:
-  enum Kind { NORMAL, FUNCTION, THIS, NEW_TARGET, ARGUMENTS };
+  enum Kind { NORMAL, FUNCTION, CLASS, THIS, NEW_TARGET, ARGUMENTS };
 
   enum Location {
     // Before and during variable allocation, a variable whose location is
@@ -97,6 +97,7 @@ class Variable: public ZoneObject {
   }
 
   bool is_function() const { return kind_ == FUNCTION; }
+  bool is_class() const { return kind_ == CLASS; }
   bool is_this() const { return kind_ == THIS; }
   bool is_new_target() const { return kind_ == NEW_TARGET; }
   bool is_arguments() const { return kind_ == ARGUMENTS; }
index 3f5039249bad827c4ac2405892957b6aeee9e9cd..020067c1e5aa3c56d8da5e58788235a868b987ac 100644 (file)
@@ -33,6 +33,10 @@ function assertThrowsHelper(code) {
   assertThrowsHelper("function f() { x; let x = 0; }");
   assertThrowsHelper("function f() { x; } let x = 0;");
 
+  assertThrowsHelper("x; const x = 0;");
+  assertThrowsHelper("function f() { x; const x = 0; }");
+  assertThrowsHelper("function f() { x; } const x = 0;");
+
   // These tests needs to be done a bit more manually, since var is not allowed
   // in strong mode:
   assertThrows(
@@ -78,6 +82,16 @@ function assertThrowsHelper(code) {
 
 
 (function DeclarationAfterUseInClasses() {
+  // Referring to a variable declared later
+  assertThrowsHelper("class C { m() { x; } } let x = 0;");
+  assertThrowsHelper("class C { static m() { x; } } let x = 0;");
+  assertThrowsHelper("class C { [x]() { } } let x = 0;");
+
+  assertThrowsHelper("class C { m() { x; } } const x = 0;");
+  assertThrowsHelper("class C { static m() { x; } } const x = 0;");
+  assertThrowsHelper("class C { [x]() { } } const x = 0;");
+
+  // Referring to the class name.
   assertThrowsHelper("class C extends C { }");
   assertThrowsHelper("let C = class C2 extends C { }");
   assertThrowsHelper("let C = class C2 extends C2 { }");
diff --git a/test/mjsunit/strong/mutually-recursive-classes.js b/test/mjsunit/strong/mutually-recursive-classes.js
new file mode 100644 (file)
index 0000000..c8d6fad
--- /dev/null
@@ -0,0 +1,77 @@
+// Copyright 2015 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --strong-mode
+
+// Note that it's essential for these tests that the reference is inside dead
+// code (because we already produce ReferenceErrors for run-time unresolved
+// variables and don't want to confuse those with strong mode errors). But the
+// errors should *not* be inside lazy, unexecuted functions, since lazy parsing
+// doesn't produce strong mode scoping errors).
+
+// In addition, assertThrows will call eval and that changes variable binding
+// types (see e.g., UNBOUND_EVAL_SHADOWED). We can avoid unwanted side effects
+// by wrapping the code to be tested inside an outer function.
+function assertThrowsHelper(code) {
+  "use strict";
+  let prologue_dead = "(function outer() { if (false) { ";
+  let epilogue_dead = " } })();";
+
+  assertThrows("'use strong'; " + prologue_dead + code + epilogue_dead, ReferenceError);
+
+  // Make sure the error happens only in strong mode (note that we need strict
+  // mode here because of let).
+  assertDoesNotThrow("'use strict'; " + prologue_dead + code + epilogue_dead);
+
+  // But if we don't put the references inside a dead code, it throws a run-time
+  // error (also in strict mode).
+  let prologue_live = "(function outer() { ";
+  let epilogue_live = "})();";
+
+  assertThrows("'use strong'; " + prologue_live + code + epilogue_live, ReferenceError);
+  assertThrows("'use strict'; " + prologue_live + code + epilogue_live, ReferenceError);
+}
+
+(function InitTimeReferenceForward() {
+  // It's never OK to have an init time reference to a class which hasn't been
+  // declared.
+  assertThrowsHelper(
+      `class A extends B { };
+      class B {}`);
+
+  assertThrowsHelper(
+      `class A {
+        [B.sm()]() { }
+      };
+      class B {
+        static sm() { return 0; }
+      }`);
+})();
+
+(function InitTimeReferenceBackward() {
+  // Backwards is of course fine.
+  "use strong";
+  class A {
+    static sm() { return 0; }
+  };
+  let i = "making these classes non-consecutive";
+  class B extends A {};
+  "by inserting statements and declarations in between";
+  class C {
+    [A.sm()]() { }
+  };
+})();
+
+(function BasicMutualRecursion() {
+  "use strong";
+  class A {
+    m() { B; }
+    static sm() { B; }
+  };
+  // No statements or declarations between the classes.
+  class B {
+    m() { A; }
+    static sm() { A; }
+  };
+})();