@pineforge/codegen-pyodide 0.8.0 → 0.9.0

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.
@@ -41,13 +41,33 @@ from ..ast_nodes import (
41
41
  from ..symbols import PineType, TypeSpec
42
42
  from .. import signatures as sigs
43
43
  from .tables import (
44
+ ARRAY_DRAWING_NEW_CTORS,
44
45
  ARRAY_METHODS,
45
46
  BAR_BUILTINS,
46
47
  BAR_FIELDS,
48
+ DRAWING_NS,
49
+ DRAWING_TYPE_TO_CPP,
47
50
  PINE_TYPE_TO_CPP,
48
51
  TA_RETURNS_BOOL,
49
52
  )
50
53
 
54
+ # Collection (array / map / matrix) methods that MUTATE the receiver in place.
55
+ # Used by the BUG-2 collection-lvalue-alias path to decide whether a local
56
+ # bound to an existing collection lvalue must alias (mutated) or may value-copy
57
+ # (read-only). A method missing here only costs the alias optimization; a
58
+ # non-mutating method accidentally present would alias a read-only local, which
59
+ # is still correct (reads through a reference equal reads through a copy).
60
+ COLLECTION_MUTATING_METHODS = frozenset({
61
+ # array
62
+ "push", "unshift", "insert", "remove", "pop", "shift", "clear",
63
+ "set", "fill", "sort", "reverse", "concat",
64
+ # map
65
+ "put", "put_all",
66
+ # matrix
67
+ "add_row", "add_col", "remove_row", "remove_col", "reshape",
68
+ "swap_rows", "swap_columns",
69
+ })
70
+
51
71
 
52
72
  class TypeInferer:
53
73
  """Type-spec / C++-type inference helpers shared across visitor mixins.
@@ -100,6 +120,12 @@ class TypeInferer:
100
120
  return TypeSpec.map(key, val)
101
121
  if name in self._udt_defs:
102
122
  return TypeSpec.udt(name)
123
+ # Drawing-objects-as-data (spec §4.1 / P3): scalar ``line``/``box``/
124
+ # ``label``/``linefill``/``chart.point`` hints carry the handle identity
125
+ # via a udt TypeSpec. (``array<line>`` already resolves via the array
126
+ # fallback above.) Drawing names are NOT in _udt_defs.
127
+ if name in DRAWING_TYPE_TO_CPP:
128
+ return TypeSpec.udt(name)
103
129
  return None
104
130
 
105
131
  def _type_spec_to_cpp(self, spec: TypeSpec | None) -> str:
@@ -110,6 +136,11 @@ class TypeInferer:
110
136
  return {"float": "double", "int": "int", "bool": "bool",
111
137
  "string": "std::string", "color": "int"}.get(spec.name or "float", "double")
112
138
  if spec.kind == "udt" and spec.name:
139
+ # Drawing handle structs (P1): map BEFORE the _udt_defs check so
140
+ # array<line> -> std::vector<Line> and scalar line -> Line instead
141
+ # of the old collapse to double / unknown-type-name.
142
+ if spec.name in DRAWING_TYPE_TO_CPP:
143
+ return DRAWING_TYPE_TO_CPP[spec.name]
113
144
  return spec.name if spec.name in self._udt_defs else "double"
114
145
  if spec.kind == "array":
115
146
  return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
@@ -143,6 +174,10 @@ class TypeInferer:
143
174
  UDTs would otherwise fall through to ``0`` which is type-incompatible.
144
175
  """
145
176
  if spec is not None and spec.kind == "udt" and spec.name:
177
+ # Drawing handle default (P2): brace-init the C++ struct name
178
+ # (Line{} = na handle), NOT the lowercase Pine name (line{}).
179
+ if spec.name in DRAWING_TYPE_TO_CPP:
180
+ return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
146
181
  return f"{spec.name}{{}}"
147
182
  cpp_type = self._type_spec_to_cpp(spec)
148
183
  if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
@@ -175,10 +210,22 @@ class TypeInferer:
175
210
  if isinstance(node, StringLiteral):
176
211
  return TypeSpec.primitive("string")
177
212
  if isinstance(node, Identifier):
213
+ loop_specs = getattr(self, "_current_loop_var_specs", None)
214
+ if loop_specs and node.name in loop_specs:
215
+ return loop_specs[node.name]
178
216
  if node.name in self._collection_types:
179
217
  return self._collection_types[node.name]
218
+ param_specs = getattr(self, "_current_func_param_specs", {})
219
+ if node.name in param_specs:
220
+ return param_specs[node.name]
180
221
  if node.name in self._udt_var_types:
181
222
  return TypeSpec.udt(self._udt_var_types[node.name])
223
+ # Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
224
+ # method receiver registers in _udt_param_udt so its body getters
225
+ # resolve to the drawing udt and dispatch through the §4.3 path.
226
+ _pu = getattr(self, "_udt_param_udt", None)
227
+ if _pu and node.name in _pu and _pu[node.name] in DRAWING_TYPE_TO_CPP:
228
+ return TypeSpec.udt(_pu[node.name])
182
229
  sym = self.ctx.symbols.resolve(node.name)
183
230
  if sym is not None and getattr(sym, "type_spec", None) is not None:
184
231
  return sym.type_spec
@@ -190,12 +237,27 @@ class TypeInferer:
190
237
  return None
191
238
  if isinstance(node, FuncCall):
192
239
  func_name, namespace = self._resolve_callee(node.callee)
240
+ # ticker.* constructors (inherit/standard/heikinashi) return a symbol
241
+ # string; without this the member-type inference defaults to double
242
+ # and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
243
+ # double then assigns a std::string. (Analyzer agrees: ticker.* -> STRING.)
244
+ if namespace == "ticker":
245
+ return TypeSpec.primitive("string")
193
246
  targs = self._template_args_from_call(node)
247
+ # Drawing-objects-as-data return typing (spec §4.5 DRAWING_RETURN_SPECS):
248
+ # *.new / *.copy -> handle of the self-type; linefill.get_line* -> line.
249
+ if namespace in DRAWING_NS:
250
+ if func_name in ("new", "copy"):
251
+ return TypeSpec.udt(namespace)
252
+ if namespace == "linefill" and func_name in ("get_line1", "get_line2"):
253
+ return TypeSpec.udt("line")
254
+ if self._is_chart_point_callee(node.callee):
255
+ return TypeSpec.udt("chart.point")
194
256
  if namespace == "str" and func_name == "split":
195
257
  return TypeSpec.array(TypeSpec.primitive("string"))
196
258
  if namespace == "array" and func_name in (
197
259
  "new", "new_float", "new_int", "new_bool", "new_string", "from",
198
- ):
260
+ ) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
199
261
  if func_name == "new_int":
