@pineforge/codegen-pyodide 0.8.0 → 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.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -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
|
|
@@ -208,9 +214,29 @@ class TopLevelEmitter:
|
|
|
208
214
|
# TA members with ctor args
|
|
209
215
|
for site in self.ctx.ta_call_sites:
|
|
210
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
|
+
)
|
|
211
237
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
212
|
-
#
|
|
213
|
-
# (
|
|
238
|
+
# Compile-time placeholder for the init list; the runtime reset
|
|
239
|
+
# (when the arg is input-derived) overwrites it on the first bar.
|
|
214
240
|
safe_resolved = []
|
|
215
241
|
for r in resolved:
|
|
216
242
|
if self._is_compile_time_value(r):
|
|
@@ -240,11 +266,24 @@ class TopLevelEmitter:
|
|
|
240
266
|
safe = self._safe_name(name)
|
|
241
267
|
if name in self._array_vars or name in self._map_vars:
|
|
242
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
|
|
243
276
|
# UDT-typed var members (``var SDZone z = na``) default-construct to
|
|
244
277
|
# na via the struct's in-class ``__pf_na = true``; a ctor init like
|
|
245
278
|
# ``z(na<double>())`` would not type-match the struct member.
|
|
246
279
|
if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
|
|
247
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
|
|
248
287
|
if name not in self.ctx.series_vars:
|
|
249
288
|
cpp_val = self._resolve_known(init_expr)
|
|
250
289
|
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
@@ -362,20 +401,22 @@ class TopLevelEmitter:
|
|
|
362
401
|
lines.append(" security_eval_states_.clear();")
|
|
363
402
|
for info in self._security_eval_info:
|
|
364
403
|
tf = info.get("tf")
|
|
365
|
-
tf_expr =
|
|
404
|
+
tf_expr = info.get("tf_expr")
|
|
366
405
|
if tf:
|
|
367
406
|
tf_expr = f'"{tf}"'
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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_"
|
|
376
413
|
if tf_expr:
|
|
377
414
|
la = "true" if info["lookahead_on"] else "false"
|
|
378
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 ""
|
|
379
420
|
sec_id = info["sec_id"]
|
|
380
421
|
# The runtime registration function is named in tables.py
|
|
381
422
|
# to keep a single source of truth and to avoid embedding
|
|
@@ -391,7 +432,7 @@ class TopLevelEmitter:
|
|
|
391
432
|
lines.append(
|
|
392
433
|
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
|
|
393
434
|
f"({sec_id}, {tf_expr}, "
|
|
394
|
-
f"input_tf_, {la}, {go});")
|
|
435
|
+
f"input_tf_, {la}, {go}{ha_arg});")
|
|
395
436
|
lines.append(" }")
|
|
396
437
|
|
|
397
438
|
# Map strategy series member name to push expression
|
|
@@ -445,6 +486,10 @@ class TopLevelEmitter:
|
|
|
445
486
|
if self.ctx.var_members:
|
|
446
487
|
lines.append(" if (!_var_initialized) {")
|
|
447
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
|
|
448
493
|
safe = self._safe_name(name)
|
|
449
494
|
if name in self._array_vars:
|
|
450
495
|
for stmt in self.ctx.ast.body:
|
|
@@ -545,6 +590,7 @@ class TopLevelEmitter:
|
|
|
545
590
|
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
546
591
|
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
547
592
|
getter = self._input_type_to_getter(func_name_i, namespace_i)
|
|
593
|
+
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
548
594
|
cpp_val = f'{getter}("{title}", {default_cpp})'
|
|
549
595
|
static_vars.append(f"{safe} = {cpp_val};")
|
|
550
596
|
|
|
@@ -709,11 +755,17 @@ class TopLevelEmitter:
|
|
|
709
755
|
base = fi.node.name if fi.node else ""
|
|
710
756
|
return self._func_safe_name(f"_udt_{udt}_{base}")
|
|
711
757
|
|
|
712
|
-
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | 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:
|
|
713
760
|
"""Emit a user-defined function as a class method.
|
|
714
761
|
|
|
715
762
|
If call_site_idx is not None, emit a per-call-site variant with
|
|
716
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.
|
|
717
769
|
"""
|
|
718
770
|
node = fi.node
|
|
719
771
|
if node is None:
|
|
@@ -729,7 +781,12 @@ class TopLevelEmitter:
|
|
|
729
781
|
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
730
782
|
for i, p in enumerate(node.params):
|
|
731
783
|
if is_udt and i == 0 and fi.udt_type_name:
|
|
732
|
-
|
|
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}&"
|
|
733
790
|
safe_p = self._safe_name(p)
|
|
734
791
|
self._udt_param_udt[safe_p] = fi.udt_type_name
|
|
735
792
|
self._udt_param_udt[p] = fi.udt_type_name
|
|
@@ -739,6 +796,24 @@ class TopLevelEmitter:
|
|
|
739
796
|
# This param uses history access (e.g. src[1]) — pass as Series
|
|
740
797
|
cpp_t = "const Series<double>&"
|
|
741
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}&"
|
|
742
817
|
elif i < len(fi.param_types):
|
|
743
818
|
pt = fi.param_types[i]
|
|
744
819
|
cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
|
|
@@ -757,13 +832,31 @@ class TopLevelEmitter:
|
|
|
757
832
|
tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
|
|
758
833
|
ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
|
|
759
834
|
elif getattr(fi, "udt_return_type", None):
|
|
760
|
-
|
|
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)
|
|
761
842
|
else:
|
|
762
843
|
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
|
|
763
844
|
|
|
764
845
|
# For per-call-site variants, suffix the function name and activate TA + var remapping
|
|
765
846
|
func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
|
|
766
|
-
|
|
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:
|
|
767
860
|
func_name = f"{func_name}_cs{call_site_idx}"
|
|
768
861
|
remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
|
|
769
862
|
self._active_ta_remap = remap
|
|
@@ -771,13 +864,27 @@ class TopLevelEmitter:
|
|
|
771
864
|
self._active_var_remap = var_remap
|
|
772
865
|
self._in_ta_func_variant = True
|
|
773
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}"
|
|
774
868
|
else:
|
|
775
869
|
self._active_ta_remap = {}
|
|
776
870
|
self._active_var_remap = {}
|
|
777
871
|
self._in_ta_func_variant = False
|
|
778
872
|
self._active_call_site_idx = None
|
|
873
|
+
self._current_instance_name = None
|
|
779
874
|
|
|
780
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()
|
|
781
888
|
self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
|
|
782
889
|
# Plain (non-persistent) scalar locals are emitted inline and live in
|
|
783
890
|
# no other set; collect them so the unknown-identifier guard in
|
|
@@ -786,24 +893,47 @@ class TopLevelEmitter:
|
|
|
786
893
|
|
|
787
894
|
lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
|
|
788
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
|
+
|
|
789
902
|
emitted_return = False
|
|
790
903
|
if node.is_single_expr and node.body:
|
|
791
904
|
expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
|
|
792
|
-
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:
|
|
793
910
|
lines.append(f" return {self._visit_expr(expr)};")
|
|
794
911
|
emitted_return = True
|
|
795
912
|
else:
|
|
796
913
|
for i, s in enumerate(node.body):
|
|
797
914
|
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
800
926
|
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
|
|
801
927
|
# Switch/if as last statement = return expression in PineScript
|
|
802
928
|
# Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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)
|
|
807
937
|
lines.append(f" {ret_type} _func_ret = {default_ret};")
|
|
808
938
|
self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
|
|
809
939
|
lines.append(f" return _func_ret;")
|
|
@@ -818,10 +948,10 @@ class TopLevelEmitter:
|
|
|
818
948
|
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
|
|
819
949
|
lines.append(f" return std::make_tuple({default_vals});")
|
|
820
950
|
else:
|
|
821
|
-
|
|
822
|
-
f"{ret_type}{{}}"
|
|
823
|
-
|
|
824
|
-
|
|
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)
|
|
825
955
|
lines.append(f" return {default_ret};")
|
|
826
956
|
|
|
827
957
|
lines.append(" }")
|
|
@@ -829,10 +959,76 @@ class TopLevelEmitter:
|
|
|
829
959
|
self._current_func_series_params = set()
|
|
830
960
|
self._udt_param_udt = {}
|
|
831
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
|
|
832
965
|
self._active_ta_remap = {}
|
|
833
966
|
self._active_var_remap = {}
|
|
834
967
|
self._in_ta_func_variant = False
|
|
835
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(" }")
|
|
836
1032
|
|
|
837
1033
|
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
838
1034
|
has_static_ta = any(getattr(site, "is_static", False) for site in self.ctx.ta_call_sites)
|
|
@@ -874,6 +1070,69 @@ class TopLevelEmitter:
|
|
|
874
1070
|
push_expr_bars = push_expr.replace("current_bar_.", "bars[i].")
|
|
875
1071
|
lines.append(f" _s_{field_name}.push({push_expr_bars});")
|
|
876
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
|
+
|
|
877
1136
|
# Set _precalc_loop_active = True
|
|
878
1137
|
self._precalc_loop_active = True
|
|
879
1138
|
try:
|
|
@@ -206,8 +206,22 @@ class InputHelper:
|
|
|
206
206
|
default = self._get_input_default(node)
|
|
207
207
|
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
208
208
|
getter = self._input_type_to_getter(func_name, namespace)
|
|
209
|
+
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
209
210
|
return f'{getter}("{title}", {default_cpp})'
|
|
210
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
|
+
|
|
211
225
|
def _enforce_enum_declared_before_input_enum(self, node: FuncCall) -> None:
|
|
212
226
|
"""Mirror the analyzer: ``input.enum(Enum.member)`` requires Enum to be defined above the call.
|
|
213
227
|
|