@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.
@@ -133,14 +133,21 @@ all tables it needs come from ``codegen/tables.py``, AST classes from
133
133
  from __future__ import annotations
134
134
 
135
135
  from ..ast_nodes import (
136
+ ASTNode,
136
137
  FuncCall,
137
138
  Identifier,
138
139
  MemberAccess,
139
140
  NaLiteral,
140
141
  TupleLiteral,
141
142
  StringLiteral,
143
+ VarDecl,
144
+ )
145
+ from ..symbols import TypeSpec, method_receiver_type_name
146
+ from ..method_binding import (
147
+ MethodBindError,
148
+ bind_method_call,
149
+ signature_from_callable,
142
150
  )
143
- from ..symbols import TypeSpec
144
151
  from .. import signatures as sigs
145
152
  from .drawing import ALL_DRAWING_METHODS
146
153
  from .tables import (
@@ -148,9 +155,11 @@ from .tables import (
148
155
  ARRAY_METHODS,
149
156
  BAR_FIELDS,
150
157
  BAR_SERIES_PUSH,
158
+ CHECKED_ARRAY_METHOD_KWARGS,
151
159
  DRAWING_NS,
152
160
  DRAWING_TYPE_TO_CPP,
153
161
  MAP_METHODS,
162
+ MAP_METHOD_KWARGS,
154
163
  MATH_FUNC_MAP,
155
164
  MATRIX_METHODS,
156
165
  MATRIX_METHOD_KWARGS,
@@ -163,7 +172,6 @@ from .tables import (
163
172
  TIME_FIELD_EXPRS,
164
173
  _math_minmax_na_expr,
165
174
  _merge_kwargs,
166
- _merge_kwargs_with_defaults,
167
175
  tz_time_field_lambda,
168
176
  )
169
177
 
@@ -243,6 +251,149 @@ class CallVisitor:
243
251
  return f"{base}_cs{self._active_call_site_idx}"
244
252
  return base
245
253
 
254
+ def _typed_user_method_info(self, receiver, member: str):
255
+ """Resolve an exact typed user method before any builtin method."""
256
+ receiver_spec = self._type_spec_from_expr(receiver)
257
+ receiver_name = method_receiver_type_name(receiver_spec)
258
+ if receiver_name is None:
259
+ return receiver_spec, None
260
+ method_info = self._func_info_map.get(f"{receiver_name}.{member}")
261
+ if (
262
+ method_info is None
263
+ or not getattr(method_info, "is_udt_method", False)
264
+ ):
265
+ return receiver_spec, None
266
+ return receiver_spec, method_info
267
+
268
+ def _bind_typed_method_args(self, method_info, node: FuncCall):
269
+ """Bind typed method arguments through the analyzer-shared rules."""
270
+
271
+ param_names = (
272
+ list(method_info.node.params[1:])
273
+ if method_info.node is not None
274
+ else []
275
+ )
276
+ param_defaults = list(
277
+ getattr(method_info, "param_defaults", ()) or ()
278
+ )[1:]
279
+ hints = list(
280
+ (getattr(method_info.node, "annotations", None) or {}).get(
281
+ "param_type_hints", ()
282
+ )
283
+ if method_info.node is not None
284
+ else ()
285
+ )[1:]
286
+ signature = signature_from_callable(
287
+ method_info.name,
288
+ param_names,
289
+ param_defaults,
290
+ hints,
291
+ )
292
+ try:
293
+ return bind_method_call(signature, node)
294
+ except MethodBindError as exc:
295
+ self._codegen_error(node, str(exc))
296
+ raise AssertionError("unreachable") from exc
297
+
298
+ def _emit_udt_new_expr(
299
+ self,
300
+ type_name: str,
301
+ field_initializers: list[str],
302
+ ) -> str:
303
+ """Allocate one authored UDT record and return its numeric object ID."""
304
+ record_type = self._udt_record_cpp_type(type_name)
305
+ arena = self._udt_arena_member_name(type_name)
306
+ return f"{arena}.create({record_type}{{{', '.join(field_initializers)}}})"
307
+
308
+ def _emit_udt_copy_expr(self, type_name: str, source_cpp: str) -> str:
309
+ """Allocate a detached outer record while shallow-copying nested IDs."""
310
+ return f"{self._udt_arena_member_name(type_name)}.copy({source_cpp})"
311
+
312
+ def _emit_typed_user_method_call(
313
+ self,
314
+ node: FuncCall,
315
+ receiver_spec: TypeSpec,
316
+ method_info,
317
+ ) -> str:
318
+ """Lower a typed extension method with ordinary callable semantics."""
319
+ callee = node.callee
320
+ assert isinstance(callee, MemberAccess)
321
+ receiver_node = callee.object
322
+ fn_cpp = self._udt_method_call_emit_name(method_info, node)
323
+
324
+ binding = self._bind_typed_method_args(method_info, node)
325
+ rest_nodes = list(binding.args_by_param)
326
+ receiver_cpp = self._visit_typed_method_param(
327
+ method_info,
328
+ node,
329
+ receiver_node,
330
+ 0,
331
+ )
332
+ rest_cpp = [
333
+ self._visit_typed_method_param(
334
+ method_info,
335
+ node,
336
+ arg,
337
+ index + 1,
338
+ )
339
+ for index, arg in enumerate(rest_nodes)
340
+ ]
341
+
342
+ receiver_root = receiver_node
343
+ while isinstance(receiver_root, MemberAccess):
344
+ receiver_root = receiver_root.object
345
+ receiver_passes_by_reference = (
346
+ receiver_spec.kind in {"array", "matrix"}
347
+ or (
348
+ receiver_spec.kind == "udt"
349
+ and receiver_spec.name not in self._udt_defs
350
+ )
351
+ )
352
+ return self._ordered_user_call_expr(
353
+ fn_cpp,
354
+ [receiver_node, *rest_nodes],
355
+ [receiver_cpp, *rest_cpp],
356
+ source_order_nodes=[
357
+ receiver_node,
358
+ *binding.evaluation_order,
359
+ ],
360
+ force_stage=(
361
+ receiver_passes_by_reference
362
+ and not isinstance(receiver_root, Identifier)
363
+ ),
364
+ )
365
+
366
+ def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None:
367
+ """Return the primitive profile selected by this emitted call path."""
368
+ cs_info = self.ctx.func_call_cs_map.get(id(node))
369
+ count = self.ctx.func_call_site_counts.get(fi.name, 0)
370
+ if self._active_call_site_idx is not None and (
371
+ cs_info is not None or count > 1
372
+ ):
373
+ return self._active_call_site_idx
374
+ if cs_info is not None and cs_info[0] == fi.name:
375
+ return cs_info[1]
376
+ return None
377
+
378
+ @staticmethod
379
+ def _series_bridge_value_expr(expr_cpp: str, cpp_type: str) -> str:
380
+ """Convert one scalar to a Series element without erasing Pine na.
381
+
382
+ Numeric C++ casts do not translate sentinels: INT64_MIN converted to
383
+ double is finite, while NaN converted to an integer is undefined. A
384
+ history bridge must re-materialize ``na<T>`` in the destination family
385
+ before pushing the value.
386
+ """
387
+ return (
388
+ "([&]() { auto _pf_series_raw = ("
389
+ + expr_cpp
390
+ + "); return is_na(_pf_series_raw) ? na<"
391
+ + cpp_type
392
+ + ">() : static_cast<"
393
+ + cpp_type
394
+ + ">(_pf_series_raw); }())"
395
+ )
396
+
246
397
  def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node) -> str:
247
398
  if isinstance(value_node, NaLiteral):
248
399
  if elem_spec is not None and elem_spec.kind == "udt":
@@ -281,27 +432,660 @@ class CallVisitor:
281
432
  for idx, arg in enumerate(arg_nodes)
282
433
  ]
283
434
 
