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