@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
|
@@ -40,16 +40,35 @@ from __future__ import annotations
|
|
|
40
40
|
from typing import Any
|
|
41
41
|
|
|
42
42
|
from ..ast_nodes import (
|
|
43
|
-
ASTNode, BoolLiteral, FuncCall, Identifier,
|
|
44
|
-
NaLiteral, NumberLiteral, StringLiteral,
|
|
43
|
+
ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, IfStmt,
|
|
44
|
+
MemberAccess, NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary,
|
|
45
|
+
SwitchStmt, TupleLiteral, UnaryOp,
|
|
45
46
|
)
|
|
46
|
-
from ..symbols import PineType, TypeSpec
|
|
47
|
+
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
47
48
|
|
|
48
49
|
# Drawing-objects-as-data type names (spec §4.1). Defined locally — the
|
|
49
50
|
# analyzer must not import from ``codegen`` (codegen imports analyzer, so the
|
|
50
51
|
# reverse would be a cycle). Mirrors codegen.tables.DRAWING_TYPE_TO_CPP keys.
|
|
51
52
|
_DRAWING_TYPE_NAMES = frozenset({"line", "box", "label", "linefill", "chart.point"})
|
|
52
53
|
_DRAWING_NS = frozenset({"line", "box", "label", "linefill"})
|
|
54
|
+
_DIRECT_ARRAY_VALUE_PRODUCERS = frozenset({
|
|
55
|
+
"from",
|
|
56
|
+
"new",
|
|
57
|
+
"new_float",
|
|
58
|
+
"new_int",
|
|
59
|
+
"new_bool",
|
|
60
|
+
"new_string",
|
|
61
|
+
"copy",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Keep this analyzer-owned mirror in sync with
|
|
65
|
+
# codegen.tables.MATRIX_RETURNING_METHODS. The analyzer cannot import from
|
|
66
|
+
# codegen (codegen already imports analyzer), but nullable selections need the
|
|
67
|
+
# exact matrix result type before codegen registers global aggregate members.
|
|
68
|
+
_MATRIX_RETURNING_METHODS = frozenset({
|
|
69
|
+
"copy", "submatrix", "transpose", "concat", "diff", "mult", "pow",
|
|
70
|
+
"inv", "pinv", "eigenvectors", "kron",
|
|
71
|
+
})
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
class TypeHelper:
|
|
@@ -134,14 +153,292 @@ class TypeHelper:
|
|
|
134
153
|
raw = ann.get("template_args") or []
|
|
135
154
|
return [str(x).replace(" ", "") for x in raw]
|
|
136
155
|
|
|
156
|
+
def _array_from_element_spec(self, value: ASTNode | None) -> TypeSpec | None:
|
|
157
|
+
"""Exact scalar element type for one ``array.from`` argument.
|
|
158
|
+
|
|
159
|
+
Keep this refinement local to array construction. General BinOp
|
|
160
|
+
TypeSpec inference changes unrelated scalar comparator lowering across
|
|
161
|
+
the corpus; array declarations only need enough structure to keep the
|
|
162
|
+
analyzer's captured LHS type aligned with codegen's RHS vector type.
|
|
163
|
+
"""
|
|
164
|
+
if value is None:
|
|
165
|
+
return None
|
|
166
|
+
if isinstance(value, NumberLiteral):
|
|
167
|
+
return TypeSpec.primitive(
|
|
168
|
+
"float" if isinstance(value.value, float) else "int"
|
|
169
|
+
)
|
|
170
|
+
if isinstance(value, BoolLiteral):
|
|
171
|
+
return TypeSpec.primitive("bool")
|
|
172
|
+
if isinstance(value, StringLiteral):
|
|
173
|
+
return TypeSpec.primitive("string")
|
|
174
|
+
if isinstance(value, BinOp):
|
|
175
|
+
left = self._array_from_element_spec(value.left)
|
|
176
|
+
right = self._array_from_element_spec(value.right)
|
|
177
|
+
if value.op in ("==", "!=", ">", "<", ">=", "<=", "and", "or"):
|
|
178
|
+
return TypeSpec.primitive("bool")
|
|
179
|
+
if (left is not None and right is not None
|
|
180
|
+
and left.kind == "primitive" and right.kind == "primitive"):
|
|
181
|
+
if left.name == "string" or right.name == "string":
|
|
182
|
+
return TypeSpec.primitive("string")
|
|
183
|
+
if value.op == "/" or left.name == "float" or right.name == "float":
|
|
184
|
+
return TypeSpec.primitive("float")
|
|
185
|
+
if left.name == "int" and right.name == "int":
|
|
186
|
+
return TypeSpec.primitive("int")
|
|
187
|
+
return None
|
|
188
|
+
spec = self._type_spec_from_expr(value)
|
|
189
|
+
if spec is not None:
|
|
190
|
+
return spec
|
|
191
|
+
if isinstance(value, Identifier):
|
|
192
|
+
sym = self._symbols.resolve(value.name)
|
|
193
|
+
if sym is not None and sym.pine_type in {
|
|
194
|
+
PineType.INT, PineType.FLOAT, PineType.BOOL,
|
|
195
|
+
PineType.STRING, PineType.COLOR,
|
|
196
|
+
}:
|
|
197
|
+
return self._pine_type_to_spec(sym.pine_type)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _selection_terminal_expr(
|
|
202
|
+
body: list[ASTNode] | None,
|
|
203
|
+
) -> ASTNode | None:
|
|
204
|
+
"""Return one if/switch branch's value expression, if present."""
|
|
205
|
+
if not body:
|
|
206
|
+
return None
|
|
207
|
+
terminal = body[-1]
|
|
208
|
+
return terminal.expr if isinstance(terminal, ExprStmt) else terminal
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _selection_node_is_na(node: ASTNode | None) -> bool:
|
|
212
|
+
"""Whether a selection arm is explicit or implicit Pine ``na``."""
|
|
213
|
+
return (
|
|
214
|
+
node is None
|
|
215
|
+
or isinstance(node, NaLiteral)
|
|
216
|
+
or (isinstance(node, Identifier) and node.name == "na")
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _nullable_collection_selection_spec(
|
|
220
|
+
self,
|
|
221
|
+
branches: list[tuple[ASTNode | None, TypeSpec | None]],
|
|
222
|
+
) -> TypeSpec | None:
|
|
223
|
+
"""Unify compatible map/matrix selection arms around typed ``na``.
|
|
224
|
+
|
|
225
|
+
A missing ``if``/``switch`` fallback is Pine's implicit ``na`` arm.
|
|
226
|
+
Every concrete arm must carry the same nullable collection TypeSpec;
|
|
227
|
+
an unknown or incompatible concrete arm fails closed.
|
|
228
|
+
"""
|
|
229
|
+
concrete: list[TypeSpec] = []
|
|
230
|
+
for node, spec in branches:
|
|
231
|
+
if self._selection_node_is_na(node):
|
|
232
|
+
continue
|
|
233
|
+
if spec is None or spec.kind not in {"map", "matrix"}:
|
|
234
|
+
return None
|
|
235
|
+
concrete.append(spec)
|
|
236
|
+
if not concrete:
|
|
237
|
+
return None
|
|
238
|
+
first = concrete[0]
|
|
239
|
+
return first if all(spec == first for spec in concrete[1:]) else None
|
|
240
|
+
|
|
137
241
|
def _type_spec_from_expr(self, value: ASTNode | None) -> TypeSpec | None:
|
|
138
242
|
if value is None:
|
|
139
243
|
return None
|
|
244
|
+
if isinstance(value, NumberLiteral):
|
|
245
|
+
return TypeSpec.primitive(
|
|
246
|
+
"float" if isinstance(value.value, float) else "int"
|
|
247
|
+
)
|
|
248
|
+
if isinstance(value, BoolLiteral):
|
|
249
|
+
return TypeSpec.primitive("bool")
|
|
250
|
+
if isinstance(value, StringLiteral):
|
|
251
|
+
return TypeSpec.primitive("string")
|
|
252
|
+
if isinstance(value, Ternary):
|
|
253
|
+
true_spec = self._type_spec_from_expr(value.true_val)
|
|
254
|
+
false_spec = self._type_spec_from_expr(value.false_val)
|
|
255
|
+
collection_spec = self._nullable_collection_selection_spec([
|
|
256
|
+
(value.true_val, true_spec),
|
|
257
|
+
(value.false_val, false_spec),
|
|
258
|
+
])
|
|
259
|
+
if collection_spec is not None:
|
|
260
|
+
return collection_spec
|
|
261
|
+
|
|
262
|
+
def direct_user_udt_ctor_name(node: ASTNode) -> str | None:
|
|
263
|
+
if not isinstance(node, FuncCall):
|
|
264
|
+
return None
|
|
265
|
+
callee = node.callee
|
|
266
|
+
if not (
|
|
267
|
+
isinstance(callee, MemberAccess)
|
|
268
|
+
and isinstance(callee.object, Identifier)
|
|
269
|
+
and callee.member == "new"
|
|
270
|
+
):
|
|
271
|
+
return None
|
|
272
|
+
name = callee.object.name
|
|
273
|
+
return name if name in self._udt_fields else None
|
|
274
|
+
|
|
275
|
+
# Selecting between two values of the same user-defined type
|
|
276
|
+
# preserves that receiver type. Codegen already applies this
|
|
277
|
+
# rule; the analyzer must agree so stateful method calls on a UDT
|
|
278
|
+
# ternary enter the written-callsite clone graph.
|
|
279
|
+
if (true_spec is not None
|
|
280
|
+
and true_spec.kind == "udt"
|
|
281
|
+
and true_spec == false_spec):
|
|
282
|
+
return true_spec
|
|
283
|
+
# A direct user-UDT constructor selected against bare ``na`` has
|
|
284
|
+
# one unambiguous value type. Require the constructor AST itself,
|
|
285
|
+
# not merely an inferred UDT expression, so temporary array-element
|
|
286
|
+
# identity returns continue to fail closed on their own surface.
|
|
287
|
+
true_ctor = direct_user_udt_ctor_name(value.true_val)
|
|
288
|
+
if (true_spec is not None
|
|
289
|
+
and true_spec.kind == "udt"
|
|
290
|
+
and true_spec.name == true_ctor
|
|
291
|
+
and isinstance(value.false_val, NaLiteral)):
|
|
292
|
+
return true_spec
|
|
293
|
+
false_ctor = direct_user_udt_ctor_name(value.false_val)
|
|
294
|
+
if (false_spec is not None
|
|
295
|
+
and false_spec.kind == "udt"
|
|
296
|
+
and false_spec.name == false_ctor
|
|
297
|
+
and isinstance(value.true_val, NaLiteral)):
|
|
298
|
+
return false_spec
|
|
299
|
+
# Drawing handles are nullable reference-like values in Pine. A
|
|
300
|
+
# bare ``na`` arm therefore acquires the other arm's exact handle
|
|
301
|
+
# type, just like the established PineMap path above. Keep this
|
|
302
|
+
# intentionally narrower than arbitrary UDTs/collections: their
|
|
303
|
+
# target-typed selection semantics are not established here.
|
|
304
|
+
if (true_spec is not None
|
|
305
|
+
and true_spec.kind == "udt"
|
|
306
|
+
and true_spec.name in _DRAWING_TYPE_NAMES
|
|
307
|
+
and isinstance(value.false_val, NaLiteral)):
|
|
308
|
+
return true_spec
|
|
309
|
+
if (false_spec is not None
|
|
310
|
+
and false_spec.kind == "udt"
|
|
311
|
+
and false_spec.name in _DRAWING_TYPE_NAMES
|
|
312
|
+
and isinstance(value.true_val, NaLiteral)):
|
|
313
|
+
return false_spec
|
|
314
|
+
return None
|
|
315
|
+
if isinstance(value, Subscript):
|
|
316
|
+
# Pine's history operator preserves the value type: a
|
|
317
|
+
# ``Series<line>`` read such as ``h[1]`` is a scalar ``line``
|
|
318
|
+
# handle, not the legacy numeric fallback. Keep this refinement
|
|
319
|
+
# drawing-only; collection subscripts have separate array/map
|
|
320
|
+
# semantics and primitive history inference already flows through
|
|
321
|
+
# PineType in ``_visit_Subscript``.
|
|
322
|
+
receiver_spec = self._type_spec_from_expr(value.object)
|
|
323
|
+
if (receiver_spec is not None
|
|
324
|
+
and receiver_spec.kind == "udt"
|
|
325
|
+
and receiver_spec.name in _DRAWING_TYPE_NAMES):
|
|
326
|
+
return receiver_spec
|
|
327
|
+
return None
|
|
328
|
+
if isinstance(value, IfStmt):
|
|
329
|
+
true_node = self._selection_terminal_expr(value.body)
|
|
330
|
+
false_node = self._selection_terminal_expr(value.else_body)
|
|
331
|
+
true_spec = self._type_spec_from_expr(true_node)
|
|
332
|
+
false_spec = self._type_spec_from_expr(false_node)
|
|
333
|
+
collection_spec = self._nullable_collection_selection_spec([
|
|
334
|
+
(true_node, true_spec),
|
|
335
|
+
(false_node, false_spec),
|
|
336
|
+
])
|
|
337
|
+
if collection_spec is not None:
|
|
338
|
+
return collection_spec
|
|
339
|
+
true_is_na = (
|
|
340
|
+
isinstance(true_node, NaLiteral)
|
|
341
|
+
or (isinstance(true_node, Identifier)
|
|
342
|
+
and true_node.name == "na")
|
|
343
|
+
)
|
|
344
|
+
false_is_na = (
|
|
345
|
+
isinstance(false_node, NaLiteral)
|
|
346
|
+
or (isinstance(false_node, Identifier)
|
|
347
|
+
and false_node.name == "na")
|
|
348
|
+
)
|
|
349
|
+
if (true_spec is not None
|
|
350
|
+
and true_spec.kind == "udt"
|
|
351
|
+
and true_spec.name in _DRAWING_TYPE_NAMES
|
|
352
|
+
and true_spec == false_spec):
|
|
353
|
+
return true_spec
|
|
354
|
+
if (true_spec is not None
|
|
355
|
+
and true_spec.kind == "udt"
|
|
356
|
+
and true_spec.name in _DRAWING_TYPE_NAMES
|
|
357
|
+
and false_is_na):
|
|
358
|
+
return true_spec
|
|
359
|
+
if (false_spec is not None
|
|
360
|
+
and false_spec.kind == "udt"
|
|
361
|
+
and false_spec.name in _DRAWING_TYPE_NAMES
|
|
362
|
+
and true_is_na):
|
|
363
|
+
return false_spec
|
|
364
|
+
return None
|
|
365
|
+
if isinstance(value, SwitchStmt):
|
|
366
|
+
branches: list[tuple[ASTNode | None, TypeSpec | None]] = []
|
|
367
|
+
for _case_expr, case_body in value.cases:
|
|
368
|
+
terminal = self._selection_terminal_expr(case_body)
|
|
369
|
+
branches.append((
|
|
370
|
+
terminal,
|
|
371
|
+
self._type_spec_from_expr(terminal),
|
|
372
|
+
))
|
|
373
|
+
default_terminal = self._selection_terminal_expr(
|
|
374
|
+
value.default_body
|
|
375
|
+
)
|
|
376
|
+
branches.append((
|
|
377
|
+
default_terminal,
|
|
378
|
+
self._type_spec_from_expr(default_terminal),
|
|
379
|
+
))
|
|
380
|
+
return self._nullable_collection_selection_spec(branches)
|
|
140
381
|
if isinstance(value, FuncCall):
|
|
141
382
|
cal = value.callee
|
|
142
383
|
func = cal.member if isinstance(cal, MemberAccess) else None
|
|
143
384
|
ns = cal.object.name if isinstance(cal, MemberAccess) and isinstance(cal.object, Identifier) else None
|
|
144
385
|
targs = self._template_args_from_call(value)
|
|
386
|
+
if isinstance(cal, MemberAccess):
|
|
387
|
+
typed_receiver_spec = self._type_spec_from_expr(cal.object)
|
|
388
|
+
typed_receiver_name = method_receiver_type_name(
|
|
389
|
+
typed_receiver_spec
|
|
390
|
+
)
|
|
391
|
+
method_key = (
|
|
392
|
+
f"{typed_receiver_name}.{func}"
|
|
393
|
+
if typed_receiver_name is not None
|
|
394
|
+
else None
|
|
395
|
+
)
|
|
396
|
+
method_info = next(
|
|
397
|
+
(
|
|
398
|
+
info
|
|
399
|
+
for info in getattr(self, "_func_infos", ())
|
|
400
|
+
if info.name == method_key
|
|
401
|
+
and getattr(info, "is_udt_method", False)
|
|
402
|
+
),
|
|
403
|
+
None,
|
|
404
|
+
) if typed_receiver_name is not None else None
|
|
405
|
+
if method_info is not None:
|
|
406
|
+
return_spec = getattr(
|
|
407
|
+
method_info, "return_type_spec", None
|
|
408
|
+
)
|
|
409
|
+
if return_spec is not None:
|
|
410
|
+
return return_spec
|
|
411
|
+
udt_return = getattr(
|
|
412
|
+
method_info, "udt_return_type", None
|
|
413
|
+
)
|
|
414
|
+
if udt_return is not None:
|
|
415
|
+
return TypeSpec.udt(udt_return)
|
|
416
|
+
if method_info.return_type in {
|
|
417
|
+
PineType.INT,
|
|
418
|
+
PineType.FLOAT,
|
|
419
|
+
PineType.BOOL,
|
|
420
|
+
PineType.STRING,
|
|
421
|
+
PineType.COLOR,
|
|
422
|
+
}:
|
|
423
|
+
return self._pine_type_to_spec(
|
|
424
|
+
method_info.return_type
|
|
425
|
+
)
|
|
426
|
+
return None
|
|
427
|
+
if (
|
|
428
|
+
method_key is not None
|
|
429
|
+
and method_key in getattr(self, "_method_signatures", {})
|
|
430
|
+
):
|
|
431
|
+
# A later authored method declaration owns this surface.
|
|
432
|
+
# Its body-derived return type is not available yet, but a
|
|
433
|
+
# same-named builtin must not lend it a false type.
|
|
434
|
+
return None
|
|
435
|
+
if (
|
|
436
|
+
typed_receiver_spec is not None
|
|
437
|
+
and typed_receiver_spec.kind == "udt"
|
|
438
|
+
and typed_receiver_spec.name in self._udt_fields
|
|
439
|
+
and func == "copy"
|
|
440
|
+
):
|
|
441
|
+
return typed_receiver_spec
|
|
145
442
|
# Drawing-objects-as-data return typing: *.new / *.copy -> handle of
|
|
146
443
|
# the self-type; linefill.get_line* -> line; chart.point.* -> point.
|
|
147
444
|
if ns in _DRAWING_NS:
|
|
@@ -166,16 +463,27 @@ class TypeHelper:
|
|
|
166
463
|
elem = self._type_spec_from_hint(targs[0]) or TypeSpec.udt(targs[0])
|
|
167
464
|
return TypeSpec.array(elem)
|
|
168
465
|
if func == "from" and value.args:
|
|
466
|
+
first_spec = self._array_from_element_spec(value.args[0])
|
|
467
|
+
if first_spec is not None:
|
|
468
|
+
return TypeSpec.array(first_spec)
|
|
169
469
|
first = self._visit(value.args[0])
|
|
170
470
|
return TypeSpec.array(self._pine_type_to_spec(first))
|
|
171
471
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
172
472
|
# Functional-form array element/copy accessors: the receiver is
|
|
173
|
-
# the first argument (``array.copy(arr)``),
|
|
174
|
-
#
|
|
175
|
-
|
|
473
|
+
# the first argument (``array.copy(arr)``), or the exact ``id``
|
|
474
|
+
# keyword for ``array.copy(id=arr)``. The latter deliberately
|
|
475
|
+
# uses the shape validator shared by terminal-return recovery so
|
|
476
|
+
# duplicate/unknown keyword forms do not acquire a type by
|
|
477
|
+
# accident.
|
|
478
|
+
if (ns == "array"
|
|
176
479
|
and func in ("copy", "slice", "get", "first", "last",
|
|
177
480
|
"pop", "shift", "remove")):
|
|
178
|
-
|
|
481
|
+
receiver = None
|
|
482
|
+
if func == "copy":
|
|
483
|
+
receiver = self._direct_namespace_array_copy_source(value)
|
|
484
|
+
elif value.args:
|
|
485
|
+
receiver = value.args[0]
|
|
486
|
+
arg_spec = self._type_spec_from_expr(receiver)
|
|
179
487
|
if arg_spec is not None and arg_spec.kind == "array":
|
|
180
488
|
if func in ("copy", "slice"):
|
|
181
489
|
return arg_spec
|
|
@@ -186,10 +494,37 @@ class TypeHelper:
|
|
|
186
494
|
else:
|
|
187
495
|
elem = TypeSpec.primitive("float")
|
|
188
496
|
return TypeSpec.matrix(elem)
|
|
497
|
+
if ns == "matrix" and func in _MATRIX_RETURNING_METHODS:
|
|
498
|
+
receiver = value.args[0] if value.args else value.kwargs.get("id")
|
|
499
|
+
receiver_spec = self._type_spec_from_expr(receiver)
|
|
500
|
+
if receiver_spec is not None and receiver_spec.kind == "matrix":
|
|
501
|
+
return receiver_spec
|
|
189
502
|
if ns == "map" and func == "new":
|
|
190
503
|
key = self._type_spec_from_hint(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
191
504
|
val = self._type_spec_from_hint(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
192
505
|
return TypeSpec.map(key or TypeSpec.primitive("string"), val or TypeSpec.primitive("float"))
|
|
506
|
+
if ns == "map" and func in {
|
|
507
|
+
"put", "get", "remove", "contains", "size", "keys",
|
|
508
|
+
"values", "copy", "put_all", "clear",
|
|
509
|
+
} and value.args:
|
|
510
|
+
recv_spec = self._type_spec_from_expr(value.args[0])
|
|
511
|
+
if recv_spec is not None and recv_spec.kind == "map":
|
|
512
|
+
if func in ("put", "get", "remove"):
|
|
513
|
+
return recv_spec.value
|
|
514
|
+
if func == "keys":
|
|
515
|
+
return TypeSpec.array(
|
|
516
|
+
recv_spec.key or TypeSpec.primitive("string")
|
|
517
|
+
)
|
|
518
|
+
if func == "values":
|
|
519
|
+
return TypeSpec.array(
|
|
520
|
+
recv_spec.value or TypeSpec.primitive("float")
|
|
521
|
+
)
|
|
522
|
+
if func == "copy":
|
|
523
|
+
return recv_spec
|
|
524
|
+
if func == "contains":
|
|
525
|
+
return TypeSpec.primitive("bool")
|
|
526
|
+
if func == "size":
|
|
527
|
+
return TypeSpec.primitive("int")
|
|
193
528
|
if ns == "str" and func == "split":
|
|
194
529
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
195
530
|
if ns == "ta" and func == "pivot_point_levels":
|
|
@@ -206,7 +541,7 @@ class TypeHelper:
|
|
|
206
541
|
if inner is None:
|
|
207
542
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
208
543
|
return TypeSpec.array(self._pine_type_to_spec(inner))
|
|
209
|
-
if ns in self._udt_fields and func
|
|
544
|
+
if ns in self._udt_fields and func in {"new", "copy"}:
|
|
210
545
|
return TypeSpec.udt(ns)
|
|
211
546
|
if isinstance(cal, MemberAccess):
|
|
212
547
|
recv_spec = self._type_spec_from_expr(cal.object)
|
|
@@ -216,14 +551,20 @@ class TypeHelper:
|
|
|
216
551
|
if func in ("copy", "slice"):
|
|
217
552
|
return recv_spec
|
|
218
553
|
if recv_spec is not None and recv_spec.kind == "map":
|
|
219
|
-
if func in ("get", "remove"):
|
|
554
|
+
if func in ("put", "get", "remove"):
|
|
220
555
|
return recv_spec.value
|
|
221
556
|
if func == "keys":
|
|
222
557
|
return TypeSpec.array(recv_spec.key or TypeSpec.primitive("string"))
|
|
223
558
|
if func == "values":
|
|
224
559
|
return TypeSpec.array(recv_spec.value or TypeSpec.primitive("float"))
|
|
560
|
+
if func == "copy":
|
|
561
|
+
return recv_spec
|
|
562
|
+
if func == "contains":
|
|
563
|
+
return TypeSpec.primitive("bool")
|
|
564
|
+
if func == "size":
|
|
565
|
+
return TypeSpec.primitive("int")
|
|
225
566
|
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
226
|
-
if func in
|
|
567
|
+
if func in _MATRIX_RETURNING_METHODS:
|
|
227
568
|
return recv_spec
|
|
228
569
|
if func in ("row", "col"):
|
|
229
570
|
return TypeSpec.array(recv_spec.element)
|
|
@@ -231,6 +572,25 @@ class TypeHelper:
|
|
|
231
572
|
return recv_spec.element
|
|
232
573
|
if func == "eigenvalues":
|
|
233
574
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
575
|
+
receiver_name = method_receiver_type_name(recv_spec)
|
|
576
|
+
if receiver_name is not None:
|
|
577
|
+
method_key = f"{receiver_name}.{func}"
|
|
578
|
+
method_info = next(
|
|
579
|
+
(
|
|
580
|
+
info
|
|
581
|
+
for info in getattr(self, "_func_infos", ())
|
|
582
|
+
if info.name == method_key
|
|
583
|
+
and getattr(info, "is_udt_method", False)
|
|
584
|
+
),
|
|
585
|
+
None,
|
|
586
|
+
)
|
|
587
|
+
return_spec = getattr(
|
|
588
|
+
method_info, "return_type_spec", None
|
|
589
|
+
)
|
|
590
|
+
if return_spec is not None:
|
|
591
|
+
return return_spec
|
|
592
|
+
if method_key in getattr(self, "_method_signatures", {}):
|
|
593
|
+
return None
|
|
234
594
|
# Drawing method-form: a.copy() -> same handle; lf.get_line*() -> line.
|
|
235
595
|
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
236
596
|
and recv_spec.name in _DRAWING_TYPE_NAMES):
|
|
@@ -240,23 +600,600 @@ class TypeHelper:
|
|
|
240
600
|
return TypeSpec.udt("line")
|
|
241
601
|
if isinstance(value, Identifier):
|
|
242
602
|
sym = self._symbols.resolve(value.name)
|
|
243
|
-
if sym is not None
|
|
244
|
-
|
|
603
|
+
if sym is not None:
|
|
604
|
+
if sym.type_spec is not None:
|
|
605
|
+
return sym.type_spec
|
|
606
|
+
if sym.pine_type in {
|
|
607
|
+
PineType.INT,
|
|
608
|
+
PineType.FLOAT,
|
|
609
|
+
PineType.BOOL,
|
|
610
|
+
PineType.STRING,
|
|
611
|
+
PineType.COLOR,
|
|
612
|
+
}:
|
|
613
|
+
return self._pine_type_to_spec(sym.pine_type)
|
|
245
614
|
if isinstance(value, FuncCall):
|
|
246
615
|
# User-function return spec (e.g. an array-returning
|
|
247
616
|
# ``buildPDLevels() => array.from(...)``), so a caller's
|
|
248
617
|
# ``allLevels = buildPDLevels()`` infers an array TypeSpec.
|
|
249
618
|
cal = value.callee
|
|
250
|
-
|
|
251
|
-
|
|
619
|
+
if isinstance(cal, MemberAccess):
|
|
620
|
+
receiver_name = method_receiver_type_name(
|
|
621
|
+
self._type_spec_from_expr(cal.object)
|
|
622
|
+
)
|
|
623
|
+
fname = (
|
|
624
|
+
f"{receiver_name}.{cal.member}"
|
|
625
|
+
if receiver_name is not None
|
|
626
|
+
else cal.member
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
fname = cal.name if isinstance(cal, Identifier) else None
|
|
252
630
|
if fname and fname in getattr(self, "_func_return_type_specs", {}):
|
|
253
631
|
return self._func_return_type_specs[fname]
|
|
632
|
+
if fname and fname in getattr(self, "_func_udt_return_types", {}):
|
|
633
|
+
udt_return = self._func_udt_return_types[fname]
|
|
634
|
+
if udt_return in _DRAWING_TYPE_NAMES:
|
|
635
|
+
return TypeSpec.udt(udt_return)
|
|
254
636
|
if isinstance(value, MemberAccess):
|
|
255
637
|
owner = self._type_spec_from_expr(value.object)
|
|
256
638
|
if owner is not None and owner.kind == "udt" and owner.name:
|
|
257
639
|
return (self._udt_field_type_specs.get(owner.name) or {}).get(value.member)
|
|
258
640
|
return None
|
|
259
641
|
|
|
642
|
+
@staticmethod
|
|
643
|
+
def _direct_terminal_return_expr(func_def) -> ASTNode | None:
|
|
644
|
+
"""Return a UDF's direct terminal expression, if it has one."""
|
|
645
|
+
if not func_def.body:
|
|
646
|
+
return None
|
|
647
|
+
last_stmt = func_def.body[-1]
|
|
648
|
+
if isinstance(last_stmt, ExprStmt):
|
|
649
|
+
return last_stmt.expr
|
|
650
|
+
if not isinstance(last_stmt, TupleLiteral) and hasattr(last_stmt, "loc"):
|
|
651
|
+
return last_stmt
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
def _terminal_map_selection_return_spec(
|
|
655
|
+
self,
|
|
656
|
+
value: ASTNode | None,
|
|
657
|
+
parameter_specs: dict[str, TypeSpec | None],
|
|
658
|
+
) -> TypeSpec | None:
|
|
659
|
+
"""Infer a map returned by a terminal ternary/block selection.
|
|
660
|
+
|
|
661
|
+
Untyped UDF parameters do not have a lexical ``TypeSpec`` while their
|
|
662
|
+
definition is first analyzed. At a concrete call site, however, the
|
|
663
|
+
argument specs are known. Propagate those specs through only the two
|
|
664
|
+
Pine selection shapes whose result type is determined by compatible
|
|
665
|
+
branches: ``condition ? a : b`` and terminal ``if`` expressions.
|
|
666
|
+
|
|
667
|
+
A direct ``na`` arm is context-typed by the opposite map arm. All
|
|
668
|
+
other unresolved or incompatible shapes return ``None`` so this helper
|
|
669
|
+
cannot turn a scalar/non-map UDF into a map-returning function.
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
def branch_terminal(node: ASTNode | None) -> ASTNode | None:
|
|
673
|
+
if not isinstance(node, IfStmt):
|
|
674
|
+
return node
|
|
675
|
+
if not node.body or not node.else_body:
|
|
676
|
+
return None
|
|
677
|
+
return node
|
|
678
|
+
|
|
679
|
+
def body_terminal(body: list[ASTNode] | None) -> ASTNode | None:
|
|
680
|
+
if not body:
|
|
681
|
+
return None
|
|
682
|
+
terminal = body[-1]
|
|
683
|
+
return terminal.expr if isinstance(terminal, ExprStmt) else terminal
|
|
684
|
+
|
|
685
|
+
def resolve(node: ASTNode | None) -> TypeSpec | None:
|
|
686
|
+
if node is None or isinstance(node, NaLiteral):
|
|
687
|
+
return None
|
|
688
|
+
if isinstance(node, Identifier) and node.name in parameter_specs:
|
|
689
|
+
spec = parameter_specs[node.name]
|
|
690
|
+
return spec if spec is not None and spec.kind == "map" else None
|
|
691
|
+
if isinstance(node, Ternary):
|
|
692
|
+
return compatible(node.true_val, node.false_val)
|
|
693
|
+
if isinstance(node, IfStmt):
|
|
694
|
+
return compatible(
|
|
695
|
+
body_terminal(node.body),
|
|
696
|
+
body_terminal(node.else_body),
|
|
697
|
+
)
|
|
698
|
+
spec = self._type_spec_from_expr(node)
|
|
699
|
+
return spec if spec is not None and spec.kind == "map" else None
|
|
700
|
+
|
|
701
|
+
def compatible(
|
|
702
|
+
left_node: ASTNode | None,
|
|
703
|
+
right_node: ASTNode | None,
|
|
704
|
+
) -> TypeSpec | None:
|
|
705
|
+
left_node = branch_terminal(left_node)
|
|
706
|
+
right_node = branch_terminal(right_node)
|
|
707
|
+
if left_node is None or right_node is None:
|
|
708
|
+
return None
|
|
709
|
+
left = resolve(left_node)
|
|
710
|
+
right = resolve(right_node)
|
|
711
|
+
if left is not None and right is not None:
|
|
712
|
+
return left if left == right else None
|
|
713
|
+
if left is not None and isinstance(right_node, NaLiteral):
|
|
714
|
+
return left
|
|
715
|
+
if right is not None and isinstance(left_node, NaLiteral):
|
|
716
|
+
return right
|
|
717
|
+
return None
|
|
718
|
+
|
|
719
|
+
if isinstance(value, Ternary):
|
|
720
|
+
return compatible(value.true_val, value.false_val)
|
|
721
|
+
if isinstance(value, IfStmt):
|
|
722
|
+
return compatible(
|
|
723
|
+
body_terminal(value.body),
|
|
724
|
+
body_terminal(value.else_body),
|
|
725
|
+
)
|
|
726
|
+
return None
|
|
727
|
+
|
|
728
|
+
def _terminal_map_call_return(
|
|
729
|
+
self,
|
|
730
|
+
value: ASTNode | None,
|
|
731
|
+
parameter_specs: dict[str, TypeSpec | None] | None = None,
|
|
732
|
+
) -> tuple[PineType, TypeSpec | None] | None:
|
|
733
|
+
"""Return metadata for a direct terminal map call in a UDF.
|
|
734
|
+
|
|
735
|
+
This deliberately does not participate in general expression or
|
|
736
|
+
declaration inference. During function-definition analysis it uses
|
|
737
|
+
the active lexical scope for locals and typed parameters; for untyped
|
|
738
|
+
parameters it may be called again with call-site ``parameter_specs``.
|
|
739
|
+
Arbitrary-expression receivers stay out of scope because their map
|
|
740
|
+
method routing is not supported yet.
|
|
741
|
+
"""
|
|
742
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
743
|
+
value.callee, MemberAccess
|
|
744
|
+
):
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
callee = value.callee
|
|
748
|
+
method = callee.member
|
|
749
|
+
receiver = None
|
|
750
|
+
if isinstance(callee.object, Identifier) and callee.object.name == "map":
|
|
751
|
+
# Functional form: map.method(id, ...). Keyword-only receiver
|
|
752
|
+
# routing remains a separate residual, so require the established
|
|
753
|
+
# positional receiver here.
|
|
754
|
+
functional_arity = {
|
|
755
|
+
"clear": 1,
|
|
756
|
+
"keys": 1,
|
|
757
|
+
"values": 1,
|
|
758
|
+
"copy": 1,
|
|
759
|
+
"put_all": 2,
|
|
760
|
+
"get": 2,
|
|
761
|
+
"remove": 2,
|
|
762
|
+
"put": 3,
|
|
763
|
+
}.get(method)
|
|
764
|
+
if (
|
|
765
|
+
functional_arity is not None
|
|
766
|
+
and len(value.args) == functional_arity
|
|
767
|
+
and not value.kwargs
|
|
768
|
+
):
|
|
769
|
+
receiver = value.args[0]
|
|
770
|
+
elif isinstance(callee.object, Identifier):
|
|
771
|
+
# Global/local/typed-parameter method forms only. Do not infer an
|
|
772
|
+
# arbitrary expression receiver that codegen cannot route.
|
|
773
|
+
is_parameter = (
|
|
774
|
+
parameter_specs is not None
|
|
775
|
+
and callee.object.name in parameter_specs
|
|
776
|
+
)
|
|
777
|
+
valid_method_shape = False
|
|
778
|
+
expected_arity = {
|
|
779
|
+
"clear": 0,
|
|
780
|
+
"keys": 0,
|
|
781
|
+
"values": 0,
|
|
782
|
+
"copy": 0,
|
|
783
|
+
"put_all": 1,
|
|
784
|
+
"get": 1,
|
|
785
|
+
"remove": 1,
|
|
786
|
+
"put": 2,
|
|
787
|
+
}.get(method)
|
|
788
|
+
if expected_arity is not None:
|
|
789
|
+
valid_method_shape = (
|
|
790
|
+
len(value.args) == expected_arity and not value.kwargs
|
|
791
|
+
)
|
|
792
|
+
if is_parameter and not valid_method_shape:
|
|
793
|
+
if method == "put_all":
|
|
794
|
+
valid_method_shape = (
|
|
795
|
+
not value.args and set(value.kwargs) == {"id2"}
|
|
796
|
+
)
|
|
797
|
+
elif method in ("get", "remove"):
|
|
798
|
+
valid_method_shape = (
|
|
799
|
+
not value.args and set(value.kwargs) == {"key"}
|
|
800
|
+
)
|
|
801
|
+
elif method == "put":
|
|
802
|
+
valid_method_shape = (
|
|
803
|
+
(
|
|
804
|
+
len(value.args) == 1
|
|
805
|
+
and set(value.kwargs) == {"value"}
|
|
806
|
+
)
|
|
807
|
+
or (
|
|
808
|
+
not value.args
|
|
809
|
+
and set(value.kwargs) == {"key", "value"}
|
|
810
|
+
)
|
|
811
|
+
)
|
|
812
|
+
if valid_method_shape:
|
|
813
|
+
receiver = callee.object
|
|
814
|
+
if receiver is None:
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
recv_spec = None
|
|
818
|
+
if (
|
|
819
|
+
isinstance(receiver, Identifier)
|
|
820
|
+
and parameter_specs is not None
|
|
821
|
+
and receiver.name in parameter_specs
|
|
822
|
+
):
|
|
823
|
+
recv_spec = parameter_specs.get(receiver.name)
|
|
824
|
+
# An unresolved parameter still shadows any same-named global.
|
|
825
|
+
if recv_spec is None:
|
|
826
|
+
return None
|
|
827
|
+
else:
|
|
828
|
+
recv_spec = self._type_spec_from_expr(receiver)
|
|
829
|
+
if recv_spec is None or recv_spec.kind != "map":
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
if method in ("clear", "put_all"):
|
|
833
|
+
return PineType.VOID, None
|
|
834
|
+
if method == "keys":
|
|
835
|
+
return (
|
|
836
|
+
PineType.VOID,
|
|
837
|
+
TypeSpec.array(recv_spec.key or TypeSpec.primitive("string")),
|
|
838
|
+
)
|
|
839
|
+
if method == "values":
|
|
840
|
+
return (
|
|
841
|
+
PineType.VOID,
|
|
842
|
+
TypeSpec.array(recv_spec.value or TypeSpec.primitive("float")),
|
|
843
|
+
)
|
|
844
|
+
if method == "copy":
|
|
845
|
+
return PineType.VOID, recv_spec
|
|
846
|
+
if (
|
|
847
|
+
method in ("put", "get", "remove")
|
|
848
|
+
and recv_spec.value is not None
|
|
849
|
+
and recv_spec.value.kind == "primitive"
|
|
850
|
+
and recv_spec.value.name == "string"
|
|
851
|
+
):
|
|
852
|
+
return PineType.STRING, None
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
def _terminal_array_get_receiver(
|
|
856
|
+
self,
|
|
857
|
+
value: ASTNode | None,
|
|
858
|
+
) -> ASTNode | None:
|
|
859
|
+
"""Return a lexically valid receiver for a direct terminal get."""
|
|
860
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
861
|
+
value.callee, MemberAccess
|
|
862
|
+
):
|
|
863
|
+
return None
|
|
864
|
+
callee = value.callee
|
|
865
|
+
if callee.member != "get":
|
|
866
|
+
return None
|
|
867
|
+
|
|
868
|
+
functional = (
|
|
869
|
+
isinstance(callee.object, Identifier)
|
|
870
|
+
and callee.object.name == "array"
|
|
871
|
+
and self._symbols.resolve("array") is None
|
|
872
|
+
)
|
|
873
|
+
if functional:
|
|
874
|
+
if len(value.args) == 2 and not value.kwargs:
|
|
875
|
+
return value.args[0]
|
|
876
|
+
if len(value.args) == 1 and set(value.kwargs) == {"index"}:
|
|
877
|
+
return value.args[0]
|
|
878
|
+
if not value.args and set(value.kwargs) == {"id", "index"}:
|
|
879
|
+
return value.kwargs["id"]
|
|
880
|
+
return None
|
|
881
|
+
|
|
882
|
+
if len(value.args) == 1 and not value.kwargs:
|
|
883
|
+
return callee.object
|
|
884
|
+
if not value.args and set(value.kwargs) == {"index"}:
|
|
885
|
+
return callee.object
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
@staticmethod
|
|
889
|
+
def _direct_namespace_array_copy_source(
|
|
890
|
+
value: ASTNode | None,
|
|
891
|
+
) -> ASTNode | None:
|
|
892
|
+
"""Return the sole receiver of an exact ``array.copy`` call shape.
|
|
893
|
+
|
|
894
|
+
Pine v6 accepts either ``array.copy(source)`` or
|
|
895
|
+
``array.copy(id=source)``. Keep this structural helper exact so an
|
|
896
|
+
invalid duplicate receiver or an unrelated keyword stays fail closed.
|
|
897
|
+
Namespace shadowing is intentionally checked by the callers whose
|
|
898
|
+
lexical scope is still live.
|
|
899
|
+
"""
|
|
900
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
901
|
+
value.callee, MemberAccess
|
|
902
|
+
):
|
|
903
|
+
return None
|
|
904
|
+
callee = value.callee
|
|
905
|
+
if not (
|
|
906
|
+
isinstance(callee.object, Identifier)
|
|
907
|
+
and callee.object.name == "array"
|
|
908
|
+
and callee.member == "copy"
|
|
909
|
+
):
|
|
910
|
+
return None
|
|
911
|
+
if len(value.args) == 1 and not value.kwargs:
|
|
912
|
+
return value.args[0]
|
|
913
|
+
if not value.args and set(value.kwargs) == {"id"}:
|
|
914
|
+
return value.kwargs["id"]
|
|
915
|
+
return None
|
|
916
|
+
|
|
917
|
+
def _is_unshadowed_direct_array_value_producer(
|
|
918
|
+
self,
|
|
919
|
+
value: ASTNode | None,
|
|
920
|
+
) -> bool:
|
|
921
|
+
"""Whether ``value`` is one direct built-in array value producer."""
|
|
922
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
923
|
+
value.callee, MemberAccess
|
|
924
|
+
):
|
|
925
|
+
return False
|
|
926
|
+
callee = value.callee
|
|
927
|
+
namespace_producer = (
|
|
928
|
+
isinstance(callee.object, Identifier)
|
|
929
|
+
and callee.object.name == "array"
|
|
930
|
+
and self._symbols.resolve("array") is None
|
|
931
|
+
and callee.member in _DIRECT_ARRAY_VALUE_PRODUCERS
|
|
932
|
+
)
|
|
933
|
+
if namespace_producer:
|
|
934
|
+
if callee.member != "copy":
|
|
935
|
+
return True
|
|
936
|
+
source = self._direct_namespace_array_copy_source(value)
|
|
937
|
+
if source is None:
|
|
938
|
+
return False
|
|
939
|
+
if isinstance(source, Identifier):
|
|
940
|
+
source_spec = self._type_spec_from_expr(source)
|
|
941
|
+
return source_spec is not None and source_spec.kind == "array"
|
|
942
|
+
return self._is_unshadowed_direct_array_value_producer(source)
|
|
943
|
+
if callee.member != "copy" or value.args or value.kwargs:
|
|
944
|
+
return False
|
|
945
|
+
if isinstance(callee.object, Identifier):
|
|
946
|
+
receiver_spec = self._type_spec_from_expr(callee.object)
|
|
947
|
+
return receiver_spec is not None and receiver_spec.kind == "array"
|
|
948
|
+
# A no-argument method copy preserves the value type of an existing
|
|
949
|
+
# direct producer. Recurse only through that already-bounded shape;
|
|
950
|
+
# arbitrary UDF/slice/map receivers remain excluded.
|
|
951
|
+
return self._is_unshadowed_direct_array_value_producer(callee.object)
|
|
952
|
+
|
|
953
|
+
def _direct_array_value_spec_without_visiting(
|
|
954
|
+
self,
|
|
955
|
+
value: ASTNode | None,
|
|
956
|
+
) -> TypeSpec | None:
|
|
957
|
+
"""Exact direct-producer spec without re-visiting a nested call."""
|
|
958
|
+
cached = self._cached_direct_array_value_spec(value)
|
|
959
|
+
if cached is not None:
|
|
960
|
+
return cached
|
|
961
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
962
|
+
value.callee, MemberAccess
|
|
963
|
+
):
|
|
964
|
+
return None
|
|
965
|
+
callee = value.callee
|
|
966
|
+
source: ASTNode | None = None
|
|
967
|
+
if (
|
|
968
|
+
isinstance(callee.object, Identifier)
|
|
969
|
+
and callee.object.name == "array"
|
|
970
|
+
and self._symbols.resolve("array") is None
|
|
971
|
+
and callee.member == "copy"
|
|
972
|
+
):
|
|
973
|
+
candidate = self._direct_namespace_array_copy_source(value)
|
|
974
|
+
if isinstance(candidate, Identifier):
|
|
975
|
+
source = candidate
|
|
976
|
+
elif callee.member == "copy" and isinstance(
|
|
977
|
+
callee.object, Identifier
|
|
978
|
+
):
|
|
979
|
+
source = callee.object
|
|
980
|
+
if source is None:
|
|
981
|
+
return None
|
|
982
|
+
spec = self._type_spec_from_expr(source)
|
|
983
|
+
return spec if spec is not None and spec.kind == "array" else None
|
|
984
|
+
|
|
985
|
+
def _terminal_array_get_uses_direct_temporary(
|
|
986
|
+
self,
|
|
987
|
+
value: ASTNode | None,
|
|
988
|
+
) -> bool:
|
|
989
|
+
"""Capture the narrow temporary shape while lexical scope is live."""
|
|
990
|
+
receiver = self._terminal_array_get_receiver(value)
|
|
991
|
+
return (
|
|
992
|
+
receiver is not None
|
|
993
|
+
and not isinstance(receiver, Identifier)
|
|
994
|
+
and self._is_unshadowed_direct_array_value_producer(receiver)
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
def _cached_primitive_expr_spec(
|
|
998
|
+
self,
|
|
999
|
+
value: ASTNode | None,
|
|
1000
|
+
) -> TypeSpec | None:
|
|
1001
|
+
"""Resolve a primitive without visiting calls or lexical symbols.
|
|
1002
|
+
|
|
1003
|
+
This helper exists only for the bounded forward-order reconciliation
|
|
1004
|
+
of direct temporary array producers. Calling ``_visit`` here would
|
|
1005
|
+
allocate stateful UDF call sites a second time.
|
|
1006
|
+
"""
|
|
1007
|
+
if isinstance(value, NumberLiteral):
|
|
1008
|
+
return TypeSpec.primitive(
|
|
1009
|
+
"float" if isinstance(value.value, float) else "int"
|
|
1010
|
+
)
|
|
1011
|
+
if isinstance(value, BoolLiteral):
|
|
1012
|
+
return TypeSpec.primitive("bool")
|
|
1013
|
+
if isinstance(value, StringLiteral):
|
|
1014
|
+
return TypeSpec.primitive("string")
|
|
1015
|
+
if isinstance(value, UnaryOp) and value.op in ("+", "-"):
|
|
1016
|
+
return self._cached_primitive_expr_spec(value.operand)
|
|
1017
|
+
if isinstance(value, FuncCall) and isinstance(value.callee, Identifier):
|
|
1018
|
+
name = value.callee.name
|
|
1019
|
+
spec = getattr(self, "_func_return_type_specs", {}).get(name)
|
|
1020
|
+
if spec is not None and spec.kind == "primitive":
|
|
1021
|
+
return spec
|
|
1022
|
+
pine_type = getattr(self, "_func_return_types", {}).get(name)
|
|
1023
|
+
primitive = {
|
|
1024
|
+
PineType.INT: "int",
|
|
1025
|
+
PineType.FLOAT: "float",
|
|
1026
|
+
PineType.BOOL: "bool",
|
|
1027
|
+
PineType.STRING: "string",
|
|
1028
|
+
PineType.COLOR: "color",
|
|
1029
|
+
}.get(pine_type)
|
|
1030
|
+
if primitive is not None:
|
|
1031
|
+
return TypeSpec.primitive(primitive)
|
|
1032
|
+
return None
|
|
1033
|
+
|
|
1034
|
+
def _cached_direct_array_value_spec(
|
|
1035
|
+
self,
|
|
1036
|
+
value: ASTNode | None,
|
|
1037
|
+
) -> TypeSpec | None:
|
|
1038
|
+
"""Resolve one registered direct producer without analyzer effects."""
|
|
1039
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
1040
|
+
value.callee, MemberAccess
|
|
1041
|
+
):
|
|
1042
|
+
return None
|
|
1043
|
+
callee = value.callee
|
|
1044
|
+
if callee.member == "copy" and not value.args and not value.kwargs:
|
|
1045
|
+
# Method syntax carries its source in the callee object rather
|
|
1046
|
+
# than in ``args``. Peel only a direct producer and stay entirely
|
|
1047
|
+
# on cached metadata so a stateful element call is never revisited.
|
|
1048
|
+
return self._cached_direct_array_value_spec(callee.object)
|
|
1049
|
+
if not (
|
|
1050
|
+
isinstance(callee.object, Identifier)
|
|
1051
|
+
and callee.object.name == "array"
|
|
1052
|
+
and callee.member in _DIRECT_ARRAY_VALUE_PRODUCERS
|
|
1053
|
+
):
|
|
1054
|
+
return None
|
|
1055
|
+
|
|
1056
|
+
producer = callee.member
|
|
1057
|
+
typed = {
|
|
1058
|
+
"new_float": "float",
|
|
1059
|
+
"new_int": "int",
|
|
1060
|
+
"new_bool": "bool",
|
|
1061
|
+
"new_string": "string",
|
|
1062
|
+
}.get(producer)
|
|
1063
|
+
if typed is not None:
|
|
1064
|
+
return TypeSpec.array(TypeSpec.primitive(typed))
|
|
1065
|
+
if producer == "new":
|
|
1066
|
+
targs = self._template_args_from_call(value)
|
|
1067
|
+
if targs:
|
|
1068
|
+
element = self._type_spec_from_hint(targs[0])
|
|
1069
|
+
if element is None or element.kind != "primitive":
|
|
1070
|
+
return None
|
|
1071
|
+
return TypeSpec.array(element)
|
|
1072
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
1073
|
+
if producer == "from" and value.args:
|
|
1074
|
+
element = self._cached_primitive_expr_spec(value.args[0])
|
|
1075
|
+
return TypeSpec.array(element) if element is not None else None
|
|
1076
|
+
if producer == "copy":
|
|
1077
|
+
source = self._direct_namespace_array_copy_source(value)
|
|
1078
|
+
if source is not None:
|
|
1079
|
+
return self._cached_direct_array_value_spec(source)
|
|
1080
|
+
return None
|
|
1081
|
+
|
|
1082
|
+
def _cached_terminal_temporary_array_get_spec(
|
|
1083
|
+
self,
|
|
1084
|
+
value: ASTNode | None,
|
|
1085
|
+
) -> TypeSpec | None:
|
|
1086
|
+
"""Side-effect-free element spec for a preregistered temporary get."""
|
|
1087
|
+
if not isinstance(value, FuncCall) or not isinstance(
|
|
1088
|
+
value.callee, MemberAccess
|
|
1089
|
+
):
|
|
1090
|
+
return None
|
|
1091
|
+
callee = value.callee
|
|
1092
|
+
if callee.member != "get":
|
|
1093
|
+
return None
|
|
1094
|
+
if isinstance(callee.object, Identifier) and callee.object.name == "array":
|
|
1095
|
+
if len(value.args) == 2 and not value.kwargs:
|
|
1096
|
+
receiver = value.args[0]
|
|
1097
|
+
elif len(value.args) == 1 and set(value.kwargs) == {"index"}:
|
|
1098
|
+
receiver = value.args[0]
|
|
1099
|
+
elif not value.args and set(value.kwargs) == {"id", "index"}:
|
|
1100
|
+
receiver = value.kwargs["id"]
|
|
1101
|
+
else:
|
|
1102
|
+
return None
|
|
1103
|
+
elif len(value.args) == 1 and not value.kwargs:
|
|
1104
|
+
receiver = callee.object
|
|
1105
|
+
elif not value.args and set(value.kwargs) == {"index"}:
|
|
1106
|
+
receiver = callee.object
|
|
1107
|
+
else:
|
|
1108
|
+
return None
|
|
1109
|
+
|
|
1110
|
+
receiver_spec = self._cached_direct_array_value_spec(receiver)
|
|
1111
|
+
if (
|
|
1112
|
+
receiver_spec is None
|
|
1113
|
+
or receiver_spec.kind != "array"
|
|
1114
|
+
or receiver_spec.element is None
|
|
1115
|
+
or receiver_spec.element.kind != "primitive"
|
|
1116
|
+
):
|
|
1117
|
+
return None
|
|
1118
|
+
return receiver_spec.element
|
|
1119
|
+
|
|
1120
|
+
def _terminal_array_get_return(
|
|
1121
|
+
self,
|
|
1122
|
+
value: ASTNode | None,
|
|
1123
|
+
parameter_specs: dict[str, TypeSpec | None] | None = None,
|
|
1124
|
+
resolved_return_spec: TypeSpec | None = None,
|
|
1125
|
+
) -> tuple[PineType, TypeSpec] | None:
|
|
1126
|
+
"""Return exact metadata for a direct terminal ``array.get`` call.
|
|
1127
|
+
|
|
1128
|
+
The regular expression ``TypeSpec`` pass already knows the element
|
|
1129
|
+
type of both ``array.get(values, index)`` and ``values.get(index)``.
|
|
1130
|
+
The coarse visitor, however, deliberately returns ``VOID`` for most
|
|
1131
|
+
array methods. When such a call is the final expression of a UDF,
|
|
1132
|
+
that ``VOID`` used to make codegen emit a ``double`` return type even
|
|
1133
|
+
for primitive elements such as strings, booleans, and integers.
|
|
1134
|
+
|
|
1135
|
+
Keep the refinement intentionally narrow: only the established
|
|
1136
|
+
positional/keyword shapes of ``get`` participate, and the receiver
|
|
1137
|
+
must resolve to an exact array ``TypeSpec``. In addition to exact
|
|
1138
|
+
identifiers, direct built-in array producers (``array.from/new/copy``)
|
|
1139
|
+
may use the terminal expression spec that the caller already captured
|
|
1140
|
+
while the lexical scope was active. Reusing that snapshot is
|
|
1141
|
+
load-bearing: re-inferring an effectful temporary here would revisit
|
|
1142
|
+
nested calls and mint phantom call-site state. Other array accessors,
|
|
1143
|
+
mutations, range/view semantics, arbitrary UDF receivers,
|
|
1144
|
+
reference/ID-like elements, and unresolved receivers remain on their
|
|
1145
|
+
existing paths. Returning UDTs or nested collections by value would
|
|
1146
|
+
lose Pine reference identity even when the generated C++ happens to
|
|
1147
|
+
compile, so this helper must not expose those specs as an apparent fix.
|
|
1148
|
+
"""
|
|
1149
|
+
receiver = self._terminal_array_get_receiver(value)
|
|
1150
|
+
if receiver is None:
|
|
1151
|
+
return None
|
|
1152
|
+
if not isinstance(receiver, Identifier):
|
|
1153
|
+
# Both functional and method forms stay inside the same narrow
|
|
1154
|
+
# contract. A temporary UDF, slice/view, map result, or a local
|
|
1155
|
+
# binding merely named ``array`` must not enter this refinement.
|
|
1156
|
+
if not self._is_unshadowed_direct_array_value_producer(receiver):
|
|
1157
|
+
return None
|
|
1158
|
+
if resolved_return_spec is None:
|
|
1159
|
+
producer_spec = self._direct_array_value_spec_without_visiting(
|
|
1160
|
+
receiver
|
|
1161
|
+
)
|
|
1162
|
+
if producer_spec is not None:
|
|
1163
|
+
resolved_return_spec = producer_spec.element
|
|
1164
|
+
if (
|
|
1165
|
+
resolved_return_spec is None
|
|
1166
|
+
or resolved_return_spec.kind != "primitive"
|
|
1167
|
+
):
|
|
1168
|
+
return None
|
|
1169
|
+
return_type = self._element_pine_type(resolved_return_spec)
|
|
1170
|
+
if return_type == PineType.VOID:
|
|
1171
|
+
return None
|
|
1172
|
+
return return_type, resolved_return_spec
|
|
1173
|
+
|
|
1174
|
+
recv_spec = None
|
|
1175
|
+
if parameter_specs is not None and receiver.name in parameter_specs:
|
|
1176
|
+
recv_spec = parameter_specs.get(receiver.name)
|
|
1177
|
+
# An unresolved parameter shadows any same-named global.
|
|
1178
|
+
if recv_spec is None:
|
|
1179
|
+
return None
|
|
1180
|
+
else:
|
|
1181
|
+
recv_spec = self._type_spec_from_expr(receiver)
|
|
1182
|
+
if (
|
|
1183
|
+
recv_spec is None
|
|
1184
|
+
or recv_spec.kind != "array"
|
|
1185
|
+
or recv_spec.element is None
|
|
1186
|
+
):
|
|
1187
|
+
return None
|
|
1188
|
+
|
|
1189
|
+
element_spec = recv_spec.element
|
|
1190
|
+
if element_spec.kind != "primitive":
|
|
1191
|
+
return None
|
|
1192
|
+
return_type = self._element_pine_type(element_spec)
|
|
1193
|
+
if return_type == PineType.VOID:
|
|
1194
|
+
return None
|
|
1195
|
+
return return_type, element_spec
|
|
1196
|
+
|
|
260
1197
|
@staticmethod
|
|
261
1198
|
def _pine_type_to_spec(pine_type: PineType) -> TypeSpec:
|
|
262
1199
|
mapping = {
|