@pineforge/codegen-pyodide 0.8.0 → 0.9.0

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.
@@ -59,16 +59,21 @@ tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
59
59
  from __future__ import annotations
60
60
 
61
61
  from ..ast_nodes import (
62
- ASTNode, Assignment, BinOp, BreakStmt, ContinueStmt, ExprStmt, ForStmt,
63
- ForInStmt, FuncCall, Identifier, IfStmt, NumberLiteral, Subscript,
64
- SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
65
- WhileStmt,
62
+ ASTNode, Assignment, BinOp, BoolLiteral, BreakStmt, ContinueStmt, ExprStmt,
63
+ ForStmt, ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
64
+ NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary, TupleAssign,
65
+ TupleLiteral, UnaryOp, VarDecl, WhileStmt,
66
66
  )
67
67
  from ..analyzer import (
68
68
  FuncInfo, TACallSite, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
69
69
  )
70
+ from .. import signatures as sigs
70
71
  from ..symbols import PineType
71
- from .tables import PINE_TYPE_TO_CPP, SECURITY_OHLC_BAR_FIELDS
72
+ from .tables import (
73
+ MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
74
+ SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, _math_minmax_na_expr,
75
+ _merge_kwargs,
76
+ )
72
77
 
73
78
 
74
79
  class SecurityEmitter:
@@ -77,6 +82,142 @@ class SecurityEmitter:
77
82
 
