| // Copyright (c) 2019, 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. |
| |
| // @dart = 2.10 |
| |
| /// TypeReferences are 'holes' in the generated JavaScript that are filled in by |
| /// the emitter with code to access a type. |
| /// |
| /// The Dart code |
| /// |
| /// foo1() => bar<int>(X<String>()); |
| /// |
| /// might be compiled to something like the following, with TypeReference1 |
| /// referring to the constructed type `X<String>`, and TypeReference2 referring |
| /// to the method type argument `int`: |
| /// |
| /// foo1: function() { |
| /// return bar(new X(TypeReference1), TypeReference2); |
| /// } |
| /// |
| /// The dart method `foo2` would be compiled separately, with the generated code |
| /// containing TypeReference3 referring to `int`: |
| /// |
| /// foo2() => bar<int>(null); |
| /// --> |
| /// foo2: function() { |
| /// return bar(null, TypeReference3); |
| /// } |
| /// |
| /// When the code for an output unit (main unit or deferred loaded unit) is |
| /// assembled, there will also be a TypeReferenceResource 'hole', so the |
| /// assembled looks something like |
| /// |
| /// foo: function() { |
| /// return bar(new X(TypeReference1), TypeReference2); |
| /// } |
| /// foo2: function() { |
| /// return bar(null, TypeReference3); |
| /// } |
| /// ... |
| /// TypeReferenceResource |
| /// |
| /// The TypeReferenceFinalizer decides on a strategy for accessing the types. In |
| /// most cases it is best to precompute the types and access them via a |
| /// object. The TypeReference nodes are filled in with property access |
| /// expressions and the TypeReferenceResource is filled in with the precomputed |
| /// data, something like: |
| /// |
| /// foo1: function() { |
| /// return bar(new X(type$.X_String), type$.int); |
| /// } |
| /// foo2: function() { |
| /// return bar(null, type$.int); |
| /// } |
| /// ... |
| /// var type$ = { |
| /// int: findType("int"), |
| /// X_String: findType("X<String>") |
| /// }; |
| /// |
| /// In minified mode, the properties `int` and `X_String` can be replaced by |
| /// shorter names. |
| library js_backend.type_reference; |
| |
| import 'package:front_end/src/api_unstable/dart2js.dart' |
| show $0, $9, $A, $Z, $_, $a, $z; |
| |
| import '../common/elements.dart' show CommonElements; |
| import '../elements/types.dart'; |
| import '../js/js.dart' as js; |
| import '../js_emitter/code_emitter_task.dart' show Emitter; |
| import '../js_model/type_recipe.dart' |
| show |
| TypeRecipe, |
| TypeExpressionRecipe, |
| SingletonTypeEnvironmentRecipe, |
| FullTypeEnvironmentRecipe; |
| import '../serialization/serialization.dart'; |
| import '../util/util.dart' show Hashing; |
| import 'frequency_assignment.dart'; |
| import 'namer.dart'; |
| import 'runtime_types_new.dart' show RecipeEncoder; |
| |
| /// Run the minifier for 'type$' property names even in non-minified mode, |
| /// making a name from minified name and the readable name. Usage: |
| /// |
| /// DART_VM_OPTIONS='-DDebugMinifyTypesHolder=true' dart2js ... |
| /// |
| const _debugMinify = bool.fromEnvironment('DebugMinifyTypesHolder'); |
| |
| /// A [TypeReference] is a deferred JavaScript expression that refers to the |
| /// runtime representation of a ground type or ground type environment. The |
| /// deferred expression is filled in by the TypeReferenceFinalizer which is |
| /// called from the fragment emitter. The replacement expression could be any |
| /// expression, e.g. a call, or a reference to a variable, or property of a |
| /// variable. |
| class TypeReference extends js.DeferredExpression implements js.AstContainer { |
| static const String tag = 'type-reference'; |
| |
| /// [typeRecipe] is a recipe for a ground type or type environment. |
| final TypeRecipe typeRecipe; |
| |
| // TODO(sra): Refine the concept of reference context and replace the |
| // 'forConstant' and 'forLazyInitializer' flags. |
| |
| // `true` if TypeReference is in code that initializes constant value. |
| bool forConstant = false; |
| |
| // `true` if TypeReference is in code for a lazy initializer of a static |
| // variable. |
| bool forLazyInitializer = false; |
| |
| js.Expression _value; |
| |
| @override |
| final js.JavaScriptNodeSourceInformation sourceInformation; |
| |
| TypeReference(this.typeRecipe) : sourceInformation = null; |
| TypeReference._(this.typeRecipe, this._value, this.sourceInformation); |
| |
| factory TypeReference.readFromDataSource(DataSourceReader source) { |
| source.begin(tag); |
| TypeRecipe recipe = source.readTypeRecipe(); |
| bool forLazyInitializer = source.readBool(); |
| source.end(tag); |
| return TypeReference(recipe)..forLazyInitializer = forLazyInitializer; |
| } |
| |
| void writeToDataSink(DataSinkWriter sink) { |
| sink.begin(tag); |
| sink.writeTypeRecipe(typeRecipe); |
| sink.writeBool(forLazyInitializer); |
| sink.end(tag); |
| } |
| |
| set value(js.Expression value) { |
| assert(!isFinalized && value != null); |
| _value = value; |
| } |
| |
| @override |
| js.Expression get value { |
| assert(isFinalized, 'TypeReference is unassigned'); |
| return _value; |
| } |
| |
| @override |
| bool get isFinalized => _value != null; |
| |
| // Precedence will be CALL or LEFT_HAND_SIDE depending on what expression the |
| // reference is resolved to. |
| @override |
| int get precedenceLevel => value.precedenceLevel; |
| |
| @override |
| TypeReference withSourceInformation( |
| js.JavaScriptNodeSourceInformation newSourceInformation) { |
| if (newSourceInformation == sourceInformation) return this; |
| if (newSourceInformation == null) return this; |
| return TypeReference._(typeRecipe, _value, newSourceInformation); |
| } |
| |
| @override |
| Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const []; |
| |
| @override |
| String nonfinalizedDebugText() { |
| TypeRecipe typeRecipe = this.typeRecipe; |
| if (typeRecipe is TypeExpressionRecipe) { |
| return 'TypeReference"${typeRecipe.type.toString()}"'; |
| } |
| return super.nonfinalizedDebugText(); |
| } |
| } |
| |
| /// A [TypeReferenceResource] is a deferred JavaScript statement determined by |
| /// the finalization of type references. It is the injection point for data or |
| /// code to support type references. For example, if the |
| /// [TypeReferenceFinalizer] decides that type should be referred to via a |
| /// variable, the [TypeReferenceResource] would be set to code that declares and |
| /// initializes the variable. |
| class TypeReferenceResource extends js.DeferredStatement |
| implements js.AstContainer { |
| js.Statement _statement; |
| |
| @override |
| final js.JavaScriptNodeSourceInformation sourceInformation; |
| |
| TypeReferenceResource() : sourceInformation = null; |
| TypeReferenceResource._(this._statement, this.sourceInformation); |
| |
| set statement(js.Statement statement) { |
| assert(!isFinalized && statement != null); |
| _statement = statement; |
| } |
| |
| @override |
| js.Statement get statement { |
| assert(isFinalized, 'TypeReferenceResource is unassigned'); |
| return _statement; |
| } |
| |
| @override |
| bool get isFinalized => _statement != null; |
| |
| @override |
| TypeReferenceResource withSourceInformation( |
| js.JavaScriptNodeSourceInformation newSourceInformation) { |
| if (newSourceInformation == sourceInformation) return this; |
| if (newSourceInformation == null) return this; |
| return TypeReferenceResource._(_statement, newSourceInformation); |
| } |
| |
| @override |
| Iterable<js.Node> get containedNodes => isFinalized ? [_statement] : const []; |
| |
| @override |
| void visitChildren<T>(js.NodeVisitor<T> visitor) { |
| _statement?.accept<T>(visitor); |
| } |
| |
| @override |
| void visitChildren1<R, A>(js.NodeVisitor1<R, A> visitor, A arg) { |
| _statement?.accept1<R, A>(visitor, arg); |
| } |
| } |
| |
| abstract class TypeReferenceFinalizer { |
| /// Collects TypeReference and TypeReferenceResource nodes from the JavaScript |
| /// AST [code]; |
| void addCode(js.Node code); |
| |
| /// Performs analysis on all collected TypeReference nodes finalizes the |
| /// values to expressions to access the types. |
| void finalize(); |
| } |
| |
| class TypeReferenceFinalizerImpl implements TypeReferenceFinalizer { |
| final Emitter _emitter; |
| final CommonElements _commonElements; |
| final RecipeEncoder _recipeEncoder; |
| final bool _minify; |
| |
| /*late final*/ _TypeReferenceCollectorVisitor _visitor; |
| TypeReferenceResource _resource; |
| |
| /// Maps the recipe (type expression) to the references with the same recipe. |
| /// Much of the algorithm's state is stored in the _ReferenceSet objects. |
| final Map<TypeRecipe, _ReferenceSet> _referencesByRecipe = {}; |
| |
| TypeReferenceFinalizerImpl( |
| this._emitter, this._commonElements, this._recipeEncoder, this._minify) { |
| _visitor = _TypeReferenceCollectorVisitor(this); |
| } |
| |
| @override |
| void addCode(js.Node code) { |
| code.accept(_visitor); |
| } |
| |
| @override |
| void finalize() { |
| assert(_resource != null, 'TypeReferenceFinalizer needs resource'); |
| _allocateNames(); |
| _updateReferences(); |
| } |
| |
| // Called from collector visitor. |
| void _registerTypeReference(TypeReference node) { |
| TypeRecipe recipe = node.typeRecipe; |
| _ReferenceSet refs = _referencesByRecipe[recipe] ??= _ReferenceSet(recipe); |
| refs.count++; |
| if (node.forConstant) refs.countInConstant++; |
| if (node.forLazyInitializer) refs.countInLazyInitializer++; |
| refs._references.add(node); |
| } |
| |
| // Called from collector visitor. |
| void _registerTypeReferenceResource(TypeReferenceResource node) { |
| assert(_resource == null); |
| _resource = node; |
| } |
| |
| void _updateReferences() { |
| js.Expression helperAccess = |
| _emitter.staticFunctionAccess(_commonElements.findType); |
| |
| js.Expression loadTypeCall(TypeRecipe recipe, String helperLocal) { |
| js.Expression recipeExpression = |
| _recipeEncoder.encodeGroundRecipe(_emitter, recipe); |
| return js.js(r'#(#)', [helperLocal ?? helperAccess, recipeExpression]); |
| } |
| |
| // Emit generate-at-use references. |
| for (_ReferenceSet referenceSet in _referencesByRecipe.values) { |
| if (referenceSet.generateAtUse) { |
| TypeRecipe recipe = referenceSet.recipe; |
| js.Expression reference = loadTypeCall(recipe, null); |
| for (TypeReference ref in referenceSet._references) { |
| ref.value = reference; |
| } |
| } |
| } |
| |
| List<_ReferenceSet> referenceSetsUsingProperties = |
| _referencesByRecipe.values.where((ref) => !ref.generateAtUse).toList(); |
| |
| // Sort by name (which is unique and mostly stable) so that similar recipes |
| // are grouped together. |
| referenceSetsUsingProperties.sort((a, b) => a.name.compareTo(b.name)); |
| |
| // We can generate a literal with calls to H.findType (minified to typically |
| // e.g. H.xy) or cache H.findType in a local in a scope created by an IIFE. |
| // Doing so saves 2-3 bytes per entry, but with an overhead of 30+ bytes for |
| // the IIFE. So it is smaller to use the IIFE only for over 10 or so types. |
| const minUseIIFE = 10; |
| String helperLocal = |
| referenceSetsUsingProperties.length < minUseIIFE ? null : 'findType'; |
| |
| List<js.Property> properties = []; |
| for (_ReferenceSet referenceSet in referenceSetsUsingProperties) { |
| TypeRecipe recipe = referenceSet.recipe; |
| var propertyName = js.string(referenceSet.propertyName); |
| properties |
| .add(js.Property(propertyName, loadTypeCall(recipe, helperLocal))); |
| var access = js.js('#.#', [typesHolderLocalName, propertyName]); |
| for (TypeReference ref in referenceSet._references) { |
| ref.value = access; |
| } |
| } |
| |
| if (properties.isEmpty) { |
| _resource.statement = js.Block.empty(); |
| } else { |
| js.Expression initializer = |
| js.ObjectInitializer(properties, isOneLiner: false); |
| if (helperLocal != null) { |
| // A named IIFE helps attribute startup time in profiling. |
| var function = js.js(r'function rtii(){var # = #; return #}', |
| [js.VariableDeclaration(helperLocal), helperAccess, initializer]); |
| initializer = js.js('#()', js.Parentheses(function)); |
| } |
| _resource.statement = js.js.statement(r'var # = #', |
| [js.VariableDeclaration(typesHolderLocalName), initializer]); |
| } |
| } |
| |
| // This is a top-level local name in the generated JavaScript top-level |
| // function, so will be minified automatically. The name should not collide |
| // with any other locals. |
| static const typesHolderLocalName = r'type$'; |
| |
| void _allocateNames() { |
| // Filter out generate-at-use cases and allocate unique names to the rest. |
| List<_ReferenceSet> referencesInTable = []; |
| Set<String> usedNames = {}; |
| for (_ReferenceSet referenceSet in _referencesByRecipe.values) { |
| // - If a type is used only once from a constant then the findType can be |
| // a subexpression of constant since it will be evaluated exactly once and |
| // need not be stored anywhere else. |
| if (referenceSet.count == 1 && referenceSet.countInConstant == 1) { |
| continue; |
| } |
| |
| // - Lazy initializers are usually evaluated on demand and only once, so |
| // it is worth deferrering evaluating the type references until the lazy |
| // initializer is executed, provided it does not increase program |
| // size too much. |
| // |
| // Assuming minification in a large program, the size for precomputed is |
| // |
| // abc:f("TTT"),/*once*/ + type$.abc/*N times*/ |
| // |
| // i.e. 13 + 5N. The size for repeated generate-at-use is |
| // |
| // H.lookupType("TTT") /*N times*/ |
| // |
| // i.e. 10N. Repeated is smaller when 10N < 13+5N, or N < 2.6. Since we |
| // get a startup benefit of not evaluating the type recipe, lets round |
| // up. Note that we don't know the size of the recipe ("TTT" assumes an |
| // infrequently referenced non-generic interface type), but if the recipe |
| // is larger, it is in a string so the program has the same number of |
| // tokens and the extra bytes will be parsed efficiently. |
| const int maxRepeatedLookups = 3; |
| |
| if (referenceSet.countInLazyInitializer == referenceSet.count && |
| referenceSet.count <= maxRepeatedLookups) { |
| continue; |
| } |
| |
| // TODO(sra): There are other contexts that would be beneficial, e.g. a |
| // type reference occuring only in a throw expression. |
| |
| String suggestedName = _RecipeToIdentifier().run(referenceSet.recipe); |
| if (usedNames.contains(suggestedName)) { |
| for (int i = 2; true; i++) { |
| String next = '${suggestedName}_$i'; |
| if (usedNames.contains(next)) continue; |
| suggestedName = next; |
| break; |
| } |
| } |
| usedNames.add(suggestedName); |
| referenceSet.name = suggestedName; |
| referencesInTable.add(referenceSet); |
| } |
| |
| if (!_minify && !_debugMinify) { |
| // For unminified code, use the characteristic names as property names. |
| // TODO(sra): Some of these names are long. We could truncate the names |
| // after the unique prefix. |
| for (_ReferenceSet referenceSet in referencesInTable) { |
| referenceSet.propertyName = referenceSet.name; |
| } |
| return; |
| } |
| |
| // Step 2. Sort by frequency to arrange common entries have shorter property |
| // names. |
| List<_ReferenceSet> referencesByFrequency = referencesInTable.toList() |
| ..sort((a, b) { |
| assert(a.name != b.name); |
| int r = b.count.compareTo(a.count); // Decreasing frequency. |
| if (r != 0) return r; |
| return a.name.compareTo(b.name); // Tie-break with characteristic name. |
| }); |
| |
| for (var referenceSet in referencesByFrequency) { |
| referenceSet.hash = _hashCharacteristicString(referenceSet.name); |
| } |
| |
| int hashOf(int index) => referencesByFrequency[index].hash; |
| int countOf(int index) => referencesByFrequency[index].count; |
| void assign(int index, String name) { |
| if (_minify) { |
| referencesByFrequency[index].propertyName = name; |
| } else { |
| var refSet = referencesByFrequency[index]; |
| refSet.propertyName = name + '_' + refSet.name; |
| } |
| } |
| |
| //naiveFrequencyAssignment( |
| semistableFrequencyAssignment(referencesByFrequency.length, |
| minifiedNameSequence(), hashOf, countOf, assign); |
| } |
| |
| static int _hashCharacteristicString(String s) { |
| int hash = 0; |
| for (int i = 0; i < s.length; i++) { |
| hash = Hashing.mixHashCodeBits(hash, s.codeUnitAt(i)); |
| } |
| return hash; |
| } |
| |
| /// Returns an infinite sequence of property names in increasing size. |
| static Iterable<String> minifiedNameSequence() sync* { |
| List<int> nextName = [$a]; |
| |
| /// Increments the letter at [pos] in the current name. Also takes care of |
| /// overflows to the left. Returns the carry bit, i.e., it returns `true` |
| /// if all positions to the left have wrapped around. |
| /// |
| /// If [nextName] is initially 'a', this will generate the sequence |
| /// |
| /// [a-zA-Z_] |
| /// [a-zA-Z_][0-9a-zA-Z_] |
| /// [a-zA-Z_][0-9a-zA-Z_][0-9a-zA-Z_] |
| /// ... |
| bool incrementPosition(int pos) { |
| bool overflow = false; |
| if (pos < 0) return true; |
| int value = nextName[pos]; |
| if (value == $9) { |
| value = $a; |
| } else if (value == $z) { |
| value = $A; |
| } else if (value == $Z) { |
| value = $_; |
| } else if (value == $_) { |
| overflow = incrementPosition(pos - 1); |
| value = (pos > 0) ? $0 : $a; |
| } else { |
| value++; |
| } |
| nextName[pos] = value; |
| return overflow; |
| } |
| |
| while (true) { |
| yield String.fromCharCodes(nextName); |
| if (incrementPosition(nextName.length - 1)) { |
| nextName.add($0); |
| } |
| } |
| } |
| } |
| |
| /// Set of references to a single recipe. |
| class _ReferenceSet { |
| final TypeRecipe recipe; |
| |
| // Number of times a TypeReference for [recipe] occurs in the tree-scan of the |
| // JavaScript ASTs. |
| int count = 0; |
| |
| // Number tree-scan occurrences in a constant initializer. |
| int countInConstant = 0; |
| // Number tree-scan occurrences in a static lazy initializer. |
| int countInLazyInitializer = 0; |
| |
| // It is possible for the JavaScript AST to be a DAG, so collect |
| // [TypeReference]s as set so we don't try to update one twice. |
| final Set<TypeReference> _references = Set.identity(); |
| |
| /// Characteristic name of the recipe - this can be used as a property name |
| /// for emitting unminified code, and as a stable hash source for minified |
| /// names. [name] is `null` if [recipe] should always be generated at use. |
| String name; |
| |
| /// Property name for 'indexing' into the precomputed types. |
| String propertyName; |
| |
| /// A stable hash code that can be used for picking stable minified names. |
| int hash = 0; |
| |
| _ReferenceSet(this.recipe); |
| |
| // If we don't assign a name it means we should not precompute the recipe. |
| bool get generateAtUse => name == null; |
| } |
| |
| /// Scans a JavaScript AST to collect all the TypeReference nodes. |
| /// |
| /// The state is kept in the finalizer so that this scan could be extended to |
| /// look for other deferred expressions in one pass. |
| class _TypeReferenceCollectorVisitor extends js.BaseVisitorVoid { |
| final TypeReferenceFinalizerImpl _finalizer; |
| |
| _TypeReferenceCollectorVisitor(this._finalizer); |
| |
| @override |
| void visitNode(js.Node node) { |
| assert(node is! TypeReference); |
| assert(node is! TypeReferenceResource); |
| if (node is js.AstContainer) { |
| for (js.Node element in node.containedNodes) { |
| element.accept(this); |
| } |
| } else { |
| super.visitNode(node); |
| } |
| } |
| |
| @override |
| void visitDeferredExpression(js.DeferredExpression node) { |
| if (node is TypeReference) { |
| _finalizer._registerTypeReference(node); |
| } else { |
| visitNode(node); |
| } |
| } |
| |
| @override |
| void visitDeferredStatement(js.DeferredStatement node) { |
| if (node is TypeReferenceResource) { |
| _finalizer._registerTypeReferenceResource(node); |
| } else { |
| visitNode(node); |
| } |
| } |
| } |
| |
| /// Returns a valid JavaScript identifier characterizing the recipe. The names |
| /// tend to look like the type expression with the non-identifier characters |
| /// removed. Separators 'of' and 'and' are sometimes used to separate complex |
| /// types. |
| /// |
| /// Map<int,int> "Map_int_int" |
| /// Map<int,List<int>> "Map_of_int_and_List_int" |
| /// |
| /// |
| /// For many common types the strings are unique, but this is not |
| /// guaranteed. This needs to be disambiguated at a higher level. |
| /// |
| /// Different types can have the same string if the types contain different |
| /// interface types with the same name (i.e. from different libraries), or types |
| /// with names that contain underscores or dollar signs. There is also some |
| /// ambiguity in the generated names in the interest of keeping most names |
| /// short, e.g. "FutureOr_int_Function" could be "FutureOr<int> Function()" or |
| /// "FutureOr<int Function()>". |
| class _RecipeToIdentifier extends DartTypeVisitor<void, DartType> { |
| final Map<DartType, int> _backrefs = Map.identity(); |
| final List<String> _fragments = []; |
| |
| String run(TypeRecipe recipe) { |
| if (recipe is TypeExpressionRecipe) { |
| _visit(recipe.type, null); |
| } else if (recipe is SingletonTypeEnvironmentRecipe) { |
| _add(r'$env'); |
| _visit(recipe.type, null); |
| } else if (recipe is FullTypeEnvironmentRecipe) { |
| _add(r'$env'); |
| if (recipe.classType != null) _visit(recipe.classType, null); |
| _add('${recipe.types.length}'); |
| int index = 0; |
| for (DartType type in recipe.types) { |
| ++index; |
| _add('${index}'); |
| _visit(type, null); |
| } |
| } else { |
| throw StateError('Unexpected recipe: $recipe'); |
| } |
| String result = _fragments.join('_'); |
| if (Namer.startsWithIdentifierCharacter(result)) return result; |
| return 'z' + result; |
| } |
| |
| void _add(String text) { |
| _fragments.add(text); |
| } |
| |
| void _identifier(String text) { |
| _add(Namer.replaceNonIdentifierCharacters(text)); |
| } |
| |
| bool _comma(bool needsComma) { |
| if (needsComma) _add('and'); |
| return true; |
| } |
| |
| void _visit(DartType type, DartType parent) { |
| type.accept(this, parent); |
| } |
| |
| @override |
| void visitLegacyType(covariant LegacyType type, _) { |
| _add('legacy'); |
| _visit(type.baseType, type); |
| } |
| |
| @override |
| void visitNullableType(covariant NullableType type, _) { |
| _add('nullable'); |
| _visit(type.baseType, type); |
| } |
| |
| @override |
| void visitNeverType(covariant NeverType type, _) { |
| _add('Never'); |
| } |
| |
| @override |
| void visitVoidType(covariant VoidType type, _) { |
| _add('void'); |
| } |
| |
| @override |
| void visitDynamicType(covariant DynamicType type, _) { |
| _add('dynamic'); |
| } |
| |
| @override |
| void visitErasedType(covariant ErasedType type, _) { |
| _add('erased'); |
| } |
| |
| @override |
| void visitAnyType(covariant AnyType type, _) { |
| _add('any'); |
| } |
| |
| @override |
| void visitTypeVariableType(covariant TypeVariableType type, DartType parent) { |
| _identifier(type.element.typeDeclaration.name); |
| _identifier(type.element.name); |
| } |
| |
| @override |
| void visitFunctionTypeVariable(covariant FunctionTypeVariable type, _) { |
| int index = type.index; |
| String name = index < 26 ? String.fromCharCode($A + index) : 'v\$${index}'; |
| _add(name); |
| } |
| |
| @override |
| void visitFunctionType(covariant FunctionType type, DartType parent) { |
| if (_dagCheck(type)) return; |
| |
| _visit(type.returnType, type); |
| _add('Function'); |
| var typeVariables = type.typeVariables; |
| if (typeVariables.isNotEmpty) { |
| bool needsComma = false; |
| for (FunctionTypeVariable typeVariable in typeVariables) { |
| needsComma = _comma(needsComma); |
| _visit(typeVariable, type); |
| DartType bound = typeVariable.bound; |
| if (!bound.isObject) { |
| _add('extends'); |
| _visit(typeVariable.bound, typeVariable); |
| } |
| } |
| } |
| var parameterTypes = type.parameterTypes; |
| var optionalParameterTypes = type.optionalParameterTypes; |
| var namedParameters = type.namedParameters; |
| var requiredNamedParameters = type.requiredNamedParameters; |
| |
| if (optionalParameterTypes.isEmpty && |
| namedParameters.isEmpty && |
| parameterTypes.every(_isSimple)) { |
| // e.g. "void_Function_int_int" |
| for (DartType parameterType in parameterTypes) { |
| _visit(parameterType, type); |
| } |
| return; |
| } |
| if (parameterTypes.length > 1) { |
| _add('${parameterTypes.length}'); |
| } |
| bool needsComma = false; |
| for (DartType parameterType in parameterTypes) { |
| needsComma = _comma(needsComma); |
| _visit(parameterType, type); |
| } |
| if (optionalParameterTypes.isNotEmpty) { |
| _add(r'$opt'); |
| bool needsOptionalComma = false; |
| for (DartType typeArgument in optionalParameterTypes) { |
| needsOptionalComma = _comma(needsOptionalComma); |
| _visit(typeArgument, type); |
| } |
| } |
| if (namedParameters.isNotEmpty) { |
| _add(r'$named'); |
| bool needsNamedComma = false; |
| for (int index = 0; index < namedParameters.length; index++) { |
| needsNamedComma = _comma(needsNamedComma); |
| if (requiredNamedParameters.contains(namedParameters[index])) { |
| _add(r'$req'); |
| } |
| _identifier(namedParameters[index]); |
| _visit(type.namedParameterTypes[index], type); |
| } |
| } |
| } |
| |
| @override |
| void visitInterfaceType(covariant InterfaceType type, _) { |
| var arguments = type.typeArguments; |
| |
| // Don't bother DAG-checking (generating back-ref encodings) for interface |
| // types which 'print' as a single identifier. |
| if (arguments.isNotEmpty && _dagCheck(type)) return; |
| |
| _identifier(type.element.name); |
| |
| if (arguments.isEmpty) return; |
| if (arguments.length == 1) { |
| // e.g. "List_of_int_Function" |
| if (arguments.first.withoutNullability is FunctionType) { |
| _add('of'); |
| } |
| // e.g. "List_int" |
| _visit(arguments.first, type); |
| return; |
| } |
| if (arguments.every(_isSimple)) { |
| // e.g. "Map_String_String" |
| for (DartType argument in arguments) { |
| _visit(argument, type); |
| } |
| return; |
| } |
| // e.g "Map_of_String_and_int_Function" |
| _add('of'); |
| bool needsComma = false; |
| for (DartType argument in arguments) { |
| needsComma = _comma(needsComma); |
| _visit(argument, type); |
| } |
| } |
| |
| bool _dagCheck(DartType type) { |
| int /*?*/ ref = _backrefs[type]; |
| if (ref != null) { |
| _add('\$$ref'); |
| return true; |
| } |
| _backrefs[type] = _backrefs.length; |
| return false; |
| } |
| |
| /// Returns `true` for types which print as a single identifier. |
| static bool _isSimple(DartType type) { |
| return type is DynamicType || |
| type is VoidType || |
| type is AnyType || |
| (type is InterfaceType && type.typeArguments.isEmpty); |
| } |
| |
| @override |
| void visitFutureOrType(covariant FutureOrType type, _) { |
| _identifier('FutureOr'); |
| _visit(type.typeArgument, type); |
| } |
| } |