@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.
- package/README.md +25 -0
- package/index.mjs +20 -0
- package/package.json +32 -0
- package/pineforge_codegen/__init__.py +53 -0
- package/pineforge_codegen/analyzer/__init__.py +60 -0
- package/pineforge_codegen/analyzer/base.py +1566 -0
- package/pineforge_codegen/analyzer/call_handlers.py +895 -0
- package/pineforge_codegen/analyzer/contracts.py +163 -0
- package/pineforge_codegen/analyzer/diagnostics.py +118 -0
- package/pineforge_codegen/analyzer/tables.py +204 -0
- package/pineforge_codegen/analyzer/types.py +261 -0
- package/pineforge_codegen/ast_nodes.py +293 -0
- package/pineforge_codegen/codegen/__init__.py +78 -0
- package/pineforge_codegen/codegen/base.py +1381 -0
- package/pineforge_codegen/codegen/emit_top.py +875 -0
- package/pineforge_codegen/codegen/helpers.py +163 -0
- package/pineforge_codegen/codegen/helpers_syminfo.py +132 -0
- package/pineforge_codegen/codegen/input.py +189 -0
- package/pineforge_codegen/codegen/security.py +1564 -0
- package/pineforge_codegen/codegen/ta.py +298 -0
- package/pineforge_codegen/codegen/tables.py +683 -0
- package/pineforge_codegen/codegen/types.py +592 -0
- package/pineforge_codegen/codegen/visit_call.py +1387 -0
- package/pineforge_codegen/codegen/visit_expr.py +729 -0
- package/pineforge_codegen/codegen/visit_stmt.py +766 -0
- package/pineforge_codegen/errors.py +98 -0
- package/pineforge_codegen/lexer.py +531 -0
- package/pineforge_codegen/parser.py +1198 -0
- package/pineforge_codegen/pragmas.py +117 -0
- package/pineforge_codegen/signatures.py +808 -0
- package/pineforge_codegen/support_checker.py +1223 -0
- package/pineforge_codegen/symbols.py +118 -0
- package/pineforge_codegen/tokens.py +406 -0
- package/pineforge_codegen/tv_input_choices.py +86 -0
- package/pineforge_codegen-0.7.0.tar.gz +0 -0
- package/release.json +7 -0
- 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)
|