@pineforge/codegen-pyodide 0.7.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.
Files changed (37) hide show
  1. package/README.md +25 -0
  2. package/index.mjs +20 -0
  3. package/package.json +32 -0
  4. package/pineforge_codegen/__init__.py +53 -0
  5. package/pineforge_codegen/analyzer/__init__.py +60 -0
  6. package/pineforge_codegen/analyzer/base.py +1566 -0
  7. package/pineforge_codegen/analyzer/call_handlers.py +895 -0
  8. package/pineforge_codegen/analyzer/contracts.py +163 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +118 -0
  10. package/pineforge_codegen/analyzer/tables.py +204 -0
  11. package/pineforge_codegen/analyzer/types.py +261 -0
  12. package/pineforge_codegen/ast_nodes.py +293 -0
  13. package/pineforge_codegen/codegen/__init__.py +78 -0
  14. package/pineforge_codegen/codegen/base.py +1381 -0
  15. package/pineforge_codegen/codegen/emit_top.py +875 -0
  16. package/pineforge_codegen/codegen/helpers.py +163 -0
  17. package/pineforge_codegen/codegen/helpers_syminfo.py +132 -0
  18. package/pineforge_codegen/codegen/input.py +189 -0
  19. package/pineforge_codegen/codegen/security.py +1564 -0
  20. package/pineforge_codegen/codegen/ta.py +298 -0
  21. package/pineforge_codegen/codegen/tables.py +683 -0
  22. package/pineforge_codegen/codegen/types.py +592 -0
  23. package/pineforge_codegen/codegen/visit_call.py +1387 -0
  24. package/pineforge_codegen/codegen/visit_expr.py +729 -0
  25. package/pineforge_codegen/codegen/visit_stmt.py +766 -0
  26. package/pineforge_codegen/errors.py +98 -0
  27. package/pineforge_codegen/lexer.py +531 -0
  28. package/pineforge_codegen/parser.py +1198 -0
  29. package/pineforge_codegen/pragmas.py +117 -0
  30. package/pineforge_codegen/signatures.py +808 -0
  31. package/pineforge_codegen/support_checker.py +1223 -0
  32. package/pineforge_codegen/symbols.py +118 -0
  33. package/pineforge_codegen/tokens.py +406 -0
  34. package/pineforge_codegen/tv_input_choices.py +86 -0
  35. package/pineforge_codegen-0.7.0.tar.gz +0 -0
  36. package/release.json +7 -0
  37. package/tables.json +1653 -0
