@pineforge/codegen-pyodide 0.7.6 → 0.8.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.
@@ -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,
@@ -136,6 +137,11 @@ class TopLevelEmitter:
136
137
  float_spec = TypeSpec.primitive("float")
137
138
  if any(spec.element != float_spec for spec in self._matrix_specs.values()):
138
139
  lines.append('#include <pineforge/generic_matrix.hpp>')
140
+ # Drawing-objects-as-data runtime (line/box/label/linefill arenas +
141
+ # ChartPoint). Gated on _uses_drawing so non-drawing strategies stay
142
+ # byte-identical — mirrors the matrix.hpp gating above.
143
+ if getattr(self, "_uses_drawing", False):
144
+ lines.append('#include <pineforge/drawing.hpp>')
139
145
  lines.append("")
140
146
  # Compatibility shim for the namespace-wrap refactor: unqualified
141
147
  # references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
@@ -182,19 +188,55 @@ class TopLevelEmitter:
182
188
  for node in self._walk_ast(self.ctx.ast):
183
189
  if not isinstance(node, FuncCall):
184
190
  continue
185
- func_name, namespace = self._resolve_callee(node.callee)
186
- if namespace == "input" and func_name == "source":
191
+ if self._is_source_input(node):
187
192
  return True
188
193
  return False
189
194
 
195
+ def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
196
+ """Re-type a bare ``na<double>()`` initializer to match a non-double
197
+ member's C++ type. A ``var int x = na`` resolves its RHS to
198
+ ``na<double>()`` (a quiet NaN); constructing/pushing that into an int or
199
+ bool member is a NaN->int conversion (UB) that yields garbage and defeats
200
+ ``is_na<T>()`` (which checks the type sentinel, e.g. INT_MIN). Returns the
201
+ value unchanged unless it is exactly ``na<double>()`` and the member type
202
+ is non-double."""
203
+ if cpp_val != "na<double>()":
204
+ return cpp_val
205
+ cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
206
+ if cpp_type == "int" and self._is_int64_builtin_init(name):
207
+ cpp_type = "int64_t"
208
+ if cpp_type == "double":
209
+ return cpp_val
210
+ return f"na<{cpp_type}>()"
211
+
190
212
  def _emit_constructor(self, lines: list[str]) -> None:
191
213
  init_parts: list[str] = []
192
214
  # TA members with ctor args
193
215
  for site in self.ctx.ta_call_sites:
194
216
  if site.ctor_args:
217
+ # If a ctor arg is neither a compile-time literal nor expandable
218
+ # to an input-backed runtime expression, the old code silently
219
+ # emitted period 1 with no overwriting reset — a wrong indicator
220
+ # masquerading as a working one. Refuse loudly instead. Args that
221
+ # DO expand to a runtime expr (input-backed / arithmetic-over-input,
222
+ # incl. function-derived lengths) are safe: the `!_ta_initialized_`
223
+ # reset overwrites the placeholder before the first compute.
224
+ for a in site.ctor_args:
225
+ r = self._resolve_known(a)
226
+ if (not self._is_compile_time_value(r)
227
+ and self._runtime_ctor_arg_for_reset(a) is None):
228
+ self._codegen_error(
229
+ getattr(site, "node", None),
230
+ f"Unsupported TA constructor length '{a}' for "
231
+ f"{site.class_name}: it is neither a compile-time "
232
+ f"constant nor derived from an input, so PineForge "
233
+ f"cannot size the indicator buffer.",
234
+ hint=("Use a literal, an input.*() value, or "
235
+ "arithmetic over those for TA lengths."),
236
+ )
195
237
  resolved = [self._resolve_known(a) for a in site.ctor_args]
196
- # If any ctor arg isn't a compile-time value, use default 1
197
- # (TA in user functions with runtime params)
238
+ # Compile-time placeholder for the init list; the runtime reset
239
+ # (when the arg is input-derived) overwrites it on the first bar.
198
240
  safe_resolved = []
199
241
  for r in resolved:
200
242
  if self._is_compile_time_value(r):
@@ -224,8 +266,27 @@ class TopLevelEmitter:
224
266
  safe = self._safe_name(name)
225
267
  if name in self._array_vars or name in self._map_vars:
226
268
  continue
