@pineforge/codegen-pyodide 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -39,6 +39,7 @@ from .tables import (
|
|
|
39
39
|
BAR_FIELDS,
|
|
40
40
|
BAR_BUILTINS,
|
|
41
41
|
BAR_SERIES_PUSH,
|
|
42
|
+
DRAWING_TYPE_TO_CPP,
|
|
42
43
|
SECURITY_OHLC_BAR_FIELDS,
|
|
43
44
|
TA_RETURNS_BOOL,
|
|
44
45
|
TA_IMPLICIT_COMPUTE,
|
|
@@ -52,6 +53,7 @@ from .tables import (
|
|
|
52
53
|
SKIP_VAR_TYPES,
|
|
53
54
|
SYMINFO_MEMBER_MAP,
|
|
54
55
|
COLOR_CONST_MAP,
|
|
56
|
+
ARRAY_NEW_CTORS,
|
|
55
57
|
ARRAY_METHODS,
|
|
56
58
|
MAP_METHODS,
|
|
57
59
|
MATRIX_METHODS,
|
|
@@ -64,6 +66,15 @@ from .tables import (
|
|
|
64
66
|
_merge_kwargs,
|
|
65
67
|
)
|
|
66
68
|
|
|
69
|
+
TA_TUPLE_RESULT_TYPES = {
|
|
70
|
+
"macd": "ta::MACDResult",
|
|
71
|
+
"supertrend": "ta::SupertrendResult",
|
|
72
|
+
"dmi": "ta::DMIResult",
|
|
73
|
+
"bb": "ta::BBResult",
|
|
74
|
+
"kc": "ta::KCResult",
|
|
75
|
+
"vwap_bands": "ta::VWAPBandsResult",
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
# (TA_IMPLICIT_COMPUTE / TA_COMPUTE_ARGS now imported from .tables above.)
|
|
68
79
|
|
|
69
80
|
# (TA_IMPLICIT_COMPUTE_FULL / TA_IMPLICIT_APPEND / PINE_TYPE_TO_CPP /
|
|
@@ -112,12 +123,17 @@ from .visit_stmt import StmtVisitor
|
|
|
112
123
|
from .visit_expr import ExprVisitor
|
|
113
124
|
from .visit_call import CallVisitor
|
|
114
125
|
|
|
126
|
+
# DrawingVisitor owns the drawing-objects-as-data dispatch (line/box/label/
|
|
127
|
+
# linefill/chart.point lowering onto the per-type arenas) plus _uses_drawing
|
|
128
|
+
# detection and arena-cap computation. See codegen/drawing.py.
|
|
129
|
+
from .drawing import DrawingVisitor
|
|
130
|
+
|
|
115
131
|
|
|
116
132
|
# ---------------------------------------------------------------------------
|
|
117
133
|
# CodeGen class
|
|
118
134
|
# ---------------------------------------------------------------------------
|
|
119
135
|
|
|
120
|
-
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, NamingHelper):
|
|
136
|
+
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
|
|
121
137
|
"""Generate C++ from an AnalyzerContext (visitor pattern).
|
|
122
138
|
|
|
123
139
|
Mixin chain (Python MRO is left-to-right; method names are
|
|
@@ -175,9 +191,41 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
175
191
|
self._func_cs_var_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
176
192
|
# Active var name remap (set during per-call-site function emission)
|
|
177
193
|
self._active_var_remap: dict[str, str] = {}
|
|
194
|
+
# When True (only while lowering a TA runtime-reset length expression
|
|
195
|
+
# through the expression visitor), an input-backed variable identifier
|
|
196
|
+
# renders as an override-aware ``get_input_*()`` read instead of its
|
|
197
|
+
# class member name. The reset can run in ``evaluate_security`` BEFORE
|
|
198
|
+
# the input members are initialised, so it must not depend on their
|
|
199
|
+
# init order. See ``_lower_reset_expr_via_visitor``.
|
|
200
|
+
self._reset_input_getter_mode: bool = False
|
|
178
201
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
179
202
|
self._func_var_members_set: set[str] = set()
|
|
203
|
+
# BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
|
|
204
|
+
# local initialised from a var/global UDT lvalue, mutated through, AND
|
|
205
|
+
# later rebound to a different lvalue). Member access lowers to ``->``
|
|
206
|
+
# and rebinds to ``&(...)``. Reset per function in _emit_func_def is not
|
|
207
|
+
# needed: names are function-unique and the value-copy fallback ignores
|
|
208
|
+
# entries for inactive functions.
|
|
209
|
+
self._udt_ptr_alias_locals: set[str] = set()
|
|
210
|
+
# Names of hoisted GLOBAL-scope UDT loop-locals bound from a UDT array
|
|
211
|
+
# element (``z = arr.get(i)``) and later field-mutated. Pine array
|
|
212
|
+
# elements of a user-defined type are references, so such a local must
|
|
213
|
+
# ALIAS the element, not value-copy — the mutation has to write back
|
|
214
|
+
# into the array. These are de-hoisted from the class-member value-copy
|
|
215
|
+
# to a fresh per-iteration ``UDT& z = arr[i];`` local reference (the same
|
|
216
|
+
# form the non-hoisted function-local alias path already emits). Read-only
|
|
217
|
+
# get-locals are NOT recorded (no field mutation) and keep value-copy
|
|
218
|
+
# semantics. Populated by _register_udt_array_get_ref_locals.
|
|
219
|
+
self._udt_array_get_ref_locals: set[str] = set()
|
|
180
220
|
self._precalc_loop_active: bool = False
|
|
221
|
+
# Names of ``var`` members that live in a FUNCTION scope (not global).
|
|
222
|
+
# These are initialized once-per-function-variant on first call (a
|
|
223
|
+
# function-local static equivalent), NOT in the constructor / on_bar
|
|
224
|
+
# preamble. See ``_emit_func_var_init_block``.
|
|
225
|
+
self._func_local_var_names: set[str] = set()
|
|
226
|
+
for _vlist in ctx.func_var_members.values():
|
|
227
|
+
for _n, _, _ in _vlist:
|
|
228
|
+
self._func_local_var_names.add(_n)
|
|
181
229
|
|
|
182
230
|
# Build per-function var/series name lists for cloning.
|
|
183
231
|
# For each function with call-site variants, collect ALL function-scoped
|
|
@@ -260,30 +308,91 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
260
308
|
|
|
261
309
|
# Build cloned site remapping for cs > 0 (must happen before _ta_site_map
|
|
262
310
|
# so cloned names are in _func_ta_members and get filtered out of the map)
|
|
311
|
+
#
|
|
312
|
+
# Default to the ``{orig}_cs{cs_idx}`` formula (matches the analyzer's clone
|
|
313
|
+
# naming), but defer to the analyzer's authoritative clone-name map for any
|
|
314
|
+
# site it had to disambiguate (a TA site reached through multiple enclosing
|
|
315
|
+
# functions would otherwise collide on the formula). Keeping the formula as
|
|
316
|
+
# the default leaves all non-colliding output byte-identical.
|
|
317
|
+
clone_names = getattr(ctx, "func_cs_ta_clone_names", {})
|
|
263
318
|
for fname, orig_names in func_ta_originals.items():
|
|
264
319
|
total_cs = ctx.func_call_site_counts.get(fname, 1)
|
|
265
320
|
for cs_idx in range(1, total_cs):
|
|
321
|
+
overrides = clone_names.get((fname, cs_idx), {})
|
|
266
322
|
remap = {}
|
|
267
323
|
for orig_name in orig_names:
|
|
268
|
-
remap[orig_name] =
|
|
324
|
+
remap[orig_name] = overrides.get(
|
|
325
|
+
orig_name, f"{orig_name}_cs{cs_idx}")
|
|
269
326
|
self._func_cs_ta_remap[(fname, cs_idx)] = remap
|
|
270
327
|
self._func_ta_members.update(remap.values())
|
|
271
328
|
|
|
272
329
|
for site in ctx.ta_call_sites:
|
|
273
330
|
if site.node is not None:
|
|
274
331
|
if site.member_name not in self._func_ta_members:
|
|
332
|
+
# Top-level (non-function) site: maps to itself.
|
|
275
333
|
self._ta_site_map[id(site.node)] = site
|
|
276
|
-
elif
|
|
277
|
-
#
|
|
334
|
+
elif id(site.node) not in self._ta_site_map:
|
|
335
|
+
# Function-local site. Multiple clones share the SAME AST
|
|
336
|
+
# node (clones copy ``node=orig.node``); the FIRST one in
|
|
337
|
+
# ``ta_call_sites`` order is the canonical original (cs0)
|
|
338
|
+
# whose ``member_name`` the per-call-site remap is keyed on.
|
|
339
|
+
# Later clones (``_cs{i}``, ``_cs{i}_cs{j}``, ``_u{n}`` …)
|
|
340
|
+
# must NOT overwrite it: doing so poisons the base name so
|
|
341
|
+
# the active-remap lookup misses and every clone collapses
|
|
342
|
+
# onto one member. Keep the original; the variant member is
|
|
343
|
+
# resolved via ``_active_ta_remap`` at emit time.
|
|
278
344
|
self._ta_site_map[id(site.node)] = site
|
|
279
345
|
self._ta_index_by_site_id: dict[int, int] = {
|
|
280
346
|
id(site): i for i, site in enumerate(ctx.ta_call_sites)
|
|
281
347
|
}
|
|
348
|
+
# Context-sensitive (call-path) instance machinery for nested stateful
|
|
349
|
+
# helpers. Built by ``_build_func_instances`` below. ``_current_instance_name``
|
|
350
|
+
# names the function clone whose body is currently being emitted (None at
|
|
351
|
+
# top level / non-variant bodies). ``_instance_dispatch`` maps
|
|
352
|
+
# ``(enclosing_instance_name, call_node_id) -> callee emit-name`` and is the
|
|
353
|
+
# authority for nested stateful-helper dispatch (see visit_call).
|
|
354
|
+
self._current_instance_name: str | None = None
|
|
355
|
+
self._instance_dispatch: dict[tuple[str | None, int], str] = {}
|
|
356
|
+
self._fresh_instances: list[dict] = []
|
|
357
|
+
self._fresh_var_members: list[tuple[str, str]] = []
|
|
358
|
+
# Fresh fixnan members for context-sensitive helper instances (nested
|
|
359
|
+
# helpers reached through >1 distinct call path). Each fresh instance
|
|
360
|
+
# gets its OWN previous-value member so two paths never share fixnan
|
|
361
|
+
# state. Populated by ``_build_func_instances``; declared in step 7.
|
|
362
|
+
self._fresh_fixnan_members: list[tuple[Any, str]] = []
|
|
363
|
+
# NOTE: _build_func_instances() runs at the top of generate() (it needs
|
|
364
|
+
# _all_member_names / _func_safe_name, which are populated later in __init__).
|
|
282
365
|
# Build lookup: node id -> FixnanCallSite (counter-based)
|
|
283
366
|
self._fixnan_counter = 0
|
|
367
|
+
# Per-call-site fixnan member remap for user functions (mirrors the TA
|
|
368
|
+
# remap): (func_name, cs_idx) -> {orig_member: cloned_member}.
|
|
369
|
+
self._func_cs_fixnan_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
370
|
+
# Active fixnan remap (set during per-call-site function emission).
|
|
371
|
+
self._active_fixnan_remap: dict[str, str] = {}
|
|
372
|
+
# node id -> original FixnanCallSite (the cs0 / source-level site).
|
|
373
|
+
self._fixnan_site_map: dict[int, Any] = {}
|
|
374
|
+
# Set of fixnan member names that belong to user functions (excluded
|
|
375
|
+
# from the site map so the active remap can dispatch per variant).
|
|
376
|
+
self._func_fixnan_members: set[str] = set()
|
|
377
|
+
# Dead fixnan site indices (owner is a dead user function). Skipped
|
|
378
|
+
# at declaration time so dead functions' fixnan state is not emitted.
|
|
379
|
+
self._dead_fixnan_indices: set[int] = set()
|
|
284
380
|
self._switch_counter = 0
|
|
285
381
|
self._security_inline_counter = 0
|
|
286
382
|
self._random_call_counter = 0
|
|
383
|
+
self._for_counter = 0
|
|
384
|
+
# Synthetic history buffers used by inline call-history and by scalar
|
|
385
|
+
# expressions passed to UDF series parameters. They are pre-registered
|
|
386
|
+
# at generate() time so declarations precede method emission, then
|
|
387
|
+
# addressed by (source node, emitted UDF variant). Each record is a
|
|
388
|
+
# real class-member Series and therefore joins _PFScriptState through
|
|
389
|
+
# the declaration-derived checkpoint inventory.
|
|
390
|
+
self._inline_history_members: list[dict] = []
|
|
391
|
+
self._inline_history_member_by_key: dict[tuple, str] = {}
|
|
392
|
+
# Unique lambda-local names used when an array lowering references its
|
|
393
|
+
# receiver more than once. The binding keeps temporary-producing or
|
|
394
|
+
# side-effectful receivers single-evaluation (see TypeInferer).
|
|
395
|
+
self._array_receiver_counter = 0
|
|
287
396
|
# UDT / enum (needed before _collect_known_vars for input.enum)
|
|
288
397
|
self._udt_defs: dict[str, list] = {}
|
|
289
398
|
self._enum_defs: dict[str, list[str]] = {}
|
|
@@ -308,10 +417,32 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
308
417
|
# Map input-backed var name -> its input.*() FuncCall node so we can
|
|
309
418
|
# later emit a runtime get_input_*() read with the same title/default.
|
|
310
419
|
self._input_var_to_call: dict[str, FuncCall] = {}
|
|
420
|
+
# Class-scope arithmetic-over-input vars (e.g. ``wilderLen = rsiLen*2-1``).
|
|
421
|
+
# Maps the derived var name -> its raw RHS expression string. When such a
|
|
422
|
+
# var feeds a TA ctor length, the runtime-reset path expands it so input
|
|
423
|
+
# overrides propagate (``(get_input_int("RSI Length",14) * 2 - 1)``); the
|
|
424
|
+
# ctor-init list still folds to the Pine-default literal via _resolve_known.
|
|
425
|
+
self._derived_input_expr: dict[str, str] = {}
|
|
311
426
|
self._timeframe_period_vars: set[str] = set()
|
|
427
|
+
# Names of class-scope vars whose value is a bar-invariant scalar —
|
|
428
|
+
# i.e. derived only from inputs, literals, ``timeframe.*`` members,
|
|
429
|
+
# ``math.*`` over stable args, and ternaries/casts/arithmetic over
|
|
430
|
+
# any of those. Such vars are safe to embed in a TA ctor runtime
|
|
431
|
+
# reset expression (they do not depend on per-bar series). Vars
|
|
432
|
+
# referencing series / ta.* results / history subscripts / strategy
|
|
433
|
+
# state are NOT here, so a TA length fed by them is still rejected
|
|
434
|
+
# by the constructor guard.
|
|
435
|
+
self._stable_runtime_vars: set[str] = set()
|
|
436
|
+
# ``_var_names`` (var/varip persistent-state members) is needed by the
|
|
437
|
+
# stability classifier during _collect_known_vars, so pre-seed it from
|
|
438
|
+
# the analyzer's var_members before that pass runs; the canonical
|
|
439
|
+
# assignment below preserves the existing initialization order.
|
|
440
|
+
self._var_names: set[str] = set()
|
|
441
|
+
for _vn, _, _ in ctx.var_members:
|
|
442
|
+
self._var_names.add(_vn)
|
|
312
443
|
self._collect_known_vars()
|
|
313
444
|
# Track var names
|
|
314
|
-
self._var_names
|
|
445
|
+
self._var_names = set()
|
|
315
446
|
for name, _, _ in ctx.var_members:
|
|
316
447
|
self._var_names.add(name)
|
|
317
448
|
# Every name bound ANYWHERE in the program (top-level, nested in
|
|
@@ -328,6 +459,87 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
328
459
|
for fi in ctx.func_infos:
|
|
329
460
|
self._func_names.add(fi.name)
|
|
330
461
|
self._func_info_map[fi.name] = fi
|
|
462
|
+
# Dead-code user functions: those that contain TA call sites but are
|
|
463
|
+
# never called anywhere in the script (no call site registered them,
|
|
464
|
+
# so func_call_site_counts reports 0). Their OWN TA ctor args still
|
|
465
|
+
# carry bare parameter names (e.g. ``dirmov_short(len) => ta.rma(ta.tr, len)``)
|
|
466
|
+
# that can never be resolved to a concrete length, and since the
|
|
467
|
+
# function never runs its TA buffers would be dead weight anyway.
|
|
468
|
+
# Track the dead TA site indices and dead function names so emission
|
|
469
|
+
# can skip both — the ctor guard no longer hard-fails on the bare
|
|
470
|
+
# param and no dangling member/function body is emitted. A function
|
|
471
|
+
# with zero call sites but NO TA state is NOT dead-by-this-rule (it
|
|
472
|
+
# may still be emitted; harmless if truly unreferenced).
|
|
473
|
+
#
|
|
474
|
+
# IMPORTANT: dead-ness of a TA site is decided by the site's
|
|
475
|
+
# ``owner_func`` (set by the analyzer), NOT by which function's
|
|
476
|
+
# ``func_ta_ranges`` slice the site happens to fall in. A function's
|
|
477
|
+
# slice can include clones of ANOTHER (live) function's sites that
|
|
478
|
+
# were minted while visiting THIS function's body (a nested call to
|
|
479
|
+
# a live callee registers the callee's cs{N} clones in the caller's
|
|
480
|
+
# TA-range slice). Keying dead-ness off the slice would drop those
|
|
481
|
+
# borrowed clones' declarations, leaving the owning callee's emitted
|
|
482
|
+
# clone body referencing undeclared members. Regression:
|
|
483
|
+
# quantbyboji-nq-hma-midday (``_ta_change_*_cs1`` / ``_ta_rma_*_cs1``
|
|
484
|
+
# minted inside dead ``adx_short``'s body but owned by live ``dirmov``).
|
|
485
|
+
self._dead_func_names: set[str] = set()
|
|
486
|
+
self._dead_ta_indices: set[int] = set()
|
|
487
|
+
for _fn in (ctx.func_ta_ranges or {}):
|
|
488
|
+
if (ctx.func_call_site_counts or {}).get(_fn, 0) > 0:
|
|
489
|
+
continue
|
|
490
|
+
# Only treat plain user functions (not UDT methods) as skippable
|
|
491
|
+
# dead code; methods are dispatched through the UDT and their
|
|
492
|
+
# call-site tracking is handled separately.
|
|
493
|
+
fi = self._func_info_map.get(_fn)
|
|
494
|
+
if fi is not None and getattr(fi, "is_udt_method", False):
|
|
495
|
+
continue
|
|
496
|
+
self._dead_func_names.add(_fn)
|
|
497
|
+
# Mark TA sites dead ONLY when their owner is a dead function. A
|
|
498
|
+
# site with ``owner_func=None`` (top-level) or whose owner is a
|
|
499
|
+
# live function survives -- even if it sits inside a dead
|
|
500
|
+
# function's TA-range slice (it's a borrowed clone).
|
|
501
|
+
for _i, _site in enumerate(ctx.ta_call_sites):
|
|
502
|
+
_owner = getattr(_site, "owner_func", None)
|
|
503
|
+
if _owner is not None and _owner in self._dead_func_names:
|
|
504
|
+
self._dead_ta_indices.add(_i)
|
|
505
|
+
# Build per-call-site fixnan remap + site map (mirrors TA remap above).
|
|
506
|
+
# Dead fixnan sites (owner is a dead function) are skipped at decl time.
|
|
507
|
+
clone_fn_names = getattr(ctx, "func_cs_fixnan_clone_names", {})
|
|
508
|
+
for _i, _fsite in enumerate(ctx.fixnan_sites):
|
|
509
|
+
_fowner = getattr(_fsite, "owner_func", None)
|
|
510
|
+
if _fowner is not None and _fowner in self._dead_func_names:
|
|
511
|
+
self._dead_fixnan_indices.add(_i)
|
|
512
|
+
# cs0 fixnan remap is identity (originals). Build originals per func.
|
|
513
|
+
func_fixnan_originals: dict[str, list[str]] = {}
|
|
514
|
+
for _fname, _idxs in (ctx.func_fixnan_indices or {}).items():
|
|
515
|
+
origs = [ctx.fixnan_sites[i].member_name for i in _idxs
|
|
516
|
+
if i not in self._dead_fixnan_indices]
|
|
517
|
+
if origs:
|
|
518
|
+
func_fixnan_originals[_fname] = origs
|
|
519
|
+
self._func_cs_fixnan_remap[(_fname, 0)] = {m: m for m in origs}
|
|
520
|
+
self._func_fixnan_members.update(origs)
|
|
521
|
+
# cs > 0 remap uses the ``{orig}_cs{cs_idx}`` formula (or the
|
|
522
|
+
# analyzer's disambiguated name from func_cs_fixnan_clone_names).
|
|
523
|
+
for _fname, _origs in func_fixnan_originals.items():
|
|
524
|
+
_total_cs = ctx.func_call_site_counts.get(_fname, 1)
|
|
525
|
+
for _cs_idx in range(1, _total_cs):
|
|
526
|
+
_overrides = clone_fn_names.get((_fname, _cs_idx), {})
|
|
527
|
+
_remap = {}
|
|
528
|
+
for _orig in _origs:
|
|
529
|
+
_remap[_orig] = _overrides.get(_orig, f"{_orig}_cs{_cs_idx}")
|
|
530
|
+
self._func_cs_fixnan_remap[(_fname, _cs_idx)] = _remap
|
|
531
|
+
self._func_fixnan_members.update(_remap.values())
|
|
532
|
+
# Site map: node id -> original site (cs0). Skip dead sites and
|
|
533
|
+
# function-local originals (the active remap dispatches variants).
|
|
534
|
+
for _i, _fsite in enumerate(ctx.fixnan_sites):
|
|
535
|
+
if _i in self._dead_fixnan_indices:
|
|
536
|
+
continue
|
|
537
|
+
if _fsite.node is None:
|
|
538
|
+
continue
|
|
539
|
+
if _fsite.member_name not in self._func_fixnan_members:
|
|
540
|
+
self._fixnan_site_map[id(_fsite.node)] = _fsite
|
|
541
|
+
elif id(_fsite.node) not in self._fixnan_site_map:
|
|
542
|
+
self._fixnan_site_map[id(_fsite.node)] = _fsite
|
|
331
543
|
# Track strategy series vars (e.g., strategy.closedtrades[1])
|
|
332
544
|
self._strategy_series_vars: set[str] = set()
|
|
333
545
|
# Track global-scope non-var declarations (emitted as class members)
|
|
@@ -337,6 +549,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
337
549
|
self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
|
|
338
550
|
self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
|
|
339
551
|
self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
|
|
552
|
+
# id(block_node) -> {raw_var_name: unique_member} for block-scoped var
|
|
553
|
+
# name collisions (see Analyzer._visit_VarDecl). Activated into
|
|
554
|
+
# ``_active_var_remap`` while emitting the owning block's statements.
|
|
555
|
+
self._block_var_renames: dict[int, dict[str, str]] = getattr(ctx, "block_var_renames", {}) or {}
|
|
340
556
|
self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = getattr(ctx, "udt_field_type_specs", {}) or {}
|
|
341
557
|
# Map UDT struct name -> set of field names that were dropped from the
|
|
342
558
|
# emitted C++ struct because they had drawing-only types (label, line,
|
|
@@ -347,7 +563,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
347
563
|
# rewrite or strip downstream references to those fields so the
|
|
348
564
|
# generated C++ never references a member that doesn't exist on the
|
|
349
565
|
# emitted struct. See: pineforge-codegen issue #10.
|
|
350
|
-
|
|
566
|
+
# Drawing-objects-as-data: line/box/label/linefill/chart.point are now
|
|
567
|
+
# REAL data (un-dropped from UDT structs). Only table/polyline stay
|
|
568
|
+
# dropped (no C++ representation). See drawing-objects-as-data.md §4.2.
|
|
569
|
+
_DRAWING_TYPES_INIT = {"table", "polyline"}
|
|
351
570
|
self._udt_omitted_fields: dict[str, set[str]] = {}
|
|
352
571
|
for _type_name, _fields in self._udt_defs.items():
|
|
353
572
|
_omitted = set()
|
|
@@ -359,12 +578,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
359
578
|
self._security_calls: list[dict] = [self._normalize_security_call(item) for item in ctx.security_calls]
|
|
360
579
|
# Current function parameter types (set during _emit_func_def)
|
|
361
580
|
self._current_func_param_types: dict[str, str] = {}
|
|
581
|
+
self._current_func_param_specs: dict[str, "TypeSpec"] = {}
|
|
362
582
|
# Current function params that are series (const Series<double>&)
|
|
363
583
|
self._current_func_series_params: set[str] = set()
|
|
364
584
|
# Locals declared in the function currently being emitted (symbol table loses them after analysis)
|
|
365
585
|
self._current_func_locals: set[str] = set()
|
|
586
|
+
self._current_func_local_types: dict[str, str] = {}
|
|
366
587
|
# for-in loop iterator names (must resolve member access, not enum fallback)
|
|
367
588
|
self._current_loop_vars: set[str] = set()
|
|
589
|
+
self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
|
|
368
590
|
# Track array variables for codegen
|
|
369
591
|
self._array_vars: set[str] = set()
|
|
370
592
|
# Track map variables for codegen
|
|
@@ -378,6 +600,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
378
600
|
self._map_vars.add(_name)
|
|
379
601
|
elif _spec.kind == "udt" and _spec.name:
|
|
380
602
|
self._udt_var_types.setdefault(_name, _spec.name)
|
|
603
|
+
# Table / polyline variables and params have NO C++ representation
|
|
604
|
+
# (SKIP_VAR_TYPES). A *method* call on such a receiver
|
|
605
|
+
# (``panel.cell(...)``, ``dash.merge_cells(...)``) is a visual no-op
|
|
606
|
+
# that must be dropped — but unlike the namespace form
|
|
607
|
+
# (``table.cell(...)``) the receiver is a bare var/param the
|
|
608
|
+
# namespace-based skip cannot see. Collect those names so
|
|
609
|
+
# ``_is_skip_expr`` can drop their method calls.
|
|
610
|
+
_SKIP_DECL_TYPES = set(SKIP_VAR_TYPES) | {"polyline"}
|
|
611
|
+
self._visual_drop_vars: set[str] = set()
|
|
612
|
+
for _node in self._walk_ast(self.ctx.ast):
|
|
613
|
+
if isinstance(_node, VarDecl):
|
|
614
|
+
if _node.type_hint in _SKIP_DECL_TYPES:
|
|
615
|
+
self._visual_drop_vars.add(_node.name)
|
|
616
|
+
elif isinstance(_node.value, FuncCall):
|
|
617
|
+
_fn, _ns = self._resolve_callee(_node.value.callee)
|
|
618
|
+
if _fn == "new" and _ns in _SKIP_DECL_TYPES:
|
|
619
|
+
self._visual_drop_vars.add(_node.name)
|
|
620
|
+
elif isinstance(_node, (FuncDef, MethodDef)):
|
|
621
|
+
_hints = (getattr(_node, "annotations", None) or {}).get("param_type_hints") or []
|
|
622
|
+
for _i, _p in enumerate(getattr(_node, "params", []) or []):
|
|
623
|
+
_h = _hints[_i] if _i < len(_hints) else None
|
|
624
|
+
if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
|
|
625
|
+
self._visual_drop_vars.add(_p)
|
|
381
626
|
# Collect request.security metadata per call
|
|
382
627
|
self._security_eval_info: list[dict] = []
|
|
383
628
|
self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
|
|
@@ -388,15 +633,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
388
633
|
lookahead_node = item.get("lookahead_node")
|
|
389
634
|
ta_range = item.get("ta_range")
|
|
390
635
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
and tf_node.name not in self._input_backed_vars):
|
|
397
|
-
val = self._known_vars[tf_node.name]
|
|
398
|
-
if isinstance(val, str):
|
|
399
|
-
tf_str = val
|
|
636
|
+
# Resolve the timeframe: a literal/const/global gives a static tf;
|
|
637
|
+
# a function-parameter tf is resolved from the call sites (the
|
|
638
|
+
# evaluator is a class method, so the param is not in scope there).
|
|
639
|
+
tf_str, tf_expr = self._resolve_security_tf(
|
|
640
|
+
tf_node, item.get("containing_func", ""))
|
|
400
641
|
|
|
401
642
|
is_lookahead_on = False
|
|
402
643
|
if lookahead_node is not None:
|
|
@@ -447,9 +688,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
447
688
|
self._security_eval_info.append({
|
|
448
689
|
"sec_id": sec_id,
|
|
449
690
|
"tf": tf_str,
|
|
691
|
+
"tf_expr": tf_expr,
|
|
450
692
|
"tf_node": tf_node,
|
|
451
693
|
"gaps_on": is_gaps_on,
|
|
452
694
|
"lookahead_on": is_lookahead_on,
|
|
695
|
+
"heikinashi": bool(item.get("heikinashi", False)),
|
|
453
696
|
"ta_range": ta_range,
|
|
454
697
|
"ta_indices": sorted(ta_indices),
|
|
455
698
|
"ta_binding_stacks": ta_binding_stacks,
|
|
@@ -467,7 +710,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
467
710
|
self._all_member_names.add(self._safe_name(name))
|
|
468
711
|
|
|
469
712
|
self._register_global_aggregate_member_types()
|
|
713
|
+
self._register_udt_array_get_ref_locals()
|
|
470
714
|
self._uses_matrix = self._detect_matrix_usage()
|
|
715
|
+
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
716
|
+
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
717
|
+
# byte-identical. Caps come from the strategy() header max_*_count.
|
|
718
|
+
self._uses_drawing = self._detect_drawing_usage()
|
|
719
|
+
self._drawing_caps = self._compute_drawing_caps() if self._uses_drawing else {}
|
|
471
720
|
|
|
472
721
|
# max_bars_back: the per-variable history depth the engine's Series<T>
|
|
473
722
|
# ring buffer should retain. Pine exposes this two ways — the
|
|
@@ -493,6 +742,225 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
493
742
|
# history reads off security-helper series.
|
|
494
743
|
self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
|
|
495
744
|
|
|
745
|
+
# ------------------------------------------------------------------
|
|
746
|
+
# Context-sensitive (call-path) instance machinery
|
|
747
|
+
# ------------------------------------------------------------------
|
|
748
|
+
def _iter_func_calls(self, root) -> list:
|
|
749
|
+
"""Collect every ``FuncCall`` node reachable from ``root`` (a stmt list
|
|
750
|
+
or single AST node). Order-independent; used by the instance pre-pass to
|
|
751
|
+
find nested user-function calls inside a function body."""
|
|
752
|
+
out: list = []
|
|
753
|
+
seen: set[int] = set()
|
|
754
|
+
stack: list = list(root) if isinstance(root, (list, tuple)) else [root]
|
|
755
|
+
while stack:
|
|
756
|
+
node = stack.pop()
|
|
757
|
+
if node is None:
|
|
758
|
+
continue
|
|
759
|
+
if isinstance(node, (list, tuple)):
|
|
760
|
+
stack.extend(node)
|
|
761
|
+
continue
|
|
762
|
+
if isinstance(node, dict):
|
|
763
|
+
stack.extend(node.values())
|
|
764
|
+
continue
|
|
765
|
+
if not hasattr(node, "__dict__"):
|
|
766
|
+
continue
|
|
767
|
+
nid = id(node)
|
|
768
|
+
if nid in seen:
|
|
769
|
+
continue
|
|
770
|
+
seen.add(nid)
|
|
771
|
+
if isinstance(node, FuncCall):
|
|
772
|
+
out.append(node)
|
|
773
|
+
for v in vars(node).values():
|
|
774
|
+
if isinstance(v, (list, tuple, dict)) or hasattr(v, "__dict__"):
|
|
775
|
+
stack.append(v)
|
|
776
|
+
return out
|
|
777
|
+
|
|
778
|
+
def _build_func_instances(self) -> None:
|
|
779
|
+
"""Context-sensitive cloning of nested stateful helper functions.
|
|
780
|
+
|
|
781
|
+
A stateful helper ``G`` (carrying TA state and/or ``var`` members) may be
|
|
782
|
+
reached through several distinct call paths — e.g. ``leg`` called from
|
|
783
|
+
three clones of ``f_get`` (lengths 10/20/30) *and* directly. Each path is
|
|
784
|
+
a logically-distinct instance that must drive its OWN TA/var members.
|
|
785
|
+
|
|
786
|
+
The analyzer already mints the per-path members (via range-widening), but
|
|
787
|
+
the flat ``{G}_cs{idx}`` clone namespace conflates a callee's own textual
|
|
788
|
+
call sites with the enclosing function's call sites. This pre-pass walks
|
|
789
|
+
the call graph from each natural clone and, for every nested stateful
|
|
790
|
+
call, composes the enclosing clone's active remap with the callee's
|
|
791
|
+
per-call-site remap:
|
|
792
|
+
|
|
793
|
+
composed_ta[m] = R_enclosing.get(R_callee_cs[m], R_callee_cs[m])
|
|
794
|
+
|
|
795
|
+
When the composition equals the callee's natural ``cs{j}`` remap the call
|
|
796
|
+
dispatches to the existing ``{G}_cs{j}`` clone (output stays byte-identical
|
|
797
|
+
for the common single-caller case). Otherwise a fresh instance is minted,
|
|
798
|
+
bound to the path-specific members (and FRESH ``var`` members so two paths
|
|
799
|
+
never share scalar state). ``_instance_dispatch`` records the resolved
|
|
800
|
+
emit-name per ``(enclosing_instance, call_node)``; ``_fresh_instances`` /
|
|
801
|
+
``_fresh_var_members`` carry the extra code to emit.
|
|
802
|
+
"""
|
|
803
|
+
ctx = self.ctx
|
|
804
|
+
stateful = (set(ctx.func_ta_ranges.keys())
|
|
805
|
+
| set(ctx.func_var_members.keys())
|
|
806
|
+
| set(ctx.func_series_vars.keys())
|
|
807
|
+
| set(ctx.func_fixnan_indices.keys())
|
|
808
|
+
| set(ctx.func_security_clone_only))
|
|
809
|
+
if not stateful:
|
|
810
|
+
return
|
|
811
|
+
|
|
812
|
+
func_bodies: dict[str, list] = {}
|
|
813
|
+
for fi in ctx.func_infos:
|
|
814
|
+
node = getattr(fi, "node", None)
|
|
815
|
+
if node is not None and getattr(node, "body", None):
|
|
816
|
+
func_bodies.setdefault(fi.name, node.body)
|
|
817
|
+
|
|
818
|
+
def ta_originals(fname: str) -> list[str]:
|
|
819
|
+
return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
|
|
820
|
+
|
|
821
|
+
def var_originals(fname: str) -> list[str]:
|
|
822
|
+
return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
|
|
823
|
+
|
|
824
|
+
def fixnan_originals(fname: str) -> list[str]:
|
|
825
|
+
return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
|
|
826
|
+
|
|
827
|
+
def natural_name(fname: str, cs_idx: int) -> str:
|
|
828
|
+
return f"{self._func_cpp_base_name(fname)}_cs{cs_idx}"
|
|
829
|
+
|
|
830
|
+
interned: dict[tuple, dict] = {}
|
|
831
|
+
worklist: list[dict] = []
|
|
832
|
+
seen_walk: set[str] = set()
|
|
833
|
+
fresh_counter = 0
|
|
834
|
+
|
|
835
|
+
# Seed with the natural clones the flat emission loop produces.
|
|
836
|
+
for fname in sorted(stateful):
|
|
837
|
+
if fname not in func_bodies:
|
|
838
|
+
continue
|
|
839
|
+
total_cs = ctx.func_call_site_counts.get(fname, 0)
|
|
840
|
+
if total_cs > 0:
|
|
841
|
+
for k in range(total_cs):
|
|
842
|
+
worklist.append({
|
|
843
|
+
"fname": fname,
|
|
844
|
+
"name": natural_name(fname, k),
|
|
845
|
+
"ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
|
|
846
|
+
"var_remap": self._func_cs_var_remap.get((fname, k), {}),
|
|
847
|
+
"fixnan_remap": self._func_cs_fixnan_remap.get((fname, k), {}),
|
|
848
|
+
})
|
|
849
|
+
else:
|
|
850
|
+
worklist.append({
|
|
851
|
+
"fname": fname,
|
|
852
|
+
"name": self._func_cpp_base_name(fname),
|
|
853
|
+
"ta_remap": {},
|
|
854
|
+
"var_remap": {},
|
|
855
|
+
"fixnan_remap": {},
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
while worklist:
|
|
859
|
+
inst = worklist.pop()
|
|
860
|
+
if inst["name"] in seen_walk:
|
|
861
|
+
continue
|
|
862
|
+
seen_walk.add(inst["name"])
|
|
863
|
+
body = func_bodies.get(inst["fname"])
|
|
864
|
+
if not body:
|
|
865
|
+
continue
|
|
866
|
+
active_ta = inst["ta_remap"]
|
|
867
|
+
active_fixnan = inst.get("fixnan_remap", {})
|
|
868
|
+
for callnode in self._iter_func_calls(body):
|
|
869
|
+
cs_info = ctx.func_call_cs_map.get(id(callnode))
|
|
870
|
+
if cs_info is None:
|
|
871
|
+
continue
|
|
872
|
+
g_name, j = cs_info
|
|
873
|
+
if g_name not in stateful:
|
|
874
|
+
continue
|
|
875
|
+
natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
|
|
876
|
+
composed_ta = {}
|
|
877
|
+
for m in ta_originals(g_name):
|
|
878
|
+
mid = natural_ta.get(m, m)
|
|
879
|
+
composed_ta[m] = active_ta.get(mid, mid)
|
|
880
|
+
natural_fixnan = self._func_cs_fixnan_remap.get((g_name, j), {})
|
|
881
|
+
composed_fixnan = {}
|
|
882
|
+
for m in fixnan_originals(g_name):
|
|
883
|
+
mid = natural_fixnan.get(m, m)
|
|
884
|
+
composed_fixnan[m] = active_fixnan.get(mid, mid)
|
|
885
|
+
if composed_ta == natural_ta and composed_fixnan == natural_fixnan:
|
|
886
|
+
# Path resolves to the callee's own cs{j} clone — reuse it.
|
|
887
|
+
self._instance_dispatch[(inst["name"], id(callnode))] = \
|
|
888
|
+
natural_name(g_name, j)
|
|
889
|
+
continue
|
|
890
|
+
key = (g_name, frozenset(composed_ta.items()),
|
|
891
|
+
frozenset(composed_fixnan.items()))
|
|
892
|
+
ginst = interned.get(key)
|
|
893
|
+
if ginst is None:
|
|
894
|
+
fresh_counter += 1
|
|
895
|
+
inst_name = f"{self._func_cpp_base_name(g_name)}__ni{fresh_counter}"
|
|
896
|
+
fvar_remap: dict[str, str] = {}
|
|
897
|
+
for v in var_originals(g_name):
|
|
898
|
+
fresh_member = f"{v}__ni{fresh_counter}"
|
|
899
|
+
fvar_remap[v] = fresh_member
|
|
900
|
+
self._fresh_var_members.append((v, fresh_member))
|
|
901
|
+
# Fresh fixnan members: each path gets its OWN previous-
|
|
902
|
+
# value member so two call paths never share fixnan state.
|
|
903
|
+
ffixnan_remap: dict[str, str] = {}
|
|
904
|
+
for orig_fn_member in fixnan_originals(g_name):
|
|
905
|
+
fresh_fn_member = f"{orig_fn_member}__ni{fresh_counter}"
|
|
906
|
+
ffixnan_remap[orig_fn_member] = fresh_fn_member
|
|
907
|
+
# Find the original FixnanCallSite to carry its type.
|
|
908
|
+
orig_fn_site = None
|
|
909
|
+
for _fs in ctx.fixnan_sites:
|
|
910
|
+
if _fs.member_name == orig_fn_member:
|
|
911
|
+
orig_fn_site = _fs
|
|
912
|
+
break
|
|
913
|
+
if orig_fn_site is not None:
|
|
914
|
+
self._fresh_fixnan_members.append(
|
|
915
|
+
(orig_fn_site, fresh_fn_member)
|
|
916
|
+
)
|
|
917
|
+
ginst = {
|
|
918
|
+
"fname": g_name,
|
|
919
|
+
"name": inst_name,
|
|
920
|
+
"ta_remap": composed_ta,
|
|
921
|
+
"var_remap": fvar_remap,
|
|
922
|
+
"fixnan_remap": ffixnan_remap,
|
|
923
|
+
}
|
|
924
|
+
interned[key] = ginst
|
|
925
|
+
self._fresh_instances.append(ginst)
|
|
926
|
+
worklist.append(ginst)
|
|
927
|
+
self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
|
|
928
|
+
|
|
929
|
+
def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
|
|
930
|
+
series_suffix: str, lines: list[str]) -> None:
|
|
931
|
+
"""Declare a per-clone copy of a function-scoped ``var`` member, matching
|
|
932
|
+
the original's C++ type (series / matrix / array / map / drawing-handle /
|
|
933
|
+
UDT / scalar). Shared by the per-call-site clone loop and the fresh
|
|
934
|
+
context-sensitive instance loop."""
|
|
935
|
+
for vname, ptype, _init_str in self.ctx.var_members:
|
|
936
|
+
if self._safe_name(vname) == orig_safe:
|
|
937
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
938
|
+
if vname in self.ctx.series_vars:
|
|
939
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
940
|
+
elif vname in self._matrix_specs:
|
|
941
|
+
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
|
|
942
|
+
elif vname in self._array_vars:
|
|
943
|
+
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
|
|
944
|
+
elif vname in self._map_vars:
|
|
945
|
+
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
|
|
946
|
+
elif vname in self._udt_var_types:
|
|
947
|
+
# Drawing handle / UDT var clone must match the original's
|
|
948
|
+
# type (Line/Label/Box/<UDT>), not the coarse PineType
|
|
949
|
+
# default (double) — otherwise the clone can't hold the
|
|
950
|
+
# handle and drawing access on it reads a garbage / na id.
|
|
951
|
+
udt_t = self._udt_var_types[vname]
|
|
952
|
+
handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
|
|
953
|
+
lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
|
|
954
|
+
else:
|
|
955
|
+
lines.append(f" {cpp_type} {cloned_safe};")
|
|
956
|
+
return
|
|
957
|
+
# Non-var series var
|
|
958
|
+
if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
|
|
959
|
+
cpp_type = self._series_type_for(orig_safe)
|
|
960
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
961
|
+
else:
|
|
962
|
+
lines.append(f" double {cloned_safe} = 0.0;")
|
|
963
|
+
|
|
496
964
|
@staticmethod
|
|
497
965
|
def _int_literal_value(node: ASTNode | None) -> int | None:
|
|
498
966
|
"""Return the integer value of a (possibly unary-minus) NumberLiteral,
|
|
@@ -575,31 +1043,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
575
1043
|
spec = self._matrix_specs.get(recv_name) or TypeSpec.matrix(TypeSpec.primitive("float"))
|
|
576
1044
|
self._matrix_specs[name] = spec
|
|
577
1045
|
self._collection_types[name] = spec
|
|
578
|
-
elif ns == "array" and fn in (
|
|
579
|
-
"new",
|
|
580
|
-
"new_float",
|
|
581
|
-
"new_int",
|
|
582
|
-
"new_bool",
|
|
583
|
-
"new_string",
|
|
584
|
-
"from",
|
|
585
|
-
):
|
|
1046
|
+
elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
|
|
586
1047
|
self._array_vars.add(name)
|
|
1048
|
+
spec = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
|
|
1049
|
+
self._collection_types[name] = spec
|
|
587
1050
|
elif ns == "map" and fn == "new":
|
|
588
1051
|
self._map_vars.add(name)
|
|
589
1052
|
|
|
590
|
-
# Also register var/varip
|
|
591
|
-
#
|
|
1053
|
+
# Also register var/varip aggregate members from AST nodes so that
|
|
1054
|
+
# class-member declarations see the precise collection type before
|
|
1055
|
+
# on_bar emits the initializer. This is required for unannotated
|
|
1056
|
+
# drawing arrays such as ``var boxes = array.new_box()``.
|
|
592
1057
|
var_decl_map: dict[str, FuncCall] = {}
|
|
593
1058
|
for stmt in (self.ctx.ast.body if hasattr(self.ctx, "ast") else []):
|
|
594
1059
|
if isinstance(stmt, VarDecl) and isinstance(stmt.value, FuncCall):
|
|
595
1060
|
var_decl_map[stmt.name] = stmt.value
|
|
596
1061
|
for name, _ptype, _init_str in self.ctx.var_members:
|
|
597
|
-
if name in self._matrix_specs:
|
|
598
|
-
continue
|
|
599
1062
|
expr = var_decl_map.get(name)
|
|
600
1063
|
if expr is None:
|
|
601
1064
|
continue
|
|
602
1065
|
fn2, ns2 = self._resolve_callee(expr.callee)
|
|
1066
|
+
if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
|
|
1067
|
+
self._array_vars.add(name)
|
|
1068
|
+
spec2 = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
|
|
1069
|
+
self._collection_types[name] = spec2
|
|
1070
|
+
continue
|
|
1071
|
+
if name in self._matrix_specs:
|
|
1072
|
+
continue
|
|
603
1073
|
if ns2 == "matrix" and fn2 == "new":
|
|
604
1074
|
targs2 = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
|
|
605
1075
|
elem_spec2 = self._type_spec_from_hint_name(targs2[0]) if targs2 else TypeSpec.primitive("float")
|
|
@@ -622,6 +1092,68 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
622
1092
|
self._matrix_specs[name] = spec2
|
|
623
1093
|
self._collection_types[name] = spec2
|
|
624
1094
|
|
|
1095
|
+
def _walk_global_scope_with_loopflag(self, stmts, in_loop):
|
|
1096
|
+
"""Yield ``(stmt, in_loop)`` for every statement in global scope,
|
|
1097
|
+
recursing into control-flow bodies (if/for/while/switch) but NOT into
|
|
1098
|
+
nested function definitions — a function-local of the same name lives in
|
|
1099
|
+
a separate scope and must not be attributed to a global member. The
|
|
1100
|
+
``in_loop`` flag is True once inside any for/while loop body."""
|
|
1101
|
+
for s in stmts:
|
|
1102
|
+
if isinstance(s, FuncDef):
|
|
1103
|
+
continue
|
|
1104
|
+
yield s, in_loop
|
|
1105
|
+
child_in_loop = in_loop or isinstance(s, (ForStmt, ForInStmt, WhileStmt))
|
|
1106
|
+
for attr in ("body", "else_body", "default_body"):
|
|
1107
|
+
child = getattr(s, attr, None)
|
|
1108
|
+
if isinstance(child, list):
|
|
1109
|
+
yield from self._walk_global_scope_with_loopflag(child, child_in_loop)
|
|
1110
|
+
cases = getattr(s, "cases", None)
|
|
1111
|
+
if isinstance(cases, list):
|
|
1112
|
+
for _case_expr, case_stmts in cases:
|
|
1113
|
+
if isinstance(case_stmts, list):
|
|
1114
|
+
yield from self._walk_global_scope_with_loopflag(case_stmts, child_in_loop)
|
|
1115
|
+
|
|
1116
|
+
def _register_udt_array_get_ref_locals(self) -> None:
|
|
1117
|
+
"""Detect global-scope UDT loop-locals that alias a UDT array element and
|
|
1118
|
+
are later field-mutated (Pine array elements of a user-defined type are
|
|
1119
|
+
references — ``z = arr.get(i)`` then ``z.f := v`` MUST write back into
|
|
1120
|
+
``arr``).
|
|
1121
|
+
|
|
1122
|
+
A non-``var`` global-scope ``UDT z = arr.get(i)`` (or ``.first`` /
|
|
1123
|
+
``.last``) nested inside a for/while loop mis-lowers to a value copy: a
|
|
1124
|
+
global ``while`` loop hoists ``z`` to a class member whose in-loop init
|
|
1125
|
+
becomes a value-copy assignment, and a global ``for`` loop keeps ``z`` a
|
|
1126
|
+
true local but the function-local alias path (``_udt_local_alias_kind``)
|
|
1127
|
+
no-ops at global scope (``_current_func_body`` is None) — both silently
|
|
1128
|
+
drop the field mutation. We record exactly this shape so the (possible)
|
|
1129
|
+
class member is suppressed and the in-loop VarDecl is emitted as a fresh
|
|
1130
|
+
per-iteration ``UDT& z = arr[i];`` reference instead (the same alias form
|
|
1131
|
+
the non-hoisted function-local path already produces). Strictly gated:
|
|
1132
|
+
the RHS must be a UDT-array-element lvalue AND the name must be field-
|
|
1133
|
+
mutated at global scope AND the declaration must be loop-nested. A
|
|
1134
|
+
read-only get-local is never recorded, so its value-copy output is
|
|
1135
|
+
unchanged. Function-local get-locals are excluded (the walker skips
|
|
1136
|
+
function bodies) — those keep using the existing alias path."""
|
|
1137
|
+
pairs = list(self._walk_global_scope_with_loopflag(self.ctx.ast.body, False))
|
|
1138
|
+
field_mutated: set[str] = set()
|
|
1139
|
+
for s, _in_loop in pairs:
|
|
1140
|
+
if (isinstance(s, Assignment)
|
|
1141
|
+
and isinstance(s.target, MemberAccess)
|
|
1142
|
+
and isinstance(s.target.object, Identifier)):
|
|
1143
|
+
field_mutated.add(s.target.object.name)
|
|
1144
|
+
for s, in_loop in pairs:
|
|
1145
|
+
if not isinstance(s, VarDecl) or s.is_var or s.is_varip:
|
|
1146
|
+
continue
|
|
1147
|
+
if not in_loop:
|
|
1148
|
+
continue
|
|
1149
|
+
if not isinstance(s.value, FuncCall):
|
|
1150
|
+
continue
|
|
1151
|
+
if self._is_udt_lvalue(s.value) is None:
|
|
1152
|
+
continue
|
|
1153
|
+
if s.name not in field_mutated:
|
|
1154
|
+
continue
|
|
1155
|
+
self._udt_array_get_ref_locals.add(s.name)
|
|
1156
|
+
|
|
625
1157
|
def _extract_receiver_name(self, call_node) -> str | None:
|
|
626
1158
|
"""Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
|
|
627
1159
|
|
|
@@ -733,6 +1265,134 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
733
1265
|
for stmt in self.ctx.ast.body:
|
|
734
1266
|
if isinstance(stmt, VarDecl) and stmt.name not in reassigned:
|
|
735
1267
|
self._collect_known_var(stmt)
|
|
1268
|
+
# A second pass handles the stable-reassigned-scalar pattern: a
|
|
1269
|
+
# class-scope scalar initialized from a stable expr and reassigned
|
|
1270
|
+
# ONLY inside top-level if/elif chains whose conditions and assigned
|
|
1271
|
+
# values are themselves stable (inputs / timeframe.* / math.*). Such
|
|
1272
|
+
# a var is a bar-invariant scalar and may feed a TA ctor length with
|
|
1273
|
+
# a runtime reset that reproduces the conditional logic. Series-
|
|
1274
|
+
# dependent reassignments are left untracked (rejected by the guard).
|
|
1275
|
+
self._collect_reassigned_stable_scalars(reassigned)
|
|
1276
|
+
|
|
1277
|
+
def _collect_reassigned_stable_scalars(self, reassigned: set[str]) -> None:
|
|
1278
|
+
"""Track class-scope scalars that are reassigned but only along stable
|
|
1279
|
+
if/elif paths (see ``test_stable_reassigned_class_scope_length``).
|
|
1280
|
+
|
|
1281
|
+
For each top-level ``v = <init>`` whose name is reassigned, build the
|
|
1282
|
+
final value as a nested ternary by folding subsequent top-level
|
|
1283
|
+
IfStmts / direct Assignments. If every condition and every assigned
|
|
1284
|
+
RHS is stable (and renderable), record the ternary in
|
|
1285
|
+
``_derived_input_expr`` and add ``v`` to ``_stable_runtime_vars`` so
|
|
1286
|
+
the TA ctor reset path can expand it. Anything non-stable (a ta.*
|
|
1287
|
+
result, a bar field, a series var) leaves the var untracked, so the
|
|
1288
|
+
ctor guard still rejects it loudly.
|
|
1289
|
+
"""
|
|
1290
|
+
from ..ast_nodes import IfStmt, Assignment
|
|
1291
|
+
body = self.ctx.ast.body or []
|
|
1292
|
+
# Pre-resolve each reassigned var's initial VarDecl.
|
|
1293
|
+
inits: dict[str, object] = {}
|
|
1294
|
+
for stmt in body:
|
|
1295
|
+
if (isinstance(stmt, VarDecl) and stmt.name in reassigned
|
|
1296
|
+
and not stmt.is_var and not stmt.is_varip):
|
|
1297
|
+
# Only consider vars whose initial value is itself stable;
|
|
1298
|
+
# an unstable init cannot become a stable scalar via later
|
|
1299
|
+
# reassignment.
|
|
1300
|
+
if stmt.value is not None and self._expr_is_stable(stmt.value):
|
|
1301
|
+
inits[stmt.name] = stmt.value
|
|
1302
|
+
if not inits:
|
|
1303
|
+
return
|
|
1304
|
+
|
|
1305
|
+
def _value_after(stmts, fallback: str | None) -> str | None:
|
|
1306
|
+
"""Fold a statement list into the final value expression for the
|
|
1307
|
+
target var, given ``fallback`` as the value on entry. Returns None
|
|
1308
|
+
if any condition / assignment is non-stable or unrenderable."""
|
|
1309
|
+
current = fallback
|
|
1310
|
+
for s in stmts or []:
|
|
1311
|
+
if isinstance(s, Assignment) and isinstance(s.target, Identifier):
|
|
1312
|
+
if s.target.name != target_name:
|
|
1313
|
+
continue
|
|
1314
|
+
if s.op != ":=":
|
|
1315
|
+
return None # compound assignment — not a stable fold
|
|
1316
|
+
if not self._expr_is_stable(s.value):
|
|
1317
|
+
return None
|
|
1318
|
+
rhs = self._arith_expr_to_str(s.value)
|
|
1319
|
+
if rhs is None:
|
|
1320
|
+
return None
|
|
1321
|
+
current = rhs
|
|
1322
|
+
elif isinstance(s, IfStmt):
|
|
1323
|
+
# Only model IfStmts that actually reassign the target var;
|
|
1324
|
+
# an unrelated IfStmt (e.g. entry/exit logic with a series
|
|
1325
|
+
# condition) must NOT abort the fold — the var simply keeps
|
|
1326
|
+
# its current value through it.
|
|
1327
|
+
if not _reassigns(s, target_name):
|
|
1328
|
+
continue
|
|
1329
|
+
if not self._expr_is_stable(s.condition):
|
|
1330
|
+
return None
|
|
1331
|
+
cond = self._arith_expr_to_str(s.condition)
|
|
1332
|
+
if cond is None:
|
|
1333
|
+
return None
|
|
1334
|
+
then_val = _value_after(s.body, current)
|
|
1335
|
+
if then_val is None:
|
|
1336
|
+
return None
|
|
1337
|
+
else_val = _value_after(s.else_body, current)
|
|
1338
|
+
if else_val is None:
|
|
1339
|
+
return None
|
|
1340
|
+
current = f"({cond} ? {then_val} : {else_val})"
|
|
1341
|
+
# Other statement shapes (for/while/switch/var decls of
|
|
1342
|
+
# other vars) are ignored for this var's value fold; they
|
|
1343
|
+
# do not reassign ``target_name`` in a way we model.
|
|
1344
|
+
return current
|
|
1345
|
+
|
|
1346
|
+
def _reassigns(node, name: str) -> bool:
|
|
1347
|
+
"""True if any ``:=`` assignment to ``name`` occurs within node."""
|
|
1348
|
+
from ..ast_nodes import IfStmt as _If, Assignment as _Asg
|
|
1349
|
+
if isinstance(node, _Asg) and isinstance(node.target, Identifier):
|
|
1350
|
+
return node.target.name == name
|
|
1351
|
+
if isinstance(node, _If):
|
|
1352
|
+
if any(_reassigns(c, name) for c in (node.body or [])):
|
|
1353
|
+
return True
|
|
1354
|
+
if any(_reassigns(c, name) for c in (node.else_body or [])):
|
|
1355
|
+
return True
|
|
1356
|
+
return False
|
|
1357
|
+
for attr in ("body", "else_body", "cases", "default_body"):
|
|
1358
|
+
sub = getattr(node, attr, None)
|
|
1359
|
+
if isinstance(sub, list):
|
|
1360
|
+
if any(_reassigns(c, name) for c in sub):
|
|
1361
|
+
return True
|
|
1362
|
+
return False
|
|
1363
|
+
|
|
1364
|
+
for target_name, init_node in inits.items():
|
|
1365
|
+
init_str = self._arith_expr_to_str(init_node)
|
|
1366
|
+
if init_str is None:
|
|
1367
|
+
continue
|
|
1368
|
+
final = _value_after(body, init_str)
|
|
1369
|
+
if final is None:
|
|
1370
|
+
continue
|
|
1371
|
+
# Sanity: the fold must actually differ from the bare init,
|
|
1372
|
+
# otherwise there were no stable reassignments and the var is
|
|
1373
|
+
# already covered (or rejected) by the main pass.
|
|
1374
|
+
if final == init_str:
|
|
1375
|
+
continue
|
|
1376
|
+
# Fold to a compile-time literal when possible (so the ctor-init
|
|
1377
|
+
# list can use it directly); otherwise record the raw expression
|
|
1378
|
+
# for the runtime reset path to expand.
|
|
1379
|
+
folded = self._resolve_known(final)
|
|
1380
|
+
if self._is_compile_time_value(folded):
|
|
1381
|
+
try:
|
|
1382
|
+
num = float(folded)
|
|
1383
|
+
self._known_vars[target_name] = (
|
|
1384
|
+
int(num) if num == int(num) else num
|
|
1385
|
+
)
|
|
1386
|
+
except ValueError:
|
|
1387
|
+
pass
|
|
1388
|
+
self._derived_input_expr[target_name] = final
|
|
1389
|
+
self._stable_runtime_vars.add(target_name)
|
|
1390
|
+
# Mark input-backed iff the expression references an input so the
|
|
1391
|
+
# override-aware get_input_*() reads are emitted on the reset path.
|
|
1392
|
+
import re as _re
|
|
1393
|
+
toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", final))
|
|
1394
|
+
if any(t in self._input_backed_vars for t in toks):
|
|
1395
|
+
self._input_backed_vars.add(target_name)
|
|
736
1396
|
|
|
737
1397
|
def _find_reassigned_vars(self) -> set[str]:
|
|
738
1398
|
"""Scan AST to find all variable names that are targets of := or compound assignment."""
|
|
@@ -759,6 +1419,180 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
759
1419
|
walk(stmt)
|
|
760
1420
|
return reassigned
|
|
761
1421
|
|
|
1422
|
+
# ``math.*`` members that are pure functions over their (stable) args, or
|
|
1423
|
+
# stable constants. Anything outside this set (e.g. ``math.random``) is
|
|
1424
|
+
# treated as non-stable. Used by ``_expr_is_stable``.
|
|
1425
|
+
_MATH_STABLE_MEMBERS: frozenset[str] = frozenset({
|
|
1426
|
+
"pi", "e", "phi", "rphi",
|
|
1427
|
+
"abs", "max", "min", "round", "floor", "ceil",
|
|
1428
|
+
"sqrt", "log", "log10", "exp", "pow",
|
|
1429
|
+
"sin", "cos", "tan", "asin", "acos", "atan", "sign",
|
|
1430
|
+
"sum", "avg", "to_precision", "round_to_mintick",
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
# ``timeframe.*`` members that are constant for the lifetime of a run —
|
|
1434
|
+
# they reflect the script's resolution, not a per-bar value.
|
|
1435
|
+
_TF_STABLE_MEMBERS: frozenset[str] = frozenset({
|
|
1436
|
+
"period", "main_period", "multiplier",
|
|
1437
|
+
"isintraday", "isminutes", "isdaily", "isweekly",
|
|
1438
|
+
"ismonthly", "isdwm", "isseconds", "in_seconds", "isticks",
|
|
1439
|
+
})
|
|
1440
|
+
|
|
1441
|
+
def _expr_is_stable(self, node) -> bool:
|
|
1442
|
+
"""True iff ``node``'s value is a bar-invariant scalar.
|
|
1443
|
+
|
|
1444
|
+
A stable expression depends only on: literals, ``input.*`` values,
|
|
1445
|
+
previously-tracked stable runtime vars, known compile-time consts,
|
|
1446
|
+
``timeframe.*`` members (constant per run), ``syminfo.*`` (constant
|
|
1447
|
+
per instrument), and ``math.*`` functions/consts over stable
|
|
1448
|
+
sub-expressions, combined with arithmetic / comparison / logical
|
|
1449
|
+
ops, ternaries, and the ``int/float/bool/string`` casts.
|
|
1450
|
+
|
|
1451
|
+
Returns False (i.e. "series") for any node that references a per-bar
|
|
1452
|
+
value: bar fields (close/open/...), series vars, history subscripts,
|
|
1453
|
+
``ta.*`` results, strategy.* state, or any unrecognised construct.
|
|
1454
|
+
The conservative False keeps the TA-ctor guard intact for genuinely
|
|
1455
|
+
dynamic lengths.
|
|
1456
|
+
"""
|
|
1457
|
+
if node is None:
|
|
1458
|
+
return False
|
|
1459
|
+
if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral)):
|
|
1460
|
+
return True
|
|
1461
|
+
if isinstance(node, NaLiteral):
|
|
1462
|
+
return True
|
|
1463
|
+
if isinstance(node, Identifier):
|
|
1464
|
+
name = node.name
|
|
1465
|
+
if name in self._known_vars:
|
|
1466
|
+
return True
|
|
1467
|
+
if name in self._stable_runtime_vars:
|
|
1468
|
+
return True
|
|
1469
|
+
if name in self._input_backed_vars:
|
|
1470
|
+
return True
|
|
1471
|
+
if name in self.ctx.series_vars:
|
|
1472
|
+
return False
|
|
1473
|
+
if name in self._var_names:
|
|
1474
|
+
# var/varip persistent state — mutable across bars.
|
|
1475
|
+
return False
|
|
1476
|
+
if name in BAR_FIELDS or name in BAR_BUILTINS:
|
|
1477
|
+
return False
|
|
1478
|
+
# Unrecognised bare identifier: be conservative so we never
|
|
1479
|
+
# silently allow an undeclared / dynamic length through.
|
|
1480
|
+
return False
|
|
1481
|
+
if isinstance(node, MemberAccess):
|
|
1482
|
+
if isinstance(node.object, Identifier):
|
|
1483
|
+
ns = node.object.name
|
|
1484
|
+
if ns == "timeframe":
|
|
1485
|
+
return node.member in self._TF_STABLE_MEMBERS
|
|
1486
|
+
if ns == "math":
|
|
1487
|
+
return node.member in self._MATH_STABLE_MEMBERS
|
|
1488
|
+
if ns == "syminfo":
|
|
1489
|
+
# syminfo.* (mintick, pointvalue, tickerid, ...) is
|
|
1490
|
+
# constant for the run — safe as a stable scalar.
|
|
1491
|
+
return True
|
|
1492
|
+
# bar.* / request.* / any other member access reads per-bar or
|
|
1493
|
+
# dynamic state.
|
|
1494
|
+
return False
|
|
1495
|
+
if isinstance(node, Subscript):
|
|
1496
|
+
# History read (``close[1]``) or indexed access — per-bar.
|
|
1497
|
+
return False
|
|
1498
|
+
if isinstance(node, Ternary):
|
|
1499
|
+
return (self._expr_is_stable(node.condition)
|
|
1500
|
+
and self._expr_is_stable(node.true_val)
|
|
1501
|
+
and self._expr_is_stable(node.false_val))
|
|
1502
|
+
if isinstance(node, BinOp):
|
|
1503
|
+
return self._expr_is_stable(node.left) and self._expr_is_stable(node.right)
|
|
1504
|
+
if isinstance(node, UnaryOp):
|
|
1505
|
+
return self._expr_is_stable(node.operand)
|
|
1506
|
+
if isinstance(node, FuncCall):
|
|
1507
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
1508
|
+
if namespace == "ta":
|
|
1509
|
+
return False
|
|
1510
|
+
if namespace == "math":
|
|
1511
|
+
if func_name not in self._MATH_STABLE_MEMBERS:
|
|
1512
|
+
return False
|
|
1513
|
+
return all(self._expr_is_stable(a) for a in node.args)
|
|
1514
|
+
if namespace == "timeframe":
|
|
1515
|
+
# ``timeframe.in_seconds()`` (and any other function-form
|
|
1516
|
+
# timeframe member) is a stable per-run scalar — it reflects
|
|
1517
|
+
# the script's resolution, not a per-bar value.
|
|
1518
|
+
if func_name not in self._TF_STABLE_MEMBERS:
|
|
1519
|
+
return False
|
|
1520
|
+
return all(self._expr_is_stable(a) for a in node.args)
|
|
1521
|
+
if namespace == "input":
|
|
1522
|
+
return True
|
|
1523
|
+
if namespace is None and func_name in ("int", "float", "bool", "string"):
|
|
1524
|
+
return all(self._expr_is_stable(a) for a in node.args)
|
|
1525
|
+
# Any other call (user functions, str.*, array.*, ...) — series
|
|
1526
|
+
# by default; the conservative answer keeps the guard honest.
|
|
1527
|
+
return False
|
|
1528
|
+
return False
|
|
1529
|
+
|
|
1530
|
+
# AST node kinds whose serialized form is self-delimiting (a literal, a
|
|
1531
|
+
# name, a member read, or a ``name(...)`` call). Non-atomic kinds (BinOp /
|
|
1532
|
+
# UnaryOp / Ternary) MUST be parenthesized when they appear as an operand,
|
|
1533
|
+
# otherwise re-parsing the flattened infix string silently reassociates the
|
|
1534
|
+
# tree: Pine grouping ``(a - b) / (c - d)`` degrades to ``a - b / c - d``
|
|
1535
|
+
# under C++ precedence. See ``_runtime_ctor_arg_for_reset`` (the string is
|
|
1536
|
+
# re-parsed and lowered through the expression visitor).
|
|
1537
|
+
_ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
|
|
1538
|
+
|
|
1539
|
+
def _arith_operand_to_str(self, node) -> str | None:
|
|
1540
|
+
"""Serialize ``node`` for use as an operand: parenthesize it unless its
|
|
1541
|
+
serialized form is already self-delimiting, so grouping survives a
|
|
1542
|
+
round-trip through the parser."""
|
|
1543
|
+
s = self._arith_expr_to_str(node)
|
|
1544
|
+
if s is None:
|
|
1545
|
+
return None
|
|
1546
|
+
if isinstance(node, self._ATOMIC_ARITH_NODES):
|
|
1547
|
+
return s
|
|
1548
|
+
return f"({s})"
|
|
1549
|
+
|
|
1550
|
+
def _arith_expr_to_str(self, node) -> str | None:
|
|
1551
|
+
"""Render a numeric arithmetic-over-identifiers expression to a string
|
|
1552
|
+
that re-parses to the SAME tree (grouping preserved via
|
|
1553
|
+
``_arith_operand_to_str``). Returns None for any node shape we don't
|
|
1554
|
+
fold (series subscripts, etc.) so the caller leaves the var untracked.
|
|
1555
|
+
"""
|
|
1556
|
+
if isinstance(node, NumberLiteral):
|
|
1557
|
+
v = node.value
|
|
1558
|
+
if isinstance(v, float) and v == int(v):
|
|
1559
|
+
return str(int(v))
|
|
1560
|
+
return str(v)
|
|
1561
|
+
if isinstance(node, Identifier):
|
|
1562
|
+
return node.name
|
|
1563
|
+
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
1564
|
+
return f"{node.object.name}.{node.member}"
|
|
1565
|
+
if isinstance(node, BinOp):
|
|
1566
|
+
l = self._arith_operand_to_str(node.left)
|
|
1567
|
+
r = self._arith_operand_to_str(node.right)
|
|
1568
|
+
if l is None or r is None:
|
|
1569
|
+
return None
|
|
1570
|
+
return f"{l} {node.op} {r}"
|
|
1571
|
+
if isinstance(node, UnaryOp):
|
|
1572
|
+
o = self._arith_operand_to_str(node.operand)
|
|
1573
|
+
if o is None:
|
|
1574
|
+
return None
|
|
1575
|
+
return f"{node.op}{o}"
|
|
1576
|
+
if isinstance(node, Ternary):
|
|
1577
|
+
c = self._arith_operand_to_str(node.condition)
|
|
1578
|
+
t = self._arith_operand_to_str(node.true_val)
|
|
1579
|
+
f = self._arith_operand_to_str(node.false_val)
|
|
1580
|
+
if c is None or t is None or f is None:
|
|
1581
|
+
return None
|
|
1582
|
+
return f"{c} ? {t} : {f}"
|
|
1583
|
+
if isinstance(node, FuncCall):
|
|
1584
|
+
callee = self._arith_expr_to_str(node.callee)
|
|
1585
|
+
if callee is None:
|
|
1586
|
+
return None
|
|
1587
|
+
parts = []
|
|
1588
|
+
for a in node.args:
|
|
1589
|
+
s = self._arith_expr_to_str(a)
|
|
1590
|
+
if s is None:
|
|
1591
|
+
return None
|
|
1592
|
+
parts.append(s)
|
|
1593
|
+
return f"{callee}({', '.join(parts)})"
|
|
1594
|
+
return None
|
|
1595
|
+
|
|
762
1596
|
def _collect_known_var(self, node: VarDecl) -> None:
|
|
763
1597
|
"""Extract known constant value from a VarDecl."""
|
|
764
1598
|
# Don't inline series variables — their values change over time
|
|
@@ -811,6 +1645,56 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
811
1645
|
if stored:
|
|
812
1646
|
self._input_backed_vars.add(node.name)
|
|
813
1647
|
self._input_var_to_call[node.name] = node.value
|
|
1648
|
+
# Class-scope arithmetic / ternaries / casts over known, input-backed,
|
|
1649
|
+
# timeframe.*, or math.* operands
|
|
1650
|
+
# (``wilderLen = rsiLen * 2 - 1``, ``fastPeriod = isM5 ? ... : ...``,
|
|
1651
|
+
# ``filterLen = math.max(1, int(math.round(2 / a)))``).
|
|
1652
|
+
# Without this branch the derived name is untracked, the TA ctor arg
|
|
1653
|
+
# never folds, and the runtime-reset path silently degenerates to a
|
|
1654
|
+
# period of 1. We (a) fold to a literal for the ctor-init list when
|
|
1655
|
+
# possible and (b) record the raw expression so the reset path can
|
|
1656
|
+
# re-expand any input-backed operand to its get_input_*() runtime read
|
|
1657
|
+
# and render timeframe.* / math.* fragments to valid C++.
|
|
1658
|
+
#
|
|
1659
|
+
# The ``_expr_is_stable`` gate is what separates a faithful stable
|
|
1660
|
+
# scalar (inputs + constants + timeframe + math) from a series-derived
|
|
1661
|
+
# value: a length that depends on a ta.* result, a history subscript,
|
|
1662
|
+
# or a bar field stays untracked and is therefore rejected by the TA
|
|
1663
|
+
# ctor guard — preserving the guardrail for genuine dynamic lengths.
|
|
1664
|
+
elif isinstance(node.value, (BinOp, UnaryOp, FuncCall, Ternary)):
|
|
1665
|
+
expr_str = self._arith_expr_to_str(node.value)
|
|
1666
|
+
if expr_str is not None and self._expr_is_stable(node.value):
|
|
1667
|
+
import re as _re
|
|
1668
|
+
tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
|
|
1669
|
+
refs_input = any(t in self._input_backed_vars for t in tokens)
|
|
1670
|
+
refs_derived = any(t in self._derived_input_expr for t in tokens)
|
|
1671
|
+
# The stability classifier already proved this expression is a
|
|
1672
|
+
# bar-invariant scalar (inputs / constants / timeframe.* /
|
|
1673
|
+
# math.* / syminfo.* only). Track it unconditionally so later
|
|
1674
|
+
# stable exprs (and the TA reset path) can reference / expand
|
|
1675
|
+
# it — e.g. ``pi = math.asin(1) * 2`` feeds ``beta`` feeds
|
|
1676
|
+
# ``alpha`` feeds a function-local ``filterLen``.
|
|
1677
|
+
folded = self._resolve_known(expr_str)
|
|
1678
|
+
if self._is_compile_time_value(folded):
|
|
1679
|
+
try:
|
|
1680
|
+
num = float(folded)
|
|
1681
|
+
self._known_vars[node.name] = (
|
|
1682
|
+
int(num) if num == int(num) else num
|
|
1683
|
+
)
|
|
1684
|
+
except ValueError:
|
|
1685
|
+
pass
|
|
1686
|
+
# Record the raw expression so the runtime-reset path can
|
|
1687
|
+
# re-expand operands. Always record for stable derived exprs
|
|
1688
|
+
# (even pure-math / pure-timeframe ones with no input) so the
|
|
1689
|
+
# reset can render them.
|
|
1690
|
+
self._derived_input_expr[node.name] = expr_str
|
|
1691
|
+
self._stable_runtime_vars.add(node.name)
|
|
1692
|
+
# Mark input-backed so use-sites are not inlined and the
|
|
1693
|
+
# override-aware get_input_*() reads are emitted on the reset
|
|
1694
|
+
# path. Pure-math / pure-timeframe exprs (no input) stay out
|
|
1695
|
+
# of this set, which is fine — they have no override to honor.
|
|
1696
|
+
if refs_input or refs_derived:
|
|
1697
|
+
self._input_backed_vars.add(node.name)
|
|
814
1698
|
|
|
815
1699
|
# ------------------------------------------------------------------
|
|
816
1700
|
# Public entry point
|
|
@@ -867,11 +1751,172 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
867
1751
|
walk(v)
|
|
868
1752
|
walk(self.ctx.ast)
|
|
869
1753
|
|
|
1754
|
+
def _func_cpp_base_name(self, fname: str) -> str:
|
|
1755
|
+
"""Return the actual emitted C++ base name for a UDF or UDT method."""
|
|
1756
|
+
fi = self._func_info_map.get(fname)
|
|
1757
|
+
if fi is not None and getattr(fi, "is_udt_method", False):
|
|
1758
|
+
return self._emit_udt_method_cpp_name(fi)
|
|
1759
|
+
return self._func_safe_name(fname)
|
|
1760
|
+
|
|
1761
|
+
def _inline_history_contexts_for_owner(self, owner: str | None) -> list[str | None]:
|
|
1762
|
+
"""Return every method-emission context that owns one source AST site.
|
|
1763
|
+
|
|
1764
|
+
``None`` denotes top-level/on_bar or a function emitted exactly once.
|
|
1765
|
+
Stateful UDF clones and fresh nested-helper instances use the same names
|
|
1766
|
+
assigned to ``_current_instance_name`` by ``_emit_func_def`` so lookup
|
|
1767
|
+
while visiting a body is deterministic and cannot collapse call sites.
|
|
1768
|
+
"""
|
|
1769
|
+
if owner is None:
|
|
1770
|
+
return [None]
|
|
1771
|
+
if owner in self._dead_func_names:
|
|
1772
|
+
return []
|
|
1773
|
+
|
|
1774
|
+
total_cs = self.ctx.func_call_site_counts.get(owner, 0)
|
|
1775
|
+
cloned = (
|
|
1776
|
+
owner in self.ctx.func_ta_ranges
|
|
1777
|
+
or owner in self.ctx.func_series_vars
|
|
1778
|
+
or owner in self.ctx.func_var_members
|
|
1779
|
+
or owner in self.ctx.func_security_clone_only
|
|
1780
|
+
) and total_cs > 0
|
|
1781
|
+
if cloned:
|
|
1782
|
+
contexts: list[str | None] = [
|
|
1783
|
+
f"{self._func_cpp_base_name(owner)}_cs{idx}"
|
|
1784
|
+
for idx in range(total_cs)
|
|
1785
|
+
]
|
|
1786
|
+
else:
|
|
1787
|
+
contexts = [None]
|
|
1788
|
+
|
|
1789
|
+
for inst in self._fresh_instances:
|
|
1790
|
+
if inst["fname"] == owner and inst["name"] not in contexts:
|
|
1791
|
+
contexts.append(inst["name"])
|
|
1792
|
+
return contexts
|
|
1793
|
+
|
|
1794
|
+
def _prepare_inline_history_members(self) -> None:
|
|
1795
|
+
"""Pre-register every generated temporary-Series class member.
|
|
1796
|
+
|
|
1797
|
+
Member declarations and the declaration-derived rollback aggregate are
|
|
1798
|
+
emitted before function/on_bar bodies. A source-order AST pass therefore
|
|
1799
|
+
reserves stable names up front. The key includes an emitted UDF context
|
|
1800
|
+
because the same body AST is rendered once per stateful call-site clone.
|
|
1801
|
+
"""
|
|
1802
|
+
self._inline_history_members = []
|
|
1803
|
+
self._inline_history_member_by_key = {}
|
|
1804
|
+
counters = {"hist_call": 0, "series_arg": 0}
|
|
1805
|
+
|
|
1806
|
+
def walk_nodes(value):
|
|
1807
|
+
"""Yield AST nodes in stable field order, including tuple elements.
|
|
1808
|
+
|
|
1809
|
+
NamingHelper._walk_ast predates several AST containers and is
|
|
1810
|
+
intentionally a best-effort utility. Member pre-registration must
|
|
1811
|
+
be exhaustive because a missed node becomes an undeclared C++
|
|
1812
|
+
member, so use the dataclass field graph directly here.
|
|
1813
|
+
"""
|
|
1814
|
+
if isinstance(value, ASTNode):
|
|
1815
|
+
yield value
|
|
1816
|
+
for child in vars(value).values():
|
|
1817
|
+
yield from walk_nodes(child)
|
|
1818
|
+
return
|
|
1819
|
+
if isinstance(value, (list, tuple)):
|
|
1820
|
+
for child in value:
|
|
1821
|
+
yield from walk_nodes(child)
|
|
1822
|
+
return
|
|
1823
|
+
if isinstance(value, dict):
|
|
1824
|
+
for child in value.values():
|
|
1825
|
+
yield from walk_nodes(child)
|
|
1826
|
+
return
|
|
1827
|
+
if isinstance(value, TypeField) and value.default is not None:
|
|
1828
|
+
yield from walk_nodes(value.default)
|
|
1829
|
+
|
|
1830
|
+
owner_by_node: dict[int, str] = {}
|
|
1831
|
+
for fi in self.ctx.func_infos:
|
|
1832
|
+
if fi.node is None:
|
|
1833
|
+
continue
|
|
1834
|
+
for child in walk_nodes(fi.node):
|
|
1835
|
+
owner_by_node[id(child)] = fi.name
|
|
1836
|
+
|
|
1837
|
+
def register(kind: str, source_key: tuple, cpp_type: str,
|
|
1838
|
+
owner: str | None) -> None:
|
|
1839
|
+
if cpp_type not in ("double", "int", "bool"):
|
|
1840
|
+
cpp_type = "double"
|
|
1841
|
+
for context in self._inline_history_contexts_for_owner(owner):
|
|
1842
|
+
key = (kind, *source_key, context)
|
|
1843
|
+
if key in self._inline_history_member_by_key:
|
|
1844
|
+
continue
|
|
1845
|
+
counters[kind] += 1
|
|
1846
|
+
member_name = f"_{kind}_{counters[kind]}"
|
|
1847
|
+
self._inline_history_member_by_key[key] = member_name
|
|
1848
|
+
self._inline_history_members.append({
|
|
1849
|
+
"kind": kind,
|
|
1850
|
+
"member_name": member_name,
|
|
1851
|
+
"cpp_type": cpp_type,
|
|
1852
|
+
"context": context,
|
|
1853
|
+
})
|
|
1854
|
+
|
|
1855
|
+
def actual_args_for(call: FuncCall, params: list[str]) -> list:
|
|
1856
|
+
if call.kwargs:
|
|
1857
|
+
return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
|
|
1858
|
+
return list(call.args)
|
|
1859
|
+
|
|
1860
|
+
for node in walk_nodes(self.ctx.ast):
|
|
1861
|
+
owner = owner_by_node.get(id(node))
|
|
1862
|
+
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
1863
|
+
register(
|
|
1864
|
+
"hist_call", (id(node),), self._infer_type(node.object), owner
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
if not isinstance(node, FuncCall):
|
|
1868
|
+
continue
|
|
1869
|
+
func_name, _ = self._resolve_callee(node.callee)
|
|
1870
|
+
fi = self._func_info_map.get(func_name)
|
|
1871
|
+
if fi is None or fi.node is None:
|
|
1872
|
+
continue
|
|
1873
|
+
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
1874
|
+
series_param_indices = {
|
|
1875
|
+
idx for idx, name in enumerate(fi.node.params) if name in func_sv
|
|
1876
|
+
}
|
|
1877
|
+
if not series_param_indices:
|
|
1878
|
+
continue
|
|
1879
|
+
args = actual_args_for(node, list(fi.node.params))
|
|
1880
|
+
for idx, arg in enumerate(args):
|
|
1881
|
+
if idx not in series_param_indices:
|
|
1882
|
+
continue
|
|
1883
|
+
if isinstance(arg, Identifier):
|
|
1884
|
+
if arg.name in BAR_FIELDS or arg.name in BAR_SERIES_PUSH:
|
|
1885
|
+
continue
|
|
1886
|
+
if arg.name in self.ctx.series_vars:
|
|
1887
|
+
continue
|
|
1888
|
+
register(
|
|
1889
|
+
"series_arg", (id(node), idx), self._infer_type(arg), owner
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
def _inline_history_member(self, kind: str, node: ASTNode,
|
|
1893
|
+
arg_idx: int | None = None) -> str:
|
|
1894
|
+
source_key = (id(node),) if arg_idx is None else (id(node), arg_idx)
|
|
1895
|
+
key = (kind, *source_key, self._current_instance_name)
|
|
1896
|
+
member = self._inline_history_member_by_key.get(key)
|
|
1897
|
+
if member is None:
|
|
1898
|
+
raise AssertionError(
|
|
1899
|
+
"missing pre-registered inline history member for "
|
|
1900
|
+
f"{kind} at {getattr(node, 'loc', None)} in context "
|
|
1901
|
+
f"{self._current_instance_name!r}"
|
|
1902
|
+
)
|
|
1903
|
+
return member
|
|
1904
|
+
|
|
870
1905
|
def generate(self) -> str:
|
|
871
1906
|
"""Generate C++ source from the AnalyzerContext."""
|
|
1907
|
+
# Context-sensitive instance pre-pass (needs the naming helpers populated
|
|
1908
|
+
# in __init__). Computes nested stateful-helper dispatch + fresh instances.
|
|
1909
|
+
self._build_func_instances()
|
|
1910
|
+
self._prepare_inline_history_members()
|
|
872
1911
|
# Pre-scan for strategy series vars
|
|
873
1912
|
self._prescan_strategy_series()
|
|
874
1913
|
self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
|
|
1914
|
+
# request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
|
|
1915
|
+
# Maps sec_id -> set of TA call-site indices needing an HTF history Series.
|
|
1916
|
+
self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
|
|
1917
|
+
# request.security helper-call results read at a history offset
|
|
1918
|
+
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
1919
|
+
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
875
1920
|
|
|
876
1921
|
lines: list[str] = []
|
|
877
1922
|
|
|
@@ -897,6 +1942,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
897
1942
|
continue
|
|
898
1943
|
spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
|
|
899
1944
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
1945
|
+
# Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
|
|
1946
|
+
# and large bar indices); emit UDT int fields as ``int64_t`` so a
|
|
1947
|
+
# field initialised from ``time``/``current_bar_.timestamp`` does
|
|
1948
|
+
# not truncate / narrow-init.
|
|
1949
|
+
if cpp_type == "int":
|
|
1950
|
+
cpp_type = "int64_t"
|
|
900
1951
|
if f.default:
|
|
901
1952
|
default = self._visit_expr(f.default)
|
|
902
1953
|
else:
|
|
@@ -930,6 +1981,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
930
1981
|
# 2. Open class
|
|
931
1982
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
932
1983
|
lines.append("public:")
|
|
1984
|
+
_script_state_decl_start = len(lines)
|
|
933
1985
|
|
|
934
1986
|
# request.security state
|
|
935
1987
|
for item in self._security_calls:
|
|
@@ -958,14 +2010,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
958
2010
|
for field in sorted(
|
|
959
2011
|
self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
960
2012
|
):
|
|
2013
|
+
ctype = self._security_bar_hist_type(field)
|
|
961
2014
|
lines.append(
|
|
962
|
-
f" Series<
|
|
2015
|
+
f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
963
2016
|
)
|
|
2017
|
+
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
2018
|
+
self._collect_security_ta_hist_indices(expr_node)
|
|
2019
|
+
)
|
|
2020
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
2021
|
+
lines.append(f" Series<double> {name}{_mbb};")
|
|
2022
|
+
self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
|
|
964
2023
|
continue
|
|
965
2024
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
966
2025
|
hist_fields: set[str] = set()
|
|
967
2026
|
for el in expr_node.elements:
|
|
968
2027
|
hist_fields |= self._collect_security_ohlc_hist_fields(el)
|
|
2028
|
+
for name in item.get("mutable_globals", []) or []:
|
|
2029
|
+
info = self._global_mutable_infos.get(name)
|
|
2030
|
+
if info is not None:
|
|
2031
|
+
for stmt in getattr(info, "source_stmts", []) or []:
|
|
2032
|
+
hist_fields |= self._collect_security_ohlc_hist_fields(stmt)
|
|
969
2033
|
self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
|
|
970
2034
|
for i, el in enumerate(expr_node.elements):
|
|
971
2035
|
ctype = self._infer_cpp_type_for_security_elem(el)
|
|
@@ -973,15 +2037,31 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
973
2037
|
lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
|
|
974
2038
|
else:
|
|
975
2039
|
lines.append(f" {ctype} _req_sec_{sec_id}_{i} = na<double>();")
|
|
2040
|
+
elif returns_tuple and tuple_size and tuple_size > 0:
|
|
2041
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
2042
|
+
self._collect_security_ohlc_hist_fields_for_call(item)
|
|
2043
|
+
)
|
|
2044
|
+
site = self._get_ta_site(expr_node)
|
|
2045
|
+
ta_name = self._ta_name_from_site(site) if site is not None else ""
|
|
2046
|
+
ctype = TA_TUPLE_RESULT_TYPES.get(ta_name, "std::tuple<double, double>")
|
|
2047
|
+
default = self._security_tuple_result_default(ctype, tuple_size)
|
|
2048
|
+
lines.append(f" {ctype} _req_sec_{sec_id} = {default};")
|
|
976
2049
|
else:
|
|
977
|
-
self._security_ohlc_hist_fields_by_sec[sec_id] =
|
|
978
|
-
|
|
2050
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
2051
|
+
self._collect_security_ohlc_hist_fields_for_call(item)
|
|
979
2052
|
)
|
|
980
2053
|
lines.append(f" double _req_sec_{sec_id} = na<double>();")
|
|
981
2054
|
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
2055
|
+
ctype = self._security_bar_hist_type(field)
|
|
982
2056
|
lines.append(
|
|
983
|
-
f" Series<
|
|
2057
|
+
f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
984
2058
|
)
|
|
2059
|
+
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
2060
|
+
self._collect_security_ta_hist_indices(expr_node)
|
|
2061
|
+
)
|
|
2062
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
2063
|
+
lines.append(f" Series<double> {name}{_mbb};")
|
|
2064
|
+
self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
|
|
985
2065
|
|
|
986
2066
|
if self._security_calls:
|
|
987
2067
|
lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
|
|
@@ -1005,9 +2085,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1005
2085
|
)
|
|
1006
2086
|
|
|
1007
2087
|
# 3. TA members
|
|
1008
|
-
for site in self.ctx.ta_call_sites:
|
|
2088
|
+
for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
|
|
2089
|
+
if _ta_idx in self._dead_ta_indices:
|
|
2090
|
+
continue
|
|
1009
2091
|
lines.append(f" {site.class_name} {site.member_name};")
|
|
1010
|
-
if
|
|
2092
|
+
if self._ta_site_uses_precalc(site):
|
|
1011
2093
|
vtype = self._ta_return_type(site)
|
|
1012
2094
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
1013
2095
|
lines.append(" bool _use_precalc = false;")
|
|
@@ -1031,8 +2113,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1031
2113
|
continue
|
|
1032
2114
|
seen_var_members.add(name)
|
|
1033
2115
|
safe = self._safe_name(name)
|
|
1034
|
-
# Detect array vars from init expression
|
|
1035
|
-
|
|
2116
|
+
# Detect array vars from init expression. Guard the substring
|
|
2117
|
+
# heuristic against a UDT constructor that merely WRAPS array.new /
|
|
2118
|
+
# array.from in its arguments — e.g.
|
|
2119
|
+
# ``var draw d = draw.new(array.new<line>(), array.new<line>())``
|
|
2120
|
+
# must declare as ``draw``, not ``std::vector<double>``. (Drawing
|
|
2121
|
+
# made this latent collision reachable.)
|
|
2122
|
+
_init_str_s = str(init_str)
|
|
2123
|
+
_is_udt_ctor_init = any(
|
|
2124
|
+
_init_str_s.startswith(f"{u}.new") for u in self._udt_defs
|
|
2125
|
+
)
|
|
2126
|
+
if (not _is_udt_ctor_init) and (
|
|
2127
|
+
"array.new" in _init_str_s or "array.from" in _init_str_s
|
|
2128
|
+
or name in self._array_vars
|
|
2129
|
+
):
|
|
1036
2130
|
self._array_vars.add(name)
|
|
1037
2131
|
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
|
|
1038
2132
|
continue
|
|
@@ -1063,6 +2157,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1063
2157
|
# decl as ``double`` and the later ``z = SDZone{...}`` would not
|
|
1064
2158
|
# compile (assigning SDZone to double).
|
|
1065
2159
|
init_s = str(init_str)
|
|
2160
|
+
# Drawing handle var member (L-N2): a ``var line x`` declares as the
|
|
2161
|
+
# C++ handle struct (Series<Line> when also history-referenced).
|
|
2162
|
+
# Drawing names are NOT in _udt_defs, so the udt branch below would
|
|
2163
|
+
# self-zero them to double; handle them first.
|
|
2164
|
+
_draw_cpp = DRAWING_TYPE_TO_CPP.get(self._udt_var_types.get(name))
|
|
2165
|
+
if _draw_cpp is not None:
|
|
2166
|
+
if name in self.ctx.series_vars:
|
|
2167
|
+
lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
|
|
2168
|
+
else:
|
|
2169
|
+
lines.append(f" {_draw_cpp} {safe};")
|
|
2170
|
+
continue
|
|
1066
2171
|
udt_type = self._udt_var_types.get(name)
|
|
1067
2172
|
if udt_type not in self._udt_defs:
|
|
1068
2173
|
udt_type = None
|
|
@@ -1092,7 +2197,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1092
2197
|
lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
|
|
1093
2198
|
|
|
1094
2199
|
# 7. Fixnan members
|
|
1095
|
-
for site in self.ctx.fixnan_sites:
|
|
2200
|
+
for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
|
|
2201
|
+
if _fi_idx in self._dead_fixnan_indices:
|
|
2202
|
+
continue
|
|
1096
2203
|
cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
|
|
1097
2204
|
lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
|
|
1098
2205
|
|
|
@@ -1106,12 +2213,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1106
2213
|
else:
|
|
1107
2214
|
lines.append(f" Series<double> {svar}{_mbb};")
|
|
1108
2215
|
|
|
2216
|
+
# 8a. Synthetic temporary history. Unlike the legacy function-local
|
|
2217
|
+
# static buffers, these members are value-copyable rollback state and
|
|
2218
|
+
# have one identity per source site / emitted UDF variant.
|
|
2219
|
+
for info in self._inline_history_members:
|
|
2220
|
+
lines.append(
|
|
2221
|
+
f" Series<{info['cpp_type']}> {info['member_name']}{_mbb};"
|
|
2222
|
+
)
|
|
2223
|
+
|
|
1109
2224
|
# 8b. Global-scope non-var declarations as class members
|
|
1110
2225
|
# (so user-defined functions can reference them)
|
|
1111
2226
|
seen_global = set()
|
|
1112
2227
|
for name, ptype in self.ctx.global_var_decls:
|
|
1113
2228
|
if name in seen_global or name in self.ctx.series_vars or name in self._var_names:
|
|
1114
2229
|
continue
|
|
2230
|
+
# De-hoisted UDT array-element alias (Pine reference semantics): the
|
|
2231
|
+
# in-loop VarDecl is emitted as a fresh ``UDT& z = arr[i];`` local
|
|
2232
|
+
# reference each iteration, so there is no persistent class member.
|
|
2233
|
+
if name in self._udt_array_get_ref_locals:
|
|
2234
|
+
seen_global.add(name)
|
|
2235
|
+
continue
|
|
1115
2236
|
seen_global.add(name)
|
|
1116
2237
|
safe = self._safe_name(name)
|
|
1117
2238
|
|
|
@@ -1131,7 +2252,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1131
2252
|
# data/validation/udt-method-probe-19-array-of-udt-method,
|
|
1132
2253
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1133
2254
|
udt_t = self._udt_var_types[name]
|
|
1134
|
-
|
|
2255
|
+
# Drawing handle global (L-N6 / U): map line/box/label/linefill
|
|
2256
|
+
# to the C++ handle struct (the default is na, id=-1).
|
|
2257
|
+
_draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
|
|
2258
|
+
if _draw_cpp is not None:
|
|
2259
|
+
lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
|
|
2260
|
+
else:
|
|
2261
|
+
lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
|
|
1135
2262
|
else:
|
|
1136
2263
|
expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
|
|
1137
2264
|
cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
@@ -1148,37 +2275,65 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1148
2275
|
if cloned_safe in emitted_clones:
|
|
1149
2276
|
continue # already declared by another function's clone
|
|
1150
2277
|
emitted_clones.add(cloned_safe)
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
2278
|
+
self._emit_cloned_var_decl(orig_safe, cloned_safe, _mbb, lines)
|
|
2279
|
+
|
|
2280
|
+
# 8c2. Fresh var members for context-sensitive helper instances (nested
|
|
2281
|
+
# helpers reached through >1 distinct call path). Each fresh instance
|
|
2282
|
+
# gets its OWN scalar/series state so two paths never collide.
|
|
2283
|
+
for orig_safe, fresh_safe in self._fresh_var_members:
|
|
2284
|
+
if fresh_safe in emitted_clones:
|
|
2285
|
+
continue
|
|
2286
|
+
emitted_clones.add(fresh_safe)
|
|
2287
|
+
self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
|
|
2288
|
+
|
|
2289
|
+
# 8c3. Fresh fixnan members for context-sensitive helper instances.
|
|
2290
|
+
# Each fresh instance gets its OWN previous-value member so two
|
|
2291
|
+
# call paths never share fixnan state (mirrors 8c2 for vars).
|
|
2292
|
+
for orig_site, fresh_safe in self._fresh_fixnan_members:
|
|
2293
|
+
if fresh_safe in emitted_clones:
|
|
2294
|
+
continue
|
|
2295
|
+
emitted_clones.add(fresh_safe)
|
|
2296
|
+
cpp_type = PINE_TYPE_TO_CPP.get(orig_site.pine_type, "double")
|
|
2297
|
+
lines.append(f" {cpp_type} {fresh_safe} = na<{cpp_type}>();")
|
|
2298
|
+
|
|
2299
|
+
# 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
|
|
2300
|
+
# non-drawing strategies emit byte-identical C++). Each arena is a
|
|
2301
|
+
# per-strategy member -> reset-per-run is automatic. Caps come from
|
|
2302
|
+
# the strategy() header max_*_count (default 50; linefill default 50).
|
|
2303
|
+
if self._uses_drawing:
|
|
2304
|
+
caps = self._drawing_caps or {}
|
|
2305
|
+
lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
|
|
2306
|
+
lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
|
|
2307
|
+
lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
|
|
2308
|
+
lines.append(f" DrawingArena<LinefillRec> _pf_linefills_{{{caps.get('linefill', 50)}}};")
|
|
1177
2309
|
|
|
1178
2310
|
# 9. _var_initialized flag
|
|
1179
2311
|
if self.ctx.var_members:
|
|
1180
2312
|
lines.append(" bool _var_initialized = false;")
|
|
1181
2313
|
|
|
2314
|
+
# 9a. Per-function-variant ``var`` init flags. A function-scoped
|
|
2315
|
+
# ``var`` (Pine "init once" semantics) is a function-local static:
|
|
2316
|
+
# its initializer runs on the FIRST call to that function variant
|
|
2317
|
+
# (with the first bar's values the function actually sees) and the
|
|
2318
|
+
# result persists for the strategy's lifetime. Each clone (cs0,
|
|
2319
|
+
# cs1, ...) is an independent instance with its own flag.
|
|
2320
|
+
# ``func_var_members`` is keyed by the plain Pine function name
|
|
2321
|
+
# (``fi.name``), so this matches both plain UDFs and UDT methods.
|
|
2322
|
+
for fi in self.ctx.func_infos:
|
|
2323
|
+
if fi.name not in self.ctx.func_var_members:
|
|
2324
|
+
continue
|
|
2325
|
+
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
2326
|
+
if total_cs > 0:
|
|
2327
|
+
for cs_idx in range(total_cs):
|
|
2328
|
+
lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)}_cs{cs_idx} = false;")
|
|
2329
|
+
else:
|
|
2330
|
+
lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")
|
|
2331
|
+
|
|
2332
|
+
# 9a2. ``var`` init flags for fresh context-sensitive helper instances.
|
|
2333
|
+
for inst in self._fresh_instances:
|
|
2334
|
+
if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
|
|
2335
|
+
lines.append(f" bool _fvinit_{inst['name']} = false;")
|
|
2336
|
+
|
|
1182
2337
|
# 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
|
|
1183
2338
|
if self.ctx.ta_call_sites:
|
|
1184
2339
|
lines.append(" bool _ta_initialized_ = false;")
|
|
@@ -1188,6 +2343,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1188
2343
|
|
|
1189
2344
|
lines.append("")
|
|
1190
2345
|
|
|
2346
|
+
# 9d. Historical execution rollback checkpoint. Derive the member
|
|
2347
|
+
# inventory from the declarations above so every future generated
|
|
2348
|
+
# state category is captured automatically (or generation fails loudly
|
|
2349
|
+
# if it introduces an unfamiliar declaration form).
|
|
2350
|
+
_script_state_members = self._collect_script_state_members(
|
|
2351
|
+
lines[_script_state_decl_start:-1]
|
|
2352
|
+
)
|
|
2353
|
+
self._emit_script_state_hooks(lines, _script_state_members)
|
|
2354
|
+
lines.append("")
|
|
2355
|
+
|
|
1191
2356
|
# 9. Constructor with TA initializer list
|
|
1192
2357
|
self._emit_constructor(lines)
|
|
1193
2358
|
lines.append("")
|
|
@@ -1195,10 +2360,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1195
2360
|
# 10. User-defined functions (with per-call-site variants for functions
|
|
1196
2361
|
# containing TA calls OR series variables that need isolation)
|
|
1197
2362
|
for fi in self.ctx.func_infos:
|
|
2363
|
+
# Dead-code user functions (defined but never called, with TA
|
|
2364
|
+
# state whose ctor args can't be sized) are skipped entirely —
|
|
2365
|
+
# their bodies reference TA members we no longer emit, and the
|
|
2366
|
+
# functions never run anyway.
|
|
2367
|
+
if fi.name in self._dead_func_names:
|
|
2368
|
+
continue
|
|
1198
2369
|
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
1199
2370
|
has_ta = fi.name in self.ctx.func_ta_ranges
|
|
1200
2371
|
has_series = fi.name in self.ctx.func_series_vars or fi.name in self.ctx.func_var_members
|
|
1201
|
-
|
|
2372
|
+
# A function whose ONLY reason to need per-call-site cloning is a
|
|
2373
|
+
# security-tf-monomorphized request.security (no TA/series state
|
|
2374
|
+
# of its own — see Analyzer._check_mixed_callsite_security_tf)
|
|
2375
|
+
# still needs N separate emitted bodies so self._active_call_site_idx
|
|
2376
|
+
# is set while each is emitted (read by the request.security
|
|
2377
|
+
# use-site lowering in visit_call.py to pick the right clone's
|
|
2378
|
+
# sec_id).
|
|
2379
|
+
needs_security_clone = fi.name in self.ctx.func_security_clone_only
|
|
2380
|
+
if (has_ta or has_series or needs_security_clone) and total_cs > 0:
|
|
1202
2381
|
# Emit one variant per call site
|
|
1203
2382
|
for cs_idx in range(total_cs):
|
|
1204
2383
|
self._emit_func_def(fi, lines, call_site_idx=cs_idx)
|
|
@@ -1207,7 +2386,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1207
2386
|
self._emit_func_def(fi, lines)
|
|
1208
2387
|
lines.append("")
|
|
1209
2388
|
|
|
2389
|
+
# 10a. Fresh context-sensitive instances of nested stateful helpers
|
|
2390
|
+
# (reached through >1 distinct call path). Each is bound to its own
|
|
2391
|
+
# path-specific TA + var members; see _build_func_instances.
|
|
2392
|
+
if self._fresh_instances:
|
|
2393
|
+
fi_by_name = {fi.name: fi for fi in self.ctx.func_infos}
|
|
2394
|
+
for inst in self._fresh_instances:
|
|
2395
|
+
fi = fi_by_name.get(inst["fname"])
|
|
2396
|
+
if fi is None:
|
|
2397
|
+
continue
|
|
2398
|
+
self._emit_func_def(fi, lines, instance=inst)
|
|
2399
|
+
lines.append("")
|
|
2400
|
+
|
|
1210
2401
|
# 11. on_bar()
|
|
2402
|
+
self._current_instance_name = None
|
|
1211
2403
|
self._emit_on_bar(lines)
|
|
1212
2404
|
lines.append("")
|
|
1213
2405
|
|
|
@@ -1319,13 +2511,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1319
2511
|
def _is_skip_expr(self, node) -> bool:
|
|
1320
2512
|
"""Check if an expression should be skipped (visual/unsupported)."""
|
|
1321
2513
|
if isinstance(node, FuncCall):
|
|
2514
|
+
# chart.point.* resolves to namespace "chart" (a SKIP_NAMESPACE) but
|
|
2515
|
+
# is REAL data (a ChartPoint aggregate literal). Never skip it.
|
|
2516
|
+
if self._is_chart_point_callee(node.callee):
|
|
2517
|
+
return False
|
|
1322
2518
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
1323
|
-
if func_name in SKIP_FUNC_NAMES:
|
|
2519
|
+
if namespace is None and func_name in SKIP_FUNC_NAMES:
|
|
1324
2520
|
return True
|
|
1325
2521
|
if namespace in SKIP_NAMESPACES:
|
|
1326
2522
|
return True
|
|
1327
2523
|
if namespace in SKIP_VAR_TYPES:
|
|
1328
2524
|
return True
|
|
2525
|
+
# Method call on a table/polyline-typed receiver var/param
|
|
2526
|
+
# (``panel.cell(...)``). These types have no C++ representation, so
|
|
2527
|
+
# the call is a visual no-op — drop it (mirrors the namespace form).
|
|
2528
|
+
if namespace in self._visual_drop_vars:
|
|
2529
|
+
return True
|
|
1329
2530
|
# strategy.risk.* — handled in _visit_stmt, not skipped
|
|
1330
2531
|
if isinstance(node, MemberAccess):
|
|
1331
2532
|
if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
|
|
@@ -1363,59 +2564,335 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1363
2564
|
# _infer_type / _infer_tuple_types live on TypeInferer — see codegen/types.py.
|
|
1364
2565
|
# _is_compile_time_value lives on TaSiteHelper — see codegen/ta.py.
|
|
1365
2566
|
|
|
2567
|
+
# Pine ``timeframe.<member>`` -> C++ runtime expression. Mirrors the
|
|
2568
|
+
# mapping in ``visit_expr._visit_member_access`` so a stable timeframe
|
|
2569
|
+
# fragment embedded in a TA ctor reset renders to the same C++ the
|
|
2570
|
+
# expression visitor would emit for a direct ``timeframe.*`` read.
|
|
2571
|
+
_TIMEFRAME_MEMBER_CPP: dict[str, str] = {
|
|
2572
|
+
"period": "script_tf_",
|
|
2573
|
+
"main_period": "main_period()",
|
|
2574
|
+
"multiplier": "tf_multiplier(script_tf_)",
|
|
2575
|
+
"isintraday": "tf_is_intraday(script_tf_)",
|
|
2576
|
+
"isminutes": "(tf_is_intraday(script_tf_) && !tf_is_seconds(script_tf_))",
|
|
2577
|
+
"isdaily": "tf_is_daily(script_tf_)",
|
|
2578
|
+
"isweekly": "tf_is_weekly(script_tf_)",
|
|
2579
|
+
"ismonthly": "tf_is_monthly(script_tf_)",
|
|
2580
|
+
"isdwm": "(tf_is_daily(script_tf_) || tf_is_weekly(script_tf_) || tf_is_monthly(script_tf_))",
|
|
2581
|
+
"isseconds": "tf_is_seconds(script_tf_)",
|
|
2582
|
+
"in_seconds": "tf_to_seconds(script_tf_)",
|
|
2583
|
+
"isticks": "false",
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
# Pine ``math.<member>`` -> C++ form. Function members map to ``std::*``;
|
|
2587
|
+
# constants map to their engine-side macro / literal.
|
|
2588
|
+
_MATH_MEMBER_CPP: dict[str, str] = {
|
|
2589
|
+
"pi": "M_PI", "e": "M_E", "phi": "1.618033988749895",
|
|
2590
|
+
"rphi": "0.6180339887498949",
|
|
2591
|
+
"abs": "std::abs", "max": "std::max", "min": "std::min",
|
|
2592
|
+
"round": "std::round", "floor": "std::floor", "ceil": "std::ceil",
|
|
2593
|
+
"sqrt": "std::sqrt", "log": "std::log", "log10": "std::log10",
|
|
2594
|
+
"exp": "std::exp", "pow": "std::pow",
|
|
2595
|
+
"sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
|
|
2596
|
+
"asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
|
|
2597
|
+
"sign": "(double)([] (double _v) { return (_v>0) - (_v<0); })",
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
# Pine logical operators (word form) -> C++ operator, used when rendering
|
|
2601
|
+
# a stable runtime expression. Matched with word boundaries.
|
|
2602
|
+
_PINE_LOGICAL_OPS: dict[str, str] = {"and": "&&", "or": "||", "not": "!"}
|
|
2603
|
+
|
|
2604
|
+
def _render_inline_input_calls(self, expr_str: str) -> tuple[str, bool]:
|
|
2605
|
+
"""Render inline ``input(...)`` / ``input.<type>(...)`` calls in a TA
|
|
2606
|
+
ctor-arg expression string to override-aware ``get_input_*()`` reads.
|
|
2607
|
+
|
|
2608
|
+
A bare input expression passed straight as a length argument
|
|
2609
|
+
(``adx(input(15), input(15))``) reaches the reset path as the raw call
|
|
2610
|
+
spelling ``input(15)`` because the analyzer's param-substitution has no
|
|
2611
|
+
intermediate variable to record in ``_input_backed_vars``. This helper
|
|
2612
|
+
finds each such call (balanced parens, ``input`` optionally followed by
|
|
2613
|
+
``.<type>``), re-parses it into a FuncCall, and renders it via the same
|
|
2614
|
+
``_render_input_value`` used for ordinary input var reads.
|
|
2615
|
+
|
|
2616
|
+
Returns ``(rewritten_str, found_any)``. When no inline input call is
|
|
2617
|
+
present, the string is returned unchanged with ``found_any=False``.
|
|
2618
|
+
"""
|
|
2619
|
+
import re
|
|
2620
|
+
# Locate ``input`` (as a word, not a substring of get_input_int etc.)
|
|
2621
|
+
# optionally followed by ``.<member>``, then a ``(`` opening a balanced
|
|
2622
|
+
# argument list.
|
|
2623
|
+
out = expr_str
|
|
2624
|
+
found = False
|
|
2625
|
+
idx = 0
|
|
2626
|
+
while idx < len(out):
|
|
2627
|
+
m = re.search(r"\binput\b", out[idx:])
|
|
2628
|
+
if m is None:
|
|
2629
|
+
break
|
|
2630
|
+
start = idx + m.start()
|
|
2631
|
+
# Reject a match that is part of a longer identifier
|
|
2632
|
+
# (e.g. ``get_input_int``) — the \b guard above already handles
|
|
2633
|
+
# alphanumerics, but be defensive.
|
|
2634
|
+
if start > 0 and (out[start - 1].isalnum() or out[start - 1] == "_"):
|
|
2635
|
+
idx = start + len("input")
|
|
2636
|
+
continue
|
|
2637
|
+
j = start + len("input")
|
|
2638
|
+
# Optional ``.<member>`` for the typed form ``input.int(...)``.
|
|
2639
|
+
member = None
|
|
2640
|
+
if j < len(out) and out[j] == ".":
|
|
2641
|
+
k = j + 1
|
|
2642
|
+
nm_start = k
|
|
2643
|
+
while k < len(out) and (out[k].isalnum() or out[k] == "_"):
|
|
2644
|
+
k += 1
|
|
2645
|
+
if k > nm_start:
|
|
2646
|
+
member = out[nm_start:k]
|
|
2647
|
+
j = k
|
|
2648
|
+
# Must be followed by ``(`` to be a call.
|
|
2649
|
+
if j >= len(out) or out[j] != "(":
|
|
2650
|
+
idx = j
|
|
2651
|
+
continue
|
|
2652
|
+
# Walk the balanced parens to extract the call substring.
|
|
2653
|
+
depth = 0
|
|
2654
|
+
k = j
|
|
2655
|
+
while k < len(out):
|
|
2656
|
+
ch = out[k]
|
|
2657
|
+
if ch == "(":
|
|
2658
|
+
depth += 1
|
|
2659
|
+
elif ch == ")":
|
|
2660
|
+
depth -= 1
|
|
2661
|
+
if depth == 0:
|
|
2662
|
+
k += 1
|
|
2663
|
+
break
|
|
2664
|
+
k += 1
|
|
2665
|
+
if depth != 0:
|
|
2666
|
+
# Unbalanced — bail on this match.
|
|
2667
|
+
idx = j + 1
|
|
2668
|
+
continue
|
|
2669
|
+
call_src = out[start:k]
|
|
2670
|
+
try:
|
|
2671
|
+
from ..lexer import Lexer
|
|
2672
|
+
from ..parser import Parser
|
|
2673
|
+
tokens = Lexer(call_src).tokenize()
|
|
2674
|
+
node = Parser(tokens, source=call_src)._parse_expression()
|
|
2675
|
+
if not self._is_input_call(node):
|
|
2676
|
+
idx = k
|
|
2677
|
+
continue
|
|
2678
|
+
func_name_i, namespace_i = self._resolve_callee(node.callee)
|
|
2679
|
+
# Inline inputs have no enclosing var; reuse the default
|
|
2680
|
+
# value as a synthetic title key so distinct defaults get
|
|
2681
|
+
# distinct input controls (and identical defaults collapse,
|
|
2682
|
+
# which is correct since they resolve to the same value).
|
|
2683
|
+
default_node = self._get_input_default(node)
|
|
2684
|
+
synth_title = self._visit_expr(default_node) if default_node is not None else ""
|
|
2685
|
+
title = self._get_input_title(node, var_name=None)
|
|
2686
|
+
if not title:
|
|
2687
|
+
title = synth_title
|
|
2688
|
+
rendered = self._render_input_value(node, func_name_i, namespace_i, title)
|
|
2689
|
+
except Exception:
|
|
2690
|
+
idx = k
|
|
2691
|
+
continue
|
|
2692
|
+
out = out[:start] + rendered + out[k:]
|
|
2693
|
+
found = True
|
|
2694
|
+
idx = start + len(rendered)
|
|
2695
|
+
return out, found
|
|
2696
|
+
|
|
1366
2697
|
def _runtime_ctor_arg_for_reset(self, arg_str: str) -> str | None:
|
|
1367
|
-
"""Convert a TA ctor-arg string into its runtime C++ expression
|
|
1368
|
-
|
|
1369
|
-
runtime expression (e.g.
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
2698
|
+
"""Convert a TA ctor-arg string into its runtime C++ expression.
|
|
2699
|
+
|
|
2700
|
+
Returns the runtime expression (e.g.
|
|
2701
|
+
``get_input_int("MACD Fast", 12)`` or a ternary / math expression
|
|
2702
|
+
over such reads and ``timeframe.*`` members) when the ctor arg
|
|
2703
|
+
depends on a stable runtime scalar — an input-backed variable, a
|
|
2704
|
+
``timeframe.*`` member, or arithmetic / ternaries / casts over
|
|
2705
|
+
those. Returns None for pure literals or expressions that contain
|
|
2706
|
+
any unrecognised (potentially series) identifier, so the caller
|
|
2707
|
+
(the TA ctor guard) rejects them loudly instead of silently
|
|
2708
|
+
emitting period 1.
|
|
1373
2709
|
"""
|
|
1374
2710
|
import re
|
|
1375
2711
|
ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
2712
|
+
|
|
2713
|
+
# Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)``,
|
|
2714
|
+
# ``fastPeriod`` -> ``(isM5 ? ... : ...)``) to their raw RHS so input
|
|
2715
|
+
# leaves become get_input_*() reads below. Recursive (bounded) to
|
|
2716
|
+
# handle chains of derived vars; guards against cycles.
|
|
2717
|
+
def _expand_derived(s: str, seen: frozenset = frozenset(), depth: int = 0) -> str:
|
|
2718
|
+
if depth > 32:
|
|
2719
|
+
return s
|
|
2720
|
+
def _rep(p: re.Match) -> str:
|
|
2721
|
+
nm = p.group(0)
|
|
2722
|
+
if nm in self._derived_input_expr and nm not in seen:
|
|
2723
|
+
inner = self._derived_input_expr[nm]
|
|
2724
|
+
return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
|
|
2725
|
+
return nm
|
|
2726
|
+
return ident_re.sub(_rep, s)
|
|
2727
|
+
|
|
2728
|
+
expanded = _expand_derived(arg_str)
|
|
2729
|
+
|
|
2730
|
+
tokens = set(ident_re.findall(expanded))
|
|
2731
|
+
|
|
2732
|
+
# Gate: every identifier token must be renderable. If any token is an
|
|
2733
|
+
# unrecognised bare identifier (not an input, not a known const, not
|
|
2734
|
+
# a structural keyword / namespace prefix, not a stable tracked var
|
|
2735
|
+
# that we already expanded), we conservatively refuse — that identifier
|
|
2736
|
+
# would otherwise leak through as an undeclared C++ symbol, or worse,
|
|
2737
|
+
# a series var that should have been rejected by the ctor guard.
|
|
2738
|
+
structural = (set(self._PINE_LOGICAL_OPS)
|
|
2739
|
+
| {"timeframe", "math", "syminfo",
|
|
2740
|
+
"int", "float", "bool", "string",
|
|
2741
|
+
"true", "false", "na"})
|
|
2742
|
+
member_tokens = (set(self._TIMEFRAME_MEMBER_CPP)
|
|
2743
|
+
| set(self._MATH_MEMBER_CPP))
|
|
2744
|
+
renderable = (self._input_backed_vars
|
|
2745
|
+
| self._stable_runtime_vars
|
|
2746
|
+
| structural
|
|
2747
|
+
| member_tokens)
|
|
2748
|
+
leftover = tokens - renderable
|
|
2749
|
+
# Known compile-time consts that survived expansion (e.g. ``pi`` was
|
|
2750
|
+
# NOT tracked as a Python value but its name token is a stable var
|
|
2751
|
+
# already; pure numeric names are in _known_vars and covered above).
|
|
2752
|
+
leftover = {t for t in leftover if t not in self._known_vars}
|
|
2753
|
+
# Inline ``input(...)`` / ``input.<t>(...)`` calls (a bare input
|
|
2754
|
+
# expression passed straight as a length arg, e.g.
|
|
2755
|
+
# ``adx(input(15), input(15))``) are re-parsed and rendered below,
|
|
2756
|
+
# after the gate. ``input`` is the only token they contribute, so
|
|
2757
|
+
# allow it through the gate here.
|
|
2758
|
+
leftover.discard("input")
|
|
2759
|
+
if leftover:
|
|
1379
2760
|
return None
|
|
1380
2761
|
|
|
1381
|
-
#
|
|
1382
|
-
#
|
|
1383
|
-
#
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2762
|
+
# Must depend on at least one runtime component (input-backed var, a
|
|
2763
|
+
# timeframe reference, or an inline input() call); otherwise it's a
|
|
2764
|
+
# pure compile-time expr and no reset is needed (the ctor-init literal
|
|
2765
|
+
# is correct).
|
|
2766
|
+
has_input = any(t in self._input_backed_vars for t in tokens)
|
|
2767
|
+
has_timeframe = "timeframe" in tokens
|
|
2768
|
+
has_inline_input = "input" in tokens
|
|
2769
|
+
if not (has_input or has_timeframe or has_inline_input):
|
|
2770
|
+
return None
|
|
2771
|
+
|
|
2772
|
+
# Preferred path: re-parse the (gate-approved, grouping-faithful)
|
|
2773
|
+
# expression and lower it through the SAME expression visitor the
|
|
2774
|
+
# statement path uses. Operator grouping and Pine numeric typing
|
|
2775
|
+
# (``/`` always yields float; ``(double)`` coercion) are then correct
|
|
2776
|
+
# by construction — reused from ``_visit_binop``, not re-derived here.
|
|
2777
|
+
# Falls back to the legacy token-substitution renderer below only if
|
|
2778
|
+
# re-parse / lowering unexpectedly fails, so a working site is never
|
|
2779
|
+
# worse off than before this change.
|
|
2780
|
+
lowered = self._lower_reset_expr_via_visitor(expanded)
|
|
2781
|
+
if lowered is not None:
|
|
2782
|
+
return lowered
|
|
2783
|
+
|
|
2784
|
+
# Render inline input() calls to override-aware get_input_*() reads
|
|
2785
|
+
# now that the gate has accepted the expression. Done after the gate
|
|
2786
|
+
# so the rendered getter tokens (get_input_int, ...) do not have to
|
|
2787
|
+
# be added to ``renderable``.
|
|
2788
|
+
if has_inline_input:
|
|
2789
|
+
expanded, _ = self._render_inline_input_calls(expanded)
|
|
2790
|
+
|
|
2791
|
+
expr = expanded
|
|
2792
|
+
|
|
2793
|
+
# 1) timeframe.<member> -> C++ (before ident substitution so the
|
|
2794
|
+
# member names don't get caught by the identifier pass). Use a
|
|
2795
|
+
# targeted regex so e.g. ``isminutes`` is not confused with
|
|
2796
|
+
# ``ismonthly``.
|
|
2797
|
+
def _tf_rep(p: re.Match) -> str:
|
|
2798
|
+
mem = p.group(1)
|
|
2799
|
+
return self._TIMEFRAME_MEMBER_CPP.get(mem, p.group(0))
|
|
2800
|
+
# ``timeframe.in_seconds()`` is a function-form member in Pine (the
|
|
2801
|
+
# only one in the table); its C++ form ``tf_to_seconds(script_tf_)``
|
|
2802
|
+
# is already a complete call, so consume the Pine ``()`` to avoid a
|
|
2803
|
+
# double-call ``tf_to_seconds(script_tf_)()``. Property-form members
|
|
2804
|
+
# (``timeframe.isdaily``) never carry ``()`` so the optional group is
|
|
2805
|
+
# a no-op for them.
|
|
2806
|
+
expr = re.sub(r"\btimeframe\.(\w+)(?:\(\))?", _tf_rep, expr)
|
|
2807
|
+
|
|
2808
|
+
# 2) math.<member> -> C++ (constants + std::* functions).
|
|
2809
|
+
def _math_rep(p: re.Match) -> str:
|
|
2810
|
+
mem = p.group(1)
|
|
2811
|
+
return self._MATH_MEMBER_CPP.get(mem, p.group(0))
|
|
2812
|
+
expr = re.sub(r"\bmath\.(\w+)", _math_rep, expr)
|
|
2813
|
+
|
|
2814
|
+
# 3) Pine word-logical operators -> C++ operators (after timeframe/math
|
|
2815
|
+
# substitution so we don't rewrite inside their C++ expansions).
|
|
2816
|
+
for pine_op, cpp_op in self._PINE_LOGICAL_OPS.items():
|
|
2817
|
+
expr = re.sub(rf"\b{pine_op}\b", cpp_op, expr)
|
|
2818
|
+
|
|
2819
|
+
# 4) Substitute input-backed vars with override-aware get_input_*()
|
|
2820
|
+
# reads, and inline known compile-time consts (non-input) as literals.
|
|
2821
|
+
def _sub(p: re.Match) -> str:
|
|
2822
|
+
name = p.group(0)
|
|
2823
|
+
if name in self._input_backed_vars:
|
|
2824
|
+
call_node = self._input_var_to_call.get(name)
|
|
2825
|
+
if call_node is None:
|
|
2826
|
+
return name
|
|
2827
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
2828
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
2829
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
2830
|
+
if name in self._known_vars and name not in self._input_backed_vars:
|
|
2831
|
+
val = self._known_vars[name]
|
|
2832
|
+
if isinstance(val, bool):
|
|
2833
|
+
return "true" if val else "false"
|
|
2834
|
+
if isinstance(val, (int, float)):
|
|
2835
|
+
return str(val)
|
|
2836
|
+
return f'std::string("{self._cpp_string_escape(val)}")'
|
|
2837
|
+
return name
|
|
1410
2838
|
|
|
1411
2839
|
rewritten = ident_re.sub(_sub, expr)
|
|
1412
|
-
|
|
1413
|
-
#
|
|
1414
|
-
# function appears (
|
|
1415
|
-
|
|
2840
|
+
|
|
2841
|
+
# 5) Pine auto-converts floats to ints for TA lengths; C++ does not.
|
|
2842
|
+
# If any math.* function appears (returns double) OR a timeframe.*
|
|
2843
|
+
# boolean is part of a ternary whose branches are doubles, wrap the
|
|
2844
|
+
# whole expression in an explicit int cast so the TA ctor gets an
|
|
2845
|
+
# integer length.
|
|
2846
|
+
had_math = "std::" in rewritten or bool(re.search(r"\btimeframe\b", expanded))
|
|
2847
|
+
if had_math:
|
|
1416
2848
|
return f"(int)({rewritten})"
|
|
1417
2849
|
return rewritten
|
|
1418
2850
|
|
|
2851
|
+
def _lower_reset_expr_via_visitor(self, expanded: str) -> str | None:
|
|
2852
|
+
"""Re-parse a gate-approved, fully-expanded TA-length expression and
|
|
2853
|
+
lower it through the SAME expression visitor the statement path uses,
|
|
2854
|
+
so operator grouping and Pine numeric typing are preserved identically
|
|
2855
|
+
(Pine ``/`` always yields float; ``math.*`` returns double; branches are
|
|
2856
|
+
parenthesized). Reuses ``_visit_binop`` etc.; nothing is re-typed here.
|
|
2857
|
+
|
|
2858
|
+
Input-backed variables render as override-aware ``get_input_*()`` reads
|
|
2859
|
+
(not member refs) via ``_reset_input_getter_mode`` — the reset can run
|
|
2860
|
+
in ``evaluate_security`` before the input members are initialised, so it
|
|
2861
|
+
must not depend on their init order. Inline ``input(...)`` calls and
|
|
2862
|
+
``math.*`` / ``timeframe.*`` members lower through the visitor's own
|
|
2863
|
+
handlers (same C++ the statement path would emit).
|
|
2864
|
+
|
|
2865
|
+
Returns None if re-parse / lowering fails, so the caller falls back to
|
|
2866
|
+
the legacy token-substitution renderer (never worse than before)."""
|
|
2867
|
+
try:
|
|
2868
|
+
from ..lexer import Lexer
|
|
2869
|
+
from ..parser import Parser
|
|
2870
|
+
tokens = Lexer(expanded).tokenize()
|
|
2871
|
+
node = Parser(tokens, source=expanded)._parse_expression()
|
|
2872
|
+
except Exception:
|
|
2873
|
+
return None
|
|
2874
|
+
prev = self._reset_input_getter_mode
|
|
2875
|
+
self._reset_input_getter_mode = True
|
|
2876
|
+
try:
|
|
2877
|
+
rendered = self._visit_expr(node)
|
|
2878
|
+
except Exception:
|
|
2879
|
+
return None
|
|
2880
|
+
finally:
|
|
2881
|
+
self._reset_input_getter_mode = prev
|
|
2882
|
+
if not rendered or "/* " in rendered:
|
|
2883
|
+
# Unknown/unhandled node leaked a placeholder — defer to legacy.
|
|
2884
|
+
return None
|
|
2885
|
+
# A TA length must be int. Truncate when the lowered form is
|
|
2886
|
+
# float-typed (a ``std::*`` call, a ``(double)`` division coercion, or a
|
|
2887
|
+
# ``timeframe.*`` helper — all timeframe helpers reference script_tf_).
|
|
2888
|
+
# A bare int / identifier length carries none of these and is left
|
|
2889
|
+
# unwrapped, so simple sites stay byte-identical to the legacy output.
|
|
2890
|
+
if ("std::" in rendered or "(double)" in rendered
|
|
2891
|
+
or "script_tf_" in rendered):
|
|
2892
|
+
return f"(int)({rendered})"
|
|
2893
|
+
return rendered
|
|
2894
|
+
|
|
2895
|
+
|
|
1419
2896
|
def _collect_ta_runtime_resets(self) -> list[str]:
|
|
1420
2897
|
"""Collect reassignment statements for every TA object whose ctor args
|
|
1421
2898
|
depend on an input-backed variable. Returned strings are raw C++
|
|
@@ -1427,7 +2904,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1427
2904
|
resets: list[str] = []
|
|
1428
2905
|
|
|
1429
2906
|
# Main-context TA objects
|
|
1430
|
-
for site in self.ctx.ta_call_sites:
|
|
2907
|
+
for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
|
|
2908
|
+
if _ta_idx in self._dead_ta_indices:
|
|
2909
|
+
continue
|
|
1431
2910
|
if not site.ctor_args:
|
|
1432
2911
|
continue
|
|
1433
2912
|
runtime_args: list[str] = []
|
|
@@ -1445,15 +2924,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1445
2924
|
f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
|
|
1446
2925
|
)
|
|
1447
2926
|
|
|
1448
|
-
# Security-context TA copies
|
|
2927
|
+
# Security-context TA copies. Normally these share the ctor args of
|
|
2928
|
+
# their main-context site, but a request.security nested in a helper
|
|
2929
|
+
# called at several sites is cloned per call site (distinct sec_id +
|
|
2930
|
+
# callsite_idx) while the shared TA site's ``ctor_args`` were resolved
|
|
2931
|
+
# ONCE (against the first call site). Every clone would then size its
|
|
2932
|
+
# indicator from call site 0's argument (e.g. four EMAs all pinned to
|
|
2933
|
+
# crossFastLen instead of the per-site fast/slow lengths). Resolve each
|
|
2934
|
+
# sec's ctor args against ITS call site by reusing the per-call-site
|
|
2935
|
+
# function-clone TA remap (identity for cs0 / non-clones, so all other
|
|
2936
|
+
# output stays byte-identical).
|
|
2937
|
+
sec_call_by_id = {it["sec_id"]: it for it in self._security_calls}
|
|
2938
|
+
ta_site_by_member = {s.member_name: s for s in self.ctx.ta_call_sites}
|
|
1449
2939
|
for info in self._security_eval_info:
|
|
2940
|
+
sec_item = sec_call_by_id.get(info["sec_id"])
|
|
2941
|
+
sec_containing = (sec_item or {}).get("containing_func") or ""
|
|
2942
|
+
sec_cs_idx = (sec_item or {}).get("callsite_idx")
|
|
1450
2943
|
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
1451
2944
|
site = self.ctx.ta_call_sites[idx]
|
|
1452
|
-
|
|
2945
|
+
ctor_site = site
|
|
2946
|
+
if sec_containing and sec_cs_idx is not None:
|
|
2947
|
+
remap = self._func_cs_ta_remap.get((sec_containing, sec_cs_idx))
|
|
2948
|
+
if remap:
|
|
2949
|
+
cloned_name = remap.get(site.member_name)
|
|
2950
|
+
if cloned_name and cloned_name != site.member_name:
|
|
2951
|
+
cand = ta_site_by_member.get(cloned_name)
|
|
2952
|
+
if cand is not None:
|
|
2953
|
+
ctor_site = cand
|
|
2954
|
+
if not ctor_site.ctor_args:
|
|
1453
2955
|
continue
|
|
1454
2956
|
runtime_args = []
|
|
1455
2957
|
any_runtime = False
|
|
1456
|
-
for a in
|
|
2958
|
+
for a in ctor_site.ctor_args:
|
|
1457
2959
|
rt = self._runtime_ctor_arg_for_reset(a)
|
|
1458
2960
|
if rt is not None:
|
|
1459
2961
|
runtime_args.append(rt)
|