@pineforge/codegen-pyodide 1.0.0 → 1.1.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 (31) hide show
  1. package/glue.py +2 -1
  2. package/package.json +1 -1
  3. package/pineforge_codegen/__init__.py +24 -9
  4. package/pineforge_codegen/analyzer/base.py +190 -1
  5. package/pineforge_codegen/analyzer/call_handlers.py +2 -0
  6. package/pineforge_codegen/analyzer/contracts.py +6 -0
  7. package/pineforge_codegen/analyzer/types.py +16 -5
  8. package/pineforge_codegen/ast_nodes.py +7 -0
  9. package/pineforge_codegen/codegen/base.py +136 -14
  10. package/pineforge_codegen/codegen/checked_settings.py +272 -0
  11. package/pineforge_codegen/codegen/collection_history.py +200 -0
  12. package/pineforge_codegen/codegen/emit_top.py +76 -14
  13. package/pineforge_codegen/codegen/helpers.py +12 -2
  14. package/pineforge_codegen/codegen/host_members.py +8 -0
  15. package/pineforge_codegen/codegen/input.py +36 -3
  16. package/pineforge_codegen/codegen/security.py +56 -1
  17. package/pineforge_codegen/codegen/ta.py +65 -1
  18. package/pineforge_codegen/codegen/types.py +136 -8
  19. package/pineforge_codegen/codegen/visit_call.py +92 -3
  20. package/pineforge_codegen/codegen/visit_expr.py +202 -3
  21. package/pineforge_codegen/codegen/visit_stmt.py +19 -4
  22. package/pineforge_codegen/collection_history.py +1595 -0
  23. package/pineforge_codegen/external_requests.py +3 -0
  24. package/pineforge_codegen/parser.py +19 -4
  25. package/pineforge_codegen/request_discovery.py +506 -0
  26. package/pineforge_codegen/support_checker.py +37 -0
  27. package/pineforge_codegen-1.1.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +1 -1
  30. package/transpile.worker.mjs +2 -1
  31. package/pineforge_codegen-1.0.0.tar.gz +0 -0
