// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:collection'; import 'dart:convert'; import 'dart:io' as io; import 'dart:math' show max, min; import 'package:_js_interop_checks/src/js_interop.dart' show getDartJSInteropJSName, hasDartJSInteropAnnotation; import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart' show ExtensionIndex; import 'package:front_end/src/api_prototype/external_effect.dart' show ExternalEffect; import 'package:front_end/src/api_unstable/ddc.dart'; import 'package:js_shared/synced/embedded_names.dart' show JsGetName, JsBuiltin; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/clone.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 '../command/options.dart' show Options; import '../compiler/js_names.dart' as js_ast; import '../compiler/js_utils.dart' as js_ast; import '../compiler/module_builder.dart' show isSdkInternalRuntimeUri, libraryUriToJsIdentifier; import '../compiler/module_containers.dart' show ModuleItemContainer; import '../compiler/rewrite_async.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 'compiler.dart' as old; import 'constants.dart'; import 'future_or_normalizer.dart'; import 'hot_reload_delta_inspector.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_environment.dart'; import 'type_recipe_generator.dart'; import 'type_table.dart'; /// The groups of compiled arguments ready to be flattened into a single /// [js_ast.Array] for statically sound calls or into separate packets for /// runtime checked calls. typedef _ArgumentGroups = ({ List? typeArguments, List? positionalArguments, List? namedArguments, }); /// Name used as a prefix for extension symbols and the identifier of the object /// used to store them. final _extensionSymbolHolderName = 'dartx'; /// A prefix for symbols used to store lazily evaluated field values. /// /// Names prefixed with this must not be reset across hot reloads. final _fieldValueStorePrefix = '_#v_'; /// Symbol data used to map library members kernel nodes to identifiers used /// in the compiled JavaScript. /// /// This data is intended to be serialized and consumed by the debugger. class SymbolData { /// 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 = {}; /// 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 = {}; /// Maps each `Procedure` 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 procedureIdentifiers = {}; /// 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 = {}; } /// Compiles a kernel [Component] to a bundle of individual libraries. class LibraryBundleCompiler implements old.Compiler { final ClassHierarchy _hierarchy; final Options _options; final Map _importToSummary; final Map _summaryToModule; final CoreTypes _coreTypes; final Ticker? _ticker; final _symbolData = SymbolData(); final _libraryCompilers = {}; LibraryBundleCompiler( Component component, this._hierarchy, this._options, this._importToSummary, this._summaryToModule, { CoreTypes? coreTypes, this._ticker, }) : _coreTypes = coreTypes ?? CoreTypes(component); @override Map get classIdentifiers => _symbolData.classIdentifiers; @override Map get memberNames => _symbolData.memberNames; @override Map get procedureIdentifiers => _symbolData.procedureIdentifiers; @override Map get variableIdentifiers => _symbolData.variableIdentifiers; @override js_ast.Program emitModule(Component component) { assert(_options.emitLibraryBundle); _ticker?.logMs('Emitting library bundle'); // When there is hot reload metadata, update the mappings to the nodes in // the current LibraryIndex before passing it on the library compilers. var repo = component.metadata[hotReloadLibraryMetadataTag] as HotReloadLibraryMetadataRepository?; repo?.mapToIndexedNodes(LibraryIndex.all(component)); var metadata = repo?.mapping; var compiledLibraries = []; for (var library in component.libraries) { var libraryCompiler = LibraryCompiler( component, _hierarchy, _options, _importToSummary, _summaryToModule, coreTypes: _coreTypes, ticker: _ticker, symbolData: _symbolData, hotReloadLibraryMetadata: metadata?[library], hotReloadGeneration: repo?.generation, ); _libraryCompilers[library] = libraryCompiler; compiledLibraries.add(libraryCompiler.emitLibrary(library)); } // TODO(nshahan): Nothing about these symbols requires them to be // represented in a library. These could be moved to a construct outside // of the language that is provided to libraries that need it. if (component.libraries.contains(_coreTypes.coreLibrary)) { // Collect all extension symbols from all SDK libraries. var allSymbols = { for (var compiler in _libraryCompilers.values) ...compiler._extensionSymbols, }; // Create dartx library var id = js_ast.Identifier(_extensionSymbolHolderName); var statements = [ for (var entry in allSymbols.entries) js.statement('# = Symbol(#);', [ js_ast.PropertyAccess(id, js.string(entry.key)), js.string('$_extensionSymbolHolderName.${entry.key}'), ]), js.statement('# = #', [ js_ast.PropertyAccess(id, js.call('dartDevEmbedder.linkSymbol')), js_ast.NamedFunction( js_ast.ScopedId('link__$_extensionSymbolHolderName'), js_ast.Fun(const [], js_ast.Block(const [])), ), ]), ]; compiledLibraries.insert( 0, js_ast.Program( statements, name: _extensionSymbolHolderName, librarySelfVar: id, // This construct is synthetic and has no Dart sources to attribute // the size to. dartSize: 0, ), ); } return js_ast.LibraryBundle( compiledLibraries, name: _options.moduleName, header: _generateCompilationHeader(), ); } @override js_ast.Fun emitFunctionIncremental( List items, Library library, Class? cls, FunctionNode functionNode, String name, ) { return _libraryCompilers[library]!._emitFunctionIncremental( items, library, cls, functionNode, name, ); } /// Creates header comments with helpful compilation information. List _generateCompilationHeader() { var headerOptions = [ if (_options.canaryFeatures) 'canary', if (_options.emitLibraryBundle) 'emitLibraryBundle', 'enableAsserts(${_options.enableAsserts})', ]; var enabledExperiments = []; _options.experiments.forEach((key, value) { if (value) enabledExperiments.add(key); }); var header = [ js_ast.Comment( 'Generated by DDC, the Dart Development Compiler (to JavaScript).', ), if (const bool.fromEnvironment('dart.library.io')) js_ast.Comment('Version: ${io.Platform.version}'), js_ast.Comment('Module: ${_options.moduleName}'), js_ast.Comment('Flags: ${headerOptions.join(', ')}'), if (enabledExperiments.isNotEmpty) js_ast.Comment('Experiments: ${enabledExperiments.join(', ')}'), ]; return header; } } /// Tracks the state of which branch the compiler is on for hot reload checks. /// /// This allows hot reload generation checks to be batched into a single /// branch statement. /// /// This batched branching allows us to avoid exponential behavior when /// recursing on deeply nested checked calls. Otherwise each branch makes 2 /// copies of all the sub-branches leading to 2^n checks. enum HotReloadBranchState { /// The compiler is not along any hot reload check branch yet. none, /// The compiler is along the branch with no extra checks. The check rewrite /// logic will be skipped and the normal call will be generated. The root of /// this branch will include a hot reload generation check. uncheckedBranch, /// The compiler is along the branch with extra checks. The check rewrite /// logic will be applied for every call that needs it. The root of this /// branch will include a hot reload generation check. checkedBranch, } class LibraryCompiler extends ComputeOnceConstantVisitor with OnceConstantVisitorDefaultMixin, StatementVisitorInternalNodeMixin, StatementVisitorExperimentExclusionMixin, ExpressionVisitorInternalNodeMixin, ExpressionVisitorExperimentExclusionMixin implements StatementVisitor, ExpressionVisitor { final Options _options; final SymbolData _symbolData; HotReloadBranchState hotReloadCheckedBranch = HotReloadBranchState.none; /// 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. Map get classIdentifiers => _symbolData.classIdentifiers; /// Maps every mixin application to a unique identifier. /// /// A mixin application is represented as a (mixin, class) pair, where /// 'mixin' is being mixed into 'class'. Anonymous mixins are already /// unique per mixin application and so pass themselves in as both 'mixin' /// and 'class'. /// /// This mapping is used when generating super property getters in mixins. final Map<(Class, Class), js_ast.Identifier> _mixinCache = {}; /// Records a reference to a mixin application's passed in superclass. /// (see [_emitMixinStatement]). final Map _mixinSuperclassCache = {}; /// 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. Map get memberNames => _symbolData.memberNames; /// Maps each `Procedure` 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. Map get procedureIdentifiers => _symbolData.procedureIdentifiers; /// 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. Map get variableIdentifiers => _symbolData.variableIdentifiers; /// Identifiers for kernel variables with an analogous identifier in JS. /// /// [VariableDeclaration.name] is not necessarily a safe identifier for JS /// transpiled code. The same name can be used in shadowing contexts. We map /// each kernel variable to a [js_ast.ScopedId] so that at code emission /// time, references that would be shadowed are given a unique name. If there /// is no risk of shadowing, the original name will be used. final Map _variableTempIds = {}; /// 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 _importToSummary; /// Maps a Kernel summary to the JS import name for the module. /// /// An entry must exist for every reachable component. final Map _summaryToModule; /// The variable for the current catch clause VariableDeclaration? _rethrowParameter; /// Temporary variables mapped to their corresponding JavaScript variable. final _tempVariables = {}; /// Let variables collected for the given function. List? _letVariables; final _constTable = js_ast.Identifier('CT'); /// Constant getters used to populate the constant table. final _constLazyAccessors = []; /// Container for holding the results of lazily-evaluated constants. var _constTableCache = ModuleItemContainer.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; /// The current type environment of type parameters introduced to the scope /// via generic classes and functions. DDCTypeEnvironment _currentTypeEnvironment = const EmptyTypeEnvironment(); final TypeRecipeGenerator _typeRecipeGenerator; /// Visitor used for testing static invocations in the dart:_rti library to /// determine if they are suitable for inlining at call sites. final BasicInlineTester _inlineTester; /// 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; late Component _component; /// The current library being compiled. Library? _currentLibrary; /// The current function being compiled, if any. FunctionNode? _currentFunction; /// Library link method statements that perform class hierarchy connections /// like `class C extends E`. final List _classExtendsLinks = []; /// Library link method statements that define extension members on mixin /// classes. final List _mixinClassDefineExtensionMemberLinks = []; /// Library link method statements that define extension members on classes. final List _defineExtensionMemberLinks = []; /// Library link method statements that apply mixins. final List _mixinApplicationLinks = []; /// Library link method statements that apply extensions on native types. final List _nativeExtensionLinks = []; /// Library link method statements that create type rules. final List _typeRuleLinks = []; /// Holds additional initialization logic for enum fields. final List _enumExtensions = []; /// 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. late 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; bool _optimizeNonVirtualFieldAccess = true; final _superHelpers = {}; /// Cache for the results of calling [_requiresRtiForInstantiation]. final _requiresRtiForInstantiationCache = {}; /// Reserved parameter used to reference RTI objects passed to generic /// constructors/factories and generic method signatures. final _rtiParam = js_ast.ScopedId('_ti'); // 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 _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 _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.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.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(); /// Maps Kernel constants to their JS aliases. final _constAliasCache = HashMap(); /// Maps uri strings in asserts and elsewhere to hoisted identifiers. var _uriContainer = ModuleItemContainer.asArray('I'); /// Index of extension and extension type members in order to filter static /// interop members. // TODO(srujzs): Is there some way to share this from the js_util_optimizer to // avoid having to recompute? final ExtensionIndex _extensionIndex; /// When inside a `[]=` operator, this will be a non-null value that should be /// returned by any `return;` statement. /// /// This lets DDC use the setter method's return value directly. final _operatorSetResultStack = []; /// Private member names in this module, organized by their library. final _privateNames = HashMap>(); /// Holds all top-level JS symbols (used for caching or indexing fields). final _symbolContainer = ModuleItemContainer.asObject( 'S', keyToString: (js_ast.Identifier i) => i.name, ); /// Extension member symbols for adding Dart members to JS types. /// /// These are added to the [_extensionSymbolsLibraryId]; see that field for more /// information. final _extensionSymbols = {}; /// The set of libraries we are currently compiling, and the temporaries used /// to refer to them. final _libraries = {}; /// Imported libraries, and the temporaries used to refer to them. final _imports = {}; /// Incremental mode for expression compilation. /// /// If set to true, triggers emitting all used types, symbols, libraries, /// constants, urs inside the generated function. bool _incrementalMode = false; /// Modules and libraries accessed during compilation in incremental mode. final _incrementalModules = >{}; /// The identifier used to reference DDC's core "dart:_runtime" library from /// generated JS code, typically called "dart" e.g. `dart.dcall`. late final js_ast.Identifier _runtimeLibraryId; /// The library referred to by [_runtimeLibraryId]. final Library _runtimeLibrary; /// The identifier used to reference DDC's "extension method" symbols, used to /// safely add Dart-specific member names to JavaScript classes, such as /// primitive types (e.g. String) or DOM types in "dart:html". late final js_ast.Identifier _extensionSymbolsLibraryId; /// The identifier used to reference DDC's core "dart:_rti" library from /// generated JS code. /// /// Must manually name the dart:_rti library because there are local variables /// within the library that inadvertently shadow the default name. final _rtiLibraryId = js_ast.ScopedId('dart_rti'); /// The library referred to by [_rtiLibraryId]. final Library _rtiLibrary; /// The `Rti` class defined in [_rtiLibrary]. final Class _rtiClass; /// Whether we're currently building the SDK, which may require special /// bootstrapping logic. /// /// This is initialized by [emitModule], which must be called before /// accessing this field. late final bool _isBuildingSdk; /// Whether or not to move top level symbols into top-level containers. /// /// This is set in both [emitModule] and [_emitLibrary]. /// Depends on [_isBuildingSdk]. bool _containerizeSymbols = false; /// The temporary variable that stores named arguments (these are passed via a /// JS object literal, to match JS conventions). final _namedArgumentTemp = js_ast.ScopedId('opts'); /// The list of output module items, in the order they need to be emitted in. final _moduleItems = []; /// The entrypoint method of a dynamic module, if any. Procedure? _dynamicEntrypoint; final Class _jsArrayClass; final Class _privateSymbolClass; final Class _linkedHashMapImplClass; final Class _identityHashMapImplClass; final Class _linkedHashSetClass; final Class _linkedHashSetImplClass; final Class _identityHashSetImplClass; // Helpers for async function lowering final Member _asyncStartMember; final Member _asyncAwaitMember; final Member _asyncReturnMember; final Member _asyncRethrowMember; final Member _asyncMakeCompleterMember; final Member _asyncWrapJsFunctionMember; // Helpers for sync* function lowering final Member _syncStarMakeIterableMember; final Member _syncStarIteratorCurrentMember; final Member _syncStarIteratorDatumMember; final Member _syncStarIteratorYieldStarMember; // Helpers for async* function lowering final Member _asyncStarHelperMember; final Member _asyncStreamOfControllerMember; final Member _asyncMakeAsyncStarStreamControllerMember; final Member _asyncIterationMarkerYieldSingleMember; final Member _asyncIterationMarkerYieldStarMember; final Class _asyncStreamIteratorClass; final Procedure _assertInteropMethod; // The direct `_as` methods for primitive types. final Member _asBool; final Member _asDouble; final Member _asInt; final Member _asNum; final Member _asObject; final Member _asString; final Member _asBoolQ; final Member _asDoubleQ; final Member _asIntQ; final Member _asNumQ; final Member _asStringQ; final DevCompilerConstants _constants; final NullableInference _nullableInference; bool _moduleEmitted = false; /// Supports verbose logging with a timer. Ticker? _ticker; /// The hot reload generation for the current compile. /// /// Initial compiles are treated as generation 0. int _hotReloadGeneration; /// The hot reload metadata for the current compile. /// /// Will be `null` on the initial compile. final HotReloadLibraryMetadata? _hotReloadLibraryMetadata; /// Whether the library is being compiled for a hot reload. bool get _compileForHotReload => _hotReloadGeneration > 0; factory LibraryCompiler( Component component, ClassHierarchy hierarchy, Options options, Map importToSummary, Map summaryToModule, { CoreTypes? coreTypes, Ticker? ticker, required SymbolData symbolData, HotReloadLibraryMetadata? hotReloadLibraryMetadata, int? hotReloadGeneration, }) { 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 LibraryCompiler._( ticker, coreTypes, coreTypes.index, nativeTypes, constants, types, hierarchy, jsTypeRep, NullableInference(jsTypeRep, staticTypeContext, options: options), staticTypeContext, options, importToSummary, summaryToModule, symbolData, hotReloadLibraryMetadata, hotReloadGeneration ?? 0, ); } LibraryCompiler._( this._ticker, this._coreTypes, LibraryIndex sdk, this._extensionTypes, this._constants, this._types, this._hierarchy, this._typeRep, this._nullableInference, this._staticTypeContext, this._options, this._importToSummary, this._summaryToModule, this._symbolData, this._hotReloadLibraryMetadata, this._hotReloadGeneration, ) : _jsArrayClass = sdk.getClass('dart:_interceptors', 'JSArray'), _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:_js_helper', 'LinkedSet'), _identityHashSetImplClass = sdk.getClass( 'dart:_js_helper', 'IdentitySet', ), _assertInteropMethod = sdk.getTopLevelProcedure( 'dart:_runtime', 'assertInterop', ), _asyncStartMember = sdk.getTopLevelMember( 'dart:async', '_asyncStartSync', ), _asyncAwaitMember = sdk.getTopLevelMember('dart:async', '_asyncAwait'), _asyncReturnMember = sdk.getTopLevelMember('dart:async', '_asyncReturn'), _asyncRethrowMember = sdk.getTopLevelMember( 'dart:async', '_asyncRethrow', ), _asyncMakeCompleterMember = sdk.getTopLevelMember( 'dart:async', '_makeAsyncAwaitCompleter', ), _asyncWrapJsFunctionMember = sdk.getTopLevelMember( 'dart:async', '_wrapJsFunctionForAsync', ), _syncStarMakeIterableMember = sdk.getTopLevelMember( 'dart:async', '_makeSyncStarIterable', ), _syncStarIteratorCurrentMember = sdk.getMember( 'dart:async', '_SyncStarIterator', '_current', ), _syncStarIteratorDatumMember = sdk.getMember( 'dart:async', '_SyncStarIterator', '_datum', ), _syncStarIteratorYieldStarMember = sdk.getMember( 'dart:async', '_SyncStarIterator', '_yieldStar', ), _asyncStarHelperMember = sdk.getTopLevelMember( 'dart:async', '_asyncStarHelper', ), _asyncStreamOfControllerMember = sdk.getTopLevelMember( 'dart:async', '_streamOfController', ), _asyncMakeAsyncStarStreamControllerMember = sdk.getTopLevelMember( 'dart:async', '_makeAsyncStarStreamController', ), _asyncIterationMarkerYieldSingleMember = sdk.getMember( 'dart:async', '_IterationMarker', 'yieldSingle', ), _asyncIterationMarkerYieldStarMember = sdk.getMember( 'dart:async', '_IterationMarker', 'yieldStar', ), _asyncStreamIteratorClass = sdk.getClass('dart:async', 'StreamIterator'), _futureOrNormalizer = FutureOrNormalizer(_coreTypes), _typeRecipeGenerator = TypeRecipeGenerator(_coreTypes, _hierarchy), _extensionIndex = ExtensionIndex( _coreTypes, _staticTypeContext.typeEnvironment, ), _inlineTester = BasicInlineTester(_constants), _runtimeLibrary = sdk.getLibrary('dart:_runtime'), _rtiLibrary = sdk.getLibrary('dart:_rti'), _rtiClass = sdk.getClass('dart:_rti', 'Rti'), _asBool = sdk.getTopLevelMember('dart:_rti', '_asBool'), _asDouble = sdk.getTopLevelMember('dart:_rti', '_asDouble'), _asInt = sdk.getTopLevelMember('dart:_rti', '_asInt'), _asNum = sdk.getTopLevelMember('dart:_rti', '_asNum'), _asObject = sdk.getTopLevelMember('dart:_rti', '_asObject'), _asString = sdk.getTopLevelMember('dart:_rti', '_asString'), _asBoolQ = sdk.getTopLevelMember('dart:_rti', '_asBoolQ'), _asDoubleQ = sdk.getTopLevelMember('dart:_rti', '_asDoubleQ'), _asIntQ = sdk.getTopLevelMember('dart:_rti', '_asIntQ'), _asNumQ = sdk.getTopLevelMember('dart:_rti', '_asNumQ'), _asStringQ = sdk.getTopLevelMember('dart:_rti', '_asStringQ'); /// The library for dart:core in the SDK. Library get _coreLibrary => _coreTypes.coreLibrary; /// The type used for private Dart [Symbol]s. InterfaceType get _privateSymbolType => _coreTypes.nonNullableRawType(_privateSymbolClass); /// The type used for public Dart [Symbol]s. InterfaceType get _internalSymbolType => _coreTypes.nonNullableRawType(_coreTypes.internalSymbolClass); final FutureOrNormalizer _futureOrNormalizer; bool _inFunctionExpression = false; bool _inAsyncExpression = false; /// Returns whether or not [uri] can be hot reloaded. /// /// These packages are not updated during the runtime of a standard program, /// allowing us to make the following optimizations: /// - methods, getters, and setters are emitted without additional levels /// of indirection /// - fields are emitted without additional lazy initialization checks. bool _isNonHotReloadableResource(Uri uri) { return (uri.isScheme('dart')) || (uri.isScheme('package') && _options.nonHotReloadablePackages.contains(uri.pathSegments[0])); } /// Module can be emitted only once, and the compiler can be reused after /// only in incremental mode, for expression compilation only. js_ast.Program emitLibrary(Library library) { if (_moduleEmitted) { throw StateError('Can only call emitLibrary once.'); } _ticker?.logMs('Emitting library'); _currentLibrary = library; _component = library.enclosingComponent!; _isBuildingSdk = library.importUri.scheme == 'dart'; // 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(); library.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 = _startLibrary(library); _nullableInference.allowNotNullDeclarations = _isBuildingSdk; _typeTable = TypeTable('T', _runtimeCall); // 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); _ticker?.logMs('Added table declarations'); // Visit the 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. _emitLibrary(library); _ticker?.logMs('Emitted library: ${library.importUri}'); // Emit hoisted assert strings _moduleItems.insertAll(safeDeclarationIndex, _uriContainer.emit()); _moduleItems.insertAll(safeDeclarationIndex, _constTableCache.emit()); if (_constLazyAccessors.isNotEmpty) { var constTableBody = _runtimeStatement('defineLazy(#, { # })', [ _constTable, _constLazyAccessors, ]); _moduleItems.insert(_constTableInsertionIndex, constTableBody); _constLazyAccessors.clear(); } // Register the local const cache for this module so it can be cleared on a // hot restart. if (_constTableCache.isNotEmpty) { _moduleItems.add( _runtimeCall('moduleConstCaches.set(#, #)', [ js_ast.string(_options.moduleName), _constTableCache.containerId, ]).toStatement(), ); } _ticker?.logMs('Added table caches'); // Add all type hierarchy rules for the interface types used in this module. // TODO(nshahan) This is likely more information than the application // really uses. It could be reduced to only the types of values that are // potentially "live" in the module which includes the types of all the // constructor invocations and the types of the constructors torn off // (potentially constructed) within the module. The current constructor // tearoff lowering does make this harder to know since all constructors // appeared to be invoked in the body of the method created by the // lowering. For now we over estimate and simply use all the interface // types introduced by all the classes defined in the module. for (var cls in library.classes) { var type = cls.getThisType(_coreTypes, Nullability.nonNullable); _typeRecipeGenerator.addLiveTypeAncestries(type); } var universeClass = _rtiLibrary.classes.firstWhere( (cls) => cls.name == '_Universe', ); // Emits either an 'addRules', 'addOrUpdateRules', or 'deleteRules' // statement for a JSON-serializable [rules] made of RTI type rules. // // 'addRules' overrides existing state. Calling this function multiple // times is safe for types whose hierarchies can be exhaustively // discovered at compile-time, which is true for all types that aren't // 'LegacyJavaScriptObject'. // // TODO: The above assumption may not hold if a class's hierarchy // changes after a hot reload. Outdated 'addRules' invocations (during // linking) may clobber updated type rules. js_ast.Statement emitRulesStatement( Object? rules, { required String rulesFunction, }) { var template = '#._Universe.#(#, JSON.parse(#))'; var rulesExpr = js.call(template, [ _emitLibraryName(_rtiLibrary), _emitMemberName(rulesFunction, memberClass: universeClass), _runtimeCall('typeUniverse'), js.string(jsonEncode(rules), "'"), ]); return rulesExpr.toStatement(); } // We must emit type rules for every interface type encountered by DDC, // with several caveats: // 1) 'LegacyJavaScriptObject' has special treatment. Its hierarchy // accumulates across libraries and must always be emitted in 'append' // mode ('addOrUpdateRules') to avoid clobbering its previous state. // 2) We manually add rules for mutual subtype relationships between // 'LegacyJavaScriptObject' and all JavaScript interop classes. There is // special redirecting rule logic in the dart:_rti library for interop // types because otherwise they would duplicate a lot of supertype // information. // 3) The RTI treats an empty type hierarchy as implicitly containing // 'Object'. We explicitly emit 'deleteRules' instructions in case // a type hierarchy was deleted or edited to extend 'Object' after hot // reload. var legacyJavaScriptObjectRecipe = _typeRecipeGenerator.interfaceTypeRecipe( _coreTypes.index.getClass('dart:_interceptors', 'LegacyJavaScriptObject'), ); var legacyJavaScriptObjectRules = _typeRecipeGenerator .liveInterfaceTypeRules[legacyJavaScriptObjectRecipe]; var typeRulesExceptLegacyJavaScriptObject = _typeRecipeGenerator.liveInterfaceTypeRules ..remove(legacyJavaScriptObjectRecipe); var typesThatOnlyExtendObject = Set.from(_typeRecipeGenerator.visitedInterfaceTypeRecipes) ..removeAll(typeRulesExceptLegacyJavaScriptObject.keys) ..remove(legacyJavaScriptObjectRecipe); var legacyJavaScriptObjectMutualSubtypingRules = _typeRecipeGenerator.updateLegacyJavaScriptObjectRules; if (typeRulesExceptLegacyJavaScriptObject.isNotEmpty) { _typeRuleLinks.add( emitRulesStatement( typeRulesExceptLegacyJavaScriptObject, rulesFunction: 'addRules', ), ); } if (_compileForHotReload && typesThatOnlyExtendObject.isNotEmpty) { _typeRuleLinks.add( emitRulesStatement( typesThatOnlyExtendObject.toList(), rulesFunction: 'deleteRules', ), ); } if (legacyJavaScriptObjectRules != null) { _typeRuleLinks.add( emitRulesStatement({ legacyJavaScriptObjectRecipe: legacyJavaScriptObjectRules, }, rulesFunction: 'addOrUpdateRules'), ); } if (legacyJavaScriptObjectMutualSubtypingRules.isNotEmpty) { _typeRuleLinks.add( emitRulesStatement( legacyJavaScriptObjectMutualSubtypingRules, rulesFunction: 'addOrUpdateRules', ), ); } var jsInteropTypeRecipes = _typeRecipeGenerator.visitedJsInteropTypeRecipes; if (jsInteropTypeRecipes.isNotEmpty) { // Update the `LegacyJavaScriptObject` class with the type tags for all // interop types in this module. This is the quick path for simple type // tests that matches the rules encoded above. var legacyJavaScriptObjectClass = _coreTypes.index.getClass( 'dart:_interceptors', 'LegacyJavaScriptObject', ); var legacyJavaScriptObjectClassRef = _emitClassRef( legacyJavaScriptObjectClass.getThisType( _coreTypes, Nullability.nonNullable, ), ); var interopRecipesArray = js_ast.stringArray([ _typeRecipeGenerator.interfaceTypeRecipe(legacyJavaScriptObjectClass), ...jsInteropTypeRecipes, ]); var jsInteropRules = _runtimeStatement('addRtiResources(#, #)', [ legacyJavaScriptObjectClassRef, interopRecipesArray, ]); _typeRuleLinks.add(jsInteropRules); } // Annotates the type parameter variances for each interface. var typeVariances = _typeRecipeGenerator.variances; if (typeVariances.isNotEmpty) { var addTypeParameterVariancesTemplate = '#._Universe.#(#, JSON.parse(#))'; var addTypeParameterVariancesStatement = js.call(addTypeParameterVariancesTemplate, [ _emitLibraryName(_rtiLibrary), _emitMemberName( 'addTypeParameterVariances', memberClass: universeClass, ), _runtimeCall('typeUniverse'), js.string(jsonEncode(typeVariances), "'"), ]).toStatement(); _typeRuleLinks.add(addTypeParameterVariancesStatement); } // Certain RTIs must be emitted during RTI normalization. We cache these // eagerly with 'findType' (without normalization) to avoid infinite loops. // See normalization functions in: sdk/lib/_internal/js_shared/lib/rti.dart if (_isSdkInternalRuntime(_currentLibrary!)) { var prerequisiteRtiTypes = [_coreTypes.objectNullableRawType]; prerequisiteRtiTypes.forEach((type) { var recipe = _typeRecipeGenerator .recipeInEnvironment(type, EmptyTypeEnvironment()) .recipe; _typeRuleLinks.add( js.call('#.findType("$recipe")', [ _emitLibraryName(_rtiLibrary), ]).toStatement(), ); }); } // Additional method used by the module system to link class hierarchies. _moduleItems.add(_emitLibraryLinkMethod(_currentLibrary!)); _ticker?.logMs('Emitted library link method'); // Attach the import Uri to the library object for use in error messages // and debugging. _moduleItems.add( js.statement('#[#] = #', [ _emitLibraryName(library), _runtimeCall('libraryImportUri'), js.string('${library.importUri}'), ]), ); // Visit directives (for exports) _emitExports(library); _ticker?.logMs('Emitted exports'); // Declare imports and extension symbols _emitImportsAndExtensionSymbols( items, forceExtensionSymbols: allowedNativeTest(library.importUri), ); _ticker?.logMs('Emitted imports and extension symbols'); // Emit the hoisted type table cache variables items.addAll(_typeTable.dischargeBoundTypes()); _ticker?.logMs('Emitted type table'); var compiledLibrary = _finishLibrary( items, '${library.importUri}', _emitLibraryName(library), ); _ticker?.logMs('Finished emitting module'); // Mark as finished for incremental mode, so it is safe to // switch to the incremental mode for expression compilation. _moduleEmitted = true; return compiledLibrary; } /// Returns a method that will perform all class hierarchy operations for the /// classes defined in this module. /// /// At a high level this method performs the prototype stitching for all /// `class A extends B` relationships but in practice will also include the /// operations that implicitly depend on those relationships to be established /// so they can walk the prototype chain. js_ast.Statement _emitLibraryLinkMethod(Library library) { var libraryName = _emitLibraryName(library); var nameExpr = js_ast.PropertyAccess( libraryName, js.call('dartDevEmbedder.linkSymbol'), ); var functionName = _emitScopedId('link__${_jsLibraryName(library)}'); var parameters = const []; var body = js_ast.Block([ ..._classExtendsLinks, // The ordering of extensions member definition and mixin applications // is fragile but important for the correct functionality of the html and // friends libraries. All mixins should have extension members defined // before being applied. Mixin classes are handled here, regular mixins // are handled inside the mixin application closure. ..._mixinClassDefineExtensionMemberLinks, ..._mixinApplicationLinks, // Extension members defined and mixed in above will be discovered during // the prototype walk during these extension member definitions. ..._defineExtensionMemberLinks, ..._nativeExtensionLinks, ..._typeRuleLinks, // Enum extensions must be emitted after type hierarchies have stabilized. ..._enumExtensions, ]); var function = js_ast.NamedFunction( functionName, js_ast.Fun(parameters, body), ); return js.statement('# = #', [nameExpr, function]); } /// Choose a canonical name from the [library] element. String _jsLibraryName(Library library) { return libraryUriToJsIdentifier(library.importUri); } /// Choose a module-unique name from the [library] element. /// /// Returns null if no alias exists or there are multiple output paths /// (e.g., when compiling the Dart SDK). /// /// This never uses the library's name (the identifier in the `library` /// declaration) as it doesn't have any meaningful rules enforced. String? _jsLibraryAlias(Library library) { var uri = library.importUri.normalizePath(); if (uri.isScheme('dart')) return null; Iterable segments; if (uri.isScheme('package')) { // Strip the package name. segments = uri.pathSegments.skip(1); } else { segments = uri.pathSegments; } var qualifiedPath = js_ast.pathToJSIdentifier( p.withoutExtension(segments.join('/')), ); return qualifiedPath == _jsLibraryName(library) ? null : qualifiedPath; } /// Debugger friendly name for a Dart [library]. String _jsLibraryDebuggerName(Library library) => '${library.importUri}'; /// True when [library] is the sdk internal library 'dart:_internal'. bool _isDartInternal(Library library) => _isDartLibrary(library, '_internal'); /// True when [library] is the sdk internal library 'dart:_js_helper'. bool _isDartJsHelper(Library library) => _isDartLibrary(library, '_js_helper'); /// True when [library] is the sdk internal library 'dart:_internal'. bool _isDartForeignHelper(Library library) => _isDartLibrary(library, '_foreign_helper'); /// True when [library] is the sdk library 'dart:js_util'. bool _isDartJsUtil(Library library) => _isDartLibrary(library, 'js_util'); /// Returns true if [library] is identified by [name]. bool _isDartLibrary(Library library, String name) { var importUri = library.importUri; return importUri.isScheme('dart') && importUri.path == name; } /// Returns true if the library [l] is "dart:_runtime". bool _isSdkInternalRuntime(Library l) { return isSdkInternalRuntimeUri(l.importUri); } /// Gets the module import URI that contains [library]. String _libraryToModule(Library library, {bool throwIfNotFound = true}) { if (library.importUri.isScheme('dart')) { // TODO(jmesserly): we need to split out HTML. return js_ast.dartSdkModule; } var summary = _importToSummary[library]; if (summary == null) { if (throwIfNotFound) { throw StateError('Could not find summary for library "$library".'); } return ''; } var moduleName = _summaryToModule[summary]; if (moduleName == null) { if (throwIfNotFound) { throw StateError( 'Could not find module name for library "$library" ' 'from component "$summary".', ); } return ''; } return moduleName; } void _emitLibrary(Library library) { _staticTypeContext.enterLibrary(_currentLibrary!); if (_isBuildingSdk) { _containerizeSymbols = _isWebLibrary(library.importUri); } if (_isSdkInternalRuntime(library)) { // Add embedded globals. _moduleItems.add( _runtimeCall('typeUniverse = #', [ js_ast.createRtiUniverse(), ]).toStatement(), ); // `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. _emitLibraryMembers(library); library.classes.forEach(_emitClass); } else { library.classes.forEach(_emitClass); _emitLibraryMembers(library); } // Creating a function and setting the library object as the prototype // serves as a signal to V8 that the members of the library should get // optimized for fast lookup. // Do not remove without testing for performance regressions. _moduleItems.add( js.statement('(function() {}).prototype = #', [ _libraries[_currentLibrary!], ]), ); _staticTypeContext.leaveLibrary(_currentLibrary!); } void _emitExports(Library library) { library.additionalExports.forEach(_emitExport); } 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(node); _moduleItems.add( js.statement('#.# = #;', [ _emitLibraryName(library), name.selector, name, ]), ); } } /// Called to emit class declarations. /// /// Class hierarchy links are collected but not emitted as part of the /// declaration. Those operations will be contained in the link method for the /// library. void _emitClass(Class c) { // Avoid attempting to compile classes we reach through emitting class // extends supertypes when they are not members of the library being // compiled. // TODO(nshahan): Once `_declareBeforeUse` is removed this escape hatch will // no longer be necessary. if (c.enclosingLibrary != _currentLibrary) return; var savedClass = _currentClass; var savedLibrary = _currentLibrary; var savedUri = _currentUri; _currentClass = c; _currentLibrary = c.enclosingLibrary; _currentUri = c.fileUri; var savedTypeEnvironment = _currentTypeEnvironment; // When compiling the type heritage of the class we can't reference an rti // object attached to an instance. Instead we construct a type environment // manually when needed. Later we use the rti attached to an instance for // a simpler representation within instance members of the class. _currentTypeEnvironment = ClassTypeEnvironment(c.typeParameters); // Store identifiers for a mixin application's passed in superclass. // (see [_emitMixinStatement]). if (c.isMixinDeclaration && !c.isMixinClass) { _mixinSuperclassCache.putIfAbsent( c, () => _emitScopedId(getLocalClassName(c.superclass!)), ); } // Mixins are unrolled in _defineClass. if (!c.isAnonymousMixin) { // If this class is annotated with `@JS`, then we only need to emit the // non-external factories and static members. if (!hasJSInteropAnnotation(c)) { _moduleItems.add(_emitClassDeclaration(c)); } else { var interopClassDef = _emitJSInteropClassNonExternalMembers(c); if (interopClassDef != null) _moduleItems.add(interopClassDef); } } // 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; _currentTypeEnvironment = savedTypeEnvironment; } static js_ast.Identifier _emitIdentifier(String name) => js_ast.Identifier(js_ast.toJSIdentifier(name)); static js_ast.ScopedId _emitScopedId( String name, { bool needsCapture = false, }) => js_ast.ScopedId(js_ast.toJSIdentifier(name), needsCapture: needsCapture); js_ast.Statement _emitClassDeclaration(Class c) { var className = _emitTopLevelNameNoExternalInterop(c); var savedClassProperties = _classProperties; _classProperties = ClassPropertyModel.build( _types, _extensionTypes, _virtualFields, c, ); var body = []; // 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 jsProperties = _emitClassProperties(c); var jsStaticMethodTypeTags = []; for (var member in c.procedures) { // TODO(#57049): We tag all static members because we don't know if // they've been changed after a hot reload. This won't be necessary if we // can tag them during the delta diff phase. if (member.isStatic && _reifyTearoff(member) && !member.isExternal) { var propertyAccessor = _emitStaticTarget(member); var result = js.call('#.#', [ propertyAccessor.receiver, propertyAccessor.selector, ]); // We only need to tag static functions that are torn off at // compile-time. We attach these at late so tearoffs have access to // their types. var reifiedType = member.function.computeThisFunctionType( Nullability.nonNullable, ); jsStaticMethodTypeTags.add( _emitFunctionTagged(result, reifiedType, asLazy: true).toStatement(), ); } } _emitSuperHelperSymbols(body); // Emit the class, e.g. `core.Object = class Object { ... }` _defineClass(c, className, jsProperties, body); body.addAll(jsCtors); body.addAll(jsStaticMethodTypeTags); // Emit things that come after the ES6 `class ... { ... }`. /// Collects all implemented types in the ancestry of [cls]. Iterable transitiveImplementedTypes(Class cls) { var allImplementedTypes = {}; var toVisit = ListQueue()..addAll(cls.implementedTypes); if (cls.isMixinApplication) { // Implemented types can come through the immediate mixin so we seed // the search with it as well. var mixedInType = cls.mixedInType; if (mixedInType != null) toVisit.add(mixedInType); } while (toVisit.isNotEmpty) { var supertype = toVisit.removeFirst(); var superclass = supertype.classNode; if (allImplementedTypes.contains(supertype) || superclass == _coreTypes.objectClass) { continue; } toVisit.addAll(superclass.supers); // Skip encoding the synthetic classes in the type rules because they // will never be instantiated or appear in type tests. if (superclass.isAnonymousMixin) continue; allImplementedTypes.add(supertype); } return allImplementedTypes; } // Tag all classes with the resources needed by the dart:_rti library. var name = _typeRecipeGenerator.interfaceTypeRecipe(c); var implementedRecipes = [ name, for (var type in transitiveImplementedTypes(c)) _typeRecipeGenerator.interfaceTypeRecipe(type.classNode), ]; body.add( _runtimeStatement('addRtiResources(#, #)', [ className, js_ast.stringArray(implementedRecipes), ]), ); _emitClassSignature(c, className, body); _initExtensionSymbols(c); if (c.isMixinClass || c.isLegacyMixinEligible(_coreTypes)) { _defineExtensionMembers(className, _mixinClassDefineExtensionMemberLinks); } else if (!c.isMixinDeclaration) { _defineExtensionMembers(className, _defineExtensionMemberLinks); } var typeFormals = c.typeParameters; if (typeFormals.isNotEmpty) { var genericClassStmts = _defineGenericClass( typeFormals, js_ast.Statement.from(body), ); body = [...genericClassStmts]; } if (c == _coreTypes.objectClass) { // Avoid polluting the native JavaScript Object prototype with the members // of the Dart Core Object class. // Instead, just assign the identity equals method. _nativeExtensionLinks.add(_runtimeStatement('_installIdentityEquals()')); } else { for (var peer in _extensionTypes.getNativePeers(c)) { _registerExtensionType(c, peer, body); } } _classProperties = savedClassProperties; return js_ast.Statement.from(body); } /// Emits a class declaration for the JS interop class [c] for any /// non-external factories or static members. /// /// If [c] is not an interop class or does not contain non-external factories /// or static members, returns null. js_ast.Statement? _emitJSInteropClassNonExternalMembers(Class c) { if (!hasJSInteropAnnotation(c)) return null; var className = _emitTopLevelNameNoExternalInterop(c); // Non-external procedures and statics are still emitted var nonExternalProperties = []; // Add factories and static methods. for (var procedure in c.procedures) { if (procedure.isExternal) continue; // Don't emit tear-offs for @staticInterop members as they're disallowed. if (_isStaticInteropTearOff(procedure)) continue; if (procedure.isFactory && !procedure.isRedirectingFactory) { // Skip redirecting factories (they've already been resolved). var factory = _emitFactoryConstructor(procedure); if (factory != null) nonExternalProperties.add(factory); } else if (procedure.isStatic) { var staticMethod = _emitMethodDeclaration(procedure); if (staticMethod != null) nonExternalProperties.add(staticMethod); } } // Add static fields and setters. var staticFields = c.fields .where((f) => f.isStatic && !f.isExternal) .toList(); var staticFieldNames = Set.of(staticFields.map((f) => f.name)); var staticSetters = c.procedures.where( (p) => p.isStatic && p.isAccessor && staticFieldNames.contains(p.name), ); var members = [...staticFields, ...staticSetters]; if (members.isNotEmpty) { nonExternalProperties.addAll( _emitLazyMembers( _emitTopLevelNameNoExternalInterop(c), members, (n) => _emitStaticMemberName(n.name.text), ), ); } // Avoid unnecessary code emission if there are no members we care about. if (nonExternalProperties.isNotEmpty) { // Note that this class has no heritage. This class should never be used // as a type. It's merely a placeholder for static members. var body = _emitClassStatement(c, className, null, nonExternalProperties); var typeFormals = c.typeParameters; if (typeFormals.isNotEmpty) { var genericClassStmts = _defineGenericClass( typeFormals, js_ast.Statement.from(body), ); body = genericClassStmts; } return js_ast.Statement.from(body); } return null; } /// Emits a generic class with additional initialization logic. List _defineGenericClass( List formals, js_ast.Statement body, ) { assert(formals.isNotEmpty); return [..._typeTable.dischargeFreeTypes(formals), body]; } List _emitClassStatement( Class c, js_ast.Expression className, js_ast.Expression? heritage, List properties, ) { var localClassName = getLocalClassName(c); var classIdentifier = _emitScopedId(localClassName); var classDefIdentifier = _emitScopedId('_$localClassName'); // In order to ensure that 'super' binds to the correct target across hot // reloads, we rebind its pre-assigned class definition's prototype during // link phase (alongside the embedder-resolved class definition's). var referencesSuperKeyword = hasSuper(c); if (_options.emitDebugSymbols) classIdentifiers[c] = classIdentifier; if (heritage != null) { if (referencesSuperKeyword) { _classExtendsLinks.add( _runtimeStatement('classExtends(#, #)', [ classDefIdentifier, heritage, ]), ); } _classExtendsLinks.add( _runtimeStatement('classExtends(#, #)', [className, heritage]), ); } var classExpr = js_ast.ClassExpression(classIdentifier, null, properties); var libraryExpr = (className as js_ast.PropertyAccess).receiver; var propertyExpr = className.selector; return referencesSuperKeyword ? [ js.statement('let # = #;', [classDefIdentifier, classExpr]), _runtimeStatement('declareClass(#, #, #)', [ libraryExpr, propertyExpr, classDefIdentifier, ]), ] : [ _runtimeStatement('declareClass(#, #, #)', [ libraryExpr, propertyExpr, 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 properties, List body, ) { var staticProperties = properties.where((m) => m.isStatic).toList(); var instanceProperties = properties.where((m) => !m.isStatic).toList(); body.addAll(_emitClassStatement(c, className, heritage, staticProperties)); var superclassId = _mixinSuperclassCache[c]!; var classId = className is js_ast.Identifier ? className : _emitScopedId(getLocalClassName(c)); var mixinMemberClass = js_ast.ClassExpression( classId, superclassId, instanceProperties, ); js_ast.Node arrowFnBody = mixinMemberClass; var extensionInit = []; // The extension members need to be defined here when the class is created // inside the `mixinOn` closure. The prototype chain is connected in this // closure as well so it is safe to perform this operation here instead of // the link method. _defineExtensionMembers(classId, extensionInit); if (extensionInit.isNotEmpty) { extensionInit.insert(0, mixinMemberClass.toStatement()); extensionInit.add(classId.toReturn()); arrowFnBody = js_ast.Block(extensionInit); } body.add( js.statement('#[#] = #', [ className, _runtimeCall('mixinOn'), js_ast.ArrowFun([superclassId], arrowFnBody), ]), ); } /// Emits code required to represent [c] as a series of statements in [body]. /// /// [properties] holds methods, fields, or properties in [c]. void _defineClass( Class c, js_ast.Expression className, List properties, List body, ) { if (c == _coreTypes.objectClass) { body.addAll(_emitClassStatement(c, className, null, properties)); return; } js_ast.Expression emitClassRef( InterfaceType t, { bool resolvedFromEmbedder = false, }) { return _emitJSInterop(t.classNode) ?? _emitClassRef(t, resolvedFromEmbedder: resolvedFromEmbedder); } // 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, Class mixinSuperclass, Class mixinClass, InterfaceType mixin, ) { for (var ctor in mixinSuperclass.constructors) { var savedUri = _currentUri; _currentUri = ctor.enclosingClass.fileUri; var sharedParams = _emitParameters(ctor.function, isForwarding: true); var mixinConstructorParams = [ if (_requiresRtiForInstantiation(mixinSuperclass)) _rtiParam, ...sharedParams, ]; var superConstructorArgs = [ if (_requiresRtiForInstantiation(ctor.enclosingClass)) js_ast.LiteralNull(), ...sharedParams, ]; js_ast.Statement? mixinCtor; if (_hasUnnamedConstructor(mixin.classNode)) { var mixinRti = _requiresRtiForInstantiation(mixin.classNode) ? js_ast.LiteralNull() : null; mixinCtor = js.statement('#.#.call(this, #);', [ emitClassRef(mixin), _usesMixinNew(mixin.classNode) ? _runtimeCall('mixinNew') : _constructorName(''), [?mixinRti], ]); } var name = ctor.name.text; var ctorBody = [ ?mixinCtor, if (name != '' || hasUnnamedSuper) _emitSuperConstructorCall( ctor, className, name, superConstructorArgs, ), ]; // TODO(nshahan) Record the name for this constructor in memberNames. body.add( _addConstructorToClass( c, className, _constructorName(name), js_ast.Fun(mixinConstructorParams, js_ast.Block(ctorBody)), ), ); _currentUri = savedUri; } } var savedTopLevelClass = _classEmittingExtends; _classEmittingExtends = c; // Unroll mixins. var baseClass = emitClassRef(supertype); // The SDK is never hot reloaded, so we can avoid the overhead of // resolving their classes through the embedder. var embedderResolvedBaseClass = emitClassRef( supertype, resolvedFromEmbedder: !_isBuildingSdk, ); // 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 mixinId = _emitMixinId(m, m.isAnonymousMixin ? m : c); // Collect all forwarding stub members from anonymous mixins classes. // These can contain covariant parameter checks that need to be applied. var savedClassProperties = _classProperties; _classProperties = ClassPropertyModel.build( _types, _extensionTypes, _virtualFields, m, ); var forwardingMembers = { for (var procedure in m.procedures) if (procedure.isForwardingStub && !procedure.isAbstract) procedure.name.text: procedure, }; // Mixin applications can introduce their own reference to the type // parameters from the class being mixed in and their use can appear in // the forwarding stubs. var savedTypeEnvironment = _currentTypeEnvironment; if (m.typeParameters.isNotEmpty) { assert(_currentTypeEnvironment is ClassTypeEnvironment); _currentTypeEnvironment = ClassTypeEnvironment(m.typeParameters); } var forwardingMethodStubs = []; for (var s in forwardingMembers.values) { // Members are marked as "forwarding stubs" when they require a type // check of the arguments before calling super. It is assumed here that // no getters will be marked as a "forwarding stub". assert(!s.isGetter); var stub = _emitMethodDeclaration(s); if (stub != null) forwardingMethodStubs.add(stub); // If there are getters matching the setters somewhere above in the // class hierarchy we must also generate a forwarding getter due to the // representation used in the compiled JavaScript. if (s.isSetter) { var getterWrapper = _emitSuperAccessorWrapper(s, const {}, const {}); if (getterWrapper != null) forwardingMethodStubs.add(getterWrapper); } } _currentTypeEnvironment = savedTypeEnvironment; _classProperties = savedClassProperties; // Mixins need to be exposed in this library in case they are // referenced in a super getter. // TODO(markzipan): We originally bound mixin classes to a temporary as a // workaround for a now-resolved Chrome issue. However, a side effect of // this operation is that mixin IDs are renamed by the local visitor. We // can remove this hoisting after we give mixins unique names. body.add( js.statement('let # = #', [ mixinId, _runtimeCall('declareClass(#, #, #)', [ _emitLibraryName(_currentLibrary!), js.string(mixinId.name), js_ast.ClassExpression( _emitScopedId('${mixinId.name}\$'), null, forwardingMethodStubs, ), ]), ]), ); _classExtendsLinks.add( _runtimeStatement('classExtends(#, #)', [ mixinId, embedderResolvedBaseClass, ]), ); emitMixinConstructors(mixinId, superclass, mixinClass, mixinType); hasUnnamedSuper = hasUnnamedSuper || _hasUnnamedConstructor(mixinClass); var mixinTargetLabel = js.string(fullyResolvedMixinClassLabel(m)); // The SDK is never hot reloaded, so we can avoid the overhead of // resolving their classes through the embedder. _mixinApplicationLinks.add( _runtimeStatement('applyMixin(#, #, #)', [ mixinId, emitClassRef(mixinType, resolvedFromEmbedder: !_isBuildingSdk), mixinTargetLabel, ]), ); baseClass = mixinId; embedderResolvedBaseClass = mixinId; } if (c.isMixinDeclaration && !c.isMixinClass) { _emitMixinStatement(c, className, baseClass, properties, body); } else { body.addAll( _emitClassStatement( c, className, embedderResolvedBaseClass, properties, ), ); } _classEmittingExtends = savedTopLevelClass; } /// Defines all constructors for this class as ES5 constructors. List _defineConstructors( Class c, js_ast.Expression className, ) { var body = []; if (c.isAnonymousMixin) { // We already handled this when we defined the class. return body; } void addConstructor(js_ast.LiteralString 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; var constructorName = _constructorName(ctor.name.text); memberNames[ctor] = constructorName.valueWithoutQuotes; addConstructor( constructorName, _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; } void _emitDartSymbols( Iterable vars, List body, ) { for (var id in vars) { body.add(js.statement('const # = Symbol(#)', [id, js.string(id.name)])); } } void _emitSuperHelperSymbols(List body) { _emitDartSymbols( _superHelpers.values.map((m) => m.name as js_ast.ScopedId), body, ); _superHelpers.clear(); } /// 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 body, ) { void emitExtensions(String helperName, Iterable extensions) { if (extensions.isEmpty) return; var names = extensions .map((e) => _propertyName(js_ast.memberNameForDartMember(e))) .toList(); body.add( _runtimeStatement('#(#, #)', [ 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 body, ) { var savedTypeEnvironment = _currentTypeEnvironment; _currentTypeEnvironment = RtiTypeEnvironment( _currentTypeEnvironment.classTypeParameters, ); var savedClass = _classEmittingSignatures; _classEmittingSignatures = c; void emitSignature(String name, List elements) { if (elements.isEmpty) return; js_ast.Statement setSignature; if (!name.startsWith('Static')) { var proto = c == _coreTypes.objectClass ? js.call('Object.create(null)') : _runtimeCall('get${name}s(#)', [ _emitJSObjectGetPrototypeOf( className, fullyQualifiedName: true, ), ]); setSignature = _runtimeStatement('set${name}Signature(#, () => #)', [ className, _emitJSObjectSetPrototypeOf( js_ast.ObjectInitializer(elements, multiline: elements.length > 1), proto, fullyQualifiedName: true, ), ]); } else { // TODO(40273) Only tagging with the names of static members until the // debugger consumes signature information from symbol files. setSignature = _runtimeStatement('set${name}Signature(#, () => #)', [ className, js_ast.ArrayInitializer(elements.map((e) => e.name).toList()), ]); } body.add(setSignature); } js_ast.Expression emitClassFieldSignature(Field field, Class fromClass) { var fieldType = _typeFromClass( field.type, field.enclosingClass!, fromClass, ).extensionTypeErasure; var uri = fieldType is InterfaceType ? _cacheUri( _jsLibraryDebuggerName(fieldType.classNode.enclosingLibrary), ) : null; var isConst = js.boolean(field.isConst); var isFinal = js.boolean(field.isFinal); var type = _emitType(fieldType); var typeResolver = js_ast.ArrowFun([_rtiParam], type); return uri == null ? js('{type: #, isConst: #, isFinal: #}', [ typeResolver, isConst, isFinal, ]) : js('{type: #, isConst: #, isFinal: #, libraryUri: #}', [ typeResolver, isConst, isFinal, uri, ]); } var extMethods = _classProperties!.extensionMethods; var extAccessors = _classProperties!.extensionAccessors; var staticMethods = []; var instanceMethods = []; var instanceMethodsDefaultTypeArgs = []; var methodsImmediateTarget = []; var staticGetters = []; var instanceGetters = []; var staticSetters = []; var instanceSetters = []; List getSignatureList(Procedure p) { // TODO(40273) Skip for all statics when the debugger consumes signature // information from symbol files. 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 cannot be called with dynamic dispatch or torn // off. Static methods can't be called with dynamic dispatch and are // tagged with a type when torn off. Most are implicitly const and // canonicalized. Static signatures are only used by the debugger and are // not needed for runtime correctness. // TODO(40273) Skip for all statics when the debugger consumes signature // information from symbol files. if (isTearOffLowering(member)) 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); var memberName = _declareMemberName( member, useExtension: member.isToStringOrNoSuchMethod, ); if (!member.isAccessor) { var immediateTarget = js.string(fullyResolvedTargetLabel(member)); methodsImmediateTarget.add( js_ast.Property(memberName, immediateTarget), ); } if (needsSignature) { js_ast.Expression type; if (member.isAccessor) { // These signatures are used for dynamic access and to inform the // debugger. The `arrayRti` accessor is only used by the dart:_rti // library internals and should not be included in the accessible // signatures. if (c == _jsArrayClass && name == 'arrayRti') continue; type = _emitType( member.isGetter ? reifiedType.returnType : reifiedType.positionalParameters[0], ); } else { type = _emitType(reifiedType); if (!member.isStatic && reifiedType.typeParameters.isNotEmpty) { // Instance methods with generic type parameters require extra // information to support dynamic calls. The default values for the // type parameters are encoded into a separate storage object for // use at runtime. var defaultTypeArgs = js_ast.ArrayInitializer([ for (var parameter in reifiedType.typeParameters) _emitType(parameter.defaultType), ]); var typeResolver = js_ast.ArrowFun([_rtiParam], defaultTypeArgs); var property = js_ast.Property(memberName, typeResolver); instanceMethodsDefaultTypeArgs.add(property); // As seen below, sometimes the member signatures are added again // using the extension symbol as the name. That logic is duplicated // here to ensure there are always default type arguments accessible // via the same name as the signature. // TODO(52867): Cleanup default type argument duplication. if (extMethods.contains(name) || extAccessors.contains(name)) { var property = js_ast.Property( _declareMemberName(member, useExtension: true), typeResolver, ); instanceMethodsDefaultTypeArgs.add(property); } } } var typeResolver = js_ast.ArrowFun([_rtiParam], type); var property = js_ast.Property(memberName, typeResolver); var signatures = getSignatureList(member); signatures.add(property); if (!member.isStatic && (extMethods.contains(name) || extAccessors.contains(name))) { // TODO(52867): Cleanup signature duplication. var typeResolver = js_ast.ArrowFun([_rtiParam], type); var property = js_ast.Property( _declareMemberName(member, useExtension: true), typeResolver, ); signatures.add(property); } } } emitSignature('Method', instanceMethods); emitSignature('MethodsDefaultTypeArg', instanceMethodsDefaultTypeArgs); emitSignature('MethodsImmediateTarget', methodsImmediateTarget); // TODO(40273) Skip for all statics when the debugger consumes signature // information from symbol files. 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 = []; var staticFields = []; var classFields = c.fields.toList(); for (var field in classFields) { // Static fields cannot be called with dynamic dispatch or torn off. The // signatures are only used by the debugger and are not needed for runtime // correctness. var memberName = _declareMemberName(field); var fieldSig = emitClassFieldSignature(field, c); var property = js_ast.Property(memberName, fieldSig); // TODO(40273) Skip static fields when the debugger consumes signature // information from symbol files. (field.isStatic ? staticFields : instanceFields).add(property); } emitSignature('Field', instanceFields); // TODO(40273) Skip for all statics when the debugger consumes signature // information from symbol files. emitSignature('StaticField', staticFields); _classEmittingSignatures = savedClass; _currentTypeEnvironment = savedTypeEnvironment; } 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)) { result = f.computeThisFunctionType(Nullability.nonNullable); } else { var fComputed = f.computeThisFunctionType(Nullability.nonNullable); var fComputedNamedByName = { for (NamedType namedParameter in fComputed.namedParameters) namedParameter.name: namedParameter, }; DartType reifyParameter( VariableDeclaration parameter, DartType fComputedParameter, ) => isCovariantParameter(parameter) ? _coreTypes.objectNullableRawType : fComputedParameter; NamedType reifyNamedParameter( VariableDeclaration parameter, NamedType fComputedNamedParameter, ) { assert(parameter.name == fComputedNamedParameter.name); return NamedType( parameter.name!, reifyParameter(parameter, fComputedNamedParameter.type), ); } // TODO(jmesserly): do covariant type parameter bounds also need to be // reified as `Object`? result = FunctionType( List.generate( f.positionalParameters.length, (index) => reifyParameter( f.positionalParameters[index], fComputed.positionalParameters[index], ), ), f.returnType, Nullability.nonNullable, namedParameters: List.generate( f.namedParameters.length, (index) => reifyNamedParameter( f.namedParameters[index], fComputedNamedByName[f.namedParameters[index].name]!, ), )..sort(), typeParameters: fComputed.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 fields, js_ast.Expression className, ) { var savedUri = _currentUri; _currentUri = node.fileUri; _staticTypeContext.enterMember(node); var savedTypeEnvironment = _currentTypeEnvironment; _currentTypeEnvironment = ClassTypeEnvironment( node.enclosingClass.typeParameters, ); var params = []; // Generic class constructors accept their RTI as their first argument. params.addAll(_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); var constructor = js_ast.Fun([ if (_requiresRtiForInstantiation(node.enclosingClass)) _rtiParam, ...params, ], js_ast.Block(body))..sourceInformation = end; _currentTypeEnvironment = savedTypeEnvironment; return constructor; } List _emitConstructorBody( Constructor node, List 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); // Class instances with type arguments are bound to their RTI on creation. // This must be bound early, as instantiated fields may reference this RTI. if (_requiresRtiForInstantiation(cls)) { // Only set the rti if there isn't one already. This avoids superclasses // overwriting the value already set by a subclass. var rtiProperty = _propertyName(js_ast.FixedNames.rtiName); body.add( js.statement('this.# = this.# || # || #', [ rtiProperty, rtiProperty, _rtiParam, _runtimeCall('getReifiedType(this)'), ]), ); } // Redirecting constructors are not allowed to have conventional // initializers but can have variable declarations in the form of // initializers to support named arguments appearing anywhere in the // arguments list. if (node.initializers.any((i) => i is RedirectingInitializer)) { body.add(_emitRedirectingConstructor(node.initializers, 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.whereType().firstOrNull; var jsSuper = _emitSuperConstructorCallIfNeeded(cls, className, superCall); if (jsSuper != null) { // TODO(50465) Fix incorrect assumption there should always be a super // initializer here. if (superCall != null) jsSuper.sourceInformation = _nodeStart(superCall); body.add(jsSuper); } body.add(_emitFunctionScopedBody(fn)); return body; } /// 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; } /// Returns `true` if [cls] requires/accepts an RTI during instantiation. /// /// We check [cls]'s transitive super classes for generic type parameters, /// but we do not consider anonymous mixins, implemented types or mixin on /// clauses - as their constructors are never invoked via super calls. /// Synthetic mixins are also skipped (despite sometimes having type /// parameters) since they can't be referenced during instantiation. /// /// Context: type arguments must be provided to a generic class during its /// instantiation. To avoid extraneous RTI evals, we pass the entire class's /// RTI instead of each type parameter's RTI individually. RTIs are attached /// to the instance on the hidden '$ti' field (see: FixedNames.rtiName). We /// attach RTIs eagerly (i.e., closer to the 'leaf' than the 'root') for /// simplicity. Setters on 'this' propagate up super calls since Dart super /// calls are synthetic. Ordinary JS super calls would require us to /// propagate the RTI all the way to the 'uppermost' generic class. bool _requiresRtiForInstantiation(Class? cls) { if (cls == null) return false; var cachedResult = _requiresRtiForInstantiationCache[cls]; if (cachedResult != null) return cachedResult; // Skip synthetic mixins since their RTIs are never needed during // instantiation. if (cls.isAnonymousMixin) { cls = _superClassAsWritten(cls); } var hasTypeParameters = cls.typeParameters.isNotEmpty || _requiresRtiForInstantiation(cls.superclass); _requiresRtiForInstantiationCache[cls] = hasTypeParameters; return hasTypeParameters; } js_ast.LiteralString _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( List initializers, js_ast.Expression className, ) { var jsInitializers = []; for (var init in initializers) { if (init is LocalInitializer) { // Temporary locals are created when named arguments don't appear at // the end of the arguments list. jsInitializers.add(visitVariableDeclaration(init.variable)); } else if (init is RedirectingInitializer) { var rtiParam = _requiresRtiForInstantiation(init.target.enclosingClass) ? _rtiParam : null; // We can't dispatch to the constructor with `this.new` as that might // hit a derived class constructor with the same name. var initializer = js.statement('#.#.call(this, #);', [ className, _constructorName(init.target.name.text), [?rtiParam, ..._emitArgumentList(init.arguments, types: false)], ]); jsInitializers.add(initializer); } } return js_ast.Block(jsInitializers); } js_ast.Statement? _emitSuperConstructorCallIfNeeded( Class c, js_ast.Expression className, SuperInitializer? superInit, ) { if (c == _coreTypes.objectClass) return null; Constructor ctor; List args; if (superInit == null) { ctor = unnamedConstructor(c.superclass!)!; args = []; } else { ctor = superInit.target; var savedTypeEnvironment = _currentTypeEnvironment; _currentTypeEnvironment = ClassTypeEnvironment(c.typeParameters); // An RTI will already have been set at the constructor call site, so // pass nothing if the superclass is expecting an RTI. var rti = _requiresRtiForInstantiation(ctor.enclosingClass) ? js_ast.LiteralNull() : null; args = [?rti, ..._emitArgumentList(superInit.arguments, types: true)]; _currentTypeEnvironment = savedTypeEnvironment; } // 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(ctor, className, ctor.name.text, args); } js_ast.Statement _emitSuperConstructorCall( Constructor constructor, js_ast.Expression className, String name, List args, ) { return js.statement('#.#.call(this, #);', [ _emitJSObjectGetPrototypeOf(className, fullyQualifiedName: true), _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); } js_ast.Expression _emitFieldValueAccessor(Field f) { 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); return access; } js_ast.Statement _emitFieldInit( Field f, Expression? initializer, TreeNode hoverInfo, ) { var access = _emitFieldValueAccessor(f); var jsInit = _visitInitializer(initializer, f.annotations); return jsInit.toAssignExpression(js.call('this.#', [access])).toStatement() ..sourceInformation = _nodeStart(hoverInfo); } /// 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 fields, [Constructor? ctor]) { // Run field initializers if they can have side-effects. var ctorFields = ctor?.initializers .whereType() .map((c) => c.field) .toSet(); var body = []; 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); body.add(_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) { body.add(_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 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) { 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, js_ast.LiteralString name, js_ast.Expression jsCtor, ) { jsCtor = _defineValueOnClass(c, className, name, jsCtor); return js.statement('#.prototype = #.prototype;', [jsCtor, className]); } /// Whether any superclass of [c] defines a static [name]. 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 superclass = c.superclass; var name = Name(memberName, c.enclosingLibrary); while (true) { if (superclass == null) return false; for (var m in superclass.members) { if (m.name == name && (m is Procedure && m.isStatic || m is Field && m.isStatic)) { return true; } } superclass = superclass.superclass; } } /// Emits a value store and getter/setter pair for [member] that constitutes /// a static field. List _emitStaticFieldAndAccessor(Member member) { return _emitLazyMember( _emitTopLevelNameNoExternalInterop(member.enclosingClass!), member, (m) => _emitStaticMemberName(m.name.text), ); } /// Emits class methods and properties. List _emitClassProperties(Class c) { var virtualFields = _classProperties!.virtualFields; var jsProperties = []; 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. jsProperties.add( js_ast.Method( _propertyName('constructor'), js.fun( r'''function() { throw Error("use `new " + # + ".new(...)` to create a Dart object"); }''', [ _runtimeCall('typeName(#)', [ _runtimeCall('getReifiedType(this)'), ]), ], ), ), ); } 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. jsProperties.add( js_ast.Method( _propertyName('constructor'), js.fun(r'function() { return []; }'), ), ); } for (var m in c.fields) { if (m.isStatic) { jsProperties.addAll(_emitStaticFieldAndAccessor(m)); } else if (_extensionTypes.isNativeClass(c)) { jsProperties.addAll(_emitNativeFieldAccessors(m)); } else if (virtualFields.containsKey(m)) { jsProperties.addAll(_emitVirtualFieldAccessor(m)); } } var getters = {}; var setters = {}; 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; if (_isForwardingStub(m)) { // TODO(jmesserly): is there any other kind of forwarding stub? jsProperties.addAll(_emitCovarianceCheckStub(m)); } else if (m.isFactory) { if (m.isRedirectingFactory) { // Skip redirecting factories (they've already been resolved). } else { jsProperties.add(_emitFactoryConstructor(m)); } } else if (m.isAccessor) { jsProperties.add(_emitMethodDeclaration(m)); jsProperties.add(_emitSuperAccessorWrapper(m, getters, setters)); if (!hasJsPeer && m.isGetter && m.name.text == 'iterator') { hasIterator = true; jsProperties.add(_emitIterable(c)); } } else { jsProperties.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 and IterableBase). // // (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) { jsProperties.add(_emitIterable(c)); } // Add all of the super helper methods jsProperties.addAll(_superHelpers.values); return jsProperties.nonNulls.toList(); } bool _isForwardingStub(Procedure member) { if (member.isForwardingStub || member.isForwardingSemiStub) { if (!_currentLibrary!.importUri.isScheme('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 = _withMethodDeclarationContext( member, () => _emitFunction( member.function, member.name.text, functionBody: _toSourceLocation(member.fileOffset), functionEnd: _toSourceLocation(member.fileEndOffset), ), ); } var method = js_ast.Method( _declareMemberName(member), fn, isGetter: member.isGetter, isSetter: member.isSetter, isStatic: member.isStatic, ); if (isTearOffLowering(member)) { // Remove all source information from static methods introduced by the // constructor tearoff CFE lowering. method.accept(js_ast.SourceInformationClearer()); } else { method.sourceInformation = _nodeEnd(member.fileEndOffset); } return method; } 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 _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 superMemberFunction = superMember.function; var name = _declareMemberName(member); if (member.isSetter) { if (superMember is Field && isCovariantField(superMember) || superMember is Procedure && isCovariantParameter( superMemberFunction!.positionalParameters[0], )) { return const []; } var setterType = substituteType( superMember.superSetterType, ).extensionTypeErasure; 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( superMemberFunction!.computeThisFunctionType( Nullability.nonNullable, ), ) as FunctionType; var function = member.function; var body = []; var typeParameters = function.typeParameters; var superTypeParameters = superMethodType.typeParameters; var typeFormals = _emitTypeFormals(typeParameters); for (var i = 0; i < typeParameters.length; i++) { var typeParameter = typeParameters[i]; var superBound = superTypeParameters[i].bound; if (typeParameter.isCovariantByClass && !_types.isTop(superBound)) { body.add( js.statement('#.checkTypeBound(#, #, #, #)', [ _emitLibraryName(_rtiLibrary), typeFormals[i], _emitType(superBound), _propertyName(typeParameter.name!), js.string(member.name.text), ]), ); } } var jsParams = List.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(superMemberFunction.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( superMemberFunction.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 (#) #;', [ _namedArgumentProbe(name), _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; final savedTypeEnvironment = _currentTypeEnvironment; _currentTypeEnvironment = RtiTypeEnvironment([ ...function.typeParameters, ..._currentTypeEnvironment.classTypeParameters, ]); var jsBody = js_ast.Block( _withCurrentFunction(function, () { var block = _emitArgumentInitializers(function, name); block.add(_emitFunctionScopedBody(function)); return block; }), ); var jsName = _constructorName(name); memberNames[node] = jsName.valueWithoutQuotes; // Generic class constructors accept their RTI as their first argument. var method = js_ast.Method( jsName, js_ast.Fun([ if (_requiresRtiForInstantiation(node.enclosingClass)) _rtiParam, ..._emitParameters(function), ], jsBody), isStatic: true, )..sourceInformation = _nodeEnd(node.fileEndOffset); _currentTypeEnvironment = savedTypeEnvironment; return method; } /// Emits the expression necessary to access a constructor of [type]; js_ast.Expression _emitConstructorAccess(InterfaceType type) => _emitJSInterop(type.classNode) ?? _emitClassRef(type); /// 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 _emitVirtualFieldAccessor(Field field) { var virtualFieldSymbol = _emitFieldValueAccessor(field); var name = _declareMemberName(field); var initializer = _visitInitializer(field.initializer, field.annotations); var getter = _isNonHotReloadableResource(_currentLibrary!.importUri) ? js.fun('function() { return this[#]; }', [virtualFieldSymbol]) : _emitLazyInitializingFunction( js.call('this.#', virtualFieldSymbol), initializer, field, ); var jsGetter = js_ast.Method(name, getter, isGetter: true) ..sourceInformation = _nodeStart(field); var body = []; var value = _emitIdentifier('value'); var args = field.isFinal ? [js_ast.Super(), name, value] : [ js_ast.This(), virtualFieldSymbol, if (isCovariantField(field)) _emitCast(value, field.type) else value, ]; body.add(js.call('#[#] = #', args).toStatement()); var jsSetter = js_ast.Method( name, js_ast.Fun([value], js_ast.Block(body)), 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 _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 = []; 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 = _emitScopedId('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 getters, Map 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: /// /// /// 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; var superclass = c.superclass!; // 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(superclass, Name('iterator')); if (parent != null) return null; var parentIterable = _hierarchy.getClassAsInstanceOf( superclass, _coreTypes.iterableClass, ); if (parentIterable != null) return null; if (c.enclosingLibrary.importUri.isScheme('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'), // TODO(nshahan) Don't access values in `runtimeModule` outside of // `runtimeCall`. js.call('function() { return new #.JsIterator(this.#); }', [ _emitLibraryName(_runtimeLibrary), _emitMemberName('iterator', memberClass: _coreTypes.iterableClass), ]) as js_ast.Fun, ); } void _registerExtensionType( Class c, String jsPeerName, List body, ) { var className = _emitTopLevelName(c); // TODO(55547): Move these operations to the library link method. if (_typeRep.isPrimitive(_coreTypes.nonNullableRawType(c))) { body.add( _runtimeStatement('definePrimitiveHashCode(#.prototype)', [className]), ); } _nativeExtensionLinks.add( _runtimeStatement('registerExtension(#, #)', [ js.string(jsPeerName), className, ]), ); } /// Generates an entrypoint function for [field] that returns the value in /// [valueCache] or initializes it to [initializer] on first access. /// /// [valueCache] is 'undefined' when uninitialized and holds a special /// sentinel value if [field] is final to detect multiple initializations. js_ast.Fun _emitLazyInitializingFunction( js_ast.Expression valueCache, js_ast.Expression initializer, Field field, ) { var initialFieldValueExpression = !_compileForHotReload ? valueCache : _emitCast(valueCache, field.type); // Lazy static fields require an additional type check around their value // cache if their type is updated after hot reload. To avoid a type check // on every access, the generated getter overrides itself with a direct // access on its underlying value cache on first access. // TODO(markzipan): The performance ramifications of a lookup vs // self-rewriting "smart" getter are unknown. We should revisit this if // property accesses become a bottleneck. if (field.isStatic) { var getterName = memberNames[field]!; // Final fields are generated with additional logic to detect // initialization cycles via a special sentinel. if (field.isFinal) { var finalLateInitDetectorSentinel = _getSymbol( _emitPrivateNameSymbol(field.enclosingLibrary, '_#initializing'), ); // Emits code like: // // if ([valueCache] === _#initializing) // dart.throwLateInitializationError(field); // if ([valueCache] === void 0) { // [valueCache] = _#initializing; // try { // [valueCache] = initializer; // } catch (e) { // // Reset the sentinel on error so it can be reinitialized. // if ([valueCache] === _#initializing) { // [valueCache] = void 0; // } // throw e; // } // } // _typeCheck([valueCache]); // Object.defineProperty(this, field, { // get() { // return [valueCache]; // } // }); // return this.field; return js.fun( r''' function() { if (# === #) #; if (# === void 0) { # = #; try { # = #; } catch (e) { if (# === #) { # = void 0; } throw e; } } #; Object.defineProperty(this, #, { get() { return #; } }); return this.#; } ''', [ valueCache, finalLateInitDetectorSentinel, _runtimeCall('throwLateInitializationError(#)', [ js.string(field.name.text), ]), valueCache, valueCache, finalLateInitDetectorSentinel, valueCache, initializer, valueCache, finalLateInitDetectorSentinel, valueCache, initialFieldValueExpression, js.string(getterName), valueCache, getterName, ], ); } else { // Emits code like: // // if ([valueCache] === void 0) { // [valueCache] = initializer; // } // _typeCheck([valueCache]); // Object.defineProperty(this, field, { // get() { // return [valueCache]; // } // }); // return this.field; return js.fun( r''' function() { if (# === void 0) { # = #; } #; Object.defineProperty(this, #, { get() { return #; } }); return this.#; } ''', [ valueCache, valueCache, initializer, initialFieldValueExpression, js.string(getterName), valueCache, getterName, ], ); } } // Final fields are generated with additional logic to detect // initialization cycles via a special sentinel. if (field.isFinal) { var finalLateInitDetectorSentinel = _getSymbol( _emitPrivateNameSymbol(field.enclosingLibrary, '_#initializing'), ); // Emits code like: // // if ([valueCache] === _#initializing) // dart.throwLateInitializationError(field); // if ([valueCache] === void 0) { // [valueCache] = _#initializing; // try { // [valueCache] = initializer; // } catch (e) { // // Reset the sentinel on error so it can be reinitialized. // if ([valueCache] === _#initializing) { // [valueCache] = void 0; // } // throw e; // } // } // return [valueCache]; return js.fun( r''' function() { if (# === #) #; if (# === void 0) { # = #; try { # = #; } catch (e) { if (# === #) { # = void 0; } throw e; } } return #; } ''', [ valueCache, finalLateInitDetectorSentinel, _runtimeCall('throwLateInitializationError(#)', [ js.string(field.name.text), ]), valueCache, valueCache, finalLateInitDetectorSentinel, valueCache, initializer, valueCache, finalLateInitDetectorSentinel, valueCache, initialFieldValueExpression, ], ); } else { return js.fun( r''' function() { if (# === void 0) { # = #; } return #; } ''', [valueCache, valueCache, initializer, initialFieldValueExpression], ); } } /// Emit a lazy field (i.e., late or static). /// /// Lazy fields are represented as an inlined initializer and a value store. /// Value stores are JS symbols prefixed by [_fieldValueStorePrefix], are /// initialized on first access, and are not replaced after a hot reload. List _emitLazyMember( js_ast.Expression objExpr, Member member, js_ast.LiteralString Function(Member) emitMemberName, ) { _currentUri = member.fileUri; _staticTypeContext.enterMember(member); var access = emitMemberName(member); memberNames[member] = access.valueWithoutQuotes; var properties = []; if (member is Field) { // Add this field's value store. Lazy members must be prefixed by // [_fieldValueStorePrefix] to allow correct hot reload semantics. // TODO(markzipan): Const values are emitted along the lazy pathway, but // their hot reload semantics seem to permit their values to change after // initialization. Revisit this later as we work on consts. var fieldValueStoreName = member.isConst ? memberNames[member]! : '$_fieldValueStorePrefix${memberNames[member]!}'; var memberValueStore = _getSymbol( _emitPrivateNameSymbol(_currentLibrary!, fieldValueStoreName), ); properties.add( js_ast.Property( memberValueStore, js.call('void 0'), isStatic: member.isStatic && member.enclosingClass != null, isClassProperty: member.enclosingClass != null, ), ); var initializer = _visitInitializer( member.initializer, member.annotations, ); js_ast.Fun getter; if (_isNonHotReloadableResource(_currentLibrary!.importUri)) { getter = js.fun( 'function() { return this[#] === void 0 ? this[#] = # : this[#]; }', [memberValueStore, memberValueStore, initializer, memberValueStore], ); } else { getter = _emitLazyInitializingFunction( js.call('this.#', memberValueStore), initializer, member, ); } properties.add( js_ast.Method( access, getter, isGetter: true, isStatic: member.isStatic && member.enclosingClass != null, ) ..sourceInformation = _hoverComment( js_ast.PropertyAccess(objExpr, access), member.fileOffset, member.name.text.length, ), ); if (!member.isFinal && !member.isConst) { var body = []; var param = _emitIdentifier('v'); body.add(js.statement('this.# = #;', [memberValueStore, param])); // Even when no null check is present a dummy setter is still required // to indicate writeable. properties.add( js_ast.Method( access, js_ast.Fun([param], js_ast.Block(body)), isSetter: true, isStatic: member.isStatic && member.enclosingClass != null, ), ); } } else if (member is Procedure) { properties.add( js_ast.Method( access, _emitFunction(member.function, member.name.text), isGetter: member.isGetter, isSetter: member.isSetter, isStatic: member.isStatic && member.enclosingClass != null, ) ..sourceInformation = _hoverComment( js_ast.PropertyAccess(objExpr, access), member.fileOffset, member.name.text.length, ), ); } else { throw UnsupportedError( 'Unsupported lazy member type ${member.runtimeType}: $member', ); } _staticTypeContext.leaveMember(member); return properties; } /// Emits [members] as lazy fields. List _emitLazyMembers( js_ast.Expression objExpr, Iterable members, js_ast.LiteralString Function(Member) emitMemberName, ) { var properties = []; var savedUri = _currentUri; for (var member in members) { properties.addAll(_emitLazyMember(objExpr, member, emitMemberName)); } _currentUri = savedUri; return properties; } List _withLetScope( List 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) { return _emitJSInterop(n) ?? _emitTopLevelNameNoExternalInterop(n, 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 = false}) { var c = m.enclosingClass; var name = m.name.text; var actualUseExtension = useExtension || (c != null && _extensionTypes.isNativeClass(c)); return _emitMemberName( name, isStatic: m is Field ? m.isStatic : (m as Procedure).isStatic, useExtension: actualUseExtension, 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: /// /// /// 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) { var memberName = _emitStaticMemberName(name, member); if (member != null && !isTearOffLowering(member)) { // No need to track the names of methods that were created by the CFE // lowering and don't exist in the original source code. memberNames[member] = memberName.valueWithoutQuotes; } return memberName; } // 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('.'); // TODO(nshahan) Record the name for this member in memberNames. 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!; if (member != null) { // TODO(40273) Move this name collection to another location. // We really only want to collect member names when the member is // created, not called. // 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); if (member != null) { // TODO(40273) Move this name collection to another location. // We really only want to collect member names when the member is created, // not called. 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.nonNullableRawType(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>(); 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 = _emitJSInteropExternalStaticMemberName(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); } if (member is Procedure && member.isFactory) { return _constructorName(member.name.text); } 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 += '_'; 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) { // Erasing any extension types here for legacy JS interop support but if // using the new extension type interop the type system requires that // `.toJS` was called. var type = f.getStaticType(_staticTypeContext).extensionTypeErasure; if (type is FunctionType || (type is InterfaceType && type.classNode == _coreTypes.functionClass)) { if (!isAllowInterop(f)) { return StaticInvocation( _assertInteropMethod, Arguments([f], types: [type]), ); } } return f; } /// Emit the name associated with external static members of interop classes. js_ast.LiteralString? _emitJSInteropExternalStaticMemberName(NamedNode n) { if (!usesJSInterop(n)) return null; if (n is Member && !n.isExternal) return null; var name = _annotationName(n, isJSInteropAnnotation) ?? getTopLevelName(n); assert( !name.contains('.'), 'JS interop checker rejects dotted names on static class members', ); return js.escapedString(name, "'"); } /// Emit the top-level name associated with [n], which should not be an /// external interop member. js_ast.PropertyAccess _emitTopLevelNameNoExternalInterop( NamedNode n, { String suffix = '', bool resolvedFromEmbedder = false, }) { // Some native tests use top-level native methods. var isTopLevelNative = n is Member && isNative(n); return js_ast.PropertyAccess( isTopLevelNative ? _runtimeCall('global.self') : (resolvedFromEmbedder ? _emitEmbedderResolvedLibrary(getLibrary(n)) : _emitLibraryName(getLibrary(n))), _emitTopLevelMemberName(n, suffix: suffix), ); } /// Emits [library] fully resolved via the Dart Dev Embedder. /// /// Used when the 'current' hot reload's generation of a library needs to be /// resolved. js_ast.Expression _emitEmbedderResolvedLibrary(Library library) { var libraryName = js.string('${library.importUri}'); return js.call('dartDevEmbedder.importLibrary(#)', [libraryName]); } /// 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()..add(c); while (classes.isNotEmpty) { var c = classes.removeFirst(); var classesToCheck = [ if (c.supertype != null) c.supertype!.classNode, for (var t in c.implementedTypes) 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), isJSInteropAnnotation); var jsName = _annotationName(n, isJSInteropAnnotation) ?? 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!; } /// Emits top level library procedures and fields. /// /// Top level fields are represented as an initializer and a value store. /// The getter initializes the value on first access, and an accompanying /// setter (if not final) sets the underlying value store. Value stores /// prefixed by [_fieldValueStorePrefix] are not replaced after a hot reload. void _emitLibraryMembers(Library library) { var libraryProperties = []; // Emit procedures var procedures = library.procedures .where( (p) => !p.isExternal && !p.isAbstract && !_isStaticInteropTearOff(p), ) .toList(); for (var p in procedures) { if (!p.isAccessor) { _moduleItems.add(_emitLibraryFunction(p)); } // TODO(#57049): We tag all static members because we don't know if // they've been changed after a hot reload. This won't be necessary if we // can tag them during the delta diff phase. if (p.isStatic && _reifyTearoff(p) && !p.isExternal) { var nameExpr = _emitTopLevelName(p); _moduleItems.add( _emitFunctionTagged( nameExpr, p.function.computeThisFunctionType(Nullability.nonNullable), asLazy: true, ).toStatement(), ); } } if (_hotReloadLibraryMetadata != null) { // Remove any top level procedures that were deleted in this hot reload // generation. for (var deletedProcedureName in _hotReloadLibraryMetadata.deletedStaticProcedureNames) { // The name used here must match the name used when creating top level // members. See [getTopLevelName]. var methodName = _propertyName(deletedProcedureName); _moduleItems.add( js.statement('delete #', [ js_ast.PropertyAccess(_emitLibraryName(library), methodName), ]), ); } } var accessors = procedures .where((p) => p.isAccessor) .map(_emitLibraryAccessor); libraryProperties.addAll(accessors); // Emit fields var fields = library.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 = []; var savedUri = _currentUri; // Helper functions to test if a constructor invocation is internal and // should be eagerly evaluated. bool 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)) { _currentUri = field.fileUri; _moduleItems.add( js.statement('# = #;', [ _emitTopLevelName(field), _visitInitializer(init, field.annotations), ]), ); } else { lazyFields.add(field); } _staticTypeContext.leaveMember(field); } _currentUri = savedUri; fields = lazyFields; } var libraryExpr = _emitLibraryName(_currentLibrary!); if (fields.isNotEmpty) { libraryProperties.addAll( _emitLazyMembers(libraryExpr, fields, _emitTopLevelMemberName), ); } if (libraryProperties.isNotEmpty) { var propertiesObject = js_ast.ObjectInitializer(libraryProperties); _moduleItems.add( _runtimeStatement('declareTopLevelProperties(#, #)', [ libraryExpr, propertiesObject, ]), ); } } /// Check whether [p] is a tear-off for an external or synthetic static /// interop member. /// /// Users are disallowed from using these tear-offs, so we should avoid /// emitting them. bool _isStaticInteropTearOff(Procedure p) { final extensionMember = _extensionIndex.getExtensionMemberForTearOff(p); if (extensionMember != null && extensionMember.asProcedure.isExternal) { return true; } final extensionTypeMember = _extensionIndex .getExtensionTypeMemberForTearOff(p); if (extensionTypeMember != null && extensionTypeMember.asProcedure.isExternal) { return true; } final enclosingClass = p.enclosingClass; if (enclosingClass != null && isStaticInteropType(enclosingClass)) { // @staticInterop types can't use generative constructors, so we only // check for tear-offs of factories. The one exception is a tear-off of a // default constructor, which is disallowed on @staticInterop classes. final factoryName = extractConstructorNameFromTearOff(p.name); if (factoryName != null) { if (factoryName.isEmpty && enclosingClass.constructors.any( (constructor) => constructor.isSynthetic && constructor.name.text.isEmpty, )) { return true; } if (enclosingClass.procedures.any( (procedure) => procedure.isFactory && procedure.isExternal && procedure.name.text == factoryName, )) { return true; } } } return false; } js_ast.Method _emitLibraryAccessor(Procedure node) { var savedUri = _currentUri; _staticTypeContext.enterMember(node); _currentUri = node.fileUri; var name = node.name.text; memberNames[node] = name; var result = js_ast.Method( _propertyName(name), _emitFunction(node.function, name), 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 = []; var fn = _emitFunction( p.function, p.name.text, functionBody: _toSourceLocation(p.fileOffset), functionEnd: _toSourceLocation(p.fileEndOffset), )..sourceInformation = _nodeEnd(p.fileEndOffset); if (_currentLibrary!.importUri.isScheme('dart') && _isInlineJSFunction(p.function.body)) { fn = js_ast.simplifyPassThroughArrowFunCallBody(fn); } var nameExpr = _emitTopLevelName(p); var jsName = _safeFunctionNameForSafari(p.name.text, fn); var functionName = _emitScopedId(jsName); procedureIdentifiers[p] = functionName; body.add( js.statement('# = #', [nameExpr, js_ast.NamedFunction(functionName, fn)]), ); _currentUri = savedUri; _staticTypeContext.leaveMember(p); if (_options.dynamicModule && _isDynamicModuleEntryPoint(p, _coreTypes)) { if (_dynamicEntrypoint == null) { if (p.function.requiredParameterCount > 0) { // TODO(sigmund): this error should be caught by a kernel checker that // runs prior to DDC. throw StateError( 'Entrypoint ${p.name.text} must accept being called ' 'with 0 arguments.', ); } else { _dynamicEntrypoint = p; } } else { // TODO(sigmund): this error should be caught by a kernel checker that // runs prior to DDC. throw StateError('A module should define a single entrypoint.'); } } 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 asLazy = false, }) { assert(type.nullability == Nullability.nonNullable); var typeRep = _emitType(type); if (type.typeParameters.isEmpty) { return asLazy ? _runtimeCall('lazyFn(#, () => #)', [fn, typeRep]) : _runtimeCall('fn(#, #)', [fn, typeRep]); } else { var typeParameterDefaults = [ for (var parameter in type.typeParameters) _emitType(parameter.defaultType), ]; var defaultInstantiatedBounds = _emitConstList( const DynamicType(), typeParameterDefaults, ); return asLazy ? _runtimeCall('lazyGFn(#, () => #, () => #)', [ fn, typeRep, defaultInstantiatedBounds, ]) : _runtimeCall('gFn(#, #, #)', [ fn, typeRep, defaultInstantiatedBounds, ]); } } /// Returns an expression that evaluates to the rti object from the dart:_rti /// library that represents [type]. /// /// [emitJSInteropGenericClassTypeParametersAsAny] indicates that we should /// emit the statically declared type as a JS interop generic class's type /// argument (rather than substituting Any). Any is required for correctness /// in most cases except for uses in non-external JS interop factories. /// Note: This only applies to the old style package:js interop and isn't /// necessary for any forms of static JS interop. js_ast.Expression _emitType( DartType type, { bool emitJSInteropGenericClassTypeParametersAsAny = true, }) { /// Returns an expression that evaluates a type [recipe] within the type /// [environment]. /// /// At runtime the expression will evaluate to an rti object. js_ast.Expression emitRtiEval( js_ast.Expression environment, String recipe, ) => js.call('#.#("$recipe")', [ environment, _emitMemberName('_eval', memberClass: _rtiClass), ]); /// Returns an expression that binds a type [parameter] within the type /// [environment]. /// /// At runtime the expression will evaluate to an rti object that has been /// extended to include the provided [parameter]. js_ast.Expression emitRtiBind( js_ast.Expression environment, TypeParameter parameter, ) { return js.call('#.#(#)', [ environment, _emitMemberName('_bind', memberClass: _rtiClass), _emitTypeParameter(parameter), ]); } /// Returns an expression that evaluates a type [recipe] in a type /// [environment] resulting in an rti object. js_ast.Expression evalInEnvironment( DDCTypeEnvironment environment, String recipe, ) { switch (environment) { case EmptyTypeEnvironment(): // Cache ground types in the type table for fast lookup. The table // will lazily lookup the RTI object on first access and then replace // the lazy getter with the initialized RTI object. return _typeTable.nameType( type, js.call('#._Universe.eval(#, "$recipe", true)', [ _emitLibraryName(_rtiLibrary), _runtimeCall('typeUniverse'), ]), ); case BindingTypeEnvironment(): js_ast.Expression env; if (environment.isSingleTypeParameter) { // An environment with a single type parameter can be simplified to // just that parameter. env = _emitTypeParameter(environment.functionTypeParameters.single); // Skip a no-op evaluation and just return the parameter. if (recipe == '0') return env; } else { var environmentTypes = environment.functionTypeParameters; // By convention we create a binding environment with "dynamic" as // the base. env = _emitType(const DynamicType()); // Bind all type arguments to it. for (var typeParameter in environmentTypes) { env = emitRtiBind(env, typeParameter); } } return emitRtiEval(env, recipe); case RtiTypeEnvironment(): // RTI type environments take the form of a preconstructed RTI that // is accessible via a known parameter name. var env = _rtiParam; return emitRtiEval(env, recipe); case ClassTypeEnvironment(): // Class type environments are already constructed and attached to the // instance of a generic class. var env = js.call('#.instanceType(this)', [ _emitLibraryName(_rtiLibrary), ]); return emitRtiEval(env, recipe); case ExtendedTypeEnvironment(): // Class type environments are already constructed and attached to the // instance of a generic class, but function type parameters need to // be bound. var env = js.call('#.instanceType(this)', [ _emitLibraryName(_rtiLibrary), ]); // Bind extra type parameters. for (var parameter in environment.functionTypeParameters) { env = emitRtiBind(env, parameter); } return emitRtiEval(env, recipe); } _typeCompilationError( type, 'Unexpected DDCTypeEnvironment type (${environment.runtimeType}).', ); } var normalizedType = _futureOrNormalizer.normalize( type.extensionTypeErasure, ); try { var result = _typeRecipeGenerator.recipeInEnvironment( normalizedType, _currentTypeEnvironment, emitJSInteropGenericClassTypeParametersAsAny: emitJSInteropGenericClassTypeParametersAsAny, ); var typeRep = evalInEnvironment( result.requiredEnvironment, result.recipe, ); return typeRep; } on UnsupportedError catch (e) { _typeCompilationError(normalizedType, e.message ?? 'Unknown Error'); } } js_ast.Expression _emitInvalidNode(Node node, [String message = '']) { if (message.isNotEmpty) message += ' '; return _runtimeCall('throwUnimplementedError(#)', [ js.escapedString('node <${node.runtimeType}> $message`$node`'), ]); } /// Emits a reference to the class described by [type]. /// /// The nullability of [type] is not considered because it is meaningless when /// describing a reference to the class itself. /// /// For generic classes, type arguments are not needed since they are /// resolved late via an RTI lookup. /// /// Note that for `package:js` types, this will emit the class we emitted /// using `_emitJSInteropClassNonExternalMembers`, and not the runtime type /// that we synthesize for `package:js` types. /// /// [resolvedFromEmbedder] looks up [type] via the embedder, which retrieves /// the correct library in the context of hot reload. This should not be set /// for external classes. js_ast.Expression _emitClassRef( InterfaceType type, { bool resolvedFromEmbedder = false, }) => _emitTopLevelNameNoExternalInterop( type.classNode, resolvedFromEmbedder: resolvedFromEmbedder, ); Never _typeCompilationError(DartType type, String description) => throw UnsupportedError( '$description Encountered while compiling ' '${_currentLibrary!.fileUri}, which contains the type: $type.', ); /// Emits an expression that lets you access statics on a [type] from code. js_ast.Expression _emitConstructorName(InterfaceType type, Member c) { var isSyntheticDefault = c is Constructor && c.isSynthetic && c.name.text.isEmpty; // If it's an external constructor or synthetic default, use the JS // constructor. var jsConstructor = _emitJSInterop(type.classNode); if (jsConstructor != null && (c.isExternal || isSyntheticDefault)) { return jsConstructor; } // If it's non-external but belongs to an interop class, we want the class // reference we defined in `_emitJSInteropClassNonExternalMembers`. return js_ast.PropertyAccess( _emitClassRef(type), _constructorName(c.name.text), ); } /// Emits an expression that lets you access statics on [c] from code. /// /// If [isExternal] is false, emits the non-external name. js_ast.Expression _emitStaticClassName(Class c, bool isExternal) { return isExternal ? _emitTopLevelName(c) : _emitTopLevelNameNoExternalInterop(c); } js_ast.Identifier _emitTypeParameter( /* TypeParameter | StructuralParameter */ Object t, ) { assert(t is TypeParameter || t is StructuralParameter); return _emitIdentifier(getTypeParameterName(t)); } /// Set incremental mode for expression compilation. /// /// Called for each expression compilation to set the incremental mode /// and clear referenced items. /// /// Sets all tables and internal structures to incremental mode so /// only referenced items will be emitted in a generated function. /// /// The compiler cannot revert to non-incremental mode, and requires the /// original module to be already emitted by the same compiler instance. void _setIncrementalMode() { if (!_moduleEmitted) { throw StateError( 'Cannot run in incremental mode before module completion', ); } _incrementalModules.clear(); _privateNames.clear(); _symbolContainer.setIncrementalMode(); _incrementalMode = true; _constTableCache = ModuleItemContainer.asArray('C'); _constLazyAccessors.clear(); _constAliasCache.clear(); _uriContainer = ModuleItemContainer.asArray('I'); _typeTable.typeContainer.setIncrementalMode(); } /// Emits function after initial compilation. /// /// Emits function from kernel [functionNode] with name [name] in the context /// of [library] and [cls], after the initial compilation of the module is /// finished. For example, this happens in expression compilation during /// expression evaluation initiated by the user from the IDE and coordinated /// by the debugger. /// Triggers incremental mode, which only emits symbols, types, constants, /// libraries, and uris referenced in the expression compilation result. js_ast.Fun _emitFunctionIncremental( List items, Library library, Class? cls, FunctionNode functionNode, String name, ) { // Setup context. _currentLibrary = library; _staticTypeContext.enterLibrary(_currentLibrary!); _currentClass = cls; // Generic parameters should be evaluated in a class environment if // provided. Otherwise we default to an empty type environment. if (cls != null) { _currentTypeEnvironment = ClassTypeEnvironment(cls.typeParameters); } // Keep all symbols in containers. _containerizeSymbols = true; // Set all tables to incremental mode, so we can only emit elements that // were referenced the compiled code for the expression. _setIncrementalMode(); // Do not add formal parameter checks for the top-level synthetic function // generated for expression evaluation, as those parameters are a set of // variables from the current scope, and should already be checked in the // original code. _checkParameters = false; // Emit function while recoding elements accessed from tables. var fun = _emitFunction(functionNode, name); var extensionSymbols = []; _emitExtensionSymbols(extensionSymbols); // Add all elements from tables accessed in the function var body = js_ast.Block([ ...extensionSymbols, ..._typeTable.dischargeBoundTypes(), ..._symbolContainer.emit(), ..._emitConstTable(), ..._uriContainer.emit(), ...fun.body.statements, ]); // Import all necessary libraries, including libraries accessed from the // current module and libraries accessed from the type table. for (var library in _typeTable.incrementalLibraries()) { _setEmitIfIncrementalLibrary(library); } _emitImports(items); _emitExportsAsImports(items, _currentLibrary!); return js_ast.Fun(fun.params, body); } List _emitConstTable() { var constTable = []; if (_constLazyAccessors.isNotEmpty) { constTable.add( js.statement('const # = Object.create(null);', [_constTable]), ); constTable.add( _runtimeStatement('defineLazy(#, { # })', [ _constTable, _constLazyAccessors, ]), ); constTable.addAll(_constTableCache.emit()); } return constTable; } js_ast.Fun _emitFunction( FunctionNode f, String? name, { SourceLocation? functionEnd, SourceLocation? functionBody, }) { var savedTypeEnvironment = _currentTypeEnvironment; if (f.typeParameters.isNotEmpty) { _currentTypeEnvironment = _currentTypeEnvironment.extend( f.typeParameters, ); } var formals = _emitParameters(f); var typeFormals = _emitTypeFormals(f.typeParameters); var parent = f.parent; if (_reifyGenericFunction(parent is Member ? parent : null)) { formals.insertAll(0, typeFormals); } // TODO(jmesserly): need a way of determining if parameters are // potentially mutated in Kernel. For now we assume all parameters are. _enterFunction(name, formals, () => true); var block = js_ast.Block( _withCurrentFunction(f, () { final bodyPrefix = _emitArgumentInitializers(f, name); // Do the async transformation before adding parameter initialization // logic. Any parameter initialization should be performed synchronously // before the async body is evaluated. final bodyFn = js_ast.Fun( formals, js_ast.Block([_emitFunctionScopedBody(f)]), ); final rewrittenFunction = _rewriteAsyncFunction( bodyFn, f.asyncMarker, name, f.emittedValueType, functionEnd: functionEnd, functionBody: functionBody, bodyPrefix: bodyPrefix, ); formals = rewrittenFunction.params; return rewrittenFunction.body.statements; }), ); block = _exitFunction(formals, block); var fn = js_ast.Fun(formals, block); _currentTypeEnvironment = savedTypeEnvironment; return fn; } /// Transforms [fun]'s body to support async execution if the function is /// async, sync*, or async*. /// /// [bodyPrefix] will get prepended to the body of the rewritten function and /// any references to parameters within it will be replaced with the correct /// temporary ID for that parameter. js_ast.Fun _rewriteAsyncFunction( js_ast.Fun fun, AsyncMarker asyncMarker, String? name, DartType? asyncType, { SourceLocation? functionEnd, SourceLocation? functionBody, List? bodyPrefix, }) { AsyncRewriterBase? asyncRewriter; final bodyName = _emitScopedId('t\$async${name ?? 'Body'}'); switch (asyncMarker) { case AsyncMarker.Sync: break; case AsyncMarker.Async: asyncRewriter = AsyncRewriter( asyncStart: _emitTopLevelNameNoExternalInterop(_asyncStartMember), asyncAwait: _emitTopLevelNameNoExternalInterop(_asyncAwaitMember), asyncReturn: _emitTopLevelNameNoExternalInterop(_asyncReturnMember), asyncRethrow: _emitTopLevelNameNoExternalInterop(_asyncRethrowMember), completerFactory: _emitTopLevelNameNoExternalInterop( _asyncMakeCompleterMember, ), completerFactoryTypeArguments: [_emitType(asyncType!)], wrapBody: _emitTopLevelNameNoExternalInterop( _asyncWrapJsFunctionMember, ), bodyName: bodyName, ); case AsyncMarker.SyncStar: asyncRewriter = SyncStarRewriter( makeSyncStarIterable: _emitTopLevelNameNoExternalInterop( _syncStarMakeIterableMember, ), syncStarIterableTypeArgument: _emitType(asyncType!), iteratorCurrentValueProperty: _emitMemberName( '_current', member: _syncStarIteratorCurrentMember, ), iteratorDatumProperty: _emitMemberName( '_datum', member: _syncStarIteratorDatumMember, ), yieldStarSelector: _emitMemberName( '_yieldStar', member: _syncStarIteratorYieldStarMember, ), bodyName: bodyName, ); case AsyncMarker.AsyncStar: asyncRewriter = AsyncStarRewriter( asyncStarHelper: _emitTopLevelNameNoExternalInterop( _asyncStarHelperMember, ), streamOfController: _emitTopLevelNameNoExternalInterop( _asyncStreamOfControllerMember, ), newController: _emitTopLevelNameNoExternalInterop( _asyncMakeAsyncStarStreamControllerMember, ), newControllerTypeArguments: [_emitType(asyncType!)], yieldExpression: _emitStaticGet( _asyncIterationMarkerYieldSingleMember, ), yieldStarExpression: _emitStaticGet( _asyncIterationMarkerYieldStarMember, ), wrapBody: _emitTopLevelNameNoExternalInterop( _asyncWrapJsFunctionMember, ), bodyName: bodyName, ); } if (asyncRewriter != null) { return asyncRewriter.rewrite( fun, functionBody, functionEnd, bodyPrefix: bodyPrefix, ); } else if (bodyPrefix != null) { fun.body.statements.insertAll(0, bodyPrefix); } return fun; } js_ast.Parameter _emitParameter( VariableDeclaration node, { bool withoutInitializer = false, }) { var initializer = node.initializer; var id = _emitVariableDef(node); if (initializer == null || withoutInitializer) return id; return js_ast.DestructuredVariable( name: id, defaultValue: _visitExpression(initializer), ); } List _emitParameters( FunctionNode f, { bool isForwarding = false, }) { // Destructure optional positional parameters in place. // Given: // - (arg1, arg2, [opt1, opt2 = def2]) // Emit: // - (arg1, arg2, opt1 = null, opt2 = def2) // Note, if [isForwarding] is set, omit initializers as this actually a // forwarded call not a parameter list. E.g., the second in: // - foo(arg1, opt1 = def1) => super(arg1, opt1). var positional = f.positionalParameters; var result = List.of( positional.map( (p) => _emitParameter(p, withoutInitializer: isForwarding), ), ); if (positional.isNotEmpty && f.requiredParameterCount == positional.length && positional.last.annotations.any(isJsRestAnnotation)) { result.last = js_ast.RestParameter(result.last as js_ast.Identifier); } if (f.namedParameters.isNotEmpty) result.add(_namedArgumentTemp); return result; } List _emitTypeFormals( List typeFormals, ) { assert( typeFormals is List || typeFormals is List, ); return typeFormals .map((t) => _emitIdentifier(getTypeParameterName(t))) .toList(); } List _withCurrentFunction( FunctionNode fn, List Function() action, ) { var savedFunction = _currentFunction; _currentFunction = fn; _nullableInference.enterFunction(fn); var result = _withLetScope(action); _nullableInference.exitFunction(fn); _currentFunction = savedFunction; return result; } T _superDisallowed(T Function() action) { var savedSuperAllowed = _superAllowed; _superAllowed = false; var result = action(); _superAllowed = savedSuperAllowed; return result; } /// Executes [action] in context of the current [member]. /// /// Saves and restores important context information about the member /// that can be used to generate code inside the body of the member. T _withMethodDeclarationContext(Procedure member, T Function() action) { // Mixin applications require using 'super' in calls to members of // the super class. Store this information to disable non-virtual // super field access optimization when compiling the member body. var savedOptimizeNonVirtualFieldAccess = _optimizeNonVirtualFieldAccess; _optimizeNonVirtualFieldAccess = member.stubKind != ProcedureStubKind.ConcreteMixinStub; var result = action(); _optimizeNonVirtualFieldAccess = savedOptimizeNonVirtualFieldAccess; return result; } /// Returns true if the underlying type does not accept a null value. bool _mustBeNonNullable(DartType type) => type.nullability == Nullability.nonNullable; /// Emits argument initializers, which handles optional/named args, as well /// as generic type checks needed due to our covariance. List _emitArgumentInitializers( FunctionNode f, String? name, ) { var body = []; _emitCovarianceBoundsCheck(f.typeParameters, body); void initParameter( VariableDeclaration p, js_ast.Identifier jsParam, bool isOptional, ) { // When the parameter is covariant, insert the null check before the // covariant cast to avoid a TypeError when testing equality with null. if (name == '==') { // In Dart `operator ==` methods are not called with a null argument. // This is handled before calling them. For performance reasons, we push // this check inside the method, to simplify our `equals` helper. // // TODO(jmesserly): in most cases this check is not necessary, because // the Dart code already handles it (typically by an `is` check). // Eliminate it when possible. body.add(js.statement('if (# == null) return false;', [jsParam])); } if (isCovariantParameter(p) || // TODO(52582): This should be unreachable once the CFE ensures that // redirecting factories parameter types match the target constructor. // Matches dart2js check semantics for redirecting factory tearoffs. // If a non-nullable optional argument with a null initializer is // detected, we add an additional covariant check at runtime. (f.parent is Procedure && isOptional && isConstructorTearOffLowering(f.parent as Procedure) && !p.type.isPotentiallyNullable && !p.initializer! .getStaticType(_staticTypeContext) .isPotentiallyNonNullable)) { var castExpr = _emitCast(jsParam, p.type); if (!identical(castExpr, jsParam)) body.add(castExpr.toStatement()); } if (name == '==') return; if (_annotatedNullCheck(p.annotations)) { body.add(_nullParameterCheck(jsParam)); } } var counter = 0; for (var p in f.positionalParameters) { var jsParam = _emitVariableRef(p); if (_checkParameters) { initParameter(p, jsParam, counter >= f.requiredParameterCount); } counter++; } for (var p in f.namedParameters) { // Parameters will be passed using their real names, not the (possibly // renamed) local variable. var jsParam = _emitVariableDef(p); var paramName = _propertyName(p.name!); var defaultValue = _defaultParamValue(p); body.add( js.statement('let # = # && # ? #.# : #;', [ jsParam, _namedArgumentTemp, _namedArgumentProbe(paramName), _namedArgumentTemp, paramName, defaultValue, ]), ); if (_checkParameters) { initParameter(p, jsParam, !p.isRequired); } } // '_checkParameters = false' is only needed once, while processing formal // parameters of the synthetic function from expression evaluation - it // will be called from emitFunctionIncremental, which is a top-level API // for expression compilation. // Here we either are done with processing those formals, or compiling // something else (in which case _checkParameters is already true). _checkParameters = true; return body; } bool _annotatedNullCheck(List annotations) => annotations.any(_nullableInference.isNullCheckAnnotation); bool _reifyGenericFunction(Member? m) => m == null || // JS interop members should not pass type arguments. !isJsMember(m) && !(m.enclosingLibrary.importUri.isScheme('dart') && m.annotations.any( (a) => isBuiltinAnnotation(a, '_js_helper', 'NoReifyGeneric'), )); js_ast.Statement _nullParameterCheck(js_ast.Expression param) { var call = _runtimeCall('argumentError((#))', [param]); return js.statement('if (# == null) #;', [param, call]); } js_ast.Expression _defaultParamValue(VariableDeclaration p) { if (p.initializer != null) { return _visitExpression(p.initializer!); } else { return js_ast.LiteralNull(); } } /// Returns a test for the existence of [propertyName] in the named argument /// package. js_ast.Expression _namedArgumentProbe(js_ast.LiteralString propertyName) => // If the name collides with the names in the native JavaScript object // prototype then use a slower but more direct test to avoid // accidentally finding a value up the prototype chain. js_ast.objectProperties.contains(propertyName.valueWithoutQuotes) ? _runtimeCall('hOP.call(#, #)', [_namedArgumentTemp, propertyName]) : js.call('# in #', [propertyName, _namedArgumentTemp]); void _emitCovarianceBoundsCheck( List typeFormals, List body, ) { for (var t in typeFormals) { var bound = t.bound.extensionTypeErasure; if (t.isCovariantByClass && !_types.isTop(bound)) { // TODO(nshahan): Ensure we have a name for the method and then // just call the `checkTypeBound()` in the rti library directly. body.add( _runtimeStatement('checkTypeBound(#, #, #)', [ _emitType(TypeParameterType(t, Nullability.undetermined)), _emitType(bound), _propertyName(t.name!), ]), ); } } } js_ast.Statement _visitStatement(Statement s) { var result = s.accept(this); // In most cases, a Dart expression statement with a child expression // compile to a JS expression statement with a child expression. // // ExpressionStatement js_ast.ExpressionStatement // | --> compiles to --> | // Expression js_ast.Expression // // Both the expression statement and child expression nodes contain their // own source location information. // // In the case of a debugger() call, the code compiles to a single node. // // ExpressionStatement js_ast.DebuggerStatement // | --> compiles to --> // Expression // // The js_ast.DebuggerStatement already has the correct source information // attached so we avoid overwriting with the incorrect source location from // [s]. // TODO(jmesserly): is the `is! Block` still necessary? if (!(s is Block || result is js_ast.DebuggerStatement)) { result.sourceInformation ??= _nodeStart(s); } // The statement might be the target of a break or continue with a label. var name = _labelNames[s]; if (name != null) result = js_ast.LabeledStatement(name, result); return result; } js_ast.Statement _emitFunctionScopedBody(FunctionNode f) { var savedInAsyncExpression = _inAsyncExpression; if (f.asyncMarker != AsyncMarker.Sync) { _inAsyncExpression = true; } var jsBody = _visitStatement(f.body!); var body = _emitScopedBody(f, jsBody); _inAsyncExpression = savedInAsyncExpression; return body; } js_ast.Statement _emitScopedBody(FunctionNode f, js_ast.Statement body) { if (f.positionalParameters.isNotEmpty || f.namedParameters.isNotEmpty) { // Handle shadowing of parameters by local variables, which is allowed in // Dart but not in JS. // // We need this for all function types, including generator-based ones // (sync*/async/async*). Our code generator assumes it can emit names for // named argument initialization, and sync* functions also emit locally // modified parameters into the function's scope. var parameterNames = { for (var p in f.positionalParameters) p.name!, for (var p in f.namedParameters) p.name!, }; return body.toScopedBlock(parameterNames); } return body; } /// Visits [nodes] with [_visitExpression]. List _visitExpressionList(Iterable nodes) { return nodes.map(_visitExpression).toList(); } /// Generates an expression for a boolean conversion context (if, while, &&, /// etc.), where conversions and null checks are implemented via `dart.test` /// to give a more helpful message. // TODO(sra): When nullablility is available earlier, it would be cleaner to // build an input AST where the boolean conversion is a single AST node. js_ast.Expression _visitTest(Expression node) { if (node is Not) { return visitNot(node); } if (node is LogicalExpression) { js_ast.Expression shortCircuit(String code) { return js.call(code, [_visitTest(node.left), _visitTest(node.right)]); } var op = node.operatorEnum; if (op == LogicalExpressionOperator.AND) return shortCircuit('# && #'); if (op == LogicalExpressionOperator.OR) return shortCircuit('# || #'); } if (node is AsExpression && node.isTypeError) { assert( node.getStaticType(_staticTypeContext) == _types.coreTypes.boolNonNullableRawType, ); return _runtimeCall('dtest(#)', [_visitExpression(node.operand)]); } var result = _visitExpression(node); if (_isNullable(node)) result = _runtimeCall('test(#)', [result]); return result; } js_ast.Expression _visitExpression(Expression e) { if (e is ConstantExpression) { return visitConstant(e.constant); } var result = e.accept(this); result.sourceInformation ??= _nodeStart(e); return result; } /// Gets the start position of [node] for use in source mapping. /// /// This is the most common kind of marking, and is used for most expressions /// and statements. SourceLocation? _nodeStart(TreeNode node) => node is StringConcatenation // Manually selecting the location of the first element to work around the // location on the StringConcatenation node that points to the end of // String. See https://github.com/dart-lang/sdk/issues/55690. ? _toSourceLocation(node.expressions.first.fileOffset) : _toSourceLocation(node.fileOffset); /// Gets the end position of [node] for use in source mapping. /// /// This is mainly used for things that compile to JS functions. JS wants a /// marking on the end of all functions for stepping purposes. /// /// This can be used to complete a hover span, when we know the start position /// has already been emitted. For example, `foo.bar` we only need to mark the /// end of `.bar` to ensure `foo.bar` has a hover tooltip. NodeEnd? _nodeEnd(int endOffset) { var loc = _toSourceLocation(endOffset); return loc != null ? NodeEnd(loc) : null; } /// Combines [_nodeStart] with the variable name length to produce a hoverable /// span for the variable. // // TODO(jmesserly): we need a lot more nodes to support hover. NodeSpan? _variableSpan(int offset, int nameLength) { var start = _toSourceLocation(offset); var end = _toSourceLocation(offset + nameLength); return start != null && end != null ? NodeSpan(start, end) : null; } SourceLocation? _toSourceLocation(int offset) { if (offset == -1) return null; var fileUri = _currentUri; if (fileUri == null) return null; try { var loc = _component.getLocation(fileUri, offset); if (loc == null || loc.line < 0) return null; return SourceLocation( offset, sourceUrl: fileUri, line: loc.line - 1, column: loc.column - 1, ); } on StateError catch (_) { // TODO(jmesserly): figure out why this is throwing. Perhaps the file URI // and offset are mismatched and don't correspond to the same source? return null; } on RangeError catch (_) { return null; } } /// Adds a hover comment for Dart node using JS expression [expr], where /// that expression would not otherwise not be generated into source code. /// /// For example, top-level and static fields are defined as lazy properties, /// on the library/class, so their access expressions do not appear in the /// source code. HoverComment? _hoverComment( js_ast.Expression expr, int offset, int nameLength, ) { var start = _toSourceLocation(offset); var end = _toSourceLocation(offset + nameLength); return start != null && end != null ? HoverComment(expr, start, end) : null; } @override js_ast.Statement visitExpressionStatement(ExpressionStatement node) { var expr = node.expression; if (expr is StaticInvocation) { if (isInlineJS(expr.target)) { return _emitInlineJSCode(expr).toStatement(); } if (_isDebuggerCall(expr.target)) { return _emitDebuggerCall(expr).toStatement(); } } var jsExpression = _visitExpression(expr); if (jsExpression is js_ast.InvocationWithHotReloadChecks) { // When the static invocation expression has been rewritten to include // hot reload correctness checks the entire resulting node shouldn't // get source-mapped to the original call. Instead the call sites in // the sub-expressions will have the correct mapping applied. return jsExpression.toStatement()..sourceInformation = continueSourceMap; } return jsExpression.toStatement(); } @override js_ast.Statement visitBlock(Block node) { // If this is the block body of a function, don't mark it as a separate // scope, because the function is the scope. This avoids generating an // unnecessary nested block. // // NOTE: we do sometimes need to handle this because Dart and JS rules are // slightly different (in Dart, there is a nested scope), but that's handled // by _emitSyncFunctionBody. var isScope = !identical(node.parent, _currentFunction); return js_ast.Block( node.statements.map(_visitStatement).toList(), isScope: isScope, ); } @override js_ast.Statement visitEmptyStatement(EmptyStatement node) => js_ast.EmptyStatement(); @override js_ast.Statement visitAssertBlock(AssertBlock node) { // AssertBlocks are introduced by the VM-specific async elimination // transformation. We do not expect them to arise here. throw UnsupportedError('compilation of an assert block'); } // Replace a string `uri` literal with a cached top-level variable containing // the value to reduce overall code size. js_ast.Expression _cacheUri(String uri) { if (!_uriContainer.contains(uri)) { _uriContainer[uri] = js_ast.LiteralString('"$uri"'); } _uriContainer.setEmitIfIncremental(uri); return _uriContainer.access(uri); } @override js_ast.Statement visitAssertStatement(AssertStatement node) { if (!_options.enableAsserts) return js_ast.EmptyStatement(); var condition = node.condition; var conditionType = condition .getStaticType(_staticTypeContext) .extensionTypeErasure; var jsCondition = _visitExpression(condition); if (conditionType != _coreTypes.boolNullableRawType && conditionType != _coreTypes.boolNonNullableRawType) { jsCondition = _runtimeCall('dtest(#)', [jsCondition]); } else if (_isNullable(condition)) { // TODO(nshahan): Is this branch even reachable in null safe code? jsCondition = _runtimeCall('test(#)', [jsCondition]); } SourceLocation? location; late String conditionSource; var assertLocation = node.location; if (assertLocation != null) { var fileUri = assertLocation.file; var source = node.enclosingComponent!.uriToSource[fileUri]!.text; conditionSource = source.substring( node.conditionStartOffset, node.conditionEndOffset, ); // Assertions that appear in debugger expressions have a synthetic Uri // that is different than the current library where the expression will // be evaluated. var savedUri = _currentUri; _currentUri = fileUri; location = _toSourceLocation(node.conditionStartOffset)!; _currentUri = savedUri; } else { // If the location is ever null, only show the error with the condition // AST instead of the source. conditionSource = node.condition.toString(); } return js.statement(' if (!#) #;', [ jsCondition, _runtimeCall('assertFailed(#, #, #, #, #)', [ if (node.message == null) js_ast.LiteralNull() else _visitExpression(node.message!), if (location == null) _cacheUri('') else _cacheUri(location.sourceUrl.toString()), // Lines and columns are typically printed with 1 based indexing. js.number(location == null ? -1 : location.line + 1), js.number(location == null ? -1 : location.column + 1), js.escapedString(conditionSource), ]), ]); } bool _isBreakable(Statement stmt) { // These are conservatively the things that compile to things that can be // the target of a break without a label. return stmt is ForStatement || stmt is WhileStatement || stmt is DoStatement || stmt is ForInStatement || stmt is SwitchStatement; } @override js_ast.Statement visitLabeledStatement(LabeledStatement node) { List? saved; // If the effective target is known then this statement is either contained // in a labeled statement or a loop. It has already been processed when // the enclosing statement was visited. if (!_effectiveTargets.containsKey(node)) { // Find the effective target by bypassing and collecting labeled // statements. var statements = [node]; var target = node.body; while (target is LabeledStatement) { var labeled = target; statements.add(labeled); target = labeled.body; } for (var statement in statements) { _effectiveTargets[statement] = target; } // If the effective target will compile to something that can have a // break from it without a label (e.g., a loop but not a block), then any // of the labeled statements can have a break from them by breaking from // the effective target. Otherwise breaks will need a label and a break // without a label can still target an outer breakable so the list of // current break targets does not change. if (_isBreakable(target)) { saved = _currentBreakTargets; _currentBreakTargets = statements; } } var result = _visitStatement(node.body); if (saved != null) _currentBreakTargets = saved; return result; } @override js_ast.Statement visitBreakStatement(BreakStatement node) { // Switch statements with continue labels must explicitly break to their // implicit label due to their being wrapped in a loop. if (_inLabeledContinueSwitch && _switchLabelStates.containsKey(node.target.body)) { return js_ast.Break(_switchLabelStates[node.target.body]!.label); } // Can it be compiled to a break without a label? if (_currentBreakTargets.contains(node.target)) { return js_ast.Break(null); } // Can it be compiled to a continue without a label? if (_currentContinueTargets.contains(node.target)) { return js_ast.Continue(null); } // Ensure the effective target is labeled. Labels are named globally per // Kernel binary. // // TODO(markzipan): Retrieve the real label name with source offsets var target = _effectiveTargets[node.target]; var name = _labelNames[target!]; if (name == null) _labelNames[target] = name = 'L${_labelNames.length}'; // It is a break if the target labeled statement encloses the effective // target. Statement current = node.target; while (current is LabeledStatement) { current = current.body; } if (identical(current, target)) { return js_ast.Break(name); } // Otherwise it is a continue. return js_ast.Continue(name); } // Labeled loop bodies can be the target of a continue without a label // (targeting the loop). Find the outermost non-labeled statement starting // from body and record all the intermediate labeled statements as continue // targets. Statement _effectiveBodyOf(Statement loop, Statement body) { // In a loop whose body is not labeled, this list should be empty because // it is not possible to continue to an outer loop without a label. _currentContinueTargets = []; while (body is LabeledStatement) { var labeled = body; _currentContinueTargets.add(labeled); _effectiveTargets[labeled] = loop; body = labeled.body; } return body; } T _translateLoop( Statement node, T Function() action, ) { List? savedBreakTargets; if (_currentBreakTargets.isNotEmpty && _effectiveTargets[_currentBreakTargets.first] != node) { // If breaking without a label targets some other (outer) loop, then // this loop prevents breaking to that loop without a label. This loop // was not labeled for a break in Kernel, otherwise it would be the // effective target of the current break targets, so it is not itself the // target of a break. savedBreakTargets = _currentBreakTargets; _currentBreakTargets = []; } var savedContinueTargets = _currentContinueTargets; var result = action(); if (savedBreakTargets != null) _currentBreakTargets = savedBreakTargets; _currentContinueTargets = savedContinueTargets; return result; } @override js_ast.While visitWhileStatement(WhileStatement node) { return _translateLoop(node, () { var condition = _visitTest(node.condition); var body = _visitScope(_effectiveBodyOf(node, node.body)); return js_ast.While(condition, body); }); } @override js_ast.Do visitDoStatement(DoStatement node) { return _translateLoop(node, () { var body = _visitScope(_effectiveBodyOf(node, node.body)); var condition = _visitTest(node.condition); return js_ast.Do(body, condition); }); } @override js_ast.Statement visitForStatement(ForStatement node) { return _translateLoop(node, () { js_ast.VariableInitialization emitForInitializer(VariableStatement s) => js_ast.VariableInitialization( _emitVariableDef(s.variable), _visitInitializer(s.variable.initializer, s.variable.annotations), ); if (node.variables.any(containsFunctionExpression)) { return _rewriteAsWhile(node); } var init = node.variables.map(emitForInitializer).toList(); var initList = init.isEmpty ? null : js_ast.VariableDeclarationList('let', init); var updates = node.updates; js_ast.Expression? update; if (updates.isNotEmpty) { update = js_ast.Expression.binary( updates.map(_visitExpression).toList(), ',', ).toVoidExpression(); } var condition = node.condition != null ? _visitTest(node.condition!) : null; var body = _visitScope(_effectiveBodyOf(node, node.body)); return js_ast.For(initList, condition, update, body); }); } /// Rewrites a `for(;;)` style loop as a while loop to produce the correct /// semantics when loop variable initializers contain function expressions /// that close over other loop variables. /// /// The Dart semantics expect that every loop iteration gets fresh loop /// variables that can be closed over. The initialization is only executed /// for the first iteration. In later iterations, the fresh loop variables are /// initialized to the values from the end of the previous iteration. /// /// These semantics differ from JavaScript when there are closures capturing /// loop variables so the simple lowering doesn't work as expected. /// /// A for loop like: /// /// ``` /// for(var v1 = init1, v2 = init2; condition; updates) { body } /// ``` /// /// Produces a rewrite like: /// /// ``` /// var initFlag = true; /// var prev_v1, prev_v2; /// while (true) { /// var v1, v2; /// if (initFlag) { /// initFlag = false; /// v1 = inti1; /// v2 = init2; /// } else { /// v1 = prev_v1; /// v2 = prev_v2; /// updates; /// } /// if (!condition) break; /// body; /// prev_v1 = v1; /// prev_v2 = v2; /// } /// ``` js_ast.Statement _rewriteAsWhile(ForStatement node) { var initFlagTempId = _emitScopedId('t#_init'); var loopVariableIds = { for (var stmt in node.variables) stmt.variable: _emitVariableDef(stmt.variable), }; var prevVariableTempIds = { for (var stmt in node.variables) stmt.variable: _emitScopedId('t#_prev_${stmt.variable.name!}'), }; var inits = js_ast.Block([ // Set init flag to false so the initialization only happens on the first // iteration of the while loop. js.statement('# = false;', [initFlagTempId]), // Initialize fresh loop variables to initial values. for (var stmt in node.variables) js.statement('# = #;', [ loopVariableIds[stmt.variable]!, _visitInitializer( stmt.variable.initializer, stmt.variable.annotations, ), ]), ]); var prevInits = js_ast.Block([ // Intialize fresh loop variables with the value from the previous // iteration. for (var stmt in node.variables) js.statement('# = #;', [ loopVariableIds[stmt.variable], prevVariableTempIds[stmt.variable], ]), // Original update expressions. for (var update in node.updates) _visitExpression(update).toStatement(), ]); return js_ast.Block([ // Create temporary variables for the initialization flag and previous // loop variables. js_ast.VariableDeclarationList('let', [ js_ast.VariableInitialization( initFlagTempId, js_ast.LiteralBool(true), ), for (var stmt in node.variables) js_ast.VariableInitialization( prevVariableTempIds[stmt.variable]!, null, ), ]).toStatement(), // The for loop transformed into a while loop. js_ast.While( js_ast.LiteralBool(true), js_ast.Block([ // Create fresh loop variables every iteration. if (node.variables.isNotEmpty) js_ast.VariableDeclarationList('let', [ for (var stmt in node.variables) js_ast.VariableInitialization( loopVariableIds[stmt.variable]!, null, ), ]).toStatement(), // Initialize loop variables. js_ast.If(initFlagTempId, inits, prevInits), // Loop condition guard. if (node.condition != null) js.statement('if (!#) break;', [_visitTest(node.condition!)]) ..sourceInformation = _nodeStart(node.condition!), // Original loop body. _visitScope(_effectiveBodyOf(node, node.body)), // Save previous loop variables for (var stmt in node.variables) js.statement('# = #;', [ prevVariableTempIds[stmt.variable]!, _emitVariableRef(stmt.variable), ]) // Map these locations to the variable declaration so stepping // in the Dart debugger doesn't jump to the previous line when // stepping. ..sourceInformation = _nodeStart(stmt.variable), ]), ) // The while loop gets mapped to the original for loop location. ..sourceInformation = _nodeStart(node), ]) // Clear the source mapping on the outer block so it doesn't automatically // get mapped to the for loop node in _visitStatement. ..sourceInformation = continueSourceMap; } @override js_ast.Statement visitForInStatement(ForInStatement node) { return _translateLoop(node, () { if (node.isAsync) { return _emitAwaitFor(node); } var iterable = _visitExpression(node.iterable); var body = _visitScope(_effectiveBodyOf(node, node.body)); var init = js.call('let #', _emitVariableDef(node.variable)); if (_annotatedNullCheck(node.variable.annotations)) { body = js_ast.Block([ _nullParameterCheck(_emitVariableRef(node.variable)), body, ]); } if (node.variable.name != null && js_ast.variableIsReferenced(node.variable.name!, iterable)) { var temp = _emitScopedId('iter'); return js_ast.Block([ iterable.toVariableDeclaration(temp), js_ast.ForOf(init, temp, body), ]); } return js_ast.ForOf(init, iterable, body); }); } js_ast.Statement _emitAwaitFor(ForInStatement node) { // Emits `await for (var value in stream) ...`, which desugars as: // // var iter = new StreamIterator(stream); // try { // while (await iter.moveNext()) { // var value = iter.current; // ... // } // } finally { // await iter.cancel(); // } // // Like the Dart VM, we call cancel() always, as it's safe to call if the // stream has already been cancelled. // // TODO(jmesserly): we may want a helper if these become common. For now the // full desugaring seems okay. var streamIterator = _coreTypes.nonNullableRawType( _asyncStreamIteratorClass, ); var streamIteratorRti = _emitType(streamIterator); var createStreamIter = js_ast.Call( _emitConstructorName( streamIterator, _asyncStreamIteratorClass.procedures.firstWhere( (p) => p.isFactory && p.name.text == '', ), ), [streamIteratorRti, _visitExpression(node.iterable)], ); var iter = _emitScopedId('iter'); var savedContinueTargets = _currentContinueTargets; var savedBreakTargets = _currentBreakTargets; _currentContinueTargets = []; _currentBreakTargets = []; var loopStmt = js.statement('while (#) { let # = #.current; #; }', [ js_ast.Await(js.call('#.moveNext()', iter)) ..sourceInformation = _nodeStart(node.variable), _emitVariableDef(node.variable), iter, _visitStatement(node.body), ]); // Any label on the Dart loop statement should target the inner loop rather // than the try-block we will wrap it in. final loopLabelName = _labelNames.remove(node); if (loopLabelName != null) { loopStmt = js_ast.LabeledStatement(loopLabelName, loopStmt); } var awaitForStmt = js_ast.Block([ js_ast.ExpressionStatement( js_ast.VariableDeclarationList('let', [ js_ast.VariableInitialization(iter, createStreamIter), ])..sourceInformation = _nodeStart(node.iterable), ), js.statement('try { # } finally { #; }', [ loopStmt, js_ast.Await(js.call('#.cancel()', iter)) ..sourceInformation = _nodeStart(node.variable), ]), ], isScope: true); _currentContinueTargets = savedContinueTargets; _currentBreakTargets = savedBreakTargets; return awaitForStmt; } @override js_ast.Statement visitSwitchStatement(SwitchStatement node) { // Switches with labeled continues are generated as an infinite loop with // an explicit variable for holding the switch's next case state and an // explicit label. Any implicit breaks are made explicit (e.g., when break // is omitted for the final case statement). var previous = _inLabeledContinueSwitch; _inLabeledContinueSwitch = hasLabeledContinue(node); var cases = []; if (_inLabeledContinueSwitch) { var labelState = _emitScopedId('labelState'); // TODO(markzipan): Retrieve the real label name with source offsets var labelName = 'SL${_switchLabelStates.length}'; _switchLabelStates[node] = _SwitchLabelState(labelName, labelState); // Since we wrap the switch in a 'while (true)' loop the continue targets // within the switch will no longer target the correct loop so we need // explicit breaks. final savedCurrentContinueTargets = _currentContinueTargets; _currentContinueTargets = []; for (var c in node.cases) { var subcases = _visitSwitchCase( c, lastSwitchCase: c == node.cases.last, ); if (subcases.isNotEmpty) cases.addAll(subcases); } _currentContinueTargets = savedCurrentContinueTargets; var switchExpr = _visitExpression(node.expression); var switchStmt = js_ast.Switch(labelState, cases); var loopBody = js_ast.Block([switchStmt, js_ast.Break(null)]); var loopStmt = js_ast.While(js.boolean(true), loopBody); // Note: Cannot use _labelNames, as the label must be on the loop. // not the block surrounding the switch statement. var labeledStmt = js_ast.LabeledStatement(labelName, loopStmt); var block = js_ast.Block([ js.statement('let # = #', [labelState, switchExpr]), labeledStmt, ]); _inLabeledContinueSwitch = previous; return block; } for (var c in node.cases) { var subcases = _visitSwitchCase(c); if (subcases.isNotEmpty) cases.addAll(subcases); } var stmt = js_ast.Switch(_visitExpression(node.expression), cases); _inLabeledContinueSwitch = previous; return stmt; } /// Helper for visiting a SwitchCase statement. /// /// [lastSwitchCase] is only used when the current switch statement contains /// labeled continues. Dart permits the final case to implicitly break, but /// switch statements with labeled continues must explicitly break/continue /// to escape the surrounding infinite loop. List _visitSwitchCase( SwitchCase node, { bool lastSwitchCase = false, }) { var cases = []; var emptyBlock = js_ast.Block.empty(); // TODO(jmesserly): make sure we are statically checking fall through var body = _visitStatement(node.body).toBlock(); var expressions = node.expressions; var lastExpr = expressions.isNotEmpty && !node.isDefault ? expressions.last : null; for (var e in expressions) { var jsExpr = _visitExpression(e); if (e is ConstantExpression && e.constant is NullConstant) { // Coerce null and undefined by adding an extra case. cases.add(js_ast.Case(js_ast.Prefix('void', js.number(0)), emptyBlock)); } cases.add(js_ast.Case(jsExpr, e == lastExpr ? body : emptyBlock)); } if (node.isDefault) { cases.add(js_ast.Default(body)); } // Switch statements with continue labels must explicitly break from their // last case to escape the additional loop around the switch. if (lastSwitchCase && _inLabeledContinueSwitch && cases.isNotEmpty) { // TODO(markzipan): avoid generating unreachable breaks var switchStmt = node.parent as SwitchStatement; assert(_switchLabelStates.containsKey(node.parent)); var breakStmt = js_ast.Break(_switchLabelStates[switchStmt]!.label); var switchBody = js_ast.Block(cases.last.body.statements..add(breakStmt)); var lastCase = cases.last; var updatedSwitch = lastCase is js_ast.Case ? js_ast.Case(lastCase.expression, switchBody) : js_ast.Default(switchBody); cases.removeLast(); cases.add(updatedSwitch); } return cases; } @override js_ast.Statement visitContinueSwitchStatement(ContinueSwitchStatement node) { var switchStmt = node.target.parent as SwitchStatement; if (_inLabeledContinueSwitch && _switchLabelStates.containsKey(switchStmt)) { var switchState = _switchLabelStates[switchStmt]!; // Use the first constant expression that can match the collated switch // case. Use an unused symbol otherwise to force the default case. var jsExpr = node.target.expressions.isEmpty ? js.call("Symbol('_default')", []) : _visitExpression(node.target.expressions[0]); var setStateStmt = js.statement('# = #', [switchState.variable, jsExpr]); var continueStmt = js_ast.Continue(switchState.label); return js_ast.Block([setStateStmt, continueStmt]); } return _emitInvalidNode( node, 'see https://github.com/dart-lang/sdk/issues/29352', ).toStatement(); } @override js_ast.Statement visitIfStatement(IfStatement node) { bool isTriviallyTrue(condition) => condition is js_ast.LiteralBool && condition.value; bool isTriviallyFalse(condition) => condition is js_ast.LiteralBool && !condition.value; var condition = _visitTest(node.condition); if (isTriviallyTrue(condition)) return _visitScope(node.then); var otherwise = node.otherwise; var hasElse = otherwise != null; if (isTriviallyFalse(condition)) { return hasElse ? _visitScope(otherwise) : js_ast.EmptyStatement(); } return hasElse ? js_ast.If(condition, _visitScope(node.then), _visitScope(otherwise)) : js_ast.If.noElse(condition, _visitScope(node.then)); } /// Visits a statement, and ensures the resulting AST handles block scope /// correctly. Essentially, we need to promote a variable declaration /// statement into a block in some cases, e.g. /// /// do var x = 5; while (false); // Dart /// do { let x = 5; } while (false); // JS js_ast.Statement _visitScope(Statement stmt) { var result = _visitStatement(stmt); if (result is js_ast.ExpressionStatement && result.expression is js_ast.VariableDeclarationList) { return js_ast.Block([result]); } return result; } @override js_ast.Statement visitReturnStatement(ReturnStatement node) { var expression = node.expression; var value = expression == null ? null : _visitExpression(expression); return _emitReturnStatement(value); } @override js_ast.Statement visitTryCatch(TryCatch node) { return js_ast.Try( _visitStatement(node.body).toBlock(), _visitCatch(node.catches), null, ); } js_ast.Catch? _visitCatch(List clauses) { if (clauses.isEmpty) return null; var caughtError = VariableDeclaration('#e', isSynthesized: true); var savedRethrow = _rethrowParameter; _rethrowParameter = caughtError; // If we have more than one catch clause, always create a temporary so we // don't shadow any names. var exceptionParameter = (clauses.length == 1 ? clauses[0].exception : null) ?? VariableDeclaration('#ex', isSynthesized: true); var stackTraceParameter = (clauses.length == 1 ? clauses[0].stackTrace : null) ?? (clauses.any((c) => c.stackTrace != null) ? VariableDeclaration('#st', isSynthesized: true) : null); js_ast.Statement catchBody = js_ast.Throw(_emitVariableRef(caughtError)); for (var clause in clauses.reversed) { catchBody = _catchClauseGuard( clause, catchBody, exceptionParameter, stackTraceParameter, ); } var catchStatements = [ js.statement('let # = #', [ _emitVariableDef(exceptionParameter), _runtimeCall('getThrown(#)', [_emitVariableRef(caughtError)]), ]), if (stackTraceParameter != null) js.statement('let # = #', [ _emitVariableDef(stackTraceParameter), _runtimeCall('stackTrace(#)', [_emitVariableRef(caughtError)]), ]), catchBody, ]; _rethrowParameter = savedRethrow; return js_ast.Catch( _emitVariableDef(caughtError), js_ast.Block(catchStatements, isScope: true), ); } js_ast.Statement _catchClauseGuard( Catch node, js_ast.Statement otherwise, VariableDeclaration exceptionParameter, VariableDeclaration? stackTraceParameter, ) { var body = []; var vars = HashSet(); void declareVariable( VariableDeclaration? variable, VariableDeclaration? value, ) { if (variable == null || value == null) return; vars.add(variable.name!); if (variable.name != value.name) { body.add( js.statement('let # = #', [ _emitVariableDef(variable), _emitVariableRef(value), ]), ); } } declareVariable(node.exception, exceptionParameter); declareVariable(node.stackTrace, stackTraceParameter); body.add(_visitStatement(node.body).toScopedBlock(vars)); // Each catch clause defines its own scope. var then = js_ast.Block(body, isScope: true); var guardType = node.guard.extensionTypeErasure; // Discard following clauses, if any, as they are unreachable. if (_types.isTop(guardType)) return then; var condition = _emitIsExpression( VariableGet(exceptionParameter), guardType, ); return js_ast.If(condition, then, otherwise) ..sourceInformation = _nodeStart(node); } @override js_ast.Statement visitTryFinally(TryFinally node) { var body = _visitStatement(node.body); var finallyBlock = _superDisallowed( () => _visitStatement(node.finalizer).toBlock(), ); if (body is js_ast.Try && body.finallyPart == null) { // Kernel represents Dart try/catch/finally as try/catch nested inside of // try/finally. Flatten that pattern in the output into JS try/catch/ // finally. return js_ast.Try(body.body, body.catchPart, finallyBlock); } return js_ast.Try(body.toBlock(), null, finallyBlock); } @override js_ast.Statement visitYieldStatement(YieldStatement node) { return js_ast.DartYield( _visitExpression(node.expression), node.isYieldStar, ); } @override js_ast.Statement visitVariableDeclaration(VariableDeclaration node) { // TODO(jmesserly): casts are sometimes required here. // Kernel does not represent these explicitly. var v = _emitVariableDef(node); return js.statement('let # = #;', [ v, _visitInitializer(node.initializer, node.annotations), ]); } @override js_ast.Statement visitFunctionDeclaration(FunctionDeclaration node) { var func = node.function; var fn = _emitFunction(func, node.variable.name); var name = _emitVariableDef(node.variable); js_ast.Statement declareFn; declareFn = js_ast.toBoundFunctionStatement(fn, name); if (_reifyFunctionType(func)) { declareFn = js_ast.Block([ declareFn, _emitFunctionTagged( _emitVariableRef(node.variable), func.computeThisFunctionType(Nullability.nonNullable), ).toStatement(), ]); } return declareFn; } @override js_ast.Expression visitInvalidExpression(InvalidExpression node) => _emitInvalidNode(node); @override js_ast.Expression visitConstantExpression(ConstantExpression node) => visitConstant(node.constant); @override js_ast.Expression visitVariableGet(VariableGet node) { var v = node.variable; var id = _emitVariableRef(v); if (id.name == v.name) { id = id.withSourceInformation( _variableSpan(node.fileOffset, v.name!.length), ); } return id; } /// Detects temporary variables so we can avoid displaying /// them in the debugger if needed. bool _isTemporaryVariable(VariableDeclaration v) { // Late local variables are be exposed to the debugger for inspection and // evaluation by treating the backing store local variable as a regular // non-temporary variable. // See https://github.com/dart-lang/sdk/issues/55918 if (isLateLoweredLocal(v)) return false; return v.isLowered || v.isSynthesized || v.name == null || v.name!.startsWith('#'); } /// Creates a temporary name recognized by the debugger. /// Assumes `_isTemporaryVariable(v)` is true. String? _debuggerFriendlyTemporaryVariableName(VariableDeclaration v) { assert(_isTemporaryVariable(v)); // Show extension 'this' in the debugger. // Do not show the rest of temporary variables. if (isExtensionThis(v)) { return extractLocalNameFromVariable(v); } else if (v.name != null) { return 't\$${v.name}'; } return null; } js_ast.ScopedId _emitVariableRef(VariableDeclaration v) { if (_isTemporaryVariable(v)) { var name = _debuggerFriendlyTemporaryVariableName(v); name ??= 't\$${_tempVariables.length}'; return _tempVariables.putIfAbsent( v, () => _emitScopedId(name!, needsCapture: true), ); } var name = v.name!; if (isLateLoweredLocal(v)) { // Late local variables are be exposed to the debugger for inspection and // evaluation by treating the backing store local variable as a regular // non-temporary variable. // See https://github.com/dart-lang/sdk/issues/55918 name = extractLocalNameFromLateLoweredLocal(name); } return js_ast.ScopedId.from( _variableTempIds[v] ??= _emitScopedId(name, needsCapture: true), ); } /// Emits the declaration of a variable. /// /// This is similar to [_emitVariableRef] but it also attaches source /// location information, so hover will work as expected. js_ast.Identifier _emitVariableDef(VariableDeclaration v) { var identifier = _emitVariableRef(v)..sourceInformation = _nodeStart(v); variableIdentifiers[v] = identifier; return identifier; } js_ast.Statement? _initLetVariables() { var letVars = _letVariables!; if (letVars.isEmpty) return null; var result = js_ast.VariableDeclarationList( 'let', letVars.map((v) => js_ast.VariableInitialization(v, null)).toList(), ).toStatement(); letVars.clear(); return result; } // TODO(jmesserly): resugar operators for kernel, such as ++x, x++, x+=. @override js_ast.Expression visitVariableSet(VariableSet node) { // Make the source information of the assignment use the start of the right // hand side, to help normalize the inconsistent locations of the CFE // lowerings for ++x, x++, x+=, etc. // See https://github.com/dart-lang/sdk/issues/55691. return _visitExpression(node.value).toAssignExpression( _emitVariableRef(node.variable), )..sourceInformation = _nodeStart(node.value); } @override js_ast.Expression visitDynamicGet(DynamicGet node) { var jsReceiver = _visitExpression(node.receiver); var jsMemberName = _emitMemberName(node.name.text); return _emitDynamicGet(jsReceiver, jsMemberName); } js_ast.Expression _emitDynamicGet( js_ast.Expression receiver, js_ast.Expression memberName, ) => _runtimeCall('dload$_replSuffix(#, #)', [receiver, memberName]); @override js_ast.Expression visitInstanceGet(InstanceGet node) { // TODO(nshahan): Marking an end span for property accessors would improve // source maps and hovering in the debugger. Unfortunately this is not // possible as Kernel does not store this data. var member = node.interfaceTarget; var receiver = node.receiver; var jsReceiver = _visitExpression(receiver); if (_isNonStaticJsInteropCallMember(member)) { // Historically DDC has treated this as a "callable class" and the access // of `.call` as a no-op. // // This is here to preserve the existing behavior for the non-static // JavaScript interop (including some failing cases) but could potentially // be cleaned up as a breaking change. return jsReceiver; } var memberName = node.name.text; if (member.isCoreObjectGetter && _shouldCallObjectMemberHelper(receiver)) { // The names of the static helper methods in the runtime must match the // names of the Object instance getters. return _runtimeCall('#(#)', [memberName, jsReceiver]); } // Otherwise generate this as a normal typed property get. var jsMemberName = _emitMemberName( memberName, member: node.interfaceTarget, ); var instanceGet = js_ast.PropertyAccess(jsReceiver, jsMemberName); if (_shouldRewriteInvocationWithHotReloadChecks(node.interfaceTarget)) { return switch (hotReloadCheckedBranch) { HotReloadBranchState.none => _emitHotReloadChecksConditional(node), HotReloadBranchState.uncheckedBranch => instanceGet, HotReloadBranchState.checkedBranch => // TODO(nshahan): Add a runtime helper to perform the checks only so // the call site can be monomorphic. // Since there are no arguments (unlike methods) the dynamic get path // can be reused for the hot reload checks on a getter. _emitCast( _emitDynamicGet(jsReceiver, _emitMemberName(memberName)), node.resultType, )..sourceInformation = _nodeStart(node), }; } return _isNullCheckableJsInterop(node.interfaceTarget) ? _wrapWithJsInteropNullCheck(instanceGet) : instanceGet; } @override js_ast.Expression visitRecordIndexGet(RecordIndexGet node) => _emitRecordElementGet(node.receiver, '\$${node.index + 1}'); @override js_ast.Expression visitRecordNameGet(RecordNameGet node) => _emitRecordElementGet(node.receiver, node.name); js_ast.Expression _emitRecordElementGet( Expression receiver, String elementName, ) => js_ast.PropertyAccess( _visitExpression(receiver), _emitMemberName(elementName), ); @override js_ast.Expression visitInstanceTearOff(InstanceTearOff node) { var member = node.interfaceTarget; var receiver = node.receiver; var jsReceiver = _visitExpression(receiver); if (_isNonStaticJsInteropCallMember(member)) { // Historically DDC has treated this as a "callable class" and the tearoff // of `.call` as a no-op. // // This is here to preserve the existing behavior for the non-static // JavaScript interop (including some failing cases) but could potentially // be cleaned up as a breaking change. return jsReceiver; } var memberName = node.name.text; if (member.isToStringOrNoSuchMethod && _shouldCallObjectMemberHelper(receiver)) { // The names of the static helper methods in the runtime must start with // the names of the Object instance methods. var tearOffName = '${memberName}Tearoff'; return _runtimeCall('#(#)', [tearOffName, jsReceiver]); } var jsMemberName = _emitMemberName(memberName, member: member); if (_reifyTearoff(member)) { return _runtimeCall('tearoff(#, null, #)', [jsReceiver, jsMemberName]); } var jsPropertyAccess = js_ast.PropertyAccess(jsReceiver, jsMemberName); return isJsMember(member) ? _runtimeCall('tearoffInterop(#, #)', [ jsPropertyAccess, js.boolean(_isNullCheckableJsInterop(member)), ]) : jsPropertyAccess; } /// Returns `true` when [member] is a `.call` member (field, getter or method) /// of a non-static JavaScript interop class. bool _isNonStaticJsInteropCallMember(Member member) => member.name.text == 'call' && isNonStaticJsInterop(member); @override js_ast.Expression visitDynamicSet(DynamicSet node) => _emitDynamicSet( _visitExpression(node.receiver), _emitMemberName(node.name.text), _visitExpression(node.value), ); js_ast.Expression _emitDynamicSet( js_ast.Expression receiver, js_ast.Expression memberName, js_ast.Expression value, ) => _runtimeCall('dput$_replSuffix(#, #, #)', [receiver, memberName, value]); @override js_ast.Expression visitInstanceSet(InstanceSet node) { var target = node.interfaceTarget; var receiver = _visitExpression(node.receiver); var memberName = _emitMemberName(node.name.text, member: target); var value = _visitExpression( isJsMember(target) ? _assertInterop(node.value) : node.value, ); var uncheckedSet = js.call('#.# = #', [receiver, memberName, value]); if (_shouldRewriteInvocationWithHotReloadChecks(target)) { return switch (hotReloadCheckedBranch) { HotReloadBranchState.none => _emitHotReloadChecksConditional(node), HotReloadBranchState.uncheckedBranch => uncheckedSet, HotReloadBranchState.checkedBranch => // TODO(nshahan): Add a runtime helper to perform the checks only so // the call site can be monomorphic. // Dart setters can contain a return statement but that value doesn't // get returned anywhere. Instead, the result of an assignment // expression is the RHS. DDC relies on the same behavior in the // JavaScript representation to carry the value through as the result // of the assignment expression. It is then sufficient to check the // type of the argument as it flows into the setter. If it is still // valid, then it should also be valid to flow through to the context // where the assignment appears. No extra cast on the assignment // expression is needed. _emitDynamicSet(receiver, memberName, value), }; } return uncheckedSet; } /// True when the result of evaluating [e] is not known to have the Object /// members installed so a helper method should be called instead of a direct /// instance invocation. /// /// This is a best effort approach determined by the static type information /// and may return `true` when the evaluation result does in fact have the /// members at runtime. bool _shouldCallObjectMemberHelper(Expression e) { if (_isNullable(e)) return true; var type = e.getStaticType(_staticTypeContext).extensionTypeErasure; if (type is RecordType || type is FunctionType) return false; if (type is InterfaceType) { // TODO(nshahan): This could be expanded to any classes where we know all // implementations at compile time and none of them are JS interop. var cls = type.classNode; // NOTE: This is not guaranteed to always be true. Currently in the SDK // none of the final classes or their subtypes use JavaScript interop. // If that was to ever change, this check will need to be updated. // For now, this is a shortcut since all subclasses of a class are not // immediately accessible. if (cls.isFinal && cls.enclosingLibrary.importUri.isScheme('dart')) { return false; } } // Constants have a static type known at compile time that will not be a // subtype at runtime. return !_triviallyConstNoInterop(e); } /// True when [e] is known to evaluate to a constant that has an interface /// type that is not a JavaScript interop type. /// /// This is a simple approach and not an exhaustive search. bool _triviallyConstNoInterop(Expression? e) { if (e is ConstantExpression) { var type = e.constant.getType(_staticTypeContext).extensionTypeErasure; if (type is InterfaceType) return !usesJSInterop(type.classNode); } else if (e is StaticGet && e.target.isConst) { var target = e.target; if (target is Field) { return _triviallyConstNoInterop(target.initializer); } } else if (e is VariableGet && e.variable.isConst) { return _triviallyConstNoInterop(e.variable.initializer); } return false; } /// Returns [expression] wrapped in an optional null check. /// /// The null check is enabled by setting a flag during the application /// bootstrap via `jsInteropNonNullAsserts(true)` in the SDK runtime library. js_ast.Expression _wrapWithJsInteropNullCheck(js_ast.Expression expression) => _runtimeCall('jsInteropNullCheck(#)', [expression]); /// Returns `true` when [member] is a JavaScript interop API that should be /// checked to be not null when the runtime flag `--interop-null-assertions` /// is enabled. /// /// These APIs are defined using the non-static package:js interop library and /// are typed to be non-nullable. bool _isNullCheckableJsInterop(Member member) { var type = member is Procedure ? member.function.returnType : member.getterType; return type.nullability == Nullability.nonNullable && isNonStaticJsInterop(member); } /// Return whether [member] returns a native object whose type needs to be /// null-checked in sound null-safety. /// /// This is true for non-nullable native return types. bool _isNullCheckableNative(Member member) { var c = member.enclosingClass; return member.isExternal && c != null && _extensionTypes.isNativeClass(c) && member is Procedure && member.function.returnType.isPotentiallyNonNullable && _isWebLibrary(member.enclosingLibrary.importUri); } // TODO(jmesserly): can we encapsulate REPL name lookups and remove this? // _emitMemberName would be a nice place to handle it, but we don't have // access to the target expression there (needed for `dart.replNameLookup`). String get _replSuffix => _options.replCompile ? 'Repl' : ''; @override js_ast.Expression visitAbstractSuperPropertyGet( AbstractSuperPropertyGet node, ) { return _emitSuperPropertyGet(node.interfaceTarget); } @override js_ast.Expression visitSuperPropertyGet(SuperPropertyGet node) { return _emitSuperPropertyGet(node.interfaceTarget); } /// Emits a reference to a distinct mixin application, represented by /// a [mixedInClass] being mixed into [baseClass]. /// /// Anonymous mixins should pass themselves as [baseClass] since they are /// already uniquely generated per distinct mixin application js_ast.Identifier _emitMixinId(Class mixedInClass, Class baseClass) { var mixinName = mixedInClass.name; if (!mixedInClass.isAnonymousMixin) { mixinName += '#${baseClass.name}'; } return _mixinCache.putIfAbsent(( mixedInClass, baseClass, ), () => _emitScopedId(mixinName)); } js_ast.Expression _emitSuperPropertyGet(Member target) { if (_reifyTearoff(target)) { if (_superAllowed) { var jsName = _declareMemberName(target); var enclosingClass = target.enclosingClass!; js_ast.Expression? supertypeReference = _mixinSuperclassCache[_currentClass!]; if (supertypeReference == null) { if (enclosingClass.isAnonymousMixin) { var mixinId = _emitMixinId(enclosingClass, enclosingClass); var enclosingLibrary = _emitLibraryName(getLibrary(enclosingClass)); supertypeReference = js_ast.PropertyAccess( enclosingLibrary, js.string(mixinId.name), ); } else { supertypeReference = _emitTopLevelNameNoExternalInterop( enclosingClass, ); } } return _runtimeCall('superTearoff(this, #, #)', [ supertypeReference, jsName, ]); } else { return _emitSuperTearoffFromDisallowedContext(target); } } return _emitSuperTarget(target); } @override js_ast.Expression visitAbstractSuperPropertySet( AbstractSuperPropertySet node, ) { return _emitSuperPropertySet(node.interfaceTarget, node.value); } @override js_ast.Expression visitSuperPropertySet(SuperPropertySet node) { return _emitSuperPropertySet(node.interfaceTarget, node.value); } js_ast.Expression _emitSuperPropertySet(Member target, Expression value) { var jsTarget = _emitSuperTarget(target, setter: true); return _visitExpression(value).toAssignExpression(jsTarget); } @override js_ast.Expression visitStaticGet(StaticGet node) { final target = node.target; if (_isDartJsHelper(target.enclosingLibrary)) { final name = target.name.text; if (name == 'staticInteropGlobalContext') { return _runtimeCall('global'); } } var staticGet = _emitStaticGet(target); return _isNullCheckableJsInterop(target) ? _wrapWithJsInteropNullCheck(staticGet) : staticGet; } @override js_ast.Expression visitStaticTearOff(StaticTearOff node) => _emitStaticGet(node.target); js_ast.Expression _emitStaticGet(Member target) { var propertyAccessor = _emitStaticTarget(target); var context = propertyAccessor.receiver; var property = propertyAccessor.selector; var result = js.call('#.#', [context, property]); if (_reifyTearoff(target)) { var targetLabel = js.string(fullyResolvedTargetLabel(target)); return _runtimeCall('staticTearoff(#, #, #)', [ context, targetLabel, property, ]); } return result; } @override js_ast.Expression visitStaticSet(StaticSet node) { var target = node.target; var result = _emitStaticTarget(target); var value = isJsMember(target) ? _assertInterop(node.value) : node.value; return _visitExpression(value).toAssignExpression(result); } @override js_ast.Expression visitDynamicInvocation(DynamicInvocation node) { return _emitDynamicInvocation( node.receiver, node.name.text, node.arguments, ); } @override js_ast.Expression visitFunctionInvocation(FunctionInvocation node) { var name = node.name.text; assert(name == 'call'); if (node.functionType == null) { // A `null` here implies the receiver is typed as `Function`. There isn't // any more type information available at compile time to know this // invocation is sound so a dynamic call will handle the checks at // runtime. return _emitDynamicInvocation(node.receiver, name, node.arguments); } return js_ast.Call( _visitExpression(node.receiver), _emitArgumentList(node.arguments), ); } @override js_ast.Expression visitInstanceInvocation(InstanceInvocation node) { var target = node.interfaceTarget; if (target.isPrimitiveOperator) { return _emitPrimitiveOperatorInvocation(node); } var receiver = node.receiver; var jsReceiver = _visitExpression(receiver); if (node.isNativeListInvariantAddInvocation(_coreTypes.listClass)) { // TODO(nshahan): If this code is retained, can it become invalid after a // hot reload? return js.call('#.push(#)', [ jsReceiver, _emitArgumentList(node.arguments, target: target), ]); } var name = node.name.text; if (name == 'call') { var directCallInvocation = _emitDirectInstanceCallInvocation(node); if (directCallInvocation != null) { return directCallInvocation; } } if (target.isToStringOrNoSuchMethodWithDefaultSignature && _shouldCallObjectMemberHelper(receiver)) { // Handle Object methods when the receiver could potentially be `null` or // JavaScript interop values with static helper methods. // The names of the static helper methods in the runtime must match the // names of the Object instance members. // TODO(nshahan): What should be checked after a hot reload. I think only // the return type of the NSM can change. return _runtimeCall('#(#, #)', [ name, jsReceiver, _emitArgumentList(node.arguments, target: target), ]); } // Otherwise generate this as a normal typed method call. var jsName = _emitMemberName(name, member: target); js_ast.Expression generateCall() { var args = _emitArgumentList(node.arguments, target: target); return js.call('#.#(#)', [jsReceiver, jsName, args]); } if (_shouldRewriteInvocationWithHotReloadChecks(target)) { return switch (hotReloadCheckedBranch) { HotReloadBranchState.none => _emitHotReloadChecksConditional(node), HotReloadBranchState.uncheckedBranch => generateCall(), HotReloadBranchState.checkedBranch => _rewriteInvocationWithHotReloadChecks( jsReceiver, jsName, target, node.arguments, node.getStaticType(_staticTypeContext), _nodeStart(node), ), }; } final invocation = generateCall(); return _isNullCheckableJsInterop(node.interfaceTarget) ? _wrapWithJsInteropNullCheck(invocation) : invocation; } /// Returns a direct invocation to support a `call()` method invocation when /// the receiver is considered directly callable, otherwise returns /// `null`. /// /// For example if `fn` is statically typed as a function type, then /// `fn.call()` would be considered directly callable and compiled as `fn()`. // TODO(nshahan): Handle retained call method invocations that have new // signatures or were deleted after a hot reload. js_ast.Expression? _emitDirectInstanceCallInvocation( InstanceInvocation node, ) { // Erasing the extension types here to support existing callable behavior // on the old style JS interop types that are callable. This should be // safe as it is a compile time error to try to dynamically invoke a call // method that is inherited from an extension type. var receiverType = node.receiver .getStaticType(_staticTypeContext) .extensionTypeErasure; if (!_isDirectCallable(receiverType)) return null; // Handle call methods on function types as function calls. var invocation = js_ast.Call( _visitExpression(node.receiver), _emitArgumentList(node.arguments, target: node.interfaceTarget), ); return _isNullCheckableJsInterop(node.interfaceTarget) ? _wrapWithJsInteropNullCheck(invocation) : invocation; } /// Returns an invocation of a primitive operator. /// /// See [ProcedureHelpers.isPrimitiveOperator]. // TODO(nshahan): Handle retained operator invocations that have new // signatures or were deleted after a hot reload. js_ast.Expression _emitPrimitiveOperatorInvocation(InstanceInvocation node) { var receiver = node.receiver; var target = node.interfaceTarget; var arguments = node.arguments; assert(arguments.types.isEmpty && arguments.named.isEmpty); // JavaScript interop does not support overloading of these operators. return switch (arguments.positional.length) { 0 => _emitUnaryOperator(receiver, target, node), 1 => _emitBinaryOperator(receiver, target, arguments.positional[0], node), // Should always be a compile time error but here for exhaustiveness. _ => throw UnsupportedError( 'Invalid number of positional arguments.\n' 'Found: ${arguments.positional.length}\n' 'Operator: ${node.name.text} ' '${node.location}', ), }; } @override js_ast.Expression visitInstanceGetterInvocation( InstanceGetterInvocation node, ) { if (node.functionType == null) { // A `null` here implies the receiver must be typed as `dynamic` or // `Function`. There isn't any more type information available at compile // time to know this invocation is sound so a dynamic call will handle the // checks at runtime. return _emitDynamicInvocation( node.receiver, node.name.text, node.arguments, ); } var getterInvocation = _emitInstanceGetterInvocation(node); return _isNullCheckableJsInterop(node.interfaceTarget) ? _wrapWithJsInteropNullCheck(getterInvocation) : getterInvocation; } js_ast.Expression _emitInstanceGetterInvocation( InstanceGetterInvocation node, ) { var receiver = _visitExpression(node.receiver); var arguments = _emitArgumentList( node.arguments, target: node.interfaceTarget, ); if (node.name.text == 'call') { // Erasing the extension types here to support existing callable behavior // on the old style JS interop types that are callable. This should be // safe as it is a compile time error to try to dynamically invoke a call // method that is inherited from an extension type. var receiverType = node.receiver .getStaticType(_staticTypeContext) .extensionTypeErasure; if (_isDirectCallable(receiverType)) { // Call methods on function types should be handled as function calls. return js_ast.Call(receiver, arguments); } } var memberName = _emitMemberName( node.name.text, member: node.interfaceTarget, ); // We must erase the extension type to potentially find the `call` method. // If the extension type has a runtime representation with a `call`: // // ``` // extension type Ext(C c) implements C {...} // class C { // call() {...} // } // ``` // // We can always erase eagerly because: // - Extension types that do not implement an interface that exposes a // `call` method will result in a static error at the call site. // - Calls to extension types that implement their own call method are // lowered by the CFE to top level static method calls. var erasedGetterType = node.interfaceTarget.getterType.extensionTypeErasure; if (erasedGetterType is InterfaceType) { var callName = _implicitCallTarget(erasedGetterType); if (callName != null) { return js.call('#.#.#(#)', [receiver, memberName, callName, arguments]); } } return js.call('#.#(#)', [receiver, memberName, arguments]); } @override js_ast.Expression visitLocalFunctionInvocation(LocalFunctionInvocation node) { assert(node.name.text == 'call'); final localName = VariableGet(node.variable)..fileOffset = node.fileOffset; return js_ast.Call( _visitExpression(localName), _emitArgumentList(node.arguments), ); } @override js_ast.Expression visitEqualsCall(EqualsCall node) { return _emitEqualityOperator( node.left, node.interfaceTarget, node.right, negated: false, ); } @override js_ast.Expression visitEqualsNull(EqualsNull node) { return _emitCoreIdenticalCall([ node.expression, NullLiteral(), ], negated: false); } /// Returns an invocation of the runtime helpers `dcall`, `dgcall`, `dsend`, /// or `dgsend` to perform dynamic checks before invoking [memberName] on /// [receiver] and passing [arguments]. js_ast.Expression _emitDynamicInvocation( Expression receiver, String memberName, Arguments arguments, ) { var jsArgs = [_visitExpression(receiver)]; String jsCode; var (:typeArguments, :positionalArguments, :namedArguments) = _emitArgumentGroups(arguments, isJSInterop: false); if (memberName == 'call') { if (typeArguments != null) { jsCode = 'dgcall(#, #'; jsArgs.add(js_ast.ArrayInitializer(typeArguments)); } else { jsCode = 'dcall(#'; } } else { if (typeArguments != null) { jsCode = 'dgsend$_replSuffix(#, #, #'; jsArgs.add(js_ast.ArrayInitializer(typeArguments)); } else { jsCode = 'dsend$_replSuffix(#, #'; } jsArgs.add(_emitMemberName(memberName)); } if (positionalArguments != null) { jsCode += ', #'; jsArgs.add(js_ast.ArrayInitializer(positionalArguments)); } else { jsCode += ', []'; } if (namedArguments != null) { jsCode += ', #'; jsArgs.add(js_ast.ObjectInitializer(namedArguments)); } jsCode += ')'; return _runtimeCall(jsCode, jsArgs); } bool _isDirectCallable(DartType t) => t is FunctionType || (t is InterfaceType && usesJSInterop(t.classNode)); js_ast.Expression? _implicitCallTarget(InterfaceType from) { var c = from.classNode; var member = _hierarchy.getInterfaceMember(c, Name('call')); if (member is Procedure && !member.isAccessor && !usesJSInterop(c)) { return _emitMemberName('call', member: member); } return null; } js_ast.Expression _emitUnaryOperator( Expression expr, Member? target, InvocationExpression node, ) { var op = node.name.text; if (target != null) { var dispatchType = _coreTypes.nonNullableRawType(target.enclosingClass!); if (_typeRep.unaryOperationIsPrimitive(dispatchType)) { if (op == '~') { if (_typeRep.isNumber(dispatchType)) { return _coerceBitOperationResultToUnsigned( node, js.call('~#', _notNull(expr)), ); } return _emitOperatorCall(expr, target, op, []); } if (op == 'unary-') op = '-'; return js.call('$op#', _notNull(expr)); } } return _emitOperatorCall(expr, target, op, []); } /// Bit operations are coerced to values on [0, 2^32). The coercion changes /// the interpretation of the 32-bit value from signed to unsigned. Most /// JavaScript operations interpret their operands as signed and generate /// signed results. js_ast.Expression _coerceBitOperationResultToUnsigned( Expression node, js_ast.Expression uncoerced, ) { // Don't coerce if the parent will coerce. var parent = node.parent; if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { return uncoerced; } // Don't do a no-op coerce if the most significant bit is zero. if (_is31BitUnsigned(node)) return uncoerced; // If the consumer of the expression is '==' or '!=' with a constant that // fits in 31 bits, adding a coercion does not change the result of the // comparison, e.g. `a & ~b == 0`. Expression? left; late Expression right; late String op; if (parent is InvocationExpression && parent.arguments.positional.length == 1) { op = parent.name.text; left = getInvocationReceiver(parent); right = parent.arguments.positional[0]; } else if (parent is EqualsCall) { left = parent.left; right = parent.right; op = '=='; } else if (parent is EqualsNull) { left = parent.expression; right = NullLiteral(); op = '=='; } if (left != null) { if (op == '==') { const MAX = 0x7fffffff; if (_asIntInRange(right, 0, MAX) != null) return uncoerced; if (_asIntInRange(left, 0, MAX) != null) return uncoerced; } else if (op == '>>') { if (_isDefinitelyNonNegative(left) && _asIntInRange(right, 0, 31) != null) { // Parent will generate `# >>> n`. return uncoerced; } } } return js.call('# >>> 0', uncoerced); } bool _nodeIsBitwiseOperation(InvocationExpression node) { switch (node.name.text) { case '&': case '|': case '^': case '~': return true; } return false; } int? _asIntInRange(Expression expr, int low, int high) { if (expr is IntLiteral) { if (expr.value >= low && expr.value <= high) return expr.value; return null; } if (_constants.isConstant(expr)) { var c = _constants.evaluate(expr); if (c is IntConstant && c.value >= low && c.value <= high) return c.value; } return null; } bool _isDefinitelyNonNegative(Expression expr) { if (expr is IntLiteral) return expr.value >= 0; // TODO(sra): Lengths of known list types etc. return expr is InvocationExpression && _nodeIsBitwiseOperation(expr); } /// Does the parent of [node] mask the result to [width] bits or fewer? bool _parentMasksToWidth(Expression node, int width) { var parent = node.parent; if (parent == null) return false; if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { if (parent.name.text == '&' && parent.arguments.positional.length == 1) { var left = getInvocationReceiver(parent); var right = parent.arguments.positional[0]; final max = (1 << width) - 1; if (left != null) { if (_asIntInRange(right, 0, max) != null) return true; if (_asIntInRange(left, 0, max) != null) return true; } } return _parentMasksToWidth(parent, width); } return false; } /// Determines if the result of evaluating [expr] will be an non-negative /// value that fits in 31 bits. bool _is31BitUnsigned(Expression expr) { const MAX = 32; // Includes larger and negative values. /// Determines how many bits are required to hold result of evaluation /// [expr]. [depth] is used to bound exploration of huge expressions. int bitWidth(Expression expr, int depth) { if (expr is IntLiteral) { return expr.value >= 0 ? expr.value.bitLength : MAX; } if (++depth > 5) return MAX; if (expr is InvocationExpression && expr.arguments.positional.length == 1) { var left = getInvocationReceiver(expr); var right = expr.arguments.positional[0]; if (left != null) { switch (expr.name.text) { case '&': return min(bitWidth(left, depth), bitWidth(right, depth)); case '|': case '^': return max(bitWidth(left, depth), bitWidth(right, depth)); case '>>': var shiftValue = _asIntInRange(right, 0, 31); if (shiftValue != null) { var leftWidth = bitWidth(left, depth); return leftWidth == MAX ? MAX : max(0, leftWidth - shiftValue); } return MAX; case '<<': var leftWidth = bitWidth(left, depth); var shiftValue = _asIntInRange(right, 0, 31); if (shiftValue != null) { return min(MAX, leftWidth + shiftValue); } var rightWidth = bitWidth(right, depth); if (rightWidth <= 5) { // e.g. `1 << (x & 7)` has a rightWidth of 3, so shifts by up // to (1 << 3) - 1 == 7 bits. return min(MAX, leftWidth + ((1 << rightWidth) - 1)); } return MAX; default: return MAX; } } } var value = _asIntInRange(expr, 0, 0x7fffffff); if (value != null) return value.bitLength; return MAX; } return bitWidth(expr, 0) < 32; } js_ast.Expression _emitBinaryOperator( Expression left, Member? target, Expression right, InvocationExpression node, ) { var op = node.name.text; if (op == '==') return _emitEqualityOperator(left, target, right); // TODO(jmesserly): using the target type here to work around: // https://github.com/dart-lang/sdk/issues/33293 if (target != null) { var targetClass = target.enclosingClass!; var leftType = _coreTypes.nonNullableRawType(targetClass); var rightType = right.getStaticType(_staticTypeContext); if (_typeRep.binaryOperationIsPrimitive(leftType, rightType) || targetClass == _coreTypes.stringClass && op == '+') { // Inline operations on primitive types where possible. // TODO(jmesserly): inline these from dart:core instead of hardcoding // the implementation details here. /// Emits an inlined binary operation using the JS [code], adding null /// checks if needed to ensure we throw the appropriate error. js_ast.Expression binary(String code) { return js.call(code, [_notNull(left), _notNull(right)]) ..sourceInformation = continueSourceMap; } js_ast.Expression bitwise(String code) { return _coerceBitOperationResultToUnsigned(node, binary(code)); } /// Similar to [binary] but applies a boolean conversion to the right /// operand, to match the boolean bitwise operators in dart:core. /// /// Short circuiting operators should not be used in [code], because the /// null checks for both operands must happen unconditionally. js_ast.Expression bitwiseBool(String code) { return js.call(code, [_notNull(left), _visitTest(right)]); } switch (op) { case '~/': // `a ~/ b` is equivalent to `(a / b).truncate()` return js.call('(# / #).#()', [ _notNull(left), _notNull(right), _emitMemberName('truncate', memberClass: targetClass), ]); case '%': // TODO(sra): We can generate `a % b + 0` if both are non-negative // (the `+ 0` is to coerce -0.0 to 0). return _emitOperatorCall(left, target, op, [right]); case '&': return _typeRep.isBoolean(leftType) ? bitwiseBool('!!(# & #)') : bitwise('# & #'); case '|': return _typeRep.isBoolean(leftType) ? bitwiseBool('!!(# | #)') : bitwise('# | #'); case '^': return _typeRep.isBoolean(leftType) ? bitwiseBool('# !== #') : bitwise('# ^ #'); case '>>': var shiftCount = _asIntInRange(right, 0, 31); if (_is31BitUnsigned(left) && shiftCount != null) { return binary('# >> #'); } if (_isDefinitelyNonNegative(left) && shiftCount != null) { return binary('# >>> #'); } // If the context selects out only bits that can't be affected by // the sign position we can use any JavaScript shift, // `(x >> 6) & 3`. if (shiftCount != null && _parentMasksToWidth(node, 31 - shiftCount)) { return binary('# >> #'); } return _emitOperatorCall(left, target, op, [right]); case '<<': if (_is31BitUnsigned(node)) { // Result is 31 bit unsigned which implies the shift count was // small enough not to pollute the sign bit. return binary('# << #'); } if (_asIntInRange(right, 0, 31) != null) { return _coerceBitOperationResultToUnsigned( node, binary('# << #'), ); } return _emitOperatorCall(left, target, op, [right]); case '>>>': if (_asIntInRange(right, 0, 31) != null) { return binary('# >>> #'); } return _emitOperatorCall(left, target, op, [right]); default: // TODO(vsm): When do Dart ops not map to JS? return binary('# $op #'); } } } return _emitOperatorCall(left, target, op, [right]); } js_ast.Expression _emitEqualityOperator( Expression left, Member? target, Expression right, { bool negated = false, }) { var targetClass = target?.enclosingClass; var leftType = left.getStaticType(_staticTypeContext).extensionTypeErasure; // Conceptually `x == y` in Dart is defined as: // // If either x or y is null, then they are equal iff they are both null. // Otherwise, equality is the result of calling `x.==(y)`. // // In practice, `x.==(y)` is equivalent to `identical(x, y)` in many cases: // - when either side is known to be `null` (literal or Null type) // - left side is an enum // - left side is a primitive type // // We also compile `operator ==` methods to ensure they check the right side // for null`. This allows us to skip the check at call sites. // // TODO(leafp,jmesserly): we could use class hierarchy analysis to check // if `operator ==` was overridden, similar to how we devirtualize private // fields. // // If we know that the left type uses identity for equality, we can // sometimes emit better code, either `===` or `==`. var isEnum = leftType is InterfaceType && leftType.classNode.isEnum; var usesIdentity = _typeRep.isPrimitive(leftType) || isEnum || _isNull(left) || _isNull(right); if (usesIdentity) { return _emitCoreIdenticalCall([left, right], negated: negated); } if (_shouldCallObjectMemberHelper(left)) { // The LHS isn't guaranteed to have an equals method we need to use a // runtime helper. return js.call(negated ? '!#' : '#', [ _runtimeCall('equals(#, #)', [ _visitExpression(left), _visitExpression(right), ]), ]); } // Otherwise it is safe to call the equals method on the LHS directly. return js.call(negated ? '!#[#](#)' : '#[#](#)', [ _visitExpression(left), _emitMemberName('==', memberClass: targetClass), _visitExpression(right), ]); } /// Emits a generic send, like an operator method. /// /// **Please note** this function does not support method invocation syntax /// `obj.name(args)` because that could be a getter followed by a call. /// See [visitMethodInvocation]. js_ast.Expression _emitOperatorCall( Expression receiver, Member? target, String name, List args, ) { // TODO(jmesserly): calls that don't pass `element` are probably broken for // `super` calls from disallowed super locations. var memberName = _emitMemberName(name, member: target); if (target == null) { // dynamic dispatch var dynamicHelper = const {'[]': 'dindex', '[]=': 'dsetindex'}[name]; if (dynamicHelper != null) { return _runtimeCall('$dynamicHelper(#, #)', [ _visitExpression(receiver), _visitExpressionList(args), ]); } else { return _runtimeCall('dsend(#, #, [#])', [ _visitExpression(receiver), memberName, _visitExpressionList(args), ]); } } // Generic dispatch to a statically known method. return js.call('#.#(#)', [ _visitExpression(receiver), memberName, _visitExpressionList(args), ]); } // TODO(jmesserly): optimize super operators for kernel @override js_ast.Expression visitAbstractSuperMethodInvocation( AbstractSuperMethodInvocation node, ) { return _emitSuperMethodInvocation(node.interfaceTarget, node.arguments); } @override js_ast.Expression visitSuperMethodInvocation(SuperMethodInvocation node) { return _emitSuperMethodInvocation(node.interfaceTarget, node.arguments); } js_ast.Expression _emitSuperMethodInvocation( Member target, Arguments arguments, ) { return js_ast.Call( _emitSuperTarget(target), _emitArgumentList(arguments, target: target), ); } /// Emits the [js_ast.PropertyAccess] for accessors or method calls to /// [jsTarget].[jsName], replacing `super` if it is not allowed in scope. js_ast.PropertyAccess _emitSuperTarget(Member member, {bool setter = false}) { var jsName = _declareMemberName(member); if (_optimizeNonVirtualFieldAccess && member is Field && !_virtualFields.isVirtual(member)) { return js_ast.PropertyAccess(js_ast.This(), jsName); } if (_superAllowed) return js_ast.PropertyAccess(js_ast.Super(), jsName); // If we can't emit `super` in this context, generate a helper that does it // for us, and call the helper. // // NOTE: This is intended to help in the cases of calling a `super` getter, // setter, or method. For the case of tearing off a `super` method in // contexts where `super` isn't allowed, see // [_emitSuperTearoffFromDisallowedContext]. var name = member.name.text; var getter = (member is Field && !setter) || (member is Procedure && member.isGetter); // Prefix applied to the name only used in the compiler for a map key. This // name does not make its way into the compiled program. var lookupPrefix = setter ? r'set$' : getter ? r'get$' : ''; var jsMethod = _superHelpers.putIfAbsent('$lookupPrefix$name', () { var isAccessor = member is Procedure ? member.isAccessor : true; if (isAccessor) { assert( member is Procedure ? member.isSetter == setter : !setter || !(member as Field).isFinal, ); var fn = js.fun( setter ? 'function(x) { super[#] = x; }' : 'function() { return super[#]; }', [jsName], ); return js_ast.Method( _emitScopedId(name), fn, isGetter: !setter, isSetter: setter, ); } else { var function = member.function; var params = [ ..._emitTypeFormals(function.typeParameters), for (var param in function.positionalParameters) _emitIdentifier(param.name!), if (function.namedParameters.isNotEmpty) _namedArgumentTemp, ]; var fn = js.fun('function(#) { return super[#](#); }', [ params, jsName, params, ]); name = js_ast.friendlyNameForDartOperator[name] ?? name; return js_ast.Method(_emitScopedId(name), fn); } }); return js_ast.PropertyAccess(js_ast.This(), jsMethod.name); } /// Generates a special string used for identifying a torn off member [m]. /// /// This tag is used for determining tearoff equality. We attach these tags /// at tearoff time for static tearoffs and in the method signature for /// dynamic tearoffs. String fullyResolvedTargetLabel(Member m) { return '${m.enclosingLibrary.importUri}:${m.enclosingClass?.name ?? ""}'; } /// Generates a special string used for identifying class [c]'s applied mixed /// in members. /// /// This tag is used for determining tearoff equality. We attach these tags /// at tearoff time for static tearoffs and in the method signature for /// dynamic tearoffs. String fullyResolvedMixinClassLabel(Class c) { return '${c.enclosingLibrary.importUri}:${c.name}'; } /// Generates a helper method that is inserted into the class that binds a /// tearoff of [member] from `super` and returns a call to the helper. /// /// This method assumes `super` is not allowed in the current context. // TODO(nshahan) Replace with a kernel transform and synthetic method filters // for devtools. js_ast.Expression _emitSuperTearoffFromDisallowedContext(Member member) { var jsName = _declareMemberName(member); var name = '_#super#tearOff#${member.name.text}'; var jsMethod = _superHelpers.putIfAbsent(name, () { var superclass = member.enclosingClass!; var supertypeReference = _mixinSuperclassCache[superclass] ?? _emitTopLevelNameNoExternalInterop(superclass); var jsReturnValue = _runtimeCall('superTearoff(this, #, #)', [ supertypeReference, jsName, ]); var fn = js.fun('function() { return #; }', [jsReturnValue]); name = js_ast.friendlyNameForDartOperator[name] ?? name; return js_ast.Method(_emitScopedId(name), fn); }); return js_ast.Call(js_ast.PropertyAccess(js_ast.This(), jsMethod.name), []); } /// If [e] is a [TypeLiteral] or a [TypeLiteralConstant] expression, return /// the underlying [DartType], otherwise returns null. // TODO(sigmund,nshahan): remove all uses of type literals in the runtime // libraries, so that this pattern can be deleted. DartType? _getTypeLiteralType(Expression e) { if (e is TypeLiteral) return e.type; if (e is ConstantExpression) { var constant = e.constant; if (constant is TypeLiteralConstant) { return constant.type.withDeclaredNullability(Nullability.nonNullable); } } return null; } @override js_ast.Expression visitStaticInvocation(StaticInvocation node) { var target = node.target; if (ExternalEffect.isExternalEffect(node)) { return js_ast.LiteralNull(); } if (isInlineJS(target)) return _emitInlineJSCode(node) as js_ast.Expression; if (target.isFactory) return _emitFactoryInvocation(node); var enclosingLibrary = target.enclosingLibrary; if (_isDartLibrary(enclosingLibrary, '_rti') && _inlineTester.canInline(target.function)) { // Transform code that would otherwise appear as a static invocation: // ``` // if (_rti._isString(object)) {...} // ``` // // to be avoid cost of extra function calls: // // ``` // if (typeof object == "string") {...} // ``` var body = node.target.function.body; Expression? bodyToInline; // Extract the body. if (body is ReturnStatement) { // Ex: foo() => ; bodyToInline = body.expression; } else if (body is Block) { // Ex: foo() { } var singleStatement = body.statements.single; if (singleStatement is ReturnStatement) { bodyToInline = singleStatement.expression; } } if (bodyToInline != null) { // Clone the function parameters and create the mappings from the clone // to the argument passed. var cloner = CloneVisitorNotMembers(); var originalParameters = target.function.positionalParameters; var replacementArguments = node.arguments.positional; var replacements = { for (var i = 0; i < originalParameters.length; i++) originalParameters[i].accept(cloner) as VariableDeclaration: replacementArguments[i], }; // Clone the body using the same cloner to ensure the cloned parameters // are correctly linked to their accesses. var cloneToInline = bodyToInline.accept(cloner); // Substitute the use of the parameters with the values passed. var replacer = VariableGetReplacer(replacements); var replaced = cloneToInline.accept(replacer) as Expression; // Compile the result normally and wrap in parenthesis. return js.call('(#)', [replaced.accept(this)]); } } if (_isDartInternal(enclosingLibrary)) { var args = node.arguments; if (args.positional.length == 1 && args.types.length == 1 && args.named.isEmpty && target.name.text == 'unsafeCast') { // Optimize some internal SDK calls by avoiding the insertion of a // runtime cast. return args.positional.single.accept(this); } else if (node.arguments.positional.length == 2 && node.arguments.types.length == 1 && node.arguments.named.isEmpty && target.name.text == 'extractTypeArguments') { // Inline the extraction and method call at compile time because we // don't preserve the original type argument names into the runtime. // Those names are needed in the evaluation string used to extract the // types from the provided instance. // At this time the only two uses of this method are extracting from // `Iterable` and `Map`. There are no extension type uses so no need for // erasure here. var extractionType = node.arguments.types.single; if (extractionType is! InterfaceType) { throw UnsupportedError( 'Type arguments can only be extracted from interface types: ' 'found $extractionType (${extractionType.runtimeType}) at ' '${node.location}', ); } var extractionTypeParameters = extractionType.classNode.typeParameters; if (extractionTypeParameters.isEmpty) { throw UnsupportedError( 'The extraction type must have type arguments to be extracted: ' 'found $extractionType (${extractionType.runtimeType}) at ' '${node.location}', ); } var extractionTypeParameterNames = extractionTypeParameters.map( (p) => '${extractionType.classNode.name}.${p.name!}', ); var instance = node.arguments.positional.first.accept(this); var function = node.arguments.positional.last.accept(this); var extractedTypeArgs = js_ast.ArrayInitializer([ for (var recipe in extractionTypeParameterNames) js.call('#.#(#, "$recipe")', [ _emitLibraryName(_rtiLibrary), _emitMemberName('evalInInstance', memberClass: _rtiClass), instance, ]), ]); return _runtimeCall('dgcall(#, #, [])', [function, extractedTypeArgs]); } } if (_isDartForeignHelper(enclosingLibrary)) { var args = node.arguments.positional; var typeArgs = node.arguments.types; var name = target.name.text; if (args.isEmpty) { if (typeArgs.isEmpty && name == 'DART_RUNTIME_LIBRARY') { return _emitLibraryName(_runtimeLibrary); } if (typeArgs.length == 1) { if (name == 'TYPE_REF') { return _emitType(typeArgs.single); } } } if (args.length == 1) { if (name == 'getInterceptor') { var argExpression = args.single.accept(this); return _runtimeCall('getInterceptorForRti(#)', [argExpression]); } if (name == 'JS_GET_NAME') { var staticGet = args.single as StaticGet; var enumField = staticGet.target as Field; return _emitExpressionForJsGetName(_asJsGetName(enumField)); } if (name == 'JS_CLASS_REF') { var constNode = args.single as ConstantExpression; var typeConstant = constNode.constant as TypeLiteralConstant; var type = typeConstant.type; if (type is NullType) { return _emitTopLevelName(_coreTypes.deprecatedNullClass); } if (type is! InterfaceType) { throw UnsupportedError( 'JS_CLASS_REF only supports interface types: found $type ' '(${type.runtimeType}) at ${node.location}', ); } return _emitTopLevelName(type.classNode); } if (name == 'RAW_DART_FUNCTION_REF') { var expression = args.single as ConstantExpression; var fn = expression.constant as StaticTearOffConstant; return _emitStaticTarget(fn.target); } if (name == 'JS_GET_FLAG') { var flag = args.single as StringLiteral; var value = flag.value; return switch (value) { 'DEV_COMPILER' => js.boolean(true), 'MINIFIED' => js.boolean(false), 'VARIANCE' => // Variance is turned on by default, but only interfaces that have // at least one type parameter with non-legacy variance will have // extra information recorded. js.boolean(true), _ => throw UnsupportedError( 'Unknown JS_GET_FLAG "$value" at ${node.location}', ), }; } } else if (args.length == 2) { if (name == 'JS_EMBEDDED_GLOBAL') return _emitEmbeddedGlobal(node); if (name == 'JS_STRING_CONCAT') { var left = _visitExpression(args.first); var right = _visitExpression(args.last); return js.call('# + #', [left, right]); } } if (name == 'JS_BUILTIN') { var staticGet = args[1] as StaticGet; var enumField = staticGet.target as Field; return _emitOperationForJsBuiltIn(_asJsBuiltin(enumField)); } if (name == 'JS_RAW_EXCEPTION') { // Serves as a way to access the wrapped JS exception. return _emitVariableRef(_rethrowParameter!); } if (name == 'JS_RTI_PARAMETER') { return _rtiParam; } } if (_isSdkInternalRuntime(enclosingLibrary)) { var name = target.name.text; if (node.arguments.positional.isEmpty) { if (name == 'hotRestartGeneration') { return js.call('dartDevEmbedder.hotRestartGeneration'); } } else if (node.arguments.positional.length == 1) { var firstArg = node.arguments.positional.single; if (name == 'extensionSymbol' && firstArg is StringLiteral) { return _getSymbol(_getExtensionSymbolInternal(firstArg.value)); } } else if (node.arguments.positional.length == 2) { var firstArg = node.arguments.positional[0]; var secondArg = node.arguments.positional[1]; var type = _getTypeLiteralType(secondArg); if (name == '_jsInstanceOf' && type is InterfaceType && type.typeArguments.isEmpty) { return js.call('# instanceof #', [ _visitExpression(firstArg), _emitTopLevelName(type.classNode), ]); } } } if (_isDartJsHelper(enclosingLibrary)) { var name = target.name.text; if (name == 'jsObjectGetPrototypeOf') { var obj = node.arguments.positional.single; return _emitJSObjectGetPrototypeOf( _visitExpression(obj), fullyQualifiedName: false, ); } if (name == 'jsObjectSetPrototypeOf') { var obj = node.arguments.positional.first; var prototype = node.arguments.positional.last; return _emitJSObjectSetPrototypeOf( _visitExpression(obj), _visitExpression(prototype), fullyQualifiedName: false, ); } } if (target.isExternal && target.isExtensionTypeMember && target.function.namedParameters.isNotEmpty) { // JS interop checks assert that only external extension type constructors // and factories have named parameters. assert(target.function.positionalParameters.isEmpty); var namedProperties = {}; for (var named in node.arguments.named) { var name = named.name; for (var parameter in target.function.namedParameters) { if (parameter.name == named.name) { var customName = getDartJSInteropJSName(parameter); if (customName.isNotEmpty) { name = customName; } break; } } namedProperties[name] = named.value; } return _emitObjectLiteral(namedProperties, isDartJsInterop: true); } if (target == _coreTypes.identicalProcedure) { return _emitCoreIdenticalCall(node.arguments.positional); } if (_isDebuggerCall(target)) { return _emitDebuggerCall(node) as js_ast.Expression; } if (_isDartJsUtil(enclosingLibrary)) { // We try and do further inlining here for the unchecked/trusted-type // variants of js_util methods. Note that we only lower the methods that // are used in transformations and are private. Also note that this // inlining ignores `sdk/lib/_internal/js_shared/lib/js_util_patch.dart`'s // implementations for the lowered methods. // // If you update the code there, you should update the code here. // Long-term, we'll need a better IR to lower interop methods to, or a DDC // inliner to do the inlining for us. var name = target.name.text; if (name == '_getPropertyTrustType') { return js_ast.PropertyAccess( _visitExpression(node.arguments.positional[0]), _visitExpression(node.arguments.positional[1]), ); } else if (name == '_setPropertyUnchecked') { return _visitExpression( node.arguments.positional[2], ).toAssignExpression( js_ast.PropertyAccess( _visitExpression(node.arguments.positional[0]), _visitExpression(node.arguments.positional[1]), ), ); } else if (_callMethodUncheckedRegex.hasMatch(name)) { // Note that we don't lower `_callMethodTrustType`. This is because it // uses `assertInterop` checks. var trustType = name.contains('TrustType'); var args = []; assert(node.arguments.named.isEmpty); // Ignore the receiver and name of the method. for (var i = 2; i < node.arguments.positional.length; i++) { args.add(_visitExpression(node.arguments.positional[i])); } js_ast.Expression call = js_ast.Call( js_ast.PropertyAccess( _visitExpression(node.arguments.positional[0]), _visitExpression(node.arguments.positional[1]), ), args, ); if (!trustType) { call = _emitCast(call, node.arguments.types[0]); } return call; } else if (_callConstructorUncheckedRegex.hasMatch(name)) { var args = []; assert(node.arguments.named.isEmpty); // Ignore the constructor. for (var i = 1; i < node.arguments.positional.length; i++) { args.add(_visitExpression(node.arguments.positional[i])); } return _emitCast( js_ast.New(_visitExpression(node.arguments.positional[0]), args), node.arguments.types[0], ); } } js_ast.Call generateCall(js_ast.PropertyAccess fn) { var args = _emitArgumentList(node.arguments, target: target); return js_ast.Call(fn, args)..sourceInformation = _nodeStart(node); } if (_shouldRewriteInvocationWithHotReloadChecks(target)) { switch (hotReloadCheckedBranch) { case HotReloadBranchState.none: return _emitHotReloadChecksConditional(node); case HotReloadBranchState.uncheckedBranch: return generateCall(_emitStaticTarget(target)); case HotReloadBranchState.checkedBranch: final fn = _emitStaticTarget(target); return _rewriteInvocationWithHotReloadChecks( fn.receiver, fn.selector, target, node.arguments, node.getStaticType(_staticTypeContext), _nodeStart(node), ); } } final staticCall = generateCall(_emitStaticTarget(target)); return _isNullCheckableJsInterop(target) ? _wrapWithJsInteropNullCheck(staticCall) : staticCall; } /// Returns `true` when an invocation of [target] should be rewritten to /// include checks to preserve soundness in the presence of hot reloads at /// runtime. bool _shouldRewriteInvocationWithHotReloadChecks(Member target) => (_inFunctionExpression || _inAsyncExpression) && !_isBuildingSdk && !usesJSInterop(target) && target != _assertInteropMethod; /// Wraps [node] in a single conditional that will test if the compile time /// hot reload generation matches the current hot reload generation at /// runtime. /// /// When the generations match the expression is simply evaluated. When they /// don't, the expression is evaluated with checks for soundness issues. js_ast.Expression _emitHotReloadChecksConditional(Expression node) { var savedCheckedBranch = hotReloadCheckedBranch; hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch; var uncheckedExpression = _visitExpression(node); hotReloadCheckedBranch = HotReloadBranchState.checkedBranch; var checkedExpression = _visitExpression(node); hotReloadCheckedBranch = savedCheckedBranch; var generationCheck = js.call('# === #', [ js.number(_hotReloadGeneration), _runtimeCall('global.dartDevEmbedder.hotReloadGeneration'), ]); return js_ast.InvocationWithHotReloadChecks( generationCheck, uncheckedExpression, checkedExpression, )..sourceInformation = continueSourceMap; } /// Rewrites an invocation of [target] that is statically sound at compile /// time to include checks to preserve soundness in the presence of hot /// reloads at runtime. /// /// The compiled JavaScript [receiver] and [selector] should be passed so that /// they can be reused for the validated invocation after the checks have /// passed. /// /// The checks are similar to the those performed when making a dynamic call. /// The [arguments] are checked for the correct shape and runtime types. /// Additionally after the invocation, the returned value is checked against /// the [expectedReturnType]. /// /// Invokes `noSuchMethod` if the check fails. If that also returns a value, /// it's cast to the [expectedReturnType]. /// /// The resulting expression for the validated call site will receive the /// [originalCallSiteSourceLocation]. js_ast.Expression _rewriteInvocationWithHotReloadChecks( js_ast.Expression receiver, js_ast.Expression selector, Procedure target, Arguments arguments, DartType expectedReturnType, SourceLocation? originalCallSiteSourceLocation, ) { // Create a temporary let variable for the receiver to handle chains of // invocations. It should be evaluated before the arguments to match the // expected evaluation order. var receiverTemp = _emitScopedId('\$rec'); var letAssignments = js.call('# = #', [receiverTemp, receiver]); _letVariables!.add(receiverTemp); var hoistedPositionalVariables = []; var hoistedNamedVariables = {}; for (var i = 0; i < arguments.positional.length; i++) { var argument = arguments.positional[i]; var argumentExpression = _visitExpression(argument); if (argument is VariableGet || argument is BasicLiteral) { // No need to create new let variables, it is safe to repeat these // expressions. hoistedPositionalVariables.add(argumentExpression); continue; } // Hoist the passed positional argument expressions into let variables. var hoistedVariable = _emitScopedId('\$p$i'); _letVariables!.add(hoistedVariable); var assignment = js.call('# = #', [hoistedVariable, argumentExpression]); hoistedPositionalVariables.add(hoistedVariable); letAssignments = js_ast.Binary(',', letAssignments, assignment); } for (var i = 0; i < arguments.named.length; i++) { var argument = arguments.named[i]; var argumentName = argument.name; var argumentValue = _visitExpression(argument.value); if (argument is VariableGet || argument is BasicLiteral) { // No need to create new let variables, it is safe to repeat these // expressions. hoistedNamedVariables[argumentName] = argumentValue; continue; } // Hoist the passed named argument expressions into let variables. var hoistedVariable = _emitScopedId('\$n$i'); _letVariables!.add(hoistedVariable); var assignment = js.call('# = #', [hoistedVariable, argumentValue]); hoistedNamedVariables[argumentName] = hoistedVariable; letAssignments = js_ast.Binary(',', letAssignments, assignment); } // Create an invocation of the correctness checks to verify the call is // still valid. var checkResult = _emitScopedId('\$result'); _letVariables!.add(checkResult); var typeArguments = [ // TODO(nshahan): Remove this check if we stop rewriting calls to SDK // functions. if (_reifyGenericFunction(target)) for (var typeArgument in arguments.types) _emitType(typeArgument), ]; var correctnessCheck = _runtimeCall( 'hotReloadCorrectnessChecks(#, #, #, #, #)', [ receiverTemp, selector, js_ast.ArrayInitializer(typeArguments), js_ast.ArrayInitializer(hoistedPositionalVariables), hoistedNamedVariables.isEmpty ? js_ast.LiteralNull() : js_ast.ObjectInitializer([ for (var e in hoistedNamedVariables.entries) js_ast.Property(js.string(e.key), e.value), ]), ], ); var checkAssignment = js.call('# = #', [checkResult, correctnessCheck]); letAssignments = js_ast.Binary(',', letAssignments, checkAssignment); // Create a new invocation of the original target but passing all the // arguments via their let variables. var validatedCallSite = js.call('#.#(#)', [ receiverTemp, selector, [ ...typeArguments, ...hoistedPositionalVariables, if (hoistedNamedVariables.isNotEmpty) js_ast.ObjectInitializer([ for (var e in hoistedNamedVariables.entries) js_ast.Property(js.string(e.key), e.value), ]), ], ])..sourceInformation = originalCallSiteSourceLocation; // Cast the result of the checked call or the value returned from a // `NoSuchMethod` invocation. return js_ast.Binary( ',', letAssignments, js.call('# == # ? # : #', [ checkResult, _runtimeCall('validArgumentsSentinel'), _emitCast(validatedCallSite, expectedReturnType), _emitCast(checkResult, expectedReturnType), ]), ); } js_ast.Expression _emitJSObjectGetPrototypeOf( js_ast.Expression obj, { required bool fullyQualifiedName, }) => fullyQualifiedName ? _runtimeCall('global.Object.getPrototypeOf(#)', [obj]) : js.call('Object.getPrototypeOf(#)', obj); js_ast.Expression _emitJSObjectSetPrototypeOf( js_ast.Expression obj, js_ast.Expression prototype, { required bool fullyQualifiedName, }) => fullyQualifiedName ? _runtimeCall('global.Object.setPrototypeOf(#, #)', [obj, prototype]) : js.call('Object.setPrototypeOf(#, #)', [obj, prototype]); bool _isDebuggerCall(Procedure target) { return target.name.text == 'debugger' && target.enclosingLibrary.importUri.toString() == 'dart:developer'; } js_ast.Node _emitDebuggerCall(StaticInvocation node) { var args = node.arguments.named; var isStatement = node.parent is ExpressionStatement; var debuggerStatement = js_ast.DebuggerStatement().withSourceInformation( _nodeStart(node), ); if (args.isEmpty) { // Inline `debugger()` with no arguments, as a statement if possible, // otherwise as an immediately invoked function. return isStatement ? debuggerStatement : js.call('(() => { #; return true})()', [debuggerStatement]); } // The signature of `debugger()` is: // // bool debugger({bool when: true, String message}) // // This code path handles the named arguments `when` and/or `message`. // Both must be evaluated in the supplied order, and then `when` is used // to decide whether to break or not. // // We also need to return the value of `when`. var jsArgs = args.map(_emitNamedExpression).toList(); var when = args.length == 1 // For a single `when` argument, use it. // // For a single `message` argument, use `{message: ...}`, which // coerces to true (the default value of `when`). ? (args[0].name == 'when' ? jsArgs[0].value : js_ast.ObjectInitializer(jsArgs)) // If we have both `message` and `when` arguments, evaluate them in // order, then extract the `when` argument. : js.call('#.when', js_ast.ObjectInitializer(jsArgs)); return isStatement ? js.statement('if (#) #;', [when, debuggerStatement]) : js.call('# && (() => { #; return true })()', [ when, debuggerStatement, ]); } /// Emits the target of a [StaticInvocation], [StaticGet], or [StaticSet]. js_ast.PropertyAccess _emitStaticTarget(Member target) { var c = target.enclosingClass; if (c != null) { // A static native element should just forward directly to the JS type's // member, for example `Css.supports(...)` in dart:html should be replaced // by a direct call to the DOM API: `global.CSS.supports`. var isExternal = _isExternal(target); if (isExternal && (target as Procedure).isStatic) { var nativeName = _extensionTypes.getNativePeers(c); if (nativeName.isNotEmpty) { var annotationName = _annotationName(target, isJSName); var memberName = annotationName == null ? _emitStaticMemberName(target.name.text, target) : js.string(annotationName); return js_ast.PropertyAccess( _runtimeCall('global.#', [nativeName[0]]), memberName, ); } } return js_ast.PropertyAccess( _emitStaticClassName(c, isExternal), _emitStaticMemberName(target.name.text, target), ); } return _emitTopLevelName(target); } /// Returns all parts of [node] flattened into a list so they can be passed in /// the calling convention for calls with no runtime checks. /// /// When [types] is `false` any type arguments present in [node] will be /// omitted. /// /// Passing [target] when applicable allows for detection of an annotation to /// omit passing type parameters and to detect if positional arguments should /// be packaged to be passed to a JavaScript using an interop call. List _emitArgumentList( Arguments node, { bool types = true, Member? target, }) { types = types && _reifyGenericFunction(target); var ( :typeArguments, :positionalArguments, :namedArguments, ) = _emitArgumentGroups( node, isJSInterop: target != null && isJsMember(target), ); return [ if (types && typeArguments != null) ...typeArguments, ...?positionalArguments, if (namedArguments != null) js_ast.ObjectInitializer([...namedArguments]), ]; } /// Returns all arguments from [node] but kept in separate packets so they can /// be further processed. /// /// Facilitates passing arguments in the calling convention used by runtime /// helpers that check arguments. /// /// When [isJSInterop] is `true` the positional arguments are packaged to /// be passed to JavaScript via an interop call. _ArgumentGroups _emitArgumentGroups( Arguments node, { required bool isJSInterop, }) { var typeArguments = node.types.isEmpty ? null : [for (var typeArgument in node.types) _emitType(typeArgument)]; var positionalArguments = node.positional.isEmpty ? null : [ for (var arg in node.positional) if (arg is StaticInvocation && isJSSpreadInvocation(arg.target) && arg.arguments.positional.length == 1) js_ast.Spread(_visitExpression(arg.arguments.positional[0])) else if (isJSInterop) _visitExpression(_assertInterop(arg)) else _visitExpression(arg), ]; // Named arguments with JS interop are only allowed in object literal // constructors. Invocations to those are always transformed by // `_emitObjectLiteral` and therefore we should never expect to see a JS // interop invocation with named arguments here. if (node.named.isNotEmpty) assert(!isJSInterop); var namedArguments = node.named.isEmpty ? null : [for (var arg in node.named) _emitNamedExpression(arg)]; return ( typeArguments: typeArguments, positionalArguments: positionalArguments, namedArguments: namedArguments, ); } js_ast.Property _emitNamedExpression(NamedExpression arg) => js_ast.Property(_propertyName(arg.name), _visitExpression(arg.value)); /// Emits code for the `JS(...)` macro. js_ast.Node _emitInlineJSCode(StaticInvocation node) { var args = node.arguments.positional; // arg[0] is static return type, used in `RestrictedStaticTypeAnalyzer` var code = args[1]; List templateArgs; String source; if (code is ConstantExpression) { templateArgs = args.skip(2).toList(); source = (code.constant as StringConstant).value; } else if (code is StringConcatenation) { if (code.expressions.every((e) => e is StringLiteral)) { templateArgs = args.skip(2).toList(); source = code.expressions.map((e) => (e as StringLiteral).value).join(); } else { if (args.length > 2) { throw ArgumentError( "Can't mix template args and string interpolation in JS calls: " '`$node`', ); } templateArgs = []; source = code.expressions.map((expression) { if (expression is StringLiteral) { return expression.value; } else { templateArgs.add(expression); return '#'; } }).join(); } } else { templateArgs = args.skip(2).toList(); source = (code as StringLiteral).value; } // TODO(jmesserly): arguments to JS() that contain type literals evaluate to // the raw runtime type instead of the wrapped Type object. // We can clean this up by switching to `unwrapType()`, which // the compiler will then optimize. var wasInForeignJS = _isInForeignJS; _isInForeignJS = true; var jsArgs = templateArgs.map(_visitExpression).toList(); _isInForeignJS = wasInForeignJS; var result = js.parseForeignJS(source).instantiate(jsArgs); // Add a check to make sure any JS() values from a native type are typed // properly in sound null-safety. if (_isWebLibrary(_currentLibrary!.importUri)) { var type = node.getStaticType(_staticTypeContext); if (type.isPotentiallyNonNullable) { result = _runtimeCall('checkNativeNonNull(#)', [result]); } } assert( result is js_ast.Expression || result is js_ast.Statement && node.parent is ExpressionStatement, ); return result.withSourceInformation(_nodeStart(node)); } js_ast.Expression _emitEmbeddedGlobal(StaticInvocation node) { var constantExpression = node.arguments.positional[1] as ConstantExpression; var name = constantExpression.constant as StringConstant; var value = name.value; if (value == 'arrayRti') { // Special case for the rti on a JSArray. These are defined via the dartx // extension functionality. return _emitMemberName('arrayRti', memberClass: _jsArrayClass); } return _runtimeCall('#', [name.value]); } /// Returns the string literal that is to be used as the result of a call to /// [JS_GET_NAME] for [name]. js_ast.Expression _emitExpressionForJsGetName(JsGetName name) { switch (name) { case JsGetName.OPERATOR_IS_PREFIX: return js.string(js_ast.FixedNames.operatorIsPrefix); case JsGetName.SIGNATURE_NAME: return _runtimeCall('#', [ js.string(js_ast.FixedNames.operatorSignature), ]); case JsGetName.RTI_NAME: return js.string(js_ast.FixedNames.rtiName); case JsGetName.FUTURE_CLASS_TYPE_NAME: return js.string( _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.futureClass), ); case JsGetName.LIST_CLASS_TYPE_NAME: return js.string( _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.listClass), ); case JsGetName.RTI_FIELD_AS: return _emitMemberName( js_ast.FixedNames.rtiAsField, memberClass: _rtiClass, ); case JsGetName.RTI_FIELD_IS: return _emitMemberName( js_ast.FixedNames.rtiIsField, memberClass: _rtiClass, ); default: throw UnsupportedError('JsGetName has no name for "$name".'); } } /// Returns the expression that is to be used as the result of a call to /// [JS_BUILTIN] for [builtin]. js_ast.Expression _emitOperationForJsBuiltIn(JsBuiltin builtin) { switch (builtin) { case JsBuiltin.dartClosureConstructor: // TODO(48585) Is this safe or will it conflict with functions that // enter the program through JS Interop? return js.call('Function'); case JsBuiltin.dartObjectConstructor: return _emitTopLevelName(_coreTypes.objectClass); default: throw UnsupportedError('JsBuiltin has no operation for "$builtin".'); } } String _enumValueName(Field field) { var enumName = field.enclosingClass!.name; var valueName = field.name.text; return '$enumName.$valueName'; } JsGetName _asJsGetName(Field field) => JsGetName.values.firstWhere( (val) => val.toString() == _enumValueName(field), ); JsBuiltin _asJsBuiltin(Field field) => JsBuiltin.values.firstWhere( (val) => val.toString() == _enumValueName(field), ); bool _isWebLibrary(Uri importUri) => importUri.isScheme('dart') && (importUri.path == 'html' || importUri.path == 'svg' || importUri.path == 'indexed_db' || importUri.path == 'web_audio' || importUri.path == 'web_gl' || importUri.path == 'web_sql' || importUri.path == 'html_common'); bool _isNull(Expression expr) => expr is NullLiteral || expr.getStaticType(_staticTypeContext).extensionTypeErasure is NullType; bool _doubleEqIsIdentity(Expression left, Expression right) { // If we statically know LHS or RHS is null we can use ==. if (_isNull(left) || _isNull(right)) return true; // If the representation of the two types will not induce conversion in // JS then we can use == . return !_typeRep.equalityMayConvert( left.getStaticType(_staticTypeContext), right.getStaticType(_staticTypeContext), ); } bool _tripleEqIsIdentity(Expression left, Expression right) { // If either is non-nullable, then we don't need to worry about // equating null and undefined, and so we can use triple equals. return !_isNullable(left) || !_isNullable(right); } /// Returns true if [expr] can be null. bool _isNullable(Expression expr) => _nullableInference.isNullable(expr); js_ast.Expression _emitJSDoubleEq( List args, { bool negated = false, }) { var op = negated ? '# != #' : '# == #'; return js.call(op, args); } js_ast.Expression _emitJSTripleEq( List args, { bool negated = false, }) { var op = negated ? '# !== #' : '# === #'; return js.call(op, args); } js_ast.Expression _emitCoreIdenticalCall( List args, { bool negated = false, }) { if (args.length != 2) { // Shouldn't happen in typechecked code return _runtimeCall( 'throw(Error("compile error: calls to `identical` require 2 args")', ); } var left = args[0]; var right = args[1]; var jsArgs = [_visitExpression(left), _visitExpression(right)]; if (_tripleEqIsIdentity(left, right)) { return _emitJSTripleEq(jsArgs, negated: negated); } if (_doubleEqIsIdentity(left, right)) { return _emitJSDoubleEq(jsArgs, negated: negated); } var code = negated ? '!#' : '#'; return js.call( code, js_ast.Call(_emitTopLevelName(_coreTypes.identicalProcedure), jsArgs), ); } /// Returns true if this [member] is a JS interop member. bool isJSInteropMember(Member member) => member.isExternal && hasJSInteropAnnotation(member.enclosingClass!); @override js_ast.Expression visitConstructorInvocation(ConstructorInvocation node) { var ctor = node.target; var ctorClass = ctor.enclosingClass; var args = node.arguments; if (isJSAnonymousType(ctorClass)) { var namedProperties = { for (final named in node.arguments.named) named.name: named.value, }; return _emitObjectLiteral( namedProperties, isDartJsInterop: hasDartJSInteropAnnotation(ctorClass), ); } // JS interop constructor calls do not provide an RTI at the call site. var shouldProvideRti = !isJSInteropMember(ctor) && _requiresRtiForInstantiation(ctorClass); var rti = shouldProvideRti ? _emitType( node.constructedType, emitJSInteropGenericClassTypeParametersAsAny: false, ) : null; var result = js_ast.New(_emitConstructorName(node.constructedType, ctor), [ ?rti, ..._emitArgumentList(args, types: false, target: ctor), ]); return node.isConst ? _canonicalizeConstObject(result) : result; } js_ast.Expression _emitFactoryInvocation(StaticInvocation node) { var args = node.arguments; var ctor = node.target; var ctorClass = ctor.enclosingClass!; // JS interop constructor calls do not require an RTI at the call site. if (isJSInteropMember(ctor)) { return _emitJSInteropNew(ctor, args); } var type = ctorClass.typeParameters.isEmpty ? _coreTypes.nonNullableRawType(ctorClass) : InterfaceType(ctorClass, Nullability.nonNullable, args.types); if (isFromEnvironmentInvocation(_coreTypes, node)) { var value = _constants.evaluate(node); if (value is PrimitiveConstant) { return visitConstant(value); } } if (args.positional.isEmpty && args.named.isEmpty && ctorClass.enclosingLibrary.importUri.isScheme('dart')) { // Skip the slow SDK factory constructors when possible. switch (ctorClass.name) { case 'Map': case 'HashMap': case 'LinkedHashMap': if (ctor.name.text == '') { var mapType = _createMapImplType(type); var mapClass = _emitClassRef(mapType); var rti = _emitType(mapType); return js.call('new #.new(#)', [mapClass, rti]); } else if (ctor.name.text == 'identity') { var mapType = _createMapImplType(type, identity: true); var mapClass = _emitClassRef(mapType); var rti = _emitType(mapType); return js.call('new #.new(#)', [mapClass, rti]); } case 'Set': case 'HashSet': case 'LinkedHashSet': if (ctor.name.text == '') { var setType = _createSetImplType(type); var setClass = _emitClassRef(setType); var rti = _emitType(setType); return js.call('new #.new(#)', [setClass, rti]); } else if (ctor.name.text == 'identity') { var setType = _createSetImplType(type, identity: true); var setClass = _emitClassRef(setType); var rti = _emitType(setType); return js.call('new #.new(#)', [setClass, rti]); } case 'List': if (ctor.name.text == '') { return _emitList(type.typeArguments[0], []); } } } var rti = _requiresRtiForInstantiation(ctorClass) ? _emitType(type, emitJSInteropGenericClassTypeParametersAsAny: false) : null; var result = js_ast.Call(_emitConstructorName(type, ctor), [ ?rti, ..._emitArgumentList(args, types: false), ]); return node.isConst ? _canonicalizeConstObject(result) : result; } js_ast.Expression _emitJSInteropNew(Member ctor, Arguments args) { var ctorClass = ctor.enclosingClass!; if (isJSAnonymousType(ctorClass)) { var namedProperties = { for (final named in args.named) named.name: named.value, }; return _emitObjectLiteral( namedProperties, isDartJsInterop: hasDartJSInteropAnnotation(ctorClass), ); } // JS interop constructor calls do not require an RTI at the call site. return js_ast.New( _emitConstructorName(_coreTypes.nonNullableRawType(ctorClass), ctor), _emitArgumentList(args, types: false, target: ctor), ); } InterfaceType _createMapImplType(InterfaceType type, {bool? identity}) { var typeArgs = type.typeArguments; if (typeArgs.isEmpty) { return type.withDeclaredNullability(Nullability.nonNullable); } identity ??= _typeRep.isPrimitive(typeArgs[0]); var c = identity ? _identityHashMapImplClass : _linkedHashMapImplClass; return InterfaceType(c, Nullability.nonNullable, typeArgs); } InterfaceType _createSetImplType(InterfaceType type, {bool? identity}) { var typeArgs = type.typeArguments; if (typeArgs.isEmpty) { return type.withDeclaredNullability(Nullability.nonNullable); } identity ??= _typeRep.isPrimitive(typeArgs[0]); var c = identity ? _identityHashSetImplClass : _linkedHashSetImplClass; return InterfaceType(c, Nullability.nonNullable, typeArgs); } /// Given a mapping [namedProperties] between names and their associated /// expressions, returns an object literal with the same names mapped to the /// transformed expressions. /// /// This is used for object literal constructors in JS interop. /// /// If [isDartJsInterop] is true, it implies that this is an object literal /// constructor using `dart:js_interop`. If false, this method wraps the /// expressions with an [_assertInterop] call. js_ast.Expression _emitObjectLiteral( Map namedProperties, { required bool isDartJsInterop, }) { if (namedProperties.isEmpty) return js.call('{}'); var properties = []; for (var name in namedProperties.keys) { // `assertInterop` is not needed for `dart:js_interop`. It statically // disallows `Function`s from being passed unless they were explicitly // passed as an opaque reference, in which case, we shouldn't require the // user to wrap them anyways. var value = isDartJsInterop ? namedProperties[name]! : _assertInterop(namedProperties[name]!); properties.add( js_ast.Property(_propertyName(name), _visitExpression(value)), ); } return js_ast.ObjectInitializer([...properties]); } @override js_ast.Expression visitNot(Not node) { var operand = node.operand; if (operand is EqualsCall) { return _emitEqualityOperator( operand.left, operand.interfaceTarget, operand.right, negated: true, ); } else if (operand is EqualsNull) { return _emitCoreIdenticalCall([ operand.expression, NullLiteral(), ], negated: true); } else if (operand is StaticInvocation && operand.target == _coreTypes.identicalProcedure) { return _emitCoreIdenticalCall( operand.arguments.positional, negated: true, ); } var jsOperand = _visitTest(operand); if (jsOperand is js_ast.LiteralBool) { // Flipping the value here for `!true` or `!false` allows for simpler // `if (true)` or `if (false)` detection and optimization. return js_ast.LiteralBool( !jsOperand.value, ).withSourceInformation(jsOperand.sourceInformation) as js_ast.LiteralBool; } // Logical negation, `!e`, is a boolean conversion context since it is // defined as `e ? false : true`. return js.call('!#', jsOperand).withSourceInformation(continueSourceMap); } @override js_ast.Expression visitNullCheck(NullCheck node) { var expr = node.operand; var jsExpr = _visitExpression(expr); // If the expression is non-nullable already, this is a no-op. return _isNullable(expr) ? _runtimeCall('nullCheck(#)', [jsExpr]) : jsExpr; } @override js_ast.Expression visitLogicalExpression(LogicalExpression node) { // The operands of logical boolean operators are subject to boolean // conversion. return _visitTest(node); } @override js_ast.Expression visitConditionalExpression(ConditionalExpression node) { var condition = _visitTest(node.condition); if (condition is js_ast.LiteralBool) { if (condition.value) { // Avoid emitting conditional when one branch is effectively dead code. // ex: `true ? foo : bar` -> `foo` return _visitExpression(node.then); } else { // ex: `false ? foo : bar` -> `bar` return _visitExpression(node.otherwise); } } var then = _visitExpression(node.then); var otherwise = _visitExpression(node.otherwise); return js.call('# ? # : #', [condition, then, otherwise]) ..sourceInformation = condition.sourceInformation ?? _nodeStart(node.condition); } @override js_ast.Expression visitStringConcatenation(StringConcatenation node) { var parts = []; for (var e in node.expressions) { var jsExpr = _visitExpression(e); if (jsExpr is js_ast.LiteralString && jsExpr.valueWithoutQuotes.isEmpty) { continue; } var type = e.getStaticType(_staticTypeContext).extensionTypeErasure; if (DartTypeEquivalence( _coreTypes, ignoreTopLevelNullability: true, ).areEqual(type, _coreTypes.stringNonNullableRawType) && !_isNullable(e)) { parts.add(jsExpr); } else if (_shouldCallObjectMemberHelper(e)) { parts.add(_runtimeCall('str(#)', [jsExpr])); } else { // It is safe to call a version of `str()` that does not probe for the // toString method before calling it. parts.add(_runtimeCall('strSafe(#)', [jsExpr])); } } if (parts.isEmpty) return js.string(''); return js_ast.Expression.binary(parts, '+'); } @override js_ast.Expression visitConstructorTearOff(ConstructorTearOff node) { throw UnsupportedError('Constructor tear off'); } @override js_ast.Expression visitRedirectingFactoryTearOff( RedirectingFactoryTearOff node, ) { throw UnsupportedError('RedirectingFactory tear off'); } @override js_ast.Expression visitTypedefTearOff(TypedefTearOff node) { throw UnsupportedError('Typedef instantiation'); } @override js_ast.Expression visitIsExpression(IsExpression node) { return _emitIsExpression(node.operand, node.type.extensionTypeErasure); } js_ast.Expression _emitIsExpression(Expression operand, DartType type) { // Generate `is` as `dart.is` or `typeof` depending on the RHS type. var lhs = _visitExpression(operand); // It is invalid to use a simplified check for a native type in place of // a type test for a `TypeParameterType`. This is because at runtime type // parameters can be instantiated as the bottom type `Never` and // `val is Never` should always evaluate to false. var typeofName = type is TypeParameterType || type is StructuralParameterType ? null : _typeRep.typeFor(type).primitiveTypeOf; // Inline non-nullable primitive types other than int (which requires a // Math.floor check). if (typeofName != null && type.nullability == Nullability.nonNullable && type != _types.coreTypes.intNonNullableRawType) { return js.call('typeof # == #', [lhs, js.string(typeofName, "'")]); } return js.call('#.#(#)', [ _emitType(type), _emitMemberName(js_ast.FixedNames.rtiIsField, memberClass: _rtiClass), lhs, ]); } @override js_ast.Expression visitAsExpression(AsExpression node) { var fromExpr = node.operand; var jsFrom = _visitExpression(fromExpr); if (node.isUnchecked) return jsFrom; return _emitCast( jsFrom, node.type, fromStaticType: fromExpr.getStaticType(_staticTypeContext), isTypeError: node.isTypeError, ); } js_ast.Expression _emitCast( js_ast.Expression value, DartType toType, { DartType? fromStaticType, bool isTypeError = false, }) { toType = toType.extensionTypeErasure; if (_types.isTop(toType)) return value; if (fromStaticType != null) { fromStaticType = fromStaticType.extensionTypeErasure; // If the check was put here by static analysis to ensure soundness, we // can't skip it. For example, one could implement covariant generic // caller side checks like this: // // typedef F(T t); // class C { // F f; // add(T t) { // // required check `t as T` // } // } // main() { // C c = new C()..f = (int x) => x.isEven; // c.f('hi'); // required check `c.f as F` // c.add('hi); // } // if (!isTypeError && _types.isSubtypeOf(fromStaticType, toType)) { return value; } if (!isTypeError && _mustBeNonNullable(toType) && DartTypeEquivalence( _coreTypes, ignoreTopLevelNullability: true, ).areEqual(fromStaticType, toType)) { // If the underlying type is the same, we only need a null check. return _runtimeCall('nullCast(#, #)', [value, _emitType(toType)]); } // All Dart number types map to a JavaScript Number. We can specialize // these cases. if (_typeRep.isNumber(fromStaticType) && _typeRep.isNumber(toType)) { // If `toType` is some form of `num`, it should have been filtered // above. if (toType == _coreTypes.doubleNullableRawType) { // Any number/nullability -> double? : no-op return value; } if (toType == _coreTypes.doubleNonNullableRawType) { if (fromStaticType.nullability == Nullability.nonNullable) { // Any non-nullable number -> double : no-op return value; } // Any number/nullability -> double : null check return _runtimeCall('nullCast(#, #)', [value, _emitType(toType)]); } } } var directMethod = _directCastMethod(toType); if (directMethod != null) { return js.call('#(#)', [_emitTopLevelName(directMethod), value]); } return js.call('#.#(#)', [ _emitType(toType), _emitMemberName(js_ast.FixedNames.rtiAsField, memberClass: _rtiClass), value, ]); } /// Returns the direct `_as` method when [type] is a primitive type otherwise, /// `null`. Member? _directCastMethod(DartType type) { if (type is InterfaceType && type.typeArguments.isEmpty) { if (type.nullability == Nullability.nonNullable) { if (type == _types.coreTypes.boolNonNullableRawType) return _asBool; if (type == _types.coreTypes.doubleNonNullableRawType) return _asDouble; if (type == _types.coreTypes.intNonNullableRawType) return _asInt; if (type == _types.coreTypes.numNonNullableRawType) return _asNum; if (type == _types.coreTypes.objectNonNullableRawType) return _asObject; if (type == _types.coreTypes.stringNonNullableRawType) return _asString; } else if (type.nullability == Nullability.nullable) { if (type == _types.coreTypes.boolNullableRawType) return _asBoolQ; if (type == _types.coreTypes.doubleNullableRawType) return _asDoubleQ; if (type == _types.coreTypes.intNullableRawType) return _asIntQ; if (type == _types.coreTypes.numNullableRawType) return _asNumQ; if (type == _types.coreTypes.stringNullableRawType) return _asStringQ; } } return null; } @override js_ast.Expression visitSymbolLiteral(SymbolLiteral node) => _emitDartSymbol(node.value); @override js_ast.Expression visitTypeLiteral(TypeLiteral node) => _emitTypeLiteral(node.type); js_ast.Expression _emitTypeLiteral(DartType type) { var typeRep = _emitType(type); // TODO(46002) All `JS()` calls in the SDK should be explicit when using the // internal rti object by calling the `TYPE_REF` helper. if (_isInForeignJS) return typeRep; // If the type is a type literal expression in Dart code, wrap the raw // runtime type in a "Type" instance. return js.call('#.createRuntimeType(#)', [ _emitLibraryName(_rtiLibrary), typeRep, ]); } @override js_ast.Expression visitThisExpression(ThisExpression node) => js_ast.This(); @override js_ast.Expression visitRethrow(Rethrow node) { return _runtimeCall('rethrow(#)', [_emitVariableRef(_rethrowParameter!)]); } @override js_ast.Expression visitThrow(Throw node) => _runtimeCall('throw(#)', [_visitExpression(node.expression)]); @override js_ast.Expression visitListLiteral(ListLiteral node) { var elementType = node.typeArgument; var elements = _visitExpressionList(node.expressions); return _emitList(elementType, elements); } js_ast.Expression _emitList( DartType itemType, List items, ) { var list = js_ast.ArrayInitializer(items); // List's type parameter is default-initialized to dynamic in our runtime. if (itemType == const DynamicType()) return list; // Call `new JSArray.of(list)` var type = InterfaceType(_jsArrayClass, Nullability.nonNullable, [ itemType, ]); var arrayClass = _emitClassRef(type); var arrayRti = _emitType(type); return js.call('#.of(#, #)', [arrayClass, arrayRti, list]); } js_ast.Expression _emitConstList( DartType elementType, List elements, ) { return _runtimeCall('constList(#, [#])', [ _emitType(elementType), elements, ]); } @override js_ast.Expression visitSetLiteral(SetLiteral node) { // TODO(markzipan): remove const check when we use front-end const eval if (!node.isConst) { var type = InterfaceType(_linkedHashSetClass, Nullability.nonNullable, [ node.typeArgument, ]); var setClass = _emitClassRef(type); var rti = _emitType(type); if (node.expressions.isEmpty) { return js.call('#.new(#)', [setClass, rti]); } return js.call('#.from(#, [#])', [ setClass, rti, _visitExpressionList(node.expressions), ]); } return _emitConstSet( node.typeArgument, _visitExpressionList(node.expressions), ); } js_ast.Expression _emitConstSet( DartType elementType, List elements, ) { return _runtimeCall('constSet(#, [#])', [_emitType(elementType), elements]); } @override js_ast.Expression visitMapLiteral(MapLiteral node) { var entries = [ for (var e in node.entries) ...[ _visitExpression(e.key), _visitExpression(e.value), ], ]; // TODO(markzipan): remove const check when we use front-end const eval if (!node.isConst) { var type = node.getStaticType(_staticTypeContext) as InterfaceType; var mapType = _createMapImplType(type); var mapClass = _emitClassRef(mapType); var rti = _emitType(mapType); if (node.entries.isEmpty) { return js.call('new #.new(#)', [mapClass, rti]); } return js.call('new #.from(#, [#])', [mapClass, rti, entries]); } return _emitConstMap(node.keyType, node.valueType, entries); } js_ast.Expression _emitConstMap( DartType keyType, DartType valueType, List entries, ) { return _runtimeCall('constMap(#, #, [#])', [ _emitType(keyType), _emitType(valueType), entries, ]); } /// Returns the key used for shape lookup at runtime. /// /// See `shapes` in dart:_runtime (records.dart) for a description. String _recordShapeKey( int positionalElementCount, Iterable namedElementNames, ) { var elementCount = positionalElementCount + namedElementNames.length; return '$elementCount;${namedElementNames.join(',')}'; } @override js_ast.Expression visitRecordLiteral(RecordLiteral node) { var names = node.named.map((element) => element.name); var positionalElementCount = node.positional.length; var shapeKey = _recordShapeKey(positionalElementCount, names); var shapeExpr = _runtimeCall('recordLiteral(#, #, #, [#])', [ js.string(shapeKey), js.number(positionalElementCount), names.isEmpty ? js.call('void 0') : js.stringArray(names), [ for (var positional in node.positional) _visitExpression(positional), for (var named in node.named) _visitExpression(named.value), ], ]); return shapeExpr; } @override js_ast.Expression visitAwaitExpression(AwaitExpression node) { var expression = _visitExpression(node.operand); var type = node.runtimeCheckType; if (type != null) { // When an expected runtime type is present there is a possible soundness // issue with the static types. The type of the await expression must be // checked at runtime to ensure soundness. var expectedType = _emitType(type); var asyncLibrary = _emitLibraryName(_coreTypes.asyncLibrary); expression = js.call('#.awaitWithTypeCheck(#, #)', [ asyncLibrary, expectedType, expression, ]); } return js_ast.Await(expression); } @override js_ast.Expression visitFunctionExpression(FunctionExpression node) { var savedInFunctionExpression = _inFunctionExpression; _inFunctionExpression = true; var fn = _emitArrowFunction(node); _inFunctionExpression = savedInFunctionExpression; if (!_reifyFunctionType(node.function)) return fn; return _emitFunctionTagged( fn, node.getStaticType(_staticTypeContext) as FunctionType, ); } js_ast.ArrowFun _emitArrowFunction(FunctionExpression node) { var f = _emitFunction(node.function, null); js_ast.Node body = f.body; // Simplify `=> { return e; }` to `=> e` if (body is js_ast.Block) { var block = body; if (block.statements.length == 1) { var s = block.statements.single; if (s is js_ast.Block) { block = s; if (block.statements.length == 1) s = block.statements.single; } if (s is js_ast.Return && s.value != null) body = s.value!; } } // Convert `function(...) { ... }` to `(...) => ...` // This is for readability, but it also ensures correct `this` binding. return js_ast.ArrowFun(f.params, body); } @override js_ast.Expression visitStringLiteral(StringLiteral node) => js.escapedString(node.value, '"'); @override js_ast.Expression visitIntLiteral(IntLiteral node) => js.uint64(node.value); @override js_ast.Expression visitDoubleLiteral(DoubleLiteral node) => js.number(node.value); @override js_ast.Expression visitBoolLiteral(BoolLiteral node) => js_ast.LiteralBool(node.value); @override js_ast.Expression visitNullLiteral(NullLiteral node) => js_ast.LiteralNull(); @override js_ast.Expression visitLet(Let node) { var v = node.variable; var init = _visitExpression(v.initializer!); var body = _visitExpression(node.body); var temp = _tempVariables.remove(v); // TODO(eernst): Remove the following `if` if anonymous-methods is rejected. // Otherwise, revise this method to be more readable. // See https://github.com/dart-lang/language/issues/260. if (temp == null && !_isTemporaryVariable(v)) { temp = _emitVariableRef(v); } if (temp != null) { if (_letVariables != null) { init = js_ast.Assignment(temp, init); _letVariables!.add(temp); } else { // TODO(jmesserly): make sure this doesn't happen on any performance // critical call path. // // Annotations on a top-level, non-lazy function type should be the only // remaining use. var arrowFunction = js_ast.ArrowFun([temp], body); final asyncAnalysis = PreTranslationAnalysis((node) { throw UnsupportedError('Unknown node in block expression: $node'); }, arrowFunction)..analyze(); final isAsyncIife = asyncAnalysis.hasAwaitOrYield.contains(body); if (isAsyncIife) { final transformedFunction = _rewriteAsyncFunction( js_ast.Fun([temp], js_ast.Block([js_ast.Return(body)])), AsyncMarker.Async, null, node.getStaticType(_staticTypeContext), functionBody: _toSourceLocation(node.fileOffset), functionEnd: _toSourceLocation(node.fileOffset), ); arrowFunction = js_ast.ArrowFun([temp], transformedFunction.body); } final call = js_ast.Call(arrowFunction, [init]); return isAsyncIife ? js_ast.Await(call) : call; } } return js_ast.Binary(',', init, body); } @override js_ast.Expression visitBlockExpression(BlockExpression node) { var jsExpr = _visitExpression(node.value); var jsStmts = [ for (var s in node.body.statements) _visitStatement(s), js_ast.Return(jsExpr), ]; final statementBlock = js_ast.Block(jsStmts); var arrowFunction = js_ast.ArrowFun(const [], statementBlock); final asyncAnalysis = PreTranslationAnalysis((node) { throw UnsupportedError( 'Unknown node in block expression: $node (${node.runtimeType}, ' '${node.sourceInformation})', ); }, arrowFunction)..analyze(); final isAsyncIife = asyncAnalysis.hasAwaitOrYield.contains(statementBlock); if (isAsyncIife) { final transformedFunction = _rewriteAsyncFunction( js_ast.Fun(const [], statementBlock), AsyncMarker.Async, null, node.getStaticType(_staticTypeContext), functionBody: _toSourceLocation(node.fileOffset), functionEnd: _toSourceLocation(node.fileOffset), ); arrowFunction = js_ast.ArrowFun(const [], transformedFunction.body); } final call = js_ast.Call(arrowFunction, const []); return isAsyncIife ? js_ast.Await(call) : call; } @override js_ast.Expression visitInstantiation(Instantiation node) { return _runtimeCall('gbind(#, #)', [ _visitExpression(node.expression), node.typeArguments.map(_emitType).toList(), ]); } @override js_ast.Expression visitLoadLibrary(LoadLibrary node) => _runtimeCall('loadLibrary(#, #, #)', [ js.string(node.import.enclosingLibrary.importUri.toString()), js.string(node.import.name!), js.string( _libraryToModule(node.import.targetLibrary, throwIfNotFound: false), ), ]); // TODO(jmesserly): DDC loads all libraries eagerly. // See // https://github.com/dart-lang/sdk/issues/27776 // https://github.com/dart-lang/sdk/issues/27777 @override js_ast.Expression visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) => _runtimeCall('checkDeferredIsLoaded(#, #)', [ js.string(node.import.enclosingLibrary.importUri.toString()), js.string(node.import.name!), ]); bool _reifyFunctionType(FunctionNode f) { var parent = f.parent; if (parent is FunctionDeclaration && (isLateLoweredLocalGetter(parent.variable) || isLateLoweredLocalSetter(parent.variable))) { // Late local variables are lowered to local get and set functions. // These functions should never need to be tagged with their types. return false; } if (!_currentLibrary!.importUri.isScheme('dart')) return true; // SDK libraries can skip reification if they request it. bool reifyFunctionTypes(Expression a) => isBuiltinAnnotation(a, '_js_helper', 'ReifyFunctionTypes'); while (parent != null) { var a = findAnnotation(parent, reifyFunctionTypes); if (a != null) { var value = _constants.getFieldValueFromAnnotation(a, 'value'); if (value is bool) return value; } parent = parent.parent; } return true; } bool _reifyTearoff(Member member) { return member is Procedure && !member.isAccessor && !member.isFactory && !(_isInForeignJS && _isBuildingSdk) && !usesJSInterop(member) && _reifyFunctionType(member.function); } /// Returns the name value of the `JSExportName` annotation (when compiling /// the SDK), or `null` if there's none. This is used to control the name /// under which functions are compiled and exported. String? _jsExportName(NamedNode n) { var library = getLibrary(n); if (!library.importUri.isScheme('dart')) return null; return _annotationName(n, isJSExportNameAnnotation); } /// If [node] has annotation matching [test] and the first argument is a /// string, this returns the string value. /// /// Calls [findAnnotation] followed by [getNameFromAnnotation]. String? _annotationName(NamedNode node, bool Function(Expression) test) { var annotation = findAnnotation(node, test); return annotation != null ? _constants.getFieldValueFromAnnotation(annotation, 'name') as String? : null; } @override js_ast.Expression visitConstant(Constant node) { if (node is StaticTearOffConstant) { // JS() or external JS consts should not be lazily loaded. var isSdk = node.target.enclosingLibrary.importUri.isScheme('dart'); if (_isInForeignJS) { return _emitStaticTarget(node.target); } if (node.target.isExternal && !isSdk) { return _runtimeCall('tearoffInterop(#, #)', [ _emitStaticTarget(node.target), js.boolean(_isNullCheckableJsInterop(node.target)), ]); } } if (node is TypeLiteralConstant) { // We bypass the use of constants, since types are already canonicalized // in the DDC output. return _emitTypeLiteral(node.type); } if (node is PrimitiveConstant) { return super.visitConstant(node); } // Avoid caching constants during evaluation while scoping issues remain. // See: #44713 if (_constTableCache.incrementalMode) { return super.visitConstant(node); } var constAlias = _constAliasCache[node]; if (constAlias != null) { return constAlias; } var constAliasString = 'C${_constAliasCache.length}'; var constAliasProperty = _propertyName(constAliasString); _constTableCache[constAliasString] = js.call('void 0'); var constAliasAccessor = _constTableCache.access(constAliasString); var constAccessor = js.call('# || #.#', [ constAliasAccessor, _constTable, constAliasProperty, ]); _constAliasCache[node] = constAccessor; var constJs = super.visitConstant(node); var func = js_ast.Fun( [], js_ast.Block([ js.statement('return # = #;', [constAliasAccessor, constJs]), ]), ); var accessor = js_ast.Method(constAliasProperty, func, isGetter: true); _constLazyAccessors.add(accessor); return constAccessor; } @override js_ast.Expression visitNullConstant(NullConstant node) => js_ast.LiteralNull(); @override js_ast.Expression visitBoolConstant(BoolConstant node) => js.boolean(node.value); @override js_ast.Expression visitIntConstant(IntConstant node) => js.number(node.value); @override js_ast.Expression visitDoubleConstant(DoubleConstant node) { var value = node.value; // Emit the constant as an integer, if possible. if (value.isFinite) { var intValue = value.toInt(); const minInt32 = -0x80000000; const maxInt32 = 0x7FFFFFFF; if (intValue.toDouble() == value && intValue >= minInt32 && intValue <= maxInt32) { return js.number(intValue); } } if (value.isInfinite) { if (value.isNegative) { return js.call('-1 / 0'); } return js.call('1 / 0'); } if (value.isNaN) { return js.call('0 / 0'); } return js.number(value); } @override js_ast.Expression visitStringConstant(StringConstant node) => js.escapedString(node.value, '"'); // DDC does not currently use the non-primitive constant nodes; rather these // are emitted via their normal expression nodes. @override js_ast.Expression defaultConstant(Constant node) => _emitInvalidNode(node); @override js_ast.Expression visitSymbolConstant(SymbolConstant node) => _emitDartSymbol(node.name, library: node.libraryReference?.asLibrary); @override js_ast.Expression visitMapConstant(MapConstant node) { var entries = [ for (var e in node.entries) ...[ visitConstant(e.key), visitConstant(e.value), ], ]; return _emitConstMap(node.keyType, node.valueType, entries); } @override js_ast.Expression visitListConstant(ListConstant node) => _emitConstList( node.typeArgument, node.entries.map(visitConstant).toList(), ); @override js_ast.Expression visitSetConstant(SetConstant node) => _emitConstSet( node.typeArgument, node.entries.map(visitConstant).toList(), ); @override js_ast.Expression visitRecordConstant(RecordConstant node) { // RecordConstant names are already sorted alphabetically in kernel. var names = node.named.keys; var positionalElementCount = node.positional.length; var shapeKey = _recordShapeKey(positionalElementCount, names); return _runtimeCall('recordLiteral(#, #, #, [#])', [ js.string(shapeKey), js.number(positionalElementCount), names.isEmpty ? js.call('void 0') : js.stringArray(names), [ ...node.positional.map(visitConstant), ...node.named.values.map(visitConstant), ], ]); } js_ast.Expression visitEnum(InstanceConstant node) { var type = node.getType(_staticTypeContext); assert( type.nullability == Nullability.nonNullable, 'An instance constant should only ever have a non-nullable type.', ); var classRef = _emitClassRef(type as InterfaceType); var prototype = js.call('#.prototype', [classRef]); var enumAccessor = _emitTopLevelName(node.classNode); // Enums are canonicalized based on their 'name' member alone. We // append other members (such as 'index' and those introduced via enhanced // enums) after canonicalization so they can be updated across hot reloads. var constantProperties = []; var additionalProperties = []; if (type.typeArguments.isNotEmpty) { // Generic interface type instances require a type information tag. var property = js_ast.Property( _propertyName(js_ast.FixedNames.rtiName), _emitType(type), ); constantProperties.add(property); } node.fieldValues.forEach((k, v) { var constant = visitConstant(v); var member = k.asField; var memberClass = member.enclosingClass!; if (!memberClass.isEnum) { if (member.name.text == 'index') { // We transform the 'index' field of Enum fields into a special // getter so that their indices are consistent across hot reloads. var value = js.call('#.values.indexOf(this)', enumAccessor); var jsMember = _getSymbol( _emitClassPrivateNameSymbol( memberClass.enclosingLibrary, getLocalClassName(memberClass), member, ), ); additionalProperties.add( js_ast.Method( jsMember, js.fun('function() { return #; }', [value]), isGetter: true, ), ); } else { var jsMember = _getSymbol( _emitClassPrivateNameSymbol( memberClass.enclosingLibrary, getLocalClassName(memberClass), member, ), ); constantProperties.add(js_ast.Property(jsMember, constant)); } } else { additionalProperties.add( js_ast.Property( _emitMemberName(member.name.text, member: member), constant, ), ); } }); var canonicalizedEnum = _canonicalizeConstObject( _emitJSObjectSetPrototypeOf( js_ast.ObjectInitializer(constantProperties, multiline: true), prototype, fullyQualifiedName: false, ), ); var enumExtension = _runtimeStatement('extendEnum(#, #)', [ canonicalizedEnum, js_ast.ObjectInitializer(additionalProperties, multiline: true), ]); _enumExtensions.add(enumExtension); return canonicalizedEnum; } @override js_ast.Expression visitInstanceConstant(InstanceConstant node) { var savedTypeEnvironment = _currentTypeEnvironment; if (node.classNode.typeParameters.isNotEmpty) { _currentTypeEnvironment = ClassTypeEnvironment( node.classNode.typeParameters, ); } if (node.classNode.isEnum) { var constant = visitEnum(node); _currentTypeEnvironment = savedTypeEnvironment; return constant; } js_ast.Property entryToProperty(MapEntry entry) { var constant = visitConstant(entry.value); var member = entry.key.asField; var cls = member.enclosingClass!; var symbol = _getSymbol( _emitClassPrivateNameSymbol( cls.enclosingLibrary, getLocalClassName(cls), member, ), ); return js_ast.Property(symbol, constant); } var type = node.getType(_staticTypeContext); assert( type.nullability == Nullability.nonNullable, 'An instance constant should only ever have a non-nullable type.', ); var classRef = _emitClassRef(type as InterfaceType); var prototype = js.call('#.prototype', [classRef]); var properties = [ if (type.typeArguments.isNotEmpty) // Generic interface type instances require a type information tag. js_ast.Property( _propertyName(js_ast.FixedNames.rtiName), _emitType(type), ), for (var e in node.fieldValues.entries.toList().reversed) entryToProperty(e), ]; var constant = _canonicalizeConstObject( _emitJSObjectSetPrototypeOf( js_ast.ObjectInitializer(properties, multiline: true), prototype, fullyQualifiedName: false, ), ); _currentTypeEnvironment = savedTypeEnvironment; return constant; } /// Emits a private name JS Symbol for [member] unique to a Dart class /// [className]. /// /// This is now required for fields of constant objects that may be overridden /// within the same library. js_ast.ScopedId _emitClassPrivateNameSymbol( Library library, String className, Member member, [ js_ast.ScopedId? id, ]) { var name = '$className.${member.name.text}'; // 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 _emitPrivateNameSymbol(library, name, id); } @override js_ast.Expression visitStaticTearOffConstant(StaticTearOffConstant node) { return _emitStaticGet(node.target); } @override js_ast.Expression visitTypeLiteralConstant(TypeLiteralConstant node) => _emitTypeLiteral(node.type); @override js_ast.Expression visitInstantiationConstant(InstantiationConstant node) => _canonicalizeConstObject( _runtimeCall('gbind(#, #)', [ visitConstant(node.tearOffConstant), node.types.map(_emitType).toList(), ]), ); @override js_ast.Expression visitUnevaluatedConstant(UnevaluatedConstant node) => throw UnsupportedError('Encountered an unevaluated constant: $node'); @override js_ast.Expression visitFunctionTearOff(FunctionTearOff node) { var receiver = node.receiver; var receiverType = receiver.getStaticType(_staticTypeContext); var jsReceiver = _visitExpression(receiver); if (receiverType is InterfaceType && receiverType.classNode == _coreTypes.functionClass) { // Historically DDC has treated this case as a dynamic get and allowed it // to evaluate at runtime. // // This is here to preserve the existing behavior for the non-static // JavaScript interop (including some failing cases) but could potentially // be cleaned up as a breaking change. return _runtimeCall('dload$_replSuffix(#, #)', [ jsReceiver, js.string('call'), ]); } // Otherwise, tearoff of `call` on a function type is a no-op. return jsReceiver; } @override js_ast.Expression visitAuxiliaryExpression(AuxiliaryExpression node) { throw UnsupportedError( 'Unsupported auxiliary expression $node (${node.runtimeType}).', ); } @override js_ast.Statement visitAuxiliaryStatement(AuxiliaryStatement node) { throw UnsupportedError( 'Unsupported auxiliary statement $node (${node.runtimeType}).', ); } void _setEmitIfIncrementalLibrary(Library library) { if (_incrementalMode) { _setEmitIfIncremental(_libraryToModule(library), _jsLibraryName(library)); } } void _setEmitIfIncremental(String module, String library) { if (_incrementalMode) { _incrementalModules.putIfAbsent(module, () => {}).add(library); } } /// When compiling the body of a `operator []=` method, this will be non-null /// and will indicate the value that should be returned from any `return;` /// statements. js_ast.Identifier? get _operatorSetResult { var stack = _operatorSetResultStack; return stack.isEmpty ? null : stack.last; } /// Called when starting to emit methods/functions, in particular so we can /// implement special handling of the user-defined `[]=` and `==` methods. /// /// See also [_exitFunction] and [_emitReturnStatement]. void _enterFunction( String? name, List formals, bool Function() isLastParamMutated, ) { if (name == '[]=') { _operatorSetResultStack.add( isLastParamMutated() ? js_ast.ScopedId((formals.last as js_ast.Identifier).name) : formals.last as js_ast.Identifier, ); } else { _operatorSetResultStack.add(null); } } /// Called when finished emitting methods/functions, and must correspond to a /// previous [_enterFunction] call. js_ast.Block _exitFunction( List formals, js_ast.Block code, ) { var setOperatorResult = _operatorSetResultStack.removeLast(); if (setOperatorResult != null) { // []= methods need to return the value. We could also address this at // call sites, but it's less code size to handle inside the operator. var valueParam = formals.last; var statements = code.statements; if (statements.isEmpty || !statements.last.alwaysReturns) { statements.add(js_ast.Return(setOperatorResult)); } if (!identical(setOperatorResult, valueParam)) { // If the value parameter was mutated, then we use a temporary // variable to track the initial value formals.last = setOperatorResult; code = js.block('{ let # = #; #; }', [ valueParam, setOperatorResult, code, ]); } } return code; } /// Emits a return statement `return ;`, handling special rules for /// the `operator []=` method. js_ast.Statement _emitReturnStatement(js_ast.Expression? value) { if (_operatorSetResult != null) { var result = js_ast.Return(_operatorSetResult); return value != null ? js_ast.Block([value.toStatement(), result]) : result; } return value != null ? value.toReturn() : js_ast.Return(); } /// Prepends the `dart.` and then uses [js.call] to parse the specified JS /// [code] template, passing [args]. /// /// For example: /// /// _runtimeCall('asInt(#)', []) /// /// Generates a JS AST representing: /// /// dart.asInt() /// js_ast.Expression _runtimeCall(String code, [List? args]) { return js.call('#.$code', [ _emitLibraryName(_runtimeLibrary), ...?args, ]); } /// Calls [_runtimeCall] and uses `toStatement()` to convert the resulting /// expression into a statement. js_ast.Statement _runtimeStatement(String code, [List? args]) => _runtimeCall(code, args).toStatement(); /// Emits a private name JS Symbol for [name] scoped to the Dart [library]. /// /// If the same name is used in multiple libraries in the same module, /// distinct symbols will be used, so each library will have distinct private /// member names, that won't collide at runtime, as required by the Dart /// language spec. /// /// If an [id] is provided, try to use that. /// /// TODO(vsm): Clean up id generation logic. This method is used to both /// define new symbols and to reference existing ones. If it's called /// multiple times with same [library] and [name], we'll allocate redundant /// top-level variables (see callers to this method). js_ast.ScopedId _emitPrivateNameSymbol( Library library, String name, [ js_ast.ScopedId? id, ]) { /// Initializes the JS `Symbol` for the private member [name] in [library]. /// /// If the library is in the current JS module ([_libraries] contains it), /// the private name will be created and exported. The exported symbol is /// used for a few things: /// /// - private fields of constant objects /// - stateful hot reload (not yet implemented) /// - correct library scope in REPL (not yet implemented) /// /// If the library is imported, then the existing private name will be /// retrieved from it. In both cases, we use the same `dart.privateName` /// runtime call. js_ast.ScopedId initPrivateNameSymbol() { var idName = name.endsWith('=') ? name.replaceAll('=', '_') : name; idName = idName.replaceAll(js_ast.invalidCharInIdentifier, '_'); var identifier = id ?? js_ast.ScopedId(idName); _addSymbol( identifier, _runtimeCall('privateName(#, #)', [ js.string('${library.importUri}'), js.string(name), ]), ); if (!_containerizeSymbols) { // TODO(vsm): Change back to `const`. // See https://github.com/dart-lang/sdk/issues/40380. _moduleItems.add( js.statement('var # = #', [ identifier, _runtimeCall('privateName(#, #)', [ js.string('${library.importUri}'), js.string(name), ]), ]), ); } return identifier; } var privateNames = _privateNames.putIfAbsent(library, () => HashMap()); var symbolId = privateNames.putIfAbsent(name, initPrivateNameSymbol); _setEmitIfIncrementalLibrary(library); _setEmitIfIncremental( _libraryToModule(_coreLibrary), _runtimeLibraryId.name, ); _symbolContainer.setEmitIfIncremental(symbolId); return symbolId; } /// Emits an expression to set the property [nameExpr] on the class /// [className], with [value]. /// /// This will use `className.name = value` if possible, otherwise it will use /// `dart.defineValue(className, name, value)`. This is required when /// `FunctionNode.prototype` already defines a getters with the same name. js_ast.Expression _defineValueOnClass( Class c, js_ast.Expression className, js_ast.Expression nameExpr, js_ast.Expression value, ) { var args = [className, nameExpr, value]; if (nameExpr is js_ast.LiteralString) { var name = nameExpr.valueWithoutQuotes; if (js_ast.isFunctionPrototypeGetter(name) || _superclassHasStatic(c, name)) { return _runtimeCall('defineValue(#, #, #)', args); } } return js.call('#.# = #', args); } /// Emits a Dart Symbol with the given member [symbolName]. /// /// If the symbol refers to a private name, its library will be set to the /// [currentLibrary] by default, so the Symbol is scoped properly. Symbol /// constants should pass the symbol's [library] if the referenced symbol was /// declared outside the current library. js_ast.Expression _emitDartSymbol(String symbolName, {Library? library}) { // TODO(vsm): Handle qualified symbols correctly. var last = symbolName.split('.').last; var name = js.escapedString(symbolName, "'"); js_ast.Expression result; if (last.startsWith('_')) { var nativeSymbolAccessor = _getSymbol( _emitPrivateNameSymbol(library ?? _currentLibrary!, last), ); result = js.call('new #.new(#, #)', [ _emitConstructorAccess(_privateSymbolType), name, nativeSymbolAccessor, ]); } else { result = js.call('new #.new(#)', [ _emitConstructorAccess(_internalSymbolType), name, ]); } return _canonicalizeConstObject(result); } /// Calls the `dart.const` function in "dart:_runtime" to canonicalize a /// constant instance of a user-defined class stored in [expr]. js_ast.Expression _canonicalizeConstObject(js_ast.Expression expr) => _runtimeCall('const(#)', [expr]); /// Emits preamble for the module containing [libraries], and returns the /// list of module items for further items to be added. /// /// The preamble consists of initializing the identifiers for each library, /// that will be used to store their members. It also generates the /// appropriate ES6 `export` declaration to export them from this module. /// /// After the code for all of the library members is emitted, /// [_emitImportsAndExtensionSymbols] should be used to emit imports/extension /// symbols into the list returned by this method. Finally, [_finishLibrary] /// can be called to complete the module and return the resulting JS AST. /// /// This also initializes several fields: [_runtimeLibraryId], /// [_extensionSymbolsLibraryId], and the [_libraries] map needed by /// [_emitLibraryName]. List _startLibrary(Library library) { if (_isSdkInternalRuntime(library)) { // Don't allow these to be renamed when we're building the SDK. // There is JS code in dart:* that depends on their names. _runtimeLibraryId = js_ast.Identifier('dart'); _extensionSymbolsLibraryId = js_ast.Identifier( _extensionSymbolHolderName, ); } else { // Otherwise allow these to be renamed so users can write them. _runtimeLibraryId = js_ast.ScopedId('dart'); _extensionSymbolsLibraryId = js_ast.ScopedId(_extensionSymbolHolderName); } // Initialize our library variables. var items = []; var exports = []; if (_isSdkInternalRuntime(library)) { _libraries[library] = _runtimeLibraryId; } else { var libraryId = _isBuildingSdk && _isDartLibrary(library, '_rti') ? _rtiLibraryId : js_ast.ScopedId(_jsLibraryName(library)); _libraries[library] = libraryId; var alias = _jsLibraryAlias(library); var aliasId = alias == null ? null : js_ast.ScopedId(alias); exports.add(js_ast.NameSpecifier(libraryId, asName: aliasId)); } items.add(js_ast.ExportDeclaration(js_ast.ExportClause(exports))); if (_isSdkInternalRuntime(library)) { // Initialize the private name function. // To bootstrap the SDK, this needs to be emitted before other code. var privateNamesId = _emitScopedId('privateNames'); items.add(js.statement('const # = new Map()', privateNamesId)); items.add( _runtimeStatement( r''' privateName = function privateName(libraryUri, name) { let names = #.get(libraryUri); if (names == null) #.set(libraryUri, names = new Map()); let symbol = names.get(name); if (symbol == null) names.set(name, symbol = Symbol(name)); return symbol; } ''', [privateNamesId, privateNamesId], ), ); } return items; } /// Returns the canonical name to refer to the Dart library. js_ast.Identifier _emitLibraryName(Library library) { _setEmitIfIncrementalLibrary(library); // It's either one of the libraries in this module, or it's an import. return _libraries[library] ?? _imports.putIfAbsent(library, () { if (_isSdkInternalRuntime(library)) return _runtimeLibraryId; if (_isDartLibrary(library, '_rti')) return _rtiLibraryId; return js_ast.ScopedId(_jsLibraryName(library)); }); } /// Emits imports into [items]. void _emitImports(List items) { var modules = >{}; for (var import in _imports.keys) { modules.putIfAbsent(_libraryToModule(import), () => []).add(import); } // TODO(nshahan): Update this code and the representation of // `ImportDeclaration`s when other module formats are no longer supported. modules.forEach((module, libraries) { if (!_incrementalMode || _incrementalModules.containsKey(module)) { var usedLibraries = _incrementalModules[module]; // Generate import directives. // // Our import variables are temps and can get renamed. Since our // renaming is integrated into js_ast, it is aware of this possibility // and will generate an "as" if needed. For example: // // import {foo} from 'foo'; // if no rename needed // import {foo as foo$} from 'foo'; // if rename was needed // for (var library in libraries) { if (!_incrementalMode || usedLibraries!.contains(_jsLibraryName(library))) { var alias = _jsLibraryAlias(library); if (alias != null) { var aliasId = js_ast.ScopedId(alias); items.add( js_ast.ImportDeclaration( from: js.string('${library.importUri}'), namedImports: [ js_ast.NameSpecifier(aliasId, asName: _imports[library]), ], ), ); } else { items.add( js_ast.ImportDeclaration( from: js.string('${library.importUri}'), namedImports: [js_ast.NameSpecifier(_imports[library])], ), ); } } } } }); items.add( js_ast.ImportDeclaration( from: js.string(_extensionSymbolHolderName), namedImports: [js_ast.NameSpecifier(_extensionSymbolsLibraryId)], ), ); } /// Emits extension methods into [items]. void _emitExtensionSymbols( List items, { bool forceExtensionSymbols = false, }) { // Initialize extension symbols _extensionSymbols.forEach((name, id) { js_ast.Expression value = js_ast.PropertyAccess( _extensionSymbolsLibraryId, _propertyName(name), ); if (forceExtensionSymbols) { value = js.call('# || (# = Symbol(#))', [ value, value, js.string('$_extensionSymbolHolderName.$name'), ]); } // Emit hoisted extension symbols that are marked as noEmit in regular as // well as incremental mode (if needed) since they are going to be // referenced as such in the generated expression. if (!_incrementalMode || _symbolContainer.incrementalModuleItems.contains(id)) { if (!_symbolContainer.canEmit(id)) { // Extension symbols marked with noEmit are managed manually. // TODO(vsm): Change back to `const`. // See https://github.com/dart-lang/sdk/issues/40380. items.add(js.statement('var # = #;', [id, value])); } } if (_symbolContainer.incrementalModuleItems.contains(id)) { _setEmitIfIncremental( _libraryToModule(_coreLibrary), _extensionSymbolsLibraryId.name, ); } _symbolContainer[id] = value; }); } /// Emits exports as imports into [items]. /// /// Use information from exports to re-define library variables referenced /// inside compiled expressions in incremental mode. That matches importing /// a current module into the symbol used to represent the library during /// original compilation in [emitLibrary]. /// /// Example of exports emitted to JavaScript during emitModule: /// /// ``` /// // Exports: /// return { /// web__main: main /// }; /// ``` /// /// The transformation to imports during expression compilation converts the /// exports above to: /// /// ``` /// const web__main = require('web/main'); /// const main = web__main.web__main; /// ``` /// /// Where the compiled expression references `main`. void _emitExportsAsImports(List items, Library current) { var exports = []; assert(_incrementalMode); assert(!_isBuildingSdk); var module = _libraryToModule(current); var usedLibraries = _incrementalModules[module] ?? {}; if (usedLibraries.isNotEmpty) { _libraries.forEach((library, libraryId) { if (usedLibraries.contains(_jsLibraryName(library))) { var alias = _jsLibraryAlias(library); var aliasId = alias == null ? libraryId : js_ast.ScopedId(alias); var asName = alias == null ? null : libraryId; exports.add(js_ast.NameSpecifier(aliasId, asName: asName)); } }); items.add( js_ast.ImportDeclaration( namedImports: exports, from: js.string(current.importUri.toString(), "'"), ), ); } } /// Emits imports and extension methods into [items]. void _emitImportsAndExtensionSymbols( List items, { bool forceExtensionSymbols = false, }) { _emitImports(items); _emitExtensionSymbols(items, forceExtensionSymbols: forceExtensionSymbols); } /// Returns an accessor for [id] via the symbol container. /// E.g., transforms $sym to S$5.$sym. /// /// A symbol lookup on an id marked no emit omits the symbol accessor. js_ast.Expression _getSymbol(js_ast.Identifier id) { _symbolContainer.setEmitIfIncremental(id); return _symbolContainer.canEmit(id) ? _symbolContainer.access(id) : id; } /// Returns the raw JS value associated with [id]. js_ast.Expression _getSymbolValue(js_ast.Identifier id) { _symbolContainer.setEmitIfIncremental(id); return _symbolContainer[id]!; } /// Inserts a symbol into the symbol table. js_ast.Expression _addSymbol(js_ast.Identifier id, js_ast.Expression symbol) { _symbolContainer[id] = symbol; _symbolContainer.setEmitIfIncremental(id); if (!_containerizeSymbols) { _symbolContainer.setNoEmit(id); } return _symbolContainer[id]!; } /// Finishes the library created by [_startLibrary], by combining the preamble /// [items] with the [_moduleItems] that have been emitted. /// /// The [libraryName] should specify the library's name, and the items should /// be the list resulting from [_startLibrary], with additional items added, /// but not including the contents of [_moduleItems] (which will be handled /// by this method itself). /// /// Note, this function mutates the items list and returns it as the `body` /// field of the result. js_ast.Program _finishLibrary( List items, String libraryName, js_ast.Identifier libraryId, ) { // TODO(jmesserly): there's probably further consolidation we can do // between DDC's two backends, by moving more code into this method, as the // code between `_startLibrary` and `_finishLibrary` is very similar in // both. // Emit all top-level JS symbol containers. items.addAll(_symbolContainer.emit()); if (_dynamicEntrypoint != null) { // Expose the entrypoint of the dynamic module under a reserved name. // TODO(sigmund): this could use a reserved symbol from dartx. var name = _emitTopLevelName(_dynamicEntrypoint!); _moduleItems.add( js_ast.ExportDeclaration( js('var __dynamic_module_entrypoint__ = #', [name]), ), ); } // Add the library's code (produced by visiting compilation units, above) _copyAndFlattenBlocks(items, _moduleItems); _moduleItems.clear(); // Build the library. return js_ast.Program( items, name: libraryName, librarySelfVar: libraryId, dartSize: _currentLibrary!.dartSize, ); } /// Flattens blocks in [items] to a single list. /// /// This will not flatten blocks that are marked as being scopes. void _copyAndFlattenBlocks( List result, Iterable items, ) { for (var item in items) { if (item is js_ast.Block && !item.isScope) { _copyAndFlattenBlocks(result, item.statements); } else { result.add(item); } } } /// This is an internal method used by [_emitMemberName] and the /// optimized `dart:_runtime extensionSymbol` builtin to get the symbol /// for `dartx.`. /// /// Do not call this directly; you want [_emitMemberName], which knows how to /// handle the many details involved in naming. js_ast.ScopedId _getExtensionSymbolInternal(String name) { if (!_extensionSymbols.containsKey(name)) { var id = js_ast.ScopedId( '\$${js_ast.friendlyNameForDartOperator[name] ?? name}', ); _extensionSymbols[name] = id; _addSymbol(id, id); } var symbolId = _extensionSymbols[name]!; _symbolContainer.setEmitIfIncremental(symbolId); return symbolId; } /// Shorthand for identifier-like property names. /// For now, we emit them as strings and the printer restores them to /// identifiers if it can. // TODO(jmesserly): avoid the round tripping through quoted form. js_ast.LiteralString _propertyName(String name) => js.string(name, "'"); /// Unique identifiers indicating the locations to inline the corresponding /// information. /// /// We cannot generate the source map before the script it is for is /// generated so we have generate the script including this identifier in the /// JS AST, and then replace it once the source map is generated. Similarly, /// metrics include the size of the source map. static const String sourceMapLocationID = 'SourceMap3G5a8h6JVhHfdGuDxZr1EF9GQC8y0e6u'; static const String metricsLocationID = 'MetricsJ7xFWBfSv6ZjrW9yLb21GNzisZr3anSf5h'; /// Matches against the `dart:js_util` `_callMethodUnchecked` and /// `_callMethodUncheckedTrustType` variants with 0 to 4 arguments. static final RegExp _callMethodUncheckedRegex = RegExp( r'^\_callMethodUnchecked(TrustType)?[0-4]', ); /// Matches against the `dart:js_util` `_callConstructorUnchecked` and /// `_callConstructorUncheckedTrustType` variants with 0 to 4 arguments. static final RegExp _callConstructorUncheckedRegex = RegExp( r'^\_callConstructorUnchecked[0-4]', ); } bool _isInlineJSFunction(Statement? body) { var block = body; if (block is Block) { var statements = block.statements; if (statements.length != 1) return false; body = statements[0]; } if (body is ReturnStatement) { var expr = body.expression; return expr is StaticInvocation && isInlineJS(expr.target); } return false; } /// Return true if this is one of the methods/properties on all Dart Objects /// (toString, hashCode, noSuchMethod, runtimeType). /// /// Operator == is excluded, as it is handled as part of the equality binary /// operator. bool _isObjectMember(String name) { // We could look these up on Object, but we have hard coded runtime helpers // so it's not really providing any benefit. switch (name) { case 'hashCode': case 'toString': case 'noSuchMethod': case 'runtimeType': case '==': return true; } return false; } class _SwitchLabelState { String label; js_ast.Identifier variable; _SwitchLabelState(this.label, this.variable); } /// Whether [expression] is a constant of the form /// `const pragma('dyn-module:entry-point')`. /// /// Used to denote the entrypoint method of a dynamic module. bool _isDynamicModuleEntryPoint(Procedure p, CoreTypes coreTypes) { return hasPragma(p, 'dyn-module:entry-point', coreTypes); }