Skip to content
Open
Show file tree
Hide file tree
Changes from 207 commits
Commits
Show all changes
216 commits
Select commit Hold shift + click to select a range
fdd5024
go
kripken May 7, 2026
aabd860
go
kripken May 7, 2026
3332b8a
Merge remote-tracking branch 'origin/main' into range.analysis
kripken Jun 2, 2026
8b18012
work
kripken Jun 2, 2026
002d237
work
kripken Jun 2, 2026
229ae81
work
kripken Jun 2, 2026
5598aec
work
kripken Jun 2, 2026
ed255d4
work
kripken Jun 2, 2026
e5d8299
work
kripken Jun 2, 2026
fa4805f
work
kripken Jun 3, 2026
ae146cf
work
kripken Jun 3, 2026
4748cc5
work
kripken Jun 3, 2026
f7e33cc
work
kripken Jun 3, 2026
80a843e
work
kripken Jun 3, 2026
2b10d87
work
kripken Jun 3, 2026
c9f17bc
work
kripken Jun 3, 2026
18a691b
form
kripken Jun 3, 2026
5e8c7c3
work
kripken Jun 3, 2026
3b22e48
work
kripken Jun 3, 2026
08e419d
work
kripken Jun 3, 2026
70add1c
work
kripken Jun 3, 2026
15c806a
work
kripken Jun 3, 2026
c30dcda
work
kripken Jun 3, 2026
c74abed
work
kripken Jun 3, 2026
da40546
work
kripken Jun 3, 2026
ddc615e
work
kripken Jun 3, 2026
2707bb1
work
kripken Jun 3, 2026
6390870
format
kripken Jun 3, 2026
3cedbcf
work
kripken Jun 3, 2026
f54afaa
work
kripken Jun 3, 2026
8071200
work
kripken Jun 3, 2026
b550c4d
work
kripken Jun 3, 2026
b1aedcf
work
kripken Jun 3, 2026
0bc8d2e
work
kripken Jun 3, 2026
61fc656
work
kripken Jun 3, 2026
3a3970b
work
kripken Jun 3, 2026
8a6d355
work
kripken Jun 3, 2026
562872d
work
kripken Jun 3, 2026
d296092
go
kripken Jun 3, 2026
a46436f
go
kripken Jun 3, 2026
413f4e8
go
kripken Jun 3, 2026
817a9ca
go
kripken Jun 3, 2026
c14b3b9
go
kripken Jun 3, 2026
35329c8
work
kripken Jun 3, 2026
1dfe44c
go
kripken Jun 3, 2026
6e6d06e
f
kripken Jun 3, 2026
c9f67d1
go
kripken Jun 3, 2026
5f1fdc8
go
kripken Jun 3, 2026
c339b79
go
kripken Jun 3, 2026
d673317
go
kripken Jun 3, 2026
c4321a9
go
kripken Jun 4, 2026
260fb83
go
kripken Jun 4, 2026
4fe70e3
go
kripken Jun 4, 2026
c0d19d9
go
kripken Jun 4, 2026
e6f2d34
go
kripken Jun 4, 2026
200566c
go
kripken Jun 4, 2026
83e845e
go
kripken Jun 4, 2026
e16096b
go
kripken Jun 4, 2026
494f67a
go
kripken Jun 4, 2026
6e76ac0
go
kripken Jun 4, 2026
cf2b0e4
go
kripken Jun 4, 2026
2a02004
form
kripken Jun 4, 2026
279dc39
work
kripken Jun 4, 2026
6e8f576
work
kripken Jun 4, 2026
fcd7e41
work
kripken Jun 4, 2026
6537ace
work
kripken Jun 4, 2026
61dd31e
work
kripken Jun 4, 2026
783e6ff
work
kripken Jun 4, 2026
3e0055e
go
kripken Jun 4, 2026
c1e3dc8
go
kripken Jun 4, 2026
b13c894
go
kripken Jun 4, 2026
d7f5732
go
kripken Jun 4, 2026
362fb95
go
kripken Jun 4, 2026
05d9e29
go
kripken Jun 4, 2026
bc1f4c9
go
kripken Jun 4, 2026
70cd80e
go
kripken Jun 4, 2026
bf12966
go
kripken Jun 4, 2026
67b6707
go
kripken Jun 5, 2026
b9fe8c0
go
kripken Jun 5, 2026
6bae184
go
kripken Jun 5, 2026
6706a99
go
kripken Jun 5, 2026
62cba65
go
kripken Jun 5, 2026
c3089ea
go
kripken Jun 5, 2026
7c5b0b7
go
kripken Jun 5, 2026
958a6d4
go
kripken Jun 5, 2026
e4079e9
go
kripken Jun 5, 2026
31a0ea5
go
kripken Jun 5, 2026
d78f0ef
go
kripken Jun 5, 2026
e78d295
go
kripken Jun 5, 2026
c9eaeed
go
kripken Jun 5, 2026
8884744
go
kripken Jun 5, 2026
eda589a
go
kripken Jun 5, 2026
72e40e4
go
kripken Jun 5, 2026
54cd47e
go
kripken Jun 5, 2026
9e1af11
go
kripken Jun 5, 2026
561c80f
go
kripken Jun 5, 2026
f29d198
go
kripken Jun 5, 2026
3d4a7bb
go
kripken Jun 5, 2026
3cabc2e
go
kripken Jun 5, 2026
690a5ab
go
kripken Jun 5, 2026
840f863
go
kripken Jun 5, 2026
e8a4a80
go
kripken Jun 5, 2026
bd77752
go
kripken Jun 8, 2026
94a3c35
go
kripken Jun 8, 2026
4c5b867
go
kripken Jun 8, 2026
f828d9d
format
kripken Jun 8, 2026
6036296
go
kripken Jun 8, 2026
889658d
go
kripken Jun 8, 2026
724ecb9
go
kripken Jun 8, 2026
0fc9e55
go
kripken Jun 8, 2026
5ad1b75
go
kripken Jun 8, 2026
8181363
go
kripken Jun 8, 2026
d0ad2f4
go
kripken Jun 8, 2026
c5b7d1d
go
kripken Jun 8, 2026
0e35b2b
go
kripken Jun 8, 2026
60d50b4
go
kripken Jun 8, 2026
626b5d7
feedback
kripken Jun 8, 2026
5896406
feedback
kripken Jun 8, 2026
cf29b58
fix
kripken Jun 8, 2026
cdaff6b
clean
kripken Jun 8, 2026
c02ba2e
Merge remote-tracking branch 'myself/inplace' into constraint.by.itself
kripken Jun 8, 2026
94d2161
clean
kripken Jun 8, 2026
735d7ea
clean
kripken Jun 8, 2026
6e80fde
const
kripken Jun 8, 2026
cf7fcc6
undo CFP change
kripken Jun 8, 2026
ac02454
Merge branch 'inplace' into constraint.by.itself
kripken Jun 8, 2026
0930461
tidy
kripken Jun 8, 2026
7b7d2ac
add.assert
kripken Jun 8, 2026
679bd24
fix.comment
kripken Jun 8, 2026
920e7a9
Merge remote-tracking branch 'origin/main' into constraint.by.itself
kripken Jun 8, 2026
602a8c3
merg
kripken Jun 8, 2026
44ad794
value => term
kripken Jun 9, 2026
2f0bdd7
check => eval
kripken Jun 10, 2026
7f77016
fix new clang-18 warning
kripken Jun 10, 2026
daf2208
Merge remote-tracking branch 'myself/constraint.by.itself' into const…
kripken Jun 10, 2026
59f5a09
update
kripken Jun 10, 2026
ed455bd
update
kripken Jun 10, 2026
03e91b6
undo
kripken Jun 10, 2026
6177a16
update
kripken Jun 10, 2026
af791b1
update
kripken Jun 10, 2026
cb35706
update
kripken Jun 10, 2026
c0f54ad
update
kripken Jun 10, 2026
745116c
update
kripken Jun 10, 2026
4c8dcca
update
kripken Jun 10, 2026
725c08d
update
kripken Jun 10, 2026
3e96dcc
update
kripken Jun 10, 2026
03ec477
update
kripken Jun 10, 2026
945e042
update
kripken Jun 10, 2026
41ffb72
update
kripken Jun 10, 2026
ba50172
update
kripken Jun 10, 2026
613ef95
update
kripken Jun 10, 2026
923a42a
update
kripken Jun 10, 2026
5e432a4
2.bad
kripken Jun 10, 2026
2da66f7
update
kripken Jun 10, 2026
3ef82bb
update
kripken Jun 10, 2026
b05dba6
update
kripken Jun 10, 2026
564b3cb
update
kripken Jun 10, 2026
4c3eef2
update
kripken Jun 10, 2026
f40a39d
form
kripken Jun 10, 2026
8a0286a
Merge remote-tracking branch 'origin/main' into constraint
kripken Jun 10, 2026
0d2f0ed
rename
kripken Jun 10, 2026
517dac0
update
kripken Jun 10, 2026
9522de6
update
kripken Jun 10, 2026
0e358e3
update
kripken Jun 10, 2026
a178135
update
kripken Jun 10, 2026
e09cbda
update
kripken Jun 10, 2026
0cb581a
update
kripken Jun 10, 2026
91cc337
update
kripken Jun 10, 2026
ea3db17
refactor to use more helpers and less nesting
kripken Jun 12, 2026
060d8aa
format
kripken Jun 12, 2026
088a425
simpl
kripken Jun 12, 2026
9267485
form
kripken Jun 12, 2026
3965c27
simpl
kripken Jun 12, 2026
280e7b3
remove
kripken Jun 12, 2026
26dfc30
move code as requested
kripken Jun 12, 2026
c1dacec
eval => proves
kripken Jun 12, 2026
3952dfc
form
kripken Jun 12, 2026
224425c
Update src/ir/constraint.cpp
kripken Jun 12, 2026
203d455
work
kripken Jun 12, 2026
e4a2a49
fix set op
kripken Jun 12, 2026
22d2d3b
fuzzyAnd, and make it drop new ones automatically, as suggested
kripken Jun 12, 2026
bdc4911
avoid fuzzy, use approximate
kripken Jun 16, 2026
9b4a261
Merge remote-tracking branch 'origin/main' into constraint.by.itself
kripken Jun 16, 2026
c924041
remove Invalid
kripken Jun 16, 2026
8e5e075
Merge remote-tracking branch 'myself/constraint.by.itself' into const…
kripken Jun 16, 2026
90aabfa
merg
kripken Jun 16, 2026
81714b2
merg
kripken Jun 16, 2026
edf8059
work
kripken Jun 16, 2026
b1522c2
go
kripken Jun 16, 2026
97ec6b5
fix
kripken Jun 16, 2026
966676d
go
kripken Jun 16, 2026
9645e71
Update src/ir/constraint.h
kripken Jun 16, 2026
64e0a86
Update src/ir/constraint.h
kripken Jun 16, 2026
0e0bfb7
Update src/ir/constraint.h
kripken Jun 16, 2026
6947fc3
Update src/ir/constraint.h
kripken Jun 16, 2026
2b30208
format
kripken Jun 16, 2026
464aba5
Merge remote-tracking branch 'myself/constraint.by.itself' into const…
kripken Jun 16, 2026
82aad7d
Merge remote-tracking branch 'origin/main' into constraint
kripken Jun 17, 2026
62e5ee9
merg
kripken Jun 17, 2026
835d863
clean
kripken Jun 17, 2026
6aec092
fix
kripken Jun 17, 2026
c014633
nice
kripken Jun 17, 2026
81f2421
form
kripken Jun 17, 2026
4f9fb9d
help
kripken Jun 17, 2026
dab23fb
avoid warning
kripken Jun 17, 2026
26b1b62
avoid warning
kripken Jun 17, 2026
bda6665
avoid warning
kripken Jun 17, 2026
9dbf1d9
add todo
kripken Jun 17, 2026
1981731
rename
kripken Jun 18, 2026
ec2c0d5
format
kripken Jun 18, 2026
53974d7
test loops, showing the problem
kripken Jun 18, 2026
e575105
reverse
kripken Jun 18, 2026
aec9542
format
kripken Jun 18, 2026
92e5efd
fix.gtest
kripken Jun 18, 2026
49a448a
add bottom/top, effectively
kripken Jun 22, 2026
e0524d5
fix
kripken Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/ir/constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,81 @@ void AndedConstraintSet::approximateOr(const AndedConstraintSet& other) {
clear();
}

