Simplistic JsBackend
diff --git a/lib/src/codegen/backend.dart b/lib/src/codegen/backend.dart
new file mode 100644
index 0000000..ae421a1
--- /dev/null
+++ b/lib/src/codegen/backend.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2015, 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.
+
+library dev_compiler.src.codegen.backend;
+
+// TODO(jmesserly): import from its own package
+import '../js/dart_nodes.dart';
+import '../js/js_ast.dart' as JS;
+import 'js_names.dart' as JS;
+import 'js_metalet.dart' as JS;
+
+abstract class JsBackend extends DartVisitor<JS.Node> {}
diff --git a/lib/src/codegen/default_backend.dart b/lib/src/codegen/default_backend.dart
new file mode 100644
index 0000000..13db110
--- /dev/null
+++ b/lib/src/codegen/default_backend.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2015, 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.
+
+library dev_compiler.src.codegen.default_backend;
+
+// TODO(jmesserly): import from its own package
+import '../js/dart_nodes.dart';
+import '../js/js_ast.dart' as JS;
+import '../js/js_ast.dart' show js;
+
+import 'backend.dart';
+import 'js_names.dart' as JS;
+import 'js_metalet.dart' as JS;
+import 'package:analyzer/src/generated/element.dart' as analyzer;
+
+const DPUT = 'dput';
+const DLOAD = 'dload';
+const DINDEX = 'dindex';
+const DSETINDEX = 'dsetindex';
+const DCALL = 'dcall';
+const DSEND = 'dsend';
+
+class DefaultJsBackend extends JsBackend {
+  Function _statement;
+
+  DefaultJsBackend(JS.Statement this._statement(List<JS.Statement> statements));
+
+  @override
+  JS.Expression visitDartMethodCall(DartMethodCall node) {
+    var target = node.target;
+    var memberName = node.memberName;
+    var arguments = node.arguments;
+
+    switch (node.callType) {
+      case DartMethodCallType.dsend:
+        return js.call('dart.$DSEND(#, #, #)',
+            [target, memberName, arguments]);
+
+      case DartMethodCallType.dcall:
+        if (target == null) {
+          return js.call('dart.$DCALL(#, #)', [memberName, arguments]);
+        } else {
+          return js.call('dart.$DCALL(#.#, #)',
+                [target, memberName, arguments]);
+        }
+
+      case DartMethodCallType.staticDispatch:
+        return js.call('dart.#(#, #)',
+            [memberName, target, arguments]);
+
+      case DartMethodCallType.directDispatch:
+        if (target == null) {
+          return js.call('#(#)', [memberName, arguments]);
+        } else {
+          return js.call('#.#(#)', [target, memberName, arguments]);
+        }
+
+      default:
+        throw new StateError('Invalid call type: ${node.callType}');
+    }
+  }
+
+  @override
+  JS.Node visitDartCallableDeclaration(DartCallableDeclaration node) {
+    JS.Method method;
+    var element = node.element;
+    // if (element is analyzer.MethodElement) {
+    //
+    // } else
+    if (element is analyzer.PropertyAccessorElement) {
+      method = new JS.Method(node.name, node.body,
+          isGetter: element.isGetter,
+          isSetter: element.isSetter,
+          isStatic: element.isStatic);
+    } else if (element is analyzer.ConstructorElement) {
+      method = new JS.Method(node.name, node.body, isStatic: element.isFactory);
+    } else if (element is analyzer.ClassElement) {
+      method = new JS.Method(node.name, node.body);
+    } else {
+      // TODO('visitDartCallableDeclaration: ${element.runtimeType}');
+      method = new JS.Method(node.name, node.body,
+          isStatic: (element?.isStatic ?? false));
+    }
+    return //annotate(
+          method;//..sourceInformation = node;
+          //node.element);
+  }
+
+  @override
+  JS.Node visitDartClassDeclaration(DartClassDeclaration node) {
+    var classElem = node.element;
+    var name = classElem.name;
+    var body = <JS.Statement>[];
+
+    var cls = new JS.ClassExpression(
+        new JS.Identifier(classElem.type.name),
+        node.parentRef,
+        node.members.map(visit).toList());
+
+    if (node.extensionNames.isNotEmpty) {
+      body.add(js.statement('dart.defineExtensionNames(#)',
+          [new JS.ArrayInitializer(node.extensionNames, multiline: true)]));
+    }
+
+    body.add(new JS.ClassDeclaration(cls));
+
+    // TODO(jmesserly): we should really just extend native Array.
+    if (node.jsPeerName != null && classElem.typeParameters.isNotEmpty) {
+      body.add(js.statement('dart.setBaseClass(#, dart.global.#);',
+          [classElem.name, node.jsPeerName]));
+    }
+
+    // Deferred Superclass
+    if (node.deferredParentRef != null) {
+      body.add(js.statement('#.prototype.__proto__ = #.prototype;',
+          [name, node.deferredParentRef]));
+    }
+
+    // Interfaces
+    if (node.implementedRefs.isNotEmpty) {
+      body.add(js.statement('#[dart.implements] = () => #;', [
+        name,
+        new JS.ArrayInitializer(node.implementedRefs)
+      ]));
+    }
+
+    // Named constructors
+    for (var ctorName in node.constructorNames) {
+      body.add(js.statement('dart.defineNamedConstructor(#, #);',
+          [name, ctorName]));
+    }
+
+    // Instance fields, if they override getter/setter pairs
+    for (var overrideField in node.overrideFields) {
+      return js.statement('dart.virtualField(#, #)', [cls.name, overrideField]);
+    }
+
+    // Emit the signature on the class recording the runtime type information
+
+    if (node.signature != null) {
+      var classExpr = new JS.Identifier(name);
+      body.add(js.statement('dart.setSignature(#, #);', [classExpr, node.signature]));
+    }
+
+    // If a concrete class implements one of our extensions, we might need to
+    // add forwarders.
+    if (node.extensionNames.isNotEmpty) {
+      body.add(js.statement('dart.defineExtensionMembers(#, #);', [
+        name,
+        new JS.ArrayInitializer(node.extensionNames,
+            multiline: node.extensionNames.length > 4)
+      ]));
+    }
+
+    // TODO(vsm): Make this optional per #268.
+    if (node.metadataExpressions.isNotEmpty) {
+      body.add(js.statement('#[dart.metadata] = () => #;', [
+        name,
+        new JS.ArrayInitializer(node.metadataExpressions)
+      ]));
+    }
+
+    return _statement(body);
+  }
+
+  @override
+  JS.Node visitDartDeclaration(DartDeclaration node) =>
+      TODO('visitDartDeclaration');
+
+  @override
+  JS.Node visitDartLibrary(DartLibrary node) =>
+      TODO('visitDartLibrary');
+
+  @override
+  JS.Node visitDartLibraryPart(DartLibraryPart node) =>
+      TODO('visitDartLibraryPart');
+
+  @override
+  JS.Node visitDartTypedef(DartTypedef node) =>
+      TODO('visitDartTypedef');
+
+  @override
+  JS.Node visitOpaqueDartDeclaration(OpaqueDartDeclaration node) =>
+      node.statement;
+
+  TODO(String s) => throw new StateError('TODO: $s');
+}
diff --git a/lib/src/codegen/js_codegen.dart b/lib/src/codegen/js_codegen.dart
index 693afed..23d59fc 100644
--- a/lib/src/codegen/js_codegen.dart
+++ b/lib/src/codegen/js_codegen.dart
@@ -30,7 +30,9 @@
 import '../options.dart' show CodegenOptions;
 import '../utils.dart';
 
+import 'backend.dart';
 import 'code_generator.dart';
+import 'default_backend.dart';
 import 'js_field_storage.dart';
 import 'js_interop.dart';
 import 'js_names.dart' as JS;
@@ -39,6 +41,8 @@
 import 'js_names.dart';
 import 'js_printer.dart' show writeJsLibrary;
 import 'side_effect_analysis.dart';
+import 'package:dev_compiler/src/js/dart_nodes.dart';
+import 'package:dev_compiler/src/js/js_types.dart';
 
 // Various dynamic helpers we call.
 // If renaming these, make sure to check other places like the
@@ -95,6 +99,7 @@
   final _namedArgTemp = new JS.TemporaryId('opts');
 
   final TypeProvider _types;
+  final JsBackend _backend;
 
   ConstFieldVisitor _constField;
 
@@ -109,7 +114,7 @@
   bool _isDartRuntime;
 
   JSCodegenVisitor(AbstractCompiler compiler, this.rules, this.currentLibrary,
-      this._extensionTypes, this._fieldsNeedingStorage)
+      this._extensionTypes, this._fieldsNeedingStorage, this._backend)
       : compiler = compiler,
         options = compiler.options.codegenOptions,
         _types = compiler.context.typeProvider {
@@ -124,6 +129,19 @@
 
   TypeProvider get types => _types;
 
+  JS.Node _emitDart(DartNode node) {
+    return _backend.visit(node);
+    // Convert things we can convert immediately, and register the rest for
+    // future conversion.
+    // if (node is DartMethodCall) {
+    //   return _backend.visit(node);
+    // } else {
+    //   var placeholder = new JS.PlaceholderExpression(node);
+    //   _placeholders.add(placeholder);
+    //   return placeholder;
+    // }
+  }
+
   JS.Program emitLibrary(LibraryUnit library) {
     // Modify the AST to make coercions explicit.
     new CoercionReifier(library, rules).reify();
@@ -445,9 +463,6 @@
       }
     }
 
-    var classExpr = new JS.ClassExpression(new JS.Identifier(type.name),
-        _classHeritage(classElem), _emitClassMethods(node, ctors, fields));
-
     String jsPeerName;
     var jsPeer = findAnnotation(classElem, isJsPeerInterface);
     if (jsPeer != null) {
@@ -455,7 +470,8 @@
           getConstantField(jsPeer, 'name', types.stringType)?.toStringValue();
     }
 
-    var body = _finishClassMembers(classElem, classExpr, ctors, fields, methods,
+    var members = _emitClassMethods(node, ctors, fields);
+    var body = _finishClassMembers(node, classElem, members, ctors, fields, methods,
         node.metadata, jsPeerName);
 
     var result = _finishClassDef(type, body);
@@ -588,7 +604,7 @@
     return false;
   }
 
-  JS.Expression _classHeritage(ClassElement element) {
+  TypeRef _classHeritage(ClassElement element) {
     var type = element.type;
     if (type.isObject) return null;
 
@@ -612,10 +628,11 @@
     }
 
     _loader.finishTopLevel(element);
-    return heritage;
+    // TODO(ochafik): Refine this.
+    return new OpaqueTypeRef(heritage);
   }
 
-  List<JS.Method> _emitClassMethods(ClassDeclaration node,
+  List<DartCallableDeclaration> _emitClassMethods(ClassDeclaration node,
       List<ConstructorDeclaration> ctors, List<FieldDeclaration> fields) {
     var element = node.element;
     var type = element.type;
@@ -623,7 +640,7 @@
 
     // Iff no constructor is specified for a class C, it implicitly has a
     // default constructor `C() : super() {}`, unless C is class Object.
-    var jsMethods = <JS.Method>[];
+    var jsMethods = <DartCallableDeclaration>[];
     if (ctors.isEmpty && !isObject) {
       jsMethods.add(_emitImplicitConstructor(node, fields));
     }
@@ -670,7 +687,7 @@
   /// This will return `null` if the adapter was already added on a super type,
   /// otherwise it returns the adapter code.
   // TODO(jmesserly): should we adapt `Iterator` too?
-  JS.Method _emitIterable(InterfaceType t) {
+  DartCallableDeclaration _emitIterable(InterfaceType t) {
     // If a parent had an `iterator` (concrete or abstract) or implements
     // Iterable, we know the adapter is already there, so we can skip it as a
     // simple code size optimization.
@@ -681,7 +698,7 @@
 
     // Otherwise, emit the adapter method, which wraps the Dart iterator in
     // an ES6 iterator.
-    return new JS.Method(
+    return newDartMethod(null, //new JS.Method(
         js.call('$_SYMBOL.iterator'),
         js.call('function() { return new dart.JsIterator(this.#); }',
             [_emitMemberName('iterator', type: t)]) as JS.Fun);
@@ -700,58 +717,46 @@
   /// Emit class members that need to come after the class declaration, such
   /// as static fields. See [_emitClassMethods] for things that are emitted
   /// inside the ES6 `class { ... }` node.
-  JS.Statement _finishClassMembers(
+  JS.Node _finishClassMembers(
+      ClassDeclaration node,
       ClassElement classElem,
-      JS.ClassExpression cls,
+      // JS.ClassExpression cls,
+      List<DartCallableDeclaration> members,
       List<ConstructorDeclaration> ctors,
       List<FieldDeclaration> fields,
       List<MethodDeclaration> methods,
       List<Annotation> metadata,
       String jsPeerName) {
-    var name = classElem.name;
-    var body = <JS.Statement>[];
+
+    // var members = _emitClassMethods(node, ctors, fields);
+
+    TypeRef parentRef = _classHeritage(classElem);
+    final mixinRefs = <TypeRef>[];
+    final genericTypeNames = <String>[];
+    final extensionNames = <JS.Expression>[];
+    final overrideFields = <JS.Expression>[];
+    final constructorNames = <JS.Expression>[];
 
     if (_extensionTypes.contains(classElem)) {
-      var dartxNames = <JS.Expression>[];
       for (var m in methods) {
         if (!m.isAbstract && !m.isStatic && m.element.isPublic) {
-          dartxNames.add(_elementMemberName(m.element, allowExtensions: false));
+          extensionNames.add(_elementMemberName(m.element, allowExtensions: false));
         }
       }
-      if (dartxNames.isNotEmpty) {
-        body.add(js.statement('dart.defineExtensionNames(#)',
-            [new JS.ArrayInitializer(dartxNames, multiline: true)]));
-      }
-    }
-
-    body.add(new JS.ClassDeclaration(cls));
-
-    // TODO(jmesserly): we should really just extend native Array.
-    if (jsPeerName != null && classElem.typeParameters.isNotEmpty) {
-      body.add(js.statement('dart.setBaseClass(#, dart.global.#);',
-          [classElem.name, _propertyName(jsPeerName)]));
     }
 
     // Deferred Superclass
-    if (_hasDeferredSupertype.contains(classElem)) {
-      body.add(js.statement('#.prototype.__proto__ = #.prototype;',
-          [name, _emitTypeName(classElem.type.superclass)]));
-    }
+    JS.Expression deferredSuperType = _hasDeferredSupertype.contains(classElem)
+       ? _emitTypeName(classElem.type.superclass) : null;
 
     // Interfaces
-    if (classElem.interfaces.isNotEmpty) {
-      body.add(js.statement('#[dart.implements] = () => #;', [
-        name,
-        new JS.ArrayInitializer(new List<JS.Expression>.from(
-            classElem.interfaces.map(_emitTypeName)))
-      ]));
-    }
+    final implementedRefs = classElem.interfaces
+        .map((i) => new OpaqueTypeRef(_emitTypeName(i))).toList();
 
     // Named constructors
     for (ConstructorDeclaration member in ctors) {
       if (member.name != null && member.factoryKeyword == null) {
-        body.add(js.statement('dart.defineNamedConstructor(#, #);',
-            [name, _emitMemberName(member.name.name, isStatic: true)]));
+        constructorNames.add(_emitMemberName(member.name.name, isStatic: true));
       }
     }
 
@@ -760,97 +765,97 @@
       for (VariableDeclaration fieldDecl in member.fields.variables) {
         var field = fieldDecl.element as FieldElement;
         if (_fieldsNeedingStorage.contains(field)) {
-          body.add(_overrideField(field));
+          overrideFields.add(_emitMemberName(field.name, type: classElem.type));
         }
       }
     }
 
     // Emit the signature on the class recording the runtime type information
     var extensions = _extensionsToImplement(classElem);
-    {
-      var tStatics = <JS.Property>[];
-      var tMethods = <JS.Property>[];
-      var sNames = <JS.Expression>[];
-      for (MethodDeclaration node in methods) {
-        if (!(node.isSetter || node.isGetter || node.isAbstract)) {
-          var name = node.name.name;
-          var element = node.element;
-          var inheritedElement =
-              classElem.lookUpInheritedConcreteMethod(name, currentLibrary);
-          if (inheritedElement != null &&
-              inheritedElement.type == element.type) {
-            continue;
-          }
-          var memberName = _elementMemberName(element);
-          var parts = _emitFunctionTypeParts(element.type);
-          var property =
-              new JS.Property(memberName, new JS.ArrayInitializer(parts));
-          if (node.isStatic) {
-            tStatics.add(property);
-            sNames.add(memberName);
-          } else {
-            tMethods.add(property);
-          }
-        }
-      }
-
-      var tCtors = <JS.Property>[];
-      for (ConstructorDeclaration node in ctors) {
-        var memberName = _constructorName(node.element);
-        var element = node.element;
-        var parts = _emitFunctionTypeParts(element.type, node.parameters);
-        var property =
-            new JS.Property(memberName, new JS.ArrayInitializer(parts));
-        tCtors.add(property);
-      }
-
-      JS.Property build(String name, List<JS.Property> elements) {
-        var o =
-            new JS.ObjectInitializer(elements, multiline: elements.length > 1);
-        var e = js.call('() => #', o);
-        return new JS.Property(_propertyName(name), e);
-      }
-      var sigFields = <JS.Property>[];
-      if (!tCtors.isEmpty) sigFields.add(build('constructors', tCtors));
-      if (!tMethods.isEmpty) sigFields.add(build('methods', tMethods));
-      if (!tStatics.isEmpty) {
-        assert(!sNames.isEmpty);
-        var aNames = new JS.Property(
-            _propertyName('names'), new JS.ArrayInitializer(sNames));
-        sigFields.add(build('statics', tStatics));
-        sigFields.add(aNames);
-      }
-      if (!sigFields.isEmpty || extensions.isNotEmpty) {
-        var sig = new JS.ObjectInitializer(sigFields);
-        var classExpr = new JS.Identifier(name);
-        body.add(js.statement('dart.setSignature(#, #);', [classExpr, sig]));
-      }
-    }
 
     // If a concrete class implements one of our extensions, we might need to
     // add forwarders.
-    if (extensions.isNotEmpty) {
-      var methodNames = <JS.Expression>[];
-      for (var e in extensions) {
-        methodNames.add(_elementMemberName(e));
+    extensionNames.addAll(extensions.map(_elementMemberName).toList());
+
+    var decl = new DartClassDeclaration(
+      element: classElem,
+      jsPeerName: _propertyName(jsPeerName),
+      parentRef: parentRef,
+      mixinRefs: mixinRefs,
+      implementedRefs: implementedRefs,
+      genericTypeNames: genericTypeNames,
+      members: members,
+      extensionNames: extensionNames,
+      // TODO(vsm): Make this optional per #268.
+      metadataExpressions: metadata.map(_instantiateAnnotation).toList(),
+      constructorNames: constructorNames,
+      overrideFields: overrideFields,
+      // TODO(ochafik): Move to backend
+      signature: makeSignature(classElem, methods, ctors, extensions),
+      deferredSuperType: deferredSuperType);
+
+    return _emitDart(decl);
+  }
+
+  JS.Expression makeSignature(
+      ClassElement classElem,
+      List<MethodDeclaration> methods,
+      List<ConstructorDeclaration> ctors,
+      List<ExecutableElement> extensions) {
+    var tStatics = <JS.Property>[];
+    var tMethods = <JS.Property>[];
+    var sNames = <JS.Expression>[];
+    for (MethodDeclaration node in methods) {
+      if (!(node.isSetter || node.isGetter || node.isAbstract)) {
+        var name = node.name.name;
+        var element = node.element;
+        var inheritedElement =
+            classElem.lookUpInheritedConcreteMethod(name, currentLibrary);
+        if (inheritedElement != null &&
+            inheritedElement.type == element.type) continue;
+        var memberName = _elementMemberName(element);
+        var parts = _emitFunctionTypeParts(element.type);
+        var property =
+            new JS.Property(memberName, new JS.ArrayInitializer(parts));
+        if (node.isStatic) {
+          tStatics.add(property);
+          sNames.add(memberName);
+        } else {
+          tMethods.add(property);
+        }
       }
-      body.add(js.statement('dart.defineExtensionMembers(#, #);', [
-        name,
-        new JS.ArrayInitializer(methodNames, multiline: methodNames.length > 4)
-      ]));
     }
 
-    // TODO(vsm): Make this optional per #268.
-    // Metadata
-    if (metadata.isNotEmpty) {
-      body.add(js.statement('#[dart.metadata] = () => #;', [
-        name,
-        new JS.ArrayInitializer(
-            new List<JS.Expression>.from(metadata.map(_instantiateAnnotation)))
-      ]));
+    var tCtors = <JS.Property>[];
+    for (ConstructorDeclaration node in ctors) {
+      var memberName = _constructorName(node.element);
+      var element = node.element;
+      var parts = _emitFunctionTypeParts(element.type, node.parameters);
+      var property =
+          new JS.Property(memberName, new JS.ArrayInitializer(parts));
+      tCtors.add(property);
     }
 
-    return _statement(body);
+    JS.Property build(String name, List<JS.Property> elements) {
+      var o =
+          new JS.ObjectInitializer(elements, multiline: elements.length > 1);
+      var e = js.call('() => #', o);
+      return new JS.Property(_propertyName(name), e);
+    }
+    var sigFields = <JS.Property>[];
+    if (!tCtors.isEmpty) sigFields.add(build('constructors', tCtors));
+    if (!tMethods.isEmpty) sigFields.add(build('methods', tMethods));
+    if (!tStatics.isEmpty) {
+      assert(!sNames.isEmpty);
+      var aNames = new JS.Property(
+          _propertyName('names'), new JS.ArrayInitializer(sNames));
+      sigFields.add(build('statics', tStatics));
+      sigFields.add(aNames);
+    }
+    if (!sigFields.isEmpty || extensions.isNotEmpty) {
+      return new JS.ObjectInitializer(sigFields);
+    }
+    return null;
   }
 
   List<ExecutableElement> _extensionsToImplement(ClassElement element) {
@@ -895,15 +900,10 @@
     _collectExtensions(type.superclass, types);
   }
 
-  JS.Statement _overrideField(FieldElement e) {
-    var cls = e.enclosingElement;
-    return js.statement('dart.virtualField(#, #)',
-        [cls.name, _emitMemberName(e.name, type: cls.type)]);
-  }
 
   /// Generates the implicit default constructor for class C of the form
   /// `C() : super() {}`.
-  JS.Method _emitImplicitConstructor(
+  DartCallableDeclaration _emitImplicitConstructor(
       ClassDeclaration node, List<FieldDeclaration> fields) {
     assert(_hasUnnamedConstructor(node.element) == fields.isNotEmpty);
 
@@ -918,12 +918,14 @@
       ];
     }
     var name = _constructorName(node.element.unnamedConstructor);
-    return annotateDefaultConstructor(
-        new JS.Method(name, js.call('function() { #; }', body) as JS.Fun),
-        node.element);
+    return newDartConstructor(node.element, name,
+        js.call('function() { #; }', body));
+    // return annotateDefaultConstructor(
+    //     new JS.Method(name, js.call('function() { #; }', body) as JS.Fun),
+    //     node.element);
   }
 
-  JS.Method _emitConstructor(ConstructorDeclaration node, InterfaceType type,
+  DartCallableDeclaration _emitConstructor(ConstructorDeclaration node, InterfaceType type,
       List<FieldDeclaration> fields, bool isObject) {
     if (_externalOrNative(node)) return null;
 
@@ -941,9 +943,10 @@
 
       var fun = js.call('function(#) { return $newKeyword #(#); }',
           [params, _visit(redirect), params]) as JS.Fun;
-      return annotate(
-          new JS.Method(name, fun, isStatic: true)..sourceInformation = node,
-          node.element);
+      return newDartConstructor(node.element, name, fun);
+      // return annotate(
+      //     new JS.Method(name, fun, isStatic: true)..sourceInformation = node,
+      //     node.element);
     }
 
     // Factory constructors are essentially static methods.
@@ -954,9 +957,10 @@
       body.add(_visit(node.body));
       var fun = new JS.Fun(
           _visit(node.parameters) as List<JS.Parameter>, new JS.Block(body));
-      return annotate(
-          new JS.Method(name, fun, isStatic: true)..sourceInformation = node,
-          node.element);
+      return newDartConstructor(node.element, name, fun);
+      // return annotate(
+      //     new JS.Method(name, fun, isStatic: true)..sourceInformation = node,
+      //     node.element);
     }
 
     // Code generation for Object's constructor.
@@ -991,11 +995,13 @@
     // We generate constructors as initializer methods in the class;
     // this allows use of `super` for instance methods/properties.
     // It also avoids V8 restrictions on `super` in default constructors.
-    return annotate(
-        new JS.Method(name,
-            new JS.Fun(_visit(node.parameters) as List<JS.Parameter>, body))
-          ..sourceInformation = node,
-        node.element);
+    return newDartConstructor(node.element, name,
+        new JS.Fun(_visit(node.parameters) as List<JS.Parameter>, body));
+    // return annotate(
+    //     new JS.Method(name,
+    //         new JS.Fun(_visit(node.parameters) as List<JS.Parameter>, body))
+    //       ..sourceInformation = node,
+    //     node.element);
   }
 
   JS.Expression _constructorName(ConstructorElement ctor) {
@@ -1251,7 +1257,7 @@
     }
   }
 
-  JS.Method _emitMethodDeclaration(DartType type, MethodDeclaration node) {
+  DartCallableDeclaration _emitMethodDeclaration(DartType type, MethodDeclaration node) {
     if (node.isAbstract || _externalOrNative(node)) {
       return null;
     }
@@ -1279,12 +1285,13 @@
         ..sourceInformation = fn.sourceInformation;
     }
 
-    return annotate(
-        new JS.Method(_elementMemberName(node.element), fn,
-            isGetter: node.isGetter,
-            isSetter: node.isSetter,
-            isStatic: node.isStatic),
-        node.element);
+    return newDartMethod(node.element, _elementMemberName(node.element), fn);
+    // return annotate(
+    //     new JS.Method(_elementMemberName(node.element), fn,
+    //         isGetter: node.isGetter,
+    //         isSetter: node.isSetter,
+    //         isStatic: node.isStatic),
+    //     node.element);
   }
 
   /// Returns the name value of the `JSExportName` annotation (when compiling
@@ -1370,12 +1377,13 @@
     return fn;
   }
 
-  JS.Method _emitTopLevelProperty(FunctionDeclaration node) {
+  DartCallableDeclaration _emitTopLevelProperty(FunctionDeclaration node) {
     var name = node.name.name;
-    return annotate(
-        new JS.Method(_propertyName(name), _visit(node.functionExpression),
-            isGetter: node.isGetter, isSetter: node.isSetter),
-        node.element);
+    return newDartMethod(node.element, _propertyName(name), _visit(node.functionExpression));
+    // return annotate(
+    //     new JS.Method(_propertyName(name), _visit(node.functionExpression),
+    //         isGetter: node.isGetter, isSetter: node.isSetter),
+    //     node.element);
   }
 
   bool _executesAtTopLevel(AstNode node) {
@@ -1906,40 +1914,44 @@
     var result = _emitForeignJS(node);
     if (result != null) return result;
 
-    String code;
+    JS.Expression callTarget;
+    JS.Expression memberName;
+    DartMethodCallType callType;
+
     if (target == null || isLibraryPrefix(target)) {
+      callTarget = null;
+      memberName = _visit(node.methodName);
       if (DynamicInvoke.get(node.methodName)) {
-        code = 'dart.$DCALL(#, #)';
+        callType = DartMethodCallType.dcall;
       } else {
-        code = '#(#)';
+        callType = DartMethodCallType.directDispatch;
       }
-      return js
-          .call(code, [_visit(node.methodName), _visit(node.argumentList)]);
-    }
-
-    var type = getStaticType(target);
-    var name = node.methodName.name;
-    var element = node.methodName.staticElement;
-    bool isStatic = element is ExecutableElement && element.isStatic;
-    var memberName = _emitMemberName(name, type: type, isStatic: isStatic);
-
-    if (DynamicInvoke.get(target)) {
-      code = 'dart.$DSEND(#, #, #)';
-    } else if (DynamicInvoke.get(node.methodName)) {
-      // This is a dynamic call to a statically known target. For example:
-      //     class Foo { Function bar; }
-      //     new Foo().bar(); // dynamic call
-      code = 'dart.$DCALL(#.#, #)';
-    } else if (_requiresStaticDispatch(target, name)) {
-      // Object methods require a helper for null checks.
-      return js.call('dart.#(#, #)',
-          [memberName, _visit(target), _visit(node.argumentList)]);
     } else {
-      code = '#.#(#)';
+      callTarget = _visit(target);
+      var type = getStaticType(target);
+      var name = node.methodName.name;
+      var element = node.methodName.staticElement;
+      bool isStatic = element is ExecutableElement && element.isStatic;
+      memberName = _emitMemberName(name, type: type, isStatic: isStatic);
+
+      if (DynamicInvoke.get(target)) {
+        callType = DartMethodCallType.dsend;
+      } else if (DynamicInvoke.get(node.methodName)) {
+        // This is a dynamic call to a statically known target. For example:
+        //     class Foo { Function bar; }
+        //     new Foo().bar(); // dynamic call
+        callType = DartMethodCallType.dcall;
+      } else if (_requiresStaticDispatch(target, name)) {
+        assert(rules.objectMembers[name] is FunctionType);
+        // Object methods require a helper for null checks.
+        callType = DartMethodCallType.staticDispatch;
+      } else {
+        callType = DartMethodCallType.directDispatch;
+      }
     }
 
-    return js
-        .call(code, [_visit(target), memberName, _visit(node.argumentList)]);
+    return _emitDart(new DartMethodCall(
+        callType, callTarget, memberName, _visit(node.argumentList)));
   }
 
   /// Emits code for the `JS(...)` builtin.
@@ -2322,7 +2334,7 @@
   void _flushLibraryProperties(List<JS.Statement> body) {
     if (_properties.isEmpty) return;
     body.add(js.statement('dart.copyProperties(#, { # });',
-        [_exportsVar, _properties.map(_emitTopLevelProperty)]));
+        [_exportsVar, _properties.map(_emitTopLevelProperty).map(_emitDart)]));
     _properties.clear();
   }
 
@@ -3123,13 +3135,6 @@
         otherwise);
   }
 
-  JS.Statement _statement(List<JS.Statement> statements) {
-    // TODO(jmesserly): empty block singleton?
-    if (statements.length == 0) return new JS.Block([]);
-    if (statements.length == 1) return statements[0];
-    return new JS.Block(statements);
-  }
-
   /// Visits the catch clause body. This skips the exception type guard, if any.
   /// That is handled in [_visitCatch].
   @override
@@ -3588,8 +3593,10 @@
     var library = unit.library.element.library;
     var fields = findFieldsNeedingStorage(unit, _extensionTypes);
     var rules = new StrongTypeSystemImpl();
-    var codegen =
-        new JSCodegenVisitor(compiler, rules, library, _extensionTypes, fields);
+    // TODO(ochafik): make this configurable.
+    var backend = new DefaultJsBackend(_statement);
+    var codegen = new JSCodegenVisitor(
+        compiler, rules, library, _extensionTypes, fields, backend);
     var module = codegen.emitLibrary(unit);
     var out = compiler.getOutputPath(library.source.uri);
     var flags = compiler.options;
@@ -3627,3 +3634,10 @@
   int get hashCode => identityHashCode(this);
   bool operator ==(Object other) => identical(this, other);
 }
