@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
|
@@ -66,8 +66,9 @@ from __future__ import annotations
|
|
|
66
66
|
from typing import Any
|
|
67
67
|
|
|
68
68
|
from ..ast_nodes import (
|
|
69
|
-
ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier,
|
|
70
|
-
|
|
69
|
+
ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier,
|
|
70
|
+
IfStmt, MemberAccess, NumberLiteral, StringLiteral, Subscript, SwitchStmt,
|
|
71
|
+
Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
71
72
|
)
|
|
72
73
|
from ..symbols import PineType
|
|
73
74
|
from .. import signatures as sigs
|
|
@@ -89,6 +90,278 @@ class CallHandlers:
|
|
|
89
90
|
# TA call handling
|
|
90
91
|
# ------------------------------------------------------------------
|
|
91
92
|
|
|
93
|
+
def _callsite_primitive_expr_type(
|
|
94
|
+
self,
|
|
95
|
+
expr: ASTNode | None,
|
|
96
|
+
parameter_types: dict[str, PineType],
|
|
97
|
+
) -> PineType:
|
|
98
|
+
"""Infer a primitive return using one written call's parameter types.
|
|
99
|
+
|
|
100
|
+
The analyzer's legacy ``FuncInfo`` is definition-wide, but Pine's
|
|
101
|
+
untyped parameters are polymorphic per written call. Keep this helper
|
|
102
|
+
intentionally primitive and expression-local: it provides the exact
|
|
103
|
+
family needed by history-preserving identity/wrapper functions without
|
|
104
|
+
attempting to monomorphize collections or reference types here.
|
|
105
|
+
"""
|
|
106
|
+
if expr is None:
|
|
107
|
+
return PineType.UNKNOWN
|
|
108
|
+
if isinstance(expr, ExprStmt):
|
|
109
|
+
return self._callsite_primitive_expr_type(
|
|
110
|
+
expr.expr, parameter_types
|
|
111
|
+
)
|
|
112
|
+
if isinstance(expr, Identifier):
|
|
113
|
+
if expr.name in parameter_types:
|
|
114
|
+
return parameter_types[expr.name]
|
|
115
|
+
if expr.name in {"true", "false"}:
|
|
116
|
+
return PineType.BOOL
|
|
117
|
+
return PineType.UNKNOWN
|
|
118
|
+
if isinstance(expr, Subscript):
|
|
119
|
+
# Pine's history operator preserves the receiver's scalar family.
|
|
120
|
+
return self._callsite_primitive_expr_type(
|
|
121
|
+
expr.object, parameter_types
|
|
122
|
+
)
|
|
123
|
+
if isinstance(expr, NumberLiteral):
|
|
124
|
+
return (
|
|
125
|
+
PineType.FLOAT
|
|
126
|
+
if isinstance(expr.value, float)
|
|
127
|
+
else PineType.INT
|
|
128
|
+
)
|
|
129
|
+
if isinstance(expr, BoolLiteral):
|
|
130
|
+
return PineType.BOOL
|
|
131
|
+
if isinstance(expr, StringLiteral):
|
|
132
|
+
return PineType.STRING
|
|
133
|
+
if isinstance(expr, UnaryOp):
|
|
134
|
+
if expr.op == "not":
|
|
135
|
+
return PineType.BOOL
|
|
136
|
+
return self._callsite_primitive_expr_type(
|
|
137
|
+
expr.operand, parameter_types
|
|
138
|
+
)
|
|
139
|
+
if isinstance(expr, BinOp):
|
|
140
|
+
left = self._callsite_primitive_expr_type(
|
|
141
|
+
expr.left, parameter_types
|
|
142
|
+
)
|
|
143
|
+
right = self._callsite_primitive_expr_type(
|
|
144
|
+
expr.right, parameter_types
|
|
145
|
+
)
|
|
146
|
+
if expr.op in {"==", "!=", ">", "<", ">=", "<=", "and", "or"}:
|
|
147
|
+
return PineType.BOOL
|
|
148
|
+
if left == PineType.STRING or right == PineType.STRING:
|
|
149
|
+
return PineType.STRING
|
|
150
|
+
if expr.op == "/" or PineType.FLOAT in {left, right}:
|
|
151
|
+
return PineType.FLOAT
|
|
152
|
+
if left == PineType.INT and right == PineType.INT:
|
|
153
|
+
return PineType.INT
|
|
154
|
+
return PineType.UNKNOWN
|
|
155
|
+
if isinstance(expr, Ternary):
|
|
156
|
+
true_type = self._callsite_primitive_expr_type(
|
|
157
|
+
expr.true_val, parameter_types
|
|
158
|
+
)
|
|
159
|
+
false_type = self._callsite_primitive_expr_type(
|
|
160
|
+
expr.false_val, parameter_types
|
|
161
|
+
)
|
|
162
|
+
if true_type == false_type:
|
|
163
|
+
return true_type
|
|
164
|
+
if PineType.STRING in {true_type, false_type}:
|
|
165
|
+
return PineType.STRING
|
|
166
|
+
if PineType.FLOAT in {true_type, false_type}:
|
|
167
|
+
return PineType.FLOAT
|
|
168
|
+
return PineType.UNKNOWN
|
|
169
|
+
if isinstance(expr, FuncCall):
|
|
170
|
+
callee = expr.callee
|
|
171
|
+
if isinstance(callee, Identifier):
|
|
172
|
+
if callee.name == "int":
|
|
173
|
+
return PineType.INT
|
|
174
|
+
if callee.name == "float":
|
|
175
|
+
return PineType.FLOAT
|
|
176
|
+
if callee.name == "bool":
|
|
177
|
+
return PineType.BOOL
|
|
178
|
+
# A user callable's definition-wide return cache is not a
|
|
179
|
+
# call-site fact. In particular, a pure untyped transform
|
|
180
|
+
# nested inside a polymorphic history wrapper may have been
|
|
181
|
+
# analyzed first as FLOAT even when this path carries an
|
|
182
|
+
# int64 timestamp or bool. Exact direct-wrapper call edges are
|
|
183
|
+
# reconciled later; arbitrary nested transforms stay UNKNOWN
|
|
184
|
+
# and therefore fail closed instead of source-order coercing.
|
|
185
|
+
return PineType.UNKNOWN
|
|
186
|
+
return PineType.UNKNOWN
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _join_callsite_primitive_types(
|
|
190
|
+
types: list[PineType],
|
|
191
|
+
) -> PineType:
|
|
192
|
+
if not types or PineType.UNKNOWN in types:
|
|
193
|
+
return PineType.UNKNOWN
|
|
194
|
+
first = types[0]
|
|
195
|
+
if all(value == first for value in types):
|
|
196
|
+
return first
|
|
197
|
+
if set(types).issubset({PineType.INT, PineType.FLOAT}):
|
|
198
|
+
return PineType.FLOAT
|
|
199
|
+
return PineType.UNKNOWN
|
|
200
|
+
|
|
201
|
+
def _apply_callsite_local_type_effects(
|
|
202
|
+
self,
|
|
203
|
+
stmt: ASTNode,
|
|
204
|
+
local_types: dict[str, PineType],
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Flow one statement's primitive local writes into ``local_types``.
|
|
207
|
+
|
|
208
|
+
This is deliberately a small callable-return analysis, not a second
|
|
209
|
+
semantic analyzer. It exists so a terminal alias such as
|
|
210
|
+
``prior = src[1]; prior`` keeps the exact written-call family. Unknown
|
|
211
|
+
loops/collections remain unknown and cannot silently pick FLOAT.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(stmt, VarDecl):
|
|
214
|
+
hinted = {
|
|
215
|
+
"int": PineType.INT,
|
|
216
|
+
"float": PineType.FLOAT,
|
|
217
|
+
"bool": PineType.BOOL,
|
|
218
|
+
"string": PineType.STRING,
|
|
219
|
+
"color": PineType.COLOR,
|
|
220
|
+
}.get(stmt.type_hint or "", PineType.UNKNOWN)
|
|
221
|
+
local_types[stmt.name] = (
|
|
222
|
+
hinted
|
|
223
|
+
if hinted != PineType.UNKNOWN
|
|
224
|
+
else self._callsite_primitive_expr_type(
|
|
225
|
+
stmt.value, local_types
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return
|
|
229
|
+
if (
|
|
230
|
+
isinstance(stmt, Assignment)
|
|
231
|
+
and isinstance(stmt.target, Identifier)
|
|
232
|
+
):
|
|
233
|
+
rhs = self._callsite_primitive_expr_type(
|
|
234
|
+
stmt.value, local_types
|
|
235
|
+
)
|
|
236
|
+
if stmt.op == ":=":
|
|
237
|
+
# Pine variables retain their declaration/inferred primitive
|
|
238
|
+
# family for their lifetime. An ``na``/otherwise unknown
|
|
239
|
+
# declaration may acquire the first concrete family here.
|
|
240
|
+
current = local_types.get(
|
|
241
|
+
stmt.target.name, PineType.UNKNOWN
|
|
242
|
+
)
|
|
243
|
+
local_types[stmt.target.name] = (
|
|
244
|
+
current if current != PineType.UNKNOWN else rhs
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
local_types[stmt.target.name] = (
|
|
248
|
+
self._join_callsite_primitive_types(
|
|
249
|
+
[
|
|
250
|
+
local_types.get(
|
|
251
|
+
stmt.target.name, PineType.UNKNOWN
|
|
252
|
+
),
|
|
253
|
+
rhs,
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
return
|
|
258
|
+
if isinstance(stmt, IfStmt):
|
|
259
|
+
before = dict(local_types)
|
|
260
|
+
branch_envs: list[dict[str, PineType]] = []
|
|
261
|
+
for body in (stmt.body, stmt.else_body):
|
|
262
|
+
branch = dict(before)
|
|
263
|
+
for child in body:
|
|
264
|
+
self._apply_callsite_local_type_effects(child, branch)
|
|
265
|
+
branch_envs.append(branch)
|
|
266
|
+
all_names = set().union(*(env.keys() for env in branch_envs))
|
|
267
|
+
for name in all_names:
|
|
268
|
+
local_types[name] = self._join_callsite_primitive_types(
|
|
269
|
+
[
|
|
270
|
+
env.get(name, before.get(name, PineType.UNKNOWN))
|
|
271
|
+
for env in branch_envs
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
if isinstance(stmt, SwitchStmt):
|
|
276
|
+
before = dict(local_types)
|
|
277
|
+
bodies = [body for _, body in stmt.cases]
|
|
278
|
+
if stmt.default_body:
|
|
279
|
+
bodies.append(stmt.default_body)
|
|
280
|
+
else:
|
|
281
|
+
bodies.append([])
|
|
282
|
+
branch_envs = []
|
|
283
|
+
for body in bodies:
|
|
284
|
+
branch = dict(before)
|
|
285
|
+
for child in body:
|
|
286
|
+
self._apply_callsite_local_type_effects(child, branch)
|
|
287
|
+
branch_envs.append(branch)
|
|
288
|
+
all_names = set().union(*(env.keys() for env in branch_envs))
|
|
289
|
+
for name in all_names:
|
|
290
|
+
local_types[name] = self._join_callsite_primitive_types(
|
|
291
|
+
[
|
|
292
|
+
env.get(name, before.get(name, PineType.UNKNOWN))
|
|
293
|
+
for env in branch_envs
|
|
294
|
+
]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _callsite_body_return_type(
|
|
298
|
+
self,
|
|
299
|
+
body: list[ASTNode],
|
|
300
|
+
parameter_types: dict[str, PineType],
|
|
301
|
+
) -> PineType:
|
|
302
|
+
if not body:
|
|
303
|
+
return PineType.UNKNOWN
|
|
304
|
+
local_types = dict(parameter_types)
|
|
305
|
+
for stmt in body[:-1]:
|
|
306
|
+
self._apply_callsite_local_type_effects(stmt, local_types)
|
|
307
|
+
terminal = body[-1]
|
|
308
|
+
if isinstance(terminal, ExprStmt):
|
|
309
|
+
terminal = terminal.expr
|
|
310
|
+
if isinstance(terminal, VarDecl):
|
|
311
|
+
return self._callsite_primitive_expr_type(
|
|
312
|
+
terminal.value, local_types
|
|
313
|
+
)
|
|
314
|
+
if isinstance(terminal, IfStmt):
|
|
315
|
+
if not terminal.body:
|
|
316
|
+
return PineType.UNKNOWN
|
|
317
|
+
branches = [terminal.body]
|
|
318
|
+
# A missing else produces contextual ``na`` in Pine and therefore
|
|
319
|
+
# does not erase the concrete family's type.
|
|
320
|
+
if terminal.else_body:
|
|
321
|
+
branches.append(terminal.else_body)
|
|
322
|
+
return self._join_callsite_primitive_types(
|
|
323
|
+
[
|
|
324
|
+
self._callsite_body_return_type(
|
|
325
|
+
branch, dict(local_types)
|
|
326
|
+
)
|
|
327
|
+
for branch in branches
|
|
328
|
+
]
|
|
329
|
+
)
|
|
330
|
+
if isinstance(terminal, SwitchStmt):
|
|
331
|
+
bodies = [case_body for _, case_body in terminal.cases]
|
|
332
|
+
if terminal.default_body:
|
|
333
|
+
bodies.append(terminal.default_body)
|
|
334
|
+
if not bodies:
|
|
335
|
+
return PineType.UNKNOWN
|
|
336
|
+
return self._join_callsite_primitive_types(
|
|
337
|
+
[
|
|
338
|
+
self._callsite_body_return_type(
|
|
339
|
+
branch, dict(local_types)
|
|
340
|
+
)
|
|
341
|
+
for branch in bodies
|
|
342
|
+
]
|
|
343
|
+
)
|
|
344
|
+
return self._callsite_primitive_expr_type(terminal, local_types)
|
|
345
|
+
|
|
346
|
+
def _callsite_callable_return_type(
|
|
347
|
+
self,
|
|
348
|
+
func_def,
|
|
349
|
+
param_types: list[PineType],
|
|
350
|
+
fallback: PineType,
|
|
351
|
+
) -> PineType:
|
|
352
|
+
inferred = self._callsite_body_return_type(
|
|
353
|
+
func_def.body,
|
|
354
|
+
{
|
|
355
|
+
name: (
|
|
356
|
+
param_types[index]
|
|
357
|
+
if index < len(param_types)
|
|
358
|
+
else PineType.UNKNOWN
|
|
359
|
+
)
|
|
360
|
+
for index, name in enumerate(func_def.params)
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
return inferred if inferred != PineType.UNKNOWN else fallback
|
|
364
|
+
|
|
92
365
|
def _merge_ta_args(self, func_name: str, node: FuncCall) -> list:
|
|
93
366
|
"""Merge positional args and kwargs into a unified positional list."""
|
|
94
367
|
param_names = sigs.get_param_names("ta", func_name)
|
|
@@ -316,6 +589,11 @@ class CallHandlers:
|
|
|
316
589
|
|
|
317
590
|
returns_tuple = isinstance(expr_node, TupleLiteral)
|
|
318
591
|
tuple_size = len(expr_node.elements) if returns_tuple else 0
|
|
592
|
+
tuple_element_types = (
|
|
593
|
+
self._tuple_element_types_by_node.get(id(expr_node), ())
|
|
594
|
+
if returns_tuple
|
|
595
|
+
else ()
|
|
596
|
+
)
|
|
319
597
|
if not returns_tuple and isinstance(expr_node, FuncCall):
|
|
320
598
|
expr_func = None
|
|
321
599
|
expr_ns = None
|
|
@@ -323,6 +601,37 @@ class CallHandlers:
|
|
|
323
601
|
and isinstance(expr_node.callee.object, Identifier)):
|
|
324
602
|
expr_ns = expr_node.callee.object.name
|
|
325
603
|
expr_func = expr_node.callee.member
|
|
604
|
+
elif isinstance(expr_node.callee, Identifier):
|
|
605
|
+
expr_func = expr_node.callee.name
|
|
606
|
+
if self._func_returns_tuple.get(expr_func, False):
|
|
607
|
+
tuple_size = self._func_tuple_element_count.get(expr_func, 0)
|
|
608
|
+
tuple_types = self._func_tuple_element_types.get(expr_func, ())
|
|
609
|
+
numeric_tuple = (
|
|
610
|
+
tuple_size >= 2
|
|
611
|
+
and len(tuple_types) == tuple_size
|
|
612
|
+
and all(
|
|
613
|
+
item in (PineType.INT, PineType.FLOAT)
|
|
614
|
+
for item in tuple_types
|
|
615
|
+
)
|
|
616
|
+
)
|
|
617
|
+
bool_tuple = (
|
|
618
|
+
tuple_size >= 2
|
|
619
|
+
and len(tuple_types) == tuple_size
|
|
620
|
+
and all(item == PineType.BOOL for item in tuple_types)
|
|
621
|
+
)
|
|
622
|
+
if not (numeric_tuple or bool_tuple):
|
|
623
|
+
inferred_types = ", ".join(
|
|
624
|
+
item.value for item in tuple_types
|
|
625
|
+
) or "unknown"
|
|
626
|
+
self._error(
|
|
627
|
+
"request.security tuple-return helpers support two or more "
|
|
628
|
+
"numeric int/float elements or homogeneous bool elements; inferred "
|
|
629
|
+
f"{tuple_size} element(s) [{inferred_types}]",
|
|
630
|
+
expr_node.loc,
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
returns_tuple = True
|
|
634
|
+
tuple_element_types = tuple_types
|
|
326
635
|
if expr_ns == "ta":
|
|
327
636
|
if expr_func == "vwap":
|
|
328
637
|
merged_v = list(expr_node.args)
|
|
@@ -351,12 +660,15 @@ class CallHandlers:
|
|
|
351
660
|
# so the codegen can resolve a parameter ``tf`` via the call sites.
|
|
352
661
|
scope_name = self._symbols.current_scope.name
|
|
353
662
|
containing_func = scope_name[5:] if scope_name.startswith("func_") else ""
|
|
663
|
+
if returns_tuple and tuple_element_types:
|
|
664
|
+
self._tuple_element_types_by_node[id(node)] = tuple_element_types
|
|
354
665
|
self._security_calls.append(SecurityCallInfo(
|
|
355
666
|
sec_id=sec_id,
|
|
356
667
|
timeframe=tf_node,
|
|
357
668
|
expression=expr_node,
|
|
358
669
|
returns_tuple=returns_tuple,
|
|
359
670
|
tuple_size=tuple_size,
|
|
671
|
+
tuple_element_types=tuple_element_types,
|
|
360
672
|
gaps=gaps_node,
|
|
361
673
|
lookahead=lookahead_node,
|
|
362
674
|
ta_range=security_ta_range,
|
|
@@ -902,7 +1214,10 @@ class CallHandlers:
|
|
|
902
1214
|
|
|
903
1215
|
def _materialize_user_func_call_site_state(
|
|
904
1216
|
self, func_name: str, cs_idx: int, node: FuncCall,
|
|
905
|
-
*, reuse_existing_owner: str | None = None
|
|
1217
|
+
*, reuse_existing_owner: str | None = None,
|
|
1218
|
+
reuse_existing_targets: dict[int, int] | None = None,
|
|
1219
|
+
ta_site_indices: list[int] | None = None,
|
|
1220
|
+
materialize_fixnan: bool = True) -> dict[int, int]:
|
|
906
1221
|
"""Materialize TA/fixnan state for one UDF call-site variant.
|
|
907
1222
|
|
|
908
1223
|
Ordinary call sites are handled while walking the AST. A second class
|
|
@@ -918,17 +1233,71 @@ class CallHandlers:
|
|
|
918
1233
|
``{member}_cs{idx}`` clone for the borrowed callee site; when that clone
|
|
919
1234
|
belongs to the parent currently being propagated, it is the desired
|
|
920
1235
|
call-path state and must be reused rather than duplicated under a
|
|
921
|
-
disambiguated-but-unused name.
|
|
1236
|
+
disambiguated-but-unused name. ``reuse_existing_targets`` maps each
|
|
1237
|
+
source TA identity to the immediate parent's already-resolved target;
|
|
1238
|
+
this extends the proof through another borrowed layer even when clone
|
|
1239
|
+
name collisions forced a disambiguating suffix.
|
|
922
1240
|
"""
|
|
923
|
-
func_def = self._func_defs
|
|
1241
|
+
func_def = self._func_defs.get(func_name)
|
|
1242
|
+
method_info = None
|
|
1243
|
+
if func_def is None:
|
|
1244
|
+
method_info = next(
|
|
1245
|
+
(
|
|
1246
|
+
info
|
|
1247
|
+
for info in self._func_infos
|
|
1248
|
+
if info.name == func_name
|
|
1249
|
+
and getattr(info, "is_udt_method", False)
|
|
1250
|
+
),
|
|
1251
|
+
None,
|
|
1252
|
+
)
|
|
1253
|
+
if method_info is not None:
|
|
1254
|
+
func_def = method_info.node
|
|
1255
|
+
if func_def is None:
|
|
1256
|
+
self._error(
|
|
1257
|
+
f"Cannot materialize callable state for unknown function '{func_name}'.",
|
|
1258
|
+
node.loc,
|
|
1259
|
+
)
|
|
1260
|
+
return {}
|
|
1261
|
+
|
|
1262
|
+
selected_ta_indices: dict[int, int] = {}
|
|
924
1263
|
|
|
925
1264
|
param_arg_map: dict[str, str] = {}
|
|
1265
|
+
if (
|
|
1266
|
+
method_info is not None
|
|
1267
|
+
and isinstance(node.callee, MemberAccess)
|
|
1268
|
+
and func_name in self._method_signatures
|
|
1269
|
+
):
|
|
1270
|
+
binding = self._bind_typed_method_call(func_name, node)
|
|
1271
|
+
positional_args = [node.callee.object, *binding.args_by_param]
|
|
1272
|
+
else:
|
|
1273
|
+
positional_args = list(node.args)
|
|
1274
|
+
if (
|
|
1275
|
+
method_info is not None
|
|
1276
|
+
and isinstance(node.callee, MemberAccess)
|
|
1277
|
+
):
|
|
1278
|
+
positional_args.insert(0, node.callee.object)
|
|
926
1279
|
for p_idx, param_name in enumerate(func_def.params):
|
|
927
|
-
if p_idx < len(
|
|
928
|
-
param_arg_map[param_name] = self._expr_to_str(
|
|
1280
|
+
if p_idx < len(positional_args):
|
|
1281
|
+
param_arg_map[param_name] = self._expr_to_str(
|
|
1282
|
+
positional_args[p_idx]
|
|
1283
|
+
)
|
|
1284
|
+
elif param_name in node.kwargs:
|
|
1285
|
+
param_arg_map[param_name] = self._expr_to_str(
|
|
1286
|
+
node.kwargs[param_name]
|
|
1287
|
+
)
|
|
929
1288
|
|
|
930
1289
|
if func_name in self._func_ta_ranges:
|
|
931
1290
|
start, end = self._func_ta_ranges[func_name]
|
|
1291
|
+
site_indices = (
|
|
1292
|
+
list(ta_site_indices)
|
|
1293
|
+
if ta_site_indices is not None
|
|
1294
|
+
else list(self._func_ta_indices.get(func_name, ()))
|
|
1295
|
+
)
|
|
1296
|
+
if ta_site_indices is None and not site_indices:
|
|
1297
|
+
site_indices = list(range(start, end))
|
|
1298
|
+
func_ctor_templates = self._func_ta_ctor_args.setdefault(
|
|
1299
|
+
func_name, {}
|
|
1300
|
+
)
|
|
932
1301
|
|
|
933
1302
|
# Map local derived length variables back to expressions over the
|
|
934
1303
|
# function's parameters before substituting call-site arguments.
|
|
@@ -966,46 +1335,105 @@ class CallHandlers:
|
|
|
966
1335
|
if cs_idx == 0:
|
|
967
1336
|
# cs0 owns the source-level sites. Preserve their parameterized
|
|
968
1337
|
# ctor args for every later direct or inherited clone.
|
|
969
|
-
for i in
|
|
1338
|
+
for i in site_indices:
|
|
970
1339
|
site = self._ta_call_sites[i]
|
|
971
1340
|
if not hasattr(site, '_orig_ctor_args'):
|
|
972
1341
|
site._orig_ctor_args = [
|
|
973
1342
|
_expand_locals(arg) for arg in site.ctor_args
|
|
974
1343
|
]
|
|
1344
|
+
# Each callable owns its own view of a borrowed site's
|
|
1345
|
+
# constructor expression. Expand locals in that owner view
|
|
1346
|
+
# before substituting actual parameters; a template first
|
|
1347
|
+
# recorded at definition time can still contain a local
|
|
1348
|
+
# alias such as ``effectiveLen``.
|
|
1349
|
+
func_ctor_templates[i] = [
|
|
1350
|
+
_expand_locals(arg)
|
|
1351
|
+
for arg in func_ctor_templates.get(
|
|
1352
|
+
i, site._orig_ctor_args
|
|
1353
|
+
)
|
|
1354
|
+
]
|
|
975
1355
|
site.ctor_args = [
|
|
976
1356
|
_subst_params(arg, param_arg_map)
|
|
977
|
-
for arg in
|
|
1357
|
+
for arg in func_ctor_templates[i]
|
|
978
1358
|
]
|
|
979
|
-
|
|
980
|
-
# retain that expression so the enclosing call can resolve
|
|
981
|
-
# it and widen the enclosing TA range as before.
|
|
1359
|
+
selected_ta_indices[i] = i
|
|
982
1360
|
if enclosing_params and self._nested_ta_touched is not None:
|
|
983
1361
|
for arg in site.ctor_args:
|
|
984
1362
|
tokens = set(_re.findall(
|
|
985
1363
|
r"[A-Za-z_][A-Za-z_0-9]*", arg))
|
|
986
1364
|
if tokens & enclosing_params:
|
|
987
|
-
|
|
988
|
-
|
|
1365
|
+
if self._enclosing_func_names:
|
|
1366
|
+
caller = self._enclosing_func_names[-1]
|
|
1367
|
+
self._func_ta_ctor_args.setdefault(
|
|
1368
|
+
caller, {}
|
|
1369
|
+
)[i] = list(site.ctor_args)
|
|
989
1370
|
break
|
|
990
1371
|
else:
|
|
991
1372
|
clone_name_map: dict[str, str] = {}
|
|
992
|
-
for i in
|
|
1373
|
+
for i in site_indices:
|
|
993
1374
|
orig = self._ta_call_sites[i]
|
|
994
|
-
|
|
1375
|
+
if not hasattr(orig, '_orig_ctor_args'):
|
|
1376
|
+
orig._orig_ctor_args = [
|
|
1377
|
+
_expand_locals(arg) for arg in orig.ctor_args
|
|
1378
|
+
]
|
|
1379
|
+
func_ctor_templates[i] = [
|
|
1380
|
+
_expand_locals(arg)
|
|
1381
|
+
for arg in func_ctor_templates.get(
|
|
1382
|
+
i, orig._orig_ctor_args
|
|
1383
|
+
)
|
|
1384
|
+
]
|
|
1385
|
+
orig_args = func_ctor_templates[i]
|
|
995
1386
|
resolved_ctor = [
|
|
996
1387
|
_subst_params(arg, param_arg_map) for arg in orig_args
|
|
997
1388
|
]
|
|
1389
|
+
reuse_target = (
|
|
1390
|
+
reuse_existing_targets.get(i)
|
|
1391
|
+
if reuse_existing_targets is not None
|
|
1392
|
+
else None
|
|
1393
|
+
)
|
|
1394
|
+
if reuse_target is not None:
|
|
1395
|
+
# The parent's active clone already resolved this
|
|
1396
|
+
# exact source identity through the next outer call
|
|
1397
|
+
# boundary. Reuse it directly; do not overwrite its
|
|
1398
|
+
# concrete constructor with this edge's still-local
|
|
1399
|
+
# parameter spelling.
|
|
1400
|
+
selected_ta_indices[i] = reuse_target
|
|
1401
|
+
target_name = self._ta_call_sites[
|
|
1402
|
+
reuse_target
|
|
1403
|
+
].member_name
|
|
1404
|
+
default_name = f"{orig.member_name}_cs{cs_idx}"
|
|
1405
|
+
if target_name != default_name:
|
|
1406
|
+
clone_name_map[orig.member_name] = target_name
|
|
1407
|
+
continue
|
|
1408
|
+
existing_target = self._func_ta_call_targets.get(
|
|
1409
|
+
(id(node), cs_idx), {}
|
|
1410
|
+
).get(i)
|
|
1411
|
+
if existing_target is not None:
|
|
1412
|
+
self._ta_call_sites[existing_target].ctor_args = resolved_ctor
|
|
1413
|
+
selected_ta_indices[i] = existing_target
|
|
1414
|
+
continue
|
|
998
1415
|
clone_name = f"{orig.member_name}_cs{cs_idx}"
|
|
999
|
-
|
|
1000
|
-
(
|
|
1001
|
-
|
|
1416
|
+
existing_pair = next(
|
|
1417
|
+
(
|
|
1418
|
+
(index, site)
|
|
1419
|
+
for index, site in enumerate(self._ta_call_sites)
|
|
1420
|
+
if site.member_name == clone_name
|
|
1421
|
+
),
|
|
1002
1422
|
None,
|
|
1003
1423
|
)
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
|
|
1424
|
+
if (
|
|
1425
|
+
existing_pair is not None
|
|
1426
|
+
and (
|
|
1427
|
+
(
|
|
1428
|
+
reuse_existing_owner is not None
|
|
1429
|
+
and existing_pair[1].owner_func
|
|
1430
|
+
== reuse_existing_owner
|
|
1431
|
+
)
|
|
1432
|
+
)
|
|
1433
|
+
):
|
|
1007
1434
|
# The active parent's widened range already made the
|
|
1008
1435
|
# exact member this inherited callee variant needs.
|
|
1436
|
+
selected_ta_indices[i] = existing_pair[0]
|
|
1009
1437
|
continue
|
|
1010
1438
|
if clone_name in self._ta_member_names:
|
|
1011
1439
|
base = clone_name
|
|
@@ -1024,15 +1452,36 @@ class CallHandlers:
|
|
|
1024
1452
|
is_static=orig.is_static,
|
|
1025
1453
|
owner_func=func_name,
|
|
1026
1454
|
)
|
|
1455
|
+
selected_ta_indices[i] = len(self._ta_call_sites)
|
|
1027
1456
|
self._ta_call_sites.append(cloned)
|
|
1028
1457
|
self._ta_member_names.add(clone_name)
|
|
1029
1458
|
if clone_name_map:
|
|
1030
1459
|
self._func_cs_ta_clone_names[(func_name, cs_idx)] = clone_name_map
|
|
1031
1460
|
|
|
1461
|
+
# A nested stateful helper belongs to the enclosing callable's
|
|
1462
|
+
# call path even when its constructor has no forwarded parameters
|
|
1463
|
+
# (``ta.change`` is the canonical example). Constructor-template
|
|
1464
|
+
# forwarding above is intentionally narrower: it only rewrites
|
|
1465
|
+
# expressions that reference enclosing parameters. Keep state
|
|
1466
|
+
# ownership independent from that substitution test so every
|
|
1467
|
+
# written parent call site receives its exact borrowed TA targets.
|
|
1468
|
+
if self._nested_ta_touched is not None:
|
|
1469
|
+
self._nested_ta_touched.update(selected_ta_indices.values())
|
|
1470
|
+
|
|
1471
|
+
call_targets = self._func_ta_call_targets.setdefault(
|
|
1472
|
+
(id(node), cs_idx), {}
|
|
1473
|
+
)
|
|
1474
|
+
call_targets.update(selected_ta_indices)
|
|
1475
|
+
call_templates = self._func_ta_call_templates.setdefault(id(node), {})
|
|
1476
|
+
for index in site_indices:
|
|
1477
|
+
template = func_ctor_templates.get(index)
|
|
1478
|
+
if template is not None:
|
|
1479
|
+
call_templates[index] = tuple(template)
|
|
1480
|
+
|
|
1032
1481
|
# fixnan is stateful for the same reason as a rolling TA reducer: each
|
|
1033
1482
|
# emitted function variant needs its own previous-value member.
|
|
1034
1483
|
fn_indices = self._func_fixnan_indices.get(func_name, [])
|
|
1035
|
-
if cs_idx > 0 and fn_indices:
|
|
1484
|
+
if materialize_fixnan and cs_idx > 0 and fn_indices:
|
|
1036
1485
|
clone_map: dict[str, str] = {}
|
|
1037
1486
|
for fi in fn_indices:
|
|
1038
1487
|
orig = self._fixnan_sites[fi]
|
|
@@ -1064,19 +1513,75 @@ class CallHandlers:
|
|
|
1064
1513
|
if clone_map:
|
|
1065
1514
|
self._func_cs_fixnan_clone_names[(func_name, cs_idx)] = clone_map
|
|
1066
1515
|
|
|
1516
|
+
return selected_ta_indices
|
|
1517
|
+
|
|
1067
1518
|
def _handle_user_func_call(self, func_name: str, node: FuncCall) -> PineType:
|
|
1068
1519
|
"""Handle calls to user-defined functions."""
|
|
1069
1520
|
func_def = self._func_defs[func_name]
|
|
1070
1521
|
|
|
1071
|
-
# Visit
|
|
1072
|
-
|
|
1522
|
+
# Visit every supplied argument in Pine source order, then bind both
|
|
1523
|
+
# positional and keyword forms into declaration order. The old
|
|
1524
|
+
# positional-only path skipped kwargs entirely, hiding nested calls and
|
|
1525
|
+
# leaving deferred map-history validation without a concrete TypeSpec.
|
|
1526
|
+
bound_args = self._bind_callable_args(node, list(func_def.params))
|
|
1527
|
+
visited_types: dict[int, PineType] = {}
|
|
1073
1528
|
for arg in node.args:
|
|
1074
|
-
|
|
1529
|
+
visited_types[id(arg)] = self._visit(arg)
|
|
1530
|
+
for arg in node.kwargs.values():
|
|
1531
|
+
visited_types[id(arg)] = self._visit(arg)
|
|
1532
|
+
|
|
1533
|
+
param_types = [
|
|
1534
|
+
visited_types.get(id(arg), PineType.UNKNOWN)
|
|
1535
|
+
if arg is not None
|
|
1536
|
+
else PineType.UNKNOWN
|
|
1537
|
+
for arg in bound_args
|
|
1538
|
+
]
|
|
1539
|
+
declared_specs = list(
|
|
1540
|
+
self._func_param_type_specs.get(func_name, ())
|
|
1541
|
+
)
|
|
1542
|
+
effective_param_types = [
|
|
1543
|
+
(
|
|
1544
|
+
self._primitive_pine_type_from_spec(declared_specs[index])
|
|
1545
|
+
if index < len(declared_specs)
|
|
1546
|
+
and self._primitive_pine_type_from_spec(
|
|
1547
|
+
declared_specs[index]
|
|
1548
|
+
) != PineType.UNKNOWN
|
|
1549
|
+
else param_type
|
|
1550
|
+
)
|
|
1551
|
+
for index, param_type in enumerate(param_types)
|
|
1552
|
+
]
|
|
1553
|
+
self._callable_bound_param_types_by_node[id(node)] = list(
|
|
1554
|
+
effective_param_types
|
|
1555
|
+
)
|
|
1075
1556
|
|
|
1076
|
-
#
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1557
|
+
# Per-param TypeSpec: declared hints are authoritative; for untyped
|
|
1558
|
+
# params, infer from this call site's argument. Validate deferred
|
|
1559
|
+
# history receivers before series propagation/cloning can reinterpret
|
|
1560
|
+
# a map handle (or map-bearing UDT) as ``Series<double>``.
|
|
1561
|
+
param_specs = list(
|
|
1562
|
+
getattr(self, "_func_param_type_specs", {}).get(func_name)
|
|
1563
|
+
or self._param_type_specs_from_def(func_def)
|
|
1564
|
+
)
|
|
1565
|
+
arg_specs = [
|
|
1566
|
+
self._type_spec_from_expr(arg) if arg is not None else None
|
|
1567
|
+
for arg in bound_args
|
|
1568
|
+
]
|
|
1569
|
+
for i in range(len(param_specs)):
|
|
1570
|
+
if param_specs[i] is None and i < len(arg_specs):
|
|
1571
|
+
param_specs[i] = arg_specs[i]
|
|
1572
|
+
self._record_deferred_param_call_edge(
|
|
1573
|
+
node,
|
|
1574
|
+
func_name,
|
|
1575
|
+
list(func_def.params),
|
|
1576
|
+
bound_args,
|
|
1577
|
+
)
|
|
1578
|
+
self._validate_deferred_param_history_refs(
|
|
1579
|
+
func_name,
|
|
1580
|
+
{
|
|
1581
|
+
name: spec
|
|
1582
|
+
for name, spec in zip(func_def.params, param_specs)
|
|
1583
|
+
},
|
|
1584
|
+
)
|
|
1080
1585
|
|
|
1081
1586
|
# Determine return type: re-analyze the function body with known param types
|
|
1082
1587
|
# For now, use the cached return type from initial analysis
|
|
@@ -1105,14 +1610,24 @@ class CallHandlers:
|
|
|
1105
1610
|
return_type = PineType.FLOAT
|
|
1106
1611
|
break
|
|
1107
1612
|
|
|
1613
|
+
# History and other primitive operations preserve an untyped
|
|
1614
|
+
# parameter's family independently at each written call. Do this
|
|
1615
|
+
# before the legacy FuncInfo merge so the first call cannot dictate
|
|
1616
|
+
# every later call's return type.
|
|
1617
|
+
return_type = self._callsite_callable_return_type(
|
|
1618
|
+
func_def, effective_param_types, return_type
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1108
1621
|
# If this function has series params, ensure bar-field arguments
|
|
1109
1622
|
# passed at the call site are registered as series_bar_fields so that
|
|
1110
1623
|
# the codegen can create Series<double> members for them.
|
|
1111
1624
|
func_sv = self._func_series_vars.get(func_name, set())
|
|
1112
1625
|
if func_sv:
|
|
1113
1626
|
for p_idx, param_name in enumerate(func_def.params):
|
|
1114
|
-
if param_name in func_sv and p_idx < len(
|
|
1115
|
-
arg =
|
|
1627
|
+
if param_name in func_sv and p_idx < len(bound_args):
|
|
1628
|
+
arg = bound_args[p_idx]
|
|
1629
|
+
if arg is None:
|
|
1630
|
+
continue
|
|
1116
1631
|
if isinstance(arg, Identifier) and arg.name in BAR_FIELDS:
|
|
1117
1632
|
self._series_bar_fields.add(arg.name)
|
|
1118
1633
|
elif isinstance(arg, Identifier):
|
|
@@ -1126,6 +1641,29 @@ class CallHandlers:
|
|
|
1126
1641
|
caller_name = sym.scope[5:]
|
|
1127
1642
|
self._func_series_vars.setdefault(caller_name, set()).add(arg.name)
|
|
1128
1643
|
else:
|
|
1644
|
+
# Keep the exact declaration/member registries
|
|
1645
|
+
# in lockstep with the legacy raw-name series
|
|
1646
|
+
# promotion. Codegen uses those identities to
|
|
1647
|
+
# distinguish the real global binding from a
|
|
1648
|
+
# same-named lexical scalar tombstone.
|
|
1649
|
+
exact_member = getattr(
|
|
1650
|
+
sym, "_pf_var_member_name", None
|
|
1651
|
+
)
|
|
1652
|
+
if exact_member is not None:
|
|
1653
|
+
self._series_var_members.add(exact_member)
|
|
1654
|
+
decl_node_id = getattr(
|
|
1655
|
+
sym, "_pf_decl_node_id", None
|
|
1656
|
+
)
|
|
1657
|
+
if decl_node_id is not None:
|
|
1658
|
+
self._series_decl_nodes.add(decl_node_id)
|
|
1659
|
+
binding_name = getattr(
|
|
1660
|
+
sym,
|
|
1661
|
+
"_pf_decl_binding_name",
|
|
1662
|
+
arg.name,
|
|
1663
|
+
)
|
|
1664
|
+
self._series_decl_bindings.add(
|
|
1665
|
+
(decl_node_id, binding_name)
|
|
1666
|
+
)
|
|
1129
1667
|
self._series_vars.add(arg.name)
|
|
1130
1668
|
|
|
1131
1669
|
# Per-call-site cloning: TA, series/var, and fixnan state all advance
|
|
@@ -1134,10 +1672,25 @@ class CallHandlers:
|
|
|
1134
1672
|
has_series = func_name in self._func_series_vars or func_name in self._func_var_members
|
|
1135
1673
|
has_fixnan = func_name in self._func_fixnan_indices
|
|
1136
1674
|
if has_ta or has_series or has_fixnan:
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1675
|
+
existing_site = self._func_call_cs_map.get(id(node))
|
|
1676
|
+
if existing_site is None or existing_site[0] != func_name:
|
|
1677
|
+
# Type/return inference may revisit the same FuncCall AST node
|
|
1678
|
+
# several times. It is still one Pine textual call site, not
|
|
1679
|
+
# a new state instance on every analyzer pass. Real caller
|
|
1680
|
+
# clones are expanded later by _propagate_call_site_counts().
|
|
1681
|
+
cs_idx = self._func_call_site_count.get(func_name, 0)
|
|
1682
|
+
self._func_call_site_count[func_name] = cs_idx + 1
|
|
1683
|
+
self._func_call_cs_map[id(node)] = (func_name, cs_idx)
|
|
1684
|
+
self._materialize_user_func_call_site_state(
|
|
1685
|
+
func_name, cs_idx, node
|
|
1686
|
+
)
|
|
1687
|
+
callsite = self._func_call_cs_map.get(id(node))
|
|
1688
|
+
if callsite is not None and callsite[0] == func_name:
|
|
1689
|
+
key = (func_name, callsite[1])
|
|
1690
|
+
self._func_callsite_param_types[key] = list(
|
|
1691
|
+
effective_param_types
|
|
1692
|
+
)
|
|
1693
|
+
self._func_callsite_return_types[key] = return_type
|
|
1141
1694
|
|
|
1142
1695
|
# Create or update FuncInfo
|
|
1143
1696
|
is_tuple = self._func_returns_tuple.get(func_name, False)
|
|
@@ -1145,19 +1698,64 @@ class CallHandlers:
|
|
|
1145
1698
|
# Forward UDT-return inference (set in _visit_FuncDef) so codegen can
|
|
1146
1699
|
# emit the struct return type. Probe: udt-method-probe-20.
|
|
1147
1700
|
udt_ret = self._func_udt_return_types.get(func_name)
|
|
1701
|
+
existing = [fi for fi in self._func_infos if fi.name == func_name]
|
|
1702
|
+
|
|
1703
|
+
# A direct terminal map call on an untyped parameter cannot be typed
|
|
1704
|
+
# while the function definition is first visited: its map TypeSpec is
|
|
1705
|
+
# learned only here, from the call-site argument. Re-run only the
|
|
1706
|
+
# terminal-map classifier with those established parameter specs; do
|
|
1707
|
+
# not re-analyze the body or participate in general return inference.
|
|
1708
|
+
effective_param_specs = list(param_specs)
|
|
1709
|
+
if existing and existing[0].param_type_specs:
|
|
1710
|
+
for i, spec in enumerate(existing[0].param_type_specs):
|
|
1711
|
+
if i < len(effective_param_specs) and spec is not None:
|
|
1712
|
+
effective_param_specs[i] = spec
|
|
1713
|
+
terminal_map_return = self._terminal_map_call_return(
|
|
1714
|
+
self._direct_terminal_return_expr(func_def),
|
|
1715
|
+
{
|
|
1716
|
+
name: spec
|
|
1717
|
+
for name, spec in zip(func_def.params, effective_param_specs)
|
|
1718
|
+
},
|
|
1719
|
+
)
|
|
1148
1720
|
ret_spec = getattr(self, "_func_return_type_specs", {}).get(func_name)
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1721
|
+
terminal_expr = self._direct_terminal_return_expr(func_def)
|
|
1722
|
+
terminal_selection_spec = self._terminal_map_selection_return_spec(
|
|
1723
|
+
terminal_expr,
|
|
1724
|
+
{
|
|
1725
|
+
name: spec
|
|
1726
|
+
for name, spec in zip(
|
|
1727
|
+
func_def.params, effective_param_specs
|
|
1728
|
+
)
|
|
1729
|
+
},
|
|
1155
1730
|
)
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1731
|
+
if (
|
|
1732
|
+
terminal_map_return is None
|
|
1733
|
+
and isinstance(terminal_expr, Identifier)
|
|
1734
|
+
and terminal_expr.name in func_def.params
|
|
1735
|
+
):
|
|
1736
|
+
terminal_index = func_def.params.index(terminal_expr.name)
|
|
1737
|
+
terminal_identity_spec = (
|
|
1738
|
+
effective_param_specs[terminal_index]
|
|
1739
|
+
if terminal_index < len(effective_param_specs)
|
|
1740
|
+
else None
|
|
1741
|
+
)
|
|
1742
|
+
if (terminal_identity_spec is not None
|
|
1743
|
+
and terminal_identity_spec.kind == "map"):
|
|
1744
|
+
# An untyped identity UDF learns the map handle type from its
|
|
1745
|
+
# call site. The handle is returned by value, preserving the
|
|
1746
|
+
# backing ID rather than cloning map contents.
|
|
1747
|
+
self._func_return_type_specs[func_name] = terminal_identity_spec
|
|
1748
|
+
ret_spec = terminal_identity_spec
|
|
1749
|
+
if terminal_selection_spec is not None:
|
|
1750
|
+
self._func_return_type_specs[func_name] = terminal_selection_spec
|
|
1751
|
+
ret_spec = terminal_selection_spec
|
|
1752
|
+
if terminal_map_return is not None:
|
|
1753
|
+
return_type, inferred_ret_spec = terminal_map_return
|
|
1754
|
+
self._func_return_types[func_name] = return_type
|
|
1755
|
+
if inferred_ret_spec is not None:
|
|
1756
|
+
self._func_return_type_specs[func_name] = inferred_ret_spec
|
|
1757
|
+
ret_spec = inferred_ret_spec
|
|
1758
|
+
|
|
1161
1759
|
if not existing:
|
|
1162
1760
|
fi = FuncInfo(
|
|
1163
1761
|
name=func_name,
|
|
@@ -1174,7 +1772,10 @@ class CallHandlers:
|
|
|
1174
1772
|
else:
|
|
1175
1773
|
# Update with better type info if available
|
|
1176
1774
|
fi = existing[0]
|
|
1177
|
-
if
|
|
1775
|
+
if terminal_map_return is not None:
|
|
1776
|
+
fi.return_type = return_type
|
|
1777
|
+
fi.return_type_spec = ret_spec
|
|
1778
|
+
elif fi.return_type in (PineType.UNKNOWN, PineType.VOID) and return_type not in (PineType.UNKNOWN, PineType.VOID):
|
|
1178
1779
|
fi.return_type = return_type
|
|
1179
1780
|
for i, pt in enumerate(param_types):
|
|
1180
1781
|
if i < len(fi.param_types) and fi.param_types[i] == PineType.UNKNOWN:
|
|
@@ -1192,4 +1793,17 @@ class CallHandlers:
|
|
|
1192
1793
|
if fi.return_type_spec is None and ret_spec is not None:
|
|
1193
1794
|
fi.return_type_spec = ret_spec
|
|
1194
1795
|
|
|
1796
|
+
# A concrete map can arrive only at an outer wrapper's eventual call
|
|
1797
|
+
# site, after every nested untyped UDF was initially analyzed with
|
|
1798
|
+
# scalar fallbacks. Propagate the concrete specs through the exact
|
|
1799
|
+
# definition-time call edges and infer returns in post-order before
|
|
1800
|
+
# codegen snapshots the FuncInfo table.
|
|
1801
|
+
self._propagate_deferred_map_callable_specs(
|
|
1802
|
+
func_name,
|
|
1803
|
+
{
|
|
1804
|
+
name: spec
|
|
1805
|
+
for name, spec in zip(func_def.params, param_specs)
|
|
1806
|
+
},
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1195
1809
|
return return_type
|