PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
exit_scratch.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include <algorithm>
9#include <array>
10#include <cstdint>
11#include <cstring>
12#include <exception>
13#include <iterator>
14#include <limits>
15#include <optional>
16#include <stdexcept>
17#include <string>
18#include <type_traits>
19#include <utility>
20
21namespace pineforge::source {
24
25namespace detail {
26using pineforge::detail::ScratchBuffer;
27using pineforge::detail::ScratchByteSource;
31using pineforge::detail::ScratchKeyOrder;
32using pineforge::detail::ScratchReadback;
33using pineforge::detail::ScratchRecordInfo;
34using pineforge::detail::ScratchSpanSource;
36using pineforge::detail::CommandScratchStore;
37
38// At most this many selected rows per construction transaction, or stored
39// records per teardown transaction. Each final partial batch commits once;
40// scope population never sizes resident storage.
41inline constexpr std::size_t kExitScopeEraseBatch = 64;
42
43// Terminal diagnostic only, like CommandScratchStore::carried_failure().
44// Retain the first teardown failure across rollback and further unwinding;
45// it is neither logical scratch state nor a population-sized allocation.
47public:
48 bool pending() const noexcept { return status_ != ScratchStatus::ok; }
49 void remember(ScratchStatus status, const ScratchReadback& store) noexcept {
50 if (pending() || status == ScratchStatus::ok) return;
51 status_ = status;
52 if (status == ScratchStatus::io_error)
53 if (const auto* failure = store.carried_failure()) failure_ = *failure;
54 }
55 void check() const;
56private:
57 ScratchStatus status_ = ScratchStatus::ok;
58 std::optional<pineforge::detail::ReportScratchIoError> failure_;
59};
60static_assert(std::is_nothrow_copy_constructible_v<pineforge::detail::ReportScratchIoError>
61 && std::is_nothrow_copy_assignable_v<pineforge::detail::ReportScratchIoError>,
62 "teardown must retain the typed backing failure without throwing");
63
64// The source run owner provisions the CX-O1C store. The version owner must retain
65// every payload field externally until release_scope, including across nested
66// Applied callbacks; it may not keep an owning row/token table in RAM. Reads
67// reproduce the frozen value, not the current source row. These integration
68// declarations do not implement a second store.
70public:
71 virtual ~ExitScratchVersions() = default;
72 virtual std::uint64_t placement(std::uint64_t scope, const PlacementSnapshot&) = 0;
73 virtual PlacementSnapshot placement(std::uint64_t version) const = 0;
74 virtual std::uint64_t request(std::uint64_t scope, const native_order::Request&) = 0;
75 virtual native_order::Request request(std::uint64_t version) const = 0;
76 virtual std::uint64_t pending(std::uint64_t scope, const native_order::Request&,
77 const PlacementSnapshot&, const std::string& key) = 0;
78 virtual void pending(std::uint64_t version, native_order::Request&,
79 PlacementSnapshot&, std::string& key) const = 0;
80 // OPEN: native owners must retain this fixed membership beyond scratch
81 // scope destruction. A live BindCohort cannot implement this operation.
82 using OpeningReader = ScratchStatus (*)(void*, std::uint64_t,
85 std::int64_t cycle, std::uint64_t count, void*, OpeningReader) = 0;
86 virtual void release_scope(std::uint64_t scope) noexcept = 0;
87 void check_teardown_failure() const { teardown_failure_.check(); }
88 bool teardown_failed() const noexcept { return teardown_failure_.pending(); }
89protected:
91};
92
97 std::uint64_t next_scope = 0;
98 void* observation = nullptr;
99 void (*observe_rows)(void*, std::uint64_t scope, std::size_t rows) noexcept = nullptr;
100 void (*observe_lookup)(void*, std::size_t candidates) noexcept = nullptr;
101 ScratchStatus (*lots)(const NativeStrategyHost&, double, void*,
102 bool (*)(void*, const NativeOpenLot&)) = nullptr;
104 bool (*)(void*, const NativeWorkingRequest&)) = nullptr;
106};
107
108class ExitScratchError final : public coded<std::logic_error> {
109public:
111 ScratchStatus status() const noexcept { return status_; }
112private:
113 ScratchStatus status_;
114};
115
116// Defined in pine_adapter.cpp, including the unbound/error paths. project()
117// installs the run owner; bind_exit_scratch supplies an explicit borrowed override.
118ExitScratchContext& exit_scratch(const PineExecutionAdapter&);
119[[noreturn]] void exit_scratch_failure(ScratchStatus);
120
122 if (status != ScratchStatus::ok) exit_scratch_failure(status);
123}
124[[noreturn]] inline void exit_scratch_failure(ScratchStatus status, const ScratchReadback& store) {
125 if (status == ScratchStatus::io_error)
127 exit_scratch_failure(status);
128}
129inline void exit_scratch_check(ScratchStatus status, const ScratchReadback& store) {
130 if (status != ScratchStatus::ok) exit_scratch_failure(status, store);
131}
133 if (!pending()) return;
134 pineforge::detail::rethrow_carried_scratch_failure(failure_ ? &*failure_ : nullptr);
135 exit_scratch_failure(status_);
136}
138 if (context.versions) context.versions->check_teardown_failure();
139 context.teardown_failure.check();
140}
143 if (!context.versions) exit_scratch_failure(ScratchStatus::programming_error);
144 return *context.versions;
145}
146
147template<class Visit> void exit_scratch_lots(ExitScratchContext& scratch,
148 const NativeStrategyHost& host, double mark, Visit visit) {
150 if (!scratch.lots) exit_scratch_failure(ScratchStatus::programming_error);
151 const auto status = scratch.lots(host, mark, &visit,
152 [](void* context, const NativeOpenLot& lot) {
153 return (*static_cast<Visit*>(context))(lot);
154 });
155 if (status != ScratchStatus::cancelled) exit_scratch_check(status, scratch.store);
156}
157template<class Visit> void exit_scratch_working(ExitScratchContext& scratch,
158 const NativeStrategyHost& host, Visit visit) {
160 if (!scratch.working) exit_scratch_failure(ScratchStatus::programming_error);
161 const auto status = scratch.working(host, &visit,
162 [](void* context, const NativeWorkingRequest& request) {
163 return (*static_cast<Visit*>(context))(request);
164 });
165 if (status != ScratchStatus::cancelled) exit_scratch_check(status, scratch.store);
166}
167
168// A handle plus a borrowed readback, never a cache pointer or owning string.
169// It remains readable across eviction and nested scopes.
170class ExitScratchText final : public ScratchByteSource {
171public:
173 ScratchField field = ScratchField::value, std::uint64_t offset = 0)
174 : reader_(&reader), handle_(handle), field_(field), offset_(offset) {
175 ScratchRecordInfo info{};
176 exit_scratch_check(reader.point(handle, info), reader);
177 const auto bytes = field == ScratchField::key ? info.key_bytes : info.value_bytes;
178 if (offset > bytes) exit_scratch_failure(ScratchStatus::corrupt);
179 size_ = bytes - offset;
180 }
181 std::uint64_t size() const noexcept override { return size_; }
183 return reader_->carried_failure();
184 }
185 bool empty() const noexcept { return size_ == 0; }
186 ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override {
187 if (offset > size_ || destination.size > size_ - offset)
188 return ScratchStatus::programming_error;
189 return reader_->read(handle_, field_, offset_ + offset, destination);
190 }
191private:
192 const ScratchReadback* reader_;
193 ScratchHandle handle_;
194 ScratchField field_;
195 std::uint64_t offset_;
196 std::uint64_t size_ = 0;
197};
198
199inline ScratchSpanSource exit_scratch_text(const std::string& text) noexcept {
200 return ScratchSpanSource({reinterpret_cast<const std::byte*>(text.data()), text.size()});
201}
202
203// The exact source_id + "\x1f" + from_entry bytes without a joined string.
205public:
206 ExitScratchPairText(const std::string& left, const std::string& right) noexcept
207 : parts_{{{reinterpret_cast<const std::byte*>(left.data()), left.size()},
208 {reinterpret_cast<const std::byte*>("\x1f"), 1},
209 {reinterpret_cast<const std::byte*>(right.data()), right.size()}}} {}
210 std::uint64_t size() const noexcept override {
211 return parts_[0].size + 1 + parts_[2].size;
212 }
213 ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override {
214 if (offset > size() || destination.size > size() - offset
215 || (destination.size && !destination.data))
216 return ScratchStatus::programming_error;
217 for (const auto& part : parts_) {
218 if (offset >= part.size) { offset -= part.size; continue; }
219 const auto count = std::min<std::size_t>(destination.size, part.size - offset);
220 if (count) std::memcpy(destination.data, part.data + offset, count);
221 if (count) destination.data += count;
222 destination.size -= count;
223 offset = 0;
224 if (!destination.size) break;
225 }
226 return ScratchStatus::ok;
227 }
228private:
229 std::array<pineforge::detail::ScratchBytes, 3> parts_;
230};
231
233 std::uint64_t left_offset, const ScratchByteSource& right,
234 std::uint64_t right_offset, int& result) noexcept {
235 if (left_offset > left.size() || right_offset > right.size())
236 return ScratchStatus::programming_error;
237 const auto left_size = left.size() - left_offset;
238 const auto right_size = right.size() - right_offset;
239 const auto common = std::min(left_size, right_size);
240 std::array<std::byte, 128> a{}, b{};
241 for (std::uint64_t offset = 0; offset < common;) {
242 const auto count = static_cast<std::size_t>(std::min<std::uint64_t>(
243 a.size(), common - offset));
244 auto status = left.read(left_offset + offset, {a.data(), count});
245 if (status != ScratchStatus::ok) return status;
246 status = right.read(right_offset + offset, {b.data(), count});
247 if (status != ScratchStatus::ok) return status;
248 const int order = std::memcmp(a.data(), b.data(), count);
249 if (order != 0) { result = order; return ScratchStatus::ok; }
250 offset += count;
251 }
252 result = left_size < right_size ? -1 : (right_size < left_size ? 1 : 0);
253 return ScratchStatus::ok;
254}
255inline bool exit_scratch_equal(const ScratchByteSource& left,
256 const ScratchByteSource& right) {
257 if (left.size() != right.size()) return false;
258 int order = 0;
259 const auto status = exit_scratch_compare_text(left, 0, right, 0, order);
260 if (status == ScratchStatus::io_error) {
263 }
264 exit_scratch_check(status);
265 return order == 0;
266}
267inline bool operator==(const ExitScratchText& left, const std::string& right) {
268 return exit_scratch_equal(left, exit_scratch_text(right));
269}
270inline bool operator==(const std::string& left, const ExitScratchText& right) {
271 return right == left;
272}
273inline bool operator!=(const ExitScratchText& left, const std::string& right) {
274 return !(left == right);
275}
276inline bool operator!=(const std::string& left, const ExitScratchText& right) {
277 return !(right == left);
278}
279
280// All rows and secondary indexes use this ONE store. Order is scope, kind,
281// major, text bytes, ordinal. Ordinal breaks exact ties in original queue
282// order. Sources borrow input bytes; no put source reads this store.
283using ExitScratchKeyWords = std::array<std::uint64_t, 4>;
284inline constexpr std::size_t exit_scratch_key_bytes = sizeof(ExitScratchKeyWords);
285class ExitScratchKey final : public ScratchByteSource {
286public:
287 ExitScratchKey(std::uint64_t scope, std::uint64_t kind = 0,
288 std::uint64_t major = 0, std::uint64_t ordinal = 0,
289 const ScratchByteSource* text = nullptr) noexcept
290 : words_{scope, kind, major, ordinal}, text_(text) {}
291 std::uint64_t size() const noexcept override {
292 return exit_scratch_key_bytes + (text_ ? text_->size() : 0);
293 }
294 ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override {
295 if (offset > size() || destination.size > size() - offset
296 || (destination.size && !destination.data))
297 return ScratchStatus::programming_error;
298 if (destination.size && offset < exit_scratch_key_bytes) {
299 const auto count = std::min<std::size_t>(destination.size,
300 exit_scratch_key_bytes - static_cast<std::size_t>(offset));
301 std::memcpy(destination.data,
302 reinterpret_cast<const std::byte*>(words_.data()) + offset, count);
303 destination.data += count;
304 destination.size -= count;
305 offset += count;
306 }
307 return destination.size
308 ? text_->read(offset - exit_scratch_key_bytes, destination) : ScratchStatus::ok;
309 }
310private:
311 ExitScratchKeyWords words_;
312 const ScratchByteSource* text_;
313};
314
315struct ExitScratchOrderStats { std::uint64_t comparisons = 0; };
317 return {0x5832355832370002ULL, stats,
318 [](void* context, const ScratchByteSource& left, const ScratchByteSource& right,
319 int& result) noexcept {
320 if (context) ++static_cast<ExitScratchOrderStats*>(context)->comparisons;
321 ExitScratchKeyWords a{}, b{};
322 if (left.size() < sizeof(a) || right.size() < sizeof(b))
323 return ScratchStatus::programming_error;
324 auto status = left.read(0, {reinterpret_cast<std::byte*>(a.data()), sizeof(a)});
325 if (status != ScratchStatus::ok) return status;
326 status = right.read(0, {reinterpret_cast<std::byte*>(b.data()), sizeof(b)});
327 if (status != ScratchStatus::ok) return status;
328 for (std::size_t i = 0; i < 3; ++i) {
329 if (a[i] == b[i]) continue;
330 result = a[i] < b[i] ? -1 : 1;
331 return ScratchStatus::ok;
332 }
333 status = exit_scratch_compare_text(left, sizeof(a), right, sizeof(b), result);
334 if (status != ScratchStatus::ok || result != 0) return status;
335 result = a[3] < b[3] ? -1 : (b[3] < a[3] ? 1 : 0);
336 return ScratchStatus::ok;
337 }};
338}
339
341 ScratchHandle handle) {
343 ExitScratchKeyWords words{};
344 exit_scratch_check(context.store.read(handle, ScratchField::key, 0,
345 {reinterpret_cast<std::byte*>(words.data()), sizeof(words)}), context.store);
346 return words;
347}
349 const ScratchByteSource& key) {
351 ScratchHandle handle = 0;
352 const auto status = context.store.lower_bound(key, handle);
353 if (status == ScratchStatus::end) return 0;
354 exit_scratch_check(status, context.store);
355 return handle;
356}
358 ScratchStatus status) {
359 // Rollback makes further backing calls. Preserve the first failure before
360 // cleanup can replace the backing's borrowed diagnostic.
361 std::optional<pineforge::detail::ReportScratchIoError> failure;
362 if (status == ScratchStatus::io_error)
363 if (const auto* carried = context.store.carried_failure()) failure = *carried;
364 const auto rollback_status = context.store.rollback();
365 pineforge::detail::rethrow_carried_scratch_failure(failure ? &*failure : nullptr);
366 exit_scratch_check(rollback_status, context.store);
367 exit_scratch_failure(status);
368}
369template<class Edit> void exit_scratch_edit(ExitScratchContext& context, Edit operation) {
371 exit_scratch_check(context.store.begin(), context.store);
372 auto status = operation();
373 if (status == ScratchStatus::ok)
374 status = context.store.commit(ScratchDurability::buffered);
375 if (status != ScratchStatus::ok) exit_scratch_edit_failure(context, status);
376}
377
378// Teardown uses only status-returning store operations. Snapshot the original
379// typed failure before rollback can replace the backing's borrowed diagnostic.
380// Reads include pending erases, so each lookup finds the next live scope row
381// without a resident handle list or a cursor invalidated by erase.
383 std::uint64_t scope, std::uint64_t first_kind, std::uint64_t last_kind,
384 ExitScratchTeardownFailure& failure) noexcept {
385 if (failure.pending()) return;
386 const ExitScratchKey key(scope, first_kind);
387 const auto next = [&](ScratchHandle& handle) noexcept {
388 auto status = store.lower_bound(key, handle);
389 if (status != ScratchStatus::ok) return status;
390 ExitScratchKeyWords words{};
391 status = store.read(handle, ScratchField::key, 0,
392 {reinterpret_cast<std::byte*>(words.data()), sizeof(words)});
393 if (status != ScratchStatus::ok) return status;
394 return words[0] != scope || words[1] > last_kind
395 ? ScratchStatus::end : ScratchStatus::ok;
396 };
397 for (;;) {
398 ScratchHandle handle = 0;
399 auto status = next(handle);
400 if (status == ScratchStatus::end) return;
401 if (status != ScratchStatus::ok) {
402 failure.remember(status, store);
403 return;
404 }
405 status = store.begin();
406 if (status != ScratchStatus::ok) {
407 failure.remember(status, store);
408 return;
409 }
410 for (std::size_t erased = 0; erased < kExitScopeEraseBatch; ++erased) {
411 status = store.erase(handle);
412 if (status != ScratchStatus::ok || erased + 1 == kExitScopeEraseBatch) break;
413 status = next(handle);
414 if (status == ScratchStatus::end) {
415 status = ScratchStatus::ok;
416 break;
417 }
418 if (status != ScratchStatus::ok) break;
419 }
420 if (status == ScratchStatus::ok)
421 status = store.commit(ScratchDurability::buffered);
422 if (status != ScratchStatus::ok) {
423 failure.remember(status, store);
424 const auto rollback_status = store.rollback();
425 failure.remember(rollback_status, store);
426 return;
427 }
428 }
429}
430
432public:
435 if (context.next_scope == std::numeric_limits<std::uint64_t>::max())
436 exit_scratch_failure(ScratchStatus::programming_error);
437 scope_ = ++context.next_scope;
438 }
442 : context_(std::exchange(other.context_, nullptr)), scope_(other.scope_) {}
443 ~ExitScratchScope() noexcept(false) {
444 if (!context_) return;
445 if (!context_->teardown_failure.pending()
446 && !(context_->versions && context_->versions->teardown_failed())) {
447 if (context_->versions) context_->versions->release_scope(scope_);
448 if (!(context_->versions && context_->versions->teardown_failed()))
449 exit_scratch_erase_scope_noexcept(context_->store, scope_, 0,
450 std::numeric_limits<std::uint64_t>::max(), context_->teardown_failure);
451 }
452 // Normal scope exit is a throwing boundary, even for the final scope
453 // in a callback. During unwinding preserve the active exception and
454 // retain the teardown fault for the next scratch/provider boundary.
455 if (std::uncaught_exceptions() == 0) exit_scratch_check_teardown(*context_);
456 }
457 std::uint64_t scope() const noexcept { return scope_; }
458 ExitScratchContext& context() const noexcept { return *context_; }
459 void clear() {
461 exit_scratch_erase_scope_noexcept(context_->store, scope_, 0,
462 std::numeric_limits<std::uint64_t>::max(), context_->teardown_failure);
464 }
465private:
466 ExitScratchContext* context_;
467 std::uint64_t scope_ = 0;
468};
469
470// An ordered index is records in the same store, not a resident side table.
471// Each lookup examines at most one candidate after lower_bound.
473public:
474 explicit ExitScratchIndex(ExitScratchContext& context) : scope_(context) {}
475 explicit ExitScratchIndex(const PineExecutionAdapter& owner) : scope_(exit_scratch(owner)) {}
476 void put(std::uint64_t major, std::uint64_t value, std::uint64_t ordinal = 0) {
477 const ExitScratchKey key(scope_.scope(), 0, major, ordinal);
478 put_key(key, value);
479 }
480 void put(const std::string& text, std::uint64_t value) {
481 const auto bytes = exit_scratch_text(text);
482 put(bytes, value);
483 }
484 void put(const ScratchByteSource& bytes, std::uint64_t value) {
485 const ExitScratchKey key(scope_.scope(), 0, 0, 0, &bytes);
486 put_key(key, value);
487 }
488 static constexpr std::uint64_t absent = std::numeric_limits<std::uint64_t>::max();
489 std::uint64_t find(std::uint64_t major) const {
490 const ExitScratchKey key(scope_.scope(), 0, major);
491 return find_key(key);
492 }
493 std::uint64_t find(const std::string& text) const {
494 const auto bytes = exit_scratch_text(text);
495 return find(bytes);
496 }
497 std::uint64_t find(const ScratchByteSource& bytes) const {
498 const ExitScratchKey key(scope_.scope(), 0, 0, 0, &bytes);
499 return find_key(key);
500 }
501 template<class Visit> void each(Visit visit) const { walk(0, false, visit); }
502 template<class Visit> void each(std::uint64_t major, Visit visit) const {
503 walk(major, true, visit);
504 }
505private:
506 void put_key(const ScratchByteSource& key, std::uint64_t value) {
507 const ScratchSpanSource payload({reinterpret_cast<const std::byte*>(&value), sizeof(value)});
508 auto& context = scope_.context();
509 exit_scratch_edit(context, [&] {
510 ScratchHandle handle = 0;
511 return context.store.put(key, payload, handle);
512 });
513 }
514 std::uint64_t find_key(const ScratchByteSource& key) const {
515 auto& context = scope_.context();
516 const auto handle = exit_scratch_seek(context, key);
517 if (context.observe_lookup)
518 context.observe_lookup(context.observation, handle ? 1 : 0);
519 if (!handle) return absent;
520 const ExitScratchText found(context.store, handle, ScratchField::key);
521 if (!exit_scratch_equal(key, found)) return absent;
522 std::uint64_t value = 0;
523 exit_scratch_check(context.store.read(handle, ScratchField::value, 0,
524 {reinterpret_cast<std::byte*>(&value), sizeof(value)}), context.store);
525 return value;
526 }
527 template<class Visit> void walk(std::uint64_t major, bool restricted, Visit visit) const {
528 auto& context = scope_.context();
529 const ExitScratchKey key(scope_.scope(), 0, major);
530 auto handle = exit_scratch_seek(context, key);
531 while (handle) {
532 const auto words = exit_scratch_words(context, handle);
533 if (words[0] != scope_.scope() || (restricted && words[2] != major)) break;
534 std::uint64_t value = 0;
535 exit_scratch_check(context.store.read(handle, ScratchField::value, 0,
536 {reinterpret_cast<std::byte*>(&value), sizeof(value)}), context.store);
537 if (!visit(value)) break;
538 const auto status = context.store.next(handle, handle);
539 if (status == ScratchStatus::end) break;
540 exit_scratch_check(status, context.store);
541 }
542 }
543 ExitScratchScope scope_;
544};
545
546template<class T> struct ExitScratchValue {
547 static_assert(std::is_trivially_copyable_v<T>,
548 "external payloads require a version, never an object/pointer spill");
549 static ScratchSpanSource source(const T& value) noexcept {
550 return ScratchSpanSource({reinterpret_cast<const std::byte*>(&value), sizeof(value)});
551 }
552 static T read(const ExitScratchContext& context, ScratchHandle handle) {
553 T value{};
554 exit_scratch_check(context.store.read(handle, ScratchField::value, 0,
555 {reinterpret_cast<std::byte*>(&value), sizeof(value)}), context.store);
556 return value;
557 }
558};
559template<> struct ExitScratchValue<std::string> {
560 static ScratchSpanSource source(const std::string& value) noexcept {
561 return exit_scratch_text(value);
562 }
563 static ExitScratchText read(const ExitScratchContext& context, ScratchHandle handle) {
564 return {context.store, handle};
565 }
566};
567
568// Every selected live handle belongs to this run. Keep its lifetime-managed
569// identity once, in the context, rather than spilling a SessionKey pointer.
570template<> struct ExitScratchValue<native_order::RequestHandle> {
572 return ScratchSpanSource({reinterpret_cast<const std::byte*>(&value.incarnation),
573 sizeof(value.incarnation)});
574 }
576 ScratchHandle handle) {
577 std::uint64_t incarnation = 0;
578 exit_scratch_check(context.store.read(handle, ScratchField::value, 0,
579 {reinterpret_cast<std::byte*>(&incarnation), sizeof(incarnation)}), context.store);
580 return {context.run, incarnation};
581 }
582};
583
584// No store visitor survives a call. Per-row mode commits each edit separately;
585// bounded mode can keep a transaction open across appends until finish().
586// Finish construction before a caller cancels, replaces, executes or creates
587// another scope, so nested Applied callbacks need no nested transactions or
588// owning snapshots. Observed construction retains per-row commits.
589enum class ExitScratchRowMode : std::uint8_t {
592};
593
594template<class T, ExitScratchRowMode Mode = ExitScratchRowMode::per_row>
596public:
598 : ExitScratchRows(exit_scratch(owner)) {}
603 : scope_(std::move(other.scope_)), count_(other.count_),
604 batch_open_(std::exchange(other.batch_open_, false)) {}
605 ~ExitScratchRows() noexcept(false) { rollback_batch_noexcept(); }
606 std::uint64_t scope() const noexcept { return scope_.scope(); }
607 ExitScratchContext& context() const noexcept { return scope_.context(); }
608 std::size_t size() const noexcept { return static_cast<std::size_t>(count_); }
609 bool empty() const noexcept { return count_ == 0; }
610
611 void push_back(const T& value) {
612 if constexpr (std::is_same_v<T, native_order::RequestHandle>) {
613 if (value.run != context().run) {
614 if constexpr (Mode == ExitScratchRowMode::bounded_batch)
615 if (batch_open_ && context().store.transaction_active())
616 fail_batch(ScratchStatus::programming_error);
617 exit_scratch_failure(ScratchStatus::programming_error);
618 }
619 }
620 if constexpr (Mode == ExitScratchRowMode::bounded_batch) {
621 if (batch_enabled()) {
622 if (count_ % kExitScopeEraseBatch == 0
623 || !context().store.transaction_active())
624 begin_batch();
625 put_batch(count_, value);
626 ++count_;
627 if (count_ % kExitScopeEraseBatch == 0) finish();
628 } else {
629 finish();
630 put(count_, value);
631 ++count_;
632 }
633 } else {
634 put(count_, value);
635 ++count_;
636 }
637 observe();
638 }
639 // Publish this builder's open partial batch before consumers can mutate
640 // scratch or create another scope. Per-row/finished builders are no-ops.
641 // After an append/finish failure, abandon the builder: a rolled-back batch
642 // can include earlier appends. Destruction rolls back any unfinished batch
643 // before scope erase; it never implicitly publishes construction.
644 void finish() {
645 if constexpr (Mode == ExitScratchRowMode::bounded_batch) {
646 if (!batch_open_) return;
647 if (!context().store.transaction_active()) {
648 batch_open_ = false;
649 return;
650 }
651 try {
653 } catch (...) {
654 rollback_batch_noexcept();
655 throw;
656 }
657 const auto status = context().store.commit(ScratchDurability::buffered);
658 if (status != ScratchStatus::ok)
659 fail_batch(status);
660 batch_open_ = false;
661 }
662 }
663 void set(std::size_t index, const T& value) {
664 static_assert(!std::is_same_v<T, std::string>, "text selections are immutable");
665 finish();
666 put(index, value);
667 }
668 auto operator[](std::size_t index) const {
669 return ExitScratchValue<T>::read(context(), find(index));
670 }
671 auto front() const { return (*this)[0]; }
672 void clear() {
673 finish();
674 scope_.clear();
675 count_ = 0;
676 observe();
677 }
678 template<class Key, class Visit> void ordered(Key key, Visit visit) const {
679 ExitScratchIndex index(context());
680 for (std::size_t i = 0; i < size(); ++i) index.put(key((*this)[i]), i, i);
681 index.each([&](std::uint64_t ordinal) {
682 return visit((*this)[ordinal], static_cast<std::size_t>(ordinal));
683 });
684 }
685 bool contains(const std::string& text) const {
686 static_assert(std::is_same_v<T, std::string>, "text index only");
687 const auto bytes = exit_scratch_text(text);
688 const ExitScratchKey key(scope(), 1, 0, 0, &bytes);
689 const auto handle = exit_scratch_seek(context(), key);
690 if (context().observe_lookup)
691 context().observe_lookup(context().observation, handle ? 1 : 0);
692 if (!handle) return false;
693 const auto words = exit_scratch_words(context(), handle);
694 return words[0] == scope() && words[1] == 1
695 && exit_scratch_equal(bytes, ExitScratchText(context().store, handle,
696 ScratchField::key, exit_scratch_key_bytes));
697 }
698 template<class Visit> void ordered_text(Visit visit, bool unique = false) const {
699 static_assert(std::is_same_v<T, std::string>, "text index only");
700 const ExitScratchKey key(scope(), 1);
701 auto handle = exit_scratch_seek(context(), key);
702 ScratchHandle previous = 0;
703 while (handle) {
704 const auto words = exit_scratch_words(context(), handle);
705 if (words[0] != scope() || words[1] != 1) break;
706 const auto row = find(words[3]);
707 const ExitScratchText text(context().store, row);
708 if (!unique || !previous || !exit_scratch_equal(
709 text, ExitScratchText(context().store, previous))) {
710 if (!visit(text)) break;
711 }
712 previous = row;
713 const auto status = context().store.next(handle, handle);
714 if (status == ScratchStatus::end) break;
715 exit_scratch_check(status, context().store);
716 }
717 }
718
720 using iterator_category = std::input_iterator_tag;
722 std::declval<const ExitScratchContext&>(), ScratchHandle{}));
723 using difference_type = std::ptrdiff_t;
724 using pointer = void;
726 const ExitScratchRows* rows = nullptr;
727 std::size_t index = 0;
728 value_type operator*() const { return (*rows)[index]; }
729 const_iterator& operator++() { ++index; return *this; }
730 const_iterator operator++(int) { auto prior = *this; ++*this; return prior; }
731 bool operator==(const const_iterator& other) const {
732 return rows == other.rows && index == other.index;
733 }
734 bool operator!=(const const_iterator& other) const { return !(*this == other); }
735 };
736 const_iterator begin() const { return {this, 0}; }
737 const_iterator end() const { return {this, size()}; }
738
739private:
740 bool batch_enabled() const noexcept {
741 if constexpr (Mode != ExitScratchRowMode::bounded_batch) {
742 return false;
743 } else {
744 return !context().store.has_io_observer()
745 && context().observe_rows == nullptr
746 && context().observe_lookup == nullptr;
747 }
748 }
749 void begin_batch() {
751 exit_scratch_check(context().store.begin(), context().store);
752 batch_open_ = true;
753 }
754 void fail_batch(ScratchStatus status) {
755 try {
757 } catch (...) {
758 batch_open_ = context().store.transaction_active();
759 throw;
760 }
761 }
762 void rollback_batch_noexcept() noexcept {
763 if constexpr (Mode == ExitScratchRowMode::bounded_batch) {
764 if (!batch_open_) return;
765 batch_open_ = false;
766 if (!context().store.transaction_active()) return;
767 const auto status = context().store.rollback();
768 context().teardown_failure.remember(status, context().store);
769 }
770 }
771 ScratchStatus put_value(std::uint64_t index, const T& value) {
772 const ExitScratchKey key(scope(), 0, 0, index);
773 const auto payload = ExitScratchValue<T>::source(value);
774 ScratchHandle handle = 0;
775 auto status = context().store.put(key, payload, handle);
776 if constexpr (std::is_same_v<T, std::string>) {
777 if (status != ScratchStatus::ok) return status;
778 const ExitScratchKey ordered(scope(), 1, 0, index, &payload);
779 const ScratchSpanSource empty({nullptr, 0});
780 status = context().store.put(ordered, empty, handle);
781 }
782 return status;
783 }
784 void put_batch(std::uint64_t index, const T& value) {
785 const auto status = put_value(index, value);
786 if (status != ScratchStatus::ok)
787 fail_batch(status);
788 }
789 void observe() const noexcept {
790 if (context().observe_rows)
791 context().observe_rows(context().observation, scope(), size());
792 }
793 ScratchHandle find(std::uint64_t index) const {
794 const ExitScratchKey key(scope(), 0, 0, index);
795 const auto handle = exit_scratch_seek(context(), key);
796 if (!handle || exit_scratch_words(context(), handle)
797 != ExitScratchKeyWords{scope(), 0, 0, index})
798 exit_scratch_failure(ScratchStatus::programming_error);
799 return handle;
800 }
801 void put(std::uint64_t index, const T& value) {
803 return put_value(index, value);
804 });
805 }
806 ExitScratchScope scope_;
807 std::uint64_t count_ = 0;
808 // Transient transaction ownership only; never part of durable row state.
809 bool batch_open_ = false;
810};
811
812template<class T>
814
815} // namespace detail
816} // namespace pineforge::source
coded(RunFailureCode code, const RunFailureArgs &args, const std::string &what)
const ReportScratchIoError * carried_failure() const noexcept override
ScratchStatus read(ScratchHandle, ScratchField, std::uint64_t offset, ScratchBuffer destination) const noexcept override
ScratchStatus commit(ScratchDurability durability) noexcept
ScratchStatus next(ScratchHandle current, ScratchHandle &result) const noexcept
ScratchStatus lower_bound(const ScratchByteSource &probe, ScratchHandle &result) const noexcept
ScratchStatus put(const ScratchByteSource &key, const ScratchByteSource &value, ScratchHandle &result) noexcept
virtual const ReportScratchIoError * carried_failure() const noexcept
virtual std::uint64_t size() const noexcept=0
virtual ScratchStatus point(ScratchHandle, ScratchRecordInfo &info) const noexcept=0
virtual const ReportScratchIoError * carried_failure() const noexcept
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
ScratchStatus status() const noexcept
void put(const std::string &text, std::uint64_t value)
void put(std::uint64_t major, std::uint64_t value, std::uint64_t ordinal=0)
std::uint64_t find(std::uint64_t major) const
ExitScratchIndex(const PineExecutionAdapter &owner)
static constexpr std::uint64_t absent
std::uint64_t find(const std::string &text) const
ExitScratchIndex(ExitScratchContext &context)
void each(std::uint64_t major, Visit visit) const
std::uint64_t find(const ScratchByteSource &bytes) const
void put(const ScratchByteSource &bytes, std::uint64_t value)
ExitScratchKey(std::uint64_t scope, std::uint64_t kind=0, std::uint64_t major=0, std::uint64_t ordinal=0, const ScratchByteSource *text=nullptr) noexcept
ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override
std::uint64_t size() const noexcept override
std::uint64_t size() const noexcept override
ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override
ExitScratchPairText(const std::string &left, const std::string &right) noexcept
void ordered(Key key, Visit visit) const
std::uint64_t scope() const noexcept
ExitScratchRows & operator=(const ExitScratchRows &)=delete
void ordered_text(Visit visit, bool unique=false) const
bool contains(const std::string &text) const
auto operator[](std::size_t index) const
ExitScratchRows(const PineExecutionAdapter &owner)
ExitScratchRows(ExitScratchRows &&other) noexcept
void set(std::size_t index, const T &value)
ExitScratchRows(ExitScratchContext &context)
ExitScratchRows(const ExitScratchRows &)=delete
ExitScratchScope(const ExitScratchScope &)=delete
ExitScratchScope(ExitScratchScope &&other) noexcept
ExitScratchContext & context() const noexcept
ExitScratchScope(ExitScratchContext &context)
std::uint64_t scope() const noexcept
ExitScratchScope & operator=(const ExitScratchScope &)=delete
void remember(ScratchStatus status, const ScratchReadback &store) noexcept
const pineforge::detail::ReportScratchIoError * carried_failure() const noexcept override
ScratchStatus read(std::uint64_t offset, ScratchBuffer destination) const noexcept override
ExitScratchText(const ScratchReadback &reader, ScratchHandle handle, ScratchField field=ScratchField::value, std::uint64_t offset=0)
std::uint64_t size() const noexcept override
virtual std::uint64_t placement(std::uint64_t scope, const PlacementSnapshot &)=0
virtual std::uint64_t request(std::uint64_t scope, const native_order::Request &)=0
virtual std::uint64_t pending(std::uint64_t scope, const native_order::Request &, const PlacementSnapshot &, const std::string &key)=0
virtual void pending(std::uint64_t version, native_order::Request &, PlacementSnapshot &, std::string &key) const =0
virtual void release_scope(std::uint64_t scope) noexcept=0
virtual native_order::Request request(std::uint64_t version) const =0
virtual native_order::Owner freeze_openings(const native_order::RunIdentity &, std::int64_t cycle, std::uint64_t count, void *, OpeningReader)=0
virtual PlacementSnapshot placement(std::uint64_t version) const =0
ScratchStatus(*)(void *, std::uint64_t, native_order::RequestHandle &) OpeningReader
std::uint64_t ScratchHandle
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
std::variant< Independent, WaitForApplied, BindOpening, BindOpenings, BindCohort > Owner
ScratchStatus exit_scratch_compare_text(const ScratchByteSource &left, std::uint64_t left_offset, const ScratchByteSource &right, std::uint64_t right_offset, int &result) noexcept
void exit_scratch_lots(ExitScratchContext &scratch, const NativeStrategyHost &host, double mark, Visit visit)
std::array< std::uint64_t, 4 > ExitScratchKeyWords
ExitScratchVersions & exit_scratch_versions(ExitScratchContext &context)
constexpr std::size_t kExitScopeEraseBatch
bool operator==(const ExitScratchText &left, const std::string &right)
ExitScratchContext & exit_scratch(const PineExecutionAdapter &)
void exit_scratch_check(ScratchStatus status)
bool exit_scratch_equal(const ScratchByteSource &left, const ScratchByteSource &right)
ScratchKeyOrder exit_scratch_key_order(ExitScratchOrderStats *stats=nullptr) noexcept
ExitScratchRows< T, ExitScratchRowMode::bounded_batch > ExitScratchBatchRows
void exit_scratch_edit_failure(ExitScratchContext &context, ScratchStatus status)
void exit_scratch_working(ExitScratchContext &scratch, const NativeStrategyHost &host, Visit visit)
ScratchSpanSource exit_scratch_text(const std::string &text) noexcept
void exit_scratch_erase_scope_noexcept(CommandScratchStore &store, std::uint64_t scope, std::uint64_t first_kind, std::uint64_t last_kind, ExitScratchTeardownFailure &failure) noexcept
constexpr std::size_t exit_scratch_key_bytes
ExitScratchKeyWords exit_scratch_words(ExitScratchContext &context, ScratchHandle handle)
ScratchHandle exit_scratch_seek(ExitScratchContext &context, const ScratchByteSource &key)
bool operator!=(const ExitScratchText &left, const std::string &right)
void exit_scratch_failure(ScratchStatus)
void exit_scratch_edit(ExitScratchContext &context, Edit operation)
void exit_scratch_check_teardown(const ExitScratchContext &context)
Owning value row for one open physical lot, copied at query time by native_open_lots(mark) — the lot-...
Owning value row for one live request, copied at query time.
Aggregate field order keeps market construction: Request{Transact{1.0}, "buy", "comment"}...
ScratchStatus(* lots)(const NativeStrategyHost &, double, void *, bool(*)(void *, const NativeOpenLot &))
void(* observe_lookup)(void *, std::size_t candidates) noexcept
void(* observe_rows)(void *, std::uint64_t scope, std::size_t rows) noexcept
ScratchStatus(* working)(const NativeStrategyHost &, void *, bool(*)(void *, const NativeWorkingRequest &))
decltype(ExitScratchValue< T >::read( std::declval< const ExitScratchContext & >(), ScratchHandle{})) value_type
bool operator==(const const_iterator &other) const
bool operator!=(const const_iterator &other) const
static ScratchSpanSource source(const native_order::RequestHandle &value) noexcept
static native_order::RequestHandle read(const ExitScratchContext &context, ScratchHandle handle)
static ScratchSpanSource source(const std::string &value) noexcept
static ExitScratchText read(const ExitScratchContext &context, ScratchHandle handle)
static ScratchSpanSource source(const T &value) noexcept
static T read(const ExitScratchContext &context, ScratchHandle handle)