pineforge-codegen 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
- pineforge_codegen/analyzer/base.py +797 -45
- pineforge_codegen/analyzer/call_handlers.py +368 -68
- pineforge_codegen/analyzer/contracts.py +91 -0
- pineforge_codegen/analyzer/diagnostics.py +18 -4
- pineforge_codegen/analyzer/tables.py +20 -2
- pineforge_codegen/analyzer/types.py +53 -0
- pineforge_codegen/codegen/__init__.py +4 -0
- pineforge_codegen/codegen/base.py +1613 -111
- pineforge_codegen/codegen/drawing.py +560 -0
- pineforge_codegen/codegen/emit_top.py +536 -45
- pineforge_codegen/codegen/input.py +31 -0
- pineforge_codegen/codegen/security.py +791 -39
- pineforge_codegen/codegen/ta.py +87 -1
- pineforge_codegen/codegen/tables.py +115 -10
- pineforge_codegen/codegen/types.py +478 -46
- pineforge_codegen/codegen/visit_call.py +331 -75
- pineforge_codegen/codegen/visit_expr.py +116 -14
- pineforge_codegen/codegen/visit_stmt.py +208 -23
- pineforge_codegen/parser.py +129 -29
- pineforge_codegen/signatures.py +2 -2
- pineforge_codegen/support_checker.py +320 -19
- {pineforge_codegen-0.8.0.dist-info → pineforge_codegen-0.9.0.dist-info}/METADATA +1 -1
- pineforge_codegen-0.9.0.dist-info/RECORD +36 -0
- {pineforge_codegen-0.8.0.dist-info → pineforge_codegen-0.9.0.dist-info}/WHEEL +1 -1
- pineforge_codegen-0.8.0.dist-info/RECORD +0 -35
- {pineforge_codegen-0.8.0.dist-info → pineforge_codegen-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -127,8 +127,29 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
127
127
|
self._global_var_decls: list[tuple[str, PineType]] = []
|
|
128
128
|
self._global_expr_map: dict[str, Any] = {}
|
|
129
129
|
self._var_member_init_exprs: dict[str, Any] = {}
|
|
130
|
+
# Block-scoped ``var``/``varip`` name-collision disambiguation.
|
|
131
|
+
# Two same-named block-scoped vars in SIBLING non-global, non-function
|
|
132
|
+
# scopes (e.g. ``var bool valid`` declared inside ``if A`` and again
|
|
133
|
+
# inside ``if B``) would otherwise dedupe to ONE C++ member and
|
|
134
|
+
# cross-contaminate. ``_block_node_stack`` tracks the enclosing
|
|
135
|
+
# block AST nodes during analysis; ``_block_var_owner`` maps a raw
|
|
136
|
+
# block-var name to the id() of the FIRST block that declared it;
|
|
137
|
+
# ``_block_var_renames`` maps id(block_node) -> {raw_name: unique}
|
|
138
|
+
# for every later colliding block so codegen can activate the
|
|
139
|
+
# rename via ``_active_var_remap`` while emitting that block.
|
|
140
|
+
self._block_node_stack: list[Any] = []
|
|
141
|
+
self._block_var_owner: dict[str, int] = {}
|
|
142
|
+
self._block_var_renames: dict[int, dict[str, str]] = {}
|
|
143
|
+
self._block_var_seq = 0
|
|
130
144
|
self._ta_counter = 0
|
|
131
145
|
self._fixnan_counter = 0
|
|
146
|
+
# All fixnan member names minted so far (base + clones), for O(1)
|
|
147
|
+
# collision detection when minting a per-call-site fixnan clone.
|
|
148
|
+
self._fixnan_member_names: set[str] = set()
|
|
149
|
+
# Authoritative fixnan clone-name map for collisions: (func, cs_idx)
|
|
150
|
+
# -> {orig_member: cloned_member}. Consumed verbatim by the codegen
|
|
151
|
+
# when the default ``{base}_cs{cs_idx}`` formula would collide.
|
|
152
|
+
self._func_cs_fixnan_clone_names: dict[tuple[str, int], dict[str, str]] = {}
|
|
132
153
|
# Track user-defined function nodes for deferred analysis
|
|
133
154
|
self._func_defs: dict[str, FuncDef] = {}
|
|
134
155
|
# Track user-defined function return types
|
|
@@ -141,6 +162,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
141
162
|
# expression (``=> Sample.new(...)`` or last stmt ``Sample.new(...)``).
|
|
142
163
|
# Probe: data/validation/udt-method-probe-20-udt-return-from-func.
|
|
143
164
|
self._func_udt_return_types: dict[str, str] = {}
|
|
165
|
+
self._func_return_type_specs: dict[str, "TypeSpec"] = {}
|
|
166
|
+
self._func_param_type_specs: dict[str, list] = {}
|
|
144
167
|
# Per-function var_members and series_vars (for call-site cloning)
|
|
145
168
|
self._func_var_members: dict[str, list] = {} # func_name -> [(name, PineType, init_str)]
|
|
146
169
|
self._func_series_vars: dict[str, set] = {} # func_name -> set[str]
|
|
@@ -148,6 +171,37 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
148
171
|
self._func_ta_ranges: dict[str, tuple[int, int]] = {} # func_name -> (start, end) indices
|
|
149
172
|
self._func_call_site_count: dict[str, int] = {} # func_name -> count
|
|
150
173
|
self._func_call_cs_map: dict[int, tuple[str, int]] = {} # call_node_id -> (func_name, cs_idx)
|
|
174
|
+
# Textual nested calls whose identity is inherited from the active
|
|
175
|
+
# parent clone rather than assigned a fixed source-level cs index.
|
|
176
|
+
# Kept separately so a second propagation pass (security TF cloning)
|
|
177
|
+
# does not accidentally backfill them as a new cs{N} call site.
|
|
178
|
+
self._func_inherited_call_nodes: set[int] = set()
|
|
179
|
+
# Per-function fixnan site ownership: func_name -> list of fixnan site
|
|
180
|
+
# indices in self._fixnan_sites owned by that function. Mirrors the
|
|
181
|
+
# TA-range slicing but for fixnan state, so per-call-site cloning can
|
|
182
|
+
# mint fresh fixnan members per variant and the codegen dead-code pass
|
|
183
|
+
# can skip fixnan state owned by dead functions.
|
|
184
|
+
self._func_fixnan_indices: dict[str, list[int]] = {}
|
|
185
|
+
# Functions that need per-call-site BODY cloning despite owning no
|
|
186
|
+
# TA/series/var state. The set originated for security-tf
|
|
187
|
+
# monomorphization; it is also the existing emitter contract used by
|
|
188
|
+
# pure wrappers around stateful callees and fixnan-only functions.
|
|
189
|
+
# Security evaluator identity remains independently tracked by each
|
|
190
|
+
# SecurityCallInfo.callsite_idx.
|
|
191
|
+
self._func_security_clone_only: set[str] = set()
|
|
192
|
+
# Authoritative clone-name map: (func_name, cs_idx) -> {orig_member_name:
|
|
193
|
+
# cloned_member_name}. The codegen rebuilds its TA remap from the
|
|
194
|
+
# ``{orig}_cs{cs_idx}`` formula by default, but a TA site reached through
|
|
195
|
+
# MULTIPLE enclosing functions (e.g. a helper cloned both directly and via
|
|
196
|
+
# a range-widened outer function) can collide on that formula. When the
|
|
197
|
+
# analyzer must disambiguate a clone's member name, it records the actual
|
|
198
|
+
# chosen name here so the codegen consumes it verbatim instead of
|
|
199
|
+
# re-deriving a colliding name. Empty for the common (no-collision) case,
|
|
200
|
+
# keeping generated output byte-identical.
|
|
201
|
+
self._func_cs_ta_clone_names: dict[tuple[str, int], dict[str, str]] = {}
|
|
202
|
+
# All TA member names minted so far (base + clones), for O(1) collision
|
|
203
|
+
# detection when minting a new clone.
|
|
204
|
+
self._ta_member_names: set[str] = set()
|
|
151
205
|
# UDT field definitions: type_name -> {field_name: PineType}
|
|
152
206
|
self._udt_fields: dict[str, dict[str, PineType]] = {}
|
|
153
207
|
# var_name -> UDT type for variables holding UDT instances
|
|
@@ -163,6 +217,21 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
163
217
|
self._current_top_level_stmt: ASTNode | None = None
|
|
164
218
|
self._global_scope = True
|
|
165
219
|
self._static_vars: set[str] = set()
|
|
220
|
+
# Stack of enclosing user-function param-name sets, pushed while visiting
|
|
221
|
+
# a FuncDef body. Lets a nested user-func call detect when it substitutes
|
|
222
|
+
# a TA ctor length with one of the OUTER function's params, so the outer
|
|
223
|
+
# call site can re-substitute (e.g. f_bbwp(_bbwLen) -> f_basisMa(_len)).
|
|
224
|
+
self._enclosing_func_params: list[set[str]] = []
|
|
225
|
+
# Parallel stack of the function NAMES whose param-sets are in
|
|
226
|
+
# ``_enclosing_func_params``. The top of stack (or None at global
|
|
227
|
+
# scope) is the owner of any ORIGINAL ``ta.*`` site minted right now
|
|
228
|
+
# -- recorded on ``TACallSite.owner_func`` so the codegen dead-code
|
|
229
|
+
# pass can tell borrowed clones apart from a dead function's own
|
|
230
|
+
# sites (see contracts.TACallSite.owner_func).
|
|
231
|
+
self._enclosing_func_names: list[str] = []
|
|
232
|
+
# Set of TA-site indices a nested user-func call rewrote in terms of the
|
|
233
|
+
# current enclosing function's params (None when not inside a FuncDef body).
|
|
234
|
+
self._nested_ta_touched: set | None = None
|
|
166
235
|
|
|
167
236
|
# Pre-populate builtins
|
|
168
237
|
self._populate_builtins()
|
|
@@ -232,6 +301,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
232
301
|
# can call g_csK with isolated state.
|
|
233
302
|
self._propagate_call_site_counts()
|
|
234
303
|
|
|
304
|
+
# Reject (loudly) a request.security whose timeframe is a UDF parameter
|
|
305
|
+
# called with multiple distinct literal timeframes — a single evaluator
|
|
306
|
+
# cannot serve them and per-callsite specialization is not yet wired.
|
|
307
|
+
self._check_mixed_callsite_security_tf()
|
|
308
|
+
|
|
235
309
|
# Keep only truly pure global expressions for request.security rebinding.
|
|
236
310
|
# Globals later reassigned with := become series/stateful variables and
|
|
237
311
|
# must not be rebound to their declaration-time initializer.
|
|
@@ -257,6 +331,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
257
331
|
var_members=self._var_members,
|
|
258
332
|
func_infos=self._func_infos,
|
|
259
333
|
fixnan_sites=self._fixnan_sites,
|
|
334
|
+
func_fixnan_indices=self._func_fixnan_indices,
|
|
335
|
+
func_cs_fixnan_clone_names=self._func_cs_fixnan_clone_names,
|
|
260
336
|
strategy_params=self._strategy_params,
|
|
261
337
|
diagnostics=self._diagnostics,
|
|
262
338
|
filename=self._filename,
|
|
@@ -266,6 +342,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
266
342
|
func_ta_ranges=self._func_ta_ranges,
|
|
267
343
|
func_call_cs_map=self._func_call_cs_map,
|
|
268
344
|
func_call_site_counts=self._func_call_site_count,
|
|
345
|
+
func_security_clone_only=self._func_security_clone_only,
|
|
346
|
+
func_cs_ta_clone_names=self._func_cs_ta_clone_names,
|
|
269
347
|
udt_defs=self._udt_fields,
|
|
270
348
|
enum_defs=self._enum_defs,
|
|
271
349
|
enum_member_strings=self._enum_member_strings,
|
|
@@ -273,9 +351,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
273
351
|
global_mutable_infos=mutable_global_infos,
|
|
274
352
|
func_var_members=self._func_var_members,
|
|
275
353
|
func_series_vars=self._func_series_vars,
|
|
354
|
+
func_return_type_specs=dict(self._func_return_type_specs),
|
|
276
355
|
udt_var_types=dict(self._udt_var_types),
|
|
277
356
|
collection_types=dict(self._collection_types),
|
|
278
357
|
udt_field_type_specs=dict(self._udt_field_type_specs),
|
|
358
|
+
block_var_renames=dict(self._block_var_renames),
|
|
279
359
|
)
|
|
280
360
|
|
|
281
361
|
def _record_global_binding_stmt(self, name: str, pine_type: PineType,
|
|
@@ -299,6 +379,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
299
379
|
if top_stmt is not None and (not info.source_stmts or info.source_stmts[-1] is not top_stmt):
|
|
300
380
|
info.source_stmts.append(top_stmt)
|
|
301
381
|
|
|
382
|
+
@staticmethod
|
|
383
|
+
def _is_input_func_call(node: FuncCall) -> bool:
|
|
384
|
+
"""True for an ``input(...)`` or ``input.<member>(...)`` call."""
|
|
385
|
+
callee = node.callee
|
|
386
|
+
if isinstance(callee, Identifier) and callee.name == "input":
|
|
387
|
+
return True
|
|
388
|
+
return (
|
|
389
|
+
isinstance(callee, MemberAccess)
|
|
390
|
+
and isinstance(callee.object, Identifier)
|
|
391
|
+
and callee.object.name == "input"
|
|
392
|
+
)
|
|
393
|
+
|
|
302
394
|
def _collect_security_mutable_globals(
|
|
303
395
|
self, node: ASTNode | None, resolving: set[str] | None = None
|
|
304
396
|
) -> set[str]:
|
|
@@ -344,6 +436,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
344
436
|
resolving.remove(call_key)
|
|
345
437
|
return out
|
|
346
438
|
|
|
439
|
+
if isinstance(node, FuncCall) and self._is_input_func_call(node):
|
|
440
|
+
# An ``input.*()`` / ``input()`` initializer is a compile-time
|
|
441
|
+
# constant. Only its defval (first positional or ``defval=``) can
|
|
442
|
+
# carry a genuine data dependency; the cosmetic kwargs
|
|
443
|
+
# (group/tooltip/title/inline/display/confirm/minval/maxval/step)
|
|
444
|
+
# are presentation-only. Walking them would falsely pull a
|
|
445
|
+
# ``var string GROUP = "..."`` label into the security's
|
|
446
|
+
# mutable-globals set and trip the "TA ctor depends on rebound
|
|
447
|
+
# mutable globals" reject (parallax / higherTimeframeLength).
|
|
448
|
+
defval = node.args[0] if node.args else node.kwargs.get("defval")
|
|
449
|
+
if defval is not None:
|
|
450
|
+
out |= self._collect_security_mutable_globals(defval, resolving)
|
|
451
|
+
return out
|
|
452
|
+
|
|
347
453
|
def walk(value: Any) -> None:
|
|
348
454
|
nonlocal out
|
|
349
455
|
if value is None:
|
|
@@ -365,40 +471,274 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
365
471
|
return out
|
|
366
472
|
|
|
367
473
|
def _propagate_call_site_counts(self) -> None:
|
|
368
|
-
"""Propagate
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
474
|
+
"""Propagate stateful UDF identity through complete call paths.
|
|
475
|
+
|
|
476
|
+
A UDF is stateful when it owns series/``var`` state, TA state, or
|
|
477
|
+
``fixnan`` state, and every pure wrapper that reaches such a UDF is
|
|
478
|
+
stateful transitively. Give each wrapper's textual calls stable cs
|
|
479
|
+
identities, then inherit a multi-call-site parent's count down to its
|
|
480
|
+
stateful callees. Any inherited TA/fixnan variants are materialized
|
|
481
|
+
immediately; exporting a count without the corresponding members would
|
|
482
|
+
make codegen reference undeclared or shared state.
|
|
374
483
|
"""
|
|
375
484
|
from pineforge_codegen.ast_nodes import FuncCall, FuncDef, Identifier
|
|
376
485
|
|
|
377
|
-
# Collect all user function definitions from AST
|
|
378
486
|
func_defs: dict[str, FuncDef] = {}
|
|
379
487
|
for stmt in self._ast.body:
|
|
380
488
|
if isinstance(stmt, FuncDef):
|
|
381
489
|
func_defs[stmt.name] = stmt
|
|
382
490
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
491
|
+
# UDT methods participate in the same stateful call graph as plain
|
|
492
|
+
# UDFs. Their analyzer identity is ``Type.method`` while FuncInfo.node
|
|
493
|
+
# carries the synthetic FuncDef used by codegen.
|
|
494
|
+
func_info_by_name = {fi.name: fi for fi in self._func_infos}
|
|
495
|
+
for fi in self._func_infos:
|
|
496
|
+
if getattr(fi, "is_udt_method", False) and fi.node is not None:
|
|
497
|
+
func_defs.setdefault(fi.name, fi.node)
|
|
498
|
+
|
|
499
|
+
# Preserve call-node identity as well as the callee name: late clone
|
|
500
|
+
# materialization needs the textual call's argument mapping to resolve
|
|
501
|
+
# parameterized TA constructor lengths.
|
|
502
|
+
def _resolved_user_call_name(call: FuncCall, owner: str | None) -> str | None:
|
|
503
|
+
if isinstance(call.callee, Identifier):
|
|
504
|
+
return call.callee.name if call.callee.name in func_defs else None
|
|
505
|
+
if not isinstance(call.callee, MemberAccess):
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
recv = call.callee.object
|
|
509
|
+
method = call.callee.member
|
|
510
|
+
udt_name: str | None = None
|
|
511
|
+
if isinstance(recv, Identifier):
|
|
512
|
+
udt_name = self._udt_var_types.get(recv.name)
|
|
513
|
+
owner_info = func_info_by_name.get(owner or "")
|
|
514
|
+
if udt_name is None and owner_info is not None \
|
|
515
|
+
and owner_info.node is not None:
|
|
516
|
+
if (getattr(owner_info, "is_udt_method", False)
|
|
517
|
+
and owner_info.node.params
|
|
518
|
+
and recv.name == owner_info.node.params[0]):
|
|
519
|
+
udt_name = owner_info.udt_type_name
|
|
520
|
+
elif recv.name in owner_info.node.params:
|
|
521
|
+
param_idx = owner_info.node.params.index(recv.name)
|
|
522
|
+
specs = getattr(owner_info, "param_type_specs", []) or []
|
|
523
|
+
spec = specs[param_idx] if param_idx < len(specs) else None
|
|
524
|
+
if spec is not None and spec.kind == "udt":
|
|
525
|
+
udt_name = spec.name
|
|
526
|
+
if udt_name is None:
|
|
527
|
+
spec = self._type_spec_from_expr(recv)
|
|
528
|
+
if spec is not None and spec.kind == "udt":
|
|
529
|
+
udt_name = spec.name
|
|
530
|
+
key = f"{udt_name}.{method}" if udt_name else ""
|
|
531
|
+
return key if key in func_defs else None
|
|
532
|
+
|
|
533
|
+
def _find_calls(node, known_funcs: set[str],
|
|
534
|
+
owner: str | None = None,
|
|
535
|
+
seen: set[int] | None = None) -> list[tuple[str, FuncCall]]:
|
|
536
|
+
calls: list[tuple[str, FuncCall]] = []
|
|
537
|
+
if node is None:
|
|
538
|
+
return calls
|
|
539
|
+
if seen is None:
|
|
540
|
+
seen = set()
|
|
541
|
+
if isinstance(node, (list, tuple, dict)) or hasattr(node, "__dict__"):
|
|
542
|
+
node_id = id(node)
|
|
543
|
+
if node_id in seen:
|
|
544
|
+
return calls
|
|
545
|
+
seen.add(node_id)
|
|
546
|
+
if isinstance(node, (list, tuple)):
|
|
547
|
+
for item in node:
|
|
548
|
+
calls.extend(_find_calls(item, known_funcs, owner, seen))
|
|
549
|
+
return calls
|
|
550
|
+
if isinstance(node, dict):
|
|
551
|
+
for item in node.values():
|
|
552
|
+
calls.extend(_find_calls(item, known_funcs, owner, seen))
|
|
553
|
+
return calls
|
|
554
|
+
if not hasattr(node, "__dict__"):
|
|
555
|
+
return calls
|
|
556
|
+
if isinstance(node, FuncCall):
|
|
557
|
+
resolved = _resolved_user_call_name(node, owner)
|
|
558
|
+
if resolved in known_funcs:
|
|
559
|
+
calls.append((resolved, node))
|
|
389
560
|
for attr_val in vars(node).values():
|
|
390
|
-
|
|
391
|
-
for item in attr_val:
|
|
392
|
-
if hasattr(item, '__dict__'):
|
|
393
|
-
calls |= _find_calls(item, known_funcs)
|
|
394
|
-
elif hasattr(attr_val, '__dict__'):
|
|
395
|
-
calls |= _find_calls(attr_val, known_funcs)
|
|
561
|
+
calls.extend(_find_calls(attr_val, known_funcs, owner, seen))
|
|
396
562
|
return calls
|
|
397
563
|
|
|
398
564
|
known_func_names = set(func_defs.keys())
|
|
565
|
+
calls_by_parent = {
|
|
566
|
+
name: _find_calls(func_def, known_func_names, name)
|
|
567
|
+
for name, func_def in func_defs.items()
|
|
568
|
+
}
|
|
569
|
+
calls_by_callee: dict[str, list[FuncCall]] = {
|
|
570
|
+
name: [] for name in known_func_names
|
|
571
|
+
}
|
|
572
|
+
# Preserve source order across definitions and top-level statements.
|
|
573
|
+
# Method bodies use their ``Type.method`` owner so ``self.sibling()``
|
|
574
|
+
# resolves without relying on a now-exited symbol-table scope.
|
|
575
|
+
for stmt in self._ast.body:
|
|
576
|
+
if isinstance(stmt, FuncDef):
|
|
577
|
+
owner = stmt.name
|
|
578
|
+
elif isinstance(stmt, MethodDef):
|
|
579
|
+
owner = f"{stmt.type_name}.{stmt.name}"
|
|
580
|
+
else:
|
|
581
|
+
owner = None
|
|
582
|
+
for callee, call in _find_calls(stmt, known_func_names, owner):
|
|
583
|
+
calls_by_callee.setdefault(callee, []).append(call)
|
|
584
|
+
|
|
585
|
+
# Codegen synthesizes a Series buffer for two expression shapes that
|
|
586
|
+
# do not appear in ``_func_series_vars`` themselves:
|
|
587
|
+
#
|
|
588
|
+
# * a call result read through history, e.g. ``f()[1]``;
|
|
589
|
+
# * a scalar expression bridged into a UDF series parameter, e.g.
|
|
590
|
+
# ``history(close + open)`` where ``history(src) => src[1]``.
|
|
591
|
+
#
|
|
592
|
+
# A buffer is mutable per-call-site state just like TA/fixnan. Mark
|
|
593
|
+
# its lexical owner stateful before the normal call-path closure so a
|
|
594
|
+
# function invoked from two source sites receives two emitted bodies
|
|
595
|
+
# (and therefore two independent generated buffer members). Without
|
|
596
|
+
# this, moving the old function-local static into a class member would
|
|
597
|
+
# accidentally merge both Pine call sites into one history stream.
|
|
598
|
+
def _actual_arg(call: FuncCall, param_name: str, param_idx: int):
|
|
599
|
+
if param_idx < len(call.args):
|
|
600
|
+
return call.args[param_idx]
|
|
601
|
+
return call.kwargs.get(param_name)
|
|
602
|
+
|
|
603
|
+
def _needs_scalar_series_bridge(call: FuncCall) -> bool:
|
|
604
|
+
if not isinstance(call.callee, Identifier):
|
|
605
|
+
return False
|
|
606
|
+
callee = call.callee.name
|
|
607
|
+
fi = func_info_by_name.get(callee)
|
|
608
|
+
if fi is None or fi.node is None:
|
|
609
|
+
return False
|
|
610
|
+
series_params = self._func_series_vars.get(callee, set())
|
|
611
|
+
if not series_params:
|
|
612
|
+
return False
|
|
613
|
+
direct_bar_series = {
|
|
614
|
+
"open", "high", "low", "close", "volume",
|
|
615
|
+
"hl2", "hlc3", "ohlc4",
|
|
616
|
+
}
|
|
617
|
+
for idx, param_name in enumerate(fi.node.params):
|
|
618
|
+
if param_name not in series_params:
|
|
619
|
+
continue
|
|
620
|
+
arg = _actual_arg(call, param_name, idx)
|
|
621
|
+
if arg is None:
|
|
622
|
+
continue
|
|
623
|
+
if isinstance(arg, Identifier) and (
|
|
624
|
+
arg.name in direct_bar_series or arg.name in self._series_vars
|
|
625
|
+
):
|
|
626
|
+
continue
|
|
627
|
+
return True
|
|
628
|
+
return False
|
|
399
629
|
|
|
400
|
-
|
|
401
|
-
|
|
630
|
+
def _has_synthetic_history_state(
|
|
631
|
+
node, seen: set[int] | None = None) -> bool:
|
|
632
|
+
if node is None:
|
|
633
|
+
return False
|
|
634
|
+
if seen is None:
|
|
635
|
+
seen = set()
|
|
636
|
+
if isinstance(node, (list, tuple, dict)) or hasattr(node, "__dict__"):
|
|
637
|
+
node_id = id(node)
|
|
638
|
+
if node_id in seen:
|
|
639
|
+
return False
|
|
640
|
+
seen.add(node_id)
|
|
641
|
+
if isinstance(node, (list, tuple)):
|
|
642
|
+
return any(
|
|
643
|
+
_has_synthetic_history_state(item, seen) for item in node
|
|
644
|
+
)
|
|
645
|
+
if isinstance(node, dict):
|
|
646
|
+
return any(
|
|
647
|
+
_has_synthetic_history_state(item, seen)
|
|
648
|
+
for item in node.values()
|
|
649
|
+
)
|
|
650
|
+
if not hasattr(node, "__dict__"):
|
|
651
|
+
return False
|
|
652
|
+
if (isinstance(node, Subscript)
|
|
653
|
+
and isinstance(node.object, FuncCall)):
|
|
654
|
+
return True
|
|
655
|
+
if isinstance(node, FuncCall) and _needs_scalar_series_bridge(node):
|
|
656
|
+
return True
|
|
657
|
+
return any(
|
|
658
|
+
_has_synthetic_history_state(value, seen)
|
|
659
|
+
for value in vars(node).values()
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
synthetic_history_stateful = {
|
|
663
|
+
name for name, func_def in func_defs.items()
|
|
664
|
+
if _has_synthetic_history_state(func_def)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
# request.security owns a separate evaluator context and already
|
|
668
|
+
# materializes/remaps its embedded TA state per SecurityCallInfo. Do
|
|
669
|
+
# not thread ordinary UDF clone indices through a function used as a
|
|
670
|
+
# security expression (or through the containing wrapper); doing so
|
|
671
|
+
# would double-clone evaluator state and disturb expression identity.
|
|
672
|
+
security_boundary_funcs: set[str] = set()
|
|
673
|
+
for sec in getattr(self, "_security_calls", []) or []:
|
|
674
|
+
containing = getattr(sec, "containing_func", "") or ""
|
|
675
|
+
if containing:
|
|
676
|
+
security_boundary_funcs.add(containing)
|
|
677
|
+
expression = getattr(sec, "expression", None)
|
|
678
|
+
if expression is not None:
|
|
679
|
+
security_boundary_funcs.update(
|
|
680
|
+
sub for sub, _ in _find_calls(
|
|
681
|
+
expression, known_func_names, containing or None
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Canonical direct-state predicate. TA-only and fixnan-only helpers
|
|
686
|
+
# are just as stateful as functions carrying an explicit series/var.
|
|
687
|
+
stateful = (
|
|
688
|
+
set(self._func_series_vars)
|
|
689
|
+
| set(self._func_var_members)
|
|
690
|
+
| set(self._func_ta_ranges)
|
|
691
|
+
| set(self._func_fixnan_indices)
|
|
692
|
+
| synthetic_history_stateful
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Close upward over the call graph so a pure A -> B -> stateful C chain
|
|
696
|
+
# receives variants at every level. The existing clone-only emitter
|
|
697
|
+
# marker is intentionally separate from request.security evaluator
|
|
698
|
+
# identity, so ordinary security calls remain shared unless the
|
|
699
|
+
# dedicated timeframe-monomorphization pass clones them.
|
|
700
|
+
changed = True
|
|
701
|
+
while changed:
|
|
702
|
+
changed = False
|
|
703
|
+
for fname, calls in calls_by_parent.items():
|
|
704
|
+
if fname in stateful:
|
|
705
|
+
continue
|
|
706
|
+
if any(sub in stateful for sub, _ in calls):
|
|
707
|
+
stateful.add(fname)
|
|
708
|
+
changed = True
|
|
709
|
+
|
|
710
|
+
# Direct fixnan-only functions and pure transitive wrappers own no
|
|
711
|
+
# TA/series member that would trip the emitter's ordinary body-clone
|
|
712
|
+
# gate. Reuse its established body-only clone marker; this does not
|
|
713
|
+
# create or renumber any SecurityCallInfo.
|
|
714
|
+
for fname in sorted(stateful):
|
|
715
|
+
if (fname not in self._func_ta_ranges
|
|
716
|
+
and fname not in self._func_series_vars
|
|
717
|
+
and fname not in self._func_var_members):
|
|
718
|
+
self._func_security_clone_only.add(fname)
|
|
719
|
+
|
|
720
|
+
# The initial visitor only numbers directly-stateful callees. Backfill
|
|
721
|
+
# stable identities for newly discovered pure wrappers without
|
|
722
|
+
# disturbing any indices already assigned by the visitor or security
|
|
723
|
+
# monomorphization.
|
|
724
|
+
for fname in sorted(stateful):
|
|
725
|
+
calls = calls_by_callee.get(fname, [])
|
|
726
|
+
current = self._func_call_site_count.get(fname, 0)
|
|
727
|
+
next_idx = current
|
|
728
|
+
for call in calls:
|
|
729
|
+
if id(call) in self._func_inherited_call_nodes:
|
|
730
|
+
continue
|
|
731
|
+
existing = self._func_call_cs_map.get(id(call))
|
|
732
|
+
if existing is not None and existing[0] == fname:
|
|
733
|
+
next_idx = max(next_idx, existing[1] + 1)
|
|
734
|
+
continue
|
|
735
|
+
self._func_call_cs_map[id(call)] = (fname, next_idx)
|
|
736
|
+
next_idx += 1
|
|
737
|
+
if next_idx > current:
|
|
738
|
+
self._func_call_site_count[fname] = next_idx
|
|
739
|
+
|
|
740
|
+
# Inherit each multi-call-site parent's index space down the full path.
|
|
741
|
+
# Re-run to a fixed point for A -> B -> C chains.
|
|
402
742
|
changed = True
|
|
403
743
|
while changed:
|
|
404
744
|
changed = False
|
|
@@ -407,17 +747,217 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
407
747
|
continue
|
|
408
748
|
if fname not in func_defs:
|
|
409
749
|
continue
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if not has_state:
|
|
750
|
+
if fname in security_boundary_funcs:
|
|
751
|
+
continue
|
|
752
|
+
for sub, call_node in calls_by_parent.get(fname, []):
|
|
753
|
+
if sub not in stateful:
|
|
415
754
|
continue
|
|
416
755
|
current = self._func_call_site_count.get(sub, 0)
|
|
417
756
|
if current < count:
|
|
757
|
+
# One textual nested call is not an independent cs0
|
|
758
|
+
# path: it inherits the active parent clone index.
|
|
759
|
+
# Remove the visitor's provisional cs0 map so codegen's
|
|
760
|
+
# established active-index fallback dispatches
|
|
761
|
+
# F_csK -> G_csK. Keeping the map would make the
|
|
762
|
+
# context-sensitive instance pre-pass pin every parent
|
|
763
|
+
# clone to G_cs0 before that fallback can run.
|
|
764
|
+
if current == 1:
|
|
765
|
+
cs_info = self._func_call_cs_map.get(id(call_node))
|
|
766
|
+
if cs_info == (sub, 0):
|
|
767
|
+
self._func_call_cs_map.pop(id(call_node), None)
|
|
768
|
+
self._func_inherited_call_nodes.add(id(call_node))
|
|
769
|
+
for cs_idx in range(current, count):
|
|
770
|
+
self._materialize_user_func_call_site_state(
|
|
771
|
+
sub,
|
|
772
|
+
cs_idx,
|
|
773
|
+
call_node,
|
|
774
|
+
reuse_existing_owner=fname,
|
|
775
|
+
)
|
|
418
776
|
self._func_call_site_count[sub] = count
|
|
419
777
|
changed = True
|
|
420
778
|
|
|
779
|
+
# ------------------------------------------------------------------
|
|
780
|
+
# Mixed-callsite UDF timeframe-param security rejection.
|
|
781
|
+
#
|
|
782
|
+
# A ``request.security`` whose ``timeframe`` is a parameter of its
|
|
783
|
+
# containing UDF maps to ONE evaluator regardless of how many times the
|
|
784
|
+
# UDF is called. When the UDF is called from >= 2 sites with DISTINCT
|
|
785
|
+
# literal timeframes, a single evaluator cannot faithfully serve them
|
|
786
|
+
# all and the resolver would silently collapse onto the chart timeframe
|
|
787
|
+
# (``input_tf_``). Per-callsite evaluator specialization (cloning the
|
|
788
|
+
# evaluator + UDF) is the correct fix but is not wired in this iteration,
|
|
789
|
+
# so we reject deterministically instead of emitting wrong semantics.
|
|
790
|
+
# ------------------------------------------------------------------
|
|
791
|
+
def _check_mixed_callsite_security_tf(self) -> None:
|
|
792
|
+
sec_calls = getattr(self, "_security_calls", None)
|
|
793
|
+
if not sec_calls:
|
|
794
|
+
return
|
|
795
|
+
# Build user-function definitions lookup once.
|
|
796
|
+
func_defs: dict[str, FuncDef] = {}
|
|
797
|
+
for stmt in self._ast.body:
|
|
798
|
+
if isinstance(stmt, FuncDef):
|
|
799
|
+
func_defs[stmt.name] = stmt
|
|
800
|
+
|
|
801
|
+
new_calls: list[SecurityCallInfo] = []
|
|
802
|
+
cloned_any = False
|
|
803
|
+
for sec in sec_calls:
|
|
804
|
+
containing = getattr(sec, "containing_func", "") or ""
|
|
805
|
+
tf_node = getattr(sec, "timeframe", None)
|
|
806
|
+
if not containing or not isinstance(tf_node, Identifier):
|
|
807
|
+
new_calls.append(sec)
|
|
808
|
+
continue
|
|
809
|
+
param_name = tf_node.name
|
|
810
|
+
fdef = func_defs.get(containing)
|
|
811
|
+
if fdef is None or param_name not in fdef.params:
|
|
812
|
+
new_calls.append(sec)
|
|
813
|
+
continue
|
|
814
|
+
pidx = fdef.params.index(param_name)
|
|
815
|
+
calls = list(self._iter_user_func_calls(containing))
|
|
816
|
+
if not calls:
|
|
817
|
+
new_calls.append(sec) # dead code — evaluator result never read
|
|
818
|
+
continue
|
|
819
|
+
# Number call sites for THIS function. If ``containing`` already
|
|
820
|
+
# has TA call sites or series/var members, the per-call-site
|
|
821
|
+
# UDF-body-cloning mechanism already numbered its call sites in
|
|
822
|
+
# _func_call_cs_map (a request.security nested ta.* registers as
|
|
823
|
+
# part of the function-body walk, same as a top-level ta.* call)
|
|
824
|
+
# — reuse that authoritative numbering so this clone's
|
|
825
|
+
# callsite_idx stays aligned with self._active_call_site_idx.
|
|
826
|
+
# Otherwise (e.g. ``f(tf) => request.security(sym, tf, close)``
|
|
827
|
+
# with no nested ta.* call) that mechanism never fired for this
|
|
828
|
+
# function at all, since it gates on has_ta/has_series; assign
|
|
829
|
+
# fresh cs_idx ourselves in call-site order and BACKFILL
|
|
830
|
+
# func_call_cs_map / func_call_site_count / func_security_clone_only
|
|
831
|
+
# so the codegen actually clones this function's body too (the
|
|
832
|
+
# has_ta/has_series emission gate ORs in func_security_clone_only
|
|
833
|
+
# — see codegen/base.py) and the existing top-level call-site
|
|
834
|
+
# naming (which keys purely off func_call_cs_map, not
|
|
835
|
+
# has_ta/has_series) picks the right ``_cs{N}`` variant.
|
|
836
|
+
already_tracked = self._func_call_site_count.get(containing, 0) > 0
|
|
837
|
+
per_cs: list[tuple[int, str | None]] = []
|
|
838
|
+
for i, call in enumerate(calls):
|
|
839
|
+
if already_tracked:
|
|
840
|
+
cs_info = self._func_call_cs_map.get(id(call))
|
|
841
|
+
if cs_info is None or cs_info[0] != containing:
|
|
842
|
+
continue # shouldn't happen: has_ta/has_series tracks ALL call sites
|
|
843
|
+
cs_idx = cs_info[1]
|
|
844
|
+
else:
|
|
845
|
+
cs_idx = i
|
|
846
|
+
self._func_call_cs_map.setdefault(id(call), (containing, cs_idx))
|
|
847
|
+
arg = call.args[pidx] if pidx < len(call.args) else None
|
|
848
|
+
lit = self._callsite_tf_literal_value(arg)
|
|
849
|
+
per_cs.append((cs_idx, lit))
|
|
850
|
+
if not per_cs:
|
|
851
|
+
new_calls.append(sec) # dead code — evaluator result never read
|
|
852
|
+
continue
|
|
853
|
+
distinct_literals = {lit for _, lit in per_cs if lit is not None}
|
|
854
|
+
if len(distinct_literals) < 2:
|
|
855
|
+
new_calls.append(sec) # single TF (or unresolved) — no cloning needed
|
|
856
|
+
continue
|
|
857
|
+
if any(lit is None for _, lit in per_cs):
|
|
858
|
+
# Some call site's tf isn't a compile-time literal — can't
|
|
859
|
+
# pin every clone to a concrete timeframe. Keep the original
|
|
860
|
+
# deterministic rejection rather than guess.
|
|
861
|
+
self._error(
|
|
862
|
+
"request.security timeframe parameter '"
|
|
863
|
+
+ param_name
|
|
864
|
+
+ "' of function '"
|
|
865
|
+
+ containing
|
|
866
|
+
+ "' is called with multiple distinct literal timeframes ("
|
|
867
|
+
+ ", ".join(sorted(distinct_literals))
|
|
868
|
+
+ "). A single request.security evaluator cannot serve "
|
|
869
|
+
"them all and would silently collapse onto the chart "
|
|
870
|
+
"timeframe. Pass a single timeframe, or inline a separate "
|
|
871
|
+
"request.security call at each call site.",
|
|
872
|
+
tf_node.loc,
|
|
873
|
+
)
|
|
874
|
+
new_calls.append(sec)
|
|
875
|
+
continue
|
|
876
|
+
if not already_tracked:
|
|
877
|
+
# First thing establishing per-call-site identity for this
|
|
878
|
+
# function — make it authoritative so the codegen's
|
|
879
|
+
# function-emission gate (has_ta or has_series or
|
|
880
|
+
# func_security_clone_only) actually clones its body, with
|
|
881
|
+
# self._active_call_site_idx set to each of our cs_idx values
|
|
882
|
+
# in turn while it does.
|
|
883
|
+
self._func_call_site_count[containing] = len(calls)
|
|
884
|
+
self._func_security_clone_only.add(containing)
|
|
885
|
+
# Clone: one SecurityCallInfo per call site, each pinned to that
|
|
886
|
+
# site's literal timeframe via a synthetic StringLiteral (so the
|
|
887
|
+
# existing literal-timeframe resolution path needs no changes)
|
|
888
|
+
# and given a fresh, currently-unused sec_id.
|
|
889
|
+
next_sec_id = max((s.sec_id for s in sec_calls), default=-1) + 1
|
|
890
|
+
next_sec_id = max(next_sec_id, len(sec_calls) + len(new_calls))
|
|
891
|
+
for cs_idx, lit in sorted(per_cs):
|
|
892
|
+
clone = SecurityCallInfo(
|
|
893
|
+
sec_id=next_sec_id,
|
|
894
|
+
timeframe=StringLiteral(value=lit, loc=tf_node.loc),
|
|
895
|
+
expression=sec.expression,
|
|
896
|
+
returns_tuple=sec.returns_tuple,
|
|
897
|
+
tuple_size=sec.tuple_size,
|
|
898
|
+
gaps=sec.gaps,
|
|
899
|
+
lookahead=sec.lookahead,
|
|
900
|
+
ta_range=sec.ta_range,
|
|
901
|
+
depends_on_mutable_globals=sec.depends_on_mutable_globals,
|
|
902
|
+
mutable_globals=sec.mutable_globals,
|
|
903
|
+
is_lower_tf_array=sec.is_lower_tf_array,
|
|
904
|
+
containing_func=sec.containing_func,
|
|
905
|
+
callsite_idx=cs_idx,
|
|
906
|
+
)
|
|
907
|
+
new_calls.append(clone)
|
|
908
|
+
next_sec_id += 1
|
|
909
|
+
cloned_any = True
|
|
910
|
+
|
|
911
|
+
if cloned_any:
|
|
912
|
+
# A freshly-backfilled func_call_site_count[containing] may need
|
|
913
|
+
# to cascade to a sub-function containing's body calls (the same
|
|
914
|
+
# propagation _propagate_call_site_counts() already did once
|
|
915
|
+
# before this method ran) — re-run it now that backfill exists.
|
|
916
|
+
self._propagate_call_site_counts()
|
|
917
|
+
# Renumber sec_id contiguously 0..N-1 in final list order — the
|
|
918
|
+
# codegen indexes _security_eval_info / register_security_eval /
|
|
919
|
+
# _eval_security_N by sec_id, all assumed dense from registration
|
|
920
|
+
# order. The provisional IDs assigned above only needed to be
|
|
921
|
+
# distinct from each other while building new_calls; this final
|
|
922
|
+
# pass is what callers (e.g. the codegen's expr_node identity
|
|
923
|
+
# lookup, which now also keys off callsite_idx) actually see.
|
|
924
|
+
for i, sec in enumerate(new_calls):
|
|
925
|
+
sec.sec_id = i
|
|
926
|
+
self._security_calls = new_calls
|
|
927
|
+
|
|
928
|
+
def _iter_user_func_calls(self, func_name: str):
|
|
929
|
+
"""Yield every ``func_name(...)`` call anywhere in the AST (top-level
|
|
930
|
+
and nested inside function bodies)."""
|
|
931
|
+
def _walk(node):
|
|
932
|
+
if node is None:
|
|
933
|
+
return
|
|
934
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
935
|
+
and node.callee.name == func_name):
|
|
936
|
+
yield node
|
|
937
|
+
for attr_val in vars(node).values():
|
|
938
|
+
if isinstance(attr_val, list):
|
|
939
|
+
for item in attr_val:
|
|
940
|
+
if hasattr(item, "__dict__"):
|
|
941
|
+
yield from _walk(item)
|
|
942
|
+
elif attr_val is not None and hasattr(attr_val, "__dict__"):
|
|
943
|
+
yield from _walk(attr_val)
|
|
944
|
+
yield from _walk(self._ast)
|
|
945
|
+
|
|
946
|
+
def _callsite_tf_literal_value(self, arg) -> str | None:
|
|
947
|
+
"""Resolve a UDF call-site timeframe argument to a literal string
|
|
948
|
+
value when it is statically known: a string literal, or a known
|
|
949
|
+
constant / input-backed variable whose stored value is a string.
|
|
950
|
+
Returns None for anything that is not a compile-time string."""
|
|
951
|
+
if isinstance(arg, StringLiteral):
|
|
952
|
+
return arg.value
|
|
953
|
+
if isinstance(arg, Identifier):
|
|
954
|
+
sym = self._symbols.resolve(arg.name)
|
|
955
|
+
if sym is not None and getattr(sym, "const_value", None) is not None:
|
|
956
|
+
val = sym.const_value
|
|
957
|
+
if isinstance(val, str):
|
|
958
|
+
return val
|
|
959
|
+
return None
|
|
960
|
+
|
|
421
961
|
def _is_static_expression(self, node: ASTNode | None) -> bool:
|
|
422
962
|
if node is None:
|
|
423
963
|
return True
|
|
@@ -535,19 +1075,105 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
535
1075
|
# ------------------------------------------------------------------
|
|
536
1076
|
|
|
537
1077
|
def _udt_name_from_ctor(self, value: ASTNode) -> str | None:
|
|
538
|
-
"""If value is ``TypeName.new(...)`` for a user-defined type
|
|
1078
|
+
"""If value is ``TypeName.new(...)`` for a user-defined type OR a
|
|
1079
|
+
drawing handle (``label.new``/``line.new``/``box.new``/``linefill.new``),
|
|
1080
|
+
return the type name."""
|
|
539
1081
|
if not isinstance(value, FuncCall):
|
|
540
1082
|
return None
|
|
541
1083
|
cal = value.callee
|
|
542
1084
|
if not isinstance(cal, MemberAccess) or not isinstance(cal.object, Identifier):
|
|
543
1085
|
return None
|
|
544
1086
|
owner = cal.object.name
|
|
545
|
-
if owner not in self._udt_fields:
|
|
546
|
-
return None
|
|
547
1087
|
m = cal.member
|
|
548
|
-
if m == "new" or (isinstance(m, str) and m.startswith("new")):
|
|
1088
|
+
if not (m == "new" or (isinstance(m, str) and m.startswith("new"))):
|
|
1089
|
+
return None
|
|
1090
|
+
# Drawing-objects-as-data: label.new(...)/line.new(...)/... return a
|
|
1091
|
+
# handle of the self-type. These are not in _udt_fields (they are not
|
|
1092
|
+
# user UDTs) but must still be recognised so a function whose body ends
|
|
1093
|
+
# in label.new(...) emits a ``Label`` (not ``double``) return type.
|
|
1094
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
1095
|
+
if owner in _DRAWING_TYPE_NAMES:
|
|
549
1096
|
return owner
|
|
550
|
-
|
|
1097
|
+
if owner not in self._udt_fields:
|
|
1098
|
+
return None
|
|
1099
|
+
return owner
|
|
1100
|
+
|
|
1101
|
+
def _func_terminal_drawing_type(self, func_node: FuncDef) -> str | None:
|
|
1102
|
+
"""Resolve the drawing-handle / UDT type of a function's terminal
|
|
1103
|
+
(return) expression for cases the direct ``_udt_name_from_ctor`` on the
|
|
1104
|
+
last statement misses:
|
|
1105
|
+
|
|
1106
|
+
- the last statement is an ``IfStmt`` whose terminal branch yields a
|
|
1107
|
+
drawing/UDT constructor (``makeEventLabel`` => ``if cond\\n
|
|
1108
|
+
label.new(...)``); and
|
|
1109
|
+
- the last statement is a bare ``Identifier`` bound to a
|
|
1110
|
+
drawing-handle local (``setTradeLine`` => ``line result = ...`` then
|
|
1111
|
+
a trailing ``result``).
|
|
1112
|
+
|
|
1113
|
+
Returns the drawing/UDT type name, or ``None``. Without this a function
|
|
1114
|
+
that returns a ``line``/``label`` handle this way is mis-typed
|
|
1115
|
+
``double`` and clang rejects ``no viable conversion from Line to
|
|
1116
|
+
double``.
|
|
1117
|
+
"""
|
|
1118
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
1119
|
+
|
|
1120
|
+
body = func_node.body
|
|
1121
|
+
if not body:
|
|
1122
|
+
return None
|
|
1123
|
+
|
|
1124
|
+
# Map a drawing-handle local var name -> drawing type. Seeded from
|
|
1125
|
+
# declared drawing type hints (``line result``) and the function's own
|
|
1126
|
+
# drawing-typed parameters, plus any local first bound to a drawing
|
|
1127
|
+
# ``<ns>.new(...)`` constructor.
|
|
1128
|
+
local_drawing: dict[str, str] = {}
|
|
1129
|
+
param_hints = (func_node.annotations or {}).get("param_type_hints", [])
|
|
1130
|
+
for i, p in enumerate(func_node.params):
|
|
1131
|
+
hint = param_hints[i] if i < len(param_hints) else None
|
|
1132
|
+
if hint in _DRAWING_TYPE_NAMES:
|
|
1133
|
+
local_drawing[p] = hint
|
|
1134
|
+
|
|
1135
|
+
def _scan(stmts):
|
|
1136
|
+
for st in stmts:
|
|
1137
|
+
if isinstance(st, VarDecl):
|
|
1138
|
+
if st.type_hint in _DRAWING_TYPE_NAMES:
|
|
1139
|
+
local_drawing[st.name] = st.type_hint
|
|
1140
|
+
else:
|
|
1141
|
+
dt = self._udt_name_from_ctor(st.value)
|
|
1142
|
+
if dt in _DRAWING_TYPE_NAMES:
|
|
1143
|
+
local_drawing.setdefault(st.name, dt)
|
|
1144
|
+
elif isinstance(st, Assignment) and isinstance(st.target, Identifier):
|
|
1145
|
+
dt = self._udt_name_from_ctor(st.value)
|
|
1146
|
+
if dt in _DRAWING_TYPE_NAMES:
|
|
1147
|
+
local_drawing.setdefault(st.target.name, dt)
|
|
1148
|
+
elif isinstance(st, IfStmt):
|
|
1149
|
+
_scan(st.body)
|
|
1150
|
+
_scan(st.else_body)
|
|
1151
|
+
|
|
1152
|
+
_scan(body)
|
|
1153
|
+
|
|
1154
|
+
def _resolve_terminal(stmt):
|
|
1155
|
+
# An if used as the function's return expression: the value is the
|
|
1156
|
+
# terminal of the executed branch — recurse into the body's (then
|
|
1157
|
+
# else's) terminal statement.
|
|
1158
|
+
if isinstance(stmt, IfStmt):
|
|
1159
|
+
for branch in (stmt.body, stmt.else_body):
|
|
1160
|
+
if branch:
|
|
1161
|
+
t = _resolve_terminal(branch[-1])
|
|
1162
|
+
if t is not None:
|
|
1163
|
+
return t
|
|
1164
|
+
return None
|
|
1165
|
+
expr = None
|
|
1166
|
+
if isinstance(stmt, ExprStmt):
|
|
1167
|
+
expr = stmt.expr
|
|
1168
|
+
elif not isinstance(stmt, TupleLiteral) and hasattr(stmt, "loc"):
|
|
1169
|
+
expr = stmt
|
|
1170
|
+
if expr is None:
|
|
1171
|
+
return None
|
|
1172
|
+
if isinstance(expr, Identifier) and expr.name in local_drawing:
|
|
1173
|
+
return local_drawing[expr.name]
|
|
1174
|
+
return self._udt_name_from_ctor(expr)
|
|
1175
|
+
|
|
1176
|
+
return _resolve_terminal(body[-1])
|
|
551
1177
|
|
|
552
1178
|
def _visit_VarDecl(self, node: VarDecl) -> PineType:
|
|
553
1179
|
# Infer type from the value expression
|
|
@@ -620,14 +1246,41 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
620
1246
|
# Track var members
|
|
621
1247
|
if node.is_var or node.is_varip:
|
|
622
1248
|
init_str = self._expr_to_str(node.value)
|
|
623
|
-
self.
|
|
1249
|
+
scope_name = self._symbols.current_scope.name
|
|
1250
|
+
# Block-scoped var name-collision disambiguation. A ``var``/``varip``
|
|
1251
|
+
# declared inside a non-global, non-function block (an ``if`` / ``for``
|
|
1252
|
+
# / ``while`` body at on_bar scope) is keyed by RAW name. Two sibling
|
|
1253
|
+
# blocks declaring the same name would dedupe to ONE C++ member and
|
|
1254
|
+
# cross-contaminate (proven: egoigor1976-1-trendline-strategy's
|
|
1255
|
+
# ``var bool valid`` in the upper- and lower-trendline ``if`` blocks).
|
|
1256
|
+
# When such a name already belongs to a DIFFERENT block, mint a
|
|
1257
|
+
# scope-unique member name and record the rename so codegen activates
|
|
1258
|
+
# it (via ``_active_var_remap``) while emitting that block.
|
|
1259
|
+
member_name = node.name
|
|
1260
|
+
is_block_scoped = (
|
|
1261
|
+
not self._global_scope
|
|
1262
|
+
and not scope_name.startswith("func_")
|
|
1263
|
+
and not scope_name.startswith("method_")
|
|
1264
|
+
and bool(self._block_node_stack)
|
|
1265
|
+
)
|
|
1266
|
+
if is_block_scoped:
|
|
1267
|
+
block_id = id(self._block_node_stack[-1])
|
|
1268
|
+
owner = self._block_var_owner.get(node.name)
|
|
1269
|
+
if owner is None:
|
|
1270
|
+
# First block to claim this name keeps the raw member name.
|
|
1271
|
+
self._block_var_owner[node.name] = block_id
|
|
1272
|
+
elif owner != block_id:
|
|
1273
|
+
# Sibling-scope collision: disambiguate this declaration.
|
|
1274
|
+
self._block_var_seq += 1
|
|
1275
|
+
member_name = f"{node.name}__blk{self._block_var_seq}"
|
|
1276
|
+
self._block_var_renames.setdefault(block_id, {})[node.name] = member_name
|
|
1277
|
+
self._var_members.append((member_name, val_type, init_str))
|
|
624
1278
|
# Capture the init AST too so codegen can inspect the RHS callee
|
|
625
1279
|
# (used to detect int64-returning builtins like ``time()`` and
|
|
626
1280
|
# promote the symbol storage type to ``int64_t``).
|
|
627
1281
|
if node.value is not None:
|
|
628
|
-
self._var_member_init_exprs[
|
|
1282
|
+
self._var_member_init_exprs[member_name] = node.value
|
|
629
1283
|
# Track function-scoped var members
|
|
630
|
-
scope_name = self._symbols.current_scope.name
|
|
631
1284
|
if scope_name.startswith("func_"):
|
|
632
1285
|
func_name = scope_name[5:] # strip "func_" prefix
|
|
633
1286
|
if func_name not in self._func_var_members:
|
|
@@ -732,6 +1385,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
732
1385
|
setattr(sym, "is_static_series", True)
|
|
733
1386
|
self._symbols.define(sym)
|
|
734
1387
|
|
|
1388
|
+
# Track global-scope tuple-assign targets (e.g.
|
|
1389
|
+
# ``[pdH, pdL] = request.security(...)``) as class members so user
|
|
1390
|
+
# functions / later references resolve — mirroring _visit_VarDecl.
|
|
1391
|
+
# Without this the names are never declared and the C++ errors with
|
|
1392
|
+
# "use of undeclared identifier".
|
|
1393
|
+
if (self._global_scope
|
|
1394
|
+
and self._symbols.current_scope.name == "global"
|
|
1395
|
+
and name not in self._series_vars):
|
|
1396
|
+
self._global_var_decls.append((name, PineType.FLOAT))
|
|
1397
|
+
self._global_expr_map[name] = node.value
|
|
1398
|
+
self._record_global_binding_stmt(
|
|
1399
|
+
name, PineType.FLOAT, False, decl_node=node,
|
|
1400
|
+
)
|
|
1401
|
+
|
|
735
1402
|
return val_type
|
|
736
1403
|
|
|
737
1404
|
# ------------------------------------------------------------------
|
|
@@ -745,18 +1412,28 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
745
1412
|
# Enter function scope
|
|
746
1413
|
self._symbols.enter_scope(f"func_{node.name}")
|
|
747
1414
|
|
|
748
|
-
# Define parameters
|
|
1415
|
+
# Define parameters. The type is UNKNOWN until inferred from a call
|
|
1416
|
+
# site, BUT a declared type hint (``string tf``, ``pivot hi``, ``line[] arr``)
|
|
1417
|
+
# is authoritative — record it as the symbol's ``type_spec`` / ``pine_type``
|
|
1418
|
+
# so (a) the param emits with the right C++ type and (b) callers passing
|
|
1419
|
+
# this param into another function can infer that function's param type
|
|
1420
|
+
# (e.g. ``getLineStyle(styleStr)`` where ``styleStr`` is a ``string`` param).
|
|
749
1421
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
750
|
-
|
|
1422
|
+
param_hints = (node.annotations or {}).get("param_type_hints", [])
|
|
1423
|
+
for i, param in enumerate(node.params):
|
|
1424
|
+
hint = param_hints[i] if i < len(param_hints) else None
|
|
1425
|
+
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
1426
|
+
ptype = self._type_hint_to_pine(hint) if hint else PineType.UNKNOWN
|
|
751
1427
|
sym = Symbol(
|
|
752
1428
|
name=param,
|
|
753
|
-
pine_type=
|
|
1429
|
+
pine_type=ptype,
|
|
754
1430
|
is_series=False,
|
|
755
1431
|
is_var=False,
|
|
756
1432
|
is_const=False,
|
|
757
1433
|
const_value=None,
|
|
758
1434
|
scope=f"func_{node.name}",
|
|
759
1435
|
loc=loc,
|
|
1436
|
+
type_spec=pspec,
|
|
760
1437
|
)
|
|
761
1438
|
self._symbols.define(sym)
|
|
762
1439
|
|
|
@@ -767,16 +1444,40 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
767
1444
|
body_type = PineType.VOID
|
|
768
1445
|
old_global = self._global_scope
|
|
769
1446
|
self._global_scope = False
|
|
1447
|
+
self._enclosing_func_params.append(set(node.params))
|
|
1448
|
+
self._enclosing_func_names.append(node.name)
|
|
1449
|
+
self._nested_ta_touched = set()
|
|
770
1450
|
try:
|
|
771
1451
|
for stmt in node.body:
|
|
772
1452
|
body_type = self._visit(stmt)
|
|
773
1453
|
finally:
|
|
774
1454
|
self._global_scope = old_global
|
|
775
|
-
|
|
776
|
-
|
|
1455
|
+
self._enclosing_func_params.pop()
|
|
1456
|
+
self._enclosing_func_names.pop()
|
|
1457
|
+
nested_touched = self._nested_ta_touched
|
|
1458
|
+
self._nested_ta_touched = None
|
|
1459
|
+
|
|
1460
|
+
# Record TA range for this function. Widen to cover any nested-callee TA
|
|
1461
|
+
# sites whose ctor args were rewritten in terms of THIS function's params
|
|
1462
|
+
# (e.g. f_basisMa's sites parameterized by f_bbwp's _bbwLen), so resolving
|
|
1463
|
+
# this function at its call site re-substitutes those nested sites too.
|
|
777
1464
|
ta_end = len(self._ta_call_sites)
|
|
778
|
-
|
|
779
|
-
|
|
1465
|
+
lo, hi = ta_start, ta_end
|
|
1466
|
+
if nested_touched:
|
|
1467
|
+
lo = min(lo, min(nested_touched))
|
|
1468
|
+
hi = max(hi, max(nested_touched) + 1)
|
|
1469
|
+
if hi > lo:
|
|
1470
|
+
self._func_ta_ranges[node.name] = (lo, hi)
|
|
1471
|
+
|
|
1472
|
+
inferred_param_specs = self._param_type_specs_from_def(node)
|
|
1473
|
+
for i, param in enumerate(node.params):
|
|
1474
|
+
if i < len(inferred_param_specs) and inferred_param_specs[i] is not None:
|
|
1475
|
+
continue
|
|
1476
|
+
sym = self._symbols.resolve(param)
|
|
1477
|
+
spec = getattr(sym, "type_spec", None) if sym is not None else None
|
|
1478
|
+
if spec is not None and i < len(inferred_param_specs):
|
|
1479
|
+
inferred_param_specs[i] = spec
|
|
1480
|
+
self._func_param_type_specs[node.name] = inferred_param_specs
|
|
780
1481
|
|
|
781
1482
|
self._symbols.exit_scope()
|
|
782
1483
|
|
|
@@ -807,8 +1508,23 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
807
1508
|
# last_stmt is itself an expression node (single-expr funcs)
|
|
808
1509
|
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
|
|
809
1510
|
udt_ret = self._udt_name_from_ctor(ret_expr) if ret_expr is not None else None
|
|
1511
|
+
if udt_ret is None:
|
|
1512
|
+
# Drawing-handle returns wrapped in an if-statement terminal
|
|
1513
|
+
# branch (``makeEventLabel``) or returned as a bare drawing-handle
|
|
1514
|
+
# local (``setTradeLine``) are not direct ctors on the last
|
|
1515
|
+
# expression — resolve them so the function emits the C++ handle
|
|
1516
|
+
# type (Line/Label/...) instead of the ``double`` default.
|
|
1517
|
+
udt_ret = self._func_terminal_drawing_type(node)
|
|
810
1518
|
if udt_ret is not None:
|
|
811
1519
|
self._func_udt_return_types[node.name] = udt_ret
|
|
1520
|
+
# Array-return inference: a function whose body ends in
|
|
1521
|
+
# ``array.from(...)`` / ``array.new<T>(...)`` / a UDT method
|
|
1522
|
+
# returning an array returns a ``std::vector<...>``. The coarse
|
|
1523
|
+
# PineType return can't represent this, so carry the TypeSpec.
|
|
1524
|
+
if ret_expr is not None:
|
|
1525
|
+
ret_spec = self._type_spec_from_expr(ret_expr)
|
|
1526
|
+
if ret_spec is not None and ret_spec.kind == "array":
|
|
1527
|
+
self._func_return_type_specs[node.name] = ret_spec
|
|
812
1528
|
|
|
813
1529
|
# Store return type
|
|
814
1530
|
self._func_return_types[node.name] = body_type
|
|
@@ -880,12 +1596,14 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
880
1596
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
881
1597
|
param_hints = (node.annotations or {}).get("param_type_hints", [])
|
|
882
1598
|
param_types: list[PineType] = []
|
|
1599
|
+
param_specs: list = []
|
|
883
1600
|
for i, p in enumerate(node.params):
|
|
884
1601
|
udt_self = node.type_name if i == 0 else None
|
|
885
1602
|
hint = param_hints[i] if i < len(param_hints) else None
|
|
886
1603
|
ptype = self._type_hint_to_pine(hint) if hint else PineType.FLOAT
|
|
887
1604
|
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
888
1605
|
param_types.append(ptype)
|
|
1606
|
+
param_specs.append(pspec)
|
|
889
1607
|
self._symbols.define(Symbol(
|
|
890
1608
|
name=p, pine_type=ptype, is_series=False,
|
|
891
1609
|
is_var=False, is_const=False, const_value=None,
|
|
@@ -939,6 +1657,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
939
1657
|
returns_tuple=returns_tuple,
|
|
940
1658
|
tuple_element_count=tuple_element_count,
|
|
941
1659
|
param_defaults=param_defaults,
|
|
1660
|
+
param_type_specs=param_specs,
|
|
942
1661
|
)
|
|
943
1662
|
self._func_infos.append(fi)
|
|
944
1663
|
return PineType.VOID
|
|
@@ -950,6 +1669,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
950
1669
|
def _visit_IfStmt(self, node: IfStmt) -> PineType:
|
|
951
1670
|
old_global = self._global_scope
|
|
952
1671
|
self._global_scope = False
|
|
1672
|
+
self._block_node_stack.append(node)
|
|
953
1673
|
try:
|
|
954
1674
|
self._visit(node.condition)
|
|
955
1675
|
body_type = PineType.VOID
|
|
@@ -958,6 +1678,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
958
1678
|
for stmt in node.else_body:
|
|
959
1679
|
self._visit(stmt)
|
|
960
1680
|
finally:
|
|
1681
|
+
self._block_node_stack.pop()
|
|
961
1682
|
self._global_scope = old_global
|
|
962
1683
|
# If used as expression (x = if ...), return last expr type
|
|
963
1684
|
return body_type
|
|
@@ -981,6 +1702,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
981
1702
|
|
|
982
1703
|
old_global = self._global_scope
|
|
983
1704
|
self._global_scope = False
|
|
1705
|
+
self._block_node_stack.append(node)
|
|
984
1706
|
try:
|
|
985
1707
|
self._visit(node.start)
|
|
986
1708
|
self._visit(node.end)
|
|
@@ -989,6 +1711,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
989
1711
|
for stmt in node.body:
|
|
990
1712
|
self._visit(stmt)
|
|
991
1713
|
finally:
|
|
1714
|
+
self._block_node_stack.pop()
|
|
992
1715
|
self._global_scope = old_global
|
|
993
1716
|
|
|
994
1717
|
self._symbols.exit_scope()
|
|
@@ -997,6 +1720,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
997
1720
|
def _visit_ForInStmt(self, node) -> PineType:
|
|
998
1721
|
old_global = self._global_scope
|
|
999
1722
|
self._global_scope = False
|
|
1723
|
+
self._block_node_stack.append(node)
|
|
1000
1724
|
try:
|
|
1001
1725
|
self._visit(node.iterable)
|
|
1002
1726
|
self._symbols.enter_scope("for_in")
|
|
@@ -1019,17 +1743,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1019
1743
|
self._visit(stmt)
|
|
1020
1744
|
self._symbols.exit_scope()
|
|
1021
1745
|
finally:
|
|
1746
|
+
self._block_node_stack.pop()
|
|
1022
1747
|
self._global_scope = old_global
|
|
1023
1748
|
return PineType.VOID
|
|
1024
1749
|
|
|
1025
1750
|
def _visit_WhileStmt(self, node: WhileStmt) -> PineType:
|
|
1026
1751
|
old_global = self._global_scope
|
|
1027
1752
|
self._global_scope = False
|
|
1753
|
+
self._block_node_stack.append(node)
|
|
1028
1754
|
try:
|
|
1029
1755
|
self._visit(node.condition)
|
|
1030
1756
|
for stmt in node.body:
|
|
1031
1757
|
self._visit(stmt)
|
|
1032
1758
|
finally:
|
|
1759
|
+
self._block_node_stack.pop()
|
|
1033
1760
|
self._global_scope = old_global
|
|
1034
1761
|
return PineType.VOID
|
|
1035
1762
|
|
|
@@ -1079,6 +1806,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1079
1806
|
|
|
1080
1807
|
# String concatenation: if either side is STRING, result is STRING
|
|
1081
1808
|
if left_type == PineType.STRING or right_type == PineType.STRING:
|
|
1809
|
+
def _mark_string_param(expr) -> None:
|
|
1810
|
+
if not isinstance(expr, Identifier):
|
|
1811
|
+
return
|
|
1812
|
+
sym = self._symbols.resolve(expr.name)
|
|
1813
|
+
if sym is None or not (sym.scope or "").startswith("func_"):
|
|
1814
|
+
return
|
|
1815
|
+
if sym.pine_type == PineType.UNKNOWN:
|
|
1816
|
+
sym.pine_type = PineType.STRING
|
|
1817
|
+
sym.type_spec = TypeSpec.primitive("string")
|
|
1818
|
+
|
|
1819
|
+
if left_type == PineType.STRING:
|
|
1820
|
+
_mark_string_param(node.right)
|
|
1821
|
+
if right_type == PineType.STRING:
|
|
1822
|
+
_mark_string_param(node.left)
|
|
1082
1823
|
return PineType.STRING
|
|
1083
1824
|
|
|
1084
1825
|
# Arithmetic: promote to FLOAT if either side is FLOAT
|
|
@@ -1151,6 +1892,15 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1151
1892
|
if isinstance(obj, Identifier) and obj.name == "str":
|
|
1152
1893
|
for arg in node.args:
|
|
1153
1894
|
self._visit(arg)
|
|
1895
|
+
# Most str.* return a string, but predicates and index helpers
|
|
1896
|
+
# are scalar. Keep this aligned with signatures.py and the C++
|
|
1897
|
+
# emitter's _infer_type path.
|
|
1898
|
+
if member in ("contains", "startswith", "endswith"):
|
|
1899
|
+
return PineType.BOOL
|
|
1900
|
+
if member == "tonumber":
|
|
1901
|
+
return PineType.FLOAT
|
|
1902
|
+
if member in ("length", "pos"):
|
|
1903
|
+
return PineType.INT
|
|
1154
1904
|
return PineType.STRING
|
|
1155
1905
|
|
|
1156
1906
|
# request.* calls
|
|
@@ -1374,9 +2124,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1374
2124
|
|
|
1375
2125
|
# syminfo.*
|
|
1376
2126
|
if ns == "syminfo":
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
2127
|
+
from .. import signatures as _pf_sigs
|
|
2128
|
+
return _pf_sigs.SYMINFO_VARIABLES.get(
|
|
2129
|
+
f"syminfo.{node.member}",
|
|
2130
|
+
PineType.STRING,
|
|
2131
|
+
)
|
|
1380
2132
|
|
|
1381
2133
|
# color.* constants
|
|
1382
2134
|
if ns == "color":
|
|
@@ -1435,7 +2187,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1435
2187
|
|
|
1436
2188
|
# text.* constants (align_left, align_right, etc.)
|
|
1437
2189
|
if ns == "text":
|
|
1438
|
-
return PineType.
|
|
2190
|
+
return PineType.STRING
|
|
1439
2191
|
|
|
1440
2192
|
# extend.* constants (left, right, both, none)
|
|
1441
2193
|
if ns == "extend":
|