@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.
@@ -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
@@ -624,6 +949,21 @@ class TopLevelEmitter:
624
949
  f"{info['member_name']}.clear();"
625
950
  )
626
951
 
952
+ # A history-reading plain-UDF parameter advances on the chart clock,
953
+ # even when lazy control flow skips its written call on this bar. Seed
954
+ # the new slot with the prior current value (``na`` before first reach);
955
+ # an executed call later in the bar updates this same slot with its
956
+ # scalar actual. Typed-method ``series_arg`` bridges intentionally keep
957
+ # their existing execution-clock behavior.
958
+ for info in self._inline_history_members:
959
+ if info["kind"] != "udf_series_arg":
960
+ continue
961
+ member = info["member_name"]
962
+ lines.append(
963
+ f" if (history_advances_new_bar()) "
964
+ f"{member}.push({member}.current());"
965
+ )
966
+
627
967
  # a. Push bar field series (with bar magnifier support)
628
968
  for field_name in sorted(self.ctx.series_bar_fields):
629
969
  push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
@@ -659,11 +999,36 @@ class TopLevelEmitter:
659
999
  if self.ctx.var_members:
660
1000
  lines.append(" if (!_var_initialized) {")
661
1001
  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.
1002
+ # Callable-scoped ``var`` members initialize at their exact
1003
+ # declaration statements, not in the global on_bar preamble.
664
1004
  if name in getattr(self, "_func_local_var_names", ()):
665
1005
  continue
666
1006
  safe = self._safe_name(name)
1007
+ runtime_info = self._runtime_scalar_var_init_by_member.get(name)
1008
+ if (runtime_info is not None
1009
+ and runtime_info.get("drawing_cpp") is not None):
1010
+ if runtime_info.get("is_series"):
1011
+ self._emit_history_series_write(
1012
+ lines,
1013
+ " ",
1014
+ safe,
1015
+ f"{runtime_info['drawing_cpp']}{{}}",
1016
+ )
1017
+ continue
1018
+ # Declaration-site initialization owns nullable collection
1019
+ # members carrying runtime metadata. A persistent map/matrix
1020
+ # if/switch initializer must not also enter the legacy
1021
+ # first-bar aggregate preamble (which cannot lower a block
1022
+ # expression and formerly emitted ``/* unknown */`` for maps).
1023
+ # Keep primitive and ternary paths on their established order.
1024
+ runtime_spec = (
1025
+ runtime_info.get("type_spec")
1026
+ if runtime_info is not None
1027
+ else None
1028
+ )
1029
+ if (runtime_spec is not None
1030
+ and runtime_spec.kind in {"map", "matrix"}):
1031
+ continue
667
1032
  if name in self._array_vars:
668
1033
  for stmt in self.ctx.ast.body:
669
1034
  if isinstance(stmt, VarDecl) and stmt.name == name:
@@ -672,39 +1037,55 @@ class TopLevelEmitter:
672
1037
  break
673
1038
  continue
674
1039
  if name in self._matrix_specs:
675
- # Matrix vars: initialize with matrix.new expression
1040
+ # Matrix vars are nullable ID handles. Initialize from the
1041
+ # authored RHS in declaration order, not only from a
1042
+ # matrix.new(...) call: ``var matrix<int> alias = source``
1043
+ # must copy source's ID once, while a later local rebind
1044
+ # remains independent.
676
1045
  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)
1046
+ if isinstance(stmt, VarDecl) and stmt.name == name:
1047
+ cpp_val = self._visit_rhs_value(
1048
+ stmt.value,
1049
+ name,
1050
+ target_cpp_type=self._type_spec_to_cpp(
1051
+ self._matrix_specs[name]
1052
+ ),
1053
+ )
679
1054
  lines.append(f" {safe} = {cpp_val};")
680
1055
  break
681
1056
  continue
682
1057
  if name in self._map_vars:
683
1058
  for stmt in self.ctx.ast.body:
