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.
- pineforge_codegen/__init__.py +53 -0
- pineforge_codegen/analyzer/__init__.py +60 -0
- pineforge_codegen/analyzer/base.py +1563 -0
- pineforge_codegen/analyzer/call_handlers.py +895 -0
- pineforge_codegen/analyzer/contracts.py +163 -0
- pineforge_codegen/analyzer/diagnostics.py +118 -0
- pineforge_codegen/analyzer/tables.py +204 -0
- pineforge_codegen/analyzer/types.py +250 -0
- pineforge_codegen/ast_nodes.py +293 -0
- pineforge_codegen/codegen/__init__.py +78 -0
- pineforge_codegen/codegen/base.py +1381 -0
- pineforge_codegen/codegen/emit_top.py +875 -0
- pineforge_codegen/codegen/helpers.py +163 -0
- pineforge_codegen/codegen/helpers_syminfo.py +134 -0
- pineforge_codegen/codegen/input.py +189 -0
- pineforge_codegen/codegen/security.py +1564 -0
- pineforge_codegen/codegen/ta.py +298 -0
- pineforge_codegen/codegen/tables.py +613 -0
- pineforge_codegen/codegen/types.py +573 -0
- pineforge_codegen/codegen/visit_call.py +1305 -0
- pineforge_codegen/codegen/visit_expr.py +701 -0
- pineforge_codegen/codegen/visit_stmt.py +729 -0
- pineforge_codegen/errors.py +98 -0
- pineforge_codegen/lexer.py +531 -0
- pineforge_codegen/parser.py +1198 -0
- pineforge_codegen/pragmas.py +117 -0
- pineforge_codegen/signatures.py +808 -0
- pineforge_codegen/support_checker.py +1111 -0
- pineforge_codegen/symbols.py +118 -0
- pineforge_codegen/tokens.py +406 -0
- pineforge_codegen/tv_input_choices.py +86 -0
- pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
- pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
- pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
- 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}}}")
|