78
83
  Mixed into ``CodeGen``; not intended to be instantiated standalone."""
79
84
 
85
+ def _resolve_security_tf(self, tf_node, containing_func: str):
86
+ """Resolve a ``request.security`` timeframe argument to ``(tf_str, tf_expr)``.
87
+
88
+ ``tf_str`` is a compile-time string literal value; ``tf_expr`` is a runtime
89
+ C++ expression used at evaluator-registration time. Exactly one is non-None
90
+ for a usable tf (both None is acceptable only as an explicit "unknown").
91
+
92
+ A function-parameter tf (e.g. ``f(tf) => request.security(sym, tf, ...)``)
93
+ is not visible at class scope (the evaluator is a class method), so it is
94
+ resolved from the function's call sites. A dead-code UDF (never called)
95
+ falls back to the chart timeframe — its evaluator result is never read.
96
+ """
97
+ if isinstance(tf_node, StringLiteral):
98
+ return tf_node.value, None
99
+ if isinstance(tf_node, Identifier):
100
+ name = tf_node.name
101
+ if name in self._timeframe_period_vars:
102
+ return None, "script_tf_"
103
+ if (name in self._known_vars and name not in self._input_backed_vars
104
+ and isinstance(self._known_vars[name], str)):
105
+ return self._known_vars[name], None
106
+ if name in self._input_backed_vars and name in self._input_var_to_call:
107
+ return None, self._visit_expr(self._input_var_to_call[name])
108
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
109
+ if name in global_expr_map:
110
+ expanded = self._security_tf_runtime_expr(
111
+ global_expr_map[name], resolving={name}
112
+ )
113
+ if expanded is not None:
114
+ return None, expanded
115
+ # class-scope resolvable (global / input member)?
116
+ if self._ident_is_resolvable(name):
117
+ try:
118
+ return None, self._visit_expr(tf_node)
119
+ except Exception:
120
+ pass
121
+ # function-parameter tf -> resolve from the call sites
122
+ if containing_func:
123
+ resolved = self._resolve_param_tf_from_callsites(containing_func, name)
124
+ if resolved is not None:
125
+ return resolved
126
+ # graceful fallback so transpile does not hard-fail
127
+ return None, "input_tf_"
128
+ # any other expression — visit if it resolves at class scope
129
+ try:
130
+ expanded = self._security_tf_runtime_expr(tf_node)
131
+ return None, expanded if expanded is not None else self._visit_expr(tf_node)
132
+ except Exception:
133
+ return None, "input_tf_"
134
+
135
+ def _security_tf_runtime_expr(self, node, resolving: set[str] | None = None) -> str | None:
136
+ """Render a request.security timeframe expression for registration time.
137
+
138
+ Security evaluators are registered before ``on_bar()`` initializes global
139
+ variables, so input-backed aliases such as ``tf = useChart ? timeframe.period
140
+ : inputTf`` must be expanded to their source expression with direct
141
+ ``get_input_*`` reads. Emitting the member name would register with its
142
+ default-constructed value (usually an empty string).
143
+ """
144
+ if node is None:
145
+ return None
146
+ resolving = resolving or set()
147
+ if isinstance(node, StringLiteral):
148
+ return self._visit_expr(node)
149
+ if isinstance(node, Identifier):
150
+ name = node.name
151
+ if name in self._timeframe_period_vars:
152
+ return "script_tf_"
153
+ if name in self._input_backed_vars and name in self._input_var_to_call:
154
+ return self._visit_expr(self._input_var_to_call[name])
155
+ if (name in self._known_vars and name not in self._input_backed_vars
156
+ and isinstance(self._known_vars[name], str)):
157
+ return self._visit_expr(StringLiteral(value=self._known_vars[name]))
158
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
159
+ if name in global_expr_map and name not in resolving:
160
+ resolving.add(name)
161
+ out = self._security_tf_runtime_expr(global_expr_map[name], resolving)
162
+ resolving.remove(name)
163
+ return out
164
+ return self._visit_expr(node)
165
+ if (
166
+ isinstance(node, MemberAccess)
167
+ and isinstance(node.object, Identifier)
168
+ and node.object.name == "timeframe"
169
+ and node.member == "period"
170
+ ):
171
+ return "script_tf_"
172
+ if isinstance(node, Ternary):
173
+ cond = self._security_tf_runtime_expr(node.condition, resolving)
174
+ tv = self._security_tf_runtime_expr(node.true_val, resolving)
175
+ fv = self._security_tf_runtime_expr(node.false_val, resolving)
176
+ if cond is not None and tv is not None and fv is not None:
177
+ return f"(({cond}) ? ({tv}) : ({fv}))"
178
+ return self._visit_expr(node)
179
+
180
+ def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
181
+ """For a ``request.security`` whose tf is function parameter ``param_name``
182
+ of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
183
+ the call sites, or None. If every call passes the same literal/member tf,
184
+ that tf is used; mixed timeframes or a never-called (dead-code) function
185
+ fall back to the chart timeframe (``input_tf_``)."""
186
+ fdef = None
187
+ for node in self._walk_ast(self.ctx.ast):
188
+ if isinstance(node, FuncDef) and node.name == func_name:
189
+ fdef = node
190
+ break
191
+ if fdef is None or param_name not in fdef.params:
192
+ return None
193
+ pidx = fdef.params.index(param_name)
194
+ resolved: list = []
195
+ found_call = False
196
+ for node in self._walk_ast(self.ctx.ast):
197
+ if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
198
+ and node.callee.name == func_name):
199
+ found_call = True
200
+ arg = node.args[pidx] if pidx < len(node.args) else None
201
+ if arg is None:
202
+ continue
203
+ # Resolve the call-site arg (no further containing func — these
204
+ # are global-scope / input args).
205
+ resolved.append(self._resolve_security_tf(arg, ""))
206
+ if not found_call:
207
+ # dead code — evaluator never read; register with chart tf.
208
+ return (None, "input_tf_")
209
+ valid = [r for r in resolved if r is not None]
210
+ if not valid:
211
+ return (None, "input_tf_")
212
+ strs = {r[0] for r in valid}
213
+ exprs = {r[1] for r in valid}
214
+ if len(strs) == 1 and next(iter(strs), None) is not None:
215
+ return (next(iter(strs)), None)
216
+ if len(exprs) == 1 and next(iter(exprs), None) is not None:
217
+ return (None, next(iter(exprs)))
218
+ # mixed timeframes across call sites — cannot pick one statically
219
+ return (None, "input_tf_")
220
+
80
221
  def _normalize_security_call(self, item) -> dict:
81
222
  if hasattr(item, "sec_id"):
82
223
  return {
@@ -88,9 +229,12 @@ class SecurityEmitter:
88
229
  "gaps_node": item.gaps,
89
230
  "lookahead_node": item.lookahead,
90
231
  "ta_range": item.ta_range,
232
+ "heikinashi": bool(getattr(item, "heikinashi", False)),
91
233
  "depends_on_mutable_globals": bool(getattr(item, "depends_on_mutable_globals", False)),
92
234
  "mutable_globals": list(getattr(item, "mutable_globals", ()) or ()),
93
235
  "is_lower_tf_array": bool(getattr(item, "is_lower_tf_array", False)),
236
+ "containing_func": getattr(item, "containing_func", "") or "",
237
+ "callsite_idx": getattr(item, "callsite_idx", None),
94
238
  }
95
239
  return {
96
240
  "sec_id": item[0],
@@ -104,6 +248,8 @@ class SecurityEmitter:
104
248
  "depends_on_mutable_globals": False,
105
249
  "mutable_globals": [],
106
250
  "is_lower_tf_array": False,
251
+ "containing_func": "",
252
+ "callsite_idx": None,
107
253
  }
108
254
 
109
255
  def _security_state_name(self, sec_id: int, name: str) -> str:
@@ -188,7 +334,7 @@ class SecurityEmitter:
188
334
  return None
189
335
 
190
336
  def _literal_int_for_security_index(self, node) -> int | None:
191
- """Integer index for OHLC[ n ] inside request.security (must be literal)."""
337
+ """Integer index for bar-field[n] inside request.security (must be literal)."""
192
338
  if isinstance(node, NumberLiteral):
193
339
  v = node.value
194
340
  if isinstance(v, bool):
@@ -207,19 +353,131 @@ class SecurityEmitter:
207
353
  return None
208
354
  return None
209
355
 
356
+ # In PineForge batch backtests these barstate flags are compile-time
357
+ # constants (see support_checker / codegen.visit_expr barstate emission):
358
+ # every bar is historical, none realtime/last. The other flags
359
+ # (isfirst/isnew/isconfirmed) depend on runtime tick state, so they are
360
+ # deliberately absent and leave a fold "unknown".
361
+ _SECURITY_CONST_BARSTATE = {
362
+ "isrealtime": False,
363
+ "islast": False,
364
+ "ishistory": True,
365
+ "islastconfirmedhistory": False,
366
+ }
367
+
368
+ def _fold_security_const_bool(
369
+ self,
370
+ node,
371
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
372
+ resolving: set[str] | None = None,
373
+ ) -> bool | None:
374
+ """Fold a boolean expression to a compile-time constant, or None.
375
+
376
+ Only reduces expressions whose non-literal leaves are the batch-constant
377
+ barstate flags above; anything runtime-dependent returns None. Used to
378
+ resolve a request.security history index expressed as
379
+ ``barstate.isrealtime ? 1 : 0`` and friends."""
380
+ if resolving is None:
381
+ resolving = set()
382
+ if isinstance(node, BoolLiteral):
383
+ return bool(node.value)
384
+ if (
385
+ isinstance(node, MemberAccess)
386
+ and isinstance(node.object, Identifier)
387
+ and node.object.name == "barstate"
388
+ ):
389
+ return self._SECURITY_CONST_BARSTATE.get(node.member)
390
+ if isinstance(node, UnaryOp) and node.op == "not":
391
+ inner = self._fold_security_const_bool(
392
+ node.operand, helper_binding_stack, resolving
393
+ )
394
+ return None if inner is None else (not inner)
395
+ if isinstance(node, BinOp) and node.op in ("and", "or"):
396
+ left = self._fold_security_const_bool(
397
+ node.left, helper_binding_stack, resolving
398
+ )
399
+ right = self._fold_security_const_bool(
400
+ node.right, helper_binding_stack, resolving
401
+ )
402
+ if left is None or right is None:
403
+ return None
404
+ return (left and right) if node.op == "and" else (left or right)
405
+ if isinstance(node, Identifier):
406
+ bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
407
+ if bound is not None:
408
+ return self._fold_security_const_bool(
409
+ bound, helper_binding_stack, resolving
410
+ )
411
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
412
+ if node.name in global_expr_map and node.name not in resolving:
413
+ resolving.add(node.name)
414
+ out = self._fold_security_const_bool(
415
+ global_expr_map[node.name], helper_binding_stack, resolving
416
+ )
417
+ resolving.remove(node.name)
418
+ return out
419
+ return None
420
+
421
+ def _resolve_security_index_literal(
422
+ self,
423
+ node,
424
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
425
+ resolving: set[str] | None = None,
426
+ ) -> int | None:
427
+ """Resolve a request.security TA/bar-field history index to a literal int.
428
+
429
+ Extends ``_literal_int_for_security_index`` by also resolving the index
430
+ through helper-parameter bindings and global aliases and constant-folding
431
+ a ternary whose condition is a batch-constant barstate flag (e.g.
432
+ ``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index short-
433
+ circuits on the first line, so behaviour is unchanged for every already-
434
+ literal index. Returns None when the index cannot be reduced to a
435
+ compile-time literal, so the caller keeps its existing rejection."""
436
+ direct = self._literal_int_for_security_index(node)
437
+ if direct is not None:
438
+ return direct
439
+ if resolving is None:
440
+ resolving = set()
441
+ if isinstance(node, Identifier):
442
+ bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
443
+ if bound is not None:
444
+ return self._resolve_security_index_literal(
445
+ bound, helper_binding_stack, resolving
446
+ )
447
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
448
+ if node.name in global_expr_map and node.name not in resolving:
449
+ resolving.add(node.name)
450
+ out = self._resolve_security_index_literal(
451
+ global_expr_map[node.name], helper_binding_stack, resolving
452
+ )
453
+ resolving.remove(node.name)
454
+ return out
455
+ return None
456
+ if isinstance(node, Ternary):
457
+ cond = self._fold_security_const_bool(node.condition, helper_binding_stack)
458
+ if cond is None:
459
+ return None
460
+ chosen = node.true_val if cond else node.false_val
461
+ return self._resolve_security_index_literal(
462
+ chosen, helper_binding_stack, resolving
463
+ )
464
+ return None
465
+
210
466
  def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
211
- """Which OHLC fields need HTF history (subscript index >= 1) for this expression."""
467
+ """Which security bar fields need HTF history (subscript index >= 1)."""
212
468
  out: set[str] = set()
213
469
 
214
470
  def walk(n):
215
471
  if n is None:
216
472
  return
217
473
  if isinstance(n, Subscript) and isinstance(n.object, Identifier):
218
- if n.object.name in SECURITY_OHLC_BAR_FIELDS:
474
+ if n.object.name in SECURITY_BAR_FIELDS:
219
475
  idx = self._literal_int_for_security_index(n.index)
220
- # high[0] uses current HTF `bar`; high[k>=1] reads prior completed HTF
221
- # bars from Series history (filled before push in _eval_security_*).
222
- if idx is not None and idx >= 1:
476
+ # high[0]/time[0] uses current HTF `bar`; k>=1 reads prior
477
+ # completed HTF bars from Series history (filled before push
478
+ # in _eval_security_*). Dynamic indices may be >=1 at
479
+ # runtime, so they need the same backing Series.
480
+ if idx is None or idx >= 1:
223
481
  out.add(n.object.name)
224
482
  if isinstance(n, (list, tuple)):
225
483
  for x in n:
@@ -236,9 +494,267 @@ class SecurityEmitter:
236
494
  walk(node)
237
495
  return out
238
496
 
497
+ def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
498
+ """Collect HTF OHLC history needed by a security expression and any
499
+ mutable-global rebinds replayed inside that security evaluator."""
500
+ fields = self._collect_security_ohlc_hist_fields(item.get("expr_node"))
501
+ for name in item.get("mutable_globals", []) or []:
502
+ info = self._global_mutable_infos.get(name)
503
+ if info is None:
504
+ continue
505
+ for stmt in getattr(info, "source_stmts", []) or []:
506
+ fields |= self._collect_security_ohlc_hist_fields(stmt)
507
+ return fields
508
+
239
509
  def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
240
510
  return f"_sec{sec_id}_hist_{field}"
241
511
 
512
+ def _security_bar_hist_type(self, field: str) -> str:
513
+ return SECURITY_BAR_FIELD_TYPES.get(field, "double")
514
+
515
+ def _security_bar_field_expr(self, field: str) -> str:
516
+ return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
517
+
518
+ @staticmethod
519
+ def _security_tuple_result_default(cpp_type: str, tuple_size: int) -> str:
520
+ vals = ", ".join("na<double>()" for _ in range(max(0, tuple_size)))
521
+ return f"{cpp_type}{{{vals}}}"
522
+
523
+ def _collect_security_ta_hist_indices(self, node) -> set[int]:
524
+ """Which security TA call-site indices need HTF history (subscript index >= 1).
525
+
526
+ ``request.security(..., ta.ema(close, 55)[1], ...)`` reads a *confirmed*
527
+ HTF TA value at a past-bar offset. The inner TA call runs in the security
528
+ (HTF) context and commits one value per COMPLETED HTF bar; offsets read a
529
+ per-site ``Series`` filled (gated on ``is_complete``) in
530
+ ``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
531
+ OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
532
+ and needs no Series, so only index >= 1 registers here."""
533
+ out: set[int] = set()
534
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
535
+
536
+ def resolve_ta_site(obj, resolving: set[str] | None = None):
537
+ """_get_ta_site only matches the literal ta.* FuncCall node by
538
+ identity; fall back through global_expr_map for an indirect
539
+ binding (``v = ta.ema(close, 55)`` then ``...v[1]...``), mirroring
540
+ the same fallback in _build_security_expr's Subscript branch."""
541
+ site = self._get_ta_site(obj)
542
+ if site is not None:
543
+ return site
544
+ if isinstance(obj, Identifier):
545
+ if resolving is None:
546
+ resolving = set()
547
+ if obj.name in global_expr_map and obj.name not in resolving:
548
+ resolving.add(obj.name)
549
+ return resolve_ta_site(global_expr_map[obj.name], resolving)
550
+ return None
551
+
552
+ def walk(n):
553
+ if n is None:
554
+ return
555
+ if isinstance(n, Subscript):
556
+ site = resolve_ta_site(n.object)
557
+ if site is not None:
558
+ idx_lit = self._resolve_security_index_literal(n.index)
559
+ if idx_lit is not None and idx_lit >= 1:
560
+ site_idx = self._ta_index_by_site_id.get(id(site))
561
+ if site_idx is not None:
562
+ out.add(site_idx)
563
+ if isinstance(n, (list, tuple)):
564
+ for x in n:
565
+ walk(x)
566
+ return
567
+ for _k, v in getattr(n, "__dict__", {}).items():
568
+ if isinstance(v, ASTNode):
569
+ walk(v)
570
+ elif isinstance(v, (list, tuple)):
571
+ for x in v:
572
+ if isinstance(x, ASTNode):
573
+ walk(x)
574
+
575
+ walk(node)
576
+ return out
577
+
578
+ def _security_ta_hist_series_cpp(self, member_name: str) -> str:
579
+ """Per-(sec, site) ``Series<double>`` backing ``ta.<fn>(...)[k>=1]`` HTF history."""
580
+ return f"{member_name}_hist"
581
+
582
+ def _security_ta_hist_series_names(self, sec_id: int) -> list[str]:
583
+ """Hist Series names for every security TA site (and variant) read at an
584
+ offset >= 1 in sec ``sec_id``."""
585
+ info = self._security_eval_info[sec_id]
586
+ names: list[str] = []
587
+ for idx in sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ())):
588
+ for variant in (info.get("ta_variants") or {}).get(idx, []):
589
+ names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
590
+ return names
591
+
592
+ def _collect_security_expr_hist_subscripts(
593
+ self, node, resolving: set[str] | None = None
594
+ ) -> list[Subscript]:
595
+ """Subscripted helper-call results needing security-context history."""
596
+ if node is None:
597
+ return []
598
+ if resolving is None:
599
+ resolving = set()
600
+
601
+ out: list[Subscript] = []
602
+ seen: set[int] = set()
603
+
604
+ def add(n: Subscript) -> None:
605
+ key = id(n)
606
+ if key not in seen:
607
+ seen.add(key)
608
+ out.append(n)
609
+
610
+ def walk(n) -> None:
611
+ if n is None:
612
+ return
613
+ if isinstance(n, Identifier):
614
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
615
+ if n.name in global_expr_map and n.name not in resolving:
616
+ resolving.add(n.name)
617
+ walk(global_expr_map[n.name])
618
+ resolving.remove(n.name)
619
+ return
620
+ if (
621
+ isinstance(n, Subscript)
622
+ and isinstance(n.object, FuncCall)
623
+ and self._get_ta_site(n.object) is None
624
+ ):
625
+ add(n)
626
+ if isinstance(n, (list, tuple)):
627
+ for x in n:
628
+ walk(x)
629
+ return
630
+ for _k, v in getattr(n, "__dict__", {}).items():
631
+ if isinstance(v, ASTNode):
632
+ walk(v)
633
+ elif isinstance(v, (list, tuple)):
634
+ for x in v:
635
+ if isinstance(x, ASTNode):
636
+ walk(x)
637
+
638
+ walk(node)
639
+ return out
640
+
641
+ def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
642
+ names = []
643
+ for (sid, _node_id), meta in sorted(self._security_expr_hist_by_node.items()):
644
+ if sid == sec_id:
645
+ names.append(meta["name"])
646
+ return names
647
+
648
+ def _emit_security_expr_hist_members(
649
+ self, sec_id: int, expr_node, lines: list[str], mbb_suffix: str
650
+ ) -> None:
651
+ for idx, node in enumerate(self._collect_security_expr_hist_subscripts(expr_node)):
652
+ cpp_t = self._infer_type(node.object)
653
+ if cpp_t not in ("double", "int", "bool"):
654
+ cpp_t = "double"
655
+ name = f"_sec{sec_id}_expr_hist_{idx}"
656
+ self._security_expr_hist_by_node[(sec_id, id(node))] = {
657
+ "name": name,
658
+ "type": cpp_t,
659
+ }
660
+ lines.append(f" Series<{cpp_t}> {name}{mbb_suffix};")
661
+
662
+ def _build_security_math_call(
663
+ self,
664
+ sec_id: int,
665
+ func_name: str,
666
+ node: FuncCall,
667
+ ta_range,
668
+ ta_results: dict,
669
+ resolving: set[str],
670
+ security_mutable_names: set[str],
671
+ helper_binding_stack: tuple[dict[str, ASTNode], ...],
672
+ emitted_lines: list[str] | None,
673
+ ) -> str:
674
+ visit = lambda arg: self._build_security_expr(
675
+ sec_id,
676
+ arg,
677
+ ta_range,
678
+ ta_results,
679
+ resolving,
680
+ security_mutable_names,
681
+ helper_binding_stack,
682
+ emitted_lines,
683
+ )
684
+ args = _merge_kwargs(
685
+ node.args,
686
+ node.kwargs,
687
+ sigs.get_param_names("math", func_name),
688
+ visit,
689
+ )
690
+ if func_name == "round" and len(args) == 2:
691
+ return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
692
+ if func_name == "round_to_mintick":
693
+ x = args[0] if args else "0.0"
694
+ return f"round_to_mintick({x})"
695
+ if func_name == "todegrees":
696
+ x = args[0] if args else "0.0"
697
+ return f"({x} * 180.0 / M_PI)"
698
+ if func_name == "toradians":
699
+ x = args[0] if args else "0.0"
700
+ return f"({x} * M_PI / 180.0)"
701
+ if func_name == "random":
702
+ lo = args[0] if len(args) > 0 else "0.0"
703
+ hi = args[1] if len(args) > 1 else "1.0"
704
+ seed = args[2] if len(args) > 2 else "0"
705
+ call_site = self._random_call_counter
706
+ self._random_call_counter += 1
707
+ return f"pine_random({lo}, {call_site}u, {hi}, (uint32_t)({seed}), bar_index_)"
708
+ if func_name == "avg" and len(args) > 2:
709
+ sum_expr = " + ".join(f"(double)({a})" for a in args)
710
+ return f"(({sum_expr}) / {len(args)}.0)"
711
+ if func_name in ("min", "max"):
712
+ return _math_minmax_na_expr(func_name, args)
713
+ if func_name in MATH_FUNC_MAP:
714
+ mapped = MATH_FUNC_MAP[func_name]
715
+ if "{0}" in mapped:
716
+ return mapped.format(*args)
717
+ return f"{mapped}({', '.join(args)})"
718
+ return f"0.0 /* unsupported: math.{func_name} */"
719
+
720
+ def _security_timeframe_expr(self, sec_id: int) -> str:
721
+ """C++ expression for the timeframe of a request.security evaluator."""
722
+ info = self._security_eval_info[sec_id]
723
+ if info.get("tf"):
724
+ return f'"{info["tf"]}"'
725
+ if info.get("tf_expr"):
726
+ return info["tf_expr"]
727
+ return "input_tf_"
728
+
729
+ def _build_security_timeframe_member(self, sec_id: int, member: str) -> str | None:
730
+ """Lower timeframe.* reads inside request.security to the requested TF."""
731
+ tf = self._security_timeframe_expr(sec_id)
732
+ if member == "period":
733
+ return tf
734
+ if member == "main_period":
735
+ return "main_period()"
736
+ if member == "multiplier":
737
+ return f"tf_multiplier({tf})"
738
+ if member == "isintraday":
739
+ return f"tf_is_intraday({tf})"
740
+ if member == "isminutes":
741
+ return f"(tf_is_intraday({tf}) && !tf_is_seconds({tf}))"
742
+ if member == "isdaily":
743
+ return f"tf_is_daily({tf})"
744
+ if member == "isweekly":
745
+ return f"tf_is_weekly({tf})"
746
+ if member == "ismonthly":
747
+ return f"tf_is_monthly({tf})"
748
+ if member == "isdwm":
749
+ return f"(tf_is_daily({tf}) || tf_is_weekly({tf}) || tf_is_monthly({tf}))"
750
+ if member == "isseconds":
751
+ return f"tf_is_seconds({tf})"
752
+ if member == "in_seconds":
753
+ return f"tf_to_seconds({tf})"
754
+ if member == "isticks":
755
+ return "false"
756
+ return None
757
+
242
758
  @staticmethod
