blob: 7b92fcd3607e3f016f67936970dbe20fc6401ec6 [file] [log] [blame]
// Copyright (c) 2012, 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 js_backend;
class NativeEmitter {
CodeEmitterTask emitter;
CodeBuffer nativeBuffer;
// Classes that participate in dynamic dispatch. These are the
// classes that contain used members.
Set<ClassElement> classesWithDynamicDispatch;
// Native classes found in the application.
Set<ClassElement> nativeClasses;
// Caches the native subtypes of a native class.
Map<ClassElement, List<ClassElement>> subtypes;
// Caches the direct native subtypes of a native class.
Map<ClassElement, List<ClassElement>> directSubtypes;
// Caches the native methods that are overridden by a native class.
// Note that the method that overrides does not have to be native:
// it's the overridden method that must make sure it will dispatch
// to its subclass if it sees an instance whose class is a subclass.
Set<FunctionElement> overriddenMethods;
// Caches the methods that have a native body.
Set<FunctionElement> nativeMethods;
// Caches the methods that redirect to a JS method.
Map<FunctionElement, String> redirectingMethods;
// Do we need the native emitter to take care of handling
// noSuchMethod for us? This flag is set to true in the emitter if
// it finds any native class that needs noSuchMethod handling.
bool handleNoSuchMethod = false;
NativeEmitter(this.emitter)
: classesWithDynamicDispatch = new Set<ClassElement>(),
nativeClasses = new Set<ClassElement>(),
subtypes = new Map<ClassElement, List<ClassElement>>(),
directSubtypes = new Map<ClassElement, List<ClassElement>>(),
overriddenMethods = new Set<FunctionElement>(),
nativeMethods = new Set<FunctionElement>(),
redirectingMethods = new Map<FunctionElement, String>(),
nativeBuffer = new CodeBuffer();
Compiler get compiler => emitter.compiler;
JavaScriptBackend get backend => compiler.backend;
void addRedirectingMethod(FunctionElement element, String name) {
redirectingMethods[element] = name;
}
String get dynamicName {
Element element = compiler.findHelper(
const SourceString('dynamicFunction'));
return backend.namer.isolateAccess(element);
}
String get dynamicSetMetadataName {
Element element = compiler.findHelper(
const SourceString('dynamicSetMetadata'));
return backend.namer.isolateAccess(element);
}
String get typeNameOfName {
Element element = compiler.findHelper(
const SourceString('getTypeNameOf'));
return backend.namer.isolateAccess(element);
}
String get defPropName {
Element element = compiler.findHelper(
const SourceString('defineProperty'));
return backend.namer.isolateAccess(element);
}
String get toStringHelperName {
Element element = compiler.findHelper(
const SourceString('toStringForNativeObject'));
return backend.namer.isolateAccess(element);
}
String get hashCodeHelperName {
Element element = compiler.findHelper(
const SourceString('hashCodeForNativeObject'));
return backend.namer.isolateAccess(element);
}
String get defineNativeClassName
=> '${backend.namer.CURRENT_ISOLATE}.\$defineNativeClass';
String get defineNativeClassFunction {
return """
function(cls, desc) {
var fields = desc[''] || [];
var generateGetterSetter = ${emitter.generateGetterSetterFunction};
for (var i = 0; i < fields.length; i++) {
generateGetterSetter(fields[i], desc);
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
for (var method in desc) {
if (method !== '') {
if (hasOwnProperty.call(desc, method)) {
$dynamicName(method)[cls] = desc[method];
}
}
}
}""";
}
void generateNativeLiteral(ClassElement classElement) {
String quotedNative = classElement.nativeName.slowToString();
String nativeCode = quotedNative.substring(2, quotedNative.length - 1);
String className = backend.namer.getName(classElement);
nativeBuffer.add(className);
nativeBuffer.add(' = ');
nativeBuffer.add(nativeCode);
nativeBuffer.add(';\n');
void defineInstanceMember(String name, CodeBuffer value) {
nativeBuffer.add("$className.$name = $value;\n");
}
classElement.implementation.forEachMember((_, Element member) {
if (member.isInstanceMember()) {
emitter.addInstanceMember(member, defineInstanceMember);
}
});
}
bool isNativeLiteral(String quotedName) {
return identical(quotedName[1], '=');
}
bool isNativeGlobal(String quotedName) {
return identical(quotedName[1], '@');
}
String toNativeName(ClassElement cls) {
String quotedName = cls.nativeName.slowToString();
if (isNativeGlobal(quotedName)) {
// Global object, just be like the other types for now.
return quotedName.substring(3, quotedName.length - 1);
} else {
return quotedName.substring(2, quotedName.length - 1);
}
}
void generateNativeClass(ClassElement classElement) {
nativeClasses.add(classElement);
assert(classElement.backendMembers.isEmpty);
String quotedName = classElement.nativeName.slowToString();
if (isNativeLiteral(quotedName)) {
generateNativeLiteral(classElement);
// The native literal kind needs to be dealt with specially when
// generating code for it.
return;
}
CodeBuffer fieldBuffer = new CodeBuffer();
CodeBuffer getterSetterBuffer = new CodeBuffer();
emitter.emitClassFields(classElement, fieldBuffer);
emitter.emitClassGettersSetters(classElement, getterSetterBuffer,
omitLeadingComma: true);
CodeBuffer methodBuffer = new CodeBuffer();
emitter.emitInstanceMembers(classElement, methodBuffer, false);
if (methodBuffer.isEmpty
&& fieldBuffer.isEmpty
&& getterSetterBuffer.isEmpty) {
return;
}
String nativeName = toNativeName(classElement);
nativeBuffer.add("$defineNativeClassName('$nativeName', ");
nativeBuffer.add('{');
bool firstInMap = true;
if (!fieldBuffer.isEmpty) {
firstInMap = false;
nativeBuffer.add(fieldBuffer);
}
if (!getterSetterBuffer.isEmpty) {
if (!firstInMap) nativeBuffer.add(",");
firstInMap = false;
nativeBuffer.add("\n ");
nativeBuffer.add(getterSetterBuffer);
}
if (!methodBuffer.isEmpty) {
if (!firstInMap) nativeBuffer.add(",");
nativeBuffer.add(methodBuffer);
}
nativeBuffer.add('\n});\n\n');
classesWithDynamicDispatch.add(classElement);
}
List<ClassElement> getDirectSubclasses(ClassElement cls) {
List<ClassElement> result = directSubtypes[cls];
return result == null ? const<ClassElement>[] : result;
}
void potentiallyConvertDartClosuresToJs(CodeBuffer code,
FunctionElement member,
List<String> argumentsBuffer) {
FunctionSignature parameters = member.computeSignature(compiler);
Element converter =
compiler.findHelper(const SourceString('convertDartClosureToJS'));
String closureConverter = backend.namer.isolateAccess(converter);
parameters.forEachParameter((Element parameter) {
String name = parameter.name.slowToString();
// If [name] is not in [argumentsBuffer], then the parameter is
// an optional parameter that was not provided for that stub.
if (argumentsBuffer.indexOf(name) == -1) return;
DartType type = parameter.computeType(compiler).unalias(compiler);
if (type is FunctionType) {
// The parameter type is a function type either directly or through
// typedef(s).
int arity = type.computeArity();
code.add(' $name = $closureConverter($name, $arity);\n');
}
});
}
String generateParameterStub(Element member,
String invocationName,
String stubParameters,
List<String> argumentsBuffer,
int indexOfLastOptionalArgumentInParameters,
CodeBuffer buffer) {
// The target JS function may check arguments.length so we need to
// make sure not to pass any unspecified optional arguments to it.
// For example, for the following Dart method:
// foo([x, y, z]);
// The call:
// foo(y: 1)
// must be turned into a JS call to:
// foo(null, y).
List<String> nativeArgumentsBuffer = argumentsBuffer.getRange(
0, indexOfLastOptionalArgumentInParameters + 1);
ClassElement classElement = member.enclosingElement;
String nativeName = classElement.nativeName.slowToString();
String nativeArguments = Strings.join(nativeArgumentsBuffer, ",");
CodeBuffer code = new CodeBuffer();
potentiallyConvertDartClosuresToJs(code, member, argumentsBuffer);
if (!nativeMethods.contains(member)) {
// When calling a method that has a native body, we call it
// with our calling conventions.
String arguments = Strings.join(argumentsBuffer, ",");
code.add(' return this.${backend.namer.getName(member)}($arguments)');
} else {
// When calling a JS method, we call it with the native name.
String name = redirectingMethods[member];
if (name == null) name = member.name.slowToString();
code.add(' return this.$name($nativeArguments);');
}
if (isNativeLiteral(nativeName) || !overriddenMethods.contains(member)) {
// Call the method directly.
buffer.add(code.toString());
} else {
native.generateMethodWithPrototypeCheck(
compiler, buffer, invocationName, code.toString(), stubParameters);
}
}
void emitDynamicDispatchMetadata() {
if (classesWithDynamicDispatch.isEmpty) return;
int length = classesWithDynamicDispatch.length;
nativeBuffer.add('// $length dynamic classes.\n');
// Build a pre-order traversal over all the classes and their subclasses.
Set<ClassElement> seen = new Set<ClassElement>();
List<ClassElement> classes = <ClassElement>[];
void visit(ClassElement cls) {
if (seen.contains(cls)) return;
seen.add(cls);
for (final ClassElement subclass in getDirectSubclasses(cls)) {
visit(subclass);
}
classes.add(cls);
}
for (final ClassElement classElement in classesWithDynamicDispatch) {
visit(classElement);
}
Collection<ClassElement> dispatchClasses = classes.filter(
(cls) => !getDirectSubclasses(cls).isEmpty &&
classesWithDynamicDispatch.contains(cls));
nativeBuffer.add('// ${classes.length} classes\n');
Collection<ClassElement> classesThatHaveSubclasses = classes.filter(
(ClassElement t) => !getDirectSubclasses(t).isEmpty);
nativeBuffer.add('// ${classesThatHaveSubclasses.length} !leaf\n');
// Generate code that builds the map from cls tags used in dynamic dispatch
// to the set of cls tags of classes that extend (TODO: or implement) those
// classes. The set is represented as a string of tags joined with '|'.
// This is easily split into an array of tags, or converted into a regexp.
//
// To reduce the size of the sets, subsets are CSE-ed out into variables.
// The sets could be much smaller if we could make assumptions about the
// cls tags of other classes (which are constructor names or part of the
// result of Object.protocls.toString). For example, if objects that are
// Dart objects could be easily excluded, then we might be able to simplify
// the test, replacing dozens of HTMLxxxElement classes with the regexp
// /HTML.*Element/.
// Temporary variables for common substrings.
List<String> varNames = <String>[];
// var -> expression
Map<String, String> varDefns = <String, String>{};
// tag -> expression (a string or a variable)
Map<ClassElement, String> tagDefns = new Map<ClassElement, String>();
String makeExpression(ClassElement classElement) {
// Expression fragments for this set of cls keys.
List<String> expressions = <String>[];
// TODO: Remove if cls is abstract.
List<String> subtags = [toNativeName(classElement)];
void walk(ClassElement cls) {
for (final ClassElement subclass in getDirectSubclasses(cls)) {
ClassElement tag = subclass;
String existing = tagDefns[tag];
if (existing == null) {
subtags.add(toNativeName(tag));
walk(subclass);
} else {
if (varDefns.containsKey(existing)) {
expressions.add(existing);
} else {
String varName = 'v${varNames.length}/*${tag}*/';
varNames.add(varName);
varDefns[varName] = existing;
tagDefns[tag] = varName;
expressions.add(varName);
}
}
}
}
walk(classElement);
String constantPart = "'${Strings.join(subtags, '|')}'";
if (constantPart != "''") expressions.add(constantPart);
String expression;
if (expressions.length == 1) {
expression = expressions[0];
} else {
expression = "[${Strings.join(expressions, ',')}].join('|')";
}
return expression;
}
for (final ClassElement classElement in dispatchClasses) {
tagDefns[classElement] = makeExpression(classElement);
}
// Write out a thunk that builds the metadata.
if (!tagDefns.isEmpty) {
nativeBuffer.add('(function(){\n');
for (final String varName in varNames) {
nativeBuffer.add(' var ${varName} = ${varDefns[varName]};\n');
}
nativeBuffer.add(' var table = [\n');
nativeBuffer.add(
' // [dynamic-dispatch-tag, '
'tags of classes implementing dynamic-dispatch-tag]');
bool needsComma = false;
List<String> entries = <String>[];
for (final ClassElement cls in dispatchClasses) {
String clsName = toNativeName(cls);
entries.add("\n ['$clsName', ${tagDefns[cls]}]");
}
nativeBuffer.add(Strings.join(entries, ','));
nativeBuffer.add('];\n');
nativeBuffer.add('$dynamicSetMetadataName(table);\n');
nativeBuffer.add('})();\n');
}
}
bool isSupertypeOfNativeClass(Element element) {
if (element.isTypeVariable()) {
compiler.cancel("Is check for type variable", element: element);
return false;
}
if (element.computeType(compiler).unalias(compiler) is FunctionType) {
// The element type is a function type either directly or through
// typedef(s).
return false;
}
if (!element.isClass()) {
compiler.cancel("Is check does not handle element", element: element);
return false;
}
return subtypes[element] != null;
}
bool requiresNativeIsCheck(Element element) {
if (!element.isClass()) return false;
ClassElement cls = element;
if (cls.isNative()) return true;
return isSupertypeOfNativeClass(element);
}
void emitIsChecks(Map<String, String> objectProperties) {
for (Element element in emitter.checkedClasses) {
if (!requiresNativeIsCheck(element)) continue;
if (element.isObject(compiler)) continue;
String name = backend.namer.operatorIs(element);
objectProperties[name] = 'function() { return false; }';
}
}
void assembleCode(CodeBuffer targetBuffer) {
if (nativeClasses.isEmpty) return;
emitDynamicDispatchMetadata();
targetBuffer.add('$defineNativeClassName = '
'$defineNativeClassFunction;\n\n');
// Because of native classes, we have to generate some is checks
// by calling a method, instead of accessing a property. So we
// attach to the JS Object prototype these methods that return
// false, and will be overridden by subclasses when they have to
// return true.
Map<String, String> objectProperties = new Map<String, String>();
emitIsChecks(objectProperties);
// In order to have the toString method on every native class,
// we must patch the JS Object prototype with a helper method.
String toStringName = backend.namer.publicInstanceMethodNameByArity(
const SourceString('toString'), 0);
objectProperties[toStringName] =
'function() { return $toStringHelperName(this); }';
// Same as above, but for hashCode.
String hashCodeName = backend.namer.publicGetterName(
const SourceString('hashCode'));
objectProperties[hashCodeName] =
'function() { return $hashCodeHelperName(this); }';
// If the native emitter has been asked to take care of the
// noSuchMethod handlers, we do that now.
if (handleNoSuchMethod) {
emitter.emitNoSuchMethodHandlers((String name, CodeBuffer buffer) {
objectProperties[name] = buffer.toString();
});
}
// If we have any properties to add to Object.prototype, we run
// through them and add them using defineProperty.
if (!objectProperties.isEmpty) {
targetBuffer.add("(function(table) {\n"
" for (var key in table) {\n"
" $defPropName(Object.prototype, key, table[key]);\n"
" }\n"
"})({\n");
bool first = true;
objectProperties.forEach((String name, String function) {
if (!first) targetBuffer.add(",\n");
targetBuffer.add(" $name: $function");
first = false;
});
targetBuffer.add("\n});\n\n");
}
targetBuffer.add(nativeBuffer);
targetBuffer.add('\n');
}
}