@pineforge/codegen-pyodide 0.8.0 → 0.8.1
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 +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -39,6 +39,7 @@ from .tables import (
|
|
|
39
39
|
BAR_FIELDS,
|
|
40
40
|
BAR_BUILTINS,
|
|
41
41
|
BAR_SERIES_PUSH,
|
|
42
|
+
DRAWING_TYPE_TO_CPP,
|
|
42
43
|
SECURITY_OHLC_BAR_FIELDS,
|
|
43
44
|
TA_RETURNS_BOOL,
|
|
44
45
|
TA_IMPLICIT_COMPUTE,
|
|
@@ -112,12 +113,17 @@ from .visit_stmt import StmtVisitor
|
|
|
112
113
|
from .visit_expr import ExprVisitor
|
|
113
114
|
from .visit_call import CallVisitor
|
|
114
115
|
|
|
116
|
+
# DrawingVisitor owns the drawing-objects-as-data dispatch (line/box/label/
|
|
117
|
+
# linefill/chart.point lowering onto the per-type arenas) plus _uses_drawing
|
|
118
|
+
# detection and arena-cap computation. See codegen/drawing.py.
|
|
119
|
+
from .drawing import DrawingVisitor
|
|
120
|
+
|
|
115
121
|
|
|
116
122
|
# ---------------------------------------------------------------------------
|
|
117
123
|
# CodeGen class
|
|
118
124
|
# ---------------------------------------------------------------------------
|
|
119
125
|
|
|
120
|
-
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, NamingHelper):
|
|
126
|
+
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
|
|
121
127
|
"""Generate C++ from an AnalyzerContext (visitor pattern).
|
|
122
128
|
|
|
123
129
|
Mixin chain (Python MRO is left-to-right; method names are
|
|
@@ -177,7 +183,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
177
183
|
self._active_var_remap: dict[str, str] = {}
|
|
178
184
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
179
185
|
self._func_var_members_set: set[str] = set()
|
|
186
|
+
# BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
|
|
187
|
+
# local initialised from a var/global UDT lvalue, mutated through, AND
|
|
188
|
+
# later rebound to a different lvalue). Member access lowers to ``->``
|
|
189
|
+
# and rebinds to ``&(...)``. Reset per function in _emit_func_def is not
|
|
190
|
+
# needed: names are function-unique and the value-copy fallback ignores
|
|
191
|
+
# entries for inactive functions.
|
|
192
|
+
self._udt_ptr_alias_locals: set[str] = set()
|
|
180
193
|
self._precalc_loop_active: bool = False
|
|
194
|
+
# Names of ``var`` members that live in a FUNCTION scope (not global).
|
|
195
|
+
# These are initialized once-per-function-variant on first call (a
|
|
196
|
+
# function-local static equivalent), NOT in the constructor / on_bar
|
|
197
|
+
# preamble. See ``_emit_func_var_init_block``.
|
|
198
|
+
self._func_local_var_names: set[str] = set()
|
|
199
|
+
for _vlist in ctx.func_var_members.values():
|
|
200
|
+
for _n, _, _ in _vlist:
|
|
201
|
+
self._func_local_var_names.add(_n)
|
|
181
202
|
|
|
182
203
|
# Build per-function var/series name lists for cloning.
|
|
183
204
|
# For each function with call-site variants, collect ALL function-scoped
|
|
@@ -260,30 +281,61 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
260
281
|
|
|
261
282
|
# Build cloned site remapping for cs > 0 (must happen before _ta_site_map
|
|
262
283
|
# so cloned names are in _func_ta_members and get filtered out of the map)
|
|
284
|
+
#
|
|
285
|
+
# Default to the ``{orig}_cs{cs_idx}`` formula (matches the analyzer's clone
|
|
286
|
+
# naming), but defer to the analyzer's authoritative clone-name map for any
|
|
287
|
+
# site it had to disambiguate (a TA site reached through multiple enclosing
|
|
288
|
+
# functions would otherwise collide on the formula). Keeping the formula as
|
|
289
|
+
# the default leaves all non-colliding output byte-identical.
|
|
290
|
+
clone_names = getattr(ctx, "func_cs_ta_clone_names", {})
|
|
263
291
|
for fname, orig_names in func_ta_originals.items():
|
|
264
292
|
total_cs = ctx.func_call_site_counts.get(fname, 1)
|
|
265
293
|
for cs_idx in range(1, total_cs):
|
|
294
|
+
overrides = clone_names.get((fname, cs_idx), {})
|
|
266
295
|
remap = {}
|
|
267
296
|
for orig_name in orig_names:
|
|
268
|
-
remap[orig_name] =
|
|
297
|
+
remap[orig_name] = overrides.get(
|
|
298
|
+
orig_name, f"{orig_name}_cs{cs_idx}")
|
|
269
299
|
self._func_cs_ta_remap[(fname, cs_idx)] = remap
|
|
270
300
|
self._func_ta_members.update(remap.values())
|
|
271
301
|
|
|
272
302
|
for site in ctx.ta_call_sites:
|
|
273
303
|
if site.node is not None:
|
|
274
304
|
if site.member_name not in self._func_ta_members:
|
|
305
|
+
# Top-level (non-function) site: maps to itself.
|
|
275
306
|
self._ta_site_map[id(site.node)] = site
|
|
276
|
-
elif
|
|
277
|
-
#
|
|
307
|
+
elif id(site.node) not in self._ta_site_map:
|
|
308
|
+
# Function-local site. Multiple clones share the SAME AST
|
|
309
|
+
# node (clones copy ``node=orig.node``); the FIRST one in
|
|
310
|
+
# ``ta_call_sites`` order is the canonical original (cs0)
|
|
311
|
+
# whose ``member_name`` the per-call-site remap is keyed on.
|
|
312
|
+
# Later clones (``_cs{i}``, ``_cs{i}_cs{j}``, ``_u{n}`` …)
|
|
313
|
+
# must NOT overwrite it: doing so poisons the base name so
|
|
314
|
+
# the active-remap lookup misses and every clone collapses
|
|
315
|
+
# onto one member. Keep the original; the variant member is
|
|
316
|
+
# resolved via ``_active_ta_remap`` at emit time.
|
|
278
317
|
self._ta_site_map[id(site.node)] = site
|
|
279
318
|
self._ta_index_by_site_id: dict[int, int] = {
|
|
280
319
|
id(site): i for i, site in enumerate(ctx.ta_call_sites)
|
|
281
320
|
}
|
|
321
|
+
# Context-sensitive (call-path) instance machinery for nested stateful
|
|
322
|
+
# helpers. Built by ``_build_func_instances`` below. ``_current_instance_name``
|
|
323
|
+
# names the function clone whose body is currently being emitted (None at
|
|
324
|
+
# top level / non-variant bodies). ``_instance_dispatch`` maps
|
|
325
|
+
# ``(enclosing_instance_name, call_node_id) -> callee emit-name`` and is the
|
|
326
|
+
# authority for nested stateful-helper dispatch (see visit_call).
|
|
327
|
+
self._current_instance_name: str | None = None
|
|
328
|
+
self._instance_dispatch: dict[tuple[str | None, int], str] = {}
|
|
329
|
+
self._fresh_instances: list[dict] = []
|
|
330
|
+
self._fresh_var_members: list[tuple[str, str]] = []
|
|
331
|
+
# NOTE: _build_func_instances() runs at the top of generate() (it needs
|
|
332
|
+
# _all_member_names / _func_safe_name, which are populated later in __init__).
|
|
282
333
|
# Build lookup: node id -> FixnanCallSite (counter-based)
|
|
283
334
|
self._fixnan_counter = 0
|
|
284
335
|
self._switch_counter = 0
|
|
285
336
|
self._security_inline_counter = 0
|
|
286
337
|
self._random_call_counter = 0
|
|
338
|
+
self._for_counter = 0
|
|
287
339
|
# UDT / enum (needed before _collect_known_vars for input.enum)
|
|
288
340
|
self._udt_defs: dict[str, list] = {}
|
|
289
341
|
self._enum_defs: dict[str, list[str]] = {}
|
|
@@ -308,6 +360,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
308
360
|
# Map input-backed var name -> its input.*() FuncCall node so we can
|
|
309
361
|
# later emit a runtime get_input_*() read with the same title/default.
|
|
310
362
|
self._input_var_to_call: dict[str, FuncCall] = {}
|
|
363
|
+
# Class-scope arithmetic-over-input vars (e.g. ``wilderLen = rsiLen*2-1``).
|
|
364
|
+
# Maps the derived var name -> its raw RHS expression string. When such a
|
|
365
|
+
# var feeds a TA ctor length, the runtime-reset path expands it so input
|
|
366
|
+
# overrides propagate (``(get_input_int("RSI Length",14) * 2 - 1)``); the
|
|
367
|
+
# ctor-init list still folds to the Pine-default literal via _resolve_known.
|
|
368
|
+
self._derived_input_expr: dict[str, str] = {}
|
|
311
369
|
self._timeframe_period_vars: set[str] = set()
|
|
312
370
|
self._collect_known_vars()
|
|
313
371
|
# Track var names
|
|
@@ -337,6 +395,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
337
395
|
self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
|
|
338
396
|
self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
|
|
339
397
|
self._collection_types: dict[str, TypeSpec] = getattr(ctx, "collection_types", {}) or {}
|
|
398
|
+
# id(block_node) -> {raw_var_name: unique_member} for block-scoped var
|
|
399
|
+
# name collisions (see Analyzer._visit_VarDecl). Activated into
|
|
400
|
+
# ``_active_var_remap`` while emitting the owning block's statements.
|
|
401
|
+
self._block_var_renames: dict[int, dict[str, str]] = getattr(ctx, "block_var_renames", {}) or {}
|
|
340
402
|
self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = getattr(ctx, "udt_field_type_specs", {}) or {}
|
|
341
403
|
# Map UDT struct name -> set of field names that were dropped from the
|
|
342
404
|
# emitted C++ struct because they had drawing-only types (label, line,
|
|
@@ -347,7 +409,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
347
409
|
# rewrite or strip downstream references to those fields so the
|
|
348
410
|
# generated C++ never references a member that doesn't exist on the
|
|
349
411
|
# emitted struct. See: pineforge-codegen issue #10.
|
|
350
|
-
|
|
412
|
+
# Drawing-objects-as-data: line/box/label/linefill/chart.point are now
|
|
413
|
+
# REAL data (un-dropped from UDT structs). Only table/polyline stay
|
|
414
|
+
# dropped (no C++ representation). See drawing-objects-as-data.md §4.2.
|
|
415
|
+
_DRAWING_TYPES_INIT = {"table", "polyline"}
|
|
351
416
|
self._udt_omitted_fields: dict[str, set[str]] = {}
|
|
352
417
|
for _type_name, _fields in self._udt_defs.items():
|
|
353
418
|
_omitted = set()
|
|
@@ -378,6 +443,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
378
443
|
self._map_vars.add(_name)
|
|
379
444
|
elif _spec.kind == "udt" and _spec.name:
|
|
380
445
|
self._udt_var_types.setdefault(_name, _spec.name)
|
|
446
|
+
# Table / polyline variables and params have NO C++ representation
|
|
447
|
+
# (SKIP_VAR_TYPES). A *method* call on such a receiver
|
|
448
|
+
# (``panel.cell(...)``, ``dash.merge_cells(...)``) is a visual no-op
|
|
449
|
+
# that must be dropped — but unlike the namespace form
|
|
450
|
+
# (``table.cell(...)``) the receiver is a bare var/param the
|
|
451
|
+
# namespace-based skip cannot see. Collect those names so
|
|
452
|
+
# ``_is_skip_expr`` can drop their method calls.
|
|
453
|
+
_SKIP_DECL_TYPES = set(SKIP_VAR_TYPES) | {"polyline"}
|
|
454
|
+
self._visual_drop_vars: set[str] = set()
|
|
455
|
+
for _node in self._walk_ast(self.ctx.ast):
|
|
456
|
+
if isinstance(_node, VarDecl):
|
|
457
|
+
if _node.type_hint in _SKIP_DECL_TYPES:
|
|
458
|
+
self._visual_drop_vars.add(_node.name)
|
|
459
|
+
elif isinstance(_node.value, FuncCall):
|
|
460
|
+
_fn, _ns = self._resolve_callee(_node.value.callee)
|
|
461
|
+
if _fn == "new" and _ns in _SKIP_DECL_TYPES:
|
|
462
|
+
self._visual_drop_vars.add(_node.name)
|
|
463
|
+
elif isinstance(_node, (FuncDef, MethodDef)):
|
|
464
|
+
_hints = (getattr(_node, "annotations", None) or {}).get("param_type_hints") or []
|
|
465
|
+
for _i, _p in enumerate(getattr(_node, "params", []) or []):
|
|
466
|
+
_h = _hints[_i] if _i < len(_hints) else None
|
|
467
|
+
if _h and str(_h).replace(" ", "") in _SKIP_DECL_TYPES:
|
|
468
|
+
self._visual_drop_vars.add(_p)
|
|
381
469
|
# Collect request.security metadata per call
|
|
382
470
|
self._security_eval_info: list[dict] = []
|
|
383
471
|
self._security_ta_variant_names: dict[tuple[int, int, tuple], str] = {}
|
|
@@ -388,15 +476,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
388
476
|
lookahead_node = item.get("lookahead_node")
|
|
389
477
|
ta_range = item.get("ta_range")
|
|
390
478
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
and tf_node.name not in self._input_backed_vars):
|
|
397
|
-
val = self._known_vars[tf_node.name]
|
|
398
|
-
if isinstance(val, str):
|
|
399
|
-
tf_str = val
|
|
479
|
+
# Resolve the timeframe: a literal/const/global gives a static tf;
|
|
480
|
+
# a function-parameter tf is resolved from the call sites (the
|
|
481
|
+
# evaluator is a class method, so the param is not in scope there).
|
|
482
|
+
tf_str, tf_expr = self._resolve_security_tf(
|
|
483
|
+
tf_node, item.get("containing_func", ""))
|
|
400
484
|
|
|
401
485
|
is_lookahead_on = False
|
|
402
486
|
if lookahead_node is not None:
|
|
@@ -447,9 +531,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
447
531
|
self._security_eval_info.append({
|
|
448
532
|
"sec_id": sec_id,
|
|
449
533
|
"tf": tf_str,
|
|
534
|
+
"tf_expr": tf_expr,
|
|
450
535
|
"tf_node": tf_node,
|
|
451
536
|
"gaps_on": is_gaps_on,
|
|
452
537
|
"lookahead_on": is_lookahead_on,
|
|
538
|
+
"heikinashi": bool(item.get("heikinashi", False)),
|
|
453
539
|
"ta_range": ta_range,
|
|
454
540
|
"ta_indices": sorted(ta_indices),
|
|
455
541
|
"ta_binding_stacks": ta_binding_stacks,
|
|
@@ -468,6 +554,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
468
554
|
|
|
469
555
|
self._register_global_aggregate_member_types()
|
|
470
556
|
self._uses_matrix = self._detect_matrix_usage()
|
|
557
|
+
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
558
|
+
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
559
|
+
# byte-identical. Caps come from the strategy() header max_*_count.
|
|
560
|
+
self._uses_drawing = self._detect_drawing_usage()
|
|
561
|
+
self._drawing_caps = self._compute_drawing_caps() if self._uses_drawing else {}
|
|
471
562
|
|
|
472
563
|
# max_bars_back: the per-variable history depth the engine's Series<T>
|
|
473
564
|
# ring buffer should retain. Pine exposes this two ways — the
|
|
@@ -493,6 +584,194 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
493
584
|
# history reads off security-helper series.
|
|
494
585
|
self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
|
|
495
586
|
|
|
587
|
+
# ------------------------------------------------------------------
|
|
588
|
+
# Context-sensitive (call-path) instance machinery
|
|
589
|
+
# ------------------------------------------------------------------
|
|
590
|
+
def _iter_func_calls(self, root) -> list:
|
|
591
|
+
"""Collect every ``FuncCall`` node reachable from ``root`` (a stmt list
|
|
592
|
+
or single AST node). Order-independent; used by the instance pre-pass to
|
|
593
|
+
find nested user-function calls inside a function body."""
|
|
594
|
+
out: list = []
|
|
595
|
+
seen: set[int] = set()
|
|
596
|
+
stack: list = list(root) if isinstance(root, (list, tuple)) else [root]
|
|
597
|
+
while stack:
|
|
598
|
+
node = stack.pop()
|
|
599
|
+
if node is None:
|
|
600
|
+
continue
|
|
601
|
+
if isinstance(node, (list, tuple)):
|
|
602
|
+
stack.extend(node)
|
|
603
|
+
continue
|
|
604
|
+
if isinstance(node, dict):
|
|
605
|
+
stack.extend(node.values())
|
|
606
|
+
continue
|
|
607
|
+
if not hasattr(node, "__dict__"):
|
|
608
|
+
continue
|
|
609
|
+
nid = id(node)
|
|
610
|
+
if nid in seen:
|
|
611
|
+
continue
|
|
612
|
+
seen.add(nid)
|
|
613
|
+
if isinstance(node, FuncCall):
|
|
614
|
+
out.append(node)
|
|
615
|
+
for v in vars(node).values():
|
|
616
|
+
if isinstance(v, (list, tuple, dict)) or hasattr(v, "__dict__"):
|
|
617
|
+
stack.append(v)
|
|
618
|
+
return out
|
|
619
|
+
|
|
620
|
+
def _build_func_instances(self) -> None:
|
|
621
|
+
"""Context-sensitive cloning of nested stateful helper functions.
|
|
622
|
+
|
|
623
|
+
A stateful helper ``G`` (carrying TA state and/or ``var`` members) may be
|
|
624
|
+
reached through several distinct call paths — e.g. ``leg`` called from
|
|
625
|
+
three clones of ``f_get`` (lengths 10/20/30) *and* directly. Each path is
|
|
626
|
+
a logically-distinct instance that must drive its OWN TA/var members.
|
|
627
|
+
|
|
628
|
+
The analyzer already mints the per-path members (via range-widening), but
|
|
629
|
+
the flat ``{G}_cs{idx}`` clone namespace conflates a callee's own textual
|
|
630
|
+
call sites with the enclosing function's call sites. This pre-pass walks
|
|
631
|
+
the call graph from each natural clone and, for every nested stateful
|
|
632
|
+
call, composes the enclosing clone's active remap with the callee's
|
|
633
|
+
per-call-site remap:
|
|
634
|
+
|
|
635
|
+
composed_ta[m] = R_enclosing.get(R_callee_cs[m], R_callee_cs[m])
|
|
636
|
+
|
|
637
|
+
When the composition equals the callee's natural ``cs{j}`` remap the call
|
|
638
|
+
dispatches to the existing ``{G}_cs{j}`` clone (output stays byte-identical
|
|
639
|
+
for the common single-caller case). Otherwise a fresh instance is minted,
|
|
640
|
+
bound to the path-specific members (and FRESH ``var`` members so two paths
|
|
641
|
+
never share scalar state). ``_instance_dispatch`` records the resolved
|
|
642
|
+
emit-name per ``(enclosing_instance, call_node)``; ``_fresh_instances`` /
|
|
643
|
+
``_fresh_var_members`` carry the extra code to emit.
|
|
644
|
+
"""
|
|
645
|
+
ctx = self.ctx
|
|
646
|
+
stateful = (set(ctx.func_ta_ranges.keys())
|
|
647
|
+
| set(ctx.func_var_members.keys())
|
|
648
|
+
| set(ctx.func_series_vars.keys()))
|
|
649
|
+
if not stateful:
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
func_bodies: dict[str, list] = {}
|
|
653
|
+
for fi in ctx.func_infos:
|
|
654
|
+
node = getattr(fi, "node", None)
|
|
655
|
+
if node is not None and getattr(node, "body", None):
|
|
656
|
+
func_bodies.setdefault(fi.name, node.body)
|
|
657
|
+
|
|
658
|
+
def ta_originals(fname: str) -> list[str]:
|
|
659
|
+
return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
|
|
660
|
+
|
|
661
|
+
def var_originals(fname: str) -> list[str]:
|
|
662
|
+
return [self._safe_name(n) for n, _, _ in ctx.func_var_members.get(fname, [])]
|
|
663
|
+
|
|
664
|
+
def natural_name(fname: str, cs_idx: int) -> str:
|
|
665
|
+
return f"{self._func_safe_name(fname)}_cs{cs_idx}"
|
|
666
|
+
|
|
667
|
+
interned: dict[tuple, dict] = {}
|
|
668
|
+
worklist: list[dict] = []
|
|
669
|
+
seen_walk: set[str] = set()
|
|
670
|
+
fresh_counter = 0
|
|
671
|
+
|
|
672
|
+
# Seed with the natural clones the flat emission loop produces.
|
|
673
|
+
for fname in stateful:
|
|
674
|
+
if fname not in func_bodies:
|
|
675
|
+
continue
|
|
676
|
+
total_cs = ctx.func_call_site_counts.get(fname, 0)
|
|
677
|
+
if total_cs > 0:
|
|
678
|
+
for k in range(total_cs):
|
|
679
|
+
worklist.append({
|
|
680
|
+
"fname": fname,
|
|
681
|
+
"name": natural_name(fname, k),
|
|
682
|
+
"ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
|
|
683
|
+
"var_remap": self._func_cs_var_remap.get((fname, k), {}),
|
|
684
|
+
})
|
|
685
|
+
else:
|
|
686
|
+
worklist.append({
|
|
687
|
+
"fname": fname,
|
|
688
|
+
"name": self._func_safe_name(fname),
|
|
689
|
+
"ta_remap": {},
|
|
690
|
+
"var_remap": {},
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
while worklist:
|
|
694
|
+
inst = worklist.pop()
|
|
695
|
+
if inst["name"] in seen_walk:
|
|
696
|
+
continue
|
|
697
|
+
seen_walk.add(inst["name"])
|
|
698
|
+
body = func_bodies.get(inst["fname"])
|
|
699
|
+
if not body:
|
|
700
|
+
continue
|
|
701
|
+
active_ta = inst["ta_remap"]
|
|
702
|
+
for callnode in self._iter_func_calls(body):
|
|
703
|
+
cs_info = ctx.func_call_cs_map.get(id(callnode))
|
|
704
|
+
if cs_info is None:
|
|
705
|
+
continue
|
|
706
|
+
g_name, j = cs_info
|
|
707
|
+
if g_name not in stateful:
|
|
708
|
+
continue
|
|
709
|
+
natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
|
|
710
|
+
composed_ta = {}
|
|
711
|
+
for m in ta_originals(g_name):
|
|
712
|
+
mid = natural_ta.get(m, m)
|
|
713
|
+
composed_ta[m] = active_ta.get(mid, mid)
|
|
714
|
+
if composed_ta == natural_ta:
|
|
715
|
+
# Path resolves to the callee's own cs{j} clone — reuse it.
|
|
716
|
+
self._instance_dispatch[(inst["name"], id(callnode))] = \
|
|
717
|
+
natural_name(g_name, j)
|
|
718
|
+
continue
|
|
719
|
+
key = (g_name, frozenset(composed_ta.items()))
|
|
720
|
+
ginst = interned.get(key)
|
|
721
|
+
if ginst is None:
|
|
722
|
+
fresh_counter += 1
|
|
723
|
+
inst_name = f"{self._func_safe_name(g_name)}__ni{fresh_counter}"
|
|
724
|
+
fvar_remap: dict[str, str] = {}
|
|
725
|
+
for v in var_originals(g_name):
|
|
726
|
+
fresh_member = f"{v}__ni{fresh_counter}"
|
|
727
|
+
fvar_remap[v] = fresh_member
|
|
728
|
+
self._fresh_var_members.append((v, fresh_member))
|
|
729
|
+
ginst = {
|
|
730
|
+
"fname": g_name,
|
|
731
|
+
"name": inst_name,
|
|
732
|
+
"ta_remap": composed_ta,
|
|
733
|
+
"var_remap": fvar_remap,
|
|
734
|
+
}
|
|
735
|
+
interned[key] = ginst
|
|
736
|
+
self._fresh_instances.append(ginst)
|
|
737
|
+
worklist.append(ginst)
|
|
738
|
+
self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
|
|
739
|
+
|
|
740
|
+
def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
|
|
741
|
+
series_suffix: str, lines: list[str]) -> None:
|
|
742
|
+
"""Declare a per-clone copy of a function-scoped ``var`` member, matching
|
|
743
|
+
the original's C++ type (series / matrix / array / map / drawing-handle /
|
|
744
|
+
UDT / scalar). Shared by the per-call-site clone loop and the fresh
|
|
745
|
+
context-sensitive instance loop."""
|
|
746
|
+
for vname, ptype, _init_str in self.ctx.var_members:
|
|
747
|
+
if self._safe_name(vname) == orig_safe:
|
|
748
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
749
|
+
if vname in self.ctx.series_vars:
|
|
750
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
751
|
+
elif vname in self._matrix_specs:
|
|
752
|
+
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
|
|
753
|
+
elif vname in self._array_vars:
|
|
754
|
+
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
|
|
755
|
+
elif vname in self._map_vars:
|
|
756
|
+
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
|
|
757
|
+
elif vname in self._udt_var_types:
|
|
758
|
+
# Drawing handle / UDT var clone must match the original's
|
|
759
|
+
# type (Line/Label/Box/<UDT>), not the coarse PineType
|
|
760
|
+
# default (double) — otherwise the clone can't hold the
|
|
761
|
+
# handle and drawing access on it reads a garbage / na id.
|
|
762
|
+
udt_t = self._udt_var_types[vname]
|
|
763
|
+
handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
|
|
764
|
+
lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
|
|
765
|
+
else:
|
|
766
|
+
lines.append(f" {cpp_type} {cloned_safe};")
|
|
767
|
+
return
|
|
768
|
+
# Non-var series var
|
|
769
|
+
if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
|
|
770
|
+
cpp_type = self._series_type_for(orig_safe)
|
|
771
|
+
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
772
|
+
else:
|
|
773
|
+
lines.append(f" double {cloned_safe} = 0.0;")
|
|
774
|
+
|
|
496
775
|
@staticmethod
|
|
497
776
|
def _int_literal_value(node: ASTNode | None) -> int | None:
|
|
498
777
|
"""Return the integer value of a (possibly unary-minus) NumberLiteral,
|
|
@@ -759,6 +1038,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
759
1038
|
walk(stmt)
|
|
760
1039
|
return reassigned
|
|
761
1040
|
|
|
1041
|
+
def _arith_expr_to_str(self, node) -> str | None:
|
|
1042
|
+
"""Render a numeric arithmetic-over-identifiers expression to a string
|
|
1043
|
+
whose token spelling matches ``_resolve_known`` / ``_runtime_ctor_arg_for_reset``
|
|
1044
|
+
(e.g. ``rsiLen * 2 - 1``). Returns None for any node shape we don't fold
|
|
1045
|
+
(series subscripts, ternaries, etc.) so the caller leaves the var untracked.
|
|
1046
|
+
"""
|
|
1047
|
+
if isinstance(node, NumberLiteral):
|
|
1048
|
+
v = node.value
|
|
1049
|
+
if isinstance(v, float) and v == int(v):
|
|
1050
|
+
return str(int(v))
|
|
1051
|
+
return str(v)
|
|
1052
|
+
if isinstance(node, Identifier):
|
|
1053
|
+
return node.name
|
|
1054
|
+
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
1055
|
+
return f"{node.object.name}.{node.member}"
|
|
1056
|
+
if isinstance(node, BinOp):
|
|
1057
|
+
l = self._arith_expr_to_str(node.left)
|
|
1058
|
+
r = self._arith_expr_to_str(node.right)
|
|
1059
|
+
if l is None or r is None:
|
|
1060
|
+
return None
|
|
1061
|
+
return f"{l} {node.op} {r}"
|
|
1062
|
+
if isinstance(node, UnaryOp):
|
|
1063
|
+
o = self._arith_expr_to_str(node.operand)
|
|
1064
|
+
if o is None:
|
|
1065
|
+
return None
|
|
1066
|
+
return f"{node.op}{o}"
|
|
1067
|
+
if isinstance(node, FuncCall):
|
|
1068
|
+
callee = self._arith_expr_to_str(node.callee)
|
|
1069
|
+
if callee is None:
|
|
1070
|
+
return None
|
|
1071
|
+
parts = []
|
|
1072
|
+
for a in node.args:
|
|
1073
|
+
s = self._arith_expr_to_str(a)
|
|
1074
|
+
if s is None:
|
|
1075
|
+
return None
|
|
1076
|
+
parts.append(s)
|
|
1077
|
+
return f"{callee}({', '.join(parts)})"
|
|
1078
|
+
return None
|
|
1079
|
+
|
|
762
1080
|
def _collect_known_var(self, node: VarDecl) -> None:
|
|
763
1081
|
"""Extract known constant value from a VarDecl."""
|
|
764
1082
|
# Don't inline series variables — their values change over time
|
|
@@ -811,6 +1129,36 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
811
1129
|
if stored:
|
|
812
1130
|
self._input_backed_vars.add(node.name)
|
|
813
1131
|
self._input_var_to_call[node.name] = node.value
|
|
1132
|
+
# Class-scope arithmetic over known/input-backed vars
|
|
1133
|
+
# (``wilderLen = rsiLen * 2 - 1``, ``n = math.round(len / 2)``).
|
|
1134
|
+
# Without this branch the derived name is untracked, the TA ctor arg
|
|
1135
|
+
# 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 and
|
|
1137
|
+
# (b) record the raw expression so the reset path can re-expand any
|
|
1138
|
+
# input-backed operand to its get_input_*() runtime read.
|
|
1139
|
+
elif isinstance(node.value, (BinOp, UnaryOp, FuncCall)):
|
|
1140
|
+
expr_str = self._arith_expr_to_str(node.value)
|
|
1141
|
+
if expr_str is not None:
|
|
1142
|
+
import re as _re
|
|
1143
|
+
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
|
+
refs_input = any(t in self._input_backed_vars for t in tokens)
|
|
1146
|
+
refs_derived = any(t in self._derived_input_expr for t in tokens)
|
|
1147
|
+
if refs_known or refs_input or refs_derived:
|
|
1148
|
+
folded = self._resolve_known(expr_str)
|
|
1149
|
+
if self._is_compile_time_value(folded):
|
|
1150
|
+
try:
|
|
1151
|
+
num = float(folded)
|
|
1152
|
+
self._known_vars[node.name] = (
|
|
1153
|
+
int(num) if num == int(num) else num
|
|
1154
|
+
)
|
|
1155
|
+
except ValueError:
|
|
1156
|
+
pass
|
|
1157
|
+
if refs_input or refs_derived:
|
|
1158
|
+
# Track as input-backed so use-sites are not inlined and
|
|
1159
|
+
# the runtime-reset path emits the override-aware expr.
|
|
1160
|
+
self._derived_input_expr[node.name] = expr_str
|
|
1161
|
+
self._input_backed_vars.add(node.name)
|
|
814
1162
|
|
|
815
1163
|
# ------------------------------------------------------------------
|
|
816
1164
|
# Public entry point
|
|
@@ -869,9 +1217,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
869
1217
|
|
|
870
1218
|
def generate(self) -> str:
|
|
871
1219
|
"""Generate C++ source from the AnalyzerContext."""
|
|
1220
|
+
# Context-sensitive instance pre-pass (needs the naming helpers populated
|
|
1221
|
+
# in __init__). Computes nested stateful-helper dispatch + fresh instances.
|
|
1222
|
+
self._build_func_instances()
|
|
872
1223
|
# Pre-scan for strategy series vars
|
|
873
1224
|
self._prescan_strategy_series()
|
|
874
1225
|
self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
|
|
1226
|
+
# request.security TA call-sites read at a history offset (``ta.ema(...)[k>=1]``).
|
|
1227
|
+
# Maps sec_id -> set of TA call-site indices needing an HTF history Series.
|
|
1228
|
+
self._security_ta_hist_idx_by_sec: dict[int, set[int]] = {}
|
|
875
1229
|
|
|
876
1230
|
lines: list[str] = []
|
|
877
1231
|
|
|
@@ -897,6 +1251,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
897
1251
|
continue
|
|
898
1252
|
spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
|
|
899
1253
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
1254
|
+
# Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
|
|
1255
|
+
# and large bar indices); emit UDT int fields as ``int64_t`` so a
|
|
1256
|
+
# field initialised from ``time``/``current_bar_.timestamp`` does
|
|
1257
|
+
# not truncate / narrow-init.
|
|
1258
|
+
if cpp_type == "int":
|
|
1259
|
+
cpp_type = "int64_t"
|
|
900
1260
|
if f.default:
|
|
901
1261
|
default = self._visit_expr(f.default)
|
|
902
1262
|
else:
|
|
@@ -961,6 +1321,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
961
1321
|
lines.append(
|
|
962
1322
|
f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
963
1323
|
)
|
|
1324
|
+
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
1325
|
+
self._collect_security_ta_hist_indices(expr_node)
|
|
1326
|
+
)
|
|
1327
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1328
|
+
lines.append(f" Series<double> {name}{_mbb};")
|
|
964
1329
|
continue
|
|
965
1330
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
966
1331
|
hist_fields: set[str] = set()
|
|
@@ -982,6 +1347,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
982
1347
|
lines.append(
|
|
983
1348
|
f" Series<double> {self._security_ohlc_hist_series_cpp(sec_id, field)}{_mbb};"
|
|
984
1349
|
)
|
|
1350
|
+
self._security_ta_hist_idx_by_sec[sec_id] = (
|
|
1351
|
+
self._collect_security_ta_hist_indices(expr_node)
|
|
1352
|
+
)
|
|
1353
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1354
|
+
lines.append(f" Series<double> {name}{_mbb};")
|
|
985
1355
|
|
|
986
1356
|
if self._security_calls:
|
|
987
1357
|
lines.append(' std::unordered_map<std::string, Series<double>> _security_helper_series_;')
|
|
@@ -1031,8 +1401,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1031
1401
|
continue
|
|
1032
1402
|
seen_var_members.add(name)
|
|
1033
1403
|
safe = self._safe_name(name)
|
|
1034
|
-
# Detect array vars from init expression
|
|
1035
|
-
|
|
1404
|
+
# Detect array vars from init expression. Guard the substring
|
|
1405
|
+
# heuristic against a UDT constructor that merely WRAPS array.new /
|
|
1406
|
+
# array.from in its arguments — e.g.
|
|
1407
|
+
# ``var draw d = draw.new(array.new<line>(), array.new<line>())``
|
|
1408
|
+
# must declare as ``draw``, not ``std::vector<double>``. (Drawing
|
|
1409
|
+
# made this latent collision reachable.)
|
|
1410
|
+
_init_str_s = str(init_str)
|
|
1411
|
+
_is_udt_ctor_init = any(
|
|
1412
|
+
_init_str_s.startswith(f"{u}.new") for u in self._udt_defs
|
|
1413
|
+
)
|
|
1414
|
+
if (not _is_udt_ctor_init) and (
|
|
1415
|
+
"array.new" in _init_str_s or "array.from" in _init_str_s
|
|
1416
|
+
or name in self._array_vars
|
|
1417
|
+
):
|
|
1036
1418
|
self._array_vars.add(name)
|
|
1037
1419
|
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
|
|
1038
1420
|
continue
|
|
@@ -1063,6 +1445,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1063
1445
|
# decl as ``double`` and the later ``z = SDZone{...}`` would not
|
|
1064
1446
|
# compile (assigning SDZone to double).
|
|
1065
1447
|
init_s = str(init_str)
|
|
1448
|
+
# Drawing handle var member (L-N2): a ``var line x`` declares as the
|
|
1449
|
+
# C++ handle struct (Series<Line> when also history-referenced).
|
|
1450
|
+
# Drawing names are NOT in _udt_defs, so the udt branch below would
|
|
1451
|
+
# self-zero them to double; handle them first.
|
|
1452
|
+
_draw_cpp = DRAWING_TYPE_TO_CPP.get(self._udt_var_types.get(name))
|
|
1453
|
+
if _draw_cpp is not None:
|
|
1454
|
+
if name in self.ctx.series_vars:
|
|
1455
|
+
lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
|
|
1456
|
+
else:
|
|
1457
|
+
lines.append(f" {_draw_cpp} {safe};")
|
|
1458
|
+
continue
|
|
1066
1459
|
udt_type = self._udt_var_types.get(name)
|
|
1067
1460
|
if udt_type not in self._udt_defs:
|
|
1068
1461
|
udt_type = None
|
|
@@ -1131,7 +1524,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1131
1524
|
# data/validation/udt-method-probe-19-array-of-udt-method,
|
|
1132
1525
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1133
1526
|
udt_t = self._udt_var_types[name]
|
|
1134
|
-
|
|
1527
|
+
# Drawing handle global (L-N6 / U): map line/box/label/linefill
|
|
1528
|
+
# to the C++ handle struct (the default is na, id=-1).
|
|
1529
|
+
_draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
|
|
1530
|
+
if _draw_cpp is not None:
|
|
1531
|
+
lines.append(f" {_draw_cpp} {safe} = {_draw_cpp}{{}};")
|
|
1532
|
+
else:
|
|
1533
|
+
lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
|
|
1135
1534
|
else:
|
|
1136
1535
|
expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
|
|
1137
1536
|
cpp_type = self._infer_type(expr) if expr is not None else PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
@@ -1148,37 +1547,55 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1148
1547
|
if cloned_safe in emitted_clones:
|
|
1149
1548
|
continue # already declared by another function's clone
|
|
1150
1549
|
emitted_clones.add(cloned_safe)
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
|
|
1173
|
-
cpp_type = self._series_type_for(orig_safe)
|
|
1174
|
-
lines.append(f" Series<{cpp_type}> {cloned_safe}{_mbb};")
|
|
1175
|
-
else:
|
|
1176
|
-
lines.append(f" double {cloned_safe} = 0.0;")
|
|
1550
|
+
self._emit_cloned_var_decl(orig_safe, cloned_safe, _mbb, lines)
|
|
1551
|
+
|
|
1552
|
+
# 8c2. Fresh var members for context-sensitive helper instances (nested
|
|
1553
|
+
# helpers reached through >1 distinct call path). Each fresh instance
|
|
1554
|
+
# gets its OWN scalar/series state so two paths never collide.
|
|
1555
|
+
for orig_safe, fresh_safe in self._fresh_var_members:
|
|
1556
|
+
if fresh_safe in emitted_clones:
|
|
1557
|
+
continue
|
|
1558
|
+
emitted_clones.add(fresh_safe)
|
|
1559
|
+
self._emit_cloned_var_decl(orig_safe, fresh_safe, _mbb, lines)
|
|
1560
|
+
|
|
1561
|
+
# 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
|
|
1562
|
+
# non-drawing strategies emit byte-identical C++). Each arena is a
|
|
1563
|
+
# per-strategy member -> reset-per-run is automatic. Caps come from
|
|
1564
|
+
# the strategy() header max_*_count (default 50; linefill default 50).
|
|
1565
|
+
if self._uses_drawing:
|
|
1566
|
+
caps = self._drawing_caps or {}
|
|
1567
|
+
lines.append(f" DrawingArena<LineRec> _pf_lines_{{{caps.get('line', 50)}}};")
|
|
1568
|
+
lines.append(f" DrawingArena<BoxRec> _pf_boxes_{{{caps.get('box', 50)}}};")
|
|
1569
|
+
lines.append(f" DrawingArena<LabelRec> _pf_labels_{{{caps.get('label', 50)}}};")
|
|
1570
|
+
lines.append(f" DrawingArena<LinefillRec> _pf_linefills_{{{caps.get('linefill', 50)}}};")
|
|
1177
1571
|
|
|
1178
1572
|
# 9. _var_initialized flag
|
|
1179
1573
|
if self.ctx.var_members:
|
|
1180
1574
|
lines.append(" bool _var_initialized = false;")
|
|
1181
1575
|
|
|
1576
|
+
# 9a. Per-function-variant ``var`` init flags. A function-scoped
|
|
1577
|
+
# ``var`` (Pine "init once" semantics) is a function-local static:
|
|
1578
|
+
# its initializer runs on the FIRST call to that function variant
|
|
1579
|
+
# (with the first bar's values the function actually sees) and the
|
|
1580
|
+
# result persists for the strategy's lifetime. Each clone (cs0,
|
|
1581
|
+
# cs1, ...) is an independent instance with its own flag.
|
|
1582
|
+
# ``func_var_members`` is keyed by the plain Pine function name
|
|
1583
|
+
# (``fi.name``), so this matches both plain UDFs and UDT methods.
|
|
1584
|
+
for fi in self.ctx.func_infos:
|
|
1585
|
+
if fi.name not in self.ctx.func_var_members:
|
|
1586
|
+
continue
|
|
1587
|
+
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
1588
|
+
if total_cs > 0:
|
|
1589
|
+
for cs_idx in range(total_cs):
|
|
1590
|
+
lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)}_cs{cs_idx} = false;")
|
|
1591
|
+
else:
|
|
1592
|
+
lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")
|
|
1593
|
+
|
|
1594
|
+
# 9a2. ``var`` init flags for fresh context-sensitive helper instances.
|
|
1595
|
+
for inst in self._fresh_instances:
|
|
1596
|
+
if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
|
|
1597
|
+
lines.append(f" bool _fvinit_{inst['name']} = false;")
|
|
1598
|
+
|
|
1182
1599
|
# 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
|
|
1183
1600
|
if self.ctx.ta_call_sites:
|
|
1184
1601
|
lines.append(" bool _ta_initialized_ = false;")
|
|
@@ -1198,7 +1615,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1198
1615
|
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
1199
1616
|
has_ta = fi.name in self.ctx.func_ta_ranges
|
|
1200
1617
|
has_series = fi.name in self.ctx.func_series_vars or fi.name in self.ctx.func_var_members
|
|
1201
|
-
|
|
1618
|
+
# A function whose ONLY reason to need per-call-site cloning is a
|
|
1619
|
+
# security-tf-monomorphized request.security (no TA/series state
|
|
1620
|
+
# of its own — see Analyzer._check_mixed_callsite_security_tf)
|
|
1621
|
+
# still needs N separate emitted bodies so self._active_call_site_idx
|
|
1622
|
+
# is set while each is emitted (read by the request.security
|
|
1623
|
+
# use-site lowering in visit_call.py to pick the right clone's
|
|
1624
|
+
# sec_id).
|
|
1625
|
+
needs_security_clone = fi.name in self.ctx.func_security_clone_only
|
|
1626
|
+
if (has_ta or has_series or needs_security_clone) and total_cs > 0:
|
|
1202
1627
|
# Emit one variant per call site
|
|
1203
1628
|
for cs_idx in range(total_cs):
|
|
1204
1629
|
self._emit_func_def(fi, lines, call_site_idx=cs_idx)
|
|
@@ -1207,7 +1632,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1207
1632
|
self._emit_func_def(fi, lines)
|
|
1208
1633
|
lines.append("")
|
|
1209
1634
|
|
|
1635
|
+
# 10a. Fresh context-sensitive instances of nested stateful helpers
|
|
1636
|
+
# (reached through >1 distinct call path). Each is bound to its own
|
|
1637
|
+
# path-specific TA + var members; see _build_func_instances.
|
|
1638
|
+
if self._fresh_instances:
|
|
1639
|
+
fi_by_name = {fi.name: fi for fi in self.ctx.func_infos}
|
|
1640
|
+
for inst in self._fresh_instances:
|
|
1641
|
+
fi = fi_by_name.get(inst["fname"])
|
|
1642
|
+
if fi is None:
|
|
1643
|
+
continue
|
|
1644
|
+
self._emit_func_def(fi, lines, instance=inst)
|
|
1645
|
+
lines.append("")
|
|
1646
|
+
|
|
1210
1647
|
# 11. on_bar()
|
|
1648
|
+
self._current_instance_name = None
|
|
1211
1649
|
self._emit_on_bar(lines)
|
|
1212
1650
|
lines.append("")
|
|
1213
1651
|
|
|
@@ -1319,6 +1757,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1319
1757
|
def _is_skip_expr(self, node) -> bool:
|
|
1320
1758
|
"""Check if an expression should be skipped (visual/unsupported)."""
|
|
1321
1759
|
if isinstance(node, FuncCall):
|
|
1760
|
+
# chart.point.* resolves to namespace "chart" (a SKIP_NAMESPACE) but
|
|
1761
|
+
# is REAL data (a ChartPoint aggregate literal). Never skip it.
|
|
1762
|
+
if self._is_chart_point_callee(node.callee):
|
|
1763
|
+
return False
|
|
1322
1764
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
1323
1765
|
if func_name in SKIP_FUNC_NAMES:
|
|
1324
1766
|
return True
|
|
@@ -1326,6 +1768,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1326
1768
|
return True
|
|
1327
1769
|
if namespace in SKIP_VAR_TYPES:
|
|
1328
1770
|
return True
|
|
1771
|
+
# Method call on a table/polyline-typed receiver var/param
|
|
1772
|
+
# (``panel.cell(...)``). These types have no C++ representation, so
|
|
1773
|
+
# the call is a visual no-op — drop it (mirrors the namespace form).
|
|
1774
|
+
if namespace in self._visual_drop_vars:
|
|
1775
|
+
return True
|
|
1329
1776
|
# strategy.risk.* — handled in _visit_stmt, not skipped
|
|
1330
1777
|
if isinstance(node, MemberAccess):
|
|
1331
1778
|
if isinstance(node.object, Identifier) and node.object.name in SKIP_NAMESPACES:
|
|
@@ -1373,6 +1820,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1373
1820
|
"""
|
|
1374
1821
|
import re
|
|
1375
1822
|
ident_re = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
|
|
1823
|
+
|
|
1824
|
+
# Expand class-scope derived vars (``wilderLen`` -> ``(rsiLen * 2 - 1)``)
|
|
1825
|
+
# to their raw RHS so the input-backed leaves become get_input_*() reads
|
|
1826
|
+
# below. Recursive (bounded) to handle chains of derived vars; guards
|
|
1827
|
+
# against cycles.
|
|
1828
|
+
def _expand_derived(s: str, seen: frozenset = frozenset(), depth: int = 0) -> str:
|
|
1829
|
+
if depth > 32:
|
|
1830
|
+
return s
|
|
1831
|
+
def _rep(m: re.Match) -> str:
|
|
1832
|
+
nm = m.group(0)
|
|
1833
|
+
if nm in self._derived_input_expr and nm not in seen:
|
|
1834
|
+
inner = self._derived_input_expr[nm]
|
|
1835
|
+
return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
|
|
1836
|
+
return nm
|
|
1837
|
+
return ident_re.sub(_rep, s)
|
|
1838
|
+
|
|
1839
|
+
arg_str = _expand_derived(arg_str)
|
|
1840
|
+
|
|
1376
1841
|
tokens = ident_re.findall(arg_str)
|
|
1377
1842
|
input_tokens = [t for t in tokens if t in self._input_backed_vars]
|
|
1378
1843
|
if not input_tokens:
|