| // 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; |
| |
| // 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>(), |
| nativeBuffer = new CodeBuffer(); |
| |
| Compiler get compiler => emitter.compiler; |
| JavaScriptBackend get backend => compiler.backend; |
| |
| String get _ => emitter._; |
| String get n => emitter.n; |
| String get N => emitter.N; |
| |
| 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 fields_array = fields ? fields.split(',') : []; |
| for (var i = 0; i < fields_array.length; i++) { |
| ${emitter.currentGenerateAccessorName}(fields_array[i], desc); |
| } |
| var hasOwnProperty = Object.prototype.hasOwnProperty; |
| for (var method in desc) { |
| if (method) { """/* Short version of: if (method != '') */""" |
| if (hasOwnProperty.call(desc, method)) { |
| $dynamicName(method)[cls] = desc[method]; |
| } |
| } |
| } |
| }"""; |
| } |
| |
| void generateNativeLiteral(ClassElement classElement) { |
| String quotedNative = classElement.nativeTagInfo.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 toNativeTag(ClassElement cls) { |
| String quotedName = cls.nativeTagInfo.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.nativeTagInfo.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(); |
| CodeBuffer methodBuffer = new CodeBuffer(); |
| |
| emitter.emitClassFields(classElement, fieldBuffer, false, |
| classIsNative: true); |
| emitter.emitClassGettersSetters(classElement, getterSetterBuffer, false); |
| emitter.emitInstanceMembers(classElement, methodBuffer, false); |
| |
| if (methodBuffer.isEmpty |
| && fieldBuffer.isEmpty |
| && getterSetterBuffer.isEmpty) { |
| return; |
| } |
| |
| String nativeTag = toNativeTag(classElement); |
| nativeBuffer.add("$defineNativeClassName('$nativeTag',$_"); |
| 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(List<js.Statement> statements, |
| FunctionElement member, |
| List<js.Parameter> stubParameters) { |
| FunctionSignature parameters = member.computeSignature(compiler); |
| Element converter = |
| compiler.findHelper(const SourceString('convertDartClosureToJS')); |
| String closureConverter = backend.namer.isolateAccess(converter); |
| Set<String> stubParameterNames = new Set<String>.from( |
| stubParameters.map((param) => param.name)); |
| parameters.forEachParameter((Element parameter) { |
| String name = parameter.name.slowToString(); |
| // If [name] is not in [stubParameters], then the parameter is an optional |
| // parameter that was not provided for this stub. |
| for (js.Parameter stubParameter in stubParameters) { |
| if (stubParameter.name == name) { |
| 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(); |
| |
| statements.add( |
| new js.ExpressionStatement( |
| new js.Assignment( |
| new js.VariableUse(name), |
| new js.VariableUse(closureConverter) |
| .callWith([new js.VariableUse(name), |
| new js.LiteralNumber('$arity')])))); |
| break; |
| } |
| } |
| } |
| }); |
| } |
| |
| List<js.Statement> generateParameterStubStatements( |
| Element member, |
| String invocationName, |
| List<js.Parameter> stubParameters, |
| List<js.Expression> argumentsBuffer, |
| int indexOfLastOptionalArgumentInParameters) { |
| // 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). |
| |
| ClassElement classElement = member.enclosingElement; |
| //String nativeTagInfo = classElement.nativeName.slowToString(); |
| String nativeTagInfo = classElement.nativeTagInfo.slowToString(); |
| |
| List<js.Statement> statements = <js.Statement>[]; |
| potentiallyConvertDartClosuresToJs(statements, member, stubParameters); |
| |
| String target; |
| List<js.Expression> arguments; |
| |
| if (!nativeMethods.contains(member)) { |
| // When calling a method that has a native body, we call it with our |
| // calling conventions. |
| target = backend.namer.getName(member); |
| arguments = argumentsBuffer; |
| } else { |
| // When calling a JS method, we call it with the native name, and only the |
| // arguments up until the last one provided. |
| target = member.fixedBackendName(); |
| arguments = argumentsBuffer.getRange( |
| 0, indexOfLastOptionalArgumentInParameters + 1); |
| } |
| statements.add( |
| new js.Return( |
| new js.VariableUse('this').dot(target).callWith(arguments))); |
| |
| if (isNativeLiteral(nativeTagInfo) || !overriddenMethods.contains(member)) { |
| // Call the method directly. |
| return statements; |
| } else { |
| return <js.Statement>[ |
| generateMethodBodyWithPrototypeCheck( |
| invocationName, new js.Block(statements), stubParameters)]; |
| } |
| } |
| |
| // If a method is overridden, we must check if the prototype of 'this' has the |
| // method available. Otherwise, we may end up calling the method from the |
| // super class. If the method is not available, we make a direct call to |
| // Object.prototype.$methodName. This method will patch the prototype of |
| // 'this' to the real method. |
| js.Statement generateMethodBodyWithPrototypeCheck( |
| String methodName, |
| js.Statement body, |
| List<js.Parameter> parameters) { |
| return new js.If( |
| new js.VariableUse('Object') |
| .dot('getPrototypeOf') |
| .callWith([new js.VariableUse('this')]) |
| .dot('hasOwnProperty') |
| .callWith([new js.LiteralString("'$methodName'")]), |
| body, |
| new js.Block( |
| <js.Statement>[ |
| new js.Return( |
| new js.VariableUse('Object') |
| .dot('prototype').dot(methodName).dot('call') |
| .callWith( |
| <js.Expression>[new js.VariableUse('this')] |
| ..addAll(parameters.map((param) => |
| new js.VariableUse(param.name))))) |
| ])); |
| } |
| |
| js.Block generateMethodBodyWithPrototypeCheckForElement( |
| FunctionElement element, |
| js.Block body, |
| List<js.Parameter> parameters) { |
| String methodName; |
| Namer namer = backend.namer; |
| if (element.kind == ElementKind.FUNCTION) { |
| methodName = namer.instanceMethodName(element); |
| } else if (element.kind == ElementKind.GETTER) { |
| methodName = namer.getterName(element.getLibrary(), element.name); |
| } else if (element.kind == ElementKind.SETTER) { |
| methodName = namer.setterName(element.getLibrary(), element.name); |
| } else { |
| compiler.internalError('unexpected kind: "${element.kind}"', |
| element: element); |
| } |
| |
| return new js.Block( |
| [generateMethodBodyWithPrototypeCheck(methodName, body, parameters)]); |
| } |
| |
| |
| void emitDynamicDispatchMetadata() { |
| if (classesWithDynamicDispatch.isEmpty) return; |
| int length = classesWithDynamicDispatch.length; |
| if (!compiler.enableMinification) { |
| 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); |
| getDirectSubclasses(cls).forEach(visit); |
| classes.add(cls); |
| } |
| classesWithDynamicDispatch.forEach(visit); |
| |
| Collection<ClassElement> preorderDispatchClasses = classes.filter( |
| (cls) => !getDirectSubclasses(cls).isEmpty && |
| classesWithDynamicDispatch.contains(cls)); |
| |
| if (!compiler.enableMinification) { |
| nativeBuffer.add('// ${classes.length} classes\n'); |
| } |
| Collection<ClassElement> classesThatHaveSubclasses = classes.filter( |
| (ClassElement t) => !getDirectSubclasses(t).isEmpty); |
| if (!compiler.enableMinification) { |
| 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>[]; |
| // Values of temporary variables. |
| Map<String, js.Expression> varDefns = new Map<String, js.Expression>(); |
| |
| // Expression to compute tags string for a class. The expression will |
| // initially be a string or expression building a string, but may be |
| // replaced with a variable reference to the common substring. |
| Map<ClassElement, js.Expression> tagDefns = |
| new Map<ClassElement, js.Expression>(); |
| |
| js.Expression makeExpression(ClassElement classElement) { |
| // Expression fragments for this set of cls keys. |
| List<js.Expression> expressions = <js.Expression>[]; |
| // TODO: Remove if cls is abstract. |
| List<String> subtags = [toNativeTag(classElement)]; |
| void walk(ClassElement cls) { |
| for (final ClassElement subclass in getDirectSubclasses(cls)) { |
| ClassElement tag = subclass; |
| js.Expression existing = tagDefns[tag]; |
| if (existing == null) { |
| // [subclass] is still within the subtree between dispatch classes. |
| subtags.add(toNativeTag(tag)); |
| walk(subclass); |
| } else { |
| // [subclass] is one of the preorderDispatchClasses, so CSE this |
| // reference with the previous reference. |
| js.VariableUse use = existing.asVariableUse(); |
| if (use != null && varDefns.containsKey(use.name)) { |
| // We end up here if the subclasses have a DAG structure. We |
| // don't have DAGs yet, but if the dispatch is used for mixins |
| // that will be a possibility. |
| // Re-use the previously created temporary variable. |
| expressions.add(new js.VariableUse(use.name)); |
| } else { |
| String varName = 'v${varNames.length}_${tag.name.slowToString()}'; |
| varNames.add(varName); |
| varDefns[varName] = existing; |
| tagDefns[tag] = new js.VariableUse(varName); |
| expressions.add(new js.VariableUse(varName)); |
| } |
| } |
| } |
| } |
| walk(classElement); |
| |
| if (!subtags.isEmpty) { |
| expressions.add( |
| new js.LiteralString("'${Strings.join(subtags, '|')}'")); |
| } |
| js.Expression expression; |
| if (expressions.length == 1) { |
| expression = expressions[0]; |
| } else { |
| js.Expression array = new js.ArrayInitializer.from(expressions); |
| expression = new js.Call( |
| new js.PropertyAccess.field(array, 'join'), |
| [new js.LiteralString("'|'")]); |
| } |
| return expression; |
| } |
| |
| for (final ClassElement classElement in preorderDispatchClasses) { |
| tagDefns[classElement] = makeExpression(classElement); |
| } |
| |
| // Write out a thunk that builds the metadata. |
| if (!tagDefns.isEmpty) { |
| List<js.Statement> statements = <js.Statement>[]; |
| |
| List<js.VariableInitialization> initializations = |
| <js.VariableInitialization>[]; |
| for (final String varName in varNames) { |
| initializations.add( |
| new js.VariableInitialization( |
| new js.VariableDeclaration(varName), |
| varDefns[varName])); |
| } |
| if (!initializations.isEmpty) { |
| statements.add( |
| new js.ExpressionStatement( |
| new js.VariableDeclarationList(initializations))); |
| } |
| |
| // [table] is a list of lists, each inner list of the form: |
| // [dynamic-dispatch-tag, tags-of-classes-implementing-dispatch-tag] |
| // E.g. |
| // [['Node', 'Text|HTMLElement|HTMLDivElement|...'], ...] |
| js.Expression table = |
| new js.ArrayInitializer.from( |
| preorderDispatchClasses.map((cls) => |
| new js.ArrayInitializer.from([ |
| new js.LiteralString("'${toNativeTag(cls)}'"), |
| tagDefns[cls]]))); |
| |
| // $.dynamicSetMetadata(table); |
| statements.add( |
| new js.ExpressionStatement( |
| new js.Call( |
| new js.VariableUse(dynamicSetMetadataName), |
| [table]))); |
| |
| // (function(){statements})(); |
| if (emitter.compiler.enableMinification) nativeBuffer.add(';'); |
| nativeBuffer.add( |
| js.prettyPrint( |
| new js.ExpressionStatement( |
| new js.Call(new js.Fun([], new js.Block(statements)), [])), |
| compiler)); |
| } |
| } |
| |
| 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) { |
| if (emitter.compiler.enableMinification) targetBuffer.add(";"); |
| 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'); |
| } |
| } |