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'); } /**