@pineforge/codegen-pyodide 0.10.0 → 0.10.2

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.
@@ -34,11 +34,11 @@ tables it needs come from ``codegen/tables.py``.
34
34
  from __future__ import annotations
35
35
 
36
36
  from ..ast_nodes import (
37
- BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
37
+ ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
38
38
  MemberAccess, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
39
- Ternary, TupleLiteral, UnaryOp, VarDecl,
39
+ Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl,
40
40
  )
41
- from ..symbols import PineType, TypeSpec
41
+ from ..symbols import PineType, TypeSpec, method_receiver_type_name
42
42
  from .. import signatures as sigs
43
43
  from .tables import (
44
44
  ARRAY_DRAWING_NEW_CTORS,
@@ -47,8 +47,10 @@ from .tables import (
47
47
  BAR_FIELDS,
48
48
  DRAWING_NS,
49
49
  DRAWING_TYPE_TO_CPP,
50
+ MATRIX_RETURNING_METHODS,
50
51
  PINE_TYPE_TO_CPP,
51
52
  TA_RETURNS_BOOL,
53
+ checked_array_slice,
52
54
  )
53
55
 
54
56
  # Collection (array / map / matrix) methods that MUTATE the receiver in place.
@@ -145,7 +147,7 @@ class TypeInferer:
145
147
  if spec.kind == "array":
146
148
  return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
147
149
  if spec.kind == "map":
148
- return f"std::unordered_map<{self._type_spec_to_cpp(spec.key)}, {self._type_spec_to_cpp(spec.value)}>"
150
+ return f"PineMap<{self._type_spec_to_cpp(spec.key)}, {self._type_spec_to_cpp(spec.value)}>"
149
151
  if spec.kind == "matrix":
150
152
  elem = self._type_spec_to_cpp(spec.element)
151
153
  if spec.element.kind == "primitive" and spec.element.name == "float":
@@ -153,17 +155,97 @@ class TypeInferer:
153
155
  return f"PineGenericMatrix<{elem}>"
154
156
  return "double"
155
157
 
158
+ def _udt_record_cpp_type(self, type_name: str) -> str:
159
+ """Generated backing-record type for one user-defined object type."""
160
+ return getattr(self, "_udt_record_cpp_names", {}).get(
161
+ type_name, f"_PFUdtRecord_{type_name}"
162
+ )
163
+
164
+ def _udt_arena_member_name(self, type_name: str) -> str:
165
+ """Per-strategy arena member owning one user-defined object's records."""
166
+ return getattr(self, "_udt_arena_member_names", {}).get(
167
+ type_name, f"_pf_udt_{self._safe_name(type_name)}"
168
+ )
169
+
170
+ def _udt_arena_cpp_type(self, type_name: str) -> str:
171
+ """Generated arena specialization for one user-defined object type."""
172
+ return (
173
+ f"{getattr(self, '_udt_arena_template_cpp_name', '_PFUdtArena')}<"
174
+ f"{type_name}, "
175
+ f"{self._udt_record_cpp_type(type_name)}>"
176
+ )
177
+
178
+ def _checkpoint_traits_cpp_type(self) -> str:
179
+ """Collision-safe generated checkpoint trait template name."""
180
+ return getattr(
181
+ self, "_checkpoint_traits_cpp_name", "_PFCheckpointTraits"
182
+ )
183
+
184
+ def _udt_direct_array_fields(self, type_name: str) -> tuple[str, ...]:
185
+ """Direct array-valued fields whose UDT copy semantics are unsupported.
186
+
187
+ Maps, matrices, and nested UDTs are numeric/shared-ID handles, so an
188
+ outer record copy is correctly shallow. Arrays still lower directly to
189
+ ``std::vector<T>``; copying such a record would silently deep-copy the
190
+ vector and diverge from Pine object identity. Keep this boundary narrow
191
+ and explicit until arrays themselves have a handle representation.
192
+ """
193
+ return tuple(
194
+ field_name
195
+ for field_name, spec in self._udt_field_type_specs.get(
196
+ type_name, {}
197
+ ).items()
198
+ if spec is not None and spec.kind == "array"
199
+ )
200
+
201
+ def _reject_unsupported_udt_copy(self, type_name: str, node) -> None:
202
+ """Fail closed before copying a record with direct vector storage."""
203
+ fields = self._udt_direct_array_fields(type_name)
204
+ if not fields:
205
+ return
206
+ rendered = ", ".join(fields)
207
+ self._codegen_error(
208
+ node,
209
+ f"{type_name}.copy() is unsupported because direct array field(s) "
210
+ f"{rendered} still use value storage",
211
+ hint=(
212
+ "Move the array outside the UDT and store a supported shared-ID "
213
+ "map/matrix or nested UDT handle until array fields gain handle "
214
+ "identity."
215
+ ),
216
+ )
217
+
156
218
  @staticmethod
157
- def _default_for_type(cpp_type: str) -> str:
219
+ def _is_nullable_collection_cpp_type(cpp_type: str | None) -> bool:
220
+ """Whether ``cpp_type`` has a default-constructed Pine ``na`` ID.
221
+
222
+ Arrays still lower to ``std::vector`` and cannot distinguish a null ID
223
+ from a valid empty array. Keep them outside this predicate until their
224
+ runtime representation carries that distinction.
225
+ """
226
+ return bool(
227
+ cpp_type
228
+ and (
229
+ cpp_type.startswith("PineMap<")
230
+ or cpp_type == "PineMatrix"
231
+ or cpp_type.startswith("PineGenericMatrix<")
232
+ )
233
+ )
234
+
235
+ def _default_for_type(self, cpp_type: str) -> str:
158
236
  """Default initialiser for a primitive C++ type (matches Pine ``na``)."""
159
237
  if cpp_type == "std::string":
160
238
  return 'std::string("")'
161
239
  if cpp_type == "bool":
162
240
  return "false"
163
- if cpp_type == "int":
241
+ if cpp_type in ("int", "int64_t"):
164
242
  return "0"
165
- if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
243
+ if cpp_type in DRAWING_TYPE_TO_CPP.values():
244
+ return f"{cpp_type}{{}}"
245
+ if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
166
246
  return f"{cpp_type}()"
247
+ if self._is_nullable_collection_cpp_type(cpp_type):
248
+ return f"{cpp_type}{{}}"
167
249
  return "0.0"
168
250
 
169
251
  def _default_for_spec(self, spec: TypeSpec | None) -> str:
@@ -180,24 +262,217 @@ class TypeInferer:
180
262
  return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
181
263
  return f"{spec.name}{{}}"
182
264
  cpp_type = self._type_spec_to_cpp(spec)
183
- if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
265
+ if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
184
266
  return f"{cpp_type}()"
267
+ if self._is_nullable_collection_cpp_type(cpp_type):
268
+ return f"{cpp_type}{{}}"
185
269
  return self._default_for_type(cpp_type)
186
270
 
271
+ def _collection_spec_for_name(self, name: str) -> TypeSpec | None:
272
+ """Resolve collection metadata with lexical precedence.
273
+
274
+ Source-ordered callable locals shadow loop bindings and parameters once
275
+ their declaration has executed; before that point, loop/parameter
276
+ bindings shadow top-level/on_bar state. The process-wide raw-name maps
277
+ are retained for top-level compatibility, but no callable may infer a
278
+ collection kind from a same-named sibling callable.
279
+ """
280
+ local_specs = getattr(self, "_current_func_collection_specs", {})
281
+ if name in local_specs:
282
+ return local_specs[name]
283
+ # A scalar/UDT local still shadows a same-named top-level collection.
284
+ # Returning None is semantically different from "not found locally": it
285
+ # prevents legacy global kind registries from resurrecting the hidden
286
+ # binding during member dispatch or alias analysis.
287
+ if name in getattr(self, "_current_func_collection_shadows", set()):
288
+ return None
289
+ loop_specs = getattr(self, "_current_loop_var_specs", None)
290
+ if loop_specs and name in loop_specs:
291
+ return loop_specs[name]
292
+ if name in getattr(self, "_current_loop_vars", set()):
293
+ return None
294
+ param_specs = getattr(self, "_current_func_param_specs", {})
295
+ if name in param_specs:
296
+ param_spec = param_specs[name]
297
+ # Keep the established unresolved/untyped-parameter compatibility
298
+ # route: scalar TypeSpecs inferred only from a call site did not
299
+ # historically mask a same-named top-level collection registry.
300
+ # Declared scalar/UDT parameters do shadow it, while inferred or
301
+ # declared collection parameters always carry their exact kind.
302
+ if (param_spec.kind in {"array", "map", "matrix", "udt"}
303
+ or name in getattr(
304
+ self, "_current_func_declared_param_names", set()
305
+ )):
306
+ return param_spec
307
+ # During callable emission this is the copy-on-write lexical overlay;
308
+ # outside a callable it is the live top-level registry, including
309
+ # aliases discovered by codegen after construction.
310
+ return self._collection_types.get(name)
311
+
312
+ def _collection_name_is_lexically_shadowed(self, name: str) -> bool:
313
+ """Whether ``name`` is bound in the active callable/block scope.
314
+
315
+ A ``None`` collection lookup alone cannot distinguish a real scalar or
316
+ UDT tombstone from an absent name. Callers that otherwise fall back to
317
+ the analyzer's popped symbol table use this predicate to avoid
318
+ resurrecting a hidden top-level collection. Untyped parameters retain
319
+ their established compatibility path and are intentionally not tested
320
+ here.
321
+ """
322
+ if name in getattr(self, "_current_loop_vars", set()):
323
+ return True
324
+ if name in getattr(self, "_current_func_collection_specs", {}):
325
+ return True
326
+ return name in getattr(self, "_current_func_collection_shadows", set())
327
+
328
+ def _activate_callable_collection_binding(
329
+ self, name: str, spec: TypeSpec | None
330
+ ) -> None:
331
+ """Install one callable-local binding after its declaration RHS.
332
+
333
+ Every raw kind marker is removed first. A collection installs its exact
334
+ TypeSpec; a scalar/UDT leaves a tombstone in the lexical shadow set.
335
+ Function and block entry already established copy-on-write state, so
336
+ this mutation is restored at the appropriate lexical boundary.
337
+ """
338
+ self._array_vars.discard(name)
339
+ self._map_vars.discard(name)
340
+ self._matrix_specs.pop(name, None)
341
+ self._current_func_collection_specs.pop(name, None)
342
+ self._collection_types.pop(name, None)
343
+ self._current_func_collection_shadows.add(name)
344
+ if spec is None or spec.kind not in {"array", "map", "matrix"}:
345
+ return
346
+ self._current_func_collection_specs[name] = spec
347
+ self._collection_types[name] = spec
348
+ if spec.kind == "array":
349
+ self._array_vars.add(name)
350
+ elif spec.kind == "map":
351
+ self._map_vars.add(name)
352
+ else:
353
+ self._matrix_specs[name] = spec
354
+
355
+ def _collection_receiver_expr(self, name: str) -> str:
356
+ """C++ receiver for an active collection identifier."""
357
+ pending = getattr(self, "_pending_decl_outer_alias", {})
358
+ if name in pending:
359
+ return pending[name]
360
+ safe = self._safe_name(name)
361
+ owner = getattr(self, "_active_func_name", None)
362
+ exact = (
363
+ self._func_var_storage_name(owner, name)
364
+ if owner is not None
365
+ else name
366
+ )
367
+ # Once an exact callable-local collection declaration has become
368
+ # lexically active, its per-written-callsite variant must read the
369
+ # same cloned member that its declaration-site initializer wrote.
370
+ # Before the declaration, ``_current_func_collection_specs`` has no
371
+ # entry, so an inherited same-named global still resolves normally.
372
+ if (
373
+ name in getattr(self, "_current_func_collection_specs", {})
374
+ and safe in self._active_var_remap
375
+ ):
376
+ return self._active_var_remap[safe]
377
+ # Preserve byte-identical output for every non-qualified callable. A
378
+ # qualified persistent array becomes visible through the raw lexical
379
+ # spelling only after its VarDecl installs this active remap.
380
+ if exact != name:
381
+ return self._active_var_remap.get(safe, safe)
382
+ return safe
383
+
187
384
  def _array_spec_for_name(self, name: str) -> TypeSpec:
188
385
  """Spec for ``array<...>`` variable ``name`` (falls back to array<float>)."""
189
- spec = self._collection_types.get(name)
386
+ spec = self._collection_spec_for_name(name)
190
387
  if spec is not None and spec.kind == "array":
191
388
  return spec
192
389
  return TypeSpec.array(TypeSpec.primitive("float"))
193
390
 
194
391
  def _map_spec_for_name(self, name: str) -> TypeSpec:
195
392
  """Spec for ``map<...>`` variable ``name`` (falls back to map<string, float>)."""
196
- spec = self._collection_types.get(name)
393
+ spec = self._collection_spec_for_name(name)
197
394
  if spec is not None and spec.kind == "map":
198
395
  return spec
199
396
  return TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
200
397
 
398
+ def _array_from_element_spec(self, node) -> TypeSpec | None:
399
+ """Exact scalar element spec used only by ``array.from`` lowering.
400
+
401
+ This intentionally does not make BinOp TypeSpecs globally visible:
402
+ doing so changes unrelated float-band comparator output. Collection
403
+ construction needs the narrower fact so its vector type agrees with
404
+ the analyzer-captured declaration type, including lexical loop binders.
405
+ A primitive user-function result may have only the coarse ``PineType``
406
+ slot populated; recover that exact scalar here rather than silently
407
+ defaulting ``array.from(string_udf())`` to ``std::vector<double>``.
408
+ """
409
+ if isinstance(node, BinOp):
410
+ left = self._array_from_element_spec(node.left)
411
+ right = self._array_from_element_spec(node.right)
412
+ if node.op in ("==", "!=", ">", "<", ">=", "<=", "and", "or"):
413
+ return TypeSpec.primitive("bool")
414
+ if (left is not None and right is not None
415
+ and left.kind == "primitive" and right.kind == "primitive"):
416
+ if left.name == "string" or right.name == "string":
417
+ return TypeSpec.primitive("string")
418
+ if node.op == "/" or left.name == "float" or right.name == "float":
419
+ return TypeSpec.primitive("float")
420
+ if left.name == "int" and right.name == "int":
421
+ return TypeSpec.primitive("int")
422
+ return None
423
+ spec = self._type_spec_from_expr(node)
424
+ if spec is not None:
425
+ return spec
426
+ if isinstance(node, FuncCall):
427
+ func_name, namespace = self._resolve_callee(node.callee)
428
+ if namespace is None:
429
+ func_info = self._func_info_map.get(func_name)
430
+ pine_type = getattr(func_info, "return_type", None)
431
+ primitive_name = {
432
+ PineType.INT: "int",
433
+ PineType.FLOAT: "float",
434
+ PineType.BOOL: "bool",
435
+ PineType.STRING: "string",
436
+ PineType.COLOR: "color",
437
+ }.get(pine_type)
438
+ if primitive_name is not None:
439
+ return TypeSpec.primitive(primitive_name)
440
+ return None
441
+
442
+ @staticmethod
443
+ def _selection_terminal_expr(body: list[ASTNode] | None) -> ASTNode | None:
444
+ """Return one if/switch branch's value expression, if present."""
445
+ if not body:
446
+ return None
447
+ terminal = body[-1]
448
+ return terminal.expr if isinstance(terminal, ExprStmt) else terminal
449
+
450
+ @staticmethod
451
+ def _selection_node_is_na(node: ASTNode | None) -> bool:
452
+ """Whether a selection arm is explicit or implicit Pine ``na``."""
453
+ return (
454
+ node is None
455
+ or isinstance(node, NaLiteral)
456
+ or (isinstance(node, Identifier) and node.name == "na")
457
+ )
458
+
459
+ def _nullable_collection_selection_spec(
460
+ self,
461
+ branches: list[tuple[ASTNode | None, TypeSpec | None]],
462
+ ) -> TypeSpec | None:
463
+ """Unify compatible map/matrix selection arms around typed ``na``."""
464
+ concrete: list[TypeSpec] = []
465
+ for node, spec in branches:
466
+ if self._selection_node_is_na(node):
467
+ continue
468
+ if spec is None or spec.kind not in {"map", "matrix"}:
469
+ return None
470
+ concrete.append(spec)
471
+ if not concrete:
472
+ return None
473
+ first = concrete[0]
474
+ return first if all(spec == first for spec in concrete[1:]) else None
475
+
201
476
  def _type_spec_from_expr(self, node) -> TypeSpec | None:
202
477
  """Best-effort TypeSpec inference for an expression node.
203
478
 
@@ -210,26 +485,139 @@ class TypeInferer:
210
485
  if isinstance(node, StringLiteral):
211
486
  return TypeSpec.primitive("string")
212
487
  if isinstance(node, Identifier):
213
- loop_specs = getattr(self, "_current_loop_var_specs", None)
214
- if loop_specs and node.name in loop_specs:
215
- return loop_specs[node.name]
216
- if node.name in self._collection_types:
217
- return self._collection_types[node.name]
218
- param_specs = getattr(self, "_current_func_param_specs", {})
219
- if node.name in param_specs:
220
- return param_specs[node.name]
221
- if node.name in self._udt_var_types:
222
- return TypeSpec.udt(self._udt_var_types[node.name])
223
- # Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
224
- # method receiver registers in _udt_param_udt so its body getters
225
- # resolve to the drawing udt and dispatch through the §4.3 path.
226
- _pu = getattr(self, "_udt_param_udt", None)
227
- if _pu and node.name in _pu and _pu[node.name] in DRAWING_TYPE_TO_CPP:
228
- return TypeSpec.udt(_pu[node.name])
488
+ # An active lexical UDT/drawing binding must beat a same-spelled
489
+ # top-level primitive or collection. This is intentionally the
490
+ # exact lexical prefix of _identifier_udt_binding(), not its raw-
491
+ # name fallback: method receivers such as ``line ln`` otherwise
492
+ # inherit an unrelated global ``ln`` before dispatch is resolved.
493
+ lexical_drawing = getattr(self, "_lexical_drawing_types", {})
494
+ drawing_cpp = lexical_drawing.get(node.name)
495
+ if drawing_cpp is not None:
496
+ for pine_name, cpp_name in DRAWING_TYPE_TO_CPP.items():
497
+ if cpp_name == drawing_cpp:
498
+ return TypeSpec.udt(pine_name)
499
+ lexical_udts = getattr(self, "_lexical_udt_types", {})
500
+ lexical_udt = lexical_udts.get(node.name)
501
+ if lexical_udt in self._udt_defs:
502
+ return TypeSpec.udt(lexical_udt)
503
+ collection_spec = self._collection_spec_for_name(node.name)
504
+ # This resolver also carries exact primitive loop/parameter/global
505
+ # metadata despite its historical name. Preserve those scalar
506
+ # families here (notably bool for array.from); only arbitrary UDTs
507
+ # need the collision-safe lexical resolver below.
508
+ if (collection_spec is not None
509
+ and collection_spec.kind != "udt"):
510
+ return collection_spec
511
+ found_udt_binding, exact_udt = self._identifier_udt_binding(
512
+ node.name
513
+ )
514
+ if found_udt_binding:
515
+ return (
516
+ TypeSpec.udt(exact_udt)
517
+ if exact_udt is not None
518
+ else None
519
+ )
520
+ if self._collection_name_is_lexically_shadowed(node.name):
521
+ return None
229
522
  sym = self.ctx.symbols.resolve(node.name)
230
- if sym is not None and getattr(sym, "type_spec", None) is not None:
231
- return sym.type_spec
523
+ if sym is not None:
524
+ if getattr(sym, "type_spec", None) is not None:
525
+ return sym.type_spec
526
+ primitive_name = {
527
+ PineType.INT: "int",
528
+ PineType.FLOAT: "float",
529
+ PineType.BOOL: "bool",
530
+ PineType.STRING: "string",
531
+ PineType.COLOR: "color",
532
+ }.get(sym.pine_type)
533
+ if primitive_name is not None:
534
+ return TypeSpec.primitive(primitive_name)
535
+ return None
536
+ if isinstance(node, Subscript):
537
+ # History access preserves the scalar drawing-handle type. This
538
+ # is intentionally not generalized to arrays/maps: their
539
+ # subscripting rules are handled by the collection paths below.
540
+ receiver_spec = self._type_spec_from_expr(node.object)
541
+ if (receiver_spec is not None
542
+ and receiver_spec.kind == "udt"
543
+ and receiver_spec.name in DRAWING_TYPE_TO_CPP):
544
+ return receiver_spec
545
+ return None
546
+ if isinstance(node, Ternary):
547
+ true_spec = self._type_spec_from_expr(node.true_val)
548
+ false_spec = self._type_spec_from_expr(node.false_val)
549
+ collection_spec = self._nullable_collection_selection_spec([
550
+ (node.true_val, true_spec),
551
+ (node.false_val, false_spec),
552
+ ])
553
+ if collection_spec is not None:
554
+ return collection_spec
555
+ if true_spec is not None and true_spec == false_spec:
556
+ return true_spec
557
+ if (true_spec is not None
558
+ and true_spec.kind == "udt"
559
+ and true_spec.name in DRAWING_TYPE_TO_CPP
560
+ and isinstance(node.false_val, NaLiteral)):
561
+ return true_spec
562
+ if (false_spec is not None
563
+ and false_spec.kind == "udt"
564
+ and false_spec.name in DRAWING_TYPE_TO_CPP
565
+ and isinstance(node.true_val, NaLiteral)):
566
+ return false_spec
232
567
  return None
568
+ if isinstance(node, IfStmt):
569
+ true_node = self._selection_terminal_expr(node.body)
570
+ false_node = self._selection_terminal_expr(node.else_body)
571
+ true_spec = self._type_spec_from_expr(true_node)
572
+ false_spec = self._type_spec_from_expr(false_node)
573
+ collection_spec = self._nullable_collection_selection_spec([
574
+ (true_node, true_spec),
575
+ (false_node, false_spec),
576
+ ])
577
+ if collection_spec is not None:
578
+ return collection_spec
579
+ true_is_na = (
580
+ isinstance(true_node, NaLiteral)
581
+ or (isinstance(true_node, Identifier)
582
+ and true_node.name == "na")
583
+ )
584
+ false_is_na = (
585
+ isinstance(false_node, NaLiteral)
586
+ or (isinstance(false_node, Identifier)
587
+ and false_node.name == "na")
588
+ )
589
+ if (true_spec is not None
590
+ and true_spec.kind == "udt"
591
+ and true_spec.name in DRAWING_TYPE_TO_CPP
592
+ and true_spec == false_spec):
593
+ return true_spec
594
+ if (true_spec is not None
595
+ and true_spec.kind == "udt"
596
+ and true_spec.name in DRAWING_TYPE_TO_CPP
597
+ and false_is_na):
598
+ return true_spec
599
+ if (false_spec is not None
600
+ and false_spec.kind == "udt"
601
+ and false_spec.name in DRAWING_TYPE_TO_CPP
602
+ and true_is_na):
603
+ return false_spec
604
+ return None
605
+ if isinstance(node, SwitchStmt):
606
+ branches: list[tuple[ASTNode | None, TypeSpec | None]] = []
607
+ for _case_expr, case_body in node.cases:
608
+ terminal = self._selection_terminal_expr(case_body)
609
+ branches.append((
610
+ terminal,
611
+ self._type_spec_from_expr(terminal),
612
+ ))
613
+ default_terminal = self._selection_terminal_expr(
614
+ node.default_body
615
+ )
616
+ branches.append((
617
+ default_terminal,
618
+ self._type_spec_from_expr(default_terminal),
619
+ ))
620
+ return self._nullable_collection_selection_spec(branches)
233
621
  if isinstance(node, MemberAccess):
234
622
  owner = self._type_spec_from_expr(node.object)
235
623
  if owner is not None and owner.kind == "udt" and owner.name:
@@ -237,6 +625,73 @@ class TypeInferer:
237
625
  return None
238
626
  if isinstance(node, FuncCall):
239
627
  func_name, namespace = self._resolve_callee(node.callee)
628
+ if namespace is None:
629
+ func_info = self._func_info_map.get(func_name)
630
+ return_spec = getattr(func_info, "return_type_spec", None)
631
+ if return_spec is not None:
632
+ return return_spec
633
+ udt_return = getattr(func_info, "udt_return_type", None)
634
+ if (udt_return in DRAWING_TYPE_TO_CPP
635
+ or udt_return in self._udt_defs):
636
+ return TypeSpec.udt(udt_return)
637
+ if isinstance(node.callee, MemberAccess):
638
+ receiver_spec = self._type_spec_from_expr(
639
+ node.callee.object
640
+ )
641
+ receiver_name = method_receiver_type_name(receiver_spec)
642
+ method_info = (
643
+ getattr(self, "_func_info_map", {}).get(
644
+ f"{receiver_name}.{node.callee.member}"
645
+ )
646
+ if receiver_name is not None
647
+ else None
648
+ )
649
+ if (
650
+ method_info is not None
651
+ and getattr(method_info, "is_udt_method", False)
652
+ ):
653
+ return_spec = getattr(
654
+ method_info, "return_type_spec", None
655
+ )
656
+ if return_spec is not None:
657
+ return return_spec
658
+ udt_return = getattr(
659
+ method_info, "udt_return_type", None
660
+ )
661
+ if (
662
+ udt_return in DRAWING_TYPE_TO_CPP
663
+ or udt_return in self._udt_defs
664
+ ):
665
+ return TypeSpec.udt(udt_return)
666
+ call_site_idx = self._callable_target_callsite_idx(
667
+ method_info,
668
+ node,
669
+ )
670
+ return_pine_type = (
671
+ self._callsite_callable_return_pine_type(
672
+ method_info,
673
+ call_site_idx,
674
+ )
675
+ )
676
+ primitive_name = {
677
+ PineType.INT: "int",
678
+ PineType.FLOAT: "float",
679
+ PineType.BOOL: "bool",
680
+ PineType.STRING: "string",
681
+ PineType.COLOR: "color",
682
+ }.get(return_pine_type)
683
+ return (
684
+ TypeSpec.primitive(primitive_name)
685
+ if primitive_name is not None
686
+ else None
687
+ )
688
+ if (
689
+ receiver_spec is not None
690
+ and receiver_spec.kind == "udt"
691
+ and receiver_spec.name in self._udt_defs
692
+ and node.callee.member == "copy"
693
+ ):
694
+ return receiver_spec
240
695
  # ticker.* constructors (inherit/standard/heikinashi) return a symbol
241
696
  # string; without this the member-type inference defaults to double
242
697
  # and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
@@ -255,6 +710,13 @@ class TypeInferer:
255
710
  return TypeSpec.udt("chart.point")
256
711
  if namespace == "str" and func_name == "split":
257
712
  return TypeSpec.array(TypeSpec.primitive("string"))
713
+ if namespace == "matrix" and func_name == "new":
714
+ elem = (
715
+ self._type_spec_from_hint_name(targs[0])
716
+ if targs
717
+ else TypeSpec.primitive("float")
718
+ )
719
+ return TypeSpec.matrix(elem or TypeSpec.primitive("float"))
258
720
  if namespace == "array" and func_name in (
259
721
  "new", "new_float", "new_int", "new_bool", "new_string", "from",
260
722
  ) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
@@ -273,41 +735,93 @@ class TypeInferer:
273
735
  if targs:
274
736
  return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
275
737
  if func_name == "from" and node.args:
276
- return TypeSpec.array(self._type_spec_from_expr(node.args[0]) or TypeSpec.primitive("float"))
738
+ return TypeSpec.array(
739
+ self._array_from_element_spec(node.args[0])
740
+ or TypeSpec.primitive("float")
741
+ )
277
742
  return TypeSpec.array(TypeSpec.primitive("float"))
278
743
  # Functional-form array element/copy accessors: the receiver is
279
744
  # the first argument (``array.copy(arr)``), mirroring the
280
745
  # method-form handling below (``arr.copy()``).
281
- if (namespace == "array" and node.args
746
+ if (namespace == "array"
282
747
  and func_name in ("copy", "slice", "get", "first", "last",
283
748
  "pop", "shift", "remove")):
284
- arg_spec = self._type_spec_from_expr(node.args[0])
749
+ receiver_node = node.args[0] if node.args else node.kwargs.get("id")
750
+ arg_spec = self._type_spec_from_expr(receiver_node)
285
751
  if arg_spec is not None and arg_spec.kind == "array":
286
752
  if func_name in ("copy", "slice"):
287
753
  return arg_spec
288
754
  return arg_spec.element
755
+ if namespace == "matrix" and func_name in MATRIX_RETURNING_METHODS:
756
+ receiver_node = node.args[0] if node.args else node.kwargs.get("id")
757
+ receiver_spec = self._type_spec_from_expr(receiver_node)
758
+ if receiver_spec is not None and receiver_spec.kind == "matrix":
759
+ return receiver_spec
289
760
  if namespace == "map" and func_name == "new":
290
761
  key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
291
762
  val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
292
763
  return TypeSpec.map(key or TypeSpec.primitive("string"), val or TypeSpec.primitive("float"))
293
- if namespace in self._udt_defs and func_name == "new":
764
+ if namespace == "map" and func_name in {
765
+ "put", "get", "remove", "contains", "size", "keys",
766
+ "values", "copy", "put_all", "clear",
767
+ } and node.args:
768
+ receiver_spec = self._type_spec_from_expr(node.args[0])
769
+ if receiver_spec is not None and receiver_spec.kind == "map":
770
+ if func_name in ("put", "get", "remove"):
771
+ return receiver_spec.value
772
+ if func_name == "keys":
773
+ return TypeSpec.array(
774
+ receiver_spec.key or TypeSpec.primitive("string")
775
+ )
776
+ if func_name == "values":
777
+ return TypeSpec.array(
778
+ receiver_spec.value or TypeSpec.primitive("float")
779
+ )
780
+ if func_name == "copy":
781
+ return receiver_spec
782
+ if func_name == "contains":
783
+ return TypeSpec.primitive("bool")
784
+ if func_name == "size":
785
+ return TypeSpec.primitive("int")
786
+ if namespace in self._udt_defs and func_name in {"new", "copy"}:
294
787
  return TypeSpec.udt(namespace)
295
788
  if isinstance(node.callee, MemberAccess):
296
789
  recv_spec = self._type_spec_from_expr(node.callee.object)
790
+ member_name = node.callee.member
297
791
  if recv_spec is not None and recv_spec.kind == "array":
298
- if func_name in ("get", "first", "last", "pop", "shift", "remove"):
792
+ if member_name in (
793
+ "get", "first", "last", "pop", "shift", "remove",
794
+ ):
299
795
  return recv_spec.element
300
- if func_name in ("copy", "slice"):
796
+ if member_name in ("copy", "slice"):
301
797
  return recv_spec
302
798
  if recv_spec is not None and recv_spec.kind == "map":
303
- if func_name in ("get", "remove"):
799
+ if member_name in ("put", "get", "remove"):
304
800
  return recv_spec.value
305
- if func_name == "keys":
801
+ if member_name == "keys":
306
802
  return TypeSpec.array(recv_spec.key or TypeSpec.primitive("string"))
307
- if func_name == "values":
803
+ if member_name == "values":
308
804
  return TypeSpec.array(recv_spec.value or TypeSpec.primitive("float"))
805
+ if member_name == "copy":
806
+ return recv_spec
807
+ if member_name == "contains":
808
+ return TypeSpec.primitive("bool")
809
+ if member_name == "size":
810
+ return TypeSpec.primitive("int")
811
+ receiver_name = method_receiver_type_name(recv_spec)
812
+ if receiver_name is not None:
813
+ method_info = getattr(
814
+ self, "_func_info_map", {}
815
+ ).get(
816
+ f"{receiver_name}.{member_name}"
817
+ )
818
+ return_spec = getattr(
819
+ method_info, "return_type_spec", None
820
+ )
821
+ if return_spec is not None:
822
+ return return_spec
309
823
  if recv_spec is not None and recv_spec.kind == "matrix":
310
- if func_name in ("copy", "submatrix", "transpose", "concat"):
824
+ if func_name in MATRIX_RETURNING_METHODS:
311
825
  return recv_spec
312
826
  if func_name in ("row", "col"):
313
827
  return TypeSpec.array(recv_spec.element)
@@ -325,6 +839,76 @@ class TypeInferer:
325
839
  return TypeSpec.udt("line")
326
840
  return None
327
841
 
842
+ def _identifier_udt_binding(
843
+ self, name: str
844
+ ) -> tuple[bool, str | None]:
845
+ """Return ``(binding_known, exact_udt_or_none)`` for ``name``.
846
+
847
+ ``ctx.udt_var_types`` is a legacy raw-name registry: a declaration in
848
+ an unrelated callable can overwrite the type of a same-named global.
849
+ Every correctness-sensitive consumer must prefer the active lexical
850
+ binding (including scalar tombstones), then exact parameter/local and
851
+ direct-program metadata, before consulting that registry.
852
+ """
853
+ known_udts = set(self._udt_defs) | set(DRAWING_TYPE_TO_CPP)
854
+
855
+ lexical_drawing = getattr(self, "_lexical_drawing_types", {})
856
+ if name in lexical_drawing and lexical_drawing[name] is not None:
857
+ cpp_type = lexical_drawing[name]
858
+ for pine_name, candidate_cpp in DRAWING_TYPE_TO_CPP.items():
859
+ if candidate_cpp == cpp_type:
860
+ return True, pine_name
861
+
862
+ lexical = getattr(self, "_lexical_udt_types", {})
863
+ if name in lexical:
864
+ candidate = lexical[name]
865
+ return True, candidate if candidate in known_udts else None
866
+
867
+ param_specs = getattr(self, "_current_func_param_specs", {})
868
+ param_spec = param_specs.get(name) or param_specs.get(
869
+ self._safe_name(name)
870
+ )
871
+ if (param_spec is not None
872
+ and param_spec.kind == "udt"
873
+ and param_spec.name in known_udts):
874
+ return True, param_spec.name
875
+ param_types = getattr(self, "_current_func_param_types", {})
876
+ if name in param_types or self._safe_name(name) in param_types:
877
+ return True, None
878
+
879
+ local_types = getattr(self, "_current_func_local_types", {})
880
+ if name in local_types:
881
+ local_cpp = local_types[name]
882
+ candidate = local_cpp.removesuffix("&").removesuffix("*")
883
+ return True, candidate if candidate in known_udts else None
884
+
885
+ param_udts = getattr(self, "_udt_param_udt", {})
886
+ candidate = param_udts.get(name) or param_udts.get(
887
+ self._safe_name(name)
888
+ )
889
+ if candidate in known_udts:
890
+ return True, candidate
891
+
892
+ global_types = getattr(self, "_global_udt_types", {})
893
+ if name in global_types:
894
+ candidate = global_types[name]
895
+ return True, candidate if candidate in known_udts else None
896
+
897
+ member_resolver = getattr(self, "_member_udt_type", None)
898
+ candidate = (
899
+ member_resolver(name)
900
+ if callable(member_resolver)
901
+ else self._udt_var_types.get(name)
902
+ )
903
+ if candidate is not None:
904
+ return True, candidate if candidate in known_udts else None
905
+ return False, None
906
+
907
+ def _identifier_udt_type(self, name: str) -> str | None:
908
+ """Exact UDT type for ``name``; scalar tombstones return ``None``."""
909
+ _found, udt_type = self._identifier_udt_binding(name)
910
+ return udt_type
911
+
328
912
  # ------------------------------------------------------------------
329
913
  # Method lowering for collection types (used by visit_call paths)
330
914
  # ------------------------------------------------------------------
@@ -377,7 +961,11 @@ class TypeInferer:
377
961
  if method == "copy":
378
962
  lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
379
963
  elif method == "slice":
380
- lower_receiver = lambda recv: f"{arr_cpp_type}({recv}.begin()+(int)({args[0]}),{recv}.begin()+(int)({args[1]}))"
964
+ # Bounds-checked in the shared helper; the element type stays
965
+ # caller-supplied so the typed lane keeps its own vector type.
966
+ lower_receiver = lambda recv: checked_array_slice(
967
+ recv, args, result_type=arr_cpp_type
968
+ )
381
969
  elif method == "join" and elem_cpp == "std::string":
382
970
  sep = args[0] if args else 'std::string(",")'
383
971
  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; }}()"
@@ -402,42 +990,77 @@ class TypeInferer:
402
990
  f"codegen: unhandled array method '{method}' — analyzer should have "
403
991
  f"rejected. Add it to ARRAY_METHODS."
404
992
  )
405
- lower_receiver = lambda recv: ARRAY_METHODS[method](recv, args)
993
+ # Pine evaluates call arguments before entering the array
994
+ # calculation, even when an empty receiver makes the result ``na``.
995
+ # The empty guards in these methods must therefore consume a
996
+ # one-evaluation binding rather than leaving the original argument
997
+ # expression after the guard. Build the binding into
998
+ # ``lower_receiver`` so a temporary receiver is still evaluated
999
+ # first by ``_array_receiver_once_expr``.
1000
+ eager_scalar_arg_methods = {
1001
+ "stdev": (0,),
1002
+ "variance": (0,),
1003
+ "percentile_linear_interpolation": (0,),
1004
+ "percentile_nearest_rank": (0,),
1005
+ }
1006
+ bound_args = list(args)
1007
+ arg_bindings: list[tuple[str, str]] = []
1008
+ occupied = "\n".join((array_expr, *args))
1009
+ counter = getattr(self, "_array_arg_counter", 0)
1010
+ for arg_index in eager_scalar_arg_methods.get(method, ()):
1011
+ if arg_index >= len(args):
1012
+ continue
1013
+ while True:
1014
+ token = f"__pf_array_arg_{counter}"
1015
+ counter += 1
1016
+ if token not in occupied:
1017
+ break
1018
+ bound_args[arg_index] = token
1019
+ arg_bindings.append((token, args[arg_index]))
1020
+ self._array_arg_counter = counter
1021
+
1022
+ def lower_receiver(recv: str) -> str:
1023
+ lowered = ARRAY_METHODS[method](recv, bound_args)
1024
+ for token, original in reversed(arg_bindings):
1025
+ lowered = (
1026
+ f"[&](){{ auto {token}=({original}); "
1027
+ f"return {lowered}; }}()"
1028
+ )
1029
+ return lowered
406
1030
 
407
1031
  return self._array_receiver_once_expr(array_expr, args, lower_receiver)
408
1032
 
409
1033
  def _map_method_expr(
410
1034
  self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
411
1035
  ) -> str:
412
- """Lower ``map.method(...)`` to its C++ form using the receiver's spec for default-key/value typing."""
413
- spec = spec or TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
414
- key_cpp = self._type_spec_to_cpp(spec.key)
415
- value_cpp = self._type_spec_to_cpp(spec.value)
416
- map_cpp = self._type_spec_to_cpp(spec)
417
- default_value = (
418
- 'std::string("")' if value_cpp == "std::string"
419
- else ("false" if value_cpp == "bool" else self._default_for_type(value_cpp))
420
- )
1036
+ """Lower a Pine map operation to the ordered, alias-preserving runtime.
1037
+
1038
+ ``PineMap`` owns all missing-value, insertion-order and explicit-copy
1039
+ semantics. Keeping this lowering as a direct method delegation also
1040
+ guarantees that each receiver and argument expression occurs exactly
1041
+ once; the typed-parameter lane may still wrap arguments to impose
1042
+ Pine's source evaluation order.
1043
+ """
421
1044
  if method == "put":
422
- return f"({map_expr}[{args[0]}] = {args[1]})"
1045
+ return f"{map_expr}.put({args[0]}, {args[1]})"
423
1046
  if method == "get":
424
- return f"({map_expr}.count({args[0]}) ? {map_expr}[{args[0]}] : {default_value})"
1047
+ return f"{map_expr}.get({args[0]})"
425
1048
  if method == "remove":
426
- return f"[&](){{ auto it={map_expr}.find({args[0]}); if(it!={map_expr}.end()){{ auto v=it->second; {map_expr}.erase(it); return v; }} return {default_value}; }}()"
1049
+ return f"{map_expr}.remove({args[0]})"
427
1050
  if method == "contains":
428
- return f"({map_expr}.count({args[0]}) > 0)"
1051
+ return f"{map_expr}.contains({args[0]})"
429
1052
  if method == "size":
430
- return f"(double){map_expr}.size()"
1053
+ return f"{map_expr}.size()"
431
1054
  if method == "clear":
432
1055
  return f"{map_expr}.clear()"
433
1056
  if method == "keys":
434
- return f"[&](){{ std::vector<{key_cpp}> v; for(auto& p:{map_expr}) v.push_back(p.first); return v; }}()"
1057
+ return f"{map_expr}.keys()"
435
1058
  if method == "values":
436
- return f"[&](){{ std::vector<{value_cpp}> v; for(auto& p:{map_expr}) v.push_back(p.second); return v; }}()"
1059
+ return f"{map_expr}.values()"
437
1060
  if method == "copy":
438
- return f"{map_cpp}({map_expr})"
1061
+ return f"{map_expr}.copy()"
439
1062
  if method == "put_all":
440
- return f"{map_expr}.insert({args[0]}.begin(), {args[0]}.end())"
1063
+ return f"{map_expr}.put_all({args[0]})"
441
1064
  # Defensive: support_checker rejects any map.* method not in SUPPORTED_MAP
442
1065
  # (derived from MAP_METHODS, which mirrors this if-chain). Reaching here
443
1066
  # means the checker was bypassed or the tables drifted.
@@ -452,13 +1075,39 @@ class TypeInferer:
452
1075
 
453
1076
  def _type_for_decl(self, node: VarDecl) -> str:
454
1077
  """Determine the C++ type for a ``VarDecl``: explicit hint, then symbol, then RHS inference."""
1078
+ def promote_wide_int(cpp_type: str) -> str:
1079
+ if cpp_type != "int":
1080
+ return cpp_type
1081
+ owner = self._func_info_map.get(
1082
+ getattr(self, "_active_func_name", "") or ""
1083
+ )
1084
+ if self._expr_returns_wide_int(
1085
+ node.value,
1086
+ owner,
1087
+ set(),
1088
+ getattr(self, "_active_call_site_idx", None),
1089
+ ) or self._is_int64_builtin_init(node.name):
1090
+ return "int64_t"
1091
+ return cpp_type
1092
+
455
1093
  if node.type_hint:
456
1094
  spec = self._type_spec_from_hint_name(node.type_hint)
457
1095
  if spec is not None:
458
- return self._type_spec_to_cpp(spec)
1096
+ return promote_wide_int(self._type_spec_to_cpp(spec))
459
1097
  if node.type_hint in self._udt_defs:
460
1098
  return node.type_hint
461
- return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
1099
+ return promote_wide_int(
1100
+ PINE_TYPE_TO_CPP.get(node.type_hint, "double")
1101
+ )
1102
+ # The analyzer records exact callable-local collection bindings before
1103
+ # its lexical scopes are popped. In particular, an inferred local
1104
+ # such as ``selected = cond ? na : global_map`` has a map TypeSpec even
1105
+ # though generic C++ expression inference sees ``na`` as ``double``.
1106
+ # Consume nullable map/matrix forms here. Arrays still lack a runtime
1107
+ # null-ID representation and retain their established inference path.
1108
+ captured = self._callable_collection_bindings.get(id(node))
1109
+ if captured is not None and captured.kind in {"map", "matrix"}:
1110
+ return self._type_spec_to_cpp(captured)
462
1111
  # Drawing handle local (L-N6): a hintless local whose RHS resolves to a
463
1112
  # drawing udt must declare as the handle struct, not the analyzer's
464
1113
  # scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
@@ -475,6 +1124,12 @@ class TypeInferer:
475
1124
  _dret = self._drawing_call_return_cpp(node.value)
476
1125
  if _dret is not None:
477
1126
  return _dret
1127
+ # Analyzer scopes are popped before codegen, so ctx.symbols.resolve may
1128
+ # find a same-named global instead of this active callable's plain
1129
+ # local. The local binding is known from the emitted body inventory;
1130
+ # infer it from its own RHS rather than borrowing the global's PineType.
1131
+ if node.name in getattr(self, "_current_func_locals", set()):
1132
+ return promote_wide_int(self._infer_type(node.value))
478
1133
  sym = self.ctx.symbols.resolve(node.name)
479
1134
  if sym is not None:
480
1135
  inferred = self._infer_type(node.value)
@@ -482,8 +1137,8 @@ class TypeInferer:
482
1137
  return inferred
483
1138
  cpp_type = PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
484
1139
  if cpp_type != "double" or sym.pine_type != PineType.UNKNOWN:
485
- return cpp_type
486
- return self._infer_type(node.value)
1140
+ return promote_wide_int(cpp_type)
1141
+ return promote_wide_int(self._infer_type(node.value))
487
1142
 
488
1143
  def _series_type_for(self, name: str) -> str:
489
1144
  """C++ element type for a series variable's history buffer."""
@@ -499,6 +1154,323 @@ class TypeInferer:
499
1154
  return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
500
1155
  return "double"
501
1156
 
1157
+ def _series_param_element_cpp_type(
1158
+ self,
1159
+ func_info,
1160
+ index: int,
1161
+ call_site_idx: int | None = None,
1162
+ ) -> str:
1163
+ """Canonical element type for one history-referenced UDF parameter.
1164
+
1165
+ A Pine parameter used as ``src[n]`` is a typed time series, not an
1166
+ implicitly-float series. The old emitter hard-coded every such
1167
+ parameter as ``Series<double>``. That happened to work for price
1168
+ sources, but rejected real ``series int``/``series bool`` bindings and
1169
+ let synthetic-expression bridges conceal the same mismatch by
1170
+ converting their values to double.
1171
+
1172
+ Pine ``int`` also carries epoch-millisecond builtins such as ``time``.
1173
+ Use the codegen's existing wide integer representation at this
1174
+ callable boundary so one declared ``int`` parameter can safely accept
1175
+ both ``bar_index`` and timestamp values without narrowing. Scalar
1176
+ integer values are widened when a synthetic bridge is required.
1177
+ """
1178
+ declared_specs = list(
1179
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
1180
+ func_info.name, ()
1181
+ )
1182
+ )
1183
+ declared_spec = (
1184
+ declared_specs[index] if index < len(declared_specs) else None
1185
+ )
1186
+ # A declared type is authoritative across every written call. Only a
1187
+ # truly untyped slot may consult the per-callsite specialization map.
1188
+ spec = declared_spec
1189
+ if spec is not None and spec.kind == "primitive":
1190
+ if spec.name == "int":
1191
+ return "int64_t"
1192
+ if spec.name == "bool":
1193
+ return "bool"
1194
+ if spec.name == "color":
1195
+ return "int"
1196
+ if spec.name == "float":
1197
+ return "double"
1198
+
1199
+ if call_site_idx is not None:
1200
+ variant = getattr(
1201
+ self.ctx, "func_callsite_param_types", {}
1202
+ ).get((func_info.name, call_site_idx))
1203
+ pine_type = (
1204
+ variant[index]
1205
+ if variant is not None and index < len(variant)
1206
+ else PineType.UNKNOWN
1207
+ )
1208
+ if pine_type == PineType.INT:
1209
+ return "int64_t"
1210
+ if pine_type == PineType.BOOL:
1211
+ return "bool"
1212
+ if pine_type == PineType.COLOR:
1213
+ return "int"
1214
+ if pine_type == PineType.FLOAT:
1215
+ return "double"
1216
+
1217
+ specs = list(getattr(func_info, "param_type_specs", ()) or ())
1218
+ spec = specs[index] if index < len(specs) else None
1219
+ if spec is not None and spec.kind == "primitive":
1220
+ if spec.name == "int":
1221
+ return "int64_t"
1222
+ if spec.name == "bool":
1223
+ return "bool"
1224
+ if spec.name == "color":
1225
+ return "int"
1226
+ if spec.name == "float":
1227
+ return "double"
1228
+ param_types = list(getattr(func_info, "param_types", ()) or ())
1229
+ pine_type = param_types[index] if index < len(param_types) else None
1230
+ if pine_type == PineType.INT:
1231
+ return "int64_t"
1232
+ if pine_type == PineType.BOOL:
1233
+ return "bool"
1234
+ if pine_type == PineType.COLOR:
1235
+ return "int"
1236
+ return "double"
1237
+
1238
+ def _callsite_callable_return_pine_type(
1239
+ self, func_info, call_site_idx: int | None
1240
+ ):
1241
+ if call_site_idx is not None:
1242
+ variant = getattr(
1243
+ self.ctx, "func_callsite_return_types", {}
1244
+ ).get((func_info.name, call_site_idx))
1245
+ if variant is not None and variant != PineType.UNKNOWN:
1246
+ return variant
1247
+ return getattr(func_info, "return_type", PineType.UNKNOWN)
1248
+
1249
+ def _func_int_return_uses_wide_history(
1250
+ self,
1251
+ func_info,
1252
+ seen: set[str] | None = None,
1253
+ call_site_idx: int | None = None,
1254
+ ) -> bool:
1255
+ """Whether an integer callable must preserve the ``int64_t`` family.
1256
+
1257
+ History-referenced ``int`` parameters are one source of wide values,
1258
+ but not the only one. A parameterless helper can capture ``time[1]``
1259
+ directly, and an otherwise-pure wrapper can return that helper. The
1260
+ analyzer's Pine-level return type is intentionally just ``INT`` in all
1261
+ of those cases, so follow the terminal expression/call chain here to
1262
+ prevent the C++ callable boundary from narrowing epoch milliseconds.
1263
+ """
1264
+ if self._callsite_callable_return_pine_type(
1265
+ func_info, call_site_idx
1266
+ ) != PineType.INT:
1267
+ return False
1268
+ node = getattr(func_info, "node", None)
1269
+ if node is None:
1270
+ return False
1271
+
1272
+ if seen is None:
1273
+ seen = set()
1274
+ func_name = getattr(func_info, "name", "")
1275
+ if func_name in seen:
1276
+ return False
1277
+ seen = {*seen, func_name}
1278
+
1279
+ series_names = self.ctx.func_series_vars.get(func_info.name, set())
1280
+ if any(
1281
+ param in series_names
1282
+ and self._series_param_element_cpp_type(
1283
+ func_info, index, call_site_idx
1284
+ )
1285
+ == "int64_t"
1286
+ for index, param in enumerate(node.params)
1287
+ ):
1288
+ return True
1289
+
1290
+ if not node.body:
1291
+ return False
1292
+ terminal = node.body[-1]
1293
+ if isinstance(terminal, ExprStmt):
1294
+ terminal = terminal.expr
1295
+ return self._expr_returns_wide_int(
1296
+ terminal, func_info, seen, call_site_idx
1297
+ )
1298
+
1299
+ def _expr_returns_wide_int(
1300
+ self,
1301
+ expr,
1302
+ owner_info,
1303
+ seen: set[str],
1304
+ call_site_idx: int | None = None,
1305
+ ) -> bool:
1306
+ """Trace wide integer provenance through a callable return expression."""
1307
+ if expr is None:
1308
+ return False
1309
+ if isinstance(expr, Subscript):
1310
+ return self._expr_returns_wide_int(
1311
+ expr.object, owner_info, seen, call_site_idx
1312
+ )
1313
+ if isinstance(expr, Identifier):
1314
+ # A terminal local can be a narrow-looking alias for ``time[1]``.
1315
+ # Resolve its declaration inside this callable without consulting
1316
+ # the already-popped analyzer scope.
1317
+ node = getattr(owner_info, "node", None)
1318
+ if node is not None:
1319
+ if expr.name in node.params:
1320
+ return False
1321
+ local_key = (
1322
+ f"{getattr(owner_info, 'name', '')}:local:{expr.name}"
1323
+ )
1324
+ if local_key in seen:
1325
+ return False
1326
+ local_seen = {*seen, local_key}
1327
+ for child in self._walk_ast_list(node.body):
1328
+ if (
1329
+ isinstance(child, VarDecl)
1330
+ and child.name == expr.name
1331
+ and child.value is not None
1332
+ ):
1333
+ if self._expr_returns_wide_int(
1334
+ child.value,
1335
+ owner_info,
1336
+ local_seen,
1337
+ call_site_idx,
1338
+ ):
1339
+ return True
1340
+ if (
1341
+ isinstance(child, Assignment)
1342
+ and isinstance(child.target, Identifier)
1343
+ and child.target.name == expr.name
1344
+ ):
1345
+ if self._expr_returns_wide_int(
1346
+ child.value,
1347
+ owner_info,
1348
+ local_seen,
1349
+ call_site_idx,
1350
+ ):
1351
+ return True
1352
+ return self._expr_is_int64_builtin(expr)
1353
+ if self._expr_is_int64_builtin(expr):
1354
+ return True
1355
+ if isinstance(expr, FuncCall):
1356
+ func_name, namespace = self._resolve_callee(expr.callee)
1357
+ if namespace is None and func_name in {
1358
+ "int",
1359
+ "float",
1360
+ "nz",
1361
+ "fixnan",
1362
+ }:
1363
+ # Casts and Pine's missing-value wrappers preserve the wide
1364
+ # integer provenance needed by an int destination. ``nz`` may
1365
+ # obtain it from either the source or replacement argument.
1366
+ return any(
1367
+ self._expr_returns_wide_int(
1368
+ arg, owner_info, seen, call_site_idx
1369
+ )
1370
+ for arg in expr.args
1371
+ )
1372
+ if namespace == "math" and func_name in {"abs", "max", "min"}:
1373
+ return any(
1374
+ self._expr_returns_wide_int(
1375
+ arg, owner_info, seen, call_site_idx
1376
+ )
1377
+ for arg in expr.args
1378
+ )
1379
+
1380
+ callee_info = None
1381
+ if namespace is None and func_name:
1382
+ callee_info = self._func_info_map.get(func_name)
1383
+ elif isinstance(expr.callee, MemberAccess):
1384
+ receiver = expr.callee.object
1385
+ owner_node = getattr(owner_info, "node", None)
1386
+ if (
1387
+ getattr(owner_info, "is_udt_method", False)
1388
+ and owner_node is not None
1389
+ and owner_node.params
1390
+ and isinstance(receiver, Identifier)
1391
+ and receiver.name == owner_node.params[0]
1392
+ ):
1393
+ receiver_specs = list(
1394
+ getattr(owner_info, "param_type_specs", ()) or ()
1395
+ )
1396
+ receiver_name = method_receiver_type_name(
1397
+ receiver_specs[0] if receiver_specs else None
1398
+ ) or getattr(owner_info, "udt_type_name", None)
1399
+ if receiver_name is not None:
1400
+ callee_info = self._func_info_map.get(
1401
+ f"{receiver_name}.{expr.callee.member}"
1402
+ )
1403
+ if callee_info is None:
1404
+ receiver_spec = self._type_spec_from_expr(receiver)
1405
+ receiver_name = method_receiver_type_name(
1406
+ receiver_spec
1407
+ )
1408
+ if receiver_name is not None:
1409
+ callee_info = self._func_info_map.get(
1410
+ f"{receiver_name}.{expr.callee.member}"
1411
+ )
1412
+ return (
1413
+ callee_info is not None
1414
+ and self._func_int_return_uses_wide_history(
1415
+ callee_info,
1416
+ seen,
1417
+ call_site_idx=call_site_idx,
1418
+ )
1419
+ )
1420
+ if isinstance(expr, BinOp):
1421
+ return (
1422
+ self._expr_returns_wide_int(
1423
+ expr.left, owner_info, seen, call_site_idx
1424
+ )
1425
+ or self._expr_returns_wide_int(
1426
+ expr.right, owner_info, seen, call_site_idx
1427
+ )
1428
+ )
1429
+ if isinstance(expr, UnaryOp):
1430
+ return self._expr_returns_wide_int(
1431
+ expr.operand, owner_info, seen, call_site_idx
1432
+ )
1433
+ if isinstance(expr, Ternary):
1434
+ return (
1435
+ self._expr_returns_wide_int(
1436
+ expr.true_val, owner_info, seen, call_site_idx
1437
+ )
1438
+ or self._expr_returns_wide_int(
1439
+ expr.false_val, owner_info, seen, call_site_idx
1440
+ )
1441
+ )
1442
+ if isinstance(expr, IfStmt):
1443
+ branches = [expr.body, expr.else_body]
1444
+ return any(
1445
+ branch
1446
+ and self._expr_returns_wide_int(
1447
+ branch[-1].expr
1448
+ if isinstance(branch[-1], ExprStmt)
1449
+ else branch[-1],
1450
+ owner_info,
1451
+ seen,
1452
+ call_site_idx,
1453
+ )
1454
+ for branch in branches
1455
+ )
1456
+ if isinstance(expr, SwitchStmt):
1457
+ branches = [body for _, body in expr.cases]
1458
+ if expr.default_body:
1459
+ branches.append(expr.default_body)
1460
+ return any(
1461
+ branch
1462
+ and self._expr_returns_wide_int(
1463
+ branch[-1].expr
1464
+ if isinstance(branch[-1], ExprStmt)
1465
+ else branch[-1],
1466
+ owner_info,
1467
+ seen,
1468
+ call_site_idx,
1469
+ )
1470
+ for branch in branches
1471
+ )
1472
+ return False
1473
+
502
1474
  def _expr_is_int64_builtin(self, expr) -> bool:
