blob: c11ced830875099b3d84da7bf17e34f4e876bf8a [file] [log] [blame]
// 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.
// @dart = 2.9
import 'dart:collection';
import 'dart:convert';
import 'dart:math' show max, min;
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/library_index.dart';
import 'package:kernel/src/dart_type_equivalence.dart';
import 'package:kernel/type_algebra.dart';
import 'package:kernel/type_environment.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart' show SourceLocation;
import '../compiler/js_names.dart' as js_ast;
import '../compiler/js_utils.dart' as js_ast;
import '../compiler/module_builder.dart'
show isSdkInternalRuntimeUri, libraryUriToJsIdentifier, pathToJSIdentifier;
import '../compiler/module_containers.dart' show ModuleItemContainer;
import '../compiler/shared_command.dart' show SharedCompilerOptions;
import '../compiler/shared_compiler.dart';
import '../js_ast/js_ast.dart' as js_ast;
import '../js_ast/js_ast.dart' show ModuleItem, js;
import '../js_ast/source_map_printer.dart'
show NodeEnd, NodeSpan, HoverComment, continueSourceMap;
import 'constants.dart';
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 'target.dart' show allowedNativeTest;
import 'type_table.dart';
class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
with SharedCompiler<Library, Class, InterfaceType, FunctionNode>
implements
StatementVisitor<js_ast.Statement>,
ExpressionVisitor<js_ast.Expression>,
DartTypeVisitor<js_ast.Expression> {
final SharedCompilerOptions _options;
/// Maps each `Class` node compiled in the module to the `Identifier`s used to
/// name the class in JavaScript.
///
/// This mapping is used when generating the symbol information for the
/// module.
final classIdentifiers = <Class, js_ast.Identifier>{};
/// Maps each class `Member` node compiled in the module to the name used for
/// the member in JavaScript.
///
/// This mapping is used when generating the symbol information for the
/// module.
final memberNames = <Member, String>{};
/// Maps each `VariableDeclaration` node compiled in the module to the name
/// used for the variable in JavaScript.
///
/// This mapping is used when generating the symbol information for the
/// module.
final variableIdentifiers = <VariableDeclaration, js_ast.Identifier>{};
/// Maps a library URI import, that is not in [_libraries], to the
/// corresponding Kernel summary module we imported it with.
///
/// An entry must exist for every reachable component.
final Map<Library, Component> _importToSummary;
/// Maps a Kernel summary to the JS import name for the module.
///
/// An entry must exist for every reachable component.
final Map<Component, String> _summaryToModule;
/// The variable for the current catch clause
VariableDeclaration _rethrowParameter;
/// In an async* function, this represents the stream controller parameter.
js_ast.TemporaryId _asyncStarController;
Set<Class> _pendingClasses;
/// Temporary variables mapped to their corresponding JavaScript variable.
final _tempVariables = <VariableDeclaration, js_ast.TemporaryId>{};
/// Let variables collected for the given function.
List<js_ast.TemporaryId> _letVariables;
final _constTable = js_ast.Identifier('CT');
/// Constant getters used to populate the constant table.
final _constLazyAccessors = <js_ast.Method>[];
/// Container for holding the results of lazily-evaluated constants.
var _constTableCache = ModuleItemContainer<String>.asArray('C');
/// Tracks the index in [moduleItems] where the const table must be inserted.
/// Required for SDK builds due to internal circular dependencies.
/// E.g., dart.constList depends on JSArray.
int _constTableInsertionIndex = 0;
/// The class that is emitting its base class or mixin references, otherwise
/// null.
///
/// This is not used when inside the class method bodies, or for other type
/// information such as `implements`.
Class _classEmittingExtends;
/// The class that is emitting its signature information, otherwise null.
Class _classEmittingSignatures;
/// True when a class is emitting a deferred class hierarchy.
bool _emittingDeferredType = false;
/// 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;
Component _component;
Library _currentLibrary;
FunctionNode _currentFunction;
/// Whether the current function needs to insert parameter checks.
///
/// Used to avoid adding checks for formal parameters inside a synthetic
/// function that is generated during expression compilation in the
/// incremental compiler, since those checks would already be done in
/// the original code.
bool _checkParameters = true;
/// 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;
final StatefulStaticTypeContext _staticTypeContext;
final ClassHierarchy _hierarchy;
/// 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 = VirtualFieldModel();
final JSTypeRep _typeRep;
bool _superAllowed = true;
final _superHelpers = <String, js_ast.Method>{};
// 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 = 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 = HashMap<Statement, String>.identity();
/// Indicates that the current context exists within a switch statement that
/// uses at least one continue statement with a target label.
///
/// JS forbids labels at case statement boundaries, so these switch
/// statements must be generated less directly.
/// Updated from the method 'visitSwitchStatement'.
bool _inLabeledContinueSwitch = false;
/// A map from switch statements to their state information.
/// State information includes the names of the switch statement's implicit
/// label name and implicit state variable name.
///
/// Entries are only created for switch statements that contain labeled
/// continue statements and are used to simulate "jumping" to case statements.
/// State variables hold the next constant case expression, while labels act
/// as targets for continue and break.
final _switchLabelStates = HashMap<Statement, _SwitchLabelState>();
/// Maps Kernel constants to their JS aliases.
final constAliasCache = HashMap<Constant, js_ast.Expression>();
/// Maps uri strings in asserts and elsewhere to hoisted identifiers.
var _uriContainer = ModuleItemContainer<String>.asArray('I');
final Class _jsArrayClass;
final Class _privateSymbolClass;
final Class _linkedHashMapImplClass;
final Class _identityHashMapImplClass;
final Class _linkedHashSetClass;
final Class _linkedHashSetImplClass;
final Class _identityHashSetImplClass;
final Class _syncIterableClass;
final Class _asyncStarImplClass;
/// The dart:async `StreamIterator<T>` type.
final Class _asyncStreamIteratorClass;
final Procedure _assertInteropMethod;
final DevCompilerConstants _constants;
final NullableInference _nullableInference;
bool _moduleEmitted = false;
factory ProgramCompiler(
Component component,
ClassHierarchy hierarchy,
SharedCompilerOptions options,
Map<Library, Component> importToSummary,
Map<Component, String> summaryToModule,
{CoreTypes coreTypes}) {
coreTypes ??= CoreTypes(component);
var types = TypeEnvironment(coreTypes, hierarchy);
var constants = DevCompilerConstants();
var nativeTypes = NativeTypeSet(coreTypes, constants, component);
var jsTypeRep = JSTypeRep(types, hierarchy);
var staticTypeContext = StatefulStaticTypeContext.stacked(types);
return ProgramCompiler._(
coreTypes,
coreTypes.index,
nativeTypes,
constants,
types,
hierarchy,
jsTypeRep,
NullableInference(jsTypeRep, staticTypeContext, options: options),
staticTypeContext,
options,
importToSummary,
summaryToModule,
);
}
ProgramCompiler._(
this._coreTypes,
LibraryIndex sdk,
this._extensionTypes,
this._constants,
this._types,
this._hierarchy,
this._typeRep,
this._nullableInference,
this._staticTypeContext,
this._options,
this._importToSummary,
this._summaryToModule)
: _jsArrayClass = sdk.getClass('dart:_interceptors', 'JSArray'),
_asyncStreamIteratorClass =
sdk.getClass('dart:async', 'StreamIterator'),
_privateSymbolClass = sdk.getClass('dart:_js_helper', 'PrivateSymbol'),
_linkedHashMapImplClass = sdk.getClass('dart:_js_helper', 'LinkedMap'),
_identityHashMapImplClass =
sdk.getClass('dart:_js_helper', 'IdentityMap'),
_linkedHashSetClass = sdk.getClass('dart:collection', 'LinkedHashSet'),
_linkedHashSetImplClass = sdk.getClass('dart:collection', '_HashSet'),
_identityHashSetImplClass =
sdk.getClass('dart:collection', '_IdentityHashSet'),
_syncIterableClass = sdk.getClass('dart:_js_helper', 'SyncIterable'),
_asyncStarImplClass = sdk.getClass('dart:async', '_AsyncStarImpl'),
_assertInteropMethod = sdk.getTopLevelMember(
'dart:_runtime', 'assertInterop') as Procedure;
@override
Uri get currentLibraryUri => _currentLibrary.importUri;
@override
Library get currentLibrary => _currentLibrary;
@override
Library get coreLibrary => _coreTypes.coreLibrary;
@override
FunctionNode get currentFunction => _currentFunction;
@override
InterfaceType get privateSymbolType =>
_coreTypes.legacyRawType(_privateSymbolClass);
@override
InterfaceType get internalSymbolType =>
_coreTypes.legacyRawType(_coreTypes.internalSymbolClass);
/// Module can be emitted only once, and the compiler can be reused after
/// only in incremental mode, for expression compilation only.
js_ast.Program emitModule(Component component) {
if (_moduleEmitted) {
throw StateError('Can only call emitModule once.');
}
_component = component;
var libraries = component.libraries;
// Initialize library variables.
isBuildingSdk = libraries.any(isSdkInternalRuntime);
// For runtime performance reasons, we only containerize SDK symbols in web
// libraries. Otherwise, we use a 600-member cutoff before a module is
// containerized. This is somewhat arbitrary but works promisingly for the
// SDK and Flutter Web.
if (!isBuildingSdk) {
// The number of DDC top-level symbols scales with the number of
// non-static class members across an entire module.
var uniqueNames = HashSet<String>();
libraries.forEach((Library l) {
l.classes.forEach((Class c) {
c.members.forEach((m) {
var isStatic =
m is Field ? m.isStatic : (m is Procedure ? m.isStatic : false);
if (isStatic) return;
var name = js_ast.toJSIdentifier(
m.name.text.replaceAll(js_ast.invalidCharInIdentifier, '_'));
uniqueNames.add(name);
});
});
});
containerizeSymbols = uniqueNames.length > 600;
}
var items = startModule(libraries);
_nullableInference.allowNotNullDeclarations = isBuildingSdk;
_typeTable = TypeTable(runtimeModule);
// Collect all class/type Element -> Node mappings
// in case we need to forward declare any classes.
_pendingClasses = HashSet.identity();
for (var l in libraries) {
_pendingClasses.addAll(l.classes);
}
// Insert a circular reference so neither the constant table or its cache
// are optimized away by V8. Required for expression evaluation.
var constTableDeclaration =
js.statement('const # = Object.create({# : () => (#, #)});', [
_constTable,
js_ast.LiteralString('_'),
_constTableCache.containerId,
_constTable
]);
moduleItems.add(constTableDeclaration);
// Record a safe index after the declaration of type generators and
// top-level symbols but before the declaration of any functions.
// Various preliminary data structures must be inserted here prior before
// referenced by the rest of the module.
var safeDeclarationIndex = moduleItems.length;
_constTableInsertionIndex = safeDeclarationIndex;
// Add implicit dart:core dependency so it is first.
emitLibraryName(_coreTypes.coreLibrary);
// Visit each library and emit its code.
//
// NOTE: classes 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);
// Emit hoisted assert strings
moduleItems.insertAll(safeDeclarationIndex, _uriContainer.emit());
moduleItems.insertAll(safeDeclarationIndex, _constTableCache.emit());
if (_constLazyAccessors.isNotEmpty) {
var constTableBody = runtimeStatement(
'defineLazy(#, { # }, false)', [_constTable, _constLazyAccessors]);
moduleItems.insert(_constTableInsertionIndex, constTableBody);
_constLazyAccessors.clear();
}
moduleItems.addAll(afterClassDefItems);
afterClassDefItems.clear();
// Visit directives (for exports)
libraries.forEach(_emitExports);
// Declare imports and extension symbols
emitImportsAndExtensionSymbols(items,
forceExtensionSymbols:
libraries.any((l) => allowedNativeTest(l.importUri)));
// Insert a check that runs when loading this module to verify that the null
// safety mode it was compiled in matches the mode used when compiling the
// dart sdk module.
//
// This serves as a sanity check at runtime that we don't have an
// infrastructure issue that loaded js files compiled with different modes
// into the same application.
js_ast.LiteralBool soundNullSafety;
switch (component.mode) {
case NonNullableByDefaultCompiledMode.Strong:
soundNullSafety = js_ast.LiteralBool(true);
break;
case NonNullableByDefaultCompiledMode.Weak:
soundNullSafety = js_ast.LiteralBool(false);
break;
default:
throw StateError('Unsupported Null Safety mode ${component.mode}, '
'in ${component?.location?.file}.');
}
if (!isBuildingSdk) {
items.add(
runtimeStatement('_checkModuleNullSafetyMode(#)', [soundNullSafety]));
}
// Emit the hoisted type table cache variables
items.addAll(_typeTable.dischargeBoundTypes());
var module = finishModule(items, _options.moduleName);
// Mark as finished for incremental mode, so it is safe to
// switch to the incremental mode for expression compilation.
_moduleEmitted = true;
return module;
}
@override
String jsLibraryName(Library library) {
return libraryUriToJsIdentifier(library.importUri);
}
@override
String jsLibraryAlias(Library library) {
var uri = library.importUri.normalizePath();
if (uri.scheme == 'dart') return null;
Iterable<String> segments;
if (uri.scheme == 'package') {
// Strip the package name.
segments = uri.pathSegments.skip(1);
} else {
segments = uri.pathSegments;
}
var qualifiedPath =
pathToJSIdentifier(p.withoutExtension(segments.join('/')));
return qualifiedPath == jsLibraryName(library) ? null : qualifiedPath;
}
@override
String jsLibraryDebuggerName(Library library) => '${library.importUri}';
@override
Iterable<String> jsPartDebuggerNames(Library library) =>
library.parts.map((part) => part.partUri);
@override
bool isSdkInternalRuntime(Library l) {
return isSdkInternalRuntimeUri(l.importUri);
}
@override
String libraryToModule(Library library) {
if (library.importUri.scheme == 'dart') {
// TODO(jmesserly): we need to split out HTML.
return js_ast.dartSdkModule;
}
var summary = _importToSummary[library];
var moduleName = _summaryToModule[summary];
if (moduleName == null) {
throw StateError('Could not find module name for library "$library" '
'from component "$summary".');
}
return moduleName;
}
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;
_staticTypeContext.enterLibrary(_currentLibrary);
if (isBuildingSdk) {
containerizeSymbols = _isWebLibrary(library.importUri);
}
if (isSdkInternalRuntime(library)) {
// `dart:_runtime` uses a different order for bootstrapping.
//
// Functions are first because we use them to associate type info
// (such as `dart.fn`), then classes/typedefs, then fields
// (which instantiate classes).
//
// For other libraries, we start with classes/types, because functions
// often use classes/types from the library in their signature.
//
// TODO(jmesserly): we can merge these once we change signatures to be
// lazily associated at the tear-off point for top-level functions.
_emitLibraryProcedures(library);
_emitTopLevelFields(library.fields);
library.classes.forEach(_emitClass);
} else {
library.classes.forEach(_emitClass);
_emitLibraryProcedures(library);
_emitTopLevelFields(library.fields);
}
_staticTypeContext.leaveLibrary(_currentLibrary);
_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.
var node = export.node;
if (node is Procedure && node.name.text == '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;
_currentLibrary = c.enclosingLibrary;
_currentUri = c.fileUri;
moduleItems.add(_emitClassDeclaration(c));
// The const table depends on dart.defineLazy, so emit it after the SDK.
if (isSdkInternalRuntime(_currentLibrary)) {
_constTableInsertionIndex = moduleItems.length;
}
_currentClass = savedClass;
_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 && _emittingClassExtends) {
_emitClass(c);
}
}
static js_ast.Identifier _emitIdentifier(String name) =>
js_ast.Identifier(js_ast.toJSIdentifier(name));
static js_ast.TemporaryId _emitTemporaryId(String name) =>
js_ast.TemporaryId(js_ast.toJSIdentifier(name));
js_ast.Statement _emitClassDeclaration(Class c) {
// Mixins are unrolled in _defineClass.
if (c.isAnonymousMixin) return null;
// If this class is annotated with `@JS`, then there is nothing to emit.
if (findAnnotation(c, isPublicJSAnnotation) != null) return null;
// Generic classes will be defined inside a function that closes over the
// type parameter. So we can use their local variable name directly.
//
// TODO(jmesserly): the special case for JSArray is to support its special
// type-tagging factory constructors. Those will go away once we fix:
// https://github.com/dart-lang/sdk/issues/31003
var className = c.typeParameters.isNotEmpty
? (c == _jsArrayClass
? _emitIdentifier(c.name)
: _emitTemporaryId(getLocalClassName(c)))
: _emitTopLevelName(c);
var savedClassProperties = _classProperties;
_classProperties =
ClassPropertyModel.build(_types, _extensionTypes, _virtualFields, c);
var body = <js_ast.Statement>[];
// ClassPropertyModel.build introduces symbols for virtual field accessors.
_classProperties.virtualFields.forEach((field, virtualField) {
// TODO(vsm): Clean up this logic.
//
// Typically, [emitClassPrivateNameSymbol] creates a new symbol. If it
// is called multiple times, that symbol is cached. If the former,
// assign directly to [virtualField]. If the latter, copy the old
// variable to [virtualField].
var symbol = _emitClassPrivateNameSymbol(
c.enclosingLibrary, getLocalClassName(c), field, virtualField);
if (symbol != virtualField) {
addSymbol(virtualField, getSymbolValue(symbol));
if (!containerizeSymbols) {
body.add(js.statement('const # = #;', [virtualField, symbol]));
}
}
});
var jsCtors = _defineConstructors(c, className);
var jsMethods = _emitClassMethods(c);
_emitSuperHelperSymbols(body);
// Deferred supertypes must be evaluated lazily while emitting classes to
// prevent evaluating a JS expression for a deferred type from influencing
// class declaration order (such as when calling 'emitDeferredType').
var deferredSupertypes = <js_ast.Statement Function()>[];
// 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 = _extensionTypes.getNativePeers(c);
if (jsPeerNames.length == 1 && c.typeParameters.isNotEmpty) {
// Special handling for JSArray<E>
body.add(runtimeStatement('setExtensionBaseClass(#, #.global.#)',
[className, runtimeModule, jsPeerNames[0]]));
}
var finishGenericTypeTest = _emitClassTypeTests(c, className, body);
// Attach caches on all canonicalized types.
body.add(runtimeStatement('addTypeCaches(#)', [className]));
_emitClassSignature(c, className, body);
_initExtensionSymbols(c);
if (!c.isMixinDeclaration) {
_defineExtensionMembers(className, body);
}
var classDef = js_ast.Statement.from(body);
var typeFormals = c.typeParameters;
var evaluatedDeferredSupertypes =
deferredSupertypes.map<js_ast.Statement>((f) => f()).toList();
if (typeFormals.isNotEmpty) {
classDef = _defineClassTypeArguments(
c, typeFormals, classDef, className, evaluatedDeferredSupertypes);
} else {
afterClassDefItems.addAll(evaluatedDeferredSupertypes);
}
body = [classDef];
_emitStaticFields(c, body);
if (finishGenericTypeTest != null) body.add(finishGenericTypeTest);
for (var peer in jsPeerNames) {
_registerExtensionType(c, peer, body);
}
_classProperties = savedClassProperties;
return js_ast.Statement.from(body);
}
/// Wraps a possibly generic class in its type arguments.
js_ast.Statement _defineClassTypeArguments(
NamedNode c, List<TypeParameter> formals, js_ast.Statement body,
[js_ast.Expression className, List<js_ast.Statement> deferredBaseClass]) {
assert(formals.isNotEmpty);
var name = getTopLevelName(c);
var jsFormals = _emitTypeFormals(formals);
// Checks for explicitly set variance to avoid emitting legacy covariance
// Variance annotations are not necessary when variance experiment flag is
// not enabled or when no type parameters have explicitly defined
// variances.
var hasOnlyLegacyCovariance = formals.every((t) => t.isLegacyCovariant);
if (!hasOnlyLegacyCovariance) {
var varianceList = formals.map(_emitVariance);
var varianceStatement = runtimeStatement(
'setGenericArgVariances(#, [#])', [className, varianceList]);
body = js_ast.Statement.from([body, varianceStatement]);
}
var typeConstructor = js.call('(#) => { #; #; return #; }', [
jsFormals,
_typeTable.dischargeFreeTypes(formals),
body,
className ?? _emitIdentifier(name)
]);
var genericArgs = [
typeConstructor,
if (deferredBaseClass != null && deferredBaseClass.isNotEmpty)
js.call('(#) => { #; }', [jsFormals, deferredBaseClass]),
];
// FutureOr types have a runtime normalization step that will call
// generic() as needed.
var genericCall = c == _coreTypes.deprecatedFutureOrClass
? runtimeCall('normalizeFutureOr(#)', [genericArgs])
: runtimeCall('generic(#)', [genericArgs]);
var genericName = _emitTopLevelNameNoInterop(c, suffix: '\$');
return js.statement('{ # = #; # = #(); }',
[genericName, genericCall, _emitTopLevelName(c), genericName]);
}
js_ast.Expression _emitVariance(TypeParameter typeParameter) {
switch (typeParameter.variance) {
case Variance.contravariant:
return runtimeCall('Variance.contravariant');
case Variance.invariant:
return runtimeCall('Variance.invariant');
case Variance.unrelated:
return runtimeCall('Variance.unrelated');
case Variance.covariant:
default:
return runtimeCall('Variance.covariant');
}
}
js_ast.Statement _emitClassStatement(Class c, js_ast.Expression className,
js_ast.Expression heritage, List<js_ast.Method> methods) {
if (c.typeParameters.isNotEmpty) {
var classIdentifier = className as js_ast.Identifier;
if (_options.emitDebugSymbols) classIdentifiers[c] = classIdentifier;
return js_ast.ClassExpression(classIdentifier, heritage, methods)
.toStatement();
}
var classIdentifier = _emitTemporaryId(getLocalClassName(c));
if (_options.emitDebugSymbols) classIdentifiers[c] = classIdentifier;
var classExpr = js_ast.ClassExpression(classIdentifier, heritage, methods);
return js.statement('# = #;', [className, classExpr]);
}
/// Like [_emitClassStatement] but emits a Dart 2.1 mixin represented by
/// [c].
///
/// Mixins work similar to normal classes, but their instance methods close
/// over the actual superclass. Given a Dart class like:
///
/// mixin M on C {
/// foo() => super.foo() + 42;
/// }
///
/// We generate a JS class like this:
///
/// lib.M = class M extends core.Object {}
/// lib.M[dart.mixinOn] = (C) => class M extends C {
/// foo() {
/// return super.foo() + 42;
/// }
/// };
///
/// The special `dart.mixinOn` symbolized property is used by the runtime
/// helper `dart.applyMixin`. The helper calls the function with the actual
/// base class, and then copies the resulting members to the destination
/// class.
///
/// In the long run we may be able to improve this so we do not have the
/// unnecessary class, but for now, this lets us get the right semantics with
/// minimal compiler and runtime changes.
void _emitMixinStatement(
Class c,
js_ast.Expression className,
js_ast.Expression heritage,
List<js_ast.Method> methods,
List<js_ast.Statement> body) {
var staticMethods = methods.where((m) => m.isStatic).toList();
var instanceMethods = methods.where((m) => !m.isStatic).toList();
body.add(_emitClassStatement(c, className, heritage, staticMethods));
var superclassId = _emitTemporaryId(getLocalClassName(c.superclass));
var classId = className is js_ast.Identifier
? className
: _emitTemporaryId(getLocalClassName(c));
var mixinMemberClass =
js_ast.ClassExpression(classId, superclassId, instanceMethods);
js_ast.Node arrowFnBody = mixinMemberClass;
var extensionInit = <js_ast.Statement>[];
_defineExtensionMembers(classId, extensionInit);
if (extensionInit.isNotEmpty) {
extensionInit.insert(0, mixinMemberClass.toStatement());
extensionInit.add(classId.toReturn());
arrowFnBody = js_ast.Block(extensionInit);
}
body.add(js.statement('#[#.mixinOn] = #', [
className,
runtimeModule,
js_ast.ArrowFun([superclassId], arrowFnBody)
]));
}
void _defineClass(
Class c,
js_ast.Expression className,
List<js_ast.Method> methods,
List<js_ast.Statement> body,
List<js_ast.Statement Function()> deferredSupertypes) {
if (c == _coreTypes.objectClass) {
body.add(_emitClassStatement(c, className, null, methods));
return;
}
js_ast.Expression emitDeferredType(DartType t,
{bool emitNullability = true}) {
js_ast.Expression _emitDeferredType(DartType t,
{bool emitNullability = true}) {
if (t is InterfaceType) {
_declareBeforeUse(t.classNode);
if (t.typeArguments.isNotEmpty) {
var typeRep = _emitGenericClassType(
t, t.typeArguments.map(_emitDeferredType));
return emitNullability
? _emitNullabilityWrapper(typeRep, t.declaredNullability)
: typeRep;
}
return _emitInterfaceType(t, emitNullability: emitNullability);
} else if (t is FutureOrType) {
var normalizedType = _normalizeFutureOr(t);
if (normalizedType is FutureOrType) {
_declareBeforeUse(_coreTypes.deprecatedFutureOrClass);
var typeRep = _emitFutureOrTypeWithArgument(
_emitDeferredType(normalizedType.typeArgument));
return emitNullability
? _emitNullabilityWrapper(
typeRep, normalizedType.declaredNullability)
: typeRep;
}
return _emitDeferredType(normalizedType,
emitNullability: emitNullability);
} else if (t is TypeParameterType) {
return _emitTypeParameterType(t, emitNullability: emitNullability);
}
return _emitType(t);
}
assert(isKnownDartTypeImplementor(t));
var savedEmittingDeferredType = _emittingDeferredType;
_emittingDeferredType = true;
var deferredClassRep =
_emitDeferredType(t, emitNullability: emitNullability);
_emittingDeferredType = savedEmittingDeferredType;
return deferredClassRep;
}
bool shouldDefer(InterfaceType t) {
var visited = <DartType>{};
bool defer(DartType t) {
assert(isKnownDartTypeImplementor(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 FutureOrType) {
if (c == _coreTypes.deprecatedFutureOrClass) return true;
if (!visited.add(t)) return false;
if (defer(t.typeArgument)) return true;
return defer(
_coreTypes.deprecatedFutureOrClass.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);
}
js_ast.Expression 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) ??
_emitInterfaceType(t, emitNullability: false);
}
js_ast.Expression getBaseClass(int count) {
var base = emitDeferredType(
c.getThisType(_coreTypes, c.enclosingLibrary.nonNullable),
emitNullability: false);
while (--count >= 0) {
base = js.call('#.__proto__', [base]);
}
return base;
}
/// Returns the "actual" superclass of [c].
///
/// Walks up the superclass chain looking for the first actual class
/// skipping any synthetic classes inserted by the CFE.
Class superClassAsWritten(Class c) {
var superclass = c.superclass;
while (superclass.isAnonymousMixin) {
superclass = superclass.superclass;
}
return superclass;
}
// Find the real (user declared) superclass and the list of mixins.
// We'll use this to unroll the intermediate classes.
//
// TODO(jmesserly): consider using Kernel's mixin unrolling.
var superclass = superClassAsWritten(c);
var supertype = identical(c.superclass, superclass)
? c.supertype.asInterfaceType
: _hierarchy.getClassAsInstanceOf(c, superclass).asInterfaceType;
// All mixins (real and anonymous) classes applied to c.
var mixinApplications = [
if (c.mixedInClass != null) c.mixedInClass,
for (var sc = c.superclass;
sc.isAnonymousMixin && sc.mixedInClass != null;
sc = sc.superclass)
sc,
].reversed.toList();
var hasUnnamedSuper = _hasUnnamedInheritedConstructor(superclass);
void emitMixinConstructors(
js_ast.Expression className, InterfaceType mixin) {
js_ast.Statement mixinCtor;
if (_hasUnnamedConstructor(mixin.classNode)) {
mixinCtor = js.statement('#.#.call(this);', [
emitClassRef(mixin),
_usesMixinNew(mixin.classNode)
? runtimeCall('mixinNew')
: _constructorName('')
]);
}
for (var ctor in superclass.constructors) {
var savedUri = _currentUri;
_currentUri = ctor.enclosingClass.fileUri;
var jsParams = _emitParameters(ctor.function, isForwarding: true);
_currentUri = savedUri;
var name = ctor.name.text;
var ctorBody = [
if (mixinCtor != null) mixinCtor,
if (name != '' || hasUnnamedSuper)
_emitSuperConstructorCall(className, name, jsParams),
];
body.add(_addConstructorToClass(
c, className, name, js_ast.Fun(jsParams, js_ast.Block(ctorBody))));
}
}
var savedTopLevelClass = _classEmittingExtends;
_classEmittingExtends = c;
// Unroll mixins.
if (shouldDefer(supertype)) {
var originalSupertype = supertype;
deferredSupertypes.add(() => runtimeStatement('setBaseClass(#, #)', [
getBaseClass(isMixinAliasClass(c) ? 0 : mixinApplications.length),
emitDeferredType(originalSupertype, emitNullability: false),
]));
// Refers to 'supertype' without type parameters. We remove these from
// the 'extends' clause for generics for cyclic dependencies and append
// them later with 'setBaseClass'.
supertype =
_coreTypes.rawType(supertype.classNode, _currentLibrary.nonNullable);
}
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;
var deferMixin = shouldDefer(m);
var mixinClass = deferMixin
? emitDeferredType(m, emitNullability: false)
: emitClassRef(m);
var classExpr = deferMixin ? getBaseClass(0) : className;
var mixinApplication =
runtimeStatement('applyMixin(#, #)', [classExpr, mixinClass]);
if (deferMixin) {
deferredSupertypes.add(() => mixinApplication);
} else {
body.add(mixinApplication);
}
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:
//
// applyMixin(C, class C$ extends M { <methods> });
var mixinApplicationWithMethods = runtimeStatement('applyMixin(#, #)', [
classExpr,
js_ast.ClassExpression(
_emitTemporaryId(getLocalClassName(c)), mixinClass, methods)
]);
if (deferMixin) {
deferredSupertypes.add(() => mixinApplicationWithMethods);
} else {
body.add(mixinApplicationWithMethods);
}
}
emitMixinConstructors(className, m);
_classEmittingExtends = savedTopLevelClass;
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 (var i = 0; i < mixinApplications.length; i++) {
var m = mixinApplications[i];
var mixinClass = m.isAnonymousMixin ? m.mixedInClass : m;
var mixinType =
_hierarchy.getClassAsInstanceOf(c, mixinClass).asInterfaceType;
var mixinName =
getLocalClassName(superclass) + '_' + getLocalClassName(mixinClass);
var mixinId = _emitTemporaryId(mixinName + '\$');
// Collect all forwarding stubs from anonymous mixins classes. These will
// contain covariant parameter checks that need to be applied.
var forwardingMethodStubs = [
for (var procedure in m.procedures)
if (procedure.isForwardingStub && !procedure.isAbstract)
_emitMethodDeclaration(procedure)
];
// 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,
js_ast.ClassExpression(
_emitTemporaryId(mixinName), baseClass, forwardingMethodStubs)
]));
emitMixinConstructors(mixinId, mixinType);
hasUnnamedSuper = hasUnnamedSuper || _hasUnnamedConstructor(mixinClass);
if (shouldDefer(mixinType)) {
deferredSupertypes.add(() => runtimeStatement('applyMixin(#, #)', [
getBaseClass(mixinApplications.length - i),
emitDeferredType(mixinType, emitNullability: false)
]));
} else {
body.add(runtimeStatement(
'applyMixin(#, #)', [mixinId, emitClassRef(mixinType)]));
}
baseClass = mixinId;
}
if (c.isMixinDeclaration) {
_emitMixinStatement(c, className, baseClass, methods, body);
} else {
body.add(_emitClassStatement(c, className, baseClass, methods));
}
_classEmittingExtends = savedTopLevelClass;
}
/// Defines all constructors for this class as ES5 constructors.
List<js_ast.Statement> _defineConstructors(
Class c, js_ast.Expression className) {
var body = <js_ast.Statement>[];
if (c.isAnonymousMixin || isMixinAliasClass(c)) {
// We already handled this when we defined the class.
return body;
}
void addConstructor(String name, js_ast.Expression jsCtor) {
body.add(_addConstructorToClass(c, className, name, jsCtor));
}
var fields = c.fields;
for (var ctor in c.constructors) {
if (ctor.isExternal) continue;
addConstructor(ctor.name.text, _emitConstructor(ctor, fields, 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,
runtimeCall('mixinNew'),
[_initializeFields(fields)],
className
]));
}
return body;
}
js_ast.Statement _emitClassTypeTests(
Class c, js_ast.Expression className, List<js_ast.Statement> body) {
js_ast.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 runtimeCall('is' + interface.name);
}
}
return null;
}
void markSubtypeOf(js_ast.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);
}
if (c.enclosingLibrary == _coreTypes.coreLibrary &&
(c == _coreTypes.objectClass ||
c == _coreTypes.stringClass ||
c == _coreTypes.functionClass ||
c == _coreTypes.intClass ||
c == _coreTypes.deprecatedNullClass ||
c == _coreTypes.numClass ||
c == _coreTypes.doubleClass ||
c == _coreTypes.boolClass)) {
// Custom type tests for these types are in the patch files.
return null;
}
if (c == _coreTypes.deprecatedFutureOrClass) {
// Custom type tests for FutureOr types are attached when the type is
// constructed in the runtime normalizeFutureOr method.
return null;
}
body.add(runtimeStatement('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 = _emitTemporaryId('_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 runtimeStatement('addTypeTests(#, #)', [defaultInst, isClassSymbol]);
}
void _emitDartSymbols(
Iterable<js_ast.TemporaryId> vars, List<js_ast.ModuleItem> body) {
for (var id in vars) {
body.add(js.statement('const # = Symbol(#)', [id, js.string(id.name)]));
}
}
void _emitSuperHelperSymbols(List<js_ast.Statement> body) {
_emitDartSymbols(
_superHelpers.values.map((m) => m.name as js_ast.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_ast.Statement> body) {
var fields = c.fields
.where((f) => f.isStatic && getRedirectingFactories(f) == null)
.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.text == 'values');
fields.remove(valueField);
fields.add(valueField);
for (var f in fields) {
assert(f.isConst);
body.add(defineValueOnClass(
c,
classRef,
_emitStaticMemberName(f.name.text),
_visitInitializer(f.initializer, f.annotations))
.toStatement());
}
} else if (fields.isNotEmpty) {
body.add(_emitLazyFields(_emitTopLevelName(c), fields,
(n) => _emitStaticMemberName(n.name.text)));
}
}
/// 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_ast.Expression className, List<js_ast.Statement> body) {
void emitExtensions(String helperName, Iterable<String> extensions) {
if (extensions.isEmpty) return;
var names = extensions
.map((e) => propertyName(js_ast.memberNameForDartMember(e)))
.toList();
body.add(js.statement('#.#(#, #);', [
runtimeModule,
helperName,
className,
js_ast.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_ast.Expression className, List<js_ast.Statement> body) {
var savedClass = _classEmittingSignatures;
_classEmittingSignatures = c;
var interfaces = c.implementedTypes.toList()
..addAll(c.superclassConstraints());
if (interfaces.isNotEmpty) {
body.add(js.statement('#[#.implements] = () => [#];', [
className,
runtimeModule,
interfaces.map((i) =>
_emitInterfaceType(i.asInterfaceType, emitNullability: false))
]));
}
void emitSignature(String name, List<js_ast.Property> elements) {
if (elements.isEmpty) return;
if (!name.startsWith('Static')) {
var proto = c == _coreTypes.objectClass
? js.call('Object.create(null)')
: runtimeCall('get${name}s(#.__proto__)', [className]);
elements.insert(0, js_ast.Property(propertyName('__proto__'), proto));
}
body.add(runtimeStatement('set${name}Signature(#, () => #)', [
className,
js_ast.ObjectInitializer(elements, multiline: elements.length > 1)
]));
}
var extMethods = _classProperties.extensionMethods;
var extAccessors = _classProperties.extensionAccessors;
var staticMethods = <js_ast.Property>[];
var instanceMethods = <js_ast.Property>[];
var staticGetters = <js_ast.Property>[];
var instanceGetters = <js_ast.Property>[];
var staticSetters = <js_ast.Property>[];
var instanceSetters = <js_ast.Property>[];
List<js_ast.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;
}
}
}
var classProcedures = c.procedures.where((p) => !p.isAbstract).toList();
for (var member in classProcedures) {
// Static getters/setters/methods cannot be called with dynamic dispatch,
// nor can they be torn off.
if (member.isStatic) continue;
var name = member.name.text;
var reifiedType = _memberRuntimeType(member, c) as FunctionType;
// 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 != _memberRuntimeType(memberOverride, c);
if (needsSignature) {
js_ast.Expression type;
if (member.isAccessor) {
type = _emitType(member.isGetter
? reifiedType.returnType
: reifiedType.positionalParameters[0]);
} else {
type = visitFunctionType(reifiedType, member: member);
}
var property = js_ast.Property(_declareMemberName(member), type);
var signatures = getSignatureList(member);
signatures.add(property);
if (!member.isStatic &&
(extMethods.contains(name) || extAccessors.contains(name))) {
signatures.add(js_ast.Property(
_declareMemberName(member, useExtension: true), type));
}
}
}
emitSignature('Method', instanceMethods);
emitSignature('StaticMethod', staticMethods);
emitSignature('Getter', instanceGetters);
emitSignature('Setter', instanceSetters);
emitSignature('StaticGetter', staticGetters);
emitSignature('StaticSetter', staticSetters);
body.add(runtimeStatement('setLibraryUri(#, #)',
[className, _cacheUri(jsLibraryDebuggerName(c.enclosingLibrary))]));
var instanceFields = <js_ast.Property>[];
var staticFields = <js_ast.Property>[];
var classFields = c.fields.toList();
for (var field in classFields) {
// Only instance fields need to be saved for dynamic dispatch.
var isStatic = field.isStatic;
if (isStatic) continue;
var memberName = _declareMemberName(field);
var fieldSig = _emitFieldSignature(field, c);
(isStatic ? staticFields : instanceFields)
.add(js_ast.Property(memberName, fieldSig));
}
emitSignature('Field', instanceFields);
emitSignature('StaticField', staticFields);
// Add static property dart._runtimeType to Object.
// All other Dart classes will (statically) inherit this property.
if (c == _coreTypes.objectClass) {
body.add(runtimeStatement('lazyFn(#, () => #.#)',
[className, emitLibraryName(_coreTypes.coreLibrary), 'Type']));
}
_classEmittingSignatures = savedClass;
}
js_ast.Expression _emitFieldSignature(Field field, Class fromClass) {
var type = _typeFromClass(field.type, field.enclosingClass, fromClass);
var args = [_emitType(type)];
return runtimeCall(
field.isFinal ? 'finalFieldType(#)' : 'fieldType(#)', [args]);
}
DartType _memberRuntimeType(Member member, Class fromClass) {
var f = member.function;
if (f == null) {
return (member as Field).type;
}
FunctionType result;
if (!f.positionalParameters.any(isCovariantParameter) &&
!f.namedParameters.any(isCovariantParameter)) {
// Avoid tagging a member as Function? or Function*
result = f.computeThisFunctionType(Nullability.nonNullable);
} else {
DartType reifyParameter(VariableDeclaration p) => isCovariantParameter(p)
? _coreTypes.objectRawType(member.enclosingLibrary.nullable)
: p.type;
NamedType reifyNamedParameter(VariableDeclaration p) =>
NamedType(p.name, reifyParameter(p));
// TODO(jmesserly): do covariant type parameter bounds also need to be
// reified as `Object`?
result = FunctionType(f.positionalParameters.map(reifyParameter).toList(),
f.returnType, Nullability.nonNullable,
namedParameters: f.namedParameters.map(reifyNamedParameter).toList()
..sort(),
typeParameters: f
.computeThisFunctionType(member.enclosingLibrary.nonNullable)
.typeParameters,
requiredParameterCount: f.requiredParameterCount);
}
return _typeFromClass(result, member.enclosingClass, fromClass)
as FunctionType;
}
DartType _typeFromClass(DartType type, Class superclass, Class subclass) {
if (identical(superclass, subclass)) return type;
return Substitution.fromSupertype(
_hierarchy.getClassAsInstanceOf(subclass, superclass))
.substituteType(type);
}
js_ast.Expression _emitConstructor(
Constructor node, List<Field> fields, js_ast.Expression className) {
var savedUri = _currentUri;
_currentUri = node.fileUri ?? savedUri;
_staticTypeContext.enterMember(node);
var params = _emitParameters(node.function);
var body = _withCurrentFunction(
node.function,
() => _superDisallowed(
() => _emitConstructorBody(node, fields, className)));
var end = _nodeEnd(node.fileEndOffset);
_currentUri = savedUri;
_staticTypeContext.leaveMember(node);
end ??= _nodeEnd(node.enclosingClass.fileEndOffset);
return js_ast.Fun(params, js_ast.Block(body))..sourceInformation = end;
}
List<js_ast.Statement> _emitConstructorBody(
Constructor node, List<Field> fields, js_ast.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, node.name.text);
// 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_ast.Expression _constructorName(String name) {
if (name == '') {
// Default constructors (factory or not) use `new` as their name.
return propertyName('new');
}
return _emitStaticMemberName(name);
}
js_ast.Statement _emitRedirectingConstructor(
RedirectingInitializer node, js_ast.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.text),
_emitArgumentList(node.arguments, types: false)
]);
}
js_ast.Statement _emitSuperConstructorCallIfNeeded(
Class c, js_ast.Expression className,
[SuperInitializer superInit]) {
if (c == _coreTypes.objectClass) return null;
Constructor ctor;
List<js_ast.Expression> args;
if (superInit == null) {
ctor = unnamedConstructor(c.superclass);
args = [];
} else {
ctor = superInit.target;
args = _emitArgumentList(superInit.arguments, types: false);
}
// 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.text == '' && !_hasUnnamedSuperConstructor(c)) {
return null;
}
return _emitSuperConstructorCall(className, ctor.name.text, args);
}
js_ast.Statement _emitSuperConstructorCall(
js_ast.Expression className, String name, List<js_ast.Expression> args) {
return js.statement('#.__proto__.#.call(this, #);',
[className, _constructorName(name), args ?? []]);
}
bool _hasUnnamedInheritedConstructor(Class c) {
if (c == null) return false;
return _hasUnnamedConstructor(c) || _hasUnnamedSuperConstructor(c);
}
bool _hasUnnamedSuperConstructor(Class c) {
return _hasUnnamedConstructor(c.mixedInClass) ||
_hasUnnamedInheritedConstructor(c.superclass);
}
bool _hasUnnamedConstructor(Class c) {
if (c == null || c == _coreTypes.objectClass) return false;
var ctor = unnamedConstructor(c);
if (ctor != null && !ctor.isSynthetic) return true;
return c.fields.any((f) => !f.isStatic);
}
/// 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_ast.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_ast.Statement>[];
void emitFieldInit(Field f, Expression initializer, TreeNode hoverInfo) {
var virtualField = _classProperties.virtualFields[f];
// Avoid calling getSymbol on _declareMemberName since _declareMemberName
// calls _emitMemberName downstream, which already invokes getSymbol.
var access = virtualField == null
? _declareMemberName(f)
: getSymbol(virtualField);
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;
}
_staticTypeContext.enterMember(f);
emitFieldInit(f, init, f);
_staticTypeContext.leaveMember(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_ast.Statement.from(body);
}
js_ast.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 js_ast.LiteralNull();
return _annotatedNullCheck(annotations)
? notNull(init)
: _visitExpression(init);
}
js_ast.Expression notNull(Expression expr) {
if (expr == null) return null;
var jsExpr = _visitExpression(expr);
if (!isNullable(expr)) return jsExpr;
return runtimeCall('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) {
// TODO(jmesserly): mixin declarations don't get implicit constructor nodes,
// even if they have fields, so we need to ensure they're getting generated.
return mixin.isMixinDeclaration && _hasUnnamedConstructor(mixin) ||
mixin.superclass?.superclass == null &&
mixin.constructors.every((c) => c.isExternal);
}
js_ast.Statement _addConstructorToClass(Class c, js_ast.Expression className,
String name, js_ast.Expression jsCtor) {
jsCtor = defineValueOnClass(c, className, _constructorName(name), jsCtor);
return js.statement('#.prototype = #.prototype;', [jsCtor, className]);
}
@override
bool superclassHasStatic(Class c, String memberName) {
// Note: because we're only considering statics, we can ignore mixins.
// We're only trying to find conflicts due to JS inheriting statics.
var name = Name(memberName, c.enclosingLibrary);
while (true) {
c = c.superclass;
if (c == null) return false;
for (var m in c.members) {
if (m.name == name &&
(m is Procedure && m.isStatic || m is Field && m.isStatic)) {
return true;
}
}
}
}
List<js_ast.Method> _emitClassMethods(Class c) {
var virtualFields = _classProperties.virtualFields;
var jsMethods = <js_ast.Method>[];
var hasJsPeer = _extensionTypes.isNativeClass(c);
var hasIterator = false;
if (c == _coreTypes.objectClass) {
// Dart does not use ES6 constructors.
// Add an error to catch any invalid usage.
jsMethods.add(
js_ast.Method(propertyName('constructor'), js.fun(r'''function() {
throw Error("use `new " + #.typeName(#.getReifiedType(this)) +
".new(...)` to create a Dart object");
}''', [runtimeModule, runtimeModule])));
} else if (c == _jsArrayClass) {
// Provide access to the Array constructor property, so it works like
// other native types (rather than calling the Dart Object "constructor"
// above, which throws).
//
// This will become obsolete when
// https://github.com/dart-lang/sdk/issues/31003 is addressed.
jsMethods.add(js_ast.Method(
propertyName('constructor'), js.fun(r'function() { return []; }')));
}
Set<Member> redirectingFactories;
for (var m in c.fields) {
if (m.isStatic) {
redirectingFactories ??= getRedirectingFactories(m)?.toSet();
} else if (_extensionTypes.isNativeClass(c)) {
jsMethods.addAll(_emitNativeFieldAccessors(m));
} else if (virtualFields.containsKey(m)) {
jsMethods.addAll(_emitVirtualFieldAccessor(m));
}
}
var getters = <String, Procedure>{};
var setters = <String, Procedure>{};
for (var m in c.procedures) {
if (m.isAbstract) continue;
if (m.isGetter) {
getters[m.name.text] = m;
} else if (m.isSetter) {
setters[m.name.text] = m;
}
}
var savedUri = _currentUri;
for (var m in c.procedures) {
_staticTypeContext.enterMember(m);
// For the Dart SDK, we use the member URI because it may be different
// from the class (because of patch files). User code does not need this.
//
// TODO(jmesserly): CFE has a bug(?) where nSM forwarders sometimes have a
// bogus file URI, that is mismatched compared to the offsets. This causes
// a crash when we look up the location. So for those forwarders, we just
// suppress source spans.
_currentUri = m.isNoSuchMethodForwarder ? null : (m.fileUri ?? savedUri);
if (_isForwardingStub(m)) {
// TODO(jmesserly): is there any other kind of forwarding stub?
jsMethods.addAll(_emitCovarianceCheckStub(m));
} else if (m.isFactory) {
if (redirectingFactories?.contains(m) ?? false) {
// Skip redirecting factories (they've already been resolved).
} else {
jsMethods.add(_emitFactoryConstructor(m));
}
} else if (m.isAccessor) {
jsMethods.add(_emitMethodDeclaration(m));
jsMethods.add(_emitSuperAccessorWrapper(m, getters, setters));
if (!hasJsPeer && m.isGetter && m.name.text == 'iterator') {
hasIterator = true;
jsMethods.add(_emitIterable(c));
}
} else {
jsMethods.add(_emitMethodDeclaration(m));
}
_staticTypeContext.leaveMember(m);
}
_currentUri = savedUri;
// 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();
}
bool _isForwardingStub(Procedure member) {
if (member.isForwardingStub || member.isForwardingSemiStub) {
if (_currentLibrary.importUri.scheme != 'dart') return true;
// TODO(jmesserly): external methods in the SDK seem to get incorrectly
// tagged as forwarding stubs even if they are patched. Perhaps there is
// an ordering issue in CFE. So for now we pattern match to see if it
// looks like an actual forwarding stub.
//
// We may be able to work around this in a cleaner way by simply emitting
// the code, and letting the normal covariance check logic handle things.
// But currently we use _emitCovarianceCheckStub to work around some
// issues in the stubs.
var body = member.function.body;
if (body is ReturnStatement) {
var expr = body.expression;
return expr is SuperMethodInvocation || expr is SuperPropertySet;
}
}
return false;
}
/// Emits a method, getter, or setter.
js_ast.Method _emitMethodDeclaration(Procedure member) {
if (member.isAbstract) {
return null;
}
js_ast.Fun fn;
if (member.isExternal && !member.isNoSuchMethodForwarder) {
if (member.isStatic) {
// TODO(vsm): Do we need to handle this case?
return null;
}
fn = _emitNativeFunctionBody(member);
} else {
fn = _emitFunction(member.function, member.name.text);
}
return js_ast.Method(_declareMemberName(member), fn,
isGetter: member.isGetter,
isSetter: member.isSetter,
isStatic: member.isStatic)
..sourceInformation = _nodeEnd(member.fileEndOffset);
}
js_ast.Fun _emitNativeFunctionBody(Procedure node) {
var name = _annotationName(node, isJSAnnotation) ?? node.name.text;
if (node.isGetter) {
var returnValue = js('this.#', [name]);
if (_isNullCheckableNative(node)) {
// Add a potential null-check on native getter if type is non-nullable.
returnValue = runtimeCall('checkNativeNonNull(#)', [returnValue]);
}
return js_ast.Fun([], js.block('{ return #; }', [returnValue]));
} else if (node.isSetter) {
var params = _emitParameters(node.function);
return js_ast.Fun(
params, js.block('{ this.# = #; }', [name, params.last]));
} else {
var returnValue = js('this.#.apply(this, args)', [name]);
if (_isNullCheckableNative(node)) {
// Add a potential null-check on return value if type is non-nullable.
returnValue = runtimeCall('checkNativeNonNull(#)', [returnValue]);
}
return js.fun('function (...args) { return #; }', [returnValue]);
}
}
List<js_ast.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 const [];
var enclosingClass = member.enclosingClass;
var superMember = member.concreteForwardingStubTarget ??
member.abstractForwardingStubTarget;
if (superMember == null) return const [];
DartType substituteType(DartType t) {
return _typeFromClass(t, superMember.enclosingClass, enclosingClass);
}
var name = _declareMemberName(member);
if (member.isSetter) {
if (superMember is Field && isCovariantField(superMember) ||
superMember is Procedure &&
isCovariantParameter(
superMember.function.positionalParameters[0])) {
return const [];
}
var setterType = substituteType(superMember.setterType);
if (_types.isTop(setterType)) return const [];
return [
js_ast.Method(
name,
js.fun('function(x) { return super.# = #; }',
[name, _emitCast(_emitIdentifier('x'), setterType)]),
isSetter: true),
js_ast.Method(name, js.fun('function() { return super.#; }', [name]),
isGetter: true)
];
}
assert(!member.isAccessor);
var superMethodType = substituteType(superMember.function
.computeThisFunctionType(superMember.enclosingLibrary.nonNullable))
as FunctionType;
var function = member.function;
var body = <js_ast.Statement>[];
var typeParameters = superMethodType.typeParameters;
_emitCovarianceBoundsCheck(typeParameters, body);
var typeFormals = _emitTypeFormals(typeParameters);
var jsParams = List<js_ast.Parameter>.from(typeFormals);
var positionalParameters = function.positionalParameters;
for (var i = 0, n = positionalParameters.length; i < n; i++) {
var param = positionalParameters[i];
var jsParam = _emitIdentifier(param.name);
jsParams.add(jsParam);
if (isCovariantParameter(param) &&
!isCovariantParameter(superMember.function.positionalParameters[i])) {
var check = _emitCast(jsParam, superMethodType.positionalParameters[i]);
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 (isCovariantParameter(param) &&
!isCovariantParameter(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 #) #;', [
name,
namedArgumentTemp,
_emitCast(
js_ast.PropertyAccess(namedArgumentTemp, name), paramType.type)
]));
}
}
if (body.isEmpty) return const []; // No checks were needed.
if (namedParameters.isNotEmpty) jsParams.add(namedArgumentTemp);
body.add(js.statement('return super.#(#);', [name, jsParams]));
return [js_ast.Method(name, js_ast.Fun(jsParams, js_ast.Block(body)))];
}
/// Emits a Dart factory constructor to a JS static method.
js_ast.Method _emitFactoryConstructor(Procedure node) {
if (node.isExternal || isUnsupportedFactoryConstructor(node)) return null;
var function = node.function;
/// Note: factory constructors can't use `sync*`/`async*`/`async` bodies
/// because it would return the wrong type, so we can assume `sync` here.
///
/// We can also skip the logic in [_emitFunction] related to operator
/// methods like ==, as well as generic method parameters.
///
/// If a future Dart version allows factory constructors to take their
/// own type parameters, this will need to be changed to call
/// [_emitFunction] instead.
var name = node.name.text;
var jsBody = _emitSyncFunctionBody(function, name);
return js_ast.Method(
_constructorName(name), js_ast.Fun(_emitParameters(function), jsBody),
isStatic: true)
..sourceInformation = _nodeEnd(node.fileEndOffset);
}
@override
js_ast.Expression emitConstructorAccess(InterfaceType type) {
return _emitJSInterop(type.classNode) ??
_emitInterfaceType(type, emitNullability: false);
}
/// 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_ast.Method> _emitVirtualFieldAccessor(Field field) {
var virtualField = _classProperties.virtualFields[field];
var virtualFieldSymbol = getSymbol(virtualField);
var name = _declareMemberName(field);
var getter = js.fun('function() { return this[#]; }', [virtualFieldSymbol]);
var jsGetter = js_ast.Method(name, getter, isGetter: true)
..sourceInformation = _nodeStart(field);
var args = field.isFinal
? [js_ast.Super(), name]
: [js_ast.This(), virtualFieldSymbol];
js_ast.Expression value = _emitIdentifier('value');
if (!field.isFinal && isCovariantField(field)) {
value = _emitCast(value, field.type);
}
args.add(value);
var jsSetter = js_ast.Method(
name, js.fun('function(value) { #[#] = #; }', args),
isSetter: true)
..sourceInformation = _nodeStart(field);
return [jsGetter, jsSetter];
}
/// 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_ast.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_ast.Method>[];
assert(!field.isStatic);
var name = _annotationName(field, isJSName) ?? field.name.text;
// Generate getter
var fn = js_ast.Fun([], js.block('{ return this.#; }', [name]));
var method = js_ast.Method(_declareMemberName(field), fn, isGetter: true);
jsMethods.add(method);
// Generate setter
if (!field.isFinal) {
var value = _emitTemporaryId('value');
fn = js_ast.Fun([value], js.block('{ this.# = #; }', [name, value]));
method = js_ast.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_ast.Method _emitSuperAccessorWrapper(Procedure member,
Map<String, Procedure> getters, Map<String, Procedure> setters) {
if (member.isAbstract) return null;
var name = member.name.text;
var memberName = _declareMemberName(member);
if (member.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 js_ast.Method(memberName, fn, isSetter: true);
}
} else {
assert(member.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 js_ast.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_ast.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, 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) => _jsExportName(m) == 'Symbol.iterator')) {
return null;
}
// Otherwise, emit the adapter method, which wraps the Dart iterator in
// an ES6 iterator.
return js_ast.Method(
js.call('Symbol.iterator'),
js.call('function() { return new #.JsIterator(this.#); }', [
runtimeModule,
_emitMemberName('iterator', memberClass: _coreTypes.iterableClass)
]) as js_ast.Fun);
}
void _registerExtensionType(
Class c, String jsPeerName, List<js_ast.Statement> body) {
var className = _emitTopLevelName(c);
if (_typeRep.isPrimitive(_coreTypes.legacyRawType(c))) {
body.add(runtimeStatement(
'definePrimitiveHashCode(#.prototype)', [className]));
}
body.add(runtimeStatement(
'registerExtension(#, #)', [js.string(jsPeerName), className]));
}
void _emitTopLevelFields(List<Field> fields) {
if (isSdkInternalRuntime(_currentLibrary)) {
/// Treat dart:_runtime fields as safe to eagerly evaluate.
// TODO(jmesserly): it'd be nice to avoid this special case.
var lazyFields = <Field>[];
var savedUri = _currentUri;
// Helper functions to test if a constructor invocation is internal and
// should be eagerly evaluated.
var isInternalConstructor = (ConstructorInvocation node) {
var type = node.getStaticType(_staticTypeContext) as InterfaceType;
var library = type.classNode.enclosingLibrary;
return isSdkInternalRuntime(library);
};
for (var field in fields) {
_staticTypeContext.enterMember(field);
var init = field.initializer;
if (init == null ||
init is BasicLiteral ||
init is ConstructorInvocation && isInternalConstructor(init) ||
init is StaticInvocation && isInlineJS(init.target)) {
if (init is ConstructorInvocation) {
// This is an eagerly executed constructor invocation. We need to
// ensure the class is emitted before this statement.
var type = init.getStaticType(_staticTypeContext) as InterfaceType;
_emitClass(type.classNode);
}
_currentUri = field.fileUri;
moduleItems.add(js.statement('# = #;', [
_emitTopLevelName(field),
_visitInitializer(init, field.annotations)
]));
} else {
lazyFields.add(field);
}
_staticTypeContext.leaveMember(field);
}
_currentUri = savedUri;
fields = lazyFields;
}
if (fields.isEmpty) return;
moduleItems.add(_emitLazyFields(
emitLibraryName(_currentLibrary), fields, _emitTopLevelMemberName));
}
js_ast.Statement _emitLazyFields(
js_ast.Expression objExpr,
Iterable<Field> fields,
js_ast.LiteralString Function(Field f) emitFieldName) {
var accessors = <js_ast.Method>[];
var savedUri = _currentUri;
for (var field in fields) {
_currentUri = field.fileUri;
_staticTypeContext.enterMember(field);
var access = emitFieldName(field);
memberNames[field] = access.valueWithoutQuotes;
accessors.add(js_ast.Method(access, _emitStaticFieldInitializer(field),
isGetter: true)
..sourceInformation = _hoverComment(
js_ast.PropertyAccess(objExpr, access),
field.fileOffset,
field.name.text.length));
// TODO(jmesserly): currently uses a dummy setter to indicate writable.
if (!field.isFinal && !field.isConst) {
accessors.add(js_ast.Method(
access, js.call('function(_) {}') as js_ast.Fun,
isSetter: true));
}
_staticTypeContext.leaveMember(field);
}
_currentUri = savedUri;
return runtimeStatement('defineLazy(#, { # }, #)', [
objExpr,
accessors,
js.boolean(!_currentLibrary.isNonNullableByDefault)
]);
}
js_ast.Fun _emitStaticFieldInitializer(Field field) {
return js_ast.Fun([], js_ast.Block(_withLetScope(() {
return [
js_ast.Return(_visitInitializer(field.initializer, field.annotations))
];
})));
}
List<js_ast.Statement> _withLetScope(
List<js_ast.Statement> Function() visitBody) {
var savedLetVariables = _letVariables;
_letVariables = [];
var body = visitBody();
var letVars = _initLetVariables();
if (letVars != null) body.insert(0, letVars);
_letVariables = savedLetVariables;
return body;
}
js_ast.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_ast.Expression _declareMemberName(Member m, {bool useExtension}) {
return _emitMemberName(m.name.text,
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_ast.Expression _emitMemberName(String name,
{bool isStatic = false,
bool useExtension,
Member member,
Class memberClass}) {
// Static members skip the rename steps and may require JS interop renames.
if (isStatic) {
// TODO(nshahan) Record the name for this member in memberNames.
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 = _jsExportName(member);
if (runtimeName != null) {
var parts = runtimeName.split('.');
if (parts.length < 2) return propertyName(runtimeName);
js_ast.Expression result = _emitIdentifier(parts[0]);
for (var i = 1; i < parts.length; i++) {
result = js_ast.PropertyAccess(result, propertyName(parts[i]));
}
// TODO(nshahan) Record the name for this member in memberNames.
return result;
}
}
memberClass ??= member?.enclosingClass;
if (name.startsWith('_')) {
// Use the library that this private member's name is scoped to.
var memberLibrary = member?.name?.library ??
memberClass?.enclosingLibrary ??
_currentLibrary;
// Wrap the name as a symbol here so it matches what you would find at
// runtime when you get all properties and symbols from an instance.
memberNames[member] = 'Symbol($name)';
return getSymbol(emitPrivateNameSymbol(memberLibrary, name));
}
useExtension ??= _isSymbolizedMember(memberClass, name);
name = js_ast.memberNameForDartMember(name, _isExternal(member));
if (useExtension) {
// TODO(nshahan) Record the name for this member in memberNames.
return getSymbol(getExtensionSymbolInternal(name));
}
var memberName = propertyName(name);
memberNames[member] = memberName.valueWithoutQuotes;
return memberName;
}
/// 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(Class c, String name) {
if (c == null) {
return _isObjectMember(name);
}
c = _typeRep.getImplementationClass(_coreTypes.legacyRawType(c)) ?? c;
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 || _isExternal(member)) {
// If the native member needs to be null-checked and we're running in
// sound null-safety, we require symbolizing it in order to access the
// null-check at the member definition.
if (_isNullCheckableNative(member)) return true;
var jsName = _annotationName(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);
}
final _forwardingCache = 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, Name(name)) ??
_hierarchy.getDispatchTarget(c, Name(name), setter: true));
}
js_ast.LiteralString _emitStaticMemberName(String name, [NamedNode member]) {
if (member != null) {
var jsName = _emitJSInteropStaticMemberName(member);
if (jsName != null) return jsName;
// Allow the Dart SDK to assign names to statics with the @JSExportName
// annotation.
var exportName = _jsExportName(member);
if (exportName != null) return propertyName(exportName);
}
switch (name) {
// Reserved for the compiler to do `x as T`.
case 'as':
// 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);
}
/// If [f] is a function passed to JS, make it throw at runtime when called if
/// it isn't wrapped with `allowInterop`.
///
/// Arguments which are _directly_ wrapped at the site they are passed are
/// unmodified.
Expression _assertInterop(Expression f) {
var type = f.getStaticType(_staticTypeContext);
if (type is FunctionType ||
(type is InterfaceType && type.classNode == _coreTypes.functionClass)) {
if (!isAllowInterop(f)) {
return StaticInvocation(
_assertInteropMethod, Arguments([f], types: [type]));
}
}
return f;
}
js_ast.LiteralString _emitJSInteropStaticMemberName(NamedNode n) {
if (!usesJSInterop(n)) return null;
var name = _annotationName(n, isPublicJSAnnotation);
if (name != null) {
if (name.contains('.')) {
throw 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_ast.PropertyAccess _emitTopLevelNameNoInterop(NamedNode n,
{String suffix = ''}) {
// Some native tests use top-level native methods.
var isTopLevelNative = n is Member && isNative(n);
return js_ast.PropertyAccess(
isTopLevelNative
? runtimeCall('global.self')
: emitLibraryName(getLibrary(n)),
_emitTopLevelMemberName(n, suffix: suffix));
}
js_ast.PropertyAccess _emitFutureOrNameNoInterop({String suffix = ''}) {
return js_ast.PropertyAccess(emitLibraryName(_coreTypes.asyncLibrary),
propertyName('FutureOr' + suffix));
}
/// Emits the member name portion of a top-level member.
///
/// NOTE: usually you should use [_emitTopLevelName] instead of this. This
/// function does not handle JS interop.
js_ast.LiteralString _emitTopLevelMemberName(NamedNode n,
{String suffix = ''}) {
var name = _jsExportName(n) ?? getTopLevelName(n);
return propertyName(name + suffix);
}
bool _isExternal(Member m) {
// Corresponds to the names in memberNameForDartMember in
// compiler/js_names.dart.
const renamedJsMembers = ['prototype', 'constructor'];
if (m is Procedure) {
if (m.isExternal) return true;
if (m.isNoSuchMethodForwarder) {
if (renamedJsMembers.contains(m.name.text)) {
return _hasExternalProcedure(m.enclosingClass, m.name.text);
}
}
}
return false;
}
/// Returns true if anything up the class hierarchy externally defines a
/// procedure with name = [name].
///
/// Used to determine when we should alias Dart-JS reserved members
/// (e.g., 'prototype' and 'constructor').
bool _hasExternalProcedure(Class c, String name) {
var classes = Queue<Class>()..add(c);
while (classes.isNotEmpty) {
var c = classes.removeFirst();
var classesToCheck = [
if (c.supertype != null) c.supertype.classNode,
for (var t in c.implementedTypes)
if (t.classNode != null) t.classNode,
];
classes.addAll(classesToCheck);
for (var procedure in c.procedures) {
if (procedure.name.text == name && !procedure.isNoSuchMethodForwarder) {
return procedure.isExternal;
}
}
}
return false;
}
String _jsNameWithoutGlobal(NamedNode n) {
if (!usesJSInterop(n)) return null;
var libraryJSName = _annotationName(getLibrary(n), isPublicJSAnnotation);
var jsName = _annotationName(n, isPublicJSAnnotation) ?? getTopLevelName(n);
return libraryJSName != null ? '$libraryJSName.$jsName' : jsName;
}
String _emitJsNameWithoutGlobal(NamedNode n) {
if (!usesJSInterop(n)) return null;
setEmitIfIncrementalLibrary(getLibrary(n));
return _jsNameWithoutGlobal(n);
}
js_ast.PropertyAccess _emitJSInterop(NamedNode n) {
var jsName = _emitJsNameWithoutGlobal(n);
if (jsName == null) return null;
return _emitJSInteropForGlobal(jsName);
}
js_ast.PropertyAccess _emitJSInteropForGlobal(String name) {
var parts = name.split('.');
if (parts.isEmpty) parts = [''];
js_ast.PropertyAccess access;
for (var part in parts) {
access = js_ast.PropertyAccess(
access ?? runtimeCall('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(runtimeStatement('copyProperties(#, { # })', [
emitLibraryName(_currentLibrary),
accessors.map(_emitLibraryAccessor).toList()
]));
}
js_ast.Method _emitLibraryAccessor(Procedure node) {
var savedUri = _currentUri;
_staticTypeContext.enterMember(node);
_currentUri = node.fileUri;
var name = node.name.text;
var result = js_ast.Method(
propertyName(name), _emitFunction(node.function, node.name.text),
isGetter: node.isGetter, isSetter: node.isSetter)
..sourceInformation = _nodeEnd(node.fileEndOffset);
_currentUri = savedUri;
_staticTypeContext.leaveMember(node);
return result;
}
js_ast.Statement _emitLibraryFunction(Procedure p) {
var savedUri = _currentUri;
_staticTypeContext.enterMember(p);
_currentUri = p.fileUri;
var body = <js_ast.Statement>[];
var fn = _emitFunction(p.function, p.name.text)
..sourceInformation = _nodeEnd(p.fileEndOffset);
if (_currentLibrary.importUri.scheme == 'dart' &&
_isInlineJSFunction(p.function.body)) {
fn = js_ast.simplifyPassThroughArrowFunCallBody(fn);
}
var nameExpr = _emitTopLevelName(p);
var jsName = _safeFunctionNameForSafari(p.name.text, fn);
body.add(js.statement('# = #',
[nameExpr, js_ast.NamedFunction(_emitTemporaryId(jsName), fn)]));
_currentUri = savedUri;
_staticTypeContext.leaveMember(p);
return js_ast.Statement.from(body);
}
/// Choose a safe name for [fn].
///
/// Most of the time we use [candidateName], except if the name collides
/// with a parameter name and the function contains default parameter values.
///
/// In ES6, functions containing default parameter values, which DDC
/// generates when Dart uses positional optional parameters, cannot have
/// two parameters with the same name. Because we have a similar restriction
/// in Dart, this is not normally an issue we need to pay attention to.
/// However, a bug in Safari makes it a syntax error to have the function
/// name overlap with the parameter names as well. This rename works around
/// such bug (dartbug.com/43520).
static String _safeFunctionNameForSafari(
String candidateName, js_ast.Fun fn) {
if (fn.params.any((p) => p is js_ast.DestructuredVariable)) {
while (fn.params.any((a) => a.parameterName == candidateName)) {
candidateName = '$candidateName\$';
}
}
return candidateName;
}
js_ast.Expression _emitFunctionTagged(js_ast.Expression fn, FunctionType type,
{bool topLevel = false}) {
var lazy = topLevel && !_canEmitTypeAtTopLevel(type);
var typeRep = visitFunctionType(
// Avoid tagging a closure as Function? or Function*
type.withDeclaredNullability(Nullability.nonNullable),
lazy: lazy);
return runtimeCall(lazy ? 'lazyFn(#, #)' : 'fn(#, #)', [fn, typeRep]);
}
/// Whether the expression for [type] can be evaluated at this point in the JS
/// module.
///
/// Types cannot be evaluated if they depend on something that hasn't been
/// defined yet. For example:
///
/// C foo() => null;
/// class C {}
///
/// If we're emitting the type information for `foo`, we cannot refer to `C`
/// yet, so we must evaluate foo's type lazily.
bool _canEmitTypeAtTopLevel(DartType type) {
assert(isKnownDartTypeImplementor(type));
if (type is InterfaceType) {
return !_pendingClasses.contains(type.classNode) &&
type.typeArguments.every(_canEmitTypeAtTopLevel);
}
if (type is FutureOrType) {
return !_pendingClasses.contains(_coreTypes.deprecatedFutureOrClass) &&
_canEmitTypeAtTopLevel(type.typeArgument);
}
if (type is FunctionType) {
// Generic functions are always safe to emit, because they're lazy until
// type arguments are applied.
if (type.typeParameters.isNotEmpty) return true;
return (_canEmitTypeAtTopLevel(type.returnType) &&
type.positionalParameters.every(_canEmitTypeAtTopLevel) &&
type.namedParameters.every((n) => _canEmitTypeAtTopLevel(n.type)));
}
if (type is TypedefType) {
return type.typeArguments.every(_canEmitTypeAtTopLevel);
}
return true;
}
/// Emits a Dart [type] into code.
js_ast.Expression _emitType(DartType type) => type.accept(this);
js_ast.Expression _emitInvalidNode(Node node, [String message = '']) {
if (message.isNotEmpty) message += ' ';
return runtimeCall('throwUnimplementedError(#)',
[js.escapedString('node <${node.runtimeType}> $message`$node`')]);
}
@override
js_ast.Expression defaultDartType(DartType type) => _emitInvalidNode(type);
@override
js_ast.Expression visitInvalidType(InvalidType type) => defaultDartType(type);
@override
js_ast.Expression visitDynamicType(DynamicType type) =>
runtimeCall('dynamic');
@override
js_ast.Expression visitVoidType(VoidType type) => runtimeCall('void');
@override
js_ast.Expression visitNullType(NullType type) =>
_emitInterfaceType(_coreTypes.deprecatedNullType);
@override
js_ast.Expression visitNeverType(NeverType type) =>
type.nullability == Nullability.nullable
? visitNullType(const NullType())
: _emitNullabilityWrapper(runtimeCall('Never'), type.nullability);
/// Normalizes `FutureOr` types.
///
/// Any changes to the normalization logic here should be mirrored in the
/// classes.dart runtime library method named `normalizeFutureOr`.
DartType _normalizeFutureOr(FutureOrType futureOr) {
var typeArgument = futureOr.typeArgument;
if (typeArgument is DynamicType) {
// FutureOr<dynamic> --> dynamic
return typeArgument;
}
if (typeArgument is VoidType) {
// FutureOr<void> --> void
return typeArgument;
}
if (typeArgument is InterfaceType &&
typeArgument.classNode == _coreTypes.objectClass) {
// Normalize FutureOr of Object, Object?, Object*.
var nullable = futureOr.nullability == Nullability.nullable ||
typeArgument.nullability == Nullability.nullable;
var legacy = futureOr.nullability == Nullability.legacy ||
typeArgument.nullability == Nullability.legacy;
var nullability = nullable
? Nullability.nullable
: legacy
? Nullability.legacy
: Nullability.nonNullable;
return typeArgument.withDeclaredNullability(nullability);
} else if (typeArgument is NeverType) {
// FutureOr<Never> --> Future<Never>
return InterfaceType(
_coreTypes.futureClass, futureOr.nullability, [typeArgument]);
} else if (typeArgument is NullType) {
// FutureOr<Null> --> Future<Null>?
return InterfaceType(
_coreTypes.futureClass, Nullability.nullable, [typeArgument]);
} else if (futureOr.declaredNullability == Nullability.nullable &&
typeArgument.nullability == Nullability.nullable) {
// FutureOr<T?>? --> FutureOr<T?>
return futureOr.withDeclaredNullability(Nullability.nonNullable);
}
// The following is not part of the normalization spec but this is a
// convenient place to perform this change of nullability consistently. This
// only applies at compile-time and is not needed in the runtime version of
// the FutureOr normalization.
// FutureOr<T%>% --> FutureOr<T%>
//
// If the type argument has undetermined nullability the CFE propagates
// it to the FutureOr type as well. In this case we can represent the
// FutureOr type without any nullability wrappers and rely on the runtime to
// handle the nullability of the instantiated type appropriately.
if (futureOr.nullability == Nullability.undetermined &&
typeArgument.nullability == Nullability.undetermined) {
return futureOr.withDeclaredNullability(Nullability.nonNullable);
}
return futureOr;
}
@override
js_ast.Expression visitInterfaceType(InterfaceType type) =>
_emitInterfaceType(type);
@override
js_ast.Expression visitExtensionType(ExtensionType type) =>
type.onType.accept(this);
@override
js_ast.Expression visitFutureOrType(FutureOrType type) {
var normalizedType = _normalizeFutureOr(type);
return normalizedType is FutureOrType
? _emitFutureOrType(normalizedType)
: normalizedType.accept(this);
}
/// Emits the representation of [type].
///
/// Will avoid emitting the type wrappers for null safety when
/// [emitNullability] is `false` to avoid cases where marking [type] with
/// nullability information makes no sense in the context.
js_ast.Expression _emitInterfaceType(InterfaceType type,
{bool emitNullability = true}) {
var c = type.classNode;
_declareBeforeUse(c);
js_ast.Expression typeRep;
// 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 { ... }
<