@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -33,18 +33,35 @@ tables it needs come from ``codegen/tables.py``.
33
33
 
34
34
  from __future__ import annotations
35
35
 
36
+ import contextlib
37
+ import re
38
+
36
39
  from ..ast_nodes import (
37
- ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
38
- MemberAccess, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
39
- Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl,
40
+ ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall, FuncDef,
41
+ Identifier, IfStmt,
42
+ MemberAccess, MethodDef, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
43
+ Subscript, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
40
44
  )
45
+ from ..errors import Phase
46
+ from ..external_requests import UNPINNED_ANNOTATION
47
+ from ..limits import iter_ast_nodes
41
48
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
49
+ from .helpers import (
50
+ NA_PRESERVING_INT_TYPES,
51
+ evaluate_args_once,
52
+ na_preserving_int_cast,
53
+ pine_truth_cast,
54
+ )
42
55
  from .. import signatures as sigs
43
56
  from .tables import (
57
+ ALERT_FREQ_VALUES,
58
+ ARRAY_ARGS_READ_REPEATEDLY,
44
59
  ARRAY_DRAWING_NEW_CTORS,
45
60
  ARRAY_METHODS,
61
+ V5_ARRAY_INDEX_METHODS,
46
62
  BAR_BUILTINS,
47
63
  BAR_FIELDS,
64
+ COLOR_CONST_MAP,
48
65
  DRAWING_NS,
49
66
  DRAWING_TYPE_TO_CPP,
50
67
  MATRIX_RETURNING_METHODS,
@@ -71,6 +88,18 @@ COLLECTION_MUTATING_METHODS = frozenset({
71
88
  })
72
89
 
73
90
 
91
+ # Binary operators whose emitted C++ result is a ``bool``, never a number.
92
+ # Pine's ``and``/``or`` lower to ``&&``/``||`` and the relational operators go
93
+ # through the na-aware comparison lambda, which returns ``bool``.
94
+ _BOOL_RESULT_BINOPS = frozenset({
95
+ "==", "!=", "<", ">", "<=", ">=", "and", "or", "&&", "||",
96
+ })
97
+
98
+
99
+ # Array methods whose result is a new array (a vector of doubles, as their
100
+ # ``ARRAY_METHODS`` templates emit it).
101
+ ARRAY_RESULT_METHODS = frozenset({"abs", "standardize", "sort_indices"})
102
+
74
103
  class TypeInferer:
75
104
  """Type-spec / C++-type inference helpers shared across visitor mixins.
76
105
 
@@ -137,14 +166,15 @@ class TypeInferer:
137
166
  if spec.kind == "primitive":
138
167
  return {"float": "double", "int": "int", "int64": "int64_t",
139
168
  "bool": "bool", "string": "std::string",
140
- "color": "int"}.get(spec.name or "float", "double")
169
+ "color": "int64_t"}.get(spec.name or "float", "double")
141
170
  if spec.kind == "udt" and spec.name:
142
171
  # Drawing handle structs (P1): map BEFORE the _udt_defs check so
143
172
  # array<line> -> std::vector<Line> and scalar line -> Line instead
144
173
  # of the old collapse to double / unknown-type-name.
145
174
  if spec.name in DRAWING_TYPE_TO_CPP:
146
175
  return DRAWING_TYPE_TO_CPP[spec.name]
147
- return spec.name if spec.name in self._udt_defs else "double"
176
+ return (self._safe_name(spec.name)
177
+ if spec.name in self._udt_defs else "double")
148
178
  if spec.kind == "array":
149
179
  return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
150
180
  if spec.kind == "map":
@@ -172,7 +202,7 @@ class TypeInferer:
172
202
  """Generated arena specialization for one user-defined object type."""
173
203
  return (
174
204
  f"{getattr(self, '_udt_arena_template_cpp_name', '_PFUdtArena')}<"
175
- f"{type_name}, "
205
+ f"{self._safe_name(type_name)}, "
176
206
  f"{self._udt_record_cpp_type(type_name)}>"
177
207
  )
178
208
 
@@ -182,6 +212,227 @@ class TypeInferer:
182
212
  self, "_checkpoint_traits_cpp_name", "_PFCheckpointTraits"
183
213
  )
184
214
 
215
+ def _udt_array_field_cpp(self, spec: TypeSpec | None,
216
+ type_name: str | None = None) -> str | None:
217
+ """The record type of an array field of the UDT ``type_name``
218
+ (``_PFArrayField<T>``, base.UDT_ARRAY_FIELD_CPP) when the type is
219
+ bar-local (``_udt_bar_local_types``), else None: its field stays the
220
+ ``std::vector<T>`` copy every earlier build stored."""
221
+ if spec is None or spec.kind != "array" or spec.element is None:
222
+ return None
223
+ if type_name not in self._udt_bar_local_types():
224
+ return None
225
+ return f"_PFArrayField<{self._type_spec_to_cpp(spec.element)}>"
226
+
227
+ def _udt_has_array_fields(self) -> bool:
228
+ """Whether any emitted UDT record holds a ``_PFArrayField``."""
229
+ return any(
230
+ self._udt_array_field_cpp(spec, type_name) is not None
231
+ for type_name, specs in self._udt_field_type_specs.items()
232
+ if type_name in self._udt_defs
233
+ for name, spec in specs.items()
234
+ if name not in self._udt_omitted_fields.get(type_name, set())
235
+ )
236
+
237
+ def _udt_array_field_target(self, node) -> str | None:
238
+ """The ``_PFArrayField<T>`` type of the UDT field ``node`` reads, when
239
+ it is one, else None."""
240
+ if not isinstance(node, MemberAccess) or self._is_omitted_udt_field(node):
241
+ return None
242
+ owner_spec = self._type_spec_from_expr(node.object)
243
+ if (owner_spec is None or owner_spec.kind != "udt"
244
+ or owner_spec.name not in self._udt_defs):
245
+ return None
246
+ return self._udt_array_field_cpp(
247
+ self._udt_field_type_specs.get(owner_spec.name, {}).get(node.member),
248
+ owner_spec.name)
249
+
250
+ # Builtin namespaces whose calls never return a user object.
251
+ _NO_USER_OBJECT_NAMESPACES = frozenset({
252
+ "ta", "math", "str", "color", "input", "timeframe", "syminfo",
253
+ "strategy", "table", "line", "label", "box", "linefill", "polyline",
254
+ "chart", "session", "ticker", "log", "runtime", "alert",
255
+ })
256
+
257
+ _PRIMITIVE_RESULTS = frozenset({
258
+ PineType.INT, PineType.FLOAT, PineType.BOOL, PineType.STRING,
259
+ PineType.COLOR,
260
+ })
261
+
262
+ def _may_hold_user_object(self, expr, seen: frozenset = frozenset()) -> bool:
263
+ """Whether ``expr`` can evaluate to a user-defined object: not a
264
+ literal, an operator's result, a builtin that returns a primitive or
265
+ a drawing, or a user function whose result is one of those (its UDT
266
+ or drawing result, a primitive result type, else what its last
267
+ expression can hold: ``pick(o) => o`` can return an object)."""
268
+ if expr is None or isinstance(expr, (
269
+ NumberLiteral, StringLiteral, BoolLiteral, ColorLiteral,
270
+ NaLiteral, UnaryOp, BinOp)):
271
+ return False
272
+ if isinstance(expr, FuncCall):
273
+ func_name, namespace = self._resolve_callee(expr.callee)
274
+ if namespace is None and func_name in self._func_info_map:
275
+ info = self._func_info_map[func_name]
276
+ returned = getattr(info, "udt_return_type", None)
277
+ if returned is not None:
278
+ return returned in self._udt_defs
279
+ if getattr(info, "return_type", None) in self._PRIMITIVE_RESULTS:
280
+ return False
281
+ body = getattr(getattr(info, "node", None), "body", None) or ()
282
+ if not body or func_name in seen:
283
+ return True
284
+ terminal = body[-1]
285
+ if isinstance(terminal, ExprStmt):
286
+ terminal = terminal.expr
287
+ return self._may_hold_user_object(terminal, seen | {func_name})
288
+ return namespace not in self._NO_USER_OBJECT_NAMESPACES
289
+ if isinstance(expr, Ternary):
290
+ return (self._may_hold_user_object(expr.true_val, seen)
291
+ or self._may_hold_user_object(expr.false_val, seen))
292
+ return True
293
+
294
+ def _udt_bar_local_types(self) -> set[str]:
295
+ """User types none of whose objects outlives the bar that creates it.
296
+
297
+ TradingView keeps a ``var`` array's value per bar: an object kept from
298
+ an earlier bar reads the array as it was then, while one built this
299
+ bar holds the array itself (lab tv pf-cgs2-udt-array-fields: a push
300
+ through ``h.xs`` reaches ``a``; a record kept from the first bar reads
301
+ one element forever). An object nothing keeps past its bar can alias
302
+ the array it is built from, which is exact and copies nothing; any
303
+ other object keeps the copy every earlier build stored. A type escapes
304
+ its bar when a ``var`` / ``varip`` declaration can hold one, a
305
+ collection's element type or another type's field is it, a history
306
+ read reads one, or a ``var`` declaration of a type this cannot tell
307
+ exists. Cached."""
308
+ cached = getattr(self, "_udt_bar_local_cache", None)
309
+ if cached is not None:
310
+ return cached
311
+ types = set(self._udt_defs)
312
+ self._udt_bar_local_cache = set()
313
+ escaping: set[str] = set()
314
+
315
+ def udt_of_hint(hint) -> set[str]:
316
+ if not hint:
317
+ return set()
318
+ return {name for name in types
319
+ if re.search(rf"(?<![A-Za-z0-9_.]){re.escape(name)}(?![A-Za-z0-9_])",
320
+ str(hint))}
321
+
322
+ def udt_of_spec(spec) -> set[str]:
323
+ found = set()
324
+ pending = [spec]
325
+ while pending:
326
+ current = pending.pop()
327
+ if current is None:
328
+ continue
329
+ if current.kind == "udt" and current.name in types:
330
+ found.add(current.name)
331
+ pending.extend((current.element, current.key, current.value))
332
+ return found
333
+
334
+ for type_name, fields in self._udt_defs.items():
335
+ for field in fields or ():
336
+ escaping |= udt_of_hint(getattr(field, "type_name", None))
337
+ ast = getattr(self.ctx, "ast", None)
338
+ for node in self._walk_ast(ast):
339
+ if isinstance(node, VarDecl):
340
+ hint = str(node.type_hint or "")
341
+ if any(mark in hint for mark in ("<", "[")):
342
+ # A collection of objects keeps them past their bar.
343
+ escaping |= udt_of_hint(hint)
344
+ if not (node.is_var or node.is_varip):
345
+ continue
346
+ if hint:
347
+ escaping |= udt_of_hint(hint)
348
+ continue
349
+ try:
350
+ spec = self._type_spec_from_expr(node.value)
351
+ except Exception as exc: # an unresolvable value may hold any type
352
+ if getattr(exc, "limit", False):
353
+ raise
354
+ spec = None
355
+ if spec is None and isinstance(node.value, FuncCall):
356
+ func_name, namespace = self._resolve_callee(node.value.callee)
357
+ info = (self._func_info_map.get(func_name)
358
+ if namespace is None else None)
359
+ returned = getattr(info, "udt_return_type", None)
360
+ if returned in types:
361
+ escaping.add(returned)
362
+ continue
363
+ if spec is None and self._may_hold_user_object(node.value):
364
+ return self._udt_bar_local_cache
365
+ escaping |= udt_of_spec(spec)
366
+ elif isinstance(node, FuncCall):
367
+ func_name, namespace = self._resolve_callee(node.callee)
368
+ if namespace in ("array", "map", "matrix") or func_name in (
369
+ "push", "unshift", "insert", "set", "fill", "put"):
370
+ for template in self._template_args_from_call(node) or ():
371
+ escaping |= udt_of_hint(template)
372
+ for arg in [*node.args, *node.kwargs.values()]:
373
+ try:
374
+ escaping |= udt_of_spec(self._type_spec_from_expr(arg))
375
+ except Exception as exc:
376
+ if getattr(exc, "limit", False):
377
+ raise
378
+ continue
379
+ elif isinstance(node, Subscript):
380
+ try:
381
+ escaping |= udt_of_spec(self._type_spec_from_expr(node.object))
382
+ except Exception as exc:
383
+ if getattr(exc, "limit", False):
384
+ raise
385
+ continue
386
+ self._udt_bar_local_cache = types - escaping
387
+ return self._udt_bar_local_cache
388
+
389
+ def _stable_var_array_names(self) -> set[str]:
390
+ """Top-level ``var`` arrays no statement rebinds (``:=``): one array
391
+ object for the whole run, whose member storage keeps that identity,
392
+ so a UDT field can alias it. Cached."""
393
+ cached = getattr(self, "_stable_var_array_cache", None)
394
+ if cached is not None:
395
+ return cached
396
+ ast = getattr(self.ctx, "ast", None)
397
+ top_level_vars = {
398
+ stmt.name for stmt in getattr(ast, "body", None) or ()
399
+ if isinstance(stmt, VarDecl) and stmt.is_var and not stmt.is_varip
400
+ }
401
+ declarations: dict[str, int] = {}
402
+ rebound: set[str] = set()
403
+ for node in self._walk_ast(ast):
404
+ if isinstance(node, VarDecl):
405
+ declarations[node.name] = declarations.get(node.name, 0) + 1
406
+ elif (isinstance(node, Assignment)
407
+ and isinstance(node.target, Identifier)):
408
+ rebound.add(node.target.name)
409
+ names = set()
410
+ for name in top_level_vars:
411
+ # Declared once in the whole program, so no local shadows it.
412
+ if declarations.get(name) != 1 or name in rebound:
413
+ continue
414
+ spec = self._collection_spec_for_name_raw(name)
415
+ if spec is not None and spec.kind == "array":
416
+ names.add(name)
417
+ self._stable_var_array_cache = names
418
+ return names
419
+
420
+ def _udt_array_field_value(self, field_cpp: str, value_node, value_cpp: str) -> str:
421
+ """The ``_PFArrayField<T>`` a UDT array field takes from ``value_node``:
422
+ an alias of a stable script ``var`` array (``_stable_var_array_names``)
423
+ read at the script's level of names, else the value itself, moved or
424
+ copied in (the snapshot every earlier build stored)."""
425
+ name = value_node.name if isinstance(value_node, Identifier) else None
426
+ if (name is not None
427
+ and name in self._stable_var_array_names()
428
+ and name not in (getattr(self, "_current_func_param_types", {}) or {})
429
+ and name not in (getattr(self, "_current_func_local_types", {}) or {})
430
+ and name not in (getattr(self, "_current_func_locals", ()) or ())
431
+ and name not in (getattr(self, "_current_loop_vars", ()) or ())
432
+ and not self._known_var_is_lexically_shadowed(name)):
433
+ return f"{field_cpp}::alias({value_cpp})"
434
+ return f"{field_cpp}({value_cpp})"
435
+
185
436
  def _udt_direct_array_fields(self, type_name: str) -> tuple[str, ...]:
186
437
  """Direct array-valued fields whose UDT copy semantics are unsupported.
187
438
 
@@ -261,7 +512,7 @@ class TypeInferer:
261
512
  # (Line{} = na handle), NOT the lowercase Pine name (line{}).
262
513
  if spec.name in DRAWING_TYPE_TO_CPP:
263
514
  return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
264
- return f"{spec.name}{{}}"
515
+ return f"{self._safe_name(spec.name)}{{}}"
265
516
  cpp_type = self._type_spec_to_cpp(spec)
266
517
  if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
267
518
  return f"{cpp_type}()"
@@ -294,8 +545,14 @@ class TypeInferer:
294
545
  def _int_fits_int32(value: int) -> bool:
295
546
  return -(1 << 31) <= value < (1 << 31)
296
547
 
297
- def _pure_int_literal_value(self, node) -> int | None:
298
- """Exact value of an expression built only from int literals and
548
+ @staticmethod
549
+ def _int_fits_int64(value: int) -> bool:
550
+ return -(1 << 63) <= value < (1 << 63)
551
+
552
+ def _pure_int_literal_value(self, node, names: bool = True,
553
+ global_scope: bool = False) -> int | None:
554
+ """Exact value of an expression built only from int literals, the
555
+ int constants ``_visit_ident`` inlines as literals (``names``), and
299
556
  ``+ - *`` / unary minus, else ``None``.
300
557
 
301
558
  Pine ``int`` is 64-bit: ``90 * 24 * 60 * 60 * 1000`` (three months in
@@ -303,21 +560,30 @@ class TypeInferer:
303
560
  literal arithmetic the same product overflows (wraps to -813 934 592),
304
561
  so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8
305
562
  family U: latibonit15 execution-signals-confluence, six lanes; lab tv
306
- u-lati-levels-nq15 vs the engine, 2026-09-05). Folding the literal
307
- subtree in Python keeps the exact value; the caller emits it as a
563
+ u-lati-levels-nq15 vs the engine, 2026-09-05). ``400 * MS`` over
564
+ ``const int MS = 7200000`` is 2 880 000 000 too (lab tv
565
+ pf-cgs-int64-const, 2026-09-28): the constant is emitted as its
566
+ literal, so the product was the same C++ ``int`` overflow. Folding
567
+ the subtree in Python keeps the exact value; the caller emits it as a
308
568
  64-bit literal only when it does not fit ``int32`` so every in-range