503
1475
  """True if ``expr`` is a top-level int64-returning Pine builtin: either a
504
1476
  call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
@@ -514,6 +1486,28 @@ class TypeInferer:
514
1486
  return expr.name in INT64_BUILTIN_IDENTIFIERS
515
1487
  return False
516
1488
 
1489
+ def _fixnan_site_cpp_type(self, site) -> str:
1490
+ """Storage type for one fixnan previous-value slot.
1491
+
1492
+ ``PineType.INT`` does not distinguish ordinary counters from epoch
1493
+ milliseconds, but fixnan stores the exact prior value. Inspect the
1494
+ authored argument's wide provenance so the state member cannot narrow
1495
+ ``time[1]`` before an otherwise-int64 local receives it.
1496
+ """
1497
+ cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
1498
+ if site.pine_type != PineType.INT:
1499
+ return cpp_type
1500
+ node = getattr(site, "node", None)
1501
+ args = getattr(node, "args", ()) if node is not None else ()
1502
+ owner_info = self._func_info_map.get(
1503
+ getattr(site, "owner_func", None) or ""
1504
+ )
1505
+ if args and self._expr_returns_wide_int(
1506
+ args[0], owner_info, set(), None
1507
+ ):
1508
+ return "int64_t"
1509
+ return cpp_type
1510
+
517
1511
  def _int64_reassign_targets(self) -> set[str]:
518
1512
  """Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
