Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
45 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
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
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
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
133 changes: 133 additions & 0 deletions src/ir/constraint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 {

// Core comparison of two constraints: whether a => b
//
// Returns a Result, or an empty option if we should keep working (i.e., a
// result of Unknown means we are certain we can just return Unknown).
std::optional<Result> checkPair(const Constraint& a, const Constraint& b) {
// A thing always implies itself.
if (a == b) {
return True;
}

// Comparisons of two constants.
if (auto* aConstant = std::get_if<Literal>(&a.value)) {
if (auto* bConstant = std::get_if<Literal>(&b.value)) {
switch (a.op) {
case Abstract::Eq: {
switch (b.op) {
case Abstract::Eq: {
// x == c vs x == c', and we already handled full equality
// earlier, hence c != c', and we found a contradiction.
assert(*aConstant != *bConstant);
return False;
}
case Abstract::Ne: {
// x == c vs x != c'. We can infer the result based on relating c
// and c'.
return *aConstant != *bConstant ? True : False;
}
default: {
}
}
break;
}
case Abstract::Ne: {
switch (b.op) {
case Abstract::Eq: {
// x != c vs x == c'. If c == c', we can infer.
if (*aConstant == *bConstant) {
return False;
}
return {};
}
case Abstract::Ne: {
// x != c vs x != c', and we already handled full equality
// earlier, hence c != c', and we can infer nothing.
assert(*aConstant != *bConstant);
return {};
}
default: {
}
}
break;
}
default: {
}
}
}
}

return {};
}

} // anonymous namespace

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

// TODO smarts for multiple constraints

// Otherwise, who knows.
return Unknown;
}

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 (check(other) == True) {
*this = other;
return;
}
if (other.check(*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

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

} // namespace wasm::constraint
152 changes: 152 additions & 0 deletions src/ir/constraint.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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 value in a constraint, either a local index or literal value.
struct Value : public std::variant<Index, Literal> {
bool operator==(const Value&) const = default;
};

// A constraint: some operation and some value, like "is equal to 17" or "is
// less than local 6".
struct Constraint {
// The operation relating two values, and the values.
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.

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 see your point now, though I'm not sure I agree - the invalid values are also nice for debugging, when you use an uninitialized value by mistake for example.

Anyhow, this PR is consistent with the current state of the codebase - how about leaving it as is, and considering your change later, separately?

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.

Though, since it is so small a change here, I guess I can remove it and keep the entire discussion for later. Done.

Value value;

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

operator bool() const { return op != Abstract::Invalid; }
Comment thread
tlively marked this conversation as resolved.
Outdated
};

// 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 check(const Constraint& condition) const;

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.

Perhaps proves or implies?

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.

Hmm, yeah. Another option is eval as @MaxGraey suggests?

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.

Thinking more on this, I think that x.implies(y) is not quite right, as it reads as 'x implies y' i.e. x is not checking if it implies y, but sounds like a new constraint, that makes x somehow imply y. Ditto for x.proves(y).

So checkImplies might work, but is longer than check? I am leaning towards check or eval.

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 would strongly prefer some variant of proves or implies. I don't think those names sound like they're adding new constraints, but something like checkImplication would also be fine with me. eval does not suggest the correct operation to me.

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.

(FWIW, the tests use "proves" in their comments)

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, I don't feel strongly. Renamed to proves.


// Check an entire other set.
Result check(const AndedConstraintSet& other) const {
if (other.empty()) {
// The empty set of constraints is always true.
return True;
}

Result result = Unknown;
for (auto& c : other) {
auto currResult = check(c);
if (currResult == Unknown) {
// If something is unknown, it all is.
return Unknown;
}
if (result == Unknown) {
// This is the first result
result = currResult;
} else if (result != currResult) {
// This is a later result, and different, so give up.
return Unknown;
}
}
return result;
}

bool full() const { return size() == MaxConstraints; }

// Add a constraint to the set, ANDed with the others. The caller must make
// sure not to add too many (i.e. it is invalid to call this when full()).
void and_(const Constraint& c) {

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 nice to avoid burdening the caller with making this decision. We could just arbitrarily drop extra constraints, or allow the user to supply a heuristic for determining which to keep. The end result will be the same if we remove this burden from the caller, since the caller will otherwise have to make the same decisions.

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, maybe that is better. Should it then be fuzzyAnd..?

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.

Yeah. Maybe "bounded" or "approximate" instead of "fuzzy?" Or alternatively we could use "join" and "meet" language to differentiate these operations from the precise logical connectives.

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's wrong with "fuzzy", as a short and clear way to put it?

I prefer something with and/or (over meet/join) as it makes the connection to the logical operator clear. I.e. mathematical logic is the right math here, not algebraic groups or set theory 😉

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.

"Fuzzy logic" is a completely different thing: https://en.wikipedia.org/wiki/Fuzzy_logic.

This is more about soundly over- or under-approximating our knowledge as we analyze the program.

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.

Ah, fair point. Ok, renamed to approximate*

assert(!full());
push_back(c);
}

// Merge constraints using OR. We cannot represent such a thing directly
Comment thread
kripken marked this conversation as resolved.
Outdated
// (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
Comment thread
kripken marked this conversation as resolved.
Outdated
// 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 check():
//
// { x >= 5 && x <= 10 }.check({ 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