@pineforge/codegen-pyodide 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2359 -249
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +732 -172
- package/pineforge_codegen/codegen/security.py +1456 -150
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +224 -77
- package/pineforge_codegen/codegen/types.py +1196 -215
- package/pineforge_codegen/codegen/visit_call.py +1168 -99
- package/pineforge_codegen/codegen/visit_expr.py +354 -33
- package/pineforge_codegen/codegen/visit_stmt.py +1144 -200
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +194 -19
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
|
@@ -58,11 +58,13 @@ tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
|
|
|
58
58
|
|
|
59
59
|
from __future__ import annotations
|
|
60
60
|
|
|
61
|
+
from dataclasses import replace
|
|
62
|
+
|
|
61
63
|
from ..ast_nodes import (
|
|
62
64
|
ASTNode, Assignment, BinOp, BoolLiteral, BreakStmt, ContinueStmt, ExprStmt,
|
|
63
65
|
ForStmt, ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
|
|
64
|
-
NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary,
|
|
65
|
-
TupleLiteral, UnaryOp, VarDecl, WhileStmt,
|
|
66
|
+
NaLiteral, NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary,
|
|
67
|
+
TupleAssign, TupleLiteral, UnaryOp, VarDecl, WhileStmt,
|
|
66
68
|
)
|
|
67
69
|
from ..analyzer import (
|
|
68
70
|
FuncInfo, TACallSite, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
|
|
@@ -76,6 +78,19 @@ from .tables import (
|
|
|
76
78
|
)
|
|
77
79
|
|
|
78
80
|
|
|
81
|
+
class _SecurityHelperArgumentFrame(dict):
|
|
82
|
+
"""Callee bindings whose argument ASTs belong to the caller scope.
|
|
83
|
+
|
|
84
|
+
A callee may reuse a caller parameter name. Keeping the caller binding
|
|
85
|
+
stack with the argument prevents that new callee frame from capturing
|
|
86
|
+
identifiers inside its own argument expression and falsely recursing.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, values: dict, caller_stack: tuple[dict, ...]) -> None:
|
|
90
|
+
super().__init__(values)
|
|
91
|
+
self.caller_stack = caller_stack
|
|
92
|
+
|
|
93
|
+
|
|
79
94
|
class SecurityEmitter:
|
|
80
95
|
"""Mixin owning ``request.security()`` lowering: evaluators, dispatch,
|
|
81
96
|
rebind/binding/TA-variant machinery, and the per-call helper plan.
|
|
@@ -96,14 +111,22 @@ class SecurityEmitter:
|
|
|
96
111
|
"""
|
|
97
112
|
if isinstance(tf_node, StringLiteral):
|
|
98
113
|
return tf_node.value, None
|
|
114
|
+
if isinstance(tf_node, SwitchStmt):
|
|
115
|
+
# Keep diagnostics from the registration-time switch renderer
|
|
116
|
+
# visible; the broad expression fallback below intentionally
|
|
117
|
+
# catches ordinary unresolved expressions.
|
|
118
|
+
return None, self._security_tf_runtime_expr(tf_node)
|
|
99
119
|
if isinstance(tf_node, Identifier):
|
|
100
120
|
name = tf_node.name
|
|
101
121
|
if name in self._timeframe_period_vars:
|
|
102
122
|
return None, "script_tf_"
|
|
103
123
|
if (name in self._known_vars and name not in self._input_backed_vars
|
|
124
|
+
and not self._known_var_is_lexically_shadowed(name)
|
|
104
125
|
and isinstance(self._known_vars[name], str)):
|
|
105
126
|
return self._known_vars[name], None
|
|
106
|
-
if name in self._input_backed_vars
|
|
127
|
+
if (name in self._input_backed_vars
|
|
128
|
+
and name in self._input_var_to_call
|
|
129
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
107
130
|
return None, self._visit_expr(self._input_var_to_call[name])
|
|
108
131
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
109
132
|
if name in global_expr_map:
|
|
@@ -140,42 +163,182 @@ class SecurityEmitter:
|
|
|
140
163
|
: inputTf`` must be expanded to their source expression with direct
|
|
141
164
|
``get_input_*`` reads. Emitting the member name would register with its
|
|
142
165
|
default-constructed value (usually an empty string).
|
|
166
|
+
|
|
167
|
+
Delegates to :meth:`_substitute_tf_input_reads`, which rewrites the
|
|
168
|
+
input-derived *leaves* of the expression tree — including leaves buried
|
|
169
|
+
inside a ternary CONDITION or any BinOp / UnaryOp / FuncCall — and then
|
|
170
|
+
renders the substituted tree through the normal expression visitor.
|
|
143
171
|
"""
|
|
144
172
|
if node is None:
|
|
145
173
|
return None
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
174
|
+
if isinstance(node, SwitchStmt):
|
|
175
|
+
return self._security_tf_switch_runtime_expr(node, resolving or set())
|
|
176
|
+
substituted = self._substitute_tf_input_reads(node, resolving or set())
|
|
177
|
+
return self._visit_expr(substituted)
|
|
178
|
+
|
|
179
|
+
def _security_tf_switch_runtime_expr(
|
|
180
|
+
self, node: SwitchStmt, resolving: set[str]
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Render a pure switch expression for evaluator registration.
|
|
183
|
+
|
|
184
|
+
Pine parses ``tf = switch timeframe.period ...`` as a ``SwitchStmt``
|
|
185
|
+
even though it is the right-hand side of a declaration. The regular
|
|
186
|
+
expression visitor deliberately does not render statement nodes, so
|
|
187
|
+
passing that alias to ``request.security`` previously produced the
|
|
188
|
+
truthy placeholder ``/* unknown */`` in generated C++. Registration
|
|
189
|
+
only needs the value selection, which can be represented as nested
|
|
190
|
+
conditional expressions over setup-time-safe leaves such as
|
|
191
|
+
``script_tf_`` and direct input getters.
|
|
192
|
+
|
|
193
|
+
Only pure, single-expression arms with an explicit default are accepted
|
|
194
|
+
here. Other shapes cannot be registered deterministically and produce
|
|
195
|
+
a clear codegen diagnostic.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def arm_value(body: list) -> str:
|
|
199
|
+
if len(body) != 1 or not isinstance(body[0], ExprStmt):
|
|
200
|
+
self._codegen_error(
|
|
201
|
+
node,
|
|
202
|
+
"request.security timeframe switch arms must contain one expression",
|
|
203
|
+
hint=(
|
|
204
|
+
"Compute multi-statement timeframe logic before the "
|
|
205
|
+
"request.security call or rewrite it as pure switch arms."
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
value = self._security_tf_runtime_expr(body[0].expr, resolving)
|
|
209
|
+
if value is None:
|
|
210
|
+
self._codegen_error(
|
|
211
|
+
node,
|
|
212
|
+
"request.security timeframe switch arm could not be resolved at setup",
|
|
213
|
+
)
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
if not node.default_body:
|
|
217
|
+
self._codegen_error(
|
|
218
|
+
node,
|
|
219
|
+
"request.security timeframe switch requires a default arm",
|
|
220
|
+
hint=(
|
|
221
|
+
"An unmatched Pine switch yields na; add an explicit default "
|
|
222
|
+
"timeframe so the evaluator can be registered deterministically."
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
result = arm_value(node.default_body)
|
|
226
|
+
|
|
227
|
+
selector = None
|
|
228
|
+
if node.expr is not None:
|
|
229
|
+
selector = self._security_tf_runtime_expr(node.expr, resolving)
|
|
230
|
+
if selector is None:
|
|
231
|
+
self._codegen_error(
|
|
232
|
+
node,
|
|
233
|
+
"request.security timeframe switch selector could not be resolved at setup",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
for case_expr, body in reversed(node.cases):
|
|
237
|
+
value = arm_value(body)
|
|
238
|
+
case = self._security_tf_runtime_expr(case_expr, resolving)
|
|
239
|
+
if case is None:
|
|
240
|
+
self._codegen_error(
|
|
241
|
+
node,
|
|
242
|
+
"request.security timeframe switch case could not be resolved at setup",
|
|
243
|
+
)
|
|
244
|
+
condition = (
|
|
245
|
+
f"(({selector}) == ({case}))" if selector is not None else case
|
|
246
|
+
)
|
|
247
|
+
result = f"(({condition}) ? ({value}) : ({result}))"
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
def _substitute_tf_input_reads(self, node, resolving: set[str]):
|
|
251
|
+
"""Return ``node`` with input-derived leaves rewritten to expressions
|
|
252
|
+
that are valid at security-registration time (before ``on_bar()``
|
|
253
|
+
assigns members from their inputs):
|
|
254
|
+
|
|
255
|
+
* an input-backed var -> its ``input.*()`` source call (``get_input_*``);
|
|
256
|
+
* a ``timeframe.period`` alias var -> ``timeframe.period`` (``script_tf_``);
|
|
257
|
+
* a known compile-time string var -> that string literal;
|
|
258
|
+
* a global alias -> its defining expression, expanded recursively.
|
|
259
|
+
|
|
260
|
+
The walk descends through Ternary / BinOp / UnaryOp / FuncCall /
|
|
261
|
+
Subscript, so an input-backed identifier nested inside e.g. a ternary
|
|
262
|
+
condition (``mode == "15" ? "240" : "60"``) resolves to its input read
|
|
263
|
+
instead of the uninitialised member. A subtree containing no
|
|
264
|
+
input-derived leaf is returned unchanged (same object) so unaffected
|
|
265
|
+
timeframe expressions render byte-identically to before the fix.
|
|
266
|
+
MemberAccess and literals are left verbatim: ``timeframe.period`` is
|
|
267
|
+
already lowered to ``script_tf_`` by the expression visitor.
|
|
268
|
+
"""
|
|
269
|
+
if not isinstance(node, ASTNode):
|
|
270
|
+
return node
|
|
149
271
|
if isinstance(node, Identifier):
|
|
150
272
|
name = node.name
|
|
151
273
|
if name in self._timeframe_period_vars:
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
274
|
+
return MemberAccess(
|
|
275
|
+
object=Identifier(name="timeframe", loc=node.loc),
|
|
276
|
+
member="period",
|
|
277
|
+
loc=node.loc,
|
|
278
|
+
annotations=node.annotations,
|
|
279
|
+
)
|
|
280
|
+
if (name in self._input_backed_vars
|
|
281
|
+
and name in self._input_var_to_call
|
|
282
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
283
|
+
return self._input_var_to_call[name]
|
|
155
284
|
if (name in self._known_vars and name not in self._input_backed_vars
|
|
285
|
+
and not self._known_var_is_lexically_shadowed(name)
|
|
156
286
|
and isinstance(self._known_vars[name], str)):
|
|
157
|
-
return
|
|
287
|
+
return StringLiteral(
|
|
288
|
+
value=self._known_vars[name],
|
|
289
|
+
loc=node.loc,
|
|
290
|
+
annotations=node.annotations,
|
|
291
|
+
)
|
|
158
292
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
159
293
|
if name in global_expr_map and name not in resolving:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return out
|
|
164
|
-
return self._visit_expr(node)
|
|
165
|
-
if (
|
|
166
|
-
isinstance(node, MemberAccess)
|
|
167
|
-
and isinstance(node.object, Identifier)
|
|
168
|
-
and node.object.name == "timeframe"
|
|
169
|
-
and node.member == "period"
|
|
170
|
-
):
|
|
171
|
-
return "script_tf_"
|
|
294
|
+
return self._substitute_tf_input_reads(
|
|
295
|
+
global_expr_map[name], resolving | {name})
|
|
296
|
+
return node
|
|
172
297
|
if isinstance(node, Ternary):
|
|
173
|
-
cond = self.
|
|
174
|
-
tv = self.
|
|
175
|
-
fv = self.
|
|
176
|
-
if cond is
|
|
177
|
-
return
|
|
178
|
-
|
|
298
|
+
cond = self._substitute_tf_input_reads(node.condition, resolving)
|
|
299
|
+
tv = self._substitute_tf_input_reads(node.true_val, resolving)
|
|
300
|
+
fv = self._substitute_tf_input_reads(node.false_val, resolving)
|
|
301
|
+
if cond is node.condition and tv is node.true_val and fv is node.false_val:
|
|
302
|
+
return node
|
|
303
|
+
return replace(node, condition=cond, true_val=tv, false_val=fv)
|
|
304
|
+
if isinstance(node, BinOp):
|
|
305
|
+
left = self._substitute_tf_input_reads(node.left, resolving)
|
|
306
|
+
right = self._substitute_tf_input_reads(node.right, resolving)
|
|
307
|
+
if left is node.left and right is node.right:
|
|
308
|
+
return node
|
|
309
|
+
return replace(node, left=left, right=right)
|
|
310
|
+
if isinstance(node, UnaryOp):
|
|
311
|
+
operand = self._substitute_tf_input_reads(node.operand, resolving)
|
|
312
|
+
if operand is node.operand:
|
|
313
|
+
return node
|
|
314
|
+
return replace(node, operand=operand)
|
|
315
|
+
if isinstance(node, FuncCall):
|
|
316
|
+
new_args = [self._substitute_tf_input_reads(a, resolving) for a in node.args]
|
|
317
|
+
new_kwargs = {
|
|
318
|
+
k: (self._substitute_tf_input_reads(v, resolving)
|
|
319
|
+
if isinstance(v, ASTNode) else v)
|
|
320
|
+
for k, v in node.kwargs.items()
|
|
321
|
+
}
|
|
322
|
+
unchanged = (
|
|
323
|
+
all(a is b for a, b in zip(new_args, node.args))
|
|
324
|
+
and all(new_kwargs[k] is node.kwargs[k] for k in node.kwargs)
|
|
325
|
+
)
|
|
326
|
+
if unchanged:
|
|
327
|
+
return node
|
|
328
|
+
# Preserve the source span and annotations of the original call.
|
|
329
|
+
# Namespace/receiver resolution uses that provenance to decide
|
|
330
|
+
# whether a same-named global binding was visible at this call's
|
|
331
|
+
# source position. Rebuilding a bare FuncCall here used to turn
|
|
332
|
+
# the span into synthetic 1:1 and could let a later ``map`` global
|
|
333
|
+
# capture this earlier built-in ``map.get(...)``.
|
|
334
|
+
return replace(node, args=new_args, kwargs=new_kwargs)
|
|
335
|
+
if isinstance(node, Subscript):
|
|
336
|
+
obj = self._substitute_tf_input_reads(node.object, resolving)
|
|
337
|
+
idx = self._substitute_tf_input_reads(node.index, resolving)
|
|
338
|
+
if obj is node.object and idx is node.index:
|
|
339
|
+
return node
|
|
340
|
+
return replace(node, object=obj, index=idx)
|
|
341
|
+
return node
|
|
179
342
|
|
|
180
343
|
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
181
344
|
"""For a ``request.security`` whose tf is function parameter ``param_name``
|
|
@@ -226,6 +389,9 @@ class SecurityEmitter:
|
|
|
226
389
|
"expr_node": item.expression,
|
|
227
390
|
"returns_tuple": item.returns_tuple,
|
|
228
391
|
"tuple_size": item.tuple_size,
|
|
392
|
+
"tuple_element_types": tuple(
|
|
393
|
+
getattr(item, "tuple_element_types", ()) or ()
|
|
394
|
+
),
|
|
229
395
|
"gaps_node": item.gaps,
|
|
230
396
|
"lookahead_node": item.lookahead,
|
|
231
397
|
"ta_range": item.ta_range,
|
|
@@ -242,6 +408,7 @@ class SecurityEmitter:
|
|
|
242
408
|
"expr_node": item[2] if len(item) > 2 else None,
|
|
243
409
|
"returns_tuple": item[3] if len(item) > 3 else False,
|
|
244
410
|
"tuple_size": item[4] if len(item) > 4 else 0,
|
|
411
|
+
"tuple_element_types": (),
|
|
245
412
|
"gaps_node": item[5] if len(item) > 5 else None,
|
|
246
413
|
"lookahead_node": item[6] if len(item) > 6 else None,
|
|
247
414
|
"ta_range": item[7] if len(item) > 7 else None,
|
|
@@ -322,6 +489,22 @@ class SecurityEmitter:
|
|
|
322
489
|
name: str,
|
|
323
490
|
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
|
|
324
491
|
):
|
|
492
|
+
resolved = self._security_lookup_helper_binding_context(
|
|
493
|
+
name, helper_binding_stack
|
|
494
|
+
)
|
|
495
|
+
return resolved[0] if resolved is not None else None
|
|
496
|
+
|
|
497
|
+
def _security_lookup_helper_binding_context(
|
|
498
|
+
self,
|
|
499
|
+
name: str,
|
|
500
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
|
|
501
|
+
):
|
|
502
|
+
"""Return ``(bound_value, lexical_stack_for_value)``.
|
|
503
|
+
|
|
504
|
+
Ordinary helper-local bindings are authored in the current stack.
|
|
505
|
+
Callee argument bindings are authored in the caller stack captured by
|
|
506
|
+
``_SecurityHelperArgumentFrame`` and must be expanded there.
|
|
507
|
+
"""
|
|
325
508
|
if not helper_binding_stack:
|
|
326
509
|
return None
|
|
327
510
|
for frame in reversed(helper_binding_stack):
|
|
@@ -330,7 +513,12 @@ class SecurityEmitter:
|
|
|
330
513
|
bound = frame[name]
|
|
331
514
|
if isinstance(bound, Identifier) and bound.name == name:
|
|
332
515
|
continue
|
|
333
|
-
|
|
516
|
+
lexical_stack = (
|
|
517
|
+
frame.caller_stack
|
|
518
|
+
if isinstance(frame, _SecurityHelperArgumentFrame)
|
|
519
|
+
else helper_binding_stack
|
|
520
|
+
)
|
|
521
|
+
return bound, lexical_stack
|
|
334
522
|
return None
|
|
335
523
|
|
|
336
524
|
def _literal_int_for_security_index(self, node) -> int | None:
|
|
@@ -353,11 +541,11 @@ class SecurityEmitter:
|
|
|
353
541
|
return None
|
|
354
542
|
return None
|
|
355
543
|
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
544
|
+
# Narrow approximations used only while resolving a request.security()
|
|
545
|
+
# history index to a literal. This is not chart-scope barstate lowering:
|
|
546
|
+
# visit_expr emits runtime state for islast/islastconfirmedhistory, whereas
|
|
547
|
+
# this security-index folder currently treats them as false. Other flags
|
|
548
|
+
# (isfirst/isnew/isconfirmed) remain unknown and are deliberately absent.
|
|
361
549
|
_SECURITY_CONST_BARSTATE = {
|
|
362
550
|
"isrealtime": False,
|
|
363
551
|
"islast": False,
|
|
@@ -373,10 +561,10 @@ class SecurityEmitter:
|
|
|
373
561
|
) -> bool | None:
|
|
374
562
|
"""Fold a boolean expression to a compile-time constant, or None.
|
|
375
563
|
|
|
376
|
-
Only reduces expressions whose non-literal leaves
|
|
377
|
-
|
|
378
|
-
resolve a request.security history index
|
|
379
|
-
``barstate.isrealtime ? 1 : 0`` and friends."""
|
|
564
|
+
Only reduces expressions whose non-literal leaves have a narrow
|
|
565
|
+
request.security history-index approximation above; anything else
|
|
566
|
+
returns None. Used to resolve a request.security history index
|
|
567
|
+
expressed as ``barstate.isrealtime ? 1 : 0`` and friends."""
|
|
380
568
|
if resolving is None:
|
|
381
569
|
resolving = set()
|
|
382
570
|
if isinstance(node, BoolLiteral):
|
|
@@ -403,16 +591,33 @@ class SecurityEmitter:
|
|
|
403
591
|
return None
|
|
404
592
|
return (left and right) if node.op == "and" else (left or right)
|
|
405
593
|
if isinstance(node, Identifier):
|
|
406
|
-
|
|
407
|
-
if
|
|
594
|
+
binding = None
|
|
595
|
+
if not self._security_identifier_is_global_binding(node):
|
|
596
|
+
binding = self._security_lookup_helper_binding_context(
|
|
597
|
+
node.name, helper_binding_stack
|
|
598
|
+
)
|
|
599
|
+
if binding is not None:
|
|
600
|
+
bound, bound_stack = binding
|
|
601
|
+
if isinstance(bound, str):
|
|
602
|
+
return None
|
|
603
|
+
return self._fold_security_const_bool(
|
|
604
|
+
bound, bound_stack, resolving
|
|
605
|
+
)
|
|
606
|
+
param_key = f"param:{id(node)}"
|
|
607
|
+
param_binding = self._security_index_param_callsite_binding(node)
|
|
608
|
+
if param_binding is not None and param_key not in resolving:
|
|
408
609
|
return self._fold_security_const_bool(
|
|
409
|
-
|
|
610
|
+
param_binding, (), resolving | {param_key}
|
|
410
611
|
)
|
|
411
612
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
412
|
-
if
|
|
613
|
+
if (
|
|
614
|
+
self._security_identifier_is_global_binding(node)
|
|
615
|
+
and node.name in global_expr_map
|
|
616
|
+
and node.name not in resolving
|
|
617
|
+
):
|
|
413
618
|
resolving.add(node.name)
|
|
414
619
|
out = self._fold_security_const_bool(
|
|
415
|
-
global_expr_map[node.name],
|
|
620
|
+
global_expr_map[node.name], (), resolving
|
|
416
621
|
)
|
|
417
622
|
resolving.remove(node.name)
|
|
418
623
|
return out
|
|
@@ -428,27 +633,44 @@ class SecurityEmitter:
|
|
|
428
633
|
|
|
429
634
|
Extends ``_literal_int_for_security_index`` by also resolving the index
|
|
430
635
|
through helper-parameter bindings and global aliases and constant-folding
|
|
431
|
-
a ternary whose condition
|
|
432
|
-
``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index
|
|
433
|
-
circuits on the first line, so behaviour is unchanged for every
|
|
434
|
-
literal index. Returns None when the index cannot be reduced to
|
|
435
|
-
compile-time literal, so the caller keeps its existing rejection."""
|
|
636
|
+
a ternary whose condition has a security-index barstate approximation
|
|
637
|
+
(e.g. ``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index
|
|
638
|
+
short-circuits on the first line, so behaviour is unchanged for every
|
|
639
|
+
already-literal index. Returns None when the index cannot be reduced to
|
|
640
|
+
a compile-time literal, so the caller keeps its existing rejection."""
|
|
436
641
|
direct = self._literal_int_for_security_index(node)
|
|
437
642
|
if direct is not None:
|
|
438
643
|
return direct
|
|
439
644
|
if resolving is None:
|
|
440
645
|
resolving = set()
|
|
441
646
|
if isinstance(node, Identifier):
|
|
442
|
-
|
|
443
|
-
if
|
|
647
|
+
binding = None
|
|
648
|
+
if not self._security_identifier_is_global_binding(node):
|
|
649
|
+
binding = self._security_lookup_helper_binding_context(
|
|
650
|
+
node.name, helper_binding_stack
|
|
651
|
+
)
|
|
652
|
+
if binding is not None:
|
|
653
|
+
bound, bound_stack = binding
|
|
654
|
+
if isinstance(bound, str):
|
|
655
|
+
return None
|
|
656
|
+
return self._resolve_security_index_literal(
|
|
657
|
+
bound, bound_stack, resolving
|
|
658
|
+
)
|
|
659
|
+
param_key = f"param:{id(node)}"
|
|
660
|
+
param_binding = self._security_index_param_callsite_binding(node)
|
|
661
|
+
if param_binding is not None and param_key not in resolving:
|
|
444
662
|
return self._resolve_security_index_literal(
|
|
445
|
-
|
|
663
|
+
param_binding, (), resolving | {param_key}
|
|
446
664
|
)
|
|
447
665
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
448
|
-
if
|
|
666
|
+
if (
|
|
667
|
+
self._security_identifier_is_global_binding(node)
|
|
668
|
+
and node.name in global_expr_map
|
|
669
|
+
and node.name not in resolving
|
|
670
|
+
):
|
|
449
671
|
resolving.add(node.name)
|
|
450
672
|
out = self._resolve_security_index_literal(
|
|
451
|
-
global_expr_map[node.name],
|
|
673
|
+
global_expr_map[node.name], (), resolving
|
|
452
674
|
)
|
|
453
675
|
resolving.remove(node.name)
|
|
454
676
|
return out
|
|
@@ -463,35 +685,395 @@ class SecurityEmitter:
|
|
|
463
685
|
)
|
|
464
686
|
return None
|
|
465
687
|
|
|
466
|
-
def
|
|
467
|
-
"""
|
|
688
|
+
def _security_identifier_is_global_binding(self, node: Identifier) -> bool:
|
|
689
|
+
"""Whether this exact identifier AST resolved to a global binding.
|
|
690
|
+
|
|
691
|
+
``global_expr_map`` is keyed only by spelling. Consulting it without
|
|
692
|
+
node provenance lets a UDF parameter, block local, or loop iterator
|
|
693
|
+
capture a same-named global input after analysis scopes have unwound.
|
|
694
|
+
"""
|
|
695
|
+
scopes = getattr(self.ctx, "identifier_binding_scopes", {}) or {}
|
|
696
|
+
return scopes.get(id(node)) == "global"
|
|
697
|
+
|
|
698
|
+
def _security_index_param_callsite_binding(self, node: Identifier):
|
|
699
|
+
"""Resolve one UDF parameter index from its call sites, conservatively.
|
|
700
|
+
|
|
701
|
+
A request.security evaluator is a class method, so a parameter of the
|
|
702
|
+
UDF that *contains* the request call is not otherwise in scope there.
|
|
703
|
+
Reuse the established timeframe-callsite model for the narrow offset
|
|
704
|
+
case: one call site is exact; several are accepted only for the same
|
|
705
|
+
literal or the same proven-global identifier. Mixed bindings fail
|
|
706
|
+
closed instead of sharing one evaluator across different offsets.
|
|
707
|
+
"""
|
|
708
|
+
scopes = getattr(self.ctx, "identifier_binding_scopes", {}) or {}
|
|
709
|
+
scope = scopes.get(id(node))
|
|
710
|
+
if not isinstance(scope, str) or not scope.startswith("func_"):
|
|
711
|
+
return None
|
|
712
|
+
func_name = scope[5:]
|
|
713
|
+
func_info = self._func_info_map.get(func_name)
|
|
714
|
+
if (
|
|
715
|
+
func_info is None
|
|
716
|
+
or func_info.node is None
|
|
717
|
+
or node.name not in func_info.node.params
|
|
718
|
+
):
|
|
719
|
+
return None
|
|
720
|
+
# A parameter is only equal to its call-site argument until the UDF
|
|
721
|
+
# reassigns it. Specializing a later ``ta.*[param]`` from the authored
|
|
722
|
+
# call argument would otherwise ignore legal ``param := ...`` writes
|
|
723
|
+
# and silently select the wrong requested-context history bar.
|
|
724
|
+
for child in self._walk_ast(func_info.node):
|
|
725
|
+
if (
|
|
726
|
+
isinstance(child, Assignment)
|
|
727
|
+
and isinstance(child.target, Identifier)
|
|
728
|
+
and child.target.name == node.name
|
|
729
|
+
):
|
|
730
|
+
return None
|
|
731
|
+
if isinstance(child, VarDecl) and child.name == node.name:
|
|
732
|
+
return None
|
|
733
|
+
if isinstance(child, TupleAssign) and node.name in child.names:
|
|
734
|
+
return None
|
|
735
|
+
if isinstance(child, ForStmt) and child.var == node.name:
|
|
736
|
+
return None
|
|
737
|
+
if isinstance(child, ForInStmt) and (
|
|
738
|
+
child.var == node.name or node.name in (child.vars or [])
|
|
739
|
+
):
|
|
740
|
+
return None
|
|
741
|
+
param_index = list(func_info.node.params).index(node.name)
|
|
742
|
+
bound_args = []
|
|
743
|
+
for call in self._walk_ast(self.ctx.ast):
|
|
744
|
+
if not (
|
|
745
|
+
isinstance(call, FuncCall)
|
|
746
|
+
and isinstance(call.callee, Identifier)
|
|
747
|
+
and call.callee.name == func_name
|
|
748
|
+
):
|
|
749
|
+
continue
|
|
750
|
+
arg = call.args[param_index] if param_index < len(call.args) else None
|
|
751
|
+
if node.name in call.kwargs:
|
|
752
|
+
arg = call.kwargs[node.name]
|
|
753
|
+
if arg is None:
|
|
754
|
+
return None
|
|
755
|
+
bound_args.append(arg)
|
|
756
|
+
if not bound_args:
|
|
757
|
+
return None
|
|
758
|
+
|
|
759
|
+
first = bound_args[0]
|
|
760
|
+
|
|
761
|
+
def equivalent(left, right) -> bool:
|
|
762
|
+
if isinstance(left, NumberLiteral) and isinstance(right, NumberLiteral):
|
|
763
|
+
return left.value == right.value
|
|
764
|
+
if isinstance(left, Identifier) and isinstance(right, Identifier):
|
|
765
|
+
return (
|
|
766
|
+
left.name == right.name
|
|
767
|
+
and self._security_identifier_is_global_binding(left)
|
|
768
|
+
and self._security_identifier_is_global_binding(right)
|
|
769
|
+
)
|
|
770
|
+
return left is right
|
|
771
|
+
|
|
772
|
+
if not all(equivalent(first, other) for other in bound_args[1:]):
|
|
773
|
+
return None
|
|
774
|
+
return first
|
|
775
|
+
|
|
776
|
+
def _resolve_security_immutable_input_int(
|
|
777
|
+
self,
|
|
778
|
+
node,
|
|
779
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
780
|
+
resolving: set[int] | None = None,
|
|
781
|
+
) -> tuple[FuncCall, tuple[dict[str, ASTNode], ...]] | None:
|
|
782
|
+
"""Resolve an immutable security-history offset to its ``input.int``.
|
|
783
|
+
|
|
784
|
+
Pine v6 permits a bar-invariant input integer as a history offset. A
|
|
785
|
+
request.security TA offset needs special admission because its backing
|
|
786
|
+
history advances in the requested context, not in chart context. Keep
|
|
787
|
+
this proof deliberately narrower than arbitrary ``series int`` support:
|
|
788
|
+
|
|
789
|
+
* the leaf must be exactly ``input.int``;
|
|
790
|
+
* helper-parameter and top-level immutable alias chains are followed;
|
|
791
|
+
* ``var``/``varip`` or any globally reassigned alias is rejected; and
|
|
792
|
+
* arithmetic/ternary expressions remain unsupported for now.
|
|
793
|
+
|
|
794
|
+
Return the proven input call plus the lexical stack in which that call
|
|
795
|
+
was authored. Rendering the original identifier under a helper's
|
|
796
|
+
stack would let a same-named helper parameter capture a global alias.
|
|
797
|
+
Crossing ``global_expr_map`` therefore resets the stack to global.
|
|
798
|
+
|
|
799
|
+
The generated ``Series`` safely returns ``na`` for insufficient depth;
|
|
800
|
+
negative runtime overrides are also lowered to ``na`` by the caller.
|
|
801
|
+
"""
|
|
802
|
+
if resolving is None:
|
|
803
|
+
resolving = set()
|
|
804
|
+
|
|
805
|
+
if isinstance(node, FuncCall):
|
|
806
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
807
|
+
if namespace == "input" and func_name == "int":
|
|
808
|
+
return node, tuple(helper_binding_stack or ())
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
if not isinstance(node, Identifier) or id(node) in resolving:
|
|
812
|
+
return None
|
|
813
|
+
|
|
814
|
+
binding = None
|
|
815
|
+
if not self._security_identifier_is_global_binding(node):
|
|
816
|
+
binding = self._security_lookup_helper_binding_context(
|
|
817
|
+
node.name, helper_binding_stack
|
|
818
|
+
)
|
|
819
|
+
if binding is not None:
|
|
820
|
+
bound, bound_stack = binding
|
|
821
|
+
if isinstance(bound, str):
|
|
822
|
+
return None
|
|
823
|
+
return self._resolve_security_immutable_input_int(
|
|
824
|
+
bound,
|
|
825
|
+
bound_stack,
|
|
826
|
+
resolving | {id(node)},
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
param_binding = self._security_index_param_callsite_binding(node)
|
|
830
|
+
if param_binding is not None:
|
|
831
|
+
return self._resolve_security_immutable_input_int(
|
|
832
|
+
param_binding,
|
|
833
|
+
(),
|
|
834
|
+
resolving | {id(node)},
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
# Mutable globals have requested-context state of their own and cannot
|
|
838
|
+
# be replaced by their authored initializer without changing Pine
|
|
839
|
+
# semantics. Reject them before following global_expr_map.
|
|
840
|
+
if node.name in getattr(self, "_global_mutable_infos", {}):
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
if not self._security_identifier_is_global_binding(node):
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
847
|
+
if node.name not in global_expr_map:
|
|
848
|
+
return None
|
|
849
|
+
return self._resolve_security_immutable_input_int(
|
|
850
|
+
global_expr_map[node.name],
|
|
851
|
+
(),
|
|
852
|
+
resolving | {id(node)},
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def _compose_security_helper_history_subscript(
|
|
856
|
+
self,
|
|
857
|
+
bound,
|
|
858
|
+
local_index,
|
|
859
|
+
source_node,
|
|
860
|
+
) -> Subscript:
|
|
861
|
+
"""Apply helper-local history to a supported bound bar series.
|
|
862
|
+
|
|
863
|
+
The security runtime currently has native requested-context history
|
|
864
|
+
storage only for direct OHLC/time bar fields. Keep that support fence
|
|
865
|
+
explicit instead of emitting a C++ subscript on a scalar expression
|
|
866
|
+
for bindings such as ``hl2`` or ``ta.ema(...)``.
|
|
867
|
+
"""
|
|
868
|
+
if isinstance(bound, Subscript):
|
|
869
|
+
if not (
|
|
870
|
+
isinstance(bound.object, Identifier)
|
|
871
|
+
and bound.object.name in SECURITY_BAR_FIELDS
|
|
872
|
+
):
|
|
873
|
+
self._codegen_error(
|
|
874
|
+
source_node,
|
|
875
|
+
"request.security helper-parameter history currently requires "
|
|
876
|
+
"a direct OHLC/time bar-series binding",
|
|
877
|
+
)
|
|
878
|
+
bound_idx = self._literal_int_for_security_index(bound.index)
|
|
879
|
+
local_idx = self._literal_int_for_security_index(local_index)
|
|
880
|
+
if bound_idx is not None and local_idx is not None:
|
|
881
|
+
combined_index = NumberLiteral(value=bound_idx + local_idx)
|
|
882
|
+
else:
|
|
883
|
+
combined_index = BinOp(
|
|
884
|
+
left=bound.index,
|
|
885
|
+
op="+",
|
|
886
|
+
right=local_index,
|
|
887
|
+
)
|
|
888
|
+
return Subscript(object=bound.object, index=combined_index)
|
|
889
|
+
if isinstance(bound, Identifier) and bound.name in SECURITY_BAR_FIELDS:
|
|
890
|
+
return Subscript(object=bound, index=local_index)
|
|
891
|
+
self._codegen_error(
|
|
892
|
+
source_node,
|
|
893
|
+
"request.security helper-parameter history currently requires "
|
|
894
|
+
"a direct OHLC/time bar-series binding",
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def _collect_security_ohlc_hist_fields(
|
|
898
|
+
self,
|
|
899
|
+
node,
|
|
900
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
901
|
+
resolving: set[str] | None = None,
|
|
902
|
+
) -> set[str]:
|
|
903
|
+
"""Which requested-context bar fields need completed-bar history.
|
|
904
|
+
|
|
905
|
+
This is the declaration prepass for ``_build_security_expr``. It must
|
|
906
|
+
follow the same helper-argument bindings and offset composition as the
|
|
907
|
+
emitter; otherwise ``f(close)`` plus ``src[1]`` can emit a history read
|
|
908
|
+
without declaring, pushing, or clearing its backing Series.
|
|
909
|
+
"""
|
|
468
910
|
out: set[str] = set()
|
|
911
|
+
if resolving is None:
|
|
912
|
+
resolving = set()
|
|
469
913
|
|
|
470
|
-
def walk(n):
|
|
471
|
-
if n is None:
|
|
914
|
+
def walk(n, bindings) -> None:
|
|
915
|
+
if n is None or isinstance(n, str):
|
|
472
916
|
return
|
|
917
|
+
|
|
918
|
+
if isinstance(n, Identifier):
|
|
919
|
+
binding = None
|
|
920
|
+
if not self._security_identifier_is_global_binding(n):
|
|
921
|
+
binding = self._security_lookup_helper_binding_context(
|
|
922
|
+
n.name, bindings
|
|
923
|
+
)
|
|
924
|
+
if binding is not None:
|
|
925
|
+
bound, bound_stack = binding
|
|
926
|
+
if not isinstance(bound, str):
|
|
927
|
+
key = f"bind:{id(bound)}"
|
|
928
|
+
if key not in resolving:
|
|
929
|
+
resolving.add(key)
|
|
930
|
+
walk(bound, bound_stack)
|
|
931
|
+
resolving.remove(key)
|
|
932
|
+
return
|
|
933
|
+
mutable_info = self._global_mutable_infos.get(n.name)
|
|
934
|
+
if mutable_info is not None and n.name not in resolving:
|
|
935
|
+
resolving.add(n.name)
|
|
936
|
+
for stmt in getattr(mutable_info, "source_stmts", []) or []:
|
|
937
|
+
walk(stmt, bindings)
|
|
938
|
+
resolving.remove(n.name)
|
|
939
|
+
return
|
|
940
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
941
|
+
if (
|
|
942
|
+
self._security_identifier_is_global_binding(n)
|
|
943
|
+
and n.name in global_expr_map
|
|
944
|
+
and n.name not in resolving
|
|
945
|
+
):
|
|
946
|
+
resolving.add(n.name)
|
|
947
|
+
walk(global_expr_map[n.name], ())
|
|
948
|
+
resolving.remove(n.name)
|
|
949
|
+
return
|
|
950
|
+
|
|
473
951
|
if isinstance(n, Subscript) and isinstance(n.object, Identifier):
|
|
952
|
+
binding = None
|
|
953
|
+
if not self._security_identifier_is_global_binding(n.object):
|
|
954
|
+
binding = self._security_lookup_helper_binding_context(
|
|
955
|
+
n.object.name, bindings
|
|
956
|
+
)
|
|
957
|
+
if binding is not None:
|
|
958
|
+
bound, bound_stack = binding
|
|
959
|
+
if not isinstance(bound, str):
|
|
960
|
+
local_index = n.index
|
|
961
|
+
resolved_local_index = self._resolve_security_index_literal(
|
|
962
|
+
n.index, bindings
|
|
963
|
+
)
|
|
964
|
+
if resolved_local_index is not None:
|
|
965
|
+
local_index = NumberLiteral(
|
|
966
|
+
value=resolved_local_index
|
|
967
|
+
)
|
|
968
|
+
walk(
|
|
969
|
+
self._compose_security_helper_history_subscript(
|
|
970
|
+
bound,
|
|
971
|
+
local_index,
|
|
972
|
+
n,
|
|
973
|
+
),
|
|
974
|
+
bound_stack,
|
|
975
|
+
)
|
|
976
|
+
return
|
|
474
977
|
if n.object.name in SECURITY_BAR_FIELDS:
|
|
475
|
-
idx = self.
|
|
476
|
-
#
|
|
477
|
-
# completed
|
|
478
|
-
# in _eval_security_*). Dynamic indices may be >=1 at
|
|
479
|
-
# runtime, so they need the same backing Series.
|
|
978
|
+
idx = self._resolve_security_index_literal(n.index, bindings)
|
|
979
|
+
# field[0] uses the current requested bar; k>=1 reads the
|
|
980
|
+
# completed-bar Series. Dynamic indices need that Series too.
|
|
480
981
|
if idx is None or idx >= 1:
|
|
481
982
|
out.add(n.object.name)
|
|
983
|
+
return
|
|
984
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
985
|
+
if (
|
|
986
|
+
self._security_identifier_is_global_binding(n.object)
|
|
987
|
+
and n.object.name in global_expr_map
|
|
988
|
+
and n.object.name not in resolving
|
|
989
|
+
):
|
|
990
|
+
resolving.add(n.object.name)
|
|
991
|
+
walk(
|
|
992
|
+
Subscript(
|
|
993
|
+
object=global_expr_map[n.object.name],
|
|
994
|
+
index=n.index,
|
|
995
|
+
),
|
|
996
|
+
bindings,
|
|
997
|
+
)
|
|
998
|
+
resolving.remove(n.object.name)
|
|
999
|
+
return
|
|
1000
|
+
|
|
1001
|
+
if isinstance(n, FuncCall) and isinstance(n.callee, Identifier):
|
|
1002
|
+
func_name = n.callee.name
|
|
1003
|
+
if func_name in self._func_names:
|
|
1004
|
+
call_key = f"func:{func_name}"
|
|
1005
|
+
if call_key in resolving:
|
|
1006
|
+
return
|
|
1007
|
+
resolving.add(call_key)
|
|
1008
|
+
plan = self._security_helper_call_plan(n, bindings)
|
|
1009
|
+
if plan["mode"] == "expr":
|
|
1010
|
+
walk(plan["expr"], plan["binding_stack"])
|
|
1011
|
+
else:
|
|
1012
|
+
local_series_names = set(plan.get("local_series_names", ()))
|
|
1013
|
+
active: dict[str, object] = {}
|
|
1014
|
+
|
|
1015
|
+
def walk_stmt(stmt, current: dict[str, object]) -> None:
|
|
1016
|
+
local_stack = plan["binding_stack"] + (current,)
|
|
1017
|
+
if isinstance(stmt, VarDecl):
|
|
1018
|
+
if stmt.value is not None:
|
|
1019
|
+
walk(stmt.value, local_stack)
|
|
1020
|
+
if stmt.name in local_series_names or stmt.is_var:
|
|
1021
|
+
current[stmt.name] = self._security_series_binding(
|
|
1022
|
+
f"{plan['func_info'].name}:{stmt.name}"
|
|
1023
|
+
)
|
|
1024
|
+
elif stmt.value is not None:
|
|
1025
|
+
current[stmt.name] = stmt.value
|
|
1026
|
+
return
|
|
1027
|
+
if isinstance(stmt, Assignment):
|
|
1028
|
+
if stmt.value is not None:
|
|
1029
|
+
walk(stmt.value, local_stack)
|
|
1030
|
+
target = self._get_target_name(stmt.target)
|
|
1031
|
+
existing = current.get(target) if target is not None else None
|
|
1032
|
+
if (
|
|
1033
|
+
target in local_series_names
|
|
1034
|
+
or (
|
|
1035
|
+
isinstance(existing, str)
|
|
1036
|
+
and self._security_series_binding_target(existing)
|
|
1037
|
+
is not None
|
|
1038
|
+
)
|
|
1039
|
+
):
|
|
1040
|
+
current[target] = self._security_series_binding(
|
|
1041
|
+
f"{plan['func_info'].name}:{target}"
|
|
1042
|
+
)
|
|
1043
|
+
elif target is not None and stmt.value is not None:
|
|
1044
|
+
current[target] = stmt.value
|
|
1045
|
+
return
|
|
1046
|
+
if isinstance(stmt, IfStmt):
|
|
1047
|
+
walk(stmt.condition, local_stack)
|
|
1048
|
+
body_bindings = dict(current)
|
|
1049
|
+
for child in stmt.body:
|
|
1050
|
+
walk_stmt(child, body_bindings)
|
|
1051
|
+
else_bindings = dict(current)
|
|
1052
|
+
for child in stmt.else_body:
|
|
1053
|
+
walk_stmt(child, else_bindings)
|
|
1054
|
+
|
|
1055
|
+
for stmt in plan["body"]:
|
|
1056
|
+
walk_stmt(stmt, active)
|
|
1057
|
+
walk(
|
|
1058
|
+
plan["expr"],
|
|
1059
|
+
plan["binding_stack"] + (active,),
|
|
1060
|
+
)
|
|
1061
|
+
resolving.remove(call_key)
|
|
1062
|
+
return
|
|
1063
|
+
|
|
482
1064
|
if isinstance(n, (list, tuple)):
|
|
483
|
-
for
|
|
484
|
-
walk(
|
|
1065
|
+
for child in n:
|
|
1066
|
+
walk(child, bindings)
|
|
485
1067
|
return
|
|
486
|
-
for
|
|
487
|
-
if isinstance(
|
|
488
|
-
walk(
|
|
489
|
-
elif isinstance(
|
|
490
|
-
for
|
|
491
|
-
if isinstance(
|
|
492
|
-
walk(
|
|
493
|
-
|
|
494
|
-
walk(node)
|
|
1068
|
+
for value in getattr(n, "__dict__", {}).values():
|
|
1069
|
+
if isinstance(value, ASTNode):
|
|
1070
|
+
walk(value, bindings)
|
|
1071
|
+
elif isinstance(value, (list, tuple)):
|
|
1072
|
+
for child in value:
|
|
1073
|
+
if isinstance(child, ASTNode):
|
|
1074
|
+
walk(child, bindings)
|
|
1075
|
+
|
|
1076
|
+
walk(node, helper_binding_stack or ())
|
|
495
1077
|
return out
|
|
496
1078
|
|
|
497
1079
|
def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
|
|
@@ -516,10 +1098,43 @@ class SecurityEmitter:
|
|
|
516
1098
|
return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
|
|
517
1099
|
|
|
518
1100
|
@staticmethod
|
|
519
|
-
def _security_tuple_result_default(
|
|
520
|
-
|
|
1101
|
+
def _security_tuple_result_default(
|
|
1102
|
+
cpp_type: str,
|
|
1103
|
+
tuple_size: int,
|
|
1104
|
+
tuple_element_types: tuple[PineType, ...] = (),
|
|
1105
|
+
) -> str:
|
|
1106
|
+
is_bool_tuple = (
|
|
1107
|
+
len(tuple_element_types) == tuple_size
|
|
1108
|
+
and tuple_size > 0
|
|
1109
|
+
and all(item == PineType.BOOL for item in tuple_element_types)
|
|
1110
|
+
)
|
|
1111
|
+
default_value = "false" if is_bool_tuple else "na<double>()"
|
|
1112
|
+
vals = ", ".join(
|
|
1113
|
+
default_value for _ in range(max(0, tuple_size))
|
|
1114
|
+
)
|
|
521
1115
|
return f"{cpp_type}{{{vals}}}"
|
|
522
1116
|
|
|
1117
|
+
@staticmethod
|
|
1118
|
+
def _security_helper_tuple_cpp_type(
|
|
1119
|
+
tuple_size: int,
|
|
1120
|
+
tuple_element_types: tuple[PineType, ...] = (),
|
|
1121
|
+
) -> str:
|
|
1122
|
+
"""C++ storage type for a supported helper tuple of arbitrary arity.
|
|
1123
|
+
|
|
1124
|
+
Numeric int/float families retain the established double-coercing
|
|
1125
|
+
representation. Homogeneous bool families use real ``bool`` fields so
|
|
1126
|
+
true/false semantics survive the requested-context boundary.
|
|
1127
|
+
"""
|
|
1128
|
+
is_bool_tuple = (
|
|
1129
|
+
len(tuple_element_types) == tuple_size
|
|
1130
|
+
and tuple_size > 0
|
|
1131
|
+
and all(item == PineType.BOOL for item in tuple_element_types)
|
|
1132
|
+
)
|
|
1133
|
+
element_cpp_type = "bool" if is_bool_tuple else "double"
|
|
1134
|
+
return "std::tuple<" + ", ".join(
|
|
1135
|
+
element_cpp_type for _ in range(max(0, tuple_size))
|
|
1136
|
+
) + ">"
|
|
1137
|
+
|
|
523
1138
|
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
524
1139
|
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
525
1140
|
|
|
@@ -529,50 +1144,172 @@ class SecurityEmitter:
|
|
|
529
1144
|
per-site ``Series`` filled (gated on ``is_complete``) in
|
|
530
1145
|
``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
|
|
531
1146
|
OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
|
|
532
|
-
and needs no Series, so only index >= 1 registers here.
|
|
1147
|
+
and needs no Series, so only index >= 1 registers here. A dynamic
|
|
1148
|
+
index is registered conservatively; the expression emitter separately
|
|
1149
|
+
admits only an immutable ``input.int`` chain and rejects everything
|
|
1150
|
+
else. This declaration prepass must expand helper/global bindings just
|
|
1151
|
+
like ``_build_security_expr`` or a valid helper-wrapped offset could
|
|
1152
|
+
emit a history read without storage, pushes, or reset."""
|
|
533
1153
|
out: set[int] = set()
|
|
534
1154
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1155
|
+
resolving: set[str] = set()
|
|
535
1156
|
|
|
536
|
-
def resolve_ta_site(obj,
|
|
1157
|
+
def resolve_ta_site(obj, bindings, seen: set[str] | None = None):
|
|
537
1158
|
"""_get_ta_site only matches the literal ta.* FuncCall node by
|
|
538
|
-
identity; fall back through
|
|
539
|
-
binding (``v = ta.ema(close, 55)`` then ``...v[1]...``)
|
|
540
|
-
the same fallback in _build_security_expr's Subscript branch."""
|
|
1159
|
+
identity; fall back through helper/global bindings for an indirect
|
|
1160
|
+
binding (``v = ta.ema(close, 55)`` then ``...v[1]...``)."""
|
|
541
1161
|
site = self._get_ta_site(obj)
|
|
542
1162
|
if site is not None:
|
|
543
1163
|
return site
|
|
544
1164
|
if isinstance(obj, Identifier):
|
|
545
|
-
if
|
|
546
|
-
|
|
547
|
-
if obj.name in
|
|
548
|
-
|
|
549
|
-
|
|
1165
|
+
if seen is None:
|
|
1166
|
+
seen = set()
|
|
1167
|
+
if obj.name in seen:
|
|
1168
|
+
return None
|
|
1169
|
+
binding = None
|
|
1170
|
+
if not self._security_identifier_is_global_binding(obj):
|
|
1171
|
+
binding = self._security_lookup_helper_binding_context(
|
|
1172
|
+
obj.name, bindings
|
|
1173
|
+
)
|
|
1174
|
+
if binding is not None:
|
|
1175
|
+
bound, bound_stack = binding
|
|
1176
|
+
if not isinstance(bound, str):
|
|
1177
|
+
return resolve_ta_site(
|
|
1178
|
+
bound, bound_stack, seen | {obj.name}
|
|
1179
|
+
)
|
|
1180
|
+
if (
|
|
1181
|
+
self._security_identifier_is_global_binding(obj)
|
|
1182
|
+
and obj.name in global_expr_map
|
|
1183
|
+
):
|
|
1184
|
+
return resolve_ta_site(
|
|
1185
|
+
global_expr_map[obj.name],
|
|
1186
|
+
(),
|
|
1187
|
+
seen | {obj.name},
|
|
1188
|
+
)
|
|
550
1189
|
return None
|
|
551
1190
|
|
|
552
|
-
def walk(n):
|
|
553
|
-
if n is None:
|
|
1191
|
+
def walk(n, bindings) -> None:
|
|
1192
|
+
if n is None or isinstance(n, str):
|
|
554
1193
|
return
|
|
1194
|
+
|
|
1195
|
+
if isinstance(n, Identifier):
|
|
1196
|
+
binding = None
|
|
1197
|
+
if not self._security_identifier_is_global_binding(n):
|
|
1198
|
+
binding = self._security_lookup_helper_binding_context(
|
|
1199
|
+
n.name, bindings
|
|
1200
|
+
)
|
|
1201
|
+
if binding is not None:
|
|
1202
|
+
bound, bound_stack = binding
|
|
1203
|
+
if not isinstance(bound, str):
|
|
1204
|
+
key = f"bind:{id(bound)}"
|
|
1205
|
+
if key not in resolving:
|
|
1206
|
+
resolving.add(key)
|
|
1207
|
+
walk(bound, bound_stack)
|
|
1208
|
+
resolving.remove(key)
|
|
1209
|
+
return
|
|
1210
|
+
mutable_info = self._global_mutable_infos.get(n.name)
|
|
1211
|
+
if mutable_info is not None and n.name not in resolving:
|
|
1212
|
+
resolving.add(n.name)
|
|
1213
|
+
for stmt in getattr(mutable_info, "source_stmts", []) or []:
|
|
1214
|
+
walk(stmt, bindings)
|
|
1215
|
+
resolving.remove(n.name)
|
|
1216
|
+
return
|
|
1217
|
+
if (
|
|
1218
|
+
self._security_identifier_is_global_binding(n)
|
|
1219
|
+
and n.name in global_expr_map
|
|
1220
|
+
and n.name not in resolving
|
|
1221
|
+
):
|
|
1222
|
+
resolving.add(n.name)
|
|
1223
|
+
walk(global_expr_map[n.name], ())
|
|
1224
|
+
resolving.remove(n.name)
|
|
1225
|
+
return
|
|
1226
|
+
|
|
555
1227
|
if isinstance(n, Subscript):
|
|
556
|
-
site = resolve_ta_site(n.object)
|
|
1228
|
+
site = resolve_ta_site(n.object, bindings)
|
|
557
1229
|
if site is not None:
|
|
558
|
-
idx_lit = self._resolve_security_index_literal(n.index)
|
|
559
|
-
if idx_lit is
|
|
1230
|
+
idx_lit = self._resolve_security_index_literal(n.index, bindings)
|
|
1231
|
+
if idx_lit is None or idx_lit >= 1:
|
|
560
1232
|
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
561
1233
|
if site_idx is not None:
|
|
562
1234
|
out.add(site_idx)
|
|
1235
|
+
|
|
1236
|
+
if isinstance(n, FuncCall) and isinstance(n.callee, Identifier):
|
|
1237
|
+
func_name = n.callee.name
|
|
1238
|
+
if func_name in self._func_names:
|
|
1239
|
+
call_key = f"func:{func_name}"
|
|
1240
|
+
if call_key in resolving:
|
|
1241
|
+
return
|
|
1242
|
+
resolving.add(call_key)
|
|
1243
|
+
plan = self._security_helper_call_plan(n, bindings)
|
|
1244
|
+
if plan["mode"] == "expr":
|
|
1245
|
+
walk(plan["expr"], plan["binding_stack"])
|
|
1246
|
+
else:
|
|
1247
|
+
local_series_names = set(
|
|
1248
|
+
plan.get("local_series_names", ())
|
|
1249
|
+
)
|
|
1250
|
+
active: dict[str, object] = {}
|
|
1251
|
+
|
|
1252
|
+
def walk_stmt(stmt, current: dict[str, object]) -> None:
|
|
1253
|
+
local_stack = plan["binding_stack"] + (current,)
|
|
1254
|
+
if isinstance(stmt, VarDecl):
|
|
1255
|
+
if stmt.value is not None:
|
|
1256
|
+
walk(stmt.value, local_stack)
|
|
1257
|
+
if stmt.name in local_series_names or stmt.is_var:
|
|
1258
|
+
current[stmt.name] = self._security_series_binding(
|
|
1259
|
+
f"{plan['func_info'].name}:{stmt.name}"
|
|
1260
|
+
)
|
|
1261
|
+
elif stmt.value is not None:
|
|
1262
|
+
current[stmt.name] = stmt.value
|
|
1263
|
+
return
|
|
1264
|
+
if isinstance(stmt, Assignment):
|
|
1265
|
+
if stmt.value is not None:
|
|
1266
|
+
walk(stmt.value, local_stack)
|
|
1267
|
+
target = self._get_target_name(stmt.target)
|
|
1268
|
+
existing = current.get(target) if target else None
|
|
1269
|
+
if (
|
|
1270
|
+
target in local_series_names
|
|
1271
|
+
or (
|
|
1272
|
+
isinstance(existing, str)
|
|
1273
|
+
and self._security_series_binding_target(
|
|
1274
|
+
existing
|
|
1275
|
+
)
|
|
1276
|
+
is not None
|
|
1277
|
+
)
|
|
1278
|
+
):
|
|
1279
|
+
current[target] = self._security_series_binding(
|
|
1280
|
+
f"{plan['func_info'].name}:{target}"
|
|
1281
|
+
)
|
|
1282
|
+
elif target is not None and stmt.value is not None:
|
|
1283
|
+
current[target] = stmt.value
|
|
1284
|
+
return
|
|
1285
|
+
if isinstance(stmt, IfStmt):
|
|
1286
|
+
walk(stmt.condition, local_stack)
|
|
1287
|
+
body_bindings = dict(current)
|
|
1288
|
+
for child in stmt.body:
|
|
1289
|
+
walk_stmt(child, body_bindings)
|
|
1290
|
+
else_bindings = dict(current)
|
|
1291
|
+
for child in stmt.else_body:
|
|
1292
|
+
walk_stmt(child, else_bindings)
|
|
1293
|
+
|
|
1294
|
+
for stmt in plan["body"]:
|
|
1295
|
+
walk_stmt(stmt, active)
|
|
1296
|
+
walk(plan["expr"], plan["binding_stack"] + (active,))
|
|
1297
|
+
resolving.remove(call_key)
|
|
1298
|
+
return
|
|
1299
|
+
|
|
563
1300
|
if isinstance(n, (list, tuple)):
|
|
564
1301
|
for x in n:
|
|
565
|
-
walk(x)
|
|
1302
|
+
walk(x, bindings)
|
|
566
1303
|
return
|
|
567
1304
|
for _k, v in getattr(n, "__dict__", {}).items():
|
|
568
1305
|
if isinstance(v, ASTNode):
|
|
569
|
-
walk(v)
|
|
1306
|
+
walk(v, bindings)
|
|
570
1307
|
elif isinstance(v, (list, tuple)):
|
|
571
1308
|
for x in v:
|
|
572
1309
|
if isinstance(x, ASTNode):
|
|
573
|
-
walk(x)
|
|
1310
|
+
walk(x, bindings)
|
|
574
1311
|
|
|
575
|
-
walk(node)
|
|
1312
|
+
walk(node, ())
|
|
576
1313
|
return out
|
|
577
1314
|
|
|
578
1315
|
def _security_ta_hist_series_cpp(self, member_name: str) -> str:
|
|
@@ -773,6 +1510,49 @@ class SecurityEmitter:
|
|
|
773
1510
|
security_mutable_names: set[str],
|
|
774
1511
|
lines: list[str],
|
|
775
1512
|
resolving: set[str] | None = None,
|
|
1513
|
+
) -> str:
|
|
1514
|
+
"""Inline one linear helper against isolated lexical collection state."""
|
|
1515
|
+
saved = (
|
|
1516
|
+
self._current_func_collection_specs,
|
|
1517
|
+
self._current_func_collection_shadows,
|
|
1518
|
+
self._collection_types,
|
|
1519
|
+
self._array_vars,
|
|
1520
|
+
self._map_vars,
|
|
1521
|
+
self._matrix_specs,
|
|
1522
|
+
)
|
|
1523
|
+
self._current_func_collection_specs = {}
|
|
1524
|
+
self._current_func_collection_shadows = set()
|
|
1525
|
+
self._collection_types = dict(self._collection_types)
|
|
1526
|
+
self._array_vars = set(self._array_vars)
|
|
1527
|
+
self._map_vars = set(self._map_vars)
|
|
1528
|
+
self._matrix_specs = dict(self._matrix_specs)
|
|
1529
|
+
try:
|
|
1530
|
+
return self._emit_security_linear_helper_call_scoped(
|
|
1531
|
+
sec_id,
|
|
1532
|
+
plan,
|
|
1533
|
+
ta_results,
|
|
1534
|
+
security_mutable_names,
|
|
1535
|
+
lines,
|
|
1536
|
+
resolving,
|
|
1537
|
+
)
|
|
1538
|
+
finally:
|
|
1539
|
+
(
|
|
1540
|
+
self._current_func_collection_specs,
|
|
1541
|
+
self._current_func_collection_shadows,
|
|
1542
|
+
self._collection_types,
|
|
1543
|
+
self._array_vars,
|
|
1544
|
+
self._map_vars,
|
|
1545
|
+
self._matrix_specs,
|
|
1546
|
+
) = saved
|
|
1547
|
+
|
|
1548
|
+
def _emit_security_linear_helper_call_scoped(
|
|
1549
|
+
self,
|
|
1550
|
+
sec_id: int,
|
|
1551
|
+
plan: dict,
|
|
1552
|
+
ta_results: dict,
|
|
1553
|
+
security_mutable_names: set[str],
|
|
1554
|
+
lines: list[str],
|
|
1555
|
+
resolving: set[str] | None = None,
|
|
776
1556
|
) -> str:
|
|
777
1557
|
if plan["mode"] != "linear":
|
|
778
1558
|
self._codegen_error(
|
|
@@ -790,8 +1570,18 @@ class SecurityEmitter:
|
|
|
790
1570
|
pad = " " * indent
|
|
791
1571
|
runtime_stack_local = plan["binding_stack"] + (active_bindings,)
|
|
792
1572
|
|
|
1573
|
+
def activate_decl() -> None:
|
|
1574
|
+
spec = self._callable_collection_bindings.get(id(stmt))
|
|
1575
|
+
if spec is None:
|
|
1576
|
+
inferred = self._type_spec_from_expr(stmt.value)
|
|
1577
|
+
if (inferred is not None
|
|
1578
|
+
and inferred.kind in {"array", "map", "matrix"}):
|
|
1579
|
+
spec = inferred
|
|
1580
|
+
self._activate_callable_collection_binding(stmt.name, spec)
|
|
1581
|
+
|
|
793
1582
|
if isinstance(stmt, VarDecl):
|
|
794
|
-
|
|
1583
|
+
is_persistent_var = stmt.is_var
|
|
1584
|
+
if stmt.name in local_series_names or is_persistent_var:
|
|
795
1585
|
binding = active_bindings.get(stmt.name)
|
|
796
1586
|
if binding is None:
|
|
797
1587
|
binding = self._security_series_binding(
|
|
@@ -810,13 +1600,53 @@ class SecurityEmitter:
|
|
|
810
1600
|
lines,
|
|
811
1601
|
)
|
|
812
1602
|
active_bindings[stmt.name] = binding
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1603
|
+
if is_persistent_var:
|
|
1604
|
+
cpp_type = self._type_for_decl(stmt)
|
|
1605
|
+
if cpp_type not in {"double", "int", "bool"}:
|
|
1606
|
+
self._codegen_error(
|
|
1607
|
+
stmt,
|
|
1608
|
+
"request.security helper-local var state currently supports only int, float, and bool values",
|
|
1609
|
+
hint="Hoist collection, string, UDT, or drawing state outside request.security().",
|
|
1610
|
+
)
|
|
1611
|
+
# A Pine ``var`` initializer runs once per helper call
|
|
1612
|
+
# site in the requested context. On a new requested
|
|
1613
|
+
# bar the prior committed value is carried forward; on
|
|
1614
|
+
# a lookahead/realtime recomputation of the current
|
|
1615
|
+
# requested bar it rolls back before the helper body is
|
|
1616
|
+
# evaluated again. ``Series[1]`` is that rollback
|
|
1617
|
+
# value after the first bar. The companion seed entry
|
|
1618
|
+
# preserves the once-only initializer for first-bar
|
|
1619
|
+
# recomputations without adding another generated
|
|
1620
|
+
# member/state family.
|
|
1621
|
+
seed_name = f"{series_name}@var_seed"
|
|
1622
|
+
lines.append(f'{pad}if (_security_helper_series_["{series_name}"].size() == 0) {{')
|
|
1623
|
+
lines.append(f'{pad} _security_helper_series_["{seed_name}"].push({expr_cpp});')
|
|
1624
|
+
lines.append(
|
|
1625
|
+
f'{pad} _security_helper_series_["{series_name}"].push('
|
|
1626
|
+
f'_security_helper_series_["{seed_name}"][0]);'
|
|
1627
|
+
)
|
|
1628
|
+
lines.append(f'{pad}}} else if (security_series_slot_is_new({sec_id})) {{')
|
|
1629
|
+
lines.append(
|
|
1630
|
+
f'{pad} _security_helper_series_["{series_name}"].push('
|
|
1631
|
+
f'_security_helper_series_["{series_name}"][0]);'
|
|
1632
|
+
)
|
|
1633
|
+
lines.append(f'{pad}}} else {{')
|
|
1634
|
+
lines.append(
|
|
1635
|
+
f'{pad} _security_helper_series_["{series_name}"].update('
|
|
1636
|
+
f'_security_helper_series_["{series_name}"].size() > 1 '
|
|
1637
|
+
f'? _security_helper_series_["{series_name}"][1] '
|
|
1638
|
+
f': _security_helper_series_["{seed_name}"][0]);'
|
|
1639
|
+
)
|
|
1640
|
+
lines.append(f'{pad}}}')
|
|
1641
|
+
else:
|
|
1642
|
+
lines.append(f'{pad}if (_security_helper_series_["{series_name}"].size() == 0) {{')
|
|
1643
|
+
lines.append(f'{pad} _security_helper_series_["{series_name}"].push({expr_cpp});')
|
|
1644
|
+
lines.append(f'{pad}}} else if (security_series_slot_is_new({sec_id})) {{')
|
|
1645
|
+
lines.append(f'{pad} _security_helper_series_["{series_name}"].push({expr_cpp});')
|
|
1646
|
+
lines.append(f'{pad}}} else {{')
|
|
1647
|
+
lines.append(f'{pad} _security_helper_series_["{series_name}"].update({expr_cpp});')
|
|
1648
|
+
lines.append(f'{pad}}}')
|
|
1649
|
+
activate_decl()
|
|
820
1650
|
return
|
|
821
1651
|
|
|
822
1652
|
local_name = active_bindings.get(stmt.name)
|
|
@@ -851,6 +1681,7 @@ class SecurityEmitter:
|
|
|
851
1681
|
lines,
|
|
852
1682
|
)
|
|
853
1683
|
lines.append(f"{pad}{local_name} = {expr_cpp};")
|
|
1684
|
+
activate_decl()
|
|
854
1685
|
return
|
|
855
1686
|
|
|
856
1687
|
if isinstance(stmt, Assignment):
|
|
@@ -907,13 +1738,69 @@ class SecurityEmitter:
|
|
|
907
1738
|
)
|
|
908
1739
|
lines.append(f"{pad}if ({cond_cpp}) {{")
|
|
909
1740
|
body_bindings = dict(active_bindings)
|
|
910
|
-
|
|
911
|
-
|
|
1741
|
+
body_state = (
|
|
1742
|
+
self._current_func_collection_specs,
|
|
1743
|
+
self._current_func_collection_shadows,
|
|
1744
|
+
self._collection_types,
|
|
1745
|
+
self._array_vars,
|
|
1746
|
+
self._map_vars,
|
|
1747
|
+
self._matrix_specs,
|
|
1748
|
+
)
|
|
1749
|
+
self._current_func_collection_specs = dict(
|
|
1750
|
+
self._current_func_collection_specs
|
|
1751
|
+
)
|
|
1752
|
+
self._current_func_collection_shadows = set(
|
|
1753
|
+
self._current_func_collection_shadows
|
|
1754
|
+
)
|
|
1755
|
+
self._collection_types = dict(self._collection_types)
|
|
1756
|
+
self._array_vars = set(self._array_vars)
|
|
1757
|
+
self._map_vars = set(self._map_vars)
|
|
1758
|
+
self._matrix_specs = dict(self._matrix_specs)
|
|
1759
|
+
try:
|
|
1760
|
+
for child in stmt.body:
|
|
1761
|
+
emit_stmt(child, body_bindings, indent + 1)
|
|
1762
|
+
finally:
|
|
1763
|
+
(
|
|
1764
|
+
self._current_func_collection_specs,
|
|
1765
|
+
self._current_func_collection_shadows,
|
|
1766
|
+
self._collection_types,
|
|
1767
|
+
self._array_vars,
|
|
1768
|
+
self._map_vars,
|
|
1769
|
+
self._matrix_specs,
|
|
1770
|
+
) = body_state
|
|
912
1771
|
if stmt.else_body:
|
|
913
1772
|
lines.append(f"{pad}}} else {{")
|
|
914
1773
|
else_bindings = dict(active_bindings)
|
|
915
|
-
|
|
916
|
-
|
|
1774
|
+
else_state = (
|
|
1775
|
+
self._current_func_collection_specs,
|
|
1776
|
+
self._current_func_collection_shadows,
|
|
1777
|
+
self._collection_types,
|
|
1778
|
+
self._array_vars,
|
|
1779
|
+
self._map_vars,
|
|
1780
|
+
self._matrix_specs,
|
|
1781
|
+
)
|
|
1782
|
+
self._current_func_collection_specs = dict(
|
|
1783
|
+
self._current_func_collection_specs
|
|
1784
|
+
)
|
|
1785
|
+
self._current_func_collection_shadows = set(
|
|
1786
|
+
self._current_func_collection_shadows
|
|
1787
|
+
)
|
|
1788
|
+
self._collection_types = dict(self._collection_types)
|
|
1789
|
+
self._array_vars = set(self._array_vars)
|
|
1790
|
+
self._map_vars = set(self._map_vars)
|
|
1791
|
+
self._matrix_specs = dict(self._matrix_specs)
|
|
1792
|
+
try:
|
|
1793
|
+
for child in stmt.else_body:
|
|
1794
|
+
emit_stmt(child, else_bindings, indent + 1)
|
|
1795
|
+
finally:
|
|
1796
|
+
(
|
|
1797
|
+
self._current_func_collection_specs,
|
|
1798
|
+
self._current_func_collection_shadows,
|
|
1799
|
+
self._collection_types,
|
|
1800
|
+
self._array_vars,
|
|
1801
|
+
self._map_vars,
|
|
1802
|
+
self._matrix_specs,
|
|
1803
|
+
) = else_state
|
|
917
1804
|
lines.append(f"{pad}}}")
|
|
918
1805
|
return
|
|
919
1806
|
|
|
@@ -949,7 +1836,7 @@ class SecurityEmitter:
|
|
|
949
1836
|
|
|
950
1837
|
sig_frames = []
|
|
951
1838
|
for idx, frame in enumerate(helper_binding_stack):
|
|
952
|
-
if idx == 0:
|
|
1839
|
+
if idx == 0 or isinstance(frame, _SecurityHelperArgumentFrame):
|
|
953
1840
|
sig_frames.append(
|
|
954
1841
|
tuple((name, _sig_value(node)) for name, node in sorted(frame.items()))
|
|
955
1842
|
)
|
|
@@ -1002,8 +1889,14 @@ class SecurityEmitter:
|
|
|
1002
1889
|
"request.security helper calls must bind every parameter explicitly",
|
|
1003
1890
|
)
|
|
1004
1891
|
|
|
1005
|
-
new_frame = {param_name: bound_args[idx] for idx, param_name in enumerate(params)}
|
|
1006
1892
|
base_stack = helper_binding_stack or ()
|
|
1893
|
+
new_frame = _SecurityHelperArgumentFrame(
|
|
1894
|
+
{
|
|
1895
|
+
param_name: bound_args[idx]
|
|
1896
|
+
for idx, param_name in enumerate(params)
|
|
1897
|
+
},
|
|
1898
|
+
base_stack,
|
|
1899
|
+
)
|
|
1007
1900
|
return fi, base_stack + (new_frame,)
|
|
1008
1901
|
|
|
1009
1902
|
def _security_helper_call_plan(
|
|
@@ -1075,11 +1968,11 @@ class SecurityEmitter:
|
|
|
1075
1968
|
"request.security multi-statement helpers may only use local declarations, assignments, and if-branches before the final expression",
|
|
1076
1969
|
)
|
|
1077
1970
|
if isinstance(stmt, VarDecl):
|
|
1078
|
-
if stmt.
|
|
1971
|
+
if stmt.is_varip:
|
|
1079
1972
|
self._codegen_error(
|
|
1080
1973
|
node,
|
|
1081
|
-
"request.security does not support
|
|
1082
|
-
hint="
|
|
1974
|
+
"request.security does not support helper-local varip state",
|
|
1975
|
+
hint="Use var for requested-context bar state; varip tick state is unavailable in batch backtests.",
|
|
1083
1976
|
)
|
|
1084
1977
|
if isinstance(stmt, Assignment):
|
|
1085
1978
|
target_name = self._get_target_name(stmt.target)
|
|
@@ -1099,6 +1992,92 @@ class SecurityEmitter:
|
|
|
1099
1992
|
"local_series_names": local_series_names,
|
|
1100
1993
|
}
|
|
1101
1994
|
|
|
1995
|
+
def _validate_security_persistent_var_control_flow(self, expr_node) -> None:
|
|
1996
|
+
"""Reject helper ``var`` state whose rollback would be conditional.
|
|
1997
|
+
|
|
1998
|
+
Persistent helper state is restored at its declaration before the body
|
|
1999
|
+
mutates it. That is safe only when the declaration executes on every
|
|
2000
|
+
requested-bar evaluation. Until rollback is hoisted to evaluator entry,
|
|
2001
|
+
reject declarations reached through ``if``/``?:``/short-circuit paths,
|
|
2002
|
+
including state owned by a nested helper called from such a path.
|
|
2003
|
+
"""
|
|
2004
|
+
call_stack: set[str] = set()
|
|
2005
|
+
|
|
2006
|
+
def visit_expr(node, conditional: bool) -> None:
|
|
2007
|
+
if node is None:
|
|
2008
|
+
return
|
|
2009
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier):
|
|
2010
|
+
name = node.callee.name
|
|
2011
|
+
if name in self._func_names:
|
|
2012
|
+
if name in call_stack:
|
|
2013
|
+
return
|
|
2014
|
+
fi = self._func_info_map.get(name)
|
|
2015
|
+
if fi is not None and fi.node is not None:
|
|
2016
|
+
call_stack.add(name)
|
|
2017
|
+
for stmt in fi.node.body:
|
|
2018
|
+
visit_stmt(stmt, conditional)
|
|
2019
|
+
call_stack.remove(name)
|
|
2020
|
+
for arg in node.args:
|
|
2021
|
+
visit_expr(arg, conditional)
|
|
2022
|
+
for value in node.kwargs.values():
|
|
2023
|
+
if isinstance(value, ASTNode):
|
|
2024
|
+
visit_expr(value, conditional)
|
|
2025
|
+
return
|
|
2026
|
+
if isinstance(node, Ternary):
|
|
2027
|
+
visit_expr(node.condition, conditional)
|
|
2028
|
+
visit_expr(node.true_val, True)
|
|
2029
|
+
visit_expr(node.false_val, True)
|
|
2030
|
+
return
|
|
2031
|
+
if isinstance(node, BinOp) and node.op in ("and", "or"):
|
|
2032
|
+
visit_expr(node.left, conditional)
|
|
2033
|
+
visit_expr(node.right, True)
|
|
2034
|
+
return
|
|
2035
|
+
for value in getattr(node, "__dict__", {}).values():
|
|
2036
|
+
if isinstance(value, ASTNode):
|
|
2037
|
+
visit_expr(value, conditional)
|
|
2038
|
+
elif isinstance(value, (list, tuple)):
|
|
2039
|
+
for child in value:
|
|
2040
|
+
if isinstance(child, ASTNode):
|
|
2041
|
+
visit_expr(child, conditional)
|
|
2042
|
+
|
|
2043
|
+
def visit_stmt(stmt, conditional: bool) -> None:
|
|
2044
|
+
if isinstance(stmt, VarDecl):
|
|
2045
|
+
if stmt.is_var and conditional:
|
|
2046
|
+
self._codegen_error(
|
|
2047
|
+
stmt,
|
|
2048
|
+
"request.security helper-local var declarations inside "
|
|
2049
|
+
"conditional control flow are not supported",
|
|
2050
|
+
hint="Declare persistent requested-context state at the helper's top level.",
|
|
2051
|
+
)
|
|
2052
|
+
# A persistent initializer runs only when its requested-context
|
|
2053
|
+
# slot is first created. Any nested persistent helper reached
|
|
2054
|
+
# from that initializer would therefore need its rollback and
|
|
2055
|
+
# mutation to remain inside the same once-only guard. The
|
|
2056
|
+
# current linear emitter hoists nested helper statements ahead
|
|
2057
|
+
# of that guard, so treat the initializer as conditional and
|
|
2058
|
+
# fail closed until it can preserve that execution boundary.
|
|
2059
|
+
visit_expr(stmt.value, conditional or stmt.is_var)
|
|
2060
|
+
return
|
|
2061
|
+
if isinstance(stmt, IfStmt):
|
|
2062
|
+
visit_expr(stmt.condition, conditional)
|
|
2063
|
+
for child in stmt.body:
|
|
2064
|
+
visit_stmt(child, True)
|
|
2065
|
+
for child in stmt.else_body:
|
|
2066
|
+
visit_stmt(child, True)
|
|
2067
|
+
return
|
|
2068
|
+
if isinstance(stmt, ExprStmt):
|
|
2069
|
+
visit_expr(stmt.expr, conditional)
|
|
2070
|
+
return
|
|
2071
|
+
for value in getattr(stmt, "__dict__", {}).values():
|
|
2072
|
+
if isinstance(value, ASTNode):
|
|
2073
|
+
visit_expr(value, conditional)
|
|
2074
|
+
elif isinstance(value, (list, tuple)):
|
|
2075
|
+
for child in value:
|
|
2076
|
+
if isinstance(child, ASTNode):
|
|
2077
|
+
visit_expr(child, conditional)
|
|
2078
|
+
|
|
2079
|
+
visit_expr(expr_node, False)
|
|
2080
|
+
|
|
1102
2081
|
def _security_next_inline_name(self, sec_id: int, func_name: str, base_name: str) -> str:
|
|
1103
2082
|
self._security_inline_counter += 1
|
|
1104
2083
|
return (
|
|
@@ -1119,13 +2098,18 @@ class SecurityEmitter:
|
|
|
1119
2098
|
resolving = set()
|
|
1120
2099
|
|
|
1121
2100
|
if isinstance(expr_node, Identifier):
|
|
1122
|
-
|
|
1123
|
-
if
|
|
2101
|
+
binding = None
|
|
2102
|
+
if not self._security_identifier_is_global_binding(expr_node):
|
|
2103
|
+
binding = self._security_lookup_helper_binding_context(
|
|
2104
|
+
expr_node.name, helper_binding_stack
|
|
2105
|
+
)
|
|
2106
|
+
if binding is not None:
|
|
2107
|
+
bound, bound_stack = binding
|
|
1124
2108
|
return self._expr_depends_on_security_mutables(
|
|
1125
2109
|
bound,
|
|
1126
2110
|
security_mutable_names,
|
|
1127
2111
|
resolving,
|
|
1128
|
-
|
|
2112
|
+
bound_stack,
|
|
1129
2113
|
)
|
|
1130
2114
|
if expr_node.name in security_mutable_names:
|
|
1131
2115
|
return True
|
|
@@ -1247,16 +2231,180 @@ class SecurityEmitter:
|
|
|
1247
2231
|
if idx < len(all_args) and all_args[idx] is not None
|
|
1248
2232
|
]
|
|
1249
2233
|
|
|
2234
|
+
def _security_ta_ctor_args_for_variant(
|
|
2235
|
+
self,
|
|
2236
|
+
sec_id: int,
|
|
2237
|
+
site: TACallSite,
|
|
2238
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
|
|
2239
|
+
fallback_args: list[str] | None = None,
|
|
2240
|
+
) -> tuple[list[str], list[bool] | None]:
|
|
2241
|
+
"""Resolve TA constructor args in one helper-call lexical context.
|
|
2242
|
+
|
|
2243
|
+
``TACallSite.ctor_args`` is a source-string snapshot which ordinary
|
|
2244
|
+
callable specialization mutates from the first call site. A single
|
|
2245
|
+
TA AST can also be reached several times while lowering one
|
|
2246
|
+
``request.security`` tuple (for example ``ema(src, 2)`` through
|
|
2247
|
+
``ema(src, 5)``). Those requested-context variants share the AST but
|
|
2248
|
+
must not share that first snapshot. Re-lower the constructor AST
|
|
2249
|
+
through the exact helper binding stack used to key the variant.
|
|
2250
|
+
|
|
2251
|
+
Calls outside an inlined helper retain the established string path so
|
|
2252
|
+
input-backed runtime resets and direct top-level TA sites stay
|
|
2253
|
+
byte-compatible. The optional boolean list records, per argument,
|
|
2254
|
+
whether the source AST is a bar-invariant scalar after resolving its
|
|
2255
|
+
exact helper bindings. Runtime-reset collection uses that provenance
|
|
2256
|
+
to reuse trusted lowered C++ without allowing a mixed input + series
|
|
2257
|
+
expression merely because it contains a generated ``get_input_*`` call.
|
|
2258
|
+
"""
|
|
2259
|
+
fallback = list(
|
|
2260
|
+
site.ctor_args if fallback_args is None else fallback_args
|
|
2261
|
+
)
|
|
2262
|
+
if not helper_binding_stack:
|
|
2263
|
+
return fallback, None
|
|
2264
|
+
arg_nodes = self._security_ta_ctor_arg_nodes(site)
|
|
2265
|
+
if len(arg_nodes) != len(fallback):
|
|
2266
|
+
return fallback, None
|
|
2267
|
+
return (
|
|
2268
|
+
[
|
|
2269
|
+
self._build_security_expr(
|
|
2270
|
+
sec_id,
|
|
2271
|
+
arg,
|
|
2272
|
+
None,
|
|
2273
|
+
{},
|
|
2274
|
+
resolving=set(),
|
|
2275
|
+
security_mutable_names=set(),
|
|
2276
|
+
helper_binding_stack=helper_binding_stack,
|
|
2277
|
+
emitted_lines=None,
|
|
2278
|
+
)
|
|
2279
|
+
for arg in arg_nodes
|
|
2280
|
+
],
|
|
2281
|
+
[
|
|
2282
|
+
self._security_ta_ctor_arg_is_stable(
|
|
2283
|
+
arg,
|
|
2284
|
+
helper_binding_stack,
|
|
2285
|
+
)
|
|
2286
|
+
for arg in arg_nodes
|
|
2287
|
+
],
|
|
2288
|
+
)
|
|
2289
|
+
|
|
2290
|
+
def _security_ta_ctor_arg_is_stable(
|
|
2291
|
+
self,
|
|
2292
|
+
node: ASTNode,
|
|
2293
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
|
|
2294
|
+
) -> bool:
|
|
2295
|
+
"""Classify one helper-bound requested-context TA constructor arg.
|
|
2296
|
+
|
|
2297
|
+
``_expr_is_stable`` already owns the compiler's conservative definition
|
|
2298
|
+
of a per-run scalar. Resolve helper parameters first, preserving the
|
|
2299
|
+
authored AST shape, and then delegate to that shared classifier. This
|
|
2300
|
+
prevents a later helper call such as ``ema(src, inputLen + int(close))``
|
|
2301
|
+
from bypassing the ordinary TA-length guard just because an earlier
|
|
2302
|
+
call through the same source TA site used a safe length.
|
|
2303
|
+
"""
|
|
2304
|
+
import dataclasses
|
|
2305
|
+
|
|
2306
|
+
def resolve(
|
|
2307
|
+
current,
|
|
2308
|
+
stack: tuple[dict[str, ASTNode], ...] | None,
|
|
2309
|
+
seen: frozenset[tuple[str, int]] = frozenset(),
|
|
2310
|
+
depth: int = 0,
|
|
2311
|
+
):
|
|
2312
|
+
if current is None or depth > 32:
|
|
2313
|
+
return None
|
|
2314
|
+
if isinstance(current, Identifier):
|
|
2315
|
+
binding = None
|
|
2316
|
+
if not self._security_identifier_is_global_binding(current):
|
|
2317
|
+
binding = self._security_lookup_helper_binding_context(
|
|
2318
|
+
current.name,
|
|
2319
|
+
stack,
|
|
2320
|
+
)
|
|
2321
|
+
if binding is None:
|
|
2322
|
+
return current
|
|
2323
|
+
bound, bound_stack = binding
|
|
2324
|
+
if isinstance(bound, str):
|
|
2325
|
+
return None
|
|
2326
|
+
key = (current.name, id(bound))
|
|
2327
|
+
if key in seen:
|
|
2328
|
+
return None
|
|
2329
|
+
return resolve(
|
|
2330
|
+
bound,
|
|
2331
|
+
bound_stack,
|
|
2332
|
+
seen | {key},
|
|
2333
|
+
depth + 1,
|
|
2334
|
+
)
|
|
2335
|
+
if isinstance(
|
|
2336
|
+
current,
|
|
2337
|
+
(NumberLiteral, StringLiteral, BoolLiteral, NaLiteral),
|
|
2338
|
+
):
|
|
2339
|
+
return current
|
|
2340
|
+
if isinstance(current, MemberAccess):
|
|
2341
|
+
obj = resolve(current.object, stack, seen, depth + 1)
|
|
2342
|
+
if obj is None:
|
|
2343
|
+
return None
|
|
2344
|
+
return dataclasses.replace(current, object=obj)
|
|
2345
|
+
if isinstance(current, BinOp):
|
|
2346
|
+
left = resolve(current.left, stack, seen, depth + 1)
|
|
2347
|
+
right = resolve(current.right, stack, seen, depth + 1)
|
|
2348
|
+
if left is None or right is None:
|
|
2349
|
+
return None
|
|
2350
|
+
return dataclasses.replace(current, left=left, right=right)
|
|
2351
|
+
if isinstance(current, UnaryOp):
|
|
2352
|
+
operand = resolve(current.operand, stack, seen, depth + 1)
|
|
2353
|
+
if operand is None:
|
|
2354
|
+
return None
|
|
2355
|
+
return dataclasses.replace(current, operand=operand)
|
|
2356
|
+
if isinstance(current, Ternary):
|
|
2357
|
+
condition = resolve(current.condition, stack, seen, depth + 1)
|
|
2358
|
+
true_val = resolve(current.true_val, stack, seen, depth + 1)
|
|
2359
|
+
false_val = resolve(current.false_val, stack, seen, depth + 1)
|
|
2360
|
+
if condition is None or true_val is None or false_val is None:
|
|
2361
|
+
return None
|
|
2362
|
+
return dataclasses.replace(
|
|
2363
|
+
current,
|
|
2364
|
+
condition=condition,
|
|
2365
|
+
true_val=true_val,
|
|
2366
|
+
false_val=false_val,
|
|
2367
|
+
)
|
|
2368
|
+
if isinstance(current, FuncCall):
|
|
2369
|
+
args = [resolve(arg, stack, seen, depth + 1) for arg in current.args]
|
|
2370
|
+
kwargs = {
|
|
2371
|
+
name: resolve(arg, stack, seen, depth + 1)
|
|
2372
|
+
for name, arg in current.kwargs.items()
|
|
2373
|
+
}
|
|
2374
|
+
if any(arg is None for arg in args) or any(
|
|
2375
|
+
arg is None for arg in kwargs.values()
|
|
2376
|
+
):
|
|
2377
|
+
return None
|
|
2378
|
+
return dataclasses.replace(current, args=args, kwargs=kwargs)
|
|
2379
|
+
# History, collections, and every other unhandled shape are not
|
|
2380
|
+
# stable TA lengths. Fail closed instead of inventing a buffer size.
|
|
2381
|
+
return None
|
|
2382
|
+
|
|
2383
|
+
resolved = resolve(node, helper_binding_stack)
|
|
2384
|
+
return resolved is not None and self._expr_is_stable(resolved)
|
|
2385
|
+
|
|
1250
2386
|
def _security_ta_ctor_depends_on_mutables(
|
|
1251
2387
|
self,
|
|
1252
2388
|
site: TACallSite,
|
|
1253
2389
|
security_mutable_names: set[str],
|
|
1254
2390
|
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
1255
2391
|
) -> bool:
|
|
2392
|
+
# A top-level ``var int L = 5`` is normally classified as mutable by
|
|
2393
|
+
# request.security because persistent state is rebound per requested
|
|
2394
|
+
# context. The TA constructor does not read that mutable member when
|
|
2395
|
+
# ``L`` passed the declaration-exact stable-literal admission proof:
|
|
2396
|
+
# all main/requested-context buffers are sized directly from ``5``.
|
|
2397
|
+
# Remove only those admitted names for this ctor-dependency query.
|
|
2398
|
+
# Compute arguments and every other security mutability path retain
|
|
2399
|
+
# the full set, and helper bindings still resolve transitively before
|
|
2400
|
+
# checking the reduced set.
|
|
2401
|
+
ctor_mutable_names = security_mutable_names.difference(
|
|
2402
|
+
getattr(self, "_stable_var_ctor_literals", {})
|
|
2403
|
+
)
|
|
1256
2404
|
return any(
|
|
1257
2405
|
self._expr_depends_on_security_mutables(
|
|
1258
2406
|
arg,
|
|
1259
|
-
|
|
2407
|
+
ctor_mutable_names,
|
|
1260
2408
|
helper_binding_stack=helper_binding_stack,
|
|
1261
2409
|
)
|
|
1262
2410
|
for arg in self._security_ta_ctor_arg_nodes(site)
|
|
@@ -1279,8 +2427,13 @@ class SecurityEmitter:
|
|
|
1279
2427
|
resolving = set()
|
|
1280
2428
|
|
|
1281
2429
|
if isinstance(expr_node, Identifier):
|
|
1282
|
-
|
|
1283
|
-
if
|
|
2430
|
+
binding = None
|
|
2431
|
+
if not self._security_identifier_is_global_binding(expr_node):
|
|
2432
|
+
binding = self._security_lookup_helper_binding_context(
|
|
2433
|
+
expr_node.name, helper_binding_stack
|
|
2434
|
+
)
|
|
2435
|
+
if binding is not None:
|
|
2436
|
+
bound, bound_stack = binding
|
|
1284
2437
|
if isinstance(bound, str):
|
|
1285
2438
|
return collected
|
|
1286
2439
|
# Guard against a cyclic helper binding: an identifier bound to
|
|
@@ -1296,7 +2449,7 @@ class SecurityEmitter:
|
|
|
1296
2449
|
self._collect_security_ta_binding_stacks(
|
|
1297
2450
|
bound,
|
|
1298
2451
|
resolving,
|
|
1299
|
-
|
|
2452
|
+
bound_stack,
|
|
1300
2453
|
collected,
|
|
1301
2454
|
inline_ta_indices,
|
|
1302
2455
|
inline_helper,
|
|
@@ -1320,12 +2473,16 @@ class SecurityEmitter:
|
|
|
1320
2473
|
return collected
|
|
1321
2474
|
|
|
1322
2475
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1323
|
-
if
|
|
2476
|
+
if (
|
|
2477
|
+
self._security_identifier_is_global_binding(expr_node)
|
|
2478
|
+
and expr_node.name in global_expr_map
|
|
2479
|
+
and expr_node.name not in resolving
|
|
2480
|
+
):
|
|
1324
2481
|
resolving.add(expr_node.name)
|
|
1325
2482
|
self._collect_security_ta_binding_stacks(
|
|
1326
2483
|
global_expr_map[expr_node.name],
|
|
1327
2484
|
resolving,
|
|
1328
|
-
|
|
2485
|
+
(),
|
|
1329
2486
|
collected,
|
|
1330
2487
|
inline_ta_indices,
|
|
1331
2488
|
inline_helper,
|
|
@@ -1944,11 +3101,17 @@ class SecurityEmitter:
|
|
|
1944
3101
|
"bb": "ta::BBResult",
|
|
1945
3102
|
"kc": "ta::KCResult",
|
|
1946
3103
|
"vwap_bands": "ta::VWAPBandsResult",
|
|
1947
|
-
}.get(
|
|
3104
|
+
}.get(
|
|
3105
|
+
ta_name,
|
|
3106
|
+
self._security_helper_tuple_cpp_type(
|
|
3107
|
+
tuple_size,
|
|
3108
|
+
item.get("tuple_element_types", ()),
|
|
3109
|
+
),
|
|
3110
|
+
)
|
|
1948
3111
|
lines.append(f" case {sec_id}:")
|
|
1949
3112
|
lines.append(
|
|
1950
3113
|
f" _req_sec_{sec_id} = "
|
|
1951
|
-
f"{self._security_tuple_result_default(ctype, tuple_size)};"
|
|
3114
|
+
f"{self._security_tuple_result_default(ctype, tuple_size, item.get('tuple_element_types', ()))};"
|
|
1952
3115
|
)
|
|
1953
3116
|
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
1954
3117
|
lines.append(
|
|
@@ -2003,8 +3166,13 @@ class SecurityEmitter:
|
|
|
2003
3166
|
helper_binding_stack = ()
|
|
2004
3167
|
|
|
2005
3168
|
if isinstance(expr_node, Identifier):
|
|
2006
|
-
|
|
2007
|
-
if
|
|
3169
|
+
binding = None
|
|
3170
|
+
if not self._security_identifier_is_global_binding(expr_node):
|
|
3171
|
+
binding = self._security_lookup_helper_binding_context(
|
|
3172
|
+
expr_node.name, helper_binding_stack
|
|
3173
|
+
)
|
|
3174
|
+
if binding is not None:
|
|
3175
|
+
bound, bound_stack = binding
|
|
2008
3176
|
if isinstance(bound, str):
|
|
2009
3177
|
series_name = self._security_series_binding_target(bound)
|
|
2010
3178
|
if series_name is not None:
|
|
@@ -2017,7 +3185,7 @@ class SecurityEmitter:
|
|
|
2017
3185
|
ta_results,
|
|
2018
3186
|
resolving,
|
|
2019
3187
|
security_mutable_names,
|
|
2020
|
-
|
|
3188
|
+
bound_stack,
|
|
2021
3189
|
emitted_lines,
|
|
2022
3190
|
)
|
|
2023
3191
|
bar_fields = {
|
|
@@ -2037,7 +3205,11 @@ class SecurityEmitter:
|
|
|
2037
3205
|
return state_name
|
|
2038
3206
|
|
|
2039
3207
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2040
|
-
if
|
|
3208
|
+
if (
|
|
3209
|
+
self._security_identifier_is_global_binding(expr_node)
|
|
3210
|
+
and expr_node.name in global_expr_map
|
|
3211
|
+
and expr_node.name not in resolving
|
|
3212
|
+
):
|
|
2041
3213
|
resolving.add(expr_node.name)
|
|
2042
3214
|
resolved = self._build_security_expr(
|
|
2043
3215
|
sec_id,
|
|
@@ -2046,7 +3218,7 @@ class SecurityEmitter:
|
|
|
2046
3218
|
ta_results,
|
|
2047
3219
|
resolving,
|
|
2048
3220
|
security_mutable_names,
|
|
2049
|
-
|
|
3221
|
+
(),
|
|
2050
3222
|
emitted_lines,
|
|
2051
3223
|
)
|
|
2052
3224
|
resolving.remove(expr_node.name)
|
|
@@ -2062,38 +3234,60 @@ class SecurityEmitter:
|
|
|
2062
3234
|
return resolved
|
|
2063
3235
|
|
|
2064
3236
|
if isinstance(expr_node, Subscript):
|
|
2065
|
-
index_cpp = self._build_security_expr(
|
|
2066
|
-
sec_id,
|
|
2067
|
-
expr_node.index,
|
|
2068
|
-
ta_range,
|
|
2069
|
-
ta_results,
|
|
2070
|
-
resolving,
|
|
2071
|
-
security_mutable_names,
|
|
2072
|
-
helper_binding_stack,
|
|
2073
|
-
emitted_lines,
|
|
2074
|
-
)
|
|
2075
3237
|
if isinstance(expr_node.object, Identifier):
|
|
2076
|
-
|
|
2077
|
-
if
|
|
3238
|
+
binding = None
|
|
3239
|
+
if not self._security_identifier_is_global_binding(expr_node.object):
|
|
3240
|
+
binding = self._security_lookup_helper_binding_context(
|
|
3241
|
+
expr_node.object.name, helper_binding_stack
|
|
3242
|
+
)
|
|
3243
|
+
if binding is not None:
|
|
3244
|
+
bound, bound_stack = binding
|
|
2078
3245
|
if isinstance(bound, str):
|
|
2079
3246
|
series_name = self._security_series_binding_target(bound)
|
|
2080
3247
|
if series_name is not None:
|
|
3248
|
+
index_cpp = self._build_security_expr(
|
|
3249
|
+
sec_id,
|
|
3250
|
+
expr_node.index,
|
|
3251
|
+
ta_range,
|
|
3252
|
+
ta_results,
|
|
3253
|
+
resolving,
|
|
3254
|
+
security_mutable_names,
|
|
3255
|
+
helper_binding_stack,
|
|
3256
|
+
emitted_lines,
|
|
3257
|
+
)
|
|
2081
3258
|
return f'_security_helper_series_["{series_name}"][{index_cpp}]'
|
|
2082
3259
|
return bound
|
|
2083
|
-
|
|
2084
|
-
|
|
3260
|
+
# Function parameters retain Pine's series identity. Apply
|
|
3261
|
+
# history to the supported bound bar series and compose
|
|
3262
|
+
# nested offsets before lowering. Unsupported scalar/
|
|
3263
|
+
# expression bindings fail closed in the shared helper.
|
|
3264
|
+
local_index = expr_node.index
|
|
3265
|
+
resolved_local_index = self._resolve_security_index_literal(
|
|
3266
|
+
expr_node.index, helper_binding_stack
|
|
3267
|
+
)
|
|
3268
|
+
if resolved_local_index is not None:
|
|
3269
|
+
local_index = NumberLiteral(value=resolved_local_index)
|
|
3270
|
+
composed = self._compose_security_helper_history_subscript(
|
|
2085
3271
|
bound,
|
|
3272
|
+
local_index,
|
|
3273
|
+
expr_node,
|
|
3274
|
+
)
|
|
3275
|
+
return self._build_security_expr(
|
|
3276
|
+
sec_id,
|
|
3277
|
+
composed,
|
|
2086
3278
|
ta_range,
|
|
2087
3279
|
ta_results,
|
|
2088
3280
|
resolving,
|
|
2089
3281
|
security_mutable_names,
|
|
2090
|
-
|
|
3282
|
+
bound_stack,
|
|
2091
3283
|
emitted_lines,
|
|
2092
3284
|
)
|
|
2093
|
-
return f"{obj_cpp}[{index_cpp}]"
|
|
2094
3285
|
if expr_node.object.name in SECURITY_BAR_FIELDS:
|
|
2095
3286
|
field = expr_node.object.name
|
|
2096
|
-
idx_lit = self.
|
|
3287
|
+
idx_lit = self._resolve_security_index_literal(
|
|
3288
|
+
expr_node.index,
|
|
3289
|
+
helper_binding_stack,
|
|
3290
|
+
)
|
|
2097
3291
|
if idx_lit is not None:
|
|
2098
3292
|
if idx_lit == 0:
|
|
2099
3293
|
return self._security_bar_field_expr(field)
|
|
@@ -2113,6 +3307,16 @@ class SecurityEmitter:
|
|
|
2113
3307
|
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
2114
3308
|
cpp_t = self._security_bar_hist_type(field)
|
|
2115
3309
|
current = self._security_bar_field_expr(field)
|
|
3310
|
+
index_cpp = self._build_security_expr(
|
|
3311
|
+
sec_id,
|
|
3312
|
+
expr_node.index,
|
|
3313
|
+
ta_range,
|
|
3314
|
+
ta_results,
|
|
3315
|
+
resolving,
|
|
3316
|
+
security_mutable_names,
|
|
3317
|
+
helper_binding_stack,
|
|
3318
|
+
emitted_lines,
|
|
3319
|
+
)
|
|
2116
3320
|
return (
|
|
2117
3321
|
f"([&]() -> {cpp_t} {{ "
|
|
2118
3322
|
f"int _hidx = (int)({index_cpp}); "
|
|
@@ -2132,8 +3336,11 @@ class SecurityEmitter:
|
|
|
2132
3336
|
# helper binding, whichever the resolved expression turns out
|
|
2133
3337
|
# to be) instead of duplicating that dispatch here.
|
|
2134
3338
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2135
|
-
if (
|
|
2136
|
-
|
|
3339
|
+
if (
|
|
3340
|
+
self._security_identifier_is_global_binding(expr_node.object)
|
|
3341
|
+
and expr_node.object.name in global_expr_map
|
|
3342
|
+
and expr_node.object.name not in resolving
|
|
3343
|
+
):
|
|
2137
3344
|
resolving.add(expr_node.object.name)
|
|
2138
3345
|
resolved = self._build_security_expr(
|
|
2139
3346
|
sec_id,
|
|
@@ -2154,6 +3361,16 @@ class SecurityEmitter:
|
|
|
2154
3361
|
meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
|
|
2155
3362
|
hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
|
|
2156
3363
|
cpp_t = meta["type"] if meta else "double"
|
|
3364
|
+
index_cpp = self._build_security_expr(
|
|
3365
|
+
sec_id,
|
|
3366
|
+
expr_node.index,
|
|
3367
|
+
ta_range,
|
|
3368
|
+
ta_results,
|
|
3369
|
+
resolving,
|
|
3370
|
+
security_mutable_names,
|
|
3371
|
+
helper_binding_stack,
|
|
3372
|
+
emitted_lines,
|
|
3373
|
+
)
|
|
2157
3374
|
inner = self._build_security_expr(
|
|
2158
3375
|
sec_id,
|
|
2159
3376
|
expr_node.object,
|
|
@@ -2185,14 +3402,72 @@ class SecurityEmitter:
|
|
|
2185
3402
|
# tick), so without a magnifier it advanced every chart bar and
|
|
2186
3403
|
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
2187
3404
|
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
2188
|
-
|
|
3405
|
+
# A top-level TA site reached through a global alias owns the
|
|
3406
|
+
# global variant even when the subscript index was authored
|
|
3407
|
+
# in a helper. Keep the helper stack for resolving that
|
|
3408
|
+
# index, but do not use it to select the TA object's state.
|
|
3409
|
+
ta_binding_stack = (
|
|
3410
|
+
()
|
|
3411
|
+
if getattr(ta_site, "owner_func", None) is None
|
|
3412
|
+
else helper_binding_stack
|
|
3413
|
+
)
|
|
3414
|
+
sig = self._security_binding_stack_signature(ta_binding_stack)
|
|
2189
3415
|
idx_lit = self._resolve_security_index_literal(
|
|
2190
3416
|
expr_node.index, helper_binding_stack
|
|
2191
3417
|
)
|
|
2192
3418
|
if idx_lit is None:
|
|
3419
|
+
resolved_input = self._resolve_security_immutable_input_int(
|
|
3420
|
+
expr_node.index, helper_binding_stack
|
|
3421
|
+
)
|
|
3422
|
+
if resolved_input is None:
|
|
3423
|
+
self._codegen_error(
|
|
3424
|
+
expr_node,
|
|
3425
|
+
"request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
|
|
3426
|
+
)
|
|
3427
|
+
input_node, input_stack = resolved_input
|
|
3428
|
+
index_cpp = self._build_security_expr(
|
|
3429
|
+
sec_id,
|
|
3430
|
+
input_node,
|
|
3431
|
+
ta_range,
|
|
3432
|
+
ta_results,
|
|
3433
|
+
resolving,
|
|
3434
|
+
security_mutable_names,
|
|
3435
|
+
input_stack,
|
|
3436
|
+
emitted_lines,
|
|
3437
|
+
)
|
|
3438
|
+
result_key = (idx, sig)
|
|
3439
|
+
if result_key not in ta_results:
|
|
3440
|
+
self._codegen_error(
|
|
3441
|
+
expr_node,
|
|
3442
|
+
"request.security multi-statement helper TA history is not supported",
|
|
3443
|
+
hint="Use a single-expression helper for TA history offsets.",
|
|
3444
|
+
)
|
|
3445
|
+
current = self._build_security_expr(
|
|
3446
|
+
sec_id,
|
|
3447
|
+
expr_node.object,
|
|
3448
|
+
ta_range,
|
|
3449
|
+
ta_results,
|
|
3450
|
+
resolving,
|
|
3451
|
+
security_mutable_names,
|
|
3452
|
+
ta_binding_stack,
|
|
3453
|
+
emitted_lines,
|
|
3454
|
+
)
|
|
3455
|
+
member_name = self._security_ta_variant_names.get(
|
|
3456
|
+
(sec_id, idx, sig),
|
|
3457
|
+
f"_sec{sec_id}_{ta_site.member_name}",
|
|
3458
|
+
)
|
|
3459
|
+
hist = self._security_ta_hist_series_cpp(member_name)
|
|
3460
|
+
return (
|
|
3461
|
+
"([&]() -> double { "
|
|
3462
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
3463
|
+
"if (is_na(_hidx) || _hidx < 0) return na<double>(); "
|
|
3464
|
+
f"return (_hidx == 0) ? {current} : {hist}[_hidx - 1]; "
|
|
3465
|
+
"}())"
|
|
3466
|
+
)
|
|
3467
|
+
if idx_lit < 0:
|
|
2193
3468
|
self._codegen_error(
|
|
2194
3469
|
expr_node,
|
|
2195
|
-
"request.security() TA history index must be
|
|
3470
|
+
"request.security() TA history index must be non-negative",
|
|
2196
3471
|
)
|
|
2197
3472
|
if idx_lit == 0:
|
|
2198
3473
|
# Current completed-HTF-bar value: reuse the bare-TA emission.
|
|
@@ -2203,9 +3478,15 @@ class SecurityEmitter:
|
|
|
2203
3478
|
ta_results,
|
|
2204
3479
|
resolving,
|
|
2205
3480
|
security_mutable_names,
|
|
2206
|
-
|
|
3481
|
+
ta_binding_stack,
|
|
2207
3482
|
emitted_lines,
|
|
2208
3483
|
)
|
|
3484
|
+
if (idx, sig) not in ta_results:
|
|
3485
|
+
self._codegen_error(
|
|
3486
|
+
expr_node,
|
|
3487
|
+
"request.security multi-statement helper TA history is not supported",
|
|
3488
|
+
hint="Use a single-expression helper for TA history offsets.",
|
|
3489
|
+
)
|
|
2209
3490
|
member_name = self._security_ta_variant_names.get(
|
|
2210
3491
|
(sec_id, idx, sig),
|
|
2211
3492
|
f"_sec{sec_id}_{ta_site.member_name}",
|
|
@@ -2226,7 +3507,10 @@ class SecurityEmitter:
|
|
|
2226
3507
|
op = cpp_ops.get(expr_node.op, expr_node.op)
|
|
2227
3508
|
if expr_node.op == "%":
|
|
2228
3509
|
return f"std::fmod((double)({left}), (double)({right}))"
|
|
2229
|
-
|
|
3510
|
+
# KI-71: honour Pine's falsy-on-na relational rule inside
|
|
3511
|
+
# request.security expressions too (this builder is a second
|
|
3512
|
+
# relational emission site independent of _visit_binop).
|
|
3513
|
+
return self._lower_relational(op, expr_node.left, expr_node.right, left, right)
|
|
2230
3514
|
|
|
2231
3515
|
if isinstance(expr_node, UnaryOp):
|
|
2232
3516
|
operand = self._build_security_expr(
|
|
@@ -2248,6 +3532,28 @@ class SecurityEmitter:
|
|
|
2248
3532
|
)
|
|
2249
3533
|
return f"(({cond}) ? ({tv}) : ({fv}))"
|
|
2250
3534
|
|
|
3535
|
+
if isinstance(expr_node, TupleLiteral):
|
|
3536
|
+
# A tuple returned by a user helper must lower every element in the
|
|
3537
|
+
# requested context. Falling through to the ordinary expression
|
|
3538
|
+
# visitor loses AST-valued helper parameter bindings (for example
|
|
3539
|
+
# ``f(src) => [src, src + 1]``) and can emit an undeclared ``src``.
|
|
3540
|
+
# Recursive lowering also composes helper locals, HTF history, and
|
|
3541
|
+
# TA results for arbitrary supported tuple arity.
|
|
3542
|
+
elements = [
|
|
3543
|
+
self._build_security_expr(
|
|
3544
|
+
sec_id,
|
|
3545
|
+
element,
|
|
3546
|
+
ta_range,
|
|
3547
|
+
ta_results,
|
|
3548
|
+
resolving,
|
|
3549
|
+
security_mutable_names,
|
|
3550
|
+
helper_binding_stack,
|
|
3551
|
+
emitted_lines,
|
|
3552
|
+
)
|
|
3553
|
+
for element in expr_node.elements
|
|
3554
|
+
]
|
|
3555
|
+
return f"std::make_tuple({', '.join(elements)})"
|
|
3556
|
+
|
|
2251
3557
|
if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
|
|
2252
3558
|
func_name = expr_node.callee.name
|
|
2253
3559
|
if func_name in self._func_names:
|