243
759
  def _security_series_binding(series_name: str) -> str:
244
760
  return f"@series:{series_name}"
@@ -767,6 +1283,16 @@ class SecurityEmitter:
767
1283
  if bound is not None:
768
1284
  if isinstance(bound, str):
769
1285
  return collected
1286
+ # Guard against a cyclic helper binding: an identifier bound to
1287
+ # an expression that transitively references the same binding
1288
+ # (e.g. mutually-referential helper params) would recurse here
1289
+ # forever — the mutable/global/func paths below already carry a
1290
+ # `resolving` guard, this path did not. Keyed by the bound node
1291
+ # so acyclic bindings are unaffected (byte-identical emission).
1292
+ bind_key = f"bind:{id(bound)}"
1293
+ if bind_key in resolving:
1294
+ return collected
1295
+ resolving.add(bind_key)
770
1296
  self._collect_security_ta_binding_stacks(
771
1297
  bound,
772
1298
  resolving,
@@ -775,6 +1301,7 @@ class SecurityEmitter:
775
1301
  inline_ta_indices,
776
1302
  inline_helper,
777
1303
  )
1304
+ resolving.discard(bind_key)
778
1305
  return collected
779
1306
 
780
1307
  mutable_info = self._global_mutable_infos.get(expr_node.name)
