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