269
+ # Function-scoped ``var`` members are initialized once-per-variant
270
+ # on first call (see _emit_func_var_init_block); exclude them from
271
+ # the constructor so they are not double-initialized and so their
272
+ # (possibly bar-dependent) initializer is lowered in the function's
273
+ # own scope (with its active var remap for clones).
274
+ if name in getattr(self, "_func_local_var_names", ()):
275
+ continue
276
+ # UDT-typed var members (``var SDZone z = na``) default-construct to
277
+ # na via the struct's in-class ``__pf_na = true``; a ctor init like
278
+ # ``z(na<double>())`` would not type-match the struct member.
279
+ if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
280
+ continue
281
+ # Drawing handle var member (L-N3): ``var line x`` / ``var box b``
282
+ # default-construct to {-1} (na). A ``b(na<double>())`` ctor init
283
+ # would not type-match the handle struct — skip it (the in-class
284
+ # member default is the once-only persistent na init).
285
+ if name in self._udt_var_types and self._udt_var_types[name] in DRAWING_TYPE_TO_CPP:
286
+ continue
227
287
  if name not in self.ctx.series_vars:
228
288
  cpp_val = self._resolve_known(init_expr)
289
+ cpp_val = self._typed_na_init(cpp_val, name, ptype)
229
290
  if self._is_compile_time_value(cpp_val):
230
291
  init_parts.append(f"{safe}({cpp_val})")
231
292
  # Strategy params that map to engine members
@@ -340,20 +401,22 @@ class TopLevelEmitter:
340
401
  lines.append(" security_eval_states_.clear();")
341
402
  for info in self._security_eval_info:
342
403
  tf = info.get("tf")
343
- tf_expr = None
404
+ tf_expr = info.get("tf_expr")
344
405
  if tf:
345
406
  tf_expr = f'"{tf}"'
346
- else:
347
- tf_node = info.get("tf_node")
348
- if tf_node is not None:
349
- if isinstance(tf_node, Identifier) and tf_node.name in self._timeframe_period_vars:
350
- tf_expr = "script_tf_"
351
- else:
352
- raw_tf_expr = self._visit_expr(tf_node)
353
- tf_expr = self._runtime_ctor_arg_for_reset(raw_tf_expr) or raw_tf_expr
407
+ elif not tf_expr:
408
+ # No static tf and no resolvable runtime expression — fall
409
+ # back to the chart timeframe so registration still compiles
410
+ # (e.g. a request.security inside a dead-code UDF, or one
411
+ # whose tf is a function param called with mixed timeframes).
412
+ tf_expr = "input_tf_"
354
413
  if tf_expr:
355
414
  la = "true" if info["lookahead_on"] else "false"
356
415
  go = "true" if info.get("gaps_on") else "false"
416
+ # Heikin-Ashi same-symbol read: emit the 6th arg only when set
417
+ # so every non-HA strategy's generated code stays byte-identical
418
+ # (the engine param defaults to false).
419
+ ha_arg = ", true" if info.get("heikinashi") else ""
357
420
  sec_id = info["sec_id"]
358
421
  # The runtime registration function is named in tables.py
359
422
  # to keep a single source of truth and to avoid embedding
@@ -369,7 +432,7 @@ class TopLevelEmitter:
369
432
  lines.append(
370
433
  f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
371
434
  f"({sec_id}, {tf_expr}, "
372
- f"input_tf_, {la}, {go});")
435
+ f"input_tf_, {la}, {go}{ha_arg});")
373
436
  lines.append(" }")
374
437
 
375
438
  # Map strategy series member name to push expression
@@ -394,6 +457,25 @@ class TopLevelEmitter:
394
457
  lines.append(f" if (is_first_tick_) _s_{field_name}.push({push_expr});")
395
458
  lines.append(f" else _s_{field_name}.update({push_expr});")
396
459
 
460
+ # a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
461
+ # hl2[n], …). They land in ``series_vars`` and are declared as Series
462
+ # members (base.py section 6) but — unlike user series vars (pushed at
463
+ # their assignment) and bar fields (pushed above) — have no push site,
464
+ # so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
465
+ # Push each from its scalar lowering. A builtin whose lowering is a
466
+ # self-referential call (e.g. ``time_close`` -> ``time_close()``) is
467
+ # skipped — the call would resolve to the shadowing Series member.
468
+ from .tables import BAR_BUILTINS
469
+ for _bname in sorted(self.ctx.series_vars):
470
+ if _bname in self._var_names:
471
+ continue
472
+ _bexpr = BAR_BUILTINS.get(_bname)
473
+ if _bexpr is None or f"{_bname}(" in _bexpr:
474
+ continue
475
+ _bsafe = self._safe_name(_bname)
476
+ lines.append(f" if (is_first_tick_) {_bsafe}.push({_bexpr});")
477
+ lines.append(f" else {_bsafe}.update({_bexpr});")
478
+
397
479
  # a2. Push strategy series
