@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.1

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 (51) 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 +767 -77
  7. package/pineforge_codegen/analyzer/call_handlers.py +268 -42
  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 +907 -159
  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 +961 -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 +1198 -101
  25. package/pineforge_codegen/codegen/tables.py +205 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +2055 -62
  28. package/pineforge_codegen/codegen/visit_call.py +929 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +765 -63
  30. package/pineforge_codegen/codegen/visit_stmt.py +610 -52
  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/symbols.py +4 -2
  47. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  48. package/release.json +2 -2
  49. package/tables.json +23 -21
  50. package/transpile.worker.mjs +24 -16
  51. package/pineforge_codegen-0.10.3.tar.gz +0 -0
@@ -82,18 +82,26 @@ 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
90
93
  from .tables import (
91
94
  BAR_SERIES_PUSH,
95
+ TA_CHART_PREV_CLOSE_ARG,
92
96
  DRAWING_TYPE_TO_CPP,
93
97
  PINE_TYPE_TO_CPP,
98
+ PINE_V6_STRATEGY_DEFAULTS,
94
99
  RUNTIME_REGISTER_SECURITY_EVAL_FN,
95
100
  RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
96
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
97
105
 
98
106
 
99
107
  class TopLevelEmitter:
@@ -104,8 +112,26 @@ class TopLevelEmitter:
104
112
  Mixed into ``CodeGen``; not intended to be instantiated standalone."""
105
113
 
106
114
  def _emit_includes(self, lines: list[str]) -> None:
107
- 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>')
108
132
  lines.append('#include <pineforge/ta.hpp>')
133
+ if self._ta_uses_dynamic_lengths():
134
+ lines.append('#include <pineforge/source/pine_ta_length.hpp>')
109
135
  lines.append('#include <pineforge/math.hpp>')
110
136
  lines.append('#include <pineforge/series.hpp>')
111
137
  lines.append('#include <pineforge/na.hpp>')
@@ -118,10 +144,15 @@ class TopLevelEmitter:
118
144
  lines.append("#include <numeric>")
119
145
  lines.append("#include <string>")
120
146
  lines.append("#include <vector>")
147
+ if self._uses_tv_number_format:
148
+ lines.extend(("#include <charconv>", "#include <system_error>",
149
+ "#include <stdexcept>"))
121
150
  if getattr(self, "_udt_defs", {}):
122
151
  lines.append("#include <deque>")
123
152
  lines.append("#include <functional>")
124
153
  lines.append("#include <limits>")
154
+ if self._udt_has_array_fields():
155
+ lines.append("#include <memory>")
125
156
  lines.append("#include <tuple>")
126
157
  lines.append("#include <optional>")
127
158
  lines.append("#include <type_traits>")
@@ -173,6 +204,11 @@ class TopLevelEmitter:
173
204
  # byte-identical — mirrors the matrix.hpp gating above.
174
205
  if getattr(self, "_uses_drawing", False):
175
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
+ ])
176
212
  lines.append("")
177
213
  # Compatibility shim for the namespace-wrap refactor: unqualified
178
214
  # references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
@@ -180,33 +216,228 @@ class TopLevelEmitter:
180
216
  # in favour of fully qualified names emitted at each call site.
181
217
  lines.append("using namespace pineforge;")
182
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("")
183
225
  # Syminfo derivation helpers (_pf_derive_main_tickerid, _pf_derive_country)
184
226
  from .helpers_syminfo import emit_syminfo_helpers
185
227
  lines.extend(emit_syminfo_helpers())
186
228
 
187
- def _script_has_strategy_close(self) -> bool:
188
- """True if the script's AST contains a ``strategy.close*`` call.
189
-
190
- Scans the entire program body (including nested function /
191
- method bodies) for any ``FuncCall`` whose callee resolves to
192
- ``strategy.close`` or ``strategy.close_all``. Comments are
193
- not considered (parser strips them). Result is independent of
194
- whether the close call ever runs at backtest time — this is a
195
- purely static script-shape check used by the engine's flip path
196
- to choose between TV's empirical growth rule and the standard
197
- Pine semantic.
198
-
199
- Mixin contract: relies on ``self._walk_ast`` (NamingHelper) and
200
- ``self._resolve_callee`` (NamingHelper); host class must
201
- provide ``self.ctx.ast``."""
202
- from ..ast_nodes import FuncCall # local to avoid circular import
203
- for node in self._walk_ast(self.ctx.ast):
204
- if not isinstance(node, FuncCall):
205
- continue
206
- func_name, namespace = self._resolve_callee(node.callee)
207
- if namespace == "strategy" and func_name in ("close", "close_all"):
208
- return True
209
- 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
+ ])
210
441
 
211
442
  def _script_has_input_source(self) -> bool:
212
443
  """True if the script's AST contains an ``input.source(...)`` call.
@@ -214,7 +445,7 @@ class TopLevelEmitter:
214
445
  Gates the engine's native source-series push: the runtime only
215
446
  advances ``_src_<field>_`` (paying the per-bar cost) when
216
447
  ``_src_series_active_`` is set, which the ctor does iff this returns
217
- True. Same static-shape scan style as ``_script_has_strategy_close``."""
448
+ True. This is a history-storage requirement, not a broker policy."""
218
449
  from ..ast_nodes import FuncCall # local to avoid circular import
219
450
  for node in self._walk_ast(self.ctx.ast):
220
451
  if not isinstance(node, FuncCall):
@@ -294,6 +525,7 @@ class TopLevelEmitter:
294
525
  """
295
526
  members: list[str] = []
296
527
  seen: set[str] = set()
528
+ varip_members = self._varip_state_member_names()
297
529
  for line in declaration_lines:
298
530
  name = self._script_state_member_name(line)
299
531
  if name is None:
@@ -303,9 +535,109 @@ class TopLevelEmitter:
303
535
  if name in seen:
304
536
  raise AssertionError(f"duplicate generated script-state member: {name}")
305
537
  seen.add(name)
538
+ if name in varip_members:
539
+ continue
306
540
  members.append(name)
307
541
  return members
308
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
+
309
641
  def _emit_handle_checkpoint_traits(self, lines: list[str]) -> None:
310
642
  """Emit recursive rollback adapters for shared-ID collection state.
311
643
 
@@ -469,6 +801,32 @@ class TopLevelEmitter:
469
801
  "",
470
802
  ])