200
262
  return TypeSpec.array(TypeSpec.primitive("int"))
201
263
  if func_name == "new_bool":
@@ -204,6 +266,10 @@ class TypeInferer:
204
266
  return TypeSpec.array(TypeSpec.primitive("string"))
205
267
  if func_name == "new_float":
206
268
  return TypeSpec.array(TypeSpec.primitive("float"))
269
+ if func_name in ARRAY_DRAWING_NEW_CTORS:
270
+ # array.new_line()/new_box()/new_label()/new_linefill() ->
271
+ # std::vector<Line/Box/Label/Linefill> (typed alias of new<T>).
272
+ return TypeSpec.array(TypeSpec.udt(ARRAY_DRAWING_NEW_CTORS[func_name]))
207
273
  if targs:
208
274
  return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
209
275
  if func_name == "from" and node.args:
@@ -249,12 +315,58 @@ class TypeInferer:
249
315
  return recv_spec.element
250
316
  if func_name == "eigenvalues":
251
317
  return TypeSpec.array(TypeSpec.primitive("float"))
318
+ # Drawing method-form: ``a.copy()`` -> same handle type;
319
+ # ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
320
+ if (recv_spec is not None and recv_spec.kind == "udt"
321
+ and recv_spec.name in DRAWING_TYPE_TO_CPP):
322
+ if func_name == "copy":
323
+ return recv_spec
324
+ if recv_spec.name == "linefill" and func_name in ("get_line1", "get_line2"):
325
+ return TypeSpec.udt("line")
252
326
  return None
253
327
 
254
328
  # ------------------------------------------------------------------
255
329
  # Method lowering for collection types (used by visit_call paths)
256
330
  # ------------------------------------------------------------------
257
331
 
