// 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 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 nodes, TreeNode parent) { if (evaluateAnnotations && nodes.length > 0) { transformExpressions(nodes, parent); } } void transformExpressions(List 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) { return constantEvaluator.evaluate(node); } return constantEvaluator.runInsideContext(treeContext, () { return constantEvaluator.evaluate(node); }); } } class ConstantEvaluator extends RecursiveVisitor { final ConstantsBackend backend; final CoreTypes coreTypes; final TypeEnvironment typeEnvironment; final bool strongMode; final bool enableAsserts; final ErrorReporter errorReporter; final Map canonicalizationCache; final Map nodeCache; final NullConstant nullConstant = new NullConstant(); final BoolConstant trueConstant = new BoolConstant(true); final BoolConstant falseConstant = new BoolConstant(false); final List contextChain = []; InstanceBuilder instanceBuilder; EvaluationEnvironment env; ConstantEvaluator(this.backend, this.typeEnvironment, this.coreTypes, this.strongMode, this.enableAsserts, [this.errorReporter = const _SimpleErrorReporter()]) : canonicalizationCache = {}, nodeCache = {}; /// Evaluates [node] and possibly cache the evaluation result. /// Returns `null` if expression can't be evaluated. Constant evaluate(Expression node) { try { return _evaluateSubexpression(node); } on _AbortCurrentEvaluation catch (_) { return null; } } /// Evaluates [node] and possibly cache the evaluation result. /// @throws _AbortCurrentEvaluation if expression can't be evaluated. Constant _evaluateSubexpression(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 entries = new List(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 usedKeys = new Set(); final List entries = new List(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 typeArguments, List positionalArguments, Map 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] : _evaluateSubexpression(parameter.initializer); env.addVariableValue(parameter, value); } for (final VariableDeclaration parameter in function.namedParameters) { final Constant value = namedArguments[parameter.name] ?? _evaluateSubexpression(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, _evaluateSubexpression(field.initializer)); } } for (final Initializer init in constructor.initializers) { if (init is FieldInitializer) { instanceBuilder.setFieldValue( init.field, _evaluateSubexpression(init.value)); } else if (init is LocalInitializer) { final VariableDeclaration variable = init.variable; env.addVariableValue( variable, _evaluateSubexpression(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 = _evaluateSubexpression(node.receiver); final List 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 = _evaluateSubexpression(node.left); switch (node.operator) { case '||': if (left is BoolConstant) { if (left.value) return trueConstant; final Constant right = _evaluateSubexpression(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 = _evaluateSubexpression(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 : _evaluateSubexpression(node.right); default: errorReporter.invalidMethodInvocation( contextChain, node, left, '${node.operator}'); throw const _AbortCurrentEvaluation(); } } visitConditionalExpression(ConditionalExpression node) { final Constant constant = _evaluateSubexpression(node.condition); if (constant == trueConstant) { return _evaluateSubexpression(node.then); } else if (constant == falseConstant) { return _evaluateSubexpression(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 = _evaluateSubexpression(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, _evaluateSubexpression(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 _evaluateSubexpression(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 _evaluateSubexpression(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 libraryReference = node.value.startsWith('_') ? libraryOf(node).reference : null; return canonicalize(new SymbolConstant(node.value, libraryReference)); } visitInstantiation(Instantiation node) { final Constant constant = _evaluateSubexpression(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.'); } @override visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) { errorReporter.deferredLibrary(contextChain, node, node.import.name); throw const _AbortCurrentEvaluation(); } // 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, [constant.keyType, constant.valueType]); } else if (constant is ListConstant) { constantType = new InterfaceType( coreTypes.stringClass, [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 evaluateTypeArguments(Arguments arguments) { return evaluateDartTypes(arguments.types); } List evaluateSuperTypeArguments(Supertype type) { return evaluateDartTypes(type.typeArguments); } List evaluateDartTypes(List types) { if (env.isEmpty) return types; return types.map(evaluateDartType).toList(); } DartType evaluateDartType(DartType type) { return env.subsituteType(type); } List evaluatePositionalArguments(Arguments arguments) { return arguments.positional.map((Expression node) { return node.accept(this) as Constant; }).toList(); } Map evaluateNamedArguments(Arguments arguments) { if (arguments.named.isEmpty) return const {}; final Map 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 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; } Library libraryOf(TreeNode node) { // The tree structure of the kernel AST ensures we always have an enclosing // library. while (true) { if (node is Library) return node; node = node.parent; } } } /// 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 typeArguments; /// The field values of the new instance. final Map fields = {}; InstanceBuilder(this.klass, this.typeArguments); void setFieldValue(Field field, Constant constant) { fields[field] = constant; } InstanceConstant buildInstance() { final Map fieldValues = {}; 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 _typeVariables = {}; /// The values of the parameters/variables in scope. final Map _variables = {}; /// 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 typeArguments, List positionalArguments, Map namedArguments); 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 context, TreeNode node, Constant receiver, DartType expectedType); invalidBinaryOperandType(List context, TreeNode node, Constant receiver, String op, DartType expectedType, DartType actualType); invalidMethodInvocation( List context, TreeNode node, Constant receiver, String op); invalidStaticInvocation( List context, TreeNode node, Procedure target); invalidStringInterpolationOperand( List context, TreeNode node, Constant constant); zeroDivisor( List context, TreeNode node, IntConstant receiver, String op); negativeShift(List context, TreeNode node, IntConstant receiver, String op, IntConstant argument); nonConstLiteral(List context, TreeNode node, String klass); duplicateKey(List context, TreeNode node, Constant key); failedAssertion(List context, TreeNode node, String message); nonConstantVariableGet( List context, TreeNode node, String variableName); deferredLibrary(List context, TreeNode node, String importName); } abstract class ErrorReporterBase implements ErrorReporter { const ErrorReporterBase(); report(List 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 context, TreeNode node, Constant receiver, DartType expectedType) { report( context, 'Expected expression to evaluate to "$expectedType" but got "$receiver.', node); } invalidBinaryOperandType( List 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 context, TreeNode node, Constant receiver, String op) { report(context, 'Cannot call "$op" on "$receiver" in constant expression', node); } invalidStaticInvocation( List context, TreeNode node, Procedure target) { report( context, 'Cannot invoke "$target" inside a constant expression', node); } invalidStringInterpolationOperand( List 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 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 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 context, TreeNode node, String klass) { report( context, 'Cannot have a non-constant $klass literal within a const context.', node); } duplicateKey(List context, TreeNode node, Constant key) { report( context, 'Duplicate keys are not allowed in constant maps (found duplicate key "$key")', node); } failedAssertion(List context, TreeNode node, String message) { report( context, 'The assertion condition evaluated to "false" with message "$message"', node); } nonConstantVariableGet( List context, TreeNode node, String variableName) { report( context, 'The variable "$variableName" cannot be used inside a constant ' 'expression.', node); } deferredLibrary(List context, TreeNode node, String importName) { report( context, 'Deferred "$importName" cannot be used inside a constant ' 'expression', node); } } class _SimpleErrorReporter extends ErrorReporterBase { const _SimpleErrorReporter(); report(List 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; }