@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -5,6 +5,7 @@ results from AnalyzerContext instead of walking the AST to collect info.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
|
|
@@ -309,6 +310,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
309
310
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
310
311
|
self._func_var_members_set: set[str] = set()
|
|
311
312
|
self._precalc_loop_active: bool = False
|
|
313
|
+
# Top-level lazy-edge TA sites hoisted to every-bar evaluation for the
|
|
314
|
+
# statement currently being lowered: FuncCall id -> local name, and
|
|
315
|
+
# Subscript id -> ``_hist_call_*`` member (see ``ta.py``).
|
|
316
|
+
self._hoisted_ta_values: dict[int, str] = {}
|
|
317
|
+
self._hoisted_hist_reads: dict[int, str] = {}
|
|
312
318
|
# Names of ``var`` members that live in a callable scope (not global).
|
|
313
319
|
# Their exact declaration statements own initialization; they must not
|
|
314
320
|
# be initialized by the constructor or the global on_bar preamble.
|
|
@@ -514,6 +520,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
514
520
|
# the declaration-derived checkpoint inventory.
|
|
515
521
|
self._inline_history_members: list[dict] = []
|
|
516
522
|
self._inline_history_member_by_key: dict[tuple, str] = {}
|
|
523
|
+
# request.security/request.security_lower_tf inline their Pine helper
|
|
524
|
+
# expressions on the requested clock instead of calling the ordinary
|
|
525
|
+
# emitted UDF methods. The chart-clocked plain-UDF history factor must
|
|
526
|
+
# therefore exclude those source nodes and any emitted helper context
|
|
527
|
+
# reached only through them.
|
|
528
|
+
self._requested_context_inline_node_ids: set[int] = set()
|
|
529
|
+
self._requested_context_only_inline_contexts: set[
|
|
530
|
+
tuple[str, str | None]
|
|
531
|
+
] = set()
|
|
517
532
|
# Unique lambda-local names used when an array lowering references its
|
|
518
533
|
# receiver more than once. The binding keeps temporary-producing or
|
|
519
534
|
# side-effectful receivers single-evaluation (see TypeInferer).
|
|
@@ -925,6 +940,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
925
940
|
self._register_global_aggregate_member_types()
|
|
926
941
|
self._uses_map = self._detect_map_usage()
|
|
927
942
|
self._uses_matrix = self._detect_matrix_usage()
|
|
943
|
+
self._uses_pine_time = self._detect_pine_time_usage()
|
|
944
|
+
self._uses_vwap_anchor = self._detect_vwap_usage()
|
|
928
945
|
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
929
946
|
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
930
947
|
# byte-identical. Caps come from the strategy() header max_*_count.
|
|
@@ -2101,6 +2118,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2101
2118
|
return True
|
|
2102
2119
|
return False
|
|
2103
2120
|
|
|
2121
|
+
def _detect_pine_time_usage(self) -> bool:
|
|
2122
|
+
"""True if emitted C++ calls the engine's pine_time()/pine_time_close().
|
|
2123
|
+
|
|
2124
|
+
Gates the PF_PINE_TIME_SESSION_DAY_ARGS prelude shim (emit_top.py) so
|
|
2125
|
+
scripts that never call ``time(tf[, session[, tz]])`` /
|
|
2126
|
+
``time_close(...)`` stay byte-identical. A ``var`` initializer is
|
|
2127
|
+
also text-scanned, mirroring ``_detect_matrix_usage``.
|
|
2128
|
+
"""
|
|
2129
|
+
pat = re.compile(r"(?<![\w.])time(?:_close)?\s*\(")
|
|
2130
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2131
|
+
if init_str and pat.search(str(init_str)):
|
|
2132
|
+
return True
|
|
2133
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2134
|
+
if isinstance(node, FuncCall) and (node.args or node.kwargs):
|
|
2135
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2136
|
+
if ns is None and fn in ("time", "time_close"):
|
|
2137
|
+
return True
|
|
2138
|
+
return False
|
|
2139
|
+
|
|
2140
|
+
def _detect_vwap_usage(self) -> bool:
|
|
2141
|
+
"""True if emitted C++ calls ta::VWAP / ta::VWAPBands compute().
|
|
2142
|
+
|
|
2143
|
+
Gates the PF_VWAP_SESSION_ANCHOR_ARGS prelude shim (emit_top.py):
|
|
2144
|
+
``ta.vwap(...)`` calls, the bare ``ta.vwap`` property read, and
|
|
2145
|
+
``var`` initializers are covered.
|
|
2146
|
+
"""
|
|
2147
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2148
|
+
if init_str and "ta.vwap" in str(init_str):
|
|
2149
|
+
return True
|
|
2150
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2151
|
+
if isinstance(node, FuncCall):
|
|
2152
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2153
|
+
if ns == "ta" and fn == "vwap":
|
|
2154
|
+
return True
|
|
2155
|
+
elif isinstance(node, MemberAccess) and node.member == "vwap":
|
|
2156
|
+
if getattr(node.object, "name", None) == "ta":
|
|
2157
|
+
return True
|
|
2158
|
+
return False
|
|
2159
|
+
|
|
2104
2160
|
def _detect_generic_matrix_usage(self) -> bool:
|
|
2105
2161
|
"""True if emitted C++ mentions a non-float matrix specialization."""
|
|
2106
2162
|
float_spec = TypeSpec.primitive("float")
|
|
@@ -3173,7 +3229,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3173
3229
|
"""
|
|
3174
3230
|
self._inline_history_members = []
|
|
3175
3231
|
self._inline_history_member_by_key = {}
|
|
3176
|
-
counters = {
|
|
3232
|
+
counters = {
|
|
3233
|
+
"hist_call": 0,
|
|
3234
|
+
"series_arg": 0,
|
|
3235
|
+
"udf_series_arg": 0,
|
|
3236
|
+
}
|
|
3177
3237
|
|
|
3178
3238
|
def walk_nodes(value):
|
|
3179
3239
|
"""Yield AST nodes in stable field order, including tuple elements.
|
|
@@ -3345,6 +3405,113 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3345
3405
|
lexical.update(self._func_collection_types.get(owner, {}))
|
|
3346
3406
|
return lexical
|
|
3347
3407
|
|
|
3408
|
+
def plain_udf_info(call: FuncCall):
|
|
3409
|
+
if not isinstance(call.callee, Identifier):
|
|
3410
|
+
return None
|
|
3411
|
+
candidate = self._func_info_map.get(call.callee.name)
|
|
3412
|
+
if (
|
|
3413
|
+
candidate is None
|
|
3414
|
+
or candidate.node is None
|
|
3415
|
+
or getattr(candidate, "is_udt_method", False)
|
|
3416
|
+
):
|
|
3417
|
+
return None
|
|
3418
|
+
return candidate
|
|
3419
|
+
|
|
3420
|
+
# Requested-context UDF expressions are lowered independently by
|
|
3421
|
+
# security.py. Classify their otherwise-emitted helper variants so
|
|
3422
|
+
# this chart-clocked factor neither allocates unused buffers for them
|
|
3423
|
+
# nor changes the legacy bodies that remain in generated C++.
|
|
3424
|
+
requested_node_ids: set[int] = set()
|
|
3425
|
+
for security_call in self._security_calls:
|
|
3426
|
+
expression = security_call.get("expr_node")
|
|
3427
|
+
if expression is None:
|
|
3428
|
+
continue
|
|
3429
|
+
requested_node_ids.update(
|
|
3430
|
+
id(child) for child in walk_nodes(expression)
|
|
3431
|
+
)
|
|
3432
|
+
self._requested_context_inline_node_ids = requested_node_ids
|
|
3433
|
+
|
|
3434
|
+
def emitted_context_for_call(
|
|
3435
|
+
fi,
|
|
3436
|
+
call: FuncCall,
|
|
3437
|
+
owner: str | None,
|
|
3438
|
+
owner_context: str | None,
|
|
3439
|
+
) -> str | None:
|
|
3440
|
+
dispatch = self._instance_dispatch.get(
|
|
3441
|
+
(owner_context, id(call))
|
|
3442
|
+
)
|
|
3443
|
+
if dispatch is not None:
|
|
3444
|
+
return dispatch
|
|
3445
|
+
target_cs = target_cs_for_context(
|
|
3446
|
+
fi, call, owner, owner_context
|
|
3447
|
+
)
|
|
3448
|
+
if target_cs is not None:
|
|
3449
|
+
return f"{self._func_cpp_base_name(fi.name)}_cs{target_cs}"
|
|
3450
|
+
return None
|
|
3451
|
+
|
|
3452
|
+
requested_roots: set[tuple[str, str | None]] = set()
|
|
3453
|
+
chart_roots: set[tuple[str, str | None]] = set()
|
|
3454
|
+
context_edges: dict[
|
|
3455
|
+
tuple[str, str | None], set[tuple[str, str | None]]
|
|
3456
|
+
] = {}
|
|
3457
|
+
for call in (
|
|
3458
|
+
candidate
|
|
3459
|
+
for candidate in walk_nodes(self.ctx.ast)
|
|
3460
|
+
if isinstance(candidate, FuncCall)
|
|
3461
|
+
):
|
|
3462
|
+
fi = plain_udf_info(call)
|
|
3463
|
+
if fi is None:
|
|
3464
|
+
continue
|
|
3465
|
+
owner = owner_by_node.get(id(call))
|
|
3466
|
+
if id(call) in requested_node_ids:
|
|
3467
|
+
owner_contexts = (
|
|
3468
|
+
[None]
|
|
3469
|
+
if owner is None
|
|
3470
|
+
else self._inline_history_contexts_for_owner(owner)
|
|
3471
|
+
)
|
|
3472
|
+
for owner_context in owner_contexts:
|
|
3473
|
+
requested_roots.add((
|
|
3474
|
+
fi.name,
|
|
3475
|
+
emitted_context_for_call(
|
|
3476
|
+
fi, call, owner, owner_context
|
|
3477
|
+
),
|
|
3478
|
+
))
|
|
3479
|
+
continue
|
|
3480
|
+
if owner is None:
|
|
3481
|
+
target = (
|
|
3482
|
+
fi.name,
|
|
3483
|
+
emitted_context_for_call(fi, call, None, None),
|
|
3484
|
+
)
|
|
3485
|
+
chart_roots.add(target)
|
|
3486
|
+
continue
|
|
3487
|
+
for owner_context in self._inline_history_contexts_for_owner(owner):
|
|
3488
|
+
source = (owner, owner_context)
|
|
3489
|
+
target = (
|
|
3490
|
+
fi.name,
|
|
3491
|
+
emitted_context_for_call(
|
|
3492
|
+
fi, call, owner, owner_context
|
|
3493
|
+
),
|
|
3494
|
+
)
|
|
3495
|
+
context_edges.setdefault(source, set()).add(target)
|
|
3496
|
+
|
|
3497
|
+
def reachable(
|
|
3498
|
+
roots: set[tuple[str, str | None]],
|
|
3499
|
+
) -> set[tuple[str, str | None]]:
|
|
3500
|
+
found = set(roots)
|
|
3501
|
+
pending = list(roots)
|
|
3502
|
+
while pending:
|
|
3503
|
+
source = pending.pop()
|
|
3504
|
+
for target in context_edges.get(source, ()):
|
|
3505
|
+
if target in found:
|
|
3506
|
+
continue
|
|
3507
|
+
found.add(target)
|
|
3508
|
+
pending.append(target)
|
|
3509
|
+
return found
|
|
3510
|
+
|
|
3511
|
+
self._requested_context_only_inline_contexts = (
|
|
3512
|
+
reachable(requested_roots) - reachable(chart_roots)
|
|
3513
|
+
)
|
|
3514
|
+
|
|
3348
3515
|
for node in walk_nodes(self.ctx.ast):
|
|
3349
3516
|
owner = owner_by_node.get(id(node))
|
|
3350
3517
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
@@ -3403,6 +3570,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3403
3570
|
expected_cpp_type = self._series_param_element_cpp_type(
|
|
3404
3571
|
fi, idx, target_cs
|
|
3405
3572
|
)
|
|
3573
|
+
# Pine gives each plain-UDF written call its own
|
|
3574
|
+
# chart-aligned parameter history. Even an exact Series
|
|
3575
|
+
# actual therefore needs a call-site-owned buffer: skipped
|
|
3576
|
+
# evaluations hold the last value in that bar's slot
|
|
3577
|
+
# instead of exposing the caller's independently changing
|
|
3578
|
+
# history. Typed methods retain their established direct
|
|
3579
|
+
# binding/bridge behavior below.
|
|
3580
|
+
chart_execution_context = (
|
|
3581
|
+
id(node) not in requested_node_ids
|
|
3582
|
+
and (owner, context)
|
|
3583
|
+
not in self._requested_context_only_inline_contexts
|
|
3584
|
+
)
|
|
3585
|
+
if not method_call and chart_execution_context:
|
|
3586
|
+
register_one(
|
|
3587
|
+
"udf_series_arg",
|
|
3588
|
+
(id(node), idx),
|
|
3589
|
+
expected_cpp_type,
|
|
3590
|
+
context,
|
|
3591
|
+
)
|
|
3592
|
+
continue
|
|
3406
3593
|
needs_bridge = True
|
|
3407
3594
|
if isinstance(arg, Identifier):
|
|
3408
3595
|
if (
|
|
@@ -3620,7 +3807,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3620
3807
|
# request.security helper-call results read at a history offset
|
|
3621
3808
|
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
3622
3809
|
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
3623
|
-
self.
|
|
3810
|
+
self._prepare_lazy_source_clock_sites()
|
|
3624
3811
|
|
|
3625
3812
|
lines: list[str] = []
|
|
3626
3813
|
|
|
@@ -3853,11 +4040,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3853
4040
|
)
|
|
3854
4041
|
lines.append("")
|
|
3855
4042
|
|
|
3856
|
-
#
|
|
3857
|
-
# script state themselves. Their
|
|
3858
|
-
#
|
|
3859
|
-
# checkpoint inventory.
|
|
3860
|
-
self.
|
|
4043
|
+
# Hold-last source clocks (change/mom/roc below a top-level lazy edge)
|
|
4044
|
+
# are generated support types, not script state themselves. Their
|
|
4045
|
+
# per-callsite instances are declared below inside GeneratedStrategy
|
|
4046
|
+
# and therefore join the automatic COOF checkpoint inventory.
|
|
4047
|
+
self._emit_lazy_source_clock_helper(lines)
|
|
3861
4048
|
|
|
3862
4049
|
# 2. Open class
|
|
3863
4050
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
@@ -4013,20 +4200,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4013
4200
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
4014
4201
|
lines.append(" bool _use_precalc = false;")
|
|
4015
4202
|
|
|
4016
|
-
for
|
|
4017
|
-
lines.append(
|
|
4018
|
-
f" {self._lazy_saturated_roc3_type_name} {clock_name};"
|
|
4019
|
-
)
|
|
4020
|
-
if self._lazy_saturated_roc3_clock_by_node:
|
|
4021
|
-
# Dedicated eager close[3] fallback. Its fixed four-slot capacity
|
|
4022
|
-
# is independent of the user's max_bars_back directive, which may
|
|
4023
|
-
# legitimately be smaller than the offset this generated route
|
|
4024
|
-
# requires. It is ordinary copyable script state and therefore
|
|
4025
|
-
# joins the automatic COOF checkpoint below.
|
|
4203
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
4026
4204
|
lines.append(
|
|
4027
|
-
"
|
|
4028
|
-
f"{self._lazy_saturated_roc3_history_name}{{4}};"
|
|
4205
|
+
f" {self._lazy_source_clock_type_name} {info['clock']};"
|
|
4029
4206
|
)
|
|
4207
|
+
# Held-source history on the chart clock. A literal length sizes
|
|
4208
|
+
# it exactly (independent of the user's max_bars_back directive,
|
|
4209
|
+
# which may legitimately be smaller than this generated route
|
|
4210
|
+
# needs); a runtime length uses the Series default capacity. Both
|
|
4211
|
+
# are ordinary copyable script state and join the automatic COOF
|
|
4212
|
+
# checkpoint below.
|
|
4213
|
+
literal = info["length_literal"]
|
|
4214
|
+
capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
|
|
4215
|
+
lines.append(f" Series<double> {info['hist']}{capacity};")
|
|
4216
|
+
if info["chart"] is not None:
|
|
4217
|
+
lines.append(f" Series<double> {info['chart']}{capacity};")
|
|
4030
4218
|
|
|
4031
4219
|
# Security evaluator TA members (cloned from expression dependencies)
|
|
4032
4220
|
# Skip for user function call expressions — their TA deps are internal to the function
|
|
@@ -136,6 +136,28 @@ class TopLevelEmitter:
|
|
|
136
136
|
lines.append('#include <pineforge/log.hpp>')
|
|
137
137
|
lines.append('#include <pineforge/str_utils.hpp>')
|
|
138
138
|
lines.append('#include <pineforge/session_time.hpp>')
|
|
139
|
+
# Feature-guarded argument tails (lab experiment cells build the
|
|
140
|
+
# candidate codegen against the BASE engine too): the symbol clock
|
|
141
|
+
# (syminfo tz + session) reaches time()/time_close() and ta.vwap only
|
|
142
|
+
# when the engine header advertises the overloads; otherwise the
|
|
143
|
+
# macros expand to nothing and the calls collapse to the legacy
|
|
144
|
+
# 5-arg pine_time / 3-arg VWAP::compute shapes (session filter in
|
|
145
|
+
# UTC, UTC-day VWAP anchor — the honest "factor absent" behaviour).
|
|
146
|
+
# Gated on the pre-scans so scripts without such calls stay
|
|
147
|
+
# byte-identical. The expansion carries no extra parentheses so the
|
|
148
|
+
# preprocessed call is byte-identical to the plain 7-arg form.
|
|
149
|
+
if getattr(self, "_uses_pine_time", False):
|
|
150
|
+
lines.append("#ifdef PF_PINE_TIME_HAS_SESSION_DAY")
|
|
151
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess) , tz, sess")
|
|
152
|
+
lines.append("#else")
|
|
153
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess)")
|
|
154
|
+
lines.append("#endif")
|
|
155
|
+
if getattr(self, "_uses_vwap_anchor", False):
|
|
156
|
+
lines.append("#ifdef PF_VWAP_HAS_SESSION_ANCHOR")
|
|
157
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess) , tz, sess")
|
|
158
|
+
lines.append("#else")
|
|
159
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess)")
|
|
160
|
+
lines.append("#endif")
|
|
139
161
|
if self._uses_matrix:
|
|
140
162
|
# Unconditional include when matrix API is used — do not gate on
|
|
141
163
|
# __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
|
|
@@ -912,28 +934,38 @@ class TopLevelEmitter:
|
|
|
912
934
|
|
|
913
935
|
# A GeneratedStrategy handle may execute multiple batch runs or
|
|
914
936
|
# streaming lifecycles. BacktestEngine resets broker/base state, but
|
|
915
|
-
# generated members survive. Reset
|
|
916
|
-
#
|
|
917
|
-
#
|
|
918
|
-
#
|
|
919
|
-
#
|
|
920
|
-
#
|
|
921
|
-
|
|
937
|
+
# generated members survive. Reset the hold-last source clocks
|
|
938
|
+
# (change/mom/roc below a top-level lazy edge) and their held-source
|
|
939
|
+
# Series at the first genuine slot of each lifecycle, then freeze this
|
|
940
|
+
# bar's base and record it once per chart bar. This lives in on_bar
|
|
941
|
+
# rather than a generated run wrapper because stream_begin() enters the
|
|
942
|
+
# base run path directly. COOF post-close recalculations have
|
|
943
|
+
# history_advances_new_bar()==false and therefore keep the same base.
|
|
944
|
+
if self._lazy_source_clock_by_node:
|
|
922
945
|
lines.append(
|
|
923
946
|
" if (history_advances_new_bar() && bar_index_ == 0) {"
|
|
924
947
|
)
|
|
925
|
-
for
|
|
926
|
-
lines.append(f" {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
)
|
|
948
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
949
|
+
lines.append(f" {info['clock']}.reset();")
|
|
950
|
+
lines.append(f" {info['hist']}.clear();")
|
|
951
|
+
if info["chart"] is not None:
|
|
952
|
+
lines.append(f" {info['chart']}.clear();")
|
|
930
953
|
lines.append(" }")
|
|
931
|
-
self.
|
|
932
|
-
lines
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
954
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
955
|
+
lines.append(f" {info['clock']}.begin_bar(bar_index_);")
|
|
956
|
+
self._emit_history_series_write(
|
|
957
|
+
lines,
|
|
958
|
+
" ",
|
|
959
|
+
info["hist"],
|
|
960
|
+
f"{info['clock']}.bar_base_source",
|
|
961
|
+
)
|
|
962
|
+
if info["chart"] is not None:
|
|
963
|
+
self._emit_history_series_write(
|
|
964
|
+
lines,
|
|
965
|
+
" ",
|
|
966
|
+
info["chart"],
|
|
967
|
+
self._visit_expr(info["chart_source"]),
|
|
968
|
+
)
|
|
937
969
|
|
|
938
970
|
# reset_run_state() owns engine/broker state, while these generated
|
|
939
971
|
# Series members belong to the strategy object. Clear all of them on
|
|
@@ -949,6 +981,21 @@ class TopLevelEmitter:
|
|
|
949
981
|
f"{info['member_name']}.clear();"
|
|
950
982
|
)
|
|
951
983
|
|
|
984
|
+
# A history-reading plain-UDF parameter advances on the chart clock,
|
|
985
|
+
# even when lazy control flow skips its written call on this bar. Seed
|
|
986
|
+
# the new slot with the prior current value (``na`` before first reach);
|
|
987
|
+
# an executed call later in the bar updates this same slot with its
|
|
988
|
+
# scalar actual. Typed-method ``series_arg`` bridges intentionally keep
|
|
989
|
+
# their existing execution-clock behavior.
|
|
990
|
+
for info in self._inline_history_members:
|
|
991
|
+
if info["kind"] != "udf_series_arg":
|
|
992
|
+
continue
|
|
993
|
+
member = info["member_name"]
|
|
994
|
+
lines.append(
|
|
995
|
+
f" if (history_advances_new_bar()) "
|
|
996
|
+
f"{member}.push({member}.current());"
|
|
997
|
+
)
|
|
998
|
+
|
|
952
999
|
# a. Push bar field series (with bar magnifier support)
|
|
953
1000
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
954
1001
|
push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
|
|
@@ -1181,9 +1228,15 @@ class TopLevelEmitter:
|
|
|
1181
1228
|
# an input (the default-sized construction already matches Pine).
|
|
1182
1229
|
self._emit_ta_runtime_reset(lines, indent=2)
|
|
1183
1230
|
|
|
1184
|
-
# d. Visit each statement
|
|
1231
|
+
# d. Visit each statement. A stateful ``ta.*`` site below a lazy
|
|
1232
|
+
# ``and``/``or`` RHS or ternary arm of a top-level statement is
|
|
1233
|
+
# evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
|
|
1185
1234
|
for stmt in self.ctx.ast.body:
|
|
1186
|
-
self.
|
|
1235
|
+
self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
|
|
1236
|
+
try:
|
|
1237
|
+
self._visit_stmt(stmt, lines, indent=2)
|
|
1238
|
+
finally:
|
|
1239
|
+
self._clear_lazy_edge_ta_hoists()
|
|
1187
1240
|
|
|
1188
1241
|
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
1189
1242
|
# reflect every assignment / strategy call made earlier in the
|
|
@@ -2844,6 +2844,156 @@ class SecurityEmitter:
|
|
|
2844
2844
|
)
|
|
2845
2845
|
return out
|
|
2846
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
|
+
|
|
2847
2997
|
def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
|
|
2848
2998
|
"""Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
|
|
2849
2999
|
``is_complete``.
|
|
@@ -2909,6 +3059,7 @@ class SecurityEmitter:
|
|
|
2909
3059
|
ta_indices = info.get("ta_indices") or []
|
|
2910
3060
|
security_mutable_names = set(info.get("mutable_globals", []))
|
|
2911
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)
|
|
2912
3063
|
|
|
2913
3064
|
lines.append(f" void _eval_security_{sec_id}(const Bar& bar, bool is_complete) {{")
|
|
2914
3065
|
|
|
@@ -2949,6 +3100,15 @@ class SecurityEmitter:
|
|
|
2949
3100
|
site = self.ctx.ta_call_sites[idx]
|
|
2950
3101
|
variants = (info.get("ta_variants") or {}).get(idx, [])
|
|
2951
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
|
|
2952
3112
|
helper_binding_stack = variant.get("binding_stack", ())
|
|
2953
3113
|
compute_args = self._security_ta_compute_args_for_site(
|
|
2954
3114
|
sec_id,
|