From 0fc9e55f2f2c42dd8efbdb637af431bb9f4ec3a1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:07:09 -0700 Subject: [PATCH 01/18] go --- src/passes/ConstantFieldPropagation.cpp | 5 +- src/support/inplace_vector.h | 193 ++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 src/support/inplace_vector.h diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 66f45bb6047..93d1ac7c567 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -60,6 +60,7 @@ #include "ir/utils.h" #include "pass.h" #include "support/hash.h" +#include "support/inplace_vector.h" #include "support/small_vector.h" #include "support/unique_deferring_queue.h" #include "wasm-builder.h" @@ -311,9 +312,7 @@ struct FunctionOptimizer : public WalkerPass> { // values = [ { 42, [A, B] }, { 1337, [C] } ]; struct Value { PossibleConstantValues constant; - // Use a SmallVector as we'll only have 2 Values, and so the stack usage - // here is fixed. - SmallVector types; + std::inplace_vector types; // Whether this slot is used. If so, |constant| has a value, and |types| // is not empty. diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h new file mode 100644 index 00000000000..0df08dd8c5e --- /dev/null +++ b/src/support/inplace_vector.h @@ -0,0 +1,193 @@ +/* + * 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. + */ + +// +// A vector of elements with a maximum size, storing them all in-place. This is +// similar to C++26's inplace_vector, and is basically a small_vector, except +// there is never any dynamic storage. +// + +#ifndef wasm_support_inplace_vector_h +#define wasm_support_inplace_vector_h + +#include +#include +#include + +#include "support/parent_index_iterator.h" + +namespace std { + +template class inplace_vector { + // fixed-space storage + size_t usedFixed = 0; + std::array fixed{}; + +public: + using value_type = T; + + inplace_vector() {} + inplace_vector(const inplace_vector& other) + : usedFixed(other.usedFixed), fixed(other.fixed) {} + inplace_vector(inplace_vector&& other) + : usedFixed(other.usedFixed), fixed(std::move(other.fixed)) {} + inplace_vector(std::initializer_list init) { + for (T item : init) { + push_back(item); + } + } + inplace_vector(size_t initialSize) { resize(initialSize); } + + inplace_vector& operator=(const inplace_vector& other) { + usedFixed = other.usedFixed; + fixed = other.fixed; + return *this; + } + + inplace_vector& operator=(inplace_vector&& other) { + usedFixed = other.usedFixed; + fixed = std::move(other.fixed); + return *this; + } + + T& operator[](size_t i) { return fixed[i]; } + + const T& operator[](size_t i) const { + return const_cast&>(*this)[i]; + } + + void push_back(const T& x) { + assert(usedFixed < N); + fixed[usedFixed++] = x; + } + + template void emplace_back(ArgTypes&&... Args) { + assert(usedFixed < N); + new (&fixed[usedFixed++]) T(std::forward(Args)...); + } + + void pop_back() { + assert(usedFixed > 0); + usedFixed--; + } + + T& back() { + assert(usedFixed > 0); + return fixed[usedFixed - 1]; + } + + const T& back() const { + assert(usedFixed > 0); + return fixed[usedFixed - 1]; + } + + size_t size() const { return usedFixed; } + + bool empty() const { return size() == 0; } + + void clear() { usedFixed = 0; } + + void resize(size_t newSize) { + assert(newSize <= N); + usedFixed = newSize; + } + + size_t capacity() const { return N; } + + bool operator==(const inplace_vector& other) const { + if (usedFixed != other.usedFixed) { + return false; + } + for (size_t i = 0; i < usedFixed; i++) { + if (fixed[i] != other.fixed[i]) { + return false; + } + } + return true; + } + + bool operator!=(const inplace_vector& other) const { + return !(*this == other); + } + + // iteration + + struct Iterator : wasm::ParentIndexIterator*, Iterator> { + using value_type = T; + using pointer = T*; + using reference = T&; + + Iterator(inplace_vector* parent, size_t index) + : wasm::ParentIndexIterator*, Iterator>{parent, + index} {} + Iterator(const Iterator& other) = default; + + T& operator*() { return (*this->parent)[this->index]; } + }; + + struct ConstIterator + : wasm::ParentIndexIterator*, ConstIterator> { + using value_type = const T; + using pointer = const T*; + using reference = const T&; + + ConstIterator(const inplace_vector* parent, size_t index) + : wasm::ParentIndexIterator*, ConstIterator>{ + parent, index} {} + ConstIterator(const ConstIterator& other) = default; + + const T& operator*() const { return (*this->parent)[this->index]; } + }; + + Iterator begin() { return Iterator(this, 0); } + Iterator end() { return Iterator(this, size()); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator end() const { return ConstIterator(this, size()); } + + void erase(Iterator a, Iterator b) { + // Atm we only support erasing at the end, which is very efficient. + assert(b == end()); + resize(a.index); + } +}; + +// A inplace_vector for which some values may be read before they are written, +// and in that case they have the value zero. +template +struct ZeroInitinplace_vector : public inplace_vector { + T& operator[](size_t i) { + if (i >= this->size()) { + resize(i + 1); + } + return inplace_vector::operator[](i); + } + + const T& operator[](size_t i) const { + return const_cast&>(*this)[i]; + } + + void resize(size_t newSize) { + auto oldSize = this->size(); + inplace_vector::resize(newSize); + for (size_t i = oldSize; i < this->size(); i++) { + (*this)[i] = 0; + } + } +}; + +} // namespace std + +#endif // wasm_support_inplace_vector_h From 5ad1b7511ec258bc946f8b68024c9994dafff8ce Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:09:53 -0700 Subject: [PATCH 02/18] go --- test/gtest/CMakeLists.txt | 1 + test/gtest/inplace_vector.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 test/gtest/inplace_vector.cpp diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 05ce44006a6..4bd358032d7 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -17,6 +17,7 @@ set(unittest_SOURCES int128.cpp leaves.cpp glbs.cpp + inplace_vector.cpp interpreter.cpp intervals.cpp istring.cpp diff --git a/test/gtest/inplace_vector.cpp b/test/gtest/inplace_vector.cpp new file mode 100644 index 00000000000..8fb2307dfe8 --- /dev/null +++ b/test/gtest/inplace_vector.cpp @@ -0,0 +1,9 @@ +#include "support/inplace_vector.h" +#include "gtest/gtest.h" + +using InplaceVectorTest = ::testing::Test; + +TEST_F(InplaceVectorTest, Basics) { + std::inplace_vector vec; + EXPECT_EQ(sizeof(vec), sizeof(size_t) + 10 * sizeof(int64_t)); +} From 81813634c5acdc2bf9e7916311923cdd4bfa0510 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:14:36 -0700 Subject: [PATCH 03/18] go --- src/support/inplace_vector.h | 24 ------------------------ test/gtest/inplace_vector.cpp | 31 ++++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h index 0df08dd8c5e..95b123a3eec 100644 --- a/src/support/inplace_vector.h +++ b/src/support/inplace_vector.h @@ -164,30 +164,6 @@ template class inplace_vector { } }; -// A inplace_vector for which some values may be read before they are written, -// and in that case they have the value zero. -template -struct ZeroInitinplace_vector : public inplace_vector { - T& operator[](size_t i) { - if (i >= this->size()) { - resize(i + 1); - } - return inplace_vector::operator[](i); - } - - const T& operator[](size_t i) const { - return const_cast&>(*this)[i]; - } - - void resize(size_t newSize) { - auto oldSize = this->size(); - inplace_vector::resize(newSize); - for (size_t i = oldSize; i < this->size(); i++) { - (*this)[i] = 0; - } - } -}; - } // namespace std #endif // wasm_support_inplace_vector_h diff --git a/test/gtest/inplace_vector.cpp b/test/gtest/inplace_vector.cpp index 8fb2307dfe8..ad0f2627c0b 100644 --- a/test/gtest/inplace_vector.cpp +++ b/test/gtest/inplace_vector.cpp @@ -3,7 +3,36 @@ using InplaceVectorTest = ::testing::Test; -TEST_F(InplaceVectorTest, Basics) { +TEST_F(InplaceVectorTest, Size) { std::inplace_vector vec; EXPECT_EQ(sizeof(vec), sizeof(size_t) + 10 * sizeof(int64_t)); } + +TEST_F(InplaceVectorTest, Basics) { + std::inplace_vector vec; + + vec.push_back(10); + EXPECT_EQ(vec[0], 10); + + vec.resize(3); + EXPECT_EQ(vec.size(), 3); + + vec[1] = 20; + vec[2] = 30; + EXPECT_EQ(vec[1], 20); + EXPECT_EQ(vec[2], 30); + + vec.pop_back(); + EXPECT_EQ(vec.size(), 2); +} + +TEST_F(InplaceVectorTest, I) { + std::inplace_vector vec{10, 20, 30}; + std::vector normal; + + for (auto x : vec) { + normal.push_back(x); + } + + EXPECT_EQ(normal, std::vector({10, 20, 30})); +} From d0ad2f48cc4cb06fa9507457816217eded05548c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:14:43 -0700 Subject: [PATCH 04/18] go --- test/gtest/inplace_vector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gtest/inplace_vector.cpp b/test/gtest/inplace_vector.cpp index ad0f2627c0b..bf651b096e7 100644 --- a/test/gtest/inplace_vector.cpp +++ b/test/gtest/inplace_vector.cpp @@ -18,7 +18,7 @@ TEST_F(InplaceVectorTest, Basics) { EXPECT_EQ(vec.size(), 3); vec[1] = 20; - vec[2] = 30; + vec[2] = 30; EXPECT_EQ(vec[1], 20); EXPECT_EQ(vec[2], 30); From c5b7d1d2752dc3b731328560f0f6e08b730e9d38 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:15:38 -0700 Subject: [PATCH 05/18] go --- src/support/inplace_vector.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h index 95b123a3eec..a4971c8b2b4 100644 --- a/src/support/inplace_vector.h +++ b/src/support/inplace_vector.h @@ -16,8 +16,9 @@ // // A vector of elements with a maximum size, storing them all in-place. This is -// similar to C++26's inplace_vector, and is basically a small_vector, except +// similar to c++26's inplace_vector, and is basically a small_vector, except // there is never any dynamic storage. +// TODO: remove when we have c++26 // #ifndef wasm_support_inplace_vector_h From 0e35b2b857e1176ffc8141a74501f455e4052414 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:16:42 -0700 Subject: [PATCH 06/18] go --- test/gtest/inplace_vector.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/gtest/inplace_vector.cpp b/test/gtest/inplace_vector.cpp index bf651b096e7..10c46b1f633 100644 --- a/test/gtest/inplace_vector.cpp +++ b/test/gtest/inplace_vector.cpp @@ -5,6 +5,7 @@ using InplaceVectorTest = ::testing::Test; TEST_F(InplaceVectorTest, Size) { std::inplace_vector vec; + // An inplace_vector is just a size plus the in-place storage. EXPECT_EQ(sizeof(vec), sizeof(size_t) + 10 * sizeof(int64_t)); } From 60d50b4b9b7c8f99c44f8168c52903b16c05f90f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:38:45 -0700 Subject: [PATCH 07/18] go --- src/ir/CMakeLists.txt | 1 + src/ir/abstract.h | 3 +- src/ir/constraint.cpp | 164 +++++++++++++++++++++++++++++++++++++ src/ir/constraint.h | 160 ++++++++++++++++++++++++++++++++++++ test/gtest/CMakeLists.txt | 1 + test/gtest/constraint.cpp | 167 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 src/ir/constraint.cpp create mode 100644 src/ir/constraint.h create mode 100644 test/gtest/constraint.cpp diff --git a/src/ir/CMakeLists.txt b/src/ir/CMakeLists.txt index 919069770c5..5521529cd91 100644 --- a/src/ir/CMakeLists.txt +++ b/src/ir/CMakeLists.txt @@ -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 diff --git a/src/ir/abstract.h b/src/ir/abstract.h index 04b04e34223..d0f3794908c 100644 --- a/src/ir/abstract.h +++ b/src/ir/abstract.h @@ -56,7 +56,8 @@ enum Op { GtS, GtU, GeS, - GeU + GeU, + Invalid }; inline bool hasAnyRotateShift(BinaryOp op) { diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp new file mode 100644 index 00000000000..5eb3528cdaa --- /dev/null +++ b/src/ir/constraint.cpp @@ -0,0 +1,164 @@ +/* + * 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 + +#include "ir/constraint.h" +#include "ir/properties.h" +#include "wasm.h" + +namespace wasm::constraint { + +namespace { + +// Core comparison of two constraints. +// +// 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 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(&a.value)) { + if (auto* bConstant = std::get_if(&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: { + } + } + } + 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. + return {}; + } + default: { + } + } + } + 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; + } + + // TODO smarts + + // Otherwise, we don't know how to nicely OR these things, and expand to the + // trivial set of no constraints (i.e., where everything is true, and we can + // prove nothing). + clear(); +} + +std::optional LocalConstraint::parse(Expression* curr) { + auto* binary = curr->dynCast(); + if (!binary) { + // TODO: unary etc. + return {}; + } + + // The left must be a get. + auto* leftGet = binary->left->dynCast(); + if (!leftGet) { + return {}; + } + + // The right must be a get or a constant. + auto* rightGet = binary->right->dynCast(); + std::optional rightConstant; + if (Properties::isSingleConstantExpression(binary->right)) { + rightConstant = Properties::getLiteral(binary->right); + } + if (!rightGet && !rightConstant) { + return {}; + } + + // The operation must be one we recognize. + for (auto op : {Abstract::Eq, Abstract::Ne}) { + if (Abstract::getBinary(binary->type, op) == binary->op) { + auto value = rightGet ? Value(rightGet->index) : Value(*rightConstant); + return LocalConstraint{leftGet->index, Constraint{op, value}}; + } + } + return {}; +} + +} // namespace wasm::constraint diff --git a/src/ir/constraint.h b/src/ir/constraint.h new file mode 100644 index 00000000000..b2fe6c17bb8 --- /dev/null +++ b/src/ir/constraint.h @@ -0,0 +1,160 @@ +/* + * 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 locals, things like x >=0, x < 42, and x == y. + +#ifndef wasm_ir_constraint_h +#define wasm_ir_constraint_h + +#include + +#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 { + bool operator==(const Value&) const = default; +}; + +// A constraint: some operation and some value, like "is equal to 17" or "is +// less than local $z". +struct Constraint { + // The operation relating two values, and the values. + Abstract::Op op = Abstract::Invalid; + Value value; + + bool operator==(const Constraint&) const = default; + + operator bool() const { return op != Abstract::Invalid; } +}; + +// We limit constraints to a low number to ensure good performance even with +// simple brute-force solving. +// TODO: use a generic constraint solver..? +inline constexpr std::size_t MaxConstraints = 3; + +// What a constraint is known to be: 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, but it could be a global or a struct field or anything else in +// general. +struct AndedConstraintSet : std::inplace_vector { + // 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; + + // 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. + void and_(const Constraint& c) { push_back(c); } + + // Add constraints that are ORed. 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 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 } + // + // Note that the fuzziness here means that fuzzyOr() can do a better or a + // worse job. It is always valid for fuzzOr 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); +}; + +// 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 parse(Expression* curr); +}; + +} // namespace wasm::constraint + +#endif // wasm_ir_constraint_h diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 05ce44006a6..6c2c9b56d68 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -9,6 +9,7 @@ set(unittest_SOURCES arena.cpp cast-check.cpp cfg.cpp + constraint.cpp dataflow.cpp delta_debugging.cpp dfa_minimization.cpp diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp new file mode 100644 index 00000000000..0c9031db116 --- /dev/null +++ b/test/gtest/constraint.cpp @@ -0,0 +1,167 @@ +#include "ir/constraint.h" +#include "ir/abstract.h" +#include "gtest/gtest.h" + +using namespace wasm; +using namespace wasm::Abstract; +using namespace wasm::constraint; + +TEST(ConstraintTest, TestEmpty) { + // An empty constraint is invalid. + Constraint c; + EXPECT_FALSE(c); +} + +TEST(ConstraintTest, TestEq) { + // Sets start empty. + AndedConstraintSet s; + EXPECT_TRUE(s.empty()); + + // x == 5 (we use "x" for the name of the thing being compared). + Constraint c{Eq, Literal(int32_t(5))}; + + // We can't infer anything using an empty set. + EXPECT_EQ(s.check(c), Unknown); + + // If we add it, then things check out. + s.and_(c); + EXPECT_EQ(s.size(), 1); + EXPECT_EQ(s.check(c), True); + + // x == 10, a different number: we can infer false. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(10))}), False); + + // x != 15: we can infer true. + EXPECT_EQ(s.check(Constraint{Ne, Literal(int32_t(15))}), True); + + // x != 5: we can infer false. + EXPECT_EQ(s.check(Constraint{Ne, Literal(int32_t(5))}), False); +} + +TEST(ConstraintTest, TestNe) { + AndedConstraintSet s; + // x != 5 + Constraint c{Ne, Literal(int32_t(5))}; + s.and_(c); + + // Checks out versus itself. + EXPECT_EQ(s.check(c), True); + + // x == 10: we don't know. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(10))}), Unknown); + + // x != 15: we don't know. + EXPECT_EQ(s.check(Constraint{Ne, Literal(int32_t(15))}), Unknown); + + // x == 5: we can infer false. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(5))}), False); +} + +TEST(ConstraintTest, TestMulti) { + AndedConstraintSet s; + // x != 5 && x != 10 + Constraint c{Ne, Literal(int32_t(5))}; + Constraint d{Ne, Literal(int32_t(10))}; + s.and_(c); + s.and_(d); + + // Each checks out versus itself. + EXPECT_EQ(s.check(c), True); + EXPECT_EQ(s.check(d), True); + + // x == 5: false. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(5))}), False); + + // x == 10: false. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(10))}), False); + + // x == 15: we don't know. + EXPECT_EQ(s.check(Constraint{Eq, Literal(int32_t(15))}), Unknown); + + // x != 15: we don't know. + EXPECT_EQ(s.check(Constraint{Ne, Literal(int32_t(15))}), Unknown); +} + +TEST(ConstraintTest, TestSets) { + // x == 5 + Constraint c{Eq, Literal(int32_t(5))}; + + AndedConstraintSet s; + + // Any set always proves itself to be true. + EXPECT_EQ(s.check(s), True); + + // Ditto after adding something. + s.and_(c); + EXPECT_EQ(s.check(s), True); + + // Another set, empty. + AndedConstraintSet t; + + // Any set always proves an empty set to be true. + EXPECT_EQ(s.check(t), True); + + // Make both sets contain the same stuff. + t.and_(c); + EXPECT_EQ(s.check(t), True); + + // Now t has *different* stuff, x == 10, which given s is false. + t.clear(); + t.and_(Constraint{Eq, Literal(int32_t(10))}); + EXPECT_EQ(s.check(t), False); + + // Same, with x != 10. Now we know it is true. + t.clear(); + t.and_(Constraint{Ne, Literal(int32_t(10))}); + EXPECT_EQ(s.check(t), True); + + // In reverse, we can infer nothing: knowing x != 10 does not say if x == 5. + EXPECT_EQ(t.check(s), Unknown); +} + +TEST(ConstraintTest, TestOrTrivial) { + // { x == 5 } + AndedConstraintSet s; + s.and_(Constraint{Eq, Literal(int32_t(5))}); + + // { } + AndedConstraintSet empty; + + // Anything ORed with the empty set is unchanged. + auto t = s; + t.fuzzyOr(empty); + EXPECT_EQ(t, s); + + // Flipped. + t = empty; + t.fuzzyOr(s); + EXPECT_EQ(t, s); + + // ORing with oneself changes nothing + t = s; + t.fuzzyOr(s); + EXPECT_EQ(t, s); +} + +TEST(ConstraintTest, TestOrImplies) { + // { x == 5 } + AndedConstraintSet s; + s.and_(Constraint{Eq, Literal(int32_t(5))}); + + // { x != 10 } + AndedConstraintSet t; + t.and_(Constraint{Ne, Literal(int32_t(10))}); + + // ORing these leaves us with x != 10. + auto u = s; + u.fuzzyOr(t); + EXPECT_EQ(u, t); + + // Flipped. + u = t; + u.fuzzyOr(s); + EXPECT_EQ(u, t); +} + +// TODO: test a fuzzyOr of { x = 10 } and { x >= 0 }, once we support +// inequalities From 626b5d77eee5f09e772404d8fb09553e7d04ee9a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:41:01 -0700 Subject: [PATCH 08/18] feedback --- src/support/inplace_vector.h | 4 ++-- test/gtest/inplace_vector.cpp | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h index a4971c8b2b4..35dd7ed30b2 100644 --- a/src/support/inplace_vector.h +++ b/src/support/inplace_vector.h @@ -30,7 +30,7 @@ #include "support/parent_index_iterator.h" -namespace std { +namespace wasm { template class inplace_vector { // fixed-space storage @@ -165,6 +165,6 @@ template class inplace_vector { } }; -} // namespace std +} // namespace wasm #endif // wasm_support_inplace_vector_h diff --git a/test/gtest/inplace_vector.cpp b/test/gtest/inplace_vector.cpp index 10c46b1f633..34c1a3d3e1b 100644 --- a/test/gtest/inplace_vector.cpp +++ b/test/gtest/inplace_vector.cpp @@ -3,17 +3,22 @@ using InplaceVectorTest = ::testing::Test; +using namespace wasm; + TEST_F(InplaceVectorTest, Size) { - std::inplace_vector vec; + inplace_vector vec; // An inplace_vector is just a size plus the in-place storage. EXPECT_EQ(sizeof(vec), sizeof(size_t) + 10 * sizeof(int64_t)); } TEST_F(InplaceVectorTest, Basics) { - std::inplace_vector vec; + inplace_vector vec; + EXPECT_EQ(vec.size(), 0); + EXPECT_TRUE(vec.empty()); vec.push_back(10); EXPECT_EQ(vec[0], 10); + EXPECT_EQ(vec.size(), 1); vec.resize(3); EXPECT_EQ(vec.size(), 3); @@ -28,7 +33,7 @@ TEST_F(InplaceVectorTest, Basics) { } TEST_F(InplaceVectorTest, I) { - std::inplace_vector vec{10, 20, 30}; + inplace_vector vec{10, 20, 30}; std::vector normal; for (auto x : vec) { From 58964063b1db7975aee5e48e546af5bb428a0657 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:41:20 -0700 Subject: [PATCH 09/18] feedback --- src/passes/ConstantFieldPropagation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 93d1ac7c567..172a266d703 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -312,7 +312,7 @@ struct FunctionOptimizer : public WalkerPass> { // values = [ { 42, [A, B] }, { 1337, [C] } ]; struct Value { PossibleConstantValues constant; - std::inplace_vector types; + inplace_vector types; // Whether this slot is used. If so, |constant| has a value, and |types| // is not empty. From cf29b5879e82d2f3e0fd705e4881455f2ee6e1e1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:44:15 -0700 Subject: [PATCH 10/18] fix --- src/ir/constraint.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/constraint.h b/src/ir/constraint.h index b2fe6c17bb8..1c69ececce7 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -58,7 +58,7 @@ enum Result { True, False, Unknown }; // the comments below, `x` is used for the thing all the constraints are talking // about, but it could be a global or a struct field or anything else in // general. -struct AndedConstraintSet : std::inplace_vector { +struct AndedConstraintSet : inplace_vector { // Check a condition against this set, that is, whether the existing // constraints prove that it must be true, false, or unknown: whether // From cdaff6bd92e0a1562596d42a1c68042891d3c0bd Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:50:30 -0700 Subject: [PATCH 11/18] clean --- src/ir/constraint.cpp | 33 ------------------------------- src/ir/constraint.h | 46 ++++++++++++++++++------------------------- 2 files changed, 19 insertions(+), 60 deletions(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 5eb3528cdaa..a4556ab2d35 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -128,37 +128,4 @@ void AndedConstraintSet::fuzzyOr(const AndedConstraintSet& other) { clear(); } -std::optional LocalConstraint::parse(Expression* curr) { - auto* binary = curr->dynCast(); - if (!binary) { - // TODO: unary etc. - return {}; - } - - // The left must be a get. - auto* leftGet = binary->left->dynCast(); - if (!leftGet) { - return {}; - } - - // The right must be a get or a constant. - auto* rightGet = binary->right->dynCast(); - std::optional rightConstant; - if (Properties::isSingleConstantExpression(binary->right)) { - rightConstant = Properties::getLiteral(binary->right); - } - if (!rightGet && !rightConstant) { - return {}; - } - - // The operation must be one we recognize. - for (auto op : {Abstract::Eq, Abstract::Ne}) { - if (Abstract::getBinary(binary->type, op) == binary->op) { - auto value = rightGet ? Value(rightGet->index) : Value(*rightConstant); - return LocalConstraint{leftGet->index, Constraint{op, value}}; - } - } - return {}; -} - } // namespace wasm::constraint diff --git a/src/ir/constraint.h b/src/ir/constraint.h index 1c69ececce7..9bdf0578ef8 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -14,7 +14,11 @@ * limitations under the License. */ -// Constraints on the values of locals, things like x >=0, x < 42, and x == y. +// +// 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 @@ -34,7 +38,7 @@ struct Value : public std::variant { }; // A constraint: some operation and some value, like "is equal to 17" or "is -// less than local $z". +// less than local 6". struct Constraint { // The operation relating two values, and the values. Abstract::Op op = Abstract::Invalid; @@ -50,14 +54,14 @@ struct Constraint { // TODO: use a generic constraint solver..? inline constexpr std::size_t MaxConstraints = 3; -// What a constraint is known to be: true/false, or unknown. +// 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, but it could be a global or a struct field or anything else in -// general. +// 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 { // Check a condition against this set, that is, whether the existing // constraints prove that it must be true, false, or unknown: whether @@ -95,10 +99,13 @@ struct AndedConstraintSet : inplace_vector { 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. - void and_(const Constraint& c) { push_back(c); } + // sure not to add too many (i.e. it is invalid to call this when full()). + void and_(const Constraint& c) { + assert(!full()); + push_back(c); + } - // Add constraints that are ORed. We cannot represent such a thing directly + // 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: // @@ -113,7 +120,7 @@ struct AndedConstraintSet : inplace_vector { // 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 + // 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(): @@ -126,8 +133,10 @@ struct AndedConstraintSet : inplace_vector { // { 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 fuzzOr to return { } or any other + // 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 } => @@ -138,23 +147,6 @@ struct AndedConstraintSet : inplace_vector { void fuzzyOr(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 parse(Expression* curr); -}; - } // namespace wasm::constraint #endif // wasm_ir_constraint_h From 94d216171c01e3acc22635649b98e821395eb169 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:54:58 -0700 Subject: [PATCH 12/18] clean --- test/gtest/constraint.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index 0c9031db116..b43ca3bde01 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -17,13 +17,14 @@ TEST(ConstraintTest, TestEq) { AndedConstraintSet s; EXPECT_TRUE(s.empty()); - // x == 5 (we use "x" for the name of the thing being compared). + // x == 5 (we use "x" for the name of the thing being compared, in these + // comments). Constraint c{Eq, Literal(int32_t(5))}; // We can't infer anything using an empty set. EXPECT_EQ(s.check(c), Unknown); - // If we add it, then things check out. + // If we add it, then things check out: a thing always proves itself true. s.and_(c); EXPECT_EQ(s.size(), 1); EXPECT_EQ(s.check(c), True); From 735d7ea9bf545108707fc19fa740f20ff9ede219 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 14:57:19 -0700 Subject: [PATCH 13/18] clean --- src/ir/constraint.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index a4556ab2d35..04547421d97 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -24,7 +24,7 @@ namespace wasm::constraint { namespace { -// Core comparison of two constraints. +// 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). @@ -123,8 +123,7 @@ void AndedConstraintSet::fuzzyOr(const AndedConstraintSet& other) { // TODO smarts // Otherwise, we don't know how to nicely OR these things, and expand to the - // trivial set of no constraints (i.e., where everything is true, and we can - // prove nothing). + // trivial set of no constraints. clear(); } From 6e80fde633f8e876e3bad5a39d6bbbfdbbc9ad12 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 15:40:13 -0700 Subject: [PATCH 14/18] const --- src/support/inplace_vector.h | 2 +- src/support/small_vector.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h index 35dd7ed30b2..21826ea737d 100644 --- a/src/support/inplace_vector.h +++ b/src/support/inplace_vector.h @@ -46,7 +46,7 @@ template class inplace_vector { inplace_vector(inplace_vector&& other) : usedFixed(other.usedFixed), fixed(std::move(other.fixed)) {} inplace_vector(std::initializer_list init) { - for (T item : init) { + for (const T& item : init) { push_back(item); } } diff --git a/src/support/small_vector.h b/src/support/small_vector.h index 8cad6ade29e..f4a1560d21f 100644 --- a/src/support/small_vector.h +++ b/src/support/small_vector.h @@ -49,7 +49,7 @@ template class SmallVector { : usedFixed(other.usedFixed), fixed(std::move(other.fixed)), flexible(std::move(other.flexible)) {} SmallVector(std::initializer_list init) { - for (T item : init) { + for (const T& item : init) { push_back(item); } } From cf7fcc6522757ee362efccf63a5ae319ecb33a3d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 15:44:56 -0700 Subject: [PATCH 15/18] undo CFP change --- src/passes/ConstantFieldPropagation.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 172a266d703..66f45bb6047 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -60,7 +60,6 @@ #include "ir/utils.h" #include "pass.h" #include "support/hash.h" -#include "support/inplace_vector.h" #include "support/small_vector.h" #include "support/unique_deferring_queue.h" #include "wasm-builder.h" @@ -312,7 +311,9 @@ struct FunctionOptimizer : public WalkerPass> { // values = [ { 42, [A, B] }, { 1337, [C] } ]; struct Value { PossibleConstantValues constant; - inplace_vector types; + // Use a SmallVector as we'll only have 2 Values, and so the stack usage + // here is fixed. + SmallVector types; // Whether this slot is used. If so, |constant| has a value, and |types| // is not empty. From 0930461f4ade92f20165631c0f0453c22e0f7c8f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 16:14:38 -0700 Subject: [PATCH 16/18] tidy --- src/ir/constraint.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 04547421d97..ff81d26807a 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -54,6 +54,7 @@ std::optional checkPair(const Constraint& a, const Constraint& b) { default: { } } + break; } case Abstract::Ne: { switch (b.op) { @@ -72,6 +73,7 @@ std::optional checkPair(const Constraint& a, const Constraint& b) { default: { } } + break; } default: { } From 7b7d2ac95fdf5bca1f7bffcfdd2189d97c3da2a7 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 16:20:26 -0700 Subject: [PATCH 17/18] add.assert --- src/ir/constraint.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index ff81d26807a..532a097bc81 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -68,6 +68,7 @@ std::optional checkPair(const Constraint& a, const Constraint& b) { 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: { From 679bd245f0e6dc2175185704372a5cf2e88ed2af Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 8 Jun 2026 16:21:30 -0700 Subject: [PATCH 18/18] fix.comment --- src/ir/constraint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 532a097bc81..3225c98f95a 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -66,7 +66,7 @@ std::optional checkPair(const Constraint& a, const Constraint& b) { return {}; } case Abstract::Ne: { - // x == c vs x == c', and we already handled full equality + // x != c vs x != c', and we already handled full equality // earlier, hence c != c', and we can infer nothing. assert(*aConstant != *bConstant); return {};