@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
@@ -82,8 +82,11 @@ and ``..analyzer``.
82
82
 
83
83
  from __future__ import annotations
84
84
 
85
+ import re
86
+
85
87
  from ..ast_nodes import (
86
- ExprStmt, FuncCall, Identifier, IfStmt, SwitchStmt, VarDecl,
88
+ BoolLiteral, ExprStmt, ForInStmt, ForStmt, FuncCall, Identifier, IfStmt,
89
+ MemberAccess, SwitchStmt, TupleAssign, TupleLiteral, VarDecl, WhileStmt,
87
90
  )
88
91
  from ..analyzer import FuncInfo
89
92
  from ..symbols import PineType, method_receiver_cpp_token
@@ -92,9 +95,13 @@ from .tables import (
92
95
  TA_CHART_PREV_CLOSE_ARG,
93
96
  DRAWING_TYPE_TO_CPP,
94
97
  PINE_TYPE_TO_CPP,
98
+ PINE_V6_STRATEGY_DEFAULTS,
95
99
  RUNTIME_REGISTER_SECURITY_EVAL_FN,
96
100
  RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
97
101
  )
102
+ from ..limits import iter_ast_nodes
103
+ from .drawing import DRAWING_LIFETIME_CPP
104
+ from .tv_number_format import TV_NUMBER_FORMAT_CPP
98
105
 
99
106
 
100
107
  class TopLevelEmitter:
@@ -105,8 +112,26 @@ class TopLevelEmitter:
105
112
  Mixed into ``CodeGen``; not intended to be instantiated standalone."""
106
113
 
107
114
  def _emit_includes(self, lines: list[str]) -> None:
108
- lines.append('#include <pineforge/engine.hpp>')
115
+ roots = [self.ctx.ast] + [
116
+ pragma.expr_node for pragma in (self.ctx.pf_trace_pragmas or [])
117
+ ]
118
+ # Every syntax child (``iter_ast_nodes``): ``_walk_ast`` does not enter
119
+ # a tuple literal, so ``str.tostring`` in a request.security tuple
120
+ # payload left ``pine_str_tostring_tv`` undeclared.
121
+ self._uses_tv_number_format = any(
122
+ (namespace == "str" and func_name in {"format", "tostring"})
123
+ or (namespace is None and func_name == "tostring")
124
+ or (namespace == "log" and func_name in {"info", "warning", "error"}
125
+ and len(node.args) > 1)
126
+ for root in roots
127
+ for node, _depth in iter_ast_nodes(root)
128
+ if isinstance(node, FuncCall)
129
+ for func_name, namespace in [self._resolve_callee(node.callee)]
130
+ )
131
+ lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
109
132
  lines.append('#include <pineforge/ta.hpp>')
133
+ if self._ta_uses_dynamic_lengths():
134
+ lines.append('#include <pineforge/source/pine_ta_length.hpp>')
110
135
  lines.append('#include <pineforge/math.hpp>')
111
136
  lines.append('#include <pineforge/series.hpp>')
112
137
  lines.append('#include <pineforge/na.hpp>')
@@ -119,10 +144,15 @@ class TopLevelEmitter:
119
144
  lines.append("#include <numeric>")
120
145
  lines.append("#include <string>")
121
146
  lines.append("#include <vector>")
147
+ if self._uses_tv_number_format:
148
+ lines.extend(("#include <charconv>", "#include <system_error>",
149
+ "#include <stdexcept>"))
122
150
  if getattr(self, "_udt_defs", {}):
123
151
  lines.append("#include <deque>")
124
152
  lines.append("#include <functional>")
125
153
  lines.append("#include <limits>")
154
+ if self._udt_has_array_fields():
155
+ lines.append("#include <memory>")
126
156
  lines.append("#include <tuple>")
127
157
  lines.append("#include <optional>")
128
158
  lines.append("#include <type_traits>")
@@ -174,6 +204,11 @@ class TopLevelEmitter:
174
204
  # byte-identical — mirrors the matrix.hpp gating above.
175
205
  if getattr(self, "_uses_drawing", False):
176
206
  lines.append('#include <pineforge/drawing.hpp>')
207
+ lines.extend([
208
+ "#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1",
209
+ '#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"',
210
+ "#endif",
211
+ ])
177
212
  lines.append("")
178
213
  # Compatibility shim for the namespace-wrap refactor: unqualified
179
214
  # references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
@@ -181,33 +216,228 @@ class TopLevelEmitter:
181
216
  # in favour of fully qualified names emitted at each call site.
182
217
  lines.append("using namespace pineforge;")
183
218
  lines.append("")
219
+ self._emit_ta_compat_shims(lines)
220
+ if getattr(self, "_uses_drawing", False):
221
+ lines.append(DRAWING_LIFETIME_CPP)
222
+ if self._uses_tv_number_format:
223
+ lines.append(TV_NUMBER_FORMAT_CPP)
224
+ lines.append("")
184
225
  # Syminfo derivation helpers (_pf_derive_main_tickerid, _pf_derive_country)
185
226
  from .helpers_syminfo import emit_syminfo_helpers
186
227
  lines.extend(emit_syminfo_helpers())
187
228
 
188
- def _script_has_strategy_close(self) -> bool:
189
- """True if the script's AST contains a ``strategy.close*`` call.
190
-
191
- Scans the entire program body (including nested function /
192
- method bodies) for any ``FuncCall`` whose callee resolves to
193
- ``strategy.close`` or ``strategy.close_all``. Comments are
194
- not considered (parser strips them). Result is independent of
195
- whether the close call ever runs at backtest time — this is a
196
- purely static script-shape check used by the engine's flip path
197
- to choose between TV's empirical growth rule and the standard
198
- Pine semantic.
199
-
200
- Mixin contract: relies on ``self._walk_ast`` (NamingHelper) and
201
- ``self._resolve_callee`` (NamingHelper); host class must
202
- provide ``self.ctx.ast``."""
203
- from ..ast_nodes import FuncCall # local to avoid circular import
204
- for node in self._walk_ast(self.ctx.ast):
205
- if not isinstance(node, FuncCall):
206
- continue
207
- func_name, namespace = self._resolve_callee(node.callee)
208
- if namespace == "strategy" and func_name in ("close", "close_all"):
209
- return True
210
- return False
229
+ def _emit_ta_compat_shims(self, lines: list[str]) -> None:
230
+ """Emit source-level adapters for TA1 arguments and anchored forms.
231
+
232
+ The adapters are selected by feature macros exported from
233
+ ``pineforge/ta.hpp``. This lets one generated TU compile against the
234
+ TA1 engine and the previous engine while keeping the Pine call-site
235
+ shape identical. The old branch deliberately preserves the historical
236
+ lowering; it is only selected when the corresponding macro is absent.
237
+ """
238
+ classes = {
239
+ site.class_name for site in getattr(self.ctx, "ta_call_sites", ())
240
+ }
241
+
242
+ if "_PFALMA" in classes:
243
+ lines.extend([
244
+ "#ifdef PF_ALMA_HAS_FLOOR",
245
+ "class _PFALMA {",
246
+ " ta::ALMA impl_;",
247
+ "public:",
248
+ " _PFALMA(int length, double offset, double sigma, bool floor = false)"
249
+ " : impl_(length, offset, sigma, floor) {}",
250
+ " double compute(double src) { return impl_.compute(src); }",
251
+ " double recompute(double src) { return impl_.recompute(src); }",
252
+ "};",
253
+ "#else",
254
+ "class _PFALMA {",
255
+ " ta::ALMA impl_;",
256
+ "public:",
257
+ " _PFALMA(int length, double offset, double sigma, bool = false)"
258
+ " : impl_(length, offset, sigma) {}",
259
+ " double compute(double src) { return impl_.compute(src); }",
260
+ " double recompute(double src) { return impl_.recompute(src); }",
261
+ "};",
262
+ "#endif",
263
+ "",
264
+ ])
265
+
266
+ if "_PFKC" in classes:
267
+ lines.extend([
268
+ "#ifdef PF_KC_HAS_USE_TRUE_RANGE",
269
+ "class _PFKC {",
270
+ " ta::KC impl_;",
271
+ "public:",
272
+ " _PFKC(int length, double mult, bool use_true_range = true)"
273
+ " : impl_(length, mult, use_true_range) {}",
274
+ " ta::KCResult compute(double src, double high, double low, double close)"
275
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
276
+ " ta::KCResult recompute(double src, double high, double low, double close)"
277
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
278
+ "};",
279
+ "#else",
280
+ "class _PFKC {",
281
+ " ta::KC impl_;",
282
+ "public:",
283
+ " _PFKC(int length, double mult, bool = true) : impl_(length, mult) {}",
284
+ " ta::KCResult compute(double src, double high, double low, double close)"
285
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
286
+ " ta::KCResult recompute(double src, double high, double low, double close)"
287
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
288
+ "};",
289
+ "#endif",
290
+ "",
291
+ ])
292
+
293
+ if "_PFKCW" in classes:
294
+ lines.extend([
295
+ "#ifdef PF_KC_HAS_USE_TRUE_RANGE",
296
+ "class _PFKCW {",
297
+ " ta::KCW impl_;",
298
+ "public:",
299
+ " _PFKCW(int length, double mult, bool use_true_range = true)"
300
+ " : impl_(length, mult, use_true_range) {}",
301
+ " double compute(double src, double high, double low, double close)"
302
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
303
+ " double recompute(double src, double high, double low, double close)"
304
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
305
+ "};",
306
+ "#else",
307
+ "class _PFKCW {",
308
+ " ta::KCW impl_;",
309
+ "public:",
310
+ " _PFKCW(int length, double mult, bool = true) : impl_(length, mult) {}",
311
+ " double compute(double src, double high, double low, double close)"
312
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
313
+ " double recompute(double src, double high, double low, double close)"
314
+ " { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
315
+ "};",
316
+ "#endif",
317
+ "",
318
+ ])
319
+
320
+ if "_PFAnchoredVWAP" in classes:
321
+ lines.extend([
322
+ "#ifdef PF_VWAP_HAS_ANCHOR_INPUT",
323
+ "class _PFAnchoredVWAP {",
324
+ " ta::AnchoredVWAP impl_;",
325
+ "public:",
326
+ " double compute(double src, double volume, int64_t timestamp,"
327
+ " const std::string& tz, const std::string& session, bool anchor) {",
328
+ " (void)timestamp; (void)tz; (void)session;",
329
+ " return impl_.compute(src, volume, anchor);",
330
+ " }",
331
+ " double recompute(double src, double volume, int64_t timestamp,"
332
+ " const std::string& tz, const std::string& session, bool anchor) {",
333
+ " (void)timestamp; (void)tz; (void)session;",
334
+ " return impl_.recompute(src, volume, anchor);",
335
+ " }",
336
+ "};",
337
+ "#else",
338
+ "class _PFAnchoredVWAP {",
339
+ " ta::VWAP impl_;",
340
+ "public:",
341
+ " double compute(double src, double volume, int64_t timestamp,"
342
+ " const std::string& tz, const std::string& session, bool) {",
343
+ "#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
344
+ " return impl_.compute(src, volume, timestamp, tz, session);",
345
+ "#else",
346
+ " (void)tz; (void)session;",
347
+ " return impl_.compute(src, volume, timestamp);",
348
+ "#endif",
349
+ " }",
350
+ " double recompute(double src, double volume, int64_t timestamp,"
351
+ " const std::string& tz, const std::string& session, bool) {",
352
+ "#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
353
+ " return impl_.recompute(src, volume, timestamp, tz, session);",
354
+ "#else",
355
+ " (void)tz; (void)session;",
356
+ " return impl_.recompute(src, volume, timestamp);",
357
+ "#endif",
358
+ " }",
359
+ "};",
360
+ "#endif",
361
+ "",
362
+ ])
363
+
364
+ if "_PFAnchoredVWAPBands" in classes:
365
+ lines.extend([
366
+ "#ifdef PF_VWAP_HAS_ANCHOR_INPUT",
367
+ "class _PFAnchoredVWAPBands {",
368
+ " ta::AnchoredVWAPBands impl_;",
369
+ "public:",
370
+ " explicit _PFAnchoredVWAPBands(double mult) : impl_(mult) {}",
371
+ " ta::VWAPBandsResult compute(double src, double volume, int64_t timestamp,"
372
+ " const std::string& tz, const std::string& session, bool anchor) {",
373
+ " (void)timestamp; (void)tz; (void)session;",
374
+ " return impl_.compute(src, volume, anchor);",
375
+ " }",
376
+ " ta::VWAPBandsResult recompute(double src, double volume, int64_t timestamp,"
377
+ " const std::string& tz, const std::string& session, bool anchor) {",
378
+ " (void)timestamp; (void)tz; (void)session;",
379
+ " return impl_.recompute(src, volume, anchor);",
380
+ " }",
381
+ "};",
382
+ "#else",
383
+ "class _PFAnchoredVWAPBands {",
384
+ " ta::VWAP impl_;",
385
+ " double mult_;",
386
+ "public:",
387
+ " explicit _PFAnchoredVWAPBands(double mult) : mult_(mult) {}",
388
+ " ta::VWAPBandsResult compute(double src, double volume, int64_t timestamp,"
389
+ " const std::string& tz, const std::string& session, bool) {",
390
+ "#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
391
+ " return impl_.compute_bands(src, volume, timestamp, mult_, tz, session);",
392
+ "#else",
393
+ " (void)tz; (void)session;",
394
+ " return impl_.compute_bands(src, volume, timestamp, mult_);",
395
+ "#endif",
396
+ " }",
397
+ " ta::VWAPBandsResult recompute(double src, double volume, int64_t timestamp,"
398
+ " const std::string& tz, const std::string& session, bool) {",
399
+ "#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
400
+ " return impl_.recompute_bands(src, volume, timestamp, mult_, tz, session);",
401
+ "#else",
402
+ " (void)tz; (void)session;",
403
+ " return impl_.recompute_bands(src, volume, timestamp, mult_);",
404
+ "#endif",
405
+ " }",
406
+ "};",
407
+ "#endif",
408
+ "",
409
+ ])
410
+
411
+ if "_PFPivotPointLevels" in classes:
412
+ lines.extend([
413
+ "#ifdef PF_PIVOT_LEVELS_HAS_ANCHOR",
414
+ "class _PFPivotPointLevels {",
415
+ " ta::PivotPointLevels impl_;",
416
+ "public:",
417
+ " std::vector<double> compute(const std::string& type, bool anchor,"
418
+ " bool developing, double open, double high, double low, double close) {",
419
+ " return impl_.compute(type, anchor, developing, open, high, low, close);",
420
+ " }",
421
+ " std::vector<double> recompute(const std::string& type, bool anchor,"
422
+ " bool developing, double open, double high, double low, double close) {",
423
+ " return impl_.recompute(type, anchor, developing, open, high, low, close);",
424
+ " }",
425
+ "};",
426
+ "#else",
427
+ "class _PFPivotPointLevels {",
428
+ "public:",
429
+ " std::vector<double> compute(const std::string& type, bool, bool,"
430
+ " double, double high, double low, double close) {",
431
+ " return ta::pivot_point_levels(type, high, low, close);",
432
+ " }",
433
+ " std::vector<double> recompute(const std::string& type, bool, bool,"
434
+ " double, double high, double low, double close) {",
435
+ " return ta::pivot_point_levels(type, high, low, close);",
436
+ " }",
437
+ "};",
438
+ "#endif",
439
+ "",
440
+ ])
211
441
 
212
442
  def _script_has_input_source(self) -> bool:
213
443
  """True if the script's AST contains an ``input.source(...)`` call.