684
1059
  if isinstance(stmt, VarDecl) and stmt.name == name:
685
- cpp_val = self._visit_expr(stmt.value)
1060
+ cpp_val = self._visit_rhs_value(
1061
+ stmt.value,
1062
+ name,
1063
+ target_cpp_type=self._type_spec_to_cpp(
1064
+ self._map_spec_for_name(name)
1065
+ ),
1066
+ )
686
1067
  lines.append(f" {safe} = {cpp_val};")
687
1068
  break
688
1069
  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:
1070
+ if name in self._runtime_scalar_var_init_members:
706
1071
  continue
707
- if name in self.ctx.series_vars:
1072
+ # Persistent authored UDT variables initialize from their full
1073
+ # source RHS, not only from ``Type.new``. In particular,
1074
+ # ``var Item alias = original`` must copy the numeric handle
1075
+ # once and therefore alias the same arena record.
1076
+ udt_name = self._member_udt_type(name)
1077
+ if udt_name in self._udt_defs:
1078
+ for stmt in self.ctx.ast.body:
1079
+ if isinstance(stmt, VarDecl) and stmt.name == name:
1080
+ cpp_val = self._visit_rhs_value(
1081
+ stmt.value,
1082
+ name,
1083
+ target_cpp_type=udt_name,
1084
+ )
1085
+ lines.append(f" {safe} = {cpp_val};")
1086
+ break
1087
+ continue
1088
+ if self._binding_is_series(name, safe):
708
1089
  cpp_val = self._resolve_known(init_expr)
709
1090
  cpp_val = self._typed_na_init(cpp_val, name, ptype)
710
1091
  lines.append(f" {safe}.push({cpp_val});")
@@ -722,9 +1103,26 @@ class TopLevelEmitter:
722
1103
  lines.append(" } else {")
723
1104
  for name, _, _ in self.ctx.var_members:
724
1105
  safe = self._safe_name(name)
1106
+ runtime_info = self._runtime_scalar_var_init_by_member.get(name)
1107
+ if (runtime_info is not None
1108
+ and runtime_info.get("drawing_cpp") is not None):
1109
+ if runtime_info.get("is_series"):
1110
+ lines.append(f" if ({runtime_info['flag']}) {{")
1111
+ self._emit_history_series_write(
1112
+ lines, " ", safe, f"{safe}[0]"
1113
+ )
1114
+ lines.append(" } else {")
1115
+ self._emit_history_series_write(
1116
+ lines,
1117
+ " ",
1118
+ safe,
1119
+ f"{runtime_info['drawing_cpp']}{{}}",
1120
+ )
1121
+ lines.append(" }")
1122
+ continue
725
1123
  if name in self._array_vars:
726
1124
  continue
727
- if name in self.ctx.series_vars:
1125
+ if self._binding_is_series(name, safe):
728
1126
  self._emit_history_series_write(
729
1127
  lines, " ", safe, f"{safe}[0]"
730
1128
  )
@@ -740,6 +1138,20 @@ class TopLevelEmitter:
740
1138
  self._emit_history_series_write(
741
1139
  lines, " ", cloned, f"{cloned}[0]"
742
1140
  )
1141
+ # Context-sensitive nested helper instances own fresh
1142
+ # Series members outside the flat cs remap table. Advance
1143
+ # each exact fresh member once per bar as well; otherwise a
1144
+ # valid ``fresh[1]`` history read never moves past slot 0.
1145
+ for _owner, orig_safe, fresh_safe in self._fresh_var_members:
1146
+ if orig_safe != safe or fresh_safe in carry_emitted:
1147
+ continue
1148
+ carry_emitted.add(fresh_safe)
1149
+ self._emit_history_series_write(
1150
+ lines,
1151
+ " ",
1152
+ fresh_safe,
1153
+ f"{fresh_safe}[0]",
1154
+ )
743
1155
  lines.append(" }")
744
1156
 