@@ -1160,6 +1687,59 @@ class SecurityEmitter:
1160
1687
  )
1161
1688
  return out
1162
1689
 
1690
+ def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
1691
+ """Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
1692
+ ``is_complete``.
1693
+
1694
+ ``request.security(..., [high[1], low[1], ...], ...)`` reads HTF OHLC at
1695
+ past-bar offsets. Each offset is backed by a per-field Series whose
1696
+ history must advance once per COMPLETED HTF bar — not once per (partial)
1697
+ chart-bar evaluation. ``_eval_security_N`` fires on every chart bar; only
1698
+ the bar that completes the HTF aggregate has ``is_complete == true``.
1699
+ Pushing unconditionally advanced the offset history every chart bar, so
1700
+ ``high[1]`` resolved to a recent partial bar instead of the prior
1701
+ completed HTF bar. Gate all pushes for this sec in one combined block."""
1702
+ fields = sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ()))
1703
+ if not fields:
1704
+ return
1705
+ lines.append(" if (is_complete) {")
1706
+ for field in fields:
1707
+ lines.append(
1708
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push({self._security_bar_field_expr(field)});"
1709
+ )
1710
+ lines.append(" }")
1711
+
1712
+ def _emit_security_ta_hist_pushes(
1713
+ self, sec_id: int, info: dict, ta_results: dict, lines: list[str]
1714
+ ) -> None:
1715
+ """Emit the TA history-offset Series pushes for ``sec_id``, gated on
1716
+ ``is_complete`` (mirrors ``_emit_security_ohlc_hist_pushes``).
1717
+
1718
+ ``request.security(..., ta.ema(close, 55)[1], ...)`` reads a confirmed
1719
+ HTF TA value at a past-bar offset. The committed value (``_secval_*``,
1720
+ produced with ``.compute()`` only when ``is_complete``) is pushed onto a
1721
+ per-site Series once per COMPLETED HTF bar, AFTER the expression
1722
+ assignment so the offset read sees the prior completed bar. Pushing on
1723
+ every chart-bar eval would otherwise advance the offset history per
1724
+ partial eval / chart tick (the bug this replaces, where the chart-context
1725
+ ``_hist_call`` buffer advanced on ``is_first_tick_``)."""
1726
+ indices = sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
1727
+ if not indices:
1728
+ return
1729
+ pushes: list[str] = []
1730
+ for idx in indices:
1731
+ for variant in (info.get("ta_variants") or {}).get(idx, []):
1732
+ result_name = ta_results.get((idx, variant["signature"]))
1733
+ if result_name is None:
1734
+ continue
1735
+ hist = self._security_ta_hist_series_cpp(variant["member_name"])
1736
+ pushes.append(f" {hist}.push({result_name});")
1737
+ if not pushes:
1738
+ return
1739
+ lines.append(" if (is_complete) {")
1740
+ lines.extend(pushes)
1741
+ lines.append(" }")
1742
+
1163
1743
  def _emit_security_evaluators(self, lines: list[str]) -> None:
1164
1744
  """Emit _eval_security_N() methods and evaluate_security() dispatch."""
1165
1745
  if not self._security_calls:
@@ -1223,7 +1803,7 @@ class SecurityEmitter:
1223
1803
  )
1224
1804
  var_name = variant["result_name"]
1225
1805
  sec_name = variant["member_name"]
1226
- lines.append(f" auto {var_name} = is_complete "
1806
+ lines.append(f" auto {var_name} = security_series_slot_is_new({sec_id}) "
1227
1807
  f"? {sec_name}.compute({compute_args}) "
1228
1808
  f": {sec_name}.recompute({compute_args});")
1229
1809
  ta_results[(idx, variant["signature"])] = var_name
@@ -1256,10 +1836,8 @@ class SecurityEmitter:
1256
1836
  emitted_lines=lines,
1257
1837
  )
1258
1838
  lines.append(f" _req_sec_{sec_id}_{i} = {el_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
- )
1839
+ self._emit_security_ohlc_hist_pushes(sec_id, lines)
1840
+ self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
1263
1841
  lines.append(" }")
1264
1842
  lines.append("")
1265
1843
  continue
@@ -1287,10 +1865,8 @@ class SecurityEmitter:
1287
1865
  )
1288
1866
  else:
1289
1867
  lines.append(f" _req_sec_{sec_id} = {expr_cpp};")
