Skip to content

[analysis] Add a "Shared" lattice to represent shared state #6067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src/analysis/lattices/shared.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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.
*/

#ifndef wasm_analysis_lattices_shared_h
#define wasm_analysis_lattices_shared_h

#include <cstdint>
#include <utility>

#include "../lattice.h"
#include "bool.h"

namespace wasm::analysis {

// A lattice whose elements are a single ascending chain in lattice `L`.
// Internally, there is only ever a single monotonically increasing element of L
// materialized. Dereferencing any element of the Shared lattice will produce
// the current value of that single element of L, which is generally safe
// because the current value always overapproximates (i.e. is higher in the
// lattice than) the value at the time of the Shared element's construction.
//
// Each element of this lattice maintains a sequence number that corresponds to
// a value the shared underlying element has had at some point in time. Higher
// sequence numbers correspond to greater values of the underlying element.
// Elements of this lattice are compared and joined using these sequence
// numbers, so blocks will correctly be re-analyzed if the value has increased
// since the last time they were analyzed.
template<Lattice L> struct Shared {
// If we ever have extremely long-running analyses, this may need to be
// changed to uint64_t.
using Seq = uint32_t;

class Element {
const typename L::Element* val;
Seq seq = 0;

Element(const typename L::Element* val) : val(val) {}

public:
Element() = default;
Element(const Element&) = default;
Element(Element&&) = default;
Element& operator=(const Element&) = default;
Element& operator=(Element&&) = default;

// Only provide const references to the value to force all updates to go
// through our API.
const typename L::Element& operator*() const noexcept { return *val; }
const typename L::Element* operator->() const noexcept { return val; }

bool operator==(const Element& other) const noexcept {
assert(val == other.val);
return seq == other.seq;
}

bool operator!=(const Element& other) const noexcept {
return !(*this == other);
}

friend Shared;
};

L lattice;

// The current value that all elements point to and the current maximum
// sequence number. The sequence numbers monotonically increase along with
// `val` and serve to provide ordering between elements of this lattice. These
// are mutable because they are logically not part of the lattice itself, but
// rather of its elements. They are only stored inside the lattice because it
// is simpler and more efficient than using shared pointers.
mutable typename L::Element val;
mutable Seq seq = 0;

Shared(L&& l) : lattice(std::move(l)), val(lattice.getBottom()) {}

// TODO: Delete the move constructor and the move assignment operator. This
// requires fixing the lattice fuzzer first, since it depends on lattices
// being moveable.

Element getBottom() const noexcept { return Element{&val}; }

LatticeComparison compare(const Element& a, const Element& b) const noexcept {
assert(a.val == b.val);
return a.seq < b.seq ? LESS : a.seq == b.seq ? EQUAL : GREATER;
}

bool join(Element& joinee, const Element& joiner) const noexcept {
assert(joinee.val == joiner.val);
if (joinee.seq < joiner.seq) {
joinee.seq = joiner.seq;
return true;
}
return false;
}

bool join(Element& joinee, const typename L::Element& joiner) const noexcept {
if (lattice.join(val, joiner)) {
// We have moved to the next value in our ascending chain. Assign it a new
// sequence number and update joinee with that sequence number.
joinee.seq = ++seq;
return true;
}
return false;
}
};

#if __cplusplus >= 202002L
static_assert(Lattice<Shared<Bool>>);
#endif // __cplusplus >= 202002L

} // namespace wasm::analysis

#endif // wasm_analysis_lattices_shared_h
23 changes: 20 additions & 3 deletions src/tools/wasm-fuzz-lattices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#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"
Expand Down Expand Up @@ -179,15 +180,17 @@ struct RandomLattice::L : std::variant<RandomFullLattice,
Lift<RandomLattice>,
ArrayLattice,
Vector<RandomLattice>,
TupleLattice> {};
TupleLattice,
Shared<RandomLattice>> {};

struct RandomLattice::ElementImpl
: 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 TupleLattice::Element,
typename Shared<RandomLattice>::Element> {};

constexpr int FullLatticePicks = 7;

Expand Down Expand Up @@ -230,7 +233,7 @@ RandomFullLattice::RandomFullLattice(Random& rand,

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 + 5);
uint32_t pick = rand.upTo(FullLatticePicks + 6);

if (pick < FullLatticePicks) {
lattice = std::make_unique<L>(L{RandomFullLattice{rand, depth, pick}});
Expand All @@ -256,6 +259,9 @@ RandomLattice::RandomLattice(Random& rand, size_t depth) : rand(rand) {
lattice = std::make_unique<L>(L{TupleLattice{
RandomLattice{rand, depth + 1}, RandomLattice{rand, depth + 1}}});
return;
case FullLatticePicks + 5:
lattice = std::make_unique<L>(L{Shared{RandomLattice{rand, depth + 1}}});
return;
}
WASM_UNREACHABLE("unexpected pick");
}
Expand Down Expand Up @@ -358,6 +364,11 @@ RandomLattice::Element RandomLattice::makeElement() const noexcept {
typename TupleLattice::Element{std::get<0>(l->lattices).makeElement(),
std::get<1>(l->lattices).makeElement()}};
}
if (const auto* l = std::get_if<Shared<RandomLattice>>(lattice.get())) {
auto elem = l->getBottom();
l->join(elem, l->lattice.makeElement());
return ElementImpl{elem};
}
WASM_UNREACHABLE("unexpected lattice");
}

