| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:collection'; |
| import 'dart:math' show max, min; |
| |
| import 'package:front_end/src/fasta/type_inference/type_schema_environment.dart'; |
| import 'package:kernel/class_hierarchy.dart'; |
| import 'package:kernel/core_types.dart'; |
| import 'package:kernel/kernel.dart' hide ConstantVisitor; |
| import 'package:kernel/type_algebra.dart'; |
| import 'package:kernel/type_environment.dart'; |
| import 'package:source_span/source_span.dart' show SourceLocation; |
| |
| import '../compiler/js_names.dart' as JS; |
| import '../compiler/js_utils.dart' as JS; |
| import '../compiler/module_builder.dart' show pathToJSIdentifier; |
| import '../js_ast/js_ast.dart' as JS; |
| import '../js_ast/js_ast.dart' show js; |
| import '../js_ast/source_map_printer.dart' show NodeEnd, NodeSpan, HoverComment; |
| import 'js_interop.dart'; |
| import 'js_typerep.dart'; |
| import 'kernel_helpers.dart'; |
| import 'native_types.dart'; |
| import 'nullable_inference.dart'; |
| import 'property_model.dart'; |
| import 'type_table.dart'; |
| |
| class ProgramCompiler |
| implements |
| StatementVisitor<JS.Statement>, |
| ExpressionVisitor<JS.Expression>, |
| DartTypeVisitor<JS.Expression> { |
| /// The list of output module items, in the order they need to be emitted in. |
| final _moduleItems = <JS.ModuleItem>[]; |
| |
| /// The set of libraries we are currently compiling, and the temporaries used |
| /// to refer to them. |
| /// |
| /// We sometimes special case codegen for a single library, as it simplifies |
| /// name scoping requirements. |
| final _libraries = new Map<Library, JS.Identifier>.identity(); |
| |
| /// Maps a library URI import, that is not in [_libraries], to the |
| /// corresponding Kernel summary module we imported it with. |
| final _importToSummary = new Map<Library, Program>.identity(); |
| |
| /// Maps a summary to the file URI we used to load it from disk. |
| final _summaryToUri = new Map<Program, Uri>.identity(); |
| |
| /// Imported libraries, and the temporaries used to refer to them. |
| final _imports = new Map<Library, JS.TemporaryId>(); |
| |
| /// The variable for the current catch clause |
| VariableDeclaration _catchParameter; |
| |
| /// In an async* function, this represents the stream controller parameter. |
| JS.TemporaryId _asyncStarController; |
| |
| // TODO(jmesserly): fuse this with notNull check. |
| final _privateNames = new HashMap<Library, HashMap<String, JS.TemporaryId>>(); |
| |
| JS.Identifier _extensionSymbolsModule; |
| final _extensionSymbols = new Map<String, JS.TemporaryId>(); |
| |
| JS.Identifier _runtimeModule; |
| final namedArgumentTemp = new JS.TemporaryId('opts'); |
| |
| Set<Class> _pendingClasses; |
| |
| /// Temporary variables mapped to their corresponding JavaScript variable. |
| final _tempVariables = <VariableDeclaration, JS.TemporaryId>{}; |
| |
| /// Let variables collected for the given function. |
| List<JS.TemporaryId> _letVariables; |
| |
| /// The class when it's emitting top-level code, used to order classes when |
| /// they extend each other. |
| /// |
| /// This is not used when inside method bodies, or for other type information |
| /// such as `implements`. |
| Class _classEmittingTopLevel; |
| |
| /// The current element being loaded. |
| /// We can use this to determine if we're loading top-level code or not: |
| /// |
| /// _currentClass == _classEmittingTopLevel |
| /// |
| Class _currentClass; |
| |
| /// The current source file URI for emitting in the source map. |
| Uri _currentUri; |
| |
| Program _program; |
| |
| Library _currentLibrary; |
| |
| FunctionNode _currentFunction; |
| |
| List<TypeParameter> _typeParamInConst; |
| |
| /// Whether we are currently generating code for the body of a `JS()` call. |
| bool _isInForeignJS = false; |
| |
| /// Table of named and possibly hoisted types. |
| TypeTable _typeTable; |
| |
| /// The global extension type table. |
| // TODO(jmesserly): rename to `_nativeTypes` |
| final NativeTypeSet _extensionTypes; |
| |
| final CoreTypes coreTypes; |
| |
| final TypeEnvironment types; |
| |
| /// Information about virtual and overridden fields/getters/setters in the |
| /// class we're currently compiling, or `null` if we aren't compiling a class. |
| ClassPropertyModel _classProperties; |
| |
| /// Information about virtual fields for all libraries in the current build |
| /// unit. |
| final virtualFields = new VirtualFieldModel(); |
| |
| final JSTypeRep _typeRep; |
| |
| bool _superAllowed = true; |
| |
| final _superHelpers = new Map<String, JS.Method>(); |
| |
| final bool emitMetadata; |
| final bool replCompile; |
| |
| final Map<String, String> declaredVariables; |
| |
| // Compilation of Kernel's [BreakStatement]. |
| // |
| // Kernel represents Dart's `break` and `continue` uniformly as |
| // [BreakStatement], by representing a loop continue as a break from the |
| // loop's body. [BreakStatement] always targets an enclosing |
| // [LabeledStatement] statement directly without naming it. (Continue to |
| // a labeled switch case is not represented by a [BreakStatement].) |
| // |
| // We prefer to compile to `continue` where possible and to avoid labeling |
| // statements where it is not necessary. We maintain some state to track |
| // which statements can be targets of break or continue without a label, which |
| // statements must be labeled to be targets, and the labels that have been |
| // assigned. |
| |
| /// A list of statements that can be the target of break without a label. |
| /// |
| /// A [BreakStatement] targeting any [LabeledStatement] in this list can be |
| /// compiled to a break without a label. All the statements in the list have |
| /// the same effective target which must compile to something that can be |
| /// targeted by break in JS. This list and [_currentContinueTargets] are |
| /// disjoint. |
| List<LabeledStatement> _currentBreakTargets = []; |
| |
| /// A list of statements that can be the target of a continue without a label. |
| /// |
| /// A [BreakStatement] targeting any [LabeledStatement] in this list can be |
| /// compiled to a continue without a label. All the statements in this list |
| /// have the same effective target which must compile to something that can be |
| /// targeted by continue in JS. This list and [_currentBreakTargets] are |
| /// disjoint. |
| List<LabeledStatement> _currentContinueTargets = []; |
| |
| /// A map from labeled statements to their 'effective targets'. |
| /// |
| /// The effective target of a labeled loop body is the enclosing loop. A |
| /// [BreakStatement] targeting this statement can be compiled to `continue` |
| /// either with or without a label. The effective target of a labeled |
| /// statement that is not a loop body is the outermost non-labeled statement |
| /// that it encloses. A [BreakStatement] targeting this statement can be |
| /// compiled to `break` either with or without a label. |
| final _effectiveTargets = new HashMap<LabeledStatement, Statement>.identity(); |
| |
| /// A map from effective targets to their label names. |
| /// |
| /// If the target needs to be labeled when compiled to JS, because it was |
| /// targeted by a break or continue with a label, then this map contains the |
| /// label name that was assigned to it. |
| final _labelNames = new HashMap<Statement, String>.identity(); |
| |
| final Class _jsArrayClass; |
| final Class _jsBoolClass; |
| final Class _jsNumberClass; |
| final Class _jsStringClass; |
| final Class privateSymbolClass; |
| final Class linkedHashMapImplClass; |
| final Class identityHashMapImplClass; |
| final Class linkedHashSetImplClass; |
| final Class identityHashSetImplClass; |
| final Class syncIterableClass; |
| |
| /// The dart:async `StreamIterator<T>` type. |
| final Class _asyncStreamIteratorClass; |
| |
| final ConstantVisitor _constants; |
| |
| final NullableInference _nullableInference; |
| |
| factory ProgramCompiler(Program program, |
| {bool emitMetadata: true, |
| bool replCompile: false, |
| Map<String, String> declaredVariables: const {}}) { |
| var nativeTypes = new NativeTypeSet(program); |
| var types = new TypeSchemaEnvironment( |
| nativeTypes.coreTypes, new ClassHierarchy(program), true); |
| return new ProgramCompiler._( |
| nativeTypes, new JSTypeRep(types, nativeTypes.sdk), |
| emitMetadata: emitMetadata, |
| replCompile: replCompile, |
| declaredVariables: declaredVariables); |
| } |
| |
| ProgramCompiler._(NativeTypeSet nativeTypes, this._typeRep, |
| {this.emitMetadata, this.replCompile, this.declaredVariables}) |
| : _extensionTypes = nativeTypes, |
| types = _typeRep.types, |
| coreTypes = nativeTypes.coreTypes, |
| _constants = new ConstantVisitor(nativeTypes.coreTypes), |
| _jsArrayClass = |
| nativeTypes.sdk.getClass('dart:_interceptors', 'JSArray'), |
| _jsBoolClass = nativeTypes.sdk.getClass('dart:_interceptors', 'JSBool'), |
| _jsNumberClass = |
| nativeTypes.sdk.getClass('dart:_interceptors', 'JSNumber'), |
| _jsStringClass = |
| nativeTypes.sdk.getClass('dart:_interceptors', 'JSString'), |
| _asyncStreamIteratorClass = |
| nativeTypes.sdk.getClass('dart:async', 'StreamIterator'), |
| privateSymbolClass = |
| nativeTypes.sdk.getClass('dart:_js_helper', 'PrivateSymbol'), |
| linkedHashMapImplClass = |
| nativeTypes.sdk.getClass('dart:_js_helper', 'LinkedMap'), |
| identityHashMapImplClass = |
| nativeTypes.sdk.getClass('dart:_js_helper', 'IdentityMap'), |
| linkedHashSetImplClass = |
| nativeTypes.sdk.getClass('dart:collection', '_HashSet'), |
| identityHashSetImplClass = |
| nativeTypes.sdk.getClass('dart:collection', '_IdentityHashSet'), |
| syncIterableClass = |
| nativeTypes.sdk.getClass('dart:_js_helper', 'SyncIterable'), |
| _nullableInference = new NullableInference(_typeRep); |
| |
| ClassHierarchy get hierarchy => types.hierarchy; |
| |
| JS.Program emitProgram( |
| Program p, List<Program> summaries, List<Uri> summaryUris) { |
| if (_moduleItems.isNotEmpty) { |
| throw new StateError('Can only call emitModule once.'); |
| } |
| _program = p; |
| |
| for (var i = 0; i < summaries.length; i++) { |
| var summary = summaries[i]; |
| var summaryUri = summaryUris[i]; |
| for (var l in summary.libraries) { |
| assert(!_importToSummary.containsKey(l)); |
| _importToSummary[l] = summary; |
| _summaryToUri[summary] = summaryUri; |
| } |
| } |
| |
| var libraries = p.libraries.where((l) => !l.isExternal); |
| var ddcRuntime = |
| libraries.firstWhere(isSdkInternalRuntime, orElse: () => null); |
| if (ddcRuntime != null) { |
| // Don't allow these to be renamed when we're building the SDK. |
| // There is JS code in dart:* that depends on their names. |
| _runtimeModule = new JS.Identifier('dart'); |
| _extensionSymbolsModule = new JS.Identifier('dartx'); |
| _nullableInference.allowNotNullDeclarations = true; |
| } else { |
| // Otherwise allow these to be renamed so users can write them. |
| _runtimeModule = new JS.TemporaryId('dart'); |
| _extensionSymbolsModule = new JS.TemporaryId('dartx'); |
| } |
| _typeTable = new TypeTable(_runtimeModule); |
| |
| // Initialize our library variables. |
| var items = <JS.ModuleItem>[]; |
| var exports = <JS.NameSpecifier>[]; |
| var root = new JS.Identifier('_root'); |
| items.add(js.statement('const # = Object.create(null)', [root])); |
| |
| void emitLibrary(JS.Identifier id) { |
| items.add(js.statement('const # = Object.create(#)', [id, root])); |
| exports.add(new JS.NameSpecifier(id)); |
| } |
| |
| for (var library in libraries) { |
| var libraryTemp = library == ddcRuntime |
| ? _runtimeModule |
| : new JS.TemporaryId(jsLibraryName(library)); |
| _libraries[library] = libraryTemp; |
| emitLibrary(libraryTemp); |
| } |
| |
| // dart:_runtime has a magic module that holds extension method symbols. |
| // TODO(jmesserly): find a cleaner design for this. |
| if (ddcRuntime != null) emitLibrary(_extensionSymbolsModule); |
| |
| items.add(new JS.ExportDeclaration(new JS.ExportClause(exports))); |
| |
| // Collect all class/type Element -> Node mappings |
| // in case we need to forward declare any classes. |
| _pendingClasses = new HashSet.identity(); |
| for (var l in libraries) { |
| _pendingClasses.addAll(l.classes); |
| } |
| |
| // Add implicit dart:core dependency so it is first. |
| emitLibraryName(coreTypes.coreLibrary); |
| |
| // Visit each library and emit its code. |
| // |
| // NOTE: clases are not necessarily emitted in this order. |
| // Order will be changed as needed so the resulting code can execute. |
| // This is done by forward declaring items. |
| libraries.forEach(_emitLibrary); |
| |
| // Visit directives (for exports) |
| libraries.forEach(_emitExports); |
| |
| // Declare imports |
| _finishImports(items); |
| // Initialize extension symbols |
| _extensionSymbols.forEach((name, id) { |
| JS.Expression value = |
| new JS.PropertyAccess(_extensionSymbolsModule, _propertyName(name)); |
| if (ddcRuntime != null) { |
| value = js.call('# = Symbol(#)', [value, js.string("dartx.$name")]); |
| } |
| items.add(js.statement('const # = #;', [id, value])); |
| }); |
| |
| // Discharge the type table cache variables and |
| // hoisted definitions. |
| items.addAll(_typeTable.discharge()); |
| |
| // Add the module's code (produced by visiting compilation units, above) |
| _copyAndFlattenBlocks(items, _moduleItems); |
| |
| // Build the module. |
| return new JS.Program(items, name: p.root.name); |
| } |
| |
| /// Flattens blocks in [items] to a single list. |
| /// |
| /// This will not flatten blocks that are marked as being scopes. |
| void _copyAndFlattenBlocks( |
| List<JS.ModuleItem> result, Iterable<JS.ModuleItem> items) { |
| for (var item in items) { |
| if (item is JS.Block && !item.isScope) { |
| _copyAndFlattenBlocks(result, item.statements); |
| } else if (item != null) { |
| result.add(item); |
| } |
| } |
| } |
| |
| /// Returns the canonical name to refer to the Dart library. |
| JS.Identifier emitLibraryName(Library library) { |
| // It's either one of the libraries in this module, or it's an import. |
| return _libraries[library] ?? |
| _imports.putIfAbsent( |
| library, () => new JS.TemporaryId(jsLibraryName(library))); |
| } |
| |
| String _libraryToModule(Library library) { |
| assert(!_libraries.containsKey(library)); |
| if (library.importUri.scheme == 'dart') { |
| // TODO(jmesserly): we need to split out HTML. |
| return JS.dartSdkModule; |
| } |
| var summary = _importToSummary[library]; |
| assert(summary != null); |
| // TODO(jmesserly): look up the appropriate relative import path if the user |
| // specified that on the command line. |
| var uri = _summaryToUri[summary]; |
| var summaryPath = uri.path; |
| var extensionIndex = summaryPath.lastIndexOf('.'); |
| // Note: These URIs do not contain absolute paths from the physical file |
| // system, but only the relevant path within a user's project. This path |
| // will match the path where the .js file is generated, so we use it as |
| // the module name. |
| var moduleName = summaryPath.substring(1, extensionIndex); |
| return moduleName; |
| } |
| |
| void _finishImports(List<JS.ModuleItem> items) { |
| var modules = new Map<String, List<Library>>(); |
| |
| for (var import in _imports.keys) { |
| modules.putIfAbsent(_libraryToModule(import), () => []).add(import); |
| } |
| |
| String coreModuleName; |
| if (!_libraries.containsKey(coreTypes.coreLibrary)) { |
| coreModuleName = _libraryToModule(coreTypes.coreLibrary); |
| } |
| modules.forEach((module, libraries) { |
| // Generate import directives. |
| // |
| // Our import variables are temps and can get renamed. Since our renaming |
| // is integrated into js_ast, it is aware of this possibility and will |
| // generate an "as" if needed. For example: |
| // |
| // import {foo} from 'foo'; // if no rename needed |
| // import {foo as foo$} from 'foo'; // if rename was needed |
| // |
| var imports = |
| libraries.map((l) => new JS.NameSpecifier(_imports[l])).toList(); |
| if (module == coreModuleName) { |
| imports.add(new JS.NameSpecifier(_runtimeModule)); |
| imports.add(new JS.NameSpecifier(_extensionSymbolsModule)); |
| } |
| |
| items.add(new JS.ImportDeclaration( |
| namedImports: imports, from: js.string(module, "'"))); |
| }); |
| } |
| |
| void _emitLibrary(Library library) { |
| // NOTE: this method isn't the right place to initialize per-library state. |
| // Classes can be visited out of order, so this is only to catch things that |
| // haven't been emitted yet. |
| // |
| // See _emitClass. |
| assert(_currentLibrary == null); |
| _currentLibrary = library; |
| |
| // `dart:_runtime` uses a different order for bootstrapping. |
| bool bootstrap = isSdkInternalRuntime(library); |
| if (bootstrap) _emitLibraryProcedures(library); |
| |
| library.classes.forEach(_emitClass); |
| library.typedefs.forEach(_emitTypedef); |
| if (bootstrap) { |
| _moduleItems.add(_emitInternalSdkFields(library.fields)); |
| } else { |
| _emitLibraryProcedures(library); |
| var fields = library.fields; |
| if (fields.isNotEmpty) |
| _moduleItems.add(_emitLazyFields(emitLibraryName(library), fields)); |
| } |
| |
| _currentLibrary = null; |
| } |
| |
| void _emitExports(Library library) { |
| assert(_currentLibrary == null); |
| _currentLibrary = library; |
| |
| library.additionalExports.forEach(_emitExport); |
| |
| _currentLibrary = null; |
| } |
| |
| void _emitExport(Reference export) { |
| var library = _currentLibrary; |
| |
| // We only need to export main as it is the only method part of the |
| // publicly exposed JS API for a library. |
| // TODO(jacobr): add a library level annotation indicating that all |
| // contents of a library need to be exposed to JS. |
| // https://github.com/dart-lang/sdk/issues/26368 |
| |
| var node = export.node; |
| if (node is Procedure && node.name.name == 'main') { |
| // Don't allow redefining names from this library. |
| var name = _emitTopLevelName(export.node); |
| _moduleItems.add(js.statement( |
| '#.# = #;', [emitLibraryName(library), name.selector, name])); |
| } |
| } |
| |
| /// Called to emit class declarations. |
| /// |
| /// During the course of emitting one item, we may emit another. For example |
| /// |
| /// class D extends B { C m() { ... } } |
| /// |
| /// Because D depends on B, we'll emit B first if needed. However C is not |
| /// used by top-level JavaScript code, so we can ignore that dependency. |
| void _emitClass(Class c) { |
| if (!_pendingClasses.remove(c)) return; |
| |
| var savedClass = _currentClass; |
| var savedLibrary = _currentLibrary; |
| var savedUri = _currentUri; |
| _currentClass = c; |
| types.thisType = c.thisType; |
| _currentLibrary = c.enclosingLibrary; |
| _currentUri = c.fileUri; |
| |
| _moduleItems.add(_emitClassDeclaration(c)); |
| |
| _currentClass = savedClass; |
| types.thisType = savedClass?.thisType; |
| _currentLibrary = savedLibrary; |
| _currentUri = savedUri; |
| } |
| |
| /// To emit top-level classes, we sometimes need to reorder them. |
| /// |
| /// This function takes care of that, and also detects cases where reordering |
| /// failed, and we need to resort to lazy loading, by marking the element as |
| /// lazy. All elements need to be aware of this possibility and generate code |
| /// accordingly. |
| /// |
| /// If we are not emitting top-level code, this does nothing, because all |
| /// declarations are assumed to be available before we start execution. |
| /// See [startTopLevel]. |
| void _declareBeforeUse(Class c) { |
| if (c == null) return; |
| if (identical(_currentClass, _classEmittingTopLevel)) _emitClass(c); |
| } |
| |
| JS.Statement _emitClassDeclaration(Class c) { |
| // If this class is annotated with `@JS`, then there is nothing to emit. |
| if (findAnnotation(c, isPublicJSAnnotation) != null) return null; |
| |
| // If this is a JavaScript type, emit it now and then exit. |
| var jsTypeDef = _emitJSType(c); |
| if (jsTypeDef != null) return jsTypeDef; |
| |
| JS.Expression className; |
| if (c.typeParameters.isNotEmpty) { |
| // Generic classes will be defined inside a function that closes over the |
| // type parameter. So we can use their local variable name directly. |
| className = new JS.Identifier(getLocalClassName(c)); |
| } else { |
| className = _emitTopLevelName(c); |
| } |
| |
| var savedClassProperties = _classProperties; |
| _classProperties = |
| new ClassPropertyModel.build(types, _extensionTypes, virtualFields, c); |
| |
| var jsCtors = _defineConstructors(c, className); |
| var jsMethods = _emitClassMethods(c); |
| |
| var body = <JS.Statement>[]; |
| _emitSuperHelperSymbols(body); |
| var deferredSupertypes = <JS.Statement>[]; |
| |
| // Emit the class, e.g. `core.Object = class Object { ... }` |
| _defineClass(c, className, jsMethods, body, deferredSupertypes); |
| body.addAll(jsCtors); |
| |
| // Emit things that come after the ES6 `class ... { ... }`. |
| var jsPeerNames = _getJSPeerNames(c); |
| if (jsPeerNames.length == 1 && c.typeParameters.isNotEmpty) { |
| // Special handling for JSArray<E> |
| body.add(_callHelperStatement('setExtensionBaseClass(#, #.global.#);', |
| [className, _runtimeModule, jsPeerNames[0]])); |
| } |
| |
| var finishGenericTypeTest = _emitClassTypeTests(c, className, body); |
| |
| _emitVirtualFieldSymbols(c, body); |
| _emitClassSignature(c, className, body); |
| _initExtensionSymbols(c); |
| _defineExtensionMembers(className, body); |
| _emitClassMetadata(c.annotations, className, body); |
| |
| var classDef = JS.Statement.from(body); |
| var typeFormals = c.typeParameters; |
| if (typeFormals.isNotEmpty) { |
| classDef = _defineClassTypeArguments( |
| c, typeFormals, classDef, className, deferredSupertypes); |
| } else { |
| body.addAll(deferredSupertypes); |
| } |
| |
| body = [classDef]; |
| _emitStaticFields(c, body); |
| if (finishGenericTypeTest != null) body.add(finishGenericTypeTest); |
| for (var peer in jsPeerNames) { |
| _registerExtensionType(c, peer, body); |
| } |
| |
| _classProperties = savedClassProperties; |
| return JS.Statement.from(body); |
| } |
| |
| /// Wraps a possibly generic class in its type arguments. |
| JS.Statement _defineClassTypeArguments( |
| NamedNode c, List<TypeParameter> formals, JS.Statement body, |
| [JS.Expression className, List<JS.Statement> deferredBaseClass]) { |
| assert(formals.isNotEmpty); |
| var name = getTopLevelName(c); |
| var jsFormals = _emitTypeFormals(formals); |
| var typeConstructor = js.call('(#) => { #; #; return #; }', [ |
| jsFormals, |
| _typeTable.discharge(formals), |
| body, |
| className ?? new JS.Identifier(name) |
| ]); |
| |
| var genericArgs = [typeConstructor]; |
| if (deferredBaseClass != null && deferredBaseClass.isNotEmpty) { |
| genericArgs.add(js.call('(#) => { #; }', [jsFormals, deferredBaseClass])); |
| } |
| |
| var genericCall = _callHelper('generic(#)', [genericArgs]); |
| |
| var genericName = _emitTopLevelNameNoInterop(c, suffix: '\$'); |
| return js.statement('{ # = #; # = #(); }', |
| [genericName, genericCall, _emitTopLevelName(c), genericName]); |
| } |
| |
| JS.Statement _emitClassStatement(Class c, JS.Expression className, |
| JS.Expression heritage, List<JS.Method> methods) { |
| var name = getLocalClassName(c); |
| var classExpr = |
| new JS.ClassExpression(new JS.Identifier(name), heritage, methods); |
| if (c.typeParameters.isNotEmpty) { |
| return classExpr.toStatement(); |
| } else { |
| return js.statement('# = #;', [className, classExpr]); |
| } |
| } |
| |
| void _defineClass(Class c, JS.Expression className, List<JS.Method> methods, |
| List<JS.Statement> body, List<JS.Statement> deferredSupertypes) { |
| if (c == coreTypes.objectClass) { |
| body.add(_emitClassStatement(c, className, null, methods)); |
| return; |
| } |
| |
| JS.Expression emitDeferredType(DartType t) { |
| if (t is InterfaceType && t.typeArguments.isNotEmpty) { |
| return _emitGenericClassType(t, t.typeArguments.map(emitDeferredType)); |
| } |
| return _emitType(t); |
| } |
| |
| bool shouldDefer(InterfaceType t) { |
| var visited = new Set<DartType>(); |
| bool defer(DartType t) { |
| if (t is InterfaceType) { |
| var tc = t.classNode; |
| if (c == tc) return true; |
| if (tc == coreTypes.objectClass || !visited.add(t)) return false; |
| if (t.typeArguments.any(defer)) return true; |
| var mixin = tc.mixedInType; |
| return mixin != null && defer(mixin.asInterfaceType) || |
| defer(tc.supertype.asInterfaceType); |
| } |
| if (t is TypedefType) { |
| return t.typeArguments.any(defer); |
| } |
| if (t is FunctionType) { |
| return defer(t.returnType) || |
| t.positionalParameters.any(defer) || |
| t.namedParameters.any((np) => defer(np.type)) || |
| t.typeParameters.any((tp) => defer(tp.bound)); |
| } |
| return false; |
| } |
| |
| return defer(t); |
| } |
| |
| emitClassRef(InterfaceType t) { |
| // TODO(jmesserly): investigate this. It seems like `lazyJSType` is |
| // invalid for use in an `extends` clause, hence this workaround. |
| return _emitJSInterop(t.classNode) ?? visitInterfaceType(t); |
| } |
| |
| getBaseClass(int count) { |
| var base = emitDeferredType(c.thisType); |
| while (--count >= 0) { |
| base = js.call('#.__proto__', [base]); |
| } |
| return base; |
| } |
| |
| var mixins = <InterfaceType>[]; |
| var mixedInType = c.mixedInType; |
| var superclass = c.superclass; |
| var supertype = c.supertype.asInterfaceType; |
| if (mixedInType != null) { |
| mixins.add(mixedInType.asInterfaceType); |
| for (; |
| superclass.isSyntheticMixinImplementation; |
| superclass = superclass.superclass) { |
| mixins.add(hierarchy |
| .getClassAsInstanceOf(c, superclass.mixedInClass) |
| .asInterfaceType); |
| } |
| if (mixins.length > 1) mixins = mixins.reversed.toList(); |
| supertype = hierarchy.getClassAsInstanceOf(c, superclass).asInterfaceType; |
| } |
| |
| var hasUnnamedSuper = _hasUnnamedConstructor(superclass); |
| var isCallable = isCallableClass(c); |
| |
| void emitMixinConstructors(JS.Expression className, InterfaceType mixin) { |
| JS.Statement mixinCtor; |
| if (_hasUnnamedConstructor(mixin.classNode)) { |
| mixinCtor = js.statement('#.#.call(this);', [ |
| emitClassRef(mixin), |
| _usesMixinNew(mixin.classNode) |
| ? _callHelper('mixinNew') |
| : _constructorName('') |
| ]); |
| } |
| |
| for (var ctor in superclass.constructors) { |
| var jsParams = _emitFormalParameters(ctor.function); |
| var ctorBody = <JS.Statement>[]; |
| if (mixinCtor != null) ctorBody.add(mixinCtor); |
| var name = ctor.name.name; |
| if (name != '' || hasUnnamedSuper) { |
| ctorBody.add(_emitSuperConstructorCall(className, name, jsParams)); |
| } |
| body.add(_addConstructorToClass( |
| className, |
| name, |
| _finishConstructorFunction( |
| jsParams, new JS.Block(ctorBody), isCallable))); |
| } |
| } |
| |
| var savedTopLevelClass = _classEmittingTopLevel; |
| _classEmittingTopLevel = c; |
| |
| // Unroll mixins. |
| if (shouldDefer(supertype)) { |
| deferredSupertypes.add(_callHelperStatement('setBaseClass(#, #)', [ |
| getBaseClass(isMixinAliasClass(c) ? 0 : mixins.length), |
| emitDeferredType(supertype), |
| ])); |
| supertype = supertype.classNode.rawType; |
| } |
| var baseClass = emitClassRef(supertype); |
| |
| if (isMixinAliasClass(c)) { |
| // Given `class C = Object with M [implements I1, I2 ...];` |
| // The resulting class C should work as a mixin. |
| // |
| // TODO(jmesserly): is there any way to merge this with the other mixin |
| // code paths, or will these always need special handling? |
| body.add(_emitClassStatement(c, className, baseClass, [])); |
| |
| var m = c.mixedInType.asInterfaceType; |
| bool deferMixin = shouldDefer(m); |
| var mixinBody = deferMixin ? deferredSupertypes : body; |
| var mixinClass = deferMixin ? emitDeferredType(m) : emitClassRef(m); |
| var classExpr = deferMixin ? getBaseClass(0) : className; |
| |
| mixinBody.add( |
| _callHelperStatement('mixinMembers(#, #)', [classExpr, mixinClass])); |
| |
| _classEmittingTopLevel = savedTopLevelClass; |
| |
| if (methods.isNotEmpty) { |
| // However we may need to add some methods to this class that call |
| // `super` such as covariance checks. |
| // |
| // We do this with the following pattern: |
| // |
| // mixinMembers(C, class C$ extends M { <methods> }); |
| mixinBody.add(_callHelperStatement('mixinMembers(#, #)', [ |
| classExpr, |
| new JS.ClassExpression( |
| new JS.TemporaryId(getLocalClassName(c)), mixinClass, methods) |
| ])); |
| } |
| |
| emitMixinConstructors(className, m); |
| return; |
| } |
| |
| // TODO(jmesserly): we need to unroll kernel mixins because the synthetic |
| // classes lack required synthetic members, such as constructors. |
| // |
| // Also, we need to generate one extra level of nesting for alias classes. |
| for (int i = 0; i < mixins.length; i++) { |
| var m = mixins[i]; |
| var mixinName = |
| getLocalClassName(superclass) + '_' + getLocalClassName(m.classNode); |
| var mixinId = new JS.TemporaryId(mixinName + '\$'); |
| // Bind the mixin class to a name to workaround a V8 bug with es6 classes |
| // and anonymous function names. |
| // TODO(leafp:) Eliminate this once the bug is fixed: |
| // https://bugs.chromium.org/p/v8/issues/detail?id=7069 |
| body.add(js.statement("const # = #", [ |
| mixinId, |
| new JS.ClassExpression(new JS.TemporaryId(mixinName), baseClass, []) |
| ])); |
| |
| emitMixinConstructors(mixinId, m); |
| hasUnnamedSuper = hasUnnamedSuper || _hasUnnamedConstructor(m.classNode); |
| |
| if (shouldDefer(m)) { |
| deferredSupertypes.add(_callHelperStatement('mixinMembers(#, #)', |
| [getBaseClass(mixins.length - i), emitDeferredType(m)])); |
| } else { |
| body.add(_callHelperStatement( |
| 'mixinMembers(#, #)', [mixinId, emitClassRef(m)])); |
| } |
| |
| baseClass = mixinId; |
| } |
| |
| _classEmittingTopLevel = savedTopLevelClass; |
| |
| body.add(_emitClassStatement(c, className, baseClass, methods)); |
| } |
| |
| /// Defines all constructors for this class as ES5 constructors. |
| List<JS.Statement> _defineConstructors(Class c, JS.Expression className) { |
| var isCallable = isCallableClass(c); |
| |
| var body = <JS.Statement>[]; |
| if (isCallable) { |
| // Our class instances will have JS `typeof this == "function"`, |
| // so make sure to attach the runtime type information the same way |
| // we would do it for function types. |
| body.add(js.statement('#.prototype[#] = #;', |
| [className, _callHelper('_runtimeType'), className])); |
| } |
| |
| if (c.isSyntheticMixinImplementation || isMixinAliasClass(c)) { |
| // We already handled this when we defined the class. |
| return body; |
| } |
| |
| addConstructor(String name, JS.Expression jsCtor) { |
| body.add(_addConstructorToClass(className, name, jsCtor)); |
| } |
| |
| var fields = c.fields; |
| for (var ctor in c.constructors) { |
| if (ctor.isExternal) continue; |
| addConstructor(ctor.name.name, |
| _emitConstructor(ctor, fields, isCallable, className)); |
| } |
| |
| // If classElement has only factory constructors, and it can be mixed in, |
| // then we need to emit a special hidden default constructor for use by |
| // mixins. |
| if (_usesMixinNew(c)) { |
| body.add( |
| js.statement('(#[#] = function() { # }).prototype = #.prototype;', [ |
| className, |
| _callHelper('mixinNew'), |
| [_initializeFields(fields)], |
| className |
| ])); |
| } |
| |
| return body; |
| } |
| |
| JS.Statement _emitClassTypeTests( |
| Class c, JS.Expression className, List<JS.Statement> body) { |
| JS.Expression getInterfaceSymbol(Class interface) { |
| var library = interface.enclosingLibrary; |
| if (library == coreTypes.coreLibrary || |
| library == coreTypes.asyncLibrary) { |
| switch (interface.name) { |
| case 'List': |
| case 'Map': |
| case 'Iterable': |
| case 'Future': |
| case 'Stream': |
| case 'StreamSubscription': |
| return _callHelper('is' + interface.name); |
| } |
| } |
| return null; |
| } |
| |
| void markSubtypeOf(JS.Expression testSymbol) { |
| body.add(js.statement('#.prototype[#] = true', [className, testSymbol])); |
| } |
| |
| for (var iface in c.implementedTypes) { |
| var prop = getInterfaceSymbol(iface.classNode); |
| if (prop != null) markSubtypeOf(prop); |
| } |
| |
| // TODO(jmesserly): share these hand coded type checks with the old back |
| // end, perhaps by factoring them into a common file, or move them to be |
| // static methdos in the SDK. (Or wait until we delete the old back end.) |
| if (c.enclosingLibrary == coreTypes.coreLibrary) { |
| if (c == coreTypes.objectClass) { |
| // Everything is an Object. |
| body.add(js.statement( |
| '#.is = function is_Object(o) { return true; }', [className])); |
| body.add(js.statement( |
| '#.as = function as_Object(o) { return o; }', [className])); |
| body.add(js.statement( |
| '#._check = function check_Object(o) { return o; }', [className])); |
| return null; |
| } |
| if (c == coreTypes.stringClass) { |
| body.add(js.statement( |
| '#.is = function is_String(o) { return typeof o == "string"; }', |
| className)); |
| body.add(js.statement( |
| '#.as = function as_String(o) {' |
| ' if (typeof o == "string" || o == null) return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_String(o) {' |
| ' if (typeof o == "string" || o == null) return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| if (c == coreTypes.functionClass) { |
| body.add(js.statement( |
| '#.is = function is_Function(o) { return typeof o == "function"; }', |
| className)); |
| body.add(js.statement( |
| '#.as = function as_Function(o) {' |
| ' if (typeof o == "function" || o == null) return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_String(o) {' |
| ' if (typeof o == "function" || o == null) return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| if (c == coreTypes.intClass) { |
| body.add(js.statement( |
| '#.is = function is_int(o) {' |
| ' return typeof o == "number" && Math.floor(o) == o;' |
| '}', |
| className)); |
| body.add(js.statement( |
| '#.as = function as_int(o) {' |
| ' if ((typeof o == "number" && Math.floor(o) == o) || o == null)' |
| ' return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_int(o) {' |
| ' if ((typeof o == "number" && Math.floor(o) == o) || o == null)' |
| ' return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| if (c == coreTypes.nullClass) { |
| body.add(js.statement( |
| '#.is = function is_Null(o) { return o == null; }', className)); |
| body.add(js.statement( |
| '#.as = function as_Null(o) {' |
| ' if (o == null) return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_Null(o) {' |
| ' if (o == null) return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| if (c == coreTypes.numClass || c == coreTypes.doubleClass) { |
| body.add(js.statement( |
| '#.is = function is_num(o) { return typeof o == "number"; }', |
| className)); |
| body.add(js.statement( |
| '#.as = function as_num(o) {' |
| ' if (typeof o == "number" || o == null) return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_num(o) {' |
| ' if (typeof o == "number" || o == null) return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| if (c == coreTypes.boolClass) { |
| body.add(js.statement( |
| '#.is = function is_bool(o) { return o === true || o === false; }', |
| className)); |
| body.add(js.statement( |
| '#.as = function as_bool(o) {' |
| ' if (o === true || o === false || o == null) return o;' |
| ' return #.as(o, #, false);' |
| '}', |
| [className, _runtimeModule, className])); |
| body.add(js.statement( |
| '#._check = function check_bool(o) {' |
| ' if (o === true || o === false || o == null) return o;' |
| ' return #.as(o, #, true);' |
| '}', |
| [className, _runtimeModule, className])); |
| return null; |
| } |
| } |
| if (c.enclosingLibrary == coreTypes.asyncLibrary) { |
| if (c == coreTypes.futureOrClass) { |
| var typeParam = new TypeParameterType(c.typeParameters[0]); |
| var typeT = visitTypeParameterType(typeParam); |
| var futureOfT = visitInterfaceType( |
| new InterfaceType(coreTypes.futureClass, [typeParam])); |
| body.add(js.statement(''' |
| #.is = function is_FutureOr(o) { |
| return #.is(o) || #.is(o); |
| } |
| ''', [className, typeT, futureOfT])); |
| // TODO(jmesserly): remove the fallback to `dart.as`. It's only for the |
| // _ignoreTypeFailure logic. |
| body.add(js.statement(''' |
| #.as = function as_FutureOr(o) { |
| if (o == null || #.is(o) || #.is(o)) return o; |
| return #.as(o, this, false); |
| } |
| ''', [className, typeT, futureOfT, _runtimeModule])); |
| body.add(js.statement(''' |
| #._check = function check_FutureOr(o) { |
| if (o == null || #.is(o) || #.is(o)) return o; |
| return #.as(o, this, true); |
| } |
| ''', [className, typeT, futureOfT, _runtimeModule])); |
| return null; |
| } |
| } |
| |
| body.add(_callHelperStatement('addTypeTests(#);', [className])); |
| |
| if (c.typeParameters.isEmpty) return null; |
| |
| // For generics, testing against the default instantiation is common, |
| // so optimize that. |
| var isClassSymbol = getInterfaceSymbol(c); |
| if (isClassSymbol == null) { |
| // TODO(jmesserly): we could export these symbols, if we want to mark |
| // implemented interfaces for user-defined classes. |
| var id = new JS.TemporaryId("_is_${getLocalClassName(c)}_default"); |
| _moduleItems.add( |
| js.statement('const # = Symbol(#);', [id, js.string(id.name, "'")])); |
| isClassSymbol = id; |
| } |
| // Marking every generic type instantiation as a subtype of its default |
| // instantiation. |
| markSubtypeOf(isClassSymbol); |
| |
| // Define the type tests on the default instantiation to check for that |
| // marker. |
| var defaultInst = _emitTopLevelName(c); |
| |
| // Return this `addTypeTests` call so we can emit it outside of the generic |
| // type parameter scope. |
| return _callHelperStatement( |
| 'addTypeTests(#, #);', [defaultInst, isClassSymbol]); |
| } |
| |
| void _emitSymbols(Iterable<JS.TemporaryId> vars, List<JS.ModuleItem> body) { |
| for (var id in vars) { |
| body.add(js.statement('const # = Symbol(#)', [id, js.string(id.name)])); |
| } |
| } |
| |
| void _emitSuperHelperSymbols(List<JS.Statement> body) { |
| _emitSymbols( |
| _superHelpers.values.map((m) => m.name as JS.TemporaryId), body); |
| _superHelpers.clear(); |
| } |
| |
| /// Emits static fields for a class, and initialize them eagerly if possible, |
| /// otherwise define them as lazy properties. |
| void _emitStaticFields(Class c, List<JS.Statement> body) { |
| var fields = c.fields.where((f) => f.isStatic).toList(); |
| if (c.isEnum) { |
| // We know enum fields can be safely emitted as const fields, as long |
| // as the `values` field is emitted last. |
| var classRef = _emitTopLevelName(c); |
| var valueField = fields.firstWhere((f) => f.name.name == 'values'); |
| fields.remove(valueField); |
| fields.add(valueField); |
| for (var f in fields) { |
| assert(f.isConst); |
| body.add(_defineValueOnClass( |
| classRef, |
| _emitStaticMemberName(f.name.name), |
| _visitInitializer(f.initializer, f.annotations)) |
| .toStatement()); |
| } |
| } else if (fields.isNotEmpty) { |
| body.add(_emitLazyFields(_emitTopLevelName(c), fields)); |
| } |
| } |
| |
| void _emitClassMetadata(List<Expression> metadata, JS.Expression className, |
| List<JS.Statement> body) { |
| // Metadata |
| if (emitMetadata && metadata.isNotEmpty) { |
| body.add(js.statement('#[#.metadata] = #;', [ |
| className, |
| _runtimeModule, |
| _arrowFunctionWithLetScope(() => new JS.ArrayInitializer( |
| metadata.map(_instantiateAnnotation).toList())) |
| ])); |
| } |
| } |
| |
| /// Ensure `dartx.` symbols we will use are present. |
| void _initExtensionSymbols(Class c) { |
| if (_extensionTypes.hasNativeSubtype(c) || c == coreTypes.objectClass) { |
| for (var m in c.procedures) { |
| if (!m.isAbstract && !m.isStatic && !m.name.isPrivate) { |
| _declareMemberName(m, useExtension: true); |
| } |
| } |
| } |
| } |
| |
| /// If a concrete class implements one of our extensions, we might need to |
| /// add forwarders. |
| void _defineExtensionMembers( |
| JS.Expression className, List<JS.Statement> body) { |
| void emitExtensions(String helperName, Iterable<String> extensions) { |
| if (extensions.isEmpty) return; |
| |
| var names = extensions |
| .map((e) => _propertyName(JS.memberNameForDartMember(e))) |
| .toList(); |
| body.add(js.statement('#.#(#, #);', [ |
| _runtimeModule, |
| helperName, |
| className, |
| new JS.ArrayInitializer(names, multiline: names.length > 4) |
| ])); |
| } |
| |
| var props = _classProperties; |
| emitExtensions('defineExtensionMethods', props.extensionMethods); |
| emitExtensions('defineExtensionAccessors', props.extensionAccessors); |
| } |
| |
| /// Emit the signature on the class recording the runtime type information |
| void _emitClassSignature( |
| Class c, JS.Expression className, List<JS.Statement> body) { |
| if (c.implementedTypes.isNotEmpty) { |
| body.add(js.statement('#[#.implements] = () => [#];', [ |
| className, |
| _runtimeModule, |
| c.implementedTypes.map((i) => _emitType(i.asInterfaceType)) |
| ])); |
| } |
| |
| void emitSignature(String name, List<JS.Property> elements) { |
| if (elements.isEmpty) return; |
| |
| if (!name.startsWith('Static')) { |
| var proto = c == coreTypes.objectClass |
| ? js.call('Object.create(null)') |
| : _callHelper('get${name}s(#.__proto__)', [className]); |
| elements.insert(0, new JS.Property(_propertyName('__proto__'), proto)); |
| } |
| body.add(_callHelperStatement('set${name}Signature(#, () => #)', [ |
| className, |
| new JS.ObjectInitializer(elements, multiline: elements.length > 1) |
| ])); |
| } |
| |
| var extMembers = _classProperties.extensionMethods; |
| var staticMethods = <JS.Property>[]; |
| var instanceMethods = <JS.Property>[]; |
| var staticGetters = <JS.Property>[]; |
| var instanceGetters = <JS.Property>[]; |
| var staticSetters = <JS.Property>[]; |
| var instanceSetters = <JS.Property>[]; |
| List<JS.Property> getSignatureList(Procedure p) { |
| if (p.isStatic) { |
| if (p.isGetter) { |
| return staticGetters; |
| } else if (p.isSetter) { |
| return staticSetters; |
| } else { |
| return staticMethods; |
| } |
| } else { |
| if (p.isGetter) { |
| return instanceGetters; |
| } else if (p.isSetter) { |
| return instanceSetters; |
| } else { |
| return instanceMethods; |
| } |
| } |
| } |
| |
| for (var member in c.procedures) { |
| if (member.isAbstract) continue; |
| |
| // Static getters/setters cannot be called with dynamic dispatch, nor |
| // can they be torn off. |
| // TODO(jmesserly): can we attach static method type info at the tearoff |
| // point, and avoid saving the information otherwise? Same trick would |
| // work for top-level functions. |
| if (!emitMetadata && member.isAccessor && member.isStatic) { |
| continue; |
| } |
| |
| var name = member.name.name; |
| var reifiedType = _getMemberRuntimeType(member); |
| |
| // Don't add redundant signatures for inherited methods whose signature |
| // did not change. If we are not overriding, or if the thing we are |
| // overriding has a different reified type from ourselves, we must |
| // emit a signature on this class. Otherwise we will inherit the |
| // signature from the superclass. |
| var memberOverride = c.superclass != null |
| ? hierarchy.getDispatchTarget(c.superclass, member.name, |
| setter: member.isSetter) |
| : null; |
| |
| var needsSignature = memberOverride == null || |
| reifiedType != |
| Substitution |
| .fromSupertype(hierarchy.getClassAsInstanceOf( |
| c, memberOverride.enclosingClass)) |
| .substituteType(_getMemberRuntimeType(memberOverride)); |
| |
| if (needsSignature) { |
| var type = _emitAnnotatedFunctionType(reifiedType, member.annotations, |
| function: member.function); |
| var property = new JS.Property(_declareMemberName(member), type); |
| var signatures = getSignatureList(member); |
| signatures.add(property); |
| if (!member.isStatic && extMembers.contains(name)) { |
| signatures.add(new JS.Property( |
| _declareMemberName(member, useExtension: true), type)); |
| } |
| } |
| } |
| |
| emitSignature('Method', instanceMethods); |
| emitSignature('StaticMethod', staticMethods); |
| emitSignature('Getter', instanceGetters); |
| emitSignature('Setter', instanceSetters); |
| emitSignature('StaticGetter', staticGetters); |
| emitSignature('StaticSetter', staticSetters); |
| |
| var instanceFields = <JS.Property>[]; |
| var staticFields = <JS.Property>[]; |
| for (var field in c.fields) { |
| // Only instance fields need to be saved for dynamic dispatch. |
| var isStatic = field.isStatic; |
| if (!emitMetadata && isStatic) continue; |
| |
| var memberName = _declareMemberName(field); |
| var fieldSig = _emitFieldSignature(field.type, |
| metadata: field.annotations, isFinal: field.isFinal); |
| (isStatic ? staticFields : instanceFields) |
| .add(new JS.Property(memberName, fieldSig)); |
| } |
| emitSignature('Field', instanceFields); |
| emitSignature('StaticField', staticFields); |
| |
| var constructors = <JS.Property>[]; |
| if (emitMetadata) { |
| for (var ctor in c.constructors) { |
| var memberName = _constructorName(ctor.name.name); |
| var type = _emitAnnotatedFunctionType( |
| ctor.function.functionType.withoutTypeParameters, ctor.annotations, |
| function: ctor.function, nameType: false, definite: true); |
| constructors.add(new JS.Property(memberName, type)); |
| } |
| } |
| emitSignature('Constructor', constructors); |
| |
| // Add static property dart._runtimeType to Object. |
| // All other Dart classes will (statically) inherit this property. |
| if (c == coreTypes.objectClass) { |
| body.add(_callHelperStatement('tagComputed(#, () => #.#);', |
| [className, emitLibraryName(coreTypes.coreLibrary), 'Type'])); |
| } |
| } |
| |
| JS.Expression _emitFieldSignature(DartType type, |
| {List<Expression> metadata, bool isFinal: true}) { |
| var args = [_emitType(type)]; |
| if (emitMetadata && metadata != null && metadata.isNotEmpty) { |
| args.add(new JS.ArrayInitializer( |
| metadata.map(_instantiateAnnotation).toList())); |
| } |
| return _callHelper(isFinal ? 'finalFieldType(#)' : 'fieldType(#)', [args]); |
| } |
| |
| FunctionType _getMemberRuntimeType(Member member) { |
| var f = member.function; |
| if (f == null) { |
| assert(member is Field); |
| return new FunctionType([], member.getterType); |
| } |
| |
| if (!f.positionalParameters.any(isCovariant) && |
| !f.namedParameters.any(isCovariant)) { |
| return f.functionType; |
| } |
| |
| reifyParameter(VariableDeclaration p) => |
| isCovariant(p) ? coreTypes.objectClass.thisType : p.type; |
| reifyNamedParameter(VariableDeclaration p) => |
| new NamedType(p.name, reifyParameter(p)); |
| |
| // TODO(jmesserly): do covariant type parameter bounds also need to be |
| // reified as `Object`? |
| return new FunctionType( |
| f.positionalParameters.map(reifyParameter).toList(), f.returnType, |
| namedParameters: f.namedParameters.map(reifyNamedParameter).toList() |
| ..sort(), |
| typeParameters: f.functionType.typeParameters, |
| requiredParameterCount: f.requiredParameterCount); |
| } |
| |
| JS.Expression _emitConstructor(Constructor node, List<Field> fields, |
| bool isCallable, JS.Expression className) { |
| var params = _emitFormalParameters(node.function); |
| var body = _withCurrentFunction( |
| node.function, |
| () => _superDisallowed( |
| () => _emitConstructorBody(node, fields, className))); |
| |
| return _finishConstructorFunction(params, new JS.Block(body), isCallable) |
| ..sourceInformation = _nodeEnd(node.fileEndOffset) ?? |
| _nodeEnd(node.enclosingClass.fileEndOffset); |
| } |
| |
| List<JS.Statement> _emitConstructorBody( |
| Constructor node, List<Field> fields, JS.Expression className) { |
| var cls = node.enclosingClass; |
| |
| // 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. |
| var fn = node.function; |
| var body = _emitArgumentInitializers(fn); |
| |
| // 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 RedirectingInitializer, orElse: () => null) |
| as RedirectingInitializer; |
| |
| if (redirectCall != null) { |
| body.add(_emitRedirectingConstructor(redirectCall, className)); |
| return 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(fields, node)); |
| |
| // 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 superCall = node.initializers.firstWhere((i) => i is SuperInitializer, |
| orElse: () => null) as SuperInitializer; |
| var jsSuper = _emitSuperConstructorCallIfNeeded(cls, className, superCall); |
| if (jsSuper != null) { |
| body.add(jsSuper..sourceInformation = _nodeStart(superCall)); |
| } |
| |
| body.add(_emitFunctionScopedBody(fn)); |
| return body; |
| } |
| |
| JS.Expression _constructorName(String name) { |
| if (name == '') { |
| // Default constructors (factory or not) use `new` as their name. |
| return _propertyName('new'); |
| } |
| return _emitStaticMemberName(name); |
| } |
| |
| JS.Statement _emitRedirectingConstructor( |
| RedirectingInitializer node, JS.Expression className) { |
| var ctor = node.target; |
| // We can't dispatch to the constructor with `this.new` as that might hit a |
| // derived class constructor with the same name. |
| return js.statement('#.#.call(this, #);', [ |
| className, |
| _constructorName(ctor.name.name), |
| _emitArgumentList(node.arguments) |
| ]); |
| } |
| |
| JS.Statement _emitSuperConstructorCallIfNeeded( |
| Class c, JS.Expression className, |
| [SuperInitializer superInit]) { |
| if (c == coreTypes.objectClass) return null; |
| |
| Constructor ctor; |
| List<JS.Expression> args; |
| if (superInit == null) { |
| ctor = unnamedConstructor(c.superclass); |
| args = []; |
| } else { |
| ctor = superInit.target; |
| args = _emitArgumentList(superInit.arguments); |
| } |
| // We can skip the super call if it's empty. Most commonly this happens for |
| // things that extend Object, and don't have any field initializers or their |
| // own default constructor. |
| if (ctor.name.name == '' && !_hasUnnamedSuperConstructor(c)) { |
| return null; |
| } |
| return _emitSuperConstructorCall(className, ctor.name.name, args); |
| } |
| |
| JS.Statement _emitSuperConstructorCall( |
| JS.Expression className, String name, List<JS.Expression> args) { |
| return js.statement('#.__proto__.#.call(this, #);', |
| [className, _constructorName(name), args ?? []]); |
| } |
| |
| bool _hasUnnamedSuperConstructor(Class c) { |
| if (c == null) return false; |
| return _hasUnnamedConstructor(c.superclass) || |
| _hasUnnamedConstructor(c.mixedInClass); |
| } |
| |
| bool _hasUnnamedConstructor(Class c) { |
| if (c == null || c == coreTypes.objectClass) return false; |
| var ctor = unnamedConstructor(c); |
| if (ctor != null && !ctor.isSynthetic) return true; |
| if (c.fields.any((f) => !f.isStatic)) return true; |
| return _hasUnnamedSuperConstructor(c); |
| } |
| |
| JS.Expression _finishConstructorFunction( |
| List<JS.Parameter> params, JS.Block body, bool isCallable) { |
| // We consider a class callable if it inherits from anything with a `call` |
| // method. As a result, we can know the callable JS function was created |
| // at the first constructor that was hit. |
| if (!isCallable) return new JS.Fun(params, body); |
| return js.call(r'''function callableClass(#) { |
| if (typeof this !== "function") { |
| function self(...args) { |
| return self.call.apply(self, args); |
| } |
| self.__proto__ = this.__proto__; |
| callableClass.call(self, #); |
| return self; |
| } |
| # |
| }''', [params, params, body]); |
| } |
| |
| /// 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(List<Field> fields, [Constructor ctor]) { |
| // Run field initializers if they can have side-effects. |
| Set<Field> ctorFields; |
| if (ctor != null) { |
| ctorFields = ctor.initializers |
| .map((c) => c is FieldInitializer ? c.field : null) |
| .toSet() |
| ..remove(null); |
| } |
| |
| var body = <JS.Statement>[]; |
| emitFieldInit(Field f, Expression initializer, TreeNode hoverInfo) { |
| var access = _classProperties.virtualFields[f] ?? _declareMemberName(f); |
| var jsInit = _visitInitializer(initializer, f.annotations); |
| body.add(jsInit |
| .toAssignExpression(js.call('this.#', [access]) |
| ..sourceInformation = _nodeStart(hoverInfo)) |
| .toStatement()); |
| } |
| |
| for (var f in fields) { |
| if (f.isStatic) continue; |
| var init = f.initializer; |
| if (ctorFields != null && |
| ctorFields.contains(f) && |
| (init == null || _constants.isConstant(init))) { |
| continue; |
| } |
| emitFieldInit(f, init, f); |
| } |
| |
| // Run constructor field initializers such as `: foo = bar.baz` |
| if (ctor != null) { |
| for (var init in ctor.initializers) { |
| if (init is FieldInitializer) { |
| emitFieldInit(init.field, init.value, init); |
| } else if (init is LocalInitializer) { |
| body.add(visitVariableDeclaration(init.variable)); |
| } else if (init is AssertInitializer) { |
| body.add(visitAssertStatement(init.statement)); |
| } |
| } |
| } |
| |
| return JS.Statement.from(body); |
| } |
| |
| JS.Expression _visitInitializer( |
| Expression init, List<Expression> annotations) { |
| // explicitly initialize to null, to avoid getting `undefined`. |
| // TODO(jmesserly): do this only for vars that aren't definitely assigned. |
| if (init == null) return new JS.LiteralNull(); |
| return _annotatedNullCheck(annotations) |
| ? notNull(init) |
| : _visitExpression(init); |
| } |
| |
| JS.Expression notNull(Expression expr) { |
| if (expr == null) return null; |
| var jsExpr = _visitExpression(expr); |
| if (!isNullable(expr)) return jsExpr; |
| return _callHelper('notNull(#)', jsExpr); |
| } |
| |
| /// If the class has only factory constructors, and it can be mixed in, |
| /// then we need to emit a special hidden default constructor for use by |
| /// mixins. |
| bool _usesMixinNew(Class mixin) { |
| return mixin.superclass?.superclass == null && |
| mixin.constructors.every((c) => c.isExternal); |
| } |
| |
| JS.Statement _addConstructorToClass( |
| JS.Expression className, String name, JS.Expression jsCtor) { |
| jsCtor = _defineValueOnClass(className, _constructorName(name), jsCtor); |
| return js.statement('#.prototype = #.prototype;', [jsCtor, className]); |
| } |
| |
| JS.Expression _defineValueOnClass( |
| JS.Expression className, JS.Expression name, JS.Expression value) { |
| var args = [className, name, value]; |
| if (name is JS.LiteralString && |
| JS.invalidStaticFieldName(name.valueWithoutQuotes)) { |
| return _callHelper('defineValue(#, #, #)', args); |
| } |
| return js.call('#.# = #', args); |
| } |
| |
| List<JS.Method> _emitClassMethods(Class c) { |
| var virtualFields = _classProperties.virtualFields; |
| |
| var jsMethods = <JS.Method>[]; |
| bool hasJsPeer = findAnnotation(c, isJsPeerInterface) != null; |
| bool hasIterator = false; |
| |
| if (c == coreTypes.objectClass) { |
| // Dart does not use ES6 constructors. |
| // Add an error to catch any invalid usage. |
| jsMethods.add( |
| new JS.Method(_propertyName('constructor'), js.fun(r'''function() { |
| throw Error("use `new " + #.typeName(#.getReifiedType(this)) + |
| ".new(...)` to create a Dart object"); |
| }''', [_runtimeModule, _runtimeModule]))); |
| } |
| |
| for (var m in c.fields) { |
| if (_extensionTypes.isNativeClass(c)) { |
| jsMethods.addAll(_emitNativeFieldAccessors(m)); |
| continue; |
| } |
| if (m.isStatic) continue; |
| if (virtualFields.containsKey(m)) { |
| jsMethods.addAll(_emitVirtualFieldAccessor(m)); |
| } |
| } |
| |
| var getters = new Map<String, Procedure>(); |
| var setters = new Map<String, Procedure>(); |
| for (var m in c.procedures) { |
| if (m.isAbstract) continue; |
| if (m.isGetter) { |
| getters[m.name.name] = m; |
| } else if (m.isSetter) { |
| setters[m.name.name] = m; |
| } |
| } |
| |
| for (var m in c.procedures) { |
| if (m.isForwardingStub) { |
| // TODO(jmesserly): is there any other kind of forwarding stub? |
| jsMethods.addAll(_emitCovarianceCheckStub(m)); |
| } else if (m.isFactory) { |
| jsMethods.add(_emitFactoryConstructor(m)); |
| } else if (m.isAccessor) { |
| jsMethods.add(_emitMethodDeclaration(m)); |
| jsMethods.add(_emitSuperAccessorWrapper(m, getters, setters)); |
| if (!hasJsPeer && m.isGetter && m.name.name == 'iterator') { |
| hasIterator = true; |
| jsMethods.add(_emitIterable(c)); |
| } |
| } else { |
| jsMethods.add(_emitMethodDeclaration(m)); |
| } |
| } |
| |
| for (Member m in _classProperties.mockMembers.values) { |
| _addMockMembers(m, c, jsMethods); |
| } |
| |
| // 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) { |
| jsMethods.add(_emitIterable(c)); |
| } |
| |
| // Add all of the super helper methods |
| jsMethods.addAll(_superHelpers.values); |
| |
| return jsMethods.where((m) => m != null).toList(); |
| } |
| |
| /// Emits a method, getter, or setter. |
| JS.Method _emitMethodDeclaration(Procedure member) { |
| if (member.isAbstract) { |
| return null; |
| } |
| |
| JS.Fun fn; |
| if (member.isExternal) { |
| if (member.isStatic) { |
| // TODO(vsm): Do we need to handle this case? |
| return null; |
| } |
| fn = _emitNativeFunctionBody(member); |
| } else { |
| fn = _emitFunction(member.function, member.name.name); |
| } |
| |
| return new JS.Method(_declareMemberName(member), fn, |
| isGetter: member.isGetter, |
| isSetter: member.isSetter, |
| isStatic: member.isStatic) |
| ..sourceInformation = _nodeEnd(member.fileEndOffset); |
| } |
| |
| JS.Fun _emitNativeFunctionBody(Procedure node) { |
| String name = getAnnotationName(node, isJSAnnotation) ?? node.name.name; |
| if (node.isGetter) { |
| return new JS.Fun([], js.block('{ return this.#; }', [name])); |
| } else if (node.isSetter) { |
| var params = _emitFormalParameters(node.function); |
| return new JS.Fun( |
| params, js.block('{ this.# = #; }', [name, params.last])); |
| } else { |
| return js.fun( |
| 'function (...args) { return this.#.apply(this, args); }', name); |
| } |
| } |
| |
| List<JS.Method> _emitCovarianceCheckStub(Procedure member) { |
| // TODO(jmesserly): kernel stubs have a few problems: |
| // - they're generated even when there is no concrete super member |
| // - the stub parameter types don't match the types we need to check to |
| // ensure soundness of the super member, so we must lookup the super |
| // member and determine checks ourselves. |
| // - it generates getter stubs, but these are not used |
| if (member.isGetter) return []; |
| |
| var enclosingClass = member.enclosingClass; |
| var superMember = member.forwardingStubSuperTarget ?? |
| member.forwardingStubInterfaceTarget; |
| |
| if (superMember == null) return []; |
| |
| var superSubstition = Substitution.fromSupertype(hierarchy |
| .getClassAsInstanceOf(enclosingClass, superMember.enclosingClass)); |
| |
| var name = _declareMemberName(member); |
| if (member.isSetter) { |
| if (superMember is Field && superMember.isGenericCovariantImpl || |
| superMember is Procedure && |
| isCovariant(superMember.function.positionalParameters[0])) { |
| return []; |
| } |
| return [ |
| new JS.Method( |
| name, |
| js.fun('function(x) { return super.# = #._check(x); }', [ |
| name, |
| _emitType(superSubstition.substituteType(superMember.setterType)) |
| ]), |
| isSetter: true), |
| new JS.Method(name, js.fun('function() { return super.#; }', [name]), |
| isGetter: true) |
| ]; |
| } |
| assert(!member.isAccessor); |
| |
| var superMethodType = superSubstition |
| .substituteType(superMember.function.functionType) as FunctionType; |
| var function = member.function; |
| |
| var body = <JS.Statement>[]; |
| var typeParameters = superMethodType.typeParameters; |
| _emitCovarianceBoundsCheck(typeParameters, body); |
| |
| var typeFormals = _emitTypeFormals(typeParameters); |
| var jsParams = new List<JS.Parameter>.from(typeFormals); |
| var positionalParameters = function.positionalParameters; |
| for (var i = 0, n = positionalParameters.length; i < n; i++) { |
| var param = positionalParameters[i]; |
| var jsParam = new JS.Identifier(param.name); |
| jsParams.add(jsParam); |
| |
| if (isCovariant(param) && |
| !isCovariant(superMember.function.positionalParameters[i])) { |
| var check = js.call('#._check(#)', |
| [_emitType(superMethodType.positionalParameters[i]), jsParam]); |
| if (i >= function.requiredParameterCount) { |
| body.add(js.statement('if (# !== void 0) #;', [jsParam, check])); |
| } else { |
| body.add(check.toStatement()); |
| } |
| } |
| } |
| var namedParameters = function.namedParameters; |
| for (var param in namedParameters) { |
| if (isCovariant(param) && |
| !isCovariant(superMember.function.namedParameters |
| .firstWhere((n) => n.name == param.name))) { |
| var name = _propertyName(param.name); |
| var paramType = superMethodType.namedParameters |
| .firstWhere((n) => n.name == param.name); |
| body.add(js.statement('if (# in #) #._check(#.#);', [ |
| name, |
| namedArgumentTemp, |
| _emitType(paramType.type), |
| namedArgumentTemp, |
| name |
| ])); |
| } |
| } |
| |
| if (namedParameters.isNotEmpty) jsParams.add(namedArgumentTemp); |
| |
| if (typeFormals.isEmpty) { |
| body.add(js.statement('return super.#(#);', [name, jsParams])); |
| } else { |
| body.add( |
| js.statement('return super.#(#)(#);', [name, typeFormals, jsParams])); |
| } |
| var fn = new JS.Fun(jsParams, new JS.Block(body)); |
| return [new JS.Method(name, fn)]; |
| } |
| |
| /// Emits a Dart factory constructor to a JS static method. |
| JS.Method _emitFactoryConstructor(Procedure node) { |
| return new JS.Method( |
| _constructorName(node.name.name), |
| new JS.Fun(_emitFormalParameters(node.function), |
| _emitFunctionBody(node.function)), |
| isStatic: true) |
| ..sourceInformation = _nodeEnd(node.fileEndOffset); |
| } |
| |
| /// Emits an expression that lets you access statics on a [type] from code. |
| /// |
| /// If [nameType] is true, then the type will be named. In addition, |
| /// if [hoistType] is true, then the named type will be hoisted. |
| JS.Expression emitConstructorAccess(InterfaceType type) { |
| return _emitJSInterop(type.classNode) ?? visitInterfaceType(type); |
| } |
| |
| /// Given a class C that implements method M from interface I, but does not |
| /// declare M, this will generate an implementation that forwards to |
| /// noSuchMethod. |
| /// |
| /// For example: |
| /// |
| /// class Cat { |
| /// bool eatFood(String food) => true; |
| /// } |
| /// class MockCat implements Cat { |
| /// noSuchMethod(Invocation invocation) => 3; |
| /// } |
| /// |
| /// It will generate an `eatFood` that looks like: |
| /// |
| /// eatFood(...args) { |
| /// return core.bool.as(this.noSuchMethod( |
| /// new dart.InvocationImpl.new('eatFood', args))); |
| /// } |
| /// |
| /// Same technique is applied if interface I has fields, and C doesn't declare |
| /// neither the fields nor the corresponding getters and setters. |
| void _addMockMembers(Member member, Class c, List<JS.Method> jsMethods) { |
| JS.Method implementMockMember( |
| List<TypeParameter> typeParameters, |
| List<VariableDeclaration> namedParameters, |
| ProcedureKind mockMemberKind, |
| DartType returnType) { |
| assert(mockMemberKind != ProcedureKind.Factory); |
| |
| var invocationProps = <JS.Property>[]; |
| addProperty(String name, JS.Expression value) { |
| invocationProps.add(new JS.Property(js.string(name), value)); |
| } |
| |
| var args = new JS.TemporaryId('args'); |
| var typeParams = _emitTypeFormals(typeParameters); |
| var fnArgs = new List<JS.Parameter>.from(typeParams); |
| JS.Expression positionalArgs; |
| |
| if (namedParameters.isNotEmpty) { |
| addProperty( |
| 'namedArguments', _callHelper('extractNamedArgs(#)', [args])); |
| } |
| |
| if (mockMemberKind != ProcedureKind.Getter && |
| mockMemberKind != ProcedureKind.Setter) { |
| addProperty('isMethod', js.boolean(true)); |
| |
| fnArgs.add(new JS.RestParameter(args)); |
| positionalArgs = args; |
| } else { |
| if (mockMemberKind == ProcedureKind.Getter) { |
| addProperty('isGetter', js.boolean(true)); |
| |
| positionalArgs = new JS.ArrayInitializer([]); |
| } else if (mockMemberKind == ProcedureKind.Setter) { |
| addProperty('isSetter', js.boolean(true)); |
| |
| fnArgs.add(args); |
| positionalArgs = new JS.ArrayInitializer([args]); |
| } |
| } |
| |
| if (typeParams.isNotEmpty) { |
| addProperty('typeArguments', new JS.ArrayInitializer(typeParams)); |
| } |
| |
| var fnBody = |
| js.call('this.noSuchMethod(new #.InvocationImpl.new(#, #, #))', [ |
| _runtimeModule, |
| _declareMemberName(member), |
| positionalArgs, |
| new JS.ObjectInitializer(invocationProps) |
| ]); |
| |
| if (!types.isTop(returnType)) { |
| fnBody = js.call('#._check(#)', [_emitType(returnType), fnBody]); |
| } |
| |
| var fn = new JS.Fun(fnArgs, js.block('{ return #; }', [fnBody]), |
| typeParams: typeParams); |
| |
| return new JS.Method( |
| _declareMemberName(member, |
| useExtension: _extensionTypes.isNativeClass(c)), |
| fn, |
| isGetter: mockMemberKind == ProcedureKind.Getter, |
| isSetter: mockMemberKind == ProcedureKind.Setter, |
| isStatic: false); |
| } |
| |
| if (member is Field) { |
| jsMethods.add(implementMockMember( |
| const <TypeParameter>[], |
| const <VariableDeclaration>[], |
| ProcedureKind.Getter, |
| Substitution |
| .fromSupertype( |
| hierarchy.getClassAsInstanceOf(c, member.enclosingClass)) |
| .substituteType(member.type))); |
| if (!member.isFinal) { |
| jsMethods.add(implementMockMember( |
| const <TypeParameter>[], |
| const <VariableDeclaration>[], |
| ProcedureKind.Setter, |
| new DynamicType())); |
| } |
| } else { |
| Procedure procedure = member as Procedure; |
| FunctionNode function = procedure.function; |
| jsMethods.add(implementMockMember( |
| function.typeParameters, |
| function.namedParameters, |
| procedure.kind, |
| Substitution |
| .fromSupertype( |
| hierarchy.getClassAsInstanceOf(c, member.enclosingClass)) |
| .substituteType(function.returnType))); |
| } |
| } |
| |
| /// This is called whenever a derived class needs to introduce a new field, |
| /// shadowing a field or getter/setter pair on its parent. |
| /// |
| /// This is important because otherwise, trying to read or write the field |
| /// would end up calling the getter or setter, and one of those might not even |
| /// exist, resulting in a runtime error. Even if they did exist, that's the |
| /// wrong behavior if a new field was declared. |
| List<JS.Method> _emitVirtualFieldAccessor(Field field) { |
| var virtualField = _classProperties.virtualFields[field]; |
| var result = <JS.Method>[]; |
| var name = _declareMemberName(field); |
| |
| var mocks = _classProperties.mockMembers; |
| if (!mocks.containsKey(field.name.name)) { |
| var getter = js.fun('function() { return this[#]; }', [virtualField]); |
| result.add(new JS.Method(name, getter, isGetter: true) |
| ..sourceInformation = _nodeStart(field)); |
| } |
| |
| if (!mocks.containsKey(field.name.name + '=')) { |
| var args = field.isFinal |
| ? [new JS.Super(), name] |
| : [new JS.This(), virtualField]; |
| |
| String jsCode; |
| if (!field.isFinal && field.isGenericCovariantImpl) { |
| args.add(_emitType(field.type)); |
| jsCode = 'function(value) { #[#] = #._check(value); }'; |
| } else { |
| jsCode = 'function(value) { #[#] = value; }'; |
| } |
| |
| result.add(new JS.Method(name, js.fun(jsCode, args), isSetter: true) |
| ..sourceInformation = _nodeStart(field)); |
| } |
| |
| return result; |
| } |
| |
| /// Provide Dart getters and setters that forward to the underlying native |
| /// field. Note that the Dart names are always symbolized to avoid |
| /// conflicts. They will be installed as extension methods on the underlying |
| /// native type. |
| List<JS.Method> _emitNativeFieldAccessors(Field field) { |
| // TODO(vsm): Can this by meta-programmed? |
| // E.g., dart.nativeField(symbol, jsName) |
| // Alternatively, perhaps it could be meta-programmed directly in |
| // dart.registerExtensions? |
| var jsMethods = <JS.Method>[]; |
| if (field.isStatic) return jsMethods; |
| |
| var name = getAnnotationName(field, isJSName) ?? field.name.name; |
| // Generate getter |
| var fn = new JS.Fun([], js.block('{ return this.#; }', [name])); |
| var method = new JS.Method(_declareMemberName(field), fn, isGetter: true); |
| jsMethods.add(method); |
| |
| // Generate setter |
| if (!field.isFinal) { |
| var value = new JS.TemporaryId('value'); |
| fn = new JS.Fun([value], js.block('{ this.# = #; }', [name, value])); |
| method = new JS.Method(_declareMemberName(field), fn, isSetter: true); |
| jsMethods.add(method); |
| } |
| |
| return jsMethods; |
| } |
| |
| /// Emit a getter (or setter) that simply forwards to the superclass getter |
| /// (or setter). |
| /// |
| /// This is needed because in ES6, if you only override a getter |
| /// (alternatively, a setter), then there is an implicit override of the |
| /// setter (alternatively, the getter) that does nothing. |
| JS.Method _emitSuperAccessorWrapper(Procedure method, |
| Map<String, Procedure> getters, Map<String, Procedure> setters) { |
| var name = method.name.name; |
| var memberName = _declareMemberName(method); |
| if (method.isGetter) { |
| if (!setters.containsKey(name) && |
| _classProperties.inheritedSetters.contains(name)) { |
| // Generate a setter that forwards to super. |
| var fn = js.fun('function(value) { super[#] = value; }', [memberName]); |
| return new JS.Method(memberName, fn, isSetter: true); |
| } |
| } else { |
| assert(method.isSetter); |
| if (!getters.containsKey(name) && |
| _classProperties.inheritedGetters.contains(name)) { |
| // Generate a getter that forwards to super. |
| var fn = js.fun('function() { return super[#]; }', [memberName]); |
| return new JS.Method(memberName, fn, isGetter: true); |
| } |
| } |
| return null; |
| } |
| |
| /// 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(Class c) { |
| var iterable = hierarchy.getClassAsInstanceOf(c, coreTypes.iterableClass); |
| if (iterable == null) return null; |
| |
| // 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 = |
| hierarchy.getDispatchTarget(c.superclass, new Name('iterator')); |
| if (parent != null) return null; |
| |
| var parentIterable = |
| hierarchy.getClassAsInstanceOf(c.superclass, coreTypes.iterableClass); |
| if (parentIterable != null) return null; |
| |
| if (c.enclosingLibrary.importUri.scheme == 'dart' && |
| c.procedures.any((m) => getJSExportName(m) == 'Symbol.iterator')) { |
| 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 #.JsIterator(this.#); }', [ |
| _runtimeModule, |
| _emitMemberName('iterator', type: iterable.asInterfaceType) |
| ]) as JS.Fun); |
| } |
| |
| JS.Expression _instantiateAnnotation(Expression node) => |
| _visitExpression(node); |
| |
| /// Gets the JS peer for this Dart type if any, otherwise null. |
| /// |
| /// For example for dart:_interceptors `JSArray` this will return "Array", |
| /// referring to the JavaScript built-in `Array` type. |
| List<String> _getJSPeerNames(Class c) { |
| var jsPeerNames = getAnnotationName( |
| c, |
| (a) => |
| isJsPeerInterface(a) || |
| isNativeAnnotation(a) && _extensionTypes.isNativeClass(c)); |
| if (c == coreTypes.objectClass) return ['Object']; |
| if (jsPeerNames == null) return []; |
| |
| // Omit the special name "!nonleaf" and any future hacks starting with "!" |
| var result = |
| jsPeerNames.split(',').where((peer) => !peer.startsWith("!")).toList(); |
| return result; |
| } |
| |
| void _registerExtensionType( |
| Class c, String jsPeerName, List<JS.Statement> body) { |
| var className = _emitTopLevelName(c); |
| if (isPrimitiveType(c.rawType)) { |
| body.add(_callHelperStatement( |
| 'definePrimitiveHashCode(#.prototype)', className)); |
| } |
| body.add(_callHelperStatement( |
| 'registerExtension(#, #);', [js.string(jsPeerName), className])); |
| } |
| |
| JS.Statement _emitJSType(Class c) { |
| var jsTypeName = getAnnotationName(c, isJSAnnotation); |
| if (jsTypeName == null || jsTypeName == c.name) return null; |
| |
| // 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. |
| return js.statement('# = #;', [_emitTopLevelName(c), jsTypeName]); |
| } |
| |
| void _emitTypedef(Typedef t) { |
| var savedUri = _currentUri; |
| _currentUri = t.fileUri; |
| var body = _callHelper( |
| 'typedef(#, () => #)', [js.string(t.name, "'"), _emitType(t.type)]); |
| |
| JS.Statement result; |
| if (t.typeParameters.isNotEmpty) { |
| result = _defineClassTypeArguments( |
| t, t.typeParameters, js.statement('const # = #;', [t.name, body])); |
| } else { |
| result = js.statement('# = #;', [_emitTopLevelName(t), body]); |
| } |
| |
| _currentUri = savedUri; |
| _moduleItems.add(result); |
| } |
| |
| /// Treat dart:_runtime fields as safe to eagerly evaluate. |
| // TODO(jmesserly): it'd be nice to avoid this special case. |
| JS.Statement _emitInternalSdkFields(Iterable<Field> fields) { |
| var lazyFields = <Field>[]; |
| var savedUri = _currentUri; |
| |
| for (var field in fields) { |
| // Skip our magic undefined constant. |
| if (field.name.name == 'undefined') continue; |
| |
| var init = field.initializer; |
| if (init == null || |
| init is BasicLiteral || |
| init is StaticInvocation && isInlineJS(init.target) || |
| init is ConstructorInvocation && |
| isSdkInternalRuntime(init.target.enclosingLibrary)) { |
| _currentUri = field.fileUri; |
| _moduleItems.add(js.statement('# = #;', [ |
| _emitTopLevelName(field), |
| _visitInitializer(init, field.annotations) |
| ])); |
| } else { |
| lazyFields.add(field); |
| } |
| } |
| |
| _currentUri = savedUri; |
| return _emitLazyFields(emitLibraryName(_currentLibrary), lazyFields); |
| } |
| |
| JS.Statement _emitLazyFields(JS.Expression objExpr, Iterable<Field> fields) { |
| var accessors = <JS.Method>[]; |
| var savedUri = _currentUri; |
| |
| for (var field in fields) { |
| _currentUri = field.fileUri; |
| var name = field.name.name; |
| var access = _emitStaticMemberName(name); |
| accessors.add(new JS.Method(access, _emitStaticFieldInitializer(field), |
| isGetter: true) |
| ..sourceInformation = _hoverComment( |
| new JS.PropertyAccess(objExpr, access), |
| field.fileOffset, |
| name.length)); |
| |
| // TODO(jmesserly): currently uses a dummy setter to indicate writable. |
| if (!field.isFinal && !field.isConst) { |
| accessors.add(new JS.Method(access, js.call('function(_) {}') as JS.Fun, |
| isSetter: true)); |
| } |
| } |
| _currentUri = _currentLibrary.fileUri; |
| |
| _currentUri = savedUri; |
| return _callHelperStatement('defineLazy(#, { # });', [objExpr, accessors]); |
| } |
| |
| JS.Fun _emitStaticFieldInitializer(Field field) { |
| return new JS.Fun( |
| [], |
| new JS.Block(_withLetScope(() => [ |
| new JS.Return( |
| _visitInitializer(field.initializer, field.annotations)) |
| ]))); |
| } |
| |
| List<JS.Statement> _withLetScope(List<JS.Statement> visitBody()) { |
| var savedLetVariables = _letVariables; |
| _letVariables = []; |
| |
| var body = visitBody(); |
| var letVars = _initLetVariables(); |
| if (letVars != null) body.insert(0, letVars); |
| |
| _letVariables = savedLetVariables; |
| return body; |
| } |
| |
| JS.ArrowFun _arrowFunctionWithLetScope(JS.Expression visitBody()) { |
| var savedLetVariables = _letVariables; |
| _letVariables = []; |
| |
| var expr = visitBody(); |
| var letVars = _initLetVariables(); |
| |
| _letVariables = savedLetVariables; |
| return new JS.ArrowFun( |
| [], letVars == null ? expr : new JS.Block([letVars, expr.toReturn()])); |
| } |
| |
| JS.PropertyAccess _emitTopLevelName(NamedNode n, {String suffix: ''}) { |
| return _emitJSInterop(n) ?? _emitTopLevelNameNoInterop(n, suffix: suffix); |
| } |
| |
| /// 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 _declareMemberName(Member m, {bool useExtension}) { |
| return _emitMemberName(m.name.name, |
| isStatic: m is Field ? m.isStatic : (m as Procedure).isStatic, |
| useExtension: |
| useExtension ?? _extensionTypes.isNativeClass(m.enclosingClass), |
| member: m); |
| } |
| |
| /// 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._negate()`. |
| /// |
| /// 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 isStatic: false, |
| bool useExtension, |
| NamedNode member}) { |
| // Static members skip the rename steps and may require JS interop renames. |
| if (isStatic) { |
| return _emitStaticMemberName(name, member); |
| } |
| |
| // We allow some (illegal in Dart) member names to be used in our private |
| // SDK code. These renames need to be included at every declaration, |
| // including overrides in subclasses. |
| if (member != null) { |
| var runtimeName = getJSExportName(member); |
| if (runtimeName != null) { |
| var parts = runtimeName.split('.'); |
| if (parts.length < 2) return _propertyName(runtimeName); |
| |
| JS.Expression result = new JS.Identifier(parts[0]); |
| for (int i = 1; i < parts.length; i++) { |
| result = new JS.PropertyAccess(result, _propertyName(parts[i])); |
| } |
| return result; |
| } |
| } |
| |
| if (name.startsWith('_')) { |
| return _emitPrivateNameSymbol(_currentLibrary, name); |
| } |
| |
| useExtension ??= _isSymbolizedMember(type, name); |
| // TODO(vsm): Do not rename members that conflict with standard JS members |
| // if we are actually try to access those JS members via interop. |
| name = JS.memberNameForDartMember(name); |
| if (useExtension) { |
| return _getExtensionSymbolInternal(name); |
| } |
| return _propertyName(name); |
| } |
| |
| /// This is an internal method used by [_emitMemberName] and the |
| /// optimized `dart:_runtime extensionSymbol` builtin to get the symbol |
| /// for `dartx.<name>`. |
| /// |
| /// Do not call this directly; you want [_emitMemberName], which knows how to |
| /// handle the many details involved in naming. |
| JS.TemporaryId _getExtensionSymbolInternal(String name) { |
| return _extensionSymbols.putIfAbsent( |
| name, |
| () => new JS.TemporaryId( |
| '\$${JS.friendlyNameForDartOperator[name] ?? name}')); |
| } |
| |
| /// Don't symbolize native members that just forward to the underlying |
| /// native member. We limit this to non-renamed members as the receiver |
| /// may be a mock type. |
| /// |
| /// Note, this is an underlying assumption here that, if another native type |
| /// subtypes this one, it also forwards this member to its underlying native |
| /// one without renaming. |
| bool _isSymbolizedMember(DartType type, String name) { |
| while (type is TypeParameterType) { |
| type = (type as TypeParameterType).bound; |
| } |
| if (type == null || |
| type == const DynamicType() || |
| type == coreTypes.objectClass) { |
| return isObjectMember(name); |
| } else if (type is InterfaceType) { |
| var c = getImplementationClass(type) ?? type.classNode; |
| if (_extensionTypes.isNativeClass(c)) { |
| var member = _lookupForwardedMember(c, name); |
| |
| // Fields on a native class are implicitly native. |
| // Methods/getters/setters are marked external/native. |
| if (member is Field || member is Procedure && member.isExternal) { |
| var jsName = getAnnotationName(member, isJSName); |
| return jsName != null && jsName != name; |
| } else { |
| // Non-external members must be symbolized. |
| return true; |
| } |
| } |
| // If the receiver *may* be a native type (i.e., an interface allowed to |
| // be implemented by a native class), conservatively symbolize - we don't |
| // know whether it'll be implemented via forwarding. |
| // TODO(vsm): Consider CHA here to be less conservative. |
| return _extensionTypes.isNativeInterface(c); |
| } else if (type is FunctionType) { |
| return true; |
| } |
| return false; |
| } |
| |
| var _forwardingCache = new HashMap<Class, Map<String, Member>>(); |
| |
| Member _lookupForwardedMember(Class c, String name) { |
| // We only care about public methods. |
| if (name.startsWith('_')) return null; |
| |
| var map = _forwardingCache.putIfAbsent(c, () => {}); |
| |
| return map.putIfAbsent( |
| name, |
| () => |
| hierarchy.getDispatchTarget(c, new Name(name)) ?? |
| hierarchy.getDispatchTarget(c, new Name(name), setter: true)); |
| } |
| |
| JS.TemporaryId _emitPrivateNameSymbol(Library library, String name) { |
| return _privateNames |
| .putIfAbsent(library, () => new HashMap()) |
| .putIfAbsent(name, () { |
| var id = new JS.TemporaryId(name); |
| _moduleItems.add( |
| js.statement('const # = Symbol(#);', [id, js.string(id.name, "'")])); |
| return id; |
| }); |
| } |
| |
| JS.Expression _emitStaticMemberName(String name, [NamedNode member]) { |
| if (member != null) { |
| var jsName = _emitJSInteropStaticMemberName(member); |
| if (jsName != null) return jsName; |
| } |
| |
| switch (name) { |
| // Reserved for the compiler to do `x as T`. |
| case 'as': |
| // Reserved for the compiler to do implicit cast `T x = y`. |
| case '_check': |
| // Reserved for the SDK to compute `Type.toString()`. |
| case 'name': |
| // Reserved by JS, not a valid static member name. |
| case 'prototype': |
| name += '_'; |
| break; |
| default: |
| // All trailing underscores static names are reserved for the compiler |
| // or SDK libraries. |
| // |
| // If user code uses them, add an extra `_`. |
| // |
| // This also avoids collision with the renames above, e.g. `static as` |
| // and `static as_` will become `as_` and `as__`. |
| if (name.endsWith('_')) { |
| name += '_'; |
| } |
| } |
| return _propertyName(name); |
| } |
| |
| JS.Expression _emitJSInteropStaticMemberName(NamedNode n) { |
| if (!isJSElement(n)) return null; |
| var name = getAnnotationName(n, isPublicJSAnnotation); |
| if (name != null) { |
| if (name.contains('.')) { |
| throw new UnsupportedError( |
| 'static members do not support "." in their names. ' |
| 'See https://github.com/dart-lang/sdk/issues/27926'); |
| } |
| } else { |
| name = getTopLevelName(n); |
| } |
| return js.escapedString(name, "'"); |
| } |
| |
| JS.PropertyAccess _emitTopLevelNameNoInterop(NamedNode n, |
| {String suffix: ''}) { |
| var name = getJSExportName(n) ?? getTopLevelName(n); |
| return new JS.PropertyAccess( |
| emitLibraryName(getLibrary(n)), _propertyName(name + suffix)); |
| } |
| |
| String _getJSNameWithoutGlobal(NamedNode n) { |
| if (!isJSElement(n)) return null; |
| var libraryJSName = getAnnotationName(getLibrary(n), isPublicJSAnnotation); |
| var jsName = |
| getAnnotationName(n, isPublicJSAnnotation) ?? getTopLevelName(n); |
| return libraryJSName != null ? '$libraryJSName.$jsName' : jsName; |
| } |
| |
| JS.PropertyAccess _emitJSInterop(NamedNode n) { |
| var jsName = _getJSNameWithoutGlobal(n); |
| if (jsName == null) return null; |
| return _emitJSInteropForGlobal(jsName); |
| } |
| |
| JS.PropertyAccess _emitJSInteropForGlobal(String name) { |
| var parts = name.split('.'); |
| if (parts.isEmpty) parts = ['']; |
| JS.PropertyAccess access; |
| for (var part in parts) { |
| access = new JS.PropertyAccess( |
| access ?? _callHelper('global'), js.escapedString(part, "'")); |
| } |
| return access; |
| } |
| |
| void _emitLibraryProcedures(Library library) { |
| var procedures = library.procedures |
| .where((p) => !p.isExternal && !p.isAbstract) |
| .toList(); |
| _moduleItems.addAll(procedures |
| .where((p) => !p.isAccessor) |
| .map(_emitLibraryFunction) |
| .toList()); |
| _emitLibraryAccessors(procedures.where((p) => p.isAccessor).toList()); |
| } |
| |
| void _emitLibraryAccessors(Iterable<Procedure> accessors) { |
| if (accessors.isEmpty) return; |
| _moduleItems.add(_callHelperStatement('copyProperties(#, { # });', [ |
| emitLibraryName(_currentLibrary), |
| accessors.map(_emitLibraryAccessor).toList() |
| ])); |
| } |
| |
| JS.Method _emitLibraryAccessor(Procedure node) { |
| var savedUri = _currentUri; |
| _currentUri = node.fileUri; |
| |
| var name = node.name.name; |
| var result = new JS.Method( |
| _propertyName(name), _emitFunction(node.function, node.name.name), |
| isGetter: node.isGetter, isSetter: node.isSetter) |
| ..sourceInformation = _nodeEnd(node.fileEndOffset); |
| |
| _currentUri = savedUri; |
| return result; |
| } |
| |
| JS.Statement _emitLibraryFunction(Procedure p) { |
| var savedUri = _currentUri; |
| _currentUri = p.fileUri; |
| |
| var body = <JS.Statement>[]; |
| var fn = _emitFunction(p.function, p.name.name) |
| ..sourceInformation = _nodeEnd(p.fileEndOffset); |
| |
| if (_currentLibrary.importUri.scheme == 'dart' && |
| _isInlineJSFunction(p.function.body)) { |
| fn = JS.simplifyPassThroughArrowFunCallBody(fn); |
| } |
| |
| var nameExpr = _emitTopLevelName(p); |
| body.add(js.statement('# = #', [nameExpr, fn])); |
| if (!isSdkInternalRuntime(_currentLibrary)) { |
| body.add( |
| _emitFunctionTagged(nameExpr, p.function.functionType, topLevel: true) |
| .toStatement()); |
| } |
| |
| _currentUri = savedUri; |
| return JS.Statement.from(body); |
| } |
| |
| JS.Expression _emitFunctionTagged(JS.Expression fn, FunctionType type, |
| {bool topLevel: false}) { |
| var lazy = topLevel && !_typeIsLoaded(type); |
| var typeRep = visitFunctionType(type); |
| return _callHelper(lazy ? 'lazyFn(#, () => #)' : 'fn(#, #)', [fn, typeRep]); |
| } |
| |
| bool _typeIsLoaded(DartType type) { |
| if (type is InterfaceType) { |
| return !_pendingClasses.contains(type.classNode) && |
| type.typeArguments.every(_typeIsLoaded); |
| } |
| if (type is FunctionType) { |
| return (_typeIsLoaded(type.returnType) && |
| type.positionalParameters.every(_typeIsLoaded) && |
| type.namedParameters.every((n) => _typeIsLoaded(n.type))); |
| } |
| if (type is TypedefType) { |
| return type.typeArguments.every(_typeIsLoaded); |
| } |
| return true; |
| } |
| |
| /// Emits a Dart [type] into code. |
| JS.Expression _emitType(DartType type) => type.accept(this) as JS.Expression; |
| |
| JS.Expression _emitInvalidNode(Node node, [String message = '']) { |
| if (message.isNotEmpty) message += ' '; |
| return _callHelper('throwUnimplementedError(#)', |
| [js.escapedString('node <${node.runtimeType}> $message`$node`')]); |
| } |
| |
| JS.Expression _nameType(DartType type, JS.Expression typeRep) => |
| _currentFunction != null ? _typeTable.nameType(type, typeRep) : typeRep; |
| |
| @override |
| defaultDartType(type) => _emitInvalidNode(type); |
| |
| @override |
| visitInvalidType(type) => defaultDartType(type); |
| |
| @override |
| visitDynamicType(type) => _callHelper('dynamic'); |
| |
| @override |
| visitVoidType(type) => _callHelper('void'); |
| |
| @override |
| visitBottomType(type) => _callHelper('bottom'); |
| |
| @override |
| visitInterfaceType(type, {bool lowerGeneric: false}) { |
| var c = type.classNode; |
| _declareBeforeUse(c); |
| |
| // Type parameters don't matter as JS interop types cannot be reified. |
| // We have to use lazy JS types because until we have proper module |
| // loading for JS libraries bundled with Dart libraries, we will sometimes |
| // need to load Dart libraries before the corresponding JS libraries are |
| // actually loaded. |
| // Given a JS type such as: |
| // @JS('google.maps.Location') |
| // class Location { ... } |
| // We can't emit a reference to MyType because the JS library that defines |
| // it may be loaded after our code. So for now, we use a special lazy type |
| // object to represent MyType. |
| // Anonymous JS types do not have a corresponding concrete JS type so we |
| // have to use a helper to define them. |
| if (isJSAnonymousType(c)) { |
| return _callHelper( |
| 'anonymousJSType(#)', js.escapedString(getLocalClassName(c))); |
| } |
| var jsName = _getJSNameWithoutGlobal(c); |
| if (jsName != null) { |
| return _callHelper('lazyJSType(() => #, #)', |
| [_emitJSInteropForGlobal(jsName), js.escapedString(jsName)]); |
| } |
| |
| var args = type.typeArguments; |
| Iterable<JS.Expression> jsArgs = null; |
| if (args.any((a) => a != const DynamicType())) { |
| jsArgs = args.map(_emitType); |
| } else if (lowerGeneric) { |
| jsArgs = []; |
| } |
| if (jsArgs != null) { |
| return _nameType(type, _emitGenericClassType(type, jsArgs)); |
| } |
| |
| return _emitTopLevelNameNoInterop(type.classNode); |
| } |
| |
| JS.Expression _emitGenericClassType( |
| InterfaceType t, Iterable<JS.Expression> typeArgs) { |
| var genericName = _emitTopLevelNameNoInterop(t.classNode, suffix: '\$'); |
| return js.call('#(#)', [genericName, typeArgs]); |
| } |
| |
| @override |
| visitVectorType(type) => defaultDartType(type); |
| |
| @override |
| visitFunctionType(type, {bool lowerTypedef: false, FunctionNode function}) { |
| var requiredTypes = |
| type.positionalParameters.take(type.requiredParameterCount).toList(); |
| var requiredParams = function?.positionalParameters |
| ?.take(type.requiredParameterCount) |
| ?.toList(); |
| var optionalTypes = |
| type.positionalParameters.skip(type.requiredParameterCount).toList(); |
| var optionalParams = function?.positionalParameters |
| ?.skip(type.requiredParameterCount) |
| ?.toList(); |
| |
| var namedTypes = type.namedParameters; |
| var rt = _emitType(type.returnType); |
| var ra = _emitTypeNames(requiredTypes, requiredParams); |
| |
| List<JS.Expression> typeParts; |
| if (namedTypes.isNotEmpty) { |
| assert(optionalTypes.isEmpty); |
| // TODO(vsm): Pass in annotations here as well. |
| var na = _emitTypeProperties(namedTypes); |
| typeParts = [rt, ra, na]; |
| } else if (optionalTypes.isNotEmpty) { |
| assert(namedTypes.isEmpty); |
| var oa = _emitTypeNames(optionalTypes, optionalParams); |
| typeParts = [rt, ra, oa]; |
| } else { |
| typeParts = [rt, ra]; |
| } |
| |
| var typeFormals = type.typeParameters; |
| String helperCall; |
| if (typeFormals.isNotEmpty) { |
| var tf = _emitTypeFormals(typeFormals); |
| |
| addTypeFormalsAsParameters(List<JS.Expression> elements) { |
| var names = _typeTable.discharge(typeFormals); |
| return names.isEmpty |
| ? js.call('(#) => [#]', [tf, elements]) |
| : js.call('(#) => {#; return [#];}', [tf, names, elements]); |
| } |
| |
| typeParts = [addTypeFormalsAsParameters(typeParts)]; |
| |
| helperCall = 'gFnType(#)'; |
| // If any explicit bounds were passed, emit them. |
| if (typeFormals.any((t) => t.bound != null)) { |
| var bounds = typeFormals.map((t) => _emitType(t.bound)).toList(); |
| typeParts.add(addTypeFormalsAsParameters(bounds)); |
| } |
| } else { |
| helperCall = 'fnType(#)'; |
| } |
| return _nameType(type, _callHelper(helperCall, [typeParts])); |
| } |
| |
| JS.Expression _emitAnnotatedFunctionType( |
| FunctionType type, List<Expression> metadata, |
| {FunctionNode function, bool nameType: true, bool definite: false}) { |
| var result = visitFunctionType(type, function: function); |
| return _emitAnnotatedResult(result, metadata); |
| } |
| |
| /// Emits an expression that lets you access statics on a [type] from code. |
| JS.Expression _emitConstructorAccess(InterfaceType type) { |
| return _emitJSInterop(type.classNode) ?? _emitType(type); |
| } |
| |
| JS.Expression _emitConstructorName(InterfaceType type, Member c) { |
| return _emitJSInterop(type.classNode) ?? |
| new JS.PropertyAccess( |
| _emitConstructorAccess(type), _constructorName(c.name.name)); |
| } |
| |
| /// Emits an expression that lets you access statics on an [element] from code. |
| JS.Expression _emitStaticAccess(Class c) { |
| _declareBeforeUse(c); |
| return _emitTopLevelName(c); |
| } |
| |
| // Wrap a result - usually a type - with its metadata. The runtime is |
| // responsible for unpacking this. |
| JS.Expression _emitAnnotatedResult( |
| JS.Expression result, List<Expression> metadata) { |
| if (emitMetadata && metadata != null && metadata.isNotEmpty) { |
| result = new JS.ArrayInitializer( |
| [result]..addAll(metadata.map(_instantiateAnnotation))); |
| } |
| return result; |
| } |
| |
| JS.ObjectInitializer _emitTypeProperties(Iterable<NamedType> types) { |
| return new JS.ObjectInitializer(types |
| .map((t) => new JS.Property(_propertyName(t.name), _emitType(t.type))) |
| .toList()); |
| } |
| |
| JS.ArrayInitializer _emitTypeNames( |
| List<DartType> types, List<VariableDeclaration> parameters) { |
| var result = <JS.Expression>[]; |
| for (int i = 0; i < types.length; ++i) { |
| var metadata = parameters != null ? parameters[i].annotations : null; |
| result.add(_emitAnnotatedResult(_emitType(types[i]), metadata)); |
| } |
| return new JS.ArrayInitializer(result); |
| } |
| |
| @override |
| visitTypeParameterType(type) => _emitTypeParameter(type.parameter); |
| |
| JS.Identifier _emitTypeParameter(TypeParameter t) { |
| _typeParamInConst?.add(t); |
| return new JS.Identifier(getTypeParameterName(t)); |
| } |
| |
| @override |
| visitTypedefType(type, {bool lowerGeneric: false}) { |
| var args = type.typeArguments; |
| List<JS.Expression> jsArgs = null; |
| if (args.any((a) => a != const DynamicType())) { |
| jsArgs = args.map(_emitType).toList(); |
| } else if (lowerGeneric) { |
| jsArgs = []; |
| } |
| if (jsArgs != null) { |
| var genericName = |
| _emitTopLevelNameNoInterop(type.typedefNode, suffix: '\$'); |
| return _nameType(type, new JS.Call(genericName, jsArgs)); |
| } |
| |
| return _emitTopLevelNameNoInterop(type.typedefNode); |
| } |
| |
| JS.Fun _emitFunction(FunctionNode f, String name) { |
| // normal function (sync), vs (sync*, async, async*) |
| var isSync = f.asyncMarker == AsyncMarker.Sync; |
| var formals = _emitFormalParameters(f); |
| var typeFormals = _emitTypeFormals(f.typeParameters); |
| formals.insertAll(0, typeFormals); |
| |
| JS.Block code = isSync |
| ? _emitFunctionBody(f) |
| : new JS.Block([ |
| _emitGeneratorFunction(f, name).toReturn() |
| ..sourceInformation = _nodeStart(f) |
| ]); |
| |
| if (name != null && formals.isNotEmpty) { |
| if (name == '[]=') { |
| // []= methods need to return the value. We could also address this at |
| // call sites, but it's cleaner to instead transform the operator method. |
| code = JS.alwaysReturnLastParameter(code, formals.last); |
| } else if (name == '==' && _currentLibrary.importUri.scheme != 'dart') { |
| // In Dart `operator ==` methods are not called with a null argument. |
| // This is handled before calling them. For performance reasons, we push |
| // this check inside the method, to simplify our `equals` helper. |
| // |
| // TODO(jmesserly): in most cases this check is not necessary, because |
| // the Dart code already handles it (typically by an `is` check). |
| // Eliminate it when possible. |
| code = new JS.Block([ |
| js.statement('if (# == null) return false;', [formals.first]), |
| code |
| ]); |
| } |
| } |
| |
| return new JS.Fun(formals, code); |
| } |
| |
| // TODO(jmesserly): rename _emitParameters |
| List<JS.Parameter> _emitFormalParameters(FunctionNode f) { |
| var result = f.positionalParameters.map(_emitVariableDef).toList(); |
| if (f.namedParameters.isNotEmpty) result.add(namedArgumentTemp); |
| return result; |
| } |
| |
| void _emitVirtualFieldSymbols(Class c, List<JS.Statement> body) { |
| _classProperties.virtualFields.forEach((field, virtualField) { |
| body.add(js.statement('const # = Symbol(#);', [ |
| virtualField, |
| js.string('${getLocalClassName(c)}.${field.name.name}') |
| ])); |
| }); |
| } |
| |
| List<JS.Identifier> _emitTypeFormals(List<TypeParameter> typeFormals) { |
| return typeFormals |
| .map((t) => new JS.Identifier(getTypeParameterName(t))) |
| .toList(); |
| } |
| |
| JS.Expression _emitGeneratorFunction(FunctionNode function, String name) { |
| // Transforms `sync*` `async` and `async*` function bodies |
| // using ES6 generators. |
| |
| emitGeneratorFn(List<JS.Parameter> getParameters(JS.Block jsBody)) { |
| var savedController = _asyncStarController; |
| _asyncStarController = function.asyncMarker == AsyncMarker.AsyncStar |
| ? new JS.TemporaryId('stream') |
| : null; |
| |
| JS.Expression gen; |
| _superDisallowed(() { |
| // Visit the body with our async* controller set. |
| // |
| // TODO(jmesserly): this will emit argument initializers (for default |
| // values) inside the generator function body. Is that the best place? |
| var jsBody = _emitFunctionBody(function); |
| var genFn = |
| new JS.Fun(getParameters(jsBody), jsBody, isGenerator: true); |
| |
| // Name the function if possible, to get better stack traces. |
| gen = genFn; |
| if (name != null) { |
| gen = new JS.NamedFunction( |
| new JS.TemporaryId(JS.friendlyNameForDartOperator[name] ?? name), |
| genFn); |
| } |
| |
| gen.sourceInformation = _nodeEnd(function.fileEndOffset); |
| if (JS.This.foundIn(gen)) gen = js.call('#.bind(this)', gen); |
| }); |
| |
| _asyncStarController = savedController; |
| return gen; |
| } |
| |
| if (function.asyncMarker == AsyncMarker.SyncStar) { |
| // `sync*` wraps a generator in a Dart Iterable<E>: |
| // |
| // function name(<args>) { |
| // return new SyncIterator<E>(() => (function* name(<mutated args>) { |
| // <body> |
| // }(<mutated args>)); |
| // } |
| // |
| // In the body of a `sync*`, `yield` is generated simply as `yield`. |
| // |
| // We need to include all <mutated args> as parameters of the generator, |
| // so each `.iterator` starts with the same initial values. |
| // |
| // We also need to ensure the correct `this` is available. |
| // |
| // In the future, we might be able to simplify this, see: |
| // https://github.com/dart-lang/sdk/issues/28320 |
| var jsParams = _emitFormalParameters(function); |
| var gen = emitGeneratorFn((fnBody) => jsParams = |
| jsParams.where(JS.findMutatedVariables(fnBody).contains).toList()); |
| if (jsParams.isNotEmpty) gen = js.call('() => #(#)', [gen, jsParams]); |
| |
| var returnType = |
| _getExpectedReturnType(function, coreTypes.iterableClass); |
| var syncIterable = |
| _emitType(new InterfaceType(syncIterableClass, [returnType])); |
| return js.call('new #.new(#)', [syncIterable, gen]); |
| } |
| |
| if (function.asyncMarker == AsyncMarker.AsyncStar) { |
| // `async*` uses the `dart.asyncStar` helper, and also has an extra |
| // `stream` parameter to the generator, which is used for passing values |
| // to the `_AsyncStarStreamController` implementation type. |
| // |
| // `yield` is specially generated inside `async*` by visitYieldStatement. |
| // `await` is generated as `yield`. |
| // |
| // dart:_runtime/generators.dart has an example of the generated code. |
| var gen = emitGeneratorFn((_) => [_asyncStarController]); |
| |
| var returnType = _getExpectedReturnType(function, coreTypes.streamClass); |
| return _callHelper('asyncStar(#, #)', [_emitType(returnType), gen]); |
| } |
| |
| assert(function.asyncMarker == AsyncMarker.Async); |
| |
| // `async` works similar to `sync*`: |
| // |
| // function name(<args>) { |
| // return async.async(E, function* name() { |
| // <body> |
| // }); |
| // } |
| // |
| // In the body of an `async`, `await` is generated simply as `yield`. |
| var gen = emitGeneratorFn((_) => []); |
| // Return type of an async body is `Future<flatten(T)>`, where T is the |
| // declared return type. |
| var returnType = types.unfutureType(function.functionType.returnType); |
| return js.call('#.async(#, #)', |
| [emitLibraryName(coreTypes.asyncLibrary), _emitType(returnType), gen]); |
| } |
| |
| /// Gets the expected return type of a `sync*` or `async*` body. |
| DartType _getExpectedReturnType(FunctionNode f, Class expected) { |
| var type = f.functionType.returnType; |
| if (type is InterfaceType) { |
| var match = hierarchy.getTypeAsInstanceOf(type, expected); |
| if (match != null) return match.typeArguments[0]; |
| } |
| return const DynamicType(); |
| } |
| |
| JS.Block _emitFunctionBody(FunctionNode f) { |
| var block = _withCurrentFunction(f, () { |
| var block = _emitArgumentInitializers(f); |
| block.add(_emitFunctionScopedBody(f)); |
| return block; |
| }); |
| |
| return new JS.Block(block); |
| } |
| |
| List<JS.Statement> _withCurrentFunction( |
| FunctionNode fn, List<JS.Statement> action()) { |
| var savedFunction = _currentFunction; |
| _currentFunction = fn; |
| _nullableInference.enterFunction(fn); |
| |
| var result = _withLetScope(action); |
| |
| _nullableInference.exitFunction(fn); |
| _currentFunction = savedFunction; |
| return result; |
| } |
| |
| T _superDisallowed<T>(T action()) { |
| var savedSuperAllowed = _superAllowed; |
| _superAllowed = false; |
| var result = action(); |
| _superAllowed = savedSuperAllowed; |
| return result; |
| } |
| |
| /// Emits argument initializers, which handles optional/named args, as well |
| /// as generic type checks needed due to our covariance. |
| List<JS.Statement> _emitArgumentInitializers(FunctionNode f) { |
| var body = <JS.Statement>[]; |
| |
| _emitCovarianceBoundsCheck(f.typeParameters, body); |
| |
| initParameter(VariableDeclaration p, JS.Identifier jsParam) { |
| if (isCovariant(p)) { |
| var castType = _emitType(p.type); |
| body.add(js.statement('#._check(#);', [castType, jsParam])); |
| } |
| if (_annotatedNullCheck(p.annotations)) { |
| body.add(_nullParameterCheck(jsParam)); |
| } |
| } |
| |
| for (var p in f.positionalParameters.take(f.requiredParameterCount)) { |
| var jsParam = new JS.Identifier(p.name); |
| initParameter(p, jsParam); |
| } |
| for (var p in f.positionalParameters.skip(f.requiredParameterCount)) { |
| var jsParam = new JS.Identifier(p.name); |
| var defaultValue = _defaultParamValue(p); |
| if (defaultValue != null) { |
| body.add(js.statement( |
| 'if (# === void 0) # = #;', [jsParam, jsParam, defaultValue])); |
| } |
| initParameter(p, jsParam); |
| } |
| for (var p in f.namedParameters) { |
| // Parameters will be passed using their real names, not the (possibly |
| // renamed) local variable. |
| var jsParam = _emitVariableDef(p); |
| var paramName = js.string(p.name, "'"); |
| var defaultValue = _defaultParamValue(p); |
| if (defaultValue != null) { |
| // TODO(ochafik): Fix `'prop' in obj` to please Closure's renaming. |
| body.add(js.statement('let # = # && # in # ? #.# : #;', [ |
| jsParam, |
| namedArgumentTemp, |
| paramName, |
| namedArgumentTemp, |
| namedArgumentTemp, |
| paramName, |
| defaultValue, |
| ])); |
| } else { |
| body.add(js.statement('let # = # && #.#;', [ |
| jsParam, |
| namedArgumentTemp, |
| namedArgumentTemp, |
| paramName, |
| ])); |
| } |
| initParameter(p, jsParam); |
| } |
| return body; |
| } |
| |
| bool _annotatedNullCheck(List<Expression> annotations) => |
| annotations.any(_nullableInference.isNullCheckAnnotation); |
| |
| JS.Statement _nullParameterCheck(JS.Expression param) { |
| var call = _callHelper('argumentError((#))', [param]); |
| return js.statement('if (# == null) #;', [param, call]); |
| } |
| |
| JS.Expression _defaultParamValue(VariableDeclaration p) { |
| if (p.initializer != null) { |
| var value = p.initializer; |
| return _isJSUndefined(value) ? null : _visitExpression(value); |
| } else { |
| return new JS.LiteralNull(); |
| } |
| } |
| |
| bool _isJSUndefined(Expression expr) { |
| expr = expr is AsExpression ? expr.operand : expr; |
| if (expr is StaticGet) { |
| var t = expr.target; |
| return isSdkInternalRuntime(getLibrary(t)) && t.name.name == 'undefined'; |
| } |
| return false; |
| } |
| |
| void _emitCovarianceBoundsCheck( |
| List<TypeParameter> typeFormals, List<JS.Statement> body) { |
| for (var t in typeFormals) { |
| if (t.isGenericCovariantImpl) { |
| body.add(_callHelperStatement('checkTypeBound(#, #, #)', [ |
| _emitType(new TypeParameterType(t)), |
| _emitType(t.bound), |
| _propertyName(t.name) |
| ])); |
| } |
| } |
| } |
| |
| JS.LiteralString _emitDynamicOperationName(String name) => |
| js.string(replCompile ? '${name}Repl' : name); |
| |
| JS.Expression _callHelper(String code, [args]) { |
| if (args is List) { |
| args.insert(0, _runtimeModule); |
| } else if (args != null) { |
| args = [_runtimeModule, args]; |
| } else { |
| args = _runtimeModule; |
| } |
| return js.call('#.$code', args); |
| } |
| |
| JS.Statement _callHelperStatement(String code, args) { |
| if (args is List) { |
| args.insert(0, _runtimeModule); |
| } else { |
| args = [_runtimeModule, args]; |
| } |
| return js.statement('#.$code', args); |
| } |
| |
| JS.Statement _visitStatement(Statement s) { |
| if (s == null) return null; |
| var result = s.accept(this) as JS.Statement; |
| // TODO(jmesserly): is the `is! Block` still necessary? |
| if (s is! Block) result.sourceInformation = _nodeStart(s); |
| |
| // The statement might be the target of a break or continue with a label. |
| var name = _labelNames[s]; |
| if (name != null) result = new JS.LabeledStatement(name, result); |
| return result; |
| } |
| |
| JS.Statement _emitFunctionScopedBody(FunctionNode f) { |
| var jsBody = _visitStatement(f.body); |
| if (f.asyncMarker == AsyncMarker.Sync) { |
| // Handle shadowing of parameters by local varaibles, which is allowed in |
| // Dart but not in JS. |
| // |
| // We only handle this for normal (sync) functions. Generator-based |
| // functions (sync*, async, and async*) have their bodies placed |
| // in an inner function scope that is a separate scope from the |
| // parameters, so they avoid this problem. |
| var parameterNames = new HashSet<String>() |
| ..addAll(f.positionalParameters.map((p) => p.name)) |
| ..addAll(f.namedParameters.map((p) => p.name)); |
| |
| return jsBody.toScopedBlock(parameterNames); |
| } |
| return jsBody; |
| } |
| |
| /// Visits [nodes] with [_visitExpression]. |
| List<JS.Expression> _visitExpressionList(Iterable<Expression> nodes) { |
| return nodes?.map(_visitExpression)?.toList(); |
| } |
| |
| /// Generates an expression for a boolean conversion context (if, while, &&, |
| /// etc.), where conversions and null checks are implemented via `dart.test` |
| /// to give a more helpful message. |
| // TODO(sra): When nullablility is available earlier, it would be cleaner to |
| // build an input AST where the boolean conversion is a single AST node. |
| JS.Expression _visitTest(Expression node) { |
| if (node == null) return null; |
| |
| if (node is Not) { |
| // TODO(leafp): consider a peephole opt for identical |
| // and == here. |
| return js.call('!#', _visitTest(node.operand)); |
| } |
| if (node is LogicalExpression) { |
| JS.Expression shortCircuit(String code) { |
| return js.call(code, [_visitTest(node.left), _visitTest(node.right)]); |
| } |
| |
| var op = node.operator; |
| if (op == '&&') return shortCircuit('# && #'); |
| if (op == '||') return shortCircuit('# || #'); |
| } |
| |
| if (node is AsExpression && node.isTypeError) { |
| assert(node.getStaticType(types) == types.boolType); |
| return _callHelper('dtest(#)', _visitExpression(node.operand)); |
| } |
| |
| var result = _visitExpression(node); |
| if (isNullable(node)) result = _callHelper('test(#)', result); |
| return result; |
| } |
| |
| JS.Expression _visitExpression(Expression e) { |
| if (e == null) return null; |
| var result = e.accept(this) as JS.Expression; |
| result.sourceInformation ??= _nodeStart(e); |
| return result; |
| } |
| |
| /// Gets the start position of [node] for use in source mapping. |
| /// |
| /// This is the most common kind of marking, and is used for most expressions |
| /// and statements. |
| SourceLocation _nodeStart(TreeNode node) => _getLocation(node.fileOffset); |
| |
| /// Gets the end position of [node] for use in source mapping. |
| /// |
| /// This is mainly used for things that compile to JS functions. JS wants a |
| /// marking on the end of all functions for stepping purposes. |
| /// |
| /// This can be used to complete a hover span, when we know the start position |
| /// has already been emitted. For example, `foo.bar` we only need to mark the |
| /// end of `.bar` to ensure `foo.bar` has a hover tooltip. |
| NodeEnd _nodeEnd(int endOffset) { |
| var loc = _getLocation(endOffset); |
| return loc != null ? new NodeEnd(loc) : null; |
| } |
| |
| /// Combines [_nodeStart] with the variable name length to produce a hoverable |
| /// span for the varaible. |
| // |
| // TODO(jmesserly): we need a lot more nodes to support hover. |
| NodeSpan _variableSpan(int offset, int nameLength) { |
| var start = _getLocation(offset); |
| var end = _getLocation(offset + nameLength); |
| return start != null && end != null ? new NodeSpan(start, end) : null; |
| } |
| |
| SourceLocation _getLocation(int offset) { |
| if (offset == -1) return null; |
| var fileUri = _currentUri; |
| if (fileUri == null) return null; |
| var loc = _program.getLocation(fileUri, offset); |
| if (loc == null) return null; |
| return new SourceLocation(offset, |
| sourceUrl: fileUri, line: loc.line - 1, column: loc.column - 1); |
| } |
| |
| /// Adds a hover comment for Dart node using JS expression [expr], where |
| /// that expression would not otherwise not be generated into source code. |
| /// |
| /// For example, top-level and static fields are defined as lazy properties, |
| /// on the library/class, so their access expressions do not appear in the |
| /// source code. |
| HoverComment _hoverComment(JS.Expression expr, int offset, int nameLength) { |
| var start = _getLocation(offset); |
| var end = _getLocation(offset + nameLength); |
| return start != null && end != null |
| ? new HoverComment(expr, start, end) |
| : null; |
| } |
| |
| @override |
| defaultStatement(Statement node) => _emitInvalidNode(node).toStatement(); |
| |
| @override |
| visitExpressionStatement(ExpressionStatement node) { |
| var expr = node.expression; |
| if (expr is StaticInvocation && isInlineJS(expr.target)) { |
| var inlineJS = _emitInlineJSCode(expr); |
| return inlineJS is JS.Statement ? inlineJS : inlineJS.toStatement(); |
| } |
| return _visitExpression(expr).toStatement(); |
| } |
| |
| @override |
| visitBlock(Block node) => |
| new JS.Block(node.statements.map(_visitStatement).toList(), |
| isScope: true); |
| |
| @override |
| visitEmptyStatement(EmptyStatement node) => new JS.EmptyStatement(); |
| |
| @override |
| visitAssertStatement(AssertStatement node) { |
| // TODO(jmesserly): only emit in checked mode. |
| var condition = node.condition; |
| var conditionType = condition.getStaticType(types); |
| var jsCondition = _visitExpression(condition); |
| |
| var boolType = coreTypes.boolClass.rawType; |
| if (conditionType is FunctionType && |
| conditionType.requiredParameterCount == 0 && |
| conditionType.returnType == boolType) { |
| jsCondition = _callHelper('test(#())', jsCondition); |
| } else if (conditionType != boolType) { |
| jsCondition = _callHelper('dassert(#)', jsCondition); |
| } else if (isNullable(condition)) { |
| jsCondition = _callHelper('test(#)', jsCondition); |
| } |
| return js.statement(' if (!#) #.assertFailed(#);', [ |
| jsCondition, |
| _runtimeModule, |
| node.message != null ? [_visitExpression(node.message)] : [] |
| ]); |
| } |
| |
| static bool isBreakable(Statement stmt) { |
| // These are conservatively the things that compile to things that can be |
| // the target of a break without a label. |
| return stmt is ForStatement || |
| stmt is WhileStatement || |
| stmt is DoStatement || |
| stmt is ForInStatement || |
| stmt is SwitchStatement; |
| } |
| |
| @override |
| visitLabeledStatement(LabeledStatement node) { |
| List<LabeledStatement> saved; |
| var target = _effectiveTargets[node]; |
| // If the effective target is known then this statement is either contained |
| // in a labeled statement or a loop. It has already been processed when |
| // the enclosing statement was visited. |
| if (target == null) { |
| // Find the effective target by bypassing and collecting labeled |
| // statements. |
| var statements = [node]; |
| target = node.body; |
| while (target is LabeledStatement) { |
| var labeled = target as LabeledStatement; |
| statements.add(labeled); |
| target = labeled.body; |
| } |
| for (var statement in statements) _effectiveTargets[statement] = target; |
| |
| // If the effective target will compile to something that can have a |
| // break from it without a label (e.g., a loop but not a block), then any |
| // of the labeled statements can have a break from them by breaking from |
| // the effective target. Otherwise breaks will need a label and a break |
| // without a label can still target an outer breakable so the list of |
| // current break targets does not change. |
| if (isBreakable(target)) { |
| saved = _currentBreakTargets; |
| _currentBreakTargets = statements; |
| } |
| } |
| |
| var result = _visitStatement(node.body); |
| if (saved != null) _currentBreakTargets = saved; |
| return result; |
| } |
| |
| @override |
| visitBreakStatement(BreakStatement node) { |
| // Can it be compiled to a break without a label? |
| if (_currentBreakTargets.contains(node.target)) { |
| return new JS.Break(null); |
| } |
| // Can it be compiled to a continue without a label? |
| if (_currentContinueTargets.contains(node.target)) { |
| return new JS.Continue(null); |
| } |
| |
| // Ensure the effective target is labeled. Labels are named globally per |
| // Kernel binary. |
| // |
| // TODO(kmillikin): Preserve Dart label names in Kernel and here. |
| var target = _effectiveTargets[node.target]; |
| var name = _labelNames[target]; |
| if (name == null) _labelNames[target] = name = 'L${_labelNames.length}'; |
| |
| // It is a break if the target labeled statement encloses the effective |
| // target. |
| Statement current = node.target; |
| while (current is LabeledStatement) { |
| current = (current as LabeledStatement).body; |
| } |
| if (identical(current, target)) { |
| return new JS.Break(name); |
| } |
| // Otherwise it is a continue. |
| return new JS.Continue(name); |
| } |
| |
| // Labeled loop bodies can be the target of a continue without a label |
| // (targeting the loop). Find the outermost non-labeled statement starting |
| // from body and record all the intermediate labeled statements as continue |
| // targets. |
| Statement effectiveBodyOf(Statement loop, Statement body) { |
| // In a loop whose body is not labeled, this list should be empty because |
| // it is not possible to continue to an outer loop without a label. |
| _currentContinueTargets = <LabeledStatement>[]; |
| while (body is LabeledStatement) { |
| var labeled = body as LabeledStatement; |
| _currentContinueTargets.add(labeled); |
| _effectiveTargets[labeled] = loop; |
| body = labeled.body; |
| } |
| return body; |
| } |
| |
| T translateLoop<T extends JS.Statement>(Statement node, T action()) { |
| List<LabeledStatement> savedBreakTargets; |
| if (_currentBreakTargets.isNotEmpty && |
| _effectiveTargets[_currentBreakTargets.first] != node) { |
| // If breaking without a label targets some other (outer) loop, then |
| // this loop prevents breaking to that loop without a label. This loop |
| // was not labeled for a break in Kernel, otherwise it would be the |
| // effective target of the current break targets, so it is not itself the |
| // target of a break. |
| savedBreakTargets = _currentBreakTargets; |
| _currentBreakTargets = <LabeledStatement>[]; |
| } |
| var savedContinueTargets = _currentContinueTargets; |
| var result = action(); |
| if (savedBreakTargets != null) _currentBreakTargets = savedBreakTargets; |
| _currentContinueTargets = savedContinueTargets; |
| return result; |
| } |
| |
| @override |
| JS.While visitWhileStatement(WhileStatement node) { |
| return translateLoop(node, () { |
| var condition = _visitTest(node.condition); |
| var body = _visitScope(effectiveBodyOf(node, node.body)); |
| return new JS.While(condition, body); |
| }); |
| } |
| |
| @override |
| JS.Do visitDoStatement(DoStatement node) { |
| return translateLoop(node, () { |
| var body = _visitScope(effectiveBodyOf(node, node.body)); |
| var condition = _visitTest(node.condition); |
| return new JS.Do(body, condition); |
| }); |
| } |
| |
| @override |
| JS.For visitForStatement(ForStatement node) { |
| return translateLoop(node, () { |
| emitForInitializer(VariableDeclaration v) => |
| new JS.VariableInitialization(_emitVariableDef(v), |
| _visitInitializer(v.initializer, v.annotations)); |
| |
| var init = node.variables.map(emitForInitializer).toList(); |
| var initList = |
| init.isEmpty ? null : new JS.VariableDeclarationList('let', init); |
| var updates = node.updates; |
| JS.Expression update; |
| if (updates.isNotEmpty) { |
| update = new JS.Expression.binary( |
| updates.map(_visitExpression).toList(), ',') |
| .toVoidExpression(); |
| } |
| var condition = _visitTest(node.condition); |
| var body = _visitScope(effectiveBodyOf(node, node.body)); |
| |
| return new JS.For(initList, condition, update, body); |
| }); |
| } |
| |
| @override |
| JS.Statement visitForInStatement(ForInStatement node) { |
| return translateLoop(node, () { |
| if (node.isAsync) { |
| return _emitAwaitFor(node); |
| } |
| |
| var iterable = _visitExpression(node.iterable); |
| var body = _visitScope(effectiveBodyOf(node, node.body)); |
| |
| var init = js.call('let #', _emitVariableDef(node.variable)); |
| if (_annotatedNullCheck(node.variable.annotations)) { |
| body = new JS.Block( |
| [_nullParameterCheck(_emitVariableRef(node.variable)), body]); |
| } |
| |
| return new JS.ForOf(init, iterable, body); |
| }); |
| } |
| |
| JS.Statement _emitAwaitFor(ForInStatement node) { |
| // Emits `await for (var value in stream) ...`, which desugars as: |
| // |
| // var iter = new StreamIterator(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 streamIterator = _asyncStreamIteratorClass.rawType; |
| var createStreamIter = new JS.Call( |
| _emitConstructorName( |
| streamIterator, |
| _asyncStreamIteratorClass.procedures |
| .firstWhere((p) => p.isFactory && p.name.name == '')), |
| [_visitExpression(node.iterable)]); |
| |
| var iter = new JS.TemporaryId('iter'); |
| return js.statement( |
| '{' |
| ' let # = #;' |
| ' try {' |
| ' while (#) { let # = #.current; #; }' |
| ' } finally { #; }' |
| '}', |
| [ |
| iter, |
| createStreamIter, |
| new JS.Yield(js.call('#.moveNext()', iter)) |
| ..sourceInformation = _nodeStart(node.variable), |
| _emitVariableDef(node.variable), |
| iter, |
| _visitStatement(node.body), |
| new JS.Yield(js.call('#.cancel()', iter)) |
| ..sourceInformation = _nodeStart(node.variable) |
| ]); |
| } |
| |
| @override |
| visitSwitchStatement(SwitchStatement node) { |
| var cases = <JS.SwitchCase>[]; |
| var emptyBlock = new JS.Block.empty(); |
| for (var c in node.cases) { |
| // TODO(jmesserly): make sure we are statically checking fall through |
| var body = _visitStatement(c.body).toBlock(); |
| var expressions = c.expressions; |
| var last = |
| expressions.isNotEmpty && !c.isDefault ? expressions.last : null; |
| for (var e in expressions) { |
| var jsExpr = _visitExpression(e); |
| cases.add(new JS.SwitchCase(jsExpr, e == last ? body : emptyBlock)); |
| } |
| if (c.isDefault) cases.add(new JS.SwitchCase.defaultCase(body)); |
| } |
| |
| return new JS.Switch(_visitExpression(node.expression), cases); |
| } |
| |
| @override |
| visitContinueSwitchStatement(ContinueSwitchStatement node) { |
| SwitchCase switchCase; |
| for (Statement current = node;;) { |
| var parent = current.parent; |
| if (parent is Block && parent.statements.last == current) { |
| current = parent; |
| continue; |
| } |
| if (parent is SwitchCase) switchCase = parent; |
| break; |
| } |
| if (switchCase != null) { |
| var switchCases = (switchCase.parent as SwitchStatement).cases; |
| var fromIndex = switchCases.indexOf(switchCase); |
| var toIndex = switchCases.indexOf(node.target); |
| if (toIndex == fromIndex + 1) { |
| return new JS.Comment('continue to next case'); |
| } |
| } |
| return _emitInvalidNode( |
| node, 'see https://github.com/dart-lang/sdk/issues/29352') |
| .toStatement(); |
| } |
| |
| @override |
| visitIfStatement(IfStatement node) { |
| return new JS.If(_visitTest(node.condition), _visitScope(node.then), |
| _visitScope(node.otherwise)); |
| } |
| |
| /// 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 = _visitStatement(stmt); |
| if (result is JS.ExpressionStatement && |
| result.expression is JS.VariableDeclarationList) { |
| return new JS.Block([result]); |
| } |
| return result; |
| } |
| |
| @override |
| JS.Statement visitReturnStatement(ReturnStatement node) { |
| var e = node.expression; |
| if (e == null) return new JS.Return(); |
| return _visitExpression(e).toReturn(); |
| } |
| |
| @override |
| visitTryCatch(TryCatch node) { |
| return new JS.Try( |
| _visitStatement(node.body).toBlock(), _visitCatch(node.catches), null); |
| } |
| |
| JS.Catch _visitCatch(List<Catch> clauses) { |
| if (clauses.isEmpty) return null; |
| |
| var savedCatch = _catchParameter; |
| |
| if (clauses.length == 1 && clauses.single.exception != null) { |
| // Special case for a single catch. |
| _catchParameter = clauses.single.exception; |
| } else { |
| _catchParameter = new VariableDeclaration('#e'); |
| } |
| |
| JS.Statement catchBody = |
| js.statement('throw #;', _emitVariableRef(_catchParameter)); |
| for (var clause in clauses.reversed) { |
| catchBody = _catchClauseGuard(clause, catchBody); |
| } |
| |
| var catchVarDecl = _emitVariableRef(_catchParameter); |
| _catchParameter = savedCatch; |
| return new JS.Catch(catchVarDecl, catchBody.toBlock()); |
| } |
| |
| JS.Statement _catchClauseGuard(Catch node, JS.Statement otherwise) { |
| var body = <JS.Statement>[]; |
| |
| var savedCatch = _catchParameter; |
| var vars = new HashSet<String>(); |
| if (node.exception != null) { |
| var name = node.exception; |
| if (name == _catchParameter) { |
| vars.add(name.name); |
| } else if (name != null) { |
| vars.add(name.name); |
| body.add(js.statement('let # = #;', |
| [_emitVariableDef(name), _emitVariableRef(_catchParameter)])); |
| _catchParameter = name; |
| } |
| var stackTrace = node.stackTrace; |
| if (stackTrace != null) { |
| vars.add(stackTrace.name); |
| body.add(js.statement('let # = #.stackTrace(#);', [ |
| _emitVariableDef(stackTrace), |
| _runtimeModule, |
| _emitVariableRef(name) |
| ])); |
| } |
| } |
| |
| body.add(_visitStatement(node.body).toScopedBlock(vars)); |
| _catchParameter = savedCatch; |
| var then = new JS.Block(body); |
| |
| if (types.isTop(node.guard)) return then; |
| |
| // TODO(jmesserly): this is inconsistent with [visitIsExpression], which |
| // has special case for typeof. |
| return new JS.If( |
| js.call('#.is(#)', |
| [_emitType(node.guard), _emitVariableRef(_catchParameter)]), |
| then, |
| otherwise) |
| ..sourceInformation = _nodeStart(node); |
| } |
| |
| @override |
| visitTryFinally(TryFinally node) { |
| var body = _visitStatement(node.body); |
| var finallyBlock = |
| _superDisallowed(() => _visitStatement(node.finalizer).toBlock()); |
| |
| if (body is JS.Try && body.finallyPart == null) { |
| // Kernel represents Dart try/catch/finally as try/catch nested inside of |
| // try/finally. Flatten that pattern in the output into JS try/catch/ |
| // finally. |
| return new JS.Try(body.body, body.catchPart, finallyBlock); |
| } |
| return new JS.Try(body.toBlock(), null, finallyBlock); |
| } |
| |
| @override |
| visitYieldStatement(YieldStatement node) { |
| var jsExpr = _visitExpression(node.expression); |
| var star = node.isYieldStar; |
| 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)..sourceInformation = _nodeStart(node) |
| ]); |
| } |
| // A normal yield in a sync* |
| return jsExpr.toYieldStatement(star: star); |
| } |
| |
| @override |
| visitVariableDeclaration(VariableDeclaration node) { |
| // TODO(jmesserly): casts are sometimes required here. |
| // Kernel does not represent these explicitly. |
| var v = _emitVariableDef(node); |
| return js.statement('let # = #;', |
| [v, _visitInitializer(node.initializer, node.annotations)]); |
| } |
| |
| @override |
| visitFunctionDeclaration(FunctionDeclaration node) { |
| var func = node.function; |
| var fn = _emitFunction(func, node.variable.name); |
| |
| var name = _emitVariableDef(node.variable); |
| JS.Statement declareFn; |
| if (JS.This.foundIn(fn)) { |
| declareFn = js.statement('const # = #.bind(this);', [name, fn]); |
| } else { |
| declareFn = new JS.FunctionDeclaration(name, fn); |
| } |
| if (_reifyFunctionType(func)) { |
| declareFn = new JS.Block([ |
| declareFn, |
| _emitFunctionTagged(_emitVariableRef(node.variable), func.functionType) |
| .toStatement() |
| ]); |
| } |
| return declareFn; |
| } |
| |
| @override |
| defaultExpression(Expression node) => _emitInvalidNode(node); |
| |
| @override |
| defaultBasicLiteral(BasicLiteral node) => defaultExpression(node); |
| |
| @override |
| visitInvalidExpression(InvalidExpression node) => defaultExpression(node); |
| |
| // [ConstantExpression] is produced by the Kernel constant evaluator, which |
| // we do not use. |
| @override |
| visitConstantExpression(ConstantExpression node) => defaultExpression(node); |
| |
| @override |
| visitVariableGet(VariableGet node) { |
| var v = node.variable; |
| var id = _emitVariableRef(v); |
| if (id.name == v.name) { |
| id.sourceInformation = _variableSpan(node.fileOffset, v.name.length); |
| } |
| return id; |
| } |
| |
| JS.Identifier _emitVariableRef(VariableDeclaration v) { |
| var name = v.name; |
| if (name == null || name.startsWith('#')) { |
| name = name == null ? 't${_tempVariables.length}' : name.substring(1); |
| return _tempVariables.putIfAbsent(v, () => new JS.TemporaryId(name)); |
| } |
| return new JS.Identifier(name); |
| } |
| |
| /// Emits the declaration of a variable. |
| /// |
| /// This is similar to [_emitVariableRef] but it also attaches source |
| /// location information, so hover will work as expected. |
| JS.Identifier _emitVariableDef(VariableDeclaration v) { |
| return _emitVariableRef(v)..sourceInformation = _nodeStart(v); |
| } |
| |
| JS.Statement _initLetVariables() { |
| if (_letVariables.isEmpty) return null; |
| var result = new JS.VariableDeclarationList( |
| 'let', |
| _letVariables |
| .map((v) => new JS.VariableInitialization(v, null)) |
| .toList()) |
| .toStatement(); |
| _letVariables.clear(); |
| return result; |
| } |
| |
| // TODO(jmesserly): resugar operators for kernel, such as ++x, x++, x+=. |
| @override |
| visitVariableSet(VariableSet node) => _visitExpression(node.value) |
| .toAssignExpression(_emitVariableRef(node.variable)); |
| |
| @override |
| visitPropertyGet(PropertyGet node) { |
| return _emitPropertyGet( |
| node.receiver, node.interfaceTarget, node.name.name); |
| } |
| |
| @override |
| visitPropertySet(PropertySet node) { |
| return _emitPropertySet( |
| node.receiver, node.interfaceTarget, node.value, node.name.name); |
| } |
| |
| @override |
| visitDirectPropertyGet(DirectPropertyGet node) { |
| return _emitPropertyGet(node.receiver, node.target); |
| } |
| |
| @override |
| visitDirectPropertySet(DirectPropertySet node) { |
| return _emitPropertySet(node.receiver, node.target, node.value); |
| } |
| |
| JS.Expression _emitPropertyGet(Expression receiver, Member member, |
| [String memberName]) { |
| var jsName = _emitMemberName(memberName ?? member.name.name, |
| type: receiver.getStaticType(types), member: member); |
| var jsReceiver = _visitExpression(receiver); |
| |
| // TODO(jmesserly): we need to mark an end span for property accessors so |
| // they can be hovered. Unfortunately this is not possible as Kernel does |
| // not store this data. |
| if (member == null) { |
| return _callHelper( |
| '#(#, #)', [_emitDynamicOperationName('dload'), jsReceiver, jsName]); |
| } |
| |
| if (_isObjectMemberCall(receiver, memberName)) { |
| if (_isObjectMethod(memberName)) { |
| return _callHelper('bind(#, #)', [jsReceiver, jsName]); |
| } else { |
| return _callHelper('#(#)', [memberName, jsReceiver]); |
| } |
| } else if (member is Procedure && |
| !member.isAccessor && |
| !_isJSNative(member.enclosingClass)) { |
| return _callHelper('bind(#, #)', [jsReceiver, jsName]); |
| } else { |
| return new JS.PropertyAccess(jsReceiver, jsName); |
| } |
| } |
| |
| JS.Expression _emitPropertySet( |
| Expression receiver, Member member, Expression value, |
| [String memberName]) { |
| var jsName = _emitMemberName(memberName ?? member.name.name, |
| type: receiver.getStaticType(types), member: member); |
| |
| var jsReceiver = _visitExpression(receiver); |
| var jsValue = _visitExpression(value); |
| |
| if (member == null) { |
| return _callHelper('#(#, #, #)', |
| [_emitDynamicOperationName('dput'), jsReceiver, jsName, jsValue]); |
| } |
| return js.call('#.# = #', [jsReceiver, jsName, jsValue]); |
| } |
| |
| @override |
| visitSuperPropertyGet(SuperPropertyGet node) { |
| var target = node.interfaceTarget; |
| var jsTarget = _emitSuperTarget(target); |
| if (target is Procedure && |
| !target.isAccessor && |
| !_isJSNative(target.enclosingClass)) { |
| return _callHelper('bind(this, #, #)', [jsTarget.selector, jsTarget]); |
| } |
| return jsTarget; |
| } |
| |
| @override |
| visitSuperPropertySet(SuperPropertySet node) { |
| var target = node.interfaceTarget; |
| var jsTarget = _emitSuperTarget(target, setter: true); |
| return _visitExpression(node.value).toAssignExpression(jsTarget); |
| } |
| |
| @override |
| visitStaticGet(StaticGet node) { |
| return _emitStaticTarget(node.target); |
| } |
| |
| @override |
| visitStaticSet(StaticSet node) { |
| return _visitExpression(node.value) |
| .toAssignExpression(_emitStaticTarget(node.target)); |
| } |
| |
| @override |
| visitMethodInvocation(MethodInvocation node) { |
| return _emitMethodCall( |
| node.receiver, node.interfaceTarget, node.arguments, node); |
| } |
| |
| @override |
| visitDirectMethodInvocation(DirectMethodInvocation node) { |
| return _emitMethodCall(node.receiver, node.target, node.arguments, node); |
| } |
| |
| JS.Expression _emitMethodCall(Expression receiver, Member target, |
| Arguments arguments, InvocationExpression node) { |
| var name = node.name.name; |
| |
| if (isOperatorMethodName(name) && arguments.named.isEmpty) { |
| int argLength = arguments.positional.length; |
| if (argLength == 0) { |
| return _emitUnaryOperator(receiver, target, node); |
| } else if (argLength == 1) { |
| return _emitBinaryOperator( |
| receiver, target, arguments.positional[0], node); |
| } |
| } |
| |
| var jsReceiver = _visitExpression(receiver); |
| var args = _emitArgumentList(arguments); |
| var receiverType = receiver.getStaticType(types); |
| var typeArgs = arguments.types; |
| |
| isDynamicOrFunction(DartType t) => |
| t == coreTypes.functionClass.rawType || t == const DynamicType(); |
| bool isCallingDynamicField = target is Member && |
| target.hasGetter && |
| isDynamicOrFunction(target.getterType); |
| if (name == 'call') { |
| if (isCallingDynamicField || isDynamicOrFunction(receiverType)) { |
| if (typeArgs.isNotEmpty) { |
| return _callHelper('dgcall(#, [#], #)', [ |
| jsReceiver, |
| args.take(typeArgs.length), |
| args.skip(typeArgs.length) |
| ]); |
| } else { |
| return _callHelper('dcall(#, #)', [jsReceiver, args]); |
| } |
| } |
| |
| // Call methods on function types or interface types should be handled as |
| // regular function invocations. |
| return new JS.Call(jsReceiver, args); |
| } |
| |
| var jsName = _emitMemberName(name, type: receiverType, member: target); |
| if (target == null || isCallingDynamicField) { |
| if (typeArgs.isNotEmpty) { |
| return _callHelper('#(#, [#], #, #)', [ |
| _emitDynamicOperationName('dgsend'), |
| jsReceiver, |
| args.take(typeArgs.length), |
| jsName, |
| args.skip(typeArgs.length) |
| ]); |
| } else { |
| return _callHelper('#(#, #, #)', |
| [_emitDynamicOperationName('dsend'), jsReceiver, jsName, args]); |
| } |
| } |
| if (_isObjectMemberCall(receiver, name)) { |
| assert(typeArgs.isEmpty); // Object methods don't take type args. |
| return _callHelper('#(#, #)', [name, jsReceiver, args]); |
| } |
| return js.call('#.#(#)', [jsReceiver, jsName, args]); |
| } |
| |
| JS.Expression _emitUnaryOperator( |
| Expression expr, Member target, InvocationExpression node) { |
| var op = node.name.name; |
| var dispatchType = expr.getStaticType(types); |
| if (_typeRep.unaryOperationIsPrimitive(dispatchType)) { |
| if (op == '~') { |
| if (_typeRep.isNumber(dispatchType)) { |
| return _coerceBitOperationResultToUnsigned( |
| node, js.call('~#', notNull(expr))); |
| } |
| return _emitSend(expr, target, op, []); |
| } |
| if (op == 'unary-') op = '-'; |
| return js.call('$op#', notNull(expr)); |
| } |
| |
| return _emitSend(expr, target, op, []); |
| } |
| |
| /// Bit operations are coerced to values on [0, 2^32). The coercion changes |
| /// the interpretation of the 32-bit value from signed to unsigned. Most |
| /// JavaScript operations interpret their operands as signed and generate |
| /// signed results. |
| JS.Expression _coerceBitOperationResultToUnsigned( |
| Expression node, JS.Expression uncoerced) { |
| // Don't coerce if the parent will coerce. |
| var parent = node.parent; |
| if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { |
| return uncoerced; |
| } |
| |
| // Don't do a no-op coerce if the most significant bit is zero. |
| if (_is31BitUnsigned(node)) return uncoerced; |
| |
| // If the consumer of the expression is '==' or '!=' with a constant that |
| // fits in 31 bits, adding a coercion does not change the result of the |
| // comparison, e.g. `a & ~b == 0`. |
| if (parent is InvocationExpression && |
| parent.arguments.positional.length == 1) { |
| var op = parent.name.name; |
| var left = getInvocationReceiver(parent); |
| var right = parent.arguments.positional[0]; |
| if (left != null && op == '==') { |
| const int MAX = 0x7fffffff; |
| if (_asIntInRange(right, 0, MAX) != null) return uncoerced; |
| if (_asIntInRange(left, 0, MAX) != null) return uncoerced; |
| } else if (left != null && op == '>>') { |
| if (_isDefinitelyNonNegative(left) && |
| _asIntInRange(right, 0, 31) != null) { |
| // Parent will generate `# >>> n`. |
| return uncoerced; |
| } |
| } |
| } |
| return js.call('# >>> 0', uncoerced); |
| } |
| |
| bool _nodeIsBitwiseOperation(InvocationExpression node) { |
| switch (node.name.name) { |
| case '&': |
| case '|': |
| case '^': |
| case '~': |
| return true; |
| } |
| return false; |
| } |
| |
| int _asIntInRange(Expression expr, int low, int high) { |
| if (expr is IntLiteral) { |
| if (expr.value >= low && expr.value <= high) return expr.value; |
| return null; |
| } |
| // TODO(jmesserly): other constant evaluation here once kernel supports it. |
| return null; |
| } |
| |
| bool _isDefinitelyNonNegative(Expression expr) { |
| if (expr is IntLiteral) return expr.value >= 0; |
| |
| // TODO(sra): Lengths of known list types etc. |
| return expr is InvocationExpression && _nodeIsBitwiseOperation(expr); |
| } |
| |
| /// Does the parent of [node] mask the result to [width] bits or fewer? |
| bool _parentMasksToWidth(Expression node, int width) { |
| var parent = node.parent; |
| if (parent == null) return false; |
| if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { |
| if (parent.name.name == '&' && parent.arguments.positional.length == 1) { |
| var left = getInvocationReceiver(parent); |
| var right = parent.arguments.positional[0]; |
| final int MAX = (1 << width) - 1; |
| if (left != null) { |
| if (_asIntInRange(right, 0, MAX) != null) return true; |
| if (_asIntInRange(left, 0, MAX) != null) return true; |
| } |
| } |
| return _parentMasksToWidth(parent, width); |
| } |
| return false; |
| } |
| |
| /// Determines if the result of evaluating [expr] will be an non-negative |
| /// value that fits in 31 bits. |
| bool _is31BitUnsigned(Expression expr) { |
| const int MAX = 32; // Includes larger and negative values. |
| /// Determines how many bits are required to hold result of evaluation |
| /// [expr]. [depth] is used to bound exploration of huge expressions. |
| int bitWidth(Expression expr, int depth) { |
| if (expr is IntLiteral) { |
| return expr.value >= 0 ? expr.value.bitLength : MAX; |
| } |
| if (++depth > 5) return MAX; |
| if (expr is InvocationExpression && |
| expr.arguments.positional.length == 1) { |
| var left = getInvocationReceiver(expr); |
| var right = expr.arguments.positional[0]; |
| if (left != null) { |
| switch (expr.name.name) { |
| case '&': |
| return min(bitWidth(left, depth), bitWidth(right, depth)); |
| |
| case '|': |
| case '^': |
| return max(bitWidth(left, depth), bitWidth(right, depth)); |
| |
| case '>>': |
| int shiftValue = _asIntInRange(right, 0, 31); |
| if (shiftValue != null) { |
| int leftWidth = bitWidth(left, depth); |
| return leftWidth == MAX ? MAX : max(0, leftWidth - shiftValue); |
| } |
| return MAX; |
| |
| case '<<': |
| int leftWidth = bitWidth(left, depth); |
| int shiftValue = _asIntInRange(right, 0, 31); |
| if (shiftValue != null) { |
| return min(MAX, leftWidth + shiftValue); |
| } |
| int rightWidth = bitWidth(right, depth); |
| if (rightWidth <= 5) { |
| // e.g. `1 << (x & 7)` has a rightWidth of 3, so shifts by up to |
| // (1 << 3) - 1 == 7 bits. |
| return min(MAX, leftWidth + ((1 << rightWidth) - 1)); |
| } |
| return MAX; |
| default: |
| return MAX; |
| } |
| } |
| } |
| int value = _asIntInRange(expr, 0, 0x7fffffff); |
| if (value != null) return value.bitLength; |
| return MAX; |
| } |
| |
| return bitWidth(expr, 0) < 32; |
| } |
| |
| JS.Expression _emitBinaryOperator(Expression left, Member target, |
| Expression right, InvocationExpression node) { |
| var op = node.name.name; |
| if (op == '==') return _emitEqualityOperator(left, target, right); |
| |
| var leftType = left.getStaticType(types); |
| var rightType = right.getStaticType(types); |
| |
| if (_typeRep.binaryOperationIsPrimitive(leftType, rightType) || |
| leftType == types.stringType && op == '+') { |
| // 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. |
| JS.Expression binary(String code) { |
| return js.call(code, [notNull(left), notNull(right)]); |
| } |
| |
| JS.Expression bitwise(String code) { |
| return _coerceBitOperationResultToUnsigned(node, binary(code)); |
| } |
| |
| switch (op) { |
| case '~/': |
| // `a ~/ b` is equivalent to `(a / b).truncate()` |
| return js.call('(# / #).#()', [ |
| notNull(left), |
| notNull(right), |
| _emitMemberName('truncate', type: leftType) |
| ]); |
| |
| case '%': |
| // TODO(sra): We can generate `a % b + 0` if both are non-negative |
| // (the `+ 0` is to coerce -0.0 to 0). |
| return _emitSend(left, target, op, [right]); |
| |
| case '&': |
| return bitwise('# & #'); |
| |
| case '|': |
| return bitwise('# | #'); |
| |
| case '^': |
| return bitwise('# ^ #'); |
| |
| case '>>': |
| int shiftCount = _asIntInRange(right, 0, 31); |
| if (_is31BitUnsigned(left) && shiftCount != null) { |
| return binary('# >> #'); |
| } |
| if (_isDefinitelyNonNegative(left) && shiftCount != null) { |
| return binary('# >>> #'); |
| } |
| // If the context selects out only bits that can't be affected by the |
| // sign position we can use any JavaScript shift, `(x >> 6) & 3`. |
| if (shiftCount != null && |
| _parentMasksToWidth(node, 31 - shiftCount)) { |
| return binary('# >> #'); |
| } |
| return _emitSend(left, target, op, [right]); |
| |
| case '<<': |
| if (_is31BitUnsigned(node)) { |
| // Result is 31 bit unsigned which implies the shift count was small |
| // enough not to pollute the sign bit. |
| return binary('# << #'); |
| } |
| if (_asIntInRange(right, 0, 31) != null) { |
| return _coerceBitOperationResultToUnsigned(node, binary('# << #')); |
| } |
| return _emitSend(left, target, op, [right]); |
| |
| default: |
| // TODO(vsm): When do Dart ops not map to JS? |
| return binary('# $op #'); |
| } |
| } |
| |
| return _emitSend(left, target, op, [right]); |
| } |
| |
| JS.Expression _emitEqualityOperator( |
| Expression left, Member target, Expression right) { |
| var leftType = left.getStaticType(types); |
| |
| // Conceptually `x == y` in Dart is defined as: |
| // |
| // If either x or y is null, then they are equal iff they are both null. |
| // Otherwise, equality is the result of calling `x.==(y)`. |
| // |
| // In practice, `x.==(y)` is equivalent to `identical(x, y)` in many cases: |
| // - when either side is known to be `null` (literal or Null type) |
| // - left side is an enum |
| // - left side is a primitive type |
| // |
| // We also compile `operator ==` methods to ensure they check the right side |
| // for null`. This allows us to skip the check at call sites. |
| // |
| // TODO(leafp,jmesserly): we could use class hierarchy analysis to check |
| // if `operator ==` was overridden, similar to how we devirtualize private |
| // fields. |
| var isEnum = leftType is InterfaceType && leftType.classNode.isEnum; |
| var usesIdentity = _typeRep.isPrimitive(leftType) || |
| isEnum || |
| _isNull(left) || |
| _isNull(right); |
| |
| // If we know that the left type uses identity for equality, we can |
| // sometimes emit better code, either `===` or `==`. |
| if (usesIdentity) { |
| return _emitCoreIdenticalCall([left, right]); |
| } |
| |
| // If the left side is nullable, we need to use a runtime helper to check |
| // for null. We could inline the null check, but it did not seem to have |
| // a measurable performance effect (possibly the helper is simple enough to |
| // be inlined). |
| if (isNullable(left)) { |
| return _callHelper( |
| 'equals(#, #)', [_visitExpression(left), _visitExpression(right)]); |
| } |
| |
| // Otherwise we emit a call to the == method. |
| return js.call('#[#](#)', [ |
| _visitExpression(left), |
| _emitMemberName('==', type: leftType), |
| _visitExpression(right) |
| ]); |
| } |
| |
| /// 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 receiver, Member target, String name, List<Expression> args) { |
| // TODO(jmesserly): calls that don't pass `element` are probably broken for |
| // `super` calls from disallowed super locations. |
| var type = receiver.getStaticType(types); |
| var memberName = _emitMemberName(name, type: type, member: target); |
| if (target == null) { |
| // dynamic dispatch |
| var dynamicHelper = const {'[]': 'dindex', '[]=': 'dsetindex'}[name]; |
| if (dynamicHelper != null) { |
| return _callHelper('$dynamicHelper(#, #)', |
| [_visitExpression(receiver), _visitExpressionList(args)]); |
| } else { |
| return _callHelper('dsend(#, #, #)', [ |
| _visitExpression(receiver), |
| memberName, |
| _visitExpressionList(args) |
| ]); |
| } |
| } |
| |
| // Generic dispatch to a statically known method. |
| return js.call('#.#(#)', |
| [_visitExpression(receiver), memberName, _visitExpressionList(args)]); |
| } |
| |
| // TODO(jmesserly): optimize super operators for kernel |
| @override |
| visitSuperMethodInvocation(SuperMethodInvocation node) { |
| return new JS.Call(_emitSuperTarget(node.interfaceTarget), |
| _emitArgumentList(node.arguments)); |
| } |
| |
| /// Emits the [JS.PropertyAccess] for accessors or method calls to |
| /// [jsTarget].[jsName], replacing `super` if it is not allowed in scope. |
| JS.PropertyAccess _emitSuperTarget(Member member, {bool setter: false}) { |
| var type = member.enclosingClass.rawType; |
| var jsName = _emitMemberName(member.name.name, type: type, member: member); |
| if (member is Field && !virtualFields.isVirtual(member)) { |
| return new JS.PropertyAccess(new JS.This(), jsName); |
| } |
| if (_superAllowed) return new JS.PropertyAccess(new JS.Super(), jsName); |
| |
| // If we can't emit `super` in this context, generate a helper that does it |
| // for us, and call the helper. |
| var name = member.name.name; |
| var jsMethod = _superHelpers.putIfAbsent(name, () { |
| var isAccessor = member is Procedure ? member.isAccessor : true; |
| if (isAccessor) { |
| assert(member is Procedure |
| ? member.isSetter == setter |
| : !setter || !(member as Field).isFinal); |
| var fn = js.fun( |
| setter |
| ? 'function(x) { super[#] = x; }' |
| : 'function() { return super[#]; }', |
| [jsName]); |
| |
| return new JS.Method(new JS.TemporaryId(name), fn, |
| isGetter: !setter, isSetter: setter); |
| } else { |
| var function = member.function; |
| var params = _emitTypeFormals(function.typeParameters); |
| for (var param in function.positionalParameters) { |
| params.add(new JS.Identifier(param.name)); |
| } |
| if (function.namedParameters.isNotEmpty) { |
| params.add(namedArgumentTemp); |
| } |
| |
| var fn = js.fun( |
| 'function(#) { return super[#](#); }', [params, jsName, params]); |
| name = JS.friendlyNameForDartOperator[name] ?? name; |
| return new JS.Method(new JS.TemporaryId(name), fn); |
| } |
| }); |
| return new JS.PropertyAccess(new JS.This(), jsMethod.name); |
| } |
| |
| @override |
| visitStaticInvocation(StaticInvocation node) { |
| var target = node.target; |
| if (isInlineJS(target)) return _emitInlineJSCode(node) as JS.Expression; |
| if (target.isFactory) return _emitFactoryInvocation(node); |
| |
| if (target.name.name == 'extensionSymbol' && |
| isSdkInternalRuntime(target.enclosingLibrary)) { |
| var args = node.arguments; |
| var firstArg = args.positional.length == 1 ? args.positional[0] : null; |
| if (firstArg is StringLiteral) { |
| return _getExtensionSymbolInternal(firstArg.value); |
| } |
| } |
| if (target == coreTypes.identicalProcedure) { |
| return _emitCoreIdenticalCall(node.arguments.positional); |
| } |
| |
| var fn = _emitStaticTarget(target); |
| var args = _emitArgumentList(node.arguments); |
| return new JS.Call(fn, args); |
| } |
| |
| /// Emits the target of a [StaticInvocation], [StaticGet], or [StaticSet]. |
| JS.Expression _emitStaticTarget(Member target) { |
| var c = target.enclosingClass; |
| if (c != null) { |
| return new JS.PropertyAccess(_emitStaticAccess(c), |
| _emitStaticMemberName(target.name.name, target)); |
| } |
| return _emitTopLevelName(target); |
| } |
| |
| List<JS.Expression> _emitArgumentList(Arguments node, {bool types: true}) { |
| var args = <JS.Expression>[]; |
| if (types) { |
| for (var typeArg in node.types) { |
| args.add(_emitType(typeArg)); |
| } |
| } |
| for (var arg in node.positional) { |
| if (arg is StaticInvocation && |
| isJSSpreadInvocation(arg.target) && |
| arg.arguments.positional.length == 1) { |
| args.add(new JS.Spread(_visitExpression(arg.arguments.positional[0]))); |
| } else { |
| args.add(_visitExpression(arg)); |
| } |
| } |
| var named = <JS.Property>[]; |
| for (var arg in node.named) { |
| named.add(new JS.Property( |
| _propertyName(arg.name), _visitExpression(arg.value))); |
| } |
| if (named.isNotEmpty) { |
| args.add(new JS.ObjectInitializer(named)); |
| } |
| return args; |
| } |
| |
| /// Emits code for the `JS(...)` macro. |
| JS.Node _emitInlineJSCode(StaticInvocation node) { |
| var args = node.arguments.positional; |
| // arg[0] is static return type, used in `RestrictedStaticTypeAnalyzer` |
| var code = args[1]; |
| List<Expression> templateArgs; |
| String source; |
| if (code is StringConcatenation) { |
| if (code.expressions.every((e) => e is StringLiteral)) { |
| templateArgs = args.skip(2).toList(); |
| source = code.expressions.map((e) => (e as StringLiteral).value).join(); |
| } else { |
| if (args.length > 2) { |
| throw new ArgumentError( |
| "Can't mix template args and string interpolation in JS calls: " |
| "`$node`"); |
| } |
| templateArgs = <Expression>[]; |
| source = code.expressions.map((expression) { |
| if (expression is StringLiteral) { |
| return expression.value; |
| } else { |
| templateArgs.add(expression); |
| return '#'; |
| } |
| }).join(); |
| } |
| } else { |
| templateArgs = args.skip(2).toList(); |
| source = (code as StringLiteral).value; |
| } |
| |
| // TODO(vsm): Constructors in dart:html and friends are trying to |
| // allocate a type defined on window/self, but this often conflicts a |
| // with the generated extension class in scope. We really should |
| // qualify explicitly in dart:html itself. |
| var constructorPattern = new RegExp("new [A-Z][A-Za-z]+\\("); |
| if (constructorPattern.matchAsPrefix(source) != null) { |
| var enclosingClass = node.parent; |
| while (enclosingClass != null && enclosingClass is! Class) { |
| enclosingClass = enclosingClass.parent; |
| } |
| if (enclosingClass is Class && |
| _extensionTypes.isNativeClass(enclosingClass)) { |
| var constructorName = source.substring(4, source.indexOf('(')); |
| var className = enclosingClass.name; |
| if (className == constructorName) { |
| source = |
| source.replaceFirst('new $className(', 'new self.$className('); |
| } |
| } |
| } |
| |
| JS.Expression visitTemplateArg(Expression arg) { |
| if (arg is StaticInvocation) { |
| var target = arg.target; |
| var positional = arg.arguments.positional; |
| if (target.name.name == 'getGenericClass' && |
| isSdkInternalRuntime(target.enclosingLibrary) && |
| positional.length == 1) { |
| var typeArg = positional[0]; |
| if (typeArg is TypeLiteral) { |
| var type = typeArg.type; |
| if (type is InterfaceType) { |
| return _emitTopLevelNameNoInterop(type.classNode, suffix: '\$'); |
| } |
| } |
| } |
| } |
| return _visitExpression(arg); |
| } |
| |
| // TODO(rnystrom): The JS() calls are almost never nested, and probably |
| // really shouldn't be, but there are at least a couple of calls in the |
| // HTML library where an argument to JS() is itself a JS() call. If those |
| // go away, this can just assert(!_isInForeignJS). |
| // Inside JS(), type names evaluate to the raw runtime type, not the |
| // wrapped Type object. |
| var wasInForeignJS = _isInForeignJS; |
| _isInForeignJS = true; |
| var jsArgs = templateArgs.map(visitTemplateArg).toList(); |
| _isInForeignJS = wasInForeignJS; |
| |
| var result = js.parseForeignJS(source).instantiate(jsArgs); |
| |
| // `throw` is emitted as a statement by `parseForeignJS`. |
| assert(result is JS.Expression || |
| result is JS.Throw && node.parent is ExpressionStatement); |
| return result; |
| } |
| |
| bool _isNull(Expression expr) => |
| expr is NullLiteral || |
| expr.getStaticType(types) == coreTypes.nullClass.rawType; |
| |
| bool _doubleEqIsIdentity(Expression left, Expression right) { |
| // If we statically know LHS or RHS is null we can use ==. |
| if (_isNull(left) || _isNull(right)) return true; |
| // If the representation of the two types will not induce conversion in |
| // JS then we can use == . |
| return !_typeRep.equalityMayConvert( |
| left.getStaticType(types), right.getStaticType(types)); |
| } |
| |
| bool _tripleEqIsIdentity(Expression left, Expression right) { |
| // If either is non-nullable, then we don't need to worry about |
| // equating null and undefined, and so we can use triple equals. |
| return !isNullable(left) || !isNullable(right); |
| } |
| |
| /// Returns true if [expr] can be null, optionally using [localIsNullable] |
| /// for locals. |
| /// |
| /// If [localIsNullable] is not supplied, this will use the known list of |
| /// [_notNullLocals]. |
| bool isNullable(Expression expr) => _nullableInference.isNullable(expr); |
| |
| bool isPrimitiveType(DartType t) => _typeRep.isPrimitive(t); |
| |
| JS.Expression _emitJSDoubleEq(List<JS.Expression> args, |
| {bool negated = false}) { |
| var op = negated ? '# != #' : '# == #'; |
| return js.call(op, args); |
| } |
| |
| JS.Expression _emitJSTripleEq(List<JS.Expression> args, |
| {bool negated = false}) { |
| var op = negated ? '# !== #' : '# === #'; |
| return js.call(op, args); |
| } |
| |
| JS.Expression _emitCoreIdenticalCall(List<Expression> args, |
| {bool negated = false}) { |
| if (args.length != 2) { |
| // Shouldn't happen in typechecked code |
| return _callHelper( |
| 'throw(Error("compile error: calls to `identical` require 2 args")'); |
| } |
| var left = args[0]; |
| var right = args[1]; |
| var jsArgs = [_visitExpression(left), _visitExpression(right)]; |
| if (_tripleEqIsIdentity(left, right)) { |
| return _emitJSTripleEq(jsArgs, negated: negated); |
| } |
| if (_doubleEqIsIdentity(left, right)) { |
| return _emitJSDoubleEq(jsArgs, negated: negated); |
| } |
| var code = negated ? '!#' : '#'; |
| return js.call(code, |
| new JS.Call(_emitTopLevelName(coreTypes.identicalProcedure), jsArgs)); |
| } |
| |
| @override |
| visitConstructorInvocation(ConstructorInvocation node) { |
| var ctor = node.target; |
| var args = node.arguments; |
| JS.Expression emitNew() { |
| return new JS.New(_emitConstructorName(node.constructedType, ctor), |
| _emitArgumentList(args, types: false)); |
| } |
| |
| return node.isConst ? _emitConst(emitNew) : emitNew(); |
| } |
| |
| JS.Expression _emitFactoryInvocation(StaticInvocation node) { |
| var args = node.arguments; |
| var ctor = node.target; |
| var ctorClass = ctor.enclosingClass; |
| if (ctor.isExternal && _isJSNative(ctorClass)) { |
| return _emitJSInteropNew(ctor, args); |
| } |
| |
| var type = ctorClass.typeParameters.isEmpty |
| ? ctorClass.rawType |
| : new InterfaceType(ctorClass, args.types); |
| |
| if (node.isConst && |
| ctor.name.name == 'fromEnvironment' && |
| ctor.enclosingLibrary == coreTypes.coreLibrary && |
| args.positional.length == 1 && |
| // TODO(jmesserly): this does not correctly handle when the arguments to |
| // fromEnvironment are constant non-literal values. |
| args.positional[0] is BasicLiteral && |
| (args.named.isEmpty || args.named[0].value is BasicLiteral)) { |
| var varName = (args.positional[0] as StringLiteral).value; |
| var value = declaredVariables[varName]; |
| var defaultArg = args.named.isNotEmpty ? args.named[0].value : null; |
| if (ctorClass == coreTypes.stringClass) { |
| if (value != null) return js.escapedString(value); |
| return _visitExpression(defaultArg) ?? new JS.LiteralNull(); |
| } else if (ctorClass == coreTypes.intClass) { |
| var intValue = int.parse(value ?? '', onError: (_) => null); |
| if (intValue != null) return js.number(intValue); |
| return _visitExpression(defaultArg) ?? new JS.LiteralNull(); |
| } else if (ctorClass == coreTypes.boolClass) { |
| if (value == "true") return js.boolean(true); |
| if (value == "false") return js.boolean(false); |
| return _visitExpression(defaultArg) ?? js.boolean(false); |
| } else { |
| return _emitInvalidNode(node, '$ctorClass.fromEnvironment constant'); |
| } |
| } |
| if (args.positional.isEmpty && |
| args.named.isEmpty && |
| ctorClass.enclosingLibrary.importUri.scheme == 'dart') { |
| // Skip the slow SDK factory constructors when possible. |
| switch (ctorClass.name) { |
| case 'Map': |
| case 'HashMap': |
| case 'LinkedHashMap': |
| if (ctor.name.name == '') { |
| return js.call('new #.new()', _emitMapImplType(type)); |
| } else if (ctor.name.name == 'identity') { |
| return js.call( |
| 'new #.new()', _emitMapImplType(type, identity: true)); |
| } |
| break; |
| case 'Set': |
| case 'HashSet': |
| case 'LinkedHashSet': |
| if (ctor.name.name == '') { |
| return js.call('new #.new()', _emitSetImplType(type)); |
| } else if (ctor.name.name == 'identity') { |
| return js.call( |
| 'new #.new()', _emitSetImplType(type, identity: true)); |
| } |
| break; |
| case 'List': |
| if (ctor.name.name == '' && type is InterfaceType) { |
| return _emitList(type.typeArguments[0], []); |
| } |
| break; |
| } |
| } |
| |
| JS.Expression emitNew() { |
| return new JS.Call(_emitConstructorName(type, ctor), |
| _emitArgumentList(args, types: false)); |
| } |
| |
| return node.isConst ? _emitConst(emitNew) : emitNew(); |
| } |
| |
| JS.Expression _emitJSInteropNew(Member ctor, Arguments args) { |
| var ctorClass = ctor.enclosingClass; |
| if (_isObjectLiteral(ctorClass)) return _emitObjectLiteral(args); |
| return new JS.New(_emitConstructorName(ctorClass.rawType, ctor), |
| _emitArgumentList(args, types: false)); |
| } |
| |
| JS.Expression _emitMapImplType(InterfaceType type, {bool identity}) { |
| var typeArgs = type.typeArguments; |
| if (typeArgs.isEmpty) return _emitType(type); |
| identity ??= isPrimitiveType(typeArgs[0]); |
| var c = identity ? identityHashMapImplClass : linkedHashMapImplClass; |
| return _emitType(new InterfaceType(c, typeArgs)); |
| } |
| |
| JS.Expression _emitSetImplType(InterfaceType type, {bool identity}) { |
| var typeArgs = type.typeArguments; |
| if (typeArgs.isEmpty) return _emitType(type); |
| identity ??= isPrimitiveType(typeArgs[0]); |
| var c = identity ? identityHashSetImplClass : linkedHashSetImplClass; |
| return _emitType(new InterfaceType(c, typeArgs)); |
| } |
| |
| bool _isObjectLiteral(Class c) { |
| return _isJSNative(c) && c.annotations.any(isJSAnonymousAnnotation); |
| } |
| |
| bool _isJSNative(Class c) => c.annotations.any(isPublicJSAnnotation); |
| |
| JS.Expression _emitObjectLiteral(Arguments node) { |
| var args = _emitArgumentList(node); |
| if (args.isEmpty) return js.call('{}'); |
| assert(args.single is JS.ObjectInitializer); |
| return args.single; |
| } |
| |
| @override |
| visitNot(Not node) { |
| // Logical negation, `!e`, is a boolean conversion context since it is |
| // defined as `e ? false : true`. |
| return _visitTest(node); |
| } |
| |
| @override |
| visitLogicalExpression(LogicalExpression node) { |
| // The operands of logical boolean operators are subject to boolean |
| // conversion. |
| return _visitTest(node); |
| } |
| |
| @override |
| visitConditionalExpression(ConditionalExpression node) { |
| return js.call('# ? # : #', [ |
| _visitTest(node.condition), |
| _visitExpression(node.then), |
| _visitExpression(node.otherwise) |
| ]) |
| ..sourceInformation = _nodeStart(node.condition); |
| } |
| |
| @override |
| visitStringConcatenation(StringConcatenation node) { |
| var expressions = node.expressions; |
| if (expressions.every((e) => e is StringLiteral)) { |
| return new JS.Expression.binary(_visitExpressionList(expressions), '+'); |
| } |
| |
| var strings = <String>[]; |
| var interpolations = <JS.Expression>[]; |
| |
| var expectString = true; |
| for (var e in expressions) { |
| if (e is StringLiteral) { |
| // Escape the string as necessary for use in the eventual `` quotes. |
| // TODO(jmesserly): this call adds quotes, and then we strip them off. |
| var str = js.escapedString(e.value, '`').value; |
| str = str.substring(1, str.length - 1); |
| if (expectString) { |
| strings.add(str); |
| } else { |
| var last = strings.length - 1; |
| strings[last] = strings[last] + str; |
| } |
| expectString = false; |
| } else { |
| if (expectString) strings.add(''); |
| interpolations.add(_visitExpression(e)); |
| expectString = true; |
| } |
| } |
| if (expectString) strings.add(''); |
| return new JS.TaggedTemplate( |
| _callHelper('str'), new JS.TemplateString(strings, interpolations)); |
| } |
| |
| @override |
| visitIsExpression(IsExpression node) { |
| // Generate `is` as `dart.is` or `typeof` depending on the RHS type. |
| JS.Expression result; |
| var type = node.type; |
| var lhs = _visitExpression(node.operand); |
| var typeofName = _jsTypeofName(type); |
| // Inline primitives other than int (which requires a Math.floor check). |
| if (typeofName != null && type != coreTypes.intClass.rawType) { |
| result = js.call('typeof # == #', [lhs, js.string(typeofName, "'")]); |
| } else { |
| // Always go through a runtime helper, because implicit interfaces. |
| var castType = _emitType(type); |
| result = js.call('#.is(#)', [castType, lhs]); |
| } |
| return result; |
| } |
| |
| String _jsTypeofName(DartType type) { |
| var t = _typeRep.typeFor(type); |
| if (t is JSNumber) return 'number'; |
| if (t is JSString) return 'string'; |
| if (t is JSBoolean) return 'boolean'; |
| return null; |
| } |
| |
| Class getImplementationClass(DartType type) { |
| var t = _typeRep.typeFor(type); |
| if (t is JSNumber) return _jsNumberClass; |
| if (t is JSString) return _jsStringClass; |
| if (t is JSBoolean) return _jsBoolClass; |
| return null; |
| } |
| |
| @override |
| visitAsExpression(AsExpression node) { |
| Expression fromExpr = node.operand; |
| var to = node.type; |
| var jsFrom = _visitExpression(fromExpr); |
| var from = fromExpr.getStaticType(types); |
| |
| // If the check was put here by static analysis to ensure soundness, we |
| // can't skip it. For example, one could implement covariant generic caller |
| // side checks like this: |
| // |
| // typedef F<T>(T t); |
| // class C<T> { |
| // F<T> f; |
| // add(T t) { |
| // // required check `t as T` |
| // } |
| // } |
| // main() { |
| // C<Object> c = new C<int>()..f = (int x) => x.isEven; |
| // c.f('hi'); // required check `c.f as F<Object>` |
| // c.add('hi); |
| // } |
| // |
| var isTypeError = node.isTypeError; |
| if (!isTypeError && types.isSubtypeOf(from, to)) return jsFrom; |
| |
| // All Dart number types map to a JS double. |
| if (_typeRep.isNumber(from) && _typeRep.isNumber(to)) { |
| // Make sure to check when converting to int. |
| if (from != coreTypes.intClass.rawType && |
| to == coreTypes.intClass.rawType) { |
| // TODO(jmesserly): fuse this with notNull check. |
| // TODO(jmesserly): this does not correctly distinguish user casts from |
| // required-for-soundness casts. |
| return _callHelper('asInt(#)', jsFrom); |
| } |
| |
| // A no-op in JavaScript. |
| return jsFrom; |
| } |
| |
| var code = isTypeError ? '#._check(#)' : '#.as(#)'; |
| return js.call(code, [_emitType(to), jsFrom]); |
| } |
| |
| @override |
| visitSymbolLiteral(SymbolLiteral node) { |
| JS.Expression emitSymbol() { |
| // TODO(vsm): Handle qualified symbols correctly. |
| var last = node.value.split('.').last; |
| var name = js.escapedString(node.value, "'"); |
| if (last.startsWith('_')) { |
| var nativeSymbol = _emitPrivateNameSymbol(_currentLibrary, last); |
| return js.call('new #.new(#, #)', [ |
| _emitConstructorAccess(privateSymbolClass.rawType), |
| name, |
| nativeSymbol |
| ]); |
| } else { |
| return js.call('#.new(#)', |
| [_emitConstructorAccess(coreTypes.symbolClass.rawType), name]); |
| } |
| } |
| |
| return _emitConst(emitSymbol); |
| } |
| |
| JS.Expression _cacheConst(JS.Expression expr()) { |
| var savedTypeParams = _typeParamInConst; |
| _typeParamInConst = []; |
| |
| var jsExpr = expr(); |
| |
| bool usesTypeParams = _typeParamInConst.isNotEmpty; |
| _typeParamInConst = savedTypeParams; |
| |
| // TODO(jmesserly): if it uses type params we can still hoist it up as far |
| // as it will go, e.g. at the level the generic class is defined where type |
| // params are available. |
| if (_currentFunction == null || usesTypeParams) return jsExpr; |
| |
| var temp = new JS.TemporaryId('const'); |
| _moduleItems.add(js.statement('let #;', [temp])); |
| return js.call('# || (# = #)', [temp, temp, jsExpr]); |
| } |
| |
| JS.Expression _emitConst(JS.Expression expr()) => |
| _cacheConst(() => _callHelper('const(#)', expr())); |
| |
| @override |
| visitTypeLiteral(TypeLiteral node) { |
| var typeRep = _emitType(node.type); |
| // If the type is a type literal expression in Dart code, wrap the raw |
| // runtime type in a "Type" instance. |
| return _isInForeignJS ? typeRep : _callHelper('wrapType(#)', typeRep); |
| } |
| |
| @override |
| visitThisExpression(ThisExpression node) => new JS.This(); |
| |
| @override |
| visitRethrow(Rethrow node) { |
| return _callHelper('rethrow(#)', _emitVariableRef(_catchParameter)); |
| } |
| |
| @override |
| visitThrow(Throw node) => |
| _callHelper('throw(#)', _visitExpression(node.expression)); |
| |
| @override |
| visitListLiteral(ListLiteral node) { |
| var elementType = node.typeArgument; |
| if (!node.isConst) { |
| return _emitList(elementType, _visitExpressionList(node.expressions)); |
| } |
| return _cacheConst(() => |
| _emitConstList(elementType, _visitExpressionList(node.expressions))); |
| } |
| |
| JS.Expression _emitConstList( |
| DartType elementType, List<JS.Expression> elements) { |
| // dart.constList helper internally depends on _interceptors.JSArray. |
| _declareBeforeUse(_jsArrayClass); |
| return _callHelper('constList([#], #)', [elements, _emitType(elementType)]); |
| } |
| |
| JS.Expression _emitList(DartType itemType, List<JS.Expression> items) { |
| var list = new JS.ArrayInitializer(items); |
| |
| // TODO(jmesserly): analyzer will usually infer `List<Object>` because |
| // that is the least upper bound of the element types. So we rarely |
| // generate a plain `List<dynamic>` anymore. |
| if (itemType == const DynamicType()) return list; |
| |
| // Call `new JSArray<E>.of(list)` |
| var arrayType = new InterfaceType(_jsArrayClass, [itemType]); |
| return js.call('#.of(#)', [_emitType(arrayType), list]); |
| } |
| |
| @override |
| visitMapLiteral(MapLiteral node) { |
| emitEntries() { |
| var entries = <JS.Expression>[]; |
| for (var e in node.entries) { |
| entries.add(_visitExpression(e.key)); |
| entries.add(_visitExpression(e.value)); |
| } |
| return new JS.ArrayInitializer(entries); |
| } |
| |
| if (!node.isConst) { |
| var mapType = |
| _emitMapImplType(node.getStaticType(types) as InterfaceType); |
| if (node.entries.isEmpty) { |
| return js.call('new #.new()', [mapType]); |
| } |
| return js.call('new #.from(#)', [mapType, emitEntries()]); |
| } |
| return _cacheConst(() => _callHelper('constMap(#, #, #)', |
| [_emitType(node.keyType), _emitType(node.valueType), emitEntries()])); |
| } |
| |
| @override |
| visitAwaitExpression(AwaitExpression node) => |
| new JS.Yield(_visitExpression(node.operand)); |
| |
| @override |
| visitFunctionExpression(FunctionExpression node) { |
| var fn = _emitArrowFunction(node); |
| if (!_reifyFunctionType(_currentFunction)) return fn; |
| return _emitFunctionTagged(fn, node.getStaticType(types) as FunctionType); |
| } |
| |
| JS.ArrowFun _emitArrowFunction(FunctionExpression node) { |
| JS.Fun f = _emitFunction(node.function, null); |
| JS.Node body = f.body; |
| |
| // Simplify `=> { return e; }` to `=> e` |
| if (body is JS.Block) { |
| JS.Block block = body; |
| if (block.statements.length == 1) { |
| JS.Statement s = block.statements[0]; |
| if (s is JS.Return && s.value != null) body = s.value; |
| } |
| } |
| |
| // Convert `function(...) { ... }` to `(...) => ...` |
| // This is for readability, but it also ensures correct `this` binding. |
| return new JS.ArrowFun(f.params, body, |
| typeParams: f.typeParams, returnType: f.returnType); |
| } |
| |
| @override |
| visitStringLiteral(StringLiteral node) => js.escapedString(node.value, '"'); |
| |
| @override |
| visitIntLiteral(IntLiteral node) => js.number(node.value); |
| |
| @override |
| visitDoubleLiteral(DoubleLiteral node) => js.number(node.value); |
| |
| @override |
| visitBoolLiteral(BoolLiteral node) => new JS.LiteralBool(node.value); |
| |
| @override |
| visitNullLiteral(NullLiteral node) => new JS.LiteralNull(); |
| |
| @override |
| visitLet(Let node) { |
| var v = node.variable; |
| var init = _visitExpression(v.initializer); |
| var body = _visitExpression(node.body); |
| var temp = _tempVariables.remove(v); |
| if (temp != null) { |
| if (_letVariables != null) { |
| init = new JS.Assignment(temp, init); |
| _letVariables.add(temp); |
| } else { |
| // TODO(jmesserly): make sure this doesn't happen on any performance |
| // critical call path. |
| // |
| // Annotations on a top-level, non-lazy function type should be the only |
| // remaining use. |
| return new JS.Call(new JS.ArrowFun([temp], body), [init]); |
| } |
| } |
| return new JS.Binary(',', init, body); |
| } |
| |
| @override |
| visitInstantiation(Instantiation node) { |
| return _callHelper('gbind(#, #)', [ |
| _visitExpression(node.expression), |
| node.typeArguments.map(_emitType).toList() |
| ]); |
| } |
| |
| @override |
| visitLoadLibrary(LoadLibrary node) => _callHelper('loadLibrary()'); |
| |
| // TODO(jmesserly): DDC loads all libraries eagerly. |
| // See |
| // https://github.com/dart-lang/sdk/issues/27776 |
| // https://github.com/dart-lang/sdk/issues/27777 |
| @override |
| visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) => js.boolean(true); |
| |
| @override |
| visitVectorCreation(VectorCreation node) => defaultExpression(node); |
| |
| @override |
| visitVectorGet(VectorGet node) => defaultExpression(node); |
| |
| @override |
| visitVectorSet(VectorSet node) => defaultExpression(node); |
| |
| @override |
| visitVectorCopy(VectorCopy node) => defaultExpression(node); |
| |
| @override |
| visitClosureCreation(ClosureCreation node) => defaultExpression(node); |
| |
| bool isCallableClass(Class c) { |
| // See if we have a "call" with a statically known function type: |
| // |
| // - if it's a method, then it does because all methods do, |
| // - if it's a getter, check the return type. |
| // |
| // Other cases like a getter returning dynamic/Object/Function will be |
| // handled at runtime by the dynamic call mechanism. So we only |
| // concern ourselves with statically known function types. |
| // |
| // We can ignore `noSuchMethod` because: |
| // * `dynamic d; d();` without a declared `call` method is handled by dcall. |
| // * for `class C implements Callable { noSuchMethod(i) { ... } }` we find |
| // the `call` method on the `Callable` interface. |
| var member = hierarchy.getInterfaceMember(c, new Name("call")); |
| return member != null && member.getterType is FunctionType; |
| } |
| |
| bool _reifyFunctionType(FunctionNode f) { |
| if (_currentLibrary.importUri.scheme != 'dart') return true; |
| var parent = f.parent; |
| |
| // SDK libraries can skip reification if they request it. |
| reifyFunctionTypes(Expression a) => |
| isBuiltinAnnotation(a, '_js_helper', 'ReifyFunctionTypes'); |
| while (parent != null) { |
| var a = findAnnotation(parent, reifyFunctionTypes); |
| if (a != null && a is ConstructorInvocation) { |
| var args = a.arguments.positional; |
| if (args.length == 1) { |
| var arg = args[0]; |
| if (arg is BoolLiteral) return arg.value; |
| } |
| } |
| parent = parent.parent; |
| } |
| return true; |
| } |
| |
| /// Everything in Dart is an Object and supports the 4 members on Object, |
| /// so we have to use a runtime helper to handle values such as `null` and |
| /// native types. |
| /// |
| /// For example `null.toString()` is legal in Dart, so we need to generate |
| /// that as `dart.toString(obj)`. |
| bool _isObjectMemberCall(Expression target, String memberName) { |
| return isObjectMember(memberName) && isNullable(target); |
| } |
| } |
| |
| bool isSdkInternalRuntime(Library l) => |
| l.importUri.toString() == 'dart:_runtime'; |
| |
| /// 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(Library library) { |
| var uri = library.importUri; |
| if (uri.scheme == 'dart') return uri.path; |
| |
| // TODO(vsm): This is not necessarily unique if '__' appears in a file name. |
| Iterable<String> segments; |
| if (uri.scheme == 'package') { |
| // Strip the package name. |
| // TODO(vsm): This is not unique if an escaped '/'appears in a filename. |
| // E.g., "foo/bar.dart" and "foo__bar.dart" would collide. |
| segments = uri.pathSegments.skip(1); |
| } else { |
| // TODO(jmesserly): this is not unique typically. |
| segments = [uri.pathSegments.last]; |
| } |
| |
| var qualifiedPath = segments.map((p) => p == '..' ? '' : p).join('__'); |
| return pathToJSIdentifier(qualifiedPath); |
| } |
| |
| /// 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, "'"); |
| |
| bool _isInlineJSFunction(Statement body) { |
| var block = body; |
| if (block is Block) { |
| var statements = block.statements; |
| if (statements.length != 1) return false; |
| body = statements[0]; |
| } |
| if (body is ReturnStatement) { |
| var expr = body.expression; |
| return expr is StaticInvocation && isInlineJS(expr.target); |
| } |
| return false; |
| } |
| |
| /// Return true if this is one of the methods/properties on all Dart Objects |
| /// (toString, hashCode, noSuchMethod, runtimeType). |
| /// |
| /// Operator == is excluded, as it is handled as part of the equality binary |
| /// operator. |
| bool isObjectMember(String name) { |
| // We could look these up on Object, but we have hard coded runtime helpers |
| // so it's not really providing any benefit. |
| switch (name) { |
| case 'hashCode': |
| case 'toString': |
| case 'noSuchMethod': |
| case 'runtimeType': |
| case '==': |
| return true; |
| } |
| return false; |
| } |
| |
| bool _isObjectMethod(String name) => |
| name == 'toString' || name == 'noSuchMethod'; |