@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.
@@ -59,7 +59,6 @@ State written by ``_emit_func_def`` (the per-function emit context):
59
59
  - ``self._current_func_locals`` (``set[str]``).
60
60
  - ``self._active_ta_remap`` (``dict[str, str]``).
61
61
  - ``self._active_var_remap`` (``dict[str, str]``).
62
- - ``self._in_ta_func_variant`` (``bool``).
63
62
  - ``self._active_call_site_idx`` (``int | None``).
64
63
 
65
64
  Sibling-mixin methods consumed via ``self``:
@@ -87,7 +86,7 @@ from ..ast_nodes import (
87
86
  ExprStmt, FuncCall, Identifier, IfStmt, SwitchStmt, VarDecl,
88
87
  )
89
88
  from ..analyzer import FuncInfo
90
- from ..symbols import TypeSpec
89
+ from ..symbols import PineType, method_receiver_cpp_token
91
90
  from .tables import (
92
91
  BAR_SERIES_PUSH,
93
92
  DRAWING_TYPE_TO_CPP,
@@ -110,6 +109,8 @@ class TopLevelEmitter:
110
109
  lines.append('#include <pineforge/math.hpp>')
111
110
  lines.append('#include <pineforge/series.hpp>')
112
111
  lines.append('#include <pineforge/na.hpp>')
112
+ if getattr(self, "_uses_map", False):
113
+ lines.append('#include <pineforge/map.hpp>')
113
114
  lines.append("#include <cstdint>")
114
115
  lines.append("#include <cmath>")
115
116
  lines.append("#include <algorithm>")
@@ -117,9 +118,16 @@ class TopLevelEmitter:
117
118
  lines.append("#include <numeric>")
118
119
  lines.append("#include <string>")
119
120
  lines.append("#include <vector>")
121
+ if getattr(self, "_udt_defs", {}):
122
+ lines.append("#include <deque>")
123
+ lines.append("#include <functional>")
124
+ lines.append("#include <limits>")
120
125
  lines.append("#include <tuple>")
121
126
  lines.append("#include <optional>")
122
127
  lines.append("#include <type_traits>")
128
+ if getattr(self, "_udt_defs", {}):
129
+ lines.append("#include <stdexcept>")
130
+ lines.append("#include <utility>")
123
131
  lines.append("#include <memory>")
124
132
  lines.append("#include <mutex>")
125
133
  lines.append("#include <unordered_map>")
@@ -136,8 +144,7 @@ class TopLevelEmitter:
136
144
  # is present in the TU. Float matrices route through PineMatrix in
137
145
  # matrix.hpp; pulling in the generic header otherwise is a wasted
138
146
  # include.
139
- float_spec = TypeSpec.primitive("float")
140
- if any(spec.element != float_spec for spec in self._matrix_specs.values()):
147
+ if self._detect_generic_matrix_usage():
141
148
  lines.append('#include <pineforge/generic_matrix.hpp>')
142
149
  # Drawing-objects-as-data runtime (line/box/label/linefill arenas +
143
150
  # ChartPoint). Gated on _uses_drawing so non-drawing strategies stay
@@ -277,26 +284,272 @@ class TopLevelEmitter:
277
284
  members.append(name)
278
285
  return members
279
286
 
287
+ def _emit_handle_checkpoint_traits(self, lines: list[str]) -> None:
288
+ """Emit recursive rollback adapters for shared-ID collection state.
289
+
290
+ This support block is emitted for scripts using a shared-ID map/matrix
291
+ or an authored UDT arena. The primary trait retains the historical
292
+ value-copy checkpoint for ordinary runtime state; specialized map,
293
+ matrix, vector, UDT-record, and arena traits snapshot their owned state
294
+ without recursively following nested UDT handle graphs.
295
+ """
296
+ checkpoint_traits = self._checkpoint_traits_cpp_type()
297
+ lines.extend([
298
+ "template <typename _PFValue>",
299
+ f"struct {checkpoint_traits} {{",
300
+ " using snapshot_type = _PFValue;",
301
+ " static snapshot_type take(const _PFValue& value) { return value; }",
302
+ " static void restore(_PFValue& value, const snapshot_type& snapshot) {",
303
+ " value = snapshot;",
304
+ " }",
305
+ "};",
306
+ "",
307
+ ])
308
+
309
+ if getattr(self, "_udt_defs", {}):
310
+ coordinator_type = self._udt_undo_coordinator_cpp_name
311
+ lines.extend([
312
+ "template <>",
313
+ f"struct {checkpoint_traits}<{coordinator_type}> {{",
314
+ f" using coordinator_type = {coordinator_type};",
315
+ " using snapshot_type = typename coordinator_type::Snapshot;",
316
+ " static snapshot_type take(coordinator_type& value) {",
317
+ " return value.snapshot();",
318
+ " }",
319
+ " static void restore(coordinator_type& value,",
320
+ " const snapshot_type& snapshot) {",
321
+ " value.restore(snapshot);",
322
+ " }",
323
+ "};",
324
+ "",
325
+ ])
326
+
327
+ if getattr(self, "_uses_map", False):
328
+ lines.extend([
329
+ "template <typename _PFKey, typename _PFValue>",
330
+ f"struct {checkpoint_traits}<PineMap<_PFKey, _PFValue>> {{",
331
+ " using map_type = PineMap<_PFKey, _PFValue>;",
332
+ " static_assert(map_type::snapshot_supported,",
333
+ ' "generated map checkpoints require primitive map values");',
334
+ " using snapshot_type = std::optional<typename map_type::Snapshot>;",
335
+ " static snapshot_type take(const map_type& value) {",
336
+ " if (value.is_na()) return std::nullopt;",
337
+ " return value.snapshot();",
338
+ " }",
339
+ " static void restore(map_type& value, const snapshot_type& snapshot) {",
340
+ " if (!snapshot) {",
341
+ " value = map_type{};",
342
+ " return;",
343
+ " }",
344
+ " value.restore(*snapshot);",
345
+ " }",
346
+ "};",
347
+ "",
348
+ ])
349
+
350
+ if getattr(self, "_uses_matrix", False):
351
+ lines.extend([
352
+ "template <>",
353
+ f"struct {checkpoint_traits}<PineMatrix> {{",
354
+ " using matrix_type = PineMatrix;",
355
+ " using snapshot_type = std::optional<typename matrix_type::Snapshot>;",
356
+ " static snapshot_type take(const matrix_type& value) {",
357
+ " if (value.is_na()) return std::nullopt;",
358
+ " return value.snapshot();",
359
+ " }",
360
+ " static void restore(matrix_type& value, const snapshot_type& snapshot) {",
361
+ " if (!snapshot) {",
362
+ " value = matrix_type{};",
363
+ " return;",
364
+ " }",
365
+ " value.restore(*snapshot);",
366
+ " }",
367
+ "};",
368
+ "",
369
+ ])
370
+
371
+ if self._detect_generic_matrix_usage():
372
+ lines.extend([
373
+ "template <typename _PFElement>",
374
+ f"struct {checkpoint_traits}<PineGenericMatrix<_PFElement>> {{",
375
+ " using matrix_type = PineGenericMatrix<_PFElement>;",
376
+ f" using element_traits = {checkpoint_traits}<_PFElement>;",
377
+ " using element_snapshot = typename element_traits::snapshot_type;",
378
+ " struct active_snapshot {",
379
+ " typename matrix_type::Snapshot outer;",
380
+ " int rows;",
381
+ " int columns;",
382
+ " std::vector<element_snapshot> elements;",
383
+ " };",
384
+ " using snapshot_type = std::optional<active_snapshot>;",
385
+ " static snapshot_type take(const matrix_type& value) {",
386
+ " if (value.is_na()) return std::nullopt;",
387
+ " active_snapshot snapshot{value.snapshot(), value.rows(),",
388
+ " value.columns(), {}};",
389
+ " snapshot.elements.reserve(",
390
+ " static_cast<std::size_t>(snapshot.rows) *",
391
+ " static_cast<std::size_t>(snapshot.columns));",
392
+ " for (int row = 0; row < snapshot.rows; ++row) {",
393
+ " for (int column = 0; column < snapshot.columns; ++column) {",
394
+ " snapshot.elements.push_back(",
395
+ " element_traits::take(value.get(row, column)));",
396
+ " }",
397
+ " }",
398
+ " return snapshot;",
399
+ " }",
400
+ " static void restore(matrix_type& value, const snapshot_type& snapshot) {",
401
+ " if (!snapshot) {",
402
+ " value = matrix_type{};",
403
+ " return;",
404
+ " }",
405
+ " value.restore(snapshot->outer);",
406
+ " std::size_t index = 0;",
407
+ " for (int row = 0; row < snapshot->rows; ++row) {",
408
+ " for (int column = 0; column < snapshot->columns; ++column) {",
409
+ " _PFElement element = value.get(row, column);",
410
+ " element_traits::restore(element, snapshot->elements[index++]);",
411
+ " value.set(row, column, element);",
412
+ " }",
413
+ " }",
414
+ " }",
415
+ "};",
416
+ "",
417
+ ])
418
+
419
+ lines.extend([
420
+ "template <typename _PFElement, typename _PFAllocator>",
421
+ f"struct {checkpoint_traits}<std::vector<_PFElement, _PFAllocator>> {{",
422
+ f" using element_traits = {checkpoint_traits}<_PFElement>;",
423
+ " using element_snapshot = typename element_traits::snapshot_type;",
424
+ " using snapshot_type = std::vector<element_snapshot>;",
425
+ " static snapshot_type take(",
426
+ " const std::vector<_PFElement, _PFAllocator>& value) {",
427
+ " snapshot_type snapshot;",
428
+ " snapshot.reserve(value.size());",
429
+ " for (std::size_t index = 0; index < value.size(); ++index) {",
430
+ " const _PFElement element = value[index];",
431
+ " snapshot.push_back(element_traits::take(element));",
432
+ " }",
433
+ " return snapshot;",
434
+ " }",
435
+ " static void restore(",
436
+ " std::vector<_PFElement, _PFAllocator>& value,",
437
+ " const snapshot_type& snapshot) {",
438
+ " value.clear();",
439
+ " value.reserve(snapshot.size());",
440
+ " for (const auto& element_snapshot_value : snapshot) {",
441
+ " _PFElement element{};",
442
+ " element_traits::restore(element, element_snapshot_value);",
443
+ " value.push_back(element);",
444
+ " }",
445
+ " }",
446
+ "};",
447
+ "",
448
+ ])
449
+
450
+ # Snapshot backing records field-by-field. A nested UDT field is only
451
+ # a numeric handle, so the primary trait copies its ID without recursing
452
+ # into another record. This makes self/cyclic object graphs finite;
453
+ # each arena snapshots every owned record exactly once. Shared-ID map
454
+ # and matrix fields still enter their specialized recursive traits.
455
+ for type_name, fields in self._udt_defs.items():
456
+ record_type = self._udt_record_cpp_type(type_name)
457
+ emitted_fields = [
458
+ field
459
+ for field in fields
460
+ if field.name not in self._udt_omitted_fields.get(type_name, set())
461
+ ]
462
+ checkpoint_fields = [field.name for field in emitted_fields]
463
+ lines.append("template <>")
464
+ lines.append(f"struct {checkpoint_traits}<{record_type}> {{")
465
+ lines.append(" struct snapshot_type {")
466
+ for index, field_name in enumerate(checkpoint_fields):
467
+ lines.append(
468
+ f" {checkpoint_traits}<"
469
+ f"decltype({record_type}::{field_name})>::snapshot_type "
470
+ f"_pf_field_{index};"
471
+ )
472
+ lines.append(" };")
473
+ lines.append(
474
+ f" static snapshot_type take(const {record_type}& value) {{"
475
+ )
476
+ lines.append(" return snapshot_type{")
477
+ for field_name in checkpoint_fields:
478
+ lines.append(
479
+ f" {checkpoint_traits}<"
480
+ f"decltype({record_type}::{field_name})>::take(value.{field_name}),"
481
+ )
482
+ lines.append(" };")
483
+ lines.append(" }")
484
+ lines.append(
485
+ f" static void restore({record_type}& value, "
486
+ "const snapshot_type& snapshot) {"
487
+ )
488
+ for index, field_name in enumerate(checkpoint_fields):
489
+ lines.append(
490
+ f" {checkpoint_traits}<"
491
+ f"decltype({record_type}::{field_name})>::restore("
492
+ f"value.{field_name}, snapshot._pf_field_{index});"
493
+ )
494
+ lines.append(" }")
495
+ lines.append("};")
496
+ lines.append("")
497
+
498
+ arena_type = self._udt_arena_cpp_type(type_name)
499
+ lines.append("template <>")
500
+ lines.append(f"struct {checkpoint_traits}<{arena_type}> {{")
501
+ lines.append(
502
+ f" using arena_type = {arena_type};"
503
+ )
504
+ lines.append(
505
+ " using snapshot_type = typename arena_type::Snapshot;"
506
+ )
507
+ lines.append(" static snapshot_type take(arena_type& value) {")
508
+ lines.append(" return value.snapshot();")
509
+ lines.append(" }")
510
+ lines.append(
511
+ " static void restore(arena_type& value, const snapshot_type& snapshot) {"
512
+ )
513
+ lines.append(" value.restore(snapshot);")
514
+ lines.append(" }")
515
+ lines.append("};")
516
+ lines.append("")
517
+
280
518
  def _emit_script_state_hooks(self, lines: list[str], members: list[str]) -> None:
281
519
  """Emit the engine's Pine rollback checkpoint hook implementation.
282
520
 
283
- The checkpoint owns value copies of all generated mutable state. Every
284
- runtime container used by generated code (Series, std::vector/map,
285
- PineMatrix/generic matrices, UDTs and drawing arenas) has value
286
- semantics, so copying recursively preserves data without retaining
287
- pointers into live state. Drawing handles themselves are stable ids;
288
- their arenas are captured in the same checkpoint.
521
+ The checkpoint owns detached snapshots of all generated mutable state.
522
+ Shared-ID maps and matrices retain their original backing identity so
523
+ rollback updates every alias. Generated UDT arenas snapshot each owned
524
+ record once; nested UDT/map/matrix fields therefore restore through
525
+ their numeric/shared IDs without recursively walking object graphs.
526
+ Vectors snapshot their elements, including UDT handle values. Drawing
527
+ handles themselves are stable ids and their arenas are captured in the
528
+ same checkpoint.
289
529
 
290
530
  The static assertions deliberately turn any future non-copyable member
291
531
  into a compile failure instead of a nominal, shallow rollback. Engine
292
532
  broker/order state lives in the base class and is intentionally absent:
293
533
  fills must survive while Pine script variables roll back.
294
534
  """
535
+ checkpoint_traits = self._checkpoint_traits_cpp_type()
536
+ handle_aware = bool(
537
+ getattr(self, "_uses_map", False)
538
+ or getattr(self, "_uses_matrix", False)
539
+ or getattr(self, "_udt_defs", {})
540
+ )
295
541
  lines.append(" struct _PFScriptState {")
296
542
  for idx, name in enumerate(members):
297
- lines.append(
298
- f" decltype(GeneratedStrategy::{name}) _pf_value_{idx};"
299
- )
543
+ if handle_aware:
544
+ lines.append(
545
+ f" {checkpoint_traits}<"
546
+ f"decltype(GeneratedStrategy::{name})>::snapshot_type "
547
+ f"_pf_value_{idx};"
548
+ )
549
+ else:
550
+ lines.append(
551
+ f" decltype(GeneratedStrategy::{name}) _pf_value_{idx};"
552
+ )
300
553
  lines.append(" };")
301
554
  lines.append(
302
555
  " static_assert(std::is_copy_constructible_v<_PFScriptState>, "
@@ -311,16 +564,29 @@ class TopLevelEmitter:
311
564
  lines.append(" void snapshot_script_state() override {")
312
565
  lines.append(" _pf_script_state_checkpoint_.emplace(_PFScriptState{")
313
566
  for name in members:
314
- lines.append(f" {name},")
567
+ if handle_aware:
568
+ lines.append(
569
+ f" {checkpoint_traits}<"
570
+ f"decltype(GeneratedStrategy::{name})>::take({name}),"
571
+ )
572
+ else:
573
+ lines.append(f" {name},")
315
574
  lines.append(" });")
316
575
  lines.append(" }")
317
576
  lines.append("")
318
577
  lines.append(" void restore_script_state() override {")
319
578
  lines.append(" if (!_pf_script_state_checkpoint_) return;")
320
579
  for idx, name in enumerate(members):
321
- lines.append(
322
- f" this->{name} = _pf_script_state_checkpoint_->_pf_value_{idx};"
323
- )
580
+ if handle_aware:
581
+ lines.append(
582
+ f" {checkpoint_traits}<"
583
+ f"decltype(GeneratedStrategy::{name})>::restore("
584
+ f"this->{name}, _pf_script_state_checkpoint_->_pf_value_{idx});"
585
+ )
586
+ else:
587
+ lines.append(
588
+ f" this->{name} = _pf_script_state_checkpoint_->_pf_value_{idx};"
589
+ )
324
590
  lines.append(" }")
325
591
  lines.append("")
326
592
  lines.append(" void commit_script_state() override {")
@@ -361,9 +627,20 @@ class TopLevelEmitter:
361
627
  # incl. function-derived lengths) are safe: the `!_ta_initialized_`
362
628
  # reset overwrites the placeholder before the first compute.
363
629
  for a in site.ctor_args:
364
- r = self._resolve_known(a)
630
+ r = self._resolve_ta_ctor_arg(a)
365
631
  if (not self._is_compile_time_value(r)
366
632
  and self._runtime_ctor_arg_for_reset(a) is None):
633
+ # A TA source reached through request.security can have
634
+ # several helper-bound constructor variants. Validate
635
+ # only variants of this exact source node before the
636
+ # ordinary guard fires, so an unstable requested-context
637
+ # expression keeps its provenance-specific diagnostic
638
+ # without allowing a later, unrelated security error to
639
+ # reorder an earlier ordinary error.
640
+ if self._security_eval_info and site.node is not None:
641
+ self._collect_ta_runtime_resets(
642
+ security_source_node=site.node
643
+ )
367
644
  self._codegen_error(
368
645
  getattr(site, "node", None),
369
646
  f"Unsupported TA constructor length '{a}' for "
@@ -373,7 +650,7 @@ class TopLevelEmitter:
373
650
  hint=("Use a literal, an input.*() value, or "
374
651
  "arithmetic over those for TA lengths."),
375
652
  )
376
- resolved = [self._resolve_known(a) for a in site.ctor_args]
653
+ resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
377
654
  # Compile-time placeholder for the init list; the runtime reset
378
655
  # (when the arg is input-derived) overwrites it on the first bar.
379
656
  safe_resolved = []
@@ -389,11 +666,18 @@ class TopLevelEmitter:
389
666
  site = self.ctx.ta_call_sites[idx]
390
667
  if not site.ctor_args:
391
668
  continue
392
- resolved = [self._resolve_known(a) for a in site.ctor_args]
393
- safe_resolved = []
394
- for r in resolved:
395
- safe_resolved.append(r if self._is_compile_time_value(r) else "1")
396
669
  for variant in variants:
670
+ ctor_args, _ctor_arg_stability = self._security_ta_ctor_args_for_variant(
671
+ info["sec_id"],
672
+ site,
673
+ variant.get("binding_stack", ()),
674
+ )
675
+ resolved = [self._resolve_ta_ctor_arg(a) for a in ctor_args]
676
+ safe_resolved = []
677
+ for r in resolved:
678
+ safe_resolved.append(
679
+ r if self._is_compile_time_value(r) else "1"
680
+ )
397
681
  init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
398
682
 
399
683
  # Non-series var members with compile-time init (deduplicate by name)
@@ -403,27 +687,39 @@ class TopLevelEmitter:
403
687
  continue
404
688
  seen_ctor_vars.add(name)
405
689
  safe = self._safe_name(name)
406
- if name in self._array_vars or name in self._map_vars:
690
+ if (name in self._array_vars
691
+ or name in self._map_vars
692
+ or name in self._matrix_specs):
693
+ # Collection members are initialized in the first-bar block.
694
+ # A matrix declared as ``var matrix<T> x = na`` has no
695
+ # constructor call to replay there and its default constructor
696
+ # is already the correctly typed null ID. Letting the generic
697
+ # scalar path add ``x(na<double>())`` is ill-typed.
407
698
  continue
408
- # Function-scoped ``var`` members are initialized once-per-variant
409
- # on first call (see _emit_func_var_init_block); exclude them from
410
- # the constructor so they are not double-initialized and so their
411
- # (possibly bar-dependent) initializer is lowered in the function's
412
- # own scope (with its active var remap for clones).
699
+ # Callable-scoped ``var`` members initialize at their exact source
700
+ # declarations; exclude them from the constructor so source order,
701
+ # first reach, and the active clone remap remain authoritative.
413
702
  if name in getattr(self, "_func_local_var_names", ()):
414
703
  continue
704
+ # Runtime primitive initializers execute at their Pine declaration
705
+ # site under a per-member once flag. This preserves source-order
706
+ # dependencies and first-entry semantics for conditional blocks.
707
+ if name in self._runtime_scalar_var_init_members:
708
+ continue
415
709
  # UDT-typed var members (``var SDZone z = na``) default-construct to
416
710
  # na via the struct's in-class ``__pf_na = true``; a ctor init like
417
711
  # ``z(na<double>())`` would not type-match the struct member.
418
- if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
712
+ member_udt_type = self._member_udt_type(name)
713
+ if member_udt_type in self._udt_defs:
419
714
  continue
420
715
  # Drawing handle var member (L-N3): ``var line x`` / ``var box b``
421
716
  # default-construct to {-1} (na). A ``b(na<double>())`` ctor init
422
717
  # would not type-match the handle struct — skip it (the in-class
423
718
  # member default is the once-only persistent na init).
424
- if name in self._udt_var_types and self._udt_var_types[name] in DRAWING_TYPE_TO_CPP:
719
+ if (name in self._drawing_var_member_cpp_types
720
+ or member_udt_type in DRAWING_TYPE_TO_CPP):
425
721
  continue
426
- if name not in self.ctx.series_vars:
722
+ if safe not in self._series_var_member_names:
427
723
  cpp_val = self._resolve_known(init_expr)
428
724
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
429
725
  if self._is_compile_time_value(cpp_val):
@@ -608,8 +904,37 @@ class TopLevelEmitter:
608
904
  lines.append(f"{pad}else {member}.update({value});")
609
905
 
610
906
  def _emit_on_bar(self, lines: list[str]) -> None:
907
+ self._lexical_drawing_types = {}
908
+ self._lexical_udt_types = {}
909
+ self._lexical_series_bindings = {}
910
+ self._lexical_known_var_tombstones = set()
611
911
  lines.append(" void on_bar(const Bar& bar) override {")
612
912
 
913
+ # A GeneratedStrategy handle may execute multiple batch runs or
914
+ # streaming lifecycles. BacktestEngine resets broker/base state, but
915
+ # generated members survive. Reset source-shaped lazy ROC clocks and
916
+ # their forced eager-fallback close history at the first genuine slot
917
+ # of each lifecycle. This lives in on_bar rather than a generated run
918
+ # wrapper because stream_begin() enters the base run path directly.
919
+ # COOF post-close recalculations have history_advances_new_bar()==false
920
+ # and therefore preserve the current committed clock/base.
921
+ if self._lazy_saturated_roc3_clock_by_node:
922
+ lines.append(
923
+ " if (history_advances_new_bar() && bar_index_ == 0) {"
924
+ )
925
+ for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
926
+ lines.append(f" {clock_name}.reset();")
927
+ lines.append(
928
+ f" {self._lazy_saturated_roc3_history_name}.clear();"
929
+ )
930
+ lines.append(" }")
931
+ self._emit_history_series_write(
932
+ lines,
933
+ " ",
934
+ self._lazy_saturated_roc3_history_name,
935
+ "current_bar_.close",
936
+ )
937
+
613
938
  # reset_run_state() owns engine/broker state, while these generated
614
939
  # Series members belong to the strategy object. Clear all of them on
615
940
  # the first genuine history slot of bar zero, unconditionally: a site
@@ -659,11 +984,36 @@ class TopLevelEmitter:
659
984
  if self.ctx.var_members:
660
985
  lines.append(" if (!_var_initialized) {")
661
986
  for name, ptype, init_expr in self.ctx.var_members:
662
- # Function-scoped ``var`` members are init'd once-per-variant
663
- # on first call (see _emit_func_var_init_block), not here.
987
+ # Callable-scoped ``var`` members initialize at their exact
988
+ # declaration statements, not in the global on_bar preamble.
664
989
  if name in getattr(self, "_func_local_var_names", ()):
665
990
  continue
666
991
  safe = self._safe_name(name)
992
+ runtime_info = self._runtime_scalar_var_init_by_member.get(name)
993
+ if (runtime_info is not None
994
+ and runtime_info.get("drawing_cpp") is not None):
995
+ if runtime_info.get("is_series"):
996
+ self._emit_history_series_write(
997
+ lines,
998
+ " ",
999
+ safe,
1000
+ f"{runtime_info['drawing_cpp']}{{}}",
1001
+ )
1002
+ continue
1003
+ # Declaration-site initialization owns nullable collection
1004
+ # members carrying runtime metadata. A persistent map/matrix
1005
+ # if/switch initializer must not also enter the legacy
1006
+ # first-bar aggregate preamble (which cannot lower a block
1007
+ # expression and formerly emitted ``/* unknown */`` for maps).
1008
+ # Keep primitive and ternary paths on their established order.
1009
+ runtime_spec = (
1010
+ runtime_info.get("type_spec")
1011
+ if runtime_info is not None
1012
+ else None
1013
+ )
1014
+ if (runtime_spec is not None
1015
+ and runtime_spec.kind in {"map", "matrix"}):
1016
+ continue
667
1017
  if name in self._array_vars:
668
1018
  for stmt in self.ctx.ast.body:
669
1019
  if isinstance(stmt, VarDecl) and stmt.name == name:
@@ -672,39 +1022,55 @@ class TopLevelEmitter:
672
1022
  break
673
1023
  continue
674
1024
  if name in self._matrix_specs:
675
- # Matrix vars: initialize with matrix.new expression
1025
+ # Matrix vars are nullable ID handles. Initialize from the
1026
+ # authored RHS in declaration order, not only from a
1027
+ # matrix.new(...) call: ``var matrix<int> alias = source``
1028
+ # must copy source's ID once, while a later local rebind
1029
+ # remains independent.
676
1030
  for stmt in self.ctx.ast.body:
677
- if isinstance(stmt, VarDecl) and stmt.name == name and isinstance(stmt.value, FuncCall):
678
- cpp_val = self._visit_expr(stmt.value)
1031
+ if isinstance(stmt, VarDecl) and stmt.name == name:
1032
+ cpp_val = self._visit_rhs_value(
1033
+ stmt.value,
1034
+ name,
1035
+ target_cpp_type=self._type_spec_to_cpp(
1036
+ self._matrix_specs[name]
1037
+ ),
1038
+ )
679
1039
  lines.append(f" {safe} = {cpp_val};")
680
1040
  break
681
1041
  continue
682
1042
  if name in self._map_vars:
683
1043
  for stmt in self.ctx.ast.body:
684
1044
  if isinstance(stmt, VarDecl) and stmt.name == name:
685
- cpp_val = self._visit_expr(stmt.value)
1045
+ cpp_val = self._visit_rhs_value(
1046
+ stmt.value,
1047
+ name,
1048
+ target_cpp_type=self._type_spec_to_cpp(
1049
+ self._map_spec_for_name(name)
1050
+ ),
1051
+ )
686
1052
  lines.append(f" {safe} = {cpp_val};")
687
1053
  break
688
1054
  continue
689
- # UDT vars: init with constructor expression
690
- init_s = str(init_expr)
691
- is_udt_init = False
692
- for udt_name in self._udt_defs:
693
- if init_s.startswith(f"{udt_name}.new"):
694
- # Find the actual AST node to generate the init expression
695
- for stmt in self.ctx.ast.body:
696
- if isinstance(stmt, VarDecl) and stmt.name == name and isinstance(stmt.value, FuncCall):
697
- cpp_val = self._visit_expr(stmt.value)
698
- lines.append(f" {safe} = {cpp_val};")
699
- is_udt_init = True
700
- break
701
- if not is_udt_init:
702
- lines.append(f" {safe} = {udt_name}{{}};")
703
- is_udt_init = True
704
- break
705
- if is_udt_init:
1055
+ if name in self._runtime_scalar_var_init_members:
1056
+ continue
1057
+ # Persistent authored UDT variables initialize from their full
1058
+ # source RHS, not only from ``Type.new``. In particular,
1059
+ # ``var Item alias = original`` must copy the numeric handle
1060
+ # once and therefore alias the same arena record.
1061
+ udt_name = self._member_udt_type(name)
1062
+ if udt_name in self._udt_defs:
1063
+ for stmt in self.ctx.ast.body:
1064
+ if isinstance(stmt, VarDecl) and stmt.name == name:
1065
+ cpp_val = self._visit_rhs_value(
1066
+ stmt.value,
1067
+ name,
1068
+ target_cpp_type=udt_name,
1069
+ )
1070
+ lines.append(f" {safe} = {cpp_val};")
1071
+ break
706
1072
  continue
707
- if name in self.ctx.series_vars:
1073
+ if self._binding_is_series(name, safe):
708
1074
  cpp_val = self._resolve_known(init_expr)
709
1075
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
710
1076
  lines.append(f" {safe}.push({cpp_val});")
@@ -722,9 +1088,26 @@ class TopLevelEmitter:
722
1088
  lines.append(" } else {")
723
1089
  for name, _, _ in self.ctx.var_members:
724
1090
  safe = self._safe_name(name)
1091
+ runtime_info = self._runtime_scalar_var_init_by_member.get(name)
1092
+ if (runtime_info is not None
1093
+ and runtime_info.get("drawing_cpp") is not None):
1094
+ if runtime_info.get("is_series"):
1095
+ lines.append(f" if ({runtime_info['flag']}) {{")
1096
+ self._emit_history_series_write(
1097
+ lines, " ", safe, f"{safe}[0]"
1098
+ )
1099
+ lines.append(" } else {")
1100
+ self._emit_history_series_write(
1101
+ lines,
1102
+ " ",
1103
+ safe,
1104
+ f"{runtime_info['drawing_cpp']}{{}}",
1105
+ )
1106
+ lines.append(" }")
1107
+ continue
725
1108
  if name in self._array_vars:
726
1109
  continue
727
- if name in self.ctx.series_vars:
1110
+ if self._binding_is_series(name, safe):
728
1111
  self._emit_history_series_write(
729
1112
  lines, " ", safe, f"{safe}[0]"
730
1113
  )
@@ -740,6 +1123,20 @@ class TopLevelEmitter:
740
1123
  self._emit_history_series_write(
741
1124
  lines, " ", cloned, f"{cloned}[0]"
742
1125
  )
1126
+ # Context-sensitive nested helper instances own fresh
1127
+ # Series members outside the flat cs remap table. Advance
1128
+ # each exact fresh member once per bar as well; otherwise a
1129
+ # valid ``fresh[1]`` history read never moves past slot 0.
1130
+ for _owner, orig_safe, fresh_safe in self._fresh_var_members:
1131
+ if orig_safe != safe or fresh_safe in carry_emitted:
1132
+ continue
1133
+ carry_emitted.add(fresh_safe)
1134
+ self._emit_history_series_write(
1135
+ lines,
1136
+ " ",
1137
+ fresh_safe,
1138
+ f"{fresh_safe}[0]",
1139
+ )
743
1140
  lines.append(" }")
744
1141
 
745
1142
  # c. Push non-var series (they start fresh each bar with a push)
@@ -927,10 +1324,18 @@ class TopLevelEmitter:
927
1324
  lines.append("")
928
1325
 
929
1326
  def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
930
- """Stable C++ identifier for a UDT instance method (``_udt_Type_method``)."""
931
- udt = fi.udt_type_name or ""
1327
+ """Stable C++ identifier for a typed instance method."""
1328
+ receiver_spec = (
1329
+ fi.param_type_specs[0]
1330
+ if getattr(fi, "param_type_specs", None)
1331
+ else None
1332
+ )
1333
+ receiver = method_receiver_cpp_token(
1334
+ receiver_spec,
1335
+ fi.udt_type_name,
1336
+ )
932
1337
  base = fi.node.name if fi.node else ""
933
- return self._func_safe_name(f"_udt_{udt}_{base}")
1338
+ return self._func_safe_name(f"_udt_{receiver}_{base}")
934
1339
 
935
1340
  def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
936
1341
  instance: dict | None = None) -> None:
@@ -948,51 +1353,191 @@ class TopLevelEmitter:
948
1353
  if node is None:
949
1354
  return
950
1355
 
951
- is_udt = bool(getattr(fi, "is_udt_method", False)) and fi.udt_type_name
1356
+ # Collection registries historically used raw variable names for the
1357
+ # whole translation unit. Emit each callable against copy-on-write
1358
+ # registries overlaid with its analyzer-captured lexical TypeSpecs, then
1359
+ # restore the top-level state. This keeps legacy readers working while
1360
+ # preventing local declarations/method dispatch from contaminating a
1361
+ # sibling UDF, UDT method, clone, or on_bar.
1362
+ prev_func_collection_specs = self._current_func_collection_specs
1363
+ prev_func_collection_shadows = self._current_func_collection_shadows
1364
+ prev_collection_types = self._collection_types
1365
+ prev_array_vars = self._array_vars
1366
+ prev_map_vars = self._map_vars
1367
+ prev_matrix_specs = self._matrix_specs
1368
+ # Start from top-level state only. Callable locals become visible at
1369
+ # their declaration statement (after its RHS), not at function entry:
1370
+ # preloading the analyzer's final inventory would make a later local
1371
+ # shadow an earlier read of a same-named global collection.
1372
+ self._current_func_collection_specs = {}
1373
+ self._current_func_collection_shadows = set()
1374
+ self._collection_types = dict(prev_collection_types)
1375
+ self._array_vars = set(prev_array_vars)
1376
+ self._map_vars = set(prev_map_vars)
1377
+ self._matrix_specs = dict(prev_matrix_specs)
1378
+
1379
+ is_method = (
1380
+ bool(getattr(fi, "is_udt_method", False))
1381
+ and fi.udt_type_name
1382
+ )
952
1383
 
953
1384
  # Determine param types and set context for type inference inside body
954
1385
  param_strs = []
955
1386
  self._current_func_param_types = {}
956
1387
  self._current_func_param_specs = {}
1388
+ param_hints = (getattr(node, "annotations", None) or {}).get(
1389
+ "param_type_hints", []
1390
+ )
1391
+ self._current_func_declared_param_names = {
1392
+ alias
1393
+ for i, param in enumerate(node.params)
1394
+ if i < len(param_hints) and param_hints[i]
1395
+ for alias in (param, self._safe_name(param))
1396
+ }
957
1397
  self._current_func_series_params = set()
1398
+ self._current_func_series_param_types = {}
958
1399
  self._udt_param_udt = {}
959
1400
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
1401
+ declared_param_specs = list(
1402
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
1403
+ fi.name, ()
1404
+ )
1405
+ )
1406
+ variant_param_types = (
1407
+ getattr(self.ctx, "func_callsite_param_types", {}).get(
1408
+ (fi.name, call_site_idx), ()
1409
+ )
1410
+ if call_site_idx is not None
1411
+ else ()
1412
+ )
960
1413
  for i, p in enumerate(node.params):