332
+ def _array_receiver_once_expr(
333
+ self, array_expr: str, args: list[str], lower_receiver,
334
+ ) -> str:
335
+ """Lower an array method without duplicating its receiver evaluation.
336
+
337
+ ``ARRAY_METHODS`` is intentionally a compact table of expression
338
+ templates. Many templates need the receiver more than once (for
339
+ example ``begin()`` + ``end()``). Substituting a temporary-producing
340
+ receiver directly into those slots creates distinct objects, so the
341
+ resulting iterator range is invalid. Render first with a fresh token;
342
+ when the template uses it repeatedly and the real receiver is not a
343
+ plain identifier lvalue, bind it to one lambda-local forwarding
344
+ reference and render every use through that binding. The lambda's
345
+ deduced ``auto`` return copies scalar results before a temporary
346
+ receiver dies; mutations still reach lvalue arrays through the
347
+ forwarding reference. Single-use and plain-identifier lowerings
348
+ remain byte-for-byte unchanged.
349
+
350
+ This is receiver-only by design: Pine argument evaluation and the
351
+ separate empty-array semantics are outside this fix.
352
+ """
353
+ counter = getattr(self, "_array_receiver_counter", 0)
354
+ occupied = "\n".join((array_expr, *args))
355
+ while True:
356
+ receiver = f"__pf_array_receiver_{counter}"
357
+ counter += 1
358
+ if receiver not in occupied:
359
+ break
360
+ self._array_receiver_counter = counter
361
+
362
+ lowered = lower_receiver(receiver)
363
+ if lowered.count(receiver) <= 1 or array_expr.isidentifier():
364
+ return lower_receiver(array_expr)
365
+ return (
366
+ f"[&]() {{ auto&& {receiver} = ({array_expr}); "
367
+ f"return {lowered}; }}()"
368
+ )
369
+
258
370
  def _array_method_expr(
259
371
  self, array_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
260
372
  ) -> str:
@@ -263,34 +375,36 @@ class TypeInferer:
263
375
  arr_cpp_type = self._type_spec_to_cpp(spec)
264
376
  elem_cpp = self._type_spec_to_cpp(spec.element) if spec.element is not None else "double"
265
377
  if method == "copy":
266
- return f"{arr_cpp_type}({array_expr})"
267
- if method == "slice":
268
- return f"{arr_cpp_type}({array_expr}.begin()+(int)({args[0]}),{array_expr}.begin()+(int)({args[1]}))"
269
- if method == "join":
378
+ lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
379
+ elif method == "slice":
380
+ lower_receiver = lambda recv: f"{arr_cpp_type}({recv}.begin()+(int)({args[0]}),{recv}.begin()+(int)({args[1]}))"
381
+ elif method == "join" and elem_cpp == "std::string":
270
382
  sep = args[0] if args else 'std::string(",")'
271
- if elem_cpp == "std::string":
272
- return f"[&](){{ std::string r; for(size_t i=0;i<{array_expr}.size();i++){{ if(i>0)r+={sep}; r+={array_expr}[i]; }} return r; }}()"
273
- numeric_only = {
274
- "sum", "avg", "min", "max", "range", "stdev", "variance", "median",
275
- "mode", "percentile_linear_interpolation", "percentile_nearest_rank",
276
- "percentrank", "abs", "standardize", "covariance", "binary_search",
277
- "binary_search_leftmost", "binary_search_rightmost", "sort_indices",
278
- }
279
- if method in numeric_only and elem_cpp not in ("double", "int"):
280
- self._codegen_error(
281
- None,
282
- f"array.{method} requires a numeric array",
283
- hint="Use numeric arrays for aggregate/statistical array functions.",
284
- )
285
- if method in ARRAY_METHODS:
286
- return ARRAY_METHODS[method](array_expr, args)
287
- # Defensive: support_checker rejects any array.* method not in
288
- # SUPPORTED_ARRAY (derived from ARRAY_METHODS). Reaching here means the
289
- # checker was bypassed or the tables drifted.
290
- raise ValueError(
291
- f"codegen: unhandled array method '{method}' — analyzer should have "
292
- f"rejected. Add it to ARRAY_METHODS."
293
- )
383
+ lower_receiver = lambda recv: f"[&](){{ std::string r; for(size_t i=0;i<{recv}.size();i++){{ if(i>0)r+={sep}; r+={recv}[i]; }} return r; }}()"
384
+ else:
385
+ numeric_only = {
386
+ "sum", "avg", "min", "max", "range", "stdev", "variance", "median",
387
+ "mode", "percentile_linear_interpolation", "percentile_nearest_rank",
388
+ "percentrank", "abs", "standardize", "covariance", "binary_search",
389
+ "binary_search_leftmost", "binary_search_rightmost", "sort_indices",
390
+ }
391
+ if method in numeric_only and elem_cpp not in ("double", "int"):
392
+ self._codegen_error(
393
+ None,
394
+ f"array.{method} requires a numeric array",
395
+ hint="Use numeric arrays for aggregate/statistical array functions.",
396
+ )
397
+ if method not in ARRAY_METHODS:
398
+ # Defensive: support_checker rejects any array.* method not in
399
+ # SUPPORTED_ARRAY (derived from ARRAY_METHODS). Reaching here means the
400
+ # checker was bypassed or the tables drifted.
401
+ raise ValueError(
402
+ f"codegen: unhandled array method '{method}' — analyzer should have "
403
+ f"rejected. Add it to ARRAY_METHODS."
404
+ )
405
+ lower_receiver = lambda recv: ARRAY_METHODS[method](recv, args)
406
+
407
+ return self._array_receiver_once_expr(array_expr, args, lower_receiver)
294
408
 
