diff --git a/src/analysis/lattices/bound.h b/src/analysis/lattices/bound.h new file mode 100644 index 00000000000..9e71ebc17f8 --- /dev/null +++ b/src/analysis/lattices/bound.h @@ -0,0 +1,303 @@ +/* + * 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. + */ + +#ifndef wasm_analysis_lattices_bound_h +#define wasm_analysis_lattices_bound_h + +#include +#include +#include +#include + +#include "analysis/lattice.h" + +namespace wasm::analysis { + +enum class BoundRelation { LT, LE, GE, GT }; + +inline std::ostream& operator<<(std::ostream& os, BoundRelation rel) { + switch (rel) { + case BoundRelation::LT: + os << "<"; + break; + case BoundRelation::LE: + os << "<="; + break; + case BoundRelation::GE: + os << ">="; + break; + case BoundRelation::GT: + os << ">"; + break; + } + return os; +} + +template struct Bound { + struct Bot { + bool operator==(const Bot&) const { return true; } + bool operator!=(const Bot&) const { return false; } + }; + struct Top { + bool operator==(const Top&) const { return true; } + bool operator!=(const Top&) const { return false; } + }; + + struct Constraint { + BoundRelation rel; + typename L::Element val; + + bool operator==(const Constraint& other) const { + return rel == other.rel && val == other.val; + } + bool operator!=(const Constraint& other) const { return !(*this == other); } + }; + + using Element = std::variant; + + L lattice; + + Bound(L&& lattice) : lattice(std::move(lattice)) {} + + Element getBottom() const noexcept { return Bot{}; } + Element getTop() const noexcept { return Top{}; } + + Element makeBound(BoundRelation rel, typename L::Element val) const { + auto bot_L = lattice.getBottom(); + std::optional top_L = std::nullopt; + if constexpr (requires { + { lattice.getTop() } -> std::same_as; + }) { + top_L = lattice.getTop(); + } + + if (rel == BoundRelation::LT && lattice.compare(val, bot_L) == EQUAL) { + return Bot{}; + } + if (top_L && rel == BoundRelation::GT && + lattice.compare(val, *top_L) == EQUAL) { + return Bot{}; + } + if (top_L && rel == BoundRelation::LE && + lattice.compare(val, *top_L) == EQUAL) { + return Top{}; + } + if (rel == BoundRelation::GE && lattice.compare(val, bot_L) == EQUAL) { + return Top{}; + } + + return Constraint{rel, val}; + } + + bool implies(const Constraint& c1, const Constraint& c2) const noexcept { + auto comp = lattice.compare(c1.val, c2.val); + switch (c1.rel) { + case BoundRelation::LT: + switch (c2.rel) { + case BoundRelation::LT: + return comp == LESS || comp == EQUAL; + case BoundRelation::LE: + return comp == LESS || comp == EQUAL; + default: + return false; + } + break; + case BoundRelation::LE: + switch (c2.rel) { + case BoundRelation::LE: + return comp == LESS || comp == EQUAL; + case BoundRelation::LT: + return comp == LESS; + default: + return false; + } + break; + case BoundRelation::GE: + switch (c2.rel) { + case BoundRelation::GE: + return comp == GREATER || comp == EQUAL; + case BoundRelation::GT: + return comp == GREATER; + default: + return false; + } + break; + case BoundRelation::GT: + switch (c2.rel) { + case BoundRelation::GT: + return comp == GREATER || comp == EQUAL; + case BoundRelation::GE: + return comp == GREATER || comp == EQUAL; + default: + return false; + } + break; + } + return false; + } + + LatticeComparison compare(const Element& a, const Element& b) const noexcept { + if (std::holds_alternative(a) && std::holds_alternative(b)) + return EQUAL; + if (std::holds_alternative(a)) + return LESS; + if (std::holds_alternative(b)) + return GREATER; + + if (std::holds_alternative(a) && std::holds_alternative(b)) + return EQUAL; + if (std::holds_alternative(a)) + return GREATER; + if (std::holds_alternative(b)) + return LESS; + + const auto& c1 = std::get(a); + const auto& c2 = std::get(b); + + bool a_implies_b = implies(c1, c2); + bool b_implies_a = implies(c2, c1); + + if (a_implies_b && b_implies_a) + return EQUAL; + if (a_implies_b) + return LESS; + if (b_implies_a) + return GREATER; + return NO_RELATION; + } + + bool join(Element& joinee, const Element& joiner) const noexcept { + if (std::holds_alternative(joiner)) + return false; + if (std::holds_alternative(joinee)) { + joinee = joiner; + return true; + } + if (std::holds_alternative(joinee)) + return false; + if (std::holds_alternative(joiner)) { + joinee = Top{}; + return true; + } + + const auto& c1 = std::get(joinee); + const auto& c2 = std::get(joiner); + + auto result = joinConstraints(c1, c2); + if (joinee == result) + return false; + joinee = std::move(result); + return true; + } + + bool meet(Element& meetee, const Element& meeter) const noexcept { + if (std::holds_alternative(meeter)) + return false; + if (std::holds_alternative(meetee)) { + meetee = meeter; + return true; + } + if (std::holds_alternative(meetee)) + return false; + if (std::holds_alternative(meeter)) { + meetee = Bot{}; + return true; + } + + const auto& c1 = std::get(meetee); + const auto& c2 = std::get(meeter); + + auto result = meetConstraints(c1, c2); + if (meetee == result) + return false; + meetee = std::move(result); + return true; + } + +private: + typename L::Element meet_values(const typename L::Element& a, + const typename L::Element& b) const { + auto copy = a; + lattice.meet(copy, b); + return copy; + } + + Element joinConstraints(const Constraint& c1, const Constraint& c2) const { + if (implies(c1, c2)) + return c2; + if (implies(c2, c1)) + return c1; + + auto is_upper = [](BoundRelation r) { + return r == BoundRelation::LT || r == BoundRelation::LE; + }; + auto is_lower = [](BoundRelation r) { + return r == BoundRelation::GT || r == BoundRelation::GE; + }; + + if (is_upper(c1.rel) && is_upper(c2.rel)) { + auto joined_val = c1.val; + lattice.join(joined_val, c2.val); + return makeBound(BoundRelation::LT, joined_val); + } + + if (is_lower(c1.rel) && is_lower(c2.rel)) { + auto met = meet_values(c1.val, c2.val); + return makeBound(BoundRelation::GT, met); + } + + return Top{}; + } + + Element meetConstraints(const Constraint& c1, const Constraint& c2) const { + if (implies(c1, c2)) + return c1; + if (implies(c2, c1)) + return c2; + + if (c1.rel == c2.rel) { + if (c1.rel == BoundRelation::LE) { + auto met_val = meet_values(c1.val, c2.val); + return makeBound(c1.rel, met_val); + } + if (c1.rel == BoundRelation::GE) { + auto joined_val = c1.val; + lattice.join(joined_val, c2.val); + return makeBound(c1.rel, joined_val); + } + } + + return Bot{}; + } +}; + +template +std::ostream& operator<<(std::ostream& os, + const typename Bound::Element& elem) { + if (std::holds_alternative::Bot>(elem)) { + os << "bound bot"; + } else if (std::holds_alternative::Top>(elem)) { + os << "bound top"; + } else { + const auto& c = std::get::Constraint>(elem); + os << "Bound(" << c.rel << " " << c.val << ")"; + } + return os; +} + +} // namespace wasm::analysis + +#endif // wasm_analysis_lattices_bound_h diff --git a/src/tools/wasm-fuzz-lattices.cpp b/src/tools/wasm-fuzz-lattices.cpp index 20bb1091f6a..b0512a83cf5 100644 --- a/src/tools/wasm-fuzz-lattices.cpp +++ b/src/tools/wasm-fuzz-lattices.cpp @@ -24,6 +24,7 @@ #include "analysis/lattice.h" #include "analysis/lattices/array.h" #include "analysis/lattices/bool.h" +#include "analysis/lattices/bound.h" #include "analysis/lattices/bounded-conjunction.h" #include "analysis/lattices/flat.h" #include "analysis/lattices/int.h" @@ -191,6 +192,8 @@ using TupleLattice = analysis::Tuple; using OneOfFullLattice = analysis::OneOf; using OneOfLattice = analysis::OneOf; +using FuzzFullBound = analysis::Bound; + using FullLatticeVariant = std::variant, TupleFullLattice, - OneOfFullLattice>; + OneOfFullLattice, + FuzzFullBound>; struct RandomFullLattice::LatticeImpl : FullLatticeVariant {}; @@ -210,7 +214,8 @@ using FullLatticeElementVariant = typename ArrayFullLattice::Element, typename Vector::Element, typename TupleFullLattice::Element, - typename OneOfFullLattice::Element>; + typename OneOfFullLattice::Element, + typename FuzzFullBound::Element>; struct RandomFullLattice::ElementImpl : FullLatticeElementVariant {}; @@ -278,7 +283,7 @@ using LatticeElementVariant = struct RandomLattice::ElementImpl : LatticeElementVariant {}; -constexpr int FullLatticePicks = 8; +constexpr int FullLatticePicks = 9; RandomFullLattice::RandomFullLattice(Random& rand, size_t depth, @@ -319,6 +324,10 @@ RandomFullLattice::RandomFullLattice(Random& rand, LatticeImpl{OneOfFullLattice{RandomFullLattice{rand, depth + 1}, RandomFullLattice{rand, depth + 1}}}); return; + case 8: + lattice = std::make_unique( + LatticeImpl{FuzzFullBound{RandomFullLattice{rand, depth + 1}}}); + return; } WASM_UNREACHABLE("unexpected pick"); } @@ -442,6 +451,22 @@ RandomFullLattice::Element RandomFullLattice::makeElement() const noexcept { return ElementImpl{l->get<1>(std::get<1>(l->lattices).makeElement())}; } } + if (const auto* l = std::get_if(lattice.get())) { + auto pick = rand.upTo(8); + if (pick == 0) { + return ElementImpl{l->getBottom()}; + } + if (pick == 1) { + return ElementImpl{l->getTop()}; + } + BoundRelation rels[] = {BoundRelation::LT, + BoundRelation::LE, + BoundRelation::GE, + BoundRelation::GT}; + BoundRelation rel = rels[rand.upTo(4)]; + auto val = l->lattice.makeElement(); + return ElementImpl{l->makeBound(rel, val)}; + } WASM_UNREACHABLE("unexpected lattice"); } @@ -586,6 +611,19 @@ void printFullElement(std::ostream& os, } else { WASM_UNREACHABLE("unexpected one-of element"); } + } else if (const auto* e = + std::get_if(&*elem)) { + if (std::holds_alternative(*e)) { + os << "bound bot\n"; + } else if (std::holds_alternative(*e)) { + os << "bound top\n"; + } else { + const auto& c = std::get(*e); + os << "Bound( " << c.rel << " \n"; + printFullElement(os, c.val, depth + 1); + indent(os, depth); + os << ")\n"; + } } else { WASM_UNREACHABLE("unexpected element"); } diff --git a/test/gtest/lattices.cpp b/test/gtest/lattices.cpp index 67f35779266..072e3f7ea41 100644 --- a/test/gtest/lattices.cpp +++ b/test/gtest/lattices.cpp @@ -18,6 +18,7 @@ #include "analysis/lattices/abstraction.h" #include "analysis/lattices/array.h" #include "analysis/lattices/bool.h" +#include "analysis/lattices/bound.h" #include "analysis/lattices/bounded-conjunction.h" #include "analysis/lattices/conetype.h" #include "analysis/lattices/flat.h" @@ -1669,3 +1670,115 @@ TEST(BoundedConjunctionLattice, BoundedMeet) { // Meet with redundancy test(e_1, e_1_2, e_1_2); } + +TEST(BoundLattice, Compare) { + analysis::Int32 integers; + analysis::Bound lattice(std::move(integers)); + + auto bot = lattice.getBottom(); + auto top = lattice.getTop(); + + auto make_lt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LT, v); + }; + auto make_le = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LE, v); + }; + auto make_ge = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GE, v); + }; + auto make_gt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GT, v); + }; + + // LT implication + EXPECT_EQ(lattice.compare(make_lt(4), make_lt(5)), + analysis::LESS); // x < 4 => x < 5 + EXPECT_EQ(lattice.compare(make_lt(5), make_lt(4)), analysis::GREATER); + EXPECT_EQ(lattice.compare(make_lt(4), make_le(4)), + analysis::LESS); // x < 4 => x <= 4 + EXPECT_EQ(lattice.compare(make_lt(4), make_le(3)), + analysis::GREATER); // x < 4 not=> x <= 3 (generically, LE(3) => + // LT(4) so LT(4) > LE(3)) + + // LT and GT (unrelated) + EXPECT_EQ(lattice.compare(make_lt(4), make_gt(4)), analysis::NO_RELATION); +} + +TEST(BoundLattice, Join) { + analysis::Int32 integers; + analysis::Bound lattice(std::move(integers)); + + auto bot = lattice.getBottom(); + auto top = lattice.getTop(); + + auto make_lt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LT, v); + }; + auto make_le = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LE, v); + }; + auto make_ge = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GE, v); + }; + auto make_gt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GT, v); + }; + + auto test = + [&](const auto& joinee, const auto& joiner, const auto& expected) { + auto copy = joinee; + EXPECT_EQ(lattice.join(copy, joiner), joinee != expected); + EXPECT_EQ(copy, expected); + }; + + // Same relations + test(make_lt(4), make_lt(5), make_lt(5)); // x < 4 or x < 5 => x < 5 + test(make_le(4), make_le(5), make_le(5)); + test(make_ge(4), make_ge(5), make_ge(4)); // x >= 4 or x >= 5 => x >= 4 + test(make_gt(4), make_gt(5), make_gt(4)); + + // Mixed relations + test(make_lt(4), make_le(4), make_le(4)); // x < 4 or x <= 4 => x <= 4 + test(make_lt(4), make_gt(5), top); // x < 4 or x > 5 => Top +} + +TEST(BoundLattice, Meet) { + analysis::Int32 integers; + analysis::Bound lattice(std::move(integers)); + + auto bot = lattice.getBottom(); + auto top = lattice.getTop(); + + auto make_lt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LT, v); + }; + auto make_le = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::LE, v); + }; + auto make_ge = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GE, v); + }; + auto make_gt = [&](int v) { + return lattice.makeBound(analysis::BoundRelation::GT, v); + }; + + auto test = + [&](const auto& meetee, const auto& meeter, const auto& expected) { + auto copy = meetee; + EXPECT_EQ(lattice.meet(copy, meeter), meetee != expected); + EXPECT_EQ(copy, expected); + }; + + // Opposite relations meeting to Bot + test(make_le(4), make_ge(5), bot); // x <= 4 and x >= 5 => Bot + test(make_lt(4), make_ge(4), bot); // x < 4 and x >= 4 => Bot + test(make_le(4), make_gt(4), bot); // x <= 4 and x > 4 => Bot + + // Same relations + test(make_le(4), make_le(5), make_le(4)); // x <= 4 and x <= 5 => x <= 4 + test(make_ge(4), make_ge(5), make_ge(5)); // x >= 4 and x >= 5 => x >= 5 + + // Related + test(make_lt(4), make_le(4), make_lt(4)); +}