@@ -215,7 +445,7 @@ class TopLevelEmitter:
215
445
  Gates the engine's native source-series push: the runtime only
216
446
  advances ``_src_<field>_`` (paying the per-bar cost) when
217
447
  ``_src_series_active_`` is set, which the ctor does iff this returns
218
- True. Same static-shape scan style as ``_script_has_strategy_close``."""
448
+ True. This is a history-storage requirement, not a broker policy."""
219
449
  from ..ast_nodes import FuncCall # local to avoid circular import
220
450
  for node in self._walk_ast(self.ctx.ast):
221
451
  if not isinstance(node, FuncCall):
@@ -295,6 +525,7 @@ class TopLevelEmitter:
295
525
  """
296
526
  members: list[str] = []
297
527
  seen: set[str] = set()
528
+ varip_members = self._varip_state_member_names()
298
529
  for line in declaration_lines:
299
530
  name = self._script_state_member_name(line)
300
531
  if name is None:
@@ -304,9 +535,109 @@ class TopLevelEmitter:
304
535
  if name in seen:
305
536
  raise AssertionError(f"duplicate generated script-state member: {name}")
306
537
  seen.add(name)
538
+ if name in varip_members:
539
+ continue
307
540
  members.append(name)
308
541
  return members
309
542
 
543
+ def _varip_state_member_names(self) -> set[str]:
544
+ """Generated members holding ``varip`` state, with their first-run
545
+ initialization latches.
546
+
547
+ A historical bar executes the script once, so ``varip`` keeps its
548
+ value exactly like ``var``, except that a calc_on_order_fills
549
+ recalculation rolls ``var`` back to the bar's committed state and
550
+ leaves ``varip`` alone (TradingView tapes, COOF on/off and with the
551
+ bar magnifier). The rollback checkpoint therefore skips these members;
552
+ a new run still resets them (``prepare_script_run``).
553
+ """
554
+ names: set[str] = set()
555
+ metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
556
+ for node_id, meta in metadata.items():
557
+ decl = meta[0]
558
+ if not (isinstance(decl, VarDecl) and decl.is_varip):
559
+ continue
560
+ base = self._safe_name(meta[1])
561
+ storages = {base}
562
+ for remap in self._func_cs_var_remap.values():
563
+ if base in remap:
564
+ storages.add(remap[base])
565
+ for instance in self._fresh_instances:
566
+ clone = (instance.get("var_remap") or {}).get(base)
567
+ if clone is not None:
568
+ storages.add(clone)
569
+ names |= storages
570
+ for storage in storages:
571
+ flag = self._runtime_var_init_flags.get((node_id, storage))
572
+ if flag is not None:
573
+ names.add(flag)
574
+ return names
575
+
576
+ def _emit_script_run_prepare(self, lines: list[str], declarations: list[str]) -> None:
577
+ """Reset every declared script member, then prepare this run's cache.
578
+
579
+ Unlike per-bar rollback, a new lifecycle also invalidates precalculated
580
+ vectors and initialization latches. Constructor arguments are taken from
581
+ the constructor's own plan; declaration defaults cover every other
582
+ member. The only nonassignable state is the UDT undo coordinator/arenas,
583
+ whose explicit reset retains their stable internal pointers.
584
+ """
585
+ constructor_values: dict[str, str] = {}
586
+ for initializer in self._script_constructor_initializers:
587
+ name, separator, arguments = initializer.partition("(")
588
+ if not separator or not arguments.endswith(")") or name in constructor_values:
589
+ raise AssertionError(f"unexpected generated constructor initializer: {initializer!r}")
590
+ constructor_values[name] = "(" + arguments
591
+
592
+ udt_state = set(getattr(self, "_udt_arena_member_names", {}).values())
593
+ if self._udt_defs:
594
+ udt_state.add(self._udt_undo_coordinator_member_name)
595
+
596
+ lines.extend([
597
+ "#ifndef PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1",
598
+ '#error "Generated lifecycle reset requires a matching PineForge engine; rebuild with script-run preparation support"',
599
+ "#endif",
600
+ " void prepare_script_run(const Bar* bars, int n, bool allow_precalculation) override {",
601
+ " _pf_script_state_checkpoint_.reset();",
602
+ ])
603
+ seen: set[str] = set()
604
+ for declaration in declarations:
605
+ name = self._script_state_member_name(declaration)
606
+ if name is None:
607
+ continue
608
+ if name in seen:
609
+ raise AssertionError(f"duplicate generated lifecycle member: {name}")
610
+ seen.add(name)
611
+ if name in udt_state:
612
+ lines.append(f" this->{name}.reset_for_run();")
613
+ continue
614
+ if name in constructor_values:
615
+ value = f"decltype(this->{name}){constructor_values[name]}"
616
+ else:
617
+ text = declaration.strip()
618
+ match = re.search(r"(?<![A-Za-z_0-9])" + re.escape(name)
619
+ + r"(?=\s*(?:[=({;]))", text)
620
+ if match is None:
621
+ raise AssertionError(f"cannot reset generated declaration: {declaration!r}")
622
+ suffix = text[match.end():-1].strip()
623
+ if not suffix:
624
+ value = f"decltype(this->{name}){{}}"
625
+ elif suffix.startswith("=") and suffix[1:].strip():
626
+ value = suffix[1:].strip()
627
+ elif ((suffix.startswith("(") and suffix.endswith(")"))
628
+ or (suffix.startswith("{") and suffix.endswith("}"))):
629
+ value = f"decltype(this->{name}){suffix}"
630
+ else:
631
+ raise AssertionError(f"unknown generated reset initializer: {declaration!r}")
632
+ lines.append(f" this->{name} = {value};")
633
+ if not set(constructor_values).issubset(seen) or not udt_state.issubset(seen):
634
+ raise AssertionError("generated lifecycle reset does not cover all constructor/UDT state")
635
+ if self._has_precalculated_ta():
636
+ lines.append(" if (allow_precalculation) precalculate(bars, n);")
637
+ else:
638
+ lines.append(" (void)bars; (void)n; (void)allow_precalculation;")
639
+ lines.append(" }")
640
+
310
641
  def _emit_handle_checkpoint_traits(self, lines: list[str]) -> None:
311
642
  """Emit recursive rollback adapters for shared-ID collection state.