309
- expression is byte-identical to before.
569
+ expression is byte-identical to before. ``global_scope`` reads names
570
+ as the script's top level binds them, whatever is being emitted.
310
571
  """
311
572
  if isinstance(node, NumberLiteral):
312
573
  return node.value if isinstance(node.value, int) and not isinstance(node.value, bool) else None
574
+ if isinstance(node, Identifier):
575
+ if not names:
576
+ return None
577
+ return (self._global_int_constant(node.name) if global_scope
578
+ else self._inlined_int_constant(node))
313
579
  if isinstance(node, UnaryOp) and node.op == "-":
314
- inner = self._pure_int_literal_value(node.operand)
580
+ inner = self._pure_int_literal_value(node.operand, names, global_scope)
315
581
  return -inner if inner is not None else None
316
582
  if isinstance(node, BinOp) and node.op in ("+", "-", "*"):
317
- left = self._pure_int_literal_value(node.left)
583
+ left = self._pure_int_literal_value(node.left, names, global_scope)
318
584
  if left is None:
319
585
  return None
320
- right = self._pure_int_literal_value(node.right)
586
+ right = self._pure_int_literal_value(node.right, names, global_scope)
321
587
  if right is None:
322
588
  return None
323
589
  if node.op == "+":
@@ -327,10 +593,528 @@ class TypeInferer:
327
593
  return left * right
328
594
  return None
329
595
 
330
- def _literal_overflows_int32(self, node) -> bool:
331
- value = self._pure_int_literal_value(node)
596
+ def _inlined_int_constant(self, node: Identifier) -> int | None:
597
+ """The int a name is emitted as when ``_visit_ident`` inlines it as a
598
+ literal (a never-reassigned constant: ``const int MS = 7200000``,
599
+ ``int step = 2 * 60 * 60 * 1000``), else None. Every read that
600
+ ``_visit_expr`` / ``_visit_ident`` renders otherwise is None: a
601
+ dropped no-data request, a ``request.security`` fallback frame, a
602
+ parameter, a bar field or built-in, an input, and a local that
603
+ shadows the constant."""
604
+ if node.annotations and UNPINNED_ANNOTATION in node.annotations:
605
+ return None
606
+ if getattr(self, "_security_fallback_frame", None) is not None:
607
+ return None
608
+ name = node.name
609
+ if name == "na" or name in getattr(self, "_pending_decl_outer_alias", {}):
610
+ return None
611
+ if (name in getattr(self, "_current_func_series_params", ())
612
+ or name in getattr(self, "_current_func_param_types", ())
613
+ or name in BAR_FIELDS or name in BAR_BUILTINS):
614
+ return None
615
+ known = getattr(self, "_known_vars", {})
616
+ if (name not in known
617
+ or name in getattr(self, "_input_backed_vars", ())
618
+ or self._known_var_is_lexically_shadowed(name)):
619
+ return None
620
+ value = known[name]
621
+ if isinstance(value, bool) or not isinstance(value, int):
622
+ return None
623
+ return value
624
+
625
+ def _global_int_constant(self, name: str) -> int | None:
626
+ """The int constant a top-level name is inlined as (``_known_vars``,
627
+ not an input, a bar field or a built-in), read at the top level."""
628
+ if name == "na" or name in BAR_FIELDS or name in BAR_BUILTINS:
629
+ return None
630
+ known = getattr(self, "_known_vars", {})
631
+ if name not in known or name in getattr(self, "_input_backed_vars", ()):
632
+ return None
633
+ value = known[name]
634
+ if isinstance(value, bool) or not isinstance(value, int):
635
+ return None
636
+ return value
637
+
638
+ def _literal_overflows_int32(self, node, names: bool = False) -> bool:
639
+ value = self._pure_int_literal_value(node, names)
332
640
  return value is not None and not self._int_fits_int32(value)
333
641
 
642
+ def _literal_wide_global(self, name: str, decl=None) -> bool:
643
+ """Whether the top-level declaration of ``name`` gives it an int
644
+ constant past int32 -- a literal (``g = 3000000000``) or constant
645
+ arithmetic (``var int e = 300 * MS``): its slot is ``int64_t``,
646
+ reassigned or not. Only that slot: the width is not a provenance
647
+ that travels by spelling through copies, parameters, arrays and
648
+ helper state (``_wide_int_provenance``), whose consumers keep their
649
+ types; a callable's or a block's declaration keeps its type too, and
650
+ so does a callable's local or parameter of that name where it is in
651
+ scope. With ``decl``, only when ``decl`` is that declaration."""
652
+ widths = getattr(self, "_top_level_constant_widths", None)
653
+ if widths is None:
654
+ widths = self._top_level_constant_widths = {}
655
+ ast = getattr(self.ctx, "ast", None)
656
+ for stmt in getattr(ast, "body", None) or ():
657
+ if (isinstance(stmt, VarDecl) and stmt.value is not None
658
+ and stmt.name not in widths):
659
+ value = self._pure_int_literal_value(stmt.value, global_scope=True)
660
+ widths[stmt.name] = (
661
+ stmt, value is not None and not self._int_fits_int32(value))
662
+ top = widths.get(name)
663
+ if top is None or not top[1]:
664
+ return False
665
+ if decl is not None:
666
+ return decl is top[0]
667
+ return not (
668
+ self._known_var_is_lexically_shadowed(name)
669
+ or name in getattr(self, "_current_func_param_types", ())
670
+ or name in getattr(self, "_current_func_series_params", ())
671
+ or name in getattr(self, "_current_func_locals", ())
672
+ or name in getattr(self, "_current_func_local_types", {})
673
+ )
674
+
675
+ # C++ text of an ``int`` expression built from decimal literals only.
676
+ _INT_LITERAL_CPP = re.compile(r"[\s()0-9+\-*]+")
677
+
678
+ @classmethod
679
+ def _int32_literal_cpp_value(cls, cpp: str) -> int | None:
680
+ """Exact value of C++ text built from decimal literals, ``+ - *`` and
681
+ parentheses that C++ evaluates in ``int`` (every literal and every
682
+ partial result fits int32), else None."""
683
+ if not cls._INT_LITERAL_CPP.fullmatch(cpp or ""):
684
+ return None
685
+ import ast as _ast
686
+ try:
687
+ tree = _ast.parse(cpp.strip(), mode="eval")
688
+ except SyntaxError:
689
+ return None
690
+
691
+ def value(node):
692
+ if isinstance(node, _ast.Constant) and type(node.value) is int:
693
+ result = node.value
694
+ elif isinstance(node, _ast.UnaryOp) and isinstance(node.op, (_ast.USub, _ast.UAdd)):
695
+ inner = value(node.operand)
696
+ if inner is None:
697
+ return None
698
+ result = -inner if isinstance(node.op, _ast.USub) else inner
699
+ elif isinstance(node, _ast.BinOp) and isinstance(
700
+ node.op, (_ast.Add, _ast.Sub, _ast.Mult)):
701
+ left, right = value(node.left), value(node.right)
702
+ if left is None or right is None:
703
+ return None
704
+ result = (left + right if isinstance(node.op, _ast.Add)
705
+ else left - right if isinstance(node.op, _ast.Sub)
706
+ else left * right)
707
+ else:
708
+ return None
709
+ return result if cls._int_fits_int32(result) else None
710
+
711
+ return value(tree.body)
712
+
713
+ def _fold_int32_overflow_cpp(self, op: str, left_cpp: str, right_cpp: str) -> str | None:
714
+ """``left op right`` as a 64-bit literal when both sides are C++
715
+ ``int`` literal arithmetic whose exact result leaves int32 (Pine's
716
+ ``int`` is 64-bit; C++ would overflow), else None. The emitted text
717
+ is what decides: a name inlined as its literal, a global expanded
718
+ into its declaration or a helper parameter bound to a literal all
719
+ read as literals here."""
720
+ if op not in ("+", "-", "*"):
721
+ return None
722
+ left = self._int32_literal_cpp_value(left_cpp)
723
+ if left is None:
724
+ return None
725
+ right = self._int32_literal_cpp_value(right_cpp)
726
+ if right is None:
727
+ return None
728
+ result = left + right if op == "+" else left - right if op == "-" else left * right
729
+ if self._int_fits_int32(result):
730
+ return None
731
+ return f"static_cast<int64_t>({result}LL)"
732
+
733
+ # A runtime integer the C++ holds in 32 bits (bar_index, a counter, a loop
734
+ # binder, an input without a declared range) is taken to stay within 2**24
735
+ # (16 777 216: more bars than thirty years of one-minute data).
736
+ _RUNTIME_INT_BOUND = 1 << 24
737
+
738
+ def _int_arith_leaves_int32(self, node, owner_info=None) -> bool:
739
+ """Whether ``node`` is an integer ``+ - *`` the C++ would compute in
740
+ 32-bit ``int`` arithmetic -- both operands are 32-bit integers
741
+ (``_narrow_int_bound``) -- while its magnitude can leave int32.
742
+
743
+ Pine's ``int`` is 64-bit: ``days * 86400000`` over ``days =
744
+ input.int(30)`` is 2592000000 on TradingView, and ``bar_index *
745
+ 7200000`` passes int32 at bar 299 (lab tv pf-cgs2-int64-products).
746
+ Such a node is computed in 64 bits (``_visit_binop``) and its value
747
+ is wide wherever it is stored (``_expr_returns_wide_int``). A node
748
+ whose magnitude fits int32 keeps its spelling."""
749
+ if not (isinstance(node, BinOp) and node.op in ("+", "-", "*")):
750
+ return False
751
+ bound = self._int_arith_bound(node, owner_info)
752
+ return bound is not None and bound > (1 << 31) - 1
753
+
754
+ def _int_arith_bound(self, node: BinOp, owner_info=None) -> int | None:
755
+ """The bound on ``|value|`` of a ``+ - *`` over two 32-bit integer
756
+ operands, uncapped; None when an operand is not one."""
757
+ left = self._narrow_int_bound(node.left, owner_info)
758
+ if left is None:
759
+ return None
760
+ right = self._narrow_int_bound(node.right, owner_info)
761
+ if right is None:
762
+ return None
763
+ return left * right if node.op == "*" else left + right
764
+
765
+ def _narrow_int_bound(self, node, owner_info=None) -> int | None:
766
+ """A bound on ``|value|`` of an integer expression the C++ holds in a
767
+ 32-bit ``int``, else None (a double, a 64-bit integer, or a shape this
768
+ does not know). Literals and inlined constants are exact, an
769
+ ``input.int`` is its declared range (its default and 2**24 without
770
+ one), and any other runtime integer is ``_RUNTIME_INT_BOUND``."""
771
+ if isinstance(node, NumberLiteral):
772
+ value = node.value
773
+ if type(value) is int and self._int_fits_int32(value):
774
+ return abs(value)
775
+ return None
776
+ if isinstance(node, UnaryOp):
777
+ if node.op != "-":
778
+ return None
779
+ return self._narrow_int_bound(node.operand, owner_info)
780
+ if isinstance(node, BinOp):
781
+ if node.op not in ("+", "-", "*"):
782
+ return None
783
+ bound = self._int_arith_bound(node, owner_info)
784
+ # A node past int32 is computed in 64 bits: no longer 32-bit.
785
+ return bound if bound is not None and bound <= (1 << 31) - 1 else None
786
+ if isinstance(node, Ternary):
787
+ arms = [self._narrow_int_bound(arm, owner_info)
788
+ for arm in (node.true_val, node.false_val)]
789
+ return None if None in arms else max(arms)
790
+ if isinstance(node, Identifier):
791
+ return self._narrow_int_name_bound(node.name, owner_info)
792
+ return None
793
+
794
+ @contextlib.contextmanager
795
+ def _int_width_scan(self, epoch_only: bool | None = None):
796
+ """The scope of a whole-script width cache computation: names resolve
797
+ against the scanned code's owner, not the callable being emitted
798
+ (``_narrow_int_name_info``); ``epoch_only`` sets the epoch-only mode
799
+ (``_wide_int_array_names``) for its duration."""
800
+ self._int_width_scan_depth = getattr(self, "_int_width_scan_depth", 0) + 1
801
+ saved = getattr(self, "_wide_int_epoch_only", False)
802
+ if epoch_only is not None:
803
+ self._wide_int_epoch_only = epoch_only
804
+ try:
805
+ yield
806
+ finally:
807
+ self._int_width_scan_depth -= 1
808
+ self._wide_int_epoch_only = saved
809
+
810
+ def _narrow_int_name_bound(self, name: str, owner_info=None) -> int | None:
811
+ """``_narrow_int_bound`` of a bare name (``_narrow_int_name_info``)."""
812
+ info = self._narrow_int_name_info(name, owner_info)
813
+ return info[0] if info is not None else None
814
+
815
+ def _narrow_int_name_info(self, name: str,
816
+ owner_info=None) -> tuple[int, bool] | None:
817
+ """``(bound, may_be_na)`` of a bare name the C++ holds in a 32-bit
818
+ ``int``: a parameter or local of the callable being emitted (or
819
+ ``owner_info``'s), a counted loop's binder, an inlined int constant,
820
+ an input, ``bar_index`` / ``last_bar_index``, or a script variable
821
+ whose slot is a C++ ``int``; None for any other name. A parameter, a
822
+ local and a script variable can hold ``na``; a binder, a constant, an
823
+ input and the bar index cannot.
824
+
825
+ While a width cache is computed (``_int_width_scan``) the callable
826
+ being emitted is not the one the scanned code belongs to, so only
827
+ ``owner_info`` resolves a callable's names."""
828
+ runtime = self._RUNTIME_INT_BOUND
829
+ # ``owner_info`` names the callable the name belongs to: resolve it
830
+ # there, whichever callable is being emitted.
831
+ emitting = (getattr(self, "_int_width_scan_depth", 0) == 0
832
+ and owner_info is None)
833
+ owner_node = getattr(owner_info, "node", None)
834
+ param_types = (getattr(self, "_current_func_param_types", {}) or {}
835
+ if emitting else {})
836
+ if name in param_types or (emitting and name in (
837
+ getattr(self, "_current_func_series_params", ()) or ())):
838
+ return (runtime, True) if param_types.get(name) == "int" else None
839
+ if owner_node is not None and name in owner_node.params:
840
+ index = owner_node.params.index(name)
841
+ if (getattr(owner_info, "is_udt_method", False) and index == 0) \
842
+ or name in self.ctx.func_series_vars.get(owner_info.name, set()) \
843
+ or (owner_info.name, index) in self._wide_int_provenance()[1]:
844
+ return None
845
+ declared = list(getattr(self.ctx, "func_declared_param_type_specs",
846
+ {}).get(owner_info.name, ()))
847
+ spec = declared[index] if index < len(declared) else None
848
+ if spec is None:
849
+ specs = list(getattr(owner_info, "param_type_specs", ()) or ())
850
+ spec = specs[index] if index < len(specs) else None
851
+ if (spec is not None and spec.kind == "primitive"
852
+ and spec.name == "int"):
853
+ return runtime, True
854
+ if (spec is None and self._func_param_int_cpp_type(
855
+ owner_info, index, None) == "int"):
856
+ # An untyped parameter whose calls carry no TypeSpec
857
+ # (``toMs(year(time))``) is the emitter's ``int`` still.
858
+ return runtime, True
859
+ return None
860
+ if emitting and name in (
861
+ getattr(self, "_current_counted_loop_vars", ()) or ()):
862
+ # A counted loop's binder is ``for (int i = ...)``.
863
+ return runtime, False
864
+ if emitting and name in (getattr(self, "_current_loop_vars", ()) or ()):
865
+ # A ``for ... in`` binder: an element, a map entry or an index,
866
+ # 32-bit only when its spec says ``int`` (a float element is a
867
+ # double), and an element can be na.
868
+ spec = (getattr(self, "_current_loop_var_specs", {}) or {}).get(name)
869
+ if (spec is not None and spec.kind == "primitive"
870
+ and spec.name == "int"):
871
+ return runtime, True
872
+ return None
873
+ local_types = (getattr(self, "_current_func_local_types", {}) or {}
874
+ if emitting else {})
875
+ if name in local_types or (emitting and name in (
876
+ getattr(self, "_current_func_locals", ()) or ())):
877
+ return (runtime, True) if local_types.get(name) == "int" else None
878
+ if owner_node is not None:
879
+ hints = {str(child.type_hint or "") for child in
880
+ self._walk_ast_list(owner_node.body)
881
+ if isinstance(child, VarDecl) and child.name == name}
882
+ if hints:
883
+ # A local of ``owner_info`` read outside its emission (its
884
+ # result type is decided before its locals are registered):
885
+ # a declared ``int`` is 32-bit, anything else unknown.
886
+ return (runtime, True) if hints == {"int"} else None
887
+ if name in BAR_FIELDS:
888
+ return None
889
+ if name in ("bar_index", "last_bar_index"):
890
+ return runtime, False
891
+ if name in BAR_BUILTINS or name in self._INTEGRAL_BUILTINS:
892
+ return None
893
+ if owner_node is None and self._known_var_is_lexically_shadowed(name):
894
+ # A block's local of that spelling: its type is not the global's.
895
+ return None
896
+ known = getattr(self, "_known_vars", {})
897
+ if name in getattr(self, "_input_backed_vars", ()):
898
+ bound = self._input_int_bound(name)
899
+ return (bound, False) if bound is not None else None
900
+ if name in known:
901
+ value = known[name]
902
+ if type(value) is int and self._int_fits_int32(value):
903
+ return abs(value), False
904
+ return None
905
+ if name in self._wide_int_provenance()[0]:
906
+ return None
907
+ if (getattr(self, "_int_width_scan_depth", 0) == 0
908
+ and self._literal_wide_global(name)):
909
+ # A top-level int constant past int32 initializes an int64_t
910
+ # slot (CGINT5a), reassigned or not: where it is emitted it is no
911
+ # 32-bit operand. A width scan keeps it one: it cannot tell the
912
+ # block local that may shadow it there (``int g = bar_index``),
913
+ # and a product it reads is 64-bit either way.
914
+ return None
915
+ return ((runtime, True)
916
+ if self._slot_scalar_cpp_type(name) == "int" else None)
917
+
918
+ def _int_operand_may_be_na(self, node, owner_info=None) -> bool:
919
+ """Whether a 32-bit int operand ``_narrow_int_bound`` accepts can be
920
+ ``na``: a leaf that is a parameter, a local or a script variable
921
+ (``_narrow_int_name_info``). Literals, inlined constants, inputs, a
922
+ counted loop's binder and the bar index cannot."""
923
+ if isinstance(node, NumberLiteral):
924
+ return False
925
+ if isinstance(node, UnaryOp):
926
+ return self._int_operand_may_be_na(node.operand, owner_info)
927
+ if isinstance(node, BinOp):
928
+ return (self._int_operand_may_be_na(node.left, owner_info)
929
+ or self._int_operand_may_be_na(node.right, owner_info))
930
+ if isinstance(node, Ternary):
931
+ return (self._int_operand_may_be_na(node.true_val, owner_info)
932
+ or self._int_operand_may_be_na(node.false_val, owner_info))
933
+ if isinstance(node, Identifier):
934
+ info = self._narrow_int_name_info(node.name, owner_info)
935
+ return info is None or info[1]
936
+ return True
937
+
938
+ def _na_aware_wide_is_double(self, node) -> bool:
939
+ """Whether ``node`` is a 64-bit ``+ - *`` (``_int_arith_leaves_int32``)
940
+ over an operand that can be na, which ``_wide_int_arith_cpp`` emits
941
+ as a double."""
942
+ return (isinstance(node, BinOp) and self._int_arith_leaves_int32(node)
943
+ and self._int_operand_may_be_na(node))
944
+
945
+ def _holds_na_aware_wide_double(self, expr) -> bool:
946
+ """Whether ``expr`` holds such a double-valued product: an integer
947
+ store of it narrows na-preserving (quirk 9), not implicitly."""
948
+ return expr is not None and any(
949
+ isinstance(sub, BinOp) and self._na_aware_wide_is_double(sub)
950
+ for sub, _depth in iter_ast_nodes(expr))
951
+
952
+ def _wide_int_arith_cpp(self, node, left: str, right: str, lower) -> str:
953
+ """The C++ of an integer ``+ - *`` computed in 64 bits
954
+ (``_int_arith_leaves_int32``): ``lower`` over the left operand cast to
955
+ ``int64_t``. An operand that can be ``na`` makes the value ``na``, as
956
+ Pine's arithmetic does: the widening ``static_cast`` reads the
957
+ sentinel ``na<int>()`` as the number -2147483648 (quirk 9). That form
958
+ is a ``double`` (``_emitted_value_is_double``): the slot such a value
959
+ lands in is a double as often as an ``int64_t`` (a global holding
960
+ integer arithmetic is declared from ``_infer_type``), and a double
961
+ keeps ``na`` where ``na<int64_t>()`` reads -9.2e18; an integer slot
962
+ narrows it na-preserving (every integer store of one does: arrays,
963
+ map and matrix values, parameters). A double is exact up to 2**53, a
964
+ product past it rounds."""
965
+ if not self._int_operand_may_be_na(node):
966
+ return lower(f"static_cast<int64_t>({left})", right)
967
+ return (f"[&]() -> double {{ auto _pf_wide_l = ({left}); "
968
+ f"auto _pf_wide_r = ({right}); "
969
+ f"return (is_na(_pf_wide_l) || is_na(_pf_wide_r)) "
970
+ f"? na<double>() : static_cast<double>("
971
+ f"{lower('static_cast<int64_t>(_pf_wide_l)', '_pf_wide_r')}); }}()")
972
+
973
+ def _input_int_bound(self, name: str) -> int | None:
974
+ """A bound on an int input's value: its declared range when it has
975
+ both ends, else its default and ``_RUNTIME_INT_BOUND`` (an override
976
+ can take any value). None for an input that is not an int."""
977
+ call = getattr(self, "_input_var_to_call", {}).get(name)
978
+ default = getattr(self, "_known_vars", {}).get(name)
979
+ if call is None or type(default) is not int:
980
+ return None
981
+ func_name, namespace = self._resolve_callee(call.callee)
982
+ if not ((namespace == "input" and func_name == "int")
983
+ or (namespace is None and func_name == "input")):
984
+ return None
985
+ ends = []
986
+ for index, key in ((2, "minval"), (3, "maxval")):
987
+ node = (call.args[index] if namespace == "input"
988
+ and len(call.args) > index else call.kwargs.get(key))
989
+ # An input is a top-level declaration: a named bound reads the
990
+ # top-level constant, whichever callable is being emitted.
991
+ value = (self._pure_int_literal_value(node, global_scope=True)
992
+ if node is not None else None)
993
+ if value is not None:
994
+ ends.append(abs(value))
995
+ if len(ends) == 2:
996
+ return max(abs(default), *ends)
997
+ return max(abs(default), self._RUNTIME_INT_BOUND, *ends)
998
+
999
+ def _holds_wide_int_constant(self, expr) -> bool:
1000
+ """Whether ``expr`` is, or selects, an int constant past int32
1001
+ (``3000000000``, ``c ? 0 : 400 * 7200000``): the slot holding its
1002
+ value -- a tuple element, a function's or method's result -- is
1003
+ ``int64_t``. Only that slot: the width does not travel through the
1004
+ spelling-keyed provenance (``_wide_int_provenance``)."""
1005
+ if isinstance(expr, ExprStmt):
1006
+ expr = expr.expr
1007
+ if isinstance(expr, NumberLiteral):
1008
+ return (type(expr.value) is int
1009
+ and not self._int_fits_int32(expr.value))
1010
+ if isinstance(expr, (BinOp, UnaryOp)):
1011
+ return self._literal_overflows_int32(expr)
1012
+ if isinstance(expr, Ternary):
1013
+ return (self._holds_wide_int_constant(expr.true_val)
1014
+ or self._holds_wide_int_constant(expr.false_val))
1015
+ return False
1016
+
1017
+ def _widen_int_slot(self, cpp_type: str, expr, owner_info=None) -> str:
1018
+ """``int64_t`` for an ``int`` slot ``expr`` gives a 64-bit value."""
1019
+ if cpp_type == "int" and (
1020
+ self._holds_wide_int_constant(expr)
1021
+ or self._expr_returns_wide_int(expr, owner_info, set(), None)):
1022
+ return "int64_t"
1023
+ return cpp_type
1024
+
1025
+ def _udt_receiver_type_name(self, receiver, owner_info=None) -> str | None:
1026
+ """The user type of a field read's receiver: a method's own receiver
1027
+ parameter by its declared type, anything else by its TypeSpec."""
1028
+ owner_node = getattr(owner_info, "node", None)
1029
+ if (getattr(owner_info, "is_udt_method", False)
1030
+ and owner_node is not None and owner_node.params
1031
+ and isinstance(receiver, Identifier)
1032
+ and receiver.name == owner_node.params[0]):
1033
+ specs = list(getattr(owner_info, "param_type_specs", ()) or ())
1034
+ return (method_receiver_type_name(specs[0] if specs else None)
1035
+ or getattr(owner_info, "udt_type_name", None))
1036
+ try:
1037
+ spec = self._type_spec_from_expr(receiver)
1038
+ except Exception as exc: # an unresolvable receiver holds no wide field
1039
+ if getattr(exc, "limit", False):
1040
+ raise
1041
+ return None
1042
+ if spec is not None and spec.kind == "udt":
1043
+ return spec.name
1044
+ return None
1045
+
1046
+ def _wide_udt_int_fields(self) -> set[tuple[str, str]]:
1047
+ """``(type, field)`` of the UDT ``int`` fields holding a 64-bit value:
1048
+ a default or a write (``T.new(...)``, ``obj.field := v``) that is a
1049
+ constant past int32 or a wide value (``_expr_returns_wide_int``).
1050
+ The field is ``int64_t`` storage already; its reads carry the width
1051
+ (``method big(U this) => this.v * 2`` over ``int v = 3000000000``
1052
+ is 6000000000 on TradingView). Iterated to a fixpoint and cached."""
1053
+ cached = getattr(self, "_wide_udt_int_field_cache", None)
1054
+ if cached is not None:
1055
+ return cached
1056
+ fields: set[tuple[str, str]] = set()
1057
+ self._wide_udt_int_field_cache = fields
1058
+ udt_defs = getattr(self, "_udt_defs", {}) or {}
1059
+ int_fields = {
1060
+ (type_name, field.name): index
1061
+ for type_name, decl_fields in udt_defs.items()
1062
+ for index, field in enumerate(decl_fields or ())
1063
+ if getattr(field, "type_name", "") == "int"
1064
+ }
1065
+ if not int_fields:
1066
+ return fields
1067
+ writes: list = []
1068
+ for (type_name, name), index in int_fields.items():
1069
+ default = udt_defs[type_name][index].default
1070
+ if default is not None:
1071
+ writes.append(((type_name, name), default, None))
1072
+
1073
+ def collect(node, owner) -> None:
1074
+ if (isinstance(node, FuncCall)
1075
+ and isinstance(node.callee, MemberAccess)
1076
+ and isinstance(node.callee.object, Identifier)
1077
+ and node.callee.member == "new"
1078
+ and node.callee.object.name in udt_defs):
1079
+ type_name = node.callee.object.name
1080
+ names = [f.name for f in udt_defs[type_name] or ()]
1081
+ bound = list(zip(names, node.args))
1082
+ bound += [(key, value) for key, value in node.kwargs.items()
1083
+ if key in names]
1084
+ for name, value in bound:
1085
+ if (type_name, name) in int_fields:
1086
+ writes.append(((type_name, name), value, owner))
1087
+ elif (isinstance(node, Assignment)
1088
+ and isinstance(node.target, MemberAccess)):
1089
+ type_name = self._udt_receiver_type_name(
1090
+ node.target.object, owner)
1091
+ if (type_name, node.target.member) in int_fields:
1092
+ writes.append(((type_name, node.target.member),
1093
+ node.value, owner))
1094
+
1095
+ for info in getattr(self.ctx, "func_infos", ()):
1096
+ node = getattr(info, "node", None)
1097
+ if node is not None:
1098
+ for child in self._walk_ast_list(node.body):
1099
+ collect(child, info)
1100
+ ast = getattr(self.ctx, "ast", None)
1101
+ for stmt in getattr(ast, "body", None) or ():
1102
+ if isinstance(stmt, (FuncDef, MethodDef)):
1103
+ continue
1104
+ for child in self._walk_ast(stmt):
1105
+ collect(child, None)
1106
+ changed = True
1107
+ with self._int_width_scan(epoch_only=False):
1108
+ while changed:
1109
+ changed = False
1110
+ for key, value, owner in writes:
1111
+ if key not in fields and (
1112
+ self._holds_wide_int_constant(value)
1113
+ or self._expr_returns_wide_int(value, owner, set(), None)):
1114
+ fields.add(key)
1115
+ changed = True
1116
+ return fields
1117
+
334
1118
  def _array_receiver_and_value(self, call):
335
1119
  """``(receiver_name, value_node)`` of an element-writing array call in
336
1120
  either form (``array.push(a, v)`` / ``a.push(v)``), else ``None``."""
@@ -364,7 +1148,15 @@ class TypeInferer:
364
1148
  ``set`` / ``insert`` / ``fill``, or is built by ``array.new_int`` /
365
1149
  ``array.new<int>`` / ``array.from`` from one. Their element type is
366
1150
  ``int64_t`` (TypeSpec primitive ``int64``): Pine's ``int`` holds the
367
- epoch, ``std::vector<int>`` truncates it. Cached on the instance."""
1151
+ epoch, ``std::vector<int>`` truncates it. Cached on the instance.
1152
+
1153
+ Only an epoch widens an array (the scan runs in the epoch-only mode):
1154
+ a declared ``array<int>`` parameter, result or field stays
1155
+ ``std::vector<int>``, which a widened argument does not bind to, so a
1156
+ value that is wide only by 64-bit arithmetic or a constant past int32
1157
+ (``bars.push(bar_index * 1000)``) keeps the array's C++ ``int``, is
1158
+ narrowed na-preserving where it is stored and warns
1159
+ (``_array_init_value_expr``)."""
368
1160
  cached = getattr(self, "_wide_int_array_cache", None)
369
1161
  if cached is not None:
370
1162
  return cached
@@ -415,16 +1207,17 @@ class TypeInferer:
415
1207
  names.add(target)
416
1208
 
417
1209
  ast = getattr(self.ctx, "ast", None)
418
- for _round in range(8):
419
- before = len(names)
420
- if ast is not None:
421
- scan(self._walk_ast(ast), None)
422
- for info in getattr(self.ctx, "func_infos", ()):
423
- node = getattr(info, "node", None)
424
- if node is not None:
425
- scan(self._walk_ast_list(node.body), info)
426
- if len(names) == before:
427
- break
1210
+ with self._int_width_scan(epoch_only=True):
1211
+ for _round in range(8):
1212
+ before = len(names)
1213
+ if ast is not None:
1214
+ scan(self._walk_ast(ast), None)
1215
+ for info in getattr(self.ctx, "func_infos", ()):
1216
+ node = getattr(info, "node", None)
1217
+ if node is not None:
1218
+ scan(self._walk_ast_list(node.body), info)
1219
+ if len(names) == before:
1220
+ break
428
1221
  return names
429
1222
 
430
1223
  def _widen_array_spec_for_name(self, name, spec):
@@ -592,6 +1385,11 @@ class TypeInferer:
592
1385
  return TypeSpec.primitive("int64")
593
1386
  return TypeSpec.primitive("int")
594
1387
  return None
1388
+ if self._is_color_value(node):
1389
+ # A color is a packed-ARGB ``int64_t``: ``array.from(color.red,
1390
+ # color.new(c, 50))`` is ``std::vector<int64_t>``, which a
1391
+ # ``double`` vector cannot be braced from.
1392
+ return TypeSpec.primitive("color")
595
1393
  spec = self._type_spec_from_expr(node)
596
1394
  if spec is not None:
597
1395
  return spec
@@ -611,6 +1409,40 @@ class TypeInferer:
611
1409
  return TypeSpec.primitive(primitive_name)
612
1410
  return None
613
1411
 
1412
+ def _is_color_value(self, node, _depth: int = 0) -> bool:
1413
+ """Whether ``node`` is a color value: a literal, a ``color.*``
1414
+ constant, a ``color.new`` / ``rgb`` / ``from_gradient`` call, a
1415
+ conditional selecting one, or a script variable declared ``color`` or
1416
+ bound to one (``c1 = color.new(...)``, which the analyzer types
1417
+ float)."""
1418
+ if _depth > 16 or node is None:
1419
+ return False
1420
+ if isinstance(node, ColorLiteral):
1421
+ return True
1422
+ if isinstance(node, MemberAccess):
1423
+ return (isinstance(node.object, Identifier)
1424
+ and node.object.name == "color"
1425
+ and node.member in COLOR_CONST_MAP)
1426
+ if isinstance(node, FuncCall):
1427
+ func_name, namespace = self._resolve_callee(node.callee)
1428
+ return namespace == "color" and func_name in (
1429
+ "new", "rgb", "from_gradient")
1430
+ if isinstance(node, Ternary):
1431
+ return (self._is_color_value(node.true_val, _depth + 1)
1432
+ or self._is_color_value(node.false_val, _depth + 1))
1433
+ if isinstance(node, Identifier):
1434
+ if self._slot_scalar_cpp_type(node.name) != "int64_t":
1435
+ return False
1436
+ sym = self._variable_symbol(node.name)
1437
+ if sym is not None and sym.pine_type == PineType.COLOR:
1438
+ return True
1439
+ if (node.name in getattr(self, "_current_func_local_types", {})
1440
+ or node.name in getattr(self, "_current_func_param_types", {})):
1441
+ return False
1442
+ value = getattr(self.ctx, "global_expr_map", {}).get(node.name)
1443
+ return self._is_color_value(value, _depth + 1)
1444
+ return False
1445
+
614
1446
  @staticmethod
615
1447
  def _selection_terminal_expr(body: list[ASTNode] | None) -> ASTNode | None:
616
1448
  """Return one if/switch branch's value expression, if present."""
@@ -650,6 +1482,13 @@ class TypeInferer:
650
1482
 
651
1483
  Returns ``None`` when the node's type cannot be narrowed beyond
652
1484
  the runtime default (most callers fall back to ``double``)."""
1485
+ budget = getattr(self, "_budget", None)
1486
+ if budget is not None:
1487
+ # A method chain re-infers each receiver more than once, so this
1488
+ # recursion can outgrow the visitor checkpoints.
1489
+ self._budget_visit_count += 1
1490
+ if self._budget_visit_count % 128 == 0:
1491
+ budget.check(getattr(node, "loc", None), Phase.CODEGEN)
653
1492
  if isinstance(node, NumberLiteral):
654
1493
  return TypeSpec.primitive("float" if isinstance(node.value, float) else "int")
655
1494
  if isinstance(node, BoolLiteral):
@@ -929,6 +1768,23 @@ class TypeInferer:
929
1768
  receiver_spec = self._type_spec_from_expr(receiver_node)
930
1769
  if receiver_spec is not None and receiver_spec.kind == "matrix":
931
1770
  return receiver_spec
1771
+ # ``matrix.row(m, i)`` / ``matrix.col`` / ``matrix.eigenvalues``
1772
+ # return arrays, as their method forms below do; an untyped
1773
+ # ``r = matrix.row(m, 0)`` was declared a double.
1774
+ if namespace == "matrix" and func_name in ("row", "col", "eigenvalues"):
1775
+ receiver_node = node.args[0] if node.args else node.kwargs.get("id")
1776
+ receiver_spec = self._type_spec_from_expr(receiver_node)
1777
+ if receiver_spec is not None and receiver_spec.kind == "matrix":
1778
+ if func_name == "eigenvalues":
1779
+ return TypeSpec.array(TypeSpec.primitive("float"))
1780
+ return TypeSpec.array(receiver_spec.element)
1781
+ # array.abs / standardize / sort_indices build a new array, which
1782
+ # their templates (``ARRAY_METHODS``) emit as a vector of doubles.
1783
+ if namespace == "array" and func_name in ARRAY_RESULT_METHODS:
1784
+ receiver_node = node.args[0] if node.args else node.kwargs.get("id")
1785
+ receiver_spec = self._type_spec_from_expr(receiver_node)
1786
+ if receiver_spec is not None and receiver_spec.kind == "array":
1787
+ return TypeSpec.array(TypeSpec.primitive("float"))
932
1788
  if namespace == "map" and func_name == "new":
933
1789
  key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
934
1790
  val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
@@ -967,6 +1823,8 @@ class TypeInferer:
967
1823
  return recv_spec.element
968
1824
  if member_name in ("copy", "slice"):
969
1825
  return recv_spec
1826
+ if member_name in ARRAY_RESULT_METHODS:
1827
+ return TypeSpec.array(TypeSpec.primitive("float"))
970
1828
  if recv_spec is not None and recv_spec.kind == "map":
971
1829
  if member_name in ("put", "get", "remove"):
972
1830
  return recv_spec.value
@@ -1124,7 +1982,8 @@ class TypeInferer:
1124
1982
  )
1125
1983
 
1126
1984
  def _array_method_expr(
1127
- self, array_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
1985
+ self, array_expr: str, method: str, args: list[str],
1986
+ spec: TypeSpec | None = None, node: ASTNode | None = None,
1128
1987
  ) -> str:
1129
1988
  """Lower ``arr.method(...)`` to its C++ form, validating numeric requirements."""
1130
1989
  spec = spec or TypeSpec.array(TypeSpec.primitive("float"))
@@ -1133,14 +1992,33 @@ class TypeInferer:
1133
1992
  if method == "copy":
1134
1993
  lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
1135
1994
  elif method == "slice":
1995
+ # TradingView aliases primitive source elements bidirectionally.
1996
+ # Our std::vector lowering owns a copy; a true view needs a
1997
+ # lifetime-safe collection representation across all array lanes.
1998
+ # Keep the compiling lowering and surface the measured divergence.
1999
+ warned = getattr(self, "_slice_alias_warned_nodes", set())
2000
+ if node is None or id(node) not in warned:
2001
+ self._codegen_warning(
2002
+ node,
2003
+ "array.slice returns a copy in PineForge; TradingView slices "
2004
+ "share source elements, so writes through either array can diverge.",
2005
+ hint="Avoid mutating a slice or its source while the slice is used.",
2006
+ )
2007
+ if node is not None:
2008
+ warned.add(id(node))
2009
+ self._slice_alias_warned_nodes = warned
1136
2010
  # Bounds-checked in the shared helper; the element type stays
1137
2011
  # caller-supplied so the typed lane keeps its own vector type.
1138
2012
  lower_receiver = lambda recv: checked_array_slice(
1139
2013
  recv, args, result_type=arr_cpp_type
1140
2014
  )
1141
2015
  elif method == "join" and elem_cpp == "std::string":
1142
- sep = args[0] if args else 'std::string(",")'
1143
- 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; }}()"
2016
+ join_args = list(args) or ['std::string(",")']
2017
+ lower_receiver = lambda recv: evaluate_args_once(
2018
+ join_args, ARRAY_ARGS_READ_REPEATEDLY["join"],
2019
+ lambda a: f"[&](){{ std::string __pf_r; for(size_t __pf_i=0;__pf_i<{recv}.size();__pf_i++){{ if(__pf_i>0)__pf_r+={a[0]}; __pf_r+={recv}[__pf_i]; }} return __pf_r; }}()",
2020
+ "_pf_array_a",
2021
+ )
1144
2022
  else:
1145
2023
  numeric_only = {
1146
2024
  "sum", "avg", "min", "max", "range", "stdev", "variance", "median",
@@ -1191,8 +2069,15 @@ class TypeInferer:
1191
2069
  arg_bindings.append((token, args[arg_index]))
1192
2070
  self._array_arg_counter = counter
1193
2071
 
2072
+ table = (V5_ARRAY_INDEX_METHODS
2073
+ if self._pine_v5_body and method in V5_ARRAY_INDEX_METHODS
2074
+ else ARRAY_METHODS)
2075
+
1194
2076
  def lower_receiver(recv: str) -> str:
1195
- lowered = ARRAY_METHODS[method](recv, bound_args)
2077
+ lowered = evaluate_args_once(
2078
+ bound_args, ARRAY_ARGS_READ_REPEATEDLY.get(method, ()),
2079
+ lambda a: table[method](recv, a), "_pf_array_a",
2080
+ )
1196
2081
  for token, original in reversed(arg_bindings):
1197
2082
  lowered = (
1198
2083
  f"[&](){{ auto {token}=({original}); "
@@ -1258,7 +2143,7 @@ class TypeInferer:
1258
2143
  owner,
1259
2144
  set(),
1260
2145
  getattr(self, "_active_call_site_idx", None),
1261
- ) or self._is_int64_builtin_init(node.name):
2146
+ ) or self._is_int64_builtin_init(node.name, node):
1262
2147
  return "int64_t"
1263
2148
  return cpp_type
1264
2149
 
@@ -1319,13 +2204,76 @@ class TypeInferer:
1319
2204
  # int64_t buffer: epoch-ms overflow int32 and the na sentinel would be
1320
2205
  # misdetected. ``_is_int64_builtin_init`` only matches user vars whose
1321
2206
  # init RHS is such a builtin, so also match the builtin name directly.
1322
- if name in INT64_BUILTINS or self._is_int64_builtin_init(name):
2207
+ if name in INT64_BUILTINS:
2208
+ return "int64_t"
2209
+ sym = self._variable_symbol(name)
2210
+ # A float or bool an epoch reaches keeps its type: the analyzer types
2211
+ # every request.security value float (a double holds an epoch
2212
+ # exactly), and a name is keyed by spelling across scopes.
2213
+ if self._is_int64_builtin_init(name) and (
2214
+ sym is None
2215
+ or sym.pine_type not in (PineType.FLOAT, PineType.BOOL, PineType.STRING)
2216
+ ):
2217
+ # So does a function's local, which the global scope does not
2218
+ # hold: ``float x = time`` is a float series (``x - x[1]`` is na
2219
+ # on the first bar, not a difference with the integer sentinel).
2220
+ local = self._callable_series_local_symbol(name) if sym is None else None
2221
+ if local is not None and local.pine_type == PineType.FLOAT:
2222
+ return "double"
1323
2223
  return "int64_t"
1324
- sym = self.ctx.symbols.resolve(name)
1325
2224
  if sym is not None:
1326
2225
  return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
1327
2226
  return "double"
1328
2227
 
2228
+ def _variable_symbol(self, name: str):
2229
+ """The symbol the variable ``name`` resolves to.
2230
+
2231
+ The symbol table keeps one name per scope, and a user function is
2232
+ defined in the global scope under its name, typed by what it
2233
+ returns. A function-local variable of the same spelling -- the
2234
+ ``float f`` series of another function beside ``f(x) =>
2235
+ str.tostring(x)`` -- resolved there once the analyzer had left its
2236
+ scope. Inside a function the variable is that function's own
2237
+ symbol; outside one, a series is the symbol of the function that
2238
+ keeps it (``func_series_vars``).
2239
+ """
2240
+ sym = self.ctx.symbols.resolve(name)
2241
+ if name not in getattr(self, "_func_names", ()):
2242
+ return sym
2243
+ active = getattr(self, "_active_func_name", None)
2244
+ if active:
2245
+ scope_names = {f"func_{active}"}
2246
+ else:
2247
+ scope_names = {
2248
+ f"func_{owner}"
2249
+ for owner, names in getattr(self.ctx, "func_series_vars", {}).items()
2250
+ if name in names
2251
+ }
2252
+ for scope in self.ctx.symbols.all_scopes:
2253
+ if scope.name in scope_names and name in scope.symbols:
2254
+ return scope.symbols[name]
2255
+ return sym
2256
+
2257
+ def _callable_series_local_symbol(self, name: str):
2258
+ """The symbol of the function-local series ``name``: the one every
2259
+ function or method keeping it as a series (``func_series_vars``)
2260
+ declares in its scope, else None when they disagree or none does."""
2261
+ scope_names = {
2262
+ # A method ``Type.name`` scopes its body as ``method_Type_name``.
2263
+ f"method_{owner.replace('.', '_', 1)}" if "." in owner
2264
+ else f"func_{owner}"
2265
+ for owner, names in getattr(self.ctx, "func_series_vars", {}).items()
2266
+ if name in names
2267
+ }
2268
+ found = [
2269
+ scope.symbols[name]
2270
+ for scope in self.ctx.symbols.all_scopes
2271
+ if scope.name in scope_names and name in scope.symbols
2272
+ ]
2273
+ if not found or any(s.pine_type != found[0].pine_type for s in found):
2274
+ return None
2275
+ return found[0]
2276
+
1329
2277
  def _series_param_element_cpp_type(
1330
2278
  self,
1331
2279
  func_info,
@@ -1364,7 +2312,7 @@ class TypeInferer:
1364
2312
  if spec.name == "bool":
1365
2313
  return "bool"
1366
2314
  if spec.name == "color":
1367
- return "int"
2315
+ return "int64_t"
1368
2316
  if spec.name == "float":
1369
2317
  return "double"
1370
2318
 
@@ -1382,7 +2330,7 @@ class TypeInferer:
1382
2330
  if pine_type == PineType.BOOL:
1383
2331
  return "bool"
1384
2332
  if pine_type == PineType.COLOR:
1385
- return "int"
2333
+ return "int64_t"
1386
2334
  if pine_type == PineType.FLOAT:
1387
2335
  return "double"
1388
2336
 
@@ -1394,7 +2342,7 @@ class TypeInferer:
1394
2342
  if spec.name == "bool":
1395
2343
  return "bool"
1396
2344
  if spec.name == "color":
1397
- return "int"
2345
+ return "int64_t"
1398
2346
  if spec.name == "float":
1399
2347
  return "double"
1400
2348
  param_types = list(getattr(func_info, "param_types", ()) or ())
@@ -1404,7 +2352,7 @@ class TypeInferer:
1404
2352
  if pine_type == PineType.BOOL:
1405
2353
  return "bool"
1406
2354
  if pine_type == PineType.COLOR:
1407
- return "int"
2355
+ return "int64_t"
1408
2356
  return "double"
1409
2357
 
1410
2358
  def _callsite_callable_return_pine_type(
@@ -1464,10 +2412,17 @@ class TypeInferer:
1464
2412
  terminal = node.body[-1]
1465
2413
  if isinstance(terminal, ExprStmt):
1466
2414
  terminal = terminal.expr
2415
+ if (not getattr(self, "_wide_int_epoch_only", False)
2416
+ and self._holds_wide_int_constant(terminal)):
2417
+ return True
1467
2418
  return self._expr_returns_wide_int(
1468
2419
  terminal, func_info, seen, call_site_idx
1469
2420
  )
1470
2421
 
2422
+ _NON_INTEGER_BINOPS = frozenset({
2423
+ "/", "==", "!=", "<", ">", "<=", ">=", "and", "or",
2424
+ })
2425
+
1471
2426
  def _expr_returns_wide_int(
1472
2427
  self,
1473
2428
  expr,
@@ -1489,7 +2444,12 @@ class TypeInferer:
1489
2444
  node = getattr(owner_info, "node", None)
1490
2445
  if node is not None:
1491
2446
  if expr.name in node.params:
1492
- return False
2447
+ # A parameter is wide when a written call feeds it a wide
2448
+ # value (``_wide_int_provenance``).
2449
+ return (
2450
+ getattr(owner_info, "name", ""),
2451
+ node.params.index(expr.name),
2452
+ ) in self._wide_int_provenance()[1]
1493
2453
  local_key = (
1494
2454
  f"{getattr(owner_info, 'name', '')}:local:{expr.name}"
1495
2455
  )
@@ -1521,15 +2481,32 @@ class TypeInferer:
1521
2481
  call_site_idx,
1522
2482
  ):
1523
2483
  return True
1524
- return self._expr_is_int64_builtin(expr)
2484
+ # A name some declaration or reassignment makes wide carries the
2485
+ # epoch wherever it is read (``_wide_int_provenance``).
2486
+ return (self._expr_is_int64_builtin(expr)
2487
+ or expr.name in self._wide_int_provenance()[0])
1525
2488
  if self._expr_is_int64_builtin(expr):
1526
2489
  return True
1527
2490
  if isinstance(expr, (BinOp, UnaryOp)) and self._literal_overflows_int32(expr):
1528
2491
  # ``90 * 24 * 60 * 60 * 1000``: an int-literal product beyond
1529
2492
  # int32 is a 64-bit value in Pine (``_pure_int_literal_value``).
1530
2493
  return True
2494
+ epoch_only = getattr(self, "_wide_int_epoch_only", False)
2495
+ if isinstance(expr, MemberAccess):
2496
+ if epoch_only:
2497
+ return False
2498
+ # A UDT ``int`` field is ``int64_t`` storage; a read of one some
2499
+ # write fills with a wide value keeps it (``_wide_udt_int_fields``).
2500
+ type_name = self._udt_receiver_type_name(expr.object, owner_info)
2501
+ return (type_name, expr.member) in self._wide_udt_int_fields()
1531
2502
  if isinstance(expr, FuncCall):
1532
2503
  func_name, namespace = self._resolve_callee(expr.callee)
2504
+ if namespace == "request" and func_name == "security":
2505
+ # The requested bar's value of the payload keeps its
2506
+ # provenance: ``request.security(t, "60", time)`` is an epoch.
2507
+ return self._expr_returns_wide_int(
2508
+ self._security_payload_node(expr), owner_info, seen, call_site_idx
2509
+ )
1533
2510
  if func_name in self._WIDE_ARRAY_ELEMENT_READS:
1534
2511
  # An element read off a wide int array (``array.get(times, i)``
1535
2512
  # / ``times.get(i)``) carries the epoch: the destination must
@@ -1607,6 +2584,13 @@ class TypeInferer:
1607
2584
  )
1608
2585
  )
1609
2586
  if isinstance(expr, BinOp):
2587
+ if expr.op in self._NON_INTEGER_BINOPS:
2588
+ # Pine v6 ``/`` is a float and a comparison a bool: no
2589
+ # integer slot holds them, whatever the operands carry.
2590
+ return False
2591
+ if not epoch_only and self._int_arith_leaves_int32(expr, owner_info):
2592
+ # ``days * 86400000``: computed in 64 bits (``_visit_binop``).
2593
+ return True
1610
2594
  return (
1611
2595
  self._expr_returns_wide_int(
1612
2596
  expr.left, owner_info, seen, call_site_idx
@@ -1616,6 +2600,8 @@ class TypeInferer:
1616
2600
  )
1617
2601
  )
1618
2602
  if isinstance(expr, UnaryOp):
2603
+ if expr.op == "not":
2604
+ return False
1619
2605
  return self._expr_returns_wide_int(
1620
2606
  expr.operand, owner_info, seen, call_site_idx
1621
2607
  )
@@ -1697,44 +2683,164 @@ class TypeInferer:
1697
2683
  return "int64_t"
1698
2684
  return cpp_type
1699
2685
 
1700
- def _int64_reassign_targets(self) -> set[str]:
1701
- """Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
1702
- with an RHS that is a top-level int64-returning builtin. Cached on the
1703
- instance. Pine ``int`` collapses these to 32-bit, but the runtime stores
1704
- the epoch in 64 bits, so the member must be promoted to ``int64_t``."""
1705
- cached = getattr(self, "_int64_reassign_cache", None)
2686
+ def _wide_int_provenance(self) -> tuple[set[str], set[tuple[str, int]]]:
2687
+ """Names, and integer callable parameters, that carry an epoch.
2688
+
2689
+ A name is wide when its declaration or any ``:=``/``=`` gives it a
2690
+ value ``_expr_returns_wide_int`` traces to a wide source; an integer
2691
+ parameter (declared ``int``, or untyped) is wide when a written call
2692
+ passes it one. A wide name or parameter is itself a wide source, so
2693
+ the width travels through copies: ``int sel = switch k => E2`` over
2694
+ ``const int E2 = timestamp(...)``, ``copy := first`` over
2695
+ ``first := time``, and ``f(int t)`` called as ``f(time)``. Pine
2696
+ ``int`` keeps the whole epoch in each (lab tv pf-krunerr-int64-probe1,
2697
+ 2026-09-26: 1743468300000 where 32-bit slots read -288422176); the
2698
+ runtime stores it, and the na sentinel, in 64 bits, so each slot is
2699
+ ``int64_t``. Iterated to a fixpoint and cached. Names are keyed by
2700
+ spelling across scopes, as the reassignment scan before this one was:
2701
+ a same-spelled narrow name widens with a wide one, which only drops a
2702
+ narrowing.
2703
+ """
2704
+ # The epoch-only mode (``_wide_int_array_names``) keeps its own sets.
2705
+ cache_attr = ("_wide_int_epoch_provenance_cache"
2706
+ if getattr(self, "_wide_int_epoch_only", False)
2707
+ else "_wide_int_provenance_cache")
2708
+ cached = getattr(self, cache_attr, None)
1706
2709
  if cached is not None:
1707
2710
  return cached
1708
- targets: set[str] = set()
1709
- # Callable locals need their lexical owner so a reassignment through a
1710
- # wrapper/method returning wide history is recognized as well as a
1711
- # direct ``time[1]`` RHS.
2711
+ names: set[str] = set()
2712
+ params: set[tuple[str, int]] = set()
2713
+ # Published before the scan: a lookup made while the fixpoint runs
2714
+ # reads the sets as they grow instead of starting a second scan.
2715
+ setattr(self, cache_attr, (names, params))
2716
+ bindings: list = []
2717
+ calls: list = []
2718
+
2719
+ def collect(node, owner) -> None:
2720
+ if isinstance(node, VarDecl) and node.value is not None:
2721
+ bindings.append((node.name, node.value, owner))
2722
+ elif (isinstance(node, Assignment)
2723
+ and isinstance(node.target, Identifier)):
2724
+ bindings.append((node.target.name, node.value, owner))
2725
+ elif isinstance(node, TupleAssign):
2726
+ # ``[t0, t1] = request.security(sym, tf, f())``: each name is
2727
+ # bound to the element its tuple evaluates.
2728
+ for index, name in enumerate(node.names):
2729
+ element = (
2730
+ self._tuple_element_value(node.value, index, owner)
2731
+ if name and name != "_" else None
2732
+ )
2733
+ if element is not None:
2734
+ bindings.append((name, *element))
2735
+ elif (isinstance(node, FuncCall)
2736
+ and isinstance(node.callee, Identifier)
2737
+ and node.callee.name in self._func_info_map):
2738
+ calls.append((node, owner))
2739
+
2740
+ # Callable bodies resolve their locals and parameters against their
2741
+ # lexical owner; everything outside them is global.
1712
2742
  for info in getattr(self.ctx, "func_infos", ()):
1713
2743
  node = getattr(info, "node", None)
1714
2744
  if node is None:
1715
2745
  continue
1716
2746
  for child in self._walk_ast_list(node.body):
1717
- if (
1718
- isinstance(child, Assignment)
1719
- and isinstance(child.target, Identifier)
1720
- and self._expr_returns_wide_int(
1721
- child.value, info, set(), None
1722
- )
1723
- ):
1724
- targets.add(child.target.name)
2747
+ collect(child, info)
1725
2748
  ast = getattr(self.ctx, "ast", None)
1726
- if ast is not None:
1727
- for node in self._walk_ast(ast):
1728
- if (isinstance(node, Assignment)
1729
- and isinstance(node.target, Identifier)
1730
- and self._expr_returns_wide_int(
1731
- node.value, None, set(), None
1732
- )):
1733
- targets.add(node.target.name)
1734
- self._int64_reassign_cache = targets
1735
- return targets
1736
-
1737
- def _is_int64_builtin_init(self, name: str) -> bool:
2749
+ for stmt in getattr(ast, "body", None) or ():
2750
+ if isinstance(stmt, (FuncDef, MethodDef)):
2751
+ continue
2752
+ for child in self._walk_ast(stmt):
2753
+ collect(child, None)
2754
+
2755
+ changed = True
2756
+ with self._int_width_scan():
2757
+ while changed:
2758
+ changed = False
2759
+ for name, value, owner in bindings:
2760
+ if name not in names and self._expr_returns_wide_int(
2761
+ value, owner, set(), None):
2762
+ names.add(name)
2763
+ changed = True
2764
+ for call, owner in calls:
2765
+ callee = self._func_info_map[call.callee.name]
2766
+ callee_params = list(getattr(callee.node, "params", ()) or ())
2767
+ bound = list(enumerate(call.args))
2768
+ bound += [
2769
+ (callee_params.index(key), arg)
2770
+ for key, arg in call.kwargs.items()
2771
+ if key in callee_params
2772
+ ]
2773
+ for index, arg in bound:
2774
+ key = (callee.name, index)
2775
+ if (key not in params
2776
+ and self._param_is_integer_scalar(callee, index)
2777
+ and self._expr_returns_wide_int(
2778
+ arg, owner, set(), None)):
2779
+ params.add(key)
2780
+ changed = True
2781
+ return names, params
2782
+
2783
+ @staticmethod
2784
+ def _security_payload_node(call: FuncCall):
2785
+ """The expression argument of a ``request.security`` call."""
2786
+ if len(call.args) > 2:
2787
+ return call.args[2]
2788
+ return call.kwargs.get("expression")
2789
+
2790
+ def _tuple_element_value(self, value, index: int, owner, depth: int = 0):
2791
+ """``(expression, lexical owner)`` element ``index`` of a tuple value
2792
+ evaluates: a tuple literal's element, through a ``request.security``
2793
+ payload and a user function's final tuple; None otherwise."""
2794
+ if depth > 32:
2795
+ return None
2796
+ if isinstance(value, TupleLiteral):
2797
+ if index < len(value.elements):
2798
+ return value.elements[index], owner
2799
+ return None
2800
+ if not isinstance(value, FuncCall):
2801
+ return None
2802
+ func_name, namespace = self._resolve_callee(value.callee)
2803
+ if namespace == "request" and func_name == "security":
2804
+ return self._tuple_element_value(
2805
+ self._security_payload_node(value), index, owner, depth + 1)
2806
+ info = self._func_info_map.get(func_name) if namespace is None else None
2807
+ node = getattr(info, "node", None)
2808
+ if node is None or not node.body:
2809
+ return None
2810
+ terminal = node.body[-1]
2811
+ if isinstance(terminal, ExprStmt):
2812
+ terminal = terminal.expr
2813
+ return self._tuple_element_value(terminal, index, info, depth + 1)
2814
+
2815
+ def _param_is_integer_scalar(self, fi, index: int) -> bool:
2816
+ """Whether parameter ``index`` of ``fi`` is a scalar integer slot:
2817
+ declared ``int`` or untyped, not a history parameter (those are
2818
+ ``Series<int64_t>`` already) and not a method receiver."""
2819
+ node = getattr(fi, "node", None)
2820
+ if node is None or index >= len(node.params or ()):
2821
+ return False
2822
+ if getattr(fi, "is_udt_method", False) and index == 0:
2823
+ return False
2824
+ if node.params[index] in self.ctx.func_series_vars.get(fi.name, set()):
2825
+ return False
2826
+ declared = list(
2827
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
2828
+ fi.name, ()
2829
+ )
2830
+ )
2831
+ spec = declared[index] if index < len(declared) else None
2832
+ if spec is None:
2833
+ return True
2834
+ return spec.kind == "primitive" and spec.name == "int"
2835
+
2836
+ def _wide_declared_int_param(self, fi, index: int, cpp_type: str) -> str:
2837
+ """``int64_t`` for a declared ``int`` parameter a call feeds an epoch
2838
+ (``_wide_int_provenance``), else ``cpp_type`` unchanged."""
2839
+ if cpp_type == "int" and (fi.name, index) in self._wide_int_provenance()[1]:
2840
+ return "int64_t"
2841
+ return cpp_type
2842
+
2843
+ def _is_int64_builtin_init(self, name: str, decl=None) -> bool:
1738
2844
  """True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
1739
2845
  RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
1740
2846
  ``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
@@ -1742,7 +2848,9 @@ class TypeInferer:
1742
2848
  (and the full epoch-ms value, which overflows int32) in 64 bits, so
1743
2849
  storing into ``int`` silently corrupts both the value and na detection.
1744
2850
  A reassignment like ``var int entryTime = na`` then ``entryTime := time``
1745
- must promote even though the *initializer* alone is ``na``.
2851
+ must promote even though the *initializer* alone is ``na``, and so
2852
+ must every name a copy of such a value reaches
2853
+ (``_wide_int_provenance``).
1746
2854
  """
