PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
drawing.hpp
Go to the documentation of this file.
1#pragma once
2//
3// PineForge drawing-objects-as-data runtime.
4//
5// Spec: docs/drawing-objects-as-data.md §3 ("C++ Runtime").
6//
7// This is a HEADLESS backtester: drawing objects (line/box/label/linefill) are
8// pure DATA the strategy can create, mutate and read back into trading logic.
9// There is NO rendering, NO graphics — only geometry. Functions are inline;
10// coded errors require the matching PineForge runtime at link time. Codegen emits
11// calls to the `pf_*` free functions below. Nothing here is exported via the C
12// ABI — these are codegen-emitted in-process calls, not `c_abi.cpp` symbols.
13//
14// The header is gated into the generated include block behind `self._uses_drawing`
15// exactly like `matrix.hpp` is gated behind `_uses_matrix`. The arenas are members
16// of the generated strategy subclass, so reset-per-run is automatic (a fresh
17// strategy instance is constructed per backtest).
18//
19// The arenas are also part of the generated script state that calc_on_order_fills
20// checkpoints by value -- TradingView rolls a fill recalculation's drawing edits
21// back with the script's variables. Unbound arenas retain the vector/shared-
22// block representation below. Explicitly configured slot arenas share a
23// complete occupied value and stage mutations before publication. Both
24// restore logical IDs together with the arena value.
25//
26#include <array>
27#include <atomic>
28#include <vector>
29#include <deque>
30#include <exception>
31#include <memory>
32#include <new>
33#include <optional>
34#include <string>
35#include <algorithm>
36#include <cstdint>
37#include <limits>
38#include <stdexcept>
39#include <cmath>
40#include <type_traits>
41#include <utility>
42#include <pineforge/na.hpp>
44
45namespace pineforge {
46
47// ---- enums (spec §3.2) -----------------------------------------------------
48enum class XLoc : int { bar_index = 0, bar_time = 1 }; // default bar_index
49enum class YLoc : int { price = 0, abovebar = 1, belowbar = 2 }; // default price
50
51// ---- value-view handles (spec §3.2) ----------------------------------------
52// Stored in vars / array<T> / UDT fields. id < 0 == na.
53struct Line { int32_t id = -1; };
54struct Box { int32_t id = -1; };
55struct Label { int32_t id = -1; };
56struct Linefill { int32_t id = -1; };
57
58inline bool is_na(const Line& h) { return h.id < 0; }
59inline bool is_na(const Box& h) { return h.id < 0; }
60inline bool is_na(const Label& h) { return h.id < 0; }
61inline bool is_na(const Linefill& h) { return h.id < 0; }
62
63// ---- pure value type: chart.point (spec §3.2) ------------------------------
64// No arena, no identity, no delete. Lowered inline as aggregate literals by
65// codegen; the engine only needs the struct + its is_na overload.
66struct ChartPoint {
67 int64_t index = na<int64_t>(); // x-coord as bar index (na sentinel INT64_MIN)
68 int64_t time = na<int64_t>(); // x-coord as UNIX ms
69 double price = na<double>(); // y-coord (NaN sentinel)
70};
71inline bool is_na(const ChartPoint& p) {
72 return is_na(p.index) && is_na(p.time) && is_na(p.price);
73}
74
75// ---- arena records (spec §3.3) ---------------------------------------------
76// Geometry only — every visual field (color/style/width/...) is dropped at
77// lowering. x-coords are int64_t (xloc.bar_time stores UNIX-ms, overflows
78// int32; na sentinel INT64_MIN); y-coords/prices are double (NaN sentinel).
79// NOTE: BoxRec has NO extend and NO text fields (unlike LineRec / LabelRec).
80struct LineRec { int64_t x1, x2; double y1, y2; XLoc xloc; bool ext_l, ext_r; bool alive; };
81struct BoxRec { int64_t left, right; double top, bottom; XLoc xloc; bool alive; };
82struct LabelRec { int64_t x; double y; XLoc xloc; YLoc yloc; std::string text; bool alive; };
83struct LinefillRec { int32_t line1, line2; bool alive; };
84
85enum class DrawingKind : std::uint8_t { line, box, label, linefill };
86enum class DrawingAccessStatus : std::uint8_t { invalid, stale, present };
87
88// Allocation outcomes are diagnostic state. In particular,
89// drawing_slot_bound_exceeded is an internal invariant outcome: it is never
90// thrown as a script-visible drawing error.
96
104
105// A failed creation has no issued ID. This result is for engine diagnostics;
106// it has no implicit handle or integer conversion and is not a script value.
111
112// ---- error type (spec §3.7) ------------------------------------------------
113// Derives from std::runtime_error so BacktestEngine::run()'s existing
114// `catch (const std::exception& e)` (src/engine_run.cpp) halts the backtest
115// exactly like TradingView when a drawing op touches a na handle.
116// Invalid script drawing accesses retain the existing typed drawing errors.
117struct pine_drawing_error : std::runtime_error {
118 explicit pine_drawing_error(const std::string& msg) : std::runtime_error(msg) {}
119};
120
122 explicit pine_drawing_id_exhausted(const char* object)
123 : pine_drawing_error(std::string(object) + " drawing ID domain exhausted") {}
124};
125
126namespace detail {
127
128struct DrawingBuiltinAccess;
129struct DrawingArenaTestAccess;
130
131template <class Rec> struct drawing_record_traits {
132 static constexpr bool builtin = false;
133};
134template <> struct drawing_record_traits<LineRec> {
135 static constexpr bool builtin = true;
137};
138template <> struct drawing_record_traits<BoxRec> {
139 static constexpr bool builtin = true;
141};
142template <> struct drawing_record_traits<LabelRec> {
143 static constexpr bool builtin = true;
145};
147 static constexpr bool builtin = true;
149};
150
151template <class Rec>
153
154template <class Rec>
155constexpr DrawingKind drawing_kind_of() noexcept {
156 static_assert(is_builtin_drawing_record_v<Rec>, "name the drawing kind of this record");
158}
159
160template <class Rec>
161inline Rec drawing_missing_value() noexcept {
162 if constexpr (std::is_same_v<Rec, LineRec>) {
163 return {na<int64_t>(), na<int64_t>(), na<double>(), na<double>(),
164 static_cast<XLoc>(-1), false, false, false};
165 } else if constexpr (std::is_same_v<Rec, BoxRec>) {
166 return {na<int64_t>(), na<int64_t>(), na<double>(), na<double>(),
167 static_cast<XLoc>(-1), false};
168 } else if constexpr (std::is_same_v<Rec, LabelRec>) {
169 return {na<int64_t>(), na<double>(), static_cast<XLoc>(-1),
170 static_cast<YLoc>(-1), std::string{}, false};
171 } else if constexpr (std::is_same_v<Rec, LinefillRec>) {
172 return {-1, -1, false};
173 } else {
174 static_assert(is_builtin_drawing_record_v<Rec>, "missing value is only for drawing records");
175 }
176}
177
178// One immutable object per builtin kind across translation units. It is never
179// exposed by a mutable accessor and owns no out-of-line text.
180template <class Rec>
181inline const Rec& drawing_missing_record() noexcept {
182 static const Rec missing = drawing_missing_value<Rec>();
183 return missing;
184}
185
186// The Pine kind of a drawing record (pine_na_reference's object), known from
187// the record type at the call and never read from the script.
188template <class Rec>
189constexpr const char* drawing_object_name() noexcept {
190 if constexpr (std::is_same_v<Rec, LineRec>) return "line";
191 else if constexpr (std::is_same_v<Rec, BoxRec>) return "box";
192 else if constexpr (std::is_same_v<Rec, LabelRec>) return "label";
193 else if constexpr (std::is_same_v<Rec, LinefillRec>) return "linefill";
194 else static_assert(!std::is_same_v<Rec, Rec>, "name the drawing kind of this record");
195}
196
197// A drawing call the script got wrong, raised as the pine_drawing_error it
198// always was (the same English): a na handle is pine_na_reference, a bad
199// argument pine_invalid_argument. Out of line and cold, so the accessors that
200// check stay as small as they were.
201[[noreturn, gnu::noinline, gnu::cold]] inline void drawing_na_error(const char* object,
202 const char* what) {
204 {{"object", object}}, what);
205}
206[[noreturn, gnu::noinline, gnu::cold]] inline void drawing_argument_error(
207 const char* function, const char* argument, const char* rule, const char* what) {
210 {{"function", function}, {"argument", argument}, {"rule", rule}}, what);
211}
212
213// The logical drawing id domain: ids are issued in [0, kDrawingIdExclusive).
214inline constexpr std::int64_t kDrawingIdExclusive =
215 static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max());
216
217// The strategy() field that declares the arena count of a drawing kind, the
218// value strategy_settings_rejected names; exactly the three kinds the
219// declared-count default accepts.
220template <class Rec>
221constexpr const char* drawing_count_field() noexcept {
222 if constexpr (std::is_same_v<Rec, LineRec>) return "max_lines_count";
223 else if constexpr (std::is_same_v<Rec, BoxRec>) return "max_boxes_count";
224 else if constexpr (std::is_same_v<Rec, LabelRec>) return "max_labels_count";
225 else static_assert(!std::is_same_v<Rec, Rec>, "name the declared-count field of this record");
226}
227
228// The logical drawing id domain is exhausted: a limit a script can reach, so
229// pine_runtime_limit (limit drawing_ids, max the id bound), raised as the
230// pine_drawing_id_exhausted it always was. The coded exception copies that
231// typed object, so its type and its English ("<object> drawing ID domain
232// exhausted") are unchanged.
233[[noreturn, gnu::noinline, gnu::cold]] inline void drawing_id_exhausted_error(const char* object) {
236 {{"limit", "drawing_ids"}, {"max", kDrawingIdExclusive}},
238}
239
240template <class Rec, bool Builtin = is_builtin_drawing_record_v<Rec>>
242 DrawingStaleSink() noexcept = default;
243 DrawingStaleSink(const DrawingStaleSink&) noexcept = default;
244 DrawingStaleSink& operator=(const DrawingStaleSink&) noexcept = default;
245 DrawingStaleSink(DrawingStaleSink&&) noexcept = default;
246 DrawingStaleSink& operator=(DrawingStaleSink&&) noexcept = default;
247 void reset() const noexcept {}
248};
249
250template <class Rec>
251struct DrawingStaleSink<Rec, true> {
252 mutable Rec value;
253 mutable std::atomic_flag resetting = ATOMIC_FLAG_INIT;
254
257 other.reset();
258 }
260 reset();
261 if (this != &other) other.reset();
262 return *this;
263 }
266 other.reset();
267 }
269 reset();
270 if (this != &other) other.reset();
271 return *this;
272 }
273
274 void reset() const noexcept {
275 // Stable-source const copies may reset the same source sink at once.
276 // Caller writes through escaped references must have ended first.
277 while (resetting.test_and_set(std::memory_order_acquire)) {}
278 if constexpr (std::is_same_v<Rec, LineRec>) {
279 value.x1 = na<int64_t>();
280 value.x2 = na<int64_t>();
281 value.y1 = na<double>();
282 value.y2 = na<double>();
283 value.xloc = static_cast<XLoc>(-1);
284 value.ext_l = false;
285 value.ext_r = false;
286 value.alive = false;
287 } else if constexpr (std::is_same_v<Rec, BoxRec>) {
288 value.left = na<int64_t>();
289 value.right = na<int64_t>();
290 value.top = na<double>();
291 value.bottom = na<double>();
292 value.xloc = static_cast<XLoc>(-1);
293 value.alive = false;
294 } else if constexpr (std::is_same_v<Rec, LabelRec>) {
295 value.x = na<int64_t>();
296 value.y = na<double>();
297 value.xloc = static_cast<XLoc>(-1);
298 value.yloc = static_cast<YLoc>(-1);
299 std::string{}.swap(value.text);
300 value.alive = false;
301 } else {
302 value.line1 = -1;
303 value.line2 = -1;
304 value.alive = false;
305 }
306 resetting.clear(std::memory_order_release);
307 }
308};
309
310// ---- record store: a vector, or blocks shared between copies ----------------
311// The records of one DrawingArena, indexed by id. A store starts as a vector,
312// exactly as the arena always was, and nothing in it is ever shared: a copy of
313// it is built from its records (O(records)). That copy holds them in blocks of
314// 32 -- the newest block, the one alloc appends to, beside a radix tree (a
315// 32-way trie over the id) that holds every earlier block -- and a copy of a
316// block store shares every block, as does a store assigned from one. A write
317// first gives the writing store its own copy of every block and tree node on
318// the way to the record that another copy still shares, then writes in place,
319// so a copy of a block store costs O(1) and a write after it one root-to-block
320// path (at most six nodes and a block for 2^31 ids). Every copy stays an
321// independent value: a shared block or node is never written.
322//
323// Under the scheduler's sequence (snapshot, restore, execute, commit) the first
324// snapshot copies the vector and the restore assigns the copy back, so from
325// the first bar on an arena and its checkpoint share blocks. An arena that is
326// never copied stays a vector.
327template <class Rec>
329 static_assert(std::is_copy_constructible_v<Rec> && std::is_nothrow_move_constructible_v<Rec>,
330 "a drawing record must be copyable and nothrow move-constructible");
331 static constexpr unsigned kBits = 5;
332 static constexpr std::uint32_t kWidth = std::uint32_t{1} << kBits;
333 static constexpr std::uint32_t kMask = kWidth - 1;
334
335 // Up to 32 records, constructed in place in slots [0, used) like a vector's
336 // elements; the other slots hold no object. Every block in the tree is full.
337 struct Block {
338 union Slot {
339 Rec rec;
340 Slot() noexcept {}
341 ~Slot() {}
342 };
343 std::uint32_t used = 0;
344 Slot slots[kWidth];
345
346 Block() noexcept {}
347 Block(const Block& other) {
348 try {
349 for (; used < other.used; ++used) ::new (&slots[used].rec) Rec(other.slots[used].rec);
350 } catch (...) {
351 clear();
352 throw;
353 }
354 }
355 Block& operator=(const Block&) = delete;
356 ~Block() { clear(); }
357
358 void clear() noexcept {
359 while (used > 0) slots[--used].rec.~Rec();
360 }
361 void append(Rec&& rec) noexcept {
362 ::new (&slots[used].rec) Rec(std::move(rec));
363 ++used;
364 }
365 };
366 struct Node { std::array<std::shared_ptr<void>, kWidth> kids{}; }; // a Node below kBits, else a Block
367
368 std::vector<Rec> flat_; // the records unless blocks_
369 std::shared_ptr<Node> root_; // the blocks before the tail; null until the first block fills
370 std::shared_ptr<Block> tail_; // records tail_first_ .. size_ - 1; null while empty
371 std::uint32_t size_ = 0;
372 std::uint32_t tail_first_ = 0; // a multiple of 32
373 unsigned shift_ = kBits; // the root's level: its kids are blocks when shift_ == kBits
374 bool blocks_ = false; // the records are in root_ and tail_, flat_ is empty
375 // Blocks only: a copy was taken of this store, or it was copied from one,
376 // so its blocks may be shared. Set on both sides by the copy; atomic
377 // because copying reads a const store, which threads may do at once.
378 mutable std::atomic<bool> shared_{false};
379
380 // The pointee of `slot`, for writing: itself when this store holds its only
381 // reference, else a private copy put in its place. The acquire fence pairs
382 // with the release decrement of a copy that dropped its reference on another
383 // thread, so its reads of the block happen before this store's writes
384 // (libstdc++ and libc++ read use_count() as an atomic load of that count).
385 template <class T>
386 T* own(std::shared_ptr<T>& slot) {
387 if (!shared_.load(std::memory_order_relaxed)) return slot.get();
388 if (slot.use_count() != 1) slot = std::make_shared<T>(*slot);
389 else std::atomic_thread_fence(std::memory_order_acquire);
390 return slot.get();
391 }
392 template <class T>
393 T* own_as(std::shared_ptr<void>& slot) {
394 if (!shared_.load(std::memory_order_relaxed)) return static_cast<T*>(slot.get());
395 if (slot.use_count() != 1) slot = std::make_shared<T>(*static_cast<const T*>(slot.get()));
396 else std::atomic_thread_fence(std::memory_order_acquire);
397 return static_cast<T*>(slot.get());
398 }
399
400 // A chain of nodes `level / kBits` high whose leftmost block is `block`.
401 static std::shared_ptr<void> path_to(unsigned level, const std::shared_ptr<Block>& block) {
402 std::shared_ptr<void> node = block;
403 for (; level > 0; level -= kBits) {
404 auto parent = std::make_shared<Node>();
405 parent->kids[0] = std::move(node);
406 node = std::move(parent);
407 }
408 return node;
409 }
410
411 // A read from blocks: the tail, or the walk from the root. Out of line and
412 // cold, like every block path below: what read(), write() and push_back()
413 // inline is the vector, the arena's own code before blocks existed.
414 [[gnu::noinline, gnu::cold]] const Rec& read_blocks(std::uint32_t id) const {
415 if (id >= tail_first_) return tail_->slots[id & kMask].rec;
416 const void* node = root_.get();
417 for (unsigned level = shift_; level > 0; level -= kBits)
418 node = static_cast<const Node*>(node)->kids[(id >> level) & kMask].get();
419 return static_cast<const Block*>(node)->slots[id & kMask].rec;
420 }
421
422 [[gnu::noinline, gnu::cold]] Rec& write_blocks(std::uint32_t id) {
423 if (!shared_.load(std::memory_order_relaxed)) return const_cast<Rec&>(read_blocks(id));
424 if (id >= tail_first_) return own(tail_)->slots[id & kMask].rec;
425 Node* node = own(root_);
426 for (unsigned level = shift_; level > kBits; level -= kBits)
427 node = own_as<Node>(node->kids[(id >> level) & kMask]);
428 return own_as<Block>(node->kids[(id >> kBits) & kMask])->slots[id & kMask].rec;
429 }
430
431 // Appends to blocks: through a tail another copy may share, or when the
432 // tail is full or absent, to a new tail block, the full one handed to the
433 // tree first.
434 [[gnu::noinline, gnu::cold]] void push_block(Rec&& rec) {
435 if (size_ != 0 && size_ - tail_first_ < kWidth) {
436 own(tail_)->append(std::move(rec));
437 ++size_;
438 return;
439 }
440 auto block = std::make_shared<Block>();
441 block->append(std::move(rec));
442 if (size_ != 0) push_tail();
443 tail_ = std::move(block);
444 tail_first_ = size_;
445 ++size_;
446 }
447 // Hands the full tail (records size_ - 32 .. size_ - 1) to the tree, growing
448 // the tree by a level when it is full. Every allocation precedes the one
449 // link that publishes the block, so a throw leaves every record as it was.
450 void push_tail() {
451 const std::uint32_t last = size_ - 1;
452 if (!root_) {
453 auto root = std::make_shared<Node>();
454 root->kids[0] = tail_;
455 root_ = std::move(root);
456 shift_ = kBits;
457 return;
458 }
459 if ((std::uint64_t{size_} >> kBits) > (std::uint64_t{1} << shift_)) {
460 auto root = std::make_shared<Node>();
461 root->kids[1] = path_to(shift_, tail_);
462 root->kids[0] = std::move(root_);
463 root_ = std::move(root);
464 shift_ += kBits;
465 return;
466 }
467 Node* node = own(root_);
468 for (unsigned level = shift_; level > kBits; level -= kBits) {
469 std::shared_ptr<void>& kid = node->kids[(last >> level) & kMask];
470 if (!kid) {
471 kid = path_to(level - kBits, tail_);
472 return;
473 }
474 node = own_as<Node>(kid);
475 }
476 node->kids[(last >> kBits) & kMask] = tail_;
477 }
478
479public:
481 // A copy of a vector store is built as blocks of its own; a copy of a block
482 // store shares its blocks.
483 DrawingRecordStore(const DrawingRecordStore& other) : blocks_(true) {
484 if (!other.blocks_) {
485 for (const Rec& rec : other.flat_) push_block(Rec(rec));
486 return;
487 }
488 other.shared_.store(true, std::memory_order_relaxed);
489 root_ = other.root_;
490 tail_ = other.tail_;
491 size_ = other.size_;
492 tail_first_ = other.tail_first_;
493 shift_ = other.shift_;
494 shared_.store(true, std::memory_order_relaxed);
495 }
497 if (this != &other) *this = DrawingRecordStore(other);
498 return *this;
499 }
500 // A moved-from store is empty, like the vector it replaced.
502 : flat_(std::move(other.flat_)), root_(std::move(other.root_)),
503 tail_(std::move(other.tail_)), size_(std::exchange(other.size_, 0)),
504 tail_first_(std::exchange(other.tail_first_, 0)),
505 shift_(std::exchange(other.shift_, kBits)), blocks_(std::exchange(other.blocks_, false)),
506 shared_(other.shared_.exchange(false, std::memory_order_relaxed)) {
507 other.flat_.clear();
508 }
510 if (this != &other) {
511 flat_ = std::move(other.flat_);
512 other.flat_.clear();
513 root_ = std::move(other.root_);
514 tail_ = std::move(other.tail_);
515 size_ = std::exchange(other.size_, 0);
516 tail_first_ = std::exchange(other.tail_first_, 0);
517 shift_ = std::exchange(other.shift_, kBits);
518 blocks_ = std::exchange(other.blocks_, false);
519 shared_.store(other.shared_.exchange(false, std::memory_order_relaxed),
520 std::memory_order_relaxed);
521 }
522 return *this;
523 }
524
525 int32_t size() const { return static_cast<int32_t>(size_); }
526
527 // Both take an id in [0, size()).
528 const Rec& read(std::uint32_t id) const {
529 if (!blocks_) return flat_[id];
530 return read_blocks(id);
531 }
532 Rec& write(std::uint32_t id) {
533 if (!blocks_) return flat_[id];
534 return write_blocks(id);
535 }
536
537 void push_back(Rec&& rec) {
538 if (!blocks_) {
539 flat_.push_back(std::move(rec));
540 ++size_;
541 return;
542 }
543 push_block(std::move(rec));
544 }
545};
546
547} // namespace detail
548
550 // Zero permits no settled occupants; separately configured pending
551 // workspace may still hold a creation. Zero for both terms returns the
552 // typed internal bound outcome on the first creation.
553 std::int32_t slot_bound = 0;
554 // Pending creation demand is transaction workspace accounted separately
555 // from the sampled occupied bound. One default workspace slot follows
556 // from this API accepting exactly one creation per call; it is not a
557 // measured TradingView allocation peak or a collection threshold.
558 std::uint32_t pending_creation_slots = 1;
559};
560
561inline DrawingBoundConfig drawing_bound(std::int32_t slot_bound,
562 std::uint32_t pending_creation_slots = 1) {
563 if (slot_bound < 0)
565 "drawing slot bound must be nonnegative");
566 return {slot_bound, pending_creation_slots};
567}
568
569namespace detail {
570
571inline constexpr std::int32_t kDrawingObservedSlack = 5;
572inline constexpr std::int32_t kDrawingLinefillObservedBound = 10;
573
574// The supplied TradingView measurements provide exactly two declared-count
575// observations for this default: labels reached 15 at declared 10, and lines
576// and boxes reached 10 at declared 5. The resulting +5 is the measured
577// default used here; those two points are the whole evidence, not a universal
578// collection law.
579template <class Rec>
581 std::int32_t declared_count = 50, std::uint32_t pending_creation_slots = 1) {
582 static_assert(std::is_same_v<Rec, LineRec> || std::is_same_v<Rec, BoxRec>
583 || std::is_same_v<Rec, LabelRec>,
584 "this default requires a declared count for line, box, or label");
585 // A strategy() header count a script wrote: strategy_settings_rejected,
586 // naming the field of this kind, raised as the std::invalid_argument it
587 // always was (the same English).
588 if (declared_count < 0
589 || declared_count > std::numeric_limits<std::int32_t>::max() - kDrawingObservedSlack)
591 {{"field", drawing_count_field<Rec>()}},
592 "drawing declared count is outside the int32 domain");
593 return {static_cast<std::int32_t>(declared_count + kDrawingObservedSlack),
594 pending_creation_slots};
595}
596
597// The supplied TradingView measurement has no declaration for this kind. Its
598// only qualified sample reached 10 while the line sample used declared 5; the
599// measurement is silent about any other relation. Keep that observed value
600// as a caller-settable parameter rather than deriving a new rule.
602 std::int32_t sampled_bound = kDrawingLinefillObservedBound,
603 std::uint32_t pending_creation_slots = 1) {
604 return drawing_bound(sampled_bound, pending_creation_slots);
605}
606
607template <class Rec>
609 struct Slot {
610 std::int32_t logical_id = -1;
611 std::optional<Rec> value;
612 };
614 std::int32_t logical_id;
615 std::uint32_t slot;
616 };
617
618 static std::size_t storage_slots(DrawingBoundConfig config) {
619 if (config.slot_bound < 0)
621 "drawing slot bound must be nonnegative");
622 const std::uint64_t total = std::min(
623 static_cast<std::uint64_t>(config.slot_bound) + config.pending_creation_slots,
624 static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()));
625 if (total > std::numeric_limits<std::size_t>::max())
627 "drawing slot storage size overflow");
628 return static_cast<std::size_t>(total);
629 }
630
632 : slots(storage_slots(config)),
633 slot_bound(config.slot_bound),
635 directory.reserve(slots.size());
636 }
637
638 std::vector<Slot> slots;
639 std::vector<DirectoryEntry> directory; // sorted; contains occupied IDs only
640 std::deque<std::int32_t> order;
641 std::int64_t next_id = 0;
642 std::int32_t slot_bound = 0;
643 std::uint32_t pending_creation_slots = 0;
644
645 std::int32_t slot_for(std::int32_t id) const noexcept {
646 const auto it = std::lower_bound(
647 directory.begin(), directory.end(), id,
648 [](const DirectoryEntry& entry, std::int32_t value) {
649 return entry.logical_id < value;
650 });
651 if (it == directory.end() || it->logical_id != id) return -1;
652 // A directory hit with no matching occupant is internal corruption,
653 // never a stale drawing. It cannot be produced by public operations.
654 if (it->slot >= slots.size()) std::terminate();
655 const Slot& candidate = slots[it->slot];
656 if (!candidate.value || candidate.logical_id != id) std::terminate();
657 return static_cast<std::int32_t>(it->slot);
658 }
659
660 std::size_t free_slot() const noexcept {
661 for (std::size_t i = 0; i < slots.size(); ++i)
662 if (!slots[i].value) return i;
663 return slots.size();
664 }
665};
666
667} // namespace detail
668
669template <class Rec>
671 std::int32_t declared_count = 50, std::uint32_t pending_creation_slots = 1) {
673 declared_count, pending_creation_slots);
674}
675
677 std::int32_t sampled_bound = detail::kDrawingLinefillObservedBound,
678 std::uint32_t pending_creation_slots = 1) {
680 sampled_bound, pending_creation_slots);
681}
682
683// ---- arena template (spec §3.4) --------------------------------------------
684// The default constructor retains the legacy resident COW arena. A
685// DrawingBoundConfig selects the bounded path: logical IDs remain monotonic,
686// only occupied IDs are in the sorted directory, and physical slots are
687// reclaimed on delete. The two paths are deliberately separate so generic
688// records and unbound callers keep their old direct-reference behavior.
689template <class Rec>
692 friend struct detail::DrawingArenaTestAccess;
694 std::deque<int32_t> order_;
695 int cap_;
696 std::shared_ptr<detail::BoundDrawingState<Rec>> bound_;
697 std::optional<DrawingBoundConfig> bound_config_;
699 // The last typed allocation outcome is part of the arena value. A
700 // failed bound admission records its diagnostic without changing the
701 // logical image; ordinary reads and edits leave it unchanged. A later
702 // successful typed allocation records status `ok`. ID exhaustion throws
703 // before this value is changed.
704 DrawingAllocationDiagnostic last_allocation_diagnostic_{};
705
706 static constexpr std::int64_t kIdExclusive = detail::kDrawingIdExclusive;
707
708 void ensure_bound_unique() {
709 if (bound_ && !bound_.unique())
710 bound_ = std::make_shared<detail::BoundDrawingState<Rec>>(*bound_);
711 }
712
713 const Rec& bound_read(int32_t id) const {
714 if (id < 0 || !bound_ || static_cast<std::int64_t>(id) >= bound_->next_id)
716 "drawing access on na handle");
717 const std::int32_t slot = bound_->slot_for(id);
718 if (slot < 0) return detail::drawing_missing_record<Rec>();
719 return *bound_->slots[static_cast<std::uint32_t>(slot)].value;
720 }
721
722 template <class Edit>
723 void bound_edit(int32_t id, Edit&& edit) {
724 if (id < 0 || !bound_ || static_cast<std::int64_t>(id) >= bound_->next_id)
726 "drawing access on na handle");
727 const std::int32_t slot = bound_->slot_for(id);
728 if (slot < 0) return; // stale builtins are typed no-ops
729 if (!bound_->slots[static_cast<std::uint32_t>(slot)].value->alive) return;
730
731 auto candidate = std::make_shared<detail::BoundDrawingState<Rec>>(*bound_);
732 const std::int32_t candidate_slot = candidate->slot_for(id);
733 if (candidate_slot < 0) std::terminate();
734 std::forward<Edit>(edit)(
735 *candidate->slots[static_cast<std::uint32_t>(candidate_slot)].value);
736 stale_sink_.reset();
737 bound_.swap(candidate);
738 }
739
740public:
741 explicit DrawingArena(int cap = 50) : cap_(std::max(1, cap)) {}
742 explicit DrawingArena(DrawingBoundConfig config) : cap_(50), bound_config_(config) {
744 "bounded arenas are only available for drawing records");
745 static_assert(!std::is_same_v<Rec, LinefillRec>,
746 "bounded linefill deletion lifetime is not yet specified");
747 if (config.slot_bound < 0)
749 "drawing slot bound must be nonnegative");
750 bound_ = std::make_shared<detail::BoundDrawingState<Rec>>(config);
751 }
752
754 : recs_(other.recs_), order_(other.order_), cap_(other.cap_),
755 bound_(other.bound_), bound_config_(other.bound_config_),
756 stale_sink_(), last_allocation_diagnostic_(other.last_allocation_diagnostic_) {
757 other.stale_sink_.reset();
758 }
759
761 if (this != &other) {
762 // All fallible work precedes publication and either sink reset.
763 detail::DrawingRecordStore<Rec> records(other.recs_);
764 auto order = other.order_;
765 auto state = other.bound_;
766 recs_ = std::move(records);
767 order_.swap(order);
768 cap_ = other.cap_;
769 bound_.swap(state);
770 bound_config_ = other.bound_config_;
771 last_allocation_diagnostic_ = other.last_allocation_diagnostic_;
772 other.stale_sink_.reset();
773 }
774 stale_sink_.reset();
775 return *this;
776 }
777
778 // A moved-from arena is empty and keeps its legacy cap. Bound state is
779 // transferred as one value; its source is left with no issued IDs.
781 : recs_(std::move(other.recs_)), order_(std::move(other.order_)),
782 cap_(other.cap_), bound_(std::move(other.bound_)),
783 bound_config_(other.bound_config_),
784 stale_sink_(std::move(other.stale_sink_)),
785 last_allocation_diagnostic_(other.last_allocation_diagnostic_) {
786 other.order_.clear();
787 other.last_allocation_diagnostic_ = {};
788 }
789
791 if (this != &other) {
792 recs_ = std::move(other.recs_);
793 order_ = std::move(other.order_);
794 bound_ = std::move(other.bound_);
795 bound_config_ = other.bound_config_;
796 stale_sink_ = std::move(other.stale_sink_);
797 last_allocation_diagnostic_ = other.last_allocation_diagnostic_;
798 other.order_.clear();
799 cap_ = other.cap_;
800 other.last_allocation_diagnostic_ = {};
801 }
802 stale_sink_.reset();
803 return *this;
804 }
805
806 // Creation for explicitly configured slot arenas. A slot-bound outcome
807 // contains no ID and records only its typed diagnostic; the logical arena
808 // state remains unchanged. ID exhaustion also contains no ID and leaves
809 // the arena value untouched. The generated caller must consume this
810 // result before publishing a script handle. Genuine allocation failures
811 // retain their C++ exception and leave the previous diagnostic in place.
812 [[nodiscard]] DrawingAllocationResult try_alloc(Rec r) {
814 "slot allocation requires a builtin drawing record");
815 if (!bound_config_)
817 "typed drawing allocation requires a slot arena");
818 DrawingAllocationDiagnostic diagnostic{};
819 diagnostic.kind = detail::drawing_kind_of<Rec>();
820 diagnostic.configured_bound = bound_config_->slot_bound;
821 diagnostic.pending_creation_slots = bound_config_->pending_creation_slots;
822 const std::int64_t next_id = bound_ ? bound_->next_id : 0;
823 if (next_id >= kIdExclusive) {
825 return {diagnostic, std::nullopt};
826 }
827 const std::uint64_t required = (bound_ ? bound_->directory.size() : 0u) + 1u;
828 diagnostic.required_slots = required;
829 const std::uint64_t available =
830 static_cast<std::uint64_t>(bound_config_->slot_bound)
831 + bound_config_->pending_creation_slots;
832 if (required > available) {
834 last_allocation_diagnostic_ = diagnostic;
835 return {diagnostic, std::nullopt};
836 }
837 auto candidate = bound_
838 ? std::make_shared<detail::BoundDrawingState<Rec>>(*bound_)
839 : std::make_shared<detail::BoundDrawingState<Rec>>(*bound_config_);
840 const std::size_t slot = candidate->free_slot();
841 if (slot >= candidate->slots.size()) std::terminate(); // corrupt occupied count
842 const std::int32_t id = static_cast<std::int32_t>(next_id);
843 r.alive = true;
844 candidate->slots[slot].logical_id = id;
845 candidate->slots[slot].value.emplace(std::move(r));
846 candidate->directory.push_back({id, static_cast<std::uint32_t>(slot)});
847 candidate->order.push_back(id);
848 candidate->next_id = next_id + 1;
849 stale_sink_.reset();
850 last_allocation_diagnostic_ = diagnostic;
851 bound_.swap(candidate);
852 return {diagnostic, id};
853 }
854
855 int32_t alloc(Rec r) {
856 // The legacy integer-only entry has no status channel. Binding the
857 // new path requires its typed creation lowering first; do not convert
858 // a slot invariant into an NA handle or a script drawing error.
859 if (bound_config_)
861 "slot drawing creation requires try_alloc");
862 if (static_cast<std::int64_t>(recs_.size()) >= kIdExclusive) {
865 else
867 }
868
869 while ((int)order_.size() >= cap_) { // exact-cap FIFO eviction (oldest first)
870 // Tombstone before leaving order_: the write may copy a shared block
871 // and throw, which must leave the record live and in order_.
872 recs_.write(static_cast<std::uint32_t>(order_.front())).alive = false;
873 order_.pop_front();
874 }
875 int32_t id = recs_.size();
876 r.alive = true;
877 recs_.push_back(std::move(r));
878 order_.push_back(id);
879 return id;
880 }
881
882 void erase(int32_t id) {
883 if (bound_) {
884 if (id < 0 || static_cast<std::int64_t>(id) >= bound_->next_id) return;
885 const std::int32_t slot = bound_->slot_for(id);
886 if (slot < 0) return; // stale delete is a no-op
887 if (!bound_->slots[static_cast<std::uint32_t>(slot)].value->alive) return;
888
889 auto candidate = std::make_shared<detail::BoundDrawingState<Rec>>(*bound_);
890 const std::int32_t candidate_slot = candidate->slot_for(id);
891 if (candidate_slot < 0) std::terminate();
892 const auto dir = std::lower_bound(
893 candidate->directory.begin(), candidate->directory.end(), id,
894 [](const auto& entry, std::int32_t value) {
895 return entry.logical_id < value;
896 });
897 if (dir == candidate->directory.end() || dir->logical_id != id) std::terminate();
898 candidate->directory.erase(dir);
899 const auto it = std::find(candidate->order.begin(), candidate->order.end(), id);
900 if (it != candidate->order.end()) candidate->order.erase(it);
901 candidate->slots[static_cast<std::uint32_t>(candidate_slot)].logical_id = -1;
902 candidate->slots[static_cast<std::uint32_t>(candidate_slot)].value.reset();
903 stale_sink_.reset();
904 bound_.swap(candidate);
905 return;
906 }
907
908 if (!alive(id)) return;
909 recs_.write(static_cast<std::uint32_t>(id)).alive = false;
910 const auto it = std::find(order_.begin(), order_.end(), id);
911 if (it != order_.end()) order_.erase(it);
912 }
913
914 bool alive(int32_t id) const {
915 if (bound_) {
916 const std::int32_t slot = (id >= 0) ? bound_->slot_for(id) : -1;
917 return slot >= 0
918 && bound_->slots[static_cast<std::uint32_t>(slot)].value->alive;
919 }
920 return id >= 0 && id < recs_.size() && recs_.read(static_cast<std::uint32_t>(id)).alive;
921 }
922
923 // In the unbound path this is the issued record count. In the bound path
924 // it is the next logical ID, so a reclaimed slot never changes old IDs.
925 int32_t size() const {
926 if (bound_) return static_cast<int32_t>(bound_->next_id);
927 return recs_.size();
928 }
929
930 DrawingAccessStatus access_status(int32_t id) const noexcept {
931 if (id < 0) return DrawingAccessStatus::invalid;
932 if (bound_) {
933 if (static_cast<std::int64_t>(id) >= bound_->next_id)
935 return bound_->slot_for(id) < 0 ? DrawingAccessStatus::stale
937 }
938 return id >= recs_.size() ? DrawingAccessStatus::invalid
940 }
941
942 bool is_bound() const noexcept { return bound_config_.has_value(); }
943 std::int32_t slot_bound() const noexcept {
944 return bound_config_ ? bound_config_->slot_bound : 0;
945 }
946 std::uint32_t pending_creation_slots() const noexcept {
947 return bound_config_ ? bound_config_->pending_creation_slots : 0;
948 }
949 std::size_t occupied_slots() const noexcept {
950 return bound_ ? bound_->directory.size() : order_.size();
951 }
953 return last_allocation_diagnostic_;
954 }
955
956 // Bound direct borrows classify stale IDs before returning the immutable
957 // NA record or this arena object's reset sink. Unbound raw access keeps
958 // its historical unchecked reference representation.
959 Rec& at(int32_t id) {
961 if (bound_config_) {
962 if (id < 0 || !bound_ || static_cast<std::int64_t>(id) >= bound_->next_id)
964 "drawing access on na handle");
965 if (bound_->slot_for(id) < 0) {
966 stale_sink_.reset();
967 return stale_sink_.value;
968 }
969 ensure_bound_unique();
970 stale_sink_.reset();
971 const std::int32_t slot = bound_->slot_for(id);
972 return *bound_->slots[static_cast<std::uint32_t>(slot)].value;
973 }
974 }
975 return recs_.write(static_cast<std::uint32_t>(id));
976 }
977
978 const Rec& at(int32_t id) const {
980 if (bound_config_) return bound_read(id);
981 }
982 return recs_.read(static_cast<std::uint32_t>(id));
983 }
984
985 const std::deque<int32_t>& order() const {
986 return bound_ ? bound_->order : order_;
987 }
988};
989
990// ---- shared helpers --------------------------------------------------------
991// Direct C++ borrow compatibility: checked invalid handles throw; an unbound
992// resident dead record is returned as-is. Builtins use separate value access.
993template <class Rec, class Handle>
994inline Rec& pf_require_live(DrawingArena<Rec>& a, Handle h) {
995 if (h.id < 0 || h.id >= a.size())
996 detail::drawing_na_error(detail::drawing_object_name<Rec>(), "drawing access on na handle");
997 return a.at(h.id);
998}
999template <class Rec, class Handle>
1000inline const Rec& pf_require_live(const DrawingArena<Rec>& a, Handle h) {
1001 if (h.id < 0 || h.id >= a.size())
1002 detail::drawing_na_error(detail::drawing_object_name<Rec>(), "drawing access on na handle");
1003 return a.at(h.id);
1004}
1005
1006namespace detail {
1007
1008// Builtins use the bound directory directly and never acquire a public
1009// mutable borrow or write a discard sink. The unbound missing policy uses
1010// alive for all removals; it is not evidence about TradingView's getters
1011// after automatic collection.
1013 template <class Rec, class Handle>
1014 static bool missing(const DrawingArena<Rec>& a, Handle h) noexcept {
1015 if (a.bound_config_) {
1016 if (!a.bound_) return false; // no issued IDs in a moved-from arena
1017 const auto slot = a.bound_->slot_for(h.id);
1018 return slot < 0
1019 || !a.bound_->slots[static_cast<std::uint32_t>(slot)].value->alive;
1020 }
1021 if constexpr (std::is_same_v<Rec, LinefillRec>) return false;
1022 else return !a.alive(h.id);
1023 }
1024
1025 template <class Rec, class Handle>
1026 static const Rec& read(const DrawingArena<Rec>& a, Handle h) {
1027 if (h.id < 0 || h.id >= a.size())
1028 drawing_na_error(drawing_object_name<Rec>(), "drawing access on na handle");
1029 if (missing(a, h)) return drawing_missing_record<Rec>();
1030 if (a.bound_config_) return a.bound_read(h.id);
1031 const Rec& r = a.recs_.read(static_cast<std::uint32_t>(h.id));
1032 return r;
1033 }
1034
1035 template <class Rec, class Handle, class Edit>
1036 static void edit(DrawingArena<Rec>& a, Handle h, Edit&& apply) {
1037 if (h.id < 0 || h.id >= a.size())
1038 drawing_na_error(drawing_object_name<Rec>(), "drawing access on na handle");
1039 if (missing(a, h)) return;
1040 if (a.bound_config_) {
1041 a.bound_edit(h.id, std::forward<Edit>(apply));
1042 return;
1043 }
1044 apply(a.recs_.write(static_cast<std::uint32_t>(h.id)));
1045 }
1046};
1047
1048// The builtin bridge has one runtime dispatch at its boundary. The
1049// unbound branch calls the legacy integer allocator, preserving its existing
1050// eviction, ID and exception behavior. Bound allocation consumes the typed
1051// result: a slot-bound outcome is the script-visible NA handle, while ID
1052// exhaustion remains a typed C++ error.
1053template <class Handle, class Rec>
1054inline Handle drawing_builtin_allocate(DrawingArena<Rec>& arena, Rec&& record) {
1055 if (!arena.is_bound())
1056 return Handle{arena.alloc(std::forward<Rec>(record))};
1057
1058 const DrawingAllocationResult result = arena.try_alloc(std::forward<Rec>(record));
1059 switch (result.diagnostic.status) {
1061 if (!result.id) std::terminate();
1062 return Handle{*result.id};
1064 return Handle{};
1067 }
1068 std::terminate();
1069}
1070
1071} // namespace detail
1072
1073// Selects the x-coordinate a ChartPoint contributes for a given xloc.
1074inline int64_t pf_point_x(const ChartPoint& p, XLoc xloc) {
1075 return (xloc == XLoc::bar_time) ? p.time : p.index;
1076}
1077
1078// ============================ line ==========================================
1079inline Line pf_line_new(DrawingArena<LineRec>& a, int64_t x1, double y1, int64_t x2, double y2,
1080 XLoc xloc = XLoc::bar_index, bool el = false, bool er = false) {
1081 // *_new(na,na,na,na) allocates a LIVE record with na coords (NOT a na handle).
1083 a, LineRec{ x1, x2, y1, y2, xloc, el, er, true });
1084}
1086 XLoc xloc = XLoc::bar_index) {
1088 a, LineRec{ pf_point_x(p1, xloc), pf_point_x(p2, xloc),
1089 p1.price, p2.price, xloc, false, false, true });
1090}
1091
1096
1097inline void pf_line_set_x1(DrawingArena<LineRec>& a, Line h, int64_t v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x1 = v; }); }
1098inline void pf_line_set_x2(DrawingArena<LineRec>& a, Line h, int64_t v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x2 = v; }); }
1099inline void pf_line_set_y1(DrawingArena<LineRec>& a, Line h, double v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.y1 = v; }); }
1100inline void pf_line_set_y2(DrawingArena<LineRec>& a, Line h, double v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.y2 = v; }); }
1101inline void pf_line_set_xy1(DrawingArena<LineRec>& a, Line h, int64_t x, double y) {
1102 detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x1 = x; r.y1 = y; });
1103}
1104inline void pf_line_set_xy2(DrawingArena<LineRec>& a, Line h, int64_t x, double y) {
1105 detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x2 = x; r.y2 = y; });
1106}
1108 detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x1 = pf_point_x(p, r.xloc); r.y1 = p.price; });
1109}
1111 detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x2 = pf_point_x(p, r.xloc); r.y2 = p.price; });
1112}
1113inline void pf_line_set_xloc(DrawingArena<LineRec>& a, Line h, int64_t x1, int64_t x2, XLoc xloc) {
1114 // REINTERPRET: stores new numbers + flag; does NOT convert coord spaces (TV-faithful, spec §3.9).
1115 detail::DrawingBuiltinAccess::edit(a, h, [=](LineRec& r) { r.x1 = x1; r.x2 = x2; r.xloc = xloc; });
1116}
1117
1118// real computation: infinite line, ignores extend; valid for xloc.bar_index (spec §3.6).
1119inline double pf_line_get_price(DrawingArena<LineRec>& a, Line h, int64_t x) {
1122 if (r.xloc == XLoc::bar_time) // TV errors on bar_time lines
1123 detail::drawing_argument_error("line.get_price", "xloc", "requires_xloc_bar_index",
1124 "line.get_price requires xloc.bar_index");
1125 if (r.x1 == r.x2) return na<double>(); // degenerate vertical -> na (matches TV)
1126 return r.y1 + (r.y2 - r.y1) / (double)(r.x2 - r.x1) * (double)(x - r.x1); // infinite line, ignores extend
1127}
1128
1130 if (!a.alive(h.id)) return Line{};
1132 if (!a.is_bound()) return Line{a.alloc(r)};
1133 return detail::drawing_builtin_allocate<Line>(a, std::move(r));
1134}
1135inline void pf_line_delete(DrawingArena<LineRec>& a, Line h) { a.erase(h.id); } // silent no-op on na/dead
1136
1137// ============================ box ===========================================
1138inline Box pf_box_new(DrawingArena<BoxRec>& a, int64_t left, double top, int64_t right, double bottom,
1139 XLoc xloc = XLoc::bar_index) {
1141 a, BoxRec{ left, right, top, bottom, xloc, true });
1142}
1144 XLoc xloc = XLoc::bar_index) {
1146 a, BoxRec{ pf_point_x(top_left, xloc), pf_point_x(bottom_right, xloc),
1147 top_left.price, bottom_right.price, xloc, true });
1148}
1149
1154
1155inline void pf_box_set_left (DrawingArena<BoxRec>& a, Box h, int64_t v) { detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.left = v; }); }
1156inline void pf_box_set_right (DrawingArena<BoxRec>& a, Box h, int64_t v) { detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.right = v; }); }
1157inline void pf_box_set_top (DrawingArena<BoxRec>& a, Box h, double v) { detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.top = v; }); }
1158inline void pf_box_set_bottom(DrawingArena<BoxRec>& a, Box h, double v) { detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.bottom = v; }); }
1159inline void pf_box_set_lefttop(DrawingArena<BoxRec>& a, Box h, int64_t left, double top) {
1160 detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.left = left; r.top = top; });
1161}
1162inline void pf_box_set_rightbottom(DrawingArena<BoxRec>& a, Box h, int64_t right, double bottom) {
1163 detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.right = right; r.bottom = bottom; });
1164}
1166 detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.left = pf_point_x(p, r.xloc); r.top = p.price; });
1167}
1169 detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.right = pf_point_x(p, r.xloc); r.bottom = p.price; });
1170}
1171inline void pf_box_set_xloc(DrawingArena<BoxRec>& a, Box h, int64_t left, int64_t right, XLoc xloc) {
1172 detail::DrawingBuiltinAccess::edit(a, h, [=](BoxRec& r) { r.left = left; r.right = right; r.xloc = xloc; });
1173}
1174
1176 if (!a.alive(h.id)) return Box{};
1178 if (!a.is_bound()) return Box{a.alloc(r)};
1179 return detail::drawing_builtin_allocate<Box>(a, std::move(r));
1180}
1181inline void pf_box_delete(DrawingArena<BoxRec>& a, Box h) { a.erase(h.id); }
1182
1183// ============================ label =========================================
1184// NOTE: yloc.abovebar / yloc.belowbar should auto-place y at the bar high/low
1185// (spec §3.9). The spec's pf_label_new signature is intentionally decoupled from
1186// the engine and does NOT receive bar high/low, so y is stored AS-IS and yloc is
1187// store-only here. yloc abovebar/belowbar auto-place needs bar high/low at the
1188// call site — codegen would pass close()/high()/low() if ever required. Inert:
1189// no corpus label reads get_y back (spec note 4), so no parity claim.
1190inline Label pf_label_new(DrawingArena<LabelRec>& a, int64_t x, double y, std::string text,
1191 XLoc xloc = XLoc::bar_index, YLoc yloc = YLoc::price) {
1193 a, LabelRec{ x, y, xloc, yloc, std::move(text), true });
1194}
1195inline Label pf_label_new_pt(DrawingArena<LabelRec>& a, ChartPoint point, std::string text,
1196 YLoc yloc = YLoc::price) {
1197 // xloc carried by the ChartPoint: bar_time only when index is na but time is set.
1198 XLoc xloc = (is_na(point.index) && !is_na(point.time)) ? XLoc::bar_time : XLoc::bar_index;
1200 a, LabelRec{ pf_point_x(point, xloc), point.price, xloc, yloc,
1201 std::move(text), true });
1202}
1203
1207
1208inline void pf_label_set_x(DrawingArena<LabelRec>& a, Label h, int64_t v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.x = v; }); }
1209inline void pf_label_set_y(DrawingArena<LabelRec>& a, Label h, double v) { detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.y = v; }); }
1210inline void pf_label_set_xy(DrawingArena<LabelRec>& a, Label h, int64_t x, double y) {
1211 detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.x = x; r.y = y; });
1212}
1214 detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.x = pf_point_x(p, r.xloc); r.y = p.price; });
1215}
1216inline void pf_label_set_xloc(DrawingArena<LabelRec>& a, Label h, int64_t x, XLoc xloc) {
1217 detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.x = x; r.xloc = xloc; });
1218}
1220 detail::DrawingBuiltinAccess::edit(a, h, [=](LabelRec& r) { r.yloc = yloc; });
1221}
1222inline void pf_label_set_text(DrawingArena<LabelRec>& a, Label h, std::string text) {
1223 detail::DrawingBuiltinAccess::edit(a, h, [&](LabelRec& r) { r.text = std::move(text); });
1224}
1225
1227 if (!a.alive(h.id)) return Label{};
1229 if (!a.is_bound()) return Label{a.alloc(std::move(r))};
1230 return detail::drawing_builtin_allocate<Label>(a, std::move(r));
1231}
1233
1234// ============================ linefill ======================================
1236 // Stores the two Line handle ids inertly (color dropped). linefill.new(na,na,na)
1237 // is legal -> a LIVE record over na line ids (spec note 1).
1238 return Linefill{ a.alloc(LinefillRec{ l1.id, l2.id, true }) };
1239}
1247
1248// ============================ visual no-op sink (spec §3.6) ==================
1249// Evaluates + discards args (so side effects/typechecking still happen), returns void.
1250// Backs every VISUAL setter/ctor-kwarg: line/box/label set_color/set_style/...
1251template <class... A> inline void pf_noop(A&&...) {}
1252
1253} // namespace pineforge
const std::deque< int32_t > & order() const
Definition drawing.hpp:985
DrawingAllocationResult try_alloc(Rec r)
Definition drawing.hpp:812
int32_t alloc(Rec r)
Definition drawing.hpp:855
DrawingAccessStatus access_status(int32_t id) const noexcept
Definition drawing.hpp:930
DrawingArena(int cap=50)
Definition drawing.hpp:741
std::int32_t slot_bound() const noexcept
Definition drawing.hpp:943
DrawingArena(const DrawingArena &other)
Definition drawing.hpp:753
std::uint32_t pending_creation_slots() const noexcept
Definition drawing.hpp:946
DrawingArena(DrawingBoundConfig config)
Definition drawing.hpp:742
DrawingArena & operator=(const DrawingArena &other)
Definition drawing.hpp:760
bool is_bound() const noexcept
Definition drawing.hpp:942
int32_t size() const
Definition drawing.hpp:925
std::size_t occupied_slots() const noexcept
Definition drawing.hpp:949
DrawingAllocationDiagnostic last_allocation_diagnostic() const noexcept
Definition drawing.hpp:952
Rec & at(int32_t id)
Definition drawing.hpp:959
const Rec & at(int32_t id) const
Definition drawing.hpp:978
DrawingArena & operator=(DrawingArena &&other) noexcept
Definition drawing.hpp:790
bool alive(int32_t id) const
Definition drawing.hpp:914
DrawingArena(DrawingArena &&other)
Definition drawing.hpp:780
void erase(int32_t id)
Definition drawing.hpp:882
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
Rec & write(std::uint32_t id)
Definition drawing.hpp:532
DrawingRecordStore & operator=(DrawingRecordStore &&other) noexcept
Definition drawing.hpp:509
DrawingRecordStore(DrawingRecordStore &&other) noexcept
Definition drawing.hpp:501
DrawingRecordStore & operator=(const DrawingRecordStore &other)
Definition drawing.hpp:496
const Rec & read(std::uint32_t id) const
Definition drawing.hpp:528
DrawingRecordStore(const DrawingRecordStore &other)
Definition drawing.hpp:483
constexpr bool is_builtin_drawing_record_v
Definition drawing.hpp:152
void drawing_id_exhausted_error(const char *object)
Definition drawing.hpp:233
constexpr const char * drawing_count_field() noexcept
Definition drawing.hpp:221
Handle drawing_builtin_allocate(DrawingArena< Rec > &arena, Rec &&record)
Definition drawing.hpp:1054
constexpr std::int64_t kDrawingIdExclusive
Definition drawing.hpp:214
void drawing_na_error(const char *object, const char *what)
Definition drawing.hpp:201
void drawing_argument_error(const char *function, const char *argument, const char *rule, const char *what)
Definition drawing.hpp:206
DrawingBoundConfig drawing_bound_from_linefill_sample(std::int32_t sampled_bound=kDrawingLinefillObservedBound, std::uint32_t pending_creation_slots=1)
Definition drawing.hpp:601
constexpr std::int32_t kDrawingLinefillObservedBound
Definition drawing.hpp:572
Rec drawing_missing_value() noexcept
Definition drawing.hpp:161
constexpr const char * drawing_object_name() noexcept
Definition drawing.hpp:189
DrawingBoundConfig drawing_bound_from_declared_count(std::int32_t declared_count=50, std::uint32_t pending_creation_slots=1)
Definition drawing.hpp:580
constexpr DrawingKind drawing_kind_of() noexcept
Definition drawing.hpp:155
constexpr std::int32_t kDrawingObservedSlack
Definition drawing.hpp:571
const Rec & drawing_missing_record() noexcept
Definition drawing.hpp:181
DrawingAccessStatus
Definition drawing.hpp:86
void pf_linefill_delete(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:1246
void pf_line_set_xy1(DrawingArena< LineRec > &a, Line h, int64_t x, double y)
Definition drawing.hpp:1101
std::string pf_label_get_text(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:1206
void pf_label_delete(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:1232
void pf_line_set_xy2(DrawingArena< LineRec > &a, Line h, int64_t x, double y)
Definition drawing.hpp:1104
double pf_line_get_y1(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1094
void pf_box_set_bottom_right_point(DrawingArena< BoxRec > &a, Box h, ChartPoint p)
Definition drawing.hpp:1168
Line pf_linefill_get_line2(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:1243
int64_t pf_point_x(const ChartPoint &p, XLoc xloc)
Definition drawing.hpp:1074
Line pf_line_copy(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1129
void pf_box_set_top(DrawingArena< BoxRec > &a, Box h, double v)
Definition drawing.hpp:1157
void pf_noop(A &&...)
Definition drawing.hpp:1251
Box pf_box_new(DrawingArena< BoxRec > &a, int64_t left, double top, int64_t right, double bottom, XLoc xloc=XLoc::bar_index)
Definition drawing.hpp:1138
void pf_box_set_lefttop(DrawingArena< BoxRec > &a, Box h, int64_t left, double top)
Definition drawing.hpp:1159
void pf_label_set_point(DrawingArena< LabelRec > &a, Label h, ChartPoint p)
Definition drawing.hpp:1213
int64_t na< int64_t >()
Definition na.hpp:66
Box pf_box_new_pts(DrawingArena< BoxRec > &a, ChartPoint top_left, ChartPoint bottom_right, XLoc xloc=XLoc::bar_index)
Definition drawing.hpp:1143
void pf_box_set_right(DrawingArena< BoxRec > &a, Box h, int64_t v)
Definition drawing.hpp:1156
Line pf_line_new_pts(DrawingArena< LineRec > &a, ChartPoint p1, ChartPoint p2, XLoc xloc=XLoc::bar_index)
Definition drawing.hpp:1085
void pf_box_set_xloc(DrawingArena< BoxRec > &a, Box h, int64_t left, int64_t right, XLoc xloc)
Definition drawing.hpp:1171
void pf_box_set_top_left_point(DrawingArena< BoxRec > &a, Box h, ChartPoint p)
Definition drawing.hpp:1165
double pf_label_get_y(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:1205
Line pf_line_new(DrawingArena< LineRec > &a, int64_t x1, double y1, int64_t x2, double y2, XLoc xloc=XLoc::bar_index, bool el=false, bool er=false)
Definition drawing.hpp:1079
void pf_label_set_xy(DrawingArena< LabelRec > &a, Label h, int64_t x, double y)
Definition drawing.hpp:1210
void pf_line_set_y1(DrawingArena< LineRec > &a, Line h, double v)
Definition drawing.hpp:1099
Line pf_linefill_get_line1(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:1240
Label pf_label_new(DrawingArena< LabelRec > &a, int64_t x, double y, std::string text, XLoc xloc=XLoc::bar_index, YLoc yloc=YLoc::price)
Definition drawing.hpp:1190
int64_t pf_box_get_left(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1150
DrawingAllocationStatus
Definition drawing.hpp:91
double na< double >()
Definition na.hpp:64
void pf_box_set_rightbottom(DrawingArena< BoxRec > &a, Box h, int64_t right, double bottom)
Definition drawing.hpp:1162
int64_t pf_line_get_x2(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1093
int64_t pf_line_get_x1(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1092
void pf_line_set_x2(DrawingArena< LineRec > &a, Line h, int64_t v)
Definition drawing.hpp:1098
double pf_box_get_bottom(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1153
Linefill pf_linefill_new(DrawingArena< LinefillRec > &a, Line l1, Line l2)
Definition drawing.hpp:1235
void pf_label_set_x(DrawingArena< LabelRec > &a, Label h, int64_t v)
Definition drawing.hpp:1208
void pf_label_set_text(DrawingArena< LabelRec > &a, Label h, std::string text)
Definition drawing.hpp:1222
void pf_label_set_yloc(DrawingArena< LabelRec > &a, Label h, YLoc yloc)
Definition drawing.hpp:1219
bool is_na(const Line &h)
Definition drawing.hpp:58
void pf_line_set_xloc(DrawingArena< LineRec > &a, Line h, int64_t x1, int64_t x2, XLoc xloc)
Definition drawing.hpp:1113
Rec & pf_require_live(DrawingArena< Rec > &a, Handle h)
Definition drawing.hpp:994
void pf_line_set_first_point(DrawingArena< LineRec > &a, Line h, ChartPoint p)
Definition drawing.hpp:1107
double pf_box_get_top(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1152
void pf_line_set_x1(DrawingArena< LineRec > &a, Line h, int64_t v)
Definition drawing.hpp:1097
Label pf_label_new_pt(DrawingArena< LabelRec > &a, ChartPoint point, std::string text, YLoc yloc=YLoc::price)
Definition drawing.hpp:1195
double pf_line_get_y2(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1095
int64_t pf_label_get_x(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:1204
DrawingBoundConfig drawing_bound(std::int32_t slot_bound, std::uint32_t pending_creation_slots=1)
Definition drawing.hpp:561
void pf_box_delete(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1181
DrawingBoundConfig drawing_bound_from_linefill_sample(std::int32_t sampled_bound=detail::kDrawingLinefillObservedBound, std::uint32_t pending_creation_slots=1)
Definition drawing.hpp:676
void pf_line_set_second_point(DrawingArena< LineRec > &a, Line h, ChartPoint p)
Definition drawing.hpp:1110
void pf_line_delete(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:1135
void pf_box_set_left(DrawingArena< BoxRec > &a, Box h, int64_t v)
Definition drawing.hpp:1155
int64_t pf_box_get_right(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1151
double pf_line_get_price(DrawingArena< LineRec > &a, Line h, int64_t x)
Definition drawing.hpp:1119
void pf_line_set_y2(DrawingArena< LineRec > &a, Line h, double v)
Definition drawing.hpp:1100
void pf_label_set_y(DrawingArena< LabelRec > &a, Label h, double v)
Definition drawing.hpp:1209
Label pf_label_copy(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:1226
void pf_label_set_xloc(DrawingArena< LabelRec > &a, Label h, int64_t x, XLoc xloc)
Definition drawing.hpp:1216
void pf_box_set_bottom(DrawingArena< BoxRec > &a, Box h, double v)
Definition drawing.hpp:1158
DrawingBoundConfig drawing_bound_from_declared_count(std::int32_t declared_count=50, std::uint32_t pending_creation_slots=1)
Definition drawing.hpp:670
Box pf_box_copy(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:1175
DrawingAllocationStatus status
Definition drawing.hpp:98
std::optional< std::int32_t > id
Definition drawing.hpp:109
DrawingAllocationDiagnostic diagnostic
Definition drawing.hpp:108
std::uint32_t pending_creation_slots
Definition drawing.hpp:558
std::string text
Definition drawing.hpp:82
std::int32_t slot_for(std::int32_t id) const noexcept
Definition drawing.hpp:645
std::size_t free_slot() const noexcept
Definition drawing.hpp:660
std::vector< DirectoryEntry > directory
Definition drawing.hpp:639
std::deque< std::int32_t > order
Definition drawing.hpp:640
BoundDrawingState(DrawingBoundConfig config)
Definition drawing.hpp:631
static std::size_t storage_slots(DrawingBoundConfig config)
Definition drawing.hpp:618
static const Rec & read(const DrawingArena< Rec > &a, Handle h)
Definition drawing.hpp:1026
static void edit(DrawingArena< Rec > &a, Handle h, Edit &&apply)
Definition drawing.hpp:1036
static bool missing(const DrawingArena< Rec > &a, Handle h) noexcept
Definition drawing.hpp:1014
DrawingStaleSink & operator=(DrawingStaleSink &&other) noexcept
Definition drawing.hpp:268
DrawingStaleSink & operator=(const DrawingStaleSink &other)
Definition drawing.hpp:259
DrawingStaleSink(const DrawingStaleSink &other)
Definition drawing.hpp:256
DrawingStaleSink(DrawingStaleSink &&other) noexcept
Definition drawing.hpp:264
void reset() const noexcept
Definition drawing.hpp:247
pine_drawing_error(const std::string &msg)
Definition drawing.hpp:118
pine_drawing_id_exhausted(const char *object)
Definition drawing.hpp:122