ConstraintAnalysis: Add conditional propagation#8867
Conversation
|
I just realized that there is probably a bug with constraints that compare two locals, e.g. |
| ;; We can infer 1 here. | ||
| (drop | ||
| (ref.is_null | ||
| (local.get $param) |
There was a problem hiding this comment.
It would be even better if we propagated values when we have inferred equality constraints. Then follow-on optimizations would be able to handle not just ref.is_null, but also things like casts.
There was a problem hiding this comment.
I'm not sure what you mean here, can you elaborate?
I think we can add cast support later in a simple way, using a new abstract operation "is subtype of". (Though I am not sure how important this is)
There was a problem hiding this comment.
If we have an x == null constraint, then instead of optimizing (ref.is_null (local.get x)) => (i32.const 1), we can optimize (local.get x) => (ref.null none).
Then follow-up optimization in other passes will be able to optimize much more than just a parent ref.is_null.
|
Another idea: in traps-never-happen mode, we could actually propagate constraints backward from conditions where one arm is post-dominated by an |
Correct, all inter-local operations are not quite right yet. That's on my list of next PRs here. |
Interesting... we can only do it one branch back, but that might be useful already. I'll add a TODO |
|
Not just one branch back! |
|
Not sure that works: Execution here goes into the else if the first if. (TNH only applies on paths the code actually traverses: the guarantee is "the program, in practice, does not trap") |
|
Oh yeah, sorry, that example was bogus. Here's one that isn't bogus. |
|
That one works, yeah, but it is not easy to prove that. It depends on how the conditions relate: Now nothing can be inferred for the |
|
The backward flow you need is similar to the forward flow. The initial state is |
|
Yes, that sounds right. Though the ORing will in generally stop working after one branch, unless we get lucky... |
| // * 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.) |
There was a problem hiding this comment.
I see, we can model uninitialized non-nullable locals as having any value instead of having no value because they will always be initialized before they are used anyway; their initial value is moot.
And modeling them that way lets us say that a block is unreachable iff any of the local constraints is bottom. Makes sense!
| // Everything starts unreachable until we reach it in the flow. | ||
| for (auto& block : basicBlocks) { | ||
| block->contents.startConstraints.unreachable = true; | ||
| } |
There was a problem hiding this comment.
Let's just make this the default state for BasicBlockConstraintMap so we don't need to initialize it separately.
| auto iter = other.map.find(local); | ||
| if (iter == other.map.end()) { | ||
| // When an entry is missing in one of the two, it means we can prove | ||
| // nothing there, which means we can prove nothing in the result. | ||
| constraints.setProvesNothing(); | ||
| } else { | ||
| // This local appears in both. OR it. | ||
| constraints.approximateOr(iter->second); | ||
| } |
There was a problem hiding this comment.
| auto iter = other.map.find(local); | |
| if (iter == other.map.end()) { | |
| // When an entry is missing in one of the two, it means we can prove | |
| // nothing there, which means we can prove nothing in the result. | |
| constraints.setProvesNothing(); | |
| } else { | |
| // This local appears in both. OR it. | |
| constraints.approximateOr(iter->second); | |
| } | |
| constraints.approximateOr(other.get(local)) |
| if (combined.provesNothing()) { | ||
| // When we prove nothing, we leave the entry empty. | ||
| map.erase(index); | ||
| return; | ||
| } |
There was a problem hiding this comment.
This can just be an assert(!combined.provesNothing()), since anything AND'd with a single constraint must not be an empty constraint set.
| } else if (LiteralUtils::canMakeZero(type)) { | ||
| // We have a default value. | ||
| if (type.size() == 1 && LiteralUtils::canMakeZero(type)) { | ||
| // We have a default value, so we can prove something. |
There was a problem hiding this comment.
Please add a comment here about how we are counterintuitively modeling non-nullable locals as having any value instead of no value.
| // We pass the next function the index of the successor among the others. | ||
| assert(succ == pred->out[0] || succ == pred->out[1]); | ||
| auto succIndex = succ == pred->out[1] ? 1 : 0; |
There was a problem hiding this comment.
We wouldn't need to calculate the index like this if we just started with an indexed for loop instead of a range-based for loop over the successors when we process work list items.
| } else { | ||
| // It must be the ifTrue. | ||
| assert(succ == predOut[0]); | ||
| assert(succIndex == 0); |
There was a problem hiding this comment.
We can drop the else and just assert(succIndex < 2) at the beginning.
| BasicBlock* succ, | ||
| const BasicBlockConstraintMap& predEnd, | ||
| If* iff) { | ||
| std::optional<LocalConstraint> getConstraintsFromIf(Index succIndex, |
There was a problem hiding this comment.
The nittiest of nits, but I would have expected the index to be the second argument for these functions, not the first.
| const BasicBlockConstraintMap& predEnd, | ||
| // Gets branch constraints using a successor index and a parsed constraint. | ||
| std::optional<LocalConstraint> | ||
| getConstraintsFromParsed(Index succIndex, |
There was a problem hiding this comment.
I don't think this function is adding much anymore. Let's inline it into its callers. (This will simplify getContraintsFromBreak in particular.)
| ;; We can infer 1 here. | ||
| (drop | ||
| (ref.is_null | ||
| (local.get $param) |
There was a problem hiding this comment.
If we have an x == null constraint, then instead of optimizing (ref.is_null (local.get x)) => (i32.const 1), we can optimize (local.get x) => (ref.null none).
Then follow-up optimization in other passes will be able to optimize much more than just a parent ref.is_null.
When we see an
if, br_if, etc., we can propagate the condition along thetrue branch, and its negation along the other.