961
1414
  spec = None
962
- if is_udt and i == 0 and fi.udt_type_name:
963
- # A method receiver whose type is a drawing primitive
964
- # (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
965
- # the unknown ``line&``. Register _udt_param_udt so the body's
966
- # getters dispatch through the §4.3 drawing path (L.6d / U.5).
967
- recv_cpp = DRAWING_TYPE_TO_CPP.get(fi.udt_type_name, fi.udt_type_name)
968
- cpp_t = f"{recv_cpp}&"
969
- safe_p = self._safe_name(p)
970
- self._udt_param_udt[safe_p] = fi.udt_type_name
971
- self._udt_param_udt[p] = fi.udt_type_name
1415
+ receiver_spec = None
1416
+ if is_method and i == 0:
1417
+ recv_spec = (
1418
+ fi.param_type_specs[i]
1419
+ if i < len(fi.param_type_specs)
1420
+ else None
1421
+ )
1422
+ receiver_spec = recv_spec
1423
+
1424
+ if (
1425
+ receiver_spec is not None
1426
+ and receiver_spec.kind == "primitive"
1427
+ and p in func_sv
1428
+ ):
1429
+ # A primitive receiver used with history is a Series boundary,
1430
+ # just like an ordinary history-bearing UDF parameter.
1431
+ elem_cpp_t = self._series_param_element_cpp_type(
1432
+ fi, i, call_site_idx
1433
+ )
1434
+ cpp_t = f"const Series<{elem_cpp_t}>&"
1435
+ spec = receiver_spec
1436
+ self._current_func_series_params.add(p)
1437
+ self._current_func_series_param_types[p] = elem_cpp_t
1438
+ self._current_func_series_param_types[
1439
+ self._safe_name(p)
1440
+ ] = elem_cpp_t
1441
+ elif is_method and i == 0 and fi.udt_type_name:
1442
+ # Receiver pass modes follow Pine's value/ID families. A user
1443
+ # UDT is itself a numeric object-ID handle, so pass it by value:
1444
+ # field mutation dereferences the same arena record while
1445
+ # ``self := other`` remains a local handle rebind.
1446
+ recv_spec = receiver_spec
1447
+ if recv_spec is None:
1448
+ recv_spec = self._type_spec_from_hint_name(
1449
+ fi.udt_type_name
1450
+ )
1451
+ spec = recv_spec
1452
+ if recv_spec is not None:
1453
+ cpp_t = self._type_spec_to_cpp(recv_spec)
1454
+ if (
1455
+ recv_spec.kind in {"array", "matrix"}
1456
+ or (
1457
+ recv_spec.kind == "udt"
1458
+ and recv_spec.name not in self._udt_defs
1459
+ )
1460
+ ):
1461
+ cpp_t = f"{cpp_t}&"
1462
+ if recv_spec.kind == "udt" and recv_spec.name:
1463
+ safe_p = self._safe_name(p)
1464
+ self._udt_param_udt[safe_p] = recv_spec.name
1465
+ self._udt_param_udt[p] = recv_spec.name
1466
+ else:
1467
+ # Compatibility for synthetic/legacy method records that
1468
+ # carry only the old receiver-name field.
1469
+ recv_cpp = DRAWING_TYPE_TO_CPP.get(
1470
+ fi.udt_type_name, fi.udt_type_name
1471
+ )
1472
+ cpp_t = (
1473
+ recv_cpp
1474
+ if fi.udt_type_name in self._udt_defs
1475
+ else f"{recv_cpp}&"
1476
+ )
1477
+ safe_p = self._safe_name(p)
1478
+ self._udt_param_udt[safe_p] = fi.udt_type_name
1479
+ self._udt_param_udt[p] = fi.udt_type_name
972
1480
  elif fi.name == "isInSession" and i < 2:
973
1481
  cpp_t = "std::string"
974
1482
  elif p in func_sv:
975
- # This param uses history access (e.g. src[1]) — pass as Series
976
- cpp_t = "const Series<double>&"
1483
+ # This param uses history access (e.g. src[1]) — preserve its
1484
+ # Pine scalar family instead of hard-coding Series<double>.
1485
+ elem_cpp_t = self._series_param_element_cpp_type(
1486
+ fi, i, call_site_idx
1487
+ )
1488
+ cpp_t = f"const Series<{elem_cpp_t}>&"
977
1489
  self._current_func_series_params.add(p)
1490
+ self._current_func_series_param_types[p] = elem_cpp_t
1491
+ self._current_func_series_param_types[
1492
+ self._safe_name(p)
1493
+ ] = elem_cpp_t
1494
+ if (
1495
+ i < len(getattr(fi, "param_type_specs", []))
1496
+ and fi.param_type_specs[i] is not None
1497
+ ):
1498
+ spec = fi.param_type_specs[i]
1499
+ elif (
1500
+ i >= len(declared_param_specs)
1501
+ or declared_param_specs[i] is None
1502
+ ) and i < len(variant_param_types) and variant_param_types[i] in {
1503
+ PineType.INT,
1504
+ PineType.FLOAT,
1505
+ PineType.BOOL,
1506
+ PineType.STRING,
1507
+ PineType.COLOR,
1508
+ }:
1509
+ # Non-history untyped parameters are polymorphic too. A pure
1510
+ # wrapper around a stateful helper is cloned per written call,
1511
+ # so its scalar boundary must use that clone's actual family
1512
+ # instead of the first call's shared FuncInfo inference.
1513
+ variant_pt = variant_param_types[i]
1514
+ cpp_t = {
1515
+ PineType.INT: "int64_t",
1516
+ PineType.FLOAT: "double",
1517
+ PineType.BOOL: "bool",
1518
+ PineType.STRING: "std::string",
1519
+ PineType.COLOR: "int",
1520
+ }[variant_pt]
978
1521
  elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