471
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
+
472
830
  # Snapshot backing records field-by-field. A nested UDT field is only
473
831
  # a numeric handle, so the primary trait copies its ID without recursing
474
832
  # into another record. This makes self/cyclic object graphs finite;
@@ -481,7 +839,8 @@ class TopLevelEmitter:
481
839
  for field in fields
482
840
  if field.name not in self._udt_omitted_fields.get(type_name, set())
483
841
  ]
484
- checkpoint_fields = [field.name for field in emitted_fields]
842
+ checkpoint_fields = [self._safe_name(field.name)
843
+ for field in emitted_fields]
485
844
  lines.append("template <>")
486
845
  lines.append(f"struct {checkpoint_traits}<{record_type}> {{")
487
846
  lines.append(" struct snapshot_type {")
@@ -640,6 +999,10 @@ class TopLevelEmitter:
640
999
  # run and their ctor args (bare param names) can never be sized.
641
1000
  if ta_idx in self._dead_ta_indices:
642
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
643
1006
  if site.ctor_args:
644
1007
  # If a ctor arg is neither a compile-time literal nor expandable
645
1008
  # to an input-backed runtime expression, the old code silently
@@ -648,10 +1011,14 @@ class TopLevelEmitter:
648
1011
  # DO expand to a runtime expr (input-backed / arithmetic-over-input,
649
1012
  # incl. function-derived lengths) are safe: the `!_ta_initialized_`
650
1013
  # reset overwrites the placeholder before the first compute.
651
- for a in site.ctor_args:
1014
+ for arg_pos, a in enumerate(site.ctor_args):
652
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.
653
1019
  if (not self._is_compile_time_value(r)
654
- 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))):
655
1022
  # A TA source reached through request.security can have
656
1023
  # several helper-bound constructor variants. Validate
657
1024
  # only variants of this exact source node before the
@@ -663,24 +1030,39 @@ class TopLevelEmitter:
663
1030
  self._collect_ta_runtime_resets(
664
1031
  security_source_node=site.node
665
1032
  )
666
- self._codegen_error(
667
- getattr(site, "node", None),
668
- f"Unsupported TA constructor length '{a}' for "
669
- f"{site.class_name}: it is neither a compile-time "
670
- f"constant nor derived from an input, so PineForge "
671
- f"cannot size the indicator buffer.",
672
- hint=("Use a literal, an input.*() value, or "
673
- "arithmetic over those for TA lengths."),
674
- )
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)
675
1054
  resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
676
1055
  # Compile-time placeholder for the init list; the runtime reset
677
1056
  # (when the arg is input-derived) overwrites it on the first bar.
678
1057
  safe_resolved = []
679
- for r in resolved:
1058
+ for arg_pos, r in enumerate(resolved):
680
1059
  if self._is_compile_time_value(r):
681
- safe_resolved.append(r)
1060
+ rendered = r
682
1061
  else:
683
- 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)
684
1066
  init_parts.append(f"{site.member_name}({', '.join(safe_resolved)})")
685
1067
  # Security evaluator TA ctor args (skip for user function call expressions)
686
1068
  for info in self._security_eval_info:
@@ -689,6 +1071,12 @@ class TopLevelEmitter:
689
1071
  if not site.ctor_args:
690
1072
  continue
691
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
692
1080
  ctor_args, _ctor_arg_stability = self._security_ta_ctor_args_for_variant(
693
1081
  info["sec_id"],
694
1082
  site,
@@ -696,10 +1084,11 @@ class TopLevelEmitter:
696
1084
  )
