@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.
@@ -458,22 +458,256 @@ ORDER_DIRECTION_MAP = {
458
458
  # Array / Map / Matrix method dispatch
459
459
  # ---------------------------------------------------------------------------
460
460
 
461
+
462
+ def _checked_array_index_prelude(
463
+ *,
464
+ normalize_negative: bool = True,
465
+ allow_size: bool = False,
466
+ name: str = "index",
467
+ ) -> str:
468
+ """C++ body shared by checked single-index operations.
469
+
470
+ Pine v6 explicitly gives end-relative negative indices to ``array.get``,
471
+ ``array.set``, ``array.insert``, and ``array.remove``. Other indexed
472
+ functions such as ``percentrank`` and the ``fill``/``slice`` range
473
+ endpoints still need the same NA/non-finite/range checks, but must reject
474
+ negative indices instead of normalizing them (``normalize_negative``).
475
+
476
+ ``allow_size`` widens the upper bound from ``index < size`` to
477
+ ``index <= size``. An *element access* is invalid at ``size``, but an
478
+ *insertion point* is not: ``array.insert(id, size, v)`` appends, and the
479
+ half-open ``[index_from, index_to)`` ranges of ``array.fill`` /
480
+ ``array.slice`` legally end at ``size``.
481
+
482
+ ``name`` renames every emitted local so two endpoints can be checked in
483
+ one C++ scope without colliding. ``name="index"`` reproduces the original
484
+ identifiers byte-for-byte.
485
+ """
486
+ raw_value = f"__pf_raw_{name}_value"
487
+ raw_type = f"__pf_raw_{name}_type"
488
+ raw_text = f"__pf_raw_{name}_text"
489
+ raw_wide = f"__pf_raw_{name}_wide"
490
+ raw = f"__pf_raw_{name}"
491
+ checked = f"__pf_array_{name}"
492
+ size = "__pf_array_size" if name == "index" else f"__pf_array_size_{name}"
493
+ checked_index = (
494
+ f"{raw}<0?{raw}+{size}:{raw}" if normalize_negative else raw
495
+ )
496
+ upper = ">" if allow_size else ">="
497
+ return (
498
+ f"using {raw_type}=std::decay_t<decltype({raw_value})>; "
499
+ f"if constexpr(!std::is_same_v<{raw_type},bool>) {{ "
500
+ f"if(is_na({raw_value})) "
501
+ "pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
502
+ "std::to_string((int64_t)__pf_array.size())); } "
503
+ f"if constexpr(std::is_floating_point_v<{raw_type}>) {{ "
504
+ f"if(!std::isfinite({raw_value})) {{ "
505
+ f"std::string {raw_text}={raw_value}>0?\"inf\":\"-inf\"; "
506
+ f"pine_runtime_error(std::string(\"Index \")+{raw_text}+"
507
+ "\" is out of bounds. Array size is \"+"
508
+ "std::to_string((int64_t)__pf_array.size())); } "
509
+ f"long double {raw_wide}=(long double){raw_value}; "
510
+ f"if({raw_wide}<(long double)std::numeric_limits<int64_t>::min()||"
511
+ f"{raw_wide}>(long double)std::numeric_limits<int64_t>::max()) "
512
+ "pine_runtime_error(std::string(\"Index \")+"
513
+ f"std::to_string((double){raw_value})+"
514
+ "\" is out of bounds. Array size is \"+"
515
+ "std::to_string((int64_t)__pf_array.size())); } "
516
+ f"int64_t {raw}=(int64_t){raw_value}; "
517
+ f"int64_t {size}=(int64_t)__pf_array.size(); "
518
+ f"int64_t {checked}={checked_index}; "
519
+ f"if({checked}<0||{checked}{upper}{size}) "
520
+ f"pine_runtime_error(std::string(\"Index \")+std::to_string({raw})+"
521
+ f"\" is out of bounds. Array size is \"+std::to_string({size})); "
522
+ )
523
+
524
+
525
+ def _checked_array_range_prelude() -> str:
526
+ """Validate the half-open ``[index_from, index_to)`` range of fill/slice.
527
+
528
+ Both endpoints are checked with ``allow_size`` (``index_to`` is exclusive,
529
+ so ``size`` is a legal endpoint) and without negative normalization
530
+ (neither function appears in the Pine v6 reference's negative-indexing
531
+ set). An inverted range is rejected rather than silently treated as empty:
532
+ ``begin()+from > begin()+to`` is undefined behaviour in the STL forms this
533
+ replaces, and no evidence pins TradingView's behaviour there.
534
+ """
535
+ return (
536
+ _checked_array_index_prelude(
537
+ normalize_negative=False, allow_size=True, name="index_from"
538
+ )
539
+ + _checked_array_index_prelude(
540
+ normalize_negative=False, allow_size=True, name="index_to"
541
+ )
542
+ + "if(__pf_array_index_from>__pf_array_index_to) "
543
+ "pine_runtime_error(std::string(\"Index range \")+"
544
+ "std::to_string(__pf_array_index_from)+\"..\"+"
545
+ "std::to_string(__pf_array_index_to)+"
546
+ "\" is invalid. Array size is \"+"
547
+ "std::to_string(__pf_array_size_index_from)); "
548
+ )
549
+
550
+
551
+ def _checked_array_insert(a: str, args: list[str]) -> str:
552
+ """``array.insert(id, index, value)`` — index is an INSERTION point.
553
+
554
+ ``index == size`` appends, so the bound is ``index <= size``; a negative
555
+ index is end-relative, exactly as for ``get``/``set``/``remove``.
556
+ """
557
+ check = _checked_array_index_prelude(allow_size=True)
558
+ return (
559
+ "[&](auto&& __pf_array){ "
560
+ "return [&](auto&& __pf_raw_index_value){ "
561
+ "return [&](auto&& __pf_array_value){ "
562
+ f"{check}"
563
+ "__pf_array.insert(__pf_array.begin()+(size_t)__pf_array_index, "
564
+ "__pf_array_value); "
565
+ f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
566
+ )
567
+
568
+
569
+ def _checked_array_fill_range(a: str, args: list[str]) -> str:
570
+ """``array.fill(id, value, index_from, index_to)`` — bounded range fill."""
571
+ check = _checked_array_range_prelude()
572
+ return (
573
+ "[&](auto&& __pf_array){ "
574
+ "return [&](auto&& __pf_array_value){ "
575
+ "return [&](auto&& __pf_raw_index_from_value){ "
576
+ "return [&](auto&& __pf_raw_index_to_value){ "
577
+ f"{check}"
578
+ "std::fill(__pf_array.begin()+(size_t)__pf_array_index_from, "
579
+ "__pf_array.begin()+(size_t)__pf_array_index_to, __pf_array_value); "
580
+ f"}}(({args[2]})); }}(({args[1]})); }}(({args[0]})); }}(({a}))"
581
+ )
582
+
583
+
584
+ def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
585
+ """``array.slice(id, index_from, index_to)`` — bounded range extraction.
586
+
587
+ ``result_type`` stays caller-supplied so the typed method lane keeps
588
+ emitting the receiver's own element type; only the bounds checks are new.
589
+ """
590
+ check = _checked_array_range_prelude()
591
+ return (
592
+ "[&](auto&& __pf_array){ "
593
+ "return [&](auto&& __pf_raw_index_from_value){ "
594
+ "return [&](auto&& __pf_raw_index_to_value){ "
595
+ f"{check}"
596
+ f"return {result_type}(__pf_array.begin()+(size_t)__pf_array_index_from, "
597
+ "__pf_array.begin()+(size_t)__pf_array_index_to); "
598
+ f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
599
+ )
600
+
601
+
602
+ def _checked_array_get(a: str, args: list[str]) -> str:
603
+ check = _checked_array_index_prelude()
604
+ return (
605
+ "[&](auto&& __pf_array)->decltype(auto){ "
606
+ "return [&](auto&& __pf_raw_index_value)->decltype(auto){ "
607
+ f"{check}"
608
+ "if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
609
+ "return (__pf_array[(size_t)__pf_array_index]); "
610
+ "else { using __pf_array_value_type=typename "
611
+ "std::decay_t<decltype(__pf_array)>::value_type; "
612
+ "return __pf_array_value_type(__pf_array[(size_t)__pf_array_index]); } "
613
+ f"}}(({args[0]})); }}(({a}))"
614
+ )
615
+
616
+
617
+ def _checked_array_set(a: str, args: list[str]) -> str:
618
+ check = _checked_array_index_prelude()
619
+ return (
620
+ "[&](auto&& __pf_array){ "
621
+ "return [&](auto&& __pf_raw_index_value){ "
622
+ "return [&](auto&& __pf_array_value){ "
623
+ f"{check}"
624
+ "__pf_array[(size_t)__pf_array_index]=__pf_array_value; "
625
+ f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
626
+ )
627
+
628
+
629
+ def _checked_array_remove(a: str, args: list[str]) -> str:
630
+ check = _checked_array_index_prelude()
631
+ return (
632
+ "[&](auto&& __pf_array){ "
633
+ "return [&](auto&& __pf_raw_index_value){ "
634
+ f"{check}"
635
+ "using __pf_array_value_type=typename "
636
+ "std::decay_t<decltype(__pf_array)>::value_type; "
637
+ "__pf_array_value_type __pf_array_value="
638
+ "__pf_array[(size_t)__pf_array_index]; "
639
+ "__pf_array.erase(__pf_array.begin()+(size_t)__pf_array_index); "
640
+ "return __pf_array_value; "
641
+ f"}}(({args[0]})); }}(({a}))"
642
+ )
643
+
644
+
645
+ def _checked_array_end_get(a: str, method: str) -> str:
646
+ access = "front" if method == "first" else "back"
647
+ return (
648
+ "[&](auto&& __pf_array)->decltype(auto){ "
649
+ f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
650
+ "if array is empty.\"); "
651
+ "if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
652
+ f"return (__pf_array.{access}()); "
653
+ "else { using __pf_array_value_type=typename "
654
+ "std::decay_t<decltype(__pf_array)>::value_type; "
655
+ f"return __pf_array_value_type(__pf_array.{access}()); }} "
656
+ f"}}(({a}))"
657
+ )
658
+
659
+
660
+ def _checked_array_end_remove(a: str, method: str) -> str:
661
+ access = "back" if method == "pop" else "front"
662
+ mutation = (
663
+ "__pf_array.pop_back();"
664
+ if method == "pop"
665
+ else "__pf_array.erase(__pf_array.begin());"
666
+ )
667
+ return (
668
+ "[&](auto&& __pf_array){ "
669
+ f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
670
+ "if array is empty.\"); "
671
+ "using __pf_array_value_type=typename "
672
+ "std::decay_t<decltype(__pf_array)>::value_type; "
673
+ f"__pf_array_value_type __pf_array_value=__pf_array.{access}(); "
674
+ f"{mutation} return __pf_array_value; }}(({a}))"
675
+ )
676
+
677
+
678
+ def _checked_array_percentrank(a: str, args: list[str]) -> str:
679
+ """Preserve degenerate results, then reject invalid PercentRank indices."""
680
+ check = _checked_array_index_prelude(normalize_negative=False)
681
+ return (
682
+ "[&](auto&& __pf_array){ "
683
+ "return [&](auto&& __pf_raw_index_value){ "
684
+ "if(__pf_array.size()<=1) return na<double>(); "
685
+ f"{check}"
686
+ "double v=__pf_array[(size_t)__pf_array_index]; "
687
+ "if(std::isnan(v)) return na<double>(); "
688
+ "int le=0; for(auto x:__pf_array) "
689
+ "if(!std::isnan(x) && x<=v) le++; "
690
+ "return (double)(le-1)/(__pf_array.size()-1)*100.0; "
691
+ f"}}(({args[0]})); }}(({a}))"
692
+ )
693
+
694
+
461
695
  # Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
462
696
  ARRAY_METHODS = {
463
- "get": lambda a, args: f"{a}[({args[0]})]",
464
- "set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
697
+ "get": _checked_array_get,
698
+ "set": _checked_array_set,
465
699
  "push": lambda a, args: f"{a}.push_back({args[0]})",
466
700
  "unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
467
- "insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
468
- "pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
469
- "shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
470
- "remove": lambda a, args: f"[&](){{ auto v={a}[({args[0]})]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
471
- "first": lambda a, args: f"{a}.front()",
472
- "last": lambda a, args: f"{a}.back()",
701
+ "insert": _checked_array_insert,
702
+ "pop": lambda a, args: _checked_array_end_remove(a, "pop"),
703
+ "shift": lambda a, args: _checked_array_end_remove(a, "shift"),
704
+ "remove": _checked_array_remove,
705
+ "first": lambda a, args: _checked_array_end_get(a, "first"),
706
+ "last": lambda a, args: _checked_array_end_get(a, "last"),
473
707
  "size": lambda a, args: f"(double){a}.size()",
474
708
  "clear": lambda a, args: f"{a}.clear()",
475
709
  "fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
476
- else f"std::fill({a}.begin()+(int)({args[1]}), {a}.begin()+(int)({args[2]}), {args[0]})",
710
+ else _checked_array_fill_range(a, args),
477
711
  "includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
478
712
  "indexof": lambda a, args: f"[&](){{ auto it=std::find({a}.begin(),{a}.end(),{args[0]}); return it!={a}.end()?(double)(it-{a}.begin()):-1.0; }}()",
479
713
  "lastindexof": lambda a, args: f"[&](){{ for(int i=(int){a}.size()-1;i>=0;i--)if({a}[i]=={args[0]})return(double)i; return -1.0; }}()",
@@ -483,58 +717,90 @@ ARRAY_METHODS = {
483
717
  ),
484
718
  "reverse": lambda a, args: f"std::reverse({a}.begin(),{a}.end())",
485
719
  "copy": lambda a, args: f"std::vector<double>({a})",
486
- "slice": lambda a, args: f"std::vector<double>({a}.begin()+(int)({args[0]}),{a}.begin()+(int)({args[1]}))",
720
+ "slice": lambda a, args: checked_array_slice(a, args, result_type="std::vector<double>"),
487
721
  "concat": lambda a, args: f"{a}.insert({a}.end(),{args[0]}.begin(),{args[0]}.end())",
488
- "sum": lambda a, args: f"std::accumulate({a}.begin(),{a}.end(),0.0)",
489
- "avg": lambda a, args: f"({a}.empty()?0.0:std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
490
- "min": lambda a, args: f"*std::min_element({a}.begin(),{a}.end())",
491
- "max": lambda a, args: f"*std::max_element({a}.begin(),{a}.end())",
492
- "range": lambda a, args: f"(*std::max_element({a}.begin(),{a}.end())-*std::min_element({a}.begin(),{a}.end()))",
722
+ "sum": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0))",
723
+ "avg": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
724
+ "min": lambda a, args: f"({a}.empty()?na<double>():*std::min_element({a}.begin(),{a}.end()))",
725
+ "max": lambda a, args: f"({a}.empty()?na<double>():*std::max_element({a}.begin(),{a}.end()))",
726
+ "range": lambda a, args: f"({a}.empty()?na<double>():*std::max_element({a}.begin(),{a}.end())-*std::min_element({a}.begin(),{a}.end()))",
493
727
  "every": lambda a, args: f"std::all_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