312
643
 
@@ -470,6 +801,32 @@ class TopLevelEmitter:
470
801
  "",
471
802
  ])
472
803
 
804
+ if self._udt_has_array_fields():
805
+ # A UDT array field: its binding, and the contents of an array it
806
+ # owns (an aliased script array is checkpointed as itself).
807
+ lines.extend([
808
+ "template <typename _PFElement>",
809
+ f"struct {checkpoint_traits}<_PFArrayField<_PFElement>> {{",
810
+ f" using vector_traits = {checkpoint_traits}<std::vector<_PFElement>>;",
811
+ " struct snapshot_type {",
812
+ " std::shared_ptr<std::vector<_PFElement>> owned;",
813
+ " std::vector<_PFElement>* data;",
814
+ " typename vector_traits::snapshot_type contents;",
815
+ " };",
816
+ " static snapshot_type take(const _PFArrayField<_PFElement>& value) {",
817
+ " snapshot_type snapshot{value.owned(), value.data(), {}};",
818
+ " if (value.owned()) snapshot.contents = vector_traits::take(*value.owned());",
819
+ " return snapshot;",
820
+ " }",
821
+ " static void restore(_PFArrayField<_PFElement>& value,",
822
+ " const snapshot_type& snapshot) {",
823
+ " value.rebind(snapshot.owned, snapshot.data);",
824
+ " if (snapshot.owned) vector_traits::restore(*snapshot.owned, snapshot.contents);",
825
+ " }",
826
+ "};",
827
+ "",
828
+ ])
829
+
473
830
  # Snapshot backing records field-by-field. A nested UDT field is only
474
831
  # a numeric handle, so the primary trait copies its ID without recursing
475
832
  # into another record. This makes self/cyclic object graphs finite;
@@ -482,7 +839,8 @@ class TopLevelEmitter:
482
839
  for field in fields
483
840
  if field.name not in self._udt_omitted_fields.get(type_name, set())
484
841
  ]
485
- checkpoint_fields = [field.name for field in emitted_fields]
842
+ checkpoint_fields = [self._safe_name(field.name)
843
+ for field in emitted_fields]
486
844
  lines.append("template <>")
487
845
  lines.append(f"struct {checkpoint_traits}<{record_type}> {{")
488
846
  lines.append(" struct snapshot_type {")
@@ -641,6 +999,10 @@ class TopLevelEmitter:
641
999
  # run and their ctor args (bare param names) can never be sized.
642
1000
  if ta_idx in self._dead_ta_indices:
643
1001
  continue
1002
+ if self._ta_dynamic_plan(site) is not None:
1003
+ # A simple or series length is read on the bar
1004
+ # (``_ta_dynamic_plan``): the member is default-constructed.
1005
+ continue
644
1006
  if site.ctor_args:
645
1007
  # If a ctor arg is neither a compile-time literal nor expandable
646
1008
  # to an input-backed runtime expression, the old code silently
@@ -649,10 +1011,14 @@ class TopLevelEmitter:
649
1011
  # DO expand to a runtime expr (input-backed / arithmetic-over-input,
650
1012
  # incl. function-derived lengths) are safe: the `!_ta_initialized_`
651
1013
  # reset overwrites the placeholder before the first compute.
652
- for a in site.ctor_args:
1014
+ for arg_pos, a in enumerate(site.ctor_args):
653
1015
  r = self._resolve_ta_ctor_arg(a)
1016
+ # An argument ``_ta_arg_takes_plan`` sends to the lowering
1017
+ # of ``_ta_dynamic_plan`` that it cannot spell is refused
1018
+ # here, as before string literals were spelled.
654
1019
  if (not self._is_compile_time_value(r)
655
- and self._runtime_ctor_arg_for_reset(a) is None):
1020
+ and (self._runtime_ctor_arg_for_reset(a) is None
1021
+ or self._ta_arg_takes_plan(site, arg_pos, a))):
656
1022
  # A TA source reached through request.security can have
657
1023
  # several helper-bound constructor variants. Validate
658
1024
  # only variants of this exact source node before the
@@ -664,24 +1030,39 @@ class TopLevelEmitter:
664
1030
  self._collect_ta_runtime_resets(
665
1031
  security_source_node=site.node
666
1032
  )
667
- self._codegen_error(
668
- getattr(site, "node", None),
669
- f"Unsupported TA constructor length '{a}' for "
670
- f"{site.class_name}: it is neither a compile-time "
671
- f"constant nor derived from an input, so PineForge "
672
- f"cannot size the indicator buffer.",
673
- hint=("Use a literal, an input.*() value, or "
674
- "arithmetic over those for TA lengths."),
675
- )
1033
+ if self._ta_ctor_arg_is_bool(site, arg_pos):
1034
+ message = (
1035
+ f"Unsupported TA constructor flag '{a}' for "
1036
+ f"{site.class_name}: it is neither a compile-time "
1037
+ "constant nor derived from an input, so PineForge "
1038
+ "cannot initialize the indicator with a stable "
1039
+ "per-run value."
1040
+ )
1041
+ hint = ("Use a literal, an input.*() value, or "
1042
+ "arithmetic over those for TA constructor "
1043
+ "arguments.")
1044
+ else:
1045
+ message = (
1046
+ f"Unsupported TA constructor length '{a}' for "
1047
+ f"{site.class_name}: it is neither a compile-time "
1048
+ "constant nor derived from an input, so PineForge "
1049
+ "cannot size the indicator buffer."
1050
+ )
1051
+ hint = ("Use a literal, an input.*() value, or "
1052
+ "arithmetic over those for TA lengths.")
1053
+ self._codegen_error(getattr(site, "node", None), message, hint=hint)
676
1054
  resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
677
1055
  # Compile-time placeholder for the init list; the runtime reset
678
1056
  # (when the arg is input-derived) overwrites it on the first bar.
679
1057
  safe_resolved = []
680
- for r in resolved:
1058
+ for arg_pos, r in enumerate(resolved):
681
1059
  if self._is_compile_time_value(r):
682
- safe_resolved.append(r)
1060
+ rendered = r
683
1061
  else:
684
- safe_resolved.append("1")
1062
+ rendered = "1"
1063
+ if self._ta_ctor_arg_is_bool(site, arg_pos):
1064
+ rendered = self._ta_ctor_bool_cpp(rendered)
1065
+ safe_resolved.append(rendered)
685
1066
  init_parts.append(f"{site.member_name}({', '.join(safe_resolved)})")
686
1067
  # Security evaluator TA ctor args (skip for user function call expressions)
687
1068
  for info in self._security_eval_info:
@@ -690,6 +1071,12 @@ class TopLevelEmitter:
690
1071
  if not site.ctor_args:
691
1072
  continue
692
1073
  for variant in variants:
1074
+ if self._ta_security_plan(
1075
+ info["sec_id"], site, variant.get("binding_stack", ())
1076
+ ) is not None:
1077
+ # Default-constructed: the length is read on the bar
1078
+ # (``_ta_security_plan``).
1079
+ continue
693
1080
  ctor_args, _ctor_arg_stability = self._security_ta_ctor_args_for_variant(
694
1081
  info["sec_id"],
695
1082
  site,
@@ -697,10 +1084,11 @@ class TopLevelEmitter:
697
1084
  )
698
1085
  resolved = [self._resolve_ta_ctor_arg(a) for a in ctor_args]
699
1086
  safe_resolved = []
700
- for r in resolved:
701
- safe_resolved.append(
702
- r if self._is_compile_time_value(r) else "1"
703
- )
1087
+ for arg_pos, r in enumerate(resolved):
1088
+ rendered = r if self._is_compile_time_value(r) else "1"
1089
+ if self._ta_ctor_arg_is_bool(site, arg_pos):
1090
+ rendered = self._ta_ctor_bool_cpp(rendered)
1091
+ safe_resolved.append(rendered)
704
1092
  init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
705
1093
 
706
1094
  # Non-series var members with compile-time init (deduplicate by name)
@@ -747,18 +1135,43 @@ class TopLevelEmitter:
747
1135
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
748
1136
  if self._is_compile_time_value(cpp_val):
749
1137
  init_parts.append(f"{safe}({cpp_val})")
750
- # Strategy params that map to engine members
751
- ctor_body: list[str] = []
1138
+ # This exact initializer plan also defines a cold generated lifecycle.
1139
+ # Engine configuration in the constructor body is deliberately excluded.
1140
+ self._script_constructor_initializers = tuple(init_parts)
1141
+
1142
+ # New engines default to an independent native constructor. Select
1143
+ # Pine compatibility before strategy_create returns and before any
1144
+ # host metadata setter. The adapter currently owns cap and retained-
1145
+ # parent priority only. Cap-only engines retain their legacy priority
1146
+ # default; older headers need neither hook. Always compile/link with
1147
+ # matching engine headers and runtime. This is not a binary ABI bridge.
1148
+ # This is configuration, deliberately outside script-state reset.
1149
+ ctor_body: list[str] = [
1150
+ "#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1)",
1151
+ " pineforge::source::PineStrategyHost::attach_pine_execution_adapter();",
1152
+ "#elif defined(PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1)",
1153
+ " pineforge::source::PineStrategyHost::enable_pine_intraday_cap();",
1154
+ "#endif",
1155
+ " pineforge::source::PineStrategyConfig cfg{};",
1156
+ ]
1157
+ # Strategy params that map to the source host's config surface.
752
1158
  sp = self.ctx.strategy_params
