@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.
- 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 +532 -51
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +331 -30
- package/pineforge_codegen/codegen/helpers.py +26 -1
- package/pineforge_codegen/codegen/input.py +39 -2
- package/pineforge_codegen/codegen/security.py +321 -9
- package/pineforge_codegen/codegen/ta.py +4 -1
- package/pineforge_codegen/codegen/tables.py +63 -7
- package/pineforge_codegen/codegen/types.py +383 -21
- package/pineforge_codegen/codegen/visit_call.py +159 -22
- package/pineforge_codegen/codegen/visit_expr.py +91 -3
- package/pineforge_codegen/codegen/visit_stmt.py +188 -26
- package/pineforge_codegen/lexer.py +68 -3
- package/pineforge_codegen/parser.py +106 -49
- package/pineforge_codegen/support_checker.py +361 -37
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +4 -18
- package/pineforge_codegen-0.7.6.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
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
197
|
-
# (
|
|
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 =
|
|
404
|
+
tf_expr = info.get("tf_expr")
|
|
344
405
|
if tf:
|
|
345
406
|
tf_expr = f'"{tf}"'
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
780
|
-
f"{ret_type}{{}}"
|
|
781
|
-
|
|
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
|
-
|
|
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
|
|