-
Notifications
You must be signed in to change notification settings - Fork 868
ConstraintAnalysis: Properly handle local changes #8878
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 all commits
5c7b3ac
5314388
75e8ea3
204ae51
06c4591
fd96dbc
c597f52
f660b23
f2b8ce9
6c78762
b5786f8
69cc1b1
2fc8369
27241f0
f74e978
4f50ee3
e2d0497
8cc8c5f
62a496f
32b873b
123c31b
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 |
|---|---|---|
|
|
@@ -64,6 +64,11 @@ Result provesPair(const Constraint& a, const Constraint& 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); | ||
|
|
@@ -250,6 +255,36 @@ LocalConstraint::parseCondition(Expression* curr) { | |
| 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. | ||
|
|
@@ -276,9 +311,19 @@ void BasicBlockConstraintMap::approximateOr( | |
| }); | ||
| } | ||
|
|
||
| void BasicBlockConstraintMap::approximateAnd(Index index, const Constraint& c) { | ||
| 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; | ||
| } | ||
|
Comment on lines
+318
to
+323
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. Can we pull this flip operation out into a helper function, maybe on
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. Good idea, done. |
||
|
|
||
| auto combined = get(index); | ||
| combined.approximateAnd(c); | ||
| combined.approximateAnd(actual); | ||
|
|
||
| if (combined.provesEverything()) { | ||
| // We just proved we are in unreachable code. | ||
|
|
@@ -293,6 +338,50 @@ void BasicBlockConstraintMap::approximateAnd(Index index, const Constraint& c) { | |
|
|
||
| // 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,7 @@ struct Constraint { | |
|
|
||
| bool operator==(const Constraint&) const = default; | ||
|
|
||
| Constraint negate() { | ||
| Constraint negate() const { | ||
| return Constraint{Abstract::negateRelational(op), term}; | ||
| } | ||
| }; | ||
|
|
@@ -186,6 +186,10 @@ struct LocalConstraint { | |
| // 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 std::optional<LocalConstraint> parseCondition(Expression* curr); | ||
|
|
||
| // Reverse the constraint. The constraint's term must, of course, be another | ||
| // local. | ||
| void flip(); | ||
| }; | ||
|
|
||
| // A map of locals and their constraints, representing the state at a basic | ||
|
|
@@ -205,6 +209,10 @@ struct LocalConstraint { | |
| // | ||
| // 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. | ||
|
Comment on lines
+213
to
+215
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. Instead of storing each variable-variable constraint in both directions (which uses the limited space in our bounded conjunctions), we could arbitrarily just store it in one direction (possibly by choosing the variable with the most remaining free space for constraints), then on lookup rely on
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. Yes, exactly. That would be more efficient in space, but annoying to work with (and maybe slower). Given we have a limit of 3 constraints per local, I think duplication is fine here. |
||
| struct BasicBlockConstraintMap { | ||
| // Blocks begin unreachable, like AndedConstraintSet. | ||
| bool unreachable = true; | ||
|
|
@@ -215,20 +223,23 @@ struct BasicBlockConstraintMap { | |
| } | ||
|
|
||
| // Apply a constraint to a local. | ||
| void set(Index index, const Constraint& c) { | ||
| assert(!unreachable); | ||
| map[index].set(c); | ||
| } | ||
| void set(Index index, const Constraint& c); | ||
|
|
||
| void setProvesNothing(Index index) { | ||
| assert(!unreachable); | ||
| map.erase(index); | ||
| } | ||
| // 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()) { | ||
| return iter->second; | ||
| auto& constraints = iter->second; | ||
| // If we can prove nothing, we should have removed it from the map. | ||
| assert(!constraints.provesNothing()); | ||
|
Comment on lines
+238
to
+239
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 also assert that it's not a contradiction, right?
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. Yes, good point, done. |
||
| // If we can prove everything, we should be entirely unreachable. | ||
| assert(!constraints.provesEverything()); | ||
| return constraints; | ||
| } | ||
| return AndedConstraintSet::makeProvesNothing(); | ||
| } | ||
|
|
@@ -239,7 +250,13 @@ struct BasicBlockConstraintMap { | |
| void approximateOr(const BasicBlockConstraintMap& other); | ||
|
|
||
| // Perform an AND as above, on a particular index. | ||
| void approximateAnd(Index index, const Constraint& c); | ||
| void approximateAnd(Index index, const Constraint& c) { | ||
| approximateAndInternal(index, c); | ||
| } | ||
|
|
||
| // TODO: Add proves() here, which could do things like: if asked x == y, we | ||
| // can answer False if we see x == c1, y == c2, and the constants c1, c2 | ||
| // differ. | ||
|
|
||
| bool operator!=(const BasicBlockConstraintMap& other) { | ||
| return unreachable != other.unreachable || map != other.map; | ||
|
|
@@ -250,6 +267,29 @@ struct BasicBlockConstraintMap { | |
|
|
||
| private: | ||
| std::unordered_map<Index, AndedConstraintSet> map; | ||
|
|
||
| // Maps an index to the locals that have constraints referring to it. 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. | ||
| std::unordered_map<Index, std::unordered_set<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. | ||
| 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). | ||
| void | ||
| approximateAndInternal(Index index, const Constraint& c, bool flip = false); | ||
| }; | ||
|
|
||
| std::ostream& operator<<(std::ostream& o, const Constraint& c); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -302,9 +302,12 @@ struct ConstraintAnalysis | |
| BasicBlockConstraintMap& constraints) { | ||
| if (auto* set = curr->dynCast<LocalSet>()) { | ||
| if (Properties::isSingleConstantExpression(set->value)) { | ||
| // We know this one constraint. | ||
| // Apply a constraint to this value. | ||
| auto value = Properties::getLiteral(set->value); | ||
| constraints.set(set->index, Constraint{Abstract::Eq, {value}}); | ||
| } else if (auto* get = set->value->dynCast<LocalGet>()) { | ||
| // Apply a constraint to this local. | ||
| constraints.set(set->index, Constraint{Abstract::Eq, {get->index}}); | ||
|
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. You could also look up everything we previously knew about
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 was thinking about handling transitivity here: Though I am not sure how deep we want to get into this... |
||
| } else { | ||
| // We know and can prove nothing. | ||
| constraints.setProvesNothing(set->index); | ||
|
|
||
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 getting weirdly caught up on whether the *-or-equal variants count as antisymmetric. Taking
<=as an example, antisymmetry means that for allxandywe havex <= y /\ y <= x ==> x == y. If the domain is integer or real values, that's certainly true, but my mental model is that the domain is terms, in which case this is not true. We can easily have distinct variablesxandywhere bothx <= yandy <= x. In that case we know the values ofxandyare equal, butxis not identical toy.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.
But the value of x must be identical to y, in that example..? Sorry, I'm not following you.