295
409
  def _map_method_expr(
296
410
  self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
@@ -345,6 +459,22 @@ class TypeInferer:
345
459
  if node.type_hint in self._udt_defs:
346
460
  return node.type_hint
347
461
  return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
462
+ # Drawing handle local (L-N6): a hintless local whose RHS resolves to a
463
+ # drawing udt must declare as the handle struct, not the analyzer's
464
+ # scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
465
+ # ``lvl.ln``, and ``c = a.copy()``. Also records _udt_var_types so later
466
+ # uses (``ln.set_x2(...)`` / ``ln.slope()``) resolve to the drawing udt.
467
+ if getattr(self, "_uses_drawing", False):
468
+ rhs_spec = self._type_spec_from_expr(node.value)
469
+ if (rhs_spec is not None and rhs_spec.kind == "udt"
470
+ and rhs_spec.name in DRAWING_TYPE_TO_CPP):
471
+ self._udt_var_types.setdefault(node.name, rhs_spec.name)
472
+ return DRAWING_TYPE_TO_CPP[rhs_spec.name]
473
+ # Scalar drawing getter local (get_text -> std::string, etc.).
474
+ if isinstance(node.value, FuncCall):
475
+ _dret = self._drawing_call_return_cpp(node.value)
476
+ if _dret is not None:
477
+ return _dret
348
478
  sym = self.ctx.symbols.resolve(node.name)
349
479
  if sym is not None:
350
480
  inferred = self._infer_type(node.value)
@@ -369,25 +499,290 @@ class TypeInferer:
369
499
  return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
370
500
  return "double"
371
501
 
502
+ def _expr_is_int64_builtin(self, expr) -> bool:
503
+ """True if ``expr`` is a top-level int64-returning Pine builtin: either a
504
+ call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
505
+ or a bare ``Identifier`` spelled like ``time`` / ``time_close`` /
506
+ ``timenow`` (which Pine exposes as a value, not a call)."""
507
+ from .tables import INT64_BUILTINS, INT64_BUILTIN_IDENTIFIERS
508
+ if expr is None:
509
+ return False
510
+ if isinstance(expr, FuncCall):
511
+ func_name, namespace = self._resolve_callee(expr.callee)
512
+ return namespace is None and func_name in INT64_BUILTINS
513
+ if isinstance(expr, Identifier):
514
+ return expr.name in INT64_BUILTIN_IDENTIFIERS
515
+ return False
516
+
517
+ def _int64_reassign_targets(self) -> set[str]:
518
+ """Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
519
+ with an RHS that is a top-level int64-returning builtin. Cached on the
520
+ instance. Pine ``int`` collapses these to 32-bit, but the runtime stores
521
+ the epoch in 64 bits, so the member must be promoted to ``int64_t``."""
522
+ cached = getattr(self, "_int64_reassign_cache", None)
523
+ if cached is not None:
524
+ return cached
525
+ from ..ast_nodes import Assignment
526
+ targets: set[str] = set()
527
+ ast = getattr(self.ctx, "ast", None)
528
+ if ast is not None:
529
+ for node in self._walk_ast(ast):
530
+ if (isinstance(node, Assignment)
531
+ and isinstance(node.target, Identifier)
532
+ and self._expr_is_int64_builtin(node.value)):
533
+ targets.add(node.target.name)
534
+ self._int64_reassign_cache = targets
535
+ return targets
536
+
372
537
  def _is_int64_builtin_init(self, name: str) -> bool:
373
- """True if ``name``'s defining expression is a top-level call to a
374
- Pine builtin that returns ``int64_t`` (``time``, ``time_close``,
375
- ``timestamp``). The Pine type system collapses these to ``int``
376
- but the engine encodes the ``na`` sentinel in the upper 32 bits,
377
- so storing into ``Series<int>`` would silently corrupt na detection.
538
+ """True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
539
+ RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
540
+ ``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
541
+ collapses these to ``int`` but the engine encodes the ``na`` sentinel
542
+ (and the full epoch-ms value, which overflows int32) in 64 bits, so
543
+ storing into ``int`` silently corrupts both the value and na detection.
544
+ A reassignment like ``var int entryTime = na`` then ``entryTime := time``
545
+ must promote even though the *initializer* alone is ``na``.
378
546
  """
379
- from .tables import INT64_BUILTINS
380
547
  expr = (
381
548
  self.ctx.global_expr_map.get(name)
382
549
  or self.ctx.var_member_init_exprs.get(name)
383
550
  )
384
- if expr is None:
385
- return False
551
+ if self._expr_is_int64_builtin(expr):
552
+ return True
553
+ return name in self._int64_reassign_targets()
554
+
555
+ # ------------------------------------------------------------------
556
+ # BUG C: user-defined-UDT lvalue aliasing
557
+ # ------------------------------------------------------------------
558
+
559
+ def _is_udt_lvalue(self, expr) -> str | None:
560
+ """If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
561
+ names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``,
562
+ or an element selected from ``array<UDT>``), return its UDT type name;
563
+ else ``None``.
564
+
565
+ Pine UDTs are reference types, so a local initialised from such an lvalue
566
+ and then mutated through must write back to the global. Drawing UDTs are
567
+ handled by the separate ``_uses_drawing`` path and are excluded here."""
386
568
  if isinstance(expr, FuncCall):
387
- func_name, namespace = self._resolve_callee(expr.callee)
388
- if namespace is None and func_name in INT64_BUILTINS:
389
- return True
390
- return False
569
+ callee = expr.callee
570
+ func_name, namespace = self._resolve_callee(callee)
571
+ receiver = None
572
+ if namespace == "array" and func_name in ("get", "first", "last") and expr.args:
573
+ receiver = expr.args[0]
574
+ elif (isinstance(callee, MemberAccess)
575
+ and func_name in ("get", "first", "last")):
576
+ receiver = callee.object
577
+ if receiver is not None:
578
+ spec = self._type_spec_from_expr(receiver)
579
+ elem = spec.element if spec is not None and spec.kind == "array" else None
580
+ if (elem is not None and elem.kind == "udt" and elem.name in self._udt_defs
581
+ and elem.name not in DRAWING_TYPE_TO_CPP):
582
+ return elem.name
583
+ return None
584
+ if not isinstance(expr, Identifier):
585
+ return None
586
+ udt_t = self._udt_var_types.get(expr.name)
587
+ if udt_t is None or udt_t not in self._udt_defs:
588
+ return None
589
+ if udt_t in DRAWING_TYPE_TO_CPP:
590
+ return None
591
+ # Must be a known global/class-scope member (not a function param or a
592
+ # plain local snapshot) for write-through to be observable.
593
+ if expr.name in getattr(self, "_current_func_locals", set()):
594
+ # A function-local of UDT type that is itself a persistent ``var``
595
+ # member still write-through aliases; but a plain inline local does
596
+ # not represent shared state. Only treat ``var`` func-locals (in
597
+ # func_var_members) as aliasable shared state.
598
+ fname = getattr(self, "_active_func_name", None)
599
+ var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
600
+ if expr.name not in var_locals:
601
+ return None
602
+ return udt_t
603
+
604
+ def _udt_lvalue_selection_type(self, expr) -> str | None:
605
+ """UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
606
+ selectable branch is a UDT lvalue of the SAME user-defined UDT type.
607
+ Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
608
+ ``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
609
+ direct = self._is_udt_lvalue(expr)
610
+ if direct is not None:
611
+ return direct
612
+ branches: list = []
613
+ if isinstance(expr, Ternary):
614
+ branches = [expr.true_val, expr.false_val]
615
+ elif isinstance(expr, SwitchStmt):
616
+ for _case_expr, stmts in (expr.cases or []):
617
+ if not stmts:
618
+ return None
619
+ last = stmts[-1]
620
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
621
+ if expr.default_body:
622
+ last = expr.default_body[-1]
623
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
624
+ else:
625
+ return None
626
+ if not branches:
627
+ return None
628
+ types = {self._is_udt_lvalue(b) for b in branches}
629
+ if len(types) == 1 and None not in types:
630
+ return next(iter(types))
631
+ return None
632
+
633
+ def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
634
+ """Decide whether a hintless/typed local UDT declaration must ALIAS the
635
+ global(s) it selects rather than value-copy (BUG C).
636
+
637
+ Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
638
+ ``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
639
+ to a *different* UDT lvalue, which a C++ reference cannot do), or
640
+ ``None`` to keep the existing value-copy semantics.
641
+
642
+ Conditions (all required):
643
+ * RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
644
+ lvalues (``_udt_lvalue_selection_type``).
645
+ * The local is MUTATED later in the enclosing function body
646
+ (``local.field := ...``) — a pure read-only snapshot needn't alias.
647
+
648
+ The mutation requirement is the safety guard: a local that is only read
649
+ keeps value semantics, and a local initialised from a non-lvalue (a
650
+ ``.new()`` ctor, a function return, or a plain local copy) returns
651
+ ``None`` here, preserving intentional independent-copy semantics."""
652
+ from ..ast_nodes import Assignment
653
+ body = getattr(self, "_current_func_body", None)
654
+ if body is None:
655
+ return None
656
+ udt_t = self._udt_lvalue_selection_type(node.value)
657
+ if udt_t is None:
658
+ return None
659
+ name = node.name
660
+ mutated = False
661
+ rebinds_to_other_lvalue = False
662
+ for stmt in self._walk_ast_list(body):
663
+ if not isinstance(stmt, Assignment):
664
+ continue
665
+ tgt = stmt.target
666
+ # Mutation through the local: ``p.field := ...``
667
+ if (isinstance(tgt, MemberAccess)
668
+ and isinstance(tgt.object, Identifier)
669
+ and tgt.object.name == name):
670
+ mutated = True
671
+ # Rebind of the local itself to another UDT lvalue: ``p := other``
672
+ elif isinstance(tgt, Identifier) and tgt.name == name:
673
+ if self._udt_lvalue_selection_type(stmt.value) is not None:
674
+ rebinds_to_other_lvalue = True
675
+ else:
676
+ # Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
677
+ # aliasing would be wrong; bail to value-copy.
678
+ return None
679
+ if not mutated:
680
+ return None
681
+ return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
682
+
683
+ # ------------------------------------------------------------------
684
+ # BUG 2: collection (array / map / matrix) lvalue aliasing
685
+ # ------------------------------------------------------------------
686
+
687
+ def _collection_lvalue_spec(self, expr):
688
+ """If ``expr`` is a bare ``Identifier`` naming an array/map/matrix
689
+ var/global member, return its ``TypeSpec``; else ``None``. Pine
690
+ collections are reference types, so a local bound to such an lvalue and
691
+ then mutated through must ALIAS it, not value-copy."""
692
+ if not isinstance(expr, Identifier):
693
+ return None
694
+ name = expr.name
695
+ if name in self._matrix_specs:
696
+ return self._matrix_specs[name]
697
+ spec = self._collection_types.get(name)
698
+ if spec is not None and spec.kind in ("array", "map", "matrix"):
699
+ return spec
700
+ if name in self._array_vars:
701
+ return self._array_spec_for_name(name)
702
+ if name in self._map_vars:
703
+ return self._map_spec_for_name(name)
704
+ return None
705
+
706
+ def _collection_lvalue_selection_spec(self, expr):
707
+ """``TypeSpec`` if ``expr`` is a collection lvalue OR a ternary/switch
708
+ whose every selectable branch is a collection lvalue of the SAME C++
709
+ type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
710
+ returns, and mixed selections keep value-copy semantics). Mirrors
711
+ ``_udt_lvalue_selection_type`` for the BUG-2 collection-alias path."""
712
+ direct = self._collection_lvalue_spec(expr)
713
+ if direct is not None:
714
+ return direct
715
+ branches: list = []
716
+ if isinstance(expr, Ternary):
717
+ branches = [expr.true_val, expr.false_val]
718
+ elif isinstance(expr, SwitchStmt):
719
+ for _case_expr, stmts in (expr.cases or []):
720
+ if not stmts:
721
+ return None
722
+ last = stmts[-1]
723
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
724
+ if expr.default_body:
725
+ last = expr.default_body[-1]
726
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
727
+ else:
728
+ return None
729
+ if not branches:
730
+ return None
731
+ specs = [self._collection_lvalue_spec(b) for b in branches]
732
+ if any(s is None for s in specs):
733
+ return None
734
+ cpp_types = {self._type_spec_to_cpp(s) for s in specs}
735
+ if len(cpp_types) == 1:
736
+ return specs[0]
737
+ return None
738
+
739
+ def _collection_local_must_alias(self, node) -> bool:
740
+ """True when the local ``node`` declares an alias of an existing
741
+ collection lvalue that is later MUTATED in the enclosing function body
742
+ (``local.push/unshift/insert/remove/set/clear/pop/...``). A purely-read
743
+ local needn't alias; a local REASSIGNED to a different value can't be a
744
+ C++ reference, so it bails to value-copy (returns ``False``)."""
745
+ from ..ast_nodes import Assignment
746
+ body = getattr(self, "_current_func_body", None)
747
+ if body is None:
748
+ return False
749
+ name = node.name
750
+ mutated = False
751
+ for stmt in self._walk_ast_list(body):
752
+ # Rebind of the local itself (``orderBlocks := other``) — a C++
753
+ # reference cannot rebind, so keep value-copy semantics.
754
+ if (isinstance(stmt, Assignment)
755
+ and isinstance(stmt.target, Identifier)
756
+ and stmt.target.name == name):
757
+ return False
758
+ if (isinstance(stmt, FuncCall)
759
+ and isinstance(stmt.callee, MemberAccess)
760
+ and isinstance(stmt.callee.object, Identifier)
761
+ and stmt.callee.object.name == name
762
+ and stmt.callee.member in COLLECTION_MUTATING_METHODS):
763
+ mutated = True
764
+ return mutated
765
+
766
+ def _walk_ast_list(self, stmts):
767
+ """Yield every node within a list of statements (depth-first)."""
768
+ for s in stmts:
769
+ yield from self._walk_ast(s)
770
+
771
+ def _addr_of_udt_selection(self, expr, local_name: str):
772
+ """Render the address-of form of a UDT lvalue selection for a pointer
773
+ alias (BUG C rebind case): ``other`` -> ``&(other)``;
774
+ ``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
775
+ guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
776
+ if isinstance(expr, Identifier):
777
+ return f"&({self._safe_name(expr.name)})"
778
+ if isinstance(expr, Ternary):
779
+ cond = self._visit_expr(expr.condition)
780
+ t = self._addr_of_udt_selection(expr.true_val, local_name)
781
+ f = self._addr_of_udt_selection(expr.false_val, local_name)
782
+ return f"({cond} ? {t} : {f})"
783
+ # Switch selection: lower to nested ternaries over case equality. Rare in
784
+ # practice; fall back to address-of the whole lowered expression.
785
+ return f"&({self._visit_expr(expr)})"
391
786
 
392
787
  def _infer_cpp_type_for_security_elem(self, node) -> str:
393
788
  """C++ type for one element of the ``request.security(..., expr, ...)`` payload.
