@pineforge/codegen-pyodide 0.9.0 → 0.10.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/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2359 -249
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +732 -172
- package/pineforge_codegen/codegen/security.py +1456 -150
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +224 -77
- package/pineforge_codegen/codegen/types.py +1196 -215
- package/pineforge_codegen/codegen/visit_call.py +1168 -99
- package/pineforge_codegen/codegen/visit_expr.py +354 -33
- package/pineforge_codegen/codegen/visit_stmt.py +1144 -200
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +194 -19
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
|
@@ -6,6 +6,8 @@ results from AnalyzerContext instead of walking the AST to collect info.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
9
11
|
from ..ast_nodes import (
|
|
10
12
|
ASTNode, Program, StrategyDecl, VarDecl, Assignment, IfStmt, ForStmt, ForInStmt,
|
|
11
13
|
WhileStmt, SwitchStmt, BreakStmt, ContinueStmt, FuncDef, ExprStmt,
|
|
@@ -23,10 +25,19 @@ from ..analyzer import (
|
|
|
23
25
|
TA_NO_CTOR,
|
|
24
26
|
TA_PERIOD_ARG,
|
|
25
27
|
)
|
|
26
|
-
from ..symbols import PineType, TypeSpec
|
|
28
|
+
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
27
29
|
from .. import signatures as sigs
|
|
28
30
|
from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
29
31
|
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class _StableVarCtorLiteral:
|
|
35
|
+
"""Exact declaration identity admitted by the TA-length literal route."""
|
|
36
|
+
|
|
37
|
+
value: int
|
|
38
|
+
declaration_id: int
|
|
39
|
+
top_level_index: int
|
|
40
|
+
|
|
30
41
|
# ---------------------------------------------------------------------------
|
|
31
42
|
# Mapping tables — definitions live in ``tables.py``; re-imported here so
|
|
32
43
|
# inline references inside this module (BAR_FIELDS[name], MATH_FUNC_MAP[fn],
|
|
@@ -63,6 +74,7 @@ from .tables import (
|
|
|
63
74
|
MATRIX_SORT_ALLOWED_GENERIC_ELEMS,
|
|
64
75
|
MATH_FUNC_MAP,
|
|
65
76
|
STR_FUNC_MAP,
|
|
77
|
+
_MATH_SIGN_CPP_FN,
|
|
66
78
|
_merge_kwargs,
|
|
67
79
|
)
|
|
68
80
|
|
|
@@ -95,7 +107,7 @@ from .helpers import CPP_RESERVED, NamingHelper
|
|
|
95
107
|
from .types import TypeInferer
|
|
96
108
|
|
|
97
109
|
# TaSiteHelper owns site lookup, .compute() arg construction, and the TA
|
|
98
|
-
#
|
|
110
|
+
# call-site machinery. The runtime-reset chain (_resolve_known and friends)
|
|
99
111
|
# stays on CodeGen for now because it relies on Python's compile-time
|
|
100
112
|
# expression evaluator.
|
|
101
113
|
from .ta import TaSiteHelper
|
|
@@ -154,7 +166,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
154
166
|
per-function emitters used by Pine functions and UDT methods
|
|
155
167
|
* ``SecurityEmitter`` -- ``request.security()`` lowering pipeline
|
|
156
168
|
(evaluators, dispatch, rebind, TA variants)
|
|
157
|
-
* ``TaSiteHelper`` -- TA call-site lookup + .compute() arg construction
|
|
169
|
+
* ``TaSiteHelper`` -- TA call-site lookup + .compute() arg construction
|
|
158
170
|
* ``TypeInferer`` -- _type_spec_*, _infer_type, _array/_map_method_expr
|
|
159
171
|
* ``InputHelper`` -- Pine ``input.*`` defaults / titles / getter dispatch
|
|
160
172
|
* ``NamingHelper`` -- _safe_name / _resolve_callee / _walk_ast / ...
|
|
@@ -172,6 +184,104 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
172
184
|
|
|
173
185
|
def __init__(self, ctx: AnalyzerContext) -> None:
|
|
174
186
|
self.ctx = ctx
|
|
187
|
+
# Lexical Pine names remain in ``ctx.func_var_members``. This overlay
|
|
188
|
+
# carries exact class-member identities only for collision-qualified
|
|
189
|
+
# ordinary FuncDefs (identity mappings for every other ordinary UDF).
|
|
190
|
+
self._func_var_storage_names: dict[str, dict[str, str]] = {
|
|
191
|
+
owner: dict(names)
|
|
192
|
+
for owner, names in getattr(
|
|
193
|
+
ctx, "func_var_storage_names", {}
|
|
194
|
+
).items()
|
|
195
|
+
}
|
|
196
|
+
self._qualified_func_var_raw_names: set[str] = {
|
|
197
|
+
raw_name
|
|
198
|
+
for storage_names in self._func_var_storage_names.values()
|
|
199
|
+
for raw_name, storage_name in storage_names.items()
|
|
200
|
+
if storage_name != raw_name
|
|
201
|
+
}
|
|
202
|
+
self._ordinary_global_binding_names: set[str] = set(
|
|
203
|
+
getattr(ctx, "ordinary_global_binding_names", set())
|
|
204
|
+
)
|
|
205
|
+
self._ordinary_global_series_names: set[str] = set(
|
|
206
|
+
getattr(ctx, "ordinary_global_series_names", set())
|
|
207
|
+
)
|
|
208
|
+
self._nonpersistent_series_decl_names: set[str] = set(
|
|
209
|
+
getattr(ctx, "nonpersistent_series_decl_names", set())
|
|
210
|
+
)
|
|
211
|
+
self._func_nonpersistent_series_vars: dict[str, set[str]] = {
|
|
212
|
+
owner: set(names)
|
|
213
|
+
for owner, names in getattr(
|
|
214
|
+
ctx, "func_nonpersistent_series_vars", {}
|
|
215
|
+
).items()
|
|
216
|
+
}
|
|
217
|
+
# Exact declarations that are direct children of Program.body. A
|
|
218
|
+
# same-named declaration inside a top-level ``if``/loop is still a
|
|
219
|
+
# lexical block local; raw membership in ``_global_member_vars`` must
|
|
220
|
+
# not route that nested declaration into the hoisted class member.
|
|
221
|
+
self._ordinary_global_var_decl_nodes: set[int] = {
|
|
222
|
+
id(stmt)
|
|
223
|
+
for stmt in ctx.ast.body
|
|
224
|
+
if isinstance(stmt, VarDecl)
|
|
225
|
+
and not stmt.is_var
|
|
226
|
+
and not stmt.is_varip
|
|
227
|
+
}
|
|
228
|
+
self._direct_program_var_decl_nodes: set[int] = {
|
|
229
|
+
id(stmt)
|
|
230
|
+
for stmt in ctx.ast.body
|
|
231
|
+
if isinstance(stmt, VarDecl)
|
|
232
|
+
}
|
|
233
|
+
self._direct_program_tuple_decl_nodes: set[int] = {
|
|
234
|
+
id(stmt)
|
|
235
|
+
for stmt in ctx.ast.body
|
|
236
|
+
if isinstance(stmt, TupleAssign)
|
|
237
|
+
}
|
|
238
|
+
self._direct_program_tuple_binding_names: set[str] = {
|
|
239
|
+
name
|
|
240
|
+
for stmt in ctx.ast.body
|
|
241
|
+
if isinstance(stmt, TupleAssign)
|
|
242
|
+
for name in stmt.names
|
|
243
|
+
if name != "_"
|
|
244
|
+
}
|
|
245
|
+
self._direct_program_binding_names: set[str] = {
|
|
246
|
+
stmt.name
|
|
247
|
+
for stmt in ctx.ast.body
|
|
248
|
+
if isinstance(stmt, VarDecl)
|
|
249
|
+
}
|
|
250
|
+
self._direct_program_binding_names.update(
|
|
251
|
+
name
|
|
252
|
+
for stmt in ctx.ast.body
|
|
253
|
+
if isinstance(stmt, TupleAssign)
|
|
254
|
+
for name in stmt.names
|
|
255
|
+
if name != "_"
|
|
256
|
+
)
|
|
257
|
+
self._callable_state_raw_names: set[str] = {
|
|
258
|
+
name
|
|
259
|
+
for members in ctx.func_var_members.values()
|
|
260
|
+
for name, _ptype, _init in members
|
|
261
|
+
}
|
|
262
|
+
self._callable_state_raw_names.update(
|
|
263
|
+
name
|
|
264
|
+
for names in ctx.func_series_vars.values()
|
|
265
|
+
for name in names
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def func_var_storage(owner: str, raw_name: str) -> str:
|
|
269
|
+
return self._func_var_storage_names.get(owner, {}).get(
|
|
270
|
+
raw_name, raw_name
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
self._func_var_storage_name = func_var_storage
|
|
274
|
+
# Security metadata collection can render runtime timeframe
|
|
275
|
+
# expressions before the rest of the constructor state is prepared.
|
|
276
|
+
# Exact persistent-member identity is immutable analyzer output, so
|
|
277
|
+
# initialize it first for every expression visitor.
|
|
278
|
+
self._persistent_var_member_names: set[str] = {
|
|
279
|
+
self._safe_name(name) for name, _, _ in ctx.var_members
|
|
280
|
+
}
|
|
281
|
+
self._series_var_member_names: set[str] = {
|
|
282
|
+
self._safe_name(name)
|
|
283
|
+
for name in getattr(ctx, "series_var_members", set())
|
|
284
|
+
}
|
|
175
285
|
# Build lookup: node id -> TACallSite (only for non-function-local sites)
|
|
176
286
|
self._ta_site_map: dict[int, TACallSite] = {}
|
|
177
287
|
# Build per-call-site TA member name remapping for user functions
|
|
@@ -179,8 +289,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
179
289
|
self._func_cs_ta_remap: dict[tuple[str, int], dict[str, str]] = {}
|
|
180
290
|
# Active TA name remap (set during per-call-site function emission)
|
|
181
291
|
self._active_ta_remap: dict[str, str] = {}
|
|
182
|
-
# Flag: inside a per-call-site function variant (enables TA hoisting)
|
|
183
|
-
self._in_ta_func_variant: bool = False
|
|
184
292
|
# Active call-site index (set during per-call-site function emission)
|
|
185
293
|
self._active_call_site_idx: int | None = None
|
|
186
294
|
# Set of TA member names that belong to user functions
|
|
@@ -200,32 +308,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
200
308
|
self._reset_input_getter_mode: bool = False
|
|
201
309
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
202
310
|
self._func_var_members_set: set[str] = set()
|
|
203
|
-
# BUG C: function-local names emitted as ``UDT*`` pointer aliases (a UDT
|
|
204
|
-
# local initialised from a var/global UDT lvalue, mutated through, AND
|
|
205
|
-
# later rebound to a different lvalue). Member access lowers to ``->``
|
|
206
|
-
# and rebinds to ``&(...)``. Reset per function in _emit_func_def is not
|
|
207
|
-
# needed: names are function-unique and the value-copy fallback ignores
|
|
208
|
-
# entries for inactive functions.
|
|
209
|
-
self._udt_ptr_alias_locals: set[str] = set()
|
|
210
|
-
# Names of hoisted GLOBAL-scope UDT loop-locals bound from a UDT array
|
|
211
|
-
# element (``z = arr.get(i)``) and later field-mutated. Pine array
|
|
212
|
-
# elements of a user-defined type are references, so such a local must
|
|
213
|
-
# ALIAS the element, not value-copy — the mutation has to write back
|
|
214
|
-
# into the array. These are de-hoisted from the class-member value-copy
|
|
215
|
-
# to a fresh per-iteration ``UDT& z = arr[i];`` local reference (the same
|
|
216
|
-
# form the non-hoisted function-local alias path already emits). Read-only
|
|
217
|
-
# get-locals are NOT recorded (no field mutation) and keep value-copy
|
|
218
|
-
# semantics. Populated by _register_udt_array_get_ref_locals.
|
|
219
|
-
self._udt_array_get_ref_locals: set[str] = set()
|
|
220
311
|
self._precalc_loop_active: bool = False
|
|
221
|
-
# Names of ``var`` members that live in a
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
# preamble. See ``_emit_func_var_init_block``.
|
|
312
|
+
# Names of ``var`` members that live in a callable scope (not global).
|
|
313
|
+
# Their exact declaration statements own initialization; they must not
|
|
314
|
+
# be initialized by the constructor or the global on_bar preamble.
|
|
225
315
|
self._func_local_var_names: set[str] = set()
|
|
226
|
-
for _vlist in ctx.func_var_members.
|
|
316
|
+
for _owner, _vlist in ctx.func_var_members.items():
|
|
227
317
|
for _n, _, _ in _vlist:
|
|
228
|
-
self._func_local_var_names.add(
|
|
318
|
+
self._func_local_var_names.add(
|
|
319
|
+
self._func_var_storage_name(_owner, _n)
|
|
320
|
+
)
|
|
229
321
|
|
|
230
322
|
# Build per-function var/series name lists for cloning.
|
|
231
323
|
# For each function with call-site variants, collect ALL function-scoped
|
|
@@ -241,17 +333,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
241
333
|
# are themselves sets (analyzer stores ``dict[str, set]``), so we must
|
|
242
334
|
# iterate each value in ``sorted`` order to be hash-seed independent.
|
|
243
335
|
all_func_scoped_series: list[str] = []
|
|
244
|
-
for svars in ctx.func_series_vars.
|
|
336
|
+
for owner, svars in ctx.func_series_vars.items():
|
|
245
337
|
for sv in sorted(svars):
|
|
246
|
-
|
|
247
|
-
|
|
338
|
+
exact = self._func_var_storage_name(owner, sv)
|
|
339
|
+
# ``func_series_vars`` also carries history-referenced
|
|
340
|
+
# parameters/plain locals. Translate only a real persistent
|
|
341
|
+
# member whose analyzer-exact identity is Series storage.
|
|
342
|
+
if (self._safe_name(exact)
|
|
343
|
+
in self._series_var_member_names):
|
|
344
|
+
storage = exact
|
|
345
|
+
elif (sv in self._qualified_func_var_raw_names
|
|
346
|
+
and sv not in self._func_nonpersistent_series_vars.get(
|
|
347
|
+
owner, set()
|
|
348
|
+
)):
|
|
349
|
+
# A history parameter/plain local in an unrelated UDF may
|
|
350
|
+
# reuse the Pine spelling of owner-qualified persistent
|
|
351
|
+
# state. It has no class-member identity to propagate into
|
|
352
|
+
# every other function's clone map; doing so remaps an
|
|
353
|
+
# initializer's earlier global read to a bogus ``x_csN``
|
|
354
|
+
# Series member before the persistent local is active.
|
|
355
|
+
continue
|
|
356
|
+
else:
|
|
357
|
+
storage = sv
|
|
358
|
+
if storage not in all_func_scoped_series:
|
|
359
|
+
all_func_scoped_series.append(storage)
|
|
248
360
|
# Also include function-scoped var_members (same ordered-list rationale).
|
|
249
361
|
# ``ctx.func_var_members`` values are lists (already insertion-ordered).
|
|
250
362
|
all_func_scoped_vars: list[str] = []
|
|
251
|
-
for vlist in ctx.func_var_members.
|
|
363
|
+
for owner, vlist in ctx.func_var_members.items():
|
|
252
364
|
for n, _, _ in vlist:
|
|
253
|
-
|
|
254
|
-
|
|
365
|
+
exact = self._func_var_storage_name(owner, n)
|
|
366
|
+
if exact not in all_func_scoped_vars:
|
|
367
|
+
all_func_scoped_vars.append(exact)
|
|
255
368
|
|
|
256
369
|
# For each function with call-site cloning (has TA ranges or is called multiple times),
|
|
257
370
|
# include ALL function-scoped series/var vars that could be used in its body.
|
|
@@ -265,13 +378,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
265
378
|
# Include function's own vars
|
|
266
379
|
if fname in ctx.func_var_members:
|
|
267
380
|
for n, _, _ in ctx.func_var_members[fname]:
|
|
268
|
-
|
|
269
|
-
|
|
381
|
+
exact = self._func_var_storage_name(fname, n)
|
|
382
|
+
if exact not in orig_names:
|
|
383
|
+
orig_names.append(exact)
|
|
270
384
|
# Include function's own series vars (set -> sorted for determinism)
|
|
271
385
|
if fname in ctx.func_series_vars:
|
|
272
386
|
for sv in sorted(ctx.func_series_vars[fname]):
|
|
273
|
-
|
|
274
|
-
|
|
387
|
+
exact = self._func_var_storage_name(fname, sv)
|
|
388
|
+
storage = (
|
|
389
|
+
exact
|
|
390
|
+
if self._safe_name(exact)
|
|
391
|
+
in self._series_var_member_names
|
|
392
|
+
else sv
|
|
393
|
+
)
|
|
394
|
+
if storage not in orig_names:
|
|
395
|
+
orig_names.append(storage)
|
|
275
396
|
# Include series vars from sub-functions (they share the same class members)
|
|
276
397
|
for sv in all_func_scoped_series:
|
|
277
398
|
if sv not in orig_names:
|
|
@@ -300,7 +421,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
300
421
|
# Build TA site map and per-call-site remapping
|
|
301
422
|
func_ta_originals: dict[str, list[str]] = {} # func_name -> list of original member names
|
|
302
423
|
for fname, (start, end) in ctx.func_ta_ranges.items():
|
|
303
|
-
|
|
424
|
+
indices = (ctx.func_ta_indices or {}).get(fname)
|
|
425
|
+
if indices is None:
|
|
426
|
+
indices = range(start, end)
|
|
427
|
+
orig_names = [ctx.ta_call_sites[i].member_name for i in indices]
|
|
304
428
|
func_ta_originals[fname] = orig_names
|
|
305
429
|
self._func_ta_members.update(orig_names)
|
|
306
430
|
# cs0 uses originals (identity mapping)
|
|
@@ -354,7 +478,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
354
478
|
self._current_instance_name: str | None = None
|
|
355
479
|
self._instance_dispatch: dict[tuple[str | None, int], str] = {}
|
|
356
480
|
self._fresh_instances: list[dict] = []
|
|
357
|
-
self._fresh_var_members: list[tuple[str, str]] = []
|
|
481
|
+
self._fresh_var_members: list[tuple[str, str, str]] = []
|
|
358
482
|
# Fresh fixnan members for context-sensitive helper instances (nested
|
|
359
483
|
# helpers reached through >1 distinct call path). Each fresh instance
|
|
360
484
|
# gets its OWN previous-value member so two paths never share fixnan
|
|
@@ -381,6 +505,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
381
505
|
self._security_inline_counter = 0
|
|
382
506
|
self._random_call_counter = 0
|
|
383
507
|
self._for_counter = 0
|
|
508
|
+
self._tuple_assign_counter = 0
|
|
384
509
|
# Synthetic history buffers used by inline call-history and by scalar
|
|
385
510
|
# expressions passed to UDF series parameters. They are pre-registered
|
|
386
511
|
# at generate() time so declarations precede method emission, then
|
|
@@ -408,6 +533,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
408
533
|
self._current_input_var_name: str | None = None
|
|
409
534
|
# Build known_vars for constant propagation
|
|
410
535
|
self._known_vars: dict[str, int | float | bool | str] = {}
|
|
536
|
+
# Source-ordered lexical declarations can shadow a same-named direct
|
|
537
|
+
# compile-time global. Constant folding is raw-name keyed, so every
|
|
538
|
+
# fold path must consult this COW tombstone set before substituting a
|
|
539
|
+
# global literal inside the nested lexical scope.
|
|
540
|
+
self._lexical_known_var_tombstones: set[str] = set()
|
|
411
541
|
# Subset of _known_vars whose value came from an input.*() call. These
|
|
412
542
|
# MUST NOT be inlined at identifier use sites because strategy_set_input()
|
|
413
543
|
# can override them at runtime. Ctor-time uses (TA buffer sizing,
|
|
@@ -433,6 +563,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
433
563
|
# state are NOT here, so a TA length fed by them is still rejected
|
|
434
564
|
# by the constructor guard.
|
|
435
565
|
self._stable_runtime_vars: set[str] = set()
|
|
566
|
+
# Narrow subset of persistent scalars that a TA constructor may fold
|
|
567
|
+
# directly: one unique top-level ``var [int] name = <int literal>``
|
|
568
|
+
# binding, never reassigned and never promoted to Series storage.
|
|
569
|
+
# Keep this separate from ``_known_vars`` so ordinary identifier use
|
|
570
|
+
# still reads the persistent member and only TA buffer sizing changes.
|
|
571
|
+
self._stable_var_ctor_literals: dict[str, _StableVarCtorLiteral] = {}
|
|
436
572
|
# ``_var_names`` (var/varip persistent-state members) is needed by the
|
|
437
573
|
# stability classifier during _collect_known_vars, so pre-seed it from
|
|
438
574
|
# the analyzer's var_members before that pass runs; the canonical
|
|
@@ -440,11 +576,30 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
440
576
|
self._var_names: set[str] = set()
|
|
441
577
|
for _vn, _, _ in ctx.var_members:
|
|
442
578
|
self._var_names.add(_vn)
|
|
579
|
+
ordinary_global_member_names = self._ordinary_global_binding_names
|
|
580
|
+
for _vlist in ctx.func_var_members.values():
|
|
581
|
+
self._var_names.update(
|
|
582
|
+
_n
|
|
583
|
+
for _n, _, _ in _vlist
|
|
584
|
+
if _n not in ordinary_global_member_names
|
|
585
|
+
)
|
|
443
586
|
self._collect_known_vars()
|
|
444
587
|
# Track var names
|
|
445
588
|
self._var_names = set()
|
|
446
589
|
for name, _, _ in ctx.var_members:
|
|
447
590
|
self._var_names.add(name)
|
|
591
|
+
# Preserve the established raw lexical allow-list even when the class
|
|
592
|
+
# member itself is owner-qualified. Declaration-site remapping resolves
|
|
593
|
+
# legal reads/writes to exact storage; this union also prevents the
|
|
594
|
+
# legacy non-var-Series loop from emitting a second raw class member.
|
|
595
|
+
# A real ordinary global with that spelling is deliberately excluded:
|
|
596
|
+
# it owns separate class state and must still be emitted.
|
|
597
|
+
for _vlist in ctx.func_var_members.values():
|
|
598
|
+
self._var_names.update(
|
|
599
|
+
_n
|
|
600
|
+
for _n, _, _ in _vlist
|
|
601
|
+
if _n not in ordinary_global_member_names
|
|
602
|
+
)
|
|
448
603
|
# Every name bound ANYWHERE in the program (top-level, nested in
|
|
449
604
|
# if/for/while/switch blocks, or inside function bodies). The
|
|
450
605
|
# unknown-identifier guard in _visit_ident uses this as a generous
|
|
@@ -548,7 +703,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
548
703
|
self._global_member_vars.add(name)
|
|
549
704
|
self._global_mutable_infos: dict[str, object] = getattr(ctx, "global_mutable_infos", {}) or {}
|
|
550
705
|
self._udt_var_types: dict[str, str] = getattr(ctx, "udt_var_types", {}) or {}
|
|
551
|
-
self.
|
|
706
|
+
self._global_collection_types: dict[str, TypeSpec] = dict(
|
|
707
|
+
getattr(ctx, "collection_types", {}) or {}
|
|
708
|
+
)
|
|
709
|
+
self._collection_types: dict[str, TypeSpec] = dict(
|
|
710
|
+
self._global_collection_types
|
|
711
|
+
)
|
|
712
|
+
self._func_collection_types: dict[str, dict[str, TypeSpec]] = {
|
|
713
|
+
name: dict(specs)
|
|
714
|
+
for name, specs in (
|
|
715
|
+
getattr(ctx, "func_collection_types", {}) or {}
|
|
716
|
+
).items()
|
|
717
|
+
}
|
|
718
|
+
self._block_collection_types: dict[int, dict[str, TypeSpec | None]] = {
|
|
719
|
+
owner_id: dict(specs)
|
|
720
|
+
for owner_id, specs in (
|
|
721
|
+
getattr(ctx, "block_collection_types", {}) or {}
|
|
722
|
+
).items()
|
|
723
|
+
}
|
|
724
|
+
self._block_collection_owners: dict[int, str] = dict(
|
|
725
|
+
getattr(ctx, "block_collection_owners", {}) or {}
|
|
726
|
+
)
|
|
727
|
+
self._callable_collection_bindings: dict[int, TypeSpec | None] = dict(
|
|
728
|
+
getattr(ctx, "callable_collection_bindings", {}) or {}
|
|
729
|
+
)
|
|
730
|
+
self._callable_collection_binding_owners: dict[int, str] = dict(
|
|
731
|
+
getattr(ctx, "callable_collection_binding_owners", {}) or {}
|
|
732
|
+
)
|
|
552
733
|
# id(block_node) -> {raw_var_name: unique_member} for block-scoped var
|
|
553
734
|
# name collisions (see Analyzer._visit_VarDecl). Activated into
|
|
554
735
|
# ``_active_var_remap`` while emitting the owning block's statements.
|
|
@@ -579,11 +760,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
579
760
|
# Current function parameter types (set during _emit_func_def)
|
|
580
761
|
self._current_func_param_types: dict[str, str] = {}
|
|
581
762
|
self._current_func_param_specs: dict[str, "TypeSpec"] = {}
|
|
763
|
+
# Copy-on-write lexical collection inventory for the function/method
|
|
764
|
+
# currently being emitted. Activated by _emit_func_def and restored
|
|
765
|
+
# together with the legacy raw-name registries at function exit.
|
|
766
|
+
self._current_func_collection_specs: dict[str, "TypeSpec"] = {}
|
|
767
|
+
# Direct callable-body declarations that shadow a top-level collection.
|
|
768
|
+
# Nested declarations are activated only by their block COW overlay.
|
|
769
|
+
self._current_func_collection_shadows: set[str] = set()
|
|
770
|
+
# While emitting ``name = RHS`` for a declaration that shadows an
|
|
771
|
+
# already-active same-named collection, RHS identifiers are redirected
|
|
772
|
+
# through a pre-declaration alias. C++ brings the new name into scope
|
|
773
|
+
# inside its own initializer, so raw ``name`` would otherwise self-bind.
|
|
774
|
+
self._pending_decl_outer_alias: dict[str, str] = {}
|
|
775
|
+
self._collection_shadow_tmp_counter = 0
|
|
776
|
+
self._collection_shadow_tmp_names: set[str] = set()
|
|
582
777
|
# Current function params that are series (const Series<double>&)
|
|
583
778
|
self._current_func_series_params: set[str] = set()
|
|
779
|
+
# Element types parallel to ``_current_func_series_params``. Keeping
|
|
780
|
+
# these separate from the full C++ reference spelling lets expression
|
|
781
|
+
# inference see ``src`` as its Pine scalar family inside the body.
|
|
782
|
+
self._current_func_series_param_types: dict[str, str] = {}
|
|
584
783
|
# Locals declared in the function currently being emitted (symbol table loses them after analysis)
|
|
585
784
|
self._current_func_locals: set[str] = set()
|
|
586
785
|
self._current_func_local_types: dict[str, str] = {}
|
|
786
|
+
# Source-ordered drawing-handle bindings in the currently emitted
|
|
787
|
+
# lexical block. Values are exact C++ handle types; ``None`` is a
|
|
788
|
+
# scalar/non-drawing tombstone that prevents an outer same-named handle
|
|
789
|
+
# from leaking inward. Block push/pop provides sibling isolation.
|
|
790
|
+
self._lexical_drawing_types: dict[str, str | None] = {}
|
|
791
|
+
# Source-ordered arbitrary UDT bindings. Drawing handles have their
|
|
792
|
+
# specialized registry above, while this parallel map preserves the
|
|
793
|
+
# exact authored UDT name needed to target-type a later bare ``na``
|
|
794
|
+
# reassignment. ``None`` is an ordinary scalar/collection tombstone;
|
|
795
|
+
# block push/pop keeps sibling declarations independent.
|
|
796
|
+
self._lexical_udt_types: dict[str, str | None] = {}
|
|
797
|
+
# Source-ordered lexical Series status. A False tombstone prevents a
|
|
798
|
+
# scalar local from inheriting a same-spelled global entry from the
|
|
799
|
+
# legacy raw-name ``ctx.series_vars`` union.
|
|
800
|
+
self._lexical_series_bindings: dict[str, bool] = {}
|
|
587
801
|
# for-in loop iterator names (must resolve member access, not enum fallback)
|
|
588
802
|
self._current_loop_vars: set[str] = set()
|
|
589
803
|
self._current_loop_var_specs: dict[str, "TypeSpec"] = {}
|
|
@@ -708,9 +922,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
708
922
|
self._all_member_names.add(self._safe_name(name))
|
|
709
923
|
for name, _, _ in ctx.var_members:
|
|
710
924
|
self._all_member_names.add(self._safe_name(name))
|
|
711
|
-
|
|
712
925
|
self._register_global_aggregate_member_types()
|
|
713
|
-
self.
|
|
926
|
+
self._uses_map = self._detect_map_usage()
|
|
714
927
|
self._uses_matrix = self._detect_matrix_usage()
|
|
715
928
|
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
716
929
|
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
@@ -742,6 +955,328 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
742
955
|
# history reads off security-helper series.
|
|
743
956
|
self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()
|
|
744
957
|
|
|
958
|
+
# Declaration-site ``var`` metadata is prepared in ``generate()``
|
|
959
|
+
# after context-sensitive function instances have been built. Once
|
|
960
|
+
# flags share the class-member namespace with those fresh members, so
|
|
961
|
+
# allocating the flags here would not yet have the complete collision
|
|
962
|
+
# inventory.
|
|
963
|
+
|
|
964
|
+
def _scalar_var_init_depends_on_runtime_input(self, init_ast) -> bool:
|
|
965
|
+
"""Whether a nominally constant initializer depends on input state.
|
|
966
|
+
|
|
967
|
+
``_resolve_known`` intentionally folds input defaults for constructor
|
|
968
|
+
sizing and other compile-time decisions. A Pine ``var`` initializer,
|
|
969
|
+
however, must observe any host override installed before the first
|
|
970
|
+
bar. Treat direct input aliases and arithmetic aliases derived from
|
|
971
|
+
them as declaration-time expressions even when their default happens
|
|
972
|
+
to fold to a C++ literal.
|
|
973
|
+
"""
|
|
974
|
+
if init_ast is None:
|
|
975
|
+
return False
|
|
976
|
+
runtime_names = set(self._input_backed_vars) | set(self._derived_input_expr)
|
|
977
|
+
for node in self._walk_ast(init_ast):
|
|
978
|
+
if isinstance(node, FuncCall) and self._is_input_call(node):
|
|
979
|
+
return True
|
|
980
|
+
if isinstance(node, Identifier) and node.name in runtime_names:
|
|
981
|
+
return True
|
|
982
|
+
return False
|
|
983
|
+
|
|
984
|
+
def _is_runtime_scalar_var_initializer(
|
|
985
|
+
self, name: str, ptype, init_str: str, init_ast,
|
|
986
|
+
drawing_cpp: str | None = None,
|
|
987
|
+
is_series: bool = False) -> bool:
|
|
988
|
+
"""Return True for a persistent primitive that must init in execution.
|
|
989
|
+
|
|
990
|
+
Global Series and aggregate state keep their specialized preamble
|
|
991
|
+
routes. Callable vars are selected separately by the exact-declaration
|
|
992
|
+
rule below. This predicate identifies the remaining primitive global /
|
|
993
|
+
on-bar-scope members that need declaration-site once guards.
|
|
994
|
+
"""
|
|
995
|
+
if init_ast is None:
|
|
996
|
+
return False
|
|
997
|
+
if name in self._visual_drop_vars:
|
|
998
|
+
return False
|
|
999
|
+
if name in self._array_vars or name in self._map_vars \
|
|
1000
|
+
or name in self._matrix_specs:
|
|
1001
|
+
return False
|
|
1002
|
+
if drawing_cpp is not None:
|
|
1003
|
+
# A nontrivial drawing-handle initializer may depend on preceding
|
|
1004
|
+
# source-order state. Default construction already represents a
|
|
1005
|
+
# bare ``na``, so only real expressions need a declaration-site
|
|
1006
|
+
# once guard.
|
|
1007
|
+
return (
|
|
1008
|
+
is_series
|
|
1009
|
+
or not self._is_na_expr(init_ast)
|
|
1010
|
+
)
|
|
1011
|
+
if is_series and name in self.ctx.series_vars:
|
|
1012
|
+
return False
|
|
1013
|
+
udt_type = self._member_udt_type(name)
|
|
1014
|
+
if udt_type in self._udt_defs:
|
|
1015
|
+
return False
|
|
1016
|
+
type_spec = self._collection_types.get(name)
|
|
1017
|
+
if type_spec is not None and type_spec.kind in {
|
|
1018
|
+
"array", "map", "matrix", "udt",
|
|
1019
|
+
}:
|
|
1020
|
+
return False
|
|
1021
|
+
|
|
1022
|
+
ctor_val = self._resolve_known(init_str)
|
|
1023
|
+
ctor_val = self._typed_na_init(ctor_val, name, ptype)
|
|
1024
|
+
return (
|
|
1025
|
+
not self._is_compile_time_value(ctor_val)
|
|
1026
|
+
or self._scalar_var_init_depends_on_runtime_input(init_ast)
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
def _prepare_runtime_scalar_var_initializers(self) -> None:
|
|
1030
|
+
"""Index declaration-site scalar ``var`` initialization and flags.
|
|
1031
|
+
|
|
1032
|
+
The analyzer supplies exact metadata for every VarDecl, including
|
|
1033
|
+
sibling-block disambiguation and callable ownership. Its insertion
|
|
1034
|
+
order follows source analysis and reaches declarations nested inside
|
|
1035
|
+
if/switch expressions, while the ownership bit excludes function and
|
|
1036
|
+
method bodies. This lets emission preserve ordinary dependency order
|
|
1037
|
+
and Pine's lazy first-entry semantics for conditional declarations.
|
|
1038
|
+
"""
|
|
1039
|
+
self._runtime_scalar_var_init_by_node: dict[int, dict] = {}
|
|
1040
|
+
self._runtime_scalar_var_init_by_member: dict[str, dict] = {}
|
|
1041
|
+
self._runtime_scalar_var_init_members: set[str] = set()
|
|
1042
|
+
self._drawing_var_member_cpp_types: dict[str, str] = {}
|
|
1043
|
+
self._drawing_var_decl_info_by_node: dict[int, dict] = {}
|
|
1044
|
+
self._global_drawing_cpp_types: dict[str, str] = {}
|
|
1045
|
+
# Exact direct-program UDT identity, including ``None`` tombstones for
|
|
1046
|
+
# primitive globals. The analyzer's legacy UDT registry is keyed only
|
|
1047
|
+
# by raw spelling and can be overwritten by an unrelated callable
|
|
1048
|
+
# local with the same name.
|
|
1049
|
+
self._global_udt_types: dict[str, str | None] = {}
|
|
1050
|
+
for stmt in self.ctx.ast.body:
|
|
1051
|
+
if not isinstance(stmt, VarDecl):
|
|
1052
|
+
continue
|
|
1053
|
+
spec = (
|
|
1054
|
+
self._type_spec_from_hint_name(stmt.type_hint)
|
|
1055
|
+
if stmt.type_hint
|
|
1056
|
+
else self._type_spec_from_expr(stmt.value)
|
|
1057
|
+
)
|
|
1058
|
+
self._global_udt_types[stmt.name] = (
|
|
1059
|
+
spec.name
|
|
1060
|
+
if spec is not None and spec.kind == "udt"
|
|
1061
|
+
else None
|
|
1062
|
+
)
|
|
1063
|
+
# Analyzer metadata preserves the collision-safe storage identity for
|
|
1064
|
+
# every persistent declaration, including chart-scope siblings that
|
|
1065
|
+
# are not direct Program children. Keep primitive tombstones too: the
|
|
1066
|
+
# raw-name UDT union must not swap ``state`` and ``state__blk1`` types.
|
|
1067
|
+
self._member_udt_types: dict[str, str | None] = {}
|
|
1068
|
+
metadata_by_node = getattr(
|
|
1069
|
+
self.ctx, "var_member_metadata_by_node", {}
|
|
1070
|
+
) or {}
|
|
1071
|
+
type_specs_by_node = getattr(
|
|
1072
|
+
self.ctx, "var_member_type_specs_by_node", {}
|
|
1073
|
+
) or {}
|
|
1074
|
+
for node_id, meta in metadata_by_node.items():
|
|
1075
|
+
stmt, member_name, _ptype, _init_str, _callable = meta
|
|
1076
|
+
spec = type_specs_by_node.get(node_id)
|
|
1077
|
+
if spec is None and isinstance(stmt, VarDecl):
|
|
1078
|
+
spec = (
|
|
1079
|
+
self._type_spec_from_hint_name(stmt.type_hint)
|
|
1080
|
+
if stmt.type_hint
|
|
1081
|
+
else self._type_spec_from_expr(stmt.value)
|
|
1082
|
+
)
|
|
1083
|
+
self._member_udt_types[member_name] = (
|
|
1084
|
+
spec.name
|
|
1085
|
+
if spec is not None and spec.kind == "udt"
|
|
1086
|
+
else None
|
|
1087
|
+
)
|
|
1088
|
+
self._runtime_var_init_flags: dict[tuple[int, str], str] = {}
|
|
1089
|
+
|
|
1090
|
+
used_names = set(self._all_member_names)
|
|
1091
|
+
# ``_all_member_names`` historically covers persistent ``var`` and
|
|
1092
|
+
# Series members only. Plain global declarations are class members as
|
|
1093
|
+
# well, so include them before minting a generated flag; otherwise a
|
|
1094
|
+
# legal user binding such as ``_pf_var_init_seeded = 1`` can collide
|
|
1095
|
+
# with the flag for ``var seeded = low``.
|
|
1096
|
+
used_names.update(
|
|
1097
|
+
self._safe_name(name) for name, _ptype in self.ctx.global_var_decls
|
|
1098
|
+
)
|
|
1099
|
+
# Natural call-site clones and context-sensitive fresh instances are
|
|
1100
|
+
# emitted later, but they occupy the same C++ class-member namespace as
|
|
1101
|
+
# these flags. Reserve the complete callable-state inventory up front;
|
|
1102
|
+
# in particular, an authored local named ``_pf_var_init_state`` also
|
|
1103
|
+
# produces a legal ``_pf_var_init_state_cs1`` storage clone.
|
|
1104
|
+
for remap in self._func_cs_var_remap.values():
|
|
1105
|
+
used_names.update(remap.values())
|
|
1106
|
+
used_names.update(
|
|
1107
|
+
fresh_safe
|
|
1108
|
+
for _owner, _orig_safe, fresh_safe in self._fresh_var_members
|
|
1109
|
+
)
|
|
1110
|
+
used_names.update(
|
|
1111
|
+
remapped
|
|
1112
|
+
for remap in self._func_cs_fixnan_remap.values()
|
|
1113
|
+
for remapped in remap.values()
|
|
1114
|
+
)
|
|
1115
|
+
used_names.update(
|
|
1116
|
+
fresh_safe for _site, fresh_safe in self._fresh_fixnan_members
|
|
1117
|
+
)
|
|
1118
|
+
# Keep function naming stable across this late preparation pass. A
|
|
1119
|
+
# flag must avoid an emitted base/clone/fresh method name rather than
|
|
1120
|
+
# making ``_func_safe_name`` change after the instance graph was built.
|
|
1121
|
+
for func_info in self.ctx.func_infos:
|
|
1122
|
+
emitted = self._func_cpp_base_name(func_info.name)
|
|
1123
|
+
used_names.add(emitted)
|
|
1124
|
+
for callsite in range(
|
|
1125
|
+
self.ctx.func_call_site_counts.get(func_info.name, 0)
|
|
1126
|
+
):
|
|
1127
|
+
used_names.add(f"{emitted}_cs{callsite}")
|
|
1128
|
+
used_names.update(inst["name"] for inst in self._fresh_instances)
|
|
1129
|
+
|
|
1130
|
+
def allocate_flag(base: str) -> str:
|
|
1131
|
+
flag = base
|
|
1132
|
+
suffix = 2
|
|
1133
|
+
while flag in used_names:
|
|
1134
|
+
flag = f"{base}_{suffix}"
|
|
1135
|
+
suffix += 1
|
|
1136
|
+
used_names.add(flag)
|
|
1137
|
+
self._all_member_names.add(flag)
|
|
1138
|
+
return flag
|
|
1139
|
+
|
|
1140
|
+
metadata_by_node = getattr(
|
|
1141
|
+
self.ctx, "var_member_metadata_by_node", {}
|
|
1142
|
+
) or {}
|
|
1143
|
+
type_specs_by_node = getattr(
|
|
1144
|
+
self.ctx, "var_member_type_specs_by_node", {}
|
|
1145
|
+
) or {}
|
|
1146
|
+
owners_by_node = getattr(
|
|
1147
|
+
self.ctx, "var_member_owners_by_node", {}
|
|
1148
|
+
) or {}
|
|
1149
|
+
top_level_node_ids = {id(stmt) for stmt in self.ctx.ast.body}
|
|
1150
|
+
for node_id, meta in metadata_by_node.items():
|
|
1151
|
+
stmt, member_name, ptype, init_str, is_callable_scoped = meta
|
|
1152
|
+
if not isinstance(stmt, VarDecl) or not (stmt.is_var or stmt.is_varip):
|
|
1153
|
+
continue
|
|
1154
|
+
stmt_spec = type_specs_by_node.get(node_id)
|
|
1155
|
+
if stmt_spec is None:
|
|
1156
|
+
stmt_spec = (
|
|
1157
|
+
self._type_spec_from_hint_name(stmt.type_hint)
|
|
1158
|
+
if stmt.type_hint
|
|
1159
|
+
else self._type_spec_from_expr(stmt.value)
|
|
1160
|
+
)
|
|
1161
|
+
drawing_cpp = (
|
|
1162
|
+
DRAWING_TYPE_TO_CPP.get(stmt_spec.name)
|
|
1163
|
+
if stmt_spec is not None and stmt_spec.kind == "udt"
|
|
1164
|
+
else None
|
|
1165
|
+
)
|
|
1166
|
+
if drawing_cpp is not None:
|
|
1167
|
+
self._drawing_var_member_cpp_types[member_name] = drawing_cpp
|
|
1168
|
+
self._drawing_var_decl_info_by_node[node_id] = {
|
|
1169
|
+
"node_id": node_id,
|
|
1170
|
+
"raw_name": stmt.name,
|
|
1171
|
+
"member_name": member_name,
|
|
1172
|
+
"drawing_cpp": drawing_cpp,
|
|
1173
|
+
"is_callable_scoped": is_callable_scoped,
|
|
1174
|
+
"owner": owners_by_node.get(node_id),
|
|
1175
|
+
"type_spec": stmt_spec,
|
|
1176
|
+
}
|
|
1177
|
+
if node_id in top_level_node_ids:
|
|
1178
|
+
self._global_drawing_cpp_types[stmt.name] = drawing_cpp
|
|
1179
|
+
is_series = self._safe_name(member_name) in self._series_var_member_names
|
|
1180
|
+
if node_id in self._drawing_var_decl_info_by_node:
|
|
1181
|
+
self._drawing_var_decl_info_by_node[node_id]["is_series"] = is_series
|
|
1182
|
+
# Every initialized callable ``var`` declaration must run at its
|
|
1183
|
+
# exact statement. Nested declarations may be unreached, while a
|
|
1184
|
+
# direct declaration can depend on ordinary statements immediately
|
|
1185
|
+
# before it; a function-entry preamble violates both Pine rules.
|
|
1186
|
+
# Exact-node once flags also preserve independent call-site clones.
|
|
1187
|
+
# A bare UDT/drawing ``na`` already has the correct default member
|
|
1188
|
+
# state and needs no assignment guard.
|
|
1189
|
+
callable_decl_site = (
|
|
1190
|
+
is_callable_scoped
|
|
1191
|
+
and stmt.value is not None
|
|
1192
|
+
and not (
|
|
1193
|
+
stmt_spec is not None
|
|
1194
|
+
and stmt_spec.kind == "udt"
|
|
1195
|
+
and self._is_na_expr(stmt.value)
|
|
1196
|
+
)
|
|
1197
|
+
)
|
|
1198
|
+
nullable_collection_selection_decl_site = (
|
|
1199
|
+
stmt_spec is not None
|
|
1200
|
+
and stmt_spec.kind in {"map", "matrix"}
|
|
1201
|
+
and isinstance(stmt.value, (IfStmt, SwitchStmt))
|
|
1202
|
+
)
|
|
1203
|
+
established_decl_site = (
|
|
1204
|
+
(not is_callable_scoped or drawing_cpp is not None)
|
|
1205
|
+
and (
|
|
1206
|
+
nullable_collection_selection_decl_site
|
|
1207
|
+
or self._is_runtime_scalar_var_initializer(
|
|
1208
|
+
member_name, ptype, init_str, stmt.value, drawing_cpp,
|
|
1209
|
+
is_series
|
|
1210
|
+
)
|
|
1211
|
+
)
|
|
1212
|
+
)
|
|
1213
|
+
if not callable_decl_site and not established_decl_site:
|
|
1214
|
+
continue
|
|
1215
|
+
|
|
1216
|
+
base_flag = f"_pf_var_init_{self._safe_name(member_name)}"
|
|
1217
|
+
flag = allocate_flag(base_flag)
|
|
1218
|
+
self._runtime_scalar_var_init_members.add(member_name)
|
|
1219
|
+
info = {
|
|
1220
|
+
"member_name": member_name,
|
|
1221
|
+
"node_id": node_id,
|
|
1222
|
+
"owner": owners_by_node.get(node_id),
|
|
1223
|
+
"is_callable_scoped": is_callable_scoped,
|
|
1224
|
+
"ptype": ptype,
|
|
1225
|
+
"flag": flag,
|
|
1226
|
+
"drawing_cpp": drawing_cpp,
|
|
1227
|
+
"type_spec": stmt_spec,
|
|
1228
|
+
"is_series": is_series,
|
|
1229
|
+
}
|
|
1230
|
+
self._runtime_scalar_var_init_by_node[node_id] = info
|
|
1231
|
+
base_storage = self._safe_name(member_name)
|
|
1232
|
+
self._runtime_var_init_flags[(node_id, base_storage)] = flag
|
|
1233
|
+
if is_callable_scoped:
|
|
1234
|
+
owner = owners_by_node.get(node_id)
|
|
1235
|
+
for (remap_owner, _cs_idx), remap in self._func_cs_var_remap.items():
|
|
1236
|
+
if remap_owner != owner or base_storage not in remap:
|
|
1237
|
+
continue
|
|
1238
|
+
storage = remap[base_storage]
|
|
1239
|
+
if storage == base_storage:
|
|
1240
|
+
continue
|
|
1241
|
+
clone_base = f"_pf_var_init_{storage}"
|
|
1242
|
+
clone_flag = allocate_flag(clone_base)
|
|
1243
|
+
self._runtime_var_init_flags[(node_id, storage)] = clone_flag
|
|
1244
|
+
for inst in self._fresh_instances:
|
|
1245
|
+
if inst.get("fname") != owner:
|
|
1246
|
+
continue
|
|
1247
|
+
storage = inst.get("var_remap", {}).get(
|
|
1248
|
+
base_storage, base_storage
|
|
1249
|
+
)
|
|
1250
|
+
key = (node_id, storage)
|
|
1251
|
+
if key in self._runtime_var_init_flags:
|
|
1252
|
+
continue
|
|
1253
|
+
fresh_base = f"_pf_var_init_{storage}"
|
|
1254
|
+
self._runtime_var_init_flags[key] = allocate_flag(
|
|
1255
|
+
fresh_base
|
|
1256
|
+
)
|
|
1257
|
+
if not is_callable_scoped:
|
|
1258
|
+
self._runtime_scalar_var_init_by_member[member_name] = info
|
|
1259
|
+
|
|
1260
|
+
self._runtime_var_init_flag_used_names = used_names
|
|
1261
|
+
|
|
1262
|
+
# Top-level non-persistent drawing declarations are not represented in
|
|
1263
|
+
# var-member metadata. Record their exact source declaration type so
|
|
1264
|
+
# functions can resolve globals without consulting the analyzer's
|
|
1265
|
+
# legacy raw-name UDT registry (which later locals may overwrite).
|
|
1266
|
+
for stmt in self.ctx.ast.body:
|
|
1267
|
+
if not isinstance(stmt, VarDecl) or stmt.is_var or stmt.is_varip:
|
|
1268
|
+
continue
|
|
1269
|
+
spec = (
|
|
1270
|
+
self._type_spec_from_hint_name(stmt.type_hint)
|
|
1271
|
+
if stmt.type_hint
|
|
1272
|
+
else self._type_spec_from_expr(stmt.value)
|
|
1273
|
+
)
|
|
1274
|
+
if (spec is not None and spec.kind == "udt"
|
|
1275
|
+
and spec.name in DRAWING_TYPE_TO_CPP):
|
|
1276
|
+
self._global_drawing_cpp_types[stmt.name] = (
|
|
1277
|
+
DRAWING_TYPE_TO_CPP[spec.name]
|
|
1278
|
+
)
|
|
1279
|
+
|
|
745
1280
|
# ------------------------------------------------------------------
|
|
746
1281
|
# Context-sensitive (call-path) instance machinery
|
|
747
1282
|
# ------------------------------------------------------------------
|
|
@@ -815,11 +1350,69 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
815
1350
|
if node is not None and getattr(node, "body", None):
|
|
816
1351
|
func_bodies.setdefault(fi.name, node.body)
|
|
817
1352
|
|
|
1353
|
+
# Pine forbids recursive callable execution. Most scalar-only cycles
|
|
1354
|
+
# never enter this state-instance pass (and some legacy dead-branch
|
|
1355
|
+
# typing probes intentionally retain them), but a cycle carrying TA,
|
|
1356
|
+
# var, series, fixnan, or transitive state would otherwise mint a new
|
|
1357
|
+
# fresh path instance forever. Reject exactly that stateful subgraph
|
|
1358
|
+
# before expanding any members or dispatch records.
|
|
1359
|
+
stateful_edges: dict[str, list[tuple[str, FuncCall]]] = {
|
|
1360
|
+
name: [] for name in stateful
|
|
1361
|
+
}
|
|
1362
|
+
inherited_names = getattr(
|
|
1363
|
+
ctx, "func_inherited_call_names", {}
|
|
1364
|
+
) or {}
|
|
1365
|
+
for owner, body in func_bodies.items():
|
|
1366
|
+
if owner not in stateful:
|
|
1367
|
+
continue
|
|
1368
|
+
for callnode in self._iter_func_calls(body):
|
|
1369
|
+
cs_info = ctx.func_call_cs_map.get(id(callnode))
|
|
1370
|
+
callee = (
|
|
1371
|
+
cs_info[0]
|
|
1372
|
+
if cs_info is not None
|
|
1373
|
+
else inherited_names.get(id(callnode))
|
|
1374
|
+
)
|
|
1375
|
+
if callee in stateful:
|
|
1376
|
+
stateful_edges[owner].append((callee, callnode))
|
|
1377
|
+
|
|
1378
|
+
colors: dict[str, int] = {}
|
|
1379
|
+
stack: list[str] = []
|
|
1380
|
+
|
|
1381
|
+
def reject_stateful_cycle(name: str) -> None:
|
|
1382
|
+
colors[name] = 1
|
|
1383
|
+
stack.append(name)
|
|
1384
|
+
for callee, callnode in stateful_edges.get(name, ()):
|
|
1385
|
+
color = colors.get(callee, 0)
|
|
1386
|
+
if color == 1:
|
|
1387
|
+
start = stack.index(callee)
|
|
1388
|
+
cycle = [*stack[start:], callee]
|
|
1389
|
+
self._codegen_error(
|
|
1390
|
+
callnode,
|
|
1391
|
+
"Recursive stateful callable cycle is not supported: "
|
|
1392
|
+
+ " -> ".join(cycle)
|
|
1393
|
+
+ ".",
|
|
1394
|
+
hint=(
|
|
1395
|
+
"Remove the recursive call; Pine user-defined "
|
|
1396
|
+
"functions cannot execute recursively."
|
|
1397
|
+
),
|
|
1398
|
+
)
|
|
1399
|
+
if color == 0:
|
|
1400
|
+
reject_stateful_cycle(callee)
|
|
1401
|
+
stack.pop()
|
|
1402
|
+
colors[name] = 2
|
|
1403
|
+
|
|
1404
|
+
for name in sorted(stateful):
|
|
1405
|
+
if colors.get(name, 0) == 0:
|
|
1406
|
+
reject_stateful_cycle(name)
|
|
1407
|
+
|
|
818
1408
|
def ta_originals(fname: str) -> list[str]:
|
|
819
1409
|
return list(self._func_cs_ta_remap.get((fname, 0), {}).keys())
|
|
820
1410
|
|
|
821
1411
|
def var_originals(fname: str) -> list[str]:
|
|
822
|
-
return [
|
|
1412
|
+
return [
|
|
1413
|
+
self._safe_name(self._func_var_storage_name(fname, n))
|
|
1414
|
+
for n, _, _ in ctx.func_var_members.get(fname, [])
|
|
1415
|
+
]
|
|
823
1416
|
|
|
824
1417
|
def fixnan_originals(fname: str) -> list[str]:
|
|
825
1418
|
return list(self._func_cs_fixnan_remap.get((fname, 0), {}).keys())
|
|
@@ -842,6 +1435,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
842
1435
|
worklist.append({
|
|
843
1436
|
"fname": fname,
|
|
844
1437
|
"name": natural_name(fname, k),
|
|
1438
|
+
"call_site_idx": k,
|
|
845
1439
|
"ta_remap": self._func_cs_ta_remap.get((fname, k), {}),
|
|
846
1440
|
"var_remap": self._func_cs_var_remap.get((fname, k), {}),
|
|
847
1441
|
"fixnan_remap": self._func_cs_fixnan_remap.get((fname, k), {}),
|
|
@@ -850,6 +1444,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
850
1444
|
worklist.append({
|
|
851
1445
|
"fname": fname,
|
|
852
1446
|
"name": self._func_cpp_base_name(fname),
|
|
1447
|
+
"call_site_idx": None,
|
|
853
1448
|
"ta_remap": {},
|
|
854
1449
|
"var_remap": {},
|
|
855
1450
|
"fixnan_remap": {},
|
|
@@ -864,12 +1459,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
864
1459
|
if not body:
|
|
865
1460
|
continue
|
|
866
1461
|
active_ta = inst["ta_remap"]
|
|
1462
|
+
active_var = inst.get("var_remap", {})
|
|
867
1463
|
active_fixnan = inst.get("fixnan_remap", {})
|
|
868
1464
|
for callnode in self._iter_func_calls(body):
|
|
869
1465
|
cs_info = ctx.func_call_cs_map.get(id(callnode))
|
|
870
1466
|
if cs_info is None:
|
|
871
|
-
|
|
872
|
-
|
|
1467
|
+
# Natural csN parents deliberately leave inherited nested
|
|
1468
|
+
# calls unmapped so visit_call can thread N through its
|
|
1469
|
+
# active-index fallback. A fresh context-sensitive parent
|
|
1470
|
+
# has no active index; recover only that removed edge's
|
|
1471
|
+
# exact callee identity and compose it from natural cs0.
|
|
1472
|
+
if not inst.get("fresh", False):
|
|
1473
|
+
continue
|
|
1474
|
+
g_name = ctx.func_inherited_call_names.get(id(callnode))
|
|
1475
|
+
if g_name is None:
|
|
1476
|
+
continue
|
|
1477
|
+
j = 0
|
|
1478
|
+
else:
|
|
1479
|
+
g_name, j = cs_info
|
|
873
1480
|
if g_name not in stateful:
|
|
874
1481
|
continue
|
|
875
1482
|
natural_ta = self._func_cs_ta_remap.get((g_name, j), {})
|
|
@@ -877,18 +1484,56 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
877
1484
|
for m in ta_originals(g_name):
|
|
878
1485
|
mid = natural_ta.get(m, m)
|
|
879
1486
|
composed_ta[m] = active_ta.get(mid, mid)
|
|
1487
|
+
g_var_originals = var_originals(g_name)
|
|
1488
|
+
full_natural_var = self._func_cs_var_remap.get(
|
|
1489
|
+
(g_name, j), {}
|
|
1490
|
+
)
|
|
1491
|
+
natural_var = {
|
|
1492
|
+
name: full_natural_var.get(name, name)
|
|
1493
|
+
for name in g_var_originals
|
|
1494
|
+
}
|
|
1495
|
+
composed_var = {}
|
|
1496
|
+
for m in g_var_originals:
|
|
1497
|
+
mid = natural_var.get(m, m)
|
|
1498
|
+
composed_var[m] = active_var.get(mid, mid)
|
|
880
1499
|
natural_fixnan = self._func_cs_fixnan_remap.get((g_name, j), {})
|
|
881
1500
|
composed_fixnan = {}
|
|
882
1501
|
for m in fixnan_originals(g_name):
|
|
883
1502
|
mid = natural_fixnan.get(m, m)
|
|
884
1503
|
composed_fixnan[m] = active_fixnan.get(mid, mid)
|
|
885
|
-
|
|
1504
|
+
owns_non_ta_state = bool(
|
|
1505
|
+
g_var_originals or fixnan_originals(g_name)
|
|
1506
|
+
)
|
|
1507
|
+
# Every non-cs0 enclosing variant is a distinct Pine written
|
|
1508
|
+
# call path. Even a pure wrapper can reach var/fixnan state
|
|
1509
|
+
# farther down the graph, so preserve the path identity here
|
|
1510
|
+
# and let the fresh wrapper compose its next edge in turn.
|
|
1511
|
+
path_requires_fresh_state = (
|
|
1512
|
+
inst.get("fresh", False)
|
|
1513
|
+
or inst.get("call_site_idx") not in (None, 0)
|
|
1514
|
+
)
|
|
1515
|
+
if (
|
|
1516
|
+
not path_requires_fresh_state
|
|
1517
|
+
and composed_ta == natural_ta
|
|
1518
|
+
and composed_var == natural_var
|
|
1519
|
+
and composed_fixnan == natural_fixnan
|
|
1520
|
+
):
|
|
886
1521
|
# Path resolves to the callee's own cs{j} clone — reuse it.
|
|
887
1522
|
self._instance_dispatch[(inst["name"], id(callnode))] = \
|
|
888
1523
|
natural_name(g_name, j)
|
|
889
1524
|
continue
|
|
890
|
-
|
|
891
|
-
|
|
1525
|
+
path_identity = (
|
|
1526
|
+
(inst["name"], id(callnode))
|
|
1527
|
+
if owns_non_ta_state or path_requires_fresh_state
|
|
1528
|
+
else None
|
|
1529
|
+
)
|
|
1530
|
+
key = (
|
|
1531
|
+
g_name,
|
|
1532
|
+
frozenset(composed_ta.items()),
|
|
1533
|
+
frozenset(composed_var.items()),
|
|
1534
|
+
frozenset(composed_fixnan.items()),
|
|
1535
|
+
path_identity,
|
|
1536
|
+
)
|
|
892
1537
|
ginst = interned.get(key)
|
|
893
1538
|
if ginst is None:
|
|
894
1539
|
fresh_counter += 1
|
|
@@ -897,7 +1542,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
897
1542
|
for v in var_originals(g_name):
|
|
898
1543
|
fresh_member = f"{v}__ni{fresh_counter}"
|
|
899
1544
|
fvar_remap[v] = fresh_member
|
|
900
|
-
self._fresh_var_members.append((v, fresh_member))
|
|
1545
|
+
self._fresh_var_members.append((g_name, v, fresh_member))
|
|
901
1546
|
# Fresh fixnan members: each path gets its OWN previous-
|
|
902
1547
|
# value member so two call paths never share fixnan state.
|
|
903
1548
|
ffixnan_remap: dict[str, str] = {}
|
|
@@ -917,6 +1562,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
917
1562
|
ginst = {
|
|
918
1563
|
"fname": g_name,
|
|
919
1564
|
"name": inst_name,
|
|
1565
|
+
"fresh": True,
|
|
1566
|
+
"call_site_idx": None,
|
|
920
1567
|
"ta_remap": composed_ta,
|
|
921
1568
|
"var_remap": fvar_remap,
|
|
922
1569
|
"fixnan_remap": ffixnan_remap,
|
|
@@ -926,8 +1573,107 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
926
1573
|
worklist.append(ginst)
|
|
927
1574
|
self._instance_dispatch[(inst["name"], id(callnode))] = ginst["name"]
|
|
928
1575
|
|
|
1576
|
+
def _callable_var_collection_spec(
|
|
1577
|
+
self, name: str, owner_func: str | None = None) -> TypeSpec | None:
|
|
1578
|
+
"""Exact TypeSpec for a function-scoped persistent collection member.
|
|
1579
|
+
|
|
1580
|
+
Only declaration nodes that actually became callable-scoped persistent
|
|
1581
|
+
members are eligible here. A raw-name scan of the general callable
|
|
1582
|
+
inventory can otherwise turn a global scalar member into a collection
|
|
1583
|
+
merely because an unrelated helper has a same-named ordinary local.
|
|
1584
|
+
Clone contexts may own state reachable through another callable, so an
|
|
1585
|
+
explicit owner is preferred and the owner-less fallback remains
|
|
1586
|
+
deliberately limited to a single unambiguous persistent TypeSpec.
|
|
1587
|
+
"""
|
|
1588
|
+
safe = self._safe_name(name)
|
|
1589
|
+
|
|
1590
|
+
candidates_by_owner: dict[str, list[TypeSpec]] = {}
|
|
1591
|
+
metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
|
|
1592
|
+
for node_id, meta in metadata.items():
|
|
1593
|
+
_node, member_name, _ptype, _init_str, is_callable_scoped = meta
|
|
1594
|
+
if (not is_callable_scoped
|
|
1595
|
+
or self._safe_name(member_name) != safe):
|
|
1596
|
+
continue
|
|
1597
|
+
spec = self._callable_collection_bindings.get(node_id)
|
|
1598
|
+
owner = self._callable_collection_binding_owners.get(node_id)
|
|
1599
|
+
if (owner is None or spec is None
|
|
1600
|
+
or spec.kind not in {"array", "map", "matrix"}):
|
|
1601
|
+
continue
|
|
1602
|
+
owner_candidates = candidates_by_owner.setdefault(owner, [])
|
|
1603
|
+
if spec not in owner_candidates:
|
|
1604
|
+
owner_candidates.append(spec)
|
|
1605
|
+
|
|
1606
|
+
if owner_func is not None:
|
|
1607
|
+
owned = candidates_by_owner.get(owner_func, [])
|
|
1608
|
+
if len(owned) == 1:
|
|
1609
|
+
return owned[0]
|
|
1610
|
+
# The existing clone inventory deliberately unions persistent vars
|
|
1611
|
+
# from reachable helpers into an enclosing function's remap. In
|
|
1612
|
+
# that case ``owner_func`` names the clone context, not the lexical
|
|
1613
|
+
# declaration owner; fall through to the unambiguous owner search.
|
|
1614
|
+
|
|
1615
|
+
candidates: list[TypeSpec] = []
|
|
1616
|
+
for owned in candidates_by_owner.values():
|
|
1617
|
+
for spec in owned:
|
|
1618
|
+
if spec not in candidates:
|
|
1619
|
+
candidates.append(spec)
|
|
1620
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
1621
|
+
|
|
1622
|
+
def _callable_var_udt_spec(
|
|
1623
|
+
self, name: str, owner_func: str | None = None) -> TypeSpec | None:
|
|
1624
|
+
"""Exact UDT TypeSpec for one callable persistent member identity.
|
|
1625
|
+
|
|
1626
|
+
The analyzer's legacy ``_udt_var_types`` registry is keyed by raw Pine
|
|
1627
|
+
spelling, so sibling declarations such as ``state`` / ``state__blk1``
|
|
1628
|
+
can overwrite each other. Declaration metadata retains both the exact
|
|
1629
|
+
collision-safe member name and lexical owner; use it for base members
|
|
1630
|
+
and every written-callsite clone.
|
|
1631
|
+
"""
|
|
1632
|
+
safe = self._safe_name(name)
|
|
1633
|
+
candidates_by_owner: dict[str, list[TypeSpec]] = {}
|
|
1634
|
+
metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
|
|
1635
|
+
specs = getattr(self.ctx, "var_member_type_specs_by_node", {}) or {}
|
|
1636
|
+
owners = getattr(self.ctx, "var_member_owners_by_node", {}) or {}
|
|
1637
|
+
for node_id, meta in metadata.items():
|
|
1638
|
+
_node, member_name, _ptype, _init_str, is_callable_scoped = meta
|
|
1639
|
+
if (not is_callable_scoped
|
|
1640
|
+
or self._safe_name(member_name) != safe):
|
|
1641
|
+
continue
|
|
1642
|
+
spec = specs.get(node_id)
|
|
1643
|
+
owner = owners.get(node_id)
|
|
1644
|
+
if (owner is None or spec is None
|
|
1645
|
+
or spec.kind != "udt"
|
|
1646
|
+
or spec.name not in self._udt_defs):
|
|
1647
|
+
continue
|
|
1648
|
+
owner_candidates = candidates_by_owner.setdefault(owner, [])
|
|
1649
|
+
if spec not in owner_candidates:
|
|
1650
|
+
owner_candidates.append(spec)
|
|
1651
|
+
|
|
1652
|
+
if owner_func is not None:
|
|
1653
|
+
owned = candidates_by_owner.get(owner_func, [])
|
|
1654
|
+
if len(owned) == 1:
|
|
1655
|
+
return owned[0]
|
|
1656
|
+
|
|
1657
|
+
candidates: list[TypeSpec] = []
|
|
1658
|
+
for owned in candidates_by_owner.values():
|
|
1659
|
+
for spec in owned:
|
|
1660
|
+
if spec not in candidates:
|
|
1661
|
+
candidates.append(spec)
|
|
1662
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
1663
|
+
|
|
1664
|
+
def _member_udt_type(self, name: str) -> str | None:
|
|
1665
|
+
"""Exact UDT type for class-member storage, with global tombstones."""
|
|
1666
|
+
global_types = getattr(self, "_global_udt_types", {})
|
|
1667
|
+
if name in global_types:
|
|
1668
|
+
return global_types[name]
|
|
1669
|
+
member_types = getattr(self, "_member_udt_types", {})
|
|
1670
|
+
if name in member_types:
|
|
1671
|
+
return member_types[name]
|
|
1672
|
+
return self._udt_var_types.get(name)
|
|
1673
|
+
|
|
929
1674
|
def _emit_cloned_var_decl(self, orig_safe: str, cloned_safe: str,
|
|
930
|
-
series_suffix: str, lines: list[str]
|
|
1675
|
+
series_suffix: str, lines: list[str],
|
|
1676
|
+
owner_func: str | None = None) -> None:
|
|
931
1677
|
"""Declare a per-clone copy of a function-scoped ``var`` member, matching
|
|
932
1678
|
the original's C++ type (series / matrix / array / map / drawing-handle /
|
|
933
1679
|
UDT / scalar). Shared by the per-call-site clone loop and the fresh
|
|
@@ -935,22 +1681,59 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
935
1681
|
for vname, ptype, _init_str in self.ctx.var_members:
|
|
936
1682
|
if self._safe_name(vname) == orig_safe:
|
|
937
1683
|
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
938
|
-
if
|
|
1684
|
+
if cpp_type == "int" and self._is_int64_builtin_init(vname):
|
|
1685
|
+
cpp_type = "int64_t"
|
|
1686
|
+
collection_spec = self._callable_var_collection_spec(
|
|
1687
|
+
vname, owner_func
|
|
1688
|
+
)
|
|
1689
|
+
udt_spec = self._callable_var_udt_spec(vname, owner_func)
|
|
1690
|
+
member_udt_type = self._member_udt_type(vname)
|
|
1691
|
+
drawing_cpp = self._drawing_var_member_cpp_types.get(vname)
|
|
1692
|
+
if (drawing_cpp is not None
|
|
1693
|
+
and orig_safe in self._series_var_member_names):
|
|
1694
|
+
lines.append(
|
|
1695
|
+
f" Series<{drawing_cpp}> {cloned_safe}{series_suffix};"
|
|
1696
|
+
)
|
|
1697
|
+
elif (orig_safe in self._series_var_member_names
|
|
1698
|
+
or vname in self.ctx.series_vars):
|
|
939
1699
|
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
1700
|
+
elif collection_spec is not None:
|
|
1701
|
+
lines.append(
|
|
1702
|
+
f" {self._type_spec_to_cpp(collection_spec)} {cloned_safe};"
|
|
1703
|
+
)
|
|
940
1704
|
elif vname in self._matrix_specs:
|
|
941
1705
|
lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[vname])} {cloned_safe};")
|
|
942
1706
|
elif vname in self._array_vars:
|
|
943
1707
|
lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(vname))} {cloned_safe};")
|
|
944
1708
|
elif vname in self._map_vars:
|
|
945
1709
|
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(vname))} {cloned_safe};")
|
|
946
|
-
elif
|
|
1710
|
+
elif drawing_cpp is not None:
|
|
1711
|
+
lines.append(
|
|
1712
|
+
f" {drawing_cpp} {cloned_safe} = {drawing_cpp}{{}};"
|
|
1713
|
+
)
|
|
1714
|
+
elif udt_spec is not None or member_udt_type is not None:
|
|
947
1715
|
# Drawing handle / UDT var clone must match the original's
|
|
948
1716
|
# type (Line/Label/Box/<UDT>), not the coarse PineType
|
|
949
1717
|
# default (double) — otherwise the clone can't hold the
|
|
950
1718
|
# handle and drawing access on it reads a garbage / na id.
|
|
951
|
-
udt_t =
|
|
1719
|
+
udt_t = (
|
|
1720
|
+
udt_spec.name
|
|
1721
|
+
if udt_spec is not None
|
|
1722
|
+
else member_udt_type
|
|
1723
|
+
)
|
|
952
1724
|
handle_cpp = DRAWING_TYPE_TO_CPP.get(udt_t, udt_t)
|
|
953
1725
|
lines.append(f" {handle_cpp} {cloned_safe} = {handle_cpp}{{}};")
|
|
1726
|
+
elif vname in self._runtime_scalar_var_init_members:
|
|
1727
|
+
# Declaration-site state may remain unreached while COOF
|
|
1728
|
+
# snapshots value-copy every clone. Match the base member's
|
|
1729
|
+
# typed Pine-na default instead of leaving primitive clone
|
|
1730
|
+
# storage indeterminate (or giving it a legacy zero).
|
|
1731
|
+
pending = self._typed_na_init(
|
|
1732
|
+
"na<double>()", vname, ptype
|
|
1733
|
+
)
|
|
1734
|
+
lines.append(
|
|
1735
|
+
f" {cpp_type} {cloned_safe} = {pending};"
|
|
1736
|
+
)
|
|
954
1737
|
else:
|
|
955
1738
|
lines.append(f" {cpp_type} {cloned_safe};")
|
|
956
1739
|
return
|
|
@@ -961,6 +1744,52 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
961
1744
|
else:
|
|
962
1745
|
lines.append(f" double {cloned_safe} = 0.0;")
|
|
963
1746
|
|
|
1747
|
+
def _binding_is_series(
|
|
1748
|
+
self,
|
|
1749
|
+
raw_name: str,
|
|
1750
|
+
resolved_safe_name: str | None = None,
|
|
1751
|
+
) -> bool:
|
|
1752
|
+
"""Resolve Series-ness without conflating renamed persistent siblings.
|
|
1753
|
+
|
|
1754
|
+
``ctx.series_vars`` is a legacy raw-name union. When a persistent
|
|
1755
|
+
member identity is known, its exact membership wins; otherwise keep the
|
|
1756
|
+
established raw-name behavior for ordinary non-var series, bar
|
|
1757
|
+
builtins, and parameters. A call-site clone resolves to ``h_cs1`` but
|
|
1758
|
+
inherits the base member ``h`` status, while a sibling rename such as
|
|
1759
|
+
``x__blk1`` is itself an exact member and therefore wins directly.
|
|
1760
|
+
"""
|
|
1761
|
+
base_safe = self._safe_name(raw_name)
|
|
1762
|
+
resolved = resolved_safe_name or base_safe
|
|
1763
|
+
if raw_name in self._lexical_series_bindings:
|
|
1764
|
+
return self._lexical_series_bindings[raw_name]
|
|
1765
|
+
if resolved in self._persistent_var_member_names:
|
|
1766
|
+
return resolved in self._series_var_member_names
|
|
1767
|
+
if (raw_name in self._qualified_func_var_raw_names
|
|
1768
|
+
and raw_name in self._ordinary_global_binding_names):
|
|
1769
|
+
return raw_name in self._ordinary_global_series_names
|
|
1770
|
+
if base_safe in self._persistent_var_member_names:
|
|
1771
|
+
return base_safe in self._series_var_member_names
|
|
1772
|
+
return raw_name in self.ctx.series_vars
|
|
1773
|
+
|
|
1774
|
+
def _known_var_is_lexically_shadowed(self, name: str) -> bool:
|
|
1775
|
+
"""Whether a local/parameter/loop binder hides a known global name."""
|
|
1776
|
+
return name in self._lexical_known_var_tombstones
|
|
1777
|
+
|
|
1778
|
+
def _decl_binding_is_series(self, node_id: int, raw_name: str) -> bool:
|
|
1779
|
+
"""Return exact history status for one declaration binding.
|
|
1780
|
+
|
|
1781
|
+
A VarDecl has one name, but a TupleAssign node owns several names. The
|
|
1782
|
+
node-only legacy set therefore cannot distinguish ``[x, y]`` when only
|
|
1783
|
+
one element is history-referenced. Prefer the exact analyzer key and
|
|
1784
|
+
retain the node-only fallback for older/manually-built contexts.
|
|
1785
|
+
"""
|
|
1786
|
+
exact = getattr(self.ctx, "series_decl_bindings", set()) or set()
|
|
1787
|
+
if (node_id, raw_name) in exact:
|
|
1788
|
+
return True
|
|
1789
|
+
if any(binding_node == node_id for binding_node, _ in exact):
|
|
1790
|
+
return False
|
|
1791
|
+
return node_id in getattr(self.ctx, "series_decl_nodes", set())
|
|
1792
|
+
|
|
964
1793
|
@staticmethod
|
|
965
1794
|
def _int_literal_value(node: ASTNode | None) -> int | None:
|
|
966
1795
|
"""Return the integer value of a (possibly unary-minus) NumberLiteral,
|
|
@@ -1018,6 +1847,29 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1018
1847
|
``global_expr_map``; without registering it here, ``m`` was emitted as a scalar
|
|
1019
1848
|
while ``on_bar`` still assigned ``PineMatrix``.
|
|
1020
1849
|
"""
|
|
1850
|
+
# A typed ``matrix<T> m = na`` or an inferred nullable selection has no
|
|
1851
|
+
# constructor call for the legacy RHS scan below to recognize. Resolve
|
|
1852
|
+
# declarations in source order from the authored hint/RHS, not from
|
|
1853
|
+
# ``ctx.collection_types``: that raw-name table describes the analyzer's
|
|
1854
|
+
# final state and can already contain an invalid later reassignment's
|
|
1855
|
+
# element type. The original declaration must remain the compatibility
|
|
1856
|
+
# baseline used to reject matrix<int> := matrix<float>.
|
|
1857
|
+
for stmt, _in_loop in self._walk_global_scope_with_loopflag(
|
|
1858
|
+
getattr(self.ctx.ast, "body", []), False
|
|
1859
|
+
):
|
|
1860
|
+
if not isinstance(stmt, VarDecl):
|
|
1861
|
+
continue
|
|
1862
|
+
if stmt.type_hint:
|
|
1863
|
+
declared_spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
1864
|
+
elif isinstance(stmt.value, (Ternary, IfStmt, SwitchStmt)):
|
|
1865
|
+
declared_spec = self._type_spec_from_expr(stmt.value)
|
|
1866
|
+
else:
|
|
1867
|
+
continue
|
|
1868
|
+
if declared_spec is None or declared_spec.kind != "matrix":
|
|
1869
|
+
continue
|
|
1870
|
+
self._matrix_specs.setdefault(stmt.name, declared_spec)
|
|
1871
|
+
self._collection_types[stmt.name] = self._matrix_specs[stmt.name]
|
|
1872
|
+
|
|
1021
1873
|
gem = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1022
1874
|
for name, _ptype in self.ctx.global_var_decls:
|
|
1023
1875
|
expr = gem.get(name)
|
|
@@ -1099,7 +1951,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1099
1951
|
a separate scope and must not be attributed to a global member. The
|
|
1100
1952
|
``in_loop`` flag is True once inside any for/while loop body."""
|
|
1101
1953
|
for s in stmts:
|
|
1102
|
-
if isinstance(s, FuncDef):
|
|
1954
|
+
if isinstance(s, (FuncDef, MethodDef)):
|
|
1103
1955
|
continue
|
|
1104
1956
|
yield s, in_loop
|
|
1105
1957
|
child_in_loop = in_loop or isinstance(s, (ForStmt, ForInStmt, WhileStmt))
|
|
@@ -1113,47 +1965,6 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1113
1965
|
if isinstance(case_stmts, list):
|
|
1114
1966
|
yield from self._walk_global_scope_with_loopflag(case_stmts, child_in_loop)
|
|
1115
1967
|
|
|
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
|
-
|
|
1157
1968
|
def _extract_receiver_name(self, call_node) -> str | None:
|
|
1158
1969
|
"""Extract receiver Identifier name from m.method(...) or matrix.method(m, ...).
|
|
1159
1970
|
|
|
@@ -1216,8 +2027,70 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1216
2027
|
else:
|
|
1217
2028
|
self._codegen_error(node, "matrix.sort requires int, bool, string, or float element type; UDT matrices cannot be sorted")
|
|
1218
2029
|
|
|
2030
|
+
def _iter_declared_matrix_specs(self):
|
|
2031
|
+
"""Yield matrix TypeSpecs reachable from declared source types.
|
|
2032
|
+
|
|
2033
|
+
A typed ``matrix<T> x = na`` has no matrix call for a syntactic scan
|
|
2034
|
+
to find. Matrix types can also be nested in UDT fields or appear only
|
|
2035
|
+
at a callable boundary, so include selection must start from analyzer
|
|
2036
|
+
metadata and recurse through aggregate types.
|
|
2037
|
+
"""
|
|
2038
|
+
roots = list(self._collection_types.values())
|
|
2039
|
+
roots.extend(
|
|
2040
|
+
spec
|
|
2041
|
+
for specs in self._func_collection_types.values()
|
|
2042
|
+
for spec in specs.values()
|
|
2043
|
+
)
|
|
2044
|
+
roots.extend(
|
|
2045
|
+
spec
|
|
2046
|
+
for specs in self._block_collection_types.values()
|
|
2047
|
+
for spec in specs.values()
|
|
2048
|
+
if spec is not None
|
|
2049
|
+
)
|
|
2050
|
+
roots.extend(
|
|
2051
|
+
spec
|
|
2052
|
+
for fields in self._udt_field_type_specs.values()
|
|
2053
|
+
for spec in fields.values()
|
|
2054
|
+
)
|
|
2055
|
+
for fi in self.ctx.func_infos:
|
|
2056
|
+
roots.extend(
|
|
2057
|
+
spec
|
|
2058
|
+
for spec in (getattr(fi, "param_type_specs", []) or [])
|
|
2059
|
+
if spec is not None
|
|
2060
|
+
)
|
|
2061
|
+
return_spec = getattr(fi, "return_type_spec", None)
|
|
2062
|
+
if return_spec is not None:
|
|
2063
|
+
roots.append(return_spec)
|
|
2064
|
+
|
|
2065
|
+
def walk(spec: TypeSpec | None, visiting_udts: frozenset[str]):
|
|
2066
|
+
if spec is None:
|
|
2067
|
+
return
|
|
2068
|
+
if spec.kind == "matrix":
|
|
2069
|
+
yield spec
|
|
2070
|
+
return
|
|
2071
|
+
if spec.kind == "array":
|
|
2072
|
+
yield from walk(spec.element, visiting_udts)
|
|
2073
|
+
return
|
|
2074
|
+
if spec.kind == "map":
|
|
2075
|
+
yield from walk(spec.key, visiting_udts)
|
|
2076
|
+
yield from walk(spec.value, visiting_udts)
|
|
2077
|
+
return
|
|
2078
|
+
if spec.kind == "udt" and spec.name:
|
|
2079
|
+
if spec.name in visiting_udts:
|
|
2080
|
+
return
|
|
2081
|
+
nested_visiting = visiting_udts | {spec.name}
|
|
2082
|
+
for field_spec in self._udt_field_type_specs.get(
|
|
2083
|
+
spec.name, {}
|
|
2084
|
+
).values():
|
|
2085
|
+
yield from walk(field_spec, nested_visiting)
|
|
2086
|
+
|
|
2087
|
+
for root in roots:
|
|
2088
|
+
yield from walk(root, frozenset())
|
|
2089
|
+
|
|
1219
2090
|
def _detect_matrix_usage(self) -> bool:
|
|
1220
|
-
"""True if emitted C++ will need
|
|
2091
|
+
"""True if emitted C++ will need a matrix runtime header."""
|
|
2092
|
+
if next(self._iter_declared_matrix_specs(), None) is not None:
|
|
2093
|
+
return True
|
|
1221
2094
|
for _, _, init_str in self.ctx.var_members:
|
|
1222
2095
|
if init_str and "matrix.new" in str(init_str):
|
|
1223
2096
|
return True
|
|
@@ -1228,6 +2101,90 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1228
2101
|
return True
|
|
1229
2102
|
return False
|
|
1230
2103
|
|
|
2104
|
+
def _detect_generic_matrix_usage(self) -> bool:
|
|
2105
|
+
"""True if emitted C++ mentions a non-float matrix specialization."""
|
|
2106
|
+
float_spec = TypeSpec.primitive("float")
|
|
2107
|
+
if any(
|
|
2108
|
+
spec.element != float_spec
|
|
2109
|
+
for spec in self._iter_declared_matrix_specs()
|
|
2110
|
+
):
|
|
2111
|
+
return True
|
|
2112
|
+
|
|
2113
|
+
# A standalone matrix.new<T>() expression may have no declaration
|
|
2114
|
+
# TypeSpec. Read its explicit template argument directly so the
|
|
2115
|
+
# generated call still receives the generic runtime declaration.
|
|
2116
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2117
|
+
if not isinstance(node, FuncCall):
|
|
2118
|
+
continue
|
|
2119
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2120
|
+
if ns != "matrix" or fn != "new":
|
|
2121
|
+
continue
|
|
2122
|
+
targs = self._template_args_from_call(node)
|
|
2123
|
+
if not targs:
|
|
2124
|
+
continue
|
|
2125
|
+
elem_spec = self._type_spec_from_hint_name(targs[0])
|
|
2126
|
+
if elem_spec is not None and elem_spec != float_spec:
|
|
2127
|
+
return True
|
|
2128
|
+
return False
|
|
2129
|
+
|
|
2130
|
+
def _detect_map_usage(self) -> bool:
|
|
2131
|
+
"""True when emitted C++ needs the PineMap handle/runtime helpers.
|
|
2132
|
+
|
|
2133
|
+
The call scan catches constructors and built-in operations. TypeSpec
|
|
2134
|
+
scans additionally catch typed ``na`` maps and map fields that never
|
|
2135
|
+
execute a map call, so their declarations still receive map.hpp and
|
|
2136
|
+
map-aware COOF checkpoint support.
|
|
2137
|
+
"""
|
|
2138
|
+
def contains_map(
|
|
2139
|
+
spec: TypeSpec | None, visiting_udts: frozenset[str] = frozenset()
|
|
2140
|
+
) -> bool:
|
|
2141
|
+
if spec is None:
|
|
2142
|
+
return False
|
|
2143
|
+
if spec.kind == "map":
|
|
2144
|
+
return True
|
|
2145
|
+
if spec.kind in {"array", "matrix"}:
|
|
2146
|
+
return contains_map(spec.element, visiting_udts)
|
|
2147
|
+
if spec.kind == "udt" and spec.name:
|
|
2148
|
+
if spec.name in visiting_udts:
|
|
2149
|
+
return False
|
|
2150
|
+
nested_visiting = visiting_udts | {spec.name}
|
|
2151
|
+
return any(
|
|
2152
|
+
contains_map(field_spec, nested_visiting)
|
|
2153
|
+
for field_spec in self._udt_field_type_specs.get(
|
|
2154
|
+
spec.name, {}
|
|
2155
|
+
).values()
|
|
2156
|
+
)
|
|
2157
|
+
return False
|
|
2158
|
+
|
|
2159
|
+
if self._map_vars:
|
|
2160
|
+
return True
|
|
2161
|
+
if any(contains_map(spec) for spec in self._collection_types.values()):
|
|
2162
|
+
return True
|
|
2163
|
+
if any(
|
|
2164
|
+
contains_map(spec)
|
|
2165
|
+
for fields in self._udt_field_type_specs.values()
|
|
2166
|
+
for spec in fields.values()
|
|
2167
|
+
):
|
|
2168
|
+
return True
|
|
2169
|
+
for specs in self._func_collection_types.values():
|
|
2170
|
+
if any(contains_map(spec) for spec in specs.values()):
|
|
2171
|
+
return True
|
|
2172
|
+
for fi in self._func_info_map.values():
|
|
2173
|
+
if any(contains_map(spec) for spec in fi.param_type_specs):
|
|
2174
|
+
return True
|
|
2175
|
+
if contains_map(getattr(fi, "return_type_spec", None)):
|
|
2176
|
+
return True
|
|
2177
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2178
|
+
if isinstance(node, FuncCall):
|
|
2179
|
+
_fn, namespace = self._resolve_callee(node.callee)
|
|
2180
|
+
if namespace == "map":
|
|
2181
|
+
return True
|
|
2182
|
+
if isinstance(node, VarDecl):
|
|
2183
|
+
spec = self._type_spec_from_hint_name(node.type_hint)
|
|
2184
|
+
if contains_map(spec):
|
|
2185
|
+
return True
|
|
2186
|
+
return False
|
|
2187
|
+
|
|
1231
2188
|
# The type-inference helpers (_type_spec_*, _infer_type, _array_method_expr,
|
|
1232
2189
|
# _map_method_expr, _template_args_from_call, ...) live on TypeInferer
|
|
1233
2190
|
# — see codegen/types.py.
|
|
@@ -1262,6 +2219,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1262
2219
|
# First, find all variables that are reassigned anywhere in the AST.
|
|
1263
2220
|
# These cannot be inlined as constants since their value changes at runtime.
|
|
1264
2221
|
reassigned = self._find_reassigned_vars()
|
|
2222
|
+
# Register never-reassigned ``var``/``varip`` scalars with a stable
|
|
2223
|
+
# init FIRST — a later derived var (or a UDF body) may reference such a
|
|
2224
|
+
# scalar as a stable length component, so it must be classified stable
|
|
2225
|
+
# before those exprs are evaluated below.
|
|
2226
|
+
self._collect_stable_var_scalars(reassigned)
|
|
1265
2227
|
for stmt in self.ctx.ast.body:
|
|
1266
2228
|
if isinstance(stmt, VarDecl) and stmt.name not in reassigned:
|
|
1267
2229
|
self._collect_known_var(stmt)
|
|
@@ -1274,6 +2236,262 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1274
2236
|
# dependent reassignments are left untracked (rejected by the guard).
|
|
1275
2237
|
self._collect_reassigned_stable_scalars(reassigned)
|
|
1276
2238
|
|
|
2239
|
+
def _collect_stable_var_scalars(self, reassigned: set[str]) -> None:
|
|
2240
|
+
"""Track top-level ``var``/``varip`` scalars declared exactly once (never
|
|
2241
|
+
reassigned) from a stable init expression.
|
|
2242
|
+
|
|
2243
|
+
A ``var`` scalar's one-shot initializer runs once and the value never
|
|
2244
|
+
changes across bars, so a never-reassigned ``var`` over a stable init
|
|
2245
|
+
(``var int _tfSec = timeframe.in_seconds()``) is a bar-invariant scalar —
|
|
2246
|
+
safe to embed in a TA ctor runtime-reset expression. Recording it in
|
|
2247
|
+
``_derived_input_expr`` + ``_stable_runtime_vars`` lets the reset path
|
|
2248
|
+
expand the name and lets ``_expr_is_stable`` classify it (the
|
|
2249
|
+
``_stable_runtime_vars`` check precedes the ``_var_names`` rejection).
|
|
2250
|
+
|
|
2251
|
+
A ``var`` that is reassigned anywhere (``:=`` in ``reassigned``), is a
|
|
2252
|
+
series var, or is initialized from a non-stable value stays untracked —
|
|
2253
|
+
so a TA length fed by genuinely-mutable persistent state is still
|
|
2254
|
+
rejected by the constructor guard. Names are NOT folded into
|
|
2255
|
+
``_known_vars`` (no use-site inlining): only the length-analysis path is
|
|
2256
|
+
affected, and the ``var`` member still emits and initializes normally.
|
|
2257
|
+
"""
|
|
2258
|
+
literal_candidates = self._admitted_stable_var_ctor_literals(reassigned)
|
|
2259
|
+
|
|
2260
|
+
for stmt in (self.ctx.ast.body or []):
|
|
2261
|
+
if not isinstance(stmt, VarDecl):
|
|
2262
|
+
continue
|
|
2263
|
+
if not (stmt.is_var or stmt.is_varip):
|
|
2264
|
+
continue
|
|
2265
|
+
literal_record = literal_candidates.get(stmt.name)
|
|
2266
|
+
if stmt.name in reassigned:
|
|
2267
|
+
continue
|
|
2268
|
+
if self._decl_binding_is_series(id(stmt), stmt.name):
|
|
2269
|
+
continue
|
|
2270
|
+
if stmt.value is None or not self._expr_is_stable(stmt.value):
|
|
2271
|
+
continue
|
|
2272
|
+
expr_str = self._arith_expr_to_str(stmt.value)
|
|
2273
|
+
if expr_str is None:
|
|
2274
|
+
continue
|
|
2275
|
+
self._derived_input_expr[stmt.name] = expr_str
|
|
2276
|
+
self._stable_runtime_vars.add(stmt.name)
|
|
2277
|
+
if literal_record is not None:
|
|
2278
|
+
self._stable_var_ctor_literals[stmt.name] = literal_record
|
|
2279
|
+
# Mark input-backed iff the init references an input, so the reset
|
|
2280
|
+
# emits override-aware get_input_*() reads for it.
|
|
2281
|
+
import re as _re
|
|
2282
|
+
toks = set(_re.findall(r"[A-Za-z_][A-Za-z_0-9]*", expr_str))
|
|
2283
|
+
if any(t in self._input_backed_vars for t in toks):
|
|
2284
|
+
self._input_backed_vars.add(stmt.name)
|
|
2285
|
+
|
|
2286
|
+
@staticmethod
|
|
2287
|
+
def _is_direct_int_var_literal(node) -> bool:
|
|
2288
|
+
"""Whether ``node`` is the one literal persistent shape we admit."""
|
|
2289
|
+
|
|
2290
|
+
return (
|
|
2291
|
+
isinstance(node, VarDecl)
|
|
2292
|
+
and node.is_var
|
|
2293
|
+
and not node.is_varip
|
|
2294
|
+
and node.type_hint in (None, "int")
|
|
2295
|
+
and isinstance(node.value, NumberLiteral)
|
|
2296
|
+
and isinstance(node.value.value, int)
|
|
2297
|
+
and not isinstance(node.value.value, bool)
|
|
2298
|
+
)
|
|
2299
|
+
|
|
2300
|
+
@staticmethod
|
|
2301
|
+
def _walk_candidate_surface(root):
|
|
2302
|
+
"""Yield every AST node reachable through compiler-owned metadata.
|
|
2303
|
+
|
|
2304
|
+
The regular codegen walker intentionally follows only historically
|
|
2305
|
+
emitted statement/expression slots. Literal admission is a safety
|
|
2306
|
+
proof and therefore needs the larger authored surface: loop bounds and
|
|
2307
|
+
iterables, enum/type defaults, function/method parameter defaults kept
|
|
2308
|
+
in ``annotations``, and any future AST-bearing metadata. ``vars()``
|
|
2309
|
+
plus identity de-duplication is both exhaustive and cycle-safe.
|
|
2310
|
+
"""
|
|
2311
|
+
|
|
2312
|
+
seen: set[int] = set()
|
|
2313
|
+
|
|
2314
|
+
def walk(value):
|
|
2315
|
+
if value is None or isinstance(
|
|
2316
|
+
value, (str, bytes, int, float, bool)
|
|
2317
|
+
):
|
|
2318
|
+
return
|
|
2319
|
+
value_id = id(value)
|
|
2320
|
+
if value_id in seen:
|
|
2321
|
+
return
|
|
2322
|
+
seen.add(value_id)
|
|
2323
|
+
|
|
2324
|
+
if isinstance(value, ASTNode):
|
|
2325
|
+
yield value
|
|
2326
|
+
if isinstance(value, dict):
|
|
2327
|
+
for key, child in value.items():
|
|
2328
|
+
yield from walk(key)
|
|
2329
|
+
yield from walk(child)
|
|
2330
|
+
return
|
|
2331
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
2332
|
+
for child in value:
|
|
2333
|
+
yield from walk(child)
|
|
2334
|
+
return
|
|
2335
|
+
if hasattr(value, "__dict__"):
|
|
2336
|
+
for child in vars(value).values():
|
|
2337
|
+
yield from walk(child)
|
|
2338
|
+
|
|
2339
|
+
yield from walk(root)
|
|
2340
|
+
|
|
2341
|
+
def _stable_var_literal_has_earlier_use(
|
|
2342
|
+
self,
|
|
2343
|
+
name: str,
|
|
2344
|
+
declaration_index: int,
|
|
2345
|
+
) -> bool:
|
|
2346
|
+
"""Whether authored code reads ``name`` before its global declaration."""
|
|
2347
|
+
|
|
2348
|
+
for root in (self.ctx.ast.body or [])[:declaration_index]:
|
|
2349
|
+
if any(
|
|
2350
|
+
isinstance(node, Identifier) and node.name == name
|
|
2351
|
+
for node in self._walk_candidate_surface(root)
|
|
2352
|
+
):
|
|
2353
|
+
return True
|
|
2354
|
+
return False
|
|
2355
|
+
|
|
2356
|
+
def _candidate_surface_mentions_name(self, root, name: str) -> bool:
|
|
2357
|
+
return any(
|
|
2358
|
+
isinstance(node, Identifier) and node.name == name
|
|
2359
|
+
for node in self._walk_candidate_surface(root)
|
|
2360
|
+
)
|
|
2361
|
+
|
|
2362
|
+
def _stable_var_literal_has_unsupported_declaration(self) -> bool:
|
|
2363
|
+
"""Whether authored metadata would require cross-boundary dataflow.
|
|
2364
|
+
|
|
2365
|
+
Stable-var constructor folding is deliberately declaration-local. A
|
|
2366
|
+
user function, method, type, or enum introduces defaults, aliases, or
|
|
2367
|
+
receiver effects that require a wider proof, so this narrow feature
|
|
2368
|
+
fails closed for the whole script instead of approximating that proof.
|
|
2369
|
+
"""
|
|
2370
|
+
|
|
2371
|
+
return any(
|
|
2372
|
+
isinstance(node, (FuncDef, MethodDef, TypeDecl, EnumDecl))
|
|
2373
|
+
for node in self._walk_candidate_surface(self.ctx.ast)
|
|
2374
|
+
)
|
|
2375
|
+
|
|
2376
|
+
def _stable_var_literal_has_parse_recovery(self) -> bool:
|
|
2377
|
+
"""Whether the parser discarded any authored source fragment.
|
|
2378
|
+
|
|
2379
|
+
Recovery intentionally keeps broad corpus transpilation moving, but a
|
|
2380
|
+
discarded declaration can hide aliases or override a builtin method.
|
|
2381
|
+
Literal admission therefore requires a lossless parse even though all
|
|
2382
|
+
existing non-admission codegen paths retain their recovery behavior.
|
|
2383
|
+
"""
|
|
2384
|
+
|
|
2385
|
+
return bool(
|
|
2386
|
+
(self.ctx.ast.annotations or {}).get("parse_recovery_count", 0)
|
|
2387
|
+
)
|
|
2388
|
+
|
|
2389
|
+
def _stable_var_literal_has_unsupported_receiver_use(
|
|
2390
|
+
self,
|
|
2391
|
+
name: str,
|
|
2392
|
+
) -> bool:
|
|
2393
|
+
"""Catch primitive extension-method syntax dropped by the parser."""
|
|
2394
|
+
|
|
2395
|
+
roots: list[object] = [self.ctx.ast]
|
|
2396
|
+
roots.extend(
|
|
2397
|
+
pragma.expr_node
|
|
2398
|
+
for pragma in (getattr(self.ctx, "pf_trace_pragmas", None) or [])
|
|
2399
|
+
if getattr(pragma, "expr_node", None) is not None
|
|
2400
|
+
)
|
|
2401
|
+
for root in roots:
|
|
2402
|
+
for node in self._walk_candidate_surface(root):
|
|
2403
|
+
if (
|
|
2404
|
+
isinstance(node, FuncCall)
|
|
2405
|
+
and isinstance(node.callee, MemberAccess)
|
|
2406
|
+
and self._candidate_surface_mentions_name(
|
|
2407
|
+
node.callee.object, name
|
|
2408
|
+
)
|
|
2409
|
+
):
|
|
2410
|
+
return True
|
|
2411
|
+
return False
|
|
2412
|
+
|
|
2413
|
+
def _stable_var_literal_has_history_use(self, name: str) -> bool:
|
|
2414
|
+
"""Whether any authored or post-analyzer expression histories ``name``."""
|
|
2415
|
+
|
|
2416
|
+
roots: list[object] = [self.ctx.ast]
|
|
2417
|
+
roots.extend(
|
|
2418
|
+
pragma.expr_node
|
|
2419
|
+
for pragma in (getattr(self.ctx, "pf_trace_pragmas", None) or [])
|
|
2420
|
+
if getattr(pragma, "expr_node", None) is not None
|
|
2421
|
+
)
|
|
2422
|
+
for root in roots:
|
|
2423
|
+
for node in self._walk_candidate_surface(root):
|
|
2424
|
+
if not isinstance(node, Subscript):
|
|
2425
|
+
continue
|
|
2426
|
+
if any(
|
|
2427
|
+
isinstance(receiver_node, Identifier)
|
|
2428
|
+
and receiver_node.name == name
|
|
2429
|
+
for receiver_node in self._walk_candidate_surface(
|
|
2430
|
+
node.object
|
|
2431
|
+
)
|
|
2432
|
+
):
|
|
2433
|
+
return True
|
|
2434
|
+
return False
|
|
2435
|
+
|
|
2436
|
+
def _admitted_stable_var_ctor_literals(
|
|
2437
|
+
self,
|
|
2438
|
+
reassigned: set[str],
|
|
2439
|
+
) -> dict[str, _StableVarCtorLiteral]:
|
|
2440
|
+
"""Return declaration-exact persistent literals safe for TA sizing."""
|
|
2441
|
+
|
|
2442
|
+
occurrences: dict[str, list[tuple[int, VarDecl]]] = {}
|
|
2443
|
+
for index, stmt in enumerate(self.ctx.ast.body or []):
|
|
2444
|
+
if self._is_direct_int_var_literal(stmt):
|
|
2445
|
+
occurrences.setdefault(stmt.name, []).append((index, stmt))
|
|
2446
|
+
|
|
2447
|
+
unique = {
|
|
2448
|
+
name: items[0]
|
|
2449
|
+
for name, items in occurrences.items()
|
|
2450
|
+
if len(items) == 1
|
|
2451
|
+
}
|
|
2452
|
+
if not unique:
|
|
2453
|
+
return {}
|
|
2454
|
+
|
|
2455
|
+
candidate_names = set(unique)
|
|
2456
|
+
binding_counts = {name: 0 for name in candidate_names}
|
|
2457
|
+
for node in self._walk_candidate_surface(self.ctx.ast):
|
|
2458
|
+
names: list[str] = []
|
|
2459
|
+
if isinstance(node, VarDecl) and node.name:
|
|
2460
|
+
names.append(node.name)
|
|
2461
|
+
elif isinstance(node, TupleAssign):
|
|
2462
|
+
names.extend(name for name in node.names if name != "_")
|
|
2463
|
+
elif isinstance(node, ForStmt) and node.var:
|
|
2464
|
+
names.append(node.var)
|
|
2465
|
+
elif isinstance(node, ForInStmt):
|
|
2466
|
+
if node.var:
|
|
2467
|
+
names.append(node.var)
|
|
2468
|
+
names.extend(name for name in (node.vars or []) if name != "_")
|
|
2469
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
2470
|
+
names.extend(name for name in node.params if name != "_")
|
|
2471
|
+
for bound_name in names:
|
|
2472
|
+
if bound_name in binding_counts:
|
|
2473
|
+
binding_counts[bound_name] += 1
|
|
2474
|
+
|
|
2475
|
+
admitted: dict[str, _StableVarCtorLiteral] = {}
|
|
2476
|
+
for name, (index, declaration) in unique.items():
|
|
2477
|
+
if (
|
|
2478
|
+
name in reassigned
|
|
2479
|
+
or binding_counts.get(name) != 1
|
|
2480
|
+
or self._decl_binding_is_series(id(declaration), name)
|
|
2481
|
+
or self._stable_var_literal_has_earlier_use(name, index)
|
|
2482
|
+
or self._stable_var_literal_has_history_use(name)
|
|
2483
|
+
or self._stable_var_literal_has_unsupported_declaration()
|
|
2484
|
+
or self._stable_var_literal_has_parse_recovery()
|
|
2485
|
+
or self._stable_var_literal_has_unsupported_receiver_use(name)
|
|
2486
|
+
):
|
|
2487
|
+
continue
|
|
2488
|
+
admitted[name] = _StableVarCtorLiteral(
|
|
2489
|
+
value=declaration.value.value,
|
|
2490
|
+
declaration_id=id(declaration),
|
|
2491
|
+
top_level_index=index,
|
|
2492
|
+
)
|
|
2493
|
+
return admitted
|
|
2494
|
+
|
|
1277
2495
|
def _collect_reassigned_stable_scalars(self, reassigned: set[str]) -> None:
|
|
1278
2496
|
"""Track class-scope scalars that are reassigned but only along stable
|
|
1279
2497
|
if/elif paths (see ``test_stable_reassigned_class_scope_length``).
|
|
@@ -1438,7 +2656,117 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1438
2656
|
"ismonthly", "isdwm", "isseconds", "in_seconds", "isticks",
|
|
1439
2657
|
})
|
|
1440
2658
|
|
|
1441
|
-
|
|
2659
|
+
# Depth ceiling for inlining nested single-expression user functions while
|
|
2660
|
+
# classifying a TA length's stability. Pine forbids recursion, so any real
|
|
2661
|
+
# chain is shallow; the cap is a backstop against pathological input and is
|
|
2662
|
+
# enforced together with a name-stack cycle guard.
|
|
2663
|
+
_UDF_INLINE_MAX_DEPTH = 16
|
|
2664
|
+
|
|
2665
|
+
def _get_udf_def(self, name: str):
|
|
2666
|
+
"""Return the top-level single-name ``FuncDef`` for ``name`` (or None).
|
|
2667
|
+
|
|
2668
|
+
Built lazily and cached. UDT ``MethodDef``s are intentionally excluded —
|
|
2669
|
+
only a free function can appear as a bare-name TA length call.
|
|
2670
|
+
"""
|
|
2671
|
+
cache = getattr(self, "_udf_def_cache", None)
|
|
2672
|
+
if cache is None:
|
|
2673
|
+
cache = {}
|
|
2674
|
+
for stmt in (self.ctx.ast.body or []):
|
|
2675
|
+
if isinstance(stmt, FuncDef):
|
|
2676
|
+
# Last definition wins; a name map is all the length path needs.
|
|
2677
|
+
cache[stmt.name] = stmt
|
|
2678
|
+
self._udf_def_cache = cache
|
|
2679
|
+
return cache.get(name)
|
|
2680
|
+
|
|
2681
|
+
def _inline_single_expr_udf(self, node, _udf_stack: frozenset = frozenset(),
|
|
2682
|
+
_depth: int = 0):
|
|
2683
|
+
"""If ``node`` is a call to a user-defined SINGLE-EXPRESSION function,
|
|
2684
|
+
return its body expression with each parameter substituted by the
|
|
2685
|
+
corresponding call-argument node. Returns None when the call is not such
|
|
2686
|
+
a function, the arity/kwargs do not match, the body is not a single
|
|
2687
|
+
expression, the body contains a shape we do not clone, or the call would
|
|
2688
|
+
recurse (cycle / depth-limit).
|
|
2689
|
+
|
|
2690
|
+
Purely structural: it does NOT judge stability (the caller does, via
|
|
2691
|
+
``_expr_is_stable`` on the returned node). The conservative None keeps
|
|
2692
|
+
the TA-ctor guard intact.
|
|
2693
|
+
"""
|
|
2694
|
+
if not isinstance(node, FuncCall):
|
|
2695
|
+
return None
|
|
2696
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
2697
|
+
if namespace is not None or func_name is None:
|
|
2698
|
+
return None
|
|
2699
|
+
if func_name in _udf_stack or _depth >= self._UDF_INLINE_MAX_DEPTH:
|
|
2700
|
+
return None
|
|
2701
|
+
fdef = self._get_udf_def(func_name)
|
|
2702
|
+
if fdef is None:
|
|
2703
|
+
return None
|
|
2704
|
+
# A body that is exactly ONE expression statement is inlinable, whether
|
|
2705
|
+
# written inline after ``=>`` (``is_single_expr=True``) or as a one-line
|
|
2706
|
+
# indented block (``f(x) =>`` then a single indented expr, which the
|
|
2707
|
+
# parser records as ``is_single_expr=False`` with a one-ExprStmt body —
|
|
2708
|
+
# the gonzowiththewind-sisyphus ``f_bars`` shape). A multi-statement
|
|
2709
|
+
# body (len != 1, or a non-ExprStmt) is conservatively refused.
|
|
2710
|
+
body = fdef.body
|
|
2711
|
+
if not body or len(body) != 1 or not isinstance(body[0], ExprStmt):
|
|
2712
|
+
return None
|
|
2713
|
+
# Require a plain positional call: one arg per param, no kwargs, no
|
|
2714
|
+
# default-parameter fill-in — anything else is conservatively refused.
|
|
2715
|
+
if node.kwargs or len(node.args) != len(fdef.params):
|
|
2716
|
+
return None
|
|
2717
|
+
subst = dict(zip(fdef.params, node.args))
|
|
2718
|
+
return self._subst_params(body[0].expr, subst)
|
|
2719
|
+
|
|
2720
|
+
def _subst_params(self, node, subst: dict):
|
|
2721
|
+
"""Return a copy of ``node`` with every ``Identifier`` whose name is a
|
|
2722
|
+
key in ``subst`` replaced by the mapped argument node. Returns None for
|
|
2723
|
+
any node outside the small arithmetic/call grammar we fold (an
|
|
2724
|
+
unrecognised construct — e.g. a history ``Subscript`` — conservatively
|
|
2725
|
+
aborts the inline). ``dataclasses.replace`` preserves ``loc``/
|
|
2726
|
+
``annotations`` so diagnostics still point at real source spans."""
|
|
2727
|
+
import dataclasses as _dc
|
|
2728
|
+
if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral)):
|
|
2729
|
+
return node
|
|
2730
|
+
if isinstance(node, Identifier):
|
|
2731
|
+
return subst.get(node.name, node)
|
|
2732
|
+
if isinstance(node, MemberAccess):
|
|
2733
|
+
obj = self._subst_params(node.object, subst)
|
|
2734
|
+
if obj is None:
|
|
2735
|
+
return None
|
|
2736
|
+
return _dc.replace(node, object=obj)
|
|
2737
|
+
if isinstance(node, Ternary):
|
|
2738
|
+
c = self._subst_params(node.condition, subst)
|
|
2739
|
+
t = self._subst_params(node.true_val, subst)
|
|
2740
|
+
f = self._subst_params(node.false_val, subst)
|
|
2741
|
+
if c is None or t is None or f is None:
|
|
2742
|
+
return None
|
|
2743
|
+
return _dc.replace(node, condition=c, true_val=t, false_val=f)
|
|
2744
|
+
if isinstance(node, BinOp):
|
|
2745
|
+
l = self._subst_params(node.left, subst)
|
|
2746
|
+
r = self._subst_params(node.right, subst)
|
|
2747
|
+
if l is None or r is None:
|
|
2748
|
+
return None
|
|
2749
|
+
return _dc.replace(node, left=l, right=r)
|
|
2750
|
+
if isinstance(node, UnaryOp):
|
|
2751
|
+
o = self._subst_params(node.operand, subst)
|
|
2752
|
+
if o is None:
|
|
2753
|
+
return None
|
|
2754
|
+
return _dc.replace(node, operand=o)
|
|
2755
|
+
if isinstance(node, FuncCall):
|
|
2756
|
+
if node.kwargs:
|
|
2757
|
+
return None
|
|
2758
|
+
new_args = []
|
|
2759
|
+
for a in node.args:
|
|
2760
|
+
sa = self._subst_params(a, subst)
|
|
2761
|
+
if sa is None:
|
|
2762
|
+
return None
|
|
2763
|
+
new_args.append(sa)
|
|
2764
|
+
return _dc.replace(node, args=new_args)
|
|
2765
|
+
# Subscript (history read) and anything else: not a stable-length shape.
|
|
2766
|
+
return None
|
|
2767
|
+
|
|
2768
|
+
def _expr_is_stable(self, node, _udf_stack: frozenset = frozenset(),
|
|
2769
|
+
_depth: int = 0) -> bool:
|
|
1442
2770
|
"""True iff ``node``'s value is a bar-invariant scalar.
|
|
1443
2771
|
|
|
1444
2772
|
A stable expression depends only on: literals, ``input.*`` values,
|
|
@@ -1462,7 +2790,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1462
2790
|
return True
|
|
1463
2791
|
if isinstance(node, Identifier):
|
|
1464
2792
|
name = node.name
|
|
1465
|
-
if
|
|
2793
|
+
if self._known_var_is_lexically_shadowed(name):
|
|
2794
|
+
return False
|
|
2795
|
+
if (name in self._known_vars
|
|
2796
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
1466
2797
|
return True
|
|
1467
2798
|
if name in self._stable_runtime_vars:
|
|
1468
2799
|
return True
|
|
@@ -1496,13 +2827,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1496
2827
|
# History read (``close[1]``) or indexed access — per-bar.
|
|
1497
2828
|
return False
|
|
1498
2829
|
if isinstance(node, Ternary):
|
|
1499
|
-
return (self._expr_is_stable(node.condition)
|
|
1500
|
-
and self._expr_is_stable(node.true_val)
|
|
1501
|
-
and self._expr_is_stable(node.false_val))
|
|
2830
|
+
return (self._expr_is_stable(node.condition, _udf_stack, _depth)
|
|
2831
|
+
and self._expr_is_stable(node.true_val, _udf_stack, _depth)
|
|
2832
|
+
and self._expr_is_stable(node.false_val, _udf_stack, _depth))
|
|
1502
2833
|
if isinstance(node, BinOp):
|
|
1503
|
-
return self._expr_is_stable(node.left
|
|
2834
|
+
return (self._expr_is_stable(node.left, _udf_stack, _depth)
|
|
2835
|
+
and self._expr_is_stable(node.right, _udf_stack, _depth))
|
|
1504
2836
|
if isinstance(node, UnaryOp):
|
|
1505
|
-
return self._expr_is_stable(node.operand)
|
|
2837
|
+
return self._expr_is_stable(node.operand, _udf_stack, _depth)
|
|
1506
2838
|
if isinstance(node, FuncCall):
|
|
1507
2839
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
1508
2840
|
if namespace == "ta":
|
|
@@ -1510,20 +2842,40 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1510
2842
|
if namespace == "math":
|
|
1511
2843
|
if func_name not in self._MATH_STABLE_MEMBERS:
|
|
1512
2844
|
return False
|
|
1513
|
-
return all(self._expr_is_stable(a
|
|
2845
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
2846
|
+
for a in node.args)
|
|
1514
2847
|
if namespace == "timeframe":
|
|
1515
2848
|
# ``timeframe.in_seconds()`` (and any other function-form
|
|
1516
2849
|
# timeframe member) is a stable per-run scalar — it reflects
|
|
1517
2850
|
# the script's resolution, not a per-bar value.
|
|
1518
2851
|
if func_name not in self._TF_STABLE_MEMBERS:
|
|
1519
2852
|
return False
|
|
1520
|
-
return all(self._expr_is_stable(a
|
|
2853
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
2854
|
+
for a in node.args)
|
|
1521
2855
|
if namespace == "input":
|
|
1522
2856
|
return True
|
|
1523
2857
|
if namespace is None and func_name in ("int", "float", "bool", "string"):
|
|
1524
|
-
return all(self._expr_is_stable(a
|
|
1525
|
-
|
|
1526
|
-
#
|
|
2858
|
+
return all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
2859
|
+
for a in node.args)
|
|
2860
|
+
# A user-defined single-expression function is stable iff every
|
|
2861
|
+
# argument is stable AND its body (with the params bound to those
|
|
2862
|
+
# args) is stable — i.e. the body references only stable scalars
|
|
2863
|
+
# (inputs / consts / timeframe.* / math.* / never-reassigned var
|
|
2864
|
+
# scalars) and no series / strategy state / ta.* results. Inlining
|
|
2865
|
+
# the body (params substituted by the arg nodes) lets the ordinary
|
|
2866
|
+
# classifier decide; the name-stack + depth guard refuses recursion
|
|
2867
|
+
# so a cyclic / malformed UDF is rejected, not looped forever.
|
|
2868
|
+
if namespace is None and func_name is not None:
|
|
2869
|
+
inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
|
|
2870
|
+
if inlined is not None:
|
|
2871
|
+
if not all(self._expr_is_stable(a, _udf_stack, _depth)
|
|
2872
|
+
for a in node.args):
|
|
2873
|
+
return False
|
|
2874
|
+
return self._expr_is_stable(
|
|
2875
|
+
inlined, _udf_stack | {func_name}, _depth + 1)
|
|
2876
|
+
# Any other call (multi-statement user functions, str.*, array.*,
|
|
2877
|
+
# ...) — series by default; the conservative answer keeps the guard
|
|
2878
|
+
# honest.
|
|
1527
2879
|
return False
|
|
1528
2880
|
return False
|
|
1529
2881
|
|
|
@@ -1536,22 +2888,28 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1536
2888
|
# re-parsed and lowered through the expression visitor).
|
|
1537
2889
|
_ATOMIC_ARITH_NODES = (NumberLiteral, Identifier, MemberAccess, FuncCall)
|
|
1538
2890
|
|
|
1539
|
-
def _arith_operand_to_str(self, node
|
|
2891
|
+
def _arith_operand_to_str(self, node, _udf_stack: frozenset = frozenset(),
|
|
2892
|
+
_depth: int = 0) -> str | None:
|
|
1540
2893
|
"""Serialize ``node`` for use as an operand: parenthesize it unless its
|
|
1541
2894
|
serialized form is already self-delimiting, so grouping survives a
|
|
1542
2895
|
round-trip through the parser."""
|
|
1543
|
-
s = self._arith_expr_to_str(node)
|
|
2896
|
+
s = self._arith_expr_to_str(node, _udf_stack, _depth)
|
|
1544
2897
|
if s is None:
|
|
1545
2898
|
return None
|
|
1546
2899
|
if isinstance(node, self._ATOMIC_ARITH_NODES):
|
|
1547
2900
|
return s
|
|
1548
2901
|
return f"({s})"
|
|
1549
2902
|
|
|
1550
|
-
def _arith_expr_to_str(self, node
|
|
2903
|
+
def _arith_expr_to_str(self, node, _udf_stack: frozenset = frozenset(),
|
|
2904
|
+
_depth: int = 0) -> str | None:
|
|
1551
2905
|
"""Render a numeric arithmetic-over-identifiers expression to a string
|
|
1552
2906
|
that re-parses to the SAME tree (grouping preserved via
|
|
1553
2907
|
``_arith_operand_to_str``). Returns None for any node shape we don't
|
|
1554
2908
|
fold (series subscripts, etc.) so the caller leaves the var untracked.
|
|
2909
|
+
|
|
2910
|
+
``_udf_stack``/``_depth`` guard the single-expression-UDF inlining below
|
|
2911
|
+
against recursion cycles (Pine forbids recursion, but a malformed source
|
|
2912
|
+
must be refused, not looped forever).
|
|
1555
2913
|
"""
|
|
1556
2914
|
if isinstance(node, NumberLiteral):
|
|
1557
2915
|
v = node.value
|
|
@@ -1563,30 +2921,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1563
2921
|
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
1564
2922
|
return f"{node.object.name}.{node.member}"
|
|
1565
2923
|
if isinstance(node, BinOp):
|
|
1566
|
-
l = self._arith_operand_to_str(node.left)
|
|
1567
|
-
r = self._arith_operand_to_str(node.right)
|
|
2924
|
+
l = self._arith_operand_to_str(node.left, _udf_stack, _depth)
|
|
2925
|
+
r = self._arith_operand_to_str(node.right, _udf_stack, _depth)
|
|
1568
2926
|
if l is None or r is None:
|
|
1569
2927
|
return None
|
|
1570
2928
|
return f"{l} {node.op} {r}"
|
|
1571
2929
|
if isinstance(node, UnaryOp):
|
|
1572
|
-
o = self._arith_operand_to_str(node.operand)
|
|
2930
|
+
o = self._arith_operand_to_str(node.operand, _udf_stack, _depth)
|
|
1573
2931
|
if o is None:
|
|
1574
2932
|
return None
|
|
1575
2933
|
return f"{node.op}{o}"
|
|
1576
2934
|
if isinstance(node, Ternary):
|
|
1577
|
-
c = self._arith_operand_to_str(node.condition)
|
|
1578
|
-
t = self._arith_operand_to_str(node.true_val)
|
|
1579
|
-
f = self._arith_operand_to_str(node.false_val)
|
|
2935
|
+
c = self._arith_operand_to_str(node.condition, _udf_stack, _depth)
|
|
2936
|
+
t = self._arith_operand_to_str(node.true_val, _udf_stack, _depth)
|
|
2937
|
+
f = self._arith_operand_to_str(node.false_val, _udf_stack, _depth)
|
|
1580
2938
|
if c is None or t is None or f is None:
|
|
1581
2939
|
return None
|
|
1582
2940
|
return f"{c} ? {t} : {f}"
|
|
1583
2941
|
if isinstance(node, FuncCall):
|
|
1584
|
-
|
|
2942
|
+
# A bare-name call to a single-expression user function has no C++
|
|
2943
|
+
# counterpart at class scope — inline its body (params substituted
|
|
2944
|
+
# by the arg expressions) so the rendered string is pure
|
|
2945
|
+
# math/timeframe/input arithmetic the ctor-reset path can expand.
|
|
2946
|
+
# Namespaced calls (math.*/timeframe.*/int(...)) fall through to the
|
|
2947
|
+
# ordinary ``callee(args)`` rendering below. The stack/depth guard
|
|
2948
|
+
# refuses a recursive UDF (returns None -> caller leaves it untracked
|
|
2949
|
+
# -> the ctor guard rejects it loudly) instead of recursing forever.
|
|
2950
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2951
|
+
if ns is None and fn is not None and self._get_udf_def(fn) is not None:
|
|
2952
|
+
inlined = self._inline_single_expr_udf(node, _udf_stack, _depth)
|
|
2953
|
+
if inlined is None:
|
|
2954
|
+
return None
|
|
2955
|
+
return self._arith_expr_to_str(inlined, _udf_stack | {fn}, _depth + 1)
|
|
2956
|
+
callee = self._arith_expr_to_str(node.callee, _udf_stack, _depth)
|
|
1585
2957
|
if callee is None:
|
|
1586
2958
|
return None
|
|
1587
2959
|
parts = []
|
|
1588
2960
|
for a in node.args:
|
|
1589
|
-
s = self._arith_expr_to_str(a)
|
|
2961
|
+
s = self._arith_expr_to_str(a, _udf_stack, _depth)
|
|
1590
2962
|
if s is None:
|
|
1591
2963
|
return None
|
|
1592
2964
|
parts.append(s)
|
|
@@ -1596,7 +2968,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1596
2968
|
def _collect_known_var(self, node: VarDecl) -> None:
|
|
1597
2969
|
"""Extract known constant value from a VarDecl."""
|
|
1598
2970
|
# Don't inline series variables — their values change over time
|
|
1599
|
-
if node.name
|
|
2971
|
+
if self._decl_binding_is_series(id(node), node.name):
|
|
1600
2972
|
return
|
|
1601
2973
|
# Don't inline var/varip variables — they're mutable state that persists
|
|
1602
2974
|
# across bars and can be reassigned with :=
|
|
@@ -1834,29 +3206,145 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1834
3206
|
for child in walk_nodes(fi.node):
|
|
1835
3207
|
owner_by_node[id(child)] = fi.name
|
|
1836
3208
|
|
|
3209
|
+
# ``ctx.series_vars`` is a legacy raw-name union. A raw name can bind
|
|
3210
|
+
# both an exact Series declaration and an unrelated scalar declaration
|
|
3211
|
+
# (sibling blocks, or a callable local shadowing a global). Such calls
|
|
3212
|
+
# may need a synthetic history bridge even though the raw name appears
|
|
3213
|
+
# in that union, so reserve the bridge up front; source-order lexical
|
|
3214
|
+
# state decides whether the emitted call actually uses it.
|
|
3215
|
+
binding_series_states: dict[str, set[bool]] = {}
|
|
3216
|
+
var_metadata = getattr(
|
|
3217
|
+
self.ctx, "var_member_metadata_by_node", {}
|
|
3218
|
+
) or {}
|
|
3219
|
+
for decl in walk_nodes(self.ctx.ast):
|
|
3220
|
+
if isinstance(decl, VarDecl):
|
|
3221
|
+
metadata = var_metadata.get(id(decl))
|
|
3222
|
+
if metadata is not None:
|
|
3223
|
+
exact_member = self._safe_name(metadata[1])
|
|
3224
|
+
is_series = exact_member in self._series_var_member_names
|
|
3225
|
+
else:
|
|
3226
|
+
is_series = self._decl_binding_is_series(
|
|
3227
|
+
id(decl), decl.name
|
|
3228
|
+
)
|
|
3229
|
+
binding_series_states.setdefault(decl.name, set()).add(
|
|
3230
|
+
is_series
|
|
3231
|
+
)
|
|
3232
|
+
elif isinstance(decl, TupleAssign):
|
|
3233
|
+
for name in decl.names:
|
|
3234
|
+
if name != "_":
|
|
3235
|
+
binding_series_states.setdefault(name, set()).add(
|
|
3236
|
+
self._decl_binding_is_series(id(decl), name)
|
|
3237
|
+
)
|
|
3238
|
+
elif isinstance(decl, ForStmt):
|
|
3239
|
+
if decl.var:
|
|
3240
|
+
binding_series_states.setdefault(decl.var, set()).add(
|
|
3241
|
+
False
|
|
3242
|
+
)
|
|
3243
|
+
elif isinstance(decl, ForInStmt):
|
|
3244
|
+
loop_names = (
|
|
3245
|
+
[decl.var]
|
|
3246
|
+
if decl.var
|
|
3247
|
+
else list(decl.vars or [])
|
|
3248
|
+
)
|
|
3249
|
+
for name in loop_names:
|
|
3250
|
+
if name and name != "_":
|
|
3251
|
+
binding_series_states.setdefault(name, set()).add(
|
|
3252
|
+
False
|
|
3253
|
+
)
|
|
3254
|
+
ambiguous_series_names = {
|
|
3255
|
+
name for name, states in binding_series_states.items()
|
|
3256
|
+
if len(states) > 1
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
def register_one(
|
|
3260
|
+
kind: str,
|
|
3261
|
+
source_key: tuple,
|
|
3262
|
+
cpp_type: str,
|
|
3263
|
+
context: str | None,
|
|
3264
|
+
) -> None:
|
|
3265
|
+
if cpp_type not in ("double", "int", "int64_t", "bool"):
|
|
3266
|
+
cpp_type = "double"
|
|
3267
|
+
key = (kind, *source_key, context)
|
|
3268
|
+
if key in self._inline_history_member_by_key:
|
|
3269
|
+
return
|
|
3270
|
+
counters[kind] += 1
|
|
3271
|
+
member_name = f"_{kind}_{counters[kind]}"
|
|
3272
|
+
self._inline_history_member_by_key[key] = member_name
|
|
3273
|
+
self._inline_history_members.append({
|
|
3274
|
+
"kind": kind,
|
|
3275
|
+
"member_name": member_name,
|
|
3276
|
+
"cpp_type": cpp_type,
|
|
3277
|
+
"context": context,
|
|
3278
|
+
})
|
|
3279
|
+
|
|
1837
3280
|
def register(kind: str, source_key: tuple, cpp_type: str,
|
|
1838
3281
|
owner: str | None) -> None:
|
|
1839
|
-
if cpp_type not in ("double", "int", "bool"):
|
|
1840
|
-
cpp_type = "double"
|
|
1841
3282
|
for context in self._inline_history_contexts_for_owner(owner):
|
|
1842
|
-
|
|
1843
|
-
if key in self._inline_history_member_by_key:
|
|
1844
|
-
continue
|
|
1845
|
-
counters[kind] += 1
|
|
1846
|
-
member_name = f"_{kind}_{counters[kind]}"
|
|
1847
|
-
self._inline_history_member_by_key[key] = member_name
|
|
1848
|
-
self._inline_history_members.append({
|
|
1849
|
-
"kind": kind,
|
|
1850
|
-
"member_name": member_name,
|
|
1851
|
-
"cpp_type": cpp_type,
|
|
1852
|
-
"context": context,
|
|
1853
|
-
})
|
|
3283
|
+
register_one(kind, source_key, cpp_type, context)
|
|
1854
3284
|
|
|
1855
|
-
def
|
|
3285
|
+
def owner_cs_for_context(
|
|
3286
|
+
owner: str | None, context: str | None
|
|
3287
|
+
) -> int | None:
|
|
3288
|
+
if owner is None or context is None:
|
|
3289
|
+
return None
|
|
3290
|
+
prefix = f"{self._func_cpp_base_name(owner)}_cs"
|
|
3291
|
+
if not context.startswith(prefix):
|
|
3292
|
+
return None
|
|
3293
|
+
suffix = context[len(prefix):]
|
|
3294
|
+
return int(suffix) if suffix.isdigit() else None
|
|
3295
|
+
|
|
3296
|
+
def target_cs_for_context(
|
|
3297
|
+
fi,
|
|
3298
|
+
call: FuncCall,
|
|
3299
|
+
owner: str | None,
|
|
3300
|
+
context: str | None,
|
|
3301
|
+
) -> int | None:
|
|
3302
|
+
owner_cs = owner_cs_for_context(owner, context)
|
|
3303
|
+
cs_info = self.ctx.func_call_cs_map.get(id(call))
|
|
3304
|
+
count = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
3305
|
+
if owner_cs is not None and (
|
|
3306
|
+
cs_info is not None or count > 1
|
|
3307
|
+
):
|
|
3308
|
+
return owner_cs
|
|
3309
|
+
if cs_info is not None and cs_info[0] == fi.name:
|
|
3310
|
+
return cs_info[1]
|
|
3311
|
+
return None
|
|
3312
|
+
|
|
3313
|
+
def actual_args_for(call: FuncCall, params: list[str], method_info=None) -> list:
|
|
3314
|
+
if method_info is not None:
|
|
3315
|
+
return list(
|
|
3316
|
+
self._bind_typed_method_args(method_info, call).args_by_param
|
|
3317
|
+
)
|
|
1856
3318
|
if call.kwargs:
|
|
1857
3319
|
return _merge_kwargs(call.args, call.kwargs, params, lambda arg: arg)
|
|
1858
3320
|
return list(call.args)
|
|
1859
3321
|
|
|
3322
|
+
def owner_lexical_specs(owner: str | None) -> dict[str, TypeSpec | None]:
|
|
3323
|
+
"""Recover callable parameter types after analyzer scopes exit."""
|
|
3324
|
+
if owner is None:
|
|
3325
|
+
return {}
|
|
3326
|
+
info = self._func_info_map.get(owner)
|
|
3327
|
+
if info is None or info.node is None:
|
|
3328
|
+
return {}
|
|
3329
|
+
specs = list(getattr(info, "param_type_specs", ()) or ())
|
|
3330
|
+
lexical = {
|
|
3331
|
+
name: specs[index] if index < len(specs) else None
|
|
3332
|
+
for index, name in enumerate(info.node.params)
|
|
3333
|
+
}
|
|
3334
|
+
if (
|
|
3335
|
+
getattr(info, "is_udt_method", False)
|
|
3336
|
+
and info.node.params
|
|
3337
|
+
and info.udt_type_name
|
|
3338
|
+
):
|
|
3339
|
+
receiver_spec = specs[0] if specs else None
|
|
3340
|
+
if receiver_spec is None:
|
|
3341
|
+
receiver_spec = self._type_spec_from_hint_name(
|
|
3342
|
+
info.udt_type_name
|
|
3343
|
+
)
|
|
3344
|
+
lexical[info.node.params[0]] = receiver_spec
|
|
3345
|
+
lexical.update(self._func_collection_types.get(owner, {}))
|
|
3346
|
+
return lexical
|
|
3347
|
+
|
|
1860
3348
|
for node in walk_nodes(self.ctx.ast):
|
|
1861
3349
|
owner = owner_by_node.get(id(node))
|
|
1862
3350
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
@@ -1866,8 +3354,28 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1866
3354
|
|
|
1867
3355
|
if not isinstance(node, FuncCall):
|
|
1868
3356
|
continue
|
|
1869
|
-
|
|
1870
|
-
|
|
3357
|
+
fi = None
|
|
3358
|
+
method_call = False
|
|
3359
|
+
if isinstance(node.callee, MemberAccess):
|
|
3360
|
+
# Method syntax is type-directed. Resolve it before considering
|
|
3361
|
+
# any same-named bare UDF, and use the AST owner's lexical
|
|
3362
|
+
# parameter types: analyzer symbol scopes no longer exist in
|
|
3363
|
+
# this source-order prepass.
|
|
3364
|
+
receiver_spec = self._map_effect_type_spec(
|
|
3365
|
+
node.callee.object, owner_lexical_specs(owner)
|
|
3366
|
+
)
|
|
3367
|
+
receiver_name = method_receiver_type_name(receiver_spec)
|
|
3368
|
+
if receiver_name is not None:
|
|
3369
|
+
method_key = f"{receiver_name}.{node.callee.member}"
|
|
3370
|
+
candidate = self._func_info_map.get(method_key)
|
|
3371
|
+
if (
|
|
3372
|
+
candidate is not None
|
|
3373
|
+
and getattr(candidate, "is_udt_method", False)
|
|
3374
|
+
):
|
|
3375
|
+
fi = candidate
|
|
3376
|
+
method_call = True
|
|
3377
|
+
elif isinstance(node.callee, Identifier):
|
|
3378
|
+
fi = self._func_info_map.get(node.callee.name)
|
|
1871
3379
|
if fi is None or fi.node is None:
|
|
1872
3380
|
continue
|
|
1873
3381
|
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
@@ -1876,18 +3384,72 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1876
3384
|
}
|
|
1877
3385
|
if not series_param_indices:
|
|
1878
3386
|
continue
|
|
1879
|
-
|
|
3387
|
+
if method_call:
|
|
3388
|
+
args = [
|
|
3389
|
+
node.callee.object,
|
|
3390
|
+
*actual_args_for(
|
|
3391
|
+
node, list(fi.node.params[1:]), method_info=fi
|
|
3392
|
+
),
|
|
3393
|
+
]
|
|
3394
|
+
else:
|
|
3395
|
+
args = actual_args_for(node, list(fi.node.params))
|
|
1880
3396
|
for idx, arg in enumerate(args):
|
|
1881
3397
|
if idx not in series_param_indices:
|
|
1882
3398
|
continue
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
3399
|
+
for context in self._inline_history_contexts_for_owner(owner):
|
|
3400
|
+
target_cs = target_cs_for_context(
|
|
3401
|
+
fi, node, owner, context
|
|
3402
|
+
)
|
|
3403
|
+
expected_cpp_type = self._series_param_element_cpp_type(
|
|
3404
|
+
fi, idx, target_cs
|
|
3405
|
+
)
|
|
3406
|
+
needs_bridge = True
|
|
3407
|
+
if isinstance(arg, Identifier):
|
|
3408
|
+
if (
|
|
3409
|
+
(
|
|
3410
|
+
arg.name in BAR_FIELDS
|
|
3411
|
+
or arg.name in BAR_SERIES_PUSH
|
|
3412
|
+
)
|
|
3413
|
+
and expected_cpp_type == "double"
|
|
3414
|
+
):
|
|
3415
|
+
needs_bridge = False
|
|
3416
|
+
owner_info = self._func_info_map.get(owner or "")
|
|
3417
|
+
owner_series_cpp_type = None
|
|
3418
|
+
if (
|
|
3419
|
+
owner_info is not None
|
|
3420
|
+
and owner_info.node is not None
|
|
3421
|
+
and arg.name in owner_info.node.params
|
|
3422
|
+
and arg.name in self.ctx.func_series_vars.get(
|
|
3423
|
+
owner_info.name, set()
|
|
3424
|
+
)
|
|
3425
|
+
):
|
|
3426
|
+
owner_index = owner_info.node.params.index(
|
|
3427
|
+
arg.name
|
|
3428
|
+
)
|
|
3429
|
+
owner_series_cpp_type = (
|
|
3430
|
+
self._series_param_element_cpp_type(
|
|
3431
|
+
owner_info,
|
|
3432
|
+
owner_index,
|
|
3433
|
+
owner_cs_for_context(owner, context),
|
|
3434
|
+
)
|
|
3435
|
+
)
|
|
3436
|
+
if owner_series_cpp_type == expected_cpp_type:
|
|
3437
|
+
needs_bridge = False
|
|
3438
|
+
if (
|
|
3439
|
+
owner_series_cpp_type is None
|
|
3440
|
+
and arg.name in self.ctx.series_vars
|
|
3441
|
+
and arg.name not in ambiguous_series_names
|
|
3442
|
+
and self._series_type_for(arg.name)
|
|
3443
|
+
== expected_cpp_type
|
|
3444
|
+
):
|
|
3445
|
+
needs_bridge = False
|
|
3446
|
+
if needs_bridge:
|
|
3447
|
+
register_one(
|
|
3448
|
+
"series_arg",
|
|
3449
|
+
(id(node), idx),
|
|
3450
|
+
expected_cpp_type,
|
|
3451
|
+
context,
|
|
3452
|
+
)
|
|
1891
3453
|
|
|
1892
3454
|
def _inline_history_member(self, kind: str, node: ASTNode,
|
|
1893
3455
|
arg_idx: int | None = None) -> str:
|
|
@@ -1902,12 +3464,153 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1902
3464
|
)
|
|
1903
3465
|
return member
|
|
1904
3466
|
|
|
3467
|
+
@staticmethod
|
|
3468
|
+
def _allocate_generated_cpp_name(preferred: str, used: set[str]) -> str:
|
|
3469
|
+
"""Reserve one deterministic generated C++ identifier.
|
|
3470
|
+
|
|
3471
|
+
Pine permits identifiers beginning with underscores, so generated
|
|
3472
|
+
support names cannot rely on a magic prefix being unavailable to the
|
|
3473
|
+
author. Preserve the historical spelling when it is free and append
|
|
3474
|
+
a stable suffix only for an actual collision.
|
|
3475
|
+
"""
|
|
3476
|
+
candidate = preferred
|
|
3477
|
+
suffix = 2
|
|
3478
|
+
while candidate in used:
|
|
3479
|
+
candidate = f"{preferred}__pf{suffix}"
|
|
3480
|
+
suffix += 1
|
|
3481
|
+
used.add(candidate)
|
|
3482
|
+
return candidate
|
|
3483
|
+
|
|
3484
|
+
def _prepare_udt_generated_names(self) -> None:
|
|
3485
|
+
"""Allocate UDT support symbols outside every authored namespace.
|
|
3486
|
+
|
|
3487
|
+
Record/trait/template names live at generated-file scope, while arena
|
|
3488
|
+
instances live in ``GeneratedStrategy``. This pass runs only after
|
|
3489
|
+
callable clones, declaration-site flags and inline-history members have
|
|
3490
|
+
their final names. Keep one complete class-scope inventory so adding a
|
|
3491
|
+
UDT member cannot make ``_func_safe_name`` change after those prepasses,
|
|
3492
|
+
then extend the file-scope inventory with the same names because an
|
|
3493
|
+
unqualified class member, parameter or local can hide a generated
|
|
3494
|
+
support type. In the ordinary non-colliding case the preferred names
|
|
3495
|
+
remain byte-for-byte unchanged.
|
|
3496
|
+
"""
|
|
3497
|
+
class_used: set[str] = set(
|
|
3498
|
+
getattr(self, "_runtime_var_init_flag_used_names", ())
|
|
3499
|
+
)
|
|
3500
|
+
class_used.update(self._all_member_names)
|
|
3501
|
+
class_used.update(
|
|
3502
|
+
self._safe_name(name) for name in self._all_bound_names
|
|
3503
|
+
)
|
|
3504
|
+
class_used.update(
|
|
3505
|
+
self._safe_name(name)
|
|
3506
|
+
for name, _ptype in self.ctx.global_var_decls
|
|
3507
|
+
)
|
|
3508
|
+
for func_info in self.ctx.func_infos:
|
|
3509
|
+
node = getattr(func_info, "node", None)
|
|
3510
|
+
if node is not None:
|
|
3511
|
+
class_used.update(
|
|
3512
|
+
self._safe_name(name)
|
|
3513
|
+
for name in (getattr(node, "params", ()) or ())
|
|
3514
|
+
)
|
|
3515
|
+
|
|
3516
|
+
# These generated members are not all recorded in
|
|
3517
|
+
# ``_runtime_var_init_flag_used_names``. Reserve them explicitly so
|
|
3518
|
+
# the inventory remains authoritative even for scripts without a
|
|
3519
|
+
# runtime scalar ``var`` initializer.
|
|
3520
|
+
class_used.update(
|
|
3521
|
+
remapped
|
|
3522
|
+
for remap in self._func_cs_ta_remap.values()
|
|
3523
|
+
for remapped in remap.values()
|
|
3524
|
+
)
|
|
3525
|
+
class_used.update(
|
|
3526
|
+
site.member_name
|
|
3527
|
+
for index, site in enumerate(self.ctx.ta_call_sites)
|
|
3528
|
+
if index not in self._dead_ta_indices
|
|
3529
|
+
)
|
|
3530
|
+
class_used.update(
|
|
3531
|
+
f"_precalc_{site.member_name}"
|
|
3532
|
+
for index, site in enumerate(self.ctx.ta_call_sites)
|
|
3533
|
+
if index not in self._dead_ta_indices
|
|
3534
|
+
and self._ta_site_uses_precalc(site)
|
|
3535
|
+
)
|
|
3536
|
+
class_used.update(
|
|
3537
|
+
site.member_name
|
|
3538
|
+
for index, site in enumerate(self.ctx.fixnan_sites)
|
|
3539
|
+
if index not in self._dead_fixnan_indices
|
|
3540
|
+
)
|
|
3541
|
+
class_used.update(
|
|
3542
|
+
info["member_name"] for info in self._inline_history_members
|
|
3543
|
+
)
|
|
3544
|
+
class_used.update(
|
|
3545
|
+
variant["member_name"]
|
|
3546
|
+
for info in self._security_eval_info
|
|
3547
|
+
for variants in (info.get("ta_variants") or {}).values()
|
|
3548
|
+
for variant in variants
|
|
3549
|
+
)
|
|
3550
|
+
for instance in self._fresh_instances:
|
|
3551
|
+
class_used.add(instance["name"])
|
|
3552
|
+
for remap_name in ("ta_remap", "var_remap", "fixnan_remap"):
|
|
3553
|
+
class_used.update(instance.get(remap_name, {}).values())
|
|
3554
|
+
|
|
3555
|
+
global_used: set[str] = set(self._udt_defs)
|
|
3556
|
+
global_used.update(self._enum_defs)
|
|
3557
|
+
global_used.update(
|
|
3558
|
+
f"{enum_name}_{member}"
|
|
3559
|
+
for enum_name, members in self._enum_defs.items()
|
|
3560
|
+
for member in members
|
|
3561
|
+
)
|
|
3562
|
+
global_used.update(
|
|
3563
|
+
f"{enum_name}_str_values"
|
|
3564
|
+
for enum_name, members in self._enum_defs.items()
|
|
3565
|
+
if self._enum_member_strings.get(enum_name)
|
|
3566
|
+
and len(self._enum_member_strings[enum_name]) == len(members)
|
|
3567
|
+
)
|
|
3568
|
+
global_used.update(class_used)
|
|
3569
|
+
|
|
3570
|
+
self._udt_arena_template_cpp_name = self._allocate_generated_cpp_name(
|
|
3571
|
+
"_PFUdtArena", global_used
|
|
3572
|
+
)
|
|
3573
|
+
self._udt_undo_coordinator_cpp_name = self._allocate_generated_cpp_name(
|
|
3574
|
+
"_PFUdtUndoCoordinator", global_used
|
|
3575
|
+
)
|
|
3576
|
+
self._checkpoint_traits_cpp_name = self._allocate_generated_cpp_name(
|
|
3577
|
+
"_PFCheckpointTraits", global_used
|
|
3578
|
+
)
|
|
3579
|
+
self._udt_record_cpp_names: dict[str, str] = {}
|
|
3580
|
+
for type_name in self._udt_defs:
|
|
3581
|
+
self._udt_record_cpp_names[type_name] = (
|
|
3582
|
+
self._allocate_generated_cpp_name(
|
|
3583
|
+
f"_PFUdtRecord_{type_name}", global_used
|
|
3584
|
+
)
|
|
3585
|
+
)
|
|
3586
|
+
|
|
3587
|
+
member_used = set(class_used)
|
|
3588
|
+
self._udt_undo_coordinator_member_name = self._allocate_generated_cpp_name(
|
|
3589
|
+
"_pf_udt_undo", member_used
|
|
3590
|
+
)
|
|
3591
|
+
self._all_member_names.add(self._udt_undo_coordinator_member_name)
|
|
3592
|
+
self._udt_arena_member_names: dict[str, str] = {}
|
|
3593
|
+
for type_name in self._udt_defs:
|
|
3594
|
+
preferred = f"_pf_udt_{self._safe_name(type_name)}"
|
|
3595
|
+
allocated = self._allocate_generated_cpp_name(
|
|
3596
|
+
preferred, member_used
|
|
3597
|
+
)
|
|
3598
|
+
self._udt_arena_member_names[type_name] = allocated
|
|
3599
|
+
self._all_member_names.add(allocated)
|
|
3600
|
+
|
|
1905
3601
|
def generate(self) -> str:
|
|
1906
3602
|
"""Generate C++ source from the AnalyzerContext."""
|
|
1907
3603
|
# Context-sensitive instance pre-pass (needs the naming helpers populated
|
|
1908
3604
|
# in __init__). Computes nested stateful-helper dispatch + fresh instances.
|
|
1909
3605
|
self._build_func_instances()
|
|
3606
|
+
# Allocate declaration-site once flags only after every natural and
|
|
3607
|
+
# context-sensitive callable-state member name is known.
|
|
3608
|
+
self._prepare_runtime_scalar_var_initializers()
|
|
1910
3609
|
self._prepare_inline_history_members()
|
|
3610
|
+
# UDT support names are the final generated-name category. They must
|
|
3611
|
+
# suffix around every authored/callable identifier already frozen by
|
|
3612
|
+
# the passes above, never force an earlier callable to rename itself.
|
|
3613
|
+
self._prepare_udt_generated_names()
|
|
1911
3614
|
# Pre-scan for strategy series vars
|
|
1912
3615
|
self._prescan_strategy_series()
|
|
1913
3616
|
self._security_ohlc_hist_fields_by_sec: dict[int, set[str]] = {}
|
|
@@ -1917,6 +3620,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1917
3620
|
# request.security helper-call results read at a history offset
|
|
1918
3621
|
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
1919
3622
|
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
3623
|
+
self._prepare_lazy_saturated_roc3_sites()
|
|
1920
3624
|
|
|
1921
3625
|
lines: list[str] = []
|
|
1922
3626
|
|
|
@@ -1927,14 +3631,197 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1927
3631
|
# 1. Includes
|
|
1928
3632
|
self._emit_includes(lines)
|
|
1929
3633
|
|
|
1930
|
-
# 1b.
|
|
1931
|
-
#
|
|
1932
|
-
#
|
|
1933
|
-
#
|
|
1934
|
-
#
|
|
1935
|
-
#
|
|
1936
|
-
|
|
3634
|
+
# 1b. User-defined object handles and backing records.
|
|
3635
|
+
#
|
|
3636
|
+
# Pine UDT values are object IDs: ordinary assignment aliases the same
|
|
3637
|
+
# object, while ``copy()`` allocates one detached outer object and keeps
|
|
3638
|
+
# nested UDT/map/matrix IDs shallow-shared. Direct array-valued fields
|
|
3639
|
+
# still use std::vector value storage, so their built-in UDT copy path
|
|
3640
|
+
# is rejected explicitly until arrays gain handle identity. Emit every
|
|
3641
|
+
# authored type as a small nullable handle first, then its value record.
|
|
3642
|
+
# The two-phase order also permits self/nested UDT fields without
|
|
3643
|
+
# embedding a C++ type recursively by value.
|
|
3644
|
+
for type_name in self._udt_defs:
|
|
1937
3645
|
lines.append(f"struct {type_name} {{")
|
|
3646
|
+
lines.append(" int32_t __pf_id = -1;")
|
|
3647
|
+
lines.append("};")
|
|
3648
|
+
lines.append(
|
|
3649
|
+
f"inline bool is_na(const {type_name}& _z) "
|
|
3650
|
+
"{ return _z.__pf_id < 0; }"
|
|
3651
|
+
)
|
|
3652
|
+
lines.append("")
|
|
3653
|
+
|
|
3654
|
+
if self._udt_defs:
|
|
3655
|
+
lines.extend([
|
|
3656
|
+
"template <typename _PFValue>",
|
|
3657
|
+
f"struct {self._checkpoint_traits_cpp_name};",
|
|
3658
|
+
"",
|
|
3659
|
+
f"class {self._udt_undo_coordinator_cpp_name} {{",
|
|
3660
|
+
" std::vector<std::function<void()>> _pf_undo_;",
|
|
3661
|
+
" uint64_t _pf_generation_ = 0;",
|
|
3662
|
+
" bool _pf_active_ = false;",
|
|
3663
|
+
"public:",
|
|
3664
|
+
" struct Snapshot { uint64_t generation; };",
|
|
3665
|
+
f" {self._udt_undo_coordinator_cpp_name}() = default;",
|
|
3666
|
+
f" {self._udt_undo_coordinator_cpp_name}(",
|
|
3667
|
+
f" const {self._udt_undo_coordinator_cpp_name}&) = delete;",
|
|
3668
|
+
f" {self._udt_undo_coordinator_cpp_name}& operator=(",
|
|
3669
|
+
f" const {self._udt_undo_coordinator_cpp_name}&) = delete;",
|
|
3670
|
+
f" {self._udt_undo_coordinator_cpp_name}(",
|
|
3671
|
+
f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
|
|
3672
|
+
f" {self._udt_undo_coordinator_cpp_name}& operator=(",
|
|
3673
|
+
f" {self._udt_undo_coordinator_cpp_name}&&) = delete;",
|
|
3674
|
+
"",
|
|
3675
|
+
" Snapshot snapshot() {",
|
|
3676
|
+
" if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
|
|
3677
|
+
' throw std::overflow_error("UDT checkpoint generation exhausted");',
|
|
3678
|
+
" }",
|
|
3679
|
+
" ++_pf_generation_;",
|
|
3680
|
+
" _pf_undo_.clear();",
|
|
3681
|
+
" _pf_active_ = true;",
|
|
3682
|
+
" return Snapshot{_pf_generation_};",
|
|
3683
|
+
" }",
|
|
3684
|
+
" uint64_t generation() const { return _pf_generation_; }",
|
|
3685
|
+
" bool active() const { return _pf_active_; }",
|
|
3686
|
+
" bool empty() const { return _pf_undo_.empty(); }",
|
|
3687
|
+
" void record(uint64_t generation, std::function<void()> undo) {",
|
|
3688
|
+
" if (!_pf_active_ || generation != _pf_generation_) {",
|
|
3689
|
+
' throw std::runtime_error("invalid UDT undo generation");',
|
|
3690
|
+
" }",
|
|
3691
|
+
" _pf_undo_.push_back(std::move(undo));",
|
|
3692
|
+
" }",
|
|
3693
|
+
" void restore(const Snapshot& snapshot) {",
|
|
3694
|
+
" if (!_pf_active_ || snapshot.generation != _pf_generation_) {",
|
|
3695
|
+
' throw std::runtime_error("invalid UDT coordinator checkpoint token");',
|
|
3696
|
+
" }",
|
|
3697
|
+
" for (auto entry = _pf_undo_.rbegin();",
|
|
3698
|
+
" entry != _pf_undo_.rend(); ++entry) (*entry)();",
|
|
3699
|
+
" _pf_undo_.clear();",
|
|
3700
|
+
" }",
|
|
3701
|
+
"};",
|
|
3702
|
+
"",
|
|
3703
|
+
"template <typename _PFHandle, typename _PFRecord>",
|
|
3704
|
+
f"class {self._udt_arena_template_cpp_name} {{",
|
|
3705
|
+
f" using _PFRecordTraits = {self._checkpoint_traits_cpp_name}<_PFRecord>;",
|
|
3706
|
+
" using _PFRecordSnapshot = typename _PFRecordTraits::snapshot_type;",
|
|
3707
|
+
" struct _PFSlot {",
|
|
3708
|
+
" _PFRecord value;",
|
|
3709
|
+
" uint64_t logged_generation = 0;",
|
|
3710
|
+
" };",
|
|
3711
|
+
" std::deque<_PFSlot> _pf_records_;",
|
|
3712
|
+
f" {self._udt_undo_coordinator_cpp_name}* _pf_coordinator_;",
|
|
3713
|
+
" std::size_t _pf_checkpoint_size_ = 0;",
|
|
3714
|
+
" uint64_t _pf_checkpoint_generation_ = 0;",
|
|
3715
|
+
" bool _pf_checkpoint_active_ = false;",
|
|
3716
|
+
"",
|
|
3717
|
+
" void capture(std::size_t index) {",
|
|
3718
|
+
" if (!_pf_checkpoint_active_) return;",
|
|
3719
|
+
" auto& slot = _pf_records_.at(index);",
|
|
3720
|
+
" if (slot.logged_generation",
|
|
3721
|
+
" == _pf_checkpoint_generation_) return;",
|
|
3722
|
+
" auto snapshot = _PFRecordTraits::take(slot.value);",
|
|
3723
|
+
" _pf_coordinator_->record(_pf_checkpoint_generation_,",
|
|
3724
|
+
" [this, index, snapshot = std::move(snapshot)]() mutable {",
|
|
3725
|
+
" auto& restore_slot = _pf_records_.at(index);",
|
|
3726
|
+
" _PFRecordTraits::restore(restore_slot.value, snapshot);",
|
|
3727
|
+
" restore_slot.logged_generation = 0;",
|
|
3728
|
+
" });",
|
|
3729
|
+
" slot.logged_generation = _pf_checkpoint_generation_;",
|
|
3730
|
+
" }",
|
|
3731
|
+
"public:",
|
|
3732
|
+
" struct Snapshot {",
|
|
3733
|
+
" uint64_t generation;",
|
|
3734
|
+
" std::size_t size;",
|
|
3735
|
+
" };",
|
|
3736
|
+
"",
|
|
3737
|
+
f" explicit {self._udt_arena_template_cpp_name}(",
|
|
3738
|
+
f" {self._udt_undo_coordinator_cpp_name}* coordinator)",
|
|
3739
|
+
" : _pf_coordinator_(coordinator) {",
|
|
3740
|
+
" if (!_pf_coordinator_)",
|
|
3741
|
+
' throw std::invalid_argument("UDT arena requires undo coordinator");',
|
|
3742
|
+
" }",
|
|
3743
|
+
f" {self._udt_arena_template_cpp_name}(",
|
|
3744
|
+
f" const {self._udt_arena_template_cpp_name}&) = delete;",
|
|
3745
|
+
f" {self._udt_arena_template_cpp_name}& operator=(",
|
|
3746
|
+
f" const {self._udt_arena_template_cpp_name}&) = delete;",
|
|
3747
|
+
f" {self._udt_arena_template_cpp_name}(",
|
|
3748
|
+
f" {self._udt_arena_template_cpp_name}&&) = delete;",
|
|
3749
|
+
f" {self._udt_arena_template_cpp_name}& operator=(",
|
|
3750
|
+
f" {self._udt_arena_template_cpp_name}&&) = delete;",
|
|
3751
|
+
"",
|
|
3752
|
+
" _PFHandle create(_PFRecord value) {",
|
|
3753
|
+
" if (_pf_records_.size() > static_cast<std::size_t>(",
|
|
3754
|
+
" std::numeric_limits<int32_t>::max())) {",
|
|
3755
|
+
' throw std::length_error("UDT object-ID capacity exceeded");',
|
|
3756
|
+
" }",
|
|
3757
|
+
" const auto id = static_cast<int32_t>(_pf_records_.size());",
|
|
3758
|
+
" _pf_records_.push_back(_PFSlot{std::move(value), 0});",
|
|
3759
|
+
" return _PFHandle{id};",
|
|
3760
|
+
" }",
|
|
3761
|
+
" _PFHandle copy(_PFHandle value) {",
|
|
3762
|
+
" return create(static_cast<const "
|
|
3763
|
+
f"{self._udt_arena_template_cpp_name}&>(*this).get(value));",
|
|
3764
|
+
" }",
|
|
3765
|
+
" _PFRecord& get(_PFHandle value) {",
|
|
3766
|
+
" if (value.__pf_id < 0",
|
|
3767
|
+
" || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
|
|
3768
|
+
' throw std::runtime_error("UDT access on na or invalid object ID");',
|
|
3769
|
+
" }",
|
|
3770
|
+
" const auto index = static_cast<std::size_t>(value.__pf_id);",
|
|
3771
|
+
" capture(index);",
|
|
3772
|
+
" return _pf_records_[index].value;",
|
|
3773
|
+
" }",
|
|
3774
|
+
" const _PFRecord& get(_PFHandle value) const {",
|
|
3775
|
+
" if (value.__pf_id < 0",
|
|
3776
|
+
" || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
|
|
3777
|
+
' throw std::runtime_error("UDT access on na or invalid object ID");',
|
|
3778
|
+
" }",
|
|
3779
|
+
" return _pf_records_[static_cast<std::size_t>(value.__pf_id)].value;",
|
|
3780
|
+
" }",
|
|
3781
|
+
" const _PFRecord& read(_PFHandle value) const {",
|
|
3782
|
+
" return get(value);",
|
|
3783
|
+
" }",
|
|
3784
|
+
" std::size_t size() const { return _pf_records_.size(); }",
|
|
3785
|
+
" _PFRecord& record_at(std::size_t index) {",
|
|
3786
|
+
" capture(index);",
|
|
3787
|
+
" return _pf_records_.at(index).value;",
|
|
3788
|
+
" }",
|
|
3789
|
+
" const _PFRecord& record_at(std::size_t index) const {",
|
|
3790
|
+
" return _pf_records_.at(index).value;",
|
|
3791
|
+
" }",
|
|
3792
|
+
" Snapshot snapshot() {",
|
|
3793
|
+
" if (!_pf_coordinator_->active()) {",
|
|
3794
|
+
' throw std::runtime_error("UDT coordinator checkpoint is not active");',
|
|
3795
|
+
" }",
|
|
3796
|
+
" _pf_checkpoint_generation_ = _pf_coordinator_->generation();",
|
|
3797
|
+
" _pf_checkpoint_size_ = _pf_records_.size();",
|
|
3798
|
+
" _pf_checkpoint_active_ = true;",
|
|
3799
|
+
" return Snapshot{_pf_checkpoint_generation_,",
|
|
3800
|
+
" _pf_checkpoint_size_};",
|
|
3801
|
+
" }",
|
|
3802
|
+
" void restore(const Snapshot& snapshot) {",
|
|
3803
|
+
" if (!_pf_checkpoint_active_",
|
|
3804
|
+
" || snapshot.generation != _pf_checkpoint_generation_",
|
|
3805
|
+
" || snapshot.generation != _pf_coordinator_->generation()",
|
|
3806
|
+
" || snapshot.size != _pf_checkpoint_size_",
|
|
3807
|
+
" || _pf_records_.size() < snapshot.size",
|
|
3808
|
+
" || !_pf_coordinator_->empty()) {",
|
|
3809
|
+
' throw std::runtime_error("invalid UDT checkpoint token");',
|
|
3810
|
+
" }",
|
|
3811
|
+
" _pf_records_.resize(snapshot.size);",
|
|
3812
|
+
" }",
|
|
3813
|
+
"};",
|
|
3814
|
+
"",
|
|
3815
|
+
])
|
|
3816
|
+
|
|
3817
|
+
# Drawing field names per record are pre-computed in __init__ as
|
|
3818
|
+
# ``self._udt_omitted_fields`` so visit_expr / visit_stmt can consult
|
|
3819
|
+
# the same map. Authored field defaults are evaluated by ``Type.new``
|
|
3820
|
+
# at the call site; record declarations use only inert typed defaults so
|
|
3821
|
+
# bar/TA expressions never leak into namespace-scope C++ initializers.
|
|
3822
|
+
for type_name, fields in self._udt_defs.items():
|
|
3823
|
+
record_type = self._udt_record_cpp_type(type_name)
|
|
3824
|
+
lines.append(f"struct {record_type} {{")
|
|
1938
3825
|
field_specs = self._udt_field_type_specs.get(type_name, {})
|
|
1939
3826
|
omitted = self._udt_omitted_fields.get(type_name, set())
|
|
1940
3827
|
for f in fields:
|
|
@@ -1942,28 +3829,16 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1942
3829
|
continue
|
|
1943
3830
|
spec = field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name)
|
|
1944
3831
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
1945
|
-
# Pine ``int`` is 64-bit (it routinely holds UNIX-ms timestamps
|
|
1946
|
-
# and large bar indices); emit UDT int fields as ``int64_t`` so a
|
|
1947
|
-
# field initialised from ``time``/``current_bar_.timestamp`` does
|
|
1948
|
-
# not truncate / narrow-init.
|
|
1949
3832
|
if cpp_type == "int":
|
|
1950
3833
|
cpp_type = "int64_t"
|
|
1951
|
-
|
|
1952
|
-
default = self._visit_expr(f.default)
|
|
1953
|
-
else:
|
|
1954
|
-
default = self._default_for_spec(spec)
|
|
3834
|
+
default = self._default_for_spec(spec)
|
|
1955
3835
|
lines.append(f" {cpp_type} {f.name} = {default};")
|
|
1956
|
-
# NA sentinel (always the last data member). A default-constructed
|
|
1957
|
-
# UDT - ``var T x = na``, an array fill slot, ``T.copy()`` no-arg -
|
|
1958
|
-
# is na; the ``T.new(...)`` lowering sets this false. This lets
|
|
1959
|
-
# ``na(udtVar)`` lower to the ``is_na(const T&)`` overload below
|
|
1960
|
-
# instead of failing because no ``is_na`` accepts a struct.
|
|
1961
|
-
lines.append(f" bool __pf_na = true;")
|
|
1962
|
-
lines.append(f" static {type_name} create() {{ return {type_name}{{}}; }}")
|
|
1963
3836
|
lines.append("};")
|
|
1964
|
-
lines.append(f"inline bool is_na(const {type_name}& _z) {{ return _z.__pf_na; }}")
|
|
1965
3837
|
lines.append("")
|
|
1966
3838
|
|
|
3839
|
+
if self._uses_map or self._uses_matrix or self._udt_defs:
|
|
3840
|
+
self._emit_handle_checkpoint_traits(lines)
|
|
3841
|
+
|
|
1967
3842
|
# 1c. Enum constants + string tables for str.tostring(enumVar)
|
|
1968
3843
|
for enum_name, members in self._enum_defs.items():
|
|
1969
3844
|
for i, member in enumerate(members):
|
|
@@ -1978,15 +3853,41 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1978
3853
|
)
|
|
1979
3854
|
lines.append("")
|
|
1980
3855
|
|
|
3856
|
+
# Source-shaped lazy ROC call clocks are generated support types, not
|
|
3857
|
+
# script state themselves. Their per-callsite instances are declared
|
|
3858
|
+
# below inside GeneratedStrategy and therefore join the automatic COOF
|
|
3859
|
+
# checkpoint inventory.
|
|
3860
|
+
self._emit_lazy_saturated_roc3_helper(lines)
|
|
3861
|
+
|
|
1981
3862
|
# 2. Open class
|
|
1982
3863
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
1983
3864
|
lines.append("public:")
|
|
1984
3865
|
_script_state_decl_start = len(lines)
|
|
3866
|
+
|
|
3867
|
+
# One strategy-local undo coordinator gives every UDT arena a single
|
|
3868
|
+
# reverse first-touch order. This is required when records from distinct
|
|
3869
|
+
# authored UDT types shallow-share the same map or matrix identity.
|
|
3870
|
+
if self._udt_defs:
|
|
3871
|
+
lines.append(
|
|
3872
|
+
f" {self._udt_undo_coordinator_cpp_name} "
|
|
3873
|
+
f"{self._udt_undo_coordinator_member_name};"
|
|
3874
|
+
)
|
|
3875
|
+
|
|
3876
|
+
# Per-type object stores precede every handle-bearing script member.
|
|
3877
|
+
# COOF rollback therefore replays the coordinator, truncates arenas,
|
|
3878
|
+
# then rebinds ordinary variables, arrays and matrices in that order.
|
|
3879
|
+
for type_name in self._udt_defs:
|
|
3880
|
+
lines.append(
|
|
3881
|
+
f" {self._udt_arena_cpp_type(type_name)} "
|
|
3882
|
+
f"{self._udt_arena_member_name(type_name)}"
|
|
3883
|
+
f"{{&{self._udt_undo_coordinator_member_name}}};"
|
|
3884
|
+
)
|
|
1985
3885
|
|
|
1986
3886
|
# request.security state
|
|
1987
3887
|
for item in self._security_calls:
|
|
1988
3888
|
sec_id = item["sec_id"]
|
|
1989
3889
|
expr_node = item["expr_node"]
|
|
3890
|
+
self._validate_security_persistent_var_control_flow(expr_node)
|
|
1990
3891
|
returns_tuple = item.get("returns_tuple", False)
|
|
1991
3892
|
tuple_size = item.get("tuple_size", 0)
|
|
1992
3893
|
if item.get("is_lower_tf_array"):
|
|
@@ -2036,15 +3937,33 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2036
3937
|
if ctype == "std::vector<double>":
|
|
2037
3938
|
lines.append(f" {ctype} _req_sec_{sec_id}_{i}{{}};")
|
|
2038
3939
|
else:
|
|
2039
|
-
|
|
3940
|
+
default = {
|
|
3941
|
+
"double": "na<double>()",
|
|
3942
|
+
"bool": "false",
|
|
3943
|
+
"int": "0",
|
|
3944
|
+
"std::string": 'std::string("")',
|
|
3945
|
+
}.get(ctype, self._default_for_type(ctype))
|
|
3946
|
+
lines.append(
|
|
3947
|
+
f" {ctype} _req_sec_{sec_id}_{i} = {default};"
|
|
3948
|
+
)
|
|
2040
3949
|
elif returns_tuple and tuple_size and tuple_size > 0:
|
|
2041
3950
|
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
2042
3951
|
self._collect_security_ohlc_hist_fields_for_call(item)
|
|
2043
3952
|
)
|
|
2044
3953
|
site = self._get_ta_site(expr_node)
|
|
2045
3954
|
ta_name = self._ta_name_from_site(site) if site is not None else ""
|
|
2046
|
-
ctype = TA_TUPLE_RESULT_TYPES.get(
|
|
2047
|
-
|
|
3955
|
+
ctype = TA_TUPLE_RESULT_TYPES.get(
|
|
3956
|
+
ta_name,
|
|
3957
|
+
self._security_helper_tuple_cpp_type(
|
|
3958
|
+
tuple_size,
|
|
3959
|
+
item.get("tuple_element_types", ()),
|
|
3960
|
+
),
|
|
3961
|
+
)
|
|
3962
|
+
default = self._security_tuple_result_default(
|
|
3963
|
+
ctype,
|
|
3964
|
+
tuple_size,
|
|
3965
|
+
item.get("tuple_element_types", ()),
|
|
3966
|
+
)
|
|
2048
3967
|
lines.append(f" {ctype} _req_sec_{sec_id} = {default};")
|
|
2049
3968
|
else:
|
|
2050
3969
|
self._security_ohlc_hist_fields_by_sec[sec_id] = (
|
|
@@ -2094,6 +4013,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2094
4013
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
2095
4014
|
lines.append(" bool _use_precalc = false;")
|
|
2096
4015
|
|
|
4016
|
+
for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
|
|
4017
|
+
lines.append(
|
|
4018
|
+
f" {self._lazy_saturated_roc3_type_name} {clock_name};"
|
|
4019
|
+
)
|
|
4020
|
+
if self._lazy_saturated_roc3_clock_by_node:
|
|
4021
|
+
# Dedicated eager close[3] fallback. Its fixed four-slot capacity
|
|
4022
|
+
# is independent of the user's max_bars_back directive, which may
|
|
4023
|
+
# legitimately be smaller than the offset this generated route
|
|
4024
|
+
# requires. It is ordinary copyable script state and therefore
|
|
4025
|
+
# joins the automatic COOF checkpoint below.
|
|
4026
|
+
lines.append(
|
|
4027
|
+
" Series<double> "
|
|
4028
|
+
f"{self._lazy_saturated_roc3_history_name}{{4}};"
|
|
4029
|
+
)
|
|
4030
|
+
|
|
2097
4031
|
# Security evaluator TA members (cloned from expression dependencies)
|
|
2098
4032
|
# Skip for user function call expressions — their TA deps are internal to the function
|
|
2099
4033
|
for info in self._security_eval_info:
|
|
@@ -2113,6 +4047,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2113
4047
|
continue
|
|
2114
4048
|
seen_var_members.add(name)
|
|
2115
4049
|
safe = self._safe_name(name)
|
|
4050
|
+
callable_collection_spec = (
|
|
4051
|
+
None
|
|
4052
|
+
if name in self._global_collection_types
|
|
4053
|
+
else self._callable_var_collection_spec(name)
|
|
4054
|
+
)
|
|
4055
|
+
if callable_collection_spec is not None:
|
|
4056
|
+
lines.append(
|
|
4057
|
+
f" {self._type_spec_to_cpp(callable_collection_spec)} {safe};"
|
|
4058
|
+
)
|
|
4059
|
+
continue
|
|
4060
|
+
callable_udt_spec = self._callable_var_udt_spec(name)
|
|
4061
|
+
if callable_udt_spec is not None:
|
|
4062
|
+
lines.append(
|
|
4063
|
+
f" {self._type_spec_to_cpp(callable_udt_spec)} {safe};"
|
|
4064
|
+
)
|
|
4065
|
+
continue
|
|
2116
4066
|
# Detect array vars from init expression. Guard the substring
|
|
2117
4067
|
# heuristic against a UDT constructor that merely WRAPS array.new /
|
|
2118
4068
|
# array.from in its arguments — e.g.
|
|
@@ -2123,12 +4073,24 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2123
4073
|
_is_udt_ctor_init = any(
|
|
2124
4074
|
_init_str_s.startswith(f"{u}.new") for u in self._udt_defs
|
|
2125
4075
|
)
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
4076
|
+
exact_member_spec = self._global_collection_types.get(name)
|
|
4077
|
+
exact_array_member = (
|
|
4078
|
+
exact_member_spec is not None
|
|
4079
|
+
and exact_member_spec.kind == "array"
|
|
4080
|
+
)
|
|
4081
|
+
if exact_array_member or (
|
|
4082
|
+
not _is_udt_ctor_init
|
|
4083
|
+
and (
|
|
4084
|
+
"array.new" in _init_str_s
|
|
4085
|
+
or "array.from" in _init_str_s
|
|
4086
|
+
or name in self._array_vars
|
|
4087
|
+
)
|
|
2129
4088
|
):
|
|
2130
4089
|
self._array_vars.add(name)
|
|
2131
|
-
|
|
4090
|
+
array_spec = exact_member_spec or self._array_spec_for_name(name)
|
|
4091
|
+
lines.append(
|
|
4092
|
+
f" {self._type_spec_to_cpp(array_spec)} {safe};"
|
|
4093
|
+
)
|
|
2132
4094
|
continue
|
|
2133
4095
|
# Detect matrix vars from init expression OR from the set
|
|
2134
4096
|
# populated by ``_register_global_aggregate_member_types``
|
|
@@ -2136,7 +4098,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2136
4098
|
# not just ``matrix.new``).
|
|
2137
4099
|
if name in self._matrix_specs:
|
|
2138
4100
|
pass # already registered upstream
|
|
2139
|
-
elif "matrix.new" in str(init_str):
|
|
4101
|
+
elif not _is_udt_ctor_init and "matrix.new" in str(init_str):
|
|
2140
4102
|
self._matrix_specs[name] = TypeSpec.matrix(TypeSpec.primitive("float"))
|
|
2141
4103
|
self._collection_types[name] = self._matrix_specs[name]
|
|
2142
4104
|
if name in self._matrix_specs:
|
|
@@ -2145,9 +4107,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2145
4107
|
if "ta.pivot_point_levels" in str(init_str):
|
|
2146
4108
|
lines.append(f" std::vector<double> {safe};")
|
|
2147
4109
|
continue
|
|
2148
|
-
|
|
4110
|
+
exact_map_member = (
|
|
4111
|
+
exact_member_spec is not None
|
|
4112
|
+
and exact_member_spec.kind == "map"
|
|
4113
|
+
)
|
|
4114
|
+
if exact_map_member or (
|
|
4115
|
+
not _is_udt_ctor_init
|
|
4116
|
+
and ("map.new" in str(init_str) or name in self._map_vars)
|
|
4117
|
+
):
|
|
2149
4118
|
self._map_vars.add(name)
|
|
2150
|
-
|
|
4119
|
+
map_spec = exact_member_spec or self._map_spec_for_name(name)
|
|
4120
|
+
lines.append(f" {self._type_spec_to_cpp(map_spec)} {safe};")
|
|
2151
4121
|
continue
|
|
2152
4122
|
# Detect UDT vars. Two signals: (1) the analyzer recorded an
|
|
2153
4123
|
# explicit UDT type annotation in ``_udt_var_types`` - this is the
|
|
@@ -2161,14 +4131,18 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2161
4131
|
# C++ handle struct (Series<Line> when also history-referenced).
|
|
2162
4132
|
# Drawing names are NOT in _udt_defs, so the udt branch below would
|
|
2163
4133
|
# self-zero them to double; handle them first.
|
|
2164
|
-
|
|
4134
|
+
member_udt_type = self._member_udt_type(name)
|
|
4135
|
+
_draw_cpp = (
|
|
4136
|
+
self._drawing_var_member_cpp_types.get(name)
|
|
4137
|
+
or DRAWING_TYPE_TO_CPP.get(member_udt_type)
|
|
4138
|
+
)
|
|
2165
4139
|
if _draw_cpp is not None:
|
|
2166
|
-
if
|
|
4140
|
+
if safe in self._series_var_member_names:
|
|
2167
4141
|
lines.append(f" Series<{_draw_cpp}> {safe}{_mbb};")
|
|
2168
4142
|
else:
|
|
2169
4143
|
lines.append(f" {_draw_cpp} {safe};")
|
|
2170
4144
|
continue
|
|
2171
|
-
udt_type =
|
|
4145
|
+
udt_type = member_udt_type
|
|
2172
4146
|
if udt_type not in self._udt_defs:
|
|
2173
4147
|
udt_type = None
|
|
2174
4148
|
if udt_type is None:
|
|
@@ -2184,14 +4158,35 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2184
4158
|
# (time/time_close/timestamp), otherwise the na sentinel narrows.
|
|
2185
4159
|
if cpp_type == "int" and self._is_int64_builtin_init(name):
|
|
2186
4160
|
cpp_type = "int64_t"
|
|
2187
|
-
|
|
4161
|
+
# Persistent declarations have collision-safe exact member names
|
|
4162
|
+
# (for example ``x`` and ``x__blk1`` for sibling lexical blocks).
|
|
4163
|
+
# ``ctx.series_vars`` is only the legacy raw-name union, so using it
|
|
4164
|
+
# here can invert the storage types: the unreferenced sibling gets
|
|
4165
|
+
# Series<T> while the exact history-referenced sibling stays a
|
|
4166
|
+
# scalar. Exact analyzer identity must win for every persistent
|
|
4167
|
+
# primitive, just as it already does for drawing handles above.
|
|
4168
|
+
if safe in self._series_var_member_names:
|
|
2188
4169
|
lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
|
|
2189
4170
|
else:
|
|
2190
|
-
|
|
4171
|
+
if name in self._runtime_scalar_var_init_members:
|
|
4172
|
+
# A conditional declaration may not execute for many bars,
|
|
4173
|
+
# while COOF rollback still value-copies every member. Give
|
|
4174
|
+
# pending runtime vars a typed Pine-na sentinel so that copy
|
|
4175
|
+
# is always defined; the declaration-site guard overwrites
|
|
4176
|
+
# it on the first actual execution.
|
|
4177
|
+
pending = self._typed_na_init("na<double>()", name, ptype)
|
|
4178
|
+
lines.append(f" {cpp_type} {safe} = {pending};")
|
|
4179
|
+
else:
|
|
4180
|
+
lines.append(f" {cpp_type} {safe};")
|
|
2191
4181
|
|
|
2192
4182
|
# 6. Non-var series vars
|
|
2193
4183
|
for name in sorted(self.ctx.series_vars):
|
|
2194
|
-
if name not in self._var_names
|
|
4184
|
+
if (name not in self._var_names
|
|
4185
|
+
or name in self._nonpersistent_series_decl_names):
|
|
4186
|
+
if (name in self._qualified_func_var_raw_names
|
|
4187
|
+
and name in self._ordinary_global_binding_names
|
|
4188
|
+
and name not in self._ordinary_global_series_names):
|
|
4189
|
+
continue
|
|
2195
4190
|
safe = self._safe_name(name)
|
|
2196
4191
|
cpp_type = self._series_type_for(name)
|
|
2197
4192
|
lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
|
|
@@ -2200,7 +4195,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2200
4195
|
for _fi_idx, site in enumerate(self.ctx.fixnan_sites):
|
|
2201
4196
|
if _fi_idx in self._dead_fixnan_indices:
|
|
2202
4197
|
continue
|
|
2203
|
-
cpp_type =
|
|
4198
|
+
cpp_type = self._fixnan_site_cpp_type(site)
|
|
2204
4199
|
lines.append(f" {cpp_type} {site.member_name} = na<{cpp_type}>();")
|
|
2205
4200
|
|
|
2206
4201
|
# 8. Strategy series (e.g., strategy.closedtrades[1])
|
|
@@ -2225,13 +4220,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2225
4220
|
# (so user-defined functions can reference them)
|
|
2226
4221
|
seen_global = set()
|
|
2227
4222
|
for name, ptype in self.ctx.global_var_decls:
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
if name in
|
|
2234
|
-
|
|
4223
|
+
exact_scalar_override = (
|
|
4224
|
+
name in self._qualified_func_var_raw_names
|
|
4225
|
+
and name in self._ordinary_global_binding_names
|
|
4226
|
+
and name not in self._ordinary_global_series_names
|
|
4227
|
+
)
|
|
4228
|
+
if (name in seen_global
|
|
4229
|
+
or (name in self.ctx.series_vars and not exact_scalar_override)
|
|
4230
|
+
or name in self._var_names):
|
|
2235
4231
|
continue
|
|
2236
4232
|
seen_global.add(name)
|
|
2237
4233
|
safe = self._safe_name(name)
|
|
@@ -2246,12 +4242,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2246
4242
|
"localPivots", "securityPivotPointsArray", "pivotPointsArray",
|
|
2247
4243
|
):
|
|
2248
4244
|
lines.append(f" std::vector<double> {safe} = std::vector<double>();")
|
|
2249
|
-
elif name
|
|
4245
|
+
elif self._member_udt_type(name) is not None:
|
|
2250
4246
|
# Non-var global of UDT type — declare as the struct so
|
|
2251
4247
|
# downstream method dispatch works. Probes:
|
|
2252
4248
|
# data/validation/udt-method-probe-19-array-of-udt-method,
|
|
2253
4249
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
2254
|
-
udt_t = self.
|
|
4250
|
+
udt_t = self._member_udt_type(name)
|
|
2255
4251
|
# Drawing handle global (L-N6 / U): map line/box/label/linefill
|
|
2256
4252
|
# to the C++ handle struct (the default is na, id=-1).
|
|
2257
4253
|
_draw_cpp = DRAWING_TYPE_TO_CPP.get(udt_t)
|
|
@@ -2261,7 +4257,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2261
4257
|
lines.append(f" {udt_t} {safe} = {udt_t}{{}};")
|
|
2262
4258
|
else:
|
|
2263
4259
|
expr = self.ctx.global_expr_map.get(name) if hasattr(self.ctx, "global_expr_map") else None
|
|
2264
|
-
|
|
4260
|
+
if (
|
|
4261
|
+
name in self._direct_program_tuple_binding_names
|
|
4262
|
+
and ptype == PineType.BOOL
|
|
4263
|
+
):
|
|
4264
|
+
# request.security homogeneous-bool helper tuples carry
|
|
4265
|
+
# their exact element family in analyzer metadata. The
|
|
4266
|
+
# coarse request.security call expression itself still
|
|
4267
|
+
# reports FLOAT, so prefer that exact binding type here.
|
|
4268
|
+
cpp_type = "bool"
|
|
4269
|
+
else:
|
|
4270
|
+
cpp_type = (
|
|
4271
|
+
self._infer_type(expr)
|
|
4272
|
+
if expr is not None
|
|
4273
|
+
else PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
4274
|
+
)
|
|
2265
4275
|
default = self._default_for_type(cpp_type)
|
|
2266
4276
|
lines.append(f" {cpp_type} {safe} = {default};")
|
|
2267
4277
|
|
|
@@ -2275,16 +4285,20 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2275
4285
|
if cloned_safe in emitted_clones:
|
|
2276
4286
|
continue # already declared by another function's clone
|
|
2277
4287
|
emitted_clones.add(cloned_safe)
|
|
2278
|
-
self._emit_cloned_var_decl(
|
|
4288
|
+
self._emit_cloned_var_decl(
|
|
4289
|
+
orig_safe, cloned_safe, _mbb, lines, owner_func=fname
|
|
4290
|
+
)
|
|
2279
4291
|
|
|
2280
4292
|
# 8c2. Fresh var members for context-sensitive helper instances (nested
|
|
2281
4293
|
# helpers reached through >1 distinct call path). Each fresh instance
|
|
2282
4294
|
# gets its OWN scalar/series state so two paths never collide.
|
|
2283
|
-
for orig_safe, fresh_safe in self._fresh_var_members:
|
|
4295
|
+
for owner_func, orig_safe, fresh_safe in self._fresh_var_members:
|
|
2284
4296
|
if fresh_safe in emitted_clones:
|
|
2285
4297
|
continue
|
|
2286
4298
|
emitted_clones.add(fresh_safe)
|
|
2287
|
-
self._emit_cloned_var_decl(
|
|
4299
|
+
self._emit_cloned_var_decl(
|
|
4300
|
+
orig_safe, fresh_safe, _mbb, lines, owner_func=owner_func
|
|
4301
|
+
)
|
|
2288
4302
|
|
|
2289
4303
|
# 8c3. Fresh fixnan members for context-sensitive helper instances.
|
|
2290
4304
|
# Each fresh instance gets its OWN previous-value member so two
|
|
@@ -2293,7 +4307,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2293
4307
|
if fresh_safe in emitted_clones:
|
|
2294
4308
|
continue
|
|
2295
4309
|
emitted_clones.add(fresh_safe)
|
|
2296
|
-
cpp_type =
|
|
4310
|
+
cpp_type = self._fixnan_site_cpp_type(orig_site)
|
|
2297
4311
|
lines.append(f" {cpp_type} {fresh_safe} = na<{cpp_type}>();")
|
|
2298
4312
|
|
|
2299
4313
|
# 8d. Drawing-objects-as-data arenas (gated on _uses_drawing so
|
|
@@ -2311,39 +4325,59 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2311
4325
|
if self.ctx.var_members:
|
|
2312
4326
|
lines.append(" bool _var_initialized = false;")
|
|
2313
4327
|
|
|
2314
|
-
# 9a. Per-
|
|
2315
|
-
#
|
|
2316
|
-
#
|
|
2317
|
-
#
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
4328
|
+
# 9a. Per-member flags for primitive runtime ``var`` / ``varip``
|
|
4329
|
+
# initializers. Unlike the global aggregate/Series latch above, these
|
|
4330
|
+
# live at the declaration site so prior statements are available and a
|
|
4331
|
+
# conditional declaration initializes on its first actual execution.
|
|
4332
|
+
emitted_runtime_flags: set[str] = set()
|
|
4333
|
+
for flag in self._runtime_var_init_flags.values():
|
|
4334
|
+
if flag in emitted_runtime_flags:
|
|
4335
|
+
continue
|
|
4336
|
+
emitted_runtime_flags.add(flag)
|
|
4337
|
+
lines.append(f" bool {flag} = false;")
|
|
4338
|
+
|
|
4339
|
+
# 9b. Legacy per-variant latch members are retained for generated-C++
|
|
4340
|
+
# and checkpoint-shape compatibility. Callable initializers no longer
|
|
4341
|
+
# consult them: every initialized declaration owns an exact-site flag.
|
|
4342
|
+
# Keep their historical spelling when free, but avoid legal authored
|
|
4343
|
+
# ``_fvinit_*`` members and every natural/fresh clone name.
|
|
4344
|
+
used_legacy_flags = set(self._runtime_var_init_flag_used_names)
|
|
4345
|
+
|
|
4346
|
+
def allocate_legacy_flag(base: str) -> str:
|
|
4347
|
+
flag = base
|
|
4348
|
+
suffix = 2
|
|
4349
|
+
while flag in used_legacy_flags:
|
|
4350
|
+
flag = f"{base}_{suffix}"
|
|
4351
|
+
suffix += 1
|
|
4352
|
+
used_legacy_flags.add(flag)
|
|
4353
|
+
return flag
|
|
4354
|
+
|
|
2322
4355
|
for fi in self.ctx.func_infos:
|
|
2323
4356
|
if fi.name not in self.ctx.func_var_members:
|
|
2324
4357
|
continue
|
|
4358
|
+
emitted = self._func_cpp_base_name(fi.name)
|
|
2325
4359
|
total_cs = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
2326
|
-
if total_cs > 0
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
lines.append(f" bool
|
|
4360
|
+
variants = range(total_cs) if total_cs > 0 else (None,)
|
|
4361
|
+
for cs_idx in variants:
|
|
4362
|
+
suffix = f"_cs{cs_idx}" if cs_idx is not None else ""
|
|
4363
|
+
flag = allocate_legacy_flag(f"_fvinit_{emitted}{suffix}")
|
|
4364
|
+
lines.append(f" bool {flag} = false;")
|
|
2331
4365
|
|
|
2332
|
-
# 9a2. ``var`` init flags for fresh context-sensitive helper instances.
|
|
2333
4366
|
for inst in self._fresh_instances:
|
|
2334
4367
|
if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
|
|
2335
|
-
|
|
4368
|
+
flag = allocate_legacy_flag(f"_fvinit_{inst['name']}")
|
|
4369
|
+
lines.append(f" bool {flag} = false;")
|
|
2336
4370
|
|
|
2337
|
-
#
|
|
4371
|
+
# 9c. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
|
|
2338
4372
|
if self.ctx.ta_call_sites:
|
|
2339
4373
|
lines.append(" bool _ta_initialized_ = false;")
|
|
2340
4374
|
|
|
2341
|
-
#
|
|
4375
|
+
# 9d. _inputs_initialized_ flag for cached global inputs.
|
|
2342
4376
|
lines.append(" bool _inputs_initialized_ = false;")
|
|
2343
4377
|
|
|
2344
4378
|
lines.append("")
|
|
2345
4379
|
|
|
2346
|
-
#
|
|
4380
|
+
# 9e. Historical execution rollback checkpoint. Derive the member
|
|
2347
4381
|
# inventory from the declarations above so every future generated
|
|
2348
4382
|
# state category is captured automatically (or generation fails loudly
|
|
2349
4383
|
# if it introduces an unfamiliar declaration form).
|
|
@@ -2447,8 +4481,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2447
4481
|
|
|
2448
4482
|
# _get_ta_site / _ta_member_name / _ta_name_from_site / _TA_IMPLICIT_REPLACE
|
|
2449
4483
|
# / _ta_compute_args_for_site / _security_ta_compute_args_for_site /
|
|
2450
|
-
|
|
2451
|
-
|
|
4484
|
+
def _resolve_ta_ctor_arg(self, arg_str: str) -> str:
|
|
4485
|
+
"""Resolve one TA constructor argument without widening const folding.
|
|
4486
|
+
|
|
4487
|
+
``var`` values deliberately stay out of ``_known_vars`` because normal
|
|
4488
|
+
expression reads must still target persistent storage. A uniquely
|
|
4489
|
+
bound, never-reassigned top-level integer literal is nevertheless a
|
|
4490
|
+
faithful static TA buffer length. Admit only that exact bare alias;
|
|
4491
|
+
arithmetic, input/timeframe expressions, shadows, and all mutable or
|
|
4492
|
+
history-bearing bindings continue through the existing resolver/reset
|
|
4493
|
+
guardrails unchanged.
|
|
4494
|
+
"""
|
|
4495
|
+
resolved = self._resolve_known(arg_str)
|
|
4496
|
+
if self._is_compile_time_value(resolved):
|
|
4497
|
+
return resolved
|
|
4498
|
+
if (
|
|
4499
|
+
arg_str in self._stable_var_ctor_literals
|
|
4500
|
+
and not self._known_var_is_lexically_shadowed(arg_str)
|
|
4501
|
+
):
|
|
4502
|
+
return str(self._stable_var_ctor_literals[arg_str].value)
|
|
4503
|
+
return resolved
|
|
2452
4504
|
|
|
2453
4505
|
def _resolve_known(self, arg_str: str) -> str:
|
|
2454
4506
|
"""Resolve a string arg, replacing known var names with their values.
|
|
@@ -2459,7 +4511,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2459
4511
|
if arg_str == "na":
|
|
2460
4512
|
return "na<double>()"
|
|
2461
4513
|
# Direct variable lookup
|
|
2462
|
-
if arg_str in self._known_vars
|
|
4514
|
+
if (arg_str in self._known_vars
|
|
4515
|
+
and not self._known_var_is_lexically_shadowed(arg_str)):
|
|
2463
4516
|
val = self._known_vars[arg_str]
|
|
2464
4517
|
if isinstance(val, bool):
|
|
2465
4518
|
return "true" if val else "false"
|
|
@@ -2476,6 +4529,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2476
4529
|
resolved = arg_str
|
|
2477
4530
|
# Sort by length (longest first) to avoid partial replacements
|
|
2478
4531
|
for name in sorted(self._known_vars, key=len, reverse=True):
|
|
4532
|
+
if self._known_var_is_lexically_shadowed(name):
|
|
4533
|
+
continue
|
|
2479
4534
|
val = self._known_vars[name]
|
|
2480
4535
|
if isinstance(val, (int, float)):
|
|
2481
4536
|
import re
|
|
@@ -2546,15 +4601,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2546
4601
|
"""
|
|
2547
4602
|
if not isinstance(node, MemberAccess):
|
|
2548
4603
|
return False
|
|
2549
|
-
#
|
|
2550
|
-
#
|
|
2551
|
-
|
|
2552
|
-
udt_name = self._udt_var_types.get(node.object.name)
|
|
2553
|
-
if udt_name is None:
|
|
2554
|
-
return False
|
|
2555
|
-
return node.member in self._udt_omitted_fields.get(udt_name, ())
|
|
2556
|
-
# General path: try to infer the receiver's UDT type via the same
|
|
2557
|
-
# spec-resolver visit_expr uses for fallback member access.
|
|
4604
|
+
# Resolve even bare identifiers through the lexical/exact TypeSpec
|
|
4605
|
+
# path. The legacy raw-name UDT registry can describe an unrelated
|
|
4606
|
+
# callable local and therefore cannot safely drive field omission.
|
|
2558
4607
|
recv_spec = self._type_spec_from_expr(node.object)
|
|
2559
4608
|
if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
|
|
2560
4609
|
return node.member in self._udt_omitted_fields.get(recv_spec.name, ())
|
|
@@ -2594,7 +4643,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2594
4643
|
"exp": "std::exp", "pow": "std::pow",
|
|
2595
4644
|
"sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
|
|
2596
4645
|
"asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
|
|
2597
|
-
"sign":
|
|
4646
|
+
"sign": _MATH_SIGN_CPP_FN,
|
|
2598
4647
|
}
|
|
2599
4648
|
|
|
2600
4649
|
# Pine logical operators (word form) -> C++ operator, used when rendering
|
|
@@ -2719,7 +4768,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2719
4768
|
return s
|
|
2720
4769
|
def _rep(p: re.Match) -> str:
|
|
2721
4770
|
nm = p.group(0)
|
|
2722
|
-
if nm in self._derived_input_expr
|
|
4771
|
+
if (nm in self._derived_input_expr
|
|
4772
|
+
and nm not in seen
|
|
4773
|
+
and not self._known_var_is_lexically_shadowed(nm)):
|
|
2723
4774
|
inner = self._derived_input_expr[nm]
|
|
2724
4775
|
return "(" + _expand_derived(inner, seen | {nm}, depth + 1) + ")"
|
|
2725
4776
|
return nm
|
|
@@ -2728,6 +4779,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2728
4779
|
expanded = _expand_derived(arg_str)
|
|
2729
4780
|
|
|
2730
4781
|
tokens = set(ident_re.findall(expanded))
|
|
4782
|
+
if any(
|
|
4783
|
+
self._known_var_is_lexically_shadowed(name)
|
|
4784
|
+
for name in tokens
|
|
4785
|
+
):
|
|
4786
|
+
return None
|
|
2731
4787
|
|
|
2732
4788
|
# Gate: every identifier token must be renderable. If any token is an
|
|
2733
4789
|
# unrecognised bare identifier (not an input, not a known const, not
|
|
@@ -2749,7 +4805,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2749
4805
|
# Known compile-time consts that survived expansion (e.g. ``pi`` was
|
|
2750
4806
|
# NOT tracked as a Python value but its name token is a stable var
|
|
2751
4807
|
# already; pure numeric names are in _known_vars and covered above).
|
|
2752
|
-
leftover = {
|
|
4808
|
+
leftover = {
|
|
4809
|
+
t for t in leftover
|
|
4810
|
+
if (t not in self._known_vars
|
|
4811
|
+
or self._known_var_is_lexically_shadowed(t))
|
|
4812
|
+
}
|
|
2753
4813
|
# Inline ``input(...)`` / ``input.<t>(...)`` calls (a bare input
|
|
2754
4814
|
# expression passed straight as a length arg, e.g.
|
|
2755
4815
|
# ``adx(input(15), input(15))``) are re-parsed and rendered below,
|
|
@@ -2820,6 +4880,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2820
4880
|
# reads, and inline known compile-time consts (non-input) as literals.
|
|
2821
4881
|
def _sub(p: re.Match) -> str:
|
|
2822
4882
|
name = p.group(0)
|
|
4883
|
+
if self._known_var_is_lexically_shadowed(name):
|
|
4884
|
+
return name
|
|
2823
4885
|
if name in self._input_backed_vars:
|
|
2824
4886
|
call_node = self._input_var_to_call.get(name)
|
|
2825
4887
|
if call_node is None:
|
|
@@ -2827,7 +4889,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2827
4889
|
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
2828
4890
|
title = self._get_input_title(call_node, var_name=name)
|
|
2829
4891
|
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
2830
|
-
if name in self._known_vars
|
|
4892
|
+
if (name in self._known_vars
|
|
4893
|
+
and name not in self._input_backed_vars
|
|
4894
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
2831
4895
|
val = self._known_vars[name]
|
|
2832
4896
|
if isinstance(val, bool):
|
|
2833
4897
|
return "true" if val else "false"
|
|
@@ -2893,7 +4957,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2893
4957
|
return rendered
|
|
2894
4958
|
|
|
2895
4959
|
|
|
2896
|
-
def _collect_ta_runtime_resets(
|
|
4960
|
+
def _collect_ta_runtime_resets(
|
|
4961
|
+
self,
|
|
4962
|
+
*,
|
|
4963
|
+
security_source_node: ASTNode | None = None,
|
|
4964
|
+
) -> list[str]:
|
|
2897
4965
|
"""Collect reassignment statements for every TA object whose ctor args
|
|
2898
4966
|
depend on an input-backed variable. Returned strings are raw C++
|
|
2899
4967
|
assignment statements (no enclosing block/indent). Empty list when no
|
|
@@ -2917,7 +4985,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2917
4985
|
runtime_args.append(rt)
|
|
2918
4986
|
any_runtime = True
|
|
2919
4987
|
else:
|
|
2920
|
-
resolved = self.
|
|
4988
|
+
resolved = self._resolve_ta_ctor_arg(a)
|
|
2921
4989
|
runtime_args.append(resolved if self._is_compile_time_value(resolved) else "1")
|
|
2922
4990
|
if any_runtime:
|
|
2923
4991
|
resets.append(
|
|
@@ -2942,6 +5010,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2942
5010
|
sec_cs_idx = (sec_item or {}).get("callsite_idx")
|
|
2943
5011
|
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
2944
5012
|
site = self.ctx.ta_call_sites[idx]
|
|
5013
|
+
if (
|
|
5014
|
+
security_source_node is not None
|
|
5015
|
+
and site.node is not security_source_node
|
|
5016
|
+
):
|
|
5017
|
+
continue
|
|
2945
5018
|
ctor_site = site
|
|
2946
5019
|
if sec_containing and sec_cs_idx is not None:
|
|
2947
5020
|
remap = self._func_cs_ta_remap.get((sec_containing, sec_cs_idx))
|
|
@@ -2953,18 +5026,55 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2953
5026
|
ctor_site = cand
|
|
2954
5027
|
if not ctor_site.ctor_args:
|
|
2955
5028
|
continue
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
5029
|
+
for variant in variants:
|
|
5030
|
+
ctor_args, ctor_arg_stability = self._security_ta_ctor_args_for_variant(
|
|
5031
|
+
info["sec_id"],
|
|
5032
|
+
site,
|
|
5033
|
+
variant.get("binding_stack", ()),
|
|
5034
|
+
fallback_args=ctor_site.ctor_args,
|
|
5035
|
+
)
|
|
5036
|
+
runtime_args = []
|
|
5037
|
+
any_runtime = False
|
|
5038
|
+
for arg_pos, a in enumerate(ctor_args):
|
|
5039
|
+
rt = self._runtime_ctor_arg_for_reset(a)
|
|
5040
|
+
resolved = self._resolve_ta_ctor_arg(a)
|
|
5041
|
+
lowered_variant = ctor_arg_stability is not None
|
|
5042
|
+
stable_variant_arg = (
|
|
5043
|
+
lowered_variant and ctor_arg_stability[arg_pos]
|
|
5044
|
+
)
|
|
5045
|
+
if (
|
|
5046
|
+
rt is None
|
|
5047
|
+
and stable_variant_arg
|
|
5048
|
+
and not self._is_compile_time_value(resolved)
|
|
5049
|
+
):
|
|
5050
|
+
# This is trusted C++ from _build_security_expr and
|
|
5051
|
+
# its fully helper-bound AST passed _expr_is_stable.
|
|
5052
|
+
rt = a
|
|
5053
|
+
if (
|
|
5054
|
+
rt is None
|
|
5055
|
+
and lowered_variant
|
|
5056
|
+
and not self._is_compile_time_value(resolved)
|
|
5057
|
+
):
|
|
5058
|
+
self._codegen_error(
|
|
5059
|
+
getattr(site, "node", None),
|
|
5060
|
+
f"Unsupported requested-context TA constructor "
|
|
5061
|
+
f"length '{a}' for {site.class_name}: the "
|
|
5062
|
+
"helper-bound expression is not a stable "
|
|
5063
|
+
"per-run scalar.",
|
|
5064
|
+
hint=("Use a literal, an input.*() value, "
|
|
5065
|
+
"timeframe.* metadata, or arithmetic "
|
|
5066
|
+
"over those for TA lengths."),
|
|
5067
|
+
)
|
|
5068
|
+
if rt is not None:
|
|
5069
|
+
runtime_args.append(rt)
|
|
5070
|
+
any_runtime = True
|
|
5071
|
+
else:
|
|
5072
|
+
runtime_args.append(
|
|
5073
|
+
resolved
|
|
5074
|
+
if self._is_compile_time_value(resolved)
|
|
5075
|
+
else "1"
|
|
5076
|
+
)
|
|
5077
|
+
if any_runtime:
|
|
2968
5078
|
resets.append(
|
|
2969
5079
|
f"{variant['member_name']} = {site.class_name}({', '.join(runtime_args)});"
|
|
2970
5080
|
)
|