697
1085
  resolved = [self._resolve_ta_ctor_arg(a) for a in ctor_args]
698
1086
  safe_resolved = []
699
- for r in resolved:
700
- safe_resolved.append(
701
- r if self._is_compile_time_value(r) else "1"
702
- )
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)
703
1092
  init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
704
1093
 
705
1094
  # Non-series var members with compile-time init (deduplicate by name)
@@ -746,18 +1135,43 @@ class TopLevelEmitter:
746
1135
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
747
1136
  if self._is_compile_time_value(cpp_val):
748
1137
  init_parts.append(f"{safe}({cpp_val})")
749
- # Strategy params that map to engine members
750
- 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.
751
1158
  sp = self.ctx.strategy_params
752
1159
 
753
1160
  if sp.get("process_orders_on_close") is True:
754
- ctor_body.append(" process_orders_on_close_ = true;")
1161
+ ctor_body.append(" cfg.process_orders_on_close = true;")
755
1162
 
756
1163
  if sp.get("calc_on_order_fills") is True:
757
- ctor_body.append(" calc_on_order_fills_ = true;")
1164
+ ctor_body.append(" cfg.calc_on_order_fills = true;")
758
1165
 
759
- if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
760
- 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)};")
761
1175
 
762
1176
  # default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
763
1177
  qty_type_map = {
@@ -765,15 +1179,18 @@ class TopLevelEmitter:
765
1179
  "strategy.percent_of_equity": "QtyType::PERCENT_OF_EQUITY",
766
1180
  "strategy.cash": "QtyType::CASH",
767
1181
  }
768
- qty_type = sp.get("default_qty_type")
1182
+ qty_type = declared_or_v6_default("default_qty_type")
769
1183
  if qty_type in qty_type_map:
770
- 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
+ )
771
1187
 
772
- if "default_qty_value" in sp and isinstance(sp["default_qty_value"], (int, float)):
773
- 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)};")
774
1191
 
775
1192
  if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
776
- ctor_body.append(f" pyramiding_ = {sp['pyramiding']};")
1193
+ ctor_body.append(f" cfg.pyramiding = {sp['pyramiding']};")
777
1194
 
778
1195
  # commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
779
1196
  comm_type_map = {
@@ -783,41 +1200,35 @@ class TopLevelEmitter:
783
1200
  }
784
1201
  comm_type = sp.get("commission_type")
785
1202
  if comm_type in comm_type_map:
786
- 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
+ )
787
1206
 
788
1207
  if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
789
- ctor_body.append(f" commission_value_ = {float(sp['commission_value'])};")
1208
+ ctor_body.append(f" cfg.commission_value = {float(sp['commission_value'])};")
790
1209
 
791
1210
  if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
792
- ctor_body.append(f" slippage_ = {int(sp['slippage'])};")
1211
+ ctor_body.append(f" cfg.slippage = {int(sp['slippage'])};")
793
1212
 
794
1213
  # margin_long / margin_short: percent of position value required as
795
1214
  # equity (default 100 = 1x leverage). When required_margin exceeds
796
1215
  # available equity, TV silently rejects the fill — engine mirrors
797
1216
  # this in execute_market_entry's FLAT branch.
798
1217
  if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
799
- ctor_body.append(f" margin_long_ = {float(sp['margin_long'])};")
1218
+ ctor_body.append(f" cfg.margin_long = {float(sp['margin_long'])};")
800
1219
  if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
801
- ctor_body.append(f" margin_short_ = {float(sp['margin_short'])};")
1220
+ ctor_body.append(f" cfg.margin_short = {float(sp['margin_short'])};")
802
1221
 
803
1222
  # close_entries_rule: "FIFO" (default) or "ANY"
804
1223
  if sp.get("close_entries_rule") == "ANY":
805
- ctor_body.append(" close_entries_rule_any_ = true;")
806
-
807
- # Detect ``strategy.close`` / ``strategy.close_all`` calls anywhere in
808
- # the script body. The runtime uses this flag in its priced-entry flip
809
- # path to reproduce TradingView's empirical
810
- # ``new_size = |old| + qty`` rule (see
811
- # docs/codegen-gaps/validation-tv-pyramiding-override.md). The flag
812
- # is set once per compilation; it is independent of how many times
813
- # the close call actually fires at runtime.
814
- if self._script_has_strategy_close():
815
- ctor_body.append(" script_has_strategy_close_ = true;")
1224
+ ctor_body.append(" cfg.close_entries_rule_any = true;")
816
1225
 
817
1226
  # Turn on native source-series history only when the script uses
818
1227
  # input.source — otherwise the engine pays nothing per bar.
819
1228
  if self._script_has_input_source():
820
- 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);")
821
1232
 
822
1233
  if init_parts and ctor_body:
823
1234
  lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
@@ -834,43 +1245,81 @@ class TopLevelEmitter:
834
1245
 
835
1246
  lines.append("")
836
1247
  lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
