@pineforge/codegen-pyodide 1.3.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 (30) hide show
  1. package/package.json +1 -1
  2. package/pineforge_codegen/__init__.py +6 -5
  3. package/pineforge_codegen/analyzer/base.py +3 -3
  4. package/pineforge_codegen/analyzer/types.py +7 -3
  5. package/pineforge_codegen/codegen/base.py +26 -16
  6. package/pineforge_codegen/codegen/checked_settings.py +113 -2
  7. package/pineforge_codegen/codegen/collection_history.py +163 -12
  8. package/pineforge_codegen/codegen/emit_top.py +24 -8
  9. package/pineforge_codegen/codegen/helpers.py +19 -1
  10. package/pineforge_codegen/codegen/host_members.py +1 -0
  11. package/pineforge_codegen/codegen/input.py +23 -8
  12. package/pineforge_codegen/codegen/run_stops.py +52 -0
  13. package/pineforge_codegen/codegen/security.py +12 -5
  14. package/pineforge_codegen/codegen/tables.py +132 -28
  15. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  16. package/pineforge_codegen/codegen/types.py +85 -4
  17. package/pineforge_codegen/codegen/visit_call.py +174 -55
  18. package/pineforge_codegen/codegen/visit_expr.py +32 -6
  19. package/pineforge_codegen/codegen/visit_stmt.py +3 -0
  20. package/pineforge_codegen/collection_history.py +122 -20
  21. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  22. package/pineforge_codegen/external_requests.py +30 -9
  23. package/pineforge_codegen/matrix_overloads.py +7 -0
  24. package/pineforge_codegen/parser.py +7 -2
  25. package/pineforge_codegen/signatures.py +2 -2
  26. package/pineforge_codegen/support_checker.py +82 -0
  27. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +7 -3
  30. package/pineforge_codegen-1.3.0.tar.gz +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)
@@ -1894,8 +1897,17 @@ class TopLevelEmitter:
1894
1897
 
1895
1898
  def _emit_extern_c(self, lines: list[str]) -> None:
1896
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")
1897
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")
1898
1909
  lines.append(" try { return new GeneratedStrategy(); } catch (...) { return nullptr; }")
1910
+ lines.append("#endif")
1899
1911
  lines.append(" }")
1900
1912
  lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
1901
1913
  lines.append(" try {")
@@ -1912,7 +1924,7 @@ class TopLevelEmitter:
1912
1924
  lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
1913
1925
  lines.append(' } catch (const std::exception& _pf_error) {')
1914
1926
  lines.append(' if (out) *out = ReportC{};')
1915
- 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);')
1916
1928
  lines.append(' } catch (...) {')
1917
1929
  lines.append(' if (out) *out = ReportC{};')
1918
1930
  lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", "unknown C++ exception");')
@@ -1969,7 +1981,7 @@ class TopLevelEmitter:
1969
1981
  " void run_backtest_full(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf,",
1970
1982
  " int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out) {",
1971
1983
  " try { _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out); }",
1972
- ' 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); }',
1973
1985
  ' catch (...) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", "unknown C++ exception"); }',
1974
1986
  " }",
1975
1987
  ])
@@ -1982,20 +1994,24 @@ class TopLevelEmitter:
1982
1994
  lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
1983
1995
  lines.append(" if (!s || !key || !value) return;")
1984
1996
  lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_input(key, value); }")
1985
- lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", _pf_error.what()); }')
1986
- 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); }')
1987
1999
  lines.append(" }")
1988
2000
  lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
1989
2001
  lines.append(" if (!s || !key || !value) return;")
1990
2002
  lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value); }")
1991
- lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", _pf_error.what()); }')
1992
- 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); }')
1993
2009
  lines.append(" }")
1994
2010
  lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
1995
2011
  lines.append(" if (!s) return;")
1996
2012
  lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0); }")
1997
- lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", _pf_error.what()); }')
1998
- 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); }')
1999
2015
  lines.append(" }")
2000
2016
  from .checked_settings import emit_settings_exports