494
728
  "some": lambda a, args: f"std::any_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
495
729
  # stdev/variance honor the optional 2nd ``biased`` arg (Pine v6:
496
- # biased=true → population (default), false → sample / n-1). The no-arg
497
- # form keeps the original population emission byte-identical.
730
+ # biased=true → population (default), false → sample / n-1).
498
731
  "stdev": lambda a, args: (
499
- f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
732
+ f"[&](){{ if({a}.empty()) return na<double>(); double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
500
733
  f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
501
734
  f"return _d>0?std::sqrt(s/_d):na<double>(); }}()"
502
735
  if args else
503
- f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return std::sqrt(s/{a}.size()); }}()"
736
+ f"[&](){{ if({a}.empty()) return na<double>(); double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return std::sqrt(s/{a}.size()); }}()"
504
737
  ),
505
738
  "variance": lambda a, args: (
506
- f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
739
+ f"[&](){{ if({a}.empty()) return na<double>(); double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
507
740
  f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
508
741
  f"return _d>0?s/_d:na<double>(); }}()"
509
742
  if args else
510
- f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return s/{a}.size(); }}()"
743
+ f"[&](){{ if({a}.empty()) return na<double>(); double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return s/{a}.size(); }}()"
511
744
  ),
512
- "median": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int n=c.size(); return n%2?c[n/2]:(c[n/2-1]+c[n/2])/2.0; }}()",
513
- "mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
514
- "percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
515
- "percentile_nearest_rank": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return c[std::min(r-1,(int)c.size()-1)]; }}()",
516
- "percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[({args[0]})]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
745
+ "median": lambda a, args: f"[&](){{ if({a}.empty()) return na<double>(); auto c={a}; std::sort(c.begin(),c.end()); int n=c.size(); return n%2?c[n/2]:(c[n/2-1]+c[n/2])/2.0; }}()",
746
+ "mode": lambda a, args: f"[&](){{ if({a}.empty()) return na<double>(); std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
747
+ "percentile_linear_interpolation": lambda a, args: f"[&](){{ if({a}.empty()) return na<double>(); auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
748
+ "percentile_nearest_rank": lambda a, args: f"[&](){{ if({a}.empty()) return na<double>(); auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return (double)c[std::min(r-1,(int)c.size()-1)]; }}()",
749
+ "percentrank": _checked_array_percentrank,
517
750
  "abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