435
+ def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:
436
+ """Merge checked-array method kwargs into Pine signature order."""
437
+ param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
438
+ if param_names is None:
439
+ return list(node.args)
440
+ return _merge_kwargs(node.args, node.kwargs, param_names, lambda arg: arg)
441
+
442
+ def _array_function_arg_nodes(self, method: str, node: FuncCall) -> list:
443
+ """Merge ``array.method(id=..., ...)`` arguments in signature order."""
444
+ if method == "copy":
445
+ if len(node.args) == 1 and not node.kwargs:
446
+ return list(node.args)
447
+ if not node.args and set(node.kwargs) == {"id"}:
448
+ return [node.kwargs["id"]]
449
+ self._codegen_error(
450
+ node,
451
+ "array.copy: expected exactly one receiver 'id'",
452
+ hint="Use array.copy(source) or array.copy(id=source).",
453
+ )
454
+ param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
455
+ if param_names is None:
456
+ return list(node.args)
457
+ return _merge_kwargs(
458
+ node.args, node.kwargs, ["id", *param_names], lambda arg: arg
459
+ )
460
+
461
+ def _map_identifier_is_visible_binding(self, node: FuncCall) -> bool:
462
+ """Whether ``map`` is a lexical value at this exact source position.
463
+
464
+ Global collection registries are intentionally pre-populated before
465
+ code emission, so raw membership in ``_var_names`` / collection maps
466
+ cannot answer this question: a later ``map`` declaration must not
467
+ capture an earlier built-in ``map.*`` call. Callable/block overlays,
468
+ by contrast, are activated in source order and therefore take first
469
+ refusal. For a direct top-level binding, compare the containing
470
+ top-level statement indexes; the binding becomes visible only after
471
+ its declaration statement, never inside its own RHS.
472
+ """
473
+ name = "map"
474
+ if (
475
+ name in getattr(self, "_current_func_param_types", {})
476
+ or name in getattr(self, "_current_func_collection_specs", {})
477
+ or name in getattr(self, "_current_func_collection_shadows", set())
478
+ or name in getattr(self, "_current_func_local_types", {})
479
+ or name in getattr(self, "_current_loop_vars", set())
480
+ or getattr(self, "_block_map_binding_visible", False)
481
+ ):
482
+ return True
483
+
484
+ top_index_by_node = getattr(self, "_top_level_index_by_ast_node", None)
485
+ top_index_by_span = getattr(self, "_top_level_index_by_source_span", None)
486
+ map_decl_index = getattr(self, "_top_level_map_decl_index", None)
487
+ if top_index_by_node is None or top_index_by_span is None:
488
+ top_index_by_node = {}
489
+ top_index_by_span = {}
490
+ ambiguous_spans = set()
491
+ map_decl_index = None
492
+ for index, statement in enumerate(self.ctx.ast.body):
493
+ for candidate in self._walk_ast(statement):
494
+ top_index_by_node[id(candidate)] = index
495
+ loc = getattr(candidate, "loc", None)
496
+ if loc is not None:
497
+ span = (
498
+ type(candidate), loc.file, loc.line, loc.col, loc.end_col,
499
+ )
500
+ previous = top_index_by_span.get(span)
501
+ if previous is None:
502
+ top_index_by_span[span] = index
503
+ elif previous != index:
504
+ ambiguous_spans.add(span)
505
+ if (
506
+ map_decl_index is None
507
+ and isinstance(statement, VarDecl)
508
+ and statement.name == name
509
+ ):
510
+ map_decl_index = index
511
+ for span in ambiguous_spans:
512
+ top_index_by_span.pop(span, None)
513
+ self._top_level_index_by_ast_node = top_index_by_node
514
+ self._top_level_index_by_source_span = top_index_by_span
515
+ self._top_level_map_decl_index = map_decl_index
516
+
517
+ if map_decl_index is None:
518
+ return False
519
+ call_index = top_index_by_node.get(id(node))
520
+ if call_index is None:
521
+ # Setup-time rewrites (notably request.security timeframe input
522
+ # substitution) may clone an expression. Those clones retain the
523
+ # original source span, so recover their containing top-level
524
+ # statement from the unmodified AST instead of treating every
525
+ # synthetic node as if all globals were already visible.
526
+ loc = getattr(node, "loc", None)
527
+ if loc is not None:
528
+ call_index = top_index_by_span.get(
529
+ (type(node), loc.file, loc.line, loc.col, loc.end_col)
530
+ )
531
+ if call_index is None:
532
+ # With no identity or source ancestry there is no evidence that a
533
+ # later global binding was visible. Fail closed to the built-in
534
+ # namespace; synthetic lexical receivers must preserve provenance.
535
+ return False
536
+ return map_decl_index < call_index
537
+
538
+ def _map_call_arg_nodes(
539
+ self,
540
+ method: str,
541
+ node: FuncCall,
542
+ *,
543
+ functional: bool,
544
+ allow_keywords: bool,
545
+ ) -> list:
546
+ """Validate and bind one established map-call form.
547
+
548
+ Map lowerings index directly into their argument arrays. Before this
549
+ guard, malformed calls therefore either reached a raw ``IndexError``
550
+ or, for keyword-only ``map.*`` functional calls, silently lowered to
551
+ ``0``. Keep routing policy separate from signature validation: only
552
+ the typed-map UDF-parameter method lane currently accepts canonical
553
+ keywords; all other lanes retain their established positional-only
554
+ semantics and fail closed when keywords are supplied.
555
+ """
556
+ if method == "new":
557
+ param_names: list[str] = []
558
+ else:
559
+ method_params = MAP_METHOD_KWARGS.get(method)
560
+ if method_params is None:
561
+ return list(node.args)
562
+ param_names = (["id", *method_params] if functional
563
+ else list(method_params))
564
+
565
+ signature = f"map.{method}({', '.join(param_names)})"
566
+ unknown = sorted(set(node.kwargs) - set(param_names))
567
+ if unknown:
568
+ name = unknown[0]
569
+ hint = f"Expected {signature}."
570
+ if method == "put_all" and name == "from":
571
+ hint = f"Use 'id2=' for the source map. Expected {signature}."
572
+ self._codegen_error(
573
+ node,
574
+ f"map.{method}: unknown keyword argument '{name}'",
575
+ hint=hint,
576
+ )
577
+
578
+ if len(node.args) > len(param_names):
579
+ self._codegen_error(
580
+ node,
581
+ (
582
+ f"map.{method}: too many positional arguments "
583
+ f"(expected {len(param_names)}, got {len(node.args)})"
584
+ ),
585
+ hint=f"Expected {signature}.",
586
+ )
587
+
588
+ for name in param_names[:len(node.args)]:
589
+ if name in node.kwargs:
590
+ self._codegen_error(
591
+ node,
592
+ (
593
+ f"map.{method}: argument '{name}' passed both "
594
+ "positionally and by keyword"
595
+ ),
596
+ hint=f"Expected {signature}; bind each argument once.",
597
+ )
598
+
599
+ bound: list = [None] * len(param_names)
600
+ for index, arg in enumerate(node.args):
601
+ bound[index] = arg
602
+ for name, value in node.kwargs.items():
603
+ bound[param_names.index(name)] = value
604
+
605
+ missing = [
606
+ name for name, value in zip(param_names, bound) if value is None
607
+ ]
608
+ if missing:
609
+ self._codegen_error(
610
+ node,
611
+ f"map.{method}: missing required argument '{missing[0]}'",
612
+ hint=f"Expected {signature}.",
613
+ )
614
+
615
+ if node.kwargs and not allow_keywords:
616
+ form = "functional" if functional else "receiver-method"
617
+ self._codegen_error(
618
+ node,
619
+ (
620
+ f"map.{method}: keyword arguments are not supported "
621
+ f"for this {form} call form"
622
+ ),
623
+ hint=f"Use the established positional form: {signature}.",
624
+ )
625
+
626
+ return bound
627
+
628
+ def _map_param_method_expr(
629
+ self, map_expr: str, method: str, arg_nodes: list, spec: TypeSpec,
630
+ ) -> str:
631
+ """Lower one map operation with receiver-first, ordered single use.
632
+
633
+ C++17 sequences a member-call receiver before its arguments but does
634
+ not order sibling arguments. Pine requires receiver, key, then value.
635
+ Nested immediately-invoked lambdas bind the receiver first and every
636
+ argument in source/signature order, while retaining lvalue aliases and
637
+ extending temporary receiver lifetime through the operation.
638
+ """
639
+ args = [self._visit_expr(arg) for arg in arg_nodes]
640
+ occupied = "\n".join((map_expr, *args))
641
+ counter = getattr(self, "_map_param_arg_counter", 0)
642
+ bindings: list[tuple[str, str]] = []
643
+ for arg in args:
644
+ while True:
645
+ token = f"__pf_map_param_arg_{counter}"
646
+ counter += 1
647
+ if token not in occupied:
648
+ break
649
+ bindings.append((token, arg))
650
+ self._map_param_arg_counter = counter
651
+
652
+ receiver_counter = getattr(self, "_map_receiver_counter", 0)
653
+ while True:
654
+ receiver_token = f"__pf_map_receiver_{receiver_counter}"
655
+ receiver_counter += 1
656
+ if receiver_token not in occupied:
657
+ break
658
+ self._map_receiver_counter = receiver_counter
659
+
660
+ lowered = self._map_method_expr(
661
+ receiver_token, method, [token for token, _arg in bindings], spec
662
+ )
663
+ for token, arg in reversed(bindings):
664
+ lowered = (
665
+ f"[&](auto&& {token})->decltype(auto){{ "
666
+ f"return {lowered}; }}(({arg}))"
667
+ )
668
+ return (
669
+ f"[&](auto&& {receiver_token})->decltype(auto){{ "
670
+ f"return {lowered}; }}(({map_expr}))"
671
+ )
672
+
673
+ def _expr_contains_map_operation(
674
+ self,
675
+ node,
676
+ visiting_callables: frozenset[str] = frozenset(),
677
+ lexical_specs: dict[str, TypeSpec | None] | None = None,
678
+ ) -> bool:
679
+ """Whether an expression directly or transitively operates on a map.
680
+
681
+ C++17 does not order sibling function arguments. We keep ordinary
682
+ calls byte-identical and only stage calls whose argument tree can
683
+ observe map identity/state (including a user helper whose body performs
684
+ such an operation).
685
+ """
686
+ if not isinstance(node, ASTNode):
687
+ return False
688
+ lexical_specs = lexical_specs or {}
689
+ if isinstance(node, FuncCall):
690
+ callee = node.callee
691
+ if isinstance(callee, MemberAccess):
692
+ if (
693
+ isinstance(callee.object, Identifier)
694
+ and callee.object.name == "map"
695
+ and callee.member in (set(MAP_METHODS) | {"new"})
696
+ ):
697
+ return True
698
+ receiver_spec = self._map_effect_type_spec(
699
+ callee.object, lexical_specs
700
+ )
701
+ if (
702
+ receiver_spec is not None
703
+ and receiver_spec.kind == "map"
704
+ and callee.member in MAP_METHODS
705
+ ):
706
+ return True
707
+
708
+ info_key, func_info = self._map_effect_callable_info(
709
+ node, lexical_specs
710
+ )
711
+ if (
712
+ func_info is not None
713
+ and getattr(func_info, "node", None) is not None
714
+ and info_key not in visiting_callables
715
+ ):
716
+ next_visiting = visiting_callables | {info_key}
717
+ child_specs = self._map_effect_callable_specs(
718
+ func_info,
719
+ node,
720
+ lexical_specs,
721
+ )
722
+ if any(
723
+ self._expr_contains_map_operation(
724
+ child,
725
+ next_visiting,
726
+ child_specs,
727
+ )
728
+ for child in func_info.node.body
729
+ ):
730
+ return True
731
+
732
+ for value in vars(node).values():
733
+ if isinstance(value, ASTNode):
734
+ if self._expr_contains_map_operation(
735
+ value, visiting_callables, lexical_specs
736
+ ):
737
+ return True
738
+ elif isinstance(value, list):
739
+ if any(
740
+ self._expr_contains_map_operation(
741
+ item, visiting_callables, lexical_specs
742
+ )
743
+ for item in value
744
+ if isinstance(item, ASTNode)
745
+ ):
746
+ return True
747
+ elif isinstance(value, dict):
748
+ if any(
749
+ self._expr_contains_map_operation(
750
+ item, visiting_callables, lexical_specs
751
+ )
752
+ for item in value.values()
753
+ if isinstance(item, ASTNode)
754
+ ):
755
+ return True
756
+ return False
757
+
758
+ def _map_effect_type_spec(
759
+ self,
760
+ node,
761
+ lexical_specs: dict[str, TypeSpec | None],
762
+ ) -> TypeSpec | None:
763
+ """Resolve an expression type in the callable being inspected.
764
+
765
+ The ordinary codegen resolver describes the function currently being
766
+ emitted, not a transitive helper whose AST is being audited for effects.
767
+ Respect that helper's parameter/local bindings first, then fall back to
768
+ the established expression inference for globals and constructors.
769
+ """
770
+ if isinstance(node, Identifier) and node.name in lexical_specs:
771
+ return lexical_specs[node.name]
772
+ if isinstance(node, MemberAccess):
773
+ owner = self._map_effect_type_spec(node.object, lexical_specs)
774
+ if owner is not None and owner.kind == "udt" and owner.name:
775
+ return (self._udt_field_type_specs.get(owner.name) or {}).get(
776
+ node.member
777
+ )
778
+ if isinstance(node, FuncCall):
779
+ _key, info = self._map_effect_callable_info(node, lexical_specs)
780
+ return_spec = getattr(info, "return_type_spec", None)
781
+ if return_spec is not None:
782
+ return return_spec
783
+ return self._type_spec_from_expr(node)
784
+
785
+ def _map_effect_callable_info(
786
+ self,
787
+ node: FuncCall,
788
+ lexical_specs: dict[str, TypeSpec | None],
789
+ ):
790
+ """Resolve a plain UDF or UDT method for transitive effect analysis."""
791
+ callee = node.callee
792
+ if isinstance(callee, Identifier):
793
+ key = callee.name
794
+ return key, self._func_info_map.get(key)
795
+ if isinstance(callee, MemberAccess):
796
+ receiver_spec = self._map_effect_type_spec(
797
+ callee.object, lexical_specs
798
+ )
799
+ receiver_name = method_receiver_type_name(receiver_spec)
800
+ if receiver_name is not None:
801
+ key = f"{receiver_name}.{callee.member}"
802
+ return key, self._func_info_map.get(key)
803
+ return "", None
804
+
805
+ def _map_effect_callable_specs(
806
+ self,
807
+ func_info,
808
+ call: FuncCall,
809
+ caller_specs: dict[str, TypeSpec | None],
810
+ ) -> dict[str, TypeSpec | None]:
811
+ """Build the inspected callee's lexical TypeSpec environment.
812
+
813
+ Declared and already-inferred parameter specs are authoritative. Any
814
+ unresolved slot is filled from this exact call's argument in the
815
+ caller's lexical environment, which also lets an untyped map flow
816
+ through multiple helper layers before analyzer call-site inference has
817
+ materialized every nested parameter.
818
+ """
819
+ func_node = func_info.node
820
+ params = list(func_node.params) if func_node is not None else []
821
+ declared_specs = list(getattr(func_info, "param_type_specs", ()) or ())
822
+ result: dict[str, TypeSpec | None] = {
823
+ param: declared_specs[index] if index < len(declared_specs) else None
824
+ for index, param in enumerate(params)
825
+ }
826
+
827
+ is_method = bool(getattr(func_info, "is_udt_method", False))
828
+ if is_method and isinstance(call.callee, MemberAccess):
829
+ binding = self._bind_typed_method_args(func_info, call)
830
+ positional = [call.callee.object, *binding.args_by_param]
831
+ else:
832
+ positional = list(call.args)
833
+ for index, actual in enumerate(positional):
834
+ if index >= len(params) or result[params[index]] is not None:
835
+ continue
836
+ result[params[index]] = self._map_effect_type_spec(
837
+ actual, caller_specs
838
+ )
839
+
840
+ if not is_method:
841
+ for name, actual in call.kwargs.items():
842
+ if name not in params:
843
+ continue
844
+ if result[name] is None:
845
+ result[name] = self._map_effect_type_spec(
846
+ actual, caller_specs
847
+ )
848
+
849
+ # Direct callable locals already carry analyzer-owned lexical
850
+ # provenance. They override same-named parameters/globals exactly as
851
+ # they do during real function emission.
852
+ result.update(self._func_collection_types.get(func_info.name, {}))
853
+ return result
854
+
855
+ def _ordered_user_call_expr(
856
+ self,
857
+ call_head: str,
858
+ arg_nodes: list,
859
+ arg_exprs: list[str],
860
+ *,
861
+ source_order_nodes: list | None = None,
862
+ force_stage: bool = False,
863
+ ) -> str:
864
+ """Emit a user call with deterministic argument evaluation when needed.
865
+
866
+ ``arg_nodes``/``arg_exprs`` are in destination parameter order.
867
+ ``source_order_nodes`` records the caller's written order, allowing
868
+ named arguments to be evaluated as written while still occupying their
869
+ declared parameter slots.
870
+ """
871
+ raw = f"{call_head}({', '.join(arg_exprs)})"
872
+ has_map_effect = any(
873
+ self._expr_contains_map_operation(node) for node in arg_nodes
874
+ )
875
+ if not force_stage and (len(arg_exprs) < 2 or not has_map_effect):
876
+ return raw
877
+
878
+ occupied = "\n".join((call_head, *arg_exprs))
879
+ counter = getattr(self, "_ordered_call_arg_counter", 0)
880
+ tokens: list[str] = []
881
+ for _ in arg_exprs:
882
+ while True:
883
+ token = f"__pf_call_arg_{counter}"
884
+ counter += 1
885
+ if token not in occupied:
886
+ break
887
+ tokens.append(token)
888
+ self._ordered_call_arg_counter = counter
889
+
890
+ remaining = list(range(len(arg_nodes)))
891
+ evaluation_order: list[int] = []
892
+ for source_node in source_order_nodes or arg_nodes:
893
+ for index in remaining:
894
+ if arg_nodes[index] is source_node:
895
+ evaluation_order.append(index)
896
+ remaining.remove(index)
897
+ break
898
+ evaluation_order.extend(remaining)
899
+
900
+ lowered = f"{call_head}({', '.join(tokens)})"
901
+ for index in reversed(evaluation_order):
902
+ lowered = (
903
+ f"[&](auto&& {tokens[index]})->decltype(auto){{ "
904
+ f"return {lowered}; }}(({arg_exprs[index]}))"
905
+ )
906
+ return lowered
907
+
908
+ def _visit_typed_method_param(
909
+ self,
910
+ func_info,
911
+ call_node: FuncCall,
912
+ arg_node,
913
+ param_index: int,
914
+ ) -> str:
915
+ """Lower one typed-method parameter with history-Series awareness."""
916
+ param_name = (
917
+ func_info.node.params[param_index]
918
+ if func_info.node is not None
919
+ and param_index < len(func_info.node.params)
920
+ else None
921
+ )
922
+ param_specs = list(
923
+ getattr(func_info, "param_type_specs", ()) or ()
924
+ )
925
+ param_spec = (
926
+ param_specs[param_index]
927
+ if param_index < len(param_specs)
928
+ else None
929
+ )
930
+ if param_spec is not None and param_spec.kind in {"map", "matrix"}:
931
+ # Both UDT-method dispatch paths funnel through this helper after
932
+ # positional/keyword/default merging. Lower a bare ``na`` (and
933
+ # nullable selections containing it) against the declared exact
934
+ # parameter type instead of the legacy numeric ``na<double>()``.
935
+ return self._visit_rhs_value(
936
+ arg_node,
937
+ target_cpp_type=self._type_spec_to_cpp(param_spec),
938
+ )
939
+ method_series = self.ctx.func_series_vars.get(func_info.name, set())
940
+ if param_name not in method_series:
941
+ return self._visit_expr(arg_node)
942
+ expected_cpp_type = self._series_param_element_cpp_type(
943
+ func_info,
944
+ param_index,
945
+ self._callable_target_callsite_idx(func_info, call_node),
946
+ )
947
+ if isinstance(arg_node, Identifier):
948
+ arg_name = arg_node.name
949
+ if (
950
+ (arg_name in BAR_FIELDS or arg_name in BAR_SERIES_PUSH)
951
+ and expected_cpp_type == "double"
952
+ ):
953
+ return f"_s_{arg_name}"
954
+ safe = self._safe_name(arg_name)
955
+ is_current_series_param = (
956
+ arg_name in self._current_func_series_params
957
+ )
958
+ if (
959
+ is_current_series_param
960
+ and self._current_func_series_param_types.get(arg_name)
961
+ == expected_cpp_type
962
+ ):
963
+ return safe
964
+ if self._active_var_remap and safe in self._active_var_remap:
965
+ safe = self._active_var_remap[safe]
966
+ if (
967
+ not is_current_series_param
968
+ and self._binding_is_series(arg_name, safe)
969
+ and self._series_type_for(arg_name) == expected_cpp_type
970
+ ):
971
+ return safe
972
+ expr_cpp = self._visit_expr(arg_node)
973
+ cpp_type = expected_cpp_type
974
+ bridge_cpp = self._series_bridge_value_expr(expr_cpp, cpp_type)
975
+ member = self._inline_history_member(
976
+ "series_arg", call_node, arg_idx=param_index
977
+ )
978
+ return (
979
+ f"([&]() -> const Series<{cpp_type}>& {{ "
980
+ f"{cpp_type} _sv = {bridge_cpp}; "
981
+ f"if (history_advances_new_bar()) {member}.push(_sv); "
982
+ f"else {member}.update(_sv); "
983
+ f"return {member}; }}())"
984
+ )
985
+
986
+ def _visit_udt_method_series_arg(
987
+ self,
988
+ func_info,
989
+ call_node: FuncCall,
990
+ arg_node,
991
+ rest_index: int,
992
+ ) -> str:
993
+ """Backward-compatible wrapper for non-receiver method arguments."""
994
+ return self._visit_typed_method_param(
995
+ func_info,
996
+ call_node,
997
+ arg_node,
998
+ rest_index + 1,
999
+ )
1000
+
284
1001
  def _visit_func_call(self, node: FuncCall) -> str:
