| /* | |
| * 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. | |
| */ | |
| // | |
| // Constraints on the values of things, like x >=0, x < 42, and x == y. Allows | |
| // inference whether other things are true given a set of constraints, like | |
| // { x == 10 } => { x >= 5 }. | |
| // | |
| // This code follows the basic rules of logic, like the law of the excluded | |
| // middle and so forth. Things that do not follow basic logic, like floating- | |
| // point NaNs (where both x < y and x >= y are possible, with NaNs), may not be | |
| // handled correctly. The caller must ensure that no such input is possible. | |
| // | |
| #ifndef wasm_ir_constraint_h | |
| #define wasm_ir_constraint_h | |
| #include <variant> | |
| #include "ir/abstract.h" | |
| #include "support/inplace_vector.h" | |
| #include "support/small_vector.h" | |
| #include "support/sorted_vector.h" | |
| #include "support/span.h" | |
| #include "support/utilities.h" | |
| #include "wasm.h" | |
| namespace wasm::constraint { | |
| // A term in a constraint, either a local index or literal value. | |
| struct Term : public std::variant<Index, Literal> { | |
| bool operator==(const Term&) const = default; | |
| bool operator<(const Term& other) const { | |
| if (index() != other.index()) { | |
| return index() < other.index(); | |
| } | |
| if (index() == 0) { | |
| return std::get<Index>(*this) < std::get<Index>(other); | |
| } | |
| return std::get<Literal>(*this) < std::get<Literal>(other); | |
| } | |
| }; | |
| // A constraint: some operation and some value, like "is equal to 17" or "is | |
| // less than local 6". | |
| struct Constraint { | |
| Abstract::Op op; | |
| Term term; | |
| bool operator==(const Constraint&) const = default; | |
| bool operator<(const Constraint& other) const { | |
| if (op != other.op) { | |
| return op < other.op; | |
| } | |
| return term < other.term; | |
| } | |
| Constraint negate() const { | |
| return Constraint{Abstract::negateRelational(op), term}; | |
| } | |
| // Convert the constraint into constant spans, if possible. For example, | |
| // "<= 100 (unsigned)" turns into the span [0, 100]. We use SpansU2 because | |
| // that can also handle signed operations stored in the unsigned range (see | |
| // span.h). | |
| // | |
| // An optional type may be passed in. If not, the type is inferred from the | |
| // term, when possible. | |
| std::optional<SpansU2> getSpans(std::optional<Type> type = {}) const; | |
| // Get spans we can prove. This is less precise than getSpans, which gets | |
| // *exact* spans for the Constraint. Here we only return spans that we can | |
| // prove are 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<SpansU2> getProvenSpans(std::optional<Type> type = {}) const; | |
| }; | |
| // We limit constraints to a low number to ensure good performance even with | |
| // simple brute-force solving. | |
| // TODO: use a generic constraint solver..? | |
| inline constexpr std::size_t MaxConstraints = 3; | |
| // What we infer from one thing about another: true/false, or unknown. | |
| enum Result { True, False, Unknown }; | |
| // A set of constraints connected by the logical "and" operation. That is, all | |
| // the constraints are simultaneously true about some value. In the examples in | |
| // the comments below, `x` is used for the thing all the constraints are talking | |
| // about, which looks like a local, but it could be a global or a struct field | |
| // or anything else in general. | |
| // | |
| // While we are a vector, the order of constraints does not logically matter, | |
| // and we keep ourselves sorted in a canonical form, so that simple ==, != etc. | |
| // comparisons work. The canonical order also makes debug printing nicer. | |
| struct AndedConstraintSet : inplace_vector<Constraint, MaxConstraints> { | |
| // We could represent a contradiction using two constraints that contradict | |
| // each other (== 0 && != 0), but for simplicity we mark this explicitly. | |
| // | |
| // A contradiction is the default value here, because it represents | |
| // unreachable code: if (x == 0 && x != 0) { .. unreachable ..}. That is, we | |
| // assume we represent the constraints in code that has not been reached, | |
| // until something changes. | |
| bool isContradiction = true; | |
| AndedConstraintSet() = default; | |
| AndedConstraintSet(std::initializer_list<Constraint> constraints) { | |
| isContradiction = false; | |
| for (auto& c : constraints) { | |
| approximateAnd(c); | |
| } | |
| } | |
| // Proving everything (even contradictions) is equivalent to being a | |
| // contradiction. (This and provesNothing can be seen as the top/bottom of a | |
| // poset, if one wants to think of things that way.) | |
| bool provesEverything() const { return isContradiction; } | |
| void setProvesEverything() { | |
| clear(); | |
| isContradiction = true; | |
| assert(provesEverything()); | |
| } | |
| // An empty set of contradictions means we know nothing, and so anything is | |
| // possible, and we can prove nothing. | |
| bool provesNothing() const { return empty(); } | |
| void setProvesNothing() { | |
| clear(); | |
| isContradiction = false; | |
| assert(provesNothing()); | |
| } | |
| static AndedConstraintSet makeProvesNothing() { | |
| AndedConstraintSet ret; | |
| ret.setProvesNothing(); | |
| return ret; | |
| } | |
| // Check a condition against this set, that is, whether the existing | |
| // constraints prove that it must be true, false, or unknown: whether | |
| // | |
| // { this } => { condition } | |
| // | |
| // https://en.wikipedia.org/wiki/Material_conditional#Truth_table | |
| Result proves(const Constraint& condition) const; | |
| // Check an entire other set. | |
| Result proves(const AndedConstraintSet& other) const; | |
| // Add a constraint to the set, ANDed with the others. This is an approximate | |
| // operation because our capacity is bounded - we cannot have more than | |
| // MaxConstraints. If too many are added, we will drop some, which means we | |
| // will be able to prove less things (but we will never prove anything | |
| // incorrectly). | |
| void approximateAnd(const Constraint& c); | |
| // Merge constraints using OR. We cannot always represent such a thing | |
| // directly (we only use AND), so we approximate it. For example, this would | |
| // be valid: | |
| // | |
| // approximateOr({ x == 5 }, { x == 10 }) == { x >= 5 && x <= 10 } | |
| // | |
| // Note how the result here still accepts the values 5 and 10, but it also | |
| // allows more. Formally, this has the following mathematical property: | |
| // | |
| // (X || Y) => approximateOr(X, Y) | |
| // | |
| // That is, if X or Y is true, the result of approximateOr is also true. But | |
| // the reverse is not always so: approximateOr may be true without X || Y | |
| // being true (see the truth table linked above, and the value 8 in the | |
| // example). | |
| // | |
| // Returning to the example, we can use this to optimize as follows: if | |
| // two code paths reaching a location have x == 5 and x == 10, so the value in | |
| // the merge location is either 5 or 10, then if we see some i32.ge_s that | |
| // does x >= 0 then we can evaluate it with proves(): | |
| // | |
| // { x >= 5 && x <= 10 }.proves({ x >= 0 }) == True | |
| // | |
| // And it is valid to optimize that i32.ge_s into a constant 1, since | |
| // | |
| // { x == 5 || x == 10 } => | |
| // { x >= 5 && x <= 10 } => | |
| // { x >= 0 } | |
| // | |
| // I.e. the constraints imply the truth of the thing we are evaluating. | |
| // | |
| // Note that the approximation here means that approximateOr() can do a better | |
| // / worse job. It is always valid for approximateOr to return { } or any | |
| // other always-true thing (see the truth table linked above). But then: | |
| // | |
| // { x == 5 || x == 10 } => | |
| // { } =!!> | |
| // { x >= 0 } | |
| // | |
| // If we become too imprecise, we lose the ability to imply anything useful. | |
| // | |
| // Returns whether we changed anything. | |
| bool approximateOr(const AndedConstraintSet& other); | |
| // Set a constraint, replacing all previous state. | |
| void set(const Constraint& c) { | |
| setProvesNothing(); | |
| push_back(c); | |
| } | |
| // If the set of constraints shows us as equal to a literal, return it. | |
| std::optional<Literal> getLiteral() const { | |
| for (auto& c : *this) { | |
| if (c.op == Abstract::Eq) { | |
| if (auto* cc = std::get_if<Literal>(&c.term)) { | |
| return *cc; | |
| } | |
| } | |
| } | |
| return {}; | |
| } | |
| }; | |
| // A local plus a constraint on it. | |
| struct LocalConstraint { | |
| Index local; | |
| Constraint constraint; | |
| LocalConstraint() = default; | |
| LocalConstraint(Index local, Constraint constraint) | |
| : local(local), constraint(std::move(constraint)) {} | |
| bool operator==(const LocalConstraint&) const = default; | |
| // Try to parse BinaryenIR into a local to which a constraint is applied. For | |
| // example | |
| // | |
| // (i32.eq (local.get $r) (i32.const 10)) | |
| // | |
| // parses into | |
| // | |
| // LocalConstraint($r, { x == 10 }) | |
| // | |
| static std::optional<LocalConstraint> parse(Expression* curr); | |
| LocalConstraint negate() const { | |
| return LocalConstraint{local, constraint.negate()}; | |
| } | |
| // Reverse the constraint. The constraint's term must, of course, be another | |
| // local. | |
| void flip(); | |
| }; | |
| // A utility to parse BinaryenIR into locals and constraints on them. This is | |
| // similar to LocalConstraint::parse, but that parses a single constraint, while | |
| // this can handle a list of ANDed ones: | |
| // | |
| // (i32.and (..A..) (..B..)) | |
| // | |
| // parses into [ A, B ]. | |
| // | |
| // We also set a field |hasUnknown| if we saw things we could not parse. E.g. | |
| // | |
| // (i32.and (call $unknown) (i32.eqz (local.get $x))) | |
| // | |
| // This parses into [ $x == 0 ] and sets hasUnknown=true. Even if there are | |
| // unknown things, we do know that definitely $x == 0 at least, which is useful | |
| // in some cases. | |
| struct ParsedAndedConstraints : public SmallVector<LocalConstraint, 1> { | |
| using SmallVector<LocalConstraint, 1>::SmallVector; | |
| bool hasUnknown = false; | |
| static ParsedAndedConstraints parse(Expression* curr); | |
| // Parse in a condition context, i.e., where (local.get $x) is the same as | |
| // $x != 0 (e.g., in an if condition, or a br_on ref). | |
| static ParsedAndedConstraints parseCondition(Expression* curr); | |
| // Negate the entire list of constraints. If we fail to generate something | |
| // that can be represented as a list of ANDed constraints, the list will be | |
| // empty (i.e., we can prove nothing). | |
| void negate(); | |
| }; | |
| // A map of locals and their constraints, representing the state at a basic | |
| // block. We use the following representation: | |
| // | |
| // * When the basic block is unreachable, we mark ourselves so, and clear the | |
| // map. | |
| // * If any local is in a contradiction, we know we are unreachable, and mark | |
| // ourselves so. | |
| // * When we can prove nothing about a local - the common case - we leave it | |
| // out of the map. | |
| // * If a local is unusable - a non-nullable local before any set - then it is | |
| // marked as being able to prove nothing. (We could also mark this as a | |
| // contradiction, but both apply - we can indeed prove nothing, as the IR | |
| // disallows any uses of it - and avoiding a contradiction avoids confusion | |
| // with the case of the basic block being unreachable.) | |
| // | |
| // As a result, the map only contains interesting things, where we can prove | |
| // something (but not everything). | |
| // | |
| // Cross-local constraints (like x == y) are duplicated, that is, they appear in | |
| // the constraints for both x and y. This makes things simple by having all | |
| // constraints related to a local in the same place. | |
| struct BasicBlockConstraintMap { | |
| // Blocks begin unreachable, like AndedConstraintSet. | |
| bool unreachable = true; | |
| void setReachable() { | |
| unreachable = false; | |
| assert(map.empty()); | |
| } | |
| // Apply a constraint to a local, replacing anything before. | |
| void set(Index index, const Constraint& c); | |
| // Apply a set of constraints to a local, replacing anything before. | |
| void set(Index index, const AndedConstraintSet& constraints); | |
| // Set the value in an expression to a local, replacing anything before. | |
| void set(Index index, Expression* value); | |
| // Mark a local as unknown and able to prove nothing. | |
| void setProvesNothing(Index index); | |
| // Get the constraints for a local. | |
| AndedConstraintSet get(Index index) const { | |
| // We should not be called in unreachable code. | |
| assert(!unreachable); | |
| if (auto iter = map.find(index); iter != map.end()) { | |
| auto& constraints = iter->value; | |
| // If we can prove nothing, we should have removed it from the map. | |
| assert(!constraints.provesNothing()); | |
| // If we can prove everything, we should be entirely unreachable. | |
| assert(!constraints.provesEverything()); | |
| return constraints; | |
| } | |
| return AndedConstraintSet::makeProvesNothing(); | |
| } | |
| // Perform an OR as above. When a local only appears in one map, we treat it | |
| // as if it contains a contradiction there, that is, as if the code is | |
| // unreachable. | |
| // | |
| // Returns whether we changed anything. | |
| bool approximateOr(const BasicBlockConstraintMap& other); | |
| // Perform an AND as above, on a particular index. | |
| void approximateAnd(Index index, const Constraint& c) { | |
| approximateAndInternal(index, c); | |
| } | |
| // Check a condition on a local, given all we know about all other locals. | |
| Result proves(LocalConstraint condition) const; | |
| bool operator!=(const BasicBlockConstraintMap& other) const { | |
| return unreachable != other.unreachable || map != other.map; | |
| } | |
| friend std::ostream& operator<<(std::ostream& o, | |
| const BasicBlockConstraintMap& map); | |
| private: | |
| // Wrap a combination of an index and a value, and sort using only the index. | |
| template<typename T> struct Indexed { | |
| Index index; | |
| T value; | |
| bool operator<(const Indexed& other) const { return index < other.index; } | |
| bool operator<(Index otherIndex) const { return index < otherIndex; } | |
| bool operator==(const Indexed& other) const { | |
| return index == other.index && value == other.value; | |
| } | |
| bool operator==(const Index& otherIndex) const { | |
| return index == otherIndex; | |
| } | |
| }; | |
| // Sorted by local Index for fast contiguous copying and linear-time merge in | |
| // approximateOr. | |
| SortedVector<Indexed<AndedConstraintSet>> map; | |
| // Maps an index to the locals that have constraints referring to it, sorted | |
| // by index. When a local is modified, we need to wipe all those constraints, | |
| // which become stale. | |
| // | |
| // It is ok (but unoptimal in efficiency) if we have stale refs here, e.g. due | |
| // to approximation removing a constraint. Whenever there is a reference, | |
| // however, it must be noted here, so that when things get stale we can remove | |
| // them. | |
| SortedVector<Indexed<SortedVector<Index>>> refs; | |
| // Given a constraint on a local, note refs. | |
| void noteRefs(Index index, const Constraint& c); | |
| // Given an index, erase constraints referring to it. This is called when the | |
| // information for this index is wiped out, so we clear the refs and the | |
| // constraints referred to. | |
| void eraseStaleRefs(Index index); | |
| // Internal version, with a flag to flip the constraint. Whenever we apply | |
| // e.g. x == y, we also apply y == x to y, to maintain the invariant described | |
| // above. When flip is true, we flip the constraint and apply it to the other | |
| // index (y == x, in this example). When isCopy is true, we are a copied | |
| // constraint from another local, and we do not need to add new copies of it. | |
| void approximateAndInternal(Index index, | |
| const Constraint& c, | |
| bool flip = false, | |
| bool isCopy = false); | |
| }; | |
| std::ostream& operator<<(std::ostream& o, const Constraint& c); | |
| std::ostream& operator<<(std::ostream& o, const LocalConstraint& c); | |
| std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set); | |
| } // namespace wasm::constraint | |
| #endif // wasm_ir_constraint_h |