1747
2855
  expr = (
1748
2856
  self.ctx.global_expr_map.get(name)
@@ -1750,7 +2858,565 @@ class TypeInferer:
1750
2858
  )
1751
2859
  if self._expr_is_int64_builtin(expr):
1752
2860
  return True
1753
- return name in self._int64_reassign_targets()
2861
+ # A script-level int constant past int32 (``_literal_wide_global``;
2862
+ # ``decl`` restricts it to that declaration).
2863
+ if self._literal_wide_global(name, decl):
2864
+ return True
2865
+ return name in self._wide_int_provenance()[0]
2866
+
2867
+ # ------------------------------------------------------------------
2868
+ # na-preserving double -> int narrowing
2869
+ # ------------------------------------------------------------------
2870
+
2871
+ # Pine functions whose declared return type is ``int`` but whose C++
2872
+ # lowering is a ``double``-valued expression, so the Pine-facing type is
2873
+ # not enough to decide whether a narrowing happens.
2874
+ _DOUBLE_EMITTING_INT_CALLS = {
2875
+ ("math", "round"), ("math", "floor"), ("math", "ceil"),
2876
+ }
2877
+
2878
+ # Built-ins whose lowering is an integer-valued engine expression, in
2879
+ # either the bare-variable or the call form: ``pine_bar_index()`` and the
2880
+ # ``pine_<field>()`` calendar helpers return ``int``, the epoch-ms readings
2881
+ # return ``int64_t``. ``_infer_type`` reports ``double`` for all of them
2882
+ # (they are ``BAR_BUILTINS``), which would wrap an already-integral value.
2883
+ _INTEGRAL_BUILTINS = frozenset({
2884
+ "bar_index", "last_bar_index",
2885
+ "year", "month", "dayofmonth", "dayofweek",
2886
+ "hour", "minute", "second", "weekofyear",
2887
+ "time", "timenow", "time_close", "last_bar_time", "time_tradingday",
2888
+ })
2889
+
2890
+ # ``strategy.closedtrades`` / ``strategy.opentrades`` read the trade-book
2891
+ # sizes, emitted as ``((int)trades_.size())``.
2892
+ _INTEGRAL_STRATEGY_MEMBERS = frozenset({"closedtrades", "opentrades"})
2893
+
2894
+ # Only an integer literal is width-independent. An integer ``na`` value,
2895
+ # input read, or prior guarded cast can still lose its sentinel when it
2896
+ # crosses into the other integer width, so those must be checked again.
2897
+ _INTEGRAL_CPP_TEXT = re.compile(
2898
+ r"^\s*(?:\(\s*)*[-+]?(?:0[xX][0-9a-fA-F]+|\d+)(?:LL|ULL|L|U)?"
2899
+ r"(?:\s*\))*\s*$"
2900
+ )
2901
+
2902
+ def _emitted_value_is_double(self, node) -> bool:
2903
+ """True when the C++ expression emitted for ``node`` has type ``double``.
2904
+
2905
+ This is deliberately *not* ``_infer_type(node) == "double"``.
2906
+ ``_infer_type`` answers "what type does this slot hold", which for
2907
+ ``math.round(x)`` is Pine's ``int`` even though the emission is
2908
+ ``std::round(x)``, a ``double`` — and which falls back to ``double``
2909
+ for any shape it cannot resolve, including integer arithmetic. Only
2910
+ this function's answer decides whether a value reaching an integer
2911
+ slot needs the na-preserving cast.
2912
+ """
2913
+ if node is None:
2914
+ return False
2915
+ if isinstance(node, NumberLiteral):
2916
+ return isinstance(node.value, float)
2917
+ if isinstance(node, (BoolLiteral, StringLiteral, ColorLiteral)):
2918
+ # A colour literal lowers to a packed-ARGB ``int64_t`` literal.
2919
+ return False
2920
+ if isinstance(node, Subscript) and isinstance(node.object, Identifier):
2921
+ # A history read keeps the series' element type.
2922
+ base = node.object.name
2923
+ if base in self._INTEGRAL_BUILTINS:
2924
+ return False
2925
+ if base in self.ctx.series_vars or base in self.ctx.series_bar_fields:
2926
+ return self._series_type_for(base) == "double"
2927
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
2928
+ # color.* lowers to a packed-ARGB integer constant; session.*
2929
+ # lowers to a ``pine_session_*`` predicate (bool).
2930
+ if node.object.name in ("color", "session"):
2931
+ return False
2932
+ if (node.object.name == "strategy"
2933
+ and node.member in self._INTEGRAL_STRATEGY_MEMBERS):
2934
+ return False
2935
+ if isinstance(node, NaLiteral):
2936
+ return True
2937
+ if isinstance(node, UnaryOp):
2938
+ if node.op in ("not", "!"):
2939
+ return False
2940
+ return self._emitted_value_is_double(node.operand)
2941
+ if isinstance(node, BinOp):
2942
+ if node.op in _BOOL_RESULT_BINOPS:
2943
+ return False
2944
+ if node.op in ("/", "%"):
2945
+ # Both lower through an explicit double form
2946
+ # (``(double)a / (double)b`` / ``std::fmod``).
2947
+ return True
2948
+ if self._na_aware_wide_is_double(node):
2949
+ # ``_wide_int_arith_cpp``'s na-aware form.
2950
+ return True
2951
+ return (self._emitted_value_is_double(node.left)
2952
+ or self._emitted_value_is_double(node.right))
2953
+ if isinstance(node, Ternary):
2954
+ # C++ gives a mixed int/double conditional the common type
2955
+ # ``double``, so one double arm narrows the whole expression.
2956
+ return (self._emitted_value_is_double(node.true_val)
2957
+ or self._emitted_value_is_double(node.false_val))
2958
+ if isinstance(node, Identifier):
2959
+ name = node.name
2960
+ if name in getattr(self, "_current_loop_vars", set()):
2961
+ # A counted-loop binder is emitted as ``for (int i = ...)``,
2962
+ # whatever ``_infer_type`` reports for the Pine binding.
2963
+ return False
2964
+ if (name in self._INTEGRAL_BUILTINS
2965
+ and name not in self._current_func_param_types
2966
+ and name not in self._known_vars):
2967
+ return False
2968
+ # Builtins, constants and parameters are resolved precisely by
2969
+ # ``_infer_type``; a user variable's *storage* is what the read
2970
+ # emits, and that can be a double where the Pine type is an int
2971
+ # (a hoisted global is declared from its initializer).
2972
+ if not (name in BAR_FIELDS or name in BAR_BUILTINS
2973
+ or name in self._known_vars
2974
+ or name in getattr(
2975
+ self, "_current_func_series_param_types", {})
2976
+ or name in self._current_func_param_types):
2977
+ declared = self._slot_scalar_cpp_type(name)
2978
+ if declared is not None:
2979
+ return declared == "double"
2980
+ if isinstance(node, FuncCall):
2981
+ func_name, namespace = self._resolve_callee(node.callee)
2982
+ if (namespace, func_name) in self._DOUBLE_EMITTING_INT_CALLS:
2983
+ # The 2-arg precision overload is a float in Pine as well and
2984
+ # falls through to the generic check.
2985
+ return len(node.args) + len(node.kwargs or {}) == 1
2986
+ if namespace is None and func_name in ("int", "bool"):
2987
+ # Already spelled as an explicit, na-preserving cast.
2988
+ return False
2989
+ if namespace is None and func_name == "nz" and node.args:
2990
+ # ``nz(x)`` / ``nz(x, y)`` keep the replaced value's type.
2991
+ return self._emitted_value_is_double(node.args[0])
2992
+ if namespace == "timeframe" and func_name == "in_seconds":
2993
+ return False
2994
+ if namespace == "session":
2995
+ return False
2996
+ if namespace is None and func_name in self._INTEGRAL_BUILTINS:
2997
+ # ``year(t)`` / ``hour(t, tz)`` lower to the same integer
2998
+ # ``pine_<field>()`` helpers as their bare-variable forms.
2999
+ return False
3000
+ if namespace == "color":
3001
+ # Every color.* lowering is an integer (packed ARGB, or a
3002
+ # channel read).
3003
+ return False
3004
+ if namespace == "ta":
3005
+ # Every ta.* call site lowers to a ta:: class
3006
+ # ``compute()``/``recompute()``, which returns a double even
3007
+ # where Pine's type is an int (``ta.highestbars``,
3008
+ # ``ta.barssince``, ...).
3009
+ return func_name not in TA_RETURNS_BOOL
3010
+ return self._infer_type(node) == "double"
3011
+
3012
+ def _slot_scalar_cpp_type(self, name: str) -> str | None:
3013
+ """The C++ type of the storage a write to the scalar ``name`` lands in.
3014
+
3015
+ Reads the declaration sites in the order the emitter itself uses:
3016
+ ``var`` member, function-local, parameter, then hoisted global member.
3017
+ Unlike ``_na_reassign_cpp_type`` the global case mirrors
3018
+ ``base.generate``'s member layout (``_infer_type`` of the initializer)
3019
+ rather than the analyzer's ``PineType``: the two disagree for
3020
+ e.g. ``x = math.round(...)``, and the declaration is the real storage.
3021
+ """
3022
+ for vname, ptype, _init in self.ctx.var_members:
3023
+ if vname == name:
3024
+ return PINE_TYPE_TO_CPP.get(ptype, "double")
3025
+ local = getattr(self, "_current_func_local_types", {}).get(name)
3026
+ if local is not None:
3027
+ return local
3028
+ param = getattr(self, "_current_func_param_types", {}).get(name)
3029
+ if param is not None:
3030
+ return param
3031
+ for gname, gptype in self.ctx.global_var_decls:
3032
+ if gname != name:
3033
+ continue
3034
+ if self._global_color_hint(name):
3035
+ return "int64_t"
3036
+ if self._global_bool_hint(name):
3037
+ return "bool"
3038
+ if (name in getattr(self, "_direct_program_tuple_binding_names", ())
3039
+ and gptype == PineType.BOOL):
3040
+ return "bool"
3041
+ if (gptype == PineType.STRING
3042
+ and name in self._security_tuple_binding_names()):
3043
+ return "std::string"
3044
+ if name in self._nonfinite_int_names():
3045
+ return "double"
3046
+ expr = getattr(self.ctx, "global_expr_map", {}).get(name)
3047
+ if expr is not None:
3048
+ return self._infer_type(expr)
3049
+ return PINE_TYPE_TO_CPP.get(gptype, "double")
3050
+ return None
3051
+
3052
+ # ------------------------------------------------------------------
3053
+ # A Pine int TradingView holds as +-Infinity
3054
+ # ------------------------------------------------------------------
3055
+
3056
+ _NONFINITE_INT_READ_OPS = frozenset({"<", ">", "<=", ">=", "==", "!="})
3057
+ NONFINITE_INT_ORDERING_OPS = frozenset({"<", ">", "<=", ">="})
3058
+
3059
+ def _nonfinite_int_names(self) -> frozenset[str]:
3060
+ """Plain global ``int`` names stored as ``double`` to keep an infinity.
3061
+
3062
+ ``math.floor(100 / 0)`` is +Infinity in TradingView, even in an ``int``
3063
+ variable: ``>``/``>=`` against any number hold and ``<``/``<=`` do not,
3064
+ while ``na()`` reads it as na, ``==``/``!=`` are false, ``str.tostring``
3065
+ prints NaN and ``strategy.entry(qty = ...)`` trades the default quantity
3066
+ (lab tv tailc-a-na-compare, tailc-a-divzero-readout,
3067
+ tailc-a-qty-nonfinite2; R5 lane TAIL-C). A C++ ``int`` narrows it to
3068
+ ``na<int>()``, which orders as na. So a name is stored as the ``double``
3069
+ its value already is when every binding of it -- a global declaration
3070
+ or ``:=`` -- is a one-argument ``math.floor``/``math.ceil``/
3071
+ ``math.round``, some read of it is an ordering comparison's operand --
3072
+ the one read whose answer an infinity changes -- and every read of it
3073
+ is an operand of a comparison, the argument of ``na``/``nz``/
3074
+ ``str.tostring`` or the quantity of ``strategy.entry``/
3075
+ ``strategy.order``. Its reads keep the int's old answers
3076
+ (``_nonfinite_int_read``): an infinity reads na, except as the operand
3077
+ of an ordering comparison, which reads it as the number.
3078
+ """
3079
+ cached = getattr(self, "_nonfinite_int_names_cache", None)
3080
+ if cached is not None:
3081
+ return cached
3082
+ self._nonfinite_int_names_cache = frozenset()
3083
+ ast = getattr(self.ctx, "ast", None)
3084
+ if ast is None:
3085
+ return self._nonfinite_int_names_cache
3086
+ # Read off the program and the analyzer alone, so every pass of the
3087
+ # emitter, the first included, gets the same answer.
3088
+ candidates = {
3089
+ name for name, _ptype in self.ctx.global_var_decls
3090
+ if name not in self.ctx.series_vars
3091
+ }
3092
+ if not candidates:
3093
+ return self._nonfinite_int_names_cache
3094
+ bound: set[str] = set()
3095
+ rejected: set[str] = set()
3096
+ ordered: set[str] = set()
3097
+
3098
+ def floor_call(value) -> bool:
3099
+ if not isinstance(value, FuncCall) or value.kwargs or len(value.args) != 1:
3100
+ return False
3101
+ func_name, namespace = self._resolve_callee(value.callee)
3102
+ return (namespace, func_name) in self._DOUBLE_EMITTING_INT_CALLS
3103
+
3104
+ def allowed_read(parent, slot: str, index: int) -> bool:
3105
+ if isinstance(parent, BinOp):
3106
+ return parent.op in self._NONFINITE_INT_READ_OPS
3107
+ if not isinstance(parent, FuncCall):
3108
+ return False
3109
+ func_name, namespace = self._resolve_callee(parent.callee)
3110
+ if slot == "args" and index == 0 and (
3111
+ (namespace is None and func_name in ("na", "nz"))
3112
+ or (namespace, func_name) == ("str", "tostring")):
3113
+ return True
3114
+ return (namespace == "strategy" and func_name in ("entry", "order")
3115
+ and (slot == "kwargs:qty" or (slot == "args" and index == 2)))
3116
+
3117
+ def children(node):
3118
+ for slot, value in vars(node).items():
3119
+ if isinstance(value, ASTNode):
3120
+ yield slot, 0, value
3121
+ elif isinstance(value, dict):
3122
+ for key, item in value.items():
3123
+ if isinstance(item, ASTNode):
3124
+ yield f"{slot}:{key}", 0, item
3125
+ elif isinstance(value, (list, tuple)):
3126
+ for index, item in enumerate(value):
3127
+ if isinstance(item, ASTNode):
3128
+ yield slot, index, item
3129
+ elif isinstance(item, (list, tuple)):
3130
+ for inner in item:
3131
+ if isinstance(inner, ASTNode):
3132
+ yield slot, index, inner
3133
+ elif isinstance(inner, list):
3134
+ for leaf in inner:
3135
+ if isinstance(leaf, ASTNode):
3136
+ yield slot, index, leaf
3137
+
3138
+ pending = [(ast, False)]
3139
+ while pending:
3140
+ node, in_callable = pending.pop()
3141
+ if isinstance(node, (FuncDef, MethodDef)):
3142
+ # A parameter or a local of that spelling would read through
3143
+ # the global's rule.
3144
+ rejected.update(set(getattr(node, "params", None) or ()) & candidates)
3145
+ in_callable = True
3146
+ if isinstance(node, VarDecl) and node.name in candidates:
3147
+ if (in_callable or node.is_var or node.is_varip
3148
+ or node.type_hint not in (None, "int")
3149
+ or not floor_call(node.value)):
3150
+ rejected.add(node.name)
3151
+ bound.add(node.name)
3152
+ elif isinstance(node, TupleAssign):
3153
+ rejected.update(set(node.names) & candidates)
3154
+ elif (isinstance(node, Assignment) and isinstance(node.target, Identifier)
3155
+ and node.target.name in candidates):
3156
+ if node.op != ":=" or not floor_call(node.value):
3157
+ rejected.add(node.target.name)
3158
+ bound.add(node.target.name)
3159
+ for slot, index, child in children(node):
3160
+ if isinstance(child, Identifier) and child.name in candidates:
3161
+ binding_target = (isinstance(node, Assignment) and slot == "target")
3162
+ if not binding_target and not allowed_read(node, slot, index):
3163
+ rejected.add(child.name)
3164
+ if (isinstance(node, BinOp)
3165
+ and node.op in self.NONFINITE_INT_ORDERING_OPS):
3166
+ ordered.add(child.name)
3167
+ pending.append((child, in_callable))
3168
+ self._nonfinite_int_names_cache = frozenset((bound & ordered) - rejected)
3169
+ return self._nonfinite_int_names_cache
3170
+
3171
+ def _global_color_hint(self, name: str) -> bool:
3172
+ """A top-level ``color`` declaration must keep its 64-bit na sentinel."""
3173
+ return any(
3174
+ isinstance(stmt, VarDecl)
3175
+ and stmt.name == name
3176
+ and stmt.type_hint == "color"
3177
+ for stmt in self.ctx.ast.body
3178
+ )
3179
+
3180
+ def _global_bool_hint(self, name: str) -> bool:
3181
+ """A top-level declared bool keeps its type when initialized by na."""
3182
+ return any(
3183
+ isinstance(stmt, VarDecl)
3184
+ and stmt.name == name
3185
+ and stmt.type_hint == "bool"
3186
+ for stmt in self.ctx.ast.body
3187
+ )
3188
+
3189
+ def _int_slot_cpp_type(self, name: str | None,
3190
+ declared: str | None = None) -> str | None:
3191
+ """``"int"``/``"int64_t"`` when a write to this slot narrows, else ``None``.
3192
+
3193
+ ``declared`` short-circuits the lookup for an emitter that already
3194
+ knows the storage type it is about to spell (a local declaration).
3195
+ """
3196
+ if declared is not None:
3197
+ resolved = declared
3198
+ elif name is None:
3199
+ return None
3200
+ else:
3201
+ resolved = self._slot_scalar_cpp_type(name)
3202
+ if (resolved == "int" and name is not None
3203
+ and self._is_int64_builtin_init(name)):
3204
+ resolved = "int64_t"
3205
+ return resolved if resolved in (*NA_PRESERVING_INT_TYPES, "bool") else None
3206
+
3207
+ def _func_param_int_cpp_type(self, fi, index: int,
3208
+ call_site_idx: int | None) -> str | None:
3209
+ """``"int"``/``"int64_t"`` when parameter ``index`` of ``fi`` is an integer.
3210
+
3211
+ Mirrors the parameter-type branches of ``emit_top._emit_func_def`` in
3212
+ the same order, keeping only the outcomes that are integers: a value
3213
+ crossing into one of those parameters narrows, and Pine's ``color`` is
3214
+ one of them (the engine packs a color as an integer). History
3215
+ parameters become ``const Series<T>&`` and are not a narrowing slot.
3216
+ """
3217
+ node = getattr(fi, "node", None)
3218
+ if node is None or index >= len(node.params or ()):
3219
+ return None
3220
+ param = node.params[index]
3221
+ if getattr(fi, "is_udt_method", False) and index == 0:
3222
+ return None
3223
+ if fi.name == "isInSession" and index < 2:
3224
+ return None
3225
+ if param in self.ctx.func_series_vars.get(fi.name, set()):
3226
+ return None
3227
+ declared = list(
3228
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
3229
+ fi.name, ()
3230
+ )
3231
+ )
3232
+ variant = (
3233
+ getattr(self.ctx, "func_callsite_param_types", {}).get(
3234
+ (fi.name, call_site_idx), ()
3235
+ )
3236
+ if call_site_idx is not None
3237
+ else ()
3238
+ )
3239
+ if (index >= len(declared) or declared[index] is None) and index < len(variant):
3240
+ return {
3241
+ PineType.INT: "int64_t",
3242
+ PineType.COLOR: "int64_t",
3243
+ }.get(variant[index])
3244
+ specs = getattr(fi, "param_type_specs", []) or []
3245
+ if index < len(specs) and specs[index] is not None:
3246
+ cpp_t = self._wide_declared_int_param(
3247
+ fi, index, self._type_spec_to_cpp(specs[index]))
3248
+ return cpp_t if cpp_t in NA_PRESERVING_INT_TYPES else None
3249
+ if index < len(fi.param_types):
3250
+ cpp_t = self._wide_declared_int_param(
3251
+ fi, index, PINE_TYPE_TO_CPP.get(fi.param_types[index], "double"))
3252
+ return cpp_t if cpp_t in NA_PRESERVING_INT_TYPES else None
3253
+ return None
3254
+
3255
+ def _func_param_is_bool(self, fi, index: int,
3256
+ call_site_idx: int | None = None) -> bool:
3257
+ """Whether one emitted user-function parameter is Pine ``bool``."""
3258
+ node = getattr(fi, "node", None)
3259
+ if node is None or index >= len(node.params or ()):
3260
+ return False
3261
+ declared = list(
3262
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
3263
+ fi.name, ()
3264
+ )
3265
+ )
3266
+ if index < len(declared) and declared[index] is not None:
3267
+ spec = declared[index]
3268
+ return spec.kind == "primitive" and spec.name == "bool"
3269
+ variant = (
3270
+ getattr(self.ctx, "func_callsite_param_types", {}).get(
3271
+ (fi.name, call_site_idx), ()
3272
+ )
3273
+ if call_site_idx is not None
3274
+ else ()
3275
+ )
3276
+ if index < len(variant):
3277
+ return variant[index] == PineType.BOOL
3278
+ specs = getattr(fi, "param_type_specs", []) or []
3279
+ if index < len(specs) and specs[index] is not None:
3280
+ spec = specs[index]
3281
+ return spec.kind == "primitive" and spec.name == "bool"
3282
+ return index < len(fi.param_types) and fi.param_types[index] == PineType.BOOL
3283
+
3284
+ def _udt_field_int_cpp_type(self, target_node) -> str | None:
3285
+ """Integer C++ type of a UDT field write target, else ``None``.
3286
+
3287
+ Mirrors the record layout in ``base.generate``, which widens a Pine
3288
+ ``int`` field to ``int64_t`` so epoch values and the na sentinel fit.
3289
+ """
3290
+ if not isinstance(target_node, MemberAccess):
3291
+ return None
3292
+ owner = self._type_spec_from_expr(target_node.object)
3293
+ if owner is None or owner.kind != "udt" or not owner.name:
3294
+ return None
3295
+ spec = (self._udt_field_type_specs.get(owner.name) or {}).get(
3296
+ target_node.member
3297
+ )
3298
+ if spec is None:
3299
+ return None
3300
+ cpp_type = self._type_spec_to_cpp(spec)
3301
+ if cpp_type == "int":
3302
+ cpp_type = "int64_t"
3303
+ return cpp_type if cpp_type in (*NA_PRESERVING_INT_TYPES, "bool") else None
3304
+
3305
+ def _coerce_int_slot(self, cpp_val: str, node, target_cpp_type: str | None,
3306
+ *, value_is_double: bool | None = None) -> str:
3307
+ """Route a value into an ``int``/``int64_t`` slot without losing ``na``.
3308
+
3309
+ A double NaN cannot be narrowed implicitly, and an integer na
3310
+ sentinel cannot cross into the other integer width without losing
3311
+ its sentinel. Same-width integral slots keep their established C++
3312
+ spelling; width changes check the original source value first.
3313
+ ``value_is_double`` overrides the AST judgement for emitters that
3314
+ build their own expression text.
3315
+ """
3316
+ if target_cpp_type == "bool":
3317
+ return self._coerce_bool_expr(cpp_val, node)
3318
+ if target_cpp_type not in NA_PRESERVING_INT_TYPES:
3319
+ return cpp_val
3320
+ if cpp_val in {"na<double>()", "na<int>()", "na<int64_t>()"}:
3321
+ # A bare na needs no round trip through the source width.
3322
+ return f"na<{target_cpp_type}>()"
3323
+ if self._INTEGRAL_CPP_TEXT.match(cpp_val):
3324
+ return cpp_val
3325
+ if ((target_cpp_type == "int" and cpp_val.startswith("get_input_int("))
3326
+ or (target_cpp_type == "int64_t"
3327
+ and cpp_val.startswith("get_input_int64("))):
3328
+ # Engine ABI declarations return exactly these widths, and their
3329
+ # integer na sentinel already matches the destination slot.
3330
+ return cpp_val
3331
+ if value_is_double is None:
3332
+ value_is_double = self._emitted_value_is_double(node)
3333
+ if value_is_double:
3334
+ return na_preserving_int_cast(cpp_val, target_cpp_type)
3335
+ if node is None:
3336
+ return cpp_val
3337
+ source_cpp_type = self._infer_type(node)
3338
+ if (isinstance(node, Identifier)
3339
+ and self._is_int64_builtin_init(node.name)):
3340
+ source_cpp_type = "int64_t"
3341
+ if (source_cpp_type in NA_PRESERVING_INT_TYPES
3342
+ and source_cpp_type != target_cpp_type):
3343
+ return na_preserving_int_cast(cpp_val, target_cpp_type)
3344
+ return cpp_val
3345
+
3346
+ def _coerce_int_slot_with_cast(
3347
+ self, cpp_val: str, node, target_cpp_type: str
3348
+ ) -> str:
3349
+ """Preserve an explicit integral cast when no Na conversion is needed.
3350
+
3351
+ A few public textual contracts intentionally show the destination cast
3352
+ (drawing ABI coordinates and security history offsets). Keep that
3353
+ spelling for values already proven integral; double-valued expressions
3354
+ still take `_coerce_int_slot`'s na-preserving lambda.
3355
+ """
3356
+ rendered = self._coerce_int_slot(cpp_val, node, target_cpp_type)
3357
+ if (rendered == cpp_val
3358
+ and (self._INTEGRAL_CPP_TEXT.match(cpp_val)
3359
+ or not self._emitted_value_is_double(node))):
3360
+ return f"({target_cpp_type})({cpp_val})"
3361
+ return rendered
3362
+
3363
+ def _coerce_bool_expr(self, cpp_val: str, node=None) -> str:
3364
+ """Lower a Pine scalar used in a boolean context.
3365
+
3366
+ Pine treats ``na`` as false when a numeric expression is consumed by
3367
+ ``if``, a ternary, ``and``/``or``, or a boolean parameter. A C++
3368
+ conversion does not: NaN and the integer ``na`` sentinel are both
3369
+ truthy. Keep compile-time literals and expressions already typed as
3370
+ ``bool`` byte-identical; every other numeric scalar goes through the
3371
+ one single-evaluation runtime helper.
3372
+
3373
+ The helper deliberately does not reject an unresolved shape. The
3374
+ analyzer has already admitted the expression, and preserving its
3375
+ existing lowering with a warning is preferable to introducing a new
3376
+ refusal for a population script.
3377
+ """
3378
+ # Text-only helper paths (notably security registration) do not carry
3379
+ # an AST node from which to prove the scalar type. Keep their
3380
+ # established spelling; the typed chart/security visitors pass the
3381
+ # original node and take the exact path below.
3382
+ if node is None:
3383
+ return cpp_val
3384
+ if cpp_val.strip() in {"true", "false", "na<bool>()"}:
3385
+ return cpp_val
3386
+ if cpp_val.strip() in {
3387
+ "is_first_tick()", "is_last_tick_", "barstate_islast_",
3388
+ "tf_change(prev_bar_timestamp_, current_bar_.timestamp)",
3389
+ }:
3390
+ return cpp_val
3391
+ if isinstance(node, FuncCall):
3392
+ func_name, namespace = self._resolve_callee(node.callee)
3393
+ if namespace == "timeframe" and func_name == "change":
3394
+ return cpp_val
3395
+ if namespace == "session":
3396
+ return cpp_val
3397
+ if (isinstance(node, MemberAccess)
3398
+ and isinstance(node.object, Identifier)
3399
+ and node.object.name in {
3400
+ "barstate", "chart", "session", "timeframe"
3401
+ }
3402
+ and self._infer_type(node) == "bool"):
3403
+ return cpp_val
3404
+ inferred = self._infer_type(node)
3405
+ if inferred == "bool":
3406
+ return cpp_val
3407
+ if inferred not in {"double", "int", "int64_t"}:
3408
+ # Non-numeric values cannot carry Pine's numeric na sentinel. The
3409
+ # ordinary C++ conversion is the established fallback for handles
3410
+ # and other engine values.
3411
+ return cpp_val
3412
+ if isinstance(node, (NumberLiteral, BoolLiteral, ColorLiteral)):
3413
+ return cpp_val
3414
+ if (isinstance(node, Identifier)
3415
+ and node.name in self._known_vars
3416
+ and node.name not in self._input_backed_vars
3417
+ and not self._known_var_is_lexically_shadowed(node.name)):
3418
+ return cpp_val
3419
+ return pine_truth_cast(cpp_val)
1754
3420
 
