@pineforge/codegen-pyodide 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
"""Opt-in settings metadata, validation and exception-contained C exports."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
from ..ast_nodes import Identifier, MemberAccess
|
|
4
6
|
from ..errors import CompileError
|
|
7
|
+
from .tables import ALERT_FREQ_VALUES, NAME_ECHO_STRING_MEMBERS, ORDER_DIRECTION_MAP
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_QUOTED_STRING = r'"(?:[^"\\\x00\r\n]|\\(?:["\\nrt]|000))*"'
|
|
11
|
+
_STRING_LITERAL = re.compile(rf'(?:std::string\({_QUOTED_STRING}\)|{_QUOTED_STRING})')
|
|
12
|
+
_STRING_ESCAPE = re.compile(r'\\(?:["\\nrt]|000)')
|
|
13
|
+
_STRING_ESCAPE_VALUES = {
|
|
14
|
+
r'\"': '"', r'\\': '\\', r'\n': '\n', r'\r': '\r', r'\t': '\t', r'\000': '\0',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _string_setting_arg(expr, lowered: str) -> str | None:
|
|
19
|
+
"""Use the body's known string constants, never its inert visual codes.
|
|
20
|
+
|
|
21
|
+
Name-echo strings are accepted only for inventoried currency/format members.
|
|
22
|
+
Unknown constants leave the checked input unsupported; legacy getters
|
|
23
|
+
and setters retain their existing execution semantics.
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(expr, MemberAccess):
|
|
26
|
+
if not isinstance(expr.object, Identifier):
|
|
27
|
+
return None
|
|
28
|
+
namespace = expr.object.name
|
|
29
|
+
known = (
|
|
30
|
+
namespace == "alert" and expr.member in ALERT_FREQ_VALUES
|
|
31
|
+
or namespace == "order" and expr.member in ORDER_DIRECTION_MAP
|
|
32
|
+
or namespace == "session" and expr.member in ("regular", "extended")
|
|
33
|
+
or expr.member in NAME_ECHO_STRING_MEMBERS.get(namespace, ())
|
|
34
|
+
)
|
|
35
|
+
if not known:
|
|
36
|
+
return None
|
|
37
|
+
return lowered if _STRING_LITERAL.fullmatch(lowered) else None
|
|
5
38
|
|
|
6
39
|
|
|
7
40
|
def _visit_setting_arg(emitter, expr) -> str:
|
|
@@ -16,22 +49,97 @@ def _visit_setting_arg(emitter, expr) -> str:
|
|
|
16
49
|
return "0"
|
|
17
50
|
|
|
18
51
|
|
|
52
|
+
def _string_setting_value(lowered: str) -> str:
|
|
53
|
+
"""Decode the emitter's literals as native std::string(const char*) values.
|
|
54
|
+
|
|
55
|
+
Only the serializer's escape grammar is accepted; Unicode stays literal.
|
|
56
|
+
The receipt's string constructors stop at the first NUL, including one
|
|
57
|
+
followed by digits, while an escaped backslash remains ordinary data.
|
|
58
|
+
"""
|
|
59
|
+
if not _STRING_LITERAL.fullmatch(lowered):
|
|
60
|
+
raise ValueError("setting value is not an emitted C++ string literal")
|
|
61
|
+
literal = lowered[len("std::string("):-1] if lowered.startswith("std::string(") else lowered
|
|
62
|
+
decoded = _STRING_ESCAPE.sub(lambda match: _STRING_ESCAPE_VALUES[match.group()], literal[1:-1])
|
|
63
|
+
return decoded.partition("\0")[0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_NUMERIC_LITERAL = re.compile(r"[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?(?:LL|ULL|L|UL|[fF])?")
|
|
67
|
+
_NUMERIC_SUFFIX = re.compile(r"(?:LL|ULL|L|UL|[fF])$")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _numeric_setting_value(lowered: str) -> int | float | bool | None:
|
|
71
|
+
"""The number or bool a lowered C++ setting expression is, or None.
|
|
72
|
+
|
|
73
|
+
Only a plain literal, signed or in parentheses, is read: what the codegen
|
|
74
|
+
emits for a literal or for a named constant it inlines. Anything the C++
|
|
75
|
+
computes at run time (an arithmetic expression, a call, a series) stays
|
|
76
|
+
with the receipt, which evaluates it.
|
|
77
|
+
"""
|
|
78
|
+
text = lowered.strip()
|
|
79
|
+
while text.startswith("(") and text.endswith(")"):
|
|
80
|
+
text = text[1:-1].strip()
|
|
81
|
+
if text in ("true", "false"):
|
|
82
|
+
return text == "true"
|
|
83
|
+
if not _NUMERIC_LITERAL.fullmatch(text):
|
|
84
|
+
return None
|
|
85
|
+
text = _NUMERIC_SUFFIX.sub("", text)
|
|
86
|
+
return float(text) if re.search(r"[.eE]", text) else int(text)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_number(value) -> bool:
|
|
90
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
91
|
+
|
|
92
|
+
|
|
19
93
|
def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
|
|
20
94
|
lines.extend([
|
|
21
95
|
" bool _pf_setting_failed_ = false;",
|
|
22
96
|
" std::string _pf_setting_failure_;",
|
|
97
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
98
|
+
" ::pineforge::RunFailureInfo _pf_setting_failure_info_{::pineforge::RunFailureCode::none, {}};",
|
|
99
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
100
|
+
" using _pf_setting_error_base = ::pineforge::checked_settings::LatchedSettingsFailure;",
|
|
101
|
+
"#else",
|
|
102
|
+
" using _pf_setting_error_base = std::runtime_error;",
|
|
103
|
+
"#endif",
|
|
104
|
+
" struct _pf_latched_setting_error : _pf_setting_error_base, ::pineforge::RunFailureInfo {",
|
|
105
|
+
" _pf_latched_setting_error(const std::string& text, const ::pineforge::RunFailureInfo& info)",
|
|
106
|
+
" : _pf_setting_error_base(text), ::pineforge::RunFailureInfo(info) {}",
|
|
107
|
+
" };",
|
|
108
|
+
"#endif",
|
|
23
109
|
" void _pf_record_failure(const char* entrypoint, const char* message) noexcept {",
|
|
110
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
111
|
+
" try { ::pineforge::note_run_failure_unknown(*this, entrypoint); } catch (...) {}",
|
|
112
|
+
"#else",
|
|
24
113
|
" try { last_error_ = entrypoint; last_error_ += \": \"; last_error_ += message; } catch (...) {}",
|
|
114
|
+
"#endif",
|
|
115
|
+
" }",
|
|
116
|
+
" void _pf_record_failure(const char* entrypoint, const std::exception& error) noexcept {",
|
|
117
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
118
|
+
" try { ::pineforge::note_run_failure(*this, entrypoint, error); } catch (...) {}",
|
|
119
|
+
"#else",
|
|
120
|
+
" _pf_record_failure(entrypoint, error.what());",
|
|
121
|
+
"#endif",
|
|
25
122
|
" }",
|
|
123
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
124
|
+
" template <typename FailureInfo>",
|
|
125
|
+
" void _pf_record_setting_failure(const char* entrypoint, const char* message, FailureInfo failure_info) noexcept {",
|
|
126
|
+
"#else",
|
|
26
127
|
" void _pf_record_setting_failure(const char* entrypoint, const char* message) noexcept {",
|
|
128
|
+
"#endif",
|
|
27
129
|
" if (!_pf_setting_failed_) {",
|
|
28
130
|
" _pf_setting_failed_ = true;",
|
|
29
131
|
' try { _pf_setting_failure_ = entrypoint; _pf_setting_failure_ += ": "; _pf_setting_failure_ += message; } catch (...) {}',
|
|
132
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
133
|
+
" try { _pf_setting_failure_info_ = failure_info(); }",
|
|
134
|
+
" catch (...) { _pf_setting_failure_info_ = ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::out_of_memory, {}); }",
|
|
135
|
+
"#endif",
|
|
30
136
|
" }",
|
|
31
137
|
" _pf_refuse_failed_setting(nullptr);",
|
|
32
138
|
" }",
|
|
33
139
|
" void _pf_require_settings_ok() const {",
|
|
34
|
-
"#ifdef
|
|
140
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
141
|
+
' if (_pf_setting_failed_) throw _pf_latched_setting_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_, _pf_setting_failure_info_);',
|
|
142
|
+
"#elif defined(PF_SETTINGS_API_VERSION)",
|
|
35
143
|
' if (_pf_setting_failed_) throw ::pineforge::checked_settings::LatchedSettingsFailure(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
36
144
|
"#else",
|
|
37
145
|
' if (_pf_setting_failed_) throw std::runtime_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
@@ -39,19 +147,25 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
39
147
|
" }",
|
|
40
148
|
" bool _pf_refuse_failed_setting(ReportC* out) noexcept {",
|
|
41
149
|
" if (!_pf_setting_failed_) return false;",
|
|
150
|
+
"#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
|
|
151
|
+
' try { ::pineforge::note_run_failure(*this, _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_, _pf_setting_failure_info_.run_failure()); } catch (...) {}',
|
|
152
|
+
"#else",
|
|
42
153
|
' try { last_error_ = _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_; } catch (...) {}',
|
|
154
|
+
"#endif",
|
|
43
155
|
" if (out) *out = ReportC{};",
|
|
44
156
|
" return true;",
|
|
45
157
|
" }",
|
|
46
158
|
])
|
|
47
159
|
inputs = []
|
|
160
|
+
emitter._input_settings_metadata = {}
|
|
48
161
|
for node, binding in emitter._global_input_calls_with_names():
|
|
49
162
|
func_name, namespace = emitter._resolve_callee(node.callee)
|
|
50
163
|
name = emitter._get_input_title(node, var_name=binding)
|
|
51
164
|
key = emitter._input_key_literal(name)
|
|
52
165
|
default = emitter._get_input_default(node)
|
|
53
166
|
getter = emitter._input_getter_for_call(node, func_name, namespace)
|
|
54
|
-
|
|
167
|
+
default_lowered = _visit_setting_arg(emitter, default) if default is not None else "0"
|
|
168
|
+
default_cpp = default_lowered
|
|
55
169
|
default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
|
|
56
170
|
value_type = {
|
|
57
171
|
"get_input_int": "int", "get_input_int64": "int",
|
|
@@ -60,9 +174,15 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
60
174
|
}[getter]
|
|
61
175
|
kind = emitter._FORM_TYPE.get(func_name, value_type) if namespace == "input" else value_type
|
|
62
176
|
options = []
|
|
177
|
+
numeric_options = []
|
|
63
178
|
option_values = []
|
|
64
179
|
supported = "true"
|
|
65
180
|
default_serialized = None
|
|
181
|
+
if getter == "get_input_string":
|
|
182
|
+
default_serialized = _string_setting_arg(default, default_lowered)
|
|
183
|
+
if default_serialized is None:
|
|
184
|
+
default_serialized = 'std::string("")'
|
|
185
|
+
supported = "false"
|
|
66
186
|
names, merged = emitter._merged_args(node, func_name, namespace)
|
|
67
187
|
arguments = dict(zip(names or [], merged))
|
|
68
188
|
arguments.update(node.kwargs)
|
|
@@ -103,8 +223,20 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
103
223
|
if declared:
|
|
104
224
|
for option in declared:
|
|
105
225
|
option_cpp = _visit_setting_arg(emitter, option)
|
|
106
|
-
|
|
107
|
-
|
|
226
|
+
if getter == "get_input_string":
|
|
227
|
+
option_cpp = _string_setting_arg(option, option_cpp)
|
|
228
|
+
if option_cpp is None:
|
|
229
|
+
supported = "false"
|
|
230
|
+
else:
|
|
231
|
+
options.append(option_cpp)
|
|
232
|
+
else:
|
|
233
|
+
options.append(f'::pineforge::checked_settings::number({option_cpp})')
|
|
234
|
+
numeric_options.append(option_cpp)
|
|
235
|
+
if getter == "get_input_string":
|
|
236
|
+
if arguments.get("options") is not None and declared is None:
|
|
237
|
+
supported = "false"
|
|
238
|
+
if supported == "false":
|
|
239
|
+
options = []
|
|
108
240
|
expression = f'{getter}({key}, {default_cpp})'
|
|
109
241
|
effective = (expression if getter == "get_input_string" else
|
|
110
242
|
f'::pineforge::checked_settings::number({expression})')
|
|
@@ -119,6 +251,31 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
119
251
|
f'{64 if getter == "get_input_int64" else 32}, {supported}, '
|
|
120
252
|
f'{{{", ".join(option_values)}}}, "{kind}"}}')
|
|
121
253
|
inputs.append((metadata, effective))
|
|
254
|
+
manifest = {}
|
|
255
|
+
if getter == "get_input_string":
|
|
256
|
+
manifest["default"] = _string_setting_value(default_serialized)
|
|
257
|
+
if arguments.get("options") is not None or supported == "false":
|
|
258
|
+
manifest["options"] = [_string_setting_value(option) for option in options]
|
|
259
|
+
elif value_type == "source":
|
|
260
|
+
manifest["default"] = source
|
|
261
|
+
manifest["options"] = [_string_setting_value(option) for option in options]
|
|
262
|
+
elif value_type == "enum":
|
|
263
|
+
manifest["options"] = [_string_setting_value(option) for option in options]
|
|
264
|
+
elif namespace == "input" and kind in ("int", "float", "bool"):
|
|
265
|
+
# A typed numeric or bool input publishes the numbers the receipt
|
|
266
|
+
# holds, where its C++ is a plain literal.
|
|
267
|
+
default_value = _numeric_setting_value(default_cpp) if default is not None else None
|
|
268
|
+
if default_value is not None and isinstance(default_value, bool) == (kind == "bool"):
|
|
269
|
+
manifest["default"] = default_value
|
|
270
|
+
for field, lowered in zip(("min", "max", "step"), constraints):
|
|
271
|
+
bound = _numeric_setting_value(lowered)
|
|
272
|
+
if _is_number(bound):
|
|
273
|
+
manifest[field] = bound
|
|
274
|
+
values = [_numeric_setting_value(option) for option in numeric_options]
|
|
275
|
+
if values and all(_is_number(value) for value in values):
|
|
276
|
+
manifest["options"] = values
|
|
277
|
+
manifest["supported"] = supported == "true"
|
|
278
|
+
emitter._input_settings_metadata[id(node)] = manifest
|
|
122
279
|
|
|
123
280
|
overrides = [
|
|
124
281
|
("initial_capital", "float", "initial_capital", "initial_capital", "0.0"),
|
|
@@ -14,20 +14,167 @@ TradingView's read-only history (fixtures/array_history_tv).
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
from ..collection_history import (
|
|
17
|
+
COLLECTION_HISTORY_ARRAY_VALUE_CPP,
|
|
17
18
|
COLLECTION_HISTORY_CLASS_CPP,
|
|
18
19
|
COLLECTION_HISTORY_CPP,
|
|
19
20
|
COLLECTION_HISTORY_GENERIC_MATRIX_CPP,
|
|
20
21
|
COLLECTION_HISTORY_MATRIX_CPP,
|
|
22
|
+
NA_MATRIX_MESSAGE,
|
|
21
23
|
history_annotation,
|
|
22
24
|
literal_offset,
|
|
23
25
|
)
|
|
24
|
-
from ..ast_nodes import FuncCall, Identifier, MemberAccess
|
|
26
|
+
from ..ast_nodes import Assignment, FuncCall, FuncDef, Identifier, MemberAccess, MethodDef, Ternary, VarDecl
|
|
27
|
+
from ..limits import iter_ast_nodes
|
|
25
28
|
from ..symbols import TypeSpec
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class CollectionHistoryEmitter:
|
|
29
32
|
"""CodeGen mixin: the history members of array and matrix variables."""
|
|
30
33
|
|
|
34
|
+
def _matrix_history_value_names(self) -> set[str]:
|
|
35
|
+
cached = getattr(self, "_matrix_history_value_bindings", None)
|
|
36
|
+
if cached is not None:
|
|
37
|
+
return cached
|
|
38
|
+
names = set()
|
|
39
|
+
bindings = {}
|
|
40
|
+
callable_depth = None
|
|
41
|
+
for node, depth in iter_ast_nodes(self.ctx.ast):
|
|
42
|
+
if callable_depth is not None:
|
|
43
|
+
if depth > callable_depth:
|
|
44
|
+
continue
|
|
45
|
+
callable_depth = None
|
|
46
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
47
|
+
callable_depth = depth
|
|
48
|
+
continue
|
|
49
|
+
if isinstance(node, VarDecl):
|
|
50
|
+
name, value = node.name, node.value
|
|
51
|
+
elif isinstance(node, Assignment) and isinstance(node.target, Identifier):
|
|
52
|
+
name, value = node.target.name, node.value
|
|
53
|
+
else:
|
|
54
|
+
continue
|
|
55
|
+
values = [value]
|
|
56
|
+
while values:
|
|
57
|
+
source = values.pop()
|
|
58
|
+
annotation = history_annotation(source)
|
|
59
|
+
if annotation is not None and annotation["kind"] == "matrix":
|
|
60
|
+
names.add(name)
|
|
61
|
+
elif isinstance(source, Identifier):
|
|
62
|
+
bindings.setdefault(source.name, []).append(name)
|
|
63
|
+
elif isinstance(source, Ternary):
|
|
64
|
+
values.extend((source.true_val, source.false_val))
|
|
65
|
+
pending = list(names)
|
|
66
|
+
while pending:
|
|
67
|
+
source = pending.pop()
|
|
68
|
+
for name in bindings.get(source, ()):
|
|
69
|
+
if name not in names:
|
|
70
|
+
names.add(name)
|
|
71
|
+
pending.append(name)
|
|
72
|
+
self._matrix_history_value_bindings = names
|
|
73
|
+
return names
|
|
74
|
+
|
|
75
|
+
def _checked_matrix_history_receiver(self, receiver, receiver_node):
|
|
76
|
+
if (isinstance(receiver_node, Identifier)
|
|
77
|
+
and receiver_node.name in self._matrix_history_value_names()
|
|
78
|
+
and not self._collection_name_is_lexically_shadowed(receiver_node.name)
|
|
79
|
+
and receiver_node.name not in self._current_func_param_types):
|
|
80
|
+
return (f'([&]() -> auto& {{ if (is_na({receiver})) '
|
|
81
|
+
f'_PF_COLLECTION_STOP("na_reference", "matrix", "{NA_MATRIX_MESSAGE}"); '
|
|
82
|
+
f'return {receiver}; }}())')
|
|
83
|
+
return receiver
|
|
84
|
+
|
|
85
|
+
def _array_history_value_names(self) -> dict[str, TypeSpec]:
|
|
86
|
+
cached = getattr(self, "_array_history_value_specs", None)
|
|
87
|
+
if cached is not None:
|
|
88
|
+
return cached
|
|
89
|
+
bindings = {}
|
|
90
|
+
specs = {}
|
|
91
|
+
callable_depth = None
|
|
92
|
+
for node, depth in iter_ast_nodes(self.ctx.ast):
|
|
93
|
+
if callable_depth is not None:
|
|
94
|
+
if depth > callable_depth:
|
|
95
|
+
continue
|
|
96
|
+
callable_depth = None
|
|
97
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
98
|
+
callable_depth = depth
|
|
99
|
+
continue
|
|
100
|
+
if isinstance(node, VarDecl):
|
|
101
|
+
name, value = node.name, node.value
|
|
102
|
+
elif isinstance(node, Assignment) and isinstance(node.target, Identifier):
|
|
103
|
+
name, value = node.target.name, node.value
|
|
104
|
+
else:
|
|
105
|
+
continue
|
|
106
|
+
annotation = history_annotation(value)
|
|
107
|
+
if annotation is not None and annotation["use"] == "reference":
|
|
108
|
+
specs[name] = annotation["spec"]
|
|
109
|
+
else:
|
|
110
|
+
values = [value]
|
|
111
|
+
while values:
|
|
112
|
+
source_value = values.pop()
|
|
113
|
+
if isinstance(source_value, Identifier):
|
|
114
|
+
bindings.setdefault(source_value.name, []).append(name)
|
|
115
|
+
elif isinstance(source_value, Ternary):
|
|
116
|
+
values.extend((source_value.true_val, source_value.false_val))
|
|
117
|
+
pending = list(specs)
|
|
118
|
+
while pending:
|
|
119
|
+
source = pending.pop()
|
|
120
|
+
for name in bindings.get(source, ()):
|
|
121
|
+
if name not in specs:
|
|
122
|
+
specs[name] = specs[source]
|
|
123
|
+
pending.append(name)
|
|
124
|
+
for name, spec in specs.items():
|
|
125
|
+
self._array_vars.add(name)
|
|
126
|
+
self._collection_types[name] = spec
|
|
127
|
+
self._array_history_value_specs = specs
|
|
128
|
+
return specs
|
|
129
|
+
|
|
130
|
+
def _array_history_value_cpp_type(self, name: str) -> str | None:
|
|
131
|
+
spec = self._array_history_value_names().get(name)
|
|
132
|
+
if spec is None:
|
|
133
|
+
return None
|
|
134
|
+
spec = self._widen_array_spec_for_name(name, spec)
|
|
135
|
+
return f"_PFArrayHistoryValue<{self._type_spec_to_cpp(spec)}>"
|
|
136
|
+
|
|
137
|
+
def _array_history_value_expr_cpp_type(self, value) -> str | None:
|
|
138
|
+
values = [value]
|
|
139
|
+
while values:
|
|
140
|
+
source = values.pop()
|
|
141
|
+
if isinstance(source, Identifier):
|
|
142
|
+
if (self._collection_name_is_lexically_shadowed(source.name)
|
|
143
|
+
or source.name in self._current_func_param_types):
|
|
144
|
+
continue
|
|
145
|
+
cpp_type = self._array_history_value_cpp_type(source.name)
|
|
146
|
+
if cpp_type is not None:
|
|
147
|
+
return cpp_type
|
|
148
|
+
elif isinstance(source, Ternary):
|
|
149
|
+
values.extend((source.true_val, source.false_val))
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def _legacy_array_history_element(self, receiver: str, spec: TypeSpec,
|
|
153
|
+
offset: str, mutable: bool = False) -> str:
|
|
154
|
+
element_cpp = self._type_spec_to_cpp(spec.element)
|
|
155
|
+
if self._array_history_value_names():
|
|
156
|
+
receiver = f"_pf_array_id({receiver})"
|
|
157
|
+
if mutable:
|
|
158
|
+
return (
|
|
159
|
+
f"([&]() -> decltype(auto) {{ "
|
|
160
|
+
f"auto& _pf_history_array = {receiver}; "
|
|
161
|
+
f"const int _pf_history_offset = {offset}; "
|
|
162
|
+
f"if (_pf_history_offset < 0 || "
|
|
163
|
+
f"static_cast<size_t>(_pf_history_offset) >= _pf_history_array.size()) "
|
|
164
|
+
f'_PF_ARRAY_STOP("index_out_of_bounds", nullptr, '
|
|
165
|
+
f'"Array history element index is out of bounds."); '
|
|
166
|
+
f"return _pf_history_array.at(_pf_history_offset); }}())"
|
|
167
|
+
)
|
|
168
|
+
return (
|
|
169
|
+
f"([&]() -> {element_cpp} {{ "
|
|
170
|
+
f"const auto& _pf_history_array = {receiver}; "
|
|
171
|
+
f"const int _pf_history_offset = {offset}; "
|
|
172
|
+
f"if (_pf_history_offset < 0 || "
|
|
173
|
+
f"static_cast<size_t>(_pf_history_offset) >= _pf_history_array.size()) "
|
|
174
|
+
f"return na<{element_cpp}>(); "
|
|
175
|
+
f"return _pf_history_array[_pf_history_offset]; }}())"
|
|
176
|
+
)
|
|
177
|
+
|
|
31
178
|
def _collection_history_variables(self) -> list:
|
|
32
179
|
"""The declarations whose history the script reads, in key order."""
|
|
33
180
|
history = getattr(self.ctx, "collection_history", None) or {}
|
|
@@ -59,11 +206,12 @@ class CollectionHistoryEmitter:
|
|
|
59
206
|
|
|
60
207
|
def _emit_collection_history_helper(self, lines: list[str]) -> None:
|
|
61
208
|
variables = self._collection_history_variables()
|
|
62
|
-
if not variables:
|
|
209
|
+
if not variables and not self._array_history_value_names():
|
|
63
210
|
return
|
|
64
211
|
float_spec = TypeSpec.primitive("float")
|
|
65
212
|
matrices = [v.spec for v in variables if v.kind == "matrix"]
|
|
66
213
|
lines.append(COLLECTION_HISTORY_CPP.strip("\n"))
|
|
214
|
+
lines.append(COLLECTION_HISTORY_ARRAY_VALUE_CPP.strip("\n"))
|
|
67
215
|
if any(spec.element == float_spec for spec in matrices):
|
|
68
216
|
lines.append(COLLECTION_HISTORY_MATRIX_CPP.strip("\n"))
|
|
69
217
|
if any(spec.element != float_spec for spec in matrices):
|
|
@@ -146,6 +294,9 @@ class CollectionHistoryEmitter:
|
|
|
146
294
|
current = self._visit_expr(node.object)
|
|
147
295
|
use = annotation["use"]
|
|
148
296
|
offset = literal_offset(node)
|
|
297
|
+
if use == "reference" and offset == 0:
|
|
298
|
+
cpp_type = self._type_spec_to_cpp(annotation["spec"])
|
|
299
|
+
return f"_PFArrayHistoryValue<{cpp_type}>({current})"
|
|
149
300
|
if use == "loop" or offset == 0:
|
|
150
301
|
return current
|
|
151
302
|
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
@@ -153,6 +304,9 @@ class CollectionHistoryEmitter:
|
|
|
153
304
|
dynamic = offset is None
|
|
154
305
|
if use == "change":
|
|
155
306
|
return f"{member}.changed({index}, {current})"
|
|
307
|
+
if use == "reference":
|
|
308
|
+
return (f"{member}.reference({index}, {current})" if dynamic
|
|
309
|
+
else f"{member}.reference({index})")
|
|
156
310
|
if use == "copy":
|
|
157
311
|
return (f"{member}.value({index}, {current})" if dynamic
|
|
158
312
|
else f"{member}.value({index})")
|
|
@@ -168,7 +322,10 @@ class CollectionHistoryEmitter:
|
|
|
168
322
|
current = self._visit_expr(node.object)
|
|
169
323
|
offset = literal_offset(node)
|
|
170
324
|
if offset == 0:
|
|
171
|
-
|
|
325
|
+
nullable_array = (isinstance(node.object, Identifier)
|
|
326
|
+
and node.object.name in self._array_history_value_names())
|
|
327
|
+
return (f"is_na({current})" if annotation["kind"] == "matrix"
|
|
328
|
+
or nullable_array else "false")
|
|
172
329
|
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
173
330
|
index = self._collection_history_offset_cpp(node)
|
|
174
331
|
if offset is None:
|
|
@@ -179,19 +336,13 @@ class CollectionHistoryEmitter:
|
|
|
179
336
|
"""A matrix method called on a matrix's history, lowered as the
|
|
180
337
|
variable's own method call is (visit_call: a matrix variable's
|
|
181
338
|
method)."""
|
|
182
|
-
from .tables import MATRIX_METHOD_KWARGS, MATRIX_METHODS, _merge_kwargs
|
|
183
|
-
|
|
184
339
|
recv = self._visit_expr(receiver)
|
|
185
340
|
self._check_matrix_method_allowed(method, spec, node)
|
|
186
|
-
|
|
187
|
-
raw_args = (
|
|
188
|
-
_merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
189
|
-
if param_names and node.kwargs else list(node.args)
|
|
190
|
-
)
|
|
341
|
+
raw_args = self._matrix_method_arg_nodes(method, node)
|
|
191
342
|
args = self._matrix_bool_value_args(
|
|
192
|
-
method, [self._visit_expr(a) for a in raw_args], raw_args, spec)
|
|
343
|
+
method, [self._visit_expr(a) if a is not None else None for a in raw_args], raw_args, spec)
|
|
193
344
|
try:
|
|
194
|
-
return
|
|
345
|
+
return self._matrix_method_expr(recv, method, args, raw_args, node)
|
|
195
346
|
except IndexError:
|
|
196
347
|
self._codegen_error(
|
|
197
348
|
node,
|
|
@@ -84,6 +84,8 @@ from __future__ import annotations
|
|
|
84
84
|
|
|
85
85
|
import re
|
|
86
86
|
|
|
87
|
+
from .run_stops import RUN_STOP_SHIMS_CPP
|
|
88
|
+
|
|
87
89
|
from ..ast_nodes import (
|
|
88
90
|
BoolLiteral, ExprStmt, ForInStmt, ForStmt, FuncCall, Identifier, IfStmt,
|
|
89
91
|
MemberAccess, SwitchStmt, TupleAssign, TupleLiteral, VarDecl, WhileStmt,
|
|
@@ -218,6 +220,7 @@ class TopLevelEmitter:
|
|
|
218
220
|
# in favour of fully qualified names emitted at each call site.
|
|
219
221
|
lines.append("using namespace pineforge;")
|
|
220
222
|
lines.append("")
|
|
223
|
+
lines.append(RUN_STOP_SHIMS_CPP)
|
|
221
224
|
self._emit_ta_compat_shims(lines)
|
|
222
225
|
if getattr(self, "_uses_drawing", False):
|
|
223
226
|
lines.append(DRAWING_LIFETIME_CPP)
|
|
@@ -1281,6 +1284,7 @@ class TopLevelEmitter:
|
|
|
1281
1284
|
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
1282
1285
|
lines.append(" }")
|
|
1283
1286
|
from .checked_settings import emit_settings_members
|
|
1287
|
+
self._order_shape_constructor = tuple(ctor_body)
|
|
1284
1288
|
emit_settings_members(self, lines, ctor_body)
|
|
1285
1289
|
|
|
1286
1290
|
if self._uses_recorded_requests():
|
|
@@ -1314,7 +1318,7 @@ class TopLevelEmitter:
|
|
|
1314
1318
|
tf = info.get("tf")
|
|
1315
1319
|
tf_expr = info.get("tf_expr")
|
|
1316
1320
|
if tf:
|
|
1317
|
-
tf_expr =
|
|
1321
|
+
tf_expr = self._security_tf_literal(tf)
|
|
1318
1322
|
elif tf == "" and not tf_expr:
|
|
1319
1323
|
# An empty timeframe string is the chart's.
|
|
1320
1324
|
tf_expr = "input_tf_"
|
|
@@ -1421,7 +1425,7 @@ class TopLevelEmitter:
|
|
|
1421
1425
|
sec_id = info["sec_id"]
|
|
1422
1426
|
symbol = self._security_tf_runtime_expr(info["symbol_node"])
|
|
1423
1427
|
column = self._security_footprint_column(sec_id)
|
|
1424
|
-
column = f', "{column}"' if column else ""
|
|
1428
|
+
column = f', "{self._cpp_string_escape(column)}"' if column else ""
|
|
1425
1429
|
ignore_node = info.get("ignore_invalid_node")
|
|
1426
1430
|
if ignore_node is None:
|
|
1427
1431
|
ignore = "false"
|
|
@@ -1886,15 +1890,24 @@ class TopLevelEmitter:
|
|
|
1886
1890
|
for pragma in pragmas:
|
|
1887
1891
|
cpp_expr = self._visit_expr(pragma.expr_node)
|
|
1888
1892
|
lines.append(
|
|
1889
|
-
f'{inner}trace(std::string("{pragma.name}"), '
|
|
1893
|
+
f'{inner}trace(std::string("{self._cpp_string_escape(pragma.name)}"), '
|
|
1890
1894
|
f"(double)({cpp_expr}));"
|
|
1891
1895
|
)
|
|
1892
1896
|
lines.append(f"{pad}}}")
|
|
1893
1897
|
|
|
1894
1898
|
def _emit_extern_c(self, lines: list[str]) -> None:
|
|
1895
1899
|
lines.append('extern "C" {')
|
|
1900
|
+
lines.append("#if defined(PINEFORGE_HAS_RUN_FAILURE_CODES_V1) && defined(PF_SETTINGS_API_VERSION)")
|
|
1901
|
+
lines.append(" int strategy_create_checked(const char* params_json, void** out, char* error, size_t error_capacity);")
|
|
1902
|
+
lines.append("#endif")
|
|
1896
1903
|
lines.append(" void* strategy_create(const char* params_json) {")
|
|
1904
|
+
lines.append("#if defined(PINEFORGE_HAS_RUN_FAILURE_CODES_V1) && defined(PF_SETTINGS_API_VERSION)")
|
|
1905
|
+
lines.append(" void* out = nullptr;")
|
|
1906
|
+
lines.append(" strategy_create_checked(nullptr, &out, nullptr, 0);")
|
|
1907
|
+
lines.append(" return out;")
|
|
1908
|
+
lines.append("#else")
|
|
1897
1909
|
lines.append(" try { return new GeneratedStrategy(); } catch (...) { return nullptr; }")
|
|
1910
|
+
lines.append("#endif")
|
|
1898
1911
|
lines.append(" }")
|
|
1899
1912
|
lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
|
|
1900
1913
|
lines.append(" try {")
|
|
@@ -1911,7 +1924,7 @@ class TopLevelEmitter:
|
|
|
1911
1924
|
lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
|
|
1912
1925
|
lines.append(' } catch (const std::exception& _pf_error) {')
|
|
1913
1926
|
lines.append(' if (out) *out = ReportC{};')
|
|
1914
|
-
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", _pf_error
|
|
1927
|
+
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", _pf_error);')
|
|
1915
1928
|
lines.append(' } catch (...) {')
|
|
1916
1929
|
lines.append(' if (out) *out = ReportC{};')
|
|
1917
1930
|
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", "unknown C++ exception");')
|
|
@@ -1968,7 +1981,7 @@ class TopLevelEmitter:
|
|
|
1968
1981
|
" void run_backtest_full(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf,",
|
|
1969
1982
|
" int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out) {",
|
|
1970
1983
|
" try { _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out); }",
|
|
1971
|
-
' catch (const std::exception& _pf_error) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", _pf_error
|
|
1984
|
+
' catch (const std::exception& _pf_error) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", _pf_error); }',
|
|
1972
1985
|
' catch (...) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", "unknown C++ exception"); }',
|
|
1973
1986
|
" }",
|
|
1974
1987
|
])
|
|
@@ -1981,25 +1994,28 @@ class TopLevelEmitter:
|
|
|
1981
1994
|
lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
|
|
1982
1995
|
lines.append(" if (!s || !key || !value) return;")
|
|
1983
1996
|
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_input(key, value); }")
|
|
1984
|
-
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)
|
|
1985
|
-
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)
|
|
1997
|
+
lines.append(' catch (const std::exception& _pf_error) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_input", _pf_error.what(), nullptr); }')
|
|
1998
|
+
lines.append(' catch (...) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_input", "unknown C++ exception", nullptr); }')
|
|
1986
1999
|
lines.append(" }")
|
|
1987
2000
|
lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
|
|
1988
2001
|
lines.append(" if (!s || !key || !value) return;")
|
|
1989
2002
|
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value); }")
|
|
1990
|
-
lines.append('
|
|
1991
|
-
lines.append(' catch (
|
|
2003
|
+
lines.append('#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1')
|
|
2004
|
+
lines.append(' catch (const std::invalid_argument& _pf_error) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_override", _pf_error.what(), "unparseable_value"); }')
|
|
2005
|
+
lines.append(' catch (const std::out_of_range& _pf_error) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_override", _pf_error.what(), "unparseable_value"); }')
|
|
2006
|
+
lines.append('#endif')
|
|
2007
|
+
lines.append(' catch (const std::exception& _pf_error) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_override", _pf_error.what(), nullptr); }')
|
|
2008
|
+
lines.append(' catch (...) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_override", "unknown C++ exception", nullptr); }')
|
|
1992
2009
|
lines.append(" }")
|
|
1993
2010
|
lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
|
|
1994
2011
|
lines.append(" if (!s) return;")
|
|
1995
2012
|
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0); }")
|
|
1996
|
-
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)
|
|
1997
|
-
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)
|
|
2013
|
+
lines.append(' catch (const std::exception& _pf_error) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_magnifier_volume_weighted", _pf_error.what(), nullptr); }')
|
|
2014
|
+
lines.append(' catch (...) { _PF_SETTING_FAILURE(static_cast<GeneratedStrategy*>(s), "strategy_set_magnifier_volume_weighted", "unknown C++ exception", nullptr); }')
|
|
1998
2015
|
lines.append(" }")
|
|
1999
2016
|
from .checked_settings import emit_settings_exports
|
|
2000
2017
|
emit_settings_exports(lines)
|
|
2001
|
-
|
|
2002
|
-
emit_capabilities_exports(self, lines)
|
|
2018
|
+
self._capabilities_export_at = len(lines)
|
|
2003
2019
|
if self._declares_bar_magnifier():
|
|
2004
2020
|
# TradingView runs a script that declares use_bar_magnifier = true
|
|
2005
2021
|
# on its bar magnifier; the host reads this export to run it on
|