@pineforge/codegen-pyodide 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,8 +60,8 @@ from __future__ import annotations
60
60
 
61
61
  from ..ast_nodes import (
62
62
  ASTNode, Assignment, BinOp, BreakStmt, ContinueStmt, ExprStmt, ForStmt,
63
- ForInStmt, FuncCall, Identifier, IfStmt, NumberLiteral, Subscript,
64
- SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
63
+ ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, NumberLiteral, StringLiteral,
64
+ Subscript, SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
65
65
  WhileStmt,
66
66
  )
67
67
  from ..analyzer import (
@@ -77,6 +77,89 @@ class SecurityEmitter:
77
77
 
78
78
  Mixed into ``CodeGen``; not intended to be instantiated standalone."""
79
79
 
80
+ def _resolve_security_tf(self, tf_node, containing_func: str):
81
+ """Resolve a ``request.security`` timeframe argument to ``(tf_str, tf_expr)``.
82
+
83
+ ``tf_str`` is a compile-time string literal value; ``tf_expr`` is a runtime
84
+ C++ expression used at evaluator-registration time. Exactly one is non-None
85
+ for a usable tf (both None is acceptable only as an explicit "unknown").
86
+
87
+ A function-parameter tf (e.g. ``f(tf) => request.security(sym, tf, ...)``)
88
+ is not visible at class scope (the evaluator is a class method), so it is
89
+ resolved from the function's call sites. A dead-code UDF (never called)
90
+ falls back to the chart timeframe — its evaluator result is never read.
91
+ """
92
+ if isinstance(tf_node, StringLiteral):
93
+ return tf_node.value, None
94
+ if isinstance(tf_node, Identifier):
95
+ name = tf_node.name
96
+ if name in self._timeframe_period_vars:
97
+ return None, "script_tf_"
98
+ if (name in self._known_vars and name not in self._input_backed_vars
99
+ and isinstance(self._known_vars[name], str)):
100
+ return self._known_vars[name], None
101
+ if name in self._input_backed_vars and name in self._input_var_to_call:
102
+ return None, self._visit_expr(self._input_var_to_call[name])
103
+ # class-scope resolvable (global / input member)?
104
+ if self._ident_is_resolvable(name):
105
+ try:
106
+ return None, self._visit_expr(tf_node)
107
+ except Exception:
108
+ pass
109
+ # function-parameter tf -> resolve from the call sites
110
+ if containing_func:
111
+ resolved = self._resolve_param_tf_from_callsites(containing_func, name)
112
+ if resolved is not None:
113
+ return resolved
114
+ # graceful fallback so transpile does not hard-fail
115
+ return None, "input_tf_"
116
+ # any other expression — visit if it resolves at class scope
117
+ try:
118
+ return None, self._visit_expr(tf_node)
119
+ except Exception:
120
+ return None, "input_tf_"
121
+
122
+ def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
123
+ """For a ``request.security`` whose tf is function parameter ``param_name``
124
+ of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
125
+ the call sites, or None. If every call passes the same literal/member tf,
126
+ that tf is used; mixed timeframes or a never-called (dead-code) function
127
+ fall back to the chart timeframe (``input_tf_``)."""
128
+ fdef = None
129
+ for node in self._walk_ast(self.ctx.ast):
130
+ if isinstance(node, FuncDef) and node.name == func_name:
131
+ fdef = node
132
+ break
133
+ if fdef is None or param_name not in fdef.params:
134
+ return None
135
+ pidx = fdef.params.index(param_name)
136
+ resolved: list = []
137
+ found_call = False
138
+ for node in self._walk_ast(self.ctx.ast):
139
+ if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
140
+ and node.callee.name == func_name):
141
+ found_call = True
142
+ arg = node.args[pidx] if pidx < len(node.args) else None
143
+ if arg is None:
144
+ continue
145
+ # Resolve the call-site arg (no further containing func — these
146
+ # are global-scope / input args).
147
+ resolved.append(self._resolve_security_tf(arg, ""))
148
+ if not found_call:
149
+ # dead code — evaluator never read; register with chart tf.
150
+ return (None, "input_tf_")
151
+ valid = [r for r in resolved if r is not None]
152
+ if not valid:
153
+ return (None, "input_tf_")
154
+ strs = {r[0] for r in valid}
155
+ exprs = {r[1] for r in valid}
156
+ if len(strs) == 1 and next(iter(strs), None) is not None:
157
+ return (next(iter(strs)), None)
158
+ if len(exprs) == 1 and next(iter(exprs), None) is not None:
159
+ return (None, next(iter(exprs)))
160
+ # mixed timeframes across call sites — cannot pick one statically
161
+ return (None, "input_tf_")
162
+
80
163
  def _normalize_security_call(self, item) -> dict:
81
164
  if hasattr(item, "sec_id"):
82
165
  return {
@@ -88,9 +171,12 @@ class SecurityEmitter:
88
171
  "gaps_node": item.gaps,
89
172
  "lookahead_node": item.lookahead,
90
173
  "ta_range": item.ta_range,
174
+ "heikinashi": bool(getattr(item, "heikinashi", False)),
91
175
  "depends_on_mutable_globals": bool(getattr(item, "depends_on_mutable_globals", False)),
92
176
  "mutable_globals": list(getattr(item, "mutable_globals", ()) or ()),
93
177
  "is_lower_tf_array": bool(getattr(item, "is_lower_tf_array", False)),
178
+ "containing_func": getattr(item, "containing_func", "") or "",
179
+ "callsite_idx": getattr(item, "callsite_idx", None),
94
180
  }
95
181
  return {
96
182
  "sec_id": item[0],
@@ -104,6 +190,8 @@ class SecurityEmitter:
104
190
  "depends_on_mutable_globals": False,
105
191
  "mutable_globals": [],
106
192
  "is_lower_tf_array": False,
193
+ "containing_func": "",
194
+ "callsite_idx": None,
107
195
  }
108
196
 
109
197
  def _security_state_name(self, sec_id: int, name: str) -> str:
@@ -239,6 +327,75 @@ class SecurityEmitter:
239
327
  def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
240
328
  return f"_sec{sec_id}_hist_{field}"
241
329
 
330
+ def _collect_security_ta_hist_indices(self, node) -> set[int]:
331
+ """Which security TA call-site indices need HTF history (subscript index >= 1).
332
+
333
+ ``request.security(..., ta.ema(close, 55)[1], ...)`` reads a *confirmed*
334
+ HTF TA value at a past-bar offset. The inner TA call runs in the security
335
+ (HTF) context and commits one value per COMPLETED HTF bar; offsets read a
336
+ per-site ``Series`` filled (gated on ``is_complete``) in
337
+ ``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
338
+ OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
339
+ and needs no Series, so only index >= 1 registers here."""
340
+ out: set[int] = set()
341
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
342
+
343
+ def resolve_ta_site(obj, resolving: set[str] | None = None):
344
+ """_get_ta_site only matches the literal ta.* FuncCall node by
345
+ identity; fall back through global_expr_map for an indirect
346
+ binding (``v = ta.ema(close, 55)`` then ``...v[1]...``), mirroring
347
+ the same fallback in _build_security_expr's Subscript branch."""
348
+ site = self._get_ta_site(obj)
349
+ if site is not None:
350
+ return site
351
+ if isinstance(obj, Identifier):
352
+ if resolving is None:
353
+ resolving = set()
354
+ if obj.name in global_expr_map and obj.name not in resolving:
355
+ resolving.add(obj.name)
356
+ return resolve_ta_site(global_expr_map[obj.name], resolving)
357
+ return None
358
+
359
+ def walk(n):
360
+ if n is None:
361
+ return
362
+ if isinstance(n, Subscript):
363
+ site = resolve_ta_site(n.object)
364
+ if site is not None:
365
+ idx_lit = self._literal_int_for_security_index(n.index)
366
+ if idx_lit is not None and idx_lit >= 1:
367
+ site_idx = self._ta_index_by_site_id.get(id(site))
368
+ if site_idx is not None:
369
+ out.add(site_idx)
370
+ if isinstance(n, (list, tuple)):
371
+ for x in n:
372
+ walk(x)
373
+ return
374
+ for _k, v in getattr(n, "__dict__", {}).items():
375
+ if isinstance(v, ASTNode):
376
+ walk(v)
377
+ elif isinstance(v, (list, tuple)):
378
+ for x in v:
379
+ if isinstance(x, ASTNode):
380
+ walk(x)
381
+
382
+ walk(node)
383
+ return out
384
+
385
+ def _security_ta_hist_series_cpp(self, member_name: str) -> str:
386
+ """Per-(sec, site) ``Series<double>`` backing ``ta.<fn>(...)[k>=1]`` HTF history."""
387
+ return f"{member_name}_hist"
388
+
389
+ def _security_ta_hist_series_names(self, sec_id: int) -> list[str]:
390
+ """Hist Series names for every security TA site (and variant) read at an
391
+ offset >= 1 in sec ``sec_id``."""
392
+ info = self._security_eval_info[sec_id]
393
+ names: list[str] = []
394
+ for idx in sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ())):
395
+ for variant in (info.get("ta_variants") or {}).get(idx, []):
396
+ names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
397
+ return names
398
+
242
399
  @staticmethod