753
1159
 
754
1160
  if sp.get("process_orders_on_close") is True:
755
- ctor_body.append(" process_orders_on_close_ = true;")
1161
+ ctor_body.append(" cfg.process_orders_on_close = true;")
756
1162
 
757
1163
  if sp.get("calc_on_order_fills") is True:
758
- ctor_body.append(" calc_on_order_fills_ = true;")
1164
+ ctor_body.append(" cfg.calc_on_order_fills = true;")
759
1165
 
760
- if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
761
- ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
1166
+ # An omitted initial_capital / default_qty_type / default_qty_value is
1167
+ # TradingView's Pine v6 default, not the host's (see
1168
+ # PINE_V6_STRATEGY_DEFAULTS); a declared argument is emitted as before.
1169
+ def declared_or_v6_default(key: str):
1170
+ return sp[key] if key in sp else PINE_V6_STRATEGY_DEFAULTS[key]
1171
+
1172
+ initial_capital = declared_or_v6_default("initial_capital")
1173
+ if isinstance(initial_capital, (int, float)):
1174
+ ctor_body.append(f" cfg.initial_capital = {float(initial_capital)};")
762
1175
 
763
1176
  # default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
764
1177
  qty_type_map = {
@@ -766,15 +1179,18 @@ class TopLevelEmitter:
766
1179
  "strategy.percent_of_equity": "QtyType::PERCENT_OF_EQUITY",
767
1180
  "strategy.cash": "QtyType::CASH",
768
1181
  }
769
- qty_type = sp.get("default_qty_type")
1182
+ qty_type = declared_or_v6_default("default_qty_type")
770
1183
  if qty_type in qty_type_map:
771
- ctor_body.append(f" default_qty_type_ = {qty_type_map[qty_type]};")
1184
+ ctor_body.append(
1185
+ f" cfg.default_qty_type = static_cast<int>({qty_type_map[qty_type]});"
1186
+ )
772
1187
 
773
- if "default_qty_value" in sp and isinstance(sp["default_qty_value"], (int, float)):
774
- ctor_body.append(f" default_qty_value_ = {float(sp['default_qty_value'])};")
1188
+ qty_value = declared_or_v6_default("default_qty_value")
1189
+ if isinstance(qty_value, (int, float)):
1190
+ ctor_body.append(f" cfg.default_qty_value = {float(qty_value)};")
775
1191
 
776
1192
  if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
777
- ctor_body.append(f" pyramiding_ = {sp['pyramiding']};")
1193
+ ctor_body.append(f" cfg.pyramiding = {sp['pyramiding']};")
778
1194
 
779
1195
  # commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
780
1196
  comm_type_map = {
@@ -784,41 +1200,35 @@ class TopLevelEmitter:
784
1200
  }
785
1201
  comm_type = sp.get("commission_type")
786
1202
  if comm_type in comm_type_map:
787
- ctor_body.append(f" commission_type_ = {comm_type_map[comm_type]};")
1203
+ ctor_body.append(
1204
+ f" cfg.commission_type = static_cast<int>({comm_type_map[comm_type]});"
1205
+ )
788
1206
 
789
1207
  if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
790
- ctor_body.append(f" commission_value_ = {float(sp['commission_value'])};")
1208
+ ctor_body.append(f" cfg.commission_value = {float(sp['commission_value'])};")
791
1209
 
792
1210
  if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
793
- ctor_body.append(f" slippage_ = {int(sp['slippage'])};")
1211
+ ctor_body.append(f" cfg.slippage = {int(sp['slippage'])};")
794
1212
 
795
1213
  # margin_long / margin_short: percent of position value required as
796
1214
  # equity (default 100 = 1x leverage). When required_margin exceeds
797
1215
  # available equity, TV silently rejects the fill — engine mirrors
798
1216
  # this in execute_market_entry's FLAT branch.
799
1217
  if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
800
- ctor_body.append(f" margin_long_ = {float(sp['margin_long'])};")
1218
+ ctor_body.append(f" cfg.margin_long = {float(sp['margin_long'])};")
801
1219
  if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
802
- ctor_body.append(f" margin_short_ = {float(sp['margin_short'])};")
1220
+ ctor_body.append(f" cfg.margin_short = {float(sp['margin_short'])};")
803
1221
 
804
1222
  # close_entries_rule: "FIFO" (default) or "ANY"
805
1223
  if sp.get("close_entries_rule") == "ANY":
806
- ctor_body.append(" close_entries_rule_any_ = true;")
807
-
808
- # Detect ``strategy.close`` / ``strategy.close_all`` calls anywhere in
809
- # the script body. The runtime uses this flag in its priced-entry flip
810
- # path to reproduce TradingView's empirical
811
- # ``new_size = |old| + qty`` rule (see
812
- # docs/codegen-gaps/validation-tv-pyramiding-override.md). The flag
813
- # is set once per compilation; it is independent of how many times
814
- # the close call actually fires at runtime.
815
- if self._script_has_strategy_close():
816
- ctor_body.append(" script_has_strategy_close_ = true;")
1224
+ ctor_body.append(" cfg.close_entries_rule_any = true;")
817
1225
 
818
1226
  # Turn on native source-series history only when the script uses
819
1227
  # input.source — otherwise the engine pays nothing per bar.
820
1228
  if self._script_has_input_source():
821
- ctor_body.append(" _src_series_active_ = true;")
1229
+ ctor_body.append(" cfg.src_series_active = true;")
1230
+
1231
+ ctor_body.append(" configure_pine_strategy(cfg);")
822
1232
 
823
1233
  if init_parts and ctor_body:
824
1234
  lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
@@ -835,43 +1245,81 @@ class TopLevelEmitter:
835
1245
 
836
1246
  lines.append("")
837
1247
  lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
838
- lines.append(' if (key == "initial_capital") { initial_capital_ = std::stod(value); return; }')
839
- lines.append(' if (key == "commission_value") { commission_value_ = std::stod(value); return; }')
840
- lines.append(' if (key == "default_qty_value") { default_qty_value_ = std::stod(value); return; }')
841
- lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
842
- lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
843
- lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
844
- lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
845
- lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
846
- lines.append(' if (key == "default_qty_type") {')
847
- lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
848
- lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") default_qty_type_ = QtyType::PERCENT_OF_EQUITY;')
849
- lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") default_qty_type_ = QtyType::CASH;')
850
- lines.append(" return;")
851
- lines.append(" }")
852
- lines.append(' if (key == "commission_type") {')
853
- lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") commission_type_ = CommissionType::PERCENT;')
854
- lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") commission_type_ = CommissionType::CASH_PER_ORDER;')
855
- lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") commission_type_ = CommissionType::CASH_PER_CONTRACT;')
1248
+ lines.append(" pineforge::source::StrategyOverrides overrides{};")
1249
+ lines.append(' if (key == "initial_capital") {')
1250
+ lines.append(" overrides.initial_capital = std::stod(value);")
1251
+ lines.append(' } else if (key == "commission_value") {')
1252
+ lines.append(" overrides.commission_value = std::stod(value);")
1253
+ lines.append(' } else if (key == "default_qty_value") {')
1254
+ lines.append(" overrides.default_qty_value = std::stod(value);")
1255
+ lines.append(' } else if (key == "pyramiding") {')
1256
+ lines.append(" overrides.pyramiding = std::stoi(value);")
1257
+ lines.append(' } else if (key == "slippage") {')
1258
+ lines.append(" overrides.slippage = std::stoi(value);")
1259
+ lines.append(' } else if (key == "process_orders_on_close") {')
1260
+ lines.append(' overrides.process_orders_on_close = (value == "true" || value == "1");')
1261
+ lines.append(' } else if (key == "calc_on_order_fills") {')
1262
+ lines.append(' overrides.calc_on_order_fills = (value == "true" || value == "1");')
1263
+ lines.append(' } else if (key == "close_entries_rule") {')
1264
+ lines.append(' overrides.close_entries_rule = (value == "ANY" || value == "any" || value == "1");')
1265
+ lines.append(' } else if (key == "default_qty_type") {')
1266
+ lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") overrides.default_qty_type = static_cast<int>(QtyType::FIXED);')
1267
+ lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") overrides.default_qty_type = static_cast<int>(QtyType::PERCENT_OF_EQUITY);')
1268
+ lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") overrides.default_qty_type = static_cast<int>(QtyType::CASH);')
1269
+ lines.append(" else return;")
1270
+ lines.append(' } else if (key == "commission_type") {')
1271
+ lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") overrides.commission_type = static_cast<int>(CommissionType::PERCENT);')
1272
+ lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_ORDER);')
1273
+ lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_CONTRACT);')
1274
+ lines.append(" else return;")
1275
+ lines.append(" } else {")
856
1276
  lines.append(" return;")
857
1277
  lines.append(" }")
1278
+ lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
858
1279
  lines.append(" }")
859
1280
 
1281
+ if self._uses_recorded_requests():
1282
+ lines.extend([
1283
+ "",
1284
+ "#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
1285
+ " // request.earnings / dividends / splits / financial: the series the requests",
1286
+ " // manifest records under the request's key, TradingView's value on the chart",
1287
+ " // bar opening at this bar's time (na where it has no row). A key nobody",
1288
+ " // installed reads na here and sets the request's flag, which its reads",
1289
+ " // test to stop the run.",
1290
+ " double _pf_recorded(const std::string& key, bool& missing) const {",
1291
+ " missing = recorded_series_.count(key) == 0;",
1292
+ " return missing ? na<double>() : recorded_series_value(key);",
1293
+ " }",
1294
+ "#else",
1295
+ " double _pf_recorded(const std::string&, bool& missing) const {",
1296
+ " missing = true;",
1297
+ " return na<double>();",
1298
+ " }",
1299
+ "#endif",
1300
+ ])
860
1301
  if self._security_eval_info:
861
1302
  lines.append("")
1303
+ if any(info.get("foreign") for info in self._security_eval_info):
1304
+ self._emit_foreign_security_lookups(lines)
862
1305
  lines.append(" void configure_security_evaluators() override {")
863
1306
  lines.append(" security_eval_states_.clear();")
1307
+ lines.extend(self._security_tf_replay_prologue())
864
1308
  for info in self._security_eval_info:
865
1309
  tf = info.get("tf")
866
1310
  tf_expr = info.get("tf_expr")
867
1311
  if tf:
868
1312
  tf_expr = f'"{tf}"'