@@ -0,0 +1,1564 @@
1
+ """``request.security()`` lowering for the codegen.
2
+
3
+ This is the most stateful mixin in the ``codegen/`` package. It owns the
4
+ ~30 helpers that lower Pine ``request.security(...)`` calls into
5
+ per-security ``_eval_security_N()`` methods, an ``evaluate_security()``
6
+ dispatch, the ``clear_security()`` reset path, and the supporting binding,
7
+ TA-variant, and mutable-global rebind machinery.
8
+
9
+ Mixin contract — the host class (``CodeGen``) must provide the following
10
+ attributes (all set by ``CodeGen.__init__`` unless noted):
11
+
12
+ - ``self.ctx`` (``AnalyzerContext``): symbol table source. Reads
13
+ ``ctx.ast.body``, ``ctx.ta_call_sites``, ``ctx.global_expr_map``,
14
+ ``ctx.func_series_vars``, and ``ctx.global_mutable_infos``.
15
+ - ``self._global_mutable_infos`` (``dict[str, MutableInfo]``):
16
+ per-mutable-global metadata captured by the analyzer
17
+ (``is_var``/``is_series``/``pine_type``/``source_stmts``).
18
+ - ``self._security_calls`` (``list[dict]``): normalized security-call
19
+ records. Built by this mixin's ``_normalize_security_call``.
20
+ - ``self._security_eval_info`` (``list[dict]``): per ``sec_id`` eval
21
+ metadata (``ta_indices``, ``ta_variants``, ``ta_binding_stacks``,
22
+ ``inline_helper_ta_indices``, ``mutable_globals``, ...).
23
+ - ``self._security_inline_counter`` (``int``): used by
24
+ ``_security_next_inline_name`` for unique helper temporary names.
25
+ - ``self._security_ta_variant_names``
26
+ (``dict[tuple[int, int, tuple], str]``):
27
+ ``(sec_id, ta_idx, signature) -> C++ member name``.
28
+ - ``self._security_ohlc_hist_fields_by_sec`` (``dict[int, set[str]]``):
29
+ set in ``CodeGen.generate()`` before ``_emit_security_evaluators`` runs.
30
+ - ``self._ta_index_by_site_id`` (``dict[int, int]``): TA call-site
31
+ identity → index in ``ctx.ta_call_sites``.
32
+ - ``self._func_names`` (``set[str]``): user-defined function names.
33
+ - ``self._func_info_map`` (``dict[str, FuncInfo]``): name -> FuncInfo.
34
+
35
+ Sibling-mixin methods consumed via ``self``:
36
+
37
+ - ``self._safe_name`` / ``self._get_target_name`` (``NamingHelper``).
38
+ - ``self._series_type_for`` / ``self._type_for_decl`` /
39
+ ``self._infer_cpp_type_for_security_elem`` (``TypeInferer``).
40
+ - ``self._get_ta_site`` / ``self._security_ta_compute_args_for_site`` /
41
+ ``self._ta_name_from_site`` (``TaSiteHelper``).
42
+ ``_security_ta_compute_args_for_site`` stays on ``TaSiteHelper`` because
43
+ it is structurally a TA helper that calls back into this mixin via
44
+ ``self._build_security_expr``.
45
+ - ``self._merge_ta_call_args`` (``CodeGen.base``): not security-specific,
46
+ kept on base.
47
+ - ``self._visit_expr`` (``CodeGen.base``): the fallback expression
48
+ renderer used by ``_build_security_expr``.
49
+ - ``self._codegen_error`` (``CodeGen.base``).
50
+ - ``self._emit_ta_runtime_reset`` (``CodeGen.base``): called from
51
+ ``_emit_security_evaluators`` to gate the TA reset before the dispatch
52
+ switch.
53
+
54
+ The mixin avoids importing from ``base.py`` to stay free of cycles; all
55
+ tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
56
+ ``..analyzer``, and ``..symbols``.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
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,
66
+ )
67
+ from ..analyzer import (
68
+ FuncInfo, TACallSite, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
69
+ )
70
+ from ..symbols import PineType
71
+ from .tables import PINE_TYPE_TO_CPP, SECURITY_OHLC_BAR_FIELDS
72
+
73
+
74
+ class SecurityEmitter:
75
+ """Mixin owning ``request.security()`` lowering: evaluators, dispatch,
76
+ rebind/binding/TA-variant machinery, and the per-call helper plan.
77
+
78
+ Mixed into ``CodeGen``; not intended to be instantiated standalone."""
79
+
80
+ def _normalize_security_call(self, item) -> dict:
81
+ if hasattr(item, "sec_id"):
82
+ return {
83
+ "sec_id": item.sec_id,
84
+ "tf_node": item.timeframe,
85
+ "expr_node": item.expression,
86
+ "returns_tuple": item.returns_tuple,
87
+ "tuple_size": item.tuple_size,
88
+ "gaps_node": item.gaps,
89
+ "lookahead_node": item.lookahead,
90
+ "ta_range": item.ta_range,
91
+ "depends_on_mutable_globals": bool(getattr(item, "depends_on_mutable_globals", False)),
92
+ "mutable_globals": list(getattr(item, "mutable_globals", ()) or ()),
93
+ "is_lower_tf_array": bool(getattr(item, "is_lower_tf_array", False)),
94
+ }
95
+ return {
96
+ "sec_id": item[0],
97
+ "tf_node": item[1] if len(item) > 1 else None,
98
+ "expr_node": item[2] if len(item) > 2 else None,
99
+ "returns_tuple": item[3] if len(item) > 3 else False,
100
+ "tuple_size": item[4] if len(item) > 4 else 0,
101
+ "gaps_node": item[5] if len(item) > 5 else None,
102
+ "lookahead_node": item[6] if len(item) > 6 else None,
103
+ "ta_range": item[7] if len(item) > 7 else None,
104
+ "depends_on_mutable_globals": False,
105
+ "mutable_globals": [],
106
+ "is_lower_tf_array": False,
107
+ }
108
+
109
+ def _security_state_name(self, sec_id: int, name: str) -> str:
110
+ return f"_sec{sec_id}_{self._safe_name(name)}"
111
+
112
+ def _security_init_flag_name(self, sec_id: int, name: str) -> str:
113
+ return f"{self._security_state_name(sec_id, name)}_initialized"
114
+
115
+ def _security_cpp_type_for_mutable(self, name: str, info) -> str:
116
+ if getattr(info, "is_series", False):
117
+ return self._series_type_for(name)
118
+ return PINE_TYPE_TO_CPP.get(getattr(info, "pine_type", PineType.FLOAT), "double")
119
+
120
+ def _security_relevant_top_level_stmts(self, mutable_globals: list[str]) -> list[ASTNode]:
121
+ if not mutable_globals:
122
+ return []
123
+ source_ids: set[int] = set()
124
+ for name in mutable_globals:
125
+ info = self._global_mutable_infos.get(name)
126
+ if info is None:
127
+ continue
128
+ for stmt in getattr(info, "source_stmts", []) or []:
129
+ source_ids.add(id(stmt))
130
+ return [stmt for stmt in self.ctx.ast.body if id(stmt) in source_ids]
131
+
132
+ def _rewrite_security_cpp(
133
+ self,
134
+ cpp: str,
135
+ sec_id: int,
136
+ security_mutable_names: set[str],
137
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
138
+ ) -> str:
139
+ import re
140
+
141
+ result = cpp.replace("current_bar_.", "bar.")
142
+ for name in sorted(security_mutable_names, key=len, reverse=True):
143
+ info = self._global_mutable_infos.get(name)
144
+ if info is None:
145
+ continue
146
+ safe = self._safe_name(name)
147
+ state = self._security_state_name(sec_id, name)
148
+ if getattr(info, "is_series", False):
149
+ result = re.sub(rf"\b{re.escape(safe)}\b(?=\s*\[)", state, result)
150
+ result = re.sub(rf"\b{re.escape(safe)}\b(?!\s*\[)", f"{state}[0]", result)
151
+ else:
152
+ result = re.sub(rf"\b{re.escape(safe)}\b", state, result)
153
+ if helper_binding_stack:
154
+ for frame in helper_binding_stack:
155
+ for name, bound in frame.items():
156
+ if not isinstance(bound, str):
157
+ continue
158
+ series_name = self._security_series_binding_target(bound)
159
+ if series_name is not None:
160
+ result = re.sub(
161
+ rf"\b{re.escape(name)}\b(?=\s*\[)",
162
+ f'_security_helper_series_["{series_name}"]',
163
+ result,
164
+ )
165
+ result = re.sub(
166
+ rf"\b{re.escape(name)}\b(?!\s*\[)",
167
+ f'_security_helper_series_["{series_name}"][0]',
168
+ result,
169
+ )
170
+ else:
171
+ result = re.sub(rf"\b{re.escape(name)}\b", bound, result)
172
+ return result
173
+
174
+ def _security_lookup_helper_binding(
175
+ self,
176
+ name: str,
177
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
178
+ ):
179
+ if not helper_binding_stack:
180
+ return None
181
+ for frame in reversed(helper_binding_stack):
182
+ if name not in frame:
183
+ continue
184
+ bound = frame[name]
185
+ if isinstance(bound, Identifier) and bound.name == name:
186
+ continue
187
+ return bound
188
+ return None
189
+
190
+ def _literal_int_for_security_index(self, node) -> int | None:
191
+ """Integer index for OHLC[ n ] inside request.security (must be literal)."""
192
+ if isinstance(node, NumberLiteral):
193
+ v = node.value
194
+ if isinstance(v, bool):
195
+ return None
196
+ if float(v) == int(v):
197
+ return int(v)
198
+ return None
199
+ if (
200
+ isinstance(node, UnaryOp)
201
+ and node.op == "-"
202
+ and isinstance(node.operand, NumberLiteral)
203
+ ):
204
+ v = -node.operand.value
205
+ if float(v) == int(v):
206
+ return int(v)
207
+ return None
208
+ return None
209
+
210
+ def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
211
+ """Which OHLC fields need HTF history (subscript index >= 1) for this expression."""
212
+ out: set[str] = set()
213
+
214
+ def walk(n):
215
+ if n is None:
216
+ return
217
+ if isinstance(n, Subscript) and isinstance(n.object, Identifier):
218
+ if n.object.name in SECURITY_OHLC_BAR_FIELDS:
219
+ 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:
223
+ out.add(n.object.name)
224
+ if isinstance(n, (list, tuple)):
225
+ for x in n:
226
+ walk(x)
227
+ return
228
+ for _k, v in getattr(n, "__dict__", {}).items():
229
+ if isinstance(v, ASTNode):
230
+ walk(v)
231
+ elif isinstance(v, (list, tuple)):
232
+ for x in v:
233
+ if isinstance(x, ASTNode):
234
+ walk(x)
235
+
236
+ walk(node)
237
+ return out
238
+
239
+ def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
240
+ return f"_sec{sec_id}_hist_{field}"
241
+
242
+ @staticmethod
243
+ def _security_series_binding(series_name: str) -> str:
244
+ return f"@series:{series_name}"
245
+
246
+ @staticmethod
247
+ def _security_series_binding_target(binding: str) -> str | None:
248
+ if isinstance(binding, str) and binding.startswith("@series:"):
249
+ return binding[len("@series:") :]
250
+ return None
251
+
252
+ def _emit_security_linear_helper_call(
253
+ self,
254
+ sec_id: int,
255
+ plan: dict,
256
+ ta_results: dict,
257
+ security_mutable_names: set[str],
258
+ lines: list[str],
259
+ resolving: set[str] | None = None,
260
+ ) -> str:
261
+ if plan["mode"] != "linear":
262
+ self._codegen_error(
263
+ plan["func_info"].node,
264
+ "Internal security helper emission requested for a non-linear helper plan",
265
+ )
266
+ local_cpp_bindings: dict[str, str] = {}
267
+ runtime_stack = plan["binding_stack"] + (local_cpp_bindings,)
268
+ local_series_names = set(plan.get("local_series_names", ()))
269
+
270
+ def _series_expr(binding_name: str, index_expr: str) -> str:
271
+ return f'_security_helper_series_["{binding_name}"][{index_expr}]'
272
+
273
+ def emit_stmt(stmt: ASTNode, active_bindings: dict[str, str], indent: int) -> None:
274
+ pad = " " * indent
275
+ runtime_stack_local = plan["binding_stack"] + (active_bindings,)
276
+
277
+ if isinstance(stmt, VarDecl):
278
+ if stmt.name in local_series_names:
279
+ binding = active_bindings.get(stmt.name)
280
+ if binding is None:
281
+ binding = self._security_series_binding(
282
+ self._security_next_inline_name(sec_id, plan["func_info"].name, stmt.name)
283
+ )
284
+ series_name = self._security_series_binding_target(binding)
285
+ assert series_name is not None
286
+ expr_cpp = self._build_security_expr(
287
+ sec_id,
288
+ stmt.value,
289
+ None,
290
+ ta_results,
291
+ resolving,
292
+ security_mutable_names,
293
+ runtime_stack_local,
294
+ lines,
295
+ )
296
+ active_bindings[stmt.name] = binding
297
+ lines.append(f'{pad}if (_security_helper_series_["{series_name}"].size() == 0) {{')
298
+ lines.append(f'{pad} _security_helper_series_["{series_name}"].push({expr_cpp});')
299
+ lines.append(f'{pad}}} else if (security_series_slot_is_new({sec_id})) {{')
300
+ lines.append(f'{pad} _security_helper_series_["{series_name}"].push({expr_cpp});')
301
+ lines.append(f'{pad}}} else {{')
302
+ lines.append(f'{pad} _security_helper_series_["{series_name}"].update({expr_cpp});')
303
+ lines.append(f'{pad}}}')
304
+ return
305
+
306
+ local_name = active_bindings.get(stmt.name)
307
+ if local_name is None:
308
+ local_name = self._security_next_inline_name(
309
+ sec_id,
310
+ plan["func_info"].name,
311
+ stmt.name,
312
+ )
313
+ cpp_type = self._type_for_decl(stmt)
314
+ expr_cpp = self._build_security_expr(
315
+ sec_id,
316
+ stmt.value,
317
+ None,
318
+ ta_results,
319
+ resolving,
320
+ security_mutable_names,
321
+ runtime_stack_local,
322
+ lines,
323
+ )
324
+ active_bindings[stmt.name] = local_name
325
+ lines.append(f"{pad}{cpp_type} {local_name} = {expr_cpp};")
326
+ else:
327
+ expr_cpp = self._build_security_expr(
328
+ sec_id,
329
+ stmt.value,
330
+ None,
331
+ ta_results,
332
+ resolving,
333
+ security_mutable_names,
334
+ runtime_stack_local,
335
+ lines,
336
+ )
337
+ lines.append(f"{pad}{local_name} = {expr_cpp};")
338
+ return
339
+
340
+ if isinstance(stmt, Assignment):
341
+ target_name = self._get_target_name(stmt.target)
342
+ if target_name is None:
343
+ self._codegen_error(
344
+ stmt,
345
+ "request.security multi-statement helpers may only assign to local identifier temporaries",
346
+ )
347
+ binding = active_bindings.get(target_name)
348
+ if binding is None:
349
+ self._codegen_error(
350
+ stmt,
351
+ "request.security multi-statement helper assignment target must be declared before use",
352
+ )
353
+ expr_cpp = self._build_security_expr(
354
+ sec_id,
355
+ stmt.value,
356
+ None,
357
+ ta_results,
358
+ resolving,
359
+ security_mutable_names,
360
+ runtime_stack_local,
361
+ lines,
362
+ )
363
+ series_name = self._security_series_binding_target(binding)
364
+ if series_name is not None:
365
+ if stmt.op == ":=":
366
+ lines.append(f'{pad}_security_helper_series_["{series_name}"].update({expr_cpp});')
367
+ else:
368
+ op_char = stmt.op[0]
369
+ lines.append(
370
+ f'{pad}_security_helper_series_["{series_name}"].update('
371
+ f'{_series_expr(series_name, "0")} {op_char} {expr_cpp});'
372
+ )
373
+ return
374
+
375
+ if stmt.op == ":=":
376
+ lines.append(f"{pad}{binding} = {expr_cpp};")
377
+ else:
378
+ lines.append(f"{pad}{binding} {stmt.op} {expr_cpp};")
379
+ return
380
+
381
+ if isinstance(stmt, IfStmt):
382
+ cond_cpp = self._build_security_expr(
383
+ sec_id,
384
+ stmt.condition,
385
+ None,
386
+ ta_results,
387
+ resolving,
388
+ security_mutable_names,
389
+ runtime_stack_local,
390
+ lines,
391
+ )
392
+ lines.append(f"{pad}if ({cond_cpp}) {{")
393
+ body_bindings = dict(active_bindings)
394
+ for child in stmt.body:
395
+ emit_stmt(child, body_bindings, indent + 1)
396
+ if stmt.else_body:
397
+ lines.append(f"{pad}}} else {{")
398
+ else_bindings = dict(active_bindings)
399
+ for child in stmt.else_body:
400
+ emit_stmt(child, else_bindings, indent + 1)
401
+ lines.append(f"{pad}}}")
402
+ return
403
+
404
+ self._codegen_error(
405
+ stmt,
406
+ "request.security multi-statement helpers may only use local declarations, assignments, and if-branches before the final expression",
407
+ )
408
+
409
+ for stmt in plan["body"]:
410
+ emit_stmt(stmt, local_cpp_bindings, indent=2)
411
+
412
+ return self._build_security_expr(
413
+ sec_id,
414
+ plan["expr"],
415
+ None,
416
+ ta_results,
417
+ resolving,
418
+ security_mutable_names,
419
+ runtime_stack,
420
+ lines,
421
+ )
422
+
423
+ def _security_binding_stack_signature(
424
+ self,
425
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
426
+ ) -> tuple:
427
+ if not helper_binding_stack:
428
+ return ()
429
+ def _sig_value(value):
430
+ if isinstance(value, str):
431
+ return value
432
+ return id(value)
433
+
434
+ sig_frames = []
435
+ for idx, frame in enumerate(helper_binding_stack):
436
+ if idx == 0:
437
+ sig_frames.append(
438
+ tuple((name, _sig_value(node)) for name, node in sorted(frame.items()))
439
+ )
440
+ else:
441
+ # Helper-local bindings only need to preserve which locals have been
442
+ # materialized at this point; using raw runtime names here causes
443
+ # the declaration/lookup paths to disagree on the same TA variant.
444
+ sig_frames.append(tuple(sorted(frame.keys())))
445
+ return tuple(sig_frames)
446
+
447
+ def _security_bind_helper_args(
448
+ self,
449
+ node: FuncCall,
450
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
451
+ ) -> tuple[FuncInfo, tuple[dict[str, ASTNode], ...]]:
452
+ if not isinstance(node.callee, Identifier):
453
+ self._codegen_error(
454
+ node,
455
+ "request.security helper calls must target named user-defined functions",
456
+ )
457
+
458
+ func_name = node.callee.name
459
+ fi = self._func_info_map.get(func_name)
460
+ if fi is None or fi.node is None:
461
+ self._codegen_error(
462
+ node,
463
+ f"request.security helper function '{func_name}' is not defined",
464
+ )
465
+
466
+ params = list(fi.node.params)
467
+ unknown_kwargs = set(node.kwargs) - set(params)
468
+ if unknown_kwargs:
469
+ unknown_list = ", ".join(sorted(unknown_kwargs))
470
+ self._codegen_error(
471
+ node,
472
+ f"request.security helper call has unknown parameter(s): {unknown_list}",
473
+ )
474
+
475
+ bound_args = list(node.args)
476
+ for idx, param_name in enumerate(params):
477
+ if param_name in node.kwargs:
478
+ while len(bound_args) <= idx:
479
+ bound_args.append(None)
480
+ if bound_args[idx] is None:
481
+ bound_args[idx] = node.kwargs[param_name]
482
+
483
+ if len(bound_args) != len(params) or any(arg is None for arg in bound_args):
484
+ self._codegen_error(
485
+ node,
486
+ "request.security helper calls must bind every parameter explicitly",
487
+ )
488
+
489
+ new_frame = {param_name: bound_args[idx] for idx, param_name in enumerate(params)}
490
+ base_stack = helper_binding_stack or ()
491
+ return fi, base_stack + (new_frame,)
492
+
493
+ def _security_helper_call_plan(
494
+ self,
495
+ node: FuncCall,
496
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
497
+ ) -> dict:
498
+ fi, bound_stack = self._security_bind_helper_args(node, helper_binding_stack)
499
+ assert fi.node is not None
500
+ body = fi.node.body
501
+ params = list(fi.node.params)
502
+
503
+ if len(body) == 1 and isinstance(body[0], ExprStmt):
504
+ return {
505
+ "mode": "expr",
506
+ "func_info": fi,
507
+ "binding_stack": bound_stack,
508
+ "expr": body[0].expr,
509
+ "body": [],
510
+ }
511
+
512
+ if not body:
513
+ self._codegen_error(
514
+ node,
515
+ "request.security multi-statement helpers must end with a final expression result",
516
+ )
517
+
518
+ stmt_body = list(body[:-1])
519
+ final_stmt = body[-1]
520
+ if isinstance(final_stmt, ExprStmt):
521
+ final_expr = final_stmt.expr
522
+ elif isinstance(final_stmt, Assignment):
523
+ target_name = self._get_target_name(final_stmt.target)
524
+ if target_name is None:
525
+ self._codegen_error(
526
+ node,
527
+ "request.security multi-statement helpers must end with a final expression result",
528
+ )
529
+ stmt_body.append(final_stmt)
530
+ final_expr = Identifier(name=target_name)
531
+ elif isinstance(final_stmt, VarDecl):
532
+ stmt_body.append(final_stmt)
533
+ final_expr = Identifier(name=final_stmt.name)
534
+ else:
535
+ self._codegen_error(
536
+ node,
537
+ "request.security multi-statement helpers must end with a final expression result",
538
+ )
539
+
540
+ unsupported_control_flow = (
541
+ ForStmt,
542
+ ForInStmt,
543
+ WhileStmt,
544
+ SwitchStmt,
545
+ BreakStmt,
546
+ ContinueStmt,
547
+ TupleAssign,
548
+ )
549
+ for stmt in stmt_body:
550
+ if isinstance(stmt, unsupported_control_flow):
551
+ self._codegen_error(
552
+ node,
553
+ "request.security does not support multi-statement helpers with control flow",
554
+ hint="Inline a straight-line helper body or hoist the control-flow helper outside request.security().",
555
+ )
556
+ if not isinstance(stmt, (VarDecl, Assignment, IfStmt)):
557
+ self._codegen_error(
558
+ node,
559
+ "request.security multi-statement helpers may only use local declarations, assignments, and if-branches before the final expression",
560
+ )
561
+ if isinstance(stmt, VarDecl):
562
+ if stmt.is_var or stmt.is_varip:
563
+ self._codegen_error(
564
+ node,
565
+ "request.security does not support multi-statement helpers with helper-local var state",
566
+ hint="Rewrite helper-local state as plain temporaries or hoist it outside request.security().",
567
+ )
568
+ if isinstance(stmt, Assignment):
569
+ target_name = self._get_target_name(stmt.target)
570
+ if target_name is None:
571
+ self._codegen_error(
572
+ stmt,
573
+ "request.security multi-statement helpers may only assign to local identifier temporaries",
574
+ )
575
+
576
+ local_series_names = sorted(set(self.ctx.func_series_vars.get(fi.name, set())) - set(params))
577
+ return {
578
+ "mode": "linear",
579
+ "func_info": fi,
580
+ "binding_stack": bound_stack,
581
+ "expr": final_expr,
582
+ "body": stmt_body,
583
+ "local_series_names": local_series_names,
584
+ }
585
+
586
+ def _security_next_inline_name(self, sec_id: int, func_name: str, base_name: str) -> str:
587
+ self._security_inline_counter += 1
588
+ return (
589
+ f"_sec{sec_id}_{self._safe_name(func_name)}_"
590
+ f"{self._security_inline_counter}_{self._safe_name(base_name)}"
591
+ )
592
+
593
+ def _expr_depends_on_security_mutables(
594
+ self,
595
+ expr_node,
596
+ security_mutable_names: set[str],
597
+ resolving: set[str] | None = None,
598
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
599
+ ) -> bool:
600
+ if expr_node is None or not security_mutable_names:
601
+ return False
602
+ if resolving is None:
603
+ resolving = set()
604
+
605
+ if isinstance(expr_node, Identifier):
606
+ bound = self._security_lookup_helper_binding(expr_node.name, helper_binding_stack)
607
+ if bound is not None:
608
+ return self._expr_depends_on_security_mutables(
609
+ bound,
610
+ security_mutable_names,
611
+ resolving,
612
+ helper_binding_stack,
613
+ )
614
+ if expr_node.name in security_mutable_names:
615
+ return True
616
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
617
+ if expr_node.name in global_expr_map and expr_node.name not in resolving:
618
+ resolving.add(expr_node.name)
619
+ depends = self._expr_depends_on_security_mutables(
620
+ global_expr_map[expr_node.name],
621
+ security_mutable_names,
622
+ resolving,
623
+ helper_binding_stack,
624
+ )
625
+ resolving.remove(expr_node.name)
626
+ return depends
627
+ return False
628
+
629
+ if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
630
+ func_name = expr_node.callee.name
631
+ if func_name in self._func_names:
632
+ call_key = f"func:{func_name}"
633
+ if call_key in resolving:
634
+ return False
635
+ resolving.add(call_key)
636
+ plan = self._security_helper_call_plan(
637
+ expr_node,
638
+ helper_binding_stack,
639
+ )
640
+ if plan["mode"] == "expr":
641
+ depends = self._expr_depends_on_security_mutables(
642
+ plan["expr"],
643
+ security_mutable_names,
644
+ resolving,
645
+ plan["binding_stack"],
646
+ )
647
+ else:
648
+ local_ast_bindings: dict[str, ASTNode] = {}
649
+ linear_stack = plan["binding_stack"] + (local_ast_bindings,)
650
+ depends = False
651
+ for stmt in plan["body"][:-1]:
652
+ value = stmt.value if isinstance(stmt, (VarDecl, Assignment)) else None
653
+ if value is not None and self._expr_depends_on_security_mutables(
654
+ value,
655
+ security_mutable_names,
656
+ resolving,
657
+ linear_stack,
658
+ ):
659
+ depends = True
660
+ break
661
+ target_name = (
662
+ stmt.name if isinstance(stmt, VarDecl) else self._get_target_name(stmt.target)
663
+ )
664
+ if target_name is not None and value is not None:
665
+ local_ast_bindings[target_name] = value
666
+ if not depends:
667
+ depends = self._expr_depends_on_security_mutables(
668
+ plan["expr"],
669
+ security_mutable_names,
670
+ resolving,
671
+ linear_stack,
672
+ )
673
+ resolving.remove(call_key)
674
+ return depends
675
+
676
+ def walk(value) -> bool:
677
+ if value is None:
678
+ return False
679
+ if hasattr(value, "__dict__"):
680
+ return self._expr_depends_on_security_mutables(
681
+ value,
682
+ security_mutable_names,
683
+ resolving,
684
+ helper_binding_stack,
685
+ )
686
+ if isinstance(value, (list, tuple)):
687
+ return any(walk(item) for item in value)
688
+ if isinstance(value, dict):
689
+ return any(walk(item) for item in value.values())
690
+ return False
691
+
692
+ return any(walk(child) for child in vars(expr_node).values())
693
+
694
+ def _security_ta_depends_on_mutables(
695
+ self,
696
+ site: TACallSite,
697
+ security_mutable_names: set[str],
698
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
699
+ ) -> bool:
700
+ return any(
701
+ self._expr_depends_on_security_mutables(
702
+ arg,
703
+ security_mutable_names,
704
+ helper_binding_stack=helper_binding_stack,
705
+ )
706
+ for arg in site.compute_args
707
+ )
708
+
709
+ def _security_ta_ctor_arg_nodes(self, site: TACallSite) -> list:
710
+ node = site.node
711
+ if not isinstance(node, FuncCall):
712
+ return []
713
+
714
+ func_name = self._ta_name_from_site(site)
715
+ all_args = self._merge_ta_call_args(func_name, node)
716
+ effective_multi_ctor = TA_MULTI_CTOR.copy()
717
+ if func_name in ("pivothigh", "pivotlow") and len(all_args) == 3:
718
+ effective_multi_ctor[func_name] = [1, 2]
719
+
720
+ ctor_indices: list[int] = []
721
+ if func_name in TA_NO_CTOR:
722
+ ctor_indices = []
723
+ elif func_name in effective_multi_ctor:
724
+ ctor_indices = list(effective_multi_ctor[func_name])
725
+ elif func_name in TA_PERIOD_ARG:
726
+ ctor_indices = [TA_PERIOD_ARG[func_name]]
727
+
728
+ return [
729
+ all_args[idx]
730
+ for idx in ctor_indices
731
+ if idx < len(all_args) and all_args[idx] is not None
732
+ ]
733
+
734
+ def _security_ta_ctor_depends_on_mutables(
735
+ self,
736
+ site: TACallSite,
737
+ security_mutable_names: set[str],
738
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
739
+ ) -> bool:
740
+ return any(
741
+ self._expr_depends_on_security_mutables(
742
+ arg,
743
+ security_mutable_names,
744
+ helper_binding_stack=helper_binding_stack,
745
+ )
746
+ for arg in self._security_ta_ctor_arg_nodes(site)
747
+ )
748
+
749
+ def _collect_security_ta_binding_stacks(
750
+ self,
751
+ expr_node,
752
+ resolving: set[str] | None = None,
753
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
754
+ collected: dict[int, tuple[dict[str, ASTNode], ...]] | None = None,
755
+ inline_ta_indices: set[int] | None = None,
756
+ inline_helper: bool = False,
757
+ ) -> dict[int, tuple[dict[str, ASTNode], ...]]:
758
+ if collected is None:
759
+ collected = {}
760
+ if expr_node is None:
761
+ return collected
762
+ if resolving is None:
763
+ resolving = set()
764
+
765
+ if isinstance(expr_node, Identifier):
766
+ bound = self._security_lookup_helper_binding(expr_node.name, helper_binding_stack)
767
+ if bound is not None:
768
+ if isinstance(bound, str):
769
+ return collected
770
+ self._collect_security_ta_binding_stacks(
771
+ bound,
772
+ resolving,
773
+ helper_binding_stack,
774
+ collected,
775
+ inline_ta_indices,
776
+ inline_helper,
777
+ )
778
+ return collected
779
+
780
+ mutable_info = self._global_mutable_infos.get(expr_node.name)
781
+ if mutable_info is not None and expr_node.name not in resolving:
782
+ resolving.add(expr_node.name)
783
+ for stmt in getattr(mutable_info, "source_stmts", []) or []:
784
+ self._collect_security_ta_binding_stacks(
785
+ stmt,
786
+ resolving,
787
+ helper_binding_stack,
788
+ collected,
789
+ inline_ta_indices,
790
+ inline_helper,
791
+ )
792
+ resolving.remove(expr_node.name)
793
+ return collected
794
+
795
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
796
+ if expr_node.name in global_expr_map and expr_node.name not in resolving:
797
+ resolving.add(expr_node.name)
798
+ self._collect_security_ta_binding_stacks(
799
+ global_expr_map[expr_node.name],
800
+ resolving,
801
+ helper_binding_stack,
802
+ collected,
803
+ inline_ta_indices,
804
+ inline_helper,
805
+ )
806
+ resolving.remove(expr_node.name)
807
+ return collected
808
+
809
+ if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
810
+ func_name = expr_node.callee.name
811
+ if func_name in self._func_names:
812
+ call_key = f"func:{func_name}"
813
+ if call_key in resolving:
814
+ return collected
815
+ resolving.add(call_key)
816
+ plan = self._security_helper_call_plan(
817
+ expr_node,
818
+ helper_binding_stack,
819
+ )
820
+ if plan["mode"] == "expr":
821
+ self._collect_security_ta_binding_stacks(
822
+ plan["expr"],
823
+ resolving,
824
+ plan["binding_stack"],
825
+ collected,
826
+ inline_ta_indices,
827
+ inline_helper,
828
+ )
829
+ else:
830
+ local_series_names = set(plan.get("local_series_names", ()))
831
+ local_ast_bindings: dict[str, object] = {}
832
+ linear_stack = plan["binding_stack"] + (local_ast_bindings,)
833
+
834
+ def collect_stmt(stmt, active_bindings: dict[str, object]) -> None:
835
+ local_stack = plan["binding_stack"] + (active_bindings,)
836
+
837
+ if isinstance(stmt, VarDecl):
838
+ value = stmt.value
839
+ if value is not None:
840
+ self._collect_security_ta_binding_stacks(
841
+ value,
842
+ resolving,
843
+ local_stack,
844
+ collected,
845
+ inline_ta_indices,
846
+ True,
847
+ )
848
+ if stmt.name in local_series_names:
849
+ active_bindings[stmt.name] = self._security_series_binding(
850
+ f"{plan['func_info'].name}:{stmt.name}"
851
+ )
852
+ elif value is not None:
853
+ active_bindings[stmt.name] = value
854
+ return
855
+
856
+ if isinstance(stmt, Assignment):
857
+ value = stmt.value
858
+ target_name = self._get_target_name(stmt.target)
859
+ if value is not None:
860
+ self._collect_security_ta_binding_stacks(
861
+ value,
862
+ resolving,
863
+ local_stack,
864
+ collected,
865
+ inline_ta_indices,
866
+ True,
867
+ )
868
+ if target_name in local_series_names:
869
+ active_bindings[target_name] = self._security_series_binding(
870
+ f"{plan['func_info'].name}:{target_name}"
871
+ )
872
+ elif target_name is not None and value is not None:
873
+ active_bindings[target_name] = value
874
+ return
875
+
876
+ if isinstance(stmt, IfStmt):
877
+ self._collect_security_ta_binding_stacks(
878
+ stmt.condition,
879
+ resolving,
880
+ local_stack,
881
+ collected,
882
+ inline_ta_indices,
883
+ True,
884
+ )
885
+ body_bindings = dict(active_bindings)
886
+ for child in stmt.body:
887
+ collect_stmt(child, body_bindings)
888
+ else_bindings = dict(active_bindings)
889
+ for child in stmt.else_body:
890
+ collect_stmt(child, else_bindings)
891
+ return
892
+
893
+ for stmt in plan["body"]:
894
+ collect_stmt(stmt, local_ast_bindings)
895
+
896
+ self._collect_security_ta_binding_stacks(
897
+ plan["expr"],
898
+ resolving,
899
+ linear_stack,
900
+ collected,
901
+ inline_ta_indices,
902
+ True,
903
+ )
904
+ resolving.remove(call_key)
905
+ return collected
906
+
907
+ site = self._get_ta_site(expr_node)
908
+ if site is not None:
909
+ idx = self._ta_index_by_site_id.get(id(site))
910
+ if idx is not None:
911
+ current_sig = self._security_binding_stack_signature(helper_binding_stack)
912
+ existing = collected.setdefault(idx, {})
913
+ existing[current_sig] = helper_binding_stack or ()
914
+ if inline_helper and inline_ta_indices is not None:
915
+ inline_ta_indices.add(idx)
916
+
917
+ def walk(value) -> None:
918
+ if value is None:
919
+ return
920
+ if hasattr(value, "__dict__"):
921
+ self._collect_security_ta_binding_stacks(
922
+ value,
923
+ resolving,
924
+ helper_binding_stack,
925
+ collected,
926
+ inline_ta_indices,
927
+ inline_helper,
928
+ )
929
+ return
930
+ if isinstance(value, (list, tuple)):
931
+ for item in value:
932
+ walk(item)
933
+ return
934
+ if isinstance(value, dict):
935
+ for item in value.values():
936
+ walk(item)
937
+
938
+ for child in vars(expr_node).values():
939
+ walk(child)
940
+ return collected
941
+
942
+ def _emit_security_rebind_var_decl(
943
+ self,
944
+ sec_id: int,
945
+ node: VarDecl,
946
+ lines: list[str],
947
+ relevant_names: set[str],
948
+ ta_results: dict[int, str],
949
+ indent: int,
950
+ emitted_lines: list[str] | None = None,
951
+ ) -> None:
952
+ if node.name not in relevant_names:
953
+ return
954
+ info = self._global_mutable_infos.get(node.name)
955
+ if info is None:
956
+ return
957
+
958
+ pad = " " * indent
959
+ state_name = self._security_state_name(sec_id, node.name)
960
+ init_flag = self._security_init_flag_name(sec_id, node.name)
961
+ expr_cpp = self._build_security_expr(
962
+ sec_id,
963
+ node.value,
964
+ None,
965
+ ta_results,
966
+ security_mutable_names=relevant_names,
967
+ emitted_lines=emitted_lines,
968
+ )
969
+
970
+ if getattr(info, "is_var", False):
971
+ if getattr(info, "is_series", False):
972
+ lines.append(f"{pad}if (!{init_flag}) {{")
973
+ lines.append(f"{pad} {state_name}.push({expr_cpp});")
974
+ lines.append(f"{pad} {init_flag} = true;")
975
+ lines.append(f"{pad}}} else if (security_series_slot_is_new({sec_id})) {{")
976
+ lines.append(f"{pad} {state_name}.push({state_name}[0]);")
977
+ lines.append(f"{pad}}}")
978
+ else:
979
+ lines.append(f"{pad}if (!{init_flag}) {{")
980
+ lines.append(f"{pad} {state_name} = {expr_cpp};")
981
+ lines.append(f"{pad} {init_flag} = true;")
982
+ lines.append(f"{pad}}}")
983
+ return
984
+
985
+ if getattr(info, "is_series", False):
986
+ lines.append(f"{pad}if (security_series_slot_is_new({sec_id})) {{")
987
+ lines.append(f"{pad} {state_name}.push({expr_cpp});")
988
+ lines.append(f"{pad}}} else {{")
989
+ lines.append(f"{pad} {state_name}.update({expr_cpp});")
990
+ lines.append(f"{pad}}}")
991
+ else:
992
+ lines.append(f"{pad}{state_name} = {expr_cpp};")
993
+
994
+ def _emit_security_rebind_assignment(
995
+ self,
996
+ sec_id: int,
997
+ node: Assignment,
998
+ lines: list[str],
999
+ relevant_names: set[str],
1000
+ ta_results: dict[int, str],
1001
+ indent: int,
1002
+ emitted_lines: list[str] | None = None,
1003
+ ) -> None:
1004
+ target_name = self._get_target_name(node.target)
1005
+ if target_name not in relevant_names:
1006
+ return
1007
+ info = self._global_mutable_infos.get(target_name)
1008
+ if info is None:
1009
+ return
1010
+
1011
+ pad = " " * indent
1012
+ state_name = self._security_state_name(sec_id, target_name)
1013
+ value_cpp = self._build_security_expr(
1014
+ sec_id,
1015
+ node.value,
1016
+ None,
1017
+ ta_results,
1018
+ security_mutable_names=relevant_names,
1019
+ emitted_lines=emitted_lines,
1020
+ )
1021
+
1022
+ if getattr(info, "is_series", False):
1023
+ if node.op == ":=":
1024
+ lines.append(f"{pad}{state_name}.update({value_cpp});")
1025
+ else:
1026
+ op_char = node.op[0]
1027
+ lines.append(f"{pad}{state_name}.update({state_name}[0] {op_char} {value_cpp});")
1028
+ return
1029
+
1030
+ if node.op == ":=":
1031
+ lines.append(f"{pad}{state_name} = {value_cpp};")
1032
+ else:
1033
+ lines.append(f"{pad}{state_name} {node.op} {value_cpp};")
1034
+
1035
+ def _emit_security_rebind_stmt(
1036
+ self,
1037
+ sec_id: int,
1038
+ node: ASTNode,
1039
+ lines: list[str],
1040
+ relevant_names: set[str],
1041
+ ta_results: dict[int, str],
1042
+ indent: int,
1043
+ emitted_lines: list[str] | None = None,
1044
+ ) -> None:
1045
+ if isinstance(node, VarDecl):
1046
+ self._emit_security_rebind_var_decl(
1047
+ sec_id, node, lines, relevant_names, ta_results, indent, emitted_lines
1048
+ )
1049
+ return
1050
+ if isinstance(node, Assignment):
1051
+ self._emit_security_rebind_assignment(
1052
+ sec_id, node, lines, relevant_names, ta_results, indent, emitted_lines
1053
+ )
1054
+ return
1055
+ if isinstance(node, IfStmt):
1056
+ body_lines: list[str] = []
1057
+ else_lines: list[str] = []
1058
+ for stmt in node.body:
1059
+ self._emit_security_rebind_stmt(
1060
+ sec_id, stmt, body_lines, relevant_names, ta_results, indent + 1, emitted_lines
1061
+ )
1062
+ for stmt in node.else_body:
1063
+ self._emit_security_rebind_stmt(
1064
+ sec_id, stmt, else_lines, relevant_names, ta_results, indent + 1, emitted_lines
1065
+ )
1066
+ if not body_lines and not else_lines:
1067
+ return
1068
+ pad = " " * indent
1069
+ cond_cpp = self._build_security_expr(
1070
+ sec_id,
1071
+ node.condition,
1072
+ None,
1073
+ ta_results,
1074
+ security_mutable_names=relevant_names,
1075
+ emitted_lines=emitted_lines,
1076
+ )
1077
+ lines.append(f"{pad}if ({cond_cpp}) {{")
1078
+ lines.extend(body_lines)
1079
+ if else_lines:
1080
+ lines.append(f"{pad}}} else {{")
1081
+ lines.extend(else_lines)
1082
+ lines.append(f"{pad}}}")
1083
+ else:
1084
+ lines.append(f"{pad}}}")
1085
+ return
1086
+ if isinstance(node, SwitchStmt):
1087
+ self._codegen_error(
1088
+ node,
1089
+ "request.security mutable global rebinding does not support top-level switch",
1090
+ hint="Rewrite the switch as if/else assignments before passing the value to request.security().",
1091
+ )
1092
+ if isinstance(node, (ForStmt, ForInStmt, WhileStmt)):
1093
+ self._codegen_error(
1094
+ node,
1095
+ "request.security mutable global rebinding does not support top-level loops",
1096
+ hint="Move loop-driven mutable state out of request.security() expressions or rewrite it as direct assignments.",
1097
+ )
1098
+
1099
+ def _emit_security_rebinds(
1100
+ self,
1101
+ sec_id: int,
1102
+ info: dict,
1103
+ lines: list[str],
1104
+ ta_results: dict[int, str],
1105
+ indent: int = 2,
1106
+ emitted_lines: list[str] | None = None,
1107
+ ) -> None:
1108
+ mutable_globals = info.get("mutable_globals") or []
1109
+ if not mutable_globals:
1110
+ return
1111
+ relevant_names = set(mutable_globals)
1112
+ for stmt in self._security_relevant_top_level_stmts(mutable_globals):
1113
+ self._emit_security_rebind_stmt(
1114
+ sec_id, stmt, lines, relevant_names, ta_results, indent, emitted_lines
1115
+ )
1116
+
1117
+ def _collect_security_ta_indices(self, expr_node, resolving: set[str] | None = None) -> set[int]:
1118
+ """Collect TA call-site indices used by a security expression.
1119
+
1120
+ Includes TA calls reachable through global identifier bindings.
1121
+ """
1122
+ if expr_node is None:
1123
+ return set()
1124
+ if resolving is None:
1125
+ resolving = set()
1126
+
1127
+ out: set[int] = set()
1128
+
1129
+ if isinstance(expr_node, Identifier):
1130
+ mutable_info = self._global_mutable_infos.get(expr_node.name)
1131
+ if mutable_info is not None and expr_node.name not in resolving:
1132
+ resolving.add(expr_node.name)
1133
+ for stmt in getattr(mutable_info, "source_stmts", []) or []:
1134
+ out |= self._collect_security_ta_indices(stmt, resolving)
1135
+ resolving.remove(expr_node.name)
1136
+ return out
1137
+
1138
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
1139
+ if expr_node.name in global_expr_map and expr_node.name not in resolving:
1140
+ resolving.add(expr_node.name)
1141
+ out |= self._collect_security_ta_indices(global_expr_map[expr_node.name], resolving)
1142
+ resolving.remove(expr_node.name)
1143
+ return out
1144
+
1145
+ if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
1146
+ func_name = expr_node.callee.name
1147
+ if func_name in self._func_names:
1148
+ return set(
1149
+ self._collect_security_ta_binding_stacks(
1150
+ expr_node,
1151
+ resolving,
1152
+ ).keys()
1153
+ )
1154
+
1155
+ out |= set(
1156
+ self._collect_security_ta_binding_stacks(
1157
+ expr_node,
1158
+ resolving,
1159
+ ).keys()
1160
+ )
1161
+ return out
1162
+
1163
+ def _emit_security_evaluators(self, lines: list[str]) -> None:
1164
+ """Emit _eval_security_N() methods and evaluate_security() dispatch."""
1165
+ if not self._security_calls:
1166
+ return
1167
+
1168
+ for item in self._security_calls:
1169
+ sec_id = item["sec_id"]
1170
+ expr_node = item["expr_node"]
1171
+ info = self._security_eval_info[sec_id]
1172
+ ta_indices = info.get("ta_indices") or []
1173
+ security_mutable_names = set(info.get("mutable_globals", []))
1174
+ inline_helper_ta_indices = set(info.get("inline_helper_ta_indices", []))
1175
+
1176
+ lines.append(f" void _eval_security_{sec_id}(const Bar& bar, bool is_complete) {{")
1177
+
1178
+ ta_results = {}
1179
+ pre_rebind_ta_indices: list[int] = []
1180
+ post_rebind_ta_indices: list[int] = []
1181
+ for idx in ta_indices:
1182
+ if idx in inline_helper_ta_indices:
1183
+ continue
1184
+ site = self.ctx.ta_call_sites[idx]
1185
+ variants = (info.get("ta_variants") or {}).get(idx, [])
1186
+ depends_on_mutables = False
1187
+ for variant in variants:
1188
+ helper_binding_stack = variant.get("binding_stack", ())
1189
+ if self._security_ta_ctor_depends_on_mutables(
1190
+ site,
1191
+ security_mutable_names,
1192
+ helper_binding_stack,
1193
+ ):
1194
+ self._codegen_error(
1195
+ site.node or expr_node,
1196
+ "request.security does not support TA constructor args that depend on rebound mutable globals",
1197
+ hint="Keep TA constructor arguments immutable/simple inside request.security(), or hoist the TA call outside the security expression.",
1198
+ )
1199
+ if self._security_ta_depends_on_mutables(
1200
+ site,
1201
+ security_mutable_names,
1202
+ helper_binding_stack,
1203
+ ):
1204
+ depends_on_mutables = True
1205
+ if depends_on_mutables:
1206
+ post_rebind_ta_indices.append(idx)
1207
+ else:
1208
+ pre_rebind_ta_indices.append(idx)
1209
+
1210
+ def emit_security_ta(indices: list[int]) -> None:
1211
+ for idx in indices:
1212
+ site = self.ctx.ta_call_sites[idx]
1213
+ variants = (info.get("ta_variants") or {}).get(idx, [])
1214
+ for variant in variants:
1215
+ helper_binding_stack = variant.get("binding_stack", ())
1216
+ compute_args = self._security_ta_compute_args_for_site(
1217
+ sec_id,
1218
+ site,
1219
+ ta_results,
1220
+ security_mutable_names,
1221
+ helper_binding_stack,
1222
+ emitted_lines=lines,
1223
+ )
1224
+ var_name = variant["result_name"]
1225
+ sec_name = variant["member_name"]
1226
+ lines.append(f" auto {var_name} = is_complete "
1227
+ f"? {sec_name}.compute({compute_args}) "
1228
+ f": {sec_name}.recompute({compute_args});")
1229
+ ta_results[(idx, variant["signature"])] = var_name
1230
+
1231
+ emit_security_ta(pre_rebind_ta_indices)
1232
+
1233
+ self._emit_security_rebinds(sec_id, info, lines, ta_results, indent=2, emitted_lines=lines)
1234
+ emit_security_ta(post_rebind_ta_indices)
1235
+ expr_cpp = self._build_security_expr(
1236
+ sec_id,
1237
+ expr_node,
1238
+ None,
1239
+ ta_results,
1240
+ security_mutable_names=security_mutable_names,
1241
+ emitted_lines=lines,
1242
+ )
1243
+ if item.get("is_lower_tf_array"):
1244
+ # ``request.security_lower_tf`` accumulates one element per
1245
+ # synthesised sub-bar of the current chart bar. The runtime's
1246
+ # ``feed_security_eval_state`` resets ``lower_tf_sub_bar_index``
1247
+ # to 0 at the start of every chart bar's synthesis loop, so
1248
+ # we clear the vector on index 0 and push for every sub-bar
1249
+ # (including index 0).
1250
+ lines.append(
1251
+ f" if (security_lower_tf_sub_bar_index({sec_id}) == 0)"
1252
+ f" _req_sec_lower_tf_{sec_id}.clear();"
1253
+ )
1254
+ lines.append(
1255
+ f" _req_sec_lower_tf_{sec_id}.push_back({expr_cpp});"
1256
+ )
1257
+ else:
1258
+ 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
+ )
1263
+ lines.append(" }")
1264
+ lines.append("")
1265
+
1266
+ # Dispatch method. Security evaluators fire BEFORE on_bar, so we also
1267
+ # gate a TA reset here: whichever path fires first (evaluate_security
1268
+ # on the bar the HTF aggregator first completes, or on_bar on bar 0)
1269
+ # will run the reset and set _ta_initialized_. This makes sure security
1270
+ # TA objects use runtime-resolved ctor args on their very first compute.
1271
+ lines.append(" void evaluate_security(int sec_id, const Bar& bar, bool is_complete) override {")
1272
+ self._emit_ta_runtime_reset(lines, indent=2)
1273
+ lines.append(" switch (sec_id) {")
1274
+ for item in self._security_calls:
1275
+ sec_id = item["sec_id"]
1276
+ lines.append(f" case {sec_id}: _eval_security_{sec_id}(bar, is_complete); break;")
1277
+ lines.append(" }")
1278
+ lines.append(" }")
1279
+
1280
+ lines.append(" void clear_security(int sec_id) override {")
1281
+ lines.append(" switch (sec_id) {")
1282
+ for item in self._security_calls:
1283
+ sec_id = item["sec_id"]
1284
+ expr_node = item["expr_node"]
1285
+ returns_tuple = item.get("returns_tuple", False)
1286
+ tuple_size = item.get("tuple_size", 0)
1287
+ if item.get("is_lower_tf_array"):
1288
+ # The accumulator is reset on each sub-bar 0 inside the
1289
+ # eval method itself, so ``clear_security`` only needs to
1290
+ # forget the previous chart bar's contents (e.g. when
1291
+ # gaps mode flushes between completions). Clearing the
1292
+ # vector is the right fallback.
1293
+ lines.append(f" case {sec_id}:")
1294
+ lines.append(f" _req_sec_lower_tf_{sec_id}.clear();")
1295
+ for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1296
+ lines.append(
1297
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1298
+ )
1299
+ lines.append(" break;")
1300
+ continue
1301
+ if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
1302
+ lines.append(f" case {sec_id}:")
1303
+ for i, el in enumerate(expr_node.elements):
1304
+ ctype = self._infer_cpp_type_for_security_elem(el)
1305
+ if ctype == "double":
1306
+ lines.append(f" _req_sec_{sec_id}_{i} = na<double>();")
1307
+ elif ctype == "bool":
1308
+ lines.append(f" _req_sec_{sec_id}_{i} = false;")
1309
+ elif ctype == "int":
1310
+ lines.append(f" _req_sec_{sec_id}_{i} = 0;")
1311
+ elif ctype == "std::string":
1312
+ lines.append(f' _req_sec_{sec_id}_{i} = std::string("");')
1313
+ elif ctype == "std::vector<double>":
1314
+ lines.append(f" _req_sec_{sec_id}_{i}.clear();")
1315
+ else:
1316
+ lines.append(f" _req_sec_{sec_id}_{i} = 0;")
1317
+ for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1318
+ lines.append(
1319
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1320
+ )
1321
+ lines.append(" break;")
1322
+ else:
1323
+ hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
1324
+ if hist:
1325
+ lines.append(f" case {sec_id}:")
1326
+ lines.append(f" _req_sec_{sec_id} = na<double>();")
1327
+ for field in sorted(hist):
1328
+ lines.append(
1329
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1330
+ )
1331
+ lines.append(" break;")
1332
+ else:
1333
+ lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
1334
+ lines.append(" }")
1335
+ lines.append(" }")
1336
+
1337
+ def _build_security_expr(
1338
+ self,
1339
+ sec_id: int,
1340
+ expr_node,
1341
+ ta_range,
1342
+ ta_results: dict,
1343
+ resolving: set[str] | None = None,
1344
+ security_mutable_names: set[str] | None = None,
1345
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
1346
+ emitted_lines: list[str] | None = None,
1347
+ ) -> str:
1348
+ """Build C++ expression for a security evaluator."""
1349
+ if expr_node is None:
1350
+ return "na<double>()"
1351
+
1352
+ if resolving is None:
1353
+ resolving = set()
1354
+ if security_mutable_names is None:
1355
+ security_mutable_names = set()
1356
+ if helper_binding_stack is None:
1357
+ helper_binding_stack = ()
1358
+
1359
+ if isinstance(expr_node, Identifier):
1360
+ bound = self._security_lookup_helper_binding(expr_node.name, helper_binding_stack)
1361
+ if bound is not None:
1362
+ if isinstance(bound, str):
1363
+ series_name = self._security_series_binding_target(bound)
1364
+ if series_name is not None:
1365
+ return f'_security_helper_series_["{series_name}"][0]'
1366
+ return bound
1367
+ return self._build_security_expr(
1368
+ sec_id,
1369
+ bound,
1370
+ ta_range,
1371
+ ta_results,
1372
+ resolving,
1373
+ security_mutable_names,
1374
+ helper_binding_stack,
1375
+ emitted_lines,
1376
+ )
1377
+ bar_fields = {
1378
+ "close": "bar.close", "high": "bar.high",
1379
+ "low": "bar.low", "open": "bar.open",
1380
+ "volume": "bar.volume",
1381
+ "hl2": "((bar.high + bar.low) / 2.0)",
1382
+ "hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
1383
+ "ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
1384
+ }
1385
+ if expr_node.name in bar_fields:
1386
+ return bar_fields[expr_node.name]
1387
+
1388
+ if expr_node.name in security_mutable_names:
1389
+ info = self._global_mutable_infos.get(expr_node.name)
1390
+ state_name = self._security_state_name(sec_id, expr_node.name)
1391
+ if info is not None and getattr(info, "is_series", False):
1392
+ return f"{state_name}[0]"
1393
+ return state_name
1394
+
1395
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
1396
+ if expr_node.name in global_expr_map and expr_node.name not in resolving:
1397
+ resolving.add(expr_node.name)
1398
+ resolved = self._build_security_expr(
1399
+ sec_id,
1400
+ global_expr_map[expr_node.name],
1401
+ ta_range,
1402
+ ta_results,
1403
+ resolving,
1404
+ security_mutable_names,
1405
+ helper_binding_stack,
1406
+ emitted_lines,
1407
+ )
1408
+ resolving.remove(expr_node.name)
1409
+ return resolved
1410
+
1411
+ if isinstance(expr_node, Subscript):
1412
+ index_cpp = self._build_security_expr(
1413
+ sec_id,
1414
+ expr_node.index,
1415
+ ta_range,
1416
+ ta_results,
1417
+ resolving,
1418
+ security_mutable_names,
1419
+ helper_binding_stack,
1420
+ emitted_lines,
1421
+ )
1422
+ if isinstance(expr_node.object, Identifier):
1423
+ bound = self._security_lookup_helper_binding(expr_node.object.name, helper_binding_stack)
1424
+ if bound is not None:
1425
+ if isinstance(bound, str):
1426
+ series_name = self._security_series_binding_target(bound)
1427
+ if series_name is not None:
1428
+ return f'_security_helper_series_["{series_name}"][{index_cpp}]'
1429
+ return bound
1430
+ obj_cpp = self._build_security_expr(
1431
+ sec_id,
1432
+ bound,
1433
+ ta_range,
1434
+ ta_results,
1435
+ resolving,
1436
+ security_mutable_names,
1437
+ helper_binding_stack,
1438
+ emitted_lines,
1439
+ )
1440
+ return f"{obj_cpp}[{index_cpp}]"
1441
+ if expr_node.object.name in SECURITY_OHLC_BAR_FIELDS:
1442
+ idx_lit = self._literal_int_for_security_index(expr_node.index)
1443
+ if idx_lit is not None:
1444
+ bar_map = {
1445
+ "open": "bar.open",
1446
+ "high": "bar.high",
1447
+ "low": "bar.low",
1448
+ "close": "bar.close",
1449
+ "volume": "bar.volume",
1450
+ }
1451
+ if idx_lit == 0:
1452
+ return bar_map[expr_node.object.name]
1453
+ if idx_lit >= 1:
1454
+ # lookahead_off: we evaluate when an HTF bar completes; `bar` is that
1455
+ # bar. On the HTF series, high[0]/close is the current (just-finished)
1456
+ # bar; high[1] is one HTF bar back = hist[field][0] *before* we push
1457
+ # `bar` (Series [0] = most recent prior push). high[k] -> hist[k-1].
1458
+ field = expr_node.object.name
1459
+ hist = self._security_ohlc_hist_series_cpp(sec_id, field)
1460
+ return f"{hist}[{idx_lit - 1}]"
1461
+ self._codegen_error(
1462
+ expr_node,
1463
+ "request.security() OHLC history index must be a literal integer (e.g. high[1])",
1464
+ )
1465
+
1466
+ if isinstance(expr_node, BinOp):
1467
+ left = self._build_security_expr(
1468
+ sec_id, expr_node.left, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1469
+ )
1470
+ right = self._build_security_expr(
1471
+ sec_id, expr_node.right, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1472
+ )
1473
+ cpp_ops = {"and": "&&", "or": "||"}
1474
+ op = cpp_ops.get(expr_node.op, expr_node.op)
1475
+ if expr_node.op == "%":
1476
+ return f"std::fmod((double)({left}), (double)({right}))"
1477
+ return f"({left} {op} {right})"
1478
+
1479
+ if isinstance(expr_node, UnaryOp):
1480
+ operand = self._build_security_expr(
1481
+ sec_id, expr_node.operand, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1482
+ )
1483
+ if expr_node.op == "not":
1484
+ return f"!({operand})"
1485
+ return f"({expr_node.op}{operand})"
1486
+
1487
+ if isinstance(expr_node, Ternary):
1488
+ cond = self._build_security_expr(
1489
+ sec_id, expr_node.condition, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1490
+ )
1491
+ tv = self._build_security_expr(
1492
+ sec_id, expr_node.true_val, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1493
+ )
1494
+ fv = self._build_security_expr(
1495
+ sec_id, expr_node.false_val, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
1496
+ )
1497
+ return f"(({cond}) ? ({tv}) : ({fv}))"
1498
+
1499
+ if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
1500
+ func_name = expr_node.callee.name
1501
+ if func_name in self._func_names:
1502
+ call_key = f"func:{func_name}"
1503
+ if call_key in resolving:
1504
+ self._codegen_error(
1505
+ expr_node,
1506
+ "request.security helper functions must not recurse while building a security context",
1507
+ )
1508
+ resolving.add(call_key)
1509
+ plan = self._security_helper_call_plan(
1510
+ expr_node,
1511
+ helper_binding_stack,
1512
+ )
1513
+ if plan["mode"] == "expr":
1514
+ resolved = self._build_security_expr(
1515
+ sec_id,
1516
+ plan["expr"],
1517
+ ta_range,
1518
+ ta_results,
1519
+ resolving,
1520
+ security_mutable_names,
1521
+ plan["binding_stack"],
1522
+ emitted_lines,
1523
+ )
1524
+ else:
1525
+ if emitted_lines is None:
1526
+ self._codegen_error(
1527
+ expr_node,
1528
+ "request.security multi-statement helpers require statement-capable security evaluation context",
1529
+ )
1530
+ resolved = self._emit_security_linear_helper_call(
1531
+ sec_id,
1532
+ plan,
1533
+ ta_results,
1534
+ security_mutable_names,
1535
+ emitted_lines,
1536
+ resolving,
1537
+ )
1538
+ resolving.remove(call_key)
1539
+ return resolved
1540
+
1541
+ site = self._get_ta_site(expr_node)
1542
+ if site:
1543
+ idx = self._ta_index_by_site_id.get(id(site))
1544
+ sig = self._security_binding_stack_signature(helper_binding_stack)
1545
+ if idx is not None:
1546
+ result_key = (idx, sig)
1547
+ if result_key in ta_results:
1548
+ return ta_results[result_key]
1549
+ sec_name = self._security_ta_variant_names.get(
1550
+ (sec_id, idx, sig),
1551
+ f"_sec{sec_id}_{site.member_name}",
1552
+ )
1553
+ compute_args = self._security_ta_compute_args_for_site(
1554
+ sec_id,
1555
+ site,
1556
+ ta_results,
1557
+ security_mutable_names,
1558
+ helper_binding_stack,
1559
+ emitted_lines,
1560
+ )
1561
+ return f"(is_complete ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
1562
+
1563
+ result = self._visit_expr(expr_node)
1564
+ return self._rewrite_security_cpp(result, sec_id, security_mutable_names, helper_binding_stack)