std::optional<LocalConstraint> LocalConstraint::parse(Expression* curr) {
auto parseEqZ = [&](Expression* value) -> std::optional<LocalConstraint> {
Comment thread
tlively marked this conversation as resolved.
Outdated
if (auto* get = value->dynCast<LocalGet>()) {
// Canonicalize EqZ to Eq of 0.
auto value = Literal::makeZero(get->type);
return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}};
}
return {};

This comment was marked as resolved.

};

if (auto* unary = curr->dynCast<Unary>()) {
if (Abstract::getUnary(unary->type, Abstract::EqZ) == unary->op) {
return parseEqZ(unary->value);
}
return {};
}

if (auto* refIsNull = curr->dynCast<RefIsNull>()) {
return parseEqZ(refIsNull->value);
}

// Parse a get or a constant.
auto parseTerm = [&](Expression* expr) -> std::optional<Term> {
if (auto* get = expr->dynCast<LocalGet>()) {
return Term{get->index};
}
if (Properties::isSingleConstantExpression(expr)) {
return Term{Properties::getLiteral(expr)};
}
return {};
};

auto parseBinary = [&](Abstract::Op op,
Expression* left,
Expression* right) -> std::optional<LocalConstraint> {
// The left must be a get.
if (auto* get = left->dynCast<LocalGet>()) {
// The right can be any term.
if (auto value = parseTerm(right)) {
return LocalConstraint{get->index, Constraint{op, *value}};
}
}
return {};
};

