@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
|
@@ -66,8 +66,8 @@ from __future__ import annotations
|
|
|
66
66
|
from typing import Any
|
|
67
67
|
|
|
68
68
|
from ..ast_nodes import (
|
|
69
|
-
ASTNode, BoolLiteral, FuncCall, Identifier, MemberAccess,
|
|
70
|
-
NumberLiteral, StringLiteral, TupleLiteral,
|
|
69
|
+
ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, MemberAccess,
|
|
70
|
+
NumberLiteral, StringLiteral, TupleLiteral, UnaryOp, VarDecl,
|
|
71
71
|
)
|
|
72
72
|
from ..symbols import PineType
|
|
73
73
|
from .. import signatures as sigs
|
|
@@ -207,6 +207,7 @@ class CallHandlers:
|
|
|
207
207
|
is_static=is_static,
|
|
208
208
|
)
|
|
209
209
|
self._ta_call_sites.append(site)
|
|
210
|
+
self._ta_member_names.add(site.member_name)
|
|
210
211
|
return PineType.FLOAT
|
|
211
212
|
|
|
212
213
|
# Determine constructor args
|
|
@@ -249,9 +250,29 @@ class CallHandlers:
|
|
|
249
250
|
is_static=is_static,
|
|
250
251
|
)
|
|
251
252
|
self._ta_call_sites.append(site)
|
|
253
|
+
self._ta_member_names.add(site.member_name)
|
|
252
254
|
|
|
253
255
|
return PineType.FLOAT
|
|
254
256
|
|
|
257
|
+
def _security_symbol_is_heikinashi(self, node, _seen=None) -> bool:
|
|
258
|
+
"""True when a request.security symbol is ``ticker.heikinashi(<chart
|
|
259
|
+
symbol>)`` — directly, or via a global alias (``haTicker =
|
|
260
|
+
ticker.heikinashi(syminfo.tickerid)``). Name-cycle-guarded. The
|
|
261
|
+
support_checker has already rejected the cross-symbol HA case, so any HA
|
|
262
|
+
reaching here is the chart's own symbol."""
|
|
263
|
+
if _seen is None:
|
|
264
|
+
_seen = set()
|
|
265
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, MemberAccess)
|
|
266
|
+
and isinstance(node.callee.object, Identifier)
|
|
267
|
+
and node.callee.object.name == "ticker"
|
|
268
|
+
and node.callee.member == "heikinashi"):
|
|
269
|
+
return True
|
|
270
|
+
if (isinstance(node, Identifier) and node.name in self._global_expr_map
|
|
271
|
+
and node.name not in _seen):
|
|
272
|
+
_seen.add(node.name)
|
|
273
|
+
return self._security_symbol_is_heikinashi(self._global_expr_map[node.name], _seen)
|
|
274
|
+
return False
|
|
275
|
+
|
|
255
276
|
def _handle_request_call(self, func_name: str, node: FuncCall) -> PineType:
|
|
256
277
|
"""Handle request.* function calls."""
|
|
257
278
|
if func_name == "security":
|
|
@@ -293,6 +314,15 @@ class CallHandlers:
|
|
|
293
314
|
lookahead_node = all_args[4] if len(all_args) > 4 else None
|
|
294
315
|
|
|
295
316
|
mutable_globals = tuple(sorted(self._collect_security_mutable_globals(expr_node)))
|
|
317
|
+
# Heikin-Ashi same-symbol read: request.security(ticker.heikinashi(
|
|
318
|
+
# syminfo.tickerid), ...) (directly or via a global alias). The engine
|
|
319
|
+
# applies the HA candle transform inside the security eval.
|
|
320
|
+
symbol_node = all_args[0] if all_args else None
|
|
321
|
+
heikinashi = self._security_symbol_is_heikinashi(symbol_node)
|
|
322
|
+
# Capture the user function (if any) whose body contains this call,
|
|
323
|
+
# so the codegen can resolve a parameter ``tf`` via the call sites.
|
|
324
|
+
scope_name = self._symbols.current_scope.name
|
|
325
|
+
containing_func = scope_name[5:] if scope_name.startswith("func_") else ""
|
|
296
326
|
self._security_calls.append(SecurityCallInfo(
|
|
297
327
|
sec_id=sec_id,
|
|
298
328
|
timeframe=tf_node,
|
|
@@ -302,8 +332,10 @@ class CallHandlers:
|
|
|
302
332
|
gaps=gaps_node,
|
|
303
333
|
lookahead=lookahead_node,
|
|
304
334
|
ta_range=security_ta_range,
|
|
335
|
+
heikinashi=heikinashi,
|
|
305
336
|
depends_on_mutable_globals=bool(mutable_globals),
|
|
306
337
|
mutable_globals=mutable_globals,
|
|
338
|
+
containing_func=containing_func,
|
|
307
339
|
))
|
|
308
340
|
|
|
309
341
|
return PineType.FLOAT
|
|
@@ -765,6 +797,57 @@ class CallHandlers:
|
|
|
765
797
|
# User-defined function calls
|
|
766
798
|
# ------------------------------------------------------------------
|
|
767
799
|
|
|
800
|
+
def _func_local_length_defs(self, func_def) -> dict[str, str]:
|
|
801
|
+
"""Collect a user function's local scalar length-vars to their RHS
|
|
802
|
+
expression string, e.g. ``qqeCalc`` with ``wp = sf * 2 - 1`` returns
|
|
803
|
+
``{"wp": "sf * 2 - 1"}``.
|
|
804
|
+
|
|
805
|
+
Only plain (non-``var``/``varip``) declarations whose RHS is a pure
|
|
806
|
+
arithmetic expression over identifiers/numbers (NumberLiteral, Identifier,
|
|
807
|
+
BinOp, UnaryOp, or a math.* FuncCall) qualify — these are the shapes that
|
|
808
|
+
can legitimately feed a TA constructor length. Series-valued locals (whose
|
|
809
|
+
RHS is a ta.* call, a subscript, a ternary, etc.) are skipped so we never
|
|
810
|
+
inline a price series into a ctor-length slot. Names reassigned with ``:=``
|
|
811
|
+
are also skipped (their value is not a stable compile-time length).
|
|
812
|
+
"""
|
|
813
|
+
def _is_arith(n) -> bool:
|
|
814
|
+
if isinstance(n, (NumberLiteral, Identifier)):
|
|
815
|
+
return True
|
|
816
|
+
if isinstance(n, BinOp):
|
|
817
|
+
return _is_arith(n.left) and _is_arith(n.right)
|
|
818
|
+
if isinstance(n, UnaryOp):
|
|
819
|
+
return _is_arith(n.operand)
|
|
820
|
+
if isinstance(n, FuncCall):
|
|
821
|
+
# Allow math.* helpers (math.round/sqrt/...) over arith args.
|
|
822
|
+
callee = n.callee
|
|
823
|
+
if (isinstance(callee, MemberAccess)
|
|
824
|
+
and isinstance(callee.object, Identifier)
|
|
825
|
+
and callee.object.name == "math"):
|
|
826
|
+
return all(_is_arith(a) for a in n.args)
|
|
827
|
+
return False
|
|
828
|
+
|
|
829
|
+
reassigned: set[str] = set()
|
|
830
|
+
def _scan_reassign(stmts):
|
|
831
|
+
from ..ast_nodes import Assignment
|
|
832
|
+
for s in stmts or []:
|
|
833
|
+
if isinstance(s, Assignment) and isinstance(s.target, Identifier):
|
|
834
|
+
reassigned.add(s.target.name)
|
|
835
|
+
for attr in ("body", "else_body"):
|
|
836
|
+
sub = getattr(s, attr, None)
|
|
837
|
+
if isinstance(sub, list):
|
|
838
|
+
_scan_reassign(sub)
|
|
839
|
+
_scan_reassign(func_def.body)
|
|
840
|
+
|
|
841
|
+
defs: dict[str, str] = {}
|
|
842
|
+
for stmt in func_def.body or []:
|
|
843
|
+
if (isinstance(stmt, VarDecl)
|
|
844
|
+
and not stmt.is_var and not stmt.is_varip
|
|
845
|
+
and stmt.name not in reassigned
|
|
846
|
+
and stmt.value is not None
|
|
847
|
+
and _is_arith(stmt.value)):
|
|
848
|
+
defs[stmt.name] = self._expr_to_str(stmt.value)
|
|
849
|
+
return defs
|
|
850
|
+
|
|
768
851
|
def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
|
|
769
852
|
"""Handle calls to user-defined functions."""
|
|
770
853
|
func_def = self._func_defs[func_name]
|
|
@@ -783,14 +866,28 @@ class CallHandlers:
|
|
|
783
866
|
# For now, use the cached return type from initial analysis
|
|
784
867
|
return_type = self._func_return_types.get(func_name, PineType.FLOAT)
|
|
785
868
|
|
|
786
|
-
# If the return type was UNKNOWN or VOID, infer
|
|
869
|
+
# If the return type was UNKNOWN or VOID, infer it ONLY when the body
|
|
870
|
+
# is a single bare identifier that returns a parameter directly
|
|
871
|
+
# (``f(s) => s``). Inferring from params for arbitrary bodies misfires
|
|
872
|
+
# when a function merely HAS a string param but returns something else
|
|
873
|
+
# (e.g. ``getLineStyle(s) => switch s ... => line.style_solid`` or a
|
|
874
|
+
# body ending in ``label.new(...)``). Other cases rely on the cached
|
|
875
|
+
# body type plus udt_return_type / tuple inference.
|
|
787
876
|
if return_type in (PineType.UNKNOWN, PineType.VOID):
|
|
788
|
-
if
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
877
|
+
if (func_def.is_single_expr and func_def.body
|
|
878
|
+
and isinstance(func_def.body[0], ExprStmt)
|
|
879
|
+
and isinstance(func_def.body[0].expr, Identifier)):
|
|
880
|
+
ret_name = func_def.body[0].expr.name
|
|
881
|
+
for idx, pname in enumerate(func_def.params):
|
|
882
|
+
if pname == ret_name and idx < len(param_types):
|
|
883
|
+
pt = param_types[idx]
|
|
884
|
+
if pt == PineType.STRING:
|
|
885
|
+
return_type = PineType.STRING
|
|
886
|
+
elif pt == PineType.INT:
|
|
887
|
+
return_type = PineType.INT
|
|
888
|
+
elif pt == PineType.FLOAT:
|
|
889
|
+
return_type = PineType.FLOAT
|
|
890
|
+
break
|
|
794
891
|
|
|
795
892
|
# If this function has series params, ensure bar-field arguments
|
|
796
893
|
# passed at the call site are registered as series_bar_fields so that
|
|
@@ -824,6 +921,16 @@ class CallHandlers:
|
|
|
824
921
|
if has_ta:
|
|
825
922
|
start, end = self._func_ta_ranges[func_name]
|
|
826
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
|
+
|
|
827
934
|
def _subst_params(arg: str, pmap: dict[str, str]) -> str:
|
|
828
935
|
"""Substitute parameter names in an expression string.
|
|
829
936
|
|
|
@@ -837,23 +944,81 @@ class CallHandlers:
|
|
|
837
944
|
result = re.sub(rf'\b{re.escape(param)}\b', value, result)
|
|
838
945
|
return result
|
|
839
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
|
+
|
|
840
975
|
if cs_idx == 0:
|
|
841
976
|
# First call site: save original param-based ctor_args for future cloning,
|
|
842
977
|
# then resolve to actual call-site values
|
|
843
978
|
for i in range(start, end):
|
|
844
979
|
site = self._ta_call_sites[i]
|
|
845
980
|
if not hasattr(site, '_orig_ctor_args'):
|
|
846
|
-
site._orig_ctor_args =
|
|
981
|
+
site._orig_ctor_args = [
|
|
982
|
+
_expand_locals(a) for a in site.ctor_args
|
|
983
|
+
]
|
|
847
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
|
|
848
996
|
else:
|
|
849
997
|
# Subsequent call sites: clone using saved original param names,
|
|
850
998
|
# substituted with this call site's arguments
|
|
999
|
+
clone_name_map: dict[str, str] = {}
|
|
851
1000
|
for i in range(start, end):
|
|
852
1001
|
orig = self._ta_call_sites[i]
|
|
853
1002
|
orig_args = getattr(orig, '_orig_ctor_args', orig.ctor_args)
|
|
854
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
|
|
855
1020
|
cloned = TACallSite(
|
|
856
|
-
member_name=
|
|
1021
|
+
member_name=clone_name,
|
|
857
1022
|
class_name=orig.class_name,
|
|
858
1023
|
ctor_args=resolved_ctor,
|
|
859
1024
|
compute_args=orig.compute_args[:],
|
|
@@ -862,6 +1027,9 @@ class CallHandlers:
|
|
|
862
1027
|
is_static=orig.is_static,
|
|
863
1028
|
)
|
|
864
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
|
|
865
1033
|
|
|
866
1034
|
# Create or update FuncInfo
|
|
867
1035
|
is_tuple = self._func_returns_tuple.get(func_name, False)
|
|
@@ -869,6 +1037,15 @@ class CallHandlers:
|
|
|
869
1037
|
# Forward UDT-return inference (set in _visit_FuncDef) so codegen can
|
|
870
1038
|
# emit the struct return type. Probe: udt-method-probe-20.
|
|
871
1039
|
udt_ret = self._func_udt_return_types.get(func_name)
|
|
1040
|
+
ret_spec = getattr(self, "_func_return_type_specs", {}).get(func_name)
|
|
1041
|
+
# Per-param TypeSpec: declared hints are authoritative; for untyped
|
|
1042
|
+
# params, infer from the call-site argument's type_spec (so an untyped
|
|
1043
|
+
# ``s`` used as a string, or a UDT passed by value, emits correctly).
|
|
1044
|
+
param_specs = self._param_type_specs_from_def(func_def)
|
|
1045
|
+
arg_specs = [self._type_spec_from_expr(arg) for arg in node.args]
|
|
1046
|
+
for i in range(len(param_specs)):
|
|
1047
|
+
if param_specs[i] is None and i < len(arg_specs):
|
|
1048
|
+
param_specs[i] = arg_specs[i]
|
|
872
1049
|
existing = [fi for fi in self._func_infos if fi.name == func_name]
|
|
873
1050
|
if not existing:
|
|
874
1051
|
fi = FuncInfo(
|
|
@@ -879,6 +1056,8 @@ class CallHandlers:
|
|
|
879
1056
|
returns_tuple=is_tuple,
|
|
880
1057
|
tuple_element_count=tuple_count,
|
|
881
1058
|
udt_return_type=udt_ret,
|
|
1059
|
+
param_type_specs=param_specs,
|
|
1060
|
+
return_type_spec=ret_spec,
|
|
882
1061
|
)
|
|
883
1062
|
self._func_infos.append(fi)
|
|
884
1063
|
else:
|
|
@@ -889,7 +1068,17 @@ class CallHandlers:
|
|
|
889
1068
|
for i, pt in enumerate(param_types):
|
|
890
1069
|
if i < len(fi.param_types) and fi.param_types[i] == PineType.UNKNOWN:
|
|
891
1070
|
fi.param_types[i] = pt
|
|
1071
|
+
# Merge per-param TypeSpecs: keep declared hints (authoritative),
|
|
1072
|
+
# fill untyped slots from this call site if still unknown.
|
|
1073
|
+
if not fi.param_type_specs:
|
|
1074
|
+
fi.param_type_specs = list(param_specs)
|
|
1075
|
+
else:
|
|
1076
|
+
for i in range(len(param_specs)):
|
|
1077
|
+
if i < len(fi.param_type_specs) and fi.param_type_specs[i] is None:
|
|
1078
|
+
fi.param_type_specs[i] = param_specs[i]
|
|
892
1079
|
if fi.udt_return_type is None and udt_ret is not None:
|
|
893
1080
|
fi.udt_return_type = udt_ret
|
|
1081
|
+
if fi.return_type_spec is None and ret_spec is not None:
|
|
1082
|
+
fi.return_type_spec = ret_spec
|
|
894
1083
|
|
|
895
1084
|
return return_type
|
|
@@ -65,6 +65,21 @@ class FuncInfo:
|
|
|
65
65
|
# ``Sample s = build_sample(...)`` then ``s.score()`` dispatches
|
|
66
66
|
# correctly. Probe: data/validation/udt-method-probe-20-udt-return-from-func.
|
|
67
67
|
udt_return_type: str | None = None
|
|
68
|
+
# Parallel to ``node.params``; each entry is a ``TypeSpec`` (or ``None``)
|
|
69
|
+
# carrying UDT / drawing-handle / precise-scalar typing that the coarse
|
|
70
|
+
# ``param_types`` (PineType) cannot represent. Populated from the
|
|
71
|
+
# function's declared parameter type hints (authoritative) and, for
|
|
72
|
+
# untyped params, from the call-site argument type. The codegen prefers
|
|
73
|
+
# this over ``param_types`` when emitting each parameter's C++ type so a
|
|
74
|
+
# ``pivot hi`` parameter emits as ``pivot hi`` (not ``double hi``) and an
|
|
75
|
+
# untyped ``s`` used as a string emits as ``std::string s``.
|
|
76
|
+
param_type_specs: list = field(default_factory=list)
|
|
77
|
+
# ``TypeSpec`` of the function's return value when it is a collection the
|
|
78
|
+
# coarse ``return_type`` (PineType) cannot represent — today this covers
|
|
79
|
+
# array-returning functions (``buildPDLevels() => array.from(...)`` ->
|
|
80
|
+
# ``std::vector<double>``). UDT / drawing-handle returns use
|
|
81
|
+
# ``udt_return_type``; tuple returns use ``returns_tuple``.
|
|
82
|
+
return_type_spec: Any = None
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
@dataclass
|
|
@@ -104,9 +119,33 @@ class SecurityCallInfo:
|
|
|
104
119
|
gaps: Any = None
|
|
105
120
|
lookahead: Any = None
|
|
106
121
|
ta_range: Any = None
|
|
122
|
+
# request.security(ticker.heikinashi(syminfo.tickerid), ...) — the chart's own
|
|
123
|
+
# symbol read as Heikin-Ashi candles. The engine applies the HA OHLC transform
|
|
124
|
+
# to each (aggregated) bar before evaluating the security expression; the
|
|
125
|
+
# expression's close/open/high/low therefore see HA values. Same-symbol only.
|
|
126
|
+
heikinashi: bool = False
|
|
107
127
|
depends_on_mutable_globals: bool = False
|
|
108
128
|
mutable_globals: tuple[str, ...] = ()
|
|
109
129
|
is_lower_tf_array: bool = False
|
|
130
|
+
# Name of the user function whose body contains this call ("" at global
|
|
131
|
+
# scope). A ``request.security(sym, tf, ...)`` whose ``tf`` is that
|
|
132
|
+
# function's parameter cannot be resolved at class scope (the security
|
|
133
|
+
# evaluator is a class method, not the function body) — the codegen resolves
|
|
134
|
+
# such a param-tf from the function's call sites instead.
|
|
135
|
+
containing_func: str = ""
|
|
136
|
+
# Set when this SecurityCallInfo is one of N per-call-site CLONES of a
|
|
137
|
+
# single source request.security(...) whose ``timeframe`` is a UDF
|
|
138
|
+
# parameter called from >= 2 sites with >= 2 distinct literal timeframes
|
|
139
|
+
# (e.g. ``scoreFromRange(tf) => request.security(sym, tf, ...)`` called as
|
|
140
|
+
# ``scoreFromRange("15")``, ``scoreFromRange("240")``, ...). Each clone's
|
|
141
|
+
# ``timeframe`` is rewritten to a literal StringLiteral for its call site
|
|
142
|
+
# and gets its own unique ``sec_id``; ``callsite_idx`` is the SAME index
|
|
143
|
+
# ``func_call_cs_map`` assigns that call site for the existing per-
|
|
144
|
+
# call-site UDF-body-cloning mechanism, so the codegen can pick the right
|
|
145
|
+
# clone's ``sec_id`` while emitting that call site's specialized function
|
|
146
|
+
# body (``self._active_call_site_idx``). None for an ordinary
|
|
147
|
+
# (non-cloned) security call.
|
|
148
|
+
callsite_idx: int | None = None
|
|
110
149
|
|
|
111
150
|
|
|
112
151
|
@dataclass
|
|
@@ -135,6 +174,21 @@ class AnalyzerContext:
|
|
|
135
174
|
func_ta_ranges: dict = field(default_factory=dict) # func_name -> (start_idx, end_idx)
|
|
136
175
|
func_call_cs_map: dict = field(default_factory=dict) # call_node_id -> (func_name, call_site_index)
|
|
137
176
|
func_call_site_counts: dict = field(default_factory=dict) # func_name -> int
|
|
177
|
+
# Functions that need per-call-site body cloning PURELY because they
|
|
178
|
+
# contain a request.security(...) whose timeframe is a UDF parameter
|
|
179
|
+
# called with >= 2 distinct literal timeframes (the security-tf-
|
|
180
|
+
# monomorphization case), even though they have no TA call sites or
|
|
181
|
+
# series/var members of their own (the usual trigger for cloning). The
|
|
182
|
+
# codegen's function-emission gate ORs this in alongside has_ta/
|
|
183
|
+
# has_series so self._active_call_site_idx is actually set while it
|
|
184
|
+
# emits each call site's body — required for the per-clone
|
|
185
|
+
# SecurityCallInfo.callsite_idx disambiguation in visit_call.py to work.
|
|
186
|
+
func_security_clone_only: set = field(default_factory=set)
|
|
187
|
+
# (func_name, cs_idx) -> {orig_member_name: cloned_member_name}. Populated by
|
|
188
|
+
# the analyzer ONLY for clones whose default ``{base}_cs{cs_idx}`` name would
|
|
189
|
+
# collide with a clone minted through another enclosing function; lets codegen
|
|
190
|
+
# use the disambiguated name instead of re-deriving a colliding one.
|
|
191
|
+
func_cs_ta_clone_names: dict = field(default_factory=dict)
|
|
138
192
|
# UDT / enum definitions:
|
|
139
193
|
udt_defs: dict = field(default_factory=dict) # type_name -> {field_name: PineType}
|
|
140
194
|
enum_defs: dict = field(default_factory=dict) # enum_name -> [member names]
|
|
@@ -145,12 +199,20 @@ class AnalyzerContext:
|
|
|
145
199
|
# Per-function var_members + series_vars (used when emitting per-function call-site variants):
|
|
146
200
|
func_var_members: dict = field(default_factory=dict)
|
|
147
201
|
func_series_vars: dict = field(default_factory=dict)
|
|
202
|
+
# Per-function array-return TypeSpec (see FuncInfo.return_type_spec).
|
|
203
|
+
func_return_type_specs: dict = field(default_factory=dict)
|
|
148
204
|
# var_name -> UDT type name for variables instantiated via TypeName.new(...)
|
|
149
205
|
udt_var_types: dict[str, str] = field(default_factory=dict)
|
|
150
206
|
# var_name -> structured collection/UDT type metadata
|
|
151
207
|
collection_types: dict[str, TypeSpec] = field(default_factory=dict)
|
|
152
208
|
# UDT name -> field_name -> structured type metadata
|
|
153
209
|
udt_field_type_specs: dict[str, dict[str, TypeSpec]] = field(default_factory=dict)
|
|
210
|
+
# id(block_node) -> {raw_var_name: scope_unique_member_name} for block-scoped
|
|
211
|
+
# ``var``/``varip`` declarations whose raw name collides with a same-named
|
|
212
|
+
# block var in a sibling scope. Codegen activates the rename via
|
|
213
|
+
# ``_active_var_remap`` while emitting that block's statements so reads/writes
|
|
214
|
+
# of the var resolve to the disambiguated member.
|
|
215
|
+
block_var_renames: dict[int, dict[str, str]] = field(default_factory=dict)
|
|
154
216
|
# ``// @pf-trace name=expr`` pragmas in source order. Populated by
|
|
155
217
|
# :func:`pineforge_codegen.pragmas.extract_pf_trace_pragmas` from
|
|
156
218
|
# the original source text and attached after :class:`Analyzer`
|
|
@@ -45,6 +45,12 @@ from ..ast_nodes import (
|
|
|
45
45
|
)
|
|
46
46
|
from ..symbols import PineType, TypeSpec
|
|
47
47
|
|
|
48
|
+
# Drawing-objects-as-data type names (spec §4.1). Defined locally — the
|
|
49
|
+
# analyzer must not import from ``codegen`` (codegen imports analyzer, so the
|
|
50
|
+
# reverse would be a cycle). Mirrors codegen.tables.DRAWING_TYPE_TO_CPP keys.
|
|
51
|
+
_DRAWING_TYPE_NAMES = frozenset({"line", "box", "label", "linefill", "chart.point"})
|
|
52
|
+
_DRAWING_NS = frozenset({"line", "box", "label", "linefill"})
|
|
53
|
+
|
|
48
54
|
|
|
49
55
|
class TypeHelper:
|
|
50
56
|
"""Pine type-hint / expression inference.
|
|
@@ -100,8 +106,28 @@ class TypeHelper:
|
|
|
100
106
|
return TypeSpec.map(key, val)
|
|
101
107
|
if hint in self._udt_fields:
|
|
102
108
|
return TypeSpec.udt(hint)
|
|
109
|
+
# Drawing-objects-as-data (P3): scalar ``line``/``box``/``label``/
|
|
110
|
+
# ``linefill``/``chart.point`` carry the handle identity via a udt
|
|
111
|
+
# TypeSpec. Without this the analyzer field-spec filter (base.py ~847)
|
|
112
|
+
# erases a scalar drawing field, collapsing it to double. Drawing names
|
|
113
|
+
# are NOT in _udt_fields.
|
|
114
|
+
if hint in _DRAWING_TYPE_NAMES:
|
|
115
|
+
return TypeSpec.udt(hint)
|
|
103
116
|
return None
|
|
104
117
|
|
|
118
|
+
def _param_type_specs_from_def(self, func_def) -> list:
|
|
119
|
+
"""Per-parameter ``TypeSpec`` (or ``None``) from a function's DECLARED
|
|
120
|
+
parameter type hints — the authoritative source for typed params
|
|
121
|
+
(``pivot hi``, ``string tf``, ``line[] arr``). Untyped params are
|
|
122
|
+
``None`` here so regular-function call-site inference can fill them.
|
|
123
|
+
"""
|
|
124
|
+
hints = (getattr(func_def, "annotations", None) or {}).get("param_type_hints", [])
|
|
125
|
+
specs: list = []
|
|
126
|
+
for i in range(len(func_def.params)):
|
|
127
|
+
hint = hints[i] if i < len(hints) else None
|
|
128
|
+
specs.append(self._type_spec_from_hint(hint) if hint else None)
|
|
129
|
+
return specs
|
|
130
|
+
|
|
105
131
|
def _template_args_from_call(self, node: FuncCall) -> list[str]:
|
|
106
132
|
callee = node.callee
|
|
107
133
|
ann = getattr(callee, "annotations", None) or {}
|
|
@@ -116,6 +142,17 @@ class TypeHelper:
|
|
|
116
142
|
func = cal.member if isinstance(cal, MemberAccess) else None
|
|
117
143
|
ns = cal.object.name if isinstance(cal, MemberAccess) and isinstance(cal.object, Identifier) else None
|
|
118
144
|
targs = self._template_args_from_call(value)
|
|
145
|
+
# Drawing-objects-as-data return typing: *.new / *.copy -> handle of
|
|
146
|
+
# the self-type; linefill.get_line* -> line; chart.point.* -> point.
|
|
147
|
+
if ns in _DRAWING_NS:
|
|
148
|
+
if func in ("new", "copy"):
|
|
149
|
+
return TypeSpec.udt(ns)
|
|
150
|
+
if ns == "linefill" and func in ("get_line1", "get_line2"):
|
|
151
|
+
return TypeSpec.udt("line")
|
|
152
|
+
if (isinstance(cal, MemberAccess) and isinstance(cal.object, MemberAccess)
|
|
153
|
+
and isinstance(cal.object.object, Identifier)
|
|
154
|
+
and cal.object.object.name == "chart" and cal.object.member == "point"):
|
|
155
|
+
return TypeSpec.udt("chart.point")
|
|
119
156
|
if ns == "array" and func in ("new", "new_float", "new_int", "new_bool", "new_string", "from"):
|
|
120
157
|
if func == "new_float":
|
|
121
158
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
@@ -194,10 +231,26 @@ class TypeHelper:
|
|
|
194
231
|
return recv_spec.element
|
|
195
232
|
if func == "eigenvalues":
|
|
196
233
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
234
|
+
# Drawing method-form: a.copy() -> same handle; lf.get_line*() -> line.
|
|
235
|
+
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
236
|
+
and recv_spec.name in _DRAWING_TYPE_NAMES):
|
|
237
|
+
if func == "copy":
|
|
238
|
+
return recv_spec
|
|
239
|
+
if recv_spec.name == "linefill" and func in ("get_line1", "get_line2"):
|
|
240
|
+
return TypeSpec.udt("line")
|
|
197
241
|
if isinstance(value, Identifier):
|
|
198
242
|
sym = self._symbols.resolve(value.name)
|
|
199
243
|
if sym is not None and sym.type_spec is not None:
|
|
200
244
|
return sym.type_spec
|
|
245
|
+
if isinstance(value, FuncCall):
|
|
246
|
+
# User-function return spec (e.g. an array-returning
|
|
247
|
+
# ``buildPDLevels() => array.from(...)``), so a caller's
|
|
248
|
+
# ``allLevels = buildPDLevels()`` infers an array TypeSpec.
|
|
249
|
+
cal = value.callee
|
|
250
|
+
fname = cal.member if isinstance(cal, MemberAccess) else (
|
|
251
|
+
cal.name if isinstance(cal, Identifier) else None)
|
|
252
|
+
if fname and fname in getattr(self, "_func_return_type_specs", {}):
|
|
253
|
+
return self._func_return_type_specs[fname]
|
|
201
254
|
if isinstance(value, MemberAccess):
|
|
202
255
|
owner = self._type_spec_from_expr(value.object)
|
|
203
256
|
if owner is not None and owner.kind == "udt" and owner.name:
|
|
@@ -41,6 +41,8 @@ from .tables import (
|
|
|
41
41
|
SYMINFO_MEMBER_MAP,
|
|
42
42
|
COLOR_CONST_MAP,
|
|
43
43
|
ARRAY_METHODS,
|
|
44
|
+
ARRAY_DRAWING_NEW_CTORS,
|
|
45
|
+
ARRAY_NEW_CTORS,
|
|
44
46
|
MAP_METHODS,
|
|
45
47
|
MATRIX_METHODS,
|
|
46
48
|
MATRIX_METHOD_KWARGS,
|
|
@@ -69,6 +71,8 @@ __all__ = [
|
|
|
69
71
|
"SYMINFO_MEMBER_MAP",
|
|
70
72
|
"COLOR_CONST_MAP",
|
|
71
73
|
"ARRAY_METHODS",
|
|
74
|
+
"ARRAY_DRAWING_NEW_CTORS",
|
|
75
|
+
"ARRAY_NEW_CTORS",
|
|
72
76
|
"MAP_METHODS",
|
|
73
77
|
"MATRIX_METHODS",
|
|
74
78
|
"MATRIX_METHOD_KWARGS",
|