PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
checked_settings.hpp
Go to the documentation of this file.
1#pragma once
2
4#include <algorithm>
5#include <charconv>
6#include <cmath>
7#include <cstring>
8#include <cstdint>
9#include <exception>
10#include <iomanip>
11#include <limits>
12#include <locale>
13#include <sstream>
14#include <stdexcept>
15#include <string>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
21
22/** A thrown legacy setter's original message, propagated to native begin. */
23class LatchedSettingsFailure : public std::runtime_error {
24public:
25 using std::runtime_error::runtime_error;
26};
27
28struct Error {
29 int status;
30 const char* message;
31};
32
33inline void require(bool condition, const char* message,
34 int status = PF_SETTINGS_INVALID_ARGUMENT) {
35 if (!condition) throw Error{status, message};
36}
37
38inline void copy_error(char* error, std::size_t capacity, const char* message) noexcept {
39 if (!error || capacity == 0) return;
40 const auto count = std::min(capacity - 1, std::strlen(message));
41 std::memcpy(error, message, count);
42 error[count] = '\0';
43}
44
45template <typename Function>
46int boundary(char* error, std::size_t capacity, Function&& function) noexcept {
47 copy_error(error, capacity, "");
48 try {
49 std::forward<Function>(function)();
50 return PF_SETTINGS_OK;
51 } catch (const Error& failure) {
52 copy_error(error, capacity, failure.message);
53 return failure.status;
54 } catch (const std::exception& failure) {
55 copy_error(error, capacity, failure.what());
56 } catch (...) {
57 copy_error(error, capacity, "unknown C++ exception");
58 }
60}
61
62inline std::string number(double value) {
63 if (!std::isfinite(value)) return "na";
64 std::ostringstream stream;
65 stream.imbue(std::locale::classic());
66 stream << std::setprecision(std::numeric_limits<double>::max_digits10) << value;
67 return stream.str();
68}
69
70template <typename Integer, std::enable_if_t<std::is_integral_v<Integer>
71 && !std::is_same_v<Integer, bool>, int> = 0>
72inline std::string number(Integer value) { return std::to_string(value); }
73inline std::string number(bool value) { return value ? "true" : "false"; }
74
75inline std::int64_t integer(const std::string& value, int bits = 32) {
76 require(!value.empty(), "expected an integer");
77 if (value.find_first_of(".eE") != std::string::npos) {
78 std::size_t offset = 0;
79 const bool negative = value[offset] == '-';
80 if (value[offset] == '+' || negative) ++offset;
81 std::string digits;
82 while (offset < value.size() && value[offset] >= '0' && value[offset] <= '9')
83 digits += value[offset++];
84 std::size_t fractional_digits = 0;
85 if (offset < value.size() && value[offset] == '.') {
86 ++offset;
87 const auto fractional_begin = digits.size();
88 while (offset < value.size() && value[offset] >= '0' && value[offset] <= '9')
89 digits += value[offset++];
90 fractional_digits = digits.size() - fractional_begin;
91 }
92 require(!digits.empty(), "expected an integer");
93 std::int64_t exponent = 0;
94 if (offset < value.size() && (value[offset] == 'e' || value[offset] == 'E')) {
95 ++offset;
96 const auto exponent_text = value.substr(offset);
97 require(exponent_text.find_first_of(".eE") == std::string::npos, "invalid integer exponent");
98 try {
99 exponent = integer(exponent_text);
100 } catch (const Error&) {
101 throw Error{PF_SETTINGS_INVALID_ARGUMENT, "invalid integer exponent"};
102 }
103 offset = value.size();
104 }
105 require(offset == value.size(), "invalid integer or trailing bytes");
106 const auto nonzero = digits.find_first_not_of('0');
107 if (nonzero == std::string::npos) return 0;
108 digits.erase(0, nonzero);
109 const auto shift = exponent - static_cast<std::int64_t>(fractional_digits);
110 if (shift < 0) {
111 require(-shift <= static_cast<std::int64_t>(digits.size()), "expected an integral value");
112 const auto integral_end = digits.size() - static_cast<std::size_t>(-shift);
113 require(digits.find_first_not_of('0', integral_end) == std::string::npos,
114 "expected an integral value");
115 digits.resize(integral_end);
116 } else {
117 require(shift <= 19 && digits.size() + static_cast<std::size_t>(shift) <= 19,
118 "integer out of range");
119 digits.append(static_cast<std::size_t>(shift), '0');
120 }
121 return integer((negative ? "-" : "") + digits, bits);
122 }
123 const char* begin = value.data();
124 const char* end = begin + value.size();
125 if (*begin == '+') {
126 ++begin;
127 require(begin != end && *begin != '-' && *begin != '+', "invalid integer sign");
128 }
129 require(begin != end && (*begin != '-' || begin + 1 != end), "expected an integer");
130 std::int64_t parsed = 0;
131 const auto result = std::from_chars(begin, end, parsed);
132 require(result.ec != std::errc::result_out_of_range, "integer out of range");
133 require(result.ec == std::errc{} && result.ptr == end, "invalid integer or trailing bytes");
134 require(bits == 64 || (parsed >= std::numeric_limits<int>::min()
135 && parsed <= std::numeric_limits<int>::max()), "integer out of range");
136 return parsed;
137}
138
139inline double real(const std::string& value) {
140 require(!value.empty(), "expected a finite decimal number");
141 std::size_t offset = 0;
142 if (value[offset] == '+' || value[offset] == '-') ++offset;
143 bool digits = false;
144 while (offset < value.size() && value[offset] >= '0' && value[offset] <= '9') {
145 digits = true;
146 ++offset;
147 }
148 if (offset < value.size() && value[offset] == '.') {
149 ++offset;
150 while (offset < value.size() && value[offset] >= '0' && value[offset] <= '9') {
151 digits = true;
152 ++offset;
153 }
154 }
155 require(digits, "expected a finite decimal number");
156 const auto mantissa_end = offset;
157 if (offset < value.size() && (value[offset] == 'e' || value[offset] == 'E')) {
158 ++offset;
159 if (offset < value.size() && (value[offset] == '+' || value[offset] == '-')) ++offset;
160 const auto exponent_begin = offset;
161 while (offset < value.size() && value[offset] >= '0' && value[offset] <= '9') ++offset;
162 require(offset != exponent_begin, "invalid numeric exponent");
163 }
164 require(offset == value.size(), "invalid number or trailing bytes");
165 std::istringstream stream(value);
166 stream.imbue(std::locale::classic());
167 double parsed = 0;
168 stream >> std::noskipws >> parsed;
169 require(!stream.fail() && stream.eof() && std::isfinite(parsed), "number out of finite range");
170 if (parsed == 0) {
171 require(value.substr(0, mantissa_end).find_first_of("123456789") == std::string::npos,
172 "number underflows to zero");
173 }
174 const auto canonical = number(parsed);
175 try {
176 std::size_t consumed = 0;
177 const double roundtrip = std::stod(canonical, &consumed);
178 require(consumed == canonical.size() && roundtrip == parsed,
179 "number cannot be consumed by the strategy getter");
180 } catch (const std::invalid_argument&) {
181 throw Error{PF_SETTINGS_INVALID_ARGUMENT, "number cannot be consumed by the strategy getter"};
182 } catch (const std::out_of_range&) {
183 throw Error{PF_SETTINGS_INVALID_ARGUMENT, "number cannot be consumed by the strategy getter"};
184 }
185 return parsed;
186}
187
188struct Setting {
189 std::string name;
190 std::string type;
191 std::string default_value;
192 std::vector<std::string> options{};
193 double minimum = std::numeric_limits<double>::quiet_NaN();
194 double maximum = std::numeric_limits<double>::quiet_NaN();
195 double step = std::numeric_limits<double>::quiet_NaN();
196 int integer_bits = 32;
197 bool supported = true;
198 std::vector<std::string> option_values{};
199 std::string kind{};
200};
201
202inline std::string validate(const Setting& setting, const std::string& value) {
203 require(setting.supported, "input cannot be honoured by this compiled strategy", PF_SETTINGS_UNSUPPORTED);
204 std::string canonical = value;
205 double numeric = 0;
206 bool is_numeric = false;
207 if (setting.type == "bool") {
208 require(value == "true" || value == "false" || value == "1" || value == "0", "invalid boolean");
209 canonical = (value == "true" || value == "1") ? "true" : "false";
210 } else if (setting.type == "int") {
211 const auto parsed = integer(value, setting.integer_bits);
212 canonical = number(parsed);
213 numeric = static_cast<double>(parsed);
214 is_numeric = true;
215 } else if (setting.type == "float") {
216 numeric = real(value);
217 canonical = number(numeric);
218 is_numeric = true;
219 } else if (setting.type == "enum") {
220 const auto found = std::find(setting.options.begin(), setting.options.end(), value);
221 std::int64_t index = 0;
222 try {
223 index = found == setting.options.end() ? integer(value)
224 : setting.option_values.empty() ? std::distance(setting.options.begin(), found)
225 : integer(setting.option_values[static_cast<std::size_t>(std::distance(setting.options.begin(), found))]);
226 } catch (const Error&) {
227 throw Error{PF_SETTINGS_INVALID_ARGUMENT, "invalid enum option"};
228 }
229 require(setting.option_values.empty()
230 ? index >= 0 && static_cast<std::size_t>(index) < setting.options.size()
231 : std::find(setting.option_values.begin(), setting.option_values.end(), number(index)) != setting.option_values.end(), "invalid enum option");
232 return number(static_cast<std::int64_t>(index));
233 }
234 if (is_numeric) {
235 require(!std::isfinite(setting.minimum) || numeric >= setting.minimum, "value below minimum");
236 require(!std::isfinite(setting.maximum) || numeric <= setting.maximum, "value above maximum");
237 }
238 if (!setting.options.empty()) {
239 require(std::find(setting.options.begin(), setting.options.end(), canonical) != setting.options.end(), "invalid input option");
240 }
241 return canonical;
242}
243
244inline std::string quote(const std::string& value) {
245 std::string result = "\"";
246 const char* hex = "0123456789abcdef";
247 for (const unsigned char byte : value) {
248 if (byte == '"' || byte == '\\') {
249 result += '\\';
250 result += static_cast<char>(byte);
251 } else if (byte < 0x20) {
252 result += "\\u00";
253 result += hex[byte >> 4];
254 result += hex[byte & 15];
255 } else {
256 result += static_cast<char>(byte);
257 }
258 }
259 return result + '"';
260}
261
262inline std::string describe(const Setting& setting, const std::string& effective) {
263 std::string result = "{\"name\":" + quote(setting.name)
264 + ",\"type\":" + quote(setting.type)
265 + ",\"kind\":" + quote(setting.kind.empty() ? setting.type : setting.kind)
266 + ",\"default\":" + quote(setting.default_value)
267 + ",\"effective_value\":" + quote(effective)
268 + ",\"supported\":" + (setting.supported ? "true" : "false") + ",\"options\":[";
269 for (std::size_t index = 0; index < setting.options.size(); ++index) {
270 if (index) result += ',';
271 result += quote(setting.options[index]);
272 }
273 result += ']';
274 result += ",\"option_values\":[";
275 for (std::size_t index = 0; index < setting.option_values.size(); ++index) {
276 if (index) result += ',';
277 result += quote(setting.option_values[index]);
278 }
279 result += ']';
280 for (const auto& constraint : std::vector<std::pair<const char*, double>>{
281 {"min", setting.minimum}, {"max", setting.maximum}, {"step", setting.step}}) {
282 result += ',';
283 result += quote(constraint.first) + ':';
284 result += std::isfinite(constraint.second) ? number(constraint.second) : "null";
285 }
286 return result + '}';
287}
288
289inline void receipt(const std::string& document, char* json, std::size_t capacity,
290 std::size_t* required) {
291 require(required != nullptr, "required byte count pointer is null");
292 *required = document.size() + 1;
293 if (!json || capacity < *required) {
294 if (json && capacity) json[0] = '\0';
295 throw Error{PF_SETTINGS_BUFFER_TOO_SMALL, "receipt buffer too small"};
296 }
297 std::memcpy(json, document.c_str(), *required);
298}
299
300}
A thrown legacy setter's original message, propagated to native begin.
@ PF_SETTINGS_UNSUPPORTED
Definition pineforge.h:1779
@ PF_SETTINGS_EXCEPTION
Definition pineforge.h:1780
@ PF_SETTINGS_BUFFER_TOO_SMALL
Definition pineforge.h:1781
@ PF_SETTINGS_INVALID_ARGUMENT
Definition pineforge.h:1778
@ PF_SETTINGS_OK
Definition pineforge.h:1777
void copy_error(char *error, std::size_t capacity, const char *message) noexcept
std::string validate(const Setting &setting, const std::string &value)
int boundary(char *error, std::size_t capacity, Function &&function) noexcept
std::string describe(const Setting &setting, const std::string &effective)
void receipt(const std::string &document, char *json, std::size_t capacity, std::size_t *required)
std::string number(double value)
void require(bool condition, const char *message, int status=PF_SETTINGS_INVALID_ARGUMENT)
double real(const std::string &value)
std::string quote(const std::string &value)
std::vector< std::string > option_values