@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.
@@ -458,18 +458,154 @@ ORDER_DIRECTION_MAP = {
458
458
  # Array / Map / Matrix method dispatch
459
459
  # ---------------------------------------------------------------------------
460
460
 
461
+
462
+ def _checked_array_index_prelude(*, normalize_negative: bool = True) -> str:
463
+ """C++ body shared by checked single-index operations.
464
+
465
+ Pine v6 explicitly gives end-relative negative indices to the checked
466
+ ``get``, ``set``, and ``remove`` cluster. Other indexed functions such as
467
+ ``percentrank`` still need the same NA/non-finite/range checks, but must
468
+ reject negative indices instead of normalizing them. ``insert`` remains a
469
+ separately bounded residual.
470
+ """
471
+ checked_index = (
472
+ "__pf_raw_index<0?__pf_raw_index+__pf_array_size:__pf_raw_index"
473
+ if normalize_negative
474
+ else "__pf_raw_index"
475
+ )
476
+ return (
477
+ "using __pf_raw_index_type=std::decay_t<decltype(__pf_raw_index_value)>; "
478
+ "if constexpr(!std::is_same_v<__pf_raw_index_type,bool>) { "
479
+ "if(is_na(__pf_raw_index_value)) "
480
+ "pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
481
+ "std::to_string((int64_t)__pf_array.size())); } "
482
+ "if constexpr(std::is_floating_point_v<__pf_raw_index_type>) { "
483
+ "if(!std::isfinite(__pf_raw_index_value)) { "
484
+ "std::string __pf_raw_index_text=__pf_raw_index_value>0?\"inf\":\"-inf\"; "
485
+ "pine_runtime_error(std::string(\"Index \")+__pf_raw_index_text+"
486
+ "\" is out of bounds. Array size is \"+"
487
+ "std::to_string((int64_t)__pf_array.size())); } "
488
+ "long double __pf_raw_index_wide=(long double)__pf_raw_index_value; "
489
+ "if(__pf_raw_index_wide<(long double)std::numeric_limits<int64_t>::min()||"
490
+ "__pf_raw_index_wide>(long double)std::numeric_limits<int64_t>::max()) "
491
+ "pine_runtime_error(std::string(\"Index \")+"
492
+ "std::to_string((double)__pf_raw_index_value)+"
493
+ "\" is out of bounds. Array size is \"+"
494
+ "std::to_string((int64_t)__pf_array.size())); } "
495
+ "int64_t __pf_raw_index=(int64_t)__pf_raw_index_value; "
496
+ "int64_t __pf_array_size=(int64_t)__pf_array.size(); "
497
+ f"int64_t __pf_array_index={checked_index}; "
498
+ "if(__pf_array_index<0||__pf_array_index>=__pf_array_size) "
499
+ "pine_runtime_error(std::string(\"Index \")+std::to_string(__pf_raw_index)+"
500
+ "\" is out of bounds. Array size is \"+std::to_string(__pf_array_size)); "
501
+ )
502
+
503
+
504
+ def _checked_array_get(a: str, args: list[str]) -> str:
505
+ check = _checked_array_index_prelude()
506
+ return (
507
+ "[&](auto&& __pf_array)->decltype(auto){ "
508
+ "return [&](auto&& __pf_raw_index_value)->decltype(auto){ "
509
+ f"{check}"
510
+ "if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
511
+ "return (__pf_array[(size_t)__pf_array_index]); "
512
+ "else { using __pf_array_value_type=typename "
513
+ "std::decay_t<decltype(__pf_array)>::value_type; "
514
+ "return __pf_array_value_type(__pf_array[(size_t)__pf_array_index]); } "
515
+ f"}}(({args[0]})); }}(({a}))"
516
+ )
517
+
518
+
519
+ def _checked_array_set(a: str, args: list[str]) -> str:
520
+ check = _checked_array_index_prelude()
521
+ return (
522
+ "[&](auto&& __pf_array){ "
523
+ "return [&](auto&& __pf_raw_index_value){ "
524
+ "return [&](auto&& __pf_array_value){ "
525
+ f"{check}"
526
+ "__pf_array[(size_t)__pf_array_index]=__pf_array_value; "
527
+ f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
528
+ )
529
+
530
+
531
+ def _checked_array_remove(a: str, args: list[str]) -> str:
532
+ check = _checked_array_index_prelude()
533
+ return (
534
+ "[&](auto&& __pf_array){ "
535
+ "return [&](auto&& __pf_raw_index_value){ "
536
+ f"{check}"
537
+ "using __pf_array_value_type=typename "
538
+ "std::decay_t<decltype(__pf_array)>::value_type; "
539
+ "__pf_array_value_type __pf_array_value="
540
+ "__pf_array[(size_t)__pf_array_index]; "
541
+ "__pf_array.erase(__pf_array.begin()+(size_t)__pf_array_index); "
542
+ "return __pf_array_value; "
543
+ f"}}(({args[0]})); }}(({a}))"
544
+ )
545
+
546
+
547
+ def _checked_array_end_get(a: str, method: str) -> str:
548
+ access = "front" if method == "first" else "back"
549
+ return (
550
+ "[&](auto&& __pf_array)->decltype(auto){ "
551
+ f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
552
+ "if array is empty.\"); "
553
+ "if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
554
+ f"return (__pf_array.{access}()); "
555
+ "else { using __pf_array_value_type=typename "
556
+ "std::decay_t<decltype(__pf_array)>::value_type; "
557
+ f"return __pf_array_value_type(__pf_array.{access}()); }} "
558
+ f"}}(({a}))"
559
+ )
560
+
561
+
562
+ def _checked_array_end_remove(a: str, method: str) -> str:
563
+ access = "back" if method == "pop" else "front"
564
+ mutation = (
565
+ "__pf_array.pop_back();"
566
+ if method == "pop"
567
+ else "__pf_array.erase(__pf_array.begin());"
568
+ )
569
+ return (
570
+ "[&](auto&& __pf_array){ "
571
+ f"if(__pf_array.empty()) pine_runtime_error(\"Cannot use {method}() "
572
+ "if array is empty.\"); "
573
+ "using __pf_array_value_type=typename "
574
+ "std::decay_t<decltype(__pf_array)>::value_type; "
575
+ f"__pf_array_value_type __pf_array_value=__pf_array.{access}(); "
576
+ f"{mutation} return __pf_array_value; }}(({a}))"
577
+ )
578
+
579
+
580
+ def _checked_array_percentrank(a: str, args: list[str]) -> str:
581
+ """Preserve degenerate results, then reject invalid PercentRank indices."""
582
+ check = _checked_array_index_prelude(normalize_negative=False)
583
+ return (
584
+ "[&](auto&& __pf_array){ "
585
+ "return [&](auto&& __pf_raw_index_value){ "
586
+ "if(__pf_array.size()<=1) return na<double>(); "
587
+ f"{check}"
588
+ "double v=__pf_array[(size_t)__pf_array_index]; "
589
+ "if(std::isnan(v)) return na<double>(); "
590
+ "int le=0; for(auto x:__pf_array) "
591
+ "if(!std::isnan(x) && x<=v) le++; "
592
+ "return (double)(le-1)/(__pf_array.size()-1)*100.0; "
593
+ f"}}(({args[0]})); }}(({a}))"
594
+ )
595
+
596
+
461
597
  # Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
462
598
  ARRAY_METHODS = {
463
- "get": lambda a, args: f"{a}[({args[0]})]",
464
- "set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
599
+ "get": _checked_array_get,
600
+ "set": _checked_array_set,
465
601
  "push": lambda a, args: f"{a}.push_back({args[0]})",
466
602
  "unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
467
603
  "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()",
604
+ "pop": lambda a, args: _checked_array_end_remove(a, "pop"),
605
+ "shift": lambda a, args: _checked_array_end_remove(a, "shift"),
606
+ "remove": _checked_array_remove,
607
+ "first": lambda a, args: _checked_array_end_get(a, "first"),
608
+ "last": lambda a, args: _checked_array_end_get(a, "last"),
473
609
  "size": lambda a, args: f"(double){a}.size()",
474
610
  "clear": lambda a, args: f"{a}.clear()",
475
611
  "fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
@@ -485,56 +621,85 @@ ARRAY_METHODS = {
485
621
  "copy": lambda a, args: f"std::vector<double>({a})",
486
622
  "slice": lambda a, args: f"std::vector<double>({a}.begin()+(int)({args[0]}),{a}.begin()+(int)({args[1]}))",
487
623
  "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()))",
624
+ "sum": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0))",
625
+ "avg": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
626
+ "min": lambda a, args: f"({a}.empty()?na<double>():*std::min_element({a}.begin(),{a}.end()))",
627
+ "max": lambda a, args: f"({a}.empty()?na<double>():*std::max_element({a}.begin(),{a}.end()))",
628
+ "range": lambda a, args: f"({a}.empty()?na<double>():*std::max_element({a}.begin(),{a}.end())-*std::min_element({a}.begin(),{a}.end()))",
493
629
  "every": lambda a, args: f"std::all_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
494
630
  "some": lambda a, args: f"std::any_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
495
631
  # 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.
632
+ # biased=true → population (default), false → sample / n-1).
498
633
  "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); "
634
+ 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
635
  f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
501
636
  f"return _d>0?std::sqrt(s/_d):na<double>(); }}()"
502
637
  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()); }}()"