519
1513
  with an RHS that is a top-level int64-returning builtin. Cached on the
@@ -522,14 +1516,31 @@ class TypeInferer:
522
1516
  cached = getattr(self, "_int64_reassign_cache", None)
523
1517
  if cached is not None:
524
1518
  return cached
525
- from ..ast_nodes import Assignment
526
1519
  targets: set[str] = set()
1520
+ # Callable locals need their lexical owner so a reassignment through a
1521
+ # wrapper/method returning wide history is recognized as well as a
1522
+ # direct ``time[1]`` RHS.
1523
+ for info in getattr(self.ctx, "func_infos", ()):
1524
+ node = getattr(info, "node", None)
1525
+ if node is None:
1526
+ continue
1527
+ for child in self._walk_ast_list(node.body):
1528
+ if (
1529
+ isinstance(child, Assignment)
1530
+ and isinstance(child.target, Identifier)
1531
+ and self._expr_returns_wide_int(
1532
+ child.value, info, set(), None
1533
+ )
1534
+ ):
1535
+ targets.add(child.target.name)
527
1536
  ast = getattr(self.ctx, "ast", None)
528
1537
  if ast is not None:
529
1538
  for node in self._walk_ast(ast):
530
1539
  if (isinstance(node, Assignment)
531
1540
  and isinstance(node.target, Identifier)
532
- and self._expr_is_int64_builtin(node.value)):
1541
+ and self._expr_returns_wide_int(
1542
+ node.value, None, set(), None
1543
+ )):
533
1544
  targets.add(node.target.name)
