blob: a6255d337a9c965083148111cddf8117f57128e4 [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.
part of dart2js.js_emitter;
class InterceptorStubGenerator {
final Compiler compiler;
final Namer namer;
final JavaScriptBackend backend;
final ClosedWorld closedWorld;
InterceptorStubGenerator(
this.compiler, this.namer, this.backend, this.closedWorld);
Emitter get emitter => backend.emitter.emitter;
BackendHelpers get helpers => backend.helpers;
jsAst.Expression generateGetInterceptorMethod(Set<ClassElement> classes) {
jsAst.Expression interceptorFor(ClassElement cls) {
return backend.emitter.interceptorPrototypeAccess(cls);
}
/**
* Build a JavaScrit AST node for doing a type check on
* [cls]. [cls] must be a non-native interceptor class.
*/
jsAst.Statement buildInterceptorCheck(ClassElement cls) {
jsAst.Expression condition;
assert(backend.isInterceptorClass(cls));
if (cls == helpers.jsBoolClass) {
condition = js('(typeof receiver) == "boolean"');
} else if (cls == helpers.jsIntClass ||
cls == helpers.jsDoubleClass ||
cls == helpers.jsNumberClass) {
throw 'internal error';
} else if (cls == helpers.jsArrayClass ||
cls == helpers.jsMutableArrayClass ||
cls == helpers.jsFixedArrayClass ||
cls == helpers.jsExtendableArrayClass) {
condition = js('receiver.constructor == Array');
} else if (cls == helpers.jsStringClass) {
condition = js('(typeof receiver) == "string"');
} else if (cls == helpers.jsNullClass) {
condition = js('receiver == null');
} else {
throw 'internal error';
}
return js.statement('if (#) return #', [condition, interceptorFor(cls)]);
}
bool hasArray = false;
bool hasBool = false;
bool hasDouble = false;
bool hasInt = false;
bool hasNull = false;
bool hasNumber = false;
bool hasString = false;
bool hasNative = false;
bool anyNativeClasses =
compiler.enqueuer.codegen.nativeEnqueuer.hasInstantiatedNativeClasses;
for (ClassElement cls in classes) {
if (cls == helpers.jsArrayClass ||
cls == helpers.jsMutableArrayClass ||
cls == helpers.jsFixedArrayClass ||
cls == helpers.jsExtendableArrayClass)
hasArray = true;
else if (cls == helpers.jsBoolClass)
hasBool = true;
else if (cls == helpers.jsDoubleClass)
hasDouble = true;
else if (cls == helpers.jsIntClass)
hasInt = true;
else if (cls == helpers.jsNullClass)
hasNull = true;
else if (cls == helpers.jsNumberClass)
hasNumber = true;
else if (cls == helpers.jsStringClass)
hasString = true;
else {
// The set of classes includes classes mixed-in to interceptor classes
// and user extensions of native classes.
//
// The set of classes also includes the 'primitive' interceptor
// PlainJavaScriptObject even when it has not been resolved, since it is
// only resolved through the reference in getNativeInterceptor when
// getNativeInterceptor is marked as used. Guard against probing
// unresolved PlainJavaScriptObject by testing for anyNativeClasses.
if (anyNativeClasses) {
if (backend.isNativeOrExtendsNative(cls)) hasNative = true;
}
}
}
if (hasDouble) {
hasNumber = true;
}
if (hasInt) hasNumber = true;
if (classes.containsAll(backend.interceptedClasses)) {
// I.e. this is the general interceptor.
hasNative = anyNativeClasses;
}
List<jsAst.Statement> statements = <jsAst.Statement>[];
if (hasNumber) {
jsAst.Statement whenNumber;
/// Note: there are two number classes in play: Dart's [num],
/// and JavaScript's Number (typeof receiver == 'number'). This
/// is the fallback used when we have determined that receiver
/// is a JavaScript Number.
jsAst.Expression interceptorForNumber = interceptorFor(
hasDouble ? helpers.jsDoubleClass : helpers.jsNumberClass);
if (hasInt) {
whenNumber = js.statement(
'''{
if (Math.floor(receiver) == receiver) return #;
return #;
}''',
[interceptorFor(helpers.jsIntClass), interceptorForNumber]);
} else {
whenNumber = js.statement('return #', interceptorForNumber);
}
statements
.add(js.statement('if (typeof receiver == "number") #;', whenNumber));
}
if (hasString) {
statements.add(buildInterceptorCheck(helpers.jsStringClass));
}
if (hasNull) {
statements.add(buildInterceptorCheck(helpers.jsNullClass));
} else {
// Returning "undefined" or "null" here will provoke a JavaScript
// TypeError which is later identified as a null-error by
// [unwrapException] in js_helper.dart.
statements.add(js.statement('if (receiver == null) return receiver'));
}
if (hasBool) {
statements.add(buildInterceptorCheck(helpers.jsBoolClass));
}
// TODO(ahe): It might be faster to check for Array before
// function and bool.
if (hasArray) {
statements.add(buildInterceptorCheck(helpers.jsArrayClass));
}
if (hasNative) {
statements.add(js.statement(
r'''{
if (typeof receiver != "object") {
if (typeof receiver == "function" ) return #;
return receiver;
}
if (receiver instanceof #) return receiver;
return #(receiver);
}''',
[
interceptorFor(helpers.jsJavaScriptFunctionClass),
backend.emitter
.constructorAccess(compiler.commonElements.objectClass),
backend.emitter
.staticFunctionAccess(helpers.getNativeInterceptorMethod)
]));
} else {
ClassElement jsUnknown = helpers.jsUnknownJavaScriptObjectClass;
if (compiler.codegenWorld.directlyInstantiatedClasses
.contains(jsUnknown)) {
statements.add(js.statement('if (!(receiver instanceof #)) return #;', [
backend.emitter
.constructorAccess(compiler.commonElements.objectClass),
interceptorFor(jsUnknown)
]));
}
statements.add(js.statement('return receiver'));
}
return js('''function(receiver) { #; }''', new jsAst.Block(statements));
}
// Returns a statement that takes care of performance critical
// common case for a one-shot interceptor, or null if there is no
// fast path.
jsAst.Statement _fastPathForOneShotInterceptor(
Selector selector, Set<ClassElement> classes) {
if (selector.isOperator) {
String name = selector.name;
if (name == '==') {
return js.statement('''{
if (receiver == null) return a0 == null;
if (typeof receiver != "object")
return a0 != null && receiver === a0;
}''');
}
if (!classes.contains(helpers.jsIntClass) &&
!classes.contains(helpers.jsNumberClass) &&
!classes.contains(helpers.jsDoubleClass)) {
return null;
}
if (selector.argumentCount == 1) {
// The following operators do not map to a JavaScript operator.
if (name == '~/' || name == '<<' || name == '%' || name == '>>') {
return null;
}
jsAst.Expression result = js('receiver $name a0');
if (name == '&' || name == '|' || name == '^') {
result = js('# >>> 0', result);
}
return js.statement(
'if (typeof receiver == "number" && typeof a0 == "number")'
' return #;',
result);
} else if (name == 'unary-') {
return js
.statement('if (typeof receiver == "number") return -receiver');
} else {
assert(name == '~');
return js.statement('''
if (typeof receiver == "number" && Math.floor(receiver) == receiver)
return (~receiver) >>> 0;
''');
}
} else if (selector.isIndex || selector.isIndexSet) {
// For an index operation, this code generates:
//
// if (typeof a0 === "number") {
// if (receiver.constructor == Array ||
// typeof receiver == "string") {
// if (a0 >>> 0 === a0 && a0 < receiver.length) {
// return receiver[a0];
// }
// }
// }
//
// For an index set operation, this code generates:
//
// if (typeof a0 === "number") {
// if (receiver.constructor == Array && !receiver.immutable$list) {
// if (a0 >>> 0 === a0 && a0 < receiver.length) {
// return receiver[a0] = a1;
// }
// }
// }
bool containsArray = classes.contains(helpers.jsArrayClass);
bool containsString = classes.contains(helpers.jsStringClass);
bool containsJsIndexable =
helpers.jsIndexingBehaviorInterface.isResolved &&
classes.any((cls) {
return closedWorld.isSubtypeOf(
cls, helpers.jsIndexingBehaviorInterface);
});
// The index set operator requires a check on its set value in
// checked mode, so we don't optimize the interceptor if the
// compiler has type assertions enabled.
if (selector.isIndexSet &&
(compiler.options.enableTypeAssertions || !containsArray)) {
return null;
}
if (!containsArray && !containsString) {
return null;
}
jsAst.Expression arrayCheck = js('receiver.constructor == Array');
jsAst.Expression indexableCheck =
backend.generateIsJsIndexableCall(js('receiver'), js('receiver'));
jsAst.Expression orExp(left, right) {
return left == null ? right : js('# || #', [left, right]);
}
if (selector.isIndex) {
jsAst.Expression typeCheck;
if (containsArray) {
typeCheck = arrayCheck;
}
if (containsString) {
typeCheck = orExp(typeCheck, js('typeof receiver == "string"'));
}
if (containsJsIndexable) {
typeCheck = orExp(typeCheck, indexableCheck);
}
return js.statement(
'''
if (typeof a0 === "number")
if (#)
if ((a0 >>> 0) === a0 && a0 < receiver.length)
return receiver[a0];
''',
typeCheck);
} else {
jsAst.Expression typeCheck;
if (containsArray) {
typeCheck = arrayCheck;
}
if (containsJsIndexable) {
typeCheck = orExp(typeCheck, indexableCheck);
}
return js.statement(
r'''
if (typeof a0 === "number")
if (# && !receiver.immutable$list &&
(a0 >>> 0) === a0 && a0 < receiver.length)
return receiver[a0] = a1;
''',
typeCheck);
}
}
return null;
}
jsAst.Expression generateOneShotInterceptor(jsAst.Name name) {
Selector selector = backend.oneShotInterceptors[name];
Set<ClassElement> classes = backend.getInterceptedClassesOn(selector.name);
jsAst.Name getInterceptorName = namer.nameForGetInterceptor(classes);
List<String> parameterNames = <String>[];
parameterNames.add('receiver');
if (selector.isSetter) {
parameterNames.add('value');
} else {
for (int i = 0; i < selector.argumentCount; i++) {
parameterNames.add('a$i');
}
}
jsAst.Name invocationName = backend.namer.invocationName(selector);
String globalObject = namer.globalObjectFor(helpers.interceptorsLibrary);
jsAst.Statement optimizedPath =
_fastPathForOneShotInterceptor(selector, classes);
if (optimizedPath == null) optimizedPath = js.statement(';');
return js('function(#) { #; return #.#(receiver).#(#) }', [
parameterNames,
optimizedPath,
globalObject,
getInterceptorName,
invocationName,
parameterNames
]);
}
jsAst.ArrayInitializer generateTypeToInterceptorMap() {
// TODO(sra): Perhaps inject a constant instead?
CustomElementsAnalysis analysis = backend.customElementsAnalysis;
if (!analysis.needsTable) return null;
List<jsAst.Expression> elements = <jsAst.Expression>[];
JavaScriptConstantCompiler handler = backend.constants;
List<ConstantValue> constants =
handler.getConstantsForEmission(emitter.compareConstants);
for (ConstantValue constant in constants) {
if (constant is TypeConstantValue) {
TypeConstantValue typeConstant = constant;
Element element = typeConstant.representedType.element;
if (element is ClassElement) {
ClassElement classElement = element;
if (!analysis.needsClass(classElement)) continue;
elements.add(emitter.constantReference(constant));
elements.add(backend.emitter.interceptorClassAccess(classElement));
// Create JavaScript Object map for by-name lookup of generative
// constructors. For example, the class A has three generative
// constructors
//
// class A {
// A() {}
// A.foo() {}
// A.bar() {}
// }
//
// Which are described by the map
//
// {"": A.A$, "foo": A.A$foo, "bar": A.A$bar}
//
// We expect most of the time the map will be a singleton.
var properties = [];
for (Element member in analysis.constructors(classElement)) {
properties.add(new jsAst.Property(js.string(member.name),
backend.emitter.staticFunctionAccess(member)));
}
var map = new jsAst.ObjectInitializer(properties);
elements.add(map);
}
}
}
return new jsAst.ArrayInitializer(elements);
}
}