@pineforge/codegen-pyodide 0.8.0 → 0.9.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.
@@ -90,6 +90,7 @@ from ..analyzer import FuncInfo
90
90
  from ..symbols import TypeSpec
91
91
  from .tables import (
92
92
  BAR_SERIES_PUSH,
93
+ DRAWING_TYPE_TO_CPP,
93
94
  PINE_TYPE_TO_CPP,
94
95
  RUNTIME_REGISTER_SECURITY_EVAL_FN,
95
96
  RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
@@ -117,6 +118,8 @@ class TopLevelEmitter:
117
118
  lines.append("#include <string>")
118
119
  lines.append("#include <vector>")
119
120
  lines.append("#include <tuple>")
121
+ lines.append("#include <optional>")
122
+ lines.append("#include <type_traits>")
120
123
  lines.append("#include <memory>")
121
124
  lines.append("#include <mutex>")
122
125
  lines.append("#include <unordered_map>")
@@ -136,6 +139,11 @@ class TopLevelEmitter:
136
139
  float_spec = TypeSpec.primitive("float")
137
140
  if any(spec.element != float_spec for spec in self._matrix_specs.values()):
138
141
  lines.append('#include <pineforge/generic_matrix.hpp>')
142
+ # Drawing-objects-as-data runtime (line/box/label/linefill arenas +
143
+ # ChartPoint). Gated on _uses_drawing so non-drawing strategies stay
144
+ # byte-identical — mirrors the matrix.hpp gating above.
145
+ if getattr(self, "_uses_drawing", False):
146
+ lines.append('#include <pineforge/drawing.hpp>')
139
147
  lines.append("")
140
148
  # Compatibility shim for the namespace-wrap refactor: unqualified
141
149
  # references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
@@ -186,6 +194,139 @@ class TopLevelEmitter:
186
194
  return True
187
195
  return False
188
196
 
197
+ @staticmethod
198
+ def _script_state_member_name(decl_line: str) -> str | None:
199
+ """Extract a generated class-member name from one declaration line.
200
+
201
+ ``CodeGen.generate`` emits the complete persistent script state as a
202
+ contiguous block of one-line declarations before the constructor. The
203
+ rollback checkpoint is derived from that block rather than from a
204
+ second, hand-maintained inventory: adding a new TA/helper/collection
205
+ member therefore automatically makes it part of Pine's historical
206
+ execution rollback.
207
+
208
+ The supported declaration shapes are the only shapes emitted in that
209
+ block today::
210
+
211
+ Type name;
212
+ Type name = value;
213
+ Type name(args);
214
+ Type name{args};
215
+
216
+ A declaration that does not match fails generation loudly. Silently
217
+ omitting an unfamiliar member would be materially worse: it would let
218
+ state leak between ``calc_on_order_fills`` executions.
219
+ """
220
+ text = decl_line.strip()
221
+ if not text:
222
+ return None
223
+ if not text.endswith(";"):
224
+ raise AssertionError(
225
+ f"unexpected generated script-state declaration: {decl_line!r}"
226
+ )
227
+ text = text[:-1].rstrip()
228
+ if " = " in text:
229
+ text = text.split(" = ", 1)[0].rstrip()
230
+ else:
231
+ # Series<T> members can carry a max_bars_back ctor suffix and
232
+ # drawing arenas use brace initialization. Both suffixes start in
233
+ # the final declarator token; C++ types emitted here never contain
234
+ # parentheses or braces.
235
+ last_space = text.rfind(" ")
236
+ if last_space < 0:
237
+ raise AssertionError(
238
+ f"missing type in generated script-state declaration: {decl_line!r}"
239
+ )
240
+ declarator = text[last_space + 1:]
241
+ cut = len(declarator)
242
+ for marker in ("(", "{"):
243
+ pos = declarator.find(marker)
244
+ if pos >= 0:
245
+ cut = min(cut, pos)
246
+ text = text[:last_space + 1] + declarator[:cut]
247
+
248
+ name = text.rsplit(" ", 1)[-1].strip()
249
+ if not name or not (name[0].isalpha() or name[0] == "_") \
250
+ or not all(ch.isalnum() or ch == "_" for ch in name):
251
+ raise AssertionError(
252
+ f"cannot identify generated script-state member: {decl_line!r}"
253
+ )
254
+ return name
255
+
256
+ def _collect_script_state_members(self, declaration_lines: list[str]) -> list[str]:
257
+ """Return every rollback-relevant generated member in declaration order.
258
+
259
+ Precalculated TA result vectors and their mode flag are immutable once
260
+ the engine starts its broker walk. Copying an O(number-of-bars) cache
261
+ before every COOF execution would be both unnecessary and catastrophic,
262
+ so those implementation caches are the sole exclusions. The live TA
263
+ objects themselves remain captured because dynamic/magnifier runs call
264
+ ``compute`` and mutate them.
265
+ """
266
+ members: list[str] = []
267
+ seen: set[str] = set()
268
+ for line in declaration_lines:
269
+ name = self._script_state_member_name(line)
270
+ if name is None:
271
+ continue
272
+ if name == "_use_precalc" or name.startswith("_precalc_"):
273
+ continue
274
+ if name in seen:
275
+ raise AssertionError(f"duplicate generated script-state member: {name}")
276
+ seen.add(name)
277
+ members.append(name)
278
+ return members
279
+
280
+ def _emit_script_state_hooks(self, lines: list[str], members: list[str]) -> None:
281
+ """Emit the engine's Pine rollback checkpoint hook implementation.
282
+
283
+ The checkpoint owns value copies of all generated mutable state. Every
284
+ runtime container used by generated code (Series, std::vector/map,
285
+ PineMatrix/generic matrices, UDTs and drawing arenas) has value
286
+ semantics, so copying recursively preserves data without retaining
287
+ pointers into live state. Drawing handles themselves are stable ids;
288
+ their arenas are captured in the same checkpoint.
289
+
290
+ The static assertions deliberately turn any future non-copyable member
291
+ into a compile failure instead of a nominal, shallow rollback. Engine
292
+ broker/order state lives in the base class and is intentionally absent:
293
+ fills must survive while Pine script variables roll back.
294
+ """
295
+ lines.append(" struct _PFScriptState {")
296
+ for idx, name in enumerate(members):
297
+ lines.append(
298
+ f" decltype(GeneratedStrategy::{name}) _pf_value_{idx};"
299
+ )
300
+ lines.append(" };")
301
+ lines.append(
302
+ " static_assert(std::is_copy_constructible_v<_PFScriptState>, "
303
+ '"generated Pine state must be deep-copy constructible");'
304
+ )
305
+ lines.append(
306
+ " static_assert(std::is_copy_assignable_v<_PFScriptState>, "
307
+ '"generated Pine state must be deep-copy assignable");'
308
+ )
309
+ lines.append(" std::optional<_PFScriptState> _pf_script_state_checkpoint_;")
310
+ lines.append("")
311
+ lines.append(" void snapshot_script_state() override {")
312
+ lines.append(" _pf_script_state_checkpoint_.emplace(_PFScriptState{")
313
+ for name in members:
314
+ lines.append(f" {name},")
315
+ lines.append(" });")
316
+ lines.append(" }")
317
+ lines.append("")
318
+ lines.append(" void restore_script_state() override {")
319
+ lines.append(" if (!_pf_script_state_checkpoint_) return;")
320
+ for idx, name in enumerate(members):
321
+ lines.append(
322
+ f" this->{name} = _pf_script_state_checkpoint_->_pf_value_{idx};"
323
+ )
324
+ lines.append(" }")
325
+ lines.append("")
326
+ lines.append(" void commit_script_state() override {")
327
+ lines.append(" snapshot_script_state();")
328
+ lines.append(" }")
329
+
189
330
  def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
190
331
  """Re-type a bare ``na<double>()`` initializer to match a non-double
191
332
  member's C++ type. A ``var int x = na`` resolves its RHS to
@@ -206,11 +347,35 @@ class TopLevelEmitter:
206
347
  def _emit_constructor(self, lines: list[str]) -> None:
207
348
  init_parts: list[str] = []
208
349
  # TA members with ctor args
209
- for site in self.ctx.ta_call_sites:
350
+ for ta_idx, site in enumerate(self.ctx.ta_call_sites):
351
+ # Skip dead-code function TA sites entirely — their buffers never
352
+ # run and their ctor args (bare param names) can never be sized.
353
+ if ta_idx in self._dead_ta_indices:
354
+ continue
210
355
  if site.ctor_args:
356
+ # If a ctor arg is neither a compile-time literal nor expandable
357
+ # to an input-backed runtime expression, the old code silently
358
+ # emitted period 1 with no overwriting reset — a wrong indicator
359
+ # masquerading as a working one. Refuse loudly instead. Args that
360
+ # DO expand to a runtime expr (input-backed / arithmetic-over-input,
361
+ # incl. function-derived lengths) are safe: the `!_ta_initialized_`
362
+ # reset overwrites the placeholder before the first compute.
363
+ for a in site.ctor_args:
364
+ r = self._resolve_known(a)
365
+ if (not self._is_compile_time_value(r)
366
+ and self._runtime_ctor_arg_for_reset(a) is None):
367
+ self._codegen_error(
368
+ getattr(site, "node", None),
369
+ f"Unsupported TA constructor length '{a}' for "
370
+ f"{site.class_name}: it is neither a compile-time "
371
+ f"constant nor derived from an input, so PineForge "
372
+ f"cannot size the indicator buffer.",
373
+ hint=("Use a literal, an input.*() value, or "
374
+ "arithmetic over those for TA lengths."),
375
+ )
211
376
  resolved = [self._resolve_known(a) for a in site.ctor_args]
212
- # If any ctor arg isn't a compile-time value, use default 1
213
- # (TA in user functions with runtime params)
377
+ # Compile-time placeholder for the init list; the runtime reset
378
+ # (when the arg is input-derived) overwrites it on the first bar.
214
379
  safe_resolved = []
215
380
  for r in resolved:
216
381
  if self._is_compile_time_value(r):
@@ -240,11 +405,24 @@ class TopLevelEmitter:
240
405
  safe = self._safe_name(name)
241
406
  if name in self._array_vars or name in self._map_vars:
242
407
  continue
408
+ # Function-scoped ``var`` members are initialized once-per-variant
409
+ # on first call (see _emit_func_var_init_block); exclude them from
410
+ # the constructor so they are not double-initialized and so their
411
+ # (possibly bar-dependent) initializer is lowered in the function's
412
+ # own scope (with its active var remap for clones).
413
+ if name in getattr(self, "_func_local_var_names", ()):
414
+ continue
243
415
  # UDT-typed var members (``var SDZone z = na``) default-construct to
244
416
  # na via the struct's in-class ``__pf_na = true``; a ctor init like
245
417
  # ``z(na<double>())`` would not type-match the struct member.
246
418
  if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
247
419
  continue
420
+ # Drawing handle var member (L-N3): ``var line x`` / ``var box b``
421
+ # default-construct to {-1} (na). A ``b(na<double>())`` ctor init
422
+ # would not type-match the handle struct — skip it (the in-class
423
+ # member default is the once-only persistent na init).
424
+ if name in self._udt_var_types and self._udt_var_types[name] in DRAWING_TYPE_TO_CPP:
425
+ continue
248
426
  if name not in self.ctx.series_vars:
249
427
  cpp_val = self._resolve_known(init_expr)
250
428
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
@@ -257,6 +435,9 @@ class TopLevelEmitter:
257
435
  if sp.get("process_orders_on_close") is True:
258
436
  ctor_body.append(" process_orders_on_close_ = true;")
259
437
 
438
+ if sp.get("calc_on_order_fills") is True:
439
+ ctor_body.append(" calc_on_order_fills_ = true;")
440
+
260
441
  if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
261
442
  ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
262
443
 
@@ -341,6 +522,7 @@ class TopLevelEmitter:
341
522
  lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
342
523
  lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
343
524
  lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
525
+ lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
344
526
  lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
345
527
  lines.append(' if (key == "default_qty_type") {')
346
528
  lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
@@ -362,20 +544,22 @@ class TopLevelEmitter:
362
544
  lines.append(" security_eval_states_.clear();")
363
545
  for info in self._security_eval_info:
364
546
  tf = info.get("tf")
365
- tf_expr = None
547
+ tf_expr = info.get("tf_expr")
366
548
  if tf:
367
549
  tf_expr = f'"{tf}"'
368
- else:
369
- tf_node = info.get("tf_node")
370
- if tf_node is not None:
371
- if isinstance(tf_node, Identifier) and tf_node.name in self._timeframe_period_vars:
372
- tf_expr = "script_tf_"
373
- else:
374
- raw_tf_expr = self._visit_expr(tf_node)
375
- tf_expr = self._runtime_ctor_arg_for_reset(raw_tf_expr) or raw_tf_expr
550
+ elif not tf_expr:
551
+ # No static tf and no resolvable runtime expression — fall
552
+ # back to the chart timeframe so registration still compiles
553
+ # (e.g. a request.security inside a dead-code UDF, or one
554
+ # whose tf is a function param called with mixed timeframes).
555
+ tf_expr = "input_tf_"
376
556
  if tf_expr:
377
557
  la = "true" if info["lookahead_on"] else "false"
378
558
  go = "true" if info.get("gaps_on") else "false"
559
+ # Heikin-Ashi same-symbol read: emit the 6th arg only when set
560
+ # so every non-HA strategy's generated code stays byte-identical
561
+ # (the engine param defaults to false).
562
+ ha_arg = ", true" if info.get("heikinashi") else ""
379
563
  sec_id = info["sec_id"]
380
564
  # The runtime registration function is named in tables.py
381
565
  # to keep a single source of truth and to avoid embedding
@@ -391,7 +575,7 @@ class TopLevelEmitter:
391
575
  lines.append(
392
576
  f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
393
577
  f"({sec_id}, {tf_expr}, "
394
- f"input_tf_, {la}, {go});")
578
+ f"input_tf_, {la}, {go}{ha_arg});")
395
579
  lines.append(" }")
396
580
 
397
581
  # Map strategy series member name to push expression
@@ -407,14 +591,45 @@ class TopLevelEmitter:
407
591
  "initial_capital": "initial_capital_",
408
592
  }
409
593
 
594
+ @staticmethod
595
+ def _emit_history_series_write(
596
+ lines: list[str], pad: str, member: str, value: str) -> None:
597
+ """Emit one Pine-series write without conflating history with isnew.
598
+
599
+ Historical fill recalculations keep ``barstate.isnew`` true, but a
600
+ post-close recalculation restored from the completed ordinary-close
601
+ checkpoint must replace that bar's current history slot rather than
602
+ append a duplicate slot. The engine exposes those independent facts
603
+ as ``is_first_tick_`` and ``history_advances_new_bar()`` respectively.
604
+ """
605
+ lines.append(
606
+ f"{pad}if (history_advances_new_bar()) {member}.push({value});"
607
+ )
608
+ lines.append(f"{pad}else {member}.update({value});")
609
+
410
610
  def _emit_on_bar(self, lines: list[str]) -> None:
411
611
  lines.append(" void on_bar(const Bar& bar) override {")
412
612
 
613
+ # reset_run_state() owns engine/broker state, while these generated
614
+ # Series members belong to the strategy object. Clear all of them on
615
+ # the first genuine history slot of bar zero, unconditionally: a site
616
+ # may live behind a branch that does not execute on bar zero. This is a
617
+ # narrow synthetic-buffer reset, not a promise that every generated
618
+ # member/init latch supports full same-handle reruns. The post-C rollback
619
+ # execution has history_advances_new_bar()==false, so it preserves the
620
+ # committed slot.
621
+ for info in self._inline_history_members:
622
+ lines.append(
623
+ " if (history_advances_new_bar() && bar_index_ == 0) "
624
+ f"{info['member_name']}.clear();"
625
+ )
626
+
413
627
  # a. Push bar field series (with bar magnifier support)
414
628
  for field_name in sorted(self.ctx.series_bar_fields):
415
629
  push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
416
- lines.append(f" if (is_first_tick_) _s_{field_name}.push({push_expr});")
417
- lines.append(f" else _s_{field_name}.update({push_expr});")
630
+ self._emit_history_series_write(
631
+ lines, " ", f"_s_{field_name}", push_expr
632
+ )
418
633
 
419
634
  # a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
420
635
  # hl2[n], …). They land in ``series_vars`` and are declared as Series
@@ -429,22 +644,25 @@ class TopLevelEmitter:
429
644
  if _bname in self._var_names:
430
645
  continue
431
646
  _bexpr = BAR_BUILTINS.get(_bname)
432
- if _bexpr is None or f"{_bname}(" in _bexpr:
647
+ if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
433
648
  continue
434
649
  _bsafe = self._safe_name(_bname)
435
- lines.append(f" if (is_first_tick_) {_bsafe}.push({_bexpr});")
436
- lines.append(f" else {_bsafe}.update({_bexpr});")
650
+ self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
437
651
 
438
652
  # a2. Push strategy series
439
653
  for svar in sorted(self._strategy_series_vars):
440
654
  member = svar.replace("_strat_", "")
441
655
  push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
442
- lines.append(f" {svar}.push({push_expr});")
656
+ self._emit_history_series_write(lines, " ", svar, push_expr)
443
657
 
444
658
  # b. Var init / carry-forward
445
659
  if self.ctx.var_members:
446
660
  lines.append(" if (!_var_initialized) {")
447
661
  for name, ptype, init_expr in self.ctx.var_members:
662
+ # Function-scoped ``var`` members are init'd once-per-variant
663
+ # on first call (see _emit_func_var_init_block), not here.
664
+ if name in getattr(self, "_func_local_var_names", ()):
665
+ continue
448
666
  safe = self._safe_name(name)
449
667
  if name in self._array_vars:
450
668
  for stmt in self.ctx.ast.body:
@@ -507,7 +725,9 @@ class TopLevelEmitter:
507
725
  if name in self._array_vars:
508
726
  continue
509
727
  if name in self.ctx.series_vars:
510
- lines.append(f" if (is_first_tick_) {safe}.push({safe}[0]);")
728
+ self._emit_history_series_write(
729
+ lines, " ", safe, f"{safe}[0]"
730
+ )
511
731
  # Also carry-forward cloned copies for per-call-site function variants
512
732
  carry_emitted: set[str] = set()
513
733
  for (fname, cs_idx), remap in self._func_cs_var_remap.items():
@@ -517,7 +737,9 @@ class TopLevelEmitter:
517
737
  cloned = remap[safe]
518
738
  if cloned not in carry_emitted:
519
739
  carry_emitted.add(cloned)
520
- lines.append(f" if (is_first_tick_) {cloned}.push({cloned}[0]);")
740
+ self._emit_history_series_write(
741
+ lines, " ", cloned, f"{cloned}[0]"
742
+ )
521
743
  lines.append(" }")
522
744
 
523
745
  # c. Push non-var series (they start fresh each bar with a push)
@@ -545,6 +767,7 @@ class TopLevelEmitter:
545
767
  default_cpp = self._visit_expr(default) if default is not None else "0"
546
768
  title = self._get_input_title(stmt.value, var_name=stmt.name)
547
769
  getter = self._input_type_to_getter(func_name_i, namespace_i)
770
+ default_cpp = self._coerce_string_input_default(getter, default_cpp)
548
771
  cpp_val = f'{getter}("{title}", {default_cpp})'
549
772
  static_vars.append(f"{safe} = {cpp_val};")
550
773
 
@@ -709,11 +932,17 @@ class TopLevelEmitter:
709
932
  base = fi.node.name if fi.node else ""
710
933
  return self._func_safe_name(f"_udt_{udt}_{base}")
711
934
 
712
- def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None) -> None:
935
+ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
936
+ instance: dict | None = None) -> None:
713
937
  """Emit a user-defined function as a class method.
714
938
 
715
939
  If call_site_idx is not None, emit a per-call-site variant with
716
940
  TA member names remapped to call-site-specific copies.
941
+
942
+ If ``instance`` is provided (a fresh context-sensitive instance minted by
943
+ ``_build_func_instances``), emit a uniquely-named clone whose TA/var
944
+ members come from the instance's composed remaps instead of the flat
945
+ ``_func_cs_*_remap`` tables.
717
946
  """
718
947
  node = fi.node
719
948
  if node is None:
@@ -724,12 +953,19 @@ class TopLevelEmitter:
724
953
  # Determine param types and set context for type inference inside body
725
954
  param_strs = []
726
955
  self._current_func_param_types = {}
956
+ self._current_func_param_specs = {}
727
957
  self._current_func_series_params = set()
728
958
  self._udt_param_udt = {}
729
959
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
730
960
  for i, p in enumerate(node.params):
961
+ spec = None
731
962
  if is_udt and i == 0 and fi.udt_type_name:
732
- cpp_t = f"{fi.udt_type_name}&"
963
+ # A method receiver whose type is a drawing primitive
964
+ # (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
965
+ # the unknown ``line&``. Register _udt_param_udt so the body's
966
+ # getters dispatch through the §4.3 drawing path (L.6d / U.5).
967
+ recv_cpp = DRAWING_TYPE_TO_CPP.get(fi.udt_type_name, fi.udt_type_name)
968
+ cpp_t = f"{recv_cpp}&"
733
969
  safe_p = self._safe_name(p)
734
970
  self._udt_param_udt[safe_p] = fi.udt_type_name
735
971
  self._udt_param_udt[p] = fi.udt_type_name
@@ -739,6 +975,24 @@ class TopLevelEmitter:
739
975
  # This param uses history access (e.g. src[1]) — pass as Series
740
976
  cpp_t = "const Series<double>&"
741
977
  self._current_func_series_params.add(p)
978
+ elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
979
+ # Precise per-param TypeSpec (declared hint or call-site inference):
980
+ # ``pivot hi`` -> ``pivot&``, ``line ln`` -> ``Line&``, an untyped
981
+ # ``s`` used as a string -> ``std::string``. UDT / collection
982
+ # params pass by reference (Pine UDTs/arrays are reference types,
983
+ # so mutations propagate and member access compiles).
984
+ spec = fi.param_type_specs[i]
985
+ cpp_t = self._type_spec_to_cpp(spec)
986
+ if spec.kind == "udt":
987
+ self._udt_param_udt[p] = spec.name
988
+ self._udt_param_udt[self._safe_name(p)] = spec.name
989
+ cpp_t = f"{cpp_t}&"
990
+ elif spec.kind in ("array", "map"):
991
+ elem = spec.element if spec.kind == "array" else spec.value
992
+ if elem is not None and elem.kind == "udt":
993
+ self._udt_param_udt[p] = elem.name
994
+ self._udt_param_udt[self._safe_name(p)] = elem.name
995
+ cpp_t = f"{cpp_t}&"
742
996
  elif i < len(fi.param_types):
743
997
  pt = fi.param_types[i]
744
998
  cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
@@ -746,6 +1000,9 @@ class TopLevelEmitter:
746
1000
  cpp_t = "double"
747
1001
  param_strs.append(f"{cpp_t} {self._safe_name(p)}")
748
1002
  self._current_func_param_types[p] = cpp_t
1003
+ if spec is not None:
1004
+ self._current_func_param_specs[p] = spec
1005
+ self._current_func_param_specs[self._safe_name(p)] = spec
749
1006
 
750
1007
  # Determine return type: tuple, UDT, or scalar.
751
1008
  # The UDT branch handles user functions whose body is ``T.new(...)``;
@@ -757,28 +1014,69 @@ class TopLevelEmitter:
757
1014
  tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
758
1015
  ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
759
1016
  elif getattr(fi, "udt_return_type", None):
760
- ret_type = fi.udt_return_type
1017
+ # A function returning a drawing handle must emit the C++ handle
1018
+ # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
1019
+ ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
1020
+ elif getattr(fi, "return_type_spec", None) is not None:
1021
+ # Array-returning function (``f() => array.from(...)``) — emit the
1022
+ # vector type from the inferred element TypeSpec.
1023
+ ret_type = self._type_spec_to_cpp(fi.return_type_spec)
761
1024
  else:
762
1025
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
763
1026
 
764
1027
  # For per-call-site variants, suffix the function name and activate TA + var remapping
765
1028
  func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
766
- if call_site_idx is not None:
1029
+ var_init_flag: str | None = None
1030
+ if instance is not None:
1031
+ # Fresh context-sensitive instance: name + composed remaps come from
1032
+ # the instance record. No textual cs index (dispatch is via the
1033
+ # instance map), but it IS a state-isolated variant.
1034
+ func_name = instance["name"]
1035
+ self._active_ta_remap = instance["ta_remap"]
1036
+ self._active_var_remap = instance["var_remap"]
1037
+ self._active_fixnan_remap = instance.get("fixnan_remap", {})
1038
+ self._in_ta_func_variant = True
1039
+ self._active_call_site_idx = None
1040
+ self._current_instance_name = instance["name"]
1041
+ var_init_flag = f"_fvinit_{instance['name']}"
1042
+ elif call_site_idx is not None:
767
1043
  func_name = f"{func_name}_cs{call_site_idx}"
768
1044
  remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
769
1045
  self._active_ta_remap = remap
770
1046
  var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
771
1047
  self._active_var_remap = var_remap
1048
+ self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
772
1049
  self._in_ta_func_variant = True
773
1050
  self._active_call_site_idx = call_site_idx
1051
+ # Use the actual emitted name. Plain UDFs are unchanged; UDT
1052
+ # methods carry their `_udt_Type_method` prefix. This identity is
1053
+ # shared with _build_func_instances and synthetic-history member
1054
+ # registration, so method call paths can dispatch independently.
1055
+ self._current_instance_name = func_name
774
1056
  else:
775
1057
  self._active_ta_remap = {}
776
1058
  self._active_var_remap = {}
1059
+ self._active_fixnan_remap = {}
777
1060
  self._in_ta_func_variant = False
778
1061
  self._active_call_site_idx = None
1062
+ self._current_instance_name = None
779
1063
 
780
1064
  prev_func_locals = self._current_func_locals
1065
+ prev_func_local_types = self._current_func_local_types
1066
+ prev_func_body = getattr(self, "_current_func_body", None)
1067
+ prev_func_name = getattr(self, "_active_func_name", None)
1068
+ # The function body is the lexical scope used by the UDT-alias analysis
1069
+ # (BUG C): a local initialised from a var/global UDT lvalue and later
1070
+ # mutated through must alias, not value-copy.
1071
+ self._current_func_body = node.body
1072
+ self._active_func_name = fi.name
1073
+ # Pointer-aliased UDT locals are function-scoped: a name like ``p_ivot``
1074
+ # may be a rebinding pointer alias in one function and a ``pivot&``
1075
+ # parameter in another, so reset per function to avoid cross-contamination.
1076
+ prev_ptr_alias = self._udt_ptr_alias_locals
1077
+ self._udt_ptr_alias_locals = set()
781
1078
  self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
1079
+ self._current_func_local_types = {}
782
1080
  # Plain (non-persistent) scalar locals are emitted inline and live in
783
1081
  # no other set; collect them so the unknown-identifier guard in
784
1082
  # _visit_ident does not mistake them for undeclared symbols.
@@ -786,24 +1084,47 @@ class TopLevelEmitter:
786
1084
 
787
1085
  lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
788
1086
 
1087
+ # Function-scoped ``var`` one-shot initializer: Pine ``var`` inside a
1088
+ # function is a function-local static — its initializer runs exactly
1089
+ # once, on the first call to THIS variant, with the first bar's values
1090
+ # the function actually sees. Each clone (cs0/cs1/…) is independent.
1091
+ self._emit_func_var_init_block(fi, call_site_idx, lines, flag_override=var_init_flag)
1092
+
789
1093
  emitted_return = False
790
1094
  if node.is_single_expr and node.body:
791
1095
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
792
- if expr:
1096
+ if expr and self._call_is_void(expr):
1097
+ # void setter as the sole body expr — emit as statement, fall
1098
+ # through to the default return.
1099
+ self._visit_stmt(node.body[0], lines, indent=2)
1100
+ elif expr:
793
1101
  lines.append(f" return {self._visit_expr(expr)};")
794
1102
  emitted_return = True
795
1103
  else:
796
1104
  for i, s in enumerate(node.body):
797
1105
  if i == len(node.body) - 1 and isinstance(s, ExprStmt):
798
- lines.append(f" return {self._visit_expr(s.expr)};")
799
- emitted_return = True
1106
+ # A void drawing setter / delete / visual-noop, or a dropped
1107
+ # table/polyline method call (``panel.cell(...)``), used as
1108
+ # the last statement cannot be the return value (it lowers to
1109
+ # a void / no-op C++ call). Emit it as a plain statement
1110
+ # (which ``_is_skip_expr`` drops) and let the default-return
1111
+ # path below supply the function's result.
1112
+ if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
1113
+ self._visit_stmt(s, lines, indent=2)
1114
+ else:
1115
+ lines.append(f" return {self._visit_expr(s.expr)};")
1116
+ emitted_return = True
800
1117
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
801
1118
  # Switch/if as last statement = return expression in PineScript
802
1119
  # Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
803
- default_ret = (
804
- f"{ret_type}{{}}" if ret_type in self._udt_defs
805
- else self._default_for_type(ret_type)
806
- )
1120
+ # A drawing-handle / UDT return type must brace-init its
1121
+ # default (``Label _func_ret = Label{};``) — falling through
1122
+ # to ``_default_for_type`` would emit ``0.0`` and clang would
1123
+ # reject ``Label _func_ret = 0.0;``.
1124
+ if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
1125
+ default_ret = f"{ret_type}{{}}"
1126
+ else:
1127
+ default_ret = self._default_for_type(ret_type)
807
1128
  lines.append(f" {ret_type} _func_ret = {default_ret};")
808
1129
  self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
809
1130
  lines.append(f" return _func_ret;")
@@ -818,41 +1139,130 @@ class TopLevelEmitter:
818
1139
  default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
819
1140
  lines.append(f" return std::make_tuple({default_vals});")
820
1141
  else:
821
- default_ret = (
822
- f"{ret_type}{{}}" if ret_type in self._udt_defs
823
- else self._default_for_type(ret_type)
824
- )
1142
+ if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
1143
+ default_ret = f"{ret_type}{{}}"
1144
+ else:
1145
+ default_ret = self._default_for_type(ret_type)
825
1146
  lines.append(f" return {default_ret};")
826
1147
 
827
1148
  lines.append(" }")
828
1149
  self._current_func_param_types = {}
1150
+ self._current_func_param_specs = {}
829
1151
  self._current_func_series_params = set()
830
1152
  self._udt_param_udt = {}
831
1153
  self._current_func_locals = prev_func_locals
1154
+ self._current_func_local_types = prev_func_local_types
1155
+ self._current_func_body = prev_func_body
1156
+ self._active_func_name = prev_func_name
1157
+ self._udt_ptr_alias_locals = prev_ptr_alias
832
1158
  self._active_ta_remap = {}
833
1159
  self._active_var_remap = {}
1160
+ self._active_fixnan_remap = {}
834
1161
  self._in_ta_func_variant = False
835
1162
  self._active_call_site_idx = None
1163
+ self._current_instance_name = None
1164
+
1165
+ def _func_var_init_flag_name(self, fname: str, call_site_idx: int | None) -> str:
1166
+ suffix = f"_cs{call_site_idx}" if call_site_idx is not None else ""
1167
+ return f"_fvinit_{self._func_safe_name(fname)}{suffix}"
1168
+
1169
+ def _emit_func_var_init_block(self, fi: FuncInfo, call_site_idx: int | None,
1170
+ lines: list[str], flag_override: str | None = None) -> None:
1171
+ """Emit the one-shot initializer block for a function's ``var`` members.
1172
+
1173
+ Pine ``var`` declared inside a function is a function-local static:
1174
+ the initializer runs exactly once on the FIRST call to this variant
1175
+ (using the first bar's values the function actually sees) and the
1176
+ result persists for the strategy's lifetime. Each per-call-site clone
1177
+ is an independent instance with its own flag and its own set of
1178
+ (remapped) members.
1179
+
1180
+ This closes the gap where a function-scoped ``var line x = line.new(...)``
1181
+ (or any non-compile-time initializer — drawing handles, UDT ctors,
1182
+ arrays, runtime expressions) was declared as a default-constructed
1183
+ class member but its initializer was dropped, leaving the member ``na``
1184
+ / uninitialised and causing "drawing access on na handle" at runtime.
1185
+ """
1186
+ members = self.ctx.func_var_members.get(fi.name)
1187
+ if not members:
1188
+ return
1189
+ flag = flag_override or self._func_var_init_flag_name(fi.name, call_site_idx)
1190
+ # ``_active_var_remap`` is already set for this variant by the caller,
1191
+ # so lowering each init expression here correctly resolves references
1192
+ # to sibling var members (which are themselves remapped for clones).
1193
+ init_lines: list[str] = []
1194
+ for name, ptype, _init_str in members:
1195
+ init_ast = self.ctx.var_member_init_exprs.get(name)
1196
+ safe = self._safe_name(name)
1197
+ target = self._active_var_remap.get(safe, safe)
1198
+ if name in self.ctx.series_vars:
1199
+ if init_ast is None:
1200
+ continue
1201
+ init_cpp = self._visit_expr(init_ast)
1202
+ init_cpp = self._typed_na_init(init_cpp, name, ptype)
1203
+ init_lines.append(f" {target}.push({init_cpp});")
1204
+ continue
1205
+ if init_ast is None:
1206
+ # No initializer to lower (e.g. bare ``var box b``); leave the
1207
+ # member at its default-constructed value.
1208
+ continue
1209
+ # Skip a plain ``na`` initializer for drawing handles / UDTs whose
1210
+ # default-constructed member is already the na sentinel; assigning
1211
+ # ``na<double>()`` would not type-match the handle / struct.
1212
+ udt_t = self._udt_var_types.get(name)
1213
+ is_drawing = udt_t in DRAWING_TYPE_TO_CPP if udt_t else False
1214
+ is_udt = udt_t in self._udt_defs if udt_t else False
1215
+ from ..ast_nodes import NaLiteral
1216
+ if (is_drawing or is_udt) and isinstance(init_ast, NaLiteral):
1217
+ continue
1218
+ init_cpp = self._visit_expr(init_ast)
1219
+ init_lines.append(f" {target} = {init_cpp};")
1220
+ if not init_lines:
1221
+ return
1222
+ lines.append(f" if (!{flag}) {{")
1223
+ lines.extend(init_lines)
1224
+ lines.append(f" {flag} = true;")
1225
+ lines.append(" }")
836
1226
 
837
1227
  def _emit_precalculate_and_run(self, lines: list[str]) -> None:
838
- has_static_ta = any(getattr(site, "is_static", False) for site in self.ctx.ta_call_sites)
1228
+ has_static_ta = any(
1229
+ self._ta_site_uses_precalc(site)
1230
+ for _ti, site in enumerate(self.ctx.ta_call_sites)
1231
+ if _ti not in self._dead_ta_indices
1232
+ )
839
1233
  if not has_static_ta:
840
1234
  return
841
1235
 
1236
+ replayed_source_series: list[str] = []
1237
+ for stmt in self.ctx.ast.body:
1238
+ if not isinstance(stmt, VarDecl):
1239
+ continue
1240
+ if stmt.name not in self._global_member_vars:
1241
+ continue
1242
+ if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1243
+ continue
1244
+ if stmt.name in self.ctx.series_vars:
1245
+ replayed_source_series.append(self._safe_name(stmt.name))
1246
+ replayed_source_series = sorted(set(replayed_source_series))
1247
+
842
1248
  lines.append(" void precalculate(const Bar* bars, int n) {")
843
1249
  lines.append(" _use_precalc = false;")
844
1250
  lines.append(" if (n <= 0 || bars == nullptr) return;")
845
1251
  lines.append("")
846
1252
 
847
1253
  # Resize precalculated vectors
848
- for site in self.ctx.ta_call_sites:
849
- if getattr(site, "is_static", False):
1254
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1255
+ if _ti in self._dead_ta_indices:
1256
+ continue
1257
+ if self._ta_site_uses_precalc(site):
850
1258
  lines.append(f" _precalc_{site.member_name}.resize(n);")
851
1259
 
852
1260
  # Reset indicators to clean slate
853
1261
  lines.append("")
854
- for site in self.ctx.ta_call_sites:
855
- if getattr(site, "is_static", False):
1262
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1263
+ if _ti in self._dead_ta_indices:
1264
+ continue
1265
+ if self._ta_site_uses_precalc(site):
856
1266
  resolved = [self._resolve_known(a) for a in site.ctor_args]
857
1267
  safe_resolved = []
858
1268
  for r in resolved:
@@ -863,6 +1273,13 @@ class TopLevelEmitter:
863
1273
  lines.append("")
864
1274
  for field_name in sorted(self.ctx.series_bar_fields):
865
1275
  lines.append(f" _s_{field_name}.clear();")
1276
+ for safe in replayed_source_series:
1277
+ lines.append(f" {safe}.clear();")
1278
+ if self._script_has_input_source():
1279
+ lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
1280
+ lines.append(" _src_close_.clear(); _src_volume_.clear();")
1281
+ lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
1282
+ lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
866
1283
 
867
1284
  # Start precalculation loop
868
1285
  lines.append("")
@@ -874,11 +1291,76 @@ class TopLevelEmitter:
874
1291
  push_expr_bars = push_expr.replace("current_bar_.", "bars[i].")
875
1292
  lines.append(f" _s_{field_name}.push({push_expr_bars});")
876
1293
 
1294
+ # Advance the native input.source() backing series (_src_open_ etc.)
1295
+ # from bars[i] too. A static TA site's compute args can reference an
1296
+ # input.source()-derived member (e.g. ``ta.stdev(bbSourceInput, 20)``)
1297
+ # which resolves at runtime to ``get_input_source(...)`` reading one
1298
+ # of these series — normally advanced once per real bar by
1299
+ # ``_push_source_series()`` inside ``dispatch_bar()``, which this
1300
+ # standalone precalc loop never calls. Without this, every static TA
1301
+ # site fed by an input.source() reads an empty series (0.0) for the
1302
+ # entire precalculation, silently corrupting its precalculated
1303
+ # values (e.g. a Bollinger Band's stdev collapsing to 0). Gated on
1304
+ # ``_src_series_active_`` to stay a no-op for scripts with no
1305
+ # input.source() usage; cleared before and after the precalc pass so
1306
+ # replayed source history cannot leak into the real run.
1307
+ lines.append(" if (_src_series_active_) {")
1308
+ lines.append(" const double _pc_o = bars[i].open;")
1309
+ lines.append(" const double _pc_h = bars[i].high;")
1310
+ lines.append(" const double _pc_l = bars[i].low;")
1311
+ lines.append(" const double _pc_c = bars[i].close;")
1312
+ lines.append(" const double _pc_v = bars[i].volume;")
1313
+ lines.append(" _src_open_.push(_pc_o); _src_high_.push(_pc_h); _src_low_.push(_pc_l);")
1314
+ lines.append(" _src_close_.push(_pc_c); _src_volume_.push(_pc_v);")
1315
+ lines.append(" _src_hl2_.push((_pc_h + _pc_l) / 2.0);")
1316
+ lines.append(" _src_hlc3_.push((_pc_h + _pc_l + _pc_c) / 3.0);")
1317
+ lines.append(" _src_ohlc4_.push((_pc_o + _pc_h + _pc_l + _pc_c) / 4.0);")
1318
+ lines.append(" _src_hlcc4_.push((_pc_h + _pc_l + _pc_c + _pc_c) / 4.0);")
1319
+ lines.append(" }")
1320
+
1321
+ # Replay every top-level ``X = input.source(...)`` (or bare
1322
+ # ``X = input(close)``) assignment. A static TA site's compute args
1323
+ # often don't reference ``get_input_source(...)`` inline — they
1324
+ # reference the top-level variable the script bound it to (e.g.
1325
+ # ``bbSourceInput = input.source(close, "BB Source")`` then
1326
+ # ``ta.stdev(bbSourceInput, 20)``). That variable is deliberately
1327
+ # NOT covered by the ``_inputs_initialized_`` once-only static-input
1328
+ # block above (see ``is_static_global_input``'s ``_is_source_input``
1329
+ # exclusion) because it tracks a live per-bar series, not a frozen
1330
+ # config value — under normal per-bar dispatch it is reassigned every
1331
+ # real bar. This precalc loop has no other path that reassigns it, so
1332
+ # without this replay every is_static site downstream of it would
1333
+ # keep reading its ctor-initialized 0.0 for the whole precalculation
1334
+ # even with the ``_src_*_`` fix above.
1335
+ for stmt in self.ctx.ast.body:
1336
+ if not isinstance(stmt, VarDecl):
1337
+ continue
1338
+ if stmt.name not in self._global_member_vars:
1339
+ continue
1340
+ if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1341
+ continue
1342
+ safe = self._safe_name(stmt.name)
1343
+ default = self._get_input_default(stmt.value)
1344
+ base = self._source_defval_to_base_series(default)
1345
+ title = self._get_input_title(stmt.value, var_name=stmt.name)
1346
+ cpp_val = f'get_input_source("{title}", {base})[0]'
1347
+ # A source var subscripted elsewhere in the script (e.g. ``src[1]``)
1348
+ # is declared ``Series<double>``, not a scalar double, mirroring
1349
+ # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
1350
+ # ``_visit_var_decl``'s ``node.name in self.ctx.series_vars``
1351
+ # branch) — a plain ``=`` there is a compile error.
1352
+ if stmt.name in self.ctx.series_vars:
1353
+ lines.append(f' {safe}.push({cpp_val});')
1354
+ else:
1355
+ lines.append(f' {safe} = {cpp_val};')
1356
+
877
1357
  # Set _precalc_loop_active = True
878
1358
  self._precalc_loop_active = True
879
1359
  try:
880
- for site in self.ctx.ta_call_sites:
881
- if getattr(site, "is_static", False):
1360
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1361
+ if _ti in self._dead_ta_indices:
1362
+ continue
1363
+ if self._ta_site_uses_precalc(site):
882
1364
  compute_args = self._ta_compute_args_for_site(site)
883
1365
  compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
884
1366
  lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
@@ -889,8 +1371,10 @@ class TopLevelEmitter:
889
1371
 
890
1372
  # Reset indicators and series for the real backtest run
891
1373
  lines.append("")
892
- for site in self.ctx.ta_call_sites:
893
- if getattr(site, "is_static", False):
1374
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1375
+ if _ti in self._dead_ta_indices:
1376
+ continue
1377
+ if self._ta_site_uses_precalc(site):
894
1378
  resolved = [self._resolve_known(a) for a in site.ctor_args]
895
1379
  safe_resolved = []
896
1380
  for r in resolved:
@@ -899,6 +1383,13 @@ class TopLevelEmitter:
899
1383
 
900
1384
  for field_name in sorted(self.ctx.series_bar_fields):
901
1385
  lines.append(f" _s_{field_name}.clear();")
1386
+ for safe in replayed_source_series:
1387
+ lines.append(f" {safe}.clear();")
1388
+ if self._script_has_input_source():
1389
+ lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
1390
+ lines.append(" _src_close_.clear(); _src_volume_.clear();")
1391
+ lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
1392
+ lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
902
1393
 
903
1394
  lines.append("")
904
1395
  lines.append(" _use_precalc = true;")