@pineforge/codegen-pyodide 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"):
@@ -179,6 +214,12 @@ class TypeInferer:
179
214
  return self._collection_types[node.name]
180
215
  if node.name in self._udt_var_types:
181
216
  return TypeSpec.udt(self._udt_var_types[node.name])
217
+ # Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
218
+ # method receiver registers in _udt_param_udt so its body getters
219
+ # resolve to the drawing udt and dispatch through the §4.3 path.
220
+ _pu = getattr(self, "_udt_param_udt", None)
221
+ if _pu and node.name in _pu and _pu[node.name] in DRAWING_TYPE_TO_CPP:
222
+ return TypeSpec.udt(_pu[node.name])
182
223
  sym = self.ctx.symbols.resolve(node.name)
183
224
  if sym is not None and getattr(sym, "type_spec", None) is not None:
184
225
  return sym.type_spec
@@ -190,12 +231,27 @@ class TypeInferer:
190
231
  return None
191
232
  if isinstance(node, FuncCall):
192
233
  func_name, namespace = self._resolve_callee(node.callee)
234
+ # ticker.* constructors (inherit/standard/heikinashi) return a symbol
235
+ # string; without this the member-type inference defaults to double
236
+ # and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
237
+ # double then assigns a std::string. (Analyzer agrees: ticker.* -> STRING.)
238
+ if namespace == "ticker":
239
+ return TypeSpec.primitive("string")
193
240
  targs = self._template_args_from_call(node)
241
+ # Drawing-objects-as-data return typing (spec §4.5 DRAWING_RETURN_SPECS):
242
+ # *.new / *.copy -> handle of the self-type; linefill.get_line* -> line.
243
+ if namespace in DRAWING_NS:
244
+ if func_name in ("new", "copy"):
245
+ return TypeSpec.udt(namespace)
246
+ if namespace == "linefill" and func_name in ("get_line1", "get_line2"):
247
+ return TypeSpec.udt("line")
248
+ if self._is_chart_point_callee(node.callee):
249
+ return TypeSpec.udt("chart.point")
194
250
  if namespace == "str" and func_name == "split":
195
251
  return TypeSpec.array(TypeSpec.primitive("string"))
196
252
  if namespace == "array" and func_name in (
197
253
  "new", "new_float", "new_int", "new_bool", "new_string", "from",
198
- ):
254
+ ) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
199
255
  if func_name == "new_int":
200
256
  return TypeSpec.array(TypeSpec.primitive("int"))
201
257
  if func_name == "new_bool":
@@ -204,6 +260,10 @@ class TypeInferer:
204
260
  return TypeSpec.array(TypeSpec.primitive("string"))
205
261
  if func_name == "new_float":
206
262
  return TypeSpec.array(TypeSpec.primitive("float"))
263
+ if func_name in ARRAY_DRAWING_NEW_CTORS:
264
+ # array.new_line()/new_box()/new_label()/new_linefill() ->
265
+ # std::vector<Line/Box/Label/Linefill> (typed alias of new<T>).
266
+ return TypeSpec.array(TypeSpec.udt(ARRAY_DRAWING_NEW_CTORS[func_name]))
207
267
  if targs:
208
268
  return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
209
269
  if func_name == "from" and node.args:
@@ -249,6 +309,14 @@ class TypeInferer:
249
309
  return recv_spec.element
250
310
  if func_name == "eigenvalues":
251
311
  return TypeSpec.array(TypeSpec.primitive("float"))
312
+ # Drawing method-form: ``a.copy()`` -> same handle type;
313
+ # ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
314
+ if (recv_spec is not None and recv_spec.kind == "udt"
315
+ and recv_spec.name in DRAWING_TYPE_TO_CPP):
316
+ if func_name == "copy":
317
+ return recv_spec
318
+ if recv_spec.name == "linefill" and func_name in ("get_line1", "get_line2"):
319
+ return TypeSpec.udt("line")
252
320
  return None
253
321
 
254
322
  # ------------------------------------------------------------------
@@ -345,6 +413,22 @@ class TypeInferer:
345
413
  if node.type_hint in self._udt_defs:
346
414
  return node.type_hint
347
415
  return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