1755
3421
  def _na_reassign_cpp_type(self, name: str) -> str | None:
1756
3422
  """Declared scalar C++ type of a ``:=`` reassignment target ``name``, so a
@@ -1794,7 +3460,11 @@ class TypeInferer:
1794
3460
  if cpp_type is None:
1795
3461
  for gname, gptype in self.ctx.global_var_decls:
1796
3462
  if gname == name:
1797
- cpp_type = PINE_TYPE_TO_CPP.get(gptype, "double")
3463
+ cpp_type = (
3464
+ "int64_t" if self._global_color_hint(name)
3465
+ else "bool" if self._global_bool_hint(name)
3466
+ else PINE_TYPE_TO_CPP.get(gptype, "double")
3467
+ )
1798
3468
  break
1799
3469
  if cpp_type is None:
1800
3470
  return None
@@ -1935,6 +3605,12 @@ class TypeInferer:
1935
3605
  and ternaries / if / switch expressions. Returns the string
1936
3606
  ``"double"`` as the safe fallback when no narrower type can be
1937
3607
  determined."""
3608
+ if isinstance(node, Subscript) and isinstance(node.object, Identifier):
3609
+ name = node.object.name
3610
+ if name in self.ctx.series_vars:
3611
+ return self._series_type_for(name)
3612
+ if name in self.ctx.series_bar_fields:
3613
+ return "double"
1938
3614
  if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