243
400
  def _security_series_binding(series_name: str) -> str:
244
401
  return f"@series:{series_name}"
@@ -1160,6 +1317,59 @@ class SecurityEmitter:
1160
1317
  )
1161
1318
  return out
1162
1319
 
1320
+ def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
1321
+ """Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
1322
+ ``is_complete``.
1323
+
1324
+ ``request.security(..., [high[1], low[1], ...], ...)`` reads HTF OHLC at
1325
+ past-bar offsets. Each offset is backed by a per-field Series whose
1326
+ history must advance once per COMPLETED HTF bar — not once per (partial)
1327
+ chart-bar evaluation. ``_eval_security_N`` fires on every chart bar; only
1328
+ the bar that completes the HTF aggregate has ``is_complete == true``.
1329
+ Pushing unconditionally advanced the offset history every chart bar, so
1330
+ ``high[1]`` resolved to a recent partial bar instead of the prior
1331
+ completed HTF bar. Gate all pushes for this sec in one combined block."""
1332
+ fields = sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ()))
1333
+ if not fields:
1334
+ return
1335
+ lines.append(" if (is_complete) {")
1336
+ for field in fields:
1337
+ lines.append(
1338
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
1339
+ )
1340
+ lines.append(" }")
1341
+
1342
+ def _emit_security_ta_hist_pushes(
1343
+ self, sec_id: int, info: dict, ta_results: dict, lines: list[str]
1344
+ ) -> None:
1345
+ """Emit the TA history-offset Series pushes for ``sec_id``, gated on
1346
+ ``is_complete`` (mirrors ``_emit_security_ohlc_hist_pushes``).
1347
+
1348
+ ``request.security(..., ta.ema(close, 55)[1], ...)`` reads a confirmed
1349
+ HTF TA value at a past-bar offset. The committed value (``_secval_*``,
1350
+ produced with ``.compute()`` only when ``is_complete``) is pushed onto a
1351
+ per-site Series once per COMPLETED HTF bar, AFTER the expression
1352
+ assignment so the offset read sees the prior completed bar. Pushing on
1353
+ every chart-bar eval would otherwise advance the offset history per
1354
+ partial eval / chart tick (the bug this replaces, where the chart-context
1355
+ ``_hist_call`` buffer advanced on ``is_first_tick_``)."""
1356
+ indices = sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
1357
+ if not indices:
1358
+ return
1359
+ pushes: list[str] = []
1360
+ for idx in indices:
1361
+ for variant in (info.get("ta_variants") or {}).get(idx, []):
1362
+ result_name = ta_results.get((idx, variant["signature"]))
1363
+ if result_name is None:
1364
+ continue
1365
+ hist = self._security_ta_hist_series_cpp(variant["member_name"])
1366
+ pushes.append(f" {hist}.push({result_name});")
1367
+ if not pushes:
1368
+ return
1369
+ lines.append(" if (is_complete) {")
1370
+ lines.extend(pushes)
1371
+ lines.append(" }")
1372
+
1163
1373
  def _emit_security_evaluators(self, lines: list[str]) -> None:
1164
1374
  """Emit _eval_security_N() methods and evaluate_security() dispatch."""
1165
1375
  if not self._security_calls:
@@ -1223,7 +1433,7 @@ class SecurityEmitter:
1223
1433
  )
1224
1434
  var_name = variant["result_name"]
1225
1435
  sec_name = variant["member_name"]
1226
- lines.append(f" auto {var_name} = is_complete "
1436
+ lines.append(f" auto {var_name} = security_series_slot_is_new({sec_id}) "
1227
1437
  f"? {sec_name}.compute({compute_args}) "
1228
1438
  f": {sec_name}.recompute({compute_args});")
1229
1439
  ta_results[(idx, variant["signature"])] = var_name
@@ -1232,6 +1442,35 @@ class SecurityEmitter:
1232
1442
 
1233
1443
  self._emit_security_rebinds(sec_id, info, lines, ta_results, indent=2, emitted_lines=lines)
1234
1444
  emit_security_ta(post_rebind_ta_indices)
1445
+ returns_tuple = item.get("returns_tuple", False)
1446
+ tuple_size = item.get("tuple_size", 0)
1447
+ if (
1448
+ returns_tuple
1449
+ and tuple_size
1450
+ and tuple_size > 0
1451
+ and isinstance(expr_node, TupleLiteral)
1452
+ ):
1453
+ # A tuple body destructures into per-element scalar members
1454
+ # ``_req_sec_{sec_id}_{i}`` (declared in ``base.py`` and reset in
1455
+ # ``clear_security``). Assign each element individually rather
1456
+ # than building the whole ``TupleLiteral`` (which lowers to an
1457
+ # ``std::make_tuple(...)`` against the non-existent aggregate
1458
+ # member ``_req_sec_{sec_id}``).
1459
+ for i, el in enumerate(expr_node.elements):
1460
+ el_cpp = self._build_security_expr(
1461
+ sec_id,
1462
+ el,
1463
+ None,
1464
+ ta_results,
1465
+ security_mutable_names=security_mutable_names,
1466
+ emitted_lines=lines,
1467
+ )
1468
+ lines.append(f" _req_sec_{sec_id}_{i} = {el_cpp};")
1469
+ self._emit_security_ohlc_hist_pushes(sec_id, lines)
1470
+ self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
1471
+ lines.append(" }")
1472
+ lines.append("")
1473
+ continue
1235
1474
  expr_cpp = self._build_security_expr(
1236
1475
  sec_id,
1237
1476
  expr_node,
@@ -1256,10 +1495,8 @@ class SecurityEmitter:
1256
1495
  )
1257
1496
  else:
1258
1497
  lines.append(f" _req_sec_{sec_id} = {expr_cpp};")
1259
- for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1260
- lines.append(
1261
- f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
1262
- )
1498
+ self._emit_security_ohlc_hist_pushes(sec_id, lines)
1499
+ self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
1263
1500
  lines.append(" }")
1264
1501
  lines.append("")
1265
1502
 
@@ -1296,6 +1533,8 @@ class SecurityEmitter:
1296
1533
  lines.append(
1297
1534
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1298
1535
  )
1536
+ for name in self._security_ta_hist_series_names(sec_id):
1537
+ lines.append(f" {name}.clear();")
1299
1538
  lines.append(" break;")
1300
1539
  continue
1301
1540
  if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
@@ -1318,16 +1557,21 @@ class SecurityEmitter:
1318
1557
  lines.append(
1319
1558
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1320
1559
  )
1560
+ for name in self._security_ta_hist_series_names(sec_id):
1561
+ lines.append(f" {name}.clear();")
1321
1562
  lines.append(" break;")
1322
1563
  else:
1323
1564
  hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
1324
- if hist:
1565
+ ta_hist_names = self._security_ta_hist_series_names(sec_id)
1566
+ if hist or ta_hist_names:
1325
1567
  lines.append(f" case {sec_id}:")
1326
1568
  lines.append(f" _req_sec_{sec_id} = na<double>();")
1327
1569
  for field in sorted(hist):
1328
1570
  lines.append(
1329
1571
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1330
1572
  )
1573
+ for name in ta_hist_names:
1574
+ lines.append(f" {name}.clear();")
1331
1575
  lines.append(" break;")
1332
1576
  else:
1333
1577
  lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
@@ -1463,6 +1707,74 @@ class SecurityEmitter:
1463
1707
  "request.security() OHLC history index must be a literal integer (e.g. high[1])",
1464
1708
  )