@@ -459,6 +854,8 @@ class TypeInferer:
459
854
  return "double"
460
855
  if node.name in self._current_func_param_types:
461
856
  return self._current_func_param_types[node.name]
857
+ if node.name in getattr(self, "_current_func_local_types", {}):
858
+ return self._current_func_local_types[node.name]
462
859
  sym = self.ctx.symbols.resolve(node.name)
463
860
  if sym is not None and getattr(sym, "type_spec", None) is not None:
464
861
  return self._type_spec_to_cpp(sym.type_spec)
@@ -467,6 +864,12 @@ class TypeInferer:
467
864
  return "double"
468
865
  if isinstance(node, FuncCall):
469
866
  func_name, namespace = self._resolve_callee(node.callee)
867
+ # Drawing scalar getter return type (get_text -> std::string,
868
+ # get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
869
+ if getattr(self, "_uses_drawing", False):
870
+ _dret = self._drawing_call_return_cpp(node)
871
+ if _dret is not None:
872
+ return _dret
470
873
  if func_name in ("time", "time_close") and namespace is None and node.args:
471
874
  return "int64_t"
472
875
  if func_name == "timestamp" and namespace is None:
@@ -491,6 +894,12 @@ class TypeInferer:
491
894
  if namespace == "str":
492
895
  if func_name == "split":
