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.

@@ -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, return TypeName."""
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
- return None
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._var_members.append((node.name, val_type, init_str))
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[node.name] = node.value
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 (type unknown until called)
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
- for param in node.params:
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=PineType.UNKNOWN,
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
- # Record TA range for this function
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
- if ta_end > ta_start:
779
- self._func_ta_ranges[node.name] = (ta_start, ta_end)
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