1465
1709
 
1710
+ # Indirect TA binding: ``v = ta.ema(close, 55)`` then
1711
+ # ``request.security(..., v[1], ...)``. _get_ta_site below only
1712
+ # matches the literal ta.* FuncCall node by identity, so a bare
1713
+ # Identifier subscript target silently misses it and falls
1714
+ # through to a chart-resolution read of the wrong (non-HTF)
1715
+ # series. Resolve through the same global_expr_map the
1716
+ # non-subscript Identifier path above already uses, and
1717
+ # recurse on a synthetic Subscript over the resolved value so
1718
+ # it re-enters this whole branch (TA site, OHLC field, or
1719
+ # helper binding, whichever the resolved expression turns out
1720
+ # to be) instead of duplicating that dispatch here.
1721
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
1722
+ if (expr_node.object.name in global_expr_map
1723
+ and expr_node.object.name not in resolving):
1724
+ resolving.add(expr_node.object.name)
1725
+ resolved = self._build_security_expr(
1726
+ sec_id,
1727
+ Subscript(object=global_expr_map[expr_node.object.name], index=expr_node.index),
1728
+ ta_range,
1729
+ ta_results,
1730
+ resolving,
1731
+ security_mutable_names,
1732
+ helper_binding_stack,
1733
+ emitted_lines,
1734
+ )
1735
+ resolving.remove(expr_node.object.name)
1736
+ return resolved
1737
+ ta_site = self._get_ta_site(expr_node.object)
1738
+ if ta_site is not None:
1739
+ # ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
1740
+ # runs in the HTF (security) context and commits one value per
1741
+ # COMPLETED HTF bar. Read the already-emitted security TA result —
1742
+ # offset 0 reuses the current committed value (``_secval_*``),
1743
+ # offset k>=1 reads a per-site Series that advances on
1744
+ # ``is_complete`` (HTF-bar boundary) in ``_eval_security_N``. The
1745
+ # buggy generic path re-lowered the inner TA to the CHART member
1746
+ # and gated a ``_hist_call`` buffer on ``is_first_tick_`` (chart
1747
+ # tick), so without a magnifier it advanced every chart bar and
1748
+ # produced the chart-tf TA instead of the confirmed HTF value.
1749
+ idx = self._ta_index_by_site_id.get(id(ta_site))
1750
+ sig = self._security_binding_stack_signature(helper_binding_stack)
1751
+ idx_lit = self._literal_int_for_security_index(expr_node.index)
1752
+ if idx_lit is None:
1753
+ self._codegen_error(
1754
+ expr_node,
1755
+ "request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
1756
+ )
1757
+ if idx_lit == 0:
1758
+ # Current completed-HTF-bar value: reuse the bare-TA emission.
1759
+ return self._build_security_expr(
1760
+ sec_id,
1761
+ expr_node.object,
1762
+ ta_range,
1763
+ ta_results,
1764
+ resolving,
1765
+ security_mutable_names,
1766
+ helper_binding_stack,
1767
+ emitted_lines,
1768
+ )
1769
+ member_name = self._security_ta_variant_names.get(
1770
+ (sec_id, idx, sig),
1771
+ f"_sec{sec_id}_{ta_site.member_name}",
1772
+ )
1773
+ hist = self._security_ta_hist_series_cpp(member_name)
1774
+ # ta(...)[k] -> hist[k-1]: hist[0] is the prior completed HTF bar
1775
+ # (current value not yet pushed — push happens after this assign).
1776
+ return f"{hist}[{idx_lit - 1}]"
1777
+
1466
1778
  if isinstance(expr_node, BinOp):
