@pineforge/codegen-pyodide 0.10.4 → 1.0.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 (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -4,8 +4,8 @@ Mixin holding stateless / near-stateless name-mangling and AST-walk
4
4
  helpers used everywhere in the codegen. Lives here so the heavier
5
5
  visitor / emitter mixins can depend on it without owning its
6
6
  implementation. Keep this module free of imports from any other
7
- ``codegen/*`` submodule so it stays at the bottom of the dependency
8
- graph.
7
+ ``codegen/*`` submodule (the generated data module ``host_members``
8
+ aside) so it stays at the bottom of the dependency graph.
9
9
 
10
10
  Mixin contract: ``NamingHelper`` reads at most one piece of host state,
11
11
  ``self._all_member_names`` (used by ``_func_safe_name``). The class
@@ -14,23 +14,363 @@ mixing this in (``CodeGen``) sets that attribute in its constructor.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import re
18
+
17
19
  from ..ast_nodes import (
18
- Identifier, MemberAccess,
20
+ Identifier, MemberAccess, TypeDecl, EnumDecl, FuncDef, MethodDef,
19
21
  VarDecl, Assignment, TupleAssign, ForStmt, ForInStmt,
20
22
  )
23
+ from ..limits import iter_ast_nodes
24
+ from ..signatures import SESSION_FLAG_MEMBERS
25
+ from .host_members import HOST_MEMBER_NAMES
26
+
27
+
28
+ # Integer C++ types an na-capable ``double`` expression may be narrowed into.
29
+ # ``bool`` has its own Pine truthiness lowering below: unlike a C++ cast, it
30
+ # treats both the floating NaN and the integer ``na`` sentinel as false.
31
+ NA_PRESERVING_INT_TYPES = ("int", "int64_t")
32
+ _INT_LITERAL_TEXT = re.compile(
33
+ r"^\s*\(?(?:[-+]?\d+)(?:LL|ULL|L|U)?\)?\s*$"
34
+ )
35
+
36
+
37
+ def na_preserving_int_cast(value_cpp: str, int_cpp_type: str = "int") -> str:
38
+ """Narrow a numeric expression to an integer type without losing ``na``.
39
+
40
+ An *implicit* ``double`` -> ``int`` conversion of a NaN is undefined
41
+ behaviour ([conv.fpint]), and compilers disagree in practice: AppleClang
42
+ arm64 and g++ aarch64 produce 0 at every ``-O``, g++ x86-64 produces
43
+ ``INT_MIN`` at ``-O0``/``-O1`` and 0 from ``-O2``. The engine's contract
44
+ (``include/pineforge/na.hpp``) is that an integer ``na`` *is*
45
+ ``std::numeric_limits<T>::min()``, which is what ``is_na(T)`` tests, so
46
+ every such narrowing must be spelled out. Check the original value type:
47
+ converting an ``int64_t`` na sentinel to ``double`` first erases it.
48
+
49
+ The value is evaluated exactly once. ``na`` in, ``na<T>()`` out; anything
50
+ else truncates toward zero exactly as the implicit conversion did, so a
51
+ non-``na`` result is bit-for-bit what it was before. A floating value
52
+ outside ``int`` reads ``na<int>()`` too: its conversion is undefined, and
53
+ x86-64 (``cvttsd2si``: ``INT_MIN``, the ``na`` sentinel) and arm64
54
+ (``fcvtzs``: saturation) disagreed on an epoch a missed provenance
55
+ narrowed (W9-CG-EPOCH-INT64); ``na<int>()`` is the x86-64 answer, so
56
+ Linux runs keep their bits.
57
+ """
58
+ # Integer literals are proven non-``na``. Keep the historical explicit
59
+ # cast for these tiny paths so a helper does not churn every matrix/color
60
+ # call that passes a literal index or channel.
61
+ if _INT_LITERAL_TEXT.fullmatch(value_cpp):
62
+ return f"({int_cpp_type})({value_cpp})"
63
+ if int_cpp_type == "int":
64
+ return (f"[&](){{ auto _pf_v = ({value_cpp}); "
65
+ f"if constexpr (std::is_floating_point_v<decltype(_pf_v)>) "
66
+ f"return (_pf_v >= -2147483648.0 && _pf_v < 2147483648.0) "
67
+ f"? (int)_pf_v : na<int>(); "
68
+ f"else return is_na(_pf_v) ? na<int>() : (int)_pf_v; }}()")
69
+ return (f"[&](){{ auto _pf_v = ({value_cpp}); "
70
+ f"return is_na(_pf_v) ? na<{int_cpp_type}>() : "
71
+ f"({int_cpp_type})_pf_v; }}()")
72
+
73
+
74
+ # C++ whose evaluation cannot be observed, so reading it twice -- or once per
75
+ # loop iteration -- is harmless: a literal, a name or member chain (a history
76
+ # read of it at a literal offset included), an na.
77
+ _PLAIN_READ_CPP = re.compile(
78
+ r"[A-Za-z_]\w*(?:(?:\.|->)[A-Za-z_]\w*)*(?:\[\d+\])?"
79
+ r"|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"
80
+ r'|std::string\("(?:[^"\\]|\\.)*"\)'
81
+ r"|na<[\w:]+>\(\)"
82
+ )
83
+
84
+
85
+ def _strip_outer_parens(text: str) -> str:
86
+ text = text.strip()
87
+ while text.startswith("(") and text.endswith(")"):
88
+ depth = 0
89
+ for i, ch in enumerate(text):
90
+ depth += ch == "("
91
+ depth -= ch == ")"
92
+ if depth == 0 and i < len(text) - 1:
93
+ return text # the first "(" closes before the end: "(a) + (b)"
94
+ text = text[1:-1].strip()
95
+ return text
96
+
97
+
98
+ def cpp_is_plain_read(value_cpp: str) -> bool:
99
+ """True when evaluating ``value_cpp`` more than once cannot be observed."""
100
+ return _PLAIN_READ_CPP.fullmatch(_strip_outer_parens(value_cpp)) is not None
101
+
102
+
103
+ def evaluate_args_once(args: list[str], indices, render, token: str) -> str:
104
+ """``render(args)`` with every argument evaluated once, left to right.
105
+
106
+ Pine evaluates each call argument exactly once. A lowering template that
107
+ reads an argument twice, or once per loop iteration, re-runs that
108
+ argument's C++, so a stateful call in it (a ``ta.*`` compute(), a user
109
+ function with state) runs more than once per execution. Every argument
110
+ in ``indices`` that is not a plain read is bound once to a lambda local,
111
+ together with every earlier argument that is not, so the left-to-right
112
+ order holds; the template then reads the bindings. A call whose
113
+ ``indices`` arguments are all plain reads renders exactly as before.
114
+ """
115
+ need = [i for i in indices if i < len(args) and not cpp_is_plain_read(args[i])]
116
+ if not need:
117
+ return render(list(args))
118
+ bound = list(args)
119
+ decls = []
120
+ for i in range(max(need) + 1):
121
+ if cpp_is_plain_read(args[i]):
122
+ continue
123
+ decls.append(f"auto&& {token}{i} = ({args[i]});")
124
+ bound[i] = f"{token}{i}"
125
+ return f"([&]() {{ {' '.join(decls)} return {render(bound)}; }}())"
126
+
127
+
128
+ def pine_index_int_cast(value_cpp: str) -> str:
129
+ """Narrow a numeric index after checking its *original* na sentinel.
130
+
131
+ Table-driven matrix methods receive C++ text without the source AST.
132
+ Converting an integer sentinel to ``double`` first loses it (INT64_MIN is
133
+ finite as a double), so this helper retains the source C++ type until the
134
+ `is_na` check. Bool has no separate na state in Pine v6.
135
+ """
136
+ if _INT_LITERAL_TEXT.fullmatch(value_cpp):
137
+ return f"(int)({value_cpp})"
138
+ return (
139
+ f"([&](){{ auto _pf_idx_v = ({value_cpp}); "
140
+ f"using _pf_idx_t = std::decay_t<decltype(_pf_idx_v)>; "
141
+ f"if constexpr (std::is_same_v<_pf_idx_t, bool>) "
142
+ f"return (int)_pf_idx_v; "
143
+ f"else return is_na(_pf_idx_v) ? na<int>() : (int)_pf_idx_v; }}())"
144
+ )
145
+
146
+
147
+ def pine_truth_cast(value_cpp: str) -> str:
148
+ """Convert one scalar expression using Pine's boolean truthiness.
149
+
150
+ Pine's boolean context is false for ``na``. C++ differs for both numeric
151
+ sentinels: ``bool(NaN)`` is true and ``bool(INT_MIN)`` is true. Keep the
152
+ expression single-evaluation and use ``if constexpr`` so the generated
153
+ lambda is valid for either a real ``bool`` or a numeric scalar. The final
154
+ branch is intentionally a normal C++ conversion for a type that cannot
155
+ carry Pine ``na`` (for example an engine handle); callers only use this
156
+ helper at scalar boolean boundaries.
157
+ """
158
+ return (
159
+ f"[&](){{ auto _pf_bool_v = ({value_cpp}); "
160
+ f"using _pf_bool_t = std::decay_t<decltype(_pf_bool_v)>; "
161
+ f"if constexpr (std::is_same_v<_pf_bool_t, bool>) {{ "
162
+ f"return _pf_bool_v; }} "
163
+ f"else if constexpr (std::is_floating_point_v<_pf_bool_t> || "
164
+ f"std::is_integral_v<_pf_bool_t>) {{ "
165
+ f"return is_na(_pf_bool_v) ? false : (_pf_bool_v != 0); }} "
166
+ f"else {{ return static_cast<bool>(_pf_bool_v); }} }}()"
167
+ )
168
+
169
+
170
+ def unary_sign_cpp(op: str, operand_cpp: str) -> str:
171
+ """``(-x)`` / ``(+x)``, the operand parenthesized when its text starts
172
+ with a sign: a negative constant inlined as its literal (``NEG = -5``)
173
+ under ``-`` was ``(--5)``, a decrement of a literal, which does not
174
+ compile. Every other operand keeps its spelling."""
175
+ if operand_cpp[:1] in ("-", "+"):
176
+ return f"({op}({operand_cpp}))"
177
+ return f"({op}{operand_cpp})"
21
178
 
22
179
 
23
- # C++ reserved names that conflict with PineScript identifiers when used
24
- # verbatim as variable names. Carried forward from the historic codegen.py
25
- # table; intentionally narrower than the full C++ keyword set because Pine
26
- # already reserves keywords like ``if``/``else``/``for``/``return`` so they
27
- # can never reach codegen as identifier strings. Lives here (not base.py)
28
- # so the mixin can read it without forcing an import cycle on ``base``.
29
- CPP_RESERVED = {
180
+ def color_alpha_cast(value_cpp: str, whole: bool = False) -> str:
181
+ """Convert Pine transparency without feeding ``na<int>()`` to color.hpp.
182
+
183
+ color.hpp's ``new_color`` takes the transparency as a double and stores
184
+ the alpha byte nearest 255 * (100 - t) / 100, as TradingView does, so a
185
+ finite value passes through unrounded: ``color.new(c, 10.5)`` reads back
186
+ 11 (tests/test_e2e_color_tapes.py). TradingView's
187
+ ``color.new``/``color.rgb`` treat a missing transparency as 100 (fully
188
+ transparent); the integer ``na`` sentinel would read as a transparency
189
+ far below 0. ``whole`` marks an integer literal from 0 to 100, which keeps
190
+ the historic ``(int)`` conversion: it leaves the value as it is.
191
+ """
192
+ value = "(int)_pf_color_v" if whole else "(double)_pf_color_v"
193
+ return (
194
+ f"[&](){{ auto _pf_color_v = ({value_cpp}); "
195
+ f"using _pf_color_t = std::decay_t<decltype(_pf_color_v)>; "
196
+ f"if constexpr (std::is_same_v<_pf_color_t, bool>) "
197
+ f"return _pf_color_v ? 1 : 0; "
198
+ f"else return is_na(_pf_color_v) ? 100 : {value}; }}()"
199
+ )
200
+
201
+
202
+ # A comment or a string or character literal of the generated C++, which
203
+ # spells no raw string literal and no digit separator.
204
+ _CPP_COMMENT_OR_LITERAL = re.compile(
205
+ r"//[^\n]*|/\*.*?\*/|\"(?:\\.|[^\"\\\n])*\"|'(?:\\.|[^'\\\n])*'", re.S
206
+ )
207
+
208
+
209
+ def cpp_code_only(cpp: str) -> str:
210
+ """``cpp`` with every comment and string or character literal replaced by
211
+ a space: what is left is code, which a script's string cannot spell."""
212
+ return _CPP_COMMENT_OR_LITERAL.sub(" ", cpp)
213
+
214
+
215
+ # Preserve the historic spelling for names already escaped in released TUs.
216
+ LEGACY_CPP_RESERVED = frozenset({
30
217
  "exp", "log", "abs", "max", "min", "and", "or", "not",
31
218
  "int", "float", "bool", "string", "short", "long", "new", "delete",
32
219
  "class", "struct", "return", "void", "auto", "const", "static",
33
- }
220
+ "prepare_script_run",
221
+ })
222
+
223
+ # Every C++17 keyword and alternative operator token, plus the C++20
224
+ # additions. Pine reserves some of these itself, but the others are legal
225
+ # Pine identifiers and must not reach a C++ declaration unchanged.
226
+ CPP_KEYWORDS = frozenset("""
227
+ alignas alignof and and_eq asm auto bitand bitor bool break case catch
228
+ char char8_t char16_t char32_t class co_await co_return co_yield compl
229
+ concept const consteval constexpr constinit const_cast continue decltype
230
+ default delete do double dynamic_cast else enum explicit export extern
231
+ false float for friend goto if inline int long mutable namespace new
232
+ noexcept not not_eq nullptr operator or or_eq private protected public
233
+ register reinterpret_cast requires return short signed sizeof static
234
+ static_assert static_cast struct switch template this thread_local throw
235
+ true try typedef typeid typename union unsigned using virtual void
236
+ volatile wchar_t while xor xor_eq
237
+ """.split())
238
+
239
+ # C++20 contextual spellings can be identifiers in some contexts, but can
240
+ # become special after future emitter changes or on newer compilers.
241
+ CPP_CONTEXTUAL = frozenset({"final", "override", "import", "module"})
242
+
243
+ # Macros visible through the standard and engine headers included by every
244
+ # generated TU. Their expansion in a member declaration is invalid C++.
245
+ CPP_STANDARD_MACROS = frozenset("""
246
+ __LINE__ __FILE__ __DATE__ __TIME__ __TIMESTAMP__ __cplusplus
247
+ __STDC__ __STDC_HOSTED__ __STDC_VERSION__ __STDC_MB_MIGHT_NEQ_WC__
248
+ __STDC_UTF_16__ __STDC_UTF_32__
249
+ NULL EOF NAN INFINITY HUGE_VAL HUGE_VALF HUGE_VALL
250
+ stdin stdout stderr assert offsetof va_start va_end va_arg va_copy
251
+ EXIT_SUCCESS EXIT_FAILURE RAND_MAX MB_CUR_MAX EDOM ERANGE EILSEQ
252
+ CHAR_BIT CHAR_MIN CHAR_MAX SCHAR_MIN SCHAR_MAX UCHAR_MAX
253
+ SHRT_MIN SHRT_MAX USHRT_MAX INT_MIN INT_MAX UINT_MAX
254
+ LONG_MIN LONG_MAX ULONG_MAX LLONG_MIN LLONG_MAX ULLONG_MAX
255
+ INT8_MIN INT8_MAX UINT8_MAX INT16_MIN INT16_MAX UINT16_MAX
256
+ INT32_MIN INT32_MAX UINT32_MAX INT64_MIN INT64_MAX UINT64_MAX
257
+ INTPTR_MIN INTPTR_MAX UINTPTR_MAX PTRDIFF_MIN PTRDIFF_MAX
258
+ SIZE_MAX SIG_ATOMIC_MIN SIG_ATOMIC_MAX WCHAR_MIN WCHAR_MAX
259
+ WINT_MIN WINT_MAX INTMAX_MIN INTMAX_MAX UINTMAX_MAX
260
+ M_E M_PI M_LOG2E M_LOG10E M_LN2 M_LN10 M_PI_2 M_PI_4
261
+ M_1_PI M_2_PI M_2_SQRTPI M_SQRT2 M_SQRT1_2
262
+ PINEFORGE_HAS_NATIVE_LOWERING_V1 PINEFORGE_HAS_NATIVE_LIVE_V1
263
+ PINEFORGE_HAS_AUX_SECURITY_FEED_V1 PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1
264
+ PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1
265
+ PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1
266
+ PINEFORGE_NO_STRATEGY_DECLS PF_PINE_TIME_HAS_SESSION_DAY
267
+ PF_PINE_TIME_SESSION_DAY_ARGS PF_VWAP_HAS_SESSION_ANCHOR
268
+ PF_VWAP_SESSION_ANCHOR_ARGS PF_ALMA_HAS_FLOOR
269
+ PF_KC_HAS_USE_TRUE_RANGE PF_VWAP_HAS_ANCHOR_INPUT
270
+ PF_PIVOT_LEVELS_HAS_ANCHOR
271
+ """.split())
272
+
273
+ # Identifiers used by the emitter as class/type names, namespaces, and
274
+ # generated or inherited methods. A user member with one of these names can
275
+ # hide a type/callee, or (GeneratedStrategy) conflict with the constructor.
276
+ CPP_EMITTER_NAMES = frozenset("""
277
+ GeneratedStrategy BacktestEngine Bar Series PineArray PineMap PineMatrix
278
+ PineGenericMatrix PineStrategyConfig PineStrategyHost StrategyOverrides
279
+ SymInfo ReportC MagnifierDistribution ChartPoint Line Box Label Linefill
280
+ std pineforge ta math
281
+ on_bar on_source_bar prepare_script_run configure_pine_strategy
282
+ configure_security_evaluators snapshot_script_state restore_script_state
283
+ commit_script_state set_strategy_override set_input
284
+ set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
285
+ precalculate
286
+ strategy_entry strategy_close strategy_close_all strategy_exit
287
+ strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order
288
+ pine_bar_index pine_last_bar_index prev_chart_close is_first_tick is_last_tick
289
+ history_advances_new_bar security_series_slot_is_new last_bar_dual_entry_path
290
+ live_position_size pending_order_count market_admission_journal
291
+ pine_time pine_time_close pine_time_tradingday pine_random
292
+ pine_runtime_error pine_enum_str_at pine_session_ismarket
293
+ pine_session_ispostmarket pine_session_ispremarket _PFSessionMarket
294
+ _pf_session_market_ script_tf_
295
+ tf_change tf_is_daily tf_is_intraday tf_is_monthly tf_is_seconds
296
+ tf_is_weekly tf_multiplier tf_to_seconds time_close timestamp main_period
297
+ round_to_mintick calc_qty
298
+ pf_noop
299
+ pf_line_new pf_line_new_pts pf_line_copy pf_line_delete
300
+ pf_line_get_price pf_line_get_x1 pf_line_get_x2 pf_line_get_y1 pf_line_get_y2
301
+ pf_line_set_first_point pf_line_set_second_point pf_line_set_x1 pf_line_set_x2
302
+ pf_line_set_xloc pf_line_set_xy1 pf_line_set_xy2 pf_line_set_y1 pf_line_set_y2
303
+ pf_label_new pf_label_new_pt pf_label_copy pf_label_delete
304
+ pf_label_get_text pf_label_get_x pf_label_get_y pf_label_set_point
305
+ pf_label_set_text pf_label_set_x pf_label_set_xloc pf_label_set_xy
306
+ pf_label_set_y pf_label_set_yloc
307
+ pf_box_new pf_box_new_pts pf_box_copy pf_box_delete
308
+ pf_box_get_bottom pf_box_get_left pf_box_get_right pf_box_get_top
309
+ pf_box_set_bottom pf_box_set_bottom_right_point pf_box_set_left
310
+ pf_box_set_lefttop pf_box_set_right pf_box_set_rightbottom pf_box_set_top
311
+ pf_box_set_top_left_point pf_box_set_xloc
312
+ pf_linefill_new pf_linefill_delete pf_linefill_get_line1 pf_linefill_get_line2
313
+ _PF_DRAWING_UNBOUNDED _PFDrawingArg _pf_drawing_get _pf_drawing_set
314
+ _pf_drawing_na _pf_collect_drawings _pf_collect_lines_ _pf_collect_boxes_
315
+ _pf_collect_labels_ _pf_new _pf_held
316
+ get_input_int get_input_float get_input_bool get_input_string
317
+ trace is_na na nz fixnan
318
+ """.split())
319
+
320
+ # The generated history members, ``_<kind>_<n>`` (codegen/base.py
321
+ # _prepare_inline_history_members), numbered past any script name spelled
322
+ # like one.
323
+ INLINE_HISTORY_KINDS = (
324
+ "hist_call", "series_arg", "udf_series_arg", "session_call", "fn_global_hist",
325
+ )
326
+
327
+
328
+ # HOST_MEMBER_NAMES (codegen/host_members.py, derived by
329
+ # scripts/gen_host_members.py from the emitter and the host header): the host
330
+ # members the generated class reads or writes unqualified. A script identifier
331
+ # with one of these names would hide the member (``session_isfirstbar_ =
332
+ # close > open`` became the value of ``session.isfirstbar``).
333
+ CPP_RESERVED = set(
334
+ LEGACY_CPP_RESERVED | CPP_KEYWORDS | CPP_CONTEXTUAL |
335
+ CPP_STANDARD_MACROS | CPP_EMITTER_NAMES | HOST_MEMBER_NAMES
336
+ )
337
+
338
+ # Temporaries the emitter declares in the C++ it generates around a user's
339
+ # expression: nz's ``[&]{ auto _nz_v = (x); ... }()``, a comparison's
340
+ # ``_pna_l`` / ``_pna_r``, a history read's ``_hv``, a switch's
341
+ # ``__switch_val_N``, timestamp()'s calendar fields, the str.* / array.*
342
+ # templates' locals (``__pf_s``, ``__pf_it``, ...). An authored identifier
343
+ # spelled like one was captured: the generated local shadowed it where the
344
+ # user's expression read it (``nz(x, _nz_v)`` read the lambda's own
345
+ # ``_nz_v``) or initialized itself from itself (``_pna_l > close`` did not
346
+ # compile). ``_safe_name`` escapes an authored spelling of any of them. A
347
+ # temporary whose name the emitter allocates against the authored spellings
348
+ # itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed.
349
+ CPP_TEMPORARY_NAMES = frozenset("""
350
+ _nz_v _nz_y _fixnan_v _pna_l _pna_r _pfc_l _pfc_r _pfc_eq _hv _hidx _sv
351
+ _out _func_ret _pf_v _pf_idx_v _pf_idx_t _pf_bool_v _pf_bool_t
352
+ _pf_color_v _pf_color_t _pf_color_base _pf_color_alpha _pf_sign_v
353
+ _pf_series_raw _pf_raw _pf_val_t _pf_out _pf_prior
354
+ _pf_hist_offset_raw _pf_hist_offset_numeric _pf_hist_offset _pf_hist_bar
355
+ _pf_calendar_ts _pf_calendar_tz
356
+ _tz _yr _mo _dy _hr _min _sc _res _old _old_tz _had_old _secs
357
+ _last_tz _last_yr _last_mo _last_dy _last_hr _last_min _last_sc
358
+ _last_res
359
+ __pf_s __pf_r __pf_p __pf_i __pf_j __pf_t __pf_it __pf_m __pf_c __pf_n
360
+ __pf_k __pf_b __pf_ma __pf_mb __pf_f __pf_d __pf_best __pf_bc __pf_rank
361
+ __pf_idx __pf_occ
362
+ """.split())
363
+ _CPP_TEMPORARY_PATTERN = re.compile(
364
+ r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
365
+ r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
366
+ r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
367
+ )
368
+
369
+
370
+ def is_emitter_temporary(name: str) -> bool:
371
+ """Whether ``name`` is spelled like a temporary of the emitter's."""
372
+ return (name in CPP_TEMPORARY_NAMES
373
+ or _CPP_TEMPORARY_PATTERN.fullmatch(name) is not None)
34
374
 