@@ -0,0 +1,200 @@
1
+ """The C++ of an array's or a matrix's history (``a[k]``, ``m[k]``).
2
+
3
+ The analyzer annotates every such read with how its value is used and lists
4
+ the variables whose history the script reads (``ctx.collection_history``;
5
+ pineforge_codegen/collection_history.py). Such a variable keeps its own
6
+ ``std::vector`` or matrix member, so every other operation on it keeps its
7
+ C++; beside it, ``_pf_collection_hist_<name>`` keeps the copies its
8
+ executions left. Each execution of the declaration -- each bar, for a top
9
+ level ``var`` -- opens a slot (``open``) and the end of the bar closes it with
10
+ a copy (``close``), so ``a[1]`` is the array as the previous bar left it,
11
+ TradingView's read-only history (fixtures/array_history_tv).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from ..collection_history import (
17
+ COLLECTION_HISTORY_CLASS_CPP,
18
+ COLLECTION_HISTORY_CPP,
19
+ COLLECTION_HISTORY_GENERIC_MATRIX_CPP,
20
+ COLLECTION_HISTORY_MATRIX_CPP,
21
+ history_annotation,
22
+ literal_offset,
23
+ )
24
+ from ..ast_nodes import FuncCall, Identifier, MemberAccess
25
+ from ..symbols import TypeSpec
26
+
27
+
28
+ class CollectionHistoryEmitter:
29
+ """CodeGen mixin: the history members of array and matrix variables."""
30
+
31
+ def _collection_history_variables(self) -> list:
32
+ """The declarations whose history the script reads, in key order."""
33
+ history = getattr(self.ctx, "collection_history", None) or {}
34
+ return [history[key] for key in sorted(history)]
35
+
36
+ def _collection_history_member(self, variable) -> str:
37
+ """``_pf_collection_hist_<name>``; a later declaration of the same
38
+ name (a sibling block's) gets ``_pf_collection_hist_<ordinal>_<name>``,
39
+ which no Pine name (none starts with a digit) can spell: ``x``'s
40
+ second declaration beside a variable ``x_1``."""
41
+ prefix = f"{variable.ordinal}_" if variable.ordinal else ""
42
+ return f"_pf_collection_hist_{prefix}{self._safe_name(variable.name)}"
43
+
44
+ def _collection_history_of(self, annotation):
45
+ history = getattr(self.ctx, "collection_history", None) or {}
46
+ return history.get(annotation.get("member"))
47
+
48
+ def _collection_history_cpp_type(self, variable) -> str:
49
+ """The history's element type: the variable member's own C++ type,
50
+ whatever its declaration spells (a wide int array included)."""
51
+ return f"decltype({self._safe_name(variable.name)})"
52
+
53
+ def _collection_history_capacity(self, variable) -> str:
54
+ """The ring's size: the largest literal offset read plus one, or the
55
+ Series default (a ``max_bars_back`` cap) for a dynamic offset."""
56
+ if variable.capacity is None:
57
+ return self._series_decl_suffix()
58
+ return f"{{{max(2, variable.capacity)}}}"
59
+
60
+ def _emit_collection_history_helper(self, lines: list[str]) -> None:
61
+ variables = self._collection_history_variables()
62
+ if not variables:
63
+ return
64
+ float_spec = TypeSpec.primitive("float")
65
+ matrices = [v.spec for v in variables if v.kind == "matrix"]
66
+ lines.append(COLLECTION_HISTORY_CPP.strip("\n"))
67
+ if any(spec.element == float_spec for spec in matrices):
68
+ lines.append(COLLECTION_HISTORY_MATRIX_CPP.strip("\n"))
69
+ if any(spec.element != float_spec for spec in matrices):
70
+ lines.append(COLLECTION_HISTORY_GENERIC_MATRIX_CPP.strip("\n"))
71
+ lines.append(COLLECTION_HISTORY_CLASS_CPP.strip("\n"))
72
+ lines.append("")
73
+
74
+ def _collection_history_open(self, variable) -> str:
75
+ """The statement opening a declaration's slot."""
76
+ return (f"{self._collection_history_member(variable)}.open("
77
+ f"{self._safe_name(variable.name)}, history_advances_new_bar());")
78
+
79
+ def _collection_history_close(self, variable) -> str:
80
+ return (f"{self._collection_history_member(variable)}.close("
81
+ f"{self._safe_name(variable.name)});")
82
+
83
+ def _collection_history_decl_statements(self, node) -> list[str]:
84
+ """What an execution of a declaration does first: close the history
85
+ of every other declaration writing the same member (a sibling
86
+ block's ``x``, which keeps the array its block left), then open its
87
+ own slot when it is not a ``var`` (a block's on the bars it runs)."""
88
+ statements = []
89
+ own = None
90
+ for variable in self._collection_history_variables():
91
+ if id(node) in variable.closed_by:
92
+ statements.append(self._collection_history_close(variable))
93
+ elif variable.decl_node_id == id(node):
94
+ own = variable
95
+ if own is not None and not (getattr(node, "is_var", False)
96
+ or getattr(node, "is_varip", False)):
97
+ statements.append(self._collection_history_open(own))
98
+ return statements
99
+
100
+ def _emit_collection_history_var_opens(self, lines: list[str], pad: str) -> None:
101
+ """A top-level ``var``'s slot opens on every bar, after its first
102
+ bar's initialization."""
103
+ for variable in self._collection_history_variables():
104
+ if variable.is_var:
105
+ lines.append(f"{pad}{self._collection_history_open(variable)}")
106
+
107
+ def _emit_collection_history_closes(self, lines: list[str], pad: str) -> None:
108
+ """The bar's end: every open slot keeps a copy of its variable."""
109
+ for variable in self._collection_history_variables():
110
+ lines.append(f"{pad}{self._collection_history_close(variable)}")
111
+
112
+ @staticmethod
113
+ def _roots_at_collection_history(value) -> bool:
114
+ """Whether ``value`` is an array's or a matrix's history or a
115
+ built-in's call on one (``m[1]``, ``(m[1]).copy()``,
116
+ ``matrix.transpose(m[1])``)."""
117
+ if history_annotation(value) is not None:
118
+ return True
119
+ if not isinstance(value, FuncCall):
120
+ return False
121
+ callee = value.callee
122
+ if isinstance(callee, MemberAccess) and history_annotation(callee.object) is not None:
123
+ return True
124
+ first = value.args[0] if value.args else None
125
+ return (isinstance(callee, MemberAccess)
126
+ and isinstance(callee.object, Identifier)
127
+ and callee.object.name in ("array", "matrix")
128
+ and first is not None and history_annotation(first) is not None)
129
+
130
+ def _collection_history_offset_cpp(self, node) -> str:
131
+ offset = literal_offset(node)
132
+ if offset is not None:
133
+ return str(offset)
134
+ return self._history_offset_cpp(self._visit_expr(node.index), node.index)
135
+
136
+ def _lower_collection_history(self, node) -> str | None:
137
+ """The C++ of an annotated array or matrix history read, by its use:
138
+ a ``const`` copy a built-in reads (``at``), a copy for a variable or a
139
+ parameter (``value``), the receiver of a built-in that changes it
140
+ (``changed``: the run stops past a zero offset), the variable itself
141
+ for a for...in loop (TradingView iterates the current array) and at a
142
+ zero offset. None for any other read."""
143
+ annotation = history_annotation(node)
144
+ if annotation is None:
145
+ return None
146
+ current = self._visit_expr(node.object)
147
+ use = annotation["use"]
148
+ offset = literal_offset(node)
149
+ if use == "loop" or offset == 0:
150
+ return current
151
+ member = self._collection_history_member(self._collection_history_of(annotation))
152
+ index = self._collection_history_offset_cpp(node)
153
+ dynamic = offset is None
154
+ if use == "change":
155
+ return f"{member}.changed({index}, {current})"
156
+ if use == "copy":
157
+ return (f"{member}.value({index}, {current})" if dynamic
158
+ else f"{member}.value({index})")
159
+ return (f"{member}.at({index}, {current})" if dynamic
160
+ else f"{member}.at({index})")
161
+
162
+ def _collection_history_na(self, node) -> str | None:
163
+ """``na(a[k])``: whether the history holds no copy there (an array
164
+ variable itself is never na in PineForge; a matrix may be)."""
165
+ annotation = history_annotation(node)
166
+ if annotation is None or annotation["use"] != "na":
167
+ return None
168
+ current = self._visit_expr(node.object)
169
+ offset = literal_offset(node)
170
+ if offset == 0:
171
+ return f"is_na({current})" if annotation["kind"] == "matrix" else "false"
172
+ member = self._collection_history_member(self._collection_history_of(annotation))
173
+ index = self._collection_history_offset_cpp(node)
174
+ if offset is None:
175
+ return f"{member}.is_na({index}, {current})"
176
+ return f"{member}.is_na({index})"
177
+
178
+ def _matrix_history_method_expr(self, receiver, method: str, spec, node) -> str:
179
+ """A matrix method called on a matrix's history, lowered as the
180
+ variable's own method call is (visit_call: a matrix variable's
181
+ method)."""
182
+ from .tables import MATRIX_METHOD_KWARGS, MATRIX_METHODS, _merge_kwargs
183
+
184
+ recv = self._visit_expr(receiver)
185
+ 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
+ )
191
+ args = self._matrix_bool_value_args(
192
+ method, [self._visit_expr(a) for a in raw_args], raw_args, spec)
193
+ try:
194
+ return MATRIX_METHODS[method](recv, args)
195
+ except IndexError:
196
+ self._codegen_error(
197
+ node,
198
+ f"matrix.{method}: wrong number of arguments",
199
+ hint="Check Pine v6 matrix method signature (positional vs keyword).",
200
+ )
@@ -130,6 +130,8 @@ class TopLevelEmitter:
130
130
  )