416
+ # Drawing handle local (L-N6): a hintless local whose RHS resolves to a
417
+ # drawing udt must declare as the handle struct, not the analyzer's
418
+ # scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
419
+ # ``lvl.ln``, and ``c = a.copy()``. Also records _udt_var_types so later
420
+ # uses (``ln.set_x2(...)`` / ``ln.slope()``) resolve to the drawing udt.
421
+ if getattr(self, "_uses_drawing", False):
422
+ rhs_spec = self._type_spec_from_expr(node.value)
423
+ if (rhs_spec is not None and rhs_spec.kind == "udt"
424
+ and rhs_spec.name in DRAWING_TYPE_TO_CPP):
425
+ self._udt_var_types.setdefault(node.name, rhs_spec.name)
426
+ return DRAWING_TYPE_TO_CPP[rhs_spec.name]
427
+ # Scalar drawing getter local (get_text -> std::string, etc.).
428
+ if isinstance(node.value, FuncCall):
429
+ _dret = self._drawing_call_return_cpp(node.value)
430
+ if _dret is not None:
431
+ return _dret
348
432
  sym = self.ctx.symbols.resolve(node.name)
349
433
  if sym is not None:
350
434
  inferred = self._infer_type(node.value)
@@ -357,32 +441,285 @@ class TypeInferer:
357
441
 
358
442
  def _series_type_for(self, name: str) -> str:
359
443
  """C++ element type for a series variable's history buffer."""
360
- if self._is_int64_builtin_init(name):
444
+ from .tables import INT64_BUILTINS
445
+ # A bare int64 bar builtin used as a history series (``time[1]``) needs an
446
+ # int64_t buffer: epoch-ms overflow int32 and the na sentinel would be
447
+ # misdetected. ``_is_int64_builtin_init`` only matches user vars whose
448
+ # init RHS is such a builtin, so also match the builtin name directly.
449
+ if name in INT64_BUILTINS or self._is_int64_builtin_init(name):
361
450
  return "int64_t"
362
451
  sym = self.ctx.symbols.resolve(name)
363
452
  if sym is not None:
364
453
  return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
365
454
  return "double"
366
455
 
456
+ def _expr_is_int64_builtin(self, expr) -> bool:
457
+ """True if ``expr`` is a top-level int64-returning Pine builtin: either a
458
+ call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
459
+ or a bare ``Identifier`` spelled like ``time`` / ``time_close`` /
460
+ ``timenow`` (which Pine exposes as a value, not a call)."""
461
+ from .tables import INT64_BUILTINS, INT64_BUILTIN_IDENTIFIERS
462
+ if expr is None:
463
+ return False
464
+ if isinstance(expr, FuncCall):
465
+ func_name, namespace = self._resolve_callee(expr.callee)
466
+ return namespace is None and func_name in INT64_BUILTINS
467
+ if isinstance(expr, Identifier):
468
+ return expr.name in INT64_BUILTIN_IDENTIFIERS
469
+ return False
470
+
471
+ def _int64_reassign_targets(self) -> set[str]:
472
+ """Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
473
+ with an RHS that is a top-level int64-returning builtin. Cached on the
474
+ instance. Pine ``int`` collapses these to 32-bit, but the runtime stores
475
+ the epoch in 64 bits, so the member must be promoted to ``int64_t``."""
476
+ cached = getattr(self, "_int64_reassign_cache", None)
477
+ if cached is not None:
478
+ return cached
479
+ from ..ast_nodes import Assignment
480
+ targets: set[str] = set()
481
+ ast = getattr(self.ctx, "ast", None)
482
+ if ast is not None:
483
+ for node in self._walk_ast(ast):
484
+ if (isinstance(node, Assignment)
485
+ and isinstance(node.target, Identifier)
486
+ and self._expr_is_int64_builtin(node.value)):
487
+ targets.add(node.target.name)
488
+ self._int64_reassign_cache = targets
489
+ return targets
490
+
367
491
  def _is_int64_builtin_init(self, name: str) -> bool:
368
- """True if ``name``'s defining expression is a top-level call to a
369
- Pine builtin that returns ``int64_t`` (``time``, ``time_close``,
370
- ``timestamp``). The Pine type system collapses these to ``int``
371
- but the engine encodes the ``na`` sentinel in the upper 32 bits,
372
- so storing into ``Series<int>`` would silently corrupt na detection.
492
+ """True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
493
+ RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
494
+ ``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
495
+ collapses these to ``int`` but the engine encodes the ``na`` sentinel
496
+ (and the full epoch-ms value, which overflows int32) in 64 bits, so
497
+ storing into ``int`` silently corrupts both the value and na detection.
498
+ A reassignment like ``var int entryTime = na`` then ``entryTime := time``
499
+ must promote even though the *initializer* alone is ``na``.
373
500
  """
374
- from .tables import INT64_BUILTINS
375
501
  expr = (
376
502
  self.ctx.global_expr_map.get(name)
377
503
  or self.ctx.var_member_init_exprs.get(name)
378
504
  )
379
- if expr is None:
505
+ if self._expr_is_int64_builtin(expr):
506
+ return True
507
+ return name in self._int64_reassign_targets()
508
+
509
+ # ------------------------------------------------------------------
510
+ # BUG C: user-defined-UDT lvalue aliasing
511
+ # ------------------------------------------------------------------
512
+
513
+ def _is_udt_lvalue(self, expr) -> str | None:
514
+ """If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
515
+ names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``),
516
+ return its UDT type name; else ``None``.
517
+
518
+ Pine UDTs are reference types, so a local initialised from such an lvalue
519
+ and then mutated through must write back to the global. Drawing UDTs are
520
+ handled by the separate ``_uses_drawing`` path and are excluded here."""
521
+ if not isinstance(expr, Identifier):
522
+ return None
523
+ udt_t = self._udt_var_types.get(expr.name)
524
+ if udt_t is None or udt_t not in self._udt_defs:
525
+ return None
526
+ if udt_t in DRAWING_TYPE_TO_CPP:
527
+ return None
528
+ # Must be a known global/class-scope member (not a function param or a
529
+ # plain local snapshot) for write-through to be observable.
530
+ if expr.name in getattr(self, "_current_func_locals", set()):
531
+ # A function-local of UDT type that is itself a persistent ``var``
532
+ # member still write-through aliases; but a plain inline local does
533
+ # not represent shared state. Only treat ``var`` func-locals (in
534
+ # func_var_members) as aliasable shared state.
535
+ fname = getattr(self, "_active_func_name", None)
536
+ var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
537
+ if expr.name not in var_locals:
538
+ return None
539
+ return udt_t
540
+
541
+ def _udt_lvalue_selection_type(self, expr) -> str | None:
542
+ """UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
543
+ selectable branch is a UDT lvalue of the SAME user-defined UDT type.
544
+ Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
545
+ ``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
546
+ direct = self._is_udt_lvalue(expr)
547
+ if direct is not None:
548
+ return direct
549
+ branches: list = []
550
+ if isinstance(expr, Ternary):
551
+ branches = [expr.true_val, expr.false_val]
552
+ elif isinstance(expr, SwitchStmt):
553
+ for _case_expr, stmts in (expr.cases or []):
554
+ if not stmts:
555
+ return None
556
+ last = stmts[-1]
557
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
558
+ if expr.default_body:
559
+ last = expr.default_body[-1]
560
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
561
+ else:
562
+ return None
563
+ if not branches:
564
+ return None
565
+ types = {self._is_udt_lvalue(b) for b in branches}
566
+ if len(types) == 1 and None not in types:
567
+ return next(iter(types))
568
+ return None
569
+
570
+ def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
571
+ """Decide whether a hintless/typed local UDT declaration must ALIAS the
572
+ global(s) it selects rather than value-copy (BUG C).
573
+
574
+ Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
575
+ ``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
576
+ to a *different* UDT lvalue, which a C++ reference cannot do), or
577
+ ``None`` to keep the existing value-copy semantics.
578
+
579
+ Conditions (all required):
580
+ * RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
581
+ lvalues (``_udt_lvalue_selection_type``).
582
+ * The local is MUTATED later in the enclosing function body
583
+ (``local.field := ...``) — a pure read-only snapshot needn't alias.
584
+
585
+ The mutation requirement is the safety guard: a local that is only read
586
+ keeps value semantics, and a local initialised from a non-lvalue (a
587
+ ``.new()`` ctor, a function return, or a plain local copy) returns
588
+ ``None`` here, preserving intentional independent-copy semantics."""
589
+ from ..ast_nodes import Assignment
590
+ body = getattr(self, "_current_func_body", None)
591
+ if body is None:
592
+ return None
593
+ udt_t = self._udt_lvalue_selection_type(node.value)
594
+ if udt_t is None:
595
+ return None
596
+ name = node.name
597
+ mutated = False
598
+ rebinds_to_other_lvalue = False
599
+ for stmt in self._walk_ast_list(body):
600
+ if not isinstance(stmt, Assignment):
601
+ continue
602
+ tgt = stmt.target
603
+ # Mutation through the local: ``p.field := ...``
604
+ if (isinstance(tgt, MemberAccess)
605
+ and isinstance(tgt.object, Identifier)
606
+ and tgt.object.name == name):
607
+ mutated = True
608
+ # Rebind of the local itself to another UDT lvalue: ``p := other``
609
+ elif isinstance(tgt, Identifier) and tgt.name == name:
610
+ if self._udt_lvalue_selection_type(stmt.value) is not None:
611
+ rebinds_to_other_lvalue = True
612
+ else:
613
+ # Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
614
+ # aliasing would be wrong; bail to value-copy.
615
+ return None
616
+ if not mutated:
617
+ return None
618
+ return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
619
+
620
+ # ------------------------------------------------------------------
621
+ # BUG 2: collection (array / map / matrix) lvalue aliasing
622
+ # ------------------------------------------------------------------
623
+
624
+ def _collection_lvalue_spec(self, expr):
625
+ """If ``expr`` is a bare ``Identifier`` naming an array/map/matrix
626
+ var/global member, return its ``TypeSpec``; else ``None``. Pine
627
+ collections are reference types, so a local bound to such an lvalue and
628
+ then mutated through must ALIAS it, not value-copy."""
629
+ if not isinstance(expr, Identifier):
630
+ return None
631
+ name = expr.name
632
+ if name in self._matrix_specs:
633
+ return self._matrix_specs[name]
634
+ spec = self._collection_types.get(name)
635
+ if spec is not None and spec.kind in ("array", "map", "matrix"):
636
+ return spec
637
+ if name in self._array_vars:
638
+ return self._array_spec_for_name(name)
639
+ if name in self._map_vars:
640
+ return self._map_spec_for_name(name)
641
+ return None
642
+
643
+ def _collection_lvalue_selection_spec(self, expr):
644
+ """``TypeSpec`` if ``expr`` is a collection lvalue OR a ternary/switch
645
+ whose every selectable branch is a collection lvalue of the SAME C++
646
+ type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
647
+ returns, and mixed selections keep value-copy semantics). Mirrors
648
+ ``_udt_lvalue_selection_type`` for the BUG-2 collection-alias path."""
649
+ direct = self._collection_lvalue_spec(expr)
650
+ if direct is not None:
651
+ return direct
652
+ branches: list = []
653
+ if isinstance(expr, Ternary):
654
+ branches = [expr.true_val, expr.false_val]
655
+ elif isinstance(expr, SwitchStmt):
656
+ for _case_expr, stmts in (expr.cases or []):
657
+ if not stmts:
658
+ return None
659
+ last = stmts[-1]
660
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
661
+ if expr.default_body:
662
+ last = expr.default_body[-1]
663
+ branches.append(last.expr if isinstance(last, ExprStmt) else last)
664
+ else:
665
+ return None
666
+ if not branches:
667
+ return None
668
+ specs = [self._collection_lvalue_spec(b) for b in branches]
669
+ if any(s is None for s in specs):
670
+ return None
671
+ cpp_types = {self._type_spec_to_cpp(s) for s in specs}
672
+ if len(cpp_types) == 1:
673
+ return specs[0]
674
+ return None
675
+
676
+ def _collection_local_must_alias(self, node) -> bool:
677
+ """True when the local ``node`` declares an alias of an existing
678
+ collection lvalue that is later MUTATED in the enclosing function body
679
+ (``local.push/unshift/insert/remove/set/clear/pop/...``). A purely-read
680
+ local needn't alias; a local REASSIGNED to a different value can't be a
681
+ C++ reference, so it bails to value-copy (returns ``False``)."""
682
+ from ..ast_nodes import Assignment
683
+ body = getattr(self, "_current_func_body", None)
684
+ if body is None:
380
685
  return False
