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. The header is fully
10// self-contained and header-only (every function is `inline`); 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 -- so checkpointing is cheap: a copy of an
22// arena costs O(records) until the arena has been assigned from a copy (which
23// the scheduler's restore does from the first bar on), and from then on its
24// copies share their records and a write clones only the blocks it reaches
25// (DrawingRecordStore below). A checkpoint costs the records a script changed,
26// not every record it ever made; an arena that is never copied keeps its
27// records in a vector.
28//
29#include <array>
30#include <atomic>
31#include <vector>
32#include <deque>
33#include <memory>
34#include <new>
35#include <string>
36#include <algorithm>
37#include <cstdint>
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
85// ---- error type (spec §3.7) ------------------------------------------------
86// Derives from std::runtime_error so BacktestEngine::run()'s existing
87// `catch (const std::exception& e)` (src/engine_run.cpp) halts the backtest
88// exactly like TradingView when a drawing op touches a na handle.
89// Drawing is ALWAYS-ON and ALWAYS TV-faithful: no compile flag, no lenient mode.
90struct pine_drawing_error : std::runtime_error {
91 explicit pine_drawing_error(const std::string& msg) : std::runtime_error(msg) {}
92};
93
94namespace detail {
95
96// The Pine kind of a drawing record (pine_na_reference's object), known from
97// the record type at the call and never read from the script.
98template <class Rec>
99constexpr const char* drawing_object_name() noexcept {
100 if constexpr (std::is_same_v<Rec, LineRec>) return "line";
101 else if constexpr (std::is_same_v<Rec, BoxRec>) return "box";
102 else if constexpr (std::is_same_v<Rec, LabelRec>) return "label";
103 else if constexpr (std::is_same_v<Rec, LinefillRec>) return "linefill";
104 else static_assert(!std::is_same_v<Rec, Rec>, "name the drawing kind of this record");
105}
106
107// A drawing call the script got wrong, raised as the pine_drawing_error it
108// always was (the same English): a na handle is pine_na_reference, a bad
109// argument pine_invalid_argument. Out of line and cold, so the accessors that
110// check stay as small as they were.
111[[noreturn, gnu::noinline, gnu::cold]] inline void drawing_na_error(const char* object,
112 const char* what) {
114 {{"object", object}}, what);
115}
116[[noreturn, gnu::noinline, gnu::cold]] inline void drawing_argument_error(
117 const char* function, const char* argument, const char* rule, const char* what) {
120 {{"function", function}, {"argument", argument}, {"rule", rule}}, what);
121}
122
123// ---- record store: a vector, or blocks shared between copies ----------------
124// The records of one DrawingArena, indexed by id. A store starts as a vector,
125// exactly as the arena always was, and nothing in it is ever shared: a copy of
126// it is built from its records (O(records)). That copy holds them in blocks of
127// 32 -- the newest block, the one alloc appends to, beside a radix tree (a
128// 32-way trie over the id) that holds every earlier block -- and a copy of a
129// block store shares every block, as does a store assigned from one. A write
130// first gives the writing store its own copy of every block and tree node on
131// the way to the record that another copy still shares, then writes in place,
132// so a copy of a block store costs O(1) and a write after it one root-to-block
133// path (at most six nodes and a block for 2^31 ids). Every copy stays an
134// independent value: a shared block or node is never written.
135//
136// Under the scheduler's sequence (snapshot, restore, execute, commit) the first
137// snapshot copies the vector and the restore assigns the copy back, so from
138// the first bar on an arena and its checkpoint share blocks. An arena that is
139// never copied stays a vector.
140template <class Rec>
142 static_assert(std::is_copy_constructible_v<Rec> && std::is_nothrow_move_constructible_v<Rec>,
143 "a drawing record must be copyable and nothrow move-constructible");
144 static constexpr unsigned kBits = 5;
145 static constexpr std::uint32_t kWidth = std::uint32_t{1} << kBits;
146 static constexpr std::uint32_t kMask = kWidth - 1;
147
148 // Up to 32 records, constructed in place in slots [0, used) like a vector's
149 // elements; the other slots hold no object. Every block in the tree is full.
150 struct Block {
151 union Slot {
152 Rec rec;
153 Slot() noexcept {}
154 ~Slot() {}
155 };
156 std::uint32_t used = 0;
157 Slot slots[kWidth];
158
159 Block() noexcept {}
160 Block(const Block& other) {
161 try {
162 for (; used < other.used; ++used) ::new (&slots[used].rec) Rec(other.slots[used].rec);
163 } catch (...) {
164 clear();
165 throw;
166 }
167 }
168 Block& operator=(const Block&) = delete;
169 ~Block() { clear(); }
170
171 void clear() noexcept {
172 while (used > 0) slots[--used].rec.~Rec();
173 }
174 void append(Rec&& rec) noexcept {
175 ::new (&slots[used].rec) Rec(std::move(rec));
176 ++used;
177 }
178 };
179 struct Node { std::array<std::shared_ptr<void>, kWidth> kids{}; }; // a Node below kBits, else a Block
180
181 std::vector<Rec> flat_; // the records unless blocks_
182 std::shared_ptr<Node> root_; // the blocks before the tail; null until the first block fills
183 std::shared_ptr<Block> tail_; // records tail_first_ .. size_ - 1; null while empty
184 std::uint32_t size_ = 0;
185 std::uint32_t tail_first_ = 0; // a multiple of 32
186 unsigned shift_ = kBits; // the root's level: its kids are blocks when shift_ == kBits
187 bool blocks_ = false; // the records are in root_ and tail_, flat_ is empty
188 // Blocks only: a copy was taken of this store, or it was copied from one,
189 // so its blocks may be shared. Set on both sides by the copy; atomic
190 // because copying reads a const store, which threads may do at once.
191 mutable std::atomic<bool> shared_{false};
192
193 // The pointee of `slot`, for writing: itself when this store holds its only
194 // reference, else a private copy put in its place. The acquire fence pairs
195 // with the release decrement of a copy that dropped its reference on another
196 // thread, so its reads of the block happen before this store's writes
197 // (libstdc++ and libc++ read use_count() as an atomic load of that count).
198 template <class T>
199 T* own(std::shared_ptr<T>& slot) {
200 if (!shared_.load(std::memory_order_relaxed)) return slot.get();
201 if (slot.use_count() != 1) slot = std::make_shared<T>(*slot);
202 else std::atomic_thread_fence(std::memory_order_acquire);
203 return slot.get();
204 }
205 template <class T>
206 T* own_as(std::shared_ptr<void>& slot) {
207 if (!shared_.load(std::memory_order_relaxed)) return static_cast<T*>(slot.get());
208 if (slot.use_count() != 1) slot = std::make_shared<T>(*static_cast<const T*>(slot.get()));
209 else std::atomic_thread_fence(std::memory_order_acquire);
210 return static_cast<T*>(slot.get());
211 }
212
213 // A chain of nodes `level / kBits` high whose leftmost block is `block`.
214 static std::shared_ptr<void> path_to(unsigned level, const std::shared_ptr<Block>& block) {
215 std::shared_ptr<void> node = block;
216 for (; level > 0; level -= kBits) {
217 auto parent = std::make_shared<Node>();
218 parent->kids[0] = std::move(node);
219 node = std::move(parent);
220 }
221 return node;
222 }
223
224 // A read from blocks: the tail, or the walk from the root. Out of line and
225 // cold, like every block path below: what read(), write() and push_back()
226 // inline is the vector, the arena's own code before blocks existed.
227 [[gnu::noinline, gnu::cold]] const Rec& read_blocks(std::uint32_t id) const {
228 if (id >= tail_first_) return tail_->slots[id & kMask].rec;
229 const void* node = root_.get();
230 for (unsigned level = shift_; level > 0; level -= kBits)
231 node = static_cast<const Node*>(node)->kids[(id >> level) & kMask].get();
232 return static_cast<const Block*>(node)->slots[id & kMask].rec;
233 }
234
235 [[gnu::noinline, gnu::cold]] Rec& write_blocks(std::uint32_t id) {
236 if (!shared_.load(std::memory_order_relaxed)) return const_cast<Rec&>(read_blocks(id));
237 if (id >= tail_first_) return own(tail_)->slots[id & kMask].rec;
238 Node* node = own(root_);
239 for (unsigned level = shift_; level > kBits; level -= kBits)
240 node = own_as<Node>(node->kids[(id >> level) & kMask]);
241 return own_as<Block>(node->kids[(id >> kBits) & kMask])->slots[id & kMask].rec;
242 }
243
244 // Appends to blocks: through a tail another copy may share, or when the
245 // tail is full or absent, to a new tail block, the full one handed to the
246 // tree first.
247 [[gnu::noinline, gnu::cold]] void push_block(Rec&& rec) {
248 if (size_ != 0 && size_ - tail_first_ < kWidth) {
249 own(tail_)->append(std::move(rec));
250 ++size_;
251 return;
252 }
253 auto block = std::make_shared<Block>();
254 block->append(std::move(rec));
255 if (size_ != 0) push_tail();
256 tail_ = std::move(block);
257 tail_first_ = size_;
258 ++size_;
259 }
260 // Hands the full tail (records size_ - 32 .. size_ - 1) to the tree, growing
261 // the tree by a level when it is full. Every allocation precedes the one
262 // link that publishes the block, so a throw leaves every record as it was.
263 void push_tail() {
264 const std::uint32_t last = size_ - 1;
265 if (!root_) {
266 auto root = std::make_shared<Node>();
267 root->kids[0] = tail_;
268 root_ = std::move(root);
269 shift_ = kBits;
270 return;
271 }
272 if ((std::uint64_t{size_} >> kBits) > (std::uint64_t{1} << shift_)) {
273 auto root = std::make_shared<Node>();
274 root->kids[1] = path_to(shift_, tail_);
275 root->kids[0] = std::move(root_);
276 root_ = std::move(root);
277 shift_ += kBits;
278 return;
279 }
280 Node* node = own(root_);
281 for (unsigned level = shift_; level > kBits; level -= kBits) {
282 std::shared_ptr<void>& kid = node->kids[(last >> level) & kMask];
283 if (!kid) {
284 kid = path_to(level - kBits, tail_);
285 return;
286 }
287 node = own_as<Node>(kid);
288 }
289 node->kids[(last >> kBits) & kMask] = tail_;
290 }
291
292public:
294 // A copy of a vector store is built as blocks of its own; a copy of a block
295 // store shares its blocks.
296 DrawingRecordStore(const DrawingRecordStore& other) : blocks_(true) {
297 if (!other.blocks_) {
298 for (const Rec& rec : other.flat_) push_block(Rec(rec));
299 return;
300 }
301 other.shared_.store(true, std::memory_order_relaxed);
302 root_ = other.root_;
303 tail_ = other.tail_;
304 size_ = other.size_;
305 tail_first_ = other.tail_first_;
306 shift_ = other.shift_;
307 shared_.store(true, std::memory_order_relaxed);
308 }
310 if (this != &other) *this = DrawingRecordStore(other);
311 return *this;
312 }
313 // A moved-from store is empty, like the vector it replaced.
315 : flat_(std::move(other.flat_)), root_(std::move(other.root_)),
316 tail_(std::move(other.tail_)), size_(std::exchange(other.size_, 0)),
317 tail_first_(std::exchange(other.tail_first_, 0)),
318 shift_(std::exchange(other.shift_, kBits)), blocks_(std::exchange(other.blocks_, false)),
319 shared_(other.shared_.exchange(false, std::memory_order_relaxed)) {
320 other.flat_.clear();
321 }
323 if (this != &other) {
324 flat_ = std::move(other.flat_);
325 other.flat_.clear();
326 root_ = std::move(other.root_);
327 tail_ = std::move(other.tail_);
328 size_ = std::exchange(other.size_, 0);
329 tail_first_ = std::exchange(other.tail_first_, 0);
330 shift_ = std::exchange(other.shift_, kBits);
331 blocks_ = std::exchange(other.blocks_, false);
332 shared_.store(other.shared_.exchange(false, std::memory_order_relaxed),
333 std::memory_order_relaxed);
334 }
335 return *this;
336 }
337
338 int32_t size() const { return static_cast<int32_t>(size_); }
339
340 // Both take an id in [0, size()).
341 const Rec& read(std::uint32_t id) const {
342 if (!blocks_) return flat_[id];
343 return read_blocks(id);
344 }
345 Rec& write(std::uint32_t id) {
346 if (!blocks_) return flat_[id];
347 return write_blocks(id);
348 }
349
350 void push_back(Rec&& rec) {
351 if (!blocks_) {
352 flat_.push_back(std::move(rec));
353 ++size_;
354 return;
355 }
356 push_block(std::move(rec));
357 }
358};
359
360} // namespace detail
361
362// A na handle (id < 0) or an out-of-range id is ALWAYS an error — there is no
363// record to read. NOTE: a DEAD record (alive == false, after an explicit
364// line.delete() or FIFO eviction) is NOT an error — TV keeps the object's data
365// read/writable after delete (the object only leaves the chart + the *.all
366// list), so the engine must too. See corpus/validation/drawing-delete-halt,
367// where TV keeps producing trades (2504) after the strategy's line.delete().
368
369// ---- arena template (spec §3.4) --------------------------------------------
370// ids are monotonic and never reused (dead slots are tombstoned, not recycled)
371// -> no ABA, reproducible across runs. Exact-cap FIFO eviction (oldest first).
372// A copy copies order_, which holds at most cap ids, and shares the records
373// once they are in blocks (detail::DrawingRecordStore): O(live), whatever the
374// arena has allocated. A copy of an arena whose records are still in a vector
375// copies them (O(records)); the arena shares with its copies once it has been
376// assigned from one.
377template <class Rec>
379 detail::DrawingRecordStore<Rec> recs_; // index == id; dead slots retained so ids are stable & monotonic
380 std::deque<int32_t> order_; // live ids, oldest -> newest == the *.all order; drives FIFO eviction
381 int cap_;
382public:
383 explicit DrawingArena(int cap = 50) : cap_(std::max(1, cap)) {}
384 DrawingArena(const DrawingArena&) = default;
386 // A moved-from arena is empty and keeps its cap. A self-move keeps the
387 // arena (a moved deque would empty order_ and strand the records the store
388 // kept).
390 : recs_(std::move(other.recs_)), order_(std::move(other.order_)), cap_(other.cap_) {
391 other.order_.clear();
392 }
394 if (this != &other) {
395 recs_ = std::move(other.recs_);
396 order_ = std::move(other.order_);
397 other.order_.clear();
398 cap_ = other.cap_;
399 }
400 return *this;
401 }
402
403 int32_t alloc(Rec r) {
404 while ((int)order_.size() >= cap_) { // exact-cap FIFO eviction (oldest first)
405 // Tombstone before leaving order_: the write may copy a shared block
406 // and throw, which must leave the record live and in order_.
407 recs_.write(static_cast<std::uint32_t>(order_.front())).alive = false;
408 order_.pop_front();
409 }
410 int32_t id = recs_.size();
411 r.alive = true;
412 recs_.push_back(std::move(r));
413 order_.push_back(id);
414 return id;
415 }
416
417 void erase(int32_t id) { // delete(): silent no-op on na/dead
418 if (!alive(id)) return;
419 recs_.write(static_cast<std::uint32_t>(id)).alive = false;
420 order_.erase(std::find(order_.begin(), order_.end(), id));
421 }
422
423 bool alive(int32_t id) const {
424 return id >= 0 && id < recs_.size() && recs_.read(static_cast<std::uint32_t>(id)).alive;
425 }
426
427 // Number of slots ever allocated (dead slots retained). ids in [0, size())
428 // reference a real record (live OR dead); ids outside that range or < 0 are
429 // na/invalid.
430 int32_t size() const { return recs_.size(); }
431
432 // The mutable form writes: it unshares the record's block from every copy.
433 // A reference either form returns lasts until the arena is next copied,
434 // assigned or written.
435 Rec& at(int32_t id) { return recs_.write(static_cast<std::uint32_t>(id)); }
436 const Rec& at(int32_t id) const { return recs_.read(static_cast<std::uint32_t>(id)); }
437
438 const std::deque<int32_t>& order() const { return order_; } // backs line.all/box.all if ever needed
439};
440
441// ---- shared helpers --------------------------------------------------------
442// Returns a reference to the record for a handle. Only a na handle (id < 0) or
443// an out-of-range id throws; a DEAD record (alive == false, e.g. after an
444// explicit line.delete() or FIFO eviction) is returned as-is — its geometry
445// remains read/writable, matching TV (delete leaves the chart + *.all, not the
446// data). See corpus/validation/drawing-delete-halt (2504 post-delete trades).
447// Setters take the mutable form; getters the const one, which never unshares.
448template <class Rec, class Handle>
449inline Rec& pf_require_live(DrawingArena<Rec>& a, Handle h) {
450 if (h.id < 0 || h.id >= a.size())
451 detail::drawing_na_error(detail::drawing_object_name<Rec>(), "drawing access on na handle");
452 return a.at(h.id);
453}
454template <class Rec, class Handle>
455inline const Rec& pf_require_live(const DrawingArena<Rec>& a, Handle h) {
456 if (h.id < 0 || h.id >= a.size())
457 detail::drawing_na_error(detail::drawing_object_name<Rec>(), "drawing access on na handle");
458 return a.at(h.id);
459}
460
461// Selects the x-coordinate a ChartPoint contributes for a given xloc.
462inline int64_t pf_point_x(const ChartPoint& p, XLoc xloc) {
463 return (xloc == XLoc::bar_time) ? p.time : p.index;
464}
465
466// ============================ line ==========================================
467inline Line pf_line_new(DrawingArena<LineRec>& a, int64_t x1, double y1, int64_t x2, double y2,
468 XLoc xloc = XLoc::bar_index, bool el = false, bool er = false) {
469 // *_new(na,na,na,na) allocates a LIVE record with na coords (NOT a na handle).
470 return Line{ a.alloc(LineRec{ x1, x2, y1, y2, xloc, el, er, true }) };
471}
473 XLoc xloc = XLoc::bar_index) {
474 return Line{ a.alloc(LineRec{ pf_point_x(p1, xloc), pf_point_x(p2, xloc),
475 p1.price, p2.price, xloc, false, false, true }) };
476}
477
478inline int64_t pf_line_get_x1(DrawingArena<LineRec>& a, Line h) { return pf_require_live(std::as_const(a), h).x1; }
479inline int64_t pf_line_get_x2(DrawingArena<LineRec>& a, Line h) { return pf_require_live(std::as_const(a), h).x2; }
480inline double pf_line_get_y1(DrawingArena<LineRec>& a, Line h) { return pf_require_live(std::as_const(a), h).y1; }
481inline double pf_line_get_y2(DrawingArena<LineRec>& a, Line h) { return pf_require_live(std::as_const(a), h).y2; }
482
483inline void pf_line_set_x1(DrawingArena<LineRec>& a, Line h, int64_t v) { pf_require_live(a, h).x1 = v; }
484inline void pf_line_set_x2(DrawingArena<LineRec>& a, Line h, int64_t v) { pf_require_live(a, h).x2 = v; }
485inline void pf_line_set_y1(DrawingArena<LineRec>& a, Line h, double v) { pf_require_live(a, h).y1 = v; }
486inline void pf_line_set_y2(DrawingArena<LineRec>& a, Line h, double v) { pf_require_live(a, h).y2 = v; }
487inline void pf_line_set_xy1(DrawingArena<LineRec>& a, Line h, int64_t x, double y) {
488 LineRec& r = pf_require_live(a, h); r.x1 = x; r.y1 = y;
489}
490inline void pf_line_set_xy2(DrawingArena<LineRec>& a, Line h, int64_t x, double y) {
491 LineRec& r = pf_require_live(a, h); r.x2 = x; r.y2 = y;
492}
494 LineRec& r = pf_require_live(a, h); r.x1 = pf_point_x(p, r.xloc); r.y1 = p.price;
495}
497 LineRec& r = pf_require_live(a, h); r.x2 = pf_point_x(p, r.xloc); r.y2 = p.price;
498}
499inline void pf_line_set_xloc(DrawingArena<LineRec>& a, Line h, int64_t x1, int64_t x2, XLoc xloc) {
500 // REINTERPRET: stores new numbers + flag; does NOT convert coord spaces (TV-faithful, spec §3.9).
501 LineRec& r = pf_require_live(a, h); r.x1 = x1; r.x2 = x2; r.xloc = xloc;
502}
503
504// real computation: infinite line, ignores extend; valid for xloc.bar_index (spec §3.6).
505inline double pf_line_get_price(DrawingArena<LineRec>& a, Line h, int64_t x) {
506 if (h.id < 0 || h.id >= a.size()) // na/out-of-range line -> throw -> halt (like TV)
507 detail::drawing_na_error("line", "drawing access on na handle");
508 const LineRec& r = std::as_const(a).at(h.id); // dead record's geometry is still readable (TV-faithful)
509 if (r.xloc == XLoc::bar_time) // TV errors on bar_time lines
510 detail::drawing_argument_error("line.get_price", "xloc", "requires_xloc_bar_index",
511 "line.get_price requires xloc.bar_index");
512 if (r.x1 == r.x2) return na<double>(); // degenerate vertical -> na (matches TV)
513 return r.y1 + (r.y2 - r.y1) / (double)(r.x2 - r.x1) * (double)(x - r.x1); // infinite line, ignores extend
514}
515
517 if (!a.alive(h.id)) return Line{}; // copy of na/dead -> na handle (silent)
518 LineRec r = std::as_const(a).at(h.id); // deep copy = independent new arena record
519 return Line{ a.alloc(r) };
520}
521inline void pf_line_delete(DrawingArena<LineRec>& a, Line h) { a.erase(h.id); } // silent no-op on na/dead
522
523// ============================ box ===========================================
524inline Box pf_box_new(DrawingArena<BoxRec>& a, int64_t left, double top, int64_t right, double bottom,
525 XLoc xloc = XLoc::bar_index) {
526 return Box{ a.alloc(BoxRec{ left, right, top, bottom, xloc, true }) };
527}
529 XLoc xloc = XLoc::bar_index) {
530 return Box{ a.alloc(BoxRec{ pf_point_x(top_left, xloc), pf_point_x(bottom_right, xloc),
531 top_left.price, bottom_right.price, xloc, true }) };
532}
533
534inline int64_t pf_box_get_left (DrawingArena<BoxRec>& a, Box h) { return pf_require_live(std::as_const(a), h).left; }
535inline int64_t pf_box_get_right (DrawingArena<BoxRec>& a, Box h) { return pf_require_live(std::as_const(a), h).right; }
536inline double pf_box_get_top (DrawingArena<BoxRec>& a, Box h) { return pf_require_live(std::as_const(a), h).top; }
537inline double pf_box_get_bottom(DrawingArena<BoxRec>& a, Box h) { return pf_require_live(std::as_const(a), h).bottom; }
538
539inline void pf_box_set_left (DrawingArena<BoxRec>& a, Box h, int64_t v) { pf_require_live(a, h).left = v; }
540inline void pf_box_set_right (DrawingArena<BoxRec>& a, Box h, int64_t v) { pf_require_live(a, h).right = v; }
541inline void pf_box_set_top (DrawingArena<BoxRec>& a, Box h, double v) { pf_require_live(a, h).top = v; }
542inline void pf_box_set_bottom(DrawingArena<BoxRec>& a, Box h, double v) { pf_require_live(a, h).bottom = v; }
543inline void pf_box_set_lefttop(DrawingArena<BoxRec>& a, Box h, int64_t left, double top) {
544 BoxRec& r = pf_require_live(a, h); r.left = left; r.top = top;
545}
546inline void pf_box_set_rightbottom(DrawingArena<BoxRec>& a, Box h, int64_t right, double bottom) {
547 BoxRec& r = pf_require_live(a, h); r.right = right; r.bottom = bottom;
548}
550 BoxRec& r = pf_require_live(a, h); r.left = pf_point_x(p, r.xloc); r.top = p.price;
551}
553 BoxRec& r = pf_require_live(a, h); r.right = pf_point_x(p, r.xloc); r.bottom = p.price;
554}
555inline void pf_box_set_xloc(DrawingArena<BoxRec>& a, Box h, int64_t left, int64_t right, XLoc xloc) {
556 BoxRec& r = pf_require_live(a, h); r.left = left; r.right = right; r.xloc = xloc; // reinterpret only
557}
558
560 if (!a.alive(h.id)) return Box{};
561 BoxRec r = std::as_const(a).at(h.id);
562 return Box{ a.alloc(r) };
563}
564inline void pf_box_delete(DrawingArena<BoxRec>& a, Box h) { a.erase(h.id); }
565
566// ============================ label =========================================
567// NOTE: yloc.abovebar / yloc.belowbar should auto-place y at the bar high/low
568// (spec §3.9). The spec's pf_label_new signature is intentionally decoupled from
569// the engine and does NOT receive bar high/low, so y is stored AS-IS and yloc is
570// store-only here. yloc abovebar/belowbar auto-place needs bar high/low at the
571// call site — codegen would pass close()/high()/low() if ever required. Inert:
572// no corpus label reads get_y back (spec note 4), so no parity claim.
573inline Label pf_label_new(DrawingArena<LabelRec>& a, int64_t x, double y, std::string text,
574 XLoc xloc = XLoc::bar_index, YLoc yloc = YLoc::price) {
575 return Label{ a.alloc(LabelRec{ x, y, xloc, yloc, std::move(text), true }) };
576}
577inline Label pf_label_new_pt(DrawingArena<LabelRec>& a, ChartPoint point, std::string text,
578 YLoc yloc = YLoc::price) {
579 // xloc carried by the ChartPoint: bar_time only when index is na but time is set.
580 XLoc xloc = (is_na(point.index) && !is_na(point.time)) ? XLoc::bar_time : XLoc::bar_index;
581 return Label{ a.alloc(LabelRec{ pf_point_x(point, xloc), point.price, xloc, yloc,
582 std::move(text), true }) };
583}
584
585inline int64_t pf_label_get_x(DrawingArena<LabelRec>& a, Label h) { return pf_require_live(std::as_const(a), h).x; }
586inline double pf_label_get_y(DrawingArena<LabelRec>& a, Label h) { return pf_require_live(std::as_const(a), h).y; }
587inline std::string pf_label_get_text(DrawingArena<LabelRec>& a, Label h) { return pf_require_live(std::as_const(a), h).text; }
588
589inline void pf_label_set_x(DrawingArena<LabelRec>& a, Label h, int64_t v) { pf_require_live(a, h).x = v; }
590inline void pf_label_set_y(DrawingArena<LabelRec>& a, Label h, double v) { pf_require_live(a, h).y = v; }
591inline void pf_label_set_xy(DrawingArena<LabelRec>& a, Label h, int64_t x, double y) {
592 LabelRec& r = pf_require_live(a, h); r.x = x; r.y = y;
593}
595 LabelRec& r = pf_require_live(a, h); r.x = pf_point_x(p, r.xloc); r.y = p.price;
596}
597inline void pf_label_set_xloc(DrawingArena<LabelRec>& a, Label h, int64_t x, XLoc xloc) {
598 LabelRec& r = pf_require_live(a, h); r.x = x; r.xloc = xloc; // sets x AND xloc
599}
601 pf_require_live(a, h).yloc = yloc;
602}
603inline void pf_label_set_text(DrawingArena<LabelRec>& a, Label h, std::string text) {
604 pf_require_live(a, h).text = std::move(text);
605}
606
608 if (!a.alive(h.id)) return Label{};
609 LabelRec r = std::as_const(a).at(h.id); // deep copy (incl. std::string text)
610 return Label{ a.alloc(std::move(r)) };
611}
613
614// ============================ linefill ======================================
616 // Stores the two Line handle ids inertly (color dropped). linefill.new(na,na,na)
617 // is legal -> a LIVE record over na line ids (spec note 1).
618 return Linefill{ a.alloc(LinefillRec{ l1.id, l2.id, true }) };
619}
621 return Line{ pf_require_live(std::as_const(a), h).line1 };
622}
624 return Line{ pf_require_live(std::as_const(a), h).line2 };
625}
627
628// ============================ visual no-op sink (spec §3.6) ==================
629// Evaluates + discards args (so side effects/typechecking still happen), returns void.
630// Backs every VISUAL setter/ctor-kwarg: line/box/label set_color/set_style/...
631template <class... A> inline void pf_noop(A&&...) {}
632
633} // namespace pineforge
DrawingArena(const DrawingArena &)=default
DrawingArena & operator=(const DrawingArena &)=default
const std::deque< int32_t > & order() const
Definition drawing.hpp:438
int32_t alloc(Rec r)
Definition drawing.hpp:403
DrawingArena(int cap=50)
Definition drawing.hpp:383
int32_t size() const
Definition drawing.hpp:430
Rec & at(int32_t id)
Definition drawing.hpp:435
const Rec & at(int32_t id) const
Definition drawing.hpp:436
DrawingArena & operator=(DrawingArena &&other) noexcept
Definition drawing.hpp:393
bool alive(int32_t id) const
Definition drawing.hpp:423
DrawingArena(DrawingArena &&other)
Definition drawing.hpp:389
void erase(int32_t id)
Definition drawing.hpp:417
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
Rec & write(std::uint32_t id)
Definition drawing.hpp:345
DrawingRecordStore & operator=(DrawingRecordStore &&other) noexcept
Definition drawing.hpp:322
DrawingRecordStore(DrawingRecordStore &&other) noexcept
Definition drawing.hpp:314
DrawingRecordStore & operator=(const DrawingRecordStore &other)
Definition drawing.hpp:309
const Rec & read(std::uint32_t id) const
Definition drawing.hpp:341
DrawingRecordStore(const DrawingRecordStore &other)
Definition drawing.hpp:296
void drawing_na_error(const char *object, const char *what)
Definition drawing.hpp:111
void drawing_argument_error(const char *function, const char *argument, const char *rule, const char *what)
Definition drawing.hpp:116
constexpr const char * drawing_object_name() noexcept
Definition drawing.hpp:99
void pf_linefill_delete(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:626
void pf_line_set_xy1(DrawingArena< LineRec > &a, Line h, int64_t x, double y)
Definition drawing.hpp:487
std::string pf_label_get_text(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:587
void pf_label_delete(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:612
void pf_line_set_xy2(DrawingArena< LineRec > &a, Line h, int64_t x, double y)
Definition drawing.hpp:490
double pf_line_get_y1(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:480
void pf_box_set_bottom_right_point(DrawingArena< BoxRec > &a, Box h, ChartPoint p)
Definition drawing.hpp:552
Line pf_linefill_get_line2(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:623
int64_t pf_point_x(const ChartPoint &p, XLoc xloc)
Definition drawing.hpp:462
Line pf_line_copy(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:516
void pf_box_set_top(DrawingArena< BoxRec > &a, Box h, double v)
Definition drawing.hpp:541
void pf_noop(A &&...)
Definition drawing.hpp:631
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:524
void pf_box_set_lefttop(DrawingArena< BoxRec > &a, Box h, int64_t left, double top)
Definition drawing.hpp:543
void pf_label_set_point(DrawingArena< LabelRec > &a, Label h, ChartPoint p)
Definition drawing.hpp:594
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:528
void pf_box_set_right(DrawingArena< BoxRec > &a, Box h, int64_t v)
Definition drawing.hpp:540
Line pf_line_new_pts(DrawingArena< LineRec > &a, ChartPoint p1, ChartPoint p2, XLoc xloc=XLoc::bar_index)
Definition drawing.hpp:472
void pf_box_set_xloc(DrawingArena< BoxRec > &a, Box h, int64_t left, int64_t right, XLoc xloc)
Definition drawing.hpp:555
void pf_box_set_top_left_point(DrawingArena< BoxRec > &a, Box h, ChartPoint p)
Definition drawing.hpp:549
double pf_label_get_y(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:586
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:467
void pf_label_set_xy(DrawingArena< LabelRec > &a, Label h, int64_t x, double y)
Definition drawing.hpp:591
void pf_line_set_y1(DrawingArena< LineRec > &a, Line h, double v)
Definition drawing.hpp:485
Line pf_linefill_get_line1(DrawingArena< LinefillRec > &a, Linefill h)
Definition drawing.hpp:620
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:573
int64_t pf_box_get_left(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:534
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:546
int64_t pf_line_get_x2(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:479
int64_t pf_line_get_x1(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:478
void pf_line_set_x2(DrawingArena< LineRec > &a, Line h, int64_t v)
Definition drawing.hpp:484
double pf_box_get_bottom(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:537
Linefill pf_linefill_new(DrawingArena< LinefillRec > &a, Line l1, Line l2)
Definition drawing.hpp:615
void pf_label_set_x(DrawingArena< LabelRec > &a, Label h, int64_t v)
Definition drawing.hpp:589
void pf_label_set_text(DrawingArena< LabelRec > &a, Label h, std::string text)
Definition drawing.hpp:603
void pf_label_set_yloc(DrawingArena< LabelRec > &a, Label h, YLoc yloc)
Definition drawing.hpp:600
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:499
Rec & pf_require_live(DrawingArena< Rec > &a, Handle h)
Definition drawing.hpp:449
void pf_line_set_first_point(DrawingArena< LineRec > &a, Line h, ChartPoint p)
Definition drawing.hpp:493
double pf_box_get_top(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:536
void pf_line_set_x1(DrawingArena< LineRec > &a, Line h, int64_t v)
Definition drawing.hpp:483
Label pf_label_new_pt(DrawingArena< LabelRec > &a, ChartPoint point, std::string text, YLoc yloc=YLoc::price)
Definition drawing.hpp:577
double pf_line_get_y2(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:481
int64_t pf_label_get_x(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:585
void pf_box_delete(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:564
void pf_line_set_second_point(DrawingArena< LineRec > &a, Line h, ChartPoint p)
Definition drawing.hpp:496
void pf_line_delete(DrawingArena< LineRec > &a, Line h)
Definition drawing.hpp:521
void pf_box_set_left(DrawingArena< BoxRec > &a, Box h, int64_t v)
Definition drawing.hpp:539
int64_t pf_box_get_right(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:535
double pf_line_get_price(DrawingArena< LineRec > &a, Line h, int64_t x)
Definition drawing.hpp:505
void pf_line_set_y2(DrawingArena< LineRec > &a, Line h, double v)
Definition drawing.hpp:486
void pf_label_set_y(DrawingArena< LabelRec > &a, Label h, double v)
Definition drawing.hpp:590
Label pf_label_copy(DrawingArena< LabelRec > &a, Label h)
Definition drawing.hpp:607
void pf_label_set_xloc(DrawingArena< LabelRec > &a, Label h, int64_t x, XLoc xloc)
Definition drawing.hpp:597
void pf_box_set_bottom(DrawingArena< BoxRec > &a, Box h, double v)
Definition drawing.hpp:542
Box pf_box_copy(DrawingArena< BoxRec > &a, Box h)
Definition drawing.hpp:559
std::string text
Definition drawing.hpp:82
pine_drawing_error(const std::string &msg)
Definition drawing.hpp:91