131
131
  lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
132
132
  lines.append('#include <pineforge/ta.hpp>')
133
+ lines.extend(['#if __has_include(<pineforge/checked_settings.hpp>)',
134
+ '#include <pineforge/checked_settings.hpp>', '#endif'])
133
135
  if self._ta_uses_dynamic_lengths():
134
136
  lines.append('#include <pineforge/source/pine_ta_length.hpp>')
135
137
  lines.append('#include <pineforge/math.hpp>')
@@ -598,6 +600,7 @@ class TopLevelEmitter:
598
600
  '#error "Generated lifecycle reset requires a matching PineForge engine; rebuild with script-run preparation support"',
599
601
  "#endif",
600
602
  " void prepare_script_run(const Bar* bars, int n, bool allow_precalculation) override {",
603
+ " _pf_require_settings_ok();",
601
604
  " _pf_script_state_checkpoint_.reset();",
602
605
  ])
603
606
  seen: set[str] = set()
@@ -1277,6 +1280,8 @@ class TopLevelEmitter:
1277
1280
  lines.append(" }")
1278
1281
  lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
1279
1282
  lines.append(" }")
1283
+ from .checked_settings import emit_settings_members
1284
+ emit_settings_members(self, lines, ctor_body)
1280
1285
 
1281
1286
  if self._uses_recorded_requests():