493
896
  return "std::vector<std::string>"
897
+ if func_name in ("contains", "startswith", "endswith"):
898
+ return "bool"
899
+ if func_name == "tonumber":
900
+ return "double"
901
+ if func_name in ("length", "pos"):
902
+ return "int"
494
903
  return "std::string"
495
904
  if namespace == "ta" and func_name == "pivot_point_levels":
496
905
  return "std::vector<double>"
@@ -539,6 +948,12 @@ class TypeInferer:
539
948
  # pine_str_tostring); bare reads must declare std::string.
540
949
  if ename == "format":
541
950
  return "std::string"
951
+ if ename == "timeframe":
952
+ if node.member in ("period", "main_period"):
953
+ return "std::string"
954
+ if node.member == "multiplier":
955
+ return "int"
956
+ return "bool"
542
957
  # syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
543
958
  # and derive C++ type from the expression (na<T>() or function call).
544
959
  if ename == "syminfo":
@@ -546,6 +961,9 @@ class TypeInferer:
546
961
  sym_key = f"syminfo.{node.member}"
547
962
  if sym_key in _pf_sigs.SYMINFO_VARIABLES:
548
963
  return PINE_TYPE_TO_CPP.get(_pf_sigs.SYMINFO_VARIABLES[sym_key], "double")
