@pineforge/codegen-pyodide 0.10.0 → 0.10.2
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 +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- 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 +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.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:
|
|
@@ -148,9 +171,82 @@ class SecurityEmitter:
|
|
|
148
171
|
"""
|
|
149
172
|
if node is None:
|
|
150
173
|
return None
|
|
174
|
+
if isinstance(node, SwitchStmt):
|
|
175
|
+
return self._security_tf_switch_runtime_expr(node, resolving or set())
|
|
151
176
|
substituted = self._substitute_tf_input_reads(node, resolving or set())
|
|
152
177
|
return self._visit_expr(substituted)
|
|
153
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
|
+
|
|
154
250
|
def _substitute_tf_input_reads(self, node, resolving: set[str]):
|
|
155
251
|
"""Return ``node`` with input-derived leaves rewritten to expressions
|
|
156
252
|
that are valid at security-registration time (before ``on_bar()``
|
|
@@ -175,12 +271,24 @@ class SecurityEmitter:
|
|
|
175
271
|
if isinstance(node, Identifier):
|
|
176
272
|
name = node.name
|
|
177
273
|
if name in self._timeframe_period_vars:
|
|
178
|
-
return MemberAccess(
|
|
179
|
-
|
|
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)):
|
|
180
283
|
return self._input_var_to_call[name]
|
|
181
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)
|
|
182
286
|
and isinstance(self._known_vars[name], str)):
|
|
183
|
-
return StringLiteral(
|
|
287
|
+
return StringLiteral(
|
|
288
|
+
value=self._known_vars[name],
|
|
289
|
+
loc=node.loc,
|
|
290
|
+
annotations=node.annotations,
|
|
291
|
+
)
|
|
184
292
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
185
293
|
if name in global_expr_map and name not in resolving:
|
|
186
294
|
return self._substitute_tf_input_reads(
|
|
@@ -192,18 +300,18 @@ class SecurityEmitter:
|
|
|
192
300
|
fv = self._substitute_tf_input_reads(node.false_val, resolving)
|
|
193
301
|
if cond is node.condition and tv is node.true_val and fv is node.false_val:
|
|
194
302
|
return node
|
|
195
|
-
return
|
|
303
|
+
return replace(node, condition=cond, true_val=tv, false_val=fv)
|
|
196
304
|
if isinstance(node, BinOp):
|
|
197
305
|
left = self._substitute_tf_input_reads(node.left, resolving)
|
|
198
306
|
right = self._substitute_tf_input_reads(node.right, resolving)
|
|
199
307
|
if left is node.left and right is node.right:
|
|
200
308
|
return node
|
|
201
|
-
return
|
|
309
|
+
return replace(node, left=left, right=right)
|
|
202
310
|
if isinstance(node, UnaryOp):
|
|
203
311
|
operand = self._substitute_tf_input_reads(node.operand, resolving)
|
|
204
312
|
if operand is node.operand:
|
|
205
313
|
return node
|
|
206
|
-
return
|
|
314
|
+
return replace(node, operand=operand)
|
|
207
315
|
if isinstance(node, FuncCall):
|
|
208
316
|
new_args = [self._substitute_tf_input_reads(a, resolving) for a in node.args]
|
|
209
317
|
new_kwargs = {
|
|
@@ -217,13 +325,19 @@ class SecurityEmitter:
|
|
|
217
325
|
)
|
|
218
326
|
if unchanged:
|
|
219
327
|
return node
|
|
220
|
-
|
|
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)
|
|
221
335
|
if isinstance(node, Subscript):
|
|
222
336
|
obj = self._substitute_tf_input_reads(node.object, resolving)
|
|
223
337
|
idx = self._substitute_tf_input_reads(node.index, resolving)
|
|
224
338
|
if obj is node.object and idx is node.index:
|
|
225
339
|
return node
|
|
226
|
-
return
|
|
340
|
+
return replace(node, object=obj, index=idx)
|
|
227
341
|
return node
|
|
228
342
|
|
|
229
343
|
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
@@ -275,6 +389,9 @@ class SecurityEmitter:
|
|
|
275
389
|
"expr_node": item.expression,
|
|
276
390
|
"returns_tuple": item.returns_tuple,
|
|
277
391
|
"tuple_size": item.tuple_size,
|
|
392
|
+
"tuple_element_types": tuple(
|
|
393
|
+
getattr(item, "tuple_element_types", ()) or ()
|
|
394
|
+
),
|
|
278
395
|
"gaps_node": item.gaps,
|
|
279
396
|
"lookahead_node": item.lookahead,
|
|
280
397
|
"ta_range": item.ta_range,
|
|
@@ -291,6 +408,7 @@ class SecurityEmitter:
|
|
|
291
408
|
"expr_node": item[2] if len(item) > 2 else None,
|
|
292
409
|
"returns_tuple": item[3] if len(item) > 3 else False,
|
|
293
410
|
"tuple_size": item[4] if len(item) > 4 else 0,
|
|
411
|
+
"tuple_element_types": (),
|
|
294
412
|
"gaps_node": item[5] if len(item) > 5 else None,
|
|
295
413
|
"lookahead_node": item[6] if len(item) > 6 else None,
|
|
296
414
|
"ta_range": item[7] if len(item) > 7 else None,
|
|
@@ -371,6 +489,22 @@ class SecurityEmitter:
|
|
|
371
489
|
name: str,
|
|
372
490
|
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None,
|
|
373
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
|
+
"""
|
|
374
508
|
if not helper_binding_stack:
|
|
375
509
|
return None
|
|
376
510
|
for frame in reversed(helper_binding_stack):
|
|
@@ -379,7 +513,12 @@ class SecurityEmitter:
|
|
|
379
513
|
bound = frame[name]
|
|
380
514
|
if isinstance(bound, Identifier) and bound.name == name:
|
|
381
515
|
continue
|
|
382
|
-
|
|
516
|
+
lexical_stack = (
|
|
517
|
+
frame.caller_stack
|
|
518
|
+
if isinstance(frame, _SecurityHelperArgumentFrame)
|
|
519
|
+
else helper_binding_stack
|
|
520
|
+
)
|
|
521
|
+
return bound, lexical_stack
|
|
383
522
|
return None
|
|
384
523
|
|
|
385
524
|
def _literal_int_for_security_index(self, node) -> int | None:
|
|
@@ -402,11 +541,11 @@ class SecurityEmitter:
|
|
|
402
541
|
return None
|
|
403
542
|
return None
|
|
404
543
|
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
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.
|
|
410
549
|
_SECURITY_CONST_BARSTATE = {
|
|
411
550
|
"isrealtime": False,
|
|
412
551
|
"islast": False,
|
|
@@ -422,10 +561,10 @@ class SecurityEmitter:
|
|
|
422
561
|
) -> bool | None:
|
|
423
562
|
"""Fold a boolean expression to a compile-time constant, or None.
|
|
424
563
|
|
|
425
|
-
Only reduces expressions whose non-literal leaves
|
|
426
|
-
|
|
427
|
-
resolve a request.security history index
|
|
428
|
-
``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."""
|
|
429
568
|
if resolving is None:
|
|
430
569
|
resolving = set()
|
|
431
570
|
if isinstance(node, BoolLiteral):
|
|
@@ -452,16 +591,33 @@ class SecurityEmitter:
|
|
|
452
591
|
return None
|
|
453
592
|
return (left and right) if node.op == "and" else (left or right)
|
|
454
593
|
if isinstance(node, Identifier):
|
|
455
|
-
|
|
456
|
-
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
|
|
457
603
|
return self._fold_security_const_bool(
|
|
458
|
-
bound,
|
|
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:
|
|
609
|
+
return self._fold_security_const_bool(
|
|
610
|
+
param_binding, (), resolving | {param_key}
|
|
459
611
|
)
|
|
460
612
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
461
|
-
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
|
+
):
|
|
462
618
|
resolving.add(node.name)
|
|
463
619
|
out = self._fold_security_const_bool(
|
|
464
|
-
global_expr_map[node.name],
|
|
620
|
+
global_expr_map[node.name], (), resolving
|
|
465
621
|
)
|
|
466
622
|
resolving.remove(node.name)
|
|
467
623
|
return out
|
|
@@ -477,27 +633,44 @@ class SecurityEmitter:
|
|
|
477
633
|
|
|
478
634
|
Extends ``_literal_int_for_security_index`` by also resolving the index
|
|
479
635
|
through helper-parameter bindings and global aliases and constant-folding
|
|
480
|
-
a ternary whose condition
|
|
481
|
-
``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index
|
|
482
|
-
circuits on the first line, so behaviour is unchanged for every
|
|
483
|
-
literal index. Returns None when the index cannot be reduced to
|
|
484
|
-
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."""
|
|
485
641
|
direct = self._literal_int_for_security_index(node)
|
|
486
642
|
if direct is not None:
|
|
487
643
|
return direct
|
|
488
644
|
if resolving is None:
|
|
489
645
|
resolving = set()
|
|
490
646
|
if isinstance(node, Identifier):
|
|
491
|
-
|
|
492
|
-
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
|
|
493
656
|
return self._resolve_security_index_literal(
|
|
494
|
-
bound,
|
|
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:
|
|
662
|
+
return self._resolve_security_index_literal(
|
|
663
|
+
param_binding, (), resolving | {param_key}
|
|
495
664
|
)
|
|
496
665
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
497
|
-
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
|
+
):
|
|
498
671
|
resolving.add(node.name)
|
|
499
672
|
out = self._resolve_security_index_literal(
|
|
500
|
-
global_expr_map[node.name],
|
|
673
|
+
global_expr_map[node.name], (), resolving
|
|
501
674
|
)
|
|
502
675
|
resolving.remove(node.name)
|
|
503
676
|
return out
|
|
@@ -512,35 +685,395 @@ class SecurityEmitter:
|
|
|
512
685
|
)
|
|
513
686
|
return None
|
|
514
687
|
|
|
515
|
-
def
|
|
516
|
-
"""
|
|
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
|
+
"""
|
|
517
910
|
out: set[str] = set()
|
|
911
|
+
if resolving is None:
|
|
912
|
+
resolving = set()
|
|
518
913
|
|
|
519
|
-
def walk(n):
|
|
520
|
-
if n is None:
|
|
914
|
+
def walk(n, bindings) -> None:
|
|
915
|
+
if n is None or isinstance(n, str):
|
|
521
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
|
+
|
|
522
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
|
|
523
977
|
if n.object.name in SECURITY_BAR_FIELDS:
|
|
524
|
-
idx = self.
|
|
525
|
-
#
|
|
526
|
-
# completed
|
|
527
|
-
# in _eval_security_*). Dynamic indices may be >=1 at
|
|
528
|
-
# 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.
|
|
529
981
|
if idx is None or idx >= 1:
|
|
530
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
|
+
|
|
531
1064
|
if isinstance(n, (list, tuple)):
|
|
532
|
-
for
|
|
533
|
-
walk(
|
|
1065
|
+
for child in n:
|
|
1066
|
+
walk(child, bindings)
|
|
534
1067
|
return
|
|
535
|
-
for
|
|
536
|
-
if isinstance(
|
|
537
|
-
walk(
|
|
538
|
-
elif isinstance(
|
|
539
|
-
for
|
|
540
|
-
if isinstance(
|
|
541
|
-
walk(
|
|
542
|
-
|
|
543
|
-
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 ())
|
|
544
1077
|
return out
|
|
545
1078
|
|
|
546
1079
|
def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
|
|
@@ -565,10 +1098,43 @@ class SecurityEmitter:
|
|
|
565
1098
|
return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
|
|
566
1099
|
|
|
567
1100
|
@staticmethod
|
|
568
|
-
def _security_tuple_result_default(
|
|
569
|
-
|
|
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
|
+
)
|
|
570
1115
|
return f"{cpp_type}{{{vals}}}"
|
|
571
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
|
+
|
|
572
1138
|
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
573
1139
|
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
574
1140
|
|
|
@@ -578,50 +1144,172 @@ class SecurityEmitter:
|
|
|
578
1144
|
per-site ``Series`` filled (gated on ``is_complete``) in
|
|
579
1145
|
``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
|
|
580
1146
|
OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
|
|
581
|
-
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."""
|
|
582
1153
|
out: set[int] = set()
|
|
583
1154
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1155
|
+
resolving: set[str] = set()
|
|
584
1156
|
|
|
585
|
-
def resolve_ta_site(obj,
|
|
1157
|
+
def resolve_ta_site(obj, bindings, seen: set[str] | None = None):
|
|
586
1158
|
"""_get_ta_site only matches the literal ta.* FuncCall node by
|
|
587
|
-
identity; fall back through
|
|
588
|
-
binding (``v = ta.ema(close, 55)`` then ``...v[1]...``)
|
|
589
|
-
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]...``)."""
|
|
590
1161
|
site = self._get_ta_site(obj)
|
|
591
1162
|
if site is not None:
|
|
592
1163
|
return site
|
|
593
1164
|
if isinstance(obj, Identifier):
|
|
594
|
-
if
|
|
595
|
-
|
|
596
|
-
if obj.name in
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
)
|
|
599
1189
|
return None
|
|
600
1190
|
|
|
601
|
-
def walk(n):
|
|
602
|
-
if n is None:
|
|
1191
|
+
def walk(n, bindings) -> None:
|
|
1192
|
+
if n is None or isinstance(n, str):
|
|
603
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
|
+
|
|
604
1227
|
if isinstance(n, Subscript):
|
|
605
|
-
site = resolve_ta_site(n.object)
|
|
1228
|
+
site = resolve_ta_site(n.object, bindings)
|
|
606
1229
|
if site is not None:
|
|
607
|
-
idx_lit = self._resolve_security_index_literal(n.index)
|
|
608
|
-
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:
|
|
609
1232
|
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
610
1233
|
if site_idx is not None:
|
|
611
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
|
+
|
|
612
1300
|
if isinstance(n, (list, tuple)):
|
|
613
1301
|
for x in n:
|
|
614
|
-
walk(x)
|
|
1302
|
+
walk(x, bindings)
|
|
615
1303
|
return
|
|
616
1304
|
for _k, v in getattr(n, "__dict__", {}).items():
|
|
617
1305
|
if isinstance(v, ASTNode):
|
|
618
|
-
walk(v)
|
|
1306
|
+
walk(v, bindings)
|
|
619
1307
|
elif isinstance(v, (list, tuple)):
|
|
620
1308
|
for x in v:
|
|
621
1309
|
if isinstance(x, ASTNode):
|
|
622
|
-
walk(x)
|
|
1310
|
+
walk(x, bindings)
|
|
623
1311
|
|
|
624
|
-
walk(node)
|
|
1312
|
+
walk(node, ())
|
|
625
1313
|
return out
|
|
626
1314
|
|
|
627
1315
|
def _security_ta_hist_series_cpp(self, member_name: str) -> str:
|
|
@@ -822,6 +1510,49 @@ class SecurityEmitter:
|
|
|
822
1510
|
security_mutable_names: set[str],
|
|
823
1511
|
lines: list[str],
|
|
824
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,
|
|
825
1556
|
) -> str:
|
|
826
1557
|
if plan["mode"] != "linear":
|
|
827
1558
|
self._codegen_error(
|
|
@@ -839,8 +1570,18 @@ class SecurityEmitter:
|
|
|
839
1570
|
pad = " " * indent
|
|
840
1571
|
runtime_stack_local = plan["binding_stack"] + (active_bindings,)
|
|
841
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
|
+
|
|
842
1582
|
if isinstance(stmt, VarDecl):
|
|
843
|
-
|
|
1583
|
+
is_persistent_var = stmt.is_var
|
|
1584
|
+
if stmt.name in local_series_names or is_persistent_var:
|
|
844
1585
|
binding = active_bindings.get(stmt.name)
|
|
845
1586
|
if binding is None:
|
|
846
1587
|
binding = self._security_series_binding(
|
|
@@ -859,13 +1600,53 @@ class SecurityEmitter:
|
|
|
859
1600
|
lines,
|
|
860
1601
|
)
|
|
861
1602
|
active_bindings[stmt.name] = binding
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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()
|
|
869
1650
|
return
|
|
870
1651
|
|
|
871
1652
|
local_name = active_bindings.get(stmt.name)
|
|
@@ -900,6 +1681,7 @@ class SecurityEmitter:
|
|
|
900
1681
|
lines,
|
|
901
1682
|
)
|
|
902
1683
|
lines.append(f"{pad}{local_name} = {expr_cpp};")
|
|
1684
|
+
activate_decl()
|
|
903
1685
|
return
|
|
904
1686
|
|
|
905
1687
|
if isinstance(stmt, Assignment):
|
|
@@ -956,13 +1738,69 @@ class SecurityEmitter:
|
|
|
956
1738
|
)
|
|
957
1739
|
lines.append(f"{pad}if ({cond_cpp}) {{")
|
|
958
1740
|
body_bindings = dict(active_bindings)
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
961
1771
|
if stmt.else_body:
|
|
962
1772
|
lines.append(f"{pad}}} else {{")
|
|
963
1773
|
else_bindings = dict(active_bindings)
|
|
964
|
-
|
|
965
|
-
|
|
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
|
|
966
1804
|
lines.append(f"{pad}}}")
|
|
967
1805
|
return
|
|
968
1806
|
|
|
@@ -998,7 +1836,7 @@ class SecurityEmitter:
|
|
|
998
1836
|
|
|
999
1837
|
sig_frames = []
|
|
1000
1838
|
for idx, frame in enumerate(helper_binding_stack):
|
|
1001
|
-
if idx == 0:
|
|
1839
|
+
if idx == 0 or isinstance(frame, _SecurityHelperArgumentFrame):
|
|
1002
1840
|
sig_frames.append(
|
|
1003
1841
|
tuple((name, _sig_value(node)) for name, node in sorted(frame.items()))
|
|
1004
1842
|
)
|
|
@@ -1051,8 +1889,14 @@ class SecurityEmitter:
|
|
|
1051
1889
|
"request.security helper calls must bind every parameter explicitly",
|
|
1052
1890
|
)
|
|
1053
1891
|
|
|
1054
|
-
new_frame = {param_name: bound_args[idx] for idx, param_name in enumerate(params)}
|
|
1055
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
|
+
)
|
|
1056
1900
|
return fi, base_stack + (new_frame,)
|
|
1057
1901
|
|
|
1058
1902
|
def _security_helper_call_plan(
|
|
@@ -1124,11 +1968,11 @@ class SecurityEmitter:
|
|
|
1124
1968
|
"request.security multi-statement helpers may only use local declarations, assignments, and if-branches before the final expression",
|
|
1125
1969
|
)
|
|
1126
1970
|
if isinstance(stmt, VarDecl):
|
|
1127
|
-
if stmt.
|
|
1971
|
+
if stmt.is_varip:
|
|
1128
1972
|
self._codegen_error(
|
|
1129
1973
|
node,
|
|
1130
|
-
"request.security does not support
|
|
1131
|
-
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.",
|
|
1132
1976
|
)
|
|
1133
1977
|
if isinstance(stmt, Assignment):
|
|
1134
1978
|
target_name = self._get_target_name(stmt.target)
|
|
@@ -1148,6 +1992,92 @@ class SecurityEmitter:
|
|
|
1148
1992
|
"local_series_names": local_series_names,
|
|
1149
1993
|
}
|
|
1150
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
|
+
|
|
1151
2081
|
def _security_next_inline_name(self, sec_id: int, func_name: str, base_name: str) -> str:
|
|
1152
2082
|
self._security_inline_counter += 1
|
|
1153
2083
|
return (
|
|
@@ -1168,13 +2098,18 @@ class SecurityEmitter:
|
|
|
1168
2098
|
resolving = set()
|
|
1169
2099
|
|
|
1170
2100
|
if isinstance(expr_node, Identifier):
|
|
1171
|
-
|
|
1172
|
-
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
|
|
1173
2108
|
return self._expr_depends_on_security_mutables(
|
|
1174
2109
|
bound,
|
|
1175
2110
|
security_mutable_names,
|
|
1176
2111
|
resolving,
|
|
1177
|
-
|
|
2112
|
+
bound_stack,
|
|
1178
2113
|
)
|
|
1179
2114
|
if expr_node.name in security_mutable_names:
|
|
1180
2115
|
return True
|
|
@@ -1296,16 +2231,180 @@ class SecurityEmitter:
|
|
|
1296
2231
|
if idx < len(all_args) and all_args[idx] is not None
|
|
1297
2232
|
]
|
|
1298
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
|
+
|
|
1299
2386
|
def _security_ta_ctor_depends_on_mutables(
|
|
1300
2387
|
self,
|
|
1301
2388
|
site: TACallSite,
|
|
1302
2389
|
security_mutable_names: set[str],
|
|
1303
2390
|
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
1304
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
|
+
)
|
|
1305
2404
|
return any(
|
|
1306
2405
|
self._expr_depends_on_security_mutables(
|
|
1307
2406
|
arg,
|
|
1308
|
-
|
|
2407
|
+
ctor_mutable_names,
|
|
1309
2408
|
helper_binding_stack=helper_binding_stack,
|
|
1310
2409
|
)
|
|
1311
2410
|
for arg in self._security_ta_ctor_arg_nodes(site)
|
|
@@ -1328,8 +2427,13 @@ class SecurityEmitter:
|
|
|
1328
2427
|
resolving = set()
|
|
1329
2428
|
|
|
1330
2429
|
if isinstance(expr_node, Identifier):
|
|
1331
|
-
|
|
1332
|
-
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
|
|
1333
2437
|
if isinstance(bound, str):
|
|
1334
2438
|
return collected
|
|
1335
2439
|
# Guard against a cyclic helper binding: an identifier bound to
|
|
@@ -1345,7 +2449,7 @@ class SecurityEmitter:
|
|
|
1345
2449
|
self._collect_security_ta_binding_stacks(
|
|
1346
2450
|
bound,
|
|
1347
2451
|
resolving,
|
|
1348
|
-
|
|
2452
|
+
bound_stack,
|
|
1349
2453
|
collected,
|
|
1350
2454
|
inline_ta_indices,
|
|
1351
2455
|
inline_helper,
|
|
@@ -1369,12 +2473,16 @@ class SecurityEmitter:
|
|
|
1369
2473
|
return collected
|
|
1370
2474
|
|
|
1371
2475
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1372
|
-
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
|
+
):
|
|
1373
2481
|
resolving.add(expr_node.name)
|
|
1374
2482
|
self._collect_security_ta_binding_stacks(
|
|
1375
2483
|
global_expr_map[expr_node.name],
|
|
1376
2484
|
resolving,
|
|
1377
|
-
|
|
2485
|
+
(),
|
|
1378
2486
|
collected,
|
|
1379
2487
|
inline_ta_indices,
|
|
1380
2488
|
inline_helper,
|
|
@@ -1736,6 +2844,156 @@ class SecurityEmitter:
|
|
|
1736
2844
|
)
|
|
1737
2845
|
return out
|
|
1738
2846
|
|
|
2847
|
+
def _security_lazy_ta_keys(
|
|
2848
|
+
self,
|
|
2849
|
+
sec_id: int,
|
|
2850
|
+
expr_node,
|
|
2851
|
+
info: dict,
|
|
2852
|
+
) -> set[tuple[int, tuple]]:
|
|
2853
|
+
"""``(ta_index, binding_signature)`` pairs Pine reaches only lazily.
|
|
2854
|
+
|
|
2855
|
+
Pine evaluates ``and``/``or`` with left-to-right short-circuit and a
|
|
2856
|
+
ternary evaluates its condition plus exactly one branch. A ``ta.*``
|
|
2857
|
+
call sitting in a skipped operand is **not evaluated on that bar**, so
|
|
2858
|
+
its series state does not advance. The evaluator used to hoist every
|
|
2859
|
+
collected site into an unconditional ``auto _secval_N = ...compute(...)``
|
|
2860
|
+
prologue, which advanced every site on every HTF bar and desynchronised
|
|
2861
|
+
the series against TradingView.
|
|
2862
|
+
|
|
2863
|
+
A site returned here is dropped from that prologue. ``_build_security_expr``
|
|
2864
|
+
then falls through to its inline
|
|
2865
|
+
``(security_series_slot_is_new(N) ? m.compute(a) : m.recompute(a))``
|
|
2866
|
+
form, emitted in expression position — C++'s own ``&&`` / ``||`` /
|
|
2867
|
+
``?:`` short-circuit then advances the state exactly when Pine would,
|
|
2868
|
+
and the relational lowering's ``_pna_l`` / ``_pna_r`` temporaries keep
|
|
2869
|
+
the evaluation order left-to-right.
|
|
2870
|
+
|
|
2871
|
+
Deliberately conservative — anything not provably single-reach and
|
|
2872
|
+
conditional keeps the existing eager hoist, so scripts without
|
|
2873
|
+
conditional security TA regenerate byte-identically:
|
|
2874
|
+
|
|
2875
|
+
- A site reached more than once must stay hoisted: sharing one
|
|
2876
|
+
``_secval_*`` is what keeps it to a single advance per bar, whereas
|
|
2877
|
+
two inline copies would advance it twice (or zero times).
|
|
2878
|
+
- Laziness does **not** propagate through a global binding. In the
|
|
2879
|
+
requested context that global is its own unconditional top-level
|
|
2880
|
+
statement, so it evaluates on every HTF bar wherever it is read
|
|
2881
|
+
(this is what keeps chart-side ``t0``-shaped code exact).
|
|
2882
|
+
- Sites with a history offset (``ta.ema(close, 55)[1]``) keep the
|
|
2883
|
+
committed ``_secval_*`` their per-bar Series push reads.
|
|
2884
|
+
- Securities that rebind mutable globals, and multi-statement helper
|
|
2885
|
+
calls, bail entirely: both lower through statement emitters that
|
|
2886
|
+
consume ``ta_results`` outside this expression.
|
|
2887
|
+
"""
|
|
2888
|
+
if info.get("mutable_globals"):
|
|
2889
|
+
return set()
|
|
2890
|
+
|
|
2891
|
+
occurrences: dict[tuple[int, tuple], list[bool]] = {}
|
|
2892
|
+
unsafe = False
|
|
2893
|
+
|
|
2894
|
+
def walk(node, lazy: bool, binding_stack, resolving: set[str]) -> None:
|
|
2895
|
+
nonlocal unsafe
|
|
2896
|
+
if node is None or unsafe:
|
|
2897
|
+
return
|
|
2898
|
+
|
|
2899
|
+
if isinstance(node, Identifier):
|
|
2900
|
+
binding = None
|
|
2901
|
+
if not self._security_identifier_is_global_binding(node):
|
|
2902
|
+
binding = self._security_lookup_helper_binding_context(
|
|
2903
|
+
node.name, binding_stack
|
|
2904
|
+
)
|
|
2905
|
+
if binding is not None:
|
|
2906
|
+
bound, bound_stack = binding
|
|
2907
|
+
if isinstance(bound, str):
|
|
2908
|
+
return
|
|
2909
|
+
bind_key = f"bind:{id(bound)}"
|
|
2910
|
+
if bind_key in resolving:
|
|
2911
|
+
return
|
|
2912
|
+
resolving.add(bind_key)
|
|
2913
|
+
walk(bound, lazy, bound_stack, resolving)
|
|
2914
|
+
resolving.discard(bind_key)
|
|
2915
|
+
return
|
|
2916
|
+
|
|
2917
|
+
if self._global_mutable_infos.get(node.name) is not None:
|
|
2918
|
+
unsafe = True
|
|
2919
|
+
return
|
|
2920
|
+
|
|
2921
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2922
|
+
if (
|
|
2923
|
+
self._security_identifier_is_global_binding(node)
|
|
2924
|
+
and node.name in global_expr_map
|
|
2925
|
+
and node.name not in resolving
|
|
2926
|
+
):
|
|
2927
|
+
resolving.add(node.name)
|
|
2928
|
+
walk(global_expr_map[node.name], False, (), resolving)
|
|
2929
|
+
resolving.remove(node.name)
|
|
2930
|
+
return
|
|
2931
|
+
|
|
2932
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier):
|
|
2933
|
+
func_name = node.callee.name
|
|
2934
|
+
if func_name in self._func_names:
|
|
2935
|
+
call_key = f"func:{func_name}"
|
|
2936
|
+
if call_key in resolving:
|
|
2937
|
+
return
|
|
2938
|
+
resolving.add(call_key)
|
|
2939
|
+
plan = self._security_helper_call_plan(node, binding_stack)
|
|
2940
|
+
if plan["mode"] == "expr":
|
|
2941
|
+
walk(plan["expr"], lazy, plan["binding_stack"], resolving)
|
|
2942
|
+
else:
|
|
2943
|
+
unsafe = True
|
|
2944
|
+
resolving.discard(call_key)
|
|
2945
|
+
return
|
|
2946
|
+
|
|
2947
|
+
if isinstance(node, BinOp) and node.op in ("and", "or"):
|
|
2948
|
+
walk(node.left, lazy, binding_stack, resolving)
|
|
2949
|
+
walk(node.right, True, binding_stack, resolving)
|
|
2950
|
+
return
|
|
2951
|
+
|
|
2952
|
+
if isinstance(node, Ternary):
|
|
2953
|
+
walk(node.condition, lazy, binding_stack, resolving)
|
|
2954
|
+
walk(node.true_val, True, binding_stack, resolving)
|
|
2955
|
+
walk(node.false_val, True, binding_stack, resolving)
|
|
2956
|
+
return
|
|
2957
|
+
|
|
2958
|
+
site = self._get_ta_site(node)
|
|
2959
|
+
if site is not None:
|
|
2960
|
+
idx = self._ta_index_by_site_id.get(id(site))
|
|
2961
|
+
if idx is not None:
|
|
2962
|
+
key = (idx, self._security_binding_stack_signature(binding_stack))
|
|
2963
|
+
occurrences.setdefault(key, []).append(lazy)
|
|
2964
|
+
|
|
2965
|
+
for child in vars(node).values():
|
|
2966
|
+
walk_value(child, lazy, binding_stack, resolving)
|
|
2967
|
+
|
|
2968
|
+
def walk_value(value, lazy: bool, binding_stack, resolving: set[str]) -> None:
|
|
2969
|
+
if value is None or unsafe:
|
|
2970
|
+
return
|
|
2971
|
+
if hasattr(value, "__dict__"):
|
|
2972
|
+
walk(value, lazy, binding_stack, resolving)
|
|
2973
|
+
return
|
|
2974
|
+
if isinstance(value, (list, tuple)):
|
|
2975
|
+
for item in value:
|
|
2976
|
+
walk_value(item, lazy, binding_stack, resolving)
|
|
2977
|
+
return
|
|
2978
|
+
if isinstance(value, dict):
|
|
2979
|
+
for item in value.values():
|
|
2980
|
+
walk_value(item, lazy, binding_stack, resolving)
|
|
2981
|
+
|
|
2982
|
+
walk(expr_node, False, (), set())
|
|
2983
|
+
if unsafe:
|
|
2984
|
+
return set()
|
|
2985
|
+
|
|
2986
|
+
hist_indices = set(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
|
|
2987
|
+
inline_helper_ta_indices = set(info.get("inline_helper_ta_indices", []))
|
|
2988
|
+
return {
|
|
2989
|
+
key
|
|
2990
|
+
for key, reaches in occurrences.items()
|
|
2991
|
+
if key[0] not in hist_indices
|
|
2992
|
+
and key[0] not in inline_helper_ta_indices
|
|
2993
|
+
and len(reaches) == 1
|
|
2994
|
+
and reaches[0]
|
|
2995
|
+
}
|
|
2996
|
+
|
|
1739
2997
|
def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
|
|
1740
2998
|
"""Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
|
|
1741
2999
|
``is_complete``.
|
|
@@ -1801,6 +3059,7 @@ class SecurityEmitter:
|
|
|
1801
3059
|
ta_indices = info.get("ta_indices") or []
|
|
1802
3060
|
security_mutable_names = set(info.get("mutable_globals", []))
|
|
1803
3061
|
inline_helper_ta_indices = set(info.get("inline_helper_ta_indices", []))
|
|
3062
|
+
lazy_ta_keys = self._security_lazy_ta_keys(sec_id, expr_node, info)
|
|
1804
3063
|
|
|
1805
3064
|
lines.append(f" void _eval_security_{sec_id}(const Bar& bar, bool is_complete) {{")
|
|
1806
3065
|
|
|
@@ -1841,6 +3100,15 @@ class SecurityEmitter:
|
|
|
1841
3100
|
site = self.ctx.ta_call_sites[idx]
|
|
1842
3101
|
variants = (info.get("ta_variants") or {}).get(idx, [])
|
|
1843
3102
|
for variant in variants:
|
|
3103
|
+
if (idx, variant["signature"]) in lazy_ta_keys:
|
|
3104
|
+
# Pine only reaches this site through a
|
|
3105
|
+
# short-circuited operand or an untaken ternary
|
|
3106
|
+
# branch. Leave it out of the eager prologue:
|
|
3107
|
+
# _build_security_expr emits its
|
|
3108
|
+
# compute()/recompute() inline in expression
|
|
3109
|
+
# position, where C++'s &&/||/?: short-circuit
|
|
3110
|
+
# advances the series exactly on the bars Pine does.
|
|
3111
|
+
continue
|
|
1844
3112
|
helper_binding_stack = variant.get("binding_stack", ())
|
|
1845
3113
|
compute_args = self._security_ta_compute_args_for_site(
|
|
1846
3114
|
sec_id,
|
|
@@ -1993,11 +3261,17 @@ class SecurityEmitter:
|
|
|
1993
3261
|
"bb": "ta::BBResult",
|
|
1994
3262
|
"kc": "ta::KCResult",
|
|
1995
3263
|
"vwap_bands": "ta::VWAPBandsResult",
|
|
1996
|
-
}.get(
|
|
3264
|
+
}.get(
|
|
3265
|
+
ta_name,
|
|
3266
|
+
self._security_helper_tuple_cpp_type(
|
|
3267
|
+
tuple_size,
|
|
3268
|
+
item.get("tuple_element_types", ()),
|
|
3269
|
+
),
|
|
3270
|
+
)
|
|
1997
3271
|
lines.append(f" case {sec_id}:")
|
|
1998
3272
|
lines.append(
|
|
1999
3273
|
f" _req_sec_{sec_id} = "
|
|
2000
|
-
f"{self._security_tuple_result_default(ctype, tuple_size)};"
|
|
3274
|
+
f"{self._security_tuple_result_default(ctype, tuple_size, item.get('tuple_element_types', ()))};"
|
|
2001
3275
|
)
|
|
2002
3276
|
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
2003
3277
|
lines.append(
|
|
@@ -2052,8 +3326,13 @@ class SecurityEmitter:
|
|
|
2052
3326
|
helper_binding_stack = ()
|
|
2053
3327
|
|
|
2054
3328
|
if isinstance(expr_node, Identifier):
|
|
2055
|
-
|
|
2056
|
-
if
|
|
3329
|
+
binding = None
|
|
3330
|
+
if not self._security_identifier_is_global_binding(expr_node):
|
|
3331
|
+
binding = self._security_lookup_helper_binding_context(
|
|
3332
|
+
expr_node.name, helper_binding_stack
|
|
3333
|
+
)
|
|
3334
|
+
if binding is not None:
|
|
3335
|
+
bound, bound_stack = binding
|
|
2057
3336
|
if isinstance(bound, str):
|
|
2058
3337
|
series_name = self._security_series_binding_target(bound)
|
|
2059
3338
|
if series_name is not None:
|
|
@@ -2066,7 +3345,7 @@ class SecurityEmitter:
|
|
|
2066
3345
|
ta_results,
|
|
2067
3346
|
resolving,
|
|
2068
3347
|
security_mutable_names,
|
|
2069
|
-
|
|
3348
|
+
bound_stack,
|
|
2070
3349
|
emitted_lines,
|
|
2071
3350
|
)
|
|
2072
3351
|
bar_fields = {
|
|
@@ -2086,7 +3365,11 @@ class SecurityEmitter:
|
|
|
2086
3365
|
return state_name
|
|
2087
3366
|
|
|
2088
3367
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2089
|
-
if
|
|
3368
|
+
if (
|
|
3369
|
+
self._security_identifier_is_global_binding(expr_node)
|
|
3370
|
+
and expr_node.name in global_expr_map
|
|
3371
|
+
and expr_node.name not in resolving
|
|
3372
|
+
):
|
|
2090
3373
|
resolving.add(expr_node.name)
|
|
2091
3374
|
resolved = self._build_security_expr(
|
|
2092
3375
|
sec_id,
|
|
@@ -2095,7 +3378,7 @@ class SecurityEmitter:
|
|
|
2095
3378
|
ta_results,
|
|
2096
3379
|
resolving,
|
|
2097
3380
|
security_mutable_names,
|
|
2098
|
-
|
|
3381
|
+
(),
|
|
2099
3382
|
emitted_lines,
|
|
2100
3383
|
)
|
|
2101
3384
|
resolving.remove(expr_node.name)
|
|
@@ -2111,38 +3394,60 @@ class SecurityEmitter:
|
|
|
2111
3394
|
return resolved
|
|
2112
3395
|
|
|
2113
3396
|
if isinstance(expr_node, Subscript):
|
|
2114
|
-
index_cpp = self._build_security_expr(
|
|
2115
|
-
sec_id,
|
|
2116
|
-
expr_node.index,
|
|
2117
|
-
ta_range,
|
|
2118
|
-
ta_results,
|
|
2119
|
-
resolving,
|
|
2120
|
-
security_mutable_names,
|
|
2121
|
-
helper_binding_stack,
|
|
2122
|
-
emitted_lines,
|
|
2123
|
-
)
|
|
2124
3397
|
if isinstance(expr_node.object, Identifier):
|
|
2125
|
-
|
|
2126
|
-
if
|
|
3398
|
+
binding = None
|
|
3399
|
+
if not self._security_identifier_is_global_binding(expr_node.object):
|
|
3400
|
+
binding = self._security_lookup_helper_binding_context(
|
|
3401
|
+
expr_node.object.name, helper_binding_stack
|
|
3402
|
+
)
|
|
3403
|
+
if binding is not None:
|
|
3404
|
+
bound, bound_stack = binding
|
|
2127
3405
|
if isinstance(bound, str):
|
|
2128
3406
|
series_name = self._security_series_binding_target(bound)
|
|
2129
3407
|
if series_name is not None:
|
|
3408
|
+
index_cpp = self._build_security_expr(
|
|
3409
|
+
sec_id,
|
|
3410
|
+
expr_node.index,
|
|
3411
|
+
ta_range,
|
|
3412
|
+
ta_results,
|
|
3413
|
+
resolving,
|
|
3414
|
+
security_mutable_names,
|
|
3415
|
+
helper_binding_stack,
|
|
3416
|
+
emitted_lines,
|
|
3417
|
+
)
|
|
2130
3418
|
return f'_security_helper_series_["{series_name}"][{index_cpp}]'
|
|
2131
3419
|
return bound
|
|
2132
|
-
|
|
2133
|
-
|
|
3420
|
+
# Function parameters retain Pine's series identity. Apply
|
|
3421
|
+
# history to the supported bound bar series and compose
|
|
3422
|
+
# nested offsets before lowering. Unsupported scalar/
|
|
3423
|
+
# expression bindings fail closed in the shared helper.
|
|
3424
|
+
local_index = expr_node.index
|
|
3425
|
+
resolved_local_index = self._resolve_security_index_literal(
|
|
3426
|
+
expr_node.index, helper_binding_stack
|
|
3427
|
+
)
|
|
3428
|
+
if resolved_local_index is not None:
|
|
3429
|
+
local_index = NumberLiteral(value=resolved_local_index)
|
|
3430
|
+
composed = self._compose_security_helper_history_subscript(
|
|
2134
3431
|
bound,
|
|
3432
|
+
local_index,
|
|
3433
|
+
expr_node,
|
|
3434
|
+
)
|
|
3435
|
+
return self._build_security_expr(
|
|
3436
|
+
sec_id,
|
|
3437
|
+
composed,
|
|
2135
3438
|
ta_range,
|
|
2136
3439
|
ta_results,
|
|
2137
3440
|
resolving,
|
|
2138
3441
|
security_mutable_names,
|
|
2139
|
-
|
|
3442
|
+
bound_stack,
|
|
2140
3443
|
emitted_lines,
|
|
2141
3444
|
)
|
|
2142
|
-
return f"{obj_cpp}[{index_cpp}]"
|
|
2143
3445
|
if expr_node.object.name in SECURITY_BAR_FIELDS:
|
|
2144
3446
|
field = expr_node.object.name
|
|
2145
|
-
idx_lit = self.
|
|
3447
|
+
idx_lit = self._resolve_security_index_literal(
|
|
3448
|
+
expr_node.index,
|
|
3449
|
+
helper_binding_stack,
|
|
3450
|
+
)
|
|
2146
3451
|
if idx_lit is not None:
|
|
2147
3452
|
if idx_lit == 0:
|
|
2148
3453
|
return self._security_bar_field_expr(field)
|
|
@@ -2162,6 +3467,16 @@ class SecurityEmitter:
|
|
|
2162
3467
|
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
2163
3468
|
cpp_t = self._security_bar_hist_type(field)
|
|
2164
3469
|
current = self._security_bar_field_expr(field)
|
|
3470
|
+
index_cpp = self._build_security_expr(
|
|
3471
|
+
sec_id,
|
|
3472
|
+
expr_node.index,
|
|
3473
|
+
ta_range,
|
|
3474
|
+
ta_results,
|
|
3475
|
+
resolving,
|
|
3476
|
+
security_mutable_names,
|
|
3477
|
+
helper_binding_stack,
|
|
3478
|
+
emitted_lines,
|
|
3479
|
+
)
|
|
2165
3480
|
return (
|
|
2166
3481
|
f"([&]() -> {cpp_t} {{ "
|
|
2167
3482
|
f"int _hidx = (int)({index_cpp}); "
|
|
@@ -2181,8 +3496,11 @@ class SecurityEmitter:
|
|
|
2181
3496
|
# helper binding, whichever the resolved expression turns out
|
|
2182
3497
|
# to be) instead of duplicating that dispatch here.
|
|
2183
3498
|
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
3499
|
+
if (
|
|
3500
|
+
self._security_identifier_is_global_binding(expr_node.object)
|
|
3501
|
+
and expr_node.object.name in global_expr_map
|
|
3502
|
+
and expr_node.object.name not in resolving
|
|
3503
|
+
):
|
|
2186
3504
|
resolving.add(expr_node.object.name)
|
|
2187
3505
|
resolved = self._build_security_expr(
|
|
2188
3506
|
sec_id,
|
|
@@ -2203,6 +3521,16 @@ class SecurityEmitter:
|
|
|
2203
3521
|
meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
|
|
2204
3522
|
hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
|
|
2205
3523
|
cpp_t = meta["type"] if meta else "double"
|
|
3524
|
+
index_cpp = self._build_security_expr(
|
|
3525
|
+
sec_id,
|
|
3526
|
+
expr_node.index,
|
|
3527
|
+
ta_range,
|
|
3528
|
+
ta_results,
|
|
3529
|
+
resolving,
|
|
3530
|
+
security_mutable_names,
|
|
3531
|
+
helper_binding_stack,
|
|
3532
|
+
emitted_lines,
|
|
3533
|
+
)
|
|
2206
3534
|
inner = self._build_security_expr(
|
|
2207
3535
|
sec_id,
|
|
2208
3536
|
expr_node.object,
|
|
@@ -2234,14 +3562,72 @@ class SecurityEmitter:
|
|
|
2234
3562
|
# tick), so without a magnifier it advanced every chart bar and
|
|
2235
3563
|
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
2236
3564
|
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
2237
|
-
|
|
3565
|
+
# A top-level TA site reached through a global alias owns the
|
|
3566
|
+
# global variant even when the subscript index was authored
|
|
3567
|
+
# in a helper. Keep the helper stack for resolving that
|
|
3568
|
+
# index, but do not use it to select the TA object's state.
|
|
3569
|
+
ta_binding_stack = (
|
|
3570
|
+
()
|
|
3571
|
+
if getattr(ta_site, "owner_func", None) is None
|
|
3572
|
+
else helper_binding_stack
|
|
3573
|
+
)
|
|
3574
|
+
sig = self._security_binding_stack_signature(ta_binding_stack)
|
|
2238
3575
|
idx_lit = self._resolve_security_index_literal(
|
|
2239
3576
|
expr_node.index, helper_binding_stack
|
|
2240
3577
|
)
|
|
2241
3578
|
if idx_lit is None:
|
|
3579
|
+
resolved_input = self._resolve_security_immutable_input_int(
|
|
3580
|
+
expr_node.index, helper_binding_stack
|
|
3581
|
+
)
|
|
3582
|
+
if resolved_input is None:
|
|
3583
|
+
self._codegen_error(
|
|
3584
|
+
expr_node,
|
|
3585
|
+
"request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
|
|
3586
|
+
)
|
|
3587
|
+
input_node, input_stack = resolved_input
|
|
3588
|
+
index_cpp = self._build_security_expr(
|
|
3589
|
+
sec_id,
|
|
3590
|
+
input_node,
|
|
3591
|
+
ta_range,
|
|
3592
|
+
ta_results,
|
|
3593
|
+
resolving,
|
|
3594
|
+
security_mutable_names,
|
|
3595
|
+
input_stack,
|
|
3596
|
+
emitted_lines,
|
|
3597
|
+
)
|
|
3598
|
+
result_key = (idx, sig)
|
|
3599
|
+
if result_key not in ta_results:
|
|
3600
|
+
self._codegen_error(
|
|
3601
|
+
expr_node,
|
|
3602
|
+
"request.security multi-statement helper TA history is not supported",
|
|
3603
|
+
hint="Use a single-expression helper for TA history offsets.",
|
|
3604
|
+
)
|
|
3605
|
+
current = self._build_security_expr(
|
|
3606
|
+
sec_id,
|
|
3607
|
+
expr_node.object,
|
|
3608
|
+
ta_range,
|
|
3609
|
+
ta_results,
|
|
3610
|
+
resolving,
|
|
3611
|
+
security_mutable_names,
|
|
3612
|
+
ta_binding_stack,
|
|
3613
|
+
emitted_lines,
|
|
3614
|
+
)
|
|
3615
|
+
member_name = self._security_ta_variant_names.get(
|
|
3616
|
+
(sec_id, idx, sig),
|
|
3617
|
+
f"_sec{sec_id}_{ta_site.member_name}",
|
|
3618
|
+
)
|
|
3619
|
+
hist = self._security_ta_hist_series_cpp(member_name)
|
|
3620
|
+
return (
|
|
3621
|
+
"([&]() -> double { "
|
|
3622
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
3623
|
+
"if (is_na(_hidx) || _hidx < 0) return na<double>(); "
|
|
3624
|
+
f"return (_hidx == 0) ? {current} : {hist}[_hidx - 1]; "
|
|
3625
|
+
"}())"
|
|
3626
|
+
)
|
|
3627
|
+
if idx_lit < 0:
|
|
2242
3628
|
self._codegen_error(
|
|
2243
3629
|
expr_node,
|
|
2244
|
-
"request.security() TA history index must be
|
|
3630
|
+
"request.security() TA history index must be non-negative",
|
|
2245
3631
|
)
|
|
2246
3632
|
if idx_lit == 0:
|
|
2247
3633
|
# Current completed-HTF-bar value: reuse the bare-TA emission.
|
|
@@ -2252,9 +3638,15 @@ class SecurityEmitter:
|
|
|
2252
3638
|
ta_results,
|
|
2253
3639
|
resolving,
|
|
2254
3640
|
security_mutable_names,
|
|
2255
|
-
|
|
3641
|
+
ta_binding_stack,
|
|
2256
3642
|
emitted_lines,
|
|
2257
3643
|
)
|
|
3644
|
+
if (idx, sig) not in ta_results:
|
|
3645
|
+
self._codegen_error(
|
|
3646
|
+
expr_node,
|
|
3647
|
+
"request.security multi-statement helper TA history is not supported",
|
|
3648
|
+
hint="Use a single-expression helper for TA history offsets.",
|
|
3649
|
+
)
|
|
2258
3650
|
member_name = self._security_ta_variant_names.get(
|
|
2259
3651
|
(sec_id, idx, sig),
|
|
2260
3652
|
f"_sec{sec_id}_{ta_site.member_name}",
|
|
@@ -2300,6 +3692,28 @@ class SecurityEmitter:
|
|
|
2300
3692
|
)
|
|
2301
3693
|
return f"(({cond}) ? ({tv}) : ({fv}))"
|
|
2302
3694
|
|
|
3695
|
+
if isinstance(expr_node, TupleLiteral):
|
|
3696
|
+
# A tuple returned by a user helper must lower every element in the
|
|
3697
|
+
# requested context. Falling through to the ordinary expression
|
|
3698
|
+
# visitor loses AST-valued helper parameter bindings (for example
|
|
3699
|
+
# ``f(src) => [src, src + 1]``) and can emit an undeclared ``src``.
|
|
3700
|
+
# Recursive lowering also composes helper locals, HTF history, and
|
|
3701
|
+
# TA results for arbitrary supported tuple arity.
|
|
3702
|
+
elements = [
|
|
3703
|
+
self._build_security_expr(
|
|
3704
|
+
sec_id,
|
|
3705
|
+
element,
|
|
3706
|
+
ta_range,
|
|
3707
|
+
ta_results,
|
|
3708
|
+
resolving,
|
|
3709
|
+
security_mutable_names,
|
|
3710
|
+
helper_binding_stack,
|
|
3711
|
+
emitted_lines,
|
|
3712
|
+
)
|
|
3713
|
+
for element in expr_node.elements
|
|
3714
|
+
]
|
|
3715
|
+
return f"std::make_tuple({', '.join(elements)})"
|
|
3716
|
+
|
|
2303
3717
|
if isinstance(expr_node, FuncCall) and isinstance(expr_node.callee, Identifier):
|
|
2304
3718
|
func_name = expr_node.callee.name
|
|
2305
3719
|
if func_name in self._func_names:
|