638
+ 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
639
  ),
505
640
  "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); "
641
+ 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
642
  f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
508
643
  f"return _d>0?s/_d:na<double>(); }}()"
509
644
  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(); }}()"
645
+ 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
646
  ),
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; }}()",
647
+ "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; }}()",
648
+ "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; }}()",
649
+ "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; }}()",
650
+ "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)]; }}()",
651
+ "percentrank": _checked_array_percentrank,
517
652
  "abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
518
653
  "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
654
  "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; }}()",
655
+ "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
656
  "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
657
  "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
658
  "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
659
  "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
660
  }
526
661
 
662
+ # Pine parameter order for the checked-index subset. Keeping the receiver
663
+ # (``id``) separate lets method syntax merge ``a.get(index = i)`` while the
664
+ # namespace-functional path merges ``array.get(id = a, index = i)``.
665
+ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
666
+ "get": ["index"],
667
+ "set": ["index", "value"],
668
+ "remove": ["index"],
669
+ "percentrank": ["index"],
670
+ "first": [],
671
+ "last": [],
672
+ "pop": [],
673
+ "shift": [],
674
+ }
675
+
527
676
  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()",
677
+ "put": lambda m, args: f"{m}.put({args[0]}, {args[1]})",
678
+ "get": lambda m, args: f"{m}.get({args[0]})",
679
+ "remove": lambda m, args: f"{m}.remove({args[0]})",
680
+ "contains": lambda m, args: f"{m}.contains({args[0]})",
681
+ "size": lambda m, args: f"{m}.size()",
533
682
  "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())",
