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,701 @@
1
+ """Expression-level visitors for the codegen.
2
+
3
+ ``ExprVisitor`` holds the expression-level visitors. ``_visit_expr`` is
4
+ the central dispatcher that inspects the AST node kind and either emits
5
+ a literal directly (numbers, strings, booleans, ``na``, color literals)
6
+ or delegates to one of the kind-specific handlers:
7
+
8
+ * ``_visit_ident`` — identifier resolution (parameters, ``BAR_FIELDS`` /
9
+ ``BAR_BUILTINS`` builtins, known constants, series-var current value
10
+ reads, per-call-site var remapping).
11
+ * ``_visit_member_access`` — namespace member access for ``strategy`` /
12
+ ``math`` / ``ta`` / ``timeframe`` / ``barstate`` / ``syminfo`` /
13
+ ``display`` / ``color``, nested ``strategy.*.*`` (oca / direction /
14
+ commission / closedtrades / opentrades), enum member access, and the
15
+ fallback path for UDT-style member access on known variables.
16
+ * ``_visit_binop`` — binary operators (with the Pine ``and`` / ``or``
17
+ -> ``&&`` / ``||`` rewrite and ``%`` -> ``std::fmod``).
18
+ * ``_visit_unaryop`` — unary ``not`` -> ``!`` plus pass-through of
19
+ numeric unary operators.
20
+ * ``_visit_subscript`` — array/series ``[k]`` history access (function
21
+ parameters, bar-field series, user series-vars, ``strategy.*[k]``
22
+ history shadow series, generic fallback).
23
+
24
+ These visitors were extracted from ``base.py``'s ``CodeGen`` class as
25
+ step 9 of the codegen package refactor; behaviour is preserved
26
+ verbatim. The mixin owns no state of its own — it reads/writes only
27
+ attributes already established on the host class (``CodeGen``).
28
+
29
+ Mixin contract — host class must provide the following attributes
30
+ (all set by ``CodeGen.__init__`` or other mixins):
31
+
32
+ - ``self.ctx`` (``AnalyzerContext``): symbol table source. The
33
+ visitors read ``ctx.symbols.resolve`` (``_visit_ident`` /
34
+ ``_visit_member_access``) to disambiguate ``color`` and UDT
35
+ identifiers, ``ctx.series_vars`` to decide between current-value
36
+ reads and history reads, and ``ctx.ta_call_sites`` to find the
37
+ no-arg TA-property call site (``ta.obv`` / ``ta.accdist`` / ...).
38
+ - ``self._known_vars`` (``dict[str, bool|int|float|str]``): inlined
39
+ literal constants; consulted by ``_visit_ident``.
40
+ - ``self._input_backed_vars`` (``set[str]``): names backed by
41
+ ``input.*`` calls — these are NOT inlined even when in
42
+ ``_known_vars`` because ``strategy_set_input()`` can override them
43
+ at runtime.
44
+ - ``self._var_names`` (``set[str]``): names declared at module scope.
45
+ Consulted by ``_visit_ident`` (to decide whether ``color`` is a
46
+ type name vs a variable), ``_visit_member_access`` (UDT fallback)
47
+ and ``_visit_subscript`` (generic fallback).
48
+ - ``self._global_member_vars`` (``set[str]``): non-``var`` global
49
+ declarations emitted as class members; ``_visit_ident`` uses it as
50
+ part of the ``color``-as-type-name guard.
51
+ - ``self._current_func_locals`` (``set[str]``): function-local names
52
+ emitted in the current function body (used by the ``color``
53
+ identifier guard in ``_visit_ident``).
54
+ - ``self._current_func_param_types`` (``dict[str, ...]``): parameter
55
+ name → type for the function currently being emitted; used by
56
+ ``_visit_ident``, ``_visit_member_access`` and ``_visit_subscript``
57
+ to treat parameters as plain locals (never series-member reads).
58
+ - ``self._current_func_series_params`` (``set[str]``): subset of the
59
+ current function's parameters that are series-typed; treated as
60
+ ``Series<T>`` so ``src`` becomes ``src[0]`` and ``src[k]`` stays
61
+ ``src[k]``.
62
+ - ``self._current_loop_vars`` (``set[str]``): for-in iterator names;
63
+ used by ``_visit_member_access`` to distinguish iterator member
64
+ access from enum constants.
65
+ - ``self._active_var_remap`` (``dict[str, str]``): per-call-site
66
+ rename map for cloned function-local var/series names; applied in
67
+ ``_visit_ident`` / ``_visit_member_access`` / ``_visit_subscript``.
68
+ - ``self._enum_defs`` (``dict[str, dict[str, ...]]``): enum name →
69
+ member map; ``_visit_member_access`` consults it before falling
70
+ through to the generic ``Identifier.member`` path.
71
+ - ``self._strategy_series_vars`` (``set[str]``): collected by
72
+ ``_visit_subscript`` whenever it encounters a
73
+ ``strategy.<member>[k]`` history access; the emitter creates
74
+ matching ``_strat_<member>`` series later.
75
+
76
+ Sibling-mixin methods consumed via ``self``:
77
+
78
+ - ``NamingHelper`` (``codegen/helpers.py``): ``_safe_name``.
79
+ - ``CodeGen.base``: ``_visit_func_call`` (still on the host class —
80
+ the call-dispatch helpers and ``_visit_func_call`` itself are
81
+ extracted in step 10 of the refactor).
82
+
83
+ The mixin avoids importing from ``base.py`` to stay free of cycles;
84
+ all tables it needs come from ``codegen/tables.py`` and all AST
85
+ classes from ``..ast_nodes``.
86
+ """
87
+
88
+ from __future__ import annotations
89
+
90
+ from ..ast_nodes import (
91
+ ASTNode,
92
+ BinOp,
93
+ BoolLiteral,
94
+ ColorLiteral,
95
+ FuncCall,
96
+ Identifier,
97
+ MemberAccess,
98
+ NaLiteral,
99
+ NumberLiteral,
100
+ StringLiteral,
101
+ Subscript,
102
+ Ternary,
103
+ TupleLiteral,
104
+ UnaryOp,
105
+ )
106
+ from .tables import (
107
+ ADJUSTMENT_MAP,
108
+ BAR_BUILTINS,
109
+ BAR_FIELDS,
110
+ BAR_SERIES_PUSH,
111
+ COLOR_CONST_MAP,
112
+ DAYOFWEEK_MAP,
113
+ DISPLAY_MAP,
114
+ ON_OFF_INHERIT_MAP,
115
+ ORDER_DIRECTION_MAP,
116
+ SKIP_NAMESPACES,
117
+ SYMINFO_MEMBER_MAP,
118
+ TA_COMPUTE_ARGS,
119
+ TA_IMPLICIT_COMPUTE_FULL,
120
+ )
121
+
122
+
123
+ def _color_literal_to_int64(value: str) -> str:
124
+ """Lower a Pine hex color literal to a packed ARGB int64 C++ literal.
125
+
126
+ Pine ``#RRGGBB`` is opaque; ``#RRGGBBAA`` carries an explicit alpha
127
+ (opacity, FF = opaque). The engine packs colors as ``0xAARRGGBB``
128
+ (see color.hpp: ``red == 0xFFFF0000``), so we reorder to alpha-first
129
+ and emit an ``int64_t`` literal — matching ``get_input_int64`` and
130
+ ``pine_color::*``. A malformed literal falls back to 0 (transparent).
131
+ """
132
+ h = value.lstrip("#").strip()
133
+ if len(h) == 6:
134
+ rr, gg, bb, aa = h[0:2], h[2:4], h[4:6], "ff"
135
+ elif len(h) == 8:
136
+ rr, gg, bb, aa = h[0:2], h[2:4], h[4:6], h[6:8]
137
+ else:
138
+ return "0"
139
+ try:
140
+ int(rr + gg + bb + aa, 16) # validate hex
141
+ except ValueError:
142
+ return "0"
143
+ return f"0x{aa}{rr}{gg}{bb}LL"
144
+
145
+
146
+ # Valid Pine v6 bare builtins that have no value in a batch backtest (no live
147
+ # feed). Reading them used to emit an undeclared C++ symbol; they now get a
148
+ # tailored hint via the unknown-identifier guard in ``_visit_ident``.
149
+ _REALTIME_ONLY_VARS: frozenset[str] = frozenset({"ask", "bid"})
150
+
151
+ # Builtin namespace prefixes (``format.mintick``, ``barmerge.gaps_on``, …).
152
+ # When a member-access handler has no dedicated branch it falls through to the
153
+ # generic tail, which visits the root identifier bare — so these must not be
154
+ # flagged as unknown variables. Mirrors the namespace list in
155
+ # ``analyzer/base.py::_visit_Identifier``.
156
+ _BUILTIN_NAMESPACE_NAMES: frozenset[str] = frozenset({
157
+ "strategy", "ta", "input", "math", "str", "color", "display", "syminfo",
158
+ "timeframe", "plot", "alert", "barstate", "position", "shape", "location",
159
+ "size", "currency", "order", "format", "text", "extend", "xloc", "yloc",
160
+ "label", "line", "box", "table", "ticker", "request", "runtime", "chart",
161
+ "barmerge", "adjustment", "earnings", "dividends", "splits", "session",
162
+ "scale", "font", "hline", "backadjustment", "settlement_as_close",
163
+ "dayofweek", "array", "matrix", "map", "polyline", "linefill",
164
+ })
165
+
166
+
167
+ class ExprVisitor:
168
+ """Expression-level visitor methods shared across the codegen.
169
+
170
+ Mixed into ``CodeGen``; not intended to be instantiated standalone.
171
+ See the module docstring for the full host-class state contract."""
172
+
173
+ # ------------------------------------------------------------------
174
+ # Expression visitors
175
+ # ------------------------------------------------------------------
176
+
177
+ def _visit_expr(self, node: ASTNode | None) -> str:
178
+ if node is None:
179
+ return "/* null */"
180
+ if isinstance(node, NumberLiteral):
181
+ return str(node.value)
182
+ if isinstance(node, StringLiteral):
183
+ return f'std::string("{node.value}")'
184
+ if isinstance(node, BoolLiteral):
185
+ return "true" if node.value else "false"
186
+ if isinstance(node, NaLiteral):
187
+ return "na<double>()"
188
+ if isinstance(node, ColorLiteral):
189
+ return _color_literal_to_int64(node.value)
190
+ if isinstance(node, Identifier):
191
+ return self._visit_ident(node)
192
+ if isinstance(node, MemberAccess):
193
+ return self._visit_member_access(node)
194
+ if isinstance(node, BinOp):
195
+ return self._visit_binop(node)
196
+ if isinstance(node, UnaryOp):
197
+ return self._visit_unaryop(node)
198
+ if isinstance(node, Ternary):
199
+ c = self._visit_expr(node.condition)
200
+ t = self._visit_expr(node.true_val)
201
+ f = self._visit_expr(node.false_val)
202
+ return f"(({c}) ? ({t}) : ({f}))"
203
+ if isinstance(node, FuncCall):
204
+ return self._visit_func_call(node)
205
+ if isinstance(node, Subscript):
206
+ return self._visit_subscript(node)
207
+ if isinstance(node, TupleLiteral):
208
+ elems = ", ".join(self._visit_expr(e) for e in node.elements)
209
+ return f"std::make_tuple({elems})"
210
+ return "/* unknown */"
211
+
212
+ def _visit_ident(self, node: Identifier) -> str:
213
+ name = node.name
214
+ # Bare 'na' identifier → na<double>()
215
+ if name == "na":
216
+ return "na<double>()"
217
+ # Function parameters that are series — read current value
218
+ if name in self._current_func_series_params:
219
+ return f"{self._safe_name(name)}[0]"
220
+ # Function parameters are plain locals, never series members
221
+ if name in self._current_func_param_types:
222
+ return self._safe_name(name)
223
+ if name in BAR_FIELDS:
224
+ return BAR_FIELDS[name]
225
+ if name in BAR_BUILTINS:
226
+ return BAR_BUILTINS[name]
227
+ # Inline known constants (literals). Input-backed names are NOT inlined
228
+ # because strategy_set_input() can override them at runtime; emit the
229
+ # variable name and let get_input_*() produce the current value.
230
+ if name in self._known_vars and name not in self._input_backed_vars:
231
+ val = self._known_vars[name]
232
+ if isinstance(val, bool):
233
+ return "true" if val else "false"
234
+ if isinstance(val, (int, float)):
235
+ return str(val)
236
+ if isinstance(val, str):
237
+ return f'std::string("{val}")'
238
+ # Pine type name `color` used as a value (no variable) → int64 color constant.
239
+ # Params handled above; symbol table does not retain function locals after analysis.
240
+ if name == "color":
241
+ sym = self.ctx.symbols.resolve(node.name)
242
+ if (
243
+ sym is None
244
+ and name not in self.ctx.series_vars
245
+ and name not in self._var_names
246
+ and name not in self._global_member_vars
247
+ and name not in self._current_func_locals
248
+ ):
249
+ return "(int64_t)pine_color::black"
250
+ # Series var — read current value
251
+ safe = self._safe_name(name)
252
+ # Apply per-call-site var remap (for function-local vars)
253
+ if self._active_var_remap and safe in self._active_var_remap:
254
+ safe = self._active_var_remap[safe]
255
+ if name in self.ctx.series_vars:
256
+ return f"{safe}[0]"
257
+ # Safety net: by here the name resolved to none of the builtins,
258
+ # constants, parameters, or declared variables handled above, so
259
+ # ``return safe`` would emit a bare identifier that is an *undeclared
260
+ # C++ symbol* (e.g. ``x = ask;``, ``x = undefined_var;``). That is a
261
+ # silent miscompile — g++ fails against generated C++ with no Pine
262
+ # line. Reject loudly. (Like the call-site guard in visit_call.py,
263
+ # this branch only fires on input that already failed to compile, so
264
+ # the all-green corpus never exercises it.)
265
+ if not self._ident_is_resolvable(name):
266
+ hint = None
267
+ if name in _REALTIME_ONLY_VARS:
268
+ hint = (f"'{name}' is a realtime-only Pine v6 builtin with no "
269
+ f"value in a batch backtest.")
270
+ self._codegen_error(
271
+ node,
272
+ f"Unknown variable '{name}' — not a PineForge builtin or a "
273
+ f"declared variable.",
274
+ hint=hint,
275
+ )
276
+ return safe
277
+
278
+ def _ident_is_resolvable(self, name: str) -> bool:
279
+ """True when a bare identifier maps to a builtin, constant, parameter,
280
+ or any declared variable/type/enum/function — i.e. emitting it will not
281
+ produce an undeclared C++ symbol. Deliberately generous: a missing
282
+ source here only means a genuinely-unknown name slips through (the
283
+ pre-existing behavior), whereas a wrong ``True`` is safe. Only a
284
+ name in NONE of these is rejected."""
285
+ if name == "na" or name == "color":
286
+ return True
287
+ if name in BAR_FIELDS or name in BAR_BUILTINS:
288
+ return True
289
+ if name in _BUILTIN_NAMESPACE_NAMES:
290
+ return True
291
+ if name in self._known_vars or name in self._input_backed_vars:
292
+ return True
293
+ if name in self._var_names or name in self._global_member_vars:
294
+ return True
295
+ if name in self._current_func_locals or name in self._current_loop_vars:
296
+ return True
297
+ if name in self._current_func_param_types or name in self._current_func_series_params:
298
+ return True
299
+ if name in self.ctx.series_vars:
300
+ return True
301
+ if (name in self._array_vars or name in self._map_vars
302
+ or name in self._matrix_specs or name in self._udt_var_types):
303
+ return True
304
+ if name in self._enum_defs or name in self._udt_defs or name in self._func_names:
305
+ return True
306
+ # Names bound anywhere in the program (block-scoped locals the per-scope
307
+ # sets above never see — e.g. a var declared inside an on_bar for-loop).
308
+ if name in self._all_bound_names:
309
+ return True
310
+ # Catch-all: the analyzer's symbol table knows every declared name it
311
+ # retained (globals, inputs, …). Anything it resolves is legitimate.
312
+ if self.ctx.symbols.resolve(name) is not None:
313
+ return True
314
+ return False
315
+
316
+ def _visit_member_access(self, node: MemberAccess) -> str:
317
+ # UDT field whose type was a drawing primitive (label/line/box/
318
+ # linefill/polyline/table/chart.point) — the field is dropped from
319
+ # the emitted struct, so a raw ``recv.field`` would fail to compile.
320
+ # Emit a labelled zero placeholder so any read site stays well-formed.
321
+ # See: pineforge-codegen issue #10.
322
+ if self._is_omitted_udt_field(node):
323
+ return "/* drawing field omitted */ 0"
324
+ if isinstance(node.object, Identifier):
325
+ ns = node.object.name
326
+ if ns == "strategy":
327
+ # Direction constants
328
+ if node.member == "long":
329
+ return "true"
330
+ if node.member == "short":
331
+ return "false"
332
+ # Position info
333
+ if node.member == "position_size":
334
+ return "signed_position_size()"
335
+ if node.member == "position_avg_price":
336
+ return "position_entry_price_"
337
+ if node.member == "position_entry_name":
338
+ return "position_entry_name()"
339
+ # Trade counts
340
+ if node.member == "opentrades":
341
+ return "((int)pyramid_entries_.size())"
342
+ if node.member == "closedtrades":
343
+ return "((int)trades_.size())"
344
+ if node.member == "wintrades":
345
+ return "count_wintrades()"
346
+ if node.member == "losstrades":
347
+ return "count_losstrades()"
348
+ if node.member == "eventrades":
349
+ return "eventrades()"
350
+ # Equity and P&L
351
+ if node.member == "equity":
352
+ return "(current_equity() + open_profit(current_bar_.close))"
353
+ if node.member == "initial_capital":
354
+ return "initial_capital_"
355
+ if node.member == "netprofit":
356
+ return "net_profit()"
357
+ if node.member == "netprofit_percent":
358
+ return "((net_profit() / initial_capital_) * 100.0)"
359
+ if node.member == "openprofit":
360
+ return "open_profit(current_bar_.close)"
361
+ if node.member == "openprofit_percent":
362
+ return "((open_profit(current_bar_.close) / initial_capital_) * 100.0)"
363
+ if node.member == "grossprofit":
364
+ return "gross_profit()"
365
+ if node.member == "grossloss":
366
+ return "gross_loss()"
367
+ if node.member == "grossprofit_percent":
368
+ return "grossprofit_percent()"
369
+ if node.member == "grossloss_percent":
370
+ return "grossloss_percent()"
371
+ if node.member == "max_contracts_held_all":
372
+ return "max_contracts_held_all()"
373
+ if node.member == "max_contracts_held_long":
374
+ return "max_contracts_held_long()"
375
+ if node.member == "max_contracts_held_short":
376
+ return "max_contracts_held_short()"
377
+ if node.member == "max_drawdown":
378
+ return "max_drawdown_"
379
+ if node.member == "max_runup":
380
+ return "max_runup_"
381
+ if node.member == "max_drawdown_percent":
382
+ return "max_drawdown_percent()"
383
+ if node.member == "max_runup_percent":
384
+ return "max_runup_percent()"
385
+ if node.member == "avg_trade":
386
+ return "avg_trade()"
387
+ if node.member == "avg_trade_percent":
388
+ return "avg_trade_percent()"
389
+ if node.member == "avg_winning_trade":
390
+ return "avg_winning_trade()"
391
+ if node.member == "avg_losing_trade":
392
+ return "avg_losing_trade()"
393
+ if node.member == "avg_winning_trade_percent":
394
+ return "avg_winning_trade_percent()"
395
+ if node.member == "avg_losing_trade_percent":
396
+ return "avg_losing_trade_percent()"
397
+ if node.member == "margin_liquidation_price":
398
+ return "margin_liquidation_price()"
399
+ # Qty type / commission constants
400
+ if node.member == "fixed":
401
+ return "0"
402
+ if node.member == "percent_of_equity":
403
+ return "1"
404
+ if node.member == "cash":
405
+ return "2"
406
+ if node.member == "account_currency":
407
+ return "syminfo_.currency"
408
+ # Sub-namespaces (oca, direction, risk, commission) handled below
409
+ if ns == "math":
410
+ if node.member == "pi":
411
+ return "M_PI"
412
+ if node.member == "e":
413
+ return "M_E"
414
+ if node.member == "phi":
415
+ return "1.618033988749895"
416
+ if node.member == "rphi":
417
+ return "0.6180339887498949"
418
+ if node.member == "abs":
419
+ return "std::abs"
420
+ if node.member == "max":
421
+ return "std::max"
422
+ if node.member == "min":
423
+ return "std::min"
424
+ if ns == "ta":
425
+ if node.member == "tr":
426
+ return ("(std::isnan(_s_close[1]) ? "
427
+ "(current_bar_.high - current_bar_.low) : "
428
+ "std::max(current_bar_.high - current_bar_.low, "
429
+ "std::max(std::abs(current_bar_.high - _s_close[1]), "
430
+ "std::abs(current_bar_.low - _s_close[1]))))")
431
+ # No-arg TA indicators used as properties (ta.obv, ta.accdist, etc.)
432
+ if (
433
+ (node.member in TA_IMPLICIT_COMPUTE_FULL and node.member in TA_COMPUTE_ARGS and TA_COMPUTE_ARGS[node.member] == [])
434
+ or node.member == "vwap"
435
+ ):
436
+ # Find the matching call site
437
+ for site in self.ctx.ta_call_sites:
438
+ ta_short = site.class_name.split("::")[-1].lower()
439
+ if site.member_name.startswith(f"_ta_{node.member}_"):
440
+ if node.member == "vwap":
441
+ return f"(is_first_tick_ ? {site.member_name}.compute(current_bar_.close, current_bar_.volume, current_bar_.timestamp) : {site.member_name}.recompute(current_bar_.close, current_bar_.volume, current_bar_.timestamp))"
442
+ return f"(is_first_tick_ ? {site.member_name}.compute({TA_IMPLICIT_COMPUTE_FULL[node.member]}) : {site.member_name}.recompute({TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
443
+ # Fallback: no call site found — treat as string
444
+ return f'std::string("{node.member}")'
445
+ if ns == "chart":
446
+ # PineForge batch engine always runs on standard OHLCV bars.
447
+ if node.member == "is_standard":
448
+ return "true"
449
+ # All non-standard chart types are false in batch mode.
450
+ if node.member in ("is_heikinashi", "is_kagi", "is_linebreak",
451
+ "is_pnf", "is_range", "is_renko"):
452
+ return "false"
453
+ # Defensive: support_checker.UNSUPPORTED_MEMBERS should already have
454
+ # rejected any unhandled chart.* member. Reaching here is a bug.
455
+ raise ValueError(
456
+ f"codegen: unhandled chart.{node.member} — analyzer should have rejected. "
457
+ f"Add a handler above or extend UNSUPPORTED_MEMBERS."
458
+ )
459
+ if ns == "timeframe":
460
+ if node.member == "period":
461
+ return 'script_tf_'
462
+ if node.member == "main_period":
463
+ return 'main_period()'
464
+ if node.member == "multiplier":
465
+ return 'tf_multiplier(script_tf_)'
466
+ if node.member == "isintraday":
467
+ return 'tf_is_intraday(script_tf_)'
468
+ if node.member == "isminutes":
469
+ return '(tf_is_intraday(script_tf_) && !tf_is_seconds(script_tf_))'
470
+ if node.member == "isdaily":
471
+ return 'tf_is_daily(script_tf_)'
472
+ if node.member == "isweekly":
473
+ return 'tf_is_weekly(script_tf_)'
474
+ if node.member == "ismonthly":
475
+ return 'tf_is_monthly(script_tf_)'
476
+ if node.member == "isdwm":
477
+ return '(tf_is_daily(script_tf_) || tf_is_weekly(script_tf_) || tf_is_monthly(script_tf_))'
478
+ if node.member == "isseconds":
479
+ return 'tf_is_seconds(script_tf_)'
480
+ if node.member == "in_seconds":
481
+ return 'tf_to_seconds(script_tf_)'
482
+ if node.member == "isticks":
483
+ # Batch engine does not support tick-resolution data.
484
+ return "false"
485
+ # Defensive: the if-chain above covers every Pine v6 timeframe.*
486
+ # namespace variable. An unknown member is invalid Pine that
487
+ # would otherwise emit a silent "0". Mirror the chart.* guard.
488
+ raise ValueError(
489
+ f"codegen: unhandled timeframe.{node.member} — not a valid "
490
+ f"Pine v6 timeframe namespace variable. Add a handler above "
491
+ f"if this is a new builtin."
492
+ )
493
+ if ns == "barstate":
494
+ if node.member == "isfirst":
495
+ return "(bar_index_ == 0)"
496
+ if node.member == "islast":
497
+ return "barstate_islast_"
498
+ if node.member == "isnew":
499
+ return "is_first_tick_"
500
+ if node.member == "isconfirmed":
501
+ return "is_last_tick_"
502
+ if node.member == "ishistory":
503
+ return "true"
504
+ if node.member == "isrealtime":
505
+ return "false"
506
+ if node.member == "islastconfirmedhistory":
507
+ return "barstate_islast_"
508
+ return "false"
509
+ if ns in ("backadjustment", "settlement_as_close"):
510
+ # backadjustment.{on,off,inherit} / settlement_as_close.{on,off,inherit}
511
+ # Emit as integer constants (accepted by engine, which ignores them).
512
+ # Codegen silently drops these from request.security kwargs.
513
+ # Unknown member falls back to "inherit" (2).
514
+ return ON_OFF_INHERIT_MAP.get(node.member, "2")
515
+ if ns == "adjustment":
516
+ # adjustment.none / dividends / splits. Unknown member -> "none" (0).
517
+ return ADJUSTMENT_MAP.get(node.member, "0")
518
+ if ns == "dayofweek":
519
+ return DAYOFWEEK_MAP.get(node.member, "0")
520
+ if ns == "session":
521
+ if node.member == "regular":
522
+ return 'std::string("regular")'
523
+ if node.member == "extended":
524
+ return 'std::string("extended")'
525
+ # session.is* predicates backed by engine session_ismarket_ etc.
526
+ # session.isfirstbar_regular / islastbar_regular are aliased to
527
+ # their non-_regular counterparts (engine has one session string;
528
+ # see session_time.hpp limitation comment).
529
+ if node.member == "ismarket":
530
+ return "pine_session_ismarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
531
+ if node.member == "ispremarket":
532
+ return "pine_session_ispremarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
533
+ if node.member == "ispostmarket":
534
+ return "pine_session_ispostmarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
535
+ if node.member in ("isfirstbar", "isfirstbar_regular"):
536
+ return "session_isfirstbar_"
537
+ if node.member in ("islastbar", "islastbar_regular"):
538
+ return "session_islastbar_"
539
+ return "false"
540
+ if ns == "syminfo":
541
+ _syminfo = SYMINFO_MEMBER_MAP.get(node.member)
542
+ if _syminfo is not None:
543
+ return _syminfo
544
+ # Defensive: support_checker rejects any syminfo.* member not in
545
+ # SUPPORTED_SYMINFO (== frozenset(SYMINFO_MEMBER_MAP)). Reaching
546
+ # here means the checker was bypassed or the two tables drifted.
547
+ raise ValueError(
548
+ f"codegen: unhandled syminfo.{node.member} — analyzer should "
549
+ f"have rejected. Add it to SYMINFO_MEMBER_MAP."
550
+ )
551
+ if ns == "display":
552
+ # plot_display (ints for C++; TV uses these in settings — backtest ignores)
553
+ return DISPLAY_MAP.get(node.member, "0")
554
+ if ns == "color":
555
+ if node.member in COLOR_CONST_MAP:
556
+ return COLOR_CONST_MAP[node.member]
557
+ return "0"
558
+ if ns in SKIP_NAMESPACES:
559
+ return "0"
560
+ if ns == "currency":
561
+ return f'std::string("{node.member}")'
562
+ if ns == "order":
563
+ # order.ascending / order.descending. Unknown member -> "ascending".
564
+ return ORDER_DIRECTION_MAP.get(node.member, 'std::string("ascending")')
565
+
566
+ # Handle nested member access: strategy.oca.reduce, strategy.closedtrades.profit(), etc.
567
+ if isinstance(node.object, MemberAccess):
568
+ if isinstance(node.object.object, Identifier):
569
+ outer_ns = node.object.object.name
570
+ if outer_ns == "strategy":
571
+ sub = node.object.member
572
+ # strategy.commission.percent, strategy.oca.*, strategy.direction.*
573
+ if sub == "oca":
574
+ if node.member == "cancel":
575
+ return "1"
576
+ if node.member == "reduce":
577
+ return "2"
578
+ if node.member == "none":
579
+ return "0"
580
+ return "0"
581
+ if sub == "direction":
582
+ if node.member == "long":
583
+ return "1"
584
+ if node.member == "short":
585
+ return "-1"
586
+ if node.member == "all":
587
+ return "0"
588
+ return "0"
589
+ if sub == "commission":
590
+ if node.member == "percent":
591
+ return "0"
592
+ if node.member == "cash_per_order":
593
+ return "1"
594
+ if node.member == "cash_per_contract":
595
+ return "2"
596
+ return "0"
597
+ # strategy.closedtrades.first_index, strategy.opentrades.capital_held
598
+ if sub == "closedtrades" and node.member == "first_index":
599
+ return "0"
600
+ if sub == "opentrades" and node.member == "capital_held":
601
+ return "open_trades_capital_held()"
602
+ return "0"
603
+
604
+ # Enum member access: EnumName.Member -> named int constant (matches emitted const)
605
+ if isinstance(node.object, Identifier):
606
+ name = node.object.name
607
+ if name in self._enum_defs:
608
+ members = self._enum_defs[name]
609
+ if node.member in members:
610
+ return f"{name}_{node.member}"
611
+
612
+ # Unknown member access — emit as string constant (e.g., enum values)
613
+ obj = self._visit_expr(node.object)
614
+ if isinstance(node.object, Identifier):
615
+ name = node.object.name
616
+ # If the variable is known (defined in symbol table or as a var),
617
+ # member access on a scalar C++ type is invalid. This happens when
618
+ # PineScript UDTs (type declarations) are used — we don't support
619
+ # them yet, so emit a safe default.
620
+ sym = self.ctx.symbols.resolve(name)
621
+ if (
622
+ sym is not None
623
+ or name in self._var_names
624
+ or name in self._current_loop_vars
625
+ or name in self._current_func_param_types
626
+ ):
627
+ safe = self._safe_name(name)
628
+ if self._active_var_remap and safe in self._active_var_remap:
629
+ safe = self._active_var_remap[safe]
630
+ return f"{safe}.{node.member}"
631
+ if name not in self.ctx.series_vars:
632
+ # Unknown identifier — likely an enum value
633
+ return f'std::string("{node.member}")'
634
+ return f"{obj}.{node.member}"
635
+
636
+ def _visit_binop(self, node: BinOp) -> str:
637
+ left = self._visit_expr(node.left)
638
+ right = self._visit_expr(node.right)
639
+ cpp_ops = {"and": "&&", "or": "||"}
640
+ op = cpp_ops.get(node.op, node.op)
641
+ # PineScript % works on floats — use std::fmod in C++
642
+ if node.op == "%":
643
+ return f"std::fmod((double)({left}), (double)({right}))"
644
+ # Pine v6 always returns float for `/`, even on int/int operands
645
+ # (breaking change from v5). C++ does int division on int operands,
646
+ # so cast both sides to double to match Pine v6 semantics.
647
+ # Ref: https://www.tradingview.com/pine-script-docs/concepts/operators/
648
+ if node.op == "/":
649
+ return f"((double)({left}) / (double)({right}))"
650
+ return f"({left} {op} {right})"
651
+
652
+ def _visit_unaryop(self, node: UnaryOp) -> str:
653
+ operand = self._visit_expr(node.operand)
654
+ if node.op == "not":
655
+ return f"!({operand})"
656
+ return f"({node.op}{operand})"
657
+
658
+ def _visit_subscript(self, node: Subscript) -> str:
659
+ idx = self._visit_expr(node.index)
660
+ if isinstance(node.object, Identifier):
661
+ name = node.object.name
662
+ # Function parameters that are series — src[N] → src[N]
663
+ if name in self._current_func_series_params:
664
+ return f"{self._safe_name(name)}[{idx}]"
665
+ # Function parameters are scalars — src[0] → src, src[N>0] → src
666
+ if name in self._current_func_param_types:
667
+ return self._safe_name(name)
668
+ if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
669
+ # Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
670
+ return f"_s_{name}[{idx}]"
671
+ if name in self.ctx.series_vars:
672
+ safe = self._safe_name(name)
673
+ # Apply per-call-site var remap
674
+ if self._active_var_remap and safe in self._active_var_remap:
675
+ safe = self._active_var_remap[safe]
676
+ # Same Pine [k] semantics as Series in runtime/series.hpp
677
+ return f"{safe}[{idx}]"
678
+ spec = self._collection_types.get(name)
679
+ if spec is not None and spec.kind in ("array", "map"):
680
+ return f"{self._safe_name(name)}[{idx}]"
681
+ # Handle strategy.* history access (e.g., strategy.position_size[1])
682
+ if isinstance(node.object, MemberAccess):
683
+ if isinstance(node.object.object, Identifier):
684
+ ns = node.object.object.name
685
+ if ns == "strategy":
686
+ # Strategy variables with history operator — use series tracking
687
+ member = node.object.member
688
+ series_name = f"_strat_{member}"
689
+ if series_name not in self._strategy_series_vars:
690
+ self._strategy_series_vars.add(series_name)
691
+ return f"{series_name}[{idx}]"
692
+ obj = self._visit_expr(node.object)
693
+ # If subscripting a non-series variable (e.g., function parameter),
694
+ # src[0] → src (current value), src[N>0] → src (can't access history)
695
+ if isinstance(node.object, Identifier):
696
+ name = node.object.name
697
+ if (name not in BAR_FIELDS and name not in BAR_SERIES_PUSH
698
+ and name not in self.ctx.series_vars
699
+ and name not in self._var_names):
700
+ return obj
701
+ return f"{obj}[{idx}]"