285
1002
  callee = node.callee
286
1003
  if isinstance(callee, MemberAccess):
1004
+ recv_spec, method_info = self._typed_user_method_info(
1005
+ callee.object,
1006
+ callee.member,
1007
+ )
1008
+ if method_info is not None and recv_spec is not None:
1009
+ # Pine extension methods are exact type-directed overloads.
1010
+ # They must win before collection/drawing builtins; otherwise
1011
+ # a user ``array<int>.push`` silently executes push_back.
1012
+ return self._emit_typed_user_method_call(
1013
+ node,
1014
+ recv_spec,
1015
+ method_info,
1016
+ )
287
1017
  recv_spec = self._type_spec_from_expr(callee.object)
1018
+ if (
1019
+ recv_spec is not None
1020
+ and recv_spec.kind == "udt"
1021
+ and recv_spec.name in self._udt_defs
1022
+ and callee.member == "copy"
1023
+ ):
1024
+ if node.args or node.kwargs:
1025
+ self._codegen_error(
1026
+ node,
1027
+ f"{recv_spec.name}.copy() expects no arguments",
1028
+ hint="Call copy() on the UDT object without arguments.",
1029
+ )
1030
+ self._reject_unsupported_udt_copy(recv_spec.name, node)
1031
+ return self._emit_udt_copy_expr(
1032
+ recv_spec.name,
1033
+ self._visit_expr(callee.object),
1034
+ )
1035
+ if (
1036
+ recv_spec is not None
1037
+ and recv_spec.kind == "map"
1038
+ and not isinstance(callee.object, (Identifier, MemberAccess))
1039
+ and callee.member in MAP_METHODS
1040
+ ):
1041
+ # Returned, constructed and selected map IDs are ordinary
1042
+ # receivers too. Parenthesize the expression so ternaries and
1043
+ # other compound producers remain one C++ receiver expression.
1044
+ arg_nodes = self._map_call_arg_nodes(
1045
+ callee.member,
1046
+ node,
1047
+ functional=False,
1048
+ allow_keywords=False,
1049
+ )
1050
+ receiver = f"({self._visit_expr(callee.object)})"
1051
+ return self._map_param_method_expr(
1052
+ receiver,
1053
+ callee.member,
1054
+ arg_nodes,
1055
+ recv_spec,
1056
+ )
288
1057
  if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