1282
1287
  lines.extend([
@@ -1721,7 +1726,12 @@ class TopLevelEmitter:
1721
1726
  name,
1722
1727
  target_cpp_type=udt_name,
1723
1728
  )
1724
- lines.append(f" {safe} = {cpp_val};")
1729
+ if self._binding_is_series(name, safe):
1730
+ # A history-read var object: its first
1731
+ # bar's slot holds the reference.
1732
+ lines.append(f" {safe}.push({cpp_val});")
1733
+ else:
1734
+ lines.append(f" {safe} = {cpp_val};")
1725
1735
  break
1726
1736
  continue
1727
1737
  if self._binding_is_series(name, safe):
@@ -1797,6 +1807,10 @@ class TopLevelEmitter:
1797
1807
  # c. Push non-var series (they start fresh each bar with a push)
1798
1808
  # (actual push happens in visit_VarDecl when the decl is visited)
1799
1809
 
1810
+ # c1. A top-level var array's or matrix's history slot opens on every
1811
+ # bar (pineforge_codegen/collection_history.py).
1812
+ self._emit_collection_history_var_opens(lines, " ")
1813
+
1800
1814
  # c2. First-bar TA resize: rebuild any TA object whose ctor args come
1801
1815
  # from input-backed variables so strategy_set_input() actually changes
1802
1816
  # the circular-buffer sizes. Emits nothing when no TA site depends on
@@ -1815,6 +1829,10 @@ class TopLevelEmitter:
1815
1829
  finally:
1816
1830
  self._clear_lazy_edge_ta_hoists()
1817
1831
 
1832
+ # d2. The bar's end: each array or matrix variable whose history the
1833
+ # script reads keeps a copy of the value it ends the bar with.
1834
+ self._emit_collection_history_closes(lines, " ")
1835
+
1818
1836
  # e. ``// @pf-trace`` pragma block — emitted last so trace values
1819
1837
  # reflect every assignment / strategy call made earlier in the
1820
1838
  # bar. The block is wrapped in ``if (trace_enabled_)`` so cost
@@ -1876,24 +1894,38 @@ class TopLevelEmitter:
1876
1894
  def _emit_extern_c(self, lines: list[str]) -> None:
1877
1895
  lines.append('extern "C" {')
1878
1896
  lines.append(" void* strategy_create(const char* params_json) {")
1879
- lines.append(" return new GeneratedStrategy();")
1897
+ lines.append(" try { return new GeneratedStrategy(); } catch (...) { return nullptr; }")
1880
1898
  lines.append(" }")
1881
1899
  lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
1900
+ lines.append(" try {")
1882
1901
  lines.append(" auto* strat = static_cast<GeneratedStrategy*>(s);")
1902
+ lines.append("#ifndef PF_SETTINGS_API_VERSION")
1903
+ lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
1904
+ lines.append("#endif")
1883
1905
  if self._security_calls:
1884
1906
  # If there are security calls, use the full run path. Pass empty strings
1885
1907
  # so the C++ runtime auto-detects input_tf from bar timestamps.
1886
1908
  lines.append(' strat->run(bars, n, "", "", false, 4, MagnifierDistribution::ENDPOINTS);')
1887
1909
  else:
1888
1910
  lines.append(" strat->run(bars, n);")
1889
- lines.append(" strat->fill_report(out);")
1911
+ lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
1912
+ lines.append(' } catch (const std::exception& _pf_error) {')
1913
+ lines.append(' if (out) *out = ReportC{};')
1914
+ lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", _pf_error.what());')
1915
+ lines.append(' } catch (...) {')
1916
+ lines.append(' if (out) *out = ReportC{};')
1917
+ lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", "unknown C++ exception");')
1918
+ lines.append(' }')
1890
1919
  lines.append(" }")
1891
- lines.append(" void run_backtest_full(void* s, Bar* bars, int n,")
1920
+ lines.append(" static void _pf_run_backtest_full_impl(void* s, Bar* bars, int n,")
1892
1921
  lines.append(' const char* input_tf, const char* script_tf,')
1893
1922
  lines.append(" int bar_magnifier, int magnifier_samples,")
1894
1923
  lines.append(" int magnifier_dist,")
1895
1924
  lines.append(" ReportC* out) {")
1896
1925
  lines.append(' auto* strat = static_cast<GeneratedStrategy*>(s);')
1926
+ lines.append("#ifndef PF_SETTINGS_API_VERSION")
1927
+ lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
1928
+ lines.append("#endif")
1897
1929
  lines.append(' std::string itf = input_tf ? input_tf : "";')
1898
1930
  lines.append(' std::string stf = script_tf ? script_tf : "";')
1899
1931
  if self._security_calls:
@@ -1930,26 +1962,42 @@ class TopLevelEmitter:
1930
1962
  lines.append(" strat->run(bars, n, itf, stf, bar_magnifier != 0, magnifier_samples,")
1931
1963
  lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
1932
1964
  lines.append(" }")
1933
- lines.append(" strat->fill_report(out);")
1965
+ lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
1934
1966
  lines.append(" }")
1967
+ lines.extend([
1968
+ " void run_backtest_full(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf,",
1969
+ " int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out) {",
1970
+ " 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()); }',
1972
+ ' catch (...) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", "unknown C++ exception"); }',
1973
+ " }",
1974
+ ])
1935
1975
  lines.append(" void strategy_free(void* s) {")
1936
- lines.append(" delete static_cast<GeneratedStrategy*>(s);")
1976
+ lines.append(" try { delete static_cast<GeneratedStrategy*>(s); } catch (...) {}")
1937
1977
  lines.append(" }")
1938
1978
  lines.append(" void report_free(ReportC* report) {")
1939
- lines.append(" BacktestEngine::free_report(report);")
1979
+ lines.append(" try { BacktestEngine::free_report(report); } catch (...) {}")
1940
1980
  lines.append(" }")
1941
1981
  lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
1942
1982
  lines.append(" if (!s || !key || !value) return;")
1943
- lines.append(" static_cast<GeneratedStrategy*>(s)->set_input(key, value);")
1983
+ 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"); }')
1944
1986
  lines.append(" }")
1945
1987
  lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
1946
1988
  lines.append(" if (!s || !key || !value) return;")
1947
- lines.append(" static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value);")
1989
+ 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"); }')
1948
1992
  lines.append(" }")
1949
1993
  lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
1950
1994
  lines.append(" if (!s) return;")
1951
- lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
1995
+ 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"); }')
1952
1998
  lines.append(" }")
1999
+ from .checked_settings import emit_settings_exports
2000
+ emit_settings_exports(lines)
1953
2001
  if self._declares_bar_magnifier():
1954
2002
  # TradingView runs a script that declares use_bar_magnifier = true
1955
2003
  # on its bar magnifier; the host reads this export to run it on
@@ -2103,11 +2151,16 @@ class TopLevelEmitter:
2103
2151
 
2104
2152
  if (
2105
2153
  receiver_spec is not None
2106
- and receiver_spec.kind == "primitive"
2154
+ and (receiver_spec.kind == "primitive"
2155
+ or (receiver_spec.kind == "udt"
2156
+ and (receiver_spec.name in DRAWING_TYPE_TO_CPP
2157
+ or receiver_spec.name in self._udt_defs)))
2107
2158
  and p in func_sv
2108
2159
  ):
2109
2160
  # A primitive receiver used with history is a Series boundary,
2110
- # just like an ordinary history-bearing UDF parameter.
2161
+ # just like an ordinary history-bearing UDF parameter; so is
2162
+ # an object or drawing receiver (``this[1]`` is the receiver of
2163
+ # the previous call: fixtures/udt_history_tv udth_method).
2111
2164
  elem_cpp_t = self._series_param_element_cpp_type(
2112
2165
  fi, i, call_site_idx
2113
2166
  )
@@ -2118,6 +2171,9 @@ class TopLevelEmitter:
2118
2171
  self._current_func_series_param_types[
2119
2172
  self._safe_name(p)
2120
2173
  ] = elem_cpp_t
2174
+ if receiver_spec.kind == "udt":
2175
+ self._udt_param_udt[self._safe_name(p)] = receiver_spec.name
2176
+ self._udt_param_udt[p] = receiver_spec.name
2121
2177
  elif is_method and i == 0 and fi.udt_type_name:
2122
2178
  # Receiver pass modes follow Pine's value/ID families. A user
2123
2179
  # UDT is itself a numeric object-ID handle, so pass it by value:
@@ -2352,10 +2408,16 @@ class TopLevelEmitter:
2352
2408
  ),
