| /* |
| * Copyright 2023 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 <random> |
| #include <string> |
| #include <type_traits> |
| #include <variant> |
| |
| #include "analysis/lattice.h" |
| #include "analysis/lattices/array.h" |
| #include "analysis/lattices/bool.h" |
| #include "analysis/lattices/flat.h" |
| #include "analysis/lattices/int.h" |
| #include "analysis/lattices/inverted.h" |
| #include "analysis/lattices/lift.h" |
| #include "analysis/lattices/shared.h" |
| #include "analysis/lattices/stack.h" |
| #include "analysis/lattices/tuple.h" |
| #include "analysis/lattices/valtype.h" |
| #include "analysis/lattices/vector.h" |
| #include "analysis/liveness-transfer-function.h" |
| #include "analysis/reaching-definitions-transfer-function.h" |
| #include "analysis/transfer-function.h" |
| |
| #include "support/command-line.h" |
| #include "tools/fuzzing.h" |
| #include "tools/fuzzing/random.h" |
| |
| namespace wasm { |
| using RandEngine = std::mt19937_64; |
| using namespace analysis; |
| |
| // Helps printing error messages. |
| std::string LatticeComparisonNames[4] = { |
| "No Relation", "Equal", "Less", "Greater"}; |
| |
| uint64_t getSeed() { |
| // Return a (truly) random 64-bit value. |
| std::random_device rand; |
| return std::uniform_int_distribution<uint64_t>{}(rand); |
| } |
| |
| // Actually a pointer to `L::ElementImpl`, but we erase the type to avoid |
| // getting into a situation where `L` satisfying `Lattice` or `FullLattice` |
| // circularly requires that `L` satisfies `Lattice` or `FullLattice`. C++ does |
| // not allow concepts to depend on themselves. Also make the pointer copyable to |
| // satisfy that constraint on lattice elements. |
| template<typename L> |
| struct RandomElement : std::unique_ptr<void, void (*)(void*)> { |
| RandomElement() = default; |
| |
| RandomElement(typename L::ElementImpl&& other) |
| : std::unique_ptr<void, void (*)(void*)>( |
| new typename L::ElementImpl(std::move(other)), |
| [](void* e) { delete static_cast<typename L::ElementImpl*>(e); }) {} |
| |
| RandomElement(const RandomElement& other) |
| : RandomElement([&]() { |
| auto copy = *other; |
| return copy; |
| }()) {} |
| |
| RandomElement(RandomElement&& other) = default; |
| RandomElement& operator=(const RandomElement& other) { |
| if (this != &other) { |
| new (this) RandomElement(other); |
| } |
| return *this; |
| } |
| |
| RandomElement& operator=(RandomElement&& other) = default; |
| |
| typename L::ElementImpl& operator*() { |
| return *static_cast<typename L::ElementImpl*>(get()); |
| } |
| |
| const typename L::ElementImpl& operator*() const { |
| return *static_cast<const typename L::ElementImpl*>(get()); |
| } |
| |
| typename L::ElementImpl* operator->() { return &*(*this); } |
| |
| const typename L::ElementImpl* operator->() const { return &*(*this); } |
| }; |
| |
| struct RandomFullLattice { |
| // The inner lattice and lattice element types. These must be defined later |
| // because they depend on `RandomFullLattice` satisfying `FullLattice`, but |
| // that requires the type to be complete. |
| struct LatticeImpl; |
| struct ElementImpl; |
| using Element = RandomElement<RandomFullLattice>; |
| |
| Random& rand; |
| |
| // Indirect because LatticeImpl recursively contains RandomFullLattice. |
| std::unique_ptr<LatticeImpl> lattice; |
| |
| RandomFullLattice(Random& rand, |
| size_t depth = 0, |
| std::optional<uint32_t> maybePick = std::nullopt); |
| |
| // Make a random element of this lattice. |
| Element makeElement() const noexcept; |
| |
| Element getBottom() const noexcept; |
| Element getTop() const noexcept; |
| LatticeComparison compare(const Element& a, const Element& b) const noexcept; |
| bool join(Element& a, const Element& b) const noexcept; |
| bool meet(Element& a, const Element& b) const noexcept; |
| }; |
| |
| struct RandomLattice { |
| // The inner lattice and lattice element types. These must be defined later |
| // because they depend on `RandomLattice` satisfying `Lattice`, but that |
| // requires the type to be complete. |
| struct LatticeImpl; |
| struct ElementImpl; |
| using Element = RandomElement<RandomLattice>; |
| |
| Random& rand; |
| |
| // Indirect because L recursively contains RandomLattice. |
| std::unique_ptr<LatticeImpl> lattice; |
| |
| RandomLattice(Random& rand, size_t depth = 0); |
| |
| // Make a random element of this lattice. |
| Element makeElement() const noexcept; |
| |
| Element getBottom() const noexcept; |
| LatticeComparison compare(const Element& a, const Element& b) const noexcept; |
| bool join(Element& a, const Element& b) const noexcept; |
| }; |
| |
| #if __cplusplus >= 202002L |
| static_assert(FullLattice<RandomFullLattice>); |
| static_assert(Lattice<RandomLattice>); |
| #endif |
| |
| using ArrayFullLattice = analysis::Array<RandomFullLattice, 2>; |
| using ArrayLattice = analysis::Array<RandomLattice, 2>; |
| |
| using TupleFullLattice = analysis::Tuple<RandomFullLattice, RandomFullLattice>; |
| using TupleLattice = analysis::Tuple<RandomLattice, RandomLattice>; |
| |
| using FullLatticeVariant = std::variant<Bool, |
| UInt32, |
| ValType, |
| Inverted<RandomFullLattice>, |
| ArrayFullLattice, |
| Vector<RandomFullLattice>, |
| TupleFullLattice>; |
| |
| struct RandomFullLattice::LatticeImpl : FullLatticeVariant {}; |
| |
| using FullLatticeElementVariant = |
| std::variant<typename Bool::Element, |
| typename UInt32::Element, |
| typename ValType::Element, |
| typename Inverted<RandomFullLattice>::Element, |
| typename ArrayFullLattice::Element, |
| typename Vector<RandomFullLattice>::Element, |
| typename TupleFullLattice::Element>; |
| |
| struct RandomFullLattice::ElementImpl : FullLatticeElementVariant {}; |
| |
| using LatticeVariant = std::variant<RandomFullLattice, |
| Flat<uint32_t>, |
| Lift<RandomLattice>, |
| ArrayLattice, |
| Vector<RandomLattice>, |
| TupleLattice, |
| SharedPath<RandomLattice>>; |
| |
| struct RandomLattice::LatticeImpl : LatticeVariant {}; |
| |
| using LatticeElementVariant = |
| std::variant<typename RandomFullLattice::Element, |
| typename Flat<uint32_t>::Element, |
| typename Lift<RandomLattice>::Element, |
| typename ArrayLattice::Element, |
| typename Vector<RandomLattice>::Element, |
| typename TupleLattice::Element, |
| typename SharedPath<RandomLattice>::Element>; |
| |
| struct RandomLattice::ElementImpl : LatticeElementVariant {}; |
| |
| constexpr int FullLatticePicks = 7; |
| |
| RandomFullLattice::RandomFullLattice(Random& rand, |
| size_t depth, |
| std::optional<uint32_t> maybePick) |
| : rand(rand) { |
| // TODO: Limit the depth once we get lattices with more fan-out. |
| uint32_t pick = maybePick ? *maybePick : rand.upTo(FullLatticePicks); |
| switch (pick) { |
| case 0: |
| lattice = std::make_unique<LatticeImpl>(LatticeImpl{Bool{}}); |
| return; |
| case 1: |
| lattice = std::make_unique<LatticeImpl>(LatticeImpl{UInt32{}}); |
| return; |
| case 2: |
| lattice = std::make_unique<LatticeImpl>(LatticeImpl{ValType{}}); |
| return; |
| case 3: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{Inverted{RandomFullLattice{rand, depth + 1}}}); |
| return; |
| case 4: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{ArrayFullLattice{RandomFullLattice{rand, depth + 1}}}); |
| return; |
| case 5: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{Vector{RandomFullLattice{rand, depth + 1}, rand.upTo(4)}}); |
| return; |
| case 6: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{TupleFullLattice{RandomFullLattice{rand, depth + 1}, |
| RandomFullLattice{rand, depth + 1}}}); |
| return; |
| } |
| WASM_UNREACHABLE("unexpected pick"); |
| } |
| |
| RandomLattice::RandomLattice(Random& rand, size_t depth) : rand(rand) { |
| // TODO: Limit the depth once we get lattices with more fan-out. |
| uint32_t pick = rand.upTo(FullLatticePicks + 6); |
| |
| if (pick < FullLatticePicks) { |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{RandomFullLattice{rand, depth, pick}}); |
| return; |
| } |
| |
| switch (pick) { |
| case FullLatticePicks + 0: |
| lattice = std::make_unique<LatticeImpl>(LatticeImpl{Flat<uint32_t>{}}); |
| return; |
| case FullLatticePicks + 1: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{Lift{RandomLattice{rand, depth + 1}}}); |
| return; |
| case FullLatticePicks + 2: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{ArrayLattice{RandomLattice{rand, depth + 1}}}); |
| return; |
| case FullLatticePicks + 3: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{Vector{RandomLattice{rand, depth + 1}, rand.upTo(4)}}); |
| return; |
| case FullLatticePicks + 4: |
| lattice = std::make_unique<LatticeImpl>(LatticeImpl{TupleLattice{ |
| RandomLattice{rand, depth + 1}, RandomLattice{rand, depth + 1}}}); |
| return; |
| case FullLatticePicks + 5: |
| lattice = std::make_unique<LatticeImpl>( |
| LatticeImpl{SharedPath{RandomLattice{rand, depth + 1}}}); |
| return; |
| } |
| WASM_UNREACHABLE("unexpected pick"); |
| } |
| |
| RandomFullLattice::Element RandomFullLattice::makeElement() const noexcept { |
| if (std::get_if<Bool>(lattice.get())) { |
| return ElementImpl{rand.pick(true, false)}; |
| } |
| if (std::get_if<UInt32>(lattice.get())) { |
| return ElementImpl{rand.upToSquared(33)}; |
| } |
| if (std::get_if<ValType>(lattice.get())) { |
| Type type; |
| // Choose a random type. No need to make all possible types available as |
| // long as we cover all the kinds of relationships between types. |
| switch (rand.upTo(8)) { |
| case 0: |
| type = Type::unreachable; |
| break; |
| case 1: |
| type = Type::none; |
| break; |
| case 2: |
| type = Type::i32; |
| break; |
| case 3: |
| type = Type::f32; |
| break; |
| case 4: |
| type = Type(HeapType::any, rand.oneIn(2) ? Nullable : NonNullable); |
| break; |
| case 5: |
| type = Type(HeapType::none, rand.oneIn(2) ? Nullable : NonNullable); |
| break; |
| case 6: |
| type = Type(HeapType::struct_, rand.oneIn(2) ? Nullable : NonNullable); |
| break; |
| case 7: |
| type = Type(HeapType::array, rand.oneIn(2) ? Nullable : NonNullable); |
| break; |
| } |
| return ElementImpl{type}; |
| } |
| if (const auto* l = std::get_if<Inverted<RandomFullLattice>>(lattice.get())) { |
| return ElementImpl{l->lattice.makeElement()}; |
| } |
| if (const auto* l = std::get_if<ArrayFullLattice>(lattice.get())) { |
| return ElementImpl{typename ArrayFullLattice::Element{ |
| l->lattice.makeElement(), l->lattice.makeElement()}}; |
| } |
| if (const auto* l = std::get_if<Vector<RandomFullLattice>>(lattice.get())) { |
| std::vector<typename RandomFullLattice::Element> elem; |
| elem.reserve(l->size); |
| for (size_t i = 0; i < l->size; ++i) { |
| elem.push_back(l->lattice.makeElement()); |
| } |
| return ElementImpl{std::move(elem)}; |
| } |
| if (const auto* l = std::get_if<TupleFullLattice>(lattice.get())) { |
| return ElementImpl{typename TupleFullLattice::Element{ |
| std::get<0>(l->lattices).makeElement(), |
| std::get<1>(l->lattices).makeElement()}}; |
| } |
| WASM_UNREACHABLE("unexpected lattice"); |
| } |
| |
| RandomLattice::Element RandomLattice::makeElement() const noexcept { |
| if (const auto* l = std::get_if<RandomFullLattice>(lattice.get())) { |
| return ElementImpl{l->makeElement()}; |
| } |
| if (const auto* l = std::get_if<Flat<uint32_t>>(lattice.get())) { |
| auto pick = rand.upTo(6); |
| switch (pick) { |
| case 4: |
| return ElementImpl{l->getBottom()}; |
| case 5: |
| return ElementImpl{l->getTop()}; |
| default: |
| return ElementImpl{l->get(std::move(pick))}; |
| } |
| } |
| if (const auto* l = std::get_if<Lift<RandomLattice>>(lattice.get())) { |
| return ElementImpl{rand.oneIn(4) ? l->getBottom() |
| : l->get(l->lattice.makeElement())}; |
| } |
| if (const auto* l = std::get_if<ArrayLattice>(lattice.get())) { |
| return ElementImpl{typename ArrayLattice::Element{ |
| l->lattice.makeElement(), l->lattice.makeElement()}}; |
| } |
| if (const auto* l = std::get_if<Vector<RandomLattice>>(lattice.get())) { |
| std::vector<typename RandomLattice::Element> elem; |
| elem.reserve(l->size); |
| for (size_t i = 0; i < l->size; ++i) { |
| elem.push_back(l->lattice.makeElement()); |
| } |
| return ElementImpl{std::move(elem)}; |
| } |
| if (const auto* l = std::get_if<TupleLattice>(lattice.get())) { |
| return ElementImpl{ |
| typename TupleLattice::Element{std::get<0>(l->lattices).makeElement(), |
| std::get<1>(l->lattices).makeElement()}}; |
| } |
| if (const auto* l = std::get_if<SharedPath<RandomLattice>>(lattice.get())) { |
| auto elem = l->getBottom(); |
| l->join(elem, l->lattice.makeElement()); |
| return ElementImpl{elem}; |
| } |
| WASM_UNREACHABLE("unexpected lattice"); |
| } |
| |
| void indent(std::ostream& os, int depth) { |
| for (int i = 0; i < depth; ++i) { |
| os << " "; |
| } |
| } |
| |
| void printFullElement(std::ostream& os, |
| const typename RandomFullLattice::Element& elem, |
| int depth) { |
| indent(os, depth); |
| |
| if (const auto* e = std::get_if<typename Bool::Element>(&*elem)) { |
| os << (*e ? "true" : "false") << "\n"; |
| } else if (const auto* e = std::get_if<typename UInt32::Element>(&*elem)) { |
| os << *e << "\n"; |
| } else if (const auto* e = std::get_if<typename ValType::Element>(&*elem)) { |
| os << *e << "\n"; |
| } else if (const auto* e = |
| std::get_if<typename Inverted<RandomFullLattice>::Element>( |
| &*elem)) { |
| os << "Inverted(\n"; |
| printFullElement(os, *e, depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } else if (const auto* e = |
| std::get_if<typename ArrayFullLattice::Element>(&*elem)) { |
| os << "Array[\n"; |
| printFullElement(os, e->front(), depth + 1); |
| printFullElement(os, e->back(), depth + 1); |
| indent(os, depth); |
| os << "]\n"; |
| } else if (const auto* vec = |
| std::get_if<typename Vector<RandomFullLattice>::Element>( |
| &*elem)) { |
| os << "Vector[\n"; |
| for (const auto& e : *vec) { |
| printFullElement(os, e, depth + 1); |
| } |
| indent(os, depth); |
| os << "]\n"; |
| } else if (const auto* e = |
| std::get_if<typename TupleFullLattice::Element>(&*elem)) { |
| os << "Tuple(\n"; |
| const auto& [first, second] = *e; |
| printFullElement(os, first, depth + 1); |
| printFullElement(os, second, depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } else { |
| WASM_UNREACHABLE("unexpected element"); |
| } |
| } |
| |
| void printElement(std::ostream& os, |
| const typename RandomLattice::Element& elem, |
| int depth = 0) { |
| if (const auto* e = |
| std::get_if<typename RandomFullLattice::Element>(&*elem)) { |
| printFullElement(os, *e, depth); |
| return; |
| } |
| |
| indent(os, depth); |
| |
| if (const auto* e = std::get_if<typename Flat<uint32_t>::Element>(&*elem)) { |
| if (e->isBottom()) { |
| os << "flat bot\n"; |
| } else if (e->isTop()) { |
| os << "flat top\n"; |
| } else { |
| os << "flat " << *e->getVal() << "\n"; |
| } |
| } else if (const auto* e = |
| std::get_if<typename Lift<RandomLattice>::Element>(&*elem)) { |
| if (e->isBottom()) { |
| os << "lift bot\n"; |
| } else { |
| os << "Lifted(\n"; |
| printElement(os, **e, depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } |
| } else if (const auto* e = |
| std::get_if<typename ArrayLattice::Element>(&*elem)) { |
| os << "Array[\n"; |
| printElement(os, e->front(), depth + 1); |
| printElement(os, e->back(), depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } else if (const auto* vec = |
| std::get_if<typename Vector<RandomLattice>::Element>(&*elem)) { |
| os << "Vector[\n"; |
| for (const auto& e : *vec) { |
| printElement(os, e, depth + 1); |
| } |
| indent(os, depth); |
| os << "]\n"; |
| } else if (const auto* e = |
| std::get_if<typename TupleLattice::Element>(&*elem)) { |
| os << "Tuple(\n"; |
| const auto& [first, second] = *e; |
| printElement(os, first, depth + 1); |
| printElement(os, second, depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } else if (const auto* e = |
| std::get_if<typename SharedPath<RandomLattice>::Element>( |
| &*elem)) { |
| os << "SharedPath(\n"; |
| printElement(os, **e, depth + 1); |
| indent(os, depth); |
| os << ")\n"; |
| } else { |
| WASM_UNREACHABLE("unexpected element"); |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& os, |
| const typename RandomLattice::Element& elem) { |
| printElement(os, elem); |
| return os; |
| } |
| |
| // Check that random lattices have the correct mathematical properties by |
| // checking the relationships between random elements. |
| void checkLatticeProperties(Random& rand, bool verbose) { |
| RandomLattice lattice(rand); |
| |
| // Generate the lattice elements we will perform checks on. |
| typename RandomLattice::Element elems[3] = { |
| lattice.makeElement(), lattice.makeElement(), lattice.makeElement()}; |
| |
| if (verbose) { |
| std::cout << "Random lattice elements:\n" |
| << elems[0] << "\n" |
| << elems[1] << "\n" |
| << elems[2]; |
| } |
| |
| // Calculate the relations between the generated elements. |
| LatticeComparison relation[3][3]; |
| for (int i = 0; i < 3; ++i) { |
| for (int j = 0; j < 3; ++j) { |
| relation[i][j] = lattice.compare(elems[i], elems[j]); |
| } |
| } |
| |
| // Reflexivity: x == x |
| for (int i = 0; i < 3; ++i) { |
| if (lattice.compare(elems[i], elems[i]) != EQUAL) { |
| Fatal() << "Lattice element is not reflexive:\n" << elems[i]; |
| } |
| } |
| |
| // Anti-symmetry: x < y implies y > x, etc. |
| for (int i = 0; i < 3; ++i) { |
| for (int j = 0; j < 3; ++j) { |
| auto forward = relation[i][j]; |
| auto reverse = relation[j][i]; |
| if (reverseComparison(forward) != reverse) { |
| Fatal() |
| << "Lattice elements are not anti-symmetric.\nFirst element:\n\n" |
| << elems[i] << "\nSecond element:\n\n" |
| << elems[j] |
| << "\nForward relation: " << LatticeComparisonNames[forward] |
| << "\nReverse relation: " << LatticeComparisonNames[reverse] << "\n"; |
| } |
| } |
| } |
| |
| // Transitivity: x < y and y < z imply x < z, etc. |
| for (int i = 0; i < 3; ++i) { |
| for (int j = 0; j < 3; ++j) { |
| for (int k = 0; k < 3; ++k) { |
| auto ij = relation[i][j], jk = relation[j][k], ik = relation[i][k]; |
| if (ij == NO_RELATION || jk == NO_RELATION) { |
| continue; |
| } |
| if ((ij == LESS && jk == GREATER) || (ij == GREATER && jk == LESS)) { |
| continue; |
| } |
| auto expected = ij == EQUAL ? jk : ij; |
| if (ik != expected) { |
| Fatal() << "Lattice elements are not transitive.\nFrist element:\n\n" |
| << elems[i] << "\nSecond element:\n\n" |
| << elems[j] << "\nThird element:\n\n" |
| << elems[k] << "\nFirst to second relation: " |
| << LatticeComparisonNames[ij] |
| << "\nSecond to third relation: " |
| << LatticeComparisonNames[jk] |
| << "\nFirst to thrid relation: " << LatticeComparisonNames[ik] |
| << "\n"; |
| } |
| } |
| } |
| } |
| |
| // Joins (i.e. least upper bounds) |
| for (int i = 0; i < 3; ++i) { |
| { |
| // Identity: elem u bot = elem |
| auto join = elems[i]; |
| lattice.join(join, lattice.getBottom()); |
| if (lattice.compare(join, elems[i]) != EQUAL) { |
| Fatal() |
| << "Join of element and bottom is not equal to element:\nElement:\n\n" |
| << elems[i] << "\nJoin:\n\n" |
| << join; |
| } |
| } |
| { |
| // Identity: bot u elem = elem |
| auto join = lattice.getBottom(); |
| lattice.join(join, elems[i]); |
| if (lattice.compare(join, elems[i]) != EQUAL) { |
| Fatal() |
| << "Join of bottom and element is not equal to element:\nElement:\n\n" |
| << elems[i] << "\nJoin:\n\n" |
| << join; |
| } |
| } |
| { |
| // Identity: elem u elem = elem |
| auto join = elems[i]; |
| lattice.join(join, elems[i]); |
| if (lattice.compare(join, elems[i]) != EQUAL) { |
| Fatal() |
| << "Join of element with itself equal to element:\nElement:\n\n" |
| << elems[i] << "\nJoin:\n\n" |
| << join; |
| } |
| } |
| for (int j = 0; j < 3; ++j) { |
| // Commutativity: x u y = y u x |
| auto ij = elems[i]; |
| bool ijModified = lattice.join(ij, elems[j]); |
| auto ji = elems[j]; |
| bool jiModified = lattice.join(ji, elems[i]); |
| if (lattice.compare(ij, ji) != EQUAL) { |
| Fatal() << "Join is not commutative:\nFirst element:\n\n" |
| << elems[i] << "\nSecond element:\n\n" |
| << elems[j] << "\nJoin(first, second):\n\n" |
| << ij << "\nJoin(second, first):\n\n" |
| << ji; |
| } |
| |
| // Identity: x < y implies x u y = y |
| if (relation[i][j] == LESS) { |
| if (lattice.compare(ij, elems[j]) != EQUAL) { |
| Fatal() |
| << "Join is not equal to greater element:\nLesser element:\n\n" |
| << elems[i] << "\nGreater element:\n\n" |
| << elems[j] << "\nJoin:\n\n" |
| << ij; |
| } |
| if (jiModified) { |
| Fatal() |
| << "Join incorrectly reported modification:\nLesser element:\n\n" |
| << elems[i] << "\nGreater element:\n\n" |
| << elems[j]; |
| } |
| if (!ijModified) { |
| Fatal() |
| << "Join should have reported modification:\nLesser element:\n\n" |
| << elems[i] << "\nGreater element:\n\n" |
| << elems[j]; |
| } |
| } |
| |
| for (int k = 0; k < 3; ++k) { |
| // *Least* upper bound: x <= z && y <= z implies x u y <= z |
| if (relation[i][k] == LESS && relation[j][k] == LESS) { |
| auto IJtoK = lattice.compare(ij, elems[k]); |
| if (IJtoK != EQUAL && IJtoK != LESS) { |
| Fatal() << "Join is not least upper bound:\nFirst element:\n\n" |
| << elems[i] << "\nSecond element:\n\n" |
| << elems[j] << "\nJoin:\n\n" |
| << ij << "\nOther upper bound:\n\n" |
| << elems[k]; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Utility class which provides methods to check properties of the transfer |
| // function and lattice of an analysis. |
| template<Lattice L, TransferFunction TxFn> struct AnalysisChecker { |
| L& lattice; |
| TxFn& txfn; |
| std::string latticeName; |
| std::string txfnName; |
| uint64_t latticeElementSeed; |
| Name funcName; |
| |
| AnalysisChecker(L& lattice, |
| TxFn& txfn, |
| std::string latticeName, |
| std::string txfnName, |
| uint64_t latticeElementSeed, |
| Name funcName) |
| : lattice(lattice), txfn(txfn), latticeName(latticeName), |
| txfnName(txfnName), latticeElementSeed(latticeElementSeed), |
| funcName(funcName) {} |
| |
| void printFailureInfo(std::ostream& os) { |
| os << "Error for " << txfnName << " and " << latticeName |
| << " at lattice element seed " << latticeElementSeed << " and function " |
| << funcName << ".\n"; |
| } |
| |
| // Prints information about a particular test case consisting of a randomly |
| // generated function and triple of randomly generate lattice elements. |
| void printVerboseFunctionCase(std::ostream& os, |
| typename L::Element& x, |
| typename L::Element& y, |
| typename L::Element& z) { |
| os << "Using lattice element seed " << latticeElementSeed << "\nGenerated " |
| << latticeName << " elements:\n"; |
| x.print(os); |
| os << ",\n"; |
| y.print(os); |
| os << ",\n"; |
| z.print(os); |
| os << "\nfor " << funcName << " to test " << txfnName << ".\n\n"; |
| } |
| |
| public: |
| // Given two input - output lattice pairs of a transfer function, checks if |
| // the transfer function is monotonic. If this is violated, then we print out |
| // the CFG block input which caused the transfer function to exhibit |
| // non-monotonic behavior. |
| void checkMonotonicity(const BasicBlock& bb, |
| typename L::Element& first, |
| typename L::Element& second, |
| typename L::Element& firstResult, |
| typename L::Element& secondResult) { |
| LatticeComparison beforeCmp = lattice.compare(first, second); |
| LatticeComparison afterCmp = lattice.compare(firstResult, secondResult); |
| |
| // Cases in which monotonicity is preserved. |
| if (beforeCmp == LatticeComparison::NO_RELATION) { |
| // If there is no relation in the first place, we can't expect anything. |
| return; |
| } else if (beforeCmp == LatticeComparison::LESS && |
| (afterCmp == LatticeComparison::LESS || |
| afterCmp == LatticeComparison::EQUAL)) { |
| // x < y and f(x) <= f(y) |
| return; |
| } else if (beforeCmp == LatticeComparison::GREATER && |
| (afterCmp == LatticeComparison::GREATER || |
| afterCmp == LatticeComparison::EQUAL)) { |
| // x > y and f(x) >= f(y) |
| return; |
| } else if (beforeCmp == LatticeComparison::EQUAL && |
| afterCmp == LatticeComparison::EQUAL) { |
| // x = y and f(x) = f(y) |
| return; |
| } |
| |
| std::stringstream ss; |
| printFailureInfo(ss); |
| |
| ss << "Elements "; |
| first.print(ss); |
| ss << " -> "; |
| firstResult.print(ss); |
| ss << " and "; |
| second.print(ss); |
| ss << " -> "; |
| secondResult.print(ss); |
| ss << "\n show that the transfer function is not monotone when given the " |
| "input:\n"; |
| bb.print(ss); |
| ss << "\n"; |
| |
| Fatal() << ss.str(); |
| } |
| |
| // Checks transfer function relevant properties given a CFG and three input |
| // states. It does this by applying the transfer function on each CFG block |
| // using the same three input states each time and then checking properties on |
| // the inputs and outputs. |
| void checkTransferFunction(CFG& cfg, |
| typename L::Element x, |
| typename L::Element y, |
| typename L::Element z) { |
| for (const auto& bb : cfg) { |
| // Apply transfer function on each lattice element. |
| auto xResult = x; |
| txfn.transfer(bb, xResult); |
| auto yResult = y; |
| txfn.transfer(bb, yResult); |
| auto zResult = z; |
| txfn.transfer(bb, zResult); |
| |
| // Check monotonicity for every pair of transfer function outputs. |
| checkMonotonicity(bb, x, y, xResult, yResult); |
| checkMonotonicity(bb, x, z, xResult, zResult); |
| checkMonotonicity(bb, y, z, yResult, zResult); |
| } |
| } |
| }; |
| |
| // Struct to set up and check liveness analysis lattice and transfer function. |
| struct LivenessChecker { |
| LivenessTransferFunction txfn; |
| FiniteIntPowersetLattice lattice; |
| AnalysisChecker<FiniteIntPowersetLattice, LivenessTransferFunction> checker; |
| LivenessChecker(Function* func, uint64_t latticeElementSeed, Name funcName) |
| : lattice(func->getNumLocals()), checker(lattice, |
| txfn, |
| "FiniteIntPowersetLattice", |
| "LivenessTransferFunction", |
| latticeElementSeed, |
| funcName) {} |
| |
| FiniteIntPowersetLattice::Element getRandomElement(Random& rand) { |
| FiniteIntPowersetLattice::Element result = lattice.getBottom(); |
| |
| // Uses rand to randomly select which members are to be included (i. e. flip |
| // bits in the bitvector). |
| for (size_t i = 0; i < lattice.getSetSize(); ++i) { |
| result.set(i, rand.oneIn(2)); |
| } |
| return result; |
| } |
| |
| // Runs all checks for liveness analysis. |
| void runChecks(CFG& cfg, Random& rand, bool verbose) { |
| FiniteIntPowersetLattice::Element x = getRandomElement(rand); |
| FiniteIntPowersetLattice::Element y = getRandomElement(rand); |
| FiniteIntPowersetLattice::Element z = getRandomElement(rand); |
| |
| if (verbose) { |
| checker.printVerboseFunctionCase(std::cout, x, y, z); |
| } |
| |
| checker.checkTransferFunction(cfg, x, y, z); |
| } |
| }; |
| |
| // Struct to set up and check reaching definitions analysis lattice and transfer |
| // function. |
| struct ReachingDefinitionsChecker { |
| LocalGraph::GetSetsMap getSetsMap; |
| LocalGraph::Locations locations; |
| ReachingDefinitionsTransferFunction txfn; |
| AnalysisChecker<FinitePowersetLattice<LocalSet*>, |
| ReachingDefinitionsTransferFunction> |
| checker; |
| ReachingDefinitionsChecker(Function* func, |
| uint64_t latticeElementSeed, |
| Name funcName) |
| : txfn(func, getSetsMap, locations), |
| checker(txfn.lattice, |
| txfn, |
| "FinitePowersetLattice<LocalSet*>", |
| "ReachingDefinitionsTransferFunction", |
| latticeElementSeed, |
| funcName) {} |
| |
| FinitePowersetLattice<LocalSet*>::Element getRandomElement(Random& rand) { |
| FinitePowersetLattice<LocalSet*>::Element result = txfn.lattice.getBottom(); |
| |
| // Uses rand to randomly select which members are to be included (i. e. flip |
| // bits in the bitvector). |
| for (size_t i = 0; i < txfn.lattice.getSetSize(); ++i) { |
| result.set(i, rand.oneIn(2)); |
| } |
| return result; |
| } |
| |
| // Runs all checks for reaching definitions analysis. |
| void runChecks(CFG& cfg, Random& rand, bool verbose) { |
| FinitePowersetLattice<LocalSet*>::Element x = getRandomElement(rand); |
| FinitePowersetLattice<LocalSet*>::Element y = getRandomElement(rand); |
| FinitePowersetLattice<LocalSet*>::Element z = getRandomElement(rand); |
| |
| if (verbose) { |
| checker.printVerboseFunctionCase(std::cout, x, y, z); |
| } |
| |
| checker.checkTransferFunction(cfg, x, y, z); |
| } |
| }; |
| |
| // Uninteresting implementation details for RandomFullLattice and RandomLattice. |
| |
| RandomFullLattice::Element RandomFullLattice::getBottom() const noexcept { |
| return std::visit( |
| [](const auto& l) -> Element { return ElementImpl{l.getBottom()}; }, |
| (const FullLatticeVariant&)*lattice); |
| } |
| |
| RandomFullLattice::Element RandomFullLattice::getTop() const noexcept { |
| return std::visit( |
| [](const auto& l) -> Element { return ElementImpl{l.getTop()}; }, |
| (const FullLatticeVariant&)*lattice); |
| } |
| |
| // TODO: use std::remove_cvref_t from C++20 instead. |
| template<typename T> using bare = std::remove_cv_t<std::remove_reference_t<T>>; |
| |
| LatticeComparison RandomFullLattice::compare(const Element& a, |
| const Element& b) const noexcept { |
| return std::visit( |
| [](const auto& l, |
| const auto& elemA, |
| const auto& elemB) -> LatticeComparison { |
| using ElemT = typename bare<decltype(l)>::Element; |
| using A = bare<decltype(elemA)>; |
| using B = bare<decltype(elemB)>; |
| if constexpr (std::is_same_v<ElemT, A> && std::is_same_v<ElemT, B>) { |
| return l.compare(elemA, elemB); |
| } |
| WASM_UNREACHABLE("unexpected element types"); |
| }, |
| (const FullLatticeVariant&)*lattice, |
| (const FullLatticeElementVariant&)*a, |
| (const FullLatticeElementVariant&)*b); |
| } |
| |
| bool RandomFullLattice::join(Element& a, const Element& b) const noexcept { |
| return std::visit( |
| [](const auto& l, auto& elemA, const auto& elemB) -> bool { |
| using ElemT = typename bare<decltype(l)>::Element; |
| using A = bare<decltype(elemA)>; |
| using B = bare<decltype(elemB)>; |
| if constexpr (std::is_same_v<ElemT, A> && std::is_same_v<ElemT, B>) { |
| return l.join(elemA, elemB); |
| } |
| WASM_UNREACHABLE("unexpected element types"); |
| }, |
| (const FullLatticeVariant&)*lattice, |
| (FullLatticeElementVariant&)*a, |
| (const FullLatticeElementVariant&)*b); |
| } |
| |
| bool RandomFullLattice::meet(Element& a, const Element& b) const noexcept { |
| return std::visit( |
| [](const auto& l, auto& elemA, const auto& elemB) -> bool { |
| using ElemT = typename bare<decltype(l)>::Element; |
| using A = bare<decltype(elemA)>; |
| using B = bare<decltype(elemB)>; |
| if constexpr (std::is_same_v<ElemT, A> && std::is_same_v<ElemT, B>) { |
| return l.meet(elemA, elemB); |
| } |
| WASM_UNREACHABLE("unexpected element types"); |
| }, |
| (const FullLatticeVariant&)*lattice, |
| (FullLatticeElementVariant&)*a, |
| (const FullLatticeElementVariant&)*b); |
| } |
| |
| RandomLattice::Element RandomLattice::getBottom() const noexcept { |
| return std::visit( |
| [](const auto& l) -> Element { return ElementImpl{l.getBottom()}; }, |
| (const LatticeVariant&)*lattice); |
| } |
| |
| LatticeComparison RandomLattice::compare(const Element& a, |
| const Element& b) const noexcept { |
| return std::visit( |
| [](const auto& l, |
| const auto& elemA, |
| const auto& elemB) -> LatticeComparison { |
| using ElemT = typename bare<decltype(l)>::Element; |
| using A = bare<decltype(elemA)>; |
| using B = bare<decltype(elemB)>; |
| if constexpr (std::is_same_v<ElemT, A> && std::is_same_v<ElemT, B>) { |
| return l.compare(elemA, elemB); |
| } |
| WASM_UNREACHABLE("unexpected element types"); |
| }, |
| (const LatticeVariant&)*lattice, |
| (const LatticeElementVariant&)*a, |
| (const LatticeElementVariant&)*b); |
| } |
| |
| bool RandomLattice::join(Element& a, const Element& b) const noexcept { |
| return std::visit( |
| [](const auto& l, auto& elemA, const auto& elemB) -> bool { |
| using ElemT = typename bare<decltype(l)>::Element; |
| using A = bare<decltype(elemA)>; |
| using B = bare<decltype(elemB)>; |
| if constexpr (std::is_same_v<ElemT, A> && std::is_same_v<ElemT, B>) { |
| return l.join(elemA, elemB); |
| } |
| WASM_UNREACHABLE("unexpected element types"); |
| }, |
| (const LatticeVariant&)*lattice, |
| (LatticeElementVariant&)*a, |
| (const LatticeElementVariant&)*b); |
| } |
| |
| // The main entry point. |
| struct Fuzzer { |
| bool verbose; |
| |
| Fuzzer(bool verbose) : verbose(verbose) {} |
| |
| // Helper function to run per-function tests. latticeElementSeed is used to |
| // generate three lattice elements randomly. It is also used to select which |
| // analysis is to be tested for the function. |
| void runOnFunction(Function* func, uint64_t latticeElementSeed) { |
| RandEngine getFuncRand(latticeElementSeed); |
| |
| // Fewer bytes are needed to generate three random lattices. |
| std::vector<char> funcBytes(128); |
| for (size_t i = 0; i < funcBytes.size(); i += sizeof(uint64_t)) { |
| *(uint64_t*)(funcBytes.data() + i) = getFuncRand(); |
| } |
| |
| Random rand(std::move(funcBytes)); |
| checkLatticeProperties(rand, verbose); |
| |
| CFG cfg = CFG::fromFunction(func); |
| |
| switch (rand.upTo(2)) { |
| case 0: { |
| LivenessChecker livenessChecker(func, latticeElementSeed, func->name); |
| livenessChecker.runChecks(cfg, rand, verbose); |
| break; |
| } |
| case 1: { |
| ReachingDefinitionsChecker reachingDefinitionsChecker( |
| func, latticeElementSeed, func->name); |
| reachingDefinitionsChecker.runChecks(cfg, rand, verbose); |
| break; |
| } |
| } |
| } |
| |
| // Generates a module. The module is used as an input to fuzz transfer |
| // functions as well as randomly generated lattice element states. Lattice |
| // properties are also fuzzed from the randomly generated states. |
| void run(uint64_t seed, |
| uint64_t* latticeElementSeed = nullptr, |
| std::string* funcName = nullptr) { |
| RandEngine getRand(seed); |
| std::cout << "Running with seed " << seed << "\n"; |
| |
| // 4kb of random bytes should be enough for anyone! |
| std::vector<char> bytes(4096); |
| for (size_t i = 0; i < bytes.size(); i += sizeof(uint64_t)) { |
| *(uint64_t*)(bytes.data() + i) = getRand(); |
| } |
| |
| Module testModule; |
| TranslateToFuzzReader reader(testModule, std::move(bytes)); |
| reader.build(); |
| |
| if (verbose) { |
| std::cout << "Generated test module: \n"; |
| std::cout << testModule; |
| std::cout << "\n"; |
| } |
| |
| // If a specific function and lattice element seed is specified, only run |
| // that. |
| if (latticeElementSeed && funcName) { |
| runOnFunction(testModule.getFunction(*funcName), *latticeElementSeed); |
| return; |
| } |
| |
| ModuleUtils::iterDefinedFunctions(testModule, [&](Function* func) { |
| uint64_t funcSeed = getRand(); |
| runOnFunction(func, funcSeed); |
| }); |
| } |
| }; |
| |
| } // namespace wasm |
| |
| int main(int argc, const char* argv[]) { |
| using namespace wasm; |
| |
| const std::string WasmFuzzTypesOption = "wasm-fuzz-lattices options"; |
| |
| Options options("wasm-fuzz-lattices", |
| "Fuzz lattices for reflexivity, transitivity, and " |
| "anti-symmetry, and tranfer functions for monotonicity."); |
| |
| std::optional<uint64_t> seed; |
| options.add("--seed", |
| "", |
| "Run a single workload generated by the given seed", |
| WasmFuzzTypesOption, |
| Options::Arguments::One, |
| [&](Options*, const std::string& arg) { |
| seed = uint64_t(std::stoull(arg)); |
| }); |
| |
| std::optional<uint64_t> latticeElementSeed; |
| options.add("--lattice-element-seed", |
| "", |
| "Seed which generated the lattice elements to be checked.", |
| WasmFuzzTypesOption, |
| Options::Arguments::One, |
| [&](Options*, const std::string& arg) { |
| latticeElementSeed = uint64_t(std::stoull(arg)); |
| }); |
| |
| std::optional<std::string> functionName; |
| options.add( |
| "--function-name", |
| "", |
| "Name of the function in the module generated by --seed to be checked.", |
| WasmFuzzTypesOption, |
| Options::Arguments::One, |
| [&](Options*, const std::string& arg) { functionName = arg; }); |
| |
| bool verbose = false; |
| options.add("--verbose", |
| "-v", |
| "Print extra information", |
| WasmFuzzTypesOption, |
| Options::Arguments::Zero, |
| [&](Options*, const std::string& arg) { verbose = true; }); |
| |
| options.parse(argc, argv); |
| |
| Fuzzer fuzzer{verbose}; |
| if (seed) { |
| if (latticeElementSeed && functionName) { |
| // Run test a single function and lattice element seed. |
| fuzzer.run(*seed, &(*latticeElementSeed), &(*functionName)); |
| } else { |
| // Run just a single workload with the given seed. |
| fuzzer.run(*seed); |
| } |
| } else { |
| // Continuously run workloads with new randomly generated seeds. |
| size_t i = 0; |
| RandEngine nextSeed(getSeed()); |
| while (true) { |
| std::cout << "Iteration " << ++i << "\n"; |
| fuzzer.run(nextSeed()); |
| } |
| } |
| return 0; |
| } |