Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 107 additions & 49 deletions src/ir/constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,63 @@

namespace wasm::constraint {

std::optional<Span<IU64>> Constraint::getSpan() const {
namespace {

std::optional<Span<IU64>>
getSpanInternal(const Constraint& c, std::optional<Type> type, bool exact) {
using namespace Abstract;

auto* c = std::get_if<Literal>(&term);
if (!c) {
// Not comparing to a constant, so cannot be a constant span.
auto* cc = std::get_if<Literal>(&c.term);
if (cc) {
// If passed in, the type must be right.
assert(!type || *type == cc->type);

type = cc->type;
}

if (type && !type->isInteger()) {
// References etc. do not convert to spans.
return {};
}

auto minSigned = c->type == Type::i32 ? std::numeric_limits<int32_t>::min()
: std::numeric_limits<int64_t>::min();
auto maxSigned = c->type == Type::i32 ? std::numeric_limits<int32_t>::max()
: std::numeric_limits<int64_t>::max();
auto maxUnsigned = c->type == Type::i32
auto minSigned = type && *type == Type::i32
? std::numeric_limits<int32_t>::min()
: std::numeric_limits<int64_t>::min();
auto maxSigned = type && *type == Type::i32
? std::numeric_limits<int32_t>::max()
: std::numeric_limits<int64_t>::max();
auto maxUnsigned = type && *type == Type::i32
? std::numeric_limits<uint32_t>::max()
: std::numeric_limits<uint64_t>::max();

switch (op) {
if (!cc) {
// Not comparing to a constant, so we can't infer anything exact, but might
// if we just need something we can prove, and if we know the type.
if (!exact && type) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's invert this and turn it into an early return.

switch (c.op) {
// x < y, i.e., x is less than *something*, proves x < MAX_INT.
case LtS:
return Span<IU64>{minSigned, maxSigned - 1};
case LtU:
return Span<IU64>{0, maxUnsigned - 1};

// Similarly, x > y proves x > MIN_INT.
case GtS:
return Span<IU64>{minSigned + 1, maxSigned};
case GtU:
return Span<IU64>{1, maxUnsigned};

default: {
}
}
}

return {};
}

switch (c.op) {
case Eq: {
auto x = c->getUnsigned();
auto x = cc->getUnsigned();
if (x <= uint64_t(maxSigned)) {
// This is in the range of both signed and unsigned values, so there is
// no ambiguity. That is, we cannot convert the bit pattern
Expand All @@ -54,46 +91,46 @@ std::optional<Span<IU64>> Constraint::getSpan() const {
}

case LtS:
if (c->getInteger() == minSigned) {
if (cc->getInteger() == minSigned) {
// Less than the lowest possible number is an empty span.
return Span<IU64>::empty();
} else {
return Span<IU64>{minSigned, c->getInteger() - 1};
return Span<IU64>{minSigned, cc->getInteger() - 1};
}
break;
case LtU:
if (c->getInteger() == 0) {
if (cc->getInteger() == 0) {
// Less than the lowest possible number is an empty span.
return Span<IU64>::empty();
} else {
return Span<IU64>{0, c->getUnsigned() - 1};
return Span<IU64>{0, cc->getUnsigned() - 1};
}
break;
case LeS:
return Span<IU64>{minSigned, c->getInteger()};
return Span<IU64>{minSigned, cc->getInteger()};
case LeU:
return Span<IU64>{0, c->getUnsigned()};
return Span<IU64>{0, cc->getUnsigned()};

case GtS:
if (c->getInteger() == maxSigned) {
if (cc->getInteger() == maxSigned) {
// Greater than the highest possible number is an empty span.
return Span<IU64>::empty();
} else {
return Span<IU64>{c->getInteger() + 1, maxSigned};
return Span<IU64>{cc->getInteger() + 1, maxSigned};
}
break;
case GtU:
if (c->getUnsigned() == maxUnsigned) {
if (cc->getUnsigned() == maxUnsigned) {
// Greater than the highest possible number is an empty span.
return Span<IU64>::empty();
} else {
return Span<IU64>{c->getUnsigned() + 1, maxUnsigned};
return Span<IU64>{cc->getUnsigned() + 1, maxUnsigned};
}
break;
case GeS:
return Span<IU64>{c->getInteger(), maxSigned};
return Span<IU64>{cc->getInteger(), maxSigned};
case GeU:
return Span<IU64>{c->getUnsigned(), maxUnsigned};
return Span<IU64>{cc->getUnsigned(), maxUnsigned};

default: {
}
Expand All @@ -102,6 +139,17 @@ std::optional<Span<IU64>> Constraint::getSpan() const {
return {};
}

} // anonymous namespace

std::optional<Span<IU64>> Constraint::getSpan(std::optional<Type> type) const {
return getSpanInternal(*this, type, true);
}

std::optional<Span<IU64>>
Constraint::getProvenSpan(std::optional<Type> type) const {
return getSpanInternal(*this, type, false);
}

namespace {

Result TrueFalse(bool x) { return x ? True : False; }
Expand Down Expand Up @@ -158,31 +206,6 @@ Result provesConstantPair(Abstract::Op aOp,
}
}

// If we can represent both as spans, we can calculate that way.
if (auto aSpan = Constraint{aOp, {aConstant}}.getSpan()) {
if (auto bSpan = Constraint{bOp, {bConstant}}.getSpan()) {
if (aSpan->isEmpty()) {
// An empty span implies a contradiction (e.g. x > MAX_INT), as it means
// no possible number can apply. And contradictions prove anything.
return True;
}
if (bSpan->isEmpty()) {
// Anything that is not a contradiction can prove a contradiction.
return False;
}
if (bSpan->contains(*aSpan)) {
// b's values contains a's, e.g., b = { 0 < x < 10 } and
// a = { 3 < x < 7 }, so a => b.
return True;
}
if (!bSpan->hasOverlap(*aSpan)) {
// There is no overlap at all, e.g., { 0 < x < 10 } vs { 20 < x < 30 },
// both cannot be true and each proves the other false.
return False;
}
}
}

if (!recursing) {
// The flipped operation may tell us something: y ==> !x implies
// x ==> y is false (because if not, then x would prove y, and y would
Expand Down Expand Up @@ -212,7 +235,42 @@ Result provesPair(const Constraint& a, const Constraint& b) {
auto* aConstant = std::get_if<Literal>(&a.term);
auto* bConstant = std::get_if<Literal>(&b.term);
if (aConstant && bConstant) {
return provesConstantPair(a.op, *aConstant, b.op, *bConstant);
auto result = provesConstantPair(a.op, *aConstant, b.op, *bConstant);
if (result != Unknown) {
return result;
}
}

// If we can represent both as spans, we can calculate that way. At least one
// must be a constant in this case, so that we know the type.
if (aConstant || bConstant) {
auto type = aConstant ? aConstant->type : bConstant->type;
// Use a proven span for a, and an exact one for b. This allows us to do
// a => proven span for a => exact span for b => b.
if (auto aSpan = a.getProvenSpan(type)) {
if (auto bSpan = b.getSpan(type)) {
if (aSpan->isEmpty()) {
// An empty span implies a contradiction (e.g. x > MAX_INT), as it
// means no possible number can apply. And contradictions prove
// anything.
return True;
}
if (bSpan->isEmpty()) {
// Anything that is not a contradiction can prove a contradiction.
return False;
}
if (bSpan->contains(*aSpan)) {
// b's values contains a's, e.g., b = { 0 < x < 10 } and
// a = { 3 < x < 7 }, so a => b.
return True;
}
if (!bSpan->hasOverlap(*aSpan)) {
// There is no overlap at all, e.g., { 0 < x < 10 } vs { 20 < x < 30
// }, both cannot be true and each proves the other false.
return False;
}
}
}
}

return Unknown;
Expand Down
12 changes: 11 additions & 1 deletion src/ir/constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,17 @@ struct Constraint {

// Convert the constraint into a constant span, if possible. For example,
// "<= 100 (unsigned)" turns into the span [0, 100].
std::optional<Span<IU64>> getSpan() const;
//
// An optional type may be passed in. If not, the type is inferred from the
// term, when possible.
std::optional<Span<IU64>> getSpan(std::optional<Type> type = {}) const;

// Get a span we can prove. This is less precise than getSpan, which gets an
// *exact* span to represent the Constraint. Here we only return a span we can
// prove is true. For example, x < y cannot be represented exactly using a
// span (y is not a constant), but that x is smaller than *something* proves
// x is not MAX_INT, so we can return the span [MIN_INT, MAX_INT - 1].
std::optional<Span<IU64>> getProvenSpan(std::optional<Type> type = {}) const;
};

// We limit constraints to a low number to ensure good performance even with
Expand Down
97 changes: 97 additions & 0 deletions test/gtest/constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ TEST(ConstraintTest, TestEq) {

// x != 5: we can infer false.
EXPECT_EQ(s.proves(Constraint{Ne, {Literal(int32_t(5))}}), False);

// x > y: we can infer nothing.
EXPECT_EQ(s.proves(Constraint{GtS, {Index(1)}}), Unknown);
}

TEST(ConstraintTest, TestNe) {
Expand Down Expand Up @@ -682,6 +685,24 @@ TEST(ConstraintTest, TestEqConstraints) {
check(map.get(1), {GtS, {Literal(int32_t(42))}});
}

TEST(ConstraintTest, ComplexOrRegression) {
// $0 == 0
BasicBlockConstraintMap left;
left.setReachable();
left.set(0, {Eq, {Literal(int32_t(0))}});

// $0 <= 100, $0 > $1
BasicBlockConstraintMap right;
right.setReachable();
right.set(0, {{LeS, {Literal(int32_t(100))}}, {GtS, {Index(1)}}});

// $0 == 0 || $0 <= 100 => $0 <= 100 (0 is included in <= 100), but the
// other constraint, $0 > $1, was only on one side, and vanishes.
right.approximateOr(left);
check(right.get(0), {LeS, {Literal(int32_t(100))}});
EXPECT_TRUE(right.get(1).empty());
}

TEST(ConstraintTest, GetSpan) {
const IU64 minI32(std::numeric_limits<int32_t>::min());
const IU64 maxI32(std::numeric_limits<int32_t>::max());
Expand Down Expand Up @@ -966,6 +987,70 @@ TEST(ConstraintTest, GetSpan) {
(Span<IU64>{maxU64, maxU64}));
}

TEST(ConstraintTest, GetSpanType) {
const IU64 minI32(std::numeric_limits<int32_t>::min());
const IU64 minI32Plus1(std::numeric_limits<int32_t>::min() + 1);

const IU64 maxI32(std::numeric_limits<int32_t>::max());
const IU64 maxI32Minus1(std::numeric_limits<int32_t>::max() - 1);

const IU64 maxU32(std::numeric_limits<uint32_t>::max());
const IU64 maxU32Minus1(std::numeric_limits<uint32_t>::max() - 1);

const IU64 minI64(std::numeric_limits<int64_t>::min());
const IU64 minI64Plus1(std::numeric_limits<int64_t>::min() + 1);

const IU64 maxI64(std::numeric_limits<int64_t>::max());
const IU64 maxI64Minus1(std::numeric_limits<int64_t>::max() - 1);

const IU64 maxU64(std::numeric_limits<uint64_t>::max());
const IU64 maxU64Minus1(std::numeric_limits<uint64_t>::max() - 1);

// Providing the type to getSpan() doesn't help with certain things.
EXPECT_EQ((Constraint{Eq, {Index(0)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{Ne, {Index(1)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{GeU, {Index(2)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{GeS, {Index(0)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{LeU, {Index(1)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{LeS, {Index(2)}}.getSpan(Type::i32)), std::nullopt);

// But it does help with others: x < y means x cannot be MAX_INT, so we can
// report a *proven* span, if not an exact one.
EXPECT_EQ((Constraint{LtS, {Index(0)}}.getProvenSpan(Type::i32)),
(Span<IU64>{minI32, maxI32Minus1}));
EXPECT_EQ((Constraint{LtS, {Index(1)}}.getProvenSpan(Type::i64)),
(Span<IU64>{minI64, maxI64Minus1}));

EXPECT_EQ((Constraint{LtU, {Index(2)}}.getProvenSpan(Type::i32)),
(Span<IU64>{0, maxU32Minus1}));
EXPECT_EQ((Constraint{LtU, {Index(0)}}.getProvenSpan(Type::i64)),
(Span<IU64>{0, maxU64Minus1}));

EXPECT_EQ((Constraint{GtS, {Index(1)}}.getProvenSpan(Type::i32)),
(Span<IU64>{minI32Plus1, maxI32}));
EXPECT_EQ((Constraint{GtS, {Index(2)}}.getProvenSpan(Type::i64)),
(Span<IU64>{minI64Plus1, maxI64}));

EXPECT_EQ((Constraint{GtU, {Index(0)}}.getProvenSpan(Type::i32)),
(Span<IU64>{1, maxU32}));
EXPECT_EQ((Constraint{GtU, {Index(1)}}.getProvenSpan(Type::i64)),
(Span<IU64>{1, maxU64}));

// But all the last things are impossible with an exact span.
EXPECT_EQ((Constraint{LtS, {Index(0)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{LtS, {Index(1)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{LtU, {Index(2)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{LtU, {Index(0)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{GtS, {Index(1)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{GtS, {Index(2)}}.getSpan(Type::i64)), std::nullopt);
EXPECT_EQ((Constraint{GtU, {Index(0)}}.getSpan(Type::i32)), std::nullopt);
EXPECT_EQ((Constraint{GtU, {Index(1)}}.getSpan(Type::i64)), std::nullopt);

// Proven spans are otherwise like normal ones.
EXPECT_EQ((Constraint{Eq, {Literal(int32_t(42))}}.getProvenSpan()),
(Span<IU64>{42, 42}));
}

TEST(ConstraintTest, SpanOptimizations) {
// Using spans, we can optimize things like {x < 100} => {x < 200}.
Constraint lts100{LtS, {Literal(int32_t(100))}};
Expand Down Expand Up @@ -1042,3 +1127,15 @@ TEST(ConstraintTest, EmptySpanContradiction) {
AndedConstraintSet impossible{gtsMax32};
checkOr(valid, impossible, valid);
}

TEST(ConstraintTest, GetSpanFloat) {
// Non-integer types do not cause errors.
EXPECT_EQ((Constraint{Eq, {Literal(float(3.14159))}}.getSpan()),
std::nullopt);
}

TEST(ConstraintTest, GetSpanGC) {
// Reference types do not cause errors.
EXPECT_EQ((Constraint{Eq, {Literal::makeNull(HeapType::eq)}}.getSpan()),
std::nullopt);
}
Loading