| // 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 native; |
| |
| /// This class is a temporary work-around until we get a more powerful DartType. |
| class SpecialType { |
| final String name; |
| const SpecialType._(this.name); |
| |
| /// The type Object, but no subtypes: |
| static const JsObject = const SpecialType._('=Object'); |
| |
| int get hashCode => name.hashCode; |
| } |
| |
| /** |
| * A summary of the behavior of a native element. |
| * |
| * Native code can return values of one type and cause native subtypes of |
| * another type to be instantiated. By default, we compute both from the |
| * declared type. |
| * |
| * A field might yield any native type that 'is' the field type. |
| * |
| * A method might create and return instances of native subclasses of its |
| * declared return type, and a callback argument may be called with instances of |
| * the callback parameter type (e.g. Event). |
| * |
| * If there is one or more `@Creates` annotations, the union of the named types |
| * replaces the inferred instantiated type, and the return type is ignored for |
| * the purpose of inferring instantiated types. |
| * |
| * @Creates('IDBCursor') // Created asynchronously. |
| * @Creates('IDBRequest') // Created synchronously (for return value). |
| * IDBRequest openCursor(); |
| * |
| * If there is one or more `@Returns` annotations, the union of the named types |
| * replaces the declared return type. |
| * |
| * @Returns('IDBRequest') |
| * IDBRequest openCursor(); |
| * |
| * Types in annotations are non-nullable, so include `@Returns('Null')` if |
| * `null` may be returned. |
| */ |
| class NativeBehavior { |
| |
| /// [DartType]s or [SpecialType]s returned or yielded by the native element. |
| final List typesReturned = []; |
| |
| /// [DartType]s or [SpecialType]s instantiated by the native element. |
| final List typesInstantiated = []; |
| |
| // If this behavior is for a JS expression, [codeTemplate] contains the |
| // parsed tree. |
| js.Template codeTemplate; |
| |
| final SideEffects sideEffects = new SideEffects.empty(); |
| |
| /// Processes the type specification string of a call to JS and stores the |
| /// result in the [typesReturned] and [typesInstantiated]. It furthermore |
| /// computes the side effects, and, if given, invokes [setSideEffects] with |
| /// the computed effects. If no side effects are encoded in the [specString] |
| /// the [setSideEffects] method is not invoked. |
| /// |
| /// Two forms of the string is supported: |
| /// 1) A single type string of the form 'void', '', 'var' or 'T1|...|Tn' |
| /// which defines the types returned and for the later form also created by |
| /// the call to JS. |
| /// 2) A sequence of the form |
| /// '<type-tag>:<type-string>;<effect-tag>:<effect-string>' |
| /// where <type-tag> is either 'returns' or 'creates' and where |
| /// <type-string> is a type string like in 1). The type string marked by |
| /// 'returns' defines the types returned and 'creates' defines the types |
| /// created by the call to JS. |
| /// |
| /// The <effect-tag> is either 'effects' or 'depends' and |
| /// <effect-string> is either 'all', 'none' or a comma-separated list of |
| /// 'no-index', 'no-instance', 'no-static'. |
| /// |
| /// The flag 'all' indicates that the call affects/depends on every |
| /// side-effect. The flag 'none' indicates that the call does not affect |
| /// (resp. depends on) anything. |
| /// |
| /// 'no-index' indicates that the call does *not* do any array index-store |
| /// (for 'effects'), or depends on any value in an array (for 'depends'). |
| /// The flag 'no-instance' indicates that the call does not modify (resp. |
| /// depends on) any instance variable. Similarly static variables are |
| /// indicated with 'no-static'. The flags 'effects' and 'depends' must be |
| /// used in unison (either both are present or none is). |
| /// |
| /// Each tag kind (including the 'type-tag's) can only occur once in the |
| /// sequence. |
| /// |
| /// [specString] is the specification string, [resolveType] resolves named |
| /// types into type values, [typesReturned] and [typesInstantiated] collects |
| /// the types defined by the specification string, and [objectType] and |
| /// [nullType] define the types for `Object` and `Null`, respectively. The |
| /// latter is used for the type strings of the form '' and 'var'. |
| // TODO(johnniwinther): Use ';' as a separator instead of a terminator. |
| static void processSpecString( |
| DiagnosticListener listener, |
| Spannable spannable, |
| String specString, |
| {void setSideEffects(SideEffects newEffects), |
| dynamic resolveType(String typeString), |
| List typesReturned, List typesInstantiated, |
| objectType, nullType}) { |
| |
| /// Resolve a type string of one of the three forms: |
| /// * 'void' - in which case [onVoid] is called, |
| /// * '' or 'var' - in which case [onVar] is called, |
| /// * 'T1|...|Tn' - in which case [onType] is called for each Ti. |
| void resolveTypesString(String typesString, |
| {onVoid(), onVar(), onType(type)}) { |
| // Various things that are not in fact types. |
| if (typesString == 'void') { |
| if (onVoid != null) { |
| onVoid(); |
| } |
| return; |
| } |
| if (typesString == '' || typesString == 'var') { |
| if (onVar != null) { |
| onVar(); |
| } |
| return; |
| } |
| for (final typeString in typesString.split('|')) { |
| onType(resolveType(typeString)); |
| } |
| } |
| |
| |
| if (specString.contains(':')) { |
| /// Find and remove a substring of the form 'tag:<string>;' from |
| /// [specString]. |
| String getTagString(String tag) { |
| String marker = '$tag:'; |
| int startPos = specString.indexOf(marker); |
| if (startPos == -1) return null; |
| int endPos = specString.indexOf(';', startPos); |
| if (endPos == -1) return null; |
| String typeString = |
| specString.substring(startPos + marker.length, endPos); |
| specString = '${specString.substring(0, startPos)}' |
| '${specString.substring(endPos + 1)}'.trim(); |
| return typeString; |
| } |
| |
| String returns = getTagString('returns'); |
| if (returns != null) { |
| resolveTypesString(returns, onVar: () { |
| typesReturned.add(objectType); |
| typesReturned.add(nullType); |
| }, onType: (type) { |
| typesReturned.add(type); |
| }); |
| } |
| |
| String creates = getTagString('creates'); |
| if (creates != null) { |
| resolveTypesString(creates, onVoid: () { |
| listener.internalError(spannable, |
| "Invalid type string 'creates:$creates'"); |
| }, onVar: () { |
| listener.internalError(spannable, |
| "Invalid type string 'creates:$creates'"); |
| }, onType: (type) { |
| typesInstantiated.add(type); |
| }); |
| } |
| |
| String effects = getTagString('effects'); |
| String depends = getTagString('depends'); |
| if (effects != null && depends == null || |
| effects == null && depends != null) { |
| listener.internalError(spannable, |
| "Invalid JS spec string. " |
| "'effects' and 'depends' must occur together."); |
| } |
| |
| if (effects != null) { |
| SideEffects sideEffects = new SideEffects(); |
| if (effects == "none") { |
| sideEffects.clearAllSideEffects(); |
| } else if (effects == "all") { |
| // Don't do anything. |
| } else { |
| List<String> splitEffects = effects.split(","); |
| if (splitEffects.isEmpty) { |
| listener.internalError(spannable, "Missing side-effect flag."); |
| } |
| for (String effect in splitEffects) { |
| switch (effect) { |
| case "no-index": |
| sideEffects.clearChangesIndex(); |
| break; |
| case "no-instance": |
| sideEffects.clearChangesInstanceProperty(); |
| break; |
| case "no-static": |
| sideEffects.clearChangesStaticProperty(); |
| break; |
| default: |
| listener.internalError(spannable, |
| "Unrecognized side-effect flag: $effect."); |
| } |
| } |
| } |
| |
| if (depends == "none") { |
| sideEffects.clearAllDependencies(); |
| } else if (depends == "all") { |
| // Don't do anything. |
| } else { |
| List<String> splitDependencies = depends.split(","); |
| if (splitDependencies.isEmpty) { |
| listener.internalError(spannable, |
| "Missing side-effect dependency flag."); |
| } |
| for (String dependency in splitDependencies) { |
| switch (dependency) { |
| case "no-index": |
| sideEffects.clearDependsOnIndexStore(); |
| break; |
| case "no-instance": |
| sideEffects.clearDependsOnInstancePropertyStore(); |
| break; |
| case "no-static": |
| sideEffects.clearDependsOnStaticPropertyStore(); |
| break; |
| default: |
| listener.internalError(spannable, |
| "Unrecognized side-effect flag: $dependency."); |
| } |
| } |
| } |
| |
| setSideEffects(sideEffects); |
| } |
| |
| if (!specString.isEmpty) { |
| listener.internalError(spannable, "Invalid JS spec string."); |
| } |
| } else { |
| resolveTypesString(specString, onVar: () { |
| typesReturned.add(objectType); |
| typesReturned.add(nullType); |
| }, onType: (type) { |
| typesInstantiated.add(type); |
| typesReturned.add(type); |
| }); |
| } |
| } |
| |
| static NativeBehavior ofJsCall(Send jsCall, Compiler compiler, resolver) { |
| // The first argument of a JS-call is a string encoding various attributes |
| // of the code. |
| // |
| // 'Type1|Type2'. A union type. |
| // '=Object'. A JavaScript Object, no subtype. |
| |
| var argNodes = jsCall.arguments; |
| if (argNodes.isEmpty) { |
| compiler.internalError(jsCall, "JS expression has no type."); |
| } |
| |
| var code = argNodes.tail.head; |
| if (code is !StringNode || code.isInterpolation) { |
| compiler.internalError(code, 'JS code must be a string literal.'); |
| } |
| |
| LiteralString specLiteral = argNodes.head.asLiteralString(); |
| if (specLiteral == null) { |
| // TODO(sra): We could accept a type identifier? e.g. JS(bool, '1<2'). It |
| // is not very satisfactory because it does not work for void, dynamic. |
| compiler.internalError(argNodes.head, "Unexpected JS first argument."); |
| } |
| |
| NativeBehavior behavior = new NativeBehavior(); |
| behavior.codeTemplate = |
| js.js.parseForeignJS(code.dartString.slowToString()); |
| |
| String specString = specLiteral.dartString.slowToString(); |
| |
| dynamic resolveType(String typeString) { |
| return _parseType( |
| typeString, |
| compiler, |
| (name) => resolver.resolveTypeFromString(specLiteral, name), |
| jsCall); |
| } |
| |
| bool sideEffectsAreEncodedInSpecString = false; |
| |
| void setSideEffects(SideEffects newEffects) { |
| sideEffectsAreEncodedInSpecString = true; |
| behavior.sideEffects.setTo(newEffects); |
| } |
| |
| processSpecString(compiler, jsCall, |
| specString, |
| setSideEffects: setSideEffects, |
| resolveType: resolveType, |
| typesReturned: behavior.typesReturned, |
| typesInstantiated: behavior.typesInstantiated, |
| objectType: compiler.objectClass.computeType(compiler), |
| nullType: compiler.nullClass.computeType(compiler)); |
| |
| if (!sideEffectsAreEncodedInSpecString) { |
| new SideEffectsVisitor(behavior.sideEffects) |
| .visit(behavior.codeTemplate.ast); |
| } |
| |
| return behavior; |
| } |
| |
| static NativeBehavior ofJsEmbeddedGlobalCall(Send jsGlobalCall, |
| Compiler compiler, |
| resolver) { |
| // The first argument of a JS-embedded global call is a string encoding |
| // the type of the code. |
| // |
| // 'Type1|Type2'. A union type. |
| // '=Object'. A JavaScript Object, no subtype. |
| |
| Link<Node> argNodes = jsGlobalCall.arguments; |
| if (argNodes.isEmpty) { |
| compiler.internalError(jsGlobalCall, |
| "JS embedded global expression has no type."); |
| } |
| |
| // We don't check the given name. That needs to be done at a later point. |
| // This is, because we want to allow non-literals as names. |
| if (argNodes.tail.isEmpty) { |
| compiler.internalError(jsGlobalCall, 'Embedded Global is missing name'); |
| } |
| |
| if (!argNodes.tail.tail.isEmpty) { |
| compiler.internalError(argNodes.tail.tail.head, |
| 'Embedded Global has more than 2 arguments'); |
| } |
| |
| LiteralString specLiteral = argNodes.head.asLiteralString(); |
| if (specLiteral == null) { |
| // TODO(sra): We could accept a type identifier? e.g. JS(bool, '1<2'). It |
| // is not very satisfactory because it does not work for void, dynamic. |
| compiler.internalError(argNodes.head, "Unexpected first argument."); |
| } |
| |
| NativeBehavior behavior = new NativeBehavior(); |
| |
| String specString = specLiteral.dartString.slowToString(); |
| |
| dynamic resolveType(String typeString) { |
| return _parseType( |
| typeString, |
| compiler, |
| (name) => resolver.resolveTypeFromString(specLiteral, name), |
| jsGlobalCall); |
| } |
| |
| void setSideEffects(SideEffects newEffects) { |
| compiler.internalError(jsGlobalCall, |
| 'Embedded global calls may not have any side-effect overwrites: ' |
| '$specString'); |
| } |
| |
| processSpecString(compiler, jsGlobalCall, |
| specString, |
| setSideEffects: setSideEffects, |
| resolveType: resolveType, |
| typesReturned: behavior.typesReturned, |
| typesInstantiated: behavior.typesInstantiated, |
| objectType: compiler.objectClass.computeType(compiler), |
| nullType: compiler.nullClass.computeType(compiler)); |
| |
| return behavior; |
| } |
| |
| static NativeBehavior ofMethod(FunctionElement method, Compiler compiler) { |
| FunctionType type = method.computeType(compiler); |
| var behavior = new NativeBehavior(); |
| behavior.typesReturned.add(type.returnType); |
| if (!type.returnType.isVoid) { |
| // Declared types are nullable. |
| behavior.typesReturned.add(compiler.nullClass.computeType(compiler)); |
| } |
| behavior._capture(type, compiler); |
| |
| // TODO(sra): Optional arguments are currently missing from the |
| // DartType. This should be fixed so the following work-around can be |
| // removed. |
| method.functionSignature.forEachOptionalParameter( |
| (ParameterElement parameter) { |
| behavior._escape(parameter.type, compiler); |
| }); |
| |
| behavior._overrideWithAnnotations(method, compiler); |
| return behavior; |
| } |
| |
| static NativeBehavior ofFieldLoad(Element field, Compiler compiler) { |
| DartType type = field.computeType(compiler); |
| var behavior = new NativeBehavior(); |
| behavior.typesReturned.add(type); |
| // Declared types are nullable. |
| behavior.typesReturned.add(compiler.nullClass.computeType(compiler)); |
| behavior._capture(type, compiler); |
| behavior._overrideWithAnnotations(field, compiler); |
| return behavior; |
| } |
| |
| static NativeBehavior ofFieldStore(Element field, Compiler compiler) { |
| DartType type = field.computeType(compiler); |
| var behavior = new NativeBehavior(); |
| behavior._escape(type, compiler); |
| // We don't override the default behaviour - the annotations apply to |
| // loading the field. |
| return behavior; |
| } |
| |
| void _overrideWithAnnotations(Element element, Compiler compiler) { |
| if (element.metadata.isEmpty) return; |
| |
| DartType lookup(String name) { |
| Element e = element.buildScope().lookup(name); |
| if (e == null) return null; |
| if (e is! ClassElement) return null; |
| ClassElement cls = e; |
| cls.ensureResolved(compiler); |
| return cls.thisType; |
| } |
| |
| NativeEnqueuer enqueuer = compiler.enqueuer.resolution.nativeEnqueuer; |
| var creates = _collect(element, compiler, enqueuer.annotationCreatesClass, |
| lookup); |
| var returns = _collect(element, compiler, enqueuer.annotationReturnsClass, |
| lookup); |
| |
| if (creates != null) { |
| typesInstantiated..clear()..addAll(creates); |
| } |
| if (returns != null) { |
| typesReturned..clear()..addAll(returns); |
| } |
| } |
| |
| /** |
| * Returns a list of type constraints from the annotations of |
| * [annotationClass]. |
| * Returns `null` if no constraints. |
| */ |
| static _collect(Element element, Compiler compiler, Element annotationClass, |
| lookup(str)) { |
| var types = null; |
| for (Link<MetadataAnnotation> link = element.metadata; |
| !link.isEmpty; |
| link = link.tail) { |
| MetadataAnnotation annotation = link.head.ensureResolved(compiler); |
| ConstantValue value = annotation.constant.value; |
| if (!value.isConstructedObject) continue; |
| ConstructedConstantValue constructedObject = value; |
| if (constructedObject.type.element != annotationClass) continue; |
| |
| List<ConstantValue> fields = constructedObject.fields; |
| // TODO(sra): Better validation of the constant. |
| if (fields.length != 1 || !fields[0].isString) { |
| PartialMetadataAnnotation partial = annotation; |
| compiler.internalError(annotation, |
| 'Annotations needs one string: ${partial.parseNode(compiler)}'); |
| } |
| StringConstantValue specStringConstant = fields[0]; |
| String specString = specStringConstant.toDartString().slowToString(); |
| for (final typeString in specString.split('|')) { |
| var type = _parseType(typeString, compiler, lookup, annotation); |
| if (types == null) types = []; |
| types.add(type); |
| } |
| } |
| return types; |
| } |
| |
| /// Models the behavior of having intances of [type] escape from Dart code |
| /// into native code. |
| void _escape(DartType type, Compiler compiler) { |
| type = type.unalias(compiler); |
| if (type is FunctionType) { |
| FunctionType functionType = type; |
| // A function might be called from native code, passing us novel |
| // parameters. |
| _escape(functionType.returnType, compiler); |
| for (DartType parameter in functionType.parameterTypes) { |
| _capture(parameter, compiler); |
| } |
| } |
| } |
| |
| /// Models the behavior of Dart code receiving instances and methods of [type] |
| /// from native code. We usually start the analysis by capturing a native |
| /// method that has been used. |
| void _capture(DartType type, Compiler compiler) { |
| type = type.unalias(compiler); |
| if (type is FunctionType) { |
| FunctionType functionType = type; |
| _capture(functionType.returnType, compiler); |
| for (DartType parameter in functionType.parameterTypes) { |
| _escape(parameter, compiler); |
| } |
| } else { |
| typesInstantiated.add(type); |
| } |
| } |
| |
| static dynamic _parseType(String typeString, Compiler compiler, |
| lookup(name), locationNodeOrElement) { |
| if (typeString == '=Object') return SpecialType.JsObject; |
| if (typeString == 'dynamic') { |
| return const DynamicType(); |
| } |
| var type = lookup(typeString); |
| if (type != null) return type; |
| |
| int index = typeString.indexOf('<'); |
| if (index < 1) { |
| compiler.internalError( |
| _errorNode(locationNodeOrElement, compiler), |
| "Type '$typeString' not found."); |
| } |
| type = lookup(typeString.substring(0, index)); |
| if (type != null) { |
| // TODO(sra): Parse type parameters. |
| return type; |
| } |
| compiler.internalError( |
| _errorNode(locationNodeOrElement, compiler), |
| "Type '$typeString' not found."); |
| return null; |
| } |
| |
| static _errorNode(locationNodeOrElement, compiler) { |
| if (locationNodeOrElement is Node) return locationNodeOrElement; |
| return locationNodeOrElement.parseNode(compiler); |
| } |
| } |