534
1545
  self._int64_reassign_cache = targets
535
1546
  return targets
@@ -566,12 +1577,15 @@ class TypeInferer:
566
1577
  for ``double`` (already the default lowering), so those paths are
567
1578
  unchanged.
568
1579
  """
569
- # Collections / UDT / drawing handles never take a scalar ``na<T>()``:
570
- # leave them to the drawing-na / default lowering in _visit_rhs_value.
571
- if (name in self._array_vars
572
- or name in self._map_vars
573
- or name in getattr(self, "_matrix_specs", {})
574
- or name in self._udt_var_types):
1580
+ # Maps and matrices use their default-constructed null ID for Pine
1581
+ # ``na``. Arrays still lack a nullable runtime representation.
1582
+ collection_spec = self._collection_spec_for_name(name)
1583
+ if (collection_spec is not None
1584
+ and collection_spec.kind in {"map", "matrix"}):
1585
+ return self._type_spec_to_cpp(collection_spec)
1586
+ if ((collection_spec is not None
1587
+ and collection_spec.kind == "array")
1588
+ or self._identifier_udt_type(name) is not None):
575
1589
  return None
576
1590
  cpp_type: str | None = None
577
1591
  # 1. ``var`` member (class-scope OR function-local: both are recorded in
@@ -605,134 +1619,6 @@ class TypeInferer:
605
1619
  return cpp_type
606
1620
  return None
607
1621
 
608
- # ------------------------------------------------------------------
609
- # BUG C: user-defined-UDT lvalue aliasing
610
- # ------------------------------------------------------------------
611
-
612
- def _is_udt_lvalue(self, expr) -> str | None:
613
- """If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
614
- names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``,
615
- or an element selected from ``array<UDT>``), return its UDT type name;
616
- else ``None``.
617
-
618
- Pine UDTs are reference types, so a local initialised from such an lvalue
619
- and then mutated through must write back to the global. Drawing UDTs are
620
- handled by the separate ``_uses_drawing`` path and are excluded here."""
621
- if isinstance(expr, FuncCall):
622
- callee = expr.callee
623
- func_name, namespace = self._resolve_callee(callee)
624
- receiver = None
625
- if namespace == "array" and func_name in ("get", "first", "last") and expr.args:
626
- receiver = expr.args[0]
627
- elif (isinstance(callee, MemberAccess)
628
- and func_name in ("get", "first", "last")):
629
- receiver = callee.object
630
- if receiver is not None:
631
- spec = self._type_spec_from_expr(receiver)
632
- elem = spec.element if spec is not None and spec.kind == "array" else None
633
- if (elem is not None and elem.kind == "udt" and elem.name in self._udt_defs
634
- and elem.name not in DRAWING_TYPE_TO_CPP):
635
- return elem.name
636
- return None
637
- if not isinstance(expr, Identifier):
638
- return None
639
- udt_t = self._udt_var_types.get(expr.name)
640
- if udt_t is None or udt_t not in self._udt_defs:
641
- return None
642
- if udt_t in DRAWING_TYPE_TO_CPP:
643
- return None
644
- # Must be a known global/class-scope member (not a function param or a
645
- # plain local snapshot) for write-through to be observable.
646
- if expr.name in getattr(self, "_current_func_locals", set()):
647
- # A function-local of UDT type that is itself a persistent ``var``
648
- # member still write-through aliases; but a plain inline local does
649
- # not represent shared state. Only treat ``var`` func-locals (in
650
- # func_var_members) as aliasable shared state.
651
- fname = getattr(self, "_active_func_name", None)
652
- var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
653
- if expr.name not in var_locals:
654
- return None
655
- return udt_t
656
-
657
- def _udt_lvalue_selection_type(self, expr) -> str | None:
658
- """UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
659
- selectable branch is a UDT lvalue of the SAME user-defined UDT type.
660
- Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
661
- ``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
662
- direct = self._is_udt_lvalue(expr)
663
- if direct is not None:
664
- return direct
665
- branches: list = []
666
- if isinstance(expr, Ternary):
667
- branches = [expr.true_val, expr.false_val]
668
- elif isinstance(expr, SwitchStmt):
669
- for _case_expr, stmts in (expr.cases or []):
670
- if not stmts:
671
- return None
672
- last = stmts[-1]
673
- branches.append(last.expr if isinstance(last, ExprStmt) else last)
674
- if expr.default_body:
675
- last = expr.default_body[-1]
676
- branches.append(last.expr if isinstance(last, ExprStmt) else last)
677
- else:
678
- return None
679
- if not branches:
680
- return None
681
- types = {self._is_udt_lvalue(b) for b in branches}
682
- if len(types) == 1 and None not in types:
683
- return next(iter(types))
684
- return None
685
-
686
- def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
687
- """Decide whether a hintless/typed local UDT declaration must ALIAS the
688
- global(s) it selects rather than value-copy (BUG C).
689
-
690
- Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
691
- ``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
692
- to a *different* UDT lvalue, which a C++ reference cannot do), or
693
- ``None`` to keep the existing value-copy semantics.
694
-
695
- Conditions (all required):
696
- * RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
697
- lvalues (``_udt_lvalue_selection_type``).
698
- * The local is MUTATED later in the enclosing function body
699
- (``local.field := ...``) — a pure read-only snapshot needn't alias.
700
-
701
- The mutation requirement is the safety guard: a local that is only read
702
- keeps value semantics, and a local initialised from a non-lvalue (a
703
- ``.new()`` ctor, a function return, or a plain local copy) returns
704
- ``None`` here, preserving intentional independent-copy semantics."""
705
- from ..ast_nodes import Assignment
706
- body = getattr(self, "_current_func_body", None)
707
- if body is None:
708
- return None
709
- udt_t = self._udt_lvalue_selection_type(node.value)
710
- if udt_t is None:
711
- return None
712
- name = node.name
713
- mutated = False
714
- rebinds_to_other_lvalue = False
715
- for stmt in self._walk_ast_list(body):
716
- if not isinstance(stmt, Assignment):
717
- continue
718
- tgt = stmt.target
719
- # Mutation through the local: ``p.field := ...``
720
- if (isinstance(tgt, MemberAccess)
721
- and isinstance(tgt.object, Identifier)
722
- and tgt.object.name == name):
723
- mutated = True
724
- # Rebind of the local itself to another UDT lvalue: ``p := other``
725
- elif isinstance(tgt, Identifier) and tgt.name == name:
726
- if self._udt_lvalue_selection_type(stmt.value) is not None:
727
- rebinds_to_other_lvalue = True
728
- else:
729
- # Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
730
- # aliasing would be wrong; bail to value-copy.
731
- return None
732
- if not mutated:
733
- return None
734
- return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
735
-
736
1622
  # ------------------------------------------------------------------