1290
- for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1291
- lines.append(
1292
- f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
1293
- )
1868
+ self._emit_security_ohlc_hist_pushes(sec_id, lines)
1869
+ self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
1294
1870
  lines.append(" }")
1295
1871
  lines.append("")
1296
1872
 
@@ -1327,6 +1903,10 @@ class SecurityEmitter:
1327
1903
  lines.append(
1328
1904
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1329
1905
  )
1906
+ for name in self._security_ta_hist_series_names(sec_id):
1907
+ lines.append(f" {name}.clear();")
1908
+ for name in self._security_expr_hist_series_names(sec_id):
1909
+ lines.append(f" {name}.clear();")
1330
1910
  lines.append(" break;")
1331
1911
  continue
1332
1912
  if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
@@ -1349,16 +1929,51 @@ class SecurityEmitter:
1349
1929
  lines.append(
1350
1930
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1351
1931
  )
1932
+ for name in self._security_ta_hist_series_names(sec_id):
1933
+ lines.append(f" {name}.clear();")
1934
+ for name in self._security_expr_hist_series_names(sec_id):
1935
+ lines.append(f" {name}.clear();")
1936
+ lines.append(" break;")
1937
+ elif returns_tuple and tuple_size and tuple_size > 0:
1938
+ site = self._get_ta_site(expr_node)
1939
+ ta_name = self._ta_name_from_site(site) if site is not None else ""
1940
+ ctype = {
1941
+ "macd": "ta::MACDResult",
1942
+ "supertrend": "ta::SupertrendResult",
1943
+ "dmi": "ta::DMIResult",
1944
+ "bb": "ta::BBResult",
1945
+ "kc": "ta::KCResult",
1946
+ "vwap_bands": "ta::VWAPBandsResult",
1947
+ }.get(ta_name, "std::tuple<double, double>")
1948
+ lines.append(f" case {sec_id}:")
1949
+ lines.append(
1950
+ f" _req_sec_{sec_id} = "
1951
+ f"{self._security_tuple_result_default(ctype, tuple_size)};"
1952
+ )
1953
+ for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1954
+ lines.append(
1955
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1956
+ )
1957
+ for name in self._security_ta_hist_series_names(sec_id):
1958
+ lines.append(f" {name}.clear();")
1959
+ for name in self._security_expr_hist_series_names(sec_id):
1960
+ lines.append(f" {name}.clear();")
1352
1961
  lines.append(" break;")
1353
1962
  else:
1354
1963
  hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