2353
2409
  )
2354
2410
  if (drawing_name in DRAWING_TYPE_TO_CPP
2355
- and self._current_func_param_types.get(
2411
+ and (self._current_func_param_types.get(
2356
2412
  param, ""
2357
2413
  ).removesuffix("&").removesuffix("*")
2358
- == DRAWING_TYPE_TO_CPP[drawing_name])
2414
+ == DRAWING_TYPE_TO_CPP[drawing_name]
2415
+ # A history-read drawing parameter or receiver is a
2416
+ # ``const Series<Box>&``: ``(x[1]).get_top()`` dispatches on
2417
+ # the handle it holds (udth_drawparam).
2418
+ or (param in self._current_func_series_params
2419
+ and self._current_func_series_param_types.get(param)
2420
+ == DRAWING_TYPE_TO_CPP[drawing_name])))
2359
2421
  }
2360
2422
  self._lexical_udt_types = {
2361
2423
  param: (
@@ -314,7 +314,13 @@ CPP_EMITTER_NAMES = frozenset("""
314
314
  _pf_drawing_na _pf_collect_drawings _pf_collect_lines_ _pf_collect_boxes_
315
315
  _pf_collect_labels_ _pf_new _pf_held
316
316
  get_input_int get_input_float get_input_bool get_input_string
317
- trace is_na na nz fixnan
317
+ _pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
318
+ _pf_record_setting_failure _pf_require_settings_ok
319
+ _pf_setting_failed_ _pf_setting_failure_
320
+ _pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
321
+ _pf_set_override_checked _pf_settings_receipt
322
+ _pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
323
+ checked_settings trace is_na na nz fixnan
318
324
  """.split())
319
325
 
320
326
  # The generated history members, ``_<kind>_<n>`` (codegen/base.py
@@ -345,7 +351,10 @@ CPP_RESERVED = set(
345
351
  # ``_nz_v``) or initialized itself from itself (``_pna_l > close`` did not
346
352
  # compile). ``_safe_name`` escapes an authored spelling of any of them. A
347
353
  # temporary whose name the emitter allocates against the authored spellings
348
- # itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed.
354
+ # itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed. An
355
+ # array's or a matrix's history member (``_pf_collection_hist_<name>``,
356
+ # ``_pf_collection_hist_<n>_<name>``) is reserved the same way: a script
357
+ # variable of that spelling was a second member of one name.
349
358
  CPP_TEMPORARY_NAMES = frozenset("""
350
359
  _nz_v _nz_y _fixnan_v _pna_l _pna_r _pfc_l _pfc_r _pfc_eq _hv _hidx _sv
351
360
  _out _func_ret _pf_v _pf_idx_v _pf_idx_t _pf_bool_v _pf_bool_t
@@ -364,6 +373,7 @@ _CPP_TEMPORARY_PATTERN = re.compile(
364
373
  r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
365
374
  r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
366
375
  r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
376
+ r"|_pf_collection_hist_\w+"
367
377
  )
368
378
 
369
379
 
@@ -8,6 +8,7 @@ strategy class, so ``_safe_name`` renames it.
8
8
  """
9
9
 
10
10
  HOST_MEMBER_NAMES = frozenset({
11
+ "StreamPhase",
11
12
  "_src_close_",
12
13
  "_src_high_",
13
14
  "_src_hl2_",
@@ -50,6 +51,7 @@ HOST_MEMBER_NAMES = frozenset({
50
51
  "closed_trade_profit_percent",
51
52
  "closed_trade_size",
52
53
  "commit_script_state",
54
+ "config_",
53
55
  "configure_pine_strategy",
54
56
  "configure_security_evaluators",
55
57
  "count_losstrades",
@@ -74,9 +76,12 @@ HOST_MEMBER_NAMES = frozenset({
74
76
  "history_advances_new_bar",
75
77
  "initial_capital_",
76
78
  "input_tf_",
79
+ "inputs_",
77
80
  "is_first_tick",
78
81
  "is_last_tick_",
79
82
  "last_bar_time_",
83
+ "last_error",
84
+ "last_error_",
80
85
  "main_period",
81
86
  "margin_liquidation_price",
82
87
  "max_contracts_held_all",
@@ -104,6 +109,7 @@ HOST_MEMBER_NAMES = frozenset({
104
109
  "open_trade_profit_percent",
105
110
  "open_trade_size",
106
111
  "open_trades_capital_held",
112
+ "override_",
107
113
  "pine_bar_index",
108
114
  "pine_last_bar_index",
109
115
  "pine_session_ismarket",
@@ -123,6 +129,7 @@ HOST_MEMBER_NAMES = frozenset({
123
129
  "restore_script_state",
124
130
  "round_to_mintick",
125
131
  "run",
132
+ "script_bars_processed",
126
133
  "script_tf_",
127
134
  "security_column_value",
128
135
  "security_eval_states_",
@@ -151,6 +158,7 @@ HOST_MEMBER_NAMES = frozenset({
151
158
  "strategy_exit",
152
159
  "strategy_exit_cancel_bracket",
153
160
  "strategy_order",
161
+ "stream_phase_",
154
162
  "symbol_facts_",
155
163
  "symbol_feeds_",
156
164
  "syminfo_",
@@ -85,14 +85,43 @@ class InputHelper:
85
85
  while merged and merged[-1] is None:
86
86
  merged.pop()
87
87
  if merged and merged[0] is not None:
88
- return merged[0]
88
+ return self._fold_enum_input_default(node, merged[0])
89
89
  return None
90
90
  if node.args:
91
- return node.args[0]
91
+ return self._fold_enum_input_default(node, node.args[0])
92
92
  if "defval" in node.kwargs:
93
- return node.kwargs["defval"]
93
+ return self._fold_enum_input_default(node, node.kwargs["defval"])
94
94
  return None
95
95
 
96
+ def _fold_enum_input_default(self, node: FuncCall, default):
97
+ """Resolve an enum defval's immutable, source-ordered initializer."""
98
+ func_name, namespace = self._resolve_callee(node.callee)
99
+ if namespace != "input" or func_name != "enum" or not isinstance(default, Identifier):
100
+ return default
101
+ declarations = {}
102
+ reassigned = self._find_reassigned_vars()
103
+ for statement in self.ctx.ast.body:
104
+ if any(candidate is node for candidate in self._walk_ast(statement)):
105
+ break
106
+ if isinstance(statement, VarDecl):
107
+ declarations.setdefault(statement.name, []).append(statement)
108
+ candidate = default
109
+ visited = set()
110
+ while isinstance(candidate, Identifier) and candidate.name not in visited:
111
+ visited.add(candidate.name)
112
+ bindings = declarations.get(candidate.name, [])
113
+ if len(bindings) != 1 or candidate.name in reassigned:
114
+ return default
115
+ declaration = bindings[0]
116
+ if declaration.is_var or declaration.is_varip:
117
+ return default
118
+ candidate = declaration.value
119
+ if isinstance(candidate, MemberAccess) and isinstance(candidate.object, Identifier):
120
+ members = self._enum_defs.get(candidate.object.name, [])
121
+ if candidate.member in members:
122
+ return candidate
123
+ return default
124
+
96
125
  def _input_default_value(self, node: FuncCall) -> tuple[bool, object]:
97
126
  """``(True, value)`` when the defval is the constant a ``v = input...``
98
127
  binding records as ``v``'s known value -- a number, bool or string
@@ -527,6 +556,10 @@ class InputHelper:
527
556
  "type": form_type,
528
557
  "default": default_val,
529
558
  }
559
+ if namespace == "input" and func_name == "symbol":
560
+ # A symbol string, which the type does not tell from a string's:
561
+ # the requests of another symbol key their feeds on its value.
562
+ entry["kind"] = "symbol"
530
563
  # Pull min/max/step/options by signature param name; emit only
531
564
  # const literals so the override form never references a runtime
532
565
  # value it can't reproduce.
@@ -88,7 +88,7 @@ from .helpers import na_preserving_int_cast, unary_sign_cpp
88
88
  from ..security_contexts import GLOBAL_ANNOTATION, UNREACHED_ANNOTATION
89
89
  from ..symbols import PineType, method_receiver_type_name
90
90
  from .tables import (
91
- BAR_BUILTINS, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
91
+ BAR_BUILTINS, DRAWING_TYPE_TO_CPP, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
92
92
  SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, TA_TUPLE_FIELDS,
93
93
  _math_minmax_na_expr, _math_round_digits_expr, _merge_kwargs,
94
94
  )
@@ -6127,11 +6127,66 @@ class SecurityEmitter:
6127
6127
  lines.append(" }")
6128
6128
  lines.append("")
6129
6129
 
6130
+ def _refuse_security_reference_history(self, item: dict) -> None:
6131
+ """A request.security expression that reads the history of an
6132
+ object or drawing reference, itself or in a function or method it
6133
+ calls. TradingView evaluates it on the requested bars, where the
6134
+ variable holds the objects of that timeframe's bars; PineForge keeps
6135
+ such a history on the chart's bars only, and the payload read the
6136
+ chart's (it never compiled before: the history was a
6137
+ ``Series<double>``). Refused."""
6138
+ if item["expr_node"] is None:
6139
+ # ``request.security(sym, tf)`` with no expression: lowered to na
6140
+ # as it always was (TradingView refuses it, CE10165).
6141
+ return
6142
+ pending = [item["expr_node"]]
6143
+ seen: set[str] = set()
6144
+ while pending:
6145
+ for node, _depth in iter_ast_nodes(pending.pop()):
6146
+ if isinstance(node, Subscript):
6147
+ receiver = node.object
6148
+ kind = (self._series_handle_type_name(receiver.name)
6149
+ if isinstance(receiver, Identifier) else None)
6150
+ if kind is None:
6151
+ spec = self._type_spec_from_expr(receiver)
6152
+ if (spec is not None and spec.kind == "udt"
6153
+ and (spec.name in DRAWING_TYPE_TO_CPP
6154
+ or spec.name in self._udt_defs)):
6155
+ kind = spec.name
6156
+ if kind is not None:
6157
+ self._codegen_error(
6158
+ node,
6159
+ f"History of a {kind} reference inside a "
6160
+ "request.security expression is not supported in "
6161
+ "PineForge: TradingView reads the references the "
6162
+ "variable held on the requested timeframe's bars, "
6163
+ "which PineForge does not keep.",
6164
+ hint="Request the field values the expression "
6165
+ "needs (request.security(..., obj.field)) "
6166
+ "and read their history on the chart.",
6167
+ )
6168
+ if not isinstance(node, FuncCall):
6169
+ continue
6170
+ func_name, namespace = self._resolve_callee(node.callee)
6171
+ keys = []
6172
+ if namespace is None and func_name in self._func_info_map:
6173
+ keys.append(func_name)
6174
+ if isinstance(node.callee, MemberAccess):
6175
+ keys.extend(key for key in self._func_info_map
6176
+ if key.endswith(f".{node.callee.member}"))
6177
+ for key in keys:
6178
+ body = getattr(self._func_info_map[key], "node", None)
6179
+ if key not in seen and body is not None:
6180
+ seen.add(key)
6181
+ pending.append(body)
6182
+
6130
6183
  def _emit_security_evaluators(self, lines: list[str]) -> None:
6131
6184
  """Emit _eval_security_N() methods and evaluate_security() dispatch."""
6132
6185
  if not self._security_calls:
6133
6186
  return
6134
6187
 
6188
+ for item in self._security_calls:
6189
+ self._refuse_security_reference_history(item)
6135
6190
  for item in self._security_calls:
6136
6191
  self._emit_security_evaluator_requested(item, lines)
6137
6192