518
751
  "join": lambda a, args: "[&](){{ std::string r; for(size_t i=0;i<{arr}.size();i++){{ if(i>0)r+={sep}; r+=std::to_string({arr}[i]); }} return r; }}()".format(arr=a, sep=args[0] if args else 'std::string(",")'),
519
752
  "standardize": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); s=std::sqrt(s/{a}.size()); std::vector<double> r; for(auto v:{a})r.push_back(s==0?1.0:(v-m)/s); return r; }}()",
520
- "covariance": lambda a, args: f"[&](){{ auto&b={args[0]}; int n=std::min({a}.size(),b.size()); double ma=0,mb=0; for(int i=0;i<n;i++){{ma+={a}[i];mb+=b[i];}} ma/=n;mb/=n; double c=0; for(int i=0;i<n;i++)c+=({a}[i]-ma)*(b[i]-mb); return c/n; }}()",
753
+ "covariance": lambda a, args: f"[&](){{ auto&&b=({args[0]}); int n=std::min({a}.size(),b.size()); if(n<=0) return na<double>(); double ma=0,mb=0; for(int i=0;i<n;i++){{ma+={a}[i];mb+=b[i];}} ma/=n;mb/=n; double c=0; for(int i=0;i<n;i++)c+=({a}[i]-ma)*(b[i]-mb); return c/n; }}()",
521
754
  "binary_search": lambda a, args: f"[&](){{ auto it=std::lower_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.end()&&*it=={args[0]})?(double)(it-{a}.begin()):-1.0; }}()",