745
1157
  # c. Push non-var series (they start fresh each bar with a push)
@@ -927,10 +1339,18 @@ class TopLevelEmitter:
927
1339
  lines.append("")
928
1340
 
929
1341
  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 ""
1342
+ """Stable C++ identifier for a typed instance method."""
1343
+ receiver_spec = (
1344
+ fi.param_type_specs[0]
1345
+ if getattr(fi, "param_type_specs", None)
1346
+ else None
1347
+ )
1348
+ receiver = method_receiver_cpp_token(
1349
+ receiver_spec,
1350
+ fi.udt_type_name,
1351
+ )
932
1352
  base = fi.node.name if fi.node else ""
933
- return self._func_safe_name(f"_udt_{udt}_{base}")
1353
+ return self._func_safe_name(f"_udt_{receiver}_{base}")
934
1354
 
935
1355
  def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
936
1356
  instance: dict | None = None) -> None:
@@ -948,51 +1368,191 @@ class TopLevelEmitter:
948
1368
  if node is None:
949
1369
  return
950
1370
 
951
- is_udt = bool(getattr(fi, "is_udt_method", False)) and fi.udt_type_name
1371
+ # Collection registries historically used raw variable names for the
1372
+ # whole translation unit. Emit each callable against copy-on-write
1373
+ # registries overlaid with its analyzer-captured lexical TypeSpecs, then
1374
+ # restore the top-level state. This keeps legacy readers working while
1375
+ # preventing local declarations/method dispatch from contaminating a
1376
+ # sibling UDF, UDT method, clone, or on_bar.
1377
+ prev_func_collection_specs = self._current_func_collection_specs
1378
+ prev_func_collection_shadows = self._current_func_collection_shadows
1379
+ prev_collection_types = self._collection_types
1380
+ prev_array_vars = self._array_vars
1381
+ prev_map_vars = self._map_vars
1382
+ prev_matrix_specs = self._matrix_specs
1383
+ # Start from top-level state only. Callable locals become visible at
1384
+ # their declaration statement (after its RHS), not at function entry:
1385
+ # preloading the analyzer's final inventory would make a later local
1386
+ # shadow an earlier read of a same-named global collection.
1387
+ self._current_func_collection_specs = {}
1388
+ self._current_func_collection_shadows = set()
1389
+ self._collection_types = dict(prev_collection_types)
1390
+ self._array_vars = set(prev_array_vars)
1391
+ self._map_vars = set(prev_map_vars)
1392
+ self._matrix_specs = dict(prev_matrix_specs)
1393
+
1394
+ is_method = (
1395
+ bool(getattr(fi, "is_udt_method", False))
1396
+ and fi.udt_type_name
1397
+ )
952
1398
 
953
1399
  # Determine param types and set context for type inference inside body
954
1400
  param_strs = []
955
1401
  self._current_func_param_types = {}
956
1402
  self._current_func_param_specs = {}
1403
+ param_hints = (getattr(node, "annotations", None) or {}).get(
1404
+ "param_type_hints", []
1405
+ )
1406
+ self._current_func_declared_param_names = {
1407
+ alias
1408
+ for i, param in enumerate(node.params)
1409
+ if i < len(param_hints) and param_hints[i]
1410
+ for alias in (param, self._safe_name(param))
1411
+ }
957
1412
  self._current_func_series_params = set()
1413
+ self._current_func_series_param_types = {}
958
1414
  self._udt_param_udt = {}
959
1415
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
1416
+ declared_param_specs = list(
1417
+ getattr(self.ctx, "func_declared_param_type_specs", {}).get(
1418
+ fi.name, ()
1419
+ )
1420
+ )
1421
+ variant_param_types = (
1422
+ getattr(self.ctx, "func_callsite_param_types", {}).get(
1423
+ (fi.name, call_site_idx), ()
1424
+ )
1425
+ if call_site_idx is not None
1426
+ else ()
1427
+ )
960
1428
  for i, p in enumerate(node.params):
