blob: a1be14c6bf007d1a055c6007656a8fc219e41e29 [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 {
// 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) {
// x == X =?=> x == Y. True iff X == Y.
if (aOp == Abstract::Eq && bOp == Abstract::Eq) {
return aConstant == bConstant ? True : False;
}
// x == X =?=> x != Y. True iff X != Y.
if (aOp == Abstract::Eq && bOp == Abstract::Ne) {
return aConstant == bConstant ? False : True;
}
// x != X =?=> x == Y. False if X = Y, else unknown.
if (aOp == Abstract::Ne && bOp == Abstract::Eq) {
if (aConstant == bConstant) {
return False;
}
}
// x != X =?=> x != Y. True if X = Y, else unknown.
if (aOp == Abstract::Ne && bOp == Abstract::Ne) {
if (aConstant == bConstant) {
return True;
}
}
// TODO: handle >, >=, <, 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;
}
void AndedConstraintSet::approximateAnd(const Constraint& c) {
if (provesEverything()) {
// Nothing to add.
return;
}
if (proves(c) == False) {
// We are now a contradiction.
isContradiction = true;
return;
}
if (size() < MaxConstraints) {
push_back(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.
}
void AndedConstraintSet::approximateOr(const AndedConstraintSet& other) {
// If one proves everything, the only thing that matters is the other.
if (provesEverything()) {
*this = other;
return;
}
if (other.provesEverything()) {
return;
}
// 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 (proves(other) == True) {
*this = other;
return;
}
if (other.proves(*this) == True) {
return;
}
// TODO smarts: handle <= > and so forth
// Otherwise, we don't know how to nicely OR these things, and expand to the
// trivial set of no constraints.
clear();
}
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}) {
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) {
assert(!unreachable);
eraseStaleRefs(index);
map[index].set(c);
noteRefs(index, c);
// If the constraint refers to another local, add it there too.
if (std::holds_alternative<Index>(c.term)) {
approximateAndInternal(index, c, true);
}
}
void BasicBlockConstraintMap::setProvesNothing(Index index) {
assert(!unreachable);
eraseStaleRefs(index);
map.erase(index);
}
void BasicBlockConstraintMap::approximateOr(
const BasicBlockConstraintMap& other) {
// If one is unreachable, it adds nothing to the other.
if (other.unreachable) {
return;
}
if (unreachable) {
*this = other;
return;
}
// 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).
for (auto& [local, constraints] : map) {
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());
return constraints.provesNothing();
});
}
void BasicBlockConstraintMap::approximateAndInternal(Index index,
const Constraint& c,
bool flip) {
Constraint actual = c;
if (flip) {
LocalConstraint flipped{index, c};
flipped.flip();
index = flipped.local;
actual = flipped.constraint;
}
auto combined = get(index);
combined.approximateAnd(actual);
if (combined.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(!combined.provesNothing());
// Otherwise, this is an interesting state; set it.
map[index] = std::move(combined);
// 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);
}
}
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{" << int(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