289
1058
  mk = f"{recv_spec.name}.{callee.member}"
290
1059
  fi_u = self._func_info_map.get(mk)
291
1060
  if fi_u is not None and getattr(fi_u, "is_udt_method", False):
292
1061
  fn_cpp = self._udt_method_call_emit_name(fi_u, node)
293
1062
  recv_e = self._visit_expr(callee.object)
294
- param_names = list(fi_u.node.params[1:]) if fi_u.node else []
295
- # Drop the leading ``self`` slot from param_defaults so the
296
- # parallel array lines up with ``param_names`` (rest of
297
- # the signature). Probe: udt-method-probe-04-default-param.
298
- param_defaults = list(getattr(fi_u, "param_defaults", []) or [])[1:]
299
- rest_nodes = _merge_kwargs_with_defaults(
300
- node.args, node.kwargs, param_names,
301
- param_defaults, lambda x: x,
1063
+ receiver_root = callee.object
1064
+ while isinstance(receiver_root, MemberAccess):
1065
+ receiver_root = receiver_root.object
1066
+ stage_receiver = not isinstance(receiver_root, Identifier)
1067
+ binding = self._bind_typed_method_args(fi_u, node)
1068
+ rest_nodes = list(binding.args_by_param)
1069
+ rest = [
1070
+ self._visit_udt_method_series_arg(
1071
+ fi_u, node, arg, index
1072
+ )
1073
+ for index, arg in enumerate(rest_nodes)
1074
+ ]
1075
+ return self._ordered_user_call_expr(
1076
+ fn_cpp,
1077
+ [callee.object, *rest_nodes],
1078
+ [recv_e, *rest],
1079
+ source_order_nodes=[
1080
+ callee.object,
1081
+ *binding.evaluation_order,
1082
+ ],
1083
+ # Generated UDT methods accept ``T&``. A constructor or
1084
+ # function-return receiver is a C++ rvalue; bind it to a
1085
+ # named forwarding-reference lambda parameter first so
1086
+ # the method sees a valid lvalue for the full call.
1087
+ force_stage=stage_receiver,
302
1088
  )
303
- rest = [self._visit_expr(a) for a in rest_nodes]
304
- return f"{fn_cpp}({', '.join([recv_e] + rest)})"
305
1089
 
306
1090
  # Drawing method dispatch (spec §4.3 / L.1). A KNOWN drawing method on a
307
1091
  # receiver that resolves to a drawing udt — gated on the METHOD NAME
@@ -320,6 +1104,21 @@ class CallVisitor:
320
1104
  list(node.args), node,
321
1105
  )