381
- if isinstance(expr, FuncCall):
382
- func_name, namespace = self._resolve_callee(expr.callee)
383
- if namespace is None and func_name in INT64_BUILTINS:
384
- return True
385
- return False
686
+ name = node.name
687
+ mutated = False
688
+ for stmt in self._walk_ast_list(body):
689
+ # Rebind of the local itself (``orderBlocks := other``) — a C++
690
+ # reference cannot rebind, so keep value-copy semantics.
691
+ if (isinstance(stmt, Assignment)
692
+ and isinstance(stmt.target, Identifier)
693
+ and stmt.target.name == name):
694
+ return False
695
+ if (isinstance(stmt, FuncCall)
696
+ and isinstance(stmt.callee, MemberAccess)
697
+ and isinstance(stmt.callee.object, Identifier)
698
+ and stmt.callee.object.name == name
699
+ and stmt.callee.member in COLLECTION_MUTATING_METHODS):
700
+ mutated = True
701
+ return mutated
702
+
703
+ def _walk_ast_list(self, stmts):
704
+ """Yield every node within a list of statements (depth-first)."""
705
+ for s in stmts:
706
+ yield from self._walk_ast(s)
707
+
708
+ def _addr_of_udt_selection(self, expr, local_name: str):
709
+ """Render the address-of form of a UDT lvalue selection for a pointer
710
+ alias (BUG C rebind case): ``other`` -> ``&(other)``;
711
+ ``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
712
+ guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
713
+ if isinstance(expr, Identifier):
714
+ return f"&({self._safe_name(expr.name)})"
715
+ if isinstance(expr, Ternary):
716
+ cond = self._visit_expr(expr.condition)
717
+ t = self._addr_of_udt_selection(expr.true_val, local_name)
718
+ f = self._addr_of_udt_selection(expr.false_val, local_name)
719
+ return f"({cond} ? {t} : {f})"
720
+ # Switch selection: lower to nested ternaries over case equality. Rare in
721
+ # practice; fall back to address-of the whole lowered expression.
722
+ return f"&({self._visit_expr(expr)})"
386
723
 
387
724
  def _infer_cpp_type_for_security_elem(self, node) -> str:
388
725
  """C++ type for one element of the ``request.security(..., expr, ...)`` payload.
