blob: b05b84053b46dcb82d51ba1fbdc5e110ae9bf207 [file] [log] [blame]
// Copyright (c) 2018, 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.
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/transformations/constants.dart';
import 'package:kernel/type_environment.dart';
import 'kernel_helpers.dart';
/// Implements constant evaluation for dev compiler:
///
/// [isConstant] determines if an expression is constant.
/// [evaluate] computes the value of a constant expression, if available.
class DevCompilerConstants {
final _ConstantVisitor _visitor;
final _ConstantEvaluator _evaluator;
DevCompilerConstants(
TypeEnvironment types, Map<String, String> declaredVariables)
: _visitor = _ConstantVisitor(types.coreTypes),
_evaluator = _ConstantEvaluator(types, declaredVariables);
/// Determines if an expression is constant.
bool isConstant(Expression e) => _visitor.isConstant(e);
/// Evaluates [e] to find its constant value, returning `null` if evaluation
/// failed, or if the constant was unavailable.
///
/// Returns [NullConstant] to represent the `null` value.
///
/// To avoid performance costs associated with try+catch on invalid constant
/// evaluation, call this after [isConstant] is known to be true.
Constant evaluate(Expression e, {bool cache = false}) {
if (e == null) return null;
try {
var result = cache ? _evaluator.evaluate(e) : e.accept(_evaluator);
return identical(result, _evaluator.unavailableConstant) ? null : result;
} on _AbortCurrentEvaluation {
// TODO(jmesserly): the try+catch is necessary because the front end is
// not issuing sufficient errors, so the constant evaluation can fail.
//
// It can also be caused by methods in the evaluator that don't understand
// unavailable constants.
return null;
} on NoSuchMethodError {
// TODO(jmesserly): this is probably the same issue as above, but verify
// that it's fixed once Kernel does constant evaluation.
return null;
}
}
/// If [node] is an annotation with a field named `name`, returns that field's
/// value.
///
/// This assumes the `name` field is populated from a named argument `name:`
/// or from the first positional argument.
///
/// For example:
///
/// class MyAnnotation {
/// final String name;
/// // ...
/// const MyAnnotation(this.name/*, ... other params ... */);
/// }
///
/// @MyAnnotation('FooBar')
/// main() { ... }
///
/// Given the node for `@MyAnnotation('FooBar')` this will return `'FooBar'`.
String getNameFromAnnotation(ConstructorInvocation node) {
if (node == null) return null;
// TODO(jmesserly): this does not use the normal evaluation engine, because
// it won't work if we don't have the const constructor body available.
//
// We may need to address this in the kernel outline files.
Expression first;
var named = node.arguments.named;
if (named.isNotEmpty) {
first =
named.firstWhere((n) => n.name == 'name', orElse: () => null)?.value;
}
var positional = node.arguments.positional;
if (positional.isNotEmpty) first ??= positional[0];
if (first != null) {
first = _followConstFields(first);
if (first is StringLiteral) return first.value;
}
return null;
}
Expression _followConstFields(Expression expr) {
if (expr is StaticGet) {
var target = expr.target;
if (target is Field) {
return _followConstFields(target.initializer);
}
}
return expr;
}
}
/// Finds constant expressions as defined in Dart language spec 4th ed,
/// 16.1 Constants.
class _ConstantVisitor extends ExpressionVisitor<bool> {
final CoreTypes coreTypes;
_ConstantVisitor(this.coreTypes);
bool isConstant(Expression e) => e.accept(this);
defaultExpression(node) => false;
defaultBasicLiteral(node) => true;
visitTypeLiteral(node) => true; // TODO(jmesserly): deferred libraries?
visitSymbolLiteral(node) => true;
visitListLiteral(node) => node.isConst;
visitMapLiteral(node) => node.isConst;
visitStaticInvocation(node) {
return node.isConst ||
node.target == coreTypes.identicalProcedure &&
node.arguments.positional.every(isConstant) ||
isFromEnvironmentInvocation(coreTypes, node) &&
node.arguments.positional.every(isConstant) &&
node.arguments.named.every((n) => isConstant(n.value));
}
visitDirectMethodInvocation(node) {
return node.receiver is BasicLiteral &&
isOperatorMethodName(node.name.name) &&
node.arguments.positional.every((p) => p is BasicLiteral);
}
visitMethodInvocation(node) {
return node.receiver is BasicLiteral &&
isOperatorMethodName(node.name.name) &&
node.arguments.positional.every((p) => p is BasicLiteral);
}
visitConstructorInvocation(node) => node.isConst;
visitStringConcatenation(node) =>
node.expressions.every((e) => e is BasicLiteral);
visitStaticGet(node) {
var target = node.target;
return target is Procedure || target is Field && target.isConst;
}
visitVariableGet(node) => node.variable.isConst;
visitNot(node) {
var operand = node.operand;
return operand is BoolLiteral ||
operand is DirectMethodInvocation &&
visitDirectMethodInvocation(operand) ||
operand is MethodInvocation && visitMethodInvocation(operand);
}
visitLogicalExpression(node) =>
node.left is BoolLiteral && node.right is BoolLiteral;
visitConditionalExpression(node) =>
node.condition is BoolLiteral &&
node.then is BoolLiteral &&
node.otherwise is BoolLiteral;
visitLet(Let node) {
var init = node.variable.initializer;
return (init == null || isConstant(init)) && isConstant(node.body);
}
}
/// The visitor that evaluates constants, building on Kernel's
/// [ConstantEvaluator] class (used by the VM) and fixing some of its behavior
/// to work better for DDC.
//
// TODO(jmesserly): make some changes in the base class to make it a better fit
// for compilers like DDC?
class _ConstantEvaluator extends ConstantEvaluator {
final Map<String, String> declaredVariables;
/// Used to denote an unavailable constant value from another module
///
// TODO(jmesserly): this happens when we try to evaluate constant values from
// an external library, that was from an outline kernel file. The kernel file
// does not contain the initializer value of the constant.
final Constant unavailableConstant;
_ConstantEvaluator(TypeEnvironment types, this.declaredVariables,
{bool enableAsserts: false})
: unavailableConstant = InstanceConstant(null, [], {}),
super(_ConstantsBackend(types.coreTypes), types, types.coreTypes,
enableAsserts, const _ErrorReporter()) {
env = EvaluationEnvironment();
}
@override
visitVariableGet(node) {
// The base evaluator expects that variable declarations are visited during
// the transformation step, so it doesn't handle constant variables.
// Instead handle them here.
if (node.variable.isConst) {
return evaluate(node.variable.initializer);
}
// Fall back to the base evaluator for other cases (e.g. parameters of a
// constant constructor).
return super.visitVariableGet(node);
}
@override
visitStaticGet(StaticGet node) {
// Handle unavailable field constants. This happens if an external library
// only has its outline available.
var target = node.target;
if (target is Field &&
target.isConst &&
target.isInExternalLibrary &&
target.initializer == null) {
return unavailableConstant;
}
return super.visitStaticGet(node);
}
@override
visitConstructorInvocation(ConstructorInvocation node) {
// Handle unavailable constructor bodies.
// This happens if an external library only has its outline available.
var target = node.target;
if (target.isConst &&
target.isInExternalLibrary &&
target.function.body is EmptyStatement &&
target.initializers.isEmpty) {
return unavailableConstant;
}
return super.visitConstructorInvocation(node);
}
@override
visitStaticInvocation(node) {
// Handle int/bool/String.fromEnvironment constructors.
//
// (The VM handles this via its `native` calls and implements it in
// VmConstantsBackend.buildConstantForNative.)
var target = node.target;
if (isFromEnvironmentInvocation(coreTypes, node)) {
var firstArg = evaluatePositionalArguments(node.arguments)[0];
var defaultArg = evaluateNamedArguments(node.arguments)['defaultValue'];
var varName = (firstArg as StringConstant).value;
var value = declaredVariables[varName];
var targetClass = target.enclosingClass;
if (targetClass == coreTypes.stringClass) {
if (value != null) return canonicalize(StringConstant(value));
return defaultArg ?? nullConstant;
} else if (targetClass == coreTypes.intClass) {
var intValue = int.tryParse(value ?? '');
if (intValue != null) return canonicalize(IntConstant(intValue));
return defaultArg ?? nullConstant;
} else if (targetClass == coreTypes.boolClass) {
if (value == "true") return trueConstant;
if (value == "false") return falseConstant;
return defaultArg ?? falseConstant;
}
}
return super.visitStaticInvocation(node);
}
@override
evaluateBinaryNumericOperation(String op, num a, num b, TreeNode node) {
// Use doubles to match JS number semantics.
return super
.evaluateBinaryNumericOperation(op, a.toDouble(), b.toDouble(), node);
}
@override
canonicalize(Constant constant) {
if (constant is DoubleConstant) {
// Convert to an integer when possible (matching the runtime behavior
// of `is int`).
var d = constant.value;
if (d.isFinite) {
var i = d.toInt();
if (d == i.toDouble()) return super.canonicalize(IntConstant(i));
}
}
return super.canonicalize(constant);
}
}
/// Implement the class for compiler specific behavior.
///
/// This is mostly unused by DDC, because we don't use the global constant
/// transformer.
class _ConstantsBackend implements ConstantsBackend {
final CoreTypes coreTypes;
final Field symbolNameField;
_ConstantsBackend(this.coreTypes)
: symbolNameField = coreTypes.internalSymbolClass.fields
.firstWhere((f) => f.name.name == '_name');
@override
buildConstantForNative(nativeName, typeArguments, positionalArguments,
namedArguments, context, node, errorReporter, abortEvaluation) {
throw StateError('unreachable'); // DDC does not use VM native syntax
}
@override
lowerMapConstant(constant) => constant;
@override
lowerListConstant(constant) => constant;
}
class _ErrorReporter extends SimpleErrorReporter {
const _ErrorReporter();
@override
report(context, message, node) => throw const _AbortCurrentEvaluation();
}
// TODO(jmesserly): this class is private in Kernel constants library, so
// we have our own version.
class _AbortCurrentEvaluation {
const _AbortCurrentEvaluation();
}