398
480
  for svar in sorted(self._strategy_series_vars):
399
481
  member = svar.replace("_strat_", "")
@@ -404,6 +486,10 @@ class TopLevelEmitter:
404
486
  if self.ctx.var_members:
405
487
  lines.append(" if (!_var_initialized) {")
406
488
  for name, ptype, init_expr in self.ctx.var_members:
489
+ # Function-scoped ``var`` members are init'd once-per-variant
490
+ # on first call (see _emit_func_var_init_block), not here.
491
+ if name in getattr(self, "_func_local_var_names", ()):
492
+ continue
407
493
  safe = self._safe_name(name)
408
494
  if name in self._array_vars:
409
495
  for stmt in self.ctx.ast.body:
@@ -447,6 +533,7 @@ class TopLevelEmitter:
447
533
  continue
448
534
  if name in self.ctx.series_vars:
449
535
  cpp_val = self._resolve_known(init_expr)
536
+ cpp_val = self._typed_na_init(cpp_val, name, ptype)
450
537
  lines.append(f" {safe}.push({cpp_val});")
451
538
  # Also init cloned copies for per-call-site function variants
452
539
  init_emitted: set[str] = set()
@@ -490,7 +577,7 @@ class TopLevelEmitter:
490
577
  func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
491
578
  is_static_global_input = (
492
579
  stmt.name in self._global_member_vars
493
- and func_name_i != "source"
580
+ and not self._is_source_input(stmt.value)
494
581
  and stmt.name not in self._array_vars
495
582
  and stmt.name not in getattr(self, "_matrix_specs", {})
496
583
  and stmt.name not in getattr(self, "_map_vars", {})
@@ -503,6 +590,7 @@ class TopLevelEmitter:
503
590
  default_cpp = self._visit_expr(default) if default is not None else "0"
504
591
  title = self._get_input_title(stmt.value, var_name=stmt.name)
505
592
  getter = self._input_type_to_getter(func_name_i, namespace_i)
593
+ default_cpp = self._coerce_string_input_default(getter, default_cpp)
506
594
  cpp_val = f'{getter}("{title}", {default_cpp})'
507
595
  static_vars.append(f"{safe} = {cpp_val};")
508
596
 
@@ -667,11 +755,17 @@ class TopLevelEmitter:
667
755
  base = fi.node.name if fi.node else ""
668
756
  return self._func_safe_name(f"_udt_{udt}_{base}")
669
757
 
670
- def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None) -> None:
758
+ def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
759
+ instance: dict | None = None) -> None:
671
760
  """Emit a user-defined function as a class method.
672
761
 
673
762
  If call_site_idx is not None, emit a per-call-site variant with
674
763
  TA member names remapped to call-site-specific copies.
764
+
765
+ If ``instance`` is provided (a fresh context-sensitive instance minted by
766
+ ``_build_func_instances``), emit a uniquely-named clone whose TA/var
767
+ members come from the instance's composed remaps instead of the flat
768
+ ``_func_cs_*_remap`` tables.
675
769
  """
676
770
  node = fi.node
677
771
  if node is None:
@@ -687,7 +781,12 @@ class TopLevelEmitter:
687
781
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
688
782
  for i, p in enumerate(node.params):
689
783
  if is_udt and i == 0 and fi.udt_type_name:
690
- cpp_t = f"{fi.udt_type_name}&"
784
+ # A method receiver whose type is a drawing primitive
785
+ # (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
786
+ # the unknown ``line&``. Register _udt_param_udt so the body's
787
+ # getters dispatch through the §4.3 drawing path (L.6d / U.5).
788
+ recv_cpp = DRAWING_TYPE_TO_CPP.get(fi.udt_type_name, fi.udt_type_name)
789
+ cpp_t = f"{recv_cpp}&"
691
790
  safe_p = self._safe_name(p)
692
791
  self._udt_param_udt[safe_p] = fi.udt_type_name
693
792
  self._udt_param_udt[p] = fi.udt_type_name
@@ -697,6 +796,24 @@ class TopLevelEmitter:
697
796
  # This param uses history access (e.g. src[1]) — pass as Series
698
797
  cpp_t = "const Series<double>&"
699
798
  self._current_func_series_params.add(p)
