Skip to content

Commit 9c67021

Browse files
authored
[analysis] Add an abstraction lattice (#8036)
The abstraction lattice is composed of increasingly abstract sub-lattices. Elements of the abstraction lattice are variants that may hold the elements of any of its constituent lattices. When elements from different sub-lattices are compared or joined, the more concrete element is first abstracted into the lattice of the other element. Unrelated elements in the same lattice may be abstracted before they are joined. This choice and the abstraction operation itself are provided by CRTP subclasses of Abstraction. This is an important building block for eventually reconstructing PossibleContents on top of the lattice framework.
1 parent 7b817d6 commit 9c67021

File tree

2 files changed

+356
-0
lines changed

2 files changed

+356
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright 2025 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <array>
18+
#include <tuple>
19+
#include <utility>
20+
#include <variant>
21+
22+
#include "../lattice.h"
23+
#include "support/utilities.h"
24+
25+
#if __cplusplus >= 202002L
26+
#include "analysis/lattices/bool.h"
27+
#endif
28+
29+
#ifndef wasm_analysis_lattices_abstraction_h
30+
#define wasm_analysis_lattices_abstraction_h
31+
32+
namespace wasm::analysis {
33+
34+
// CRTP lattice composed of increasingly abstract sub-lattices. The subclass is
35+
// responsible for providing two method templates. The first abstracts an
36+
// element of one sub-lattice into an element of the next sub-lattice:
37+
//
38+
// template<size_t I, typename E1, typename E2>
39+
// E2 abstract(const E1&) const
40+
//
41+
// The template method should be specialized for each sub-lattice index I, its
42+
// element type E1, and the next element type E2.
43+
//
44+
// The `abstract` method is used to abstract elements of the more specific
45+
// lattice whenever elements from different lattices are compared or joined. It
46+
// may also be used to abstract two joined elements from the same lattice when
47+
// those elements are unrelated and the second method returns true:
48+
//
49+
// template<size_t I, typename E>
50+
// bool shouldAbstract(const E&. const E&) const
51+
//
52+
// shouldAbstract is only queried for unrelated elements. Related elements of
53+
// the same sub-lattice are always joined as normal.
54+
//
55+
// `abstract` should be monotonic. Making its input more general should either
56+
// not change its output or make its output more general.
57+
//
58+
// `shouldAbstract` should return true only when no upper bound of its arguments
59+
// in their original sub-lattice is used. If such an upper bound is used in a
60+
// comparison or join, the operation may fail to uphold the properties of a
61+
// lattice.
62+
template<typename Self, typename... Ls> struct Abstraction {
63+
using Element = std::variant<typename Ls::Element...>;
64+
65+
std::tuple<Ls...> lattices;
66+
67+
Abstraction(Ls&&... lattices) : lattices({std::move(lattices)...}) {}
68+
69+
Element getBottom() const noexcept {
70+
return std::get<0>(lattices).getBottom();
71+
}
72+
73+
LatticeComparison compare(const Element& a, const Element& b) const noexcept {
74+
if (a.index() < b.index()) {
75+
auto abstractedA = a;
76+
abstractToIndex(abstractedA, b.index());
77+
switch (compare()[b.index()](lattices, abstractedA, b)) {
78+
case EQUAL:
79+
case LESS:
80+
return LESS;
81+
case NO_RELATION:
82+
case GREATER:
83+
return NO_RELATION;
84+
}
85+
WASM_UNREACHABLE("unexpected comparison");
86+
}
87+
if (a.index() > b.index()) {
88+
auto abstractedB = b;
89+
abstractToIndex(abstractedB, a.index());
90+
switch (compare()[a.index()](lattices, a, abstractedB)) {
91+
case EQUAL:
92+
case GREATER:
93+
return GREATER;
94+
case NO_RELATION:
95+
case LESS:
96+
return NO_RELATION;
97+
}
98+
WASM_UNREACHABLE("unexpected comparison");
99+
}
100+
return compare()[a.index()](lattices, a, b);
101+
}
102+
103+
bool join(Element& joinee, const Element& _joiner) const noexcept {
104+
Element joiner = _joiner;
105+
bool changed = false;
106+
if (joinee.index() < joiner.index()) {
107+
abstractToIndex(joinee, joiner.index());
108+
changed = true;
109+
} else if (joinee.index() > joiner.index()) {
110+
abstractToIndex(joiner, joinee.index());
111+
}
112+
while (true) {
113+
assert(joinee.index() == joiner.index());
114+
if (joiner.index() == sizeof...(Ls) - 1) {
115+
// Cannot abstract further, so we must join no matter what.
116+
break;
117+
}
118+
switch (compare()[joiner.index()](lattices, joinee, joiner)) {
119+
case NO_RELATION:
120+
if (shouldAbstract()[joiner.index()](self(), joinee, joiner)) {
121+
// Try abstracting further.
122+
joinee = abstract()[joinee.index()](self(), joinee);
123+
joiner = abstract()[joiner.index()](self(), joiner);
124+
changed = true;
125+
continue;
126+
}
127+
break;
128+
case EQUAL:
129+
case LESS:
130+
case GREATER:
131+
break;
132+
}
133+
break;
134+
}
135+
return join()[joiner.index()](lattices, joinee, joiner) || changed;
136+
}
137+
138+
private:
139+
const Self& self() const noexcept { return *static_cast<const Self*>(this); }
140+
141+
// TODO: Use C++26 pack indexing.
142+
template<std::size_t I> using L = std::tuple_element_t<I, std::tuple<Ls...>>;
143+
144+
// Compute tables of functions that forward operations to the CRTP subtype or
145+
// the lattices. These tables map the dynamic variant indices to compile-time
146+
// lattice indices.
147+
148+
template<std::size_t... I>
149+
static constexpr auto makeAbstractFuncs(std::index_sequence<I...>) noexcept {
150+
using F = Element (*)(const Self&, const Element& elem);
151+
return std::array<F, sizeof...(I)>{
152+
[](const Self& self, const Element& elem) -> Element {
153+
if constexpr (I < sizeof...(Ls) - 1) {
154+
using E1 = typename L<I>::Element;
155+
using E2 = typename L<I + 1>::Element;
156+
return Element(std::in_place_index_t<I + 1>{},
157+
self.template abstract<I, E1, E2>(std::get<I>(elem)));
158+
} else {
159+
WASM_UNREACHABLE("unexpected abstraction");
160+
}
161+
}...};
162+
}
163+
static constexpr auto abstract() noexcept {
164+
return makeAbstractFuncs(std::make_index_sequence<sizeof...(Ls)>{});
165+
}
166+
167+
void abstractToIndex(Element& elem, std::size_t index) const noexcept {
168+
while (elem.index() < index) {
169+
elem = abstract()[elem.index()](self(), elem);
170+
}
171+
}
172+
173+
template<std::size_t... I>
174+
static constexpr auto
175+
makeShouldAbstractFuncs(std::index_sequence<I...>) noexcept {
176+
using F = bool (*)(const Self&, const Element&, const Element&);
177+
return std::array<F, sizeof...(I)>{
178+
[](const Self& self, const Element& a, const Element& b) -> bool {
179+
if constexpr (I < sizeof...(Ls) - 1) {
180+
return self.template shouldAbstract<I>(std::get<I>(a),
181+
std::get<I>(b));
182+
} else {
183+
WASM_UNREACHABLE("unexpected abstraction check");
184+
}
185+
}...};
186+
}
187+
static constexpr auto shouldAbstract() noexcept {
188+
return makeShouldAbstractFuncs(std::make_index_sequence<sizeof...(Ls)>{});
189+
}
190+
191+
template<std::size_t... I>
192+
static constexpr auto makeCompareFuncs(std::index_sequence<I...>) noexcept {
193+
using F = LatticeComparison (*)(
194+
const std::tuple<Ls...>&, const Element&, const Element&);
195+
return std::array<F, sizeof...(I)>{
196+
[](const std::tuple<Ls...>& lattices,
197+
const Element& a,
198+
const Element& b) -> LatticeComparison {
199+
return std::get<I>(lattices).compare(std::get<I>(a), std::get<I>(b));
200+
}...};
201+
}
202+
static constexpr auto compare() noexcept {
203+
return makeCompareFuncs(std::make_index_sequence<sizeof...(Ls)>{});
204+
}
205+
206+
template<std::size_t... I>
207+
static constexpr auto makeJoinFuncs(std::index_sequence<I...>) noexcept {
208+
using F = bool (*)(const std::tuple<Ls...>&, Element&, const Element&);
209+
return std::array<F, sizeof...(I)>{[](const std::tuple<Ls...>& lattices,
210+
Element& joinee,
211+
const Element& joiner) {
212+
return std::get<I>(lattices).join(std::get<I>(joinee),
213+
std::get<I>(joiner));
214+
}...};
215+
}
216+
static constexpr auto join() noexcept {
217+
return makeJoinFuncs(std::make_index_sequence<sizeof...(Ls)>{});
218+
}
219+
};
220+
221+
#if __cplusplus >= 202002L
222+
static_assert(Lattice<Abstraction<Bool, Bool, Bool>>);
223+
#endif
224+
225+
} // namespace wasm::analysis
226+
227+
#endif // wasm_analysis_lattices_abstraction_h

test/gtest/lattices.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
#include "analysis/lattice.h"
18+
#include "analysis/lattices/abstraction.h"
1719
#include "analysis/lattices/array.h"
1820
#include "analysis/lattices/bool.h"
1921
#include "analysis/lattices/flat.h"
@@ -723,3 +725,130 @@ TEST(StackLattice, Join) {
723725
{flat.get(0), flat.get(1)},
724726
{flat.get(0), flat.getTop()});
725727
}
728+
729+
using OddEvenInt = analysis::Flat<uint32_t>;
730+
using OddEvenBool = analysis::Flat<bool>;
731+
struct OddEvenAbstraction
732+
: analysis::Abstraction<OddEvenAbstraction, OddEvenInt, OddEvenBool> {
733+
OddEvenAbstraction()
734+
: analysis::Abstraction<OddEvenAbstraction, OddEvenInt, OddEvenBool>(
735+
OddEvenInt{}, OddEvenBool{}) {}
736+
737+
template<size_t I, typename E1, typename E2> E2 abstract(const E1&) const;
738+
739+
template<std::size_t I, typename E>
740+
bool shouldAbstract(const E&, const E&) const;
741+
};
742+
743+
template<>
744+
OddEvenBool::Element
745+
OddEvenAbstraction::abstract<0>(const OddEvenInt::Element& elem) const {
746+
if (elem.isTop()) {
747+
return OddEvenBool{}.getTop();
748+
}
749+
if (elem.isBottom()) {
750+
return OddEvenBool{}.getBottom();
751+
}
752+
return OddEvenBool{}.get((*elem.getVal() & 1) == 0);
753+
}
754+
755+
template<>
756+
bool OddEvenAbstraction::shouldAbstract<0>(const OddEvenInt::Element&,
757+
const OddEvenInt::Element&) const {
758+
// Since the elements are not related, they must be different integers.
759+
// Always abstract them.
760+
return true;
761+
}
762+
763+
TEST(AbstractionLattice, GetBottom) {
764+
OddEvenAbstraction abstraction;
765+
auto expected = OddEvenAbstraction::Element(OddEvenInt{}.getBottom());
766+
EXPECT_EQ(abstraction.getBottom(), expected);
767+
}
768+
769+
TEST(AbstractionLattice, Join) {
770+
OddEvenAbstraction abstraction;
771+
772+
auto expectJoin = [&](const char* file,
773+
int line,
774+
const auto& joinee,
775+
const auto& joiner,
776+
const auto& expected) {
777+
testing::ScopedTrace trace(file, line, "");
778+
switch (abstraction.compare(joinee, joiner)) {
779+
case analysis::NO_RELATION:
780+
EXPECT_NE(joinee, joiner);
781+
EXPECT_EQ(abstraction.compare(joiner, joinee), analysis::NO_RELATION);
782+
EXPECT_EQ(abstraction.compare(joinee, expected), analysis::LESS);
783+
EXPECT_EQ(abstraction.compare(joiner, expected), analysis::LESS);
784+
break;
785+
case analysis::EQUAL:
786+
EXPECT_EQ(joinee, joiner);
787+
EXPECT_EQ(abstraction.compare(joiner, joinee), analysis::EQUAL);
788+
EXPECT_EQ(abstraction.compare(joinee, expected), analysis::EQUAL);
789+
EXPECT_EQ(abstraction.compare(joiner, expected), analysis::EQUAL);
790+
break;
791+
case analysis::LESS:
792+
EXPECT_EQ(joiner, expected);
793+
EXPECT_EQ(abstraction.compare(joiner, joinee), analysis::GREATER);
794+
EXPECT_EQ(abstraction.compare(joinee, expected), analysis::LESS);
795+
EXPECT_EQ(abstraction.compare(joiner, expected), analysis::EQUAL);
796+
break;
797+
case analysis::GREATER:
798+
EXPECT_EQ(joinee, expected);
799+
EXPECT_EQ(abstraction.compare(joiner, joinee), analysis::LESS);
800+
EXPECT_EQ(abstraction.compare(joinee, expected), analysis::EQUAL);
801+
EXPECT_EQ(abstraction.compare(joiner, expected), analysis::LESS);
802+
}
803+
{
804+
auto copy = joinee;
805+
EXPECT_EQ(abstraction.join(copy, joiner), joinee != expected);
806+
EXPECT_EQ(copy, expected);
807+
}
808+
{
809+
auto copy = joiner;
810+
EXPECT_EQ(abstraction.join(copy, joinee), joiner != expected);
811+
EXPECT_EQ(copy, expected);
812+
}
813+
};
814+
815+
#define JOIN(a, b, c) expectJoin(__FILE__, __LINE__, a, b, c)
816+
817+
auto bot = abstraction.getBottom();
818+
auto one = OddEvenAbstraction::Element(OddEvenInt{}.get(1));
819+
auto two = OddEvenAbstraction::Element(OddEvenInt{}.get(2));
820+
auto three = OddEvenAbstraction::Element(OddEvenInt{}.get(3));
821+
auto four = OddEvenAbstraction::Element(OddEvenInt{}.get(4));
822+
auto even = OddEvenAbstraction::Element(OddEvenBool{}.get(true));
823+
auto odd = OddEvenAbstraction::Element(OddEvenBool{}.get(false));
824+
auto top = OddEvenAbstraction::Element(OddEvenBool{}.getTop());
825+
826+
JOIN(bot, bot, bot);
827+
JOIN(bot, one, one);
828+
JOIN(bot, two, two);
829+
JOIN(bot, even, even);
830+
JOIN(bot, odd, odd);
831+
JOIN(bot, top, top);
832+
833+
JOIN(one, one, one);
834+
JOIN(one, two, top);
835+
JOIN(one, three, odd);
836+
JOIN(one, even, top);
837+
JOIN(one, odd, odd);
838+
839+
JOIN(two, two, two);
840+
JOIN(two, three, top);
841+
JOIN(two, four, even);
842+
JOIN(two, even, even);
843+
JOIN(two, odd, top);
844+
JOIN(two, top, top);
845+
846+
JOIN(even, even, even);
847+
JOIN(even, odd, top);
848+
JOIN(even, top, top);
849+
850+
JOIN(odd, odd, odd);
851+
JOIN(odd, top, top);
852+
853+
#undef JOIN
854+
}

0 commit comments

Comments
 (0)