Index: pkg/compiler/lib/src/cps_ir/type_propagation.dart |
diff --git a/pkg/compiler/lib/src/cps_ir/type_propagation.dart b/pkg/compiler/lib/src/cps_ir/type_propagation.dart |
index 027a3d67a972c19c2f67004f01160ababc442565..615b4c44f0d1e369ca9844b8650da20fe6b9debc 100644 |
--- a/pkg/compiler/lib/src/cps_ir/type_propagation.dart |
+++ b/pkg/compiler/lib/src/cps_ir/type_propagation.dart |
@@ -38,10 +38,16 @@ class TypeMaskSystem { |
TypeMask get mapType => inferrer.mapType; |
TypeMask get nonNullType => inferrer.nonNullType; |
+ TypeMask numStringBoolType; |
+ |
// TODO(karlklose): remove compiler here. |
TypeMaskSystem(dart2js.Compiler compiler) |
: inferrer = compiler.typesTask, |
- classWorld = compiler.world; |
+ classWorld = compiler.world { |
+ numStringBoolType = |
+ new TypeMask.unionOf(<TypeMask>[numType, stringType, boolType], |
+ classWorld); |
+ } |
TypeMask getParameterType(ParameterElement parameter) { |
return inferrer.getGuaranteedTypeOfElement(parameter); |
@@ -51,8 +57,8 @@ class TypeMaskSystem { |
return inferrer.getGuaranteedReturnTypeOfElement(function); |
} |
- TypeMask getSelectorReturnType(Selector selector) { |
- return inferrer.getGuaranteedTypeOfSelector(selector); |
+ TypeMask getInvokeReturnType(Selector typedSelector) { |
+ return inferrer.getGuaranteedTypeOfSelector(typedSelector); |
} |
TypeMask getFieldType(FieldElement field) { |
@@ -67,19 +73,37 @@ class TypeMaskSystem { |
return computeTypeMask(inferrer.compiler, constant); |
} |
- TypeMask exact(ClassElement element) { |
+ TypeMask nonNullExact(ClassElement element) { |
// The class world does not know about classes created by |
// closure conversion, so just treat those as a subtypes of Function. |
// TODO(asgerf): Maybe closure conversion should create a new ClassWorld? |
if (element.isClosure) return functionType; |
- return new TypeMask.exact(element, classWorld); |
+ return new TypeMask.nonNullExact(element, classWorld); |
+ } |
+ |
+ bool isDefinitelyBool(TypeMask t, {bool allowNull: false}) { |
+ if (!allowNull && t.isNullable) return false; |
+ return t.containsOnlyBool(classWorld); |
+ } |
+ |
+ bool isDefinitelyNum(TypeMask t, {bool allowNull: false}) { |
+ if (!allowNull && t.isNullable) return false; |
+ return t.containsOnlyNum(classWorld); |
} |
- bool isDefinitelyBool(TypeMask t) { |
- return t.containsOnlyBool(classWorld) && !t.isNullable; |
+ bool isDefinitelyString(TypeMask t, {bool allowNull: false}) { |
+ if (!allowNull && t.isNullable) return false; |
+ return t.containsOnlyString(classWorld); |
} |
- bool isDefinitelyNotNull(TypeMask t) => !t.isNullable; |
+ bool isDefinitelyNumStringBool(TypeMask t, {bool allowNull: false}) { |
+ if (!allowNull && t.isNullable) return false; |
+ return numStringBoolType.containsMask(t, classWorld); |
+ } |
+ |
+ bool isDefinitelyNotNumStringBool(TypeMask t) { |
+ return areDisjoint(t, numStringBoolType); |
+ } |
bool areDisjoint(TypeMask leftType, TypeMask rightType) { |
TypeMask intersection = leftType.intersection(rightType, classWorld); |
@@ -91,7 +115,6 @@ class TypeMaskSystem { |
{bool allowNull}) { |
assert(allowNull != null); |
if (type is types.DynamicType) { |
- if (!allowNull && value.isNullable) return AbstractBool.Maybe; |
return AbstractBool.True; |
} |
if (type is types.InterfaceType) { |
@@ -162,15 +185,33 @@ class ConstantPropagationLattice { |
} |
/// True if all members of this value are booleans. |
- bool isDefinitelyBool(AbstractValue value) { |
- return value.isNothing || typeSystem.isDefinitelyBool(value.type); |
+ bool isDefinitelyBool(AbstractValue value, {bool allowNull: false}) { |
+ return value.isNothing || |
+ typeSystem.isDefinitelyBool(value.type, allowNull: allowNull); |
+ } |
+ |
+ /// True if all members of this value are numbers. |
+ bool isDefinitelyNum(AbstractValue value, {bool allowNull: false}) { |
+ return value.isNothing || |
+ typeSystem.isDefinitelyNum(value.type, allowNull: allowNull); |
+ } |
+ |
+ /// True if all members of this value are strings. |
+ bool isDefinitelyString(AbstractValue value, {bool allowNull: false}) { |
+ return value.isNothing || |
+ typeSystem.isDefinitelyString(value.type, allowNull: allowNull); |
+ } |
+ |
+ /// True if all members of this value are numbers, strings, or booleans. |
+ bool isDefinitelyNumStringBool(AbstractValue value, {bool allowNull: false}) { |
+ return value.isNothing || |
+ typeSystem.isDefinitelyNumStringBool(value.type, allowNull: allowNull); |
} |
- /// True if null is not a member of this value. |
- bool isDefinitelyNotNull(AbstractValue value) { |
- if (value.isNothing) return true; |
- if (value.isConstant) return !value.constant.isNull; |
- return typeSystem.isDefinitelyNotNull(value.type); |
+ /// True if this value cannot be a string, number, or boolean. |
+ bool isDefinitelyNotNumStringBool(AbstractValue value) { |
+ return value.isNothing || |
+ typeSystem.isDefinitelyNotNumStringBool(value.type); |
} |
/// Returns whether the given [value] is an instance of [type]. |
@@ -203,6 +244,11 @@ class ConstantPropagationLattice { |
} |
return AbstractBool.False; |
} |
+ if (type == dartTypes.coreTypes.intType) { |
+ return constantSystem.isInt(value.constant) |
+ ? AbstractBool.True |
+ : AbstractBool.False; |
+ } |
types.DartType valueType = value.constant.getType(dartTypes.coreTypes); |
if (constantSystem.isSubtype(dartTypes, valueType, type)) { |
return AbstractBool.True; |
@@ -221,6 +267,9 @@ class ConstantPropagationLattice { |
/// Because we do not explicitly track thrown values, we currently use the |
/// convention that constant values are returned from this method only |
/// if the operation is known not to throw. |
+ /// |
+ /// This method returns `null` if a good result could not be found. In that |
+ /// case, it is best to fall back on interprocedural type information. |
AbstractValue unaryOp(UnaryOperator operator, |
AbstractValue value) { |
// TODO(asgerf): Also return information about whether this can throw? |
@@ -233,7 +282,7 @@ class ConstantPropagationLattice { |
if (result == null) return anything; |
return constant(result); |
} |
- return anything; // TODO(asgerf): Look up type. |
+ return null; // TODO(asgerf): Look up type? |
} |
/// Returns the possible results of applying [operator] to [left], [right], |
@@ -242,6 +291,9 @@ class ConstantPropagationLattice { |
/// Because we do not explicitly track thrown values, we currently use the |
/// convention that constant values are returned from this method only |
/// if the operation is known not to throw. |
+ /// |
+ /// This method returns `null` if a good result could not be found. In that |
+ /// case, it is best to fall back on interprocedural type information. |
AbstractValue binaryOp(BinaryOperator operator, |
AbstractValue left, |
AbstractValue right) { |
@@ -254,7 +306,14 @@ class ConstantPropagationLattice { |
if (result == null) return anything; |
return constant(result); |
} |
- return anything; // TODO(asgerf): Look up type. |
+ return null; // TODO(asgerf): Look up type? |
+ } |
+ |
+ /// The possible return types of a method that may be targeted by |
+ /// [typedSelector]. If the given selector is not a [TypedSelector], any |
+ /// reachable method matching the selector may be targeted. |
+ AbstractValue getInvokeReturnType(Selector typedSelector) { |
+ return nonConstant(typeSystem.getInvokeReturnType(typedSelector)); |
} |
} |
@@ -316,6 +375,20 @@ class TypePropagator extends Pass { |
getType(Node node) => _values[node]; |
} |
+final Map<String, BuiltinOperator> NumBinaryBuiltins = |
+ const <String, BuiltinOperator>{ |
+ '+': BuiltinOperator.NumAdd, |
+ '-': BuiltinOperator.NumSubtract, |
+ '*': BuiltinOperator.NumMultiply, |
+ '&': BuiltinOperator.NumAnd, |
+ '|': BuiltinOperator.NumOr, |
+ '^': BuiltinOperator.NumXor, |
+ '<': BuiltinOperator.NumLt, |
+ '<=': BuiltinOperator.NumLe, |
+ '>': BuiltinOperator.NumGt, |
+ '>=': BuiltinOperator.NumGe |
+}; |
+ |
/** |
* Uses the information from a preceding analysis pass in order to perform the |
* actual transformations on the CPS graph. |
@@ -340,6 +413,23 @@ class TransformingVisitor extends RecursiveVisitor { |
visit(root); |
} |
+ /// Removes the entire subtree of [node] and inserts [replacement]. |
+ /// All references in the [node] subtree are unlinked, and parent pointers |
+ /// in [replacement] are initialized. |
+ /// |
+ /// [replacement] must be "fresh", i.e. it must not contain significant parts |
+ /// of the original IR inside of it since the [ParentVisitor] will |
+ /// redundantly reprocess it. |
+ void replaceSubtree(Expression node, Expression replacement) { |
+ InteriorNode parent = node.parent; |
+ parent.body = replacement; |
+ replacement.parent = parent; |
+ node.parent = null; |
+ RemovalVisitor.remove(node); |
+ new ParentVisitor().visit(replacement); |
+ } |
+ |
+ /// Make a constant primitive for [constant] and set its entry in [values]. |
Constant makeConstantPrimitive(ConstantValue constant) { |
ConstantExpression constExp = |
const ConstantExpressionCreator().convert(constant); |
@@ -349,32 +439,38 @@ class TransformingVisitor extends RecursiveVisitor { |
return primitive; |
} |
- /// Given an expression with a known constant result and a continuation, |
- /// replaces the expression by a new LetPrim / InvokeContinuation construct. |
- /// `unlink` is a closure responsible for unlinking all removed references. |
- LetPrim constifyExpression(Expression node, |
- Continuation continuation, |
- void unlink()) { |
- ConstantValue constant = replacements[node]; |
- if (constant == null) return null; |
- |
+ /// Builds `(LetPrim p (InvokeContinuation k p))`. |
+ /// |
+ /// No parent pointers are set. |
+ LetPrim makeLetPrimInvoke(Primitive primitive, Continuation continuation) { |
assert(continuation.parameters.length == 1); |
- InteriorNode parent = node.parent; |
- Constant primitive = makeConstantPrimitive(constant); |
- LetPrim letPrim = new LetPrim(primitive); |
+ LetPrim letPrim = new LetPrim(primitive); |
InvokeContinuation invoke = |
new InvokeContinuation(continuation, <Primitive>[primitive]); |
- parent.body = letPrim; |
letPrim.body = invoke; |
- invoke.parent = letPrim; |
- letPrim.parent = parent; |
- |
- unlink(); |
+ primitive.hint = continuation.parameters.single.hint; |
return letPrim; |
} |
+ /// Side-effect free expressions with constant results are be replaced by: |
+ /// |
+ /// (LetPrim p = constant (InvokeContinuation k p)). |
+ /// |
+ /// The new expression will be visited. |
+ /// |
+ /// Returns true if the node was replaced. |
+ bool constifyExpression(Expression node, Continuation continuation) { |
+ ConstantValue constant = replacements[node]; |
+ if (constant == null) return false; |
+ Constant primitive = makeConstantPrimitive(constant); |
+ LetPrim letPrim = makeLetPrimInvoke(primitive, continuation); |
+ replaceSubtree(node, letPrim); |
+ visitLetPrim(letPrim); |
+ return true; |
+ } |
+ |
// A branch can be eliminated and replaced by an invocation if only one of |
// the possible continuations is reachable. Removal often leads to both dead |
// primitives (the condition variable) and dead continuations (the unreachable |
@@ -402,63 +498,135 @@ class TransformingVisitor extends RecursiveVisitor { |
InvokeContinuation invoke = |
new InvokeContinuation(successor, <Primitive>[]); |
- InteriorNode parent = node.parent; |
- invoke.parent = parent; |
- parent.body = invoke; |
+ replaceSubtree(node, invoke); |
+ visitInvokeContinuation(invoke); |
+ } |
- // Unlink all removed references. |
+ /// True if the given reference is a use that converts its value to a boolean |
+ /// and only uses the coerced value. |
+ bool isBooleanUse(Reference<Primitive> ref) { |
+ Node use = ref.parent; |
+ return use is IsTrue || |
+ use is ApplyBuiltinOperator && use.operator == BuiltinOperator.IsFalsy; |
+ } |
- node.trueContinuation.unlink(); |
- node.falseContinuation.unlink(); |
- IsTrue isTrue = node.condition; |
- isTrue.value.unlink(); |
+ /// True if all uses of [prim] only use its value after boolean conversion. |
+ bool isOnlyUsedAsBoolean(Primitive prim) { |
+ for (Reference ref = prim.firstRef; ref != null; ref = ref.next) { |
+ Node use = ref.parent; |
+ // Ignore uses in dead primitives. |
+ // This happens after rewriting identical(x, true) to x. |
+ if (use is Primitive && use.hasNoUses) continue; |
+ if (!isBooleanUse(ref)) return false; |
+ } |
+ return true; |
+ } |
- visitInvokeContinuation(invoke); |
+ /// Replaces [node] with a more specialized instruction, if possible. |
+ /// |
+ /// Returns `true` if the node was replaced. |
+ bool specializeInvoke(InvokeMethod node) { |
+ Continuation cont = node.continuation.definition; |
+ bool replaceWithBinary(BuiltinOperator operator, |
+ Primitive left, |
+ Primitive right) { |
+ Primitive prim = |
+ new ApplyBuiltinOperator(operator, <Primitive>[left, right]); |
+ LetPrim let = makeLetPrimInvoke(prim, cont); |
+ replaceSubtree(node, let); |
+ visitLetPrim(let); |
+ return true; // So returning early is more convenient. |
+ } |
+ bool replaceWithUnary(BuiltinOperator operator, |
+ Primitive argument) { |
+ Primitive prim = |
+ new ApplyBuiltinOperator(operator, <Primitive>[argument]); |
+ LetPrim let = makeLetPrimInvoke(prim, cont); |
+ replaceSubtree(node, let); |
+ visitLetPrim(let); |
+ return true; |
+ } |
+ |
+ if (node.selector.isOperator && node.arguments.length == 2) { |
+ // The operators we specialize are are intercepted calls, so the operands |
+ // are in the argument list. |
+ Primitive leftArg = node.arguments[0].definition; |
+ Primitive rightArg = node.arguments[1].definition; |
+ AbstractValue left = getValue(leftArg); |
+ AbstractValue right = getValue(rightArg); |
+ |
+ if (node.selector.name == '==') { |
+ // Equality is special due to its treatment of null values and the |
+ // fact that Dart-null corresponds to both JS-null and JS-undefined. |
+ // Please see documentation for IsFalsy, StrictEq, and LooseEq. |
+ bool isBoolified = isOnlyUsedAsBoolean(cont.parameters.single); |
+ // Comparison with null constants. |
+ if (isBoolified && |
+ right.isNullConstant && |
+ lattice.isDefinitelyNotNumStringBool(left)) { |
+ // TODO(asgerf): This is shorter but might confuse te VM? Evaluate. |
+ return replaceWithUnary(BuiltinOperator.IsFalsy, leftArg); |
+ } |
+ if (isBoolified && |
+ left.isNullConstant && |
+ lattice.isDefinitelyNotNumStringBool(right)) { |
+ return replaceWithUnary(BuiltinOperator.IsFalsy, rightArg); |
+ } |
+ if (left.isNullConstant || right.isNullConstant) { |
+ return replaceWithBinary(BuiltinOperator.LooseEq, leftArg, rightArg); |
+ } |
+ // Comparison of numbers, strings, and booleans. |
+ if (lattice.isDefinitelyNumStringBool(left, allowNull: true) && |
+ lattice.isDefinitelyNumStringBool(right, allowNull: true) && |
+ !(left.isNullable && right.isNullable)) { |
+ return replaceWithBinary(BuiltinOperator.StrictEq, leftArg, rightArg); |
+ } |
+ if (lattice.isDefinitelyNum(left, allowNull: true) && |
+ lattice.isDefinitelyNum(right, allowNull: true)) { |
+ return replaceWithBinary(BuiltinOperator.LooseEq, leftArg, rightArg); |
+ } |
+ if (lattice.isDefinitelyString(left, allowNull: true) && |
+ lattice.isDefinitelyString(right, allowNull: true)) { |
+ return replaceWithBinary(BuiltinOperator.LooseEq, leftArg, rightArg); |
+ } |
+ if (lattice.isDefinitelyBool(left, allowNull: true) && |
+ lattice.isDefinitelyBool(right, allowNull: true)) { |
+ return replaceWithBinary(BuiltinOperator.LooseEq, leftArg, rightArg); |
+ } |
+ } else { |
+ // Try to insert a numeric operator. |
+ if (lattice.isDefinitelyNum(left, allowNull: false) && |
+ lattice.isDefinitelyNum(right, allowNull: false)) { |
+ BuiltinOperator operator = NumBinaryBuiltins[node.selector.name]; |
+ if (operator != null) { |
+ return replaceWithBinary(operator, leftArg, rightArg); |
+ } |
+ } |
+ } |
+ } |
+ // We should only get here if the node was not specialized. |
+ assert(node.parent != null); |
+ return false; |
} |
- // Side-effect free method calls with constant results can be replaced by |
- // a LetPrim / InvokeContinuation pair. May lead to dead primitives which |
- // are removed by the shrinking reductions pass. |
- // |
- // (InvokeMethod v0 == v1 k0) |
- // -> (assuming the result is a constant `true`) |
- // (LetPrim v2 (Constant true)) |
- // (InvokeContinuation k0 v2) |
void visitInvokeMethod(InvokeMethod node) { |
Continuation cont = node.continuation.definition; |
- LetPrim letPrim = constifyExpression(node, cont, () { |
- node.receiver.unlink(); |
- node.continuation.unlink(); |
- node.arguments.forEach((Reference ref) => ref.unlink()); |
- }); |
+ if (constifyExpression(node, cont)) return; |
+ if (specializeInvoke(node)) return; |
- if (letPrim == null) { |
- AbstractValue receiver = getValue(node.receiver.definition); |
- node.receiverIsNotNull = lattice.isDefinitelyNotNull(receiver); |
- super.visitInvokeMethod(node); |
- } else { |
- visitLetPrim(letPrim); |
- } |
+ AbstractValue receiver = getValue(node.receiver.definition); |
+ node.receiverIsNotNull = receiver.isDefinitelyNotNull; |
+ super.visitInvokeMethod(node); |
} |
- // See [visitInvokeMethod]. |
void visitConcatenateStrings(ConcatenateStrings node) { |
Continuation cont = node.continuation.definition; |
- LetPrim letPrim = constifyExpression(node, cont, () { |
- node.continuation.unlink(); |
- node.arguments.forEach((Reference ref) => ref.unlink()); |
- }); |
- |
- if (letPrim == null) { |
- super.visitConcatenateStrings(node); |
- } else { |
- visitLetPrim(letPrim); |
- } |
+ if (constifyExpression(node, cont)) return; |
+ super.visitConcatenateStrings(node); |
} |
void visitTypeCast(TypeCast node) { |
Continuation cont = node.continuation.definition; |
- InteriorNode parent = node.parent; |
AbstractValue value = getValue(node.value.definition); |
switch (lattice.isSubtypeOf(value, node.type, allowNull: true)) { |
@@ -469,17 +637,15 @@ class TransformingVisitor extends RecursiveVisitor { |
case AbstractBool.True: |
// Cast always succeeds, replace it with InvokeContinuation. |
InvokeContinuation invoke = |
- new InvokeContinuation.fromCall(node.continuation, node.value); |
- parent.body = invoke; |
- invoke.parent = parent; |
- super.visitInvokeContinuation(invoke); |
+ new InvokeContinuation(cont, <Primitive>[node.value.definition]); |
+ replaceSubtree(node, invoke); |
+ visitInvokeContinuation(invoke); |
return; |
case AbstractBool.False: |
// Cast always fails, remove unreachable continuation body. |
assert(!reachable.contains(cont)); |
- RemovalVisitor.remove(cont.body); |
- cont.body = new Unreachable()..parent = cont; |
+ replaceSubtree(cont.body, new Unreachable()); |
break; |
} |
@@ -509,17 +675,11 @@ class TransformingVisitor extends RecursiveVisitor { |
if (node.primitive is! Constant && value.isConstant) { |
// If the value is a known constant, compile it as a constant. |
Constant newPrim = makeConstantPrimitive(value.constant); |
- LetPrim newLet = new LetPrim(newPrim); |
- node.parent.body = newLet; |
- newLet.body = node.body; |
- node.body.parent = newLet; |
- newLet.parent = node.parent; |
newPrim.substituteFor(node.primitive); |
RemovalVisitor.remove(node.primitive); |
- visit(newLet.body); |
- } else { |
- super.visitLetPrim(node); |
+ node.primitive = newPrim; |
} |
+ super.visitLetPrim(node); |
} |
} |
@@ -723,43 +883,50 @@ class TypePropagationVisitor implements Visitor { |
} |
} |
- AbstractValue lhs = getValue(node.receiver.definition); |
- if (lhs.isNothing) { |
+ AbstractValue receiver = getValue(node.receiver.definition); |
+ if (receiver.isNothing) { |
return; // And come back later. |
} |
if (!node.selector.isOperator) { |
// TODO(jgruber): Handle known methods on constants such as String.length. |
- setResult(nonConstant(typeSystem.getSelectorReturnType(node.selector))); |
+ setResult(lattice.getInvokeReturnType(node.selector)); |
return; |
} |
- // TODO(asgerf): Support constant folding on intercepted calls! |
- |
// Calculate the resulting constant if possible. |
+ // Operators are intercepted, so the operands are in the argument list. |
AbstractValue result; |
String opname = node.selector.name; |
- if (node.selector.argumentCount == 0) { |
+ if (node.arguments.length == 1) { |
+ AbstractValue argument = getValue(node.arguments[0].definition); |
// Unary operator. |
if (opname == "unary-") { |
opname = "-"; |
} |
UnaryOperator operator = UnaryOperator.parse(opname); |
- result = lattice.unaryOp(operator, lhs); |
- } else if (node.selector.argumentCount == 1) { |
+ result = lattice.unaryOp(operator, argument); |
+ } else if (node.arguments.length == 2) { |
// Binary operator. |
- AbstractValue rhs = getValue(node.arguments[0].definition); |
+ AbstractValue left = getValue(node.arguments[0].definition); |
+ AbstractValue right = getValue(node.arguments[1].definition); |
BinaryOperator operator = BinaryOperator.parse(opname); |
- result = lattice.binaryOp(operator, lhs, rhs); |
+ result = lattice.binaryOp(operator, left, right); |
} |
// Update value of the continuation parameter. Again, this is effectively |
// a phi. |
if (result == null) { |
- setResult(nonConstant()); |
+ setResult(lattice.getInvokeReturnType(node.selector)); |
} else { |
setResult(result, canReplace: true); |
} |
- } |
+ } |
+ |
+ void visitApplyBuiltinOperator(ApplyBuiltinOperator node) { |
+ // Not actually reachable yet. |
+ // TODO(asgerf): Implement type propagation for builtin operators. |
+ setValue(node, nonConstant()); |
+ } |
void visitInvokeMethodDirectly(InvokeMethodDirectly node) { |
Continuation cont = node.continuation.definition; |
@@ -1055,7 +1222,7 @@ class TypePropagationVisitor implements Visitor { |
} |
void visitCreateInstance(CreateInstance node) { |
- setValue(node, nonConstant(typeSystem.exact(node.classElement))); |
+ setValue(node, nonConstant(typeSystem.nonNullExact(node.classElement))); |
} |
void visitReifyRuntimeType(ReifyRuntimeType node) { |
@@ -1115,6 +1282,10 @@ class AbstractValue { |
bool get isNothing => (kind == NOTHING); |
bool get isConstant => (kind == CONSTANT); |
bool get isNonConst => (kind == NONCONST); |
+ bool get isNullConstant => kind == CONSTANT && constant.isNull; |
+ |
+ bool get isNullable => kind != NOTHING && type.isNullable; |
+ bool get isDefinitelyNotNull => kind == NOTHING || !type.isNullable; |
int get hashCode { |
int hash = kind * 31 + constant.hashCode * 59 + type.hashCode * 67; |
@@ -1130,7 +1301,7 @@ class AbstractValue { |
String toString() { |
switch (kind) { |
case NOTHING: return "Nothing"; |
- case CONSTANT: return "Constant: $constant: $type"; |
+ case CONSTANT: return "Constant: ${constant.unparse()}: $type"; |
case NONCONST: return "Non-constant: $type"; |
default: assert(false); |
} |