blob: 619ed90455ae72e6b28754e71bd45fb53f894051 [file] [log] [blame]
// Copyright (c) 2018, 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 'package:meta/meta.dart';
import '../compiler/js_metalet.dart' as JS;
import '../compiler/js_names.dart' as JS;
import '../compiler/js_utils.dart' as JS;
import '../js_ast/js_ast.dart' as JS;
import '../js_ast/js_ast.dart' show js;
/// Shared code between Analyzer and Kernel backends.
///
/// This class should only implement functionality that depends purely on JS
/// classes, rather than on Analyzer/Kernel types.
abstract class SharedCompiler<Library, Class, InterfaceType, FunctionNode> {
/// 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 List<JS.Identifier> _operatorSetResultStack = [];
/// Private member names in this module, organized by their library.
final _privateNames = HashMap<Library, HashMap<String, JS.TemporaryId>>();
/// 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.TemporaryId>{};
/// The set of libraries we are currently compiling, and the temporaries used
/// to refer to them.
final _libraries = <Library, JS.Identifier>{};
/// Imported libraries, and the temporaries used to refer to them.
final _imports = <Library, JS.TemporaryId>{};
/// The identifier used to reference DDC's core "dart:_runtime" library from
/// generated JS code, typically called "dart" e.g. `dart.dcall`.
@protected
JS.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".
@protected
JS.Identifier extensionSymbolsModule;
/// Whether we're currently building the SDK, which may require special
/// bootstrapping logic.
///
/// This is initialized by [startModule], which must be called before
/// accessing this field.
@protected
bool isBuildingSdk;
/// The temporary variable that stores named arguments (these are passed via a
/// JS object literal, to match JS conventions).
@protected
final namedArgumentTemp = JS.TemporaryId('opts');
/// The list of output module items, in the order they need to be emitted in.
@protected
final moduleItems = <JS.ModuleItem>[];
/// Like [moduleItems] but for items that should be emitted after classes.
///
/// This is used for deferred supertypes of mutually recursive non-generic
/// classes.
@protected
final afterClassDefItems = <JS.ModuleItem>[];
/// The type used for private Dart [Symbol]s.
@protected
InterfaceType get privateSymbolType;
/// The type used for public Dart [Symbol]s.
@protected
InterfaceType get internalSymbolType;
/// The current library being compiled.
@protected
Library get currentLibrary;
/// The library for dart:core in the SDK.
@protected
Library get coreLibrary;
/// The import URI of current library.
@protected
Uri get currentLibraryUri;
/// The current function being compiled, if any.
@protected
FunctionNode get currentFunction;
/// Choose a canonical name from the [library] element.
///
/// This never uses the library's name (the identifier in the `library`
/// declaration) as it doesn't have any meaningful rules enforced.
@protected
String jsLibraryName(Library library);
/// Debugger friendly name for a Dart [library].
@protected
String jsLibraryDebuggerName(Library library);
/// Gets the module import URI that contains [library].
@protected
String libraryToModule(Library library);
/// Returns true if the library [l] is "dart:_runtime".
@protected
bool isSdkInternalRuntime(Library l);
/// Whether any superclass of [c] defines a static [name].
@protected
bool superclassHasStatic(Class c, String name);
/// Emits the expression necessary to access a constructor of [type];
@protected
JS.Expression emitConstructorAccess(InterfaceType type);
/// When compiling the body of a `operator []=` method, this will be non-null
/// and will indicate the the value that should be returned from any `return;`
/// statements.
JS.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].
@protected
void enterFunction(String name, List<JS.Parameter> formals,
bool Function() isLastParamMutated) {
if (name == '[]=') {
_operatorSetResultStack.add(isLastParamMutated()
? JS.TemporaryId((formals.last as JS.Identifier).name)
: formals.last as JS.Identifier);
} else {
_operatorSetResultStack.add(null);
}
}
/// Called when finished emitting methods/functions, and must correspond to a
/// previous [enterFunction] call.
@protected
JS.Block exitFunction(
String name, List<JS.Parameter> formals, JS.Block code) {
if (name == "==" &&
formals.isNotEmpty &&
currentLibraryUri.scheme != 'dart') {
// In Dart `operator ==` methods are not called with a null argument.
// This is handled before calling them. For performance reasons, we push
// this check inside the method, to simplify our `equals` helper.
//
// TODO(jmesserly): in most cases this check is not necessary, because
// the Dart code already handles it (typically by an `is` check).
// Eliminate it when possible.
code = js
.block('{ if (# == null) return false; #; }', [formals.first, 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.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.
@protected
JS.Statement emitReturnStatement(JS.Expression value) {
if (_operatorSetResult != null) {
var result = JS.Return(_operatorSetResult);
return value != null ? JS.Block([value.toStatement(), result]) : result;
}
return value != null ? value.toReturn() : JS.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>)
///
@protected
JS.Expression runtimeCall(String code, [args]) {
if (args != null) {
var newArgs = <Object>[runtimeModule];
if (args is Iterable) {
newArgs.addAll(args);
} else {
newArgs.add(args);
}
args = newArgs;
} else {
args = runtimeModule;
}
return js.call('#.$code', args);
}
/// Calls [runtimeCall] and uses `toStatement()` to convert the resulting
/// expression into a statement.
@protected
JS.Statement runtimeStatement(String code, [args]) {
return 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.
@protected
JS.TemporaryId emitPrivateNameSymbol(Library library, String name) {
/// 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.TemporaryId initPrivateNameSymbol() {
var idName = name.endsWith('=') ? name.replaceAll('=', '_') : name;
var id = JS.TemporaryId(idName);
moduleItems.add(js.statement('const # = #.privateName(#, #)',
[id, runtimeModule, emitLibraryName(library), js.string(name)]));
return id;
}
var privateNames = _privateNames.putIfAbsent(library, () => HashMap());
return privateNames.putIfAbsent(name, initPrivateNameSymbol);
}
/// 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 defins a getters with the same name.
@protected
JS.Expression defineValueOnClass(Class c, JS.Expression className,
JS.Expression nameExpr, JS.Expression value) {
var args = [className, nameExpr, value];
if (nameExpr is JS.LiteralString) {
var name = nameExpr.valueWithoutQuotes;
if (JS.isFunctionPrototypeGetter(name) || superclassHasStatic(c, name)) {
return runtimeCall('defineValue(#, #, #)', args);
}
}
return js.call('#.# = #', args);
}
/// Caches a constant (list/set/map or class instance) in a variable, so it's
/// only canonicalized once at this location in the code, which improves
/// performance.
///
/// This method ensures the constant is not initialized until use.
///
/// The expression [jsExpr] should contain the already-canonicalized constant.
/// If the constant is not canonicalized yet, it should be wrapped in the
/// appropriate call, such as:
///
/// - dart.constList (for Lists),
/// - dart.constMap (for Maps),
/// - dart.constSet (for Sets),
/// - dart.const (for other instances of classes)
///
/// [canonicalizeConstObject] can be used for class instances; it will wrap
/// the expression in `dart.const` and then call this method.
///
/// If the same consant is used elsewhere (in this module, or another module),
/// that will require a second canonicalization. In general it is uncommon
/// to define the same large constant (such as lists, maps) in different
/// locations, because that requires copy+paste, so in practice this
/// optimization is rather effective (we should consider caching once
/// per-module, though, as that would be relatively easy for the compiler to
/// implement once we have a single Kernel backend).
@protected
JS.Expression cacheConst(JS.Expression jsExpr) {
if (currentFunction == null) return jsExpr;
var temp = JS.TemporaryId('const');
moduleItems.add(js.statement('let #;', [temp]));
return js.call('# || (# = #)', [temp, temp, jsExpr]);
}
/// Emits a Dart Symbol with the given member [symbolName].
///
/// If the symbol refers to a private name, its library will be set to the
/// [currentLibrary], so the Symbol is scoped properly.
@protected
JS.Expression emitDartSymbol(String symbolName) {
// TODO(vsm): Handle qualified symbols correctly.
var last = symbolName.split('.').last;
var name = js.escapedString(symbolName, "'");
JS.Expression result;
if (last.startsWith('_')) {
var nativeSymbol = emitPrivateNameSymbol(currentLibrary, last);
result = js.call('new #.new(#, #)',
[emitConstructorAccess(privateSymbolType), name, nativeSymbol]);
} 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].
@protected
JS.Expression canonicalizeConstObject(JS.Expression expr) =>
cacheConst(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: [isBuildingSdk], [runtimeModule],
/// [extensionSymbolsModule], as well as the [_libraries] map needed by
/// [emitLibraryName].
@protected
List<JS.ModuleItem> startModule(Iterable<Library> libraries) {
isBuildingSdk = libraries.any(isSdkInternalRuntime);
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.Identifier('dart');
extensionSymbolsModule = JS.Identifier('dartx');
} else {
// Otherwise allow these to be renamed so users can write them.
runtimeModule = JS.TemporaryId('dart');
extensionSymbolsModule = JS.TemporaryId('dartx');
}
// Initialize our library variables.
var items = <JS.ModuleItem>[];
var exports = <JS.NameSpecifier>[];
if (isBuildingSdk) {
// Bootstrap the ability to create Dart library objects.
var libraryProto = JS.TemporaryId('_library');
items.add(js.statement('const # = Object.create(null)', libraryProto));
items.add(js.statement(
'const # = Object.create(#)', [runtimeModule, libraryProto]));
items.add(js.statement('#.library = #', [runtimeModule, libraryProto]));
exports.add(JS.NameSpecifier(runtimeModule));
}
for (var library in libraries) {
if (isBuildingSdk && isSdkInternalRuntime(library)) {
_libraries[library] = runtimeModule;
continue;
}
var id = JS.TemporaryId(jsLibraryName(library));
_libraries[library] = id;
items.add(js.statement(
'const # = Object.create(#.library)', [id, runtimeModule]));
exports.add(JS.NameSpecifier(id));
}
// dart:_runtime has a magic module that holds extension method symbols.
// TODO(jmesserly): find a cleaner design for this.
if (isBuildingSdk) {
var id = extensionSymbolsModule;
items.add(js.statement(
'const # = Object.create(#.library)', [id, runtimeModule]));
exports.add(JS.NameSpecifier(id));
}
items.add(JS.ExportDeclaration(JS.ExportClause(exports)));
if (isBuildingSdk) {
// Initialize the private name function.
// To bootstrap the SDK, this needs to be emitted before other code.
var symbol = JS.TemporaryId('_privateNames');
items.add(js.statement('const # = Symbol("_privateNames")', symbol));
items.add(js.statement(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;
}
''', [runtimeModule, symbol, symbol]));
}
return items;
}
/// Returns the canonical name to refer to the Dart library.
@protected
JS.Identifier emitLibraryName(Library library) {
// It's either one of the libraries in this module, or it's an import.
return _libraries[library] ??
_imports.putIfAbsent(
library, () => JS.TemporaryId(jsLibraryName(library)));
}
/// Emits imports and extension methods into [items].
@protected
void emitImportsAndExtensionSymbols(List<JS.ModuleItem> items) {
var modules = Map<String, List<Library>>();
for (var import in _imports.keys) {
modules.putIfAbsent(libraryToModule(import), () => []).add(import);
}
String coreModuleName;
if (!_libraries.containsKey(coreLibrary)) {
coreModuleName = libraryToModule(coreLibrary);
}
modules.forEach((module, libraries) {
// Generate import directives.
//
// Our import variables are temps and can get renamed. Since our renaming
// is integrated into js_ast, it is aware of this possibility and will
// generate an "as" if needed. For example:
//
// import {foo} from 'foo'; // if no rename needed
// import {foo as foo$} from 'foo'; // if rename was needed
//
var imports =
libraries.map((l) => JS.NameSpecifier(_imports[l])).toList();
if (module == coreModuleName) {
imports.add(JS.NameSpecifier(runtimeModule));
imports.add(JS.NameSpecifier(extensionSymbolsModule));
}
items.add(JS.ImportDeclaration(
namedImports: imports, from: js.string(module, "'")));
});
// Initialize extension symbols
_extensionSymbols.forEach((name, id) {
JS.Expression value =
JS.PropertyAccess(extensionSymbolsModule, propertyName(name));
if (isBuildingSdk) {
value = js.call('# = Symbol(#)', [value, js.string("dartx.$name")]);
}
items.add(js.statement('const # = #;', [id, value]));
});
}
void _emitDebuggerExtensionInfo(String name) {
var properties = <JS.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.
properties.add(
JS.Property(js.escapedString(jsLibraryDebuggerName(library)), value));
});
var module = JS.ObjectInitializer(properties, multiline: true);
// Track the module name for each library in the module.
// This data is only required for debugging.
moduleItems.add(js.statement(
'#.trackLibraries(#, #, $sourceMapLocationID);',
[runtimeModule, js.string(name), module]));
}
/// Finishes the module created by [startModule], by combining the preable
/// [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.
@protected
JS.Program finishModule(List<JS.ModuleItem> items, String moduleName) {
// 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);
// Add the module's code (produced by visiting compilation units, above)
_copyAndFlattenBlocks(items, moduleItems);
moduleItems.clear();
// Build the module.
return JS.Program(items, name: moduleName);
}
/// Flattens blocks in [items] to a single list.
///
/// This will not flatten blocks that are marked as being scopes.
void _copyAndFlattenBlocks(
List<JS.ModuleItem> result, Iterable<JS.ModuleItem> items) {
for (var item in items) {
if (item is JS.Block && !item.isScope) {
_copyAndFlattenBlocks(result, item.statements);
} else if (item != null) {
result.add(item);
}
}
}
/// 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.
@protected
JS.TemporaryId getExtensionSymbolInternal(String name) {
return _extensionSymbols.putIfAbsent(
name,
() => JS.TemporaryId(
'\$${JS.friendlyNameForDartOperator[name] ?? name}'));
}
/// 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.
@protected
JS.LiteralString propertyName(String name) => js.string(name, "'");
/// Unique identifier indicating the location to inline the source map.
///
/// 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.
static const String sourceMapLocationID =
'SourceMap3G5a8h6JVhHfdGuDxZr1EF9GQC8y0e6u';
}
/// Whether a variable with [name] is referenced in the [node].
bool variableIsReferenced(String name, JS.Node node) {
var finder = _IdentifierFinder.instance;
finder.nameToFind = name;
finder.found = false;
node.accept(finder);
return finder.found;
}
class _IdentifierFinder extends JS.BaseVisitor<void> {
String nameToFind;
bool found = false;
static final instance = _IdentifierFinder();
@override
visitIdentifier(node) {
if (node.name == nameToFind) found = true;
}
@override
visitNode(node) {
if (!found) super.visitNode(node);
}
}
class YieldFinder extends JS.BaseVisitor {
bool hasYield = false;
bool hasThis = false;
bool _nestedFunction = false;
@override
visitThis(JS.This node) {
hasThis = true;
}
@override
visitFunctionExpression(JS.FunctionExpression node) {
var savedNested = _nestedFunction;
_nestedFunction = true;
super.visitFunctionExpression(node);
_nestedFunction = savedNested;
}
@override
visitYield(JS.Yield node) {
if (!_nestedFunction) hasYield = true;
super.visitYield(node);
}
@override
visitNode(JS.Node node) {
if (hasYield && hasThis) return; // found both, nothing more to do.
super.visitNode(node);
}
}
/// Given the function [fn], returns a function declaration statement, binding
/// `this` and `super` if necessary (using an arrow function).
JS.Statement toBoundFunctionStatement(JS.Fun fn, JS.Identifier name) {
if (usesThisOrSuper(fn)) {
return js.statement('const # = (#) => {#}', [name, fn.params, fn.body]);
} else {
return JS.FunctionDeclaration(name, fn);
}
}
/// Returns whether [node] uses `this` or `super`.
bool usesThisOrSuper(JS.Expression node) {
var finder = _ThisOrSuperFinder.instance;
finder.found = false;
node.accept(finder);
return finder.found;
}
class _ThisOrSuperFinder extends JS.BaseVisitor<void> {
bool found = false;
static final instance = _ThisOrSuperFinder();
@override
visitThis(JS.This node) {
found = true;
}
@override
visitSuper(JS.Super node) {
found = true;
}
@override
visitNode(JS.Node node) {
if (!found) super.visitNode(node);
}
}