ConstraintAnalysis: Add conditional propagation#8867
Conversation
|
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... |
| // 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.
There was a problem hiding this comment.
Hmm, I suppose, but the code seems overall simpler to me as is? getBranchConstraints handles blocks, without caring about indexing; when we do start to care about indexing, we get the index in this trivial way.
There was a problem hiding this comment.
The other advantage is that we would then be consistently using indices everywhere instead of using blocks in the signature of this function and then indices in the signatures of the getConstraintsFromXYZ functions. (Although this may be moot; see below.)
There was a problem hiding this comment.
If we want to avoid succIndex, another option is "fallthrough" as in, are we falling through to the block right after us, or are we branching somewhere else. That is really the operation here. Though, I'm not sure if it's any better?
There was a problem hiding this comment.
If-else does not really have a fallthrough, but it is the case that for the operations handled here, we just care about whether we need to negate the condition or not. (br_table will of course have to care about the specific index.
There was a problem hiding this comment.
We could replace succIndex with a boolean of either of those names, if we wanted?
There was a problem hiding this comment.
The property we care about is "is this the block I end up in when the condition expression was truthy," not about the relative positions of the blocks if we were to linearize the CFG. But I expect this to be moot if we make the change I suggest in #8867 (comment).
There was a problem hiding this comment.
I implemented that comment, but I'm not sure if you think it is moot? getBranchConstraints is still doing more than just call the handlers, it also looks at succIndex.
The property we care about is "is this the block I end up in when the condition expression was truthy," not about the relative positions of the blocks if we were to linearize the CFG.
Yes, it is just that we compute "is this the ifTrue path" using the relative positions of blocks. We have to compute this somewhere, or stash it in the IR. Atm the IR construction rule is, as I said, that the physically adjacent block is the first successor.
There was a problem hiding this comment.
I renamed it to physicalSuccessor now, which avoids succIndex, and I think makes it clear how we relate that physical successor to the ifTrue/false paths.
There was a problem hiding this comment.
I don't love it, but I see where you're coming from and I don't think it's worth going back and forth more on this one, so lgtm.
| 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.)
There was a problem hiding this comment.
The function body is 14 lines of code/comments - inlining it would lead to duplication?
It handles the case where nothing was parsed, and negating the condition based on succIndex.
There was a problem hiding this comment.
Concrete demonstration of what I have in mind in #8867 (comment).
| ;; 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.
| // 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.
The other advantage is that we would then be consistently using indices everywhere instead of using blocks in the signature of this function and then indices in the signatures of the getConstraintsFromXYZ functions. (Although this may be moot; see below.)
| return getConstraintsFromBreak(br, succIndex); | ||
| } else if (auto* br = brancher->dynCast<BrOn>()) { | ||
| return getConstraintsFromBrOn(br, succIndex); | ||
| } |
There was a problem hiding this comment.
Example of how simple this could be without helper function overhead:
| } | |
| Expression* cond = nullptr; | |
| bool negate = false; | |
| if (auto* iff = brancher->dynCast<If>()) { | |
| cond = iff->condition; | |
| } else if (auto* br = brancher->dynCast<Break>()) { | |
| cond = br->condition; | |
| negate = true; | |
| } else if (auto* br = brancher->dynCast<BrOn>(); | |
| br && (br->op == BrOnNull || br->op == BrOnNonNull)) { | |
| cond = br->ref; | |
| negate = br->op == BrOnNonNull; | |
| } else { | |
| return {}; | |
| } | |
| if (auto parsed = LocalConstraint::parseBoolean(cond)) { | |
| if (negate ^ (succIndex == 1)) { | |
| parsed.constraint = parsed.constraint.negate(); | |
| } | |
| return parsed; | |
| } | |
| return {}; |
We could make this even simpler by removing only setting negatenegate for BrOnNull if we updated the CFG walker so that the ifTrue successors were consistently at index 0 for if, br_if, and br_on*.
There was a problem hiding this comment.
Thanks, I see better what you mean now.
However,
- BrOn doesn't use parseBoolean, it parses a LocalGet directly - the constraint depends on the BrOn opcode, unlike the others.
- Once we add Switch support, we'll need to call out to other code anyhow - i.e. they won't all be boolean-style branches. At that point, I think it will be nicer as in the current code, to call "handle If", "handle BrOn", "handle Switch" and so forth, which is very high level.
There was a problem hiding this comment.
BrOn doesn't use parseBoolean, it parses a LocalGet directly - the constraint depends on the BrOn opcode, unlike the others.
But it might as well use parseBoolean since that already handles the LocalGet. Then you can just negate the condition when you have br_on_null instead of br_on_non_null. No need to duplicate the LocalGet parsing logic.
2. Once we add Switch support, we'll need to call out to other code anyhow
No argument there, but there is still room to reduce repetition and boilerplate in the existing code, mostly be doing less constraint parsing in the BrOn case, but also potentially via tweaks like not taking std::optional in getContraintsFromParsed.
There was a problem hiding this comment.
But it might as well use parseBoolean since that already handles the LocalGet
...yes, it does handle that, but logically these are different operations. Atm we can't write (br_on_null (ref.eq ..)), but parseBoolean would parse such a thing. This could lead to odd bugs later. I think it's better to be explicit: br_on only handles one constraint, and we can parse it directly.
not taking std::optional in getContraintsFromParsed.
I guess I can move it from there to the main user. See last commit. That simplifies the API slightly I think.
There was a problem hiding this comment.
(br_on_null (ref.eq ...)) would of course be disallowed by validation, the same way (br_if (ref.null none)) would be. But the two cases are similar enough that we're happy using the same Literal::makeZero(type) method to create the "falsey" value for both of them. Both br_on_non_null and br_if branch on "truthy" values (and br_on_null just negates br_on_non_null), so I think it's very natural to use parseBoolean for both of them.
There was a problem hiding this comment.
I'd be ok with that, but we should rename it then - BrOn's input is a reference, not an integer boolean, so parseBoolean is logically wrong. I don't have a good idea for a shared name, though... do you?
There was a problem hiding this comment.
parseCondition (I discarded parseTruthyOrFalsey and parseBooleany)
There was a problem hiding this comment.
Ok, switched to parseCondition.
| return getConstraintsFromBreak(br, succIndex); | ||
| } else if (auto* br = brancher->dynCast<BrOn>()) { | ||
| return getConstraintsFromBrOn(br, succIndex); | ||
| } |
There was a problem hiding this comment.
(br_on_null (ref.eq ...)) would of course be disallowed by validation, the same way (br_if (ref.null none)) would be. But the two cases are similar enough that we're happy using the same Literal::makeZero(type) method to create the "falsey" value for both of them. Both br_on_non_null and br_if branch on "truthy" values (and br_on_null just negates br_on_non_null), so I think it's very natural to use parseBoolean for both of them.
| getConstraintsFromParsed(LocalConstraint parsed, Index succIndex) { | ||
| auto& [local, constraint] = parsed; |
There was a problem hiding this comment.
I'm still not very happy with this helper. All it does is determine whether the constraint needs to be negated or not, but for the Break and BrOn callers, it does not even do this job fully. The logic for negation is different enough for the different branch instructions that I don't think it is useful to try to factor it out. See suggested replacements below.
There was a problem hiding this comment.
Thanks, I removed getConstraintsFromParsed and inlined the negation into the callers. I agree this is simpler that way.
Co-authored-by: Thomas Lively <tlively123@gmail.com>
tlively
left a comment
There was a problem hiding this comment.
LGTM with possible final simplifications:
- Renaming
parseBooleanso we can reuse it for theBrOncase. getBranchConstraintscould do nothing but dispatch to the propergetConstraintsFromXXXhandler if we used a traditional for loop instead of a range-based for loop to iterate over the successor blocks.
When we see an
if, br_if, etc., we can propagate the condition along thetrue branch, and its negation along the other.