blob: 223c24a303c8f3b52a87fcabdfc83e228c1206f1 [file] [log] [blame]
// Copyright (c) 2015, 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.
library dart2js.cps_ir.bounds_checker;
import 'cps_ir_nodes.dart';
import 'optimizers.dart' show Pass;
import 'octagon.dart';
import '../constants/values.dart';
import 'cps_fragment.dart';
import 'type_mask_system.dart';
import '../types/types.dart';
import '../world.dart';
import '../elements/elements.dart';
import 'loop_effects.dart';
/// Eliminates bounds checks when they can be proven safe.
///
/// In general, this pass will try to eliminate any branch with arithmetic
/// in the condition, i.e. `x < y`, `x <= y`, `x == y` etc.
///
/// The analysis uses an [Octagon] abstract domain. Unlike traditional octagon
/// analyzers, we do not use a closed matrix representation, but just maintain
/// a bucket of constraints. Constraints can therefore be added and removed
/// on-the-fly without significant overhead.
///
/// We never copy the constraint system. While traversing the IR, the
/// constraint system is mutated to take into account the knowledge that is
/// valid for the current location. Constraints are added when entering a
/// branch, for instance, and removed again after the branch has been processed.
///
/// Loops are analyzed in two passes. The first pass establishes monotonicity
/// of loop variables, which the second pass uses to compute upper/lower bounds.
///
/// The two-pass scheme is suboptimal compared to a least fixed-point
/// computation, but does not require repeated iteration. Repeated iteration
/// would be expensive, since we cannot perform a sparse analysis with our
/// mutable octagon representation.
class BoundsChecker extends TrampolineRecursiveVisitor implements Pass {
String get passName => 'Bounds checker';
static const int MAX_UINT32 = (1 << 32) - 1;
/// All integers of this magnitude or less are representable as JS numbers.
static const int MAX_SAFE_INT = (1 << 53) - 1;
/// Marker to indicate that a continuation should get a unique effect number.
static const int NEW_EFFECT = -1;
final TypeMaskSystem types;
final World world;
/// Fields for the constraint system and its variables.
final Octagon octagon = new Octagon();
final Map<Primitive, SignedVariable> valueOf = {};
final Map<Primitive, Map<int, SignedVariable>> lengthOf = {};
/// Fields for the two-pass handling of loops.
final Map<Parameter, Monotonicity> monotonicity = <Parameter, Monotonicity>{};
bool isStrongLoopPass;
bool foundLoop = false;
/// Fields for tracking side effects.
///
/// The IR is divided into regions wherein the lengths of indexable objects
/// are known not to change. Regions are identified by their "effect number".
LoopSideEffects loopEffects;
final Map<Continuation, int> effectNumberAt = <Continuation, int>{};
int currentEffectNumber = 0;
int effectNumberCounter = 0;
BoundsChecker(this.types, this.world);
void rewrite(FunctionDefinition node) {
loopEffects = new LoopSideEffects(node, world);
isStrongLoopPass = false;
visit(node);
if (foundLoop) {
isStrongLoopPass = true;
effectNumberAt.clear();
visit(node);
}
}
// ------------- VARIABLES -----------------
int makeNewEffect() => ++effectNumberCounter;
bool isInt(Primitive prim) {
return types.isDefinitelyInt(prim.type);
}
bool isUInt32(Primitive prim) {
return types.isDefinitelyUint32(prim.type);
}
bool isNonNegativeInt(Primitive prim) {
return types.isDefinitelyNonNegativeInt(prim.type);
}
/// Get a constraint variable representing the numeric value of [number].
SignedVariable getValue(Primitive number) {
number = number.effectiveDefinition;
int min, max;
if (isUInt32(number)) {
min = 0;
max = MAX_UINT32;
} else if (isNonNegativeInt(number)) {
min = 0;
}
return valueOf.putIfAbsent(number, () => octagon.makeVariable(min, max));
}
/// Get a constraint variable representing the length of [indexableObject] at
/// program locations with the given [effectNumber].
SignedVariable getLength(Primitive indexableObject, int effectNumber) {
indexableObject = indexableObject.effectiveDefinition;
TypeMask type = indexableObject.type.nonNullable();
if (types.isDefinitelyFixedLengthIndexable(type)) {
// Always use the same effect number if the length is immutable.
effectNumber = 0;
}
return lengthOf
.putIfAbsent(indexableObject, () => <int, SignedVariable>{})
.putIfAbsent(effectNumber, () {
int length = types.getContainerLength(type);
if (length != null) {
return octagon.makeVariable(length, length);
} else {
return octagon.makeVariable(0, MAX_UINT32);
}
});
}
// ------------- CONSTRAINT HELPERS -----------------
/// Puts the given constraint "in scope" by adding it to the octagon, and
/// pushing a stack action that will remove it again.
void applyConstraint(SignedVariable v1, SignedVariable v2, int k) {
Constraint constraint = new Constraint(v1, v2, k);
octagon.pushConstraint(constraint);
pushAction(() => octagon.popConstraint(constraint));
}
/// Return true if we can prove that `v1 + v2 <= k`.
bool testConstraint(SignedVariable v1, SignedVariable v2, int k) {
// Add the negated constraint and check for solvability.
// !(v1 + v2 <= k) <==> -v1 - v2 <= -k-1
Constraint constraint = new Constraint(v1.negated, v2.negated, -k - 1);
octagon.pushConstraint(constraint);
bool answer = octagon.isUnsolvable;
octagon.popConstraint(constraint);
return answer;
}
void makeLessThanOrEqual(SignedVariable v1, SignedVariable v2) {
// v1 <= v2 <==> v1 - v2 <= 0
applyConstraint(v1, v2.negated, 0);
}
void makeLessThan(SignedVariable v1, SignedVariable v2) {
// v1 < v2 <==> v1 - v2 <= -1
applyConstraint(v1, v2.negated, -1);
}
void makeGreaterThanOrEqual(SignedVariable v1, SignedVariable v2) {
// v1 >= v2 <==> v2 - v1 <= 0
applyConstraint(v2, v1.negated, 0);
}
void makeGreaterThan(SignedVariable v1, SignedVariable v2) {
// v1 > v2 <==> v2 - v1 <= -1
applyConstraint(v2, v1.negated, -1);
}
void makeLessThanOrEqualToConstant(SignedVariable v1, int k) {
// v1 + v1 <= 2k
applyConstraint(v1, v1, 2 * k);
}
void makeGreaterThanOrEqualToConstant(SignedVariable v1, int k) {
// -v1 - v1 <= -2k
applyConstraint(v1.negated, v1.negated, -2 * k);
}
void makeConstant(SignedVariable v1, int k) {
// We model this using the constraints:
// v1 + v1 <= 2k
// -v1 - v1 <= -2k
applyConstraint(v1, v1, 2 * k);
applyConstraint(v1.negated, v1.negated, -2 * k);
}
/// Make `v1 = v2 + k`.
void makeExactSum(SignedVariable v1, SignedVariable v2, int k) {
applyConstraint(v1, v2.negated, k);
applyConstraint(v1.negated, v2, -k);
}
/// Make `v1 = v2 [+] k` where [+] represents floating-point addition.
void makeFloatingPointSum(SignedVariable v1, SignedVariable v2, int k) {
if (isDefinitelyLessThanOrEqualToConstant(v2, MAX_SAFE_INT - k) &&
isDefinitelyGreaterThanOrEqualToConstant(v2, -MAX_SAFE_INT + k)) {
// The result is known to be in the 53-bit range, so no rounding occurs.
makeExactSum(v1, v2, k);
} else {
// A rounding error may occur, so the result may not be exactly v2 + k.
// We can still add monotonicity constraints:
// adding a positive number cannot return a lesser number
// adding a negative number cannot return a greater number
if (k >= 0) {
// v1 >= v2 <==> v2 - v1 <= 0 <==> -v1 + v2 <= 0
applyConstraint(v1.negated, v2, 0);
} else {
// v1 <= v2 <==> v1 - v2 <= 0
applyConstraint(v1, v2.negated, 0);
}
}
}
void makeEqual(SignedVariable v1, SignedVariable v2) {
// We model this using the constraints:
// v1 <= v2 <==> v1 - v2 <= 0
// v1 >= v2 <==> v2 - v1 <= 0
applyConstraint(v1, v2.negated, 0);
applyConstraint(v2, v1.negated, 0);
}
void makeNotEqual(SignedVariable v1, SignedVariable v2) {
// The octagon cannot represent non-equality, but we can sharpen a weak
// inequality to a sharp one. If v1 and v2 are already known to be equal,
// this will create a contradiction and eliminate a dead branch.
// This is necessary for eliminating concurrent modification checks.
if (isDefinitelyLessThanOrEqualTo(v1, v2)) {
makeLessThan(v1, v2);
} else if (isDefinitelyGreaterThanOrEqualTo(v1, v2)) {
makeGreaterThan(v1, v2);
}
}
/// Return true if we can prove that `v1 <= v2`.
bool isDefinitelyLessThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
return testConstraint(v1, v2.negated, 0);
}
/// Return true if we can prove that `v1 < v2`.
bool isDefinitelyLessThan(SignedVariable v1, SignedVariable v2) {
return testConstraint(v1, v2.negated, -1);
}
/// Return true if we can prove that `v1 >= v2`.
bool isDefinitelyGreaterThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
return testConstraint(v2, v1.negated, 0);
}
bool isDefinitelyLessThanOrEqualToConstant(SignedVariable v1, int value) {
// v1 <= value <==> v1 + v1 <= 2 * value
return testConstraint(v1, v1, 2 * value);
}
bool isDefinitelyGreaterThanOrEqualToConstant(SignedVariable v1, int value) {
// v1 >= value <==> -v1 - v1 <= -2 * value
return testConstraint(v1.negated, v1.negated, -2 * value);
}
// ------------- TAIL EXPRESSIONS -----------------
@override
void visitBranch(Branch node) {
Primitive condition = node.condition.definition;
Continuation trueCont = node.trueContinuation.definition;
Continuation falseCont = node.falseContinuation.definition;
effectNumberAt[trueCont] = currentEffectNumber;
effectNumberAt[falseCont] = currentEffectNumber;
pushAction(() {
// If the branching condition is known statically, either or both of the
// branch continuations will be replaced by Unreachable. Clean up the
// branch afterwards.
if (trueCont.body is Unreachable && falseCont.body is Unreachable) {
destroyAndReplace(node, new Unreachable());
} else if (trueCont.body is Unreachable) {
destroyAndReplace(
node, new InvokeContinuation(falseCont, <Parameter>[]));
} else if (falseCont.body is Unreachable) {
destroyAndReplace(
node, new InvokeContinuation(trueCont, <Parameter>[]));
}
});
void pushTrue(makeConstraint()) {
pushAction(() {
makeConstraint();
push(trueCont);
});
}
void pushFalse(makeConstraint()) {
pushAction(() {
makeConstraint();
push(falseCont);
});
}
if (condition is ApplyBuiltinOperator &&
condition.arguments.length == 2 &&
isInt(condition.arguments[0].definition) &&
isInt(condition.arguments[1].definition)) {
SignedVariable v1 = getValue(condition.arguments[0].definition);
SignedVariable v2 = getValue(condition.arguments[1].definition);
switch (condition.operator) {
case BuiltinOperator.NumLe:
pushTrue(() => makeLessThanOrEqual(v1, v2));
pushFalse(() => makeGreaterThan(v1, v2));
return;
case BuiltinOperator.NumLt:
pushTrue(() => makeLessThan(v1, v2));
pushFalse(() => makeGreaterThanOrEqual(v1, v2));
return;
case BuiltinOperator.NumGe:
pushTrue(() => makeGreaterThanOrEqual(v1, v2));
pushFalse(() => makeLessThan(v1, v2));
return;
case BuiltinOperator.NumGt:
pushTrue(() => makeGreaterThan(v1, v2));
pushFalse(() => makeLessThanOrEqual(v1, v2));
return;
case BuiltinOperator.StrictEq:
pushTrue(() => makeEqual(v1, v2));
pushFalse(() => makeNotEqual(v1, v2));
return;
case BuiltinOperator.StrictNeq:
pushTrue(() => makeNotEqual(v1, v2));
pushFalse(() => makeEqual(v1, v2));
return;
default:
}
}
push(trueCont);
push(falseCont);
}
@override
void visitConstant(Constant node) {
// TODO(asgerf): It might be faster to inline the constant in the
// constraints that reference it.
if (node.value.isInt) {
IntConstantValue constant = node.value;
makeConstant(getValue(node), constant.primitiveValue);
}
}
@override
void visitApplyBuiltinOperator(ApplyBuiltinOperator node) {
if (!isInt(node)) return;
if (node.arguments.length == 1) {
applyUnaryOperator(node);
} else if (node.arguments.length == 2) {
applyBinaryOperator(node);
}
}
void applyBinaryOperator(ApplyBuiltinOperator node) {
Primitive left = node.arguments[0].definition;
Primitive right = node.arguments[1].definition;
if (!isInt(left) || !isInt(right)) {
return;
}
SignedVariable leftVar = getValue(left);
SignedVariable rightVar = getValue(right);
SignedVariable result = getValue(node);
switch (node.operator) {
case BuiltinOperator.NumAdd:
int leftConst = getIntConstant(left);
if (leftConst != null) {
makeFloatingPointSum(result, rightVar, leftConst);
}
int rightConst = getIntConstant(right);
if (rightConst != null) {
makeFloatingPointSum(result, leftVar, rightConst);
}
// Attempt to compute the sign of the result.
// TODO(asgerf): Compute upper/lower bounds instead of using 0.
if (testConstraint(leftVar, rightVar, 0)) {
makeLessThanOrEqualToConstant(result, 0);
}
if (testConstraint(leftVar.negated, rightVar.negated, 0)) {
makeGreaterThanOrEqualToConstant(result, 0);
}
// Classical octagon-based analyzers would compute upper and lower
// bounds for the two operands and add constraints for the result based
// on those. For performance reasons we only compute the sign
// TODO(asgerf): It seems expensive, but we should evaluate it.
break;
case BuiltinOperator.NumSubtract:
int leftConst = getIntConstant(left);
if (leftConst != null) {
// result = leftConst - right = (-right) + leftConst
makeFloatingPointSum(result, rightVar.negated, leftConst);
}
int rightConst = getIntConstant(right);
if (rightConst != null) {
// result = left - rightConst = left + (-rightConst)
makeFloatingPointSum(result, leftVar, -rightConst);
}
// Attempt to compute the sign of the result.
if (isDefinitelyGreaterThanOrEqualTo(leftVar, rightVar)) {
makeGreaterThanOrEqualToConstant(result, 0);
}
if (isDefinitelyLessThanOrEqualTo(leftVar, rightVar)) {
makeLessThanOrEqualToConstant(result, 0);
}
break;
case BuiltinOperator.NumTruncatingDivideToSigned32:
if (isDefinitelyGreaterThanOrEqualToConstant(leftVar, 0)) {
// If we divide by a positive number, the result is closer to zero.
// If we divide by a negative number, the result is negative, and
// thus less than the original (non-negative) number.
// TODO(asgerf): The divisor is currently always positive, because
// type propagation checks that, but we could do better.
makeLessThanOrEqual(result, leftVar);
}
break;
case BuiltinOperator.NumShr:
if (isDefinitelyGreaterThanOrEqualToConstant(leftVar, 0)) {
makeLessThanOrEqual(result, leftVar);
}
int shiftAmount = getIntConstant(right);
if (shiftAmount != null) {
// TODO(asgerf): Compute upper bound on [leftVar] and use that
// instead of MAX_UINT32.
makeLessThanOrEqualToConstant(result, MAX_UINT32 >> shiftAmount);
}
break;
case BuiltinOperator.NumRemainder:
// TODO(asgerf): This check overlaps with checks performed in a type
// propagation transformation, and we can do it more precisely here.
// Should we do the rewrite here?
if (isDefinitelyGreaterThanOrEqualToConstant(leftVar, 0) &&
isDefinitelyGreaterThanOrEqualToConstant(rightVar, 1)) {
makeLessThanOrEqual(result, leftVar);
makeLessThan(result, rightVar);
}
break;
case BuiltinOperator.NumAnd:
// We use the faster UInt32 check instead of constraint based checks
// here, because the common case is that one operand is a constant.
if (isUInt32(left)) {
makeLessThanOrEqual(result, leftVar);
}
if (isUInt32(right)) {
makeLessThanOrEqual(result, rightVar);
}
break;
default:
}
}
void applyUnaryOperator(ApplyBuiltinOperator node) {
Primitive argument = node.arguments[0].definition;
if (!isInt(argument)) return;
if (node.operator == BuiltinOperator.NumNegate) {
valueOf[node] = getValue(argument).negated;
}
}
int getIntConstant(Primitive prim) {
if (prim is Constant && prim.value.isInt) {
IntConstantValue constant = prim.value;
return constant.primitiveValue;
}
return null;
}
@override
void visitRefinement(Refinement node) {
// In general we should get the container length of the refined type and
// add a constraint if we know the length after the refinement.
// However, our current type system removes container information when a
// type becomes part of a union, so this cannot happen.
}
@override
void visitGetLength(GetLength node) {
valueOf[node] = getLength(node.object.definition, currentEffectNumber);
}
@override
void visitBoundsCheck(BoundsCheck node) {
if (node.checks == BoundsCheck.NONE) return;
assert(node.index != null); // Because there is at least one check.
Primitive object = node.object.definition;
SignedVariable length = node.length == null
? null
: getValue(node.length.definition);
SignedVariable index = getValue(node.index.definition);
if (node.hasUpperBoundCheck) {
if (isDefinitelyLessThan(index, length)) {
node.checks &= ~BoundsCheck.UPPER_BOUND;
} else {
makeLessThan(index, length);
}
}
if (node.hasLowerBoundCheck) {
if (isDefinitelyGreaterThanOrEqualToConstant(index, 0)) {
node.checks &= ~BoundsCheck.LOWER_BOUND;
} else {
makeGreaterThanOrEqualToConstant(index, 0);
}
}
if (node.hasEmptinessCheck) {
if (isDefinitelyGreaterThanOrEqualToConstant(length, 1)) {
node.checks &= ~BoundsCheck.EMPTINESS;
} else {
makeGreaterThanOrEqualToConstant(length, 1);
}
}
if (!node.lengthUsedInCheck && node.length != null) {
node..length.unlink()..length = null;
}
if (node.checks == BoundsCheck.NONE) {
// We can't remove the bounds check node because it may still be used to
// restrict code motion. But the index is no longer needed.
node..index.unlink()..index = null;
}
}
void analyzeLoopEntry(InvokeContinuation node) {
foundLoop = true;
Continuation cont = node.continuation.definition;
if (isStrongLoopPass) {
for (int i = 0; i < node.arguments.length; ++i) {
Parameter param = cont.parameters[i];
if (!isInt(param)) continue;
Primitive initialValue = node.arguments[i].definition;
SignedVariable initialVariable = getValue(initialValue);
Monotonicity mono = monotonicity[param];
if (mono == null) {
// Value never changes. This is extremely uncommon.
param.replaceUsesWith(initialValue);
} else if (mono == Monotonicity.Increasing) {
makeGreaterThanOrEqual(getValue(param), initialVariable);
} else if (mono == Monotonicity.Decreasing) {
makeLessThanOrEqual(getValue(param), initialVariable);
}
}
}
if (loopEffects.loopChangesLength(cont)) {
currentEffectNumber = effectNumberAt[cont] = makeNewEffect();
}
push(cont);
}
void analyzeLoopContinue(InvokeContinuation node) {
Continuation cont = node.continuation.definition;
// During the strong loop phase, there is no need to compute monotonicity,
// and we already put bounds on the loop variables when we went into the
// loop.
if (isStrongLoopPass) return;
// For each loop parameter, try to prove that the new value is definitely
// less/greater than its old value. When we fail to prove this, update the
// monotonicity flag accordingly.
for (int i = 0; i < node.arguments.length; ++i) {
Parameter param = cont.parameters[i];
if (!isInt(param)) continue;
SignedVariable arg = getValue(node.arguments[i].definition);
SignedVariable paramVar = getValue(param);
if (!isDefinitelyLessThanOrEqualTo(arg, paramVar)) {
// We couldn't prove that the value does not increase, so assume
// henceforth that it might be increasing.
markMonotonicity(cont.parameters[i], Monotonicity.Increasing);
}
if (!isDefinitelyGreaterThanOrEqualTo(arg, paramVar)) {
// We couldn't prove that the value does not decrease, so assume
// henceforth that it might be decreasing.
markMonotonicity(cont.parameters[i], Monotonicity.Decreasing);
}
}
}
void markMonotonicity(Parameter param, Monotonicity mono) {
Monotonicity current = monotonicity[param];
if (current == null) {
monotonicity[param] = mono;
} else if (current != mono) {
monotonicity[param] = Monotonicity.NotMonotone;
}
}
@override
void visitInvokeContinuation(InvokeContinuation node) {
Continuation cont = node.continuation.definition;
if (node.isRecursive) {
analyzeLoopContinue(node);
} else if (cont.isRecursive) {
analyzeLoopEntry(node);
} else {
int effect = effectNumberAt[cont];
if (effect == null) {
effectNumberAt[cont] = currentEffectNumber;
} else if (effect != currentEffectNumber && effect != NEW_EFFECT) {
effectNumberAt[cont] = NEW_EFFECT;
}
// TODO(asgerf): Compute join for parameters to increase precision?
}
}
// ---------------- PRIMITIVES --------------------
@override
void visitInvokeMethod(InvokeMethod node) {
// TODO(asgerf): What we really need is a "changes length" side effect flag.
if (world
.getSideEffectsOfSelector(node.selector, node.mask)
.changesIndex()) {
currentEffectNumber = makeNewEffect();
}
}
@override
void visitInvokeStatic(InvokeStatic node) {
if (world.getSideEffectsOfElement(node.target).changesIndex()) {
currentEffectNumber = makeNewEffect();
}
}
@override
void visitInvokeMethodDirectly(InvokeMethodDirectly node) {
FunctionElement target = node.target;
if (target is ConstructorBodyElement) {
ConstructorBodyElement body = target;
target = body.constructor;
}
if (world.getSideEffectsOfElement(target).changesIndex()) {
currentEffectNumber = makeNewEffect();
}
}
@override
void visitInvokeConstructor(InvokeConstructor node) {
if (world.getSideEffectsOfElement(node.target).changesIndex()) {
currentEffectNumber = makeNewEffect();
}
}
@override
void visitTypeCast(TypeCast node) {
}
@override
void visitGetLazyStatic(GetLazyStatic node) {
// TODO(asgerf): How do we get the side effects of a lazy field initializer?
currentEffectNumber = makeNewEffect();
}
@override
void visitForeignCode(ForeignCode node) {
if (node.nativeBehavior.sideEffects.changesIndex()) {
currentEffectNumber = makeNewEffect();
}
}
@override
void visitAwait(Await node) {
currentEffectNumber = makeNewEffect();
}
@override
void visitYield(Yield node) {
currentEffectNumber = makeNewEffect();
}
@override
void visitApplyBuiltinMethod(ApplyBuiltinMethod node) {
Primitive receiver = node.receiver.definition;
int effectBefore = currentEffectNumber;
currentEffectNumber = makeNewEffect();
int effectAfter = currentEffectNumber;
SignedVariable lengthBefore = getLength(receiver, effectBefore);
SignedVariable lengthAfter = getLength(receiver, effectAfter);
switch (node.method) {
case BuiltinMethod.Push:
// after = before + count
int count = node.arguments.length;
makeExactSum(lengthAfter, lengthBefore, count);
break;
case BuiltinMethod.Pop:
// after = before - 1
makeExactSum(lengthAfter, lengthBefore, -1);
break;
}
}
@override
void visitLiteralList(LiteralList node) {
makeConstant(getLength(node, currentEffectNumber), node.values.length);
}
// ---------------- INTERIOR EXPRESSIONS --------------------
@override
Expression traverseContinuation(Continuation cont) {
if (octagon.isUnsolvable) {
destroyAndReplace(cont.body, new Unreachable());
} else {
int effect = effectNumberAt[cont];
if (effect != null) {
currentEffectNumber = effect == NEW_EFFECT ? makeNewEffect() : effect;
}
}
return cont.body;
}
@override
Expression traverseLetCont(LetCont node) {
// Join continuations should be pushed at declaration-site, so all their
// call sites are seen before they are analyzed.
// Other continuations are pushed at the use site.
for (Continuation cont in node.continuations) {
if (cont.hasAtLeastOneUse &&
!cont.isRecursive &&
cont.firstRef.parent is InvokeContinuation) {
push(cont);
}
}
return node.body;
}
}
/// Lattice representing the known (weak) monotonicity of a loop variable.
///
/// The lattice bottom is represented by `null` and represents the case where
/// the loop variable never changes value during the loop.
enum Monotonicity { NotMonotone, Increasing, Decreasing, }