| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:collection'; | |
| import 'dart:convert'; | |
| import 'dart:io' as io; | |
| import 'dart:math' show max, min; | |
| import 'package:_js_interop_checks/src/js_interop.dart' | |
| show getDartJSInteropJSName, hasDartJSInteropAnnotation; | |
| import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart' | |
| show ExtensionIndex; | |
| import 'package:front_end/src/api_prototype/external_effect.dart' | |
| show ExternalEffect; | |
| import 'package:front_end/src/api_unstable/ddc.dart'; | |
| import 'package:js_shared/synced/embedded_names.dart' show JsGetName, JsBuiltin; | |
| import 'package:kernel/class_hierarchy.dart'; | |
| import 'package:kernel/clone.dart'; | |
| import 'package:kernel/core_types.dart'; | |
| import 'package:kernel/kernel.dart'; | |
| import 'package:kernel/library_index.dart'; | |
| import 'package:kernel/src/dart_type_equivalence.dart'; | |
| import 'package:kernel/type_algebra.dart'; | |
| import 'package:kernel/type_environment.dart'; | |
| import 'package:source_span/source_span.dart' show SourceLocation; | |
| import '../command/options.dart' show Options; | |
| import '../compiler/js_names.dart' as js_ast; | |
| import '../compiler/js_utils.dart' as js_ast; | |
| import '../compiler/module_builder.dart' | |
| show | |
| isSdkInternalRuntimeUri, | |
| libraryUriToImportName, | |
| libraryUriToJsIdentifier; | |
| import '../compiler/module_containers.dart' show ModuleItemContainer; | |
| import '../compiler/rewrite_async.dart'; | |
| import '../js_ast/js_ast.dart' as js_ast; | |
| import '../js_ast/js_ast.dart' show ModuleItem, js; | |
| import '../js_ast/source_map_printer.dart' | |
| show NodeEnd, NodeSpan, HoverComment, continueSourceMap; | |
| import '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'; | |
| /// The groups of compiled invocation arguments ready to be flattened into a | |
| /// single [js_ast.Array] for statically sound calls or into separate packets | |
| /// for runtime checked calls. | |
| typedef _ArgumentGroups = ({ | |
| List<js_ast.Expression>? typeArguments, | |
| List<js_ast.Expression>? positionalArguments, | |
| List<js_ast.Property>? namedArguments, | |
| }); | |
| /// Public API for DDC compilers. | |
| abstract class Compiler { | |
| js_ast.Program emitModule(Component component); | |
| Map<Class, js_ast.Identifier> get classIdentifiers; | |
| Map<Member, String> get memberNames; | |
| Map<Procedure, js_ast.Identifier> get procedureIdentifiers; | |
| Map<Variable, js_ast.Identifier> get variableIdentifiers; | |
| js_ast.Fun emitFunctionIncremental( | |
| List<ModuleItem> items, | |
| Library library, | |
| Class? cls, | |
| FunctionNode functionNode, | |
| String name, | |
| ); | |
| } | |
| class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression> | |
| with | |
| OnceConstantVisitorDefaultMixin<js_ast.Expression>, | |
| StatementVisitorInternalNodeMixin<js_ast.Statement>, | |
| StatementVisitorExperimentExclusionMixin<js_ast.Statement>, | |
| ExpressionVisitorInternalNodeMixin<js_ast.Expression>, | |
| ExpressionVisitorExperimentExclusionMixin<js_ast.Expression> | |
| implements | |
| StatementVisitor<js_ast.Statement>, | |
| ExpressionVisitor<js_ast.Expression>, | |
| Compiler { | |
| final Options _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. | |
| @override | |
| final classIdentifiers = <Class, js_ast.Identifier>{}; | |
| /// Maps every mixin application to a unique identifier. | |
| /// | |
| /// A mixin application is represented as a (mixin, class) pair, where | |
| /// 'mixin' is being mixed into 'class'. Anonymous mixins are already | |
| /// unique per mixin application and so pass themselves in as both 'mixin' | |
| /// and 'class'. | |
| /// | |
| /// This mapping is used when generating super property getters in mixins. | |
| final Map<(Class, Class), js_ast.Identifier> _mixinCache = {}; | |
| /// Records a reference to a mixin application's passed in superclass. | |
| /// (see [_emitMixinStatement]). | |
| final Map<Class, js_ast.Identifier> _mixinSuperclassCache = {}; | |
| /// Maps each class `Member` node compiled in the module to the name used for | |
| /// the member in JavaScript. | |
| /// | |
| /// This mapping is used when generating the symbol information for the | |
| /// module. | |
| @override | |
| final memberNames = <Member, String>{}; | |
| /// 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. | |
| @override | |
| final procedureIdentifiers = <Procedure, js_ast.Identifier>{}; | |
| /// 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. | |
| @override | |
| final variableIdentifiers = <Variable, js_ast.Identifier>{}; | |
| /// Identifiers for kernel variables with an analogous identifier in JS. | |
| /// | |
| /// [Variable.name] is not necessarily a safe identifier for JS | |
| /// transpiled code. The same name can be used in shadowing contexts. We map | |
| /// each kernel variable to a [js_ast.ScopedId] so that at code emission | |
| /// time, declarations that would be shadowed are given a unique name. If | |
| /// there is no risk of shadowing, the original name will be used. | |
| final Map<Variable, js_ast.ScopedId> _variableTempIds = {}; | |
| /// Maps a library URI import, that is not in [_libraries], to the | |
| /// corresponding Kernel summary module we imported it with. | |
| /// | |
| /// An entry must exist for every reachable component. | |
| final Map<Library, Component> _importToSummary; | |
| /// Maps a Kernel summary to the JS import name for the module. | |
| /// | |
| /// An entry must exist for every reachable component. | |
| final Map<Component, String> _summaryToModule; | |
| /// The variable for the current catch clause | |
| Variable? _rethrowParameter; | |
| Set<Class>? _pendingClasses; | |
| /// Temporary variables mapped to their corresponding JavaScript variable. | |
| final _tempVariables = <Variable, js_ast.ScopedId>{}; | |
| /// Let variables collected for the given function. | |
| List<js_ast.ScopedId>? _letVariables; | |
| final _constTable = js_ast.Identifier('CT'); | |
| /// Constant getters used to populate the constant table. | |
| final _constLazyAccessors = <js_ast.Method>[]; | |
| /// Container for holding the results of lazily-evaluated constants. | |
| var _constTableCache = ModuleItemContainer<String>.asArray('C'); | |
| /// Tracks the index in [_moduleItems] where the const table must be inserted. | |
| /// Required for SDK builds due to internal circular dependencies. | |
| /// E.g., dart.constList depends on JSArray. | |
| int _constTableInsertionIndex = 0; | |
| /// The class that is emitting its base class or mixin references, otherwise | |
| /// null. | |
| /// | |
| /// This is not used when inside the class method bodies, or for other type | |
| /// information such as `implements`. | |
| Class? _classEmittingExtends; | |
| /// The class that is emitting its signature information, otherwise null. | |
| Class? _classEmittingSignatures; | |
| /// True when a class is emitting a deferred class hierarchy. | |
| bool _emittingDeferredType = false; | |
| /// The current 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; | |
| /// 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 = <String, js_ast.Method>{}; | |
| /// Cache for the results of calling [_requiresRtiForInstantiation]. | |
| final _requiresRtiForInstantiationCache = <Class, bool>{}; | |
| /// Reserved parameter used to reference RTI objects passed to generic | |
| /// constructors/factories and generic method signatures. | |
| final _rtiParam = js_ast.ScopedId('_ti'); | |
| // Compilation of Kernel's [BreakStatement]. | |
| // | |
| // Kernel represents Dart's `break` and `continue` uniformly as | |
| // [BreakStatement], by representing a loop continue as a break from the | |
| // loop's body. [BreakStatement] always targets an enclosing | |
| // [LabeledStatement] statement directly without naming it. (Continue to | |
| // a labeled switch case is not represented by a [BreakStatement].) | |
| // | |
| // We prefer to compile to `continue` where possible and to avoid labeling | |
| // statements where it is not necessary. We maintain some state to track | |
| // which statements can be targets of break or continue without a label, which | |
| // statements must be labeled to be targets, and the labels that have been | |
| // assigned. | |
| /// A list of statements that can be the target of break without a label. | |
| /// | |
| /// A [BreakStatement] targeting any [LabeledStatement] in this list can be | |
| /// compiled to a break without a label. All the statements in the list have | |
| /// the same effective target which must compile to something that can be | |
| /// targeted by break in JS. This list and [_currentContinueTargets] are | |
| /// disjoint. | |
| List<LabeledStatement> _currentBreakTargets = []; | |
| /// A list of statements that can be the target of a continue without a label. | |
| /// | |
| /// A [BreakStatement] targeting any [LabeledStatement] in this list can be | |
| /// compiled to a continue without a label. All the statements in this list | |
| /// have the same effective target which must compile to something that can be | |
| /// targeted by continue in JS. This list and [_currentBreakTargets] are | |
| /// disjoint. | |
| List<LabeledStatement> _currentContinueTargets = []; | |
| /// A map from labeled statements to their 'effective targets'. | |
| /// | |
| /// The effective target of a labeled loop body is the enclosing loop. A | |
| /// [BreakStatement] targeting this statement can be compiled to `continue` | |
| /// either with or without a label. The effective target of a labeled | |
| /// statement that is not a loop body is the outermost non-labeled statement | |
| /// that it encloses. A [BreakStatement] targeting this statement can be | |
| /// compiled to `break` either with or without a label. | |
| final _effectiveTargets = HashMap<LabeledStatement, Statement>.identity(); | |
| /// A map from effective targets to their label names. | |
| /// | |
| /// If the target needs to be labeled when compiled to JS, because it was | |
| /// targeted by a break or continue with a label, then this map contains the | |
| /// label name that was assigned to it. | |
| final _labelNames = HashMap<Statement, String>.identity(); | |
| /// Indicates that the current context exists within a switch statement that | |
| /// uses at least one continue statement with a target label. | |
| /// | |
| /// JS forbids labels at case statement boundaries, so these switch | |
| /// statements must be generated less directly. | |
| /// Updated from the method 'visitSwitchStatement'. | |
| bool _inLabeledContinueSwitch = false; | |
| /// A map from switch statements to their state information. | |
| /// State information includes the names of the switch statement's implicit | |
| /// label name and implicit state variable name. | |
| /// | |
| /// Entries are only created for switch statements that contain labeled | |
| /// continue statements and are used to simulate "jumping" to case statements. | |
| /// State variables hold the next constant case expression, while labels act | |
| /// as targets for continue and break. | |
| final _switchLabelStates = HashMap<Statement, _SwitchLabelState>(); | |
| /// Maps Kernel constants to their JS aliases. | |
| final _constAliasCache = HashMap<Constant, js_ast.Expression>(); | |
| /// Maps uri strings in asserts and elsewhere to hoisted identifiers. | |
| var _uriContainer = ModuleItemContainer<String>.asArray('I'); | |
| /// 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 = <js_ast.Identifier?>[]; | |
| /// Private member names in this module, organized by their library. | |
| final _privateNames = HashMap<Library, HashMap<String, js_ast.ScopedId>>(); | |
| /// Holds all top-level JS symbols (used for caching or indexing fields). | |
| final _symbolContainer = ModuleItemContainer<js_ast.Identifier>.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 = <String, js_ast.ScopedId>{}; | |
| /// The set of libraries we are currently compiling, and the temporaries used | |
| /// to refer to them. | |
| final _libraries = <Library, js_ast.Identifier>{}; | |
| /// Imported libraries, and the temporaries used to refer to them. | |
| final _imports = <Library, js_ast.ScopedId>{}; | |
| /// Imported libraries in incremental mode, and the temporaries used to refer | |
| /// to them. | |
| final _incrementalImports = <Library, js_ast.ScopedId>{}; | |
| /// 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 = <String, Set<String>>{}; | |
| /// 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.ScopedId('dart_rti'); | |
| /// The library referred to by [_rtiLibraryId]. | |
| final Library _rtiLibrary; | |
| /// The `Rti` class defined in [_rtiLibrary]. | |
| final Class _rtiClass; | |
| /// Whether we're currently building the SDK, which may require special | |
| /// bootstrapping logic. | |
| /// | |
| /// This is initialized by [emitModule], which must be called before | |
| /// accessing this field. | |
| 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.ScopedId('opts'); | |
| /// The list of output module items, in the order they need to be emitted in. | |
| final _moduleItems = <js_ast.ModuleItem>[]; | |
| /// 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 = <js_ast.ModuleItem>[]; | |
| /// The entrypoint method of a dynamic module, if any. | |
| Procedure? _dynamicEntrypoint; | |
| final Class _jsArrayClass; | |
| final Class _privateSymbolClass; | |
| final Class _linkedHashMapImplClass; | |
| final Class _identityHashMapImplClass; | |
| final Class _linkedHashSetClass; | |
| final Class _linkedHashSetImplClass; | |
| final Class _identityHashSetImplClass; | |
| // Helpers for async function lowering | |
| final Member _asyncStartMember; | |
| final Member _asyncAwaitMember; | |
| final Member _asyncReturnMember; | |
| final Member _asyncRethrowMember; | |
| final Member _asyncMakeCompleterMember; | |
| final Member _asyncWrapJsFunctionMember; | |
| // Helpers for sync* function lowering | |
| final Member _syncStarMakeIterableMember; | |
| final Member _syncStarIteratorCurrentMember; | |
| final Member _syncStarIteratorDatumMember; | |
| final Member _syncStarIteratorYieldStarMember; | |
| // Helpers for async* function lowering | |
| final Member _asyncStarHelperMember; | |
| final Member _asyncStreamOfControllerMember; | |
| final Member _asyncMakeAsyncStarStreamControllerMember; | |
| final Member _asyncIterationMarkerYieldSingleMember; | |
| final Member _asyncIterationMarkerYieldStarMember; | |
| final Class _asyncStreamIteratorClass; | |
| final Procedure _assertInteropMethod; | |
| // The direct `_as` methods for primitive types. | |
| final Member _asBool; | |
| final Member _asDouble; | |
| final Member _asInt; | |
| final Member _asNum; | |
| final Member _asObject; | |
| final Member _asString; | |
| final Member _asBoolQ; | |
| final Member _asDoubleQ; | |
| final Member _asIntQ; | |
| final Member _asNumQ; | |
| final Member _asStringQ; | |
| final DevCompilerConstants _constants; | |
| final NullableInference _nullableInference; | |
| bool _moduleEmitted = false; | |
| /// Supports verbose logging with a timer. | |
| Ticker? _ticker; | |
| factory ProgramCompiler( | |
| Component component, | |
| ClassHierarchy hierarchy, | |
| Options options, | |
| Map<Library, Component> importToSummary, | |
| Map<Component, String> 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'), | |
| _asBool = sdk.getTopLevelMember('dart:_rti', '_asBool'), | |
| _asDouble = sdk.getTopLevelMember('dart:_rti', '_asDouble'), | |
| _asInt = sdk.getTopLevelMember('dart:_rti', '_asInt'), | |
| _asNum = sdk.getTopLevelMember('dart:_rti', '_asNum'), | |
| _asObject = sdk.getTopLevelMember('dart:_rti', '_asObject'), | |
| _asString = sdk.getTopLevelMember('dart:_rti', '_asString'), | |
| _asBoolQ = sdk.getTopLevelMember('dart:_rti', '_asBoolQ'), | |
| _asDoubleQ = sdk.getTopLevelMember('dart:_rti', '_asDoubleQ'), | |
| _asIntQ = sdk.getTopLevelMember('dart:_rti', '_asIntQ'), | |
| _asNumQ = sdk.getTopLevelMember('dart:_rti', '_asNumQ'), | |
| _asStringQ = sdk.getTopLevelMember('dart:_rti', '_asStringQ'); | |
| /// The library for dart:core in the SDK. | |
| Library get _coreLibrary => _coreTypes.coreLibrary; | |
| /// The type used for private Dart [Symbol]s. | |
| InterfaceType get _privateSymbolType => | |
| _coreTypes.nonNullableRawType(_privateSymbolClass); | |
| /// The type used for public Dart [Symbol]s. | |
| InterfaceType get _internalSymbolType => | |
| _coreTypes.nonNullableRawType(_coreTypes.internalSymbolClass); | |
| final FutureOrNormalizer _futureOrNormalizer; | |
| /// Module can be emitted only once, and the compiler can be reused after | |
| /// only in incremental mode, for expression compilation only. | |
| @override | |
| js_ast.Program emitModule(Component component) { | |
| assert(!_options.emitLibraryBundle); | |
| 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<String>(); | |
| libraries.forEach((Library l) { | |
| l.classes.forEach((Class c) { | |
| c.members.forEach((m) { | |
| var isStatic = m is Field | |
| ? m.isStatic | |
| : (m is Procedure ? m.isStatic : false); | |
| if (isStatic) return; | |
| var name = js_ast.toJSIdentifier( | |
| m.name.text.replaceAll(js_ast.invalidCharInIdentifier, '_'), | |
| ); | |
| uniqueNames.add(name); | |
| }); | |
| }); | |
| }); | |
| _containerizeSymbols = uniqueNames.length > 600; | |
| } | |
| var items = _startModule(libraries); | |
| // 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); | |
| // Collect all class/type Element -> Node mappings | |
| // in case we need to forward declare any classes. | |
| var classes = HashSet<Class>.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(#, { # })', [ | |
| _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; | |
| var legacyJavaScriptObjectRecipe = _typeRecipeGenerator.interfaceTypeRecipe( | |
| _coreTypes.index.getClass('dart:_interceptors', 'LegacyJavaScriptObject'), | |
| ); | |
| var legacyJavaScriptObjectRules = typeRules.remove( | |
| legacyJavaScriptObjectRecipe, | |
| ); | |
| 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); | |
| } | |
| if (legacyJavaScriptObjectRules != null) { | |
| var legacyJavaScriptObjectAddRules = { | |
| legacyJavaScriptObjectRecipe: legacyJavaScriptObjectRules, | |
| }; | |
| // The recipe for 'LegacyJavaScriptObject' is updated during the lifetime | |
| // of the program and should be emitted with 'addOrUpdateRules' to avoid | |
| // clobbering its previous state. | |
| var updateRulesStatement = js.statement( | |
| '#._Universe.#(#, JSON.parse(#))', | |
| [ | |
| _emitLibraryName(_rtiLibrary), | |
| _emitMemberName('addOrUpdateRules', memberClass: universeClass), | |
| _runtimeCall('typeUniverse'), | |
| js.string(jsonEncode(legacyJavaScriptObjectAddRules), "'"), | |
| ], | |
| ); | |
| _moduleItems.add(updateRulesStatement); | |
| } | |
| // 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 | |
| if (_isBuildingSdk) { | |
| var prerequisiteRtiTypes = [_coreTypes.objectNullableRawType]; | |
| 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'); | |
| // Emit the hoisted type table cache variables | |
| items.addAll(_typeTable.dischargeBoundTypes()); | |
| _ticker?.logMs('Emitted type 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 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; | |
| return libraryUriToImportName(uri); | |
| } | |
| /// Debugger friendly name for a Dart [library]. | |
| String _jsLibraryDebuggerName(Library library) => '${library.importUri}'; | |
| /// Debugger friendly names for all parts in a Dart [library]. | |
| Iterable<String> _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); | |
| } | |
| // Creating a function and setting the library object as the prototype | |
| // serves as a signal to V8 that the members of the library should get | |
| // optimized for fast lookup. | |
| // Do not remove without testing for performance regressions. | |
| _moduleItems.add( | |
| js.statement('(function() {}).prototype = #', [ | |
| _libraries[_currentLibrary!], | |
| ]), | |
| ); | |
| _staticTypeContext.leaveLibrary(_currentLibrary!); | |
| _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); | |
| // Store identifiers for a mixin application's passed in superclass. | |
| // (see [_emitMixinStatement]). | |
| if (c.isMixinDeclaration && !c.isMixinClass) { | |
| _mixinSuperclassCache.putIfAbsent( | |
| c, | |
| () => _emitScopedId(getLocalClassName(c.superclass!)), | |
| ); | |
| } | |
| // Mixins are unrolled in _defineClass. | |
| if (!c.isAnonymousMixin) { | |
| // If this class is annotated with `@JS`, then we only need to emit the | |
| // non-external factories and static members. | |
| if (!hasJSInteropAnnotation(c)) { | |
| _moduleItems.add(_emitClassDeclaration(c)); | |
| } else { | |
| var interopClassDef = _emitJSInteropClassNonExternalMembers(c); | |
| if (interopClassDef != null) _moduleItems.add(interopClassDef); | |
| } | |
| } | |
| // The const table depends on dart.defineLazy, so emit it after the SDK. | |
| if (_isSdkInternalRuntime(_currentLibrary!)) { | |
| _constTableInsertionIndex = _moduleItems.length; | |
| } | |
| _currentClass = savedClass; | |
| _currentLibrary = savedLibrary; | |
| _currentUri = savedUri; | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| } | |
| /// 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.ScopedId _emitScopedId( | |
| String name, { | |
| bool needsCapture = false, | |
| }) => | |
| js_ast.ScopedId(js_ast.toJSIdentifier(name), needsCapture: needsCapture); | |
| js_ast.Statement _emitClassDeclaration(Class c) { | |
| var className = _emitTopLevelNameNoExternalInterop(c); | |
| var savedClassProperties = _classProperties; | |
| _classProperties = ClassPropertyModel.build( | |
| _types, | |
| _extensionTypes, | |
| _virtualFields, | |
| c, | |
| ); | |
| var body = <js_ast.Statement>[]; | |
| // ClassPropertyModel.build introduces symbols for virtual field accessors. | |
| _classProperties!.virtualFields.forEach((field, virtualField) { | |
| // TODO(vsm): Clean up this logic. | |
| // | |
| // Typically, [emitClassPrivateNameSymbol] creates a new symbol. If it | |
| // is called multiple times, that symbol is cached. If the former, | |
| // assign directly to [virtualField]. If the latter, copy the old | |
| // variable to [virtualField]. | |
| var symbol = _emitClassPrivateNameSymbol( | |
| c.enclosingLibrary, | |
| getLocalClassName(c), | |
| field, | |
| virtualField, | |
| ); | |
| if (symbol != virtualField) { | |
| _addSymbol(virtualField, _getSymbolValue(symbol)); | |
| if (!_containerizeSymbols) { | |
| body.add(js.statement('const # = #;', [virtualField, symbol])); | |
| } | |
| } | |
| }); | |
| var jsCtors = _defineConstructors(c, className); | |
| var jsMethods = _emitClassMethods(c); | |
| var jsStaticMethodTypeTags = <js_ast.Statement>[]; | |
| for (var member in c.procedures) { | |
| // TODO(#57049): We tag all static members because we don't know if | |
| // they've been changed after a hot reload. This won't be necessary if we | |
| // can tag them during the delta diff phase. | |
| if (member.isStatic && _reifyTearoff(member) && !member.isExternal) { | |
| var result = _emitStaticTarget(member); | |
| // We only need to tag static functions that are torn off at | |
| // compile-time. We attach these late so tearoffs have access to | |
| // their types. | |
| var reifiedType = member.function.computeThisFunctionType( | |
| member.enclosingLibrary.nonNullable, | |
| ); | |
| jsStaticMethodTypeTags.add( | |
| _emitFunctionTagged(result, reifiedType, asLazy: true).toStatement(), | |
| ); | |
| } | |
| } | |
| _emitSuperHelperSymbols(body); | |
| // Deferred supertypes must be evaluated lazily while emitting classes to | |
| // prevent evaluating a JS expression for a deferred type from influencing | |
| // class declaration order (such as when calling 'emitDeferredType'). | |
| var deferredSupertypes = <js_ast.Statement Function()>[]; | |
| // Emit the class, e.g. `core.Object = class Object { ... }` | |
| _defineClass(c, className, jsMethods, body, deferredSupertypes); | |
| body.addAll(jsCtors); | |
| body.addAll(jsStaticMethodTypeTags); | |
| // Emit things that come after the ES6 `class ... { ... }`. | |
| /// Collects all implemented types in the ancestry of [cls]. | |
| Iterable<Supertype> transitiveImplementedTypes(Class cls) { | |
| var allImplementedTypes = <Supertype>{}; | |
| var toVisit = ListQueue<Supertype>()..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<js_ast.Statement>((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 _extensionTypes.getNativePeers(c)) { | |
| _registerExtensionType(c, peer, body); | |
| } | |
| } | |
| _classProperties = savedClassProperties; | |
| return js_ast.Statement.from(body); | |
| } | |
| /// Emits a class declaration for the JS interop class [c] for any | |
| /// non-external factories or static members. | |
| /// | |
| /// If [c] is not an interop class or does not contain non-external factories | |
| /// or static members, returns null. | |
| js_ast.Statement? _emitJSInteropClassNonExternalMembers(Class c) { | |
| if (!hasJSInteropAnnotation(c)) return null; | |
| var className = _emitTopLevelNameNoExternalInterop(c); | |
| var nonExternalMethods = <js_ast.Method>[]; | |
| 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 = <js_ast.Statement>[]; | |
| _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 = <js_ast.Statement>[ | |
| _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<js_ast.Statement> _defineGenericClass( | |
| List<TypeParameter> formals, | |
| js_ast.Statement body, | |
| List<js_ast.Statement> 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<js_ast.Method> methods, | |
| ) { | |
| var classIdentifier = _emitScopedId(getLocalClassName(c)); | |
| if (_options.emitDebugSymbols) classIdentifiers[c] = classIdentifier; | |
| var classExpr = js_ast.ClassExpression(classIdentifier, heritage, methods); | |
| return js.statement('# = #;', [className, classExpr]); | |
| } | |
| /// Like [_emitClassStatement] but emits a Dart 2.1 mixin represented by | |
| /// [c]. | |
| /// | |
| /// Mixins work similar to normal classes, but their instance methods close | |
| /// over the actual superclass. Given a Dart class like: | |
| /// | |
| /// mixin M on C { | |
| /// foo() => super.foo() + 42; | |
| /// } | |
| /// | |
| /// We generate a JS class like this: | |
| /// | |
| /// lib.M = class M extends core.Object {} | |
| /// lib.M[dart.mixinOn] = (C) => class M extends C { | |
| /// foo() { | |
| /// return super.foo() + 42; | |
| /// } | |
| /// }; | |
| /// | |
| /// The special `dart.mixinOn` symbolized property is used by the runtime | |
| /// helper `dart.applyMixin`. The helper calls the function with the actual | |
| /// base class, and then copies the resulting members to the destination | |
| /// class. | |
| /// | |
| /// In the long run we may be able to improve this so we do not have the | |
| /// unnecessary class, but for now, this lets us get the right semantics with | |
| /// minimal compiler and runtime changes. | |
| void _emitMixinStatement( | |
| Class c, | |
| js_ast.Expression className, | |
| js_ast.Expression heritage, | |
| List<js_ast.Method> methods, | |
| List<js_ast.Statement> body, | |
| ) { | |
| var staticMethods = methods.where((m) => m.isStatic).toList(); | |
| var instanceMethods = methods.where((m) => !m.isStatic).toList(); | |
| body.add(_emitClassStatement(c, className, heritage, staticMethods)); | |
| var superclassId = _mixinSuperclassCache[c]!; | |
| var classId = className is js_ast.Identifier | |
| ? className | |
| : _emitScopedId(getLocalClassName(c)); | |
| var mixinMemberClass = js_ast.ClassExpression( | |
| classId, | |
| superclassId, | |
| instanceMethods, | |
| ); | |
| js_ast.Node arrowFnBody = mixinMemberClass; | |
| var extensionInit = <js_ast.Statement>[]; | |
| _defineExtensionMembers(classId, extensionInit); | |
| if (extensionInit.isNotEmpty) { | |
| extensionInit.insert(0, mixinMemberClass.toStatement()); | |
| extensionInit.add(classId.toReturn()); | |
| arrowFnBody = js_ast.Block(extensionInit); | |
| } | |
| body.add( | |
| js.statement('#[#] = #', [ | |
| className, | |
| _runtimeCall('mixinOn'), | |
| js_ast.ArrowFun([superclassId], arrowFnBody), | |
| ]), | |
| ); | |
| } | |
| void _defineClass( | |
| Class c, | |
| js_ast.Expression className, | |
| List<js_ast.Method> methods, | |
| List<js_ast.Statement> body, | |
| List<js_ast.Statement Function()> deferredSupertypes, | |
| ) { | |
| if (c == _coreTypes.objectClass) { | |
| body.add(_emitClassStatement(c, className, null, methods)); | |
| return; | |
| } | |
| js_ast.Expression 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 = <DartType>{}; | |
| 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); | |
| } | |
| // Find the real (user declared) superclass and the list of mixins. | |
| // We'll use this to unroll the intermediate classes. | |
| // | |
| // TODO(jmesserly): consider using Kernel's mixin unrolling. | |
| var superclass = _superClassAsWritten(c); | |
| var supertype = identical(c.superclass, superclass) | |
| ? c.supertype!.asInterfaceType | |
| : _hierarchy.getClassAsInstanceOf(c, superclass)!.asInterfaceType; | |
| // All mixins (real and anonymous) classes applied to c. | |
| var mixinApplications = [ | |
| if (c.mixedInClass != null) c.mixedInClass, | |
| for ( | |
| var sc = c.superclass!; | |
| sc.isAnonymousMixin && sc.mixedInClass != null; | |
| sc = sc.superclass! | |
| ) | |
| sc, | |
| ].reversed.toList(); | |
| var hasUnnamedSuper = _hasUnnamedInheritedConstructor(superclass); | |
| void emitMixinConstructors( | |
| js_ast.Expression className, | |
| Class mixinSuperclass, | |
| Class mixinClass, | |
| InterfaceType mixin, | |
| ) { | |
| for (var ctor in mixinSuperclass.constructors) { | |
| var savedUri = _currentUri; | |
| _currentUri = ctor.enclosingClass.fileUri; | |
| var sharedParams = _emitParameters(ctor.function, isForwarding: true); | |
| var mixinConstructorParams = [ | |
| if (_requiresRtiForInstantiation(mixinSuperclass)) _rtiParam, | |
| ...sharedParams, | |
| ]; | |
| var superConstructorArgs = [ | |
| if (_requiresRtiForInstantiation(ctor.enclosingClass)) | |
| js_ast.LiteralNull(), | |
| ...sharedParams, | |
| ]; | |
| js_ast.Statement? mixinCtor; | |
| if (_hasUnnamedConstructor(mixin.classNode)) { | |
| var mixinRti = _requiresRtiForInstantiation(mixin.classNode) | |
| ? js_ast.LiteralNull() | |
| : null; | |
| mixinCtor = js.statement('#.#.call(this, #);', [ | |
| emitClassRef(mixin), | |
| _usesMixinNew(mixin.classNode) | |
| ? _runtimeCall('mixinNew') | |
| : _constructorName(''), | |
| [?mixinRti], | |
| ]); | |
| } | |
| var name = ctor.name.text; | |
| var ctorBody = [ | |
| ?mixinCtor, | |
| if (name != '' || hasUnnamedSuper) | |
| _emitSuperConstructorCall( | |
| ctor, | |
| className, | |
| name, | |
| superConstructorArgs, | |
| ), | |
| ]; | |
| // TODO(nshahan) Record the name for this constructor in memberNames. | |
| body.add( | |
| _addConstructorToClass( | |
| c, | |
| className, | |
| _constructorName(name), | |
| js_ast.Fun(mixinConstructorParams, js_ast.Block(ctorBody)), | |
| ), | |
| ); | |
| _currentUri = savedUri; | |
| } | |
| } | |
| var savedTopLevelClass = _classEmittingExtends; | |
| _classEmittingExtends = c; | |
| // Unroll mixins. | |
| var baseClass = 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 mixinId = _emitMixinId(m, m.isAnonymousMixin ? m : c); | |
| // Collect all forwarding stub members from anonymous mixins classes. | |
| // These can contain covariant parameter checks that need to be applied. | |
| var savedClassProperties = _classProperties; | |
| _classProperties = ClassPropertyModel.build( | |
| _types, | |
| _extensionTypes, | |
| _virtualFields, | |
| m, | |
| ); | |
| var forwardingMembers = { | |
| for (var procedure in m.procedures) | |
| if (procedure.isForwardingStub && !procedure.isAbstract) | |
| procedure.name.text: procedure, | |
| }; | |
| // Mixin applications can introduce their own reference to the type | |
| // parameters from the class being mixed in and their use can appear in | |
| // the forwarding stubs. | |
| var savedTypeEnvironment = _currentTypeEnvironment; | |
| if (m.typeParameters.isNotEmpty) { | |
| assert(_currentTypeEnvironment is ClassTypeEnvironment); | |
| _currentTypeEnvironment = ClassTypeEnvironment(m.typeParameters); | |
| } | |
| var forwardingMethodStubs = <js_ast.Method>[]; | |
| for (var s in forwardingMembers.values) { | |
| // Members are marked as "forwarding stubs" when they require a type | |
| // check of the arguments before calling super. It is assumed here that | |
| // no getters will be marked as a "forwarding stub". | |
| assert(!s.isGetter); | |
| var stub = _emitMethodDeclaration(s); | |
| if (stub != null) forwardingMethodStubs.add(stub); | |
| // If there are getters matching the setters somewhere above in the | |
| // class hierarchy we must also generate a forwarding getter due to the | |
| // representation used in the compiled JavaScript. | |
| if (s.isSetter) { | |
| var getterWrapper = _emitSuperAccessorWrapper(s, const {}, const {}); | |
| if (getterWrapper != null) forwardingMethodStubs.add(getterWrapper); | |
| } | |
| } | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| _classProperties = savedClassProperties; | |
| // Mixins need to be exposed in this library in case they are | |
| // referenced in a super getter. | |
| // TODO(markzipan): We originally bound mixin classes to a temporary as a | |
| // workaround for a now-resolved Chrome issue. However, a side effect of | |
| // this operation is that mixin IDs are renamed by the local visitor. We | |
| // can remove this hoisting after we give mixins unique names. | |
| var enclosingLibrary = _emitLibraryName(_currentLibrary!); | |
| var mixinAccessor = js_ast.PropertyAccess( | |
| enclosingLibrary, | |
| js.string(mixinId.name), | |
| ); | |
| body.addAll([ | |
| js.statement('const # = #', [ | |
| mixinId, | |
| js_ast.ClassExpression( | |
| _emitScopedId('${mixinId.name}\$'), | |
| baseClass, | |
| forwardingMethodStubs, | |
| ), | |
| ]), | |
| js.statement('# = #', [mixinAccessor, mixinId]), | |
| ]); | |
| emitMixinConstructors(mixinId, superclass, mixinClass, mixinType); | |
| hasUnnamedSuper = hasUnnamedSuper || _hasUnnamedConstructor(mixinClass); | |
| var mixinTargetLabel = js.string(fullyResolvedMixinClassLabel(m)); | |
| if (shouldDefer(mixinType)) { | |
| deferredSupertypes.add( | |
| () => _runtimeStatement('applyMixin(#, #, #)', [ | |
| mixinId, | |
| emitDeferredClassRef(mixinType), | |
| mixinTargetLabel, | |
| ]), | |
| ); | |
| } else { | |
| body.add( | |
| _runtimeStatement('applyMixin(#, #, #)', [ | |
| mixinId, | |
| emitClassRef(mixinType), | |
| baseClass, | |
| ]), | |
| ); | |
| } | |
| 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<js_ast.Statement> _defineConstructors( | |
| Class c, | |
| js_ast.Expression className, | |
| ) { | |
| var body = <js_ast.Statement>[]; | |
| 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<js_ast.ScopedId> vars, | |
| List<js_ast.ModuleItem> body, | |
| ) { | |
| for (var id in vars) { | |
| body.add(js.statement('const # = Symbol(#)', [id, js.string(id.name)])); | |
| } | |
| } | |
| void _emitSuperHelperSymbols(List<js_ast.Statement> body) { | |
| _emitDartSymbols( | |
| _superHelpers.values.map((m) => m.name as js_ast.ScopedId), | |
| 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<js_ast.Statement> body) { | |
| var fields = c.fields.where((f) => f.isStatic && !f.isExternal).toList(); | |
| var fieldNames = Set.of(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<js_ast.Statement> body, | |
| ) { | |
| void emitExtensions(String helperName, Iterable<String> extensions) { | |
| if (extensions.isEmpty) return; | |
| var names = extensions | |
| .map((e) => _propertyName(js_ast.memberNameForDartMember(e))) | |
| .toList(); | |
| body.add( | |
| _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<js_ast.Statement> body, | |
| ) { | |
| var savedTypeEnvironment = _currentTypeEnvironment; | |
| _currentTypeEnvironment = RtiTypeEnvironment( | |
| _currentTypeEnvironment.classTypeParameters, | |
| ); | |
| var savedClass = _classEmittingSignatures; | |
| _classEmittingSignatures = c; | |
| void emitSignature(String name, List<js_ast.Property> 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 = <js_ast.Property>[]; | |
| var instanceMethods = <js_ast.Property>[]; | |
| var instanceMethodsDefaultTypeArgs = <js_ast.Property>[]; | |
| var methodsImmediateTarget = <js_ast.Property>[]; | |
| var staticGetters = <js_ast.Property>[]; | |
| var instanceGetters = <js_ast.Property>[]; | |
| var staticSetters = <js_ast.Property>[]; | |
| var instanceSetters = <js_ast.Property>[]; | |
| List<js_ast.Property> getSignatureList(Procedure p) { | |
| // TODO(40273) Skip for all statics when the debugger consumes signature | |
| // information from symbol files. | |
| if (p.isStatic) { | |
| if (p.isGetter) { | |
| return staticGetters; | |
| } else if (p.isSetter) { | |
| return staticSetters; | |
| } else { | |
| return staticMethods; | |
| } | |
| } else { | |
| if (p.isGetter) { | |
| return instanceGetters; | |
| } else if (p.isSetter) { | |
| return instanceSetters; | |
| } else { | |
| return instanceMethods; | |
| } | |
| } | |
| } | |
| var classProcedures = c.procedures.where((p) => !p.isAbstract).toList(); | |
| for (var member in classProcedures) { | |
| // Static getters/setters cannot be called with dynamic dispatch or torn | |
| // off. Static methods can't be called with dynamic dispatch and are | |
| // tagged with a type when torn off. Most are implicitly const and | |
| // canonicalized. Static signatures are only used by the debugger and are | |
| // not needed for runtime correctness. | |
| // TODO(40273) Skip for all statics when the debugger consumes signature | |
| // information from symbol files. | |
| if (isTearOffLowering(member)) continue; | |
| var name = member.name.text; | |
| var reifiedType = _memberRuntimeType(member, c) as FunctionType; | |
| // Don't add redundant signatures for inherited methods whose signature | |
| // did not change. If we are not overriding, or if the thing we are | |
| // overriding has a different reified type from ourselves, we must | |
| // emit a signature on this class. Otherwise we will inherit the | |
| // signature from the superclass. | |
| var memberOverride = c.superclass != null | |
| ? _hierarchy.getDispatchTarget( | |
| c.superclass!, | |
| member.name, | |
| setter: member.isSetter, | |
| ) | |
| : null; | |
| var needsSignature = | |
| memberOverride == null || | |
| reifiedType != _memberRuntimeType(memberOverride, c); | |
| var memberName = _declareMemberName(member); | |
| if (!member.isAccessor) { | |
| var immediateTarget = js.string(fullyResolvedTargetLabel(member)); | |
| methodsImmediateTarget.add( | |
| js_ast.Property(memberName, immediateTarget), | |
| ); | |
| } | |
| if (needsSignature) { | |
| js_ast.Expression type; | |
| if (member.isAccessor) { | |
| // These signatures are used for dynamic access and to inform the | |
| // debugger. The `arrayRti` accessor is only used by the dart:_rti | |
| // library internals and should not be included in the accessible | |
| // signatures. | |
| if (c == _jsArrayClass && name == 'arrayRti') continue; | |
| type = _emitType( | |
| member.isGetter | |
| ? reifiedType.returnType | |
| : reifiedType.positionalParameters[0], | |
| ); | |
| } else { | |
| type = _emitType(reifiedType); | |
| if (!member.isStatic && reifiedType.typeParameters.isNotEmpty) { | |
| // Instance methods with generic type parameters require extra | |
| // information to support dynamic calls. The default values for the | |
| // type parameters are encoded into a separate storage object for | |
| // use at runtime. | |
| var defaultTypeArgs = js_ast.ArrayInitializer([ | |
| for (var parameter in reifiedType.typeParameters) | |
| _emitType(parameter.defaultType), | |
| ]); | |
| var typeResolver = js_ast.ArrowFun([_rtiParam], defaultTypeArgs); | |
| var property = js_ast.Property(memberName, typeResolver); | |
| instanceMethodsDefaultTypeArgs.add(property); | |
| // As seen below, sometimes the member signatures are added again | |
| // using the extension symbol as the name. That logic is duplicated | |
| // here to ensure there are always default type arguments accessible | |
| // via the same name as the signature. | |
| // TODO(52867): Cleanup default type argument duplication. | |
| if (extMethods.contains(name) || extAccessors.contains(name)) { | |
| var property = js_ast.Property( | |
| _declareMemberName(member, useExtension: true), | |
| typeResolver, | |
| ); | |
| instanceMethodsDefaultTypeArgs.add(property); | |
| } | |
| } | |
| } | |
| var typeResolver = js_ast.ArrowFun([_rtiParam], type); | |
| var property = js_ast.Property(memberName, typeResolver); | |
| var signatures = getSignatureList(member); | |
| signatures.add(property); | |
| if (!member.isStatic && | |
| (extMethods.contains(name) || extAccessors.contains(name))) { | |
| // TODO(52867): Cleanup signature duplication. | |
| var typeResolver = js_ast.ArrowFun([_rtiParam], type); | |
| var property = js_ast.Property( | |
| _declareMemberName(member, useExtension: true), | |
| typeResolver, | |
| ); | |
| signatures.add(property); | |
| } | |
| } | |
| } | |
| emitSignature('Method', instanceMethods); | |
| emitSignature('MethodsDefaultTypeArg', instanceMethodsDefaultTypeArgs); | |
| emitSignature('MethodsImmediateTarget', methodsImmediateTarget); | |
| // TODO(40273) Skip for all statics when the debugger consumes signature | |
| // information from symbol files. | |
| emitSignature('StaticMethod', staticMethods); | |
| emitSignature('Getter', instanceGetters); | |
| emitSignature('Setter', instanceSetters); | |
| emitSignature('StaticGetter', staticGetters); | |
| emitSignature('StaticSetter', staticSetters); | |
| body.add( | |
| _runtimeStatement('setLibraryUri(#, #)', [ | |
| className, | |
| _cacheUri(_jsLibraryDebuggerName(c.enclosingLibrary)), | |
| ]), | |
| ); | |
| var instanceFields = <js_ast.Property>[]; | |
| var staticFields = <js_ast.Property>[]; | |
| var classFields = c.fields.toList(); | |
| for (var field in classFields) { | |
| // Static fields cannot be called with dynamic dispatch or torn off. The | |
| // signatures are only used by the debugger and are not needed for runtime | |
| // correctness. | |
| var memberName = _declareMemberName(field); | |
| var fieldSig = emitClassFieldSignature(field, c); | |
| var property = js_ast.Property(memberName, fieldSig); | |
| // TODO(40273) Skip static fields when the debugger consumes signature | |
| // information from symbol files. | |
| (field.isStatic ? staticFields : instanceFields).add(property); | |
| } | |
| emitSignature('Field', instanceFields); | |
| // TODO(40273) Skip for all statics when the debugger consumes signature | |
| // information from symbol files. | |
| emitSignature('StaticField', staticFields); | |
| _classEmittingSignatures = savedClass; | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| } | |
| DartType _memberRuntimeType(Member member, Class fromClass) { | |
| var f = member.function; | |
| if (f == null) { | |
| return (member as Field).type; | |
| } | |
| FunctionType result; | |
| if (!f.positionalParameters.any(isCovariantParameter) && | |
| !f.namedParameters.any(isCovariantParameter)) { | |
| result = f.computeThisFunctionType(Nullability.nonNullable); | |
| } else { | |
| var fComputed = f.computeThisFunctionType(Nullability.nonNullable); | |
| var fComputedNamedByName = { | |
| for (NamedType namedParameter in fComputed.namedParameters) | |
| namedParameter.name: namedParameter, | |
| }; | |
| DartType reifyParameter( | |
| FunctionParameter parameter, | |
| DartType fComputedParameter, | |
| ) => isCovariantParameter(parameter) | |
| ? _coreTypes.objectNullableRawType | |
| : fComputedParameter; | |
| NamedType reifyNamedParameter( | |
| NamedParameter parameter, | |
| NamedType fComputedNamedParameter, | |
| ) { | |
| assert(parameter.parameterName == fComputedNamedParameter.name); | |
| return NamedType( | |
| parameter.parameterName, | |
| reifyParameter(parameter, fComputedNamedParameter.type), | |
| ); | |
| } | |
| // TODO(jmesserly): do covariant type parameter bounds also need to be | |
| // reified as `Object`? | |
| result = FunctionType( | |
| DartTypeList.generate( | |
| f.positionalParameters.length, | |
| (index) => reifyParameter( | |
| f.positionalParameters[index], | |
| fComputed.positionalParameters[index], | |
| ), | |
| ), | |
| f.returnType, | |
| Nullability.nonNullable, | |
| namedParameters: NamedDartTypeList.mapped( | |
| f.namedParameters, | |
| (p) => reifyNamedParameter(p, fComputedNamedByName[p.parameterName]!), | |
| )..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<Field> fields, | |
| js_ast.Expression className, | |
| ) { | |
| var savedUri = _currentUri; | |
| _currentUri = node.fileUri; | |
| _staticTypeContext.enterMember(node); | |
| var savedTypeEnvironment = _currentTypeEnvironment; | |
| _currentTypeEnvironment = ClassTypeEnvironment( | |
| node.enclosingClass.typeParameters, | |
| ); | |
| var params = <js_ast.Parameter>[]; | |
| // 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<js_ast.Statement> _emitConstructorBody( | |
| Constructor node, | |
| List<Field> fields, | |
| js_ast.Expression className, | |
| ) { | |
| var cls = node.enclosingClass; | |
| // Generate optional/named argument value assignment. These can not have | |
| // side effects, and may be used by the constructor's initializers, so it's | |
| // nice to do them first. | |
| // Also for const constructors we need to ensure default values are | |
| // available for use by top-level constant initializers. | |
| var fn = node.function; | |
| var body = _emitArgumentInitializers(fn, node.name.text); | |
| // 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<SuperInitializer>().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<Initializer> initializers, | |
| js_ast.Expression className, | |
| ) { | |
| var jsInitializers = <js_ast.Statement>[]; | |
| 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(visitVariable(init.variable)); | |
| } else if (init is RedirectingInitializer) { | |
| var rtiParam = _requiresRtiForInstantiation(init.target.enclosingClass) | |
| ? _rtiParam | |
| : null; | |
| // We can't dispatch to the constructor with `this.new` as that might | |
| // hit a derived class constructor with the same name. | |
| var initializer = js.statement('#.#.call(this, #);', [ | |
| className, | |
| _constructorName(init.target.name.text), | |
| [?rtiParam, ..._emitArgumentList(init.arguments, types: false)], | |
| ]); | |
| jsInitializers.add(initializer); | |
| } | |
| } | |
| return js_ast.Block(jsInitializers); | |
| } | |
| js_ast.Statement? _emitSuperConstructorCallIfNeeded( | |
| Class c, | |
| js_ast.Expression className, | |
| SuperInitializer? superInit, | |
| ) { | |
| if (c == _coreTypes.objectClass) return null; | |
| Constructor ctor; | |
| List<js_ast.Expression> args; | |
| if (superInit == null) { | |
| ctor = unnamedConstructor(c.superclass!)!; | |
| args = []; | |
| } else { | |
| ctor = superInit.target; | |
| var savedTypeEnvironment = _currentTypeEnvironment; | |
| _currentTypeEnvironment = ClassTypeEnvironment(c.typeParameters); | |
| // An RTI will already have been set at the constructor call site, so | |
| // pass nothing if the superclass is expecting an RTI. | |
| var rti = _requiresRtiForInstantiation(ctor.enclosingClass) | |
| ? js_ast.LiteralNull() | |
| : null; | |
| args = [?rti, ..._emitArgumentList(superInit.arguments, types: true)]; | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| } | |
| // We can skip the super call if it's empty. Most commonly this happens for | |
| // things that extend Object, and don't have any field initializers or their | |
| // own default constructor. | |
| if (ctor.name.text == '' && !_hasUnnamedSuperConstructor(c)) { | |
| return null; | |
| } | |
| return _emitSuperConstructorCall(ctor, className, ctor.name.text, args); | |
| } | |
| js_ast.Statement _emitSuperConstructorCall( | |
| Constructor constructor, | |
| js_ast.Expression className, | |
| String name, | |
| List<js_ast.Expression> 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<Field> fields, [Constructor? ctor]) { | |
| // Run field initializers if they can have side-effects. | |
| var ctorFields = ctor?.initializers | |
| .whereType<FieldInitializer>() | |
| .map((c) => c.field) | |
| .toSet(); | |
| var body = <js_ast.Statement>[]; | |
| void emitFieldInit(Field f, Expression? initializer, TreeNode hoverInfo) { | |
| var virtualField = _classProperties!.virtualFields[f]; | |
| // Avoid calling getSymbol on _declareMemberName since _declareMemberName | |
| // calls _emitMemberName downstream, which already invokes getSymbol. | |
| var access = virtualField == null | |
| ? _declareMemberName(f) | |
| : _getSymbol(virtualField); | |
| var jsInit = _visitInitializer(initializer, f.annotations); | |
| body.add( | |
| jsInit.toAssignExpression(js.call('this.#', [access])).toStatement() | |
| ..sourceInformation = _nodeStart(hoverInfo), | |
| ); | |
| } | |
| 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(visitVariable(init.variable)); | |
| } else if (init is AssertInitializer) { | |
| body.add(visitAssertStatement(init.statement)); | |
| } | |
| } | |
| } | |
| return js_ast.Statement.from(body); | |
| } | |
| js_ast.Expression _visitInitializer( | |
| Expression? init, | |
| List<Expression> annotations, | |
| ) { | |
| // explicitly initialize to null, to avoid getting `undefined`. | |
| // TODO(jmesserly): do this only for vars that aren't definitely assigned. | |
| if (init == null) return js_ast.LiteralNull(); | |
| return _annotatedNullCheck(annotations) | |
| ? _notNull(init) | |
| : _visitExpression(init); | |
| } | |
| js_ast.Expression _notNull(Expression expr) { | |
| 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<js_ast.Method> _emitClassMethods(Class c) { | |
| var virtualFields = _classProperties!.virtualFields; | |
| var jsMethods = <js_ast.Method?>[]; | |
| var hasJsPeer = _extensionTypes.isNativeClass(c); | |
| var hasIterator = false; | |
| if (c == _coreTypes.objectClass) { | |
| // Dart does not use ES6 constructors. | |
| // Add an error to catch any invalid usage. | |
| jsMethods.add( | |
| js_ast.Method( | |
| _propertyName('constructor'), | |
| js.fun( | |
| r'''function() { | |
| throw Error("use `new " + # + | |
| ".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 = <Name>{}; | |
| 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 = <String, Procedure>{}; | |
| var setters = <String, Procedure>{}; | |
| for (var m in c.procedures) { | |
| if (m.isAbstract) continue; | |
| if (m.isGetter) { | |
| getters[m.name.text] = m; | |
| } else if (m.isSetter) { | |
| setters[m.name.text] = m; | |
| } | |
| } | |
| var savedUri = _currentUri; | |
| for (var m in c.procedures) { | |
| // 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<E> and IterableBase<E>). | |
| // | |
| // (We could do this same optimization for any interface with an `iterator` | |
| // method, but that's more expensive to check for, so it doesn't seem worth | |
| // it. The above case for an explicit `iterator` method will catch those.) | |
| if (!hasJsPeer && !hasIterator) { | |
| jsMethods.add(_emitIterable(c)); | |
| } | |
| // Add all of the super helper methods | |
| jsMethods.addAll(_superHelpers.values); | |
| return jsMethods.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<js_ast.Method> _emitCovarianceCheckStub(Procedure member) { | |
| // TODO(jmesserly): kernel stubs have a few problems: | |
| // - they're generated even when there is no concrete super member | |
| // - the stub parameter types don't match the types we need to check to | |
| // ensure soundness of the super member, so we must lookup the super | |
| // member and determine checks ourselves. | |
| // - it generates getter stubs, but these are not used | |
| if (member.isGetter) return const []; | |
| var enclosingClass = member.enclosingClass; | |
| var superMember = | |
| member.concreteForwardingStubTarget ?? | |
| member.abstractForwardingStubTarget; | |
| if (superMember == null) return const []; | |
| DartType substituteType(DartType t) { | |
| return _typeFromClass(t, superMember.enclosingClass!, enclosingClass!); | |
| } | |
| var superMemberFunction = superMember.function; | |
| var name = _declareMemberName(member); | |
| if (member.isSetter) { | |
| if (superMember is Field && isCovariantField(superMember) || | |
| superMember is Procedure && | |
| isCovariantParameter( | |
| superMemberFunction!.positionalParameters[0], | |
| )) { | |
| return const []; | |
| } | |
| var setterType = substituteType(superMember.superSetterType) | |
| .extensionTypeErasure; | |
| if (_types.isTop(setterType)) return const []; | |
| return [ | |
| js_ast.Method( | |
| name, | |
| js.fun('function(x) { return super.# = #; }', [ | |
| name, | |
| _emitCast(_emitIdentifier('x'), setterType), | |
| ]), | |
| isSetter: true, | |
| ), | |
| js_ast.Method( | |
| name, | |
| js.fun('function() { return super.#; }', [name]), | |
| isGetter: true, | |
| ), | |
| ]; | |
| } | |
| assert(!member.isAccessor); | |
| var superMethodType = substituteType( | |
| superMemberFunction!.computeThisFunctionType(Nullability.nonNullable), | |
| ) as FunctionType; | |
| var function = member.function; | |
| var body = <js_ast.Statement>[]; | |
| var typeParameters = function.typeParameters; | |
| var superTypeParameters = superMethodType.typeParameters; | |
| var typeFormals = _emitTypeFormals(typeParameters); | |
| for (var i = 0; i < typeParameters.length; i++) { | |
| var typeParameter = typeParameters[i]; | |
| var superBound = superTypeParameters[i].bound; | |
| if (typeParameter.isCovariantByClass && !_types.isTop(superBound)) { | |
| body.add( | |
| js.statement('#.checkTypeBound(#, #, #, #)', [ | |
| _emitLibraryName(_rtiLibrary), | |
| typeFormals[i], | |
| _emitType(superBound), | |
| _propertyName(typeParameter.name!), | |
| js.string(member.name.text), | |
| ]), | |
| ); | |
| } | |
| } | |
| var jsParams = List<js_ast.Parameter>.from(typeFormals); | |
| var positionalParameters = function.positionalParameters; | |
| for (var i = 0, n = positionalParameters.length; i < n; i++) { | |
| var param = positionalParameters[i]; | |
| var jsParam = _emitIdentifier(param.parameterName); | |
| 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.parameterName == param.parameterName, | |
| ), | |
| )) { | |
| var name = _propertyName(param.parameterName); | |
| var paramType = superMethodType.namedParameters.firstWhere( | |
| (n) => n.name == param.parameterName, | |
| ); | |
| body.add( | |
| js.statement('if (#) #;', [ | |
| _namedArgumentProbe(name), | |
| _emitCast( | |
| js_ast.PropertyAccess(_namedArgumentTemp, name), | |
| paramType.type, | |
| ), | |
| ]), | |
| ); | |
| } | |
| } | |
| if (body.isEmpty) return const []; // No checks were needed. | |
| if (namedParameters.isNotEmpty) jsParams.add(_namedArgumentTemp); | |
| body.add(js.statement('return super.#(#);', [name, jsParams])); | |
| return [js_ast.Method(name, js_ast.Fun(jsParams, js_ast.Block(body)))]; | |
| } | |
| /// Emits a Dart factory constructor to a JS static method. | |
| js_ast.Method? _emitFactoryConstructor(Procedure node) { | |
| if (node.isExternal || isUnsupportedFactoryConstructor(node)) return null; | |
| var function = node.function; | |
| /// Note: factory constructors can't use `sync*`/`async*`/`async` bodies | |
| /// because it would return the wrong type, so we can assume `sync` here. | |
| /// | |
| /// We can also skip the logic in [_emitFunction] related to operator | |
| /// methods like ==, as well as generic method parameters. | |
| /// | |
| /// If a future Dart version allows factory constructors to take their | |
| /// own type parameters, this will need to be changed to call | |
| /// [_emitFunction] instead. | |
| var name = node.name.text; | |
| final savedTypeEnvironment = _currentTypeEnvironment; | |
| _currentTypeEnvironment = RtiTypeEnvironment([ | |
| ...function.typeParameters, | |
| ..._currentTypeEnvironment.classTypeParameters, | |
| ]); | |
| var jsBody = js_ast.Block( | |
| _withCurrentFunction(function, () { | |
| var block = _emitArgumentInitializers(function, name); | |
| block.add(_emitFunctionScopedBody(function)); | |
| return block; | |
| }), | |
| ); | |
| var jsName = _constructorName(name); | |
| memberNames[node] = jsName.valueWithoutQuotes; | |
| // Generic class constructors accept their RTI as their first argument. | |
| var method = js_ast.Method( | |
| jsName, | |
| js_ast.Fun([ | |
| if (_requiresRtiForInstantiation(node.enclosingClass)) _rtiParam, | |
| ..._emitParameters(function), | |
| ], jsBody), | |
| isStatic: true, | |
| )..sourceInformation = _nodeEnd(node.fileEndOffset); | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| return method; | |
| } | |
| /// Emits the expression necessary to access a constructor of [type]; | |
| js_ast.Expression _emitConstructorAccess(InterfaceType type) => | |
| _emitJSInterop(type.classNode) ?? _emitClassRef(type); | |
| /// This is called whenever a derived class needs to introduce a new field, | |
| /// shadowing a field or getter/setter pair on its parent. | |
| /// | |
| /// This is important because otherwise, trying to read or write the field | |
| /// would end up calling the getter or setter, and one of those might not even | |
| /// exist, resulting in a runtime error. Even if they did exist, that's the | |
| /// wrong behavior if a new field was declared. | |
| List<js_ast.Method> _emitVirtualFieldAccessor(Field field) { | |
| var virtualField = _classProperties!.virtualFields[field]!; | |
| var virtualFieldSymbol = _getSymbol(virtualField); | |
| var name = _declareMemberName(field); | |
| var getter = js.fun('function() { return this[#]; }', [virtualFieldSymbol]); | |
| var jsGetter = js_ast.Method(name, getter, isGetter: true) | |
| ..sourceInformation = _nodeStart(field); | |
| var body = <js_ast.Statement>[]; | |
| var value = _emitIdentifier('value'); | |
| var args = field.isFinal | |
| ? [js_ast.Super(), name, value] | |
| : [ | |
| js_ast.This(), | |
| virtualFieldSymbol, | |
| if (isCovariantField(field)) | |
| _emitCast(value, field.type) | |
| else | |
| value, | |
| ]; | |
| body.add(js.call('#[#] = #', args).toStatement()); | |
| var jsSetter = js_ast.Method( | |
| name, | |
| js_ast.Fun([value], js_ast.Block(body)), | |
| isSetter: true, | |
| )..sourceInformation = _nodeStart(field); | |
| return [jsGetter, jsSetter]; | |
| } | |
| /// Provide Dart getters and setters that forward to the underlying native | |
| /// field. Note that the Dart names are always symbolized to avoid | |
| /// conflicts. They will be installed as extension methods on the underlying | |
| /// native type. | |
| List<js_ast.Method> _emitNativeFieldAccessors(Field field) { | |
| // TODO(vsm): Can this by meta-programmed? | |
| // E.g., dart.nativeField(symbol, jsName) | |
| // Alternatively, perhaps it could be meta-programmed directly in | |
| // dart.registerExtensions? | |
| var jsMethods = <js_ast.Method>[]; | |
| assert(!field.isStatic); | |
| var name = _annotationName(field, isJSName) ?? field.name.text; | |
| // Generate getter | |
| var fn = js_ast.Fun([], js.block('{ return this.#; }', [name])); | |
| var method = js_ast.Method(_declareMemberName(field), fn, isGetter: true); | |
| jsMethods.add(method); | |
| // Generate setter | |
| if (!field.isFinal) { | |
| var value = _emitScopedId('value'); | |
| fn = js_ast.Fun([value], js.block('{ this.# = #; }', [name, value])); | |
| method = js_ast.Method(_declareMemberName(field), fn, isSetter: true); | |
| jsMethods.add(method); | |
| } | |
| return jsMethods; | |
| } | |
| /// Emit a getter (or setter) that simply forwards to the superclass getter | |
| /// (or setter). | |
| /// | |
| /// This is needed because in ES6, if you only override a getter | |
| /// (alternatively, a setter), then there is an implicit override of the | |
| /// setter (alternatively, the getter) that does nothing. | |
| js_ast.Method? _emitSuperAccessorWrapper( | |
| Procedure member, | |
| Map<String, Procedure> getters, | |
| Map<String, Procedure> setters, | |
| ) { | |
| if (member.isAbstract) return null; | |
| var name = member.name.text; | |
| var memberName = _declareMemberName(member); | |
| if (member.isGetter) { | |
| if (!setters.containsKey(name) && | |
| _classProperties!.inheritedSetters.contains(name)) { | |
| // Generate a setter that forwards to super. | |
| var fn = js.fun('function(value) { super[#] = value; }', [memberName]); | |
| return js_ast.Method(memberName, fn, isSetter: true); | |
| } | |
| } else { | |
| assert(member.isSetter); | |
| if (!getters.containsKey(name) && | |
| _classProperties!.inheritedGetters.contains(name)) { | |
| // Generate a getter that forwards to super. | |
| var fn = js.fun('function() { return super[#]; }', [memberName]); | |
| return js_ast.Method(memberName, fn, isGetter: true); | |
| } | |
| } | |
| return null; | |
| } | |
| /// Support for adapting dart:core Iterable to ES6 versions. | |
| /// | |
| /// This lets them use for-of loops transparently: | |
| /// <https://github.com/lukehoban/es6features#iterators--forof> | |
| /// | |
| /// This will return `null` if the adapter was already added on a super type, | |
| /// otherwise it returns the adapter code. | |
| // TODO(jmesserly): should we adapt `Iterator` too? | |
| js_ast.Method? _emitIterable(Class c) { | |
| var iterable = _hierarchy.getClassAsInstanceOf(c, _coreTypes.iterableClass); | |
| if (iterable == null) return null; | |
| 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<js_ast.Statement> 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<Field> fields) { | |
| if (_isSdkInternalRuntime(_currentLibrary!)) { | |
| /// Treat dart:_runtime fields as safe to eagerly evaluate. | |
| // TODO(jmesserly): it'd be nice to avoid this special case. | |
| var lazyFields = <Field>[]; | |
| var savedUri = _currentUri; | |
| // Helper functions to test if a constructor invocation is internal and | |
| // should be eagerly evaluated. | |
| 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<Member> members, | |
| js_ast.LiteralString Function(Member) emitMemberName, | |
| ) { | |
| var accessors = <js_ast.Method>[]; | |
| 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) { | |
| // A dummy setter is still required to indicate writeable. | |
| accessors.add( | |
| js_ast.Method( | |
| access, | |
| js_ast.Fun([_emitIdentifier('value')], js_ast.Block(const [])), | |
| 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_ast.Fun _emitStaticFieldInitializer(Field field) { | |
| return js_ast.Fun( | |
| [], | |
| js_ast.Block( | |
| _withLetScope(() { | |
| return [ | |
| js_ast.Return( | |
| _visitInitializer(field.initializer, field.annotations), | |
| ), | |
| ]; | |
| }), | |
| ), | |
| ); | |
| } | |
| List<js_ast.Statement> _withLetScope( | |
| List<js_ast.Statement> Function() visitBody, | |
| ) { | |
| var savedLetVariables = _letVariables; | |
| _letVariables = []; | |
| var body = visitBody(); | |
| var letVars = _initLetVariables(); | |
| if (letVars != null) body.insert(0, letVars); | |
| _letVariables = savedLetVariables; | |
| return body; | |
| } | |
| js_ast.PropertyAccess _emitTopLevelName(NamedNode n) { | |
| return _emitJSInterop(n) ?? | |
| _emitTopLevelNameNoExternalInterop(n, suffix: ''); | |
| } | |
| /// Like [_emitMemberName], but for declaration sites. | |
| /// | |
| /// Unlike call sites, we always have an element available, so we can use it | |
| /// directly rather than computing the relevant options for [_emitMemberName]. | |
| js_ast.Expression _declareMemberName(Member m, {bool useExtension = false}) { | |
| var c = m.enclosingClass; | |
| var actualUseExtension = | |
| useExtension || (c != null && _extensionTypes.isNativeClass(c)); | |
| return _emitMemberName( | |
| m.name.text, | |
| isStatic: m is Field ? m.isStatic : (m as Procedure).isStatic, | |
| useExtension: actualUseExtension, | |
| member: m, | |
| ); | |
| } | |
| /// This handles member renaming for private names and operators. | |
| /// | |
| /// Private names are generated using ES6 symbols: | |
| /// | |
| /// // At the top of the module: | |
| /// let _x = Symbol('_x'); | |
| /// let _y = Symbol('_y'); | |
| /// ... | |
| /// | |
| /// class Point { | |
| /// Point(x, y) { | |
| /// this[_x] = x; | |
| /// this[_y] = y; | |
| /// } | |
| /// get x() { return this[_x]; } | |
| /// get y() { return this[_y]; } | |
| /// } | |
| /// | |
| /// For user-defined operators the following names are allowed: | |
| /// | |
| /// <, >, <=, >=, ==, -, +, /, ~/, *, %, |, ^, &, <<, >>, >>>, []=, [], ~ | |
| /// | |
| /// They generate code like: | |
| /// | |
| /// x['+'](y) | |
| /// | |
| /// There are three exceptions: [], []= and unary -. | |
| /// The indexing operators we use `get` and `set` instead: | |
| /// | |
| /// x.get('hi') | |
| /// x.set('hi', 123) | |
| /// | |
| /// This follows the same pattern as ECMAScript 6 Map: | |
| /// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map> | |
| /// | |
| /// Unary minus looks like: `x._negate()`. | |
| /// | |
| /// Equality is a bit special, it is generated via the Dart `equals` runtime | |
| /// helper, that checks for null. The user defined method is called '=='. | |
| /// | |
| js_ast.Expression _emitMemberName( | |
| String name, { | |
| bool isStatic = false, | |
| bool? useExtension, | |
| Member? member, | |
| Class? memberClass, | |
| }) { | |
| // Static members skip the rename steps and may require JS interop renames. | |
| if (isStatic) { | |
| 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<Class, Map<String, Member?>>(); | |
| Member? _lookupForwardedMember(Class c, String name) { | |
| // We only care about public methods. | |
| if (name.startsWith('_')) return null; | |
| var map = _forwardingCache.putIfAbsent(c, () => {}); | |
| return map.putIfAbsent( | |
| name, | |
| () => | |
| _hierarchy.getDispatchTarget(c, Name(name)) ?? | |
| _hierarchy.getDispatchTarget(c, Name(name), setter: true), | |
| ); | |
| } | |
| js_ast.LiteralString _emitStaticMemberName(String name, [NamedNode? member]) { | |
| if (member != null) { | |
| var jsName = _emitJSInteropExternalStaticMemberName(member); | |
| if (jsName != null) return jsName; | |
| // Allow the Dart SDK to assign names to statics with the @JSExportName | |
| // annotation. | |
| var exportName = _jsExportName(member); | |
| if (exportName != null) return _propertyName(exportName); | |
| } | |
| if (member is Procedure && member.isFactory) { | |
| return _constructorName(member.name.text); | |
| } | |
| switch (name) { | |
| // Reserved for the compiler to do `x as T`. | |
| case 'as': | |
| // Reserved for the SDK to compute `Type.toString()`. | |
| case 'name': | |
| // Reserved by JS, not a valid static member name. | |
| case 'prototype': | |
| name += '_'; | |
| default: | |
| // All trailing underscores static names are reserved for the compiler | |
| // or SDK libraries. | |
| // | |
| // If user code uses them, add an extra `_`. | |
| // | |
| // This also avoids collision with the renames above, e.g. `static as` | |
| // and `static as_` will become `as_` and `as__`. | |
| if (name.endsWith('_')) { | |
| name += '_'; | |
| } | |
| } | |
| return _propertyName(name); | |
| } | |
| /// If [f] is a function passed to JS, make it throw at runtime when called if | |
| /// it isn't wrapped with `allowInterop`. | |
| /// | |
| /// Arguments which are _directly_ wrapped at the site they are passed are | |
| /// unmodified. | |
| Expression _assertInterop(Expression f) { | |
| // Erasing any extension types here for legacy JS interop support but if | |
| // using the new extension type interop the type system requires that | |
| // `.toJS` was called. | |
| var type = f.getStaticType(_staticTypeContext).extensionTypeErasure; | |
| if (type is FunctionType || | |
| (type is InterfaceType && type.classNode == _coreTypes.functionClass)) { | |
| if (!isAllowInterop(f)) { | |
| return StaticInvocation( | |
| _assertInteropMethod, | |
| Arguments(ExpressionList(f), types: DartTypeList(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<Class>()..add(c); | |
| while (classes.isNotEmpty) { | |
| var c = classes.removeFirst(); | |
| var classesToCheck = [ | |
| if (c.supertype != null) c.supertype!.classNode, | |
| for (var t in c.implementedTypes) 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(); | |
| for (var p in procedures) { | |
| if (!p.isAccessor) { | |
| _moduleItems.add(_emitLibraryFunction(p)); | |
| } | |
| // TODO(#57049): We tag all static members because we don't know if | |
| // they've been changed after a hot reload. This won't be necessary if we | |
| // can tag them during the delta diff phase. | |
| if (p.isStatic && _reifyTearoff(p) && !p.isExternal) { | |
| var nameExpr = _emitTopLevelName(p); | |
| _moduleItems.add( | |
| _emitFunctionTagged( | |
| nameExpr, | |
| p.function.computeThisFunctionType(p.enclosingLibrary.nonNullable), | |
| asLazy: true, | |
| ).toStatement(), | |
| ); | |
| } | |
| } | |
| _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<Procedure> accessors) { | |
| if (accessors.isEmpty) return; | |
| _moduleItems.add( | |
| _runtimeStatement('copyProperties(#, { # })', [ | |
| _emitLibraryName(_currentLibrary!), | |
| accessors.map(_emitLibraryAccessor).toList(), | |
| ]), | |
| ); | |
| } | |
| js_ast.Method _emitLibraryAccessor(Procedure node) { | |
| var savedUri = _currentUri; | |
| _staticTypeContext.enterMember(node); | |
| _currentUri = node.fileUri; | |
| var name = node.name.text; | |
| 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 = <js_ast.Statement>[]; | |
| var fn = _emitFunction( | |
| p.function, | |
| p.name.text, | |
| functionBody: _toSourceLocation(p.fileOffset), | |
| functionEnd: _toSourceLocation(p.fileEndOffset), | |
| )..sourceInformation = _nodeEnd(p.fileEndOffset); | |
| if (_currentLibrary!.importUri.isScheme('dart') && | |
| _isInlineJSFunction(p.function.body)) { | |
| fn = js_ast.simplifyPassThroughArrowFunCallBody(fn); | |
| } | |
| var nameExpr = _emitTopLevelName(p); | |
| var jsName = _safeFunctionNameForSafari(p.name.text, fn); | |
| var functionName = _emitScopedId(jsName); | |
| procedureIdentifiers[p] = functionName; | |
| body.add( | |
| js.statement('# = #', [nameExpr, js_ast.NamedFunction(functionName, fn)]), | |
| ); | |
| _currentUri = savedUri; | |
| _staticTypeContext.leaveMember(p); | |
| if (_options.dynamicModule && _isDynamicModuleEntryPoint(p, _coreTypes)) { | |
| if (_dynamicEntrypoint == null) { | |
| if (p.function.requiredParameterCount > 0) { | |
| // TODO(sigmund): this error should be caught by a kernel checker that | |
| // runs prior to DDC. | |
| throw StateError( | |
| 'Entrypoint ${p.name.text} must accept being called ' | |
| 'with 0 arguments.', | |
| ); | |
| } else { | |
| _dynamicEntrypoint = p; | |
| } | |
| } else { | |
| // TODO(sigmund): this error should be caught by a kernel checker that | |
| // runs prior to DDC. | |
| throw StateError('A module should define a single entrypoint.'); | |
| } | |
| } | |
| return js_ast.Statement.from(body); | |
| } | |
| /// Choose a safe name for [fn]. | |
| /// | |
| /// Most of the time we use [candidateName], except if the name collides | |
| /// with a parameter name and the function contains default parameter values. | |
| /// | |
| /// In ES6, functions containing default parameter values, which DDC | |
| /// generates when Dart uses positional optional parameters, cannot have | |
| /// two parameters with the same name. Because we have a similar restriction | |
| /// in Dart, this is not normally an issue we need to pay attention to. | |
| /// However, a bug in Safari makes it a syntax error to have the function | |
| /// name overlap with the parameter names as well. This rename works around | |
| /// such bug (dartbug.com/43520). | |
| static String _safeFunctionNameForSafari( | |
| String candidateName, | |
| js_ast.Fun fn, | |
| ) { | |
| if (fn.params.any((p) => p is js_ast.DestructuredVariable)) { | |
| while (fn.params.any((a) => a.parameterName == candidateName)) { | |
| candidateName = '$candidateName\$'; | |
| } | |
| } | |
| return candidateName; | |
| } | |
| js_ast.Expression _emitFunctionTagged( | |
| js_ast.Expression fn, | |
| FunctionType type, { | |
| bool asLazy = false, | |
| }) { | |
| var typeRep = _emitType( | |
| // Avoid tagging a closure as Function? or Function* | |
| type.withDeclaredNullability(Nullability.nonNullable), | |
| ); | |
| if (type.typeParameters.isEmpty) { | |
| return asLazy | |
| ? _runtimeCall('lazyFn(#, () => #)', [fn, typeRep]) | |
| : _runtimeCall('fn(#, #)', [fn, typeRep]); | |
| } else { | |
| var typeParameterDefaults = [ | |
| for (var parameter in type.typeParameters) | |
| _emitType(parameter.defaultType), | |
| ]; | |
| var defaultInstantiatedBounds = _emitConstList( | |
| const DynamicType(), | |
| typeParameterDefaults, | |
| ); | |
| return asLazy | |
| ? _runtimeCall('lazyGFn(#, () => #, () => #)', [ | |
| fn, | |
| typeRep, | |
| defaultInstantiatedBounds, | |
| ]) | |
| : _runtimeCall('gFn(#, #, #)', [ | |
| fn, | |
| typeRep, | |
| defaultInstantiatedBounds, | |
| ]); | |
| } | |
| } | |
| /// Returns an expression that evaluates to the rti object from the dart:_rti | |
| /// library that represents [type]. | |
| /// | |
| /// [emitJSInteropGenericClassTypeParametersAsAny] indicates that we should | |
| /// emit the statically declared type as a JS interop generic class's type | |
| /// argument (rather than substituting Any). Any is required for correctness | |
| /// in most cases except for uses in non-external JS interop factories. | |
| /// Note: This only applies to the old style package:js interop and isn't | |
| /// necessary for any forms of static JS interop. | |
| js_ast.Expression _emitType( | |
| DartType type, { | |
| bool emitJSInteropGenericClassTypeParametersAsAny = true, | |
| }) { | |
| /// Returns an expression that evaluates a type [recipe] within the type | |
| /// [environment]. | |
| /// | |
| /// At runtime the expression will evaluate to an rti object. | |
| js_ast.Expression emitRtiEval( | |
| js_ast.Expression environment, | |
| String recipe, | |
| ) => js.call('#.#("$recipe")', [ | |
| environment, | |
| _emitMemberName('_eval', memberClass: _rtiClass), | |
| ]); | |
| /// Returns an expression that binds a type [parameter] within the type | |
| /// [environment]. | |
| /// | |
| /// At runtime the expression will evaluate to an rti object that has been | |
| /// extended to include the provided [parameter]. | |
| js_ast.Expression emitRtiBind( | |
| js_ast.Expression environment, | |
| TypeParameter parameter, | |
| ) { | |
| return js.call('#.#(#)', [ | |
| environment, | |
| _emitMemberName('_bind', memberClass: _rtiClass), | |
| _emitTypeParameter(parameter), | |
| ]); | |
| } | |
| /// Returns an expression that evaluates a type [recipe] in a type | |
| /// [environment] resulting in an rti object. | |
| js_ast.Expression evalInEnvironment( | |
| DDCTypeEnvironment environment, | |
| String recipe, | |
| ) { | |
| switch (environment) { | |
| case EmptyTypeEnvironment(): | |
| // Cache ground types in the type table for fast lookup. The table | |
| // will lazily lookup the RTI object on first access and then replace | |
| // the lazy getter with the initialized RTI object. | |
| return _typeTable.nameType( | |
| type, | |
| js.call('#._Universe.eval(#, "$recipe", true)', [ | |
| _emitLibraryName(_rtiLibrary), | |
| _runtimeCall('typeUniverse'), | |
| ]), | |
| ); | |
| case BindingTypeEnvironment(): | |
| js_ast.Expression env; | |
| if (environment.isSingleTypeParameter) { | |
| // An environment with a single type parameter can be simplified to | |
| // just that parameter. | |
| env = _emitTypeParameter(environment.functionTypeParameters.single); | |
| // Skip a no-op evaluation and just return the parameter. | |
| if (recipe == '0') return env; | |
| } else { | |
| var environmentTypes = environment.functionTypeParameters; | |
| // By convention we create a binding environment with "dynamic" as | |
| // the base. | |
| env = _emitType(const DynamicType()); | |
| // Bind all type arguments to it. | |
| for (var typeParameter in environmentTypes) { | |
| env = emitRtiBind(env, typeParameter); | |
| } | |
| } | |
| return emitRtiEval(env, recipe); | |
| case RtiTypeEnvironment(): | |
| // RTI type environments take the form of a preconstructed RTI that | |
| // is accessible via a known parameter name. | |
| var env = _rtiParam; | |
| return emitRtiEval(env, recipe); | |
| case ClassTypeEnvironment(): | |
| // Class type environments are already constructed and attached to the | |
| // instance of a generic class. | |
| var env = js.call('#.instanceType(this)', [ | |
| _emitLibraryName(_rtiLibrary), | |
| ]); | |
| return emitRtiEval(env, recipe); | |
| case ExtendedTypeEnvironment(): | |
| // Class type environments are already constructed and attached to the | |
| // instance of a generic class, but function type parameters need to | |
| // be bound. | |
| var env = js.call('#.instanceType(this)', [ | |
| _emitLibraryName(_rtiLibrary), | |
| ]); | |
| // Bind extra type parameters. | |
| for (var parameter in environment.functionTypeParameters) { | |
| env = emitRtiBind(env, parameter); | |
| } | |
| return emitRtiEval(env, recipe); | |
| } | |
| _typeCompilationError( | |
| type, | |
| 'Unexpected DDCTypeEnvironment type (${environment.runtimeType}).', | |
| ); | |
| } | |
| var normalizedType = _futureOrNormalizer.normalize( | |
| type.extensionTypeErasure, | |
| ); | |
| try { | |
| var result = _typeRecipeGenerator.recipeInEnvironment( | |
| normalizedType, | |
| _currentTypeEnvironment, | |
| emitJSInteropGenericClassTypeParametersAsAny: | |
| emitJSInteropGenericClassTypeParametersAsAny, | |
| ); | |
| var typeRep = evalInEnvironment( | |
| result.requiredEnvironment, | |
| result.recipe, | |
| ); | |
| return typeRep; | |
| } on UnsupportedError catch (e) { | |
| _typeCompilationError(normalizedType, e.message ?? 'Unknown Error'); | |
| } | |
| } | |
| js_ast.Expression _emitInvalidNode(Node node, [String message = '']) { | |
| if (message.isNotEmpty) message += ' '; | |
| return _runtimeCall('throwUnimplementedError(#)', [ | |
| js.escapedString('node <${node.runtimeType}> $message`$node`'), | |
| ]); | |
| } | |
| /// Emits a reference to the class described by [type]. | |
| /// | |
| /// The nullability of [type] is not considered because it is meaningless when | |
| /// describing a reference to the class itself. | |
| /// | |
| /// For generic classes, type arguments are not needed since they are | |
| /// resolved late via an RTI lookup. | |
| /// | |
| /// Note that for `package:js` types, this will emit the class we emitted | |
| /// using `_emitJSInteropClassNonExternalMembers`, and not the runtime type | |
| /// that we synthesize for `package:js` types. | |
| 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(); | |
| _incrementalImports.clear(); | |
| _privateNames.clear(); | |
| _symbolContainer.setIncrementalMode(); | |
| _incrementalMode = true; | |
| _constTableCache = ModuleItemContainer<String>.asArray('C'); | |
| _constLazyAccessors.clear(); | |
| _constAliasCache.clear(); | |
| _uriContainer = ModuleItemContainer<String>.asArray('I'); | |
| _typeTable.typeContainer.setIncrementalMode(); | |
| } | |
| /// Emits function after initial compilation. | |
| /// | |
| /// Emits function from kernel [functionNode] with name [name] in the context | |
| /// of [library] and [cls], after the initial compilation of the module is | |
| /// finished. For example, this happens in expression compilation during | |
| /// expression evaluation initiated by the user from the IDE and coordinated | |
| /// by the debugger. | |
| /// Triggers incremental mode, which only emits symbols, types, constants, | |
| /// libraries, and uris referenced in the expression compilation result. | |
| @override | |
| js_ast.Fun emitFunctionIncremental( | |
| List<ModuleItem> 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 = <js_ast.Statement>[]; | |
| _emitExtensionSymbols(extensionSymbols); | |
| // Add all elements from tables accessed in the function | |
| var body = js_ast.Block([ | |
| ...extensionSymbols, | |
| ..._typeTable.dischargeBoundTypes(), | |
| ..._symbolContainer.emit(), | |
| ..._emitConstTable(), | |
| ..._uriContainer.emit(), | |
| ...fun.body.statements, | |
| ]); | |
| // Import all necessary libraries, including libraries accessed from the | |
| // current module and libraries accessed from the type table. | |
| for (var library in _typeTable.incrementalLibraries()) { | |
| _emitLibraryName(library); | |
| } | |
| _emitImports(items); | |
| _emitExportsAsImports(items, _currentLibrary!); | |
| return js_ast.Fun(fun.params, body); | |
| } | |
| List<js_ast.Statement> _emitConstTable() { | |
| var constTable = <js_ast.Statement>[]; | |
| if (_constLazyAccessors.isNotEmpty) { | |
| constTable.add( | |
| js.statement('const # = Object.create(null);', [_constTable]), | |
| ); | |
| constTable.add( | |
| _runtimeStatement('defineLazy(#, { # })', [ | |
| _constTable, | |
| _constLazyAccessors, | |
| ]), | |
| ); | |
| constTable.addAll(_constTableCache.emit()); | |
| } | |
| return constTable; | |
| } | |
| js_ast.Fun _emitFunction( | |
| FunctionNode f, | |
| String? name, { | |
| SourceLocation? functionEnd, | |
| SourceLocation? functionBody, | |
| }) { | |
| var savedTypeEnvironment = _currentTypeEnvironment; | |
| if (f.typeParameters.isNotEmpty) { | |
| _currentTypeEnvironment = _currentTypeEnvironment.extend( | |
| f.typeParameters, | |
| ); | |
| } | |
| var formals = _emitParameters(f); | |
| var typeFormals = _emitTypeFormals(f.typeParameters); | |
| var parent = f.parent; | |
| if (_reifyGenericFunction(parent is Member ? parent : null)) { | |
| formals.insertAll(0, typeFormals); | |
| } | |
| // TODO(jmesserly): need a way of determining if parameters are | |
| // potentially mutated in Kernel. For now we assume all parameters are. | |
| _enterFunction(name, formals, () => true); | |
| var block = js_ast.Block( | |
| _withCurrentFunction(f, () { | |
| final bodyPrefix = _emitArgumentInitializers(f, name); | |
| // Do the async transformation before adding parameter initialization | |
| // logic. Any parameter initialization should be performed synchronously | |
| // before the async body is evaluated. | |
| final bodyFn = js_ast.Fun( | |
| formals, | |
| js_ast.Block([_emitFunctionScopedBody(f)]), | |
| ); | |
| final rewrittenFunction = _rewriteAsyncFunction( | |
| bodyFn, | |
| f.asyncMarker, | |
| name, | |
| f.emittedValueType, | |
| functionEnd: functionEnd, | |
| functionBody: functionBody, | |
| bodyPrefix: bodyPrefix, | |
| ); | |
| formals = rewrittenFunction.params; | |
| return rewrittenFunction.body.statements; | |
| }), | |
| ); | |
| block = _exitFunction(formals, block); | |
| var fn = js_ast.Fun(formals, block); | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| return fn; | |
| } | |
| /// Transforms [fun]'s body to support async execution if the function is | |
| /// async, sync*, or async*. | |
| /// | |
| /// [bodyPrefix] will get prepended to the body of the rewritten function and | |
| /// any references to parameters within it will be replaced with the correct | |
| /// temporary ID for that parameter. | |
| js_ast.Fun _rewriteAsyncFunction( | |
| js_ast.Fun fun, | |
| AsyncMarker asyncMarker, | |
| String? name, | |
| DartType? asyncType, { | |
| SourceLocation? functionEnd, | |
| SourceLocation? functionBody, | |
| List<js_ast.Statement>? bodyPrefix, | |
| }) { | |
| AsyncRewriterBase? asyncRewriter; | |
| final bodyName = _emitScopedId('t\$async${name ?? 'Body'}'); | |
| switch (asyncMarker) { | |
| case AsyncMarker.Sync: | |
| break; | |
| case AsyncMarker.Async: | |
| asyncRewriter = AsyncRewriter( | |
| asyncStart: _emitTopLevelNameNoExternalInterop(_asyncStartMember), | |
| asyncAwait: _emitTopLevelNameNoExternalInterop(_asyncAwaitMember), | |
| asyncReturn: _emitTopLevelNameNoExternalInterop(_asyncReturnMember), | |
| asyncRethrow: _emitTopLevelNameNoExternalInterop(_asyncRethrowMember), | |
| completerFactory: _emitTopLevelNameNoExternalInterop( | |
| _asyncMakeCompleterMember, | |
| ), | |
| completerFactoryTypeArguments: [_emitType(asyncType!)], | |
| wrapBody: _emitTopLevelNameNoExternalInterop( | |
| _asyncWrapJsFunctionMember, | |
| ), | |
| bodyName: bodyName, | |
| ); | |
| case AsyncMarker.SyncStar: | |
| asyncRewriter = SyncStarRewriter( | |
| makeSyncStarIterable: _emitTopLevelNameNoExternalInterop( | |
| _syncStarMakeIterableMember, | |
| ), | |
| syncStarIterableTypeArgument: _emitType(asyncType!), | |
| iteratorCurrentValueProperty: _emitMemberName( | |
| '_current', | |
| member: _syncStarIteratorCurrentMember, | |
| ), | |
| iteratorDatumProperty: _emitMemberName( | |
| '_datum', | |
| member: _syncStarIteratorDatumMember, | |
| ), | |
| yieldStarSelector: _emitMemberName( | |
| '_yieldStar', | |
| member: _syncStarIteratorYieldStarMember, | |
| ), | |
| bodyName: bodyName, | |
| ); | |
| case AsyncMarker.AsyncStar: | |
| asyncRewriter = AsyncStarRewriter( | |
| asyncStarHelper: _emitTopLevelNameNoExternalInterop( | |
| _asyncStarHelperMember, | |
| ), | |
| streamOfController: _emitTopLevelNameNoExternalInterop( | |
| _asyncStreamOfControllerMember, | |
| ), | |
| newController: _emitTopLevelNameNoExternalInterop( | |
| _asyncMakeAsyncStarStreamControllerMember, | |
| ), | |
| newControllerTypeArguments: [_emitType(asyncType!)], | |
| yieldExpression: _emitStaticGet( | |
| _asyncIterationMarkerYieldSingleMember, | |
| ), | |
| yieldStarExpression: _emitStaticGet( | |
| _asyncIterationMarkerYieldStarMember, | |
| ), | |
| wrapBody: _emitTopLevelNameNoExternalInterop( | |
| _asyncWrapJsFunctionMember, | |
| ), | |
| bodyName: bodyName, | |
| ); | |
| } | |
| if (asyncRewriter != null) { | |
| return asyncRewriter.rewrite( | |
| fun, | |
| functionBody, | |
| functionEnd, | |
| bodyPrefix: bodyPrefix, | |
| ); | |
| } else if (bodyPrefix != null) { | |
| fun.body.statements.insertAll(0, bodyPrefix); | |
| } | |
| return fun; | |
| } | |
| js_ast.Parameter _emitParameter( | |
| Variable 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<js_ast.Parameter> _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<js_ast.Parameter>.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<js_ast.Identifier> _emitTypeFormals( | |
| List</*TypeParameter | StructuralParameter */ Object> typeFormals, | |
| ) { | |
| assert( | |
| typeFormals is List<TypeParameter> || | |
| typeFormals is List<StructuralParameter>, | |
| ); | |
| return typeFormals | |
| .map((t) => _emitIdentifier(getTypeParameterName(t))) | |
| .toList(); | |
| } | |
| List<js_ast.Statement> _withCurrentFunction( | |
| FunctionNode fn, | |
| List<js_ast.Statement> Function() action, | |
| ) { | |
| var savedFunction = _currentFunction; | |
| _currentFunction = fn; | |
| _nullableInference.enterFunction(fn); | |
| var result = _withLetScope(action); | |
| _nullableInference.exitFunction(fn); | |
| _currentFunction = savedFunction; | |
| return result; | |
| } | |
| T _superDisallowed<T>(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<T>(Procedure member, T Function() action) { | |
| // Mixin applications require using 'super' in calls to members of | |
| // the super class. Store this information to disable non-virtual | |
| // super field access optimization when compiling the member body. | |
| var savedOptimizeNonVirtualFieldAccess = _optimizeNonVirtualFieldAccess; | |
| _optimizeNonVirtualFieldAccess = | |
| member.stubKind != ProcedureStubKind.ConcreteMixinStub; | |
| var result = action(); | |
| _optimizeNonVirtualFieldAccess = savedOptimizeNonVirtualFieldAccess; | |
| return result; | |
| } | |
| /// Returns true if the underlying type does not accept a null value. | |
| bool _mustBeNonNullable(DartType type) => | |
| type.nullability == Nullability.nonNullable; | |
| /// Emits argument initializers, which handles optional/named args, as well | |
| /// as generic type checks needed due to our covariance. | |
| List<js_ast.Statement> _emitArgumentInitializers( | |
| FunctionNode f, | |
| String? name, | |
| ) { | |
| var body = <js_ast.Statement>[]; | |
| _emitCovarianceBoundsCheck(f.typeParameters, body); | |
| void initParameter( | |
| FunctionParameter p, | |
| js_ast.Identifier jsParam, | |
| bool isOptional, | |
| ) { | |
| // When the parameter is covariant, insert the null check before the | |
| // covariant cast to avoid a TypeError when testing equality with null. | |
| if (name == '==') { | |
| // In Dart `operator ==` methods are not called with a null argument. | |
| // This is handled before calling them. For performance reasons, we push | |
| // this check inside the method, to simplify our `equals` helper. | |
| // | |
| // TODO(jmesserly): in most cases this check is not necessary, because | |
| // the Dart code already handles it (typically by an `is` check). | |
| // Eliminate it when possible. | |
| body.add(js.statement('if (# == null) return false;', [jsParam])); | |
| } | |
| if (isCovariantParameter(p) || | |
| // TODO(52582): This should be unreachable once the CFE ensures that | |
| // redirecting factories parameter types match the target constructor. | |
| // Matches dart2js check semantics for redirecting factory tearoffs. | |
| // If a non-nullable optional argument with a null initializer is | |
| // detected, we add an additional covariant check at runtime. | |
| (f.parent is Procedure && | |
| isOptional && | |
| isConstructorTearOffLowering(f.parent as Procedure) && | |
| !p.type.isPotentiallyNullable && | |
| !p.defaultValue! | |
| .getStaticType(_staticTypeContext) | |
| .isPotentiallyNonNullable)) { | |
| var castExpr = _emitCast(jsParam, p.type); | |
| if (!identical(castExpr, jsParam)) body.add(castExpr.toStatement()); | |
| } | |
| if (name == '==') return; | |
| if (_annotatedNullCheck(p.annotations)) { | |
| body.add(_nullParameterCheck(jsParam)); | |
| } | |
| } | |
| var counter = 0; | |
| for (var p in f.positionalParameters) { | |
| var jsParam = _emitVariableRef(p); | |
| if (_checkParameters) { | |
| initParameter(p, jsParam, counter >= f.requiredParameterCount); | |
| } | |
| counter++; | |
| } | |
| for (var p in f.namedParameters) { | |
| // Parameters will be passed using their real names, not the (possibly | |
| // renamed) local variable. | |
| var jsParam = _emitVariableDef(p); | |
| var paramName = _propertyName(p.parameterName); | |
| var defaultValue = _defaultParamValue(p); | |
| body.add( | |
| js.statement('let # = # && # ? #.# : #;', [ | |
| jsParam, | |
| _namedArgumentTemp, | |
| _namedArgumentProbe(paramName), | |
| _namedArgumentTemp, | |
| paramName, | |
| defaultValue, | |
| ]), | |
| ); | |
| if (_checkParameters) { | |
| initParameter(p, jsParam, !p.isRequired); | |
| } | |
| } | |
| // '_checkParameters = false' is only needed once, while processing formal | |
| // parameters of the synthetic function from expression evaluation - it | |
| // will be called from emitFunctionIncremental, which is a top-level API | |
| // for expression compilation. | |
| // Here we either are done with processing those formals, or compiling | |
| // something else (in which case _checkParameters is already true). | |
| _checkParameters = true; | |
| return body; | |
| } | |
| bool _annotatedNullCheck(List<Expression> annotations) => | |
| annotations.any(_nullableInference.isNullCheckAnnotation); | |
| bool _reifyGenericFunction(Member? m) => | |
| m == null || | |
| // JS interop members should not pass type arguments. | |
| !isJsMember(m) && | |
| !(m.enclosingLibrary.importUri.isScheme('dart') && | |
| m.annotations.any( | |
| (a) => isBuiltinAnnotation(a, '_js_helper', 'NoReifyGeneric'), | |
| )); | |
| js_ast.Statement _nullParameterCheck(js_ast.Expression param) { | |
| var call = _runtimeCall('argumentError((#))', [param]); | |
| return js.statement('if (# == null) #;', [param, call]); | |
| } | |
| js_ast.Expression _defaultParamValue(Variable 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> typeFormals, | |
| List<js_ast.Statement> body, | |
| ) { | |
| for (var t in typeFormals) { | |
| var bound = t.bound.extensionTypeErasure; | |
| if (t.isCovariantByClass && !_types.isTop(bound)) { | |
| // TODO(nshahan): Ensure we have a name for the method and then | |
| // just call the `checkTypeBound()` in the rti library directly. | |
| body.add( | |
| _runtimeStatement('checkTypeBound(#, #, #)', [ | |
| _emitType(TypeParameterType(t, Nullability.undetermined)), | |
| _emitType(bound), | |
| _propertyName(t.name!), | |
| ]), | |
| ); | |
| } | |
| } | |
| } | |
| js_ast.Statement _visitStatement(Statement s) { | |
| var result = s.accept(this); | |
| // In most cases, a Dart expression statement with a child expression | |
| // compile to a JS expression statement with a child expression. | |
| // | |
| // ExpressionStatement js_ast.ExpressionStatement | |
| // | --> compiles to --> | | |
| // Expression js_ast.Expression | |
| // | |
| // Both the expression statement and child expression nodes contain their | |
| // own source location information. | |
| // | |
| // In the case of a debugger() call, the code compiles to a single node. | |
| // | |
| // ExpressionStatement js_ast.DebuggerStatement | |
| // | --> compiles to --> | |
| // Expression | |
| // | |
| // The js_ast.DebuggerStatement already has the correct source information | |
| // attached so we avoid overwriting with the incorrect source location from | |
| // [s]. | |
| // TODO(jmesserly): is the `is! Block` still necessary? | |
| if (!(s is Block || result is js_ast.DebuggerStatement)) { | |
| result.sourceInformation ??= _nodeStart(s); | |
| } | |
| // The statement might be the target of a break or continue with a label. | |
| var name = _labelNames[s]; | |
| if (name != null) result = js_ast.LabeledStatement(name, result); | |
| return result; | |
| } | |
| js_ast.Statement _emitFunctionScopedBody(FunctionNode f) { | |
| var 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.parameterName, | |
| for (var p in f.namedParameters) p.parameterName, | |
| }; | |
| return body.toScopedBlock(parameterNames); | |
| } | |
| return body; | |
| } | |
| /// Visits [nodes] with [_visitExpression]. | |
| List<js_ast.Expression> _visitExpressionList(Iterable<Expression> nodes) { | |
| return nodes.map(_visitExpression).toList(); | |
| } | |
| /// Generates an expression for a boolean conversion context (if, while, &&, | |
| /// etc.), where conversions and null checks are implemented via `dart.test` | |
| /// to give a more helpful message. | |
| // TODO(sra): When nullablility is available earlier, it would be cleaner to | |
| // build an input AST where the boolean conversion is a single AST node. | |
| js_ast.Expression _visitTest(Expression node) { | |
| if (node is Not) { | |
| return visitNot(node); | |
| } | |
| if (node is LogicalExpression) { | |
| js_ast.Expression shortCircuit(String code) { | |
| return js.call(code, [_visitTest(node.left), _visitTest(node.right)]); | |
| } | |
| var op = node.operatorEnum; | |
| if (op == LogicalExpressionOperator.AND) return shortCircuit('# && #'); | |
| if (op == LogicalExpressionOperator.OR) return shortCircuit('# || #'); | |
| } | |
| if (node is AsExpression && node.isTypeError) { | |
| assert( | |
| node.getStaticType(_staticTypeContext) == | |
| _types.coreTypes.boolNonNullableRawType, | |
| ); | |
| return _runtimeCall('dtest(#)', [_visitExpression(node.operand)]); | |
| } | |
| var result = _visitExpression(node); | |
| if (_isNullable(node)) result = _runtimeCall('test(#)', [result]); | |
| return result; | |
| } | |
| js_ast.Expression _visitExpression(Expression e) { | |
| if (e is ConstantExpression) { | |
| return visitConstant(e.constant); | |
| } | |
| var result = e.accept(this); | |
| result.sourceInformation ??= _nodeStart(e); | |
| return result; | |
| } | |
| /// Gets the start position of [node] for use in source mapping. | |
| /// | |
| /// This is the most common kind of marking, and is used for most expressions | |
| /// and statements. | |
| SourceLocation? _nodeStart(TreeNode node) => node is StringConcatenation | |
| // Manually selecting the location of the first element to work around the | |
| // location on the StringConcatenation node that points to the end of | |
| // String. See https://github.com/dart-lang/sdk/issues/55690. | |
| ? _toSourceLocation(node.expressions.first.fileOffset) | |
| : _toSourceLocation(node.fileOffset); | |
| /// Gets the end position of [node] for use in source mapping. | |
| /// | |
| /// This is mainly used for things that compile to JS functions. JS wants a | |
| /// marking on the end of all functions for stepping purposes. | |
| /// | |
| /// This can be used to complete a hover span, when we know the start position | |
| /// has already been emitted. For example, `foo.bar` we only need to mark the | |
| /// end of `.bar` to ensure `foo.bar` has a hover tooltip. | |
| NodeEnd? _nodeEnd(int endOffset) { | |
| var loc = _toSourceLocation(endOffset); | |
| return loc != null ? NodeEnd(loc) : null; | |
| } | |
| /// Combines [_nodeStart] with the variable name length to produce a hoverable | |
| /// span for the variable. | |
| // | |
| // TODO(jmesserly): we need a lot more nodes to support hover. | |
| NodeSpan? _variableSpan(int offset, int nameLength) { | |
| var start = _toSourceLocation(offset); | |
| var end = _toSourceLocation(offset + nameLength); | |
| return start != null && end != null ? NodeSpan(start, end) : null; | |
| } | |
| SourceLocation? _toSourceLocation(int offset) { | |
| if (offset == -1) return null; | |
| var fileUri = _currentUri; | |
| if (fileUri == null) return null; | |
| try { | |
| var loc = _component.getLocation(fileUri, offset); | |
| if (loc == null || loc.line < 0) return null; | |
| return SourceLocation( | |
| offset, | |
| sourceUrl: fileUri, | |
| line: loc.line - 1, | |
| column: loc.column - 1, | |
| ); | |
| } on StateError catch (_) { | |
| // TODO(jmesserly): figure out why this is throwing. Perhaps the file URI | |
| // and offset are mismatched and don't correspond to the same source? | |
| return null; | |
| } on RangeError catch (_) { | |
| return null; | |
| } | |
| } | |
| /// Adds a hover comment for Dart node using JS expression [expr], where | |
| /// that expression would not otherwise not be generated into source code. | |
| /// | |
| /// For example, top-level and static fields are defined as lazy properties, | |
| /// on the library/class, so their access expressions do not appear in the | |
| /// source code. | |
| HoverComment? _hoverComment( | |
| js_ast.Expression expr, | |
| int offset, | |
| int nameLength, | |
| ) { | |
| var start = _toSourceLocation(offset); | |
| var end = _toSourceLocation(offset + nameLength); | |
| return start != null && end != null ? HoverComment(expr, start, end) : null; | |
| } | |
| @override | |
| js_ast.Statement visitExpressionStatement(ExpressionStatement node) { | |
| var expr = node.expression; | |
| if (expr is StaticInvocation) { | |
| if (isInlineJS(expr.target)) { | |
| return _emitInlineJSCode(expr).toStatement(); | |
| } | |
| if (_isDebuggerCall(expr.target)) { | |
| return _emitDebuggerCall(expr).toStatement(); | |
| } | |
| } | |
| 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.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('<unknown source>') | |
| 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<LabeledStatement>? 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 = <LabeledStatement>[]; | |
| while (body is LabeledStatement) { | |
| var labeled = body; | |
| _currentContinueTargets.add(labeled); | |
| _effectiveTargets[labeled] = loop; | |
| body = labeled.body; | |
| } | |
| return body; | |
| } | |
| T _translateLoop<T extends js_ast.Statement>( | |
| Statement node, | |
| T Function() action, | |
| ) { | |
| List<LabeledStatement>? savedBreakTargets; | |
| if (_currentBreakTargets.isNotEmpty && | |
| _effectiveTargets[_currentBreakTargets.first] != node) { | |
| // If breaking without a label targets some other (outer) loop, then | |
| // this loop prevents breaking to that loop without a label. This loop | |
| // was not labeled for a break in Kernel, otherwise it would be the | |
| // effective target of the current break targets, so it is not itself the | |
| // target of a break. | |
| savedBreakTargets = _currentBreakTargets; | |
| _currentBreakTargets = <LabeledStatement>[]; | |
| } | |
| var savedContinueTargets = _currentContinueTargets; | |
| var result = action(); | |
| if (savedBreakTargets != null) _currentBreakTargets = savedBreakTargets; | |
| _currentContinueTargets = savedContinueTargets; | |
| return result; | |
| } | |
| @override | |
| js_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 d) => | |
| js_ast.VariableInitialization( | |
| _emitVariableDef(d.variable), | |
| _visitInitializer(d.variable.initializer, d.variable.annotations), | |
| ); | |
| if (node.variables.any(containsFunctionExpression)) { | |
| return _rewriteAsWhile(node); | |
| } | |
| var init = node.variables.map(emitForInitializer).toList(); | |
| var initList = init.isEmpty | |
| ? null | |
| : js_ast.VariableDeclarationList('let', init); | |
| var updates = node.updates; | |
| js_ast.Expression? update; | |
| if (updates.isNotEmpty) { | |
| update = js_ast.Expression.binary( | |
| updates.map(_visitExpression).toList(), | |
| ',', | |
| ).toVoidExpression(); | |
| } | |
| var condition = node.condition != null | |
| ? _visitTest(node.condition!) | |
| : null; | |
| var body = _visitScope(_effectiveBodyOf(node, node.body)); | |
| return js_ast.For(initList, condition, update, body); | |
| }); | |
| } | |
| /// Rewrites a `for(;;)` style loop as a while loop to produce the correct | |
| /// semantics when loop variable initializers contain function expressions | |
| /// that close over other loop variables. | |
| /// | |
| /// The Dart semantics expect that every loop iteration gets fresh loop | |
| /// variables that can be closed over. The initialization is only executed | |
| /// for the first iteration. In later iterations, the fresh loop variables are | |
| /// initialized to the values from the end of the previous iteration. | |
| /// | |
| /// These semantics differ from JavaScript when there are closures capturing | |
| /// loop variables so the simple lowering doesn't work as expected. | |
| /// | |
| /// A for loop like: | |
| /// | |
| /// ``` | |
| /// for(var v1 = init1, v2 = init2; condition; updates) { body } | |
| /// ``` | |
| /// | |
| /// Produces a rewrite like: | |
| /// | |
| /// ``` | |
| /// var initFlag = true; | |
| /// var prev_v1, prev_v2; | |
| /// while (true) { | |
| /// var v1, v2; | |
| /// if (initFlag) { | |
| /// initFlag = false; | |
| /// v1 = inti1; | |
| /// v2 = init2; | |
| /// } else { | |
| /// v1 = prev_v1; | |
| /// v2 = prev_v2; | |
| /// updates; | |
| /// } | |
| /// if (!condition) break; | |
| /// body; | |
| /// prev_v1 = v1; | |
| /// prev_v2 = v2; | |
| /// } | |
| /// ``` | |
| js_ast.Statement _rewriteAsWhile(ForStatement node) { | |
| var initFlagTempId = _emitScopedId('t#_init'); | |
| var loopVariableIds = { | |
| for (var stmt in node.variables) | |
| stmt.variable: _emitVariableDef(stmt.variable), | |
| }; | |
| var prevVariableTempIds = { | |
| for (var stmt in node.variables) | |
| stmt.variable: _emitScopedId('t#_prev_${stmt.variable.cosmeticName!}'), | |
| }; | |
| var inits = js_ast.Block([ | |
| // Set init flag to false so the initialization only happens on the first | |
| // iteration of the while loop. | |
| js.statement('# = false;', [initFlagTempId]), | |
| // Initialize fresh loop variables to initial values. | |
| for (var stmt in node.variables) | |
| js.statement('# = #;', [ | |
| loopVariableIds[stmt.variable]!, | |
| _visitInitializer( | |
| stmt.variable.initializer, | |
| stmt.variable.annotations, | |
| ), | |
| ]), | |
| ]); | |
| var prevInits = js_ast.Block([ | |
| // Initialize fresh loop variables with the value from the previous | |
| // iteration. | |
| for (var stmt in node.variables) | |
| js.statement('# = #;', [ | |
| loopVariableIds[stmt.variable], | |
| prevVariableTempIds[stmt.variable], | |
| ]), | |
| // Original update expressions. | |
| for (var update in node.updates) _visitExpression(update).toStatement(), | |
| ]); | |
| return js_ast.Block([ | |
| // Create temporary variables for the initialization flag and previous | |
| // loop variables. | |
| js_ast.VariableDeclarationList('let', [ | |
| js_ast.VariableInitialization( | |
| initFlagTempId, | |
| js_ast.LiteralBool(true), | |
| ), | |
| for (var stmt in node.variables) | |
| js_ast.VariableInitialization( | |
| prevVariableTempIds[stmt.variable]!, | |
| null, | |
| ), | |
| ]).toStatement(), | |
| // The for loop transformed into a while loop. | |
| js_ast.While( | |
| js_ast.LiteralBool(true), | |
| js_ast.Block([ | |
| // Create fresh loop variables every iteration. | |
| if (node.variables.isNotEmpty) | |
| js_ast.VariableDeclarationList('let', [ | |
| for (var stmt in node.variables) | |
| js_ast.VariableInitialization( | |
| loopVariableIds[stmt.variable]!, | |
| null, | |
| ), | |
| ]).toStatement(), | |
| // Initialize loop variables. | |
| js_ast.If(initFlagTempId, inits, prevInits), | |
| // Loop condition guard. | |
| if (node.condition != null) | |
| js.statement('if (!#) break;', [_visitTest(node.condition!)]) | |
| ..sourceInformation = _nodeStart(node.condition!), | |
| // Original loop body. | |
| _visitScope(_effectiveBodyOf(node, node.body)), | |
| // Save previous loop variables | |
| for (var stmt in node.variables) | |
| js.statement('# = #;', [ | |
| prevVariableTempIds[stmt.variable]!, | |
| _emitVariableRef(stmt.variable), | |
| ]) | |
| // Map these locations to the variable declaration so stepping | |
| // in the Dart debugger doesn't jump to the previous line when | |
| // stepping. | |
| ..sourceInformation = _nodeStart(stmt.variable), | |
| ]), | |
| ) | |
| // The while loop gets mapped to the original for loop location. | |
| ..sourceInformation = _nodeStart(node), | |
| ]) | |
| // Clear the source mapping on the outer block so it doesn't automatically | |
| // get mapped to the for loop node in _visitStatement. | |
| ..sourceInformation = continueSourceMap; | |
| } | |
| @override | |
| js_ast.Statement visitForInStatement(ForInStatement node) { | |
| return _translateLoop(node, () { | |
| if (node.isAsync) { | |
| return _emitAwaitFor(node); | |
| } | |
| var iterable = _visitExpression(node.iterable); | |
| var body = _visitScope(_effectiveBodyOf(node, node.body)); | |
| var init = js.call('let #', _emitVariableDef(node.variable)); | |
| if (_annotatedNullCheck(node.variable.annotations)) { | |
| body = js_ast.Block([ | |
| _nullParameterCheck(_emitVariableRef(node.variable)), | |
| body, | |
| ]); | |
| } | |
| if (node.variable.cosmeticName != null && | |
| js_ast.variableIsReferenced(node.variable.cosmeticName!, iterable)) { | |
| var temp = _emitScopedId('iter'); | |
| return js_ast.Block([ | |
| iterable.toVariableDeclaration(temp), | |
| js_ast.ForOf(init, temp, body), | |
| ]); | |
| } | |
| return js_ast.ForOf(init, iterable, body); | |
| }); | |
| } | |
| js_ast.Statement _emitAwaitFor(ForInStatement node) { | |
| // Emits `await for (var value in stream) ...`, which desugars as: | |
| // | |
| // var iter = new StreamIterator(stream); | |
| // try { | |
| // while (await iter.moveNext()) { | |
| // var value = iter.current; | |
| // ... | |
| // } | |
| // } finally { | |
| // await iter.cancel(); | |
| // } | |
| // | |
| // Like the Dart VM, we call cancel() always, as it's safe to call if the | |
| // stream has already been cancelled. | |
| // | |
| // TODO(jmesserly): we may want a helper if these become common. For now the | |
| // full desugaring seems okay. | |
| var streamIterator = _coreTypes.nonNullableRawType( | |
| _asyncStreamIteratorClass, | |
| ); | |
| var streamIteratorRti = _emitType(streamIterator); | |
| var createStreamIter = js_ast.Call( | |
| _emitConstructorName( | |
| streamIterator, | |
| _asyncStreamIteratorClass.procedures.firstWhere( | |
| (p) => p.isFactory && p.name.text == '', | |
| ), | |
| ), | |
| [streamIteratorRti, _visitExpression(node.iterable)], | |
| ); | |
| var iter = _emitScopedId('iter'); | |
| var savedContinueTargets = _currentContinueTargets; | |
| var savedBreakTargets = _currentBreakTargets; | |
| _currentContinueTargets = <LabeledStatement>[]; | |
| _currentBreakTargets = <LabeledStatement>[]; | |
| 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 = <js_ast.SwitchClause>[]; | |
| if (_inLabeledContinueSwitch) { | |
| var labelState = _emitScopedId('labelState'); | |
| // TODO(markzipan): Retrieve the real label name with source offsets | |
| var labelName = 'SL${_switchLabelStates.length}'; | |
| _switchLabelStates[node] = _SwitchLabelState(labelName, labelState); | |
| // Since we wrap the switch in a 'while (true)' loop the continue targets | |
| // within the switch will no longer target the correct loop so we need | |
| // explicit breaks. | |
| final savedCurrentContinueTargets = _currentContinueTargets; | |
| _currentContinueTargets = []; | |
| for (var c in node.cases) { | |
| var subcases = _visitSwitchCase( | |
| c, | |
| lastSwitchCase: c == node.cases.last, | |
| ); | |
| if (subcases.isNotEmpty) cases.addAll(subcases); | |
| } | |
| _currentContinueTargets = savedCurrentContinueTargets; | |
| var switchExpr = _visitExpression(node.expression); | |
| var switchStmt = js_ast.Switch(labelState, cases); | |
| var loopBody = js_ast.Block([switchStmt, js_ast.Break(null)]); | |
| var loopStmt = js_ast.While(js.boolean(true), loopBody); | |
| // Note: Cannot use _labelNames, as the label must be on the loop. | |
| // not the block surrounding the switch statement. | |
| var labeledStmt = js_ast.LabeledStatement(labelName, loopStmt); | |
| var block = js_ast.Block([ | |
| js.statement('let # = #', [labelState, switchExpr]), | |
| labeledStmt, | |
| ]); | |
| _inLabeledContinueSwitch = previous; | |
| return block; | |
| } | |
| for (var c in node.cases) { | |
| var subcases = _visitSwitchCase(c); | |
| if (subcases.isNotEmpty) cases.addAll(subcases); | |
| } | |
| var stmt = js_ast.Switch(_visitExpression(node.expression), cases); | |
| _inLabeledContinueSwitch = previous; | |
| return stmt; | |
| } | |
| /// Helper for visiting a SwitchCase statement. | |
| /// | |
| /// [lastSwitchCase] is only used when the current switch statement contains | |
| /// labeled continues. Dart permits the final case to implicitly break, but | |
| /// switch statements with labeled continues must explicitly break/continue | |
| /// to escape the surrounding infinite loop. | |
| List<js_ast.SwitchClause> _visitSwitchCase( | |
| SwitchCase node, { | |
| bool lastSwitchCase = false, | |
| }) { | |
| var cases = <js_ast.SwitchClause>[]; | |
| 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<Catch> clauses) { | |
| if (clauses.isEmpty) return null; | |
| var caughtError = SyntheticVariable(cosmeticName: '#e'); | |
| 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) ?? | |
| SyntheticVariable(cosmeticName: '#ex'); | |
| var stackTraceParameter = | |
| (clauses.length == 1 ? clauses[0].stackTrace : null) ?? | |
| (clauses.any((c) => c.stackTrace != null) | |
| ? SyntheticVariable(cosmeticName: '#st') | |
| : 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, | |
| Variable exceptionParameter, | |
| Variable? stackTraceParameter, | |
| ) { | |
| var body = <js_ast.Statement>[]; | |
| var vars = HashSet<String>(); | |
| void declareVariable(Variable? variable, Variable? value) { | |
| if (variable == null || value == null) return; | |
| vars.add(variable.cosmeticName!); | |
| if (variable.cosmeticName != value.cosmeticName) { | |
| 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 visitVariable(Variable node) { | |
| // TODO(jmesserly): casts are sometimes required here. | |
| // Kernel does not represent these explicitly. | |
| var v = _emitVariableDef(node); | |
| return js.statement('let # = #;', [ | |
| v, | |
| _visitInitializer(node.initializer, node.annotations), | |
| ]); | |
| } | |
| @override | |
| js_ast.Statement visitFunctionDeclaration(FunctionDeclaration node) { | |
| var func = node.function; | |
| var fn = _emitFunction(func, node.variable.name); | |
| var name = _emitVariableDef(node.variable); | |
| js_ast.Statement declareFn; | |
| declareFn = js_ast.toBoundFunctionStatement(fn, name); | |
| if (_reifyFunctionType(func)) { | |
| declareFn = js_ast.Block([ | |
| declareFn, | |
| _emitFunctionTagged( | |
| _emitVariableRef(node.variable), | |
| func.computeThisFunctionType(Nullability.nonNullable), | |
| ).toStatement(), | |
| ]); | |
| } | |
| return declareFn; | |
| } | |
| @override | |
| js_ast.Expression visitInvalidExpression(InvalidExpression node) => | |
| _emitInvalidNode(node); | |
| @override | |
| js_ast.Expression visitConstantExpression(ConstantExpression node) => | |
| visitConstant(node.constant); | |
| @override | |
| js_ast.Expression visitVariableGet(VariableGet node) { | |
| var v = node.variable; | |
| var id = _emitVariableRef(v); | |
| if (id.name == v.cosmeticName) { | |
| id = id.withSourceInformation( | |
| _variableSpan(node.fileOffset, v.cosmeticName!.length), | |
| ); | |
| } | |
| return id; | |
| } | |
| /// Detects temporary variables so we can avoid displaying | |
| /// them in the debugger if needed. | |
| bool _isTemporaryVariable(Variable 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.cosmeticName == null || | |
| v.cosmeticName!.startsWith('#'); | |
| } | |
| /// Creates a temporary name recognized by the debugger. | |
| /// Assumes `_isTemporaryVariable(v)` is true. | |
| String? _debuggerFriendlyTemporaryVariableName(Variable 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.cosmeticName != null) { | |
| return 't\$${v.cosmeticName}'; | |
| } | |
| return null; | |
| } | |
| js_ast.ScopedId _emitVariableRef(Variable v) { | |
| if (_isTemporaryVariable(v)) { | |
| var name = _debuggerFriendlyTemporaryVariableName(v); | |
| name ??= 't\$${_tempVariables.length}'; | |
| return _tempVariables.putIfAbsent( | |
| v, | |
| () => _emitScopedId(name!, needsCapture: true), | |
| ); | |
| } | |
| var name = v.cosmeticName!; | |
| if (isLateLoweredLocal(v)) { | |
| // Late local variables are be exposed to the debugger for inspection and | |
| // evaluation by treating the backing store local variable as a regular | |
| // non-temporary variable. | |
| // See https://github.com/dart-lang/sdk/issues/55918 | |
| name = extractLocalNameFromLateLoweredLocal(name); | |
| } | |
| return js_ast.ScopedId.from( | |
| _variableTempIds[v] ??= _emitScopedId(name, needsCapture: true), | |
| ); | |
| } | |
| /// Emits the declaration of a variable. | |
| /// | |
| /// This is similar to [_emitVariableRef] but it also attaches source | |
| /// location information, so hover will work as expected. | |
| js_ast.Identifier _emitVariableDef(Variable 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 (member.isCoreObjectGetter && _shouldCallObjectMemberHelper(receiver)) { | |
| // The names of the static helper methods in the runtime must match the | |
| // names of the Object instance getters. | |
| return _runtimeCall('#(#)', [memberName, jsReceiver]); | |
| } | |
| // Otherwise generate this as a normal typed property get. | |
| var jsMemberName = _emitMemberName( | |
| memberName, | |
| member: node.interfaceTarget, | |
| ); | |
| var instanceGet = js_ast.PropertyAccess(jsReceiver, jsMemberName); | |
| return _isNullCheckableJsInterop(node.interfaceTarget) | |
| ? _wrapWithJsInteropNullCheck(instanceGet) | |
| : instanceGet; | |
| } | |
| @override | |
| js_ast.Expression visitRecordIndexGet(RecordIndexGet node) => | |
| _emitRecordElementGet(node.receiver, '\$${node.index + 1}'); | |
| @override | |
| js_ast.Expression visitRecordNameGet(RecordNameGet node) => | |
| _emitRecordElementGet(node.receiver, node.name); | |
| js_ast.Expression _emitRecordElementGet( | |
| Expression receiver, | |
| String elementName, | |
| ) => js_ast.PropertyAccess( | |
| _visitExpression(receiver), | |
| _emitMemberName(elementName), | |
| ); | |
| @override | |
| js_ast.Expression visitInstanceTearOff(InstanceTearOff node) { | |
| var member = node.interfaceTarget; | |
| var receiver = node.receiver; | |
| var jsReceiver = _visitExpression(receiver); | |
| if (_isNonStaticJsInteropCallMember(member)) { | |
| // Historically DDC has treated this as a "callable class" and the tearoff | |
| // of `.call` as a no-op. | |
| // | |
| // This is here to preserve the existing behavior for the non-static | |
| // JavaScript interop (including some failing cases) but could potentially | |
| // be cleaned up as a breaking change. | |
| return jsReceiver; | |
| } | |
| var memberName = node.name.text; | |
| if (member.isToStringOrNoSuchMethod && | |
| _shouldCallObjectMemberHelper(receiver)) { | |
| // The names of the static helper methods in the runtime must start with | |
| // the names of the Object instance methods. | |
| var tearOffName = '${memberName}Tearoff'; | |
| return _runtimeCall('#(#)', [tearOffName, jsReceiver]); | |
| } | |
| var jsMemberName = _emitMemberName(memberName, member: member); | |
| if (_reifyTearoff(member)) { | |
| return _runtimeCall('tearoff(#, null, #)', [jsReceiver, jsMemberName]); | |
| } | |
| var jsPropertyAccess = js_ast.PropertyAccess(jsReceiver, jsMemberName); | |
| return isJsMember(member) | |
| ? _runtimeCall('tearoffInterop(#, #)', [ | |
| jsPropertyAccess, | |
| js.boolean(_isNullCheckableJsInterop(member)), | |
| ]) | |
| : jsPropertyAccess; | |
| } | |
| /// Returns `true` when [member] is a `.call` member (field, getter or method) | |
| /// of a non-static JavaScript interop class. | |
| bool _isNonStaticJsInteropCallMember(Member member) => | |
| member.name.text == 'call' && isNonStaticJsInterop(member); | |
| @override | |
| js_ast.Expression visitDynamicSet(DynamicSet node) { | |
| return _runtimeCall('dput$_replSuffix(#, #, #)', [ | |
| _visitExpression(node.receiver), | |
| _emitMemberName(node.name.text), | |
| _visitExpression(node.value), | |
| ]); | |
| } | |
| @override | |
| js_ast.Expression visitInstanceSet(InstanceSet node) { | |
| var target = node.interfaceTarget; | |
| var value = isJsMember(target) ? _assertInterop(node.value) : node.value; | |
| return js.call('#.# = #', [ | |
| _visitExpression(node.receiver), | |
| _emitMemberName(node.name.text, member: target), | |
| _visitExpression(value), | |
| ]); | |
| } | |
| /// True when the result of evaluating [e] is not known to have the Object | |
| /// members installed so a helper method should be called instead of a direct | |
| /// instance invocation. | |
| /// | |
| /// This is a best effort approach determined by the static type information | |
| /// and may return `true` when the evaluation result does in fact have the | |
| /// members at runtime. | |
| bool _shouldCallObjectMemberHelper(Expression e) { | |
| if (_isNullable(e)) return true; | |
| var type = e.getStaticType(_staticTypeContext).extensionTypeErasure; | |
| if (type is RecordType || type is FunctionType) return false; | |
| if (type is InterfaceType) { | |
| // TODO(nshahan): This could be expanded to any classes where we know all | |
| // implementations at compile time and none of them are JS interop. | |
| var cls = type.classNode; | |
| // NOTE: This is not guaranteed to always be true. Currently in the SDK | |
| // none of the final classes or their subtypes use JavaScript interop. | |
| // If that was to ever change, this check will need to be updated. | |
| // For now, this is a shortcut since all subclasses of a class are not | |
| // immediately accessible. | |
| if (cls.isFinal && cls.enclosingLibrary.importUri.isScheme('dart')) { | |
| return false; | |
| } | |
| } | |
| // Constants have a static type known at compile time that will not be a | |
| // subtype at runtime. | |
| return !_triviallyConstNoInterop(e); | |
| } | |
| /// True when [e] is known to evaluate to a constant that has an interface | |
| /// type that is not a JavaScript interop type. | |
| /// | |
| /// This is a simple approach and not an exhaustive search. | |
| bool _triviallyConstNoInterop(Expression? e) { | |
| if (e is ConstantExpression) { | |
| var type = e.constant.getType(_staticTypeContext).extensionTypeErasure; | |
| if (type is InterfaceType) return !usesJSInterop(type.classNode); | |
| } else if (e is StaticGet && e.target.isConst) { | |
| var target = e.target; | |
| if (target is Field) { | |
| return _triviallyConstNoInterop(target.initializer); | |
| } | |
| } else if (e is VariableGet && e.variable.isConst) { | |
| return _triviallyConstNoInterop(e.variable.initializer); | |
| } | |
| return false; | |
| } | |
| /// Returns [expression] wrapped in an optional null check. | |
| /// | |
| /// The null check is enabled by setting a flag during the application | |
| /// bootstrap via `jsInteropNonNullAsserts(true)` in the SDK runtime library. | |
| js_ast.Expression _wrapWithJsInteropNullCheck(js_ast.Expression expression) => | |
| _runtimeCall('jsInteropNullCheck(#)', [expression]); | |
| /// Returns `true` when [member] is a JavaScript interop API that should be | |
| /// checked to be not null when the runtime flag `--interop-null-assertions` | |
| /// is enabled. | |
| /// | |
| /// These APIs are defined using the non-static package:js interop library and | |
| /// are typed to be non-nullable. | |
| bool _isNullCheckableJsInterop(Member member) { | |
| var type = member is Procedure | |
| ? member.function.returnType | |
| : member.getterType; | |
| return type.nullability == Nullability.nonNullable && | |
| isNonStaticJsInterop(member); | |
| } | |
| /// Return whether [member] returns a native object whose type needs to be | |
| /// null-checked in sound null-safety. | |
| /// | |
| /// This is true for non-nullable native return types. | |
| bool _isNullCheckableNative(Member member) { | |
| var c = member.enclosingClass; | |
| return member.isExternal && | |
| c != null && | |
| _extensionTypes.isNativeClass(c) && | |
| member is Procedure && | |
| member.function.returnType.isPotentiallyNonNullable && | |
| _isWebLibrary(member.enclosingLibrary.importUri); | |
| } | |
| // TODO(jmesserly): can we encapsulate REPL name lookups and remove this? | |
| // _emitMemberName would be a nice place to handle it, but we don't have | |
| // access to the target expression there (needed for `dart.replNameLookup`). | |
| String get _replSuffix => _options.replCompile ? 'Repl' : ''; | |
| @override | |
| js_ast.Expression visitAbstractSuperPropertyGet( | |
| AbstractSuperPropertyGet node, | |
| ) { | |
| return _emitSuperPropertyGet(node.interfaceTarget); | |
| } | |
| @override | |
| js_ast.Expression visitSuperPropertyGet(SuperPropertyGet node) { | |
| return _emitSuperPropertyGet(node.interfaceTarget); | |
| } | |
| /// Emits a reference to a distinct mixin application, represented by | |
| /// a [mixedInClass] being mixed into [baseClass]. | |
| /// | |
| /// Anonymous mixins should pass themselves as [baseClass] since they are | |
| /// already uniquely generated per distinct mixin application | |
| js_ast.Identifier _emitMixinId(Class mixedInClass, Class baseClass) { | |
| var mixinName = mixedInClass.name; | |
| if (!mixedInClass.isAnonymousMixin) { | |
| mixinName += '#${baseClass.name}'; | |
| } | |
| return _mixinCache.putIfAbsent(( | |
| mixedInClass, | |
| baseClass, | |
| ), () => _emitScopedId(mixinName)); | |
| } | |
| js_ast.Expression _emitSuperPropertyGet(Member target) { | |
| if (_reifyTearoff(target)) { | |
| if (_superAllowed) { | |
| var jsName = _declareMemberName(target); | |
| var enclosingClass = target.enclosingClass!; | |
| js_ast.Expression? supertypeReference = | |
| _mixinSuperclassCache[_currentClass!]; | |
| if (supertypeReference == null) { | |
| if (enclosingClass.isAnonymousMixin) { | |
| var mixinId = _emitMixinId(enclosingClass, enclosingClass); | |
| var enclosingLibrary = _emitLibraryName(getLibrary(enclosingClass)); | |
| supertypeReference = js_ast.PropertyAccess( | |
| enclosingLibrary, | |
| js.string(mixinId.name), | |
| ); | |
| } else { | |
| supertypeReference = _emitTopLevelNameNoExternalInterop( | |
| enclosingClass, | |
| ); | |
| } | |
| } | |
| return _runtimeCall('superTearoff(this, #, #)', [ | |
| supertypeReference, | |
| jsName, | |
| ]); | |
| } else { | |
| return _emitSuperTearoffFromDisallowedContext(target); | |
| } | |
| } | |
| return _emitSuperTarget(target); | |
| } | |
| @override | |
| js_ast.Expression visitAbstractSuperPropertySet( | |
| AbstractSuperPropertySet node, | |
| ) { | |
| return _emitSuperPropertySet(node.interfaceTarget, node.value); | |
| } | |
| @override | |
| js_ast.Expression visitSuperPropertySet(SuperPropertySet node) { | |
| return _emitSuperPropertySet(node.interfaceTarget, node.value); | |
| } | |
| js_ast.Expression _emitSuperPropertySet(Member target, Expression value) { | |
| var jsTarget = _emitSuperTarget(target, setter: true); | |
| return _visitExpression(value).toAssignExpression(jsTarget); | |
| } | |
| @override | |
| js_ast.Expression visitStaticGet(StaticGet node) { | |
| final target = node.target; | |
| if (_isDartJsHelper(target.enclosingLibrary)) { | |
| final name = target.name.text; | |
| if (name == 'staticInteropGlobalContext') { | |
| return _runtimeCall('global'); | |
| } | |
| } | |
| var staticGet = _emitStaticGet(target); | |
| return _isNullCheckableJsInterop(target) | |
| ? _wrapWithJsInteropNullCheck(staticGet) | |
| : staticGet; | |
| } | |
| @override | |
| js_ast.Expression visitStaticTearOff(StaticTearOff node) => | |
| _emitStaticGet(node.target); | |
| js_ast.Expression _emitStaticGet(Member target) { | |
| var propertyAccessor = _emitStaticTarget(target); | |
| var context = propertyAccessor.receiver; | |
| var property = propertyAccessor.selector; | |
| var result = js.call('#.#', [context, property]); | |
| if (_reifyTearoff(target)) { | |
| var enclosingMemberTargetName = js.string( | |
| fullyResolvedTargetLabel(target), | |
| ); | |
| return _runtimeCall('staticTearoff(#, #, #)', [ | |
| context, | |
| enclosingMemberTargetName, | |
| property, | |
| ]); | |
| } | |
| return result; | |
| } | |
| @override | |
| js_ast.Expression visitStaticSet(StaticSet node) { | |
| var target = node.target; | |
| var result = _emitStaticTarget(target); | |
| var value = isJsMember(target) ? _assertInterop(node.value) : node.value; | |
| return _visitExpression(value).toAssignExpression(result); | |
| } | |
| @override | |
| js_ast.Expression visitDynamicInvocation(DynamicInvocation node) { | |
| return _emitDynamicInvocation( | |
| node.receiver, | |
| node.name.text, | |
| node.arguments, | |
| ); | |
| } | |
| @override | |
| js_ast.Expression visitFunctionInvocation(FunctionInvocation node) { | |
| var name = node.name.text; | |
| assert(name == 'call'); | |
| if (node.functionType == null) { | |
| // A `null` here implies the receiver is typed as `Function`. There isn't | |
| // any more type information available at compile time to know this | |
| // invocation is sound so a dynamic call will handle the checks at | |
| // runtime. | |
| return _emitDynamicInvocation(node.receiver, name, node.arguments); | |
| } | |
| return js_ast.Call( | |
| _visitExpression(node.receiver), | |
| _emitArgumentList(node.arguments), | |
| ); | |
| } | |
| @override | |
| js_ast.Expression visitInstanceInvocation(InstanceInvocation node) { | |
| var target = node.interfaceTarget; | |
| if (target.isPrimitiveOperator) { | |
| return _emitPrimitiveOperatorInvocation(node); | |
| } | |
| var receiver = node.receiver; | |
| var jsReceiver = _visitExpression(receiver); | |
| var jsArguments = _emitArgumentList(node.arguments, target: target); | |
| if (node.isNativeListInvariantAddInvocation(_coreTypes.listClass)) { | |
| return js.call('#.push(#)', [jsReceiver, jsArguments]); | |
| } | |
| var name = node.name.text; | |
| if (name == 'call') { | |
| var directCallInvocation = _emitDirectInstanceCallInvocation(node); | |
| if (directCallInvocation != null) { | |
| return directCallInvocation; | |
| } | |
| } | |
| if (target.isToStringOrNoSuchMethodWithDefaultSignature && | |
| _shouldCallObjectMemberHelper(receiver)) { | |
| // Handle Object methods when the receiver could potentially be `null` or | |
| // JavaScript interop values with static helper methods. | |
| // The names of the static helper methods in the runtime must match the | |
| // names of the Object instance members. | |
| return _runtimeCall('#(#, #)', [name, jsReceiver, jsArguments]); | |
| } | |
| // Otherwise generate this as a normal typed method call. | |
| var jsName = _emitMemberName(name, member: target); | |
| var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]); | |
| return _isNullCheckableJsInterop(node.interfaceTarget) | |
| ? _wrapWithJsInteropNullCheck(invocation) | |
| : invocation; | |
| } | |
| /// Returns a direct invocation to support a `call()` method invocation when | |
| /// the receiver is considered directly callable, otherwise returns | |
| /// `null`. | |
| /// | |
| /// For example if `fn` is statically typed as a function type, then | |
| /// `fn.call()` would be considered directly callable and compiled as `fn()`. | |
| js_ast.Expression? _emitDirectInstanceCallInvocation( | |
| InstanceInvocation node, | |
| ) { | |
| // Erasing the extension types here to support existing callable behavior | |
| // on the old style JS interop types that are callable. This should be | |
| // safe as it is a compile time error to try to dynamically invoke a call | |
| // method that is inherited from an extension type. | |
| var receiverType = node.receiver | |
| .getStaticType(_staticTypeContext) | |
| .extensionTypeErasure; | |
| if (!_isDirectCallable(receiverType)) return null; | |
| // Handle call methods on function types as function calls. | |
| var invocation = js_ast.Call( | |
| _visitExpression(node.receiver), | |
| _emitArgumentList(node.arguments, target: node.interfaceTarget), | |
| ); | |
| return _isNullCheckableJsInterop(node.interfaceTarget) | |
| ? _wrapWithJsInteropNullCheck(invocation) | |
| : invocation; | |
| } | |
| /// Returns an invocation of a primitive operator. | |
| /// | |
| /// See [ProcedureHelpers.isPrimitiveOperator]. | |
| js_ast.Expression _emitPrimitiveOperatorInvocation(InstanceInvocation node) { | |
| var receiver = node.receiver; | |
| var target = node.interfaceTarget; | |
| var arguments = node.arguments; | |
| assert(arguments.types.isEmpty && arguments.named.isEmpty); | |
| // JavaScript interop does not support overloading of these operators. | |
| return switch (arguments.positional.length) { | |
| 0 => _emitUnaryOperator(receiver, target, node), | |
| 1 => _emitBinaryOperator(receiver, target, arguments.positional[0], node), | |
| // Should always be a compile time error but here for exhaustiveness. | |
| _ => throw UnsupportedError( | |
| 'Invalid number of positional arguments.\n' | |
| 'Found: ${arguments.positional.length}\n' | |
| 'Operator: ${node.name.text} ' | |
| '${node.location}', | |
| ), | |
| }; | |
| } | |
| @override | |
| js_ast.Expression visitInstanceGetterInvocation( | |
| InstanceGetterInvocation node, | |
| ) { | |
| if (node.functionType == null) { | |
| // A `null` here implies the receiver must be typed as `dynamic` or | |
| // `Function`. There isn't any more type information available at compile | |
| // time to know this invocation is sound so a dynamic call will handle the | |
| // checks at runtime. | |
| return _emitDynamicInvocation( | |
| node.receiver, | |
| node.name.text, | |
| node.arguments, | |
| ); | |
| } | |
| var getterInvocation = _emitInstanceGetterInvocation(node); | |
| return _isNullCheckableJsInterop(node.interfaceTarget) | |
| ? _wrapWithJsInteropNullCheck(getterInvocation) | |
| : getterInvocation; | |
| } | |
| js_ast.Expression _emitInstanceGetterInvocation( | |
| InstanceGetterInvocation node, | |
| ) { | |
| var receiver = _visitExpression(node.receiver); | |
| var arguments = _emitArgumentList( | |
| node.arguments, | |
| target: node.interfaceTarget, | |
| ); | |
| if (node.name.text == 'call') { | |
| // Erasing the extension types here to support existing callable behavior | |
| // on the old style JS interop types that are callable. This should be | |
| // safe as it is a compile time error to try to dynamically invoke a call | |
| // method that is inherited from an extension type. | |
| var receiverType = node.receiver | |
| .getStaticType(_staticTypeContext) | |
| .extensionTypeErasure; | |
| if (_isDirectCallable(receiverType)) { | |
| // Call methods on function types should be handled as function calls. | |
| return js_ast.Call(receiver, arguments); | |
| } | |
| } | |
| var memberName = _emitMemberName( | |
| node.name.text, | |
| member: node.interfaceTarget, | |
| ); | |
| // We must erase the extension type to potentially find the `call` method. | |
| // If the extension type has a runtime representation with a `call`: | |
| // | |
| // ``` | |
| // extension type Ext(C c) implements C {...} | |
| // class C { | |
| // call() {...} | |
| // } | |
| // ``` | |
| // | |
| // We can always erase eagerly because: | |
| // - Extension types that do not implement an interface that exposes a | |
| // `call` method will result in a static error at the call site. | |
| // - Calls to extension types that implement their own call method are | |
| // lowered by the CFE to top level static method calls. | |
| var erasedGetterType = node.interfaceTarget.getterType.extensionTypeErasure; | |
| if (erasedGetterType is TypeParameterType) { | |
| // Instantiate with the static type so there is a concrete interface to | |
| // inspect for the callable target check. | |
| var receiverClass = node.interfaceTarget.enclosingClass!; | |
| var instantiatedClassType = node.receiver.getStaticTypeAsInstanceOf( | |
| receiverClass, | |
| _staticTypeContext, | |
| ); | |
| var instantiatedGetterType = Substitution.fromInterfaceType( | |
| instantiatedClassType, | |
| ).substituteType(erasedGetterType); | |
| erasedGetterType = instantiatedGetterType.extensionTypeErasure; | |
| } | |
| if (erasedGetterType is InterfaceType) { | |
| var callName = _implicitCallTarget(erasedGetterType); | |
| if (callName != null) { | |
| return js.call('#.#.#(#)', [receiver, memberName, callName, arguments]); | |
| } | |
| } | |
| return js.call('#.#(#)', [receiver, memberName, arguments]); | |
| } | |
| @override | |
| js_ast.Expression visitLocalFunctionInvocation(LocalFunctionInvocation node) { | |
| assert(node.name.text == 'call'); | |
| final localName = VariableGet(node.variable)..fileOffset = node.fileOffset; | |
| return js_ast.Call( | |
| _visitExpression(localName), | |
| _emitArgumentList(node.arguments), | |
| ); | |
| } | |
| @override | |
| js_ast.Expression visitEqualsCall(EqualsCall node) { | |
| return _emitEqualityOperator( | |
| node.left, | |
| node.interfaceTarget, | |
| node.right, | |
| negated: false, | |
| ); | |
| } | |
| @override | |
| js_ast.Expression visitEqualsNull(EqualsNull node) { | |
| return _emitCoreIdenticalCall([ | |
| node.expression, | |
| NullLiteral(), | |
| ], negated: false); | |
| } | |
| /// Returns an invocation of the runtime helpers `dcall`, `dgcall`, `dsend`, | |
| /// or `dgsend` to perform dynamic checks before invoking [memberName] on | |
| /// [receiver] and passing [arguments]. | |
| js_ast.Expression _emitDynamicInvocation( | |
| Expression receiver, | |
| String memberName, | |
| Arguments arguments, | |
| ) { | |
| var jsArgs = [_visitExpression(receiver)]; | |
| String jsCode; | |
| var (:typeArguments, :positionalArguments, :namedArguments) = | |
| _emitArgumentGroups(arguments, isJSInterop: false); | |
| if (memberName == 'call') { | |
| if (typeArguments != null) { | |
| jsCode = 'dgcall(#, #'; | |
| jsArgs.add(js_ast.ArrayInitializer(typeArguments)); | |
| } else { | |
| jsCode = 'dcall(#'; | |
| } | |
| } else { | |
| if (typeArguments != null) { | |
| jsCode = 'dgsend$_replSuffix(#, #, #'; | |
| jsArgs.add(js_ast.ArrayInitializer(typeArguments)); | |
| } else { | |
| jsCode = 'dsend$_replSuffix(#, #'; | |
| } | |
| jsArgs.add(_emitMemberName(memberName)); | |
| } | |
| if (positionalArguments != null) { | |
| jsCode += ', #'; | |
| jsArgs.add(js_ast.ArrayInitializer(positionalArguments)); | |
| } else { | |
| jsCode += ', []'; | |
| } | |
| if (namedArguments != null) { | |
| jsCode += ', #'; | |
| jsArgs.add(js_ast.ObjectInitializer(namedArguments)); | |
| } | |
| jsCode += ')'; | |
| return _runtimeCall(jsCode, jsArgs); | |
| } | |
| bool _isDirectCallable(DartType t) => | |
| t is FunctionType || (t is InterfaceType && usesJSInterop(t.classNode)); | |
| js_ast.Expression? _implicitCallTarget(InterfaceType from) { | |
| var c = from.classNode; | |
| var member = _hierarchy.getInterfaceMember(c, Name('call')); | |
| if (member is Procedure && !member.isAccessor && !usesJSInterop(c)) { | |
| return _emitMemberName('call', member: member); | |
| } | |
| return null; | |
| } | |
| js_ast.Expression _emitUnaryOperator( | |
| Expression expr, | |
| Member? target, | |
| InvocationExpression node, | |
| ) { | |
| var op = node.name.text; | |
| if (target != null) { | |
| var dispatchType = _coreTypes.nonNullableRawType(target.enclosingClass!); | |
| if (_typeRep.unaryOperationIsPrimitive(dispatchType)) { | |
| if (op == '~') { | |
| if (_typeRep.isNumber(dispatchType)) { | |
| return _coerceBitOperationResultToUnsigned( | |
| node, | |
| js.call('~#', _notNull(expr)), | |
| ); | |
| } | |
| return _emitOperatorCall(expr, target, op, []); | |
| } | |
| if (op == 'unary-') op = '-'; | |
| return js.call('$op#', _notNull(expr)); | |
| } | |
| } | |
| return _emitOperatorCall(expr, target, op, []); | |
| } | |
| /// Bit operations are coerced to values on [0, 2^32). The coercion changes | |
| /// the interpretation of the 32-bit value from signed to unsigned. Most | |
| /// JavaScript operations interpret their operands as signed and generate | |
| /// signed results. | |
| js_ast.Expression _coerceBitOperationResultToUnsigned( | |
| Expression node, | |
| js_ast.Expression uncoerced, | |
| ) { | |
| // Don't coerce if the parent will coerce. | |
| var parent = node.parent; | |
| if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { | |
| return uncoerced; | |
| } | |
| // Don't do a no-op coerce if the most significant bit is zero. | |
| if (_is31BitUnsigned(node)) return uncoerced; | |
| // If the consumer of the expression is '==' or '!=' with a constant that | |
| // fits in 31 bits, adding a coercion does not change the result of the | |
| // comparison, e.g. `a & ~b == 0`. | |
| Expression? left; | |
| late Expression right; | |
| late String op; | |
| if (parent is InvocationExpression && | |
| parent.arguments.positional.length == 1) { | |
| op = parent.name.text; | |
| left = getInvocationReceiver(parent); | |
| right = parent.arguments.positional[0]; | |
| } else if (parent is EqualsCall) { | |
| left = parent.left; | |
| right = parent.right; | |
| op = '=='; | |
| } else if (parent is EqualsNull) { | |
| left = parent.expression; | |
| right = NullLiteral(); | |
| op = '=='; | |
| } | |
| if (left != null) { | |
| if (op == '==') { | |
| const MAX = 0x7fffffff; | |
| if (_asIntInRange(right, 0, MAX) != null) return uncoerced; | |
| if (_asIntInRange(left, 0, MAX) != null) return uncoerced; | |
| } else if (op == '>>') { | |
| if (_isDefinitelyNonNegative(left) && | |
| _asIntInRange(right, 0, 31) != null) { | |
| // Parent will generate `# >>> n`. | |
| return uncoerced; | |
| } | |
| } | |
| } | |
| return js.call('# >>> 0', uncoerced); | |
| } | |
| bool _nodeIsBitwiseOperation(InvocationExpression node) { | |
| switch (node.name.text) { | |
| case '&': | |
| case '|': | |
| case '^': | |
| case '~': | |
| return true; | |
| } | |
| return false; | |
| } | |
| int? _asIntInRange(Expression expr, int low, int high) { | |
| if (expr is IntLiteral) { | |
| if (expr.value >= low && expr.value <= high) return expr.value; | |
| return null; | |
| } | |
| if (_constants.isConstant(expr)) { | |
| var c = _constants.evaluate(expr); | |
| if (c is IntConstant && c.value >= low && c.value <= high) return c.value; | |
| } | |
| return null; | |
| } | |
| bool _isDefinitelyNonNegative(Expression expr) { | |
| if (expr is IntLiteral) return expr.value >= 0; | |
| // TODO(sra): Lengths of known list types etc. | |
| return expr is InvocationExpression && _nodeIsBitwiseOperation(expr); | |
| } | |
| /// Does the parent of [node] mask the result to [width] bits or fewer? | |
| bool _parentMasksToWidth(Expression node, int width) { | |
| var parent = node.parent; | |
| if (parent == null) return false; | |
| if (parent is InvocationExpression && _nodeIsBitwiseOperation(parent)) { | |
| if (parent.name.text == '&' && parent.arguments.positional.length == 1) { | |
| var left = getInvocationReceiver(parent); | |
| var right = parent.arguments.positional[0]; | |
| final max = (1 << width) - 1; | |
| if (left != null) { | |
| if (_asIntInRange(right, 0, max) != null) return true; | |
| if (_asIntInRange(left, 0, max) != null) return true; | |
| } | |
| } | |
| return _parentMasksToWidth(parent, width); | |
| } | |
| return false; | |
| } | |
| /// Determines if the result of evaluating [expr] will be an non-negative | |
| /// value that fits in 31 bits. | |
| bool _is31BitUnsigned(Expression expr) { | |
| const MAX = 32; // Includes larger and negative values. | |
| /// Determines how many bits are required to hold result of evaluation | |
| /// [expr]. [depth] is used to bound exploration of huge expressions. | |
| int bitWidth(Expression expr, int depth) { | |
| if (expr is IntLiteral) { | |
| return expr.value >= 0 ? expr.value.bitLength : MAX; | |
| } | |
| if (++depth > 5) return MAX; | |
| if (expr is InvocationExpression && | |
| expr.arguments.positional.length == 1) { | |
| var left = getInvocationReceiver(expr); | |
| var right = expr.arguments.positional[0]; | |
| if (left != null) { | |
| switch (expr.name.text) { | |
| case '&': | |
| return min(bitWidth(left, depth), bitWidth(right, depth)); | |
| case '|': | |
| case '^': | |
| return max(bitWidth(left, depth), bitWidth(right, depth)); | |
| case '>>': | |
| var shiftValue = _asIntInRange(right, 0, 31); | |
| if (shiftValue != null) { | |
| var leftWidth = bitWidth(left, depth); | |
| return leftWidth == MAX ? MAX : max(0, leftWidth - shiftValue); | |
| } | |
| return MAX; | |
| case '<<': | |
| var leftWidth = bitWidth(left, depth); | |
| var shiftValue = _asIntInRange(right, 0, 31); | |
| if (shiftValue != null) { | |
| return min(MAX, leftWidth + shiftValue); | |
| } | |
| var rightWidth = bitWidth(right, depth); | |
| if (rightWidth <= 5) { | |
| // e.g. `1 << (x & 7)` has a rightWidth of 3, so shifts by up to | |
| // (1 << 3) - 1 == 7 bits. | |
| return min(MAX, leftWidth + ((1 << rightWidth) - 1)); | |
| } | |
| return MAX; | |
| default: | |
| return MAX; | |
| } | |
| } | |
| } | |
| var value = _asIntInRange(expr, 0, 0x7fffffff); | |
| if (value != null) return value.bitLength; | |
| return MAX; | |
| } | |
| return bitWidth(expr, 0) < 32; | |
| } | |
| js_ast.Expression _emitBinaryOperator( | |
| Expression left, | |
| Member? target, | |
| Expression right, | |
| InvocationExpression node, | |
| ) { | |
| var op = node.name.text; | |
| if (op == '==') return _emitEqualityOperator(left, target, right); | |
| // TODO(jmesserly): using the target type here to work around: | |
| // https://github.com/dart-lang/sdk/issues/33293 | |
| if (target != null) { | |
| var targetClass = target.enclosingClass!; | |
| var leftType = _coreTypes.nonNullableRawType(targetClass); | |
| var rightType = right.getStaticType(_staticTypeContext); | |
| if (_typeRep.binaryOperationIsPrimitive(leftType, rightType) || | |
| targetClass == _coreTypes.stringClass && op == '+') { | |
| // Inline operations on primitive types where possible. | |
| // TODO(jmesserly): inline these from dart:core instead of hardcoding | |
| // the implementation details here. | |
| /// Emits an inlined binary operation using the JS [code], adding null | |
| /// checks if needed to ensure we throw the appropriate error. | |
| js_ast.Expression binary(String code) { | |
| return js.call(code, [_notNull(left), _notNull(right)]) | |
| ..sourceInformation = continueSourceMap; | |
| } | |
| js_ast.Expression bitwise(String code) { | |
| return _coerceBitOperationResultToUnsigned(node, binary(code)); | |
| } | |
| /// Similar to [binary] but applies a boolean conversion to the right | |
| /// operand, to match the boolean bitwise operators in dart:core. | |
| /// | |
| /// Short circuiting operators should not be used in [code], because the | |
| /// null checks for both operands must happen unconditionally. | |
| js_ast.Expression bitwiseBool(String code) { | |
| return js.call(code, [_notNull(left), _visitTest(right)]); | |
| } | |
| switch (op) { | |
| case '~/': | |
| // `a ~/ b` is equivalent to `(a / b).truncate()` | |
| return js.call('(# / #).#()', [ | |
| _notNull(left), | |
| _notNull(right), | |
| _emitMemberName('truncate', memberClass: targetClass), | |
| ]); | |
| case '%': | |
| // TODO(sra): We can generate `a % b + 0` if both are non-negative | |
| // (the `+ 0` is to coerce -0.0 to 0). | |
| return _emitOperatorCall(left, target, op, [right]); | |
| case '&': | |
| return _typeRep.isBoolean(leftType) | |
| ? bitwiseBool('!!(# & #)') | |
| : bitwise('# & #'); | |
| case '|': | |
| return _typeRep.isBoolean(leftType) | |
| ? bitwiseBool('!!(# | #)') | |
| : bitwise('# | #'); | |
| case '^': | |
| return _typeRep.isBoolean(leftType) | |
| ? bitwiseBool('# !== #') | |
| : bitwise('# ^ #'); | |
| case '>>': | |
| var shiftCount = _asIntInRange(right, 0, 31); | |
| if (_is31BitUnsigned(left) && shiftCount != null) { | |
| return binary('# >> #'); | |
| } | |
| if (_isDefinitelyNonNegative(left) && shiftCount != null) { | |
| return binary('# >>> #'); | |
| } | |
| // If the context selects out only bits that can't be affected by | |
| // the sign position we can use any JavaScript shift, | |
| // `(x >> 6) & 3`. | |
| if (shiftCount != null && | |
| _parentMasksToWidth(node, 31 - shiftCount)) { | |
| return binary('# >> #'); | |
| } | |
| return _emitOperatorCall(left, target, op, [right]); | |
| case '<<': | |
| if (_is31BitUnsigned(node)) { | |
| // Result is 31 bit unsigned which implies the shift count was | |
| // small enough not to pollute the sign bit. | |
| return binary('# << #'); | |
| } | |
| if (_asIntInRange(right, 0, 31) != null) { | |
| return _coerceBitOperationResultToUnsigned( | |
| node, | |
| binary('# << #'), | |
| ); | |
| } | |
| return _emitOperatorCall(left, target, op, [right]); | |
| case '>>>': | |
| if (_asIntInRange(right, 0, 31) != null) { | |
| return binary('# >>> #'); | |
| } | |
| return _emitOperatorCall(left, target, op, [right]); | |
| default: | |
| // TODO(vsm): When do Dart ops not map to JS? | |
| return binary('# $op #'); | |
| } | |
| } | |
| } | |
| return _emitOperatorCall(left, target, op, [right]); | |
| } | |
| js_ast.Expression _emitEqualityOperator( | |
| Expression left, | |
| Member? target, | |
| Expression right, { | |
| bool negated = false, | |
| }) { | |
| var targetClass = target?.enclosingClass; | |
| var leftType = left.getStaticType(_staticTypeContext).extensionTypeErasure; | |
| // Conceptually `x == y` in Dart is defined as: | |
| // | |
| // If either x or y is null, then they are equal iff they are both null. | |
| // Otherwise, equality is the result of calling `x.==(y)`. | |
| // | |
| // In practice, `x.==(y)` is equivalent to `identical(x, y)` in many cases: | |
| // - when either side is known to be `null` (literal or Null type) | |
| // - left side is an enum | |
| // - left side is a primitive type | |
| // | |
| // We also compile `operator ==` methods to ensure they check the right side | |
| // for null`. This allows us to skip the check at call sites. | |
| // | |
| // TODO(leafp,jmesserly): we could use class hierarchy analysis to check | |
| // if `operator ==` was overridden, similar to how we devirtualize private | |
| // fields. | |
| // | |
| // If we know that the left type uses identity for equality, we can | |
| // sometimes emit better code, either `===` or `==`. | |
| var isEnum = leftType is InterfaceType && leftType.classNode.isEnum; | |
| var usesIdentity = | |
| _typeRep.isPrimitive(leftType) || | |
| isEnum || | |
| _isNull(left) || | |
| _isNull(right); | |
| if (usesIdentity) { | |
| return _emitCoreIdenticalCall([left, right], negated: negated); | |
| } | |
| if (_shouldCallObjectMemberHelper(left)) { | |
| // The LHS isn't guaranteed to have an equals method we need to use a | |
| // runtime helper. | |
| return js.call(negated ? '!#' : '#', [ | |
| _runtimeCall('equals(#, #)', [ | |
| _visitExpression(left), | |
| _visitExpression(right), | |
| ]), | |
| ]); | |
| } | |
| // Otherwise it is safe to call the equals method on the LHS directly. | |
| return js.call(negated ? '!#[#](#)' : '#[#](#)', [ | |
| _visitExpression(left), | |
| _emitMemberName('==', memberClass: targetClass), | |
| _visitExpression(right), | |
| ]); | |
| } | |
| /// Emits a generic send, like an operator method. | |
| /// | |
| /// **Please note** this function does not support method invocation syntax | |
| /// `obj.name(args)` because that could be a getter followed by a call. | |
| /// See [visitMethodInvocation]. | |
| js_ast.Expression _emitOperatorCall( | |
| Expression receiver, | |
| Member? target, | |
| String name, | |
| List<Expression> args, | |
| ) { | |
| // TODO(jmesserly): calls that don't pass `element` are probably broken for | |
| // `super` calls from disallowed super locations. | |
| var memberName = _emitMemberName(name, member: target); | |
| if (target == null) { | |
| // dynamic dispatch | |
| var dynamicHelper = const {'[]': 'dindex', '[]=': 'dsetindex'}[name]; | |
| if (dynamicHelper != null) { | |
| return _runtimeCall('$dynamicHelper(#, #)', [ | |
| _visitExpression(receiver), | |
| _visitExpressionList(args), | |
| ]); | |
| } else { | |
| return _runtimeCall('dsend(#, #, [#])', [ | |
| _visitExpression(receiver), | |
| memberName, | |
| _visitExpressionList(args), | |
| ]); | |
| } | |
| } | |
| // Generic dispatch to a statically known method. | |
| return js.call('#.#(#)', [ | |
| _visitExpression(receiver), | |
| memberName, | |
| _visitExpressionList(args), | |
| ]); | |
| } | |
| // TODO(jmesserly): optimize super operators for kernel | |
| @override | |
| js_ast.Expression visitAbstractSuperMethodInvocation( | |
| AbstractSuperMethodInvocation node, | |
| ) { | |
| return _emitSuperMethodInvocation(node.interfaceTarget, node.arguments); | |
| } | |
| @override | |
| js_ast.Expression visitSuperMethodInvocation(SuperMethodInvocation node) { | |
| return _emitSuperMethodInvocation(node.interfaceTarget, node.arguments); | |
| } | |
| js_ast.Expression _emitSuperMethodInvocation( | |
| Member target, | |
| Arguments arguments, | |
| ) { | |
| return js_ast.Call( | |
| _emitSuperTarget(target), | |
| _emitArgumentList(arguments, target: target), | |
| ); | |
| } | |
| /// Emits the [js_ast.PropertyAccess] for accessors or method calls to | |
| /// [jsTarget].[jsName], replacing `super` if it is not allowed in scope. | |
| js_ast.PropertyAccess _emitSuperTarget(Member member, {bool setter = false}) { | |
| var jsName = _declareMemberName(member); | |
| if (_optimizeNonVirtualFieldAccess && | |
| member is Field && | |
| !_virtualFields.isVirtual(member)) { | |
| return js_ast.PropertyAccess(js_ast.This(), jsName); | |
| } | |
| if (_superAllowed) return js_ast.PropertyAccess(js_ast.Super(), jsName); | |
| // If we can't emit `super` in this context, generate a helper that does it | |
| // for us, and call the helper. | |
| // | |
| // NOTE: This is intended to help in the cases of calling a `super` getter, | |
| // setter, or method. For the case of tearing off a `super` method in | |
| // contexts where `super` isn't allowed, see | |
| // [_emitSuperTearoffFromDisallowedContext]. | |
| var name = member.name.text; | |
| var getter = | |
| (member is Field && !setter) || | |
| (member is Procedure && member.isGetter); | |
| // Prefix applied to the name only used in the compiler for a map key. This | |
| // name does not make its way into the compiled program. | |
| var lookupPrefix = setter | |
| ? r'set$' | |
| : getter | |
| ? r'get$' | |
| : ''; | |
| var jsMethod = _superHelpers.putIfAbsent('$lookupPrefix$name', () { | |
| var isAccessor = member is Procedure ? member.isAccessor : true; | |
| if (isAccessor) { | |
| assert( | |
| member is Procedure | |
| ? member.isSetter == setter | |
| : !setter || !(member as Field).isFinal, | |
| ); | |
| var fn = js.fun( | |
| setter | |
| ? 'function(x) { super[#] = x; }' | |
| : 'function() { return super[#]; }', | |
| [jsName], | |
| ); | |
| return js_ast.Method( | |
| _emitScopedId(name), | |
| fn, | |
| isGetter: !setter, | |
| isSetter: setter, | |
| ); | |
| } else { | |
| var function = member.function; | |
| var params = [ | |
| ..._emitTypeFormals(function.typeParameters), | |
| for (var param in function.positionalParameters) | |
| _emitIdentifier(param.parameterName), | |
| if (function.namedParameters.isNotEmpty) _namedArgumentTemp, | |
| ]; | |
| var fn = js.fun('function(#) { return super[#](#); }', [ | |
| params, | |
| jsName, | |
| params, | |
| ]); | |
| name = js_ast.friendlyNameForDartOperator[name] ?? name; | |
| return js_ast.Method(_emitScopedId(name), fn); | |
| } | |
| }); | |
| return js_ast.PropertyAccess(js_ast.This(), jsMethod.name); | |
| } | |
| /// Generates a special string used for identifying a torn off member [m]. | |
| /// | |
| /// This tag is used for determining tearoff equality. We attach these tags | |
| /// at tearoff time for static tearoffs and in the method signature for | |
| /// dynamic tearoffs. | |
| String fullyResolvedTargetLabel(Member m) { | |
| return '${m.enclosingLibrary.importUri}:${m.enclosingClass?.name ?? ""}'; | |
| } | |
| /// Generates a special string used for identifying class [c]'s applied mixed | |
| /// in members. | |
| /// | |
| /// This tag is used for determining tearoff equality. We attach these tags | |
| /// at tearoff time for static tearoffs and in the method signature for | |
| /// dynamic tearoffs. | |
| String fullyResolvedMixinClassLabel(Class c) { | |
| return '${c.enclosingLibrary.importUri}:${c.name}'; | |
| } | |
| /// Generates a helper method that is inserted into the class that binds a | |
| /// tearoff of [member] from `super` and returns a call to the helper. | |
| /// | |
| /// This method assumes `super` is not allowed in the current context. | |
| // TODO(nshahan) Replace with a kernel transform and synthetic method filters | |
| // for devtools. | |
| js_ast.Expression _emitSuperTearoffFromDisallowedContext(Member member) { | |
| var jsName = _declareMemberName(member); | |
| var name = '_#super#tearOff#${member.name.text}'; | |
| var jsMethod = _superHelpers.putIfAbsent(name, () { | |
| var superclass = member.enclosingClass!; | |
| var supertypeReference = | |
| _mixinSuperclassCache[superclass] ?? | |
| _emitTopLevelNameNoExternalInterop(superclass); | |
| var jsReturnValue = _runtimeCall('superTearoff(this, #, #)', [ | |
| supertypeReference, | |
| jsName, | |
| ]); | |
| var fn = js.fun('function() { return #; }', [jsReturnValue]); | |
| name = js_ast.friendlyNameForDartOperator[name] ?? name; | |
| return js_ast.Method(_emitScopedId(name), fn); | |
| }); | |
| return js_ast.Call(js_ast.PropertyAccess(js_ast.This(), jsMethod.name), []); | |
| } | |
| /// If [e] is a [TypeLiteral] or a [TypeLiteralConstant] expression, return | |
| /// the underlying [DartType], otherwise returns null. | |
| // TODO(sigmund,nshahan): remove all uses of type literals in the runtime | |
| // libraries, so that this pattern can be deleted. | |
| DartType? _getTypeLiteralType(Expression e) { | |
| if (e is TypeLiteral) return e.type; | |
| if (e is ConstantExpression) { | |
| var constant = e.constant; | |
| if (constant is TypeLiteralConstant) { | |
| return constant.type.withDeclaredNullability(Nullability.nonNullable); | |
| } | |
| } | |
| return null; | |
| } | |
| @override | |
| js_ast.Expression visitStaticInvocation(StaticInvocation node) { | |
| var target = node.target; | |
| if (ExternalEffect.isExternalEffect(node)) { | |
| return js_ast.LiteralNull(); | |
| } | |
| if (isInlineJS(target)) return _emitInlineJSCode(node) as js_ast.Expression; | |
| if (target.isFactory) return _emitFactoryInvocation(node); | |
| var enclosingLibrary = target.enclosingLibrary; | |
| if (_isDartLibrary(enclosingLibrary, '_rti') && | |
| _inlineTester.canInline(target.function)) { | |
| // Transform code that would otherwise appear as a static invocation: | |
| // ``` | |
| // if (_rti._isString(object)) {...} | |
| // ``` | |
| // | |
| // to be avoid cost of extra function calls: | |
| // | |
| // ``` | |
| // if (typeof object == "string") {...} | |
| // ``` | |
| var body = node.target.function.body; | |
| Expression? bodyToInline; | |
| // Extract the body. | |
| if (body is ReturnStatement) { | |
| // Ex: foo() => <body>; | |
| bodyToInline = body.expression; | |
| } else if (body is Block) { | |
| // Ex: foo() { <body> } | |
| 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 Variable: | |
| replacementArguments[i], | |
| }; | |
| // Clone the body using the same cloner to ensure the cloned parameters | |
| // are correctly linked to their accesses. | |
| var cloneToInline = bodyToInline.accept(cloner); | |
| // Substitute the use of the parameters with the values passed. | |
| var replacer = VariableGetReplacer(replacements); | |
| var replaced = cloneToInline.accept(replacer) as Expression; | |
| // Compile the result normally and wrap in parenthesis. | |
| return js.call('(#)', [replaced.accept(this)]); | |
| } | |
| } | |
| if (_isDartInternal(enclosingLibrary)) { | |
| var args = node.arguments; | |
| if (args.positional.length == 1 && | |
| args.types.length == 1 && | |
| args.named.isEmpty && | |
| target.name.text == 'unsafeCast') { | |
| // Optimize some internal SDK calls by avoiding the insertion of a | |
| // runtime cast. | |
| return args.positional.single.accept(this); | |
| } else if (node.arguments.positional.length == 2 && | |
| node.arguments.types.length == 1 && | |
| node.arguments.named.isEmpty && | |
| target.name.text == 'extractTypeArguments') { | |
| // Inline the extraction and method call at compile time because we | |
| // don't preserve the original type argument names into the runtime. | |
| // Those names are needed in the evaluation string used to extract the | |
| // types from the provided instance. | |
| // At this time the only two uses of this method are extracting from | |
| // `Iterable` and `Map`. There are no extension type uses so no need for | |
| // erasure here. | |
| var extractionType = node.arguments.types.single; | |
| if (extractionType is! InterfaceType) { | |
| throw UnsupportedError( | |
| 'Type arguments can only be extracted from interface types: ' | |
| 'found $extractionType (${extractionType.runtimeType}) at ' | |
| '${node.location}', | |
| ); | |
| } | |
| var extractionTypeParameters = extractionType.classNode.typeParameters; | |
| if (extractionTypeParameters.isEmpty) { | |
| throw UnsupportedError( | |
| 'The extraction type must have type arguments to be extracted: ' | |
| 'found $extractionType (${extractionType.runtimeType}) at ' | |
| '${node.location}', | |
| ); | |
| } | |
| var extractionTypeParameterNames = extractionTypeParameters.map( | |
| (p) => '${extractionType.classNode.name}.${p.name!}', | |
| ); | |
| var instance = node.arguments.positional.first.accept(this); | |
| var function = node.arguments.positional.last.accept(this); | |
| var extractedTypeArgs = js_ast.ArrayInitializer([ | |
| for (var recipe in extractionTypeParameterNames) | |
| js.call('#.#(#, "$recipe")', [ | |
| _emitLibraryName(_rtiLibrary), | |
| _emitMemberName('evalInInstance', memberClass: _rtiClass), | |
| instance, | |
| ]), | |
| ]); | |
| return _runtimeCall('dgcall(#, #, [])', [function, extractedTypeArgs]); | |
| } | |
| } | |
| if (_isDartForeignHelper(enclosingLibrary)) { | |
| var args = node.arguments.positional; | |
| var typeArgs = node.arguments.types; | |
| var name = target.name.text; | |
| if (args.isEmpty) { | |
| if (typeArgs.isEmpty && name == 'DART_RUNTIME_LIBRARY') { | |
| return _runtimeModule; | |
| } | |
| if (typeArgs.length == 1) { | |
| if (name == 'TYPE_REF') { | |
| return _emitType(typeArgs.single); | |
| } | |
| } | |
| } | |
| if (args.length == 1) { | |
| if (name == 'getInterceptor') { | |
| var argExpression = args.single.accept(this); | |
| return _runtimeCall('getInterceptorForRti(#)', [argExpression]); | |
| } | |
| if (name == 'JS_GET_NAME') { | |
| var staticGet = args.single as StaticGet; | |
| var enumField = staticGet.target as Field; | |
| return _emitExpressionForJsGetName(_asJsGetName(enumField)); | |
| } | |
| if (name == 'JS_CLASS_REF') { | |
| var constNode = args.single as ConstantExpression; | |
| var typeConstant = constNode.constant as TypeLiteralConstant; | |
| var type = typeConstant.type; | |
| if (type is NullType) { | |
| return _emitTopLevelName(_coreTypes.deprecatedNullClass); | |
| } | |
| if (type is! InterfaceType) { | |
| throw UnsupportedError( | |
| 'JS_CLASS_REF only supports interface types: found $type ' | |
| '(${type.runtimeType}) at ${node.location}', | |
| ); | |
| } | |
| return _emitTopLevelName(type.classNode); | |
| } | |
| if (name == 'RAW_DART_FUNCTION_REF') { | |
| var expression = args.single as ConstantExpression; | |
| var fn = expression.constant as StaticTearOffConstant; | |
| return _emitStaticTarget(fn.target); | |
| } | |
| if (name == 'JS_GET_FLAG') { | |
| var flag = args.single as StringLiteral; | |
| var value = flag.value; | |
| return switch (value) { | |
| 'DEV_COMPILER' => js.boolean(true), | |
| 'MINIFIED' => js.boolean(false), | |
| 'VARIANCE' => | |
| // Variance is turned on by default, but only interfaces that have | |
| // at least one type parameter with non-legacy variance will have | |
| // extra information recorded. | |
| js.boolean(true), | |
| _ => throw UnsupportedError( | |
| 'Unknown JS_GET_FLAG "$value" at ${node.location}', | |
| ), | |
| }; | |
| } | |
| } else if (args.length == 2) { | |
| if (name == 'JS_EMBEDDED_GLOBAL') return _emitEmbeddedGlobal(node); | |
| if (name == 'JS_STRING_CONCAT') { | |
| var left = _visitExpression(args.first); | |
| var right = _visitExpression(args.last); | |
| return js.call('# + #', [left, right]); | |
| } | |
| } | |
| if (name == 'JS_BUILTIN') { | |
| var staticGet = args[1] as StaticGet; | |
| var enumField = staticGet.target as Field; | |
| return _emitOperationForJsBuiltIn(_asJsBuiltin(enumField)); | |
| } | |
| if (name == 'JS_RAW_EXCEPTION') { | |
| // Serves as a way to access the wrapped JS exception. | |
| return _emitVariableRef(_rethrowParameter!); | |
| } | |
| if (name == 'JS_RTI_PARAMETER') { | |
| return _rtiParam; | |
| } | |
| } | |
| if (_isSdkInternalRuntime(enclosingLibrary)) { | |
| var name = target.name.text; | |
| if (node.arguments.positional.isEmpty) { | |
| if (name == 'hotRestartGeneration') { | |
| return _runtimeCall('hotRestartIteration'); | |
| } | |
| } | |
| if (node.arguments.positional.length == 1) { | |
| var firstArg = node.arguments.positional.single; | |
| if (name == 'extensionSymbol' && firstArg is StringLiteral) { | |
| return _getSymbol(_getExtensionSymbolInternal(firstArg.value)); | |
| } | |
| } else if (node.arguments.positional.length == 2) { | |
| var firstArg = node.arguments.positional[0]; | |
| var secondArg = node.arguments.positional[1]; | |
| var type = _getTypeLiteralType(secondArg); | |
| if (name == '_jsInstanceOf' && | |
| type is InterfaceType && | |
| type.typeArguments.isEmpty) { | |
| return js.call('# instanceof #', [ | |
| _visitExpression(firstArg), | |
| _emitTopLevelName(type.classNode), | |
| ]); | |
| } | |
| } | |
| } | |
| if (_isDartJsHelper(enclosingLibrary)) { | |
| var name = target.name.text; | |
| if (name == 'jsObjectGetPrototypeOf') { | |
| var obj = node.arguments.positional.single; | |
| return _emitJSObjectGetPrototypeOf( | |
| _visitExpression(obj), | |
| fullyQualifiedName: false, | |
| ); | |
| } | |
| if (name == 'jsObjectSetPrototypeOf') { | |
| var obj = node.arguments.positional.first; | |
| var prototype = node.arguments.positional.last; | |
| return _emitJSObjectSetPrototypeOf( | |
| _visitExpression(obj), | |
| _visitExpression(prototype), | |
| fullyQualifiedName: false, | |
| ); | |
| } | |
| } | |
| if (target.isExternal && | |
| target.isExtensionTypeMember && | |
| target.function.namedParameters.isNotEmpty) { | |
| // JS interop checks assert that only external extension type constructors | |
| // and factories have named parameters. | |
| assert(target.function.positionalParameters.isEmpty); | |
| var namedProperties = <String, Expression>{}; | |
| for (var named in node.arguments.named) { | |
| var name = named.name; | |
| for (var parameter in target.function.namedParameters) { | |
| if (parameter.parameterName == named.name) { | |
| var customName = getDartJSInteropJSName(parameter); | |
| if (customName.isNotEmpty) { | |
| name = customName; | |
| } | |
| break; | |
| } | |
| } | |
| namedProperties[name] = named.value; | |
| } | |
| return _emitObjectLiteral(namedProperties, isDartJsInterop: true); | |
| } | |
| if (target == _coreTypes.identicalProcedure) { | |
| return _emitCoreIdenticalCall(node.arguments.positional); | |
| } | |
| if (_isDebuggerCall(target)) { | |
| return _emitDebuggerCall(node) as js_ast.Expression; | |
| } | |
| if (_isDartJsUtil(enclosingLibrary)) { | |
| // We try and do further inlining here for the unchecked/trusted-type | |
| // variants of js_util methods. Note that we only lower the methods that | |
| // are used in transformations and are private. Also note that this | |
| // inlining ignores `sdk/lib/_internal/js_shared/lib/js_util_patch.dart`'s | |
| // implementations for the lowered methods. | |
| // | |
| // If you update the code there, you should update the code here. | |
| // Long-term, we'll need a better IR to lower interop methods to, or a DDC | |
| // inliner to do the inlining for us. | |
| var name = target.name.text; | |
| if (name == '_getPropertyTrustType') { | |
| return js_ast.PropertyAccess( | |
| _visitExpression(node.arguments.positional[0]), | |
| _visitExpression(node.arguments.positional[1]), | |
| ); | |
| } else if (name == '_setPropertyUnchecked') { | |
| return _visitExpression(node.arguments.positional[2]) | |
| .toAssignExpression( | |
| js_ast.PropertyAccess( | |
| _visitExpression(node.arguments.positional[0]), | |
| _visitExpression(node.arguments.positional[1]), | |
| ), | |
| ); | |
| } else if (_callMethodUncheckedRegex.hasMatch(name)) { | |
| // Note that we don't lower `_callMethodTrustType`. This is because it | |
| // uses `assertInterop` checks. | |
| var trustType = name.contains('TrustType'); | |
| var args = <js_ast.Expression>[]; | |
| assert(node.arguments.named.isEmpty); | |
| // Ignore the receiver and name of the method. | |
| for (var i = 2; i < node.arguments.positional.length; i++) { | |
| args.add(_visitExpression(node.arguments.positional[i])); | |
| } | |
| js_ast.Expression call = js_ast.Call( | |
| js_ast.PropertyAccess( | |
| _visitExpression(node.arguments.positional[0]), | |
| _visitExpression(node.arguments.positional[1]), | |
| ), | |
| args, | |
| ); | |
| if (!trustType) { | |
| call = _emitCast(call, node.arguments.types[0]); | |
| } | |
| return call; | |
| } else if (_callConstructorUncheckedRegex.hasMatch(name)) { | |
| var args = <js_ast.Expression>[]; | |
| 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.PropertyAccess _emitStaticTarget(Member target) { | |
| var c = target.enclosingClass; | |
| if (c != null) { | |
| // A static native element should just forward directly to the JS type's | |
| // member, for example `Css.supports(...)` in dart:html should be replaced | |
| // by a direct call to the DOM API: `global.CSS.supports`. | |
| var isExternal = _isExternal(target); | |
| if (isExternal && (target as Procedure).isStatic) { | |
| var nativeName = _extensionTypes.getNativePeers(c); | |
| if (nativeName.isNotEmpty) { | |
| var annotationName = _annotationName(target, isJSName); | |
| var memberName = annotationName == null | |
| ? _emitStaticMemberName(target.name.text, target) | |
| : js.string(annotationName); | |
| return js_ast.PropertyAccess( | |
| _runtimeCall('global.#', [nativeName[0]]), | |
| memberName, | |
| ); | |
| } | |
| } | |
| return js_ast.PropertyAccess( | |
| _emitStaticClassName(c, isExternal), | |
| _emitStaticMemberName(target.name.text, target), | |
| ); | |
| } | |
| return _emitTopLevelName(target); | |
| } | |
| /// Returns all parts of [node] flattened into a list so they can be passed in | |
| /// the calling convention for calls with no runtime checks. | |
| /// | |
| /// When [types] is `false` any type arguments present in [node] will be | |
| /// omitted. | |
| /// | |
| /// Passing [target] when applicable allows for detection of an annotation to | |
| /// omit passing type parameters and to detect if positional arguments should | |
| /// be packaged to be passed to a JavaScript using an interop call. | |
| List<js_ast.Expression> _emitArgumentList( | |
| Arguments node, { | |
| bool types = true, | |
| Member? target, | |
| }) { | |
| types = types && _reifyGenericFunction(target); | |
| var ( | |
| :typeArguments, | |
| :positionalArguments, | |
| :namedArguments, | |
| ) = _emitArgumentGroups( | |
| node, | |
| isJSInterop: target != null && isJsMember(target), | |
| ); | |
| return [ | |
| if (types && typeArguments != null) ...typeArguments, | |
| ...?positionalArguments, | |
| if (namedArguments != null) js_ast.ObjectInitializer([...namedArguments]), | |
| ]; | |
| } | |
| /// Returns all arguments from [node] but kept in separate packets so they can | |
| /// be further processed. | |
| /// | |
| /// Facilitates passing arguments in the calling convention used by runtime | |
| /// helpers that check arguments. | |
| /// | |
| /// When [isJSInterop] is `true` the positional arguments are packaged to | |
| /// be passed to JavaScript via an interop call. | |
| _ArgumentGroups _emitArgumentGroups( | |
| Arguments node, { | |
| required bool isJSInterop, | |
| }) { | |
| var typeArguments = node.types.isEmpty | |
| ? null | |
| : [for (var typeArgument in node.types) _emitType(typeArgument)]; | |
| var positionalArguments = node.positional.isEmpty | |
| ? null | |
| : [ | |
| for (var arg in node.positional) | |
| if (arg is StaticInvocation && | |
| isJSSpreadInvocation(arg.target) && | |
| arg.arguments.positional.length == 1) | |
| js_ast.Spread(_visitExpression(arg.arguments.positional[0])) | |
| else if (isJSInterop) | |
| _visitExpression(_assertInterop(arg)) | |
| else | |
| _visitExpression(arg), | |
| ]; | |
| // Named arguments with JS interop are only allowed in object literal | |
| // constructors. Invocations to those are always transformed by | |
| // `_emitObjectLiteral` and therefore we should never expect to see a JS | |
| // interop invocation with named arguments here. | |
| if (node.named.isNotEmpty) assert(!isJSInterop); | |
| var namedArguments = node.named.isEmpty | |
| ? null | |
| : [for (var arg in node.named) _emitNamedExpression(arg)]; | |
| return ( | |
| typeArguments: typeArguments, | |
| positionalArguments: positionalArguments, | |
| namedArguments: namedArguments, | |
| ); | |
| } | |
| js_ast.Property _emitNamedExpression(NamedExpression arg) => | |
| js_ast.Property(_propertyName(arg.name), _visitExpression(arg.value)); | |
| /// Emits code for the `JS(...)` macro. | |
| js_ast.Node _emitInlineJSCode(StaticInvocation node) { | |
| var args = node.arguments.positional; | |
| // arg[0] is static return type, used in `RestrictedStaticTypeAnalyzer` | |
| var code = args[1]; | |
| List<Expression> 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 = <Expression>[]; | |
| source = code.expressions.map((expression) { | |
| if (expression is StringLiteral) { | |
| return expression.value; | |
| } else { | |
| templateArgs.add(expression); | |
| return '#'; | |
| } | |
| }).join(); | |
| } | |
| } else { | |
| templateArgs = args.skip(2).toList(); | |
| source = (code as StringLiteral).value; | |
| } | |
| // TODO(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(<type literal>)`, which | |
| // the compiler will then optimize. | |
| var wasInForeignJS = _isInForeignJS; | |
| _isInForeignJS = true; | |
| var jsArgs = templateArgs.map(_visitExpression).toList(); | |
| _isInForeignJS = wasInForeignJS; | |
| var result = js.parseForeignJS(source).instantiate(jsArgs); | |
| // Add a check to make sure any JS() values from a native type are typed | |
| // properly in sound null-safety. | |
| if (_isWebLibrary(_currentLibrary!.importUri)) { | |
| var type = node.getStaticType(_staticTypeContext); | |
| if (type.isPotentiallyNonNullable) { | |
| result = _runtimeCall('checkNativeNonNull(#)', [result]); | |
| } | |
| } | |
| assert( | |
| result is js_ast.Expression || | |
| result is js_ast.Statement && node.parent is ExpressionStatement, | |
| ); | |
| return result.withSourceInformation(_nodeStart(node)); | |
| } | |
| js_ast.Expression _emitEmbeddedGlobal(StaticInvocation node) { | |
| var constantExpression = node.arguments.positional[1] as ConstantExpression; | |
| var name = constantExpression.constant as StringConstant; | |
| var value = name.value; | |
| if (value == 'arrayRti') { | |
| // Special case for the rti on a JSArray. These are defined via the dartx | |
| // extension functionality. | |
| return _emitMemberName('arrayRti', memberClass: _jsArrayClass); | |
| } | |
| return _runtimeCall('#', [name.value]); | |
| } | |
| /// Returns the string literal that is to be used as the result of a call to | |
| /// [JS_GET_NAME] for [name]. | |
| js_ast.Expression _emitExpressionForJsGetName(JsGetName name) { | |
| switch (name) { | |
| case JsGetName.OPERATOR_IS_PREFIX: | |
| return js.string(js_ast.FixedNames.operatorIsPrefix); | |
| case JsGetName.SIGNATURE_NAME: | |
| return _runtimeCall('#', [ | |
| js.string(js_ast.FixedNames.operatorSignature), | |
| ]); | |
| case JsGetName.RTI_NAME: | |
| return js.string(js_ast.FixedNames.rtiName); | |
| case JsGetName.FUTURE_CLASS_TYPE_NAME: | |
| return js.string( | |
| _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.futureClass), | |
| ); | |
| case JsGetName.LIST_CLASS_TYPE_NAME: | |
| return js.string( | |
| _typeRecipeGenerator.interfaceTypeRecipe(_coreTypes.listClass), | |
| ); | |
| case JsGetName.RTI_FIELD_AS: | |
| return _emitMemberName( | |
| js_ast.FixedNames.rtiAsField, | |
| memberClass: _rtiClass, | |
| ); | |
| case JsGetName.RTI_FIELD_IS: | |
| return _emitMemberName( | |
| js_ast.FixedNames.rtiIsField, | |
| memberClass: _rtiClass, | |
| ); | |
| default: | |
| throw UnsupportedError('JsGetName has no name for "$name".'); | |
| } | |
| } | |
| /// Returns the expression that is to be used as the result of a call to | |
| /// [JS_BUILTIN] for [builtin]. | |
| js_ast.Expression _emitOperationForJsBuiltIn(JsBuiltin builtin) { | |
| switch (builtin) { | |
| case JsBuiltin.dartClosureConstructor: | |
| // TODO(48585) Is this safe or will it conflict with functions that | |
| // enter the program through JS Interop? | |
| return js.call('Function'); | |
| case JsBuiltin.dartObjectConstructor: | |
| return _emitTopLevelName(_coreTypes.objectClass); | |
| default: | |
| throw UnsupportedError('JsBuiltin has no operation for "$builtin".'); | |
| } | |
| } | |
| String _enumValueName(Field field) { | |
| var enumName = field.enclosingClass!.name; | |
| var valueName = field.name.text; | |
| return '$enumName.$valueName'; | |
| } | |
| JsGetName _asJsGetName(Field field) => JsGetName.values.firstWhere( | |
| (val) => val.toString() == _enumValueName(field), | |
| ); | |
| JsBuiltin _asJsBuiltin(Field field) => JsBuiltin.values.firstWhere( | |
| (val) => val.toString() == _enumValueName(field), | |
| ); | |
| bool _isWebLibrary(Uri importUri) => | |
| importUri.isScheme('dart') && | |
| (importUri.path == 'html' || | |
| importUri.path == 'svg' || | |
| importUri.path == 'indexed_db' || | |
| importUri.path == 'web_audio' || | |
| importUri.path == 'web_gl' || | |
| importUri.path == 'web_sql' || | |
| importUri.path == 'html_common'); | |
| bool _isNull(Expression expr) => | |
| expr is NullLiteral || | |
| expr.getStaticType(_staticTypeContext).extensionTypeErasure is NullType; | |
| bool _doubleEqIsIdentity(Expression left, Expression right) { | |
| // If we statically know LHS or RHS is null we can use ==. | |
| if (_isNull(left) || _isNull(right)) return true; | |
| // If the representation of the two types will not induce conversion in | |
| // JS then we can use == . | |
| return !_typeRep.equalityMayConvert( | |
| left.getStaticType(_staticTypeContext), | |
| right.getStaticType(_staticTypeContext), | |
| ); | |
| } | |
| bool _tripleEqIsIdentity(Expression left, Expression right) { | |
| // If either is non-nullable, then we don't need to worry about | |
| // equating null and undefined, and so we can use triple equals. | |
| return !_isNullable(left) || !_isNullable(right); | |
| } | |
| /// Returns true if [expr] can be null. | |
| bool _isNullable(Expression expr) => _nullableInference.isNullable(expr); | |
| js_ast.Expression _emitJSDoubleEq( | |
| List<js_ast.Expression> args, { | |
| bool negated = false, | |
| }) { | |
| var op = negated ? '# != #' : '# == #'; | |
| return js.call(op, args); | |
| } | |
| js_ast.Expression _emitJSTripleEq( | |
| List<js_ast.Expression> args, { | |
| bool negated = false, | |
| }) { | |
| var op = negated ? '# !== #' : '# === #'; | |
| return js.call(op, args); | |
| } | |
| js_ast.Expression _emitCoreIdenticalCall( | |
| List<Expression> args, { | |
| bool negated = false, | |
| }) { | |
| if (args.length != 2) { | |
| // Shouldn't happen in typechecked code | |
| return _runtimeCall( | |
| 'throw(Error("compile error: calls to `identical` require 2 args")', | |
| ); | |
| } | |
| var left = args[0]; | |
| var right = args[1]; | |
| var jsArgs = [_visitExpression(left), _visitExpression(right)]; | |
| if (_tripleEqIsIdentity(left, right)) { | |
| return _emitJSTripleEq(jsArgs, negated: negated); | |
| } | |
| if (_doubleEqIsIdentity(left, right)) { | |
| return _emitJSDoubleEq(jsArgs, negated: negated); | |
| } | |
| var code = negated ? '!#' : '#'; | |
| return js.call( | |
| code, | |
| js_ast.Call(_emitTopLevelName(_coreTypes.identicalProcedure), jsArgs), | |
| ); | |
| } | |
| /// Returns true if this [member] is a JS interop member. | |
| bool isJSInteropMember(Member member) => | |
| member.isExternal && hasJSInteropAnnotation(member.enclosingClass!); | |
| @override | |
| js_ast.Expression visitConstructorInvocation(ConstructorInvocation node) { | |
| var ctor = node.target; | |
| var ctorClass = ctor.enclosingClass; | |
| var args = node.arguments; | |
| if (isJSAnonymousType(ctorClass)) { | |
| var namedProperties = { | |
| for (final named in node.arguments.named) named.name: named.value, | |
| }; | |
| return _emitObjectLiteral( | |
| namedProperties, | |
| isDartJsInterop: hasDartJSInteropAnnotation(ctorClass), | |
| ); | |
| } | |
| // JS interop constructor calls do not provide an RTI at the call site. | |
| var shouldProvideRti = | |
| !isJSInteropMember(ctor) && _requiresRtiForInstantiation(ctorClass); | |
| var rti = shouldProvideRti | |
| ? _emitType( | |
| node.constructedType, | |
| emitJSInteropGenericClassTypeParametersAsAny: false, | |
| ) | |
| : null; | |
| var result = js_ast.New(_emitConstructorName(node.constructedType, ctor), [ | |
| ?rti, | |
| ..._emitArgumentList(args, types: false, target: ctor), | |
| ]); | |
| return node.isConst ? _canonicalizeConstObject(result) : result; | |
| } | |
| js_ast.Expression _emitFactoryInvocation(StaticInvocation node) { | |
| var args = node.arguments; | |
| var ctor = node.target; | |
| var ctorClass = ctor.enclosingClass!; | |
| // JS interop constructor calls do not require an RTI at the call site. | |
| if (isJSInteropMember(ctor)) { | |
| return _emitJSInteropNew(ctor, args); | |
| } | |
| var type = ctorClass.typeParameters.isEmpty | |
| ? _coreTypes.nonNullableRawType(ctorClass) | |
| : InterfaceType(ctorClass, Nullability.nonNullable, args.types); | |
| if (isFromEnvironmentInvocation(_coreTypes, node)) { | |
| var value = _constants.evaluate(node); | |
| if (value is PrimitiveConstant) { | |
| return visitConstant(value); | |
| } | |
| } | |
| if (args.positional.isEmpty && | |
| args.named.isEmpty && | |
| ctorClass.enclosingLibrary.importUri.isScheme('dart')) { | |
| // Skip the slow SDK factory constructors when possible. | |
| switch (ctorClass.name) { | |
| case 'Map': | |
| case 'HashMap': | |
| case 'LinkedHashMap': | |
| if (ctor.name.text == '') { | |
| var mapType = _createMapImplType(type); | |
| var mapClass = _emitClassRef(mapType); | |
| var rti = _emitType(mapType); | |
| return js.call('new #.new(#)', [mapClass, rti]); | |
| } else if (ctor.name.text == 'identity') { | |
| var mapType = _createMapImplType(type, identity: true); | |
| var mapClass = _emitClassRef(mapType); | |
| var rti = _emitType(mapType); | |
| return js.call('new #.new(#)', [mapClass, rti]); | |
| } | |
| case 'Set': | |
| case 'HashSet': | |
| case 'LinkedHashSet': | |
| if (ctor.name.text == '') { | |
| var setType = _createSetImplType(type); | |
| var setClass = _emitClassRef(setType); | |
| var rti = _emitType(setType); | |
| return js.call('new #.new(#)', [setClass, rti]); | |
| } else if (ctor.name.text == 'identity') { | |
| var setType = _createSetImplType(type, identity: true); | |
| var setClass = _emitClassRef(setType); | |
| var rti = _emitType(setType); | |
| return js.call('new #.new(#)', [setClass, rti]); | |
| } | |
| case 'List': | |
| if (ctor.name.text == '') { | |
| return _emitList(type.typeArguments[0], []); | |
| } | |
| } | |
| } | |
| var rti = _requiresRtiForInstantiation(ctorClass) | |
| ? _emitType(type, emitJSInteropGenericClassTypeParametersAsAny: false) | |
| : null; | |
| var result = js_ast.Call(_emitConstructorName(type, ctor), [ | |
| ?rti, | |
| ..._emitArgumentList(args, types: false), | |
| ]); | |
| return node.isConst ? _canonicalizeConstObject(result) : result; | |
| } | |
| js_ast.Expression _emitJSInteropNew(Member ctor, Arguments args) { | |
| var ctorClass = ctor.enclosingClass!; | |
| if (isJSAnonymousType(ctorClass)) { | |
| var namedProperties = { | |
| for (final named in args.named) named.name: named.value, | |
| }; | |
| return _emitObjectLiteral( | |
| namedProperties, | |
| isDartJsInterop: hasDartJSInteropAnnotation(ctorClass), | |
| ); | |
| } | |
| // JS interop constructor calls do not require an RTI at the call site. | |
| return js_ast.New( | |
| _emitConstructorName(_coreTypes.nonNullableRawType(ctorClass), ctor), | |
| _emitArgumentList(args, types: false, target: ctor), | |
| ); | |
| } | |
| InterfaceType _createMapImplType(InterfaceType type, {bool? identity}) { | |
| var typeArgs = type.typeArguments; | |
| if (typeArgs.isEmpty) { | |
| return type.withDeclaredNullability(Nullability.nonNullable); | |
| } | |
| identity ??= _typeRep.isPrimitive(typeArgs[0]); | |
| var c = identity ? _identityHashMapImplClass : _linkedHashMapImplClass; | |
| return InterfaceType(c, Nullability.nonNullable, typeArgs); | |
| } | |
| InterfaceType _createSetImplType(InterfaceType type, {bool? identity}) { | |
| var typeArgs = type.typeArguments; | |
| if (typeArgs.isEmpty) { | |
| return type.withDeclaredNullability(Nullability.nonNullable); | |
| } | |
| identity ??= _typeRep.isPrimitive(typeArgs[0]); | |
| var c = identity ? _identityHashSetImplClass : _linkedHashSetImplClass; | |
| return InterfaceType(c, Nullability.nonNullable, typeArgs); | |
| } | |
| /// Given a mapping [namedProperties] between names and their associated | |
| /// expressions, returns an object literal with the same names mapped to the | |
| /// transformed expressions. | |
| /// | |
| /// This is used for object literal constructors in JS interop. | |
| /// | |
| /// If [isDartJsInterop] is true, it implies that this is an object literal | |
| /// constructor using `dart:js_interop`. If false, this method wraps the | |
| /// expressions with an [_assertInterop] call. | |
| js_ast.Expression _emitObjectLiteral( | |
| Map<String, Expression> namedProperties, { | |
| required bool isDartJsInterop, | |
| }) { | |
| if (namedProperties.isEmpty) return js.call('{}'); | |
| var properties = <js_ast.Property>[]; | |
| for (var name in namedProperties.keys) { | |
| // `assertInterop` is not needed for `dart:js_interop`. It statically | |
| // disallows `Function`s from being passed unless they were explicitly | |
| // passed as an opaque reference, in which case, we shouldn't require the | |
| // user to wrap them anyways. | |
| var value = isDartJsInterop | |
| ? namedProperties[name]! | |
| : _assertInterop(namedProperties[name]!); | |
| properties.add( | |
| js_ast.Property(_propertyName(name), _visitExpression(value)), | |
| ); | |
| } | |
| return js_ast.ObjectInitializer([...properties]); | |
| } | |
| @override | |
| js_ast.Expression visitNot(Not node) { | |
| var operand = node.operand; | |
| if (operand is EqualsCall) { | |
| return _emitEqualityOperator( | |
| operand.left, | |
| operand.interfaceTarget, | |
| operand.right, | |
| negated: true, | |
| ); | |
| } else if (operand is EqualsNull) { | |
| return _emitCoreIdenticalCall([ | |
| operand.expression, | |
| NullLiteral(), | |
| ], negated: true); | |
| } else if (operand is StaticInvocation && | |
| operand.target == _coreTypes.identicalProcedure) { | |
| return _emitCoreIdenticalCall( | |
| operand.arguments.positional, | |
| negated: true, | |
| ); | |
| } | |
| var jsOperand = _visitTest(operand); | |
| if (jsOperand is js_ast.LiteralBool) { | |
| // Flipping the value here for `!true` or `!false` allows for simpler | |
| // `if (true)` or `if (false)` detection and optimization. | |
| return js_ast.LiteralBool(!jsOperand.value) | |
| .withSourceInformation(jsOperand.sourceInformation) | |
| as js_ast.LiteralBool; | |
| } | |
| // Logical negation, `!e`, is a boolean conversion context since it is | |
| // defined as `e ? false : true`. | |
| return js.call('!#', jsOperand).withSourceInformation(continueSourceMap); | |
| } | |
| @override | |
| js_ast.Expression visitNullCheck(NullCheck node) { | |
| var expr = node.operand; | |
| var jsExpr = _visitExpression(expr); | |
| // If the expression is non-nullable already, this is a no-op. | |
| return _isNullable(expr) ? _runtimeCall('nullCheck(#)', [jsExpr]) : jsExpr; | |
| } | |
| @override | |
| js_ast.Expression visitLogicalExpression(LogicalExpression node) { | |
| // The operands of logical boolean operators are subject to boolean | |
| // conversion. | |
| return _visitTest(node); | |
| } | |
| @override | |
| js_ast.Expression visitConditionalExpression(ConditionalExpression node) { | |
| var condition = _visitTest(node.condition); | |
| if (condition is js_ast.LiteralBool) { | |
| if (condition.value) { | |
| // Avoid emitting conditional when one branch is effectively dead code. | |
| // ex: `true ? foo : bar` -> `foo` | |
| return _visitExpression(node.then); | |
| } else { | |
| // ex: `false ? foo : bar` -> `bar` | |
| return _visitExpression(node.otherwise); | |
| } | |
| } | |
| var then = _visitExpression(node.then); | |
| var otherwise = _visitExpression(node.otherwise); | |
| return js.call('# ? # : #', [condition, then, otherwise]) | |
| ..sourceInformation = | |
| condition.sourceInformation ?? _nodeStart(node.condition); | |
| } | |
| @override | |
| js_ast.Expression visitStringConcatenation(StringConcatenation node) { | |
| var parts = <js_ast.Expression>[]; | |
| for (var e in node.expressions) { | |
| var jsExpr = _visitExpression(e); | |
| if (jsExpr is js_ast.LiteralString && jsExpr.valueWithoutQuotes.isEmpty) { | |
| continue; | |
| } | |
| var type = e.getStaticType(_staticTypeContext).extensionTypeErasure; | |
| if (DartTypeEquivalence( | |
| _coreTypes, | |
| ignoreTopLevelNullability: true, | |
| ).areEqual(type, _coreTypes.stringNonNullableRawType) && | |
| !_isNullable(e)) { | |
| parts.add(jsExpr); | |
| } else if (_shouldCallObjectMemberHelper(e)) { | |
| parts.add(_runtimeCall('str(#)', [jsExpr])); | |
| } else { | |
| // It is safe to call a version of `str()` that does not probe for the | |
| // toString method before calling it. | |
| parts.add(_runtimeCall('strSafe(#)', [jsExpr])); | |
| } | |
| } | |
| if (parts.isEmpty) return js.string(''); | |
| return js_ast.Expression.binary(parts, '+'); | |
| } | |
| @override | |
| js_ast.Expression visitConstructorTearOff(ConstructorTearOff node) { | |
| throw UnsupportedError('Constructor tear off'); | |
| } | |
| @override | |
| js_ast.Expression visitRedirectingFactoryTearOff( | |
| RedirectingFactoryTearOff node, | |
| ) { | |
| throw UnsupportedError('RedirectingFactory tear off'); | |
| } | |
| @override | |
| js_ast.Expression visitTypedefTearOff(TypedefTearOff node) { | |
| throw UnsupportedError('Typedef instantiation'); | |
| } | |
| @override | |
| js_ast.Expression visitIsExpression(IsExpression node) { | |
| return _emitIsExpression(node.operand, node.type.extensionTypeErasure); | |
| } | |
| js_ast.Expression _emitIsExpression(Expression operand, DartType type) { | |
| // Generate `is` as `dart.is` or `typeof` depending on the RHS type. | |
| var lhs = _visitExpression(operand); | |
| // It is invalid to use a simplified check for a native type in place of | |
| // a type test for a `TypeParameterType`. This is because at runtime type | |
| // parameters can be instantiated as the bottom type `Never` and | |
| // `val is Never` should always evaluate to false. | |
| var typeofName = | |
| type is TypeParameterType || type is StructuralParameterType | |
| ? null | |
| : _typeRep.typeFor(type).primitiveTypeOf; | |
| // Inline non-nullable primitive types other than int (which requires a | |
| // Math.floor check). | |
| if (typeofName != null && | |
| type.nullability == Nullability.nonNullable && | |
| type != _types.coreTypes.intNonNullableRawType) { | |
| return js.call('typeof # == #', [lhs, js.string(typeofName, "'")]); | |
| } | |
| return js.call('#.#(#)', [ | |
| _emitType(type), | |
| _emitMemberName(js_ast.FixedNames.rtiIsField, memberClass: _rtiClass), | |
| lhs, | |
| ]); | |
| } | |
| @override | |
| js_ast.Expression visitAsExpression(AsExpression node) { | |
| var fromExpr = node.operand; | |
| var jsFrom = _visitExpression(fromExpr); | |
| if (node.isUnchecked) return jsFrom; | |
| return _emitCast( | |
| jsFrom, | |
| node.type, | |
| fromStaticType: fromExpr.getStaticType(_staticTypeContext), | |
| isTypeError: node.isTypeError, | |
| ); | |
| } | |
| js_ast.Expression _emitCast( | |
| js_ast.Expression value, | |
| DartType toType, { | |
| DartType? fromStaticType, | |
| bool isTypeError = false, | |
| }) { | |
| toType = toType.extensionTypeErasure; | |
| if (_types.isTop(toType)) return value; | |
| if (fromStaticType != null) { | |
| fromStaticType = fromStaticType.extensionTypeErasure; | |
| // If the check was put here by static analysis to ensure soundness, we | |
| // can't skip it. For example, one could implement covariant generic | |
| // caller side checks like this: | |
| // | |
| // typedef F<T>(T t); | |
| // class C<T> { | |
| // F<T> f; | |
| // add(T t) { | |
| // // required check `t as T` | |
| // } | |
| // } | |
| // main() { | |
| // C<Object> c = new C<int>()..f = (int x) => x.isEven; | |
| // c.f('hi'); // required check `c.f as F<Object>` | |
| // c.add('hi); | |
| // } | |
| // | |
| if (!isTypeError && _types.isSubtypeOf(fromStaticType, toType)) { | |
| return value; | |
| } | |
| if (!isTypeError && | |
| _mustBeNonNullable(toType) && | |
| DartTypeEquivalence( | |
| _coreTypes, | |
| ignoreTopLevelNullability: true, | |
| ).areEqual(fromStaticType, toType)) { | |
| // If the underlying type is the same, we only need a null check. | |
| return _runtimeCall('nullCast(#, #)', [value, _emitType(toType)]); | |
| } | |
| // All Dart number types map to a JavaScript Number. We can specialize | |
| // these cases. | |
| if (_typeRep.isNumber(fromStaticType) && _typeRep.isNumber(toType)) { | |
| // If `toType` is some form of `num`, it should have been filtered | |
| // above. | |
| if (toType == _coreTypes.doubleNullableRawType) { | |
| // Any number/nullability -> double? : no-op | |
| return value; | |
| } | |
| if (toType == _coreTypes.doubleNonNullableRawType) { | |
| if (fromStaticType.nullability == Nullability.nonNullable) { | |
| // Any non-nullable number -> double : no-op | |
| return value; | |
| } | |
| // Any number/nullability -> double : null check | |
| return _runtimeCall('nullCast(#, #)', [value, _emitType(toType)]); | |
| } | |
| } | |
| } | |
| var directMethod = _directCastMethod(toType); | |
| if (directMethod != null) { | |
| return js.call('#(#)', [_emitTopLevelName(directMethod), value]); | |
| } | |
| return js.call('#.#(#)', [ | |
| _emitType(toType), | |
| _emitMemberName(js_ast.FixedNames.rtiAsField, memberClass: _rtiClass), | |
| value, | |
| ]); | |
| } | |
| /// Returns the direct `_as` method when [type] is a primitive type otherwise, | |
| /// `null`. | |
| Member? _directCastMethod(DartType type) { | |
| if (type is InterfaceType && type.typeArguments.isEmpty) { | |
| if (type.nullability == Nullability.nonNullable) { | |
| if (type == _types.coreTypes.boolNonNullableRawType) return _asBool; | |
| if (type == _types.coreTypes.doubleNonNullableRawType) return _asDouble; | |
| if (type == _types.coreTypes.intNonNullableRawType) return _asInt; | |
| if (type == _types.coreTypes.numNonNullableRawType) return _asNum; | |
| if (type == _types.coreTypes.objectNonNullableRawType) return _asObject; | |
| if (type == _types.coreTypes.stringNonNullableRawType) return _asString; | |
| } else if (type.nullability == Nullability.nullable) { | |
| if (type == _types.coreTypes.boolNullableRawType) return _asBoolQ; | |
| if (type == _types.coreTypes.doubleNullableRawType) return _asDoubleQ; | |
| if (type == _types.coreTypes.intNullableRawType) return _asIntQ; | |
| if (type == _types.coreTypes.numNullableRawType) return _asNumQ; | |
| if (type == _types.coreTypes.stringNullableRawType) return _asStringQ; | |
| } | |
| } | |
| return null; | |
| } | |
| @override | |
| js_ast.Expression visitSymbolLiteral(SymbolLiteral node) => | |
| _emitDartSymbol(node.value); | |
| @override | |
| js_ast.Expression visitTypeLiteral(TypeLiteral node) => | |
| _emitTypeLiteral(node.type); | |
| js_ast.Expression _emitTypeLiteral(DartType type) { | |
| var typeRep = _emitType(type); | |
| // TODO(46002) All `JS()` calls in the SDK should be explicit when using the | |
| // internal rti object by calling the `TYPE_REF` helper. | |
| if (_isInForeignJS) return typeRep; | |
| // If the type is a type literal expression in Dart code, wrap the raw | |
| // runtime type in a "Type" instance. | |
| return js.call('#.createRuntimeType(#)', [ | |
| _emitLibraryName(_rtiLibrary), | |
| typeRep, | |
| ]); | |
| } | |
| @override | |
| js_ast.Expression visitThisExpression(ThisExpression node) => js_ast.This(); | |
| @override | |
| js_ast.Expression visitRethrow(Rethrow node) { | |
| return _runtimeCall('rethrow(#)', [_emitVariableRef(_rethrowParameter!)]); | |
| } | |
| @override | |
| js_ast.Expression visitThrow(Throw node) => | |
| _runtimeCall('throw(#)', [_visitExpression(node.expression)]); | |
| @override | |
| js_ast.Expression visitListLiteral(ListLiteral node) { | |
| var elementType = node.typeArgument; | |
| var elements = _visitExpressionList(node.expressions); | |
| return _emitList(elementType, elements); | |
| } | |
| js_ast.Expression _emitList( | |
| DartType itemType, | |
| List<js_ast.Expression> 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<E>.of(list)` | |
| var type = InterfaceType( | |
| _jsArrayClass, | |
| Nullability.nonNullable, | |
| DartTypeList(itemType), | |
| ); | |
| var arrayClass = _emitClassRef(type); | |
| var arrayRti = _emitType(type); | |
| return js.call('#.of(#, #)', [arrayClass, arrayRti, list]); | |
| } | |
| js_ast.Expression _emitConstList( | |
| DartType elementType, | |
| List<js_ast.Expression> 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, | |
| DartTypeList(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<js_ast.Expression> 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<js_ast.Expression> 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<String> 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(node.value); | |
| var body = _visitExpression(node.body); | |
| var temp = _tempVariables.remove(v); | |
| // TODO(eernst): Remove the following `if` if anonymous-methods is rejected. | |
| // Otherwise, revise this method to be more readable. | |
| // See https://github.com/dart-lang/language/issues/260. | |
| if (temp == null && !_isTemporaryVariable(v)) { | |
| temp = _emitVariableRef(v); | |
| } | |
| if (temp != null) { | |
| if (_letVariables != null) { | |
| init = js_ast.Assignment(temp, init); | |
| _letVariables!.add(temp); | |
| } else { | |
| // TODO(jmesserly): make sure this doesn't happen on any performance | |
| // critical call path. | |
| // | |
| // Annotations on a top-level, non-lazy function type should be the only | |
| // remaining use. | |
| var arrowFunction = js_ast.ArrowFun([temp], body); | |
| final asyncAnalysis = PreTranslationAnalysis((node) { | |
| throw UnsupportedError('Unknown node in block expression: $node'); | |
| }, arrowFunction)..analyze(); | |
| final isAsyncIife = asyncAnalysis.hasAwaitOrYield.contains(body); | |
| if (isAsyncIife) { | |
| final transformedFunction = _rewriteAsyncFunction( | |
| js_ast.Fun([temp], js_ast.Block([js_ast.Return(body)])), | |
| AsyncMarker.Async, | |
| null, | |
| node.getStaticType(_staticTypeContext), | |
| functionBody: _toSourceLocation(node.fileOffset), | |
| functionEnd: _toSourceLocation(node.fileOffset), | |
| ); | |
| arrowFunction = js_ast.ArrowFun([temp], transformedFunction.body); | |
| } | |
| final call = js_ast.Call(arrowFunction, [init]); | |
| return isAsyncIife ? js_ast.Await(call) : call; | |
| } | |
| } | |
| return js_ast.Binary(',', init, body); | |
| } | |
| @override | |
| js_ast.Expression visitBlockExpression(BlockExpression node) { | |
| var jsExpr = _visitExpression(node.value); | |
| var jsStmts = [ | |
| for (var s in node.body.statements) _visitStatement(s), | |
| js_ast.Return(jsExpr), | |
| ]; | |
| final statementBlock = js_ast.Block(jsStmts); | |
| var arrowFunction = js_ast.ArrowFun(const [], statementBlock); | |
| final asyncAnalysis = PreTranslationAnalysis((node) { | |
| throw UnsupportedError( | |
| 'Unknown node in block expression: $node (${node.runtimeType}, ' | |
| '${node.sourceInformation})', | |
| ); | |
| }, arrowFunction)..analyze(); | |
| final isAsyncIife = asyncAnalysis.hasAwaitOrYield.contains(statementBlock); | |
| if (isAsyncIife) { | |
| final transformedFunction = _rewriteAsyncFunction( | |
| js_ast.Fun(const [], statementBlock), | |
| AsyncMarker.Async, | |
| null, | |
| node.getStaticType(_staticTypeContext), | |
| functionBody: _toSourceLocation(node.fileOffset), | |
| functionEnd: _toSourceLocation(node.fileOffset), | |
| ); | |
| arrowFunction = js_ast.ArrowFun(const [], transformedFunction.body); | |
| } | |
| final call = js_ast.Call(arrowFunction, const []); | |
| return isAsyncIife ? js_ast.Await(call) : call; | |
| } | |
| @override | |
| js_ast.Expression visitInstantiation(Instantiation node) { | |
| return _runtimeCall('gbind(#, #)', [ | |
| _visitExpression(node.expression), | |
| node.typeArguments.map(_emitType).toList(), | |
| ]); | |
| } | |
| @override | |
| js_ast.Expression visitLoadLibrary(LoadLibrary node) => | |
| _runtimeCall('loadLibrary(#, #, #)', [ | |
| js.string(node.import.enclosingLibrary.importUri.toString()), | |
| js.string(node.import.name!), | |
| js.string( | |
| _libraryToModule(node.import.targetLibrary, throwIfNotFound: false), | |
| ), | |
| ]); | |
| // TODO(jmesserly): DDC loads all libraries eagerly. | |
| // See | |
| // https://github.com/dart-lang/sdk/issues/27776 | |
| // https://github.com/dart-lang/sdk/issues/27777 | |
| @override | |
| js_ast.Expression visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) => | |
| _runtimeCall('checkDeferredIsLoaded(#, #)', [ | |
| js.string(node.import.enclosingLibrary.importUri.toString()), | |
| js.string(node.import.name!), | |
| ]); | |
| bool _reifyFunctionType(FunctionNode f) { | |
| var parent = f.parent; | |
| if (parent is FunctionDeclaration && | |
| (isLateLoweredLocalGetter(parent.variable) || | |
| isLateLoweredLocalSetter(parent.variable))) { | |
| // Late local variables are lowered to local get and set functions. | |
| // These functions should never need to be tagged with their types. | |
| return false; | |
| } | |
| if (!_currentLibrary!.importUri.isScheme('dart')) return true; | |
| // SDK libraries can skip reification if they request it. | |
| bool reifyFunctionTypes(Expression a) => | |
| isBuiltinAnnotation(a, '_js_helper', 'ReifyFunctionTypes'); | |
| while (parent != null) { | |
| var a = findAnnotation(parent, reifyFunctionTypes); | |
| if (a != null) { | |
| var value = _constants.getFieldValueFromAnnotation(a, 'value'); | |
| if (value is bool) return value; | |
| } | |
| parent = parent.parent; | |
| } | |
| return true; | |
| } | |
| bool _reifyTearoff(Member member) { | |
| return member is Procedure && | |
| !member.isAccessor && | |
| !member.isFactory && | |
| !(_isInForeignJS && _isBuildingSdk) && | |
| !usesJSInterop(member) && | |
| _reifyFunctionType(member.function); | |
| } | |
| /// Returns the name value of the `JSExportName` annotation (when compiling | |
| /// the SDK), or `null` if there's none. This is used to control the name | |
| /// under which functions are compiled and exported. | |
| String? _jsExportName(NamedNode n) { | |
| var library = getLibrary(n); | |
| if (!library.importUri.isScheme('dart')) return null; | |
| return _annotationName(n, isJSExportNameAnnotation); | |
| } | |
| /// If [node] has annotation matching [test] and the first argument is a | |
| /// string, this returns the string value. | |
| /// | |
| /// Calls [findAnnotation] followed by [getNameFromAnnotation]. | |
| String? _annotationName(NamedNode node, bool Function(Expression) test) { | |
| var annotation = findAnnotation(node, test); | |
| return annotation != null | |
| ? _constants.getFieldValueFromAnnotation(annotation, 'name') as String? | |
| : null; | |
| } | |
| @override | |
| js_ast.Expression visitConstant(Constant node) { | |
| if (node is StaticTearOffConstant) { | |
| // JS() or external JS consts should not be lazily loaded. | |
| var isSdk = node.target.enclosingLibrary.importUri.isScheme('dart'); | |
| if (_isInForeignJS) { | |
| return _emitStaticTarget(node.target); | |
| } | |
| if (node.target.isExternal && !isSdk) { | |
| return _runtimeCall('tearoffInterop(#, #)', [ | |
| _emitStaticTarget(node.target), | |
| js.boolean(_isNullCheckableJsInterop(node.target)), | |
| ]); | |
| } | |
| } | |
| if (node is TypeLiteralConstant) { | |
| // We bypass the use of constants, since types are already canonicalized | |
| // in the DDC output. | |
| return _emitTypeLiteral(node.type); | |
| } | |
| if (node is PrimitiveConstant) { | |
| return super.visitConstant(node); | |
| } | |
| // Avoid caching constants during evaluation while scoping issues remain. | |
| // See: #44713 | |
| if (_constTableCache.incrementalMode) { | |
| return super.visitConstant(node); | |
| } | |
| var constAlias = _constAliasCache[node]; | |
| if (constAlias != null) { | |
| return constAlias; | |
| } | |
| var constAliasString = 'C${_constAliasCache.length}'; | |
| var constAliasProperty = _propertyName(constAliasString); | |
| _constTableCache[constAliasString] = js.call('void 0'); | |
| var constAliasAccessor = _constTableCache.access(constAliasString); | |
| var constAccessor = js.call('# || #.#', [ | |
| constAliasAccessor, | |
| _constTable, | |
| constAliasProperty, | |
| ]); | |
| _constAliasCache[node] = constAccessor; | |
| var constJs = super.visitConstant(node); | |
| var func = js_ast.Fun( | |
| [], | |
| js_ast.Block([ | |
| js.statement('return # = #;', [constAliasAccessor, constJs]), | |
| ]), | |
| ); | |
| var accessor = js_ast.Method(constAliasProperty, func, isGetter: true); | |
| _constLazyAccessors.add(accessor); | |
| return constAccessor; | |
| } | |
| @override | |
| js_ast.Expression visitNullConstant(NullConstant node) => | |
| js_ast.LiteralNull(); | |
| @override | |
| js_ast.Expression visitBoolConstant(BoolConstant node) => | |
| js.boolean(node.value); | |
| @override | |
| js_ast.Expression visitIntConstant(IntConstant node) => js.number(node.value); | |
| @override | |
| js_ast.Expression visitDoubleConstant(DoubleConstant node) { | |
| var value = node.value; | |
| // Emit the constant as an integer, if possible. | |
| if (value.isFinite) { | |
| var intValue = value.toInt(); | |
| const minInt32 = -0x80000000; | |
| const maxInt32 = 0x7FFFFFFF; | |
| if (intValue.toDouble() == value && | |
| intValue >= minInt32 && | |
| intValue <= maxInt32) { | |
| return js.number(intValue); | |
| } | |
| } | |
| if (value.isInfinite) { | |
| if (value.isNegative) { | |
| return js.call('-1 / 0'); | |
| } | |
| return js.call('1 / 0'); | |
| } | |
| if (value.isNaN) { | |
| return js.call('0 / 0'); | |
| } | |
| return js.number(value); | |
| } | |
| @override | |
| js_ast.Expression visitStringConstant(StringConstant node) => | |
| js.escapedString(node.value, '"'); | |
| // DDC does not currently use the non-primitive constant nodes; rather these | |
| // are emitted via their normal expression nodes. | |
| @override | |
| js_ast.Expression defaultConstant(Constant node) => _emitInvalidNode(node); | |
| @override | |
| js_ast.Expression visitSymbolConstant(SymbolConstant node) => | |
| _emitDartSymbol(node.name, library: node.libraryReference?.asLibrary); | |
| @override | |
| js_ast.Expression visitMapConstant(MapConstant node) { | |
| var entries = [ | |
| for (var e in node.entries) ...[ | |
| visitConstant(e.key), | |
| visitConstant(e.value), | |
| ], | |
| ]; | |
| return _emitConstMap(node.keyType, node.valueType, entries); | |
| } | |
| @override | |
| js_ast.Expression visitListConstant(ListConstant node) => _emitConstList( | |
| node.typeArgument, | |
| node.entries.map(visitConstant).toList(), | |
| ); | |
| @override | |
| js_ast.Expression visitSetConstant(SetConstant node) => _emitConstSet( | |
| node.typeArgument, | |
| node.entries.map(visitConstant).toList(), | |
| ); | |
| @override | |
| js_ast.Expression visitRecordConstant(RecordConstant node) { | |
| // RecordConstant names are already sorted alphabetically in kernel. | |
| var names = node.named.keys; | |
| var positionalElementCount = node.positional.length; | |
| var shapeKey = _recordShapeKey(positionalElementCount, names); | |
| return _runtimeCall('recordLiteral(#, #, #, [#])', [ | |
| js.string(shapeKey), | |
| js.number(positionalElementCount), | |
| names.isEmpty ? js.call('void 0') : js.stringArray(names), | |
| [ | |
| ...node.positional.map(visitConstant), | |
| ...node.named.values.map(visitConstant), | |
| ], | |
| ]); | |
| } | |
| @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<Reference, Constant> 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); | |
| } | |
| var type = node.getType(_staticTypeContext); | |
| assert( | |
| type.nullability == Nullability.nonNullable, | |
| 'An instance constant should only ever have a non-nullable type.', | |
| ); | |
| var classRef = _emitClassRef(type as InterfaceType); | |
| var prototype = js.call('#.prototype', [classRef]); | |
| var properties = [ | |
| if (type.typeArguments.isNotEmpty) | |
| // Generic interface type instances require a type information tag. | |
| js_ast.Property( | |
| _propertyName(js_ast.FixedNames.rtiName), | |
| _emitType(type), | |
| ), | |
| for (var e in node.fieldValues.entries.toList().reversed) | |
| entryToProperty(e), | |
| ]; | |
| var constant = _canonicalizeConstObject( | |
| _emitJSObjectSetPrototypeOf( | |
| js_ast.ObjectInitializer(properties, multiline: true), | |
| prototype, | |
| fullyQualifiedName: false, | |
| ), | |
| ); | |
| _currentTypeEnvironment = savedTypeEnvironment; | |
| return constant; | |
| } | |
| /// Emits a private name JS Symbol for [member] unique to a Dart class | |
| /// [className]. | |
| /// | |
| /// This is now required for fields of constant objects that may be overridden | |
| /// within the same library. | |
| js_ast.ScopedId _emitClassPrivateNameSymbol( | |
| Library library, | |
| String className, | |
| Member member, [ | |
| js_ast.ScopedId? id, | |
| ]) { | |
| var name = '$className.${member.name.text}'; | |
| // Wrap the name as a symbol here so it matches what you would find at | |
| // runtime when you get all properties and symbols from an instance. | |
| memberNames[member] = 'Symbol($name)'; | |
| return _emitPrivateNameSymbol(library, name, id); | |
| } | |
| @override | |
| js_ast.Expression visitStaticTearOffConstant(StaticTearOffConstant node) { | |
| _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<js_ast.Comment> _generateCompilationHeader() { | |
| var headerOptions = [ | |
| if (_options.canaryFeatures) 'canary', | |
| if (_options.emitLibraryBundle) 'emitLibraryBundle', | |
| 'enableAsserts(${_options.enableAsserts})', | |
| ]; | |
| var enabledExperiments = <String>[]; | |
| _options.experiments.forEach((key, value) { | |
| if (value) enabledExperiments.add(key); | |
| }); | |
| var header = [ | |
| js_ast.Comment( | |
| 'Generated by DDC, the Dart Development Compiler (to JavaScript).', | |
| ), | |
| if (const bool.fromEnvironment('dart.library.io')) | |
| js_ast.Comment('Version: ${io.Platform.version}'), | |
| js_ast.Comment('Module: ${_options.moduleName}'), | |
| js_ast.Comment('Flags: ${headerOptions.join(', ')}'), | |
| if (enabledExperiments.isNotEmpty) | |
| js_ast.Comment('Experiments: ${enabledExperiments.join(', ')}'), | |
| ]; | |
| return header; | |
| } | |
| @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.ScopedId id) { | |
| _imports[library] = id; | |
| } | |
| void _setEmitIfIncrementalLibrary(Library library) { | |
| if (_incrementalMode) { | |
| _setEmitIfIncremental( | |
| _libraryToModule(library), | |
| libraryUriToJsIdentifier(library.importUri), | |
| ); | |
| } | |
| } | |
| 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<js_ast.Parameter> formals, | |
| bool Function() isLastParamMutated, | |
| ) { | |
| if (name == '[]=') { | |
| _operatorSetResultStack.add( | |
| isLastParamMutated() | |
| ? js_ast.ScopedId((formals.last as js_ast.Identifier).name) | |
| : formals.last as js_ast.Identifier, | |
| ); | |
| } else { | |
| _operatorSetResultStack.add(null); | |
| } | |
| } | |
| /// Called when finished emitting methods/functions, and must correspond to a | |
| /// previous [_enterFunction] call. | |
| js_ast.Block _exitFunction( | |
| List<js_ast.Parameter> 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 <value>;`, 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(#)', [<expr>]) | |
| /// | |
| /// Generates a JS AST representing: | |
| /// | |
| /// dart.asInt(<expr>) | |
| /// | |
| js_ast.Expression _runtimeCall(String code, [List<Object>? args]) { | |
| _setEmitIfIncremental(_libraryToModule(_coreLibrary), _runtimeModule.name); | |
| return js.call('#.$code', <Object>[_runtimeModule, ...?args]); | |
| } | |
| /// Calls [_runtimeCall] and uses `toStatement()` to convert the resulting | |
| /// expression into a statement. | |
| js_ast.Statement _runtimeStatement(String code, [List<Object>? args]) => | |
| _runtimeCall(code, args).toStatement(); | |
| /// Emits a private name JS Symbol for [name] scoped to the Dart [library]. | |
| /// | |
| /// If the same name is used in multiple libraries in the same module, | |
| /// distinct symbols will be used, so each library will have distinct private | |
| /// member names, that won't collide at runtime, as required by the Dart | |
| /// language spec. | |
| /// | |
| /// If an [id] is provided, try to use that. | |
| /// | |
| /// TODO(vsm): Clean up id generation logic. This method is used to both | |
| /// define new symbols and to reference existing ones. If it's called | |
| /// multiple times with same [library] and [name], we'll allocate redundant | |
| /// top-level variables (see callers to this method). | |
| js_ast.ScopedId _emitPrivateNameSymbol( | |
| Library library, | |
| String name, [ | |
| js_ast.ScopedId? id, | |
| ]) { | |
| /// Initializes the JS `Symbol` for the private member [name] in [library]. | |
| /// | |
| /// If the library is in the current JS module ([_libraries] contains it), | |
| /// the private name will be created and exported. The exported symbol is | |
| /// used for a few things: | |
| /// | |
| /// - private fields of constant objects | |
| /// - stateful hot reload (not yet implemented) | |
| /// - correct library scope in REPL (not yet implemented) | |
| /// | |
| /// If the library is imported, then the existing private name will be | |
| /// retrieved from it. In both cases, we use the same `dart.privateName` | |
| /// runtime call. | |
| js_ast.ScopedId initPrivateNameSymbol() { | |
| var idName = name.endsWith('=') ? name.replaceAll('=', '_') : name; | |
| idName = idName.replaceAll(js_ast.invalidCharInIdentifier, '_'); | |
| var identifier = id ?? js_ast.ScopedId(idName); | |
| _addSymbol( | |
| identifier, | |
| _runtimeCall('privateName(#, #)', [ | |
| _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] by default, so the Symbol is scoped properly. Symbol | |
| /// constants should pass the symbol's [library] if the referenced symbol was | |
| /// declared outside the current library. | |
| js_ast.Expression _emitDartSymbol(String symbolName, {Library? library}) { | |
| // TODO(vsm): Handle qualified symbols correctly. | |
| var last = symbolName.split('.').last; | |
| var name = js.escapedString(symbolName, "'"); | |
| js_ast.Expression result; | |
| if (last.startsWith('_')) { | |
| var nativeSymbolAccessor = _getSymbol( | |
| _emitPrivateNameSymbol(library ?? _currentLibrary!, last), | |
| ); | |
| result = js.call('new #.new(#, #)', [ | |
| _emitConstructorAccess(_privateSymbolType), | |
| name, | |
| nativeSymbolAccessor, | |
| ]); | |
| } else { | |
| result = js.call('new #.new(#)', [ | |
| _emitConstructorAccess(_internalSymbolType), | |
| name, | |
| ]); | |
| } | |
| return _canonicalizeConstObject(result); | |
| } | |
| /// Calls the `dart.const` function in "dart:_runtime" to canonicalize a | |
| /// constant instance of a user-defined class stored in [expr]. | |
| js_ast.Expression _canonicalizeConstObject(js_ast.Expression expr) => | |
| _runtimeCall('const(#)', [expr]); | |
| /// Emits preamble for the module containing [libraries], and returns the | |
| /// list of module items for further items to be added. | |
| /// | |
| /// The preamble consists of initializing the identifiers for each library, | |
| /// that will be used to store their members. It also generates the | |
| /// appropriate ES6 `export` declaration to export them from this module. | |
| /// | |
| /// After the code for all of the library members is emitted, | |
| /// [_emitImportsAndExtensionSymbols] should be used to emit imports/extension | |
| /// symbols into the list returned by this method. Finally, [_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<js_ast.ModuleItem> _startModule(Iterable<Library> 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.ScopedId('dart'); | |
| _extensionSymbolsModule = js_ast.ScopedId('dartx'); | |
| } | |
| // Initialize our library variables. | |
| var items = <js_ast.ModuleItem>[]; | |
| var exports = <js_ast.NameSpecifier>[]; | |
| if (_isBuildingSdk) { | |
| // Bootstrap the ability to create Dart library objects. | |
| var libraryProto = js_ast.ScopedId('_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.ScopedId(libraryUriToJsIdentifier(library.importUri)); | |
| _libraries[library] = libraryId; | |
| var alias = _jsLibraryAlias(library); | |
| var aliasId = alias == null ? null : js_ast.ScopedId(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.ScopedId('_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; | |
| var activeImports = _incrementalMode ? _incrementalImports : _imports; | |
| // It's either one of the libraries in this module, or it's an import. | |
| return _libraries[library] ?? | |
| activeImports.putIfAbsent( | |
| library, | |
| () => _isDartLibrary(library, '_rti') | |
| ? _rtiLibraryId | |
| : js_ast.ScopedId(libraryUriToJsIdentifier(library.importUri)), | |
| ); | |
| } | |
| /// Emits imports into [items]. | |
| void _emitImports(List<js_ast.ModuleItem> items) { | |
| var activeImports = _incrementalMode ? _incrementalImports : _imports; | |
| var modules = <String, List<Library>>{}; | |
| for (var import in activeImports.keys) { | |
| modules.putIfAbsent(_libraryToModule(import), () => []).add(import); | |
| } | |
| String? coreModuleName; | |
| if (!_libraries.containsKey(_coreLibrary)) { | |
| coreModuleName = _libraryToModule(_coreLibrary); | |
| } | |
| if (_incrementalMode && coreModuleName != null) { | |
| if (_incrementalModules.containsKey(coreModuleName)) { | |
| modules.putIfAbsent(coreModuleName, () => []); | |
| } | |
| } | |
| 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 = <js_ast.NameSpecifier>[]; | |
| for (var library in libraries) { | |
| if (!_incrementalMode || | |
| usedLibraries!.contains( | |
| libraryUriToJsIdentifier(library.importUri), | |
| )) { | |
| var alias = _jsLibraryAlias(library); | |
| if (alias != null) { | |
| var aliasId = js_ast.ScopedId(alias); | |
| imports.add( | |
| js_ast.NameSpecifier(aliasId, asName: activeImports[library]), | |
| ); | |
| } else { | |
| imports.add(js_ast.NameSpecifier(activeImports[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<js_ast.ModuleItem> 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<js_ast.ModuleItem> items, Library current) { | |
| var exports = <js_ast.NameSpecifier>[]; | |
| assert(_incrementalMode); | |
| assert(!_isBuildingSdk); | |
| var module = _libraryToModule(current); | |
| var usedLibraries = _incrementalModules[module] ?? {}; | |
| if (usedLibraries.isNotEmpty) { | |
| _libraries.forEach((library, libraryId) { | |
| if (usedLibraries.contains( | |
| libraryUriToJsIdentifier(library.importUri), | |
| )) { | |
| var alias = _jsLibraryAlias(library); | |
| var aliasId = alias == null ? libraryId : js_ast.ScopedId(alias); | |
| var asName = alias == null ? null : libraryId; | |
| exports.add(js_ast.NameSpecifier(aliasId, asName: asName)); | |
| } | |
| }); | |
| items.add( | |
| js_ast.ImportDeclaration( | |
| namedImports: exports, | |
| from: js.string(module, "'"), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Emits imports and extension methods into [items]. | |
| void _emitImportsAndExtensionSymbols( | |
| List<js_ast.ModuleItem> items, { | |
| bool forceExtensionSymbols = false, | |
| }) { | |
| _emitImports(items); | |
| _emitExtensionSymbols(items, forceExtensionSymbols: forceExtensionSymbols); | |
| } | |
| void _emitDebuggerExtensionInfo(String name) { | |
| var properties = <js_ast.Property>[]; | |
| var parts = <js_ast.Property>[]; | |
| _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)); | |
| // Dynamic modules shouldn't define a library that was previously defined. | |
| // We leverage that we track which libraries have been defined via | |
| // `trackedLibraries` to query whether a library already exists. | |
| if (_options.dynamicModule) { | |
| _moduleItems.add( | |
| js.statement( | |
| '''if (# != null) { | |
| throw Error( | |
| "Dynamic module provides second definition for " + #); | |
| }''', | |
| [ | |
| _runtimeCall('getLibrary(#)', [libraryName]), | |
| libraryName, | |
| ], | |
| ), | |
| ); | |
| } | |
| 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 mainly consumed by the debugger and by the stack trace | |
| // mapper. It is also used for the experimental dynamic modules feature | |
| // to validate that a dynamic module doesn't reintroduce an existing | |
| // library. | |
| // | |
| // 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<js_ast.ModuleItem> items, | |
| String moduleName, { | |
| List<js_ast.Comment> 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()); | |
| if (_dynamicEntrypoint != null) { | |
| // Expose the entrypoint of the dynamic module under a reserved name. | |
| // TODO(sigmund): this could use a reserved symbol from dartx. | |
| var name = _emitTopLevelName(_dynamicEntrypoint!); | |
| _moduleItems.add( | |
| js_ast.ExportDeclaration( | |
| js('var __dynamic_module_entrypoint__ = #', [name]), | |
| ), | |
| ); | |
| } | |
| // Add the 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<js_ast.ModuleItem> result, | |
| Iterable<js_ast.ModuleItem> 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.<name>`. | |
| /// | |
| /// Do not call this directly; you want [_emitMemberName], which knows how to | |
| /// handle the many details involved in naming. | |
| js_ast.ScopedId _getExtensionSymbolInternal(String name) { | |
| if (!_extensionSymbols.containsKey(name)) { | |
| var id = js_ast.ScopedId( | |
| '\$${js_ast.friendlyNameForDartOperator[name] ?? name}', | |
| ); | |
| _extensionSymbols[name] = id; | |
| _addSymbol(id, id); | |
| } | |
| var symbolId = _extensionSymbols[name]!; | |
| _symbolContainer.setEmitIfIncremental(symbolId); | |
| return symbolId; | |
| } | |
| /// Shorthand for identifier-like property names. | |
| /// For now, we emit them as strings and the printer restores them to | |
| /// identifiers if it can. | |
| // TODO(jmesserly): avoid the round tripping through quoted form. | |
| js_ast.LiteralString _propertyName(String name) => js.string(name, "'"); | |
| /// Unique identifiers indicating the locations to inline the corresponding | |
| /// information. | |
| /// | |
| /// We cannot generate the source map before the script it is for is | |
| /// generated so we have generate the script including this identifier in the | |
| /// JS AST, and then replace it once the source map is generated. Similarly, | |
| /// metrics include the size of the source map. | |
| static const String sourceMapLocationID = | |
| 'SourceMap3G5a8h6JVhHfdGuDxZr1EF9GQC8y0e6u'; | |
| static const String metricsLocationID = | |
| 'MetricsJ7xFWBfSv6ZjrW9yLb21GNzisZr3anSf5h'; | |
| /// Matches against the `dart:js_util` `_callMethodUnchecked` and | |
| /// `_callMethodUncheckedTrustType` variants with 0 to 4 arguments. | |
| static final RegExp _callMethodUncheckedRegex = RegExp( | |
| r'^\_callMethodUnchecked(TrustType)?[0-4]', | |
| ); | |
| /// Matches against the `dart:js_util` `_callConstructorUnchecked` and | |
| /// `_callConstructorUncheckedTrustType` variants with 0 to 4 arguments. | |
| static final RegExp _callConstructorUncheckedRegex = RegExp( | |
| r'^\_callConstructorUnchecked[0-4]', | |
| ); | |
| } | |
| bool _isInlineJSFunction(Statement? body) { | |
| var block = body; | |
| if (block is Block) { | |
| var statements = block.statements; | |
| if (statements.length != 1) return false; | |
| body = statements[0]; | |
| } | |
| if (body is ReturnStatement) { | |
| var expr = body.expression; | |
| return expr is StaticInvocation && isInlineJS(expr.target); | |
| } | |
| return false; | |
| } | |
| /// Return true if this is one of the methods/properties on all Dart Objects | |
| /// (toString, hashCode, noSuchMethod, runtimeType). | |
| /// | |
| /// Operator == is excluded, as it is handled as part of the equality binary | |
| /// operator. | |
| bool _isObjectMember(String name) { | |
| // We could look these up on Object, but we have hard coded runtime helpers | |
| // so it's not really providing any benefit. | |
| switch (name) { | |
| case 'hashCode': | |
| case 'toString': | |
| case 'noSuchMethod': | |
| case 'runtimeType': | |
| case '==': | |
| return true; | |
| } | |
| return false; | |
| } | |
| class _SwitchLabelState { | |
| String label; | |
| js_ast.Identifier variable; | |
| _SwitchLabelState(this.label, this.variable); | |
| } | |
| /// Whether [expression] is a constant of the form | |
| /// `const pragma('dyn-module:entry-point')`. | |
| /// | |
| /// Used to denote the entrypoint method of a dynamic module. | |
| // TODO(sigmund): move to package:kernel. | |
| bool _isDynamicModuleEntryPoint(Procedure p, CoreTypes coreTypes) { | |
| return hasPragma(p, 'dyn-module:entry-point', coreTypes); | |
| } |