961
1429
  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
1430
+ receiver_spec = None
1431
+ if is_method and i == 0:
1432
+ recv_spec = (
1433
+ fi.param_type_specs[i]
1434
+ if i < len(fi.param_type_specs)
1435
+ else None
1436
+ )
1437
+ receiver_spec = recv_spec
1438
+
1439
+ if (
1440
+ receiver_spec is not None
1441
+ and receiver_spec.kind == "primitive"
1442
+ and p in func_sv
1443
+ ):
1444
+ # A primitive receiver used with history is a Series boundary,
1445
+ # just like an ordinary history-bearing UDF parameter.
1446
+ elem_cpp_t = self._series_param_element_cpp_type(
1447
+ fi, i, call_site_idx
1448
+ )
1449
+ cpp_t = f"const Series<{elem_cpp_t}>&"
1450
+ spec = receiver_spec
1451
+ self._current_func_series_params.add(p)
1452
+ self._current_func_series_param_types[p] = elem_cpp_t
1453
+ self._current_func_series_param_types[
1454
+ self._safe_name(p)
1455
+ ] = elem_cpp_t
1456
+ elif is_method and i == 0 and fi.udt_type_name:
1457
+ # Receiver pass modes follow Pine's value/ID families. A user
1458
+ # UDT is itself a numeric object-ID handle, so pass it by value:
1459
+ # field mutation dereferences the same arena record while
1460
+ # ``self := other`` remains a local handle rebind.
1461
+ recv_spec = receiver_spec
1462
+ if recv_spec is None:
1463
+ recv_spec = self._type_spec_from_hint_name(
1464
+ fi.udt_type_name
1465
+ )
1466
+ spec = recv_spec
1467
+ if recv_spec is not None:
1468
+ cpp_t = self._type_spec_to_cpp(recv_spec)
1469
+ if (
1470
+ recv_spec.kind in {"array", "matrix"}
1471
+ or (
1472
+ recv_spec.kind == "udt"
1473
+ and recv_spec.name not in self._udt_defs
1474
+ )
1475
+ ):
1476
+ cpp_t = f"{cpp_t}&"
1477
+ if recv_spec.kind == "udt" and recv_spec.name:
1478
+ safe_p = self._safe_name(p)
1479
+ self._udt_param_udt[safe_p] = recv_spec.name
1480
+ self._udt_param_udt[p] = recv_spec.name
1481
+ else:
1482
+ # Compatibility for synthetic/legacy method records that
1483
+ # carry only the old receiver-name field.
1484
+ recv_cpp = DRAWING_TYPE_TO_CPP.get(
1485
+ fi.udt_type_name, fi.udt_type_name
1486
+ )
1487
+ cpp_t = (
1488
+ recv_cpp
1489
+ if fi.udt_type_name in self._udt_defs
1490
+ else f"{recv_cpp}&"
1491
+ )
1492
+ safe_p = self._safe_name(p)
1493
+ self._udt_param_udt[safe_p] = fi.udt_type_name
1494
+ self._udt_param_udt[p] = fi.udt_type_name
972
1495
  elif fi.name == "isInSession" and i < 2:
973
1496
  cpp_t = "std::string"
974
1497
  elif p in func_sv:
975
- # This param uses history access (e.g. src[1]) — pass as Series
976
- cpp_t = "const Series<double>&"
1498
+ # This param uses history access (e.g. src[1]) — preserve its
1499
+ # Pine scalar family instead of hard-coding Series<double>.
1500
+ elem_cpp_t = self._series_param_element_cpp_type(
1501
+ fi, i, call_site_idx
1502
+ )
1503
+ cpp_t = f"const Series<{elem_cpp_t}>&"
977
1504
  self._current_func_series_params.add(p)