+
+JS.Statement _statement(List<JS.Statement> statements) {
+  // TODO(jmesserly): empty block singleton?
+  if (statements.length == 0) return new JS.Block([]);
+  if (statements.length == 1) return statements[0];
+  return new JS.Block(statements);
+}
diff --git a/lib/src/codegen/js_codegen_backends.dart b/lib/src/codegen/js_codegen_backends.dart
new file mode 100644
index 0000000..c856118
--- /dev/null
+++ b/lib/src/codegen/js_codegen_backends.dart
@@ -0,0 +1,3467 @@
+// Copyright (c) 2015, 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.
+
+library dev_compiler.src.codegen.js_codegen_backends;
+
+import 'dart:collection' show HashSet, HashMap, SplayTreeSet;
+
+import 'package:analyzer/analyzer.dart' hide ConstantEvaluator;
+import 'package:analyzer/src/generated/ast.dart' hide ConstantEvaluator;
+import 'package:analyzer/src/generated/constant.dart';
+import 'package:analyzer/src/generated/element.dart';
+import 'package:analyzer/src/generated/resolver.dart' show TypeProvider;
+import 'package:analyzer/src/generated/scanner.dart'
+    show StringToken, Token, TokenType;
+import 'package:analyzer/src/task/dart.dart' show PublicNamespaceBuilder;
+import 'package:analyzer/src/task/strong/rules.dart';
+
+import 'ast_builder.dart' show AstBuilder;
+import 'reify_coercions.dart' show CoercionReifier, Tuple2;
+
+// TODO(jmesserly): import from its own package
+import '../js/js_ast.dart' as JS;
+import '../js/js_ast.dart' show js;
+import '../js/dart_nodes.dart';
+
+import '../closure/closure_annotator.dart' show ClosureAnnotator;
+import '../compiler.dart' show AbstractCompiler;
+import '../info.dart';
+import '../options.dart' show CodegenOptions;
+import '../utils.dart';
+
+import 'code_generator.dart';
+import 'js_field_storage.dart';
+import 'js_interop.dart';
+import 'js_names.dart' as JS;
+import 'js_metalet.dart' as JS;
+import 'js_module_item_order.dart';
+import 'js_names.dart';
+import 'js_printer.dart' show writeJsLibrary;
+import 'side_effect_analysis.dart';
+import 'package:collection/equality.dart';
+
+// Various dynamic helpers we call.
+// If renaming these, make sure to check other places like the
+// _runtime.js file and comments.
+// TODO(jmesserly): ideally we'd have a "dynamic call" dart library we can
+// import and generate calls to, rather than dart_runtime.js
+const DPUT = 'dput';
+const DLOAD = 'dload';
+const DINDEX = 'dindex';
+const DSETINDEX = 'dsetindex';
+const DCALL = 'dcall';
+const DSEND = 'dsend';
+
+const ListEquality _listEquality = const ListEquality();
+
+class JSCodegenVisitor extends GeneralizingAstVisitor with ClosureAnnotator {
+  final AbstractCompiler compiler;
+  final CodegenOptions options;
+  final TypeRules rules;
+  final LibraryElement currentLibrary;
+
+  /// The global extension type table.
+  final HashSet<ClassElement> _extensionTypes;
+
+  /// Information that is precomputed for this library, indicates which fields
+  /// need storage slots.
+  final HashSet<FieldElement> _fieldsNeedingStorage;
+
+  /// The variable for the target of the current `..` cascade expression.
+  ///
+  /// Usually a [SimpleIdentifier], but it can also be other expressions
+  /// that are safe to evaluate multiple times, such as `this`.
+  Expression _cascadeTarget;
+
+  /// The variable for the current catch clause
+  SimpleIdentifier _catchParameter;
+
+  /// In an async* function, this represents the stream controller parameter.
+  JS.TemporaryId _asyncStarController;
+
+  /// Imported libraries, and the temporaries used to refer to them.
+  final _imports = new Map<LibraryElement, JS.TemporaryId>();
+  final _exports = new Set<String>();
+  final _lazyFields = <VariableDeclaration>[];
+  final _properties = <FunctionDeclaration>[];
+  final _privateNames = new HashMap<String, JS.TemporaryId>();
+  final _moduleItems = <JS.Statement>[];
+  final _temps = new HashMap<Element, JS.TemporaryId>();
+  final _qualifiedIds = new List<Tuple2<Element, JS.MaybeQualifiedId>>();
+
+  /// The name for the library's exports inside itself.
+  /// `exports` was chosen as the most similar to ES module patterns.
+  final _dartxVar = new JS.Identifier('dartx');
+  final _exportsVar = new JS.TemporaryId('exports');
+  final _runtimeLibVar = new JS.Identifier('dart');
+  final _namedArgTemp = new JS.TemporaryId('opts');
+
+  final TypeProvider _types;
+
+  ConstFieldVisitor _constField;
+
+  ModuleItemLoadOrder _loader;
+
+  /// _interceptors.JSArray<E>, used for List literals.
+  ClassElement _jsArray;
+
+  /// The default value of the module object. See [visitLibraryDirective].
+  String _jsModuleValue;
+
+  bool _isDartUtils;
+
+  Map<String, DartType> _objectMembers;
+
+  JSCodegenVisitor(AbstractCompiler compiler, this.rules, this.currentLibrary,
+      this._extensionTypes, this._fieldsNeedingStorage)
+      : compiler = compiler,
+        options = compiler.options.codegenOptions,
+        _types = compiler.context.typeProvider {
+    _loader = new ModuleItemLoadOrder(_emitModuleItem);
+
+    var context = compiler.context;
+    var src = context.sourceFactory.forUri('dart:_interceptors');
+    var interceptors = context.computeLibraryElement(src);
+    _jsArray = interceptors.getType('JSArray');
+    _isDartUtils = currentLibrary.source.uri.toString() == 'dart:_utils';
+
+    _objectMembers = getObjectMemberMap(types);
+  }
+
+  TypeProvider get types => rules.provider;
+
+  JS.Visitable emitLibrary(LibraryUnit library) {
+    // Modify the AST to make coercions explicit.
+    new CoercionReifier(library, rules).reify();
+
+    // Build the public namespace for this library. This allows us to do
+    // constant time lookups (contrast with `Element.getChild(name)`).
+    if (currentLibrary.publicNamespace == null) {
+      (currentLibrary as LibraryElementImpl).publicNamespace =
+          new PublicNamespaceBuilder().build(currentLibrary);
+    }
+
+    library.library.directives.forEach(_visit);
+
+    // Rather than directly visit declarations, we instead use [_loader] to
+    // visit them. It has the ability to sort elements on demand, so
+    // dependencies between top level items are handled with a minimal
+    // reordering of the user's input code. The loader will call back into
+    // this visitor via [_emitModuleItem] when it's ready to visit the item
+    // for real.
+    _loader.collectElements(currentLibrary, library.partsThenLibrary);
+
+    var parts = <DartLibraryPart>[];
+
+    for (var unit in library.partsThenLibrary) {
+      var declarations = <DartDeclaration>[];
+
+      _constField = new ConstFieldVisitor(types, unit);
+
+      for (var decl in unit.declarations) {
+        if (decl is TopLevelVariableDeclaration) {
+          // Split variables.
+          for (VariableDeclaration d in decl.variables.variables) {
+            declarations.add(newDartTopLevelValue(d.element, visitVariableDeclaration(d)));
+          }
+        } else if (decl is ClassDeclaration) {
+          declarations.add(visitClassDeclaration(decl));
+        } else if (decl is FunctionTypeAlias) {
+          declarations.add(visitFunctionTypeAlias(decl));
+        } else if (decl is FunctionDeclaration) {
+          declarations.add(visitFunctionDeclaration(decl));
+        }
+      }
+      parts.add(new DartLibraryPart(unit.element, declarations));
+    }
+
+    // Flush any unwritten fields/properties.
+    _flushLazyFields(_moduleItems);
+    _flushLibraryProperties(_moduleItems);
+
+    // Mark all qualified names as qualified or not, depending on if they need
+    // to be loaded lazily or not.
+    for (var elementIdPairs in _qualifiedIds) {
+      var element = elementIdPairs.e0;
+      var id = elementIdPairs.e1;
+      id.setQualified(!_loader.isLoaded(element));
+    }
+
+    if (_exports.isNotEmpty) _moduleItems.add(js.comment('Exports:'));
+
+    // TODO(jmesserly): make these immutable in JS?
+    for (var name in _exports) {
+      _moduleItems.add(js.statement('#.# = #;', [_exportsVar, name, name]));
+    }
+
+    var jsPath = compiler.getModuleName(currentLibrary.source.uri);
+
+    // TODO(jmesserly): it would be great to run the renamer on the body,
+    // then figure out if we really need each of these parameters.
+    // See ES6 modules: https://github.com/dart-lang/dev_compiler/issues/34
+    var params = [_exportsVar, _runtimeLibVar];
+    var processImport =
+        (LibraryElement library, JS.TemporaryId temp, List list) {
+      params.add(temp);
+      list.add(js.string(compiler.getModuleName(library.source.uri), "'"));
+    };
+
+    var needsDartRuntime = !_isDartUtils;
+
+    var imports = <JS.Expression>[];
+    var moduleStatements = <JS.Statement>[];
+    if (needsDartRuntime) {
+      imports.add(js.string('dart/_runtime'));
+
+      var dartxImport =
+          js.statement("let # = #.dartx;", [_dartxVar, _runtimeLibVar]);
+      moduleStatements.add(dartxImport);
+    }
+    moduleStatements.addAll(_moduleItems);
+
+    _imports.forEach((library, temp) {
+      if (_loader.libraryIsLoaded(library)) {
+        processImport(library, temp, imports);
+      }
+    });
+
+    var lazyImports = <JS.Expression>[];
+    _imports.forEach((library, temp) {
+      if (!_loader.libraryIsLoaded(library)) {
+        processImport(library, temp, lazyImports);
+      }
+    });
+
+    var module =
+        js.call("function(#) { 'use strict'; #; }", [params, moduleStatements]);
+
+    var moduleDef = js.statement("dart_library.library(#, #, #, #, #)", [
+      js.string(jsPath, "'"),
+      _jsModuleValue ?? new JS.LiteralNull(),
+      js.commentExpression(
+          "Imports", new JS.ArrayInitializer(imports, multiline: true)),
+      js.commentExpression("Lazy imports",
+          new JS.ArrayInitializer(lazyImports, multiline: true)),
+      module
+    ]);
+
+    // TODO(jmesserly): scriptTag support.
+    // Enable this if we know we're targetting command line environment?
+    // It doesn't work in browser.
+    // var jsBin = compiler.options.runnerOptions.v8Binary;
+    // String scriptTag = null;
+    // if (library.library.scriptTag != null) scriptTag = '/usr/bin/env $jsBin';
+    return new JS.Program(<JS.Statement>[moduleDef]);
+  }
+
+  void _emitModuleItem(AstNode node) {
+    // Attempt to group adjacent fields/properties.
+    if (node is! VariableDeclaration) _flushLazyFields(_moduleItems);
+    if (node is! FunctionDeclaration) _flushLibraryProperties(_moduleItems);
+
+    var code = _visit(node);
+    if (code != null) _moduleItems.add(code);
+  }
+
+  @override
+  void visitLibraryDirective(LibraryDirective node) {
+    assert(_jsModuleValue == null);
+
+    var jsName = findAnnotation(node.element, isJSAnnotation);
+    _jsModuleValue =
+        getConstantField(jsName, 'name', types.stringType)?.toStringValue();
+  }
+
+  @override
+  void visitImportDirective(ImportDirective node) {
+    // Nothing to do yet, but we'll want to convert this to an ES6 import once
+    // we have support for modules.
+  }
+
+  @override void visitPartDirective(PartDirective node) {}
+  @override void visitPartOfDirective(PartOfDirective node) {}
+
+  @override
+  void visitExportDirective(ExportDirective node) {
+    var exportName = _libraryName(node.uriElement);
+
+    var currentLibNames = currentLibrary.publicNamespace.definedNames;
+
+    var args = [_exportsVar, exportName];
+    if (node.combinators.isNotEmpty) {
+      var shownNames = <JS.Expression>[];
+      var hiddenNames = <JS.Expression>[];
+
+      var show = node.combinators.firstWhere((c) => c is ShowCombinator,
+          orElse: () => null) as ShowCombinator;
+      var hide = node.combinators.firstWhere((c) => c is HideCombinator,
+          orElse: () => null) as HideCombinator;
+      if (show != null) {
+        shownNames.addAll(show.shownNames
+            .map((i) => i.name)
+            .where((s) => !currentLibNames.containsKey(s))
+            .map((s) => js.string(s, "'")));
+      }
+      if (hide != null) {
+        hiddenNames.addAll(hide.hiddenNames.map((i) => js.string(i.name, "'")));
+      }
+      args.add(new JS.ArrayInitializer(shownNames));
+      args.add(new JS.ArrayInitializer(hiddenNames));
+    }
+    _moduleItems.add(js.statement('dart.export_(#);', [args]));
+  }
+
+  JS.Identifier _initSymbol(JS.Identifier id) {
+    var s =
+        js.statement('const # = $_SYMBOL(#);', [id, js.string(id.name, "'")]);
+    _moduleItems.add(s);
+    return id;
+  }
+
+  // TODO(jmesserly): this is a temporary workaround for `Symbol` in core,
+  // until we have better name tracking.
+  String get _SYMBOL {
+    var name = currentLibrary.name;
+    if (name == 'dart.core' || name == 'dart._internal') return 'dart.JsSymbol';
+    return 'Symbol';
+  }
+
+  bool isPublic(String name) => !name.startsWith('_');
+
+  @override
+  visitAsExpression(AsExpression node) {
+    var from = getStaticType(node.expression);
+    var to = node.type.type;
+
+    var fromExpr = _visit(node.expression);
+
+    // Skip the cast if it's not needed.
+    if (rules.isSubTypeOf(from, to)) return fromExpr;
+
+    // All Dart number types map to a JS double.
+    if (_isNumberInJS(from) && _isNumberInJS(to)) {
+      // Make sure to check when converting to int.
+      if (from != _types.intType && to == _types.intType) {
+        return js.call('dart.asInt(#)', [fromExpr]);
+      }
+
+      // A no-op in JavaScript.
+      return fromExpr;
+    }
+
+    return js.call('dart.as(#, #)', [fromExpr, _emitTypeName(to)]);
+  }
+
+  @override
+  visitIsExpression(IsExpression node) {
+    // Generate `is` as `dart.is` or `typeof` depending on the RHS type.
+    JS.Expression result;
+    var type = node.type.type;
+    var lhs = _visit(node.expression);
+    var typeofName = _jsTypeofName(type);
+    if (typeofName != null) {
+      result = js.call('typeof # == #', [lhs, js.string(typeofName, "'")]);
+    } else {
+      // Always go through a runtime helper, because implicit interfaces.
+      result = js.call('dart.is(#, #)', [lhs, _emitTypeName(type)]);
+    }
+
+    if (node.notOperator != null) {
+      return js.call('!#', result);
+    }
+    return result;
+  }
+
+  String _jsTypeofName(DartType t) {
+    if (_isNumberInJS(t)) return 'number';
+    if (t == _types.stringType) return 'string';
+    if (t == _types.boolType) return 'boolean';
+    return null;
+  }
+
+  @override
+  visitFunctionTypeAlias(FunctionTypeAlias node) {
+    var element = node.element;
+    var type = element.type;
+    var name = element.name;
+
+    var fnType = annotateTypeDef(
+        js.statement('const # = dart.typedef(#, () => #);', [
+          name,
+          js.string(name, "'"),
+          _emitTypeName(type, lowerTypedef: true)
+        ]),
+        node.element);
+
+    return _finishClassDef(type, fnType);
+  }
+
+  @override
+  JS.Expression visitTypeName(TypeName node) => _emitTypeName(node.type);
+
+  @override
+  JS.Statement visitClassTypeAlias(ClassTypeAlias node) {
+    var element = node.element;
+
+    // Forward all generative constructors from the base class.
+    var body = <JS.Method>[];
+
+    var supertype = element.supertype;
+    if (!supertype.isObject) {
+      for (var ctor in element.constructors) {
+        var parentCtor = supertype.lookUpConstructor(ctor.name, ctor.library);
+        var fun = js.call('function() { super.#(...arguments); }',
+            [_constructorName(parentCtor)]) as JS.Fun;
+        body.add(new JS.Method(_constructorName(ctor), fun));
+      }
+    }
+
+    var classDecl = new JS.ClassDeclaration(new JS.ClassExpression(
+        new JS.Identifier(element.name), _classHeritage(element), body));
+
+    return _finishClassDef(element.type, classDecl);
+  }
+
+  JS.Statement _emitJsType(String dartClassName, DartObject jsName) {
+    var jsTypeName =
+        getConstantField(jsName, 'name', types.stringType)?.toStringValue();
+
+    if (jsTypeName != null && jsTypeName != dartClassName) {
+      // We export the JS type as if it was a Dart type. For example this allows
+      // `dom.InputElement` to actually be HTMLInputElement.
+      // TODO(jmesserly): if we had the JS name on the Element, we could just
+      // generate it correctly when we refer to it.
+      if (isPublic(dartClassName)) _addExport(dartClassName);
+      return js.statement('const # = #;', [dartClassName, jsTypeName]);
+    }
+    return null;
+  }
+
+  @override
+  JS.Statement visitClassDeclaration(ClassDeclaration node) {
+    var classElem = node.element;
+    var type = classElem.type;
+    var jsName = findAnnotation(classElem, isJSAnnotation);
+
+    if (jsName != null) return _emitJsType(node.name.name, jsName);
+
+    var ctors = <ConstructorDeclaration>[];
+    var fields = <FieldDeclaration>[];
+    var methods = <MethodDeclaration>[];
+    for (var member in node.members) {
+      if (member is ConstructorDeclaration) {
+        ctors.add(member);
+      } else if (member is FieldDeclaration && !member.isStatic) {
+        fields.add(member);
+      } else if (member is MethodDeclaration) {
+        methods.add(member);
+      }
+    }
+
+    var classExpr = new JS.ClassExpression(new JS.Identifier(type.name),
+        _classHeritage(classElem), _emitClassMethods(node, ctors, fields));
+
+    String jsPeerName;
+    var jsPeer = findAnnotation(classElem, isJsPeerInterface);
+    if (jsPeer != null) {
+      jsPeerName =
+          getConstantField(jsPeer, 'name', types.stringType)?.toStringValue();
+    }
+
+    return new DartClassDeclaration(
+        element: node.element,
+        jsPeerName: jsPeerName,
+        parentRef: parentRef,
+        mixinRefs: mixinRefs,
+        implementedRefs: implementedRefs,
+        genericTypeNames: genericTypeNames,
+        members: members);
+  }
+
+  convertTypeRef(DartType type) => new DartTypeRef(type);
+
+  @override
+  DartClassDeclaration visitEnumDeclaration(EnumDeclaration node) {
+    var element = node.element;
+    var type = element.type;
+    var name = js.string(type.name);
+    var id = newDartTypeExpression(type);
+
+    // Generate a class per section 13 of the spec.
+    // TODO(vsm): Generate any accompanying metadata
+    var members = <DartCallableDeclaration>[];
+
+    // Create constructor and initialize index
+    members.add(newDartConstructor(node.element,
+        js.call('function(index) { this.index = index; }') as JS.Fun));
+
+    var fields = new List<ConstFieldElementImpl>.from(
+        element.fields.where((f) => f.type == type));
+
+    // Create toString() method
+    var properties = new List<JS.Property>();
+    for (var i = 0; i < fields.length; ++i) {
+      properties.add(new JS.Property(
+          js.number(i), js.string('${type.name}.${fields[i].name}')));
+    }
+    var nameMap = new JS.ObjectInitializer(properties, multiline: true);
+    members.add(newDartSyntheticMethod('toString', types.stringType, {},
+        js.call('function() { return #[this.index]; }', nameMap) as JS.Fun));
+
+    int i = 0;
+    for (ConstFieldElementImpl f in element.fields) {
+      if (f.type == type) {
+        members.add(newDartField(f,
+          js.call('dart.const(new #(#));',
+              [id, js.number(i)])));
+        i++;
+      }
+    }
+
+    // Create enum class
+    var result = <JS.Statement>[js.statement('#', classExpr)];
+
+    // Create static fields for each enum value
+    for (var i = 0; i < fields.length; ++i) {
+      result.add(js.statement('#.# = dart.const(new #(#));',
+          [id, fields[i].name, id, js.number(i)]));
+    }
+
+    // Create static values list
+    var values = new JS.ArrayInitializer(new List<JS.Expression>.from(
+        fields.map((f) => js.call('#.#', [id, f.name]))));
+    result.add(js.statement('#.values = dart.const(dart.list(#, #));',
+        [id, values, _emitTypeName(type)]));
+
+    if (isPublic(type.name)) _addExport(type.name);
+    return _statement(result);
+  }
+
+  /// Given a class element and body, complete the class declaration.
+  /// This handles generic type parameters, laziness (in library-cycle cases),
+  /// and ensuring dependencies are loaded first.
+  JS.Statement _finishClassDef(ParameterizedType type, JS.Statement body) {
+    var name = type.name;
+    var genericName = '$name\$';
+
+    JS.Statement genericDef = null;
+    if (type.typeParameters.isNotEmpty) {
+      genericDef = _emitGenericClassDef(type, body);
+    }
+
+    // The base class and all mixins must be declared before this class.
+    if (!_loader.isLoaded(type.element)) {
+      // TODO(jmesserly): the lazy class def is a simple solution for now.
+      // We may want to consider other options in the future.
+
+      if (genericDef != null) {
+        return js.statement(
+            '{ #; dart.defineLazyClassGeneric(#, #, { get: # }); }',
+            [genericDef, _exportsVar, _propertyName(name), genericName]);
+      }
+
+      return js.statement(
+          'dart.defineLazyClass(#, { get #() { #; return #; } });',
+          [_exportsVar, _propertyName(name), body, name]);
+    }
+
+    if (isPublic(name)) _addExport(name);
+
+    if (genericDef != null) {
+      var dynType = fillDynamicTypeArgs(type, types);
+      var genericInst = _emitTypeName(dynType, lowerGeneric: true);
+      return js.statement('{ #; let # = #; }', [genericDef, name, genericInst]);
+    }
+    return body;
+  }
+
+  JS.Statement _emitGenericClassDef(ParameterizedType type, JS.Statement body) {
+    var name = type.name;
+    var genericName = '$name\$';
+    var typeParams = type.typeParameters.map((p) => p.name);
+    if (isPublic(name)) _exports.add(genericName);
+    return js.statement('const # = dart.generic(function(#) { #; return #; });',
+        [genericName, typeParams, body, name]);
+  }
+
+  final _hasDeferredSupertype = new HashSet<ClassElement>();
+
+  bool _deferIfNeeded(DartType type, ClassElement current) {
+    if (type is ParameterizedType) {
+      var typeArguments = type.typeArguments;
+      for (var typeArg in typeArguments) {
+        var typeElement = typeArg.element;
+        // FIXME(vsm): This does not track mutual recursive dependences.
+        if (current == typeElement || _deferIfNeeded(typeArg, current)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  JS.Expression _classHeritage(ClassElement element) {
+    var type = element.type;
+    if (type.isObject) return null;
+
+    // Assume we can load eagerly, until proven otherwise.
+    _loader.startTopLevel(element);
+
+    // Find the super type
+    JS.Expression heritage;
+    var supertype = type.superclass;
+    if (_deferIfNeeded(supertype, element)) {
+      // Fall back to raw type.
+      supertype = fillDynamicTypeArgs(supertype.element.type, rules.provider);
+      _hasDeferredSupertype.add(element);
+    }
+    heritage = _emitTypeName(supertype);
+
+    if (type.mixins.isNotEmpty) {
+      var mixins = type.mixins.map(_emitTypeName).toList();
+      mixins.insert(0, heritage);
+      heritage = js.call('dart.mixin(#)', [mixins]);
+    }
+
+    _loader.finishTopLevel(element);
+    return heritage;
+  }
+
+  List<JS.Method> _emitClassMethods(ClassDeclaration node,
+      List<ConstructorDeclaration> ctors, List<FieldDeclaration> fields) {
+    var element = node.element;
+    var type = element.type;
+    var isObject = type.isObject;
+
+    // Iff no constructor is specified for a class C, it implicitly has a
+    // default constructor `C() : super() {}`, unless C is class Object.
+    var jsMethods = <DartCallableDeclaration>[];
+    if (ctors.isEmpty && !isObject) {
+      jsMethods.add(_emitImplicitConstructor(node, fields));
+    }
+
+    bool hasJsPeer = findAnnotation(element, isJsPeerInterface) != null;
+
+    bool hasIterator = false;
+    for (var m in node.members) {
+      if (m is ConstructorDeclaration) {
+        jsMethods.add(_emitConstructor(m, type, fields, isObject));
+      } else if (m is MethodDeclaration) {
+        jsMethods.add(_emitMethodDeclaration(type, m));
+
+        if (!hasJsPeer && m.isGetter && m.name.name == 'iterator') {
+          hasIterator = true;
+          jsMethods.add(_emitIterable(type));
+        }
+      }
+    }
+
+    // If the type doesn't have an `iterator`, but claims to implement Iterable,
+    // we inject the adaptor method here, as it's less code size to put the
+    // helper on a parent class. This pattern is common in the core libraries
+    // (e.g. IterableMixin<E> and IterableBase<E>).
+    //
+    // (We could do this same optimization for any interface with an `iterator`
+    // method, but that's more expensive to check for, so it doesn't seem worth
+    // it. The above case for an explicit `iterator` method will catch those.)
+    if (!hasJsPeer && !hasIterator && _implementsIterable(type)) {
+      jsMethods.add(_emitIterable(type));
+    }
+
+    return jsMethods.where((m) => m != null).toList(growable: false);
+  }
+
+  bool _implementsIterable(InterfaceType t) =>
+      t.interfaces.any((i) => i.element.type == types.iterableType);
+
+  /// Support for adapting dart:core Iterable to ES6 versions.
+  ///
+  /// This lets them use for-of loops transparently:
+  /// <https://github.com/lukehoban/es6features#iterators--forof>
+  ///
+  /// This will return `null` if the adapter was already added on a super type,
+  /// otherwise it returns the adapter code.
+  // TODO(jmesserly): should we adapt `Iterator` too?
+  JS.Method _emitIterable(InterfaceType t) {
+    // If a parent had an `iterator` (concrete or abstract) or implements
+    // Iterable, we know the adapter is already there, so we can skip it as a
+    // simple code size optimization.
+    var parent = t.lookUpGetterInSuperclass('iterator', t.element.library);
+    if (parent != null) return null;
+    var parentType = findSupertype(t, _implementsIterable);
+    if (parentType != null) return null;
+
+    // Otherwise, emit the adapter method, which wraps the Dart iterator in
+    // an ES6 iterator.
+    return new JS.Method(
+        js.call('$_SYMBOL.iterator'),
+        js.call('function() { return new dart.JsIterator(this.#); }',
+            [_emitMemberName('iterator', type: t)]) as JS.Fun);
+  }
+
+  JS.Expression _instantiateAnnotation(Annotation node) {
+    var element = node.element;
+    if (element is ConstructorElement) {
+      return _emitInstanceCreationExpression(element, element.returnType,
+          node.constructorName, node.arguments, true);
+    } else {
+      return _visit(node.name);
+    }
+  }
+
+  /// Emit class members that need to come after the class declaration, such
+  /// as static fields. See [_emitClassMethods] for things that are emitted
+  /// inside the ES6 `class { ... }` node.
+  JS.Statement _finishClassMembers(
+      ClassElement classElem,
+      JS.ClassExpression cls,
+      List<ConstructorDeclaration> ctors,
+      List<FieldDeclaration> fields,
+      List<MethodDeclaration> methods,
+      List<Annotation> metadata,
+      String jsPeerName) {
+    var name = classElem.name;
+    var body = <JS.Statement>[];
+
+    if (_extensionTypes.contains(classElem)) {
+      var dartxNames = <JS.Expression>[];
+      for (var m in methods) {
+        if (!m.isAbstract && !m.isStatic && m.element.isPublic) {
+          dartxNames.add(_elementMemberName(m.element, allowExtensions: false));
+        }
+      }
+      if (dartxNames.isNotEmpty) {
+        body.add(js.statement('dart.defineExtensionNames(#)',
+            [new JS.ArrayInitializer(dartxNames, multiline: true)]));
+      }
+    }
+
+    body.add(new JS.ClassDeclaration(cls));
+
+    // TODO(jmesserly): we should really just extend native Array.
+    if (jsPeerName != null && classElem.typeParameters.isNotEmpty) {
+      body.add(js.statement('dart.setBaseClass(#, dart.global.#);',
+          [classElem.name, _propertyName(jsPeerName)]));
+    }
+
+    // Deferred Superclass
+    if (_hasDeferredSupertype.contains(classElem)) {
+      body.add(js.statement('#.prototype.__proto__ = #.prototype;',
+          [name, _emitTypeName(classElem.type.superclass)]));
+    }
+
+    // Interfaces
+    if (classElem.interfaces.isNotEmpty) {
+      body.add(js.statement('#[dart.implements] = () => #;', [
+        name,
+        new JS.ArrayInitializer(new List<JS.Expression>.from(
+            classElem.interfaces.map(_emitTypeName)))
+      ]));
+    }
+
+    // Named constructors
+    for (ConstructorDeclaration member in ctors) {
+      if (member.name != null && member.factoryKeyword == null) {
+        body.add(js.statement('dart.defineNamedConstructor(#, #);',
+            [name, _emitMemberName(member.name.name, isStatic: true)]));
+      }
+    }
+
+    // Instance fields, if they override getter/setter pairs
+    for (FieldDeclaration member in fields) {
+      for (VariableDeclaration fieldDecl in member.fields.variables) {
+        var field = fieldDecl.element as FieldElement;
+        if (_fieldsNeedingStorage.contains(field)) {
+          body.add(_overrideField(field));
+        }
+      }
+    }
+
+    // Emit the signature on the class recording the runtime type information
+    var extensions = _extensionsToImplement(classElem);
+    {
+      var tStatics = <JS.Property>[];
+      var tMethods = <JS.Property>[];
+      var sNames = <JS.Expression>[];
+      for (MethodDeclaration node in methods) {
+        if (!(node.isSetter || node.isGetter || node.isAbstract)) {
+          var name = node.name.name;
+          var element = node.element;
+          var inheritedElement =
+              classElem.lookUpInheritedConcreteMethod(name, currentLibrary);
+          if (inheritedElement != null &&
+              inheritedElement.type == element.type) continue;
+          var memberName = _elementMemberName(element);
+          var parts = _emitFunctionTypeParts(element.type);
+          var property =
+              new JS.Property(memberName, new JS.ArrayInitializer(parts));
+          if (node.isStatic) {
+            tStatics.add(property);
+            sNames.add(memberName);
+          } else {
+            tMethods.add(property);
+          }
+        }
+      }
+
+      var tCtors = <JS.Property>[];
+      for (ConstructorDeclaration node in ctors) {
+        var memberName = _constructorName(node.element);
+        var element = node.element;
+        var parts = _emitFunctionTypeParts(element.type, node.parameters);
+        var property =
+            new JS.Property(memberName, new JS.ArrayInitializer(parts));
+        tCtors.add(property);
+      }
+
+      JS.Property build(String name, List<JS.Property> elements) {
+        var o =
+            new JS.ObjectInitializer(elements, multiline: elements.length > 1);
+        var e = js.call('() => #', o);
+        return new JS.Property(_propertyName(name), e);
+      }
+      var sigFields = <JS.Property>[];
+      if (!tCtors.isEmpty) sigFields.add(build('constructors', tCtors));
+      if (!tMethods.isEmpty) sigFields.add(build('methods', tMethods));
+      if (!tStatics.isEmpty) {
+        assert(!sNames.isEmpty);
+        var aNames = new JS.Property(
+            _propertyName('names'), new JS.ArrayInitializer(sNames));
+        sigFields.add(build('statics', tStatics));
+        sigFields.add(aNames);
+      }
+      if (!sigFields.isEmpty || extensions.isNotEmpty) {
+        var sig = new JS.ObjectInitializer(sigFields);
+        var classExpr = new JS.Identifier(name);
+        body.add(js.statement('dart.setSignature(#, #);', [classExpr, sig]));
+      }
+    }
+
+    // If a concrete class implements one of our extensions, we might need to
+    // add forwarders.
+    if (extensions.isNotEmpty) {
+      var methodNames = <JS.Expression>[];
+      for (var e in extensions) {
+        methodNames.add(_elementMemberName(e));
+      }
+      body.add(js.statement('dart.defineExtensionMembers(#, #);', [
+        name,
+        new JS.ArrayInitializer(methodNames, multiline: methodNames.length > 4)
+      ]));
+    }
+
+    // TODO(vsm): Make this optional per #268.
+    // Metadata
+    if (metadata.isNotEmpty) {
+      body.add(js.statement('#[dart.metadata] = () => #;', [
+        name,
+        new JS.ArrayInitializer(
+            new List<JS.Expression>.from(metadata.map(_instantiateAnnotation)))
+      ]));
+    }
+
+    return _statement(body);
+  }
+
+  List<ExecutableElement> _extensionsToImplement(ClassElement element) {
+    var members = <ExecutableElement>[];
+    if (_extensionTypes.contains(element)) return members;
+
+    // Collect all extension types we implement.
+    var type = element.type;
+    var types = new Set<ClassElement>();
+    _collectExtensions(type, types);
+    if (types.isEmpty) return members;
+
+    // Collect all possible extension method names.
+    var extensionMembers = new HashSet<String>();
+    for (var t in types) {
+      for (var m in [t.methods, t.accessors].expand((e) => e)) {
+        if (!m.isStatic) extensionMembers.add(m.name);
+      }
+    }
+
+    // Collect all of extension methods this type implements.
+    for (var m in [type.methods, type.accessors].expand((e) => e)) {
+      if (!m.isStatic && !m.isAbstract && extensionMembers.contains(m.name)) {
+        members.add(m);
+      }
+    }
+    return members;
+  }
+
+  /// Collections the type and all supertypes, including interfaces, but
+  /// excluding [Object].
+  void _collectExtensions(InterfaceType type, Set<ClassElement> types) {
+    if (type.isObject) return;
+    var element = type.element;
+    if (_extensionTypes.contains(element)) types.add(element);
+    for (var m in type.mixins.reversed) {
+      _collectExtensions(m, types);
+    }
+    for (var i in type.interfaces) {
+      _collectExtensions(i, types);
+    }
+    _collectExtensions(type.superclass, types);
+  }
+
+  JS.Statement _overrideField(FieldElement e) {
+    var cls = e.enclosingElement;
+    return js.statement('dart.virtualField(#, #)',
+        [cls.name, _emitMemberName(e.name, type: cls.type)]);
+  }
+
+  /// Generates the implicit default constructor for class C of the form
+  /// `C() : super() {}`.
+  DartCallableDeclaration _emitImplicitConstructor(
+      ClassDeclaration node, List<FieldDeclaration> fields) {
+    assert(_hasUnnamedConstructor(node.element) == fields.isNotEmpty);
+
+    // If we don't have a method body, skip this.
+    var superCall = _superConstructorCall(node.element);
+    if (fields.isEmpty && superCall == null) return null;
+
+    dynamic body = _initializeFields(node, fields);
+    if (superCall != null) body = [
+      [body, superCall]
+    ];
+    return newDartConstructor(node.element, js.call('function() { #; }', body) as JS.Fun);
+  }
+
+  DartCallableDeclaration _emitConstructor(ConstructorDeclaration node, InterfaceType type,
+      List<FieldDeclaration> fields, bool isObject) {
+    if (_externalOrNative(node)) return null;
+
+    var name = _constructorName(node.element);
+
+    // Wacky factory redirecting constructors: factory Foo.q(x, y) = Bar.baz;
+    var redirect = node.redirectedConstructor;
+    if (redirect != null) {
+      var newKeyword = redirect.staticElement.isFactory ? '' : 'new';
+      // Pass along all arguments verbatim, and let the callee handle them.
+      // TODO(jmesserly): we'll need something different once we have
+      // rest/spread support, but this should work for now.
+      var params =
+          _emitFormalParameterList(node.parameters, allowDestructuring: false);
+
+      var fun = js.call('function(#) { return $newKeyword #(#); }',
+          [params, _visit(redirect), params]) as JS.Fun;
+      return annotate(
+          new JS.Method(name, fun, isStatic: true)..sourceInformation = node,
+          node.element);
+    }
+
+    // Factory constructors are essentially static methods.
+    if (node.factoryKeyword != null) {
+      var body = <JS.Statement>[];
+      var init = _emitArgumentInitializers(node, constructor: true);
+      if (init != null) body.add(init);
+      body.add(_visit(node.body));
+      var fun = new JS.Fun(
+          _visit(node.parameters) as List<JS.Parameter>, new JS.Block(body));
+      return newDartConstructor(node.element, fun);
+    }
+
+    // Code generation for Object's constructor.
+    JS.Block body;
+    if (isObject &&
+        node.body is EmptyFunctionBody &&
+        node.constKeyword != null &&
+        node.name == null) {
+      // Implements Dart constructor behavior. Because of V8 `super`
+      // [constructor restrictions]
+      // (https://code.google.com/p/v8/issues/detail?id=3330#c65)
+      // we cannot currently emit actual ES6 constructors with super calls.
+      // Instead we use the same trick as named constructors, and do them as
+      // instance methods that perform initialization.
+      // TODO(jmesserly): we'll need to rethink this once the ES6 spec and V8
+      // settles. See <https://github.com/dart-lang/dev_compiler/issues/51>.
+      // Performance of this pattern is likely to be bad.
+      name = _propertyName('constructor');
+      // Mark the parameter as no-rename.
+      body = js.statement('''{
+        // Get the class name for this instance.
+        let name = this.constructor.name;
+        // Call the default constructor.
+        let result = void 0;
+        if (name in this) result = this[name](...arguments);
+        return result === void 0 ? this : result;
+      }''') as JS.Block;
+    } else {
+      body = _emitConstructorBody(node, fields);
+    }
+
+    // We generate constructors as initializer methods in the class;
+    // this allows use of `super` for instance methods/properties.
+    // It also avoids V8 restrictions on `super` in default constructors.
+    return newDartConstructor(
+        node.element,
+        new JS.Fun(_visit(node.parameters) as List<JS.Parameter>, body));
+  }
+
+  JS.Expression _constructorName(ConstructorElement ctor) {
+    var name = ctor.name;
+    if (name != '') {
+      return _emitMemberName(name, isStatic: true);
+    }
+
+    // Factory default constructors use `new` as their name, for readability
+    // Other default constructors use the class name, as they aren't called
+    // from call sites, but rather from Object's constructor.
+    // TODO(jmesserly): revisit in the context of Dart metaclasses, and cleaning
+    // up constructors to integrate more closely with ES6.
+    return _propertyName(ctor.isFactory ? 'new' : ctor.enclosingElement.name);
+  }
+
+  JS.Block _emitConstructorBody(
+      ConstructorDeclaration node, List<FieldDeclaration> fields) {
+    var body = <JS.Statement>[];
+
+    // Generate optional/named argument value assignment. These can not have
+    // side effects, and may be used by the constructor's initializers, so it's
+    // nice to do them first.
+    // Also for const constructors we need to ensure default values are
+    // available for use by top-level constant initializers.
+    ClassDeclaration cls = node.parent;
+    if (node.constKeyword != null) _loader.startTopLevel(cls.element);
+    var init = _emitArgumentInitializers(node, constructor: true);
+    if (node.constKeyword != null) _loader.finishTopLevel(cls.element);
+    if (init != null) body.add(init);
+
+    // Redirecting constructors: these are not allowed to have initializers,
+    // and the redirecting ctor invocation runs before field initializers.
+    var redirectCall = node.initializers.firstWhere(
+        (i) => i is RedirectingConstructorInvocation,
+        orElse: () => null);
+
+    if (redirectCall != null) {
+      body.add(_visit(redirectCall));
+      return new JS.Block(body);
+    }
+
+    // Generate field initializers.
+    // These are expanded into each non-redirecting constructor.
+    // In the future we may want to create an initializer function if we have
+    // multiple constructors, but it needs to be balanced against readability.
+    body.add(_initializeFields(cls, fields, node));
+
+    var superCall = node.initializers.firstWhere(
+        (i) => i is SuperConstructorInvocation,
+        orElse: () => null) as SuperConstructorInvocation;
+
+    // If no superinitializer is provided, an implicit superinitializer of the
+    // form `super()` is added at the end of the initializer list, unless the
+    // enclosing class is class Object.
+    var jsSuper = _superConstructorCall(cls.element, superCall);
+    if (jsSuper != null) body.add(jsSuper);
+
+    body.add(_visit(node.body));
+    return new JS.Block(body)..sourceInformation = node;
+  }
+
+  @override
+  JS.Statement visitRedirectingConstructorInvocation(
+      RedirectingConstructorInvocation node) {
+    var name = _constructorName(node.staticElement);
+    return js.statement('this.#(#);', [name, _visit(node.argumentList)]);
+  }
+
+  JS.Statement _superConstructorCall(ClassElement element,
+      [SuperConstructorInvocation node]) {
+    ConstructorElement superCtor;
+    if (node != null) {
+      superCtor = node.staticElement;
+    } else {
+      // Get the supertype's unnamed constructor.
+      superCtor = element.supertype.element.unnamedConstructor;
+      if (superCtor == null) {
+        // This will only happen if the code has errors:
+        // we're trying to generate an implicit constructor for a type where
+        // we don't have a default constructor in the supertype.
+        assert(options.forceCompile);
+        return null;
+      }
+    }
+
+    if (superCtor.name == '' && !_shouldCallUnnamedSuperCtor(element)) {
+      return null;
+    }
+
+    var name = _constructorName(superCtor);
+    var args = node != null ? _visit(node.argumentList) : [];
+    return js.statement('super.#(#);', [name, args])..sourceInformation = node;
+  }
+
+  bool _shouldCallUnnamedSuperCtor(ClassElement e) {
+    var supertype = e.supertype;
+    if (supertype == null) return false;
+    if (_hasUnnamedConstructor(supertype.element)) return true;
+    for (var mixin in e.mixins) {
+      if (_hasUnnamedConstructor(mixin.element)) return true;
+    }
+    return false;
+  }
+
+  bool _hasUnnamedConstructor(ClassElement e) {
+    if (e.type.isObject) return false;
+    if (!e.unnamedConstructor.isSynthetic) return true;
+    return e.fields.any((f) => !f.isStatic && !f.isSynthetic);
+  }
+
+  /// Initialize fields. They follow the sequence:
+  ///
+  ///   1. field declaration initializer if non-const,
+  ///   2. field initializing parameters,
+  ///   3. constructor field initializers,
+  ///   4. initialize fields not covered in 1-3
+  JS.Statement _initializeFields(
+      ClassDeclaration cls, List<FieldDeclaration> fieldDecls,
+      [ConstructorDeclaration ctor]) {
+    var unit = cls.getAncestor((a) => a is CompilationUnit) as CompilationUnit;
+    var constField = new ConstFieldVisitor(types, unit);
+    bool isConst = ctor != null && ctor.constKeyword != null;
+    if (isConst) _loader.startTopLevel(cls.element);
+
+    // Run field initializers if they can have side-effects.
+    var fields = new Map<FieldElement, JS.Expression>();
+    var unsetFields = new Map<FieldElement, VariableDeclaration>();
+    for (var declaration in fieldDecls) {
+      for (var fieldNode in declaration.fields.variables) {
+        var element = fieldNode.element;
+        if (constField.isFieldInitConstant(fieldNode)) {
+          unsetFields[element as FieldElement] = fieldNode;
+        } else {
+          fields[element as FieldElement] = _visitInitializer(fieldNode);
+        }
+      }
+    }
+
+    // Initialize fields from `this.fieldName` parameters.
+    if (ctor != null) {
+      for (var p in ctor.parameters.parameters) {
+        var element = p.element;
+        if (element is FieldFormalParameterElement) {
+          fields[element.field] = _visit(p);
+        }
+      }
+
+      // Run constructor field initializers such as `: foo = bar.baz`
+      for (var init in ctor.initializers) {
+        if (init is ConstructorFieldInitializer) {
+          fields[init.fieldName.staticElement as FieldElement] =
+              _visit(init.expression);
+        }
+      }
+    }
+
+    for (var f in fields.keys) unsetFields.remove(f);
+
+    // Initialize all remaining fields
+    unsetFields.forEach((element, fieldNode) {
+      JS.Expression value;
+      if (fieldNode.initializer != null) {
+        value = _visit(fieldNode.initializer);
+      } else {
+        value = new JS.LiteralNull();
+      }
+      fields[element] = value;
+    });
+
+    var body = <JS.Statement>[];
+    fields.forEach((FieldElement e, JS.Expression initialValue) {
+      var access = _emitMemberName(e.name, type: e.enclosingElement.type);
+      body.add(js.statement('this.# = #;', [access, initialValue]));
+    });
+
+    if (isConst) _loader.finishTopLevel(cls.element);
+    return _statement(body);
+  }
+
+  FormalParameterList _parametersOf(node) {
+    // TODO(jmesserly): clean this up. If we can model ES6 spread/rest args, we
+    // could handle argument initializers more consistently in a separate
+    // lowering pass.
+    if (node is ConstructorDeclaration) return node.parameters;
+    if (node is MethodDeclaration) return node.parameters;
+    if (node is FunctionDeclaration) node = node.functionExpression;
+    return (node as FunctionExpression).parameters;
+  }
+
+  /// Emits argument initializers, which handles optional/named args, as well
+  /// as generic type checks needed due to our covariance.
+  JS.Statement _emitArgumentInitializers(node, {bool constructor: false}) {
+    // Constructor argument initializers are emitted earlier in the code, rather
+    // than always when we visit the function body, so we control it explicitly.
+    if (node is ConstructorDeclaration != constructor) return null;
+
+    var parameters = _parametersOf(node);
+    if (parameters == null) return null;
+
+    var body = <JS.Statement>[];
+    for (var param in parameters.parameters) {
+      var jsParam = _visit(param.identifier);
+
+      if (param.kind == ParameterKind.NAMED) {
+        if (!_isDestructurableNamedParam(param)) {
+          // Parameters will be passed using their real names, not the (possibly
+          // renamed) local variable.
+          var paramName = js.string(param.identifier.name, "'");
+
+          // TODO(ochafik): Fix `'prop' in obj` to please Closure's renaming.
+          body.add(js.statement('let # = # && # in # ? #.# : #;', [
+            jsParam,
+            _namedArgTemp,
+            paramName,
+            _namedArgTemp,
+            _namedArgTemp,
+            paramName,
+            _defaultParamValue(param),
+          ]));
+        }
+      } else if (param.kind == ParameterKind.POSITIONAL) {
+        body.add(js.statement('if (# === void 0) # = #;',
+            [jsParam, jsParam, _defaultParamValue(param)]));
+      }
+
+      // TODO(jmesserly): various problems here, see:
+      // https://github.com/dart-lang/dev_compiler/issues/161
+      var paramType = param.element.type;
+      if (!constructor && _hasTypeParameter(paramType)) {
+        body.add(js.statement(
+            'dart.as(#, #);', [jsParam, _emitTypeName(paramType)]));
+      }
+    }
+    return body.isEmpty ? null : _statement(body);
+  }
+
+  bool _hasTypeParameter(DartType t) => t is TypeParameterType ||
+      t is ParameterizedType && t.typeArguments.any(_hasTypeParameter);
+
+  JS.Expression _defaultParamValue(FormalParameter param) {
+    if (param is DefaultFormalParameter && param.defaultValue != null) {
+      return _visit(param.defaultValue);
+    } else {
+      return new JS.LiteralNull();
+    }
+  }
+
+  JS.Method _emitMethodDeclaration(DartType type, MethodDeclaration node) {
+    if (node.isAbstract || _externalOrNative(node)) {
+      return null;
+    }
+
+    var params = _visit(node.parameters) as List<JS.Parameter>;
+    if (params == null) params = <JS.Parameter>[];
+
+    JS.Fun fn = _emitFunctionBody(params, node.body);
+    if (node.operatorKeyword != null &&
+        node.name.name == '[]=' &&
+        params.isNotEmpty) {
+      // []= methods need to return the value. We could also address this at
+      // call sites, but it's cleaner to instead transform the operator method.
+      var returnValue = new JS.Return(params.last);
+      var body = fn.body;
+      if (JS.Return.foundIn(fn)) {
+        // If a return is inside body, transform `(params) { body }` to
+        // `(params) { (() => { body })(); return value; }`.
+        // TODO(jmesserly): we could instead generate the return differently,
+        // and avoid the immediately invoked function.
+        body = new JS.Call(new JS.ArrowFun([], fn.body), []).toStatement();
+      }
+      // Rewrite the function to include the return.
+      fn = new JS.Fun(fn.params, new JS.Block([body, returnValue]))
+        ..sourceInformation = fn.sourceInformation;
+    }
+
+    return annotate(
+        new JS.Method(_elementMemberName(node.element), fn,
+            isGetter: node.isGetter,
+            isSetter: node.isSetter,
+            isStatic: node.isStatic),
+        node.element);
+  }
+
+  @override
+  DartCallableDeclaration visitFunctionDeclaration(FunctionDeclaration node) {
+    assert(node.parent is CompilationUnit);
+
+    if (_externalOrNative(node)) return null;
+
+    if (node.isGetter || node.isSetter) {
+      // Add these later so we can use getter/setter syntax.
+      _properties.add(node);
+      return null;
+    }
+
+    var body = <JS.Statement>[];
+    _flushLibraryProperties(body);
+
+    var name = node.name.name;
+
+    var fn = _visit(node.functionExpression);
+    bool needsTagging = true;
+
+    if (currentLibrary.source.isInSystemLibrary &&
+        _isInlineJSFunction(node.functionExpression)) {
+      fn = _simplifyPassThroughArrowFunCallBody(fn);
+      needsTagging = !_isDartUtils;
+    }
+
+    var id = new JS.Identifier(name);
+    body.add(annotate(new JS.FunctionDeclaration(id, fn), node.element));
+    if (needsTagging) {
+      body.add(_emitFunctionTagged(id, node.element.type, topLevel: true)
+          .toStatement());
+    }
+
+    if (isPublic(name)) _addExport(name);
+    return _statement(body);
+  }
+
+  bool _isInlineJSFunction(FunctionExpression functionExpression) {
+    var body = functionExpression.body;
+    if (body is ExpressionFunctionBody) {
+      return _isJSInvocation(body.expression);
+    } else if (body is BlockFunctionBody) {
+      if (body.block.statements.length == 1) {
+        var stat = body.block.statements.single;
+        if (stat is ReturnStatement) {
+          return _isJSInvocation(stat.expression);
+        }
+      }
+    }
+    return false;
+  }
+
+  bool _isJSInvocation(Expression expr) =>
+      expr is MethodInvocation && isInlineJS(expr.methodName.staticElement);
+
+  // Simplify `(args) => ((x, y) => { ... })(x, y)` to `(args) => { ... }`.
+  // Note: we don't check if the top-level args match the ones passed through
+  // the arrow function, which allows silently passing args through to the
+  // body (which only works if we don't do weird renamings of Dart params).
+  JS.Fun _simplifyPassThroughArrowFunCallBody(JS.Fun fn) {
+    String getIdent(JS.Node node) => node is JS.Identifier ? node.name : null;
+    List<String> getIdents(List params) =>
+        params.map(getIdent).toList(growable: false);
+
+    if (fn.body is JS.Block && fn.body.statements.length == 1) {
+      var stat = fn.body.statements.single;
+      if (stat is JS.Return && stat.value is JS.Call) {
+        JS.Call call = stat.value;
+        if (call.target is JS.ArrowFun) {
+          var passedArgs = getIdents(call.arguments);
+          JS.ArrowFun innerFun = call.target;
+          if (_listEquality.equals(getIdents(innerFun.params), passedArgs)) {
+            return new JS.Fun(fn.params, innerFun.body);
+          }
+        }
+      }
+    }
+    return fn;
+  }
+
+  JS.Method _emitTopLevelProperty(FunctionDeclaration node) {
+    var name = node.name.name;
+    return annotate(
+        new JS.Method(_propertyName(name), _visit(node.functionExpression),
+            isGetter: node.isGetter, isSetter: node.isSetter),
+        node.element);
+  }
+
+  bool _executesAtTopLevel(AstNode node) {
+    var ancestor = node.getAncestor((n) => n is FunctionBody ||
+        (n is FieldDeclaration && n.staticKeyword == null) ||
+        (n is ConstructorDeclaration && n.constKeyword == null));
+    return ancestor == null;
+  }
+
+  bool _typeIsLoaded(DartType type) {
+    if (type is FunctionType && (type.name == '' || type.name == null)) {
+      return (_typeIsLoaded(type.returnType) &&
+          type.optionalParameterTypes.every(_typeIsLoaded) &&
+          type.namedParameterTypes.values.every(_typeIsLoaded) &&
+          type.normalParameterTypes.every(_typeIsLoaded));
+    }
+    if (type.isDynamic || type.isVoid || type.isBottom) return true;
+    if (type is ParameterizedType && !type.typeArguments.every(_typeIsLoaded)) {
+      return false;
+    }
+    return _loader.isLoaded(type.element);
+  }
+
+  JS.Expression _emitFunctionTagged(JS.Expression clos, DartType type,
+      {topLevel: false}) {
+    var name = type.name;
+    var lazy = topLevel && !_typeIsLoaded(type);
+
+    if (type is FunctionType && (name == '' || name == null)) {
+      if (type.returnType.isDynamic &&
+          type.optionalParameterTypes.isEmpty &&
+          type.namedParameterTypes.isEmpty &&
+          type.normalParameterTypes.every((t) => t.isDynamic)) {
+        return js.call('dart.fn(#)', [clos]);
+      }
+      if (lazy) {
+        return js.call('dart.fn(#, () => #)', [clos, _emitFunctionRTTI(type)]);
+      }
+      return js.call('dart.fn(#, #)', [clos, _emitFunctionTypeParts(type)]);
+    }
+    throw 'Function has non function type: $type';
+  }
+
+  @override
+  JS.Expression visitFunctionExpression(FunctionExpression node) {
+    var params = _visit(node.parameters) as List<JS.Parameter>;
+    if (params == null) params = <JS.Parameter>[];
+
+    var parent = node.parent;
+    var inStmt = parent.parent is FunctionDeclarationStatement;
+    if (parent is FunctionDeclaration) {
+      return _emitFunctionBody(params, node.body);
+    } else {
+      // Chrome Canary does not accept default values with destructuring in
+      // arrow functions yet (e.g. `({a} = {}) => 1`) but happily accepts them
+      // with regular functions (e.g. `function({a} = {}) { return 1 }`).
+      // Note that Firefox accepts both syntaxes just fine.
+      // TODO(ochafik): Simplify this code when Chrome Canary catches up.
+      var canUseArrowFun = !node.parameters.parameters.any(_isNamedParam);
+
+      String code = canUseArrowFun ? '(#) => #' : 'function(#) { return # }';
+      JS.Node jsBody;
+      var body = node.body;
+      if (body.isGenerator || body.isAsynchronous) {
+        jsBody = _emitGeneratorFunctionBody(params, body);
+      } else if (body is ExpressionFunctionBody) {
+        jsBody = _visit(body.expression);
+      } else {
+        code = canUseArrowFun ? '(#) => { #; }' : 'function(#) { #; }';
+        jsBody = _visit(body);
+      }
+      var clos = js.call(code, [params, jsBody]);
+      if (!inStmt) {
+        var type = getStaticType(node);
+        return _emitFunctionTagged(clos, type,
+            topLevel: _executesAtTopLevel(node));
+      }
+      return clos;
+    }
+  }
+
+  JS.Fun _emitFunctionBody(List<JS.Parameter> params, FunctionBody body) {
+    // sync*, async, async*
+    if (body.isAsynchronous || body.isGenerator) {
+      return new JS.Fun(
+          params,
+          js.statement(
+              '{ return #; }', [_emitGeneratorFunctionBody(params, body)]));
+    }
+    // normal function (sync)
+    return new JS.Fun(params, _visit(body));
+  }
+
+  JS.Expression _emitGeneratorFunctionBody(
+      List<JS.Parameter> params, FunctionBody body) {
+    var kind = body.isSynchronous ? 'sync' : 'async';
+    if (body.isGenerator) kind += 'Star';
+
+    // Transforms `sync*` `async` and `async*` function bodies
+    // using ES6 generators.
+    //
+    // `sync*` wraps a generator in a Dart Iterable<T>:
+    //
+    // function name(<args>) {
+    //   return dart.syncStar(function*(<args>) {
+    //     <body>
+    //   }, T, <args>).bind(this);
+    // }
+    //
+    // We need to include <args> in case any are mutated, so each `.iterator`
+    // gets the same initial values.
+    //
+    // TODO(jmesserly): we could omit the args for the common case where args
+    // are not mutated inside the generator.
+    //
+    // In the future, we might be able to simplify this, see:
+    // https://github.com/dart-lang/dev_compiler/issues/247.
+    //
+    // `async` works the same, but uses the `dart.async` helper.
+    //
+    // In the body of a `sync*` and `async`, `yield`/`await` are both generated
+    // simply as `yield`.
+    //
+    // `async*` uses the `dart.asyncStar` helper, and also has an extra `stream`
+    // argument to the generator, which is used for passing values to the
+    // _AsyncStarStreamController implementation type.
+    // `yield` is specially generated inside `async*`, see visitYieldStatement.
+    // `await` is generated as `yield`.
+    // runtime/_generators.js has an example of what the code is generated as.
+    var savedController = _asyncStarController;
+    List jsParams;
+    if (kind == 'asyncStar') {
+      _asyncStarController = new JS.TemporaryId('stream');
+      jsParams = [_asyncStarController]..addAll(params);
+    } else {
+      _asyncStarController = null;
+      jsParams = params;
+    }
+    JS.Expression gen = new JS.Fun(jsParams, _visit(body), isGenerator: true);
+    if (JS.This.foundIn(gen)) {
+      gen = js.call('#.bind(this)', gen);
+    }
+    _asyncStarController = savedController;
+
+    var T = _emitTypeName(rules.getExpectedReturnType(body));
+    return js.call('dart.#(#)', [
+      kind,
+      [gen, T]..addAll(params)
+    ]);
+  }
+
+  @override
+  JS.Statement visitFunctionDeclarationStatement(
+      FunctionDeclarationStatement node) {
+    var func = node.functionDeclaration;
+    if (func.isGetter || func.isSetter) {
+      return js.comment('Unimplemented function get/set statement: $node');
+    }
+
+    var fn = _visit(func.functionExpression);
+
+    var name = new JS.Identifier(func.name.name);
+    JS.Statement declareFn;
+    if (JS.This.foundIn(fn)) {
+      declareFn = js.statement('const # = #.bind(this);', [name, fn]);
+    } else {
+      declareFn = new JS.FunctionDeclaration(name, fn);
+    }
+    declareFn = annotate(declareFn, node.functionDeclaration.element);
+
+    return new JS.Block([
+      declareFn,
+      _emitFunctionTagged(name, func.element.type).toStatement()
+    ]);
+  }
+
+  /// Writes a simple identifier. This can handle implicit `this` as well as
+  /// going through the qualified library name if necessary.
+  @override
+  JS.Expression visitSimpleIdentifier(SimpleIdentifier node) {
+    var accessor = node.staticElement;
+    if (accessor == null) {
+      return js.commentExpression(
+          'Unimplemented unknown name', new JS.Identifier(node.name));
+    }
+
+    // Get the original declaring element. If we had a property accessor, this
+    // indirects back to a (possibly synthetic) field.
+    var element = accessor;
+    if (accessor is PropertyAccessorElement) element = accessor.variable;
+
+    _loader.declareBeforeUse(element);
+
+    var name = element.name;
+
+    // type literal
+    if (element is ClassElement ||
+        element is DynamicElementImpl ||
+        element is FunctionTypeAliasElement) {
+      return _emitTypeName(
+          fillDynamicTypeArgs((element as dynamic).type, types));
+    }
+
+    // library member
+    if (element.enclosingElement is CompilationUnitElement) {
+      return _maybeQualifiedName(element);
+    }
+
+    // Unqualified class member. This could mean implicit-this, or implicit
+    // call to a static from the same class.
+    if (element is ClassMemberElement && element is! ConstructorElement) {
+      bool isStatic = element.isStatic;
+      var type = element.enclosingElement.type;
+      var member = _emitMemberName(name, isStatic: isStatic, type: type);
+
+      // For static methods, we add the raw type name, without generics or
+      // library prefix. We don't need those because static calls can't use
+      // the generic type.
+      if (isStatic) {
+        var dynType = _emitTypeName(fillDynamicTypeArgs(type, types));
+        return new JS.PropertyAccess(dynType, member);
+      }
+
+      // For instance members, we add implicit-this.
+      // For method tear-offs, we ensure it's a bound method.
+      var tearOff = element is MethodElement && !inInvocationContext(node);
+      var code = (tearOff) ? 'dart.bind(this, #)' : 'this.#';
+      return js.call(code, member);
+    }
+
+    // initializing formal parameter, e.g. `Point(this.x)`
+    if (element is ParameterElement &&
+        element.isInitializingFormal &&
+        element.isPrivate) {
+      /// Rename private names so they don't shadow the private field symbol.
+      /// The renamer would handle this, but it would prefer to rename the
+      /// temporary used for the private symbol. Instead rename the parameter.
+      return _getTemp(element, '${name.substring(1)}');
+    }
+
+    if (element is TemporaryVariableElement) {
+      if (name[0] == '#') {
+        return new JS.InterpolatedExpression(name.substring(1));
+      } else {
+        return _getTemp(element, name);
+      }
+    }
+
+    return new JS.Identifier(name);
+  }
+
+  JS.TemporaryId _getTemp(Element key, String name) =>
+      _temps.putIfAbsent(key, () => new JS.TemporaryId(name));
+
+  List<Annotation> _parameterMetadata(FormalParameter p) =>
+      (p is NormalFormalParameter)
+          ? p.metadata
+          : (p as DefaultFormalParameter).parameter.metadata;
+
+  JS.ArrayInitializer _emitTypeNames(List<DartType> types,
+      [List<FormalParameter> parameters]) {
+    var result = <JS.Expression>[];
+    for (int i = 0; i < types.length; ++i) {
+      var metadata =
+          parameters != null ? _parameterMetadata(parameters[i]) : [];
+      var typeName = _emitTypeName(types[i]);
+      var value = typeName;
+      // TODO(vsm): Make this optional per #268.
+      if (metadata.isNotEmpty) {
+        metadata = metadata.map(_instantiateAnnotation).toList();
+        value = new JS.ArrayInitializer([typeName]..addAll(metadata));
+      }
+      result.add(value);
+    }
+    return new JS.ArrayInitializer(result);
+  }
+
+  JS.ObjectInitializer _emitTypeProperties(Map<String, DartType> types) {
+    var properties = <JS.Property>[];
+    types.forEach((name, type) {
+      var key = _propertyName(name);
+      var value = _emitTypeName(type);
+      properties.add(new JS.Property(key, value));
+    });
+    return new JS.ObjectInitializer(properties);
+  }
+
+  /// Emit the pieces of a function type, as an array of return type,
+  /// regular args, and optional/named args.
+  List<JS.Expression> _emitFunctionTypeParts(FunctionType type,
+      [FormalParameterList parameterList]) {
+    var parameters = parameterList?.parameters;
+    var returnType = type.returnType;
+    var parameterTypes = type.normalParameterTypes;
+    var optionalTypes = type.optionalParameterTypes;
+    var namedTypes = type.namedParameterTypes;
+    var rt = _emitTypeName(returnType);
+    var ra = _emitTypeNames(parameterTypes, parameters);
+    if (!namedTypes.isEmpty) {
+      assert(optionalTypes.isEmpty);
+      // TODO(vsm): Pass in annotations here as well.
+      var na = _emitTypeProperties(namedTypes);
+      return [rt, ra, na];
+    }
+    if (!optionalTypes.isEmpty) {
+      assert(namedTypes.isEmpty);
+      var oa = _emitTypeNames(
+          optionalTypes, parameters?.sublist(parameterTypes.length));
+      return [rt, ra, oa];
+    }
+    return [rt, ra];
+  }
+
+  JS.Expression _emitFunctionRTTI(FunctionType type) {
+    var parts = _emitFunctionTypeParts(type);
+    return js.call('dart.definiteFunctionType(#)', [parts]);
+  }
+
+  JS.Expression _maybeQualifiedName(Element e, [String name]) {
+    var libName = _libraryName(e.library);
+    var nameExpr = _propertyName(name ?? e.name);
+
+    // Always qualify:
+    // * mutable top-level fields
+    // * elements from other libraries
+    bool mutableTopLevel = e is TopLevelVariableElement &&
+        !e.isConst &&
+        !_isFinalJSDecl(e.computeNode());
+    bool fromAnotherLibrary = e.library != currentLibrary;
+    if (mutableTopLevel || fromAnotherLibrary) {
+      return new JS.PropertyAccess(libName, nameExpr);
+    }
+
+    var id = new JS.MaybeQualifiedId(libName, nameExpr);
+    _qualifiedIds.add(new Tuple2(e, id));
+    return id;
+  }
+
+  @override
+  JS.Expression visitAssignmentExpression(AssignmentExpression node) {
+    var left = node.leftHandSide;
+    var right = node.rightHandSide;
+    if (node.operator.type == TokenType.EQ) return _emitSet(left, right);
+    var op = node.operator.lexeme;
+    assert(op.endsWith('='));
+    op = op.substring(0, op.length - 1); // remove trailing '='
+    return _emitOpAssign(left, right, op, node.staticElement, context: node);
+  }
+
+  JS.MetaLet _emitOpAssign(
+      Expression left, Expression right, String op, MethodElement element,
+      {Expression context}) {
+    if (op == '??') {
+      // Desugar `l ??= r` as ((x) => x == null ? l = r : x)(l)
+      // Note that if `x` contains subexpressions, we need to ensure those
+      // are also evaluated only once. This is similar to desguaring for
+      // postfix expressions like `i++`.
+
+      // Handle the left hand side, to ensure each of its subexpressions are
+      // evaluated only once.
+      var vars = <String, JS.Expression>{};
+      var x = _bindLeftHandSide(vars, left, context: left);
+      // Capture the result of evaluating the left hand side in a temp.
+      var t = _bindValue(vars, 't', x, context: x);
+      return new JS.MetaLet(vars, [
+        js.call('# == null ? # : #', [_visit(t), _emitSet(x, right), _visit(t)])
+      ]);
+    }
+
+    // Desugar `x += y` as `x = x + y`, ensuring that if `x` has subexpressions
+    // (for example, x is IndexExpression) we evaluate those once.
+    var vars = <String, JS.Expression>{};
+    var lhs = _bindLeftHandSide(vars, left, context: context);
+    var inc = AstBuilder.binaryExpression(lhs, op, right);
+    inc.staticElement = element;
+    inc.staticType = getStaticType(left);
+    return new JS.MetaLet(vars, [_emitSet(lhs, inc)]);
+  }
+
+  JS.Expression _emitSet(Expression lhs, Expression rhs) {
+    if (lhs is IndexExpression) {
+      var target = _getTarget(lhs);
+      if (_useNativeJsIndexer(target.staticType)) {
+        return js.call(
+            '#[#] = #', [_visit(target), _visit(lhs.index), _visit(rhs)]);
+      }
+      return _emitSend(target, '[]=', [lhs.index, rhs]);
+    }
+
+    Expression target = null;
+    SimpleIdentifier id;
+    if (lhs is PropertyAccess) {
+      if (lhs.operator.lexeme == '?.') {
+        return _emitNullSafeSet(lhs, rhs);
+      }
+
+      target = _getTarget(lhs);
+      id = lhs.propertyName;
+    } else if (lhs is PrefixedIdentifier) {
+      target = lhs.prefix;
+      id = lhs.identifier;
+    }
+
+    if (target != null && DynamicInvoke.get(target)) {
+      return js.call('dart.$DPUT(#, #, #)', [
+        _visit(target),
+        _emitMemberName(id.name, type: getStaticType(target)),
+        _visit(rhs)
+      ]);
+    }
+
+    return _visit(rhs).toAssignExpression(_visit(lhs));
+  }
+
+  JS.Expression _emitNullSafeSet(PropertyAccess node, Expression right) {
+    // Emit `obj?.prop = expr` as:
+    //
+    //     (_ => _ == null ? null : _.prop = expr)(obj).
+    //
+    // We could use a helper, e.g.:  `nullSafeSet(e1, _ => _.v = e2)`
+    //
+    // However with MetaLet, we get clean code in statement or void context,
+    // or when one of the expressions is stateless, which seems common.
+    var vars = <String, JS.Expression>{};
+    var left = _bindValue(vars, 'l', node.target);
+    var body = js.call('# == null ? null : #',
+        [_visit(left), _emitSet(_stripNullAwareOp(node, left), right)]);
+    return new JS.MetaLet(vars, [body]);
+  }
+
+  @override
+  JS.Block visitExpressionFunctionBody(ExpressionFunctionBody node) {
+    var initArgs = _emitArgumentInitializers(node.parent);
+    var ret = new JS.Return(_visit(node.expression));
+    return new JS.Block(initArgs != null ? [initArgs, ret] : [ret]);
+  }
+
+  @override
+  JS.Block visitEmptyFunctionBody(EmptyFunctionBody node) => new JS.Block([]);
+
+  @override
+  JS.Block visitBlockFunctionBody(BlockFunctionBody node) {
+    var initArgs = _emitArgumentInitializers(node.parent);
+    var stmts = _visitList(node.block.statements) as List<JS.Statement>;
+    if (initArgs != null) stmts.insert(0, initArgs);
+    return new JS.Block(stmts);
+  }
+
+  @override
+  JS.Block visitBlock(Block node) =>
+      new JS.Block(_visitList(node.statements) as List<JS.Statement>,
+          isScope: true);
+
+  @override
+  JS.Expression visitMethodInvocation(MethodInvocation node) {
+    if (node.operator != null && node.operator.lexeme == '?.') {
+      return _emitNullSafe(node);
+    }
+
+    var target = _getTarget(node);
+    var result = _emitForeignJS(node);
+    if (result != null) return result;
+
+    String code;
+
+    var methodName = _visit(node.methodName);
+    var arguments = _visit(node.argumentList);
+    if (target == null || isLibraryPrefix(target)) {
+      return newDartMethodCall(null, methodName, arguments);
+      if (DynamicInvoke.get(node.methodName)) {
+        code = 'dart.$DCALL(#, #)';
+      } else {
+        code = '#(#)';
+      }
+      return js.call(
+          code, [_visit(node.methodName), _visit(node.argumentList)]);
+    }
+
+    var type = getStaticType(target);
+    var name = node.methodName.name;
+    var element = node.methodName.staticElement;
+    bool isStatic = element is ExecutableElement && element.isStatic;
+    var memberName = _emitMemberName(name, type: type, isStatic: isStatic);
+
+    if (DynamicInvoke.get(target)) {
+      code = 'dart.$DSEND(#, #, #)';
+    } else if (DynamicInvoke.get(node.methodName)) {
+      // This is a dynamic call to a statically known target. For example:
+      //     class Foo { Function bar; }
+      //     new Foo().bar(); // dynamic call
+      code = 'dart.$DCALL(#.#, #)';
+    } else if (_requiresStaticDispatch(target, name)) {
+      assert(rules.objectMembers[name] is FunctionType);
+      // Object methods require a helper for null checks.
+      return js.call('dart.#(#, #)',
+          [memberName, _visit(target), _visit(node.argumentList)]);
+    } else {
+      code = '#.#(#)';
+    }
+
+    return js.call(
+        code, [_visit(target), memberName, _visit(node.argumentList)]);
+  }
+
+  /// Emits code for the `JS(...)` builtin.
+  _emitForeignJS(MethodInvocation node) {
+    var e = node.methodName.staticElement;
+    if (isInlineJS(e)) {
+      var args = node.argumentList.arguments;
+      // arg[0] is static return type, used in `RestrictedStaticTypeAnalyzer`
+      var code = args[1];
+      var templateArgs;
+      var source;
+      if (code is StringInterpolation) {
+        if (args.length > 2) {
+          throw new ArgumentError(
+              "Can't mix template args and string interpolation in JS calls.");
+        }
+        templateArgs = <Expression>[];
+        source = code.elements.map((element) {
+          if (element is InterpolationExpression) {
+            templateArgs.add(element.expression);
+            return '#';
+          } else {
+            return (element as InterpolationString).value;
+          }
+        }).join();
+      } else {
+        templateArgs = args.skip(2);
+        source = (code as StringLiteral).stringValue;
+      }
+
+      var template = js.parseForeignJS(source);
+      var result = template.instantiate(_visitList(templateArgs));
+      // `throw` is emitted as a statement by `parseForeignJS`.
+      assert(result is JS.Expression || node.parent is ExpressionStatement);
+      return result;
+    }
+    return null;
+  }
+
+  @override
+  JS.Expression visitFunctionExpressionInvocation(
+      FunctionExpressionInvocation node) {
+    var code;
+    if (DynamicInvoke.get(node.function)) {
+      code = 'dart.$DCALL(#, #)';
+    } else {
+      code = '#(#)';
+    }
+    return js.call(code, [_visit(node.function), _visit(node.argumentList)]);
+  }
+
+  @override
+  List<JS.Expression> visitArgumentList(ArgumentList node) {
+    var args = <JS.Expression>[];
+    var named = <JS.Property>[];
+    for (var arg in node.arguments) {
+      if (arg is NamedExpression) {
+        named.add(_visit(arg));
+      } else if (arg is MethodInvocation && isJsSpreadInvocation(arg)) {
+        args.add(
+            new JS.RestParameter(_visit(arg.argumentList.arguments.single)));
+      } else {
+        args.add(_visit(arg));
+      }
+    }
+    if (named.isNotEmpty) {
+      args.add(new JS.ObjectInitializer(named));
+    }
+    return args;
+  }
+
+  @override
+  JS.Property visitNamedExpression(NamedExpression node) {
+    assert(node.parent is ArgumentList);
+    return new JS.Property(
+        _propertyName(node.name.label.name), _visit(node.expression));
+  }
+
+  bool _isNamedParam(FormalParameter param) =>
+      param.kind == ParameterKind.NAMED;
+
+  /// We cannot destructure named params that clash with JS reserved names:
+  /// see discussion in https://github.com/dart-lang/dev_compiler/issues/392.
+  bool _isDestructurableNamedParam(FormalParameter param) =>
+      _isNamedParam(param) &&
+          !invalidVariableName(param.identifier.name) &&
+          options.destructureNamedParams;
+
+  @override
+  List<JS.Parameter> visitFormalParameterList(FormalParameterList node) =>
+      _emitFormalParameterList(node);
+
+  List<JS.Parameter> _emitFormalParameterList(FormalParameterList node,
+      {bool allowDestructuring: true}) {
+    var result = <JS.Parameter>[];
+
+    var namedVars = <JS.DestructuredVariable>[];
+    var destructure = allowDestructuring &&
+        node.parameters.where(_isNamedParam).every(_isDestructurableNamedParam);
+    var hasNamedArgsConflictingWithObjectProperties = false;
+    var needsOpts = false;
+
+    for (FormalParameter param in node.parameters) {
+      if (param.kind == ParameterKind.NAMED) {
+        if (destructure) {
+          if (_jsObjectProperties.contains(param.identifier.name)) {
+            hasNamedArgsConflictingWithObjectProperties = true;
+          }
+          namedVars.add(new JS.DestructuredVariable(
+              name: _visit(param.identifier),
+              defaultValue: _defaultParamValue(param)));
+        } else {
+          needsOpts = true;
+        }
+      } else {
+        result.add(_visit(param));
+      }
+    }
+
+    if (needsOpts) {
+      result.add(_namedArgTemp);
+    } else if (namedVars.isNotEmpty) {
+      // Note: `var {valueOf} = {}` extracts `Object.prototype.valueOf`, so
+      // in case there are conflicting names we create an object without
+      // any prototype.
+      var defaultOpts = hasNamedArgsConflictingWithObjectProperties
+          ? js.call('Object.create(null)')
+          : js.call('{}');
+      result.add(new JS.DestructuredVariable(
+          structure: new JS.ObjectBindingPattern(namedVars),
+          defaultValue: defaultOpts));
+    }
+    return result;
+  }
+
+  /// See ES6 spec (and `Object.getOwnPropertyNames(Object.prototype)`):
+  /// http://www.ecma-international.org/ecma-262/6.0/#sec-properties-of-the-object-prototype-object
+  /// http://www.ecma-international.org/ecma-262/6.0/#sec-additional-properties-of-the-object.prototype-object
+  static final Set<String> _jsObjectProperties = new Set<String>()
+    ..addAll([
+      "constructor",
+      "toString",
+      "toLocaleString",
+      "valueOf",
+      "hasOwnProperty",
+      "isPrototypeOf",
+      "propertyIsEnumerable",
+      "__defineGetter__",
+      "__lookupGetter__",
+      "__defineSetter__",
+      "__lookupSetter__",
+      "__proto__"
+    ]);
+
+  @override
+  JS.Statement visitExpressionStatement(ExpressionStatement node) =>
+      _visit(node.expression).toStatement();
+
+  @override
+  JS.EmptyStatement visitEmptyStatement(EmptyStatement node) =>
+      new JS.EmptyStatement();
+
+  @override
+  JS.Statement visitAssertStatement(AssertStatement node) =>
+      // TODO(jmesserly): only emit in checked mode.
+      js.statement('dart.assert(#);', _visit(node.condition));
+
+  @override
+  JS.Statement visitReturnStatement(ReturnStatement node) {
+    var e = node.expression;
+    if (e == null) return new JS.Return();
+    return (_visit(e) as JS.Expression).toReturn();
+  }
+
+  @override
+  JS.Statement visitYieldStatement(YieldStatement node) {
+    JS.Expression jsExpr = _visit(node.expression);
+    var star = node.star != null;
+    if (_asyncStarController != null) {
+      // async* yields are generated differently from sync* yields. `yield e`
+      // becomes:
+      //
+      //     if (stream.add(e)) return;
+      //     yield;
+      //
+      // `yield* e` becomes:
+      //
+      //     if (stream.addStream(e)) return;
+      //     yield;
+      var helperName = star ? 'addStream' : 'add';
+      return js.statement('{ if(#.#(#)) return; #; }',
+          [_asyncStarController, helperName, jsExpr, new JS.Yield(null)]);
+    }
+    // A normal yield in a sync*
+    return jsExpr.toYieldStatement(star: star);
+  }
+
+  @override
+  JS.Expression visitAwaitExpression(AwaitExpression node) {
+    return new JS.Yield(_visit(node.expression));
+  }
+
+  /// Emits static fields.
+  ///
+  /// Instance fields are emitted in [_initializeFields].
+  ///
+  /// These are generally treated the same as top-level fields, see
+  /// [visitTopLevelVariableDeclaration].
+  @override
+  visitFieldDeclaration(FieldDeclaration node) {
+    if (!node.isStatic) return;
+
+    for (var f in node.fields.variables) {
+      _loader.loadDeclaration(f, f.element);
+    }
+  }
+
+  _addExport(String name) {
+    if (!_exports.add(name)) throw 'Duplicate top level name found: $name';
+  }
+
+  @override
+  JS.Statement visitVariableDeclarationStatement(
+      VariableDeclarationStatement node) {
+    // Special case a single variable with an initializer.
+    // This helps emit cleaner code for things like:
+    //     var result = []..add(1)..add(2);
+    if (node.variables.variables.length == 1) {
+      var v = node.variables.variables.single;
+      if (v.initializer != null) {
+        var name = new JS.Identifier(v.name.name);
+        return _visit(v.initializer).toVariableDeclaration(name);
+      }
+    }
+    return _visit(node.variables).toStatement();
+  }
+
+  @override
+  visitVariableDeclarationList(VariableDeclarationList node) {
+    return new JS.VariableDeclarationList(
+        'let', _visitList(node.variables) as List<JS.VariableInitialization>);
+  }
+
+  @override
+  visitVariableDeclaration(VariableDeclaration node) {
+    if (node.element is PropertyInducingElement) return _emitStaticField(node);
+
+    var name = new JS.Identifier(node.name.name);
+    return new JS.VariableInitialization(name, _visitInitializer(node));
+  }
+
+  bool _isFinalJSDecl(AstNode field) => field is VariableDeclaration &&
+      field.isFinal &&
+      _isJSInvocation(field.initializer);
+
+  /// Emits a static or top-level field.
+  JS.Statement _emitStaticField(VariableDeclaration field) {
+    PropertyInducingElement element = field.element;
+    assert(element.isStatic);
+
+    bool eagerInit;
+    JS.Expression jsInit;
+    if (field.isConst || _constField.isFieldInitConstant(field)) {
+      // If the field is constant, try and generate it at the top level.
+      _loader.startTopLevel(element);
+      jsInit = _visitInitializer(field);
+      _loader.finishTopLevel(element);
+      eagerInit = _loader.isLoaded(element);
+    } else {
+      jsInit = _visitInitializer(field);
+      eagerInit = false;
+    }
+
+    // Treat `final x = JS('', '...')` as a const (non-lazy) to help compile
+    // runtime helpers.
+    var isJSTopLevel = field.isFinal && _isFinalJSDecl(field);
+    if (isJSTopLevel) eagerInit = true;
+
+    var fieldName = field.name.name;
+    if ((field.isConst && eagerInit && element is TopLevelVariableElement) ||
+        isJSTopLevel) {
+      // constant fields don't change, so we can generate them as `let`
+      // but add them to the module's exports. However, make sure we generate
+      // anything they depend on first.
+
+      if (isPublic(fieldName)) _addExport(fieldName);
+      var declKeyword = field.isConst || field.isFinal ? 'const' : 'let';
+      return annotateVariable(
+          js.statement(
+              '$declKeyword # = #;', [new JS.Identifier(fieldName), jsInit]),
+          field.element);
+    }
+
+    if (eagerInit && !JS.invalidStaticFieldName(fieldName)) {
+      return annotateVariable(
+          js.statement('# = #;', [_visit(field.name), jsInit]), field.element);
+    }
+
+    var body = <JS.Statement>[];
+    if (_lazyFields.isNotEmpty) {
+      var existingTarget = _lazyFields[0].element.enclosingElement;
+      if (existingTarget != element.enclosingElement) {
+        _flushLazyFields(body);
+      }
+    }
+
+    _lazyFields.add(field);
+
+    return _statement(body);
+  }
+
+  JS.Expression _visitInitializer(VariableDeclaration node) {
+    var value = _visit(node.initializer);
+    // explicitly initialize to null, to avoid getting `undefined`.
+    // TODO(jmesserly): do this only for vars that aren't definitely assigned.
+    return value ?? new JS.LiteralNull();
+  }
+
+  void _flushLazyFields(List<JS.Statement> body) {
+    if (_lazyFields.isEmpty) return;
+    body.add(_emitLazyFields(_lazyFields));
+    _lazyFields.clear();
+  }
+
+  JS.Statement _emitLazyFields(List<VariableDeclaration> fields) {
+    var methods = [];
+    for (var node in fields) {
+      var name = node.name.name;
+      var element = node.element;
+      var access = _emitMemberName(name, type: element.type, isStatic: true);
+      methods.add(annotate(
+          new JS.Method(
+              access,
+              js.call('function() { return #; }', _visit(node.initializer))
+              as JS.Fun,
+              isGetter: true),
+          _findAccessor(element, getter: true)));
+
+      // TODO(jmesserly): use a dummy setter to indicate writable.
+      if (!node.isFinal) {
+        methods.add(annotate(
+            new JS.Method(access, js.call('function(_) {}') as JS.Fun,
+                isSetter: true),
+            _findAccessor(element, getter: false)));
+      }
+    }
+
+    JS.Expression objExpr = _exportsVar;
+    var target = _lazyFields[0].element.enclosingElement;
+    if (target is ClassElement) {
+      objExpr = new JS.Identifier(target.type.name);
+    }
+
+    return js.statement(
+        'dart.defineLazyProperties(#, { # });', [objExpr, methods]);
+  }
+
+  PropertyAccessorElement _findAccessor(VariableElement element,
+      {bool getter}) {
+    var parent = element.enclosingElement;
+    if (parent is ClassElement) {
+      return getter
+          ? parent.getGetter(element.name)
+          : parent.getSetter(element.name);
+    }
+    return null;
+  }
+
+  void _flushLibraryProperties(List<JS.Statement> body) {
+    if (_properties.isEmpty) return;
+    body.add(js.statement('dart.copyProperties(#, { # });',
+        [_exportsVar, _properties.map(_emitTopLevelProperty)]));
+    _properties.clear();
+  }
+
+  JS.Expression _emitConstructorName(
+      ConstructorElement element, DartType type, SimpleIdentifier name) {
+    var typeName = _emitTypeName(type);
+    if (name != null || element.isFactory) {
+      var namedCtor = _constructorName(element);
+      return new JS.PropertyAccess(typeName, namedCtor);
+    }
+    return typeName;
+  }
+
+  @override
+  visitConstructorName(ConstructorName node) {
+    return _emitConstructorName(node.staticElement, node.type.type, node.name);
+  }
+
+  JS.Expression _emitInstanceCreationExpression(
+      ConstructorElement element,
+      DartType type,
+      SimpleIdentifier name,
+      ArgumentList argumentList,
+      bool isConst) {
+    JS.Expression emitNew() {
+      JS.Expression ctor;
+      bool isFactory = false;
+      // var element = node.staticElement;
+      if (element == null) {
+        // TODO(jmesserly): this only happens if we had a static error.
+        // Should we generate a throw instead?
+        ctor = _emitTypeName(type);
+        if (name != null) {
+          ctor = new JS.PropertyAccess(ctor, _propertyName(name.name));
+        }
+      } else {
+        ctor = _emitConstructorName(element, type, name);
+        isFactory = element.isFactory;
+      }
+      var args = _visit(argumentList) as List<JS.Expression>;
+      return isFactory ? new JS.Call(ctor, args) : new JS.New(ctor, args);
+    }
+    if (isConst) return _emitConst(emitNew);
+    return emitNew();
+  }
+
+  @override
+  visitInstanceCreationExpression(InstanceCreationExpression node) {
+    var element = node.staticElement;
+    var constructor = node.constructorName;
+    var name = constructor.name;
+    var type = constructor.type.type;
+    return _emitInstanceCreationExpression(
+        element, type, name, node.argumentList, node.isConst);
+  }
+
+  /// True if this type is built-in to JS, and we use the values unwrapped.
+  /// For these types we generate a calling convention via static
+  /// "extension methods". This allows types to be extended without adding
+  /// extensions directly on the prototype.
+  bool _isJSBuiltinType(DartType t) =>
+      typeIsPrimitiveInJS(t) || t == _types.stringType;
+
+  bool typeIsPrimitiveInJS(DartType t) =>
+      _isNumberInJS(t) || t == _types.boolType;
+
+  bool binaryOperationIsPrimitive(DartType leftT, DartType rightT) =>
+      typeIsPrimitiveInJS(leftT) && typeIsPrimitiveInJS(rightT);
+
+  bool unaryOperationIsPrimitive(DartType t) => typeIsPrimitiveInJS(t);
+
+  bool _isNonNullableExpression(Expression expr) {
+    // TODO(vsm): Revisit whether we really need this when we get
+    // better non-nullability in the type system.
+    // TODO(jmesserly): we do recursive calls in a few places. This could
+    // leads to O(depth) cost for calling this function. We could store the
+    // resulting value if that becomes an issue, so we maintain the invariant
+    // that each node is visited once.
+
+    if (expr is Literal && expr is! NullLiteral) return true;
+    if (expr is IsExpression) return true;
+    if (expr is ThisExpression) return true;
+    if (expr is SuperExpression) return true;
+    if (expr is ParenthesizedExpression) {
+      return _isNonNullableExpression(expr.expression);
+    }
+    if (expr is SimpleIdentifier) {
+      // Type literals are not null.
+      Element e = expr.staticElement;
+      if (e is ClassElement || e is FunctionTypeAliasElement) return true;
+    }
+    DartType type = null;
+    if (expr is BinaryExpression) {
+      switch (expr.operator.type) {
+        case TokenType.EQ_EQ:
+        case TokenType.BANG_EQ:
+        case TokenType.AMPERSAND_AMPERSAND:
+        case TokenType.BAR_BAR:
+          return true;
+        case TokenType.QUESTION_QUESTION:
+          return _isNonNullableExpression(expr.rightOperand);
+      }
+      type = getStaticType(expr.leftOperand);
+    } else if (expr is PrefixExpression) {
+      if (expr.operator.type == TokenType.BANG) return true;
+      type = getStaticType(expr.operand);
+    } else if (expr is PostfixExpression) {
+      type = getStaticType(expr.operand);
+    }
+    if (type != null && _isJSBuiltinType(type)) {
+      return true;
+    }
+    if (expr is MethodInvocation) {
+      // TODO(vsm): This logic overlaps with the resolver.
+      // Where is the best place to put this?
+      var e = expr.methodName.staticElement;
+      if (isInlineJS(e)) {
+        // Fix types for JS builtin calls.
+        //
+        // This code was taken from analyzer. It's not super sophisticated:
+        // only looks for the type name in dart:core, so we just copy it here.
+        //
+        // TODO(jmesserly): we'll likely need something that can handle a wider
+        // variety of types, especially when we get to JS interop.
+        var args = expr.argumentList.arguments;
+        var first = args.isNotEmpty ? args.first : null;
+        if (first is SimpleStringLiteral) {
+          var types = first.stringValue;
+          if (!types.split('|').contains('Null')) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  JS.Expression notNull(Expression expr) {
+    if (expr == null) return null;
+    var jsExpr = _visit(expr);
+    if (_isNonNullableExpression(expr)) return jsExpr;
+    return js.call('dart.notNull(#)', jsExpr);
+  }
+
+  @override
+  JS.Expression visitBinaryExpression(BinaryExpression node) {
+    var op = node.operator;
+    var left = node.leftOperand;
+    var right = node.rightOperand;
+
+    var leftType = getStaticType(left);
+    var rightType = getStaticType(right);
+
+    var code;
+    if (op.type.isEqualityOperator) {
+      // If we statically know LHS or RHS is null we can generate a clean check.
+      // We can also do this if both sides are the same primitive type.
+      if (_canUsePrimitiveEquality(left, right)) {
+        code = op.type == TokenType.EQ_EQ ? '# == #' : '# != #';
+      } else if (left is SuperExpression) {
+        return _emitSend(left, op.lexeme, [right]);
+      } else {
+        var bang = op.type == TokenType.BANG_EQ ? '!' : '';
+        code = '${bang}dart.equals(#, #)';
+      }
+      return js.call(code, [_visit(left), _visit(right)]);
+    }
+
+    if (op.type.lexeme == '??') {
+      // TODO(jmesserly): leave RHS for debugging?
+      // This should be a hint or warning for dead code.
+      if (_isNonNullableExpression(left)) return _visit(left);
+
+      var vars = <String, JS.Expression>{};
+      // Desugar `l ?? r` as `l != null ? l : r`
+      var l = _visit(_bindValue(vars, 'l', left, context: left));
+      return new JS.MetaLet(vars, [
+        js.call('# != null ? # : #', [l, l, _visit(right)])
+      ]);
+    }
+
+    if (binaryOperationIsPrimitive(leftType, rightType) ||
+        leftType == _types.stringType && op.type == TokenType.PLUS) {
+      // special cases where we inline the operation
+      // these values are assumed to be non-null (determined by the checker)
+      // TODO(jmesserly): it would be nice to just inline the method from core,
+      // instead of special cases here.
+      if (op.type == TokenType.TILDE_SLASH) {
+        // `a ~/ b` is equivalent to `(a / b).truncate()`
+        var div = AstBuilder.binaryExpression(left, '/', right)
+          ..staticType = node.staticType;
+        return _emitSend(div, 'truncate', []);
+      } else {
+        // TODO(vsm): When do Dart ops not map to JS?
+        code = '# $op #';
+      }
+      return js.call(code, [notNull(left), notNull(right)]);
+    }
+
+    return _emitSend(left, op.lexeme, [right]);
+  }
+
+  /// If the type [t] is [int] or [double], returns [num].
+  /// Otherwise returns [t].
+  DartType _canonicalizeNumTypes(DartType t) {
+    var numType = types.numType;
+    if (t is InterfaceType && t.superclass == numType) return numType;
+    return t;
+  }
+
+  bool _canUsePrimitiveEquality(Expression left, Expression right) {
+    if (_isNull(left) || _isNull(right)) return true;
+
+    var leftType = _canonicalizeNumTypes(getStaticType(left));
+    var rightType = _canonicalizeNumTypes(getStaticType(right));
+    return _isJSBuiltinType(leftType) && leftType == rightType;
+  }
+
+  bool _isNull(Expression expr) => expr is NullLiteral;
+
+  SimpleIdentifier _createTemporary(String name, DartType type) {
+    // We use an invalid source location to signal that this is a temporary.
+    // See [_isTemporary].
+    // TODO(jmesserly): alternatives are
+    // * (ab)use Element.isSynthetic, which isn't currently used for
+    //   LocalVariableElementImpl, so we could repurpose to mean "temp".
+    // * add a new property to LocalVariableElementImpl.
+    // * create a new subtype of LocalVariableElementImpl to mark a temp.
+    var id =
+        new SimpleIdentifier(new StringToken(TokenType.IDENTIFIER, name, -1));
+    id.staticElement = new TemporaryVariableElement.forNode(id);
+    id.staticType = type;
+    DynamicInvoke.set(id, type.isDynamic);
+    return id;
+  }
+
+  JS.Expression _emitConst(JS.Expression expr()) {
+    // TODO(jmesserly): emit the constants at top level if possible.
+    // This wasn't quite working, so disabled for now.
+    return js.call('dart.const(#)', expr());
+  }
+
+  /// Returns a new expression, which can be be used safely *once* on the
+  /// left hand side, and *once* on the right side of an assignment.
+  /// For example: `expr1[expr2] += y` can be compiled as
+  /// `expr1[expr2] = expr1[expr2] + y`.
+  ///
+  /// The temporary scope will ensure `expr1` and `expr2` are only evaluated
+  /// once: `((x1, x2) => x1[x2] = x1[x2] + y)(expr1, expr2)`.
+  ///
+  /// If the expression does not end up using `x1` or `x2` more than once, or
+  /// if those expressions can be treated as stateless (e.g. they are
+  /// non-mutated variables), then the resulting code will be simplified
+  /// automatically.
+  ///
+  /// [scope] can be mutated to contain any new temporaries that were created,
+  /// unless [expr] is a SimpleIdentifier, in which case a temporary is not
+  /// needed.
+  Expression _bindLeftHandSide(
+      Map<String, JS.Expression> scope, Expression expr,
+      {Expression context}) {
+    Expression result;
+    if (expr is IndexExpression) {
+      IndexExpression index = expr;
+      result = new IndexExpression.forTarget(
+          _bindValue(scope, 'o', index.target, context: context),
+          index.leftBracket,
+          _bindValue(scope, 'i', index.index, context: context),
+          index.rightBracket);
+    } else if (expr is PropertyAccess) {
+      PropertyAccess prop = expr;
+      result = new PropertyAccess(
+          _bindValue(scope, 'o', _getTarget(prop), context: context),
+          prop.operator,
+          prop.propertyName);
+    } else if (expr is PrefixedIdentifier) {
+      PrefixedIdentifier ident = expr;
+      if (isLibraryPrefix(ident.prefix)) {
+        return expr;
+      }
+      result = new PrefixedIdentifier(
+          _bindValue(scope, 'o', ident.prefix, context: context)
+          as SimpleIdentifier,
+          ident.period,
+          ident.identifier);
+    } else {
+      return expr as SimpleIdentifier;
+    }
+    result.staticType = expr.staticType;
+    DynamicInvoke.set(result, DynamicInvoke.get(expr));
+    return result;
+  }
+
+  /// Creates a temporary to contain the value of [expr]. The temporary can be
+  /// used multiple times in the resulting expression. For example:
+  /// `expr ** 2` could be compiled as `expr * expr`. The temporary scope will
+  /// ensure `expr` is only evaluated once: `(x => x * x)(expr)`.
+  ///
+  /// If the expression does not end up using `x` more than once, or if those
+  /// expressions can be treated as stateless (e.g. they are non-mutated
+  /// variables), then the resulting code will be simplified automatically.
+  ///
+  /// [scope] will be mutated to contain the new temporary's initialization.
+  Expression _bindValue(
+      Map<String, JS.Expression> scope, String name, Expression expr,
+      {Expression context}) {
+    // No need to do anything for stateless expressions.
+    if (isStateless(expr, context)) return expr;
+
+    var t = _createTemporary('#$name', getStaticType(expr));
+    scope[name] = _visit(expr);
+    return t;
+  }
+
+  /// Desugars postfix increment.
+  ///
+  /// In the general case [expr] can be one of [IndexExpression],
+  /// [PrefixExpression] or [PropertyAccess] and we need to
+  /// ensure sub-expressions are evaluated once.
+  ///
+  /// We also need to ensure we can return the original value of the expression,
+  /// and that it is only evaluated once.
+  ///
+  /// We desugar this using let*.
+  ///
+  /// For example, `expr1[expr2]++` can be transformed to this:
+  ///
+  ///     // psuedocode mix of Scheme and JS:
+  ///     (let* (x1=expr1, x2=expr2, t=expr1[expr2]) { x1[x2] = t + 1; t })
+  ///
+  /// The [JS.JS.MetaLet] nodes automatically simplify themselves if they can.
+  /// For example, if the result value is not used, then `t` goes away.
+  @override
+  JS.Expression visitPostfixExpression(PostfixExpression node) {
+    var op = node.operator;
+    var expr = node.operand;
+
+    var dispatchType = getStaticType(expr);
+    if (unaryOperationIsPrimitive(dispatchType)) {
+      if (_isNonNullableExpression(expr)) {
+        return js.call('#$op', _visit(expr));
+      }
+    }
+
+    assert(op.lexeme == '++' || op.lexeme == '--');
+
+    // Handle the left hand side, to ensure each of its subexpressions are
+    // evaluated only once.
+    var vars = <String, JS.Expression>{};
+    var left = _bindLeftHandSide(vars, expr, context: expr);
+
+    // Desugar `x++` as `(x1 = x0 + 1, x0)` where `x0` is the original value
+    // and `x1` is the new value for `x`.
+    var x = _bindValue(vars, 'x', left, context: expr);
+
+    var one = AstBuilder.integerLiteral(1)..staticType = types.intType;
+    var increment = AstBuilder.binaryExpression(x, op.lexeme[0], one)
+      ..staticElement = node.staticElement
+      ..staticType = getStaticType(expr);
+
+    var body = <JS.Expression>[_emitSet(left, increment), _visit(x)];
+    return new JS.MetaLet(vars, body, statelessResult: true);
+  }
+
+  @override
+  JS.Expression visitPrefixExpression(PrefixExpression node) {
+    var op = node.operator;
+    var expr = node.operand;
+
+    var dispatchType = getStaticType(expr);
+    if (unaryOperationIsPrimitive(dispatchType)) {
+      if (_isNonNullableExpression(expr)) {
+        return js.call('$op#', _visit(expr));
+      } else if (op.lexeme == '++' || op.lexeme == '--') {
+        // We need a null check, so the increment must be expanded out.
+        var vars = <String, JS.Expression>{};
+        var x = _bindLeftHandSide(vars, expr, context: expr);
+
+        var one = AstBuilder.integerLiteral(1)..staticType = types.intType;
+        var increment = AstBuilder.binaryExpression(x, op.lexeme[0], one)
+          ..staticElement = node.staticElement
+          ..staticType = getStaticType(expr);
+
+        return new JS.MetaLet(vars, [_emitSet(x, increment)]);
+      } else {
+        return js.call('$op#', notNull(expr));
+      }
+    }
+
+    if (op.lexeme == '++' || op.lexeme == '--') {
+      // Increment or decrement requires expansion.
+      // Desugar `++x` as `x = x + 1`, ensuring that if `x` has subexpressions
+      // (for example, x is IndexExpression) we evaluate those once.
+      var one = AstBuilder.integerLiteral(1)..staticType = types.intType;
+      return _emitOpAssign(expr, one, op.lexeme[0], node.staticElement,
+          context: expr);
+    }
+
+    return _emitSend(expr, op.lexeme[0], []);
+  }
+
+  // Cascades can contain [IndexExpression], [MethodInvocation] and
+  // [PropertyAccess]. The code generation for those is handled in their
+  // respective visit methods.
+  @override
+  JS.Node visitCascadeExpression(CascadeExpression node) {
+    var savedCascadeTemp = _cascadeTarget;
+
+    var vars = <String, JS.Expression>{};
+    _cascadeTarget = _bindValue(vars, '_', node.target, context: node);
+    var sections = _visitList(node.cascadeSections) as List<JS.Expression>;
+    sections.add(_visit(_cascadeTarget));
+    var result = new JS.MetaLet(vars, sections, statelessResult: true);
+    _cascadeTarget = savedCascadeTemp;
+    return result;
+  }
+
+  @override
+  visitParenthesizedExpression(ParenthesizedExpression node) =>
+      // The printer handles precedence so we don't need to.
+      _visit(node.expression);
+
+  @override
+  visitFormalParameter(FormalParameter node) {
+    var id = visitSimpleIdentifier(node.identifier);
+
+    var isRestArg = findAnnotation(node.element, isJsRestAnnotation) != null;
+    return isRestArg ? new JS.RestParameter(id) : id;
+  }
+
+  @override
+  JS.This visitThisExpression(ThisExpression node) => new JS.This();
+
+  @override
+  JS.Super visitSuperExpression(SuperExpression node) => new JS.Super();
+
+  @override
+  visitPrefixedIdentifier(PrefixedIdentifier node) {
+    if (isLibraryPrefix(node.prefix)) {
+      return _visit(node.identifier);
+    } else {
+      return _emitGet(node.prefix, node.identifier);
+    }
+  }
+
+  @override
+  visitPropertyAccess(PropertyAccess node) {
+    if (node.operator.lexeme == '?.') {
+      return _emitNullSafe(node);
+    }
+    return _emitGet(_getTarget(node), node.propertyName);
+  }
+
+  JS.Expression _emitNullSafe(Expression node) {
+    // Desugar ?. sequence by passing a sequence of callbacks that applies
+    // each operation in sequence:
+    //
+    //     obj?.foo()?.bar
+    // -->
+    //     nullSafe(obj, _ => _.foo(), _ => _.bar);
+    //
+    // This pattern has the benefit of preserving order, as well as minimizing
+    // code expansion: each `?.` becomes `, _ => _`, plus one helper call.
+    //
+    // TODO(jmesserly): we could desugar with MetaLet instead, which may
+    // lead to higher performing code, but at the cost of readability.
+    var tail = <JS.Expression>[];
+    for (;;) {
+      var op = _getOperator(node);
+      if (op != null && op.lexeme == '?.') {
+        var nodeTarget = _getTarget(node);
+        if (_isNonNullableExpression(nodeTarget)) {
+          node = _stripNullAwareOp(node, nodeTarget);
+          break;
+        }
+
+        var param = _createTemporary('_', nodeTarget.staticType);
+        var baseNode = _stripNullAwareOp(node, param);
+        tail.add(new JS.ArrowFun([_visit(param)], _visit(baseNode)));
+        node = nodeTarget;
+      } else {
+        break;
+      }
+    }
+    if (tail.isEmpty) return _visit(node);
+    return js.call('dart.nullSafe(#, #)', [_visit(node), tail.reversed]);
+  }
+
+  static Token _getOperator(Expression node) {
+    if (node is PropertyAccess) return node.operator;
+    if (node is MethodInvocation) return node.operator;
+    return null;
+  }
+
+  // TODO(jmesserly): this is dropping source location.
+  Expression _stripNullAwareOp(Expression node, Expression newTarget) {
+    if (node is PropertyAccess) {
+      return AstBuilder.propertyAccess(newTarget, node.propertyName);
+    } else {
+      var invoke = node as MethodInvocation;
+      return AstBuilder.methodInvoke(
+          newTarget, invoke.methodName, invoke.argumentList.arguments);
+    }
+  }
+
+  bool _requiresStaticDispatch(Expression target, String memberName) {
+    var type = getStaticType(target);
+    if (!rules.objectMembers.containsKey(memberName)) {
+      return false;
+    }
+    if (!type.isObject &&
+        !_isJSBuiltinType(type) &&
+        _isNonNullableExpression(target)) {
+      return false;
+    }
+    return true;
+  }
+
+  /// Shared code for [PrefixedIdentifier] and [PropertyAccess].
+  JS.Expression _emitGet(Expression target, SimpleIdentifier memberId) {
+    var member = memberId.staticElement;
+    if (member is PropertyAccessorElement) {
+      member = (member as PropertyAccessorElement).variable;
+    }
+    bool isStatic = member is ClassMemberElement && member.isStatic;
+    if (isStatic) {
+      _loader.declareBeforeUse(member);
+    }
+    var name = _emitMemberName(memberId.name,
+        type: getStaticType(target), isStatic: isStatic);
+    if (DynamicInvoke.get(target)) {
+      return js.call('dart.$DLOAD(#, #)', [_visit(target), name]);
+    }
+
+    String code;
+    if (member != null && member is MethodElement && !isStatic) {
+      // Tear-off methods: explicitly bind it.
+      if (target is SuperExpression) {
+        return js.call('dart.bind(this, #, #.#)', [name, _visit(target), name]);
+      } else if (_requiresStaticDispatch(target, memberId.name)) {
+        var type = member.type;
+        var clos = js.call('dart.#.bind(#)', [name, _visit(target)]);
+        return js.call('dart.fn(#, #)', [clos, _emitFunctionTypeParts(type)]);
+      }
+      code = 'dart.bind(#, #)';
+    } else if (_requiresStaticDispatch(target, memberId.name)) {
+      return js.call('dart.#(#)', [name, _visit(target)]);
+    } else {
+      code = '#.#';
+    }
+
+    return js.call(code, [_visit(target), name]);
+  }
+
+  /// Emits a generic send, like an operator method.
+  ///
+  /// **Please note** this function does not support method invocation syntax
+  /// `obj.name(args)` because that could be a getter followed by a call.
+  /// See [visitMethodInvocation].
+  JS.Expression _emitSend(
+      Expression target, String name, List<Expression> args) {
+    var type = getStaticType(target);
+    var memberName = _emitMemberName(name, unary: args.isEmpty, type: type);
+    if (DynamicInvoke.get(target)) {
+      // dynamic dispatch
+      var dynamicHelper = const {'[]': DINDEX, '[]=': DSETINDEX}[name];
+      if (dynamicHelper != null) {
+        return js.call(
+            'dart.$dynamicHelper(#, #)', [_visit(target), _visitList(args)]);
+      }
+      return js.call('dart.$DSEND(#, #, #)',
+          [_visit(target), memberName, _visitList(args)]);
+    }
+
+    // Generic dispatch to a statically known method.
+    return js.call('#.#(#)', [_visit(target), memberName, _visitList(args)]);
+  }
+
+  @override
+  visitIndexExpression(IndexExpression node) {
+    var target = _getTarget(node);
+    if (_useNativeJsIndexer(target.staticType)) {
+      return new JS.PropertyAccess(_visit(target), _visit(node.index));
+    }
+    return _emitSend(target, '[]', [node.index]);
+  }
+
+  // TODO(jmesserly): ideally we'd check the method and see if it is marked
+  // `external`, but that doesn't work because it isn't in the element model.
+  bool _useNativeJsIndexer(DartType type) =>
+      findAnnotation(type.element, isJSAnnotation) != null;
+
+  /// Gets the target of a [PropertyAccess], [IndexExpression], or
+  /// [MethodInvocation]. These three nodes can appear in a [CascadeExpression].
+  Expression _getTarget(node) {
+    assert(node is IndexExpression ||
+        node is PropertyAccess ||
+        node is MethodInvocation);
+    return node.isCascaded ? _cascadeTarget : node.target;
+  }
+
+  @override
+  visitConditionalExpression(ConditionalExpression node) {
+    return js.call('# ? # : #', [
+      notNull(node.condition),
+      _visit(node.thenExpression),
+      _visit(node.elseExpression)
+    ]);
+  }
+
+  @override
+  visitThrowExpression(ThrowExpression node) {
+    var expr = _visit(node.expression);
+    if (node.parent is ExpressionStatement) {
+      return js.statement('dart.throw(#);', expr);
+    } else {
+      return js.call('dart.throw(#)', expr);
+    }
+  }
+
+  @override
+  visitRethrowExpression(RethrowExpression node) {
+    if (node.parent is ExpressionStatement) {
+      return js.statement('throw #;', _visit(_catchParameter));
+    } else {
+      return js.call('throw #', _visit(_catchParameter));
+    }
+  }
+
+  /// Visits a statement, and ensures the resulting AST handles block scope
+  /// correctly. Essentially, we need to promote a variable declaration
+  /// statement into a block in some cases, e.g.
+  ///
+  ///     do var x = 5; while (false); // Dart
+  ///     do { let x = 5; } while (false); // JS
+  JS.Statement _visitScope(Statement stmt) {
+    var result = _visit(stmt);
+    if (result is JS.ExpressionStatement &&
+        result.expression is JS.VariableDeclarationList) {
+      return new JS.Block([result]);
+    }
+    return result;
+  }
+
+  @override
+  JS.If visitIfStatement(IfStatement node) {
+    return new JS.If(notNull(node.condition), _visitScope(node.thenStatement),
+        _visitScope(node.elseStatement));
+  }
+
+  @override
+  JS.For visitForStatement(ForStatement node) {
+    var init = _visit(node.initialization);
+    if (init == null) init = _visit(node.variables);
+    var update = _visitListToBinary(node.updaters, ',');
+    if (update != null) update = update.toVoidExpression();
+    return new JS.For(
+        init, notNull(node.condition), update, _visitScope(node.body));
+  }
+
+  @override
+  JS.While visitWhileStatement(WhileStatement node) {
+    return new JS.While(notNull(node.condition), _visitScope(node.body));
+  }
+
+  @override
+  JS.Do visitDoStatement(DoStatement node) {
+    return new JS.Do(_visitScope(node.body), notNull(node.condition));
+  }
+
+  @override
+  JS.Statement visitForEachStatement(ForEachStatement node) {
+    if (node.awaitKeyword != null) {
+      return _emitAwaitFor(node);
+    }
+
+    var init = _visit(node.identifier);
+    if (init == null) {
+      init = js.call('let #', node.loopVariable.identifier.name);
+    }
+    return new JS.ForOf(init, _visit(node.iterable), _visitScope(node.body));
+  }
+
+  JS.Statement _emitAwaitFor(ForEachStatement node) {
+    // Emits `await for (var value in stream) ...`, which desugars as:
+    //
+    // var iter = new StreamIterator<T>(stream);
+    // try {
+    //   while (await iter.moveNext()) {
+    //     var value = iter.current;
+    //     ...
+    //   }
+    // } finally {
+    //   await iter.cancel();
+    // }
+    //
+    // Like the Dart VM, we call cancel() always, as it's safe to call if the
+    // stream has already been cancelled.
+    //
+    // TODO(jmesserly): we may want a helper if these become common. For now the
+    // full desugaring seems okay.
+    var context = compiler.context;
+    var dart_async = context
+        .computeLibraryElement(context.sourceFactory.forUri('dart:async'));
+    var T = node.loopVariable.element.type;
+    var StreamIterator_T =
+        dart_async.getType('StreamIterator').type.substitute4([T]);
+
+    var createStreamIter = _emitInstanceCreationExpression(
+        StreamIterator_T.element.unnamedConstructor,
+        StreamIterator_T,
+        null,
+        AstBuilder.argumentList([node.iterable]),
+        false);
+    var iter = _visit(_createTemporary('it', StreamIterator_T));
+
+    var init = _visit(node.identifier);
+    if (init == null) {
+      init = js.call(
+          'let # = #.current', [node.loopVariable.identifier.name, iter]);
+    } else {
+      init = js.call('# = #.current', [init, iter]);
+    }
+    return js.statement(
+        '{'
+        '  let # = #;'
+        '  try {'
+        '    while (#) { #; #; }'
+        '  } finally { #; }'
+        '}',
+        [
+      iter,
+      createStreamIter,
+      new JS.Yield(js.call('#.moveNext()', iter)),
+      init,
+      _visit(node.body),
+      new JS.Yield(js.call('#.cancel()', iter))
+    ]);
+  }
+
+  @override
+  visitBreakStatement(BreakStatement node) {
+    var label = node.label;
+    return new JS.Break(label?.name);
+  }
+
+  @override
+  visitContinueStatement(ContinueStatement node) {
+    var label = node.label;
+    return new JS.Continue(label?.name);
+  }
+
+  @override
+  visitTryStatement(TryStatement node) {
+    return new JS.Try(_visit(node.body), _visitCatch(node.catchClauses),
+        _visit(node.finallyBlock));
+  }
+
+  _visitCatch(NodeList<CatchClause> clauses) {
+    if (clauses == null || clauses.isEmpty) return null;
+
+    // TODO(jmesserly): need a better way to get a temporary variable.
+    // This could incorrectly shadow a user's name.
+    var savedCatch = _catchParameter;
+
+    if (clauses.length == 1 && clauses.single.exceptionParameter != null) {
+      // Special case for a single catch.
+      _catchParameter = clauses.single.exceptionParameter;
+    } else {
+      _catchParameter = _createTemporary('e', types.dynamicType);
+    }
+
+    JS.Statement catchBody = js.statement('throw #;', _visit(_catchParameter));
+    for (var clause in clauses.reversed) {
+      catchBody = _catchClauseGuard(clause, catchBody);
+    }
+
+    var catchVarDecl = _visit(_catchParameter);
+    _catchParameter = savedCatch;
+    return new JS.Catch(catchVarDecl, new JS.Block([catchBody]));
+  }
+
+  JS.Statement _catchClauseGuard(CatchClause clause, JS.Statement otherwise) {
+    var then = visitCatchClause(clause);
+
+    // Discard following clauses, if any, as they are unreachable.
+    if (clause.exceptionType == null) return then;
+
+    // TODO(jmesserly): this is inconsistent with [visitIsExpression], which
+    // has special case for typeof.
+    return new JS.If(
+        js.call('dart.is(#, #)', [
+          _visit(_catchParameter),
+          _emitTypeName(clause.exceptionType.type),
+        ]),
+        then,
+        otherwise);
+  }
+
+  JS.Statement _statement(List<JS.Statement> statements) {
+    // TODO(jmesserly): empty block singleton?
+    if (statements.length == 0) return new JS.Block([]);
+    if (statements.length == 1) return statements[0];
+    return new JS.Block(statements);
+  }
+
+  /// Visits the catch clause body. This skips the exception type guard, if any.
+  /// That is handled in [_visitCatch].
+  @override
+  JS.Statement visitCatchClause(CatchClause node) {
+    var body = <JS.Statement>[];
+
+    var savedCatch = _catchParameter;
+    if (node.catchKeyword != null) {
+      var name = node.exceptionParameter;
+      if (name != null && name != _catchParameter) {
+        body.add(js.statement(
+            'let # = #;', [_visit(name), _visit(_catchParameter)]));
+        _catchParameter = name;
+      }
+      if (node.stackTraceParameter != null) {
+        var stackVar = node.stackTraceParameter.name;
+        body.add(js.statement(
+            'let # = dart.stackTrace(#);', [stackVar, _visit(name)]));
+      }
+    }
+
+    body.add(
+        new JS.Block(_visitList(node.body.statements) as List<JS.Statement>));
+    _catchParameter = savedCatch;
+    return _statement(body);
+  }
+
+  @override
+  JS.Case visitSwitchCase(SwitchCase node) {
+    var expr = _visit(node.expression);
+    var body = _visitList(node.statements) as List<JS.Statement>;
+    if (node.labels.isNotEmpty) {
+      body.insert(0, js.comment('Unimplemented case labels: ${node.labels}'));
+    }
+    // TODO(jmesserly): make sure we are statically checking fall through
+    return new JS.Case(expr, new JS.Block(body));
+  }
+
+  @override
+  JS.Default visitSwitchDefault(SwitchDefault node) {
+    var body = _visitList(node.statements) as List<JS.Statement>;
+    if (node.labels.isNotEmpty) {
+      body.insert(0, js.comment('Unimplemented case labels: ${node.labels}'));
+    }
+    // TODO(jmesserly): make sure we are statically checking fall through
+    return new JS.Default(new JS.Block(body));
+  }
+
+  @override
+  JS.Switch visitSwitchStatement(SwitchStatement node) => new JS.Switch(
+      _visit(node.expression),
+      _visitList(node.members) as List<JS.SwitchClause>);
+
+  @override
+  JS.Statement visitLabeledStatement(LabeledStatement node) {
+    var result = _visit(node.statement);
+    for (var label in node.labels.reversed) {
+      result = new JS.LabeledStatement(label.label.name, result);
+    }
+    return result;
+  }
+
+  @override
+  visitIntegerLiteral(IntegerLiteral node) => js.number(node.value);
+
+  @override
+  visitDoubleLiteral(DoubleLiteral node) => js.number(node.value);
+
+  @override
+  visitNullLiteral(NullLiteral node) => new JS.LiteralNull();
+
+  @override
+  visitSymbolLiteral(SymbolLiteral node) {
+    JS.New emitSymbol() {
+      // TODO(vsm): When we canonicalize, we need to treat private symbols
+      // correctly.
+      var name = js.string(node.components.join('.'), "'");
+      return new JS.New(_emitTypeName(types.symbolType), [name]);
+    }
+    return _emitConst(emitSymbol);
+  }
+
+  @override
+  visitListLiteral(ListLiteral node) {
+    JS.Expression emitList() {
+      JS.Expression list = new JS.ArrayInitializer(
+          _visitList(node.elements) as List<JS.Expression>);
+      ParameterizedType type = node.staticType;
+      var elementType = type.typeArguments.single;
+      if (elementType != types.dynamicType) {
+        // dart.list helper internally depends on _interceptors.JSArray.
+        _loader.declareBeforeUse(_jsArray);
+        list = js.call('dart.list(#, #)', [list, _emitTypeName(elementType)]);
+      }
+      return list;
+    }
+    if (node.constKeyword != null) return _emitConst(emitList);
+    return emitList();
+  }
+
+  @override
+  visitMapLiteral(MapLiteral node) {
+    // TODO(jmesserly): we can likely make these faster.
+    JS.Expression emitMap() {
+      var entries = node.entries;
+      var mapArguments = null;
+      if (entries.isEmpty) {
+        mapArguments = [];
+      } else if (entries.every((e) => e.key is StringLiteral)) {
+        // Use JS object literal notation if possible, otherwise use an array.
+        // We could do this any time all keys are non-nullable String type.
+        // For now, support StringLiteral as the common non-nullable String case.
+        var props = <JS.Property>[];
+        for (var e in entries) {
+          props.add(new JS.Property(_visit(e.key), _visit(e.value)));
+        }
+        mapArguments = new JS.ObjectInitializer(props);
+      } else {
+        var values = <JS.Expression>[];
+        for (var e in entries) {
+          values.add(_visit(e.key));
+          values.add(_visit(e.value));
+        }
+        mapArguments = new JS.ArrayInitializer(values);
+      }
+      // TODO(jmesserly): add generic types args.
+      return js.call('dart.map(#)', [mapArguments]);
+    }
+    if (node.constKeyword != null) return _emitConst(emitMap);
+    return emitMap();
+  }
+
+  @override
+  JS.LiteralString visitSimpleStringLiteral(SimpleStringLiteral node) =>
+      js.escapedString(node.value, node.isSingleQuoted ? "'" : '"');
+
+  @override
+  JS.Expression visitAdjacentStrings(AdjacentStrings node) =>
+      _visitListToBinary(node.strings, '+');
+
+  @override
+  JS.TemplateString visitStringInterpolation(StringInterpolation node) {
+    // Assuming we implement toString() on our objects, we can avoid calling it
+    // in most cases. Builtin types may differ though. We could handle this with
+    // a tagged template.
+    return new JS.TemplateString(_visitList(node.elements));
+  }
+
+  @override
+  String visitInterpolationString(InterpolationString node) {
+    // TODO(jmesserly): this call adds quotes, and then we strip them off.
+    var str = js.escapedString(node.value, '`').value;
+    return str.substring(1, str.length - 1);
+  }
+
+  @override
+  visitInterpolationExpression(InterpolationExpression node) =>
+      _visit(node.expression);
+
+  @override
+  visitBooleanLiteral(BooleanLiteral node) => js.boolean(node.value);
+
+  @override
+  JS.Expression visitExpression(Expression node) =>
+      _unimplementedCall('Unimplemented ${node.runtimeType}: $node');
+
+  JS.Expression _unimplementedCall(String comment) {
+    return js.call('dart.throw(#)', [js.escapedString(comment)]);
+  }
+
+  @override
+  visitNode(AstNode node) {
+    // TODO(jmesserly): verify this is unreachable.
+    throw 'Unimplemented ${node.runtimeType}: $node';
+  }
+
+  _visit(AstNode node) {
+    if (node == null) return null;
+    var result = node.accept(this);
+    if (result is JS.Node) result.sourceInformation = node;
+    return result;
+  }
+
+  // TODO(jmesserly): this will need to be a generic method, if we ever want to
+  // self-host strong mode.
+  List /*<T>*/ _visitList /*<T>*/ (Iterable<AstNode> nodes) {
+    if (nodes == null) return null;
+    var result = /*<T>*/ [];
+    for (var node in nodes) result.add(_visit(node));
+    return result;
+  }
+
+  /// Visits a list of expressions, creating a comma expression if needed in JS.
+  JS.Expression _visitListToBinary(List<Expression> nodes, String operator) {
+    if (nodes == null || nodes.isEmpty) return null;
+    return new JS.Expression.binary(
+        _visitList(nodes) as List<JS.Expression>, operator);
+  }
+
+  /// Like [_emitMemberName], but for declaration sites.
+  ///
+  /// Unlike call sites, we always have an element available, so we can use it
+  /// directly rather than computing the relevant options for [_emitMemberName].
+  JS.Expression _elementMemberName(ExecutableElement e,
+      {bool allowExtensions: true}) {
+    String name;
+    if (e is PropertyAccessorElement) {
+      name = e.variable.name;
+    } else {
+      name = e.name;
+    }
+    return _emitMemberName(name,
+        type: (e.enclosingElement as ClassElement).type,
+        unary: e.parameters.isEmpty,
+        isStatic: e.isStatic,
+        allowExtensions: allowExtensions);
+  }
+
+  /// This handles member renaming for private names and operators.
+  ///
+  /// Private names are generated using ES6 symbols:
+  ///
+  ///     // At the top of the module:
+  ///     let _x = Symbol('_x');
+  ///     let _y = Symbol('_y');
+  ///     ...
+  ///
+  ///     class Point {
+  ///       Point(x, y) {
+  ///         this[_x] = x;
+  ///         this[_y] = y;
+  ///       }
+  ///       get x() { return this[_x]; }
+  ///       get y() { return this[_y]; }
+  ///     }
+  ///
+  /// For user-defined operators the following names are allowed:
+  ///
+  ///     <, >, <=, >=, ==, -, +, /, ~/, *, %, |, ^, &, <<, >>, []=, [], ~
+  ///
+  /// They generate code like:
+  ///
+  ///     x['+'](y)
+  ///
+  /// There are three exceptions: [], []= and unary -.
+  /// The indexing operators we use `get` and `set` instead:
+  ///
+  ///     x.get('hi')
+  ///     x.set('hi', 123)
+  ///
+  /// This follows the same pattern as EcmaScript 6 Map:
+  /// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map>
+  ///
+  /// Unary minus looks like: `x['unary-']()`. Note that [unary] must be passed
+  /// for this transformation to happen, otherwise binary minus is assumed.
+  ///
+  /// Equality is a bit special, it is generated via the Dart `equals` runtime
+  /// helper, that checks for null. The user defined method is called '=='.
+  ///
+  JS.Expression _emitMemberName(String name,
+      {DartType type,
+      bool unary: false,
+      bool isStatic: false,
+      bool allowExtensions: true}) {
+    // Static members skip the rename steps.
+    if (isStatic) return _propertyName(name);
+
+    if (name.startsWith('_')) {
+      return _privateNames.putIfAbsent(
+          name, () => _initSymbol(new JS.TemporaryId(name)) as JS.TemporaryId);
+    }
+
+    if (name == '[]') {
+      name = 'get';
+    } else if (name == '[]=') {
+      name = 'set';
+    } else if (name == '-' && unary) {
+      name = 'unary-';
+    } else if (name == 'constructor' || name == 'prototype') {
+      // This uses an illegal (in Dart) character for a member, avoiding the
+      // conflict. We could use practically any character for this.
+      name = '+$name';
+    }
+
+    // Dart "extension" methods. Used for JS Array, Boolean, Number, String.
+    if (allowExtensions &&
+        _extensionTypes.contains(type.element) &&
+        !_objectMembers.containsKey(name)) {
+      return js.call('dartx.#', _propertyName(name));
+    }
+
+    return _propertyName(name);
+  }
+
+  bool _externalOrNative(node) =>
+      node.externalKeyword != null || _functionBody(node) is NativeFunctionBody;
+
+  FunctionBody _functionBody(node) =>
+      node is FunctionDeclaration ? node.functionExpression.body : node.body;
+
+  /// Choose a canonical name from the library element.
+  /// This never uses the library's name (the identifier in the `library`
+  /// declaration) as it doesn't have any meaningful rules enforced.
+  JS.Identifier _libraryName(LibraryElement library) {
+    if (library == currentLibrary) return _exportsVar;
+    return _imports.putIfAbsent(
+        library, () => new JS.TemporaryId(jsLibraryName(library)));
+  }
+
+  DartType getStaticType(Expression e) => rules.getStaticType(e);
+
+  @override
+  String getQualifiedName(TypeDefiningElement type) {
+    JS.TemporaryId id = _imports[type.library];
+    return id == null ? type.name : '${id.name}.${type.name}';
+  }
+
+  JS.Node annotate(JS.Node method, ExecutableElement e) =>
+      options.closure && e != null
+          ? method.withClosureAnnotation(
+              closureAnnotationFor(e, _namedArgTemp.name))
+          : method;
+
+  JS.Node annotateDefaultConstructor(JS.Node method, ClassElement e) =>
+      options.closure && e != null
+          ? method
+              .withClosureAnnotation(closureAnnotationForDefaultConstructor(e))
+          : method;
+
+  JS.Node annotateVariable(JS.Node node, VariableElement e) =>
+      options.closure && e != null
+          ? node.withClosureAnnotation(closureAnnotationForVariable(e))
+          : node;
+
+  JS.Node annotateTypeDef(JS.Node node, FunctionTypeAliasElement e) =>
+      options.closure && e != null
+          ? node.withClosureAnnotation(closureAnnotationForTypeDef(e))
+          : node;
+
+  /// Returns true if this is any kind of object represented by `Number` in JS.
+  ///
+  /// In practice, this is 4 types: num, int, double, and JSNumber.
+  ///
+  /// JSNumber is the type that actually "implements" all numbers, hence it's
+  /// a subtype of int and double (and num). It's in our "dart:_interceptors".
+  bool _isNumberInJS(DartType t) => rules.isSubTypeOf(t, _types.numType);
+}
+
+class JSGenerator extends CodeGenerator {
+  final _extensionTypes = new HashSet<ClassElement>();
+
+  JSGenerator(AbstractCompiler compiler) : super(compiler) {
+    // TODO(jacobr): determine the the set of types with extension methods from
+    // the annotations rather than hard coding the list once the analyzer
+    // supports summaries.
+    var context = compiler.context;
+    var src = context.sourceFactory.forUri('dart:_interceptors');
+    var interceptors = context.computeLibraryElement(src);
+    for (var t in ['JSArray', 'JSString', 'JSNumber', 'JSBool']) {
+      _addExtensionType(interceptors.getType(t).type);
+    }
+    // TODO(jmesserly): manually add `int` and `double`
+    // Unfortunately our current analyzer rejects "implements int".
+    // Fix was landed, so we can remove this hack once we're updated:
+    // https://github.com/dart-lang/sdk/commit/d7cd11f86a02f55269fc8d9843e7758ebeeb81c8
+    _addExtensionType(context.typeProvider.intType);
+    _addExtensionType(context.typeProvider.doubleType);
+  }
+
+  void _addExtensionType(InterfaceType t) {
+    if (t.isObject || !_extensionTypes.add(t.element)) return;
+    t = fillDynamicTypeArgs(t, rules.provider) as InterfaceType;
+    t.interfaces.forEach(_addExtensionType);
+    t.mixins.forEach(_addExtensionType);
+    _addExtensionType(t.superclass);
+  }
+
+  String generateLibrary(LibraryUnit unit) {
+    // Clone the AST first, so we can mutate it.
+    unit = unit.clone();
+    var library = unit.library.element.library;
+    var fields = findFieldsNeedingStorage(unit, _extensionTypes);
+    var codegen =
+        new JSCodegenVisitor(compiler, rules, library, _extensionTypes, fields);
+    var module = codegen.emitLibrary(unit);
+    var out = compiler.getOutputPath(library.source.uri);
+    return writeJsLibrary(module, out,
+        emitSourceMaps: options.emitSourceMaps,
+        arrowFnBindThisWorkaround: options.arrowFnBindThisWorkaround);
+  }
+}
+
+/// Choose a canonical name from the library element.
+/// This never uses the library's name (the identifier in the `library`
+/// declaration) as it doesn't have any meaningful rules enforced.
+String jsLibraryName(LibraryElement library) => canonicalLibraryName(library);
+
+/// Shorthand for identifier-like property names.
+/// For now, we emit them as strings and the printer restores them to
+/// identifiers if it can.
+// TODO(jmesserly): avoid the round tripping through quoted form.
+JS.LiteralString _propertyName(String name) => js.string(name, "'");
+
+// TODO(jacobr): we would like to do something like the following
+// but we don't have summary support yet.
+// bool _supportJsExtensionMethod(AnnotatedNode node) =>
+//    _getAnnotation(node, "SupportJsExtensionMethod") != null;
+
+/// A special kind of element created by the compiler, signifying a temporary
+/// variable. These objects use instance equality, and should be shared
+/// everywhere in the tree where they are treated as the same variable.
+class TemporaryVariableElement extends LocalVariableElementImpl {
+  TemporaryVariableElement.forNode(Identifier name) : super.forNode(name);
+
+  int get hashCode => identityHashCode(this);
+  bool operator ==(Object other) => identical(this, other);
+}
diff --git a/lib/src/codegen/js_names.dart b/lib/src/codegen/js_names.dart
index c84991f..cd13794 100644
--- a/lib/src/codegen/js_names.dart
+++ b/lib/src/codegen/js_names.dart
@@ -22,6 +22,8 @@
 /// be qualified until [setQualified] is called.
 ///
 /// This expression is transparent to visiting after [setQualified].
+///
+/// TODO(ochafik): Remove in favour of [PlaceholderExpression].
 class MaybeQualifiedId extends Expression {
   Expression _expr;
 
diff --git a/lib/src/js/dart_nodes.dart b/lib/src/js/dart_nodes.dart
new file mode 100644
index 0000000..7943a32
--- /dev/null
+++ b/lib/src/js/dart_nodes.dart
@@ -0,0 +1,315 @@
+// Copyright (c) 2012, 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.
+
+/// The pseudo-nodes defined here are not JS [Node] subclasses, but they are
+/// still [Visitable] as they may contain nested JS [Node].
+///
+/// For instance, a [DartClassDeclaration] will contain JS [Fun] bodies for
+/// its converted methods. A [DartMethodCall] will contain the converted JS
+/// arguments of the method call, etc.
+library dart_ast;
+
+import '../js/js_ast.dart' as JS;
+import 'js_types.dart';
+import 'package:analyzer/src/generated/element.dart';
+import 'package:dev_compiler/src/info.dart';
+import 'package:analyzer/analyzer.dart';
+
+part 'dart_visitor.dart';
+
+enum DartMethodCallType {
+  dsend, dcall, directDispatch, staticDispatch
+}
+
+abstract class DartNode {
+  var sourceInformation;
+  acceptDart(DartVisitor visitor);
+}
+
+class DartMethodCall extends JS.Node with DartNode {
+  /// Can be this, an expression or null (for global functions).
+  final JS.Expression target;
+  //final SimpleIdentifier memberName;
+  final JS.Expression memberName;
+  final List<JS.Expression> arguments;
+  /// Prepare for generic method calls.
+  final List<TypeRef> typeArguments;
+  /// If true, some dynamic call will be needed (which might involve a reference
+  /// to the signature).
+  final DartMethodCallType callType;
+  DartMethodCall(this.callType, this.target, this.memberName, this.arguments,
+      [this.typeArguments = const<TypeRef>[]]);
+
+  @override accept(JS.NodeVisitor visitor) => null;
+  @override acceptDart(DartVisitor visitor) =>
+      visitor.visitDartMethodCall(this);
+
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    target?.accept(visitor);
+    arguments.forEach((a) => a.accept(visitor));
+    typeArguments.forEach((a) => a.accept(visitor));
+  }
+}
+
+/// TODO(ochafik): Store [LibraryElement]?
+class DartLibrary extends DartNode {
+  final LibraryUnit libraryUnit;
+  final List<DartLibraryPart> parts;
+  DartLibrary(this.libraryUnit, this.parts);
+
+  @override
+  acceptDart(DartVisitor visitor) => visitor.visitDartLibrary(this);
+}
+
+class DartLibraryPart extends DartNode {
+  /// Note: has uri.
+  final CompilationUnitElement compilationUnitElement;
+  final List<DartDeclaration> declarations;
+  DartLibraryPart(this.compilationUnitElement, this.declarations);
+
+  @override
+  acceptDart(DartVisitor visitor) => visitor.visitDartLibraryPart(this);
+}
+
+abstract class DartDeclaration extends JS.Visitable with DartNode {
+  Element get element;
+  // String get name => element.name;
+  List<String> get genericTypeNames;
+}
+
+/// TODO(ochafik): Remove this once all top-level statements are migrated.
+class OpaqueDartDeclaration extends DartDeclaration {
+  JS.Statement statement;
+  OpaqueDartDeclaration(this.statement);
+
+  Element get element => null;
+  List<String> get genericTypeNames => const[];
+
+  @override accept(JS.NodeVisitor visitor) => statement.accept(visitor);
+  @override acceptDart(DartVisitor visitor) => visitor.visitOpaqueDartDeclaration(this);
+
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    statement.visitChildren(visitor);
+  }
+}
+
+class DartTypedef extends DartDeclaration {
+  final FunctionTypeAliasElement element;
+  final TypeRef returnType;
+  final List<TypeRef> paramTypes;
+  List<String> get genericTypeNames => const<String>[];
+
+  DartTypedef(this.element, this.returnType, this.paramTypes);
+
+  @override
+  acceptDart(DartVisitor visitor) => visitor.visitDartTypedef(this);
+
+  @override accept(JS.NodeVisitor visitor) => null;
+
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    returnType?.accept(visitor);
+    paramTypes.forEach((a) => a.accept(visitor));
+  }
+}
+
+/// JS node that gathers the JS output of a Dart class.
+/// This output is kept abstract in this node and needs to be lowered down to
+/// "normal" JS nodes like [ClassDeclaration] and potentially other support
+/// statements / function calls.
+///
+/// TODO(ochafik): Store [ClassElement]?
+class DartClassDeclaration extends DartDeclaration {
+  final ClassElement element;
+  final JS.Expression jsPeerName;
+  final TypeRef parentRef;
+  final TypeRef deferredParentRef;
+  final List<TypeRef> mixinRefs;
+  final List<TypeRef> implementedRefs;
+  final List<String> genericTypeNames;
+  final List<DartCallableDeclaration> members;
+  final List<JS.Expression> dartxNames;
+  final List<JS.Expression> overrideFields;
+  final List<JS.Expression> extensionNames;
+  final List<JS.Expression> metadataExpressions;
+  final List<JS.Expression> constructorNames;
+  JS.Expression signature;
+  JS.Expression deferredSuperType;
+
+  DartClassDeclaration(
+      {this.element,
+      this.jsPeerName,
+      this.parentRef,
+      this.deferredParentRef,
+      this.mixinRefs,
+      this.implementedRefs,
+      this.genericTypeNames,
+      this.dartxNames,
+      this.overrideFields,
+      this.extensionNames,
+      this.metadataExpressions,
+      this.constructorNames,
+      this.signature,
+      this.deferredSuperType,
+      this.members});
+
+  @override
+  acceptDart(DartVisitor visitor) => visitor.visitDartClassDeclaration(this);
+
+  @override accept(JS.NodeVisitor visitor) => null;
+
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    parentRef?.accept(visitor);
+    mixinRefs.forEach((a) => a.accept(visitor));
+    implementedRefs.forEach((a) => a.accept(visitor));
+    members.forEach((a) => a.accept(visitor));
+  }
+}
+
+enum DartCallableKind {
+  constructor,
+  namedConstructor,
+  function,
+  /// Note that the body of static fields is lazy.
+  value,
+  getter,
+  setter
+}
+
+enum DartCallableStorage {
+  topLevel,
+  instanceMember,
+  classMember
+}
+
+class DartModifiers {
+  // final bool isStatic;
+  final bool isConst;
+  final bool isFinal;
+  DartModifiers(
+      {this.isConst : false,
+      this.isFinal : false});
+}
+
+/// Representation of a "callable" Dart element such as fields, top-level values
+/// and functions, constructors, methods, accessors.
+/// TODO(ochafik): Store [ExecutableElement]?
+class DartCallableDeclaration extends DartDeclaration {
+  /// A [ClassMemberElement] or [VariableElement].
+  final Element element;
+  final JS.Expression name;
+  final DartCallableStorage storage;
+  final DartCallableKind kind;
+  List<String> get genericTypeNames => const<String>[];
+  /// Can be a [Fun] for executable declarations (getters, functions), or any
+  /// expression for fields / top-level vars.
+  final JS.Expression body;
+
+  /// Left intentionally mutable for now. Some passes will update this.
+  String comment;
+
+  DartCallableDeclaration._(this.element, this.kind, this.storage, this.name, this.body) {
+    // assert(element is PropertyAccessorElement || element is || element is VariableElement);
+    // Check body type.
+    assert(body is JS.Fun);
+    // assert((body is JS.Fun) || (kind == DartCallableKind.value));
+    // Check params.
+    switch (kind) {
+      case DartCallableKind.getter:
+        assert(name != null);
+        assert(body is JS.Fun && (body as JS.Fun).params.isEmpty);
+        break;
+      case DartCallableKind.setter:
+        assert(name != null);
+        assert(body is JS.Fun && (body as JS.Fun).params.length == 1);
+        break;
+      case DartCallableKind.function:
+        assert(name != null);
+        assert(body is JS.Fun);
+        break;
+      default:
+        break;
+    }
+  }
+
+  @override
+  acceptDart(DartVisitor visitor) => visitor.visitDartCallableDeclaration(this);
+
+  @override accept(JS.NodeVisitor visitor) => null;
+
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    body?.accept(visitor);
+  }
+}
+
+// // Dart AST creation helpers.
+// // TODO(ochafik): Simplify? Remove? Beautify?
+//
+// /// [element] is a [ClassElement] or a [ConstructorElement].
+// /// [body] is null for redirected factory constructors.
+DartCallableDeclaration newDartConstructor(Element element, JS.Expression name, JS.Expression body) =>
+    new DartCallableDeclaration._(
+        element,
+        DartCallableKind.constructor,
+        DartCallableStorage.topLevel,
+        name,
+        body);
+
+DartCallableDeclaration newDartNamedConstructor(Element element, JS.Expression name, JS.Expression body) =>
+    new DartCallableDeclaration._(
+        element,
+        DartCallableKind.constructor,
+        DartCallableStorage.topLevel,
+        name,
+        body);
+
+// DartCallableDeclaration newDartField(FieldElement element, JS.Expression body) =>
+//     new DartCallableDeclaration._(
+//         element,
+//         DartCallableKind.value,
+//         element.isStatic ? DartCallableStorage.classMember : DartCallableStorage.instanceMember,
+//         body);
+//
+// DartCallableDeclaration newDartTopLevelValue(TopLevelVariableElement element, JS.Expression body) =>
+//     new DartCallableDeclaration._(element, DartCallableKind.value, DartCallableStorage.topLevel, body);
+//
+// DartCallableDeclaration newDartTopLevelFunction(FunctionElement element, JS.Fun body) =>
+//     new DartCallableDeclaration._(element, DartCallableKind.function, DartCallableStorage.topLevel, body);
+//
+DartCallableDeclaration newDartMethod(ExecutableElement element, JS.Expression name, JS.Fun body) =>
+    new DartCallableDeclaration._(
+        element,
+        DartCallableKind.function,
+        element?.isStatic == true ? DartCallableStorage.classMember : DartCallableStorage.instanceMember,
+        name,
+        body);
+//
+// DartCallableDeclaration newDartSyntheticMethod(String name, DartType returnType, Map<String, DartType> paramTypes, JS.Fun body) {
+//   var params = <ParameterElement>[];
+//   paramTypes.forEach((name, type) {
+//     params.add(new ParameterElementImpl(name, -1)..synthetic = true..type = type);
+//   });
+//   var element = new MethodElementImpl(name, -1)
+//     ..synthetic = true
+//     ..returnType = returnType
+//     ..parameters = params;
+//   element.type = new FunctionTypeImpl(element);
+//   return newDartMethod(element, body);
+// }
+//
+// DartCallableDeclaration newDartGetter(PropertyAccessorElement element, JS.Fun body) =>
+//     new DartCallableDeclaration._(
+//         element,
+//         DartCallableKind.getter,
+//         element.isStatic ? DartCallableStorage.classMember : DartCallableStorage.instanceMember,
+//         body);
+//
+// DartCallableDeclaration newDartSetter(PropertyAccessorElement element, JS.Fun body) =>
+//     new DartCallableDeclaration._(
+//         element,
+//         DartCallableKind.setter,
+//         element.isStatic ? DartCallableStorage.classMember : DartCallableStorage.instanceMember,
+//         body);
+//
+// JS.Expression newDartTypeExpression(DartType type) =>
+//     new JS.PlaceholderExpression(new DartTypeRef(type));
diff --git a/lib/src/js/dart_visitor.dart b/lib/src/js/dart_visitor.dart
new file mode 100644
index 0000000..5517561
--- /dev/null
+++ b/lib/src/js/dart_visitor.dart
@@ -0,0 +1,14 @@
+part of dart_ast;
+
+abstract class DartVisitor<T> {
+  T visit(DartNode node) => node.acceptDart(this);
+
+  T visitDartMethodCall(DartMethodCall node);
+  T visitDartLibrary(DartLibrary node);
+  T visitDartLibraryPart(DartLibraryPart node);
+  T visitDartDeclaration(DartDeclaration node);
+  T visitOpaqueDartDeclaration(OpaqueDartDeclaration node);
+  T visitDartTypedef(DartTypedef node);
+  T visitDartClassDeclaration(DartClassDeclaration node);
+  T visitDartCallableDeclaration(DartCallableDeclaration node);
+}
diff --git a/lib/src/js/js_ast.dart b/lib/src/js/js_ast.dart
index 1c94817..f1595cf 100644
--- a/lib/src/js/js_ast.dart
+++ b/lib/src/js/js_ast.dart
@@ -8,6 +8,7 @@
 
 import 'precedence.dart';
 import 'characters.dart' as charCodes;
+import 'js_types.dart';
 
 part 'nodes.dart';
 part 'builder.dart';
diff --git a/lib/src/js/js_types.dart b/lib/src/js/js_types.dart
new file mode 100644
index 0000000..9058594
--- /dev/null
+++ b/lib/src/js/js_types.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2012, 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.
+
+library js_types;
+
+import '../closure/closure_type.dart';
+
+import '../js/js_ast.dart' as JS;
+import '../js/precedence.dart';
+import 'dart_nodes.dart';
+import 'package:analyzer/src/generated/element.dart';
+
+/// JavaScript type reference.
+abstract class TypeRef extends JS.Expression {
+  @override accept(JS.NodeVisitor visitor) => visitor.visitTypeRef(this);
+  @override void visitChildren(JS.NodeVisitor visitor) {}
+
+  int get precedenceLevel => EXPRESSION;
+}
+
+abstract class TypeRefVisitor<T> {
+  T visitTypeRef(TypeRef typeRef);
+  T visitClosureTypeRef(ClosureTypeRef typeRef);
+  T visitImportedTypeRef(ImportedTypeRef typeRef);
+  T visitTypeParamRef(TypeParamRef typeRef);
+  T visitGenericTypeRef(GenericTypeRef typeRef);
+}
+
+class DartTypeRef extends TypeRef {
+  final DartType type;
+  DartTypeRef(this.type);
+}
+
+/// Used for interop (imported Closure libs) and for basic types.
+/// TODO(ochafik): Merge [ClosureType] in this hierarchy.
+class ClosureTypeRef extends TypeRef {
+  final ClosureType type;
+  ClosureTypeRef(this.type);
+  // TODO(ochafik): Visit sub-types here.
+  @override void visitChildren(JS.NodeVisitor visitor) {}
+}
+
+/// Similar in mind to [MaybeQualifiedId], but not a JS [Node].
+class ImportedTypeRef extends TypeRef {
+  final String library;
+  /// Note: the same type might be imported multiple times with different prefixes(?),
+  /// so a pass might just walk through the imported refs and coalesce them, maybe respecting
+  /// some of the original prefixes, then outputting modules as it wants / replacing
+  /// those refs as needed.
+  final String originalPrefix;
+  final String name;
+  ImportedTypeRef(this.library, this.originalPrefix, this.name);
+}
+/// Reference to a type parameter defined in a Dart class / method.
+class TypeParamRef extends TypeRef {
+  final DartDeclaration owner;
+  final String name;
+  TypeParamRef(this.owner, this.name);
+}
+class GenericTypeRef extends TypeRef {
+  final TypeRef rawType;
+  final List<TypeRef> typeParams;
+  GenericTypeRef(this.rawType, this.typeParams);
+  @override void visitChildren(JS.NodeVisitor visitor) {
+    rawType.accept(visitor);
+    typeParams.forEach((p) => p.accept(visitor));
+  }
+}
+class OpaqueTypeRef extends TypeRef {
+  final JS.Expression expression;
+  OpaqueTypeRef(this.expression);
+
+
+  @override accept(JS.NodeVisitor visitor) =>
+      expression.accept(visitor);
+
+  @override void visitChildren(JS.NodeVisitor visitor) =>
+      expression.visitChildren(visitor);
+}
diff --git a/lib/src/js/nodes.dart b/lib/src/js/nodes.dart
index af0fc12..9659534 100644
--- a/lib/src/js/nodes.dart
+++ b/lib/src/js/nodes.dart
@@ -92,6 +92,9 @@
   T visitArrayBindingPattern(ArrayBindingPattern node);
   T visitObjectBindingPattern(ObjectBindingPattern node);
   T visitDestructuredVariable(DestructuredVariable node);
+
+  T visitTypeRef(TypeRef node);
+  T visitPlaceholderExpression(PlaceholderExpression node);
 }
 
 class BaseVisitor<T> implements NodeVisitor<T> {
@@ -221,9 +224,18 @@
   T visitObjectBindingPattern(ObjectBindingPattern node)
       => visitBindingPattern(node);
   T visitDestructuredVariable(DestructuredVariable node) => visitNode(node);
+
+  T visitTypeRef(TypeRef node) => null;
+  T visitPlaceholderExpression(PlaceholderExpression node) =>
+      node.data.accept(this);
 }
 
-abstract class Node {
+abstract class Visitable {
+  accept(NodeVisitor visitor);
+  void visitChildren(NodeVisitor visitor);
+}
+
+abstract class Node extends Visitable {
   /// Sets the source location of this node. For performance reasons, we allow
   /// setting this after construction.
   Object sourceInformation;
@@ -232,9 +244,6 @@
   /// Closure annotation of this node.
   ClosureAnnotation get closureAnnotation => _closureAnnotation;
 
-  accept(NodeVisitor visitor);
-  void visitChildren(NodeVisitor visitor);
-
   // Shallow clone of node.  Does not clone positions since the only use of this
   // private method is create a copy with a new position.
   Node _clone();
@@ -688,6 +697,28 @@
           [new VariableInitialization(name, this)]).toStatement();
 }
 
+/// Mutable placeholder expression that provides an entry point for simple
+/// AST transforms / expansions.
+///
+/// The data might not be JS [Node] but may contain JS nodes and be visitable.
+class PlaceholderExpression extends Expression {
+  var data;
+  PlaceholderExpression(this.data);
+
+  int get precedenceLevel =>
+      data is Expression ? (data as Expression).precedenceLevel : EXPRESSION;
+
+  @override
+  accept(NodeVisitor visitor) => visitor.visitPlaceholderExpression(this);
+
+  @override
+  void visitChildren(NodeVisitor visitor) {}
+
+  @override
+  Node _clone() =>
+      new PlaceholderExpression(data is Node ? (data as Node)._clone() : data);
+}
+
 class LiteralExpression extends Expression {
   final String template;
   final List<Expression> inputs;
@@ -1022,13 +1053,17 @@
   int get precedenceLevel => UNARY;
 }
 
-abstract class Parameter implements Expression, VariableBinding {}
+abstract class Parameter implements Expression, VariableBinding {
+  JsType get type;
+}
 
 class Identifier extends Expression implements Parameter, VariableBinding {
   final String name;
   final bool allowRename;
+  final JsType type;
 
-  Identifier(this.name, {this.allowRename: true}) {
+  // TODO(ochafik): Make type non-optional.
+  Identifier(this.name, {this.allowRename: true, this.type}) {
     assert(_identifierRE.hasMatch(name));
   }
   static RegExp _identifierRE = new RegExp(r'^[A-Za-z_$][A-Za-z_$0-9]*$');
@@ -1043,8 +1078,10 @@
 // This is an expression for convenience in the AST.
 class RestParameter extends Expression implements Parameter {
   final Identifier parameter;
+  final JsType type;
 
-  RestParameter(this.parameter);
+  // TODO(ochafik): Make type non-optional.
+  RestParameter(this.parameter, [this.type]);
 
   RestParameter _clone() => new RestParameter(parameter);
   accept(NodeVisitor visitor) => visitor.visitRestParameter(this);
@@ -1105,21 +1142,28 @@
   int get precedenceLevel => PRIMARY_LOW_PRECEDENCE;
 }
 
+abstract class JsType {}
+
 abstract class FunctionExpression extends Expression {
   List<Parameter> get params;
+
   get body; // Expression or block
+  JsType get returnType; // Type of the body or of its return type.
 }
 
 class Fun extends FunctionExpression {
   final List<Parameter> params;
   final Block body;
+  final JsType returnType;
   /** Whether this is a JS generator (`function*`) that may contain `yield`. */
   final bool isGenerator;
 
   final AsyncModifier asyncModifier;
 
   Fun(this.params, this.body, {this.isGenerator: false,
-      this.asyncModifier: const AsyncModifier.sync()});
+      this.asyncModifier: const AsyncModifier.sync(),
+      // TODO(ochafik): Make this non-optional.
+      this.returnType});
 
   accept(NodeVisitor visitor) => visitor.visitFun(this);
 
@@ -1137,10 +1181,13 @@
 class ArrowFun extends FunctionExpression {
   final List<Parameter> params;
   final body; // Expression or Block
+  final JsType returnType;
 
   bool _closesOverThis; // lazy initialized
 
-  ArrowFun(this.params, this.body);
+  ArrowFun(this.params, this.body,
+      // TODO(ochafik): Make this non-optional.
+      {this.returnType});
 
   accept(NodeVisitor visitor) => visitor.visitArrowFun(this);
 
diff --git a/lib/src/js/printer.dart b/lib/src/js/printer.dart
index e0e2c14..4de9aaa 100644
--- a/lib/src/js/printer.dart
+++ b/lib/src/js/printer.dart
@@ -6,6 +6,7 @@
 
 
 class JavaScriptPrintingOptions {
+  final bool outputTypeAnnotations;
   final bool shouldCompressOutput;
   final bool minifyLocalVariables;
   final bool preferSemicolonToNewlineInMinifiedOutput;
@@ -24,6 +25,7 @@
        this.preferSemicolonToNewlineInMinifiedOutput: false,
        this.allowKeywordsInProperties: false,
        this.allowSingleLineIfStatements: false,
+       this.outputTypeAnnotations: false,
        this.arrowFnBindThisWorkaround: false});
 }
 
@@ -1327,6 +1329,23 @@
     out("await ");
     visit(node.expression);
   }
+
+  @override
+  visitPlaceholderExpression(PlaceholderExpression node) {
+    var data = node.data;
+    if (data is Node) {
+      visit(data);
+    } else {
+      // Note: in debug we could still visit data.
+      throw new ArgumentError("Cannot pretty print non-JS-node data: $data");
+    }
+  }
+
+  @override
+  visitTypeRef(TypeRef node) {
+    throw new ArgumentError(
+        "Cannot pretty print type ref $node: it must be converted to JS nodes.");
+  }
 }
 
 // Collects all the var declarations in the function.  We need to do this in a
diff --git a/lib/src/js/template.dart b/lib/src/js/template.dart
index 0dea734..9343168 100644
--- a/lib/src/js/template.dart
+++ b/lib/src/js/template.dart
@@ -857,6 +857,11 @@
           makeVars.map((m) => m(arguments)).toList());
     };
   }
+
+  Instantiator visitTypeRef(TypeRef node) =>
+    TODO('visitTypeRef');
+  Instantiator visitPlaceholderExpression(PlaceholderExpression node) =>
+    TODO('visitPlaceholderExpression');
 }
 
 /**