322
1106
 
1107
+ # Array method on an arbitrary expression receiver, e.g.
1108
+ # ``make_array().get(-1)``, ``m.row(0).last()``, or
1109
+ # ``(cond ? a : b).get(-1)``. Identifier and member receivers have
1110
+ # dedicated paths below; other receiver shapes used to fall through to
1111
+ # ``None(...)`` despite carrying a known array spec.
1112
+ if (isinstance(callee, MemberAccess)
1113
+ and not isinstance(callee.object, (Identifier, MemberAccess))
1114
+ and callee.member in ARRAY_METHODS):
1115
+ recv_spec = self._type_spec_from_expr(callee.object)
1116
+ if recv_spec is not None and recv_spec.kind == "array":
1117
+ recv = self._visit_expr(callee.object)
1118
+ arg_nodes = self._array_method_arg_nodes(callee.member, node)
1119
+ args = self._array_method_args(callee.member, arg_nodes, recv_spec)
1120
+ return self._array_method_expr(recv, callee.member, args, recv_spec)
1121
+
323
1122
  # chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
324
1123
  # aggregate). Routed here BEFORE the obj.field.method receiver logic,
325
1124
  # which would otherwise mis-treat ``chart.point`` as a receiver object
@@ -333,20 +1132,49 @@ class CallVisitor:
333
1132
  obj = callee.object
334
1133
  if isinstance(obj, MemberAccess):
335
1134
  root = obj.object
336
- if not (isinstance(root, Identifier) and root.name in (
1135
+ root_is_builtin_namespace = (
1136
+ isinstance(root, Identifier)
1137
+ and root.name in (
337
1138
  "strategy", "ta", "math", "input", "str", "timeframe", "syminfo",
338
1139
  "barstate", "color", "request", "runtime", "array", "matrix", "map",
339
- )):
1140
+ )
1141
+ )
1142
+ if (
1143
+ root_is_builtin_namespace
1144
+ and root.name == "map"
1145
+ and self._map_identifier_is_visible_binding(node)
1146
+ ):
1147
+ root_is_builtin_namespace = False
1148
+ if not root_is_builtin_namespace:
340
1149
  recv_spec = self._type_spec_from_expr(obj)
341
1150
  recv = self._visit_expr(obj)
342
1151
  meth = callee.member
1152
+ if (
1153
+ recv_spec is not None
1154
+ and recv_spec.kind == "map"
1155
+ and meth in MAP_METHODS
1156
+ ):
1157
+ arg_nodes = self._map_call_arg_nodes(
1158
+ meth,
1159
+ node,
1160
+ functional=False,
1161
+ allow_keywords=False,
1162
+ )
1163
+ return self._map_param_method_expr(
1164
+ recv,
1165
+ meth,
1166
+ arg_nodes,
1167
+ recv_spec,
1168
+ )
343
1169
  raw_args = [self._visit_expr(a) for a in node.args]
344
1170
  if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
1171
+ arg_nodes = self._array_method_arg_nodes(meth, node)
345
1172
  return self._array_method_expr(
346
- recv, meth, self._array_method_args(meth, node.args, recv_spec), recv_spec
1173
+ recv,
1174
+ meth,
1175
+ self._array_method_args(meth, arg_nodes, recv_spec),
1176
+ recv_spec,
347
1177
  )
348
- if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
349
- return self._map_method_expr(recv, meth, raw_args, recv_spec)
350
1178
  args = ", ".join(raw_args)
351
1179
  if meth == "delete":
352
1180
  meth = "_delete_"
@@ -354,27 +1182,101 @@ class CallVisitor:
354
1182
  # obj.method() where obj is a user var/param — not namespace::method
355
1183
  if isinstance(obj, Identifier):
356
1184
  oname = obj.name
