PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
udt_graph.hpp
Go to the documentation of this file.
1// Generated UDT object graphs over o1::Store.
2//
3// Layout. A graph owns one 8-byte control record (the directory head). Every
4// node is ONE Store object: a fixed 72-byte record (its own id, the graph's
5// control id, the directory link, the schema tag, flags, the payload size,
6// the edge count and two collector/copy links), then the field payload bytes,
7// then the owning edge ids (kNullId is na). The directory threads every node
8// through its record, newest first. No native object directory, reference
9// table, visited set, work queue or recursion stack grows with the graph:
10// tracing and copying keep their work lists in node records and read edges in
11// fixed stack batches. Residency is the Store's fixed workspace; ids are the
12// Store's ids, never reused (not after release, not after a rollback).
13//
14// Resident-O(1): the live graph spills to the scratch backing, which in a
15// jailed run is RAM; flat in bars only for a bounded live graph. The fixed
16// bound covers the Store workspace and transaction handles, not process RSS
17// or a cgroup's memory charge. Backing usage also includes storage metadata,
18// transactional versions and its retained high water; bytes_live is not RSS.
19// The Store fixes the two admission limits a graph meets: its caller handle
20// pool and its live-object limit (both below).
21//
22// Transaction handles: every public operation runs in one Store::scope().
23// Inside a caller's open transaction it joins without a handle; with none open
24// it owns one handle until it returns. Live handles are therefore the caller's
25// explicit begin() handles, independent of graph depth and of how many graph
26// calls a collection operation makes (covered by the depth-six live-handle
27// row), or one while a lone operation runs. Collection code holding graph
28// edges runs each of its operations in store().scope() as well. Scopes
29// flatten; joining is not a savepoint. At most Store::max_caller_handles()
30// explicit handles are live at once: one more begin() throws TxPoolExhausted
31// before it changes any transaction or graph state.
32//
33// This module has no destructor-driven release. close() and collect() are
34// ordinary scope() operations, taking a caller handle slot only when no
35// transaction is open; call them from non-destructor code. A future RAII
36// wrapper that releases graph state in its destructor must use the Store's
37// cleanup slot (begin_cleanup()), which takes no caller handle.
38//
39// Every node is one Store object, so a graph is admitted up to the Store's
40// live-object limit (Shape::max_live_objects, counting the control record):
41// creating one more throws ScratchCapacityError {"resource":"cohort"} before
42// any Store change. The operation that saw it ends as any failed operation
43// does: its own scope rolls back, a joined one leaves the caller rollback-only.
44//
45// After a graph call fails inside its joined scope, the only valid next
46// operation is rollback of the outer scope. copy_deep() can leave copy/work
47// links set until that rollback; do not retry it in the same scope, where a
48// stale copy link still names a clone. Child validation in make()/root()
49// runs before joining: a rejected argument leaves the caller's scope usable
50// unless validation encounters a backing failure.
51//
52// Reclamation requires compiler safe points. If at most F nodes are made
53// per bar, collection is at most K bars apart, and L nodes survive each
54// collection (roots included), the directory has at most L + F*K nodes
55// before a collection. With encoded node sizes at most S, logical bytes
56// are at most 8 + S*(L + F*K). collect() walks that whole directory and
57// every reachable edge: O(directory nodes + reachable edges) Store calls
58// per invocation. It is O(live) when node fan-out is bounded; collecting
59// every bar with a large live set costs O(bars * live) Store calls.
60// No finite bound on garbage between calls exists without a bound on K.
61//
62// What the RUNTIME owns (this module):
63// * ids: stable for every node; a released or rolled-back id is dead and a
64// later make() never answers it (Store ids are monotonic);
65// * field bytes and edges: exact reads/writes, bounds checked before any
66// write, so a cross-end request changes nothing;
67// * na: kNullId is the na object; field access on it stops with
68// pine_na_reference {object: udt_object}; copies of na are na;
69// * copy_shallow (Pine copy()): new id, independent field bytes, the same
70// child ids; copy_deep: a whole owned subgraph with aliases and cycles
71// preserved inside the copy (an explicit operation, never implicit);
72// * reclamation: collect() releases exactly the nodes no registered root
73// reaches, cycles included; an aliased child is one node, released once;
74// * rollback: every node record, payload, edge, root rebinding, directory
75// link and released node is Store state, so the caller's Store transaction
76// restores the exact graph, deletions included;
77// * atomicity: an operation touching several records runs in one Store
78// transaction (joined inside the caller's), so a failure leaves all or none;
79// * hash_state(): every record, payload and edge byte, in directory order;
80// creation-order dependent, for deterministic replay of the same run.
81//
82// What the COMPILER must supply, nothing of which the
83// runtime can infer:
84// * one static UdtType per UDT schema: tag, payload bytes (scalar fields at
85// fixed offsets) and edge count (every reference field at a fixed index);
86// * every owning reference as an edge: nested UDTs, array/map/matrix values
87// holding UDTs, string fields (an immutable string leaf node; assignment
88// rebinds the edge, never mutates a shared leaf). A payload byte must never
89// hide an Id. Unbounded values are nodes, never decoded native vectors;
90// * one root per var/varip variable, series history slot or live temporary
91// that holds a UDT: root() registers it, edge(root, 0) / set_edge(root, 0, id)
92// read and rebind it (na included), drop_root() retires it. Every native
93// temporary holding an Id needs a root before the next collect(). The
94// module cannot detect an unrooted Id held in a native temporary; keeping
95// such a temporary alive across collect() is the compiler's contract;
96// * the safe points where collect() runs (it walks the directory and edges);
97// * the calc-on-fill scope: begin() at snapshot, commit at the committed
98// checkpoint, rollback at restore (engine.hpp snapshot/restore/commit
99// hooks). Native C++ variables holding Ids are NOT rolled back; only roots
100// are. Store scopes flatten; there is no savepoint. varip exemption from a
101// rollback is codegen policy: a Store transaction rolls back every node;
102// * the Pine na guard before field access when the script tests `na(obj)`;
103// * folding hash_state() into the generated script's state hash;
104// * close() after the run, outside any transaction, once nothing borrows ids.
105//
106// Collections (arrays, maps, matrices) that hold UDT
107// values store the element as an Id in an edge of a graph node (one node per
108// collection, edge index = element slot), resize with resize_edges(), and
109// keep their own payload layout in the payload bytes.
110#pragma once
111
114
115#include <cstddef>
116#include <cstdint>
117#include <memory>
118#include <type_traits>
119
120namespace pineforge::detail::o1 {
121
122// One generated descriptor per UDT schema. Only the numeric tag and the two
123// sizes are retained in the graph, never a native descriptor pointer.
124struct UdtType {
125 std::uint64_t tag = 0;
126 std::uint64_t payload_bytes = 0;
127 std::uint64_t edge_count = 0;
128};
129
130class UdtGraph {
131public:
132 // Creates the control record. The Store is borrowed for the graph's life.
133 explicit UdtGraph(Store&);
134 UdtGraph(const UdtGraph&) = delete;
135 UdtGraph& operator=(const UdtGraph&) = delete;
136
137 Store& store() const noexcept { return store_; }
138 Id identity() const noexcept { return control_; }
139 // Releases every node and the control record. Destroying the facade
140 // releases nothing, so ids borrowed elsewhere never dangle silently.
141 void close();
142
143 // A node with zero field bytes and na edges, or with the given initial
144 // field bytes / edge ids (either pointer may be null). Children are
145 // validated before opening a transaction or writing anything.
146 Id make(std::uint64_t payload_bytes, std::uint64_t edge_count = 0);
147 Id make(const UdtType&);
148 Id make(const UdtType&, const void* payload, const Id* children);
149 // True only for a live node of THIS graph; false for na or a dead id. A
150 // backing failure throws (it is never reported as a dead id).
151 bool alive(Id) const;
152 std::uint64_t type_tag(Id) const;
153 std::uint64_t payload_size(Id) const;
154 std::uint64_t edge_count(Id) const;
155
156 // Versioned root: a node with one edge, registered until drop_root().
159 Id edge(Id, std::uint64_t index) const;
160 void edges(Id, std::uint64_t first, Id* out, std::size_t count) const;
161 void set_edge(Id, std::uint64_t index, Id child);
162 // Growth reads as zero bytes / na edges; shrink drops the tail.
163 void resize_edges(Id, std::uint64_t count);
164 void resize_payload(Id, std::uint64_t bytes);
165 // Bounds are validated BEFORE a write: a cross-end request never writes
166 // a prefix. Null access stops as na; dead or foreign access stops too;
167 // neither ever answers zero bytes.
168 void read(Id, std::uint64_t off, void* dst, std::size_t bytes) const;
169 void write(Id, std::uint64_t off, const void* src, std::size_t bytes);
170 // Read a whole generated schema (tag AND both extents must match).
171 // Either output may be null to omit it. Small records use one fixed
172 // staging buffer and one Store read; larger records stream to the
173 // caller's buffers. Header identity and schema are checked before a
174 // short body is reported. A schema mismatch changes neither output.
175 void read_object(Id, const UdtType&, void* payload, Id* children) const;
176 template<class T>
177 T scalar(Id id, std::uint64_t off = 0) const {
178 static_assert(std::is_trivially_copyable_v<T>, "generated scalar encoding");
179 T value{};
180 read(id, off, &value, sizeof(value));
181 return value;
182 }
183 template<class T>
184 void set_scalar(Id id, const T& value, std::uint64_t off = 0) {
185 static_assert(std::is_trivially_copyable_v<T>, "generated scalar encoding");
186 write(id, off, &value, sizeof(value));
187 }
188
189 // Both copies propagate na. Roots are bindings, not copyable objects.
192 // An explicit caller handle (one pool slot); graph operations join it.
193 std::unique_ptr<Tx> begin() { return store_.begin(); }
194 // Returns the number of nodes released.
195 std::uint64_t collect();
196 // Creation-order dependent; compare replays with the same creation order.
197 std::uint64_t hash_state() const;
198
199private:
200 struct Control {
201 Id head = 0;
202 };
203 struct Node {
204 Id self = 0;
205 Id owner = 0;
206 Id next = 0;
207 std::uint64_t type = 0;
208 std::uint64_t flags = 0;
209 std::uint64_t payload_bytes = 0;
210 std::uint64_t edge_count = 0;
211 Id copy = 0; // copy_deep source -> clone link; zero after successful calls
212 Id work = 0; // collect/copy_deep work list; zero after successful calls
213 };
214 static_assert(sizeof(Control) == 8 && sizeof(Node) == 72, "fixed graph records");
215 static_assert(std::is_trivially_copyable_v<Node>, "records are stored as bytes");
216 static constexpr std::uint64_t kRoot = 1;
217 static constexpr std::uint64_t kRegistered = 2;
218 static constexpr std::uint64_t kMarked = 4;
219
220 [[noreturn]] static void fail(const char*);
221 static std::uint64_t edge_bytes(std::uint64_t count);
222 static std::uint64_t object_bytes(std::uint64_t payload_bytes, std::uint64_t edge_count);
223 static std::uint64_t edges_offset(const Node& n) { return sizeof(Node) + n.payload_bytes; }
224 static void check_range(std::uint64_t size, std::uint64_t off, std::size_t bytes,
225 const void* data);
226
227 template<class T>
228 T get(Id object, std::uint64_t off = 0) const {
229 T value{};
230 read_exact(object, off, &value, sizeof(value));
231 return value;
232 }
233 template<class T>
234 void put(Id object, const T& value, std::uint64_t off = 0) {
235 write_exact(object, off, &value, sizeof(value));
236 }
237 void read_exact(Id object, std::uint64_t off, void* dst, std::uint64_t bytes) const;
238 void write_exact(Id object, std::uint64_t off, const void* src, std::uint64_t bytes);
239 void move_bytes(Id object, std::uint64_t from, std::uint64_t to, std::uint64_t bytes);
240 void zero_bytes(Id object, std::uint64_t off, std::uint64_t bytes);
241 void copy_region(Id from, Id to, std::uint64_t off, std::uint64_t bytes);
242
243 bool read_node(Id, Node&) const;
244 Node node(Id) const;
245 void validate_child(Id) const;
246 Id make_node(const UdtType&, const void* payload, const Id* children, std::uint64_t flags);
247 Id clone_node(const Node&);
248
249 Store& store_;
250 Id control_ = 0;
251};
252
253} // namespace pineforge::detail::o1
std::uint64_t type_tag(Id) const
void resize_edges(Id, std::uint64_t count)
UdtGraph(const UdtGraph &)=delete
void set_scalar(Id id, const T &value, std::uint64_t off=0)
T scalar(Id id, std::uint64_t off=0) const
Id identity() const noexcept
void read_object(Id, const UdtType &, void *payload, Id *children) const
void set_edge(Id, std::uint64_t index, Id child)
Id make(const UdtType &, const void *payload, const Id *children)
UdtGraph & operator=(const UdtGraph &)=delete
Id root(Id value=kNullId)
Store & store() const noexcept
Id edge(Id, std::uint64_t index) const
Id make(std::uint64_t payload_bytes, std::uint64_t edge_count=0)
void write(Id, std::uint64_t off, const void *src, std::size_t bytes)
std::unique_ptr< Tx > begin()
void edges(Id, std::uint64_t first, Id *out, std::size_t count) const
void resize_payload(Id, std::uint64_t bytes)
void read(Id, std::uint64_t off, void *dst, std::size_t bytes) const
std::uint64_t hash_state() const
std::uint64_t payload_size(Id) const
std::uint64_t edge_count(Id) const
std::uint64_t Id
Definition o1_store.hpp:47
constexpr Id kNullId
Definition o1_store.hpp:48