522
755
  "binary_search_leftmost": lambda a, args: f"[&](){{ auto it=std::lower_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.end()&&*it=={args[0]})?(double)(it-{a}.begin()):(double)(it-{a}.begin()-1); }}()",
523
756
  "binary_search_rightmost": lambda a, args: f"[&](){{ auto it=std::upper_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.begin()&&*(it-1)=={args[0]})?(double)(it-{a}.begin()-1):(double)(it-{a}.begin()); }}()",
524
757
  "sort_indices": lambda a, args: f"[&](){{ std::vector<double> idx({a}.size()); std::iota(idx.begin(),idx.end(),0); std::sort(idx.begin(),idx.end(),[&](int i,int j){{return {a}[i]<{a}[j];}}); return idx; }}()",
525
758
  }
526
759
 
760
+ # Pine parameter order for the checked-index subset. Keeping the receiver
761
+ # (``id``) separate lets method syntax merge ``a.get(index = i)`` while the
762
+ # namespace-functional path merges ``array.get(id = a, index = i)``.
763
+ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
764
+ "get": ["index"],
765
+ "set": ["index", "value"],
766
+ "remove": ["index"],
767
+ "percentrank": ["index"],
768
+ "insert": ["index", "value"],
769
+ "fill": ["value", "index_from", "index_to"],
770
+ "slice": ["index_from", "index_to"],
771
+ "first": [],
772
+ "last": [],
773
+ "pop": [],
774
+ "shift": [],
775
+ }
776
+
527
777
  MAP_METHODS = {
528
- "put": lambda m, args: f"({m}[{args[0]}] = {args[1]})",
529
- "get": lambda m, args: f"({m}.count({args[0]}) ? {m}[{args[0]}] : na<double>())",
530
- "remove": lambda m, args: f"[&](){{ auto it={m}.find({args[0]}); if(it!={m}.end()){{ auto v=it->second; {m}.erase(it); return v; }} return na<double>(); }}()",
531
- "contains": lambda m, args: f"({m}.count({args[0]}) > 0)",
532
- "size": lambda m, args: f"(double){m}.size()",
778
+ "put": lambda m, args: f"{m}.put({args[0]}, {args[1]})",
779
+ "get": lambda m, args: f"{m}.get({args[0]})",
780
+ "remove": lambda m, args: f"{m}.remove({args[0]})",
781
+ "contains": lambda m, args: f"{m}.contains({args[0]})",
782
+ "size": lambda m, args: f"{m}.size()",
533
783
  "clear": lambda m, args: f"{m}.clear()",
534
- "keys": lambda m, args: f"[&](){{ std::vector<std::string> v; for(auto& p:{m}) v.push_back(p.first); return v; }}()",
535
- "values": lambda m, args: f"[&](){{ std::vector<double> v; for(auto& p:{m}) v.push_back(p.second); return v; }}()",
536
- "copy": lambda m, args: f"std::unordered_map<std::string,double>({m})",
537
- "put_all": lambda m, args: f"{m}.insert({args[0]}.begin(), {args[0]}.end())",
784
+ "keys": lambda m, args: f"{m}.keys()",
785
+ "values": lambda m, args: f"{m}.values()",
786
+ "copy": lambda m, args: f"{m}.copy()",
787
+ "put_all": lambda m, args: f"{m}.put_all({args[0]})",
788
+ }
789
+
790
+ # Declared signature order after removing the method receiver ``id``. This is
791
+ # consumed by the typed-map UDF-parameter lane; established global/local and
792
+ # namespace map lowerings intentionally retain their existing output.
793
+ MAP_METHOD_KWARGS: dict[str, list[str]] = {
794
+ "put": ["key", "value"],
795
+ "get": ["key"],
796
+ "remove": ["key"],
797
+ "contains": ["key"],
798
+ "size": [],
799
+ "clear": [],
800
+ "keys": [],
801
+ "values": [],
802
+ "copy": [],
803
+ "put_all": ["id2"],
538
804
  }