1939
3615
  # A callable-result history read keeps the callable's scalar
1940
3616
  # family. Collection subscripts follow separate element-type paths.
@@ -1943,6 +3619,8 @@ class TypeInferer:
1943
3619
  return "double" if isinstance(node.value, float) else "int"
1944
3620
  if isinstance(node, BoolLiteral):
1945
3621
  return "bool"
3622
+ if isinstance(node, ColorLiteral):
3623
+ return "int64_t"
1946
3624
  if isinstance(node, StringLiteral):
1947
3625
  return "std::string"
1948
3626
  if isinstance(node, NaLiteral):
@@ -1973,7 +3651,7 @@ class TypeInferer:
1973
3651
  return self._current_func_local_types[node.name]
1974
3652
  if node.name in getattr(self, "_current_loop_vars", set()):
1975
3653
  return "double"
1976
- sym = self.ctx.symbols.resolve(node.name)
3654
+ sym = self._variable_symbol(node.name)
1977
3655
  if sym is not None and getattr(sym, "type_spec", None) is not None:
1978
3656
  return self._type_spec_to_cpp(sym.type_spec)
1979
3657
  if sym is not None and sym.pine_type != PineType.UNKNOWN:
@@ -1981,6 +3659,11 @@ class TypeInferer:
1981
3659
  return "double"