1505
+ self._current_func_series_param_types[p] = elem_cpp_t
1506
+ self._current_func_series_param_types[
1507
+ self._safe_name(p)
1508
+ ] = elem_cpp_t
1509
+ if (
1510
+ i < len(getattr(fi, "param_type_specs", []))
1511
+ and fi.param_type_specs[i] is not None
1512
+ ):
1513
+ spec = fi.param_type_specs[i]
1514
+ elif (
1515
+ i >= len(declared_param_specs)
1516
+ or declared_param_specs[i] is None
1517
+ ) and i < len(variant_param_types) and variant_param_types[i] in {
1518
+ PineType.INT,
1519
+ PineType.FLOAT,
1520
+ PineType.BOOL,
1521
+ PineType.STRING,
1522
+ PineType.COLOR,
1523
+ }:
1524
+ # Non-history untyped parameters are polymorphic too. A pure
1525
+ # wrapper around a stateful helper is cloned per written call,
1526
+ # so its scalar boundary must use that clone's actual family
1527
+ # instead of the first call's shared FuncInfo inference.
1528
+ variant_pt = variant_param_types[i]
1529
+ cpp_t = {
1530
+ PineType.INT: "int64_t",
1531
+ PineType.FLOAT: "double",
1532
+ PineType.BOOL: "bool",
1533
+ PineType.STRING: "std::string",
1534
+ PineType.COLOR: "int",
1535
+ }[variant_pt]
978
1536
  elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
979
1537
  # 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).
1538
+ # ``pivot hi`` -> ``pivot`` numeric handle, ``line ln`` keeps its
1539
+ # established drawing-handle route, and an untyped ``s`` used as
1540
+ # a string -> ``std::string``. User UDT field mutation propagates
1541
+ # through the arena even though the handle parameter is by value.
984
1542
  spec = fi.param_type_specs[i]
985
1543
  cpp_t = self._type_spec_to_cpp(spec)
986
1544
  if spec.kind == "udt":
987
1545
  self._udt_param_udt[p] = spec.name
988
1546
  self._udt_param_udt[self._safe_name(p)] = spec.name
989
- cpp_t = f"{cpp_t}&"
1547
+ if spec.name not in self._udt_defs:
1548
+ cpp_t = f"{cpp_t}&"
990
1549
  elif spec.kind in ("array", "map"):
991
1550
  elem = spec.element if spec.kind == "array" else spec.value
992
1551
  if elem is not None and elem.kind == "udt":
993
1552
  self._udt_param_udt[p] = elem.name
994
1553
  self._udt_param_udt[self._safe_name(p)] = elem.name
995
- cpp_t = f"{cpp_t}&"
1554
+ if spec.kind == "array":
1555
+ cpp_t = f"{cpp_t}&"
996
1556
  elif i < len(fi.param_types):
997
1557
  pt = fi.param_types[i]
998
1558
  cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
@@ -1017,16 +1577,44 @@ class TopLevelEmitter:
1017
1577
  # A function returning a drawing handle must emit the C++ handle
1018
1578
  # struct (Line/Box/Label/Linefill), not the unknown lowercase name.
1019
1579
  ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
1580
+ elif self._func_int_return_uses_wide_history(
1581
+ fi, call_site_idx=call_site_idx
1582
+ ):
1583
+ # The history parameter is represented as int64_t so timestamp
1584
+ # values and their na sentinel cannot narrow at the return edge.
1585
+ ret_type = "int64_t"
1586
+ elif (
1587
+ call_site_idx is not None
1588
+ and self._callsite_callable_return_pine_type(
1589
+ fi, call_site_idx
1590
+ ) != PineType.UNKNOWN
1591
+ ):
1592
+ ret_type = PINE_TYPE_TO_CPP.get(
1593
+ self._callsite_callable_return_pine_type(
1594
+ fi, call_site_idx
1595
+ ),
1596
+ "double",
1597
+ )
1020
1598
  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.
1599
+ # Exact return TypeSpec (collections plus narrowly cached primitive
1600
+ # terminal reads) takes precedence over the coarse PineType slot.
1023
1601
  ret_type = self._type_spec_to_cpp(fi.return_type_spec)