837
- lines.append(' if (key == "initial_capital") { initial_capital_ = std::stod(value); return; }')
838
- lines.append(' if (key == "commission_value") { commission_value_ = std::stod(value); return; }')
839
- lines.append(' if (key == "default_qty_value") { default_qty_value_ = std::stod(value); return; }')
840
- lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
841
- lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
842
- lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
843
- lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
844
- lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
845
- lines.append(' if (key == "default_qty_type") {')
846
- lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
847
- lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") default_qty_type_ = QtyType::PERCENT_OF_EQUITY;')
848
- lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") default_qty_type_ = QtyType::CASH;')
849
- lines.append(" return;")
850
- lines.append(" }")
851
- lines.append(' if (key == "commission_type") {')
852
- lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") commission_type_ = CommissionType::PERCENT;')
853
- lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") commission_type_ = CommissionType::CASH_PER_ORDER;')
854
- 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 {")
855
1276
  lines.append(" return;")
856
1277
  lines.append(" }")
1278
+ lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
857
1279
  lines.append(" }")
858
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
+ ])
859
1301
  if self._security_eval_info:
860
1302
  lines.append("")
1303
+ if any(info.get("foreign") for info in self._security_eval_info):
1304
+ self._emit_foreign_security_lookups(lines)
861
1305
  lines.append(" void configure_security_evaluators() override {")
862
1306
  lines.append(" security_eval_states_.clear();")
1307
+ lines.extend(self._security_tf_replay_prologue())
863
1308
  for info in self._security_eval_info:
864
1309
  tf = info.get("tf")
865
1310
  tf_expr = info.get("tf_expr")
866
1311
  if tf:
867
1312
  tf_expr = f'"{tf}"'
868
- elif not tf_expr:
869
- # No static tf and no resolvable runtime expression — fall
870
- # back to the chart timeframe so registration still compiles
871
- # (e.g. a request.security inside a dead-code UDF, or one
872
- # 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.
873
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
+ )
874
1323
  if tf_expr:
875
1324
  la = "true" if info["lookahead_on"] else "false"
876
1325
  go = "true" if info.get("gaps_on") else "false"
@@ -889,6 +1338,8 @@ class TopLevelEmitter:
889
1338
  f" {RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN}"
890
1339
  f"({sec_id}, {tf_expr}, input_tf_);"
891
1340
  )
1341
+ elif info.get("foreign"):
1342
+ self._emit_foreign_security_registration(info, tf_expr, la, go, lines)
892
1343
  else:
893
1344
  lines.append(
894
1345
  f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
@@ -896,6 +1347,109 @@ class TopLevelEmitter:
896
1347
  f"input_tf_, {la}, {go}{ha_arg});")
897
1348
  lines.append(" }")
898
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
+
899
1453
  # Map strategy series member name to push expression
900
1454
  _STRAT_SERIES_PUSH = {
901
1455
  "position_size": "signed_position_size()",
@@ -930,7 +1484,7 @@ class TopLevelEmitter:
930
1484
  self._lexical_udt_types = {}
931
1485
  self._lexical_series_bindings = {}
932
1486
  self._lexical_known_var_tombstones = set()
933
- lines.append(" void on_bar(const Bar& bar) override {")
1487
+ lines.append(" void on_source_bar(const Bar& bar) override {")
934
1488
 
935
1489
  # A GeneratedStrategy handle may execute multiple batch runs or
936
1490
  # streaming lifecycles. BacktestEngine resets broker/base state, but
@@ -948,8 +1502,6 @@ class TopLevelEmitter:
948
1502
  for info in self._lazy_source_clock_by_node.values():
949
1503
  lines.append(f" {info['clock']}.reset();")
950
1504
  lines.append(f" {info['hist']}.clear();")
951
- if info["chart"] is not None:
952
- lines.append(f" {info['chart']}.clear();")
953
1505
  lines.append(" }")
954
1506
  for info in self._lazy_source_clock_by_node.values():
955
1507
  lines.append(f" {info['clock']}.begin_bar(bar_index_);")
@@ -959,13 +1511,6 @@ class TopLevelEmitter:
959
1511
  info["hist"],
960
1512
  f"{info['clock']}.bar_base_source",
961
1513
  )
962
- if info["chart"] is not None:
963
- self._emit_history_series_write(
964
- lines,
965
- " ",
966
- info["chart"],
967
- self._visit_expr(info["chart_source"]),
968
- )
969
1514
 
970
1515
  # reset_run_state() owns engine/broker state, while these generated
971
1516
  # Series members belong to the strategy object. Clear all of them on
@@ -985,10 +1530,12 @@ class TopLevelEmitter:
985
1530
  # even when lazy control flow skips its written call on this bar. Seed
986
1531
  # the new slot with the prior current value (``na`` before first reach);
987
1532
  # an executed call later in the bar updates this same slot with its
988
- # scalar actual. Typed-method ``series_arg`` bridges intentionally keep
989
- # 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.
990
1537
  for info in self._inline_history_members:
991
- if info["kind"] != "udf_series_arg":
1538
+ if info["kind"] not in ("udf_series_arg", "fn_global_hist"):
992
1539
  continue
993
1540
  member = info["member_name"]
994
1541
  lines.append(
@@ -1008,17 +1555,18 @@ class TopLevelEmitter:
1008
1555
  # members (base.py section 6) but — unlike user series vars (pushed at
1009
1556
  # their assignment) and bar fields (pushed above) — have no push site,
1010
1557
  # so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
1011
- # Push each from its scalar lowering. A builtin whose lowering is a
1012
- # self-referential call (e.g. ``time_close`` -> ``time_close()``) is
1013
- # 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.
1014
1562
  from .tables import BAR_BUILTINS
1015
1563
  for _bname in sorted(self.ctx.series_vars):
1016
1564
  if _bname in self._var_names:
1017
1565
  continue
1018
1566
  _bexpr = BAR_BUILTINS.get(_bname)
1019
- if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
1020
- continue
1021
1567
  _bsafe = self._safe_name(_bname)
1568
+ if _bexpr is None or _bexpr.strip().startswith(f"{_bsafe}("):
1569
+ continue
1022
1570
  self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
1023
1571
 
1024
1572
  # a2. Push strategy series
@@ -1027,6 +1575,53 @@ class TopLevelEmitter:
1027
1575
  push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
1028
1576
  self._emit_history_series_write(lines, " ", svar, push_expr)
1029
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
+
1030
1625
  # b. Var init / carry-forward
1031
1626
  if self.ctx.var_members:
1032
1627
  lines.append(" if (!_var_initialized) {")
@@ -1035,6 +1630,11 @@ class TopLevelEmitter:
1035
1630
  # declaration statements, not in the global on_bar preamble.
1036
1631
  if name in getattr(self, "_func_local_var_names", ()):
1037
1632
  continue
1633
+ # A wide int array member (``std::vector<int64_t>``, see
1634
+ # ``_wide_int_array_names``) needs its first-bar constructor
1635
+ # spelled with the same element type: name the target for
1636
+ # visit_call's ``array.new_*`` / ``array.from`` lowering.
1637
+ self._array_ctor_target_name = name
1038
1638
  safe = self._safe_name(name)
1039
1639
  runtime_info = self._runtime_scalar_var_init_by_member.get(name)
1040
1640
  if (runtime_info is not None
@@ -1100,6 +1700,13 @@ class TopLevelEmitter:
1100
1700
  break
1101
1701
  continue
1102
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});")
1103
1710
  continue
1104
1711
  # Persistent authored UDT variables initialize from their full
1105
1712
  # source RHS, not only from ``Type.new``. In particular,
@@ -1131,6 +1738,7 @@ class TopLevelEmitter:
1131
1738
  if cloned not in init_emitted:
1132
1739
  init_emitted.add(cloned)
1133
1740
  lines.append(f" {cloned}.push({cpp_val});")
1741
+ self._array_ctor_target_name = None
1134
1742
  lines.append(" _var_initialized = true;")
1135
1743
  lines.append(" } else {")
1136
1744
  for name, _, _ in self.ctx.var_members:
@@ -1189,39 +1797,6 @@ class TopLevelEmitter:
1189
1797
  # c. Push non-var series (they start fresh each bar with a push)
1190
1798
  # (actual push happens in visit_VarDecl when the decl is visited)
1191
1799
 
1192
- # c3. Evaluate static global inputs and variables once
1193
- static_vars = []
1194
- for stmt in self.ctx.ast.body:
1195
- if isinstance(stmt, VarDecl):
1196
- is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
1197
- if is_input:
1198
- func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
1199
- is_static_global_input = (
1200
- stmt.name in self._global_member_vars
1201
- and not self._is_source_input(stmt.value)
1202
- and stmt.name not in self._array_vars
1203
- and stmt.name not in getattr(self, "_matrix_specs", {})
1204
- and stmt.name not in getattr(self, "_map_vars", {})
1205
- and not stmt.is_var
1206
- and not stmt.is_varip
1207
- )
1208
- if is_static_global_input:
1209
- safe = self._safe_name(stmt.name)
1210
- default = self._get_input_default(stmt.value)
1211
- default_cpp = self._visit_expr(default) if default is not None else "0"
1212
- title = self._get_input_title(stmt.value, var_name=stmt.name)
1213
- getter = self._input_type_to_getter(func_name_i, namespace_i)
1214
- default_cpp = self._coerce_string_input_default(getter, default_cpp)
1215
- cpp_val = f'{getter}("{title}", {default_cpp})'
1216
- static_vars.append(f"{safe} = {cpp_val};")
1217
-
1218
- if static_vars:
1219
- lines.append(" if (!_inputs_initialized_) {")
1220
- for var_expr in static_vars:
1221
- lines.append(f" {var_expr}")
1222
- lines.append(" _inputs_initialized_ = true;")
1223
- lines.append(" }")
1224
-
1225
1800
  # c2. First-bar TA resize: rebuild any TA object whose ctor args come
