@pineforge/codegen-pyodide 1.2.0 → 1.4.0
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/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
|
@@ -194,13 +194,19 @@ class SecurityEmitter:
|
|
|
194
194
|
chart timeframe — its evaluator result is never read. Any other
|
|
195
195
|
timeframe registration cannot compute is refused: it used to register
|
|
196
196
|
the chart timeframe, silently.
|
|
197
|
+
|
|
198
|
+
Every timeframe string resolved from the script -- the literal, a
|
|
199
|
+
constant, a helper parameter's argument, a ternary or switch arm --
|
|
200
|
+
is a Pine timeframe (``_refuse_invalid_security_tf``).
|
|
197
201
|
"""
|
|
198
202
|
if isinstance(tf_node, StringLiteral):
|
|
203
|
+
self._refuse_invalid_security_tf(tf_node.value, tf_node)
|
|
199
204
|
return tf_node.value, None
|
|
200
205
|
if isinstance(tf_node, SwitchStmt):
|
|
201
206
|
# Keep diagnostics from the registration-time switch renderer
|
|
202
207
|
# visible; the broad expression fallback below intentionally
|
|
203
208
|
# catches ordinary unresolved expressions.
|
|
209
|
+
self._check_security_tf_arms(tf_node, set())
|
|
204
210
|
return None, self._security_tf_runtime_expr(tf_node)
|
|
205
211
|
if isinstance(tf_node, Identifier):
|
|
206
212
|
name = tf_node.name
|
|
@@ -209,6 +215,7 @@ class SecurityEmitter:
|
|
|
209
215
|
if (name in self._known_vars and name not in self._input_backed_vars
|
|
210
216
|
and not self._known_var_is_lexically_shadowed(name)
|
|
211
217
|
and isinstance(self._known_vars[name], str)):
|
|
218
|
+
self._refuse_invalid_security_tf(self._known_vars[name], tf_node)
|
|
212
219
|
return self._known_vars[name], None
|
|
213
220
|
if (name in self._input_backed_vars
|
|
214
221
|
and name in self._input_var_to_call
|
|
@@ -216,6 +223,7 @@ class SecurityEmitter:
|
|
|
216
223
|
return None, self._visit_expr(self._input_var_to_call[name])
|
|
217
224
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
218
225
|
if name in global_expr_map:
|
|
226
|
+
self._check_security_tf_arms(global_expr_map[name], {name})
|
|
219
227
|
expanded = self._security_tf_runtime_expr(
|
|
220
228
|
global_expr_map[name], resolving={name}
|
|
221
229
|
)
|
|
@@ -238,6 +246,7 @@ class SecurityEmitter:
|
|
|
238
246
|
self._security_tf_unresolved(tf_node, f"timeframe '{name}'")
|
|
239
247
|
# any other expression — visit if it resolves at class scope
|
|
240
248
|
try:
|
|
249
|
+
self._check_security_tf_arms(tf_node, set())
|
|
241
250
|
expanded = self._security_tf_runtime_expr(tf_node)
|
|
242
251
|
return None, expanded if expanded is not None else self._visit_expr(tf_node)
|
|
243
252
|
except CompileError:
|
|
@@ -245,6 +254,45 @@ class SecurityEmitter:
|
|
|
245
254
|
except Exception:
|
|
246
255
|
self._security_tf_unresolved(tf_node, "timeframe expression")
|
|
247
256
|
|
|
257
|
+
def _refuse_invalid_security_tf(self, value: str, node) -> None:
|
|
258
|
+
"""Refuse a timeframe string resolved from the script that is not a
|
|
259
|
+
Pine timeframe, with the support checker's diagnostic for the same
|
|
260
|
+
string written in the call (``invalid_tf_literal``): it reaches the
|
|
261
|
+
C++ registration. An empty string registers the chart's timeframe."""
|
|
262
|
+
if not value:
|
|
263
|
+
return
|
|
264
|
+
from ..support_checker import invalid_tf_literal
|
|
265
|
+
fn_label = ("request.security_lower_tf" if getattr(self, "_security_tf_lower", False)
|
|
266
|
+
else "request.security")
|
|
267
|
+
refusal = invalid_tf_literal(value, fn_label)
|
|
268
|
+
if refusal is not None:
|
|
269
|
+
message, hint = refusal
|
|
270
|
+
self._codegen_error(node, message, hint=hint)
|
|
271
|
+
|
|
272
|
+
def _check_security_tf_arms(self, node, resolving: set[str]) -> None:
|
|
273
|
+
"""Refuse an invalid timeframe string a registration-time timeframe
|
|
274
|
+
expression can select: the expression itself, a ternary or switch
|
|
275
|
+
arm, read through the constants and globals it names."""
|
|
276
|
+
def check(value) -> None:
|
|
277
|
+
if isinstance(value, StringLiteral):
|
|
278
|
+
self._refuse_invalid_security_tf(value.value, value)
|
|
279
|
+
elif isinstance(value, Ternary):
|
|
280
|
+
check(value.true_val)
|
|
281
|
+
check(value.false_val)
|
|
282
|
+
elif isinstance(value, SwitchStmt):
|
|
283
|
+
arms = [body for _case, body in value.cases]
|
|
284
|
+
for body in arms + ([value.default_body] if value.default_body else []):
|
|
285
|
+
if len(body) == 1 and isinstance(body[0], ExprStmt):
|
|
286
|
+
check(self._substitute_tf_input_reads(body[0].expr, resolving))
|
|
287
|
+
|
|
288
|
+
check(node if isinstance(node, SwitchStmt)
|
|
289
|
+
else self._substitute_tf_input_reads(node, resolving))
|
|
290
|
+
|
|
291
|
+
def _security_tf_literal(self, tf: str) -> str:
|
|
292
|
+
"""A timeframe string as the C++ string literal registration and the
|
|
293
|
+
evaluator's ``timeframe.*`` reads spell it."""
|
|
294
|
+
return f'"{self._cpp_string_escape(tf)}"'
|
|
295
|
+
|
|
248
296
|
def _security_tf_unresolved(self, tf_node, what: str) -> None:
|
|
249
297
|
"""Refuse a request.security timeframe registration cannot compute."""
|
|
250
298
|
self._codegen_error(
|
|
@@ -1981,8 +2029,9 @@ class SecurityEmitter:
|
|
|
1981
2029
|
parts = request.annotations[RECORDED_KEY_ANNOTATION]
|
|
1982
2030
|
tail = (f"|{parts['field']}|{parts['period']}|gaps_{parts['gaps']}"
|
|
1983
2031
|
f"|lookahead_{parts['lookahead']}")
|
|
1984
|
-
|
|
1985
|
-
|
|
2032
|
+
fn = self._cpp_string_escape(parts["fn"])
|
|
2033
|
+
return (f'(std::string("{fn}|") + {self._visit_expr(request.args[0])} + '
|
|
2034
|
+
f'std::string("{self._cpp_string_escape(tail)}"))')
|
|
1986
2035
|
|
|
1987
2036
|
def _recorded_sites(self) -> dict[int, int]:
|
|
1988
2037
|
"""Each recorded request's index N: ``_pf_recorded`` sets its
|
|
@@ -2606,7 +2655,7 @@ class SecurityEmitter:
|
|
|
2606
2655
|
|
|
2607
2656
|
def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
|
|
2608
2657
|
names = []
|
|
2609
|
-
for (sid, _node_id), meta in
|
|
2658
|
+
for (sid, _node_id), meta in self._security_expr_hist_by_node.items():
|
|
2610
2659
|
if sid == sec_id:
|
|
2611
2660
|
names.append(meta["name"])
|
|
2612
2661
|
return names
|
|
@@ -2709,7 +2758,7 @@ class SecurityEmitter:
|
|
|
2709
2758
|
"""C++ expression for the timeframe of a request.security evaluator."""
|
|
2710
2759
|
info = self._security_eval_info[sec_id]
|
|
2711
2760
|
if info.get("tf"):
|
|
2712
|
-
return
|
|
2761
|
+
return self._security_tf_literal(info["tf"])
|
|
2713
2762
|
if info.get("tf_expr"):
|
|
2714
2763
|
return info["tf_expr"]
|
|
2715
2764
|
return "input_tf_"
|
|
@@ -2775,7 +2824,7 @@ class SecurityEmitter:
|
|
|
2775
2824
|
if series_name in self._security_string_series
|
|
2776
2825
|
else "_security_helper_series_"
|
|
2777
2826
|
)
|
|
2778
|
-
return f'{store}["{series_name}"]'
|
|
2827
|
+
return f'{store}["{self._cpp_string_escape(series_name)}"]'
|
|
2779
2828
|
|
|
2780
2829
|
def _security_helper_var_state_type(self, stmt: VarDecl) -> str:
|
|
2781
2830
|
"""The type family of a helper ``var`` whose declaration reads
|
|
@@ -6377,7 +6426,7 @@ class SecurityEmitter:
|
|
|
6377
6426
|
if column is not None and self._security_foreign(sec_id):
|
|
6378
6427
|
# request.footprint(...) of another symbol: the delta its feed
|
|
6379
6428
|
# records for the requested bar (the value its delta() reads).
|
|
6380
|
-
return f'_pf_symbol_column({sec_id}, "{column}")'
|
|
6429
|
+
return f'_pf_symbol_column({sec_id}, "{self._cpp_string_escape(column)}")'
|
|
6381
6430
|
|
|
6382
6431
|
if resolving is None:
|
|
6383
6432
|
resolving = set()
|
|
@@ -7178,7 +7227,8 @@ class SecurityEmitter:
|
|
|
7178
7227
|
"request.security: a Heikin-Ashi request inside another request's "
|
|
7179
7228
|
"expression reads that request's Heikin-Ashi bars, which PineForge "
|
|
7180
7229
|
"does not build")
|
|
7181
|
-
|
|
7230
|
+
line = expr_node.loc.line if expr_node.loc else 1
|
|
7231
|
+
return (f'([&]() -> {cpp_t} {{ _PF_UNSUPPORTED_STOP("nested_heikinashi_request", {line}, "{message}"); '
|
|
7182
7232
|
f"return {na_value}; }}())")
|
|
7183
7233
|
|
|
7184
7234
|
site = self._get_ta_site(expr_node)
|
|
@@ -7837,8 +7887,9 @@ class SecurityEmitter:
|
|
|
7837
7887
|
def _security_variant_order_key(self, signature: tuple, binding_stack) -> tuple:
|
|
7838
7888
|
"""The order of a TA site's requested-context variants (``_v0``,
|
|
7839
7889
|
``_v1``, ...): where each argument binding's value is written in the
|
|
7840
|
-
source, then the signature
|
|
7841
|
-
|
|
7890
|
+
source, then the signature's stable scope snapshot, preserving
|
|
7891
|
+
traversal order at ties. The signature names a bound node by its
|
|
7892
|
+
``id()``, so ordering by its raw ``repr`` followed memory addresses, and
|
|
7842
7893
|
the same script could number its variants differently from run to
|
|
7843
7894
|
run or between CPython and Pyodide."""
|
|
7844
7895
|
frames = []
|
|
@@ -7860,4 +7911,9 @@ class SecurityEmitter:
|
|
|
7860
7911
|
frames.append(("arguments", tuple(items)))
|
|
7861
7912
|
else:
|
|
7862
7913
|
frames.append(("locals", tuple(sorted(frame.keys()))))
|
|
7863
|
-
|
|
7914
|
+
def stable_part(value):
|
|
7915
|
+
if isinstance(value, tuple):
|
|
7916
|
+
return tuple(stable_part(item) for item in value)
|
|
7917
|
+
return 0 if isinstance(value, int) else value
|
|
7918
|
+
|
|
7919
|
+
return tuple(frames), repr(stable_part(signature))
|
|
@@ -545,6 +545,18 @@ ORDER_DIRECTION_MAP = {
|
|
|
545
545
|
"descending": 'std::string("descending")',
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
NAME_ECHO_STRING_MEMBERS = {
|
|
549
|
+
"currency": frozenset({
|
|
550
|
+
"AED", "ARS", "AUD", "BDT", "BHD", "BRL", "BTC", "CAD", "CHF", "CLP",
|
|
551
|
+
"CNY", "COP", "CZK", "DKK", "EGP", "ETH", "EUR", "GBP", "HKD", "HUF",
|
|
552
|
+
"IDR", "ILS", "INR", "ISK", "JPY", "KES", "KRW", "KWD", "LKR", "MAD",
|
|
553
|
+
"MXN", "MYR", "NGN", "NOK", "NONE", "NZD", "PEN", "PHP", "PKR", "PLN",
|
|
554
|
+
"QAR", "RON", "RSD", "RUB", "SAR", "SEK", "SGD", "THB", "TND", "TRY",
|
|
555
|
+
"TWD", "USD", "USDT", "VES", "VND", "ZAR",
|
|
556
|
+
}),
|
|
557
|
+
"format": frozenset({"inherit", "price", "volume", "percent", "mintick"}),
|
|
558
|
+
}
|
|
559
|
+
|
|
548
560
|
|
|
549
561
|
# ---------------------------------------------------------------------------
|
|
550
562
|
# Array / Map / Matrix method dispatch
|
|
@@ -553,6 +565,7 @@ ORDER_DIRECTION_MAP = {
|
|
|
553
565
|
|
|
554
566
|
def _checked_array_index_prelude(
|
|
555
567
|
*,
|
|
568
|
+
method: str,
|
|
556
569
|
normalize_negative: bool = True,
|
|
557
570
|
allow_size: bool = False,
|
|
558
571
|
name: str = "index",
|
|
@@ -590,18 +603,18 @@ def _checked_array_index_prelude(
|
|
|
590
603
|
f"using {raw_type}=std::decay_t<decltype({raw_value})>; "
|
|
591
604
|
f"if constexpr(!std::is_same_v<{raw_type},bool>) {{ "
|
|
592
605
|
f"if(is_na({raw_value})) "
|
|
593
|
-
"
|
|
606
|
+
f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index na is out of bounds. Array size is ")+ '
|
|
594
607
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
595
608
|
f"if constexpr(std::is_floating_point_v<{raw_type}>) {{ "
|
|
596
609
|
f"if(!std::isfinite({raw_value})) {{ "
|
|
597
610
|
f"std::string {raw_text}={raw_value}>0?\"inf\":\"-inf\"; "
|
|
598
|
-
f"
|
|
611
|
+
f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+{raw_text}+'
|
|
599
612
|
"\" is out of bounds. Array size is \"+"
|
|
600
613
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
601
614
|
f"long double {raw_wide}=(long double){raw_value}; "
|
|
602
615
|
f"if({raw_wide}<(long double)std::numeric_limits<int64_t>::min()||"
|
|
603
616
|
f"{raw_wide}>(long double)std::numeric_limits<int64_t>::max()) "
|
|
604
|
-
"
|
|
617
|
+
f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+ '
|
|
605
618
|
f"std::to_string((double){raw_value})+"
|
|
606
619
|
"\" is out of bounds. Array size is \"+"
|
|
607
620
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
@@ -609,12 +622,12 @@ def _checked_array_index_prelude(
|
|
|
609
622
|
f"int64_t {size}=(int64_t)__pf_array.size(); "
|
|
610
623
|
f"int64_t {checked}={checked_index}; "
|
|
611
624
|
f"if({checked}<0||{checked}{upper}{size}) "
|
|
612
|
-
f"
|
|
625
|
+
f'_PF_ARRAY_STOP("index_out_of_bounds", "{method}", std::string("Index ")+std::to_string({raw})+'
|
|
613
626
|
f"\" is out of bounds. Array size is \"+std::to_string({size})); "
|
|
614
627
|
)
|
|
615
628
|
|
|
616
629
|
|
|
617
|
-
def _checked_array_range_prelude(*, reject_inverted: bool = True) -> str:
|
|
630
|
+
def _checked_array_range_prelude(*, method: str, reject_inverted: bool = True) -> str:
|
|
618
631
|
"""Validate the half-open ``[index_from, index_to)`` range of fill/slice.
|
|
619
632
|
|
|
620
633
|
Both endpoints are checked with ``allow_size`` (``index_to`` is exclusive,
|
|
@@ -625,14 +638,14 @@ def _checked_array_range_prelude(*, reject_inverted: bool = True) -> str:
|
|
|
625
638
|
"""
|
|
626
639
|
return (
|
|
627
640
|
_checked_array_index_prelude(
|
|
628
|
-
normalize_negative=False, allow_size=True, name="index_from"
|
|
641
|
+
method=method, normalize_negative=False, allow_size=True, name="index_from"
|
|
629
642
|
)
|
|
630
643
|
+ _checked_array_index_prelude(
|
|
631
|
-
normalize_negative=False, allow_size=True, name="index_to"
|
|
644
|
+
method=method, normalize_negative=False, allow_size=True, name="index_to"
|
|
632
645
|
)
|
|
633
646
|
+ (
|
|
634
647
|
"if(__pf_array_index_from>__pf_array_index_to) "
|
|
635
|
-
|
|
648
|
+
'_PF_ARRAY_STOP("slice_range_inverted", "slice", "Index \'from\' should be less than index \'to\'."); '
|
|
636
649
|
if reject_inverted else ""
|
|
637
650
|
)
|
|
638
651
|
)
|
|
@@ -644,7 +657,7 @@ def _checked_array_insert(a: str, args: list[str], normalize_negative: bool = Tr
|
|
|
644
657
|
``index == size`` appends, so the bound is ``index <= size``; a negative
|
|
645
658
|
index is end-relative, exactly as for ``get``/``set``/``remove``.
|
|
646
659
|
"""
|
|
647
|
-
check = _checked_array_index_prelude(allow_size=True,
|
|
660
|
+
check = _checked_array_index_prelude(method="insert", allow_size=True,
|
|
648
661
|
normalize_negative=normalize_negative)
|
|
649
662
|
return (
|
|
650
663
|
"[&](auto&& __pf_array){ "
|
|
@@ -659,7 +672,7 @@ def _checked_array_insert(a: str, args: list[str], normalize_negative: bool = Tr
|
|
|
659
672
|
|
|
660
673
|
def _checked_array_fill_range(a: str, args: list[str]) -> str:
|
|
661
674
|
"""``array.fill(id, value, index_from, index_to)`` — bounded range fill."""
|
|
662
|
-
check = _checked_array_range_prelude(reject_inverted=False)
|
|
675
|
+
check = _checked_array_range_prelude(method="fill", reject_inverted=False)
|
|
663
676
|
return (
|
|
664
677
|
"[&](auto&& __pf_array){ "
|
|
665
678
|
"return [&](auto&& __pf_array_value){ "
|
|
@@ -679,7 +692,7 @@ def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
|
|
|
679
692
|
``result_type`` stays caller-supplied so the typed method lane keeps
|
|
680
693
|
emitting the receiver's own element type; only the bounds checks are new.
|
|
681
694
|
"""
|
|
682
|
-
check = _checked_array_range_prelude()
|
|
695
|
+
check = _checked_array_range_prelude(method="slice")
|
|
683
696
|
return (
|
|
684
697
|
"[&](auto&& __pf_array){ "
|
|
685
698
|
"return [&](auto&& __pf_raw_index_from_value){ "
|
|
@@ -692,7 +705,7 @@ def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
|
|
|
692
705
|
|
|
693
706
|
|
|
694
707
|
def _checked_array_get(a: str, args: list[str], normalize_negative: bool = True) -> str:
|
|
695
|
-
check = _checked_array_index_prelude(normalize_negative=normalize_negative)
|
|
708
|
+
check = _checked_array_index_prelude(method="get", normalize_negative=normalize_negative)
|
|
696
709
|
return (
|
|
697
710
|
"[&](auto&& __pf_array)->decltype(auto){ "
|
|
698
711
|
"return [&](auto&& __pf_raw_index_value)->decltype(auto){ "
|
|
@@ -707,7 +720,7 @@ def _checked_array_get(a: str, args: list[str], normalize_negative: bool = True)
|
|
|
707
720
|
|
|
708
721
|
|
|
709
722
|
def _checked_array_set(a: str, args: list[str], normalize_negative: bool = True) -> str:
|
|
710
|
-
check = _checked_array_index_prelude(normalize_negative=normalize_negative)
|
|
723
|
+
check = _checked_array_index_prelude(method="set", normalize_negative=normalize_negative)
|
|
711
724
|
return (
|
|
712
725
|
"[&](auto&& __pf_array){ "
|
|
713
726
|
"return [&](auto&& __pf_raw_index_value){ "
|
|
@@ -719,7 +732,7 @@ def _checked_array_set(a: str, args: list[str], normalize_negative: bool = True)
|
|
|
719
732
|
|
|
720
733
|
|
|
721
734
|
def _checked_array_remove(a: str, args: list[str], normalize_negative: bool = True) -> str:
|
|
722
|
-
check = _checked_array_index_prelude(normalize_negative=normalize_negative)
|
|
735
|
+
check = _checked_array_index_prelude(method="remove", normalize_negative=normalize_negative)
|
|
723
736
|
return (
|
|
724
737
|
"[&](auto&& __pf_array){ "
|
|
725
738
|
"return [&](auto&& __pf_raw_index_value){ "
|
|
@@ -738,7 +751,7 @@ def _checked_array_end_get(a: str, method: str) -> str:
|
|
|
738
751
|
access = "front" if method == "first" else "back"
|
|
739
752
|
return (
|
|
740
753
|
"[&](auto&& __pf_array)->decltype(auto){ "
|
|
741
|
-
f
|
|
754
|
+
f'if(__pf_array.empty()) _PF_ARRAY_STOP("empty_array_access", "{method}", "Cannot use {method}() '
|
|
742
755
|
"if array is empty.\"); "
|
|
743
756
|
"if constexpr(std::is_lvalue_reference_v<decltype(__pf_array)>) "
|
|
744
757
|
f"return (__pf_array.{access}()); "
|
|
@@ -758,7 +771,7 @@ def _checked_array_end_remove(a: str, method: str) -> str:
|
|
|
758
771
|
)
|
|
759
772
|
return (
|
|
760
773
|
"[&](auto&& __pf_array){ "
|
|
761
|
-
f
|
|
774
|
+
f'if(__pf_array.empty()) _PF_ARRAY_STOP("empty_array_access", "{method}", "Cannot use {method}() '
|
|
762
775
|
"if array is empty.\"); "
|
|
763
776
|
"using __pf_array_value_type=typename "
|
|
764
777
|
"std::decay_t<decltype(__pf_array)>::value_type; "
|
|
@@ -769,7 +782,7 @@ def _checked_array_end_remove(a: str, method: str) -> str:
|
|
|
769
782
|
|
|
770
783
|
def _checked_array_percentrank(a: str, args: list[str]) -> str:
|
|
771
784
|
"""Preserve degenerate results, then reject invalid PercentRank indices."""
|
|
772
|
-
check = _checked_array_index_prelude(normalize_negative=False)
|
|
785
|
+
check = _checked_array_index_prelude(method="percentrank", normalize_negative=False)
|
|
773
786
|
return (
|
|
774
787
|
"[&](auto&& __pf_array){ "
|
|
775
788
|
"return [&](auto&& __pf_raw_index_value){ "
|
|
@@ -810,7 +823,7 @@ ARRAY_METHODS = {
|
|
|
810
823
|
"size": lambda a, args: f"(double){a}.size()",
|
|
811
824
|
"clear": lambda a, args: f"{a}.clear()",
|
|
812
825
|
"fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
|
|
813
|
-
else _checked_array_fill_range(a, args),
|
|
826
|
+
else _checked_array_fill_range(a, args + [f"(int){a}.size()"] if len(args) == 2 else args),
|
|
814
827
|
"includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
|
|
815
828
|
"indexof": lambda a, args: f"[&](){{ auto __pf_it=std::find({a}.begin(),{a}.end(),{args[0]}); return __pf_it!={a}.end()?(double)(__pf_it-{a}.begin()):-1.0; }}()",
|
|
816
829
|
"lastindexof": lambda a, args: f"[&](){{ for(int __pf_i=(int){a}.size()-1;__pf_i>=0;__pf_i--)if({a}[__pf_i]=={args[0]})return(double)__pf_i; return -1.0; }}()",
|
|
@@ -888,6 +901,9 @@ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
|
|
|
888
901
|
"last": [],
|
|
889
902
|
"pop": [],
|
|
890
903
|
"shift": [],
|
|
904
|
+
"sort": ["order"],
|
|
905
|
+
"indexof": ["value"],
|
|
906
|
+
"unshift": ["value"],
|
|
891
907
|
}
|
|
892
908
|
|
|
893
909
|
MAP_METHODS = {
|
|
@@ -942,10 +958,109 @@ def _matrix_add_col(m: str, args: list) -> str:
|
|
|
942
958
|
raise IndexError("matrix.add_col")
|
|
943
959
|
|
|
944
960
|
|
|
945
|
-
|
|
961
|
+
def _matrix_sum(receiver: str, arguments: list[str]) -> str:
|
|
962
|
+
"""Use the runtime's exposed Eigen arithmetic for matrix/scalar sums."""
|
|
963
|
+
if not arguments:
|
|
964
|
+
return f"{receiver}.sum()"
|
|
965
|
+
if len(arguments) != 1:
|
|
966
|
+
raise IndexError("matrix.sum")
|
|
967
|
+
return (
|
|
968
|
+
"([](const auto& _pf_sum_left, const auto& _pf_sum_right) { "
|
|
969
|
+
"auto _pf_sum_result = _pf_sum_left.copy(); "
|
|
970
|
+
"if constexpr (std::is_arithmetic_v<std::decay_t<decltype(_pf_sum_right)>>) { "
|
|
971
|
+
"_pf_sum_result.data().array() += _pf_sum_right; "
|
|
972
|
+
"} else { "
|
|
973
|
+
"if (_pf_sum_left.rows() != _pf_sum_right.rows() || "
|
|
974
|
+
"_pf_sum_left.columns() != _pf_sum_right.columns()) "
|
|
975
|
+
'throw std::runtime_error("Cannot sum matrices with different dimensions."); '
|
|
976
|
+
"_pf_sum_result.data() += _pf_sum_right.data(); "
|
|
977
|
+
"} return _pf_sum_result; "
|
|
978
|
+
f"}}({receiver}, {arguments[0]}))"
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _matrix_submatrix(receiver: str, arguments: list[str]) -> str:
|
|
983
|
+
if len(arguments) > 4:
|
|
984
|
+
raise IndexError("matrix.submatrix")
|
|
985
|
+
if len(arguments) == 4 and all(argument is not None for argument in arguments):
|
|
986
|
+
return f"{receiver}.submatrix({', '.join(_matrix_int_arg(argument) for argument in arguments)})"
|
|
987
|
+
defaults = ["0", "_pf_matrix_target.rows()", "0", "_pf_matrix_target.columns()"]
|
|
988
|
+
arguments = arguments + [None] * (4 - len(arguments))
|
|
989
|
+
bounds = [argument if argument is not None else defaults[index]
|
|
990
|
+
for index, argument in enumerate(arguments)]
|
|
991
|
+
return ("[&](auto&& _pf_matrix_target) { return _pf_matrix_target.submatrix("
|
|
992
|
+
f"{', '.join(_matrix_int_arg(argument) for argument in bounds)}); }}({receiver})")
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
# Parameter order of the matrix methods (Pine v6), receiver excluded: the
|
|
996
|
+
# slots ``CallVisitor._collection_arg_nodes`` binds keyword arguments to. A
|
|
997
|
+
# method absent here takes only its receiver. ``fill`` names its range
|
|
998
|
+
# parameters so that a keyword one binds as a positional one does: the
|
|
999
|
+
# lowering reads only ``value`` (the engine fills the whole matrix), a
|
|
1000
|
+
# pre-existing approximation of both spellings that the call does not refuse.
|
|
946
1001
|
MATRIX_METHOD_KWARGS: dict[str, list[str]] = {
|
|
947
|
-
"add_row": ["
|
|
948
|
-
"add_col": ["
|
|
1002
|
+
"add_row": ["row", "array_id"],
|
|
1003
|
+
"add_col": ["column", "array_id"],
|
|
1004
|
+
"sort": ["column", "order"],
|
|
1005
|
+
"submatrix": ["from_row", "to_row", "from_column", "to_column"],
|
|
1006
|
+
"sum": ["id2"],
|
|
1007
|
+
"diff": ["id2"],
|
|
1008
|
+
"mult": ["id2"],
|
|
1009
|
+
"get": ["row", "column"],
|
|
1010
|
+
"set": ["row", "column", "value"],
|
|
1011
|
+
"fill": ["value", "from_row", "to_row", "from_column", "to_column"],
|
|
1012
|
+
"row": ["row"],
|
|
1013
|
+
"col": ["column"],
|
|
1014
|
+
"remove_row": ["row"],
|
|
1015
|
+
"remove_col": ["column"],
|
|
1016
|
+
"swap_rows": ["row1", "row2"],
|
|
1017
|
+
"swap_columns": ["column1", "column2"],
|
|
1018
|
+
"reshape": ["rows", "columns"],
|
|
1019
|
+
"concat": ["id2", "vertical"],
|
|
1020
|
+
"pow": ["power"],
|
|
1021
|
+
"kron": ["id2"],
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
# Keyword spellings an earlier build bound that Pine's reference does not
|
|
1025
|
+
# name; they keep binding to the same slot.
|
|
1026
|
+
MATRIX_METHOD_KWARG_ALIASES: dict[str, dict[str, str]] = {
|
|
1027
|
+
"add_row": {"row_index": "row"},
|
|
1028
|
+
"add_col": {"col_index": "column"},
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
# The receiver's own keyword in the namespace form (``matrix.diff(id1=m, ...)``).
|
|
1032
|
+
MATRIX_RECEIVER_KEYWORD: dict[str, str] = {
|
|
1033
|
+
"sum": "id1", "diff": "id1", "mult": "id1", "concat": "id1", "kron": "id1",
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _matrix_functional_receiver(method: str, node):
|
|
1038
|
+
"""The receiver node of ``matrix.<method>(...)``: its first positional
|
|
1039
|
+
argument, else its keyword."""
|
|
1040
|
+
return node.args[0] if node.args else node.kwargs.get(
|
|
1041
|
+
MATRIX_RECEIVER_KEYWORD.get(method, "id"))
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
# The optional parameters of the collection methods whose keywords bind by
|
|
1045
|
+
# position, and what a gap left before one stands for when only a later
|
|
1046
|
+
# keyword is written (``m.sort(order = order.descending)`` leaves ``column``
|
|
1047
|
+
# open): a number is Pine's default, ``None`` an omitted argument the method's
|
|
1048
|
+
# template defaults from the receiver's own extent (``_matrix_submatrix``'s
|
|
1049
|
+
# ``to_row`` / ``to_column``, the defaulted ``add_row`` / ``add_col`` of
|
|
1050
|
+
# ``TypeInferer._matrix_method_expr``). A parameter that is not listed is
|
|
1051
|
+
# required: a gap before it is refused.
|
|
1052
|
+
MATRIX_OPTIONAL_PARAMS: dict[str, dict[str, int | None]] = {
|
|
1053
|
+
"sort": {"column": 0, "order": None},
|
|
1054
|
+
"submatrix": {"from_row": 0, "to_row": None, "from_column": 0, "to_column": None},
|
|
1055
|
+
"add_row": {"row": None, "array_id": None},
|
|
1056
|
+
"add_col": {"column": None, "array_id": None},
|
|
1057
|
+
"fill": {"from_row": None, "to_row": None, "from_column": None, "to_column": None},
|
|
1058
|
+
"sum": {"id2": None},
|
|
1059
|
+
"concat": {"vertical": None},
|
|
1060
|
+
}
|
|
1061
|
+
ARRAY_OPTIONAL_PARAMS: dict[str, dict[str, int | None]] = {
|
|
1062
|
+
"fill": {"index_from": 0, "index_to": None},
|
|
1063
|
+
"sort": {"order": None},
|
|
949
1064
|
}
|
|
950
1065
|
|
|
951
1066
|
# Matrix mutators whose established C++ lowering returns ``void``. A Pine
|
|
@@ -1012,17 +1127,17 @@ MATRIX_METHODS = {
|
|
|
1012
1127
|
"swap_rows": lambda m, args: f"{m}.swap_rows({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
|
|
1013
1128
|
"swap_columns": lambda m, args: f"{m}.swap_columns({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
|
|
1014
1129
|
"copy": lambda m, args: f"{m}.copy()",
|
|
1015
|
-
"submatrix":
|
|
1130
|
+
"submatrix": _matrix_submatrix,
|
|
1016
1131
|
"reshape": lambda m, args: f"{m}.reshape({_matrix_int_arg(args[0])}, {_matrix_int_arg(args[1])})",
|
|
1017
1132
|
"reverse": lambda m, args: f"{m}.reverse()",
|
|
1018
1133
|
"transpose": lambda m, args: f"{m}.transpose()",
|
|
1019
|
-
"sort": lambda m, args: f"{m}.sort({_matrix_int_arg(args[0])}, {args[1]} != \"descending\")" if len(args)>1 else f"{m}.sort({_matrix_int_arg(args[0])})",
|
|
1134
|
+
"sort": lambda m, args: f"{m}.sort({_matrix_int_arg(args[0])}, {args[1]} != \"descending\")" if len(args)>1 else f"{m}.sort({_matrix_int_arg(args[0])})" if args else f"{m}.sort(0)",
|
|
1020
1135
|
"concat": lambda m, args: f"{m}.concat({args[0]}, {pine_truth_cast(args[1])})" if len(args)>1 else f"{m}.concat({args[0]}, true)",
|
|
1021
1136
|
"avg": lambda m, args: f"{m}.avg()",
|
|
1022
1137
|
"min": lambda m, args: f"{m}.min()",
|
|
1023
1138
|
"max": lambda m, args: f"{m}.max()",
|
|
1024
1139
|
"mode": lambda m, args: f"{m}.mode()",
|
|
1025
|
-
"sum":
|
|
1140
|
+
"sum": _matrix_sum,
|
|
1026
1141
|
"diff": lambda m, args: f"{m}.diff({args[0]})",
|
|
1027
1142
|
"mult": lambda m, args: f"{m}.mult({args[0]})",
|
|
1028
1143
|
"pow": lambda m, args: f"{m}.pow({_matrix_int_arg(args[0])})",
|
|
@@ -1116,10 +1231,11 @@ MATH_FUNC_MAP = {
|
|
|
1116
1231
|
STR_FUNC_MAP = {
|
|
1117
1232
|
"tostring": None, # handled separately (already works)
|
|
1118
1233
|
"tonumber": lambda args: (
|
|
1119
|
-
f"[&](){{ "
|
|
1120
|
-
f"try {{ return std::stod(
|
|
1121
|
-
f"catch (
|
|
1122
|
-
f"}}
|
|
1234
|
+
f"[&](std::string _pf_number_text){{ "
|
|
1235
|
+
f"try {{ return std::stod(_pf_number_text); }} "
|
|
1236
|
+
f"catch (const std::invalid_argument&) {{ return na<double>(); }} "
|
|
1237
|
+
f"catch (const std::out_of_range&) {{ return na<double>(); }} "
|
|
1238
|
+
f"}}(({args[0]}))"
|
|
1123
1239
|
),
|
|
1124
1240
|
"length": lambda args: f"(int){args[0]}.length()",
|
|
1125
1241
|
"contains": lambda args: f"({args[0]}.find({args[1]}) != std::string::npos)",
|
|
@@ -60,7 +60,7 @@ static std::string _pf_tv_decimal(double value, int min_fraction,
|
|
|
60
60
|
char buffer[128];
|
|
61
61
|
const auto converted = std::to_chars(buffer, buffer + sizeof buffer, value);
|
|
62
62
|
if (converted.ec != std::errc{})
|
|
63
|
-
|
|
63
|
+
_PF_ENGINE_INVARIANT("shortest-decimal conversion failed", std::runtime_error);
|
|
64
64
|
std::string spelling(buffer, converted.ptr);
|
|
65
65
|
const bool negative = !spelling.empty() && spelling[0] == '-';
|
|
66
66
|
if (negative) spelling.erase(0, 1);
|
|
@@ -240,7 +240,14 @@ static std::string pine_str_format_tv(
|
|
|
240
240
|
i = end + 1;
|
|
241
241
|
continue;
|
|
242
242
|
}
|
|
243
|
-
|
|
243
|
+
size_t index = 0;
|
|
244
|
+
for (char _pf_digit : index_text) {
|
|
245
|
+
const size_t _pf_value = static_cast<size_t>(_pf_digit - '0');
|
|
246
|
+
if (index > (std::numeric_limits<size_t>::max() - _pf_value) / 10) {
|
|
247
|
+
_PF_STRING_STOP("format_index_overflow", "stoul");
|
|
248
|
+
}
|
|
249
|
+
index = index * 10 + _pf_value;
|
|
250
|
+
}
|
|
244
251
|
if (index >= args.size()) {
|
|
245
252
|
result += format_string.substr(i, end - i + 1);
|
|
246
253
|
i = end + 1;
|