PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
run_failure.hpp
Go to the documentation of this file.
1#pragma once
2
3// Stable run-failure codes: every engine-originated run failure carries one.
4//
5// A run that fails leaves two things beside each other: the English text
6// strategy_get_last_error() has always returned, unchanged byte for byte, and
7// a code from a closed vocabulary (strategy_get_last_error_code()) with its
8// typed arguments as one canonical JSON object
9// (strategy_get_last_error_args()). The vocabulary, its classes, its argument
10// kinds and closed value lists, and the English each code is written with,
11// are published as run_failure_codes.json (schema
12// pineforge-run-failure-catalog/v1); include/pineforge/run_failure_codes.hpp
13// and src/run_failure_registry.inc are generated from it
14// (scripts/check_run_failure_codes.py --write-registry) and the source guard
15// holds them to it.
16//
17// The rules the channel keeps:
18// * a code is chosen where the failure is raised, never read from data: a
19// text a script writes (runtime.error) can only ever read
20// strategy_runtime_error, whatever it says;
21// * an argument is built only from a literal the transpiler emitted (a call's
22// spelling, its line, a literal symbol, an input title), a closed engine
23// vocabulary, or a value the run request supplied (a row of its OHLCV, its
24// timeframes) -- never from a value the script computes while it runs;
25// Q7's run_aborted additionally carries only engine-owned completion count
26// and started witnesses, never a script-computed index or value;
27// * a vocabulary argument outside its closed list, an undeclared argument, a
28// wrong kind or a missing required argument turns the failure into
29// engine_invariant (the English is kept): a wrong call cannot mint a code
30// or an open string.
31//
32// Thrown types do not change: coded<Base> derives from Base, so every typed
33// catch and every what() is what it was. The classifier
34// (classify_run_failure) reads the carried code; an exception without one is
35// classified by its type as a safety net only -- std::bad_alloc is
36// out_of_memory, the std::logic_error family engine_invariant, anything else
37// engine_unclassified_error.
38//
39// ---------------------------------------------------------------------------
40// The codegen contract (PINEFORGE_HAS_RUN_FAILURE_CODES_V1). Generated code
41// may call exactly these, under `#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1`
42// (and keep `pine_runtime_error(std::string(english))` in the `#else` arm).
43// Every helper is [[noreturn]], throws coded<std::runtime_error>, hardwires its
44// code and never takes one; `english` is the text the stop has today, and is
45// what strategy_get_last_error() reads, byte for byte. Pointer arguments are
46// codegen literals (`const char*`, never NULL except where marked); `line` is
47// the call's line in the source the transpiler read.
48//
49// pine_runtime_error(const std::string& message) (log.hpp)
50// -> strategy_runtime_error {}
51// pine_no_data_stop(const char* function, const char* call,
52// std::int32_t line, const char* english)
53// -> no_data_request {function, call, line}
54// function: request.financial | request.earnings | request.dividends |
55// request.splits | request.footprint | request.economic |
56// request.quandl | request.seed | request.currency_rate
57// pine_other_symbol_stop(const char* function,
58// const char* symbol_literal_or_null,
59// const char* call, std::int32_t line,
60// const char* english)
61// -> other_symbol_request {function, symbol?, call, line}
62// function: request.security | request.security_lower_tf; symbol only
63// when the call spells it as a literal (NULL otherwise)
64// pine_array_stop(const char* reason, const char* method_or_null,
65// const char* english)
66// -> pine_array_error {reason, method?}
67// reason: index_out_of_bounds | slice_range_inverted |
68// empty_array_access | size_invalid
69// method: new | get | set | insert | remove | percentrank | fill |
70// slice | first | last | pop | shift
71// pine_collection_stop(const char* collection, const char* reason,
72// const char* english)
73// collection: array | matrix (any other collection: engine_invariant)
74// reason historical_modified -> pine_array_error {reason, collection}
75// reason na_reference -> pine_na_reference {object: collection}
76// pine_na_stop(const char* object, const char* english)
77// -> pine_na_reference {object}
78// object: array | matrix | map | line | box | label | linefill |
79// polyline | table | udt_object
80// pine_limit_stop(const char* limit, std::int64_t max, const char* english)
81// -> pine_runtime_limit {limit, max}
82// limit: map_pairs | max_bars_back | matrix_elements | udt_objects;
83// max is the limit's engine or transpiler constant (pass -1 for none)
84// pine_unsupported_stop(const char* reason, std::int32_t line,
85// const char* english)
86// -> request_unsupported {reason, line}
87// reason: lower_tf_lookahead_or_gaps | nested_heikinashi_request
88// (line -1: none)
89// pine_string_stop(const char* reason, const char* english)
90// -> pine_string_error {reason}
91// reason: substring_out_of_range | format_index_overflow
92// pine_engine_invariant(const char* english)
93// -> engine_invariant {}
94//
95// note_run_failure(BacktestEngine&, const char* entrypoint,
96// const std::exception&)
97// the generated wrapper's catch: writes "<entrypoint>: <what()>" (the
98// text it writes today) with the exception's code;
99// note_run_failure_unknown(BacktestEngine&, const char* entrypoint)
100// its catch (...): "<entrypoint>: unknown C++ exception",
101// engine_unclassified_error;
102// note_run_failure(BacktestEngine&, std::string text, RunFailureCode,
103// const RunFailureArgs&)
104// a latched setter refusal: setting_rejected with its entrypoint and
105// reason;
106// note_run_failure(BacktestEngine&, std::string text,
107// const RunFailureValue&)
108// the same refusal with the value its RunFailureInfo holds.
109//
110// The latched setter failure also uses these names: RunFailureInfo as a
111// public base of its own exception type (beside
112// checked_settings::LatchedSettingsFailure) and as a copy-assignable member,
113// built with the (RunFailureCode, const RunFailureArgs&) constructor and read
114// with run_failure(); RunFailureValue; and the codes RunFailureCode::none,
115// ::setting_rejected and ::out_of_memory. An exception thrown where the script
116// is prepared that already carries a code keeps it; a latched failure without
117// one reads setting_rejected.
118// ---------------------------------------------------------------------------
119
121
122#include <cstddef>
123#include <cstdint>
124#include <cstring>
125#include <exception>
126#include <memory>
127#include <stdexcept>
128#include <string>
129#include <type_traits>
130#include <utility>
131#include <vector>
132
133#define PINEFORGE_HAS_RUN_FAILURE_CODES_V1 1
134
135namespace pineforge {
136
137inline namespace engine_script_run_v19 {
138class BacktestEngine;
139}
140
141/// The class a code belongs to (the catalog's `class`).
153
154/// An argument's kind (the catalog's `kind`).
155enum class RunFailureArgKind : std::uint8_t {
164 // The kinds below are appended, so every earlier kind keeps its value.
165 // string: text. nullable_string: text or a JSON null. scalar: text, an
166 // integer, a finite number, a boolean or a JSON null. None takes an array
167 // or an object.
171 // Q7: exactly a JSON true/false, with all earlier kind values preserved.
173};
174
175/// One typed argument as a raise site spells it. `name` is a literal; a text
176/// value is copied.
178 // boolean and null_value are appended, so the earlier values keep theirs. A
179 // boolean is held in `integer` (0 or 1) and serializes as true or false; a
180 // null_value holds nothing and serializes as null. Neither is ever a string.
181 enum class Type : std::uint8_t { text, integer, number, boolean, null_value };
182 const char* name = "";
184 std::string text;
185 std::int64_t integer = 0;
186 double number = 0.0;
187
188 // Out of line (src/run_failure.cpp): a literal value is handed over by
189 // address, so it stays one contiguous string wherever it is compiled.
190 RunFailureArg(const char* arg_name, const char* value);
191 RunFailureArg(const char* arg_name, std::string value)
192 : name(arg_name), text(std::move(value)) {}
193 // A JSON null, spelled with the literal nullptr. A `const char*` variable
194 // that happens to be null still reads as the empty text, as it always did.
195 RunFailureArg(const char* arg_name, std::nullptr_t)
196 : name(arg_name), type(Type::null_value) {}
197 // A JSON true or false. Only an argument of exactly type bool takes it: no
198 // pointer or integer converts to a boolean here.
199 template <typename Boolean,
200 typename std::enable_if<std::is_same<Boolean, bool>::value, int>::type = 0>
201 RunFailureArg(const char* arg_name, Boolean value)
202 : name(arg_name), type(Type::boolean), integer(value ? 1 : 0) {}
203 template <typename Integer,
204 typename std::enable_if<std::is_integral<Integer>::value
205 && !std::is_same<Integer, bool>::value, int>::type = 0>
206 RunFailureArg(const char* arg_name, Integer value)
207 : name(arg_name), type(Type::integer), integer(static_cast<std::int64_t>(value)) {}
208 template <typename Floating,
209 typename std::enable_if<std::is_floating_point<Floating>::value, int>::type = 0>
210 RunFailureArg(const char* arg_name, Floating value)
211 : name(arg_name), type(Type::number), number(static_cast<double>(value)) {}
212};
213
214using RunFailureArgs = std::vector<RunFailureArg>;
215
216/// A validated code and its canonical arguments: the JSON object with its keys
217/// sorted and no whitespace, or null for a code without arguments ("{}").
220 std::shared_ptr<const std::string> args;
221};
222
223/// Validate `args` against `code`'s registry entry and canonicalize them. Any
224/// violation yields engine_invariant without arguments. Allocation failure
225/// yields out_of_memory.
227
228/// The code's catalog name ("" for none or an unknown value).
229const char* run_failure_code_name(RunFailureCode code) noexcept;
230/// The code's catalog class and retryable flag.
233
234/// The code an exception carries: a coded exception's own; else the safety
235/// net by type (std::bad_alloc -> out_of_memory, std::logic_error ->
236/// engine_invariant, other -> engine_unclassified_error).
237RunFailureValue classify_run_failure(const std::exception& error) noexcept;
238
239/// The code carried beside a thrown exception. A mixin: it adds no base of
240/// std::exception, so coded<Base> is still exactly a Base to every catch.
242public:
243 RunFailureInfo(RunFailureCode code, const RunFailureArgs& args) noexcept
244 : value_(make_run_failure(code, args)) {}
245 // An already validated value (a code another site recorded, carried on).
246 explicit RunFailureInfo(RunFailureValue value) noexcept : value_(std::move(value)) {}
247 RunFailureInfo(const RunFailureInfo&) noexcept = default;
248 RunFailureInfo& operator=(const RunFailureInfo&) noexcept = default;
250
251 RunFailureCode run_failure_code() const noexcept { return value_.code; }
252 const RunFailureValue& run_failure() const noexcept { return value_; }
253
254private:
255 RunFailureValue value_;
256};
257
258/// `Base` with a code: throw coded<std::invalid_argument>(code, {args}, text).
259template <class Base>
260class coded : public Base, public RunFailureInfo {
261public:
262 coded(RunFailureCode code, const RunFailureArgs& args, const std::string& what)
263 : Base(what), RunFailureInfo(code, args) {}
264 coded(RunFailureCode code, const RunFailureArgs& args, const char* what)
265 : Base(what), RunFailureInfo(code, args) {}
266 // Keep structured exception context (for example system_error::code())
267 // when the base cannot be reconstructed from its English text alone.
268 coded(RunFailureCode code, const RunFailureArgs& args, const Base& error)
269 : Base(error), RunFailureInfo(code, args) {}
270 coded(RunFailureValue value, const std::string& what)
271 : Base(what), RunFailureInfo(std::move(value)) {}
272 coded(RunFailureValue value, const char* what)
273 : Base(what), RunFailureInfo(std::move(value)) {}
274};
275
276// The record beside BacktestEngine::last_error(): writes the text and the
277// code together, so they never disagree. Defined with the execution consumer
278// that stores the record (src/run_failure.cpp).
279void note_run_failure(BacktestEngine& engine, std::string text, RunFailureCode code,
280 const RunFailureArgs& args = {});
281// The same for a literal text (preferred for one): taken by address.
282void note_run_failure(BacktestEngine& engine, const char* text, RunFailureCode code,
283 const RunFailureArgs& args = {});
284void note_run_failure(BacktestEngine& engine, std::string text, const RunFailureValue& value);
285void note_run_failure(BacktestEngine& engine, const char* entrypoint,
286 const std::exception& error);
287void note_run_failure_unknown(BacktestEngine& engine, const char* entrypoint);
288void clear_run_failure(BacktestEngine& engine) noexcept;
289
290/// The getters' answers (strategy_get_last_error_code / _args): the stored code
291/// while its text is the engine's last error; engine_unclassified_error for a
292/// non-empty error no coded site wrote; "" when there is no failure.
293const char* run_failure_code_of(const BacktestEngine& engine) noexcept;
294const char* run_failure_args_of(const BacktestEngine& engine) noexcept;
295/// The recorded value under the same rule (none when there is no failure).
297
298// ---------------------------------------------------------------------------
299// The generated-code helpers (the codegen contract above).
300
301[[noreturn]] inline void pine_no_data_stop(const char* function, const char* call,
302 std::int32_t line, const char* english) {
305 {{"function", function}, {"call", call}, {"line", line}}, english ? english : "");
306}
307
308[[noreturn]] inline void pine_other_symbol_stop(const char* function,
309 const char* symbol_literal_or_null,
310 const char* call, std::int32_t line,
311 const char* english) {
312 RunFailureArgs args{{"function", function}, {"call", call}, {"line", line}};
313 if (symbol_literal_or_null) args.emplace_back("symbol", symbol_literal_or_null);
315 english ? english : "");
316}
317
318[[noreturn]] inline void pine_array_stop(const char* reason, const char* method_or_null,
319 const char* english) {
320 RunFailureArgs args{{"reason", reason}};
321 if (method_or_null) args.emplace_back("method", method_or_null);
323 english ? english : "");
324}
325
326[[noreturn]] inline void pine_collection_stop(const char* collection, const char* reason,
327 const char* english) {
328 const char* text = english ? english : "";
329 // Only the contract's two collections: pine_na_reference alone would also
330 // take the other objects of its list.
331 const bool listed = collection && (std::strcmp(collection, "array") == 0
332 || std::strcmp(collection, "matrix") == 0);
333 if (listed && reason && std::strcmp(reason, "na_reference") == 0) {
335 {{"object", collection}}, text);
336 }
337 if (listed && reason && std::strcmp(reason, "historical_modified") == 0) {
340 {{"reason", "historical_modified"}, {"collection", collection}}, text);
341 }
343}
344
345[[noreturn]] inline void pine_na_stop(const char* object, const char* english) {
347 english ? english : "");
348}
349
350[[noreturn]] inline void pine_limit_stop(const char* limit, std::int64_t max,
351 const char* english) {
352 RunFailureArgs args{{"limit", limit}};
353 if (max >= 0) args.emplace_back("max", max);
355 english ? english : "");
356}
357
358[[noreturn]] inline void pine_unsupported_stop(const char* reason, std::int32_t line,
359 const char* english) {
360 RunFailureArgs args{{"reason", reason}};
361 if (line >= 0) args.emplace_back("line", line);
363 english ? english : "");
364}
365
366[[noreturn]] inline void pine_string_stop(const char* reason, const char* english) {
368 english ? english : "");
369}
370
371[[noreturn]] inline void pine_engine_invariant(const char* english) {
373 english ? english : "");
374}
375
376} // namespace pineforge
RunFailureCode run_failure_code() const noexcept
RunFailureInfo(const RunFailureInfo &) noexcept=default
RunFailureInfo(RunFailureValue value) noexcept
const RunFailureValue & run_failure() const noexcept
RunFailureInfo(RunFailureCode code, const RunFailureArgs &args) noexcept
RunFailureInfo & operator=(const RunFailureInfo &) noexcept=default
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
coded(RunFailureValue value, const char *what)
coded(RunFailureCode code, const RunFailureArgs &args, const char *what)
coded(RunFailureCode code, const RunFailureArgs &args, const std::string &what)
coded(RunFailureValue value, const std::string &what)
coded(RunFailureCode code, const RunFailureArgs &args, const Base &error)
bool run_failure_code_retryable(RunFailureCode code) noexcept
const char * run_failure_args_of(const BacktestEngine &engine) noexcept
void pine_unsupported_stop(const char *reason, std::int32_t line, const char *english)
void pine_string_stop(const char *reason, const char *english)
void note_run_failure(BacktestEngine &engine, std::string text, RunFailureCode code, const RunFailureArgs &args={})
void clear_run_failure(BacktestEngine &engine) noexcept
RunFailureValue make_run_failure(RunFailureCode code, const RunFailureArgs &args) noexcept
Validate args against code's registry entry and canonicalize them.
void pine_limit_stop(const char *limit, std::int64_t max, const char *english)
RunFailureClass run_failure_code_class(RunFailureCode code) noexcept
The code's catalog class and retryable flag.
RunFailureValue run_failure_value_of(const BacktestEngine &engine) noexcept
The recorded value under the same rule (none when there is no failure).
RunFailureValue classify_run_failure(const std::exception &error) noexcept
The code an exception carries: a coded exception's own; else the safety net by type (std::bad_alloc -...
RunFailureClass
The class a code belongs to (the catalog's class).
const char * run_failure_code_of(const BacktestEngine &engine) noexcept
The getters' answers (strategy_get_last_error_code / _args): the stored code while its text is the en...
void pine_array_stop(const char *reason, const char *method_or_null, const char *english)
RunFailureArgKind
An argument's kind (the catalog's kind).
void pine_other_symbol_stop(const char *function, const char *symbol_literal_or_null, const char *call, std::int32_t line, const char *english)
std::vector< RunFailureArg > RunFailureArgs
const char * run_failure_code_name(RunFailureCode code) noexcept
The code's catalog name ("" for none or an unknown value).
void pine_na_stop(const char *object, const char *english)
void pine_engine_invariant(const char *english)
void note_run_failure_unknown(BacktestEngine &engine, const char *entrypoint)
void pine_collection_stop(const char *collection, const char *reason, const char *english)
void pine_no_data_stop(const char *function, const char *call, std::int32_t line, const char *english)
RunFailureArg(const char *arg_name, const char *value)
RunFailureArg(const char *arg_name, std::string value)
RunFailureArg(const char *arg_name, Integer value)
RunFailureArg(const char *arg_name, std::nullptr_t)
RunFailureArg(const char *arg_name, Floating value)
RunFailureArg(const char *arg_name, Boolean value)
A validated code and its canonical arguments: the JSON object with its keys sorted and no whitespace,...
std::shared_ptr< const std::string > args