Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
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
44ad794
value => term
kripken Jun 9, 2026
2f0bdd7
check => eval
kripken Jun 10, 2026
7f77016
fix new clang-18 warning
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
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
1 change: 1 addition & 0 deletions src/ir/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FILE(GLOB ir_HEADERS *.h)
set(ir_SOURCES
ExpressionAnalyzer.cpp
ExpressionManipulator.cpp
constraint.cpp
drop.cpp
effects.cpp
eh-utils.cpp
Expand Down
3 changes: 2 additions & 1 deletion src/ir/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ enum Op {
GtS,
GtU,
GeS,
GeU
GeU,
Invalid
};

inline bool hasAnyRotateShift(BinaryOp op) {
Expand Down
156 changes: 156 additions & 0 deletions src/ir/constraint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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.
*/

#include <optional>

#include "ir/constraint.h"
#include "ir/properties.h"
#include "wasm.h"

namespace wasm::constraint {

namespace {

// Evaluate whether a => b, where a and b are operations on constants.
Result provesConstantPair(Abstract::Op aOp,
const Literal& aConstant,
Abstract::Op bOp,
const Literal& bConstant) {
// x == X =?=> x == Y. True iff X == Y.
if (aOp == Abstract::Eq && bOp == Abstract::Eq) {
return aConstant == bConstant ? True : False;
}

// x == X =?=> x != Y. True iff X != Y.
if (aOp == Abstract::Eq && bOp == Abstract::Ne) {
return aConstant == bConstant ? False : True;
}

// x != X =?=> x == Y. False if X = Y, else unknown.
if (aOp == Abstract::Ne && bOp == Abstract::Eq) {
if (aConstant == bConstant) {
return False;
}
}

// x != X =?=> x != Y. True if X = Y, else unknown.
if (aOp == Abstract::Ne && bOp == Abstract::Ne) {
if (aConstant == bConstant) {
return True;
}
}

// TODO: handle >, >=, <, and <=
return Unknown;
Comment thread
kripken marked this conversation as resolved.
}

// Core comparison of two constraints: whether a => b
Result provesPair(const Constraint& a, const Constraint& b) {
// A thing always implies itself.
if (a == b) {
return True;
}

// Comparisons of two constants.
auto* aConstant = std::get_if<Literal>(&a.term);
auto* bConstant = std::get_if<Literal>(&b.term);
if (aConstant && bConstant) {
return provesConstantPair(a.op, *aConstant, b.op, *bConstant);
}

return Unknown;
}

} // anonymous namespace

Result AndedConstraintSet::proves(const Constraint& condition) const {
// Sometimes a single constraint is enough to determine the condition.
for (auto& c : *this) {
auto result = provesPair(c, condition);
if (result != Unknown) {
return result;
}
}

// TODO smarts for multiple constraints

// Otherwise, who knows.
return Unknown;
}

Result AndedConstraintSet::proves(const AndedConstraintSet& other) const {
if (other.empty()) {
// The empty set of constraints is always true.
return True;
}

bool hasUnknown = false;

for (auto& c : other) {
auto result = proves(c);
if (result == False) {
// The entire conjunction is proven false.
return False;
}
if (result == Unknown) {
hasUnknown = true;
}
}

return hasUnknown ? Unknown : True;
}

void AndedConstraintSet::fuzzyAnd(const Constraint& c) {
if (size() < MaxConstraints) {
push_back(c);
return;
}

// Otherwise, just do not add this one.
// TODO: We could try to be clever and see if one of the existing ones makes
// more sense to drop.
}

void AndedConstraintSet::fuzzyOr(const AndedConstraintSet& other) {
// If one is empty (no constraints, everything is true, and we can prove
// nothing useful) then it does not add anything to the other.
if (empty()) {
*this = other;
return;
}
if (other.empty()) {
return;
}

// If this is already implied by current constraints, then it is redundant.
// E.g. if we are { x = 10 } and other is { x >= 0 } then all we need is
// { x >= 0 } as the result of the OR.
if (proves(other) == True) {
*this = other;
return;
}
if (other.proves(*this) == True) {
return;
}
Comment on lines +138 to +147

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 doesn't handle the case where the constraints can be relaxed in both directions separately. For example:

{ x >= 2 /\ x <= 4 } \/ { x >= 1 /\ x <= 3 }

This should give { x >= 1 /\ x <= 4 }, but right now it just gives up.

This might be included in the // TODO smarts below, but I think it's important to see the full complexity here so we can get it factored as nicely as possible.

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.

What do you mean by "see the full complexity"? I'm not sure what you are asking this PR to do aside from have the existing 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.

I'm suggesting we resolve the TODO :)

But I guess I can just say in advance that the simplest way to do this will be in terms of a fuzzyOr operation on a pair of Constraints, so I guess we don't need to do it now.

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 am trying to keep this PR small.

I added to the comment.


// TODO smarts: handle <= > and so forth

// Otherwise, we don't know how to nicely OR these things, and expand to the
// trivial set of no constraints.
clear();
}

} // namespace wasm::constraint
125 changes: 125 additions & 0 deletions src/ir/constraint.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.
*/

//
// Constraints on the values of things, like x >=0, x < 42, and x == y. Allows
// inference whether other things are true given a set of constraints, like
// { x == 10 } => { x >= 5 }.
//

#ifndef wasm_ir_constraint_h
#define wasm_ir_constraint_h

#include <variant>

#include "ir/abstract.h"
#include "support/inplace_vector.h"
#include "support/utilities.h"
#include "wasm.h"

namespace wasm::constraint {

// A term in a constraint, either a local index or literal value.
struct Term : public std::variant<Index, Literal> {
bool operator==(const Term&) const = default;
};

// A constraint: some operation and some value, like "is equal to 17" or "is
// less than local 6".
struct Constraint {
Abstract::Op op = Abstract::Invalid;

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 there's much value in making invalid constraints representable (nor in reusing the Abstract enum). How about using a new enum that can be specific to this use case that does not have an Invalid member?

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.

Reusing Abstract is useful because we have code to parse IR into it. E.g. we need to parse AddInt32 into Abstract::Add (the next PR does this).

Without this reuse, we'd need to duplicate that code, or add a mapping of Abstract into a new enum.

I think this is exactly what Abstract is meant for: an abstract operation, without the details of a Type. This is precisely the right level of abstraction for a mathematical constraint system, mirroring the == etc. notation in math.

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.

Makes sense to reuse the parsing code 👍

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 still avoid adding Abstract::Invalid, though.

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.

What should the default value be, then? (0 seems risky, as it is a valid code)

Note that we already have such invalids everywhere else, so this change just brings Abstract into parity:

InvalidUnary

InvalidBinary

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 should not have a default value. Users that need some placeholder can use std::optional. I'd argue that having the invalids everywhere else is an anti-pattern that we should be trying to eliminate and we never would have introduced the invalids if we had had std::optional from the beginning.

Term term;

bool operator==(const Constraint&) const = default;
Comment thread
tlively marked this conversation as resolved.
};

// We limit constraints to a low number to ensure good performance even with
// simple brute-force solving.
// TODO: use a generic constraint solver..?

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 did have that POC for pulling in Z3. In the limit I guess that's what we'd want. 5c2bbb7

inline constexpr std::size_t MaxConstraints = 3;

// What we infer from one thing about another: true/false, or unknown.
enum Result { True, False, Unknown };

// A set of constraints connected by the logical "and" operation. That is, all
// the constraints are simultaneously true about some value. In the examples in
// the comments below, `x` is used for the thing all the constraints are talking
// about, which looks like a local, but it could be a global or a struct field
// or anything else in general.
struct AndedConstraintSet : inplace_vector<Constraint, MaxConstraints> {
// Check a condition against this set, that is, whether the existing
// constraints prove that it must be true, false, or unknown: whether
//
// { this } => { condition }
//
// https://en.wikipedia.org/wiki/Material_conditional#Truth_table
Result proves(const Constraint& condition) const;

// Check an entire other set.
Result proves(const AndedConstraintSet& other) const;

// Add a constraint to the set, ANDed with the others. This is a fuzzy
// operation because our capacity is bounded - we cannot have more than
// MaxConstraints. If too many are added, we will drop some, which means we
// will be able to prove less things (but we will never prove anything
// incorrectly).
void fuzzyAnd(const Constraint& c);

// Merge constraints using OR. We cannot represent such a thing directly
// (we only use AND), so we approximate it in a fuzzy way. For example, this
// would be valid:
//
// fuzzyOr({ x == 5 }, { x == 10 }) == { x >= 5 && x <= 10 }
//
// Note how the result here still accepts the values 5 and 10, but it also
// allows more. Formally, this has the following mathematical property:
//
// (X || Y) => fuzzyOr(X, Y)
//
// That is, if X or Y is true, the result of fuzzOr is also true. But the
// reverse is not always the case: fuzzyOr may be true without X || Y being
// true (see the truth table linked above, and the value 8 in the example).
//
// Returning to the example, we can use this to optimize as follows: if
// two code paths reaching a location have x == 5 and x == 10, so the value in
// the merge location is either 5 or 10, then if we see some i32.ge_s that
// does x >= 0 then we can evaluate it with proves():
//
// { x >= 5 && x <= 10 }.proves({ x >= 0 }) == True
//
// And it is valid to optimize that i32.ge_s into a constant 1, since
//
// { x == 5 || x == 10 } =>
// { x >= 5 && x <= 10 } =>
// { x >= 0 }
//
// I.e. the constraints imply the truth of the thing we are evaluating.
//
// Note that the fuzziness here means that fuzzyOr() can do a better or a
// worse job. It is always valid for fuzzyOr to return { } or any other
// always-true thing (see the truth table linked above). But then:
//
// { x == 5 || x == 10 } =>
// { } =!!>
// { x >= 0 }
//
// If we become too fuzzy, we lose the ability to imply anything useful.
void fuzzyOr(const AndedConstraintSet& other);
};

} // namespace wasm::constraint

#endif // wasm_ir_constraint_h
1 change: 1 addition & 0 deletions test/gtest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set(unittest_SOURCES
arena.cpp
cast-check.cpp
cfg.cpp
constraint.cpp
dataflow.cpp
delta_debugging.cpp
dfa_minimization.cpp
Expand Down
Loading
Loading