@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.
Files changed (51) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +767 -77
  7. package/pineforge_codegen/analyzer/call_handlers.py +268 -42
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +907 -159
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +961 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1198 -101
  25. package/pineforge_codegen/codegen/tables.py +205 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +2055 -62
  28. package/pineforge_codegen/codegen/visit_call.py +929 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +765 -63
  30. package/pineforge_codegen/codegen/visit_stmt.py +610 -52
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen/symbols.py +4 -2
  47. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  48. package/release.json +2 -2
  49. package/tables.json +23 -21
  50. package/transpile.worker.mjs +24 -16
  51. 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": ("max_intraday_filled_orders_", "int"),
350
- "max_drawdown": ("risk_max_drawdown_", "double"),
351
- "max_intraday_loss": ("risk_max_intraday_loss_", "double"),
352
- "max_position_size": ("risk_max_position_size_", "double"),
353
- "max_cons_loss_days": ("risk_max_cons_loss_days_", "int"),
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
- lines.append(f"{pad}risk_direction_ = RiskDirection::LONG_ONLY;")
366
+ direction = "1"
359
367
  elif val == "-1":
360
- lines.append(f"{pad}risk_direction_ = RiskDirection::SHORT_ONLY;")
368
+ direction = "-1"
361
369
  else:
362
- lines.append(f"{pad}risk_direction_ = RiskDirection::BOTH;")
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
- member, cast_type = _RISK_MEMBER_MAP[risk_func]
374
+ setter, cast_type = _RISK_MEMBER_MAP[risk_func]
366
375
  val = self._visit_expr(node.expr.args[0])
367
- lines.append(f"{pad}{member} = ({cast_type})({val});")
368
- # Handle percent_of_equity flag for max_drawdown / max_intraday_loss
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
- if is_pct:
376
- pct_flag = "risk_max_drawdown_is_pct_" if risk_func == "max_drawdown" else "risk_max_intraday_loss_is_pct_"
377
- lines.append(f"{pad}{pct_flag} = true;")
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
- init = self._visit_expr(node.value)
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 = self._visit_expr(node.value.args[2]) if len(node.value.args) > 2 else self._default_for_spec(elem_spec)
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 = "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL else "double"
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._visit_if_switch_expr(
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._visit_if_switch_expr(
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
- if node.op == ":=":
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
- lines.append(f"{pad}{safe}.update({safe}[0] {op_char} {val_cpp});")
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
- lines.append(f"{pad}{safe} {node.op} {val_cpp};")
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
- lines.append(f"{pad}{safe} {node.op} {val_cpp};")
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(node.names)
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(f"{pad}double {name} = {field_expr};")
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
- call_expr = self._visit_func_call(node.value)
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._visit_expr(node.condition)
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) -> None:
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 ({end}); }};")
1516
- lines.append(f"{pad}int {s_var} = ({start});")
1517
- end_expr = f"{end_eval}()" if end_eval is not None else f"({end})"
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} = ({step});")
1520
- lines.append(f"{pad}if ({step_var} < 0) {step_var} = -{step_var};")
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"{e_var} = {end_expr}) {{"
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
- for s in node.body:
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) -> None:
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(node.vars)
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
- for s in node.body:
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) -> None:
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._visit_expr(node.condition)
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
- for s in node.body:
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._visit_expr(case_expr)
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._visit_rhs_value(
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=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._visit_expr(node.condition)
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._visit_expr(case_expr)
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};")