@pineforge/codegen-pyodide 0.10.1 → 0.10.3
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/codegen/base.py +207 -19
- package/pineforge_codegen/codegen/emit_top.py +73 -20
- package/pineforge_codegen/codegen/security.py +160 -0
- package/pineforge_codegen/codegen/ta.py +569 -287
- package/pineforge_codegen/codegen/tables.py +140 -34
- package/pineforge_codegen/codegen/types.py +6 -1
- package/pineforge_codegen/codegen/visit_call.py +144 -72
- package/pineforge_codegen/codegen/visit_expr.py +12 -2
- package/pineforge_codegen/support_checker.py +68 -4
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
|
@@ -25,10 +25,11 @@ from __future__ import annotations
|
|
|
25
25
|
from typing import TYPE_CHECKING
|
|
26
26
|
|
|
27
27
|
from ..ast_nodes import (
|
|
28
|
-
Assignment, BinOp, BoolLiteral, ColorLiteral,
|
|
29
|
-
ForInStmt, ForStmt, FuncDef, Identifier,
|
|
30
|
-
NumberLiteral, StringLiteral, Subscript,
|
|
31
|
-
TupleLiteral, TypeDecl, UnaryOp, VarDecl,
|
|
28
|
+
ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, EnumDecl, ExprStmt,
|
|
29
|
+
FuncCall, ForInStmt, ForStmt, FuncDef, Identifier, IfStmt, MemberAccess,
|
|
30
|
+
MethodDef, NaLiteral, NumberLiteral, StringLiteral, Subscript, SwitchStmt,
|
|
31
|
+
Ternary, TupleAssign, TupleLiteral, TypeDecl, TypeField, UnaryOp, VarDecl,
|
|
32
|
+
WhileStmt,
|
|
32
33
|
)
|
|
33
34
|
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
|
|
34
35
|
|
|
@@ -200,18 +201,26 @@ class TaSiteHelper:
|
|
|
200
201
|
return False
|
|
201
202
|
return False
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
# Lazy-edge classification in every scope -- consumed by the precalc gate
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
_LAZY_SCOPE_STMT_TYPES = (
|
|
209
|
+
VarDecl, Assignment, TupleAssign, ExprStmt, IfStmt, ForStmt, ForInStmt,
|
|
210
|
+
WhileStmt, SwitchStmt, FuncDef, MethodDef, TypeDecl, EnumDecl,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _ta_call_nodes_by_lazy_scope(self) -> tuple[set[int], set[int]]:
|
|
214
|
+
"""Classify chart ``ta.*`` call nodes below Pine-v6 lazy edges, everywhere.
|
|
215
|
+
|
|
216
|
+
Returns ``(and_rhs, lazy_rhs)``: sites below an ``and`` RHS, and sites
|
|
217
|
+
below any lazy edge (an ``and``/``or`` RHS or a ``?:`` arm), walking the
|
|
218
|
+
top level, control-flow bodies, user-function bodies and UDT field
|
|
219
|
+
defaults. Entering a statement resets both flags, expression nodes
|
|
220
|
+
propagate them, and a ``request.security*`` payload is skipped because
|
|
221
|
+
its own evaluator lowers it. Consumed only by the block-scope opt-outs
|
|
222
|
+
in ``_ta_site_uses_precalc``; top-level statement operands are governed
|
|
223
|
+
by ``_lazy_edge_ta_hoist_plan`` instead.
|
|
215
224
|
"""
|
|
216
225
|
cached = getattr(self, "_lazy_scope_ta_call_nodes", None)
|
|
217
226
|
if cached is not None:
|
|
@@ -219,198 +228,78 @@ class TaSiteHelper:
|
|
|
219
228
|
|
|
220
229
|
and_rhs: set[int] = set()
|
|
221
230
|
lazy_rhs: set[int] = set()
|
|
222
|
-
plain_and_rhs: set[int] = set()
|
|
223
231
|
|
|
224
|
-
def
|
|
225
|
-
|
|
226
|
-
under_and_rhs: bool,
|
|
227
|
-
under_lazy_rhs: bool,
|
|
228
|
-
under_disallowed_shape: bool,
|
|
229
|
-
) -> None:
|
|
230
|
-
if expr is None:
|
|
232
|
+
def note(value, under_and: bool, under_lazy: bool) -> None:
|
|
233
|
+
if value is None:
|
|
231
234
|
return
|
|
232
|
-
if isinstance(
|
|
233
|
-
|
|
235
|
+
if isinstance(value, (list, tuple)):
|
|
236
|
+
for item in value:
|
|
237
|
+
note(item, under_and, under_lazy)
|
|
238
|
+
return
|
|
239
|
+
if isinstance(value, dict):
|
|
240
|
+
for item in value.values():
|
|
241
|
+
note(item, under_and, under_lazy)
|
|
242
|
+
return
|
|
243
|
+
if isinstance(value, TypeField):
|
|
244
|
+
note(value.default, False, False)
|
|
245
|
+
return
|
|
246
|
+
if not isinstance(value, ASTNode):
|
|
247
|
+
return
|
|
248
|
+
if isinstance(value, self._LAZY_SCOPE_STMT_TYPES):
|
|
249
|
+
for child in vars(value).values():
|
|
250
|
+
note(child, False, False)
|
|
251
|
+
return
|
|
252
|
+
if isinstance(value, FuncCall):
|
|
253
|
+
callee = value.callee
|
|
234
254
|
is_security = (
|
|
235
255
|
isinstance(callee, MemberAccess)
|
|
236
256
|
and isinstance(callee.object, Identifier)
|
|
237
257
|
and callee.object.name == "request"
|
|
238
258
|
and callee.member in ("security", "security_lower_tf")
|
|
239
259
|
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if under_lazy_rhs:
|
|
246
|
-
lazy_rhs.add(id(expr))
|
|
247
|
-
if under_and_rhs and not under_disallowed_shape:
|
|
248
|
-
plain_and_rhs.add(id(expr))
|
|
249
|
-
# A call target may itself be an evaluated expression, as in
|
|
250
|
-
# ``array.new_float(...).get(0)``. Walk the callee subtree so
|
|
251
|
-
# TA nested in a chained receiver inherits the surrounding
|
|
252
|
-
# lazy context. Plain identifiers and namespace receivers are
|
|
253
|
-
# leaves, so ordinary ``ta.sma`` / ``request.security`` calls
|
|
254
|
-
# remain unaffected here.
|
|
255
|
-
note_ta_calls(
|
|
256
|
-
callee,
|
|
257
|
-
under_and_rhs,
|
|
258
|
-
under_lazy_rhs,
|
|
259
|
-
under_disallowed_shape,
|
|
260
|
-
)
|
|
261
|
-
for idx, arg in enumerate(getattr(expr, "args", ()) or ()):
|
|
262
|
-
# The third request.security* argument is evaluated by its
|
|
263
|
-
# own security evaluator. Symbol, timeframe, and remaining
|
|
264
|
-
# options are chart-context expressions and must still be
|
|
265
|
-
# inspected for lazy chart TA.
|
|
260
|
+
# A chained receiver (``label.new(...).get_y()``) is an
|
|
261
|
+
# evaluated expression too; namespace/identifier callees are
|
|
262
|
+
# leaves.
|
|
263
|
+
note(callee, under_and, under_lazy)
|
|
264
|
+
for idx, arg in enumerate(getattr(value, "args", ()) or ()):
|
|
266
265
|
if is_security and idx == 2:
|
|
267
266
|
continue
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
under_and_rhs,
|
|
271
|
-
under_lazy_rhs,
|
|
272
|
-
under_disallowed_shape,
|
|
273
|
-
)
|
|
274
|
-
for key, value in (getattr(expr, "kwargs", None) or {}).items():
|
|
267
|
+
note(arg, under_and, under_lazy)
|
|
268
|
+
for key, kw_value in (getattr(value, "kwargs", None) or {}).items():
|
|
275
269
|
if is_security and key == "expression":
|
|
276
270
|
continue
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
note_ta_calls(
|
|
288
|
-
expr.left,
|
|
289
|
-
under_and_rhs,
|
|
290
|
-
under_lazy_rhs,
|
|
291
|
-
under_disallowed_shape,
|
|
292
|
-
)
|
|
293
|
-
note_ta_calls(
|
|
294
|
-
expr.right,
|
|
295
|
-
True,
|
|
296
|
-
True,
|
|
297
|
-
under_disallowed_shape,
|
|
298
|
-
)
|
|
299
|
-
elif expr.op == "or":
|
|
300
|
-
# ``or`` preserves an enclosing ``and`` scope, but its own
|
|
301
|
-
# RHS is a Pine-v6 lazy edge for the EMA classifier.
|
|
302
|
-
note_ta_calls(expr.left, under_and_rhs, under_lazy_rhs, True)
|
|
303
|
-
note_ta_calls(expr.right, under_and_rhs, True, True)
|
|
304
|
-
else:
|
|
305
|
-
note_ta_calls(
|
|
306
|
-
expr.left,
|
|
307
|
-
under_and_rhs,
|
|
308
|
-
under_lazy_rhs,
|
|
309
|
-
under_disallowed_shape,
|
|
310
|
-
)
|
|
311
|
-
note_ta_calls(
|
|
312
|
-
expr.right,
|
|
313
|
-
under_and_rhs,
|
|
314
|
-
under_lazy_rhs,
|
|
315
|
-
under_disallowed_shape,
|
|
316
|
-
)
|
|
317
|
-
return
|
|
318
|
-
if isinstance(expr, Ternary):
|
|
319
|
-
note_ta_calls(expr.condition, under_and_rhs, under_lazy_rhs, True)
|
|
320
|
-
note_ta_calls(expr.true_val, under_and_rhs, True, True)
|
|
321
|
-
note_ta_calls(expr.false_val, under_and_rhs, True, True)
|
|
322
|
-
return
|
|
323
|
-
if isinstance(expr, UnaryOp):
|
|
324
|
-
note_ta_calls(
|
|
325
|
-
expr.operand,
|
|
326
|
-
under_and_rhs,
|
|
327
|
-
under_lazy_rhs,
|
|
328
|
-
under_disallowed_shape,
|
|
329
|
-
)
|
|
330
|
-
return
|
|
331
|
-
if isinstance(expr, (MemberAccess, Subscript)):
|
|
332
|
-
note_ta_calls(
|
|
333
|
-
getattr(expr, "object", None),
|
|
334
|
-
under_and_rhs,
|
|
335
|
-
under_lazy_rhs,
|
|
336
|
-
under_disallowed_shape,
|
|
337
|
-
)
|
|
338
|
-
note_ta_calls(
|
|
339
|
-
getattr(expr, "index", None),
|
|
340
|
-
under_and_rhs,
|
|
341
|
-
under_lazy_rhs,
|
|
342
|
-
under_disallowed_shape,
|
|
343
|
-
)
|
|
344
|
-
return
|
|
345
|
-
if isinstance(expr, TupleLiteral):
|
|
346
|
-
for elem in expr.elements:
|
|
347
|
-
note_ta_calls(
|
|
348
|
-
elem,
|
|
349
|
-
under_and_rhs,
|
|
350
|
-
under_lazy_rhs,
|
|
351
|
-
under_disallowed_shape,
|
|
352
|
-
)
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
def walk_stmt(stmt) -> None:
|
|
356
|
-
if stmt is None:
|
|
357
|
-
return
|
|
358
|
-
if isinstance(stmt, VarDecl):
|
|
359
|
-
note_ta_calls(stmt.value, False, False, False)
|
|
360
|
-
return
|
|
361
|
-
if isinstance(stmt, ExprStmt):
|
|
362
|
-
note_ta_calls(
|
|
363
|
-
getattr(stmt, "value", None) or getattr(stmt, "expr", None),
|
|
364
|
-
False,
|
|
365
|
-
False,
|
|
366
|
-
False,
|
|
367
|
-
)
|
|
271
|
+
note(kw_value, under_and, under_lazy)
|
|
272
|
+
if (
|
|
273
|
+
isinstance(callee, MemberAccess)
|
|
274
|
+
and isinstance(callee.object, Identifier)
|
|
275
|
+
and callee.object.name == "ta"
|
|
276
|
+
):
|
|
277
|
+
if under_and:
|
|
278
|
+
and_rhs.add(id(value))
|
|
279
|
+
if under_lazy:
|
|
280
|
+
lazy_rhs.add(id(value))
|
|
368
281
|
return
|
|
369
|
-
if isinstance(
|
|
370
|
-
|
|
371
|
-
|
|
282
|
+
if isinstance(value, BinOp) and value.op == "and":
|
|
283
|
+
note(value.left, under_and, under_lazy)
|
|
284
|
+
note(value.right, True, True)
|
|
372
285
|
return
|
|
373
|
-
if isinstance(
|
|
374
|
-
|
|
286
|
+
if isinstance(value, BinOp) and value.op == "or":
|
|
287
|
+
note(value.left, under_and, under_lazy)
|
|
288
|
+
note(value.right, under_and, True)
|
|
375
289
|
return
|
|
376
|
-
if isinstance(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
)
|
|
290
|
+
if isinstance(value, Ternary):
|
|
291
|
+
note(value.condition, under_and, under_lazy)
|
|
292
|
+
note(value.true_val, under_and, True)
|
|
293
|
+
note(value.false_val, under_and, True)
|
|
381
294
|
return
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
child = getattr(stmt, attr, None)
|
|
385
|
-
if child is None:
|
|
386
|
-
continue
|
|
387
|
-
if isinstance(child, list):
|
|
388
|
-
for item in child:
|
|
389
|
-
if isinstance(item, (list, tuple)):
|
|
390
|
-
for sub in item:
|
|
391
|
-
walk_stmt(sub)
|
|
392
|
-
elif hasattr(item, "body") or hasattr(item, "name") or hasattr(item, "value"):
|
|
393
|
-
walk_stmt(item)
|
|
394
|
-
else:
|
|
395
|
-
note_ta_calls(item, False, False, False)
|
|
396
|
-
elif hasattr(child, "op") or hasattr(child, "args") or hasattr(child, "left"):
|
|
397
|
-
note_ta_calls(child, False, False, False)
|
|
398
|
-
elif hasattr(child, "body") or hasattr(child, "value") or hasattr(child, "condition"):
|
|
399
|
-
walk_stmt(child)
|
|
295
|
+
for child in vars(value).values():
|
|
296
|
+
note(child, under_and, under_lazy)
|
|
400
297
|
|
|
401
|
-
|
|
402
|
-
for stmt in getattr(ast, "body", ()) or ():
|
|
403
|
-
walk_stmt(stmt)
|
|
404
|
-
# User function bodies (original sites live here; clones share node ids
|
|
405
|
-
# only when they reuse the same FuncCall object — still best-effort).
|
|
298
|
+
note(getattr(self.ctx, "ast", None), False, False)
|
|
406
299
|
for finfo in getattr(self.ctx, "func_infos", None) or []:
|
|
407
|
-
|
|
408
|
-
body = getattr(node, "body", None) if node is not None else None
|
|
409
|
-
if body:
|
|
410
|
-
for stmt in body:
|
|
411
|
-
walk_stmt(stmt)
|
|
300
|
+
note(getattr(finfo, "node", None), False, False)
|
|
412
301
|
|
|
413
|
-
result = (and_rhs, lazy_rhs
|
|
302
|
+
result = (and_rhs, lazy_rhs)
|
|
414
303
|
self._lazy_scope_ta_call_nodes = result
|
|
415
304
|
return result
|
|
416
305
|
|
|
@@ -420,23 +309,83 @@ class TaSiteHelper:
|
|
|
420
309
|
def _ta_call_nodes_under_lazy_rhs(self) -> set[int]:
|
|
421
310
|
return self._ta_call_nodes_by_lazy_scope()[1]
|
|
422
311
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
312
|
+
# ------------------------------------------------------------------
|
|
313
|
+
# TradingView's per-family clocks below a top-level lazy edge
|
|
314
|
+
# ------------------------------------------------------------------
|
|
315
|
+
#
|
|
316
|
+
# Pinned 2026-09-03 with ``lab tv`` on NYSE:F 1D (range 2025-04-01 ..
|
|
317
|
+
# 2026-05-01; cadence-7 ternary probes ``v = bar_index % 7 == 3 ? <call> :
|
|
318
|
+
# na`` exposing the value through the entry size, plus lazy-``and``
|
|
319
|
+
# probes), each scored per call against three models:
|
|
320
|
+
#
|
|
321
|
+
# every-bar natives highest 9/9, sma 25/25, ema 23/23 -- the runtime
|
|
322
|
+
# advances the built-in on every chart bar; only the
|
|
323
|
+
# value is gated (``_lazy_edge_ta_hoist_plan``).
|
|
324
|
+
# hold-last source roc 38/38 (+39/39 entries), change 39/39, mom
|
|
325
|
+
# 39/39 -- TradingView computes these from the CALL'S
|
|
326
|
+
# OWN ``source[length]`` history: the source is
|
|
327
|
+
# written only on bars where the call executes, the
|
|
328
|
+
# last executed value is held on skipped bars, and
|
|
329
|
+
# the history is na before the first execution
|
|
330
|
+
# (call 1 of every value probe has no TV entry).
|
|
331
|
+
# every-bar 0/38..0/39, ring-of-executions 0..1/39.
|
|
332
|
+
# per-execution cum, barssince, valuewhen, cross, crossover 39/39,
|
|
333
|
+
# rising 39/39 (strictly monotonic over its executed
|
|
334
|
+
# samples) and math.sum 39/39 (na until three
|
|
335
|
+
# executed samples) -- the native only ever sees the
|
|
336
|
+
# samples of bars where the call executes, which is
|
|
337
|
+
# exactly the reached-only inline ``compute`` lowering
|
|
338
|
+
# (every-bar 0..31/39).
|
|
339
|
+
#
|
|
340
|
+
# The hoist is an ALLOW-LIST and requires a direct ``[k]`` read. A broad
|
|
341
|
+
# every-bar hoist of every stateful family measured on Cloud Run against
|
|
342
|
+
# the full population (2026-09-04, like-for-like vs the same lab bundle)
|
|
343
|
+
# fixed the pinned shapes but cost 170 tiers / 30 hard-lane regressions,
|
|
344
|
+
# and hoisting allow-listed families WITHOUT a history read still broke
|
|
345
|
+
# four ETH hard-lane probes that main matched at 100%: ``adxVal > 30 and
|
|
346
|
+
# adxVal > ta.sma(adxVal, 7)`` (louislapis9), ``volume < ta.sma(volume,
|
|
347
|
+
# 20)`` under ``and`` (oliver1002 -- the original pf-probe-oliver-dual-
|
|
348
|
+
# vol-sma pin), ``ta.change(ta.sma(close, 50)) > 0`` under ``and``
|
|
349
|
+
# (ycelestine77), five ``ta.ema(close, 200)`` under ``or`` (quantbyboji),
|
|
350
|
+
# ``ta.highest(high, n) / entry > 1.05`` under ``and`` (miemomo3). Every
|
|
351
|
+
# every-bar tape (highest/sma/ema ``[1]`` under ``and``, the ternary
|
|
352
|
+
# ``ta.highest(high, 5)[1]``, robmagnaye) reads ``[k]`` on the call, and
|
|
353
|
+
# the bare twins of those tapes (2026-09-04, same cadence-7 probes) are
|
|
354
|
+
# per-execution: ``close > ta.sma(close, 5)`` ring 39/39 (every-bar 23),
|
|
355
|
+
# ``close > ta.ema(close, 5)`` ring 39/39 (26), ``? ta.sma(close, 5) : na``
|
|
356
|
+
# ring 39/39 (2, na until five executed samples), ``? ta.highest(high, 5)
|
|
357
|
+
# : na`` ring 38/39 (11; TV is na with one executed sample, then the
|
|
358
|
+
# partial maximum). So: TradingView keeps a lazily executed call on the
|
|
359
|
+
# reached-only clock unless the call's own history is referenced (``[k]``),
|
|
360
|
+
# in which case it materialises a continuous series and the call is
|
|
361
|
+
# evaluated on every bar. Only ``LAZY_EVERY_BAR_TA`` families
|
|
362
|
+
# (``lowest`` is ``highest``'s mirror) with such a read hoist.
|
|
363
|
+
# ``LAZY_SOURCE_CLOCK_TA`` routes to the hold-last clock;
|
|
364
|
+
# ``LAZY_PER_EXECUTION_TA`` (the seven taped families -- ``math.sum``
|
|
365
|
+
# 39/39 per-execution, na until three executed samples -- plus the exact
|
|
366
|
+
# mirrors ``crossunder``/``falling``) keeps the inline compute and never
|
|
367
|
+
# precalcs. Every other family keeps its existing lowering unchanged
|
|
368
|
+
# (inline compute when reached; precalc when static) until a tape pins
|
|
369
|
+
# it -- ``_lazy_edge_hoist_plan()["skipped"]`` lists them per script.
|
|
370
|
+
LAZY_EVERY_BAR_TA = frozenset({"highest", "lowest", "sma", "ema"})
|
|
371
|
+
LAZY_SOURCE_CLOCK_TA = frozenset({"change", "mom", "roc"})
|
|
372
|
+
LAZY_PER_EXECUTION_TA = frozenset({
|
|
373
|
+
"cum", "barssince", "valuewhen", "cross", "crossover", "crossunder",
|
|
374
|
+
"rising", "falling", "sum",
|
|
375
|
+
})
|
|
376
|
+
UNPINNED_LAZY_EDGE_REASON = (
|
|
377
|
+
"family not pinned every-bar by tape (LAZY_EVERY_BAR_TA allow-list): "
|
|
378
|
+
"existing lowering"
|
|
379
|
+
)
|
|
380
|
+
NO_HISTORY_READ_REASON = (
|
|
381
|
+
"allow-listed family without a direct [k] history read: TradingView "
|
|
382
|
+
"keeps the reached-only clock (louislapis9/oliver1002/miemomo3/"
|
|
383
|
+
"ycelestine77/quantbyboji exact on main, 2026-09-04)"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
_LAZY_SOURCE_CLOCK_BAR_SOURCES = frozenset({
|
|
387
|
+
"open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4",
|
|
388
|
+
})
|
|
440
389
|
|
|
441
390
|
def _script_has_user_binding(self, name: str) -> bool:
|
|
442
391
|
cache = getattr(self, "_user_binding_names", None)
|
|
@@ -460,44 +409,53 @@ class TaSiteHelper:
|
|
|
460
409
|
self._user_binding_names = cache
|
|
461
410
|
return name in cache
|
|
462
411
|
|
|
463
|
-
def
|
|
464
|
-
"""
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
functions, control-flow bodies, request.security evaluators,
|
|
470
|
-
aliases/other sources, named or dynamic lengths, shadowed built-ins,
|
|
471
|
-
and any surrounding ``or``/ternary shape remain on their existing eager
|
|
472
|
-
route. Comparator polarity is intentionally absent; two otherwise
|
|
473
|
-
identical callsites must not acquire different history semantics merely
|
|
474
|
-
because one compares above zero and one below it.
|
|
412
|
+
def _lazy_source_clock_chart_source(self, site: "TACallSite"):
|
|
413
|
+
"""The unshadowed bar-builtin source node, or None.
|
|
414
|
+
|
|
415
|
+
Between two executions closer than ``length`` bars the pin does not
|
|
416
|
+
distinguish the held read from #64's eager chart ``source[length]``;
|
|
417
|
+
the eager read is kept where the source is a plain chart builtin.
|
|
475
418
|
"""
|
|
476
|
-
|
|
419
|
+
source = site.compute_args[0] if site.compute_args else None
|
|
477
420
|
if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
or id(node) not in self._ta_call_nodes_under_plain_and_rhs()
|
|
482
|
-
or id(node) not in self._ta_call_nodes_in_top_level_var_values()
|
|
483
|
-
or self._script_has_user_binding("close")
|
|
484
|
-
or not isinstance(node, FuncCall)
|
|
485
|
-
or getattr(node, "kwargs", None)
|
|
486
|
-
or len(getattr(node, "args", ())) != 2
|
|
421
|
+
isinstance(source, Identifier)
|
|
422
|
+
and source.name in self._LAZY_SOURCE_CLOCK_BAR_SOURCES
|
|
423
|
+
and not self._script_has_user_binding(source.name)
|
|
487
424
|
):
|
|
425
|
+
return source
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _lazy_source_clock_length_node(node):
|
|
430
|
+
kwargs = getattr(node, "kwargs", None) or {}
|
|
431
|
+
if "length" in kwargs:
|
|
432
|
+
return kwargs["length"]
|
|
433
|
+
args = getattr(node, "args", ()) or ()
|
|
434
|
+
return args[1] if len(args) > 1 else None
|
|
435
|
+
|
|
436
|
+
@staticmethod
|
|
437
|
+
def _lazy_source_clock_length_literal(length_node) -> int | None:
|
|
438
|
+
if length_node is None:
|
|
439
|
+
return 1
|
|
440
|
+
if (
|
|
441
|
+
isinstance(length_node, NumberLiteral)
|
|
442
|
+
and not isinstance(length_node.value, bool)
|
|
443
|
+
and float(length_node.value) == int(length_node.value)
|
|
444
|
+
):
|
|
445
|
+
return int(length_node.value)
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
def _lazy_source_clock_eligible(self, site: "TACallSite") -> bool:
|
|
449
|
+
"""change/mom/roc with a numeric source; anything else keeps its lowering."""
|
|
450
|
+
if getattr(site, "owner_func", None) is not None or getattr(site, "returns_tuple", False):
|
|
488
451
|
return False
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
and source.name == "close"
|
|
493
|
-
and isinstance(length, NumberLiteral)
|
|
494
|
-
and not isinstance(length.value, bool)
|
|
495
|
-
and length.value == 3
|
|
496
|
-
)
|
|
452
|
+
if not site.compute_args:
|
|
453
|
+
return False
|
|
454
|
+
return self._infer_type(site.compute_args[0]) in ("double", "int", "int64_t")
|
|
497
455
|
|
|
498
|
-
def
|
|
499
|
-
"""
|
|
500
|
-
if hasattr(self, "
|
|
456
|
+
def _prepare_lazy_source_clock_sites(self) -> None:
|
|
457
|
+
"""Allocate one clock + one held-source Series per routed site (idempotent)."""
|
|
458
|
+
if hasattr(self, "_lazy_source_clock_by_node"):
|
|
501
459
|
return
|
|
502
460
|
|
|
503
461
|
def allocate(base: str, reserved: set[str]) -> str:
|
|
@@ -531,45 +489,70 @@ class TaSiteHelper:
|
|
|
531
489
|
reserved_members.add(f"{emitted}_cs{callsite}")
|
|
532
490
|
for instance in getattr(self, "_fresh_instances", ()):
|
|
533
491
|
reserved_members.add(instance["name"])
|
|
534
|
-
self.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
self.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
492
|
+
self._lazy_source_clock_type_name = allocate("_PFLazySourceClock", reserved_types)
|
|
493
|
+
|
|
494
|
+
clocks: dict[int, dict] = {}
|
|
495
|
+
routed = self._lazy_edge_ta_hoist_plan()["source_clock"]
|
|
496
|
+
for index, (node_id, info) in enumerate(routed.items(), start=1):
|
|
497
|
+
chart_source = self._lazy_source_clock_chart_source(info["site"])
|
|
498
|
+
clocks[node_id] = {
|
|
499
|
+
"clock": allocate(f"_pf_lazy_src_clock_{index}", reserved_members),
|
|
500
|
+
"hist": allocate(f"_pf_lazy_src_hist_{index}", reserved_members),
|
|
501
|
+
"chart": (
|
|
502
|
+
allocate(f"_pf_lazy_src_chart_{index}", reserved_members)
|
|
503
|
+
if chart_source is not None
|
|
504
|
+
else None
|
|
505
|
+
),
|
|
506
|
+
"chart_source": chart_source,
|
|
507
|
+
"site": info["site"],
|
|
508
|
+
"node": info["node"],
|
|
509
|
+
"length_literal": info["length_literal"],
|
|
510
|
+
}
|
|
511
|
+
self._lazy_source_clock_by_node = clocks
|
|
512
|
+
|
|
513
|
+
def _lazy_source_clock_expr(self, site: "TACallSite", node) -> str:
|
|
514
|
+
"""Lower a reached change/mom/roc site through its hold-last source clock."""
|
|
515
|
+
info = self._lazy_source_clock_by_node[id(node)]
|
|
516
|
+
source = self._visit_expr(site.compute_args[0])
|
|
517
|
+
length_node = self._lazy_source_clock_length_node(node)
|
|
518
|
+
literal = self._lazy_source_clock_length_literal(length_node)
|
|
519
|
+
chart = info["chart"]
|
|
520
|
+
if literal is not None:
|
|
521
|
+
length_expr = str(literal)
|
|
522
|
+
held = f"{info['hist']}[{literal - 1}]" if literal >= 1 else "na<double>()"
|
|
523
|
+
eager = f"{chart}[{literal}]" if chart is not None else held
|
|
524
|
+
else:
|
|
525
|
+
length_expr = f"(int)({self._visit_expr(length_node)})"
|
|
526
|
+
held = (
|
|
527
|
+
f"(({length_expr}) >= 1 ? {info['hist']}[({length_expr}) - 1] "
|
|
528
|
+
f": na<double>())"
|
|
529
|
+
)
|
|
530
|
+
eager = f"{chart}[{length_expr}]" if chart is not None else held
|
|
531
|
+
previous = (
|
|
532
|
+
f"{info['clock']}.previous_source({held}, {eager}, {length_expr}, "
|
|
533
|
+
f"bar_index_)"
|
|
563
534
|
)
|
|
535
|
+
method = "roc" if self._ta_name_from_site(site) == "roc" else "change"
|
|
536
|
+
return f"{info['clock']}.{method}({source}, {previous})"
|
|
564
537
|
|
|
565
|
-
def
|
|
538
|
+
def _emit_lazy_source_clock_helper(self, lines: list[str]) -> None:
|
|
566
539
|
"""Emit the value-copyable generated runtime helper when needed."""
|
|
567
|
-
self.
|
|
568
|
-
if not self.
|
|
540
|
+
self._prepare_lazy_source_clock_sites()
|
|
541
|
+
if not self._lazy_source_clock_by_node:
|
|
569
542
|
return
|
|
570
|
-
type_name = self.
|
|
543
|
+
type_name = self._lazy_source_clock_type_name
|
|
571
544
|
lines.extend(
|
|
572
545
|
[
|
|
546
|
+
"// TradingView keeps a lazily executed call's `source[k]` history per",
|
|
547
|
+
"// call: the source is written only on bars where the call executes,",
|
|
548
|
+
"// the last executed value is held on the bars it skips, and the",
|
|
549
|
+
"// history is na before the first execution. The paired hist Series",
|
|
550
|
+
"// holds `bar_base_source` (the value committed by earlier bars) once",
|
|
551
|
+
"// per chart bar, so `hist[length - 1]` is the source at the most",
|
|
552
|
+
"// recent execution at or before bar-length. Between two executions",
|
|
553
|
+
"// closer than `length` bars the tapes do not distinguish that read",
|
|
554
|
+
"// from the eager chart `source[length]`, so the #64 eager read is",
|
|
555
|
+
"// kept there for chart-builtin sources.",
|
|
573
556
|
f"struct {type_name} {{",
|
|
574
557
|
" double committed_source = na<double>();",
|
|
575
558
|
" int committed_bar = -1;",
|
|
@@ -585,22 +568,40 @@ class TaSiteHelper:
|
|
|
585
568
|
" working_bar = -1;",
|
|
586
569
|
" }",
|
|
587
570
|
"",
|
|
588
|
-
"
|
|
571
|
+
" // Once per bar before the script body; a same-bar recalculation",
|
|
572
|
+
" // keeps the base frozen so the evaluation stays idempotent.",
|
|
573
|
+
" void begin_bar(int bar) {",
|
|
589
574
|
" if (working_bar != bar) {",
|
|
590
575
|
" bar_base_source = committed_source;",
|
|
591
576
|
" bar_base_bar = committed_bar;",
|
|
592
577
|
" working_bar = bar;",
|
|
593
578
|
" }",
|
|
594
|
-
"
|
|
595
|
-
"
|
|
596
|
-
"
|
|
597
|
-
"
|
|
598
|
-
" if (
|
|
599
|
-
"
|
|
579
|
+
" }",
|
|
580
|
+
"",
|
|
581
|
+
" double previous_source(double held, double eager, int length,",
|
|
582
|
+
" int bar) const {",
|
|
583
|
+
" if (bar_base_bar < 0 || length < 1) {",
|
|
584
|
+
" return na<double>();",
|
|
585
|
+
" }",
|
|
586
|
+
" return bar - bar_base_bar >= length ? held : eager;",
|
|
587
|
+
" }",
|
|
588
|
+
"",
|
|
589
|
+
" double change(double source, double previous) {",
|
|
590
|
+
" committed_source = source;",
|
|
591
|
+
" committed_bar = working_bar;",
|
|
592
|
+
" if (is_na(source) || is_na(previous)) {",
|
|
593
|
+
" return na<double>();",
|
|
600
594
|
" }",
|
|
595
|
+
" return source - previous;",
|
|
596
|
+
" }",
|
|
597
|
+
"",
|
|
598
|
+
" double roc(double source, double previous) {",
|
|
601
599
|
" committed_source = source;",
|
|
602
|
-
" committed_bar =
|
|
603
|
-
"
|
|
600
|
+
" committed_bar = working_bar;",
|
|
601
|
+
" if (is_na(source) || is_na(previous) || previous == 0.0) {",
|
|
602
|
+
" return na<double>();",
|
|
603
|
+
" }",
|
|
604
|
+
" return (source - previous) / previous * 100.0;",
|
|
604
605
|
" }",
|
|
605
606
|
"};",
|
|
606
607
|
"",
|
|
@@ -618,16 +619,39 @@ class TaSiteHelper:
|
|
|
618
619
|
all-``na``. Opting that site out preserves correctness; it simply uses
|
|
619
620
|
the ordinary stateful TA object during ``on_bar``.
|
|
620
621
|
|
|
621
|
-
A
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
622
|
+
A site hoisted to unconditional per-bar evaluation (top-level lazy
|
|
623
|
+
``and``/``or`` RHS or ternary arm -- see
|
|
624
|
+
``_lazy_edge_ta_hoist_plan``) advances on every chart bar by the pinned
|
|
625
|
+
TV rule, which is exactly what precalc computes; the lazy-edge opt-outs
|
|
626
|
+
below therefore never apply to it. A top-level lazy-edge site routed to
|
|
627
|
+
the hold-last source clock (change/mom/roc) or left on its
|
|
628
|
+
per-execution inline compute (cum/barssince/valuewhen/cross*/rising/...)
|
|
629
|
+
must never precalc.
|
|
630
|
+
|
|
631
|
+
Re-pinned 2026-09-03: the old opt-out for a chart ``ta.sma`` under an
|
|
632
|
+
``and`` RHS (pf-probe-oliver-dual-vol-sma, "eager precalc is
|
|
633
|
+
TV-incorrect") and the matching recursive ``ta.ema`` rule encoded a
|
|
634
|
+
per-call clock that ``lab tv`` refuted for top-level shapes (NYSE:F 1D,
|
|
635
|
+
``... and close > ta.sma(close,5)[1]``: every-bar 25/25 vs per-call 28;
|
|
636
|
+
``ta.ema``: 23/23 vs 27). The analyzer never marks a site inside an
|
|
637
|
+
``if``/loop/function scope static, so after hoisting the opt-outs
|
|
638
|
+
only reach a static lazy-edge ``ta.sma``/``ta.ema`` that is not a
|
|
639
|
+
top-level statement operand -- in practice a UDT field default
|
|
640
|
+
(``test_ta_precalc_walks_type_field_defaults``), whose clock is the
|
|
641
|
+
``Type.new()`` call site's and may sit in a block. Other TA families
|
|
625
642
|
and request.security sites retain their existing behavior."""
|
|
626
643
|
if not getattr(site, "is_static", False):
|
|
627
644
|
return False
|
|
628
|
-
if self._is_lazy_saturated_roc3_site(site):
|
|
629
|
-
return False
|
|
630
645
|
node = getattr(site, "node", None)
|
|
646
|
+
plan = self._lazy_edge_ta_hoist_plan()
|
|
647
|
+
if node is not None and (
|
|
648
|
+
id(node) in plan["source_clock"] or id(node) in plan["per_execution_nodes"]
|
|
649
|
+
):
|
|
650
|
+
# Hold-last / per-execution clocks: precalc would advance the site
|
|
651
|
+
# on every bar, which the tapes refute for these families.
|
|
652
|
+
return False
|
|
653
|
+
if node is not None and id(node) in plan["call_nodes"]:
|
|
654
|
+
return all(self._expr_safe_for_ta_precalc(arg) for arg in site.compute_args)
|
|
631
655
|
ta_name = self._ta_name_from_site(site)
|
|
632
656
|
if (
|
|
633
657
|
ta_name == "sma"
|
|
@@ -643,6 +667,264 @@ class TaSiteHelper:
|
|
|
643
667
|
return False
|
|
644
668
|
return all(self._expr_safe_for_ta_precalc(arg) for arg in site.compute_args)
|
|
645
669
|
|
|
670
|
+
# ------------------------------------------------------------------
|
|
671
|
+
# Every-bar hoisting of stateful ``ta.*`` sites below top-level lazy edges
|
|
672
|
+
# ------------------------------------------------------------------
|
|
673
|
+
#
|
|
674
|
+
# TradingView rule, pinned 2026-09-03 with ``lab tv`` on NYSE:F 1D (tapes
|
|
675
|
+
# out-pin-ring-lazyand / lazyand-sma / lazyand-ema / ring-ternary): a
|
|
676
|
+
# stateful ``ta.*`` call inside ANY expression operand of a top-level
|
|
677
|
+
# statement -- the RHS of a Pine-v6 lazy ``and``/``or``, either branch of a
|
|
678
|
+
# ternary, nested comparisons -- is evaluated on EVERY bar. Short-circuiting
|
|
679
|
+
# and branch selection gate only the *value*, never the built-in's state,
|
|
680
|
+
# and ``[1]`` on such a call is the previous BAR's value:
|
|
681
|
+
#
|
|
682
|
+
# c = bar_index % 7 == 3 and close > ta.highest(high,5)[1] 9/9 entries
|
|
683
|
+
# c = bar_index % 7 == 3 and close > ta.sma(close,5)[1] 25/25
|
|
684
|
+
# c = bar_index % 7 == 3 and close > ta.ema(close,5)[1] 23/23
|
|
685
|
+
# v = bar_index % 7 == 3 ? ta.highest(high,5)[1] : na 38/39
|
|
686
|
+
#
|
|
687
|
+
# (the per-call / "previous evaluation" model predicts 8/9, 28, 27 and
|
|
688
|
+
# 2/39). Production instance: robmagnaye14 ``bullMSS = setupAlive and
|
|
689
|
+
# dir == 1 and close > ta.highest(high, 10)[1]``.
|
|
690
|
+
#
|
|
691
|
+
# A stateful call INSIDE an ``if``/local-block/function body that does not
|
|
692
|
+
# execute every bar IS execution-gated on TV (pinned separately) and keeps
|
|
693
|
+
# the in-block compute + ``_hist_call_*`` push lowering untouched.
|
|
694
|
+
#
|
|
695
|
+
# Lowering: every eligible site below a lazy edge of a top-level statement
|
|
696
|
+
# is evaluated once, unconditionally, in a ``const auto _pf_every_bar_ta_N``
|
|
697
|
+
# local emitted BEFORE the statement (in dynamic mode too -- this is
|
|
698
|
+
# independent of ``_use_precalc``). A direct ``[k]`` on the hoisted call
|
|
699
|
+
# pushes its ``_hist_call_*`` Series there as well, so ``[1]`` reads the
|
|
700
|
+
# previous chart bar. The statement's expression then reads the local /
|
|
701
|
+
# the Series instead of stepping the indicator when the operand is reached.
|
|
702
|
+
|
|
703
|
+
_LAZY_EDGE_HOIST_NAME_PREFIX = "_pf_every_bar_ta_"
|
|
704
|
+
|
|
705
|
+
def _lazy_edge_hoist_block_reason(
|
|
706
|
+
self, site: "TACallSite", history_read: bool = False
|
|
707
|
+
) -> str | None:
|
|
708
|
+
"""Why an otherwise-eligible lazy-edge site is left on its lowering."""
|
|
709
|
+
if getattr(site, "owner_func", None) is not None:
|
|
710
|
+
return "site belongs to a user function body"
|
|
711
|
+
if getattr(site, "returns_tuple", False):
|
|
712
|
+
return "tuple-returning site"
|
|
713
|
+
family = self._ta_name_from_site(site)
|
|
714
|
+
if family in self.LAZY_PER_EXECUTION_TA:
|
|
715
|
+
return (
|
|
716
|
+
"per-execution native: the reached-only inline compute is "
|
|
717
|
+
"TradingView's clock (lab tv 2026-09-03)"
|
|
718
|
+
)
|
|
719
|
+
if family in self.LAZY_SOURCE_CLOCK_TA:
|
|
720
|
+
return "hold-last source-clock family with a non-numeric source"
|
|
721
|
+
if family not in self.LAZY_EVERY_BAR_TA:
|
|
722
|
+
return self.UNPINNED_LAZY_EDGE_REASON
|
|
723
|
+
if not history_read:
|
|
724
|
+
return self.NO_HISTORY_READ_REASON
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
def _lazy_edge_ta_hoist_plan(self) -> dict:
|
|
728
|
+
"""Plan (once) which top-level lazy-edge TA sites are hoisted.
|
|
729
|
+
|
|
730
|
+
Returns ``{"by_stmt": {id(stmt): [unit, ...]}, "call_nodes": set,
|
|
731
|
+
"source_clock": {id(node): {"node", "site", "length_literal"}},
|
|
732
|
+
"per_execution_nodes": set, "skipped": [(node, site, reason), ...]}``.
|
|
733
|
+
Only ``LAZY_EVERY_BAR_TA`` families whose call carries a direct
|
|
734
|
+
``[k]`` history read (``Subscript`` on the call) become hoist units.
|
|
735
|
+
``source_clock`` sites (change/mom/roc) lower through
|
|
736
|
+
``_lazy_source_clock_expr``; ``per_execution_nodes`` keep the inline
|
|
737
|
+
compute; both are opted out of precalc. Every other family is listed
|
|
738
|
+
in ``skipped`` with ``UNPINNED_LAZY_EDGE_REASON`` and keeps its
|
|
739
|
+
existing lowering. A hoist unit is either
|
|
740
|
+
``{"kind": "call", "node": FuncCall, "site": TACallSite, "name": str}``
|
|
741
|
+
or ``{"kind": "hist", "node": Subscript}`` (a direct ``[k]`` on a hoisted
|
|
742
|
+
call). Units are in evaluation order: a nested hoisted call precedes the
|
|
743
|
+
call whose argument it is, and a ``hist`` unit follows its call.
|
|
744
|
+
|
|
745
|
+
Only top-level statement expressions are scanned (``VarDecl`` values
|
|
746
|
+
except ``var``/``varip`` initializers, ``Assignment`` target/value,
|
|
747
|
+
``TupleAssign`` value, ``ExprStmt`` expression, and the head condition
|
|
748
|
+
of an ``IfStmt``). ``if``/``for``/``while``/``switch`` bodies, ``else
|
|
749
|
+
if`` conditions (Pine's ``else if`` is an ``if`` inside the ``else``
|
|
750
|
+
local block) and user-function bodies are never hoisted. Inside an
|
|
751
|
+
expression, the walk descends into every operand and call argument
|
|
752
|
+
except a ``request.security*`` payload, which its own evaluator runs.
|
|
753
|
+
Block-local arguments cannot occur at top level, so the skip list only
|
|
754
|
+
carries the shapes named in ``_lazy_edge_hoist_block_reason``.
|
|
755
|
+
"""
|
|
756
|
+
cached = getattr(self, "_lazy_edge_hoist_plan_cache", None)
|
|
757
|
+
if cached is not None:
|
|
758
|
+
return cached
|
|
759
|
+
|
|
760
|
+
by_stmt: dict[int, list[dict]] = {}
|
|
761
|
+
call_nodes: set[int] = set()
|
|
762
|
+
source_clock: dict[int, dict] = {}
|
|
763
|
+
per_execution_nodes: set[int] = set()
|
|
764
|
+
skipped: list[tuple] = []
|
|
765
|
+
counter = [0]
|
|
766
|
+
|
|
767
|
+
def scan(
|
|
768
|
+
expr, under_lazy: bool, units: list[dict], history_read: bool = False
|
|
769
|
+
) -> None:
|
|
770
|
+
if expr is None:
|
|
771
|
+
return
|
|
772
|
+
if isinstance(expr, FuncCall):
|
|
773
|
+
callee = expr.callee
|
|
774
|
+
is_security = (
|
|
775
|
+
isinstance(callee, MemberAccess)
|
|
776
|
+
and isinstance(callee.object, Identifier)
|
|
777
|
+
and callee.object.name == "request"
|
|
778
|
+
and callee.member in ("security", "security_lower_tf")
|
|
779
|
+
)
|
|
780
|
+
# Chained receivers (``label.new(...).get_y()``) are evaluated
|
|
781
|
+
# expressions too; namespace/identifier callees are leaves.
|
|
782
|
+
scan(callee, under_lazy, units)
|
|
783
|
+
for idx, arg in enumerate(getattr(expr, "args", ()) or ()):
|
|
784
|
+
if is_security and idx == 2:
|
|
785
|
+
continue
|
|
786
|
+
scan(arg, under_lazy, units)
|
|
787
|
+
for key, value in (getattr(expr, "kwargs", None) or {}).items():
|
|
788
|
+
if is_security and key == "expression":
|
|
789
|
+
continue
|
|
790
|
+
scan(value, under_lazy, units)
|
|
791
|
+
if not under_lazy:
|
|
792
|
+
return
|
|
793
|
+
site = self._get_ta_site(expr)
|
|
794
|
+
if site is None:
|
|
795
|
+
return
|
|
796
|
+
family = self._ta_name_from_site(site)
|
|
797
|
+
if family in self.LAZY_SOURCE_CLOCK_TA and self._lazy_source_clock_eligible(site):
|
|
798
|
+
source_clock[id(expr)] = {
|
|
799
|
+
"node": expr,
|
|
800
|
+
"site": site,
|
|
801
|
+
"length_literal": self._lazy_source_clock_length_literal(
|
|
802
|
+
self._lazy_source_clock_length_node(expr)
|
|
803
|
+
),
|
|
804
|
+
}
|
|
805
|
+
return
|
|
806
|
+
reason = self._lazy_edge_hoist_block_reason(site, history_read)
|
|
807
|
+
if reason is not None:
|
|
808
|
+
if family in self.LAZY_PER_EXECUTION_TA:
|
|
809
|
+
per_execution_nodes.add(id(expr))
|
|
810
|
+
skipped.append((expr, site, reason))
|
|
811
|
+
return
|
|
812
|
+
counter[0] += 1
|
|
813
|
+
units.append({
|
|
814
|
+
"kind": "call",
|
|
815
|
+
"node": expr,
|
|
816
|
+
"site": site,
|
|
817
|
+
"name": f"{self._LAZY_EDGE_HOIST_NAME_PREFIX}{counter[0]}",
|
|
818
|
+
})
|
|
819
|
+
call_nodes.add(id(expr))
|
|
820
|
+
return
|
|
821
|
+
if isinstance(expr, BinOp):
|
|
822
|
+
if expr.op in ("and", "or"):
|
|
823
|
+
scan(expr.left, under_lazy, units)
|
|
824
|
+
scan(expr.right, True, units)
|
|
825
|
+
else:
|
|
826
|
+
scan(expr.left, under_lazy, units)
|
|
827
|
+
scan(expr.right, under_lazy, units)
|
|
828
|
+
return
|
|
829
|
+
if isinstance(expr, Ternary):
|
|
830
|
+
scan(expr.condition, under_lazy, units)
|
|
831
|
+
scan(expr.true_val, True, units)
|
|
832
|
+
scan(expr.false_val, True, units)
|
|
833
|
+
return
|
|
834
|
+
if isinstance(expr, UnaryOp):
|
|
835
|
+
scan(expr.operand, under_lazy, units)
|
|
836
|
+
return
|
|
837
|
+
if isinstance(expr, Subscript):
|
|
838
|
+
# ``call(...)[k]``: the call's own history is referenced.
|
|
839
|
+
scan(expr.object, under_lazy, units, history_read=True)
|
|
840
|
+
scan(expr.index, under_lazy, units)
|
|
841
|
+
if isinstance(expr.object, FuncCall) and id(expr.object) in call_nodes:
|
|
842
|
+
units.append({"kind": "hist", "node": expr})
|
|
843
|
+
return
|
|
844
|
+
if isinstance(expr, MemberAccess):
|
|
845
|
+
scan(expr.object, under_lazy, units)
|
|
846
|
+
return
|
|
847
|
+
if isinstance(expr, TupleLiteral):
|
|
848
|
+
for element in expr.elements:
|
|
849
|
+
scan(element, under_lazy, units)
|
|
850
|
+
return
|
|
851
|
+
# ``x = if cond ... else ...`` / ``x = switch ...`` value forms: the
|
|
852
|
+
# head expression is top-level, the bodies are local blocks.
|
|
853
|
+
if isinstance(expr, IfStmt):
|
|
854
|
+
scan(expr.condition, under_lazy, units)
|
|
855
|
+
return
|
|
856
|
+
if isinstance(expr, SwitchStmt):
|
|
857
|
+
scan(expr.expr, under_lazy, units)
|
|
858
|
+
return
|
|
859
|
+
# Literals, identifiers and anything else are leaves.
|
|
860
|
+
|
|
861
|
+
def roots(stmt) -> list:
|
|
862
|
+
if isinstance(stmt, VarDecl):
|
|
863
|
+
if stmt.is_var or stmt.is_varip:
|
|
864
|
+
return []
|
|
865
|
+
return [stmt.value]
|
|
866
|
+
if isinstance(stmt, Assignment):
|
|
867
|
+
return [stmt.target, stmt.value]
|
|
868
|
+
if isinstance(stmt, TupleAssign):
|
|
869
|
+
return [stmt.value]
|
|
870
|
+
if isinstance(stmt, ExprStmt):
|
|
871
|
+
return [stmt.expr]
|
|
872
|
+
if isinstance(stmt, IfStmt):
|
|
873
|
+
return [stmt.condition]
|
|
874
|
+
return []
|
|
875
|
+
|
|
876
|
+
ast = getattr(self.ctx, "ast", None)
|
|
877
|
+
for stmt in getattr(ast, "body", ()) or ():
|
|
878
|
+
units: list[dict] = []
|
|
879
|
+
for root in roots(stmt):
|
|
880
|
+
scan(root, False, units)
|
|
881
|
+
if units:
|
|
882
|
+
by_stmt[id(stmt)] = units
|
|
883
|
+
|
|
884
|
+
plan = {
|
|
885
|
+
"by_stmt": by_stmt,
|
|
886
|
+
"call_nodes": call_nodes,
|
|
887
|
+
"source_clock": source_clock,
|
|
888
|
+
"per_execution_nodes": per_execution_nodes,
|
|
889
|
+
"skipped": skipped,
|
|
890
|
+
}
|
|
891
|
+
self._lazy_edge_hoist_plan_cache = plan
|
|
892
|
+
return plan
|
|
893
|
+
|
|
894
|
+
def _lazy_edge_hoisted_ta_call_nodes(self) -> set[int]:
|
|
895
|
+
return self._lazy_edge_ta_hoist_plan()["call_nodes"]
|
|
896
|
+
|
|
897
|
+
def _emit_lazy_edge_ta_hoists(self, stmt, lines: list[str], indent: int) -> None:
|
|
898
|
+
"""Emit the every-bar evaluations a top-level statement depends on.
|
|
899
|
+
|
|
900
|
+
Must be paired with ``_clear_lazy_edge_ta_hoists`` after the statement
|
|
901
|
+
is visited: the maps make ``_visit_func_call`` / ``_visit_subscript``
|
|
902
|
+
read the hoisted local / Series while the statement is lowered.
|
|
903
|
+
"""
|
|
904
|
+
units = self._lazy_edge_ta_hoist_plan()["by_stmt"].get(id(stmt))
|
|
905
|
+
if not units:
|
|
906
|
+
return
|
|
907
|
+
pad = " " * indent
|
|
908
|
+
lines.append(
|
|
909
|
+
f"{pad}// Pine v6 lazy operand: TA state advances every bar, only the "
|
|
910
|
+
"value is gated."
|
|
911
|
+
)
|
|
912
|
+
for unit in units:
|
|
913
|
+
node = unit["node"]
|
|
914
|
+
if unit["kind"] == "call":
|
|
915
|
+
rendered = self._visit_expr(node)
|
|
916
|
+
lines.append(f"{pad}const auto {unit['name']} = {rendered};")
|
|
917
|
+
self._hoisted_ta_values[id(node)] = unit["name"]
|
|
918
|
+
else:
|
|
919
|
+
member = self._inline_history_member("hist_call", node)
|
|
920
|
+
value = self._hoisted_ta_values[id(node.object)]
|
|
921
|
+
self._emit_history_series_write(lines, pad, member, value)
|
|
922
|
+
self._hoisted_hist_reads[id(node)] = member
|
|
923
|
+
|
|
924
|
+
def _clear_lazy_edge_ta_hoists(self) -> None:
|
|
925
|
+
self._hoisted_ta_values.clear()
|
|
926
|
+
self._hoisted_hist_reads.clear()
|
|
927
|
+
|
|
646
928
|
def _security_ta_compute_args_for_site(
|
|
647
929
|
self,
|
|
648
930
|
sec_id: int,
|