737
1623
  # BUG 2: collection (array / map / matrix) lvalue aliasing
738
1624
  # ------------------------------------------------------------------
@@ -745,15 +1631,9 @@ class TypeInferer:
745
1631
  if not isinstance(expr, Identifier):
746
1632
  return None
747
1633
  name = expr.name
748
- if name in self._matrix_specs:
749
- return self._matrix_specs[name]
750
- spec = self._collection_types.get(name)
1634
+ spec = self._collection_spec_for_name(name)
751
1635
  if spec is not None and spec.kind in ("array", "map", "matrix"):
752
1636
  return spec
753
- if name in self._array_vars:
754
- return self._array_spec_for_name(name)
755
- if name in self._map_vars:
756
- return self._map_spec_for_name(name)
757
1637
  return None
758
1638
 
759
1639
  def _collection_lvalue_selection_spec(self, expr):
@@ -761,7 +1641,7 @@ class TypeInferer:
761
1641
  whose every selectable branch is a collection lvalue of the SAME C++
762
1642
  type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
763
1643
  returns, and mixed selections keep value-copy semantics). Mirrors
764
- ``_udt_lvalue_selection_type`` for the BUG-2 collection-alias path."""
1644
+ the former UDT alias-selection path."""
765
1645
  direct = self._collection_lvalue_spec(expr)
