From 8aa1812082879fe32ea69453b45f37e34127c9b9 Mon Sep 17 00:00:00 2001 From: Nicholas Shahan Date: Mon, 19 Aug 2024 23:10:16 +0000 Subject: [PATCH] [ddc] Create fork of compiler.dart The fork (compiler_new.dart) will contain changes to compile libraries in isolation even within the same module. It will eventually replace compiler.dart. Change-Id: I8b1c99895a6618ae6f8e8ed5173886e82ce6364b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/379260 Reviewed-by: Nate Biggs Reviewed-by: Mark Zhou Commit-Queue: Nicholas Shahan --- .../lib/src/kernel/compiler_new.dart | 8320 +++++++++++++++++ 1 file changed, 8320 insertions(+) create mode 100644 pkg/dev_compiler/lib/src/kernel/compiler_new.dart diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart new file mode 100644 index 000000000000..dc051ecda82e --- /dev/null +++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart @@ -0,0 +1,8320 @@ +// 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/transformations/js_util_optimizer.dart' + show ExtensionIndex; +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 '../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 '../compiler/shared_command.dart' show SharedCompilerOptions; +import '../js_ast/js_ast.dart' as js_ast; +import '../js_ast/js_ast.dart' show ModuleItem, js; +import '../js_ast/source_map_printer.dart' + show NodeEnd, NodeSpan, HoverComment, continueSourceMap; +import 'constants.dart'; +import 'future_or_normalizer.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'; + +class ProgramCompiler extends ComputeOnceConstantVisitor + with OnceConstantVisitorDefaultMixin + implements + StatementVisitor, + ExpressionVisitor { + final SharedCompilerOptions _options; + + /// Maps each `Class` node compiled in the module to the `Identifier`s used to + /// name the class in JavaScript. + /// + /// This mapping is used when generating the symbol information for the + /// module. + final classIdentifiers = {}; + + /// 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 = {}; + + /// 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; + + Set? _pendingClasses; + + /// 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; + + /// True when a class is emitting a deferred class hierarchy. + bool _emittingDeferredType = false; + + /// 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; + + /// 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; + + /// Table of instantiated generic class references. + /// + /// Provides a cache for the instantiated generic types local to a module. + late TypeTable _genericClassTable; + + /// 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.TemporaryId('_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 [_extensionSymbolsModule]; 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 _runtimeModule; + + /// 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 _extensionSymbolsModule; + + /// 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.TemporaryId('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. + bool _isBuildingSdk = false; + + /// 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.TemporaryId('opts'); + + /// The list of output module items, in the order they need to be emitted in. + final _moduleItems = []; + + /// Like [_moduleItems] but for items that should be emitted after classes. + /// + /// This is used for deferred supertypes of mutually recursive non-generic + /// classes. + final _afterClassDefItems = []; + + 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; + + final DevCompilerConstants _constants; + + final NullableInference _nullableInference; + + bool _moduleEmitted = false; + + /// Supports verbose logging with a timer. + Ticker? _ticker; + + factory ProgramCompiler( + Component component, + ClassHierarchy hierarchy, + SharedCompilerOptions options, + Map importToSummary, + Map summaryToModule, { + CoreTypes? coreTypes, + Ticker? ticker, + }) { + coreTypes ??= CoreTypes(component); + var types = TypeEnvironment(coreTypes, hierarchy); + var constants = DevCompilerConstants(); + var nativeTypes = NativeTypeSet(coreTypes, constants, component); + var jsTypeRep = JSTypeRep(types, hierarchy); + var staticTypeContext = StatefulStaticTypeContext.stacked(types); + return ProgramCompiler._( + ticker, + coreTypes, + coreTypes.index, + nativeTypes, + constants, + types, + hierarchy, + jsTypeRep, + NullableInference(jsTypeRep, staticTypeContext, options: options), + staticTypeContext, + options, + importToSummary, + summaryToModule, + ); + } + + ProgramCompiler._( + 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) + : _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), + _rtiLibrary = sdk.getLibrary('dart:_rti'), + _rtiClass = sdk.getClass('dart:_rti', 'Rti'); + + /// 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; + + /// Module can be emitted only once, and the compiler can be reused after + /// only in incremental mode, for expression compilation only. + js_ast.Program emitModule(Component component) { + if (_moduleEmitted) { + throw StateError('Can only call emitModule once.'); + } + _ticker?.logMs('Emitting module'); + _component = component; + + var libraries = component.libraries; + + // Initialize library variables. + _isBuildingSdk = libraries.any(_isSdkInternalRuntime); + + // For runtime performance reasons, we only containerize SDK symbols in web + // libraries. Otherwise, we use a 600-member cutoff before a module is + // containerized. This is somewhat arbitrary but works promisingly for the + // SDK and Flutter Web. + if (!_isBuildingSdk) { + // The number of DDC top-level symbols scales with the number of + // non-static class members across an entire module. + var uniqueNames = HashSet(); + libraries.forEach((Library l) { + l.classes.forEach((Class c) { + c.members.forEach((m) { + var isStatic = + m is Field ? m.isStatic : (m is Procedure ? m.isStatic : false); + if (isStatic) return; + var name = js_ast.toJSIdentifier( + m.name.text.replaceAll(js_ast.invalidCharInIdentifier, '_')); + uniqueNames.add(name); + }); + }); + }); + _containerizeSymbols = uniqueNames.length > 600; + } + + var items = _startModule(libraries); + // TODO(nshahan) Move into `startModule()` once `SharedCompiler` and + // `ProgramCompiler` have been refactored together. + if (!_isBuildingSdk) { + _forceLibraryImport(_rtiLibrary, _rtiLibraryId); + } + + _nullableInference.allowNotNullDeclarations = _isBuildingSdk; + _typeTable = TypeTable('T', _runtimeCall); + _genericClassTable = TypeTable('G', _runtimeCall); + + // Collect all class/type Element -> Node mappings + // in case we need to forward declare any classes. + var classes = HashSet.identity(); + for (var l in libraries) { + classes.addAll(l.classes); + } + _pendingClasses = classes; + + // Insert a circular reference so neither the constant table or its cache + // are optimized away by V8. Required for expression evaluation. + var constTableDeclaration = + js.statement('const # = Object.create({# : () => (#, #)});', [ + _constTable, + js_ast.LiteralString('_'), + _constTableCache.containerId, + _constTable + ]); + _moduleItems.add(constTableDeclaration); + + // Record a safe index after the declaration of type generators and + // top-level symbols but before the declaration of any functions. + // Various preliminary data structures must be inserted here prior before + // referenced by the rest of the module. + var safeDeclarationIndex = _moduleItems.length; + _constTableInsertionIndex = safeDeclarationIndex; + + // Add implicit dart:core dependency so it is first. + _emitLibraryName(_coreTypes.coreLibrary); + _ticker?.logMs('Added table declarations'); + + // Visit each library and emit its code. + // + // NOTE: classes are not necessarily emitted in this order. + // Order will be changed as needed so the resulting code can execute. + // This is done by forward declaring items. + libraries.forEach(_emitLibrary); + _ticker?.logMs('Emitted ${libraries.length} libraries'); + + // Emit hoisted assert strings + _moduleItems.insertAll(safeDeclarationIndex, _uriContainer.emit()); + + _moduleItems.insertAll(safeDeclarationIndex, _constTableCache.emit()); + + if (_constLazyAccessors.isNotEmpty) { + var constTableBody = _runtimeStatement( + 'defineLazy(#, { # }, false)', [_constTable, _constLazyAccessors]); + _moduleItems.insert(_constTableInsertionIndex, constTableBody); + _constLazyAccessors.clear(); + } + + _moduleItems.addAll(_afterClassDefItems); + _afterClassDefItems.clear(); + // 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 library in libraries) { + 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'); + var typeRules = _typeRecipeGenerator.liveInterfaceTypeRules; + if (typeRules.isNotEmpty) { + var template = '#._Universe.#(#, JSON.parse(#))'; + var addRulesStatement = js.call(template, [ + _emitLibraryName(_rtiLibrary), + _emitMemberName('addRules', memberClass: universeClass), + _runtimeCall('typeUniverse'), + js.string(jsonEncode(typeRules), "'") + ]).toStatement(); + _moduleItems.add(addRulesStatement); + } + // Update type rules for `LegacyJavaScriptObject` to add all interop + // types in this module as a supertype. + var updateRules = _typeRecipeGenerator.updateLegacyJavaScriptObjectRules; + if (updateRules.isNotEmpty) { + // All JavaScript interop classes should be mutual subtypes with + // `LegacyJavaScriptObject`. To achieve this the rules are manually + // added here. There is special redirecting rule logic in the dart:_rti + // library for interop types because otherwise they would duplicate + // a lot of supertype information. + var updateRulesStatement = + js.statement('#._Universe.#(#, JSON.parse(#))', [ + _emitLibraryName(_rtiLibrary), + _emitMemberName('addOrUpdateRules', memberClass: universeClass), + _runtimeCall('typeUniverse'), + js.string(jsonEncode(updateRules), "'") + ]); + _moduleItems.add(updateRulesStatement); + } + 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]); + _moduleItems.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(); + _moduleItems.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 + var prerequisiteRtiTypes = [ + _coreTypes.objectLegacyRawType, + _coreTypes.objectNullableRawType, + NeverType.legacy() + ]; + prerequisiteRtiTypes.forEach((type) { + var recipe = _typeRecipeGenerator + .recipeInEnvironment(type, EmptyTypeEnvironment()) + .recipe; + _moduleItems.add(js.call('#.findType("$recipe")', + [_emitLibraryName(_rtiLibrary)]).toStatement()); + }); + + // Visit directives (for exports) + libraries.forEach(_emitExports); + _ticker?.logMs('Emitted exports'); + + // Declare imports and extension symbols + _emitImportsAndExtensionSymbols(items, + forceExtensionSymbols: + libraries.any((l) => allowedNativeTest(l.importUri))); + _ticker?.logMs('Emitted imports and extension symbols'); + + // Insert a check that runs when loading this module to verify that the null + // safety mode it was compiled in matches the mode used when compiling the + // dart sdk module. + // + // This serves as a sanity check at runtime that we don't have an + // infrastructure issue that loaded js files compiled with different modes + // into the same application. + js_ast.LiteralBool soundNullSafety; + switch (component.mode) { + case NonNullableByDefaultCompiledMode.Strong: + soundNullSafety = js_ast.LiteralBool(true); + break; + case NonNullableByDefaultCompiledMode.Weak: + soundNullSafety = js_ast.LiteralBool(false); + break; + default: + throw StateError('Unsupported Null Safety mode ${component.mode}, ' + 'in ${component.location?.file}.'); + } + if (!_isBuildingSdk) { + items.add(_runtimeStatement( + '_checkModuleNullSafetyMode(#)', [soundNullSafety])); + } + + // Emit the hoisted type table cache variables + items.addAll(_typeTable.dischargeBoundTypes()); + _ticker?.logMs('Emitted type table'); + + // Emit the hoisted instantiated generic class table cache variables + items.addAll(_genericClassTable.dischargeBoundTypes()); + _ticker?.logMs('Emitted instantiated generic class table'); + + var module = _finishModule(items, _options.moduleName, + header: _generateCompilationHeader()); + _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 module; + } + + /// 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}'; + + /// Debugger friendly names for all parts in a Dart [library]. + Iterable _jsPartDebuggerNames(Library library) => + library.parts.map((part) => part.partUri); + + /// 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) { + // NOTE: this method isn't the right place to initialize per-library state. + // Classes can be visited out of order, so this is only to catch things that + // haven't been emitted yet. + // + // See _emitClass. + assert(_currentLibrary == null); + _currentLibrary = library; + _staticTypeContext.enterLibrary(_currentLibrary!); + + if (_isBuildingSdk) { + _containerizeSymbols = _isWebLibrary(library.importUri); + } + + if (_isSdkInternalRuntime(library)) { + // 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. + _emitLibraryProcedures(library); + _emitTopLevelFields(library.fields); + library.classes.forEach(_emitClass); + } else { + library.classes.forEach(_emitClass); + _emitLibraryProcedures(library); + _emitTopLevelFields(library.fields); + } + + _staticTypeContext.leaveLibrary(_currentLibrary!); + _currentLibrary = null; + } + + void _emitExports(Library library) { + assert(_currentLibrary == null); + _currentLibrary = library; + + library.additionalExports.forEach(_emitExport); + + _currentLibrary = null; + } + + void _emitExport(Reference export) { + var library = _currentLibrary!; + + // We only need to export main as it is the only method part of the + // publicly exposed JS API for a library. + + var node = export.node; + if (node is Procedure && node.name.text == 'main') { + // Don't allow redefining names from this library. + var name = _emitTopLevelName(node); + _moduleItems.add(js.statement( + '#.# = #;', [_emitLibraryName(library), name.selector, name])); + } + } + + /// Called to emit class declarations. + /// + /// During the course of emitting one item, we may emit another. For example + /// + /// class D extends B { C m() { ... } } + /// + /// Because D depends on B, we'll emit B first if needed. However C is not + /// used by top-level JavaScript code, so we can ignore that dependency. + void _emitClass(Class c) { + if (!_pendingClasses!.remove(c)) return; + + var savedClass = _currentClass; + var savedLibrary = _currentLibrary; + var savedUri = _currentUri; + _currentClass = c; + _currentLibrary = c.enclosingLibrary; + _currentUri = c.fileUri; + 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); + + // 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; + } + + /// To emit top-level classes, we sometimes need to reorder them. + /// + /// This function takes care of that, and also detects cases where reordering + /// failed, and we need to resort to lazy loading, by marking the element as + /// lazy. All elements need to be aware of this possibility and generate code + /// accordingly. + /// + /// If we are not emitting top-level code, this does nothing, because all + /// declarations are assumed to be available before we start execution. + /// See [startTopLevel]. + void _declareBeforeUse(Class? c) { + if (c != null && _emittingClassExtends) { + _emitClass(c); + } + } + + static js_ast.Identifier _emitIdentifier(String name) => + js_ast.Identifier(js_ast.toJSIdentifier(name)); + + static js_ast.TemporaryId _emitTemporaryId(String name) => + js_ast.TemporaryId(js_ast.toJSIdentifier(name)); + + js_ast.Statement _emitClassDeclaration(Class c) { + 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 jsMethods = _emitClassMethods(c); + + _emitSuperHelperSymbols(body); + // Deferred supertypes must be evaluated lazily while emitting classes to + // prevent evaluating a JS expression for a deferred type from influencing + // class declaration order (such as when calling 'emitDeferredType'). + var deferredSupertypes = []; + + // Emit the class, e.g. `core.Object = class Object { ... }` + _defineClass(c, className, jsMethods, body, deferredSupertypes); + body.addAll(jsCtors); + + // Emit things that come after the ES6 `class ... { ... }`. + var jsPeerNames = _extensionTypes.getNativePeers(c); + if (jsPeerNames.length == 1 && c.typeParameters.isNotEmpty) { + // Special handling for JSArray + body.add(_runtimeStatement('setExtensionBaseClass(#, #)', [ + className, + _runtimeCall('global.#', [jsPeerNames[0]]) + ])); + } + + /// 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.isMixinDeclaration) { + _defineExtensionMembers(className, body); + } + + var typeFormals = c.typeParameters; + var evaluatedDeferredSupertypes = + deferredSupertypes.map((f) => f()).toList(); + if (typeFormals.isNotEmpty) { + var genericClassStmts = _defineGenericClass(typeFormals, + js_ast.Statement.from(body), evaluatedDeferredSupertypes); + body = [...genericClassStmts]; + } else { + _afterClassDefItems.addAll(evaluatedDeferredSupertypes); + } + + _emitStaticFieldsAndAccessors(c, body); + 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. + body.add(_runtimeStatement('_installIdentityEquals()')); + } else { + for (var peer in jsPeerNames) { + _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); + + var nonExternalMethods = []; + 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) nonExternalMethods.add(factory); + } else if (procedure.isStatic) { + var staticMethod = _emitMethodDeclaration(procedure); + if (staticMethod != null) nonExternalMethods.add(staticMethod); + } + } + + // Emit static fields, if there are any. + var fieldInitialization = []; + _emitStaticFieldsAndAccessors(c, fieldInitialization); + + // Avoid unnecessary code emission if there are no members we care about. + if (nonExternalMethods.isNotEmpty || fieldInitialization.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, nonExternalMethods) + .toStatement() + ]; + var typeFormals = c.typeParameters; + if (typeFormals.isNotEmpty) { + var genericClassStmts = + _defineGenericClass(typeFormals, js_ast.Statement.from(body), []); + body = [...genericClassStmts, ...fieldInitialization]; + } else { + body = [...body, ...fieldInitialization]; + } + return js_ast.Statement.from(body); + } + return null; + } + + /// Emits a generic class with additional initialization logic. + List _defineGenericClass(List formals, + js_ast.Statement body, List deferredBaseClass) { + assert(formals.isNotEmpty); + return [ + ..._typeTable.dischargeFreeTypes(formals), + body, + ...deferredBaseClass, + ]; + } + + js_ast.Statement _emitClassStatement(Class c, js_ast.Expression className, + js_ast.Expression? heritage, List methods) { + var classIdentifier = _emitTemporaryId(getLocalClassName(c)); + if (_options.emitDebugSymbols) classIdentifiers[c] = classIdentifier; + var classExpr = js_ast.ClassExpression(classIdentifier, heritage, methods); + return js.statement('# = #;', [className, classExpr]); + } + + /// Like [_emitClassStatement] but emits a Dart 2.1 mixin represented by + /// [c]. + /// + /// Mixins work similar to normal classes, but their instance methods close + /// over the actual superclass. Given a Dart class like: + /// + /// mixin M on C { + /// foo() => super.foo() + 42; + /// } + /// + /// We generate a JS class like this: + /// + /// lib.M = class M extends core.Object {} + /// lib.M[dart.mixinOn] = (C) => class M extends C { + /// foo() { + /// return super.foo() + 42; + /// } + /// }; + /// + /// The special `dart.mixinOn` symbolized property is used by the runtime + /// helper `dart.applyMixin`. The helper calls the function with the actual + /// base class, and then copies the resulting members to the destination + /// class. + /// + /// In the long run we may be able to improve this so we do not have the + /// unnecessary class, but for now, this lets us get the right semantics with + /// minimal compiler and runtime changes. + void _emitMixinStatement( + Class c, + js_ast.Expression className, + js_ast.Expression heritage, + List methods, + List body) { + var staticMethods = methods.where((m) => m.isStatic).toList(); + var instanceMethods = methods.where((m) => !m.isStatic).toList(); + + body.add(_emitClassStatement(c, className, heritage, staticMethods)); + var superclassId = _emitTemporaryId(getLocalClassName(c.superclass!)); + var classId = className is js_ast.Identifier + ? className + : _emitTemporaryId(getLocalClassName(c)); + + var mixinMemberClass = + js_ast.ClassExpression(classId, superclassId, instanceMethods); + + js_ast.Node arrowFnBody = mixinMemberClass; + var extensionInit = []; + _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) + ])); + } + + void _defineClass( + Class c, + js_ast.Expression className, + List methods, + List body, + List deferredSupertypes) { + if (c == _coreTypes.objectClass) { + body.add(_emitClassStatement(c, className, null, methods)); + return; + } + + js_ast.Expression emitDeferredClassRef(InterfaceType type) { + var savedEmittingDeferredType = _emittingDeferredType; + _emittingDeferredType = true; + _declareBeforeUse(type.classNode); + var deferredClassRef = _emitClassRef(type); + _emittingDeferredType = savedEmittingDeferredType; + return deferredClassRef; + } + + bool shouldDefer(InterfaceType type) { + var visited = {}; + bool defer(InterfaceType t) { + var tc = t.classNode; + if (c == tc) return true; + if (tc == _coreTypes.objectClass || !visited.add(t)) return false; + var mixin = tc.mixedInType; + return mixin != null && defer(mixin.asInterfaceType) || + defer(tc.supertype!.asInterfaceType); + } + + return defer(type); + } + + js_ast.Expression emitClassRef(InterfaceType t) { + // TODO(jmesserly): investigate this. It seems like `lazyJSType` is + // invalid for use in an `extends` clause, hence this workaround. + return _emitJSInterop(t.classNode) ?? _emitClassRef(t); + } + + js_ast.Expression getBaseClass(int count) { + var base = emitDeferredClassRef( + c.getThisType(_coreTypes, c.enclosingLibrary.nonNullable)); + while (--count >= 0) { + base = _emitJSObjectGetPrototypeOf(base, fullyQualifiedName: true); + } + return base; + } + + // 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(''), + [if (mixinRti != null) mixinRti], + ]); + } + + var name = ctor.name.text; + var ctorBody = [ + if (mixinCtor != null) 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 = shouldDefer(supertype) + ? emitDeferredClassRef(supertype) + : emitClassRef(supertype); + + // 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; + _declareBeforeUse(mixinClass); + var mixinType = + _hierarchy.getClassAsInstanceOf(c, mixinClass)!.asInterfaceType; + var mixinName = + '${getLocalClassName(superclass)}_${getLocalClassName(mixinClass)}'; + var mixinId = _emitTemporaryId('$mixinName\$'); + // 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; + + // Bind the mixin class to a name to workaround a V8 bug with es6 classes + // and anonymous function names. + // TODO(leafp:) Eliminate this once the bug is fixed: + // https://bugs.chromium.org/p/v8/issues/detail?id=7069 + body.add(js.statement('const # = #', [ + mixinId, + js_ast.ClassExpression( + _emitTemporaryId(mixinName), baseClass, forwardingMethodStubs) + ])); + + emitMixinConstructors(mixinId, superclass, mixinClass, mixinType); + hasUnnamedSuper = hasUnnamedSuper || _hasUnnamedConstructor(mixinClass); + + if (shouldDefer(mixinType)) { + deferredSupertypes.add(() => _runtimeStatement('applyMixin(#, #)', [ + getBaseClass(mixinApplications.length - i), + emitDeferredClassRef(mixinType) + ])); + } else { + body.add(_runtimeStatement( + 'applyMixin(#, #)', [mixinId, emitClassRef(mixinType)])); + } + + baseClass = mixinId; + } + + if (c.isMixinDeclaration && !c.isMixinClass) { + _emitMixinStatement(c, className, baseClass, methods, body); + } else { + body.add(_emitClassStatement(c, className, baseClass, methods)); + } + + _classEmittingExtends = savedTopLevelClass; + } + + /// Defines all constructors for this class as ES5 constructors. + List _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.TemporaryId), body); + _superHelpers.clear(); + } + + /// Emits non-external static fields for a class, and initialize them eagerly + /// if possible, otherwise define them as lazy properties. + void _emitStaticFieldsAndAccessors(Class c, List body) { + var fields = c.fields.where((f) => f.isStatic && !f.isExternal).toList(); + var fieldNames = Set.from(fields.map((f) => f.name)); + var staticSetters = c.procedures.where( + (p) => p.isStatic && p.isAccessor && fieldNames.contains(p.name)); + var members = [...fields, ...staticSetters]; + if (fields.isNotEmpty) { + body.add(_emitLazyMembers(_emitTopLevelNameNoExternalInterop(c), members, + (n) => _emitStaticMemberName(n.name.text))); + } + } + + /// Ensure `dartx.` symbols we will use are present. + void _initExtensionSymbols(Class c) { + if (_extensionTypes.hasNativeSubtype(c) || c == _coreTypes.objectClass) { + for (var m in c.procedures) { + if (!m.isAbstract && !m.isStatic && !m.name.isPrivate) { + _declareMemberName(m, useExtension: true); + } + } + } + } + + /// If a concrete class implements one of our extensions, we might need to + /// add forwarders. + void _defineExtensionMembers( + js_ast.Expression className, List 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 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); + + if (needsSignature) { + js_ast.Expression type; + var memberName = _declareMemberName(member); + 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); + // 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)) { + // Avoid tagging a member as Function? or Function* + result = f.computeThisFunctionType(Nullability.nonNullable); + } else { + var fComputed = + f.computeThisFunctionType(member.enclosingLibrary.nonNullable); + var fComputedNamedByName = { + for (NamedType namedParameter in fComputed.namedParameters) + namedParameter.name: namedParameter + }; + DartType reifyParameter( + VariableDeclaration parameter, DartType fComputedParameter) => + isCovariantParameter(parameter) + ? _coreTypes.objectRawType(member.enclosingLibrary.nullable) + : 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), + [ + if (rtiParam != null) 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 = [ + if (rti != null) 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); + } + + /// 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 = []; + void emitFieldInit(Field f, Expression? initializer, TreeNode hoverInfo) { + var virtualField = _classProperties!.virtualFields[f]; + + // Avoid calling getSymbol on _declareMemberName since _declareMemberName + // calls _emitMemberName downstream, which already invokes getSymbol. + var access = virtualField == null + ? _declareMemberName(f) + : _getSymbol(virtualField); + var jsInit = _visitInitializer(initializer, f.annotations); + body.add(jsInit + .toAssignExpression(js.call('this.#', [access]) + ..sourceInformation = _nodeStart(hoverInfo)) + .toStatement()); + } + + for (var f in fields) { + if (f.isStatic) continue; + var init = f.initializer; + if (ctorFields != null && + ctorFields.contains(f) && + (init == null || _constants.isConstant(init))) { + continue; + } + _staticTypeContext.enterMember(f); + emitFieldInit(f, init, f); + _staticTypeContext.leaveMember(f); + } + + // Run constructor field initializers such as `: foo = bar.baz` + if (ctor != null) { + for (var init in ctor.initializers) { + if (init is FieldInitializer) { + emitFieldInit(init.field, init.value, init); + } else if (init is LocalInitializer) { + body.add(visitVariableDeclaration(init.variable)); + } else if (init is AssertInitializer) { + body.add(visitAssertStatement(init.statement)); + } + } + } + + return js_ast.Statement.from(body); + } + + js_ast.Expression _visitInitializer( + Expression? init, List 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; + } + } + + List _emitClassMethods(Class c) { + var virtualFields = _classProperties!.virtualFields; + + var jsMethods = []; + var hasJsPeer = _extensionTypes.isNativeClass(c); + var hasIterator = false; + + if (c == _coreTypes.objectClass) { + // Dart does not use ES6 constructors. + // Add an error to catch any invalid usage. + jsMethods.add(js_ast.Method( + _propertyName('constructor'), + js.fun(r'''function() { + throw Error("use `new " + # + + ".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. + jsMethods.add(js_ast.Method( + _propertyName('constructor'), js.fun(r'function() { return []; }'))); + } + + var staticFieldNames = {}; + for (var m in c.fields) { + if (m.isStatic) { + staticFieldNames.add(m.name); + } else if (_extensionTypes.isNativeClass(c)) { + jsMethods.addAll(_emitNativeFieldAccessors(m)); + } else if (virtualFields.containsKey(m)) { + jsMethods.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) { + // Static accessors on static/lazy fields are emitted earlier in + // `_emitStaticFieldsAndAccessors`. + if (m.isStatic && m.isAccessor && staticFieldNames.contains(m.name)) { + continue; + } + _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? + jsMethods.addAll(_emitCovarianceCheckStub(m)); + } else if (m.isFactory) { + if (m.isRedirectingFactory) { + // Skip redirecting factories (they've already been resolved). + } else { + jsMethods.add(_emitFactoryConstructor(m)); + } + } else if (m.isAccessor) { + jsMethods.add(_emitMethodDeclaration(m)); + jsMethods.add(_emitSuperAccessorWrapper(m, getters, setters)); + if (!hasJsPeer && m.isGetter && m.name.text == 'iterator') { + hasIterator = true; + jsMethods.add(_emitIterable(c)); + } + } else { + jsMethods.add(_emitMethodDeclaration(m)); + } + _staticTypeContext.leaveMember(m); + } + _currentUri = savedUri; + + // If the type doesn't have an `iterator`, but claims to implement Iterable, + // we inject the adaptor method here, as it's less code size to put the + // helper on a parent class. This pattern is common in the core libraries + // (e.g. IterableMixin 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) { + jsMethods.add(_emitIterable(c)); + } + + // Add all of the super helper methods + jsMethods.addAll(_superHelpers.values); + + return jsMethods.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(superMember.enclosingLibrary.nonNullable)) + as FunctionType; + var function = member.function; + + var body = []; + var typeParameters = superMethodType.typeParameters; + _emitCovarianceBoundsCheck(typeParameters, body); + + var typeFormals = _emitTypeFormals(typeParameters); + 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; + var 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 virtualField = _classProperties!.virtualFields[field]!; + var virtualFieldSymbol = _getSymbol(virtualField); + var name = _declareMemberName(field); + + var getter = js.fun('function() { return this[#]; }', [virtualFieldSymbol]); + var jsGetter = js_ast.Method(name, getter, isGetter: true) + ..sourceInformation = _nodeStart(field); + + var body = []; + var value = _emitIdentifier('value'); + // Avoid adding a null checks on forwarding field setters. + if (field.hasSetter && + _requiresExtraNullCheck(field.setterType, field.annotations)) { + body.add( + _nullSafetyParameterCheck(value, field.location, field.name.text)); + } + 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 = _emitTemporaryId('value'); + fn = js_ast.Fun([value], js.block('{ this.# = #; }', [name, value])); + method = js_ast.Method(_declareMemberName(field), fn, isSetter: true); + jsMethods.add(method); + } + + return jsMethods; + } + + /// Emit a getter (or setter) that simply forwards to the superclass getter + /// (or setter). + /// + /// This is needed because in ES6, if you only override a getter + /// (alternatively, a setter), then there is an implicit override of the + /// setter (alternatively, the getter) that does nothing. + js_ast.Method? _emitSuperAccessorWrapper(Procedure member, + Map 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.#); }', [ + _runtimeModule, + _emitMemberName('iterator', memberClass: _coreTypes.iterableClass) + ]) as js_ast.Fun); + } + + void _registerExtensionType( + Class c, String jsPeerName, List body) { + var className = _emitTopLevelName(c); + if (_typeRep.isPrimitive(_coreTypes.nonNullableRawType(c))) { + body.add(_runtimeStatement( + 'definePrimitiveHashCode(#.prototype)', [className])); + } + body.add(_runtimeStatement( + 'registerExtension(#, #)', [js.string(jsPeerName), className])); + } + + void _emitTopLevelFields(List 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)) { + if (init is ConstructorInvocation) { + // This is an eagerly executed constructor invocation. We need to + // ensure the class is emitted before this statement. + var type = init.getStaticType(_staticTypeContext) as InterfaceType; + _emitClass(type.classNode); + } + _currentUri = field.fileUri; + _moduleItems.add(js.statement('# = #;', [ + _emitTopLevelName(field), + _visitInitializer(init, field.annotations) + ])); + } else { + lazyFields.add(field); + } + _staticTypeContext.leaveMember(field); + } + + _currentUri = savedUri; + fields = lazyFields; + } + + if (fields.isEmpty) return; + _moduleItems.add(_emitLazyMembers( + _emitLibraryName(_currentLibrary!), fields, _emitTopLevelMemberName)); + } + + js_ast.Statement _emitLazyMembers( + js_ast.Expression objExpr, + Iterable members, + js_ast.LiteralString Function(Member) emitMemberName, + ) { + var accessors = []; + var savedUri = _currentUri; + + for (var member in members) { + _currentUri = member.fileUri; + _staticTypeContext.enterMember(member); + var access = emitMemberName(member); + memberNames[member] = access.valueWithoutQuotes; + + if (member is Field) { + accessors.add(js_ast.Method(access, _emitStaticFieldInitializer(member), + isGetter: true) + ..sourceInformation = _hoverComment( + js_ast.PropertyAccess(objExpr, access), + member.fileOffset, + member.name.text.length)); + if (!member.isFinal && !member.isConst) { + var body = []; + var value = _emitIdentifier('value'); + if (_requiresExtraNullCheck(member.setterType, member.annotations)) { + body.add(_nullSafetyParameterCheck( + value, member.location, member.name.text)); + } + // Even when no null check is present a dummy setter is still required + // to indicate writeable. + accessors.add(js_ast.Method( + access, js_ast.Fun([value], js_ast.Block(body)), + isSetter: true)); + } + } else if (member is Procedure) { + accessors.add(js_ast.Method( + access, _emitFunction(member.function, member.name.text), + isGetter: member.isGetter, isSetter: member.isSetter) + ..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); + } + _currentUri = savedUri; + + return _runtimeStatement( + 'defineLazy(#, { # }, #)', [objExpr, accessors, js.boolean(false)]); + } + + js_ast.Fun _emitStaticFieldInitializer(Field field) { + return js_ast.Fun([], js_ast.Block(_withLetScope(() { + return [ + js_ast.Return(_visitInitializer(field.initializer, field.annotations)) + ]; + }))); + } + + 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, {String suffix = ''}) { + return _emitJSInterop(n) ?? + _emitTopLevelNameNoExternalInterop(n, suffix: suffix); + } + + /// Like [_emitMemberName], but for declaration sites. + /// + /// Unlike call sites, we always have an element available, so we can use it + /// directly rather than computing the relevant options for [_emitMemberName]. + js_ast.Expression _declareMemberName(Member m, {bool? useExtension}) { + var c = m.enclosingClass; + return _emitMemberName(m.name.text, + isStatic: m is Field ? m.isStatic : (m as Procedure).isStatic, + useExtension: + useExtension ?? c != null && _extensionTypes.isNativeClass(c), + 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 += '_'; + break; + default: + // All trailing underscores static names are reserved for the compiler + // or SDK libraries. + // + // If user code uses them, add an extra `_`. + // + // This also avoids collision with the renames above, e.g. `static as` + // and `static as_` will become `as_` and `as__`. + if (name.endsWith('_')) { + name += '_'; + } + } + return _propertyName(name); + } + + /// If [f] is a function passed to JS, make it throw at runtime when called if + /// it isn't wrapped with `allowInterop`. + /// + /// Arguments which are _directly_ wrapped at the site they are passed are + /// unmodified. + Expression _assertInterop(Expression f) { + // 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 = ''}) { + // Some native tests use top-level native methods. + var isTopLevelNative = n is Member && isNative(n); + return js_ast.PropertyAccess( + isTopLevelNative + ? _runtimeCall('global.self') + : _emitLibraryName(getLibrary(n)), + _emitTopLevelMemberName(n, suffix: suffix)); + } + + /// 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!; + } + + void _emitLibraryProcedures(Library library) { + var procedures = library.procedures + .where((p) => + !p.isExternal && !p.isAbstract && !_isStaticInteropTearOff(p)) + .toList(); + _moduleItems.addAll(procedures + .where((p) => !p.isAccessor) + .map(_emitLibraryFunction) + .toList()); + _emitLibraryAccessors(procedures.where((p) => p.isAccessor).toList()); + } + + /// 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; + } + + void _emitLibraryAccessors(Iterable accessors) { + if (accessors.isEmpty) return; + _moduleItems.add(_runtimeStatement('copyProperties(#, { # })', [ + _emitLibraryName(_currentLibrary!), + accessors.map(_emitLibraryAccessor).toList() + ])); + } + + js_ast.Method _emitLibraryAccessor(Procedure node) { + var savedUri = _currentUri; + _staticTypeContext.enterMember(node); + _currentUri = node.fileUri; + + var name = node.name.text; + 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 = _emitTemporaryId(jsName); + procedureIdentifiers[p] = functionName; + body.add(js.statement( + '# = #', [nameExpr, js_ast.NamedFunction(functionName, fn)])); + + _currentUri = savedUri; + _staticTypeContext.leaveMember(p); + return js_ast.Statement.from(body); + } + + /// Choose a safe name for [fn]. + /// + /// Most of the time we use [candidateName], except if the name collides + /// with a parameter name and the function contains default parameter values. + /// + /// In ES6, functions containing default parameter values, which DDC + /// generates when Dart uses positional optional parameters, cannot have + /// two parameters with the same name. Because we have a similar restriction + /// in Dart, this is not normally an issue we need to pay attention to. + /// However, a bug in Safari makes it a syntax error to have the function + /// name overlap with the parameter names as well. This rename works around + /// such bug (dartbug.com/43520). + static String _safeFunctionNameForSafari( + String candidateName, js_ast.Fun fn) { + if (fn.params.any((p) => p is js_ast.DestructuredVariable)) { + while (fn.params.any((a) => a.parameterName == candidateName)) { + candidateName = '$candidateName\$'; + } + } + return candidateName; + } + + js_ast.Expression _emitFunctionTagged(js_ast.Expression fn, FunctionType type, + {bool topLevel = false}) { + var lazy = topLevel && !_canEmitTypeAtTopLevel(type); + var typeRep = _emitType( + // Avoid tagging a closure as Function? or Function* + type.withDeclaredNullability(Nullability.nonNullable)); + if (type.typeParameters.isEmpty) { + return _runtimeCall(lazy ? 'lazyFn(#, #)' : 'fn(#, #)', [fn, typeRep]); + } else { + var typeParameterDefaults = [ + for (var parameter in type.typeParameters) + _emitType(parameter.defaultType) + ]; + var defaultInstantiatedBounds = + _emitConstList(const DynamicType(), typeParameterDefaults); + return _runtimeCall( + 'gFn(#, #, #)', [fn, typeRep, defaultInstantiatedBounds]); + } + } + + /// Whether the expression for [type] can be evaluated at this point in the JS + /// module. + /// + /// Types cannot be evaluated if they depend on something that hasn't been + /// defined yet. For example: + /// + /// C foo() => null; + /// class C {} + /// + /// If we're emitting the type information for `foo`, we cannot refer to `C` + /// yet, so we must evaluate foo's type lazily. + bool _canEmitTypeAtTopLevel(DartType type) { + switch (type) { + case InterfaceType(): + return !_pendingClasses!.contains(type.classNode) && + type.typeArguments.every(_canEmitTypeAtTopLevel); + case FutureOrType(): + return !_pendingClasses!.contains(_coreTypes.deprecatedFutureOrClass) && + _canEmitTypeAtTopLevel(type.typeArgument); + case FunctionType(): + // Generic functions are always safe to emit, because they're lazy until + // type arguments are applied. + if (type.typeParameters.isNotEmpty) return true; + return _canEmitTypeAtTopLevel(type.returnType) && + type.positionalParameters.every(_canEmitTypeAtTopLevel) && + type.namedParameters.every((n) => _canEmitTypeAtTopLevel(n.type)); + case RecordType(): + return type.positional.every(_canEmitTypeAtTopLevel) && + type.named.every((n) => _canEmitTypeAtTopLevel(n.type)); + case TypedefType(): + return type.typeArguments.every(_canEmitTypeAtTopLevel); + case ExtensionType(): + return _canEmitTypeAtTopLevel(type.extensionTypeErasure); + case DynamicType(): + case VoidType(): + case NeverType(): + case NullType(): + case IntersectionType(): + case TypeParameterType(): + case StructuralParameterType(): + return true; + case AuxiliaryType(): + throwUnsupportedAuxiliaryType(type); + case InvalidType(): + throwUnsupportedInvalidType(type); + } + } + + /// 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(): + return js + .call('#._Universe.eval(#._theUniverse(), "$recipe", true)', [ + _emitLibraryName(_rtiLibrary), + _emitLibraryName(_rtiLibrary), + ]); + 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; + // Create a dummy interface type to "hold" type arguments. + env = + emitRtiEval(_emitTypeParameter(environmentTypes.first), '@<0>'); + // Bind remaining type arguments. + for (var i = 1; i < environmentTypes.length; i++) { + env = emitRtiBind(env, environmentTypes[i]); + } + } + return emitRtiEval(env, recipe); + case RtiTypeEnvironment(): + // RTI type environments are already constructed and attached to the + // provided RTI. + 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 ExtendedClassTypeEnvironment(): + // 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. + js_ast.Expression _emitClassRef(InterfaceType type) { + var cls = type.classNode; + _declareBeforeUse(cls); + if (!_emittingClassExtends && type.typeArguments.isNotEmpty) { + var genericName = _emitTopLevelNameNoExternalInterop(type.classNode); + return js.call('#', [genericName]); + } + return _emitTopLevelNameNoExternalInterop(type.classNode); + } + + Never _typeCompilationError(DartType type, String description) => + throw UnsupportedError('$description Encountered while compiling ' + '${_currentLibrary!.fileUri}, which contains the type: $type.'); + + bool get _emittingClassExtends => + _currentClass != null && identical(_currentClass, _classEmittingExtends); + + /// 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) { + _declareBeforeUse(c); + 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(); + _genericClassTable.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(), + ..._genericClassTable.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(#, { # }, false)', [_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 = _emitTemporaryId('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); + break; + 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); + break; + 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); + break; + } + 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< /*TypeParameter | StructuralParameter */ Object> 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; + if (_isDartLibrary(_currentLibrary!, '_rti') || + _isSdkInternalRuntime(_currentLibrary!)) { + _nullableInference.treatDeclaredTypesAsSound = true; + } + _nullableInference.enterFunction(fn); + var result = _withLetScope(action); + _nullableInference.exitFunction(fn); + _nullableInference.treatDeclaredTypesAsSound = false; + + _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; + + /// Returns `true` when an additional null check is needed because of the + /// null safety compile mode, the null safety migration status of the current + /// library and the provided [type] with its [annotations]. + bool _requiresExtraNullCheck(DartType type, List annotations) => + !_options.soundNullSafety && + // Libraries that haven't been migrated to null safety represent + // non-nullable as legacy. + _currentLibrary!.nonNullable == Nullability.nonNullable && + _mustBeNonNullable(type) && + !_annotatedNotNull(annotations) && + // Trust the nullability of types in the dart:_rti library. + !_isDartLibrary(_currentLibrary!, '_rti'); + + /// Returns a null check for [value] that if fails produces an error message + /// containing the [location] and [name] of the original value being checked. + /// + /// This is used to generate checks for non-nullable parameters when running + /// with weak null safety. The checks can be silent, warn, or throw, depending + /// on the flags set in the SDK at runtime. + js_ast.Statement _nullSafetyParameterCheck( + js_ast.Identifier value, Location? location, String? name) { + // TODO(nshahan): Remove when weak mode null safety assertions are no longer + // supported. + // The check on `field.setterType` is per: + // https://github.com/dart-lang/language/blob/master/accepted/2.12/nnbd/feature-specification.md#automatic-debug-assertion-insertion + var condition = js.call('# == null', [value]); + // Offsets are not available for compiler-generated variables + // Get the best available location even if the offset is missing. + // https://github.com/dart-lang/sdk/issues/34942 + return js.statement(' if (#) #;', [ + condition, + _runtimeCall('nullFailed(#, #, #, #)', [ + location != null + ? _cacheUri(location.file.toString()) + : js_ast.LiteralNull(), + js.number(location?.line ?? -1), + js.number(location?.column ?? -1), + js.escapedString('$name') + ]) + ]); + } + + /// 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) { + // 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)) { + 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)); + } else if (_requiresExtraNullCheck(p.type, p.annotations)) { + body.add(_nullSafetyParameterCheck(jsParam, p.location, p.name)); + } + } + + for (var p in f.positionalParameters) { + var jsParam = _emitVariableRef(p); + if (_checkParameters) { + initParameter(p, jsParam); + } + } + for (var p in f.namedParameters) { + // Parameters will be passed using their real names, not the (possibly + // renamed) local variable. + var jsParam = _emitVariableDef(p); + var paramName = _propertyName(p.name!); + var defaultValue = _defaultParamValue(p); + body.add(js.statement('let # = # && # ? #.# : #;', [ + jsParam, + _namedArgumentTemp, + _namedArgumentProbe(paramName), + _namedArgumentTemp, + paramName, + defaultValue, + ])); + + if (_checkParameters) { + initParameter(p, jsParam); + } + } + + // '_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 _annotatedNotNull(List annotations) => + annotations.any(_nullableInference.isNotNullAnnotation); + + 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< /* TypeParameter | StructuralParameter */ Object> typeFormals, + List body) { + assert(typeFormals is List || + typeFormals is List); + for (var t in typeFormals) { + bool? isCovariantByClass; + DartType bound; + String name; + DartType typeParameterType; + if (t is TypeParameter) { + isCovariantByClass = t.isCovariantByClass; + bound = t.bound.extensionTypeErasure; + name = t.name!; + typeParameterType = TypeParameterType(t, Nullability.undetermined); + } else { + t as StructuralParameter; + bound = t.bound.extensionTypeErasure; + name = t.name!; + typeParameterType = + StructuralParameterType(t, Nullability.undetermined); + } + + if (isCovariantByClass != null && + isCovariantByClass && + !_types.isTop(bound)) { + body.add(_runtimeStatement('checkTypeBound(#, #, #)', [ + _emitType(typeParameterType), + _emitType(bound), + _propertyName(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 jsBody = _visitStatement(f.body!); + return _emitScopedBody(f, jsBody); + } + + 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.boolRawType(_currentLibrary!.nonNullable)); + 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(); + } + } + return _visitExpression(expr).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.boolLegacyRawType && + 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(VariableDeclaration v) => + js_ast.VariableInitialization(_emitVariableDef(v), + _visitInitializer(v.initializer, v.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 initialziers 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 + /// initalized 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 = _emitTemporaryId('t#_init'); + var loopVariableIds = { + for (var variable in node.variables) variable: _emitVariableDef(variable), + }; + var prevVariableTempIds = { + for (var variable in node.variables) + variable: _emitTemporaryId('t#_prev_${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 variable in node.variables) + js.statement('# = #;', [ + loopVariableIds[variable]!, + _visitInitializer(variable.initializer, variable.annotations) + ]), + ]); + var prevInits = js_ast.Block([ + // Intialize fresh loop variables with the value from the previous + // iteration. + for (var variable in node.variables) + js.statement('# = #;', + [loopVariableIds[variable], prevVariableTempIds[variable]]), + // Original update expressions. + for (var update in node.updates) _visitExpression(update).toStatement(), + ]); + return js_ast.Block([ + // Create temporary variables for the intialization flag and previous + // loop variables. + js_ast.VariableDeclarationList('let', [ + js_ast.VariableInitialization(initFlagTempId, js_ast.LiteralBool(true)), + for (var variable in node.variables) + js_ast.VariableInitialization(prevVariableTempIds[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 variable in node.variables) + js_ast.VariableInitialization( + loopVariableIds[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 variable in node.variables) + js.statement('# = #;', + [prevVariableTempIds[variable]!, _emitVariableRef(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(variable), + ])) + // The while loop gets mapped to the orginal 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 = _emitTemporaryId('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.rawType( + _asyncStreamIteratorClass, _currentLibrary!.nonNullable); + 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 = _emitTemporaryId('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 = _emitTemporaryId('labelState'); + // TODO(markzipan): Retrieve the real label name with source offsets + var labelName = 'SL${_switchLabelStates.length}'; + _switchLabelStates[node] = _SwitchLabelState(labelName, labelState); + + for (var c in node.cases) { + var subcases = + _visitSwitchCase(c, lastSwitchCase: c == node.cases.last); + if (subcases.isNotEmpty) cases.addAll(subcases); + } + + 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(_currentLibrary!.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.sourceInformation = _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.Identifier _emitVariableRef(VariableDeclaration v) { + if (_isTemporaryVariable(v)) { + var name = _debuggerFriendlyTemporaryVariableName(v); + name ??= 't\$${_tempVariables.length}'; + return _tempVariables.putIfAbsent(v, () => _emitTemporaryId(name!)); + } + 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 _emitIdentifier(name); + } + + /// Emits the declaration of a variable. + /// + /// This is similar to [_emitVariableRef] but it also attaches source + /// location information, so hover will work as expected. + js_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 _runtimeCall('dload$_replSuffix(#, #)', [jsReceiver, jsMemberName]); + } + + @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 (_isObjectGetter(memberName) && + _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); + 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 (_isObjectMethodTearoff(memberName) && + _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('bind(#, #)', [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) { + return _emitPropertySet(node.receiver, null, node.value, node.name.text); + } + + @override + js_ast.Expression visitInstanceSet(InstanceSet node) { + return _emitPropertySet( + node.receiver, node.interfaceTarget, node.value, node.name.text); + } + + /// 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 _options.soundNullSafety && + 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' : ''; + + js_ast.Expression _emitPropertySet(Expression receiver, Member? member, + Expression value, String memberName) { + var jsName = _emitMemberName(memberName, member: member); + + if (member != null && isJsMember(member)) { + value = _assertInterop(value); + } + + var jsReceiver = _visitExpression(receiver); + var jsValue = _visitExpression(value); + + if (member == null) { + return _runtimeCall( + 'dput$_replSuffix(#, #, #)', [jsReceiver, jsName, jsValue]); + } + return js.call('#.# = #', [jsReceiver, jsName, jsValue]); + } + + @override + js_ast.Expression visitAbstractSuperPropertyGet( + AbstractSuperPropertyGet node) { + return _emitSuperPropertyGet(node.interfaceTarget); + } + + @override + js_ast.Expression visitSuperPropertyGet(SuperPropertyGet node) { + return _emitSuperPropertyGet(node.interfaceTarget); + } + + js_ast.Expression _emitSuperPropertyGet(Member target) { + if (_reifyTearoff(target)) { + if (_superAllowed) { + var jsTarget = _emitSuperTarget(target); + return _runtimeCall('bind(this, #, #)', [jsTarget.selector, jsTarget]); + } else { + return _emitSuperTearoff(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 result = _emitStaticTarget(target); + if (_reifyTearoff(target)) { + // TODO(jmesserly): we could tag static/top-level function types once + // in the module initialization, rather than at the point where they + // escape. + return _emitFunctionTagged( + result, + target.function! + .computeThisFunctionType(target.enclosingLibrary.nonNullable)); + } + 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 _emitMethodCall(node.receiver, null, node.arguments, node); + } + + @override + js_ast.Expression visitFunctionInvocation(FunctionInvocation node) { + return _emitMethodCall(node.receiver, null, node.arguments, node); + } + + @override + js_ast.Expression visitInstanceInvocation(InstanceInvocation node) { + var invocation = _emitMethodCall( + node.receiver, node.interfaceTarget, node.arguments, node); + return _isNullCheckableJsInterop(node.interfaceTarget) + ? _wrapWithJsInteropNullCheck(invocation) + : invocation; + } + + @override + js_ast.Expression visitInstanceGetterInvocation( + InstanceGetterInvocation node) { + var getterInvocation = _emitMethodCall( + node.receiver, node.interfaceTarget, node.arguments, node); + return _isNullCheckableJsInterop(node.interfaceTarget) + ? _wrapWithJsInteropNullCheck(getterInvocation) + : getterInvocation; + } + + @override + js_ast.Expression visitLocalFunctionInvocation(LocalFunctionInvocation node) { + return _emitMethodCall( + VariableGet(node.variable)..fileOffset = node.fileOffset, + null, + node.arguments, + node); + } + + @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); + } + + js_ast.Expression _emitMethodCall(Expression receiver, Member? target, + Arguments arguments, InvocationExpression node) { + var name = node.name.text; + + /// Returns `true` when [node] represents an invocation of `List.add()` that + /// can be optimized. + /// + /// The optimized add operation can skip checks for a growable or modifiable + /// list and the element type is known to be invariant so it can skip the + /// type check. + bool isNativeListInvariantAdd(InvocationExpression node) { + if (node is InstanceInvocation && + node.isInvariant && + node.name.text == 'add') { + // The call to add is marked as invariant, so the type check on the + // parameter to add is not needed. + var receiver = node.receiver; + if (receiver is VariableGet && + receiver.variable.isFinal && + !receiver.variable.isLate) { + // The receiver is a final variable, so it only contains the + // initializer value. Also, avoid late variables in case the CFE + // lowering of late variables is changed in the future. + var initializer = receiver.variable.initializer; + if (initializer is ListLiteral) { + // The initializer is a list literal, so we know the list can be + // grown, modified, and is represented by a JavaScript Array. + return true; + } + if (initializer is StaticInvocation && + initializer.target.enclosingClass == _coreTypes.listClass && + initializer.target.name.text == 'of' && + initializer.arguments.named.isEmpty) { + // The initializer is a `List.of()` call from the dart:core library + // and the growable named argument has not been passed (it defaults + // to true). + return true; + } + } + } + return false; + } + + if (isOperatorMethodName(name) && arguments.named.isEmpty) { + var argLength = arguments.positional.length; + if (argLength == 0) { + return _emitUnaryOperator(receiver, target, node); + } else if (argLength == 1) { + return _emitBinaryOperator( + receiver, target, arguments.positional[0], node); + } + } + + var jsReceiver = _visitExpression(receiver); + var args = _emitArgumentList(arguments, target: target); + + if (isNativeListInvariantAdd(node)) { + return js.call('#.push(#)', [jsReceiver, args]); + } + + var isCallingDynamicField = target is Member && + target.hasGetter && + // Erasing extension types here doesn't make sense. If there is an + // extension type on dynamic or Function it will only be callable if it + // defines a call method which would be invoked statically. + _isDynamicOrFunction(target.getterType); + if (name == 'call') { + // Erasing the extension types here to support existing callable behaivor + // 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 = + receiver.getStaticType(_staticTypeContext).extensionTypeErasure; + if (isCallingDynamicField || _isDynamicOrFunction(receiverType)) { + return _emitDynamicInvoke(jsReceiver, null, args, arguments); + } else if (_isDirectCallable(receiverType)) { + // Call methods on function types should be handled as function calls. + return js_ast.Call(jsReceiver, args); + } + } + + var jsName = _emitMemberName(name, member: target); + + // Handle Object methods that are supported by `null` and potentially + // JavaScript interop values. + if (_isObjectMethodCall(name, arguments)) { + if (_shouldCallObjectMemberHelper(receiver)) { + // The names of the static helper methods in the runtime must match the + // names of the Object instance members. + return _runtimeCall('#(#, #)', [name, jsReceiver, args]); + } + // Otherwise generate this as a normal typed method call. + } else if (target == null || isCallingDynamicField) { + return _emitDynamicInvoke(jsReceiver, jsName, args, arguments); + } + // TODO(jmesserly): remove when Kernel desugars this for us. + // Handle `o.m(a)` where `o.m` is a getter returning a class with `call`. + if (target is Field || target is Procedure && target.isAccessor) { + // We must erase the extension type to 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 becuase: + // - Extension types that do not implment 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 fromType = target!.getterType.extensionTypeErasure; + if (fromType is InterfaceType) { + var callName = _implicitCallTarget(fromType); + if (callName != null) { + return js.call('#.#.#(#)', [jsReceiver, jsName, callName, args]); + } + } + } + return js.call('#.#(#)', [jsReceiver, jsName, args]); + } + + js_ast.Expression _emitDynamicInvoke( + js_ast.Expression fn, + js_ast.Expression? methodName, + Iterable args, + Arguments arguments) { + var jsArgs = [fn]; + String jsCode; + + var typeArgs = arguments.types; + if (typeArgs.isNotEmpty) { + jsArgs.add(args.take(typeArgs.length)); + args = args.skip(typeArgs.length); + if (methodName != null) { + jsCode = 'dgsend$_replSuffix(#, [#], #'; + jsArgs.add(methodName); + } else { + jsCode = 'dgcall(#, [#]'; + } + } else if (methodName != null) { + jsCode = 'dsend$_replSuffix(#, #'; + jsArgs.add(methodName); + } else { + jsCode = 'dcall(#'; + } + + var hasNamed = arguments.named.isNotEmpty; + if (hasNamed) { + jsCode += ', [#], #)'; + jsArgs.add(args.take(args.length - 1)); + jsArgs.add(args.last); + } else { + jsArgs.add(args); + 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; + } + + bool _isDynamicOrFunction(DartType t) => + DartTypeEquivalence(_coreTypes, ignoreTopLevelNullability: true) + .areEqual(t, _coreTypes.functionNonNullableRawType) || + t == const DynamicType(); + + 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 = _emitMemberName(member.name.text, member: member); + // Optimize access to non-virtual fields, if allowed in the current context. + 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 [_emitSuperTearoff]. + 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(_emitTemporaryId(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(_emitTemporaryId(name), fn); + } + }); + return js_ast.PropertyAccess(js_ast.This(), jsMethod.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 _emitSuperTearoff(Member member) { + var jsName = _emitMemberName(member.name.text, member: member); + var name = '_#super#tearOff#${member.name.text}'; + var jsMethod = _superHelpers.putIfAbsent(name, () { + var jsReturnValue = + _runtimeCall('bind(this, #, super[#])', [jsName, jsName]); + var fn = js.fun('function() { return #; }', [jsReturnValue]); + name = js_ast.friendlyNameForDartOperator[name] ?? name; + return js_ast.Method(_emitTemporaryId(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 (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 && typeArgs.length == 1) { + if (name == 'TYPE_REF') { + return _emitType(typeArgs.single); + } + if (name == 'LEGACY_TYPE_REF') { + return _emitType( + typeArgs.single.withDeclaredNullability(Nullability.legacy)); + } + } + + 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}'); + } + if (type.typeArguments.isNotEmpty) { + throw UnsupportedError( + 'JS_CLASS_REF does not support type arguments: found ' + '${type.typeArguments} 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), + 'PRINT_LEGACY_STARS' => js.boolean(_options.printLegacyStars), + 'LEGACY' => _options.soundNullSafety + ? js.boolean(false) + // When running the new runtime type system with weak null + // safety this flag gets toggled when performing `is` and `as` + // checks. This allows DDC to produce optional warnings or + // errors when tests pass but would fail in sound null safety. + : _runtimeCall('legacyTypeChecks'), + 'SOUND_NULL_SAFETY' => js.boolean(_options.soundNullSafety), + 'EXTRA_NULL_SAFETY_CHECKS' => _options.soundNullSafety + ? js.boolean(false) + // When running the new runtime type system with weak null + // safety this flag gets toggled when performing `is` and `as` + // checks. This allows DDC to produce optional warnings or + // errors when tests pass but would fail in sound null safety. + : _runtimeCall('extraNullSafetyChecks'), + '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.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); + return _emitObjectLiteral( + Arguments(node.arguments.positional, + types: node.arguments.types, named: node.arguments.named), + target); + } + 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 (RegExp(r'^\_callMethodUnchecked(TrustType)?[0-4]') + .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 (RegExp(r'^\_callConstructorUnchecked[0-4]').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]); + } + } + + var fn = _emitStaticTarget(target); + var args = _emitArgumentList(node.arguments, target: target); + var staticCall = js_ast.Call(fn, args); + return _isNullCheckableJsInterop(target) + ? _wrapWithJsInteropNullCheck(staticCall) + : staticCall; + } + + 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.Expression _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 memberName = _annotationName(target, isJSName) ?? + _emitStaticMemberName(target.name.text, target); + return _runtimeCall('global.#.#', [nativeName[0], memberName]); + } + } + return js_ast.PropertyAccess(_emitStaticClassName(c, isExternal), + _emitStaticMemberName(target.name.text, target)); + } + return _emitTopLevelName(target); + } + + List _emitArgumentList(Arguments node, + {bool types = true, Member? target}) { + types = types && _reifyGenericFunction(target); + final isJsInterop = target != null && isJsMember(target); + return [ + if (types) + for (var typeArg in node.types) _emitType(typeArg), + 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), + if (node.named.isNotEmpty) + js_ast.ObjectInitializer([ + for (var arg in node.named) _emitNamedExpression(arg, isJsInterop) + ]), + ]; + } + + js_ast.Property _emitNamedExpression(NamedExpression arg, + [bool isJsInterop = false]) { + var value = isJsInterop ? _assertInterop(arg.value) : arg.value; + return js_ast.Property(_propertyName(arg.name), _visitExpression(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) && _options.soundNullSafety) { + 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) { + if (_isDartLibrary(_currentLibrary!, '_rti') || + _isSdkInternalRuntime(_currentLibrary!)) { + _nullableInference.treatDeclaredTypesAsSound = true; + } + final result = _nullableInference.isNullable(expr); + _nullableInference.treatDeclaredTypesAsSound = false; + return result; + } + + 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)) return _emitObjectLiteral(args, ctor); + // 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), + [ + if (rti != null) 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]); + } + break; + 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]); + } + break; + case 'List': + if (ctor.name.text == '') { + return _emitList(type.typeArguments[0], []); + } + break; + } + } + var rti = _requiresRtiForInstantiation(ctorClass) + ? _emitType(type, emitJSInteropGenericClassTypeParametersAsAny: false) + : null; + var result = js_ast.Call(_emitConstructorName(type, ctor), + [if (rti != null) 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)) return _emitObjectLiteral(args, ctor); + // 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); + } + + js_ast.Expression _emitObjectLiteral(Arguments node, Member ctor) { + var args = _emitArgumentList(node, types: false, target: ctor); + if (args.isEmpty) return js.call('{}'); + assert(args.single is js_ast.ObjectInitializer); + return args.single; + } + + @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 visitListConcatenation(ListConcatenation node) { + // Only occurs inside unevaluated constants. + throw UnsupportedError('List concatenation'); + } + + @override + js_ast.Expression visitSetConcatenation(SetConcatenation node) { + // Only occurs inside unevaluated constants. + throw UnsupportedError('Set concatenation'); + } + + @override + js_ast.Expression visitMapConcatenation(MapConcatenation node) { + // Only occurs inside unevaluated constants. + throw UnsupportedError('Map concatenation'); + } + + @override + js_ast.Expression visitInstanceCreation(InstanceCreation node) { + // Only occurs inside unevaluated constants. + throw UnsupportedError('Instance creation'); + } + + @override + js_ast.Expression visitFileUriExpression(FileUriExpression node) { + // Only occurs inside unevaluated constants. + throw UnsupportedError('File URI expression'); + } + + @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; + var to = node.type.extensionTypeErasure; + var from = fromExpr.getStaticType(_staticTypeContext).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); + // } + // + var isTypeError = node.isTypeError; + if (!isTypeError && + _types.isSubtypeOf(from, to, SubtypeCheckMode.withNullabilities)) { + return jsFrom; + } + + if (!isTypeError && + DartTypeEquivalence(_coreTypes, ignoreTopLevelNullability: true) + .areEqual(from, to) && + _mustBeNonNullable(to)) { + // If the underlying type is the same, we only need a null check. + return _runtimeCall('nullCast(#, #)', [jsFrom, _emitType(to)]); + } + + // All Dart number types map to a JS double. We can specialize these + // cases. + if (_typeRep.isNumber(from) && _typeRep.isNumber(to)) { + // If `to` is some form of `num`, it should have been filtered above. + + // * -> double? | double* : no-op + if (to == _coreTypes.doubleLegacyRawType || + to == _coreTypes.doubleNullableRawType) { + return jsFrom; + } + + // * -> double : null check + if (to == _coreTypes.doubleNonNullableRawType) { + if (from.nullability == Nullability.nonNullable) { + return jsFrom; + } + return _runtimeCall('nullCast(#, #)', [jsFrom, _emitType(to)]); + } + + // * -> int : asInt check + if (to == _coreTypes.intNonNullableRawType) { + return _runtimeCall('asInt(#)', [jsFrom]); + } + + // * -> int? | int* : asNullableInt check + if (to == _coreTypes.intLegacyRawType || + to == _coreTypes.intNullableRawType) { + return _runtimeCall('asNullableInt(#)', [jsFrom]); + } + } + + return _emitCast(jsFrom, to); + } + + js_ast.Expression _emitCast(js_ast.Expression expr, DartType type) { + var normalizedType = type.extensionTypeErasure; + if (_types.isTop(normalizedType)) return expr; + return js.call('#.#(#)', [ + _emitType(normalizedType), + _emitMemberName(js_ast.FixedNames.rtiAsField, memberClass: _rtiClass), + expr + ]); + } + + @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) { + // dart.constList helper internally depends on _interceptors.JSArray. + _declareBeforeUse(_jsArrayClass); + 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 fn = _emitArrowFunction(node); + 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); + 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. DDC emits type literals in two contexts: + // * Foreign JS functions: we use the non-nullable version of some types + // directly in the runtime libraries (e.g. dart:_runtime). For + // correctness of those libraries, we need to remove the legacy marker + // that was added by the CFE normalization of type literals. + // + // * Regular user code: we need to emit a canonicalized type. We do so + // by calling `wrapType` on the type at runtime. By emitting the + // non-nullable version we save some redundant work at runtime. + // Technically, emitting a legacy type in this case would be correct, + // only more verbose and inefficient. + var type = node.type; + if (type.nullability == Nullability.legacy) { + type = type.withDeclaredNullability(Nullability.nonNullable); + } + assert(!_isInForeignJS || + type.nullability == Nullability.nonNullable || + // The types dynamic, void, and Null all intrinsically have + // `Nullability.nullable` but are handled explicitly without emitting + // the nullable runtime wrapper. They are safe to allow through + // unchanged. + type == const DynamicType() || + type == const NullType() || + type == const VoidType()); + return _emitTypeLiteral(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); + + @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) + ] + ]); + } + + @override + js_ast.Expression visitInstanceConstant(InstanceConstant node) { + _declareBeforeUse(node.classNode); + var savedTypeEnvironment = _currentTypeEnvironment; + if (node.classNode.typeParameters.isNotEmpty) { + _currentTypeEnvironment = + ClassTypeEnvironment(node.classNode.typeParameters); + } + + js_ast.Property entryToProperty(MapEntry entry) { + var constant = visitConstant(entry.value); + var member = entry.key.asField; + var cls = member.enclosingClass!; + // Enums cannot be overridden, so we can safely use the field name + // directly. Otherwise, use a private symbol in case the field + // was overridden. + var symbol = cls.isEnum + ? _emitMemberName(member.name.text, member: member) + : _getSymbol(_emitClassPrivateNameSymbol( + cls.enclosingLibrary, getLocalClassName(cls), member)); + return js_ast.Property(symbol, constant); + } + + // Non-nullable is forced here because the type of an instance constant + // should never appear as legacy "*" at runtime but the library where the + // constant is defined can cause those types to appear here. + var type = node + .getType(_staticTypeContext) + .withDeclaredNullability(Nullability.nonNullable); + 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.TemporaryId _emitClassPrivateNameSymbol( + Library library, String className, Member member, + [js_ast.TemporaryId? 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) { + _declareBeforeUse(node.target.enclosingClass); + 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; + } + + /// Creates header comments with helpful compilation information. + List _generateCompilationHeader() { + var headerOptions = [ + if (_options.canaryFeatures) 'canary', + 'soundNullSafety(${_options.soundNullSafety})', + '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).'), + 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; + } + + @override + js_ast.Statement visitIfCaseStatement(IfCaseStatement node) { + // This node is internal to the front end and removed by the constant + // evaluator. + throw UnsupportedError('ProgramCompiler.visitIfCaseStatement'); + } + + @override + js_ast.Expression visitPatternAssignment(PatternAssignment node) { + // This node is internal to the front end and removed by the constant + // evaluator. + throw UnsupportedError('ProgramCompiler.visitPatternAssignment'); + } + + @override + js_ast.Statement visitPatternSwitchStatement(PatternSwitchStatement node) { + // This node is internal to the front end and removed by the constant + // evaluator. + throw UnsupportedError('ProgramCompiler.visitPatternSwitchStatement'); + } + + @override + js_ast.Statement visitPatternVariableDeclaration( + PatternVariableDeclaration node) { + // This node is internal to the front end and removed by the constant + // evaluator. + throw UnsupportedError('ProgramCompiler.visitPatternVariableDeclaration'); + } + + @override + js_ast.Expression visitSwitchExpression(SwitchExpression node) { + // This node is internal to the front end and removed by the constant + // evaluator. + throw UnsupportedError('ProgramCompiler.visitSwitchExpression'); + } + + @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}).'); + } + + /// Adds an import mapping from [library] to [id]. + /// + /// This is a temporary work around until imports can be manually added in + /// `startModule()`. + void _forceLibraryImport(Library library, js_ast.TemporaryId id) { + _imports[library] = id; + } + + 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.TemporaryId((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]) { + _setEmitIfIncremental(_libraryToModule(_coreLibrary), _runtimeModule.name); + return js.call('#.$code', [_runtimeModule, ...?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.TemporaryId _emitPrivateNameSymbol(Library library, String name, + [js_ast.TemporaryId? 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.TemporaryId initPrivateNameSymbol() { + var idName = name.endsWith('=') ? name.replaceAll('=', '_') : name; + idName = idName.replaceAll(js_ast.invalidCharInIdentifier, '_'); + var identifier = id ?? js_ast.TemporaryId(idName); + _addSymbol( + identifier, + _runtimeCall('privateName(#, #)', + [_emitLibraryName(library), 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(#, #)', [_emitLibraryName(library), js.string(name)]) + ])); + } + return identifier; + } + + var privateNames = _privateNames.putIfAbsent(library, () => HashMap()); + var symbolId = privateNames.putIfAbsent(name, initPrivateNameSymbol); + + _setEmitIfIncrementalLibrary(library); + _setEmitIfIncremental(_libraryToModule(_coreLibrary), _runtimeModule.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], so the Symbol is scoped properly. + js_ast.Expression _emitDartSymbol(String symbolName) { + // 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(_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, [_finishModule] + /// can be called to complete the module and return the resulting JS AST. + /// + /// This also initializes several fields: [_runtimeModule], + /// [_extensionSymbolsModule], and the [_libraries] map needed by + /// [_emitLibraryName]. + List _startModule(Iterable libraries) { + if (_isBuildingSdk) { + // Don't allow these to be renamed when we're building the SDK. + // There is JS code in dart:* that depends on their names. + _runtimeModule = js_ast.Identifier('dart'); + _extensionSymbolsModule = js_ast.Identifier('dartx'); + } else { + // Otherwise allow these to be renamed so users can write them. + _runtimeModule = js_ast.TemporaryId('dart'); + _extensionSymbolsModule = js_ast.TemporaryId('dartx'); + } + + // Initialize our library variables. + var items = []; + var exports = []; + + if (_isBuildingSdk) { + // Bootstrap the ability to create Dart library objects. + var libraryProto = js_ast.TemporaryId('_library'); + items.add(js.statement('const # = Object.create(null)', libraryProto)); + items.add(js.statement( + 'const # = Object.create(#)', [_runtimeModule, libraryProto])); + items.add(js.statement('#.library = #', [_runtimeModule, libraryProto])); + exports.add(js_ast.NameSpecifier(_runtimeModule)); + } + + for (var library in libraries) { + if (_isBuildingSdk && _isSdkInternalRuntime(library)) { + _libraries[library] = _runtimeModule; + continue; + } + var libraryId = _isBuildingSdk && _isDartLibrary(library, '_rti') + ? _rtiLibraryId + : js_ast.TemporaryId(_jsLibraryName(library)); + + _libraries[library] = libraryId; + var alias = _jsLibraryAlias(library); + var aliasId = alias == null ? null : js_ast.TemporaryId(alias); + + // TODO(vsm): Change back to `const`. + // See https://github.com/dart-lang/sdk/issues/40380. + items.add(js.statement( + 'var # = Object.create(#.library)', [libraryId, _runtimeModule])); + exports.add(js_ast.NameSpecifier(libraryId, asName: aliasId)); + } + + // dart:_runtime has a magic module that holds extension method symbols. + // TODO(jmesserly): find a cleaner design for this. + if (_isBuildingSdk) { + var id = _extensionSymbolsModule; + // TODO(vsm): Change back to `const`. + // See https://github.com/dart-lang/sdk/issues/40380. + items.add(js + .statement('var # = Object.create(#.library)', [id, _runtimeModule])); + exports.add(js_ast.NameSpecifier(id)); + } + items.add(js_ast.ExportDeclaration(js_ast.ExportClause(exports))); + + if (_isBuildingSdk) { + // Initialize the private name function. + // To bootstrap the SDK, this needs to be emitted before other code. + var symbol = js_ast.TemporaryId('_privateNames'); + items.add(js.statement('const # = Symbol("_privateNames")', symbol)); + items.add(_runtimeStatement(r''' + privateName = function(library, name) { + let names = library[#]; + if (names == null) names = library[#] = new Map(); + let symbol = names.get(name); + if (symbol == null) names.set(name, symbol = Symbol(name)); + return symbol; + } + ''', [symbol, symbol])); + } + + return items; + } + + /// Returns the canonical name to refer to the Dart library. + js_ast.Identifier _emitLibraryName(Library library) { + _setEmitIfIncrementalLibrary(library); + + // Avoid adding the dart:_runtime to _imports when our runtime unit tests + // import it explicitly. It will always be implicitly imported. + if (_isSdkInternalRuntime(library)) return _runtimeModule; + + // It's either one of the libraries in this module, or it's an import. + return _libraries[library] ?? + _imports.putIfAbsent( + library, () => js_ast.TemporaryId(_jsLibraryName(library))); + } + + /// Emits imports into [items]. + void _emitImports(List items) { + var modules = >{}; + for (var import in _imports.keys) { + modules.putIfAbsent(_libraryToModule(import), () => []).add(import); + } + + String? coreModuleName; + if (!_libraries.containsKey(_coreLibrary)) { + coreModuleName = _libraryToModule(_coreLibrary); + } + + 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 + // + var imports = []; + for (var library in libraries) { + if (!_incrementalMode || + usedLibraries!.contains(_jsLibraryName(library))) { + var alias = _jsLibraryAlias(library); + if (alias != null) { + var aliasId = js_ast.TemporaryId(alias); + imports.add( + js_ast.NameSpecifier(aliasId, asName: _imports[library])); + } else { + imports.add(js_ast.NameSpecifier(_imports[library])); + } + } + } + + if (module == coreModuleName) { + if (!_incrementalMode || + usedLibraries!.contains(_runtimeModule.name)) { + imports.add(js_ast.NameSpecifier(_runtimeModule)); + } + if (!_incrementalMode || + usedLibraries!.contains(_extensionSymbolsModule.name)) { + imports.add(js_ast.NameSpecifier(_extensionSymbolsModule)); + } + } + + if (!_incrementalMode || imports.isNotEmpty) { + items.add(js_ast.ImportDeclaration( + namedImports: imports, from: js.string(module, "'"))); + } + } + }); + } + + /// 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(_extensionSymbolsModule, _propertyName(name)); + if (_isBuildingSdk) { + value = js.call('# = Symbol(#)', [value, js.string('dartx.$name')]); + } else if (forceExtensionSymbols) { + value = js.call( + '# || (# = Symbol(#))', [value, value, js.string('dartx.$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), _extensionSymbolsModule.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 [ProgramCompiler.emitModule]. + /// + /// Example of exports emitted to JavaScript during emitModule: + /// + /// ``` + /// dart.trackLibraries("web/main", { ... }); + /// // 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.TemporaryId(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(module, "'"))); + } + } + + /// Emits imports and extension methods into [items]. + void _emitImportsAndExtensionSymbols(List items, + {bool forceExtensionSymbols = false}) { + _emitImports(items); + _emitExtensionSymbols(items, forceExtensionSymbols: forceExtensionSymbols); + } + + void _emitDebuggerExtensionInfo(String name) { + var properties = []; + var parts = []; + _libraries.forEach((library, value) { + // TODO(jacobr): we could specify a short library name instead of the + // full library uri if we wanted to save space. + var libraryName = js.escapedString(_jsLibraryDebuggerName(library)); + properties.add(js_ast.Property(libraryName, value)); + var partNames = _jsPartDebuggerNames(library); + if (partNames.isNotEmpty) { + parts.add(js_ast.Property(libraryName, js.stringArray(partNames))); + } + }); + var module = js_ast.ObjectInitializer(properties, multiline: true); + var partMap = js_ast.ObjectInitializer(parts, multiline: true); + + // Track the module name for each library in the module. + // This data is consumed by the debugger and by the stack trace mapper. + // + // See also the implementation of this API in the SDK. + _moduleItems.add(_runtimeStatement( + 'trackLibraries(#, #, #, $sourceMapLocationID)', + [js.string(name), module, partMap])); + } + + /// 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 module created by [_startModule], by combining the preamble + /// [items] with the [_moduleItems] that have been emitted. + /// + /// The [moduleName] should specify the module's name, and the items should + /// be the list resulting from startModule, 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 _finishModule(List items, String moduleName, + {List header = const []}) { + // 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 `startModule` and `finishModule` is very similar in both. + _emitDebuggerExtensionInfo(moduleName); + + // Emit all top-level JS symbol containers. + items.addAll(_symbolContainer.emit()); + + // Add the module's code (produced by visiting compilation units, above) + _copyAndFlattenBlocks(items, _moduleItems); + _moduleItems.clear(); + + // Build the module. + return js_ast.Program(items, name: moduleName, header: header); + } + + /// 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.TemporaryId _getExtensionSymbolInternal(String name) { + if (!_extensionSymbols.containsKey(name)) { + var id = js_ast.TemporaryId( + '\$${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'; +} + +bool _isInlineJSFunction(Statement? body) { + var block = body; + if (block is Block) { + var statements = block.statements; + if (statements.length != 1) return false; + body = statements[0]; + } + if (body is ReturnStatement) { + var expr = body.expression; + return expr is StaticInvocation && isInlineJS(expr.target); + } + return false; +} + +/// Return true if this is one of the methods/properties on all Dart Objects +/// (toString, hashCode, noSuchMethod, runtimeType). +/// +/// Operator == is excluded, as it is handled as part of the equality binary +/// operator. +bool _isObjectMember(String name) { + // We could look these up on Object, but we have hard coded runtime helpers + // so it's not really providing any benefit. + switch (name) { + case 'hashCode': + case 'toString': + case 'noSuchMethod': + case 'runtimeType': + case '==': + return true; + } + return false; +} + +bool _isObjectGetter(String name) => + name == 'hashCode' || name == 'runtimeType'; + +bool _isObjectMethodTearoff(String name) => + // "==" isn't in here because there is no syntax to tear it off. + name == 'toString' || name == 'noSuchMethod'; + +bool _isObjectMethodCall(String name, Arguments args) { + if (name == 'toString') { + return args.positional.isEmpty && args.named.isEmpty && args.types.isEmpty; + } else if (name == 'noSuchMethod') { + return args.positional.length == 1 && + args.named.isEmpty && + args.types.isEmpty; + } + return false; +} + +class _SwitchLabelState { + String label; + js_ast.Identifier variable; + + _SwitchLabelState(this.label, this.variable); +}