blob: f38ccc43f10663e59d0e43ec2f63206d2c9ead90 [file] [log] [blame]
// Copyright (c) 2014, 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' show Queue;
import '../common.dart';
import '../common/backend_api.dart' show ForeignResolver;
import '../common/registry.dart' show Registry;
import '../common/resolution.dart' show Resolution;
import '../compiler.dart' show Compiler;
import '../constants/values.dart';
import '../core_types.dart' show CoreTypes;
import '../dart_types.dart';
import '../elements/elements.dart';
import '../elements/modelx.dart' show FunctionElementX;
import '../enqueue.dart' show Enqueuer;
import '../js_backend/backend_helpers.dart' show BackendHelpers;
import '../js_backend/js_backend.dart';
import '../js_emitter/js_emitter.dart' show CodeEmitterTask, NativeEmitter;
import '../tokens/token.dart' show BeginGroupToken, Token;
import '../tokens/token_constants.dart' as Tokens show EOF_TOKEN;
import '../tree/tree.dart';
import 'behavior.dart';
/**
* This could be an abstract class but we use it as a stub for the dart_backend.
*/
class NativeEnqueuer {
/// Initial entry point to native enqueuer.
void processNativeClasses(Iterable<LibraryElement> libraries) {}
/// Registers the [nativeBehavior]. Adds the liveness of its instantiated
/// types to the world.
void registerNativeBehavior(NativeBehavior nativeBehavior, cause) {}
/// Notification of a main Enqueuer worklist element. For methods, adds
/// information from metadata attributes, and computes types instantiated due
/// to calling the method.
void registerElement(Element element) {}
/// Notification of native field. Adds information from metadata attributes.
void handleFieldAnnotations(Element field) {}
/// Computes types instantiated due to getting a native field.
void registerFieldLoad(Element field) {}
/// Computes types instantiated due to setting a native field.
void registerFieldStore(Element field) {}
/// Returns whether native classes are being used.
bool hasInstantiatedNativeClasses() => false;
/// Emits a summary information using the [log] function.
void logSummary(log(message)) {}
// Do not use annotations in dart2dart.
ClassElement get annotationCreatesClass => null;
ClassElement get annotationReturnsClass => null;
ClassElement get annotationJsNameClass => null;
}
abstract class NativeEnqueuerBase implements NativeEnqueuer {
static final RegExp _identifier = new RegExp(r'^[a-zA-Z_$][a-zA-Z0-9_$]*$');
/**
* The set of all native classes. Each native class is in [nativeClasses] and
* exactly one of [unusedClasses], [pendingClasses] and [registeredClasses].
*/
final Set<ClassElement> nativeClasses = new Set<ClassElement>();
final Set<ClassElement> registeredClasses = new Set<ClassElement>();
final Set<ClassElement> pendingClasses = new Set<ClassElement>();
final Set<ClassElement> unusedClasses = new Set<ClassElement>();
final Set<LibraryElement> processedLibraries;
bool hasInstantiatedNativeClasses() => !registeredClasses.isEmpty;
final Set<ClassElement> nativeClassesAndSubclasses = new Set<ClassElement>();
final Map<ClassElement, Set<ClassElement>> nonNativeSubclasses =
new Map<ClassElement, Set<ClassElement>>();
/**
* Records matched constraints ([SpecialType] or [DartType]). Once a type
* constraint has been matched, there is no need to match it again.
*/
final Set matchedTypeConstraints = new Set();
/// Pending actions. Classes in [pendingClasses] have action thunks in
/// [queue] to register the class.
final queue = new Queue();
bool flushing = false;
final Enqueuer world;
final Compiler compiler;
final bool enableLiveTypeAnalysis;
ClassElement _annotationCreatesClass;
ClassElement _annotationReturnsClass;
ClassElement _annotationJsNameClass;
/// Subclasses of [NativeEnqueuerBase] are constructed by the backend.
NativeEnqueuerBase(this.world, Compiler compiler, this.enableLiveTypeAnalysis)
: this.compiler = compiler,
processedLibraries = compiler.cacheStrategy.newSet();
JavaScriptBackend get backend => compiler.backend;
BackendHelpers get helpers => backend.helpers;
Resolution get resolution => compiler.resolution;
DiagnosticReporter get reporter => compiler.reporter;
CoreTypes get coreTypes => compiler.coreTypes;
void processNativeClasses(Iterable<LibraryElement> libraries) {
if (compiler.options.hasIncrementalSupport) {
// Since [Set.add] returns bool if an element was added, this restricts
// [libraries] to ones that haven't already been processed. This saves
// time during incremental compiles.
libraries = libraries.where(processedLibraries.add);
}
libraries.forEach(processNativeClassesInLibrary);
if (helpers.isolateHelperLibrary != null) {
processNativeClassesInLibrary(helpers.isolateHelperLibrary);
}
processSubclassesOfNativeClasses(libraries);
if (!enableLiveTypeAnalysis) {
nativeClasses.forEach((c) => enqueueClass(c, 'forced'));
flushQueue();
}
}
void processNativeClassesInLibrary(LibraryElement library) {
// Use implementation to ensure the inclusion of injected members.
library.implementation.forEachLocalMember((Element element) {
if (element.isClass && backend.isNative(element)) {
processNativeClass(element);
}
});
}
void processNativeClass(ClassElement classElement) {
nativeClasses.add(classElement);
unusedClasses.add(classElement);
// Resolve class to ensure the class has valid inheritance info.
classElement.ensureResolved(resolution);
}
void processSubclassesOfNativeClasses(Iterable<LibraryElement> libraries) {
// Collect potential subclasses, e.g.
//
// class B extends foo.A {}
//
// String "A" has a potential subclass B.
var potentialExtends = new Map<String, Set<ClassElement>>();
libraries.forEach((library) {
library.implementation.forEachLocalMember((element) {
if (element.isClass) {
String extendsName = findExtendsNameOfClass(element);
if (extendsName != null) {
Set<ClassElement> potentialSubclasses = potentialExtends
.putIfAbsent(extendsName, () => new Set<ClassElement>());
potentialSubclasses.add(element);
}
}
});
});
// Resolve all the native classes and any classes that might extend them in
// [potentialExtends], and then check that the properly resolved class is in
// fact a subclass of a native class.
ClassElement nativeSuperclassOf(ClassElement classElement) {
if (backend.isNative(classElement)) return classElement;
if (classElement.superclass == null) return null;
return nativeSuperclassOf(classElement.superclass);
}
void walkPotentialSubclasses(ClassElement element) {
if (nativeClassesAndSubclasses.contains(element)) return;
element.ensureResolved(resolution);
ClassElement nativeSuperclass = nativeSuperclassOf(element);
if (nativeSuperclass != null) {
nativeClassesAndSubclasses.add(element);
if (!backend.isNative(element)) {
nonNativeSubclasses
.putIfAbsent(nativeSuperclass, () => new Set<ClassElement>())
.add(element);
}
Set<ClassElement> potentialSubclasses = potentialExtends[element.name];
if (potentialSubclasses != null) {
potentialSubclasses.forEach(walkPotentialSubclasses);
}
}
}
nativeClasses.forEach(walkPotentialSubclasses);
nativeClasses.addAll(nativeClassesAndSubclasses);
unusedClasses.addAll(nativeClassesAndSubclasses);
}
/**
* Returns the source string of the class named in the extends clause, or
* `null` if there is no extends clause.
*/
String findExtendsNameOfClass(ClassElement classElement) {
if (classElement.isResolved) {
ClassElement superClass = classElement.superclass;
while (superClass != null) {
if (!superClass.isUnnamedMixinApplication) {
return superClass.name;
}
superClass = superClass.superclass;
}
return null;
}
// "class B extends A ... {}" --> "A"
// "class B extends foo.A ... {}" --> "A"
// "class B<T> extends foo.A<T,T> with M1, M2 ... {}" --> "A"
// We want to avoid calling classElement.parseNode on every class. Doing so
// will slightly increase parse time and size and cause compiler errors and
// warnings to me emitted in more unused code.
// An alternative to this code is to extend the API of ClassElement to
// expose the name of the extended element.
// Pattern match the above cases in the token stream.
// [abstract] class X extends [id.]* id
Token skipTypeParameters(Token token) {
BeginGroupToken beginGroupToken = token;
Token endToken = beginGroupToken.endGroup;
return endToken.next;
//for (;;) {
// token = token.next;
// if (token.stringValue == '>') return token.next;
// if (token.stringValue == '<') return skipTypeParameters(token);
//}
}
String scanForExtendsName(Token token) {
if (token.stringValue == 'abstract') token = token.next;
if (token.stringValue != 'class') return null;
token = token.next;
if (!token.isIdentifier()) return null;
token = token.next;
// class F<X extends B<X>> extends ...
if (token.stringValue == '<') {
token = skipTypeParameters(token);
}
if (token.stringValue != 'extends') return null;
token = token.next;
Token id = token;
while (token.kind != Tokens.EOF_TOKEN) {
token = token.next;
if (token.stringValue != '.') break;
token = token.next;
if (!token.isIdentifier()) return null;
id = token;
}
// Should be at '{', 'with', 'implements', '<' or 'native'.
return id.value;
}
return reporter.withCurrentElement(classElement, () {
return scanForExtendsName(classElement.position);
});
}
ClassElement get annotationCreatesClass {
findAnnotationClasses();
return _annotationCreatesClass;
}
ClassElement get annotationReturnsClass {
findAnnotationClasses();
return _annotationReturnsClass;
}
ClassElement get annotationJsNameClass {
findAnnotationClasses();
return _annotationJsNameClass;
}
void findAnnotationClasses() {
if (_annotationCreatesClass != null) return;
ClassElement find(name) {
Element e = helpers.findHelper(name);
if (e == null || e is! ClassElement) {
reporter.internalError(NO_LOCATION_SPANNABLE,
"Could not find implementation class '${name}'.");
}
return e;
}
_annotationCreatesClass = find('Creates');
_annotationReturnsClass = find('Returns');
_annotationJsNameClass = find('JSName');
}
/// Returns the JSName annotation string or `null` if no JSName annotation is
/// present.
String findJsNameFromAnnotation(Element element) {
String name = null;
ClassElement annotationClass = annotationJsNameClass;
for (MetadataAnnotation annotation in element.implementation.metadata) {
annotation.ensureResolved(resolution);
ConstantValue value =
compiler.constants.getConstantValue(annotation.constant);
if (!value.isConstructedObject) continue;
ConstructedConstantValue constructedObject = value;
if (constructedObject.type.element != annotationClass) continue;
Iterable<ConstantValue> fields = constructedObject.fields.values;
// TODO(sra): Better validation of the constant.
if (fields.length != 1 || fields.single is! StringConstantValue) {
reporter.internalError(
annotation, 'Annotations needs one string: ${annotation.node}');
}
StringConstantValue specStringConstant = fields.single;
String specString = specStringConstant.toDartString().slowToString();
if (name == null) {
name = specString;
} else {
reporter.internalError(
annotation, 'Too many JSName annotations: ${annotation.node}');
}
}
return name;
}
enqueueClass(ClassElement classElement, cause) {
assert(unusedClasses.contains(classElement));
unusedClasses.remove(classElement);
pendingClasses.add(classElement);
queue.add(() {
processClass(classElement, cause);
});
}
void flushQueue() {
if (flushing) return;
flushing = true;
while (!queue.isEmpty) {
(queue.removeFirst())();
}
flushing = false;
}
processClass(ClassElement classElement, cause) {
// TODO(ahe): Fix this assertion to work in incremental compilation.
assert(compiler.options.hasIncrementalSupport ||
!registeredClasses.contains(classElement));
bool firstTime = registeredClasses.isEmpty;
pendingClasses.remove(classElement);
registeredClasses.add(classElement);
// TODO(ahe): Is this really a global dependency?
classElement.ensureResolved(resolution);
compiler.backend.registerInstantiatedType(
classElement.rawType, world, compiler.globalDependencies);
if (firstTime) {
queue.add(onFirstNativeClass);
}
}
registerElement(Element element) {
reporter.withCurrentElement(element, () {
if (element.isFunction ||
element.isFactoryConstructor ||
element.isGetter ||
element.isSetter) {
handleMethodAnnotations(element);
if (backend.isNative(element)) {
registerMethodUsed(element);
}
} else if (element.isField) {
handleFieldAnnotations(element);
if (backend.isNative(element)) {
registerFieldLoad(element);
registerFieldStore(element);
}
}
});
}
void handleFieldAnnotations(Element element) {
if (compiler.serialization.isDeserialized(element)) {
return;
}
if (backend.isNative(element.enclosingElement)) {
// Exclude non-instance (static) fields - they not really native and are
// compiled as isolate globals. Access of a property of a constructor
// function or a non-method property in the prototype chain, must be coded
// using a JS-call.
if (element.isInstanceMember) {
_setNativeName(element);
}
}
}
void handleMethodAnnotations(Element method) {
if (compiler.serialization.isDeserialized(method)) {
return;
}
if (isNativeMethod(method)) {
if (method.isStatic) {
_setNativeNameForStaticMethod(method);
} else {
_setNativeName(method);
}
}
}
/// Sets the native name of [element], either from an annotation, or
/// defaulting to the Dart name.
void _setNativeName(MemberElement element) {
String name = findJsNameFromAnnotation(element);
if (name == null) name = element.name;
backend.nativeData.setNativeMemberName(element, name);
}
/// Sets the native name of the static native method [element], using the
/// following rules:
/// 1. If [element] has a @JSName annotation that is an identifier, qualify
/// that identifier to the @Native name of the enclosing class
/// 2. If [element] has a @JSName annotation that is not an identifier,
/// use the declared @JSName as the expression
/// 3. If [element] does not have a @JSName annotation, qualify the name of
/// the method with the @Native name of the enclosing class.
void _setNativeNameForStaticMethod(MethodElement element) {
String name = findJsNameFromAnnotation(element);
if (name == null) name = element.name;
if (isIdentifier(name)) {
List<String> nativeNames =
backend.nativeData.getNativeTagsOfClassRaw(element.enclosingClass);
if (nativeNames.length != 1) {
reporter.internalError(
element,
'Unable to determine a native name for the enclosing class, '
'options: $nativeNames');
}
backend.nativeData
.setNativeMemberName(element, '${nativeNames[0]}.$name');
} else {
backend.nativeData.setNativeMemberName(element, name);
}
}
bool isIdentifier(String s) => _identifier.hasMatch(s);
bool isNativeMethod(FunctionElementX element) {
if (!backend.canLibraryUseNative(element.library)) return false;
// Native method?
return reporter.withCurrentElement(element, () {
Node node = element.parseNode(resolution.parsingContext);
if (node is! FunctionExpression) return false;
FunctionExpression functionExpression = node;
node = functionExpression.body;
Token token = node.getBeginToken();
if (identical(token.stringValue, 'native')) return true;
return false;
});
}
void registerNativeBehavior(NativeBehavior nativeBehavior, cause) {
processNativeBehavior(nativeBehavior, cause);
flushQueue();
}
void registerMethodUsed(Element method) {
registerNativeBehavior(NativeBehavior.ofMethod(method, compiler), method);
}
void registerFieldLoad(Element field) {
registerNativeBehavior(NativeBehavior.ofFieldLoad(field, compiler), field);
}
void registerFieldStore(Element field) {
registerNativeBehavior(NativeBehavior.ofFieldStore(field, compiler), field);
}
processNativeBehavior(NativeBehavior behavior, cause) {
// TODO(ahe): Is this really a global dependency?
Registry registry = compiler.globalDependencies;
bool allUsedBefore = unusedClasses.isEmpty;
for (var type in behavior.typesInstantiated) {
if (matchedTypeConstraints.contains(type)) continue;
matchedTypeConstraints.add(type);
if (type is SpecialType) {
if (type == SpecialType.JsObject) {
backend.registerInstantiatedType(
compiler.coreTypes.objectType, world, registry);
}
continue;
}
if (type is InterfaceType) {
if (type == coreTypes.intType) {
backend.registerInstantiatedType(type, world, registry);
} else if (type == coreTypes.doubleType) {
backend.registerInstantiatedType(type, world, registry);
} else if (type == coreTypes.numType) {
backend.registerInstantiatedType(
coreTypes.doubleType, world, registry);
backend.registerInstantiatedType(coreTypes.intType, world, registry);
} else if (type == coreTypes.stringType) {
backend.registerInstantiatedType(type, world, registry);
} else if (type == coreTypes.nullType) {
backend.registerInstantiatedType(type, world, registry);
} else if (type == coreTypes.boolType) {
backend.registerInstantiatedType(type, world, registry);
} else if (compiler.types
.isSubtype(type, backend.listImplementation.rawType)) {
backend.registerInstantiatedType(type, world, registry);
}
// TODO(johnniwinther): Improve spec string precision to handle type
// arguments and implements relations that preserve generics. Currently
// we cannot distinguish between `List`, `List<dynamic>`, and
// `List<int>` and take all to mean `List<E>`; in effect not including
// any native subclasses of generic classes.
// TODO(johnniwinther,sra): Find and replace uses of `List` with the
// actual implementation classes such as `JSArray` et al.
enqueueUnusedClassesMatching((ClassElement nativeClass) {
InterfaceType nativeType = nativeClass.thisType;
InterfaceType specType = type.element.thisType;
return compiler.types.isSubtype(nativeType, specType);
}, cause, 'subtypeof($type)');
} else if (type.isDynamic) {
enqueueUnusedClassesMatching((_) => true, cause, 'subtypeof($type)');
} else {
assert(type is VoidType);
}
}
// Give an info so that library developers can compile with -v to find why
// all the native classes are included.
if (unusedClasses.isEmpty && !allUsedBefore) {
reporter.log('All native types marked as used due to $cause.');
}
}
enqueueUnusedClassesMatching(bool predicate(classElement), cause,
[String reason]) {
Iterable matches = unusedClasses.where(predicate);
matches.toList().forEach((c) => enqueueClass(c, cause));
}
onFirstNativeClass() {
staticUse(name) {
backend.enqueue(
world, helpers.findHelper(name), compiler.globalDependencies);
}
staticUse('defineProperty');
staticUse('toStringForNativeObject');
staticUse('hashCodeForNativeObject');
staticUse('convertDartClosureToJS');
addNativeExceptions();
}
addNativeExceptions() {
enqueueUnusedClassesMatching((classElement) {
// TODO(sra): Annotate exception classes in dart:html.
String name = classElement.name;
if (name.contains('Exception')) return true;
if (name.contains('Error')) return true;
return false;
}, 'native exception');
}
}
class NativeResolutionEnqueuer extends NativeEnqueuerBase {
Map<String, ClassElement> tagOwner = new Map<String, ClassElement>();
NativeResolutionEnqueuer(Enqueuer world, Compiler compiler)
: super(world, compiler, compiler.options.enableNativeLiveTypeAnalysis);
void processNativeClass(ClassElement classElement) {
super.processNativeClass(classElement);
// Js Interop interfaces do not have tags.
if (backend.isJsInterop(classElement)) return;
// Since we map from dispatch tags to classes, a dispatch tag must be used
// on only one native class.
for (String tag in backend.nativeData.getNativeTagsOfClass(classElement)) {
ClassElement owner = tagOwner[tag];
if (owner != null) {
if (owner != classElement) {
reporter.internalError(
classElement, "Tag '$tag' already in use by '${owner.name}'");
}
} else {
tagOwner[tag] = classElement;
}
}
}
void logSummary(log(message)) {
log('Resolved ${registeredClasses.length} native elements used, '
'${unusedClasses.length} native elements dead.');
}
/**
* Handles JS-calls, which can be an instantiation point for types.
*
* For example, the following code instantiates and returns native classes
* that are `_DOMWindowImpl` or a subtype.
*
* JS('_DOMWindowImpl', 'window')
*
*/
NativeBehavior resolveJsCall(Send node, ForeignResolver resolver) {
return NativeBehavior.ofJsCall(
node, reporter, compiler.parsingContext, compiler.coreTypes, resolver);
}
/**
* Handles JS-embedded global calls, which can be an instantiation point for
* types.
*
* For example, the following code instantiates and returns a String class
*
* JS_EMBEDDED_GLOBAL('String', 'foo')
*
*/
NativeBehavior resolveJsEmbeddedGlobalCall(
Send node, ForeignResolver resolver) {
return NativeBehavior.ofJsEmbeddedGlobalCall(
node, reporter, compiler.parsingContext, compiler.coreTypes, resolver);
}
/**
* Handles JS-compiler builtin calls, which can be an instantiation point for
* types.
*
* For example, the following code instantiates and returns a String class
*
* JS_BUILTIN('String', 'int2string', 0)
*
*/
NativeBehavior resolveJsBuiltinCall(Send node, ForeignResolver resolver) {
return NativeBehavior.ofJsBuiltinCall(
node, reporter, compiler.parsingContext, compiler.coreTypes, resolver);
}
}
class NativeCodegenEnqueuer extends NativeEnqueuerBase {
final CodeEmitterTask emitter;
final Set<ClassElement> doneAddSubtypes = new Set<ClassElement>();
NativeCodegenEnqueuer(Enqueuer world, Compiler compiler, this.emitter)
: super(world, compiler, compiler.options.enableNativeLiveTypeAnalysis);
void processNativeClasses(Iterable<LibraryElement> libraries) {
super.processNativeClasses(libraries);
// HACK HACK - add all the resolved classes.
NativeEnqueuerBase enqueuer = compiler.enqueuer.resolution.nativeEnqueuer;
for (final classElement in enqueuer.registeredClasses) {
if (unusedClasses.contains(classElement)) {
enqueueClass(classElement, 'was resolved');
}
}
flushQueue();
}
processClass(ClassElement classElement, cause) {
super.processClass(classElement, cause);
// Add the information that this class is a subtype of its supertypes. The
// code emitter and the ssa builder use that information.
addSubtypes(classElement, emitter.nativeEmitter);
}
void addSubtypes(ClassElement cls, NativeEmitter emitter) {
if (!backend.isNative(cls)) return;
if (doneAddSubtypes.contains(cls)) return;
doneAddSubtypes.add(cls);
// Walk the superclass chain since classes on the superclass chain might not
// be instantiated (abstract or simply unused).
addSubtypes(cls.superclass, emitter);
for (DartType type in cls.allSupertypes) {
List<Element> subtypes =
emitter.subtypes.putIfAbsent(type.element, () => <ClassElement>[]);
subtypes.add(cls);
}
// Skip through all the mixin applications in the super class
// chain. That way, the direct subtypes set only contain the
// natives classes.
ClassElement superclass = cls.superclass;
while (superclass != null && superclass.isMixinApplication) {
assert(!backend.isNative(superclass));
superclass = superclass.superclass;
}
List<Element> directSubtypes =
emitter.directSubtypes.putIfAbsent(superclass, () => <ClassElement>[]);
directSubtypes.add(cls);
}
void logSummary(log(message)) {
log('Compiled ${registeredClasses.length} native classes, '
'${unusedClasses.length} native classes omitted.');
}
}