Generative constructor factories for native objects

The generative constructor factory function 'upgrades' the pre-existing native object.

Future improvements:

1. The lookup of the constructor function could be handled via some kind of reflection.  When relection is finished, we should see if we can 'slice' it to provide the small footprint of the current table and lookup function.

2. Inlining of generative constructor bodies is disabled for native classes - it is broken and needs investigation.

3. dart2js should prevent 'new X.c()' where X.c is a native class generative constructor.

4. dart2js should warn on native class generative constructors that have arguments (the only scenario where arguments work is via superconstructor calls - this might be useful. I have not tested redirections.)

R=kasperl@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@28278 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/sdk/lib/_internal/compiler/implementation/elements/elements.dart b/sdk/lib/_internal/compiler/implementation/elements/elements.dart
index 09be16f..d0e370e 100644
--- a/sdk/lib/_internal/compiler/implementation/elements/elements.dart
+++ b/sdk/lib/_internal/compiler/implementation/elements/elements.dart
@@ -349,6 +349,7 @@
   static bool isNativeOrExtendsNative(ClassElement element) {
     if (element == null) return false;
     if (element.isNative()) return true;
+    assert(element.resolutionState == STATE_DONE);
     return isNativeOrExtendsNative(element.superclass);
   }
 
diff --git a/sdk/lib/_internal/compiler/implementation/js_backend/backend.dart b/sdk/lib/_internal/compiler/implementation/js_backend/backend.dart
index 754e5aa..a60feea 100644
--- a/sdk/lib/_internal/compiler/implementation/js_backend/backend.dart
+++ b/sdk/lib/_internal/compiler/implementation/js_backend/backend.dart
@@ -477,9 +477,11 @@
   }
 
   bool isInterceptedMethod(Element element) {
-    return element.isInstanceMember()
-        && !element.isGenerativeConstructorBody()
-        && interceptedElements[element.name] != null;
+    if (!element.isInstanceMember()) return false;
+    if (element.isGenerativeConstructorBody()) {
+      return Elements.isNativeOrExtendsNative(element.getEnclosingClass());
+    }
+    return interceptedElements[element.name] != null;
   }
 
   bool fieldHasInterceptedGetter(Element element) {
@@ -952,6 +954,30 @@
     if (element.isTypedef()) {
       typedefTypeLiterals.add(element);
     }
+    if (element.isClass()) {
+      // TODO(sra): Can we register via a type parameter?
+      registerEscapingConstructorsOfClass(element, enqueuer);
+    }
+  }
+
+  void registerEscapingConstructorsOfClass(ClassElement classElement,
+                                           Enqueuer enqueuer) {
+    // Web component classes have constructors that are escaped to the host
+    // environment.
+    // TODO(13835): Defer registering generative constructors until the helper
+    // functions that fetch the constructors is seen.  These functions are
+    // called by document.register.
+    classElement.ensureResolved(compiler);
+    if (Elements.isNativeOrExtendsNative(classElement)) {
+      registerGenerativeConstructors(ClassElement enclosing, Element member) {
+        if (member.isGenerativeConstructor()) {
+          enqueuer.registerStaticUse(member);
+        }
+      }
+      classElement.forEachMember(registerGenerativeConstructors,
+          includeBackendMembers: false,
+          includeSuperAndInjectedMembers: false);
+    }
   }
 
   void registerStackTraceInCatch(TreeElements elements) {
diff --git a/sdk/lib/_internal/compiler/implementation/js_emitter/code_emitter_task.dart b/sdk/lib/_internal/compiler/implementation/js_emitter/code_emitter_task.dart
index e9d9642..cea45c9 100644
--- a/sdk/lib/_internal/compiler/implementation/js_emitter/code_emitter_task.dart
+++ b/sdk/lib/_internal/compiler/implementation/js_emitter/code_emitter_task.dart
@@ -1493,6 +1493,8 @@
   void emitMapTypeToInterceptor(CodeBuffer buffer) {
     // TODO(sra): Perhaps inject a constant instead?
     // TODO(sra): Filter by subclasses of native types.
+    // TODO(13835): Don't generate this unless we generated the functions that
+    // use the data structure.
     List<jsAst.Expression> elements = <jsAst.Expression>[];
     ConstantHandler handler = compiler.constantHandler;
     List<Constant> constants = handler.getConstantsForEmission();
@@ -1504,6 +1506,38 @@
           ClassElement classElement = element;
           elements.add(backend.emitter.constantReference(constant));
           elements.add(js(namer.isolateAccess(classElement)));
+
+          // Create JavaScript Object map for by-name lookup of generative
+          // constructors.  For example, the class A has three generative
+          // constructors
+          //
+          //     class A {
+          //       A() {}
+          //       A.foo() {}
+          //       A.bar() {}
+          //     }
+          //
+          // Which are described by the map
+          //
+          //     {"": A.A$, "foo": A.A$foo, "bar": A.A$bar}
+          //
+          // We expect most of the time the map will be a singleton.
+          var properties = [];
+          classElement.forEachMember(
+              (ClassElement enclosingClass, Element member) {
+                if (member.isGenerativeConstructor()) {
+                  properties.add(
+                      new jsAst.Property(
+                          js.string(member.name.slowToString()),
+                          new jsAst.VariableUse(
+                              backend.namer.isolateAccess(member))));
+                }
+              },
+              includeBackendMembers: false,
+              includeSuperAndInjectedMembers: false);
+
+          var map = new jsAst.ObjectInitializer(properties);
+          elements.add(map);
         }
       }
     }