2001
2017
  emit_settings_exports(lines)
@@ -262,12 +262,18 @@ CPP_STANDARD_MACROS = frozenset("""
262
262
  PINEFORGE_HAS_NATIVE_LOWERING_V1 PINEFORGE_HAS_NATIVE_LIVE_V1
263
263
  PINEFORGE_HAS_AUX_SECURITY_FEED_V1 PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1
264
264
  PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1
265
+ PINEFORGE_HAS_RUN_FAILURE_CODES_V1
266
+ _PF_NO_DATA_STOP _PF_OTHER_SYMBOL_STOP _PF_ARRAY_STOP _PF_COLLECTION_STOP
267
+ _PF_NA_STOP _PF_LIMIT_STOP _PF_UNSUPPORTED_STOP _PF_STRING_STOP
268
+ _PF_ENGINE_INVARIANT _PF_INVARIANT_AT _PF_SETTING_FAILURE
265
269
  PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1
266
270
  PINEFORGE_NO_STRATEGY_DECLS PF_PINE_TIME_HAS_SESSION_DAY
267
271
  PF_PINE_TIME_SESSION_DAY_ARGS PF_VWAP_HAS_SESSION_ANCHOR
268
272
  PF_VWAP_SESSION_ANCHOR_ARGS PF_ALMA_HAS_FLOOR
269
273
  PF_KC_HAS_USE_TRUE_RANGE PF_VWAP_HAS_ANCHOR_INPUT
270
274
  PF_PIVOT_LEVELS_HAS_ANCHOR
275
+ PF_ABI_VERSION PF_NATIVE_API_VERSION
276
+ PF_CAPABILITIES_API_VERSION PF_SETTINGS_API_VERSION
271
277
  """.split())
272
278
 
273
279
  # Identifiers used by the emitter as class/type names, namespaces, and