357
- if (
1185
+ receiver_is_visible = (
1186
+ oname != "map"
1187
+ or self._map_identifier_is_visible_binding(node)
1188
+ )
1189
+ if receiver_is_visible and (
358
1190
  oname in self._var_names
359
1191
  or oname in self._current_func_param_types
360
1192
  or oname in self._current_loop_vars
361
1193
  or oname in self._global_member_vars
362
1194
  ):
363
1195
  meth_raw = callee.member
364
- # map.put / map.get / … must lower to unordered_map ops, not `.put` on C++
365
- if oname in self._map_vars and meth_raw in MAP_METHODS:
366
- m = self._safe_name(oname)
367
- margs = [self._visit_expr(a) for a in node.args]
368
- return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
369
- if oname in self._array_vars and meth_raw in ARRAY_METHODS:
370
- arr = self._safe_name(oname)
1196
+ # Function parameters have lexical precedence over the
1197
+ # global collection registries. Route a declared/inferred
1198
+ # ``array<T>`` parameter before consulting same-named
1199
+ # global map/array/matrix entries, and retain its element
1200
+ # TypeSpec for checked reads and mutations.
1201
+ param_spec = getattr(
1202
+ self, "_current_func_param_specs", {}
1203
+ ).get(oname)
1204
+ active_local_shadow = (
1205
+ oname in getattr(
1206
+ self, "_current_func_collection_specs", {}
1207
+ )
1208
+ or oname in getattr(
1209
+ self, "_current_func_collection_shadows", set()
1210
+ )
1211
+ )
1212
+ if (
1213
+ param_spec is not None
1214
+ and param_spec.kind == "array"
1215
+ and not active_local_shadow
1216
+ and meth_raw in ARRAY_METHODS
1217
+ ):
1218
+ arr = self._collection_receiver_expr(oname)
1219
+ arg_nodes = self._array_method_arg_nodes(meth_raw, node)
371
1220
  margs = self._array_method_args(
372
- meth_raw, node.args, self._array_spec_for_name(oname)
1221
+ meth_raw, arg_nodes, param_spec
1222
+ )
1223
+ return self._array_method_expr(
1224
+ arr, meth_raw, margs, param_spec
1225
+ )
1226
+ if (
1227
+ param_spec is not None
1228
+ and param_spec.kind == "map"
1229
+ and not active_local_shadow
1230
+ and meth_raw in MAP_METHODS
1231
+ ):
1232
+ m = self._collection_receiver_expr(oname)
1233
+ arg_nodes = self._map_call_arg_nodes(
1234
+ meth_raw,
1235
+ node,
1236
+ functional=False,
1237
+ allow_keywords=True,
373
1238
  )
374
- return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
375
- if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
376
- arr = self._safe_name(oname)
377
- self._check_matrix_method_allowed(meth_raw, self._matrix_specs[oname], node)
1239
+ return self._map_param_method_expr(
1240
+ m, meth_raw, arg_nodes, param_spec
1241
+ )
1242
+ recv_spec = self._collection_spec_for_name(oname)
1243
+ # Resolve the lexical TypeSpec once and route by its exact
1244
+ # kind. Raw membership sets can contain a same-named
1245
+ # binding from another scope; overlapping methods such as
1246
+ # ``get`` must never use those as a tie-breaker.
1247
+ if (
1248
+ recv_spec is not None
1249
+ and recv_spec.kind == "map"
1250
+ and meth_raw in MAP_METHODS
1251
+ ):
1252
+ m = self._collection_receiver_expr(oname)
1253
+ arg_nodes = self._map_call_arg_nodes(
1254
+ meth_raw,
1255
+ node,
1256
+ functional=False,
1257
+ allow_keywords=False,
1258
+ )
1259
+ return self._map_param_method_expr(
1260
+ m, meth_raw, arg_nodes, recv_spec
1261
+ )
1262
+ if (
1263
+ recv_spec is not None
1264
+ and recv_spec.kind == "array"
1265
+ and meth_raw in ARRAY_METHODS
1266
+ ):
1267
+ arr = self._collection_receiver_expr(oname)
1268
+ arg_nodes = self._array_method_arg_nodes(meth_raw, node)
1269
+ margs = self._array_method_args(
1270
+ meth_raw, arg_nodes, recv_spec
1271
+ )
1272
+ return self._array_method_expr(arr, meth_raw, margs, recv_spec)
1273
+ if (
1274
+ recv_spec is not None
1275
+ and recv_spec.kind == "matrix"
1276
+ and meth_raw in MATRIX_METHODS
1277
+ ):
1278
+ arr = self._collection_receiver_expr(oname)
1279
+ self._check_matrix_method_allowed(meth_raw, recv_spec, node)
378
1280
  param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
379
1281
  if param_names and node.kwargs:
380
1282
  margs = _merge_kwargs(
@@ -391,27 +1293,35 @@ class CallVisitor:
391
1293
  f"matrix.{meth_raw}: wrong number of arguments",
392
1294
  hint="Check Pine v6 matrix method signature (positional vs keyword).",
393
1295
  )
394
- safe_o = self._safe_name(oname)
395
- udt_t = self._udt_var_types.get(oname) or self._udt_var_types.get(safe_o)
396
- if udt_t is None:
397
- udt_t = self._udt_param_udt.get(oname) or self._udt_param_udt.get(safe_o)
1296
+ recv_spec = self._type_spec_from_expr(obj)
1297
+ udt_t = (
1298
+ recv_spec.name
1299
+ if recv_spec is not None and recv_spec.kind == "udt"
1300
+ else None
1301
+ )
398
1302
  if udt_t is not None:
399
1303
  mk = f"{udt_t}.{meth_raw}"
400
1304
  fi_u = self._func_info_map.get(mk)
401
1305
  if fi_u is not None and getattr(fi_u, "is_udt_method", False):
402
1306
  fn_cpp = self._udt_method_call_emit_name(fi_u, node)
403
1307
  recv_e = self._visit_expr(obj)
404
- param_names = list(fi_u.node.params[1:]) if fi_u.node else []
405
- # Drop the leading ``self`` slot so param_defaults
406
- # lines up with ``param_names``. Probe:
407
- # udt-method-probe-04-default-param.
408
- param_defaults = list(getattr(fi_u, "param_defaults", []) or [])[1:]
409
- rest_nodes = _merge_kwargs_with_defaults(
410
- node.args, node.kwargs, param_names,
411
- param_defaults, lambda x: x,
1308
+ binding = self._bind_typed_method_args(fi_u, node)
1309
+ rest_nodes = list(binding.args_by_param)
1310
+ rest = [
1311
+ self._visit_udt_method_series_arg(
1312
+ fi_u, node, arg, index
1313
+ )
1314
+ for index, arg in enumerate(rest_nodes)
1315
+ ]
1316
+ return self._ordered_user_call_expr(
1317
+ fn_cpp,
1318
+ [obj, *rest_nodes],
1319
+ [recv_e, *rest],
1320
+ source_order_nodes=[
1321
+ obj,
1322
+ *binding.evaluation_order,
1323
+ ],
412
1324
  )
413
- rest = [self._visit_expr(a) for a in rest_nodes]
414
- return f"{fn_cpp}({', '.join([recv_e] + rest)})"
415
1325
  args = ", ".join(self._visit_expr(a) for a in node.args)
416
1326
  recv = self._visit_expr(obj)
417
1327
  meth = meth_raw
@@ -466,6 +1376,8 @@ class CallVisitor:
466
1376
  uses_precalc = self._ta_site_uses_precalc(site)
467
1377
  if getattr(self, "_precalc_loop_active", False) and uses_precalc:
468
1378
  return f"_precalc_{ta_mem}[i]"
1379
+ if self._is_lazy_saturated_roc3_site(site):
1380
+ return self._lazy_saturated_roc3_expr(site)
469
1381
  if uses_precalc:
470
1382
  return (
471
1383
  f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : "
@@ -497,26 +1409,64 @@ class CallVisitor:
497
1409
  return self._visit_str_call(func_name, node)
498
1410
 
499
1411
  # Map method syntax: m.put(key, val) where namespace is the map variable name
500
- if namespace is not None and namespace in self._map_vars and func_name in MAP_METHODS:
501
- m = self._safe_name(namespace)
502
- args = [self._visit_expr(a) for a in node.args]
503
- return self._map_method_expr(m, func_name, args, self._map_spec_for_name(namespace))
1412
+ namespace_spec = (
1413
+ self._collection_spec_for_name(namespace)
1414
+ if namespace is not None
1415
+ else None
1416
+ )
1417
+ if (
1418
+ namespace == "map"
1419
+ and not self._map_identifier_is_visible_binding(node)
1420
+ ):
1421
+ # A later global named ``map`` is already present in the flat
1422
+ # collection registry, but is not yet a lexical receiver here.
1423
+ namespace_spec = None
1424
+ if (
1425
+ namespace_spec is not None
1426
+ and namespace_spec.kind == "map"
1427
+ and func_name in MAP_METHODS
1428
+ ):
1429
+ m = self._collection_receiver_expr(namespace)
1430
+ arg_nodes = self._map_call_arg_nodes(
1431
+ func_name,
1432
+ node,
1433
+ functional=False,
1434
+ allow_keywords=False,
1435
+ )
1436
+ return self._map_param_method_expr(
1437
+ m, func_name, arg_nodes, namespace_spec
1438
+ )
504
1439
 
505
1440
  # map.method(m, args...) — functional form
506
1441
  if namespace == "map":
1442
+ if func_name == "new" or func_name in MAP_METHODS:
1443
+ # This dispatch point comes after lexical receiver routing, so
1444
+ # a parameter/local/global named ``map`` retains precedence
1445
+ # over the built-in namespace.
1446
+ self._map_call_arg_nodes(
1447
+ func_name,
1448
+ node,
1449
+ functional=True,
1450
+ allow_keywords=False,
1451
+ )
507
1452
  if func_name == "new":
508
1453
  spec = self._type_spec_from_expr(node) or TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
509
- return f"{self._type_spec_to_cpp(spec)}()"
1454
+ return f"{self._type_spec_to_cpp(spec)}::new_()"
510
1455
  if func_name in MAP_METHODS and node.args:
511
1456
  m = self._visit_expr(node.args[0])
512
- rest = [self._visit_expr(a) for a in node.args[1:]]
513
1457
  spec = self._type_spec_from_expr(node.args[0]) if node.args else None
514
- return self._map_method_expr(m, func_name, rest, spec)
1458
+ return self._map_param_method_expr(
1459
+ m, func_name, list(node.args[1:]), spec
1460
+ )
515
1461
  return "0"
516
1462
 
517
- if namespace is not None and namespace in self._matrix_specs and func_name in MATRIX_METHODS:
518
- arr = self._safe_name(namespace)
519
- self._check_matrix_method_allowed(func_name, self._matrix_specs[namespace], node)
1463
+ if (
1464
+ namespace_spec is not None
1465
+ and namespace_spec.kind == "matrix"
1466
+ and func_name in MATRIX_METHODS
1467
+ ):
1468
+ arr = self._collection_receiver_expr(namespace)
1469
+ self._check_matrix_method_allowed(func_name, namespace_spec, node)
520
1470
  param_names = MATRIX_METHOD_KWARGS.get(func_name)
521
1471
  if param_names and node.kwargs:
522
1472
  args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
@@ -533,10 +1483,15 @@ class CallVisitor:
533
1483
  )
534
1484
 
535
1485
  # Array method syntax: arr.push(val) where namespace is the array variable name
536
- if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
537
- arr = self._safe_name(namespace)
538
- spec = self._array_spec_for_name(namespace)
539
- args = self._array_method_args(func_name, node.args, spec)
1486
+ if (
1487
+ namespace_spec is not None
1488
+ and namespace_spec.kind == "array"
1489
+ and func_name in ARRAY_METHODS
1490
+ ):
1491
+ arr = self._collection_receiver_expr(namespace)
1492
+ spec = namespace_spec
1493
+ arg_nodes = self._array_method_arg_nodes(func_name, node)
1494
+ args = self._array_method_args(func_name, arg_nodes, spec)
540
1495
  return self._array_method_expr(arr, func_name, args, spec)
541
1496
 
542
1497
  # Array operations — emit proper C++ vector operations
@@ -559,10 +1514,15 @@ class CallVisitor:
559
1514
  elems = ", ".join(self._visit_expr(a) for a in node.args)
560
1515
  return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
561
1516
  # Method calls: array.method(arr, args...)
562
- if func_name in ARRAY_METHODS and node.args:
563
- arr = self._visit_expr(node.args[0])
564
- spec = self._type_spec_from_expr(node.args[0])
565
- rest = self._array_method_args(func_name, node.args[1:], spec)
1517
+ if func_name in ARRAY_METHODS and (
1518
+ node.args or node.kwargs or func_name == "copy"
1519
+ ):
1520
+ all_nodes = self._array_function_arg_nodes(func_name, node)
1521
+ if not all_nodes:
1522
+ return "0"
1523
+ arr = self._visit_expr(all_nodes[0])
1524
+ spec = self._type_spec_from_expr(all_nodes[0])
1525
+ rest = self._array_method_args(func_name, all_nodes[1:], spec)
566
1526
  return self._array_method_expr(arr, func_name, rest, spec)
567
1527
  return "0"
568
1528
 
@@ -1017,14 +1977,16 @@ class CallVisitor:
1017
1977
  if not isinstance(node.args[0], _Ident):
1018
1978
  self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
1019
1979
  recv_name = node.args[0].name
1020
- if recv_name not in self._matrix_specs:
1980
+ recv_spec = self._collection_spec_for_name(recv_name)
1981
+ if recv_spec is None or recv_spec.kind != "matrix":
1021
1982
  self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
1022
- self._check_matrix_method_allowed(func_name, self._matrix_specs[recv_name], node)
1983
+ self._check_matrix_method_allowed(func_name, recv_spec, node)
1023
1984
  if func_name == "sort":
1024
1985
  if isinstance(node.args[0], _Ident):
1025
1986
  recv_name = node.args[0].name
1026
- if recv_name in self._matrix_specs:
1027
- self._check_matrix_method_allowed(func_name, self._matrix_specs[recv_name], node)
1987
+ recv_spec = self._collection_spec_for_name(recv_name)
1988
+ if recv_spec is not None and recv_spec.kind == "matrix":
1989
+ self._check_matrix_method_allowed(func_name, recv_spec, node)
1028
1990
  obj = self._visit_expr(node.args[0])
1029
1991
  param_names = MATRIX_METHOD_KWARGS.get(func_name)
1030
1992
  if param_names:
@@ -1071,21 +2033,21 @@ class CallVisitor:
1071
2033
  if namespace in self._udt_defs and func_name == "new":
1072
2034
  fields = self._udt_defs[namespace]
1073
2035
  field_names = [f.name for f in fields]
1074
- init_vals = {}
2036
+ init_nodes = {}
1075
2037
  for i, a in enumerate(node.args):
1076
2038
  if i < len(field_names):
1077
- init_vals[field_names[i]] = self._visit_expr(a)
2039
+ init_nodes[field_names[i]] = a
1078
2040
  for k, v in node.kwargs.items():
1079
- init_vals[k] = self._visit_expr(v)
2041
+ init_nodes[k] = v
1080
2042
  field_inits = []
1081
2043
  field_specs = self._udt_field_type_specs.get(namespace, {})
1082
2044
  for f in fields:
1083
- val = None
1084
- if f.name in init_vals:
1085
- val = init_vals[f.name]
2045
+ value_node = None
2046
+ if f.name in init_nodes:
2047
+ value_node = init_nodes[f.name]
1086
2048
  elif f.default:
1087
- val = self._visit_expr(f.default)
1088
- if val is not None:
2049
+ value_node = f.default
2050
+ if value_node is not None:
1089
2051
  # Fix narrowing: brace-init (``T{.field = v}``) disallows
1090
2052
  # narrowing. Pine ``int`` UDT fields are emitted as
1091
2053
  # ``int64_t`` (see base.py) but are initialised from
@@ -1094,24 +2056,40 @@ class CallVisitor:
1094
2056
  f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
1095
2057
  if f_cpp_type == "int":
1096
2058
  f_cpp_type = "int64_t"
2059
+ val = self._visit_rhs_value(
2060
+ value_node,
2061
+ target_cpp_type=(
2062
+ f_cpp_type
2063
+ if (self._is_nullable_collection_cpp_type(f_cpp_type)
2064
+ or f_cpp_type in self._udt_defs
2065
+ or f_cpp_type in DRAWING_TYPE_TO_CPP.values())
2066
+ else None
2067
+ ),
2068
+ )
1097
2069
  if f_cpp_type == "int64_t":
1098
2070
  if "na<double>" in val:
1099
2071
  val = val.replace("na<double>()", "na<int64_t>()")
1100
2072
  else:
1101
2073
  val = f"(int64_t)({val})"
2074
+ elif (self._is_nullable_collection_cpp_type(f_cpp_type)
2075
+ and val == "na<double>()"):
2076
+ val = f"{f_cpp_type}{{}}"
1102
2077
  field_inits.append(f".{f.name} = {val}")
1103
- # Mark the constructed object non-na (the struct's ``__pf_na`` is the
1104
- # last declared field, so this designator stays in declaration order).
1105
- # A bare default-constructed UDT keeps ``__pf_na = true`` (na); only a
1106
- # real ``.new(...)`` flips it false so ``na(obj)`` reports correctly.
1107
- field_inits.append(".__pf_na = false")
1108
- return f"{namespace}{{{', '.join(field_inits)}}}"
2078
+ return self._emit_udt_new_expr(namespace, field_inits)
1109
2079
 
1110
2080
  # UDT copy: TypeName.copy(obj)
1111
2081
  if namespace in self._udt_defs and func_name == "copy":
1112
- if node.args:
1113
- return self._visit_expr(node.args[0])
1114
- return f"{namespace}{{}}"
2082
+ if len(node.args) != 1 or node.kwargs:
2083
+ self._codegen_error(
2084
+ node,
2085
+ f"{namespace}.copy(...) expects exactly one object argument",
2086
+ hint="Prefer the canonical object.copy() form.",
2087
+ )
2088
+ self._reject_unsupported_udt_copy(namespace, node)
2089
+ return self._emit_udt_copy_expr(
2090
+ namespace,
2091
+ self._visit_expr(node.args[0]),
2092
+ )
1115
2093
 
1116
2094
  # Safety net before the generic emitter. Every builtin namespace and
1117
2095
  # bare builtin that codegen knows how to emit has been dispatched (and
@@ -1162,33 +2140,96 @@ class CallVisitor:
1162
2140
  def _visit_arg_for_series(arg_node, arg_idx):
1163
2141
  """Visit a function argument, returning Series ref for series params."""
1164
2142
  if arg_idx in _func_series_param_indices:
1165
- if isinstance(arg_node, Identifier):
2143
+ expected_cpp_type = self._series_param_element_cpp_type(
2144
+ fi_lookup,
2145
+ arg_idx,
2146
+ self._callable_target_callsite_idx(fi_lookup, node),
2147
+ )
2148
+ chart_execution_context = (
2149
+ id(node) not in self._requested_context_inline_node_ids
2150
+ and (
2151
+ getattr(self, "_active_func_name", None),
2152
+ self._current_instance_name,
2153
+ ) not in self._requested_context_only_inline_contexts
2154
+ )
2155
+ if not chart_execution_context and isinstance(arg_node, Identifier):
2156
+ # request.security/request.security_lower_tf own a separate
2157
+ # evaluator clock and inline their UDF expressions in
2158
+ # security.py. Preserve the pre-factor direct-Series
2159
+ # lowering in the otherwise-emitted helper bodies.
1166
2160
  aname = arg_node.name
1167
- # Bar field: pass _s_close instead of current_bar_.close
1168
- if aname in BAR_FIELDS or aname in BAR_SERIES_PUSH:
2161
+ if (
2162
+ (aname in BAR_FIELDS or aname in BAR_SERIES_PUSH)
2163
+ and expected_cpp_type == "double"
2164
+ ):
1169
2165
  return f"_s_{aname}"
1170
- # Series var: pass the Series object directly
1171
- if aname in self.ctx.series_vars:
1172
- safe = self._safe_name(aname)
1173
- if self._active_var_remap and safe in self._active_var_remap:
1174
- safe = self._active_var_remap[safe]
2166
+ safe = self._safe_name(aname)
2167
+ is_current_series_param = (
2168
+ aname in self._current_func_series_params
2169
+ )
2170
+ if (
2171
+ is_current_series_param
2172
+ and self._current_func_series_param_types.get(aname)
2173
+ == expected_cpp_type
2174
+ ):
2175
+ return safe
2176
+ if self._active_var_remap and safe in self._active_var_remap:
2177
+ safe = self._active_var_remap[safe]
2178
+ if (
2179
+ not is_current_series_param
2180
+ and self._binding_is_series(aname, safe)
2181
+ and self._series_type_for(aname)
2182
+ == expected_cpp_type
2183
+ ):
1175
2184
  return safe
1176
2185
  expr_cpp = self._visit_expr(arg_node)
1177
- cpp_t = self._infer_type(arg_node)
1178
- if cpp_t not in ("double", "int", "bool"):
1179
- cpp_t = "double"
2186
+ cpp_t = expected_cpp_type
2187
+ bridge_cpp = self._series_bridge_value_expr(
2188
+ expr_cpp, cpp_t
2189
+ )
1180
2190
  member = self._inline_history_member(
1181
- "series_arg", node, arg_idx=arg_idx
2191
+ (
2192
+ "udf_series_arg"
2193
+ if chart_execution_context
2194
+ else "series_arg"
2195
+ ),
2196
+ node,
2197
+ arg_idx=arg_idx,
1182
2198
  )
2199
+ if not chart_execution_context:
2200
+ return (
2201
+ f"([&]() -> const Series<{cpp_t}>& {{ "
2202
+ f"{cpp_t} _sv = {bridge_cpp}; "
2203
+ f"if (history_advances_new_bar()) {member}.push(_sv); "
2204
+ f"else {member}.update(_sv); "
2205
+ f"return {member}; }}())"
2206
+ )
2207
+ # A chart-executed plain UDF's history-reading parameter
2208
+ # belongs to this lexical call site, not to the caller Series
2209
+ # object. The on_bar preamble has already advanced its
2210
+ # synthetic buffer by one chart slot, so execution replaces
2211
+ # that slot rather than pushing a second one.
1183
2212
  return (
1184
2213
  f"([&]() -> const Series<{cpp_t}>& {{ "
1185
- f"{cpp_t} _sv = ({expr_cpp}); "
1186
- f"if (history_advances_new_bar()) {member}.push(_sv); "
1187
- f"else {member}.update(_sv); "
2214
+ f"{cpp_t} _sv = {bridge_cpp}; "
2215
+ f"{member}.update(_sv); "
1188
2216
  f"return {member}; }}())"
1189
2217
  )
2218
+ # A concrete nullable collection specialization learned through an
2219
+ # untyped wrapper chain also target-types its call arguments.
2220
+ if fi_lookup is not None:
2221
+ param_specs = getattr(fi_lookup, "param_type_specs", []) or []
2222
+ if arg_idx < len(param_specs):
2223
+ param_spec = param_specs[arg_idx]
2224
+ if (param_spec is not None
2225
+ and param_spec.kind in {"map", "matrix"}):
2226
+ return self._visit_rhs_value(
2227
+ arg_node,
2228
+ target_cpp_type=self._type_spec_to_cpp(param_spec),
2229
+ )
1190
2230
  return self._visit_expr(arg_node)
1191
2231
 
2232
+ ordered_arg_nodes: list = []
1192
2233
  if node.kwargs:
1193
2234
  # Try to resolve kwargs using FuncInfo params for user-defined functions
1194
2235
  fi = self._func_info_map.get(func_name)
@@ -1196,16 +2237,28 @@ class CallVisitor:
1196
2237
  param_names = list(fi.node.params) # params is list[str]
1197
2238
  # Merge kwargs then visit with series awareness
1198
2239
  merged = _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
2240
+ ordered_arg_nodes = list(merged)
1199
2241
  all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(merged)]
1200
2242
  elif sigs.is_intrinsic_function(namespace, func_name):
1201
2243
  # Known intrinsic — use signature registry for kwargs resolution
1202
2244
  param_names = sigs.get_param_names(namespace, func_name)
1203
- all_args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
2245
+ merged = _merge_kwargs(
2246
+ node.args, node.kwargs, param_names, lambda a: a
2247
+ )
2248
+ ordered_arg_nodes = list(merged)
2249
+ all_args = [self._visit_expr(a) for a in merged]
1204
2250
  else:
1205
2251
  # Unknown function: positional args + kwargs values as fallback
1206
- all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
1207
- all_args.extend(self._visit_expr(v) for v in node.kwargs.values())
2252
+ ordered_arg_nodes = [*node.args, *node.kwargs.values()]
2253
+ all_args = [
2254
+ _visit_arg_for_series(a, i)
2255
+ for i, a in enumerate(node.args)
2256
+ ]
2257
+ all_args.extend(
2258
+ self._visit_expr(v) for v in node.kwargs.values()
2259
+ )
1208
2260
  else:
2261
+ ordered_arg_nodes = list(node.args)
1209
2262
  all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
1210
2263
  # Drawing-style/visual CONSTANT passed positionally into a user function's
1211
2264
  # ``string`` parameter: ``label.style_*`` / ``size.*`` / other
@@ -1224,6 +2277,7 @@ class CallVisitor:
1224
2277
  if fi and fi.node and fi.name == "isInSession" and len(fi.node.params) >= 2 and len(all_args) == 1:
1225
2278
  # Mirror Pine default `timeframe.period` instead of hard-coding 15m.
1226
2279
  all_args.append("script_tf_")
2280
+ ordered_arg_nodes.append(None)
1227
2281
  prefix = f"{namespace}::" if namespace else ""
1228
2282
  # Use safe name for user-defined functions to avoid member name collision
1229
2283
  emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
@@ -1253,7 +2307,15 @@ class CallVisitor:
1253
2307
  # Inside a per-call-site variant: propagate call-site index to
1254
2308
  # sub-functions that also have variants (for state isolation)
1255
2309
  emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
1256
- return f"{prefix}{emit_name}({', '.join(all_args)})"
2310
+ call_head = f"{prefix}{emit_name}"
2311
+ if namespace is None and func_name in self._func_names:
2312
+ return self._ordered_user_call_expr(
2313
+ call_head,
2314
+ ordered_arg_nodes,
2315
+ all_args,
2316
+ source_order_nodes=[*node.args, *node.kwargs.values()],
2317
+ )
2318
+ return f"{call_head}({', '.join(all_args)})"
1257
2319
 
1258
2320
  def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
1259
2321
  """In-place coerce positional args bound to a ``std::string`` user-function
@@ -1314,6 +2376,24 @@ class CallVisitor:
1314
2376
  x = self._visit_expr(node.args[0])
1315
2377
  return f"(is_na({x}) ? {member} : ({member} = {x}))"
1316
2378
 
2379
+ @staticmethod
2380
+ def _strategy_close_callsite_token(node: FuncCall) -> str:
2381
+ """Return the stable runtime token for an authored close statement.
2382
+
2383
+ A generated strategy is compiled from one Pine source unit, so the
2384
+ parser's 1-based line and column identify the syntactic callsite. The
2385
+ same inner UDF/loop node retains that location across every emitted or
2386
+ runtime execution. Zero is reserved for source-less synthetic calls
2387
+ so existing compatibility behavior remains available.
2388
+ """
2389
+ loc = node.loc
2390
+ if loc is None or loc.line <= 0 or loc.col <= 0:
2391
+ return "0ULL"
2392
+ token = ((int(loc.line) & 0xFFFFFFFF) << 32) | (
2393
+ int(loc.col) & 0xFFFFFFFF
2394
+ )
2395
+ return f"{token}ULL"
2396
+
1317
2397
  def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
1318
2398
  if func_name in ("convert_to_account", "convert_to_symbol"):
1319
2399
  p = self._resolve_func_args(node, f"strategy.{func_name}")
@@ -1359,10 +2439,17 @@ class CallVisitor:
1359
2439
  qty = self._visit_expr(p.get("qty")) if p.get("qty") is not None else "na<double>()"
1360
2440
  qty_pct = self._visit_expr(p.get("qty_percent")) if p.get("qty_percent") is not None else "na<double>()"
1361
2441
  immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
1362
- return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately})"
2442
+ callsite = self._strategy_close_callsite_token(node)
2443
+ return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately}, {callsite})"
1363
2444
 
1364
2445
  if func_name == "close_all":
1365
- return "strategy_close_all()"
2446
+ p = self._resolve_func_args(node, "strategy.close_all")
2447
+ comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
2448
+ immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
2449
+ # The engine's ID-less strategy_close path closes the complete
2450
+ # position. Reuse it so close_all preserves the Pine order comment
2451
+ # and same-tick fill flag instead of silently discarding both.
2452
+ return f'strategy_close("", {comment}, na<double>(), na<double>(), {immediately})'
1366
2453
 
1367
2454
  if func_name == "exit":
1368
2455
  p = self._resolve_func_args(node, "strategy.exit")