if (auto* binary = curr->dynCast<Binary>()) {
// The operation must be one we recognize.
for (auto op : {Abstract::Eq, Abstract::Ne}) {
if (Abstract::getBinary(binary->type, op) == binary->op) {
return parseBinary(op, binary->left, binary->right);
}
}
return {};
}

if (auto* refEq = curr->dynCast<RefEq>()) {
return parseBinary(Abstract::Eq, refEq->left, refEq->right);
}

return {};
}

void LocalConstraintMap::approximateOr(const LocalConstraintMap& other) {
// Find things in both, and OR them.
for (auto& [local, constraints] : other) {
if (auto iter = find(local); iter != end()) {
iter->second.approximateOr(constraints);
}
}

// Remove things only in us.
std::erase_if(*this, [&](const auto& item) {
const auto& [local, constraints] = item;
return !other.contains(local);
});
}

} // namespace wasm::constraint
25 changes: 25 additions & 0 deletions src/ir/constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,31 @@ struct AndedConstraintSet : inplace_vector<Constraint, MaxConstraints> {
void approximateOr(const AndedConstraintSet& other);
};

// A local plus a constraint on it.
struct LocalConstraint {
Index local;
Constraint constraint;

// Try to parse BinaryenIR into a local to which a constraint is applied. For
// example
//
// (i32.eq (local.get $r) (i32.const 10))
//
// parses into
//
// LocalConstraint($r, { x == 10 })
//
static std::optional<LocalConstraint> parse(Expression* curr);
};