@@ -283,6 +289,9 @@ CPP_EMITTER_NAMES = frozenset("""
283
289
  commit_script_state set_strategy_override set_input
284
290
  set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
285
291
  strategy_capabilities_api_version strategy_capabilities_receipt
292
+ strategy_confirmed_bar_api_version strategy_confirmed_bar_receipt
293
+ strategy_order_shapes_api_version strategy_order_shapes_receipt
294
+ strategy_settings_api_version strategy_get_effective_settings
286
295
  precalculate
287
296
  strategy_entry strategy_close strategy_close_all strategy_exit
288
297
  strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order
@@ -291,6 +300,9 @@ CPP_EMITTER_NAMES = frozenset("""
291
300
  live_position_size pending_order_count market_admission_journal
292
301
  pine_time pine_time_close pine_time_tradingday pine_random
293
302
  pine_runtime_error pine_enum_str_at pine_session_ismarket
303
+ pine_no_data_stop pine_other_symbol_stop pine_array_stop pine_collection_stop
304
+ pine_na_stop pine_limit_stop pine_unsupported_stop pine_string_stop
305
+ pine_engine_invariant note_run_failure note_run_failure_unknown
294
306
  pine_session_ispostmarket pine_session_ispremarket _PFSessionMarket
295
307
  _pf_session_market_ script_tf_
296
308
  tf_change tf_is_daily tf_is_intraday tf_is_monthly tf_is_seconds
@@ -317,7 +329,8 @@ CPP_EMITTER_NAMES = frozenset("""
317
329
  get_input_int get_input_float get_input_bool get_input_string
318
330
  _pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
319
331
  _pf_record_setting_failure _pf_require_settings_ok
320
- _pf_setting_failed_ _pf_setting_failure_
332
+ _pf_setting_failed_ _pf_setting_failure_ _pf_setting_failure_info_
333
+ _pf_setting_error_base _pf_latched_setting_error _pf_invariant_at
321
334
  _pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
322
335
  _pf_set_override_checked _pf_settings_receipt
323
336
  _pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
@@ -369,12 +382,16 @@ CPP_TEMPORARY_NAMES = frozenset("""
369
382
  __pf_s __pf_r __pf_p __pf_i __pf_j __pf_t __pf_it __pf_m __pf_c __pf_n
370
383
  __pf_k __pf_b __pf_ma __pf_mb __pf_f __pf_d __pf_best __pf_bc __pf_rank
371
384
  __pf_idx __pf_occ
385
+ _pf_number_text
386
+ _pf_substring_text _pf_substring_begin _pf_substring_end _pf_substring_error
387
+ _pf_array_new_size _pf_array_new_value _pf_array_new_size_type _pf_array_new_error
372
388
  """.split())
373
389
  _CPP_TEMPORARY_PATTERN = re.compile(
374
390
  r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
375
391
  r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
376
392
  r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
377
393
  r"|_pf_collection_hist_\w+"
394
+ r"|_pf_substring_arg\d+"
378
395
  )
379
396
 
380
397
 
@@ -432,6 +449,7 @@ class NamingHelper:
432
449
  .replace("\n", "\\n")
433
450
  .replace("\r", "\\r")
434
451
  .replace("\t", "\\t")
452
+ .replace("\0", "\\000")
435
453
  )
436
454
 
437
455
  @staticmethod
@@ -148,6 +148,7 @@ HOST_MEMBER_NAMES = frozenset({
148
148
  "set_pine_risk_max_intraday_filled_orders",
149
149
  "set_pine_risk_max_intraday_loss",
150
150
  "set_pine_risk_max_position_size",
151
+ "set_recorded_series",
151
152
  "set_strategy_override",
152
153
  "signed_position_size",
153
154
  "snapshot_script_state",
@@ -25,6 +25,7 @@ from ..ast_nodes import (
25
25
  MemberAccess,
26
26
  NumberLiteral,
27
27
  StringLiteral,
28
+ UnaryOp,
28
29
  VarDecl,
29
30
  )
30
31
  from .. import signatures as sigs
@@ -453,7 +454,7 @@ class InputHelper:
453
454
  # Input manifest extraction (host UI override-form source of truth)
454
455
  # ------------------------------------------------------------------
455
456
 
456
- def _literal_or_none(self, node):
457
+ def _literal_or_none(self, node, *, signed: bool = False):
457
458
  """Return a JSON scalar for a *const* literal AST node, else None.
458
459
 
459
460
  ``None`` signals non-const (an identifier, computed expression, …) so
@@ -469,6 +470,10 @@ class InputHelper:
469
470
  return node.value
470
471
  if isinstance(node, NumberLiteral):
471
472
  return node.value
473
+ if signed and isinstance(node, UnaryOp) and node.op in ("+", "-"):
474
+ value = self._literal_or_none(node.operand, signed=True)
475
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
476
+ return value if node.op == "+" else -value
472
477
  # enum member ref like ``Dir.Up`` -> "Dir.Up" (string tag)
473
478
  if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
474
479
  return f"{node.object.name}.{node.member}"
@@ -503,12 +508,17 @@ class InputHelper:
503
508
  ``var = input.*(...)`` declaration and an inline call inside an
504
509
  expression (``ta.ema(close, input.int(9, "Fast"))``) alike.
505
510
 
506
- Each entry: ``{title, type, default[, min, max, step, options]}``. The
507
- optional keys are emitted only when the corresponding signature
508
- argument is a const literal; a bound/option referencing a non-literal
509
- is omitted (never crashes). ``title`` is the key the emitted C++ reads
510
- the input by: the title argument, else the name of the declaration
511
- holding the call (``pine_spelling.input_binding_names``), else "".
511
+ Each entry: ``{title, type, default, supported[, min, max, step,
512
+ options]}``. ``supported``, a string input's default and options, a
513
+ source input's default and choices, an enum's choices and a typed
514
+ numeric input's literal default, bounds and options are the values
515
+ of the checked-settings receipt (the descriptor ``generate()``
516
+ builds with it). The optional keys are emitted only when the
517
+ corresponding signature argument is a const literal; a bound/option
518
+ referencing a non-literal is omitted (never crashes). ``title`` is
519
+ the key the emitted C++ reads the input by: the title argument, else
520
+ the name of the declaration holding the call
521
+ (``pine_spelling.input_binding_names``), else "".
512
522
  """
513
523
  return [self._input_manifest_entry(node, name)
514
524
  for node, name in self._global_input_calls_with_names()]
@@ -528,7 +538,7 @@ class InputHelper:
528
538
  title = self._get_input_title(node, var_name=var_name)
529
539
  default_node = self._get_input_default(node)
530
540
  default_val = (
531
- self._literal_or_none(default_node)
541
+ self._literal_or_none(default_node, signed=True)
532
542
  if default_node is not None
533
543
  else None
534
544
  )
@@ -581,4 +591,9 @@ class InputHelper:
581
591
  # any non-const element -> omit the whole options list
582
592
  if vals and all(isinstance(v, str) for v in vals):
583
593
  entry["options"] = vals
594
+ descriptors = getattr(self, "_input_settings_metadata", None)
595
+ if descriptors is None:
596
+ raise RuntimeError("the input manifest reads the settings descriptors "
597
+ "generate() builds: call generate() first")
598
+ entry.update(descriptors[id(node)])
584
599
  return entry
@@ -0,0 +1,52 @@
1
+ """Optional coded run stops, with the original throws on older engines."""
2
+
3
+ from .helpers import NamingHelper
4
+
5
+
6
+ RUN_STOP_SHIMS_CPP = r"""
7
+ #ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
8
+ #define _PF_NO_DATA_STOP(function, call, line, english) ::pineforge::pine_no_data_stop(function, call, line, english)
9
+ #define _PF_OTHER_SYMBOL_STOP(function, symbol, call, line, english) ::pineforge::pine_other_symbol_stop(function, symbol, call, line, english)
10
+ #define _PF_ARRAY_STOP(reason, method, english) ::pineforge::pine_array_stop(reason, method, std::string(english).c_str())
11
+ #define _PF_COLLECTION_STOP(reason, object, english) ::pineforge::pine_collection_stop(object, reason, english)
12
+ #define _PF_NA_STOP(object, english) ::pineforge::pine_na_stop(object, english)
13
+ #define _PF_LIMIT_STOP(limit, max, english) ::pineforge::pine_limit_stop(limit, max, english)
14
+ #define _PF_UNSUPPORTED_STOP(reason, line, english) ::pineforge::pine_unsupported_stop(reason, line, english)
15
+ #define _PF_STRING_STOP(reason, english) ::pineforge::pine_string_stop(reason, english)
16
+ #define _PF_ENGINE_INVARIANT(english, legacy_type) ::pineforge::pine_engine_invariant(english)
17
+ #define _PF_INVARIANT_AT(container, index) _pf_invariant_at(container, index)
18
+ #define _PF_SETTING_FAILURE(strategy, entrypoint, message, reason) (strategy)->_pf_record_setting_failure(entrypoint, message, [] { return reason ? ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::setting_rejected, {{"entrypoint", entrypoint}, {"reason", reason}}) : ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::setting_rejected, {{"entrypoint", entrypoint}}); })
19
+ template <typename Container, typename Index>
20
+ decltype(auto) _pf_invariant_at(Container& container, Index index) {
21
+ try { return container.at(index); }
22
+ catch (const std::out_of_range& error) { ::pineforge::pine_engine_invariant(error.what()); }
23
+ }
24
+ #else
25
+ #define _PF_NO_DATA_STOP(function, call, line, english) pine_runtime_error(std::string(english))
26
+ #define _PF_OTHER_SYMBOL_STOP(function, symbol, call, line, english) pine_runtime_error(std::string(english))
27
+ #define _PF_ARRAY_STOP(reason, method, english) pine_runtime_error(english)
28
+ #define _PF_COLLECTION_STOP(reason, object, english) pine_runtime_error(english)
29
+ #define _PF_NA_STOP(object, english) throw std::runtime_error(english)
30
+ #define _PF_LIMIT_STOP(limit, max, english) throw std::length_error(english)
31
+ #define _PF_UNSUPPORTED_STOP(reason, line, english) pine_runtime_error(std::string(english))
32
+ #define _PF_STRING_STOP(reason, english) pine_runtime_error(std::string(english))
33
+ #define _PF_ENGINE_INVARIANT(english, legacy_type) throw legacy_type(english)
34
+ #define _PF_INVARIANT_AT(container, index) (container).at(index)
35
+ #define _PF_SETTING_FAILURE(strategy, entrypoint, message, reason) (strategy)->_pf_record_setting_failure(entrypoint, message)
36
+ #endif
37
+ """
38
+
39
+
40
+ def request_stop(marker: dict) -> str:
41
+ """Metadata comes only from the original request's compile-time spelling."""
42
+ escape = NamingHelper._cpp_string_escape
43
+ function = f'"{escape(marker["function"])}"'
44
+ call = f'"{escape(marker["call"])}"'
45
+ english = f'"{escape(marker["message"])}"'
46
+ arguments = f'{function}, {call}, {marker["line"]}, {english}'
47
+ if marker["kind"] == "other_symbol":
48
+ literal = marker["symbol_literal"]
49
+ symbol = "nullptr" if literal is None else f'"{escape(literal)}"'
50
+ arguments = f'{function}, {symbol}, {call}, {marker["line"]}, {english}'
51
+ return f"_PF_OTHER_SYMBOL_STOP({arguments})"
52
+ return f"_PF_NO_DATA_STOP({arguments})"
@@ -2655,7 +2655,7 @@ class SecurityEmitter:
2655
2655
 
2656
2656
  def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
2657
2657
  names = []
2658
- for (sid, _node_id), meta in sorted(self._security_expr_hist_by_node.items()):
2658
+ for (sid, _node_id), meta in self._security_expr_hist_by_node.items():
2659
2659
  if sid == sec_id:
2660
2660
  names.append(meta["name"])
2661
2661
  return names
@@ -7227,7 +7227,8 @@ class SecurityEmitter:
7227
7227
  "request.security: a Heikin-Ashi request inside another request's "
7228
7228
  "expression reads that request's Heikin-Ashi bars, which PineForge "
7229
7229
  "does not build")
7230
- return (f'([&]() -> {cpp_t} {{ pine_runtime_error(std::string("{message}")); '
7230
+ line = expr_node.loc.line if expr_node.loc else 1
7231
+ return (f'([&]() -> {cpp_t} {{ _PF_UNSUPPORTED_STOP("nested_heikinashi_request", {line}, "{message}"); '
7231
7232
  f"return {na_value}; }}())")
7232
7233
 
7233
7234
  site = self._get_ta_site(expr_node)
@@ -7886,8 +7887,9 @@ class SecurityEmitter:
7886
7887
  def _security_variant_order_key(self, signature: tuple, binding_stack) -> tuple:
7887
7888
  """The order of a TA site's requested-context variants (``_v0``,
7888
7889
  ``_v1``, ...): where each argument binding's value is written in the
7889
- source, then the signature. The signature names a bound node by its
7890
- ``id()``, so ordering by its ``repr`` followed memory addresses, and
7890
+ source, then the signature's stable scope snapshot, preserving
7891
+ traversal order at ties. The signature names a bound node by its
7892
+ ``id()``, so ordering by its raw ``repr`` followed memory addresses, and
7891
7893
  the same script could number its variants differently from run to
7892
7894
  run or between CPython and Pyodide."""
7893
7895
  frames = []
@@ -7909,4 +7911,9 @@ class SecurityEmitter:
7909
7911
  frames.append(("arguments", tuple(items)))
7910
7912
  else:
7911
7913
  frames.append(("locals", tuple(sorted(frame.keys()))))
7912
- return tuple(frames), repr(signature)
7914
+ def stable_part(value):
7915
+ if isinstance(value, tuple):
7916
+ return tuple(stable_part(item) for item in value)
7917
+ return 0 if isinstance(value, int) else value
7918
+
7919
+ return tuple(frames), repr(stable_part(signature))