Skip to content

ConstraintAnalysis: Add conditional propagation#8867

Open
kripken wants to merge 274 commits into
WebAssembly:mainfrom
kripken:constraint.2
Open

ConstraintAnalysis: Add conditional propagation#8867
kripken wants to merge 274 commits into
WebAssembly:mainfrom
kripken:constraint.2

Conversation

@kripken

@kripken kripken commented Jun 25, 2026

Copy link
Copy Markdown
Member

When we see an if, br_if, etc., we can propagate the condition along the
true branch, and its negation along the other.

@kripken kripken requested a review from tlively June 25, 2026 17:26
@kripken kripken requested a review from a team as a code owner June 25, 2026 17:26

@tlively tlively left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments on code.

Comment thread src/ir/abstract.h Outdated
Comment thread src/ir/constraint.cpp
Comment thread src/passes/ConstraintAnalysis.cpp Outdated
Comment thread src/passes/ConstraintAnalysis.cpp Outdated
@tlively

tlively commented Jun 25, 2026

Copy link
Copy Markdown
Member

I just realized that there is probably a bug with constraints that compare two locals, e.g. x == y. When there is a set to x, that constraint will be removed and possibly replaced with a new constraint. But when there is a set to y, I believe we still keep the x == y constraint even though it may no longer be correct.

Comment thread test/lit/passes/constraint-analysis.wast
;; We can infer 1 here.
(drop
(ref.is_null
(local.get $param)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kripken kripken Jun 26, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tlively

tlively commented Jun 25, 2026

Copy link
Copy Markdown
Member

Another idea: in traps-never-happen mode, we could actually propagate constraints backward from conditions where one arm is post-dominated by an (unreachable).

@kripken

kripken commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

I just realized that there is probably a bug with constraints that compare two locals

Correct, all inter-local operations are not quite right yet. That's on my list of next PRs here.

@kripken

kripken commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Another idea: in traps-never-happen mode, we could actually propagate constraints backward from conditions where one arm is post-dominated by an (unreachable).

Interesting... we can only do it one branch back, but that might be useful already. I'll add a TODO

@tlively

tlively commented Jun 25, 2026

Copy link
Copy Markdown
Member

Not just one branch back!

log(x <= 10 && x >= 0) ;; optimizable to log(1) in TNH
... arbitrary stuff that doesn't modify x ...
if c
  if x > 10:
    unreachable
else
  if x < 0:
    unreachable

@kripken

kripken commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Not sure that works:

;; counterexample values
x = 100
c = 0

;; rest of testcase is unchanged
log(x <= 10 && x >= 0)
if c
  if x > 10:
    unreachable
else
  if x < 0:
    unreachable

Execution here goes into the else if the first if. x == 100, so we don't trap. And that proves x >= 0 earlier. But nothing proves x <= 10, which is in fact false. We therefore cannot infer that 1 is logged.

(TNH only applies on paths the code actually traverses: the guarantee is "the program, in practice, does not trap")

@tlively

tlively commented Jun 26, 2026

Copy link
Copy Markdown
Member

Oh yeah, sorry, that example was bogus. Here's one that isn't bogus.

log(x) ;; Optimizable to log(1) in TNH
if x < 1:
  unreachable
if x > 1:
  unreachable

@kripken

kripken commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

That one works, yeah, but it is not easy to prove that. It depends on how the conditions relate:

log(x)
if y:        ;; these two lines changed
  return     ;;
if x > 1:
  unreachable

Now nothing can be inferred for the x in log(x), not even that x <= 1.

@tlively

tlively commented Jun 26, 2026

Copy link
Copy Markdown
Member

The backward flow you need is similar to the forward flow. The initial state is bottom for each block ending in unreachable (or at which a contradiction is discovered during the forward flow) and top at the end of other exit blocks (if you are not interleaving it with the forward flow). Then you flow backward. Branch conditions are OR'd into the predecessor blocks instead of AND'd into successor blocks. If we follow the C/C++ model, some care needs to be taken around calls and atomic operations because they are allowed to happen in loops that may be followed by unreachables.

@kripken

kripken commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Yes, that sounds right. Though the ORing will in generally stop working after one branch, unless we get lucky...

Comment thread src/ir/constraint.h
Comment on lines +200 to +204
// * 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.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment on lines +120 to +123
// Everything starts unreachable until we reach it in the flow.
for (auto& block : basicBlocks) {
block->contents.startConstraints.unreachable = true;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just make this the default state for BasicBlockConstraintMap so we don't need to initialize it separately.

Comment thread src/ir/constraint.cpp
Comment on lines +264 to +272
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);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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))

Comment thread src/ir/constraint.cpp
Comment on lines +295 to 299
if (combined.provesNothing()) {
// When we prove nothing, we leave the entry empty.
map.erase(index);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment here about how we are counterintuitively modeling non-nullable locals as having any value instead of no value.

Comment on lines +237 to +239
// 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants