@pineforge/codegen-pyodide 0.8.1 → 0.10.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 +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1348 -113
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +258 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +559 -36
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +165 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +149 -15
- package/pineforge_codegen/codegen/visit_stmt.py +85 -38
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -53,6 +53,7 @@ from .tables import (
|
|
|
53
53
|
SKIP_VAR_TYPES,
|
|
54
54
|
SYMINFO_MEMBER_MAP,
|
|
55
55
|
COLOR_CONST_MAP,
|
|
56
|
+
ARRAY_NEW_CTORS,
|
|
56
57
|
ARRAY_METHODS,
|
|
57
58
|
MAP_METHODS,
|
|
58
59
|
MATRIX_METHODS,
|
|
@@ -65,6 +66,15 @@ from .tables import (
|
|
|
65
66
|
_merge_kwargs,
|
|
66
67
|
)
|
|
67
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
|
+
|
|
68
78
|
# (TA_IMPLICIT_COMPUTE / TA_COMPUTE_ARGS now imported from .tables above.)
|
|
69
79
|
|
|
70
80
|
# (TA_IMPLICIT_COMPUTE_FULL / TA_IMPLICIT_APPEND / PINE_TYPE_TO_CPP /
|
|
@@ -181,6 +191,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
181
191
|
self._func_cs_var_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
182
192
|
# Active var name remap (set during per-call-site function emission)
|
|
183
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
|
|
184
201
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
185
202
|
self._func_var_members_set: set[str] = set()
|
|
186
203
|
# BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
|
|
@@ -190,6 +207,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
190
207
|
# needed: names are function-unique and the value-copy fallback ignores
|
|
191
208
|
# entries for inactive functions.
|
|
192
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()
|
|
193
220
|
self._precalc_loop_active: bool = False
|
|
194
221
|
# Names of ``var`` members that live in a FUNCTION scope (not global).
|
|
195
222
|
# These are initialized once-per-function-variant on first call (a
|
|
@@ -328,14 +355,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
328
355
|
self._instance_dispatch: dict[tuple[str | None, int], str] = {}
|
|
329
356
|
self._fresh_instances: list[dict] = []
|
|
330
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]] = []
|
|
331
363
|
# NOTE: _build_func_instances() runs at the top of generate() (it needs
|
|
332
364
|
# _all_member_names / _func_safe_name, which are populated later in __init__).
|
|
333
365
|
# Build lookup: node id -> FixnanCallSite (counter-based)
|
|
334
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()
|
|
335
380
|
self._switch_counter = 0
|
|
336
381
|
self._security_inline_counter = 0
|
|
337
382
|
self._random_call_counter = 0
|
|
338
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
|
|
339
396
|
# UDT / enum (needed before _collect_known_vars for input.enum)
|
|
340
397
|
self._udt_defs: dict[str, list] = {}
|
|
341
398
|
self._enum_defs: dict[str, list[str]] = {}
|
|
@@ -367,9 +424,25 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
367
424
|
# ctor-init list still folds to the Pine-default literal via _resolve_known.
|
|
368
425
|
self._derived_input_expr: dict[str, str] = {}
|
|
369
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)
|
|
370
443
|
self._collect_known_vars()
|
|
371
444
|
# Track var names
|
|
372
|
-
self._var_names
|
|
445
|
+
self._var_names = set()
|
|
373
446
|
for name, _, _ in ctx.var_members:
|
|
374
447
|
self._var_names.add(name)
|
|
375
448
|
# Every name bound ANYWHERE in the program (top-level, nested in
|
|
@@ -386,6 +459,87 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
386
459
|
for fi in ctx.func_infos:
|
|
387
460
|
self._func_names.add(fi.name)
|
|
388
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
|
|
389
543
|
# Track strategy series vars (e.g., strategy.closedtrades[1])
|
|
390
544
|
self._strategy_series_vars: set[str] = set()
|
|
391
545
|
# Track global-scope non-var declarations (emitted as class members)
|
|
@@ -424,12 +578,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
424
578
|
self._security_calls: list[dict] = [self._normalize_security_call(item) for item in ctx.security_calls]
|
|
425
579
|
# Current function parameter types (set during _emit_func_def)
|
|
426
580
|
self._current_func_param_types: dict[str, str] = {}
|
|
581
|
+
self._current_func_param_specs: dict[str, "TypeSpec"] = {}
|
|
427
582
|
# Current function params that are series (const Series<double>&)
|
|
428
583
|
self._current_func_series_params: set[str] = set()
|
|
429
584
|
# Locals declared in the function currently being emitted (symbol table loses them after analysis)
|
|
430
585
|
self._current_func_locals: set[str] = set()
|
|
586
|
+
self._current_func_local_types: dict[str, str] = {}
|
|
431
587
|
# for-in loop iterator names (must resolve member access, not enum fallback)
|
|
432
588
|
self._current_loop_vars: set[str] = set()
|
|
589
|
+
self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
|
|
433
590
|
# Track array variables for codegen
|
|
434
591
|
self._array_vars: set[str] = set()
|
|
435
592
|
# Track map variables for codegen
|
|
@@ -553,6 +710,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
553
710
|
self._all_member_names.add(self._safe_name(name))
|
|
554
711
|
|
|
555
712
|
self._register_global_aggregate_member_types()
|
|
713
|
+
self._register_udt_array_get_ref_locals()
|
|
556
714
|
self._uses_matrix = self._detect_matrix_usage()
|
|
557
715
|
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
558
716
|
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
@@ -645,7 +803,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
645
803
|
ctx = self.ctx
|
|
646
804
|
stateful = (set(ctx.func_ta_ranges.keys())
|
|
647
805
|
| set(ctx.func_var_members.keys())
|
|
648
|
-
| set(ctx.func_series_vars.keys())
|
|
806
|
+
| set(ctx.func_series_vars.keys())
|
|
807
|
+
| set(ctx.func_fixnan_indices.keys())
|
|
808
|
+
| set(ctx.func_security_clone_only))
|
|
649
809
|
if not stateful:
|
|
650
810
|
return
|
|
651
811
|
|
|
@@ -661,8 +821,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
661
821
|
def var_originals(fname: str) -> list[str]:
|
|
662
822
|
return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
|
|
663
823
|
|
|
824
|
+
def fixnan_originals(fname: str) -> list[str]:
|
|
825
|
+
return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
|
|
826
|
+
|
|
664
827
|
def natural_name(fname: str, cs_idx: int) -> str:
|
|
665
|
-
return f"{self.
|
|
828
|
+
return f"{self._func_cpp_base_name(fname)}_cs{cs_idx}"
|
|
666
829
|
|
|
667
830
|
interned: dict[tuple, dict] = {}
|
|
668
831
|
worklist: list[dict] = []
|
|
@@ -670,7 +833,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
670
833
|
fresh_counter = 0
|
|
671
834
|
|
|
672
835
|
# Seed with the natural clones the flat emission loop produces.
|
|
673
|
-
for fname in stateful:
|
|
836
|
+
for fname in sorted(stateful):
|
|
674
837
|
if fname not in func_bodies:
|
|
675
838
|
continue
|
|
676
839
|
total_cs = ctx.func_call_site_counts.get(fname, 0)
|
|
@@ -681,13 +844,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
681
844
|
"name": natural_name(fname, k),
|
|
682
845
|
"ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
|
|
683
846
|
"var_remap": self._func_cs_var_remap.get((fname, k), {}),
|
|
847
|
+
"fixnan_remap": self._func_cs_fixnan_remap.get((fname, k), {}),
|
|
684
848
|
})
|
|
685
849
|
else:
|
|
686
850
|
worklist.append({
|
|
687
851
|
"fname": fname,
|
|
688
|
-
"name": self.
|
|
852
|
+
"name": self._func_cpp_base_name(fname),
|
|
689
853
|
"ta_remap": {},
|
|
690
854
|
"var_remap": {},
|
|
855
|
+
"fixnan_remap": {},
|
|
691
856
|
})
|
|
692
857
|
|
|
693
858
|
while worklist:
|
|
@@ -699,6 +864,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
699
864
|
if not body:
|
|
700
865
|
continue
|
|
701
866
|
active_ta = inst["ta_remap"]
|
|
867
|
+
active_fixnan = inst.get("fixnan_remap", {})
|
|
702
868
|
for callnode in self._iter_func_calls(body):
|
|
703
869
|
cs_info = ctx.func_call_cs_map.get(id(callnode))
|
|
704
870
|
if cs_info is None:
|
|
@@ -711,26 +877,49 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
711
877
|
for m in ta_originals(g_name):
|
|
712
878
|
mid = natural_ta.get(m, m)
|
|
713
879
|
composed_ta[m] = active_ta.get(mid, mid)
|
|
714
|
-
|
|
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:
|
|
715
886
|
# Path resolves to the callee's own cs{j} clone — reuse it.
|
|
716
887
|
self._instance_dispatch[(inst["name"], id(callnode))] = \
|
|
717
888
|
natural_name(g_name, j)
|
|
718
889
|
continue
|
|
719
|
-
key = (g_name, frozenset(composed_ta.items())
|
|
890
|
+
key = (g_name, frozenset(composed_ta.items()),
|
|
891
|
+
frozenset(composed_fixnan.items()))
|
|
720
892
|
ginst = interned.get(key)
|
|
721
893
|
if ginst is None:
|
|
722
894
|
fresh_counter += 1
|
|
723
|
-
inst_name = f"{self.
|
|
895
|
+
inst_name = f"{self._func_cpp_base_name(g_name)}__ni{fresh_counter}"
|
|
724
896
|
fvar_remap: dict[str, str] = {}
|
|
725
897
|
for v in var_originals(g_name):
|
|
726
898
|
fresh_member = f"{v}__ni{fresh_counter}"
|
|
727
899
|
fvar_remap[v] = fresh_member
|
|
728
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
|
+
)
|
|
729
917
|
ginst = {
|
|
730
918
|
"fname": g_name,
|
|
731
919
|
"name": inst_name,
|
|
732
920
|
"ta_remap": composed_ta,
|
|
733
921
|
"var_remap": fvar_remap,
|
|
922
|
+
"fixnan_remap": ffixnan_remap,
|
|
734
923
|
}
|
|
735
924
|
interned[key] = ginst
|
|
736
925
|
self._fresh_instances.append(ginst)
|
|
@@ -854,31 +1043,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
854
1043
|
spec = self._matrix_specs.get(recv_name) or TypeSpec.matrix(TypeSpec.primitive("float"))
|
|
855
1044
|
self._matrix_specs[name] = spec
|
|
856
1045
|
self._collection_types[name] = spec
|
|
857
|
-
elif ns == "array" and fn in (
|
|
858
|
-
"new",
|
|
859
|
-
"new_float",
|
|
860
|
-
"new_int",
|
|
861
|
-
"new_bool",
|
|
862
|
-
"new_string",
|
|
863
|
-
"from",
|
|
864
|
-
):
|
|
1046
|
+
elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
|
|
865
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
|
|
866
1050
|
elif ns == "map" and fn == "new":
|
|
867
1051
|
self._map_vars.add(name)
|
|
868
1052
|
|
|
869
|
-
# Also register var/varip
|
|
870
|
-
#
|
|
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()``.
|
|
871
1057
|
var_decl_map: dict[str, FuncCall] = {}
|
|
872
1058
|
for stmt in (self.ctx.ast.body if hasattr(self.ctx, "ast") else []):
|
|
873
1059
|
if isinstance(stmt, VarDecl) and isinstance(stmt.value, FuncCall):
|
|
874
1060
|
var_decl_map[stmt.name] = stmt.value
|
|
875
1061
|
for name, _ptype, _init_str in self.ctx.var_members:
|
|
876
|
-
if name in self._matrix_specs:
|
|
877
|
-
continue
|
|
878
1062
|
expr = var_decl_map.get(name)
|
|
879
1063
|
if expr is None:
|
|
880
1064
|
continue
|
|
881
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
|
|
882
1073
|
if ns2 == "matrix" and fn2 == "new":
|
|
883
1074
|
targs2 = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
|
|
884
1075
|
elem_spec2 = self._type_spec_from_hint_name(targs2[0]) if targs2 else TypeSpec.primitive("float")
|
|
@@ -901,6 +1092,68 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
901
1092
|
self._matrix_specs[name] = spec2
|
|
902
1093
|
self._collection_types[name] = spec2
|
|
903
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
|
+
|
|
904
1157
|
def _extract_receiver_name(self, call_node) -> str | None:
|
|
905
1158
|
"""Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
|
|
906
1159
|
|
|
@@ -1009,9 +1262,184 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1009
1262
|
# First, find all variables that are reassigned anywhere in the AST.
|
|
1010
1263
|
# These cannot be inlined as constants since their value changes at runtime.
|
|
1011
1264
|
reassigned = self._find_reassigned_vars()
|
|
1265
|
+
# Register never-reassigned ``var``/``varip`` scalars with a stable
|
|
1266
|
+
# init FIRST — a later derived var (or a UDF body) may reference such a
|
|
1267
|
+
# scalar as a stable length component, so it must be classified stable
|
|
1268
|
+
# before those exprs are evaluated below.
|
|
1269
|
+
self._collect_stable_var_scalars(reassigned)
|
|
1012
1270
|
for stmt in self.ctx.ast.body:
|
|
1013
1271
|
if isinstance(stmt, VarDecl) and stmt.name not in reassigned:
|
|
1014
1272
|
self._collect_known_var(stmt)
|
|
1273
|
+
# A second pass handles the stable-reassigned-scalar pattern: a
|
|
1274
|
+
# class-scope scalar initialized from a stable expr and reassigned
|
|
1275
|
+
# ONLY inside top-level if/elif chains whose conditions and assigned
|
|
1276
|
+
# values are themselves stable (inputs / timeframe.* / math.*). Such
|
|
1277
|
+
# a var is a bar-invariant scalar and may feed a TA ctor length with
|
|
1278
|
+
# a runtime reset that reproduces the conditional logic. Series-
|
|
1279
|
+
# dependent reassignments are left untracked (rejected by the guard).
|
|
1280
|
+
self._collect_reassigned_stable_scalars(reassigned)
|
|
1281
|
+
|
|
1282
|
+
def _collect_stable_var_scalars(self, reassigned: set[str]) -> None:
|
|
1283
|
+
"""Track top-level ``var``/``varip`` scalars declared exactly once (never
|
|
1284
|
+
reassigned) from a stable init expression.
|
|
1285
|
+
|
|
1286
|
+
A ``var`` scalar's one-shot initializer runs once and the value never
|
|
1287
|
+
changes across bars, so a never-reassigned ``var`` over a stable init
|
|
1288
|
+
(``var int _tfSec = timeframe.in_seconds()``) is a bar-invariant scalar —
|
|
1289
|
+
safe to embed in a TA ctor runtime-reset expression. Recording it in
|
|
1290
|
+
``_derived_input_expr`` + ``_stable_runtime_vars`` lets the reset path
|
|
1291
|
+
expand the name and lets ``_expr_is_stable`` classify it (the
|
|
1292
|
+
``_stable_runtime_vars`` check precedes the ``_var_names`` rejection).
|
|
1293
|
+
|
|
1294
|
+
A ``var`` that is reassigned anywhere (``:=`` in ``reassigned``), is a
|
|
1295
|
+
series var, or is initialized from a non-stable value stays untracked —
|
|
1296
|
+
so a TA length fed by genuinely-mutable persistent state is still
|
|
1297
|
+
rejected by the constructor guard. Names are NOT folded into
|
|
1298
|
+
``_known_vars`` (no use-site inlining): only the length-analysis path is
|
|
1299
|
+
affected, and the ``var`` member still emits and initializes normally.
|
|
1300
|
+
"""
|
|
1301
|
+
for stmt in (self.ctx.ast.body or []):
|
|
1302
|
+
if not isinstance(stmt, VarDecl):
|
|
1303
|
+
continue
|
|
1304
|
+
if not (stmt.is_var or stmt.is_varip):
|
|
1305
|
+
continue
|
|
1306
|
+
if stmt.name in reassigned:
|
|
1307
|
+
continue
|
|
1308
|
+
if stmt.name in self.ctx.series_vars:
|
|
1309
|
+
continue
|
|
1310
|
+
if stmt.value is None or not self._expr_is_stable(stmt.value):
|
|
1311
|
+
continue
|
|
1312
|
+
expr_str = self._arith_expr_to_str(stmt.value)
|
|
1313
|
+
if expr_str is None:
|
|
1314
|
+
continue
|
|
1315
|
+
self._derived_input_expr[stmt.name] = expr_str
|
|
1316
|
+
self._stable_runtime_vars.add(stmt.name)
|
|
1317
|
+
# Mark input-backed iff the init references an input, so the reset
|
|
1318
|
+
# emits override-aware get_input_*() reads for it.
|
|
1319
|
+
import re as _re
|
|
1320
|
+
toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
|
|
1321
|
+
if any(t in self._input_backed_vars for t in toks):
|
|
1322
|
+
self._input_backed_vars.add(stmt.name)
|
|
1323
|
+
|
|
1324
|
+
def _collect_reassigned_stable_scalars(self, reassigned: set[str]) -> None:
|
|
1325
|
+
"""Track class-scope scalars that are reassigned but only along stable
|
|
1326
|
+
if/elif paths (see ``test_stable_reassigned_class_scope_length``).
|
|
1327
|
+
|
|
1328
|
+
For each top-level ``v = <init>`` whose name is reassigned, build the
|
|
1329
|
+
final value as a nested ternary by folding subsequent top-level
|
|
1330
|
+
IfStmts / direct Assignments. If every condition and every assigned
|
|
1331
|
+
RHS is stable (and renderable), record the ternary in
|
|
1332
|
+
``_derived_input_expr`` and add ``v`` to ``_stable_runtime_vars`` so
|
|
1333
|
+
the TA ctor reset path can expand it. Anything non-stable (a ta.*
|
|
1334
|
+
result, a bar field, a series var) leaves the var untracked, so the
|
|
1335
|
+
ctor guard still rejects it loudly.
|
|
1336
|
+
"""
|
|
1337
|
+
from ..ast_nodes import IfStmt, Assignment
|
|
1338
|
+
body = self.ctx.ast.body or []
|
|
1339
|
+
# Pre-resolve each reassigned var's initial VarDecl.
|
|
1340
|
+
inits: dict[str, object] = {}
|
|
1341
|
+
for stmt in body:
|
|
1342
|
+
if (isinstance(stmt, VarDecl) and stmt.name in reassigned
|
|
1343
|
+
and not stmt.is_var and not stmt.is_varip):
|
|
1344
|
+
# Only consider vars whose initial value is itself stable;
|
|
1345
|
+
# an unstable init cannot become a stable scalar via later
|
|
1346
|
+
# reassignment.
|
|
1347
|
+
if stmt.value is not None and self._expr_is_stable(stmt.value):
|
|
1348
|
+
inits[stmt.name] = stmt.value
|
|
1349
|
+
if not inits:
|
|
1350
|
+
return
|
|
1351
|
+
|
|
1352
|
+
def _value_after(stmts, fallback: str | None) -> str | None:
|
|
1353
|
+
"""Fold a statement list into the final value expression for the
|
|
1354
|
+
target var, given ``fallback`` as the value on entry. Returns None
|
|
1355
|
+
if any condition / assignment is non-stable or unrenderable."""
|
|
1356
|
+
current = fallback
|
|
1357
|
+
for s in stmts or []:
|
|
1358
|
+
if isinstance(s, Assignment) and isinstance(s.target, Identifier):
|
|
1359
|
+
if s.target.name != target_name:
|
|
1360
|
+
continue
|
|
1361
|
+
if s.op != ":=":
|
|
1362
|
+
return None # compound assignment — not a stable fold
|
|
1363
|
+
if not self._expr_is_stable(s.value):
|
|
1364
|
+
return None
|
|
1365
|
+
rhs = self._arith_expr_to_str(s.value)
|
|
1366
|
+
if rhs is None:
|
|
1367
|
+
return None
|
|
1368
|
+
current = rhs
|
|
1369
|
+
elif isinstance(s, IfStmt):
|
|
1370
|
+
# Only model IfStmts that actually reassign the target var;
|
|
1371
|
+
# an unrelated IfStmt (e.g. entry/exit logic with a series
|
|
1372
|
+
# condition) must NOT abort the fold — the var simply keeps
|
|
1373
|
+
# its current value through it.
|
|
1374
|
+
if not _reassigns(s, target_name):
|
|
1375
|
+
continue
|
|
1376
|
+
if not self._expr_is_stable(s.condition):
|
|
1377
|
+
return None
|
|
1378
|
+
cond = self._arith_expr_to_str(s.condition)
|
|
1379
|
+
if cond is None:
|
|
1380
|
+
return None
|
|
1381
|
+
then_val = _value_after(s.body, current)
|
|
1382
|
+
if then_val is None:
|
|
1383
|
+
return None
|
|
1384
|
+
else_val = _value_after(s.else_body, current)
|
|
1385
|
+
if else_val is None:
|
|
1386
|
+
return None
|
|
1387
|
+
current = f"({cond} ? {then_val} : {else_val})"
|
|
1388
|
+
# Other statement shapes (for/while/switch/var decls of
|
|
1389
|
+
# other vars) are ignored for this var's value fold; they
|
|
1390
|
+
# do not reassign ``target_name`` in a way we model.
|
|
1391
|
+
return current
|
|
1392
|
+
|
|
1393
|
+
def _reassigns(node, name: str) -> bool:
|
|
1394
|
+
"""True if any ``:=`` assignment to ``name`` occurs within node."""
|
|
1395
|
+
from ..ast_nodes import IfStmt as _If, Assignment as _Asg
|
|
1396
|
+
if isinstance(node, _Asg) and isinstance(node.target, Identifier):
|
|
1397
|
+
return node.target.name == name
|
|
1398
|
+
if isinstance(node, _If):
|
|
1399
|
+
if any(_reassigns(c, name) for c in (node.body or [])):
|
|
1400
|
+
return True
|
|
1401
|
+
if any(_reassigns(c, name) for c in (node.else_body or [])):
|
|
1402
|
+
return True
|
|
1403
|
+
return False
|
|
1404
|
+
for attr in ("body", "else_body", "cases", "default_body"):
|
|
1405
|
+
sub = getattr(node, attr, None)
|
|
1406
|
+
if isinstance(sub, list):
|
|
1407
|
+
if any(_reassigns(c, name) for c in sub):
|
|
1408
|
+
return True
|
|
1409
|
+
return False
|
|
1410
|
+
|
|
1411
|
+
for target_name, init_node in inits.items():
|
|
1412
|
+
init_str = self._arith_expr_to_str(init_node)
|
|
1413
|
+
if init_str is None:
|
|
1414
|
+
continue
|
|
1415
|
+
final = _value_after(body, init_str)
|
|
1416
|
+
if final is None:
|
|
1417
|
+
continue
|
|
1418
|
+
# Sanity: the fold must actually differ from the bare init,
|
|
1419
|
+
# otherwise there were no stable reassignments and the var is
|
|
1420
|
+
# already covered (or rejected) by the main pass.
|
|
1421
|
+
if final == init_str:
|
|
1422
|
+
continue
|
|
1423
|
+
# Fold to a compile-time literal when possible (so the ctor-init
|
|
1424
|
+
# list can use it directly); otherwise record the raw expression
|
|
1425
|
+
# for the runtime reset path to expand.
|
|
1426
|
+
folded = self._resolve_known(final)
|
|
1427
|
+
if self._is_compile_time_value(folded):
|
|
1428
|
+
try:
|
|
1429
|
+
num = float(folded)
|
|
1430
|
+
self._known_vars[target_name] = (
|
|
1431
|
+
int(num) if num == int(num) else num
|
|
1432
|
+
)
|
|
1433
|
+
except ValueError:
|
|
1434
|
+
pass
|
|
1435
|
+
self._derived_input_expr[target_name] = final
|
|
1436
|
+
self._stable_runtime_vars.add(target_name)
|
|
1437
|
+
# Mark input-backed iff the expression references an input so the
|
|
1438
|
+
# override-aware get_input_*() reads are emitted on the reset path.
|
|
1439
|
+
import re as _re
|
|
1440
|
+
toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", final))
|
|
1441
|
+
if any(t in self._input_backed_vars for t in toks):
|
|
1442
|
+
self._input_backed_vars.add(target_name)
|
|
1015
1443
|
|
|
1016
1444
|
def _find_reassigned_vars(self) -> set[str]:
|
|
1017
1445
|
"""Scan AST to find all variable names that are targets of := or compound assignment."""
|
|
@@ -1038,11 +1466,276 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1038
1466
|
walk(stmt)
|
|
1039
1467
|
return reassigned
|
|
1040
1468
|
|
|
1041
|
-
|
|
1469
|
+
# ``math.*`` members that are pure functions over their (stable) args, or
|
|
1470
|
+
# stable constants. Anything outside this set (e.g. ``math.random``) is
|
|
1471
|
+
# treated as non-stable. Used by ``_expr_is_stable``.
|
|
1472
|
+
_MATH_STABLE_MEMBERS: frozenset[str] = frozenset({
|
|
1473
|
+
"pi", "e", "phi", "rphi",
|
|
1474
|
+
"abs", "max", "min", "round", "floor", "ceil",
|
|
1475
|
+
"sqrt", "log", "log10", "exp", "pow",
|
|
1476
|
+
"sin", "cos", "tan", "asin", "acos", "atan", "sign",
|
|
1477
|
+
"sum", "avg", "to_precision", "round_to_mintick",
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
# ``timeframe.*`` members that are constant for the lifetime of a run —
|
|
1481
|
+
# they reflect the script's resolution, not a per-bar value.
|
|
1482
|
+
_TF_STABLE_MEMBERS: frozenset[str] = frozenset({
|
|
1483
|
+
"period", "main_period", "multiplier",
|
|
1484
|
+
"isintraday", "isminutes", "isdaily", "isweekly",
|
|
1485
|
+
"ismonthly", "isdwm", "isseconds", "in_seconds", "isticks",
|
|
1486
|
+
})
|
|
1487
|
+
|
|
1488
|
+
# Depth ceiling for inlining nested single-expression user functions while
|
|
1489
|
+
# classifying a TA length's stability. Pine forbids recursion, so any real
|
|
1490
|
+
# chain is shallow; the cap is a backstop against pathological input and is
|
|
1491
|
+
# enforced together with a name-stack cycle guard.
|
|
1492
|
+
_UDF_INLINE_MAX_DEPTH = 16
|
|
1493
|
+
|
|
1494
|
+
def _get_udf_def(self, name: str):
|
|
1495
|
+
"""Return the top-level single-name ``FuncDef`` for ``name`` (or None).
|
|
1496
|
+
|
|
1497
|
+
Built lazily and cached. UDT ``MethodDef``s are intentionally excluded —
|
|
1498
|
+
only a free function can appear as a bare-name TA length call.
|
|
1499
|
+
"""
|
|
1500
|
+
cache = getattr(self, "_udf_def_cache", None)
|
|
1501
|
+
if cache is None:
|
|
1502
|
+
cache = {}
|
|
1503
|
+
for stmt in (self.ctx.ast.body or []):
|
|
1504
|
+
if isinstance(stmt, FuncDef):
|
|
1505
|
+
# Last definition wins; a name map is all the length path needs.
|
|
1506
|
+
cache[stmt.name] = stmt
|
|
1507
|
+
self._udf_def_cache = cache
|
|
1508
|
+
return cache.get(name)
|
|
1509
|
+
|
|
1510
|
+
def _inline_single_expr_udf(self, node, _udf_stack: frozenset = frozenset(),
|
|
1511
|
+
_depth: int = 0):
|
|
1512
|
+
"""If ``node`` is a call to a user-defined SINGLE-EXPRESSION function,
|
|
1513
|
+
return its body expression with each parameter substituted by the
|
|
1514
|
+
corresponding call-argument node. Returns None when the call is not such
|
|
1515
|
+
a function, the arity/kwargs do not match, the body is not a single
|
|
1516
|
+
expression, the body contains a shape we do not clone, or the call would
|
|
1517
|
+
recurse (cycle / depth-limit).
|
|
1518
|
+
|
|
1519
|
+
Purely structural: it does NOT judge stability (the caller does, via
|
|
1520
|
+
``_expr_is_stable`` on the returned node). The conservative None keeps
|
|
1521
|
+
the TA-ctor guard intact.
|
|
1522
|
+
"""
|
|
1523
|
+
if not isinstance(node, FuncCall):
|
|
1524
|
+
return None
|
|
1525
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
1526
|
+
if namespace is not None or func_name is None:
|
|
1527
|
+
return None
|
|
1528
|
+
if func_name in _udf_stack or _depth >= self._UDF_INLINE_MAX_DEPTH:
|
|
1529
|
+
return None
|
|
1530
|
+
fdef = self._get_udf_def(func_name)
|
|
1531
|
+
if fdef is None:
|
|
1532
|
+
return None
|
|
1533
|
+
# A body that is exactly ONE expression statement is inlinable, whether
|
|
1534
|
+
# written inline after ``=>`` (``is_single_expr=True``) or as a one-line
|
|
1535
|
+
# indented block (``f(x) =>`` then a single indented expr, which the
|
|
1536
|
+
# parser records as ``is_single_expr=False`` with a one-ExprStmt body —
|
|
1537
|
+
# the gonzowiththewind-sisyphus ``f_bars`` shape). A multi-statement
|
|
1538
|
+
# body (len != 1, or a non-ExprStmt) is conservatively refused.
|
|
1539
|
+
body = fdef.body
|
|
1540
|
+
if not body or len(body) != 1 or not isinstance(body[0], ExprStmt):
|
|
1541
|
+
return None
|
|
1542
|
+
# Require a plain positional call: one arg per param, no kwargs, no
|
|
1543
|
+
# default-parameter fill-in — anything else is conservatively refused.
|
|
1544
|
+
if node.kwargs or len(node.args) != len(fdef.params):
|
|
1545
|
+
return None
|
|
1546
|
+
subst = dict(zip(fdef.params, node.args))
|
|
1547
|
+
return self._subst_params(body[0].expr, subst)
|
|
1548
|
+
|
|
1549
|
+
def _subst_params(self, node, subst: dict):
|
|
1550
|
+
"""Return a copy of ``node`` with every ``Identifier`` whose name is a
|
|
1551
|
+
key in ``subst`` replaced by the mapped argument node. Returns None for
|
|
1552
|
+
any node outside the small arithmetic/call grammar we fold (an
|
|
1553
|
+
unrecognised construct — e.g. a history ``Subscript`` — conservatively
|
|
1554
|
+
aborts the inline). ``dataclasses.replace`` preserves ``loc``/
|
|
1555
|
+
``annotations`` so diagnostics still point at real source spans."""
|
|
1556
|
+
import dataclasses as _dc
|
|
1557
|
+
if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral)):
|
|
1558
|
+
return node
|
|
1559
|
+
if isinstance(node, Identifier):
|
|
1560
|
+
return subst.get(node.name, node)
|
|
1561
|
+
if isinstance(node, MemberAccess):
|
|
1562
|
+
obj = self._subst_params(node.object, subst)
|
|
1563
|
+
if obj is None:
|
|
1564
|
+
return None
|
|
1565
|
+
return _dc.replace(node, object=obj)
|
|
1566
|
+
if isinstance(node, Ternary):
|
|
1567
|
+
c = self._subst_params(node.condition, subst)
|
|
1568
|
+
t = self._subst_params(node.true_val, subst)
|
|
1569
|
+
f = self._subst_params(node.false_val, subst)
|
|
1570
|
+
if c is None or t is None or f is None:
|
|
1571
|
+
return None
|
|
1572
|
+
return _dc.replace(node, condition=c, true_val=t, false_val=f)
|
|
1573
|
+
if isinstance(node, BinOp):
|
|
1574
|
+
l = self._subst_params(node.left, subst)
|
|
1575
|
+
r = self._subst_params(node.right, subst)
|
|
1576
|
+
if l is None or r is None:
|
|
1577
|
+
return None
|
|
1578
|
+
return _dc.replace(node, left=l, right=r)
|
|
1579
|
+
if isinstance(node, UnaryOp):
|
|
1580
|
+
o = self._subst_params(node.operand, subst)
|
|
1581
|
+
if o is None:
|
|
1582
|
+
return None
|
|
1583
|
+
return _dc.replace(node, operand=o)
|
|
1584
|
+
if isinstance(node, FuncCall):
|
|
1585
|
+
if node.kwargs:
|
|
1586
|
+
return None
|
|
1587
|
+
new_args = []
|
|
1588
|
+
for a in node.args:
|
|
1589
|
+
sa = self._subst_params(a, subst)
|
|
1590
|
+
if sa is None:
|
|
1591
|
+
return None
|
|
1592
|
+
new_args.append(sa)
|
|
1593
|
+
return _dc.replace(node, args=new_args)
|
|
1594
|
+
# Subscript (history read) and anything else: not a stable-length shape.
|
|
1595
|
+
return None
|
|
1596
|
+
|
|
1597
|
+
def _expr_is_stable(self, node, _udf_stack: frozenset = frozenset(),
|
|
1598
|
+
_depth: int = 0) -> bool:
|
|
1599
|
+
"""True iff ``node``'s value is a bar-invariant scalar.
|
|
1600
|
+
|
|
1601
|
+
A stable expression depends only on: literals, ``input.*`` values,
|
|
1602
|
+
previously-tracked stable runtime vars, known compile-time consts,
|
|
1603
|
+
``timeframe.*`` members (constant per run), ``syminfo.*`` (constant
|
|
1604
|
+
per instrument), and ``math.*`` functions/consts over stable
|
|
1605
|
+
sub-expressions, combined with arithmetic / comparison / logical
|
|
1606
|
+
ops, ternaries, and the ``int/float/bool/string`` casts.
|
|
1607
|
+
|
|
1608
|
+
Returns False (i.e. "series") for any node that references a per-bar
|
|
1609
|
+
value: bar fields (close/open/...), series vars, history subscripts,
|
|
1610
|
+
``ta.*`` results, strategy.* state, or any unrecognised construct.
|
|
1611
|
+
The conservative False keeps the TA-ctor guard intact for genuinely
|
|
1612
|
+
dynamic lengths.
|
|
1613
|
+
"""
|
|
1614
|
+
if node is None:
|
|
1615
|
+
return False
|
|
1616
|
+
if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral)):
|
|
1617
|
+
return True
|
|
1618
|
+
if isinstance(node, NaLiteral):
|
|
1619
|
+
return True
|
|
1620
|
+
if isinstance(node, Identifier):
|
|
1621
|
+
name = node.name
|
|
1622
|
+
if name in self._known_vars:
|
|
1623
|
+
return True
|
|
1624
|
+
if name in self._stable_runtime_vars:
|
|
1625
|
+
return True
|
|
1626
|
+
if name in self._input_backed_vars:
|
|
1627
|
+
return True
|
|
1628
|
+
if name in self.ctx.series_vars:
|
|
1629
|
+
return False
|
|
1630
|
+
if name in self._var_names:
|
|
1631
|
+
# var/varip persistent state — mutable across bars.
|
|
1632
|
+
return False
|
|
1633
|
+
if name in BAR_FIELDS or name in BAR_BUILTINS:
|
|
1634
|
+
return False
|
|
1635
|
+
# Unrecognised bare identifier: be conservative so we never
|
|
1636
|
+
# silently allow an undeclared / dynamic length through.
|
|
1637
|
+
return False
|
|
1638
|
+
if isinstance(node, MemberAccess):
|
|
1639
|
+
if isinstance(node.object, Identifier):
|
|
1640
|
+
ns = node.object.name
|
|
1641
|
+
if ns == "timeframe":
|
|
1642
|
+
return node.member in self._TF_STABLE_MEMBERS
|
|
1643
|
+
if ns == "math":
|
|
1644
|
+
return node.member in self._MATH_STABLE_MEMBERS
|
|
1645
|
+
if ns == "syminfo":
|
|
1646
|
+
# syminfo.* (mintick, pointvalue, tickerid, ...) is
|
|
1647
|
+
# constant for the run — safe as a stable scalar.
|
|
1648
|
+
return True
|
|
1649
|
+
# bar.* / request.* / any other member access reads per-bar or
|
|
1650
|
+
# dynamic state.
|
|
1651
|
+
return False
|
|
1652
|
+
if isinstance(node, Subscript):
|
|
1653
|
+
# History read (``close[1]``) or indexed access — per-bar.
|
|
1654
|
+
return False
|
|
1655
|
+
if isinstance(node, Ternary):
|
|
1656
|
+
return (self._expr_is_stable(node.condition, _udf_stack, _depth)
|
|
1657
|
+
and self._expr_is_stable(node.true_val, _udf_stack, _depth)
|
|
1658
|
+
and self._expr_is_stable(node.false_val, _udf_stack, _depth))
|
|
1659
|
+
if isinstance(node, BinOp):
|
|
1660
|
+
return (self._expr_is_stable(node.left, _udf_stack, _depth)
|
|
1661
|
+
and self._expr_is_stable(node.right, _udf_stack, _depth))
|
|
1662
|
+
if isinstance(node, UnaryOp):
|
|
1663
|
+
return self._expr_is_stable(node.operand, _udf_stack, _depth)
|
|
1664
|
+
if isinstance(node, FuncCall):
|
|
1665
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
1666
|
+
if namespace == "ta":
|
|
1667
|
+
return False
|
|
1668
|
+
if namespace == "math":
|
|
1669
|
+
if func_name not in self._MATH_STABLE_MEMBERS:
|
|
1670
|
+
return False
|
|
1671
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
1672
|
+
for a in node.args)
|
|
1673
|
+
if namespace == "timeframe":
|
|
1674
|
+
# ``timeframe.in_seconds()`` (and any other function-form
|
|
1675
|
+
# timeframe member) is a stable per-run scalar — it reflects
|
|
1676
|
+
# the script's resolution, not a per-bar value.
|
|
1677
|
+
if func_name not in self._TF_STABLE_MEMBERS:
|
|
1678
|
+
return False
|
|
1679
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
1680
|
+
for a in node.args)
|
|
1681
|
+
if namespace == "input":
|
|
1682
|
+
return True
|
|
1683
|
+
if namespace is None and func_name in ("int", "float", "bool", "string"):
|
|
1684
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
1685
|
+
for a in node.args)
|
|
1686
|
+
# A user-defined single-expression function is stable iff every
|
|
1687
|
+
# argument is stable AND its body (with the params bound to those
|
|
1688
|
+
# args) is stable — i.e. the body references only stable scalars
|
|
1689
|
+
# (inputs / consts / timeframe.* / math.* / never-reassigned var
|
|
1690
|
+
# scalars) and no series / strategy state / ta.* results. Inlining
|
|
1691
|
+
# the body (params substituted by the arg nodes) lets the ordinary
|
|
1692
|
+
# classifier decide; the name-stack + depth guard refuses recursion
|
|
1693
|
+
# so a cyclic / malformed UDF is rejected, not looped forever.
|
|
1694
|
+
if namespace is None and func_name is not None:
|
|
1695
|
+
inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
|
|
1696
|
+
if inlined is not None:
|
|
1697
|
+
if not all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
1698
|
+
for a in node.args):
|
|
1699
|
+
return False
|
|
1700
|
+
return self._expr_is_stable(
|
|
1701
|
+
inlined, _udf_stack | {func_name}, _depth + 1)
|
|
1702
|
+
# Any other call (multi-statement user functions, str.*, array.*,
|
|
1703
|
+
# ...) — series by default; the conservative answer keeps the guard
|
|
1704
|
+
# honest.
|
|
1705
|
+
return False
|
|
1706
|
+
return False
|
|
1707
|
+
|
|
1708
|
+
# AST node kinds whose serialized form is self-delimiting (a literal, a
|
|
1709
|
+
# name, a member read, or a ``name(...)`` call). Non-atomic kinds (BinOp /
|
|
1710
|
+
# UnaryOp / Ternary) MUST be parenthesized when they appear as an operand,
|
|
1711
|
+
# otherwise re-parsing the flattened infix string silently reassociates the
|
|
1712
|
+
# tree: Pine grouping ``(a - b) / (c - d)`` degrades to ``a - b / c - d``
|
|
1713
|
+
# under C++ precedence. See ``_runtime_ctor_arg_for_reset`` (the string is
|
|
1714
|
+
# re-parsed and lowered through the expression visitor).
|
|
1715
|
+
_ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
|
|
1716
|
+
|
|
1717
|
+
def _arith_operand_to_str(self, node, _udf_stack: frozenset = frozenset(),
|
|
1718
|
+
_depth: int = 0) -> str | None:
|
|
1719
|
+
"""Serialize ``node`` for use as an operand: parenthesize it unless its
|
|
1720
|
+
serialized form is already self-delimiting, so grouping survives a
|
|
1721
|
+
round-trip through the parser."""
|
|
1722
|
+
s = self._arith_expr_to_str(node, _udf_stack, _depth)
|
|
1723
|
+
if s is None:
|
|
1724
|
+
return None
|
|
1725
|
+
if isinstance(node, self._ATOMIC_ARITH_NODES):
|
|
1726
|
+
return s
|
|
1727
|
+
return f"({s})"
|
|
1728
|
+
|
|
1729
|
+
def _arith_expr_to_str(self, node, _udf_stack: frozenset = frozenset(),
|
|
1730
|
+
_depth: int = 0) -> str | None:
|
|
1042
1731
|
"""Render a numeric arithmetic-over-identifiers expression to a string
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
(series subscripts,
|
|
1732
|
+
that re-parses to the SAME tree (grouping preserved via
|
|
1733
|
+
``_arith_operand_to_str``). Returns None for any node shape we don't
|
|
1734
|
+
fold (series subscripts, etc.) so the caller leaves the var untracked.
|
|
1735
|
+
|
|
1736
|
+
``_udf_stack``/``_depth`` guard the single-expression-UDF inlining below
|
|
1737
|
+
against recursion cycles (Pine forbids recursion, but a malformed source
|
|
1738
|
+
must be refused, not looped forever).
|
|
1046
1739
|
"""
|
|
1047
1740
|
if isinstance(node, NumberLiteral):
|
|
1048
1741
|
v = node.value
|
|
@@ -1054,23 +1747,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1054
1747
|
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
1055
1748
|
return f"{node.object.name}.{node.member}"
|
|
1056
1749
|
if isinstance(node, BinOp):
|
|
1057
|
-
l = self.
|
|
1058
|
-
r = self.
|
|
1750
|
+
l = self._arith_operand_to_str(node.left, _udf_stack, _depth)
|
|
1751
|
+
r = self._arith_operand_to_str(node.right, _udf_stack, _depth)
|
|
1059
1752
|
if l is None or r is None:
|
|
1060
1753
|
return None
|
|
1061
1754
|
return f"{l} {node.op} {r}"
|
|
1062
1755
|
if isinstance(node, UnaryOp):
|
|
1063
|
-
o = self.
|
|
1756
|
+
o = self._arith_operand_to_str(node.operand, _udf_stack, _depth)
|
|
1064
1757
|
if o is None:
|
|
1065
1758
|
return None
|
|
1066
1759
|
return f"{node.op}{o}"
|
|
1760
|
+
if isinstance(node, Ternary):
|
|
1761
|
+
c = self._arith_operand_to_str(node.condition, _udf_stack, _depth)
|
|
1762
|
+
t = self._arith_operand_to_str(node.true_val, _udf_stack, _depth)
|
|
1763
|
+
f = self._arith_operand_to_str(node.false_val, _udf_stack, _depth)
|
|
1764
|
+
if c is None or t is None or f is None:
|
|
1765
|
+
return None
|
|
1766
|
+
return f"{c} ? {t} : {f}"
|
|
1067
1767
|
if isinstance(node, FuncCall):
|
|
1068
|
-
|
|
1768
|
+
# A bare-name call to a single-expression user function has no C++
|
|
1769
|
+
# counterpart at class scope — inline its body (params substituted
|
|
1770
|
+
# by the arg expressions) so the rendered string is pure
|
|
1771
|
+
# math/timeframe/input arithmetic the ctor-reset path can expand.
|
|
1772
|
+
# Namespaced calls (math.*/timeframe.*/int(...)) fall through to the
|
|
1773
|
+
# ordinary ``callee(args)`` rendering below. The stack/depth guard
|
|
1774
|
+
# refuses a recursive UDF (returns None -> caller leaves it untracked
|
|
1775
|
+
# -> the ctor guard rejects it loudly) instead of recursing forever.
|
|
1776
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
1777
|
+
if ns is None and fn is not None and self._get_udf_def(fn) is not None:
|
|
1778
|
+
inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
|
|
1779
|
+
if inlined is None:
|
|
1780
|
+
return None
|
|
1781
|
+
return self._arith_expr_to_str(inlined, _udf_stack | {fn}, _depth + 1)
|
|
1782
|
+
callee = self._arith_expr_to_str(node.callee, _udf_stack, _depth)
|
|
1069
1783
|
if callee is None:
|
|
1070
1784
|
return None
|
|
1071
1785
|
parts = []
|
|
1072
1786
|
for a in node.args:
|
|
1073
|
-
s = self._arith_expr_to_str(a)
|
|
1787
|
+
s = self._arith_expr_to_str(a, _udf_stack, _depth)
|
|
1074
1788
|
if s is None:
|
|
1075
1789
|
return None
|
|
1076
1790
|
parts.append(s)
|
|
@@ -1129,36 +1843,56 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1129
1843
|
if stored:
|
|
1130
1844
|
self._input_backed_vars.add(node.name)
|
|
1131
1845
|
self._input_var_to_call[node.name] = node.value
|
|
1132
|
-
# Class-scope arithmetic over known
|
|
1133
|
-
#
|
|
1846
|
+
# Class-scope arithmetic / ternaries / casts over known, input-backed,
|
|
1847
|
+
# timeframe.*, or math.* operands
|
|
1848
|
+
# (``wilderLen = rsiLen * 2 - 1``, ``fastPeriod = isM5 ? ... : ...``,
|
|
1849
|
+
# ``filterLen = math.max(1, int(math.round(2 / a)))``).
|
|
1134
1850
|
# Without this branch the derived name is untracked, the TA ctor arg
|
|
1135
1851
|
# never folds, and the runtime-reset path silently degenerates to a
|
|
1136
|
-
# period of 1. We (a) fold to a literal for the ctor-init list
|
|
1137
|
-
# (b) record the raw expression so the reset path can
|
|
1138
|
-
# input-backed operand to its get_input_*() runtime read
|
|
1139
|
-
|
|
1852
|
+
# period of 1. We (a) fold to a literal for the ctor-init list when
|
|
1853
|
+
# possible and (b) record the raw expression so the reset path can
|
|
1854
|
+
# re-expand any input-backed operand to its get_input_*() runtime read
|
|
1855
|
+
# and render timeframe.* / math.* fragments to valid C++.
|
|
1856
|
+
#
|
|
1857
|
+
# The ``_expr_is_stable`` gate is what separates a faithful stable
|
|
1858
|
+
# scalar (inputs + constants + timeframe + math) from a series-derived
|
|
1859
|
+
# value: a length that depends on a ta.* result, a history subscript,
|
|
1860
|
+
# or a bar field stays untracked and is therefore rejected by the TA
|
|
1861
|
+
# ctor guard — preserving the guardrail for genuine dynamic lengths.
|
|
1862
|
+
elif isinstance(node.value, (BinOp, UnaryOp, FuncCall, Ternary)):
|
|
1140
1863
|
expr_str = self._arith_expr_to_str(node.value)
|
|
1141
|
-
if expr_str is not None:
|
|
1864
|
+
if expr_str is not None and self._expr_is_stable(node.value):
|
|
1142
1865
|
import re as _re
|
|
1143
1866
|
tokens = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
|
|
1144
|
-
refs_known = any(t in self._known_vars for t in tokens)
|
|
1145
1867
|
refs_input = any(t in self._input_backed_vars for t in tokens)
|
|
1146
1868
|
refs_derived = any(t in self._derived_input_expr for t in tokens)
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1869
|
+
# The stability classifier already proved this expression is a
|
|
1870
|
+
# bar-invariant scalar (inputs / constants / timeframe.* /
|
|
1871
|
+
# math.* / syminfo.* only). Track it unconditionally so later
|
|
1872
|
+
# stable exprs (and the TA reset path) can reference / expand
|
|
1873
|
+
# it — e.g. ``pi = math.asin(1) * 2`` feeds ``beta`` feeds
|
|
1874
|
+
# ``alpha`` feeds a function-local ``filterLen``.
|
|
1875
|
+
folded = self._resolve_known(expr_str)
|
|
1876
|
+
if self._is_compile_time_value(folded):
|
|
1877
|
+
try:
|
|
1878
|
+
num = float(folded)
|
|
1879
|
+
self._known_vars[node.name] = (
|
|
1880
|
+
int(num) if num == int(num) else num
|
|
1881
|
+
)
|
|
1882
|
+
except ValueError:
|
|
1883
|
+
pass
|
|
1884
|
+
# Record the raw expression so the runtime-reset path can
|
|
1885
|
+
# re-expand operands. Always record for stable derived exprs
|
|
1886
|
+
# (even pure-math / pure-timeframe ones with no input) so the
|
|
1887
|
+
# reset can render them.
|
|
1888
|
+
self._derived_input_expr[node.name] = expr_str
|
|
1889
|
+
self._stable_runtime_vars.add(node.name)
|
|
1890
|
+
# Mark input-backed so use-sites are not inlined and the
|
|
1891
|
+
# override-aware get_input_*() reads are emitted on the reset
|
|
1892
|
+
# path. Pure-math / pure-timeframe exprs (no input) stay out
|
|
1893
|
+
# of this set, which is fine — they have no override to honor.
|
|
1894
|
+
if refs_input or refs_derived:
|
|
1895
|
+
self._input_backed_vars.add(node.name)
|
|
1162
1896
|
|
|
1163
1897
|
# ------------------------------------------------------------------
|
|
1164
1898
|
# Public entry point
|
|
@@ -1215,17 +1949,172 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1215
1949
|
walk(v)
|
|
1216
1950
|
walk(self.ctx.ast)
|
|
1217
1951
|
|
|
1952
|
+
def _func_cpp_base_name(self, fname: str) -> str:
|
|
1953
|
+
"""Return the actual emitted C++ base name for a UDF or UDT method."""
|
|
1954
|
+
fi = self._func_info_map.get(fname)
|
|
1955
|
+
if fi is not None and getattr(fi, "is_udt_method", False):
|
|
1956
|
+
return self._emit_udt_method_cpp_name(fi)
|
|
1957
|
+
return self._func_safe_name(fname)
|
|
1958
|
+
|
|
1959
|
+
def _inline_history_contexts_for_owner(self, owner: str | None) -> list[str | None]:
|
|
1960
|
+
"""Return every method-emission context that owns one source AST site.
|
|
1961
|
+
|
|
1962
|
+
``None`` denotes top-level/on_bar or a function emitted exactly once.
|
|
1963
|
+
Stateful UDF clones and fresh nested-helper instances use the same names
|
|
1964
|
+
assigned to ``_current_instance_name`` by ``_emit_func_def`` so lookup
|
|
1965
|
+
while visiting a body is deterministic and cannot collapse call sites.
|
|
1966
|
+
"""
|
|
1967
|
+
if owner is None:
|
|
1968
|
+
return [None]
|
|
1969
|
+
if owner in self._dead_func_names:
|
|
1970
|
+
return []
|
|
1971
|
+
|
|
1972
|
+
total_cs = self.ctx.func_call_site_counts.get(owner, 0)
|
|
1973
|
+
cloned = (
|
|
1974
|
+
owner in self.ctx.func_ta_ranges
|
|
1975
|
+
or owner in self.ctx.func_series_vars
|
|
1976
|
+
or owner in self.ctx.func_var_members
|
|
1977
|
+
or owner in self.ctx.func_security_clone_only
|
|
1978
|
+
) and total_cs > 0
|
|
1979
|
+
if cloned:
|
|
1980
|
+
contexts: list[str | None] = [
|
|
1981
|
+
f"{self._func_cpp_base_name(owner)}_cs{idx}"
|
|
1982
|
+
for idx in range(total_cs)
|
|
1983
|
+
]
|
|
1984
|
+
else:
|
|
1985
|
+
contexts = [None]
|
|
1986
|
+
|
|
1987
|
+
for inst in self._fresh_instances:
|
|
1988
|
+
if inst["fname"] == owner and inst["name"] not in contexts:
|
|
1989
|
+
contexts.append(inst["name"])
|
|
1990
|
+
return contexts
|
|
1991
|
+
|
|
1992
|
+
def _prepare_inline_history_members(self) -> None:
|
|
1993
|
+
"""Pre-register every generated temporary-Series class member.
|
|
1994
|
+
|
|
1995
|
+
Member declarations and the declaration-derived rollback aggregate are
|
|
1996
|
+
emitted before function/on_bar bodies. A source-order AST pass therefore
|
|
1997
|
+
reserves stable names up front. The key includes an emitted UDF context
|
|
1998
|
+
because the same body AST is rendered once per stateful call-site clone.
|
|
1999
|
+
"""
|
|
2000
|
+
self._inline_history_members = []
|
|
2001
|
+
self._inline_history_member_by_key = {}
|
|
2002
|
+
counters = {"hist_call": 0, "series_arg": 0}
|
|
2003
|
+
|
|
2004
|
+
def walk_nodes(value):
|
|
2005
|
+
"""Yield AST nodes in stable field order, including tuple elements.
|
|
2006
|
+
|
|
2007
|
+
NamingHelper._walk_ast predates several AST containers and is
|
|
2008
|
+
intentionally a best-effort utility. Member pre-registration must
|
|
2009
|
+
be exhaustive because a missed node becomes an undeclared C++
|
|
2010
|
+
member, so use the dataclass field graph directly here.
|
|
2011
|
+
"""
|
|
2012
|
+
if isinstance(value, ASTNode):
|
|
2013
|
+
yield value
|
|
2014
|
+
for child in vars(value).values():
|
|
2015
|
+
yield from walk_nodes(child)
|
|
2016
|
+
return
|
|
2017
|
+
if isinstance(value, (list, tuple)):
|
|
2018
|
+
for child in value:
|
|
2019
|
+
yield from walk_nodes(child)
|
|
2020
|
+
return
|
|
2021
|
+
if isinstance(value, dict):
|
|
2022
|
+
for child in value.values():
|
|
2023
|
+
yield from walk_nodes(child)
|
|
2024
|
+
return
|
|
2025
|
+
if isinstance(value, TypeField) and value.default is not None:
|
|
2026
|
+
yield from walk_nodes(value.default)
|
|
2027
|
+
|
|
2028
|
+
owner_by_node: dict[int, str] = {}
|
|
2029
|
+
for fi in self.ctx.func_infos:
|
|
2030
|
+
if fi.node is None:
|
|
2031
|
+
continue
|
|
2032
|
+
for child in walk_nodes(fi.node):
|
|
2033
|
+
owner_by_node[id(child)] = fi.name
|
|
2034
|
+
|
|
2035
|
+
def register(kind: str, source_key: tuple, cpp_type: str,
|
|
2036
|
+
owner: str | None) -> None:
|
|
2037
|
+
if cpp_type not in ("double", "int", "bool"):
|
|
2038
|
+
cpp_type = "double"
|
|
2039
|
+
for context in self._inline_history_contexts_for_owner(owner):
|
|
2040
|
+
key = (kind, *source_key, context)
|
|
2041
|
+
if key in self._inline_history_member_by_key:
|
|
2042
|
+
continue
|
|
2043
|
+
counters[kind] += 1
|
|
2044
|
+
member_name = f"_{kind}_{counters[kind]}"
|
|
2045
|
+
self._inline_history_member_by_key[key] = member_name
|
|
2046
|
+
self._inline_history_members.append({
|
|
2047
|
+
"kind": kind,
|
|
2048
|
+
"member_name": member_name,
|
|
2049
|
+
"cpp_type": cpp_type,
|
|
2050
|
+
"context": context,
|
|
2051
|
+
})
|
|
2052
|
+
|
|
2053
|
+
def actual_args_for(call: FuncCall, params: list[str]) -> list:
|
|
2054
|
+
if call.kwargs:
|
|
2055
|
+
return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
|
|
2056
|
+
return list(call.args)
|
|
2057
|
+
|
|
2058
|
+
for node in walk_nodes(self.ctx.ast):
|
|
2059
|
+
owner = owner_by_node.get(id(node))
|
|
2060
|
+
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
2061
|
+
register(
|
|
2062
|
+
"hist_call", (id(node),), self._infer_type(node.object), owner
|
|
2063
|
+
)
|
|
2064
|
+
|
|
2065
|
+
if not isinstance(node, FuncCall):
|
|
2066
|
+
continue
|
|
2067
|
+
func_name, _ = self._resolve_callee(node.callee)
|
|
2068
|
+
fi = self._func_info_map.get(func_name)
|
|
2069
|
+
if fi is None or fi.node is None:
|
|
2070
|
+
continue
|
|
2071
|
+
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
2072
|
+
series_param_indices = {
|
|
2073
|
+
idx for idx, name in enumerate(fi.node.params) if name in func_sv
|
|
2074
|
+
}
|
|
2075
|
+
if not series_param_indices:
|
|
2076
|
+
continue
|
|
2077
|
+
args = actual_args_for(node, list(fi.node.params))
|
|
2078
|
+
for idx, arg in enumerate(args):
|
|
2079
|
+
if idx not in series_param_indices:
|
|
2080
|
+
continue
|
|
2081
|
+
if isinstance(arg, Identifier):
|
|
2082
|
+
if arg.name in BAR_FIELDS or arg.name in BAR_SERIES_PUSH:
|
|
2083
|
+
continue
|
|
2084
|
+
if arg.name in self.ctx.series_vars:
|
|
2085
|
+
continue
|
|
2086
|
+
register(
|
|
2087
|
+
"series_arg", (id(node), idx), self._infer_type(arg), owner
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
def _inline_history_member(self, kind: str, node: ASTNode,
|
|
2091
|
+
arg_idx: int | None = None) -> str:
|
|
2092
|
+
source_key = (id(node),) if arg_idx is None else (id(node), arg_idx)
|
|
2093
|
+
key = (kind, *source_key, self._current_instance_name)
|
|
2094
|
+
member = self._inline_history_member_by_key.get(key)
|
|
2095
|
+
if member is None:
|
|
2096
|
+
raise AssertionError(
|
|
2097
|
+
"missing pre-registered inline history member for "
|
|
2098
|
+
f"{kind} at {getattr(node, 'loc', None)} in context "
|
|
2099
|
+
f"{self._current_instance_name!r}"
|
|
2100
|
+
)
|
|
2101
|
+
return member
|
|
2102
|
+
|
|
1218
2103
|
def generate(self) -> str:
|
|
1219
2104
|
"""Generate C++ source from the AnalyzerContext."""
|
|
1220
2105
|
# Context-sensitive instance pre-pass (needs the naming helpers populated
|
|
1221
2106
|
# in __init__). Computes nested stateful-helper dispatch + fresh instances.
|
|
1222
2107
|
self._build_func_instances()
|
|
2108
|
+
self._prepare_inline_history_members()
|
|
1223
2109
|
# Pre-scan for strategy series vars
|
|
1224
2110
|
self._prescan_strategy_series()
|
|
1225
2111
|
self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
|
|
1226
2112
|
# request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
|
|
1227
2113
|
# Maps sec_id -> set of TA call-site indices needing an HTF history Series.
|
|
1228
2114
|
self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
|
|
2115
|
+
# request.security helper-call results read at a history offset
|
|
2116
|
+
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
2117
|
+
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
1229
2118
|
|
|
1230
2119
|
lines: list[str] = []
|
|
1231
2120
|
|
|
@@ -1290,6 +2179,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1290
2179
|
# 2. Open class
|
|
1291
2180
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
1292
2181
|
lines.append("public:")
|
|
2182
|
+
_script_state_decl_start = len(lines)
|
|
1293
2183
|
|
|
1294
2184
|
# request.security state
|
|
1295
2185
|
for item in self._security_calls:
|
|
@@ -1318,19 +2208,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1318
2208
|
for field in sorted(
|
|
1319
2209
|
self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1320
2210
|
):
|
|
2211
|
+
ctype = self._security_bar_hist_type(field)
|
|
1321
2212
|
lines.append(
|
|
1322
|
-
f" Series<
|
|
2213
|
+
f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
1323
2214
|
)
|
|
1324
2215
|
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
1325
2216
|
self._collect_security_ta_hist_indices(expr_node)
|
|
1326
2217
|
)
|
|
1327
2218
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1328
2219
|
lines.append(f" Series<double> {name}{_mbb};")
|
|
2220
|
+
self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
|
|
1329
2221
|
continue
|
|
1330
2222
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
1331
2223
|
hist_fields: set[str] = set()
|
|
1332
2224
|
for el in expr_node.elements:
|
|
1333
2225
|
hist_fields |= self._collect_security_ohlc_hist_fields(el)
|
|
2226
|
+
for name in item.get("mutable_globals", []) or []:
|
|
2227
|
+
info = self._global_mutable_infos.get(name)
|
|
2228
|
+
if info is not None:
|
|
2229
|
+
for stmt in getattr(info, "source_stmts", []) or []:
|
|
2230
|
+
hist_fields |= self._collect_security_ohlc_hist_fields(stmt)
|
|
1334
2231
|
self._security_ohlc_hist_fields_by_sec[sec_id] = hist_fields
|
|
1335
2232
|
for i, el in enumerate(expr_node.elements):
|
|
1336
2233
|
ctype = self._infer_cpp_type_for_security_elem(el)
|
|
@@ -1338,20 +2235,31 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1338
2235
|
lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
|
|
1339
2236
|
else:
|
|
1340
2237
|
lines.append(f" {ctype} _req_sec_{sec_id}_{i} = na<double>();")
|
|
2238
|
+
elif returns_tuple and tuple_size and tuple_size > 0:
|
|
2239
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
2240
|
+
self._collect_security_ohlc_hist_fields_for_call(item)
|
|
2241
|
+
)
|
|
2242
|
+
site = self._get_ta_site(expr_node)
|
|
2243
|
+
ta_name = self._ta_name_from_site(site) if site is not None else ""
|
|
2244
|
+
ctype = TA_TUPLE_RESULT_TYPES.get(ta_name, "std::tuple<double, double>")
|
|
2245
|
+
default = self._security_tuple_result_default(ctype, tuple_size)
|
|
2246
|
+
lines.append(f" {ctype} _req_sec_{sec_id} = {default};")
|
|
1341
2247
|
else:
|
|
1342
|
-
self._security_ohlc_hist_fields_by_sec[sec_id] =
|
|
1343
|
-
|
|
2248
|
+
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
2249
|
+
self._collect_security_ohlc_hist_fields_for_call(item)
|
|
1344
2250
|
)
|
|
1345
2251
|
lines.append(f" double _req_sec_{sec_id} = na<double>();")
|
|
1346
2252
|
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
2253
|
+
ctype = self._security_bar_hist_type(field)
|
|
1347
2254
|
lines.append(
|
|
1348
|
-
f" Series<
|
|
2255
|
+
f" Series<{ctype}> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
1349
2256
|
)
|
|
1350
2257
|
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
1351
2258
|
self._collect_security_ta_hist_indices(expr_node)
|
|
1352
2259
|
)
|
|
1353
2260
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1354
2261
|
lines.append(f" Series<double> {name}{_mbb};")
|
|
2262
|
+
self._emit_security_expr_hist_members(sec_id, expr_node, lines, _mbb)
|
|
1355
2263
|
|
|
1356
2264
|
if self._security_calls:
|
|
1357
2265
|
lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
|
|
@@ -1375,9 +2283,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1375
2283
|
)
|
|
1376
2284
|
|
|
1377
2285
|
# 3. TA members
|
|
1378
|
-
for site in self.ctx.ta_call_sites:
|
|
2286
|
+
for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
|
|
2287
|
+
if _ta_idx in self._dead_ta_indices:
|
|
2288
|
+
continue
|
|
1379
2289
|
lines.append(f" {site.class_name} {site.member_name};")
|
|
1380
|
-
if
|
|
2290
|
+
if self._ta_site_uses_precalc(site):
|
|
1381
2291
|
vtype = self._ta_return_type(site)
|
|
1382
2292
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
1383
2293
|
lines.append(" bool _use_precalc = false;")
|
|
@@ -1485,7 +2395,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1485
2395
|
lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
|
|
1486
2396
|
|
|
1487
2397
|
# 7. Fixnan members
|
|
1488
|
-
for site in self.ctx.fixnan_sites:
|
|
2398
|
+
for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
|
|
2399
|
+
if _fi_idx in self._dead_fixnan_indices:
|
|
2400
|
+
continue
|
|
1489
2401
|
cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
|
|
1490
2402
|
lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
|
|
1491
2403
|
|
|
@@ -1499,12 +2411,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1499
2411
|
else:
|
|
1500
2412
|
lines.append(f" Series<double> {svar}{_mbb};")
|
|
1501
2413
|
|
|
2414
|
+
# 8a. Synthetic temporary history. Unlike the legacy function-local
|
|
2415
|
+
# static buffers, these members are value-copyable rollback state and
|
|
2416
|
+
# have one identity per source site / emitted UDF variant.
|
|
2417
|
+
for info in self._inline_history_members:
|
|
2418
|
+
lines.append(
|
|
2419
|
+
f" Series<{info['cpp_type']}> {info['member_name']}{_mbb};"
|
|
2420
|
+
)
|
|
2421
|
+
|
|
1502
2422
|
# 8b. Global-scope non-var declarations as class members
|
|
1503
2423
|
# (so user-defined functions can reference them)
|
|
1504
2424
|
seen_global = set()
|
|
1505
2425
|
for name, ptype in self.ctx.global_var_decls:
|
|
1506
2426
|
if name in seen_global or name in self.ctx.series_vars or name in self._var_names:
|
|
1507
2427
|
continue
|
|
2428
|
+
# De-hoisted UDT array-element alias (Pine reference semantics): the
|
|
2429
|
+
# in-loop VarDecl is emitted as a fresh ``UDT& z = arr[i];`` local
|
|
2430
|
+
# reference each iteration, so there is no persistent class member.
|
|
2431
|
+
if name in self._udt_array_get_ref_locals:
|
|
2432
|
+
seen_global.add(name)
|
|
2433
|
+
continue
|
|
1508
2434
|
seen_global.add(name)
|
|
1509
2435
|
safe = self._safe_name(name)
|
|
1510
2436
|
|
|
@@ -1558,6 +2484,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1558
2484
|
emitted_clones.add(fresh_safe)
|
|
1559
2485
|
self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
|
|
1560
2486
|
|
|
2487
|
+
# 8c3. Fresh fixnan members for context-sensitive helper instances.
|
|
2488
|
+
# Each fresh instance gets its OWN previous-value member so two
|
|
2489
|
+
# call paths never share fixnan state (mirrors 8c2 for vars).
|
|
2490
|
+
for orig_site, fresh_safe in self._fresh_fixnan_members:
|
|
2491
|
+
if fresh_safe in emitted_clones:
|
|
2492
|
+
continue
|
|
2493
|
+
emitted_clones.add(fresh_safe)
|
|
2494
|
+
cpp_type = PINE_TYPE_TO_CPP.get(orig_site.pine_type, "double")
|
|
2495
|
+
lines.append(f" {cpp_type} {fresh_safe} = na<{cpp_type}>();")
|
|
2496
|
+
|
|
1561
2497
|
# 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
|
|
1562
2498
|
# non-drawing strategies emit byte-identical C++). Each arena is a
|
|
1563
2499
|
# per-strategy member -> reset-per-run is automatic. Caps come from
|
|
@@ -1605,6 +2541,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1605
2541
|
|
|
1606
2542
|
lines.append("")
|
|
1607
2543
|
|
|
2544
|
+
# 9d. Historical execution rollback checkpoint. Derive the member
|
|
2545
|
+
# inventory from the declarations above so every future generated
|
|
2546
|
+
# state category is captured automatically (or generation fails loudly
|
|
2547
|
+
# if it introduces an unfamiliar declaration form).
|
|
2548
|
+
_script_state_members = self._collect_script_state_members(
|
|
2549
|
+
lines[_script_state_decl_start:-1]
|
|
2550
|
+
)
|
|
2551
|
+
self._emit_script_state_hooks(lines, _script_state_members)
|
|
2552
|
+
lines.append("")
|
|
2553
|
+
|
|
1608
2554
|
# 9. Constructor with TA initializer list
|
|
1609
2555
|
self._emit_constructor(lines)
|
|
1610
2556
|
lines.append("")
|
|
@@ -1612,6 +2558,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1612
2558
|
# 10. User-defined functions (with per-call-site variants for functions
|
|
1613
2559
|
# containing TA calls OR series variables that need isolation)
|
|
1614
2560
|
for fi in self.ctx.func_infos:
|
|
2561
|
+
# Dead-code user functions (defined but never called, with TA
|
|
2562
|
+
# state whose ctor args can't be sized) are skipped entirely —
|
|
2563
|
+
# their bodies reference TA members we no longer emit, and the
|
|
2564
|
+
# functions never run anyway.
|
|
2565
|
+
if fi.name in self._dead_func_names:
|
|
2566
|
+
continue
|
|
1615
2567
|
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
1616
2568
|
has_ta = fi.name in self.ctx.func_ta_ranges
|
|
1617
2569
|
has_series = fi.name in self.ctx.func_series_vars or fi.name in self.ctx.func_var_members
|
|
@@ -1762,7 +2714,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1762
2714
|
if self._is_chart_point_callee(node.callee):
|
|
1763
2715
|
return False
|
|
1764
2716
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
1765
|
-
if func_name in SKIP_FUNC_NAMES:
|
|
2717
|
+
if namespace is None and func_name in SKIP_FUNC_NAMES:
|
|
1766
2718
|
return True
|
|
1767
2719
|
if namespace in SKIP_NAMESPACES:
|
|
1768
2720
|
return True
|
|
@@ -1810,77 +2762,335 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1810
2762
|
# _infer_type / _infer_tuple_types live on TypeInferer — see codegen/types.py.
|
|
1811
2763
|
# _is_compile_time_value lives on TaSiteHelper — see codegen/ta.py.
|
|
1812
2764
|
|
|
2765
|
+
# Pine ``timeframe.<member>`` -> C++ runtime expression. Mirrors the
|
|
2766
|
+
# mapping in ``visit_expr._visit_member_access`` so a stable timeframe
|
|
2767
|
+
# fragment embedded in a TA ctor reset renders to the same C++ the
|
|
2768
|
+
# expression visitor would emit for a direct ``timeframe.*`` read.
|
|
2769
|
+
_TIMEFRAME_MEMBER_CPP: dict[str, str] = {
|
|
2770
|
+
"period": "script_tf_",
|
|
2771
|
+
"main_period": "main_period()",
|
|
2772
|
+
"multiplier": "tf_multiplier(script_tf_)",
|
|
2773
|
+
"isintraday": "tf_is_intraday(script_tf_)",
|
|
2774
|
+
"isminutes": "(tf_is_intraday(script_tf_) && !tf_is_seconds(script_tf_))",
|
|
2775
|
+
"isdaily": "tf_is_daily(script_tf_)",
|
|
2776
|
+
"isweekly": "tf_is_weekly(script_tf_)",
|
|
2777
|
+
"ismonthly": "tf_is_monthly(script_tf_)",
|
|
2778
|
+
"isdwm": "(tf_is_daily(script_tf_) || tf_is_weekly(script_tf_) || tf_is_monthly(script_tf_))",
|
|
2779
|
+
"isseconds": "tf_is_seconds(script_tf_)",
|
|
2780
|
+
"in_seconds": "tf_to_seconds(script_tf_)",
|
|
2781
|
+
"isticks": "false",
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
# Pine ``math.<member>`` -> C++ form. Function members map to ``std::*``;
|
|
2785
|
+
# constants map to their engine-side macro / literal.
|
|
2786
|
+
_MATH_MEMBER_CPP: dict[str, str] = {
|
|
2787
|
+
"pi": "M_PI", "e": "M_E", "phi": "1.618033988749895",
|
|
2788
|
+
"rphi": "0.6180339887498949",
|
|
2789
|
+
"abs": "std::abs", "max": "std::max", "min": "std::min",
|
|
2790
|
+
"round": "std::round", "floor": "std::floor", "ceil": "std::ceil",
|
|
2791
|
+
"sqrt": "std::sqrt", "log": "std::log", "log10": "std::log10",
|
|
2792
|
+
"exp": "std::exp", "pow": "std::pow",
|
|
2793
|
+
"sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
|
|
2794
|
+
"asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
|
|
2795
|
+
"sign": "(double)([] (double _v) { return (_v>0) - (_v<0); })",
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
# Pine logical operators (word form) -> C++ operator, used when rendering
|
|
2799
|
+
# a stable runtime expression. Matched with word boundaries.
|
|
2800
|
+
_PINE_LOGICAL_OPS: dict[str, str] = {"and": "&&", "or": "||", "not": "!"}
|
|
2801
|
+
|
|
2802
|
+
def _render_inline_input_calls(self, expr_str: str) -> tuple[str, bool]:
|
|
2803
|
+
"""Render inline ``input(...)`` / ``input.<type>(...)`` calls in a TA
|
|
2804
|
+
ctor-arg expression string to override-aware ``get_input_*()`` reads.
|
|
2805
|
+
|
|
2806
|
+
A bare input expression passed straight as a length argument
|
|
2807
|
+
(``adx(input(15), input(15))``) reaches the reset path as the raw call
|
|
2808
|
+
spelling ``input(15)`` because the analyzer's param-substitution has no
|
|
2809
|
+
intermediate variable to record in ``_input_backed_vars``. This helper
|
|
2810
|
+
finds each such call (balanced parens, ``input`` optionally followed by
|
|
2811
|
+
``.<type>``), re-parses it into a FuncCall, and renders it via the same
|
|
2812
|
+
``_render_input_value`` used for ordinary input var reads.
|
|
2813
|
+
|
|
2814
|
+
Returns ``(rewritten_str, found_any)``. When no inline input call is
|
|
2815
|
+
present, the string is returned unchanged with ``found_any=False``.
|
|
2816
|
+
"""
|
|
2817
|
+
import re
|
|
2818
|
+
# Locate ``input`` (as a word, not a substring of get_input_int etc.)
|
|
2819
|
+
# optionally followed by ``.<member>``, then a ``(`` opening a balanced
|
|
2820
|
+
# argument list.
|
|
2821
|
+
out = expr_str
|
|
2822
|
+
found = False
|
|
2823
|
+
idx = 0
|
|
2824
|
+
while idx < len(out):
|
|
2825
|
+
m = re.search(r"\binput\b", out[idx:])
|
|
2826
|
+
if m is None:
|
|
2827
|
+
break
|
|
2828
|
+
start = idx + m.start()
|
|
2829
|
+
# Reject a match that is part of a longer identifier
|
|
2830
|
+
# (e.g. ``get_input_int``) — the \b guard above already handles
|
|
2831
|
+
# alphanumerics, but be defensive.
|
|
2832
|
+
if start > 0 and (out[start - 1].isalnum() or out[start - 1] == "_"):
|
|
2833
|
+
idx = start + len("input")
|
|
2834
|
+
continue
|
|
2835
|
+
j = start + len("input")
|
|
2836
|
+
# Optional ``.<member>`` for the typed form ``input.int(...)``.
|
|
2837
|
+
member = None
|
|
2838
|
+
if j < len(out) and out[j] == ".":
|
|
2839
|
+
k = j + 1
|
|
2840
|
+
nm_start = k
|
|
2841
|
+
while k < len(out) and (out[k].isalnum() or out[k] == "_"):
|
|
2842
|
+
k += 1
|
|
2843
|
+
if k > nm_start:
|
|
2844
|
+
member = out[nm_start:k]
|
|
2845
|
+
j = k
|
|
2846
|
+
# Must be followed by ``(`` to be a call.
|
|
2847
|
+
if j >= len(out) or out[j] != "(":
|
|
2848
|
+
idx = j
|
|
2849
|
+
continue
|
|
2850
|
+
# Walk the balanced parens to extract the call substring.
|
|
2851
|
+
depth = 0
|
|
2852
|
+
k = j
|
|
2853
|
+
while k < len(out):
|
|
2854
|
+
ch = out[k]
|
|
2855
|
+
if ch == "(":
|
|
2856
|
+
depth += 1
|
|
2857
|
+
elif ch == ")":
|
|
2858
|
+
depth -= 1
|
|
2859
|
+
if depth == 0:
|
|
2860
|
+
k += 1
|
|
2861
|
+
break
|
|
2862
|
+
k += 1
|
|
2863
|
+
if depth != 0:
|
|
2864
|
+
# Unbalanced — bail on this match.
|
|
2865
|
+
idx = j + 1
|
|
2866
|
+
continue
|
|
2867
|
+
call_src = out[start:k]
|
|
2868
|
+
try:
|
|
2869
|
+
from ..lexer import Lexer
|
|
2870
|
+
from ..parser import Parser
|
|
2871
|
+
tokens = Lexer(call_src).tokenize()
|
|
2872
|
+
node = Parser(tokens, source=call_src)._parse_expression()
|
|
2873
|
+
if not self._is_input_call(node):
|
|
2874
|
+
idx = k
|
|
2875
|
+
continue
|
|
2876
|
+
func_name_i, namespace_i = self._resolve_callee(node.callee)
|
|
2877
|
+
# Inline inputs have no enclosing var; reuse the default
|
|
2878
|
+
# value as a synthetic title key so distinct defaults get
|
|
2879
|
+
# distinct input controls (and identical defaults collapse,
|
|
2880
|
+
# which is correct since they resolve to the same value).
|
|
2881
|
+
default_node = self._get_input_default(node)
|
|
2882
|
+
synth_title = self._visit_expr(default_node) if default_node is not None else ""
|
|
2883
|
+
title = self._get_input_title(node, var_name=None)
|
|
2884
|
+
if not title:
|
|
2885
|
+
title = synth_title
|
|
2886
|
+
rendered = self._render_input_value(node, func_name_i, namespace_i, title)
|
|
2887
|
+
except Exception:
|
|
2888
|
+
idx = k
|
|
2889
|
+
continue
|
|
2890
|
+
out = out[:start] + rendered + out[k:]
|
|
2891
|
+
found = True
|
|
2892
|
+
idx = start + len(rendered)
|
|
2893
|
+
return out, found
|
|
2894
|
+
|
|
1813
2895
|
def _runtime_ctor_arg_for_reset(self, arg_str: str) -> str | None:
|
|
1814
|
-
"""Convert a TA ctor-arg string into its runtime C++ expression
|
|
1815
|
-
|
|
1816
|
-
runtime expression (e.g.
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2896
|
+
"""Convert a TA ctor-arg string into its runtime C++ expression.
|
|
2897
|
+
|
|
2898
|
+
Returns the runtime expression (e.g.
|
|
2899
|
+
``get_input_int("MACD Fast", 12)`` or a ternary / math expression
|
|
2900
|
+
over such reads and ``timeframe.*`` members) when the ctor arg
|
|
2901
|
+
depends on a stable runtime scalar — an input-backed variable, a
|
|
2902
|
+
``timeframe.*`` member, or arithmetic / ternaries / casts over
|
|
2903
|
+
those. Returns None for pure literals or expressions that contain
|
|
2904
|
+
any unrecognised (potentially series) identifier, so the caller
|
|
2905
|
+
(the TA ctor guard) rejects them loudly instead of silently
|
|
2906
|
+
emitting period 1.
|
|
1820
2907
|
"""
|
|
1821
2908
|
import re
|
|
1822
2909
|
ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
|
|
1823
2910
|
|
|
1824
|
-
# Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)
|
|
1825
|
-
# to their raw RHS so
|
|
1826
|
-
# below. Recursive (bounded) to
|
|
1827
|
-
# against cycles.
|
|
2911
|
+
# Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)``,
|
|
2912
|
+
# ``fastPeriod`` -> ``(isM5 ? ... : ...)``) to their raw RHS so input
|
|
2913
|
+
# leaves become get_input_*() reads below. Recursive (bounded) to
|
|
2914
|
+
# handle chains of derived vars; guards against cycles.
|
|
1828
2915
|
def _expand_derived(s: str, seen: frozenset = frozenset(), depth: int = 0) -> str:
|
|
1829
2916
|
if depth > 32:
|
|
1830
2917
|
return s
|
|
1831
|
-
def _rep(
|
|
1832
|
-
nm =
|
|
2918
|
+
def _rep(p: re.Match) -> str:
|
|
2919
|
+
nm = p.group(0)
|
|
1833
2920
|
if nm in self._derived_input_expr and nm not in seen:
|
|
1834
2921
|
inner = self._derived_input_expr[nm]
|
|
1835
2922
|
return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
|
|
1836
2923
|
return nm
|
|
1837
2924
|
return ident_re.sub(_rep, s)
|
|
1838
2925
|
|
|
1839
|
-
|
|
2926
|
+
expanded = _expand_derived(arg_str)
|
|
2927
|
+
|
|
2928
|
+
tokens = set(ident_re.findall(expanded))
|
|
2929
|
+
|
|
2930
|
+
# Gate: every identifier token must be renderable. If any token is an
|
|
2931
|
+
# unrecognised bare identifier (not an input, not a known const, not
|
|
2932
|
+
# a structural keyword / namespace prefix, not a stable tracked var
|
|
2933
|
+
# that we already expanded), we conservatively refuse — that identifier
|
|
2934
|
+
# would otherwise leak through as an undeclared C++ symbol, or worse,
|
|
2935
|
+
# a series var that should have been rejected by the ctor guard.
|
|
2936
|
+
structural = (set(self._PINE_LOGICAL_OPS)
|
|
2937
|
+
| {"timeframe", "math", "syminfo",
|
|
2938
|
+
"int", "float", "bool", "string",
|
|
2939
|
+
"true", "false", "na"})
|
|
2940
|
+
member_tokens = (set(self._TIMEFRAME_MEMBER_CPP)
|
|
2941
|
+
| set(self._MATH_MEMBER_CPP))
|
|
2942
|
+
renderable = (self._input_backed_vars
|
|
2943
|
+
| self._stable_runtime_vars
|
|
2944
|
+
| structural
|
|
2945
|
+
| member_tokens)
|
|
2946
|
+
leftover = tokens - renderable
|
|
2947
|
+
# Known compile-time consts that survived expansion (e.g. ``pi`` was
|
|
2948
|
+
# NOT tracked as a Python value but its name token is a stable var
|
|
2949
|
+
# already; pure numeric names are in _known_vars and covered above).
|
|
2950
|
+
leftover = {t for t in leftover if t not in self._known_vars}
|
|
2951
|
+
# Inline ``input(...)`` / ``input.<t>(...)`` calls (a bare input
|
|
2952
|
+
# expression passed straight as a length arg, e.g.
|
|
2953
|
+
# ``adx(input(15), input(15))``) are re-parsed and rendered below,
|
|
2954
|
+
# after the gate. ``input`` is the only token they contribute, so
|
|
2955
|
+
# allow it through the gate here.
|
|
2956
|
+
leftover.discard("input")
|
|
2957
|
+
if leftover:
|
|
2958
|
+
return None
|
|
1840
2959
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2960
|
+
# Must depend on at least one runtime component (input-backed var, a
|
|
2961
|
+
# timeframe reference, or an inline input() call); otherwise it's a
|
|
2962
|
+
# pure compile-time expr and no reset is needed (the ctor-init literal
|
|
2963
|
+
# is correct).
|
|
2964
|
+
has_input = any(t in self._input_backed_vars for t in tokens)
|
|
2965
|
+
has_timeframe = "timeframe" in tokens
|
|
2966
|
+
has_inline_input = "input" in tokens
|
|
2967
|
+
if not (has_input or has_timeframe or has_inline_input):
|
|
1844
2968
|
return None
|
|
1845
2969
|
|
|
1846
|
-
#
|
|
1847
|
-
#
|
|
1848
|
-
#
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2970
|
+
# Preferred path: re-parse the (gate-approved, grouping-faithful)
|
|
2971
|
+
# expression and lower it through the SAME expression visitor the
|
|
2972
|
+
# statement path uses. Operator grouping and Pine numeric typing
|
|
2973
|
+
# (``/`` always yields float; ``(double)`` coercion) are then correct
|
|
2974
|
+
# by construction — reused from ``_visit_binop``, not re-derived here.
|
|
2975
|
+
# Falls back to the legacy token-substitution renderer below only if
|
|
2976
|
+
# re-parse / lowering unexpectedly fails, so a working site is never
|
|
2977
|
+
# worse off than before this change.
|
|
2978
|
+
lowered = self._lower_reset_expr_via_visitor(expanded)
|
|
2979
|
+
if lowered is not None:
|
|
2980
|
+
return lowered
|
|
2981
|
+
|
|
2982
|
+
# Render inline input() calls to override-aware get_input_*() reads
|
|
2983
|
+
# now that the gate has accepted the expression. Done after the gate
|
|
2984
|
+
# so the rendered getter tokens (get_input_int, ...) do not have to
|
|
2985
|
+
# be added to ``renderable``.
|
|
2986
|
+
if has_inline_input:
|
|
2987
|
+
expanded, _ = self._render_inline_input_calls(expanded)
|
|
2988
|
+
|
|
2989
|
+
expr = expanded
|
|
2990
|
+
|
|
2991
|
+
# 1) timeframe.<member> -> C++ (before ident substitution so the
|
|
2992
|
+
# member names don't get caught by the identifier pass). Use a
|
|
2993
|
+
# targeted regex so e.g. ``isminutes`` is not confused with
|
|
2994
|
+
# ``ismonthly``.
|
|
2995
|
+
def _tf_rep(p: re.Match) -> str:
|
|
2996
|
+
mem = p.group(1)
|
|
2997
|
+
return self._TIMEFRAME_MEMBER_CPP.get(mem, p.group(0))
|
|
2998
|
+
# ``timeframe.in_seconds()`` is a function-form member in Pine (the
|
|
2999
|
+
# only one in the table); its C++ form ``tf_to_seconds(script_tf_)``
|
|
3000
|
+
# is already a complete call, so consume the Pine ``()`` to avoid a
|
|
3001
|
+
# double-call ``tf_to_seconds(script_tf_)()``. Property-form members
|
|
3002
|
+
# (``timeframe.isdaily``) never carry ``()`` so the optional group is
|
|
3003
|
+
# a no-op for them.
|
|
3004
|
+
expr = re.sub(r"\btimeframe\.(\w+)(?:\(\))?", _tf_rep, expr)
|
|
3005
|
+
|
|
3006
|
+
# 2) math.<member> -> C++ (constants + std::* functions).
|
|
3007
|
+
def _math_rep(p: re.Match) -> str:
|
|
3008
|
+
mem = p.group(1)
|
|
3009
|
+
return self._MATH_MEMBER_CPP.get(mem, p.group(0))
|
|
3010
|
+
expr = re.sub(r"\bmath\.(\w+)", _math_rep, expr)
|
|
3011
|
+
|
|
3012
|
+
# 3) Pine word-logical operators -> C++ operators (after timeframe/math
|
|
3013
|
+
# substitution so we don't rewrite inside their C++ expansions).
|
|
3014
|
+
for pine_op, cpp_op in self._PINE_LOGICAL_OPS.items():
|
|
3015
|
+
expr = re.sub(rf"\b{pine_op}\b", cpp_op, expr)
|
|
3016
|
+
|
|
3017
|
+
# 4) Substitute input-backed vars with override-aware get_input_*()
|
|
3018
|
+
# reads, and inline known compile-time consts (non-input) as literals.
|
|
3019
|
+
def _sub(p: re.Match) -> str:
|
|
3020
|
+
name = p.group(0)
|
|
3021
|
+
if name in self._input_backed_vars:
|
|
3022
|
+
call_node = self._input_var_to_call.get(name)
|
|
3023
|
+
if call_node is None:
|
|
3024
|
+
return name
|
|
3025
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
3026
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
3027
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
3028
|
+
if name in self._known_vars and name not in self._input_backed_vars:
|
|
3029
|
+
val = self._known_vars[name]
|
|
3030
|
+
if isinstance(val, bool):
|
|
3031
|
+
return "true" if val else "false"
|
|
3032
|
+
if isinstance(val, (int, float)):
|
|
3033
|
+
return str(val)
|
|
3034
|
+
return f'std::string("{self._cpp_string_escape(val)}")'
|
|
3035
|
+
return name
|
|
1875
3036
|
|
|
1876
3037
|
rewritten = ident_re.sub(_sub, expr)
|
|
1877
|
-
|
|
1878
|
-
#
|
|
1879
|
-
# function appears (
|
|
1880
|
-
|
|
3038
|
+
|
|
3039
|
+
# 5) Pine auto-converts floats to ints for TA lengths; C++ does not.
|
|
3040
|
+
# If any math.* function appears (returns double) OR a timeframe.*
|
|
3041
|
+
# boolean is part of a ternary whose branches are doubles, wrap the
|
|
3042
|
+
# whole expression in an explicit int cast so the TA ctor gets an
|
|
3043
|
+
# integer length.
|
|
3044
|
+
had_math = "std::" in rewritten or bool(re.search(r"\btimeframe\b", expanded))
|
|
3045
|
+
if had_math:
|
|
1881
3046
|
return f"(int)({rewritten})"
|
|
1882
3047
|
return rewritten
|
|
1883
3048
|
|
|
3049
|
+
def _lower_reset_expr_via_visitor(self, expanded: str) -> str | None:
|
|
3050
|
+
"""Re-parse a gate-approved, fully-expanded TA-length expression and
|
|
3051
|
+
lower it through the SAME expression visitor the statement path uses,
|
|
3052
|
+
so operator grouping and Pine numeric typing are preserved identically
|
|
3053
|
+
(Pine ``/`` always yields float; ``math.*`` returns double; branches are
|
|
3054
|
+
parenthesized). Reuses ``_visit_binop`` etc.; nothing is re-typed here.
|
|
3055
|
+
|
|
3056
|
+
Input-backed variables render as override-aware ``get_input_*()`` reads
|
|
3057
|
+
(not member refs) via ``_reset_input_getter_mode`` — the reset can run
|
|
3058
|
+
in ``evaluate_security`` before the input members are initialised, so it
|
|
3059
|
+
must not depend on their init order. Inline ``input(...)`` calls and
|
|
3060
|
+
``math.*`` / ``timeframe.*`` members lower through the visitor's own
|
|
3061
|
+
handlers (same C++ the statement path would emit).
|
|
3062
|
+
|
|
3063
|
+
Returns None if re-parse / lowering fails, so the caller falls back to
|
|
3064
|
+
the legacy token-substitution renderer (never worse than before)."""
|
|
3065
|
+
try:
|
|
3066
|
+
from ..lexer import Lexer
|
|
3067
|
+
from ..parser import Parser
|
|
3068
|
+
tokens = Lexer(expanded).tokenize()
|
|
3069
|
+
node = Parser(tokens, source=expanded)._parse_expression()
|
|
3070
|
+
except Exception:
|
|
3071
|
+
return None
|
|
3072
|
+
prev = self._reset_input_getter_mode
|
|
3073
|
+
self._reset_input_getter_mode = True
|
|
3074
|
+
try:
|
|
3075
|
+
rendered = self._visit_expr(node)
|
|
3076
|
+
except Exception:
|
|
3077
|
+
return None
|
|
3078
|
+
finally:
|
|
3079
|
+
self._reset_input_getter_mode = prev
|
|
3080
|
+
if not rendered or "/* " in rendered:
|
|
3081
|
+
# Unknown/unhandled node leaked a placeholder — defer to legacy.
|
|
3082
|
+
return None
|
|
3083
|
+
# A TA length must be int. Truncate when the lowered form is
|
|
3084
|
+
# float-typed (a ``std::*`` call, a ``(double)`` division coercion, or a
|
|
3085
|
+
# ``timeframe.*`` helper — all timeframe helpers reference script_tf_).
|
|
3086
|
+
# A bare int / identifier length carries none of these and is left
|
|
3087
|
+
# unwrapped, so simple sites stay byte-identical to the legacy output.
|
|
3088
|
+
if ("std::" in rendered or "(double)" in rendered
|
|
3089
|
+
or "script_tf_" in rendered):
|
|
3090
|
+
return f"(int)({rendered})"
|
|
3091
|
+
return rendered
|
|
3092
|
+
|
|
3093
|
+
|
|
1884
3094
|
def _collect_ta_runtime_resets(self) -> list[str]:
|
|
1885
3095
|
"""Collect reassignment statements for every TA object whose ctor args
|
|
1886
3096
|
depend on an input-backed variable. Returned strings are raw C++
|
|
@@ -1892,7 +3102,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1892
3102
|
resets: list[str] = []
|
|
1893
3103
|
|
|
1894
3104
|
# Main-context TA objects
|
|
1895
|
-
for site in self.ctx.ta_call_sites:
|
|
3105
|
+
for _ta_idx, site in enumerate(self.ctx.ta_call_sites):
|
|
3106
|
+
if _ta_idx in self._dead_ta_indices:
|
|
3107
|
+
continue
|
|
1896
3108
|
if not site.ctor_args:
|
|
1897
3109
|
continue
|
|
1898
3110
|
runtime_args: list[str] = []
|
|
@@ -1910,15 +3122,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1910
3122
|
f"{site.member_name} = {site.class_name}({', '.join(runtime_args)});"
|
|
1911
3123
|
)
|
|
1912
3124
|
|
|
1913
|
-
# Security-context TA copies
|
|
3125
|
+
# Security-context TA copies. Normally these share the ctor args of
|
|
3126
|
+
# their main-context site, but a request.security nested in a helper
|
|
3127
|
+
# called at several sites is cloned per call site (distinct sec_id +
|
|
3128
|
+
# callsite_idx) while the shared TA site's ``ctor_args`` were resolved
|
|
3129
|
+
# ONCE (against the first call site). Every clone would then size its
|
|
3130
|
+
# indicator from call site 0's argument (e.g. four EMAs all pinned to
|
|
3131
|
+
# crossFastLen instead of the per-site fast/slow lengths). Resolve each
|
|
3132
|
+
# sec's ctor args against ITS call site by reusing the per-call-site
|
|
3133
|
+
# function-clone TA remap (identity for cs0 / non-clones, so all other
|
|
3134
|
+
# output stays byte-identical).
|
|
3135
|
+
sec_call_by_id = {it["sec_id"]: it for it in self._security_calls}
|
|
3136
|
+
ta_site_by_member = {s.member_name: s for s in self.ctx.ta_call_sites}
|
|
1914
3137
|
for info in self._security_eval_info:
|
|
3138
|
+
sec_item = sec_call_by_id.get(info["sec_id"])
|
|
3139
|
+
sec_containing = (sec_item or {}).get("containing_func") or ""
|
|
3140
|
+
sec_cs_idx = (sec_item or {}).get("callsite_idx")
|
|
1915
3141
|
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
1916
3142
|
site = self.ctx.ta_call_sites[idx]
|
|
1917
|
-
|
|
3143
|
+
ctor_site = site
|
|
3144
|
+
if sec_containing and sec_cs_idx is not None:
|
|
3145
|
+
remap = self._func_cs_ta_remap.get((sec_containing, sec_cs_idx))
|
|
3146
|
+
if remap:
|
|
3147
|
+
cloned_name = remap.get(site.member_name)
|
|
3148
|
+
if cloned_name and cloned_name != site.member_name:
|
|
3149
|
+
cand = ta_site_by_member.get(cloned_name)
|
|
3150
|
+
if cand is not None:
|
|
3151
|
+
ctor_site = cand
|
|
3152
|
+
if not ctor_site.ctor_args:
|
|
1918
3153
|
continue
|
|
1919
3154
|
runtime_args = []
|
|
1920
3155
|
any_runtime = False
|
|
1921
|
-
for a in
|
|
3156
|
+
for a in ctor_site.ctor_args:
|
|
1922
3157
|
rt = self._runtime_ctor_arg_for_reset(a)
|
|
1923
3158
|
if rt is not None:
|
|
1924
3159
|
runtime_args.append(rt)
|