@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.
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -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":
|
|
464
|
-
"set":
|
|
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":
|
|
468
|
-
"pop": lambda a, args:
|
|
469
|
-
"shift": lambda a, args:
|
|
470
|
-
"remove":
|
|
471
|
-
"first": lambda a, args:
|
|
472
|
-
"last": lambda a, args:
|
|
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
|
|
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:
|
|
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()?
|
|
490
|
-
"min": lambda a, args: f"
|
|
491
|
-
"max": lambda a, args: f"
|
|
492
|
-
"range": lambda a, args: f"(
|
|
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).
|
|
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":
|
|
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
|
|
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"
|
|
529
|
-
"get": lambda m, args: f"
|
|
530
|
-
"remove": lambda m, args: f"
|
|
531
|
-
"contains": lambda m, args: f"
|
|
532
|
-
"size": lambda m, args: f"
|
|
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"
|
|
535
|
-
"values": lambda m, args: f"
|
|
536
|
-
"copy": lambda m, args: f"
|
|
537
|
-
"put_all": lambda m, args: f"{m}.
|
|
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":
|
|
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]
|