964
+ spec = self._type_spec_from_expr(node)
965
+ if spec is not None:
966
+ return self._type_spec_to_cpp(spec)
549
967
  if isinstance(node, Ternary):
550
968
  tt = self._infer_type(node.true_val)
551
969
  ft = self._infer_type(node.false_val)
@@ -553,6 +971,12 @@ class TypeInferer:
553
971
  return tt if tt.startswith("std::vector") else ft
554
972
  if tt == "std::string" or ft == "std::string":
555
973
  return "std::string"
974
+ if tt == "double" or ft == "double":
975
+ return "double"
976
+ if tt == "int64_t" or ft == "int64_t":
977
+ return "int64_t"
978
+ if tt == "bool" and ft == "bool":
979
+ return "bool"
556
980
  return tt
557
981
  # Block-as-expression cases: read the type of the last statement of
558
982
  # the first branch / case; matches Pine semantics for ``x = if...``.
@@ -575,16 +999,24 @@ class TypeInferer:
575
999
  def _infer_tuple_types(self, func_node: FuncDef, count: int) -> list[str]:
576
1000
  """Infer the C++ type of each element returned by a tuple-returning function.
577
1001
 
578
- Builds a lightweight local-type map from the function's
579
- ``VarDecl``s so identifiers referenced inside the final
580
- ``[a, b, c]`` literal resolve precisely; falls back to
581
- ``_infer_type`` when no local declaration matches."""
1002
+ Builds a lightweight local-type map from the function's ``VarDecl``s
1003
+ (including ones nested inside if/for/switch blocks) so identifiers
1004
+ referenced inside the final ``[a, b, c]`` literal resolve precisely.
1005
+ An explicit type hint wins (``string tag = na`` -> ``std::string``,
1006
+ not the ``double`` implied by ``na``); otherwise the initializer
1007
+ expression is inferred. Falls back to ``_infer_type`` when no local
1008
+ declaration matches."""
582
1009
  if not func_node.body:
583
1010
  return ["double"] * count
584
1011
 
585
1012
  local_types: dict[str, str] = {}
586
- for stmt in func_node.body:
587
- if isinstance(stmt, VarDecl) and stmt.value is not None:
1013
+ for stmt in self._walk_ast(func_node):
1014
+ if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
1015
+ if stmt.type_hint:
1016
+ spec = self._type_spec_from_hint_name(stmt.type_hint)
1017
+ if spec is not None:
1018
+ local_types[stmt.name] = self._type_spec_to_cpp(spec)
1019
+ continue
588
1020
  local_types[stmt.name] = self._infer_type(stmt.value)
589
1021
 
590
1022
  last_stmt = func_node.body[-1]