@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
|
@@ -73,6 +73,7 @@ classes from ``..ast_nodes``.
|
|
|
73
73
|
|
|
74
74
|
from __future__ import annotations
|
|
75
75
|
|
|
76
|
+
from ..errors import Phase
|
|
76
77
|
from ..ast_nodes import (
|
|
77
78
|
ASTNode,
|
|
78
79
|
Assignment,
|
|
@@ -99,6 +100,7 @@ from ..ast_nodes import (
|
|
|
99
100
|
WhileStmt,
|
|
100
101
|
)
|
|
101
102
|
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
103
|
+
from ..external_requests import UNPINNED_ANNOTATION
|
|
102
104
|
from .tables import (
|
|
103
105
|
ARRAY_NEW_CTORS,
|
|
104
106
|
DRAWING_TYPE_TO_CPP,
|
|
@@ -123,6 +125,10 @@ class StmtVisitor:
|
|
|
123
125
|
# ------------------------------------------------------------------
|
|
124
126
|
|
|
125
127
|
def _visit_stmt(self, node: ASTNode, lines: list[str], indent: int) -> None:
|
|
128
|
+
if self._budget is not None:
|
|
129
|
+
self._budget_visit_count += 1
|
|
130
|
+
if self._budget_visit_count % 128 == 0:
|
|
131
|
+
self._budget.check(node.loc, Phase.CODEGEN)
|
|
126
132
|
pad = " " * indent
|
|
127
133
|
|
|
128
134
|
if isinstance(node, StrategyDecl):
|
|
@@ -346,35 +352,44 @@ class StmtVisitor:
|
|
|
346
352
|
and node.expr.args):
|
|
347
353
|
risk_func = c.member
|
|
348
354
|
_RISK_MEMBER_MAP = {
|
|
349
|
-
"max_intraday_filled_orders": (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
"
|
|
353
|
-
"
|
|
355
|
+
"max_intraday_filled_orders": (
|
|
356
|
+
"set_pine_risk_max_intraday_filled_orders", "int"
|
|
357
|
+
),
|
|
358
|
+
"max_drawdown": ("set_pine_risk_max_drawdown", "double"),
|
|
359
|
+
"max_intraday_loss": ("set_pine_risk_max_intraday_loss", "double"),
|
|
360
|
+
"max_position_size": ("set_pine_risk_max_position_size", "double"),
|
|
361
|
+
"max_cons_loss_days": ("set_pine_risk_max_cons_loss_days", "int"),
|
|
354
362
|
}
|
|
355
363
|
if risk_func == "allow_entry_in":
|
|
356
364
|
val = self._visit_expr(node.expr.args[0])
|
|
357
365
|
if val == "1":
|
|
358
|
-
|
|
366
|
+
direction = "1"
|
|
359
367
|
elif val == "-1":
|
|
360
|
-
|
|
368
|
+
direction = "-1"
|
|
361
369
|
else:
|
|
362
|
-
|
|
370
|
+
direction = "0"
|
|
371
|
+
lines.append(f"{pad}set_pine_risk_direction({direction});")
|
|
363
372
|
return
|
|
364
373
|
if risk_func in _RISK_MEMBER_MAP:
|
|
365
|
-
|
|
374
|
+
setter, cast_type = _RISK_MEMBER_MAP[risk_func]
|
|
366
375
|
val = self._visit_expr(node.expr.args[0])
|
|
367
|
-
|
|
368
|
-
#
|
|
376
|
+
# The percent flag travels with the matching setter so
|
|
377
|
+
# every risk update atomically replaces both fields.
|
|
369
378
|
if risk_func in ("max_drawdown", "max_intraday_loss") and len(node.expr.args) >= 2:
|
|
370
379
|
arg2 = node.expr.args[1]
|
|
371
380
|
is_pct = (isinstance(arg2, MemberAccess)
|
|
372
381
|
and isinstance(arg2.object, Identifier)
|
|
373
382
|
and arg2.object.name == "strategy"
|
|
374
383
|
and arg2.member == "percent_of_equity")
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
384
|
+
else:
|
|
385
|
+
is_pct = False
|
|
386
|
+
if risk_func in ("max_drawdown", "max_intraday_loss"):
|
|
387
|
+
lines.append(
|
|
388
|
+
f"{pad}{setter}(({cast_type})({val}), "
|
|
389
|
+
f"{'true' if is_pct else 'false'});"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
lines.append(f"{pad}{setter}(({cast_type})({val}));")
|
|
378
393
|
return
|
|
379
394
|
if self._is_skip_expr(node.expr):
|
|
380
395
|
return
|
|
@@ -495,6 +510,11 @@ class StmtVisitor:
|
|
|
495
510
|
)
|
|
496
511
|
previous_input_name = self._current_input_var_name
|
|
497
512
|
self._current_input_var_name = node.name
|
|
513
|
+
# A ``var`` int array that stores epoch milliseconds is declared
|
|
514
|
+
# ``std::vector<int64_t>``; its one-shot constructor here must
|
|
515
|
+
# spell the same type (visit_call array.new_* / array.from).
|
|
516
|
+
previous_ctor_target = getattr(self, "_array_ctor_target_name", None)
|
|
517
|
+
self._array_ctor_target_name = node.name
|
|
498
518
|
try:
|
|
499
519
|
type_spec = info.get("type_spec")
|
|
500
520
|
target_cpp_type = info.get("drawing_cpp")
|
|
@@ -525,6 +545,37 @@ class StmtVisitor:
|
|
|
525
545
|
lines.append(f"{pad} {flag_expr} = true;")
|
|
526
546
|
lines.append(f"{pad}}}")
|
|
527
547
|
return
|
|
548
|
+
if isinstance(node.value, (IfStmt, SwitchStmt)):
|
|
549
|
+
# Any other ``var`` whose initializer is an if/switch
|
|
550
|
+
# expression: the selection, once, at first reach. It
|
|
551
|
+
# rendered as ``/* unknown */``, which did not compile
|
|
552
|
+
# (``var string dashPos = switch dashPosInput``). A
|
|
553
|
+
# history-referenced one replaces its current slot.
|
|
554
|
+
selection_cpp_type = (
|
|
555
|
+
target_cpp_type or self._int_slot_cpp_type(member_name)
|
|
556
|
+
)
|
|
557
|
+
indent = len(pad) // 4 + 1
|
|
558
|
+
lines.append(f"{pad}if (!{flag_expr}) {{")
|
|
559
|
+
if info.get("is_series"):
|
|
560
|
+
# The member's own element type (string, drawing
|
|
561
|
+
# or number), from its current slot: the carry
|
|
562
|
+
# has pushed the var's na there before its first
|
|
563
|
+
# reach, which an arm no case selects keeps.
|
|
564
|
+
selected = f"_pf_selection_{flag}"
|
|
565
|
+
lines.append(f"{pad} auto {selected} = {target_expr}[0];")
|
|
566
|
+
self._visit_if_switch_expr(
|
|
567
|
+
node.value, selected, lines, indent,
|
|
568
|
+
target_cpp_type=selection_cpp_type,
|
|
569
|
+
)
|
|
570
|
+
lines.append(f"{pad} {target_expr}.update({selected});")
|
|
571
|
+
else:
|
|
572
|
+
self._visit_if_switch_expr(
|
|
573
|
+
node.value, target_expr, lines, indent,
|
|
574
|
+
target_cpp_type=selection_cpp_type,
|
|
575
|
+
)
|
|
576
|
+
lines.append(f"{pad} {flag_expr} = true;")
|
|
577
|
+
lines.append(f"{pad}}}")
|
|
578
|
+
return
|
|
528
579
|
if target_cpp_type is not None:
|
|
529
580
|
init_cpp = self._visit_rhs_value(
|
|
530
581
|
node.value,
|
|
@@ -535,10 +586,15 @@ class StmtVisitor:
|
|
|
535
586
|
init_cpp = self._visit_expr(node.value)
|
|
536
587
|
finally:
|
|
537
588
|
self._current_input_var_name = previous_input_name
|
|
589
|
+
self._array_ctor_target_name = previous_ctor_target
|
|
538
590
|
if info.get("drawing_cpp") is None:
|
|
539
591
|
init_cpp = self._typed_na_init(
|
|
540
592
|
init_cpp, member_name, info["ptype"]
|
|
541
593
|
)
|
|
594
|
+
init_cpp = self._coerce_int_slot(
|
|
595
|
+
init_cpp, node.value,
|
|
596
|
+
self._int_slot_cpp_type(member_name),
|
|
597
|
+
)
|
|
542
598
|
lines.append(f"{pad}if (!{flag_expr}) {{")
|
|
543
599
|
# A history-referenced persistent primitive is a Series<T> too,
|
|
544
600
|
# not just a drawing handle. The per-bar carry has already
|
|
@@ -618,13 +674,19 @@ class StmtVisitor:
|
|
|
618
674
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
619
675
|
if namespace == "array" and func_name in ARRAY_NEW_CTORS | {"new", "from", "copy", "slice"}:
|
|
620
676
|
captured = self._callable_collection_bindings.get(id(node))
|
|
621
|
-
spec = (
|
|
677
|
+
spec = self._widen_array_spec_for_name(
|
|
678
|
+
node.name,
|
|
622
679
|
captured
|
|
623
680
|
if captured is not None and captured.kind == "array"
|
|
624
681
|
else self._type_spec_from_expr(node.value)
|
|
625
|
-
or self._array_spec_for_name(node.name)
|
|
682
|
+
or self._array_spec_for_name(node.name),
|
|
626
683
|
)
|
|
627
|
-
|
|
684
|
+
previous_target = getattr(self, "_array_ctor_target_name", None)
|
|
685
|
+
self._array_ctor_target_name = node.name
|
|
686
|
+
try:
|
|
687
|
+
init = self._visit_expr(node.value)
|
|
688
|
+
finally:
|
|
689
|
+
self._array_ctor_target_name = previous_target
|
|
628
690
|
self._array_vars.add(node.name)
|
|
629
691
|
self._collection_types.setdefault(node.name, spec)
|
|
630
692
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
@@ -648,10 +710,17 @@ class StmtVisitor:
|
|
|
648
710
|
)
|
|
649
711
|
elem_spec = spec.element or elem_spec
|
|
650
712
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
713
|
+
if len(node.value.args) > 2:
|
|
714
|
+
self._warn_narrow_int_element(
|
|
715
|
+
node.value, spec, node.value.args[2])
|
|
651
716
|
if len(node.value.args) >= 2:
|
|
652
717
|
r = self._visit_expr(node.value.args[0])
|
|
653
718
|
c = self._visit_expr(node.value.args[1])
|
|
654
|
-
v =
|
|
719
|
+
v = (
|
|
720
|
+
self._array_init_value_expr(elem_spec, node.value.args[2])
|
|
721
|
+
if len(node.value.args) > 2
|
|
722
|
+
else self._default_for_spec(elem_spec)
|
|
723
|
+
)
|
|
655
724
|
init = f"{cpp_type}::new_({r}, {c}, {v})"
|
|
656
725
|
else:
|
|
657
726
|
init = f"{cpp_type}::new_(0, 0, {self._default_for_spec(elem_spec)})"
|
|
@@ -726,12 +795,24 @@ class StmtVisitor:
|
|
|
726
795
|
site = self._get_ta_site(node.value)
|
|
727
796
|
if site is not None:
|
|
728
797
|
compute_args = self._ta_compute_args_for_site(site)
|
|
729
|
-
ret_type =
|
|
798
|
+
ret_type = (
|
|
799
|
+
self._type_for_decl(node)
|
|
800
|
+
if node.type_hint and not is_global_member
|
|
801
|
+
else "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL
|
|
802
|
+
else "double"
|
|
803
|
+
)
|
|
730
804
|
ta_name = self._ta_member_name(site)
|
|
731
805
|
ta_expr = (
|
|
732
806
|
f"(history_advances_new_bar() ? {ta_name}.compute({compute_args}) "
|
|
733
807
|
f": {ta_name}.recompute({compute_args}))"
|
|
734
808
|
)
|
|
809
|
+
ta_expr = self._coerce_int_slot(
|
|
810
|
+
ta_expr,
|
|
811
|
+
node.value,
|
|
812
|
+
self._int_slot_cpp_type(
|
|
813
|
+
node.name, None if is_global_member else ret_type
|
|
814
|
+
),
|
|
815
|
+
)
|
|
735
816
|
if declaration_is_series:
|
|
736
817
|
self._emit_history_series_write(lines, pad, safe, ta_expr)
|
|
737
818
|
elif is_global_member:
|
|
@@ -752,6 +833,10 @@ class StmtVisitor:
|
|
|
752
833
|
else None
|
|
753
834
|
),
|
|
754
835
|
)
|
|
836
|
+
cpp_val = self._coerce_int_slot(
|
|
837
|
+
cpp_val, node.value,
|
|
838
|
+
self._int_slot_cpp_type(None, self._series_type_for(node.name)),
|
|
839
|
+
)
|
|
755
840
|
self._emit_history_series_write(lines, pad, safe, cpp_val)
|
|
756
841
|
return
|
|
757
842
|
|
|
@@ -779,17 +864,25 @@ class StmtVisitor:
|
|
|
779
864
|
node.name,
|
|
780
865
|
cpp_type,
|
|
781
866
|
)
|
|
867
|
+
if selection_cpp_type is None:
|
|
868
|
+
selection_cpp_type = self._int_slot_cpp_type(
|
|
869
|
+
node.name, cpp_type,
|
|
870
|
+
)
|
|
782
871
|
if not is_global_member:
|
|
783
872
|
default = self._default_for_type(cpp_type)
|
|
784
873
|
lines.append(f"{pad}{cpp_type} {safe} = {default};")
|
|
785
874
|
remember_local_type(cpp_type)
|
|
786
875
|
indent = len(pad) // 4
|
|
787
|
-
self.
|
|
876
|
+
self._visit_selection_value(
|
|
788
877
|
node.value,
|
|
789
878
|
safe,
|
|
790
879
|
lines,
|
|
791
880
|
indent,
|
|
792
881
|
target_cpp_type=selection_cpp_type,
|
|
882
|
+
slot_cpp_type=(
|
|
883
|
+
cpp_type if cpp_type is not None
|
|
884
|
+
else self._infer_type(Identifier(name=node.name))
|
|
885
|
+
),
|
|
793
886
|
)
|
|
794
887
|
return
|
|
795
888
|
|
|
@@ -839,6 +932,12 @@ class StmtVisitor:
|
|
|
839
932
|
node.name,
|
|
840
933
|
target_cpp_type=target_cpp_type,
|
|
841
934
|
)
|
|
935
|
+
# ``cpp_type`` is None for a hoisted global; the slot resolver then
|
|
936
|
+
# reads the member declaration instead.
|
|
937
|
+
cpp_val = self._coerce_int_slot(
|
|
938
|
+
cpp_val, node.value,
|
|
939
|
+
self._int_slot_cpp_type(node.name, cpp_type),
|
|
940
|
+
)
|
|
842
941
|
if is_global_member:
|
|
843
942
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
844
943
|
else:
|
|
@@ -971,13 +1070,16 @@ class StmtVisitor:
|
|
|
971
1070
|
target_name,
|
|
972
1071
|
None,
|
|
973
1072
|
)
|
|
1073
|
+
if selection_cpp_type is None:
|
|
1074
|
+
selection_cpp_type = self._int_slot_cpp_type(target_name)
|
|
974
1075
|
indent = len(pad) // 4
|
|
975
|
-
self.
|
|
1076
|
+
self._visit_selection_value(
|
|
976
1077
|
node.value,
|
|
977
1078
|
safe,
|
|
978
1079
|
lines,
|
|
979
1080
|
indent,
|
|
980
1081
|
target_cpp_type=selection_cpp_type,
|
|
1082
|
+
slot_cpp_type=self._infer_type(node.target),
|
|
981
1083
|
)
|
|
982
1084
|
return
|
|
983
1085
|
|
|
@@ -1010,13 +1112,28 @@ class StmtVisitor:
|
|
|
1010
1112
|
val_cpp = self._visit_rhs_value(
|
|
1011
1113
|
node.value, target_cpp_type=target_cpp_type
|
|
1012
1114
|
)
|
|
1013
|
-
|
|
1115
|
+
field_int = self._udt_field_int_cpp_type(node.target)
|
|
1116
|
+
array_field = self._udt_array_field_target(node.target)
|
|
1117
|
+
if node.op == ":=" and array_field is not None:
|
|
1118
|
+
# The field rebinds to the array: an alias of a stable
|
|
1119
|
+
# script var array, else the value moved or copied in.
|
|
1120
|
+
lines.append(
|
|
1121
|
+
f"{pad}{target_cpp} = "
|
|
1122
|
+
f"{self._udt_array_field_value(array_field, node.value, val_cpp)};")
|
|
1123
|
+
elif node.op == ":=":
|
|
1124
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, field_int)
|
|
1014
1125
|
lines.append(f"{pad}{target_cpp} = {val_cpp};")
|
|
1015
1126
|
else:
|
|
1016
1127
|
rhs = self._compound_assign_rhs(target_cpp, node.op, val_cpp)
|
|
1017
1128
|
if rhs is not None:
|
|
1129
|
+
rhs = self._coerce_int_slot(
|
|
1130
|
+
rhs, node.value, field_int, value_is_double=True,
|
|
1131
|
+
)
|
|
1018
1132
|
lines.append(f"{pad}{target_cpp} = {rhs};")
|
|
1019
1133
|
else:
|
|
1134
|
+
val_cpp = self._coerce_int_slot(
|
|
1135
|
+
val_cpp, node.value, field_int,
|
|
1136
|
+
)
|
|
1020
1137
|
lines.append(f"{pad}{target_cpp} {node.op} {val_cpp};")
|
|
1021
1138
|
return
|
|
1022
1139
|
|
|
@@ -1034,17 +1151,27 @@ class StmtVisitor:
|
|
|
1034
1151
|
None,
|
|
1035
1152
|
),
|
|
1036
1153
|
)
|
|
1154
|
+
elem_int = self._int_slot_cpp_type(
|
|
1155
|
+
None, self._series_type_for(target_name),
|
|
1156
|
+
)
|
|
1037
1157
|
if node.op == ":=":
|
|
1158
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, elem_int)
|
|
1038
1159
|
lines.append(f"{pad}{safe}.update({val_cpp});")
|
|
1039
1160
|
else:
|
|
1040
1161
|
rhs = self._compound_assign_rhs(f"{safe}[0]", node.op, val_cpp)
|
|
1041
1162
|
if rhs is not None:
|
|
1042
1163
|
# x /= y → x.update((double)x[0] / (double)y); x %= y → fmod
|
|
1164
|
+
rhs = self._coerce_int_slot(
|
|
1165
|
+
rhs, node.value, elem_int, value_is_double=True,
|
|
1166
|
+
)
|
|
1043
1167
|
lines.append(f"{pad}{safe}.update({rhs});")
|
|
1044
1168
|
else:
|
|
1045
1169
|
# Compound assignment: x += y → x.update(x[0] + y)
|
|
1046
1170
|
op_char = node.op[0] # e.g., "+" from "+="
|
|
1047
|
-
|
|
1171
|
+
combined = self._coerce_int_slot(
|
|
1172
|
+
f"{safe}[0] {op_char} {val_cpp}", node.value, elem_int,
|
|
1173
|
+
)
|
|
1174
|
+
lines.append(f"{pad}{safe}.update({combined});")
|
|
1048
1175
|
elif target_name in self._var_names:
|
|
1049
1176
|
# A bare-``na`` reassignment must adopt the target's declared scalar
|
|
1050
1177
|
# type (``x := na`` -> ``na<int>()`` not ``na<double>()``); otherwise
|
|
@@ -1057,14 +1184,30 @@ class StmtVisitor:
|
|
|
1057
1184
|
if tct is None and self._is_na_expr(node.value):
|
|
1058
1185
|
tct = self._na_reassign_cpp_type(target_name)
|
|
1059
1186
|
val_cpp = self._visit_rhs_value(node.value, target_name, target_cpp_type=tct)
|
|
1187
|
+
int_slot = self._int_slot_cpp_type(target_name)
|
|
1060
1188
|
if node.op == ":=":
|
|
1189
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
|
|
1061
1190
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
1062
1191
|
else:
|
|
1063
1192
|
rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
|
|
1064
1193
|
if rhs is not None:
|
|
1194
|
+
# ``/=`` and ``%=`` lower to an always-double form, so an
|
|
1195
|
+
# int target narrows whatever the operands were.
|
|
1196
|
+
rhs = self._coerce_int_slot(
|
|
1197
|
+
rhs, node.value, int_slot, value_is_double=True,
|
|
1198
|
+
)
|
|
1065
1199
|
lines.append(f"{pad}{safe} = {rhs};")
|
|
1066
1200
|
else:
|
|
1067
|
-
|
|
1201
|
+
# Narrow the WHOLE compound expression, not the operand:
|
|
1202
|
+
# ``x -= 0.5`` on an int slot truncated (x - 0.5), and
|
|
1203
|
+
# truncating the operand first would round differently.
|
|
1204
|
+
combined = self._coerce_int_slot(
|
|
1205
|
+
f"{safe} {node.op[0]} {val_cpp}", node.value, int_slot,
|
|
1206
|
+
)
|
|
1207
|
+
if combined != f"{safe} {node.op[0]} {val_cpp}":
|
|
1208
|
+
lines.append(f"{pad}{safe} = {combined};")
|
|
1209
|
+
else:
|
|
1210
|
+
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
1068
1211
|
else:
|
|
1069
1212
|
tct = self._nullable_collection_target_cpp_type(name=target_name)
|
|
1070
1213
|
if tct is None:
|
|
@@ -1072,14 +1215,30 @@ class StmtVisitor:
|
|
|
1072
1215
|
if tct is None and self._is_na_expr(node.value):
|
|
1073
1216
|
tct = self._na_reassign_cpp_type(target_name)
|
|
1074
1217
|
val_cpp = self._visit_rhs_value(node.value, target_name, target_cpp_type=tct)
|
|
1218
|
+
int_slot = self._int_slot_cpp_type(target_name)
|
|
1075
1219
|
if node.op == ":=":
|
|
1220
|
+
val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
|
|
1076
1221
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
1077
1222
|
else:
|
|
1078
1223
|
rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
|
|
1079
1224
|
if rhs is not None:
|
|
1225
|
+
# ``/=`` and ``%=`` lower to an always-double form, so an
|
|
1226
|
+
# int target narrows whatever the operands were.
|
|
1227
|
+
rhs = self._coerce_int_slot(
|
|
1228
|
+
rhs, node.value, int_slot, value_is_double=True,
|
|
1229
|
+
)
|
|
1080
1230
|
lines.append(f"{pad}{safe} = {rhs};")
|
|
1081
1231
|
else:
|
|
1082
|
-
|
|
1232
|
+
# Narrow the WHOLE compound expression, not the operand:
|
|
1233
|
+
# ``x -= 0.5`` on an int slot truncated (x - 0.5), and
|
|
1234
|
+
# truncating the operand first would round differently.
|
|
1235
|
+
combined = self._coerce_int_slot(
|
|
1236
|
+
f"{safe} {node.op[0]} {val_cpp}", node.value, int_slot,
|
|
1237
|
+
)
|
|
1238
|
+
if combined != f"{safe} {node.op[0]} {val_cpp}":
|
|
1239
|
+
lines.append(f"{pad}{safe} = {combined};")
|
|
1240
|
+
else:
|
|
1241
|
+
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
1083
1242
|
|
|
1084
1243
|
def _visit_tuple_assign(self, node: TupleAssign, lines: list[str], pad: str) -> None:
|
|
1085
1244
|
is_top_level = any(id(node) == id(stmt) for stmt in self.ctx.ast.body)
|
|
@@ -1140,6 +1299,14 @@ class StmtVisitor:
|
|
|
1140
1299
|
and len(tuple_types) == tuple_size
|
|
1141
1300
|
and all(item == PineType.BOOL for item in tuple_types)
|
|
1142
1301
|
)
|
|
1302
|
+
# A helper tuple mixing bool, string and numeric
|
|
1303
|
+
# elements (the analyzer admits nothing else).
|
|
1304
|
+
mixed_helper_tuple = (
|
|
1305
|
+
not isinstance(expr_node, TupleLiteral)
|
|
1306
|
+
and info.get("returns_tuple", False)
|
|
1307
|
+
and tuple_size >= 2
|
|
1308
|
+
and len(tuple_types) == tuple_size
|
|
1309
|
+
)
|
|
1143
1310
|
exact_direct_tuple = (
|
|
1144
1311
|
isinstance(expr_node, TupleLiteral)
|
|
1145
1312
|
and tuple_size >= 2
|
|
@@ -1165,6 +1332,7 @@ class StmtVisitor:
|
|
|
1165
1332
|
if (
|
|
1166
1333
|
numeric_tuple
|
|
1167
1334
|
or bool_tuple
|
|
1335
|
+
or mixed_helper_tuple
|
|
1168
1336
|
or exact_direct_tuple
|
|
1169
1337
|
or known_ta_tuple
|
|
1170
1338
|
):
|
|
@@ -1181,6 +1349,10 @@ class StmtVisitor:
|
|
|
1181
1349
|
the tuple once whenever either case applies; ordinary lexical scalar
|
|
1182
1350
|
elements remain locals, Series elements advance their remapped
|
|
1183
1351
|
members, and top-level scalars assign their class storage.
|
|
1352
|
+
Pine's ``_`` binds nothing, but a structured binding declares
|
|
1353
|
+
every name: C++17 has no placeholder, and GCC rejects a second
|
|
1354
|
+
``_`` in one scope (``[a, _, _] = f()``, or two tuples of one
|
|
1355
|
+
block) as a redeclaration. Each ``_`` gets a name of its own.
|
|
1184
1356
|
"""
|
|
1185
1357
|
series_names = {
|
|
1186
1358
|
name
|
|
@@ -1189,7 +1361,11 @@ class StmtVisitor:
|
|
|
1189
1361
|
and self._decl_binding_is_series(id(node), name)
|
|
1190
1362
|
}
|
|
1191
1363
|
if not series_names and not global_targets.intersection(node.names):
|
|
1192
|
-
binding_names = ", ".join(
|
|
1364
|
+
binding_names = ", ".join(
|
|
1365
|
+
self._tuple_placeholder_name() if name == "_"
|
|
1366
|
+
else self._safe_name(name)
|
|
1367
|
+
for name in node.names
|
|
1368
|
+
)
|
|
1193
1369
|
lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
|
|
1194
1370
|
return
|
|
1195
1371
|
|
|
@@ -1258,14 +1434,38 @@ class StmtVisitor:
|
|
|
1258
1434
|
f"{pad}{self._safe_name(name)} = {field_expr};"
|
|
1259
1435
|
)
|
|
1260
1436
|
else:
|
|
1261
|
-
lines.append(
|
|
1437
|
+
lines.append(
|
|
1438
|
+
f"{pad}double {self._safe_name(name)} = {field_expr};"
|
|
1439
|
+
)
|
|
1440
|
+
return
|
|
1441
|
+
|
|
1442
|
+
# ``[a, b] = switch x ...`` / ``= if c ...``: every arm yields a tuple
|
|
1443
|
+
# (the analyzer's ``_selection_tuple_shape``). Materialize the selected
|
|
1444
|
+
# tuple, na in every position when no arm runs, then destructure it.
|
|
1445
|
+
if isinstance(node.value, (IfStmt, SwitchStmt)):
|
|
1446
|
+
types = self._infer_selection_tuple_types(
|
|
1447
|
+
node.value, len(node.names)
|
|
1448
|
+
)
|
|
1449
|
+
tuple_t = f"std::tuple<{', '.join(types)}>"
|
|
1450
|
+
temp = f"_tuple_result_{self._tuple_assign_counter}"
|
|
1451
|
+
self._tuple_assign_counter += 1
|
|
1452
|
+
lines.append(f"{pad}{tuple_t} {temp} = {self._tuple_default_expr(types)};")
|
|
1453
|
+
self._visit_if_switch_expr(
|
|
1454
|
+
node.value, temp, lines, len(pad) // 4, target_cpp_type=tuple_t
|
|
1455
|
+
)
|
|
1456
|
+
emit_call_tuple(temp)
|
|
1262
1457
|
return
|
|
1263
1458
|
|
|
1264
1459
|
# User-defined function returning a tuple: use C++17 structured bindings
|
|
1265
1460
|
if isinstance(node.value, FuncCall):
|
|
1266
1461
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
1267
1462
|
if namespace == "request" and func_name == "security":
|
|
1268
|
-
|
|
1463
|
+
# A request whose names are reassigned is read where it is
|
|
1464
|
+
# evaluated (external_requests): its data-missing stop.
|
|
1465
|
+
if UNPINNED_ANNOTATION in (node.value.annotations or {}):
|
|
1466
|
+
call_expr = self._unpinned_read(node.value)
|
|
1467
|
+
else:
|
|
1468
|
+
call_expr = self._visit_func_call(node.value)
|
|
1269
1469
|
emit_call_tuple(call_expr)
|
|
1270
1470
|
return
|
|
1271
1471
|
if func_name and namespace is None and func_name in self._func_names:
|
|
@@ -1299,6 +1499,13 @@ class StmtVisitor:
|
|
|
1299
1499
|
|
|
1300
1500
|
lines.append(f"{pad}/* unsupported tuple assignment */")
|
|
1301
1501
|
|
|
1502
|
+
def _tuple_placeholder_name(self) -> str:
|
|
1503
|
+
"""A fresh C++ name for one ``_`` of a tuple declaration's structured
|
|
1504
|
+
binding, which must name every element."""
|
|
1505
|
+
index = getattr(self, "_tuple_placeholder_counter", 0)
|
|
1506
|
+
self._tuple_placeholder_counter = index + 1
|
|
1507
|
+
return f"_tuple_unused_{index}"
|
|
1508
|
+
|
|
1302
1509
|
def _tuple_binding_cpp_types(self, node: TupleAssign) -> list[str]:
|
|
1303
1510
|
"""Exact supported tuple element types for later lexical operations."""
|
|
1304
1511
|
count = len(node.names)
|
|
@@ -1465,7 +1672,9 @@ class StmtVisitor:
|
|
|
1465
1672
|
def _visit_if_body(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
1466
1673
|
pad = " " * indent
|
|
1467
1674
|
|
|
1468
|
-
cond = self.
|
|
1675
|
+
cond = self._coerce_bool_expr(
|
|
1676
|
+
self._visit_expr(node.condition), node.condition
|
|
1677
|
+
)
|
|
1469
1678
|
lines.append(f"{pad}if ({cond}) {{")
|
|
1470
1679
|
self._visit_block_statements(node.body, lines, indent + 1)
|
|
1471
1680
|
lines.append(f"{pad}}}")
|
|
@@ -1480,7 +1689,8 @@ class StmtVisitor:
|
|
|
1480
1689
|
)
|
|
1481
1690
|
lines.append(f"{pad}}}")
|
|
1482
1691
|
|
|
1483
|
-
def _visit_for(self, node: ForStmt, lines: list[str], indent: int
|
|
1692
|
+
def _visit_for(self, node: ForStmt, lines: list[str], indent: int,
|
|
1693
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1484
1694
|
pad = " " * indent
|
|
1485
1695
|
start = self._visit_expr(node.start)
|
|
1486
1696
|
end = self._visit_expr(node.end)
|
|
@@ -1506,35 +1716,69 @@ class StmtVisitor:
|
|
|
1506
1716
|
)
|
|
1507
1717
|
)
|
|
1508
1718
|
end_eval = f"_for_end_eval_{fid}" if end_mentions_binder else None
|
|
1719
|
+
# ``from``/``to``/``by`` land in ``int`` slots. A double-valued bound
|
|
1720
|
+
# (an array size, an na-able expression, ...) narrows there, which is
|
|
1721
|
+
# undefined for na — so spell the conversion out. A bound that IS na
|
|
1722
|
+
# then reads as na<int>() (INT_MIN), and the loop must run zero times
|
|
1723
|
+
# rather than descend two billion steps toward it, so the guarded form
|
|
1724
|
+
# also tests the bounds. Loops whose bounds were already integral emit
|
|
1725
|
+
# exactly as before.
|
|
1726
|
+
start_cpp = self._coerce_int_slot(f"({start})", node.start, "int")
|
|
1727
|
+
end_cpp = self._coerce_int_slot(f"({end})", node.end, "int")
|
|
1728
|
+
step_cpp = self._coerce_int_slot(f"({step})", node.step, "int")
|
|
1729
|
+
na_capable = (
|
|
1730
|
+
start_cpp != f"({start})"
|
|
1731
|
+
or end_cpp != f"({end})"
|
|
1732
|
+
or step_cpp != f"({step})"
|
|
1733
|
+
)
|
|
1509
1734
|
if end_eval is not None:
|
|
1510
1735
|
# The ``to`` expression is authored outside the loop-binder scope,
|
|
1511
1736
|
# but its refresh executes inside the generated C++ ``for`` where
|
|
1512
1737
|
# the binder would shadow a same-named outer member/parameter. A
|
|
1513
1738
|
# pre-binder lambda preserves the authored lexical binding while
|
|
1514
1739
|
# still reevaluating the expression after every iteration.
|
|
1515
|
-
lines.append(f"{pad}auto {end_eval} = [&]() {{ return
|
|
1516
|
-
lines.append(f"{pad}int {s_var} =
|
|
1517
|
-
end_expr = f"{end_eval}()" if end_eval is not None else
|
|
1740
|
+
lines.append(f"{pad}auto {end_eval} = [&]() {{ return {end_cpp}; }};")
|
|
1741
|
+
lines.append(f"{pad}int {s_var} = {start_cpp};")
|
|
1742
|
+
end_expr = f"{end_eval}()" if end_eval is not None else end_cpp
|
|
1518
1743
|
lines.append(f"{pad}int {e_var} = {end_expr};")
|
|
1519
|
-
lines.append(f"{pad}int {step_var} =
|
|
1520
|
-
|
|
1744
|
+
lines.append(f"{pad}int {step_var} = {step_cpp};")
|
|
1745
|
+
if na_capable:
|
|
1746
|
+
# ``-na<int>()`` would overflow; leave an na step alone and let the
|
|
1747
|
+
# loop condition reject it.
|
|
1748
|
+
lines.append(
|
|
1749
|
+
f"{pad}if (!is_na({step_var}) && {step_var} < 0)"
|
|
1750
|
+
f" {step_var} = -{step_var};"
|
|
1751
|
+
)
|
|
1752
|
+
else:
|
|
1753
|
+
lines.append(f"{pad}if ({step_var} < 0) {step_var} = -{step_var};")
|
|
1521
1754
|
lines.append(f"{pad}if ({step_var} == 0) {step_var} = 1;")
|
|
1522
1755
|
lines.append(f"{pad}const bool {down_var} = ({s_var} > {e_var});")
|
|
1756
|
+
na_guard = (
|
|
1757
|
+
f"!is_na({s_var}) && !is_na({e_var}) && !is_na({step_var}) && "
|
|
1758
|
+
if na_capable else ""
|
|
1759
|
+
)
|
|
1760
|
+
# Pine v6 evaluates ``to`` before every iteration; v5 (a v5 library's
|
|
1761
|
+
# body) fixes it before the first one.
|
|
1762
|
+
refresh = "" if self._pine_v5_body else f", {e_var} = {end_expr}"
|
|
1523
1763
|
lines.append(
|
|
1524
1764
|
f"{pad}for (int {var} = {s_var}; "
|
|
1765
|
+
f"{na_guard}"
|
|
1525
1766
|
f"({down_var} ? ({var} >= {e_var}) : ({var} <= {e_var})); "
|
|
1526
|
-
f"{var} += ({down_var} ? -{step_var} : {step_var})
|
|
1527
|
-
f"{
|
|
1767
|
+
f"{var} += ({down_var} ? -{step_var} : {step_var})"
|
|
1768
|
+
f"{refresh}) {{"
|
|
1528
1769
|
)
|
|
1529
1770
|
# Register the loop counter so reads of it inside the body resolve (the
|
|
1530
1771
|
# unknown-identifier guard in _visit_ident would otherwise flag it).
|
|
1531
1772
|
saved_loop = self._current_loop_vars
|
|
1532
1773
|
saved_loop_specs = self._current_loop_var_specs
|
|
1774
|
+
saved_counted = self._current_counted_loop_vars
|
|
1533
1775
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
1534
1776
|
self._current_loop_var_specs = dict(self._current_loop_var_specs)
|
|
1777
|
+
self._current_counted_loop_vars = set(self._current_counted_loop_vars)
|
|
1535
1778
|
if node.var:
|
|
1536
1779
|
self._current_loop_vars.add(node.var)
|
|
1537
1780
|
self._current_loop_var_specs[node.var] = TypeSpec.primitive("int")
|
|
1781
|
+
self._current_counted_loop_vars.add(node.var)
|
|
1538
1782
|
_blk_saved = self._push_block_var_remap(node)
|
|
1539
1783
|
if node.var:
|
|
1540
1784
|
# The loop counter is a fresh primitive lexical binding. Keep it
|
|
@@ -1544,21 +1788,26 @@ class StmtVisitor:
|
|
|
1544
1788
|
self._lexical_series_bindings[node.var] = False
|
|
1545
1789
|
self._lexical_known_var_tombstones.add(node.var)
|
|
1546
1790
|
try:
|
|
1547
|
-
|
|
1548
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
1791
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1549
1792
|
finally:
|
|
1550
1793
|
self._pop_block_var_remap(_blk_saved)
|
|
1551
1794
|
self._current_loop_vars = saved_loop
|
|
1552
1795
|
self._current_loop_var_specs = saved_loop_specs
|
|
1796
|
+
self._current_counted_loop_vars = saved_counted
|
|
1553
1797
|
lines.append(f"{pad}}}")
|
|
1554
1798
|
|
|
1555
|
-
def _visit_for_in(self, node, lines: list[str], indent: int
|
|
1799
|
+
def _visit_for_in(self, node, lines: list[str], indent: int,
|
|
1800
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1556
1801
|
pad = " " * indent
|
|
1557
1802
|
iterable = self._visit_expr(node.iterable)
|
|
1558
1803
|
saved_loop = self._current_loop_vars
|
|
1559
1804
|
saved_loop_specs = self._current_loop_var_specs
|
|
1805
|
+
saved_counted = self._current_counted_loop_vars
|
|
1560
1806
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
1561
1807
|
self._current_loop_var_specs = dict(self._current_loop_var_specs)
|
|
1808
|
+
# Its binders shadow a counted loop's of the same spelling.
|
|
1809
|
+
self._current_counted_loop_vars = (
|
|
1810
|
+
self._current_counted_loop_vars - {node.var, *(node.vars or ())})
|
|
1562
1811
|
iterable_spec = self._type_spec_from_expr(node.iterable)
|
|
1563
1812
|
elem_spec = (
|
|
1564
1813
|
iterable_spec.element
|
|
@@ -1569,16 +1818,23 @@ class StmtVisitor:
|
|
|
1569
1818
|
self._current_loop_vars.add(node.var)
|
|
1570
1819
|
if elem_spec is not None:
|
|
1571
1820
|
self._current_loop_var_specs[node.var] = elem_spec
|
|
1821
|
+
else:
|
|
1822
|
+
# An outer binder's spec of the same spelling is not this one's.
|
|
1823
|
+
self._current_loop_var_specs.pop(node.var, None)
|
|
1572
1824
|
if node.vars:
|
|
1573
1825
|
tuple_specs: list[TypeSpec | None] = []
|
|
1574
1826
|
if iterable_spec is not None and iterable_spec.kind == "map":
|
|
1575
1827
|
tuple_specs = [iterable_spec.key, iterable_spec.value]
|
|
1828
|
+
elif elem_spec is not None:
|
|
1829
|
+
tuple_specs = [TypeSpec.primitive("int"), elem_spec]
|
|
1576
1830
|
for idx, v in enumerate(node.vars):
|
|
1577
1831
|
if v != "_":
|
|
1578
1832
|
self._current_loop_vars.add(v)
|
|
1579
1833
|
if (idx < len(tuple_specs)
|
|
1580
1834
|
and tuple_specs[idx] is not None):
|
|
1581
1835
|
self._current_loop_var_specs[v] = tuple_specs[idx]
|
|
1836
|
+
else:
|
|
1837
|
+
self._current_loop_var_specs.pop(v, None)
|
|
1582
1838
|
map_pair_loop = (
|
|
1583
1839
|
iterable_spec is not None
|
|
1584
1840
|
and iterable_spec.kind == "map"
|
|
@@ -1644,8 +1900,46 @@ class StmtVisitor:
|
|
|
1644
1900
|
lines.append(
|
|
1645
1901
|
f"{pad} auto {value_cpp} = {map_token}.get({key_cpp});"
|
|
1646
1902
|
)
|
|
1903
|
+
elif node.vars and elem_spec is not None and len(node.vars) == 2:
|
|
1904
|
+
# ``for [i, v] in arr``: the index and the element. A vector has
|
|
1905
|
+
# no pairs to decompose (``auto [i, v] : arr`` did not compile);
|
|
1906
|
+
# bind the array once, then index it, reading its size on every
|
|
1907
|
+
# iteration as the single-name loop's element order does.
|
|
1908
|
+
index_name, value_name = node.vars
|
|
1909
|
+
authored_names = (
|
|
1910
|
+
set(self._all_bound_names)
|
|
1911
|
+
| set(self._func_names)
|
|
1912
|
+
| set(self._udt_defs)
|
|
1913
|
+
| set(self._current_func_param_types)
|
|
1914
|
+
)
|
|
1915
|
+
occupied_names = authored_names | {
|
|
1916
|
+
self._safe_name(name) for name in authored_names
|
|
1917
|
+
}
|
|
1918
|
+
while True:
|
|
1919
|
+
fid = self._for_counter
|
|
1920
|
+
self._for_counter += 1
|
|
1921
|
+
array_token = f"__pf_array_iter_{fid}"
|
|
1922
|
+
index_token = f"__pf_array_index_{fid}"
|
|
1923
|
+
if not ({array_token, index_token} & occupied_names):
|
|
1924
|
+
break
|
|
1925
|
+
lines.append(f"{pad}auto&& {array_token} = {iterable};")
|
|
1926
|
+
lines.append(
|
|
1927
|
+
f"{pad}for (int {index_token} = 0; "
|
|
1928
|
+
f"{index_token} < (int){array_token}.size(); ++{index_token}) {{")
|
|
1929
|
+
if index_name != "_":
|
|
1930
|
+
lines.append(f"{pad} int {self._safe_name(index_name)} = {index_token};")
|
|
1931
|
+
if value_name != "_":
|
|
1932
|
+
# The element's value, not a reference: a ``std::vector<bool>``
|
|
1933
|
+
# element read through ``auto`` is a proxy that a later
|
|
1934
|
+
# ``arr.set(i, ...)`` in the body would change.
|
|
1935
|
+
lines.append(
|
|
1936
|
+
f"{pad} typename std::decay_t<decltype({array_token})>"
|
|
1937
|
+
f"::value_type {self._safe_name(value_name)} = "
|
|
1938
|
+
f"{array_token}[(size_t){index_token}];")
|
|
1647
1939
|
elif node.vars:
|
|
1648
|
-
bindings = ", ".join(
|
|
1940
|
+
bindings = ", ".join(
|
|
1941
|
+
self._safe_name(name) for name in node.vars
|
|
1942
|
+
)
|
|
1649
1943
|
lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
|
|
1650
1944
|
_blk_saved = self._push_block_var_remap(node)
|
|
1651
1945
|
loop_binding_names = (
|
|
@@ -1688,26 +1982,248 @@ class StmtVisitor:
|
|
|
1688
1982
|
self._lexical_series_bindings[name] = False
|
|
1689
1983
|
self._lexical_known_var_tombstones.add(name)
|
|
1690
1984
|
try:
|
|
1691
|
-
|
|
1692
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
1985
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1693
1986
|
finally:
|
|
1694
1987
|
self._pop_block_var_remap(_blk_saved)
|
|
1695
1988
|
lines.append(f"{pad}}}")
|
|
1696
1989
|
self._current_loop_vars = saved_loop
|
|
1697
1990
|
self._current_loop_var_specs = saved_loop_specs
|
|
1991
|
+
self._current_counted_loop_vars = saved_counted
|
|
1698
1992
|
|
|
1699
|
-
def _visit_while(self, node: WhileStmt, lines: list[str], indent: int
|
|
1993
|
+
def _visit_while(self, node: WhileStmt, lines: list[str], indent: int,
|
|
1994
|
+
value_target: tuple[str, str | None] | None = None) -> None:
|
|
1700
1995
|
pad = " " * indent
|
|
1701
|
-
cond = self.
|
|
1996
|
+
cond = self._coerce_bool_expr(
|
|
1997
|
+
self._visit_expr(node.condition), node.condition
|
|
1998
|
+
)
|
|
1702
1999
|
lines.append(f"{pad}while ({cond}) {{")
|
|
1703
2000
|
_blk_saved = self._push_block_var_remap(node)
|
|
1704
2001
|
try:
|
|
1705
|
-
|
|
1706
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
2002
|
+
self._emit_loop_body(node.body, lines, indent + 1, value_target)
|
|
1707
2003
|
finally:
|
|
1708
2004
|
self._pop_block_var_remap(_blk_saved)
|
|
1709
2005
|
lines.append(f"{pad}}}")
|
|
1710
2006
|
|
|
2007
|
+
def _emit_loop_body(self, body: list, lines: list[str], indent: int,
|
|
2008
|
+
value_target: tuple[str, str | None] | None) -> None:
|
|
2009
|
+
"""A loop body; a loop used as a value assigns its last statement's
|
|
2010
|
+
value to ``value_target`` (``(target, target_cpp_type)``)."""
|
|
2011
|
+
if value_target is None:
|
|
2012
|
+
for s in body:
|
|
2013
|
+
self._visit_stmt(s, lines, indent)
|
|
2014
|
+
return
|
|
2015
|
+
target, target_cpp_type = value_target
|
|
2016
|
+
value_cpp = self._block_value_cpp_type(body, target_cpp_type)
|
|
2017
|
+
saved = getattr(self, "_unmatched_value_na", None)
|
|
2018
|
+
# TradingView: an if without else (a switch without default) is na
|
|
2019
|
+
# when no arm runs, so a loop whose body ends in one is na after an
|
|
2020
|
+
# iteration that ran none (lab tv probe pf-w2-f04_if_tails).
|
|
2021
|
+
self._unmatched_value_na = (
|
|
2022
|
+
(target, self._na_value_for_type(value_cpp))
|
|
2023
|
+
if value_cpp is not None else None
|
|
2024
|
+
)
|
|
2025
|
+
try:
|
|
2026
|
+
self._emit_body_with_assign(
|
|
2027
|
+
body, target, lines, indent, target_cpp_type=target_cpp_type,
|
|
2028
|
+
)
|
|
2029
|
+
finally:
|
|
2030
|
+
self._unmatched_value_na = saved
|
|
2031
|
+
|
|
2032
|
+
def _emit_loop_with_assign(
|
|
2033
|
+
self,
|
|
2034
|
+
node,
|
|
2035
|
+
target: str,
|
|
2036
|
+
lines: list[str],
|
|
2037
|
+
indent: int,
|
|
2038
|
+
target_cpp_type: str | None = None,
|
|
2039
|
+
reset: bool = True,
|
|
2040
|
+
) -> None:
|
|
2041
|
+
"""A loop that is a function's or an if/switch arm's last statement.
|
|
2042
|
+
|
|
2043
|
+
TradingView makes it the value its body's last statement produced on
|
|
2044
|
+
the last iteration that reached that statement -- a ``break`` or
|
|
2045
|
+
``continue`` before it keeps the previous value -- and ``na`` when
|
|
2046
|
+
none did (lab tv probes pf-w2-f04_loop_tail / f04_loop_edges).
|
|
2047
|
+
"""
|
|
2048
|
+
visit = {
|
|
2049
|
+
ForStmt: self._visit_for,
|
|
2050
|
+
ForInStmt: self._visit_for_in,
|
|
2051
|
+
WhileStmt: self._visit_while,
|
|
2052
|
+
}[type(node)]
|
|
2053
|
+
value_cpp = self._loop_value_cpp_type(node, target_cpp_type)
|
|
2054
|
+
if value_cpp is None:
|
|
2055
|
+
# No scalar or string value to leave (a drawing call, a void
|
|
2056
|
+
# call, a handle): the loop is the plain statement it always was.
|
|
2057
|
+
visit(node, lines, indent)
|
|
2058
|
+
return
|
|
2059
|
+
if reset:
|
|
2060
|
+
pad = " " * indent
|
|
2061
|
+
lines.append(f"{pad}{target} = {self._na_value_for_type(value_cpp)};")
|
|
2062
|
+
visit(node, lines, indent, value_target=(target, target_cpp_type))
|
|
2063
|
+
|
|
2064
|
+
def _unmatched_arm_na(self, target: str) -> str | None:
|
|
2065
|
+
"""The na a value ``target`` takes when an if/switch assigning it runs
|
|
2066
|
+
no arm (a loop's value, ``_visit_selection_value``), or None outside
|
|
2067
|
+
such a value position."""
|
|
2068
|
+
active = getattr(self, "_unmatched_value_na", None)
|
|
2069
|
+
if active is not None and active[0] == target:
|
|
2070
|
+
return active[1]
|
|
2071
|
+
return None
|
|
2072
|
+
|
|
2073
|
+
_UNMATCHED_NA_SLOTS = ("double", "int", "int64_t", "std::string", "bool")
|
|
2074
|
+
|
|
2075
|
+
def _visit_selection_value(
|
|
2076
|
+
self,
|
|
2077
|
+
node,
|
|
2078
|
+
target: str,
|
|
2079
|
+
lines: list[str],
|
|
2080
|
+
indent: int,
|
|
2081
|
+
target_cpp_type: str | None = None,
|
|
2082
|
+
slot_cpp_type: str | None = None,
|
|
2083
|
+
) -> None:
|
|
2084
|
+
"""An if/switch whose value a declaration, a reassignment or a
|
|
2085
|
+
function's last statement takes.
|
|
2086
|
+
|
|
2087
|
+
TradingView: an if without else, an else-if chain without a final
|
|
2088
|
+
else or a switch without default is na when no arm runs -- a numeric
|
|
2089
|
+
or string na, false for a bool -- as a function's last statement,
|
|
2090
|
+
nested in a taken arm, and as the value a global, reassigned or local
|
|
2091
|
+
variable takes, which does not keep its previous bar's value (lab tv
|
|
2092
|
+
probe pf-oi-if-tail-na). ``slot_cpp_type`` is the target's C++ type;
|
|
2093
|
+
a handle or collection slot keeps its existing lowering. An ``int``
|
|
2094
|
+
an epoch reaches is stored ``int64_t`` (``target_cpp_type``, the
|
|
2095
|
+
slot's integer width), and its na is the 64-bit sentinel: ``na<int>()``
|
|
2096
|
+
widened there is a value, not na (quirk 9).
|
|
2097
|
+
"""
|
|
2098
|
+
saved = getattr(self, "_unmatched_value_na", None)
|
|
2099
|
+
if slot_cpp_type == "int" and target_cpp_type == "int64_t":
|
|
2100
|
+
slot_cpp_type = "int64_t"
|
|
2101
|
+
if slot_cpp_type in self._UNMATCHED_NA_SLOTS:
|
|
2102
|
+
self._unmatched_value_na = (
|
|
2103
|
+
target, self._na_value_for_type(slot_cpp_type))
|
|
2104
|
+
try:
|
|
2105
|
+
self._visit_if_switch_expr(
|
|
2106
|
+
node, target, lines, indent, target_cpp_type=target_cpp_type,
|
|
2107
|
+
)
|
|
2108
|
+
finally:
|
|
2109
|
+
self._unmatched_value_na = saved
|
|
2110
|
+
|
|
2111
|
+
def _na_value_for_type(self, cpp_type: str | None) -> str:
|
|
2112
|
+
"""``na`` of a value slot: a numeric or string na, else its default."""
|
|
2113
|
+
if cpp_type is None:
|
|
2114
|
+
return "na<double>()"
|
|
2115
|
+
if cpp_type in ("double", "int", "int64_t", "std::string"):
|
|
2116
|
+
return f"na<{cpp_type}>()"
|
|
2117
|
+
return self._default_for_type(cpp_type)
|
|
2118
|
+
|
|
2119
|
+
_TAIL_SCALAR_CPP = ("double", "int", "int64_t", "bool")
|
|
2120
|
+
|
|
2121
|
+
def _tail_value_cpp_type(self, stmt) -> str | None:
|
|
2122
|
+
"""C++ type of the value a value-producing last statement leaves, or
|
|
2123
|
+
None when it is a drawing, UDT or collection handle (or unknown).
|
|
2124
|
+
|
|
2125
|
+
Judged from the statement itself -- a declaration's type hint and
|
|
2126
|
+
right-hand side, an assignment's target and right-hand side -- so the
|
|
2127
|
+
answer does not depend on whether its local has been emitted yet.
|
|
2128
|
+
"""
|
|
2129
|
+
if isinstance(stmt, ExprStmt):
|
|
2130
|
+
parts, typed = [stmt.expr], stmt.expr
|
|
2131
|
+
elif isinstance(stmt, VarDecl) and stmt.name:
|
|
2132
|
+
parts, typed = [stmt.value], stmt.value
|
|
2133
|
+
if stmt.type_hint:
|
|
2134
|
+
spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
2135
|
+
if spec is None or spec.kind != "primitive":
|
|
2136
|
+
return None
|
|
2137
|
+
return self._type_spec_to_cpp(spec)
|
|
2138
|
+
elif (isinstance(stmt, Assignment)
|
|
2139
|
+
and isinstance(stmt.target, (Identifier, MemberAccess))):
|
|
2140
|
+
parts, typed = [stmt.target, stmt.value], stmt.target
|
|
2141
|
+
else:
|
|
2142
|
+
return None
|
|
2143
|
+
for part in parts:
|
|
2144
|
+
if part is None:
|
|
2145
|
+
return None
|
|
2146
|
+
spec = self._type_spec_from_expr(part)
|
|
2147
|
+
if spec is not None and spec.kind != "primitive":
|
|
2148
|
+
return None
|
|
2149
|
+
value_cpp = self._infer_type(typed)
|
|
2150
|
+
if value_cpp in self._TAIL_SCALAR_CPP or value_cpp == "std::string":
|
|
2151
|
+
return value_cpp
|
|
2152
|
+
return None
|
|
2153
|
+
|
|
2154
|
+
def _tail_value_fits(self, stmt, slot_cpp_type: str | None) -> bool:
|
|
2155
|
+
"""Whether the value a last statement leaves fits the slot it feeds.
|
|
2156
|
+
|
|
2157
|
+
Only a scalar (numeric or bool) or a string is returned; a drawing,
|
|
2158
|
+
UDT or collection handle keeps the statement-then-default lowering,
|
|
2159
|
+
which is what every such tail compiled to before the value was
|
|
2160
|
+
returned. ``slot_cpp_type`` None is a double or string slot.
|
|
2161
|
+
"""
|
|
2162
|
+
value_cpp = self._tail_value_cpp_type(stmt)
|
|
2163
|
+
if value_cpp is None:
|
|
2164
|
+
return False
|
|
2165
|
+
if slot_cpp_type is None:
|
|
2166
|
+
return True
|
|
2167
|
+
if slot_cpp_type in self._TAIL_SCALAR_CPP:
|
|
2168
|
+
return value_cpp in self._TAIL_SCALAR_CPP
|
|
2169
|
+
return slot_cpp_type == "std::string" == value_cpp
|
|
2170
|
+
|
|
2171
|
+
def _loop_value_cpp_type(self, node, slot_cpp_type: str | None) -> str | None:
|
|
2172
|
+
"""C++ type of the value a loop leaves, when its body's last statement
|
|
2173
|
+
yields one that fits ``slot_cpp_type``; None otherwise."""
|
|
2174
|
+
return self._block_value_cpp_type(
|
|
2175
|
+
getattr(node, "body", None) or [], slot_cpp_type
|
|
2176
|
+
)
|
|
2177
|
+
|
|
2178
|
+
def _block_value_cpp_type(self, body: list, slot_cpp_type: str | None) -> str | None:
|
|
2179
|
+
"""C++ type of the value a block's last statement yields into
|
|
2180
|
+
``slot_cpp_type`` (through nested loops and if/switch arms), or None
|
|
2181
|
+
when it yields none that fits."""
|
|
2182
|
+
if not body:
|
|
2183
|
+
return None
|
|
2184
|
+
last = body[-1]
|
|
2185
|
+
if isinstance(last, (ForStmt, ForInStmt, WhileStmt)):
|
|
2186
|
+
return self._block_value_cpp_type(last.body, slot_cpp_type)
|
|
2187
|
+
if isinstance(last, IfStmt):
|
|
2188
|
+
arms = [last.body] + ([last.else_body] if last.else_body else [])
|
|
2189
|
+
elif isinstance(last, SwitchStmt):
|
|
2190
|
+
arms = [body_ for _, body_ in last.cases] + (
|
|
2191
|
+
[last.default_body] if last.default_body else [])
|
|
2192
|
+
else:
|
|
2193
|
+
arms = None
|
|
2194
|
+
if arms is not None:
|
|
2195
|
+
# Every arm must yield a fitting value of one type; a missing
|
|
2196
|
+
# else/default arm is na on its path.
|
|
2197
|
+
found = {self._block_value_cpp_type(arm, slot_cpp_type) for arm in arms}
|
|
2198
|
+
return found.pop() if len(found) == 1 and None not in found else None
|
|
2199
|
+
if self._statement_value_node(last) is None and not isinstance(last, ExprStmt):
|
|
2200
|
+
return None
|
|
2201
|
+
if isinstance(last, ExprStmt) and (
|
|
2202
|
+
self._call_is_void(last.expr) or self._is_skip_expr(last.expr)):
|
|
2203
|
+
return None
|
|
2204
|
+
if not self._tail_value_fits(last, slot_cpp_type):
|
|
2205
|
+
return None
|
|
2206
|
+
return slot_cpp_type or self._tail_value_cpp_type(last)
|
|
2207
|
+
|
|
2208
|
+
@staticmethod
|
|
2209
|
+
def _statement_value_node(stmt):
|
|
2210
|
+
"""The node whose value a value-producing statement leaves, or None.
|
|
2211
|
+
|
|
2212
|
+
TradingView makes the last statement of a function or an if/switch
|
|
2213
|
+
arm its value: ``x := e`` and ``x += e`` (every compound operator)
|
|
2214
|
+
yield the target's new value, ``obj.f := e`` the field's, and a
|
|
2215
|
+
declaration ``[var] [T] x = e`` the declared variable's (lab tv probes
|
|
2216
|
+
pf-w2-f04_tails / f04_decl_tail / f04_var_decl_tail).
|
|
2217
|
+
"""
|
|
2218
|
+
if isinstance(stmt, Assignment) and isinstance(
|
|
2219
|
+
stmt.target, (Identifier, MemberAccess)):
|
|
2220
|
+
return stmt.target
|
|
2221
|
+
if isinstance(stmt, VarDecl) and stmt.name:
|
|
2222
|
+
node = Identifier(name=stmt.name)
|
|
2223
|
+
node.loc = stmt.loc
|
|
2224
|
+
return node
|
|
2225
|
+
return None
|
|
2226
|
+
|
|
1711
2227
|
def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
|
|
1712
2228
|
pad = " " * indent
|
|
1713
2229
|
if node.expr:
|
|
@@ -1723,7 +2239,9 @@ class StmtVisitor:
|
|
|
1723
2239
|
else:
|
|
1724
2240
|
for i, (case_expr, case_body) in enumerate(node.cases):
|
|
1725
2241
|
prefix = "if" if i == 0 else "else if"
|
|
1726
|
-
cond = self.
|
|
2242
|
+
cond = self._coerce_bool_expr(
|
|
2243
|
+
self._visit_expr(case_expr), case_expr
|
|
2244
|
+
)
|
|
1727
2245
|
lines.append(f"{pad}{prefix} ({cond}) {{")
|
|
1728
2246
|
self._visit_block_statements(case_body, lines, indent + 1)
|
|
1729
2247
|
lines.append(f"{pad}}}")
|
|
@@ -1765,9 +2283,13 @@ class StmtVisitor:
|
|
|
1765
2283
|
if self._call_is_void(stmt.expr):
|
|
1766
2284
|
self._visit_stmt(stmt, lines, indent)
|
|
1767
2285
|
return
|
|
1768
|
-
cpp = self.
|
|
2286
|
+
cpp = self._coerce_int_slot(
|
|
2287
|
+
self._visit_rhs_value(
|
|
2288
|
+
stmt.expr,
|
|
2289
|
+
target_cpp_type=target_cpp_type,
|
|
2290
|
+
),
|
|
1769
2291
|
stmt.expr,
|
|
1770
|
-
target_cpp_type
|
|
2292
|
+
self._int_slot_cpp_type(None, target_cpp_type),
|
|
1771
2293
|
)
|
|
1772
2294
|
pad = " " * indent
|
|
1773
2295
|
lines.append(f"{pad}{target} = {cpp};")
|
|
@@ -1788,6 +2310,26 @@ class StmtVisitor:
|
|
|
1788
2310
|
indent,
|
|
1789
2311
|
target_cpp_type=target_cpp_type,
|
|
1790
2312
|
)
|
|
2313
|
+
elif (self._statement_value_node(stmt) is not None
|
|
2314
|
+
and self._tail_value_fits(stmt, target_cpp_type)):
|
|
2315
|
+
# ``y := v * 5`` / ``y += v`` / ``float y = v`` as the
|
|
2316
|
+
# value: run the statement, then read what it left.
|
|
2317
|
+
self._visit_stmt(stmt, lines, indent)
|
|
2318
|
+
value = self._statement_value_node(stmt)
|
|
2319
|
+
cpp = self._coerce_int_slot(
|
|
2320
|
+
self._visit_rhs_value(
|
|
2321
|
+
value, target_cpp_type=target_cpp_type,
|
|
2322
|
+
),
|
|
2323
|
+
value,
|
|
2324
|
+
self._int_slot_cpp_type(None, target_cpp_type),
|
|
2325
|
+
)
|
|
2326
|
+
pad = " " * indent
|
|
2327
|
+
lines.append(f"{pad}{target} = {cpp};")
|
|
2328
|
+
elif isinstance(stmt, (ForStmt, ForInStmt, WhileStmt)):
|
|
2329
|
+
self._emit_loop_with_assign(
|
|
2330
|
+
stmt, target, lines, indent,
|
|
2331
|
+
target_cpp_type=target_cpp_type,
|
|
2332
|
+
)
|
|
1791
2333
|
else:
|
|
1792
2334
|
self._visit_stmt(stmt, lines, indent)
|
|
1793
2335
|
else:
|
|
@@ -1816,7 +2358,9 @@ class StmtVisitor:
|
|
|
1816
2358
|
lines.append(f"{pad}}}")
|
|
1817
2359
|
|
|
1818
2360
|
if isinstance(node, IfStmt):
|
|
1819
|
-
cond = self.
|
|
2361
|
+
cond = self._coerce_bool_expr(
|
|
2362
|
+
self._visit_expr(node.condition), node.condition
|
|
2363
|
+
)
|
|
1820
2364
|
lines.append(f"{pad}if ({cond}) {{")
|
|
1821
2365
|
self._emit_block_with_assign(
|
|
1822
2366
|
node.body,
|
|
@@ -1852,6 +2396,10 @@ class StmtVisitor:
|
|
|
1852
2396
|
# non-var globals and reassignments: retaining the prior bar's
|
|
1853
2397
|
# map/matrix ID would turn the expression into implicit state.
|
|
1854
2398
|
emit_implicit_na_fallback()
|
|
2399
|
+
elif self._unmatched_arm_na(target) is not None:
|
|
2400
|
+
lines.append(f"{pad}else {{")
|
|
2401
|
+
lines.append(f"{pad} {target} = {self._unmatched_arm_na(target)};")
|
|
2402
|
+
lines.append(f"{pad}}}")
|
|
1855
2403
|
elif isinstance(node, SwitchStmt):
|
|
1856
2404
|
if node.expr:
|
|
1857
2405
|
expr_var = f"__switch_val_{self._switch_counter}"
|
|
@@ -1872,7 +2420,9 @@ class StmtVisitor:
|
|
|
1872
2420
|
else:
|
|
1873
2421
|
for i, (case_expr, case_body) in enumerate(node.cases):
|
|
1874
2422
|
prefix = "if" if i == 0 else "else if"
|
|
1875
|
-
cond = self.
|
|
2423
|
+
cond = self._coerce_bool_expr(
|
|
2424
|
+
self._visit_expr(case_expr), case_expr
|
|
2425
|
+
)
|
|
1876
2426
|
lines.append(f"{pad}{prefix} ({cond}) {{")
|
|
1877
2427
|
self._emit_block_with_assign(
|
|
1878
2428
|
case_body,
|
|
@@ -1908,3 +2458,11 @@ class StmtVisitor:
|
|
|
1908
2458
|
lines.append(
|
|
1909
2459
|
f"{pad}{target} = {target_cpp_type}{{}};"
|
|
1910
2460
|
)
|
|
2461
|
+
elif self._unmatched_arm_na(target) is not None:
|
|
2462
|
+
na_value = self._unmatched_arm_na(target)
|
|
2463
|
+
if node.cases:
|
|
2464
|
+
lines.append(f"{pad}else {{")
|
|
2465
|
+
lines.append(f"{pad} {target} = {na_value};")
|
|
2466
|
+
lines.append(f"{pad}}}")
|
|
2467
|
+
else:
|
|
2468
|
+
lines.append(f"{pad}{target} = {na_value};")
|