1982
3660
  if isinstance(node, FuncCall):
1983
3661
  func_name, namespace = self._resolve_callee(node.callee)
3662
+ if namespace == "color":
3663
+ return "int64_t" if func_name in {"new", "rgb", "from_gradient"} else "int"
3664
+ if (namespace == "request" and func_name == "security"
3665
+ and self._security_call_returns_string(node)):
3666
+ return "std::string"
1984
3667
  # Nested trade-accessor calls bypass the flat namespace signature
1985
3668
  # table. Their textual metadata accessors return std::string from
1986
3669
  # the runtime, so hintless locals must not use the double fallback.
@@ -2005,7 +3688,9 @@ class TypeInferer:
2005
3688
  return "int64_t"
2006
3689
  if func_name == "na":
2007
3690
  return "bool"
2008
- if namespace == "input" or (namespace is None and func_name == "input"):
3691
+ if namespace is None and func_name == "input":
3692
+ return self._generic_input_cpp_type(node)
3693
+ if namespace == "input":
2009
3694
  if func_name in ("string", "timeframe", "session", "symbol", "text_area"):
2010
3695
  return "std::string"
2011
3696
  if func_name == "bool":
@@ -2102,18 +3787,30 @@ class TypeInferer:
2102
3787
  return "bool"
2103
3788
  if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