1024
1602
  else:
1025
1603
  ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
1604
+ rhs_return_cpp_type = (
1605
+ ret_type
1606
+ if (self._is_nullable_collection_cpp_type(ret_type)
1607
+ or ret_type in DRAWING_TYPE_TO_CPP.values()
1608
+ or ret_type in self._udt_defs)
1609
+ else None
1610
+ )
1026
1611
 
1027
1612
  # 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
1613
+ func_name = (
1614
+ self._emit_udt_method_cpp_name(fi)
1615
+ if is_method
1616
+ else self._func_safe_name(fi.name)
1617
+ )
1030
1618
  if instance is not None:
1031
1619
  # Fresh context-sensitive instance: name + composed remaps come from
1032
1620
  # the instance record. No textual cs index (dispatch is via the
@@ -1035,10 +1623,8 @@ class TopLevelEmitter:
1035
1623
  self._active_ta_remap = instance["ta_remap"]
1036
1624
  self._active_var_remap = instance["var_remap"]
1037
1625
  self._active_fixnan_remap = instance.get("fixnan_remap", {})
1038
- self._in_ta_func_variant = True
1039
1626
  self._active_call_site_idx = None
1040
1627
  self._current_instance_name = instance["name"]
1041
- var_init_flag = f"_fvinit_{instance['name']}"
1042
1628
  elif call_site_idx is not None:
1043
1629
  func_name = f"{func_name}_cs{call_site_idx}"
1044
1630
  remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
@@ -1046,7 +1632,6 @@ class TopLevelEmitter:
1046
1632
  var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
1047
1633
  self._active_var_remap = var_remap
1048
1634
  self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
1049
- self._in_ta_func_variant = True
1050
1635
  self._active_call_site_idx = call_site_idx
1051
1636
  # Use the actual emitted name. Plain UDFs are unchanged; UDT
1052
1637
  # methods carry their `_udt_Type_method` prefix. This identity is
@@ -1057,26 +1642,62 @@ class TopLevelEmitter:
1057
1642
  self._active_ta_remap = {}
1058
1643
  self._active_var_remap = {}
1059
1644
  self._active_fixnan_remap = {}
1060
- self._in_ta_func_variant = False
1061
1645
  self._active_call_site_idx = None
1062
1646
  self._current_instance_name = None
1063
1647
 
1064
1648
  prev_func_locals = self._current_func_locals
1065
1649
  prev_func_local_types = self._current_func_local_types
1650
+ prev_lexical_drawing_types = self._lexical_drawing_types
1651
+ prev_lexical_udt_types = self._lexical_udt_types
1652
+ prev_lexical_series_bindings = self._lexical_series_bindings
1653
+ prev_known_var_tombstones = self._lexical_known_var_tombstones
1066
1654
  prev_func_body = getattr(self, "_current_func_body", None)
1067
1655
  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
1656
  self._current_func_body = node.body
1072
1657
  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
1658
  self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
1079
1659
  self._current_func_local_types = {}
