-
Notifications
You must be signed in to change notification settings - Fork 867
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
Changes from 21 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
bdc4911
9b4a261
c924041
9645e71
64e0a86
0e0bfb7
6947fc3
2b30208
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,133 @@ | ||
| /* | ||
| * 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 { | ||
|
|
||
| // Core comparison of two constraints: whether a => b | ||
| // | ||
| // Returns a Result, or an empty option if we should keep working (i.e., a | ||
| // result of Unknown means we are certain we can just return Unknown). | ||
| std::optional<Result> checkPair(const Constraint& a, const Constraint& b) { | ||
| // A thing always implies itself. | ||
| if (a == b) { | ||
| return True; | ||
| } | ||
|
|
||
| // Comparisons of two constants. | ||
| if (auto* aConstant = std::get_if<Literal>(&a.value)) { | ||
| if (auto* bConstant = std::get_if<Literal>(&b.value)) { | ||
| switch (a.op) { | ||
| case Abstract::Eq: { | ||
| switch (b.op) { | ||
| case Abstract::Eq: { | ||
| // x == c vs x == c', and we already handled full equality | ||
| // earlier, hence c != c', and we found a contradiction. | ||
| assert(*aConstant != *bConstant); | ||
| return False; | ||
| } | ||
| case Abstract::Ne: { | ||
| // x == c vs x != c'. We can infer the result based on relating c | ||
| // and c'. | ||
| return *aConstant != *bConstant ? True : False; | ||
| } | ||
| default: { | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| case Abstract::Ne: { | ||
| switch (b.op) { | ||
| case Abstract::Eq: { | ||
| // x != c vs x == c'. If c == c', we can infer. | ||
| if (*aConstant == *bConstant) { | ||
| return False; | ||
| } | ||
| return {}; | ||
| } | ||
| case Abstract::Ne: { | ||
| // x != c vs x != c', and we already handled full equality | ||
| // earlier, hence c != c', and we can infer nothing. | ||
| assert(*aConstant != *bConstant); | ||
| return {}; | ||
| } | ||
| default: { | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| default: { | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return {}; | ||
| } | ||
|
|
||
| } // anonymous namespace | ||
|
|
||
| Result AndedConstraintSet::check(const Constraint& condition) const { | ||
| // Sometimes a single constraint is enough to determine the condition. | ||
| for (auto& c : *this) { | ||
| if (auto result = checkPair(c, condition)) { | ||
| return *result; | ||
| } | ||
| } | ||
|
|
||
| // TODO smarts for multiple constraints | ||
|
|
||
| // Otherwise, who knows. | ||
| return Unknown; | ||
| } | ||
|
|
||
| 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 (check(other) == True) { | ||
| *this = other; | ||
| return; | ||
| } | ||
| if (other.check(*this) == True) { | ||
| return; | ||
| } | ||
|
|
||
| // TODO smarts | ||
|
|
||
| // 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,152 @@ | ||
| /* | ||
| * 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 value in a constraint, either a local index or literal value. | ||
| struct Value : public std::variant<Index, Literal> { | ||
| bool operator==(const Value&) const = default; | ||
| }; | ||
|
|
||
| // A constraint: some operation and some value, like "is equal to 17" or "is | ||
| // less than local 6". | ||
| struct Constraint { | ||
| // The operation relating two values, and the values. | ||
| 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
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. I see your point now, though I'm not sure I agree - the invalid values are also nice for debugging, when you use an uninitialized value by mistake for example. Anyhow, this PR is consistent with the current state of the codebase - how about leaving it as is, and considering your change later, separately?
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. Though, since it is so small a change here, I guess I can remove it and keep the entire discussion for later. Done. |
||
| Value value; | ||
|
|
||
| bool operator==(const Constraint&) const = default; | ||
|
tlively marked this conversation as resolved.
|
||
|
|
||
| operator bool() const { return op != Abstract::Invalid; } | ||
|
tlively marked this conversation as resolved.
Outdated
|
||
| }; | ||
|
|
||
| // 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 check(const Constraint& condition) const; | ||
|
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. Perhaps
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. Hmm, yeah. Another option is
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. Thinking more on this, I think that So
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 would strongly prefer some variant of
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. (FWIW, the tests use "proves" in their comments)
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. Ok, I don't feel strongly. Renamed to |
||
|
|
||
| // Check an entire other set. | ||
| Result check(const AndedConstraintSet& other) const { | ||
| if (other.empty()) { | ||
| // The empty set of constraints is always true. | ||
| return True; | ||
| } | ||
|
|
||
| Result result = Unknown; | ||
| for (auto& c : other) { | ||
| auto currResult = check(c); | ||
| if (currResult == Unknown) { | ||
| // If something is unknown, it all is. | ||
| return Unknown; | ||
| } | ||
| if (result == Unknown) { | ||
| // This is the first result | ||
| result = currResult; | ||
| } else if (result != currResult) { | ||
| // This is a later result, and different, so give up. | ||
| return Unknown; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| bool full() const { return size() == MaxConstraints; } | ||
|
|
||
| // Add a constraint to the set, ANDed with the others. The caller must make | ||
| // sure not to add too many (i.e. it is invalid to call this when full()). | ||
| void and_(const Constraint& c) { | ||
|
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. It would be nice to avoid burdening the caller with making this decision. We could just arbitrarily drop extra constraints, or allow the user to supply a heuristic for determining which to keep. The end result will be the same if we remove this burden from the caller, since the caller will otherwise have to make the same decisions.
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. Interesting, yeah, maybe that is better. Should it then be
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. Yeah. Maybe "bounded" or "approximate" instead of "fuzzy?" Or alternatively we could use "join" and "meet" language to differentiate these operations from the precise logical connectives.
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's wrong with "fuzzy", as a short and clear way to put it? I prefer something with
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. "Fuzzy logic" is a completely different thing: https://en.wikipedia.org/wiki/Fuzzy_logic. This is more about soundly over- or under-approximating our knowledge as we analyze the program.
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. Ah, fair point. Ok, renamed to |
||
| assert(!full()); | ||
| push_back(c); | ||
| } | ||
|
|
||
| // Merge constraints using OR. We cannot represent such a thing directly | ||
|
kripken marked this conversation as resolved.
Outdated
|
||
| // (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 | ||
|
kripken marked this conversation as resolved.
Outdated
|
||
| // 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 check(): | ||
| // | ||
| // { x >= 5 && x <= 10 }.check({ 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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
{ x >= 1 /\ x <= 4 }, but right now it just gives up.This might be included in the
// TODO smartsbelow, but I think it's important to see the full complexity here so we can get it factored as nicely as possible.There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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
Constraints, so I guess we don't need to do it now.There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.