PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
disk_id_table.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <algorithm>
6#include <cerrno>
7#include <cstddef>
8#include <cstdint>
9#include <limits>
10#include <optional>
11#include <string>
12#include <type_traits>
13#include <utility>
14
15namespace pineforge::detail {
16
17// An AVL tree of byte-string keys and fixed-size values in positional scratch.
18// A node is 56 + sizeof(Value) bytes followed by the key bytes. Parent links
19// allow rotations and sorted walks with O(1) RAM, including adversarial ids.
20// The independent next link preserves first insertion order. Updating a value
21// overwrites its slot: repeated ids cost no additional disk or resident heap.
22// File offsets/tree links are storage metadata; callers fold logical values.
23template<class Value>
25 static_assert(std::is_trivially_copyable_v<Value>, "disk id values are fixed records");
26 struct Links {
27 std::uint64_t left = 0, right = 0, parent = 0, next = 0;
28 std::uint64_t key_size = 0, height = 1, reserved = 0;
29 };
30 static_assert(sizeof(Links) == 56, "scratch id node layout");
31
32public:
33 using Offset = std::uint64_t;
34 DiskIdTable() = default;
35 DiskIdTable(const DiskIdTable&) = delete;
37 DiskIdTable(DiskIdTable&& other) noexcept { *this = std::move(other); }
38 DiskIdTable& operator=(DiskIdTable&& other) noexcept {
39 if (this != &other) {
40 file_ = std::move(other.file_);
41 root_ = std::exchange(other.root_, 0);
42 first_ = std::exchange(other.first_, 0);
43 last_ = std::exchange(other.last_, 0);
44 end_ = std::exchange(other.end_, 1);
45 size_ = std::exchange(other.size_, 0);
46 }
47 return *this;
48 }
49 void set_store(ScratchStore store) noexcept { file_.set_store(store); }
50 bool arena_backed() const noexcept { return file_.arena_backed(); }
51 bool bounded_cohorts() const noexcept {
52 return file_.scratch_owner() && file_.scratch_resource() == ScratchResource::cohort;
53 }
54 void set_directory(const std::string& directory) { file_.set_directory(directory); }
55 void bind_scratch(const std::shared_ptr<ScratchSlab>& slab, ScratchResource resource) {
56 if (size_ != 0)
58 "scratch id backend changed after use");
61 "scratch id table requires a tree region");
62 file_.bind_scratch(slab, resource);
63 }
64 void clear() noexcept {
65 file_.clear();
66 root_ = first_ = last_ = size_ = 0;
67 end_ = 1; // offset zero is the null link
68 }
69 std::uint64_t size() const noexcept { return size_; }
70 std::uint64_t file_bytes() const noexcept { return size_ == 0 ? 0 : end_; }
71 Offset first_inserted() const noexcept { return first_; }
72 Offset next_inserted(Offset at) const { return links(at).next; }
73 std::string key(Offset at) const { return read_key(at, links(at)); }
74 Offset first_sorted() const { return leftmost(root_); }
76 auto row = links(at);
77 if (row.right != 0) return leftmost(row.right);
78 Offset child = at;
79 at = row.parent;
80 while (at != 0) {
81 row = links(at);
82 if (row.left == child) break;
83 child = at;
84 at = row.parent;
85 }
86 return at;
87 }
88
89 std::optional<Offset> find(const std::string& key) const {
90 Offset at = root_;
91 while (at != 0) {
92 const auto row = links(at);
93 const int order = key.compare(read_key(at, row));
94 if (order == 0) return at;
95 at = order < 0 ? row.left : row.right;
96 }
97 return std::nullopt;
98 }
99 // Read-only whole-node ticket, also used before a caller spills resident
100 // facts or accepts a kernel command. Duplicates need no count/byte growth.
101 // Disk mode deliberately retains its existing search and I/O behaviour.
102 void require_insert(const std::string& key) const {
103 if (!file_.scratch_owner() || find(key)) return;
104 require_new_key(key.size());
105 }
106 // enumerate(visit) replays a stable, borrowed workset. Count each absent
107 // key once without a growing side index: O(workset^2) comparisons, O(1)
108 // retained scratch. No caller may mutate the workset during this ticket.
109 template<class Enumerate> void require_inserts(Enumerate&& enumerate) const {
110 if (!file_.scratch_owner()) return;
111 std::uint64_t ordinal = 0, extra_rows = 0, extra_bytes = 0;
112 enumerate([&](const std::string& key) {
113 require_id_size(key.size());
114 const auto current = ordinal++;
115 if (find(key)) return;
116 bool repeated = false;
117 std::uint64_t prior = 0;
118 enumerate([&](const std::string& other) {
119 if (prior++ < current && other == key) repeated = true;
120 });
121 if (repeated) return;
122 const auto maximum = std::numeric_limits<std::uint64_t>::max();
123 if (extra_rows == maximum || key.size() > maximum - extra_bytes) {
124 const auto resource = file_.scratch_resource();
125 const auto& manifest = file_.scratch_owner()->manifest();
126 throw ScratchCapacityError(resource, extra_rows == maximum
128 resource == ScratchResource::cohort ? manifest.U : manifest.V)
130 resource == ScratchResource::cohort ? manifest.B_U : manifest.B_V));
131 }
132 ++extra_rows;
133 extra_bytes += key.size();
134 require_growth(extra_rows, extra_bytes);
135 });
136 }
137 Value get(Offset at) const {
138 validate(at);
139 Value value{};
140 file_.read(at + sizeof(Links), &value, sizeof value);
141 return value;
142 }
143 void set(Offset at, const Value& value) {
144 validate(at);
145 file_.write(at + sizeof(Links), &value, sizeof value);
146 }
147 std::pair<Offset, bool> insert(const std::string& key, const Value& value) {
148 Offset parent = 0;
149 Offset at = root_;
150 bool left = false;
151 while (at != 0) {
152 const auto row = links(at);
153 const int order = key.compare(read_key(at, row));
154 if (order == 0) return {at, false};
155 parent = at;
156 left = order < 0;
157 at = left ? row.left : row.right;
158 }
159 require_new_key(key.size());
160 constexpr std::size_t header = sizeof(Links) + sizeof(Value);
161 const bool arena = file_.opened() ? file_.arena_backed() : ScratchArena::attached() != nullptr;
164 if (size_ == std::numeric_limits<std::uint64_t>::max())
165 throw AuxiliaryScratchIoError(EFBIG, "scratch id count exhausted");
166 file_.open("pineforge-ids");
167 at = end_;
168 Links row;
169 row.parent = parent;
170 row.key_size = key.size();
171 file_.write(at, &row, sizeof row);
172 file_.write(at + sizeof(Links), &value, sizeof value);
173 file_.write(at + header, key.data(), key.size());
174 // All data bytes exist before linking the row into the logical table.
175 // Any partial link/rotation I/O error fails the run; it is never
176 // converted to a successful absent lookup or retried as another id.
177 end_ += header + key.size();
178 if (parent != 0) {
179 auto old = links(parent);
180 (left ? old.left : old.right) = at;
181 put_links(parent, old);
182 } else root_ = at;
183 if (last_ != 0) {
184 auto previous = links(last_);
185 previous.next = at;
186 put_links(last_, previous);
187 } else first_ = at;
188 last_ = at;
189 ++size_;
190 rebalance(parent);
191 return {at, true};
192 }
193 template<class F> void for_each_sorted(F&& visit) const {
194 for (Offset at = first_sorted(); at != 0; at = next_sorted(at))
195 visit(key(at), get(at));
196 }
197 template<class F> void for_each_inserted(F&& visit) const {
198 for (Offset at = first_; at != 0;) {
199 const auto row = links(at);
200 visit(read_key(at, row), get(at));
201 at = row.next;
202 }
203 }
204
205private:
206 void require_id_size(std::size_t key_size) const {
208 && key_size > 64)
209 throw ScratchCapacityError(ScratchResource::cohort, "id longer than 64 bytes");
210 }
211 void require_new_key(std::size_t key_size) const {
212 require_id_size(key_size);
213 require_growth(1, key_size);
214 }
215 void require_growth(std::uint64_t extra_rows, std::uint64_t extra_bytes) const {
216 const auto& slab = file_.scratch_owner();
217 if (!slab) return;
218 const auto resource = file_.scratch_resource();
219 const auto& manifest = slab->manifest();
220 const auto rows = resource == ScratchResource::cohort ? manifest.U : manifest.V;
221 const auto bytes = resource == ScratchResource::cohort ? manifest.B_U : manifest.B_V;
222 constexpr std::uint64_t header = sizeof(Links) + sizeof(Value);
223 // Packed nodes have no inter-row alignment: the append cursor minus
224 // the null byte and fixed headers is exactly aggregate stored key bytes.
225 const auto key_bytes = end_ - 1 - header * size_;
226 if (size_ > rows || extra_rows > rows - size_)
227 throw ScratchCapacityError(resource, scratch_count_limit_message(resource, rows));
228 if (key_bytes > bytes || extra_bytes > bytes - key_bytes)
229 throw ScratchCapacityError(resource, scratch_byte_limit_message(resource, bytes));
230 const auto capacity = slab->region(resource).capacity;
231 if (end_ > capacity || extra_rows > (capacity - end_) / header)
232 throw ScratchCapacityError(resource, scratch_byte_limit_message(resource, bytes));
233 const auto fixed_bytes = header * extra_rows;
234 if (extra_bytes > capacity - end_ - fixed_bytes)
235 throw ScratchCapacityError(resource, scratch_byte_limit_message(resource, bytes));
236 slab->require_extend(resource, end_ + fixed_bytes + extra_bytes);
237 }
238 [[noreturn]] void io_error(int error, const char* operation) const {
239 throw AuxiliaryScratchIoError(error, operation);
240 }
241 Offset leftmost(Offset at) const {
242 while (at != 0) {
243 const auto row = links(at);
244 if (row.left == 0) break;
245 at = row.left;
246 }
247 return at;
248 }
249 void validate(Offset at) const {
250 if (at == 0 || at >= end_ || end_ - at < sizeof(Links) + sizeof(Value))
251 io_error(EIO, "scratch id invalid offset");
252 }
253 Links links(Offset at) const {
254 validate(at);
255 Links row{};
256 file_.read(at, &row, sizeof row);
257 if (row.height == 0 || row.height > 128
258 || row.key_size > end_ - at - sizeof(Links) - sizeof(Value))
259 io_error(EIO, "scratch id invalid node");
260 return row;
261 }
262 void put_links(Offset at, const Links& row) { file_.write(at, &row, sizeof row); }
263 std::string read_key(Offset at, const Links& row) const {
264 if (row.key_size > std::string{}.max_size())
265 io_error(EFBIG, "scratch id key too long");
266 std::string key(static_cast<std::size_t>(row.key_size), '\0');
267 file_.read(at + sizeof(Links) + sizeof(Value), key.data(), key.size());
268 return key;
269 }
270 std::uint64_t height(Offset at) const { return at == 0 ? 0 : links(at).height; }
271 void refresh(Links& row) const { row.height = 1 + std::max(height(row.left), height(row.right)); }
272 int balance(const Links& row) const {
273 return static_cast<int>(height(row.left)) - static_cast<int>(height(row.right));
274 }
275 Offset rotate(Offset at, bool left) {
276 auto old = links(at);
277 const Offset top_at = left ? old.right : old.left;
278 auto top = links(top_at);
279 const Offset middle_at = left ? top.left : top.right;
280 const Offset parent_at = old.parent;
281 if (parent_at != 0) {
282 auto parent = links(parent_at);
283 (parent.left == at ? parent.left : parent.right) = top_at;
284 put_links(parent_at, parent);
285 } else root_ = top_at;
286 top.parent = parent_at;
287 old.parent = top_at;
288 (left ? old.right : old.left) = middle_at;
289 if (middle_at != 0) {
290 auto middle = links(middle_at);
291 middle.parent = at;
292 put_links(middle_at, middle);
293 }
294 refresh(old);
295 put_links(at, old);
296 (left ? top.left : top.right) = at;
297 refresh(top);
298 put_links(top_at, top);
299 return top_at;
300 }
301 void rebalance(Offset at) {
302 while (at != 0) {
303 auto row = links(at);
304 refresh(row);
305 put_links(at, row);
306 const int lean = balance(row);
307 if (lean > 1) {
308 if (balance(links(row.left)) < 0) (void)rotate(row.left, true);
309 at = rotate(at, false);
310 } else if (lean < -1) {
311 if (balance(links(row.right)) > 0) (void)rotate(row.right, false);
312 at = rotate(at, true);
313 }
314 at = links(at).parent;
315 }
316 }
317
318 ScratchFile file_{ScratchFile::IoRaiser::auxiliary};
319 Offset root_ = 0, first_ = 0, last_ = 0, end_ = 1;
320 std::uint64_t size_ = 0;
321};
322} // namespace pineforge::detail
#define F(name)
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
std::pair< Offset, bool > insert(const std::string &key, const Value &value)
void for_each_sorted(F &&visit) const
Offset next_inserted(Offset at) const
void for_each_inserted(F &&visit) const
void require_insert(const std::string &key) const
std::uint64_t file_bytes() const noexcept
Offset next_sorted(Offset at) const
void set_directory(const std::string &directory)
void set(Offset at, const Value &value)
std::string key(Offset at) const
void require_inserts(Enumerate &&enumerate) const
bool bounded_cohorts() const noexcept
DiskIdTable & operator=(const DiskIdTable &)=delete
void bind_scratch(const std::shared_ptr< ScratchSlab > &slab, ScratchResource resource)
DiskIdTable(const DiskIdTable &)=delete
DiskIdTable & operator=(DiskIdTable &&other) noexcept
std::optional< Offset > find(const std::string &key) const
void set_store(ScratchStore store) noexcept
std::uint64_t size() const noexcept
DiskIdTable(DiskIdTable &&other) noexcept
Offset first_inserted() const noexcept
bool arena_backed() const noexcept
static ScratchArena * attached() noexcept
static void check_extent(std::uint64_t offset, std::size_t length, bool arena=false, IoRaiser raiser=IoRaiser::report_sink)
ScratchResource scratch_resource() const noexcept
const std::shared_ptr< ScratchSlab > & scratch_owner() const noexcept
std::string scratch_count_limit_message(ScratchResource resource, std::uint64_t limit)
std::string scratch_byte_limit_message(ScratchResource resource, std::uint64_t limit)