1355
- if hist:
1964
+ ta_hist_names = self._security_ta_hist_series_names(sec_id)
1965
+ expr_hist_names = self._security_expr_hist_series_names(sec_id)
1966
+ if hist or ta_hist_names or expr_hist_names:
1356
1967
  lines.append(f" case {sec_id}:")
1357
1968
  lines.append(f" _req_sec_{sec_id} = na<double>();")
1358
1969
  for field in sorted(hist):
1359
1970
  lines.append(
1360
1971
  f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1361
1972
  )
1973
+ for name in ta_hist_names:
1974
+ lines.append(f" {name}.clear();")
1975
+ for name in expr_hist_names:
1976
+ lines.append(f" {name}.clear();")
1362
1977
  lines.append(" break;")
1363
1978
  else:
1364
1979
  lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
@@ -1406,9 +2021,7 @@ class SecurityEmitter:
1406
2021
  emitted_lines,
1407
2022
  )
1408
2023
  bar_fields = {
1409
- "close": "bar.close", "high": "bar.high",
1410
- "low": "bar.low", "open": "bar.open",
1411
- "volume": "bar.volume",
2024
+ **SECURITY_BAR_FIELD_EXPRS,
1412
2025
  "hl2": "((bar.high + bar.low) / 2.0)",
1413
2026
  "hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
1414
2027
  "ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
@@ -1439,6 +2052,15 @@ class SecurityEmitter:
1439
2052
  resolving.remove(expr_node.name)
1440
2053
  return resolved
1441
2054
 
2055
+ if (
2056
+ isinstance(expr_node, MemberAccess)
2057
+ and isinstance(expr_node.object, Identifier)
2058
+ and expr_node.object.name == "timeframe"
2059
+ ):
2060
+ resolved = self._build_security_timeframe_member(sec_id, expr_node.member)
2061
+ if resolved is not None:
2062
+ return resolved
2063
+
1442
2064
  if isinstance(expr_node, Subscript):
1443
2065
  index_cpp = self._build_security_expr(
1444
2066
  sec_id,
@@ -1469,30 +2091,129 @@ class SecurityEmitter:
1469
2091
  emitted_lines,
1470
2092
  )
1471
2093
  return f"{obj_cpp}[{index_cpp}]"
1472
- if expr_node.object.name in SECURITY_OHLC_BAR_FIELDS:
2094
+ if expr_node.object.name in SECURITY_BAR_FIELDS:
2095
+ field = expr_node.object.name
1473
2096
  idx_lit = self._literal_int_for_security_index(expr_node.index)
1474
2097
  if idx_lit is not None:
1475
- bar_map = {
1476
- "open": "bar.open",
1477
- "high": "bar.high",
1478
- "low": "bar.low",
1479
- "close": "bar.close",
1480
- "volume": "bar.volume",
1481
- }
1482
2098
  if idx_lit == 0:
1483
- return bar_map[expr_node.object.name]
2099
+ return self._security_bar_field_expr(field)
1484
2100
  if idx_lit >= 1:
1485
2101
  # lookahead_off: we evaluate when an HTF bar completes; `bar` is that
1486
- # bar. On the HTF series, high[0]/close is the current (just-finished)
1487
- # bar; high[1] is one HTF bar back = hist[field][0] *before* we push
1488
- # `bar` (Series [0] = most recent prior push). high[k] -> hist[k-1].
1489
- field = expr_node.object.name
2102
+ # bar. On the HTF series, high[0]/time[0] is the current
2103
+ # (just-finished) bar; high[1]/time[1] is one HTF bar back
2104
+ # = hist[field][0] *before* we push `bar` (Series [0] =
2105
+ # most recent prior push). field[k] -> hist[k-1].
1490
2106
  hist = self._security_ohlc_hist_series_cpp(sec_id, field)
1491
2107
  return f"{hist}[{idx_lit - 1}]"
2108
+ if idx_lit is not None:
2109
+ self._codegen_error(
2110
+ expr_node,
2111
+ "request.security() bar-field history index must be non-negative",
2112
+ )
2113
+ hist = self._security_ohlc_hist_series_cpp(sec_id, field)
2114
+ cpp_t = self._security_bar_hist_type(field)
2115
+ current = self._security_bar_field_expr(field)
2116
+ return (
2117
+ f"([&]() -> {cpp_t} {{ "
2118
+ f"int _hidx = (int)({index_cpp}); "
2119
+ f"return (_hidx <= 0) ? {current} : {hist}[_hidx - 1]; "
2120
+ f"}}())"
2121
+ )
2122
+
2123
+ # Indirect TA binding: ``v = ta.ema(close, 55)`` then
2124
+ # ``request.security(..., v[1], ...)``. _get_ta_site below only
2125
+ # matches the literal ta.* FuncCall node by identity, so a bare
2126
+ # Identifier subscript target silently misses it and falls
2127
+ # through to a chart-resolution read of the wrong (non-HTF)
2128
+ # series. Resolve through the same global_expr_map the
2129
+ # non-subscript Identifier path above already uses, and
2130
+ # recurse on a synthetic Subscript over the resolved value so
2131
+ # it re-enters this whole branch (TA site, OHLC field, or
2132
+ # helper binding, whichever the resolved expression turns out
2133
+ # to be) instead of duplicating that dispatch here.
2134
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
2135
+ if (expr_node.object.name in global_expr_map
2136
+ and expr_node.object.name not in resolving):
2137
+ resolving.add(expr_node.object.name)
2138
+ resolved = self._build_security_expr(
2139
+ sec_id,
2140
+ Subscript(object=global_expr_map[expr_node.object.name], index=expr_node.index),
2141
+ ta_range,
2142
+ ta_results,
2143
+ resolving,
2144
+ security_mutable_names,
2145
+ helper_binding_stack,
2146
+ emitted_lines,
2147
+ )
2148
+ resolving.remove(expr_node.object.name)
2149
+ return resolved
2150
+ if (
2151
+ isinstance(expr_node.object, FuncCall)
2152
+ and self._get_ta_site(expr_node.object) is None
2153
+ ):
2154
+ meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
2155
+ hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
2156
+ cpp_t = meta["type"] if meta else "double"
2157
+ inner = self._build_security_expr(
2158
+ sec_id,
2159
+ expr_node.object,
2160
+ ta_range,
2161
+ ta_results,
2162
+ resolving,
2163
+ security_mutable_names,
2164
+ helper_binding_stack,
2165
+ emitted_lines,
2166
+ )
2167
+ return (
2168
+ f"([&]() -> {cpp_t} {{ "
2169
+ f"{cpp_t} _hv = ({inner}); "
2170
+ f"int _hidx = (int)({index_cpp}); "
2171
+ f"{cpp_t} _out = (_hidx <= 0) ? _hv : {hist}[_hidx - 1]; "
2172
+ f"if (is_complete) {hist}.push(_hv); "
2173
+ f"return _out; }}())"
2174
+ )
2175
+ ta_site = self._get_ta_site(expr_node.object)
2176
+ if ta_site is not None:
2177
+ # ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
2178
+ # runs in the HTF (security) context and commits one value per
2179
+ # COMPLETED HTF bar. Read the already-emitted security TA result —
2180
+ # offset 0 reuses the current committed value (``_secval_*``),
2181
+ # offset k>=1 reads a per-site Series that advances on
2182
+ # ``is_complete`` (HTF-bar boundary) in ``_eval_security_N``. The
2183
+ # buggy generic path re-lowered the inner TA to the CHART member
2184
+ # and gated a ``_hist_call`` buffer on ``is_first_tick_`` (chart
2185
+ # tick), so without a magnifier it advanced every chart bar and
2186
+ # produced the chart-tf TA instead of the confirmed HTF value.
2187
+ idx = self._ta_index_by_site_id.get(id(ta_site))
2188
+ sig = self._security_binding_stack_signature(helper_binding_stack)
2189
+ idx_lit = self._resolve_security_index_literal(
2190
+ expr_node.index, helper_binding_stack
2191
+ )
2192
+ if idx_lit is None:
1492
2193
  self._codegen_error(
1493
2194
  expr_node,
1494
- "request.security() OHLC history index must be a literal integer (e.g. high[1])",
2195
+ "request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
2196
+ )
2197
+ if idx_lit == 0:
2198
+ # Current completed-HTF-bar value: reuse the bare-TA emission.
2199
+ return self._build_security_expr(
2200
+ sec_id,
2201
+ expr_node.object,
2202
+ ta_range,
2203
+ ta_results,
2204
+ resolving,
2205
+ security_mutable_names,
2206
+ helper_binding_stack,
2207
+ emitted_lines,
1495
2208
  )
2209
+ member_name = self._security_ta_variant_names.get(
2210
+ (sec_id, idx, sig),
2211
+ f"_sec{sec_id}_{ta_site.member_name}",
2212
+ )
2213
+ hist = self._security_ta_hist_series_cpp(member_name)
2214
+ # ta(...)[k] -> hist[k-1]: hist[0] is the prior completed HTF bar
2215
+ # (current value not yet pushed — push happens after this assign).
2216
+ return f"{hist}[{idx_lit - 1}]"
1496
2217
 
1497
2218
  if isinstance(expr_node, BinOp):
1498
2219
  left = self._build_security_expr(
@@ -1569,6 +2290,37 @@ class SecurityEmitter:
1569
2290
  resolving.remove(call_key)
1570
2291
  return resolved
1571
2292
 
2293
+ if (
2294
+ isinstance(expr_node, FuncCall)
2295
+ and isinstance(expr_node.callee, MemberAccess)
2296
+ and isinstance(expr_node.callee.object, Identifier)
2297
+ and expr_node.callee.object.name == "math"
2298
+ ):
2299
+ # A rolling/stateful math reducer (e.g. math.sum -> math::Sum) is
2300
+ # precomputed into a committed _secval_* by the security TA
2301
+ # machinery, exactly like a ta.* call. Return that committed value
2302
+ # instead of falling through to _build_security_math_call, whose
2303
+ # inline lowering only covers scalar math and emits a broken
2304
+ # "unsupported: math.<f>" 0.0 for a reducer. Scalar math
2305
+ # (abs/round/min/max/...) is not a TA site, so this is a no-op.
2306
+ math_site = self._get_ta_site(expr_node)
2307
+ if math_site is not None:
2308
+ math_idx = self._ta_index_by_site_id.get(id(math_site))
2309
+ math_sig = self._security_binding_stack_signature(helper_binding_stack)
2310
+ if math_idx is not None and (math_idx, math_sig) in ta_results:
2311
+ return ta_results[(math_idx, math_sig)]
2312
+ return self._build_security_math_call(
2313
+ sec_id,
2314
+ expr_node.callee.member,
2315
+ expr_node,
2316
+ ta_range,
2317
+ ta_results,
2318
+ resolving,
2319
+ security_mutable_names,
2320
+ helper_binding_stack,
2321
+ emitted_lines,
2322
+ )
2323
+
1572
2324
  site = self._get_ta_site(expr_node)
1573
2325
  if site:
1574
2326
  idx = self._ta_index_by_site_id.get(id(site))
@@ -1589,7 +2341,7 @@ class SecurityEmitter:
1589
2341
  helper_binding_stack,
1590
2342
  emitted_lines,
1591
2343
  )
1592
- return f"(is_complete ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
2344
+ return f"(security_series_slot_is_new({sec_id}) ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
1593
2345
 
1594
2346
  result = self._visit_expr(expr_node)
1595
2347
  return self._rewrite_security_cpp(result, sec_id, security_mutable_names, helper_binding_stack)