1467
1779
  left = self._build_security_expr(
1468
1780
  sec_id, expr_node.left, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
@@ -1558,7 +1870,7 @@ class SecurityEmitter:
1558
1870
  helper_binding_stack,
1559
1871
  emitted_lines,
1560
1872
  )
1561
- return f"(is_complete ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
1873
+ return f"(security_series_slot_is_new({sec_id}) ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
1562
1874
 
1563
1875
  result = self._visit_expr(expr_node)
1564
1876
  return self._rewrite_security_cpp(result, sec_id, security_mutable_names, helper_binding_stack)
@@ -295,4 +295,7 @@ class TaSiteHelper:
295
295
  return True
296
296
  except ValueError:
297
297
  pass
298
- return val in ("true", "false", "0", "0.0", "na<double>()")
298
+ return val in (
299
+ "true", "false", "0", "0.0",
300
+ "na<double>()", "na<int>()", "na<int64_t>()", "na<bool>()",
301
+ )
@@ -255,7 +255,12 @@ TA_TUPLE_FIELDS = {
255
255
  # must be promoted to ``int64_t`` so the ``na`` sentinel (``INT64_MIN``)
256
256
  # survives — narrowing to 32-bit ``int`` collapses it to ``0`` and breaks
257
257
  # ``is_na<int>`` detection.
258
- INT64_BUILTINS = {"time", "time_close", "timestamp", "time_tradingday"}
258
+ INT64_BUILTINS = {"time", "time_close", "timenow", "timestamp", "time_tradingday"}
259
+
260
+ # Subset of INT64_BUILTINS spelled as a bare identifier (no call args) in Pine.
261
+ # ``entryTime := time`` parses the RHS as an ``Identifier`` named ``time`` (not a
262
+ # ``FuncCall``), so int64 promotion must match these by identifier name too.
263
+ INT64_BUILTIN_IDENTIFIERS = {"time", "time_close", "timenow"}
259
264
 
260
265
  PINE_TYPE_TO_CPP = {
261
266
  "int": "int", "float": "double", "bool": "bool", "string": "std::string",
@@ -263,15 +268,66 @@ PINE_TYPE_TO_CPP = {
263
268
  PineType.STRING: "std::string", PineType.NA: "double",
264
269
  PineType.UNKNOWN: "double", PineType.VOID: "double",
265
270
  PineType.COLOR: "int",
271
+ # Drawing-objects-as-data: the value-view handle structs (see
272
+ # drawing.hpp). Explicit-hint decls (``var line x``), UDT-method drawing
273
+ # params, and ``_type_for_decl`` resolve through here.
274
+ "line": "Line", "box": "Box", "label": "Label",
275
+ "linefill": "Linefill", "chart.point": "ChartPoint",
276
+ }
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Drawing-objects-as-data (spec §4.1)
280
+ # ---------------------------------------------------------------------------
281
+ # Pine drawing type name -> C++ handle struct (pineforge/drawing.hpp).
282
+ # ``table`` / ``polyline`` are DELIBERATELY absent — they stay 100% no-op
283
+ # (kept in SKIP_NAMESPACES / SKIP_VAR_TYPES / _DRAWING_TYPES_INIT).
284
+ DRAWING_TYPE_TO_CPP = {
285
+ "line": "Line", "box": "Box", "label": "Label",
286
+ "linefill": "Linefill", "chart.point": "ChartPoint",
266
287
  }
288
+ # Function-call namespaces routed to the per-type arena dispatch.
289
+ DRAWING_NS = {"line", "box", "label", "linefill"}
290
+ # Constant member reads (``line.style_solid`` -> "0"). chart.point/linefill
291
+ # carry no style constants.
292
+ DRAWING_STYLE_NS = {"line", "box", "label"}
293
+ # Per-type arena member name on the generated strategy.
294
+ DRAWING_ARENA = {
295
+ "line": "_pf_lines_", "box": "_pf_boxes_",
296
+ "label": "_pf_labels_", "linefill": "_pf_linefills_",
297
+ }
298
+
299
+ # Pine typed array constructors for drawing handles — ``array.new_line(...)``
300
+ # is the typed alias of ``array.new<line>(...)`` (and likewise for box/label/
301
+ # linefill). Maps the constructor name to the drawing element type name, so the
302
+ # element TypeSpec resolves to the C++ handle struct (Line/Box/Label/Linefill).
303
+ ARRAY_DRAWING_NEW_CTORS = {
304
+ "new_line": "line", "new_box": "box",
305
+ "new_label": "label", "new_linefill": "linefill",
306
+ }
307
+ # Pine array methods whose C++ lowering is void (or a non-scalar iterator) and
308
+ # whose Pine semantics is mutate-in-place: a function whose body ends in one of
309
+ # these returns void, so the codegen must emit it as a statement + default
310
+ # return rather than ``return arr.clear();`` / ``return arr.insert(...);``.
311
+ ARRAY_VOID_METHODS = frozenset({
312
+ "push", "unshift", "insert", "clear", "set", "fill",
313
+ "sort", "reverse", "concat",
314
+ })
315
+ # All recognised ``array.new_*`` scalar+drawing constructor names (``new`` itself
316
+ # is template-form and handled separately).
317
+ ARRAY_NEW_CTORS = frozenset(
318
+ {"new_float", "new_int", "new_bool", "new_string"} | set(ARRAY_DRAWING_NEW_CTORS)
319
+ )
267
320
 
268
321
  SKIP_FUNC_NAMES = {
269
322
  "plot", "plotshape", "plotchar", "plotcandle", "plotbar", "plotarrow",
270
323
  "fill", "hline", "barcolor", "bgcolor", "alert", "alertcondition",
271
324
  }
325
+ # line / box / label / linefill REMOVED — they now route to real drawing
326
+ # codegen (gated on _uses_drawing). table / polyline / chart / display / size
327
+ # / position stay no-op.
272
328
  SKIP_NAMESPACES = {
273
- "table", "label", "line", "box", "polyline", "chart",
274
- "linefill", "display", "size", "position",
329
+ "table", "polyline", "chart",
330
+ "display", "size", "position",
275
331
  }
276
332
  SKIP_VAR_TYPES = {"table"}
277
333
 
@@ -382,14 +438,14 @@ ORDER_DIRECTION_MAP = {
382
438
 
383
439
  # Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
384
440
  ARRAY_METHODS = {
385
- "get": lambda a, args: f"{a}[{args[0]}]",
386
- "set": lambda a, args: f"{a}[{args[0]}] = {args[1]}",
441
+ "get": lambda a, args: f"{a}[({args[0]})]",
442
+ "set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
387
443
  "push": lambda a, args: f"{a}.push_back({args[0]})",
388
444
  "unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
389
445
  "insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
390
446
  "pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
391
447
  "shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
392
- "remove": lambda a, args: f"[&](){{ auto v={a}[{args[0]}]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
448
+ "remove": lambda a, args: f"[&](){{ auto v={a}[({args[0]})]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
393
449
  "first": lambda a, args: f"{a}.front()",
394
450
  "last": lambda a, args: f"{a}.back()",
395
451
  "size": lambda a, args: f"(double){a}.size()",
@@ -435,7 +491,7 @@ ARRAY_METHODS = {
435
491
  "mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
436
492
  "percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
437
493
  "percentile_nearest_rank": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return c[std::min(r-1,(int)c.size()-1)]; }}()",
438
- "percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[{args[0]}]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
494
+ "percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[({args[0]})]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
439
495
  "abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
440
496
  "join": lambda a, args: "[&](){{ std::string r; for(size_t i=0;i<{arr}.size();i++){{ if(i>0)r+={sep}; r+=std::to_string({arr}[i]); }} return r; }}()".format(arr=a, sep=args[0] if args else 'std::string(",")'),
441
497
  "standardize": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); s=std::sqrt(s/{a}.size()); std::vector<double> r; for(auto v:{a})r.push_back(s==0?1.0:(v-m)/s); return r; }}()",