@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.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/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
|
@@ -29,6 +29,9 @@ from ..symbols import (
|
|
|
29
29
|
method_receiver_type_name,
|
|
30
30
|
)
|
|
31
31
|
from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase
|
|
32
|
+
from ..limits import TimeBudget, iter_ast_nodes
|
|
33
|
+
from ..security_contexts import PASS_WARNINGS_ANNOTATION, context_key, reads_bar_series
|
|
34
|
+
from ..session_reads import emitted_session_reads
|
|
32
35
|
from ..method_binding import (
|
|
33
36
|
BoundMethodArgs,
|
|
34
37
|
MethodBindError,
|
|
@@ -124,9 +127,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
124
127
|
the package layout.
|
|
125
128
|
"""
|
|
126
129
|
|
|
127
|
-
def __init__(self, ast: Program, filename: str = "<stdin>"
|
|
130
|
+
def __init__(self, ast: Program, filename: str = "<stdin>",
|
|
131
|
+
budget: TimeBudget | None = None,
|
|
132
|
+
session_clones: frozenset[str] | None = None) -> None:
|
|
128
133
|
self._ast = ast
|
|
129
134
|
self._filename = filename
|
|
135
|
+
self._budget = budget
|
|
136
|
+
# The functions that may be emitted once per call site for their
|
|
137
|
+
# session.<flag>[k] reads; None: every one that reads a flag at an
|
|
138
|
+
# offset (pineforge_codegen._generate passes none first, then each
|
|
139
|
+
# time adds the ones whose reads the C++ holds).
|
|
140
|
+
self._session_clones = session_clones
|
|
141
|
+
self._session_uncloned: frozenset[str] = frozenset()
|
|
142
|
+
self._budget_visit_count = 0
|
|
130
143
|
self._method_signatures = inventory_method_signatures(ast)
|
|
131
144
|
self._method_call_bindings: dict[
|
|
132
145
|
tuple[int, str], BoundMethodArgs
|
|
@@ -142,7 +155,12 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
142
155
|
self._func_infos: list[FuncInfo] = []
|
|
143
156
|
self._fixnan_sites: list[FixnanCallSite] = []
|
|
144
157
|
self._strategy_params: dict = {}
|
|
145
|
-
|
|
158
|
+
# The warnings of the passes between the support checker and here,
|
|
159
|
+
# held on the Program as (message, hint, location).
|
|
160
|
+
self._diagnostics: list[Diagnostic] = [
|
|
161
|
+
Diagnostic(level=Level.WARNING, phase=Phase.ANALYZER, location=loc,
|
|
162
|
+
message=message, hint=hint)
|
|
163
|
+
for message, hint, loc in (ast.annotations or {}).get(PASS_WARNINGS_ANNOTATION, ())]
|
|
146
164
|
self._global_var_decls: list[tuple[str, PineType]] = []
|
|
147
165
|
# Top-level ordinary bindings are lexical global state even when a
|
|
148
166
|
# same-named callable history reference has already polluted the
|
|
@@ -231,6 +249,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
231
249
|
self._func_tuple_element_count: dict[str, int] = {}
|
|
232
250
|
self._func_tuple_element_types: dict[str, tuple[PineType, ...]] = {}
|
|
233
251
|
self._tuple_element_types_by_node: dict[int, tuple[PineType, ...]] = {}
|
|
252
|
+
# request.security call id -> its tuple's size (``call_handlers``).
|
|
253
|
+
self._security_tuple_shapes: dict[int, int] = {}
|
|
234
254
|
# Track user-defined functions whose body returns a UDT instance —
|
|
235
255
|
# maps func_name -> UDT type name. Detected from the body's final
|
|
236
256
|
# expression (``=> Sample.new(...)`` or last stmt ``Sample.new(...)``).
|
|
@@ -282,6 +302,14 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
282
302
|
# their established paths.
|
|
283
303
|
self._func_var_storage_names: dict[str, dict[str, str]] = {}
|
|
284
304
|
self._func_series_vars: dict[str, set] = {} # func_name -> set[str]
|
|
305
|
+
self._session_history_unsafe: dict[str, str] = {}
|
|
306
|
+
# Plain UDF -> script variables (and ``bar_index``) its body reads
|
|
307
|
+
# through history, in source order; id(Subscript) -> (UDF, name, node)
|
|
308
|
+
# for each such read. See ``_note_function_global_history_read``.
|
|
309
|
+
self._func_global_history_reads: dict[str, list[str]] = {}
|
|
310
|
+
self._func_global_history_nodes: dict[int, tuple] = {}
|
|
311
|
+
self._global_history_only_stateful: set[str] = set()
|
|
312
|
+
self._global_history_typing_warned: set[tuple] = set()
|
|
285
313
|
# Declaration-bound non-persistent history locals are distinct from
|
|
286
314
|
# history parameters/global reads carried by ``func_series_vars``.
|
|
287
315
|
# Codegen needs this exact subset when a raw spelling also belongs to
|
|
@@ -604,6 +632,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
604
632
|
for name, specs in self._func_param_type_specs.items()
|
|
605
633
|
},
|
|
606
634
|
func_security_clone_only=self._func_security_clone_only,
|
|
635
|
+
session_history_unsafe=self._session_history_unsafe,
|
|
636
|
+
session_uncloned=self._session_uncloned,
|
|
607
637
|
func_cs_ta_clone_names=self._func_cs_ta_clone_names,
|
|
608
638
|
udt_defs=self._udt_fields,
|
|
609
639
|
enum_defs=self._enum_defs,
|
|
@@ -622,6 +652,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
622
652
|
for owner, names in self._func_var_storage_names.items()
|
|
623
653
|
},
|
|
624
654
|
func_series_vars=self._func_series_vars,
|
|
655
|
+
func_global_history_reads={
|
|
656
|
+
owner: list(names)
|
|
657
|
+
for owner, names in self._func_global_history_reads.items()
|
|
658
|
+
},
|
|
659
|
+
func_global_history_nodes=dict(self._func_global_history_nodes),
|
|
625
660
|
nonpersistent_series_decl_names=set(
|
|
626
661
|
self._nonpersistent_series_decl_names
|
|
627
662
|
),
|
|
@@ -2204,8 +2239,14 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2204
2239
|
out |= self._collect_security_mutable_globals(arg, resolving)
|
|
2205
2240
|
for value in node.kwargs.values():
|
|
2206
2241
|
out |= self._collect_security_mutable_globals(value, resolving)
|
|
2207
|
-
|
|
2208
|
-
|
|
2242
|
+
# One walk of a body holds every global it reads, and the walk
|
|
2243
|
+
# returns their union: a helper reached again (``f(x) + f(x)``,
|
|
2244
|
+
# a diamond of such helpers) is not walked once per path.
|
|
2245
|
+
walked = f"walked:{func_name}"
|
|
2246
|
+
if walked not in resolving:
|
|
2247
|
+
resolving.add(walked)
|
|
2248
|
+
for stmt in self._func_defs[func_name].body:
|
|
2249
|
+
out |= self._collect_security_mutable_globals(stmt, resolving)
|
|
2209
2250
|
resolving.remove(call_key)
|
|
2210
2251
|
return out
|
|
2211
2252
|
|
|
@@ -2243,6 +2284,99 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2243
2284
|
|
|
2244
2285
|
return out
|
|
2245
2286
|
|
|
2287
|
+
def _session_history_unsafe_functions(
|
|
2288
|
+
self, func_defs: dict, find_calls, known_func_names: set[str],
|
|
2289
|
+
calls_by_parent: dict) -> dict[str, str]:
|
|
2290
|
+
"""The functions and methods that cannot keep a ``session.*`` flag's
|
|
2291
|
+
history per call site, each with the reason (name -> why).
|
|
2292
|
+
|
|
2293
|
+
A read in a function keeps its call site's history: the function is
|
|
2294
|
+
emitted once per call site (``_session_call_*``, codegen). Four kinds
|
|
2295
|
+
of callable do not get that:
|
|
2296
|
+
|
|
2297
|
+
* a method: a call on a receiver the analyzer cannot type
|
|
2298
|
+
(``mk().m()``, a loop variable) is not told apart from the method's
|
|
2299
|
+
other calls;
|
|
2300
|
+
* a function a method calls, once in the method's one body;
|
|
2301
|
+
* a function a request.security expression evaluates: the evaluator
|
|
2302
|
+
inlines it or calls its chart variant, on the requested clock. The
|
|
2303
|
+
functions reached are those the expression calls, those they call,
|
|
2304
|
+
and those a global variable's definition or a mutable global's
|
|
2305
|
+
statements call when anything it reaches spells the global's name
|
|
2306
|
+
(the evaluator follows a mutable global by name, so a function's
|
|
2307
|
+
own binding of the name does not stop it);
|
|
2308
|
+
* a function a UDT field default calls, once for every ``new()``.
|
|
2309
|
+
|
|
2310
|
+
Codegen refuses a ``session.<flag>[k]`` it emits in one of them; a read
|
|
2311
|
+
it never emits (in ``plot()``, a dropped argument) refuses nothing.
|
|
2312
|
+
Every such emitted read failed the C++ compile before this lane.
|
|
2313
|
+
"""
|
|
2314
|
+
unsafe: dict[str, str] = {}
|
|
2315
|
+
|
|
2316
|
+
def mark(names, why: str) -> None:
|
|
2317
|
+
for name in sorted(names):
|
|
2318
|
+
unsafe.setdefault(name, why)
|
|
2319
|
+
|
|
2320
|
+
def closure(roots) -> set[str]:
|
|
2321
|
+
found, pending = set(), list(roots)
|
|
2322
|
+
while pending:
|
|
2323
|
+
name = pending.pop()
|
|
2324
|
+
if name in found or name not in func_defs:
|
|
2325
|
+
continue
|
|
2326
|
+
found.add(name)
|
|
2327
|
+
pending.extend(callee for callee, _call in calls_by_parent.get(name, []))
|
|
2328
|
+
return found
|
|
2329
|
+
|
|
2330
|
+
methods = {name for name in func_defs if "." in name}
|
|
2331
|
+
mark(methods, "a method, whose calls PineForge cannot always tell apart")
|
|
2332
|
+
mark(closure(callee for m in methods for callee, _call in calls_by_parent.get(m, [])),
|
|
2333
|
+
"which a method calls")
|
|
2334
|
+
|
|
2335
|
+
# A request.security expression's reach.
|
|
2336
|
+
pending: list[tuple[Any, str | None]] = [
|
|
2337
|
+
(sec.expression, getattr(sec, "containing_func", "") or None)
|
|
2338
|
+
for sec in getattr(self, "_security_calls", []) or []
|
|
2339
|
+
if getattr(sec, "expression", None) is not None
|
|
2340
|
+
]
|
|
2341
|
+
reached: set[str] = set()
|
|
2342
|
+
seen_nodes: set[int] = set()
|
|
2343
|
+
followed: set[str] = set()
|
|
2344
|
+
while pending:
|
|
2345
|
+
node, owner = pending.pop()
|
|
2346
|
+
if id(node) in seen_nodes:
|
|
2347
|
+
continue
|
|
2348
|
+
seen_nodes.add(id(node))
|
|
2349
|
+
if self._budget is not None:
|
|
2350
|
+
self._budget.check(getattr(node, "loc", None), Phase.ANALYZER)
|
|
2351
|
+
for callee, _call in find_calls(node, known_func_names, owner):
|
|
2352
|
+
if callee not in reached and callee in func_defs:
|
|
2353
|
+
reached.add(callee)
|
|
2354
|
+
pending.append((func_defs[callee], callee))
|
|
2355
|
+
for child, _depth in iter_ast_nodes(node):
|
|
2356
|
+
if not isinstance(child, Identifier) or child.name in followed:
|
|
2357
|
+
continue
|
|
2358
|
+
name = child.name
|
|
2359
|
+
info = self._global_binding_infos.get(name)
|
|
2360
|
+
if info is not None and (info.is_var or name in self._global_reassigned_names):
|
|
2361
|
+
followed.add(name)
|
|
2362
|
+
pending.extend((stmt, None) for stmt in info.source_stmts)
|
|
2363
|
+
elif name in self._global_expr_map:
|
|
2364
|
+
followed.add(name)
|
|
2365
|
+
pending.append((self._global_expr_map[name], None))
|
|
2366
|
+
mark(reached, "which a request.security expression evaluates on the requested clock")
|
|
2367
|
+
|
|
2368
|
+
# A UDT field default's call, for the types a script builds.
|
|
2369
|
+
built = {node.callee.object.name for node, _depth in iter_ast_nodes(self._ast)
|
|
2370
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, MemberAccess)
|
|
2371
|
+
and node.callee.member == "new" and isinstance(node.callee.object, Identifier)}
|
|
2372
|
+
mark(closure(callee
|
|
2373
|
+
for stmt in self._ast.body
|
|
2374
|
+
if isinstance(stmt, TypeDecl) and stmt.name in built
|
|
2375
|
+
for field in stmt.fields if field.default is not None
|
|
2376
|
+
for callee, _call in find_calls(field.default, known_func_names, None)),
|
|
2377
|
+
"which a UDT field default calls once for every new()")
|
|
2378
|
+
return unsafe
|
|
2379
|
+
|
|
2246
2380
|
def _propagate_call_site_counts(self) -> None:
|
|
2247
2381
|
"""Propagate stateful UDF identity through complete call paths.
|
|
2248
2382
|
|
|
@@ -2564,12 +2698,15 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2564
2698
|
owner_requirements.add(actual.name)
|
|
2565
2699
|
series_changed = True
|
|
2566
2700
|
|
|
2567
|
-
# Codegen synthesizes a Series buffer for
|
|
2701
|
+
# Codegen synthesizes a Series buffer for three expression shapes that
|
|
2568
2702
|
# do not appear in ``_func_series_vars`` themselves:
|
|
2569
2703
|
#
|
|
2570
2704
|
# * a call result read through history, e.g. ``f()[1]``;
|
|
2571
2705
|
# * a scalar expression bridged into a UDF series parameter, e.g.
|
|
2572
|
-
# ``history(close + open)`` where ``history(src) => src[1]
|
|
2706
|
+
# ``history(close + open)`` where ``history(src) => src[1]``;
|
|
2707
|
+
# * a session.* flag read at an offset, e.g. ``session.ismarket[1]``,
|
|
2708
|
+
# whose history in a function is the calls' of its call site
|
|
2709
|
+
# (TradingView's tapes, tests/test_e2e_session_history.py).
|
|
2573
2710
|
#
|
|
2574
2711
|
# A buffer is mutable per-call-site state just like TA/fixnan. Mark
|
|
2575
2712
|
# its lexical owner stateful before the normal call-path closure so a
|
|
@@ -2609,6 +2746,15 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2609
2746
|
return True
|
|
2610
2747
|
return False
|
|
2611
2748
|
|
|
2749
|
+
# A read written in a request.security expression keeps the requested
|
|
2750
|
+
# clock's history (codegen security.py), not its function's calls.
|
|
2751
|
+
requested_node_ids = {
|
|
2752
|
+
id(child)
|
|
2753
|
+
for sec in getattr(self, "_security_calls", []) or []
|
|
2754
|
+
if getattr(sec, "expression", None) is not None
|
|
2755
|
+
for child, _depth in iter_ast_nodes(sec.expression)
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2612
2758
|
def _has_synthetic_history_state(
|
|
2613
2759
|
node, seen: set[int] | None = None) -> bool:
|
|
2614
2760
|
if node is None:
|
|
@@ -2645,6 +2791,21 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2645
2791
|
name for name, func_def in func_defs.items()
|
|
2646
2792
|
if _has_synthetic_history_state(func_def)
|
|
2647
2793
|
}
|
|
2794
|
+
# A plain function that reads a flag at an offset is emitted once per
|
|
2795
|
+
# call site, if this analysis may clone it. A callable that cannot
|
|
2796
|
+
# keep its call sites apart gets no clone for it (codegen refuses such
|
|
2797
|
+
# a read where it emits one).
|
|
2798
|
+
self._session_history_unsafe = self._session_history_unsafe_functions(
|
|
2799
|
+
func_defs, _find_calls, known_func_names, calls_by_parent)
|
|
2800
|
+
session_readers = frozenset(
|
|
2801
|
+
name for name, func_def in func_defs.items()
|
|
2802
|
+
if name not in self._session_history_unsafe
|
|
2803
|
+
and emitted_session_reads(func_def, requested_node_ids)
|
|
2804
|
+
)
|
|
2805
|
+
cloned = (session_readers if self._session_clones is None
|
|
2806
|
+
else session_readers & self._session_clones)
|
|
2807
|
+
self._session_uncloned = session_readers - cloned
|
|
2808
|
+
synthetic_history_stateful |= cloned
|
|
2648
2809
|
|
|
2649
2810
|
# request.security owns a separate evaluator context and already
|
|
2650
2811
|
# materializes/remaps its embedded TA state per SecurityCallInfo. Do
|
|
@@ -2665,8 +2826,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2665
2826
|
)
|
|
2666
2827
|
|
|
2667
2828
|
# Canonical direct-state predicate. TA-only and fixnan-only helpers
|
|
2668
|
-
# are just as stateful as functions carrying an explicit series/var
|
|
2669
|
-
|
|
2829
|
+
# are just as stateful as functions carrying an explicit series/var,
|
|
2830
|
+
# and so is a function reading a script variable through history: its
|
|
2831
|
+
# call sites each own that history.
|
|
2832
|
+
direct_state = (
|
|
2670
2833
|
set(self._func_series_vars)
|
|
2671
2834
|
| set(self._func_var_members)
|
|
2672
2835
|
| set(self._func_ta_ranges)
|
|
@@ -2679,15 +2842,40 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2679
2842
|
# marker is intentionally separate from request.security evaluator
|
|
2680
2843
|
# identity, so ordinary security calls remain shared unless the
|
|
2681
2844
|
# dedicated timeframe-monomorphization pass clones them.
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
changed =
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2845
|
+
def close_over_callers(seed: set[str]) -> set[str]:
|
|
2846
|
+
closed = set(seed)
|
|
2847
|
+
changed = True
|
|
2848
|
+
while changed:
|
|
2849
|
+
changed = False
|
|
2850
|
+
for fname, calls in calls_by_parent.items():
|
|
2851
|
+
if fname in closed:
|
|
2852
|
+
continue
|
|
2853
|
+
if any(sub in closed for sub, _ in calls):
|
|
2854
|
+
closed.add(fname)
|
|
2855
|
+
changed = True
|
|
2856
|
+
return closed
|
|
2857
|
+
|
|
2858
|
+
# A function whose untyped parameter receives two primitive families
|
|
2859
|
+
# at its written calls is TradingView's once per family: one variant
|
|
2860
|
+
# per call site, each typed from its own call.
|
|
2861
|
+
family_polymorphic = self._untyped_param_family_conflicts(
|
|
2862
|
+
func_defs, func_info_by_name, call_edges, _bound_user_call_args)
|
|
2863
|
+
family_polymorphic = self._bounded_family_polymorphism(
|
|
2864
|
+
family_polymorphic,
|
|
2865
|
+
direct_state | set(self._func_global_history_reads),
|
|
2866
|
+
close_over_callers, call_edges, func_info_by_name)
|
|
2867
|
+
stateful = close_over_callers(
|
|
2868
|
+
direct_state
|
|
2869
|
+
| set(self._func_global_history_reads)
|
|
2870
|
+
| family_polymorphic
|
|
2871
|
+
)
|
|
2872
|
+
# Callables stateful only through a script variable's history, or
|
|
2873
|
+
# their parameters' families: before those rules they shared one body,
|
|
2874
|
+
# so their call-site typing must not refuse a script that transpiled
|
|
2875
|
+
# then (``merge_profile`` below).
|
|
2876
|
+
self._global_history_only_stateful = (
|
|
2877
|
+
stateful - close_over_callers(direct_state)
|
|
2878
|
+
)
|
|
2691
2879
|
|
|
2692
2880
|
# Direct fixnan-only functions and pure transitive wrappers own no
|
|
2693
2881
|
# TA/series member that would trip the emitter's ordinary body-clone
|
|
@@ -2862,6 +3050,200 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2862
3050
|
_bound_user_call_args,
|
|
2863
3051
|
)
|
|
2864
3052
|
|
|
3053
|
+
# The primitive families a written call can pass an untyped parameter.
|
|
3054
|
+
_ARGUMENT_FAMILIES = frozenset({
|
|
3055
|
+
PineType.INT, PineType.FLOAT, PineType.BOOL, PineType.STRING,
|
|
3056
|
+
PineType.COLOR,
|
|
3057
|
+
})
|
|
3058
|
+
|
|
3059
|
+
def _untyped_param_family_conflicts(
|
|
3060
|
+
self, func_defs, func_info_by_name, call_edges, bound_args,
|
|
3061
|
+
) -> set[str]:
|
|
3062
|
+
"""Plain functions TradingView compiles once per argument family.
|
|
3063
|
+
|
|
3064
|
+
An untyped parameter takes the type of each written call's argument:
|
|
3065
|
+
``s(x) => str.tostring(x)`` spells ``s(5)`` "5" and ``s(2.5)`` "2.5"
|
|
3066
|
+
(lab tv pf-cgs2-untyped-params). One shared body is typed from the
|
|
3067
|
+
first call, so a function whose untyped scalar parameter receives
|
|
3068
|
+
another family -- anything but an int where the body holds a float,
|
|
3069
|
+
which holds it exactly -- narrowed it (a float into an ``int``).
|
|
3070
|
+
Only an argument whose type is known at its call counts: one reading
|
|
3071
|
+
an untyped parameter of the calling function is typed by that
|
|
3072
|
+
function's variant. The set holds each such function and, to a fixed
|
|
3073
|
+
point, every function one of them forwards an untyped parameter to
|
|
3074
|
+
(``wrap(y) => twice(y)``)."""
|
|
3075
|
+
|
|
3076
|
+
def untyped_params(name: str | None) -> dict[int, str]:
|
|
3077
|
+
node = func_defs.get(name) if name is not None else None
|
|
3078
|
+
if node is None:
|
|
3079
|
+
return {}
|
|
3080
|
+
specs = list(self._func_param_type_specs.get(name, ()))
|
|
3081
|
+
return {
|
|
3082
|
+
index: param
|
|
3083
|
+
for index, param in enumerate(node.params)
|
|
3084
|
+
if index >= len(specs) or specs[index] is None
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
def scalar_untyped_params(name: str) -> dict[int, str]:
|
|
3088
|
+
info = func_info_by_name.get(name)
|
|
3089
|
+
if (info is None or getattr(info, "is_udt_method", False)
|
|
3090
|
+
or not isinstance(func_defs.get(name), FuncDef)):
|
|
3091
|
+
return {}
|
|
3092
|
+
series = self._func_series_vars.get(name, set())
|
|
3093
|
+
return {
|
|
3094
|
+
index: param
|
|
3095
|
+
for index, param in untyped_params(name).items()
|
|
3096
|
+
if param not in series
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
def shared_family(name: str, index: int) -> PineType:
|
|
3100
|
+
info = func_info_by_name[name]
|
|
3101
|
+
specs = list(getattr(info, "param_type_specs", ()) or ())
|
|
3102
|
+
spec = specs[index] if index < len(specs) else None
|
|
3103
|
+
if spec is not None:
|
|
3104
|
+
return self._primitive_pine_type_from_spec(spec)
|
|
3105
|
+
types = list(getattr(info, "param_types", ()) or ())
|
|
3106
|
+
return types[index] if index < len(types) else PineType.UNKNOWN
|
|
3107
|
+
|
|
3108
|
+
def reads(value, names: set[str]) -> bool:
|
|
3109
|
+
return bool(names) and any(
|
|
3110
|
+
isinstance(sub, Identifier) and sub.name in names
|
|
3111
|
+
for sub, _depth in iter_ast_nodes(value)
|
|
3112
|
+
)
|
|
3113
|
+
|
|
3114
|
+
conflicted: set[str] = set()
|
|
3115
|
+
# The first conflicting call of each directly conflicted function:
|
|
3116
|
+
# (parameter index, shared family, call family, call).
|
|
3117
|
+
self._family_conflict_calls = {}
|
|
3118
|
+
for owner, callee, call in call_edges:
|
|
3119
|
+
params = scalar_untyped_params(callee)
|
|
3120
|
+
if not params or callee in conflicted:
|
|
3121
|
+
continue
|
|
3122
|
+
owner_params = set(untyped_params(owner).values())
|
|
3123
|
+
actuals = bound_args(callee, call)
|
|
3124
|
+
recorded = self._callable_bound_param_types_by_node.get(id(call), [])
|
|
3125
|
+
for index in params:
|
|
3126
|
+
if (index >= len(actuals) or index >= len(recorded)
|
|
3127
|
+
or actuals[index] is None
|
|
3128
|
+
or reads(actuals[index], owner_params)):
|
|
3129
|
+
continue
|
|
3130
|
+
family = recorded[index]
|
|
3131
|
+
shared = shared_family(callee, index)
|
|
3132
|
+
if (family in self._ARGUMENT_FAMILIES
|
|
3133
|
+
and shared in self._ARGUMENT_FAMILIES
|
|
3134
|
+
and family != shared
|
|
3135
|
+
and not (family == PineType.INT
|
|
3136
|
+
and shared == PineType.FLOAT)):
|
|
3137
|
+
conflicted.add(callee)
|
|
3138
|
+
self._family_conflict_calls[callee] = (
|
|
3139
|
+
index, shared, family, call)
|
|
3140
|
+
break
|
|
3141
|
+
|
|
3142
|
+
changed = True
|
|
3143
|
+
while changed:
|
|
3144
|
+
changed = False
|
|
3145
|
+
for owner, callee, call in call_edges:
|
|
3146
|
+
if owner not in conflicted or callee in conflicted:
|
|
3147
|
+
continue
|
|
3148
|
+
params = scalar_untyped_params(callee)
|
|
3149
|
+
owner_params = set(untyped_params(owner).values())
|
|
3150
|
+
actuals = bound_args(callee, call)
|
|
3151
|
+
if any(
|
|
3152
|
+
index < len(actuals) and actuals[index] is not None
|
|
3153
|
+
and reads(actuals[index], owner_params)
|
|
3154
|
+
for index in params
|
|
3155
|
+
):
|
|
3156
|
+
conflicted.add(callee)
|
|
3157
|
+
changed = True
|
|
3158
|
+
return conflicted
|
|
3159
|
+
|
|
3160
|
+
# Copies of one function family polymorphism may add: one per call path
|
|
3161
|
+
# through the functions it makes stateful (``_build_func_instances``).
|
|
3162
|
+
_MAX_FAMILY_VARIANTS = 64
|
|
3163
|
+
|
|
3164
|
+
def _bounded_family_polymorphism(
|
|
3165
|
+
self, family, seed, close_over_callers, call_edges, func_info_by_name,
|
|
3166
|
+
) -> set[str]:
|
|
3167
|
+
"""``family`` without the functions whose variants would outgrow the
|
|
3168
|
+
script: more copies in all than ``_MAX_FAMILY_VARIANTS`` and four per
|
|
3169
|
+
written call, beyond those the functions' own state already makes.
|
|
3170
|
+
|
|
3171
|
+
Each function the family set makes stateful is emitted once per call
|
|
3172
|
+
path from the script through stateful functions, so a diamond of
|
|
3173
|
+
forwarding helpers (``fk(x) => f(k-1)(x) + f(k-1)(x)``) is 2**depth
|
|
3174
|
+
copies of its leaf (42 MB of C++ at depth 16) and a forwarding chain
|
|
3175
|
+
is its depth times its calls. Seventy flat calls are seventy copies,
|
|
3176
|
+
one per written call, and stay, as do those of a helper forwarding
|
|
3177
|
+
the parameter once. Past the bound, the function gaining the most
|
|
3178
|
+
copies and the family functions it is reached from keep the one body
|
|
3179
|
+
every earlier build emitted, typed from the first call, and a
|
|
3180
|
+
conflicting parameter warns; the rest keep their variants."""
|
|
3181
|
+
if not family:
|
|
3182
|
+
return family
|
|
3183
|
+
callers: dict[str, list[str | None]] = {}
|
|
3184
|
+
for owner, callee, _call in call_edges:
|
|
3185
|
+
callers.setdefault(callee, []).append(owner)
|
|
3186
|
+
cap = 1 << 20
|
|
3187
|
+
|
|
3188
|
+
def path_counts(stateful: set[str]) -> dict[str, int]:
|
|
3189
|
+
counts = {name: 0 for name in stateful}
|
|
3190
|
+
for _round in range(len(stateful) + 1):
|
|
3191
|
+
changed = False
|
|
3192
|
+
for name in stateful:
|
|
3193
|
+
total = min(cap, sum(
|
|
3194
|
+
counts.get(owner, 0) if owner in stateful else 1
|
|
3195
|
+
for owner in callers.get(name, ())))
|
|
3196
|
+
if total != counts[name]:
|
|
3197
|
+
counts[name] = total
|
|
3198
|
+
changed = True
|
|
3199
|
+
if not changed:
|
|
3200
|
+
break
|
|
3201
|
+
return counts
|
|
3202
|
+
|
|
3203
|
+
before = path_counts(close_over_callers(seed))
|
|
3204
|
+
bound = max(self._MAX_FAMILY_VARIANTS, 4 * len(call_edges))
|
|
3205
|
+
kept = set(family)
|
|
3206
|
+
dropped: set[str] = set()
|
|
3207
|
+
while kept:
|
|
3208
|
+
after = path_counts(close_over_callers(seed | kept))
|
|
3209
|
+
added = {name: count - before.get(name, 0)
|
|
3210
|
+
for name, count in after.items()
|
|
3211
|
+
if count > before.get(name, 0)}
|
|
3212
|
+
if sum(added.values()) <= bound:
|
|
3213
|
+
break
|
|
3214
|
+
worst = max(sorted(added), key=lambda name: added[name])
|
|
3215
|
+
reached_from: set[str] = set()
|
|
3216
|
+
pending = [worst]
|
|
3217
|
+
while pending:
|
|
3218
|
+
name = pending.pop()
|
|
3219
|
+
if name in reached_from:
|
|
3220
|
+
continue
|
|
3221
|
+
reached_from.add(name)
|
|
3222
|
+
pending.extend(owner for owner in callers.get(name, ())
|
|
3223
|
+
if owner is not None)
|
|
3224
|
+
implicated = kept & reached_from
|
|
3225
|
+
if not implicated:
|
|
3226
|
+
break
|
|
3227
|
+
kept -= implicated
|
|
3228
|
+
dropped |= implicated
|
|
3229
|
+
for callee, (index, shared, incoming, call) in sorted(
|
|
3230
|
+
getattr(self, "_family_conflict_calls", {}).items()):
|
|
3231
|
+
info = func_info_by_name.get(callee)
|
|
3232
|
+
if callee not in dropped or info is None or info.node is None:
|
|
3233
|
+
continue
|
|
3234
|
+
self._warn(
|
|
3235
|
+
"Untyped parameter '" + info.node.params[index]
|
|
3236
|
+
+ "' of callable '" + callee + "' receives "
|
|
3237
|
+
+ shared.value + " and " + incoming.value
|
|
3238
|
+
+ " at its written calls; a copy per call path would exceed "
|
|
3239
|
+
+ f"{self._MAX_FAMILY_VARIANTS} copies and four per written "
|
|
3240
|
+
+ "call, so PineForge types it " + shared.value
|
|
3241
|
+
+ ", as its first call, and converts the other argument. "
|
|
3242
|
+
+ "Declare the parameter type to choose it.",
|
|
3243
|
+
call.loc,
|
|
3244
|
+
)
|
|
3245
|
+
return kept
|
|
3246
|
+
|
|
2865
3247
|
@staticmethod
|
|
2866
3248
|
def _primitive_pine_type_from_spec(spec) -> PineType:
|
|
2867
3249
|
if spec is None or getattr(spec, "kind", None) != "primitive":
|
|
@@ -2932,6 +3314,31 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2932
3314
|
current[index] = candidate
|
|
2933
3315
|
changed = True
|
|
2934
3316
|
elif current[index] != candidate:
|
|
3317
|
+
if callee in self._global_history_only_stateful:
|
|
3318
|
+
# Its call sites shared one body before this callable
|
|
3319
|
+
# read a script variable's history; keep that body's
|
|
3320
|
+
# typing, the first type, rather than refuse a script
|
|
3321
|
+
# that transpiled then.
|
|
3322
|
+
warned = (callee, cs_idx, index)
|
|
3323
|
+
if warned not in self._global_history_typing_warned:
|
|
3324
|
+
self._global_history_typing_warned.add(warned)
|
|
3325
|
+
self._warn(
|
|
3326
|
+
"Untyped parameter '"
|
|
3327
|
+
+ info.node.params[index]
|
|
3328
|
+
+ "' of callable '"
|
|
3329
|
+
+ callee
|
|
3330
|
+
+ "' receives "
|
|
3331
|
+
+ current[index].value
|
|
3332
|
+
+ " and "
|
|
3333
|
+
+ candidate.value
|
|
3334
|
+
+ " through calls that share one written-call "
|
|
3335
|
+
+ f"variant (cs{cs_idx}); PineForge types it "
|
|
3336
|
+
+ current[index].value
|
|
3337
|
+
+ ", so the other argument is converted. "
|
|
3338
|
+
+ "Declare the parameter type to choose it.",
|
|
3339
|
+
call.loc,
|
|
3340
|
+
)
|
|
3341
|
+
continue
|
|
2935
3342
|
self._error(
|
|
2936
3343
|
"Cannot safely specialize untyped parameter '"
|
|
2937
3344
|
+ info.node.params[index]
|
|
@@ -3223,16 +3630,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3223
3630
|
)
|
|
3224
3631
|
|
|
3225
3632
|
# ------------------------------------------------------------------
|
|
3226
|
-
#
|
|
3633
|
+
# Per-call-site request.security contexts of a helper.
|
|
3227
3634
|
#
|
|
3228
|
-
# A ``request.security`` whose ``timeframe`` is a parameter
|
|
3229
|
-
# containing UDF maps to ONE evaluator
|
|
3230
|
-
#
|
|
3231
|
-
#
|
|
3232
|
-
# all
|
|
3233
|
-
#
|
|
3234
|
-
#
|
|
3235
|
-
#
|
|
3635
|
+
# A ``request.security`` whose ``timeframe`` or ``symbol`` is a parameter
|
|
3636
|
+
# of its containing UDF maps to ONE evaluator however often the UDF is
|
|
3637
|
+
# called. When its call sites pass different contexts -- timeframes, or a
|
|
3638
|
+
# Heikin-Ashi and a plain chart symbol -- one evaluator cannot serve them
|
|
3639
|
+
# all: the request is cloned per call site (``callsite_idx``), each clone
|
|
3640
|
+
# registered with its site's timeframe argument (a literal as a literal,
|
|
3641
|
+
# anything else -- an input, a global -- as that expression, read at
|
|
3642
|
+
# registration) and its site's symbol. A context reaching the helper
|
|
3643
|
+
# through another helper's parameter or a local is resolved before the
|
|
3644
|
+
# analyzer runs (``security_contexts``) and arrives here resolved.
|
|
3236
3645
|
# ------------------------------------------------------------------
|
|
3237
3646
|
def _check_mixed_callsite_security_tf(self) -> None:
|
|
3238
3647
|
sec_calls = getattr(self, "_security_calls", None)
|
|
@@ -3248,16 +3657,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3248
3657
|
cloned_any = False
|
|
3249
3658
|
for sec in sec_calls:
|
|
3250
3659
|
containing = getattr(sec, "containing_func", "") or ""
|
|
3660
|
+
fdef = func_defs.get(containing) if containing else None
|
|
3251
3661
|
tf_node = getattr(sec, "timeframe", None)
|
|
3252
|
-
|
|
3662
|
+
symbol_node = getattr(sec, "symbol", None)
|
|
3663
|
+
if fdef is None or sec.context_resolved or sec.is_lower_tf_array:
|
|
3253
3664
|
new_calls.append(sec)
|
|
3254
3665
|
continue
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3666
|
+
tf_param = (tf_node.name if isinstance(tf_node, Identifier)
|
|
3667
|
+
and tf_node.name in fdef.params else None)
|
|
3668
|
+
symbol_param = (symbol_node.name if isinstance(symbol_node, Identifier)
|
|
3669
|
+
and symbol_node.name in fdef.params else None)
|
|
3670
|
+
if tf_param is None and symbol_param is None:
|
|
3258
3671
|
new_calls.append(sec)
|
|
3259
3672
|
continue
|
|
3260
|
-
pidx = fdef.params.index(param_name)
|
|
3261
3673
|
calls = list(self._iter_user_func_calls(containing))
|
|
3262
3674
|
if not calls:
|
|
3263
3675
|
new_calls.append(sec) # dead code — evaluator result never read
|
|
@@ -3280,7 +3692,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3280
3692
|
# naming (which keys purely off func_call_cs_map, not
|
|
3281
3693
|
# has_ta/has_series) picks the right ``_cs{N}`` variant.
|
|
3282
3694
|
already_tracked = self._func_call_site_count.get(containing, 0) > 0
|
|
3283
|
-
|
|
3695
|
+
# (cs_idx, context key, registered timeframe, heikinashi)
|
|
3696
|
+
per_cs: list[tuple[int, tuple, object, bool]] = []
|
|
3284
3697
|
for i, call in enumerate(calls):
|
|
3285
3698
|
if already_tracked:
|
|
3286
3699
|
cs_info = self._func_call_cs_map.get(id(call))
|
|
@@ -3288,33 +3701,49 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3288
3701
|
continue # shouldn't happen: has_ta/has_series tracks ALL call sites
|
|
3289
3702
|
cs_idx = cs_info[1]
|
|
3290
3703
|
else:
|
|
3704
|
+
# Numbered in func_call_cs_map only if the request is
|
|
3705
|
+
# cloned below: a call mapped to ``f_cs0`` while one
|
|
3706
|
+
# context left ``f`` uncloned named a function nothing
|
|
3707
|
+
# emits (``f(string sym) => request.security(sym, "60",
|
|
3708
|
+
# close[1])`` did not compile).
|
|
3291
3709
|
cs_idx = i
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3710
|
+
key: list = []
|
|
3711
|
+
timeframe = tf_node
|
|
3712
|
+
if tf_param is not None:
|
|
3713
|
+
pidx = fdef.params.index(tf_param)
|
|
3714
|
+
arg = call.args[pidx] if pidx < len(call.args) else None
|
|
3715
|
+
lit = self._callsite_tf_literal_value(arg)
|
|
3716
|
+
if lit is not None:
|
|
3717
|
+
key.append(("lit", lit))
|
|
3718
|
+
timeframe = StringLiteral(value=lit, loc=tf_node.loc)
|
|
3719
|
+
else:
|
|
3720
|
+
key.append(("expr", self._security_context_key(arg)))
|
|
3721
|
+
timeframe = arg
|
|
3722
|
+
heikinashi = sec.heikinashi
|
|
3723
|
+
if symbol_param is not None:
|
|
3724
|
+
sidx = fdef.params.index(symbol_param)
|
|
3725
|
+
arg = call.args[sidx] if sidx < len(call.args) else None
|
|
3726
|
+
heikinashi = arg is not None and self._security_symbol_is_heikinashi(arg)
|
|
3727
|
+
key.append(heikinashi)
|
|
3728
|
+
per_cs.append((cs_idx, tuple(key), timeframe, heikinashi))
|
|
3296
3729
|
if not per_cs:
|
|
3297
3730
|
new_calls.append(sec) # dead code — evaluator result never read
|
|
3298
3731
|
continue
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3732
|
+
if len({key for _, key, _, _ in per_cs}) < 2:
|
|
3733
|
+
# One context on every call site: the helper's own request.
|
|
3734
|
+
sec.heikinashi = per_cs[0][3]
|
|
3735
|
+
new_calls.append(sec)
|
|
3302
3736
|
continue
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3737
|
+
# With a timeframe parameter, a key's first part is its timeframe.
|
|
3738
|
+
series = tf_param and next(
|
|
3739
|
+
(name for _, key, timeframe, _ in per_cs
|
|
3740
|
+
if key[0][0] == "expr" and (name := reads_bar_series(timeframe))), None)
|
|
3741
|
+
if series is not None:
|
|
3307
3742
|
self._error(
|
|
3308
|
-
"request.security timeframe parameter '"
|
|
3309
|
-
+
|
|
3310
|
-
+ "'
|
|
3311
|
-
|
|
3312
|
-
+ "' is called with multiple distinct literal timeframes ("
|
|
3313
|
-
+ ", ".join(sorted(distinct_literals))
|
|
3314
|
-
+ "). A single request.security evaluator cannot serve "
|
|
3315
|
-
"them all and would silently collapse onto the chart "
|
|
3316
|
-
"timeframe. Pass a single timeframe, or inline a separate "
|
|
3317
|
-
"request.security call at each call site.",
|
|
3743
|
+
"request.security timeframe parameter '" + str(tf_param)
|
|
3744
|
+
+ "' of function '" + containing + "' is passed a timeframe "
|
|
3745
|
+
"that reads the chart bar ('" + series + "'): PineForge "
|
|
3746
|
+
"registers each call site's timeframe before the first bar.",
|
|
3318
3747
|
tf_node.loc,
|
|
3319
3748
|
)
|
|
3320
3749
|
new_calls.append(sec)
|
|
@@ -3326,18 +3755,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3326
3755
|
# func_security_clone_only) actually clones its body, with
|
|
3327
3756
|
# self._active_call_site_idx set to each of our cs_idx values
|
|
3328
3757
|
# in turn while it does.
|
|
3758
|
+
for cs_idx, call in enumerate(calls):
|
|
3759
|
+
self._func_call_cs_map.setdefault(id(call), (containing, cs_idx))
|
|
3329
3760
|
self._func_call_site_count[containing] = len(calls)
|
|
3330
3761
|
self._func_security_clone_only.add(containing)
|
|
3331
|
-
# Clone: one SecurityCallInfo per call site, each
|
|
3332
|
-
# site's
|
|
3333
|
-
# existing literal-timeframe resolution path needs no changes)
|
|
3334
|
-
# and given a fresh, currently-unused sec_id.
|
|
3762
|
+
# Clone: one SecurityCallInfo per call site, each registered with
|
|
3763
|
+
# its site's context and given a fresh, currently-unused sec_id.
|
|
3335
3764
|
next_sec_id = max((s.sec_id for s in sec_calls), default=-1) + 1
|
|
3336
3765
|
next_sec_id = max(next_sec_id, len(sec_calls) + len(new_calls))
|
|
3337
|
-
for cs_idx,
|
|
3766
|
+
for cs_idx, _key, timeframe, heikinashi in sorted(per_cs, key=lambda c: c[0]):
|
|
3338
3767
|
clone = SecurityCallInfo(
|
|
3339
3768
|
sec_id=next_sec_id,
|
|
3340
|
-
timeframe=
|
|
3769
|
+
timeframe=timeframe,
|
|
3341
3770
|
expression=sec.expression,
|
|
3342
3771
|
returns_tuple=sec.returns_tuple,
|
|
3343
3772
|
tuple_size=sec.tuple_size,
|
|
@@ -3345,11 +3774,16 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3345
3774
|
gaps=sec.gaps,
|
|
3346
3775
|
lookahead=sec.lookahead,
|
|
3347
3776
|
ta_range=sec.ta_range,
|
|
3777
|
+
heikinashi=heikinashi,
|
|
3348
3778
|
depends_on_mutable_globals=sec.depends_on_mutable_globals,
|
|
3349
3779
|
mutable_globals=sec.mutable_globals,
|
|
3350
3780
|
is_lower_tf_array=sec.is_lower_tf_array,
|
|
3351
3781
|
containing_func=sec.containing_func,
|
|
3352
3782
|
callsite_idx=cs_idx,
|
|
3783
|
+
string_result=sec.string_result,
|
|
3784
|
+
symbol=sec.symbol,
|
|
3785
|
+
foreign=sec.foreign,
|
|
3786
|
+
ignore_invalid=sec.ignore_invalid,
|
|
3353
3787
|
)
|
|
3354
3788
|
new_calls.append(clone)
|
|
3355
3789
|
next_sec_id += 1
|
|
@@ -3391,20 +3825,29 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3391
3825
|
yield from _walk(self._ast)
|
|
3392
3826
|
|
|
3393
3827
|
def _callsite_tf_literal_value(self, arg) -> str | None:
|
|
3394
|
-
"""
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3828
|
+
"""A UDF call-site timeframe argument's value when it is a string
|
|
3829
|
+
literal, directly or through never-reassigned global aliases, else
|
|
3830
|
+
None. An input's default is not its value: an override changes it."""
|
|
3831
|
+
seen: set[str] = set()
|
|
3832
|
+
while (isinstance(arg, Identifier) and arg.name not in seen
|
|
3833
|
+
and arg.name not in self._global_reassigned_names):
|
|
3834
|
+
seen.add(arg.name)
|
|
3835
|
+
arg = self._global_expr_map.get(arg.name)
|
|
3398
3836
|
if isinstance(arg, StringLiteral):
|
|
3399
3837
|
return arg.value
|
|
3400
|
-
if isinstance(arg, Identifier):
|
|
3401
|
-
sym = self._symbols.resolve(arg.name)
|
|
3402
|
-
if sym is not None and getattr(sym, "const_value", None) is not None:
|
|
3403
|
-
val = sym.const_value
|
|
3404
|
-
if isinstance(val, str):
|
|
3405
|
-
return val
|
|
3406
3838
|
return None
|
|
3407
3839
|
|
|
3840
|
+
def _security_context_key(self, arg):
|
|
3841
|
+
"""A call-site context argument, compared by what it spells after
|
|
3842
|
+
following never-reassigned global aliases."""
|
|
3843
|
+
seen: set[str] = set()
|
|
3844
|
+
while (isinstance(arg, Identifier) and arg.name not in seen
|
|
3845
|
+
and arg.name not in self._global_reassigned_names
|
|
3846
|
+
and isinstance(self._global_expr_map.get(arg.name), Identifier)):
|
|
3847
|
+
seen.add(arg.name)
|
|
3848
|
+
arg = self._global_expr_map[arg.name]
|
|
3849
|
+
return context_key(arg)
|
|
3850
|
+
|
|
3408
3851
|
def _is_static_expression(self, node: ASTNode | None) -> bool:
|
|
3409
3852
|
if node is None:
|
|
3410
3853
|
return True
|
|
@@ -3474,6 +3917,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3474
3917
|
"""Dispatch to the appropriate visitor and return the inferred type."""
|
|
3475
3918
|
if node is None:
|
|
3476
3919
|
return PineType.VOID
|
|
3920
|
+
if self._budget is not None:
|
|
3921
|
+
self._budget_visit_count += 1
|
|
3922
|
+
if self._budget_visit_count % 128 == 0:
|
|
3923
|
+
self._budget.check(node.loc, Phase.ANALYZER)
|
|
3477
3924
|
|
|
3478
3925
|
method_name = f"_visit_{type(node).__name__}"
|
|
3479
3926
|
# Convert CamelCase to snake_case for method lookup
|
|
@@ -3508,12 +3955,16 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3508
3955
|
return PineType.VOID
|
|
3509
3956
|
|
|
3510
3957
|
def _visit_ImportStmt(self, node: ImportStmt) -> PineType:
|
|
3958
|
+
# Imported here: the support checker imports the analyzer package.
|
|
3959
|
+
from ..support_checker import import_is_builtin_namespace_no_op, import_refusal
|
|
3960
|
+
if import_is_builtin_namespace_no_op(self._ast, node):
|
|
3961
|
+
return PineType.VOID
|
|
3511
3962
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
3512
3963
|
diag = Diagnostic(
|
|
3513
3964
|
level=Level.ERROR,
|
|
3514
3965
|
phase=Phase.ANALYZER,
|
|
3515
3966
|
location=loc,
|
|
3516
|
-
message=
|
|
3967
|
+
message=import_refusal(node),
|
|
3517
3968
|
)
|
|
3518
3969
|
raise CompileError([diag])
|
|
3519
3970
|
|
|
@@ -4047,20 +4498,112 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4047
4498
|
if hasattr(sym, "is_static_series"):
|
|
4048
4499
|
delattr(sym, "is_static_series")
|
|
4049
4500
|
else:
|
|
4050
|
-
self._visit(node.target)
|
|
4501
|
+
target_type = self._visit(node.target)
|
|
4051
4502
|
base_name = self._get_target_base_name(node.target)
|
|
4052
4503
|
if base_name:
|
|
4053
4504
|
self._static_vars.discard(base_name)
|
|
4054
4505
|
sym = self._symbols.resolve(base_name)
|
|
4055
4506
|
if sym and hasattr(sym, "is_static_series"):
|
|
4056
4507
|
delattr(sym, "is_static_series")
|
|
4508
|
+
# The statement's value is the target's new value, typed as the
|
|
4509
|
+
# target: ``o.v += 1`` on a float field is a float.
|
|
4510
|
+
if (isinstance(node.target, MemberAccess)
|
|
4511
|
+
and target_type not in (None, PineType.UNKNOWN, PineType.VOID)):
|
|
4512
|
+
return target_type
|
|
4057
4513
|
|
|
4514
|
+
if isinstance(node.target, Identifier):
|
|
4515
|
+
target_sym = self._symbols.resolve(node.target.name)
|
|
4516
|
+
if (target_sym is not None and target_sym.pine_type
|
|
4517
|
+
not in (PineType.UNKNOWN, PineType.VOID)):
|
|
4518
|
+
return target_sym.pine_type
|
|
4058
4519
|
return val_type
|
|
4059
4520
|
|
|
4521
|
+
def _selection_tuple_shape(
|
|
4522
|
+
self, node
|
|
4523
|
+
) -> tuple[int, tuple[PineType, ...]] | None:
|
|
4524
|
+
"""``(size, element types)`` of an if/switch whose every arm yields a
|
|
4525
|
+
tuple: a ``[a, b]`` literal, a call of a tuple-returning user function,
|
|
4526
|
+
or a nested selection of those. Pine types the selection as that
|
|
4527
|
+
tuple; arms must agree on its size. ``None`` for any other shape.
|
|
4528
|
+
|
|
4529
|
+
Element types are the arms' common type per position (int and float
|
|
4530
|
+
make float); a position the arms disagree on is left untyped.
|
|
4531
|
+
"""
|
|
4532
|
+
if isinstance(node, IfStmt):
|
|
4533
|
+
arms = [node.body] + ([node.else_body] if node.else_body else [])
|
|
4534
|
+
elif isinstance(node, SwitchStmt):
|
|
4535
|
+
arms = [body for _case, body in node.cases]
|
|
4536
|
+
if node.default_body:
|
|
4537
|
+
arms.append(node.default_body)
|
|
4538
|
+
else:
|
|
4539
|
+
return None
|
|
4540
|
+
shapes: list[tuple[int, tuple[PineType, ...]]] = []
|
|
4541
|
+
for body in arms:
|
|
4542
|
+
if not body:
|
|
4543
|
+
return None
|
|
4544
|
+
terminal = body[-1]
|
|
4545
|
+
expr = terminal.expr if isinstance(terminal, ExprStmt) else terminal
|
|
4546
|
+
if isinstance(expr, TupleLiteral):
|
|
4547
|
+
shapes.append((
|
|
4548
|
+
len(expr.elements),
|
|
4549
|
+
self._tuple_element_types_by_node.get(id(expr), ()),
|
|
4550
|
+
))
|
|
4551
|
+
elif (isinstance(expr, FuncCall)
|
|
4552
|
+
and isinstance(expr.callee, Identifier)
|
|
4553
|
+
and self._func_returns_tuple.get(expr.callee.name, False)):
|
|
4554
|
+
shapes.append((
|
|
4555
|
+
self._func_tuple_element_count.get(expr.callee.name, 0),
|
|
4556
|
+
self._func_tuple_element_types.get(expr.callee.name, ()),
|
|
4557
|
+
))
|
|
4558
|
+
elif isinstance(expr, (IfStmt, SwitchStmt)):
|
|
4559
|
+
nested = self._selection_tuple_shape(expr)
|
|
4560
|
+
if nested is None:
|
|
4561
|
+
return None
|
|
4562
|
+
shapes.append(nested)
|
|
4563
|
+
else:
|
|
4564
|
+
return None
|
|
4565
|
+
sizes = {size for size, _types in shapes}
|
|
4566
|
+
if len(sizes) != 1:
|
|
4567
|
+
return None
|
|
4568
|
+
size = sizes.pop()
|
|
4569
|
+
if size < 2:
|
|
4570
|
+
return None
|
|
4571
|
+
if any(len(types) != size for _size, types in shapes):
|
|
4572
|
+
return size, ()
|
|
4573
|
+
element_types: list[PineType] = []
|
|
4574
|
+
for position in range(size):
|
|
4575
|
+
known = {
|
|
4576
|
+
types[position] for _size, types in shapes
|
|
4577
|
+
if types[position] not in (PineType.NA, PineType.UNKNOWN)
|
|
4578
|
+
}
|
|
4579
|
+
if len(known) == 1:
|
|
4580
|
+
element_types.append(known.pop())
|
|
4581
|
+
elif known and known <= {PineType.INT, PineType.FLOAT}:
|
|
4582
|
+
element_types.append(PineType.FLOAT)
|
|
4583
|
+
else:
|
|
4584
|
+
return size, ()
|
|
4585
|
+
return size, tuple(element_types)
|
|
4586
|
+
|
|
4060
4587
|
def _visit_TupleAssign(self, node: TupleAssign) -> PineType:
|
|
4061
4588
|
val_type = self._visit(node.value)
|
|
4062
4589
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
4063
4590
|
element_types = self._tuple_element_types_by_node.get(id(node.value), ())
|
|
4591
|
+
if not element_types and isinstance(node.value, (IfStmt, SwitchStmt)):
|
|
4592
|
+
shape = self._selection_tuple_shape(node.value)
|
|
4593
|
+
if shape is not None:
|
|
4594
|
+
element_types = shape[1]
|
|
4595
|
+
if (not element_types
|
|
4596
|
+
and isinstance(node.value, FuncCall)
|
|
4597
|
+
and isinstance(node.value.callee, Identifier)
|
|
4598
|
+
and self._func_returns_tuple.get(node.value.callee.name, False)):
|
|
4599
|
+
# A user function's string elements bind as strings. Its other
|
|
4600
|
+
# families keep the historical double storage of a call tuple.
|
|
4601
|
+
element_types = tuple(
|
|
4602
|
+
PineType.STRING if item == PineType.STRING else PineType.FLOAT
|
|
4603
|
+
for item in self._func_tuple_element_types.get(
|
|
4604
|
+
node.value.callee.name, ()
|
|
4605
|
+
)
|
|
4606
|
+
)
|
|
4064
4607
|
|
|
4065
4608
|
is_val_static = self._is_static_expression(node.value)
|
|
4066
4609
|
|
|
@@ -4075,10 +4618,12 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4075
4618
|
)
|
|
4076
4619
|
# Tuple bindings historically use double storage for every
|
|
4077
4620
|
# numeric element, including integer literals. Preserve that
|
|
4078
|
-
# contract while retaining the
|
|
4621
|
+
# contract while retaining the bool and string families: a string
|
|
4622
|
+
# element read as a double compared by value (``a == b``) through
|
|
4623
|
+
# a static_cast of a std::string.
|
|
4079
4624
|
element_type = (
|
|
4080
|
-
|
|
4081
|
-
if inferred_element_type
|
|
4625
|
+
inferred_element_type
|
|
4626
|
+
if inferred_element_type in (PineType.BOOL, PineType.STRING)
|
|
4082
4627
|
else PineType.FLOAT
|
|
4083
4628
|
)
|
|
4084
4629
|
|
|
@@ -4134,6 +4679,32 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4134
4679
|
# Function definition
|
|
4135
4680
|
# ------------------------------------------------------------------
|
|
4136
4681
|
|
|
4682
|
+
def _returns_tuple_call(self, value) -> bool:
|
|
4683
|
+
"""A call to a tuple-returning user function or ``ta.*`` function."""
|
|
4684
|
+
if not isinstance(value, FuncCall):
|
|
4685
|
+
return False
|
|
4686
|
+
callee = value.callee
|
|
4687
|
+
if isinstance(callee, Identifier):
|
|
4688
|
+
return self._func_returns_tuple.get(callee.name, False)
|
|
4689
|
+
return (isinstance(callee, MemberAccess)
|
|
4690
|
+
and isinstance(callee.object, Identifier)
|
|
4691
|
+
and callee.object.name == "ta"
|
|
4692
|
+
and callee.member in TA_TUPLE_RETURNS)
|
|
4693
|
+
|
|
4694
|
+
def _statement_value_type(self, stmt, fallback: PineType) -> PineType:
|
|
4695
|
+
"""Type of the value a function's last statement leaves.
|
|
4696
|
+
|
|
4697
|
+
A trailing declaration ``[T] x = e`` returns ``x``, typed as the
|
|
4698
|
+
variable (``float y = 4`` is a float); a trailing assignment types
|
|
4699
|
+
itself as its target in ``_visit_Assignment``.
|
|
4700
|
+
"""
|
|
4701
|
+
if not isinstance(stmt, VarDecl) or not stmt.name:
|
|
4702
|
+
return fallback
|
|
4703
|
+
sym = self._symbols.resolve(stmt.name)
|
|
4704
|
+
if sym is None or sym.pine_type in (PineType.UNKNOWN, PineType.VOID):
|
|
4705
|
+
return fallback
|
|
4706
|
+
return sym.pine_type
|
|
4707
|
+
|
|
4137
4708
|
def _visit_FuncDef(self, node: FuncDef) -> PineType:
|
|
4138
4709
|
# Store the function def for later analysis
|
|
4139
4710
|
self._func_defs[node.name] = node
|
|
@@ -4181,6 +4752,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4181
4752
|
try:
|
|
4182
4753
|
for stmt in node.body:
|
|
4183
4754
|
body_type = self._visit(stmt)
|
|
4755
|
+
if node.body:
|
|
4756
|
+
body_type = self._statement_value_type(node.body[-1], body_type)
|
|
4184
4757
|
finally:
|
|
4185
4758
|
self._global_scope = old_global
|
|
4186
4759
|
self._collection_scope_stack.pop()
|
|
@@ -4251,6 +4824,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4251
4824
|
terminal_direct_return_spec,
|
|
4252
4825
|
)
|
|
4253
4826
|
|
|
4827
|
+
# ``[p, q] = pair(v)`` last returns that tuple: its element types are
|
|
4828
|
+
# the declared names', resolvable only inside the function scope.
|
|
4829
|
+
tuple_decl_types = ()
|
|
4830
|
+
if (node.body and isinstance(node.body[-1], TupleAssign)
|
|
4831
|
+
and "_" not in node.body[-1].names
|
|
4832
|
+
and self._returns_tuple_call(node.body[-1].value)):
|
|
4833
|
+
tuple_decl_types = tuple(
|
|
4834
|
+
sym.pine_type if sym is not None else PineType.FLOAT
|
|
4835
|
+
for sym in (self._symbols.resolve(name)
|
|
4836
|
+
for name in node.body[-1].names)
|
|
4837
|
+
)
|
|
4838
|
+
|
|
4254
4839
|
self._symbols.exit_scope()
|
|
4255
4840
|
|
|
4256
4841
|
# Detect if function returns a tuple (last stmt is TupleLiteral)
|
|
@@ -4264,12 +4849,27 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4264
4849
|
tuple_node = last_stmt.expr
|
|
4265
4850
|
elif isinstance(last_stmt, TupleLiteral):
|
|
4266
4851
|
tuple_node = last_stmt
|
|
4852
|
+
selection_shape = (
|
|
4853
|
+
self._selection_tuple_shape(last_stmt)
|
|
4854
|
+
if isinstance(last_stmt, (IfStmt, SwitchStmt))
|
|
4855
|
+
else None
|
|
4856
|
+
)
|
|
4267
4857
|
if tuple_node is not None:
|
|
4268
4858
|
self._func_returns_tuple[node.name] = True
|
|
4269
4859
|
self._func_tuple_element_count[node.name] = len(tuple_node.elements)
|
|
4270
4860
|
self._func_tuple_element_types[node.name] = (
|
|
4271
4861
|
self._tuple_element_types_by_node.get(id(tuple_node), ())
|
|
4272
4862
|
)
|
|
4863
|
+
elif tuple_decl_types:
|
|
4864
|
+
self._func_returns_tuple[node.name] = True
|
|
4865
|
+
self._func_tuple_element_count[node.name] = len(tuple_decl_types)
|
|
4866
|
+
self._func_tuple_element_types[node.name] = tuple_decl_types
|
|
4867
|
+
elif selection_shape is not None:
|
|
4868
|
+
# ``f() => if c ... g() else [a, b]``: every arm yields a
|
|
4869
|
+
# tuple of one size, so the function returns that tuple.
|
|
4870
|
+
self._func_returns_tuple[node.name] = True
|
|
4871
|
+
self._func_tuple_element_count[node.name] = selection_shape[0]
|
|
4872
|
+
self._func_tuple_element_types[node.name] = selection_shape[1]
|
|
4273
4873
|
elif (
|
|
4274
4874
|
isinstance(terminal_ret_expr, FuncCall)
|
|
4275
4875
|
and isinstance(terminal_ret_expr.callee, Identifier)
|
|
@@ -4289,6 +4889,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4289
4889
|
self._func_tuple_element_types[node.name] = (
|
|
4290
4890
|
self._func_tuple_element_types.get(terminal_callee, ())
|
|
4291
4891
|
)
|
|
4892
|
+
elif (
|
|
4893
|
+
isinstance(terminal_ret_expr, FuncCall)
|
|
4894
|
+
and id(terminal_ret_expr) in self._security_tuple_shapes
|
|
4895
|
+
):
|
|
4896
|
+
# ``htf(sym, tf) => request.security(sym, tf, pack())``
|
|
4897
|
+
# returns the requested tuple: it was typed a double, which
|
|
4898
|
+
# did not compile, and its callers destructure it.
|
|
4899
|
+
self._func_returns_tuple[node.name] = True
|
|
4900
|
+
self._func_tuple_element_count[node.name] = (
|
|
4901
|
+
self._security_tuple_shapes[id(terminal_ret_expr)]
|
|
4902
|
+
)
|
|
4903
|
+
self._func_tuple_element_types[node.name] = (
|
|
4904
|
+
self._tuple_element_types_by_node.get(id(terminal_ret_expr), ())
|
|
4905
|
+
)
|
|
4292
4906
|
|
|
4293
4907
|
# Re-run direct-wrapper propagation to a fixed point whenever a new
|
|
4294
4908
|
# definition is analyzed. This makes ``outer()=>inner()`` source-order
|
|
@@ -4516,6 +5130,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4516
5130
|
try:
|
|
4517
5131
|
for stmt in node.body:
|
|
4518
5132
|
ret_type = self._visit(stmt)
|
|
5133
|
+
if node.body:
|
|
5134
|
+
ret_type = self._statement_value_type(node.body[-1], ret_type)
|
|
4519
5135
|
if terminal_ret_expr is not None:
|
|
4520
5136
|
terminal_spec = self._type_spec_from_expr(terminal_ret_expr)
|
|
4521
5137
|
if terminal_spec is not None and terminal_spec.kind == "map":
|
|
@@ -5255,6 +5871,17 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5255
5871
|
self._visit(arg)
|
|
5256
5872
|
return PineType.COLOR
|
|
5257
5873
|
|
|
5874
|
+
# string(x): Pine's cast to string, usually of na
|
|
5875
|
+
# (``[string(na), string(na)]`` arms of a string tuple). It fell
|
|
5876
|
+
# to the unknown-builtin default FLOAT, which untyped a selection
|
|
5877
|
+
# tuple whose other arm carries strings.
|
|
5878
|
+
if func_name == "string":
|
|
5879
|
+
for arg in node.args:
|
|
5880
|
+
self._visit(arg)
|
|
5881
|
+
for val in node.kwargs.values():
|
|
5882
|
+
self._visit(val)
|
|
5883
|
+
return PineType.STRING
|
|
5884
|
+
|
|
5258
5885
|
# User-defined function call
|
|
5259
5886
|
if func_name in self._func_defs:
|
|
5260
5887
|
return self._handle_user_func_call(func_name, node)
|
|
@@ -5521,6 +6148,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5521
6148
|
"""Re-run map-history safety after untyped callable args are known."""
|
|
5522
6149
|
if owner in visiting:
|
|
5523
6150
|
return
|
|
6151
|
+
# The same owner, specs and path check the same nodes again, so a
|
|
6152
|
+
# helper that a diamond of callers reaches (``f1(x) => f0(x) +
|
|
6153
|
+
# f0(x)``, ...) is validated once, not once per path. A failed check
|
|
6154
|
+
# raises, so only a passed validation is remembered.
|
|
6155
|
+
key = (
|
|
6156
|
+
owner, visiting, tuple(sorted(parameter_specs.items())),
|
|
6157
|
+
len(self._deferred_param_history_refs.get(owner, [])),
|
|
6158
|
+
len(self._deferred_param_call_edges.get(owner, [])),
|
|
6159
|
+
)
|
|
6160
|
+
validated = self.__dict__.setdefault("_deferred_param_history_validated", set())
|
|
6161
|
+
if key in validated:
|
|
6162
|
+
return
|
|
5524
6163
|
next_visiting = visiting | {owner}
|
|
5525
6164
|
for node, parameter_nodes in self._deferred_param_history_refs.get(
|
|
5526
6165
|
owner, []
|
|
@@ -5569,6 +6208,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5569
6208
|
callee_specs,
|
|
5570
6209
|
next_visiting,
|
|
5571
6210
|
)
|
|
6211
|
+
validated.add(key)
|
|
5572
6212
|
|
|
5573
6213
|
def _propagate_deferred_map_callable_specs(
|
|
5574
6214
|
self,
|
|
@@ -5866,9 +6506,40 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5866
6506
|
self._func_series_history_nodes.setdefault(
|
|
5867
6507
|
(func_name, name), node
|
|
5868
6508
|
)
|
|
6509
|
+
self._note_function_global_history_read(node, name, sym)
|
|
5869
6510
|
|
|
5870
6511
|
return obj_type
|
|
5871
6512
|
|
|
6513
|
+
def _note_function_global_history_read(
|
|
6514
|
+
self, node: Subscript, name: str, sym) -> None:
|
|
6515
|
+
"""Record ``x[k]`` in a plain UDF body on a script variable.
|
|
6516
|
+
|
|
6517
|
+
TradingView builds the history of a series used inside a function
|
|
6518
|
+
through each successive call of it: in ``f() => gv[1]``, ``gv[1]`` is
|
|
6519
|
+
``gv`` as that call site saw it on its latest call at or before the
|
|
6520
|
+
previous bar, whether ``gv`` is ``var`` or not, and ``bar_index`` reads
|
|
6521
|
+
the same way; the chart built-ins keep the chart's history
|
|
6522
|
+
(tests/fixtures/function_global_history). Codegen gives every emitted
|
|
6523
|
+
body of the function its own buffer, so the function needs one body
|
|
6524
|
+
per call site, like any other stateful function.
|
|
6525
|
+
"""
|
|
6526
|
+
if not self._enclosing_func_names or sym.scope != "global":
|
|
6527
|
+
return
|
|
6528
|
+
owner = self._enclosing_func_names[-1]
|
|
6529
|
+
if not isinstance(self._func_defs.get(owner), FuncDef):
|
|
6530
|
+
return
|
|
6531
|
+
# A built-in other than bar_index is chart history.
|
|
6532
|
+
if name != "bar_index" and getattr(sym, "_pf_decl_node_id", None) is None:
|
|
6533
|
+
return
|
|
6534
|
+
spec = getattr(sym, "type_spec", None)
|
|
6535
|
+
if (sym.pine_type not in (PineType.INT, PineType.FLOAT, PineType.BOOL)
|
|
6536
|
+
or (spec is not None and spec.kind != "primitive")):
|
|
6537
|
+
return
|
|
6538
|
+
reads = self._func_global_history_reads.setdefault(owner, [])
|
|
6539
|
+
if name not in reads:
|
|
6540
|
+
reads.append(name)
|
|
6541
|
+
self._func_global_history_nodes[id(node)] = (owner, name, node)
|
|
6542
|
+
|
|
5872
6543
|
def _visit_Identifier(self, node: Identifier) -> PineType:
|
|
5873
6544
|
# Some identifiers are namespace prefixes handled elsewhere
|
|
5874
6545
|
if node.name in ("strategy", "ta", "input", "math", "str", "color",
|
|
@@ -5973,7 +6644,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5973
6644
|
callee=MemberAccess(object=Identifier(name="ta"), member=node.member),
|
|
5974
6645
|
args=[], kwargs={},
|
|
5975
6646
|
)
|
|
5976
|
-
|
|
6647
|
+
before = len(self._ta_call_sites)
|
|
6648
|
+
result = self._handle_ta_call(node.member, synthetic_call)
|
|
6649
|
+
# The site belongs to THIS read: key it on the AST node the
|
|
6650
|
+
# codegen will meet (the MemberAccess), not on the synthetic
|
|
6651
|
+
# call nobody else holds. Without this a bare ``ta.vwap``
|
|
6652
|
+
# inside request.security() could not find its own site and
|
|
6653
|
+
# was lowered to the first live CHART member (advanced by the
|
|
6654
|
+
# chart bar AND every requested sub-bar, read through
|
|
6655
|
+
# history_advances_new_bar()); a second top-level read bound
|
|
6656
|
+
# to the first read's member as well.
|
|
6657
|
+
if len(self._ta_call_sites) > before:
|
|
6658
|
+
self._ta_call_sites[-1].node = node
|
|
6659
|
+
return result
|
|
5977
6660
|
return PineType.FLOAT
|
|
5978
6661
|
|
|
5979
6662
|
# math.* properties
|
|
@@ -6008,9 +6691,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6008
6691
|
if ns == "barstate":
|
|
6009
6692
|
return PineType.BOOL
|
|
6010
6693
|
|
|
6011
|
-
# alert
|
|
6694
|
+
# alert.freq_* are const strings ("all", "once_per_bar",
|
|
6695
|
+
# "once_per_bar_close"); any other alert.* member is refused by
|
|
6696
|
+
# the support checker.
|
|
6012
6697
|
if ns == "alert":
|
|
6013
|
-
return PineType.
|
|
6698
|
+
return PineType.STRING
|
|
6014
6699
|
|
|
6015
6700
|
# position.* constants for tables (middle_right, top_left, etc.)
|
|
6016
6701
|
if ns == "position":
|
|
@@ -6100,6 +6785,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6100
6785
|
return PineType.FLOAT
|
|
6101
6786
|
|
|
6102
6787
|
sym = self._symbols.resolve(ns)
|
|
6788
|
+
# A field read's receiver is an identifier read like any other:
|
|
6789
|
+
# codegen needs its binding scope (``_call_site_var_name``).
|
|
6790
|
+
self._identifier_binding_scopes[id(node.object)] = (
|
|
6791
|
+
getattr(sym, "scope", None) if sym is not None else None
|
|
6792
|
+
)
|
|
6103
6793
|
udt_name = None
|
|
6104
6794
|
if sym is not None:
|
|
6105
6795
|
udt_name = sym.udt_type_name
|