683
+ "keys": lambda m, args: f"{m}.keys()",
684
+ "values": lambda m, args: f"{m}.values()",
685
+ "copy": lambda m, args: f"{m}.copy()",
686
+ "put_all": lambda m, args: f"{m}.put_all({args[0]})",
687
+ }
688
+
689
+ # Declared signature order after removing the method receiver ``id``. This is
690
+ # consumed by the typed-map UDF-parameter lane; established global/local and
691
+ # namespace map lowerings intentionally retain their existing output.
692
+ MAP_METHOD_KWARGS: dict[str, list[str]] = {
693
+ "put": ["key", "value"],
694
+ "get": ["key"],
695
+ "remove": ["key"],
696
+ "contains": ["key"],
697
+ "size": [],
698
+ "clear": [],
699
+ "keys": [],
700
+ "values": [],
701
+ "copy": [],
702
+ "put_all": ["id2"],
538
703
  }
539
704
 
540
705
 
@@ -566,6 +731,20 @@ MATRIX_METHOD_KWARGS: dict[str, list[str]] = {
566
731
  "add_col": ["col_index", "array_id"],
567
732
  }
568
733
 
734
+ # Matrix mutators whose established C++ lowering returns ``void``. A Pine
735
+ # callable may use one as its terminal expression, but its generated numeric
736
+ # fallback wrapper must emit the mutation as a statement and then return the
737
+ # default value; ``return matrix.set(...)`` is invalid C++.
738
+ #
739
+ # ``remove_row`` / ``remove_col`` are intentionally absent: their lowering in
740
+ # ``MATRIX_METHODS`` already wraps the runtime mutation in a lambda that returns
741
+ # a numeric sentinel. Matrix-returning transforms belong exclusively in
742
+ # ``MATRIX_RETURNING_METHODS`` below.
743
+ MATRIX_VOID_METHODS: frozenset[str] = frozenset({
744
+ "set", "fill", "add_row", "add_col", "swap_rows", "swap_columns",
745
+ "reshape", "reverse", "sort",
746
+ })
747
+
569
748
  # matrix.* method names whose RUNTIME return type is ``PineMatrix``. Used by
570
749
  # the codegen aggregate-type registration to declare the LHS variable as
571
750
  # ``PineMatrix`` instead of the analyzer's default ``double``. Without this
@@ -683,6 +862,17 @@ def _math_minmax_na_expr(func_name: str, args: list[str]) -> str:
683
862
  )
684
863
 
685
864
 
865
+ # Pine ``math.sign`` returns a float, propagates numeric ``na``, and must not
866
+ # evaluate its argument more than once. Keep the callable separate from the
867
+ # call template because the stable-runtime TA reset fallback also substitutes
868
+ # bare ``math.sign`` member text with this same C++ function expression.
869
+ _MATH_SIGN_CPP_FN = (
870
+ "([](const auto _pf_sign_v) -> double { "
871
+ "return is_na(_pf_sign_v) ? na<double>() "
872
+ ": (double)((_pf_sign_v > 0) - (_pf_sign_v < 0)); })"
873
+ )
874
+
875
+
686
876
  MATH_FUNC_MAP = {
687
877
  "abs": "std::abs", "max": "std::max", "min": "std::min",
688
878
  "ceil": "std::ceil", "floor": "std::floor", "round": "std::round",
@@ -691,7 +881,7 @@ MATH_FUNC_MAP = {
691
881
  "sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
692
882
  "asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
693
883
  "atan2": "std::atan2({0}, {1})",
694
- "sign": "((({0}) > 0) - (({0}) < 0))",
884
+ "sign": _MATH_SIGN_CPP_FN,
695
885
  "avg": "(({0} + {1}) / 2.0)",
696
886
  }
697
887
 
@@ -743,46 +933,3 @@ def _merge_kwargs(args: list, kwargs: dict, param_names: list | None, visit_expr
743
933
  while merged and merged[-1] is None:
744
934
  merged.pop()
745
935
  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]