799
+ elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
800
+ # Precise per-param TypeSpec (declared hint or call-site inference):
801
+ # ``pivot hi`` -> ``pivot&``, ``line ln`` -> ``Line&``, an untyped
802
+ # ``s`` used as a string -> ``std::string``. UDT / collection
803
+ # params pass by reference (Pine UDTs/arrays are reference types,
804
+ # so mutations propagate and member access compiles).
805
+ spec = fi.param_type_specs[i]
806
+ cpp_t = self._type_spec_to_cpp(spec)
807
+ if spec.kind == "udt":
808
+ self._udt_param_udt[p] = spec.name
809
+ self._udt_param_udt[self._safe_name(p)] = spec.name
810
+ cpp_t = f"{cpp_t}&"
811
+ elif spec.kind in ("array", "map"):
812
+ elem = spec.element if spec.kind == "array" else spec.value
813
+ if elem is not None and elem.kind == "udt":
814
+ self._udt_param_udt[p] = elem.name
815
+ self._udt_param_udt[self._safe_name(p)] = elem.name
816
+ cpp_t = f"{cpp_t}&"
700
817
  elif i < len(fi.param_types):
701
818
  pt = fi.param_types[i]
702
819
  cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
@@ -715,13 +832,31 @@ class TopLevelEmitter:
715
832
  tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
716
833
  ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
717
834
  elif getattr(fi, "udt_return_type", None):
718
- ret_type = fi.udt_return_type
835
+ # A function returning a drawing handle must emit the C++ handle
836
+ # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
837
+ ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
838
+ elif getattr(fi, "return_type_spec", None) is not None:
839
+ # Array-returning function (``f() => array.from(...)``) — emit the
840
+ # vector type from the inferred element TypeSpec.
841
+ ret_type = self._type_spec_to_cpp(fi.return_type_spec)
719
842
  else:
720
843
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
721
844
 
722
845
  # For per-call-site variants, suffix the function name and activate TA + var remapping
723
846
  func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
724
- if call_site_idx is not None:
847
+ var_init_flag: str | None = None
848
+ if instance is not None:
849
+ # Fresh context-sensitive instance: name + composed remaps come from
850
+ # the instance record. No textual cs index (dispatch is via the
851
+ # instance map), but it IS a state-isolated variant.
852
+ func_name = instance["name"]
853
+ self._active_ta_remap = instance["ta_remap"]
854
+ self._active_var_remap = instance["var_remap"]
855
+ self._in_ta_func_variant = True
856
+ self._active_call_site_idx = None
857
+ self._current_instance_name = instance["name"]
858
+ var_init_flag = f"_fvinit_{instance['name']}"
859
+ elif call_site_idx is not None:
725
860
  func_name = f"{func_name}_cs{call_site_idx}"
726
861
  remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
727
862
  self._active_ta_remap = remap
@@ -729,13 +864,27 @@ class TopLevelEmitter:
729
864
  self._active_var_remap = var_remap
730
865
  self._in_ta_func_variant = True
731
866
  self._active_call_site_idx = call_site_idx
867
+ self._current_instance_name = f"{self._func_safe_name(fi.name)}_cs{call_site_idx}"
732
868
  else:
733
869
  self._active_ta_remap = {}
734
870
  self._active_var_remap = {}
735
871
  self._in_ta_func_variant = False
736
872
  self._active_call_site_idx = None
873
+ self._current_instance_name = None
737
874
 
738
875
  prev_func_locals = self._current_func_locals
876
+ prev_func_body = getattr(self, "_current_func_body", None)
877
+ prev_func_name = getattr(self, "_active_func_name", None)
878
+ # The function body is the lexical scope used by the UDT-alias analysis
879
+ # (BUG C): a local initialised from a var/global UDT lvalue and later
880
+ # mutated through must alias, not value-copy.
881
+ self._current_func_body = node.body
882
+ self._active_func_name = fi.name
883
+ # Pointer-aliased UDT locals are function-scoped: a name like ``p_ivot``
884
+ # may be a rebinding pointer alias in one function and a ``pivot&``
885
+ # parameter in another, so reset per function to avoid cross-contamination.
886
+ prev_ptr_alias = self._udt_ptr_alias_locals
887
+ self._udt_ptr_alias_locals = set()
739
888
  self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
740
889
  # Plain (non-persistent) scalar locals are emitted inline and live in
741
890
  # no other set; collect them so the unknown-identifier guard in
@@ -744,24 +893,47 @@ class TopLevelEmitter:
744
893
 
745
894
  lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
746
895
 