diff --git a/sdk/lib/_internal/compiler/implementation/ssa/builder.dart b/sdk/lib/_internal/compiler/implementation/ssa/builder.dart
index 0541709..60bb2ad 100644
--- a/sdk/lib/_internal/compiler/implementation/ssa/builder.dart
+++ b/sdk/lib/_internal/compiler/implementation/ssa/builder.dart
@@ -275,6 +275,8 @@
     // classes, or the same as [:this:] for non-intercepted classes.
     ClassElement cls = element.getEnclosingClass();
     JavaScriptBackend backend = compiler.backend;
+    bool isNativeUpgradeFactory = element.isGenerativeConstructor()
+        && Elements.isNativeOrExtendsNative(cls);
     if (backend.isInterceptedMethod(element)) {
       bool isInterceptorClass = backend.isInterceptorClass(cls.declaration);
       SourceString name = isInterceptorClass
@@ -291,6 +293,14 @@
         directLocals[closureData.thisElement] = value;
       }
       value.instructionType = builder.getTypeOfThis();
+    } else if (isNativeUpgradeFactory) {
+      bool isInterceptorClass = backend.isInterceptorClass(cls.declaration);
+      Element parameter = new InterceptedElement(
+          cls.computeType(compiler), const SourceString('receiver'), element);
+      HParameterValue value = new HParameterValue(parameter);
+      builder.graph.explicitReceiverParameter = value;
+      builder.graph.entry.addAtEntry(value);
+      value.instructionType = builder.getTypeOfThis();
     }
   }
 
@@ -1267,6 +1277,13 @@
         }
       }
 
+      // Generative constructors of native classes should not be called directly
+      // and have an extra argument that causes problems with inlining.
+      if (element.isGenerativeConstructor()
+          && Elements.isNativeOrExtendsNative(element.getEnclosingClass())) {
+        return false;
+      }
+
       // A generative constructor body is not seen by global analysis,
       // so we should not query for its type.
       if (!element.isGenerativeConstructorBody()) {
@@ -1586,9 +1603,13 @@
             TreeElements definitions = compiler.analyzeElement(member);
             Node node = member.parseNode(compiler);
             SendSet assignment = node.asSendSet();
-            HInstruction value;
             if (assignment == null) {
-              value = graph.addConstantNull(compiler);
+              // Unassigned fields of native classes are not initialized to
+              // prevent overwriting pre-initialized native properties.
+              if (!Elements.isNativeOrExtendsNative(classElement)) {
+                HInstruction value = graph.addConstantNull(compiler);
+                fieldValues[member] = value;
+              }
             } else {
               Node right = assignment.arguments.head;
               TreeElements savedElements = elements;
@@ -1599,9 +1620,9 @@
                   member, node, elements);
               inlinedFrom(member, () => right.accept(this));
               elements = savedElements;
-              value = pop();
+              HInstruction value = pop();
+              fieldValues[member] = value;
             }
