pineforge-codegen 0.7.6__py3-none-any.whl → 0.8.1__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.
Potentially problematic release.
This version of pineforge-codegen might be problematic. Click here for more details.
- pineforge_codegen/analyzer/base.py +453 -15
- pineforge_codegen/analyzer/call_handlers.py +200 -11
- pineforge_codegen/analyzer/contracts.py +62 -0
- pineforge_codegen/analyzer/types.py +53 -0
- pineforge_codegen/codegen/__init__.py +4 -0
- pineforge_codegen/codegen/base.py +532 -51
- pineforge_codegen/codegen/drawing.py +560 -0
- pineforge_codegen/codegen/emit_top.py +331 -30
- pineforge_codegen/codegen/helpers.py +26 -1
- pineforge_codegen/codegen/input.py +39 -2
- pineforge_codegen/codegen/security.py +321 -9
- pineforge_codegen/codegen/ta.py +4 -1
- pineforge_codegen/codegen/tables.py +63 -7
- pineforge_codegen/codegen/types.py +383 -21
- pineforge_codegen/codegen/visit_call.py +159 -22
- pineforge_codegen/codegen/visit_expr.py +91 -3
- pineforge_codegen/codegen/visit_stmt.py +188 -26
- pineforge_codegen/lexer.py +68 -3
- pineforge_codegen/parser.py +106 -49
- pineforge_codegen/support_checker.py +361 -37
- {pineforge_codegen-0.7.6.dist-info → pineforge_codegen-0.8.1.dist-info}/METADATA +1 -1
- pineforge_codegen-0.8.1.dist-info/RECORD +36 -0
- pineforge_codegen-0.7.6.dist-info/RECORD +0 -35
- {pineforge_codegen-0.7.6.dist-info → pineforge_codegen-0.8.1.dist-info}/WHEEL +0 -0
- {pineforge_codegen-0.7.6.dist-info → pineforge_codegen-0.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -127,6 +127,20 @@ 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
|
|
132
146
|
# Track user-defined function nodes for deferred analysis
|
|
@@ -141,6 +155,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
141
155
|
# expression (``=> Sample.new(...)`` or last stmt ``Sample.new(...)``).
|
|
142
156
|
# Probe: data/validation/udt-method-probe-20-udt-return-from-func.
|
|
143
157
|
self._func_udt_return_types: dict[str, str] = {}
|
|
158
|
+
self._func_return_type_specs: dict[str, "TypeSpec"] = {}
|
|
144
159
|
# Per-function var_members and series_vars (for call-site cloning)
|
|
145
160
|
self._func_var_members: dict[str, list] = {} # func_name -> [(name, PineType, init_str)]
|
|
146
161
|
self._func_series_vars: dict[str, set] = {} # func_name -> set[str]
|
|
@@ -148,6 +163,23 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
148
163
|
self._func_ta_ranges: dict[str, tuple[int, int]] = {} # func_name -> (start, end) indices
|
|
149
164
|
self._func_call_site_count: dict[str, int] = {} # func_name -> count
|
|
150
165
|
self._func_call_cs_map: dict[int, tuple[str, int]] = {} # call_node_id -> (func_name, cs_idx)
|
|
166
|
+
# Functions whose ONLY reason for needing per-call-site body cloning
|
|
167
|
+
# is a security-tf-monomorphized request.security (no TA/series state
|
|
168
|
+
# of their own — see _check_mixed_callsite_security_tf).
|
|
169
|
+
self._func_security_clone_only: set[str] = set()
|
|
170
|
+
# Authoritative clone-name map: (func_name, cs_idx) -> {orig_member_name:
|
|
171
|
+
# cloned_member_name}. The codegen rebuilds its TA remap from the
|
|
172
|
+
# ``{orig}_cs{cs_idx}`` formula by default, but a TA site reached through
|
|
173
|
+
# MULTIPLE enclosing functions (e.g. a helper cloned both directly and via
|
|
174
|
+
# a range-widened outer function) can collide on that formula. When the
|
|
175
|
+
# analyzer must disambiguate a clone's member name, it records the actual
|
|
176
|
+
# chosen name here so the codegen consumes it verbatim instead of
|
|
177
|
+
# re-deriving a colliding name. Empty for the common (no-collision) case,
|
|
178
|
+
# keeping generated output byte-identical.
|
|
179
|
+
self._func_cs_ta_clone_names: dict[tuple[str, int], dict[str, str]] = {}
|
|
180
|
+
# All TA member names minted so far (base + clones), for O(1) collision
|
|
181
|
+
# detection when minting a new clone.
|
|
182
|
+
self._ta_member_names: set[str] = set()
|
|
151
183
|
# UDT field definitions: type_name -> {field_name: PineType}
|
|
152
184
|
self._udt_fields: dict[str, dict[str, PineType]] = {}
|
|
153
185
|
# var_name -> UDT type for variables holding UDT instances
|
|
@@ -163,6 +195,14 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
163
195
|
self._current_top_level_stmt: ASTNode | None = None
|
|
164
196
|
self._global_scope = True
|
|
165
197
|
self._static_vars: set[str] = set()
|
|
198
|
+
# Stack of enclosing user-function param-name sets, pushed while visiting
|
|
199
|
+
# a FuncDef body. Lets a nested user-func call detect when it substitutes
|
|
200
|
+
# a TA ctor length with one of the OUTER function's params, so the outer
|
|
201
|
+
# call site can re-substitute (e.g. f_bbwp(_bbwLen) -> f_basisMa(_len)).
|
|
202
|
+
self._enclosing_func_params: list[set[str]] = []
|
|
203
|
+
# Set of TA-site indices a nested user-func call rewrote in terms of the
|
|
204
|
+
# current enclosing function's params (None when not inside a FuncDef body).
|
|
205
|
+
self._nested_ta_touched: set | None = None
|
|
166
206
|
|
|
167
207
|
# Pre-populate builtins
|
|
168
208
|
self._populate_builtins()
|
|
@@ -232,6 +272,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
232
272
|
# can call g_csK with isolated state.
|
|
233
273
|
self._propagate_call_site_counts()
|
|
234
274
|
|
|
275
|
+
# Reject (loudly) a request.security whose timeframe is a UDF parameter
|
|
276
|
+
# called with multiple distinct literal timeframes — a single evaluator
|
|
277
|
+
# cannot serve them and per-callsite specialization is not yet wired.
|
|
278
|
+
self._check_mixed_callsite_security_tf()
|
|
279
|
+
|
|
235
280
|
# Keep only truly pure global expressions for request.security rebinding.
|
|
236
281
|
# Globals later reassigned with := become series/stateful variables and
|
|
237
282
|
# must not be rebound to their declaration-time initializer.
|
|
@@ -266,6 +311,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
266
311
|
func_ta_ranges=self._func_ta_ranges,
|
|
267
312
|
func_call_cs_map=self._func_call_cs_map,
|
|
268
313
|
func_call_site_counts=self._func_call_site_count,
|
|
314
|
+
func_security_clone_only=self._func_security_clone_only,
|
|
315
|
+
func_cs_ta_clone_names=self._func_cs_ta_clone_names,
|
|
269
316
|
udt_defs=self._udt_fields,
|
|
270
317
|
enum_defs=self._enum_defs,
|
|
271
318
|
enum_member_strings=self._enum_member_strings,
|
|
@@ -273,9 +320,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
273
320
|
global_mutable_infos=mutable_global_infos,
|
|
274
321
|
func_var_members=self._func_var_members,
|
|
275
322
|
func_series_vars=self._func_series_vars,
|
|
323
|
+
func_return_type_specs=dict(self._func_return_type_specs),
|
|
276
324
|
udt_var_types=dict(self._udt_var_types),
|
|
277
325
|
collection_types=dict(self._collection_types),
|
|
278
326
|
udt_field_type_specs=dict(self._udt_field_type_specs),
|
|
327
|
+
block_var_renames=dict(self._block_var_renames),
|
|
279
328
|
)
|
|
280
329
|
|
|
281
330
|
def _record_global_binding_stmt(self, name: str, pine_type: PineType,
|
|
@@ -299,6 +348,18 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
299
348
|
if top_stmt is not None and (not info.source_stmts or info.source_stmts[-1] is not top_stmt):
|
|
300
349
|
info.source_stmts.append(top_stmt)
|
|
301
350
|
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _is_input_func_call(node: FuncCall) -> bool:
|
|
353
|
+
"""True for an ``input(...)`` or ``input.<member>(...)`` call."""
|
|
354
|
+
callee = node.callee
|
|
355
|
+
if isinstance(callee, Identifier) and callee.name == "input":
|
|
356
|
+
return True
|
|
357
|
+
return (
|
|
358
|
+
isinstance(callee, MemberAccess)
|
|
359
|
+
and isinstance(callee.object, Identifier)
|
|
360
|
+
and callee.object.name == "input"
|
|
361
|
+
)
|
|
362
|
+
|
|
302
363
|
def _collect_security_mutable_globals(
|
|
303
364
|
self, node: ASTNode | None, resolving: set[str] | None = None
|
|
304
365
|
) -> set[str]:
|
|
@@ -344,6 +405,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
344
405
|
resolving.remove(call_key)
|
|
345
406
|
return out
|
|
346
407
|
|
|
408
|
+
if isinstance(node, FuncCall) and self._is_input_func_call(node):
|
|
409
|
+
# An ``input.*()`` / ``input()`` initializer is a compile-time
|
|
410
|
+
# constant. Only its defval (first positional or ``defval=``) can
|
|
411
|
+
# carry a genuine data dependency; the cosmetic kwargs
|
|
412
|
+
# (group/tooltip/title/inline/display/confirm/minval/maxval/step)
|
|
413
|
+
# are presentation-only. Walking them would falsely pull a
|
|
414
|
+
# ``var string GROUP = "..."`` label into the security's
|
|
415
|
+
# mutable-globals set and trip the "TA ctor depends on rebound
|
|
416
|
+
# mutable globals" reject (parallax / higherTimeframeLength).
|
|
417
|
+
defval = node.args[0] if node.args else node.kwargs.get("defval")
|
|
418
|
+
if defval is not None:
|
|
419
|
+
out |= self._collect_security_mutable_globals(defval, resolving)
|
|
420
|
+
return out
|
|
421
|
+
|
|
347
422
|
def walk(value: Any) -> None:
|
|
348
423
|
nonlocal out
|
|
349
424
|
if value is None:
|
|
@@ -418,6 +493,188 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
418
493
|
self._func_call_site_count[sub] = count
|
|
419
494
|
changed = True
|
|
420
495
|
|
|
496
|
+
# ------------------------------------------------------------------
|
|
497
|
+
# Mixed-callsite UDF timeframe-param security rejection.
|
|
498
|
+
#
|
|
499
|
+
# A ``request.security`` whose ``timeframe`` is a parameter of its
|
|
500
|
+
# containing UDF maps to ONE evaluator regardless of how many times the
|
|
501
|
+
# UDF is called. When the UDF is called from >= 2 sites with DISTINCT
|
|
502
|
+
# literal timeframes, a single evaluator cannot faithfully serve them
|
|
503
|
+
# all and the resolver would silently collapse onto the chart timeframe
|
|
504
|
+
# (``input_tf_``). Per-callsite evaluator specialization (cloning the
|
|
505
|
+
# evaluator + UDF) is the correct fix but is not wired in this iteration,
|
|
506
|
+
# so we reject deterministically instead of emitting wrong semantics.
|
|
507
|
+
# ------------------------------------------------------------------
|
|
508
|
+
def _check_mixed_callsite_security_tf(self) -> None:
|
|
509
|
+
sec_calls = getattr(self, "_security_calls", None)
|
|
510
|
+
if not sec_calls:
|
|
511
|
+
return
|
|
512
|
+
# Build user-function definitions lookup once.
|
|
513
|
+
func_defs: dict[str, FuncDef] = {}
|
|
514
|
+
for stmt in self._ast.body:
|
|
515
|
+
if isinstance(stmt, FuncDef):
|
|
516
|
+
func_defs[stmt.name] = stmt
|
|
517
|
+
|
|
518
|
+
new_calls: list[SecurityCallInfo] = []
|
|
519
|
+
cloned_any = False
|
|
520
|
+
for sec in sec_calls:
|
|
521
|
+
containing = getattr(sec, "containing_func", "") or ""
|
|
522
|
+
tf_node = getattr(sec, "timeframe", None)
|
|
523
|
+
if not containing or not isinstance(tf_node, Identifier):
|
|
524
|
+
new_calls.append(sec)
|
|
525
|
+
continue
|
|
526
|
+
param_name = tf_node.name
|
|
527
|
+
fdef = func_defs.get(containing)
|
|
528
|
+
if fdef is None or param_name not in fdef.params:
|
|
529
|
+
new_calls.append(sec)
|
|
530
|
+
continue
|
|
531
|
+
pidx = fdef.params.index(param_name)
|
|
532
|
+
calls = list(self._iter_user_func_calls(containing))
|
|
533
|
+
if not calls:
|
|
534
|
+
new_calls.append(sec) # dead code — evaluator result never read
|
|
535
|
+
continue
|
|
536
|
+
# Number call sites for THIS function. If ``containing`` already
|
|
537
|
+
# has TA call sites or series/var members, the per-call-site
|
|
538
|
+
# UDF-body-cloning mechanism already numbered its call sites in
|
|
539
|
+
# _func_call_cs_map (a request.security nested ta.* registers as
|
|
540
|
+
# part of the function-body walk, same as a top-level ta.* call)
|
|
541
|
+
# — reuse that authoritative numbering so this clone's
|
|
542
|
+
# callsite_idx stays aligned with self._active_call_site_idx.
|
|
543
|
+
# Otherwise (e.g. ``f(tf) => request.security(sym, tf, close)``
|
|
544
|
+
# with no nested ta.* call) that mechanism never fired for this
|
|
545
|
+
# function at all, since it gates on has_ta/has_series; assign
|
|
546
|
+
# fresh cs_idx ourselves in call-site order and BACKFILL
|
|
547
|
+
# func_call_cs_map / func_call_site_count / func_security_clone_only
|
|
548
|
+
# so the codegen actually clones this function's body too (the
|
|
549
|
+
# has_ta/has_series emission gate ORs in func_security_clone_only
|
|
550
|
+
# — see codegen/base.py) and the existing top-level call-site
|
|
551
|
+
# naming (which keys purely off func_call_cs_map, not
|
|
552
|
+
# has_ta/has_series) picks the right ``_cs{N}`` variant.
|
|
553
|
+
already_tracked = self._func_call_site_count.get(containing, 0) > 0
|
|
554
|
+
per_cs: list[tuple[int, str | None]] = []
|
|
555
|
+
for i, call in enumerate(calls):
|
|
556
|
+
if already_tracked:
|
|
557
|
+
cs_info = self._func_call_cs_map.get(id(call))
|
|
558
|
+
if cs_info is None or cs_info[0] != containing:
|
|
559
|
+
continue # shouldn't happen: has_ta/has_series tracks ALL call sites
|
|
560
|
+
cs_idx = cs_info[1]
|
|
561
|
+
else:
|
|
562
|
+
cs_idx = i
|
|
563
|
+
self._func_call_cs_map.setdefault(id(call), (containing, cs_idx))
|
|
564
|
+
arg = call.args[pidx] if pidx < len(call.args) else None
|
|
565
|
+
lit = self._callsite_tf_literal_value(arg)
|
|
566
|
+
per_cs.append((cs_idx, lit))
|
|
567
|
+
if not per_cs:
|
|
568
|
+
new_calls.append(sec) # dead code — evaluator result never read
|
|
569
|
+
continue
|
|
570
|
+
distinct_literals = {lit for _, lit in per_cs if lit is not None}
|
|
571
|
+
if len(distinct_literals) < 2:
|
|
572
|
+
new_calls.append(sec) # single TF (or unresolved) — no cloning needed
|
|
573
|
+
continue
|
|
574
|
+
if any(lit is None for _, lit in per_cs):
|
|
575
|
+
# Some call site's tf isn't a compile-time literal — can't
|
|
576
|
+
# pin every clone to a concrete timeframe. Keep the original
|
|
577
|
+
# deterministic rejection rather than guess.
|
|
578
|
+
self._error(
|
|
579
|
+
"request.security timeframe parameter '"
|
|
580
|
+
+ param_name
|
|
581
|
+
+ "' of function '"
|
|
582
|
+
+ containing
|
|
583
|
+
+ "' is called with multiple distinct literal timeframes ("
|
|
584
|
+
+ ", ".join(sorted(distinct_literals))
|
|
585
|
+
+ "). A single request.security evaluator cannot serve "
|
|
586
|
+
"them all and would silently collapse onto the chart "
|
|
587
|
+
"timeframe. Pass a single timeframe, or inline a separate "
|
|
588
|
+
"request.security call at each call site.",
|
|
589
|
+
tf_node.loc,
|
|
590
|
+
)
|
|
591
|
+
new_calls.append(sec)
|
|
592
|
+
continue
|
|
593
|
+
if not already_tracked:
|
|
594
|
+
# First thing establishing per-call-site identity for this
|
|
595
|
+
# function — make it authoritative so the codegen's
|
|
596
|
+
# function-emission gate (has_ta or has_series or
|
|
597
|
+
# func_security_clone_only) actually clones its body, with
|
|
598
|
+
# self._active_call_site_idx set to each of our cs_idx values
|
|
599
|
+
# in turn while it does.
|
|
600
|
+
self._func_call_site_count[containing] = len(calls)
|
|
601
|
+
self._func_security_clone_only.add(containing)
|
|
602
|
+
# Clone: one SecurityCallInfo per call site, each pinned to that
|
|
603
|
+
# site's literal timeframe via a synthetic StringLiteral (so the
|
|
604
|
+
# existing literal-timeframe resolution path needs no changes)
|
|
605
|
+
# and given a fresh, currently-unused sec_id.
|
|
606
|
+
next_sec_id = max((s.sec_id for s in sec_calls), default=-1) + 1
|
|
607
|
+
next_sec_id = max(next_sec_id, len(sec_calls) + len(new_calls))
|
|
608
|
+
for cs_idx, lit in sorted(per_cs):
|
|
609
|
+
clone = SecurityCallInfo(
|
|
610
|
+
sec_id=next_sec_id,
|
|
611
|
+
timeframe=StringLiteral(value=lit, loc=tf_node.loc),
|
|
612
|
+
expression=sec.expression,
|
|
613
|
+
returns_tuple=sec.returns_tuple,
|
|
614
|
+
tuple_size=sec.tuple_size,
|
|
615
|
+
gaps=sec.gaps,
|
|
616
|
+
lookahead=sec.lookahead,
|
|
617
|
+
ta_range=sec.ta_range,
|
|
618
|
+
depends_on_mutable_globals=sec.depends_on_mutable_globals,
|
|
619
|
+
mutable_globals=sec.mutable_globals,
|
|
620
|
+
is_lower_tf_array=sec.is_lower_tf_array,
|
|
621
|
+
containing_func=sec.containing_func,
|
|
622
|
+
callsite_idx=cs_idx,
|
|
623
|
+
)
|
|
624
|
+
new_calls.append(clone)
|
|
625
|
+
next_sec_id += 1
|
|
626
|
+
cloned_any = True
|
|
627
|
+
|
|
628
|
+
if cloned_any:
|
|
629
|
+
# A freshly-backfilled func_call_site_count[containing] may need
|
|
630
|
+
# to cascade to a sub-function containing's body calls (the same
|
|
631
|
+
# propagation _propagate_call_site_counts() already did once
|
|
632
|
+
# before this method ran) — re-run it now that backfill exists.
|
|
633
|
+
self._propagate_call_site_counts()
|
|
634
|
+
# Renumber sec_id contiguously 0..N-1 in final list order — the
|
|
635
|
+
# codegen indexes _security_eval_info / register_security_eval /
|
|
636
|
+
# _eval_security_N by sec_id, all assumed dense from registration
|
|
637
|
+
# order. The provisional IDs assigned above only needed to be
|
|
638
|
+
# distinct from each other while building new_calls; this final
|
|
639
|
+
# pass is what callers (e.g. the codegen's expr_node identity
|
|
640
|
+
# lookup, which now also keys off callsite_idx) actually see.
|
|
641
|
+
for i, sec in enumerate(new_calls):
|
|
642
|
+
sec.sec_id = i
|
|
643
|
+
self._security_calls = new_calls
|
|
644
|
+
|
|
645
|
+
def _iter_user_func_calls(self, func_name: str):
|
|
646
|
+
"""Yield every ``func_name(...)`` call anywhere in the AST (top-level
|
|
647
|
+
and nested inside function bodies)."""
|
|
648
|
+
def _walk(node):
|
|
649
|
+
if node is None:
|
|
650
|
+
return
|
|
651
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
652
|
+
and node.callee.name == func_name):
|
|
653
|
+
yield node
|
|
654
|
+
for attr_val in vars(node).values():
|
|
655
|
+
if isinstance(attr_val, list):
|
|
656
|
+
for item in attr_val:
|
|
657
|
+
if hasattr(item, "__dict__"):
|
|
658
|
+
yield from _walk(item)
|
|
659
|
+
elif attr_val is not None and hasattr(attr_val, "__dict__"):
|
|
660
|
+
yield from _walk(attr_val)
|
|
661
|
+
yield from _walk(self._ast)
|
|
662
|
+
|
|
663
|
+
def _callsite_tf_literal_value(self, arg) -> str | None:
|
|
664
|
+
"""Resolve a UDF call-site timeframe argument to a literal string
|
|
665
|
+
value when it is statically known: a string literal, or a known
|
|
666
|
+
constant / input-backed variable whose stored value is a string.
|
|
667
|
+
Returns None for anything that is not a compile-time string."""
|
|
668
|
+
if isinstance(arg, StringLiteral):
|
|
669
|
+
return arg.value
|
|
670
|
+
if isinstance(arg, Identifier):
|
|
671
|
+
sym = self._symbols.resolve(arg.name)
|
|
672
|
+
if sym is not None and getattr(sym, "const_value", None) is not None:
|
|
673
|
+
val = sym.const_value
|
|
674
|
+
if isinstance(val, str):
|
|
675
|
+
return val
|
|
676
|
+
return None
|
|
677
|
+
|
|
421
678
|
def _is_static_expression(self, node: ASTNode | None) -> bool:
|
|
422
679
|
if node is None:
|
|
423
680
|
return True
|
|
@@ -535,19 +792,105 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
535
792
|
# ------------------------------------------------------------------
|
|
536
793
|
|
|
537
794
|
def _udt_name_from_ctor(self, value: ASTNode) -> str | None:
|
|
538
|
-
"""If value is ``TypeName.new(...)`` for a user-defined type
|
|
795
|
+
"""If value is ``TypeName.new(...)`` for a user-defined type OR a
|
|
796
|
+
drawing handle (``label.new``/``line.new``/``box.new``/``linefill.new``),
|
|
797
|
+
return the type name."""
|
|
539
798
|
if not isinstance(value, FuncCall):
|
|
540
799
|
return None
|
|
541
800
|
cal = value.callee
|
|
542
801
|
if not isinstance(cal, MemberAccess) or not isinstance(cal.object, Identifier):
|
|
543
802
|
return None
|
|
544
803
|
owner = cal.object.name
|
|
545
|
-
if owner not in self._udt_fields:
|
|
546
|
-
return None
|
|
547
804
|
m = cal.member
|
|
548
|
-
if m == "new" or (isinstance(m, str) and m.startswith("new")):
|
|
805
|
+
if not (m == "new" or (isinstance(m, str) and m.startswith("new"))):
|
|
806
|
+
return None
|
|
807
|
+
# Drawing-objects-as-data: label.new(...)/line.new(...)/... return a
|
|
808
|
+
# handle of the self-type. These are not in _udt_fields (they are not
|
|
809
|
+
# user UDTs) but must still be recognised so a function whose body ends
|
|
810
|
+
# in label.new(...) emits a ``Label`` (not ``double``) return type.
|
|
811
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
812
|
+
if owner in _DRAWING_TYPE_NAMES:
|
|
549
813
|
return owner
|
|
550
|
-
|
|
814
|
+
if owner not in self._udt_fields:
|
|
815
|
+
return None
|
|
816
|
+
return owner
|
|
817
|
+
|
|
818
|
+
def _func_terminal_drawing_type(self, func_node: FuncDef) -> str | None:
|
|
819
|
+
"""Resolve the drawing-handle / UDT type of a function's terminal
|
|
820
|
+
(return) expression for cases the direct ``_udt_name_from_ctor`` on the
|
|
821
|
+
last statement misses:
|
|
822
|
+
|
|
823
|
+
- the last statement is an ``IfStmt`` whose terminal branch yields a
|
|
824
|
+
drawing/UDT constructor (``makeEventLabel`` => ``if cond\\n
|
|
825
|
+
label.new(...)``); and
|
|
826
|
+
- the last statement is a bare ``Identifier`` bound to a
|
|
827
|
+
drawing-handle local (``setTradeLine`` => ``line result = ...`` then
|
|
828
|
+
a trailing ``result``).
|
|
829
|
+
|
|
830
|
+
Returns the drawing/UDT type name, or ``None``. Without this a function
|
|
831
|
+
that returns a ``line``/``label`` handle this way is mis-typed
|
|
832
|
+
``double`` and clang rejects ``no viable conversion from Line to
|
|
833
|
+
double``.
|
|
834
|
+
"""
|
|
835
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
836
|
+
|
|
837
|
+
body = func_node.body
|
|
838
|
+
if not body:
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
# Map a drawing-handle local var name -> drawing type. Seeded from
|
|
842
|
+
# declared drawing type hints (``line result``) and the function's own
|
|
843
|
+
# drawing-typed parameters, plus any local first bound to a drawing
|
|
844
|
+
# ``<ns>.new(...)`` constructor.
|
|
845
|
+
local_drawing: dict[str, str] = {}
|
|
846
|
+
param_hints = (func_node.annotations or {}).get("param_type_hints", [])
|
|
847
|
+
for i, p in enumerate(func_node.params):
|
|
848
|
+
hint = param_hints[i] if i < len(param_hints) else None
|
|
849
|
+
if hint in _DRAWING_TYPE_NAMES:
|
|
850
|
+
local_drawing[p] = hint
|
|
851
|
+
|
|
852
|
+
def _scan(stmts):
|
|
853
|
+
for st in stmts:
|
|
854
|
+
if isinstance(st, VarDecl):
|
|
855
|
+
if st.type_hint in _DRAWING_TYPE_NAMES:
|
|
856
|
+
local_drawing[st.name] = st.type_hint
|
|
857
|
+
else:
|
|
858
|
+
dt = self._udt_name_from_ctor(st.value)
|
|
859
|
+
if dt in _DRAWING_TYPE_NAMES:
|
|
860
|
+
local_drawing.setdefault(st.name, dt)
|
|
861
|
+
elif isinstance(st, Assignment) and isinstance(st.target, Identifier):
|
|
862
|
+
dt = self._udt_name_from_ctor(st.value)
|
|
863
|
+
if dt in _DRAWING_TYPE_NAMES:
|
|
864
|
+
local_drawing.setdefault(st.target.name, dt)
|
|
865
|
+
elif isinstance(st, IfStmt):
|
|
866
|
+
_scan(st.body)
|
|
867
|
+
_scan(st.else_body)
|
|
868
|
+
|
|
869
|
+
_scan(body)
|
|
870
|
+
|
|
871
|
+
def _resolve_terminal(stmt):
|
|
872
|
+
# An if used as the function's return expression: the value is the
|
|
873
|
+
# terminal of the executed branch — recurse into the body's (then
|
|
874
|
+
# else's) terminal statement.
|
|
875
|
+
if isinstance(stmt, IfStmt):
|
|
876
|
+
for branch in (stmt.body, stmt.else_body):
|
|
877
|
+
if branch:
|
|
878
|
+
t = _resolve_terminal(branch[-1])
|
|
879
|
+
if t is not None:
|
|
880
|
+
return t
|
|
881
|
+
return None
|
|
882
|
+
expr = None
|
|
883
|
+
if isinstance(stmt, ExprStmt):
|
|
884
|
+
expr = stmt.expr
|
|
885
|
+
elif not isinstance(stmt, TupleLiteral) and hasattr(stmt, "loc"):
|
|
886
|
+
expr = stmt
|
|
887
|
+
if expr is None:
|
|
888
|
+
return None
|
|
889
|
+
if isinstance(expr, Identifier) and expr.name in local_drawing:
|
|
890
|
+
return local_drawing[expr.name]
|
|
891
|
+
return self._udt_name_from_ctor(expr)
|
|
892
|
+
|
|
893
|
+
return _resolve_terminal(body[-1])
|
|
551
894
|
|
|
552
895
|
def _visit_VarDecl(self, node: VarDecl) -> PineType:
|
|
553
896
|
# Infer type from the value expression
|
|
@@ -620,14 +963,41 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
620
963
|
# Track var members
|
|
621
964
|
if node.is_var or node.is_varip:
|
|
622
965
|
init_str = self._expr_to_str(node.value)
|
|
623
|
-
self.
|
|
966
|
+
scope_name = self._symbols.current_scope.name
|
|
967
|
+
# Block-scoped var name-collision disambiguation. A ``var``/``varip``
|
|
968
|
+
# declared inside a non-global, non-function block (an ``if`` / ``for``
|
|
969
|
+
# / ``while`` body at on_bar scope) is keyed by RAW name. Two sibling
|
|
970
|
+
# blocks declaring the same name would dedupe to ONE C++ member and
|
|
971
|
+
# cross-contaminate (proven: egoigor1976-1-trendline-strategy's
|
|
972
|
+
# ``var bool valid`` in the upper- and lower-trendline ``if`` blocks).
|
|
973
|
+
# When such a name already belongs to a DIFFERENT block, mint a
|
|
974
|
+
# scope-unique member name and record the rename so codegen activates
|
|
975
|
+
# it (via ``_active_var_remap``) while emitting that block.
|
|
976
|
+
member_name = node.name
|
|
977
|
+
is_block_scoped = (
|
|
978
|
+
not self._global_scope
|
|
979
|
+
and not scope_name.startswith("func_")
|
|
980
|
+
and not scope_name.startswith("method_")
|
|
981
|
+
and bool(self._block_node_stack)
|
|
982
|
+
)
|
|
983
|
+
if is_block_scoped:
|
|
984
|
+
block_id = id(self._block_node_stack[-1])
|
|
985
|
+
owner = self._block_var_owner.get(node.name)
|
|
986
|
+
if owner is None:
|
|
987
|
+
# First block to claim this name keeps the raw member name.
|
|
988
|
+
self._block_var_owner[node.name] = block_id
|
|
989
|
+
elif owner != block_id:
|
|
990
|
+
# Sibling-scope collision: disambiguate this declaration.
|
|
991
|
+
self._block_var_seq += 1
|
|
992
|
+
member_name = f"{node.name}__blk{self._block_var_seq}"
|
|
993
|
+
self._block_var_renames.setdefault(block_id, {})[node.name] = member_name
|
|
994
|
+
self._var_members.append((member_name, val_type, init_str))
|
|
624
995
|
# Capture the init AST too so codegen can inspect the RHS callee
|
|
625
996
|
# (used to detect int64-returning builtins like ``time()`` and
|
|
626
997
|
# promote the symbol storage type to ``int64_t``).
|
|
627
998
|
if node.value is not None:
|
|
628
|
-
self._var_member_init_exprs[
|
|
999
|
+
self._var_member_init_exprs[member_name] = node.value
|
|
629
1000
|
# Track function-scoped var members
|
|
630
|
-
scope_name = self._symbols.current_scope.name
|
|
631
1001
|
if scope_name.startswith("func_"):
|
|
632
1002
|
func_name = scope_name[5:] # strip "func_" prefix
|
|
633
1003
|
if func_name not in self._func_var_members:
|
|
@@ -732,6 +1102,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
732
1102
|
setattr(sym, "is_static_series", True)
|
|
733
1103
|
self._symbols.define(sym)
|
|
734
1104
|
|
|
1105
|
+
# Track global-scope tuple-assign targets (e.g.
|
|
1106
|
+
# ``[pdH, pdL] = request.security(...)``) as class members so user
|
|
1107
|
+
# functions / later references resolve — mirroring _visit_VarDecl.
|
|
1108
|
+
# Without this the names are never declared and the C++ errors with
|
|
1109
|
+
# "use of undeclared identifier".
|
|
1110
|
+
if (self._global_scope
|
|
1111
|
+
and self._symbols.current_scope.name == "global"
|
|
1112
|
+
and name not in self._series_vars):
|
|
1113
|
+
self._global_var_decls.append((name, PineType.FLOAT))
|
|
1114
|
+
self._global_expr_map[name] = node.value
|
|
1115
|
+
self._record_global_binding_stmt(
|
|
1116
|
+
name, PineType.FLOAT, False, decl_node=node,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
735
1119
|
return val_type
|
|
736
1120
|
|
|
737
1121
|
# ------------------------------------------------------------------
|
|
@@ -745,18 +1129,28 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
745
1129
|
# Enter function scope
|
|
746
1130
|
self._symbols.enter_scope(f"func_{node.name}")
|
|
747
1131
|
|
|
748
|
-
# Define parameters
|
|
1132
|
+
# Define parameters. The type is UNKNOWN until inferred from a call
|
|
1133
|
+
# site, BUT a declared type hint (``string tf``, ``pivot hi``, ``line[] arr``)
|
|
1134
|
+
# is authoritative — record it as the symbol's ``type_spec`` / ``pine_type``
|
|
1135
|
+
# so (a) the param emits with the right C++ type and (b) callers passing
|
|
1136
|
+
# this param into another function can infer that function's param type
|
|
1137
|
+
# (e.g. ``getLineStyle(styleStr)`` where ``styleStr`` is a ``string`` param).
|
|
749
1138
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
750
|
-
|
|
1139
|
+
param_hints = (node.annotations or {}).get("param_type_hints", [])
|
|
1140
|
+
for i, param in enumerate(node.params):
|
|
1141
|
+
hint = param_hints[i] if i < len(param_hints) else None
|
|
1142
|
+
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
1143
|
+
ptype = self._type_hint_to_pine(hint) if hint else PineType.UNKNOWN
|
|
751
1144
|
sym = Symbol(
|
|
752
1145
|
name=param,
|
|
753
|
-
pine_type=
|
|
1146
|
+
pine_type=ptype,
|
|
754
1147
|
is_series=False,
|
|
755
1148
|
is_var=False,
|
|
756
1149
|
is_const=False,
|
|
757
1150
|
const_value=None,
|
|
758
1151
|
scope=f"func_{node.name}",
|
|
759
1152
|
loc=loc,
|
|
1153
|
+
type_spec=pspec,
|
|
760
1154
|
)
|
|
761
1155
|
self._symbols.define(sym)
|
|
762
1156
|
|
|
@@ -767,16 +1161,28 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
767
1161
|
body_type = PineType.VOID
|
|
768
1162
|
old_global = self._global_scope
|
|
769
1163
|
self._global_scope = False
|
|
1164
|
+
self._enclosing_func_params.append(set(node.params))
|
|
1165
|
+
self._nested_ta_touched = set()
|
|
770
1166
|
try:
|
|
771
1167
|
for stmt in node.body:
|
|
772
1168
|
body_type = self._visit(stmt)
|
|
773
1169
|
finally:
|
|
774
1170
|
self._global_scope = old_global
|
|
775
|
-
|
|
776
|
-
|
|
1171
|
+
self._enclosing_func_params.pop()
|
|
1172
|
+
nested_touched = self._nested_ta_touched
|
|
1173
|
+
self._nested_ta_touched = None
|
|
1174
|
+
|
|
1175
|
+
# Record TA range for this function. Widen to cover any nested-callee TA
|
|
1176
|
+
# sites whose ctor args were rewritten in terms of THIS function's params
|
|
1177
|
+
# (e.g. f_basisMa's sites parameterized by f_bbwp's _bbwLen), so resolving
|
|
1178
|
+
# this function at its call site re-substitutes those nested sites too.
|
|
777
1179
|
ta_end = len(self._ta_call_sites)
|
|
778
|
-
|
|
779
|
-
|
|
1180
|
+
lo, hi = ta_start, ta_end
|
|
1181
|
+
if nested_touched:
|
|
1182
|
+
lo = min(lo, min(nested_touched))
|
|
1183
|
+
hi = max(hi, max(nested_touched) + 1)
|
|
1184
|
+
if hi > lo:
|
|
1185
|
+
self._func_ta_ranges[node.name] = (lo, hi)
|
|
780
1186
|
|
|
781
1187
|
self._symbols.exit_scope()
|
|
782
1188
|
|
|
@@ -807,8 +1213,23 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
807
1213
|
# last_stmt is itself an expression node (single-expr funcs)
|
|
808
1214
|
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
|
|
809
1215
|
udt_ret = self._udt_name_from_ctor(ret_expr) if ret_expr is not None else None
|
|
1216
|
+
if udt_ret is None:
|
|
1217
|
+
# Drawing-handle returns wrapped in an if-statement terminal
|
|
1218
|
+
# branch (``makeEventLabel``) or returned as a bare drawing-handle
|
|
1219
|
+
# local (``setTradeLine``) are not direct ctors on the last
|
|
1220
|
+
# expression — resolve them so the function emits the C++ handle
|
|
1221
|
+
# type (Line/Label/...) instead of the ``double`` default.
|
|
1222
|
+
udt_ret = self._func_terminal_drawing_type(node)
|
|
810
1223
|
if udt_ret is not None:
|
|
811
1224
|
self._func_udt_return_types[node.name] = udt_ret
|
|
1225
|
+
# Array-return inference: a function whose body ends in
|
|
1226
|
+
# ``array.from(...)`` / ``array.new<T>(...)`` / a UDT method
|
|
1227
|
+
# returning an array returns a ``std::vector<...>``. The coarse
|
|
1228
|
+
# PineType return can't represent this, so carry the TypeSpec.
|
|
1229
|
+
if ret_expr is not None:
|
|
1230
|
+
ret_spec = self._type_spec_from_expr(ret_expr)
|
|
1231
|
+
if ret_spec is not None and ret_spec.kind == "array":
|
|
1232
|
+
self._func_return_type_specs[node.name] = ret_spec
|
|
812
1233
|
|
|
813
1234
|
# Store return type
|
|
814
1235
|
self._func_return_types[node.name] = body_type
|
|
@@ -880,12 +1301,14 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
880
1301
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
881
1302
|
param_hints = (node.annotations or {}).get("param_type_hints", [])
|
|
882
1303
|
param_types: list[PineType] = []
|
|
1304
|
+
param_specs: list = []
|
|
883
1305
|
for i, p in enumerate(node.params):
|
|
884
1306
|
udt_self = node.type_name if i == 0 else None
|
|
885
1307
|
hint = param_hints[i] if i < len(param_hints) else None
|
|
886
1308
|
ptype = self._type_hint_to_pine(hint) if hint else PineType.FLOAT
|
|
887
1309
|
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
888
1310
|
param_types.append(ptype)
|
|
1311
|
+
param_specs.append(pspec)
|
|
889
1312
|
self._symbols.define(Symbol(
|
|
890
1313
|
name=p, pine_type=ptype, is_series=False,
|
|
891
1314
|
is_var=False, is_const=False, const_value=None,
|
|
@@ -939,6 +1362,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
939
1362
|
returns_tuple=returns_tuple,
|
|
940
1363
|
tuple_element_count=tuple_element_count,
|
|
941
1364
|
param_defaults=param_defaults,
|
|
1365
|
+
param_type_specs=param_specs,
|
|
942
1366
|
)
|
|
943
1367
|
self._func_infos.append(fi)
|
|
944
1368
|
return PineType.VOID
|
|
@@ -950,6 +1374,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
950
1374
|
def _visit_IfStmt(self, node: IfStmt) -> PineType:
|
|
951
1375
|
old_global = self._global_scope
|
|
952
1376
|
self._global_scope = False
|
|
1377
|
+
self._block_node_stack.append(node)
|
|
953
1378
|
try:
|
|
954
1379
|
self._visit(node.condition)
|
|
955
1380
|
body_type = PineType.VOID
|
|
@@ -958,6 +1383,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
958
1383
|
for stmt in node.else_body:
|
|
959
1384
|
self._visit(stmt)
|
|
960
1385
|
finally:
|
|
1386
|
+
self._block_node_stack.pop()
|
|
961
1387
|
self._global_scope = old_global
|
|
962
1388
|
# If used as expression (x = if ...), return last expr type
|
|
963
1389
|
return body_type
|
|
@@ -981,6 +1407,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
981
1407
|
|
|
982
1408
|
old_global = self._global_scope
|
|
983
1409
|
self._global_scope = False
|
|
1410
|
+
self._block_node_stack.append(node)
|
|
984
1411
|
try:
|
|
985
1412
|
self._visit(node.start)
|
|
986
1413
|
self._visit(node.end)
|
|
@@ -989,6 +1416,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
989
1416
|
for stmt in node.body:
|
|
990
1417
|
self._visit(stmt)
|
|
991
1418
|
finally:
|
|
1419
|
+
self._block_node_stack.pop()
|
|
992
1420
|
self._global_scope = old_global
|
|
993
1421
|
|
|
994
1422
|
self._symbols.exit_scope()
|
|
@@ -997,6 +1425,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
997
1425
|
def _visit_ForInStmt(self, node) -> PineType:
|
|
998
1426
|
old_global = self._global_scope
|
|
999
1427
|
self._global_scope = False
|
|
1428
|
+
self._block_node_stack.append(node)
|
|
1000
1429
|
try:
|
|
1001
1430
|
self._visit(node.iterable)
|
|
1002
1431
|
self._symbols.enter_scope("for_in")
|
|
@@ -1019,17 +1448,20 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1019
1448
|
self._visit(stmt)
|
|
1020
1449
|
self._symbols.exit_scope()
|
|
1021
1450
|
finally:
|
|
1451
|
+
self._block_node_stack.pop()
|
|
1022
1452
|
self._global_scope = old_global
|
|
1023
1453
|
return PineType.VOID
|
|
1024
1454
|
|
|
1025
1455
|
def _visit_WhileStmt(self, node: WhileStmt) -> PineType:
|
|
1026
1456
|
old_global = self._global_scope
|
|
1027
1457
|
self._global_scope = False
|
|
1458
|
+
self._block_node_stack.append(node)
|
|
1028
1459
|
try:
|
|
1029
1460
|
self._visit(node.condition)
|
|
1030
1461
|
for stmt in node.body:
|
|
1031
1462
|
self._visit(stmt)
|
|
1032
1463
|
finally:
|
|
1464
|
+
self._block_node_stack.pop()
|
|
1033
1465
|
self._global_scope = old_global
|
|
1034
1466
|
return PineType.VOID
|
|
1035
1467
|
|
|
@@ -1151,6 +1583,12 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1151
1583
|
if isinstance(obj, Identifier) and obj.name == "str":
|
|
1152
1584
|
for arg in node.args:
|
|
1153
1585
|
self._visit(arg)
|
|
1586
|
+
# Most str.* return a string, but a few don't:
|
|
1587
|
+
# str.tonumber -> float, str.length -> int
|
|
1588
|
+
if member == "tonumber":
|
|
1589
|
+
return PineType.FLOAT
|
|
1590
|
+
if member == "length":
|
|
1591
|
+
return PineType.INT
|
|
1154
1592
|
return PineType.STRING
|
|
1155
1593
|
|
|
1156
1594
|
# request.* calls
|