766
1646
  if direct is not None:
767
1647
  return direct
@@ -821,22 +1701,6 @@ class TypeInferer:
821
1701
  for s in stmts:
822
1702
  yield from self._walk_ast(s)
823
1703
 
824
- def _addr_of_udt_selection(self, expr, local_name: str):
825
- """Render the address-of form of a UDT lvalue selection for a pointer
826
- alias (BUG C rebind case): ``other`` -> ``&(other)``;
827
- ``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
828
- guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
829
- if isinstance(expr, Identifier):
830
- return f"&({self._safe_name(expr.name)})"
831
- if isinstance(expr, Ternary):
832
- cond = self._visit_expr(expr.condition)
833
- t = self._addr_of_udt_selection(expr.true_val, local_name)
834
- f = self._addr_of_udt_selection(expr.false_val, local_name)
835
- return f"({cond} ? {t} : {f})"
836
- # Switch selection: lower to nested ternaries over case equality. Rare in
837
- # practice; fall back to address-of the whole lowered expression.
838
- return f"&({self._visit_expr(expr)})"
839
-
840
1704
  def _infer_cpp_type_for_security_elem(self, node) -> str:
841
1705
  """C++ type for one element of the ``request.security(..., expr, ...)`` payload.
842
1706
 
@@ -882,6 +1746,10 @@ class TypeInferer:
882
1746
  and ternaries / if / switch expressions. Returns the string