1660
+ self._lexical_drawing_types = {
1661
+ param: DRAWING_TYPE_TO_CPP[drawing_name]
1662
+ for param in node.params
1663
+ for drawing_name in (
1664
+ (
1665
+ self._current_func_param_specs[param].name
1666
+ if (param in self._current_func_param_specs
1667
+ and self._current_func_param_specs[param].kind == "udt")
1668
+ else self._udt_param_udt.get(param)
1669
+ ),
1670
+ )
1671
+ if (drawing_name in DRAWING_TYPE_TO_CPP
1672
+ and self._current_func_param_types.get(
1673
+ param, ""
1674
+ ).removesuffix("&").removesuffix("*")
1675
+ == DRAWING_TYPE_TO_CPP[drawing_name])
1676
+ }
1677
+ self._lexical_udt_types = {
1678
+ param: (
1679
+ spec.name
1680
+ if spec is not None
1681
+ and spec.kind == "udt"
1682
+ and spec.name in self._udt_defs
1683
+ else (
1684
+ self._udt_param_udt.get(param)
1685
+ if (self._udt_param_udt.get(param) in self._udt_defs
1686
+ and self._current_func_param_types.get(
1687
+ param, ""
1688
+ ).removesuffix("&").removesuffix("*")
1689
+ == self._udt_param_udt.get(param))
1690
+ else None
1691
+ )
1692
+ )
1693
+ for param in node.params
1694
+ for spec in (self._current_func_param_specs.get(param),)
1695
+ }
1696
+ self._lexical_series_bindings = {
1697
+ param: param in self._current_func_series_params
1698
+ for param in node.params
1699
+ }
1700
+ self._lexical_known_var_tombstones = set(node.params)
1080
1701
  # Plain (non-persistent) scalar locals are emitted inline and live in
1081
1702
  # no other set; collect them so the unknown-identifier guard in
1082
1703
  # _visit_ident does not mistake them for undeclared symbols.
@@ -1084,12 +1705,6 @@ class TopLevelEmitter:
1084
1705
 
1085
1706
  lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
1086
1707
 
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
1708
  emitted_return = False
1094
1709
  if node.is_single_expr and node.body:
1095
1710
  expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
@@ -1098,7 +1713,10 @@ class TopLevelEmitter:
1098
1713
  # through to the default return.
1099
1714
  self._visit_stmt(node.body[0], lines, indent=2)
1100
1715
  elif expr:
1101
- lines.append(f" return {self._visit_expr(expr)};")
1716
+ lines.append(
1717
+ " return "
1718
+ f"{self._visit_rhs_value(expr, target_cpp_type=rhs_return_cpp_type)};"
1719
+ )
1102
1720
  emitted_return = True
1103
1721
  else:
1104
1722
  for i, s in enumerate(node.body):
@@ -1112,7 +1730,10 @@ class TopLevelEmitter:
1112
1730
  if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
1113
1731
  self._visit_stmt(s, lines, indent=2)
1114
1732
  else:
1115
- lines.append(f" return {self._visit_expr(s.expr)};")
1733
+ lines.append(
1734
+ " return "
1735
+ f"{self._visit_rhs_value(s.expr, target_cpp_type=rhs_return_cpp_type)};"
1736
+ )
1116
1737
  emitted_return = True
1117
1738
  elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
1118
1739
  # Switch/if as last statement = return expression in PineScript
@@ -1126,7 +1747,13 @@ class TopLevelEmitter:
1126
1747
  else:
1127
1748
  default_ret = self._default_for_type(ret_type)
1128
1749
  lines.append(f" {ret_type} _func_ret = {default_ret};")
1129
- self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
1750
+ self._visit_if_switch_expr(
1751
+ s,
1752
+ "_func_ret",
1753
+ lines,
1754
+ indent=2,
1755
+ target_cpp_type=rhs_return_cpp_type,
1756
+ )
1130
1757
  lines.append(f" return _func_ret;")
1131
1758
  emitted_return = True
1132
1759
  else:
@@ -1148,87 +1775,30 @@ class TopLevelEmitter:
1148
1775
  lines.append(" }")
1149
1776
  self._current_func_param_types = {}
1150
1777
  self._current_func_param_specs = {}
1778
+ self._current_func_declared_param_names = set()
1151
1779
  self._current_func_series_params = set()
1780
+ self._current_func_series_param_types = {}
1152
1781
  self._udt_param_udt = {}
1153
1782
  self._current_func_locals = prev_func_locals
1154
1783
  self._current_func_local_types = prev_func_local_types
1784
+ self._lexical_drawing_types = prev_lexical_drawing_types
1785
+ self._lexical_udt_types = prev_lexical_udt_types
1786
+ self._lexical_series_bindings = prev_lexical_series_bindings
1787
+ self._lexical_known_var_tombstones = prev_known_var_tombstones
1155
1788
  self._current_func_body = prev_func_body