979
1522
  # Precise per-param TypeSpec (declared hint or call-site inference):
980
- # ``pivot hi`` -> ``pivot&``, ``line ln`` -> ``Line&``, an untyped
981
- # ``s`` used as a string -> ``std::string``. UDT / collection
982
- # params pass by reference (Pine UDTs/arrays are reference types,
983
- # so mutations propagate and member access compiles).
1523
+ # ``pivot hi`` -> ``pivot`` numeric handle, ``line ln`` keeps its
1524
+ # established drawing-handle route, and an untyped ``s`` used as
1525
+ # a string -> ``std::string``. User UDT field mutation propagates
1526
+ # through the arena even though the handle parameter is by value.
984
1527
  spec = fi.param_type_specs[i]
985
1528
  cpp_t = self._type_spec_to_cpp(spec)
986
1529
  if spec.kind == "udt":
987
1530
  self._udt_param_udt[p] = spec.name
988
1531
  self._udt_param_udt[self._safe_name(p)] = spec.name
989
- cpp_t = f"{cpp_t}&"
1532
+ if spec.name not in self._udt_defs:
1533
+ cpp_t = f"{cpp_t}&"
990
1534
  elif spec.kind in ("array", "map"):
991
1535
  elem = spec.element if spec.kind == "array" else spec.value