35
375
 
36
376
  # Bare C++ identifiers that ``strategy.*`` (and a few other) read-only
@@ -55,6 +395,9 @@ BUILTIN_ACCESSOR_NAMES = {
55
395
  "avg_winning_trade_percent", "avg_losing_trade_percent",
56
396
  "margin_liquidation_price", "open_profit", "current_equity",
57
397
  "open_trades_capital_held",
398
+ # The host's own ``time_close()``: a ``time_close[k]`` history Series
399
+ # member of that name would shadow the call its value is pushed from.
400
+ "time_close",
58
401
  }
59
402
 
60
403
 
@@ -77,12 +420,90 @@ class NamingHelper:
77
420
  .replace('"', '\\"')
78
421
  .replace("\n", "\\n")
79
422
  .replace("\r", "\\r")
423
+ .replace("\t", "\\t")
80
424
  )
81
425
 
426
+ def _initialise_safe_names(self, ast) -> None:
427
+ """Reserve all authored spellings before assigning escaped ones.
428
+
429
+ A fixed ``_name_`` escape can itself collide with an authored
430
+ ``_name_``. Pre-allocating against the entire AST makes the mapping
431
+ stable and one-to-one regardless of emission order.
432
+ """
433
+ authored: set[str] = set()
434
+ bound: set[str] = set()
435
+ for node, _depth in iter_ast_nodes(ast):
436
+ for attr in ("name", "var", "member"):
437
+ value = getattr(node, attr, None)
438
+ if isinstance(value, str) and value:
439
+ authored.add(value)
440
+ for attr in ("names", "vars", "params", "members"):
441
+ values = getattr(node, attr, None)
442
+ if isinstance(values, list):
443
+ authored.update(value for value in values
444
+ if isinstance(value, str) and value)
445
+ if isinstance(node, TypeDecl):
446
+ authored.update(field.name for field in node.fields)
447
+ bound.add(node.name)
448
+ bound.update(field.name for field in node.fields)
449
+ elif isinstance(node, EnumDecl):
450
+ bound.add(node.name)
451
+ bound.update(node.members)
452
+ elif isinstance(node, VarDecl):
453
+ bound.add(node.name)
454
+ elif isinstance(node, TupleAssign):
455
+ bound.update(node.names)
456
+ elif isinstance(node, (FuncDef, MethodDef)):
457
+ bound.add(node.name)
458
+ bound.update(node.params)
459
+ elif isinstance(node, (ForStmt, ForInStmt)):
460
+ if node.var:
461
+ bound.add(node.var)
462
+ if isinstance(getattr(node, "vars", None), list):
463
+ bound.update(node.vars)
464
+ # A history-read builtin becomes a Series member of its own name.
465
+ ctx = getattr(self, "ctx", None)
466
+ if "time_close" in getattr(ctx, "series_vars", ()):
467
+ bound.add("time_close")
468
+ self._safe_name_map: dict[str, str] = {}
469
+ self._safe_name_occupied = authored
470
+ self._safe_name_bound = bound
471
+ for name in sorted(bound):
472
+ if (name in CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES
473
+ or is_emitter_temporary(name)):
474
+ self._allocate_safe_name(name)
475
+
476
+ def _allocate_safe_name(self, name: str) -> str:
477
+ if name in LEGACY_CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES:
478
+ base = f"_{name}_"
479
+ else:
480
+ base = f"pf_safe_{name}"
481
+ occupied = getattr(self, "_safe_name_occupied", set())
482
+ candidate = base
483
+ suffix = 2
484
+ while (candidate in occupied or candidate in CPP_RESERVED
485
+ or candidate in BUILTIN_ACCESSOR_NAMES
486
+ or is_emitter_temporary(candidate)):
487
+ candidate = f"{base}_{suffix}"
488
+ suffix += 1
489
+ self._safe_name_map[name] = candidate
490
+ occupied.add(candidate)
491
+ return candidate
492
+
82
493
  def _safe_name(self, name: str) -> str:
