-
Notifications
You must be signed in to change notification settings - Fork 871
ConstraintAnalysis: Handle x == C || { x > C && x <= D } #8933
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
37e3f98
aa3dc24
6c7e2b0
8854733
f9ae5f1
f708a61
d00bed6
e58adf7
d9f1f87
1617154
0da0740
e793be3
e508ca6
48ee24c
c4b9d36
b31d7f9
f0dd6c8
cf8f32e
f6b58b4
60e9620
a4fc9e9
7d337dc
fb07869
023cb40
6268c82
f15411c
fd61010
9a7e163
9458a8a
0628af6
7e267ba
71736fd
859faa7
e72fa61
ebcfa80
8c76ee7
65f7508
f28a35a
2b6e73b
753490d
fa1144a
dc858e8
ed9d41f
00b33c5
e470ae1
65f85dd
2ac1514
e713c6b
f478cd0
70085e5
d153b16
e16c4c6
5ad4c40
abcfee8
a3d9a54
4b16180
14e5098
3c59516
ea598cb
f31ae72
6a3d5bc
437174c
ba4f0b5
8a4f04a
1b9efc0
0789050
5f0ad62
242dd6e
cda9027
b141979
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 |
|---|---|---|
|
|
@@ -205,6 +205,192 @@ void AndedConstraintSet::approximateAnd(const Constraint& c) { | |
| // useful to implement that). | ||
| } | ||
|
|
||
| namespace { | ||
|
|
||
| // A variable in a match. If we see the same Var - identified by address - in | ||
| // two places, it must be equal in them. For example, | ||
| // | ||
| // Var A; | ||
| // ..match expression with foo(A, A).. | ||
| // | ||
| // The matcher will check that foo is sent the same thing twice. | ||
| struct Var {}; | ||
|
|
||
| // A matcher constraint: an abstraction over a normal Constraint, which can also | ||
| // contain Vars. | ||
| struct MatcherConstraint { | ||
| Abstract::Op op; | ||
| // Var addresses are how we identify them, so we store a pointer. | ||
| Var* term = nullptr; | ||
|
|
||
| MatcherConstraint() = default; | ||
| MatcherConstraint(Abstract::Op op, Var& term) : op(op), term(&term) {} | ||
|
|
||
| bool operator==(const MatcherConstraint&) const = default; | ||
| bool operator<(const MatcherConstraint& other) const { | ||
| // Ordered in a parallel way to Constraints, so that when we compare, things | ||
| // line up. | ||
| if (op != other.op) { | ||
| return op < other.op; | ||
| } | ||
| return term < other.term; | ||
| } | ||
| }; | ||
|
|
||
| // A matcher AndedConstraintSet, which abstracts over the normal one to support | ||
| // MatcherConstraints. | ||
| using MatcherSet = inplace_vector<MatcherConstraint, MaxConstraints>; | ||
|
|
||
| // A matcher object. This pattern-matches over abstractions of | ||
| // AndedConstraintSets. | ||
| struct Matcher { | ||
| // Set up a pattern containing two sets of constraints. | ||
| Matcher(const MatcherSet& ms1_, const MatcherSet& ms2_); | ||
|
|
||
| // Add a requirement on this pattern, a demand on the Vars. For example, we | ||
| // can require that Var A - whatever we matched it as - is less than Var B - | ||
| // whatever we matched that as. | ||
| // | ||
| // For convenience, return this Matcher object (i.e. the builder pattern). | ||
| Matcher& require(Var& a, Abstract::Op op, Var& b); | ||
|
|
||
| // When a match succeeds, we return a map of Vars to Terms, allowing the user | ||
| // to find out what each Var matched against. For example, the pattern foo(A) | ||
| // when matched successfully against foo(5) will provide a mapping of A to 5. | ||
| struct VarTermMap : public std::unordered_map<Var*, Term> { | ||
| // As a convenience, allow using the object instead of the pointer. | ||
| Term& operator[](Var& var) { | ||
| return std::unordered_map<Var*, Term>::operator[](&var); | ||
| } | ||
| }; | ||
|
|
||
| // Check if the pattern matches given inputs. The order of the inputs does not | ||
| // matter. Returns the mapping described above, if we succeed. | ||
| std::optional<VarTermMap> checkUnordered(const AndedConstraintSet& a, | ||
| const AndedConstraintSet& b) { | ||
| return checkUnorderedInternal(a, b); | ||
| } | ||
|
|
||
| private: | ||
| std::optional<VarTermMap> checkUnorderedInternal(const AndedConstraintSet& a, | ||
| const AndedConstraintSet& b, | ||
| bool flipped = false); | ||
|
|
||
| MatcherSet ms1; | ||
| MatcherSet ms2; | ||
|
|
||
| struct Requirement { | ||
| Var* a; | ||
| Abstract::Op op; | ||
| Var* b; | ||
| }; | ||
|
|
||
| SmallVector<Requirement, 2> requirements; | ||
| }; | ||
|
|
||
| Matcher::Matcher(const MatcherSet& ms1_, const MatcherSet& ms2_) | ||
| : ms1(ms1_), ms2(ms2_) { | ||
| // Sort the sets like Constraints are sorted, so that when we compare, things | ||
| // line up. | ||
| std::sort(ms1.begin(), ms1.end()); | ||
| std::sort(ms2.begin(), ms2.end()); | ||
| } | ||
|
|
||
| Matcher& Matcher::require(Var& a, Abstract::Op op, Var& b) { | ||
| requirements.push_back({&a, op, &b}); | ||
| return *this; | ||
| } | ||
|
|
||
| std::optional<Matcher::VarTermMap> Matcher::checkUnorderedInternal( | ||
| const AndedConstraintSet& a, const AndedConstraintSet& b, bool flipped) { | ||
|
|
||
| // TODO: optimize all this for speed | ||
|
|
||
| auto fail = [&]() -> std::optional<Matcher::VarTermMap> { | ||
| // We failed, but try the flipped inputs if we haven't already. | ||
| if (!flipped) { | ||
| return checkUnorderedInternal(b, a, true); | ||
| } | ||
| return {}; | ||
| }; | ||
|
|
||
| if (a.size() != ms1.size() || b.size() != ms2.size()) { | ||
| return fail(); | ||
| } | ||
|
|
||
| // The sizes match, at least. Parse in more detail, building up a mapping of | ||
| // Vars to concrete Terms. | ||
| VarTermMap varTermMap; | ||
|
|
||
| auto parse = [&](const AndedConstraintSet& input, const MatcherSet& pattern) { | ||
| for (Index i = 0; i < input.size(); i++) { | ||
| // The operation must match. | ||
| if (input[i].op != pattern[i].op) { | ||
| return false; | ||
| } | ||
|
|
||
| // The term must match, or define a new unknown value. | ||
| auto [iter, inserted] = | ||
| varTermMap.insert({pattern[i].term, input[i].term}); | ||
| if (!inserted) { | ||
| // The Var in the pattern is already mapped and known. The input here | ||
| // must match the prior appearance. | ||
| if (input[i].term != iter->second) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| return true; | ||
| }; | ||
|
|
||
| if (!parse(a, ms1) || !parse(b, ms2)) { | ||
| return fail(); | ||
| } | ||
|
|
||
| // Check requirements on the vars. | ||
| for (auto& [a, op, b] : requirements) { | ||
| auto aTerm = varTermMap[*a]; | ||
| auto bTerm = varTermMap[*b]; | ||
|
|
||
| // Check if { x == a } proves { x op b } is true. | ||
| if (provesPair({Abstract::Eq, aTerm}, {op, bTerm}) != True) { | ||
| return fail(); | ||
| } | ||
| } | ||
|
|
||
| return varTermMap; | ||
| } | ||
|
|
||
| // Do an approximate OR on two inputs that are disjoint, that is, each proves | ||
| // the other false. | ||
| // | ||
| // If we recognize a pattern, we update |self| and return whether anything | ||
| // changed (otherwise, we return nullopt). | ||
| std::optional<bool> approximateOrDisjoint(AndedConstraintSet& self, | ||
| const AndedConstraintSet& other) { | ||
| using namespace Abstract; | ||
|
|
||
| // Simple range fusing, add an equality to turn > into >= : | ||
| // | ||
| // { x == A } || { x > A && x <= B } , A < B ==> { x >= A && X <= B } | ||
| // | ||
| // (A < B is required to avoid a contradiction on the right) | ||
| Var A, B; | ||
| if (auto result = Matcher({{Eq, A}}, {{GtS, A}, {LeS, B}}) | ||
| .require(A, LtS, B) | ||
| .checkUnordered(self, other)) { | ||
|
Comment on lines
+373
to
+381
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's not clear to me that we need this kind of global view on the constraints and all the complexity that brings. We should be able to define an
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, I had a related thought on a walk a took now. ORing can never generate a new contradiction, by definition - so my caution in this PR is not really needed. So, yes, I think I can make us look at individual constraints here, as you wrote (but 1 is not needed, only 2).
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. Ah, a case that does require a more global view is |
||
| auto& values = *result; | ||
| self = {{GeS, values[A]}, {LeS, values[B]}}; | ||
| return true; | ||
| // TODO: unsigned etc. | ||
| } | ||
|
|
||
| // Otherwise, we have no idea. | ||
| return {}; | ||
| } | ||
|
|
||
| } // anonymous namespace | ||
|
|
||
| bool AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { | ||
| // If one proves everything, the only thing that matters is the other. | ||
| if (other.provesEverything()) { | ||
|
|
@@ -218,18 +404,28 @@ bool AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { | |
| // 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) { | ||
| auto otherProves = other.proves(*this); | ||
| if (otherProves == True) { | ||
| return false; | ||
| } | ||
| if (proves(other) == True) { | ||
| auto thisProves = proves(other); | ||
| if (thisProves == True) { | ||
| *this = other; | ||
| return true; | ||
| } | ||
|
|
||
| // TODO smarts: handle <= > and so forth | ||
| if (otherProves == False && thisProves == False) { | ||
| if (auto result = approximateOrDisjoint(*this, other)) { | ||
| return *result; | ||
| } | ||
| } | ||
| // TODO more 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. | ||
| // TODO: We could keep constraints that appear on both sides and only drop the | ||
| // others. In fact, we could do that first, and leave smarts only to the | ||
| // others. | ||
| clear(); | ||
| return true; | ||
| } | ||
|
|
||
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 does not seem worth defining a subtype of
std::unordered_mapfor.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.
Moot point, but look how pretty it makes the final matching code 😄 Not a single wasted character, not even a
&