| // Copyright (c) 2017, 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. |
| |
| /// This library implements a kernel2kernel constant evaluation transformation. |
| /// |
| /// Even though it is expected that the frontend does not emit kernel AST which |
| /// contains compile-time errors, this transformation still performs some |
| /// valiation and throws a [ConstantEvaluationError] if there was a compile-time |
| /// errors. |
| /// |
| /// Due to the lack information which is is only available in the front-end, |
| /// this validation is incomplete (e.g. whether an integer literal used the |
| /// hexadecimal syntax or not). |
| /// |
| /// Furthermore due to the lowering of certain constructs in the front-end |
| /// (e.g. '??') we need to support a super-set of the normal constant expression |
| /// language. Issue(http://dartbug.com/31799) |
| library kernel.transformations.constants; |
| |
| import 'dart:io' as io; |
| |
| import '../ast.dart'; |
| import '../class_hierarchy.dart'; |
| import '../core_types.dart'; |
| import '../external_name.dart' show getExternalName; |
| import '../kernel.dart'; |
| import '../type_algebra.dart'; |
| import '../type_environment.dart'; |
| |
| Component transformComponent(Component component, ConstantsBackend backend, |
| {bool keepFields: false, |
| bool strongMode: false, |
| bool enableAsserts: false, |
| bool evaluateAnnotations: true, |
| CoreTypes coreTypes, |
| ClassHierarchy hierarchy, |
| ErrorReporter errorReporter: const _SimpleErrorReporter()}) { |
| coreTypes ??= new CoreTypes(component); |
| hierarchy ??= new ClassHierarchy(component); |
| |
| final typeEnvironment = |
| new TypeEnvironment(coreTypes, hierarchy, strongMode: strongMode); |
| |
| transformLibraries(component.libraries, backend, coreTypes, typeEnvironment, |
| keepFields: keepFields, |
| strongMode: strongMode, |
| enableAsserts: enableAsserts, |
| evaluateAnnotations: evaluateAnnotations, |
| errorReporter: errorReporter); |
| return component; |
| } |
| |
| void transformLibraries(List<Library> libraries, ConstantsBackend backend, |
| CoreTypes coreTypes, TypeEnvironment typeEnvironment, |
| {bool keepFields: false, |
| bool keepVariables: false, |
| bool evaluateAnnotations: true, |
| bool strongMode: false, |
| bool enableAsserts: false, |
| ErrorReporter errorReporter: const _SimpleErrorReporter()}) { |
| final ConstantsTransformer constantsTransformer = new ConstantsTransformer( |
| backend, |
| keepFields, |
| keepVariables, |
| evaluateAnnotations, |
| coreTypes, |
| typeEnvironment, |
| strongMode, |
| enableAsserts, |
| errorReporter); |
| for (final Library library in libraries) { |
| constantsTransformer.convertLibrary(library); |
| } |
| } |
| |
| class ConstantsTransformer extends Transformer { |
| final ConstantEvaluator constantEvaluator; |
| final CoreTypes coreTypes; |
| final TypeEnvironment typeEnvironment; |
| |
| /// Whether to preserve constant [Field]s. All use-sites will be rewritten. |
| final bool keepFields; |
| final bool keepVariables; |
| final bool evaluateAnnotations; |
| |
| ConstantsTransformer( |
| ConstantsBackend backend, |
| this.keepFields, |
| this.keepVariables, |
| this.evaluateAnnotations, |
| this.coreTypes, |
| this.typeEnvironment, |
| bool strongMode, |
| bool enableAsserts, |
| ErrorReporter errorReporter) |
| : constantEvaluator = new ConstantEvaluator(backend, typeEnvironment, |
| coreTypes, strongMode, enableAsserts, errorReporter); |
| |
| // Transform the library/class members: |
| |
| void convertLibrary(Library library) { |
| transformAnnotations(library.annotations, library); |
| |
| transformList(library.dependencies, this, library); |
| transformList(library.parts, this, library); |
| transformList(library.typedefs, this, library); |
| transformList(library.classes, this, library); |
| transformList(library.procedures, this, library); |
| transformList(library.fields, this, library); |
| |
| if (!keepFields) { |
| // The transformer API does not iterate over `Library.additionalExports`, |
| // so we manually delete the references to shaken nodes. |
| library.additionalExports.removeWhere((Reference reference) { |
| return reference.canonicalName == null; |
| }); |
| } |
| } |
| |
| visitLibraryPart(LibraryPart node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| }); |
| return node; |
| } |
| |
| visitLibraryDependency(LibraryDependency node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| }); |
| return node; |
| } |
| |
| visitClass(Class node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| transformList(node.fields, this, node); |
| transformList(node.typeParameters, this, node); |
| transformList(node.constructors, this, node); |
| transformList(node.procedures, this, node); |
| transformList(node.redirectingFactoryConstructors, this, node); |
| }); |
| return node; |
| } |
| |
| visitProcedure(Procedure node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| node.function = node.function.accept(this)..parent = node; |
| }); |
| return node; |
| } |
| |
| visitConstructor(Constructor node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| transformList(node.initializers, this, node); |
| node.function = node.function.accept(this)..parent = node; |
| }); |
| return node; |
| } |
| |
| visitTypedef(Typedef node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| }); |
| return node; |
| } |
| |
| visitRedirectingFactoryConstructor(RedirectingFactoryConstructor node) { |
| constantEvaluator.withNewEnvironment(() { |
| transformAnnotations(node.annotations, node); |
| transformList(node.typeParameters, this, node); |
| transformList(node.positionalParameters, this, node); |
| transformList(node.namedParameters, this, node); |
| }); |
| return node; |
| } |
| |
| visitTypeParameter(TypeParameter node) { |
| transformAnnotations(node.annotations, node); |
| return node; |
| } |
| |
| void transformAnnotations(List<Expression> nodes, TreeNode parent) { |
| if (evaluateAnnotations && nodes.length > 0) { |
| transformExpressions(nodes, parent); |
| } |
| } |
| |
| void transformExpressions(List<Expression> nodes, TreeNode parent) { |
| constantEvaluator.withNewEnvironment(() { |
| for (int i = 0; i < nodes.length; ++i) { |
| nodes[i] = tryEvaluateAndTransformWithContext(parent, nodes[i]) |
| ..parent = parent; |
| } |
| }); |
| } |
| |
| // Handle definition of constants: |
| |
| visitFunctionNode(FunctionNode node) { |
| final positionalParameterCount = node.positionalParameters.length; |
| for (int i = node.requiredParameterCount; |
| i < positionalParameterCount; |
| ++i) { |
| final VariableDeclaration variable = node.positionalParameters[i]; |
| if (variable.initializer != null) { |
| variable.initializer = |
| tryEvaluateAndTransformWithContext(variable, variable.initializer) |
| ..parent = node; |
| } |
| } |
| for (final VariableDeclaration variable in node.namedParameters) { |
| if (variable.initializer != null) { |
| variable.initializer = |
| tryEvaluateAndTransformWithContext(variable, variable.initializer) |
| ..parent = node; |
| } |
| } |
| if (node.body != null) { |
| node.body = node.body.accept(this)..parent = node; |
| } |
| return node; |
| } |
| |
| visitVariableDeclaration(VariableDeclaration node) { |
| transformAnnotations(node.annotations, node); |
| |
| if (node.isConst) { |
| final Constant constant = tryEvaluateWithContext(node, node.initializer); |
| |
| // If there was a constant evaluation error we will not continue and |
| // simply keep the old [node]. |
| if (constant == null) { |
| return node; |
| } |
| |
| constantEvaluator.env.addVariableValue(node, constant); |
| |
| if (keepVariables) { |
| // So the value of the variable is still available for debugging |
| // purposes we convert the constant variable to be a final variable |
| // initialized to the evaluated constant expression. |
| node.initializer = new ConstantExpression(constant)..parent = node; |
| node.isFinal = true; |
| node.isConst = false; |
| } else { |
| // Since we convert all use-sites of constants, the constant |
| // [VariableDeclaration] is unused and we'll therefore remove it. |
| return null; |
| } |
| } |
| if (node.initializer != null) { |
| node.initializer = node.initializer.accept(this)..parent = node; |
| } |
| return node; |
| } |
| |
| visitField(Field node) { |
| return constantEvaluator.withNewEnvironment(() { |
| if (node.isConst) { |
| // Since we convert all use-sites of constants, the constant [Field] |
| // cannot be referenced anymore. We therefore get rid of it if |
| // [keepFields] was not specified. |
| if (!keepFields) { |
| return null; |
| } |
| |
| // Otherwise we keep the constant [Field] and convert it's initializer. |
| transformAnnotations(node.annotations, node); |
| if (node.initializer != null) { |
| node.initializer = |
| tryEvaluateAndTransformWithContext(node, node.initializer) |
| ..parent = node; |
| } |
| } else { |
| transformAnnotations(node.annotations, node); |
| if (node.initializer != null) { |
| node.initializer = node.initializer.accept(this)..parent = node; |
| } |
| } |
| return node; |
| }); |
| } |
| |
| // Handle use-sites of constants (and "inline" constant expressions): |
| |
| visitSymbolLiteral(SymbolLiteral node) { |
| return new ConstantExpression(constantEvaluator.evaluate(node)); |
| } |
| |
| visitStaticGet(StaticGet node) { |
| final Member target = node.target; |
| if (target is Field && target.isConst) { |
| final Constant constant = |
| tryEvaluateWithContext(node, target.initializer); |
| return constant != null ? new ConstantExpression(constant) : node; |
| } else if (target is Procedure && target.kind == ProcedureKind.Method) { |
| return tryEvaluateAndTransformWithContext(node, node); |
| } |
| return super.visitStaticGet(node); |
| } |
| |
| visitSwitchCase(SwitchCase node) { |
| transformExpressions(node.expressions, node); |
| return super.visitSwitchCase(node); |
| } |
| |
| visitVariableGet(VariableGet node) { |
| if (node.variable.isConst) { |
| return tryEvaluateAndTransformWithContext(node, node); |
| } |
| return super.visitVariableGet(node); |
| } |
| |
| visitListLiteral(ListLiteral node) { |
| if (node.isConst) { |
| return tryEvaluateAndTransformWithContext(node, node); |
| } |
| return super.visitListLiteral(node); |
| } |
| |
| visitMapLiteral(MapLiteral node) { |
| if (node.isConst) { |
| return tryEvaluateAndTransformWithContext(node, node); |
| } |
| return super.visitMapLiteral(node); |
| } |
| |
| visitConstructorInvocation(ConstructorInvocation node) { |
| if (node.isConst) { |
| return tryEvaluateAndTransformWithContext(node, node); |
| } |
| return super.visitConstructorInvocation(node); |
| } |
| |
| tryEvaluateAndTransformWithContext(TreeNode treeContext, Expression node) { |
| final Constant constant = tryEvaluateWithContext(treeContext, node); |
| return constant != null ? new ConstantExpression(constant) : node; |
| } |
| |
| tryEvaluateWithContext(TreeNode treeContext, Expression node) { |
| if (treeContext == node) { |
| try { |
| return constantEvaluator.evaluate(node); |
| } on _AbortCurrentEvaluation catch (_) { |
| return null; |
| } |
| } |
| |
| return constantEvaluator.runInsideContext(treeContext, () { |
| try { |
| return constantEvaluator.evaluate(node); |
| } on _AbortCurrentEvaluation catch (_) { |
| return null; |
| } |
| }); |
| } |
| } |
| |
| class ConstantEvaluator extends RecursiveVisitor { |
| final ConstantsBackend backend; |
| final CoreTypes coreTypes; |
| final TypeEnvironment typeEnvironment; |
| final bool strongMode; |
| final bool enableAsserts; |
| final ErrorReporter errorReporter; |
| |
| final Map<Constant, Constant> canonicalizationCache; |
| final Map<Node, Object> nodeCache; |
| |
| final NullConstant nullConstant = new NullConstant(); |
| final BoolConstant trueConstant = new BoolConstant(true); |
| final BoolConstant falseConstant = new BoolConstant(false); |
| |
| final List<TreeNode> contextChain = []; |
| |
| InstanceBuilder instanceBuilder; |
| EvaluationEnvironment env; |
| |
| ConstantEvaluator(this.backend, this.typeEnvironment, this.coreTypes, |
| this.strongMode, this.enableAsserts, |
| [this.errorReporter = const _SimpleErrorReporter()]) |
| : canonicalizationCache = <Constant, Constant>{}, |
| nodeCache = <Node, Constant>{}; |
| |
| /// Evaluates [node] and possibly cache the evaluation result. |
| Constant evaluate(Expression node) { |
| if (node == null) return nullConstant; |
| if (env.isEmpty) { |
| // We only try to evaluate the same [node] *once* within an empty |
| // environment. |
| if (nodeCache.containsKey(node)) { |
| final Constant constant = nodeCache[node]; |
| if (constant == null) throw const _AbortCurrentEvaluation(); |
| return constant; |
| } |
| |
| nodeCache[node] = null; |
| return nodeCache[node] = node.accept(this); |
| } |
| return node.accept(this); |
| } |
| |
| Constant runInsideContext(TreeNode node, Constant fun()) { |
| try { |
| pushContext(node); |
| return fun(); |
| } finally { |
| popContext(node); |
| } |
| } |
| |
| Constant runInsideContextIfNoContext(TreeNode node, Constant fun()) { |
| if (contextChain.isEmpty) { |
| return runInsideContext(node, fun); |
| } else { |
| return fun(); |
| } |
| } |
| |
| pushContext(TreeNode contextNode) { |
| contextChain.add(contextNode); |
| } |
| |
| popContext(TreeNode contextNode) { |
| assert(contextChain.last == contextNode); |
| contextChain.length = contextChain.length - 1; |
| } |
| |
| defaultTreeNode(Node node) { |
| // Only a subset of the expression language is valid for constant |
| // evaluation. |
| throw 'Constant evaluation has no support for ${node.runtimeType} yet!'; |
| } |
| |
| visitNullLiteral(NullLiteral node) => nullConstant; |
| |
| visitBoolLiteral(BoolLiteral node) { |
| return node.value ? trueConstant : falseConstant; |
| } |
| |
| visitIntLiteral(IntLiteral node) { |
| // The frontend will ensure the integer literals are in signed 64-bit range |
| // in strong mode. |
| return canonicalize(new IntConstant(node.value)); |
| } |
| |
| visitDoubleLiteral(DoubleLiteral node) { |
| return canonicalize(new DoubleConstant(node.value)); |
| } |
| |
| visitStringLiteral(StringLiteral node) { |
| return canonicalize(new StringConstant(node.value)); |
| } |
| |
| visitTypeLiteral(TypeLiteral node) { |
| final DartType type = evaluateDartType(node.type); |
| return canonicalize(new TypeLiteralConstant(type)); |
| } |
| |
| visitConstantExpression(ConstantExpression node) { |
| // If there were already constants in the AST then we make sure we |
| // re-canonicalize them. After running the transformer we will therefore |
| // have a fully-canonicalized constant DAG with roots coming from the |
| // [ConstantExpression] nodes in the AST. |
| return canonicalize(node.constant); |
| } |
| |
| visitListLiteral(ListLiteral node) { |
| if (!node.isConst) { |
| errorReporter.nonConstLiteral(contextChain, node, 'List'); |
| throw const _AbortCurrentEvaluation(); |
| } |
| final List<Constant> entries = new List<Constant>(node.expressions.length); |
| for (int i = 0; i < node.expressions.length; ++i) { |
| entries[i] = node.expressions[i].accept(this); |
| } |
| final DartType typeArgument = evaluateDartType(node.typeArgument); |
| final ListConstant listConstant = new ListConstant(typeArgument, entries); |
| return canonicalize(backend.lowerListConstant(listConstant)); |
| } |
| |
| visitMapLiteral(MapLiteral node) { |
| if (!node.isConst) { |
| errorReporter.nonConstLiteral(contextChain, node, 'Map'); |
| throw const _AbortCurrentEvaluation(); |
| } |
| final Set<Constant> usedKeys = new Set<Constant>(); |
| final List<ConstantMapEntry> entries = |
| new List<ConstantMapEntry>(node.entries.length); |
| for (int i = 0; i < node.entries.length; ++i) { |
| final key = node.entries[i].key.accept(this); |
| final value = node.entries[i].value.accept(this); |
| if (!usedKeys.add(key)) { |
| // TODO(kustermann): We should change the context handling from just |
| // capturing the `TreeNode`s to a `(TreeNode, String message)` tuple and |
| // report where the first key with the same value was. |
| errorReporter.duplicateKey(contextChain, node.entries[i], key); |
| throw const _AbortCurrentEvaluation(); |
| } |
| entries[i] = new ConstantMapEntry(key, value); |
| } |
| final DartType keyType = evaluateDartType(node.keyType); |
| final DartType valueType = evaluateDartType(node.valueType); |
| final MapConstant mapConstant = |
| new MapConstant(keyType, valueType, entries); |
| return canonicalize(backend.lowerMapConstant(mapConstant)); |
| } |
| |
| visitFunctionExpression(FunctionExpression node) { |
| errorReporter.nonConstLiteral(contextChain, node, 'Function'); |
| throw const _AbortCurrentEvaluation(); |
| } |
| |
| visitConstructorInvocation(ConstructorInvocation node) { |
| final Constructor constructor = node.target; |
| final Class klass = constructor.enclosingClass; |
| if (!constructor.isConst) { |
| throw 'The front-end should ensure we do not encounter a ' |
| 'constructor invocation of a non-const constructor.'; |
| } |
| if (constructor.function.body != null && |
| constructor.function.body is! EmptyStatement) { |
| throw 'Constructor "$node" has non-trivial body "${constructor.function.body.runtimeType}".'; |
| } |
| if (klass.isAbstract) { |
| throw 'Constructor "$node" belongs to abstract class "${klass}".'; |
| } |
| |
| final typeArguments = evaluateTypeArguments(node.arguments); |
| final positionals = evaluatePositionalArguments(node.arguments); |
| final named = evaluateNamedArguments(node.arguments); |
| |
| // Fill in any missing type arguments with "dynamic". |
| for (int i = typeArguments.length; i < klass.typeParameters.length; i++) { |
| typeArguments.add(const DynamicType()); |
| } |
| |
| // Start building a new instance. |
| return withNewInstanceBuilder(klass, typeArguments, () { |
| return runInsideContextIfNoContext(node, () { |
| // "Run" the constructor (and any super constructor calls), which will |
| // initialize the fields of the new instance. |
| handleConstructorInvocation( |
| constructor, typeArguments, positionals, named); |
| return canonicalize(instanceBuilder.buildInstance()); |
| }); |
| }); |
| } |
| |
| handleConstructorInvocation( |
| Constructor constructor, |
| List<DartType> typeArguments, |
| List<Constant> positionalArguments, |
| Map<String, Constant> namedArguments) { |
| return runInsideContext(constructor, () { |
| return withNewEnvironment(() { |
| final Class klass = constructor.enclosingClass; |
| final FunctionNode function = constructor.function; |
| |
| // We simulate now the constructor invocation. |
| |
| // Step 1) Map type arguments and normal arguments from caller to callee. |
| for (int i = 0; i < klass.typeParameters.length; i++) { |
| env.addTypeParameterValue(klass.typeParameters[i], typeArguments[i]); |
| } |
| for (int i = 0; i < function.positionalParameters.length; i++) { |
| final VariableDeclaration parameter = |
| function.positionalParameters[i]; |
| final Constant value = (i < positionalArguments.length) |
| ? positionalArguments[i] |
| : evaluate(parameter.initializer); |
| env.addVariableValue(parameter, value); |
| } |
| for (final VariableDeclaration parameter in function.namedParameters) { |
| final Constant value = |
| namedArguments[parameter.name] ?? evaluate(parameter.initializer); |
| env.addVariableValue(parameter, value); |
| } |
| |
| // Step 2) Run all initializers (including super calls) with environment setup. |
| for (final Field field in klass.fields) { |
| if (!field.isStatic) { |
| instanceBuilder.setFieldValue(field, evaluate(field.initializer)); |
| } |
| } |
| for (final Initializer init in constructor.initializers) { |
| if (init is FieldInitializer) { |
| instanceBuilder.setFieldValue(init.field, evaluate(init.value)); |
| } else if (init is LocalInitializer) { |
| final VariableDeclaration variable = init.variable; |
| env.addVariableValue(variable, evaluate(variable.initializer)); |
| } else if (init is SuperInitializer) { |
| handleConstructorInvocation( |
| init.target, |
| evaluateSuperTypeArguments( |
| constructor.enclosingClass.supertype), |
| evaluatePositionalArguments(init.arguments), |
| evaluateNamedArguments(init.arguments)); |
| } else if (init is RedirectingInitializer) { |
| // Since a redirecting constructor targets a constructor of the same |
| // class, we pass the same [typeArguments]. |
| handleConstructorInvocation( |
| init.target, |
| typeArguments, |
| evaluatePositionalArguments(init.arguments), |
| evaluateNamedArguments(init.arguments)); |
| } else if (init is AssertInitializer) { |
| if (enableAsserts) { |
| final Constant condition = init.statement.condition.accept(this); |
| |
| if (condition is BoolConstant) { |
| if (!condition.value) { |
| final Constant message = init.statement.message?.accept(this); |
| if (message == null) { |
| errorReporter.failedAssertion( |
| contextChain, init.statement.condition, null); |
| throw const _AbortCurrentEvaluation(); |
| } else if (message is StringConstant) { |
| errorReporter.failedAssertion( |
| contextChain, init.statement.condition, message.value); |
| throw const _AbortCurrentEvaluation(); |
| } |
| errorReporter.invalidDartType( |
| contextChain, |
| init.statement.message, |
| message, |
| typeEnvironment.stringType); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } else { |
| errorReporter.invalidDartType( |
| contextChain, |
| init.statement.condition, |
| condition, |
| typeEnvironment.boolType); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| } else { |
| throw new Exception( |
| 'No support for handling initializer of type "${init.runtimeType}".'); |
| } |
| } |
| }); |
| }); |
| } |
| |
| visitMethodInvocation(MethodInvocation node) { |
| // We have no support for generic method invocation atm. |
| assert(node.arguments.named.isEmpty); |
| |
| final Constant receiver = evaluate(node.receiver); |
| final List<Constant> arguments = |
| evaluatePositionalArguments(node.arguments); |
| |
| // TODO(http://dartbug.com/31799): Ensure we only invoke ==/!= on |
| // null/bool/int/double/String objects. |
| |
| // Handle == and != first (it's common between all types). |
| if (arguments.length == 1 && node.name.name == '==') { |
| final right = arguments[0]; |
| return receiver == right ? trueConstant : falseConstant; |
| } |
| if (arguments.length == 1 && node.name.name == '!=') { |
| final right = arguments[0]; |
| return receiver != right ? trueConstant : falseConstant; |
| } |
| |
| // This is a white-listed set of methods we need to support on constants. |
| if (receiver is StringConstant) { |
| if (arguments.length == 1) { |
| switch (node.name.name) { |
| case '+': |
| final Constant other = arguments[0]; |
| if (other is StringConstant) { |
| return canonicalize( |
| new StringConstant(receiver.value + other.value)); |
| } |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| receiver, |
| '+', |
| typeEnvironment.stringType, |
| other.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| } else if (receiver is BoolConstant) { |
| if (arguments.length == 1) { |
| switch (node.name.name) { |
| case '!': |
| return !receiver.value ? trueConstant : falseConstant; |
| } |
| } else if (arguments.length == 2) { |
| final right = arguments[0]; |
| if (right is BoolConstant) { |
| switch (node.name.name) { |
| case '&&': |
| return (receiver.value && right.value) |
| ? trueConstant |
| : falseConstant; |
| case '||': |
| return (receiver.value || right.value) |
| ? trueConstant |
| : falseConstant; |
| } |
| } |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| receiver, |
| '${node.name.name}', |
| typeEnvironment.boolType, |
| right.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } else if (receiver is IntConstant) { |
| if (arguments.length == 0) { |
| switch (node.name.name) { |
| case 'unary-': |
| return canonicalize(new IntConstant(-receiver.value)); |
| case '~': |
| return canonicalize(new IntConstant(~receiver.value)); |
| } |
| } else if (arguments.length == 1) { |
| final Constant other = arguments[0]; |
| final op = node.name.name; |
| if (other is IntConstant) { |
| if ((op == '<<' || op == '>>') && other.value < 0) { |
| errorReporter.negativeShift(contextChain, |
| node.arguments.positional.first, receiver, op, other); |
| throw const _AbortCurrentEvaluation(); |
| } |
| switch (op) { |
| case '|': |
| return canonicalize( |
| new IntConstant(receiver.value | other.value)); |
| case '&': |
| return canonicalize( |
| new IntConstant(receiver.value & other.value)); |
| case '^': |
| return canonicalize( |
| new IntConstant(receiver.value ^ other.value)); |
| case '<<': |
| return canonicalize( |
| new IntConstant(receiver.value << other.value)); |
| case '>>': |
| return canonicalize( |
| new IntConstant(receiver.value >> other.value)); |
| } |
| } |
| |
| if (other is IntConstant) { |
| if (other.value == 0 && (op == '%' || op == '~/')) { |
| errorReporter.zeroDivisor( |
| contextChain, node.arguments.positional.first, receiver, op); |
| throw const _AbortCurrentEvaluation(); |
| } |
| |
| return evaluateBinaryNumericOperation( |
| node.name.name, receiver.value, other.value, node); |
| } else if (other is DoubleConstant) { |
| return evaluateBinaryNumericOperation( |
| node.name.name, receiver.value, other.value, node); |
| } |
| |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| receiver, |
| '${node.name.name}', |
| typeEnvironment.numType, |
| other.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } else if (receiver is DoubleConstant) { |
| if (arguments.length == 0) { |
| switch (node.name.name) { |
| case 'unary-': |
| return canonicalize(new DoubleConstant(-receiver.value)); |
| } |
| } else if (arguments.length == 1) { |
| final Constant other = arguments[0]; |
| |
| if (other is IntConstant || other is DoubleConstant) { |
| final num value = (other is IntConstant) |
| ? other.value |
| : (other as DoubleConstant).value; |
| return evaluateBinaryNumericOperation( |
| node.name.name, receiver.value, value, node); |
| } |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| receiver, |
| '${node.name.name}', |
| typeEnvironment.numType, |
| other.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| errorReporter.invalidMethodInvocation( |
| contextChain, node, receiver, node.name.name); |
| throw const _AbortCurrentEvaluation(); |
| } |
| |
| visitLogicalExpression(LogicalExpression node) { |
| final Constant left = evaluate(node.left); |
| switch (node.operator) { |
| case '||': |
| if (left is BoolConstant) { |
| if (left.value) return trueConstant; |
| |
| final Constant right = evaluate(node.right); |
| if (right is BoolConstant) { |
| return right; |
| } |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| left, |
| '${node.operator}', |
| typeEnvironment.boolType, |
| right.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| errorReporter.invalidMethodInvocation( |
| contextChain, node, left, '${node.operator}'); |
| throw const _AbortCurrentEvaluation(); |
| case '&&': |
| if (left is BoolConstant) { |
| if (!left.value) return falseConstant; |
| |
| final Constant right = evaluate(node.right); |
| if (right is BoolConstant) { |
| return right; |
| } |
| errorReporter.invalidBinaryOperandType( |
| contextChain, |
| node, |
| left, |
| '${node.operator}', |
| typeEnvironment.boolType, |
| right.getType(typeEnvironment)); |
| throw const _AbortCurrentEvaluation(); |
| } |
| errorReporter.invalidMethodInvocation( |
| contextChain, node, left, '${node.operator}'); |
| throw const _AbortCurrentEvaluation(); |
| case '??': |
| return (left is! NullConstant) ? left : evaluate(node.right); |
| default: |
| errorReporter.invalidMethodInvocation( |
| contextChain, node, left, '${node.operator}'); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| |
| visitConditionalExpression(ConditionalExpression node) { |
| final Constant constant = evaluate(node.condition); |
| if (constant == trueConstant) { |
| return evaluate(node.then); |
| } else if (constant == falseConstant) { |
| return evaluate(node.otherwise); |
| } else { |
| errorReporter.invalidDartType( |
| contextChain, node, constant, typeEnvironment.boolType); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| |
| visitPropertyGet(PropertyGet node) { |
| if (node.receiver is ThisExpression) { |
| // Access "this" during instance creation. |
| for (final Field field in instanceBuilder.fields.keys) { |
| if (field.name == node.name) { |
| return instanceBuilder.fields[field]; |
| } |
| } |
| throw 'Could not evaluate field get ${node.name} on incomplete instance'; |
| } |
| |
| final Constant receiver = evaluate(node.receiver); |
| if (receiver is StringConstant && node.name.name == 'length') { |
| return canonicalize(new IntConstant(receiver.value.length)); |
| } else if (receiver is InstanceConstant) { |
| for (final Reference fieldRef in receiver.fieldValues.keys) { |
| if (fieldRef.asField.name == node.name) { |
| return receiver.fieldValues[fieldRef]; |
| } |
| } |
| } |
| throw 'Could not evaluate property get on $receiver.'; |
| } |
| |
| visitLet(Let node) { |
| env.addVariableValue(node.variable, evaluate(node.variable.initializer)); |
| return node.body.accept(this); |
| } |
| |
| visitVariableGet(VariableGet node) { |
| // Not every variable which a [VariableGet] refers to must be marked as |
| // constant. For example function parameters as well as constructs |
| // desugared to [Let] expressions are ok. |
| // |
| // TODO(kustermann): The heuristic of allowing all [VariableGet]s on [Let] |
| // variables might allow more than it should. |
| final VariableDeclaration variable = node.variable; |
| if (variable.parent is Let || _isFormalParameter(variable)) { |
| final Constant constant = env.lookupVariable(node.variable); |
| if (constant == null) { |
| errorReporter.nonConstantVariableGet(contextChain, node, variable.name); |
| throw const _AbortCurrentEvaluation(); |
| } |
| return constant; |
| } |
| if (variable.isConst) { |
| return evaluate(variable.initializer); |
| } |
| throw new Exception('The front-end should ensure we do not encounter a ' |
| 'variable get of a non-const variable.'); |
| } |
| |
| visitStaticGet(StaticGet node) { |
| return withNewEnvironment(() { |
| final Member target = node.target; |
| if (target is Field && target.isConst) { |
| return runInsideContext(target, () { |
| return evaluate(target.initializer); |
| }); |
| } else if (target is Procedure) { |
| if (target.kind == ProcedureKind.Method) { |
| return canonicalize(new TearOffConstant(target)); |
| } |
| errorReporter.invalidStaticInvocation(contextChain, node, target); |
| throw const _AbortCurrentEvaluation(); |
| } else { |
| throw new Exception( |
| 'No support for ${target.runtimeType} in a static-get.'); |
| } |
| }); |
| } |
| |
| visitStringConcatenation(StringConcatenation node) { |
| final String value = node.expressions.map((Expression node) { |
| final Constant constant = node.accept(this); |
| |
| if (constant is NullConstant) { |
| return 'null'; |
| } else if (constant is BoolConstant) { |
| return constant.value ? 'true' : 'false'; |
| } else if (constant is IntConstant) { |
| return constant.value.toString(); |
| } else if (constant is DoubleConstant) { |
| return constant.value.toString(); |
| } else if (constant is StringConstant) { |
| return constant.value; |
| } else { |
| errorReporter.invalidStringInterpolationOperand( |
| contextChain, node, constant); |
| throw const _AbortCurrentEvaluation(); |
| } |
| }).join(''); |
| return canonicalize(new StringConstant(value)); |
| } |
| |
| visitStaticInvocation(StaticInvocation node) { |
| final Procedure target = node.target; |
| if (target.kind == ProcedureKind.Factory) { |
| final String nativeName = getExternalName(target); |
| if (nativeName != null) { |
| final Constant constant = backend.buildConstantForNative( |
| nativeName, |
| evaluateTypeArguments(node.arguments), |
| evaluatePositionalArguments(node.arguments), |
| evaluateNamedArguments(node.arguments)); |
| assert(constant != null); |
| return canonicalize(constant); |
| } |
| } else if (target.name.name == 'identical') { |
| // Ensure the "identical()" function comes from dart:core. |
| final parent = target.parent; |
| if (parent is Library && parent == coreTypes.coreLibrary) { |
| final positionalArguments = evaluatePositionalArguments(node.arguments); |
| final Constant left = positionalArguments[0]; |
| final Constant right = positionalArguments[1]; |
| // Since we canonicalize constants during the evaluation, we can use |
| // identical here. |
| assert(left == right); |
| return identical(left, right) ? trueConstant : falseConstant; |
| } |
| } |
| errorReporter.invalidStaticInvocation(contextChain, node, target); |
| throw const _AbortCurrentEvaluation(); |
| } |
| |
| visitAsExpression(AsExpression node) { |
| final Constant constant = node.operand.accept(this); |
| ensureIsSubtype(constant, evaluateDartType(node.type), node); |
| return constant; |
| } |
| |
| visitNot(Not node) { |
| final Constant constant = node.operand.accept(this); |
| if (constant is BoolConstant) { |
| return constant == trueConstant ? falseConstant : trueConstant; |
| } |
| errorReporter.invalidDartType( |
| contextChain, node, constant, typeEnvironment.boolType); |
| throw const _AbortCurrentEvaluation(); |
| } |
| |
| visitSymbolLiteral(SymbolLiteral node) { |
| final value = canonicalize(new StringConstant(node.value)); |
| return canonicalize(backend.buildSymbolConstant(value)); |
| } |
| |
| visitInstantiation(Instantiation node) { |
| final Constant constant = evaluate(node.expression); |
| if (constant is TearOffConstant) { |
| if (node.typeArguments.length == |
| constant.procedure.function.typeParameters.length) { |
| return canonicalize( |
| new PartialInstantiationConstant(constant, node.typeArguments)); |
| } |
| throw new Exception( |
| 'The number of type arguments supplied in the partial instantiation ' |
| 'does not match the number of type arguments of the $constant.'); |
| } |
| throw new Exception( |
| 'Only tear-off constants can be partially instantiated.'); |
| } |
| |
| // Helper methods: |
| |
| void ensureIsSubtype(Constant constant, DartType type, TreeNode node) { |
| DartType constantType; |
| if (constant is NullConstant) { |
| constantType = new InterfaceType(coreTypes.nullClass); |
| } else if (constant is BoolConstant) { |
| constantType = new InterfaceType(coreTypes.boolClass); |
| } else if (constant is IntConstant) { |
| constantType = new InterfaceType(coreTypes.intClass); |
| } else if (constant is DoubleConstant) { |
| constantType = new InterfaceType(coreTypes.doubleClass); |
| } else if (constant is StringConstant) { |
| constantType = new InterfaceType(coreTypes.stringClass); |
| } else if (constant is MapConstant) { |
| constantType = new InterfaceType( |
| coreTypes.mapClass, <DartType>[constant.keyType, constant.valueType]); |
| } else if (constant is ListConstant) { |
| constantType = new InterfaceType( |
| coreTypes.stringClass, <DartType>[constant.typeArgument]); |
| } else if (constant is InstanceConstant) { |
| constantType = new InterfaceType(constant.klass, constant.typeArguments); |
| } else if (constant is TearOffConstant) { |
| constantType = constant.procedure.function.functionType; |
| } else if (constant is TypeLiteralConstant) { |
| constantType = new InterfaceType(coreTypes.typeClass); |
| } else { |
| throw new Exception('No support for ${constant.runtimeType}.runtimeType'); |
| } |
| |
| if (!typeEnvironment.isSubtypeOf(constantType, type)) { |
| errorReporter.invalidDartType(contextChain, node, constant, type); |
| throw const _AbortCurrentEvaluation(); |
| } |
| } |
| |
| List<DartType> evaluateTypeArguments(Arguments arguments) { |
| return evaluateDartTypes(arguments.types); |
| } |
| |
| List<DartType> evaluateSuperTypeArguments(Supertype type) { |
| return evaluateDartTypes(type.typeArguments); |
| } |
| |
| List<DartType> evaluateDartTypes(List<DartType> types) { |
| if (env.isEmpty) return types; |
| return types.map(evaluateDartType).toList(); |
| } |
| |
| DartType evaluateDartType(DartType type) { |
| return env.subsituteType(type); |
| } |
| |
| List<Constant> evaluatePositionalArguments(Arguments arguments) { |
| return arguments.positional.map((Expression node) { |
| return node.accept(this) as Constant; |
| }).toList(); |
| } |
| |
| Map<String, Constant> evaluateNamedArguments(Arguments arguments) { |
| if (arguments.named.isEmpty) return const <String, Constant>{}; |
| |
| final Map<String, Constant> named = {}; |
| arguments.named.forEach((NamedExpression pair) { |
| named[pair.name] = pair.value.accept(this); |
| }); |
| return named; |
| } |
| |
| canonicalize(Constant constant) { |
| return canonicalizationCache.putIfAbsent(constant, () => constant); |
| } |
| |
| withNewInstanceBuilder(Class klass, List<DartType> typeArguments, fn()) { |
| InstanceBuilder old = instanceBuilder; |
| try { |
| instanceBuilder = new InstanceBuilder(klass, typeArguments); |
| return fn(); |
| } finally { |
| instanceBuilder = old; |
| } |
| } |
| |
| withNewEnvironment(fn()) { |
| final EvaluationEnvironment oldEnv = env; |
| try { |
| env = new EvaluationEnvironment(); |
| return fn(); |
| } finally { |
| env = oldEnv; |
| } |
| } |
| |
| evaluateBinaryNumericOperation(String op, num a, num b, TreeNode node) { |
| num result; |
| switch (op) { |
| case '+': |
| result = a + b; |
| break; |
| case '-': |
| result = a - b; |
| break; |
| case '*': |
| result = a * b; |
| break; |
| case '/': |
| result = a / b; |
| break; |
| case '~/': |
| result = a ~/ b; |
| break; |
| case '%': |
| result = a % b; |
| break; |
| } |
| |
| if (result != null) { |
| return canonicalize(result is int |
| ? new IntConstant(_wrapAroundInteger(result)) |
| : new DoubleConstant(result as double)); |
| } |
| |
| switch (op) { |
| case '<': |
| return a < b ? trueConstant : falseConstant; |
| case '<=': |
| return a <= b ? trueConstant : falseConstant; |
| case '>=': |
| return a >= b ? trueConstant : falseConstant; |
| case '>': |
| return a > b ? trueConstant : falseConstant; |
| } |
| |
| throw new Exception("Unexpected binary numeric operation '$op'."); |
| } |
| |
| int _wrapAroundInteger(int value) { |
| if (strongMode) { |
| return value.toSigned(64); |
| } |
| return value; |
| } |
| } |
| |
| /// Holds the necessary information for a constant object, namely |
| /// * the [klass] being instantiated |
| /// * the [typeArguments] used for the instantiation |
| /// * the [fields] the instance will obtain (all fields from the |
| /// instantiated [klass] up to the [Object] klass). |
| class InstanceBuilder { |
| /// The class of the new instance. |
| final Class klass; |
| |
| /// The values of the type parameters of the new instance. |
| final List<DartType> typeArguments; |
| |
| /// The field values of the new instance. |
| final Map<Field, Constant> fields = <Field, Constant>{}; |
| |
| InstanceBuilder(this.klass, this.typeArguments); |
| |
| void setFieldValue(Field field, Constant constant) { |
| fields[field] = constant; |
| } |
| |
| InstanceConstant buildInstance() { |
| final Map<Reference, Constant> fieldValues = <Reference, Constant>{}; |
| fields.forEach((Field field, Constant value) { |
| fieldValues[field.reference] = value; |
| }); |
| return new InstanceConstant(klass.reference, typeArguments, fieldValues); |
| } |
| } |
| |
| /// Holds an environment of type parameters, parameters and variables. |
| class EvaluationEnvironment { |
| /// The values of the type parameters in scope. |
| final Map<TypeParameter, DartType> _typeVariables = |
| <TypeParameter, DartType>{}; |
| |
| /// The values of the parameters/variables in scope. |
| final Map<VariableDeclaration, Constant> _variables = |
| <VariableDeclaration, Constant>{}; |
| |
| /// Whether the current environment is empty. |
| bool get isEmpty => _typeVariables.isEmpty && _variables.isEmpty; |
| |
| void addTypeParameterValue(TypeParameter parameter, DartType value) { |
| assert(!_typeVariables.containsKey(parameter)); |
| _typeVariables[parameter] = value; |
| } |
| |
| void addVariableValue(VariableDeclaration variable, Constant value) { |
| assert(!_variables.containsKey(variable)); |
| _variables[variable] = value; |
| } |
| |
| DartType lookupParameterValue(TypeParameter parameter) { |
| final DartType value = _typeVariables[parameter]; |
| assert(value != null); |
| return value; |
| } |
| |
| Constant lookupVariable(VariableDeclaration variable) { |
| return _variables[variable]; |
| } |
| |
| DartType subsituteType(DartType type) { |
| if (_typeVariables.isEmpty) return type; |
| return substitute(type, _typeVariables); |
| } |
| } |
| |
| abstract class ConstantsBackend { |
| Constant buildConstantForNative( |
| String nativeName, |
| List<DartType> typeArguments, |
| List<Constant> positionalArguments, |
| Map<String, Constant> namedArguments); |
| Constant buildSymbolConstant(StringConstant value); |
| |
| Constant lowerListConstant(ListConstant constant); |
| Constant lowerMapConstant(MapConstant constant); |
| } |
| |
| // Used as control-flow to abort the current evaluation. |
| class _AbortCurrentEvaluation { |
| const _AbortCurrentEvaluation(); |
| } |
| |
| abstract class ErrorReporter { |
| const ErrorReporter(); |
| |
| invalidDartType(List<TreeNode> context, TreeNode node, Constant receiver, |
| DartType expectedType); |
| invalidBinaryOperandType(List<TreeNode> context, TreeNode node, |
| Constant receiver, String op, DartType expectedType, DartType actualType); |
| invalidMethodInvocation( |
| List<TreeNode> context, TreeNode node, Constant receiver, String op); |
| invalidStaticInvocation( |
| List<TreeNode> context, TreeNode node, Procedure target); |
| invalidStringInterpolationOperand( |
| List<TreeNode> context, TreeNode node, Constant constant); |
| zeroDivisor( |
| List<TreeNode> context, TreeNode node, IntConstant receiver, String op); |
| negativeShift(List<TreeNode> context, TreeNode node, IntConstant receiver, |
| String op, IntConstant argument); |
| nonConstLiteral(List<TreeNode> context, TreeNode node, String klass); |
| duplicateKey(List<TreeNode> context, TreeNode node, Constant key); |
| failedAssertion(List<TreeNode> context, TreeNode node, String message); |
| nonConstantVariableGet( |
| List<TreeNode> context, TreeNode node, String variableName); |
| } |
| |
| abstract class ErrorReporterBase implements ErrorReporter { |
| const ErrorReporterBase(); |
| |
| report(List<TreeNode> context, String message, TreeNode node); |
| |
| getFileUri(TreeNode node) { |
| while (node is! FileUriNode) { |
| node = node.parent; |
| } |
| return (node as FileUriNode).fileUri; |
| } |
| |
| getFileOffset(TreeNode node) { |
| while (node.fileOffset == TreeNode.noOffset) { |
| node = node.parent; |
| } |
| return node == null ? TreeNode.noOffset : node.fileOffset; |
| } |
| |
| invalidDartType(List<TreeNode> context, TreeNode node, Constant receiver, |
| DartType expectedType) { |
| report( |
| context, |
| 'Expected expression to evaluate to "$expectedType" but got "$receiver.', |
| node); |
| } |
| |
| invalidBinaryOperandType( |
| List<TreeNode> context, |
| TreeNode node, |
| Constant receiver, |
| String op, |
| DartType expectedType, |
| DartType actualType) { |
| report( |
| context, |
| 'Calling "$op" on "$receiver" needs operand of type ' |
| '"$expectedType" (but got "$actualType")', |
| node); |
| } |
| |
| invalidMethodInvocation( |
| List<TreeNode> context, TreeNode node, Constant receiver, String op) { |
| report(context, 'Cannot call "$op" on "$receiver" in constant expression', |
| node); |
| } |
| |
| invalidStaticInvocation( |
| List<TreeNode> context, TreeNode node, Procedure target) { |
| report( |
| context, 'Cannot invoke "$target" inside a constant expression', node); |
| } |
| |
| invalidStringInterpolationOperand( |
| List<TreeNode> context, TreeNode node, Constant constant) { |
| report( |
| context, |
| 'Only null/bool/int/double/String values are allowed as string ' |
| 'interpolation expressions during constant evaluation (was: "$constant").', |
| node); |
| } |
| |
| zeroDivisor( |
| List<TreeNode> context, TreeNode node, IntConstant receiver, String op) { |
| report( |
| context, |
| "Binary operator '$op' on '${receiver.value}' requires non-zero " |
| "divisor, but divisor was '0'.", |
| node); |
| } |
| |
| negativeShift(List<TreeNode> context, TreeNode node, IntConstant receiver, |
| String op, IntConstant argument) { |
| report( |
| context, |
| "Binary operator '$op' on '${receiver.value}' requires non-negative " |
| "operand, but was '${argument.value}'.", |
| node); |
| } |
| |
| nonConstLiteral(List<TreeNode> context, TreeNode node, String klass) { |
| report( |
| context, |
| 'Cannot have a non-constant $klass literal within a const context.', |
| node); |
| } |
| |
| duplicateKey(List<TreeNode> context, TreeNode node, Constant key) { |
| report( |
| context, |
| 'Duplicate keys are not allowed in constant maps (found duplicate key "$key")', |
| node); |
| } |
| |
| failedAssertion(List<TreeNode> context, TreeNode node, String message) { |
| report( |
| context, |
| 'The assertion condition evaluated to "false" with message "$message"', |
| node); |
| } |
| |
| nonConstantVariableGet( |
| List<TreeNode> context, TreeNode node, String variableName) { |
| report( |
| context, |
| 'The variable "$variableName" cannot be used inside a constant ' |
| 'expression.', |
| node); |
| } |
| } |
| |
| class _SimpleErrorReporter extends ErrorReporterBase { |
| const _SimpleErrorReporter(); |
| |
| report(List<TreeNode> context, String message, TreeNode node) { |
| io.exitCode = 42; |
| final Uri uri = getFileUri(node); |
| final int fileOffset = getFileOffset(node); |
| |
| io.stderr.writeln('$uri:$fileOffset Constant evaluation error: $message'); |
| } |
| } |
| |
| bool _isFormalParameter(VariableDeclaration variable) { |
| final parent = variable.parent; |
| if (parent is FunctionNode) { |
| return parent.positionalParameters.contains(variable) || |
| parent.namedParameters.contains(variable); |
| } |
| return false; |
| } |