-
Notifications
You must be signed in to change notification settings - Fork 863
Add a mathematical constraint system #8816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0fc9e55
5ad1b75
8181363
d0ad2f4
c5b7d1d
0e35b2b
60d50b4
626b5d7
5896406
cf29b58
cdaff6b
c02ba2e
94d2161
735d7ea
6e80fde
cf7fcc6
ac02454
0930461
7b7d2ac
679bd24
920e7a9
44ad794
2f0bdd7
7f77016
ea3db17
060d8aa
088a425
9267485
3965c27
280e7b3
26dfc30
c1dacec
3952dfc
224425c
203d455
e4a2a49
22d2d3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,8 @@ enum Op { | |
| GtS, | ||
| GtU, | ||
| GeS, | ||
| GeU | ||
| GeU, | ||
| Invalid | ||
| }; | ||
|
|
||
| inline bool hasAnyRotateShift(BinaryOp op) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| /* | ||
| * 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; | ||
| } | ||
|
|
||
| // 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 { | ||
| // 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 (other.empty()) { | ||
| // The empty set of constraints is always true. | ||
| return True; | ||
| } | ||
|
|
||
| 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::fuzzyAnd(const Constraint& c) { | ||
| 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::fuzzyOr(const AndedConstraintSet& other) { | ||
| // If one is empty (no constraints, everything is true, and we can prove | ||
| // nothing useful) then it does not add anything to the other. | ||
| if (empty()) { | ||
| *this = other; | ||
| return; | ||
| } | ||
| if (other.empty()) { | ||
| 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; | ||
| } | ||
|
Comment on lines
+138
to
+147
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't handle the case where the constraints can be relaxed in both directions separately. For example: This should give This might be included in the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean by "see the full complexity"? I'm not sure what you are asking this PR to do aside from have the existing TODO.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm suggesting we resolve the TODO :) But I guess I can just say in advance that the simplest way to do this will be in terms of a fuzzyOr operation on a pair of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I am trying to keep this PR small. I added to the comment. |
||
|
|
||
| // 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(); | ||
| } | ||
|
|
||
| } // namespace wasm::constraint | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /* | ||
| * 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 }. | ||
| // | ||
|
|
||
| #ifndef wasm_ir_constraint_h | ||
| #define wasm_ir_constraint_h | ||
|
|
||
| #include <variant> | ||
|
|
||
| #include "ir/abstract.h" | ||
| #include "support/inplace_vector.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; | ||
| }; | ||
|
|
||
| // A constraint: some operation and some value, like "is equal to 17" or "is | ||
| // less than local 6". | ||
| struct Constraint { | ||
| Abstract::Op op = Abstract::Invalid; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's much value in making invalid constraints representable (nor in reusing the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reusing Abstract is useful because we have code to parse IR into it. E.g. we need to parse AddInt32 into Abstract::Add (the next PR does this). Without this reuse, we'd need to duplicate that code, or add a mapping of Abstract into a new enum. I think this is exactly what Abstract is meant for: an abstract operation, without the details of a Type. This is precisely the right level of abstraction for a mathematical constraint system, mirroring the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to reuse the parsing code 👍
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can still avoid adding
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not have a default value. Users that need some placeholder can use |
||
| Term term; | ||
|
|
||
| bool operator==(const Constraint&) const = default; | ||
|
tlively marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| // We limit constraints to a low number to ensure good performance even with | ||
| // simple brute-force solving. | ||
| // TODO: use a generic constraint solver..? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did have that POC for pulling in Z3. In the limit I guess that's what we'd want. 5c2bbb7 |
||
| 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. | ||
| struct AndedConstraintSet : inplace_vector<Constraint, MaxConstraints> { | ||
| // 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 a fuzzy | ||
| // 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 fuzzyAnd(const Constraint& c); | ||
|
|
||
| // Merge constraints using OR. We cannot represent such a thing directly | ||
| // (we only use AND), so we approximate it in a fuzzy way. For example, this | ||
| // would be valid: | ||
| // | ||
| // fuzzyOr({ 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) => fuzzyOr(X, Y) | ||
| // | ||
| // That is, if X or Y is true, the result of fuzzOr is also true. But the | ||
| // reverse is not always the case: fuzzyOr 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 fuzziness here means that fuzzyOr() can do a better or a | ||
| // worse job. It is always valid for fuzzyOr 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 fuzzy, we lose the ability to imply anything useful. | ||
| void fuzzyOr(const AndedConstraintSet& other); | ||
| }; | ||
|
|
||
| } // namespace wasm::constraint | ||
|
|
||
| #endif // wasm_ir_constraint_h | ||
Uh oh!
There was an error while loading. Please reload this page.