@@ -462,6 +799,12 @@ class TypeInferer:
462
799
  return "double"
463
800
  if isinstance(node, FuncCall):
464
801
  func_name, namespace = self._resolve_callee(node.callee)
802
+ # Drawing scalar getter return type (get_text -> std::string,
803
+ # get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
804
+ if getattr(self, "_uses_drawing", False):
805
+ _dret = self._drawing_call_return_cpp(node)
806
+ if _dret is not None:
807
+ return _dret
465
808
  if func_name in ("time", "time_close") and namespace is None and node.args:
466
809
  return "int64_t"
467
810
  if func_name == "timestamp" and namespace is None:
@@ -473,12 +816,23 @@ class TypeInferer:
473
816
  return "std::string"
474
817
  if func_name == "bool":
475
818
  return "bool"
476
- if func_name in ("int", "color", "time"):
819
+ if func_name == "int":
477
820
  return "int"
821
+ # ``input.time`` returns an epoch-MS timestamp and ``input.color``
822
+ # a packed ARGB int — both use the ``get_input_int64`` getter
823
+ # (input.py), so their storage must be ``int64_t`` or the value
824
+ # truncates under int32 (e.g. a date-window bound flips sign and
825
+ # the guard is permanently false).
826
+ if func_name in ("color", "time"):
827
+ return "int64_t"
478
828
  return "double"
479
829
  if namespace == "str":
480
830
  if func_name == "split":
481
831
  return "std::vector<std::string>"
832
+ if func_name == "tonumber":
833
+ return "double"
834
+ if func_name == "length":
835
+ return "int"
482
836
  return "std::string"
483
837
  if namespace == "ta" and func_name == "pivot_point_levels":
484
838
  return "std::vector<double>"
@@ -563,16 +917,24 @@ class TypeInferer:
563
917
  def _infer_tuple_types(self, func_node: FuncDef, count: int) -> list[str]:
564
918
  """Infer the C++ type of each element returned by a tuple-returning function.
565
919
 
566
- Builds a lightweight local-type map from the function's
567
- ``VarDecl``s so identifiers referenced inside the final
568
- ``[a, b, c]`` literal resolve precisely; falls back to
569
- ``_infer_type`` when no local declaration matches."""
920
+ Builds a lightweight local-type map from the function's ``VarDecl``s
921
+ (including ones nested inside if/for/switch blocks) so identifiers
922
+ referenced inside the final ``[a, b, c]`` literal resolve precisely.
923
+ An explicit type hint wins (``string tag = na`` -> ``std::string``,
924
+ not the ``double`` implied by ``na``); otherwise the initializer
925
+ expression is inferred. Falls back to ``_infer_type`` when no local
926
+ declaration matches."""
570
927
  if not func_node.body:
571
928
  return ["double"] * count
572
929
 
573
930
  local_types: dict[str, str] = {}
574
- for stmt in func_node.body:
575
- if isinstance(stmt, VarDecl) and stmt.value is not None:
931
+ for stmt in self._walk_ast(func_node):
932
+ if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
933
+ if stmt.type_hint:
934
+ spec = self._type_spec_from_hint_name(stmt.type_hint)
935
+ if spec is not None:
936
+ local_types[stmt.name] = self._type_spec_to_cpp(spec)
937
+ continue
576
938
  local_types[stmt.name] = self._infer_type(stmt.value)
577
939
 
578
940
  last_stmt = func_node.body[-1]