PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
map.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pineforge/na.hpp>
4
5#include <cmath>
6#include <cstddef>
7#include <cstdint>
8#include <functional>
9#include <iterator>
10#include <limits>
11#include <list>
12#include <memory>
13#include <stdexcept>
14#include <string>
15#include <type_traits>
16#include <unordered_map>
17#include <utility>
18#include <vector>
19
20namespace pineforge {
21
22namespace detail {
23
24template <typename T>
25using PineMapBare = std::remove_cv_t<std::remove_reference_t<T>>;
26
27template <typename T>
28inline constexpr bool is_pine_map_key_v =
29 std::is_integral_v<PineMapBare<T>> ||
30 std::is_floating_point_v<PineMapBare<T>> ||
31 std::is_enum_v<PineMapBare<T>> ||
32 std::is_same_v<PineMapBare<T>, std::string>;
33
34template <typename T>
35inline constexpr bool is_pine_map_snapshot_value_v =
36 !std::is_reference_v<T> &&
37 (std::is_arithmetic_v<PineMapBare<T>> ||
38 std::is_enum_v<PineMapBare<T>> ||
39 std::is_same_v<PineMapBare<T>, std::string>);
40
41template <typename Key, bool = std::is_floating_point_v<PineMapBare<Key>>>
43 bool operator()(const Key& lhs, const Key& rhs) const
44 noexcept(noexcept(lhs == rhs)) {
45 return lhs == rhs;
46 }
47};
48
49template <typename Key>
50struct PineMapKeyEqual<Key, true> {
51 bool operator()(Key lhs, Key rhs) const noexcept {
52 return (std::isnan(lhs) && std::isnan(rhs)) || lhs == rhs;
53 }
54};
55
56template <typename Key, bool = std::is_floating_point_v<PineMapBare<Key>>>
58 std::size_t operator()(const Key& key) const
59 noexcept(noexcept(std::hash<Key>{}(key))) {
60 return std::hash<Key>{}(key);
61 }
62};
63
64template <typename Key>
65struct PineMapKeyHash<Key, true> {
66 std::size_t operator()(Key key) const noexcept {
67 if (std::isnan(key)) {
68 // All Pine na float keys compare as one key, irrespective of the
69 // NaN payload supplied by a consumer.
70 return static_cast<std::size_t>(UINT64_C(0x9e3779b97f4a7c15));
71 }
72 // KeyEqual considers -0.0 and +0.0 equal. Hash the canonical form so
73 // the unordered-map equality/hash contract remains explicit.
74 if (key == static_cast<Key>(0)) key = static_cast<Key>(0);
75 return std::hash<Key>{}(key);
76 }
77};
78
79} // namespace detail
80
81template <typename T>
82inline constexpr bool is_pine_map_snapshot_value_v =
84
85// PineMap models a Pine map ID, not a value-owned C++ associative container.
86// Copying the handle therefore aliases the same backing store. Pine's
87// map.copy() operation is represented by copy(), which allocates a distinct
88// store and copies the entries into it.
89//
90// A list holds the authoritative entry sequence so overwrites never move an
91// existing key and removals do not disturb the relative order of survivors.
92// The hash index keeps get/put/remove/contains constant-time on average.
93template <typename Key, typename Value>
94class PineMap {
96 "PineMap key must be a Pine fundamental or enum type");
97
98 using Entry = std::pair<Key, Value>;
99 using EntryList = std::list<Entry>;
100 using EntryIterator = typename EntryList::iterator;
101 using Index = std::unordered_map<Key, EntryIterator,
104
105 static_assert(noexcept(std::declval<EntryList&>().swap(
106 std::declval<EntryList&>())),
107 "PineMap ordered storage must support no-throw swap");
108 static_assert(noexcept(std::declval<Index&>().swap(
109 std::declval<Index&>())),
110 "PineMap index must support no-throw swap");
111
112 static constexpr const char* kNaIdError =
113 "map operation on na ID";
114 static constexpr const char* kInvalidSnapshotError =
115 "map restore from invalid snapshot";
116 static constexpr const char* kCapacityError =
117 "map cannot contain more than 50000 key-value pairs";
118 static constexpr const char* kIndexInvariantError =
119 "map key index invariant violation";
120
121 struct Storage {
122 EntryList entries;
123 Index index;
124
125 Storage() = default;
126
127 Storage(const Storage& other)
128 : entries(other.entries),
129 index(0, other.index.hash_function(), other.index.key_eq()) {
130 rebuild_index();
131 }
132
133 Storage& operator=(const Storage& other) {
134 if (this == &other) return *this;
135 Storage replacement(other);
136 swap(replacement);
137 return *this;
138 }
139
140 Storage(Storage&&) = default;
141 Storage& operator=(Storage&&) = default;
142
143 void rebuild_index() {
144 Index replacement;
145 replacement.reserve(entries.size());
146 for (auto it = entries.begin(); it != entries.end(); ++it) {
147 const auto inserted = replacement.emplace(it->first, it).second;
148 if (!inserted) {
149 throw std::runtime_error(kIndexInvariantError);
150 }
151 }
152 index.swap(replacement);
153 }
154
155 void swap(Storage& other) noexcept {
156 // Swapping both containers preserves the relationship between
157 // each index iterator and its corresponding list. list::swap
158 // does not invalidate iterators.
159 entries.swap(other.entries);
160 index.swap(other.index);
161 }
162 };
163
164public:
165 static constexpr int max_pairs = 50'000;
166 static constexpr bool snapshot_supported =
168
169 // Snapshot is deliberately opaque. Besides a value copy of the entries,
170 // it retains the original backing-store identity. restore() mutates that
171 // store in place before reattaching the receiving handle, which both rolls
172 // back existing aliases and handles a map variable that was rebound after
173 // the snapshot, including a variable rebound to Pine na. restore() is an
174 // internal generated-checkpoint hook rather than a Pine map method, so a
175 // valid snapshot may reattach a null receiver.
176 //
177 // Value is copied according to its normal C++ copy semantics. This is a
178 // complete deep checkpoint for primitive Pine key/value instantiations.
179 // Generated UDTs or collections containing other handle types require a
180 // recursive, alias-aware checkpoint supplied by the code generator.
181 class Snapshot {
182 static_assert(snapshot_supported,
183 "PineMap::Snapshot supports primitive Pine values only; "
184 "UDT and collection handles require recursive codegen checkpointing");
185
186 std::shared_ptr<Storage> identity_;
187 Storage state_;
188
189 Snapshot(std::shared_ptr<Storage> identity, const Storage& state)
190 : identity_(std::move(identity)), state_(state) {}
191
192 friend class PineMap;
193
194 public:
195 Snapshot(const Snapshot&) = default;
196 Snapshot& operator=(const Snapshot&) = default;
197 Snapshot(Snapshot&&) = default;
198 Snapshot& operator=(Snapshot&&) = default;
199 };
200
201 // The default value represents Pine na. map.new<K,V>() is the operation
202 // that allocates an actual map ID.
203 PineMap() noexcept = default;
204
205 PineMap(const PineMap&) noexcept = default;
206 PineMap& operator=(const PineMap&) noexcept = default;
207
208 // A Pine map is an ID. Treat C++ moves like ordinary Pine assignment so
209 // compiler-generated std::move paths cannot invalidate the source handle.
210 PineMap(PineMap&& other) noexcept : storage_(other.storage_) {}
211
212 PineMap& operator=(PineMap&& other) noexcept {
213 if (this != &other) storage_ = other.storage_;
214 return *this;
215 }
216
217 [[nodiscard]] static PineMap new_() {
218 return PineMap(std::make_shared<Storage>());
219 }
220
221 [[nodiscard]] bool is_na() const noexcept { return !storage_; }
222
223 // Returns the previous value, or the value type's Pine na sentinel when
224 // the key is inserted for the first time.
225 Value put(Key key, Value value) {
226 Storage& storage = require_storage();
227 auto found = storage.index.find(key);
228 if (found != storage.index.end()) {
229 Value previous = found->second->second;
230 found->second->second = std::move(value);
231 return previous;
232 }
233
234 if (storage.entries.size() >= static_cast<std::size_t>(max_pairs)) {
235 throw std::runtime_error(kCapacityError);
236 }
237
238 storage.entries.emplace_back(std::move(key), std::move(value));
239 auto entry = std::prev(storage.entries.end());
240 try {
241 const auto inserted = storage.index.emplace(entry->first, entry).second;
242 if (!inserted) {
243 throw std::runtime_error(kIndexInvariantError);
244 }
245 } catch (...) {
246 // Both a false emplace result and hashing/allocation exceptions
247 // leave the index without the new key.
248 storage.entries.pop_back();
249 throw;
250 }
251 return pineforge::na<Value>();
252 }
253
254 [[nodiscard]] Value get(const Key& key) const {
255 const Storage& storage = require_storage();
256 auto found = storage.index.find(key);
257 if (found == storage.index.end()) return pineforge::na<Value>();
258 return found->second->second;
259 }
260
261 // Returns the removed value, or the value type's Pine na sentinel when no
262 // such key exists.
263 Value remove(const Key& key) {
264 Storage& storage = require_storage();
265 auto found = storage.index.find(key);
266 if (found == storage.index.end()) return pineforge::na<Value>();
267
268 Value previous = found->second->second;
269 storage.entries.erase(found->second);
270 storage.index.erase(found);
271 return previous;
272 }
273
274 [[nodiscard]] bool contains(const Key& key) const {
275 const Storage& storage = require_storage();
276 return storage.index.find(key) != storage.index.end();
277 }
278
279 [[nodiscard]] int size() const {
280 const Storage& storage = require_storage();
281 if (storage.entries.size() >
282 static_cast<std::size_t>(std::numeric_limits<int>::max())) {
283 throw std::overflow_error("map.size: result exceeds int range");
284 }
285 return static_cast<int>(storage.entries.size());
286 }
287
288 void clear() {
289 Storage& storage = require_storage();
290 storage.index.clear();
291 storage.entries.clear();
292 }
293
294 // Existing target keys retain their positions. New keys are appended in
295 // source insertion order. Iterating a map into itself is a no-op.
296 void put_all(const PineMap& source) {
297 Storage& target_storage = require_storage();
298 const Storage& source_storage = source.require_storage();
299 if (storage_ == source.storage_) return;
300
301 std::size_t new_keys = 0;
302 for (const auto& entry : source_storage.entries) {
303 if (target_storage.index.find(entry.first) ==
304 target_storage.index.end()) {
305 ++new_keys;
306 }
307 }
308 const auto available = static_cast<std::size_t>(max_pairs) -
309 target_storage.entries.size();
310 if (new_keys > available) {
311 // Preflight keeps put_all transactional with respect to the
312 // capacity check: existing values are not overwritten before a
313 // later new key discovers that the map is full.
314 throw std::runtime_error(kCapacityError);
315 }
316
317 for (const auto& entry : source_storage.entries) {
318 (void)put(entry.first, entry.second);
319 }
320 }
321
322 [[nodiscard]] std::vector<Key> keys() const {
323 const Storage& storage = require_storage();
324 std::vector<Key> result;
325 result.reserve(storage.entries.size());
326 for (const auto& entry : storage.entries) {
327 result.push_back(entry.first);
328 }
329 return result;
330 }
331
332 [[nodiscard]] std::vector<Value> values() const {
333 const Storage& storage = require_storage();
334 std::vector<Value> result;
335 result.reserve(storage.entries.size());
336 for (const auto& entry : storage.entries) {
337 result.push_back(entry.second);
338 }
339 return result;
340 }
341
342 [[nodiscard]] PineMap copy() const {
343 // The outer map ID is independent. Values use their regular copy
344 // semantics: primitive values are independent, while a handle-valued
345 // entry continues to alias that nested handle (Pine's shallow object
346 // copy behavior). Recursive checkpoint cloning belongs to generated
347 // UDT/collection state, not map.copy().
348 return PineMap(std::make_shared<Storage>(require_storage()));
349 }
350
351 template <typename T = Value,
352 std::enable_if_t<
354 std::is_same_v<T, Value>, int> = 0>
355 [[nodiscard]] Snapshot snapshot() const {
356 const Storage& storage = require_storage();
357 return Snapshot(storage_, storage);
358 }
359
360 template <typename T = Value,
361 std::enable_if_t<
363 std::is_same_v<T, Value>, int> = 0>
364 void restore(const Snapshot& snapshot) {
365 if (!snapshot.identity_) {
366 throw std::runtime_error(kInvalidSnapshotError);
367 }
368 // Build the replacement completely before touching live state. The
369 // subsequent swaps preserve snapshot.identity_ itself, so all handles
370 // that already alias that ID observe the restored contents.
371 Storage replacement(snapshot.state_);
372 snapshot.identity_->swap(replacement);
373 storage_ = snapshot.identity_;
374 }
375
376private:
377 [[nodiscard]] Storage& require_storage() {
378 if (!storage_) throw std::runtime_error(kNaIdError);
379 return *storage_;
380 }
381
382 [[nodiscard]] const Storage& require_storage() const {
383 if (!storage_) throw std::runtime_error(kNaIdError);
384 return *storage_;
385 }
386
387 explicit PineMap(std::shared_ptr<Storage> storage)
388 : storage_(std::move(storage)) {}
389
390 std::shared_ptr<Storage> storage_;
391};
392
393template <typename Key, typename Value>
394inline bool is_na(const PineMap<Key, Value>& map) noexcept {
395 return map.is_na();
396}
397
398} // namespace pineforge
Snapshot(const Snapshot &)=default
Snapshot & operator=(Snapshot &&)=default
Snapshot & operator=(const Snapshot &)=default
Snapshot(Snapshot &&)=default
Value get(const Key &key) const
Definition map.hpp:254
void put_all(const PineMap &source)
Definition map.hpp:296
Snapshot snapshot() const
Definition map.hpp:355
std::vector< Value > values() const
Definition map.hpp:332
PineMap & operator=(PineMap &&other) noexcept
Definition map.hpp:212
bool is_na() const noexcept
Definition map.hpp:221
std::vector< Key > keys() const
Definition map.hpp:322
static constexpr bool snapshot_supported
Definition map.hpp:166
PineMap() noexcept=default
bool contains(const Key &key) const
Definition map.hpp:274
Value remove(const Key &key)
Definition map.hpp:263
void restore(const Snapshot &snapshot)
Definition map.hpp:364
int size() const
Definition map.hpp:279
PineMap & operator=(const PineMap &) noexcept=default
PineMap copy() const
Definition map.hpp:342
static constexpr int max_pairs
Definition map.hpp:165
Value put(Key key, Value value)
Definition map.hpp:225
static PineMap new_()
Definition map.hpp:217
constexpr bool is_pine_map_snapshot_value_v
Definition map.hpp:35
std::remove_cv_t< std::remove_reference_t< T > > PineMapBare
Definition map.hpp:25
constexpr bool is_pine_map_key_v
Definition map.hpp:28
constexpr bool is_pine_map_snapshot_value_v
Definition map.hpp:82
bool is_na(const Line &h)
Definition drawing.hpp:42
T na()
Definition na.hpp:12
bool operator()(Key lhs, Key rhs) const noexcept
Definition map.hpp:51
bool operator()(const Key &lhs, const Key &rhs) const noexcept(noexcept(lhs==rhs))
Definition map.hpp:43
std::size_t operator()(Key key) const noexcept
Definition map.hpp:66
std::size_t operator()(const Key &key) const noexcept(noexcept(std::hash< Key >{}(key)))
Definition map.hpp:58