869
- elif not tf_expr:
870
- # No static tf and no resolvable runtime expression — fall
871
- # back to the chart timeframe so registration still compiles
872
- # (e.g. a request.security inside a dead-code UDF, or one
873
- # whose tf is a function param called with mixed timeframes).
1313
+ elif tf == "" and not tf_expr:
1314
+ # An empty timeframe string is the chart's.
874
1315
  tf_expr = "input_tf_"
1316
+ elif not tf_expr:
1317
+ # Every timeframe is resolved or refused
1318
+ # (``_resolve_security_tf``); none registers by default.
1319
+ self._codegen_error(
1320
+ info.get("tf_node"),
1321
+ "request.security timeframe was not resolved for registration",
1322
+ )
875
1323
  if tf_expr:
876
1324
  la = "true" if info["lookahead_on"] else "false"
877
1325
  go = "true" if info.get("gaps_on") else "false"
@@ -890,6 +1338,8 @@ class TopLevelEmitter:
890
1338
  f" {RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN}"
891
1339
  f"({sec_id}, {tf_expr}, input_tf_);"
892
1340
  )
1341
+ elif info.get("foreign"):
1342
+ self._emit_foreign_security_registration(info, tf_expr, la, go, lines)
893
1343
  else:
894
1344
  lines.append(
895
1345
  f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
@@ -897,6 +1347,109 @@ class TopLevelEmitter:
897
1347
  f"input_tf_, {la}, {go}{ha_arg});")
898
1348
  lines.append(" }")
899
1349
 
1350
+ def _emit_foreign_security_lookups(self, lines: list[str]) -> None:
1351
+ """The run-time lookups of another symbol's request (lane XSYM-E):
1352
+ whether its symbol string is the chart's, and whether its data is
1353
+ installed (``strategy_set_symbol_feed`` / ``_facts``). An engine
1354
+ without the symbol-keyed registration has neither."""
1355
+ lines.extend([
1356
+ "#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
1357
+ " // A request of another symbol reads the chart when its symbol string is the",
1358
+ " // chart's: the one the requests manifest resolves it to, else the chart's",
1359
+ " // ticker id or ticker (syminfo.ticker reads the chart).",
1360
+ " bool _pf_symbol_is_chart(const std::string& symbol) const {",
1361
+ " const auto facts = symbol_facts_.find(symbol);",
1362
+ " if (facts != symbol_facts_.end() && !facts->second.canonical.empty())",
1363
+ " return facts->second.canonical == syminfo_.tickerid;",
1364
+ " return symbol == syminfo_.tickerid || symbol == syminfo_.ticker;",
1365
+ " }",
1366
+ " // Otherwise it reads the feed installed for (symbol, timeframe), in the",
1367
+ " // engine's timeframe spelling (\"D\" is \"1D\"), holding the named",
1368
+ " // column it reads (a footprint's), or the symbol's facts say it is",
1369
+ " // invalid (na under ignore_invalid_symbol, a stopped run without it).",
1370
+ " bool _pf_symbol_data_installed(const std::string& symbol,",
1371
+ " const std::string& timeframe,",
1372
+ " const char* column = nullptr) const {",
1373
+ " const auto facts = symbol_facts_.find(symbol);",
1374
+ " if (facts != symbol_facts_.end() && facts->second.valid && !*facts->second.valid)",
1375
+ " return true;",
1376
+ " std::string tf = timeframe.empty() ? script_tf_ : timeframe;",
1377
+ " if (tf.size() == 1 && (tf[0] == 'D' || tf[0] == 'W' || tf[0] == 'M' || tf[0] == 'S'))",
1378
+ " tf = \"1\" + tf;",
1379
+ " for (const auto& feed : symbol_feeds_) {",
1380
+ " if (feed.instrument != symbol || feed.tf != tf) continue;",
1381
+ " if (column == nullptr) return true;",
1382
+ " for (const auto& named : feed.columns) {",
1383
+ " if (named.name == column) return true;",
1384
+ " }",
1385
+ " }",
1386
+ " return false;",
1387
+ " }",
1388
+ " // Its payload seeds ta.ema as TradingView does in the requested context:",
1389
+ " // na until `length` values, then their mean (EmaSeeding::SimpleAverage).",
1390
+ " struct _PFForeignEmaSeeding {",
1391
+ " bool prior_ = ta::ema_na_warmup_flag();",
1392
+ " _PFForeignEmaSeeding() { ta::ema_na_warmup_flag() = true; }",
1393
+ " ~_PFForeignEmaSeeding() { ta::ema_na_warmup_flag() = prior_; }",
1394
+ " _PFForeignEmaSeeding(const _PFForeignEmaSeeding&) = delete;",
1395
+ " _PFForeignEmaSeeding& operator=(const _PFForeignEmaSeeding&) = delete;",
1396
+ " };",
1397
+ ])
1398
+ if any(self._security_footprint_column(info["sec_id"]) for info in self._security_eval_info):
1399
+ lines.extend([
1400
+ " // A footprint's delta: the named column of the requested bar.",
1401
+ " double _pf_symbol_column(int sec_id, const char* name) const {",
1402
+ " return security_column_value(sec_id, name);",
1403
+ " }",
1404
+ "#else",
1405
+ " double _pf_symbol_column(int, const char*) const { return na<double>(); }",
1406
+ ])
1407
+ lines.append("#endif")
1408
+
1409
+ def _emit_foreign_security_registration(
1410
+ self, info: dict, tf_expr: str, la: str, go: str, lines: list[str]) -> None:
1411
+ """Register a request of another symbol by its symbol string as the
1412
+ run computes it before the first bar: on the chart when the string
1413
+ is the chart's, on the symbol's installed feed otherwise, and not at
1414
+ all without one -- its reads then stop the run
1415
+ (``_pf_sec_missing_N``), never reading the chart instead."""
1416
+ sec_id = info["sec_id"]
1417
+ symbol = self._security_tf_runtime_expr(info["symbol_node"])
1418
+ column = self._security_footprint_column(sec_id)
1419
+ column = f', "{column}"' if column else ""
1420
+ ignore_node = info.get("ignore_invalid_node")
1421
+ if ignore_node is None:
1422
+ ignore = "false"
1423
+ elif isinstance(ignore_node, BoolLiteral):
1424
+ ignore = "true" if ignore_node.value else "false"
1425
+ else:
1426
+ ignore = f"static_cast<bool>({self._security_tf_runtime_expr(ignore_node)})"
1427
+ # A footprint is read from a feed's column, which the chart's bars do
1428
+ # not carry: its site reads a feed even for the chart's own symbol.
1429
+ chart = [] if column else [
1430
+ " if (_pf_symbol_is_chart(_pf_symbol)) {",
1431
+ f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}({sec_id}, {tf_expr}, input_tf_, {la}, {go});",
1432
+ f" _pf_sec_missing_{sec_id} = false;",
1433
+ ]
1434
+ feed = f"if (_pf_symbol_data_installed(_pf_symbol, {tf_expr}{column})) {{"
1435
+ lines.extend([
1436
+ "#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
1437
+ " {",
1438
+ f" const std::string _pf_symbol = {symbol};",
1439
+ *chart,
1440
+ (" } else " if chart else " ") + feed,
1441
+ f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}({sec_id}, _pf_symbol, {tf_expr}, "
1442
+ f"input_tf_, {la}, {go}, {ignore});",
1443
+ f" _pf_sec_missing_{sec_id} = false;",
1444
+ " } else {",
1445
+ f" _pf_sec_missing_{sec_id} = true;",
1446
+ " }",
1447
+ " }",
1448
+ "#else",
1449
+ f" _pf_sec_missing_{sec_id} = true;",
1450
+ "#endif",
1451
+ ])
1452
+
900
1453
  # Map strategy series member name to push expression
901
1454
  _STRAT_SERIES_PUSH = {
902
1455
  "position_size": "signed_position_size()",
@@ -931,7 +1484,7 @@ class TopLevelEmitter:
931
1484
  self._lexical_udt_types = {}
932
1485
  self._lexical_series_bindings = {}
933
1486
  self._lexical_known_var_tombstones = set()
934
- lines.append(" void on_bar(const Bar& bar) override {")
1487
+ lines.append(" void on_source_bar(const Bar& bar) override {")
935
1488
 
936
1489
  # A GeneratedStrategy handle may execute multiple batch runs or
937
1490
  # streaming lifecycles. BacktestEngine resets broker/base state, but
@@ -949,8 +1502,6 @@ class TopLevelEmitter:
949
1502
  for info in self._lazy_source_clock_by_node.values():
950
1503
  lines.append(f" {info['clock']}.reset();")
951
1504
  lines.append(f" {info['hist']}.clear();")
952
- if info["chart"] is not None:
953
- lines.append(f" {info['chart']}.clear();")
954
1505
  lines.append(" }")
955
1506
  for info in self._lazy_source_clock_by_node.values():
956
1507
  lines.append(f" {info['clock']}.begin_bar(bar_index_);")
@@ -960,13 +1511,6 @@ class TopLevelEmitter:
960
1511
  info["hist"],
961
1512
  f"{info['clock']}.bar_base_source",
962
1513
  )
963
- if info["chart"] is not None:
964
- self._emit_history_series_write(
965
- lines,
966
- " ",
967
- info["chart"],
968
- self._visit_expr(info["chart_source"]),
969
- )
970
1514
 
971
1515
  # reset_run_state() owns engine/broker state, while these generated
972
1516
  # Series members belong to the strategy object. Clear all of them on
@@ -986,10 +1530,12 @@ class TopLevelEmitter:
986
1530
  # even when lazy control flow skips its written call on this bar. Seed
987
1531
  # the new slot with the prior current value (``na`` before first reach);
988
1532
  # an executed call later in the bar updates this same slot with its
989
- # scalar actual. Typed-method ``series_arg`` bridges intentionally keep
990
- # their existing execution-clock behavior.
1533
+ # scalar actual. A plain UDF's history of a script variable
1534
+ # (``fn_global_hist``) is on the same clock. Typed-method
1535
+ # ``series_arg`` bridges intentionally keep their existing
1536
+ # execution-clock behavior.
991
1537
  for info in self._inline_history_members:
992
- if info["kind"] != "udf_series_arg":
1538
+ if info["kind"] not in ("udf_series_arg", "fn_global_hist"):
993
1539
  continue
994
1540
  member = info["member_name"]
