@pineforge/codegen-pyodide 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1348 -113
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +258 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +559 -36
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +165 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +149 -15
- package/pineforge_codegen/codegen/visit_stmt.py +85 -38
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -67,7 +67,7 @@ from typing import Any
|
|
|
67
67
|
|
|
68
68
|
from ..ast_nodes import (
|
|
69
69
|
ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, MemberAccess,
|
|
70
|
-
NumberLiteral, StringLiteral, TupleLiteral, UnaryOp, VarDecl,
|
|
70
|
+
NumberLiteral, StringLiteral, Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
71
71
|
)
|
|
72
72
|
from ..symbols import PineType
|
|
73
73
|
from .. import signatures as sigs
|
|
@@ -75,7 +75,7 @@ from .. import tv_input_choices as tv_in
|
|
|
75
75
|
from .contracts import FixnanCallSite, FuncInfo, SecurityCallInfo, TACallSite
|
|
76
76
|
from .tables import (
|
|
77
77
|
BAR_FIELDS, TA_CLASS_MAP, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
|
|
78
|
-
TA_TUPLE_RETURNS,
|
|
78
|
+
TA_TUPLE_RETURNS, TA_TUPLE_ELEMENT_COUNTS, TA_COMPUTE_ARGS,
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
@@ -205,6 +205,7 @@ class CallHandlers:
|
|
|
205
205
|
returns_tuple=returns_tuple,
|
|
206
206
|
node=node,
|
|
207
207
|
is_static=is_static,
|
|
208
|
+
owner_func=(self._enclosing_func_names[-1] if self._enclosing_func_names else None),
|
|
208
209
|
)
|
|
209
210
|
self._ta_call_sites.append(site)
|
|
210
211
|
self._ta_member_names.add(site.member_name)
|
|
@@ -235,9 +236,14 @@ class CallHandlers:
|
|
|
235
236
|
elif func_name in TA_PERIOD_ARG:
|
|
236
237
|
ctor_indices = {TA_PERIOD_ARG[func_name]}
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
if func_name in TA_COMPUTE_ARGS:
|
|
240
|
+
for i in TA_COMPUTE_ARGS[func_name]:
|
|
241
|
+
if i < len(all_args) and all_args[i] is not None:
|
|
242
|
+
compute_args.append(all_args[i])
|
|
243
|
+
else:
|
|
244
|
+
for i, arg in enumerate(all_args):
|
|
245
|
+
if i not in ctor_indices and arg is not None:
|
|
246
|
+
compute_args.append(arg)
|
|
241
247
|
|
|
242
248
|
is_static = self._global_scope and all(self._is_static_expression(arg) for arg in compute_args)
|
|
243
249
|
site = TACallSite(
|
|
@@ -248,6 +254,7 @@ class CallHandlers:
|
|
|
248
254
|
returns_tuple=returns_tuple,
|
|
249
255
|
node=node,
|
|
250
256
|
is_static=is_static,
|
|
257
|
+
owner_func=(self._enclosing_func_names[-1] if self._enclosing_func_names else None),
|
|
251
258
|
)
|
|
252
259
|
self._ta_call_sites.append(site)
|
|
253
260
|
self._ta_member_names.add(site.member_name)
|
|
@@ -309,6 +316,27 @@ class CallHandlers:
|
|
|
309
316
|
|
|
310
317
|
returns_tuple = isinstance(expr_node, TupleLiteral)
|
|
311
318
|
tuple_size = len(expr_node.elements) if returns_tuple else 0
|
|
319
|
+
if not returns_tuple and isinstance(expr_node, FuncCall):
|
|
320
|
+
expr_func = None
|
|
321
|
+
expr_ns = None
|
|
322
|
+
if (isinstance(expr_node.callee, MemberAccess)
|
|
323
|
+
and isinstance(expr_node.callee.object, Identifier)):
|
|
324
|
+
expr_ns = expr_node.callee.object.name
|
|
325
|
+
expr_func = expr_node.callee.member
|
|
326
|
+
if expr_ns == "ta":
|
|
327
|
+
if expr_func == "vwap":
|
|
328
|
+
merged_v = list(expr_node.args)
|
|
329
|
+
for i, pname in enumerate(["source", "anchor", "stdev_mult"]):
|
|
330
|
+
if pname in expr_node.kwargs:
|
|
331
|
+
while len(merged_v) <= i:
|
|
332
|
+
merged_v.append(None)
|
|
333
|
+
if merged_v[i] is None:
|
|
334
|
+
merged_v[i] = expr_node.kwargs[pname]
|
|
335
|
+
if len(merged_v) >= 3:
|
|
336
|
+
expr_func = "vwap_bands"
|
|
337
|
+
if expr_func in TA_TUPLE_RETURNS:
|
|
338
|
+
returns_tuple = True
|
|
339
|
+
tuple_size = TA_TUPLE_ELEMENT_COUNTS.get(expr_func, 0)
|
|
312
340
|
|
|
313
341
|
gaps_node = all_args[3] if len(all_args) > 3 else None
|
|
314
342
|
lookahead_node = all_args[4] if len(all_args) > 4 else None
|
|
@@ -785,11 +813,18 @@ class CallHandlers:
|
|
|
785
813
|
arg_type = self._visit(arg)
|
|
786
814
|
|
|
787
815
|
self._fixnan_counter += 1
|
|
816
|
+
owner = self._enclosing_func_names[-1] if self._enclosing_func_names else None
|
|
788
817
|
site = FixnanCallSite(
|
|
789
818
|
member_name=f"_prev_fixnan_{self._fixnan_counter}",
|
|
790
819
|
pine_type=arg_type,
|
|
820
|
+
node=node,
|
|
821
|
+
owner_func=owner,
|
|
791
822
|
)
|
|
823
|
+
idx = len(self._fixnan_sites)
|
|
792
824
|
self._fixnan_sites.append(site)
|
|
825
|
+
self._fixnan_member_names.add(site.member_name)
|
|
826
|
+
if owner is not None:
|
|
827
|
+
self._func_fixnan_indices.setdefault(owner, []).append(idx)
|
|
793
828
|
|
|
794
829
|
return arg_type
|
|
795
830
|
|
|
@@ -817,13 +852,30 @@ class CallHandlers:
|
|
|
817
852
|
return _is_arith(n.left) and _is_arith(n.right)
|
|
818
853
|
if isinstance(n, UnaryOp):
|
|
819
854
|
return _is_arith(n.operand)
|
|
855
|
+
if isinstance(n, Ternary):
|
|
856
|
+
# ``cond ? a : b`` — expand when both branches are arith.
|
|
857
|
+
# The condition may be a comparison/logical over arith leaves;
|
|
858
|
+
# rely on the codegen stability gate to reject series deps.
|
|
859
|
+
return (_is_arith(n.true_val) and _is_arith(n.false_val)
|
|
860
|
+
and _is_arith(n.condition))
|
|
861
|
+
if isinstance(n, MemberAccess):
|
|
862
|
+
# ``timeframe.*`` / ``syminfo.*`` / ``math.pi`` etc. — stable
|
|
863
|
+
# per-run scalars that may appear inside a function-local
|
|
864
|
+
# derived length. Let the codegen stability classifier decide.
|
|
865
|
+
return True
|
|
820
866
|
if isinstance(n, FuncCall):
|
|
821
|
-
# Allow math.* helpers (math.round/sqrt/...) over arith args.
|
|
822
867
|
callee = n.callee
|
|
868
|
+
# Allow math.* helpers (math.round/sqrt/cos/...) over arith args.
|
|
823
869
|
if (isinstance(callee, MemberAccess)
|
|
824
870
|
and isinstance(callee.object, Identifier)
|
|
825
871
|
and callee.object.name == "math"):
|
|
826
872
|
return all(_is_arith(a) for a in n.args)
|
|
873
|
+
# Pine type-cast builtins ``int(x)`` / ``float(x)`` / ``bool(x)``
|
|
874
|
+
# / ``string(x)`` — transparent over arith args. Common in
|
|
875
|
+
# derived TA lengths (``int(math.round(2 / a))``).
|
|
876
|
+
if (isinstance(callee, Identifier)
|
|
877
|
+
and callee.name in ("int", "float", "bool", "string")):
|
|
878
|
+
return all(_is_arith(a) for a in n.args)
|
|
827
879
|
return False
|
|
828
880
|
|
|
829
881
|
reassigned: set[str] = set()
|
|
@@ -848,6 +900,170 @@ class CallHandlers:
|
|
|
848
900
|
defs[stmt.name] = self._expr_to_str(stmt.value)
|
|
849
901
|
return defs
|
|
850
902
|
|
|
903
|
+
def _materialize_user_func_call_site_state(
|
|
904
|
+
self, func_name: str, cs_idx: int, node: FuncCall,
|
|
905
|
+
*, reuse_existing_owner: str | None = None) -> None:
|
|
906
|
+
"""Materialize TA/fixnan state for one UDF call-site variant.
|
|
907
|
+
|
|
908
|
+
Ordinary call sites are handled while walking the AST. A second class
|
|
909
|
+
is discovered only after that walk: a stateful helper reached through a
|
|
910
|
+
multi-call-site parent needs the parent's additional call-path indices
|
|
911
|
+
even though the helper has only one textual call. The late propagation
|
|
912
|
+
pass in ``Analyzer._propagate_call_site_counts`` calls this same helper
|
|
913
|
+
for those inherited variants so the exported count never references a
|
|
914
|
+
TA/fixnan clone that was not actually declared.
|
|
915
|
+
|
|
916
|
+
``reuse_existing_owner`` is used only by late propagation. A
|
|
917
|
+
range-widened parent may already have materialized the default
|
|
918
|
+
``{member}_cs{idx}`` clone for the borrowed callee site; when that clone
|
|
919
|
+
belongs to the parent currently being propagated, it is the desired
|
|
920
|
+
call-path state and must be reused rather than duplicated under a
|
|
921
|
+
disambiguated-but-unused name.
|
|
922
|
+
"""
|
|
923
|
+
func_def = self._func_defs[func_name]
|
|
924
|
+
|
|
925
|
+
param_arg_map: dict[str, str] = {}
|
|
926
|
+
for p_idx, param_name in enumerate(func_def.params):
|
|
927
|
+
if p_idx < len(node.args):
|
|
928
|
+
param_arg_map[param_name] = self._expr_to_str(node.args[p_idx])
|
|
929
|
+
|
|
930
|
+
if func_name in self._func_ta_ranges:
|
|
931
|
+
start, end = self._func_ta_ranges[func_name]
|
|
932
|
+
|
|
933
|
+
# Map local derived length variables back to expressions over the
|
|
934
|
+
# function's parameters before substituting call-site arguments.
|
|
935
|
+
local_defs = self._func_local_length_defs(func_def)
|
|
936
|
+
|
|
937
|
+
def _subst_params(arg: str, pmap: dict[str, str]) -> str:
|
|
938
|
+
import re
|
|
939
|
+
result = arg
|
|
940
|
+
for param, value in sorted(
|
|
941
|
+
pmap.items(), key=lambda item: len(item[0]), reverse=True):
|
|
942
|
+
result = re.sub(rf'\b{re.escape(param)}\b', value, result)
|
|
943
|
+
return result
|
|
944
|
+
|
|
945
|
+
def _expand_locals(arg: str) -> str:
|
|
946
|
+
import re
|
|
947
|
+
if not local_defs:
|
|
948
|
+
return arg
|
|
949
|
+
for _ in range(32):
|
|
950
|
+
def _rep(match: re.Match) -> str:
|
|
951
|
+
name = match.group(0)
|
|
952
|
+
if name in local_defs:
|
|
953
|
+
return "(" + local_defs[name] + ")"
|
|
954
|
+
return name
|
|
955
|
+
expanded = re.sub(r"[A-Za-z_][A-Za-z_0-9]*", _rep, arg)
|
|
956
|
+
if expanded == arg:
|
|
957
|
+
break
|
|
958
|
+
arg = expanded
|
|
959
|
+
return arg
|
|
960
|
+
|
|
961
|
+
import re as _re
|
|
962
|
+
enclosing_params: set[str] = set()
|
|
963
|
+
for names in self._enclosing_func_params:
|
|
964
|
+
enclosing_params |= names
|
|
965
|
+
|
|
966
|
+
if cs_idx == 0:
|
|
967
|
+
# cs0 owns the source-level sites. Preserve their parameterized
|
|
968
|
+
# ctor args for every later direct or inherited clone.
|
|
969
|
+
for i in range(start, end):
|
|
970
|
+
site = self._ta_call_sites[i]
|
|
971
|
+
if not hasattr(site, '_orig_ctor_args'):
|
|
972
|
+
site._orig_ctor_args = [
|
|
973
|
+
_expand_locals(arg) for arg in site.ctor_args
|
|
974
|
+
]
|
|
975
|
+
site.ctor_args = [
|
|
976
|
+
_subst_params(arg, param_arg_map)
|
|
977
|
+
for arg in site._orig_ctor_args
|
|
978
|
+
]
|
|
979
|
+
# If a ctor is now expressed in an enclosing UDF's params,
|
|
980
|
+
# retain that expression so the enclosing call can resolve
|
|
981
|
+
# it and widen the enclosing TA range as before.
|
|
982
|
+
if enclosing_params and self._nested_ta_touched is not None:
|
|
983
|
+
for arg in site.ctor_args:
|
|
984
|
+
tokens = set(_re.findall(
|
|
985
|
+
r"[A-Za-z_][A-Za-z_0-9]*", arg))
|
|
986
|
+
if tokens & enclosing_params:
|
|
987
|
+
site._orig_ctor_args = list(site.ctor_args)
|
|
988
|
+
self._nested_ta_touched.add(i)
|
|
989
|
+
break
|
|
990
|
+
else:
|
|
991
|
+
clone_name_map: dict[str, str] = {}
|
|
992
|
+
for i in range(start, end):
|
|
993
|
+
orig = self._ta_call_sites[i]
|
|
994
|
+
orig_args = getattr(orig, '_orig_ctor_args', orig.ctor_args)
|
|
995
|
+
resolved_ctor = [
|
|
996
|
+
_subst_params(arg, param_arg_map) for arg in orig_args
|
|
997
|
+
]
|
|
998
|
+
clone_name = f"{orig.member_name}_cs{cs_idx}"
|
|
999
|
+
existing = next(
|
|
1000
|
+
(site for site in self._ta_call_sites
|
|
1001
|
+
if site.member_name == clone_name),
|
|
1002
|
+
None,
|
|
1003
|
+
)
|
|
1004
|
+
if (reuse_existing_owner is not None
|
|
1005
|
+
and existing is not None
|
|
1006
|
+
and existing.owner_func == reuse_existing_owner):
|
|
1007
|
+
# The active parent's widened range already made the
|
|
1008
|
+
# exact member this inherited callee variant needs.
|
|
1009
|
+
continue
|
|
1010
|
+
if clone_name in self._ta_member_names:
|
|
1011
|
+
base = clone_name
|
|
1012
|
+
suffix = 2
|
|
1013
|
+
while clone_name in self._ta_member_names:
|
|
1014
|
+
clone_name = f"{base}_u{suffix}"
|
|
1015
|
+
suffix += 1
|
|
1016
|
+
clone_name_map[orig.member_name] = clone_name
|
|
1017
|
+
cloned = TACallSite(
|
|
1018
|
+
member_name=clone_name,
|
|
1019
|
+
class_name=orig.class_name,
|
|
1020
|
+
ctor_args=resolved_ctor,
|
|
1021
|
+
compute_args=orig.compute_args[:],
|
|
1022
|
+
returns_tuple=orig.returns_tuple,
|
|
1023
|
+
node=orig.node,
|
|
1024
|
+
is_static=orig.is_static,
|
|
1025
|
+
owner_func=func_name,
|
|
1026
|
+
)
|
|
1027
|
+
self._ta_call_sites.append(cloned)
|
|
1028
|
+
self._ta_member_names.add(clone_name)
|
|
1029
|
+
if clone_name_map:
|
|
1030
|
+
self._func_cs_ta_clone_names[(func_name, cs_idx)] = clone_name_map
|
|
1031
|
+
|
|
1032
|
+
# fixnan is stateful for the same reason as a rolling TA reducer: each
|
|
1033
|
+
# emitted function variant needs its own previous-value member.
|
|
1034
|
+
fn_indices = self._func_fixnan_indices.get(func_name, [])
|
|
1035
|
+
if cs_idx > 0 and fn_indices:
|
|
1036
|
+
clone_map: dict[str, str] = {}
|
|
1037
|
+
for fi in fn_indices:
|
|
1038
|
+
orig = self._fixnan_sites[fi]
|
|
1039
|
+
clone_name = f"{orig.member_name}_cs{cs_idx}"
|
|
1040
|
+
existing = next(
|
|
1041
|
+
(site for site in self._fixnan_sites
|
|
1042
|
+
if site.member_name == clone_name),
|
|
1043
|
+
None,
|
|
1044
|
+
)
|
|
1045
|
+
if (reuse_existing_owner is not None
|
|
1046
|
+
and existing is not None
|
|
1047
|
+
and existing.owner_func == reuse_existing_owner):
|
|
1048
|
+
continue
|
|
1049
|
+
if clone_name in self._fixnan_member_names:
|
|
1050
|
+
base = clone_name
|
|
1051
|
+
suffix = 2
|
|
1052
|
+
while clone_name in self._fixnan_member_names:
|
|
1053
|
+
clone_name = f"{base}_u{suffix}"
|
|
1054
|
+
suffix += 1
|
|
1055
|
+
clone_map[orig.member_name] = clone_name
|
|
1056
|
+
cloned = FixnanCallSite(
|
|
1057
|
+
member_name=clone_name,
|
|
1058
|
+
pine_type=orig.pine_type,
|
|
1059
|
+
node=orig.node,
|
|
1060
|
+
owner_func=func_name,
|
|
1061
|
+
)
|
|
1062
|
+
self._fixnan_sites.append(cloned)
|
|
1063
|
+
self._fixnan_member_names.add(clone_name)
|
|
1064
|
+
if clone_map:
|
|
1065
|
+
self._func_cs_fixnan_clone_names[(func_name, cs_idx)] = clone_map
|
|
1066
|
+
|
|
851
1067
|
def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
|
|
852
1068
|
"""Handle calls to user-defined functions."""
|
|
853
1069
|
func_def = self._func_defs[func_name]
|
|
@@ -899,137 +1115,29 @@ class CallHandlers:
|
|
|
899
1115
|
arg = node.args[p_idx]
|
|
900
1116
|
if isinstance(arg, Identifier) and arg.name in BAR_FIELDS:
|
|
901
1117
|
self._series_bar_fields.add(arg.name)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1118
|
+
elif isinstance(arg, Identifier):
|
|
1119
|
+
sym = self._symbols.resolve(arg.name)
|
|
1120
|
+
spec = getattr(sym, "type_spec", None) if sym is not None else None
|
|
1121
|
+
if spec is not None and spec.kind in ("array", "map", "matrix"):
|
|
1122
|
+
continue
|
|
1123
|
+
if sym is not None:
|
|
1124
|
+
sym.is_series = True
|
|
1125
|
+
if sym.scope and sym.scope.startswith("func_"):
|
|
1126
|
+
caller_name = sym.scope[5:]
|
|
1127
|
+
self._func_series_vars.setdefault(caller_name, set()).add(arg.name)
|
|
1128
|
+
else:
|
|
1129
|
+
self._series_vars.add(arg.name)
|
|
1130
|
+
|
|
1131
|
+
# Per-call-site cloning: TA, series/var, and fixnan state all advance
|
|
1132
|
+
# across bars/calls and therefore require isolated UDF variants.
|
|
907
1133
|
has_ta = func_name in self._func_ta_ranges
|
|
908
1134
|
has_series = func_name in self._func_series_vars or func_name in self._func_var_members
|
|
909
|
-
|
|
1135
|
+
has_fixnan = func_name in self._func_fixnan_indices
|
|
1136
|
+
if has_ta or has_series or has_fixnan:
|
|
910
1137
|
cs_idx = self._func_call_site_count.get(func_name, 0)
|
|
911
1138
|
self._func_call_site_count[func_name] = cs_idx + 1
|
|
912
1139
|
self._func_call_cs_map[id(node)] = (func_name, cs_idx)
|
|
913
|
-
|
|
914
|
-
# Build parameter -> call-site argument string mapping
|
|
915
|
-
param_arg_map: dict[str, str] = {}
|
|
916
|
-
for p_idx, param_name in enumerate(func_def.params):
|
|
917
|
-
if p_idx < len(node.args):
|
|
918
|
-
param_arg_map[param_name] = self._expr_to_str(node.args[p_idx])
|
|
919
|
-
|
|
920
|
-
# Clone TA call sites (only if function has TA ranges)
|
|
921
|
-
if has_ta:
|
|
922
|
-
start, end = self._func_ta_ranges[func_name]
|
|
923
|
-
|
|
924
|
-
# Map of this function's local (non-param, non-series) derived
|
|
925
|
-
# length vars to their raw RHS expression strings, e.g.
|
|
926
|
-
# ``qqeCalc`` => ``wp = sf * 2 - 1`` -> {"wp": "sf * 2 - 1"}.
|
|
927
|
-
# A TA ctor arg captured as the bare local name ("wp") must be
|
|
928
|
-
# expanded to its definition so the subsequent param-substitution
|
|
929
|
-
# turns it into a class-scope expression ("rsiSmooth * 2 - 1")
|
|
930
|
-
# rather than leaving a dangling local that degenerates to
|
|
931
|
-
# period 1 in codegen.
|
|
932
|
-
local_defs = self._func_local_length_defs(func_def)
|
|
933
|
-
|
|
934
|
-
def _subst_params(arg: str, pmap: dict[str, str]) -> str:
|
|
935
|
-
"""Substitute parameter names in an expression string.
|
|
936
|
-
|
|
937
|
-
Handles both exact matches ('len' -> 'len3') and parameter
|
|
938
|
-
names within expressions ('len / 2' -> 'len3 / 2').
|
|
939
|
-
"""
|
|
940
|
-
import re
|
|
941
|
-
result = arg
|
|
942
|
-
# Sort by length descending to avoid partial replacements
|
|
943
|
-
for param, value in sorted(pmap.items(), key=lambda x: len(x[0]), reverse=True):
|
|
944
|
-
result = re.sub(rf'\b{re.escape(param)}\b', value, result)
|
|
945
|
-
return result
|
|
946
|
-
|
|
947
|
-
def _expand_locals(arg: str) -> str:
|
|
948
|
-
"""Recursively expand function-local length vars to their RHS
|
|
949
|
-
(parenthesized) so only params / class-scope names remain."""
|
|
950
|
-
import re
|
|
951
|
-
if not local_defs:
|
|
952
|
-
return arg
|
|
953
|
-
for _ in range(32):
|
|
954
|
-
def _rep(m: re.Match) -> str:
|
|
955
|
-
nm = m.group(0)
|
|
956
|
-
if nm in local_defs:
|
|
957
|
-
return "(" + local_defs[nm] + ")"
|
|
958
|
-
return nm
|
|
959
|
-
new = re.sub(r"[A-Za-z_][A-Za-z_0-9]*", _rep, arg)
|
|
960
|
-
if new == arg:
|
|
961
|
-
break
|
|
962
|
-
arg = new
|
|
963
|
-
return arg
|
|
964
|
-
|
|
965
|
-
# Params of the function we are *currently inside* (if this is a
|
|
966
|
-
# nested user-func call). Used to detect when a substituted ctor
|
|
967
|
-
# arg becomes parameterized by the OUTER function, so the outer
|
|
968
|
-
# call site can resolve it (f_bbwp's _bbwLen -> i_bbwLen reaches
|
|
969
|
-
# f_basisMa's sites).
|
|
970
|
-
import re as _re
|
|
971
|
-
enclosing_params: set[str] = set()
|
|
972
|
-
for s in self._enclosing_func_params:
|
|
973
|
-
enclosing_params |= s
|
|
974
|
-
|
|
975
|
-
if cs_idx == 0:
|
|
976
|
-
# First call site: save original param-based ctor_args for future cloning,
|
|
977
|
-
# then resolve to actual call-site values
|
|
978
|
-
for i in range(start, end):
|
|
979
|
-
site = self._ta_call_sites[i]
|
|
980
|
-
if not hasattr(site, '_orig_ctor_args'):
|
|
981
|
-
site._orig_ctor_args = [
|
|
982
|
-
_expand_locals(a) for a in site.ctor_args
|
|
983
|
-
]
|
|
984
|
-
site.ctor_args = [_subst_params(a, param_arg_map) for a in site._orig_ctor_args]
|
|
985
|
-
# If a substituted arg is now expressed in terms of an
|
|
986
|
-
# enclosing function's params, promote it to the original
|
|
987
|
-
# so the enclosing call re-substitutes, and mark the site
|
|
988
|
-
# so the enclosing function's TA range widens to cover it.
|
|
989
|
-
if enclosing_params and self._nested_ta_touched is not None:
|
|
990
|
-
for a in site.ctor_args:
|
|
991
|
-
toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", a))
|
|
992
|
-
if toks & enclosing_params:
|
|
993
|
-
site._orig_ctor_args = list(site.ctor_args)
|
|
994
|
-
self._nested_ta_touched.add(i)
|
|
995
|
-
break
|
|
996
|
-
else:
|
|
997
|
-
# Subsequent call sites: clone using saved original param names,
|
|
998
|
-
# substituted with this call site's arguments
|
|
999
|
-
clone_name_map: dict[str, str] = {}
|
|
1000
|
-
for i in range(start, end):
|
|
1001
|
-
orig = self._ta_call_sites[i]
|
|
1002
|
-
orig_args = getattr(orig, '_orig_ctor_args', orig.ctor_args)
|
|
1003
|
-
resolved_ctor = [_subst_params(a, param_arg_map) for a in orig_args]
|
|
1004
|
-
# Default name follows the ``{base}_cs{cs_idx}`` formula the
|
|
1005
|
-
# codegen re-derives. But the SAME base TA site can be reached
|
|
1006
|
-
# through more than one enclosing function (e.g. a helper cloned
|
|
1007
|
-
# both via its own call sites AND via a range-widened outer
|
|
1008
|
-
# function), so two distinct (func, cs_idx) namespaces can mint
|
|
1009
|
-
# the same name. Detect that collision and fall back to a
|
|
1010
|
-
# globally-unique name; record the chosen name so the codegen
|
|
1011
|
-
# consumes it verbatim (see _func_cs_ta_clone_names).
|
|
1012
|
-
clone_name = f"{orig.member_name}_cs{cs_idx}"
|
|
1013
|
-
if clone_name in self._ta_member_names:
|
|
1014
|
-
base = clone_name
|
|
1015
|
-
n = 2
|
|
1016
|
-
while clone_name in self._ta_member_names:
|
|
1017
|
-
clone_name = f"{base}_u{n}"
|
|
1018
|
-
n += 1
|
|
1019
|
-
clone_name_map[orig.member_name] = clone_name
|
|
1020
|
-
cloned = TACallSite(
|
|
1021
|
-
member_name=clone_name,
|
|
1022
|
-
class_name=orig.class_name,
|
|
1023
|
-
ctor_args=resolved_ctor,
|
|
1024
|
-
compute_args=orig.compute_args[:],
|
|
1025
|
-
returns_tuple=orig.returns_tuple,
|
|
1026
|
-
node=orig.node,
|
|
1027
|
-
is_static=orig.is_static,
|
|
1028
|
-
)
|
|
1029
|
-
self._ta_call_sites.append(cloned)
|
|
1030
|
-
self._ta_member_names.add(clone_name)
|
|
1031
|
-
if clone_name_map:
|
|
1032
|
-
self._func_cs_ta_clone_names[(func_name, cs_idx)] = clone_name_map
|
|
1140
|
+
self._materialize_user_func_call_site_state(func_name, cs_idx, node)
|
|
1033
1141
|
|
|
1034
1142
|
# Create or update FuncInfo
|
|
1035
1143
|
is_tuple = self._func_returns_tuple.get(func_name, False)
|
|
@@ -1041,7 +1149,10 @@ class CallHandlers:
|
|
|
1041
1149
|
# Per-param TypeSpec: declared hints are authoritative; for untyped
|
|
1042
1150
|
# params, infer from the call-site argument's type_spec (so an untyped
|
|
1043
1151
|
# ``s`` used as a string, or a UDT passed by value, emits correctly).
|
|
1044
|
-
param_specs =
|
|
1152
|
+
param_specs = list(
|
|
1153
|
+
getattr(self, "_func_param_type_specs", {}).get(func_name)
|
|
1154
|
+
or self._param_type_specs_from_def(func_def)
|
|
1155
|
+
)
|
|
1045
1156
|
arg_specs = [self._type_spec_from_expr(arg) for arg in node.args]
|
|
1046
1157
|
for i in range(len(param_specs)):
|
|
1047
1158
|
if param_specs[i] is None and i < len(arg_specs):
|
|
@@ -37,6 +37,20 @@ class TACallSite:
|
|
|
37
37
|
returns_tuple: bool # e.g., MACD, supertrend
|
|
38
38
|
node: Any = None # the FuncCall AST node
|
|
39
39
|
is_static: bool = False # true if global scope & arguments are recursively static
|
|
40
|
+
# Name of the user function that OWNS this site, or None for a top-level
|
|
41
|
+
# site. For an ORIGINAL site this is the function whose body textually
|
|
42
|
+
# contains the ``ta.*`` call. For a CLONE minted in
|
|
43
|
+
# ``_handle_user_func_call`` this is the CALLEE (the function being
|
|
44
|
+
# called, NOT the caller whose body visit triggered the clone) -- the
|
|
45
|
+
# clone belongs to the callee's per-call-site namespace.
|
|
46
|
+
#
|
|
47
|
+
# The codegen's dead-code pass keys off this rather than off
|
|
48
|
+
# ``func_ta_ranges`` slices, because a function's slice can include
|
|
49
|
+
# clones of ANOTHER (live) function's sites (minted while visiting a
|
|
50
|
+
# caller's body). Marking such a borrowed clone dead would leave the
|
|
51
|
+
# owning callee's emitted clone body referencing undeclared members
|
|
52
|
+
# (regression: quantbyboji-nq-hma-midday ``_ta_change_*_cs1``).
|
|
53
|
+
owner_func: str | None = None
|
|
40
54
|
|
|
41
55
|
|
|
42
56
|
@dataclass
|
|
@@ -87,6 +101,13 @@ class FixnanCallSite:
|
|
|
87
101
|
"""Per-call-site state for ``fixnan(...)`` (one previous-value member each)."""
|
|
88
102
|
member_name: str # e.g., "_prev_fixnan_1"
|
|
89
103
|
pine_type: Any # PineType
|
|
104
|
+
node: Any = None # the FuncCall AST node (for variant-aware lookup)
|
|
105
|
+
# Name of the user function that OWNS this site, or None for a top-level
|
|
106
|
+
# site. Mirrors ``TACallSite.owner_func``: the codegen's dead-code pass
|
|
107
|
+
# and per-variant clone logic key off this so a fixnan site minted inside
|
|
108
|
+
# a dead caller's body but cloned for a live callee survives, and each
|
|
109
|
+
# emitted function variant references its OWN fixnan member.
|
|
110
|
+
owner_func: str | None = None
|
|
90
111
|
|
|
91
112
|
|
|
92
113
|
@dataclass
|
|
@@ -164,6 +185,14 @@ class AnalyzerContext:
|
|
|
164
185
|
var_members: list = field(default_factory=list) # [(name, PineType, init_expr_str)]
|
|
165
186
|
func_infos: list = field(default_factory=list)
|
|
166
187
|
fixnan_sites: list = field(default_factory=list)
|
|
188
|
+
# Per-function fixnan site ownership (func_name -> list of indices into
|
|
189
|
+
# ``fixnan_sites``). Used by the codegen to clone fixnan members per
|
|
190
|
+
# call-site variant and to skip fixnan state owned by dead functions.
|
|
191
|
+
func_fixnan_indices: dict = field(default_factory=dict)
|
|
192
|
+
# (func_name, cs_idx) -> {orig_member_name: cloned_member_name}. Like
|
|
193
|
+
# ``func_cs_ta_clone_names`` but for fixnan: populated only when the
|
|
194
|
+
# default ``{base}_cs{cs_idx}`` clone name collides.
|
|
195
|
+
func_cs_fixnan_clone_names: dict = field(default_factory=dict)
|
|
167
196
|
strategy_params: dict = field(default_factory=dict)
|
|
168
197
|
diagnostics: list = field(default_factory=list) # warnings
|
|
169
198
|
filename: str = "<stdin>"
|
|
@@ -89,8 +89,22 @@ class DiagnosticsHelper:
|
|
|
89
89
|
loc,
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
+
# Non-atomic node kinds whose serialized infix form must be parenthesized
|
|
93
|
+
# when used as an operand, so the string re-parses to the SAME tree. A TA
|
|
94
|
+
# ctor-arg string produced here is later re-parsed and lowered by the
|
|
95
|
+
# codegen (``_runtime_ctor_arg_for_reset``); a flattened ``(a - b) / c``
|
|
96
|
+
# would otherwise silently reassociate under C++ precedence.
|
|
97
|
+
_NONATOMIC_EXPR_NODES = (BinOp, UnaryOp, Ternary)
|
|
98
|
+
|
|
99
|
+
def _operand_to_str(self, node: ASTNode) -> str:
|
|
100
|
+
s = self._expr_to_str(node)
|
|
101
|
+
if isinstance(node, self._NONATOMIC_EXPR_NODES):
|
|
102
|
+
return f"({s})"
|
|
103
|
+
return s
|
|
104
|
+
|
|
92
105
|
def _expr_to_str(self, node: ASTNode) -> str:
|
|
93
|
-
"""Convert an expression node to a
|
|
106
|
+
"""Convert an expression node to a string that re-parses to the same
|
|
107
|
+
tree (grouping preserved for non-atomic operands)."""
|
|
94
108
|
if isinstance(node, NumberLiteral):
|
|
95
109
|
return str(node.value)
|
|
96
110
|
if isinstance(node, StringLiteral):
|
|
@@ -104,9 +118,9 @@ class DiagnosticsHelper:
|
|
|
104
118
|
if isinstance(node, MemberAccess):
|
|
105
119
|
return f"{self._expr_to_str(node.object)}.{node.member}"
|
|
106
120
|
if isinstance(node, BinOp):
|
|
107
|
-
return f"{self.
|
|
121
|
+
return f"{self._operand_to_str(node.left)} {node.op} {self._operand_to_str(node.right)}"
|
|
108
122
|
if isinstance(node, UnaryOp):
|
|
109
|
-
return f"{node.op}{self.
|
|
123
|
+
return f"{node.op}{self._operand_to_str(node.operand)}"
|
|
110
124
|
if isinstance(node, FuncCall):
|
|
111
125
|
args = ", ".join(self._expr_to_str(a) for a in node.args)
|
|
112
126
|
callee_str = self._expr_to_str(node.callee)
|
|
@@ -114,5 +128,5 @@ class DiagnosticsHelper:
|
|
|
114
128
|
if isinstance(node, Subscript):
|
|
115
129
|
return f"{self._expr_to_str(node.object)}[{self._expr_to_str(node.index)}]"
|
|
116
130
|
if isinstance(node, Ternary):
|
|
117
|
-
return f"{self.
|
|
131
|
+
return f"{self._operand_to_str(node.condition)} ? {self._operand_to_str(node.true_val)} : {self._operand_to_str(node.false_val)}"
|
|
118
132
|
return "<?>"
|
|
@@ -110,7 +110,7 @@ TA_CLASS_MAP = {
|
|
|
110
110
|
TA_PERIOD_ARG = {
|
|
111
111
|
"sma": 1, "ema": 1, "rma": 1, "rsi": 1, "atr": 0,
|
|
112
112
|
"highest": 1, "lowest": 1, "change": 1,
|
|
113
|
-
"wma": 1, "hma": 1,
|
|
113
|
+
"wma": 1, "hma": 1,
|
|
114
114
|
# Task 6
|
|
115
115
|
"sum": 1,
|
|
116
116
|
# Task 7 Batch 1
|
|
@@ -119,7 +119,7 @@ TA_PERIOD_ARG = {
|
|
|
119
119
|
"mom": 1, "roc": 1, "rising": 1, "falling": 1, "cci": 1,
|
|
120
120
|
# cum has no period arg — handled in TA_NO_CTOR
|
|
121
121
|
# Task 7 Batch 3
|
|
122
|
-
"
|
|
122
|
+
"median": 1, "highestbars": 1, "lowestbars": 1,
|
|
123
123
|
# Batch 4
|
|
124
124
|
"cmo": 1, "cog": 1, "correlation": 2,
|
|
125
125
|
"percentile_nearest_rank": 1, "percentile_linear_interpolation": 1,
|
|
@@ -130,6 +130,14 @@ TA_PERIOD_ARG = {
|
|
|
130
130
|
|
|
131
131
|
# Functions that return tuples
|
|
132
132
|
TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands"}
|
|
133
|
+
TA_TUPLE_ELEMENT_COUNTS = {
|
|
134
|
+
"macd": 3,
|
|
135
|
+
"supertrend": 2,
|
|
136
|
+
"dmi": 3,
|
|
137
|
+
"bb": 3,
|
|
138
|
+
"kc": 3,
|
|
139
|
+
"vwap_bands": 3,
|
|
140
|
+
}
|
|
133
141
|
|
|
134
142
|
# Functions with multiple constructor args
|
|
135
143
|
TA_MULTI_CTOR = {
|
|
@@ -156,6 +164,16 @@ TA_MULTI_CTOR = {
|
|
|
156
164
|
"bbw": [1, 2], # length, mult
|
|
157
165
|
"kcw": [1, 2], # length, mult
|
|
158
166
|
"tr": [0], # handle_na (compile-time bool)
|
|
167
|
+
"stdev": [1, 2], # length, biased
|
|
168
|
+
"variance": [1, 2], # length, biased
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Compute-arg indices: which positional args are forwarded to ``.compute()``.
|
|
172
|
+
# Entries here override the default analyzer behavior of forwarding every
|
|
173
|
+
# non-constructor argument.
|
|
174
|
+
TA_COMPUTE_ARGS = {
|
|
175
|
+
"stdev": [0],
|
|
176
|
+
"variance": [0],
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
# No-state functions (no constructor args, stateless or self-contained)
|