| // Copyright (c) 2013, 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 ClassEmitter extends CodeEmitterHelper { |
| /** |
| * Documentation wanted -- johnniwinther |
| * |
| * Invariant: [classElement] must be a declaration element. |
| */ |
| void generateClass(ClassElement classElement, CodeBuffer buffer) { |
| final onlyForRti = |
| task.typeTestEmitter.rtiNeededClasses.contains(classElement); |
| |
| assert(invariant(classElement, classElement.isDeclaration)); |
| assert(invariant(classElement, !classElement.isNative() || onlyForRti)); |
| |
| task.needsDefineClass = true; |
| String className = namer.getNameOfClass(classElement); |
| |
| ClassElement superclass = classElement.superclass; |
| String superName = ""; |
| if (superclass != null) { |
| superName = namer.getNameOfClass(superclass); |
| } |
| String runtimeName = |
| namer.getPrimitiveInterceptorRuntimeName(classElement); |
| |
| if (classElement.isMixinApplication) { |
| String mixinName = namer.getNameOfClass(computeMixinClass(classElement)); |
| superName = '$superName+$mixinName'; |
| task.needsMixinSupport = true; |
| } |
| |
| ClassBuilder builder = new ClassBuilder(); |
| emitClassConstructor(classElement, builder, runtimeName, |
| onlyForRti: onlyForRti); |
| emitFields(classElement, builder, superName, onlyForRti: onlyForRti); |
| emitClassGettersSetters(classElement, builder, onlyForRti: onlyForRti); |
| emitInstanceMembers(classElement, builder, onlyForRti: onlyForRti); |
| task.typeTestEmitter.emitIsTests(classElement, builder); |
| |
| emitClassBuilderWithReflectionData( |
| className, classElement, builder, buffer); |
| } |
| |
| void emitClassConstructor(ClassElement classElement, |
| ClassBuilder builder, |
| String runtimeName, |
| {bool onlyForRti: false}) { |
| List<String> fields = <String>[]; |
| if (!onlyForRti && !classElement.isNative()) { |
| visitFields(classElement, false, |
| (Element member, |
| String name, |
| String accessorName, |
| bool needsGetter, |
| bool needsSetter, |
| bool needsCheckedSetter) { |
| fields.add(name); |
| }); |
| } |
| String constructorName = namer.getNameOfClass(classElement); |
| task.precompiledFunction.add(new jsAst.FunctionDeclaration( |
| new jsAst.VariableDeclaration(constructorName), |
| js.fun(fields, fields.map( |
| (name) => js('this.$name = $name')).toList()))); |
| if (runtimeName == null) { |
| runtimeName = constructorName; |
| } |
| task.precompiledFunction.addAll([ |
| js('$constructorName.builtin\$cls = "$runtimeName"'), |
| js.if_('!"name" in $constructorName', |
| js('$constructorName.name = "$constructorName"')), |
| js('\$desc=\$collectedClasses.$constructorName'), |
| js.if_('\$desc instanceof Array', js('\$desc = \$desc[1]')), |
| js('$constructorName.prototype = \$desc'), |
| ]); |
| |
| task.precompiledConstructorNames.add(js(constructorName)); |
| } |
| |
| /// Returns `true` if fields added. |
| bool emitFields(Element element, |
| ClassBuilder builder, |
| String superName, |
| { bool classIsNative: false, |
| bool emitStatics: false, |
| bool onlyForRti: false }) { |
| assert(!emitStatics || !onlyForRti); |
| bool isClass = false; |
| bool isLibrary = false; |
| if (element.isClass()) { |
| isClass = true; |
| } else if (element.isLibrary()) { |
| isLibrary = false; |
| assert(invariant(element, emitStatics)); |
| } else { |
| throw new SpannableAssertionFailure( |
| element, 'Must be a ClassElement or a LibraryElement'); |
| } |
| StringBuffer buffer = new StringBuffer(); |
| if (emitStatics) { |
| assert(invariant(element, superName == null, message: superName)); |
| } else { |
| assert(invariant(element, superName != null)); |
| String nativeName = |
| namer.getPrimitiveInterceptorRuntimeName(element); |
| if (nativeName != null) { |
| buffer.write('$nativeName/'); |
| } |
| buffer.write('$superName;'); |
| } |
| int bufferClassLength = buffer.length; |
| |
| String separator = ''; |
| |
| var fieldMetadata = []; |
| bool hasMetadata = false; |
| |
| if (!onlyForRti) { |
| visitFields(element, emitStatics, |
| (VariableElement field, |
| String name, |
| String accessorName, |
| bool needsGetter, |
| bool needsSetter, |
| bool needsCheckedSetter) { |
| // Ignore needsCheckedSetter - that is handled below. |
| bool needsAccessor = (needsGetter || needsSetter); |
| // We need to output the fields for non-native classes so we can auto- |
| // generate the constructor. For native classes there are no |
| // constructors, so we don't need the fields unless we are generating |
| // accessors at runtime. |
| if (!classIsNative || needsAccessor) { |
| buffer.write(separator); |
| separator = ','; |
| var metadata = task.metadataEmitter.buildMetadataFunction(field); |
| if (metadata != null) { |
| hasMetadata = true; |
| } else { |
| metadata = new jsAst.LiteralNull(); |
| } |
| fieldMetadata.add(metadata); |
| recordMangledField(field, accessorName, field.name.slowToString()); |
| if (!needsAccessor) { |
| // Emit field for constructor generation. |
| assert(!classIsNative); |
| buffer.write(name); |
| } else { |
| // Emit (possibly renaming) field name so we can add accessors at |
| // runtime. |
| buffer.write(accessorName); |
| if (name != accessorName) { |
| buffer.write(':$name'); |
| // Only the native classes can have renaming accessors. |
| assert(classIsNative); |
| } |
| |
| int getterCode = 0; |
| if (needsGetter) { |
| if (field.isInstanceMember()) { |
| // 01: function() { return this.field; } |
| // 10: function(receiver) { return receiver.field; } |
| // 11: function(receiver) { return this.field; } |
| bool isIntercepted = backend.fieldHasInterceptedGetter(field); |
| getterCode += isIntercepted ? 2 : 0; |
| getterCode += backend.isInterceptorClass(element) ? 0 : 1; |
| // TODO(sra): 'isInterceptorClass' might not be the correct test |
| // for methods forced to use the interceptor convention because |
| // the method's class was elsewhere mixed-in to an interceptor. |
| assert(!field.isInstanceMember() || getterCode != 0); |
| if (isIntercepted) { |
| task.interceptorEmitter.interceptorInvocationNames.add( |
| namer.getterName(field)); |
| } |
| } else { |
| getterCode = 1; |
| } |
| } |
| int setterCode = 0; |
| if (needsSetter) { |
| if (field.isInstanceMember()) { |
| // 01: function(value) { this.field = value; } |
| // 10: function(receiver, value) { receiver.field = value; } |
| // 11: function(receiver, value) { this.field = value; } |
| bool isIntercepted = backend.fieldHasInterceptedSetter(field); |
| setterCode += isIntercepted ? 2 : 0; |
| setterCode += backend.isInterceptorClass(element) ? 0 : 1; |
| assert(!field.isInstanceMember() || setterCode != 0); |
| if (isIntercepted) { |
| task.interceptorEmitter.interceptorInvocationNames.add( |
| namer.setterName(field)); |
| } |
| } else { |
| setterCode = 1; |
| } |
| } |
| int code = getterCode + (setterCode << 2); |
| if (code == 0) { |
| compiler.reportInternalError( |
| field, 'Internal error: code is 0 ($element/$field)'); |
| } else { |
| buffer.write(FIELD_CODE_CHARACTERS[code - FIRST_FIELD_CODE]); |
| } |
| } |
| if (backend.isAccessibleByReflection(field)) { |
| buffer.write(new String.fromCharCode(REFLECTION_MARKER)); |
| } |
| } |
| }); |
| } |
| |
| bool fieldsAdded = buffer.length > bufferClassLength; |
| String compactClassData = buffer.toString(); |
| jsAst.Expression classDataNode = js.string(compactClassData); |
| if (hasMetadata) { |
| fieldMetadata.insert(0, classDataNode); |
| classDataNode = new jsAst.ArrayInitializer.from(fieldMetadata); |
| } |
| builder.addProperty('', classDataNode); |
| return fieldsAdded; |
| } |
| |
| void emitClassGettersSetters(ClassElement classElement, |
| ClassBuilder builder, |
| {bool onlyForRti: false}) { |
| if (onlyForRti) return; |
| |
| visitFields(classElement, false, |
| (VariableElement member, |
| String name, |
| String accessorName, |
| bool needsGetter, |
| bool needsSetter, |
| bool needsCheckedSetter) { |
| compiler.withCurrentElement(member, () { |
| if (needsCheckedSetter) { |
| assert(!needsSetter); |
| generateCheckedSetter(member, name, accessorName, builder); |
| } |
| if (needsGetter) { |
| generateGetter(member, name, accessorName, builder); |
| } |
| if (needsSetter) { |
| generateSetter(member, name, accessorName, builder); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Documentation wanted -- johnniwinther |
| * |
| * Invariant: [classElement] must be a declaration element. |
| */ |
| void emitInstanceMembers(ClassElement classElement, |
| ClassBuilder builder, |
| {bool onlyForRti: false}) { |
| assert(invariant(classElement, classElement.isDeclaration)); |
| |
| if (onlyForRti || classElement.isMixinApplication) return; |
| |
| void visitMember(ClassElement enclosing, Element member) { |
| assert(invariant(classElement, member.isDeclaration)); |
| if (member.isInstanceMember()) { |
| task.containerBuilder.addMember(member, builder); |
| } |
| } |
| |
| classElement.implementation.forEachMember( |
| visitMember, |
| includeBackendMembers: true); |
| |
| if (identical(classElement, compiler.objectClass) |
| && compiler.enabledNoSuchMethod) { |
| // Emit the noSuchMethod handlers on the Object prototype now, |
| // so that the code in the dynamicFunction helper can find |
| // them. Note that this helper is invoked before analyzing the |
| // full JS script. |
| if (!task.nativeEmitter.handleNoSuchMethod) { |
| task.nsmEmitter.emitNoSuchMethodHandlers(builder.addProperty); |
| } |
| } |
| } |
| |
| void emitClassBuilderWithReflectionData(String className, |
| ClassElement classElement, |
| ClassBuilder builder, |
| CodeBuffer buffer) { |
| var metadata = task.metadataEmitter.buildMetadataFunction(classElement); |
| if (metadata != null) { |
| builder.addProperty("@", metadata); |
| } |
| |
| if (backend.isNeededForReflection(classElement)) { |
| Link typeVars = classElement.typeVariables; |
| List properties = []; |
| for (TypeVariableType typeVar in typeVars) { |
| properties.add(js.string(typeVar.name.slowToString())); |
| properties.add( |
| js.toExpression( |
| task.metadataEmitter.reifyType(typeVar.element.bound))); |
| } |
| |
| ClassElement superclass = classElement.superclass; |
| bool hasSuper = superclass != null; |
| if ((!properties.isEmpty && !hasSuper) || |
| (hasSuper && superclass.typeVariables != typeVars)) { |
| builder.addProperty('<>', new jsAst.ArrayInitializer.from(properties)); |
| } |
| } |
| List<CodeBuffer> classBuffers = task.elementBuffers[classElement]; |
| if (classBuffers == null) { |
| classBuffers = []; |
| } else { |
| task.elementBuffers.remove(classElement); |
| } |
| CodeBuffer statics = new CodeBuffer(); |
| statics.write('{$n'); |
| bool hasStatics = false; |
| ClassBuilder staticsBuilder = new ClassBuilder(); |
| if (emitFields(classElement, staticsBuilder, null, emitStatics: true)) { |
| hasStatics = true; |
| statics.write('"":$_'); |
| statics.write( |
| jsAst.prettyPrint(staticsBuilder.properties.single.value, compiler)); |
| statics.write(',$n'); |
| } |
| for (CodeBuffer classBuffer in classBuffers) { |
| // TODO(ahe): What about deferred? |
| if (classBuffer != null) { |
| hasStatics = true; |
| statics.addBuffer(classBuffer); |
| } |
| } |
| statics.write('}$n'); |
| if (hasStatics) { |
| builder.addProperty('static', new jsAst.Blob(statics)); |
| } |
| |
| // TODO(ahe): This method (generateClass) should return a jsAst.Expression. |
| if (!buffer.isEmpty) { |
| buffer.write(',$n$n'); |
| } |
| buffer.write('$className:$_'); |
| buffer.write(jsAst.prettyPrint(builder.toObjectInitializer(), compiler)); |
| String reflectionName = task.getReflectionName(classElement, className); |
| if (reflectionName != null) { |
| List<int> interfaces = <int>[]; |
| for (DartType interface in classElement.interfaces) { |
| interfaces.add(task.metadataEmitter.reifyType(interface)); |
| } |
| buffer.write(',$n$n"+$reflectionName": $interfaces'); |
| } |
| } |
| |
| /** |
| * Calls [addField] for each of the fields of [element]. |
| * |
| * [element] must be a [ClassElement] or a [LibraryElement]. |
| * |
| * If [element] is a [ClassElement], the static fields of the class are |
| * visited if [visitStatics] is true and the instance fields are visited if |
| * [visitStatics] is false. |
| * |
| * If [element] is a [LibraryElement], [visitStatics] must be true. |
| * |
| * When visiting the instance fields of a class, the fields of its superclass |
| * are also visited if the class is instantiated. |
| * |
| * Invariant: [element] must be a declaration element. |
| */ |
| void visitFields(Element element, bool visitStatics, AcceptField f) { |
| assert(invariant(element, element.isDeclaration)); |
| |
| bool isClass = false; |
| bool isLibrary = false; |
| if (element.isClass()) { |
| isClass = true; |
| } else if (element.isLibrary()) { |
| isLibrary = true; |
| assert(invariant(element, visitStatics)); |
| } else { |
| throw new SpannableAssertionFailure( |
| element, 'Expected a ClassElement or a LibraryElement.'); |
| } |
| |
| // If the class is never instantiated we still need to set it up for |
| // inheritance purposes, but we can simplify its JavaScript constructor. |
| bool isInstantiated = |
| compiler.codegenWorld.instantiatedClasses.contains(element); |
| |
| void visitField(Element holder, VariableElement field) { |
| assert(invariant(element, field.isDeclaration)); |
| SourceString name = field.name; |
| |
| // Keep track of whether or not we're dealing with a field mixin |
| // into a native class. |
| bool isMixinNativeField = |
| isClass && element.isNative() && holder.isMixinApplication; |
| |
| // See if we can dynamically create getters and setters. |
| // We can only generate getters and setters for [element] since |
| // the fields of super classes could be overwritten with getters or |
| // setters. |
| bool needsGetter = false; |
| bool needsSetter = false; |
| // We need to name shadowed fields differently, so they don't clash with |
| // the non-shadowed field. |
| bool isShadowed = false; |
| if (isLibrary || isMixinNativeField || holder == element) { |
| needsGetter = fieldNeedsGetter(field); |
| needsSetter = fieldNeedsSetter(field); |
| } else { |
| ClassElement cls = element; |
| isShadowed = cls.isShadowedByField(field); |
| } |
| |
| if ((isInstantiated && !holder.isNative()) |
| || needsGetter |
| || needsSetter) { |
| String accessorName = isShadowed |
| ? namer.shadowedFieldName(field) |
| : namer.getNameOfField(field); |
| String fieldName = field.hasFixedBackendName() |
| ? field.fixedBackendName() |
| : (isMixinNativeField ? name.slowToString() : accessorName); |
| bool needsCheckedSetter = false; |
| if (compiler.enableTypeAssertions |
| && needsSetter |
| && canGenerateCheckedSetter(field)) { |
| needsCheckedSetter = true; |
| needsSetter = false; |
| } |
| // Getters and setters with suffixes will be generated dynamically. |
| f(field, fieldName, accessorName, needsGetter, needsSetter, |
| needsCheckedSetter); |
| } |
| } |
| |
| if (isLibrary) { |
| LibraryElement library = element; |
| library.implementation.forEachLocalMember((Element member) { |
| if (member.isField()) visitField(library, member); |
| }); |
| } else if (visitStatics) { |
| ClassElement cls = element; |
| cls.implementation.forEachStaticField(visitField); |
| } else { |
| ClassElement cls = element; |
| // TODO(kasperl): We should make sure to only emit one version of |
| // overridden fields. Right now, we rely on the ordering so the |
| // fields pulled in from mixins are replaced with the fields from |
| // the class definition. |
| |
| // If a class is not instantiated then we add the field just so we can |
| // generate the field getter/setter dynamically. Since this is only |
| // allowed on fields that are in [element] we don't need to visit |
| // superclasses for non-instantiated classes. |
| cls.implementation.forEachInstanceField( |
| visitField, includeSuperAndInjectedMembers: isInstantiated); |
| } |
| } |
| |
| void recordMangledField(Element member, |
| String accessorName, |
| String memberName) { |
| if (!backend.shouldRetainGetter(member)) return; |
| String previousName; |
| if (member.isInstanceMember()) { |
| previousName = task.mangledFieldNames.putIfAbsent( |
| '${namer.getterPrefix}$accessorName', |
| () => memberName); |
| } else { |
| previousName = task.mangledGlobalFieldNames.putIfAbsent( |
| accessorName, |
| () => memberName); |
| } |
| assert(invariant(member, previousName == memberName, |
| message: '$previousName != ${memberName}')); |
| } |
| |
| bool fieldNeedsGetter(VariableElement field) { |
| assert(field.isField()); |
| if (fieldAccessNeverThrows(field)) return false; |
| return backend.shouldRetainGetter(field) |
| || compiler.codegenWorld.hasInvokedGetter(field, compiler); |
| } |
| |
| bool fieldNeedsSetter(VariableElement field) { |
| assert(field.isField()); |
| if (fieldAccessNeverThrows(field)) return false; |
| return (!field.modifiers.isFinalOrConst()) |
| && (backend.shouldRetainSetter(field) |
| || compiler.codegenWorld.hasInvokedSetter(field, compiler)); |
| } |
| |
| // We never access a field in a closure (a captured variable) without knowing |
| // that it is there. Therefore we don't need to use a getter (that will throw |
| // if the getter method is missing), but can always access the field directly. |
| static bool fieldAccessNeverThrows(VariableElement field) { |
| return field is ClosureFieldElement; |
| } |
| |
| bool canGenerateCheckedSetter(VariableElement field) { |
| // We never generate accessors for top-level/static fields. |
| if (!field.isInstanceMember()) return false; |
| DartType type = field.computeType(compiler).unalias(compiler); |
| if (type.element.isTypeVariable() || |
| (type is FunctionType && type.containsTypeVariables) || |
| type.treatAsDynamic || |
| type.element == compiler.objectClass) { |
| // TODO(ngeoffray): Support type checks on type parameters. |
| return false; |
| } |
| return true; |
| } |
| |
| void generateCheckedSetter(Element member, |
| String fieldName, |
| String accessorName, |
| ClassBuilder builder) { |
| assert(canGenerateCheckedSetter(member)); |
| DartType type = member.computeType(compiler); |
| // TODO(ahe): Generate a dynamic type error here. |
| if (type.element.isErroneous()) return; |
| type = type.unalias(compiler); |
| // TODO(11273): Support complex subtype checks. |
| type = type.asRaw(); |
| CheckedModeHelper helper = |
| backend.getCheckedModeHelper(type, typeCast: false); |
| FunctionElement helperElement = helper.getElement(compiler); |
| String helperName = namer.isolateAccess(helperElement); |
| List<jsAst.Expression> arguments = <jsAst.Expression>[js('v')]; |
| if (helperElement.computeSignature(compiler).parameterCount != 1) { |
| arguments.add(js.string(namer.operatorIsType(type))); |
| } |
| |
| String setterName = namer.setterNameFromAccessorName(accessorName); |
| String receiver = backend.isInterceptorClass(member.getEnclosingClass()) |
| ? 'receiver' : 'this'; |
| List<String> args = backend.isInterceptedMethod(member) |
| ? ['receiver', 'v'] |
| : ['v']; |
| builder.addProperty(setterName, |
| js.fun(args, |
| js('$receiver.$fieldName = #', js(helperName)(arguments)))); |
| generateReflectionDataForFieldGetterOrSetter( |
| member, setterName, builder, isGetter: false); |
| } |
| |
| void generateGetter(Element member, String fieldName, String accessorName, |
| ClassBuilder builder) { |
| String getterName = namer.getterNameFromAccessorName(accessorName); |
| ClassElement cls = member.getEnclosingClass(); |
| String className = namer.getNameOfClass(cls); |
| String receiver = backend.isInterceptorClass(cls) ? 'receiver' : 'this'; |
| List<String> args = backend.isInterceptedMethod(member) ? ['receiver'] : []; |
| task.precompiledFunction.add( |
| js('$className.prototype.$getterName = #', |
| js.fun(args, js.return_(js('$receiver.$fieldName'))))); |
| if (backend.isNeededForReflection(member)) { |
| task.precompiledFunction.add( |
| js('$className.prototype.$getterName.${namer.reflectableField} = 1')); |
| } |
| } |
| |
| void generateSetter(Element member, String fieldName, String accessorName, |
| ClassBuilder builder) { |
| String setterName = namer.setterNameFromAccessorName(accessorName); |
| ClassElement cls = member.getEnclosingClass(); |
| String className = namer.getNameOfClass(cls); |
| String receiver = backend.isInterceptorClass(cls) ? 'receiver' : 'this'; |
| List<String> args = |
| backend.isInterceptedMethod(member) ? ['receiver', 'v'] : ['v']; |
| task.precompiledFunction.add( |
| js('$className.prototype.$setterName = #', |
| js.fun(args, js.return_(js('$receiver.$fieldName = v'))))); |
| if (backend.isNeededForReflection(member)) { |
| task.precompiledFunction.add( |
| js('$className.prototype.$setterName.${namer.reflectableField} = 1')); |
| } |
| } |
| |
| void generateReflectionDataForFieldGetterOrSetter(Element member, |
| String name, |
| ClassBuilder builder, |
| {bool isGetter}) { |
| Selector selector = isGetter |
| ? new Selector.getter(member.name, member.getLibrary()) |
| : new Selector.setter(member.name, member.getLibrary()); |
| String reflectionName = task.getReflectionName(selector, name); |
| if (reflectionName != null) { |
| var reflectable = |
| js(backend.isAccessibleByReflection(member) ? '1' : '0'); |
| builder.addProperty('+$reflectionName', reflectable); |
| } |
| } |
| } |