2104
3789
  ename = node.object.name
3790
+ if ename == "color":
3791
+ return "int64_t"
2105
3792
  if ename in self._enum_defs and node.member in self._enum_defs[ename]:
2106
3793
  return "int"
2107
3794
  # format.* constants emit std::string literals (consumed by
2108
3795
  # pine_str_tostring); bare reads must declare std::string.
2109
3796
  if ename == "format":
2110
3797
  return "std::string"
3798
+ if ename == "alert" and node.member in ALERT_FREQ_VALUES:
3799
+ return "std::string"
2111
3800
  if ename == "timeframe":
2112
3801
  if node.member in ("period", "main_period"):
2113
3802
  return "std::string"
2114
3803
  if node.member == "multiplier":
2115
3804
  return "int"
2116
3805
  return "bool"
3806
+ # chart.is_* are Pine bools (the emitter lowers them to C++
3807
+ # ``true``/``false``): str.tostring(chart.is_standard) reads "true"
3808
+ # on TradingView (lab tv pf-krunerr-env-facts, 2026-09-26), not the
3809
+ # numeric "1".
3810
+ if ename == "chart" and node.member in (
3811
+ "is_standard", "is_heikinashi", "is_kagi", "is_linebreak",
3812
+ "is_pnf", "is_range", "is_renko"):
3813
+ return "bool"
2117
3814
  # syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
