PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
scratch_slab.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cerrno>
9#include <cstddef>
10#include <cstdint>
11#include <cstring>
12#include <limits>
13#include <new>
14#include <sys/types.h>
15
16namespace pineforge::detail {
17
18inline constexpr std::size_t kScratchRegionCount = 9;
19inline constexpr std::uint64_t kScratchPageSize = 4096;
20
21struct Region {
22 std::uint8_t* base;
23 std::uint64_t capacity;
24 std::uint64_t logical_extent;
26};
27
30 std::uint64_t offset;
31 std::uint64_t capacity; // Exact admitted bytes; page padding is not capacity.
32 std::uint64_t span;
33};
34
35struct Layout {
36 std::array<RegionLayout, kScratchRegionCount> regions{};
37 std::uint64_t slab_size = kScratchPageSize;
38};
39
41inline std::uint64_t add(std::uint64_t left, std::uint64_t right) {
42 if (right > std::numeric_limits<std::uint64_t>::max() - left)
44 return left + right;
45}
46inline std::uint64_t multiply(std::uint64_t left, std::uint64_t right) {
47 if (left != 0 && right > std::numeric_limits<std::uint64_t>::max() / left)
49 return left * right;
50}
51inline std::uint64_t slots(std::uint64_t keys) {
52 if (keys == 0) return 0;
53 const auto minimum = std::max(std::uint64_t{64}, multiply(2, keys));
54 std::uint64_t result = 64;
55 while (result < minimum) result = multiply(result, 2);
56 return result;
57}
58inline std::uint64_t page(std::uint64_t bytes) {
59 const auto pages = add(bytes / kScratchPageSize,
60 bytes % kScratchPageSize == 0 ? 0 : 1);
61 return multiply(pages, kScratchPageSize);
62}
63template<class HostSize>
64inline bool fits(std::uint64_t bytes) noexcept {
65 if constexpr (std::numeric_limits<HostSize>::digits
66 < std::numeric_limits<std::uint64_t>::digits)
67 return bytes <= static_cast<std::uint64_t>(std::numeric_limits<HostSize>::max());
68 else
69 return true;
70}
71} // namespace scratch_layout_detail
72
73// No allocation or mutation. Every intermediate, page ceiling and prefix sum
74// is checked before the caller can allocate or publish any run ownership.
75inline Layout compute_layout(const ScratchManifest& manifest) {
76 using namespace scratch_layout_detail;
77 if (manifest.V > 27) throw ScratchCapacityError(ScratchResource::layout);
78 if (manifest.K_q != 0 || manifest.B_q != 0) throw ScratchCapacityError(ScratchResource::layout);
79 const std::array<std::uint64_t, kScratchRegionCount> capacities{{
80 add(multiply(40, manifest.K_q), manifest.B_q),
81 multiply(16, slots(manifest.K_q)),
82 add(multiply(40, manifest.K_e), manifest.B_e),
83 multiply(16, slots(manifest.K_e)),
84 add(multiply(40, manifest.K_c), manifest.B_c),
85 multiply(16, slots(manifest.K_c)),
86 manifest.U == 0 ? 0 : add(add(1, multiply(96, manifest.U)), manifest.B_U),
87 manifest.V == 0 ? 0 : add(add(1, multiply(60, manifest.V)), manifest.B_V),
88 multiply(16, manifest.R),
89 }};
91 std::uint64_t offset = 0;
92 for (std::size_t i = 0; i < capacities.size(); ++i) {
93 const auto span = page(capacities[i]);
94 layout.regions[i] = {static_cast<ScratchResource>(i + 1), offset,
95 capacities[i], span};
96 offset = add(offset, span);
97 }
98 layout.slab_size = std::max(kScratchPageSize, offset);
99 if (!fits<std::size_t>(layout.slab_size) || !fits<off_t>(layout.slab_size))
101 return layout;
102}
103
104// One 4096-aligned backing allocation, never grown, moved or replaced by an
105// operation on this object. Region metadata is outside that allocation.
106// Binders retain Region identities, not byte views: base is directory metadata
107// only. Callers must not dereference/cache base or retain a pointer into the
108// slab; all payload access copies through read_at/write_at. This also permits
109// a future mapped-window byte store without exposing window lifetimes.
111public:
113 : manifest_(manifest), layout_(compute_layout(manifest)),
114 allocation_(static_cast<std::uint8_t*>(::operator new(
115 static_cast<std::size_t>(layout_.slab_size),
116 std::align_val_t{static_cast<std::size_t>(kScratchPageSize)}))) {
117 for (std::size_t i = 0; i < regions_.size(); ++i) {
118 const auto& item = layout_.regions[i];
119 regions_[i] = {allocation_ + static_cast<std::size_t>(item.offset),
120 item.capacity, 0, item.resource};
121 }
122 zero();
123 }
125 ::operator delete(allocation_,
126 std::align_val_t{static_cast<std::size_t>(kScratchPageSize)});
127 }
128 ScratchSlab(const ScratchSlab&) = delete;
132
133 const ScratchManifest& manifest() const noexcept { return manifest_; }
134 const Layout& layout() const noexcept { return layout_; }
135 const std::array<Region, kScratchRegionCount>& regions() const noexcept { return regions_; }
136 const Region& region(ScratchResource resource) const { return regions_[index(resource)]; }
137
138 // Read-only reservation: neither bytes nor logical_extent change, even
139 // on success. Backend row/key tickets must precede their own first mutation.
140 bool can_extend(ScratchResource resource, std::uint64_t new_extent) const noexcept {
141 const auto word = static_cast<std::uint32_t>(resource);
142 return word >= 1 && word <= kScratchRegionCount
143 && new_extent <= regions_[word - 1].capacity;
144 }
145 bool can_extend(const Region& item, std::uint64_t new_extent) const {
146 validate(item);
147 return can_extend(item.resource, new_extent);
148 }
149 void require_extend(ScratchResource resource, std::uint64_t new_extent) const {
150 if (!can_extend(resource, new_extent)) throw ScratchCapacityError(resource);
151 }
152 void require_extend(const Region& item, std::uint64_t new_extent) const {
153 validate(item);
154 require_extend(item.resource, new_extent);
155 }
156
157 // Publish a zero-initialized readable prefix (e.g. the entire fixed hash
158 // index at bind). Extents are monotonic until reset; this never resizes.
159 void extend(ScratchResource resource, std::uint64_t new_extent) {
160 require_extend(resource, new_extent);
161 auto& item = regions_[index(resource)];
162 item.logical_extent = std::max(item.logical_extent, new_extent);
163 }
164
165 // Reads require [offset,offset+len) inside BOTH capacity and logical_extent.
166 // Writes require capacity and publish max(old extent,offset+len) only after
167 // the copy; zeroed holes are permitted. A zero-length operation checks its
168 // offset but neither dereferences its buffer nor extends the prefix.
169 void read_at(ScratchResource resource, std::uint64_t offset,
170 void* destination, std::size_t length) const {
171 const auto& item = region(resource);
172 if (offset > item.capacity || length > item.capacity - offset
173 || offset > item.logical_extent || length > item.logical_extent - offset)
174 throw AuxiliaryScratchIoError(EIO, "scratch read outside logical extent");
175 if (length == 0) return;
176 if (!destination) throw AuxiliaryScratchIoError(EINVAL, "scratch read null destination");
177 std::memcpy(destination, item.base + static_cast<std::size_t>(offset), length);
178 }
179 void read_at(const Region& item, std::uint64_t offset,
180 void* destination, std::size_t length) const {
181 validate(item);
182 read_at(item.resource, offset, destination, length);
183 }
184 void write_at(ScratchResource resource, std::uint64_t offset,
185 const void* source, std::size_t length) {
186 auto& item = regions_[index(resource)];
187 if (offset > item.capacity || length > item.capacity - offset)
189 if (length == 0) return;
190 if (!source) throw AuxiliaryScratchIoError(EINVAL, "scratch write null source");
191 std::memcpy(item.base + static_cast<std::size_t>(offset), source, length);
192 item.logical_extent = std::max(item.logical_extent,
193 offset + static_cast<std::uint64_t>(length));
194 }
195 void write_at(const Region& item, std::uint64_t offset,
196 const void* source, std::size_t length) {
197 validate(item);
198 write_at(item.resource, offset, source, length);
199 }
200
201 // zero preserves readable extents; reset reinitializes bytes AND extents.
202 // Per-region reset supports clearing/refolding one store in the same run.
203 void zero() noexcept {
204 std::memset(allocation_, 0, static_cast<std::size_t>(layout_.slab_size));
205 }
207 auto& item = regions_[index(resource)];
208 if (item.capacity != 0)
209 std::memset(item.base, 0, static_cast<std::size_t>(item.capacity));
210 }
211 void reset() noexcept {
212 zero();
213 for (auto& item : regions_) item.logical_extent = 0;
214 }
216 zero(resource);
217 regions_[index(resource)].logical_extent = 0;
218 }
219
220private:
221 static std::size_t index(ScratchResource resource) {
222 const auto word = static_cast<std::uint32_t>(resource);
223 if (word < 1 || word > kScratchRegionCount)
225 return static_cast<std::size_t>(word - 1);
226 }
227 void validate(const Region& item) const {
228 if (&item != &regions_[index(item.resource)])
229 throw ScratchCapacityError(ScratchResource::layout,
230 "scratch region belongs to another slab");
231 }
232 const ScratchManifest manifest_;
233 const Layout layout_;
234 std::uint8_t* const allocation_;
235 std::array<Region, kScratchRegionCount> regions_{};
236};
237
238} // namespace pineforge::detail
void read_at(ScratchResource resource, std::uint64_t offset, void *destination, std::size_t length) const
ScratchSlab & operator=(const ScratchSlab &)=delete
void require_extend(const Region &item, std::uint64_t new_extent) const
void read_at(const Region &item, std::uint64_t offset, void *destination, std::size_t length) const
ScratchSlab & operator=(ScratchSlab &&)=delete
void reset(ScratchResource resource)
bool can_extend(ScratchResource resource, std::uint64_t new_extent) const noexcept
void require_extend(ScratchResource resource, std::uint64_t new_extent) const
const Layout & layout() const noexcept
ScratchSlab(ScratchSlab &&)=delete
void extend(ScratchResource resource, std::uint64_t new_extent)
ScratchSlab(const ScratchManifest &manifest)
ScratchSlab(const ScratchSlab &)=delete
const Region & region(ScratchResource resource) const
const std::array< Region, kScratchRegionCount > & regions() const noexcept
void zero(ScratchResource resource)
bool can_extend(const Region &item, std::uint64_t new_extent) const
void write_at(ScratchResource resource, std::uint64_t offset, const void *source, std::size_t length)
const ScratchManifest & manifest() const noexcept
void write_at(const Region &item, std::uint64_t offset, const void *source, std::size_t length)
bool fits(std::uint64_t bytes) noexcept
std::uint64_t slots(std::uint64_t keys)
std::uint64_t add(std::uint64_t left, std::uint64_t right)
std::uint64_t multiply(std::uint64_t left, std::uint64_t right)
std::uint64_t page(std::uint64_t bytes)
Layout compute_layout(const ScratchManifest &manifest)
constexpr std::uint64_t kScratchPageSize
constexpr std::size_t kScratchRegionCount
std::array< RegionLayout, kScratchRegionCount > regions