@pineforge/codegen-pyodide 0.10.0 → 0.10.2
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 +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- 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 +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -21,8 +21,20 @@ from ..ast_nodes import (
|
|
|
21
21
|
TupleLiteral,
|
|
22
22
|
TypeDecl, EnumDecl, MethodDef, TypeField,
|
|
23
23
|
)
|
|
24
|
-
from ..symbols import
|
|
24
|
+
from ..symbols import (
|
|
25
|
+
PineType,
|
|
26
|
+
Symbol,
|
|
27
|
+
SymbolTable,
|
|
28
|
+
TypeSpec,
|
|
29
|
+
method_receiver_type_name,
|
|
30
|
+
)
|
|
25
31
|
from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase
|
|
32
|
+
from ..method_binding import (
|
|
33
|
+
BoundMethodArgs,
|
|
34
|
+
MethodBindError,
|
|
35
|
+
bind_method_call,
|
|
36
|
+
inventory_method_signatures,
|
|
37
|
+
)
|
|
26
38
|
from .. import signatures as sigs
|
|
27
39
|
from .. import tv_input_choices as tv_in
|
|
28
40
|
# Output dataclasses (contract with the codegen) live in contracts.py so
|
|
@@ -115,9 +127,16 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
115
127
|
def __init__(self, ast: Program, filename: str = "<stdin>") -> None:
|
|
116
128
|
self._ast = ast
|
|
117
129
|
self._filename = filename
|
|
130
|
+
self._method_signatures = inventory_method_signatures(ast)
|
|
131
|
+
self._method_call_bindings: dict[
|
|
132
|
+
tuple[int, str], BoundMethodArgs
|
|
133
|
+
] = {}
|
|
118
134
|
self._symbols = SymbolTable()
|
|
119
135
|
self._ta_call_sites: list[TACallSite] = []
|
|
120
136
|
self._series_vars: set[str] = set()
|
|
137
|
+
self._series_var_members: set[str] = set()
|
|
138
|
+
self._series_decl_nodes: set[int] = set()
|
|
139
|
+
self._series_decl_bindings: set[tuple[int, str]] = set()
|
|
121
140
|
self._series_bar_fields: set[str] = set()
|
|
122
141
|
self._var_members: list[tuple[str, PineType, str]] = []
|
|
123
142
|
self._func_infos: list[FuncInfo] = []
|
|
@@ -125,22 +144,75 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
125
144
|
self._strategy_params: dict = {}
|
|
126
145
|
self._diagnostics: list[Diagnostic] = []
|
|
127
146
|
self._global_var_decls: list[tuple[str, PineType]] = []
|
|
147
|
+
# Top-level ordinary bindings are lexical global state even when a
|
|
148
|
+
# same-named callable history reference has already polluted the
|
|
149
|
+
# legacy raw ``series_vars`` set and suppresses ``global_var_decls``.
|
|
150
|
+
# Keep an AST-authoritative inventory for storage collision routing.
|
|
151
|
+
self._ordinary_global_binding_names: set[str] = {
|
|
152
|
+
stmt.name
|
|
153
|
+
for stmt in ast.body
|
|
154
|
+
if isinstance(stmt, VarDecl)
|
|
155
|
+
and not stmt.is_var
|
|
156
|
+
and not stmt.is_varip
|
|
157
|
+
}
|
|
158
|
+
self._ordinary_global_binding_names.update(
|
|
159
|
+
name
|
|
160
|
+
for stmt in ast.body
|
|
161
|
+
if isinstance(stmt, TupleAssign)
|
|
162
|
+
for name in stmt.names
|
|
163
|
+
if name != "_"
|
|
164
|
+
)
|
|
165
|
+
self._direct_program_binding_names: set[str] = {
|
|
166
|
+
stmt.name
|
|
167
|
+
for stmt in ast.body
|
|
168
|
+
if isinstance(stmt, VarDecl)
|
|
169
|
+
}
|
|
170
|
+
self._direct_program_binding_names.update(
|
|
171
|
+
name
|
|
172
|
+
for stmt in ast.body
|
|
173
|
+
if isinstance(stmt, TupleAssign)
|
|
174
|
+
for name in stmt.names
|
|
175
|
+
if name != "_"
|
|
176
|
+
)
|
|
177
|
+
# Exact declaration typing retained only for lexical-boundary checks.
|
|
178
|
+
# The legacy symbol/type registries are raw-name keyed and therefore
|
|
179
|
+
# cannot distinguish a direct script binding from a same-spelled local
|
|
180
|
+
# declared inside a top-level control-flow block.
|
|
181
|
+
self._var_decl_types_by_node: dict[
|
|
182
|
+
int, tuple[PineType, TypeSpec | None]
|
|
183
|
+
] = {}
|
|
184
|
+
self._ordinary_global_binding_info: dict[
|
|
185
|
+
str, tuple[int, PineType, ASTNode | None]
|
|
186
|
+
] = {}
|
|
187
|
+
self._ordinary_global_series_names: set[str] = set()
|
|
128
188
|
self._global_expr_map: dict[str, Any] = {}
|
|
129
189
|
self._var_member_init_exprs: dict[str, Any] = {}
|
|
190
|
+
# Exact declaration-node ownership for persistent vars. The member
|
|
191
|
+
# name can differ from ``node.name`` when sibling blocks declare the
|
|
192
|
+
# same raw identifier, so codegen must not reconstruct this mapping
|
|
193
|
+
# from names when it emits declaration-site initialization.
|
|
194
|
+
self._var_member_metadata_by_node: dict[int, tuple] = {}
|
|
195
|
+
self._var_member_type_specs_by_node: dict[int, TypeSpec | None] = {}
|
|
196
|
+
self._var_member_owners_by_node: dict[int, str | None] = {}
|
|
130
197
|
# Block-scoped ``var``/``varip`` name-collision disambiguation.
|
|
131
198
|
# Two same-named block-scoped vars in SIBLING non-global, non-function
|
|
132
199
|
# scopes (e.g. ``var bool valid`` declared inside ``if A`` and again
|
|
133
200
|
# inside ``if B``) would otherwise dedupe to ONE C++ member and
|
|
134
201
|
# cross-contaminate. ``_block_node_stack`` tracks the enclosing
|
|
135
|
-
#
|
|
136
|
-
# block-var name to the id() of the FIRST
|
|
137
|
-
# ``_block_var_renames`` maps id(
|
|
202
|
+
# branch/loop body owners during analysis; ``_block_var_owner`` maps a
|
|
203
|
+
# raw block-var name to the id() of the FIRST body that declared it;
|
|
204
|
+
# ``_block_var_renames`` maps id(body_owner) -> {raw_name: unique}
|
|
138
205
|
# for every later colliding block so codegen can activate the
|
|
139
206
|
# rename via ``_active_var_remap`` while emitting that block.
|
|
140
207
|
self._block_node_stack: list[Any] = []
|
|
141
208
|
self._block_var_owner: dict[str, int] = {}
|
|
142
209
|
self._block_var_renames: dict[int, dict[str, str]] = {}
|
|
143
210
|
self._block_var_seq = 0
|
|
211
|
+
# Identifier node identity -> the lexical Symbol.scope resolved while
|
|
212
|
+
# that node's source scope is live. Codegen uses this narrow
|
|
213
|
+
# provenance to distinguish true global aliases from same-named local
|
|
214
|
+
# shadows after the analyzer's scope stack has unwound.
|
|
215
|
+
self._identifier_binding_scopes: dict[int, str | None] = {}
|
|
144
216
|
self._ta_counter = 0
|
|
145
217
|
self._fixnan_counter = 0
|
|
146
218
|
# All fixnan member names minted so far (base + clones), for O(1)
|
|
@@ -157,6 +229,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
157
229
|
# Track user-defined function tuple returns
|
|
158
230
|
self._func_returns_tuple: dict[str, bool] = {}
|
|
159
231
|
self._func_tuple_element_count: dict[str, int] = {}
|
|
232
|
+
self._func_tuple_element_types: dict[str, tuple[PineType, ...]] = {}
|
|
233
|
+
self._tuple_element_types_by_node: dict[int, tuple[PineType, ...]] = {}
|
|
160
234
|
# Track user-defined functions whose body returns a UDT instance —
|
|
161
235
|
# maps func_name -> UDT type name. Detected from the body's final
|
|
162
236
|
# expression (``=> Sample.new(...)`` or last stmt ``Sample.new(...)``).
|
|
@@ -164,18 +238,101 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
164
238
|
self._func_udt_return_types: dict[str, str] = {}
|
|
165
239
|
self._func_return_type_specs: dict[str, "TypeSpec"] = {}
|
|
166
240
|
self._func_param_type_specs: dict[str, list] = {}
|
|
241
|
+
# Direct terminal gets on an unshadowed built-in temporary are
|
|
242
|
+
# registered while their lexical scope is live. A later-defined UDF
|
|
243
|
+
# used by ``array.from`` may make the first-pass scalar fallback stale;
|
|
244
|
+
# a bounded, side-effect-free refresh corrects only this proven shape.
|
|
245
|
+
self._direct_terminal_array_temporary_exprs: dict[str, ASTNode] = {}
|
|
246
|
+
# History receivers can mention an untyped UDF parameter before its
|
|
247
|
+
# concrete TypeSpec is learned from a call site. Keep the exact AST
|
|
248
|
+
# identifier identities that resolved to parameters during the
|
|
249
|
+
# definition pass; call handling re-checks those receivers once the
|
|
250
|
+
# argument specs are available. Node identity (rather than raw name)
|
|
251
|
+
# prevents a later local/loop binding with the same spelling from
|
|
252
|
+
# being mistaken for the parameter it shadows.
|
|
253
|
+
self._deferred_param_history_refs: dict[
|
|
254
|
+
str, list[tuple[Subscript, dict[int, str]]]
|
|
255
|
+
] = {}
|
|
256
|
+
# Parameter flow between user callables, captured while the caller's
|
|
257
|
+
# lexical symbols are still in scope. This lets a later concrete map
|
|
258
|
+
# call revalidate history hidden behind wrappers without re-analyzing
|
|
259
|
+
# whole function bodies or conflating same-spelled locals.
|
|
260
|
+
self._deferred_param_call_edges: dict[
|
|
261
|
+
str,
|
|
262
|
+
list[
|
|
263
|
+
tuple[
|
|
264
|
+
int,
|
|
265
|
+
str,
|
|
266
|
+
list[str],
|
|
267
|
+
list[ASTNode | None],
|
|
268
|
+
list[dict[int, str]],
|
|
269
|
+
]
|
|
270
|
+
],
|
|
271
|
+
] = {}
|
|
272
|
+
self._func_series_history_nodes: dict[
|
|
273
|
+
tuple[str, str], Subscript
|
|
274
|
+
] = {}
|
|
167
275
|
# Per-function var_members and series_vars (for call-site cloning)
|
|
168
276
|
self._func_var_members: dict[str, list] = {} # func_name -> [(name, PineType, init_str)]
|
|
277
|
+
# Ordinary FuncDef raw persistent name -> exact emitted member. The
|
|
278
|
+
# map is identity for ordinary non-colliding functions and changes only
|
|
279
|
+
# when distinct persistent declarations would otherwise share a
|
|
280
|
+
# supported primitive/collection ``var`` owned by an ordinary FuncDef
|
|
281
|
+
# with the same raw spelling. Methods and top-level bindings remain on
|
|
282
|
+
# their established paths.
|
|
283
|
+
self._func_var_storage_names: dict[str, dict[str, str]] = {}
|
|
169
284
|
self._func_series_vars: dict[str, set] = {} # func_name -> set[str]
|
|
285
|
+
# Declaration-bound non-persistent history locals are distinct from
|
|
286
|
+
# history parameters/global reads carried by ``func_series_vars``.
|
|
287
|
+
# Codegen needs this exact subset when a raw spelling also belongs to
|
|
288
|
+
# owner-qualified persistent state.
|
|
289
|
+
self._nonpersistent_series_decl_names: set[str] = set()
|
|
290
|
+
self._func_nonpersistent_series_vars: dict[str, set[str]] = {}
|
|
170
291
|
# Per-call-site TA tracking for user functions
|
|
171
292
|
self._func_ta_ranges: dict[str, tuple[int, int]] = {} # func_name -> (start, end) indices
|
|
293
|
+
# Exact counterpart to the legacy contiguous ranges. Borrowed nested
|
|
294
|
+
# sites can straddle unrelated TA allocations, so constructor argument
|
|
295
|
+
# substitution and codegen remaps must iterate these identities only.
|
|
296
|
+
self._func_ta_indices: dict[str, list[int]] = {}
|
|
297
|
+
# A shared TA site can be viewed through more than one callable's
|
|
298
|
+
# parameter names (innerLen -> outerLen). Preserve one constructor
|
|
299
|
+
# template per callable/site instead of mutating the site's single
|
|
300
|
+
# source template and relying on same-spelled forwarding parameters.
|
|
301
|
+
self._func_ta_ctor_args: dict[str, dict[int, list[str]]] = {}
|
|
302
|
+
# Exact TA-site mapping and source-template snapshot for each textual
|
|
303
|
+
# callable edge. Late whole-program propagation uses these to revisit
|
|
304
|
+
# an already-numbered edge when its callee gains another owned TA site
|
|
305
|
+
# (or refines an existing per-owner constructor template), without
|
|
306
|
+
# cloning the edge's previously-materialized state a second time.
|
|
307
|
+
self._func_ta_call_targets: dict[
|
|
308
|
+
tuple[int, int], dict[int, int]
|
|
309
|
+
] = {}
|
|
310
|
+
self._func_ta_call_templates: dict[
|
|
311
|
+
int, dict[int, tuple[str, ...]]
|
|
312
|
+
] = {}
|
|
172
313
|
self._func_call_site_count: dict[str, int] = {} # func_name -> count
|
|
173
314
|
self._func_call_cs_map: dict[int, tuple[str, int]] = {} # call_node_id -> (func_name, cs_idx)
|
|
315
|
+
# Primitive type facts retained per written call AST and reconciled to
|
|
316
|
+
# the emitted cs0/cs1/... identities after the stateful call graph is
|
|
317
|
+
# closed. Pine explicitly permits an untyped parameter to inherit a
|
|
318
|
+
# different type at each written call site.
|
|
319
|
+
self._callable_bound_param_types_by_node: dict[int, list[PineType]] = {}
|
|
320
|
+
self._func_callsite_param_types: dict[
|
|
321
|
+
tuple[str, int], list[PineType]
|
|
322
|
+
] = {}
|
|
323
|
+
self._func_callsite_return_types: dict[
|
|
324
|
+
tuple[str, int], PineType
|
|
325
|
+
] = {}
|
|
174
326
|
# Textual nested calls whose identity is inherited from the active
|
|
175
327
|
# parent clone rather than assigned a fixed source-level cs index.
|
|
176
328
|
# Kept separately so a second propagation pass (security TF cloning)
|
|
177
329
|
# does not accidentally backfill them as a new cs{N} call site.
|
|
178
330
|
self._func_inherited_call_nodes: set[int] = set()
|
|
331
|
+
# Exact callee identity for those removed mappings. Natural parent
|
|
332
|
+
# clones still use active-index fallback, while a context-sensitive
|
|
333
|
+
# fresh parent has no active index and needs this edge identity to
|
|
334
|
+
# compose and dispatch its nested instance explicitly.
|
|
335
|
+
self._func_inherited_call_names: dict[int, str] = {}
|
|
179
336
|
# Per-function fixnan site ownership: func_name -> list of fixnan site
|
|
180
337
|
# indices in self._fixnan_sites owned by that function. Mirrors the
|
|
181
338
|
# TA-range slicing but for fixnan state, so per-call-site cloning can
|
|
@@ -207,6 +364,23 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
207
364
|
# var_name -> UDT type for variables holding UDT instances
|
|
208
365
|
self._udt_var_types: dict[str, str] = {}
|
|
209
366
|
self._collection_types: dict[str, TypeSpec] = {}
|
|
367
|
+
# Collection metadata for callable locals must retain lexical identity.
|
|
368
|
+
# Keys match FuncInfo.name: ``func`` for ordinary UDFs and
|
|
369
|
+
# ``Type.method`` for UDT methods. ``_collection_types`` remains the
|
|
370
|
+
# top-level/on_bar registry consumed outside callable emission.
|
|
371
|
+
self._func_collection_types: dict[str, dict[str, TypeSpec]] = {}
|
|
372
|
+
# id(immediate branch/loop owner) -> raw local name -> TypeSpec.
|
|
373
|
+
# A callable may legally reuse one raw name for unrelated collections
|
|
374
|
+
# in sibling lexical blocks; keeping those bindings in the flat
|
|
375
|
+
# callable inventory would make the last analyzed branch win.
|
|
376
|
+
self._block_collection_types: dict[int, dict[str, TypeSpec | None]] = {}
|
|
377
|
+
self._block_collection_owners: dict[int, str] = {}
|
|
378
|
+
# id(callable-local VarDecl) -> its exact collection TypeSpec, or None
|
|
379
|
+
# for a scalar/UDT tombstone. Codegen activates these in source order
|
|
380
|
+
# after emitting each declaration RHS.
|
|
381
|
+
self._callable_collection_bindings: dict[int, TypeSpec | None] = {}
|
|
382
|
+
self._callable_collection_binding_owners: dict[int, str] = {}
|
|
383
|
+
self._collection_scope_stack: list[str] = []
|
|
210
384
|
self._udt_field_type_specs: dict[str, dict[str, TypeSpec]] = {}
|
|
211
385
|
# Enum definitions: enum_name -> list of member names
|
|
212
386
|
self._enum_defs: dict[str, list[str]] = {}
|
|
@@ -229,8 +403,11 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
229
403
|
# pass can tell borrowed clones apart from a dead function's own
|
|
230
404
|
# sites (see contracts.TACallSite.owner_func).
|
|
231
405
|
self._enclosing_func_names: list[str] = []
|
|
232
|
-
#
|
|
233
|
-
# current
|
|
406
|
+
# Exact TA targets borrowed through nested callable edges while visiting
|
|
407
|
+
# the current callable body. Constructor templates are tracked
|
|
408
|
+
# separately in ``_func_ta_ctor_args`` because state ownership also
|
|
409
|
+
# matters for parameterless helpers such as ``ta.change``. ``None``
|
|
410
|
+
# means no callable body is active.
|
|
234
411
|
self._nested_ta_touched: set | None = None
|
|
235
412
|
|
|
236
413
|
# Pre-populate builtins
|
|
@@ -286,6 +463,58 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
286
463
|
]
|
|
287
464
|
)
|
|
288
465
|
|
|
466
|
+
def _check_direct_terminal_array_element_callee_shadows(self) -> None:
|
|
467
|
+
"""Reject terminal temporary reads whose element call is shadowed.
|
|
468
|
+
|
|
469
|
+
This is a whole-program syntactic preflight: it runs before the first
|
|
470
|
+
AST visit so definition order cannot leave partial function, call-site,
|
|
471
|
+
or TA state behind. Only bindings whose lexical lifetime reaches the
|
|
472
|
+
read are relevant: FuncDef parameters and declarations in the immediate
|
|
473
|
+
function body before the direct terminal or adjacent alias initializer.
|
|
474
|
+
The alias declaration is not active on its own RHS. Nested block locals
|
|
475
|
+
have expired and must not shadow the global UDF at either read point.
|
|
476
|
+
"""
|
|
477
|
+
for func_def in self._ast.body:
|
|
478
|
+
if not isinstance(func_def, FuncDef):
|
|
479
|
+
continue
|
|
480
|
+
terminal = self._direct_terminal_return_expr(func_def)
|
|
481
|
+
element_call: FuncCall | None = None
|
|
482
|
+
read_index: int | None = None
|
|
483
|
+
if self._terminal_array_get_uses_direct_temporary(terminal):
|
|
484
|
+
element_call = (
|
|
485
|
+
self._direct_terminal_array_temporary_element_call(
|
|
486
|
+
terminal
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
read_index = len(func_def.body) - 1
|
|
490
|
+
else:
|
|
491
|
+
alias_candidate = (
|
|
492
|
+
self._direct_terminal_array_temporary_alias_candidate(
|
|
493
|
+
func_def, terminal
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
if alias_candidate is not None:
|
|
497
|
+
read_index, _, element_call = alias_candidate
|
|
498
|
+
if element_call is None or read_index is None:
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
bindings = set(func_def.params)
|
|
502
|
+
for statement in func_def.body[:read_index]:
|
|
503
|
+
if isinstance(statement, VarDecl):
|
|
504
|
+
bindings.add(statement.name)
|
|
505
|
+
elif isinstance(statement, TupleAssign):
|
|
506
|
+
bindings.update(
|
|
507
|
+
name for name in statement.names if name != "_"
|
|
508
|
+
)
|
|
509
|
+
if element_call.callee.name not in bindings:
|
|
510
|
+
continue
|
|
511
|
+
self._error(
|
|
512
|
+
"Direct temporary-array element call "
|
|
513
|
+
f"'{element_call.callee.name}()' resolves to a local or "
|
|
514
|
+
"parameter, not a user-defined function.",
|
|
515
|
+
element_call.loc,
|
|
516
|
+
)
|
|
517
|
+
|
|
289
518
|
# ------------------------------------------------------------------
|
|
290
519
|
# Public entry point
|
|
291
520
|
# ------------------------------------------------------------------
|
|
@@ -293,7 +522,17 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
293
522
|
def analyze(self) -> AnalyzerContext:
|
|
294
523
|
"""Run semantic analysis and return the analyzer context."""
|
|
295
524
|
self._ensure_pine_v6()
|
|
525
|
+
self._check_direct_terminal_array_element_callee_shadows()
|
|
296
526
|
self._visit(self._ast)
|
|
527
|
+
self._check_direct_terminal_array_temporary_cycles()
|
|
528
|
+
self._register_resolved_direct_terminal_array_forward_calls()
|
|
529
|
+
self._refresh_direct_terminal_array_temporary_returns()
|
|
530
|
+
self._check_forward_tuple_helper_wrappers()
|
|
531
|
+
self._check_top_level_block_shadow_boundaries()
|
|
532
|
+
self._qualify_colliding_func_var_members()
|
|
533
|
+
self._check_cross_callable_series_collection_collisions()
|
|
534
|
+
self._check_declaration_exact_series_storage_boundaries()
|
|
535
|
+
self._check_persistent_drawing_member_collisions()
|
|
297
536
|
|
|
298
537
|
# Propagate call-site counts to sub-functions called within
|
|
299
538
|
# multi-call-site functions. If f() has N call sites and calls g()
|
|
@@ -327,6 +566,9 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
327
566
|
symbols=self._symbols,
|
|
328
567
|
ta_call_sites=self._ta_call_sites,
|
|
329
568
|
series_vars=self._series_vars,
|
|
569
|
+
series_var_members=self._series_var_members,
|
|
570
|
+
series_decl_nodes=self._series_decl_nodes,
|
|
571
|
+
series_decl_bindings=self._series_decl_bindings,
|
|
330
572
|
series_bar_fields=self._series_bar_fields,
|
|
331
573
|
var_members=self._var_members,
|
|
332
574
|
func_infos=self._func_infos,
|
|
@@ -338,10 +580,29 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
338
580
|
filename=self._filename,
|
|
339
581
|
global_var_decls=self._global_var_decls,
|
|
340
582
|
global_expr_map=pure_global_expr_map,
|
|
583
|
+
identifier_binding_scopes=dict(self._identifier_binding_scopes),
|
|
341
584
|
var_member_init_exprs=self._var_member_init_exprs,
|
|
585
|
+
var_member_metadata_by_node=self._var_member_metadata_by_node,
|
|
586
|
+
var_member_type_specs_by_node=self._var_member_type_specs_by_node,
|
|
587
|
+
var_member_owners_by_node=self._var_member_owners_by_node,
|
|
342
588
|
func_ta_ranges=self._func_ta_ranges,
|
|
589
|
+
func_ta_indices=self._func_ta_indices,
|
|
343
590
|
func_call_cs_map=self._func_call_cs_map,
|
|
591
|
+
func_inherited_call_names=dict(
|
|
592
|
+
self._func_inherited_call_names
|
|
593
|
+
),
|
|
344
594
|
func_call_site_counts=self._func_call_site_count,
|
|
595
|
+
func_callsite_param_types={
|
|
596
|
+
key: tuple(types)
|
|
597
|
+
for key, types in self._func_callsite_param_types.items()
|
|
598
|
+
},
|
|
599
|
+
func_callsite_return_types=dict(
|
|
600
|
+
self._func_callsite_return_types
|
|
601
|
+
),
|
|
602
|
+
func_declared_param_type_specs={
|
|
603
|
+
name: tuple(specs)
|
|
604
|
+
for name, specs in self._func_param_type_specs.items()
|
|
605
|
+
},
|
|
345
606
|
func_security_clone_only=self._func_security_clone_only,
|
|
346
607
|
func_cs_ta_clone_names=self._func_cs_ta_clone_names,
|
|
347
608
|
udt_defs=self._udt_fields,
|
|
@@ -349,15 +610,1527 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
349
610
|
enum_member_strings=self._enum_member_strings,
|
|
350
611
|
security_calls=getattr(self, "_security_calls", []),
|
|
351
612
|
global_mutable_infos=mutable_global_infos,
|
|
613
|
+
ordinary_global_binding_names=set(
|
|
614
|
+
self._ordinary_global_binding_names
|
|
615
|
+
),
|
|
616
|
+
ordinary_global_series_names=set(
|
|
617
|
+
self._ordinary_global_series_names
|
|
618
|
+
),
|
|
352
619
|
func_var_members=self._func_var_members,
|
|
620
|
+
func_var_storage_names={
|
|
621
|
+
owner: dict(names)
|
|
622
|
+
for owner, names in self._func_var_storage_names.items()
|
|
623
|
+
},
|
|
353
624
|
func_series_vars=self._func_series_vars,
|
|
625
|
+
nonpersistent_series_decl_names=set(
|
|
626
|
+
self._nonpersistent_series_decl_names
|
|
627
|
+
),
|
|
628
|
+
func_nonpersistent_series_vars={
|
|
629
|
+
owner: set(names)
|
|
630
|
+
for owner, names in (
|
|
631
|
+
self._func_nonpersistent_series_vars.items()
|
|
632
|
+
)
|
|
633
|
+
},
|
|
354
634
|
func_return_type_specs=dict(self._func_return_type_specs),
|
|
355
635
|
udt_var_types=dict(self._udt_var_types),
|
|
356
636
|
collection_types=dict(self._collection_types),
|
|
637
|
+
func_collection_types={
|
|
638
|
+
name: dict(specs)
|
|
639
|
+
for name, specs in self._func_collection_types.items()
|
|
640
|
+
},
|
|
641
|
+
block_collection_types={
|
|
642
|
+
owner_id: dict(specs)
|
|
643
|
+
for owner_id, specs in self._block_collection_types.items()
|
|
644
|
+
},
|
|
645
|
+
block_collection_owners=dict(self._block_collection_owners),
|
|
646
|
+
callable_collection_bindings=dict(
|
|
647
|
+
self._callable_collection_bindings
|
|
648
|
+
),
|
|
649
|
+
callable_collection_binding_owners=dict(
|
|
650
|
+
self._callable_collection_binding_owners
|
|
651
|
+
),
|
|
357
652
|
udt_field_type_specs=dict(self._udt_field_type_specs),
|
|
358
653
|
block_var_renames=dict(self._block_var_renames),
|
|
359
654
|
)
|
|
360
655
|
|
|
656
|
+
def _check_forward_tuple_helper_wrappers(self) -> None:
|
|
657
|
+
"""Fail before codegen on a tuple wrapper calling a later definition.
|
|
658
|
+
|
|
659
|
+
Direct terminal tuple metadata can be propagated after the callee is
|
|
660
|
+
analyzed, but the earlier wrapper body did not register that callee's
|
|
661
|
+
ordinary FuncInfo/call graph. Emitting it would therefore reach an
|
|
662
|
+
unknown-function error in codegen. Keep the boundary explicit until
|
|
663
|
+
forward callable registration itself is implemented.
|
|
664
|
+
"""
|
|
665
|
+
positions = {
|
|
666
|
+
stmt.name: index
|
|
667
|
+
for index, stmt in enumerate(self._ast.body)
|
|
668
|
+
if isinstance(stmt, FuncDef)
|
|
669
|
+
}
|
|
670
|
+
for wrapper_name, wrapper_def in self._func_defs.items():
|
|
671
|
+
terminal = self._direct_terminal_return_expr(wrapper_def)
|
|
672
|
+
if not (
|
|
673
|
+
isinstance(terminal, FuncCall)
|
|
674
|
+
and isinstance(terminal.callee, Identifier)
|
|
675
|
+
):
|
|
676
|
+
continue
|
|
677
|
+
callee_name = terminal.callee.name
|
|
678
|
+
if not self._func_returns_tuple.get(callee_name, False):
|
|
679
|
+
continue
|
|
680
|
+
if positions.get(callee_name, -1) <= positions.get(wrapper_name, -1):
|
|
681
|
+
continue
|
|
682
|
+
self._error(
|
|
683
|
+
"Tuple-return helper wrapper "
|
|
684
|
+
f"'{wrapper_name}' calls later-defined helper '{callee_name}'; "
|
|
685
|
+
"forward tuple-wrapper calls are not supported yet. "
|
|
686
|
+
f"Move '{callee_name}' above '{wrapper_name}' before passing "
|
|
687
|
+
"the wrapper to request.security().",
|
|
688
|
+
terminal.loc,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def _refresh_direct_typed_method_wrapper_returns(self) -> None:
|
|
692
|
+
"""Reconcile direct sibling-method return types without revisiting AST.
|
|
693
|
+
|
|
694
|
+
Method signatures are inventoried before source-order analysis so a
|
|
695
|
+
call to a later declaration binds to the authored method, but the
|
|
696
|
+
callee's body-derived return type does not exist yet. A direct wrapper
|
|
697
|
+
such as ``outer(H self) => self.inner()`` therefore initially receives
|
|
698
|
+
the generic scalar fallback. Once declarations are registered, copy
|
|
699
|
+
the exact terminal callee contract through direct first-parameter
|
|
700
|
+
receiver edges to a fixed point. This pass is deliberately structural: it does
|
|
701
|
+
not call ``_visit`` and cannot duplicate TA, fixnan or written-callsite
|
|
702
|
+
state.
|
|
703
|
+
"""
|
|
704
|
+
infos = {info.name: info for info in self._func_infos}
|
|
705
|
+
if not infos:
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
def direct_callee_key(info: FuncInfo) -> str | None:
|
|
709
|
+
node = info.node
|
|
710
|
+
if (
|
|
711
|
+
node is None
|
|
712
|
+
or not info.is_udt_method
|
|
713
|
+
or not node.params
|
|
714
|
+
):
|
|
715
|
+
return None
|
|
716
|
+
terminal = self._direct_terminal_return_expr(node)
|
|
717
|
+
if not (
|
|
718
|
+
isinstance(terminal, FuncCall)
|
|
719
|
+
and isinstance(terminal.callee, MemberAccess)
|
|
720
|
+
and isinstance(terminal.callee.object, Identifier)
|
|
721
|
+
and terminal.callee.object.name == node.params[0]
|
|
722
|
+
):
|
|
723
|
+
return None
|
|
724
|
+
if not info.param_type_specs:
|
|
725
|
+
return None
|
|
726
|
+
receiver_name = method_receiver_type_name(
|
|
727
|
+
info.param_type_specs[0]
|
|
728
|
+
)
|
|
729
|
+
if receiver_name is None:
|
|
730
|
+
return None
|
|
731
|
+
return f"{receiver_name}.{terminal.callee.member}"
|
|
732
|
+
|
|
733
|
+
# One newly resolved leaf can unlock one wrapper edge per pass. Cycles
|
|
734
|
+
# without an exact leaf retain their existing fail-closed fallback.
|
|
735
|
+
for _ in range(len(infos) + 1):
|
|
736
|
+
changed = False
|
|
737
|
+
for wrapper in infos.values():
|
|
738
|
+
callee_key = direct_callee_key(wrapper)
|
|
739
|
+
callee = infos.get(callee_key) if callee_key else None
|
|
740
|
+
if callee is None:
|
|
741
|
+
continue
|
|
742
|
+
|
|
743
|
+
if (
|
|
744
|
+
callee.return_type not in {PineType.UNKNOWN, PineType.VOID}
|
|
745
|
+
and wrapper.return_type != callee.return_type
|
|
746
|
+
):
|
|
747
|
+
wrapper.return_type = callee.return_type
|
|
748
|
+
self._func_return_types[wrapper.name] = callee.return_type
|
|
749
|
+
changed = True
|
|
750
|
+
|
|
751
|
+
callee_spec = callee.return_type_spec
|
|
752
|
+
if (
|
|
753
|
+
callee_spec is not None
|
|
754
|
+
and wrapper.return_type_spec != callee_spec
|
|
755
|
+
):
|
|
756
|
+
wrapper.return_type_spec = callee_spec
|
|
757
|
+
self._func_return_type_specs[wrapper.name] = callee_spec
|
|
758
|
+
changed = True
|
|
759
|
+
|
|
760
|
+
if (
|
|
761
|
+
callee.udt_return_type is not None
|
|
762
|
+
and wrapper.udt_return_type != callee.udt_return_type
|
|
763
|
+
):
|
|
764
|
+
wrapper.udt_return_type = callee.udt_return_type
|
|
765
|
+
self._func_udt_return_types[wrapper.name] = (
|
|
766
|
+
callee.udt_return_type
|
|
767
|
+
)
|
|
768
|
+
changed = True
|
|
769
|
+
if not changed:
|
|
770
|
+
break
|
|
771
|
+
|
|
772
|
+
def _refresh_direct_terminal_array_temporary_returns(self) -> None:
|
|
773
|
+
"""Reconcile exact primitive returns without revisiting call sites.
|
|
774
|
+
|
|
775
|
+
``array.from(later_defined_udf()).get(0)`` can be visited before the
|
|
776
|
+
producer's return type exists. The ordinary fallback is then float,
|
|
777
|
+
while final codegen correctly learns the producer's primitive type.
|
|
778
|
+
Re-run only a preregistered, direct built-in temporary shape using the
|
|
779
|
+
cached structural resolver. It never calls ``_visit`` and therefore
|
|
780
|
+
cannot mint phantom TA/series/fixnan call-site state.
|
|
781
|
+
"""
|
|
782
|
+
pending = self._direct_terminal_array_temporary_exprs
|
|
783
|
+
if not pending:
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
# One new exact result can unlock one earlier direct dependency per
|
|
787
|
+
# pass. Cycles retain their existing fail-closed fallback.
|
|
788
|
+
for _ in range(len(pending) + 1):
|
|
789
|
+
changed = False
|
|
790
|
+
for name, terminal in pending.items():
|
|
791
|
+
spec = self._cached_terminal_temporary_array_get_spec(terminal)
|
|
792
|
+
if spec is None or spec.kind != "primitive":
|
|
793
|
+
continue
|
|
794
|
+
pine_type = self._element_pine_type(spec)
|
|
795
|
+
if pine_type in (PineType.UNKNOWN, PineType.VOID):
|
|
796
|
+
continue
|
|
797
|
+
if (
|
|
798
|
+
self._func_return_types.get(name) == pine_type
|
|
799
|
+
and self._func_return_type_specs.get(name) == spec
|
|
800
|
+
):
|
|
801
|
+
continue
|
|
802
|
+
self._func_return_types[name] = pine_type
|
|
803
|
+
self._func_return_type_specs[name] = spec
|
|
804
|
+
symbol = self._symbols.resolve(name)
|
|
805
|
+
if symbol is not None:
|
|
806
|
+
symbol.pine_type = pine_type
|
|
807
|
+
symbol.type_spec = spec
|
|
808
|
+
for func_info in self._func_infos:
|
|
809
|
+
if func_info.name == name:
|
|
810
|
+
func_info.return_type = pine_type
|
|
811
|
+
func_info.return_type_spec = spec
|
|
812
|
+
changed = True
|
|
813
|
+
if not changed:
|
|
814
|
+
break
|
|
815
|
+
|
|
816
|
+
def _direct_terminal_array_temporary_return_expr(
|
|
817
|
+
self,
|
|
818
|
+
func_def: FuncDef,
|
|
819
|
+
terminal: ASTNode | None,
|
|
820
|
+
) -> ASTNode | None:
|
|
821
|
+
"""Return the exact temporary read that determines a UDF return.
|
|
822
|
+
|
|
823
|
+
The established path accepts a direct terminal ``array.get``. A
|
|
824
|
+
single ordinary local may also carry that same value to a bare
|
|
825
|
+
terminal identity return::
|
|
826
|
+
|
|
827
|
+
reader() =>
|
|
828
|
+
value = array.from(later()).get(0)
|
|
829
|
+
value
|
|
830
|
+
|
|
831
|
+
Capture only that adjacent one-hop lexical shape while the function
|
|
832
|
+
scope is still live. Deferred reconciliation can then reuse the
|
|
833
|
+
initializer AST without re-visiting it after ``later`` is known.
|
|
834
|
+
Persistent/typed locals, alias chains, intervening statements, block
|
|
835
|
+
bindings, argument-bearing element calls, and every non-direct
|
|
836
|
+
producer stay outside this path.
|
|
837
|
+
"""
|
|
838
|
+
if self._terminal_array_get_uses_direct_temporary(terminal):
|
|
839
|
+
return terminal
|
|
840
|
+
alias_candidate = self._direct_terminal_array_temporary_alias_candidate(
|
|
841
|
+
func_def, terminal
|
|
842
|
+
)
|
|
843
|
+
if alias_candidate is None:
|
|
844
|
+
return None
|
|
845
|
+
_, declaration, element_call = alias_candidate
|
|
846
|
+
|
|
847
|
+
# Raw-name lookup is safe only because the exact declaration identity
|
|
848
|
+
# was attached to its live lexical Symbol by _visit_VarDecl.
|
|
849
|
+
symbol = self._symbols.resolve(terminal.name)
|
|
850
|
+
if (
|
|
851
|
+
symbol is None
|
|
852
|
+
or getattr(symbol, "_pf_decl_node_id", None) != id(declaration)
|
|
853
|
+
):
|
|
854
|
+
return None
|
|
855
|
+
|
|
856
|
+
known_definition = self._func_defs.get(element_call.callee.name)
|
|
857
|
+
if known_definition is not None and known_definition.params:
|
|
858
|
+
return None
|
|
859
|
+
return declaration.value
|
|
860
|
+
|
|
861
|
+
def _direct_terminal_array_temporary_alias_candidate(
|
|
862
|
+
self,
|
|
863
|
+
func_def: FuncDef,
|
|
864
|
+
terminal: ASTNode | None,
|
|
865
|
+
) -> tuple[int, VarDecl, FuncCall] | None:
|
|
866
|
+
"""Return one exact adjacent identity alias and its element call.
|
|
867
|
+
|
|
868
|
+
This helper is deliberately syntactic so the same program point can be
|
|
869
|
+
checked before any AST visit and captured later while declaration
|
|
870
|
+
identity is live. The declaration itself is therefore excluded from
|
|
871
|
+
the caller's active-binding scan: Pine evaluates its initializer before
|
|
872
|
+
introducing the new local name.
|
|
873
|
+
"""
|
|
874
|
+
if not isinstance(terminal, Identifier):
|
|
875
|
+
return None
|
|
876
|
+
|
|
877
|
+
declarations = [
|
|
878
|
+
(index, stmt)
|
|
879
|
+
for index, stmt in enumerate(func_def.body[:-1])
|
|
880
|
+
if isinstance(stmt, VarDecl) and stmt.name == terminal.name
|
|
881
|
+
]
|
|
882
|
+
if len(declarations) != 1:
|
|
883
|
+
return None
|
|
884
|
+
declaration_index, declaration = declarations[0]
|
|
885
|
+
if (
|
|
886
|
+
declaration.is_var
|
|
887
|
+
or declaration.is_varip
|
|
888
|
+
or declaration.type_hint is not None
|
|
889
|
+
or declaration_index != len(func_def.body) - 2
|
|
890
|
+
):
|
|
891
|
+
return None
|
|
892
|
+
|
|
893
|
+
initializer = declaration.value
|
|
894
|
+
if not self._terminal_array_get_uses_direct_temporary(initializer):
|
|
895
|
+
return None
|
|
896
|
+
element_call = self._direct_terminal_array_temporary_element_call(
|
|
897
|
+
initializer
|
|
898
|
+
)
|
|
899
|
+
if (
|
|
900
|
+
element_call is None
|
|
901
|
+
or element_call.args
|
|
902
|
+
or element_call.kwargs
|
|
903
|
+
):
|
|
904
|
+
return None
|
|
905
|
+
return declaration_index, declaration, element_call
|
|
906
|
+
|
|
907
|
+
@classmethod
|
|
908
|
+
def _direct_terminal_array_temporary_element_call(
|
|
909
|
+
cls,
|
|
910
|
+
terminal: ASTNode | None,
|
|
911
|
+
) -> FuncCall | None:
|
|
912
|
+
"""Return a direct UDF element call from the registered shape."""
|
|
913
|
+
if not isinstance(terminal, FuncCall) or not isinstance(
|
|
914
|
+
terminal.callee, MemberAccess
|
|
915
|
+
):
|
|
916
|
+
return None
|
|
917
|
+
callee = terminal.callee
|
|
918
|
+
if callee.member != "get":
|
|
919
|
+
return None
|
|
920
|
+
if isinstance(callee.object, Identifier) and callee.object.name == "array":
|
|
921
|
+
receiver = (
|
|
922
|
+
terminal.args[0]
|
|
923
|
+
if terminal.args
|
|
924
|
+
else terminal.kwargs.get("id")
|
|
925
|
+
)
|
|
926
|
+
else:
|
|
927
|
+
receiver = callee.object
|
|
928
|
+
|
|
929
|
+
while True:
|
|
930
|
+
copy_source = cls._direct_namespace_array_copy_source(receiver)
|
|
931
|
+
if copy_source is not None:
|
|
932
|
+
receiver = copy_source
|
|
933
|
+
continue
|
|
934
|
+
if (
|
|
935
|
+
isinstance(receiver, FuncCall)
|
|
936
|
+
and isinstance(receiver.callee, MemberAccess)
|
|
937
|
+
and receiver.callee.member == "copy"
|
|
938
|
+
and not receiver.args
|
|
939
|
+
and not receiver.kwargs
|
|
940
|
+
):
|
|
941
|
+
receiver = receiver.callee.object
|
|
942
|
+
continue
|
|
943
|
+
break
|
|
944
|
+
if not (
|
|
945
|
+
isinstance(receiver, FuncCall)
|
|
946
|
+
and isinstance(receiver.callee, MemberAccess)
|
|
947
|
+
and isinstance(receiver.callee.object, Identifier)
|
|
948
|
+
and receiver.callee.object.name == "array"
|
|
949
|
+
and receiver.callee.member == "from"
|
|
950
|
+
and receiver.args
|
|
951
|
+
):
|
|
952
|
+
return None
|
|
953
|
+
element = receiver.args[0]
|
|
954
|
+
if (
|
|
955
|
+
isinstance(element, FuncCall)
|
|
956
|
+
and isinstance(element.callee, Identifier)
|
|
957
|
+
):
|
|
958
|
+
return element
|
|
959
|
+
return None
|
|
960
|
+
|
|
961
|
+
@classmethod
|
|
962
|
+
def _direct_terminal_array_temporary_user_call(
|
|
963
|
+
cls,
|
|
964
|
+
terminal: ASTNode | None,
|
|
965
|
+
) -> FuncCall | None:
|
|
966
|
+
"""Return a forward-registerable namespace-functional element call."""
|
|
967
|
+
if not (
|
|
968
|
+
isinstance(terminal, FuncCall)
|
|
969
|
+
and isinstance(terminal.callee, MemberAccess)
|
|
970
|
+
and isinstance(terminal.callee.object, Identifier)
|
|
971
|
+
and terminal.callee.object.name == "array"
|
|
972
|
+
):
|
|
973
|
+
return None
|
|
974
|
+
element = cls._direct_terminal_array_temporary_element_call(terminal)
|
|
975
|
+
if element is not None and not element.args and not element.kwargs:
|
|
976
|
+
return element
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
def _register_resolved_direct_terminal_array_forward_calls(self) -> None:
|
|
980
|
+
"""Register a formerly forward zero-argument element call once.
|
|
981
|
+
|
|
982
|
+
The initial lexical visit cannot dispatch a later-defined UDF. Once
|
|
983
|
+
its definition exists, ordinary call handling can safely register the
|
|
984
|
+
call because this deliberately bounded shape has no arguments whose
|
|
985
|
+
lexical bindings could have gone out of scope.
|
|
986
|
+
"""
|
|
987
|
+
for terminal in self._direct_terminal_array_temporary_exprs.values():
|
|
988
|
+
call = self._direct_terminal_array_temporary_user_call(terminal)
|
|
989
|
+
if call is None:
|
|
990
|
+
continue
|
|
991
|
+
name = call.callee.name
|
|
992
|
+
if name not in self._func_defs:
|
|
993
|
+
continue
|
|
994
|
+
# Regular UDF default arguments are not emitted in C++ today.
|
|
995
|
+
# A syntactically empty call is forward-registerable only when the
|
|
996
|
+
# declaration itself is genuinely zero-parameter.
|
|
997
|
+
if self._func_defs[name].params:
|
|
998
|
+
continue
|
|
999
|
+
stateful = (
|
|
1000
|
+
name in self._func_ta_ranges
|
|
1001
|
+
or name in self._func_series_vars
|
|
1002
|
+
or name in self._func_var_members
|
|
1003
|
+
or name in self._func_fixnan_indices
|
|
1004
|
+
)
|
|
1005
|
+
if stateful and id(call) in self._func_call_cs_map:
|
|
1006
|
+
continue
|
|
1007
|
+
if not stateful and any(
|
|
1008
|
+
func_info.name == name for func_info in self._func_infos
|
|
1009
|
+
):
|
|
1010
|
+
continue
|
|
1011
|
+
self._handle_user_func_call(name, call)
|
|
1012
|
+
|
|
1013
|
+
def _check_direct_terminal_array_temporary_cycles(self) -> None:
|
|
1014
|
+
"""Reject recursion reached through a temporary-reader UDF edge.
|
|
1015
|
+
|
|
1016
|
+
Pine forbids recursive UDF execution. Forward registration makes a
|
|
1017
|
+
formerly unknown helper visible, so preserve the language boundary
|
|
1018
|
+
explicitly instead of generating a C++ recursion that only fails at
|
|
1019
|
+
runtime.
|
|
1020
|
+
"""
|
|
1021
|
+
wrappers = self._direct_terminal_array_temporary_exprs
|
|
1022
|
+
known = set(self._func_defs)
|
|
1023
|
+
|
|
1024
|
+
def callees(node: Any, seen: set[int] | None = None) -> set[str]:
|
|
1025
|
+
if node is None:
|
|
1026
|
+
return set()
|
|
1027
|
+
if seen is None:
|
|
1028
|
+
seen = set()
|
|
1029
|
+
if isinstance(node, (list, tuple, dict)) or hasattr(node, "__dict__"):
|
|
1030
|
+
node_id = id(node)
|
|
1031
|
+
if node_id in seen:
|
|
1032
|
+
return set()
|
|
1033
|
+
seen.add(node_id)
|
|
1034
|
+
if isinstance(node, (list, tuple)):
|
|
1035
|
+
return set().union(*(callees(item, seen) for item in node))
|
|
1036
|
+
if isinstance(node, dict):
|
|
1037
|
+
return set().union(*(callees(item, seen) for item in node.values()))
|
|
1038
|
+
if not hasattr(node, "__dict__"):
|
|
1039
|
+
return set()
|
|
1040
|
+
found: set[str] = set()
|
|
1041
|
+
if (
|
|
1042
|
+
isinstance(node, FuncCall)
|
|
1043
|
+
and isinstance(node.callee, Identifier)
|
|
1044
|
+
and node.callee.name in known
|
|
1045
|
+
):
|
|
1046
|
+
found.add(node.callee.name)
|
|
1047
|
+
for value in vars(node).values():
|
|
1048
|
+
found.update(callees(value, seen))
|
|
1049
|
+
return found
|
|
1050
|
+
|
|
1051
|
+
graph = {
|
|
1052
|
+
name: callees(func_def.body)
|
|
1053
|
+
for name, func_def in self._func_defs.items()
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
def path_to(start: str, target: str) -> list[str] | None:
|
|
1057
|
+
stack: list[tuple[str, list[str]]] = [(start, [start])]
|
|
1058
|
+
visited: set[str] = set()
|
|
1059
|
+
while stack:
|
|
1060
|
+
name, path = stack.pop()
|
|
1061
|
+
if name == target:
|
|
1062
|
+
return path
|
|
1063
|
+
if name in visited:
|
|
1064
|
+
continue
|
|
1065
|
+
visited.add(name)
|
|
1066
|
+
for child in sorted(graph.get(name, ()), reverse=True):
|
|
1067
|
+
stack.append((child, [*path, child]))
|
|
1068
|
+
return None
|
|
1069
|
+
|
|
1070
|
+
for owner, terminal in wrappers.items():
|
|
1071
|
+
element = self._direct_terminal_array_temporary_element_call(
|
|
1072
|
+
terminal
|
|
1073
|
+
)
|
|
1074
|
+
if element is None or element.callee.name not in known:
|
|
1075
|
+
continue
|
|
1076
|
+
return_path = path_to(element.callee.name, owner)
|
|
1077
|
+
if return_path is None:
|
|
1078
|
+
continue
|
|
1079
|
+
cycle = [owner, *return_path]
|
|
1080
|
+
self._error(
|
|
1081
|
+
"Recursive direct temporary-array reader cycle is not "
|
|
1082
|
+
f"supported: {' -> '.join(cycle)}.",
|
|
1083
|
+
element.loc,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
def _qualify_colliding_func_var_members(self) -> None:
|
|
1087
|
+
"""Give colliding ordinary-UDF primitive/collection vars distinct storage.
|
|
1088
|
+
|
|
1089
|
+
Pine function locals are lexical, but persistent locals are lowered to
|
|
1090
|
+
generated class members. Historically the direct FuncDef path used the
|
|
1091
|
+
raw Pine spelling as that member identity, so two functions declaring
|
|
1092
|
+
``var float state`` shared both the member and the raw-name-keyed
|
|
1093
|
+
initializer cache. Separate init flags did not help: both flags still
|
|
1094
|
+
guarded writes into one member, and the later definition's initializer
|
|
1095
|
+
won for *both* functions.
|
|
1096
|
+
|
|
1097
|
+
Keep this first migration deliberately bounded:
|
|
1098
|
+
|
|
1099
|
+
* direct persistent declarations in ordinary ``FuncDef`` bodies only;
|
|
1100
|
+
* only groups whose exact types are primitive or a supported collection;
|
|
1101
|
+
* only when its member collides with another persistent/global binding;
|
|
1102
|
+
* no output change for a non-colliding source.
|
|
1103
|
+
|
|
1104
|
+
``func_var_members`` deliberately retains raw lexical names. The new
|
|
1105
|
+
``func_var_storage_names`` overlay lets codegen clone exact members and
|
|
1106
|
+
activate raw->storage remaps at each declaration site without making a
|
|
1107
|
+
future local shadow earlier global reads.
|
|
1108
|
+
"""
|
|
1109
|
+
direct_node_ids_by_owner = {
|
|
1110
|
+
owner: {
|
|
1111
|
+
id(stmt)
|
|
1112
|
+
for stmt in func_def.body
|
|
1113
|
+
if isinstance(stmt, VarDecl)
|
|
1114
|
+
and (stmt.is_var or stmt.is_varip)
|
|
1115
|
+
}
|
|
1116
|
+
for owner, func_def in self._func_defs.items()
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
bindings_by_member: dict[str, list[int]] = {}
|
|
1120
|
+
for node_id, meta in self._var_member_metadata_by_node.items():
|
|
1121
|
+
bindings_by_member.setdefault(meta[1], []).append(node_id)
|
|
1122
|
+
ordinary_global_members = set(self._ordinary_global_binding_names)
|
|
1123
|
+
nonpersistent_series_raw_names = {
|
|
1124
|
+
name
|
|
1125
|
+
for node_id, name in self._series_decl_bindings
|
|
1126
|
+
if node_id not in self._var_member_metadata_by_node
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
qualifiable_bindings: list[tuple[int, str, VarDecl]] = []
|
|
1130
|
+
for node_id, meta in self._var_member_metadata_by_node.items():
|
|
1131
|
+
node, _member_name, _ptype, _init, is_callable_scoped = meta
|
|
1132
|
+
owner = self._var_member_owners_by_node.get(node_id)
|
|
1133
|
+
if (not is_callable_scoped
|
|
1134
|
+
or owner not in self._func_defs
|
|
1135
|
+
or node_id not in direct_node_ids_by_owner.get(owner, set())
|
|
1136
|
+
or not isinstance(node, VarDecl)):
|
|
1137
|
+
continue
|
|
1138
|
+
self._func_var_storage_names.setdefault(owner, {})[node.name] = (
|
|
1139
|
+
meta[1]
|
|
1140
|
+
)
|
|
1141
|
+
spec = self._var_member_type_specs_by_node.get(node_id)
|
|
1142
|
+
reserves_raw_series = (
|
|
1143
|
+
meta[1] in nonpersistent_series_raw_names
|
|
1144
|
+
# Preserve the established, more specific scalar-Series vs
|
|
1145
|
+
# persistent-map fail-closed diagnostic. Primitive/array/
|
|
1146
|
+
# matrix storage can still move out of the raw Series name.
|
|
1147
|
+
and (spec is None or spec.kind != "map")
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
# Only a storage identity that is currently shared needs
|
|
1151
|
+
# migration. This includes global-vs-UDF and method-vs-UDF
|
|
1152
|
+
# collisions, plus a declaration-bound non-persistent Series that
|
|
1153
|
+
# needs to retain the raw member spelling, not just two ordinary
|
|
1154
|
+
# UDF owners.
|
|
1155
|
+
if (len(bindings_by_member.get(meta[1], [])) < 2
|
|
1156
|
+
and meta[1] not in ordinary_global_members
|
|
1157
|
+
and not reserves_raw_series):
|
|
1158
|
+
continue
|
|
1159
|
+
|
|
1160
|
+
# The current overlay is owner/raw keyed. If this owner has two
|
|
1161
|
+
# declaration nodes already sharing the member (for example a
|
|
1162
|
+
# direct local plus a first nested-block shadow), it cannot route
|
|
1163
|
+
# them independently. Leave both untouched so the exact-member
|
|
1164
|
+
# collision diagnostic below fails closed instead of pretending
|
|
1165
|
+
# the owner was repaired while silently targeting the outer var.
|
|
1166
|
+
same_owner_nodes = [
|
|
1167
|
+
other_id
|
|
1168
|
+
for other_id in bindings_by_member[meta[1]]
|
|
1169
|
+
if self._var_member_owners_by_node.get(other_id) == owner
|
|
1170
|
+
]
|
|
1171
|
+
if len(same_owner_nodes) > 1:
|
|
1172
|
+
continue
|
|
1173
|
+
|
|
1174
|
+
if (spec is not None
|
|
1175
|
+
and spec.kind in {
|
|
1176
|
+
"primitive", "array", "map", "matrix",
|
|
1177
|
+
}):
|
|
1178
|
+
qualifiable_bindings.append((node_id, owner, node))
|
|
1179
|
+
|
|
1180
|
+
# Protect every existing user/class identity, including names declared
|
|
1181
|
+
# later in source. The deterministic allocator advances its leading
|
|
1182
|
+
# token when the user already occupies a base or derived clone spelling.
|
|
1183
|
+
used_names = {
|
|
1184
|
+
name for name, _ptype, _init in self._var_members
|
|
1185
|
+
}
|
|
1186
|
+
used_names.update(name for name, _ptype in self._global_var_decls)
|
|
1187
|
+
used_names.update(self._series_vars)
|
|
1188
|
+
used_names.update(self._func_defs)
|
|
1189
|
+
|
|
1190
|
+
# Generated members are referenced as bare identifiers inside emitted
|
|
1191
|
+
# methods. Protect against every authored identifier, not only class
|
|
1192
|
+
# members: a parameter/plain local/loop binder named like the helper
|
|
1193
|
+
# token (or one of its ``_csN`` / ``__niN`` clones) would otherwise
|
|
1194
|
+
# shadow the intended member in C++.
|
|
1195
|
+
authored_names: set[str] = set()
|
|
1196
|
+
|
|
1197
|
+
def collect_authored_names(value: Any) -> None:
|
|
1198
|
+
if isinstance(value, Identifier):
|
|
1199
|
+
authored_names.add(value.name)
|
|
1200
|
+
if isinstance(value, VarDecl):
|
|
1201
|
+
authored_names.add(value.name)
|
|
1202
|
+
elif isinstance(value, TupleAssign):
|
|
1203
|
+
authored_names.update(
|
|
1204
|
+
name for name in value.names if name != "_"
|
|
1205
|
+
)
|
|
1206
|
+
elif isinstance(value, ForStmt):
|
|
1207
|
+
if value.var:
|
|
1208
|
+
authored_names.add(value.var)
|
|
1209
|
+
elif isinstance(value, ForInStmt):
|
|
1210
|
+
if value.var:
|
|
1211
|
+
authored_names.add(value.var)
|
|
1212
|
+
authored_names.update(
|
|
1213
|
+
name for name in (value.vars or []) if name != "_"
|
|
1214
|
+
)
|
|
1215
|
+
elif isinstance(value, (FuncDef, MethodDef)):
|
|
1216
|
+
authored_names.add(value.name)
|
|
1217
|
+
authored_names.update(value.params)
|
|
1218
|
+
if isinstance(value, ASTNode):
|
|
1219
|
+
for child in vars(value).values():
|
|
1220
|
+
collect_authored_names(child)
|
|
1221
|
+
elif isinstance(value, (list, tuple)):
|
|
1222
|
+
for child in value:
|
|
1223
|
+
collect_authored_names(child)
|
|
1224
|
+
elif isinstance(value, dict):
|
|
1225
|
+
for child in value.values():
|
|
1226
|
+
collect_authored_names(child)
|
|
1227
|
+
|
|
1228
|
+
collect_authored_names(self._ast)
|
|
1229
|
+
used_names.update(authored_names)
|
|
1230
|
+
|
|
1231
|
+
allocated_storage_names: set[str] = set()
|
|
1232
|
+
|
|
1233
|
+
def generated_namespace_overlaps(candidate: str, other: str) -> bool:
|
|
1234
|
+
"""Whether either base can be emitted as the other's clone.
|
|
1235
|
+
|
|
1236
|
+
Stateful call-site and nested-instance storage is derived by
|
|
1237
|
+
appending ``_csN`` / ``__niN`` to the persistent member base. A
|
|
1238
|
+
distinct owner can legitimately make one of those suffixes part
|
|
1239
|
+
of its authored function name (for example ``left`` and
|
|
1240
|
+
``left_cs1``), so exact-name reservation alone is insufficient.
|
|
1241
|
+
Reserve the derived namespaces symmetrically, independent of
|
|
1242
|
+
source-definition order.
|
|
1243
|
+
"""
|
|
1244
|
+
return any(
|
|
1245
|
+
candidate.startswith(f"{other}{suffix}")
|
|
1246
|
+
or other.startswith(f"{candidate}{suffix}")
|
|
1247
|
+
for suffix in ("_cs", "__ni")
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
def helper_name_conflicts(candidate: str) -> bool:
|
|
1251
|
+
return (
|
|
1252
|
+
candidate in used_names
|
|
1253
|
+
or any(
|
|
1254
|
+
generated_namespace_overlaps(candidate, name)
|
|
1255
|
+
for name in used_names
|
|
1256
|
+
)
|
|
1257
|
+
or any(
|
|
1258
|
+
generated_namespace_overlaps(candidate, allocated)
|
|
1259
|
+
for allocated in allocated_storage_names
|
|
1260
|
+
)
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
storage_sequence = 0
|
|
1264
|
+
for node_id, owner, node in qualifiable_bindings:
|
|
1265
|
+
# Start in a codegen-reserved namespace so an adversarial but
|
|
1266
|
+
# valid raw name such as ``_fvinit_left`` cannot equal a
|
|
1267
|
+
# generated function-init flag. Put a monotonically allocated
|
|
1268
|
+
# token before every authored component: appending ``_2`` to a
|
|
1269
|
+
# clone-shaped base (``left_cs1``) would remain inside the
|
|
1270
|
+
# earlier base's ``_cs*`` namespace forever. Advancing this
|
|
1271
|
+
# leading token instead always reaches a disjoint namespace.
|
|
1272
|
+
# Non-colliding sources never enter this path and therefore
|
|
1273
|
+
# retain byte-identical output.
|
|
1274
|
+
while True:
|
|
1275
|
+
storage_sequence += 1
|
|
1276
|
+
member_name = (
|
|
1277
|
+
f"_pfv_{storage_sequence}_{node.name}__{owner}"
|
|
1278
|
+
)
|
|
1279
|
+
if not helper_name_conflicts(member_name):
|
|
1280
|
+
break
|
|
1281
|
+
used_names.add(member_name)
|
|
1282
|
+
allocated_storage_names.add(member_name)
|
|
1283
|
+
|
|
1284
|
+
old = self._var_member_metadata_by_node[node_id]
|
|
1285
|
+
self._var_member_metadata_by_node[node_id] = (
|
|
1286
|
+
old[0], member_name, old[2], old[3], old[4],
|
|
1287
|
+
)
|
|
1288
|
+
self._func_var_storage_names[owner][node.name] = member_name
|
|
1289
|
+
|
|
1290
|
+
qualified_raw_names = {
|
|
1291
|
+
raw_name
|
|
1292
|
+
for storage_names in self._func_var_storage_names.values()
|
|
1293
|
+
for raw_name, storage_name in storage_names.items()
|
|
1294
|
+
if storage_name != raw_name
|
|
1295
|
+
}
|
|
1296
|
+
exact_series_bindings = set(self._series_decl_bindings)
|
|
1297
|
+
exact_series_node_ids = {
|
|
1298
|
+
node_id for node_id, _name in exact_series_bindings
|
|
1299
|
+
}
|
|
1300
|
+
for name, (node_id, ptype, expr) in (
|
|
1301
|
+
self._ordinary_global_binding_info.items()
|
|
1302
|
+
):
|
|
1303
|
+
is_exact_series = (
|
|
1304
|
+
(node_id, name) in exact_series_bindings
|
|
1305
|
+
or (
|
|
1306
|
+
node_id in self._series_decl_nodes
|
|
1307
|
+
and node_id not in exact_series_node_ids
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1310
|
+
if is_exact_series:
|
|
1311
|
+
self._ordinary_global_series_names.add(name)
|
|
1312
|
+
continue
|
|
1313
|
+
if name not in qualified_raw_names:
|
|
1314
|
+
continue
|
|
1315
|
+
# A same-named callable history binding can suppress this scalar
|
|
1316
|
+
# or collection from the legacy raw ``global_var_decls`` list.
|
|
1317
|
+
# Restore only collision-qualified globals from their exact
|
|
1318
|
+
# declaration record; all unrelated generated output stays stable.
|
|
1319
|
+
if not any(existing == name for existing, _ in self._global_var_decls):
|
|
1320
|
+
self._global_var_decls.append((name, ptype))
|
|
1321
|
+
if expr is not None:
|
|
1322
|
+
self._global_expr_map[name] = expr
|
|
1323
|
+
|
|
1324
|
+
# Rebuild the two flat, member-keyed inventories from the exact
|
|
1325
|
+
# declaration metadata. Dict insertion order is source-analysis order;
|
|
1326
|
+
# unresolved out-of-scope raw collisions retain their established
|
|
1327
|
+
# last-initializer-wins behavior rather than being silently broadened.
|
|
1328
|
+
self._var_members = []
|
|
1329
|
+
self._var_member_init_exprs = {}
|
|
1330
|
+
for _node_id, meta in self._var_member_metadata_by_node.items():
|
|
1331
|
+
node, member_name, ptype, init_str, _callable = meta
|
|
1332
|
+
self._var_members.append((member_name, ptype, init_str))
|
|
1333
|
+
if node.value is not None:
|
|
1334
|
+
self._var_member_init_exprs[member_name] = node.value
|
|
1335
|
+
|
|
1336
|
+
# History tracking is declaration-exact. Replace only the persistent
|
|
1337
|
+
# member portion of the set so renamed Series members (and later their
|
|
1338
|
+
# call-site clones) keep the right storage type.
|
|
1339
|
+
old_persistent_members = {
|
|
1340
|
+
meta[1] for meta in self._var_member_metadata_by_node.values()
|
|
1341
|
+
}
|
|
1342
|
+
# The set above contains the post-rename names; include pre-rename raw
|
|
1343
|
+
# spellings from every declaration to remove stale entries as well.
|
|
1344
|
+
old_persistent_members.update(
|
|
1345
|
+
meta[0].name for meta in self._var_member_metadata_by_node.values()
|
|
1346
|
+
)
|
|
1347
|
+
nonpersistent_series = (
|
|
1348
|
+
self._series_var_members - old_persistent_members
|
|
1349
|
+
)
|
|
1350
|
+
exact_persistent_series = {
|
|
1351
|
+
self._var_member_metadata_by_node[node_id][1]
|
|
1352
|
+
for node_id in self._series_decl_nodes
|
|
1353
|
+
if node_id in self._var_member_metadata_by_node
|
|
1354
|
+
}
|
|
1355
|
+
self._series_var_members = (
|
|
1356
|
+
nonpersistent_series | exact_persistent_series
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
# Any exact member identity still owned by multiple persistent
|
|
1360
|
+
# declarations is outside the owner/raw overlay's safe scope (for
|
|
1361
|
+
# example two UDT methods, or a direct UDF var plus its first
|
|
1362
|
+
# same-named nested-block shadow). Do not retain the historical
|
|
1363
|
+
# last-initializer-wins behavior: fail closed until that declaration
|
|
1364
|
+
# shape has declaration-exact codegen remapping. Drawing collisions
|
|
1365
|
+
# keep their more specific diagnostic in the following check.
|
|
1366
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
1367
|
+
|
|
1368
|
+
remaining_by_member: dict[str, list[int]] = {}
|
|
1369
|
+
for node_id, meta in self._var_member_metadata_by_node.items():
|
|
1370
|
+
remaining_by_member.setdefault(meta[1], []).append(node_id)
|
|
1371
|
+
|
|
1372
|
+
for member_name, node_ids in remaining_by_member.items():
|
|
1373
|
+
binding_count = len(node_ids) + int(
|
|
1374
|
+
member_name in ordinary_global_members
|
|
1375
|
+
)
|
|
1376
|
+
if binding_count < 2:
|
|
1377
|
+
continue
|
|
1378
|
+
specs = [
|
|
1379
|
+
self._var_member_type_specs_by_node.get(node_id)
|
|
1380
|
+
for node_id in node_ids
|
|
1381
|
+
]
|
|
1382
|
+
if any(
|
|
1383
|
+
spec is not None
|
|
1384
|
+
and spec.kind == "udt"
|
|
1385
|
+
and spec.name in _DRAWING_TYPE_NAMES
|
|
1386
|
+
for spec in specs
|
|
1387
|
+
):
|
|
1388
|
+
continue
|
|
1389
|
+
# An ordinary global contributes to ``binding_count`` without a
|
|
1390
|
+
# persistent declaration-node entry, so a single unsupported
|
|
1391
|
+
# persistent binding can still reach this diagnostic.
|
|
1392
|
+
node = self._var_member_metadata_by_node[node_ids[-1]][0]
|
|
1393
|
+
raw_name = getattr(node, "name", member_name)
|
|
1394
|
+
self._error(
|
|
1395
|
+
"Persistent bindings named "
|
|
1396
|
+
f"'{raw_name}' still share generated storage across distinct "
|
|
1397
|
+
"lexical declarations; this declaration shape is not "
|
|
1398
|
+
"supported yet.",
|
|
1399
|
+
node.loc,
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
def _check_declaration_exact_series_storage_boundaries(self) -> None:
|
|
1403
|
+
"""Reject raw Series identities that still cross lexical owners.
|
|
1404
|
+
|
|
1405
|
+
Non-persistent history locals are currently emitted as class-level
|
|
1406
|
+
``Series`` members under their raw Pine spelling. That is safe for a
|
|
1407
|
+
single declaration, but the raw member is shared (or suppressed by a
|
|
1408
|
+
persistent member) when the spelling is reused by another lexical
|
|
1409
|
+
declaration. Parameters are intentionally absent from
|
|
1410
|
+
``series_decl_bindings`` and remain legal: their history buffers are
|
|
1411
|
+
routed by the existing callable parameter path.
|
|
1412
|
+
|
|
1413
|
+
Keep this boundary declaration-exact and fail closed until ordinary
|
|
1414
|
+
local Series storage has the same owner-qualified overlay as persistent
|
|
1415
|
+
callable state. Direct script Series declarations remain supported,
|
|
1416
|
+
including a same-named persistent UDF member that was qualified above.
|
|
1417
|
+
"""
|
|
1418
|
+
exact_series = set(self._series_decl_bindings)
|
|
1419
|
+
if not exact_series:
|
|
1420
|
+
return
|
|
1421
|
+
|
|
1422
|
+
# A helper reached exclusively through a request.security expression
|
|
1423
|
+
# does not use the ordinary raw class Series member: the security
|
|
1424
|
+
# emitter keys its local history by evaluator/function/source identity.
|
|
1425
|
+
# Preserve that proven isolation (including transitive helper calls)
|
|
1426
|
+
# while keeping any helper also reachable on the chart path subject to
|
|
1427
|
+
# the ordinary raw-storage collision checks below.
|
|
1428
|
+
known_func_names = set(self._func_defs)
|
|
1429
|
+
security_expression_ids = {
|
|
1430
|
+
id(sec.expression)
|
|
1431
|
+
for sec in getattr(self, "_security_calls", []) or []
|
|
1432
|
+
if getattr(sec, "expression", None) is not None
|
|
1433
|
+
}
|
|
1434
|
+
ordinary_roots: set[str] = set()
|
|
1435
|
+
security_roots: set[str] = set()
|
|
1436
|
+
call_edges: dict[str, set[str]] = {}
|
|
1437
|
+
|
|
1438
|
+
def scan_calls(
|
|
1439
|
+
value: Any,
|
|
1440
|
+
owner: str | None,
|
|
1441
|
+
in_security: bool = False,
|
|
1442
|
+
) -> None:
|
|
1443
|
+
if value is None:
|
|
1444
|
+
return
|
|
1445
|
+
in_security = in_security or id(value) in security_expression_ids
|
|
1446
|
+
if isinstance(value, (FuncDef, MethodDef)):
|
|
1447
|
+
return
|
|
1448
|
+
if isinstance(value, FuncCall):
|
|
1449
|
+
callee = value.callee
|
|
1450
|
+
if (isinstance(callee, Identifier)
|
|
1451
|
+
and callee.name in known_func_names):
|
|
1452
|
+
if in_security:
|
|
1453
|
+
security_roots.add(callee.name)
|
|
1454
|
+
elif owner is None:
|
|
1455
|
+
ordinary_roots.add(callee.name)
|
|
1456
|
+
else:
|
|
1457
|
+
call_edges.setdefault(owner, set()).add(callee.name)
|
|
1458
|
+
if isinstance(value, (list, tuple)):
|
|
1459
|
+
for item in value:
|
|
1460
|
+
scan_calls(item, owner, in_security)
|
|
1461
|
+
return
|
|
1462
|
+
if isinstance(value, dict):
|
|
1463
|
+
for item in value.values():
|
|
1464
|
+
scan_calls(item, owner, in_security)
|
|
1465
|
+
return
|
|
1466
|
+
if isinstance(value, ASTNode):
|
|
1467
|
+
for child in vars(value).values():
|
|
1468
|
+
scan_calls(child, owner, in_security)
|
|
1469
|
+
|
|
1470
|
+
scan_calls(self._ast.body, None)
|
|
1471
|
+
for owner, func_def in self._func_defs.items():
|
|
1472
|
+
scan_calls(func_def.body, owner)
|
|
1473
|
+
|
|
1474
|
+
def reachable(roots: set[str]) -> set[str]:
|
|
1475
|
+
found = set(roots)
|
|
1476
|
+
pending = list(roots)
|
|
1477
|
+
while pending:
|
|
1478
|
+
owner = pending.pop()
|
|
1479
|
+
for callee in call_edges.get(owner, set()):
|
|
1480
|
+
if callee in found:
|
|
1481
|
+
continue
|
|
1482
|
+
found.add(callee)
|
|
1483
|
+
pending.append(callee)
|
|
1484
|
+
return found
|
|
1485
|
+
|
|
1486
|
+
security_evaluator_only = (
|
|
1487
|
+
reachable(security_roots) - reachable(ordinary_roots)
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
self._nonpersistent_series_decl_names = {
|
|
1491
|
+
name
|
|
1492
|
+
for node_id, name in exact_series
|
|
1493
|
+
if node_id not in self._var_member_metadata_by_node
|
|
1494
|
+
}
|
|
1495
|
+
self._func_nonpersistent_series_vars = {}
|
|
1496
|
+
|
|
1497
|
+
persistent_raw_names = {
|
|
1498
|
+
meta[0].name
|
|
1499
|
+
for meta in self._var_member_metadata_by_node.values()
|
|
1500
|
+
if isinstance(meta[0], VarDecl)
|
|
1501
|
+
}
|
|
1502
|
+
unqualified_persistent_raw_names = {
|
|
1503
|
+
meta[0].name
|
|
1504
|
+
for meta in self._var_member_metadata_by_node.values()
|
|
1505
|
+
if (isinstance(meta[0], VarDecl)
|
|
1506
|
+
and meta[1] == meta[0].name)
|
|
1507
|
+
}
|
|
1508
|
+
callable_nonpersistent: dict[
|
|
1509
|
+
str, list[tuple[str, ASTNode]]
|
|
1510
|
+
] = {}
|
|
1511
|
+
block_nonpersistent: dict[str, list[ASTNode]] = {}
|
|
1512
|
+
|
|
1513
|
+
def record_bindings(
|
|
1514
|
+
stmt: ASTNode,
|
|
1515
|
+
names: list[str],
|
|
1516
|
+
callable_owner: str | None,
|
|
1517
|
+
direct_program: bool,
|
|
1518
|
+
) -> None:
|
|
1519
|
+
is_persistent = id(stmt) in self._var_member_metadata_by_node
|
|
1520
|
+
if is_persistent:
|
|
1521
|
+
return
|
|
1522
|
+
for name in names:
|
|
1523
|
+
if name == "_" or (id(stmt), name) not in exact_series:
|
|
1524
|
+
continue
|
|
1525
|
+
if callable_owner is not None:
|
|
1526
|
+
self._func_nonpersistent_series_vars.setdefault(
|
|
1527
|
+
callable_owner, set()
|
|
1528
|
+
).add(name)
|
|
1529
|
+
if (callable_owner in security_evaluator_only
|
|
1530
|
+
and name not in self._ordinary_global_binding_names
|
|
1531
|
+
and name not in unqualified_persistent_raw_names):
|
|
1532
|
+
continue
|
|
1533
|
+
callable_nonpersistent.setdefault(name, []).append(
|
|
1534
|
+
(callable_owner, stmt)
|
|
1535
|
+
)
|
|
1536
|
+
elif not direct_program:
|
|
1537
|
+
block_nonpersistent.setdefault(name, []).append(stmt)
|
|
1538
|
+
|
|
1539
|
+
def walk_embedded(
|
|
1540
|
+
value: Any,
|
|
1541
|
+
callable_owner: str | None,
|
|
1542
|
+
) -> None:
|
|
1543
|
+
"""Visit expression-valued if/switch branches as lexical blocks."""
|
|
1544
|
+
if value is None or isinstance(value, (FuncDef, MethodDef)):
|
|
1545
|
+
return
|
|
1546
|
+
if isinstance(value, IfStmt):
|
|
1547
|
+
walk_embedded(value.condition, callable_owner)
|
|
1548
|
+
walk_statements(value.body, callable_owner, False)
|
|
1549
|
+
walk_statements(
|
|
1550
|
+
value.else_body or [], callable_owner, False
|
|
1551
|
+
)
|
|
1552
|
+
return
|
|
1553
|
+
if isinstance(value, SwitchStmt):
|
|
1554
|
+
walk_embedded(value.expr, callable_owner)
|
|
1555
|
+
for case_expr, body in value.cases:
|
|
1556
|
+
walk_embedded(case_expr, callable_owner)
|
|
1557
|
+
walk_statements(body, callable_owner, False)
|
|
1558
|
+
walk_statements(
|
|
1559
|
+
value.default_body or [], callable_owner, False
|
|
1560
|
+
)
|
|
1561
|
+
return
|
|
1562
|
+
if isinstance(value, (list, tuple)):
|
|
1563
|
+
for item in value:
|
|
1564
|
+
walk_embedded(item, callable_owner)
|
|
1565
|
+
return
|
|
1566
|
+
if isinstance(value, dict):
|
|
1567
|
+
for item in value.values():
|
|
1568
|
+
walk_embedded(item, callable_owner)
|
|
1569
|
+
return
|
|
1570
|
+
if isinstance(value, ASTNode):
|
|
1571
|
+
for child in vars(value).values():
|
|
1572
|
+
walk_embedded(child, callable_owner)
|
|
1573
|
+
|
|
1574
|
+
def walk_statements(
|
|
1575
|
+
stmts: list[ASTNode],
|
|
1576
|
+
callable_owner: str | None,
|
|
1577
|
+
direct_program: bool,
|
|
1578
|
+
) -> None:
|
|
1579
|
+
for stmt in stmts:
|
|
1580
|
+
if isinstance(stmt, FuncDef):
|
|
1581
|
+
walk_statements(stmt.body, stmt.name, False)
|
|
1582
|
+
continue
|
|
1583
|
+
# Method state still uses a separate legacy ownership path.
|
|
1584
|
+
# Do not misclassify a method-local declaration as a script
|
|
1585
|
+
# block while this guard is deliberately scoped to FuncDef.
|
|
1586
|
+
if isinstance(stmt, MethodDef):
|
|
1587
|
+
continue
|
|
1588
|
+
if isinstance(stmt, VarDecl):
|
|
1589
|
+
walk_embedded(stmt.value, callable_owner)
|
|
1590
|
+
record_bindings(
|
|
1591
|
+
stmt, [stmt.name], callable_owner, direct_program
|
|
1592
|
+
)
|
|
1593
|
+
elif isinstance(stmt, TupleAssign):
|
|
1594
|
+
walk_embedded(stmt.value, callable_owner)
|
|
1595
|
+
record_bindings(
|
|
1596
|
+
stmt, stmt.names, callable_owner, direct_program
|
|
1597
|
+
)
|
|
1598
|
+
elif isinstance(stmt, Assignment):
|
|
1599
|
+
walk_embedded(stmt.target, callable_owner)
|
|
1600
|
+
walk_embedded(stmt.value, callable_owner)
|
|
1601
|
+
elif isinstance(stmt, ExprStmt):
|
|
1602
|
+
walk_embedded(stmt.expr, callable_owner)
|
|
1603
|
+
|
|
1604
|
+
if isinstance(stmt, IfStmt):
|
|
1605
|
+
walk_embedded(stmt.condition, callable_owner)
|
|
1606
|
+
walk_statements(stmt.body, callable_owner, False)
|
|
1607
|
+
walk_statements(
|
|
1608
|
+
stmt.else_body or [], callable_owner, False
|
|
1609
|
+
)
|
|
1610
|
+
elif isinstance(stmt, ForStmt):
|
|
1611
|
+
record_bindings(
|
|
1612
|
+
stmt, [stmt.var], callable_owner, False
|
|
1613
|
+
)
|
|
1614
|
+
walk_embedded(stmt.start, callable_owner)
|
|
1615
|
+
walk_embedded(stmt.end, callable_owner)
|
|
1616
|
+
walk_embedded(stmt.step, callable_owner)
|
|
1617
|
+
walk_statements(stmt.body, callable_owner, False)
|
|
1618
|
+
elif isinstance(stmt, ForInStmt):
|
|
1619
|
+
record_bindings(
|
|
1620
|
+
stmt,
|
|
1621
|
+
([stmt.var] if stmt.var else (stmt.vars or [])),
|
|
1622
|
+
callable_owner,
|
|
1623
|
+
False,
|
|
1624
|
+
)
|
|
1625
|
+
walk_embedded(stmt.iterable, callable_owner)
|
|
1626
|
+
walk_statements(stmt.body, callable_owner, False)
|
|
1627
|
+
elif isinstance(stmt, WhileStmt):
|
|
1628
|
+
walk_embedded(stmt.condition, callable_owner)
|
|
1629
|
+
walk_statements(stmt.body, callable_owner, False)
|
|
1630
|
+
elif isinstance(stmt, SwitchStmt):
|
|
1631
|
+
walk_embedded(stmt.expr, callable_owner)
|
|
1632
|
+
for _case, body in stmt.cases:
|
|
1633
|
+
walk_embedded(_case, callable_owner)
|
|
1634
|
+
walk_statements(body, callable_owner, False)
|
|
1635
|
+
walk_statements(
|
|
1636
|
+
stmt.default_body or [], callable_owner, False
|
|
1637
|
+
)
|
|
1638
|
+
|
|
1639
|
+
walk_statements(self._ast.body, None, True)
|
|
1640
|
+
|
|
1641
|
+
# A block-local history declaration is normally emitted as the raw
|
|
1642
|
+
# class Series member. A persistent or callable-local claimant with
|
|
1643
|
+
# that spelling either suppresses the member or shares its buffer.
|
|
1644
|
+
for name, bindings in block_nonpersistent.items():
|
|
1645
|
+
if (name not in persistent_raw_names
|
|
1646
|
+
and name not in callable_nonpersistent
|
|
1647
|
+
and len(bindings) == 1):
|
|
1648
|
+
continue
|
|
1649
|
+
self._error(
|
|
1650
|
+
"A top-level block history binding named "
|
|
1651
|
+
f"'{name}' shares raw generated Series state with another "
|
|
1652
|
+
"lexical declaration; declaration-exact block storage is not "
|
|
1653
|
+
"supported yet.",
|
|
1654
|
+
getattr(bindings[-1], "loc", None),
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
# A direct ordinary global reserves its class-member spelling even when
|
|
1658
|
+
# it is scalar. Persistent declarations reserve their raw spelling in
|
|
1659
|
+
# legacy var inventories even after exact owner qualification. Either
|
|
1660
|
+
# conflicts with a raw non-persistent callable Series member.
|
|
1661
|
+
for name, bindings in callable_nonpersistent.items():
|
|
1662
|
+
if (name not in self._ordinary_global_binding_names
|
|
1663
|
+
and name not in persistent_raw_names
|
|
1664
|
+
and len(bindings) == 1):
|
|
1665
|
+
continue
|
|
1666
|
+
self._error(
|
|
1667
|
+
"A callable history local named "
|
|
1668
|
+
f"'{name}' shares raw generated Series state with another "
|
|
1669
|
+
"lexical declaration; declaration-exact callable storage is "
|
|
1670
|
+
"not supported yet.",
|
|
1671
|
+
getattr(bindings[-1][1], "loc", None),
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
def _check_cross_callable_series_collection_collisions(self) -> None:
|
|
1675
|
+
"""Fail closed when legacy raw member names cannot preserve scoping.
|
|
1676
|
+
|
|
1677
|
+
Persistent callable locals are class members. A scalar Series local
|
|
1678
|
+
with the same spelling in another callable would otherwise bind to the
|
|
1679
|
+
collection member during C++ emission (``slot.push`` on PineMap). Keep
|
|
1680
|
+
the valid source out of malformed C++ until callable-owned Series
|
|
1681
|
+
members have fully namespaced storage.
|
|
1682
|
+
"""
|
|
1683
|
+
persistent_collections: dict[str, set[str]] = {}
|
|
1684
|
+
for owner, specs in self._func_collection_types.items():
|
|
1685
|
+
persistent_names = {
|
|
1686
|
+
item[0] for item in self._func_var_members.get(owner, [])
|
|
1687
|
+
}
|
|
1688
|
+
persistent_collections[owner] = {
|
|
1689
|
+
self._func_var_storage_names.get(owner, {}).get(name, name)
|
|
1690
|
+
for name, spec in specs.items()
|
|
1691
|
+
if name in persistent_names
|
|
1692
|
+
and self._type_spec_contains_map(spec)
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
emitted: set[tuple[str, str, str]] = set()
|
|
1696
|
+
for series_owner, names in self._func_series_vars.items():
|
|
1697
|
+
exact_series_names = {
|
|
1698
|
+
(
|
|
1699
|
+
self._func_var_storage_names.get(
|
|
1700
|
+
series_owner, {}
|
|
1701
|
+
).get(name, name)
|
|
1702
|
+
if self._func_var_storage_names.get(
|
|
1703
|
+
series_owner, {}
|
|
1704
|
+
).get(name, name) in self._series_var_members
|
|
1705
|
+
else name
|
|
1706
|
+
)
|
|
1707
|
+
|
|
1708
|
+
for name in names
|
|
1709
|
+
}
|
|
1710
|
+
for collection_owner, collection_names in persistent_collections.items():
|
|
1711
|
+
if series_owner == collection_owner:
|
|
1712
|
+
continue
|
|
1713
|
+
for exact_name in exact_series_names & collection_names:
|
|
1714
|
+
raw_name = next(
|
|
1715
|
+
(
|
|
1716
|
+
name for name in names
|
|
1717
|
+
if self._func_var_storage_names.get(
|
|
1718
|
+
series_owner, {}
|
|
1719
|
+
).get(name, name) == exact_name
|
|
1720
|
+
),
|
|
1721
|
+
exact_name,
|
|
1722
|
+
)
|
|
1723
|
+
key = (series_owner, collection_owner, exact_name)
|
|
1724
|
+
if key in emitted:
|
|
1725
|
+
continue
|
|
1726
|
+
emitted.add(key)
|
|
1727
|
+
node = self._func_series_history_nodes.get(
|
|
1728
|
+
(series_owner, raw_name)
|
|
1729
|
+
)
|
|
1730
|
+
self._error(
|
|
1731
|
+
"History references on a scalar callable local named "
|
|
1732
|
+
f"'{raw_name}' conflict with a persistent map local of the "
|
|
1733
|
+
"same name in another callable; scoped Series member "
|
|
1734
|
+
"storage is not implemented yet.",
|
|
1735
|
+
node.loc if node is not None else None,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
def _check_top_level_block_shadow_boundaries(self) -> None:
|
|
1739
|
+
"""Fail closed for block shadows whose storage is not node-exact yet.
|
|
1740
|
+
|
|
1741
|
+
Same-typed primitive non-history declarations can use an ordinary C++
|
|
1742
|
+
lexical local once the analyzer preserves the outer symbol (see the
|
|
1743
|
+
scoped control-flow visitors below). Aggregate/UDT registries, local
|
|
1744
|
+
history Series storage, and cross-type codegen typing remain raw-name
|
|
1745
|
+
keyed, so allowing those shapes to shadow a direct script binding could
|
|
1746
|
+
silently retarget the class member or emit invalid C++.
|
|
1747
|
+
"""
|
|
1748
|
+
def tuple_binding_type(
|
|
1749
|
+
stmt: TupleAssign, name: str
|
|
1750
|
+
) -> tuple[PineType, TypeSpec]:
|
|
1751
|
+
try:
|
|
1752
|
+
index = stmt.names.index(name)
|
|
1753
|
+
except ValueError:
|
|
1754
|
+
index = -1
|
|
1755
|
+
element_types = self._tuple_element_types_by_node.get(
|
|
1756
|
+
id(stmt.value), ()
|
|
1757
|
+
)
|
|
1758
|
+
inferred = (
|
|
1759
|
+
element_types[index]
|
|
1760
|
+
if 0 <= index < len(element_types)
|
|
1761
|
+
else PineType.FLOAT
|
|
1762
|
+
)
|
|
1763
|
+
# Mirrors _visit_TupleAssign: bool is the only newly exact family;
|
|
1764
|
+
# all numeric elements retain the established float storage.
|
|
1765
|
+
pine_type = (
|
|
1766
|
+
PineType.BOOL if inferred == PineType.BOOL else PineType.FLOAT
|
|
1767
|
+
)
|
|
1768
|
+
return pine_type, TypeSpec.primitive(pine_type.value)
|
|
1769
|
+
|
|
1770
|
+
direct: dict[str, tuple[PineType, TypeSpec | None]] = {}
|
|
1771
|
+
for stmt in self._ast.body:
|
|
1772
|
+
if isinstance(stmt, VarDecl):
|
|
1773
|
+
direct[stmt.name] = self._var_decl_types_by_node.get(
|
|
1774
|
+
id(stmt), (PineType.UNKNOWN, None)
|
|
1775
|
+
)
|
|
1776
|
+
elif isinstance(stmt, TupleAssign):
|
|
1777
|
+
for name in stmt.names:
|
|
1778
|
+
if name != "_":
|
|
1779
|
+
direct[name] = tuple_binding_type(stmt, name)
|
|
1780
|
+
|
|
1781
|
+
def nested_bindings(
|
|
1782
|
+
stmts: list[ASTNode], primitive_mismatch_unsafe: bool
|
|
1783
|
+
):
|
|
1784
|
+
for stmt in stmts:
|
|
1785
|
+
if isinstance(stmt, VarDecl):
|
|
1786
|
+
yield from embedded_bindings(stmt.value)
|
|
1787
|
+
yield stmt, stmt.name, primitive_mismatch_unsafe
|
|
1788
|
+
continue
|
|
1789
|
+
if isinstance(stmt, TupleAssign):
|
|
1790
|
+
yield from embedded_bindings(stmt.value)
|
|
1791
|
+
for name in stmt.names:
|
|
1792
|
+
if name != "_":
|
|
1793
|
+
yield stmt, name, primitive_mismatch_unsafe
|
|
1794
|
+
continue
|
|
1795
|
+
if isinstance(stmt, IfStmt):
|
|
1796
|
+
yield from embedded_bindings(stmt.condition)
|
|
1797
|
+
yield from nested_bindings(stmt.body, True)
|
|
1798
|
+
yield from nested_bindings(stmt.else_body or [], True)
|
|
1799
|
+
elif isinstance(stmt, (ForStmt, ForInStmt)):
|
|
1800
|
+
if isinstance(stmt, ForStmt):
|
|
1801
|
+
yield from embedded_bindings(stmt.start)
|
|
1802
|
+
yield from embedded_bindings(stmt.end)
|
|
1803
|
+
yield from embedded_bindings(stmt.step)
|
|
1804
|
+
else:
|
|
1805
|
+
yield from embedded_bindings(stmt.iterable)
|
|
1806
|
+
yield from nested_bindings(stmt.body, True)
|
|
1807
|
+
elif isinstance(stmt, WhileStmt):
|
|
1808
|
+
yield from embedded_bindings(stmt.condition)
|
|
1809
|
+
yield from nested_bindings(stmt.body, True)
|
|
1810
|
+
elif isinstance(stmt, SwitchStmt):
|
|
1811
|
+
yield from embedded_bindings(stmt.expr)
|
|
1812
|
+
for _case, body in stmt.cases:
|
|
1813
|
+
yield from embedded_bindings(_case)
|
|
1814
|
+
yield from nested_bindings(body, True)
|
|
1815
|
+
yield from nested_bindings(stmt.default_body or [], True)
|
|
1816
|
+
elif isinstance(stmt, Assignment):
|
|
1817
|
+
yield from embedded_bindings(stmt.target)
|
|
1818
|
+
yield from embedded_bindings(stmt.value)
|
|
1819
|
+
elif isinstance(stmt, ExprStmt):
|
|
1820
|
+
yield from embedded_bindings(stmt.expr)
|
|
1821
|
+
|
|
1822
|
+
def embedded_bindings(value: Any):
|
|
1823
|
+
"""Yield declarations in expression-valued control-flow blocks."""
|
|
1824
|
+
if value is None or isinstance(value, (FuncDef, MethodDef)):
|
|
1825
|
+
return
|
|
1826
|
+
if isinstance(value, IfStmt):
|
|
1827
|
+
yield from embedded_bindings(value.condition)
|
|
1828
|
+
yield from nested_bindings(value.body, True)
|
|
1829
|
+
yield from nested_bindings(value.else_body or [], True)
|
|
1830
|
+
return
|
|
1831
|
+
if isinstance(value, SwitchStmt):
|
|
1832
|
+
yield from embedded_bindings(value.expr)
|
|
1833
|
+
for case_expr, body in value.cases:
|
|
1834
|
+
yield from embedded_bindings(case_expr)
|
|
1835
|
+
yield from nested_bindings(body, True)
|
|
1836
|
+
yield from nested_bindings(value.default_body or [], True)
|
|
1837
|
+
return
|
|
1838
|
+
if isinstance(value, (list, tuple)):
|
|
1839
|
+
for item in value:
|
|
1840
|
+
yield from embedded_bindings(item)
|
|
1841
|
+
return
|
|
1842
|
+
if isinstance(value, dict):
|
|
1843
|
+
for item in value.values():
|
|
1844
|
+
yield from embedded_bindings(item)
|
|
1845
|
+
return
|
|
1846
|
+
if isinstance(value, ASTNode):
|
|
1847
|
+
for child in vars(value).values():
|
|
1848
|
+
yield from embedded_bindings(child)
|
|
1849
|
+
|
|
1850
|
+
top_level_blocks: list[tuple[list[ASTNode], bool]] = []
|
|
1851
|
+
for stmt in self._ast.body:
|
|
1852
|
+
if isinstance(stmt, IfStmt):
|
|
1853
|
+
top_level_blocks.extend([
|
|
1854
|
+
(stmt.body, True), (stmt.else_body or [], True)
|
|
1855
|
+
])
|
|
1856
|
+
elif isinstance(stmt, (ForStmt, ForInStmt)):
|
|
1857
|
+
top_level_blocks.append((stmt.body, True))
|
|
1858
|
+
elif isinstance(stmt, WhileStmt):
|
|
1859
|
+
top_level_blocks.append((stmt.body, True))
|
|
1860
|
+
elif isinstance(stmt, SwitchStmt):
|
|
1861
|
+
top_level_blocks.extend(
|
|
1862
|
+
(body, True) for _case, body in stmt.cases
|
|
1863
|
+
)
|
|
1864
|
+
top_level_blocks.append((stmt.default_body or [], True))
|
|
1865
|
+
|
|
1866
|
+
# Pine control flow is also expression-valued (for example
|
|
1867
|
+
# ``result = if ...``). Inspect those blocks without treating the
|
|
1868
|
+
# containing direct Program declaration itself as a nested binding.
|
|
1869
|
+
embedded_top_level_bindings: list[
|
|
1870
|
+
tuple[ASTNode, str, bool]
|
|
1871
|
+
] = []
|
|
1872
|
+
for stmt in self._ast.body:
|
|
1873
|
+
if isinstance(stmt, VarDecl):
|
|
1874
|
+
embedded_top_level_bindings.extend(
|
|
1875
|
+
embedded_bindings(stmt.value)
|
|
1876
|
+
)
|
|
1877
|
+
elif isinstance(stmt, TupleAssign):
|
|
1878
|
+
embedded_top_level_bindings.extend(
|
|
1879
|
+
embedded_bindings(stmt.value)
|
|
1880
|
+
)
|
|
1881
|
+
elif isinstance(stmt, Assignment):
|
|
1882
|
+
embedded_top_level_bindings.extend(
|
|
1883
|
+
embedded_bindings(stmt.target)
|
|
1884
|
+
)
|
|
1885
|
+
embedded_top_level_bindings.extend(
|
|
1886
|
+
embedded_bindings(stmt.value)
|
|
1887
|
+
)
|
|
1888
|
+
elif isinstance(stmt, ExprStmt):
|
|
1889
|
+
embedded_top_level_bindings.extend(
|
|
1890
|
+
embedded_bindings(stmt.expr)
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
exact_series = set(self._series_decl_bindings)
|
|
1894
|
+
exact_series_nodes = {node_id for node_id, _name in exact_series}
|
|
1895
|
+
aggregate_kinds = {"array", "map", "matrix", "udt"}
|
|
1896
|
+
candidate_bindings = list(embedded_top_level_bindings)
|
|
1897
|
+
for block, primitive_mismatch_unsafe in top_level_blocks:
|
|
1898
|
+
candidate_bindings.extend(
|
|
1899
|
+
nested_bindings(block, primitive_mismatch_unsafe)
|
|
1900
|
+
)
|
|
1901
|
+
for node, name, mismatch_unsafe in candidate_bindings:
|
|
1902
|
+
if name not in direct:
|
|
1903
|
+
continue
|
|
1904
|
+
if isinstance(node, ForStmt):
|
|
1905
|
+
nested_type, nested_spec = (
|
|
1906
|
+
PineType.INT, TypeSpec.primitive("int")
|
|
1907
|
+
)
|
|
1908
|
+
elif isinstance(node, TupleAssign):
|
|
1909
|
+
nested_type, nested_spec = tuple_binding_type(node, name)
|
|
1910
|
+
else:
|
|
1911
|
+
nested_type, nested_spec = self._var_decl_types_by_node.get(
|
|
1912
|
+
id(node), (PineType.FLOAT, None)
|
|
1913
|
+
)
|
|
1914
|
+
direct_type, direct_spec = direct[name]
|
|
1915
|
+
is_exact_series = (
|
|
1916
|
+
(id(node), name) in exact_series
|
|
1917
|
+
or (
|
|
1918
|
+
id(node) in self._series_decl_nodes
|
|
1919
|
+
and id(node) not in exact_series_nodes
|
|
1920
|
+
)
|
|
1921
|
+
)
|
|
1922
|
+
has_aggregate = any(
|
|
1923
|
+
spec is not None and spec.kind in aggregate_kinds
|
|
1924
|
+
for spec in (direct_spec, nested_spec)
|
|
1925
|
+
)
|
|
1926
|
+
has_incompatible_primitive = (
|
|
1927
|
+
mismatch_unsafe and nested_type != direct_type
|
|
1928
|
+
# The standard corpus uses a loop-local inferred int under
|
|
1929
|
+
# a direct float binding (Hexatrades ``base``). C++ widens
|
|
1930
|
+
# that RHS safely. The reverse direction and all other
|
|
1931
|
+
# cross-type shadows can truncate or fail compilation.
|
|
1932
|
+
and not (
|
|
1933
|
+
direct_type == PineType.FLOAT
|
|
1934
|
+
and nested_type == PineType.INT
|
|
1935
|
+
)
|
|
1936
|
+
)
|
|
1937
|
+
if (not is_exact_series
|
|
1938
|
+
and not has_aggregate
|
|
1939
|
+
and not has_incompatible_primitive):
|
|
1940
|
+
continue
|
|
1941
|
+
self._error(
|
|
1942
|
+
"A top-level block binding named "
|
|
1943
|
+
f"'{name}' shadows a direct script binding with history, "
|
|
1944
|
+
"aggregate, or incompatible typed state; declaration-exact "
|
|
1945
|
+
"block storage is not supported yet.",
|
|
1946
|
+
getattr(node, "loc", None),
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
def _check_persistent_drawing_member_collisions(self) -> None:
|
|
1950
|
+
"""Fail closed when two lexical drawing vars share one C++ member.
|
|
1951
|
+
|
|
1952
|
+
Persistent callable locals and the first block-scoped declaration still
|
|
1953
|
+
use their raw Pine spelling as the class-member identity. Reusing that
|
|
1954
|
+
spelling in another callable/global scope would silently share state
|
|
1955
|
+
(and can emit uncompilable C++ when the handle kinds differ). Sibling
|
|
1956
|
+
on-bar blocks that received ``__blkN`` identities are already safe.
|
|
1957
|
+
Until all callable storage is owner-qualified, reject only the drawing
|
|
1958
|
+
collisions made reachable by drawing-handle target typing.
|
|
1959
|
+
"""
|
|
1960
|
+
from .types import _DRAWING_TYPE_NAMES
|
|
1961
|
+
|
|
1962
|
+
persistent_groups: dict[str, list[tuple[ASTNode, TypeSpec | None]]] = {}
|
|
1963
|
+
for node_id, meta in self._var_member_metadata_by_node.items():
|
|
1964
|
+
node, member_name, _ptype, _init, _callable = meta
|
|
1965
|
+
spec = self._var_member_type_specs_by_node.get(node_id)
|
|
1966
|
+
persistent_groups.setdefault(member_name, []).append((node, spec))
|
|
1967
|
+
|
|
1968
|
+
def is_drawing(spec: TypeSpec | None) -> bool:
|
|
1969
|
+
return bool(
|
|
1970
|
+
spec is not None
|
|
1971
|
+
and spec.kind == "udt"
|
|
1972
|
+
and spec.name in _DRAWING_TYPE_NAMES
|
|
1973
|
+
)
|
|
1974
|
+
|
|
1975
|
+
for member_name, bindings in persistent_groups.items():
|
|
1976
|
+
if len(bindings) < 2 or not any(is_drawing(spec) for _, spec in bindings):
|
|
1977
|
+
continue
|
|
1978
|
+
node = bindings[1][0]
|
|
1979
|
+
self._error(
|
|
1980
|
+
"Persistent drawing bindings named "
|
|
1981
|
+
f"'{getattr(node, 'name', member_name)}' in distinct lexical "
|
|
1982
|
+
"owners would share one generated state member; rename one "
|
|
1983
|
+
"binding until callable-owned persistent storage is "
|
|
1984
|
+
"owner-qualified.",
|
|
1985
|
+
node.loc,
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
global_names = set(self._ordinary_global_binding_names)
|
|
1989
|
+
global_drawing_names: set[str] = set()
|
|
1990
|
+
for stmt in self._ast.body:
|
|
1991
|
+
if not isinstance(stmt, VarDecl) or stmt.is_var or stmt.is_varip:
|
|
1992
|
+
continue
|
|
1993
|
+
spec = (
|
|
1994
|
+
self._type_spec_from_hint(stmt.type_hint)
|
|
1995
|
+
if stmt.type_hint
|
|
1996
|
+
else self._type_spec_from_expr(stmt.value)
|
|
1997
|
+
)
|
|
1998
|
+
if is_drawing(spec):
|
|
1999
|
+
global_drawing_names.add(stmt.name)
|
|
2000
|
+
|
|
2001
|
+
for member_name, bindings in persistent_groups.items():
|
|
2002
|
+
if member_name not in global_names:
|
|
2003
|
+
continue
|
|
2004
|
+
if not (
|
|
2005
|
+
member_name in global_drawing_names
|
|
2006
|
+
or any(is_drawing(spec) for _, spec in bindings)
|
|
2007
|
+
):
|
|
2008
|
+
continue
|
|
2009
|
+
node = bindings[0][0]
|
|
2010
|
+
self._error(
|
|
2011
|
+
"Persistent drawing state named "
|
|
2012
|
+
f"'{getattr(node, 'name', member_name)}' collides with a "
|
|
2013
|
+
"top-level class-member binding; rename one binding until "
|
|
2014
|
+
"persistent storage is owner-qualified.",
|
|
2015
|
+
node.loc,
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
# Callable persistent drawing storage is currently cloned as a class
|
|
2019
|
+
# member and remapped by raw Pine name. If such a declaration shadows
|
|
2020
|
+
# an ancestor local or parameter, the preloaded clone remap and C++
|
|
2021
|
+
# lexical scope disagree about which binding is visible before/after
|
|
2022
|
+
# the declaration. Reject this narrow shape instead of silently
|
|
2023
|
+
# reading the parameter/local from the wrong storage. Independent
|
|
2024
|
+
# sibling branches receive separate lexical inventories here (the
|
|
2025
|
+
# existing distinct-owner collision gate above may still reject two
|
|
2026
|
+
# persistent drawings that would share the same member identity).
|
|
2027
|
+
callable_drawing_nodes = {
|
|
2028
|
+
node_id
|
|
2029
|
+
for node_id, meta in self._var_member_metadata_by_node.items()
|
|
2030
|
+
if meta[4] and is_drawing(
|
|
2031
|
+
self._var_member_type_specs_by_node.get(node_id)
|
|
2032
|
+
)
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
def walk_callable_body(
|
|
2036
|
+
body: list[ASTNode],
|
|
2037
|
+
inherited_names: set[str],
|
|
2038
|
+
) -> None:
|
|
2039
|
+
def walk_embedded_controls(value: Any, visible: set[str]) -> None:
|
|
2040
|
+
"""Find block-valued if/switch expressions inside an RHS.
|
|
2041
|
+
|
|
2042
|
+
Pine permits ``x = if ...``. Those branch declarations have
|
|
2043
|
+
the same storage hazard as statement-level blocks, and the RHS
|
|
2044
|
+
must be inspected against the pre-declaration environment.
|
|
2045
|
+
"""
|
|
2046
|
+
if value is None:
|
|
2047
|
+
return
|
|
2048
|
+
if isinstance(value, IfStmt):
|
|
2049
|
+
walk_embedded_controls(value.condition, visible)
|
|
2050
|
+
walk_callable_body(value.body, visible)
|
|
2051
|
+
walk_callable_body(value.else_body, visible)
|
|
2052
|
+
return
|
|
2053
|
+
if isinstance(value, SwitchStmt):
|
|
2054
|
+
walk_embedded_controls(value.expr, visible)
|
|
2055
|
+
for case_expr, case_body in value.cases:
|
|
2056
|
+
walk_embedded_controls(case_expr, visible)
|
|
2057
|
+
walk_callable_body(case_body, visible)
|
|
2058
|
+
walk_callable_body(value.default_body, visible)
|
|
2059
|
+
return
|
|
2060
|
+
if isinstance(value, (FuncDef, MethodDef)):
|
|
2061
|
+
return
|
|
2062
|
+
if isinstance(value, (list, tuple)):
|
|
2063
|
+
for item in value:
|
|
2064
|
+
walk_embedded_controls(item, visible)
|
|
2065
|
+
return
|
|
2066
|
+
if isinstance(value, dict):
|
|
2067
|
+
for item in value.values():
|
|
2068
|
+
walk_embedded_controls(item, visible)
|
|
2069
|
+
return
|
|
2070
|
+
if isinstance(value, ASTNode):
|
|
2071
|
+
for child in vars(value).values():
|
|
2072
|
+
walk_embedded_controls(child, visible)
|
|
2073
|
+
|
|
2074
|
+
visible = set(inherited_names)
|
|
2075
|
+
for stmt in body:
|
|
2076
|
+
if isinstance(stmt, VarDecl):
|
|
2077
|
+
walk_embedded_controls(stmt.value, visible)
|
|
2078
|
+
if (id(stmt) in callable_drawing_nodes
|
|
2079
|
+
and stmt.name in visible):
|
|
2080
|
+
self._error(
|
|
2081
|
+
"Persistent drawing binding "
|
|
2082
|
+
f"'{stmt.name}' shadows an ancestor callable "
|
|
2083
|
+
"parameter or local; rename one binding until "
|
|
2084
|
+
"callable-owned persistent storage has lexical "
|
|
2085
|
+
"owner qualification.",
|
|
2086
|
+
stmt.loc,
|
|
2087
|
+
)
|
|
2088
|
+
visible.add(stmt.name)
|
|
2089
|
+
continue
|
|
2090
|
+
if isinstance(stmt, TupleAssign):
|
|
2091
|
+
walk_embedded_controls(stmt.value, visible)
|
|
2092
|
+
visible.update(stmt.names)
|
|
2093
|
+
continue
|
|
2094
|
+
if isinstance(stmt, Assignment):
|
|
2095
|
+
walk_embedded_controls(stmt.target, visible)
|
|
2096
|
+
walk_embedded_controls(stmt.value, visible)
|
|
2097
|
+
continue
|
|
2098
|
+
if isinstance(stmt, ExprStmt):
|
|
2099
|
+
walk_embedded_controls(stmt.expr, visible)
|
|
2100
|
+
continue
|
|
2101
|
+
if isinstance(stmt, IfStmt):
|
|
2102
|
+
walk_embedded_controls(stmt.condition, visible)
|
|
2103
|
+
walk_callable_body(stmt.body, visible)
|
|
2104
|
+
walk_callable_body(stmt.else_body, visible)
|
|
2105
|
+
continue
|
|
2106
|
+
if isinstance(stmt, WhileStmt):
|
|
2107
|
+
walk_embedded_controls(stmt.condition, visible)
|
|
2108
|
+
walk_callable_body(stmt.body, visible)
|
|
2109
|
+
continue
|
|
2110
|
+
if isinstance(stmt, ForStmt):
|
|
2111
|
+
walk_embedded_controls(stmt.start, visible)
|
|
2112
|
+
walk_embedded_controls(stmt.end, visible)
|
|
2113
|
+
walk_embedded_controls(stmt.step, visible)
|
|
2114
|
+
walk_callable_body(stmt.body, visible | {stmt.var})
|
|
2115
|
+
continue
|
|
2116
|
+
if isinstance(stmt, ForInStmt):
|
|
2117
|
+
walk_embedded_controls(stmt.iterable, visible)
|
|
2118
|
+
loop_names = set(stmt.vars or [])
|
|
2119
|
+
if stmt.var:
|
|
2120
|
+
loop_names.add(stmt.var)
|
|
2121
|
+
walk_callable_body(stmt.body, visible | loop_names)
|
|
2122
|
+
continue
|
|
2123
|
+
if isinstance(stmt, SwitchStmt):
|
|
2124
|
+
walk_embedded_controls(stmt.expr, visible)
|
|
2125
|
+
for case_expr, case_body in stmt.cases:
|
|
2126
|
+
walk_embedded_controls(case_expr, visible)
|
|
2127
|
+
walk_callable_body(case_body, visible)
|
|
2128
|
+
walk_callable_body(stmt.default_body, visible)
|
|
2129
|
+
|
|
2130
|
+
for stmt in self._ast.body:
|
|
2131
|
+
if isinstance(stmt, (FuncDef, MethodDef)):
|
|
2132
|
+
walk_callable_body(stmt.body, set(stmt.params))
|
|
2133
|
+
|
|
361
2134
|
def _record_global_binding_stmt(self, name: str, pine_type: PineType,
|
|
362
2135
|
is_var: bool, decl_node: ASTNode | None = None) -> None:
|
|
363
2136
|
info = self._global_binding_infos.get(name)
|
|
@@ -507,27 +2280,43 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
507
2280
|
|
|
508
2281
|
recv = call.callee.object
|
|
509
2282
|
method = call.callee.member
|
|
510
|
-
|
|
2283
|
+
receiver_type_name: str | None = None
|
|
511
2284
|
if isinstance(recv, Identifier):
|
|
512
|
-
udt_name = self._udt_var_types.get(recv.name)
|
|
513
2285
|
owner_info = func_info_by_name.get(owner or "")
|
|
514
|
-
|
|
515
|
-
|
|
2286
|
+
# Resolve the active callable's lexical parameters before the
|
|
2287
|
+
# flat variable registry. A parameter may legally shadow a
|
|
2288
|
+
# top-level UDT variable with a different type; consulting the
|
|
2289
|
+
# global binding first misidentifies the method call edge and
|
|
2290
|
+
# silently shares state between written wrapper call sites.
|
|
2291
|
+
if owner_info is not None and owner_info.node is not None:
|
|
516
2292
|
if (getattr(owner_info, "is_udt_method", False)
|
|
517
2293
|
and owner_info.node.params
|
|
518
2294
|
and recv.name == owner_info.node.params[0]):
|
|
519
|
-
|
|
2295
|
+
owner_specs = list(
|
|
2296
|
+
getattr(owner_info, "param_type_specs", ()) or ()
|
|
2297
|
+
)
|
|
2298
|
+
receiver_type_name = method_receiver_type_name(
|
|
2299
|
+
owner_specs[0] if owner_specs else None
|
|
2300
|
+
) or owner_info.udt_type_name
|
|
520
2301
|
elif recv.name in owner_info.node.params:
|
|
521
2302
|
param_idx = owner_info.node.params.index(recv.name)
|
|
522
2303
|
specs = getattr(owner_info, "param_type_specs", []) or []
|
|
523
2304
|
spec = specs[param_idx] if param_idx < len(specs) else None
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
2305
|
+
receiver_type_name = method_receiver_type_name(spec)
|
|
2306
|
+
# Resolve the surviving exact global/lexical symbol before the
|
|
2307
|
+
# flat raw-name registry. A later callable-local declaration can
|
|
2308
|
+
# overwrite that registry and otherwise attach the wrong stateful
|
|
2309
|
+
# method edge to wrappers and their written call sites.
|
|
2310
|
+
if receiver_type_name is None:
|
|
527
2311
|
spec = self._type_spec_from_expr(recv)
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
2312
|
+
receiver_type_name = method_receiver_type_name(spec)
|
|
2313
|
+
if receiver_type_name is None and isinstance(recv, Identifier):
|
|
2314
|
+
receiver_type_name = self._udt_var_types.get(recv.name)
|
|
2315
|
+
key = (
|
|
2316
|
+
f"{receiver_type_name}.{method}"
|
|
2317
|
+
if receiver_type_name
|
|
2318
|
+
else ""
|
|
2319
|
+
)
|
|
531
2320
|
return key if key in func_defs else None
|
|
532
2321
|
|
|
533
2322
|
def _find_calls(node, known_funcs: set[str],
|
|
@@ -569,6 +2358,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
569
2358
|
calls_by_callee: dict[str, list[FuncCall]] = {
|
|
570
2359
|
name: [] for name in known_func_names
|
|
571
2360
|
}
|
|
2361
|
+
call_edges: list[tuple[str | None, str, FuncCall]] = []
|
|
572
2362
|
# Preserve source order across definitions and top-level statements.
|
|
573
2363
|
# Method bodies use their ``Type.method`` owner so ``self.sibling()``
|
|
574
2364
|
# resolves without relying on a now-exited symbol-table scope.
|
|
@@ -581,6 +2371,198 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
581
2371
|
owner = None
|
|
582
2372
|
for callee, call in _find_calls(stmt, known_func_names, owner):
|
|
583
2373
|
calls_by_callee.setdefault(callee, []).append(call)
|
|
2374
|
+
call_edges.append((owner, callee, call))
|
|
2375
|
+
|
|
2376
|
+
# A source-ordered method visit threads TA constructor arguments through
|
|
2377
|
+
# any already-known method callee. Forward definitions require the
|
|
2378
|
+
# complete call graph above, and an edge may need revisiting even after
|
|
2379
|
+
# it has a cs identity: its callee can acquire another exact TA owner (or
|
|
2380
|
+
# a refined per-owner ctor template) later in this same closure.
|
|
2381
|
+
#
|
|
2382
|
+
# Track the exact source-index/template snapshot materialized for each
|
|
2383
|
+
# textual edge. Only the delta is materialized on a later round, which
|
|
2384
|
+
# prevents cs>0 state from being cloned twice. A finite callable graph
|
|
2385
|
+
# converges in at most its path depth; retain a conservative explicit
|
|
2386
|
+
# bound and fail closed rather than looping on a recursive TA cycle.
|
|
2387
|
+
max_ta_rounds = max(2, len(func_defs) + len(call_edges) + 2)
|
|
2388
|
+
last_changed_call: FuncCall | None = None
|
|
2389
|
+
for _round in range(max_ta_rounds):
|
|
2390
|
+
late_method_ta_changed = False
|
|
2391
|
+
for owner, callee, call in call_edges:
|
|
2392
|
+
call_id = id(call)
|
|
2393
|
+
if call_id in self._func_inherited_call_nodes:
|
|
2394
|
+
continue
|
|
2395
|
+
callee_info = func_info_by_name.get(callee)
|
|
2396
|
+
if (
|
|
2397
|
+
callee_info is None
|
|
2398
|
+
or not getattr(callee_info, "is_udt_method", False)
|
|
2399
|
+
or callee not in self._func_ta_ranges
|
|
2400
|
+
):
|
|
2401
|
+
continue
|
|
2402
|
+
|
|
2403
|
+
source_indices = list(self._func_ta_indices.get(callee, ()))
|
|
2404
|
+
if not source_indices:
|
|
2405
|
+
source_range = self._func_ta_ranges[callee]
|
|
2406
|
+
source_indices = list(range(*source_range))
|
|
2407
|
+
callee_templates = self._func_ta_ctor_args.get(callee, {})
|
|
2408
|
+
processed_templates = self._func_ta_call_templates.get(
|
|
2409
|
+
call_id, {}
|
|
2410
|
+
)
|
|
2411
|
+
existing_site = self._func_call_cs_map.get(call_id)
|
|
2412
|
+
candidate_cs_idx = (
|
|
2413
|
+
existing_site[1]
|
|
2414
|
+
if existing_site is not None and existing_site[0] == callee
|
|
2415
|
+
else self._func_call_site_count.get(callee, 0)
|
|
2416
|
+
)
|
|
2417
|
+
selected_targets = self._func_ta_call_targets.get(
|
|
2418
|
+
(call_id, candidate_cs_idx), {}
|
|
2419
|
+
)
|
|
2420
|
+
owner_indices = (
|
|
2421
|
+
set(self._func_ta_indices.get(owner, ()))
|
|
2422
|
+
if owner is not None
|
|
2423
|
+
else set()
|
|
2424
|
+
)
|
|
2425
|
+
owner_templates = (
|
|
2426
|
+
self._func_ta_ctor_args.get(owner, {})
|
|
2427
|
+
if owner is not None
|
|
2428
|
+
else {}
|
|
2429
|
+
)
|
|
2430
|
+
stale_indices: list[int] = []
|
|
2431
|
+
for index in source_indices:
|
|
2432
|
+
site = self._ta_call_sites[index]
|
|
2433
|
+
source_template = tuple(
|
|
2434
|
+
callee_templates.get(
|
|
2435
|
+
index,
|
|
2436
|
+
getattr(site, "_orig_ctor_args", site.ctor_args),
|
|
2437
|
+
)
|
|
2438
|
+
)
|
|
2439
|
+
target = selected_targets.get(index)
|
|
2440
|
+
if (
|
|
2441
|
+
processed_templates.get(index) != source_template
|
|
2442
|
+
or target is None
|
|
2443
|
+
or (
|
|
2444
|
+
owner is not None
|
|
2445
|
+
and (
|
|
2446
|
+
target not in owner_indices
|
|
2447
|
+
or target not in owner_templates
|
|
2448
|
+
)
|
|
2449
|
+
)
|
|
2450
|
+
):
|
|
2451
|
+
stale_indices.append(index)
|
|
2452
|
+
if not stale_indices:
|
|
2453
|
+
continue
|
|
2454
|
+
|
|
2455
|
+
if existing_site is not None and existing_site[0] == callee:
|
|
2456
|
+
cs_idx = existing_site[1]
|
|
2457
|
+
materialize_fixnan = False
|
|
2458
|
+
else:
|
|
2459
|
+
cs_idx = candidate_cs_idx
|
|
2460
|
+
self._func_call_site_count[callee] = cs_idx + 1
|
|
2461
|
+
self._func_call_cs_map[call_id] = (callee, cs_idx)
|
|
2462
|
+
materialize_fixnan = True
|
|
2463
|
+
|
|
2464
|
+
selected = self._materialize_user_func_call_site_state(
|
|
2465
|
+
callee,
|
|
2466
|
+
cs_idx,
|
|
2467
|
+
call,
|
|
2468
|
+
ta_site_indices=stale_indices,
|
|
2469
|
+
materialize_fixnan=materialize_fixnan,
|
|
2470
|
+
)
|
|
2471
|
+
if selected and owner is not None:
|
|
2472
|
+
current_indices = self._func_ta_indices.setdefault(owner, [])
|
|
2473
|
+
current_indices[:] = sorted(
|
|
2474
|
+
set(current_indices) | set(selected.values())
|
|
2475
|
+
)
|
|
2476
|
+
self._func_ta_ranges[owner] = (
|
|
2477
|
+
min(current_indices), max(current_indices) + 1
|
|
2478
|
+
)
|
|
2479
|
+
owner_templates = self._func_ta_ctor_args.setdefault(
|
|
2480
|
+
owner, {}
|
|
2481
|
+
)
|
|
2482
|
+
for target in selected.values():
|
|
2483
|
+
owner_templates[target] = list(
|
|
2484
|
+
self._ta_call_sites[target].ctor_args
|
|
2485
|
+
)
|
|
2486
|
+
late_method_ta_changed = True
|
|
2487
|
+
last_changed_call = call
|
|
2488
|
+
if not late_method_ta_changed:
|
|
2489
|
+
break
|
|
2490
|
+
else:
|
|
2491
|
+
self._error(
|
|
2492
|
+
"Callable TA ownership propagation did not converge; "
|
|
2493
|
+
"recursive stateful call paths are unsupported.",
|
|
2494
|
+
last_changed_call.loc if last_changed_call is not None else None,
|
|
2495
|
+
)
|
|
2496
|
+
|
|
2497
|
+
# A history-reading callable receives ``Series<T>`` parameters. That
|
|
2498
|
+
# requirement must flow outward through every wrapper parameter that
|
|
2499
|
+
# is forwarded unchanged; otherwise a wrapper stays ``double`` and
|
|
2500
|
+
# generated C++ cannot bind it to the callee's ``const Series&``.
|
|
2501
|
+
# Resolve this after every UDF/method definition is known and iterate
|
|
2502
|
+
# to a fixed point so method <- inner <- outer chains are independent
|
|
2503
|
+
# of declaration/source order.
|
|
2504
|
+
def _bound_user_call_args(callee: str, call: FuncCall) -> list:
|
|
2505
|
+
info = func_info_by_name.get(callee)
|
|
2506
|
+
if info is None or info.node is None:
|
|
2507
|
+
return []
|
|
2508
|
+
params = list(info.node.params)
|
|
2509
|
+
if (
|
|
2510
|
+
getattr(info, "is_udt_method", False)
|
|
2511
|
+
and isinstance(call.callee, MemberAccess)
|
|
2512
|
+
):
|
|
2513
|
+
if callee in self._method_signatures:
|
|
2514
|
+
binding = self._bind_typed_method_call(callee, call)
|
|
2515
|
+
return [call.callee.object, *binding.args_by_param]
|
|
2516
|
+
return [
|
|
2517
|
+
call.callee.object,
|
|
2518
|
+
*self._bind_callable_args(call, params[1:]),
|
|
2519
|
+
]
|
|
2520
|
+
return self._bind_callable_args(call, params)
|
|
2521
|
+
|
|
2522
|
+
series_requirements: dict[str, set[str]] = {
|
|
2523
|
+
name: set(params)
|
|
2524
|
+
for name, params in self._func_series_vars.items()
|
|
2525
|
+
if params
|
|
2526
|
+
}
|
|
2527
|
+
series_changed = True
|
|
2528
|
+
while series_changed:
|
|
2529
|
+
series_changed = False
|
|
2530
|
+
for owner, callee, call in call_edges:
|
|
2531
|
+
callee_info = func_info_by_name.get(callee)
|
|
2532
|
+
if callee_info is None or callee_info.node is None:
|
|
2533
|
+
continue
|
|
2534
|
+
callee_series = series_requirements.get(callee, set())
|
|
2535
|
+
if not callee_series:
|
|
2536
|
+
continue
|
|
2537
|
+
actuals = _bound_user_call_args(callee, call)
|
|
2538
|
+
for index, param_name in enumerate(callee_info.node.params):
|
|
2539
|
+
if param_name not in callee_series or index >= len(actuals):
|
|
2540
|
+
continue
|
|
2541
|
+
actual = actuals[index]
|
|
2542
|
+
if not isinstance(actual, Identifier):
|
|
2543
|
+
continue
|
|
2544
|
+
if actual.name in BAR_FIELDS:
|
|
2545
|
+
self._series_bar_fields.add(actual.name)
|
|
2546
|
+
if owner is None:
|
|
2547
|
+
continue
|
|
2548
|
+
owner_info = func_info_by_name.get(owner)
|
|
2549
|
+
if (
|
|
2550
|
+
owner_info is None
|
|
2551
|
+
or owner_info.node is None
|
|
2552
|
+
or actual.name not in owner_info.node.params
|
|
2553
|
+
):
|
|
2554
|
+
continue
|
|
2555
|
+
owner_series = self._func_series_vars.setdefault(
|
|
2556
|
+
owner, set()
|
|
2557
|
+
)
|
|
2558
|
+
if actual.name not in owner_series:
|
|
2559
|
+
owner_series.add(actual.name)
|
|
2560
|
+
owner_requirements = series_requirements.setdefault(
|
|
2561
|
+
owner, set()
|
|
2562
|
+
)
|
|
2563
|
+
if actual.name not in owner_requirements:
|
|
2564
|
+
owner_requirements.add(actual.name)
|
|
2565
|
+
series_changed = True
|
|
584
2566
|
|
|
585
2567
|
# Codegen synthesizes a Series buffer for two expression shapes that
|
|
586
2568
|
# do not appear in ``_func_series_vars`` themselves:
|
|
@@ -733,12 +2715,89 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
733
2715
|
next_idx = max(next_idx, existing[1] + 1)
|
|
734
2716
|
continue
|
|
735
2717
|
self._func_call_cs_map[id(call)] = (fname, next_idx)
|
|
2718
|
+
# Most ordinary UDFs were already materialized by the lexical
|
|
2719
|
+
# call visitor. UDT method calls are resolved in this late
|
|
2720
|
+
# graph pass, so their TA/fixnan state still needs matching
|
|
2721
|
+
# cs0/cs1/... members before codegen emits the variants.
|
|
2722
|
+
self._materialize_user_func_call_site_state(
|
|
2723
|
+
fname, next_idx, call
|
|
2724
|
+
)
|
|
736
2725
|
next_idx += 1
|
|
737
2726
|
if next_idx > current:
|
|
738
2727
|
self._func_call_site_count[fname] = next_idx
|
|
739
2728
|
|
|
740
2729
|
# Inherit each multi-call-site parent's index space down the full path.
|
|
741
2730
|
# Re-run to a fixed point for A -> B -> C chains.
|
|
2731
|
+
def _ta_variant_target_map(
|
|
2732
|
+
func_name: str, cs_idx: int
|
|
2733
|
+
) -> dict[int, int]:
|
|
2734
|
+
source_indices = list(self._func_ta_indices.get(func_name, ()))
|
|
2735
|
+
if not source_indices and func_name in self._func_ta_ranges:
|
|
2736
|
+
source_indices = list(range(*self._func_ta_ranges[func_name]))
|
|
2737
|
+
if cs_idx == 0:
|
|
2738
|
+
return {index: index for index in source_indices}
|
|
2739
|
+
overrides = self._func_cs_ta_clone_names.get(
|
|
2740
|
+
(func_name, cs_idx), {}
|
|
2741
|
+
)
|
|
2742
|
+
by_member = {
|
|
2743
|
+
site.member_name: index
|
|
2744
|
+
for index, site in enumerate(self._ta_call_sites)
|
|
2745
|
+
}
|
|
2746
|
+
targets: dict[int, int] = {}
|
|
2747
|
+
for source_index in source_indices:
|
|
2748
|
+
source_name = self._ta_call_sites[source_index].member_name
|
|
2749
|
+
target_name = overrides.get(
|
|
2750
|
+
source_name, f"{source_name}_cs{cs_idx}"
|
|
2751
|
+
)
|
|
2752
|
+
target_index = by_member.get(target_name)
|
|
2753
|
+
if target_index is not None:
|
|
2754
|
+
targets[source_index] = target_index
|
|
2755
|
+
return targets
|
|
2756
|
+
|
|
2757
|
+
def _edge_ta_variant_target_map(
|
|
2758
|
+
parent_name: str,
|
|
2759
|
+
parent_cs_idx: int,
|
|
2760
|
+
callee_name: str,
|
|
2761
|
+
call_node: FuncCall,
|
|
2762
|
+
) -> dict[int, int]:
|
|
2763
|
+
"""Compose callee source identity through one parent call edge.
|
|
2764
|
+
|
|
2765
|
+
``_ta_variant_target_map`` is keyed by the parent's base TA
|
|
2766
|
+
identities. Those identities need not equal the callee's source
|
|
2767
|
+
indices: an earlier textual call can make this edge select a
|
|
2768
|
+
shifted parent site. The edge's original materialization records
|
|
2769
|
+
the exact ``callee source -> parent base`` relation; compose that
|
|
2770
|
+
with the active parent variant instead of assuming equal keys.
|
|
2771
|
+
"""
|
|
2772
|
+
parent_targets = _ta_variant_target_map(
|
|
2773
|
+
parent_name, parent_cs_idx
|
|
2774
|
+
)
|
|
2775
|
+
cs_info = self._func_call_cs_map.get(id(call_node))
|
|
2776
|
+
edge_cs_idx = (
|
|
2777
|
+
cs_info[1]
|
|
2778
|
+
if cs_info is not None and cs_info[0] == callee_name
|
|
2779
|
+
else 0
|
|
2780
|
+
)
|
|
2781
|
+
edge_targets = self._func_ta_call_targets.get(
|
|
2782
|
+
(id(call_node), edge_cs_idx), {}
|
|
2783
|
+
)
|
|
2784
|
+
source_indices = list(
|
|
2785
|
+
self._func_ta_indices.get(callee_name, ())
|
|
2786
|
+
)
|
|
2787
|
+
if not source_indices and callee_name in self._func_ta_ranges:
|
|
2788
|
+
source_indices = list(
|
|
2789
|
+
range(*self._func_ta_ranges[callee_name])
|
|
2790
|
+
)
|
|
2791
|
+
composed: dict[int, int] = {}
|
|
2792
|
+
for source_index in source_indices:
|
|
2793
|
+
parent_source_index = edge_targets.get(
|
|
2794
|
+
source_index, source_index
|
|
2795
|
+
)
|
|
2796
|
+
active_target = parent_targets.get(parent_source_index)
|
|
2797
|
+
if active_target is not None:
|
|
2798
|
+
composed[source_index] = active_target
|
|
2799
|
+
return composed
|
|
2800
|
+
|
|
742
2801
|
changed = True
|
|
743
2802
|
while changed:
|
|
744
2803
|
changed = False
|
|
@@ -766,16 +2825,403 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
766
2825
|
if cs_info == (sub, 0):
|
|
767
2826
|
self._func_call_cs_map.pop(id(call_node), None)
|
|
768
2827
|
self._func_inherited_call_nodes.add(id(call_node))
|
|
2828
|
+
self._func_inherited_call_names[
|
|
2829
|
+
id(call_node)
|
|
2830
|
+
] = sub
|
|
2831
|
+
# Its definition-time type profile was likewise
|
|
2832
|
+
# provisional: forwarded untyped owner params
|
|
2833
|
+
# are still UNKNOWN during that visit. Each
|
|
2834
|
+
# inherited parent clone supplies the real
|
|
2835
|
+
# primitive profile below.
|
|
2836
|
+
self._func_callsite_param_types.pop(
|
|
2837
|
+
(sub, 0), None
|
|
2838
|
+
)
|
|
2839
|
+
self._func_callsite_return_types.pop(
|
|
2840
|
+
(sub, 0), None
|
|
2841
|
+
)
|
|
769
2842
|
for cs_idx in range(current, count):
|
|
2843
|
+
parent_ta_targets = _edge_ta_variant_target_map(
|
|
2844
|
+
fname,
|
|
2845
|
+
cs_idx,
|
|
2846
|
+
sub,
|
|
2847
|
+
call_node,
|
|
2848
|
+
)
|
|
770
2849
|
self._materialize_user_func_call_site_state(
|
|
771
2850
|
sub,
|
|
772
2851
|
cs_idx,
|
|
773
2852
|
call_node,
|
|
774
2853
|
reuse_existing_owner=fname,
|
|
2854
|
+
reuse_existing_targets=parent_ta_targets,
|
|
775
2855
|
)
|
|
776
2856
|
self._func_call_site_count[sub] = count
|
|
777
2857
|
changed = True
|
|
778
2858
|
|
|
2859
|
+
self._resolve_callable_callsite_primitive_types(
|
|
2860
|
+
call_edges,
|
|
2861
|
+
func_info_by_name,
|
|
2862
|
+
_bound_user_call_args,
|
|
2863
|
+
)
|
|
2864
|
+
|
|
2865
|
+
@staticmethod
|
|
2866
|
+
def _primitive_pine_type_from_spec(spec) -> PineType:
|
|
2867
|
+
if spec is None or getattr(spec, "kind", None) != "primitive":
|
|
2868
|
+
return PineType.UNKNOWN
|
|
2869
|
+
return {
|
|
2870
|
+
"int": PineType.INT,
|
|
2871
|
+
"float": PineType.FLOAT,
|
|
2872
|
+
"bool": PineType.BOOL,
|
|
2873
|
+
"string": PineType.STRING,
|
|
2874
|
+
"color": PineType.COLOR,
|
|
2875
|
+
}.get(getattr(spec, "name", None), PineType.UNKNOWN)
|
|
2876
|
+
|
|
2877
|
+
def _resolve_callable_callsite_primitive_types(
|
|
2878
|
+
self,
|
|
2879
|
+
call_edges,
|
|
2880
|
+
func_info_by_name,
|
|
2881
|
+
bound_user_call_args,
|
|
2882
|
+
) -> None:
|
|
2883
|
+
"""Reconcile per-written-call primitive types after clone closure.
|
|
2884
|
+
|
|
2885
|
+
Direct UDF calls are typed during the lexical visit, but UDT methods
|
|
2886
|
+
and pure wrappers receive their cs identities only in the late stateful
|
|
2887
|
+
call-graph pass. Propagate an enclosing variant's parameter profile
|
|
2888
|
+
through exact forwarded argument ASTs until every emitted history
|
|
2889
|
+
callable variant has its own stable primitive profile.
|
|
2890
|
+
"""
|
|
2891
|
+
edge_by_call_id = {
|
|
2892
|
+
id(call): (owner, callee, call)
|
|
2893
|
+
for owner, callee, call in call_edges
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
def declared_types(name: str, size: int) -> list[PineType]:
|
|
2897
|
+
specs = list(self._func_param_type_specs.get(name, ()))
|
|
2898
|
+
return [
|
|
2899
|
+
self._primitive_pine_type_from_spec(
|
|
2900
|
+
specs[index] if index < len(specs) else None
|
|
2901
|
+
)
|
|
2902
|
+
for index in range(size)
|
|
2903
|
+
]
|
|
2904
|
+
|
|
2905
|
+
def merge_profile(
|
|
2906
|
+
callee: str,
|
|
2907
|
+
cs_idx: int,
|
|
2908
|
+
incoming: list[PineType],
|
|
2909
|
+
call,
|
|
2910
|
+
) -> bool:
|
|
2911
|
+
info = func_info_by_name.get(callee)
|
|
2912
|
+
if info is None or info.node is None:
|
|
2913
|
+
return False
|
|
2914
|
+
size = len(info.node.params)
|
|
2915
|
+
declared = declared_types(callee, size)
|
|
2916
|
+
key = (callee, cs_idx)
|
|
2917
|
+
current = list(
|
|
2918
|
+
self._func_callsite_param_types.get(
|
|
2919
|
+
key, [PineType.UNKNOWN] * size
|
|
2920
|
+
)
|
|
2921
|
+
)
|
|
2922
|
+
while len(current) < size:
|
|
2923
|
+
current.append(PineType.UNKNOWN)
|
|
2924
|
+
changed = False
|
|
2925
|
+
for index in range(size):
|
|
2926
|
+
candidate = declared[index]
|
|
2927
|
+
if candidate == PineType.UNKNOWN and index < len(incoming):
|
|
2928
|
+
candidate = incoming[index]
|
|
2929
|
+
if candidate == PineType.UNKNOWN:
|
|
2930
|
+
continue
|
|
2931
|
+
if current[index] == PineType.UNKNOWN:
|
|
2932
|
+
current[index] = candidate
|
|
2933
|
+
changed = True
|
|
2934
|
+
elif current[index] != candidate:
|
|
2935
|
+
self._error(
|
|
2936
|
+
"Cannot safely specialize untyped parameter '"
|
|
2937
|
+
+ info.node.params[index]
|
|
2938
|
+
+ "' of callable '"
|
|
2939
|
+
+ callee
|
|
2940
|
+
+ "': distinct primitive types collapse onto the same "
|
|
2941
|
+
+ f"written-call variant cs{cs_idx}. Inline the calls "
|
|
2942
|
+
+ "or declare an explicit parameter type.",
|
|
2943
|
+
call.loc,
|
|
2944
|
+
)
|
|
2945
|
+
if changed or key not in self._func_callsite_param_types:
|
|
2946
|
+
self._func_callsite_param_types[key] = current
|
|
2947
|
+
return changed
|
|
2948
|
+
|
|
2949
|
+
# Backfill every direct UDF/method identity assigned by the late graph
|
|
2950
|
+
# pass. Definition-time wrapper calls can still contain UNKNOWN params;
|
|
2951
|
+
# the fixed point below resolves those from their owner variant.
|
|
2952
|
+
for _owner, callee, call in call_edges:
|
|
2953
|
+
if _owner is not None:
|
|
2954
|
+
# Definition-time analyzer fallbacks for an expression over an
|
|
2955
|
+
# untyped owner parameter are not concrete facts (``y + 0``
|
|
2956
|
+
# historically reports FLOAT while ``y`` is still UNKNOWN).
|
|
2957
|
+
# The owner-variant fixed point below resolves nested calls.
|
|
2958
|
+
continue
|
|
2959
|
+
cs_info = self._func_call_cs_map.get(id(call))
|
|
2960
|
+
if cs_info is None or cs_info[0] != callee:
|
|
2961
|
+
continue
|
|
2962
|
+
incoming = self._callable_bound_param_types_by_node.get(
|
|
2963
|
+
id(call), []
|
|
2964
|
+
)
|
|
2965
|
+
merge_profile(callee, cs_info[1], incoming, call)
|
|
2966
|
+
|
|
2967
|
+
def owner_profile(owner: str, owner_cs: int | None) -> list[PineType]:
|
|
2968
|
+
info = func_info_by_name.get(owner)
|
|
2969
|
+
if info is None or info.node is None:
|
|
2970
|
+
return []
|
|
2971
|
+
size = len(info.node.params)
|
|
2972
|
+
if owner_cs is not None:
|
|
2973
|
+
profile = self._func_callsite_param_types.get(
|
|
2974
|
+
(owner, owner_cs)
|
|
2975
|
+
)
|
|
2976
|
+
if profile is not None:
|
|
2977
|
+
return list(profile)
|
|
2978
|
+
declared = declared_types(owner, size)
|
|
2979
|
+
legacy = list(getattr(info, "param_types", ()) or ())
|
|
2980
|
+
return [
|
|
2981
|
+
declared[index]
|
|
2982
|
+
if declared[index] != PineType.UNKNOWN
|
|
2983
|
+
else (
|
|
2984
|
+
legacy[index]
|
|
2985
|
+
if index < len(legacy)
|
|
2986
|
+
else PineType.UNKNOWN
|
|
2987
|
+
)
|
|
2988
|
+
for index in range(size)
|
|
2989
|
+
]
|
|
2990
|
+
|
|
2991
|
+
def target_variant(
|
|
2992
|
+
owner: str | None,
|
|
2993
|
+
owner_cs: int | None,
|
|
2994
|
+
callee: str,
|
|
2995
|
+
call,
|
|
2996
|
+
) -> int | None:
|
|
2997
|
+
cs_info = self._func_call_cs_map.get(id(call))
|
|
2998
|
+
callee_count = self._func_call_site_count.get(callee, 0)
|
|
2999
|
+
if owner is None:
|
|
3000
|
+
if cs_info is not None and cs_info[0] == callee:
|
|
3001
|
+
return cs_info[1]
|
|
3002
|
+
return None
|
|
3003
|
+
if cs_info is not None and cs_info[0] == callee:
|
|
3004
|
+
# A surviving lexical mapping is authoritative. Codegen's
|
|
3005
|
+
# context-sensitive instance dispatcher pins every clone of
|
|
3006
|
+
# this owner to that same written callee variant. If two owner
|
|
3007
|
+
# profiles disagree, merge_profile must reject the collapse;
|
|
3008
|
+
# pretending the owner index selects another callee clone
|
|
3009
|
+
# would type a function different from the one actually
|
|
3010
|
+
# emitted at the call edge.
|
|
3011
|
+
return cs_info[1]
|
|
3012
|
+
if owner_cs is not None and callee_count > 1:
|
|
3013
|
+
# A removed lexical map marks an inherited single-call path;
|
|
3014
|
+
# those variants deliberately follow the enclosing clone.
|
|
3015
|
+
return owner_cs if owner_cs < callee_count else None
|
|
3016
|
+
return None
|
|
3017
|
+
|
|
3018
|
+
def mentions_owner_parameter(value, names: set[str]) -> bool:
|
|
3019
|
+
if value is None:
|
|
3020
|
+
return False
|
|
3021
|
+
if isinstance(value, Identifier):
|
|
3022
|
+
return value.name in names
|
|
3023
|
+
if isinstance(value, (list, tuple)):
|
|
3024
|
+
return any(
|
|
3025
|
+
mentions_owner_parameter(item, names) for item in value
|
|
3026
|
+
)
|
|
3027
|
+
if isinstance(value, dict):
|
|
3028
|
+
return any(
|
|
3029
|
+
mentions_owner_parameter(item, names)
|
|
3030
|
+
for item in value.values()
|
|
3031
|
+
)
|
|
3032
|
+
if not hasattr(value, "__dict__"):
|
|
3033
|
+
return False
|
|
3034
|
+
return any(
|
|
3035
|
+
mentions_owner_parameter(child, names)
|
|
3036
|
+
for child in vars(value).values()
|
|
3037
|
+
)
|
|
3038
|
+
|
|
3039
|
+
# Calls visited inside an untyped callable are initially analyzed
|
|
3040
|
+
# before any concrete outer call-site profile exists. The ordinary
|
|
3041
|
+
# expression analyzer therefore records defaults for transformed
|
|
3042
|
+
# expressions (many numeric built-ins default to FLOAT). That value
|
|
3043
|
+
# is not evidence about the eventual written call: discard it for
|
|
3044
|
+
# every nested argument that depends on an untyped owner parameter so
|
|
3045
|
+
# the fixed point below either derives the type from the owner variant
|
|
3046
|
+
# or rejects the unresolved transformation deterministically.
|
|
3047
|
+
for owner, callee, call in call_edges:
|
|
3048
|
+
if owner is None:
|
|
3049
|
+
continue
|
|
3050
|
+
owner_info = func_info_by_name.get(owner)
|
|
3051
|
+
callee_info = func_info_by_name.get(callee)
|
|
3052
|
+
if (
|
|
3053
|
+
owner_info is None
|
|
3054
|
+
or owner_info.node is None
|
|
3055
|
+
or callee_info is None
|
|
3056
|
+
or callee_info.node is None
|
|
3057
|
+
):
|
|
3058
|
+
continue
|
|
3059
|
+
owner_declared = declared_types(
|
|
3060
|
+
owner, len(owner_info.node.params)
|
|
3061
|
+
)
|
|
3062
|
+
untyped_owner_params = {
|
|
3063
|
+
param
|
|
3064
|
+
for index, param in enumerate(owner_info.node.params)
|
|
3065
|
+
if owner_declared[index] == PineType.UNKNOWN
|
|
3066
|
+
}
|
|
3067
|
+
if not untyped_owner_params:
|
|
3068
|
+
continue
|
|
3069
|
+
actuals = bound_user_call_args(callee, call)
|
|
3070
|
+
dependent_slots = {
|
|
3071
|
+
index
|
|
3072
|
+
for index, actual in enumerate(actuals)
|
|
3073
|
+
if mentions_owner_parameter(actual, untyped_owner_params)
|
|
3074
|
+
}
|
|
3075
|
+
if not dependent_slots:
|
|
3076
|
+
continue
|
|
3077
|
+
callee_declared = declared_types(
|
|
3078
|
+
callee, len(callee_info.node.params)
|
|
3079
|
+
)
|
|
3080
|
+
owner_count = self._func_call_site_count.get(owner, 0)
|
|
3081
|
+
owner_variants = range(owner_count) if owner_count > 0 else (None,)
|
|
3082
|
+
for owner_cs in owner_variants:
|
|
3083
|
+
callee_cs = target_variant(owner, owner_cs, callee, call)
|
|
3084
|
+
if callee_cs is None:
|
|
3085
|
+
continue
|
|
3086
|
+
key = (callee, callee_cs)
|
|
3087
|
+
current = list(
|
|
3088
|
+
self._func_callsite_param_types.get(
|
|
3089
|
+
key,
|
|
3090
|
+
[PineType.UNKNOWN] * len(callee_info.node.params),
|
|
3091
|
+
)
|
|
3092
|
+
)
|
|
3093
|
+
while len(current) < len(callee_info.node.params):
|
|
3094
|
+
current.append(PineType.UNKNOWN)
|
|
3095
|
+
invalidated = False
|
|
3096
|
+
for index in dependent_slots:
|
|
3097
|
+
if (
|
|
3098
|
+
index < len(current)
|
|
3099
|
+
and index < len(callee_declared)
|
|
3100
|
+
and callee_declared[index] == PineType.UNKNOWN
|
|
3101
|
+
and current[index] != PineType.UNKNOWN
|
|
3102
|
+
):
|
|
3103
|
+
current[index] = PineType.UNKNOWN
|
|
3104
|
+
invalidated = True
|
|
3105
|
+
if invalidated:
|
|
3106
|
+
self._func_callsite_param_types[key] = current
|
|
3107
|
+
self._func_callsite_return_types.pop(key, None)
|
|
3108
|
+
|
|
3109
|
+
# Resolve parameter forwarding and direct-wrapper returns together.
|
|
3110
|
+
# The graph is finite and primitive types only move UNKNOWN -> known.
|
|
3111
|
+
for _ in range(64):
|
|
3112
|
+
changed = False
|
|
3113
|
+
for owner, callee, call in call_edges:
|
|
3114
|
+
if owner is None:
|
|
3115
|
+
continue
|
|
3116
|
+
owner_count = self._func_call_site_count.get(owner, 0)
|
|
3117
|
+
owner_variants = (
|
|
3118
|
+
range(owner_count) if owner_count > 0 else (None,)
|
|
3119
|
+
)
|
|
3120
|
+
owner_info = func_info_by_name.get(owner)
|
|
3121
|
+
if owner_info is None or owner_info.node is None:
|
|
3122
|
+
continue
|
|
3123
|
+
actuals = bound_user_call_args(callee, call)
|
|
3124
|
+
recorded = self._callable_bound_param_types_by_node.get(
|
|
3125
|
+
id(call), []
|
|
3126
|
+
)
|
|
3127
|
+
for owner_cs in owner_variants:
|
|
3128
|
+
callee_cs = target_variant(
|
|
3129
|
+
owner, owner_cs, callee, call
|
|
3130
|
+
)
|
|
3131
|
+
if callee_cs is None:
|
|
3132
|
+
continue
|
|
3133
|
+
profile = owner_profile(owner, owner_cs)
|
|
3134
|
+
param_map = {
|
|
3135
|
+
name: (
|
|
3136
|
+
profile[index]
|
|
3137
|
+
if index < len(profile)
|
|
3138
|
+
else PineType.UNKNOWN
|
|
3139
|
+
)
|
|
3140
|
+
for index, name in enumerate(owner_info.node.params)
|
|
3141
|
+
}
|
|
3142
|
+
incoming: list[PineType] = []
|
|
3143
|
+
owner_param_names = set(owner_info.node.params)
|
|
3144
|
+
for index, actual in enumerate(actuals):
|
|
3145
|
+
inferred = self._callsite_primitive_expr_type(
|
|
3146
|
+
actual, param_map
|
|
3147
|
+
)
|
|
3148
|
+
if (
|
|
3149
|
+
inferred == PineType.UNKNOWN
|
|
3150
|
+
and not mentions_owner_parameter(
|
|
3151
|
+
actual, owner_param_names
|
|
3152
|
+
)
|
|
3153
|
+
and index < len(recorded)
|
|
3154
|
+
):
|
|
3155
|
+
inferred = recorded[index]
|
|
3156
|
+
incoming.append(inferred)
|
|
3157
|
+
if merge_profile(callee, callee_cs, incoming, call):
|
|
3158
|
+
changed = True
|
|
3159
|
+
|
|
3160
|
+
# Recompute each variant's primitive return. A direct terminal
|
|
3161
|
+
# wrapper call uses the exact callee variant return rather than the
|
|
3162
|
+
# shared definition-level cache.
|
|
3163
|
+
for key, profile in list(
|
|
3164
|
+
self._func_callsite_param_types.items()
|
|
3165
|
+
):
|
|
3166
|
+
name, cs_idx = key
|
|
3167
|
+
info = func_info_by_name.get(name)
|
|
3168
|
+
if info is None or info.node is None:
|
|
3169
|
+
continue
|
|
3170
|
+
ret = self._callsite_callable_return_type(
|
|
3171
|
+
info.node, list(profile), info.return_type
|
|
3172
|
+
)
|
|
3173
|
+
terminal = self._direct_terminal_return_expr(info.node)
|
|
3174
|
+
if isinstance(terminal, FuncCall):
|
|
3175
|
+
edge = edge_by_call_id.get(id(terminal))
|
|
3176
|
+
if edge is not None:
|
|
3177
|
+
_, terminal_callee, terminal_call = edge
|
|
3178
|
+
terminal_cs = target_variant(
|
|
3179
|
+
name, cs_idx, terminal_callee, terminal_call
|
|
3180
|
+
)
|
|
3181
|
+
if terminal_cs is not None:
|
|
3182
|
+
nested_ret = self._func_callsite_return_types.get(
|
|
3183
|
+
(terminal_callee, terminal_cs),
|
|
3184
|
+
PineType.UNKNOWN,
|
|
3185
|
+
)
|
|
3186
|
+
if nested_ret != PineType.UNKNOWN:
|
|
3187
|
+
ret = nested_ret
|
|
3188
|
+
if self._func_callsite_return_types.get(key) != ret:
|
|
3189
|
+
self._func_callsite_return_types[key] = ret
|
|
3190
|
+
changed = True
|
|
3191
|
+
if not changed:
|
|
3192
|
+
break
|
|
3193
|
+
|
|
3194
|
+
# A live untyped history parameter without a variant type cannot be
|
|
3195
|
+
# emitted faithfully. Reject it rather than falling back to the first
|
|
3196
|
+
# call's global FuncInfo type and reintroducing source-order coercion.
|
|
3197
|
+
for name, series_names in self._func_series_vars.items():
|
|
3198
|
+
info = func_info_by_name.get(name)
|
|
3199
|
+
if info is None or info.node is None:
|
|
3200
|
+
continue
|
|
3201
|
+
count = self._func_call_site_count.get(name, 0)
|
|
3202
|
+
declared = declared_types(name, len(info.node.params))
|
|
3203
|
+
for cs_idx in range(count):
|
|
3204
|
+
profile = self._func_callsite_param_types.get(
|
|
3205
|
+
(name, cs_idx), []
|
|
3206
|
+
)
|
|
3207
|
+
for index, param in enumerate(info.node.params):
|
|
3208
|
+
if param not in series_names:
|
|
3209
|
+
continue
|
|
3210
|
+
if declared[index] != PineType.UNKNOWN:
|
|
3211
|
+
continue
|
|
3212
|
+
resolved = (
|
|
3213
|
+
profile[index]
|
|
3214
|
+
if index < len(profile)
|
|
3215
|
+
else PineType.UNKNOWN
|
|
3216
|
+
)
|
|
3217
|
+
if resolved == PineType.UNKNOWN:
|
|
3218
|
+
self._error(
|
|
3219
|
+
"Cannot infer the per-callsite primitive type of "
|
|
3220
|
+
f"history parameter '{param}' in callable '{name}' "
|
|
3221
|
+
f"variant cs{cs_idx}; declare its type explicitly.",
|
|
3222
|
+
info.node.loc,
|
|
3223
|
+
)
|
|
3224
|
+
|
|
779
3225
|
# ------------------------------------------------------------------
|
|
780
3226
|
# Mixed-callsite UDF timeframe-param security rejection.
|
|
781
3227
|
#
|
|
@@ -895,6 +3341,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
895
3341
|
expression=sec.expression,
|
|
896
3342
|
returns_tuple=sec.returns_tuple,
|
|
897
3343
|
tuple_size=sec.tuple_size,
|
|
3344
|
+
tuple_element_types=sec.tuple_element_types,
|
|
898
3345
|
gaps=sec.gaps,
|
|
899
3346
|
lookahead=sec.lookahead,
|
|
900
3347
|
ta_range=sec.ta_range,
|
|
@@ -1098,6 +3545,63 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1098
3545
|
return None
|
|
1099
3546
|
return owner
|
|
1100
3547
|
|
|
3548
|
+
def _udt_name_from_nullable_ctor_selection(
|
|
3549
|
+
self, value: ASTNode | None
|
|
3550
|
+
) -> str | None:
|
|
3551
|
+
"""Exact user-UDT type for ctor-only nullable selections.
|
|
3552
|
+
|
|
3553
|
+
Keep this deliberately narrower than generic UDT expression
|
|
3554
|
+
inference. In particular, a terminal ``array.get(...UDT...)`` is a
|
|
3555
|
+
reference-identity surface with its own fail-closed rules; treating
|
|
3556
|
+
every UDT-valued expression selected against ``na`` as a by-value
|
|
3557
|
+
return would accidentally bypass those rules.
|
|
3558
|
+
"""
|
|
3559
|
+
nullable = object()
|
|
3560
|
+
|
|
3561
|
+
def terminal(body: list[ASTNode]) -> ASTNode | None:
|
|
3562
|
+
if not body:
|
|
3563
|
+
return None
|
|
3564
|
+
node = body[-1]
|
|
3565
|
+
return node.expr if isinstance(node, ExprStmt) else node
|
|
3566
|
+
|
|
3567
|
+
def resolve(node: ASTNode | None) -> str | None | object:
|
|
3568
|
+
if node is None:
|
|
3569
|
+
return nullable
|
|
3570
|
+
if isinstance(node, ExprStmt):
|
|
3571
|
+
return resolve(node.expr)
|
|
3572
|
+
if isinstance(node, NaLiteral):
|
|
3573
|
+
return nullable
|
|
3574
|
+
direct = self._udt_name_from_ctor(node)
|
|
3575
|
+
if direct in self._udt_fields:
|
|
3576
|
+
return direct
|
|
3577
|
+
if isinstance(node, Ternary):
|
|
3578
|
+
return merge((resolve(node.true_val), resolve(node.false_val)))
|
|
3579
|
+
if isinstance(node, IfStmt):
|
|
3580
|
+
return merge((
|
|
3581
|
+
resolve(terminal(node.body)),
|
|
3582
|
+
resolve(terminal(node.else_body)),
|
|
3583
|
+
))
|
|
3584
|
+
if isinstance(node, SwitchStmt):
|
|
3585
|
+
results = [
|
|
3586
|
+
resolve(terminal(branch))
|
|
3587
|
+
for _case, branch in node.cases
|
|
3588
|
+
]
|
|
3589
|
+
results.append(resolve(terminal(node.default_body)))
|
|
3590
|
+
return merge(results)
|
|
3591
|
+
return None
|
|
3592
|
+
|
|
3593
|
+
def merge(results) -> str | None | object:
|
|
3594
|
+
resolved = list(results)
|
|
3595
|
+
if any(item is None for item in resolved):
|
|
3596
|
+
return None
|
|
3597
|
+
concrete = {item for item in resolved if item is not nullable}
|
|
3598
|
+
if not concrete:
|
|
3599
|
+
return nullable
|
|
3600
|
+
return next(iter(concrete)) if len(concrete) == 1 else None
|
|
3601
|
+
|
|
3602
|
+
result = resolve(value)
|
|
3603
|
+
return result if isinstance(result, str) else None
|
|
3604
|
+
|
|
1101
3605
|
def _func_terminal_drawing_type(self, func_node: FuncDef) -> str | None:
|
|
1102
3606
|
"""Resolve the drawing-handle / UDT type of a function's terminal
|
|
1103
3607
|
(return) expression for cases the direct ``_udt_name_from_ctor`` on the
|
|
@@ -1121,66 +3625,193 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1121
3625
|
if not body:
|
|
1122
3626
|
return None
|
|
1123
3627
|
|
|
1124
|
-
#
|
|
1125
|
-
#
|
|
1126
|
-
#
|
|
1127
|
-
|
|
1128
|
-
local_drawing: dict[str, str] = {}
|
|
3628
|
+
# Build a source-ordered lexical binding map. ``None`` is an explicit
|
|
3629
|
+
# non-drawing tombstone: it prevents a same-named drawing declaration
|
|
3630
|
+
# from a nested or earlier block from poisoning a later scalar return.
|
|
3631
|
+
bindings: dict[str, str | None] = {}
|
|
1129
3632
|
param_hints = (func_node.annotations or {}).get("param_type_hints", [])
|
|
1130
3633
|
for i, p in enumerate(func_node.params):
|
|
1131
3634
|
hint = param_hints[i] if i < len(param_hints) else None
|
|
1132
|
-
if hint in _DRAWING_TYPE_NAMES
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
#
|
|
1157
|
-
#
|
|
3635
|
+
bindings[p] = hint if hint in _DRAWING_TYPE_NAMES else None
|
|
3636
|
+
|
|
3637
|
+
def _decl_drawing_type(stmt: VarDecl) -> str | None:
|
|
3638
|
+
if stmt.type_hint in _DRAWING_TYPE_NAMES:
|
|
3639
|
+
return stmt.type_hint
|
|
3640
|
+
direct = self._udt_name_from_ctor(stmt.value)
|
|
3641
|
+
if direct in _DRAWING_TYPE_NAMES:
|
|
3642
|
+
return direct
|
|
3643
|
+
exact = self._var_member_type_specs_by_node.get(id(stmt))
|
|
3644
|
+
if (exact is not None
|
|
3645
|
+
and exact.kind == "udt"
|
|
3646
|
+
and exact.name in _DRAWING_TYPE_NAMES):
|
|
3647
|
+
return exact.name
|
|
3648
|
+
spec = self._type_spec_from_expr(stmt.value)
|
|
3649
|
+
if (spec is not None
|
|
3650
|
+
and spec.kind == "udt"
|
|
3651
|
+
and spec.name in _DRAWING_TYPE_NAMES):
|
|
3652
|
+
return spec.name
|
|
3653
|
+
return None
|
|
3654
|
+
|
|
3655
|
+
def _scan_direct_prefix(
|
|
3656
|
+
stmts: list[ASTNode],
|
|
3657
|
+
env: dict[str, str | None],
|
|
3658
|
+
) -> None:
|
|
3659
|
+
# Declarations inside nested control-flow bodies never leak into
|
|
3660
|
+
# this lexical environment. Their own branch environment is
|
|
3661
|
+
# created only when that control node is the terminal expression.
|
|
3662
|
+
for stmt in stmts:
|
|
3663
|
+
if isinstance(stmt, VarDecl):
|
|
3664
|
+
env[stmt.name] = _decl_drawing_type(stmt)
|
|
3665
|
+
elif isinstance(stmt, TupleAssign):
|
|
3666
|
+
for name in stmt.names:
|
|
3667
|
+
env[name] = None
|
|
3668
|
+
|
|
3669
|
+
terminal_na = object()
|
|
3670
|
+
|
|
3671
|
+
def _resolve_terminal(
|
|
3672
|
+
stmt: ASTNode,
|
|
3673
|
+
env: dict[str, str | None],
|
|
3674
|
+
) -> str | None | object:
|
|
3675
|
+
if isinstance(stmt, ExprStmt):
|
|
3676
|
+
return _resolve_terminal(stmt.expr, env)
|
|
3677
|
+
if (isinstance(stmt, NaLiteral)
|
|
3678
|
+
or (isinstance(stmt, Identifier)
|
|
3679
|
+
and stmt.name == "na")):
|
|
3680
|
+
return terminal_na
|
|
1158
3681
|
if isinstance(stmt, IfStmt):
|
|
3682
|
+
branch_results: list[str | None | object] = []
|
|
1159
3683
|
for branch in (stmt.body, stmt.else_body):
|
|
1160
|
-
if branch:
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
return
|
|
1174
|
-
|
|
3684
|
+
if not branch:
|
|
3685
|
+
continue # implicit na arm inherits the drawing type
|
|
3686
|
+
branch_env = dict(env)
|
|
3687
|
+
_scan_direct_prefix(branch[:-1], branch_env)
|
|
3688
|
+
branch_results.append(
|
|
3689
|
+
_resolve_terminal(branch[-1], branch_env)
|
|
3690
|
+
)
|
|
3691
|
+
concrete = [
|
|
3692
|
+
item for item in branch_results if item is not terminal_na
|
|
3693
|
+
]
|
|
3694
|
+
if not concrete:
|
|
3695
|
+
return terminal_na
|
|
3696
|
+
first = concrete[0]
|
|
3697
|
+
return (
|
|
3698
|
+
first
|
|
3699
|
+
if all(item == first for item in concrete)
|
|
3700
|
+
else None
|
|
3701
|
+
)
|
|
3702
|
+
if isinstance(stmt, SwitchStmt):
|
|
3703
|
+
branch_results: list[str | None | object] = []
|
|
3704
|
+
for _case_expr, branch in stmt.cases:
|
|
3705
|
+
if not branch:
|
|
3706
|
+
continue
|
|
3707
|
+
branch_env = dict(env)
|
|
3708
|
+
_scan_direct_prefix(branch[:-1], branch_env)
|
|
3709
|
+
branch_results.append(
|
|
3710
|
+
_resolve_terminal(branch[-1], branch_env)
|
|
3711
|
+
)
|
|
3712
|
+
if stmt.default_body:
|
|
3713
|
+
branch_env = dict(env)
|
|
3714
|
+
_scan_direct_prefix(stmt.default_body[:-1], branch_env)
|
|
3715
|
+
branch_results.append(
|
|
3716
|
+
_resolve_terminal(stmt.default_body[-1], branch_env)
|
|
3717
|
+
)
|
|
3718
|
+
concrete = [
|
|
3719
|
+
item for item in branch_results if item is not terminal_na
|
|
3720
|
+
]
|
|
3721
|
+
if not concrete:
|
|
3722
|
+
return terminal_na
|
|
3723
|
+
first = concrete[0]
|
|
3724
|
+
return (
|
|
3725
|
+
first
|
|
3726
|
+
if all(item == first for item in concrete)
|
|
3727
|
+
else None
|
|
3728
|
+
)
|
|
3729
|
+
if isinstance(stmt, VarDecl):
|
|
3730
|
+
return _decl_drawing_type(stmt)
|
|
3731
|
+
if isinstance(stmt, Identifier):
|
|
3732
|
+
return env.get(stmt.name)
|
|
3733
|
+
direct = self._udt_name_from_ctor(stmt)
|
|
3734
|
+
if direct in _DRAWING_TYPE_NAMES:
|
|
3735
|
+
return direct
|
|
3736
|
+
spec = self._type_spec_from_expr(stmt)
|
|
3737
|
+
if (spec is not None
|
|
3738
|
+
and spec.kind == "udt"
|
|
3739
|
+
and spec.name in _DRAWING_TYPE_NAMES):
|
|
3740
|
+
return spec.name
|
|
3741
|
+
return None
|
|
1175
3742
|
|
|
1176
|
-
|
|
3743
|
+
_scan_direct_prefix(body[:-1], bindings)
|
|
3744
|
+
resolved = _resolve_terminal(body[-1], bindings)
|
|
3745
|
+
return resolved if isinstance(resolved, str) else None
|
|
3746
|
+
|
|
3747
|
+
def _record_collection_type(
|
|
3748
|
+
self,
|
|
3749
|
+
name: str,
|
|
3750
|
+
spec: TypeSpec,
|
|
3751
|
+
*,
|
|
3752
|
+
symbol_scope: str | None = None,
|
|
3753
|
+
) -> None:
|
|
3754
|
+
"""Record collection metadata without erasing another callable's local.
|
|
3755
|
+
|
|
3756
|
+
``symbol_scope`` is supplied for reassignments so a UDF that mutates a
|
|
3757
|
+
top-level collection keeps updating the top-level binding. A VarDecl
|
|
3758
|
+
inside an active callable is necessarily lexical to that callable.
|
|
3759
|
+
UDT TypeSpecs retain their established registry path; this overlay is
|
|
3760
|
+
intentionally limited to array/map/matrix dispatch.
|
|
3761
|
+
"""
|
|
3762
|
+
callable_key = (
|
|
3763
|
+
self._collection_scope_stack[-1]
|
|
3764
|
+
if self._collection_scope_stack
|
|
3765
|
+
else None
|
|
3766
|
+
)
|
|
3767
|
+
if (callable_key is not None
|
|
3768
|
+
and symbol_scope != "global"
|
|
3769
|
+
and self._block_node_stack):
|
|
3770
|
+
owner_id = id(self._block_node_stack[-1])
|
|
3771
|
+
self._block_collection_owners[owner_id] = callable_key
|
|
3772
|
+
self._block_collection_types.setdefault(owner_id, {})[name] = (
|
|
3773
|
+
spec if spec.kind in {"array", "map", "matrix"} else None
|
|
3774
|
+
)
|
|
3775
|
+
if spec.kind in {"array", "map", "matrix"}:
|
|
3776
|
+
return
|
|
3777
|
+
if spec.kind not in {"array", "map", "matrix"}:
|
|
3778
|
+
# Primitive locals have no collection metadata to export and must
|
|
3779
|
+
# not overwrite a same-named top-level collection. Preserve the
|
|
3780
|
+
# established UDT registry path until UDT identity gets its own
|
|
3781
|
+
# lexical migration.
|
|
3782
|
+
if (callable_key is not None
|
|
3783
|
+
and symbol_scope != "global"
|
|
3784
|
+
and spec.kind == "primitive"):
|
|
3785
|
+
return
|
|
3786
|
+
self._collection_types[name] = spec
|
|
3787
|
+
return
|
|
3788
|
+
if callable_key is not None and symbol_scope != "global":
|
|
3789
|
+
self._func_collection_types.setdefault(callable_key, {})[name] = spec
|
|
3790
|
+
return
|
|
3791
|
+
self._collection_types[name] = spec
|
|
1177
3792
|
|
|
1178
3793
|
def _visit_VarDecl(self, node: VarDecl) -> PineType:
|
|
3794
|
+
outer_symbol = self._symbols.resolve(node.name)
|
|
3795
|
+
outer_spec = (
|
|
3796
|
+
getattr(outer_symbol, "type_spec", None)
|
|
3797
|
+
if outer_symbol is not None
|
|
3798
|
+
else self._collection_types.get(node.name)
|
|
3799
|
+
)
|
|
1179
3800
|
# Infer type from the value expression
|
|
1180
3801
|
val_type = self._visit(node.value)
|
|
1181
3802
|
type_spec = self._type_spec_from_hint(node.type_hint) if node.type_hint else None
|
|
1182
3803
|
if type_spec is None:
|
|
1183
3804
|
type_spec = self._type_spec_from_expr(node.value)
|
|
3805
|
+
if self._collection_scope_stack:
|
|
3806
|
+
self._callable_collection_bindings[id(node)] = (
|
|
3807
|
+
type_spec
|
|
3808
|
+
if type_spec is not None
|
|
3809
|
+
and type_spec.kind in {"array", "map", "matrix"}
|
|
3810
|
+
else None
|
|
3811
|
+
)
|
|
3812
|
+
self._callable_collection_binding_owners[id(node)] = (
|
|
3813
|
+
self._collection_scope_stack[-1]
|
|
3814
|
+
)
|
|
1184
3815
|
|
|
1185
3816
|
# Check for type hint override
|
|
1186
3817
|
if node.type_hint:
|
|
@@ -1211,6 +3842,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1211
3842
|
if udt_ctor is not None and type_spec is None:
|
|
1212
3843
|
type_spec = TypeSpec.udt(udt_ctor)
|
|
1213
3844
|
|
|
3845
|
+
self._var_decl_types_by_node[id(node)] = (val_type, type_spec)
|
|
3846
|
+
|
|
1214
3847
|
if self._global_scope:
|
|
1215
3848
|
if self._is_static_expression(node.value):
|
|
1216
3849
|
self._static_vars.add(node.name)
|
|
@@ -1233,34 +3866,68 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1233
3866
|
udt_type_name=udt_ctor,
|
|
1234
3867
|
type_spec=type_spec,
|
|
1235
3868
|
)
|
|
3869
|
+
# A direct persistent primitive in an ordinary UDF can safely shadow
|
|
3870
|
+
# a top-level map once post-analysis owner qualification gives it exact
|
|
3871
|
+
# storage. Other scalar/map shadows still need the legacy fail-closed
|
|
3872
|
+
# marker because their scoped Series identity is unresolved.
|
|
3873
|
+
if (
|
|
3874
|
+
not self._global_scope
|
|
3875
|
+
and self._type_spec_contains_map(outer_spec)
|
|
3876
|
+
and not self._type_spec_contains_map(type_spec)
|
|
3877
|
+
and not (
|
|
3878
|
+
(node.is_var or node.is_varip)
|
|
3879
|
+
and not self._block_node_stack
|
|
3880
|
+
and bool(self._collection_scope_stack)
|
|
3881
|
+
and self._collection_scope_stack[-1] in self._func_defs
|
|
3882
|
+
and (
|
|
3883
|
+
node.name in self._ordinary_global_binding_names
|
|
3884
|
+
or any(
|
|
3885
|
+
meta[1] == node.name
|
|
3886
|
+
for meta in self._var_member_metadata_by_node.values()
|
|
3887
|
+
)
|
|
3888
|
+
)
|
|
3889
|
+
)
|
|
3890
|
+
):
|
|
3891
|
+
setattr(sym, "_pf_shadows_map_state", True)
|
|
1236
3892
|
if node.name in self._static_vars:
|
|
1237
3893
|
setattr(sym, "is_static_series", True)
|
|
1238
3894
|
self._symbols.define(sym)
|
|
3895
|
+
setattr(sym, "_pf_decl_node_id", id(node))
|
|
3896
|
+
setattr(sym, "_pf_decl_binding_name", node.name)
|
|
3897
|
+
if (type_spec is None
|
|
3898
|
+
and self._collection_scope_stack
|
|
3899
|
+
and self._block_node_stack
|
|
3900
|
+
and self._symbols.current_scope.name != "global"):
|
|
3901
|
+
owner_id = id(self._block_node_stack[-1])
|
|
3902
|
+
self._block_collection_owners[owner_id] = self._collection_scope_stack[-1]
|
|
3903
|
+
self._block_collection_types.setdefault(owner_id, {})[node.name] = None
|
|
1239
3904
|
if udt_ctor is not None:
|
|
1240
3905
|
self._udt_var_types[node.name] = udt_ctor
|
|
1241
3906
|
if type_spec is not None:
|
|
1242
|
-
self.
|
|
3907
|
+
self._record_collection_type(
|
|
3908
|
+
node.name,
|
|
3909
|
+
type_spec,
|
|
3910
|
+
symbol_scope=self._symbols.current_scope.name,
|
|
3911
|
+
)
|
|
1243
3912
|
if type_spec.kind == "udt" and type_spec.name:
|
|
1244
3913
|
self._udt_var_types[node.name] = type_spec.name
|
|
1245
3914
|
|
|
1246
3915
|
# Track var members
|
|
1247
3916
|
if node.is_var or node.is_varip:
|
|
1248
3917
|
init_str = self._expr_to_str(node.value)
|
|
1249
|
-
scope_name = self._symbols.current_scope.name
|
|
1250
3918
|
# Block-scoped var name-collision disambiguation. A ``var``/``varip``
|
|
1251
|
-
# declared inside
|
|
1252
|
-
#
|
|
1253
|
-
# blocks declaring the same name would dedupe
|
|
1254
|
-
# cross-contaminate (proven:
|
|
1255
|
-
# ``var bool valid`` in the
|
|
3919
|
+
# declared inside any non-global block (an ``if`` / ``for`` /
|
|
3920
|
+
# ``while`` body, including one nested in a callable) is keyed by
|
|
3921
|
+
# RAW name. Two sibling blocks declaring the same name would dedupe
|
|
3922
|
+
# to ONE C++ member and cross-contaminate (proven:
|
|
3923
|
+
# egoigor1976-1-trendline-strategy's ``var bool valid`` in the
|
|
3924
|
+
# upper- and lower-trendline ``if`` blocks).
|
|
1256
3925
|
# When such a name already belongs to a DIFFERENT block, mint a
|
|
1257
3926
|
# scope-unique member name and record the rename so codegen activates
|
|
1258
3927
|
# it (via ``_active_var_remap``) while emitting that block.
|
|
1259
3928
|
member_name = node.name
|
|
1260
3929
|
is_block_scoped = (
|
|
1261
3930
|
not self._global_scope
|
|
1262
|
-
and not scope_name.startswith("func_")
|
|
1263
|
-
and not scope_name.startswith("method_")
|
|
1264
3931
|
and bool(self._block_node_stack)
|
|
1265
3932
|
)
|
|
1266
3933
|
if is_block_scoped:
|
|
@@ -1275,25 +3942,54 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1275
3942
|
member_name = f"{node.name}__blk{self._block_var_seq}"
|
|
1276
3943
|
self._block_var_renames.setdefault(block_id, {})[node.name] = member_name
|
|
1277
3944
|
self._var_members.append((member_name, val_type, init_str))
|
|
3945
|
+
scope_cursor = self._symbols.current_scope
|
|
3946
|
+
is_callable_scoped = False
|
|
3947
|
+
callable_owner: str | None = None
|
|
3948
|
+
while scope_cursor is not None:
|
|
3949
|
+
if scope_cursor.name.startswith(("func_", "method_")):
|
|
3950
|
+
is_callable_scoped = True
|
|
3951
|
+
callable_owner = (
|
|
3952
|
+
self._collection_scope_stack[-1]
|
|
3953
|
+
if self._collection_scope_stack
|
|
3954
|
+
else scope_cursor.name.split("_", 1)[1]
|
|
3955
|
+
)
|
|
3956
|
+
break
|
|
3957
|
+
scope_cursor = scope_cursor.parent
|
|
3958
|
+
self._var_member_metadata_by_node[id(node)] = (
|
|
3959
|
+
node, member_name, val_type, init_str, is_callable_scoped,
|
|
3960
|
+
)
|
|
3961
|
+
self._var_member_type_specs_by_node[id(node)] = type_spec
|
|
3962
|
+
self._var_member_owners_by_node[id(node)] = callable_owner
|
|
3963
|
+
# Preserve the emitted storage identity on the lexical Symbol so a
|
|
3964
|
+
# later history read can mark the exact sibling member rather than
|
|
3965
|
+
# only the legacy raw spelling.
|
|
3966
|
+
setattr(sym, "_pf_var_member_name", member_name)
|
|
1278
3967
|
# Capture the init AST too so codegen can inspect the RHS callee
|
|
1279
3968
|
# (used to detect int64-returning builtins like ``time()`` and
|
|
1280
3969
|
# promote the symbol storage type to ``int64_t``).
|
|
1281
3970
|
if node.value is not None:
|
|
1282
3971
|
self._var_member_init_exprs[member_name] = node.value
|
|
1283
|
-
# Track
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
3972
|
+
# Track callable-scoped var members under the analyzer's canonical
|
|
3973
|
+
# owner identity. Ordinary UDFs use ``name`` and UDT methods use
|
|
3974
|
+
# ``Type.method``; both feed the same written-callsite clone graph.
|
|
3975
|
+
if is_callable_scoped and callable_owner is not None:
|
|
3976
|
+
if callable_owner not in self._func_var_members:
|
|
3977
|
+
self._func_var_members[callable_owner] = []
|
|
3978
|
+
self._func_var_members[callable_owner].append(
|
|
3979
|
+
(member_name, val_type, init_str)
|
|
3980
|
+
)
|
|
1289
3981
|
|
|
1290
3982
|
# Track global-scope non-var declarations (needed as class members
|
|
1291
3983
|
# so user functions can reference them)
|
|
1292
3984
|
if (not node.is_var and not node.is_varip
|
|
1293
|
-
and self._symbols.current_scope.name == "global"
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
3985
|
+
and self._symbols.current_scope.name == "global"):
|
|
3986
|
+
if self._global_scope and not self._block_node_stack:
|
|
3987
|
+
self._ordinary_global_binding_info[node.name] = (
|
|
3988
|
+
id(node), val_type, node.value,
|
|
3989
|
+
)
|
|
3990
|
+
if node.name not in self._series_vars:
|
|
3991
|
+
self._global_var_decls.append((node.name, val_type))
|
|
3992
|
+
self._global_expr_map[node.name] = node.value
|
|
1297
3993
|
|
|
1298
3994
|
if self._symbols.current_scope.name == "global":
|
|
1299
3995
|
self._record_global_binding_stmt(
|
|
@@ -1315,7 +4011,12 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1315
4011
|
if isinstance(node.target, Identifier):
|
|
1316
4012
|
spec = self._type_spec_from_expr(node.value)
|
|
1317
4013
|
if spec is not None:
|
|
1318
|
-
self.
|
|
4014
|
+
target_sym = self._symbols.resolve(node.target.name)
|
|
4015
|
+
self._record_collection_type(
|
|
4016
|
+
node.target.name,
|
|
4017
|
+
spec,
|
|
4018
|
+
symbol_scope=(target_sym.scope if target_sym is not None else None),
|
|
4019
|
+
)
|
|
1319
4020
|
|
|
1320
4021
|
# Resolve the target
|
|
1321
4022
|
if isinstance(node.target, Identifier):
|
|
@@ -1359,13 +4060,28 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1359
4060
|
def _visit_TupleAssign(self, node: TupleAssign) -> PineType:
|
|
1360
4061
|
val_type = self._visit(node.value)
|
|
1361
4062
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
4063
|
+
element_types = self._tuple_element_types_by_node.get(id(node.value), ())
|
|
1362
4064
|
|
|
1363
4065
|
is_val_static = self._is_static_expression(node.value)
|
|
1364
4066
|
|
|
1365
|
-
for name in node.names:
|
|
4067
|
+
for idx, name in enumerate(node.names):
|
|
1366
4068
|
if name == "_":
|
|
1367
4069
|
continue
|
|
1368
4070
|
|
|
4071
|
+
inferred_element_type = (
|
|
4072
|
+
element_types[idx]
|
|
4073
|
+
if idx < len(element_types)
|
|
4074
|
+
else PineType.FLOAT
|
|
4075
|
+
)
|
|
4076
|
+
# Tuple bindings historically use double storage for every
|
|
4077
|
+
# numeric element, including integer literals. Preserve that
|
|
4078
|
+
# contract while retaining the newly-authoritative bool family.
|
|
4079
|
+
element_type = (
|
|
4080
|
+
PineType.BOOL
|
|
4081
|
+
if inferred_element_type == PineType.BOOL
|
|
4082
|
+
else PineType.FLOAT
|
|
4083
|
+
)
|
|
4084
|
+
|
|
1369
4085
|
if self._global_scope and is_val_static:
|
|
1370
4086
|
self._static_vars.add(name)
|
|
1371
4087
|
else:
|
|
@@ -1373,7 +4089,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1373
4089
|
|
|
1374
4090
|
sym = Symbol(
|
|
1375
4091
|
name=name,
|
|
1376
|
-
pine_type=
|
|
4092
|
+
pine_type=element_type,
|
|
1377
4093
|
is_series=False,
|
|
1378
4094
|
is_var=False,
|
|
1379
4095
|
is_const=False,
|
|
@@ -1384,6 +4100,15 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1384
4100
|
if name in self._static_vars:
|
|
1385
4101
|
setattr(sym, "is_static_series", True)
|
|
1386
4102
|
self._symbols.define(sym)
|
|
4103
|
+
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4104
|
+
setattr(sym, "_pf_decl_binding_name", name)
|
|
4105
|
+
|
|
4106
|
+
if (self._collection_scope_stack
|
|
4107
|
+
and self._block_node_stack
|
|
4108
|
+
and self._symbols.current_scope.name != "global"):
|
|
4109
|
+
owner_id = id(self._block_node_stack[-1])
|
|
4110
|
+
self._block_collection_owners[owner_id] = self._collection_scope_stack[-1]
|
|
4111
|
+
self._block_collection_types.setdefault(owner_id, {})[name] = None
|
|
1387
4112
|
|
|
1388
4113
|
# Track global-scope tuple-assign targets (e.g.
|
|
1389
4114
|
# ``[pdH, pdL] = request.security(...)``) as class members so user
|
|
@@ -1392,11 +4117,15 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1392
4117
|
# "use of undeclared identifier".
|
|
1393
4118
|
if (self._global_scope
|
|
1394
4119
|
and self._symbols.current_scope.name == "global"
|
|
1395
|
-
|
|
1396
|
-
self.
|
|
1397
|
-
|
|
4120
|
+
):
|
|
4121
|
+
self._ordinary_global_binding_info[name] = (
|
|
4122
|
+
id(node), element_type, node.value,
|
|
4123
|
+
)
|
|
4124
|
+
if name not in self._series_vars:
|
|
4125
|
+
self._global_var_decls.append((name, element_type))
|
|
4126
|
+
self._global_expr_map[name] = node.value
|
|
1398
4127
|
self._record_global_binding_stmt(
|
|
1399
|
-
name,
|
|
4128
|
+
name, element_type, False, decl_node=node,
|
|
1400
4129
|
)
|
|
1401
4130
|
|
|
1402
4131
|
return val_type
|
|
@@ -1435,6 +4164,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1435
4164
|
loc=loc,
|
|
1436
4165
|
type_spec=pspec,
|
|
1437
4166
|
)
|
|
4167
|
+
setattr(sym, "_pf_parameter_owner", node.name)
|
|
1438
4168
|
self._symbols.define(sym)
|
|
1439
4169
|
|
|
1440
4170
|
# Record TA counter before visiting body
|
|
@@ -1446,28 +4176,36 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1446
4176
|
self._global_scope = False
|
|
1447
4177
|
self._enclosing_func_params.append(set(node.params))
|
|
1448
4178
|
self._enclosing_func_names.append(node.name)
|
|
4179
|
+
self._collection_scope_stack.append(node.name)
|
|
1449
4180
|
self._nested_ta_touched = set()
|
|
1450
4181
|
try:
|
|
1451
4182
|
for stmt in node.body:
|
|
1452
4183
|
body_type = self._visit(stmt)
|
|
1453
4184
|
finally:
|
|
1454
4185
|
self._global_scope = old_global
|
|
4186
|
+
self._collection_scope_stack.pop()
|
|
1455
4187
|
self._enclosing_func_params.pop()
|
|
1456
4188
|
self._enclosing_func_names.pop()
|
|
1457
4189
|
nested_touched = self._nested_ta_touched
|
|
1458
4190
|
self._nested_ta_touched = None
|
|
1459
4191
|
|
|
1460
|
-
# Record TA
|
|
1461
|
-
#
|
|
1462
|
-
#
|
|
1463
|
-
# this function at its call site re-substitutes those nested sites too.
|
|
4192
|
+
# Record exact TA ownership for this function: direct allocations plus
|
|
4193
|
+
# targets borrowed from every nested stateful call. Per-owner ctor
|
|
4194
|
+
# templates retain forwarded-parameter expressions independently.
|
|
1464
4195
|
ta_end = len(self._ta_call_sites)
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
self._func_ta_ranges[node.name] = (
|
|
4196
|
+
exact_indices = sorted(
|
|
4197
|
+
set(range(ta_start, ta_end)) | set(nested_touched or ())
|
|
4198
|
+
)
|
|
4199
|
+
if exact_indices:
|
|
4200
|
+
self._func_ta_indices[node.name] = exact_indices
|
|
4201
|
+
self._func_ta_ranges[node.name] = (
|
|
4202
|
+
min(exact_indices), max(exact_indices) + 1
|
|
4203
|
+
)
|
|
4204
|
+
templates = self._func_ta_ctor_args.setdefault(node.name, {})
|
|
4205
|
+
for index in exact_indices:
|
|
4206
|
+
templates.setdefault(
|
|
4207
|
+
index, list(self._ta_call_sites[index].ctor_args)
|
|
4208
|
+
)
|
|
1471
4209
|
|
|
1472
4210
|
inferred_param_specs = self._param_type_specs_from_def(node)
|
|
1473
4211
|
for i, param in enumerate(node.params):
|
|
@@ -1479,11 +4217,46 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1479
4217
|
inferred_param_specs[i] = spec
|
|
1480
4218
|
self._func_param_type_specs[node.name] = inferred_param_specs
|
|
1481
4219
|
|
|
4220
|
+
# Capture direct terminal map-call metadata before leaving the lexical
|
|
4221
|
+
# function scope. Local map variables and typed map parameters are no
|
|
4222
|
+
# longer resolvable from the symbol table after ``exit_scope()``. Keep
|
|
4223
|
+
# this narrow to map terminals so general/nonterminal inference and
|
|
4224
|
+
# generated output remain unchanged.
|
|
4225
|
+
terminal_ret_expr = self._direct_terminal_return_expr(node)
|
|
4226
|
+
temporary_return_expr = (
|
|
4227
|
+
self._direct_terminal_array_temporary_return_expr(
|
|
4228
|
+
node, terminal_ret_expr
|
|
4229
|
+
)
|
|
4230
|
+
)
|
|
4231
|
+
if temporary_return_expr is not None:
|
|
4232
|
+
self._direct_terminal_array_temporary_exprs[node.name] = (
|
|
4233
|
+
temporary_return_expr
|
|
4234
|
+
)
|
|
4235
|
+
terminal_direct_return_spec = self._type_spec_from_expr(
|
|
4236
|
+
terminal_ret_expr
|
|
4237
|
+
)
|
|
4238
|
+
terminal_map_return = self._terminal_map_call_return(
|
|
4239
|
+
terminal_ret_expr,
|
|
4240
|
+
{
|
|
4241
|
+
name: spec
|
|
4242
|
+
for name, spec in zip(node.params, inferred_param_specs)
|
|
4243
|
+
},
|
|
4244
|
+
)
|
|
4245
|
+
terminal_array_get_return = self._terminal_array_get_return(
|
|
4246
|
+
terminal_ret_expr,
|
|
4247
|
+
{
|
|
4248
|
+
name: spec
|
|
4249
|
+
for name, spec in zip(node.params, inferred_param_specs)
|
|
4250
|
+
},
|
|
4251
|
+
terminal_direct_return_spec,
|
|
4252
|
+
)
|
|
4253
|
+
|
|
1482
4254
|
self._symbols.exit_scope()
|
|
1483
4255
|
|
|
1484
4256
|
# Detect if function returns a tuple (last stmt is TupleLiteral)
|
|
1485
4257
|
self._func_returns_tuple[node.name] = False
|
|
1486
4258
|
self._func_tuple_element_count[node.name] = 0
|
|
4259
|
+
self._func_tuple_element_types[node.name] = ()
|
|
1487
4260
|
if node.body:
|
|
1488
4261
|
last_stmt = node.body[-1]
|
|
1489
4262
|
tuple_node = None
|
|
@@ -1494,20 +4267,89 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1494
4267
|
if tuple_node is not None:
|
|
1495
4268
|
self._func_returns_tuple[node.name] = True
|
|
1496
4269
|
self._func_tuple_element_count[node.name] = len(tuple_node.elements)
|
|
4270
|
+
self._func_tuple_element_types[node.name] = (
|
|
4271
|
+
self._tuple_element_types_by_node.get(id(tuple_node), ())
|
|
4272
|
+
)
|
|
4273
|
+
elif (
|
|
4274
|
+
isinstance(terminal_ret_expr, FuncCall)
|
|
4275
|
+
and isinstance(terminal_ret_expr.callee, Identifier)
|
|
4276
|
+
):
|
|
4277
|
+
# A direct terminal helper wrapper preserves the callee's
|
|
4278
|
+
# tuple shape. Without this metadata request.security treats
|
|
4279
|
+
# ``outer(x) => inner(x)`` as scalar and emits std::get against
|
|
4280
|
+
# a double result. Keep propagation deliberately direct: more
|
|
4281
|
+
# complex conditional/collection return shapes remain outside
|
|
4282
|
+
# the supported helper-tuple contract.
|
|
4283
|
+
terminal_callee = terminal_ret_expr.callee.name
|
|
4284
|
+
if self._func_returns_tuple.get(terminal_callee, False):
|
|
4285
|
+
self._func_returns_tuple[node.name] = True
|
|
4286
|
+
self._func_tuple_element_count[node.name] = (
|
|
4287
|
+
self._func_tuple_element_count.get(terminal_callee, 0)
|
|
4288
|
+
)
|
|
4289
|
+
self._func_tuple_element_types[node.name] = (
|
|
4290
|
+
self._func_tuple_element_types.get(terminal_callee, ())
|
|
4291
|
+
)
|
|
4292
|
+
|
|
4293
|
+
# Re-run direct-wrapper propagation to a fixed point whenever a new
|
|
4294
|
+
# definition is analyzed. This makes ``outer()=>inner()`` source-order
|
|
4295
|
+
# safe when ``inner`` is defined later: once the callee's literal tuple
|
|
4296
|
+
# shape is known, every already-seen wrapper chain is updated before a
|
|
4297
|
+
# following request.security call is analyzed.
|
|
4298
|
+
changed = True
|
|
4299
|
+
while changed:
|
|
4300
|
+
changed = False
|
|
4301
|
+
for wrapper_name, wrapper_def in self._func_defs.items():
|
|
4302
|
+
wrapper_terminal = self._direct_terminal_return_expr(
|
|
4303
|
+
wrapper_def
|
|
4304
|
+
)
|
|
4305
|
+
if not (
|
|
4306
|
+
isinstance(wrapper_terminal, FuncCall)
|
|
4307
|
+
and isinstance(wrapper_terminal.callee, Identifier)
|
|
4308
|
+
):
|
|
4309
|
+
continue
|
|
4310
|
+
callee_name = wrapper_terminal.callee.name
|
|
4311
|
+
if not self._func_returns_tuple.get(callee_name, False):
|
|
4312
|
+
continue
|
|
4313
|
+
tuple_count = self._func_tuple_element_count.get(
|
|
4314
|
+
callee_name, 0
|
|
4315
|
+
)
|
|
4316
|
+
tuple_types = self._func_tuple_element_types.get(
|
|
4317
|
+
callee_name, ()
|
|
4318
|
+
)
|
|
4319
|
+
if (
|
|
4320
|
+
self._func_returns_tuple.get(wrapper_name, False)
|
|
4321
|
+
and self._func_tuple_element_count.get(wrapper_name, 0)
|
|
4322
|
+
== tuple_count
|
|
4323
|
+
and self._func_tuple_element_types.get(wrapper_name, ())
|
|
4324
|
+
== tuple_types
|
|
4325
|
+
):
|
|
4326
|
+
continue
|
|
4327
|
+
self._func_returns_tuple[wrapper_name] = True
|
|
4328
|
+
self._func_tuple_element_count[wrapper_name] = tuple_count
|
|
4329
|
+
self._func_tuple_element_types[wrapper_name] = tuple_types
|
|
4330
|
+
for info in self._func_infos:
|
|
4331
|
+
if info.name == wrapper_name:
|
|
4332
|
+
info.returns_tuple = True
|
|
4333
|
+
info.tuple_element_count = tuple_count
|
|
4334
|
+
changed = True
|
|
1497
4335
|
|
|
1498
4336
|
# Detect if the function returns a UDT instance via ``T.new(...)`` —
|
|
1499
4337
|
# used by codegen to emit the C++ return type as the struct name and
|
|
1500
4338
|
# to propagate UDT typing onto the caller's local. Probe:
|
|
1501
4339
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1502
4340
|
if node.body:
|
|
1503
|
-
|
|
1504
|
-
ret_expr = None
|
|
1505
|
-
if isinstance(last_stmt, ExprStmt):
|
|
1506
|
-
ret_expr = last_stmt.expr
|
|
1507
|
-
elif not isinstance(last_stmt, (TupleLiteral,)):
|
|
1508
|
-
# last_stmt is itself an expression node (single-expr funcs)
|
|
1509
|
-
ret_expr = last_stmt if hasattr(last_stmt, "loc") else None
|
|
4341
|
+
ret_expr = terminal_ret_expr
|
|
1510
4342
|
udt_ret = self._udt_name_from_ctor(ret_expr) if ret_expr is not None else None
|
|
4343
|
+
if udt_ret is None:
|
|
4344
|
+
udt_ret = self._udt_name_from_nullable_ctor_selection(ret_expr)
|
|
4345
|
+
if (udt_ret is None
|
|
4346
|
+
and terminal_direct_return_spec is not None
|
|
4347
|
+
and terminal_direct_return_spec.kind == "udt"):
|
|
4348
|
+
# A user UDT is a numeric object-ID handle just like a drawing
|
|
4349
|
+
# handle. Returning a parameter/local identity must retain
|
|
4350
|
+
# that exact handle type; limiting this route to drawings made
|
|
4351
|
+
# ``identity(Item value) => value`` emit ``double``.
|
|
4352
|
+
udt_ret = terminal_direct_return_spec.name
|
|
1511
4353
|
if udt_ret is None:
|
|
1512
4354
|
# Drawing-handle returns wrapped in an if-statement terminal
|
|
1513
4355
|
# branch (``makeEventLabel``) or returned as a bare drawing-handle
|
|
@@ -1522,9 +4364,40 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1522
4364
|
# returning an array returns a ``std::vector<...>``. The coarse
|
|
1523
4365
|
# PineType return can't represent this, so carry the TypeSpec.
|
|
1524
4366
|
if ret_expr is not None:
|
|
1525
|
-
|
|
4367
|
+
# This exact spec was captured while the function's lexical
|
|
4368
|
+
# symbols were still active. Re-resolving the terminal after
|
|
4369
|
+
# ``exit_scope()`` can bind a same-named top-level collection
|
|
4370
|
+
# and falsely turn a scalar UDF return into an array.
|
|
4371
|
+
ret_spec = terminal_direct_return_spec
|
|
1526
4372
|
if ret_spec is not None and ret_spec.kind == "array":
|
|
1527
4373
|
self._func_return_type_specs[node.name] = ret_spec
|
|
4374
|
+
if (terminal_direct_return_spec is not None
|
|
4375
|
+
and terminal_direct_return_spec.kind == "map"):
|
|
4376
|
+
self._func_return_type_specs[node.name] = terminal_direct_return_spec
|
|
4377
|
+
if (isinstance(terminal_ret_expr, Identifier)
|
|
4378
|
+
and terminal_direct_return_spec is not None
|
|
4379
|
+
and terminal_direct_return_spec.kind == "primitive"):
|
|
4380
|
+
# Preserve the lexical terminal identity before leaving the
|
|
4381
|
+
# function. Deferred map propagation must not reinterpret a
|
|
4382
|
+
# same-named scalar local through a top-level collection. Keep
|
|
4383
|
+
# this to direct identifier returns so unrelated call results
|
|
4384
|
+
# (for example ``map.size()``) retain legacy output typing.
|
|
4385
|
+
self._func_return_type_specs[node.name] = (
|
|
4386
|
+
terminal_direct_return_spec
|
|
4387
|
+
)
|
|
4388
|
+
|
|
4389
|
+
if terminal_map_return is not None:
|
|
4390
|
+
body_type, terminal_spec = terminal_map_return
|
|
4391
|
+
if terminal_spec is not None:
|
|
4392
|
+
self._func_return_type_specs[node.name] = terminal_spec
|
|
4393
|
+
|
|
4394
|
+
if terminal_array_get_return is not None:
|
|
4395
|
+
body_type, terminal_spec = terminal_array_get_return
|
|
4396
|
+
# Preserve the exact primitive TypeSpec as well as the coarse
|
|
4397
|
+
# PineType. Downstream collection construction consults this cache
|
|
4398
|
+
# directly; without it, asking for the UDF's element type can
|
|
4399
|
+
# re-visit the call and duplicate stateful call sites.
|
|
4400
|
+
self._func_return_type_specs[node.name] = terminal_spec
|
|
1528
4401
|
|
|
1529
4402
|
# Store return type
|
|
1530
4403
|
self._func_return_types[node.name] = body_type
|
|
@@ -1542,6 +4415,13 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1542
4415
|
)
|
|
1543
4416
|
self._symbols.define(sym)
|
|
1544
4417
|
|
|
4418
|
+
# A definition just completed may supply the primitive return needed
|
|
4419
|
+
# by an earlier direct ``array.from(udf()).get(...)`` reader. Refresh
|
|
4420
|
+
# before the next top-level statement is analyzed so its call site
|
|
4421
|
+
# observes the reconciled type.
|
|
4422
|
+
self._register_resolved_direct_terminal_array_forward_calls()
|
|
4423
|
+
self._refresh_direct_terminal_array_temporary_returns()
|
|
4424
|
+
|
|
1545
4425
|
return PineType.VOID
|
|
1546
4426
|
|
|
1547
4427
|
# ------------------------------------------------------------------
|
|
@@ -1590,7 +4470,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1590
4470
|
return PineType.VOID
|
|
1591
4471
|
|
|
1592
4472
|
def _visit_MethodDef(self, node) -> PineType:
|
|
1593
|
-
"""Register
|
|
4473
|
+
"""Register a typed instance method under ``TypeName.methodName``."""
|
|
1594
4474
|
method_key = f"{node.type_name}.{node.name}"
|
|
1595
4475
|
self._symbols.enter_scope(f"method_{node.type_name}_{node.name}")
|
|
1596
4476
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
@@ -1598,28 +4478,90 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1598
4478
|
param_types: list[PineType] = []
|
|
1599
4479
|
param_specs: list = []
|
|
1600
4480
|
for i, p in enumerate(node.params):
|
|
1601
|
-
udt_self = node.type_name if i == 0 else None
|
|
1602
4481
|
hint = param_hints[i] if i < len(param_hints) else None
|
|
1603
|
-
|
|
4482
|
+
# Only the receiver is required to be typed in Pine methods. Every
|
|
4483
|
+
# other omitted type is polymorphic per written call, exactly like
|
|
4484
|
+
# a regular UDF parameter; seeding it as FLOAT makes even a single
|
|
4485
|
+
# bool/int history call silently coerce through Series<double>.
|
|
4486
|
+
ptype = self._type_hint_to_pine(hint) if hint else PineType.UNKNOWN
|
|
1604
4487
|
pspec = self._type_spec_from_hint(hint) if hint else None
|
|
4488
|
+
udt_self = (
|
|
4489
|
+
node.type_name
|
|
4490
|
+
if i == 0 and pspec is not None and pspec.kind == "udt"
|
|
4491
|
+
else None
|
|
4492
|
+
)
|
|
1605
4493
|
param_types.append(ptype)
|
|
1606
4494
|
param_specs.append(pspec)
|
|
1607
|
-
|
|
4495
|
+
sym = Symbol(
|
|
1608
4496
|
name=p, pine_type=ptype, is_series=False,
|
|
1609
4497
|
is_var=False, is_const=False, const_value=None,
|
|
1610
4498
|
scope=self._symbols.current_scope.name, loc=loc,
|
|
1611
4499
|
udt_type_name=udt_self,
|
|
1612
4500
|
type_spec=pspec,
|
|
1613
|
-
)
|
|
4501
|
+
)
|
|
4502
|
+
setattr(sym, "_pf_parameter_owner", method_key)
|
|
4503
|
+
self._symbols.define(sym)
|
|
1614
4504
|
ret_type = PineType.VOID
|
|
4505
|
+
ta_start = len(self._ta_call_sites)
|
|
1615
4506
|
old_global = self._global_scope
|
|
1616
4507
|
self._global_scope = False
|
|
4508
|
+
self._enclosing_func_params.append(set(node.params))
|
|
4509
|
+
self._enclosing_func_names.append(method_key)
|
|
4510
|
+
self._collection_scope_stack.append(method_key)
|
|
4511
|
+
previous_nested_ta_touched = self._nested_ta_touched
|
|
4512
|
+
self._nested_ta_touched = set()
|
|
4513
|
+
terminal_ret_expr = self._direct_terminal_return_expr(node)
|
|
4514
|
+
return_type_spec = None
|
|
4515
|
+
method_udt_return = None
|
|
1617
4516
|
try:
|
|
1618
4517
|
for stmt in node.body:
|
|
1619
4518
|
ret_type = self._visit(stmt)
|
|
4519
|
+
if terminal_ret_expr is not None:
|
|
4520
|
+
terminal_spec = self._type_spec_from_expr(terminal_ret_expr)
|
|
4521
|
+
if terminal_spec is not None and terminal_spec.kind == "map":
|
|
4522
|
+
return_type_spec = terminal_spec
|
|
4523
|
+
if terminal_spec is not None and terminal_spec.kind == "udt":
|
|
4524
|
+
# Methods may return ``self`` or another UDT-typed
|
|
4525
|
+
# parameter/local, not only a syntactic ``Type.new``.
|
|
4526
|
+
method_udt_return = terminal_spec.name
|
|
4527
|
+
return_type_spec = terminal_spec
|
|
4528
|
+
else:
|
|
4529
|
+
method_udt_return = (
|
|
4530
|
+
self._udt_name_from_ctor(terminal_ret_expr)
|
|
4531
|
+
or self._udt_name_from_nullable_ctor_selection(
|
|
4532
|
+
terminal_ret_expr
|
|
4533
|
+
)
|
|
4534
|
+
)
|
|
4535
|
+
if method_udt_return is not None:
|
|
4536
|
+
return_type_spec = TypeSpec.udt(method_udt_return)
|
|
1620
4537
|
finally:
|
|
1621
4538
|
self._global_scope = old_global
|
|
4539
|
+
self._collection_scope_stack.pop()
|
|
4540
|
+
self._enclosing_func_params.pop()
|
|
4541
|
+
self._enclosing_func_names.pop()
|
|
4542
|
+
nested_touched = self._nested_ta_touched
|
|
4543
|
+
self._nested_ta_touched = previous_nested_ta_touched
|
|
4544
|
+
|
|
4545
|
+
# UDT methods participate in the same stateful call graph as ordinary
|
|
4546
|
+
# UDFs. Preserve exact direct and borrowed TA ownership under the
|
|
4547
|
+
# canonical ``Type.method`` identity so written call sites can clone it.
|
|
4548
|
+
ta_end = len(self._ta_call_sites)
|
|
4549
|
+
exact_indices = sorted(
|
|
4550
|
+
set(range(ta_start, ta_end)) | set(nested_touched or ())
|
|
4551
|
+
)
|
|
4552
|
+
if exact_indices:
|
|
4553
|
+
self._func_ta_indices[method_key] = exact_indices
|
|
4554
|
+
self._func_ta_ranges[method_key] = (
|
|
4555
|
+
min(exact_indices), max(exact_indices) + 1
|
|
4556
|
+
)
|
|
4557
|
+
templates = self._func_ta_ctor_args.setdefault(method_key, {})
|
|
4558
|
+
for index in exact_indices:
|
|
4559
|
+
templates.setdefault(
|
|
4560
|
+
index, list(self._ta_call_sites[index].ctor_args)
|
|
4561
|
+
)
|
|
1622
4562
|
self._symbols.exit_scope()
|
|
4563
|
+
if method_udt_return is not None:
|
|
4564
|
+
self._func_udt_return_types[method_key] = method_udt_return
|
|
1623
4565
|
|
|
1624
4566
|
# Detect tuple return on UDT methods (mirrors the regular FuncDef logic
|
|
1625
4567
|
# earlier in this file). Without this, codegen emits the method with a
|
|
@@ -1646,44 +4588,113 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1646
4588
|
# them (e.g., synthetic MethodDef nodes from tests).
|
|
1647
4589
|
while len(param_defaults) < len(node.params):
|
|
1648
4590
|
param_defaults.append(None)
|
|
4591
|
+
self._func_param_type_specs[method_key] = list(param_specs)
|
|
1649
4592
|
fi = FuncInfo(
|
|
1650
4593
|
name=method_key,
|
|
1651
4594
|
param_types=param_types,
|
|
1652
4595
|
return_type=ret_type,
|
|
1653
|
-
node=FuncDef(
|
|
1654
|
-
|
|
4596
|
+
node=FuncDef(
|
|
4597
|
+
name=node.name,
|
|
4598
|
+
params=node.params,
|
|
4599
|
+
body=node.body,
|
|
4600
|
+
is_single_expr=node.is_single_expr,
|
|
4601
|
+
annotations=dict(node.annotations or {}),
|
|
4602
|
+
),
|
|
1655
4603
|
is_udt_method=True,
|
|
1656
4604
|
udt_type_name=node.type_name,
|
|
1657
4605
|
returns_tuple=returns_tuple,
|
|
1658
4606
|
tuple_element_count=tuple_element_count,
|
|
1659
4607
|
param_defaults=param_defaults,
|
|
1660
4608
|
param_type_specs=param_specs,
|
|
4609
|
+
return_type_spec=return_type_spec,
|
|
4610
|
+
udt_return_type=method_udt_return,
|
|
1661
4611
|
)
|
|
1662
4612
|
self._func_infos.append(fi)
|
|
4613
|
+
# A just-registered later sibling can resolve an earlier direct method
|
|
4614
|
+
# wrapper before the next source statement is analyzed.
|
|
4615
|
+
self._refresh_direct_typed_method_wrapper_returns()
|
|
1663
4616
|
return PineType.VOID
|
|
1664
4617
|
|
|
1665
4618
|
# ------------------------------------------------------------------
|
|
1666
4619
|
# Control flow
|
|
1667
4620
|
# ------------------------------------------------------------------
|
|
1668
4621
|
|
|
4622
|
+
def _top_level_branch_needs_lexical_scope(
|
|
4623
|
+
self, body: list[ASTNode]
|
|
4624
|
+
) -> bool:
|
|
4625
|
+
"""Whether a top-level branch declares over a direct script binding.
|
|
4626
|
+
|
|
4627
|
+
Functions already own a symbol-table scope and ``for``/``for in``
|
|
4628
|
+
create one explicitly. Top-level if/while/switch branches historically
|
|
4629
|
+
reused the global Scope, so a local declaration could overwrite the
|
|
4630
|
+
analyzer's outer Symbol even though codegen emits a C++ lexical local.
|
|
4631
|
+
Add a child scope only for the collision shape handled by this change;
|
|
4632
|
+
every unrelated program keeps the established analysis path.
|
|
4633
|
+
"""
|
|
4634
|
+
if self._collection_scope_stack:
|
|
4635
|
+
return False
|
|
4636
|
+
for stmt in body:
|
|
4637
|
+
if (isinstance(stmt, VarDecl)
|
|
4638
|
+
and stmt.name in self._direct_program_binding_names):
|
|
4639
|
+
return True
|
|
4640
|
+
if (isinstance(stmt, TupleAssign)
|
|
4641
|
+
and any(
|
|
4642
|
+
name in self._direct_program_binding_names
|
|
4643
|
+
for name in stmt.names
|
|
4644
|
+
if name != "_"
|
|
4645
|
+
)):
|
|
4646
|
+
return True
|
|
4647
|
+
return False
|
|
4648
|
+
|
|
4649
|
+
def _enter_top_level_branch_scope(
|
|
4650
|
+
self, body: list[ASTNode], name: str
|
|
4651
|
+
) -> bool:
|
|
4652
|
+
scoped = self._top_level_branch_needs_lexical_scope(body)
|
|
4653
|
+
if scoped:
|
|
4654
|
+
self._symbols.enter_scope(name)
|
|
4655
|
+
return scoped
|
|
4656
|
+
|
|
1669
4657
|
def _visit_IfStmt(self, node: IfStmt) -> PineType:
|
|
1670
4658
|
old_global = self._global_scope
|
|
1671
4659
|
self._global_scope = False
|
|
1672
|
-
self._block_node_stack.append(node)
|
|
1673
4660
|
try:
|
|
1674
4661
|
self._visit(node.condition)
|
|
1675
4662
|
body_type = PineType.VOID
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
4663
|
+
body_scoped = self._enter_top_level_branch_scope(
|
|
4664
|
+
node.body, "top_if"
|
|
4665
|
+
)
|
|
4666
|
+
self._block_node_stack.append(node.body)
|
|
4667
|
+
try:
|
|
4668
|
+
for stmt in node.body:
|
|
4669
|
+
body_type = self._visit(stmt)
|
|
4670
|
+
finally:
|
|
4671
|
+
self._block_node_stack.pop()
|
|
4672
|
+
if body_scoped:
|
|
4673
|
+
self._symbols.exit_scope()
|
|
4674
|
+
if node.else_body:
|
|
4675
|
+
else_scoped = self._enter_top_level_branch_scope(
|
|
4676
|
+
node.else_body, "top_else"
|
|
4677
|
+
)
|
|
4678
|
+
self._block_node_stack.append(node.else_body)
|
|
4679
|
+
try:
|
|
4680
|
+
for stmt in node.else_body:
|
|
4681
|
+
self._visit(stmt)
|
|
4682
|
+
finally:
|
|
4683
|
+
self._block_node_stack.pop()
|
|
4684
|
+
if else_scoped:
|
|
4685
|
+
self._symbols.exit_scope()
|
|
1680
4686
|
finally:
|
|
1681
|
-
self._block_node_stack.pop()
|
|
1682
4687
|
self._global_scope = old_global
|
|
1683
4688
|
# If used as expression (x = if ...), return last expr type
|
|
1684
4689
|
return body_type
|
|
1685
4690
|
|
|
1686
4691
|
def _visit_ForStmt(self, node: ForStmt) -> PineType:
|
|
4692
|
+
outer_symbol = self._symbols.resolve(node.var)
|
|
4693
|
+
outer_spec = (
|
|
4694
|
+
getattr(outer_symbol, "type_spec", None)
|
|
4695
|
+
if outer_symbol is not None
|
|
4696
|
+
else self._collection_types.get(node.var)
|
|
4697
|
+
)
|
|
1687
4698
|
self._symbols.enter_scope("for")
|
|
1688
4699
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
1689
4700
|
|
|
@@ -1697,8 +4708,13 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1697
4708
|
const_value=None,
|
|
1698
4709
|
scope="for",
|
|
1699
4710
|
loc=loc,
|
|
4711
|
+
type_spec=TypeSpec.primitive("int"),
|
|
1700
4712
|
)
|
|
4713
|
+
if self._type_spec_contains_map(outer_spec):
|
|
4714
|
+
setattr(sym, "_pf_shadows_map_state", True)
|
|
1701
4715
|
self._symbols.define(sym)
|
|
4716
|
+
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4717
|
+
setattr(sym, "_pf_decl_binding_name", node.var)
|
|
1702
4718
|
|
|
1703
4719
|
old_global = self._global_scope
|
|
1704
4720
|
self._global_scope = False
|
|
@@ -1723,22 +4739,66 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1723
4739
|
self._block_node_stack.append(node)
|
|
1724
4740
|
try:
|
|
1725
4741
|
self._visit(node.iterable)
|
|
4742
|
+
iterable_spec = self._type_spec_from_expr(node.iterable)
|
|
4743
|
+
element_spec = (
|
|
4744
|
+
iterable_spec.element
|
|
4745
|
+
if iterable_spec is not None and iterable_spec.kind == "array"
|
|
4746
|
+
else None
|
|
4747
|
+
)
|
|
4748
|
+
tuple_specs: list[TypeSpec | None] = []
|
|
4749
|
+
if iterable_spec is not None and iterable_spec.kind == "map":
|
|
4750
|
+
tuple_specs = [iterable_spec.key, iterable_spec.value]
|
|
1726
4751
|
self._symbols.enter_scope("for_in")
|
|
1727
4752
|
if node.var:
|
|
1728
4753
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
1729
|
-
self.
|
|
1730
|
-
|
|
4754
|
+
pine_type = self._element_pine_type(element_spec)
|
|
4755
|
+
if pine_type == PineType.VOID:
|
|
4756
|
+
pine_type = PineType.FLOAT
|
|
4757
|
+
outer_symbol = self._symbols.resolve(node.var)
|
|
4758
|
+
outer_spec = (
|
|
4759
|
+
getattr(outer_symbol, "type_spec", None)
|
|
4760
|
+
if outer_symbol is not None
|
|
4761
|
+
else self._collection_types.get(node.var)
|
|
4762
|
+
)
|
|
4763
|
+
sym = Symbol(
|
|
4764
|
+
name=node.var, pine_type=pine_type, is_series=False,
|
|
1731
4765
|
is_var=False, is_const=False, const_value=None,
|
|
1732
4766
|
scope=self._symbols.current_scope.name, loc=loc,
|
|
1733
|
-
|
|
4767
|
+
type_spec=element_spec,
|
|
4768
|
+
)
|
|
4769
|
+
if self._type_spec_contains_map(outer_spec):
|
|
4770
|
+
setattr(sym, "_pf_shadows_map_state", True)
|
|
4771
|
+
self._symbols.define(sym)
|
|
4772
|
+
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4773
|
+
setattr(sym, "_pf_decl_binding_name", node.var)
|
|
1734
4774
|
if node.vars:
|
|
1735
4775
|
loc = node.loc or SourceLocation(file=self._filename, line=1, col=1, end_col=1)
|
|
1736
|
-
for v in node.vars:
|
|
1737
|
-
|
|
1738
|
-
|
|
4776
|
+
for idx, v in enumerate(node.vars):
|
|
4777
|
+
binder_spec = (
|
|
4778
|
+
tuple_specs[idx]
|
|
4779
|
+
if idx < len(tuple_specs)
|
|
4780
|
+
else None
|
|
4781
|
+
)
|
|
4782
|
+
pine_type = self._element_pine_type(binder_spec)
|
|
4783
|
+
if pine_type == PineType.VOID:
|
|
4784
|
+
pine_type = PineType.FLOAT
|
|
4785
|
+
outer_symbol = self._symbols.resolve(v)
|
|
4786
|
+
outer_spec = (
|
|
4787
|
+
getattr(outer_symbol, "type_spec", None)
|
|
4788
|
+
if outer_symbol is not None
|
|
4789
|
+
else self._collection_types.get(v)
|
|
4790
|
+
)
|
|
4791
|
+
sym = Symbol(
|
|
4792
|
+
name=v, pine_type=pine_type, is_series=False,
|
|
1739
4793
|
is_var=False, is_const=False, const_value=None,
|
|
1740
4794
|
scope=self._symbols.current_scope.name, loc=loc,
|
|
1741
|
-
|
|
4795
|
+
type_spec=binder_spec,
|
|
4796
|
+
)
|
|
4797
|
+
if self._type_spec_contains_map(outer_spec):
|
|
4798
|
+
setattr(sym, "_pf_shadows_map_state", True)
|
|
4799
|
+
self._symbols.define(sym)
|
|
4800
|
+
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4801
|
+
setattr(sym, "_pf_decl_binding_name", v)
|
|
1742
4802
|
for stmt in node.body:
|
|
1743
4803
|
self._visit(stmt)
|
|
1744
4804
|
self._symbols.exit_scope()
|
|
@@ -1750,6 +4810,9 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1750
4810
|
def _visit_WhileStmt(self, node: WhileStmt) -> PineType:
|
|
1751
4811
|
old_global = self._global_scope
|
|
1752
4812
|
self._global_scope = False
|
|
4813
|
+
body_scoped = self._enter_top_level_branch_scope(
|
|
4814
|
+
node.body, "top_while"
|
|
4815
|
+
)
|
|
1753
4816
|
self._block_node_stack.append(node)
|
|
1754
4817
|
try:
|
|
1755
4818
|
self._visit(node.condition)
|
|
@@ -1757,6 +4820,8 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1757
4820
|
self._visit(stmt)
|
|
1758
4821
|
finally:
|
|
1759
4822
|
self._block_node_stack.pop()
|
|
4823
|
+
if body_scoped:
|
|
4824
|
+
self._symbols.exit_scope()
|
|
1760
4825
|
self._global_scope = old_global
|
|
1761
4826
|
return PineType.VOID
|
|
1762
4827
|
|
|
@@ -1770,10 +4835,29 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1770
4835
|
for case_expr, case_body in node.cases:
|
|
1771
4836
|
if case_expr:
|
|
1772
4837
|
self._visit(case_expr)
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
self.
|
|
4838
|
+
case_scoped = self._enter_top_level_branch_scope(
|
|
4839
|
+
case_body, "top_switch_case"
|
|
4840
|
+
)
|
|
4841
|
+
self._block_node_stack.append(case_body)
|
|
4842
|
+
try:
|
|
4843
|
+
for stmt in case_body:
|
|
4844
|
+
result_type = self._visit(stmt)
|
|
4845
|
+
finally:
|
|
4846
|
+
self._block_node_stack.pop()
|
|
4847
|
+
if case_scoped:
|
|
4848
|
+
self._symbols.exit_scope()
|
|
4849
|
+
if node.default_body:
|
|
4850
|
+
default_scoped = self._enter_top_level_branch_scope(
|
|
4851
|
+
node.default_body, "top_switch_default"
|
|
4852
|
+
)
|
|
4853
|
+
self._block_node_stack.append(node.default_body)
|
|
4854
|
+
try:
|
|
4855
|
+
for stmt in node.default_body:
|
|
4856
|
+
self._visit(stmt)
|
|
4857
|
+
finally:
|
|
4858
|
+
self._block_node_stack.pop()
|
|
4859
|
+
if default_scoped:
|
|
4860
|
+
self._symbols.exit_scope()
|
|
1777
4861
|
finally:
|
|
1778
4862
|
self._global_scope = old_global
|
|
1779
4863
|
# If used as expression (x = switch ...), return last expr type
|
|
@@ -1907,12 +4991,210 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1907
4991
|
if isinstance(obj, Identifier) and obj.name == "request":
|
|
1908
4992
|
return self._handle_request_call(member, node)
|
|
1909
4993
|
|
|
4994
|
+
# strategy.closedtrades.*(idx) / strategy.opentrades.*(idx).
|
|
4995
|
+
# The callee is a nested MemberAccess, so it does not enter the
|
|
4996
|
+
# direct ``strategy.*`` branch above. Its MemberAccess visitor
|
|
4997
|
+
# already owns the authoritative accessor return-type mapping;
|
|
4998
|
+
# preserve that type for the call instead of falling through to
|
|
4999
|
+
# VOID (which later becomes a C++ double declaration).
|
|
5000
|
+
if (isinstance(obj, MemberAccess)
|
|
5001
|
+
and isinstance(obj.object, Identifier)
|
|
5002
|
+
and obj.object.name == "strategy"
|
|
5003
|
+
and obj.member in ("closedtrades", "opentrades")):
|
|
5004
|
+
for arg in node.args:
|
|
5005
|
+
self._visit(arg)
|
|
5006
|
+
for val in node.kwargs.values():
|
|
5007
|
+
self._visit(val)
|
|
5008
|
+
return self._visit(callee)
|
|
5009
|
+
|
|
5010
|
+
# Compatibility functional form: ``Type.copy(object)``. The
|
|
5011
|
+
# canonical Pine spelling is ``object.copy()`` below, but retaining
|
|
5012
|
+
# this existing surface keeps older corpus sources well-defined.
|
|
5013
|
+
if (
|
|
5014
|
+
isinstance(obj, Identifier)
|
|
5015
|
+
and obj.name in self._udt_fields
|
|
5016
|
+
and member == "copy"
|
|
5017
|
+
):
|
|
5018
|
+
for arg in node.args:
|
|
5019
|
+
self._visit(arg)
|
|
5020
|
+
for val in node.kwargs.values():
|
|
5021
|
+
self._visit(val)
|
|
5022
|
+
if len(node.args) != 1 or node.kwargs:
|
|
5023
|
+
self._error(
|
|
5024
|
+
f"{obj.name}.copy(...) expects exactly one object argument",
|
|
5025
|
+
node.loc,
|
|
5026
|
+
)
|
|
5027
|
+
return PineType.UNKNOWN
|
|
5028
|
+
return self._visit(node.args[0])
|
|
5029
|
+
|
|
1910
5030
|
# General member call (e.g., array.push, etc.)
|
|
1911
|
-
self._visit(obj)
|
|
5031
|
+
receiver_pine_type = self._visit(obj)
|
|
5032
|
+
visited_member_arg_types: dict[int, PineType] = {}
|
|
1912
5033
|
for arg in node.args:
|
|
1913
|
-
self._visit(arg)
|
|
5034
|
+
visited_member_arg_types[id(arg)] = self._visit(arg)
|
|
1914
5035
|
for val in node.kwargs.values():
|
|
1915
|
-
self._visit(val)
|
|
5036
|
+
visited_member_arg_types[id(val)] = self._visit(val)
|
|
5037
|
+
# UDT method call-site typing. Method definitions may contain an
|
|
5038
|
+
# untyped parameter history read; resolve receiver + args here and
|
|
5039
|
+
# apply the same deferred map-history gate as regular UDFs before
|
|
5040
|
+
# codegen can emit the parameter as a scalar double.
|
|
5041
|
+
receiver_spec = self._type_spec_from_expr(obj)
|
|
5042
|
+
receiver_type_name = method_receiver_type_name(receiver_spec)
|
|
5043
|
+
if receiver_type_name is not None:
|
|
5044
|
+
method_key = f"{receiver_type_name}.{member}"
|
|
5045
|
+
signature = self._method_signatures.get(method_key)
|
|
5046
|
+
strict_binding = (
|
|
5047
|
+
self._bind_typed_method_call(method_key, node)
|
|
5048
|
+
if signature is not None
|
|
5049
|
+
else None
|
|
5050
|
+
)
|
|
5051
|
+
if strict_binding is not None:
|
|
5052
|
+
# Defaults are real call-site expressions. Visit only the
|
|
5053
|
+
# inserted nodes here; written actuals were visited above.
|
|
5054
|
+
supplied_ids = {
|
|
5055
|
+
id(arg) for arg in [*node.args, *node.kwargs.values()]
|
|
5056
|
+
}
|
|
5057
|
+
for arg in strict_binding.evaluation_order:
|
|
5058
|
+
if id(arg) not in supplied_ids:
|
|
5059
|
+
visited_member_arg_types[id(arg)] = self._visit(arg)
|
|
5060
|
+
method_info = next(
|
|
5061
|
+
(
|
|
5062
|
+
info
|
|
5063
|
+
for info in self._func_infos
|
|
5064
|
+
if info.name == method_key
|
|
5065
|
+
and getattr(info, "is_udt_method", False)
|
|
5066
|
+
),
|
|
5067
|
+
None,
|
|
5068
|
+
)
|
|
5069
|
+
if method_info is not None and method_info.node is not None:
|
|
5070
|
+
method_params = list(method_info.node.params)
|
|
5071
|
+
rest_params = method_params[1:]
|
|
5072
|
+
rest_bound = (
|
|
5073
|
+
list(strict_binding.args_by_param)
|
|
5074
|
+
if strict_binding is not None
|
|
5075
|
+
else self._bind_callable_args(node, rest_params)
|
|
5076
|
+
)
|
|
5077
|
+
full_bound: list[ASTNode | None] = [obj, *rest_bound]
|
|
5078
|
+
full_param_types = [
|
|
5079
|
+
receiver_pine_type,
|
|
5080
|
+
*[
|
|
5081
|
+
visited_member_arg_types.get(
|
|
5082
|
+
id(arg), PineType.UNKNOWN
|
|
5083
|
+
)
|
|
5084
|
+
if arg is not None
|
|
5085
|
+
else PineType.UNKNOWN
|
|
5086
|
+
for arg in rest_bound
|
|
5087
|
+
],
|
|
5088
|
+
]
|
|
5089
|
+
declared_specs = list(
|
|
5090
|
+
self._func_param_type_specs.get(method_key, ())
|
|
5091
|
+
)
|
|
5092
|
+
full_param_types = [
|
|
5093
|
+
(
|
|
5094
|
+
self._primitive_pine_type_from_spec(
|
|
5095
|
+
declared_specs[index]
|
|
5096
|
+
)
|
|
5097
|
+
if index < len(declared_specs)
|
|
5098
|
+
and self._primitive_pine_type_from_spec(
|
|
5099
|
+
declared_specs[index]
|
|
5100
|
+
) != PineType.UNKNOWN
|
|
5101
|
+
else param_type
|
|
5102
|
+
)
|
|
5103
|
+
for index, param_type in enumerate(full_param_types)
|
|
5104
|
+
]
|
|
5105
|
+
self._callable_bound_param_types_by_node[id(node)] = list(
|
|
5106
|
+
full_param_types
|
|
5107
|
+
)
|
|
5108
|
+
effective_specs = list(
|
|
5109
|
+
getattr(method_info, "param_type_specs", None) or []
|
|
5110
|
+
)
|
|
5111
|
+
while len(effective_specs) < len(method_params):
|
|
5112
|
+
effective_specs.append(None)
|
|
5113
|
+
for index, arg in enumerate(full_bound):
|
|
5114
|
+
if effective_specs[index] is None and arg is not None:
|
|
5115
|
+
effective_specs[index] = self._type_spec_from_expr(arg)
|
|
5116
|
+
self._record_deferred_param_call_edge(
|
|
5117
|
+
node,
|
|
5118
|
+
method_key,
|
|
5119
|
+
method_params,
|
|
5120
|
+
full_bound,
|
|
5121
|
+
)
|
|
5122
|
+
self._validate_deferred_param_history_refs(
|
|
5123
|
+
method_key,
|
|
5124
|
+
{
|
|
5125
|
+
name: spec
|
|
5126
|
+
for name, spec in zip(
|
|
5127
|
+
method_params, effective_specs
|
|
5128
|
+
)
|
|
5129
|
+
},
|
|
5130
|
+
)
|
|
5131
|
+
method_series = self._func_series_vars.get(
|
|
5132
|
+
method_key, set()
|
|
5133
|
+
)
|
|
5134
|
+
for index, param_name in enumerate(method_params):
|
|
5135
|
+
if (
|
|
5136
|
+
param_name not in method_series
|
|
5137
|
+
or index >= len(full_bound)
|
|
5138
|
+
):
|
|
5139
|
+
continue
|
|
5140
|
+
arg = full_bound[index]
|
|
5141
|
+
if isinstance(arg, Identifier) and arg.name in BAR_FIELDS:
|
|
5142
|
+
self._series_bar_fields.add(arg.name)
|
|
5143
|
+
elif isinstance(arg, Identifier):
|
|
5144
|
+
arg_sym = self._symbols.resolve(arg.name)
|
|
5145
|
+
if arg_sym is not None:
|
|
5146
|
+
arg_sym.is_series = True
|
|
5147
|
+
# Materialize TA state while the lexical caller is still
|
|
5148
|
+
# active, just as ``_handle_user_func_call`` does for a
|
|
5149
|
+
# bare UDF call. Deferring every UDT-method call to the
|
|
5150
|
+
# whole-program call-graph pass loses the enclosing
|
|
5151
|
+
# callable's parameter stack: ``outer(self, len) =>
|
|
5152
|
+
# self.inner(len)`` then leaves ``inner``'s constructor
|
|
5153
|
+
# argument as the bare local name ``len`` instead of
|
|
5154
|
+
# threading the eventual top-level input through both
|
|
5155
|
+
# boundaries. Besides substituting the arguments, the
|
|
5156
|
+
# cs0 path records the borrowed TA site in
|
|
5157
|
+
# ``_nested_ta_touched`` so the caller's range is widened
|
|
5158
|
+
# and can be resolved again at its own call site.
|
|
5159
|
+
if method_key in self._func_ta_ranges:
|
|
5160
|
+
existing_site = self._func_call_cs_map.get(id(node))
|
|
5161
|
+
if (
|
|
5162
|
+
existing_site is None
|
|
5163
|
+
or existing_site[0] != method_key
|
|
5164
|
+
):
|
|
5165
|
+
cs_idx = self._func_call_site_count.get(
|
|
5166
|
+
method_key, 0
|
|
5167
|
+
)
|
|
5168
|
+
self._func_call_site_count[method_key] = cs_idx + 1
|
|
5169
|
+
self._func_call_cs_map[id(node)] = (
|
|
5170
|
+
method_key, cs_idx
|
|
5171
|
+
)
|
|
5172
|
+
self._materialize_user_func_call_site_state(
|
|
5173
|
+
method_key, cs_idx, node
|
|
5174
|
+
)
|
|
5175
|
+
return self._callsite_callable_return_type(
|
|
5176
|
+
method_info.node,
|
|
5177
|
+
full_param_types,
|
|
5178
|
+
method_info.return_type,
|
|
5179
|
+
)
|
|
5180
|
+
if signature is not None:
|
|
5181
|
+
# The exact authored method exists later in source. Its
|
|
5182
|
+
# body-derived return/effect metadata is intentionally not
|
|
5183
|
+
# guessed here, and the call must not fall through to a
|
|
5184
|
+
# same-named builtin such as UDT ``copy()``.
|
|
5185
|
+
return PineType.UNKNOWN
|
|
5186
|
+
if (
|
|
5187
|
+
receiver_spec.kind == "udt"
|
|
5188
|
+
and receiver_spec.name in self._udt_fields
|
|
5189
|
+
and member == "copy"
|
|
5190
|
+
):
|
|
5191
|
+
if node.args or node.kwargs:
|
|
5192
|
+
self._error(
|
|
5193
|
+
f"{receiver_spec.name}.copy() expects no arguments",
|
|
5194
|
+
node.loc,
|
|
5195
|
+
)
|
|
5196
|
+
return PineType.UNKNOWN
|
|
5197
|
+
return receiver_pine_type
|
|
1916
5198
|
# Matrix method dispatch: ``m.get(0, 0)`` on ``matrix<int>`` must
|
|
1917
5199
|
# type as INT, not VOID, so ``v = m.get(...)`` propagates the
|
|
1918
5200
|
# element PineType. ``_type_spec_from_expr`` already carries the
|
|
@@ -1921,7 +5203,16 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1921
5203
|
# arithmetic inference) honest. See call_handlers.py
|
|
1922
5204
|
# ``_handle_matrix_method``.
|
|
1923
5205
|
if isinstance(obj, Identifier):
|
|
1924
|
-
|
|
5206
|
+
recv_sym = self._symbols.resolve(obj.name)
|
|
5207
|
+
recv_spec = (
|
|
5208
|
+
getattr(recv_sym, "type_spec", None)
|
|
5209
|
+
if recv_sym is not None
|
|
5210
|
+
else None
|
|
5211
|
+
)
|
|
5212
|
+
# Only fall back to the top-level raw registry when lexical
|
|
5213
|
+
# resolution did not find a shadowing local/parameter.
|
|
5214
|
+
if recv_sym is None or recv_sym.scope == "global":
|
|
5215
|
+
recv_spec = recv_spec or self._collection_types.get(obj.name)
|
|
1925
5216
|
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
1926
5217
|
return self._handle_matrix_method(member, recv_spec)
|
|
1927
5218
|
return PineType.VOID
|
|
@@ -1990,10 +5281,530 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1990
5281
|
self._visit(val)
|
|
1991
5282
|
return PineType.VOID
|
|
1992
5283
|
|
|
5284
|
+
def _type_spec_contains_map(
|
|
5285
|
+
self,
|
|
5286
|
+
spec: TypeSpec | None,
|
|
5287
|
+
visiting_udts: frozenset[str] = frozenset(),
|
|
5288
|
+
) -> bool:
|
|
5289
|
+
"""Whether a lexical TypeSpec recursively owns a PineMap handle.
|
|
5290
|
+
|
|
5291
|
+
History buffers value-copy their elements. A map ID anywhere in that
|
|
5292
|
+
shape would therefore retain live storage across bars and silently
|
|
5293
|
+
turn ``[1]`` into a current-state alias. This check belongs in the
|
|
5294
|
+
analyzer, where symbol resolution is scope-aware: a scalar parameter
|
|
5295
|
+
or block local can safely shadow a same-named global map, while typed
|
|
5296
|
+
map parameters and inferred aliases are still rejected.
|
|
5297
|
+
"""
|
|
5298
|
+
if spec is None:
|
|
5299
|
+
return False
|
|
5300
|
+
if spec.kind == "map":
|
|
5301
|
+
return True
|
|
5302
|
+
if spec.kind in {"array", "matrix"}:
|
|
5303
|
+
return self._type_spec_contains_map(spec.element, visiting_udts)
|
|
5304
|
+
if spec.kind == "udt" and spec.name:
|
|
5305
|
+
if spec.name in visiting_udts:
|
|
5306
|
+
return False
|
|
5307
|
+
nested_visiting = visiting_udts | {spec.name}
|
|
5308
|
+
return any(
|
|
5309
|
+
self._type_spec_contains_map(field_spec, nested_visiting)
|
|
5310
|
+
for field_spec in self._udt_field_type_specs.get(
|
|
5311
|
+
spec.name, {}
|
|
5312
|
+
).values()
|
|
5313
|
+
)
|
|
5314
|
+
return False
|
|
5315
|
+
|
|
5316
|
+
def _history_receiver_type_spec(
|
|
5317
|
+
self,
|
|
5318
|
+
node: ASTNode,
|
|
5319
|
+
parameter_specs_by_node: dict[int, TypeSpec | None] | None = None,
|
|
5320
|
+
) -> TypeSpec | None:
|
|
5321
|
+
"""Resolve the value shape on the left of Pine's history operator.
|
|
5322
|
+
|
|
5323
|
+
General expression inference intentionally stays conservative. The
|
|
5324
|
+
history safety gate needs two extra, narrow facts: a ternary selecting
|
|
5325
|
+
two equal aggregate handles keeps that aggregate type, and an untyped
|
|
5326
|
+
parameter can be substituted with the TypeSpec learned at its call
|
|
5327
|
+
site. The substitution is keyed by AST identifier identity so a
|
|
5328
|
+
same-spelled local or loop binder is never confused with the parameter.
|
|
5329
|
+
"""
|
|
5330
|
+
overrides = parameter_specs_by_node or {}
|
|
5331
|
+
if isinstance(node, Identifier):
|
|
5332
|
+
if id(node) in overrides:
|
|
5333
|
+
return overrides[id(node)]
|
|
5334
|
+
return self._type_spec_from_expr(node)
|
|
5335
|
+
if isinstance(node, Ternary):
|
|
5336
|
+
true_spec = self._history_receiver_type_spec(
|
|
5337
|
+
node.true_val, overrides
|
|
5338
|
+
)
|
|
5339
|
+
false_spec = self._history_receiver_type_spec(
|
|
5340
|
+
node.false_val, overrides
|
|
5341
|
+
)
|
|
5342
|
+
if true_spec is not None and true_spec == false_spec:
|
|
5343
|
+
return true_spec
|
|
5344
|
+
# ``na`` is context-typed by the opposite branch in Pine. Keep
|
|
5345
|
+
# this rule local to the history gate rather than broadening all
|
|
5346
|
+
# declaration inference.
|
|
5347
|
+
if isinstance(node.true_val, NaLiteral):
|
|
5348
|
+
return false_spec
|
|
5349
|
+
if isinstance(node.false_val, NaLiteral):
|
|
5350
|
+
return true_spec
|
|
5351
|
+
return None
|
|
5352
|
+
if isinstance(node, MemberAccess):
|
|
5353
|
+
owner = self._history_receiver_type_spec(node.object, overrides)
|
|
5354
|
+
if owner is not None and owner.kind == "udt" and owner.name:
|
|
5355
|
+
return (self._udt_field_type_specs.get(owner.name) or {}).get(
|
|
5356
|
+
node.member
|
|
5357
|
+
)
|
|
5358
|
+
return self._type_spec_from_expr(node)
|
|
5359
|
+
if isinstance(node, FuncCall) and isinstance(
|
|
5360
|
+
node.callee, MemberAccess
|
|
5361
|
+
):
|
|
5362
|
+
callee = node.callee
|
|
5363
|
+
receiver = None
|
|
5364
|
+
if (
|
|
5365
|
+
isinstance(callee.object, Identifier)
|
|
5366
|
+
and callee.object.name == "map"
|
|
5367
|
+
and node.args
|
|
5368
|
+
):
|
|
5369
|
+
receiver = node.args[0]
|
|
5370
|
+
elif not (
|
|
5371
|
+
isinstance(callee.object, Identifier)
|
|
5372
|
+
and callee.object.name in {
|
|
5373
|
+
"array", "matrix", "map", "request", "ta"
|
|
5374
|
+
}
|
|
5375
|
+
):
|
|
5376
|
+
receiver = callee.object
|
|
5377
|
+
if receiver is not None:
|
|
5378
|
+
recv_spec = self._history_receiver_type_spec(
|
|
5379
|
+
receiver, overrides
|
|
5380
|
+
)
|
|
5381
|
+
if recv_spec is not None and recv_spec.kind == "map":
|
|
5382
|
+
if callee.member == "copy":
|
|
5383
|
+
return recv_spec
|
|
5384
|
+
if callee.member in {"put", "get", "remove"}:
|
|
5385
|
+
return recv_spec.value
|
|
5386
|
+
if callee.member == "keys":
|
|
5387
|
+
return TypeSpec.array(
|
|
5388
|
+
recv_spec.key or TypeSpec.primitive("string")
|
|
5389
|
+
)
|
|
5390
|
+
if callee.member == "values":
|
|
5391
|
+
return TypeSpec.array(
|
|
5392
|
+
recv_spec.value or TypeSpec.primitive("float")
|
|
5393
|
+
)
|
|
5394
|
+
return self._type_spec_from_expr(node)
|
|
5395
|
+
|
|
5396
|
+
def _parameter_identifiers_in_expr(
|
|
5397
|
+
self, node: ASTNode
|
|
5398
|
+
) -> dict[str, dict[int, str]]:
|
|
5399
|
+
"""Return parameter identifier nodes grouped by their callable owner."""
|
|
5400
|
+
grouped: dict[str, dict[int, str]] = {}
|
|
5401
|
+
|
|
5402
|
+
def visit(value) -> None:
|
|
5403
|
+
if value is None:
|
|
5404
|
+
return
|
|
5405
|
+
if isinstance(value, Identifier):
|
|
5406
|
+
sym = self._symbols.resolve(value.name)
|
|
5407
|
+
owner = getattr(sym, "_pf_parameter_owner", None)
|
|
5408
|
+
if owner:
|
|
5409
|
+
grouped.setdefault(owner, {})[id(value)] = value.name
|
|
5410
|
+
return
|
|
5411
|
+
if isinstance(value, (list, tuple)):
|
|
5412
|
+
for item in value:
|
|
5413
|
+
visit(item)
|
|
5414
|
+
return
|
|
5415
|
+
if isinstance(value, dict):
|
|
5416
|
+
for item in value.values():
|
|
5417
|
+
visit(item)
|
|
5418
|
+
return
|
|
5419
|
+
if isinstance(value, ASTNode):
|
|
5420
|
+
for child in vars(value).values():
|
|
5421
|
+
visit(child)
|
|
5422
|
+
|
|
5423
|
+
visit(node)
|
|
5424
|
+
return grouped
|
|
5425
|
+
|
|
5426
|
+
@staticmethod
|
|
5427
|
+
def _bind_callable_args(
|
|
5428
|
+
node: FuncCall,
|
|
5429
|
+
param_names: list[str],
|
|
5430
|
+
) -> list[ASTNode | None]:
|
|
5431
|
+
"""Bind positional/keyword AST arguments into declaration order."""
|
|
5432
|
+
bound: list[ASTNode | None] = [None] * len(param_names)
|
|
5433
|
+
for index, arg in enumerate(node.args):
|
|
5434
|
+
if index < len(bound):
|
|
5435
|
+
bound[index] = arg
|
|
5436
|
+
for name, value in node.kwargs.items():
|
|
5437
|
+
if name in param_names:
|
|
5438
|
+
bound[param_names.index(name)] = value
|
|
5439
|
+
return bound
|
|
5440
|
+
|
|
5441
|
+
def _bind_typed_method_call(
|
|
5442
|
+
self,
|
|
5443
|
+
method_key: str,
|
|
5444
|
+
node: FuncCall,
|
|
5445
|
+
) -> BoundMethodArgs:
|
|
5446
|
+
"""Strict shared binding for one exact typed method call."""
|
|
5447
|
+
|
|
5448
|
+
cache_key = (id(node), method_key)
|
|
5449
|
+
cached = self._method_call_bindings.get(cache_key)
|
|
5450
|
+
if cached is not None:
|
|
5451
|
+
return cached
|
|
5452
|
+
signature = self._method_signatures[method_key]
|
|
5453
|
+
try:
|
|
5454
|
+
bound = bind_method_call(signature, node)
|
|
5455
|
+
except MethodBindError as exc:
|
|
5456
|
+
self._error(str(exc), node.loc)
|
|
5457
|
+
raise AssertionError("unreachable") from exc
|
|
5458
|
+
self._method_call_bindings[cache_key] = bound
|
|
5459
|
+
return bound
|
|
5460
|
+
|
|
5461
|
+
def _record_deferred_param_call_edge(
|
|
5462
|
+
self,
|
|
5463
|
+
call_node: FuncCall,
|
|
5464
|
+
callee_owner: str,
|
|
5465
|
+
callee_param_names: list[str],
|
|
5466
|
+
bound_args: list[ASTNode | None],
|
|
5467
|
+
) -> None:
|
|
5468
|
+
"""Capture caller-param flow while lexical symbol identity is known."""
|
|
5469
|
+
if not self._collection_scope_stack:
|
|
5470
|
+
return
|
|
5471
|
+
caller_owner = self._collection_scope_stack[-1]
|
|
5472
|
+
parameter_nodes: list[dict[int, str]] = []
|
|
5473
|
+
has_flow = False
|
|
5474
|
+
for arg in bound_args:
|
|
5475
|
+
grouped = (
|
|
5476
|
+
self._parameter_identifiers_in_expr(arg)
|
|
5477
|
+
if arg is not None
|
|
5478
|
+
else {}
|
|
5479
|
+
)
|
|
5480
|
+
current = grouped.get(caller_owner, {})
|
|
5481
|
+
parameter_nodes.append(current)
|
|
5482
|
+
has_flow = has_flow or bool(current)
|
|
5483
|
+
if not has_flow:
|
|
5484
|
+
return
|
|
5485
|
+
edges = self._deferred_param_call_edges.setdefault(caller_owner, [])
|
|
5486
|
+
if any(edge[0] == id(call_node) for edge in edges):
|
|
5487
|
+
return
|
|
5488
|
+
edges.append(
|
|
5489
|
+
(
|
|
5490
|
+
id(call_node),
|
|
5491
|
+
callee_owner,
|
|
5492
|
+
list(callee_param_names),
|
|
5493
|
+
list(bound_args),
|
|
5494
|
+
parameter_nodes,
|
|
5495
|
+
)
|
|
5496
|
+
)
|
|
5497
|
+
|
|
5498
|
+
def _reject_unsupported_map_history(
|
|
5499
|
+
self, spec: TypeSpec, node: Subscript
|
|
5500
|
+
) -> None:
|
|
5501
|
+
if spec.kind == "map":
|
|
5502
|
+
message = (
|
|
5503
|
+
"History references on map IDs are not supported in "
|
|
5504
|
+
"PineForge; map rollback uses identity snapshots rather "
|
|
5505
|
+
"than Series<PineMap>."
|
|
5506
|
+
)
|
|
5507
|
+
else:
|
|
5508
|
+
message = (
|
|
5509
|
+
"History references on map-bearing UDTs or collections "
|
|
5510
|
+
"are not supported in PineForge; their Series value copy "
|
|
5511
|
+
"would retain live map aliases."
|
|
5512
|
+
)
|
|
5513
|
+
self._error(message, node.loc)
|
|
5514
|
+
|
|
5515
|
+
def _validate_deferred_param_history_refs(
|
|
5516
|
+
self,
|
|
5517
|
+
owner: str,
|
|
5518
|
+
parameter_specs: dict[str, TypeSpec | None],
|
|
5519
|
+
visiting: frozenset[str] = frozenset(),
|
|
5520
|
+
) -> None:
|
|
5521
|
+
"""Re-run map-history safety after untyped callable args are known."""
|
|
5522
|
+
if owner in visiting:
|
|
5523
|
+
return
|
|
5524
|
+
next_visiting = visiting | {owner}
|
|
5525
|
+
for node, parameter_nodes in self._deferred_param_history_refs.get(
|
|
5526
|
+
owner, []
|
|
5527
|
+
):
|
|
5528
|
+
overrides = {
|
|
5529
|
+
node_id: parameter_specs.get(name)
|
|
5530
|
+
for node_id, name in parameter_nodes.items()
|
|
5531
|
+
}
|
|
5532
|
+
object_spec = self._history_receiver_type_spec(
|
|
5533
|
+
node.object, overrides
|
|
5534
|
+
)
|
|
5535
|
+
if self._type_spec_contains_map(object_spec):
|
|
5536
|
+
assert object_spec is not None
|
|
5537
|
+
self._reject_unsupported_map_history(object_spec, node)
|
|
5538
|
+
|
|
5539
|
+
# Propagate concrete caller specs through wrapper calls. Each edge is
|
|
5540
|
+
# identity-keyed from the definition pass, so a local that shadows a
|
|
5541
|
+
# caller parameter cannot accidentally inherit its map TypeSpec.
|
|
5542
|
+
for (
|
|
5543
|
+
_call_id,
|
|
5544
|
+
callee_owner,
|
|
5545
|
+
callee_param_names,
|
|
5546
|
+
bound_args,
|
|
5547
|
+
parameter_nodes_by_arg,
|
|
5548
|
+
) in self._deferred_param_call_edges.get(owner, []):
|
|
5549
|
+
callee_specs: dict[str, TypeSpec | None] = {}
|
|
5550
|
+
for index, param_name in enumerate(callee_param_names):
|
|
5551
|
+
arg = bound_args[index] if index < len(bound_args) else None
|
|
5552
|
+
if arg is None:
|
|
5553
|
+
callee_specs[param_name] = None
|
|
5554
|
+
continue
|
|
5555
|
+
parameter_nodes = (
|
|
5556
|
+
parameter_nodes_by_arg[index]
|
|
5557
|
+
if index < len(parameter_nodes_by_arg)
|
|
5558
|
+
else {}
|
|
5559
|
+
)
|
|
5560
|
+
overrides = {
|
|
5561
|
+
node_id: parameter_specs.get(name)
|
|
5562
|
+
for node_id, name in parameter_nodes.items()
|
|
5563
|
+
}
|
|
5564
|
+
callee_specs[param_name] = self._history_receiver_type_spec(
|
|
5565
|
+
arg, overrides
|
|
5566
|
+
)
|
|
5567
|
+
self._validate_deferred_param_history_refs(
|
|
5568
|
+
callee_owner,
|
|
5569
|
+
callee_specs,
|
|
5570
|
+
next_visiting,
|
|
5571
|
+
)
|
|
5572
|
+
|
|
5573
|
+
def _propagate_deferred_map_callable_specs(
|
|
5574
|
+
self,
|
|
5575
|
+
owner: str,
|
|
5576
|
+
parameter_specs: dict[str, TypeSpec | None],
|
|
5577
|
+
visiting: frozenset[str] = frozenset(),
|
|
5578
|
+
) -> bool:
|
|
5579
|
+
"""Monomorphize an all-untyped wrapper chain for one concrete map call.
|
|
5580
|
+
|
|
5581
|
+
Function definitions are analyzed before their concrete top-level call
|
|
5582
|
+
sites. Consequently ``wrapper(a) => identity(a)`` initially creates
|
|
5583
|
+
both ``FuncInfo`` records with scalar fallbacks. The definition pass
|
|
5584
|
+
already captured identity-keyed parameter-flow edges for deferred map
|
|
5585
|
+
history validation; reuse those exact edges to carry a later map
|
|
5586
|
+
``TypeSpec`` inward, then infer map returns on the way back out.
|
|
5587
|
+
|
|
5588
|
+
This is deliberately bounded and map-triggered. Cycles stop at the
|
|
5589
|
+
active owner, incompatible previously-established parameter specs stop
|
|
5590
|
+
that edge, and scalar-only call graphs never mutate. PineForge emits
|
|
5591
|
+
one C++ body per ordinary UDF, so silently replacing one concrete map
|
|
5592
|
+
specialization with a different one would be a false polymorphic
|
|
5593
|
+
inference rather than a valid widening.
|
|
5594
|
+
"""
|
|
5595
|
+
if owner in visiting:
|
|
5596
|
+
return False
|
|
5597
|
+
if not any(
|
|
5598
|
+
spec is not None and spec.kind == "map"
|
|
5599
|
+
for spec in parameter_specs.values()
|
|
5600
|
+
):
|
|
5601
|
+
return False
|
|
5602
|
+
|
|
5603
|
+
func_info = next(
|
|
5604
|
+
(info for info in self._func_infos if info.name == owner),
|
|
5605
|
+
None,
|
|
5606
|
+
)
|
|
5607
|
+
func_def = self._func_defs.get(owner)
|
|
5608
|
+
if func_info is None or func_def is None:
|
|
5609
|
+
return False
|
|
5610
|
+
|
|
5611
|
+
changed = False
|
|
5612
|
+
while len(func_info.param_type_specs) < len(func_def.params):
|
|
5613
|
+
func_info.param_type_specs.append(None)
|
|
5614
|
+
declared_specs = list(
|
|
5615
|
+
self._func_param_type_specs.get(owner)
|
|
5616
|
+
or self._param_type_specs_from_def(func_def)
|
|
5617
|
+
)
|
|
5618
|
+
while len(declared_specs) < len(func_def.params):
|
|
5619
|
+
declared_specs.append(None)
|
|
5620
|
+
|
|
5621
|
+
# Refuse to overwrite a declared or previously learned concrete map
|
|
5622
|
+
# specialization. Equal specs and still-unresolved slots are safe.
|
|
5623
|
+
for index, param_name in enumerate(func_def.params):
|
|
5624
|
+
incoming = parameter_specs.get(param_name)
|
|
5625
|
+
if incoming is None:
|
|
5626
|
+
continue
|
|
5627
|
+
established = declared_specs[index] or func_info.param_type_specs[index]
|
|
5628
|
+
if (
|
|
5629
|
+
established is not None
|
|
5630
|
+
and established != incoming
|
|
5631
|
+
and (established.kind == "map" or incoming.kind == "map")
|
|
5632
|
+
):
|
|
5633
|
+
self._error(
|
|
5634
|
+
f"User function '{owner}' is called with incompatible "
|
|
5635
|
+
"map parameter types; PineForge cannot emit multiple "
|
|
5636
|
+
"map specializations for one untyped function.",
|
|
5637
|
+
func_def.loc,
|
|
5638
|
+
)
|
|
5639
|
+
for index, param_name in enumerate(func_def.params):
|
|
5640
|
+
incoming = parameter_specs.get(param_name)
|
|
5641
|
+
if incoming is None or func_info.param_type_specs[index] is not None:
|
|
5642
|
+
continue
|
|
5643
|
+
func_info.param_type_specs[index] = incoming
|
|
5644
|
+
changed = True
|
|
5645
|
+
|
|
5646
|
+
next_visiting = visiting | {owner}
|
|
5647
|
+
edges = self._deferred_param_call_edges.get(owner, [])
|
|
5648
|
+
# Nested actual arguments can depend on a sibling edge's newly learned
|
|
5649
|
+
# return spec. A bounded local fixed point removes source-order
|
|
5650
|
+
# dependence without turning this into whole-program re-analysis.
|
|
5651
|
+
for _ in range(max(1, len(edges) + 1)):
|
|
5652
|
+
pass_changed = False
|
|
5653
|
+
for (
|
|
5654
|
+
_call_id,
|
|
5655
|
+
callee_owner,
|
|
5656
|
+
callee_param_names,
|
|
5657
|
+
bound_args,
|
|
5658
|
+
parameter_nodes_by_arg,
|
|
5659
|
+
) in edges:
|
|
5660
|
+
callee_specs: dict[str, TypeSpec | None] = {}
|
|
5661
|
+
for index, param_name in enumerate(callee_param_names):
|
|
5662
|
+
arg = bound_args[index] if index < len(bound_args) else None
|
|
5663
|
+
if arg is None:
|
|
5664
|
+
callee_specs[param_name] = None
|
|
5665
|
+
continue
|
|
5666
|
+
parameter_nodes = (
|
|
5667
|
+
parameter_nodes_by_arg[index]
|
|
5668
|
+
if index < len(parameter_nodes_by_arg)
|
|
5669
|
+
else {}
|
|
5670
|
+
)
|
|
5671
|
+
overrides = {
|
|
5672
|
+
node_id: parameter_specs.get(name)
|
|
5673
|
+
for node_id, name in parameter_nodes.items()
|
|
5674
|
+
}
|
|
5675
|
+
callee_specs[param_name] = self._history_receiver_type_spec(
|
|
5676
|
+
arg, overrides
|
|
5677
|
+
)
|
|
5678
|
+
# A bare ``na`` actual argument acquires the one unambiguous
|
|
5679
|
+
# map type carried by its sibling arguments. This is needed
|
|
5680
|
+
# for wrappers such as ``select(c, m) => choose(c, m, na)``;
|
|
5681
|
+
# without it the inner untyped parameter remains scalar even
|
|
5682
|
+
# though Pine context-types the na handle. Multiple distinct
|
|
5683
|
+
# map specs stay unresolved rather than guessing.
|
|
5684
|
+
concrete_map_specs = {
|
|
5685
|
+
spec
|
|
5686
|
+
for spec in callee_specs.values()
|
|
5687
|
+
if spec is not None and spec.kind == "map"
|
|
5688
|
+
}
|
|
5689
|
+
if len(concrete_map_specs) == 1:
|
|
5690
|
+
contextual_map_spec = next(iter(concrete_map_specs))
|
|
5691
|
+
for index, param_name in enumerate(callee_param_names):
|
|
5692
|
+
arg = (
|
|
5693
|
+
bound_args[index]
|
|
5694
|
+
if index < len(bound_args)
|
|
5695
|
+
else None
|
|
5696
|
+
)
|
|
5697
|
+
if (
|
|
5698
|
+
callee_specs.get(param_name) is None
|
|
5699
|
+
and (
|
|
5700
|
+
isinstance(arg, NaLiteral)
|
|
5701
|
+
or (
|
|
5702
|
+
isinstance(arg, Identifier)
|
|
5703
|
+
and arg.name == "na"
|
|
5704
|
+
)
|
|
5705
|
+
)
|
|
5706
|
+
):
|
|
5707
|
+
callee_specs[param_name] = contextual_map_spec
|
|
5708
|
+
if self._propagate_deferred_map_callable_specs(
|
|
5709
|
+
callee_owner,
|
|
5710
|
+
callee_specs,
|
|
5711
|
+
next_visiting,
|
|
5712
|
+
):
|
|
5713
|
+
pass_changed = True
|
|
5714
|
+
changed = changed or pass_changed
|
|
5715
|
+
if not pass_changed:
|
|
5716
|
+
break
|
|
5717
|
+
|
|
5718
|
+
terminal = self._direct_terminal_return_expr(func_def)
|
|
5719
|
+
return_spec = None
|
|
5720
|
+
scalar_return_type = None
|
|
5721
|
+
if isinstance(terminal, Identifier) and terminal.name in parameter_specs:
|
|
5722
|
+
candidate = parameter_specs.get(terminal.name)
|
|
5723
|
+
if candidate is not None and candidate.kind == "map":
|
|
5724
|
+
return_spec = candidate
|
|
5725
|
+
if return_spec is None:
|
|
5726
|
+
return_spec = self._terminal_map_selection_return_spec(
|
|
5727
|
+
terminal, parameter_specs
|
|
5728
|
+
)
|
|
5729
|
+
if return_spec is None:
|
|
5730
|
+
terminal_map_call = self._terminal_map_call_return(
|
|
5731
|
+
terminal, parameter_specs
|
|
5732
|
+
)
|
|
5733
|
+
if terminal_map_call is not None:
|
|
5734
|
+
terminal_return_type, candidate = terminal_map_call
|
|
5735
|
+
if candidate is not None and candidate.kind == "map":
|
|
5736
|
+
return_spec = candidate
|
|
5737
|
+
elif terminal_return_type not in {
|
|
5738
|
+
PineType.UNKNOWN, PineType.VOID
|
|
5739
|
+
}:
|
|
5740
|
+
scalar_return_type = terminal_return_type
|
|
5741
|
+
if return_spec is None:
|
|
5742
|
+
candidate = self._type_spec_from_expr(terminal)
|
|
5743
|
+
if candidate is not None and candidate.kind == "map":
|
|
5744
|
+
return_spec = candidate
|
|
5745
|
+
if return_spec is not None:
|
|
5746
|
+
established_return = self._func_return_type_specs.get(owner)
|
|
5747
|
+
if established_return is None:
|
|
5748
|
+
self._func_return_type_specs[owner] = return_spec
|
|
5749
|
+
func_info.return_type_spec = return_spec
|
|
5750
|
+
changed = True
|
|
5751
|
+
elif established_return == return_spec:
|
|
5752
|
+
if func_info.return_type_spec is None:
|
|
5753
|
+
func_info.return_type_spec = return_spec
|
|
5754
|
+
changed = True
|
|
5755
|
+
# A different established return belongs to another concrete map
|
|
5756
|
+
# specialization. Keep it unchanged rather than falsely widening.
|
|
5757
|
+
|
|
5758
|
+
if scalar_return_type is None and isinstance(terminal, FuncCall):
|
|
5759
|
+
callee = terminal.callee
|
|
5760
|
+
callee_name = (
|
|
5761
|
+
callee.name if isinstance(callee, Identifier) else None
|
|
5762
|
+
)
|
|
5763
|
+
callee_info = next(
|
|
5764
|
+
(
|
|
5765
|
+
info
|
|
5766
|
+
for info in self._func_infos
|
|
5767
|
+
if info.name == callee_name
|
|
5768
|
+
),
|
|
5769
|
+
None,
|
|
5770
|
+
)
|
|
5771
|
+
if (
|
|
5772
|
+
callee_info is not None
|
|
5773
|
+
and callee_info.return_type
|
|
5774
|
+
not in {PineType.UNKNOWN, PineType.VOID}
|
|
5775
|
+
):
|
|
5776
|
+
scalar_return_type = callee_info.return_type
|
|
5777
|
+
if (
|
|
5778
|
+
return_spec is None
|
|
5779
|
+
and scalar_return_type is not None
|
|
5780
|
+
and func_info.return_type != scalar_return_type
|
|
5781
|
+
):
|
|
5782
|
+
func_info.return_type = scalar_return_type
|
|
5783
|
+
self._func_return_types[owner] = scalar_return_type
|
|
5784
|
+
changed = True
|
|
5785
|
+
|
|
5786
|
+
return changed
|
|
5787
|
+
|
|
1993
5788
|
def _visit_Subscript(self, node: Subscript) -> PineType:
|
|
1994
5789
|
obj_type = self._visit(node.object)
|
|
1995
5790
|
self._visit(node.index)
|
|
1996
5791
|
|
|
5792
|
+
object_spec = self._history_receiver_type_spec(node.object)
|
|
5793
|
+
if self._type_spec_contains_map(object_spec):
|
|
5794
|
+
assert object_spec is not None
|
|
5795
|
+
self._reject_unsupported_map_history(object_spec, node)
|
|
5796
|
+
|
|
5797
|
+
# An untyped parameter has no aggregate TypeSpec during the function
|
|
5798
|
+
# definition pass. Remember only the identifier nodes that resolved
|
|
5799
|
+
# to actual parameters; the call handler validates them before any
|
|
5800
|
+
# codegen state is committed.
|
|
5801
|
+
for owner, parameter_nodes in self._parameter_identifiers_in_expr(
|
|
5802
|
+
node.object
|
|
5803
|
+
).items():
|
|
5804
|
+
self._deferred_param_history_refs.setdefault(owner, []).append(
|
|
5805
|
+
(node, parameter_nodes)
|
|
5806
|
+
)
|
|
5807
|
+
|
|
1997
5808
|
# Detect series vars / bar fields
|
|
1998
5809
|
if isinstance(node.object, Identifier):
|
|
1999
5810
|
name = node.object.name
|
|
@@ -2002,15 +5813,59 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2002
5813
|
else:
|
|
2003
5814
|
sym = self._symbols.resolve(name)
|
|
2004
5815
|
if sym is not None:
|
|
5816
|
+
if getattr(sym, "_pf_shadows_map_state", False):
|
|
5817
|
+
self._error(
|
|
5818
|
+
"History references on scalar local or loop "
|
|
5819
|
+
"bindings that shadow a map ID are not supported "
|
|
5820
|
+
"until PineForge can allocate a lexically scoped "
|
|
5821
|
+
"Series buffer for that binding.",
|
|
5822
|
+
node.loc,
|
|
5823
|
+
)
|
|
2005
5824
|
if getattr(sym, "type_spec", None) is None or sym.type_spec.kind not in ("array", "map"):
|
|
2006
|
-
|
|
5825
|
+
exact_member = getattr(sym, "_pf_var_member_name", None)
|
|
5826
|
+
if exact_member is not None:
|
|
5827
|
+
self._series_var_members.add(exact_member)
|
|
5828
|
+
decl_node_id = getattr(sym, "_pf_decl_node_id", None)
|
|
5829
|
+
if decl_node_id is not None:
|
|
5830
|
+
self._series_decl_nodes.add(decl_node_id)
|
|
5831
|
+
binding_name = getattr(
|
|
5832
|
+
sym, "_pf_decl_binding_name", name
|
|
5833
|
+
)
|
|
5834
|
+
self._series_decl_bindings.add(
|
|
5835
|
+
(decl_node_id, binding_name)
|
|
5836
|
+
)
|
|
5837
|
+
global_sym = self._symbols.global_scope.symbols.get(name)
|
|
5838
|
+
shadows_map_state = (
|
|
5839
|
+
sym.scope != "global"
|
|
5840
|
+
and global_sym is not None
|
|
5841
|
+
and self._type_spec_contains_map(global_sym.type_spec)
|
|
5842
|
+
)
|
|
5843
|
+
# ``_series_vars`` is a legacy global-by-name set. Do
|
|
5844
|
+
# not let a lexical scalar series parameter poison a
|
|
5845
|
+
# same-named global PineMap member; the scoped series
|
|
5846
|
+
# registry below is authoritative for the function.
|
|
5847
|
+
if not shadows_map_state:
|
|
5848
|
+
self._series_vars.add(name)
|
|
2007
5849
|
sym.is_series = True
|
|
2008
|
-
# Track
|
|
5850
|
+
# Track callable-scoped series vars. Method symbol
|
|
5851
|
+
# scopes use ``method_Type_name`` while every later
|
|
5852
|
+
# clone/remap table is keyed by canonical
|
|
5853
|
+
# ``Type.method`` identity.
|
|
5854
|
+
func_name: str | None = None
|
|
2009
5855
|
if sym.scope and sym.scope.startswith("func_"):
|
|
2010
5856
|
func_name = sym.scope[5:]
|
|
5857
|
+
elif (
|
|
5858
|
+
sym.scope != "global"
|
|
5859
|
+
and self._collection_scope_stack
|
|
5860
|
+
):
|
|
5861
|
+
func_name = self._collection_scope_stack[-1]
|
|
5862
|
+
if func_name is not None:
|
|
2011
5863
|
if func_name not in self._func_series_vars:
|
|
2012
5864
|
self._func_series_vars[func_name] = set()
|
|
2013
5865
|
self._func_series_vars[func_name].add(name)
|
|
5866
|
+
self._func_series_history_nodes.setdefault(
|
|
5867
|
+
(func_name, name), node
|
|
5868
|
+
)
|
|
2014
5869
|
|
|
2015
5870
|
return obj_type
|
|
2016
5871
|
|
|
@@ -2029,6 +5884,9 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2029
5884
|
return PineType.VOID
|
|
2030
5885
|
|
|
2031
5886
|
sym = self._symbols.resolve(node.name)
|
|
5887
|
+
self._identifier_binding_scopes[id(node)] = (
|
|
5888
|
+
getattr(sym, "scope", None) if sym is not None else None
|
|
5889
|
+
)
|
|
2032
5890
|
if sym is not None:
|
|
2033
5891
|
return sym.pine_type
|
|
2034
5892
|
|
|
@@ -2313,6 +6171,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
2313
6171
|
return PineType.COLOR
|
|
2314
6172
|
|
|
2315
6173
|
def _visit_TupleLiteral(self, node: TupleLiteral) -> PineType:
|
|
2316
|
-
|
|
2317
|
-
self._visit(elem)
|
|
6174
|
+
self._tuple_element_types_by_node[id(node)] = tuple(
|
|
6175
|
+
self._visit(elem) for elem in node.elements
|
|
6176
|
+
)
|
|
2318
6177
|
return PineType.FLOAT
|