896
+ # Function-scoped ``var`` one-shot initializer: Pine ``var`` inside a
897
+ # function is a function-local static — its initializer runs exactly
898
+ # once, on the first call to THIS variant, with the first bar's values
899
+ # the function actually sees. Each clone (cs0/cs1/…) is independent.
900
+ self._emit_func_var_init_block(fi, call_site_idx, lines, flag_override=var_init_flag)
901
+
747
902
  emitted_return = False
748
903
  if node.is_single_expr and node.body:
749
904
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
750
- if expr:
905
+ if expr and self._call_is_void(expr):
906
+ # void setter as the sole body expr — emit as statement, fall
907
+ # through to the default return.
908
+ self._visit_stmt(node.body[0], lines, indent=2)
909
+ elif expr:
751
910
  lines.append(f" return {self._visit_expr(expr)};")
752
911
  emitted_return = True
753
912
  else:
754
913
  for i, s in enumerate(node.body):
755
914
  if i == len(node.body) - 1 and isinstance(s, ExprStmt):
756
- lines.append(f" return {self._visit_expr(s.expr)};")
757
- emitted_return = True
915
+ # A void drawing setter / delete / visual-noop, or a dropped
916
+ # table/polyline method call (``panel.cell(...)``), used as
917
+ # the last statement cannot be the return value (it lowers to
918
+ # a void / no-op C++ call). Emit it as a plain statement
919
+ # (which ``_is_skip_expr`` drops) and let the default-return
920
+ # path below supply the function's result.
921
+ if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
922
+ self._visit_stmt(s, lines, indent=2)
923
+ else:
924
+ lines.append(f" return {self._visit_expr(s.expr)};")
925
+ emitted_return = True
758
926
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
759
927
  # Switch/if as last statement = return expression in PineScript
760
928
  # Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
761
- default_ret = (
762
- f"{ret_type}{{}}" if ret_type in self._udt_defs
763
- else self._default_for_type(ret_type)
764
- )
929
+ # A drawing-handle / UDT return type must brace-init its
930
+ # default (``Label _func_ret = Label{};``) — falling through
931
+ # to ``_default_for_type`` would emit ``0.0`` and clang would
932
+ # reject ``Label _func_ret = 0.0;``.
933
+ if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
934
+ default_ret = f"{ret_type}{{}}"
935
+ else:
936
+ default_ret = self._default_for_type(ret_type)
765
937
  lines.append(f" {ret_type} _func_ret = {default_ret};")
766
938
  self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
767
939
  lines.append(f" return _func_ret;")
@@ -776,10 +948,10 @@ class TopLevelEmitter:
776
948
  default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
777
949
  lines.append(f" return std::make_tuple({default_vals});")
778
950
  else:
779
- default_ret = (
780
- f"{ret_type}{{}}" if ret_type in self._udt_defs
781
- else self._default_for_type(ret_type)
782
- )
951
+ if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
952
+ default_ret = f"{ret_type}{{}}"
953
+ else:
954
+ default_ret = self._default_for_type(ret_type)
783
955
  lines.append(f" return {default_ret};")
784
956
 
785
957
  lines.append(" }")
@@ -787,10 +959,76 @@ class TopLevelEmitter:
787
959
  self._current_func_series_params = set()
788
960
  self._udt_param_udt = {}
789
961
  self._current_func_locals = prev_func_locals
962
+ self._current_func_body = prev_func_body
963
+ self._active_func_name = prev_func_name
964
+ self._udt_ptr_alias_locals = prev_ptr_alias
790
965
  self._active_ta_remap = {}
791
966
  self._active_var_remap = {}
792
967
  self._in_ta_func_variant = False
793
968
  self._active_call_site_idx = None