-            fieldValues[member] = value;
           });
         });
   }
@@ -1619,6 +1640,8 @@
     functionElement = functionElement.implementation;
     ClassElement classElement =
         functionElement.getEnclosingClass().implementation;
+    bool isNativeUpgradeFactory =
+        Elements.isNativeOrExtendsNative(classElement);
     FunctionExpression function = functionElement.parseNode(compiler);
     // Note that constructors (like any other static function) do not need
     // to deal with optional arguments. It is the callers job to provide all
@@ -1654,10 +1677,20 @@
 
     // Call the JavaScript constructor with the fields as argument.
     List<HInstruction> constructorArguments = <HInstruction>[];
+    List<Element> fields = <Element>[];
+
     classElement.forEachInstanceField(
         (ClassElement enclosingClass, Element member) {
-          constructorArguments.add(potentiallyCheckType(
-              fieldValues[member], member.computeType(compiler)));
+          HInstruction value = fieldValues[member];
+          if (value == null) {
+            // Uninitialized native fields are pre-initialized by the native
+            // implementation.
+            assert(isNativeUpgradeFactory);
+          } else {
+            fields.add(member);
+            constructorArguments.add(
+                potentiallyCheckType(value, member.computeType(compiler)));
+          }
         },
         includeSuperAndInjectedMembers: true);
 
@@ -1668,11 +1701,25 @@
     if (!currentInlinedInstantiations.isEmpty) {
       instantiatedTypes = new List<DartType>.from(currentInlinedInstantiations);
     }
-    HForeignNew newObject = new HForeignNew(classElement,
-                                            ssaType,
-                                            constructorArguments,
-                                            instantiatedTypes);
-    add(newObject);
+
+    HInstruction newObject;
+    if (!isNativeUpgradeFactory) {
+      newObject = new HForeignNew(classElement,
+          ssaType,
+          constructorArguments,
+          instantiatedTypes);
+      add(newObject);
+    } else {
+      // Bulk assign to the initialized fields.
+      newObject = graph.explicitReceiverParameter;
+      // Null guard ensures an error if we are being called from an explicit
+      // 'new' of the constructor instead of via an upgrade. It is optimized out
+      // if there are field initializers.
+      add(new HFieldGet(null, newObject, isAssignable: false));
+      for (int i = 0; i < fields.length; i++) {
+        add(new HFieldSet(fields[i], newObject, constructorArguments[i]));
+      }
+    }
     removeInlinedInstantiation(type);
     // Create the runtime type information, if needed.
     if (backend.classNeedsRti(classElement)) {
@@ -1684,12 +1731,22 @@
     }
 
     // Generate calls to the constructor bodies.