Expand Down Expand Up @@ -466,6 +477,12 @@ void printElement(std::ostream& os,
printElement(os, second, depth + 1);
indent(os, depth);
os << ")\n";
} else if (const auto* e =
std::get_if<typename Shared<RandomLattice>::Element>(&*elem)) {
os << "Shared(\n";
printElement(os, **e, depth + 1);
indent(os, depth);
os << ")\n";
} else {
WASM_UNREACHABLE("unexpected element");
}
Expand Down
108 changes: 108 additions & 0 deletions test/gtest/lattices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "analysis/lattices/int.h"
#include "analysis/lattices/inverted.h"
#include "analysis/lattices/lift.h"
#include "analysis/lattices/shared.h"
#include "analysis/lattices/tuple.h"
#include "analysis/lattices/valtype.h"
#include "analysis/lattices/vector.h"
Expand Down Expand Up @@ -546,3 +547,110 @@ TEST(ValTypeLattice, Meet) {
Type(HeapType::array, Nullable),
Type(HeapType::eq, Nullable));
}

TEST(SharedLattice, GetBottom) {
analysis::Shared<analysis::UInt32> shared{analysis::UInt32{}};
EXPECT_EQ(*shared.getBottom(), 0u);
}

TEST(SharedLattice, Compare) {
analysis::Shared<analysis::UInt32> shared{analysis::UInt32{}};

auto zero = shared.getBottom();

auto one = shared.getBottom();
shared.join(one, 1);

// This join will not change the value.
auto uno = one;
shared.join(uno, 1);

auto two = shared.getBottom();
shared.join(two, 2);

EXPECT_EQ(shared.compare(zero, zero), analysis::EQUAL);
EXPECT_EQ(shared.compare(zero, one), analysis::LESS);
EXPECT_EQ(shared.compare(zero, uno), analysis::LESS);
EXPECT_EQ(shared.compare(zero, two), analysis::LESS);

EXPECT_EQ(shared.compare(one, zero), analysis::GREATER);
EXPECT_EQ(shared.compare(one, one), analysis::EQUAL);
EXPECT_EQ(shared.compare(one, uno), analysis::EQUAL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I follow, uno had an extra join done on it, but it was of the same value as we had before, so the sequence number was not increased, hence it is equal to one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right 👍

EXPECT_EQ(shared.compare(one, two), analysis::LESS);

EXPECT_EQ(shared.compare(two, zero), analysis::GREATER);
EXPECT_EQ(shared.compare(two, one), analysis::GREATER);
EXPECT_EQ(shared.compare(two, uno), analysis::GREATER);
EXPECT_EQ(shared.compare(two, two), analysis::EQUAL);

EXPECT_EQ(*zero, 2u);
EXPECT_EQ(*one, 2u);
EXPECT_EQ(*uno, 2u);
EXPECT_EQ(*two, 2u);
}

TEST(SharedLattice, Join) {
analysis::Shared<analysis::UInt32> shared{analysis::UInt32{}};

auto zero = shared.getBottom();

auto one = shared.getBottom();
shared.join(one, 1);

auto two = shared.getBottom();
shared.join(two, 2);

{
auto elem = zero;
EXPECT_FALSE(shared.join(elem, zero));
EXPECT_EQ(elem, zero);
}

{
auto elem = zero;
EXPECT_TRUE(shared.join(elem, one));
EXPECT_EQ(elem, one);
}

{
auto elem = zero;
EXPECT_TRUE(shared.join(elem, two));
EXPECT_EQ(elem, two);
}

{
auto elem = one;
EXPECT_FALSE(shared.join(elem, zero));
EXPECT_EQ(elem, one);
}

{
auto elem = one;
EXPECT_FALSE(shared.join(elem, one));
EXPECT_EQ(elem, one);
}

{
auto elem = one;
EXPECT_TRUE(shared.join(elem, two));
EXPECT_EQ(elem, two);
}

{
auto elem = two;
EXPECT_FALSE(shared.join(elem, zero));
EXPECT_EQ(elem, two);
}

{
auto elem = two;
EXPECT_FALSE(shared.join(elem, one));
EXPECT_EQ(elem, two);
}

{
auto elem = two;
EXPECT_FALSE(shared.join(elem, two));
EXPECT_EQ(elem, two);
}
}