83
- """Rename identifiers that collide with C++ reserved words."""
84
- if name in CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES:
85
- return f"_{name}_"
494
+ """Rename Pine identifiers that collide with emitted C++ names."""
495
+ mapping = getattr(self, "_safe_name_map", None)
496
+ if mapping is not None and name in mapping:
497
+ return mapping[name]
498
+ if mapping is not None and name not in self._safe_name_bound:
499
+ return name
500
+ if (name in CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES
501
+ or is_emitter_temporary(name)):
502
+ if mapping is None:
503
+ return (f"_{name}_" if name in LEGACY_CPP_RESERVED
504
+ or name in BUILTIN_ACCESSOR_NAMES
505
+ else f"pf_safe_{name}")
506
+ return self._allocate_safe_name(name)
86
507
  return name
87
508
 
88
509
  def _func_safe_name(self, name: str) -> str:
@@ -0,0 +1,162 @@
1
+ """Host members the generated C++ can read or write. GENERATED: do not
2
+ edit; run ``scripts/gen_host_members.py``.
3
+
4
+ Every public or protected member of ``pineforge::source::PineStrategyHost`` and its
5
+ bases that the emitter's string constants spell. A script identifier
6
+ with one of these names would hide the host's member from the generated
7
+ strategy class, so ``_safe_name`` renames it.
8
+ """
9
+
10
+ HOST_MEMBER_NAMES = frozenset({
11
+ "_src_close_",
12
+ "_src_high_",
13
+ "_src_hl2_",
14
+ "_src_hlc3_",
15
+ "_src_hlcc4_",
16
+ "_src_low_",
17
+ "_src_ohlc4_",
18
+ "_src_open_",
19
+ "_src_series_active_",
20
+ "_src_volume_",
21
+ "avg_losing_trade",
22
+ "avg_losing_trade_percent",
23
+ "avg_trade",
24
+ "avg_trade_percent",
25
+ "avg_winning_trade",
26
+ "avg_winning_trade_percent",
27
+ "bar_index_",
28
+ "barstate_islast_",
29
+ "cancel",
30
+ "cancel_all",
31
+ "clear_security",
32
+ "closed_trade_commission",
33
+ "closed_trade_direction",
34
+ "closed_trade_entry_bar_index",
35
+ "closed_trade_entry_comment",
36
+ "closed_trade_entry_id",
37
+ "closed_trade_entry_price",
38
+ "closed_trade_entry_time",
39
+ "closed_trade_equity",
40
+ "closed_trade_exit_bar_index",
41
+ "closed_trade_exit_comment",
42
+ "closed_trade_exit_id",
43
+ "closed_trade_exit_price",
44
+ "closed_trade_exit_time",
45
+ "closed_trade_max_drawdown",
46
+ "closed_trade_max_drawdown_percent",
47
+ "closed_trade_max_runup",
48
+ "closed_trade_max_runup_percent",
49
+ "closed_trade_profit",
50
+ "closed_trade_profit_percent",
51
+ "closed_trade_size",
52
+ "commit_script_state",
53
+ "configure_pine_strategy",
54
+ "configure_security_evaluators",
55
+ "count_losstrades",
56
+ "count_wintrades",
57
+ "current_bar_",
58
+ "current_equity",
59
+ "evaluate_security",
60
+ "eventrades",
61
+ "fill_report",
62
+ "foreign_context_",
63
+ "get_input_bool",
64
+ "get_input_double",
65
+ "get_input_int",
66
+ "get_input_int64",
67
+ "get_input_source",
68
+ "get_input_string",
69
+ "get_syminfo_metadata",
70
+ "gross_loss",
71
+ "gross_profit",
72
+ "grossloss_percent",
73
+ "grossprofit_percent",
74
+ "history_advances_new_bar",
75
+ "initial_capital_",
76
+ "input_tf_",
77
+ "is_first_tick",
78
+ "is_last_tick_",
79
+ "last_bar_time_",
80
+ "main_period",
81
+ "margin_liquidation_price",
82
+ "max_contracts_held_all",
83
+ "max_contracts_held_long",
84
+ "max_contracts_held_short",
85
+ "max_drawdown_",
86
+ "max_drawdown_percent",
87
+ "max_runup_",
88
+ "max_runup_percent",
89
+ "net_profit",
90
+ "on_source_bar",
91
+ "open_profit",
92
+ "open_trade_commission",
93
+ "open_trade_direction",
94
+ "open_trade_entry_bar_index",
95
+ "open_trade_entry_comment",
96
+ "open_trade_entry_id",
97
+ "open_trade_entry_price",
98
+ "open_trade_entry_time",
99
+ "open_trade_max_drawdown",
100
+ "open_trade_max_drawdown_percent",
101
+ "open_trade_max_runup",
102
+ "open_trade_max_runup_percent",
103
+ "open_trade_profit",
104
+ "open_trade_profit_percent",
105
+ "open_trade_size",
106
+ "open_trades_capital_held",
107
+ "pine_bar_index",
108
+ "pine_last_bar_index",
109
+ "pine_session_ismarket",
110
+ "pine_session_ispostmarket",
111
+ "pine_session_ispremarket",
112
+ "pine_time_offset",
113
+ "position_entry_name",
114
+ "position_entry_price_",
115
+ "prepare_script_run",
116
+ "prev_bar_timestamp_",
117
+ "prev_chart_close",
118
+ "pyramid_entries_",
119
+ "recorded_series_",
120
+ "recorded_series_value",
121
+ "register_security_eval",
122
+ "register_security_lower_tf_eval",
123
+ "restore_script_state",
124
+ "round_to_mintick",
125
+ "run",
126
+ "script_tf_",
127
+ "security_column_value",
128
+ "security_eval_states_",
129
+ "security_lower_tf_sub_bar_index",
130
+ "security_series_slot_is_new",
131
+ "session_isfirstbar_",
132
+ "session_isfirstbar_regular_",
133
+ "session_islastbar_",
134
+ "session_islastbar_regular_",
135
+ "session_ismarket_",
136
+ "set_input",
137
+ "set_magnifier_volume_weighted",
138
+ "set_pine_risk_direction",
139
+ "set_pine_risk_max_cons_loss_days",
140
+ "set_pine_risk_max_drawdown",
141
+ "set_pine_risk_max_intraday_filled_orders",
142
+ "set_pine_risk_max_intraday_loss",
143
+ "set_pine_risk_max_position_size",
144
+ "set_strategy_override",
145
+ "signed_position_size",
146
+ "snapshot_script_state",
147
+ "strategy_cancel",
148
+ "strategy_cancel_all",
149
+ "strategy_close",
150
+ "strategy_entry",
151
+ "strategy_exit",
152
+ "strategy_exit_cancel_bracket",
153
+ "strategy_order",
154
+ "symbol_facts_",
155
+ "symbol_feeds_",
156
+ "syminfo_",
157
+ "syminfo_mintick_",
158
+ "time_close",
159
+ "trace",
160
+ "trace_enabled_",
161
+ "trades_",
162
+ })