539
805
 
540
806
 
@@ -566,6 +832,20 @@ MATRIX_METHOD_KWARGS: dict[str, list[str]] = {
566
832
  "add_col": ["col_index", "array_id"],
567
833
  }
568
834
 
835
+ # Matrix mutators whose established C++ lowering returns ``void``. A Pine
836
+ # callable may use one as its terminal expression, but its generated numeric
837
+ # fallback wrapper must emit the mutation as a statement and then return the
838
+ # default value; ``return matrix.set(...)`` is invalid C++.
839
+ #
840
+ # ``remove_row`` / ``remove_col`` are intentionally absent: their lowering in
841
+ # ``MATRIX_METHODS`` already wraps the runtime mutation in a lambda that returns
842
+ # a numeric sentinel. Matrix-returning transforms belong exclusively in
843
+ # ``MATRIX_RETURNING_METHODS`` below.
844
+ MATRIX_VOID_METHODS: frozenset[str] = frozenset({
845
+ "set", "fill", "add_row", "add_col", "swap_rows", "swap_columns",
846
+ "reshape", "reverse", "sort",
847
+ })
848
+
569
849
  # matrix.* method names whose RUNTIME return type is ``PineMatrix``. Used by
570
850
  # the codegen aggregate-type registration to declare the LHS variable as