1156
1789
  self._active_func_name = prev_func_name
1157
- self._udt_ptr_alias_locals = prev_ptr_alias
1790
+ self._current_func_collection_specs = prev_func_collection_specs
1791
+ self._current_func_collection_shadows = prev_func_collection_shadows
1792
+ self._collection_types = prev_collection_types
1793
+ self._array_vars = prev_array_vars
1794
+ self._map_vars = prev_map_vars
1795
+ self._matrix_specs = prev_matrix_specs
1158
1796
  self._active_ta_remap = {}
1159
1797
  self._active_var_remap = {}
1160
1798
  self._active_fixnan_remap = {}
1161
- self._in_ta_func_variant = False
1162
1799
  self._active_call_site_idx = None
1163
1800
  self._current_instance_name = None
1164
1801
 
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
- # A bare-``na`` initializer for an int/int64_t/bool ``var`` member
1220
- # must be typed (``na<int>()``), mirroring the class-scope ctor
1221
- # init (``_typed_na_init`` at _emit_constructor); a raw ``na<double>()``
1222
- # NaN stored into an int member is UB and defeats is_na<T>().
1223
- init_cpp = self._typed_na_init(init_cpp, name, ptype)
1224
- init_lines.append(f" {target} = {init_cpp};")
1225
- if not init_lines:
1226
- return
1227
- lines.append(f" if (!{flag}) {{")
1228
- lines.extend(init_lines)
1229
- lines.append(f" {flag} = true;")
1230
- lines.append(" }")
1231
-
1232
1802
  def _emit_precalculate_and_run(self, lines: list[str]) -> None:
1233
1803
  has_static_ta = any(
1234
1804
  self._ta_site_uses_precalc(site)
@@ -1246,7 +1816,7 @@ class TopLevelEmitter:
1246
1816
  continue
1247
1817
  if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1248
1818
  continue
1249
- if stmt.name in self.ctx.series_vars:
1819
+ if self._decl_binding_is_series(id(stmt), stmt.name):
1250
1820
  replayed_source_series.append(self._safe_name(stmt.name))
1251
1821
  replayed_source_series = sorted(set(replayed_source_series))
1252
1822
 
@@ -1268,7 +1838,7 @@ class TopLevelEmitter:
1268
1838
  if _ti in self._dead_ta_indices:
1269
1839
  continue
1270
1840
  if self._ta_site_uses_precalc(site):
1271
- resolved = [self._resolve_known(a) for a in site.ctor_args]
1841
+ resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1272
1842
  safe_resolved = []
1273
1843
  for r in resolved:
1274
1844
  safe_resolved.append(r if self._is_compile_time_value(r) else "1")
@@ -1354,7 +1924,7 @@ class TopLevelEmitter:
1354
1924
  # the normal per-bar path's ``{safe}.push({cpp_val})`` (see
1355
1925
  # ``_visit_var_decl``'s ``node.name in self.ctx.series_vars``
1356
1926
  # branch) — a plain ``=`` there is a compile error.
1357
- if stmt.name in self.ctx.series_vars:
1927
+ if self._decl_binding_is_series(id(stmt), stmt.name):
1358
1928
  lines.append(f' {safe}.push({cpp_val});')
1359
1929
  else:
1360
1930
  lines.append(f' {safe} = {cpp_val};')
@@ -1380,7 +1950,7 @@ class TopLevelEmitter:
1380
1950
  if _ti in self._dead_ta_indices:
1381
1951
  continue
1382
1952
  if self._ta_site_uses_precalc(site):
1383
- resolved = [self._resolve_known(a) for a in site.ctor_args]
1953
+ resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
1384
1954
  safe_resolved = []
1385
1955
  for r in resolved:
1386
1956
  safe_resolved.append(r if self._is_compile_time_value(r) else "1")