Skip to content

Add a ConstraintAnalysis pass#8853

Open
kripken wants to merge 210 commits into
WebAssembly:mainfrom
kripken:constraint
Open

Add a ConstraintAnalysis pass#8853
kripken wants to merge 210 commits into
WebAssembly:mainfrom
kripken:constraint

Conversation

@kripken

@kripken kripken commented Jun 17, 2026

Copy link
Copy Markdown
Member

This simply parses IR into constraints, flows them around, and sees
if we can prove things.

This is a minimal version, without conditional propagation etc.

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

@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 so far. Will look at tests next.

Comment on lines +107 to +110
// We now know the values at the end of the block. If something changed,
// flow it onward.
if (constraints != block->contents.endConstraints) {
block->contents.endConstraints = std::move(constraints);

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.

Can we prove this will actually converge? Can we create a pathological case where the analysis alternates between two different constraint sets forever?

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.

It must converge now because we simply drop extra things in approximateAnd. If we did something more complex, we'd need to be careful and define a total order, I think.

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'm worried that a sequence of ORs (control flow merges) and ANDs (from the contents of blocks) could change the order of constraints so that the one that is dropped is not stable.

Comment on lines +48 to +50
// For each local index, we track the constraints we know about it. We only do
// so at the end of each block, which is enough for the analysis below.
LocalConstraintMap endConstraints;

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.

Why not keep the beginning constraints instead? Then when we can get to the end of a block, we can merge the current constraints (and eventually any additional constraints due to the specific control flow edge) into the beginning constraints of each successor block and only process that successor block again if its starting constraints are different.

In contrast, the current approach may reprocess successor blocks even if they don't learn anything new from the single predecessor that was updated.

Storing the beginning constraints would also let us avoid re-merging all the predecessors for each block in the optimization phase.

This would also help prove convergence. If we can show that the merge operation on constraint sets is monotonic on some partial order on constraint sets and converges after a bounded number of steps, then we will know the analysis will converge. In contrast, it's hard to say anything about how the end constraints will change over time because they are the result of non-monotonic meet operations over the course of the block.

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.

Interesting, yeah, storing the start might have benefits. It does mean adding a bottom element though, so we can merge incrementally like that.

However, about the very last point: storing the beginning or the end is NFC, so I don't see how it helps with convergence?

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.

Instead of a bottom element it might be cleaner to use std::optional in the pass. That is, there is no logical (as in mathematical logic) meaning to the bottom element - it is not a logical constraint - so a "null" can be handled by the users, rather than inside constraint.h.

However - thinking more on this, I'm not sure it's right. We can't simply keep merging in content as it flows around. The input to a block is, effectively, X || Y || Z, and it matters which of those we update. If we update X three times with a == 10 that is very different than if we update X, Y, Z to that constraint - only then we can apply something.

So I think it is best to do this as it is written: merge the inputs in a loop, seeing them all at once.

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.

Instead of a bottom element it might be cleaner to use std::optional in the pass. That is, there is no logical (as in mathematical logic) meaning to the bottom element - it is not a logical constraint - so a "null" can be handled by the users, rather than inside constraint.h.

Sure, bottom is not literally a bounded set of constraints like other values would be, but it is certainly a meaningful point in the space of possible known constraints. Representing it in constraint.h keeps the analysis code simpler while ensuring that we can never "forget" that we have observed a contradiction. It also lets us write unit tests for it.

If we update X three times with a == 10 that is very different than if we update X, Y, Z to that constraint - only then we can apply something.

I'm not sure what you mean here.

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.

If we receive data from blocks X, Y, and Z, then we only have a valid state after seeing all of X, Y, and Z. That is, if our state starts at some null/bottom, and X arrives, we cannot flow X onward.

Concretely, if X has a == 10 but we haven't seen Y or Z yet, then we don't know if a == 10 is true in this block, and it would be invalid to apply a == 10 in the block and/or to flow it onward.

We only find the valid state of inputs to the block after merging X, Y, and Z. Doing so at once is the simplest way to get the valid state.

Comment on lines +169 to +175
if (pred == *block->in.begin()) {
// This is the first. Just copy.
constraints = predConstraints;
} else {
// Merge in subsequent ones.
constraints.approximateOr(predConstraints);
}

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 had a bottom value to initialize constraints to, then we wouldn't have to distinguish the first constraints like this.

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.

True, yeah. Maybe worth adding, though this might be the only place it helps?

Comment on lines +148 to +149
// If we parsed something using two locals, like x != y, we can also look
// for the flipped condition among y's constraints TODO

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.

Probably better to canonicalize to have e.g. the lower local index on the LHS of the constraint.

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.

Yeah, I was thinking about that too. A later PR will add these local-local operations, we can pick the best thing there.

Comment thread src/ir/constraint.cpp Outdated
}

std::optional<LocalConstraint> LocalConstraint::parse(Expression* curr) {
auto parseEqZ = [&](Expression* value) -> std::optional<LocalConstraint> {

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 parseEqZ = [&](Expression* value) -> std::optional<LocalConstraint> {
auto parseEqZArgument = [&](Expression* value) -> std::optional<LocalConstraint> {

These lambdas can also be pulled out as helpers in an anonymous namespace.

These might also be nice places to use match.h.

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.

Sorry, I'm not following this - why is "argument" improving the name?

I don't feel strongly about moving these out to a namespace, but I like that they are enclosed here, which is the one place they might ever be used?

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 experimented with match.h but it didn't really shorten much.

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.

Sorry, I'm not following this - why is "argument" improving the name?

Based on the current name I was expecting the lambda to look for an eqz node and I was confused when it didn't. What it's doing is parsing the argument to an eqz (or similar) node.

I don't feel strongly about moving these out to a namespace, but I like that they are enclosed here, which is the one place they might ever be used?

I think it would help the reader keep less in mind at once, but I don't feel too strongly about this instance. It's fine as-is.

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.

Oh, I see. Yes, you're right, argument does help here, I renamed this and parseBinaryArgument.

Comment thread src/ir/constraint.cpp
auto value = Literal::makeZero(get->type);
return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}};
}
return {};

This comment was marked as resolved.

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