571
851
  # ``PineMatrix`` instead of the analyzer's default ``double``. Without this
@@ -683,6 +963,17 @@ def _math_minmax_na_expr(func_name: str, args: list[str]) -> str:
683
963
  )
684
964
 
685
965
 
966
+ # Pine ``math.sign`` returns a float, propagates numeric ``na``, and must not
967
+ # evaluate its argument more than once. Keep the callable separate from the
968
+ # call template because the stable-runtime TA reset fallback also substitutes
969
+ # bare ``math.sign`` member text with this same C++ function expression.
970
+ _MATH_SIGN_CPP_FN = (
971
+ "([](const auto _pf_sign_v) -> double { "
972
+ "return is_na(_pf_sign_v) ? na<double>() "
973
+ ": (double)((_pf_sign_v > 0) - (_pf_sign_v < 0)); })"
974
+ )
975
+
976
+
686
977
  MATH_FUNC_MAP = {
687
978
  "abs": "std::abs", "max": "std::max", "min": "std::min",
688
979
  "ceil": "std::ceil", "floor": "std::floor", "round": "std::round",
@@ -691,7 +982,7 @@ MATH_FUNC_MAP = {
691
982
  "sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
692
983
  "asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
693
984
  "atan2": "std::atan2({0}, {1})",
694
- "sign": "((({0}) > 0) - (({0}) < 0))",
985
+ "sign": _MATH_SIGN_CPP_FN,
695
986
  "avg": "(({0} + {1}) / 2.0)",
696
987
  }
697
988
 
@@ -743,46 +1034,3 @@ def _merge_kwargs(args: list, kwargs: dict, param_names: list | None, visit_expr
743
1034
  while merged and merged[-1] is None:
744
1035
  merged.pop()
745
1036
  return [visit_expr(a) for a in merged if a is not None]
746
-
747
-
748
- def _merge_kwargs_with_defaults(
749
- args: list,
750
- kwargs: dict,
751
- param_names: list,
752
- param_defaults: list,
753
- visit_expr,
754
- ) -> list:
755
- """Like ``_merge_kwargs`` but fills missing slots from ``param_defaults``.
756
-
757
- ``param_defaults`` must be parallel to ``param_names`` (use ``None`` for
758
- parameters without a default). Used by the UDT-method call lowering so
759
- callers may invoke ``cfg.threshold()``, ``cfg.threshold(atrVal)``, or
760
- ``cfg.threshold(mult=2.0, base=rsiVal)`` against ``method threshold(Cfg
761
- self, float mult = 1.0, float base = 0.0) =>`` and have clang see the
762
- full positional argument list. PineScript has no overloading, so every
763
- parameter must be filled at the call site.
764
-
765
- Probe: data/validation/udt-method-probe-04-default-param.
766
-
767
- The result preserves the parameter order from ``param_names`` and
768
- contains ``visit_expr(node)`` for each filled slot. Trailing slots that
769
- have neither a caller-supplied value nor a default are dropped (matches
770
- ``_merge_kwargs`` behaviour for required-only signatures).
771
- """
772
- n = len(param_names)
773
- slots: list = [None] * n
774
- for i, a in enumerate(args):
775
- if i < n:
776
- slots[i] = a
777
- for k, v in kwargs.items():
778
- if k in param_names:
779
- slots[param_names.index(k)] = v
780
- # Fill remaining holes from defaults (only where the caller omitted the
781
- # slot AND the parameter has a declared default).
782
- if param_defaults:
783
- for i in range(n):
784
- if slots[i] is None and i < len(param_defaults) and param_defaults[i] is not None:
785
- slots[i] = param_defaults[i]
786
- while slots and slots[-1] is None:
787
- slots.pop()
788
- return [visit_expr(a) for a in slots if a is not None]