PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
o1_store.hpp
Go to the documentation of this file.
1// CR-150 A5 — O(1) resident script-state store contract.
2//
3// PURPOSE. Script-owned state (arrays, strings, map payloads, matrices, UDT graphs,
4// drawing records) grows with bars or with objects the script creates. This header
5// fixes the one interface every A5 lane consumes so that state lives in a fixed
6// resident working set over a spilling backing, addressed by stable ids, with exact
7// transactional versions for calc-on-fill rollback.
8//
9// RULES THIS CONTRACT ENCODES (owner-settled, 1.7.0):
10// - No capacity refusal. A write or resize never fails because the resident window
11// is full; it spills. Callers must not add a cap the old semantics did not have.
12// - Resident bytes are bounded by the ScratchStore workspace picked at open,
13// independent of bars B and of objects created.
14// - Ids are stable and never recycled while reachable; a released id is dead, and
15// reads of a dead id are a caller error, not a silent zero.
16// - Transaction rollback restores every touched root byte-exactly; version chains
17// live in the backing, never in a growing in-heap copy.
18// - Every read and write is exact: no approximation, no dropped old element, no
19// reordering that a script can observe.
20//
21// IMPLEMENTATION SHAPE (CORE-TOP ruling L6, 2026-10-21). This interface is a THIN
22// OBJECT LAYER over pineforge::detail::ScratchStore, so the project has one page
23// cache and one backing boundary. An object is a header record plus fixed-size data
24// chunk records, each record keyed by the 16-byte big-endian pair (object id, chunk
25// number) and ordered by a plain bytewise compare. Chunk 0's value is the 16-byte
26// header (logical size, alignment) followed by that chunk's data bytes; every later
27// chunk's value is its data bytes. The layer keeps no per-object, per-chunk or
28// per-byte heap state: an id counter, a few scalars and fixed buffers only.
29//
30// DECLARATIONS ONLY. The single implementation is src/detail/o1_store.cpp on the
31// lane that owns this file; other lanes bind to this header and must not fork it.
32
33#ifndef PINEFORGE_DETAIL_O1_STORE_HPP
34#define PINEFORGE_DETAIL_O1_STORE_HPP
35
36#include <cstddef>
37#include <cstdint>
38#include <memory>
39
41
42namespace pineforge {
43namespace detail {
44namespace o1 {
45
46using Id = std::uint64_t;
47inline constexpr Id kNullId = UINT64_C(0);
48
49// Fixed chunk-byte and resident-page-COUNT bounds, not a resident-byte cap.
50// ScratchStore resident = (resident_pages + 6 work pages + metadata in pages)
51// x page_bytes: at most about 1 GiB at its own 1 MiB page-size limit.
52// Larger chunk_bytes/resident_pages are caller errors; raising either bound
53// needs CR150-CX's review.
54inline constexpr std::uint32_t kMaxChunkBytes = 64u * 1024u;
55inline constexpr std::uint32_t kMaxResidentPages = 1024u;
56
57struct Shape {
58 std::uint32_t page_bytes = 4096; // ScratchStore page size; must equal the backing's
59 std::uint32_t resident_pages = 64; // ScratchStore resident cache pages; fixed at open
60 // EXT: one data chunk record covers this many object bytes, so it is the backing
61 // cost of one element write and the object's rounding granularity. Appended after
62 // the first contract; the fields above keep their meaning and defaults.
63 std::uint32_t chunk_bytes = 1024;
64 // EXT: ScratchStore requires these counts >= 1 (zero is normalized to 1 here).
65 // Otherwise passed through; ScratchStore's workspace sizing never reads them.
66 std::uint32_t journal_slots = 8;
67 std::uint32_t index_slots = 8;
68};
69
70struct Stats {
71 std::uint64_t bytes_live = 0; // logical bytes held by live ids
72 std::uint64_t bytes_resident = 0; // fixed ScratchStore workspace, constant per store
73 std::uint64_t pages_spilled = 0; // backing pages currently allocated
74 std::uint64_t bytes_backing = 0; // file growth, never charged as resident
75};
76
77// Transactional root for calc-on-fill rollback. All mutations between begin() and
78// commit() belong to a new version; rollback() restores the previous version of
79// every object exactly.
80//
81// One outer transaction at a time, flattened. begin() with no active transaction
82// maps to the ScratchStore transaction. begin() while one is active returns a
83// JOINED scope: its commit() is a no-op, its rollback() marks the outer transaction
84// rollback-only and undoes nothing by itself, and destroying it uncommitted marks
85// the outer transaction rollback-only too. While rollback-only begin() and every
86// mutation throw std::logic_error; the outer commit() performs the real rollback
87// and then throws the same error; the outer rollback() always works. There is no
88// savepoint. A backing failure in any call, including a read-only one, makes the
89// open outer transaction rollback-only. Destroy all Tx handles before the next
90// outer begin(): destroying an unfinished stale outer handle later can roll that
91// NEW transaction back.
92class Tx {
93 public:
94 virtual ~Tx();
95 virtual void commit() = 0;
96 virtual void rollback() = 0;
97};
98
99// Single-threaded: read() const mutates its key and chunk buffers.
100class Store {
101 public:
102 virtual ~Store();
103
104 // Opens a fresh object layer over one borrowed backing. The backing must be
105 // empty (create_empty) and its page size must equal Shape::page_bytes.
106 static std::unique_ptr<Store> open(const Shape&, ScratchBacking&);
107
108 // Object lifecycle. `bytes` is the initial logical size; `align` is a power of two.
109 virtual Id create(std::uint64_t bytes, std::uint64_t align) = 0;
110 virtual void release(Id) = 0; // id becomes dead; never reused
111 virtual bool alive(Id) const = 0;
112 virtual std::uint64_t size(Id) const = 0; // logical bytes
113
114 // Resize preserves contents up to min(old, new) and never refuses. Growth is
115 // backed by the backing, not by the resident window.
116 virtual bool resize(Id, std::uint64_t bytes) = 0;
117
118 // Exact page-granular access. Return the byte count moved; a short count means
119 // the request crossed the logical end, which the caller must treat as an error.
120 // A mutating call outside a transaction runs as its own one-call transaction.
121 virtual std::size_t read(Id, std::uint64_t off, void* dst, std::size_t n) const = 0;
122 virtual std::size_t write(Id, std::uint64_t off, const void* src, std::size_t n) = 0;
123
124 // Each successful begin() makes exactly one heap allocation for its Tx handle.
125 // Other successful operations reuse the workspace allocated at open().
126 virtual std::unique_ptr<Tx> begin() = 0;
127
128 virtual Stats stats() const = 0;
129};
130
131} // namespace o1
132} // namespace detail
133} // namespace pineforge
134
135#endif // PINEFORGE_DETAIL_O1_STORE_HPP
virtual Stats stats() const =0
virtual std::size_t write(Id, std::uint64_t off, const void *src, std::size_t n)=0
virtual std::unique_ptr< Tx > begin()=0
virtual std::uint64_t size(Id) const =0
virtual std::size_t read(Id, std::uint64_t off, void *dst, std::size_t n) const =0
virtual bool alive(Id) const =0
virtual bool resize(Id, std::uint64_t bytes)=0
static std::unique_ptr< Store > open(const Shape &, ScratchBacking &)
virtual Id create(std::uint64_t bytes, std::uint64_t align)=0
virtual void release(Id)=0
virtual void rollback()=0
virtual void commit()=0
constexpr std::uint32_t kMaxResidentPages
Definition o1_store.hpp:55
std::uint64_t Id
Definition o1_store.hpp:46
constexpr std::uint32_t kMaxChunkBytes
Definition o1_store.hpp:54
constexpr Id kNullId
Definition o1_store.hpp:47