+    HInstruction interceptor = null;
     for (int index = constructors.length - 1; index >= 0; index--) {
       FunctionElement constructor = constructors[index];
       assert(invariant(functionElement, constructor.isImplementation));
       ConstructorBodyElement body = getConstructorBody(constructor);
       if (body == null) continue;
+
       List bodyCallInputs = <HInstruction>[];
+      if (isNativeUpgradeFactory) {
+        if (interceptor == null) {
+          Constant constant = new InterceptorConstant(
+              classElement.computeType(compiler));
+          interceptor = graph.addConstant(constant, compiler);
+        }
+        bodyCallInputs.add(interceptor);
+      }
       bodyCallInputs.add(newObject);
       TreeElements elements =
           compiler.enqueuer.resolution.getCachedElements(constructor);
@@ -1697,7 +1754,6 @@
       ClosureClassMap parameterClosureData =
           compiler.closureToClassMapper.getMappingForNestedFunction(node);
 
-
       FunctionSignature functionSignature = body.computeSignature(compiler);
       // Provide the parameters to the generative constructor body.
       functionSignature.orderedForEachParameter((parameter) {
@@ -1724,7 +1780,8 @@
         bodyCallInputs.add(localsHandler.readLocal(scopeData.boxElement));
       }
 
-      if (tryInlineMethod(body, null, bodyCallInputs, function)) {
+      if (!isNativeUpgradeFactory && // TODO(13836): Fix inlining.
+          tryInlineMethod(body, null, bodyCallInputs, function)) {
         pop();
       } else {
         HInvokeConstructorBody invoke =
@@ -3565,6 +3622,11 @@
     }
 
     var inputs = <HInstruction>[];
+    if (constructor.isGenerativeConstructor() &&
+        Elements.isNativeOrExtendsNative(constructor.getEnclosingClass())) {
+      // Native class generative constructors take a pre-constructed object.
+      inputs.add(graph.addConstantNull(compiler));
+    }
     // TODO(5347): Try to avoid the need for calling [implementation] before
     // calling [addStaticSendArgumentsToList].
     bool succeeded = addStaticSendArgumentsToList(selector, send.arguments,
diff --git a/sdk/lib/_internal/compiler/implementation/ssa/codegen.dart b/sdk/lib/_internal/compiler/implementation/ssa/codegen.dart
index 7b0b1a1..d0e882f 100644
--- a/sdk/lib/_internal/compiler/implementation/ssa/codegen.dart
+++ b/sdk/lib/_internal/compiler/implementation/ssa/codegen.dart
@@ -1796,7 +1796,8 @@
     List<js.Expression> arguments = visitArguments(node.inputs, start: 0);
     // TODO(floitsch): jsClassReference is an Access. We shouldn't treat it
     // as if it was a string.
-    push(new js.New(new js.VariableUse(jsClassReference), arguments), node);
+    js.Expression constructor = new js.VariableUse(jsClassReference);
+    push(new js.New(constructor, arguments), node);
     registerForeignTypes(node);
     if (node.instantiatedTypes == null) {
       return;
@@ -1820,6 +1821,15 @@
       FunctionConstant function = constant;
       world.registerStaticUse(function.element);
     }
+    if (constant.isType()) {
+      // If the type is a web component, we need to ensure the constructors are
+      // available to 'upgrade' the native object.
+      TypeConstant type = constant;
+      Element element = type.representedType.element;
+      if (element != null && element.isClass()) {
+        backend.registerEscapingConstructorsOfClass(element, world);
+      }
+    }
     push(backend.emitter.constantReference(constant));
   }
 
diff --git a/sdk/lib/_internal/compiler/implementation/ssa/nodes.dart b/sdk/lib/_internal/compiler/implementation/ssa/nodes.dart
index c3dc5a6..af8648d 100644
--- a/sdk/lib/_internal/compiler/implementation/ssa/nodes.dart
+++ b/sdk/lib/_internal/compiler/implementation/ssa/nodes.dart
@@ -1454,6 +1454,9 @@
 }
 
 class HInvokeConstructorBody extends HInvokeStatic {
+  // The 'inputs' are
+  //     [receiver, arg1, ..., argN] or
+  //     [interceptor, receiver, arg1, ... argN].
   HInvokeConstructorBody(element, inputs)
       : super(element, inputs, HType.UNKNOWN);
 
diff --git a/sdk/lib/_internal/lib/interceptors.dart b/sdk/lib/_internal/lib/interceptors.dart
index 9dff6fb..78b591d 100644
--- a/sdk/lib/_internal/lib/interceptors.dart
+++ b/sdk/lib/_internal/lib/interceptors.dart
@@ -151,9 +151,10 @@
 
 
 /**
- * Data structure used to map a [Type] to the [Interceptor] for that type.  It
- * is JavaScript array of 2N entries of adjacent slots containing a [Type]
- * followed by an [Interceptor] class for the type.
+ * Data structure used to map a [Type] to the [Interceptor] and constructors for
+ * that type.  It is JavaScript array of 3N entries of adjacent slots containing
+ * a [Type], followed by an [Interceptor] class for the type, followed by a
+ * JavaScript object map for the constructors.
  *
  * The value of this variable is set by the compiler and contains only types
  * that are user extensions of native classes where the type occurs as a
@@ -162,18 +163,40 @@
 // TODO(sra): Mark this as initialized to a constant with unknown value.
 var mapTypeToInterceptor;
 
-findInterceptorConstructorForType(Type type) {
+int findIndexForWebComponentType(Type type) {
   JS_EFFECT((_){ mapTypeToInterceptor = _; });
   if (mapTypeToInterceptor == null) return null;
   List map = JS('JSFixedArray', '#', mapTypeToInterceptor);
-  for (int i = 0; i + 1 < map.length; i += 2) {
+  for (int i = 0; i + 1 < map.length; i += 3) {
     if (type == map[i]) {
-      return map[i + 1];
+      return i;
     }
   }
   return null;
 }
 
+findInterceptorConstructorForType(Type type) {
+  var index = findIndexForWebComponentType(type);
+  if (index == null) return null;
+  List map = JS('JSFixedArray', '#', mapTypeToInterceptor);
+  return mapTypeToInterceptor[index + 1];
+}
+
+/**
+ * Returns a JavaScript function that runs the constructor on its argument, or
+ * `null` if there is no such constructor.
+ *
+ * The returned function takes one argument, the web component object.
+ */
+findConstructorForWebComponentType(Type type, String name) {
+  var index = findIndexForWebComponentType(type);
+  if (index == null) return null;
+  List map = JS('JSFixedArray', '#', mapTypeToInterceptor);
+  var constructorMap = mapTypeToInterceptor[index + 2];
+  var constructorFn = JS('', '#[#]', constructorMap, name);
+  return constructorFn;
+}
+
 findInterceptorForType(Type type) {
   var constructor = findInterceptorConstructorForType(type);
   if (constructor == null) return null;
diff --git a/tests/compiler/dart2js_native/oddly_named_fields_test.dart b/tests/compiler/dart2js_native/oddly_named_fields_test.dart
index 6ade208..b2dae29 100644
--- a/tests/compiler/dart2js_native/oddly_named_fields_test.dart
+++ b/tests/compiler/dart2js_native/oddly_named_fields_test.dart
@@ -1369,7 +1369,7 @@
   if (object.yieldValue) throw 'incorrect value in "yieldValue"';
 }
 
-makeNativeClassWithOddNames() native;
+NativeClassWithOddNames makeNativeClassWithOddNames() native;
 
 setup() native """
 function NativeClassWithOddNames() {}
@@ -1378,7 +1378,7 @@
 
 main() {
   setup();
-  var object = new NativeClassWithOddNames();
+  var object = makeNativeClassWithOddNames();
   object.testMyFields();
   testObjectStronglyTyped(object);
   testObjectWeaklyTyped([object]);
diff --git a/tests/compiler/dart2js_native/subclassing_constructor_1_test.dart b/tests/compiler/dart2js_native/subclassing_constructor_1_test.dart
new file mode 100644
index 0000000..333a9f2
--- /dev/null
+++ b/tests/compiler/dart2js_native/subclassing_constructor_1_test.dart
@@ -0,0 +1,176 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import "package:expect/expect.dart";
+import 'dart:_foreign_helper' show JS;
+import 'dart:_js_helper' show Creates, setNativeSubclassDispatchRecord;
+import 'dart:_interceptors' show
+    findInterceptorForType, findConstructorForWebComponentType;
+
+// Test that subclasses of native classes can be initialized by calling the
+// 'upgrade' constructor.
+
+var trace = [];
+
+var log;
+
+class A native "A" {
+  final a1 = log(101);  // Only initialized IF named constructor called.
+  final a2;             // Initialized by native constructor.
+  final a3;             // Initialized only by A.two.
+  var a4 = log(104);
+
+  A.one();
+
+  A.two() : a3 = log(103) {
+    log('body(A.two)');
+    log(a4 += increment);
+  }
+
+  A.three(x, this.a4) {
+    log('body(A.three)');
+    log(a4 = '($a4, $x)');
+  }
+
+  get increment => 10;
+}
+
+class B extends A {
+  final b1;
+  final b2 = log(202);
+  var b3;
+
+  B.one() : super.one();
+
+  B.two() : b1 = log(201), super.two(), b3 = log(203) {
+    log('body(B.two)');
+  }
+
+  B.three([x]) : super.three(205, x);
+
+  get increment => 20;
+}
+
+
+makeB() native;
+
+@Creates('=Object')
+getBPrototype() native;
+
+void setup() native r"""
+function B() { this.a2 = 102; }
+
+makeB = function(){return new B;};
+
+getBPrototype = function(){return B.prototype;};
+""";
+
+
+test_one() {
+  trace = [];
+  var constructor = findConstructorForWebComponentType(B, 'one');
+  Expect.isNotNull(constructor);
+  Expect.isNull(findConstructorForWebComponentType(B, 'Missing'));
+
+  var b = makeB();
+  Expect.isTrue(b is B);
+  // Call constructor to initialize native object.
+  var b2 = JS('', '#(#)', constructor, b);
+  Expect.identical(b, b2);
+  Expect.isTrue(b is B);
+
+  Expect.equals(101, b.a1);
+  Expect.equals(102, b.a2);
+  Expect.equals(null, b.a3);
+  Expect.equals(104, b.a4);
+  Expect.equals(null, b.b1);
+  Expect.equals(202, b.b2);
+  Expect.equals(null, b.b3);
+
+  Expect.equals('[202, 101, 104]', '$trace');
+}
+
+test_two() {
+  trace = [];
+  var constructor = findConstructorForWebComponentType(B, 'two');
+  Expect.isNotNull(constructor);
+
+  var b = makeB();
+  Expect.isTrue(b is B);
+  // Call constructor to initialize native object.
+  JS('', '#(#)', constructor, b);
+  Expect.isTrue(b is B);
+
+  Expect.equals(101, b.a1);
+  Expect.equals(102, b.a2);
+  Expect.equals(103, b.a3);
+  Expect.equals(124, b.a4);
+  Expect.equals(201, b.b1);
+  Expect.equals(202, b.b2);
+  Expect.equals(203, b.b3);
+
+  Expect.equals(
+      '[202, 201, 101, 104, 103, 203, body(A.two), 124, body(B.two)]',
+      '$trace');
+}
+
+test_three() {
+  trace = [];
+  var constructor = findConstructorForWebComponentType(B, 'three');
+  Expect.isNotNull(constructor);
+
+  var b = makeB();
+  Expect.isTrue(b is B);
+  // Call constructor to initialize native object.
+  //
+  // Since the constructor takes some optional arguments that are not passed, it
+  // is as though the web components runtime explicitly passed `null` for all
+  // parameters.
+  //
+  // TODO(sra): The constructor returned by findConstructorForWebComponentType
+  // should be a function that fills in the default values.
+  JS('', '#(#)', constructor, b);
+  Expect.isTrue(b is B);
+
+  Expect.equals(101, b.a1);
+  Expect.equals(102, b.a2);
+  Expect.equals(null, b.a3);
+  Expect.equals('(null, 205)', b.a4);
+  Expect.equals(null, b.b1);
+  Expect.equals(202, b.b2);
+  Expect.equals(null, b.b3);
+  print(trace);
+  Expect.equals('[202, 101, 104, body(A.three), (null, 205)]', '$trace');
+}
+
+test_new() {
+  trace = [];
+  checkThrows(action, description) {
+    Expect.throws(action, (e) => true, "'$description must fail'");
+  }
+
+  checkThrows(() => new B.one(), 'new B.one()');
+  checkThrows(() => new B.two(), 'new B.two()');
+  checkThrows(() => new B.three(), 'new B.three()');
+  checkThrows(() => new B.three(1), 'new B.three(1)');
+  checkThrows(() => new B.three([]), 'new B.three([])');
+}
+
+var inscrutable;
+
+main() {
+  setup();
+  inscrutable = (x) => x;
+  log = (message) {
+    trace.add('$message');
+    return message;
+  };
+
+  setNativeSubclassDispatchRecord(getBPrototype(), findInterceptorForType(B));
+
+  test_one();
+  test_two();
+  test_three();
+  test_new();
+}