blob: eb4f65e9a9ddf229044a235afd69d78b0b82fc8a [file] [edit]
/*
* Copyright 2026 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <optional>
#include "ir/constraint.h"
#include "ir/properties.h"
#include "wasm.h"
namespace wasm::constraint {
namespace {
Result TrueFalse(bool x) { return x ? True : False; }
Result TrueFalse(Literal x) { return TrueFalse(x.getUnsigned()); }
// Evaluate whether a => b, where a and b are operations on constants.
Result provesConstantPair(Abstract::Op aOp,
const Literal& aConstant,
Abstract::Op bOp,
const Literal& bConstant,
bool recursing = false) {
using namespace Abstract;
// a == A =?=> a op B. Simply apply A to the operation against B.
if (aOp == Eq) {
switch (bOp) {
case Eq:
return TrueFalse(aConstant == bConstant);
case Ne:
return TrueFalse(aConstant != bConstant);
case LtS:
return TrueFalse(aConstant.ltS(bConstant));
case LeS:
return TrueFalse(aConstant.leS(bConstant));
case GtS:
return TrueFalse(aConstant.gtS(bConstant));
case GeS:
return TrueFalse(aConstant.geS(bConstant));
case LtU:
return TrueFalse(aConstant.ltU(bConstant));
case LeU:
return TrueFalse(aConstant.leU(bConstant));
case GtU:
return TrueFalse(aConstant.gtU(bConstant));
case GeU:
return TrueFalse(aConstant.geU(bConstant));
default: {
}
}
}
// a != A =?=> a == B. False if A = B, else unknown.
if (aOp == Ne && bOp == Eq) {
if (aConstant == bConstant) {
return False;
}
}
// a != A =?=> a != B. True if A = B, else unknown.
if (aOp == Ne && bOp == Ne) {
if (aConstant == bConstant) {
return True;
}
}
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
// prove !x, a contradiction).
if (provesConstantPair(bOp, bConstant, aOp, aConstant, true) == False) {
return False;
}
}
// TODO: handle all the rest of >, >=, <, and <=
return Unknown;
}
// Core comparison of two constraints: whether a => b
Result provesPair(const Constraint& a, const Constraint& b) {
// A thing always implies itself.
if (a == b) {
return True;
}
// A thing always implies its negation is false.
if (a == b.negate()) {
return False;
}
// Comparisons of two constants.
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);
}
return Unknown;
}
} // anonymous namespace
Result AndedConstraintSet::proves(const Constraint& condition) const {
if (provesEverything()) {
return True;
}
// Note we do not need to handle the provesNothing case in a special way: the
// loop below finds nothing.
// Sometimes a single constraint is enough to determine the condition.
for (auto& c : *this) {
auto result = provesPair(c, condition);
if (result != Unknown) {
return result;
}
}
// TODO smarts for multiple constraints
// Otherwise, who knows.
return Unknown;
}
Result AndedConstraintSet::proves(const AndedConstraintSet& other) const {
if (provesEverything()) {
return True;
}
if (other.provesEverything()) {
// We are not a contradiction, but other is, so we prove it false.
return False;
}
bool hasUnknown = false;
for (auto& c : other) {
auto result = proves(c);
if (result == False) {
// The entire conjunction is proven false.
return False;
}
if (result == Unknown) {
hasUnknown = true;
}
}
return hasUnknown ? Unknown : True;
}
namespace {
// Do an AND on a pair of constraints, looking for a way to fuse them together
// into a single constraint that represents them both, while assuming the
// constraints have an equal term. If we fail, return nullopt.
std::optional<Constraint> fusedApproximateAndTermEqualPair(
const Abstract::Op aOp, const Abstract::Op bOp, const Term& term) {
using namespace Abstract;
// x < C && x <= C === x < C
if (aOp == LtS && bOp == LeS) {
return Constraint{LtS, term};
}
if (aOp == LtU && bOp == LeU) {
return Constraint{LtU, term};
}
// TODO: all the rest
return {};
}
// Do an AND on a pair of constraints, looking for a way to fuse them together
// into a single constraint that represents them both. If we fail, return
// nullopt.
std::optional<Constraint> fusedApproximateAndPair(const Constraint& a,
const Constraint& b,
bool recursing = false) {
// If a proves b is true, all we need is a (e.g. { x == 5 && x > 0 } => x == 5
if (provesPair(a, b) == True) {
return a;
}
if (a.term == b.term) {
if (auto result = fusedApproximateAndTermEqualPair(a.op, b.op, a.term)) {
return result;
}
}
if (!recursing) {
// The flipped form may be recognized.
return fusedApproximateAndPair(b, a, true);
}
return {};
}
} // anonymous namespace
void AndedConstraintSet::approximateAnd(const Constraint& c) {
if (provesEverything()) {
// Nothing to add.
return;
}
auto result = proves(c);
if (result == True) {
// We already prove c to be true, so it adds nothing.
return;
} else if (result == False) {
// We are now a contradiction.
setProvesEverything();
return;
}
for (auto& existing : *this) {
// Some ANDed constraints fuse together into a new constraint.
if (auto fused = fusedApproximateAndPair(existing, c)) {
existing = *fused;
// Sort to ensure we are in the right place.
std::sort(begin(), end());
return;
}
}
if (size() < MaxConstraints) {
// Insert into the right place, keeping us sorted.
insert(std::upper_bound(begin(), end(), c), c);
return;
}
// Otherwise, just do not add this one.
// TODO: We could try to be clever and see if one of the existing ones makes
// more sense to drop. In particular, we should prefer "better" ones
// like > over >= and so forth (sorting more precise ones earlier may be
// useful to implement that).
}
namespace {
// Do an OR of a pair of constraints where the terms are known to be equal. If
// we can't find a good way to express their ORing, return nullopt.
std::optional<Constraint> approximateOrTermEqualPair(const Abstract::Op aOp,
const Abstract::Op bOp,
const Term& term) {
using namespace Abstract;
// x == C || x > C === x >= C
if (aOp == Eq && bOp == GtS) {
return Constraint{GeS, term};
}
// TODO: all the rest
return {};
}
// Do an OR of a pair of constraints. If we can't find a good way to express
// their ORing, return nullopt.
std::optional<Constraint> approximateOrPair(const Constraint& a,
const Constraint& b,
bool recursing = false) {
if (a.term == b.term) {
if (auto result = approximateOrTermEqualPair(a.op, b.op, a.term)) {
return result;
}
}
// If a proves b, e.g. x = 5 proves x >= 0 is true, then the OR is b.
if (provesPair(a, b) == True) {
return b;
}
// TODO: more smarts
if (!recursing) {
// The flipped form may be recognized.
return approximateOrPair(b, a, true);
}
return {};
}
// Do an OR in full detail, looking at every constraint in each of the given
// sets.
AndedConstraintSet detailedApproximateOr(const AndedConstraintSet& a,
const AndedConstraintSet& b) {
// We can process this in full detail by looking at all the combinations of
// individual constraints, because of the distributive property:
//
// (A & B) | (C & D) == ((A & B) | C) & ((A & B) | D)
// == (A | C) & (B | C) & (A | D) & (B | D)
//
// This is quadratic, but constraint sets are limited to a very small size,
// making this reasonable.
//
// Also, note that we don't need to worry about new contradictions here: ORing
// things never leads to a contradiction, and we can assume the inputs are
// not contradictions.
assert(!a.provesEverything() && !b.provesEverything());
auto result = AndedConstraintSet::makeProvesNothing();
for (auto& ac : a) {
for (auto& bc : b) {
if (auto combined = approximateOrPair(ac, bc)) {
// We found something useful by ORing them, keep it.
result.approximateAnd(*combined);
}
}
}
return result;
}
} // anonymous namespace
bool AndedConstraintSet::approximateOr(const AndedConstraintSet& other) {
// If one proves everything, the only thing that matters is the other.
if (other.provesEverything()) {
return false;
}
if (provesEverything()) {
*this = other;
return true;
}
// If this is already implied by current constraints, then it is redundant.
// E.g. if we are { x = 10 } and other is { x >= 0 } then all we need is
// { x >= 0 } as the result of the OR.
if (other.proves(*this) == True) {
return false;
}
if (proves(other) == True) {
*this = other;
return true;
}
// For more complex cases, do a detailed analysis.
auto result = detailedApproximateOr(*this, other);
auto changed = (result != *this);
*this = result;
return changed;
}
std::optional<LocalConstraint> LocalConstraint::parse(Expression* curr) {
auto parseEqZArgument =
[&](Expression* value) -> std::optional<LocalConstraint> {
if (auto* get = value->dynCast<LocalGet>()) {
// Canonicalize EqZ to Eq of 0.
auto value = Literal::makeZero(get->type);
return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}};
}
// TODO: Recursively parse and reverse a constraint
return {};
};
if (auto* unary = curr->dynCast<Unary>()) {
if (Abstract::getUnary(unary->type, Abstract::EqZ) == unary->op) {
return parseEqZArgument(unary->value);
}
return {};
}
if (auto* refIsNull = curr->dynCast<RefIsNull>()) {
return parseEqZArgument(refIsNull->value);
}
// Parse a get or a constant.
auto parseTerm = [&](Expression* expr) -> std::optional<Term> {
if (auto* get = expr->dynCast<LocalGet>()) {
return Term{get->index};
}
if (Properties::isSingleConstantExpression(expr)) {
return Term{Properties::getLiteral(expr)};
}
return {};
};
auto parseBinaryArguments =
[&](Abstract::Op op,
Expression* left,
Expression* right) -> std::optional<LocalConstraint> {
// The left must be a get.
if (auto* get = left->dynCast<LocalGet>()) {
// The right can be any term.
if (auto value = parseTerm(right)) {
return LocalConstraint{get->index, Constraint{op, *value}};
}
}
return {};
};
if (auto* binary = curr->dynCast<Binary>()) {
// The operation must be one we recognize.
for (auto op : {Abstract::Eq,
Abstract::Ne,
Abstract::LtS,
Abstract::LtU,
Abstract::LeS,
Abstract::LeU,
Abstract::GtS,
Abstract::GtU,
Abstract::GeS,
Abstract::GeU}) {
if (Abstract::getBinary(binary->type, op) == binary->op) {
return parseBinaryArguments(op, binary->left, binary->right);
}
}
return {};
}
if (auto* refEq = curr->dynCast<RefEq>()) {
return parseBinaryArguments(Abstract::Eq, refEq->left, refEq->right);
}
return {};
}
std::optional<LocalConstraint>
LocalConstraint::parseCondition(Expression* curr) {
// A get by itself is a check for not being null.
if (auto* get = curr->dynCast<LocalGet>()) {
auto value = Literal::makeZero(get->type);
return LocalConstraint{get->index, Constraint{Abstract::Ne, {value}}};
}
// Otherwise, parse normally.
return parse(curr);
};
void LocalConstraint::flip() {
auto other = std::get<Index>(constraint.term);
constraint.term = Term{local};
local = other;
if (Abstract::isRelationalAntisymmetric(constraint.op)) {
constraint.op = Abstract::negateRelational(constraint.op);
} else {
// All we support for now are symmetric and antisymmetric operations.
assert(Abstract::isRelationalSymmetric(constraint.op));
}
}
void BasicBlockConstraintMap::set(Index index, const Constraint& c) {
// We should not set values in unreachable code.
assert(!unreachable);
// Clear the old state.
eraseStaleRefs(index);
map.erase(index);
// Apply the constraint.
approximateAnd(index, c);
}
void BasicBlockConstraintMap::setProvesNothing(Index index) {
assert(!unreachable);
eraseStaleRefs(index);
map.erase(index);
}
bool BasicBlockConstraintMap::approximateOr(
const BasicBlockConstraintMap& other) {
// If one is unreachable, it adds nothing to the other.
if (other.unreachable) {
return false;
}
if (unreachable) {
*this = other;
return true;
}
// We only need to loop on our locals, as any local that is missing in us is
// one that would end up proving nothing (and get removed).
bool changed = false;
for (auto& [local, constraints] : map) {
changed |= constraints.approximateOr(other.get(local));
}
// Anything that became trivial after the OR must be removed.
std::erase_if(map, [&](const auto& item) {
const auto& [local, constraints] = item;
// We do not store contradictions.
assert(!constraints.provesEverything());
if (constraints.provesNothing()) {
changed = true;
return true;
}
return false;
});
return changed;
}
void BasicBlockConstraintMap::approximateAndInternal(Index index,
const Constraint& c,
bool flip,
bool isCopy) {
// We should not be applying constraints when already unreachable.
assert(!unreachable);
Constraint actual = c;
if (flip) {
LocalConstraint flipped{index, c};
flipped.flip();
index = flipped.local;
actual = flipped.constraint;
}
// Never add constraints to ourselves (x == x, etc., which can happen due to
// copying/flipping).
if (auto* other = std::get_if<Index>(&actual.term)) {
if (*other == index) {
return;
}
}
// Refer to the constraints for this index. If this is the first access of
// the local, then we insert a new item into the map, which has a default of
// proxesEverything, which we need to flip (provesEverything cannot otherwise
// be found in the map, as we never store it).
auto [iter, _] = map.insert({index, AndedConstraintSet::makeProvesNothing()});
auto& indexConstraints = iter->second;
// As in ::set(), this makes the map temporarily invalid until the
// approximateAnd, as we don't store proves-nothing in the map, normally.
indexConstraints.approximateAnd(actual);
if (indexConstraints.provesEverything()) {
// We just proved we are in unreachable code.
unreachable = true;
map.clear();
return;
}
// We just added a constraint, so we can prove something (we may lose some
// information as this is an approximate AND, but we cannot lose it all).
assert(!indexConstraints.provesNothing());
// Add a ref of what we are adding. Note that the approximation above may end
// up not actually adding this, or adding only part of this, but it is safe to
// always add a ref (at the cost of minor wasted work).
noteRefs(index, actual);
// If this is not the flipped version, and it refers to a local, add the
// flipped one too.
if (!flip && std::holds_alternative<Index>(actual.term)) {
approximateAndInternal(index, actual, true, isCopy);
if (unreachable) {
// We just found a contradiction.
return;
}
}
// If this constraint is simply "== x", then we are equal to that other local
// x, and can copy its constraints (if we are not already such a copy).
if (!isCopy) {
if (auto* other = std::get_if<Index>(&actual.term)) {
if (actual.op == Abstract::Eq) {
for (auto& otherC : get(*other)) {
approximateAndInternal(index, otherC, false, true);
if (unreachable) {
return;
}
}
}
}
}
}
void BasicBlockConstraintMap::noteRefs(Index index, const Constraint& c) {
if (auto* i = std::get_if<Index>(&c.term)) {
refs[*i].insert(index);
}
}
void BasicBlockConstraintMap::eraseStaleRefs(Index index) {
auto iter = refs.find(index);
if (iter == refs.end()) {
return;
}
auto& refIndexes = iter->second;
for (auto refIndex : refIndexes) {
if (auto iter = map.find(refIndex); iter != map.end()) {
auto& refConstraints = iter->second;
std::erase_if(refConstraints, [&](const auto& c) {
if (auto* i = std::get_if<Index>(&c.term)) {
if (*i == index) {
return true;
}
}
return false;
});
if (refConstraints.empty()) {
// This became trivial.
map.erase(iter);
}
}
}
}
std::ostream& operator<<(std::ostream& o, const Constraint& c) {
o << "Constraint{" << c.op << ", ";
if (auto* cc = std::get_if<Literal>(&c.term)) {
o << *cc;
} else if (auto* i = std::get_if<Index>(&c.term)) {
o << "Index(" << *i << ')';
}
o << '}';
return o;
}
std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set) {
if (set.provesEverything()) {
o << "AndedConstraintSet(contradiction)";
return o;
}
o << "AndedConstraintSet{";
bool first = true;
for (auto& constraint : set) {
if (first) {
first = false;
} else {
o << ", ";
}
o << constraint;
}
o << '}';
return o;
}
std::ostream& operator<<(std::ostream& o, const BasicBlockConstraintMap& map) {
if (map.unreachable) {
o << "BasicBlockConstraintMap(unreachable)";
return o;
}
o << "BasicBlockConstraintMap{";
bool first = true;
for (auto& [local, constraints] : map.map) {
if (first) {
first = false;
} else {
o << ", ";
}
o << local << ": " << constraints;
}
o << '}';
return o;
}
} // namespace wasm::constraint