2118
3815
  # and derive C++ type from the expression (na<T>() or function call).
2119
3816
  if ename == "syminfo":
@@ -2193,6 +3890,16 @@ class TypeInferer:
2193
3890
  local_types[stmt.name] = self._type_spec_to_cpp(spec)
2194
3891
  continue
2195
3892
  local_types[stmt.name] = self._infer_type(stmt.value)
3893
+ elif (isinstance(stmt, TupleAssign)
3894
+ and isinstance(stmt.value, (IfStmt, SwitchStmt))):
3895
+ for name, cpp_t in zip(
3896
+ stmt.names,
3897
+ self._infer_selection_tuple_types(
3898
+ stmt.value, len(stmt.names), local_types
3899
+ ),
3900
+ ):
3901
+ if name != "_":
3902
+ local_types[name] = cpp_t
2196
3903
 
2197
3904
  last_stmt = func_node.body[-1]
2198
3905
  expr = None
@@ -2200,12 +3907,109 @@ class TypeInferer:
2200
3907
  expr = last_stmt.expr
2201
3908
  elif isinstance(last_stmt, TupleLiteral):
2202
3909
  expr = last_stmt
3910
+ request = last_stmt.expr if isinstance(last_stmt, ExprStmt) else last_stmt
3911
+ if (isinstance(request, FuncCall)
3912
+ and self._resolve_callee(request.callee) == ("security", "request")):
3913
+ # A helper returning a request's tuple returns it as the value
3914
+ # read stores it (``_req_sec_N`` or its per-element members).
3915
+ item = self._security_call_for_request(request)
3916
+ if item is not None and item.get("returns_tuple"):
3917
+ payload = item["expr_node"]
3918
+ if isinstance(payload, TupleLiteral):
3919
+ return ["double" if item.get("foreign") and cpp_t == "int" else cpp_t
3920
+ for cpp_t in map(self._infer_cpp_type_for_security_elem,
3921
+ payload.elements)]
3922
+ return self._security_tuple_element_cpp_types(
3923
+ count, item.get("tuple_element_types", ()))
2203
3924
  if expr is not None:
3925
+ owner_info = next(
3926
+ (info for info in self._func_info_map.values()
3927
+ if getattr(info, "node", None) is func_node), None)
2204
3928
  result: list[str] = []
2205
3929
  for e in expr.elements:
2206
3930
  if isinstance(e, Identifier) and e.name in local_types:
2207
- result.append(local_types[e.name])
3931
+ cpp_t = local_types[e.name]
2208
3932
  else:
2209
- result.append(self._infer_type(e))
3933
+ cpp_t = self._infer_type(e)
3934
+ result.append(self._widen_int_slot(cpp_t, e, owner_info))
2210
3935
  return result
3936
+ if isinstance(last_stmt, (IfStmt, SwitchStmt)):
3937
+ return self._infer_selection_tuple_types(
3938
+ last_stmt, count, local_types
3939
+ )
3940
+ if (isinstance(last_stmt, TupleAssign)
3941
+ and isinstance(last_stmt.value, FuncCall)
3942
+ and isinstance(last_stmt.value.callee, Identifier)):
3943
+ # ``[p, q] = pair(v)`` last: the names are the callee's elements.
3944
+ callee = self._func_info_map.get(last_stmt.value.callee.name)
3945
+ callee_node = getattr(callee, "node", None)
3946
+ if isinstance(callee_node, FuncDef) and callee_node is not func_node:
3947
+ return self._infer_tuple_types(callee_node, count)
2211
3948
  return ["double"] * count
3949
+
3950
+ def _infer_selection_tuple_types(
3951
+ self, node, count: int, local_types: dict[str, str] | None = None,
3952
+ ) -> list[str]:
3953
+ """C++ element types of a tuple-valued if/switch.
3954
+
3955
+ Each arm ends in a ``[a, b]`` literal, a tuple-returning user
3956
+ function call or a nested selection (the analyzer's
3957
+ ``_selection_tuple_shape``). Arms agreeing on a position keep that
3958
+ type; numeric arms that differ widen to ``double``; any other
3959
+ disagreement keeps the first arm's type.
3960
+ """
3961
+ local_types = local_types or {}
3962
+ if isinstance(node, IfStmt):
3963
+ arms = [node.body] + ([node.else_body] if node.else_body else [])
3964
+ elif isinstance(node, SwitchStmt):
3965
+ arms = [body for _case, body in node.cases]
3966
+ if node.default_body:
3967
+ arms.append(node.default_body)
3968
+ else:
3969
+ return ["double"] * count
3970
+ per_arm: list[list[str]] = []
3971
+ for body in arms:
3972
+ if not body:
3973
+ continue
3974
+ terminal = body[-1]
3975
+ expr = terminal.expr if isinstance(terminal, ExprStmt) else terminal
3976
+ if isinstance(expr, TupleLiteral) and len(expr.elements) == count:
3977
+ per_arm.append([
3978
+ local_types[e.name]
3979
+ if isinstance(e, Identifier) and e.name in local_types
3980
+ else self._infer_type(e)
3981
+ for e in expr.elements
3982
+ ])
3983
+ elif isinstance(expr, FuncCall) and isinstance(expr.callee, Identifier):
3984
+ fi = self._func_info_map.get(expr.callee.name)
3985
+ if (fi is not None and fi.node is not None
3986
+ and getattr(fi, "returns_tuple", False)
3987
+ and fi.tuple_element_count == count):
3988
+ per_arm.append(self._infer_tuple_types(fi.node, count))
3989
+ elif isinstance(expr, (IfStmt, SwitchStmt)):
3990
+ per_arm.append(
3991
+ self._infer_selection_tuple_types(expr, count, local_types)
3992
+ )
3993
+ if not per_arm:
3994
+ return ["double"] * count
3995
+ result: list[str] = []
3996
+ for position in range(count):
3997
+ found = [types[position] for types in per_arm]
3998
+ if len(set(found)) == 1:
3999
+ result.append(found[0])
4000
+ elif set(found) <= {"double", "int", "int64_t"}:
4001
+ result.append("double")
4002
+ else:
4003
+ result.append(found[0])
4004
+ return result
4005
+
4006
+ def _tuple_default_expr(self, types: list[str]) -> str:
4007
+ """``std::tuple<...>`` of na elements: a tuple selection no arm of
4008
+ which runs yields na in every position."""
4009
+ values = [
4010
+ f"na<{cpp_t}>()"
4011
+ if cpp_t in ("double", "int", "int64_t", "bool", "std::string")
4012
+ else self._default_for_type(cpp_t)
4013
+ for cpp_t in types
4014
+ ]
4015
+ return f"std::tuple<{', '.join(types)}>({', '.join(values)})"