1226
1801
  # from input-backed variables so strategy_set_input() actually changes
1227
1802
  # the circular-buffer sizes. Emits nothing when no TA site depends on
@@ -1230,9 +1805,11 @@ class TopLevelEmitter:
1230
1805
 
1231
1806
  # d. Visit each statement. A stateful ``ta.*`` site below a lazy
1232
1807
  # ``and``/``or`` RHS or ternary arm of a top-level statement is
1233
- # 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``).
1234
1810
  for stmt in self.ctx.ast.body:
1235
1811
  self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
1812
+ self._emit_lazy_call_history_hoists(stmt, lines, indent=2)
1236
1813
  try:
1237
1814
  self._visit_stmt(stmt, lines, indent=2)
1238
1815
  finally:
@@ -1373,9 +1950,37 @@ class TopLevelEmitter:
1373
1950
  lines.append(" if (!s) return;")
1374
1951
  lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
1375
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(" }")
1376
1961
  lines.append("}")
1377
1962
  lines.append("")
1378
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
+
1379
1984
  def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
1380
1985
  """Stable C++ identifier for a typed instance method."""
1381
1986
  receiver_spec = (
@@ -1392,6 +1997,22 @@ class TopLevelEmitter:
1392
1997
 
1393
1998
  def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
1394
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:
1395
2016
  """Emit a user-defined function as a class method.
1396
2017
 
1397
2018
  If call_site_idx is not None, emit a per-call-site variant with
@@ -1405,6 +2026,12 @@ class TopLevelEmitter:
1405
2026
  node = fi.node
1406
2027
  if node is None:
1407
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
+ )
1408
2035
 
1409
2036
  # Collection registries historically used raw variable names for the
1410
2037
  # whole translation unit. Emit each callable against copy-on-write
@@ -1458,9 +2085,9 @@ class TopLevelEmitter:
1458
2085
  )
1459
2086
  variant_param_types = (
1460
2087
  getattr(self.ctx, "func_callsite_param_types", {}).get(
1461
- (fi.name, call_site_idx), ()
2088
+ (fi.name, type_call_site_idx), ()
1462
2089
  )
1463
- if call_site_idx is not None
2090
+ if type_call_site_idx is not None
1464
2091
  else ()
1465
2092
  )
1466
2093
  for i, p in enumerate(node.params):
@@ -1569,7 +2196,7 @@ class TopLevelEmitter:
1569
2196
  PineType.FLOAT: "double",
1570
2197
  PineType.BOOL: "bool",
1571
2198
  PineType.STRING: "std::string",
1572
- PineType.COLOR: "int",
2199
+ PineType.COLOR: "int64_t",
1573
2200
  }[variant_pt]
1574
2201
  elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
1575
2202
  # Precise per-param TypeSpec (declared hint or call-site inference):
@@ -1578,7 +2205,8 @@ class TopLevelEmitter:
1578
2205
  # a string -> ``std::string``. User UDT field mutation propagates
1579
2206
  # through the arena even though the handle parameter is by value.
1580
2207
  spec = fi.param_type_specs[i]
1581
- 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))
1582
2210
  if spec.kind == "udt":
1583
2211
  self._udt_param_udt[p] = spec.name
1584
2212
  self._udt_param_udt[self._safe_name(p)] = spec.name
@@ -1593,7 +2221,8 @@ class TopLevelEmitter:
1593
2221
  cpp_t = f"{cpp_t}&"
1594
2222
  elif i < len(fi.param_types):
1595
2223
  pt = fi.param_types[i]
1596
- 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"))
1597
2226
  else:
1598
2227
  cpp_t = "double"
1599
2228
  param_strs.append(f"{cpp_t} {self._safe_name(p)}")
@@ -1607,14 +2236,27 @@ class TopLevelEmitter:
1607
2236
  # without it the function would be emitted as returning ``double`` and
1608
2237
  # clang errors with "no viable conversion from T to double". Probe:
1609
2238
  # data/validation/udt-method-probe-20-udt-return-from-func.
1610
- 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:
1611
2248
  # Infer actual tuple element types from function body's last expression
1612
2249
  tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
1613
2250
  ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
1614
- elif getattr(fi, "udt_return_type", None):
2251
+ elif return_udt_name:
1615
2252
  # A function returning a drawing handle must emit the C++ handle
1616
2253
  # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
1617
- 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
+ )
1618
2260
  elif self._func_int_return_uses_wide_history(
1619
2261
  fi, call_site_idx=call_site_idx
1620
2262
  ):
@@ -1622,14 +2264,14 @@ class TopLevelEmitter:
1622
2264
  # values and their na sentinel cannot narrow at the return edge.
1623
2265
  ret_type = "int64_t"
1624
2266
  elif (
1625
- call_site_idx is not None
2267
+ type_call_site_idx is not None
1626
2268
  and self._callsite_callable_return_pine_type(
1627
- fi, call_site_idx
2269
+ fi, type_call_site_idx
1628
2270
  ) != PineType.UNKNOWN
1629
2271
  ):
1630
2272
  ret_type = PINE_TYPE_TO_CPP.get(
1631
2273
  self._callsite_callable_return_pine_type(
1632
- fi, call_site_idx
2274
+ fi, type_call_site_idx
1633
2275
  ),
1634
2276
  "double",
1635
2277
  )
@@ -1639,13 +2281,16 @@ class TopLevelEmitter:
1639
2281
  ret_type = self._type_spec_to_cpp(fi.return_type_spec)
1640
2282
  else:
1641
2283
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
1642
- rhs_return_cpp_type = (
1643
- ret_type
1644
- if (self._is_nullable_collection_cpp_type(ret_type)
1645
- or ret_type in DRAWING_TYPE_TO_CPP.values()
1646
- or ret_type in self._udt_defs)
1647
- else None
1648
- )
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
1649
2294
 
1650
2295
  # For per-call-site variants, suffix the function name and activate TA + var remapping
1651
2296
  func_name = (
@@ -1742,22 +2387,45 @@ class TopLevelEmitter:
1742
2387
  self._current_func_locals |= self._collect_binding_names(node.body)
1743
2388
 
1744
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)
1745
2403
 
1746
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] = []
1747
2408
  if node.is_single_expr and node.body:
2409
+ hoisted = self._emit_lazy_call_history_hoists(node.body[0], lines, indent=2)
1748
2410
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
1749
2411
  if expr and self._call_is_void(expr):
1750
2412
  # void setter as the sole body expr — emit as statement, fall
1751
2413
  # through to the default return.
1752
2414
  self._visit_stmt(node.body[0], lines, indent=2)
1753
2415
  elif expr:
1754
- lines.append(
1755
- " return "
1756
- 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),
1757
2422
  )
2423
+ lines.append(f" return {ret_cpp};")
1758
2424
  emitted_return = True
1759
2425
  else:
1760
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)
1761
2429
  if i == len(node.body) - 1 and isinstance(s, ExprStmt):
1762
2430
  # A void drawing setter / delete / visual-noop, or a dropped
1763
2431
  # table/polyline method call (``panel.cell(...)``), used as
@@ -1768,10 +2436,14 @@ class TopLevelEmitter:
1768
2436
  if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
1769
2437
  self._visit_stmt(s, lines, indent=2)
1770
2438
  else:
1771
- lines.append(
1772
- " return "
1773
- 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),
1774
2445
  )
2446
+ lines.append(f" return {ret_cpp};")
1775
2447
  emitted_return = True
1776
2448
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
1777
2449
  # Switch/if as last statement = return expression in PineScript
@@ -1780,31 +2452,92 @@ class TopLevelEmitter:
1780
2452
  # default (``Label _func_ret = Label{};``) — falling through
1781
2453
  # to ``_default_for_type`` would emit ``0.0`` and clang would
1782
2454
  # reject ``Label _func_ret = 0.0;``.
1783
- 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():
1784
2456
  default_ret = f"{ret_type}{{}}"
2457
+ elif fi.returns_tuple:
2458
+ default_ret = self._tuple_default_expr(tuple_types_list)
1785
2459
  else:
1786
2460
  default_ret = self._default_for_type(ret_type)
1787
2461
  lines.append(f" {ret_type} _func_ret = {default_ret};")
1788
- self._visit_if_switch_expr(
2462
+ self._visit_selection_value(
2463
+ s,
2464
+ "_func_ret",
2465
+ lines,
2466
+ indent=2,
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,
2473
+ )
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(
1789
2518
  s,
1790
2519
  "_func_ret",
1791
2520
  lines,
1792
2521
  indent=2,
1793
- target_cpp_type=rhs_return_cpp_type,
2522
+ target_cpp_type=ret_type,
2523
+ reset=False,
1794
2524
  )
1795
- lines.append(f" return _func_ret;")
2525
+ lines.append(" return _func_ret;")
1796
2526
  emitted_return = True
1797
2527
  else:
1798
2528
  self._visit_stmt(s, lines, indent=2)
2529
+ self._clear_lazy_call_history_hoists(hoisted)
1799
2530
 
1800
2531
  # Always emit a default return if no explicit return was emitted,
1801
2532
  # to avoid non-void function without return value.
1802
2533
  if not emitted_return:
1803
- if fi.returns_tuple:
2534
+ if request_struct is not None:
2535
+ lines.append(f" return {request_struct}{{}};")
2536
+ elif fi.returns_tuple:
1804
2537
  default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
1805
2538
  lines.append(f" return std::make_tuple({default_vals});")
1806
2539
  else:
1807
- 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():
1808
2541
  default_ret = f"{ret_type}{{}}"
1809
2542
  else:
1810
2543
  default_ret = self._default_for_type(ret_type)
@@ -1837,13 +2570,42 @@ class TopLevelEmitter:
1837
2570
  self._active_call_site_idx = None
1838
2571
  self._current_instance_name = None
1839
2572
 
1840
- def _emit_precalculate_and_run(self, lines: list[str]) -> None:
1841
- 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(
1842
2602
  self._ta_site_uses_precalc(site)
1843
2603
  for _ti, site in enumerate(self.ctx.ta_call_sites)
1844
2604
  if _ti not in self._dead_ta_indices
1845
2605
  )
1846
- if not has_static_ta:
2606
+
2607
+ def _emit_precalculate_and_run(self, lines: list[str]) -> None:
2608
+ if not self._has_precalculated_ta():
1847
2609
  return
1848
2610
 
1849
2611
  replayed_source_series: list[str] = []
@@ -1870,17 +2632,15 @@ class TopLevelEmitter:
1870
2632
  if self._ta_site_uses_precalc(site):
1871
2633
  lines.append(f" _precalc_{site.member_name}.resize(n);")
1872
2634
 
1873
- # 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).
1874
2637
  lines.append("")
1875
2638
  for _ti, site in enumerate(self.ctx.ta_call_sites):
1876
2639
  if _ti in self._dead_ta_indices:
1877
2640
  continue
1878
2641
  if self._ta_site_uses_precalc(site):
1879
- resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1880
- safe_resolved = []
1881
- for r in resolved:
1882
- safe_resolved.append(r if self._is_compile_time_value(r) else "1")
1883
- 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)});")
1884
2644
 
1885
2645
  # Clear series
1886
2646
  lines.append("")
@@ -1956,7 +2716,7 @@ class TopLevelEmitter:
1956
2716
  default = self._get_input_default(stmt.value)
1957
2717
  base = self._source_defval_to_base_series(default)
1958
2718
  title = self._get_input_title(stmt.value, var_name=stmt.name)
1959
- cpp_val = f'get_input_source("{title}", {base})[0]'
2719
+ cpp_val = f'get_input_source({self._input_key_literal(title)}, {base})[0]'
1960
2720
  # A source var subscripted elsewhere in the script (e.g. ``src[1]``)
1961
2721
  # is declared ``Series<double>``, not a scalar double, mirroring
1962
2722
  # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
@@ -1976,6 +2736,14 @@ class TopLevelEmitter:
1976
2736
  if self._ta_site_uses_precalc(site):
1977
2737
  compute_args = self._ta_compute_args_for_site(site)
1978
2738
  compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
2739
+ # issue #178: the precalc pre-pass walks the bar array
2740
+ # itself, so the previous CHART close is bars[i - 1]
2741
+ # (the engine's prev_chart_close() tracker only advances
2742
+ # at on_bar dispatch).
2743
+ compute_args_bars = compute_args_bars.replace(
2744
+ TA_CHART_PREV_CLOSE_ARG,
2745
+ "(i > 0 ? bars[i - 1].close : na<double>())",
2746
+ )
1979
2747
  lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
1980
2748
  finally:
1981
2749
  self._precalc_loop_active = False
@@ -1988,11 +2756,8 @@ class TopLevelEmitter:
1988
2756
  if _ti in self._dead_ta_indices:
1989
2757
  continue
1990
2758
  if self._ta_site_uses_precalc(site):
1991
- resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1992
- safe_resolved = []
1993
- for r in resolved:
1994
- safe_resolved.append(r if self._is_compile_time_value(r) else "1")
1995
- 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)});")
1996
2761
 
1997
2762
  for field_name in sorted(self.ctx.series_bar_fields):
1998
2763
  lines.append(f" _s_{field_name}.clear();")
@@ -2009,23 +2774,6 @@ class TopLevelEmitter:
2009
2774
  lines.append(" }")
2010
2775
  lines.append("")
2011
2776
 
2012
- # Overridden run methods
2013
- lines.append(" void run(const Bar* bars, int n) {")
2014
- lines.append(" precalculate(bars, n);")
2015
- lines.append(" BacktestEngine::run(bars, n);")
2016
- lines.append(" }")
2017
- lines.append("")
2018
- lines.append(" void run(const Bar* input_bars, int n_input,")
2019
- lines.append(" const std::string& input_tf,")
2020
- lines.append(" const std::string& script_tf,")
2021
- lines.append(" bool bar_magnifier = false,")
2022
- lines.append(" int magnifier_samples = 4,")
2023
- lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
2024
- lines.append(" bool needs_dynamic = bar_magnifier || !input_tf.empty() || !script_tf.empty();")
2025
- lines.append(" if (needs_dynamic) {")
2026
- lines.append(" _use_precalc = false;")
2027
- lines.append(" } else {")
2028
- lines.append(" precalculate(input_bars, n_input);")
2029
- lines.append(" }")
2030
- lines.append(" BacktestEngine::run(input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist);")
2031
- 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.