PineForge v1.4.0-4-g873b25da
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 docker/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// * a vocabulary argument outside its closed list, an undeclared argument, a
26// wrong kind or a missing required argument turns the failure into
27// engine_invariant (the English is kept): a wrong call cannot mint a code
28// or an open string.
29//
30// Thrown types do not change: coded<Base> derives from Base, so every typed
31// catch and every what() is what it was. The classifier
32// (classify_run_failure) reads the carried code; an exception without one is
33// classified by its type as a safety net only -- std::bad_alloc is
34// out_of_memory, the std::logic_error family engine_invariant, anything else
35// engine_unclassified_error.
36//
37// ---------------------------------------------------------------------------
38// The codegen contract (PINEFORGE_HAS_RUN_FAILURE_CODES_V1). Generated code
39// may call exactly these, under `#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1`
40// (and keep `pine_runtime_error(std::string(english))` in the `#else` arm).
41// Every helper is [[noreturn]], throws coded<std::runtime_error>, hardwires its
42// code and never takes one; `english` is the text the stop has today, and is
43// what strategy_get_last_error() reads, byte for byte. Pointer arguments are
44// codegen literals (`const char*`, never NULL except where marked); `line` is
45// the call's line in the source the transpiler read.
46//
47// pine_runtime_error(const std::string& message) (log.hpp)
48// -> strategy_runtime_error {}
49// pine_no_data_stop(const char* function, const char* call,
50// std::int32_t line, const char* english)
51// -> no_data_request {function, call, line}
52// function: request.financial | request.earnings | request.dividends |
53// request.splits | request.footprint | request.economic |
54// request.quandl | request.seed | request.currency_rate
55// pine_other_symbol_stop(const char* function,
56// const char* symbol_literal_or_null,
57// const char* call, std::int32_t line,
58// const char* english)
59// -> other_symbol_request {function, symbol?, call, line}
60// function: request.security | request.security_lower_tf; symbol only
61// when the call spells it as a literal (NULL otherwise)
62// pine_array_stop(const char* reason, const char* method_or_null,
63// const char* english)
64// -> pine_array_error {reason, method?}
65// reason: index_out_of_bounds | slice_range_inverted |
66// empty_array_access | size_invalid
67// method: new | get | set | insert | remove | percentrank | fill |
68// slice | first | last | pop | shift
69// pine_collection_stop(const char* collection, const char* reason,
70// const char* english)
71// collection: array | matrix (any other collection: engine_invariant)
72// reason historical_modified -> pine_array_error {reason, collection}
73// reason na_reference -> pine_na_reference {object: collection}
74// pine_na_stop(const char* object, const char* english)
75// -> pine_na_reference {object}
76// object: array | matrix | map | line | box | label | linefill |
77// polyline | table | udt_object
78// pine_limit_stop(const char* limit, std::int64_t max, const char* english)
79// -> pine_runtime_limit {limit, max}
80// limit: map_pairs | max_bars_back | matrix_elements | udt_objects;
81// max is the limit's engine or transpiler constant (pass -1 for none)
82// pine_unsupported_stop(const char* reason, std::int32_t line,
83// const char* english)
84// -> request_unsupported {reason, line}
85// reason: lower_tf_lookahead_or_gaps | nested_heikinashi_request
86// (line -1: none)
87// pine_string_stop(const char* reason, const char* english)
88// -> pine_string_error {reason}
89// reason: substring_out_of_range | format_index_overflow
90// pine_engine_invariant(const char* english)
91// -> engine_invariant {}
92//
93// note_run_failure(BacktestEngine&, const char* entrypoint,
94// const std::exception&)
95// the generated wrapper's catch: writes "<entrypoint>: <what()>" (the
96// text it writes today) with the exception's code;
97// note_run_failure_unknown(BacktestEngine&, const char* entrypoint)
98// its catch (...): "<entrypoint>: unknown C++ exception",
99// engine_unclassified_error;
100// note_run_failure(BacktestEngine&, std::string text, RunFailureCode,
101// const RunFailureArgs&)
102// a latched setter refusal: setting_rejected with its entrypoint and
103// reason;
104// note_run_failure(BacktestEngine&, std::string text,
105// const RunFailureValue&)
106// the same refusal with the value its RunFailureInfo holds.
107//
108// The latched setter failure also uses these names: RunFailureInfo as a
109// public base of its own exception type (beside
110// checked_settings::LatchedSettingsFailure) and as a copy-assignable member,
111// built with the (RunFailureCode, const RunFailureArgs&) constructor and read
112// with run_failure(); RunFailureValue; and the codes RunFailureCode::none,
113// ::setting_rejected and ::out_of_memory. An exception thrown where the script
114// is prepared that already carries a code keeps it; a latched failure without
115// one reads setting_rejected.
116// ---------------------------------------------------------------------------
117
119
120#include <cstdint>
121#include <cstring>
122#include <exception>
123#include <memory>
124#include <stdexcept>
125#include <string>
126#include <type_traits>
127#include <utility>
128#include <vector>
129
130#define PINEFORGE_HAS_RUN_FAILURE_CODES_V1 1
131
132namespace pineforge {
133
134inline namespace engine_script_run_v19 {
135class BacktestEngine;
136}
137
138/// The class a code belongs to (the catalog's `class`).
150
151/// An argument's kind (the catalog's `kind`).
162
163/// One typed argument as a raise site spells it. `name` is a literal; a text
164/// value is copied.
166 enum class Type : std::uint8_t { text, integer, number };
167 const char* name = "";
169 std::string text;
170 std::int64_t integer = 0;
171 double number = 0.0;
172
173 // Out of line (src/run_failure.cpp): a literal value is handed over by
174 // address, so it stays one contiguous string wherever it is compiled.
175 RunFailureArg(const char* arg_name, const char* value);
176 RunFailureArg(const char* arg_name, std::string value)
177 : name(arg_name), text(std::move(value)) {}
178 template <typename Integer,
179 typename std::enable_if<std::is_integral<Integer>::value
180 && !std::is_same<Integer, bool>::value, int>::type = 0>
181 RunFailureArg(const char* arg_name, Integer value)
182 : name(arg_name), type(Type::integer), integer(static_cast<std::int64_t>(value)) {}
183 template <typename Floating,
184 typename std::enable_if<std::is_floating_point<Floating>::value, int>::type = 0>
185 RunFailureArg(const char* arg_name, Floating value)
186 : name(arg_name), type(Type::number), number(static_cast<double>(value)) {}
187};
188
189using RunFailureArgs = std::vector<RunFailureArg>;
190
191/// A validated code and its canonical arguments: the JSON object with its keys
192/// sorted and no whitespace, or null for a code without arguments ("{}").
195 std::shared_ptr<const std::string> args;
196};
197
198/// Validate `args` against `code`'s registry entry and canonicalize them. Any
199/// violation yields engine_invariant without arguments. Allocation failure
200/// yields out_of_memory.
202
203/// The code's catalog name ("" for none or an unknown value).
204const char* run_failure_code_name(RunFailureCode code) noexcept;
205/// The code's catalog class and retryable flag.
208
209/// The code an exception carries: a coded exception's own; else the safety
210/// net by type (std::bad_alloc -> out_of_memory, std::logic_error ->
211/// engine_invariant, other -> engine_unclassified_error).
212RunFailureValue classify_run_failure(const std::exception& error) noexcept;
213
214/// The code carried beside a thrown exception. A mixin: it adds no base of
215/// std::exception, so coded<Base> is still exactly a Base to every catch.
217public:
218 RunFailureInfo(RunFailureCode code, const RunFailureArgs& args) noexcept
219 : value_(make_run_failure(code, args)) {}
220 // An already validated value (a code another site recorded, carried on).
221 explicit RunFailureInfo(RunFailureValue value) noexcept : value_(std::move(value)) {}
222 RunFailureInfo(const RunFailureInfo&) noexcept = default;
223 RunFailureInfo& operator=(const RunFailureInfo&) noexcept = default;
225
226 RunFailureCode run_failure_code() const noexcept { return value_.code; }
227 const RunFailureValue& run_failure() const noexcept { return value_; }
228
229private:
230 RunFailureValue value_;
231};
232
233/// `Base` with a code: throw coded<std::invalid_argument>(code, {args}, text).
234template <class Base>
235class coded : public Base, public RunFailureInfo {
236public:
237 coded(RunFailureCode code, const RunFailureArgs& args, const std::string& what)
238 : Base(what), RunFailureInfo(code, args) {}
239 coded(RunFailureCode code, const RunFailureArgs& args, const char* what)
240 : Base(what), RunFailureInfo(code, args) {}
241 coded(RunFailureValue value, const std::string& what)
242 : Base(what), RunFailureInfo(std::move(value)) {}
243 coded(RunFailureValue value, const char* what)
244 : Base(what), RunFailureInfo(std::move(value)) {}
245};
246
247// The record beside BacktestEngine::last_error(): writes the text and the
248// code together, so they never disagree. Defined with the execution consumer
249// that stores the record (src/run_failure.cpp).
250void note_run_failure(BacktestEngine& engine, std::string text, RunFailureCode code,
251 const RunFailureArgs& args = {});
252// The same for a literal text (preferred for one): taken by address.
253void note_run_failure(BacktestEngine& engine, const char* text, RunFailureCode code,
254 const RunFailureArgs& args = {});
255void note_run_failure(BacktestEngine& engine, std::string text, const RunFailureValue& value);
256void note_run_failure(BacktestEngine& engine, const char* entrypoint,
257 const std::exception& error);
258void note_run_failure_unknown(BacktestEngine& engine, const char* entrypoint);
259void clear_run_failure(BacktestEngine& engine) noexcept;
260
261/// The getters' answers (strategy_get_last_error_code / _args): the stored code
262/// while its text is the engine's last error; engine_unclassified_error for a
263/// non-empty error no coded site wrote; "" when there is no failure.
264const char* run_failure_code_of(const BacktestEngine& engine) noexcept;
265const char* run_failure_args_of(const BacktestEngine& engine) noexcept;
266/// The recorded value under the same rule (none when there is no failure).
268
269// ---------------------------------------------------------------------------
270// The generated-code helpers (the codegen contract above).
271
272[[noreturn]] inline void pine_no_data_stop(const char* function, const char* call,
273 std::int32_t line, const char* english) {
276 {{"function", function}, {"call", call}, {"line", line}}, english ? english : "");
277}
278
279[[noreturn]] inline void pine_other_symbol_stop(const char* function,
280 const char* symbol_literal_or_null,
281 const char* call, std::int32_t line,
282 const char* english) {
283 RunFailureArgs args{{"function", function}, {"call", call}, {"line", line}};
284 if (symbol_literal_or_null) args.emplace_back("symbol", symbol_literal_or_null);
286 english ? english : "");
287}
288
289[[noreturn]] inline void pine_array_stop(const char* reason, const char* method_or_null,
290 const char* english) {
291 RunFailureArgs args{{"reason", reason}};
292 if (method_or_null) args.emplace_back("method", method_or_null);
294 english ? english : "");
295}
296
297[[noreturn]] inline void pine_collection_stop(const char* collection, const char* reason,
298 const char* english) {
299 const char* text = english ? english : "";
300 // Only the contract's two collections: pine_na_reference alone would also
301 // take the other objects of its list.
302 const bool listed = collection && (std::strcmp(collection, "array") == 0
303 || std::strcmp(collection, "matrix") == 0);
304 if (listed && reason && std::strcmp(reason, "na_reference") == 0) {
306 {{"object", collection}}, text);
307 }
308 if (listed && reason && std::strcmp(reason, "historical_modified") == 0) {
311 {{"reason", "historical_modified"}, {"collection", collection}}, text);
312 }
314}
315
316[[noreturn]] inline void pine_na_stop(const char* object, const char* english) {
318 english ? english : "");
319}
320
321[[noreturn]] inline void pine_limit_stop(const char* limit, std::int64_t max,
322 const char* english) {
323 RunFailureArgs args{{"limit", limit}};
324 if (max >= 0) args.emplace_back("max", max);
326 english ? english : "");
327}
328
329[[noreturn]] inline void pine_unsupported_stop(const char* reason, std::int32_t line,
330 const char* english) {
331 RunFailureArgs args{{"reason", reason}};
332 if (line >= 0) args.emplace_back("line", line);
334 english ? english : "");
335}
336
337[[noreturn]] inline void pine_string_stop(const char* reason, const char* english) {
339 english ? english : "");
340}
341
342[[noreturn]] inline void pine_engine_invariant(const char* english) {
344 english ? english : "");
345}
346
347} // 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)
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, Floating 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