PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
series.hpp
Go to the documentation of this file.
1#pragma once
5#include <array>
6#include <vector>
7#include <algorithm>
8#include <cstddef>
9#include <cstdint>
10#include <exception>
11#include <limits>
12#include <memory>
13#include <mutex>
14#include <new>
15#include <optional>
16#include <stdexcept>
17#include <type_traits>
18#include <utility>
19#include "na.hpp"
20
21namespace pineforge {
22
23// Optional numeric-window residency, in 1024-value pages. The default keeps
24// today's owning in-memory window. A finite smaller cache uses scratch backing;
25// zero bypasses the cache. Neither setting changes logical history capacity.
27 static constexpr std::size_t kKeepResident = std::numeric_limits<std::size_t>::max();
28 std::size_t pages = kKeepResident;
29};
30
32 static thread_local WindowCacheCapacity capacity;
33 return capacity;
34}
35
36// Constructor default for call sites built inside an evaluation context.
37// Objects latch it at construction; restoring the scope cannot resize them.
39 WindowCacheCapacity previous_;
40public:
42 : previous_(window_cache_capacity()) { window_cache_capacity() = capacity; }
46};
47
49 std::uint64_t hits = 0, misses = 0, evictions = 0, bypass_reads = 0;
50 std::uint64_t backing_reads = 0, backing_read_bytes = 0;
51 std::uint64_t backing_writes = 0, backing_write_bytes = 0;
53 std::uint64_t allocation_bypasses = 0, io_errors = 0;
55 std::size_t workspace_bytes = 0;
56 bool scratch_backed = false;
57};
58
59namespace detail {
60
61// Typed window backing and copy-out access through the shared page cache.
62// Non-arithmetic values keep owning typed pages: an object handle cannot be
63// serialized as native bytes without its own codec/lifetime contract.
64template<class T>
66public:
67 static constexpr std::size_t kChunk = 1024;
68
70 : capacity_(capacity) {}
71 WindowValues(std::size_t size, T initial,
73 : capacity_(capacity) { reset(size, std::move(initial)); }
75 : capacity_(other.capacity_), size_(other.size_), initial_(other.initial_),
76 spilled_(other.spilled_), pages_(other.pages_), backing_(other.backing_) {}
78 if (this != &other) {
79 WindowValues copy(other);
80 *this = std::move(copy);
81 }
82 return *this;
83 }
84 WindowValues(WindowValues&& other) noexcept(std::is_nothrow_move_constructible_v<T>)
85 : capacity_(other.capacity_), size_(other.size_), initial_(std::move(other.initial_)),
86 spilled_(other.spilled_), pages_(std::move(other.pages_)),
87 backing_(std::move(other.backing_)), stats_(other.stats_) {
88 absorb_cache_stats(other.read_cache_stats());
89 other.clear_moved_from();
90 }
92 noexcept(std::is_nothrow_move_assignable_v<T>) {
93 if (this != &other) {
94 // ReadThroughCache is immovable and its callback names this owner.
95 // Reconstruct lazily instead of retaining the old owner's context.
96 cache_.reset();
97 cache_failure_ = nullptr;
98 capacity_ = other.capacity_;
99 size_ = other.size_;
100 initial_ = std::move(other.initial_);
101 spilled_ = other.spilled_;
102 pages_ = std::move(other.pages_);
103 backing_ = std::move(other.backing_);
104 stats_ = other.stats_;
105 absorb_cache_stats(other.read_cache_stats());
106 other.clear_moved_from();
107 }
108 return *this;
109 }
110
111 void reset(std::size_t size, T initial) {
112 // This is representable storage geometry, never a data/cache limit.
113 if (size > std::numeric_limits<std::size_t>::max() / sizeof(T))
115 {{"resource", "layout"}}, "window byte extent is not representable");
116 size_ = size;
117 initial_ = std::move(initial);
118 spilled_ = std::is_arithmetic_v<T> && capacity_.pages < page_count();
119 pages_.clear();
120 backing_.reset();
121 cache_.reset();
122 cache_failure_ = nullptr;
123 stats_ = {};
124 if (!spilled_) pages_.resize(page_count());
125 }
126
127 T read(std::size_t index) const {
128 const std::size_t page = index / kChunk, offset = index % kChunk;
129 if (!spilled_) return pages_[page] ? pages_[page]->values[offset] : initial_;
130 if constexpr (std::is_arithmetic_v<T>) {
131 // An unopened backing is the authoritative uniform initial value.
132 if (!backing_) return initial_;
133 ensure_cache();
134 std::array<T, kChunk> transfer;
135 cache_failure_ = nullptr;
136 const auto status = cache_->get(cache_key(page),
137 {reinterpret_cast<std::byte*>(transfer.data()), page_size(page) * sizeof(T)});
138 if (cache_failure_)
139 std::rethrow_exception(std::exchange(cache_failure_, {}));
140 require_cache_status(status);
141 return transfer[offset];
142 }
143 return initial_; // Non-arithmetic storage always takes the owning path.
144 }
145
146 void write(std::size_t index, T value) {
147 const std::size_t page = index / kChunk, offset = index % kChunk;
148 if (!spilled_) {
149 auto& stored = pages_[page];
150 if (!stored) stored = std::make_shared<Page>(page_size(page), initial_);
151 else if (!stored.unique()) stored = std::make_shared<Page>(*stored);
152 stored->values[offset] = std::move(value);
153 return;
154 }
155 if constexpr (std::is_arithmetic_v<T>) {
156 // A failed write cannot leave an old cache tag answering success.
157 // Scalar writes are not page prefixes: invalidate, then retain the
158 // exact existing write without a speculative read/modify/write.
159 if (cache_) require_cache_status(cache_->invalidate(cache_key(page)));
160 make_writable_backing();
161 write_backing(*backing_, index, &value, 1);
162 }
163 }
164
165 // The owner has semantically retired this entire page. Copies retain
166 // their own ownership; ordinary cache eviction never calls this method.
167 void retire_page(std::size_t page) {
168 if (!spilled_) pages_[page].reset();
169 if (cache_) require_cache_status(cache_->invalidate(cache_key(page)));
170 }
171
172 std::size_t size() const { return size_; }
173 WindowCacheCapacity cache_capacity() const { return capacity_; }
175 const auto bytes = sizeof(T) * kChunk;
176 const auto line = ReadThroughCache::line_bytes(bytes);
177 const auto requested = spilled_ ? std::min(capacity_.pages, page_count()) : 0;
178 // An unrepresentable optional budget reduces residency, never data.
179 const auto slots = line ? std::min(requested,
180 std::numeric_limits<std::size_t>::max() / line) : 0;
181 return {slots * line, bytes, ReadThroughCacheReplacement::direct_mapped,
183 }
185 return cache_ ? cache_->stats() : ReadThroughCacheStats{};
186 }
188 auto result = stats_;
189 const auto shared = read_cache_stats();
190 add_counter(result.hits, shared.hits);
191 add_counter(result.misses, shared.misses);
192 add_counter(result.evictions, shared.evictions);
193 add_counter(result.bypass_reads, shared.bypass_reads);
194 add_counter(result.allocation_bypasses, shared.allocation_bypass_count);
195 result.scratch_backed = spilled_;
196 result.workspace_bytes = spilled_ ? sizeof(T) * kChunk : 0;
197 result.resident_metadata_bytes = sizeof(*this) +
198 pages_.capacity() * sizeof(std::shared_ptr<Page>) +
199 shared.current_metadata_bytes;
200 result.resident_payload_bytes = shared.current_payload_bytes;
201 for (const auto& page : pages_) {
202 if (!page) continue;
203 if constexpr (std::is_same_v<T, bool>) {
204 constexpr auto bits_per_byte = std::numeric_limits<unsigned char>::digits;
205 result.resident_payload_bytes += page->values.capacity() / bits_per_byte +
206 (page->values.capacity() % bits_per_byte != 0);
207 } else {
208 result.resident_payload_bytes += page->values.capacity() * sizeof(T);
209 }
210 result.resident_metadata_bytes += sizeof(Page);
211 }
212 if (backing_) result.resident_metadata_bytes += sizeof(Backing);
213 return result;
214 }
215
216private:
217 struct Page {
218 std::vector<T> values;
219 Page(std::size_t size, const T& initial) : values(size, initial) {}
220 };
221 struct Backing {
222 ScratchFile file{ScratchFile::IoRaiser::auxiliary};
223 // Checkpoint copies can read one backing from distinct owners.
224 std::mutex io_mutex;
225 Backing() {
226 file.bound_cache();
227 file.open("pineforge-window-");
228 }
229 };
230 std::size_t page_count() const noexcept { return size_ / kChunk + (size_ % kChunk != 0); }
231 std::size_t page_size(std::size_t page) const {
232 return std::min(kChunk, size_ - page * kChunk);
233 }
234 void ensure_cache() const {
235 if (cache_) return;
236 cache_.emplace(read_cache_policy(),
237 ReadThroughCacheBacking{const_cast<WindowValues*>(this), &cache_read, nullptr});
238 require_cache_status(cache_->status());
239 }
240 static ReadThroughCacheKey cache_key(std::size_t page) noexcept {
241 // A cache belongs to one owner/backing snapshot. Reset/copy/move
242 // discard it; cloning preserves all bytes except the invalidated page.
243 return {1, 0, static_cast<std::uint64_t>(page)};
244 }
245 static ReadThroughCacheIoResult cache_read(void* context, ReadThroughCacheKey key,
246 ScratchBuffer destination) noexcept {
247 auto& owner = *static_cast<WindowValues*>(context);
248 if (key.backing_identity != 1 || key.generation != 0
249 || key.page_number >= owner.page_count() || !destination.data
250 || !destination.size || destination.size % sizeof(T) != 0
251 || destination.size > owner.page_size(static_cast<std::size_t>(key.page_number)) * sizeof(T))
253 try {
254 owner.read_backing(static_cast<std::size_t>(key.page_number) * kChunk,
255 destination.data, destination.size / sizeof(T));
256 return {ScratchStatus::ok, destination.size};
257 } catch (...) {
258 // The status port is noexcept. Rethrow the original dynamic type,
259 // errno and arena/resource fields immediately after get returns.
260 owner.cache_failure_ = std::current_exception();
262 }
263 }
264 static void require_cache_status(ScratchStatus status) {
266 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
267 "window read cache contract failed");
268 }
269 static void add_counter(std::uint64_t& value, std::uint64_t count) noexcept {
270 value += std::min(count, std::numeric_limits<std::uint64_t>::max() - value);
271 }
272 void absorb_cache_stats(const ReadThroughCacheStats& shared) noexcept {
273 add_counter(stats_.hits, shared.hits);
274 add_counter(stats_.misses, shared.misses);
275 add_counter(stats_.evictions, shared.evictions);
276 add_counter(stats_.bypass_reads, shared.bypass_reads);
277 add_counter(stats_.allocation_bypasses, shared.allocation_bypass_count);
278 }
279 void clear_moved_from() noexcept {
280 cache_.reset();
281 cache_failure_ = nullptr;
282 pages_.clear();
283 size_ = 0;
284 spilled_ = false;
285 stats_ = {};
286 }
287 void read_backing(std::size_t index, void* values, std::size_t count) const {
288 std::lock_guard<std::mutex> lock(backing_->io_mutex);
289 add_counter(stats_.backing_read_attempts, 1);
290 try { backing_->file.read(index * sizeof(T), values, count * sizeof(T)); }
291 catch (const std::system_error&) { add_counter(stats_.io_errors, 1); throw; }
292 add_counter(stats_.backing_reads, 1);
293 add_counter(stats_.backing_read_bytes, count * sizeof(T));
294 }
295 void write_backing(Backing& backing, std::size_t index, const T* values,
296 std::size_t count) {
297 std::lock_guard<std::mutex> lock(backing.io_mutex);
298 add_counter(stats_.backing_write_attempts, 1);
299 try { backing.file.write(index * sizeof(T), values, count * sizeof(T)); }
300 catch (const std::system_error&) { add_counter(stats_.io_errors, 1); throw; }
301 add_counter(stats_.backing_writes, 1);
302 add_counter(stats_.backing_write_bytes, count * sizeof(T));
303 }
304 void make_writable_backing() {
305 if (backing_ && backing_.unique()) return;
306 auto replacement = std::make_shared<Backing>();
307 std::array<T, kChunk> transfer;
308 if (!backing_) transfer.fill(initial_);
309 for (std::size_t page = 0; page != page_count(); ++page) {
310 const auto count = page_size(page);
311 if (backing_) read_backing(page * kChunk, transfer.data(), count);
312 write_backing(*replacement, page * kChunk, transfer.data(), count);
313 }
314 // Copies/checkpoints share immutable bytes until a successful clone.
315 backing_ = std::move(replacement);
316 }
317
318 WindowCacheCapacity capacity_;
319 std::size_t size_ = 0;
320 T initial_ = na<T>();
321 bool spilled_ = false;
322 std::vector<std::shared_ptr<Page>> pages_;
323 std::shared_ptr<Backing> backing_;
324 mutable std::optional<ReadThroughCache> cache_;
325 mutable std::exception_ptr cache_failure_;
326 mutable WindowCacheStats stats_;
327};
328
329} // namespace detail
330
331template<typename T>
334 std::size_t head_ = 0;
335 std::size_t size_ = 0;
336 std::size_t capacity_ = 0;
337
338public:
339 explicit DynamicRingBuffer(std::size_t capacity,
341 : buffer_(cache), capacity_(capacity) {}
342
343 void push_front(T val) {
344 if (capacity_ == 0) return;
345 // Many declared Series never execute. Materialize on first write.
346 if (buffer_.size() == 0) buffer_.reset(capacity_, na<T>());
347 if (size_ == 0) {
348 buffer_.write(0, val);
349 size_ = 1;
350 head_ = 0;
351 } else {
352 const auto next = (head_ == 0) ? capacity_ - 1 : head_ - 1;
353 buffer_.write(next, val);
354 head_ = next;
355 if (size_ < capacity_) {
356 size_++;
357 }
358 }
359 }
360
361 void update_front(T val) {
362 if (size_ == 0) {
363 push_front(val);
364 } else {
365 buffer_.write(head_, val);
366 }
367 }
368
369 T operator[](std::size_t offset) const {
370 if (offset >= size_ || capacity_ == 0) {
371 return na<T>();
372 }
373 // offset < size_ <= capacity_ and head_ < capacity_, so
374 // head_ + offset < 2 * capacity_: one conditional subtract
375 // replaces the hardware divide of `% capacity_`.
376 std::size_t idx = head_ + offset;
377 if (idx >= capacity_) idx -= capacity_;
378 return buffer_.read(idx);
379 }
380
381 std::size_t size() const { return size_; }
382 std::size_t capacity() const { return capacity_; }
383 WindowCacheCapacity cache_capacity() const { return buffer_.cache_capacity(); }
384 WindowCacheStats cache_stats() const { return buffer_.cache_stats(); }
385 // Optional read lines only; cache_stats() also counts fixed owner metadata.
387 return buffer_.read_cache_stats();
388 }
389
390 void clear() {
391 head_ = 0;
392 size_ = 0;
393 buffer_.reset(0, na<T>());
394 }
395
396 void resize(std::size_t new_capacity) {
397 if (new_capacity == capacity_) return;
398 detail::WindowValues<T> new_buffer(buffer_.cache_capacity());
399 std::size_t new_size = std::min(size_, new_capacity);
400 if (new_size != 0) new_buffer.reset(new_capacity, na<T>());
401 for (std::size_t i = 0; i < new_size; ++i) {
402 new_buffer.write(i, (*this)[i]);
403 }
404 buffer_ = std::move(new_buffer);
405 capacity_ = new_capacity;
406 head_ = 0;
407 size_ = new_size;
408 }
409};
410
411/*
412 * PineScript history indexing for transpiled series (close, open, user vars, etc.)
413 *
414 * Pine uses expr[k] with k = 0 on the current bar, k >= 1 meaning k bars into the past
415 * (close[1] = previous bar). Newest values live at the front of the ring; operator[](k)
416 * reads k steps into the past. Codegen emits the same k for _s_close[k] and Series vars.
417 *
418 * push() — new bar; update() — same bar (magnifier intrabar). Negative or out-of-range
419 * offsets return na (insufficient history).
420 */
421
422template<typename T>
423class Series {
425
426public:
427 explicit Series(int max_len = 500, WindowCacheCapacity cache = window_cache_capacity())
428 : buf(max_len, cache) {}
429
430 void push(T value) {
431 buf.push_front(value);
432 }
433
434 void update(T value) {
435 buf.update_front(value);
436 }
437
438 T operator[](int offset) const {
439 if (offset < 0) {
440 return na<T>();
441 }
442 return buf[static_cast<std::size_t>(offset)];
443 }
444
445 T current() const {
446 return (*this)[0];
447 }
448
449 int size() const {
450 return static_cast<int>(buf.size());
451 }
452 WindowCacheCapacity cache_capacity() const { return buf.cache_capacity(); }
453 WindowCacheStats cache_stats() const { return buf.cache_stats(); }
454
455 void clear() {
456 buf.clear();
457 }
458};
459
460} // namespace pineforge
WindowCacheStats cache_stats() const
Definition series.hpp:384
detail::ReadThroughCacheStats read_cache_stats() const noexcept
Definition series.hpp:386
T operator[](std::size_t offset) const
Definition series.hpp:369
DynamicRingBuffer(std::size_t capacity, WindowCacheCapacity cache=window_cache_capacity())
Definition series.hpp:339
std::size_t size() const
Definition series.hpp:381
std::size_t capacity() const
Definition series.hpp:382
void resize(std::size_t new_capacity)
Definition series.hpp:396
WindowCacheCapacity cache_capacity() const
Definition series.hpp:383
T operator[](int offset) const
Definition series.hpp:438
void push(T value)
Definition series.hpp:430
int size() const
Definition series.hpp:449
T current() const
Definition series.hpp:445
WindowCacheCapacity cache_capacity() const
Definition series.hpp:452
WindowCacheStats cache_stats() const
Definition series.hpp:453
void update(T value)
Definition series.hpp:434
Series(int max_len=500, WindowCacheCapacity cache=window_cache_capacity())
Definition series.hpp:427
WindowCacheScope & operator=(const WindowCacheScope &)=delete
WindowCacheScope(const WindowCacheScope &)=delete
WindowCacheScope(WindowCacheCapacity capacity)
Definition series.hpp:41
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
static std::size_t line_bytes(std::size_t page_size) noexcept
void retire_page(std::size_t page)
Definition series.hpp:167
WindowValues(const WindowValues &other)
Definition series.hpp:74
ReadThroughCacheStats read_cache_stats() const noexcept
Definition series.hpp:184
void reset(std::size_t size, T initial)
Definition series.hpp:111
WindowValues & operator=(WindowValues &&other) noexcept(std::is_nothrow_move_assignable_v< T >)
Definition series.hpp:91
std::size_t size() const
Definition series.hpp:172
WindowValues(WindowCacheCapacity capacity=window_cache_capacity())
Definition series.hpp:69
WindowValues(WindowValues &&other) noexcept(std::is_nothrow_move_constructible_v< T >)
Definition series.hpp:84
WindowValues & operator=(const WindowValues &other)
Definition series.hpp:77
WindowCacheStats cache_stats() const
Definition series.hpp:187
WindowCacheCapacity cache_capacity() const
Definition series.hpp:173
static constexpr std::size_t kChunk
Definition series.hpp:67
T read(std::size_t index) const
Definition series.hpp:127
WindowValues(std::size_t size, T initial, WindowCacheCapacity capacity=window_cache_capacity())
Definition series.hpp:71
void write(std::size_t index, T value)
Definition series.hpp:146
ReadThroughCachePolicy read_cache_policy() const noexcept
Definition series.hpp:174
std::uint64_t page(std::uint64_t bytes)
WindowCacheCapacity & window_cache_capacity()
Definition series.hpp:31
T na()
Definition na.hpp:54
static constexpr std::size_t kKeepResident
Definition series.hpp:27
std::uint64_t bypass_reads
Definition series.hpp:49
std::uint64_t backing_write_bytes
Definition series.hpp:51
std::size_t resident_metadata_bytes
Definition series.hpp:54
std::size_t resident_payload_bytes
Definition series.hpp:54
std::size_t workspace_bytes
Definition series.hpp:55
std::uint64_t backing_write_attempts
Definition series.hpp:52
std::uint64_t io_errors
Definition series.hpp:53
std::uint64_t backing_read_bytes
Definition series.hpp:50
std::uint64_t evictions
Definition series.hpp:49
std::uint64_t backing_writes
Definition series.hpp:51
std::uint64_t backing_read_attempts
Definition series.hpp:52
std::uint64_t backing_reads
Definition series.hpp:50
std::uint64_t allocation_bypasses
Definition series.hpp:53