@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.
Files changed (34) hide show
  1. package/LICENSE +19 -3
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +6 -5
  5. package/pineforge_codegen/analyzer/base.py +3 -3
  6. package/pineforge_codegen/analyzer/types.py +7 -3
  7. package/pineforge_codegen/codegen/base.py +36 -17
  8. package/pineforge_codegen/codegen/capabilities.py +143 -10
  9. package/pineforge_codegen/codegen/checked_settings.py +161 -4
  10. package/pineforge_codegen/codegen/collection_history.py +163 -12
  11. package/pineforge_codegen/codegen/emit_top.py +29 -13
  12. package/pineforge_codegen/codegen/helpers.py +25 -1
  13. package/pineforge_codegen/codegen/host_members.py +1 -0
  14. package/pineforge_codegen/codegen/input.py +23 -8
  15. package/pineforge_codegen/codegen/order_shapes.py +356 -0
  16. package/pineforge_codegen/codegen/run_stops.py +52 -0
  17. package/pineforge_codegen/codegen/security.py +66 -10
  18. package/pineforge_codegen/codegen/tables.py +144 -28
  19. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  20. package/pineforge_codegen/codegen/types.py +85 -4
  21. package/pineforge_codegen/codegen/visit_call.py +235 -55
  22. package/pineforge_codegen/codegen/visit_expr.py +40 -8
  23. package/pineforge_codegen/codegen/visit_stmt.py +5 -4
  24. package/pineforge_codegen/collection_history.py +122 -20
  25. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  26. package/pineforge_codegen/external_requests.py +30 -9
  27. package/pineforge_codegen/matrix_overloads.py +7 -0
  28. package/pineforge_codegen/parser.py +7 -2
  29. package/pineforge_codegen/signatures.py +2 -2
  30. package/pineforge_codegen/support_checker.py +101 -10
  31. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  32. package/release.json +2 -2
  33. package/tables.json +7 -3
  34. 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 PF_SETTINGS_API_VERSION",
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
- default_cpp = _visit_setting_arg(emitter, default) if default is not None else "0"
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
- options.append(option_cpp if getter == "get_input_string" else
107
- f'::pineforge::checked_settings::number({option_cpp})')
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
- return f"is_na({current})" if annotation["kind"] == "matrix" else "false"
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
- param_names = MATRIX_METHOD_KWARGS.get(method)
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 MATRIX_METHODS[method](recv, args)
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 = f'"{tf}"'
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.what());')
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.what()); }',
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)->_pf_record_setting_failure("strategy_set_input", _pf_error.what()); }')
1985
- lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", "unknown C++ exception"); }')
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(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", _pf_error.what()); }')
1991
- lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", "unknown C++ exception"); }')
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)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", _pf_error.what()); }')
1997
- lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", "unknown C++ exception"); }')
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
- from .capabilities import emit_capabilities_exports
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