FE 0.15.0
A C++23 toolkit for writing compiler/interpreter frontends.
Loading...
Searching...
No Matches
sym.h
Go to the documentation of this file.
1#pragma once
2
3#include <cassert>
4#include <concepts>
5
6#include <array>
7#include <bit>
8#include <iosfwd>
9#include <optional>
10#include <string>
11
12#ifdef FE_ABSL
13# include <absl/container/flat_hash_map.h>
14# include <absl/container/flat_hash_set.h>
15# include <absl/hash/hash.h>
16#else
17# include <unordered_map>
18# include <unordered_set>
19#endif
20
21#include "fe/arena.h"
22#include "fe/hash.h"
23
24namespace fe {
25
26/// A Sym%bol just wraps a pointer to Sym::String, so pass Sym itself around as value.
27/// Retrieving a `std::string_view` (via Sym::view) is basically free.
28/// You can also obtain a `std::string` (via Sym::str), but this involves a copy.
29/// The characters are *not* null-terminated.
30/// With the exception of the empty string, you should only create Sym%bols via SymPool::sym.
31/// This in turn will toss all Sym%bols into a big hash set.
32/// This makes Sym::operator== and Sym::operator!= an O(1) operation.
33/// The empty string is internally handled as `nullptr`.
34/// Thus, you can create a Sym%bol representing an empty string without having access to the SymPool.
35/// @note The empty `std::string`/`std::string_view` and `nullptr` are all identified as Sym::Sym().
36/// @warning Big endian version has not been tested.
37class Sym {
38public:
39 static constexpr size_t Short_String_Bytes = sizeof(uintptr_t);
40 static constexpr size_t Short_String_Mask = Short_String_Bytes - 1;
41
42 struct String {
43 constexpr String() noexcept = default;
44 constexpr String(size_t size) noexcept
45 : size(size) {}
46
47 size_t size = 0;
48 char chars[]; // This is actually a C-only feature, but all C++ compilers support that anyway.
49
50 struct Equal {
51 constexpr bool operator()(const String* s1, const String* s2) const noexcept {
52 bool res = s1->size == s2->size;
53 for (size_t i = 0, e = s1->size; res && i != e; ++i)
54 res &= s1->chars[i] == s2->chars[i];
55 return res;
56 }
57 };
58
59 /// Hashes the characters, not the pointer - String::Equal compares them, and the two have to agree.
60 struct Hash {
61 size_t operator()(const String* s) const noexcept {
62 auto sv = std::string_view(s->chars, s->size);
63#ifdef FE_ABSL
64 return absl::HashOf(sv); // a few percent ahead of ours, and we link it anyway
65#else
66 return hash_begin(sv);
67#endif
68 }
69 };
70 };
71
72 static_assert(sizeof(String) == sizeof(size_t), "String.chars should be 0");
73
74private:
75 constexpr Sym(uintptr_t ptr) noexcept
76 : ptr_(ptr) {}
77
78public:
79 constexpr Sym() noexcept = default;
80
81 /// @name Getters
82 ///@{
83 [[nodiscard]] constexpr bool empty() const noexcept { return ptr_ == 0; }
84 [[nodiscard]] constexpr size_t size() const noexcept {
85 if (empty()) return 0;
86 if (auto size = ptr_ & Short_String_Mask) return size;
87 return ((const String*)ptr_)->size;
88 }
89 [[nodiscard]] constexpr uintptr_t raw() const noexcept { return ptr_; }
90 ///@}
91
92 /// @name Access
93 ///@{
94 constexpr char operator[](size_t i) const noexcept {
95 assert(i < size());
96 return view()[i];
97 }
98 constexpr char front() const noexcept { return (*this)[0]; }
99 constexpr char back() const noexcept { return (*this)[size() - 1]; }
100 ///@}
101
102 /// @name Iterators
103 ///@{
104 constexpr auto begin() const noexcept { return view().data(); }
105 constexpr auto end() const noexcept { return begin() + size(); }
106 constexpr auto cbegin() const noexcept { return begin(); }
107 constexpr auto cend() const noexcept { return end(); }
108 constexpr auto rbegin() const noexcept { return std::reverse_iterator(end()); }
109 constexpr auto rend() const noexcept { return std::reverse_iterator(begin()); }
110 constexpr auto crbegin() const noexcept { return rbegin(); }
111 constexpr auto crend() const noexcept { return rend(); }
112 ///@}
113
114 /// @name Comparison: Sym w/ Sym
115 ///@{
116 friend constexpr auto operator<=>(Sym s1, Sym s2) noexcept { return s1.view() <=> s2.view(); }
117 friend constexpr bool operator==(Sym s1, Sym s2) noexcept { return s1.ptr_ == s2.ptr_; }
118 ///@}
119
120 /// @name Comparison: Sym w/ char
121 ///@{
122 friend constexpr std::strong_ordering operator<=>(Sym s, char c) noexcept { return cmp<false>(s, c); }
123 friend constexpr std::strong_ordering operator<=>(char c, Sym s) noexcept { return cmp<true>(s, c); }
124 friend constexpr bool operator==(Sym s, char c) noexcept { return (s.size() == 1) && (s[0] == c); }
125 friend constexpr bool operator==(char c, Sym s) noexcept { return (s.size() == 1) && (s[0] == c); }
126 ///@}
127
128 /// @name Comparison: Sym w/ convertible to std::string_view
129 ///@{
130 friend constexpr auto operator<=>(Sym lhs, const std::convertible_to<std::string_view> auto& rhs) noexcept {
131 return lhs.view() <=> std::string_view(rhs);
132 }
133 friend constexpr auto operator<=>(const std::convertible_to<std::string_view> auto& lhs, Sym rhs) noexcept {
134 return std::string_view(lhs) <=> rhs.view();
135 }
136
137 friend constexpr bool operator==(Sym lhs, const std::convertible_to<std::string_view> auto& rhs) noexcept {
138 return lhs.view() == std::string_view(rhs);
139 }
140
141 friend constexpr bool operator==(const std::convertible_to<std::string_view> auto& lhs, Sym rhs) noexcept {
142 return std::string_view(lhs) == rhs.view();
143 }
144 ///@}
145
146 /// @name Conversions
147 ///@{
148 /// @warning For a *short* Sym%bol (size < Sym::Short_String_Bytes) the characters are stored inline within this
149 /// object's `ptr_`, so the returned view points into *this* Sym%bol and is only valid as long as it lives - it
150 /// dangles for a temporary (e.g. `pool.sym("ab").view()`). Use Sym::str if the string must outlive the Sym%bol.
151 [[nodiscard]] constexpr std::string_view view() const noexcept {
152 if (empty()) return {std::bit_cast<const char*>(&ptr_), 0};
153 // Little endian: 2 a b ? register: ?ba2
154 // Big endian: a b ? 2 register: ab?2
155 uintptr_t offset = std::endian::native == std::endian::little ? 1 : 0;
156 if (auto size = ptr_ & Short_String_Mask) return {std::bit_cast<const char*>(&ptr_) + offset, size};
157 auto S = std::bit_cast<const String*>(ptr_);
158 return std::string_view(S->chars, S->size);
159 }
160 constexpr operator std::string_view() const noexcept { return view(); }
161 constexpr std::string_view operator*() const noexcept { return view(); }
162 // Unfortunately, this doesn't work:
163 // std::string_view operator->() const { return view(); }
164
165 constexpr std::string str() const { return std::string(view()); } ///< This involves a copy.
166 constexpr explicit operator std::string() const { return str(); } ///< `explicit` as this involves a copy.
167 constexpr explicit operator bool() const noexcept { return ptr_; } ///< Is not empty?
168 ///@}
169
170 template<class H>
171 friend constexpr H AbslHashValue(H h, Sym sym) noexcept {
172 return H::combine(std::move(h), sym.ptr_);
173 }
174 friend struct ::std::hash<fe::Sym>;
175 friend std::ostream& operator<<(std::ostream& os, Sym sym);
176
177 /// @name Hash/Eq for hash tables.
178 /// Both work on the interned pointer, so they are O(1).
179 /// @note There is deliberately *no* heterogeneous (`std::string_view`) lookup:
180 /// a content-based hash would be inconsistent with the pointer-based one.
181 /// Intern via SymPool::sym first, then look up with the resulting Sym.
182 ///@{
183 struct Hash {
184 size_t operator()(Sym s) const noexcept { return fe::hash(s.ptr_); }
185 };
186
187 struct Eq {
188 constexpr bool operator()(Sym a, Sym b) const noexcept { return a.ptr_ == b.ptr_; }
189 };
190 ///@}
191
192private:
193 template<bool Rev>
194 static constexpr std::strong_ordering cmp(Sym s, char c) noexcept {
195 const auto n = s.size();
196 if (n == 0) return Rev ? std::strong_ordering::greater : std::strong_ordering::less;
197
198 auto cmp = s[0] <=> c;
199 if (cmp != 0) return cmp;
200
201 return (n == 1) ? std::strong_ordering::equal
202 : (Rev ? std::strong_ordering::less : std::strong_ordering::greater);
203 }
204
205 // Little endian: 2 a b ? register: ?ba2
206 // Big endian: a b ? 2 register: ab?2
207 uintptr_t ptr_ = 0;
208
209 friend class SymPool;
210};
211
212#ifndef DOXYGEN
213} // namespace fe
214
215template<>
216struct std::hash<fe::Sym> {
217 size_t operator()(fe::Sym sym) const noexcept { return fe::hash(sym.ptr_); }
218};
219
220namespace fe {
221#endif
222
223/// @name SymMap/SymSet
224/// Set/Map is keyed by pointer - which is hashed in SymPool.
225///@{
226///
227#ifdef FE_ABSL
228template<class V>
229using SymMap = absl::flat_hash_map<Sym, V, Sym::Hash, Sym::Eq>;
230using SymSet = absl::flat_hash_set<Sym, Sym::Hash, Sym::Eq>;
231#else
232template<class V>
233using SymMap = std::unordered_map<Sym, V, Sym::Hash, Sym::Eq>;
234using SymSet = std::unordered_set<Sym, Sym::Hash, Sym::Eq>;
235#endif
236///@}
237
238/// A fixed-capacity Sym%bol -> @p V map for a *closed* set of @p Size entries: filled once, then only
239/// looked up - a Lexer's reserved words, say.
240/// Prefer this over SymMap for that use: both hash and compare the same interned pointer, but SymMap
241/// pays a full finalizer and a SwissTable group probe where this pays one multiply and one probe of
242/// a table whose capacity is a compile-time constant.
243/// @note @p V has to be default-constructible; an absent key yields no @p V at all - see SymTab::find.
244/// @warning Insert-only: there is no erase, and SymTab::emplace asserts on a key already present.
245template<class V, size_t Size>
246class SymTab {
247public:
248 /// Twice @p Size, rounded up to a power of two, so the load factor stays below `1/2`.
249 static constexpr size_t Capacity = std::bit_ceil(2 * Size);
250
251 /// @name Access
252 ///@{
253 void emplace(Sym sym, V v) {
254 assert(!sym.empty() && "the empty Sym%bol marks a free slot");
255 for (auto i = idx(sym);; i = (i + 1) & Mask) {
256 assert(slots_[i].sym != sym && "already present");
257 if (slots_[i].sym.empty()) {
258 slots_[i] = {sym, std::move(v)};
259 return;
260 }
261 }
262 }
263
264 /// Yields nothing if @p sym is not present - the empty Sym%bol never is.
265 /// @note The free-slot test comes first: the empty Sym%bol *is* a free slot's key.
266 std::optional<V> find(Sym sym) const {
267 for (auto i = idx(sym);; i = (i + 1) & Mask) {
268 if (slots_[i].sym.empty()) return {};
269 if (slots_[i].sym == sym) return slots_[i].v;
270 }
271 }
272
273 bool contains(Sym sym) const { return (bool)find(sym); }
274 ///@}
275
276private:
277 static constexpr size_t Mask = Capacity - 1;
278 static constexpr size_t Shift = 64 - std::bit_width(Mask);
279 static constexpr uint64_t Magic = 0x9E3779B97F4A7C15ull; ///< `2^64/phi` - Fibonacci hashing.
280
281 /// Takes the *high* bits of the product: a long Sym%bol is an 8-byte aligned pointer and a short
282 /// one's low byte is a size of `1 .. Sym::Short_String_Bytes-1`, so the low bits cluster.
283 static size_t idx(Sym sym) { return size_t((uint64_t(sym.raw()) * Magic) >> Shift); }
284
285 struct Slot {
286 Sym sym;
287 V v = {};
288 };
289
290 std::array<Slot, Capacity> slots_ = {};
291};
292
293/// Hash set where all strings - wrapped in Sym%bol - live in.
294/// You can access the SymPool from Driver.
295class SymPool {
296public:
298
299 /// @name Constructor & Destruction
300 ///@{
301 SymPool(const SymPool&) = delete;
302 SymPool() noexcept {}
303 SymPool(SymPool&& other) noexcept
304 : SymPool() {
305 swap(*this, other);
306 }
308 ///@}
309
310 /// @name sym
311 ///@{
312 Sym sym(std::string_view s);
313 Sym sym(const std::string& s) { return sym((std::string_view)s); }
314 /// @p s is a null-terminated C-string.
315 Sym sym(const char* s) { return s == nullptr ? Sym() : sym(std::string_view(s)); }
316 // TODO we can try to fit s in current page and hence eliminate the explicit use of strlen
317 ///@}
318
319 friend void swap(SymPool& p1, SymPool& p2) noexcept {
320 using std::swap;
321 // clang-format off
322 swap(p1.strings_, p2.strings_);
323 swap(p1.pool_, p2.pool_ );
324 // clang-format on
325 }
326
327private:
328 Arena strings_;
329#ifdef FE_ABSL
330 absl::flat_hash_set<const String*, String::Hash, String::Equal> pool_;
331#else
332 std::unordered_set<const String*, String::Hash, String::Equal> pool_;
333#endif
334};
335
336static_assert(std::is_trivially_copyable_v<Sym>);
337static_assert(sizeof(uintptr_t) == sizeof(void*), "uintptr_t must match pointer size");
338static_assert(std::has_unique_object_representations_v<uintptr_t>);
339static_assert(std::endian::native == std::endian::little || std::endian::native == std::endian::big,
340 "mixed endianness not supported");
341
342} // namespace fe
An arena pre-allocates so-called pages of size Arena::page_size_.
Definition arena.h:25
Sym sym(const char *s)
s is a null-terminated C-string.
Definition sym.h:315
Sym::String String
Definition sym.h:297
Sym sym(std::string_view s)
SymPool & operator=(SymPool)=delete
SymPool(const SymPool &)=delete
Sym sym(const std::string &s)
Definition sym.h:313
SymPool(SymPool &&other) noexcept
Definition sym.h:303
SymPool() noexcept
Definition sym.h:302
friend void swap(SymPool &p1, SymPool &p2) noexcept
Definition sym.h:319
A fixed-capacity Symbol -> V map for a closed set of Size entries: filled once, then only looked up -...
Definition sym.h:246
static constexpr size_t Capacity
Twice Size, rounded up to a power of two, so the load factor stays below 1/2.
Definition sym.h:249
void emplace(Sym sym, V v)
Definition sym.h:253
bool contains(Sym sym) const
Definition sym.h:273
std::optional< V > find(Sym sym) const
Yields nothing if sym is not present - the empty Symbol never is.
Definition sym.h:266
A Symbol just wraps a pointer to Sym::String, so pass Sym itself around as value.
Definition sym.h:37
constexpr uintptr_t raw() const noexcept
Definition sym.h:89
friend constexpr std::strong_ordering operator<=>(char c, Sym s) noexcept
Definition sym.h:123
friend constexpr bool operator==(Sym lhs, const std::convertible_to< std::string_view > auto &rhs) noexcept
Definition sym.h:137
friend constexpr auto operator<=>(Sym s1, Sym s2) noexcept
Definition sym.h:116
constexpr auto rend() const noexcept
Definition sym.h:109
constexpr auto begin() const noexcept
Definition sym.h:104
constexpr char front() const noexcept
Definition sym.h:98
constexpr std::string_view operator*() const noexcept
Definition sym.h:161
constexpr bool empty() const noexcept
Definition sym.h:83
static constexpr size_t Short_String_Mask
Definition sym.h:40
static constexpr size_t Short_String_Bytes
Definition sym.h:39
constexpr auto cend() const noexcept
Definition sym.h:107
friend constexpr bool operator==(char c, Sym s) noexcept
Definition sym.h:125
constexpr size_t size() const noexcept
Definition sym.h:84
constexpr Sym() noexcept=default
constexpr char back() const noexcept
Definition sym.h:99
friend constexpr auto operator<=>(const std::convertible_to< std::string_view > auto &lhs, Sym rhs) noexcept
Definition sym.h:133
friend constexpr bool operator==(Sym s1, Sym s2) noexcept
Definition sym.h:117
friend class SymPool
Definition sym.h:209
constexpr auto crbegin() const noexcept
Definition sym.h:110
constexpr char operator[](size_t i) const noexcept
Definition sym.h:94
friend constexpr bool operator==(Sym s, char c) noexcept
Definition sym.h:124
constexpr auto crend() const noexcept
Definition sym.h:111
friend std::ostream & operator<<(std::ostream &os, Sym sym)
constexpr auto rbegin() const noexcept
Definition sym.h:108
friend constexpr bool operator==(const std::convertible_to< std::string_view > auto &lhs, Sym rhs) noexcept
Definition sym.h:141
friend constexpr H AbslHashValue(H h, Sym sym) noexcept
Definition sym.h:171
constexpr std::string str() const
This involves a copy.
Definition sym.h:165
constexpr auto cbegin() const noexcept
Definition sym.h:106
friend constexpr auto operator<=>(Sym lhs, const std::convertible_to< std::string_view > auto &rhs) noexcept
Definition sym.h:130
constexpr auto end() const noexcept
Definition sym.h:105
constexpr std::string_view view() const noexcept
Definition sym.h:151
friend constexpr std::strong_ordering operator<=>(Sym s, char c) noexcept
Definition sym.h:122
Definition algo.h:17
constexpr size_t hash(size_t h) noexcept
Mixes h with murmur3 or splitmix64 - whichever matches sizeof(size_t).
Definition hash.h:42
std::unordered_map< Sym, V, Sym::Hash, Sym::Eq > SymMap
Definition sym.h:233
constexpr size_t hash_begin() noexcept
Seeds a hash chain with the FNV-1 offset basis.
Definition hash.h:69
std::unordered_set< Sym, Sym::Hash, Sym::Eq > SymSet
Definition sym.h:234
constexpr bool operator()(Sym a, Sym b) const noexcept
Definition sym.h:188
size_t operator()(Sym s) const noexcept
Definition sym.h:184
constexpr bool operator()(const String *s1, const String *s2) const noexcept
Definition sym.h:51
Hashes the characters, not the pointer - String::Equal compares them, and the two have to agree.
Definition sym.h:60
size_t operator()(const String *s) const noexcept
Definition sym.h:61
constexpr String() noexcept=default
char chars[]
Definition sym.h:48
size_t size
Definition sym.h:47