969
+ self._current_instance_name = None
970
+
971
+ def _func_var_init_flag_name(self, fname: str, call_site_idx: int | None) -> str:
972
+ suffix = f"_cs{call_site_idx}" if call_site_idx is not None else ""
973
+ return f"_fvinit_{self._func_safe_name(fname)}{suffix}"
974
+
975
+ def _emit_func_var_init_block(self, fi: FuncInfo, call_site_idx: int | None,
976
+ lines: list[str], flag_override: str | None = None) -> None:
977
+ """Emit the one-shot initializer block for a function's ``var`` members.
978
+
979
+ Pine ``var`` declared inside a function is a function-local static:
980
+ the initializer runs exactly once on the FIRST call to this variant
981
+ (using the first bar's values the function actually sees) and the
982
+ result persists for the strategy's lifetime. Each per-call-site clone
983
+ is an independent instance with its own flag and its own set of
984
+ (remapped) members.
985
+
986
+ This closes the gap where a function-scoped ``var line x = line.new(...)``
987
+ (or any non-compile-time initializer — drawing handles, UDT ctors,
988
+ arrays, runtime expressions) was declared as a default-constructed
989
+ class member but its initializer was dropped, leaving the member ``na``
990
+ / uninitialised and causing "drawing access on na handle" at runtime.
991
+ """
992
+ members = self.ctx.func_var_members.get(fi.name)
993
+ if not members:
994
+ return
995
+ flag = flag_override or self._func_var_init_flag_name(fi.name, call_site_idx)
996
+ # ``_active_var_remap`` is already set for this variant by the caller,
997
+ # so lowering each init expression here correctly resolves references
998
+ # to sibling var members (which are themselves remapped for clones).
999
+ init_lines: list[str] = []
1000
+ for name, ptype, _init_str in members:
1001
+ init_ast = self.ctx.var_member_init_exprs.get(name)
1002
+ safe = self._safe_name(name)
1003
+ target = self._active_var_remap.get(safe, safe)
1004
+ if name in self.ctx.series_vars:
1005
+ if init_ast is None:
1006
+ continue
1007
+ init_cpp = self._visit_expr(init_ast)
1008
+ init_cpp = self._typed_na_init(init_cpp, name, ptype)
1009
+ init_lines.append(f" {target}.push({init_cpp});")
1010
+ continue
1011
+ if init_ast is None:
1012
+ # No initializer to lower (e.g. bare ``var box b``); leave the
1013
+ # member at its default-constructed value.
1014
+ continue
1015
+ # Skip a plain ``na`` initializer for drawing handles / UDTs whose
1016
+ # default-constructed member is already the na sentinel; assigning
1017
+ # ``na<double>()`` would not type-match the handle / struct.
1018
+ udt_t = self._udt_var_types.get(name)
1019
+ is_drawing = udt_t in DRAWING_TYPE_TO_CPP if udt_t else False
1020
+ is_udt = udt_t in self._udt_defs if udt_t else False
1021
+ from ..ast_nodes import NaLiteral
1022
+ if (is_drawing or is_udt) and isinstance(init_ast, NaLiteral):
1023
+ continue
1024
+ init_cpp = self._visit_expr(init_ast)
1025
+ init_lines.append(f" {target} = {init_cpp};")
1026
+ if not init_lines:
1027
+ return
1028
+ lines.append(f" if (!{flag}) {{")
1029
+ lines.extend(init_lines)
1030
+ lines.append(f" {flag} = true;")
1031
+ lines.append(" }")
794
1032
 
795
1033
  def _emit_precalculate_and_run(self, lines: list[str]) -> None:
796
1034
  has_static_ta = any(getattr(site, "is_static", False) for site in self.ctx.ta_call_sites)
@@ -832,6 +1070,69 @@ class TopLevelEmitter:
832
1070
  push_expr_bars = push_expr.replace("current_bar_.", "bars[i].")
833
1071
  lines.append(f" _s_{field_name}.push({push_expr_bars});")
834
1072
 