// A map of locals and their constraints.
struct LocalConstraintMap
: public std::unordered_map<Index, AndedConstraintSet> {
// Perform an OR as above on each local that appears in both maps. If a local
// appears only in one, we can infer nothing, and drop it.
void approximateOr(const LocalConstraintMap& other);
};

} // namespace wasm::constraint

#endif // wasm_ir_constraint_h
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ set(passes_SOURCES
CodeFolding.cpp
ConstantFieldPropagation.cpp
ConstHoisting.cpp
ConstraintAnalysis.cpp
DataFlowOpts.cpp
DeadArgumentElimination.cpp
DeadArgumentElimination2.cpp
Expand Down
224 changes: 224 additions & 0 deletions src/passes/ConstraintAnalysis.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright 2026 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//
// Use mathematical constraint solving to optimize. For example:
//
// if (x == 10) {
// assert(x != 0); // redundant and can be removed.
// }
//

#include "cfg/cfg-traversal.h"
#include "ir/constraint.h"
#include "ir/drop.h"
#include "ir/literal-utils.h"
#include "ir/local-graph.h"
#include "ir/properties.h"
#include "pass.h"
#include "support/unique_deferring_queue.h"
#include "support/utilities.h"
#include "wasm-builder.h"
#include "wasm.h"

namespace wasm {

using namespace wasm::constraint;

namespace {

// In each basic block we will store the relevant operations, which are all
// local gets and sets, branches, and uses of them.
struct Info {
std::vector<Expression**> actions;

// 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.

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.

When an analysis uses a proper join-semilattice, it's fine to start out with incomplete information because we know that eventually we will reach a sound fixed point where we know everything we need to know.

If you're right that it's not sound to start with incomplete information for one block, then it seems that worklist algorithm for flowing information around would not be sound, since it necessarily starts with incomplete information at loop heads.

But I think it should be fine to start with incomplete information. We don't have a join-semilattice because the OR operation does not go to a unique least upper bound, but it is at least monotonic. We know that the analysis will only ever learn more possibilities (i.e. loosen or drop constraints) as information flows around. So if we start out knowing X has a == 10 and propagate some constraints based on that information, but then later we process Y or Z and learn that a might have some other value, then we will recalculate what we can infer (which will be fewer/looser constraints in general) and then propagate those looser constraints to the successors.

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.

When an analysis uses a proper join-semilattice, it's fine to start out with incomplete information because we know that eventually we will reach a sound fixed point where we know everything we need to know.

Let me put it this way: if we see this as an abstract interpretation situation, then we must calculate the entire transfer function at each node. And the entire transfer function is f(X, Y, Z) = X || Y || Z in this example: we cannot receive only X and propagate that onwards.

Or, here is the actual problem: again, if one predecessor X supplies a == 10, and we are in a block with if (a == 10), then if we just use a == 10 at the start of the block and start to apply it, we'd optimize that if condition. But we can't. Again, we must calculate the entire transfer function at the start of the block before doing anything with that data, so that we have something valid.

Now, we could define the transfer function so that f(X, null, null) == null - effectively blocking propagation until we get info from all sides - but I think it is far more natural to just do the expected thing from the point of view of mathematical logic: we are computing X || Y || Z, so just compute that at the top of each block.

That is, what does null mean from the point of view of logic? I am trying to keep the math pure 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.

Ok, after offline discussion, I rewrote this to save the start of blocks, and now it handles loops as well, see last commits.

This uncovered a bug in OR, which did not handle the empty set properly, also fixed.

};

struct ConstraintAnalysis
: public WalkerPass<
CFGWalker<ConstraintAnalysis, Visitor<ConstraintAnalysis>, Info>> {
bool isFunctionParallel() override { return true; }

// Locals are not modified here.
bool requiresNonNullableLocalFixups() override { return false; }

std::unique_ptr<Pass> create() override {
return std::make_unique<ConstraintAnalysis>();
}

// Branches outside of the function can be ignored, as we only look at local
// state in the function.
bool ignoreBranchesOutsideOfFunc = true;

// Store the actions we care about.
void addAction() {
if (currBasicBlock) {
currBasicBlock->contents.actions.push_back(getCurrentPointer());
}
}

void visitLocalSet(LocalSet* curr) { addAction(); }
void visitUnary(Unary* curr) { addAction(); }
void visitBinary(Binary* curr) { addAction(); }
void visitRefEq(RefEq* curr) { addAction(); }
void visitRefIsNull(RefIsNull* curr) { addAction(); }

void visitFunction(Function* curr) {
// TODO: optimize for speed, find relevant locals etc.
flow();
optimize();
}

// Flow infos around until we have inferred all we can about the constraints
// in each location.
void flow() {
// Start from all the blocks, and keep going while we find something new.
UniqueDeferredQueue<BasicBlock*> work;
for (auto& block : basicBlocks) {
work.push(block.get());
}
while (!work.empty()) {
auto* block = work.pop();

// Merge incoming data to get the status at the start of the block.
LocalConstraintMap constraints = mergeIncoming(block);

// Go through the block, applying things.
for (auto** currp : block->contents.actions) {
applyToConstraints(*currp, constraints);
}

// 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.

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.

Atm we don't drop, but just refrain from adding. So once a set saturates, it freezes, basically.

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.

(Which is monotonic.)

for (auto* out : block->out) {
work.push(out);
}
}
}
}

// After inferring all we can, apply it to optimize the code.
void optimize() {
for (auto& block : basicBlocks) {
// Follow the general shape of flow(): we need to see what the state is
// at each intermediate point inside the block. (Flowing between blocks is
// of course not needed at this stage.)
LocalConstraintMap constraints = mergeIncoming(block.get());
for (auto** currp : block->contents.actions) {
applyToConstraints(*currp, constraints);
optimizeExpression(currp, constraints);
}
}
}

// Given an expression and the constraints on it, optimize it.
void optimizeExpression(Expression** currp,
const LocalConstraintMap& constraints) {
auto* curr = *currp;
auto parsed = LocalConstraint::parse(curr);
if (!parsed) {
return;
}

auto iter = constraints.find(parsed->local);
if (iter == constraints.end()) {
return;
}
auto& localConstraints = iter->second;
Result result = localConstraints.proves(parsed->constraint);
if (result == Unknown) {
// If we parsed something using two locals, like x != y, we can also look
// for the flipped condition among y's constraints TODO
Comment thread
tlively marked this conversation as resolved.
return;
}

// We know the result!
auto& wasm = *getModule();
auto value =
LiteralUtils::makeFromInt32(result == True ? 1 : 0, curr->type, wasm);
*currp = getDroppedChildrenAndAppend(
curr, wasm, getPassOptions(), value, DropMode::IgnoreParentEffects);
}

// Merge incoming data to a block, by looking at the data arriving from each
// of the predecessor blocks.
LocalConstraintMap mergeIncoming(BasicBlock* block) {
LocalConstraintMap constraints;

// Merge all preds.
for (auto* pred : block->in) {
auto& predConstraints = getConstraintsFromPredToSucc(pred, block);
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?

}

// The entry block has incoming values - defaults - for each var.
if (block == entry) {
auto* func = getFunction();
auto numLocals = func->getNumLocals();
for (Index i = 0; i < numLocals; i++) {
if (!func->isVar(i)) {
continue;
}
auto type = func->getLocalType(i);
// TODO: support tuples
if (type.size() == 1 && LiteralUtils::canMakeZero(type)) {
auto value = Literal::makeZero(type);
constraints[i].approximateAnd(Constraint{Abstract::Eq, {value}});
}
}
}

return constraints;
}

// Given a source (predecessor) and a target (successor) block, find the
// constraints for locals as they arrive to that target from that successor.
const LocalConstraintMap& getConstraintsFromPredToSucc(BasicBlock* pred,
BasicBlock* block) {
// TODO: use conditional branching to send different values along branches
return pred->contents.endConstraints;
}

// Given an expression, apply it to the constraints. For example, a local.set
// sets the value for that local.
void applyToConstraints(Expression* curr, LocalConstraintMap& constraints) {
if (auto* set = curr->dynCast<LocalSet>()) {
auto& localConstraints = constraints[set->index];
localConstraints.clear();
if (Properties::isSingleConstantExpression(set->value)) {
auto value = Properties::getLiteral(set->value);
localConstraints.approximateAnd(Constraint{Abstract::Eq, {value}});

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.

Same comment. This should logically be an OR (or an assignment), and the current code is only correct because the default value is top rather than bottom.

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.

Yes, this was the same issue. Fixed as above.

}
}
}
};

} // anonymous namespace

Pass* createConstraintAnalysisPass() { return new ConstraintAnalysis(); }

} // namespace wasm
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ void PassRegistry::registerPasses() {
registerPass("cfp-reftest",
"propagate constant struct field values, using ref.test",
createConstantFieldPropagationRefTestPass);
registerPass("constraint-analysis",
"finds and uses mathematical constraints on locals",
createConstraintAnalysisPass);
registerPass(
"dce", "removes unreachable code", createDeadCodeEliminationPass);
registerPass("dealign",
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Pass* createCodePushingPass();
Pass* createConstHoistingPass();
Pass* createConstantFieldPropagationPass();
Pass* createConstantFieldPropagationRefTestPass();
Pass* createConstraintAnalysisPass();
Pass* createDAEPass();
Pass* createDAEOptimizingPass();
Pass* createDAE2Pass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-NEXT:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-NEXT:
;; CHECK-NEXT: --dae removes arguments to calls in an
;; CHECK-NEXT: lto-like manner
;; CHECK-NEXT:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-NEXT:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-NEXT:
;; CHECK-NEXT: --dae removes arguments to calls in an
;; CHECK-NEXT: lto-like manner
;; CHECK-NEXT:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-NEXT:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-NEXT:
;; CHECK-NEXT: --dae removes arguments to calls in an
;; CHECK-NEXT: lto-like manner
;; CHECK-NEXT:
Expand Down
Loading
Loading