995
1541
  lines.append(
@@ -1009,17 +1555,18 @@ class TopLevelEmitter:
1009
1555
  # members (base.py section 6) but — unlike user series vars (pushed at
1010
1556
  # their assignment) and bar fields (pushed above) — have no push site,
1011
1557
  # so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
1012
- # Push each from its scalar lowering. A builtin whose lowering is a
1013
- # self-referential call (e.g. ``time_close`` -> ``time_close()``) is
1014
- # skipped — the call would resolve to the shadowing Series member.
1558
+ # Push each from its scalar lowering. ``time_close``'s Series
1559
+ # member is escaped (``_time_close_``) so its ``time_close()``
1560
+ # lowering still calls the host; a lowering that would call its
1561
+ # own member is skipped.
1015
1562
  from .tables import BAR_BUILTINS
1016
1563
  for _bname in sorted(self.ctx.series_vars):
1017
1564
  if _bname in self._var_names:
1018
1565
  continue
1019
1566
  _bexpr = BAR_BUILTINS.get(_bname)
1020
- if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
1021
- continue
1022
1567
  _bsafe = self._safe_name(_bname)
1568
+ if _bexpr is None or _bexpr.strip().startswith(f"{_bsafe}("):
1569
+ continue
1023
1570
  self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
1024
1571
 
1025
1572
  # a2. Push strategy series
@@ -1028,6 +1575,53 @@ class TopLevelEmitter:
1028
1575
  push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
1029
1576
  self._emit_history_series_write(lines, " ", svar, push_expr)
1030
1577
 
1578
+ # a3. Push the session.* flags the top level reads at an offset: its
1579
+ # history is the chart bars', whether or not the read runs on this
1580
+ # bar (TradingView's tapes: tests/test_e2e_session_history.py).
1581
+ for flag in sorted(self._session_history_flags):
1582
+ value = self._visit_expr(MemberAccess(object=Identifier(name="session"),
1583
+ member=flag))
1584
+ self._emit_history_series_write(
1585
+ lines, " ", self._session_history_member(flag), value)
1586
+
1587
+ # b0. Evaluate static global inputs once, BEFORE the first-bar ``var``
1588
+ # latch below: a ``var`` array / matrix / map / UDT initializer
1589
+ # reads input members (``array.new_bool(gridLines, false)``); built
1590
+ # first, it read their zero and sized an empty array. TradingView
1591
+ # requires constant input defaults, so the inputs read nothing the
1592
+ # latch builds.
1593
+ static_vars = []
1594
+ for stmt in self.ctx.ast.body:
1595
+ if isinstance(stmt, VarDecl):
1596
+ is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
1597
+ if is_input:
1598
+ func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
1599
+ is_static_global_input = (
1600
+ stmt.name in self._global_member_vars
1601
+ and not self._is_source_input(stmt.value)
1602
+ and stmt.name not in self._array_vars
1603
+ and stmt.name not in getattr(self, "_matrix_specs", {})
1604
+ and stmt.name not in getattr(self, "_map_vars", {})
1605
+ and not stmt.is_var
1606
+ and not stmt.is_varip
1607
+ )
1608
+ if is_static_global_input:
1609
+ safe = self._safe_name(stmt.name)
1610
+ default = self._get_input_default(stmt.value)
1611
+ default_cpp = self._visit_expr(default) if default is not None else "0"
1612
+ title = self._get_input_title(stmt.value, var_name=stmt.name)
1613
+ getter = self._input_getter_for_call(stmt.value, func_name_i, namespace_i)
1614
+ default_cpp = self._coerce_string_input_default(getter, default_cpp)
1615
+ cpp_val = f'{getter}({self._input_key_literal(title)}, {default_cpp})'
1616
+ static_vars.append(f"{safe} = {cpp_val};")
1617
+
1618
+ if static_vars:
1619
+ lines.append(" if (!_inputs_initialized_) {")
1620
+ for var_expr in static_vars:
1621
+ lines.append(f" {var_expr}")
1622
+ lines.append(" _inputs_initialized_ = true;")
1623
+ lines.append(" }")
1624
+
1031
1625
  # b. Var init / carry-forward
1032
1626
  if self.ctx.var_members:
1033
1627
  lines.append(" if (!_var_initialized) {")
@@ -1106,6 +1700,13 @@ class TopLevelEmitter:
1106
1700
  break
1107
1701
  continue
1108
1702
  if name in self._runtime_scalar_var_init_members:
1703
+ if runtime_info is not None and runtime_info.get("is_series"):
1704
+ # A history-read var whose initializer reads what the
1705
+ # body computes (``_series_var_init_keeps_preamble``):
1706
+ # the first bar's slot holds its na until the
1707
+ # declaration replaces it (``_visit_var_decl``).
1708
+ na_cpp = self._typed_na_init("na<double>()", name, ptype)
1709
+ lines.append(f" {safe}.push({na_cpp});")
1109
1710
  continue
1110
1711
  # Persistent authored UDT variables initialize from their full
1111
1712
  # source RHS, not only from ``Type.new``. In particular,
@@ -1196,39 +1797,6 @@ class TopLevelEmitter:
1196
1797
  # c. Push non-var series (they start fresh each bar with a push)
1197
1798
  # (actual push happens in visit_VarDecl when the decl is visited)
1198
1799
 
1199
- # c3. Evaluate static global inputs and variables once
1200
- static_vars = []
1201
- for stmt in self.ctx.ast.body:
1202
- if isinstance(stmt, VarDecl):
1203
- is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
1204
- if is_input:
1205
- func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
1206
- is_static_global_input = (
1207
- stmt.name in self._global_member_vars
1208
- and not self._is_source_input(stmt.value)
1209
- and stmt.name not in self._array_vars
1210
- and stmt.name not in getattr(self, "_matrix_specs", {})
1211
- and stmt.name not in getattr(self, "_map_vars", {})
1212
- and not stmt.is_var
1213
- and not stmt.is_varip
1214
- )
1215
- if is_static_global_input:
1216
- safe = self._safe_name(stmt.name)
1217
- default = self._get_input_default(stmt.value)
1218
- default_cpp = self._visit_expr(default) if default is not None else "0"
1219
- title = self._get_input_title(stmt.value, var_name=stmt.name)
1220
- getter = self._input_type_to_getter(func_name_i, namespace_i)
1221
- default_cpp = self._coerce_string_input_default(getter, default_cpp)
1222
- cpp_val = f'{getter}("{title}", {default_cpp})'
1223
- static_vars.append(f"{safe} = {cpp_val};")
1224
-
1225
- if static_vars:
1226
- lines.append(" if (!_inputs_initialized_) {")
1227
- for var_expr in static_vars:
1228
- lines.append(f" {var_expr}")
1229
- lines.append(" _inputs_initialized_ = true;")
1230
- lines.append(" }")
1231
-
1232
1800
  # c2. First-bar TA resize: rebuild any TA object whose ctor args come
1233
1801
  # from input-backed variables so strategy_set_input() actually changes
1234
1802
  # the circular-buffer sizes. Emits nothing when no TA site depends on
@@ -1237,9 +1805,11 @@ class TopLevelEmitter:
1237
1805
 
1238
1806
  # d. Visit each statement. A stateful ``ta.*`` site below a lazy
1239
1807
  # ``and``/``or`` RHS or ternary arm of a top-level statement is
1240
- # evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
1808
+ # evaluated every bar BEFORE the statement, and so is a pure user
1809
+ # call read at an offset there (TV rules, see ``ta.py``).
1241
1810
  for stmt in self.ctx.ast.body:
1242
1811
  self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
1812
+ self._emit_lazy_call_history_hoists(stmt, lines, indent=2)
1243
1813
  try:
1244
1814
  self._visit_stmt(stmt, lines, indent=2)
1245
1815
  finally:
@@ -1380,9 +1950,37 @@ class TopLevelEmitter:
1380
1950
  lines.append(" if (!s) return;")
1381
1951
  lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
1382
1952
  lines.append(" }")
1953
+ if self._declares_bar_magnifier():
1954
+ # TradingView runs a script that declares use_bar_magnifier = true
1955
+ # on its bar magnifier; the host reads this export to run it on
1956
+ # intrabars with the magnifier on (the run parameters are the
1957
+ # host's: bar_magnifier, a finer input_tf and its feed).
1958
+ lines.append(" int strategy_declares_bar_magnifier(void) {")
1959
+ lines.append(" return 1;")
1960
+ lines.append(" }")
1383
1961
  lines.append("}")
1384
1962
  lines.append("")
1385
1963
 
1964
+ def _declares_bar_magnifier(self) -> bool:
1965
+ """``strategy(use_bar_magnifier = true)``. The argument is a const
1966
+ bool; one that is not a literal cannot be read here, so the TU does
1967
+ not declare the magnifier and the codegen warns."""
1968
+ from ..ast_nodes import BoolLiteral, StrategyDecl
1969
+ for node in self._walk_ast(self.ctx.ast):
1970
+ if not isinstance(node, StrategyDecl):
1971
+ continue
1972
+ value = node.kwargs.get("use_bar_magnifier")
1973
+ if value is None or isinstance(value, BoolLiteral):
1974
+ return value is not None and value.value is True
1975
+ self._codegen_warning(
1976
+ value,
1977
+ "strategy(use_bar_magnifier=...) is not a literal bool: the "
1978
+ "generated strategy does not declare the bar magnifier, so a host "
1979
+ "runs it without one.",
1980
+ hint="Write use_bar_magnifier = true or false.")
1981
+ return False
1982
+ return False
1983
+
1386
1984
  def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
1387
1985
  """Stable C++ identifier for a typed instance method."""
1388
1986
  receiver_spec = (
@@ -1399,6 +1997,22 @@ class TopLevelEmitter:
1399
1997
 
1400
1998
  def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
1401
1999
  instance: dict | None = None) -> None:
2000
+ """Emit a user-defined function as a class method, under v5's rules
2001
+ when it was inlined from a v5 library (``library_v5``)."""
2002
+ node = fi.node
2003
+ previous = self._pine_v5_body
2004
+ self._pine_v5_body = (
2005
+ node is not None
2006
+ and (getattr(node, "annotations", None) or {}).get("pine_version") == 5
2007
+ )
2008
+ try:
2009
+ self._emit_func_def_body(fi, lines, call_site_idx, instance)
2010
+ finally:
2011
+ self._pine_v5_body = previous
2012
+
2013
+ def _emit_func_def_body(self, fi: FuncInfo, lines: list[str],
2014
+ call_site_idx: int | None = None,
2015
+ instance: dict | None = None) -> None:
1402
2016
  """Emit a user-defined function as a class method.
1403
2017
 
1404
2018
  If call_site_idx is not None, emit a per-call-site variant with
@@ -1412,6 +2026,12 @@ class TopLevelEmitter:
1412
2026
  node = fi.node
1413
2027
  if node is None:
1414
2028
  return
2029
+ # The written call whose argument types type this emission: the
2030
+ # variant's own, or the one a fresh nested instance runs.
2031
+ type_call_site_idx = (
2032
+ call_site_idx if call_site_idx is not None
2033
+ else (instance or {}).get("type_call_site_idx")
2034
+ )
1415
2035
 
1416
2036
  # Collection registries historically used raw variable names for the
1417
2037
  # whole translation unit. Emit each callable against copy-on-write
@@ -1465,9 +2085,9 @@ class TopLevelEmitter:
1465
2085
  )
1466
2086
  variant_param_types = (
1467
2087
  getattr(self.ctx, "func_callsite_param_types", {}).get(
1468
- (fi.name, call_site_idx), ()
2088
+ (fi.name, type_call_site_idx), ()
1469
2089
  )
1470
- if call_site_idx is not None
2090
+ if type_call_site_idx is not None
1471
2091
  else ()
1472
2092
  )
1473
2093
  for i, p in enumerate(node.params):
@@ -1576,7 +2196,7 @@ class TopLevelEmitter:
1576
2196
  PineType.FLOAT: "double",
1577
2197
  PineType.BOOL: "bool",
1578
2198
  PineType.STRING: "std::string",
1579
- PineType.COLOR: "int",
2199
+ PineType.COLOR: "int64_t",
1580
2200
  }[variant_pt]
1581
2201
  elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
1582
2202
  # Precise per-param TypeSpec (declared hint or call-site inference):
@@ -1585,7 +2205,8 @@ class TopLevelEmitter:
1585
2205
  # a string -> ``std::string``. User UDT field mutation propagates
1586
2206
  # through the arena even though the handle parameter is by value.
1587
2207
  spec = fi.param_type_specs[i]
1588
- cpp_t = self._type_spec_to_cpp(spec)
2208
+ cpp_t = self._wide_declared_int_param(
2209
+ fi, i, self._type_spec_to_cpp(spec))
1589
2210
  if spec.kind == "udt":
1590
2211
  self._udt_param_udt[p] = spec.name
1591
2212
  self._udt_param_udt[self._safe_name(p)] = spec.name
@@ -1600,7 +2221,8 @@ class TopLevelEmitter:
1600
2221
  cpp_t = f"{cpp_t}&"
1601
2222
  elif i < len(fi.param_types):
1602
2223
  pt = fi.param_types[i]
1603
- cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
2224
+ cpp_t = self._wide_declared_int_param(
2225
+ fi, i, PINE_TYPE_TO_CPP.get(pt, "double"))
1604
2226
  else:
1605
2227
  cpp_t = "double"
1606
2228
  param_strs.append(f"{cpp_t} {self._safe_name(p)}")
@@ -1614,14 +2236,27 @@ class TopLevelEmitter:
1614
2236
  # without it the function would be emitted as returning ``double`` and
1615
2237
  # clang errors with "no viable conversion from T to double". Probe:
1616
2238
  # data/validation/udt-method-probe-20-udt-return-from-func.
1617
- if fi.returns_tuple:
2239
+ return_udt_name = getattr(fi, "udt_return_type", None)
2240
+ return_udt = bool(return_udt_name and return_udt_name in self._udt_defs)
2241
+ request_struct = self._security_helper_request_struct(node)
2242
+ if request_struct is not None:
2243
+ # A helper whose value is a request of a TA tuple
2244
+ # (``request.security(..., ta.macd(...))``) returns the request's
2245
+ # stored result struct, which ``[m, s, h] = htf()`` decomposes.
2246
+ ret_type = request_struct
2247
+ elif fi.returns_tuple:
1618
2248
  # Infer actual tuple element types from function body's last expression
1619
2249
  tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
1620
2250
  ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
1621
- elif getattr(fi, "udt_return_type", None):
2251
+ elif return_udt_name:
1622
2252
  # A function returning a drawing handle must emit the C++ handle
1623
2253
  # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
1624
- ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
2254
+ ret_type = DRAWING_TYPE_TO_CPP.get(
2255
+ return_udt_name,
2256
+ self._safe_name(return_udt_name)
2257
+ if return_udt
2258
+ else return_udt_name,
2259
+ )
1625
2260
  elif self._func_int_return_uses_wide_history(
1626
2261
  fi, call_site_idx=call_site_idx
1627
2262
  ):
@@ -1629,14 +2264,14 @@ class TopLevelEmitter:
1629
2264
  # values and their na sentinel cannot narrow at the return edge.
1630
2265
  ret_type = "int64_t"
1631
2266
  elif (
1632
- call_site_idx is not None
2267
+ type_call_site_idx is not None
1633
2268
  and self._callsite_callable_return_pine_type(
1634
- fi, call_site_idx
2269
+ fi, type_call_site_idx
1635
2270
  ) != PineType.UNKNOWN
1636
2271
  ):
1637
2272
  ret_type = PINE_TYPE_TO_CPP.get(
1638
2273
  self._callsite_callable_return_pine_type(
1639
- fi, call_site_idx
2274
+ fi, type_call_site_idx
1640
2275
  ),
1641
2276
  "double",
1642
2277
  )
@@ -1646,13 +2281,16 @@ class TopLevelEmitter:
1646
2281
  ret_type = self._type_spec_to_cpp(fi.return_type_spec)
1647
2282
  else:
1648
2283
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
1649
- rhs_return_cpp_type = (
1650
- ret_type
1651
- if (self._is_nullable_collection_cpp_type(ret_type)
1652
- or ret_type in DRAWING_TYPE_TO_CPP.values()
1653
- or ret_type in self._udt_defs)
1654
- else None
1655
- )
2284
+ if return_udt:
2285
+ # Keep the authored UDT spelling here for target typing; the
2286
+ # expression visitor converts it to the escaped C++ handle name
2287
+ # at the point it emits ``T{}``.
2288
+ rhs_return_cpp_type = return_udt_name
2289
+ elif (self._is_nullable_collection_cpp_type(ret_type)
2290
+ or ret_type in DRAWING_TYPE_TO_CPP.values()):
2291
+ rhs_return_cpp_type = ret_type
2292
+ else:
2293
+ rhs_return_cpp_type = None
1656
2294
 
1657
2295
  # For per-call-site variants, suffix the function name and activate TA + var remapping
1658
2296
  func_name = (
@@ -1749,22 +2387,45 @@ class TopLevelEmitter:
1749
2387
  self._current_func_locals |= self._collect_binding_names(node.body)
1750
2388
 
1751
2389
  lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
2390
+ self._emit_function_global_history_updates(fi, lines)
2391
+
2392
+ # A session.* flag the body reads at an offset: its history is this
2393
+ # call site's calls, so push the flag once per call, before a lazy
2394
+ # operand or a block can skip the read (TradingView's function tape,
2395
+ # tests/test_e2e_session_history.py).
2396
+ for flag in sorted(self._session_call_flags.get(fi.name, ())):
2397
+ member = self._inline_history_member_by_key.get(
2398
+ ("session_call", fi.name, flag, self._current_instance_name))
2399
+ if member is not None:
2400
+ value = self._visit_expr(MemberAccess(object=Identifier(name="session"),
2401
+ member=flag))
2402
+ self._emit_history_series_write(lines, " ", member, value)
1752
2403
 
1753
2404
  emitted_return = False
2405
+ # A pure call read at an offset below a lazy edge runs once per call
2406
+ # of this function, before its statement (``ta.py``).
2407
+ hoisted: list[int] = []
1754
2408
  if node.is_single_expr and node.body:
2409
+ hoisted = self._emit_lazy_call_history_hoists(node.body[0], lines, indent=2)
1755
2410
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
1756
2411
  if expr and self._call_is_void(expr):
1757
2412
  # void setter as the sole body expr — emit as statement, fall
1758
2413
  # through to the default return.
1759
2414
  self._visit_stmt(node.body[0], lines, indent=2)
1760
2415
  elif expr:
1761
- lines.append(
1762
- " return "
1763
- f"{self._visit_rhs_value(expr, target_cpp_type=rhs_return_cpp_type)};"
2416
+ ret_cpp = self._coerce_int_slot(
2417
+ self._visit_rhs_value(
2418
+ expr, target_cpp_type=rhs_return_cpp_type
2419
+ ),
2420
+ expr,
2421
+ self._int_slot_cpp_type(None, ret_type),
1764
2422
  )
2423
+ lines.append(f" return {ret_cpp};")
1765
2424
  emitted_return = True
1766
2425
  else:
1767
2426
  for i, s in enumerate(node.body):
2427
+ self._clear_lazy_call_history_hoists(hoisted)
2428
+ hoisted = self._emit_lazy_call_history_hoists(s, lines, indent=2)
1768
2429
  if i == len(node.body) - 1 and isinstance(s, ExprStmt):
1769
2430
  # A void drawing setter / delete / visual-noop, or a dropped
1770
2431
  # table/polyline method call (``panel.cell(...)``), used as
@@ -1775,10 +2436,14 @@ class TopLevelEmitter:
1775
2436
  if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
1776
2437
  self._visit_stmt(s, lines, indent=2)
1777
2438
  else:
1778
- lines.append(
1779
- " return "
1780
- f"{self._visit_rhs_value(s.expr, target_cpp_type=rhs_return_cpp_type)};"
2439
+ ret_cpp = self._coerce_int_slot(
2440
+ self._visit_rhs_value(
2441
+ s.expr, target_cpp_type=rhs_return_cpp_type
2442
+ ),
2443
+ s.expr,
2444
+ self._int_slot_cpp_type(None, ret_type),
1781
2445
  )
2446
+ lines.append(f" return {ret_cpp};")
1782
2447
  emitted_return = True
1783
2448
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
1784
2449
  # Switch/if as last statement = return expression in PineScript
@@ -1787,31 +2452,92 @@ class TopLevelEmitter:
1787
2452
  # default (``Label _func_ret = Label{};``) — falling through
1788
2453
  # to ``_default_for_type`` would emit ``0.0`` and clang would
1789
2454
  # reject ``Label _func_ret = 0.0;``.
1790
- if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
2455
+ if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
1791
2456
  default_ret = f"{ret_type}{{}}"
2457
+ elif fi.returns_tuple:
2458
+ default_ret = self._tuple_default_expr(tuple_types_list)
1792
2459
  else:
1793
2460
  default_ret = self._default_for_type(ret_type)
1794
2461
  lines.append(f" {ret_type} _func_ret = {default_ret};")
1795
- self._visit_if_switch_expr(
2462
+ self._visit_selection_value(
1796
2463
  s,
1797
2464
  "_func_ret",
1798
2465
  lines,
1799
2466
  indent=2,
1800
- target_cpp_type=rhs_return_cpp_type,
2467
+ target_cpp_type=(
2468
+ rhs_return_cpp_type
2469
+ if rhs_return_cpp_type is not None
2470
+ else self._int_slot_cpp_type(None, ret_type)
2471
+ ),
2472
+ slot_cpp_type=None if fi.returns_tuple else ret_type,
1801
2473
  )
1802
- lines.append(f" return _func_ret;")
2474
+ lines.append(" return _func_ret;")
2475
+ emitted_return = True
2476
+ elif (i == len(node.body) - 1 and isinstance(s, TupleAssign)
2477
+ and fi.returns_tuple and "_" not in s.names):
2478
+ # ``[p, q] = pair(v)`` last returns the declared tuple.
2479
+ self._visit_stmt(s, lines, indent=2)
2480
+ declared = TupleLiteral(
2481
+ elements=[Identifier(name=name) for name in s.names]
2482
+ )
2483
+ declared.loc = s.loc
2484
+ for element in declared.elements:
2485
+ element.loc = s.loc
2486
+ lines.append(
2487
+ f" return {self._visit_rhs_value(declared, target_cpp_type=rhs_return_cpp_type)};"
2488
+ )
2489
+ emitted_return = True
2490
+ elif (i == len(node.body) - 1
2491
+ and self._statement_value_node(s) is not None
2492
+ and self._tail_value_fits(s, ret_type)):
2493
+ # ``_f := expr`` / ``y += v`` / ``b = a * 3`` last: the
2494
+ # function returns the value the statement leaves. It
2495
+ # used to fall through to the default return, so
2496
+ # cs-lev-tradleware's Gaussian filter was always 0.0.
2497
+ self._visit_stmt(s, lines, indent=2)
2498
+ value = self._statement_value_node(s)
2499
+ ret_cpp = self._coerce_int_slot(
2500
+ self._visit_rhs_value(
2501
+ value, target_cpp_type=rhs_return_cpp_type
2502
+ ),
2503
+ value,
2504
+ self._int_slot_cpp_type(None, ret_type),
2505
+ )
2506
+ lines.append(f" return {ret_cpp};")
2507
+ emitted_return = True
2508
+ elif (i == len(node.body) - 1
2509
+ and isinstance(s, (ForStmt, ForInStmt, WhileStmt))
2510
+ and not fi.returns_tuple
2511
+ and self._loop_value_cpp_type(s, ret_type) is not None):
2512
+ # A loop last: its body's last value on the last iteration
2513
+ # that reached it, na when none did.
2514
+ lines.append(
2515
+ f" {ret_type} _func_ret = {self._na_value_for_type(ret_type)};"
2516
+ )
2517
+ self._emit_loop_with_assign(
2518
+ s,
2519
+ "_func_ret",
2520
+ lines,
2521
+ indent=2,
2522
+ target_cpp_type=ret_type,
2523
+ reset=False,
2524
+ )
2525
+ lines.append(" return _func_ret;")
1803
2526
  emitted_return = True
1804
2527
  else:
1805
2528
  self._visit_stmt(s, lines, indent=2)
2529
+ self._clear_lazy_call_history_hoists(hoisted)
1806
2530
 
1807
2531
  # Always emit a default return if no explicit return was emitted,
1808
2532
  # to avoid non-void function without return value.
1809
2533
  if not emitted_return:
1810
- if fi.returns_tuple:
2534
+ if request_struct is not None:
2535
+ lines.append(f" return {request_struct}{{}};")
2536
+ elif fi.returns_tuple:
1811
2537
  default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
1812
2538
  lines.append(f" return std::make_tuple({default_vals});")
1813
2539
  else:
1814
- if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
2540
+ if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
1815
2541
  default_ret = f"{ret_type}{{}}"
1816
2542
  else:
1817
2543
  default_ret = self._default_for_type(ret_type)
@@ -1844,13 +2570,42 @@ class TopLevelEmitter:
1844
2570
  self._active_call_site_idx = None
1845
2571
  self._current_instance_name = None
1846
2572
 
1847
- def _emit_precalculate_and_run(self, lines: list[str]) -> None:
1848
- has_static_ta = any(
2573
+ def _emit_function_global_history_updates(
2574
+ self, fi: FuncInfo, lines: list[str]) -> None:
2575
+ """Record, on entry, each script variable the body reads through history.
2576
+
2577
+ The on_bar preamble has already advanced the call site's buffer by one
2578
+ chart slot, holding the previous value; this call replaces that slot
2579
+ with the value the call sees, so ``x[k]`` reads the site's latest call
2580
+ at or before ``k`` bars ago (``na`` before its first call).
2581
+ """
2582
+ names = self.ctx.func_global_history_reads.get(fi.name, ())
2583
+ first_read = {}
2584
+ for owner, name, subscript in self.ctx.func_global_history_nodes.values():
2585
+ if owner == fi.name:
2586
+ first_read.setdefault(name, subscript.object)
2587
+ cpp_types = {
2588
+ info["member_name"]: info["cpp_type"]
2589
+ for info in self._inline_history_members
2590
+ }
2591
+ for name in names:
2592
+ member = self._function_global_history_member(name)
2593
+ if member is None:
2594
+ continue
2595
+ value = self._series_bridge_value_expr(
2596
+ self._visit_expr(first_read[name]), cpp_types[member]
2597
+ )
2598
+ lines.append(f" {member}.update({value});")
2599
+
2600
+ def _has_precalculated_ta(self) -> bool:
2601
+ return any(
1849
2602
  self._ta_site_uses_precalc(site)
1850
2603
  for _ti, site in enumerate(self.ctx.ta_call_sites)
1851
2604
  if _ti not in self._dead_ta_indices
1852
2605
  )
1853
- if not has_static_ta:
2606
+
2607
+ def _emit_precalculate_and_run(self, lines: list[str]) -> None:
2608
+ if not self._has_precalculated_ta():
1854
2609
  return
1855
2610
 
1856
2611
  replayed_source_series: list[str] = []
@@ -1877,17 +2632,15 @@ class TopLevelEmitter:
1877
2632
  if self._ta_site_uses_precalc(site):
1878
2633
  lines.append(f" _precalc_{site.member_name}.resize(n);")
1879
2634
 
1880
- # Reset indicators to clean slate
2635
+ # Reset indicators to clean slate, built with the arguments this run
2636
+ # uses (an input override resizes the precalculated series too).
1881
2637
  lines.append("")
1882
2638
  for _ti, site in enumerate(self.ctx.ta_call_sites):
1883
2639
  if _ti in self._dead_ta_indices:
1884
2640
  continue
1885
2641
  if self._ta_site_uses_precalc(site):
1886
- resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1887
- safe_resolved = []
1888
- for r in resolved:
1889
- safe_resolved.append(r if self._is_compile_time_value(r) else "1")
1890
- lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
2642
+ run_args, _any_runtime = self._ta_run_ctor_args(site)
2643
+ lines.append(f" {site.member_name} = {site.class_name}({', '.join(run_args)});")
1891
2644
 
1892
2645
  # Clear series
1893
2646
  lines.append("")
@@ -1963,7 +2716,7 @@ class TopLevelEmitter:
1963
2716
  default = self._get_input_default(stmt.value)
1964
2717
  base = self._source_defval_to_base_series(default)
1965
2718
  title = self._get_input_title(stmt.value, var_name=stmt.name)
1966
- cpp_val = f'get_input_source("{title}", {base})[0]'
2719
+ cpp_val = f'get_input_source({self._input_key_literal(title)}, {base})[0]'
1967
2720
  # A source var subscripted elsewhere in the script (e.g. ``src[1]``)
1968
2721
  # is declared ``Series<double>``, not a scalar double, mirroring
1969
2722
  # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
@@ -2003,11 +2756,8 @@ class TopLevelEmitter:
2003
2756
  if _ti in self._dead_ta_indices:
2004
2757
  continue
2005
2758
  if self._ta_site_uses_precalc(site):
2006
- resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
2007
- safe_resolved = []
2008
- for r in resolved:
2009
- safe_resolved.append(r if self._is_compile_time_value(r) else "1")
2010
- lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
2759
+ run_args, _any_runtime = self._ta_run_ctor_args(site)
2760
+ lines.append(f" {site.member_name} = {site.class_name}({', '.join(run_args)});")
2011
2761
 
2012
2762
  for field_name in sorted(self.ctx.series_bar_fields):
2013
2763
  lines.append(f" _s_{field_name}.clear();")
@@ -2024,23 +2774,6 @@ class TopLevelEmitter:
2024
2774
  lines.append(" }")
2025
2775
  lines.append("")
2026
2776
 
2027
- # Overridden run methods
2028
- lines.append(" void run(const Bar* bars, int n) {")
2029
- lines.append(" precalculate(bars, n);")
2030
- lines.append(" BacktestEngine::run(bars, n);")
2031
- lines.append(" }")
2032
- lines.append("")
2033
- lines.append(" void run(const Bar* input_bars, int n_input,")
2034
- lines.append(" const std::string& input_tf,")
2035
- lines.append(" const std::string& script_tf,")
2036
- lines.append(" bool bar_magnifier = false,")
2037
- lines.append(" int magnifier_samples = 4,")
2038
- lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
2039
- lines.append(" bool needs_dynamic = bar_magnifier || !input_tf.empty() || !script_tf.empty();")
2040
- lines.append(" if (needs_dynamic) {")
2041
- lines.append(" _use_precalc = false;")
2042
- lines.append(" } else {")
2043
- lines.append(" precalculate(input_bars, n_input);")
2044
- lines.append(" }")
2045
- lines.append(" BacktestEngine::run(input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist);")
2046
- lines.append(" }")
2777
+ # All run overloads are inherited. The base-owned lifecycle hook resets
2778
+ # generated state before preparing this run's cache, including calls
2779
+ # entered through stream_begin rather than the generated C wrappers.