992
1536
  if elem is not None and elem.kind == "udt":
993
1537
  self._udt_param_udt[p] = elem.name
994
1538
  self._udt_param_udt[self._safe_name(p)] = elem.name
995
- cpp_t = f"{cpp_t}&"
1539
+ if spec.kind == "array":
1540
+ cpp_t = f"{cpp_t}&"
996
1541
  elif i < len(fi.param_types):
997
1542
  pt = fi.param_types[i]
998
1543
  cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
@@ -1017,16 +1562,44 @@ class TopLevelEmitter:
1017
1562
  # A function returning a drawing handle must emit the C++ handle
1018
1563
  # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
1019
1564
  ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
1565
+ elif self._func_int_return_uses_wide_history(
1566
+ fi, call_site_idx=call_site_idx
1567
+ ):
1568
+ # The history parameter is represented as int64_t so timestamp
1569
+ # values and their na sentinel cannot narrow at the return edge.
1570
+ ret_type = "int64_t"
1571
+ elif (
1572
+ call_site_idx is not None
1573
+ and self._callsite_callable_return_pine_type(
1574
+ fi, call_site_idx
1575
+ ) != PineType.UNKNOWN
1576
+ ):
1577
+ ret_type = PINE_TYPE_TO_CPP.get(
1578
+ self._callsite_callable_return_pine_type(
1579
+ fi, call_site_idx
1580
+ ),
1581
+ "double",
1582
+ )
1020
1583
  elif getattr(fi, "return_type_spec", None) is not None:
1021
- # Array-returning function (``f() => array.from(...)``) — emit the
1022
- # vector type from the inferred element TypeSpec.
1584
+ # Exact return TypeSpec (collections plus narrowly cached primitive
1585
+ # terminal reads) takes precedence over the coarse PineType slot.
1023
1586
  ret_type = self._type_spec_to_cpp(fi.return_type_spec)
1024
1587
  else:
1025
1588
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
1589
+ rhs_return_cpp_type = (
1590
+ ret_type
1591
+ if (self._is_nullable_collection_cpp_type(ret_type)
1592
+ or ret_type in DRAWING_TYPE_TO_CPP.values()
1593
+ or ret_type in self._udt_defs)
1594
+ else None
1595
+ )
1026
1596
 
1027
1597
  # For per-call-site variants, suffix the function name and activate TA + var remapping
1028
- func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
1029
- var_init_flag: str | None = None
1598
+ func_name = (
1599
+ self._emit_udt_method_cpp_name(fi)
1600
+ if is_method
1601
+ else self._func_safe_name(fi.name)
1602
+ )
1030
1603
  if instance is not None:
1031
1604
  # Fresh context-sensitive instance: name + composed remaps come from
1032
1605
  # the instance record. No textual cs index (dispatch is via the
@@ -1035,10 +1608,8 @@ class TopLevelEmitter:
1035
1608
  self._active_ta_remap = instance["ta_remap"]
