@pineforge/codegen-pyodide 0.10.4 → 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 (50) 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 +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  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 +896 -156
  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 +946 -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 +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  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-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.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
@@ -530,6 +545,37 @@ class StmtVisitor:
530
545
  lines.append(f"{pad} {flag_expr} = true;")
531
546
  lines.append(f"{pad}}}")
532
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
533
579
  if target_cpp_type is not None:
534
580
  init_cpp = self._visit_rhs_value(
535
581
  node.value,
@@ -545,6 +591,10 @@ class StmtVisitor:
545
591
  init_cpp = self._typed_na_init(
546
592
  init_cpp, member_name, info["ptype"]
547
593
  )
594
+ init_cpp = self._coerce_int_slot(
595
+ init_cpp, node.value,
596
+ self._int_slot_cpp_type(member_name),
597
+ )
548
598
  lines.append(f"{pad}if (!{flag_expr}) {{")
549
599
  # A history-referenced persistent primitive is a Series<T> too,
550
600
  # not just a drawing handle. The per-bar carry has already
@@ -660,10 +710,17 @@ class StmtVisitor:
660
710
  )
661
711
  elem_spec = spec.element or elem_spec
662
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])
663
716
  if len(node.value.args) >= 2:
664
717
  r = self._visit_expr(node.value.args[0])
665
718
  c = self._visit_expr(node.value.args[1])
666
- 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
+ )
667
724
  init = f"{cpp_type}::new_({r}, {c}, {v})"
668
725
  else:
669
726
  init = f"{cpp_type}::new_(0, 0, {self._default_for_spec(elem_spec)})"
@@ -738,12 +795,24 @@ class StmtVisitor:
738
795
  site = self._get_ta_site(node.value)
739
796
  if site is not None:
740
797
  compute_args = self._ta_compute_args_for_site(site)
741
- 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
+ )
742
804
  ta_name = self._ta_member_name(site)
743
805
  ta_expr = (
744
806
  f"(history_advances_new_bar() ? {ta_name}.compute({compute_args}) "
745
807
  f": {ta_name}.recompute({compute_args}))"
746
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
+ )
747
816
  if declaration_is_series:
748
817
  self._emit_history_series_write(lines, pad, safe, ta_expr)
749
818
  elif is_global_member:
@@ -764,6 +833,10 @@ class StmtVisitor:
764
833
  else None
765
834
  ),
766
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
+ )
767
840
  self._emit_history_series_write(lines, pad, safe, cpp_val)
768
841
  return
769
842
 
@@ -791,17 +864,25 @@ class StmtVisitor:
791
864
  node.name,
792
865
  cpp_type,
793
866
  )
867
+ if selection_cpp_type is None:
868
+ selection_cpp_type = self._int_slot_cpp_type(
869
+ node.name, cpp_type,
870
+ )
794
871
  if not is_global_member:
795
872
  default = self._default_for_type(cpp_type)
796
873
  lines.append(f"{pad}{cpp_type} {safe} = {default};")
797
874
  remember_local_type(cpp_type)
798
875
  indent = len(pad) // 4
799
- self._visit_if_switch_expr(
876
+ self._visit_selection_value(
800
877
  node.value,
801
878
  safe,
802
879
  lines,
803
880
  indent,
804
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
+ ),
805
886
  )
806
887
  return
807
888
 
@@ -851,6 +932,12 @@ class StmtVisitor:
851
932
  node.name,
852
933
  target_cpp_type=target_cpp_type,
853
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
+ )
854
941
  if is_global_member:
855
942
  lines.append(f"{pad}{safe} = {cpp_val};")
856
943
  else:
@@ -983,13 +1070,16 @@ class StmtVisitor:
983
1070
  target_name,
984
1071
  None,
985
1072
  )
1073
+ if selection_cpp_type is None:
1074
+ selection_cpp_type = self._int_slot_cpp_type(target_name)
986
1075
  indent = len(pad) // 4
987
- self._visit_if_switch_expr(
1076
+ self._visit_selection_value(
988
1077
  node.value,
989
1078
  safe,
990
1079
  lines,
991
1080
  indent,
992
1081
  target_cpp_type=selection_cpp_type,
1082
+ slot_cpp_type=self._infer_type(node.target),
993
1083
  )
994
1084
  return
995
1085
 
@@ -1022,13 +1112,28 @@ class StmtVisitor:
1022
1112
  val_cpp = self._visit_rhs_value(
1023
1113
  node.value, target_cpp_type=target_cpp_type
1024
1114
  )
1025
- 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)
1026
1125
  lines.append(f"{pad}{target_cpp} = {val_cpp};")
1027
1126
  else:
1028
1127
  rhs = self._compound_assign_rhs(target_cpp, node.op, val_cpp)
1029
1128
  if rhs is not None:
1129
+ rhs = self._coerce_int_slot(
1130
+ rhs, node.value, field_int, value_is_double=True,
1131
+ )
1030
1132
  lines.append(f"{pad}{target_cpp} = {rhs};")
1031
1133
  else:
1134
+ val_cpp = self._coerce_int_slot(
1135
+ val_cpp, node.value, field_int,
1136
+ )
1032
1137
  lines.append(f"{pad}{target_cpp} {node.op} {val_cpp};")
1033
1138
  return
1034
1139
 
@@ -1046,17 +1151,27 @@ class StmtVisitor:
1046
1151
  None,
1047
1152
  ),
1048
1153
  )
1154
+ elem_int = self._int_slot_cpp_type(
1155
+ None, self._series_type_for(target_name),
1156
+ )
1049
1157
  if node.op == ":=":
1158
+ val_cpp = self._coerce_int_slot(val_cpp, node.value, elem_int)
1050
1159
  lines.append(f"{pad}{safe}.update({val_cpp});")
1051
1160
  else:
1052
1161
  rhs = self._compound_assign_rhs(f"{safe}[0]", node.op, val_cpp)
1053
1162
  if rhs is not None:
1054
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
+ )
1055
1167
  lines.append(f"{pad}{safe}.update({rhs});")
1056
1168
  else:
1057
1169
  # Compound assignment: x += y → x.update(x[0] + y)
1058
1170
  op_char = node.op[0] # e.g., "+" from "+="
1059
- 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});")
1060
1175
  elif target_name in self._var_names:
1061
1176
  # A bare-``na`` reassignment must adopt the target's declared scalar
1062
1177
  # type (``x := na`` -> ``na<int>()`` not ``na<double>()``); otherwise
@@ -1069,14 +1184,30 @@ class StmtVisitor:
1069
1184
  if tct is None and self._is_na_expr(node.value):
1070
1185
  tct = self._na_reassign_cpp_type(target_name)
1071
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)
1072
1188
  if node.op == ":=":
1189
+ val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
1073
1190
  lines.append(f"{pad}{safe} = {val_cpp};")
1074
1191
  else:
1075
1192
  rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
1076
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
+ )
1077
1199
  lines.append(f"{pad}{safe} = {rhs};")
1078
1200
  else:
1079
- 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};")
1080
1211
  else:
1081
1212
  tct = self._nullable_collection_target_cpp_type(name=target_name)
1082
1213
  if tct is None:
@@ -1084,14 +1215,30 @@ class StmtVisitor:
1084
1215
  if tct is None and self._is_na_expr(node.value):
1085
1216
  tct = self._na_reassign_cpp_type(target_name)
1086
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)
1087
1219
  if node.op == ":=":
1220
+ val_cpp = self._coerce_int_slot(val_cpp, node.value, int_slot)
1088
1221
  lines.append(f"{pad}{safe} = {val_cpp};")
1089
1222
  else:
1090
1223
  rhs = self._compound_assign_rhs(safe, node.op, val_cpp)
1091
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
+ )
1092
1230
  lines.append(f"{pad}{safe} = {rhs};")
1093
1231
  else:
1094
- 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};")
1095
1242
 
1096
1243
  def _visit_tuple_assign(self, node: TupleAssign, lines: list[str], pad: str) -> None:
1097
1244
  is_top_level = any(id(node) == id(stmt) for stmt in self.ctx.ast.body)
@@ -1152,6 +1299,14 @@ class StmtVisitor:
1152
1299
  and len(tuple_types) == tuple_size
1153
1300
  and all(item == PineType.BOOL for item in tuple_types)
1154
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
+ )
1155
1310
  exact_direct_tuple = (
1156
1311
  isinstance(expr_node, TupleLiteral)
1157
1312
  and tuple_size >= 2
@@ -1177,6 +1332,7 @@ class StmtVisitor:
1177
1332
  if (
1178
1333
  numeric_tuple
1179
1334
  or bool_tuple
1335
+ or mixed_helper_tuple
1180
1336
  or exact_direct_tuple
1181
1337
  or known_ta_tuple
1182
1338
  ):
@@ -1193,6 +1349,10 @@ class StmtVisitor:
1193
1349
  the tuple once whenever either case applies; ordinary lexical scalar
1194
1350
  elements remain locals, Series elements advance their remapped
1195
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.
1196
1356
  """
1197
1357
  series_names = {
1198
1358
  name
@@ -1201,7 +1361,11 @@ class StmtVisitor:
1201
1361
  and self._decl_binding_is_series(id(node), name)
1202
1362
  }
1203
1363
  if not series_names and not global_targets.intersection(node.names):
1204
- 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
+ )
1205
1369
  lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
1206
1370
  return
1207
1371
 
@@ -1270,14 +1434,38 @@ class StmtVisitor:
1270
1434
  f"{pad}{self._safe_name(name)} = {field_expr};"
1271
1435
  )
1272
1436
  else:
1273
- 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)
1274
1457
  return
1275
1458
 
1276
1459
  # User-defined function returning a tuple: use C++17 structured bindings
1277
1460
  if isinstance(node.value, FuncCall):
1278
1461
  func_name, namespace = self._resolve_callee(node.value.callee)
1279
1462
  if namespace == "request" and func_name == "security":
1280
- 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)
1281
1469
  emit_call_tuple(call_expr)
1282
1470
  return
1283
1471
  if func_name and namespace is None and func_name in self._func_names:
@@ -1311,6 +1499,13 @@ class StmtVisitor:
1311
1499
 
1312
1500
  lines.append(f"{pad}/* unsupported tuple assignment */")
1313
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
+
1314
1509
  def _tuple_binding_cpp_types(self, node: TupleAssign) -> list[str]:
1315
1510
  """Exact supported tuple element types for later lexical operations."""
1316
1511
  count = len(node.names)
@@ -1477,7 +1672,9 @@ class StmtVisitor:
1477
1672
  def _visit_if_body(self, node: IfStmt, lines: list[str], indent: int) -> None:
1478
1673
  pad = " " * indent
1479
1674
 
1480
- cond = self._visit_expr(node.condition)
1675
+ cond = self._coerce_bool_expr(
1676
+ self._visit_expr(node.condition), node.condition
1677
+ )
1481
1678
  lines.append(f"{pad}if ({cond}) {{")
1482
1679
  self._visit_block_statements(node.body, lines, indent + 1)
1483
1680
  lines.append(f"{pad}}}")
@@ -1492,7 +1689,8 @@ class StmtVisitor:
1492
1689
  )
1493
1690
  lines.append(f"{pad}}}")
1494
1691
 
1495
- 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:
1496
1694
  pad = " " * indent
1497
1695
  start = self._visit_expr(node.start)
1498
1696
  end = self._visit_expr(node.end)
@@ -1518,35 +1716,69 @@ class StmtVisitor:
1518
1716
  )
1519
1717
  )
1520
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
+ )
1521
1734
  if end_eval is not None:
1522
1735
  # The ``to`` expression is authored outside the loop-binder scope,
1523
1736
  # but its refresh executes inside the generated C++ ``for`` where
1524
1737
  # the binder would shadow a same-named outer member/parameter. A
1525
1738
  # pre-binder lambda preserves the authored lexical binding while
1526
1739
  # still reevaluating the expression after every iteration.
1527
- lines.append(f"{pad}auto {end_eval} = [&]() {{ return ({end}); }};")
1528
- lines.append(f"{pad}int {s_var} = ({start});")
1529
- 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
1530
1743
  lines.append(f"{pad}int {e_var} = {end_expr};")
1531
- lines.append(f"{pad}int {step_var} = ({step});")
1532
- 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};")
1533
1754
  lines.append(f"{pad}if ({step_var} == 0) {step_var} = 1;")
1534
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}"
1535
1763
  lines.append(
1536
1764
  f"{pad}for (int {var} = {s_var}; "
1765
+ f"{na_guard}"
1537
1766
  f"({down_var} ? ({var} >= {e_var}) : ({var} <= {e_var})); "
1538
- f"{var} += ({down_var} ? -{step_var} : {step_var}), "
1539
- f"{e_var} = {end_expr}) {{"
1767
+ f"{var} += ({down_var} ? -{step_var} : {step_var})"
1768
+ f"{refresh}) {{"
1540
1769
  )
1541
1770
  # Register the loop counter so reads of it inside the body resolve (the
1542
1771
  # unknown-identifier guard in _visit_ident would otherwise flag it).
1543
1772
  saved_loop = self._current_loop_vars
1544
1773
  saved_loop_specs = self._current_loop_var_specs
1774
+ saved_counted = self._current_counted_loop_vars
1545
1775
  self._current_loop_vars = set(self._current_loop_vars)
1546
1776
  self._current_loop_var_specs = dict(self._current_loop_var_specs)
1777
+ self._current_counted_loop_vars = set(self._current_counted_loop_vars)
1547
1778
  if node.var:
1548
1779
  self._current_loop_vars.add(node.var)
1549
1780
  self._current_loop_var_specs[node.var] = TypeSpec.primitive("int")
1781
+ self._current_counted_loop_vars.add(node.var)
1550
1782
  _blk_saved = self._push_block_var_remap(node)
1551
1783
  if node.var:
1552
1784
  # The loop counter is a fresh primitive lexical binding. Keep it
@@ -1556,21 +1788,26 @@ class StmtVisitor:
1556
1788
  self._lexical_series_bindings[node.var] = False
1557
1789
  self._lexical_known_var_tombstones.add(node.var)
1558
1790
  try:
1559
- for s in node.body:
1560
- self._visit_stmt(s, lines, indent + 1)
1791
+ self._emit_loop_body(node.body, lines, indent + 1, value_target)
1561
1792
  finally:
1562
1793
  self._pop_block_var_remap(_blk_saved)
1563
1794
  self._current_loop_vars = saved_loop
1564
1795
  self._current_loop_var_specs = saved_loop_specs
1796
+ self._current_counted_loop_vars = saved_counted
1565
1797
  lines.append(f"{pad}}}")
1566
1798
 
1567
- 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:
1568
1801
  pad = " " * indent
1569
1802
  iterable = self._visit_expr(node.iterable)
1570
1803
  saved_loop = self._current_loop_vars
1571
1804
  saved_loop_specs = self._current_loop_var_specs
1805
+ saved_counted = self._current_counted_loop_vars
1572
1806
  self._current_loop_vars = set(self._current_loop_vars)
1573
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 ())})
1574
1811
  iterable_spec = self._type_spec_from_expr(node.iterable)
1575
1812
  elem_spec = (
1576
1813
  iterable_spec.element
@@ -1581,16 +1818,23 @@ class StmtVisitor:
1581
1818
  self._current_loop_vars.add(node.var)
1582
1819
  if elem_spec is not None:
1583
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)
1584
1824
  if node.vars:
1585
1825
  tuple_specs: list[TypeSpec | None] = []
1586
1826
  if iterable_spec is not None and iterable_spec.kind == "map":
1587
1827
  tuple_specs = [iterable_spec.key, iterable_spec.value]
1828
+ elif elem_spec is not None:
1829
+ tuple_specs = [TypeSpec.primitive("int"), elem_spec]
1588
1830
  for idx, v in enumerate(node.vars):
1589
1831
  if v != "_":
1590
1832
  self._current_loop_vars.add(v)
1591
1833
  if (idx < len(tuple_specs)
1592
1834
  and tuple_specs[idx] is not None):
1593
1835
  self._current_loop_var_specs[v] = tuple_specs[idx]
1836
+ else:
1837
+ self._current_loop_var_specs.pop(v, None)
1594
1838
  map_pair_loop = (
1595
1839
  iterable_spec is not None
1596
1840
  and iterable_spec.kind == "map"
@@ -1656,8 +1900,46 @@ class StmtVisitor:
1656
1900
  lines.append(
1657
1901
  f"{pad} auto {value_cpp} = {map_token}.get({key_cpp});"
1658
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}];")
1659
1939
  elif node.vars:
1660
- bindings = ", ".join(node.vars)
1940
+ bindings = ", ".join(
1941
+ self._safe_name(name) for name in node.vars
1942
+ )
1661
1943
  lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
1662
1944
  _blk_saved = self._push_block_var_remap(node)
1663
1945
  loop_binding_names = (
@@ -1700,26 +1982,248 @@ class StmtVisitor:
1700
1982
  self._lexical_series_bindings[name] = False
1701
1983
  self._lexical_known_var_tombstones.add(name)
1702
1984
  try:
1703
- for s in node.body:
1704
- self._visit_stmt(s, lines, indent + 1)
1985
+ self._emit_loop_body(node.body, lines, indent + 1, value_target)
1705
1986
  finally:
1706
1987
  self._pop_block_var_remap(_blk_saved)
1707
1988
  lines.append(f"{pad}}}")
1708
1989
  self._current_loop_vars = saved_loop
1709
1990
  self._current_loop_var_specs = saved_loop_specs
1991
+ self._current_counted_loop_vars = saved_counted
1710
1992
 
1711
- 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:
1712
1995
  pad = " " * indent
1713
- cond = self._visit_expr(node.condition)
1996
+ cond = self._coerce_bool_expr(
1997
+ self._visit_expr(node.condition), node.condition
1998
+ )
1714
1999
  lines.append(f"{pad}while ({cond}) {{")
1715
2000
  _blk_saved = self._push_block_var_remap(node)
1716
2001
  try:
1717
- for s in node.body:
1718
- self._visit_stmt(s, lines, indent + 1)
2002
+ self._emit_loop_body(node.body, lines, indent + 1, value_target)
1719
2003
  finally:
1720
2004
  self._pop_block_var_remap(_blk_saved)
1721
2005
  lines.append(f"{pad}}}")
1722
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
+
1723
2227
  def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
1724
2228
  pad = " " * indent
1725
2229
  if node.expr:
@@ -1735,7 +2239,9 @@ class StmtVisitor:
1735
2239
  else:
1736
2240
  for i, (case_expr, case_body) in enumerate(node.cases):
1737
2241
  prefix = "if" if i == 0 else "else if"
1738
- cond = self._visit_expr(case_expr)
2242
+ cond = self._coerce_bool_expr(
2243
+ self._visit_expr(case_expr), case_expr
2244
+ )
1739
2245
  lines.append(f"{pad}{prefix} ({cond}) {{")
1740
2246
  self._visit_block_statements(case_body, lines, indent + 1)
1741
2247
  lines.append(f"{pad}}}")
@@ -1777,9 +2283,13 @@ class StmtVisitor:
1777
2283
  if self._call_is_void(stmt.expr):
1778
2284
  self._visit_stmt(stmt, lines, indent)
1779
2285
  return
1780
- 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
+ ),
1781
2291
  stmt.expr,
1782
- target_cpp_type=target_cpp_type,
2292
+ self._int_slot_cpp_type(None, target_cpp_type),
1783
2293
  )
1784
2294
  pad = " " * indent
1785
2295
  lines.append(f"{pad}{target} = {cpp};")
@@ -1800,6 +2310,26 @@ class StmtVisitor:
1800
2310
  indent,
1801
2311
  target_cpp_type=target_cpp_type,
1802
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
+ )
1803
2333
  else:
1804
2334
  self._visit_stmt(stmt, lines, indent)
1805
2335
  else:
@@ -1828,7 +2358,9 @@ class StmtVisitor:
1828
2358
  lines.append(f"{pad}}}")
1829
2359
 
1830
2360
  if isinstance(node, IfStmt):
1831
- cond = self._visit_expr(node.condition)
2361
+ cond = self._coerce_bool_expr(
2362
+ self._visit_expr(node.condition), node.condition
2363
+ )
1832
2364
  lines.append(f"{pad}if ({cond}) {{")
1833
2365
  self._emit_block_with_assign(
1834
2366
  node.body,
@@ -1864,6 +2396,10 @@ class StmtVisitor:
1864
2396
  # non-var globals and reassignments: retaining the prior bar's
1865
2397
  # map/matrix ID would turn the expression into implicit state.
1866
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}}}")
1867
2403
  elif isinstance(node, SwitchStmt):
1868
2404
  if node.expr:
1869
2405
  expr_var = f"__switch_val_{self._switch_counter}"
@@ -1884,7 +2420,9 @@ class StmtVisitor:
1884
2420
  else:
1885
2421
  for i, (case_expr, case_body) in enumerate(node.cases):
1886
2422
  prefix = "if" if i == 0 else "else if"
1887
- cond = self._visit_expr(case_expr)
2423
+ cond = self._coerce_bool_expr(
2424
+ self._visit_expr(case_expr), case_expr
2425
+ )
1888
2426
  lines.append(f"{pad}{prefix} ({cond}) {{")
1889
2427
  self._emit_block_with_assign(
1890
2428
  case_body,
@@ -1920,3 +2458,11 @@ class StmtVisitor:
1920
2458
  lines.append(
1921
2459
  f"{pad}{target} = {target_cpp_type}{{}};"
1922
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};")