pineforge-codegen 0.6.5__py3-none-any.whl

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 (35) hide show
  1. pineforge_codegen/__init__.py +53 -0
  2. pineforge_codegen/analyzer/__init__.py +60 -0
  3. pineforge_codegen/analyzer/base.py +1563 -0
  4. pineforge_codegen/analyzer/call_handlers.py +895 -0
  5. pineforge_codegen/analyzer/contracts.py +163 -0
  6. pineforge_codegen/analyzer/diagnostics.py +118 -0
  7. pineforge_codegen/analyzer/tables.py +204 -0
  8. pineforge_codegen/analyzer/types.py +250 -0
  9. pineforge_codegen/ast_nodes.py +293 -0
  10. pineforge_codegen/codegen/__init__.py +78 -0
  11. pineforge_codegen/codegen/base.py +1381 -0
  12. pineforge_codegen/codegen/emit_top.py +875 -0
  13. pineforge_codegen/codegen/helpers.py +163 -0
  14. pineforge_codegen/codegen/helpers_syminfo.py +134 -0
  15. pineforge_codegen/codegen/input.py +189 -0
  16. pineforge_codegen/codegen/security.py +1564 -0
  17. pineforge_codegen/codegen/ta.py +298 -0
  18. pineforge_codegen/codegen/tables.py +613 -0
  19. pineforge_codegen/codegen/types.py +573 -0
  20. pineforge_codegen/codegen/visit_call.py +1305 -0
  21. pineforge_codegen/codegen/visit_expr.py +701 -0
  22. pineforge_codegen/codegen/visit_stmt.py +729 -0
  23. pineforge_codegen/errors.py +98 -0
  24. pineforge_codegen/lexer.py +531 -0
  25. pineforge_codegen/parser.py +1198 -0
  26. pineforge_codegen/pragmas.py +117 -0
  27. pineforge_codegen/signatures.py +808 -0
  28. pineforge_codegen/support_checker.py +1111 -0
  29. pineforge_codegen/symbols.py +118 -0
  30. pineforge_codegen/tokens.py +406 -0
  31. pineforge_codegen/tv_input_choices.py +86 -0
  32. pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
  33. pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
  34. pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
  35. pineforge_codegen-0.6.5.dist-info/licenses/LICENSE +197 -0
@@ -0,0 +1,729 @@
1
+ """Statement-level visitors for the codegen.
2
+
3
+ ``StmtVisitor`` holds the statement-level visitors that recurse into a
4
+ function body or top-level program. ``_visit_stmt`` is the central
5
+ dispatcher: it inspects the AST node kind and delegates to one of the
6
+ statement-kind handlers (``_visit_var_decl``, ``_visit_assignment``,
7
+ ``_visit_tuple_assign``, ``_visit_if``, ``_visit_for``,
8
+ ``_visit_for_in``, ``_visit_while``, ``_visit_switch``) or emits a
9
+ trivial ``break;`` / ``continue;`` / expression statement directly.
10
+ The two if/switch-as-expression helpers (``_emit_body_with_assign``
11
+ and ``_visit_if_switch_expr``) live alongside the other visitors
12
+ because they recurse back into ``_visit_stmt`` for nested control
13
+ flow.
14
+
15
+ These visitors were extracted from ``base.py``'s ``CodeGen`` class as
16
+ step 8 of the codegen package refactor; behaviour is preserved
17
+ verbatim. The mixin owns no state of its own — it reads/writes only
18
+ attributes already established on the host class (``CodeGen``).
19
+
20
+ Mixin contract — host class must provide the following attributes
21
+ (all set by ``CodeGen.__init__`` or other mixins):
22
+
23
+ - ``self.ctx`` (``AnalyzerContext``): symbol table source. Reads
24
+ ``ctx.series_vars`` to decide between ``Series<T>::push`` /
25
+ ``Series<T>::update`` and a plain assignment.
26
+ - ``self._var_names`` (``set[str]``): names declared at module scope
27
+ (used to drive the assignment lowering).
28
+ - ``self._global_member_vars`` (``set[str]``): non-``var`` global
29
+ declarations emitted as class members (assignment-only path in
30
+ ``_visit_var_decl``).
31
+ - ``self._array_vars`` / ``self._map_vars`` (``set[str]``) and
32
+ ``self._matrix_specs`` (``dict[str, TypeSpec]``): collection-typed
33
+ variables; ``_visit_var_decl``
34
+ registers new entries when it sees ``array.new`` / ``map.new`` /
35
+ ``matrix.new``.
36
+ - ``self._collection_types`` (``dict[str, TypeSpec]``):
37
+ ``_visit_var_decl`` populates it from inferred specs.
38
+ - ``self._active_var_remap`` (``dict[str, str]``): per-call-site
39
+ rename map for cloned function-local var/series names.
40
+ - ``self._in_ta_func_variant`` (``bool``): set during per-call-site
41
+ function emission; gates the TA-hoist branch in ``_visit_if``.
42
+ - ``self._current_loop_vars`` (``set[str]``): for-in iterator names;
43
+ saved/restored around ``_visit_for_in`` bodies so member-access
44
+ resolution can distinguish iterators from enum constants.
45
+ - ``self._switch_counter`` (``int``): monotonically incremented by
46
+ ``_visit_switch`` / ``_visit_if_switch_expr`` to mint fresh
47
+ ``__switch_val_<n>`` temporary names.
48
+ - ``self._func_names`` (``set[str]``): user-defined function names;
49
+ consulted by ``_visit_tuple_assign`` to spot tuple-returning calls.
50
+
51
+ Sibling-mixin methods consumed via ``self``:
52
+
53
+ - ``NamingHelper`` (``codegen/helpers.py``): ``_safe_name``,
54
+ ``_resolve_callee``, ``_get_target_name``.
55
+ - ``TypeInferer`` (``codegen/types.py``): ``_type_for_decl``,
56
+ ``_type_spec_to_cpp``, ``_default_for_type``,
57
+ ``_type_spec_from_expr``, ``_array_spec_for_name``,
58
+ ``_map_spec_for_name``.
59
+ - ``TaSiteHelper`` (``codegen/ta.py``): ``_get_ta_site``,
60
+ ``_ta_member_name``, ``_ta_compute_args_for_site``,
61
+ ``_ta_name_from_site``, ``_if_body_has_ta``, ``_hoist_if_body``.
62
+ - ``InputHelper`` (``codegen/input.py``): ``_is_input_call``,
63
+ ``_get_input_default``, ``_get_input_title``,
64
+ ``_input_type_to_getter``,
65
+ ``_enforce_enum_declared_before_input_enum``.
66
+ - ``CodeGen.base``: ``_visit_expr``, ``_visit_func_call``,
67
+ ``_is_skip_expr`` (still on the host class — the expression
68
+ visitors and the skip-expression predicate are extracted in later
69
+ refactor steps).
70
+
71
+ The mixin avoids importing from ``base.py`` to stay free of cycles;
72
+ all tables it needs come from ``codegen/tables.py`` and all AST
73
+ classes from ``..ast_nodes``.
74
+ """
75
+
76
+ from __future__ import annotations
77
+
78
+ from ..ast_nodes import (
79
+ ASTNode,
80
+ Assignment,
81
+ BreakStmt,
82
+ ContinueStmt,
83
+ EnumDecl,
84
+ ExprStmt,
85
+ ForInStmt,
86
+ ForStmt,
87
+ FuncCall,
88
+ FuncDef,
89
+ Identifier,
90
+ IfStmt,
91
+ ImportStmt,
92
+ MemberAccess,
93
+ MethodDef,
94
+ StrategyDecl,
95
+ SwitchStmt,
96
+ TupleAssign,
97
+ TypeDecl,
98
+ VarDecl,
99
+ WhileStmt,
100
+ )
101
+ from ..symbols import TypeSpec
102
+ from .tables import (
103
+ SKIP_VAR_TYPES,
104
+ TA_RETURNS_BOOL,
105
+ TA_TUPLE_FIELDS,
106
+ MATRIX_RETURNING_METHODS,
107
+ )
108
+
109
+
110
+ class StmtVisitor:
111
+ """Statement-level visitor methods shared across the codegen.
112
+
113
+ Mixed into ``CodeGen``; not intended to be instantiated standalone.
114
+ See the module docstring for the full host-class state contract."""
115
+
116
+ # ------------------------------------------------------------------
117
+ # Statement visitors
118
+ # ------------------------------------------------------------------
119
+
120
+ def _visit_stmt(self, node: ASTNode, lines: list[str], indent: int) -> None:
121
+ pad = " " * indent
122
+
123
+ if isinstance(node, StrategyDecl):
124
+ return
125
+ if isinstance(node, ImportStmt):
126
+ return
127
+ if isinstance(node, FuncDef):
128
+ return # handled separately as class methods
129
+ if isinstance(node, TypeDecl):
130
+ return # handled in struct emission
131
+ if isinstance(node, EnumDecl):
132
+ return # handled in enum constant emission
133
+ if isinstance(node, MethodDef):
134
+ return # handled as class method via FuncInfo
135
+ if isinstance(node, VarDecl):
136
+ self._visit_var_decl(node, lines, pad)
137
+ elif isinstance(node, Assignment):
138
+ self._visit_assignment(node, lines, pad)
139
+ elif isinstance(node, TupleAssign):
140
+ self._visit_tuple_assign(node, lines, pad)
141
+ elif isinstance(node, IfStmt):
142
+ self._visit_if(node, lines, indent)
143
+ elif isinstance(node, ForStmt):
144
+ self._visit_for(node, lines, indent)
145
+ elif isinstance(node, ForInStmt):
146
+ self._visit_for_in(node, lines, indent)
147
+ elif isinstance(node, WhileStmt):
148
+ self._visit_while(node, lines, indent)
149
+ elif isinstance(node, SwitchStmt):
150
+ self._visit_switch(node, lines, indent)
151
+ elif isinstance(node, BreakStmt):
152
+ lines.append(f"{pad}break;")
153
+ elif isinstance(node, ContinueStmt):
154
+ lines.append(f"{pad}continue;")
155
+ elif isinstance(node, ExprStmt):
156
+ # Intercept strategy.risk.* calls
157
+ if isinstance(node.expr, FuncCall) and isinstance(node.expr.callee, MemberAccess):
158
+ c = node.expr.callee
159
+ if (isinstance(c.object, MemberAccess) and isinstance(c.object.object, Identifier)
160
+ and c.object.object.name == "strategy" and c.object.member == "risk"
161
+ and node.expr.args):
162
+ risk_func = c.member
163
+ _RISK_MEMBER_MAP = {
164
+ "max_intraday_filled_orders": ("max_intraday_filled_orders_", "int"),
165
+ "max_drawdown": ("risk_max_drawdown_", "double"),
166
+ "max_intraday_loss": ("risk_max_intraday_loss_", "double"),
167
+ "max_position_size": ("risk_max_position_size_", "double"),
168
+ "max_cons_loss_days": ("risk_max_cons_loss_days_", "int"),
169
+ }
170
+ if risk_func == "allow_entry_in":
171
+ val = self._visit_expr(node.expr.args[0])
172
+ if val == "1":
173
+ lines.append(f"{pad}risk_direction_ = RiskDirection::LONG_ONLY;")
174
+ elif val == "-1":
175
+ lines.append(f"{pad}risk_direction_ = RiskDirection::SHORT_ONLY;")
176
+ else:
177
+ lines.append(f"{pad}risk_direction_ = RiskDirection::BOTH;")
178
+ return
179
+ if risk_func in _RISK_MEMBER_MAP:
180
+ member, cast_type = _RISK_MEMBER_MAP[risk_func]
181
+ val = self._visit_expr(node.expr.args[0])
182
+ lines.append(f"{pad}{member} = ({cast_type})({val});")
183
+ # Handle percent_of_equity flag for max_drawdown / max_intraday_loss
184
+ if risk_func in ("max_drawdown", "max_intraday_loss") and len(node.expr.args) >= 2:
185
+ arg2 = node.expr.args[1]
186
+ is_pct = (isinstance(arg2, MemberAccess)
187
+ and isinstance(arg2.object, Identifier)
188
+ and arg2.object.name == "strategy"
189
+ and arg2.member == "percent_of_equity")
190
+ if is_pct:
191
+ pct_flag = "risk_max_drawdown_is_pct_" if risk_func == "max_drawdown" else "risk_max_intraday_loss_is_pct_"
192
+ lines.append(f"{pad}{pct_flag} = true;")
193
+ return
194
+ if self._is_skip_expr(node.expr):
195
+ return
196
+ # matrix.concat / m.concat as a statement: engine concat returns a
197
+ # new matrix and is marked [[nodiscard]]. Pine semantics is mutate
198
+ # the first argument. Capture the result back into the receiver so
199
+ # we get the mutation AND avoid the warning.
200
+ recv_for_concat = self._concat_receiver_name(node.expr)
201
+ if recv_for_concat is not None:
202
+ cpp = self._visit_expr(node.expr)
203
+ target = self._safe_name(recv_for_concat)
204
+ lines.append(f"{pad}{target} = {cpp};")
205
+ return
206
+ cpp = self._visit_expr(node.expr)
207
+ if cpp.startswith("/* "):
208
+ return
209
+ # Never emit a bare invalid C++ token (e.g. type names leaked as statements).
210
+ stripped = cpp.strip()
211
+ if stripped == "color" or stripped.startswith("(int64_t)pine_color::"):
212
+ return
213
+ lines.append(f"{pad}{cpp};")
214
+
215
+ def _concat_receiver_name(self, expr) -> str | None:
216
+ """If ``expr`` is a Pine ``matrix.concat`` call (in either method
217
+ form ``m.concat(other, ...)`` or namespaced form
218
+ ``matrix.concat(m, other, ...)``) on a known matrix variable,
219
+ return the receiver variable name. Otherwise return None.
220
+
221
+ Engine ``PineGenericMatrix::concat`` is ``[[nodiscard]]`` and Pine
222
+ semantics is mutate-receiver, so the statement form must be lowered
223
+ to ``recv = recv.concat(...);``.
224
+ """
225
+ if not isinstance(expr, FuncCall) or not isinstance(expr.callee, MemberAccess):
226
+ return None
227
+ callee = expr.callee
228
+ if callee.member != "concat":
229
+ return None
230
+ # m.concat(other, ...) — receiver is callee.object
231
+ if isinstance(callee.object, Identifier):
232
+ recv = callee.object.name
233
+ if recv in getattr(self, "_matrix_specs", {}):
234
+ return recv
235
+ # matrix.concat(m, other, ...) — receiver is first arg
236
+ if (isinstance(callee.object, Identifier)
237
+ and callee.object.name == "matrix"
238
+ and expr.args
239
+ and isinstance(expr.args[0], Identifier)):
240
+ recv = expr.args[0].name
241
+ if recv in getattr(self, "_matrix_specs", {}):
242
+ return recv
243
+ return None
244
+
245
+ def _visit_var_decl(self, node: VarDecl, lines: list[str], pad: str) -> None:
246
+ # var/varip — handled as members in on_bar preamble
247
+ if node.is_var or node.is_varip:
248
+ return
249
+
250
+ safe = self._safe_name(node.name)
251
+ # Apply per-call-site var remap (for function-local vars)
252
+ if self._active_var_remap and safe in self._active_var_remap:
253
+ safe = self._active_var_remap[safe]
254
+ # Global-scope non-var vars are class members — emit assignment, not declaration
255
+ is_global_member = node.name in self._global_member_vars
256
+
257
+ # Check if it is a static (non-series) global member variable already evaluated inside _inputs_initialized_ block
258
+ is_static_global_input = False
259
+ if is_global_member and isinstance(node.value, FuncCall) and self._is_input_call(node.value):
260
+ func_name_i, namespace_i = self._resolve_callee(node.value.callee)
261
+ is_static_global_input = (
262
+ func_name_i != "source"
263
+ and node.name not in self._array_vars
264
+ and node.name not in getattr(self, "_matrix_specs", {})
265
+ and node.name not in getattr(self, "_map_vars", {})
266
+ and not node.is_var
267
+ and not node.is_varip
268
+ )
269
+
270
+ if is_static_global_input:
271
+ # Skip, already evaluated in _inputs_initialized_ block!
272
+ return
273
+
274
+ # input() call — emit runtime get_input_*() lookup
275
+ if isinstance(node.value, FuncCall) and self._is_input_call(node.value):
276
+ func_name_i, namespace_i = self._resolve_callee(node.value.callee)
277
+
278
+ if namespace_i == "input" and func_name_i == "enum":
279
+ self._enforce_enum_declared_before_input_enum(node.value)
280
+ title = self._get_input_title(node.value, var_name=node.name)
281
+ cpp_val = self._render_input_value(node.value, func_name_i, namespace_i, title)
282
+ if node.name in self.ctx.series_vars:
283
+ lines.append(f"{pad}{safe}.push({cpp_val});")
284
+ elif is_global_member:
285
+ lines.append(f"{pad}{safe} = {cpp_val};")
286
+ else:
287
+ cpp_type = self._type_for_decl(node)
288
+ lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
289
+ return
290
+
291
+ # Array variable declarations: array.new<T>(), array.from(), array.new_float() etc.
292
+ if isinstance(node.value, FuncCall):
293
+ func_name, namespace = self._resolve_callee(node.value.callee)
294
+ if namespace == "array" and func_name in ("new", "new_float", "new_int", "new_bool", "new_string", "from"):
295
+ self._array_vars.add(node.name)
296
+ spec = self._type_spec_from_expr(node.value) or self._array_spec_for_name(node.name)
297
+ self._collection_types.setdefault(node.name, spec)
298
+ init = self._visit_expr(node.value)
299
+ cpp_type = self._type_spec_to_cpp(spec)
300
+ if is_global_member:
301
+ lines.append(f"{pad}{safe} = {init};")
302
+ else:
303
+ lines.append(f"{pad}{cpp_type} {safe} = {init};")
304
+ return
305
+
306
+ # Map variable declarations: map.new<K,V>()
307
+ if isinstance(node.value, FuncCall):
308
+ func_name, namespace = self._resolve_callee(node.value.callee)
309
+ if namespace == "matrix" and func_name == "new":
310
+ targs = self._template_args_from_call(node.value) if hasattr(node.value, "annotations") else []
311
+ elem_spec = self._type_spec_from_hint_name(targs[0]) if targs else TypeSpec.primitive("float")
312
+ spec = TypeSpec.matrix(elem_spec)
313
+ self._matrix_specs[node.name] = spec
314
+ self._collection_types[node.name] = spec
315
+ cpp_type = self._type_spec_to_cpp(spec)
316
+ if len(node.value.args) >= 2:
317
+ r = self._visit_expr(node.value.args[0])
318
+ c = self._visit_expr(node.value.args[1])
319
+ v = self._visit_expr(node.value.args[2]) if len(node.value.args) > 2 else self._default_for_spec(elem_spec)
320
+ init = f"{cpp_type}::new_({r}, {c}, {v})"
321
+ else:
322
+ init = f"{cpp_type}::new_(0, 0, {self._default_for_spec(elem_spec)})"
323
+ if is_global_member:
324
+ lines.append(f"{pad}{safe} = {init};")
325
+ else:
326
+ lines.append(f"{pad}{cpp_type} {safe} = {init};")
327
+ return
328
+ # ``var inv = matrix.inv(m)`` — RHS is a matrix-returning method
329
+ # (inv / pinv / transpose / copy / submatrix / concat / diff /
330
+ # mult / pow / eigenvectors / kron). Without this branch the LHS
331
+ # falls through to the analyzer's default ``double`` and clang
332
+ # rejects ``double = PineMatrix``. The RHS expression itself is
333
+ # already lowered to the right ``m.inv()`` form by visit_call.
334
+ if namespace == "matrix" and func_name in MATRIX_RETURNING_METHODS:
335
+ recv_spec = self._matrix_specs.get(self._extract_receiver_name(node.value))
336
+ if recv_spec is None:
337
+ recv_spec = TypeSpec.matrix(TypeSpec.primitive("float"))
338
+ self._matrix_specs[node.name] = recv_spec
339
+ self._collection_types[node.name] = recv_spec
340
+ init = self._visit_expr(node.value)
341
+ cpp_type = self._type_spec_to_cpp(recv_spec)
342
+ if is_global_member:
343
+ lines.append(f"{pad}{safe} = {init};")
344
+ else:
345
+ lines.append(f"{pad}{cpp_type} {safe} = {init};")
346
+ return
347
+ if namespace == "map" and func_name == "new":
348
+ self._map_vars.add(node.name)
349
+ spec = self._type_spec_from_expr(node.value) or self._map_spec_for_name(node.name)
350
+ self._collection_types.setdefault(node.name, spec)
351
+ cpp_type = self._type_spec_to_cpp(spec)
352
+ if is_global_member:
353
+ lines.append(f"{pad}{safe} = {cpp_type}();")
354
+ else:
355
+ lines.append(f"{pad}{cpp_type} {safe};")
356
+ return
357
+
358
+ # Skip visual function assignments — but still emit declaration for
359
+ # table function results since the var may be used later
360
+ if isinstance(node.value, FuncCall) and self._is_skip_expr(node.value):
361
+ func_name, namespace = self._resolve_callee(node.value.callee)
362
+ if namespace in SKIP_VAR_TYPES:
363
+ # Emit var with default value so references don't fail
364
+ if not is_global_member:
365
+ cpp_type = self._type_for_decl(node)
366
+ default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
367
+ lines.append(f"{pad}{cpp_type} {safe} = {default};")
368
+ return
369
+
370
+ # TA call
371
+ site = self._get_ta_site(node.value)
372
+ if site is not None:
373
+ compute_args = self._ta_compute_args_for_site(site)
374
+ ret_type = "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL else "double"
375
+ ta_name = self._ta_member_name(site)
376
+ ta_expr = f"(is_first_tick_ ? {ta_name}.compute({compute_args}) : {ta_name}.recompute({compute_args}))"
377
+ if node.name in self.ctx.series_vars:
378
+ lines.append(f"{pad}{safe}.push({ta_expr});")
379
+ elif is_global_member:
380
+ lines.append(f"{pad}{safe} = {ta_expr};")
381
+ else:
382
+ lines.append(f"{pad}{ret_type} {safe} = {ta_expr};")
383
+ return
384
+
385
+ # Non-var series variable — push instead of declare
386
+ if node.name in self.ctx.series_vars:
387
+ cpp_val = self._visit_expr(node.value)
388
+ lines.append(f"{pad}{safe}.push({cpp_val});")
389
+ return
390
+
391
+ # If/switch expression: x = if cond ... else ...
392
+ if isinstance(node.value, (IfStmt, SwitchStmt)):
393
+ if not is_global_member:
394
+ cpp_type = self._type_for_decl(node)
395
+ default = self._default_for_type(cpp_type)
396
+ lines.append(f"{pad}{cpp_type} {safe} = {default};")
397
+ indent = len(pad) // 4
398
+ self._visit_if_switch_expr(node.value, safe, lines, indent)
399
+ return
400
+
401
+ # General declaration
402
+ cpp_val = self._visit_expr(node.value)
403
+ if is_global_member:
404
+ lines.append(f"{pad}{safe} = {cpp_val};")
405
+ else:
406
+ cpp_type = self._type_for_decl(node)
407
+ lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
408
+
409
+ def _visit_assignment(self, node: Assignment, lines: list[str], pad: str) -> None:
410
+ if isinstance(node.value, FuncCall) and self._is_skip_expr(node.value):
411
+ return
412
+
413
+ # If/switch expression in assignment: x := if cond ...
414
+ if isinstance(node.value, (IfStmt, SwitchStmt)):
415
+ target_name = self._get_target_name(node.target)
416
+ safe = self._safe_name(target_name) if target_name else self._visit_expr(node.target)
417
+ indent = len(pad) // 4
418
+ self._visit_if_switch_expr(node.value, safe, lines, indent)
419
+ return
420
+
421
+ # Get target name
422
+ target_name = self._get_target_name(node.target)
423
+ if target_name is None:
424
+ # Assignment to a UDT field that was dropped from the emitted
425
+ # struct because it had a drawing-only type (label/line/box/
426
+ # linefill/polyline/table/chart.point). The struct has no such
427
+ # member, so emit a placeholder comment instead of a real C++
428
+ # assignment. We intentionally do NOT visit the RHS here: drawing
429
+ # constructors (label.new / line.new / ...) live in
430
+ # SKIP_NAMESPACES, so they have no observable side effects in
431
+ # the backtest runtime. See: pineforge-codegen issue #10.
432
+ if self._is_omitted_udt_field(node.target):
433
+ recv = self._visit_expr(node.target.object)
434
+ lines.append(
435
+ f"{pad}/* drawing field assignment omitted: "
436
+ f"{recv}.{node.target.member} {node.op} ... */"
437
+ )
438
+ return
439
+ # General expression target (e.g., member access)
440
+ target_cpp = self._visit_expr(node.target)
441
+ val_cpp = self._visit_expr(node.value)
442
+ if node.op == ":=":
443
+ lines.append(f"{pad}{target_cpp} = {val_cpp};")
444
+ else:
445
+ lines.append(f"{pad}{target_cpp} {node.op} {val_cpp};")
446
+ return
447
+
448
+ safe = self._safe_name(target_name)
449
+ # Apply per-call-site var remap (for function-local vars)
450
+ if self._active_var_remap and safe in self._active_var_remap:
451
+ safe = self._active_var_remap[safe]
452
+
453
+ if target_name in self.ctx.series_vars:
454
+ val_cpp = self._visit_expr(node.value)
455
+ if node.op == ":=":
456
+ lines.append(f"{pad}{safe}.update({val_cpp});")
457
+ else:
458
+ # Compound assignment: x += y → x.update(x[0] + y)
459
+ op_char = node.op[0] # e.g., "+" from "+="
460
+ lines.append(f"{pad}{safe}.update({safe}[0] {op_char} {self._visit_expr(node.value)});")
461
+ elif target_name in self._var_names:
462
+ if node.op == ":=" and target_name in self._matrix_specs and isinstance(node.value, FuncCall):
463
+ rhs_fn, rhs_ns = self._resolve_callee(node.value.callee)
464
+ rhs_spec = None
465
+ if rhs_ns == "matrix" and rhs_fn == "new":
466
+ targs = self._template_args_from_call(node.value) if hasattr(node.value, "annotations") else []
467
+ elem = self._type_spec_from_hint_name(targs[0]) if targs else TypeSpec.primitive("float")
468
+ rhs_spec = TypeSpec.matrix(elem)
469
+ elif rhs_ns == "matrix" and rhs_fn in MATRIX_RETURNING_METHODS:
470
+ rcv = self._extract_receiver_name(node.value)
471
+ rhs_spec = self._matrix_specs.get(rcv)
472
+ if rhs_spec is not None:
473
+ lhs_spec = self._matrix_specs[target_name]
474
+ if rhs_spec.element != lhs_spec.element:
475
+ self._codegen_error(
476
+ node,
477
+ f"matrix '{target_name}' element type mismatch on reassignment: "
478
+ f"expected {self._type_spec_to_cpp(lhs_spec)}, "
479
+ f"got {self._type_spec_to_cpp(rhs_spec)}",
480
+ )
481
+ val_cpp = self._visit_expr(node.value)
482
+ if node.op == ":=":
483
+ lines.append(f"{pad}{safe} = {val_cpp};")
484
+ else:
485
+ lines.append(f"{pad}{safe} {node.op} {val_cpp};")
486
+ else:
487
+ val_cpp = self._visit_expr(node.value)
488
+ if node.op == ":=":
489
+ lines.append(f"{pad}{safe} = {val_cpp};")
490
+ else:
491
+ lines.append(f"{pad}{safe} {node.op} {val_cpp};")
492
+
493
+ def _visit_tuple_assign(self, node: TupleAssign, lines: list[str], pad: str) -> None:
494
+ site = self._get_ta_site(node.value)
495
+ if site is not None:
496
+ compute_args = self._ta_compute_args_for_site(site)
497
+ ta_mem = self._ta_member_name(site)
498
+ result_var = f"_result_{ta_mem}"
499
+ lines.append(f"{pad}auto {result_var} = (is_first_tick_ ? {ta_mem}.compute({compute_args}) : {ta_mem}.recompute({compute_args}));")
500
+
501
+ ta_name = self._ta_name_from_site(site)
502
+ fields = TA_TUPLE_FIELDS.get(ta_name, [f"field{i}" for i in range(len(node.names))])
503
+
504
+ for i, name in enumerate(node.names):
505
+ if name == "_":
506
+ continue
507
+ if i < len(fields):
508
+ lines.append(f"{pad}double {name} = {result_var}.{fields[i]};")
509
+ return
510
+
511
+ # User-defined function returning a tuple: use C++17 structured bindings
512
+ if isinstance(node.value, FuncCall):
513
+ func_name, namespace = self._resolve_callee(node.value.callee)
514
+ if namespace == "request" and func_name == "security":
515
+ binding_names = ", ".join(n for n in node.names if n != "_")
516
+ call_expr = self._visit_func_call(node.value)
517
+ lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
518
+ return
519
+ if func_name and namespace is None and func_name in self._func_names:
520
+ binding_names = ", ".join(node.names)
521
+ call_expr = self._visit_func_call(node.value)
522
+ lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
523
+ return
524
+
525
+ # UDT instance method returning a tuple: ``[a, b, c] = receiver.method(...)``.
526
+ # The plain-function branch above misses this because _resolve_callee
527
+ # returns ``("method", "receiver")`` for ``recv.method(...)``, not
528
+ # ``(key, None)``. We resolve the receiver's UDT type and look up
529
+ # the method-key ``TypeName.methodName`` in the FuncInfo map; when
530
+ # its FuncInfo carries ``returns_tuple=True`` we know
531
+ # ``_visit_func_call`` has already lowered the call as
532
+ # ``_udt_TypeName_method(receiver, ...)`` returning
533
+ # ``std::tuple<...>``, so structured bindings drop in.
534
+ # Probe: data/validation/udt-method-probe-17-tuple-return-destructure.
535
+ callee = node.value.callee
536
+ if isinstance(callee, MemberAccess):
537
+ recv_spec = self._type_spec_from_expr(callee.object)
538
+ if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
539
+ method_key = f"{recv_spec.name}.{callee.member}"
540
+ fi_u = self._func_info_map.get(method_key)
541
+ if (fi_u is not None
542
+ and getattr(fi_u, "is_udt_method", False)
543
+ and getattr(fi_u, "returns_tuple", False)):
544
+ binding_names = ", ".join(node.names)
545
+ call_expr = self._visit_func_call(node.value)
546
+ lines.append(f"{pad}auto [{binding_names}] = {call_expr};")
547
+ return
548
+
549
+ lines.append(f"{pad}/* unsupported tuple assignment */")
550
+
551
+ def _visit_if(self, node: IfStmt, lines: list[str], indent: int) -> None:
552
+ pad = " " * indent
553
+
554
+ # TA hoisting: inside per-call-site function variants, execute ALL
555
+ # statements unconditionally (PineScript execution model) but wrap
556
+ # the result assignment in the condition.
557
+ if self._in_ta_func_variant and self._if_body_has_ta(node.body):
558
+ cond = self._visit_expr(node.condition)
559
+ self._hoist_if_body(node.body, cond, lines, pad, indent)
560
+ # Handle else_body similarly
561
+ if node.else_body:
562
+ if len(node.else_body) == 1 and isinstance(node.else_body[0], IfStmt):
563
+ self._visit_if(node.else_body[0], lines, indent)
564
+ else:
565
+ neg_cond = f"!({cond})"
566
+ self._hoist_if_body(node.else_body, neg_cond, lines, pad, indent)
567
+ return
568
+
569
+ cond = self._visit_expr(node.condition)
570
+ lines.append(f"{pad}if ({cond}) {{")
571
+ for s in node.body:
572
+ self._visit_stmt(s, lines, indent + 1)
573
+ lines.append(f"{pad}}}")
574
+ if node.else_body:
575
+ if len(node.else_body) == 1 and isinstance(node.else_body[0], IfStmt):
576
+ lines[-1] = f"{pad}}} else"
577
+ self._visit_if(node.else_body[0], lines, indent)
578
+ else:
579
+ lines[-1] = f"{pad}}} else {{"
580
+ for s in node.else_body:
581
+ self._visit_stmt(s, lines, indent + 1)
582
+ lines.append(f"{pad}}}")
583
+
584
+ def _visit_for(self, node: ForStmt, lines: list[str], indent: int) -> None:
585
+ pad = " " * indent
586
+ start = self._visit_expr(node.start)
587
+ end = self._visit_expr(node.end)
588
+ step = self._visit_expr(node.step) if node.step else "1"
589
+ var = node.var # new AST uses .var instead of .var_name
590
+ lines.append(f"{pad}for (int {var} = {start}; {var} <= {end}; {var} += {step}) {{")
591
+ # Register the loop counter so reads of it inside the body resolve (the
592
+ # unknown-identifier guard in _visit_ident would otherwise flag it).
593
+ saved_loop = self._current_loop_vars
594
+ self._current_loop_vars = set(self._current_loop_vars)
595
+ if var:
596
+ self._current_loop_vars.add(var)
597
+ for s in node.body:
598
+ self._visit_stmt(s, lines, indent + 1)
599
+ self._current_loop_vars = saved_loop
600
+ lines.append(f"{pad}}}")
601
+
602
+ def _visit_for_in(self, node, lines: list[str], indent: int) -> None:
603
+ pad = " " * indent
604
+ iterable = self._visit_expr(node.iterable)
605
+ saved_loop = self._current_loop_vars
606
+ self._current_loop_vars = set(self._current_loop_vars)
607
+ if node.var:
608
+ self._current_loop_vars.add(node.var)
609
+ if node.vars:
610
+ for v in node.vars:
611
+ if v != "_":
612
+ self._current_loop_vars.add(v)
613
+ if node.var:
614
+ v_cpp = self._safe_name(node.var)
615
+ lines.append(f"{pad}for (auto {v_cpp} : {iterable}) {{")
616
+ elif node.vars:
617
+ bindings = ", ".join(node.vars)
618
+ lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
619
+ for s in node.body:
620
+ self._visit_stmt(s, lines, indent + 1)
621
+ lines.append(f"{pad}}}")
622
+ self._current_loop_vars = saved_loop
623
+
624
+ def _visit_while(self, node: WhileStmt, lines: list[str], indent: int) -> None:
625
+ pad = " " * indent
626
+ cond = self._visit_expr(node.condition)
627
+ lines.append(f"{pad}while ({cond}) {{")
628
+ for s in node.body:
629
+ self._visit_stmt(s, lines, indent + 1)
630
+ lines.append(f"{pad}}}")
631
+
632
+ def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
633
+ pad = " " * indent
634
+ if node.expr:
635
+ expr_var = f"__switch_val_{self._switch_counter}"
636
+ self._switch_counter += 1
637
+ lines.append(f"{pad}auto {expr_var} = {self._visit_expr(node.expr)};")
638
+ for i, (case_expr, case_body) in enumerate(node.cases):
639
+ prefix = "if" if i == 0 else "else if"
640
+ case_val = self._visit_expr(case_expr)
641
+ lines.append(f"{pad}{prefix} ({expr_var} == {case_val}) {{")
642
+ for s in case_body:
643
+ self._visit_stmt(s, lines, indent + 1)
644
+ lines.append(f"{pad}}}")
645
+ else:
646
+ for i, (case_expr, case_body) in enumerate(node.cases):
647
+ prefix = "if" if i == 0 else "else if"
648
+ cond = self._visit_expr(case_expr)
649
+ lines.append(f"{pad}{prefix} ({cond}) {{")
650
+ for s in case_body:
651
+ self._visit_stmt(s, lines, indent + 1)
652
+ lines.append(f"{pad}}}")
653
+
654
+ if node.default_body:
655
+ lines.append(f"{pad}else {{")
656
+ for s in node.default_body:
657
+ self._visit_stmt(s, lines, indent + 1)
658
+ lines.append(f"{pad}}}")
659
+
660
+ # ------------------------------------------------------------------
661
+ # If/switch expression helpers
662
+ # ------------------------------------------------------------------
663
+
664
+ # _default_for_type lives on TypeInferer — see codegen/types.py.
665
+
666
+ def _emit_body_with_assign(self, body: list, target: str,
667
+ lines: list[str], indent: int) -> None:
668
+ """Emit a body block where the last expression becomes an assignment."""
669
+ if not body:
670
+ return
671
+ for i, stmt in enumerate(body):
672
+ if i == len(body) - 1:
673
+ # Last statement — try to turn into assignment
674
+ if isinstance(stmt, ExprStmt):
675
+ # Check if it's a skip expr
676
+ if self._is_skip_expr(stmt.expr):
677
+ return
678
+ cpp = self._visit_expr(stmt.expr)
679
+ pad = " " * indent
680
+ lines.append(f"{pad}{target} = {cpp};")
681
+ elif isinstance(stmt, IfStmt):
682
+ # Nested if expression
683
+ self._visit_if_switch_expr(stmt, target, lines, indent)
684
+ elif isinstance(stmt, SwitchStmt):
685
+ self._visit_if_switch_expr(stmt, target, lines, indent)
686
+ else:
687
+ self._visit_stmt(stmt, lines, indent)
688
+ else:
689
+ self._visit_stmt(stmt, lines, indent)
690
+
691
+ def _visit_if_switch_expr(self, node, target: str,
692
+ lines: list[str], indent: int) -> None:
693
+ """Emit an if/switch used as an expression, assigning to target."""
694
+ pad = " " * indent
695
+ if isinstance(node, IfStmt):
696
+ cond = self._visit_expr(node.condition)
697
+ lines.append(f"{pad}if ({cond}) {{")
698
+ self._emit_body_with_assign(node.body, target, lines, indent + 1)
699
+ lines.append(f"{pad}}}")
700
+ if node.else_body:
701
+ if len(node.else_body) == 1 and isinstance(node.else_body[0], IfStmt):
702
+ lines[-1] = f"{pad}}} else"
703
+ self._visit_if_switch_expr(node.else_body[0], target, lines, indent)
704
+ else:
705
+ lines[-1] = f"{pad}}} else {{"
706
+ self._emit_body_with_assign(node.else_body, target, lines, indent + 1)
707
+ lines.append(f"{pad}}}")
708
+ elif isinstance(node, SwitchStmt):
709
+ if node.expr:
710
+ expr_var = f"__switch_val_{self._switch_counter}"
711
+ self._switch_counter += 1
712
+ lines.append(f"{pad}auto {expr_var} = {self._visit_expr(node.expr)};")
713
+ for i, (case_expr, case_body) in enumerate(node.cases):
714
+ prefix = "if" if i == 0 else "else if"
715
+ case_val = self._visit_expr(case_expr)
716
+ lines.append(f"{pad}{prefix} ({expr_var} == {case_val}) {{")
717
+ self._emit_body_with_assign(case_body, target, lines, indent + 1)
718
+ lines.append(f"{pad}}}")
719
+ else:
720
+ for i, (case_expr, case_body) in enumerate(node.cases):
721
+ prefix = "if" if i == 0 else "else if"
722
+ cond = self._visit_expr(case_expr)
723
+ lines.append(f"{pad}{prefix} ({cond}) {{")
724
+ self._emit_body_with_assign(case_body, target, lines, indent + 1)
725
+ lines.append(f"{pad}}}")
726
+ if node.default_body:
727
+ lines.append(f"{pad}else {{")
728
+ self._emit_body_with_assign(node.default_body, target, lines, indent + 1)
729
+ lines.append(f"{pad}}}")