1036
1609
  self._active_var_remap = instance["var_remap"]
1037
1610
  self._active_fixnan_remap = instance.get("fixnan_remap", {})
1038
- self._in_ta_func_variant = True
1039
1611
  self._active_call_site_idx = None
1040
1612
  self._current_instance_name = instance["name"]
1041
- var_init_flag = f"_fvinit_{instance['name']}"
1042
1613
  elif call_site_idx is not None:
1043
1614
  func_name = f"{func_name}_cs{call_site_idx}"
1044
1615
  remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
@@ -1046,7 +1617,6 @@ class TopLevelEmitter:
1046
1617
  var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
1047
1618
  self._active_var_remap = var_remap
1048
1619
  self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
1049
- self._in_ta_func_variant = True
1050
1620
  self._active_call_site_idx = call_site_idx
1051
1621
  # Use the actual emitted name. Plain UDFs are unchanged; UDT
1052
1622
  # methods carry their `_udt_Type_method` prefix. This identity is
@@ -1057,26 +1627,62 @@ class TopLevelEmitter:
1057
1627
  self._active_ta_remap = {}
1058
1628
  self._active_var_remap = {}
1059
1629
  self._active_fixnan_remap = {}
1060
- self._in_ta_func_variant = False
1061
1630
  self._active_call_site_idx = None
1062
1631
  self._current_instance_name = None
1063
1632
 
1064
1633
  prev_func_locals = self._current_func_locals
1065
1634
  prev_func_local_types = self._current_func_local_types
1635
+ prev_lexical_drawing_types = self._lexical_drawing_types
1636
+ prev_lexical_udt_types = self._lexical_udt_types
1637
+ prev_lexical_series_bindings = self._lexical_series_bindings
1638
+ prev_known_var_tombstones = self._lexical_known_var_tombstones
1066
1639
  prev_func_body = getattr(self, "_current_func_body", None)
1067
1640
  prev_func_name = getattr(self, "_active_func_name", None)
1068
- # The function body is the lexical scope used by the UDT-alias analysis
1069
- # (BUG C): a local initialised from a var/global UDT lvalue and later
1070
- # mutated through must alias, not value-copy.
1071
1641
  self._current_func_body = node.body
1072
1642
  self._active_func_name = fi.name
1073
- # Pointer-aliased UDT locals are function-scoped: a name like ``p_ivot``
1074
- # may be a rebinding pointer alias in one function and a ``pivot&``
1075
- # parameter in another, so reset per function to avoid cross-contamination.
1076
- prev_ptr_alias = self._udt_ptr_alias_locals
1077
- self._udt_ptr_alias_locals = set()
1078
1643
  self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
1079
1644
  self._current_func_local_types = {}
1645
+ self._lexical_drawing_types = {
1646
+ param: DRAWING_TYPE_TO_CPP[drawing_name]
1647
+ for param in node.params
1648
+ for drawing_name in (
1649
+ (
1650
+ self._current_func_param_specs[param].name
1651
+ if (param in self._current_func_param_specs
1652
+ and self._current_func_param_specs[param].kind == "udt")
1653
+ else self._udt_param_udt.get(param)
1654
+ ),
1655
+ )
1656
+ if (drawing_name in DRAWING_TYPE_TO_CPP
1657
+ and self._current_func_param_types.get(
1658
+ param, ""
1659
+ ).removesuffix("&").removesuffix("*")
1660
+ == DRAWING_TYPE_TO_CPP[drawing_name])
1661
+ }
1662
+ self._lexical_udt_types = {
1663
+ param: (
1664
+ spec.name
1665
+ if spec is not None
1666
+ and spec.kind == "udt"
1667
+ and spec.name in self._udt_defs
1668
+ else (
1669
+ self._udt_param_udt.get(param)
1670
+ if (self._udt_param_udt.get(param) in self._udt_defs
1671
+ and self._current_func_param_types.get(
1672
+ param, ""
1673
+ ).removesuffix("&").removesuffix("*")
1674
+ == self._udt_param_udt.get(param))
1675
+ else None
1676
+ )
1677
+ )
1678
+ for param in node.params
1679
+ for spec in (self._current_func_param_specs.get(param),)
1680
+ }
1681
+ self._lexical_series_bindings = {
1682
+ param: param in self._current_func_series_params
1683
+ for param in node.params
1684
+ }
1685
+ self._lexical_known_var_tombstones = set(node.params)
1080
1686
  # Plain (non-persistent) scalar locals are emitted inline and live in
1081
1687
  # no other set; collect them so the unknown-identifier guard in
1082
1688
  # _visit_ident does not mistake them for undeclared symbols.
@@ -1084,12 +1690,6 @@ class TopLevelEmitter:
1084
1690
 
1085
1691
  lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
1086
1692
 
1087
- # Function-scoped ``var`` one-shot initializer: Pine ``var`` inside a
1088
- # function is a function-local static — its initializer runs exactly
1089
- # once, on the first call to THIS variant, with the first bar's values
1090
- # the function actually sees. Each clone (cs0/cs1/…) is independent.
1091
- self._emit_func_var_init_block(fi, call_site_idx, lines, flag_override=var_init_flag)
1092
-
1093
1693
  emitted_return = False
1094
1694
  if node.is_single_expr and node.body:
1095
1695
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
@@ -1098,7 +1698,10 @@ class TopLevelEmitter:
1098
1698
  # through to the default return.
1099
1699
  self._visit_stmt(node.body[0], lines, indent=2)
1100
1700
  elif expr:
1101
- lines.append(f" return {self._visit_expr(expr)};")
1701
+ lines.append(
1702
+ " return "
1703
+ f"{self._visit_rhs_value(expr, target_cpp_type=rhs_return_cpp_type)};"
1704
+ )
1102
1705
  emitted_return = True
1103
1706
  else:
1104
1707
  for i, s in enumerate(node.body):
@@ -1112,7 +1715,10 @@ class TopLevelEmitter:
1112
1715
  if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
1113
1716
  self._visit_stmt(s, lines, indent=2)
1114
1717
  else:
1115
- lines.append(f" return {self._visit_expr(s.expr)};")
1718
+ lines.append(
1719
+ " return "
1720
+ f"{self._visit_rhs_value(s.expr, target_cpp_type=rhs_return_cpp_type)};"
1721
+ )
1116
1722
  emitted_return = True
1117
1723
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
1118
1724
  # Switch/if as last statement = return expression in PineScript
@@ -1126,7 +1732,13 @@ class TopLevelEmitter:
1126
1732
  else:
1127
1733
  default_ret = self._default_for_type(ret_type)
1128
1734
  lines.append(f" {ret_type} _func_ret = {default_ret};")
1129
- self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
1735
+ self._visit_if_switch_expr(
1736
+ s,
1737
+ "_func_ret",
1738
+ lines,
1739
+ indent=2,
1740
+ target_cpp_type=rhs_return_cpp_type,
1741
+ )
1130
1742
  lines.append(f" return _func_ret;")
1131
1743
  emitted_return = True
1132
1744
  else:
@@ -1148,82 +1760,30 @@ class TopLevelEmitter:
1148
1760
  lines.append(" }")
1149
1761
  self._current_func_param_types = {}
1150
1762
  self._current_func_param_specs = {}
1763
+ self._current_func_declared_param_names = set()
1151
1764
  self._current_func_series_params = set()
1765
+ self._current_func_series_param_types = {}
1152
1766
  self._udt_param_udt = {}
1153
1767
  self._current_func_locals = prev_func_locals
1154
1768
  self._current_func_local_types = prev_func_local_types
1769
+ self._lexical_drawing_types = prev_lexical_drawing_types
1770
+ self._lexical_udt_types = prev_lexical_udt_types
1771
+ self._lexical_series_bindings = prev_lexical_series_bindings
1772
+ self._lexical_known_var_tombstones = prev_known_var_tombstones
1155
1773
  self._current_func_body = prev_func_body
1156
1774
  self._active_func_name = prev_func_name
1157
- self._udt_ptr_alias_locals = prev_ptr_alias
1775
+ self._current_func_collection_specs = prev_func_collection_specs
1776
+ self._current_func_collection_shadows = prev_func_collection_shadows
1777
+ self._collection_types = prev_collection_types
1778
+ self._array_vars = prev_array_vars
1779
+ self._map_vars = prev_map_vars
1780
+ self._matrix_specs = prev_matrix_specs
1158
1781
  self._active_ta_remap = {}
1159
1782
  self._active_var_remap = {}
1160
1783
  self._active_fixnan_remap = {}
1161
- self._in_ta_func_variant = False
1162
1784
  self._active_call_site_idx = None
1163
1785
  self._current_instance_name = None
1164
1786
 
1165
- def _func_var_init_flag_name(self, fname: str, call_site_idx: int | None) -> str:
1166
- suffix = f"_cs{call_site_idx}" if call_site_idx is not None else ""
1167
- return f"_fvinit_{self._func_safe_name(fname)}{suffix}"
1168
-
1169
- def _emit_func_var_init_block(self, fi: FuncInfo, call_site_idx: int | None,
1170
- lines: list[str], flag_override: str | None = None) -> None:
1171
- """Emit the one-shot initializer block for a function's ``var`` members.
1172
-
1173
- Pine ``var`` declared inside a function is a function-local static:
1174
- the initializer runs exactly once on the FIRST call to this variant
1175
- (using the first bar's values the function actually sees) and the
1176
- result persists for the strategy's lifetime. Each per-call-site clone
1177
- is an independent instance with its own flag and its own set of
1178
- (remapped) members.
1179
-
1180
- This closes the gap where a function-scoped ``var line x = line.new(...)``
1181
- (or any non-compile-time initializer — drawing handles, UDT ctors,
1182
- arrays, runtime expressions) was declared as a default-constructed
1183
- class member but its initializer was dropped, leaving the member ``na``
1184
- / uninitialised and causing "drawing access on na handle" at runtime.
1185
- """
1186
- members = self.ctx.func_var_members.get(fi.name)
1187
- if not members:
1188
- return
1189
- flag = flag_override or self._func_var_init_flag_name(fi.name, call_site_idx)
1190
- # ``_active_var_remap`` is already set for this variant by the caller,
1191
- # so lowering each init expression here correctly resolves references
1192
- # to sibling var members (which are themselves remapped for clones).
1193
- init_lines: list[str] = []
1194
- for name, ptype, _init_str in members:
1195
- init_ast = self.ctx.var_member_init_exprs.get(name)
1196
- safe = self._safe_name(name)
1197
- target = self._active_var_remap.get(safe, safe)
1198
- if name in self.ctx.series_vars:
1199
- if init_ast is None:
1200
- continue
1201
- init_cpp = self._visit_expr(init_ast)
1202
- init_cpp = self._typed_na_init(init_cpp, name, ptype)
1203
- init_lines.append(f" {target}.push({init_cpp});")
1204
- continue
1205
- if init_ast is None:
1206
- # No initializer to lower (e.g. bare ``var box b``); leave the
1207
- # member at its default-constructed value.
1208
- continue
1209
- # Skip a plain ``na`` initializer for drawing handles / UDTs whose
1210
- # default-constructed member is already the na sentinel; assigning
1211
- # ``na<double>()`` would not type-match the handle / struct.
1212
- udt_t = self._udt_var_types.get(name)
1213
- is_drawing = udt_t in DRAWING_TYPE_TO_CPP if udt_t else False
1214
- is_udt = udt_t in self._udt_defs if udt_t else False
1215
- from ..ast_nodes import NaLiteral
1216
- if (is_drawing or is_udt) and isinstance(init_ast, NaLiteral):
1217
- continue
1218
- init_cpp = self._visit_expr(init_ast)
1219
- init_lines.append(f" {target} = {init_cpp};")
1220
- if not init_lines:
1221
- return
1222
- lines.append(f" if (!{flag}) {{")
1223
- lines.extend(init_lines)
1224
- lines.append(f" {flag} = true;")
1225
- lines.append(" }")
1226
-
1227
1787
  def _emit_precalculate_and_run(self, lines: list[str]) -> None:
1228
1788
  has_static_ta = any(
1229
1789
  self._ta_site_uses_precalc(site)
@@ -1241,7 +1801,7 @@ class TopLevelEmitter:
1241
1801
  continue
1242
1802
  if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1243
1803
  continue
1244
- if stmt.name in self.ctx.series_vars:
1804
+ if self._decl_binding_is_series(id(stmt), stmt.name):
1245
1805
  replayed_source_series.append(self._safe_name(stmt.name))
1246
1806
  replayed_source_series = sorted(set(replayed_source_series))
1247
1807
 
@@ -1263,7 +1823,7 @@ class TopLevelEmitter:
1263
1823
  if _ti in self._dead_ta_indices:
1264
1824
  continue
1265
1825
  if self._ta_site_uses_precalc(site):
1266
- resolved = [self._resolve_known(a) for a in site.ctor_args]
1826
+ resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1267
1827
  safe_resolved = []
1268
1828
  for r in resolved:
1269
1829
  safe_resolved.append(r if self._is_compile_time_value(r) else "1")
@@ -1349,7 +1909,7 @@ class TopLevelEmitter:
1349
1909
  # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
1350
1910
  # ``_visit_var_decl``'s ``node.name in self.ctx.series_vars``
1351
1911
  # branch) — a plain ``=`` there is a compile error.
1352
- if stmt.name in self.ctx.series_vars:
1912
+ if self._decl_binding_is_series(id(stmt), stmt.name):
1353
1913
  lines.append(f' {safe}.push({cpp_val});')
1354
1914
  else:
1355
1915
  lines.append(f' {safe} = {cpp_val};')
@@ -1375,7 +1935,7 @@ class TopLevelEmitter:
1375
1935
  if _ti in self._dead_ta_indices:
1376
1936
  continue
1377
1937
  if self._ta_site_uses_precalc(site):
1378
- resolved = [self._resolve_known(a) for a in site.ctor_args]
1938
+ resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1379
1939
  safe_resolved = []
1380
1940
  for r in resolved:
1381
1941
  safe_resolved.append(r if self._is_compile_time_value(r) else "1")