883
1747
  ``"double"`` as the safe fallback when no narrower type can be
884
1748
  determined."""
1749
+ if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
1750
+ # A callable-result history read keeps the callable's scalar
1751
+ # family. Collection subscripts follow separate element-type paths.
1752
+ return self._infer_type(node.object)
885
1753
  if isinstance(node, NumberLiteral):
886
1754
  return "double" if isinstance(node.value, float) else "int"
887
1755
  if isinstance(node, BoolLiteral):
@@ -895,7 +1763,8 @@ class TypeInferer:
895
1763
  return "int64_t"
896
1764
  if node.name in BAR_FIELDS or node.name in BAR_BUILTINS:
897
1765
  return "double"
898
- if node.name in self._known_vars:
1766
+ if (node.name in self._known_vars
1767
+ and not self._known_var_is_lexically_shadowed(node.name)):
899
1768
  val = self._known_vars[node.name]
900
1769
  if isinstance(val, bool):
901
1770
  return "bool"
@@ -905,10 +1774,16 @@ class TypeInferer:
905
1774
  return "int"
906
1775
  if isinstance(val, float):
907
1776
  return "double"
1777
+ if node.name in getattr(
1778
+ self, "_current_func_series_param_types", {}
1779
+ ):
1780
+ return self._current_func_series_param_types[node.name]
908
1781
  if node.name in self._current_func_param_types:
909
1782
  return self._current_func_param_types[node.name]
910
1783
  if node.name in getattr(self, "_current_func_local_types", {}):
911
1784
  return self._current_func_local_types[node.name]
1785
+ if node.name in getattr(self, "_current_loop_vars", set()):
1786
+ return "double"
912
1787
  sym = self.ctx.symbols.resolve(node.name)
913
1788
  if sym is not None and getattr(sym, "type_spec", None) is not None:
914
1789
  return self._type_spec_to_cpp(sym.type_spec)
@@ -917,6 +1792,18 @@ class TypeInferer:
917
1792
  return "double"
918
1793
  if isinstance(node, FuncCall):
919
1794
  func_name, namespace = self._resolve_callee(node.callee)
1795
+ # Nested trade-accessor calls bypass the flat namespace signature
1796
+ # table. Their textual metadata accessors return std::string from
1797
+ # the runtime, so hintless locals must not use the double fallback.
1798
+ if (isinstance(node.callee, MemberAccess)
1799
+ and isinstance(node.callee.object, MemberAccess)
1800
+ and isinstance(node.callee.object.object, Identifier)
1801
+ and node.callee.object.object.name == "strategy"
1802
+ and node.callee.object.member in ("closedtrades", "opentrades")
1803
+ and func_name in (
1804
+ "entry_id", "exit_id", "entry_comment", "exit_comment",
1805
+ )):
1806
+ return "std::string"
920
1807
  # Drawing scalar getter return type (get_text -> std::string,
921
1808
  # get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
922
1809
  if getattr(self, "_uses_drawing", False):
@@ -965,17 +1852,48 @@ class TypeInferer:
965
1852
  recv_spec = self._type_spec_from_expr(node.callee.object)
966
1853
  if recv_spec is not None and recv_spec.kind == "array" and member_name == "join":
967
1854
  return "std::string"
968
- if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
969
- fi_u = self._func_info_map.get(f"{recv_spec.name}.{member_name}")
1855
+ receiver_name = method_receiver_type_name(recv_spec)
1856
+ if receiver_name is not None:
1857
+ fi_u = self._func_info_map.get(
1858
+ f"{receiver_name}.{member_name}"
1859
+ )
970
1860
  if fi_u is not None:
971
- return PINE_TYPE_TO_CPP.get(fi_u.return_type, "double")
1861
+ call_site_idx = self._callable_target_callsite_idx(
1862
+ fi_u, node
1863
+ )
1864
+ if self._func_int_return_uses_wide_history(
1865
+ fi_u, call_site_idx=call_site_idx
1866
+ ):
1867
+ return "int64_t"
1868
+ if getattr(fi_u, "return_type_spec", None) is not None:
1869
+ return self._type_spec_to_cpp(fi_u.return_type_spec)
1870
+ return PINE_TYPE_TO_CPP.get(
1871
+ self._callsite_callable_return_pine_type(
1872
+ fi_u, call_site_idx
1873
+ ),
1874
+ "double",
1875
+ )
1876
+ if namespace is None and func_name in self._func_info_map:
1877
+ fi_u = self._func_info_map[func_name]
1878
+ call_site_idx = self._callable_target_callsite_idx(fi_u, node)
1879
+ if self._func_int_return_uses_wide_history(
1880
+ fi_u, call_site_idx=call_site_idx
1881
+ ):
1882
+ return "int64_t"
972
1883
  spec = self._type_spec_from_expr(node)
973
1884
  if spec is not None:
974
1885
  return self._type_spec_to_cpp(spec)
975
1886
  if namespace in self._udt_defs and func_name == "new":
976
1887
  return namespace
977
1888
  if namespace is None and func_name in self._func_info_map:
978
- return PINE_TYPE_TO_CPP.get(self._func_info_map[func_name].return_type, "double")
1889
+ fi_u = self._func_info_map[func_name]
1890
+ return PINE_TYPE_TO_CPP.get(
1891
+ self._callsite_callable_return_pine_type(
1892
+ fi_u,
1893
+ self._callable_target_callsite_idx(fi_u, node),
1894
+ ),
1895
+ "double",
1896
+ )
979
1897
  site = self._get_ta_site(node)
980
1898
  if site is not None:
981
1899
  ta_name = self._ta_name_from_site(site)
@@ -1018,6 +1936,11 @@ class TypeInferer:
1018
1936
  if spec is not None:
1019
1937
  return self._type_spec_to_cpp(spec)
1020
1938
  if isinstance(node, Ternary):
1939
+ ternary_spec = self._type_spec_from_expr(node)
1940
+ if (ternary_spec is not None
1941
+ and ternary_spec.kind == "udt"
1942
+ and ternary_spec.name in DRAWING_TYPE_TO_CPP):
1943
+ return self._type_spec_to_cpp(ternary_spec)
1021
1944
  tt = self._infer_type(node.true_val)
1022
1945
  ft = self._infer_type(node.false_val)
1023
1946
  if tt.startswith("std::vector") or ft.startswith("std::vector"):
@@ -1034,6 +1957,11 @@ class TypeInferer:
1034
1957
  # Block-as-expression cases: read the type of the last statement of
1035
1958
  # the first branch / case; matches Pine semantics for ``x = if...``.
1036
1959
  if isinstance(node, IfStmt):
1960
+ block_spec = self._type_spec_from_expr(node)
1961
+ if (block_spec is not None
1962
+ and block_spec.kind == "udt"
1963
+ and block_spec.name in DRAWING_TYPE_TO_CPP):
1964
+ return self._type_spec_to_cpp(block_spec)
1037
1965
  if node.body:
1038
1966
  last = node.body[-1]
1039
1967
  if isinstance(last, ExprStmt):
@@ -1065,6 +1993,11 @@ class TypeInferer:
1065
1993
  local_types: dict[str, str] = {}
1066
1994
  for stmt in self._walk_ast(func_node):
1067
1995
  if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
1996
+ captured = self._callable_collection_bindings.get(id(stmt))
1997
+ if (captured is not None
1998
+ and captured.kind in {"array", "map", "matrix"}):
1999
+ local_types[stmt.name] = self._type_spec_to_cpp(captured)
2000
+ continue
1068
2001
  if stmt.type_hint:
1069
2002
  spec = self._type_spec_from_hint_name(stmt.type_hint)
1070
2003
  if spec is not None: