PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
map.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <pineforge/na.hpp>
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <cstring>
15#include <functional>
16#include <iterator>
17#include <limits>
18#include <memory>
19#include <mutex>
20#include <stdexcept>
21#include <string>
22#include <type_traits>
23#include <utility>
24#include <vector>
25
26namespace pineforge {
27
28namespace detail {
29
30template <typename T>
31using PineMapBare = std::remove_cv_t<std::remove_reference_t<T>>;
32
33// A map method called on an na map ID: pine_na_reference. These raise the
34// exception type and English the map always had; out of line and cold, so
35// the methods that check stay as small as they were.
36[[noreturn, gnu::noinline, gnu::cold]] inline void map_na_error(const char* what) {
38 {{"object", "map"}}, what);
39}
40
41// A new key beyond the map's pair cap: pine_runtime_limit.
42[[noreturn, gnu::noinline, gnu::cold]] inline void map_capacity_error(int cap,
43 const char* what) {
45 {{"limit", "map_pairs"}, {"max", cap}}, what);
46}
47
48// A broken key index, an unreachable size or a checkpoint never taken: an
49// engine invariant.
50template <class Base>
51[[noreturn, gnu::noinline, gnu::cold]] void map_invariant_error(const char* what) {
53}
54
55template <typename T>
56inline constexpr bool is_pine_map_key_v =
57 std::is_integral_v<PineMapBare<T>> ||
58 std::is_floating_point_v<PineMapBare<T>> ||
59 std::is_enum_v<PineMapBare<T>> ||
60 std::is_same_v<PineMapBare<T>, std::string> ||
61 std::is_same_v<PineMapBare<T>, ScriptString>;
62
63template <typename T>
64inline constexpr bool is_pine_map_snapshot_value_v =
65 !std::is_reference_v<T> &&
66 (std::is_arithmetic_v<PineMapBare<T>> ||
67 std::is_enum_v<PineMapBare<T>> ||
68 std::is_same_v<PineMapBare<T>, std::string> ||
69 std::is_same_v<PineMapBare<T>, ScriptString>);
70
71template <typename Key, bool = std::is_floating_point_v<PineMapBare<Key>>>
73 bool operator()(const Key& lhs, const Key& rhs) const
74 noexcept(noexcept(lhs == rhs)) {
75 return lhs == rhs;
76 }
77};
78
79template <typename Key>
80struct PineMapKeyEqual<Key, true> {
81 bool operator()(Key lhs, Key rhs) const noexcept {
82 return (std::isnan(lhs) && std::isnan(rhs)) || lhs == rhs;
83 }
84};
85
86template <typename Key, bool = std::is_floating_point_v<PineMapBare<Key>>>
88 std::size_t operator()(const Key& key) const
89 noexcept(noexcept(std::hash<Key>{}(key))) {
90 return std::hash<Key>{}(key);
91 }
92};
93
94template <typename Key>
95struct PineMapKeyHash<Key, true> {
96 std::size_t operator()(Key key) const noexcept {
97 if (std::isnan(key)) {
98 // All Pine na float keys compare as one key, irrespective of the
99 // NaN payload supplied by a consumer.
100 return static_cast<std::size_t>(UINT64_C(0x9e3779b97f4a7c15));
101 }
102 // KeyEqual considers -0.0 and +0.0 equal. Hash the canonical form so
103 // the unordered-map equality/hash contract remains explicit.
104 if (key == static_cast<Key>(0)) key = static_cast<Key>(0);
105 return std::hash<Key>{}(key);
106 }
107};
108
109// ---------------------------------------------------------------------------
110// Map state over the shared O(1) script-state store (include/pineforge/detail/
111// o1_store.hpp). For scalars, strings and enabled external codecs, the entry
112// arena, the hash index and every retained string payload live in store
113// objects, so the resident heap is the store's fixed page window however many
114// pairs or payload bytes the script has put. Only the handle (a few words) and
115// the ids of its two arena objects are heap.
116//
117// Payload types. A string (key or value) of up to kMapInlineBytes is held in
118// its slot; a longer one is one immutable store object, written once and never
119// changed while any map, snapshot or enumeration can still read it. Reference
120// counts live beside the bytes in the Store. A trivially copyable value is
121// held in its slot as bytes. Any other
122// value (a handle to a collection or to another map) is a handle of a few
123// words that the map keeps in a heap table indexed by slot: the payload behind
124// such a handle belongs to its own container. Store objects keep their declared
125// bounds, but materialize data chunks only when a write reaches them; an
126// unmaterialized chunk reads as zero.
127
128inline constexpr const char* kMapStoreTransferError = "map store transfer is short";
129inline constexpr const char* kMapSequenceRangeError = "map sequence index out of range";
130
131// One store plus its backing. Maps created while a holder is current share it;
132// the store is not thread-safe, so every map operation holds the mutex.
135 explicit MapStoreHolder(std::unique_ptr<ScratchBacking> supplied,
136 const o1::Shape& shape = {})
137 : backing(std::move(supplied)),
138 owned_store(o1::Store::open(shape, *backing)),
139 store(owned_store.get()) {}
140 explicit MapStoreHolder(o1::Store& borrowed) noexcept : store(&borrowed) {}
143
144 std::unique_ptr<ScratchBacking> backing; // declared first: destroyed last
145 std::unique_ptr<o1::Store> owned_store;
147 std::recursive_mutex mutex;
148};
149
150inline std::shared_ptr<MapStoreHolder>& map_store_slot() {
151 static thread_local std::shared_ptr<MapStoreHolder> slot;
152 return slot;
153}
154
155// The store a map created now binds to: the one a MapStoreScope made current,
156// else the calling thread's own, opened when the first map needs it and closed
157// with the last map, snapshot or enumeration that still reads it (so a thread
158// that runs one script after another does not carry the spilled bytes of the
159// last into the next).
160inline std::shared_ptr<MapStoreHolder> map_current_store() {
161 if (auto& scoped = map_store_slot()) return scoped;
162 static thread_local std::weak_ptr<MapStoreHolder> ambient;
163 std::shared_ptr<MapStoreHolder> holder = ambient.lock();
164 if (!holder) {
165 holder = std::make_shared<MapStoreHolder>();
166 ambient = holder;
167 }
168 return holder;
169}
170
171// Makes `holder` the store of every map created on this thread while the scope
172// lives, so a run can own the store its script state lives in.
174public:
175 explicit MapStoreScope(std::shared_ptr<MapStoreHolder> holder)
176 : previous_(map_store_slot()) {
177 map_store_slot() = std::move(holder);
178 }
179 MapStoreScope(const MapStoreScope&) = delete;
181 ~MapStoreScope() { map_store_slot() = std::move(previous_); }
182
183private:
184 std::shared_ptr<MapStoreHolder> previous_;
185};
186
187inline constexpr std::size_t kMapInlineBytes = 24;
188
189// A generated external UDT/collection codec can specialize this hook. The
190// representation must contain stable Store ids/scalars only; retain/release
191// own the representation independently of any C++ temporary. Encode/retain/
192// release execute inside the caller's Store transaction. A codec must define
193// its alias identity, recursive checkpoint and canonical hash contract in
194// generated code; merely being trivially copyable does not prove that.
195// Opaque legacy values retain their old shallow-copy semantics in the
196// prepared handle table. Only ScriptString has a shipped codec here.
197template <typename T>
199 static constexpr bool enabled = false;
200 using Rep = char;
201};
202
203template <>
205 static constexpr bool enabled = true;
207 static Rep encode(o1::Store& store, const ScriptString& text) {
208 if (!text.store() || text.store() == &store) return text.pin();
209 ScriptString copy(store, {}, text.size());
210 copy.append(text);
211 return copy.pin();
212 }
213 static ScriptString decode(o1::Store& store, Rep rep) {
214 return ScriptString::from_handle(store, rep);
215 }
216 static void retain(o1::Store& store, Rep rep) {
217 auto value = ScriptString::from_handle(store, rep);
218 (void)value.pin();
219 }
220 static void release(o1::Store& store, Rep rep) { ScriptString::unpin(store, rep); }
221};
222
223// A string as a slot holds it: inline bytes, or the id of the immutable store
224// object that holds all of them.
230static_assert(std::is_trivially_copyable_v<MapStringRep>,
231 "a string slot must be copyable as bytes");
232
233template <typename T>
234inline constexpr bool is_map_string_v = std::is_same_v<PineMapBare<T>, std::string>;
235
236template <typename T>
237inline constexpr bool is_map_inline_v =
239 std::is_trivially_copyable_v<PineMapBare<T>> &&
240 std::is_default_constructible_v<PineMapBare<T>>;
241
242// What a slot stores for a T: the T itself, a MapStringRep, or (a handle kept
243// in the heap table) one unused byte.
244template <typename T>
245using MapRepOf = std::conditional_t<
247 std::conditional_t<MapExternalCodec<PineMapBare<T>>::enabled,
248 typename MapExternalCodec<PineMapBare<T>>::Rep,
249 std::conditional_t<is_map_inline_v<T>, PineMapBare<T>, char>>>;
250
251// A map read cache keeps only fixed-size Store representations. Only positive
252// lookups are cached: retaining absent query strings would make the cache
253// footprint depend on key length. The cache never owns a decoded std::string
254// or handle value, so its footprint does not follow the map population.
255inline constexpr std::size_t kMapReadCacheSlots = 8;
256
257template <typename KeyRep, typename ValueRep, std::size_t Slots = kMapReadCacheSlots>
259 static_assert(Slots != 0, "a map read cache needs at least one slot");
260
261 struct Entry {
262 std::uint64_t hash = 0;
263 std::uint32_t slot = 0;
264 bool valid = false;
265 KeyRep key{};
266 ValueRep value{};
267 };
268 static_assert(std::is_trivially_copyable_v<Entry>,
269 "map read cache entries must remain fixed-size bytes");
270
271 std::array<Entry, Slots> entries{};
272 std::size_t next = 0;
273 std::uint64_t epoch = 0;
274
275 static constexpr std::size_t capacity = Slots;
276
277 void clear() noexcept {
278 for (auto& entry : entries) entry.valid = false;
279 next = 0;
280 }
281
282 // Round-robin replacement makes the cache's memory and replacement rule
283 // independent of the map population. The representations are borrowed
284 // from the live map slot; the map slot owns any external payload.
285 void record(std::uint64_t hash, std::uint32_t slot, const KeyRep& key,
286 const ValueRep& value) noexcept {
287 Entry& entry = entries[next];
288 next = (next + 1) % Slots;
289 entry.valid = false;
290 entry.hash = hash;
291 entry.slot = slot;
292 std::memcpy(&entry.key, &key, sizeof(KeyRep));
293 std::memcpy(&entry.value, &value, sizeof(ValueRep));
294 entry.valid = true;
295 }
296};
297
298[[nodiscard]] inline MapStringRep map_string_encode(o1::Store& store, const std::string& text) {
299 MapStringRep rep{};
300 rep.length = text.size();
301 if (text.size() <= kMapInlineBytes) {
302 if (!text.empty()) std::memcpy(rep.inline_bytes, text.data(), text.size());
303 return rep;
304 }
305 const std::uint64_t references = 1;
306 const o1::Id id = store.create(sizeof references + text.size(), 8);
307 if (store.write(id, 0, &references, sizeof references) != sizeof references ||
308 store.write(id, sizeof references, text.data(), text.size()) != text.size()) {
310 }
311 rep.external = id;
312 return rep;
313}
314
315[[nodiscard]] inline std::string map_string_decode(const o1::Store& store,
316 const MapStringRep& rep) {
317 std::string text(static_cast<std::size_t>(rep.length), '\0');
318 if (rep.length <= kMapInlineBytes) {
319 if (rep.length != 0) std::memcpy(&text[0], rep.inline_bytes, text.size());
320 return text;
321 }
322 if (store.read(rep.external, sizeof(std::uint64_t), &text[0], text.size()) != text.size()) {
324 }
325 return text;
326}
327
328[[nodiscard]] inline bool map_string_equals(const o1::Store& store, const MapStringRep& rep,
329 const std::string& text) {
330 if (rep.length != text.size()) return false;
331 if (rep.length <= kMapInlineBytes) {
332 return text.empty() || std::memcmp(rep.inline_bytes, text.data(), text.size()) == 0;
333 }
334 char chunk[512];
335 std::uint64_t done = 0;
336 while (done < rep.length) {
337 const std::size_t want =
338 static_cast<std::size_t>(std::min<std::uint64_t>(sizeof chunk, rep.length - done));
339 if (store.read(rep.external, sizeof(std::uint64_t) + done, chunk, want) != want) {
341 }
342 if (std::memcmp(chunk, text.data() + done, want) != 0) return false;
343 done += want;
344 }
345 return true;
346}
347
348template <typename T>
349[[nodiscard]] MapRepOf<T> map_encode(o1::Store& store, const T& value) {
350 if constexpr (is_map_string_v<T>) {
351 return map_string_encode(store, value);
352 } else if constexpr (MapExternalCodec<PineMapBare<T>>::enabled) {
353 return MapExternalCodec<PineMapBare<T>>::encode(store, value);
354 } else if constexpr (is_map_inline_v<T>) {
355 (void)store;
356 return value;
357 } else {
358 (void)store;
359 (void)value;
360 return MapRepOf<T>{};
361 }
362}
363
364template <typename T>
365[[nodiscard]] T map_decode(o1::Store& store, const MapRepOf<T>& rep,
366 const std::shared_ptr<MapStoreHolder>& owner = {}) {
367 if constexpr (is_map_string_v<T>) {
368 return map_string_decode(store, rep);
369 } else if constexpr (MapExternalCodec<PineMapBare<T>>::enabled) {
370 auto value = MapExternalCodec<PineMapBare<T>>::decode(store, rep);
371 if constexpr (std::is_same_v<PineMapBare<T>, ScriptString>)
372 value.keep_store_alive(owner);
373 return value;
374 } else {
375 (void)store;
376 return rep;
377 }
378}
379
380template <typename T>
381void map_retain(o1::Store& store, const MapRepOf<T>& rep) {
382 if constexpr (is_map_string_v<T>) {
383 if (rep.length <= kMapInlineBytes) return;
384 std::uint64_t references = 0;
385 if (store.read(rep.external, 0, &references, sizeof references) != sizeof references ||
386 references == UINT64_MAX)
388 ++references;
389 if (store.write(rep.external, 0, &references, sizeof references) != sizeof references)
391 } else if constexpr (MapExternalCodec<PineMapBare<T>>::enabled) {
392 MapExternalCodec<PineMapBare<T>>::retain(store, rep);
393 }
394}
395
396template <typename T>
397void map_release(o1::Store& store, const MapRepOf<T>& rep) {
398 if constexpr (is_map_string_v<T>) {
399 if (rep.length <= kMapInlineBytes) return;
400 std::uint64_t references = 0;
401 if (store.read(rep.external, 0, &references, sizeof references) != sizeof references ||
402 references == 0)
404 if (--references == 0) store.release(rep.external);
405 else if (store.write(rep.external, 0, &references, sizeof references) != sizeof references)
407 } else if constexpr (MapExternalCodec<PineMapBare<T>>::enabled) {
409 }
410}
411
412// The first bytes of a map's entry object. Slot links are slot index + 1,
413// 0 meaning none.
414struct MapHeader {
415 std::uint32_t count;
416 std::uint32_t head;
417 std::uint32_t tail;
418 std::uint32_t high_water; // slots ever handed out
419 std::uint32_t free_head;
420 std::uint32_t reserved[3];
421};
422static_assert(sizeof(MapHeader) == 32, "map header is 32 bytes");
423
424inline constexpr std::uint32_t kMapNoBucket = ~std::uint32_t{0};
425
426// The first bytes of a slot. `bucket` doubles as the live mark and as the
427// back reference the hash index is validated against: a bucket entry counts only
428// when it names a slot below the high-water mark whose `bucket` is that bucket,
429// so an index object needs no zero fill and a clear needs no walk.
430struct MapLinks {
431 std::uint64_t hash;
432 std::uint32_t prev;
433 std::uint32_t next;
434 std::uint32_t chain;
435 std::uint32_t bucket;
436};
437static_assert(sizeof(MapLinks) == 24, "map slot links are 24 bytes");
438
439// A store object that holds an enumeration or a snapshot, released with its
440// last reference. `count` is the number of elements (an enumeration) or of
441// bytes (a snapshot).
442struct MapImage {
443 MapImage(std::shared_ptr<MapStoreHolder> owner, o1::Id object, std::uint64_t extent) noexcept
444 : holder(std::move(owner)), id(object), count(extent) {}
445 MapImage(const MapImage&) = delete;
446 MapImage& operator=(const MapImage&) = delete;
448 try {
449 std::lock_guard<std::recursive_mutex> lock(holder->mutex);
450 if (holder->store->alive(id)) {
451 auto tx = holder->store->begin_cleanup();
452 if (cleanup) cleanup(*holder->store, id, count);
453 holder->store->release(id);
454 tx->commit();
455 }
456 } catch (const o1::RollbackOnly&) {
457 holder->store->note_cleanup_skipped(o1::CleanupSkip::rollback_only);
458 } catch (...) {
459 holder->store->note_cleanup_skipped(o1::CleanupSkip::failed);
460 }
461 }
462
463 std::shared_ptr<MapStoreHolder> holder;
465 std::uint64_t count;
466 void (*cleanup)(o1::Store&, o1::Id, std::uint64_t) = nullptr;
467};
468
469// The result of keys() and values(): the elements as they were when it was
470// made, read one at a time from a store object. Nothing of the map's payload is
471// copied into the heap until an element is asked for, and later changes to the
472// map do not reach it.
473template <typename Elem>
475 using Rep = MapRepOf<Elem>;
476
477public:
478 using value_type = Elem;
479 using size_type = std::size_t;
480
482 public:
483 using iterator_category = std::input_iterator_tag;
484 using value_type = Elem;
485 using difference_type = std::ptrdiff_t;
486 using pointer = void;
487 using reference = Elem;
488
489 const_iterator() = default;
490
491 Elem operator*() const { return sequence_->at(index_); }
493 ++index_;
494 return *this;
495 }
497 const_iterator previous = *this;
498 ++index_;
499 return previous;
500 }
501 friend bool operator==(const const_iterator& lhs, const const_iterator& rhs) noexcept {
502 return lhs.sequence_ == rhs.sequence_ && lhs.index_ == rhs.index_;
503 }
504 friend bool operator!=(const const_iterator& lhs, const const_iterator& rhs) noexcept {
505 return !(lhs == rhs);
506 }
507
508 private:
509 friend class MapSequence;
510 const_iterator(const MapSequence* sequence, std::size_t index) noexcept
511 : sequence_(sequence), index_(index) {}
512
513 const MapSequence* sequence_ = nullptr;
514 std::size_t index_ = 0;
515 };
516
517 MapSequence() = default;
518 MapSequence(std::shared_ptr<MapImage> image, std::size_t count) noexcept
519 : image_(std::move(image)), count_(count) {}
520
521 [[nodiscard]] std::size_t size() const noexcept { return count_; }
522 [[nodiscard]] bool empty() const noexcept { return count_ == 0; }
523 [[nodiscard]] const_iterator begin() const noexcept { return const_iterator(this, 0); }
524 [[nodiscard]] const_iterator end() const noexcept { return const_iterator(this, count_); }
525
526 [[nodiscard]] Elem at(std::size_t index) const {
528 std::lock_guard<std::recursive_mutex> lock(image_->holder->mutex);
529 Rep rep{};
530 if (image_->holder->store->read(image_->id, index * sizeof(Rep), &rep, sizeof(Rep)) !=
531 sizeof(Rep)) {
533 }
534 return map_decode<Elem>(*image_->holder->store, rep, image_->holder);
535 }
536 [[nodiscard]] Elem operator[](std::size_t index) const { return at(index); }
537 [[nodiscard]] Elem front() const { return at(0); }
538 [[nodiscard]] Elem back() const { return at(count_ - 1); }
539
540 // For a consumer that needs the whole sequence in a contiguous container:
541 // this is the one place the elements are materialized, on request.
542 operator std::vector<Elem>() const {
543 std::vector<Elem> result;
544 result.reserve(count_);
545 for (std::size_t index = 0; index != count_; ++index) result.push_back(at(index));
546 return result;
547 }
548
549 template <typename Alloc>
550 friend bool operator==(const MapSequence& lhs, const std::vector<Elem, Alloc>& rhs) {
551 if (lhs.size() != rhs.size()) return false;
552 for (std::size_t index = 0; index != rhs.size(); ++index) {
553 if (!(lhs.at(index) == rhs[index])) return false;
554 }
555 return true;
556 }
557 friend bool operator==(const MapSequence& lhs, const MapSequence& rhs) {
558 if (lhs.size() != rhs.size()) return false;
559 for (std::size_t index = 0; index != lhs.size(); ++index)
560 if (!(lhs.at(index) == rhs.at(index))) return false;
561 return true;
562 }
563 friend bool operator!=(const MapSequence& lhs, const MapSequence& rhs) {
564 return !(lhs == rhs);
565 }
566 template <typename Alloc>
567 friend bool operator==(const std::vector<Elem, Alloc>& lhs, const MapSequence& rhs) {
568 return rhs == lhs;
569 }
570 template <typename Alloc>
571 friend bool operator!=(const MapSequence& lhs, const std::vector<Elem, Alloc>& rhs) {
572 return !(lhs == rhs);
573 }
574 template <typename Alloc>
575 friend bool operator!=(const std::vector<Elem, Alloc>& lhs, const MapSequence& rhs) {
576 return !(rhs == lhs);
577 }
578
579private:
580 std::shared_ptr<MapImage> image_;
581 std::size_t count_ = 0;
582};
583
584} // namespace detail
585
586template <typename T>
587inline constexpr bool is_pine_map_snapshot_value_v =
589
590// PineMap models a Pine map ID, not a value-owned C++ associative container.
591// Copying the handle therefore aliases the same backing store. Pine's
592// map.copy() operation is represented by copy(), which allocates a distinct
593// store and copies the entries into it.
594//
595// The pairs live in two objects of the shared script-state store: an entry
596// arena of max_pairs slots, in which a doubly linked list holds the
597// authoritative entry sequence (so overwrites never move an existing key and
598// removals do not disturb the relative order of survivors), and a fixed table of
599// hash chains that keeps get/put/remove/contains constant-time on average. Both
600// are sized once when the map is created, from the existing 50,000-pair limit:
601// a put never allocates a node or rehashes, and a removed slot is the next one
602// a put reuses. The Store's fixed chunk step makes backing allocation grow only
603// as slots and buckets are written, with a fixed resident page cache.
604template <typename Key, typename Value>
605class PineMap {
606 static_assert(detail::is_pine_map_key_v<Key>,
607 "PineMap key must be a Pine fundamental or enum type");
608
609public:
610 static constexpr int max_pairs = 50'000;
611 static constexpr bool snapshot_supported =
613
614private:
615 using StoreId = detail::o1::Id;
616 using KeyRep = detail::MapRepOf<Key>;
617 using ValueRep = detail::MapRepOf<Value>;
618 static_assert(std::is_trivially_copyable_v<KeyRep> &&
619 std::is_trivially_copyable_v<ValueRep>,
620 "map external representations must be copyable as Store bytes");
622
623public:
624 static constexpr std::size_t hot_read_cache_slots =
626 static constexpr std::size_t hot_read_cache_bytes = sizeof(ReadCache);
627
628private:
629
630 // A value that is neither a string nor trivially copyable (a handle) is
631 // kept whole in a heap table indexed by slot.
632 static constexpr bool kHeapValue =
635
636 static constexpr std::uint64_t kHeaderBytes = sizeof(detail::MapHeader);
637 static constexpr std::uint64_t kKeyOffset = sizeof(detail::MapLinks);
638 static constexpr std::uint64_t kValueOffset = kKeyOffset + sizeof(KeyRep);
639 static constexpr std::uint64_t kSlotBytes = (kValueOffset + sizeof(ValueRep) + 7) / 8 * 8;
640 static constexpr std::uint64_t kCapacity = static_cast<std::uint64_t>(max_pairs);
641 static constexpr std::uint32_t kBucketBits = 16;
642 static constexpr std::uint64_t kBucketBytes =
643 (std::uint64_t{1} << kBucketBits) * sizeof(std::uint32_t);
644
645 static constexpr const char* kNaIdError =
646 "map operation on na ID";
647 static constexpr const char* kInvalidSnapshotError =
648 "map restore from invalid snapshot";
649 static constexpr const char* kCapacityError =
650 "map cannot contain more than 50000 key-value pairs";
651
652 // The roots a map's state hangs from. Swapping them cannot fail, which is
653 // what lets a replacement be built completely before it takes the place of
654 // live state.
655 struct EntryRoot {
656 StoreId id = detail::o1::kNullId;
657 void swap(EntryRoot& other) noexcept { std::swap(id, other.id); }
658 };
659 struct IndexRoot {
660 StoreId id = detail::o1::kNullId;
661 void swap(IndexRoot& other) noexcept { std::swap(id, other.id); }
662 };
663
664 static_assert(noexcept(std::declval<EntryRoot&>().swap(
665 std::declval<EntryRoot&>())),
666 "PineMap ordered storage must support no-throw swap");
667 static_assert(noexcept(std::declval<IndexRoot&>().swap(
668 std::declval<IndexRoot&>())),
669 "PineMap index must support no-throw swap");
670
671 struct Storage {
672 std::shared_ptr<detail::MapStoreHolder> holder;
673 EntryRoot entries;
674 IndexRoot index;
675 ReadCache read_cache;
676 std::vector<Value> heap_values; // used only when kHeapValue
677
678 Storage() = default;
679 Storage(const Storage&) = delete;
680 Storage& operator=(const Storage&) = delete;
681 ~Storage() {
682 if (!holder) return;
683 try {
684 std::lock_guard<std::recursive_mutex> lock(holder->mutex);
685 // Return the cleanup slot before heap_values destroys nested maps.
686 auto tx = holder->store->begin_cleanup();
687 if (entries.id != detail::o1::kNullId && holder->store->alive(entries.id)) {
688 release_entries(*this, load_header(*this));
689 holder->store->release(entries.id);
690 }
691 if (index.id != detail::o1::kNullId && holder->store->alive(index.id))
692 holder->store->release(index.id);
693 tx->commit();
694 } catch (const detail::o1::RollbackOnly&) {
695 holder->store->note_cleanup_skipped(detail::o1::CleanupSkip::rollback_only);
696 } catch (...) {
697 holder->store->note_cleanup_skipped(detail::o1::CleanupSkip::failed);
698 }
699 }
700
701 detail::o1::Store& store() const { return *holder->store; }
702 };
703
704 using ValuesResult =
705 std::conditional_t<kHeapValue, std::vector<Value>, detail::MapSequence<Value>>;
706
707public:
708 // Snapshot is deliberately opaque. Besides a copy of the entries held in a
709 // store object, it retains the original backing-store identity. restore()
710 // mutates that store in place before reattaching the receiving handle, which
711 // both rolls back existing aliases and handles a map variable that was
712 // rebound after the snapshot, including a variable rebound to Pine na.
713 // restore() is an internal generated-checkpoint hook rather than a Pine map
714 // method, so a valid snapshot may reattach a null receiver.
715 //
716 // The copy is made page by page inside the store: no heap copy of the
717 // entries or of any payload exists at any time. A string payload object is
718 // immutable, so the copy shares it. The snapshot is reusable: restoring
719 // does not consume it.
720 //
721 // Value is copied according to its normal C++ copy semantics. This is a
722 // complete deep checkpoint for primitive Pine key/value instantiations.
723 // Generated UDTs or collections containing other handle types require a
724 // recursive, alias-aware checkpoint supplied by the code generator.
725 class Snapshot {
726 static_assert(snapshot_supported,
727 "PineMap::Snapshot supports primitive Pine values only; "
728 "UDT and collection handles require recursive codegen checkpointing");
729
730 std::shared_ptr<Storage> identity_;
731 std::shared_ptr<detail::MapImage> state_;
732
733 Snapshot(std::shared_ptr<Storage> identity, std::shared_ptr<detail::MapImage> state)
734 : identity_(std::move(identity)), state_(std::move(state)) {}
735
736 friend class PineMap;
737
738 public:
739 Snapshot(const Snapshot&) = default;
740 Snapshot& operator=(const Snapshot&) = default;
741 Snapshot(Snapshot&&) = default;
742 Snapshot& operator=(Snapshot&&) = default;
743 };
744
745 // The default value represents Pine na. map.new<K,V>() is the operation
746 // that allocates an actual map ID.
747 PineMap() noexcept = default;
748
749 PineMap(const PineMap&) noexcept = default;
750 PineMap& operator=(const PineMap&) noexcept = default;
751
752 // A Pine map is an ID. Treat C++ moves like ordinary Pine assignment so
753 // compiler-generated std::move paths cannot invalidate the source handle.
754 PineMap(PineMap&& other) noexcept : storage_(other.storage_) {}
755
756 PineMap& operator=(PineMap&& other) noexcept {
757 if (this != &other) storage_ = other.storage_;
758 return *this;
759 }
760
761 [[nodiscard]] static PineMap new_() {
762 auto storage = std::make_shared<Storage>();
763 storage->holder = detail::map_current_store();
764 std::lock_guard<std::recursive_mutex> lock(storage->holder->mutex);
765 auto tx = storage->store().scope();
766 create_objects(*storage);
767 tx.commit();
768 return PineMap(std::move(storage));
769 }
770
771 [[nodiscard]] bool is_na() const noexcept { return !storage_; }
772
773 // Returns the previous value, or the value type's Pine na sentinel when
774 // the key is inserted for the first time.
775 Value put(Key key, Value value) {
776 Storage& storage = require_storage();
777 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
778 storage.read_cache.clear();
779 auto tx = storage.store().scope();
780 Value previous = [&] {
781 try {
782 return put_locked(storage, key, std::move(value));
783 } catch (...) {
784 // Undo an owned transaction now. A joined scope marks the
785 // outer transaction rollback-only; its owner rolls it back.
786 tx.rollback();
787 throw;
788 }
789 }();
790 tx.commit();
791 return previous;
792 }
793
794 [[nodiscard]] Value get(const Key& key) const {
795 Storage& storage = require_storage();
796 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
797 const std::uint64_t hash = hash_key(key);
798 if (const auto* cached = find_cached(storage, key, hash))
799 return read_cached_value(storage, *cached);
800 const detail::MapHeader header = load_header(storage);
801 const Located found = locate(storage, header, key, hash);
802 if (found.slot == 0) return pineforge::na<Value>();
803 const std::uint32_t slot = found.slot - 1;
804 if constexpr (kHeapValue) {
805 record_cache(storage, hash, slot, found.key, ValueRep{});
806 return storage.heap_values[slot];
807 } else {
808 const ValueRep rep = load_value_rep(storage, slot);
809 record_cache(storage, hash, slot, found.key, rep);
810 return detail::map_decode<Value>(storage.store(), rep, storage.holder);
811 }
812 }
813
814 // Returns the removed value, or the value type's Pine na sentinel when no
815 // such key exists.
816 Value remove(const Key& key) {
817 Storage& storage = require_storage();
818 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
819 storage.read_cache.clear();
820 auto tx = storage.store().scope();
821 detail::MapHeader header = load_header(storage);
822 const Located found = locate(storage, header, key, hash_key(key));
823 if (found.slot == 0) { tx.commit(); return pineforge::na<Value>(); }
824
825 const std::uint32_t slot = found.slot - 1;
826 const detail::MapLinks links = load_links(storage, slot);
827 Value previous = read_value(storage, slot);
828 detail::map_release<Key>(storage.store(), load_key_rep(storage, slot));
829 if constexpr (!kHeapValue)
830 detail::map_release<Value>(storage.store(), load_value_rep(storage, slot));
831
832 // Out of the hash chain.
833 if (found.previous == 0) {
834 save_bucket(storage, found.bucket, links.chain);
835 } else {
836 detail::MapLinks before = load_links(storage, found.previous - 1);
837 before.chain = links.chain;
838 save_links(storage, found.previous - 1, before);
839 }
840 // Out of the entry sequence.
841 if (links.prev != 0) {
842 detail::MapLinks before = load_links(storage, links.prev - 1);
843 before.next = links.next;
844 save_links(storage, links.prev - 1, before);
845 } else {
846 header.head = links.next;
847 }
848 if (links.next != 0) {
849 detail::MapLinks after = load_links(storage, links.next - 1);
850 after.prev = links.prev;
851 save_links(storage, links.next - 1, after);
852 } else {
853 header.tail = links.prev;
854 }
855 // The slot is the next one a put reuses.
856 detail::MapLinks dead{};
857 dead.next = header.free_head;
858 dead.bucket = detail::kMapNoBucket;
859 save_links(storage, slot, dead);
860 header.free_head = slot + 1;
861 header.count -= 1;
862 save_header(storage, header);
863 if constexpr (kHeapValue) storage.heap_values[slot] = Value{};
864 tx.commit();
865 return previous;
866 }
867
868 [[nodiscard]] bool contains(const Key& key) const {
869 Storage& storage = require_storage();
870 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
871 const std::uint64_t hash = hash_key(key);
872 if (find_cached(storage, key, hash) != nullptr) return true;
873 const detail::MapHeader header = load_header(storage);
874 const Located found = locate<true>(storage, header, key, hash);
875 if (found.slot == 0) return false;
876 record_cache(storage, hash, found.slot - 1, found.key, found.value);
877 return true;
878 }
879
880 [[nodiscard]] int size() const {
881 Storage& storage = require_storage();
882 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
883 const detail::MapHeader header = load_header(storage);
884 if (header.count > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
886 "map.size: result exceeds int range");
887 }
888 return static_cast<int>(header.count);
889 }
890
891 // Generated state hashes provide their type-specific canonical scalar,
892 // UDT and alias-aware handle folds. Enumerate logical insertion order
893 // directly, without a keys()/values() image or whole-payload vector.
894 // Visitors must not mutate this map.
895 template <class Sink, class HashKey, class HashValue>
896 void hash_into(Sink& sink, HashKey hash_key_value, HashValue hash_value) const {
897 Storage& storage = require_storage();
898 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
899 const auto header = load_header(storage);
900 sink.u(header.count);
901 for (std::uint32_t at = header.head; at != 0;) {
902 hash_key_value(sink, read_key(storage, at - 1));
903 hash_value(sink, read_value(storage, at - 1));
904 at = load_links(storage, at - 1).next;
905 }
906 }
907
908 void clear() {
909 Storage& storage = require_storage();
910 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
911 storage.read_cache.clear();
912 auto tx = storage.store().scope();
913 // Release payload ownership before resetting the arena. Its scalar
914 // geometry and hash domain remain prepared for reuse.
915 release_entries(storage, load_header(storage));
916 save_header(storage, detail::MapHeader{});
917 storage.heap_values.clear();
918 tx.commit();
919 }
920
921 // Existing target keys retain their positions. New keys are appended in
922 // source insertion order. Iterating a map into itself is a no-op.
923 void put_all(const PineMap& source) {
924 Storage& target = require_storage();
925 Storage& from = source.require_storage();
926 if (storage_ == source.storage_) return;
927
928 std::unique_lock<std::recursive_mutex> target_lock(target.holder->mutex, std::defer_lock);
929 std::unique_lock<std::recursive_mutex> source_lock(from.holder->mutex, std::defer_lock);
930 if (target.holder == from.holder) {
931 target_lock.lock();
932 } else {
933 std::lock(target_lock, source_lock);
934 }
935
936 target.read_cache.clear();
937 const detail::MapHeader source_header = load_header(from);
938 const detail::MapHeader target_header = load_header(target);
939 std::size_t new_keys = 0;
940 for (std::uint32_t at = source_header.head; at != 0;) {
941 const Key key = read_key(from, at - 1);
942 if (locate(target, target_header, key, hash_key(key)).slot == 0) ++new_keys;
943 at = load_links(from, at - 1).next;
944 }
945 const auto available = static_cast<std::size_t>(max_pairs) -
946 static_cast<std::size_t>(target_header.count);
947 if (new_keys > available) {
948 // Preflight keeps put_all transactional with respect to the
949 // capacity check: existing values are not overwritten before a
950 // later new key discovers that the map is full.
951 detail::map_capacity_error(max_pairs, kCapacityError);
952 }
953
954 auto tx = target.store().scope();
955 for (std::uint32_t at = source_header.head; at != 0;) {
956 const Key key = read_key(from, at - 1);
957 (void)put_locked(target, key, read_value(from, at - 1));
958 at = load_links(from, at - 1).next;
959 }
960 tx.commit();
961 }
962
963 // The keys in insertion order, as of now. A virtual sequence over a store
964 // object: no heap vector of the keys exists unless the caller converts it.
965 [[nodiscard]] detail::MapSequence<Key> keys() const {
966 Storage& storage = require_storage();
967 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
968 auto tx = storage.store().scope();
969 const detail::MapHeader header = load_header(storage);
970 auto image = make_image(storage, static_cast<std::uint64_t>(header.count) * sizeof(KeyRep));
971 fill_image<Key>(storage, header, image->id, [&storage](std::uint32_t slot) {
972 return load_key_rep(storage, slot);
973 });
974 image->cleanup = &release_sequence<Key>;
975 tx.commit();
976 return detail::MapSequence<Key>(std::move(image), header.count);
977 }
978
979 // The values in insertion order, as of now. Strings and trivially copyable
980 // values come back as a virtual sequence over a store object, which shares
981 // the immutable string payloads rather than copying them. A handle value is
982 // a few words, so its sequence is the plain vector of those handles.
983 [[nodiscard]] ValuesResult values() const {
984 Storage& storage = require_storage();
985 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
986 const detail::MapHeader header = load_header(storage);
987 if constexpr (kHeapValue) {
988 std::vector<Value> result;
989 result.reserve(header.count);
990 for (std::uint32_t at = header.head; at != 0;) {
991 result.push_back(storage.heap_values[at - 1]);
992 at = load_links(storage, at - 1).next;
993 }
994 return result;
995 } else {
996 auto tx = storage.store().scope();
997 auto image = make_image(storage,
998 static_cast<std::uint64_t>(header.count) * sizeof(ValueRep));
999 fill_image<Value>(storage, header, image->id, [&storage](std::uint32_t slot) {
1000 return load_value_rep(storage, slot);
1001 });
1002 image->cleanup = &release_sequence<Value>;
1003 tx.commit();
1004 return detail::MapSequence<Value>(std::move(image), header.count);
1005 }
1006 }
1007
1008 [[nodiscard]] PineMap copy() const {
1009 // The outer map ID is independent. Values use their regular copy
1010 // semantics: primitive values are independent, while a handle-valued
1011 // entry continues to alias that nested handle (Pine's shallow object
1012 // copy behavior). Recursive checkpoint cloning belongs to generated
1013 // UDT/collection state, not map.copy(). The arena is copied inside the
1014 // store; a string payload is immutable, so both maps share it.
1015 Storage& source = require_storage();
1016 auto storage = std::make_shared<Storage>();
1017 storage->holder = source.holder;
1018 std::lock_guard<std::recursive_mutex> lock(source.holder->mutex);
1019 auto tx = source.store().scope();
1020 create_objects(*storage);
1021 const detail::MapHeader header = load_header(source);
1022 copy_range(source.store(), source.entries.id, storage->entries.id,
1023 kHeaderBytes + static_cast<std::uint64_t>(header.high_water) * kSlotBytes);
1024 rebuild_index(*storage, header);
1025 retain_entries(*storage, header);
1026 if constexpr (kHeapValue) storage->heap_values = source.heap_values;
1027 tx.commit();
1028 return PineMap(std::move(storage));
1029 }
1030
1031 template <typename T = Value,
1032 std::enable_if_t<
1034 std::is_same_v<T, Value>, int> = 0>
1035 [[nodiscard]] Snapshot snapshot() const {
1036 Storage& storage = require_storage();
1037 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
1038 auto tx = storage.store().scope();
1039 const detail::MapHeader header = load_header(storage);
1040 const std::uint64_t bytes =
1041 kHeaderBytes + static_cast<std::uint64_t>(header.high_water) * kSlotBytes;
1042 auto image = make_image(storage, bytes);
1043 copy_range(storage.store(), storage.entries.id, image->id, bytes);
1044 image_entry_refs(storage.store(), image->id, true);
1045 image->cleanup = &release_snapshot;
1046 tx.commit();
1047 return Snapshot(storage_, std::move(image));
1048 }
1049
1050 template <typename T = Value,
1051 std::enable_if_t<
1053 std::is_same_v<T, Value>, int> = 0>
1055 if (!snapshot.identity_ || !snapshot.state_) {
1057 }
1058 {
1059 // The snapshot's copy is laid back over the original identity's
1060 // arena and its hash chains re-linked, so every alias of that
1061 // identity sees the restored contents. The snapshot is untouched
1062 // and can be restored again.
1063 Storage& storage = *snapshot.identity_;
1064 std::lock_guard<std::recursive_mutex> lock(storage.holder->mutex);
1065 storage.read_cache.clear();
1066 auto tx = storage.store().scope();
1067 detail::MapHeader header{};
1068 read_exact(storage.store(), snapshot.state_->id, 0, &header, sizeof header);
1069 release_entries(storage, load_header(storage));
1070 copy_range(storage.store(), snapshot.state_->id, storage.entries.id,
1071 snapshot.state_->count);
1072 retain_entries(storage, header);
1073 rebuild_index(storage, header);
1074 tx.commit();
1075 }
1076 storage_ = snapshot.identity_;
1077 }
1078
1079private:
1080 // Where a key sits in its hash chain: the slot (index + 1, 0 if absent), the
1081 // slot before it in the chain, the chain's first slot and the bucket.
1082 struct Located {
1083 std::uint32_t slot = 0;
1084 std::uint32_t previous = 0;
1085 std::uint32_t head = 0;
1086 std::uint32_t bucket = 0;
1087 KeyRep key{};
1088 ValueRep value{}; // Filled with the value by locate<true> for contains().
1089 };
1090
1091 [[nodiscard]] Storage& require_storage() const {
1092 if (!storage_) detail::map_na_error(kNaIdError);
1093 return *storage_;
1094 }
1095
1096 explicit PineMap(std::shared_ptr<Storage> storage)
1097 : storage_(std::move(storage)) {}
1098
1099 static void create_objects(Storage& storage) {
1100 detail::o1::Store& store = storage.store();
1101 storage.entries.id = store.create(kHeaderBytes + kCapacity * kSlotBytes, 8);
1102 storage.index.id = store.create(kBucketBytes, 8);
1103 // A missing entry chunk reads as zero, so the all-zero MapHeader needs
1104 // no data record until the first map mutation.
1105 // Handle values grow their bounded table as slots are used.
1106 }
1107
1108 static void read_exact(const detail::o1::Store& store, StoreId id, std::uint64_t offset,
1109 void* destination, std::size_t bytes) {
1110 if (store.read(id, offset, destination, bytes) != bytes) {
1112 }
1113 }
1114
1115 static void write_exact(detail::o1::Store& store, StoreId id, std::uint64_t offset,
1116 const void* source, std::size_t bytes) {
1117 if (store.write(id, offset, source, bytes) != bytes) {
1119 }
1120 }
1121
1122 // Copies a prefix of one store object into another through one stack
1123 // buffer.
1124 static void copy_range(detail::o1::Store& store, StoreId from, StoreId to,
1125 std::uint64_t bytes) {
1126 unsigned char buffer[4096];
1127 std::uint64_t done = 0;
1128 while (done < bytes) {
1129 const std::size_t chunk =
1130 static_cast<std::size_t>(std::min<std::uint64_t>(sizeof buffer, bytes - done));
1131 read_exact(store, from, done, buffer, chunk);
1132 write_exact(store, to, done, buffer, chunk);
1133 done += chunk;
1134 }
1135 }
1136
1137 static detail::MapHeader load_header(Storage& storage) {
1138 detail::MapHeader header{};
1139 read_exact(storage.store(), storage.entries.id, 0, &header, sizeof header);
1140 return header;
1141 }
1142
1143 static void save_header(Storage& storage, const detail::MapHeader& header) {
1144 write_exact(storage.store(), storage.entries.id, 0, &header, sizeof header);
1145 }
1146
1147 static std::uint64_t slot_offset(std::uint32_t slot) {
1148 return kHeaderBytes + static_cast<std::uint64_t>(slot) * kSlotBytes;
1149 }
1150
1151 static detail::MapLinks load_links(Storage& storage, std::uint32_t slot) {
1152 detail::MapLinks links{};
1153 read_exact(storage.store(), storage.entries.id, slot_offset(slot), &links, sizeof links);
1154 return links;
1155 }
1156
1157 static void save_links(Storage& storage, std::uint32_t slot, const detail::MapLinks& links) {
1158 write_exact(storage.store(), storage.entries.id, slot_offset(slot), &links, sizeof links);
1159 }
1160
1161 static KeyRep load_key_rep(Storage& storage, std::uint32_t slot) {
1162 KeyRep rep{};
1163 read_exact(storage.store(), storage.entries.id, slot_offset(slot) + kKeyOffset, &rep,
1164 sizeof rep);
1165 return rep;
1166 }
1167
1168 static ValueRep load_value_rep(Storage& storage, std::uint32_t slot) {
1169 ValueRep rep{};
1170 read_exact(storage.store(), storage.entries.id, slot_offset(slot) + kValueOffset, &rep,
1171 sizeof rep);
1172 return rep;
1173 }
1174
1175 static void save_value_rep(Storage& storage, std::uint32_t slot, const ValueRep& rep) {
1176 write_exact(storage.store(), storage.entries.id, slot_offset(slot) + kValueOffset, &rep,
1177 sizeof rep);
1178 }
1179
1180 // A whole slot in one store write.
1181 static void save_slot(Storage& storage, std::uint32_t slot, const detail::MapLinks& links,
1182 const KeyRep& key, const ValueRep& value) {
1183 unsigned char raw[kSlotBytes] = {};
1184 std::memcpy(raw, &links, sizeof links);
1185 std::memcpy(raw + kKeyOffset, &key, sizeof key);
1186 std::memcpy(raw + kValueOffset, &value, sizeof value);
1187 write_exact(storage.store(), storage.entries.id, slot_offset(slot), raw, sizeof raw);
1188 }
1189
1190 static std::uint32_t load_bucket(Storage& storage, std::uint32_t bucket) {
1191 std::uint32_t head = 0;
1192 read_exact(storage.store(), storage.index.id,
1193 static_cast<std::uint64_t>(bucket) * sizeof head, &head, sizeof head);
1194 return head;
1195 }
1196
1197 static void save_bucket(Storage& storage, std::uint32_t bucket, std::uint32_t head) {
1198 write_exact(storage.store(), storage.index.id,
1199 static_cast<std::uint64_t>(bucket) * sizeof head, &head, sizeof head);
1200 }
1201
1202 static std::uint64_t hash_key(const Key& key) {
1203 return static_cast<std::uint64_t>(detail::PineMapKeyHash<Key>{}(key));
1204 }
1205
1206 static std::uint32_t bucket_of(std::uint64_t hash) noexcept {
1207 return static_cast<std::uint32_t>((hash * UINT64_C(0x9e3779b97f4a7c15)) >>
1208 (64 - kBucketBits));
1209 }
1210
1211 static bool key_matches(detail::o1::Store& store, const KeyRep& rep, const Key& key) {
1212 if constexpr (detail::is_map_string_v<Key>) {
1213 return detail::map_string_equals(store, rep, key);
1214 } else if constexpr (detail::MapExternalCodec<detail::PineMapBare<Key>>::enabled) {
1215 return detail::map_decode<Key>(store, rep) == key;
1216 } else {
1217 (void)store;
1218 return detail::PineMapKeyEqual<Key>{}(rep, key);
1219 }
1220 }
1221
1222 static const typename ReadCache::Entry* find_cached(Storage& storage, const Key& key,
1223 std::uint64_t hash) {
1224 // An outer Store rollback rewrites slots without calling a map mutator.
1225 // Check before touching borrowed external key/value representations.
1226 if (storage.read_cache.epoch != storage.store().rollback_epoch()) {
1227 storage.read_cache.clear();
1228 return nullptr;
1229 }
1230 // A hash is only a prefilter. The complete stored representation is
1231 // compared before a cache hit is accepted, including all string bytes.
1232 for (const auto& entry : storage.read_cache.entries) {
1233 if (entry.valid && entry.hash == hash &&
1234 key_matches(storage.store(), entry.key, key))
1235 return &entry;
1236 }
1237 return nullptr;
1238 }
1239
1240 static Value read_cached_value(Storage& storage, const typename ReadCache::Entry& entry) {
1241 if constexpr (kHeapValue) {
1242 return storage.heap_values[entry.slot];
1243 } else {
1244 return detail::map_decode<Value>(storage.store(), entry.value, storage.holder);
1245 }
1246 }
1247
1248 static void record_cache(Storage& storage, std::uint64_t hash, std::uint32_t slot,
1249 const KeyRep& key, const ValueRep& value) {
1250 storage.read_cache.epoch = storage.store().rollback_epoch();
1251 storage.read_cache.record(hash, slot, key, value);
1252 }
1253
1254 static Key read_key(Storage& storage, std::uint32_t slot) {
1255 return detail::map_decode<Key>(storage.store(), load_key_rep(storage, slot), storage.holder);
1256 }
1257
1258 static Value read_value(Storage& storage, std::uint32_t slot) {
1259 if constexpr (kHeapValue) {
1260 return storage.heap_values[slot];
1261 } else {
1262 return detail::map_decode<Value>(storage.store(), load_value_rep(storage, slot), storage.holder);
1263 }
1264 }
1265
1266 static void set_heap_value(Storage& storage, std::uint32_t slot, Value&& value) {
1267 if (slot >= storage.heap_values.size()) storage.heap_values.resize(slot + 1);
1268 storage.heap_values[slot] = std::move(value);
1269 }
1270
1271 // Walks the key's bucket. A bucket entry counts only if it names a slot
1272 // below the high-water mark that is itself in that bucket.
1273 template <bool WithValue = false>
1274 static Located locate(Storage& storage, const detail::MapHeader& header, const Key& key,
1275 std::uint64_t hash) {
1276 Located found;
1277 found.bucket = bucket_of(hash);
1278 std::uint32_t at = load_bucket(storage, found.bucket);
1279 std::uint32_t before = 0;
1280 bool first = true;
1281 while (at != 0 && at <= header.high_water) {
1282 const detail::MapLinks links = load_links(storage, at - 1);
1283 if (links.bucket != found.bucket) break;
1284 if (first) {
1285 found.head = at;
1286 first = false;
1287 }
1288 if (links.hash == hash) {
1289 if constexpr (WithValue && !kHeapValue) {
1290 // contains() needs the value only for cache fill. Fetch
1291 // the adjacent reps in the existing key read, adding no
1292 // Store calls to its uncached lookup.
1293 unsigned char raw[sizeof(KeyRep) + sizeof(ValueRep)];
1294 read_exact(storage.store(), storage.entries.id,
1295 slot_offset(at - 1) + kKeyOffset, raw, sizeof raw);
1296 std::memcpy(&found.key, raw, sizeof(KeyRep));
1297 std::memcpy(&found.value, raw + sizeof(KeyRep), sizeof(ValueRep));
1298 } else {
1299 found.key = load_key_rep(storage, at - 1);
1300 }
1301 if (key_matches(storage.store(), found.key, key)) {
1302 found.slot = at;
1303 found.previous = before;
1304 return found;
1305 }
1306 }
1307 before = at;
1308 at = links.chain;
1309 }
1310 return found;
1311 }
1312
1313 static Value put_locked(Storage& storage, const Key& key, Value value) {
1314 const detail::MapHeader before = load_header(storage);
1315 const std::uint64_t hash = hash_key(key);
1316 const Located found = locate(storage, before, key, hash);
1317 if (found.slot != 0) {
1318 const std::uint32_t slot = found.slot - 1;
1319 Value previous = read_value(storage, slot);
1320 if constexpr (kHeapValue) {
1321 set_heap_value(storage, slot, std::move(value));
1322 } else {
1323 const ValueRep old = load_value_rep(storage, slot);
1324 save_value_rep(storage, slot,
1325 detail::map_encode<Value>(storage.store(), value));
1326 detail::map_release<Value>(storage.store(), old);
1327 }
1328 return previous;
1329 }
1330
1331 if (before.count >= static_cast<std::uint32_t>(max_pairs)) {
1332 detail::map_capacity_error(max_pairs, kCapacityError);
1333 }
1334
1335 // Encode payloads inside the caller's transaction before linking the
1336 // new slot. An allocation or I/O error rolls these objects back too.
1337 const KeyRep key_rep = detail::map_encode<Key>(storage.store(), key);
1338 const ValueRep value_rep = detail::map_encode<Value>(storage.store(), value);
1339 detail::MapHeader after = before;
1340 std::uint32_t slot = 0;
1341 if (before.free_head != 0) {
1342 slot = before.free_head - 1;
1343 after.free_head = load_links(storage, slot).next;
1344 } else {
1345 slot = before.high_water;
1346 after.high_water = before.high_water + 1;
1347 }
1348 if constexpr (kHeapValue) set_heap_value(storage, slot, std::move(value));
1349
1350 detail::MapLinks links{};
1351 links.hash = hash;
1352 links.prev = before.tail;
1353 links.next = 0;
1354 links.chain = found.head;
1355 links.bucket = found.bucket;
1356 after.count = before.count + 1;
1357 after.tail = slot + 1;
1358 if (before.tail == 0) after.head = slot + 1;
1359 save_slot(storage, slot, links, key_rep, value_rep);
1360 if (before.tail != 0) {
1361 detail::MapLinks last = load_links(storage, before.tail - 1);
1362 last.next = slot + 1;
1363 save_links(storage, before.tail - 1, last);
1364 }
1365 save_bucket(storage, found.bucket, slot + 1);
1366 save_header(storage, after);
1367 return pineforge::na<Value>();
1368 }
1369
1370 static void release_entries(Storage& storage, const detail::MapHeader& header) {
1371 for (std::uint32_t at = header.head; at != 0;) {
1372 detail::map_release<Key>(storage.store(), load_key_rep(storage, at - 1));
1373 if constexpr (!kHeapValue)
1374 detail::map_release<Value>(storage.store(), load_value_rep(storage, at - 1));
1375 at = load_links(storage, at - 1).next;
1376 }
1377 }
1378
1379 static void retain_entries(Storage& storage, const detail::MapHeader& header) {
1380 for (std::uint32_t at = header.head; at != 0;) {
1381 detail::map_retain<Key>(storage.store(), load_key_rep(storage, at - 1));
1382 if constexpr (!kHeapValue)
1383 detail::map_retain<Value>(storage.store(), load_value_rep(storage, at - 1));
1384 at = load_links(storage, at - 1).next;
1385 }
1386 }
1387
1388 static void image_entry_refs(detail::o1::Store& store, StoreId image, bool retain) {
1389 detail::MapHeader header{};
1390 read_exact(store, image, 0, &header, sizeof header);
1391 for (std::uint32_t at = header.head; at != 0;) {
1392 const auto offset = slot_offset(at - 1);
1393 detail::MapLinks links{};
1394 KeyRep key{};
1395 ValueRep value{};
1396 read_exact(store, image, offset, &links, sizeof links);
1397 read_exact(store, image, offset + kKeyOffset, &key, sizeof key);
1398 read_exact(store, image, offset + kValueOffset, &value, sizeof value);
1399 if (retain) {
1400 detail::map_retain<Key>(store, key);
1401 if constexpr (!kHeapValue) detail::map_retain<Value>(store, value);
1402 } else {
1403 detail::map_release<Key>(store, key);
1404 if constexpr (!kHeapValue) detail::map_release<Value>(store, value);
1405 }
1406 at = links.next;
1407 }
1408 }
1409
1410 static void release_snapshot(detail::o1::Store& store, StoreId image, std::uint64_t) {
1411 image_entry_refs(store, image, false);
1412 }
1413
1414 template <typename Elem>
1415 static void release_sequence(detail::o1::Store& store, StoreId image, std::uint64_t bytes) {
1416 using Rep = detail::MapRepOf<Elem>;
1417 for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(Rep)) {
1418 Rep rep{};
1419 read_exact(store, image, offset, &rep, sizeof rep);
1420 detail::map_release<Elem>(store, rep);
1421 }
1422 }
1423
1424 // Re-links the hash index to an entry sequence whose slots (links, hashes,
1425 // chains and buckets) were just copied in: the chain head of a bucket is
1426 // the last slot of the sequence that sits in it.
1427 static void rebuild_index(Storage& storage, const detail::MapHeader& header) {
1428 for (std::uint32_t at = header.head; at != 0;) {
1429 const detail::MapLinks links = load_links(storage, at - 1);
1430 save_bucket(storage, links.bucket, at);
1431 at = links.next;
1432 }
1433 }
1434
1435 static std::shared_ptr<detail::MapImage> make_image(Storage& storage, std::uint64_t bytes) {
1436 const StoreId id = storage.store().create(bytes == 0 ? 1 : bytes, 8);
1437 return std::make_shared<detail::MapImage>(storage.holder, id, bytes);
1438 }
1439
1440 // Writes one rep per entry, in insertion order, through one stack buffer.
1441 template <typename Elem, typename Fetch>
1442 static void fill_image(Storage& storage, const detail::MapHeader& header, StoreId image,
1443 Fetch fetch) {
1444 using Rep = detail::MapRepOf<Elem>;
1445 constexpr std::size_t kPerChunk = std::max<std::size_t>(1, 4096 / sizeof(Rep));
1446 Rep chunk[kPerChunk] = {};
1447 std::size_t filled = 0;
1448 std::uint64_t written = 0;
1449 for (std::uint32_t at = header.head; at != 0;) {
1450 const Rep rep = fetch(at - 1);
1451 detail::map_retain<Elem>(storage.store(), rep);
1452 chunk[filled++] = rep;
1453 at = load_links(storage, at - 1).next;
1454 if (filled == kPerChunk) {
1455 write_exact(storage.store(), image, written * sizeof(Rep), chunk,
1456 filled * sizeof(Rep));
1457 written += filled;
1458 filled = 0;
1459 }
1460 }
1461 if (filled != 0) {
1462 write_exact(storage.store(), image, written * sizeof(Rep), chunk, filled * sizeof(Rep));
1463 }
1464 }
1465
1466 std::shared_ptr<Storage> storage_;
1467};
1468
1469template <typename Key, typename Value>
1470inline bool is_na(const PineMap<Key, Value>& map) noexcept {
1471 return map.is_na();
1472}
1473
1474} // namespace pineforge
Snapshot(const Snapshot &)=default
Snapshot & operator=(Snapshot &&)=default
Snapshot & operator=(const Snapshot &)=default
Snapshot(Snapshot &&)=default
detail::MapSequence< Key > keys() const
Definition map.hpp:965
Value get(const Key &key) const
Definition map.hpp:794
void put_all(const PineMap &source)
Definition map.hpp:923
Snapshot snapshot() const
Definition map.hpp:1035
PineMap & operator=(PineMap &&other) noexcept
Definition map.hpp:756
bool is_na() const noexcept
Definition map.hpp:771
static constexpr bool snapshot_supported
Definition map.hpp:611
ValuesResult values() const
Definition map.hpp:983
PineMap() noexcept=default
bool contains(const Key &key) const
Definition map.hpp:868
Value remove(const Key &key)
Definition map.hpp:816
void restore(const Snapshot &snapshot)
Definition map.hpp:1054
int size() const
Definition map.hpp:880
void hash_into(Sink &sink, HashKey hash_key_value, HashValue hash_value) const
Definition map.hpp:896
static constexpr std::size_t hot_read_cache_slots
Definition map.hpp:624
PineMap copy() const
Definition map.hpp:1008
static constexpr int max_pairs
Definition map.hpp:610
Value put(Key key, Value value)
Definition map.hpp:775
static PineMap new_()
Definition map.hpp:761
static constexpr std::size_t hot_read_cache_bytes
Definition map.hpp:626
ScriptString & append(const ScriptString &)
static ScriptString from_handle(detail::o1::Store &, Handle)
static void unpin(detail::o1::Store &, Handle)
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
friend bool operator!=(const const_iterator &lhs, const const_iterator &rhs) noexcept
Definition map.hpp:504
std::input_iterator_tag iterator_category
Definition map.hpp:483
friend bool operator==(const const_iterator &lhs, const const_iterator &rhs) noexcept
Definition map.hpp:501
friend bool operator!=(const std::vector< Elem, Alloc > &lhs, const MapSequence &rhs)
Definition map.hpp:575
const_iterator end() const noexcept
Definition map.hpp:524
const_iterator begin() const noexcept
Definition map.hpp:523
friend bool operator==(const MapSequence &lhs, const MapSequence &rhs)
Definition map.hpp:557
Elem at(std::size_t index) const
Definition map.hpp:526
friend bool operator!=(const MapSequence &lhs, const MapSequence &rhs)
Definition map.hpp:563
bool empty() const noexcept
Definition map.hpp:522
std::size_t size() const noexcept
Definition map.hpp:521
MapSequence(std::shared_ptr< MapImage > image, std::size_t count) noexcept
Definition map.hpp:518
friend bool operator==(const std::vector< Elem, Alloc > &lhs, const MapSequence &rhs)
Definition map.hpp:567
friend bool operator!=(const MapSequence &lhs, const std::vector< Elem, Alloc > &rhs)
Definition map.hpp:571
friend bool operator==(const MapSequence &lhs, const std::vector< Elem, Alloc > &rhs)
Definition map.hpp:550
Elem operator[](std::size_t index) const
Definition map.hpp:536
MapStoreScope(std::shared_ptr< MapStoreHolder > holder)
Definition map.hpp:175
MapStoreScope & operator=(const MapStoreScope &)=delete
MapStoreScope(const MapStoreScope &)=delete
virtual std::size_t write(Id, std::uint64_t off, const void *src, std::size_t n)=0
virtual std::size_t read(Id, std::uint64_t off, void *dst, std::size_t n) const =0
virtual Id create(std::uint64_t bytes, std::uint64_t align)=0
virtual void release(Id)=0
std::uint64_t Id
Definition o1_store.hpp:49
constexpr Id kNullId
Definition o1_store.hpp:50
void write_exact(o1::Store &store, o1::Id id, std::uint64_t offset, const void *source, std::size_t bytes)
Definition array.hpp:75
void read_exact(const o1::Store &store, o1::Id id, std::uint64_t offset, void *destination, std::size_t bytes)
Definition array.hpp:69
constexpr bool is_map_inline_v
Definition map.hpp:237
bool map_string_equals(const o1::Store &store, const MapStringRep &rep, const std::string &text)
Definition map.hpp:328
constexpr std::uint32_t kMapNoBucket
Definition map.hpp:424
std::shared_ptr< MapStoreHolder > map_current_store()
Definition map.hpp:160
void map_na_error(const char *what)
Definition map.hpp:36
constexpr std::size_t kMapReadCacheSlots
Definition map.hpp:255
T map_decode(o1::Store &store, const MapRepOf< T > &rep, const std::shared_ptr< MapStoreHolder > &owner={})
Definition map.hpp:365
constexpr std::size_t kMapInlineBytes
Definition map.hpp:187
void map_invariant_error(const char *what)
Definition map.hpp:51
constexpr bool is_pine_map_snapshot_value_v
Definition map.hpp:64
void map_retain(o1::Store &store, const MapRepOf< T > &rep)
Definition map.hpp:381
std::unique_ptr< ScratchBacking > make_script_scratch_backing()
constexpr const char * kMapSequenceRangeError
Definition map.hpp:129
MapStringRep map_string_encode(o1::Store &store, const std::string &text)
Definition map.hpp:298
std::shared_ptr< MapStoreHolder > & map_store_slot()
Definition map.hpp:150
MapRepOf< T > map_encode(o1::Store &store, const T &value)
Definition map.hpp:349
constexpr const char * kMapStoreTransferError
Definition map.hpp:128
constexpr bool is_map_string_v
Definition map.hpp:234
void map_release(o1::Store &store, const MapRepOf< T > &rep)
Definition map.hpp:397
std::remove_cv_t< std::remove_reference_t< T > > PineMapBare
Definition map.hpp:31
constexpr bool is_pine_map_key_v
Definition map.hpp:56
std::conditional_t< is_map_string_v< T >, MapStringRep, std::conditional_t< MapExternalCodec< PineMapBare< T > >::enabled, typename MapExternalCodec< PineMapBare< T > >::Rep, std::conditional_t< is_map_inline_v< T >, PineMapBare< T >, char > > > MapRepOf
Definition map.hpp:245
std::string map_string_decode(const o1::Store &store, const MapStringRep &rep)
Definition map.hpp:315
void map_capacity_error(int cap, const char *what)
Definition map.hpp:42
constexpr bool is_pine_map_snapshot_value_v
Definition map.hpp:587
bool is_na(const Line &h)
Definition drawing.hpp:58
T na()
Definition na.hpp:54
void swap(ScriptString &a, ScriptString &b) noexcept
static void retain(o1::Store &store, Rep rep)
Definition map.hpp:216
static Rep encode(o1::Store &store, const ScriptString &text)
Definition map.hpp:207
static void release(o1::Store &store, Rep rep)
Definition map.hpp:220
static ScriptString decode(o1::Store &store, Rep rep)
Definition map.hpp:213
static constexpr bool enabled
Definition map.hpp:199
std::uint32_t reserved[3]
Definition map.hpp:420
std::uint32_t free_head
Definition map.hpp:419
std::uint32_t high_water
Definition map.hpp:418
MapImage(const MapImage &)=delete
std::uint64_t count
Definition map.hpp:465
std::shared_ptr< MapStoreHolder > holder
Definition map.hpp:463
void(* cleanup)(o1::Store &, o1::Id, std::uint64_t)
Definition map.hpp:466
MapImage & operator=(const MapImage &)=delete
MapImage(std::shared_ptr< MapStoreHolder > owner, o1::Id object, std::uint64_t extent) noexcept
Definition map.hpp:443
void record(std::uint64_t hash, std::uint32_t slot, const KeyRep &key, const ValueRep &value) noexcept
Definition map.hpp:285
static constexpr std::size_t capacity
Definition map.hpp:275
std::array< Entry, Slots > entries
Definition map.hpp:271
std::unique_ptr< o1::Store > owned_store
Definition map.hpp:145
MapStoreHolder(o1::Store &borrowed) noexcept
Definition map.hpp:140
MapStoreHolder(const MapStoreHolder &)=delete
MapStoreHolder(std::unique_ptr< ScratchBacking > supplied, const o1::Shape &shape={})
Definition map.hpp:135
MapStoreHolder & operator=(const MapStoreHolder &)=delete
std::recursive_mutex mutex
Definition map.hpp:147
std::unique_ptr< ScratchBacking > backing
Definition map.hpp:144
char inline_bytes[kMapInlineBytes]
Definition map.hpp:228
bool operator()(Key lhs, Key rhs) const noexcept
Definition map.hpp:81
bool operator()(const Key &lhs, const Key &rhs) const noexcept(noexcept(lhs==rhs))
Definition map.hpp:73
std::size_t operator()(Key key) const noexcept
Definition map.hpp:96
std::size_t operator()(const Key &key) const noexcept(noexcept(std::hash< Key >{}(key)))
Definition map.hpp:88