1073
+ # Advance the native input.source() backing series (_src_open_ etc.)
1074
+ # from bars[i] too. A static TA site's compute args can reference an
1075
+ # input.source()-derived member (e.g. ``ta.stdev(bbSourceInput, 20)``)
1076
+ # which resolves at runtime to ``get_input_source(...)`` reading one
1077
+ # of these series — normally advanced once per real bar by
1078
+ # ``_push_source_series()`` inside ``dispatch_bar()``, which this
1079
+ # standalone precalc loop never calls. Without this, every static TA
1080
+ # site fed by an input.source() reads an empty series (0.0) for the
1081
+ # entire precalculation, silently corrupting its precalculated
1082
+ # values (e.g. a Bollinger Band's stdev collapsing to 0). Gated on
1083
+ # ``_src_series_active_`` to stay a no-op for scripts with no
1084
+ # input.source() usage; cleared again before the real run begins
1085
+ # (BacktestEngine::run() clears every _src_*_ series unconditionally).
1086
+ lines.append(" if (_src_series_active_) {")
1087
+ lines.append(" const double _pc_o = bars[i].open;")
1088
+ lines.append(" const double _pc_h = bars[i].high;")
1089
+ lines.append(" const double _pc_l = bars[i].low;")
1090
+ lines.append(" const double _pc_c = bars[i].close;")
1091
+ lines.append(" const double _pc_v = bars[i].volume;")
1092
+ lines.append(" _src_open_.push(_pc_o); _src_high_.push(_pc_h); _src_low_.push(_pc_l);")
1093
+ lines.append(" _src_close_.push(_pc_c); _src_volume_.push(_pc_v);")
1094
+ lines.append(" _src_hl2_.push((_pc_h + _pc_l) / 2.0);")
1095
+ lines.append(" _src_hlc3_.push((_pc_h + _pc_l + _pc_c) / 3.0);")
1096
+ lines.append(" _src_ohlc4_.push((_pc_o + _pc_h + _pc_l + _pc_c) / 4.0);")
1097
+ lines.append(" _src_hlcc4_.push((_pc_h + _pc_l + _pc_c + _pc_c) / 4.0);")
1098
+ lines.append(" }")
1099
+
1100
+ # Replay every top-level ``X = input.source(...)`` (or bare
1101
+ # ``X = input(close)``) assignment. A static TA site's compute args
1102
+ # often don't reference ``get_input_source(...)`` inline — they
1103
+ # reference the top-level variable the script bound it to (e.g.
1104
+ # ``bbSourceInput = input.source(close, "BB Source")`` then
1105
+ # ``ta.stdev(bbSourceInput, 20)``). That variable is deliberately
1106
+ # NOT covered by the ``_inputs_initialized_`` once-only static-input
1107
+ # block above (see ``is_static_global_input``'s ``_is_source_input``
1108
+ # exclusion) because it tracks a live per-bar series, not a frozen
1109
+ # config value — under normal per-bar dispatch it is reassigned every
1110
+ # real bar. This precalc loop has no other path that reassigns it, so
1111
+ # without this replay every is_static site downstream of it would
1112
+ # keep reading its ctor-initialized 0.0 for the whole precalculation
1113
+ # even with the ``_src_*_`` fix above.
1114
+ for stmt in self.ctx.ast.body:
1115
+ if not isinstance(stmt, VarDecl):
1116
+ continue
1117
+ if stmt.name not in self._global_member_vars:
1118
+ continue
1119
+ if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1120
+ continue
1121
+ safe = self._safe_name(stmt.name)
1122
+ default = self._get_input_default(stmt.value)
1123
+ base = self._source_defval_to_base_series(default)
1124
+ title = self._get_input_title(stmt.value, var_name=stmt.name)
1125
+ cpp_val = f'get_input_source("{title}", {base})[0]'
1126
+ # A source var subscripted elsewhere in the script (e.g. ``src[1]``)
1127
+ # is declared ``Series<double>``, not a scalar double, mirroring
1128
+ # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
1129
+ # ``_visit_var_decl``'s ``node.name in self.ctx.series_vars``
1130
+ # branch) — a plain ``=`` there is a compile error.
1131
+ if stmt.name in self.ctx.series_vars:
1132
+ lines.append(f' {safe}.push({cpp_val});')
1133
+ else:
1134
+ lines.append(f' {safe} = {cpp_val};')
1135
+
835
1136
  # Set _precalc_loop_active = True
836
1137
  self._precalc_loop_active = True
837
1138
  try:
@@ -33,6 +33,31 @@ CPP_RESERVED = {
33
33
  }
34
34
 
35
35
 
36
+ # Bare C++ identifiers that ``strategy.*`` (and a few other) read-only
37
+ # accessors lower to as zero-arg free-function calls — e.g.
38
+ # ``strategy.grossprofit`` -> ``gross_profit()`` (see codegen/visit_expr.py
39
+ # and codegen/emit_top.py). A user variable emitted with one of these names
40
+ # becomes a class member that shadows the engine accessor, so the codegen
41
+ # would emit ``gross_profit = gross_profit();`` and clang rejects the call
42
+ # ("called object type 'double' is not a function"). Escaping such user
43
+ # identifiers in ``_safe_name`` keeps the two namespaces disjoint; the
44
+ # accessor call strings are emitted verbatim and never routed through
45
+ # ``_safe_name``, so they are unaffected. Keep in sync with the accessor
46
+ # lowerings if new bare-call accessors are added.
47
+ BUILTIN_ACCESSOR_NAMES = {
48
+ "signed_position_size", "position_entry_name",
49
+ "count_wintrades", "count_losstrades", "eventrades",
50
+ "net_profit", "gross_profit", "gross_loss",
51
+ "grossprofit_percent", "grossloss_percent",
52
+ "max_contracts_held_all", "max_contracts_held_long",
53
+ "max_contracts_held_short", "max_drawdown_percent", "max_runup_percent",
54
+ "avg_trade", "avg_trade_percent", "avg_winning_trade", "avg_losing_trade",
55
+ "avg_winning_trade_percent", "avg_losing_trade_percent",
56
+ "margin_liquidation_price", "open_profit", "current_equity",
57
+ "open_trades_capital_held",
58
+ }
59
+
60
+
36
61
  class NamingHelper:
37
62
  """Identifier escaping, callee resolution, and a generic AST walker.
38
63
 
@@ -56,7 +81,7 @@ class NamingHelper:
56
81
 
57
82
  def _safe_name(self, name: str) -> str:
58
83
  """Rename identifiers that collide with C++ reserved words."""
59
- if name in CPP_RESERVED:
84
+ if name in CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES:
60
85
  return f"_{name}_"
61
86
  return name
62
87
 
@@ -156,6 +156,27 @@ class InputHelper:
156
156
  return "get_input_int"
157
157
  return "get_input_double"
158
158
 
159
+ def _is_source_input(self, node: FuncCall) -> bool:
160
+ """True if an ``input.*`` call yields a *live per-bar source series*.
161
+
162
+ Covers both ``input.source(<native series>)`` and a bare
163
+ ``input(<native series>)`` — in Pine v6 ``input(close)`` is the
164
+ source-input overload and behaves like ``input.source(close)``,
165
+ returning a series that tracks ``close`` every bar (not a constant
166
+ frozen at the first bar). The defval is restricted to the native
167
+ OHLCV series the engine can resolve at runtime (the same set
168
+ ``input.source`` is restricted to); a bare ``input(14)`` /
169
+ ``input(\"x\")`` / ``input(true)`` stays a frozen scalar."""
170
+ func_name, namespace = self._resolve_callee(node.callee)
171
+ if namespace == "input" and func_name == "source":
172
+ return True
173
+ if func_name == "input" and namespace is None:
174
+ default = self._get_input_default(node)
175
+ if (isinstance(default, Identifier)
176
+ and default.name in self._NATIVE_SOURCE_SERIES):
177
+ return True
178
+ return False
179
+
159
180
  def _source_defval_to_base_series(self, default) -> str:
160
181
  """Map an input.source defval (close/high/hl2/…) to its engine base
161
182
  source series member (``_src_close_`` …). Falls back to
@@ -175,16 +196,32 @@ class InputHelper:
175
196
  series and we read its current value; a subscripted source var is
176
197
  already tracked as a series var by the analyzer so ``src[1]`` lowers
177
198
  to a Series subscript. Every other input type routes through the
178
- scalar getter table."""
179
- if namespace == "input" and func_name == "source":
199
+ scalar getter table. A bare ``input(<native series>)`` is the Pine
200
+ source-input overload and is rendered identically to
201
+ ``input.source``."""
202
+ if self._is_source_input(node):
180
203
  default = self._get_input_default(node)
181
204
  base = self._source_defval_to_base_series(default)
182
205
  return f'get_input_source("{title}", {base})[0]'
183
206
  default = self._get_input_default(node)
184
207
  default_cpp = self._visit_expr(default) if default is not None else "0"
185
208
  getter = self._input_type_to_getter(func_name, namespace)
209
+ default_cpp = self._coerce_string_input_default(getter, default_cpp)
186
210
  return f'{getter}("{title}", {default_cpp})'
187
211
 
212
+ def _coerce_string_input_default(self, getter: str, default_cpp: str) -> str:
213
+ """String getters take a ``std::string`` default. An unresolved default
214
+ (e.g. ``input.string(size.tiny, ...)`` where ``size.tiny`` lowers to
215
+ ``0``) would pass a null char* and crash ``strlen`` inside the getter,
216
+ so coerce any non-string default to a safe empty string. (Label size is
217
+ a visual no-op in the backtest, so the exact default does not affect
218
+ trading logic.)"""
219
+ if getter == "get_input_string":
220
+ stripped = default_cpp.strip()
221
+ if not stripped.startswith("std::string(") and not stripped.startswith('"'):
222
+ return 'std::string("")'
223
+ return default_cpp
224
+
188
225
  def _enforce_enum_declared_before_input_enum(self, node: FuncCall) -> None:
189
226
  """Mirror the analyzer: ``input.enum(Enum.member)`` requires Enum to be defined above the call.
190
227