@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -33,18 +33,35 @@ tables it needs come from ``codegen/tables.py``.
|
|
|
33
33
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
|
+
import contextlib
|
|
37
|
+
import re
|
|
38
|
+
|
|
36
39
|
from ..ast_nodes import (
|
|
37
|
-
ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall, FuncDef,
|
|
41
|
+
Identifier, IfStmt,
|
|
42
|
+
MemberAccess, MethodDef, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
|
|
43
|
+
Subscript, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
|
|
40
44
|
)
|
|
45
|
+
from ..errors import Phase
|
|
46
|
+
from ..external_requests import UNPINNED_ANNOTATION
|
|
47
|
+
from ..limits import iter_ast_nodes
|
|
41
48
|
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
49
|
+
from .helpers import (
|
|
50
|
+
NA_PRESERVING_INT_TYPES,
|
|
51
|
+
evaluate_args_once,
|
|
52
|
+
na_preserving_int_cast,
|
|
53
|
+
pine_truth_cast,
|
|
54
|
+
)
|
|
42
55
|
from .. import signatures as sigs
|
|
43
56
|
from .tables import (
|
|
57
|
+
ALERT_FREQ_VALUES,
|
|
58
|
+
ARRAY_ARGS_READ_REPEATEDLY,
|
|
44
59
|
ARRAY_DRAWING_NEW_CTORS,
|
|
45
60
|
ARRAY_METHODS,
|
|
61
|
+
V5_ARRAY_INDEX_METHODS,
|
|
46
62
|
BAR_BUILTINS,
|
|
47
63
|
BAR_FIELDS,
|
|
64
|
+
COLOR_CONST_MAP,
|
|
48
65
|
DRAWING_NS,
|
|
49
66
|
DRAWING_TYPE_TO_CPP,
|
|
50
67
|
MATRIX_RETURNING_METHODS,
|
|
@@ -71,6 +88,18 @@ COLLECTION_MUTATING_METHODS = frozenset({
|
|
|
71
88
|
})
|
|
72
89
|
|
|
73
90
|
|
|
91
|
+
# Binary operators whose emitted C++ result is a ``bool``, never a number.
|
|
92
|
+
# Pine's ``and``/``or`` lower to ``&&``/``||`` and the relational operators go
|
|
93
|
+
# through the na-aware comparison lambda, which returns ``bool``.
|
|
94
|
+
_BOOL_RESULT_BINOPS = frozenset({
|
|
95
|
+
"==", "!=", "<", ">", "<=", ">=", "and", "or", "&&", "||",
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Array methods whose result is a new array (a vector of doubles, as their
|
|
100
|
+
# ``ARRAY_METHODS`` templates emit it).
|
|
101
|
+
ARRAY_RESULT_METHODS = frozenset({"abs", "standardize", "sort_indices"})
|
|
102
|
+
|
|
74
103
|
class TypeInferer:
|
|
75
104
|
"""Type-spec / C++-type inference helpers shared across visitor mixins.
|
|
76
105
|
|
|
@@ -137,14 +166,15 @@ class TypeInferer:
|
|
|
137
166
|
if spec.kind == "primitive":
|
|
138
167
|
return {"float": "double", "int": "int", "int64": "int64_t",
|
|
139
168
|
"bool": "bool", "string": "std::string",
|
|
140
|
-
"color": "
|
|
169
|
+
"color": "int64_t"}.get(spec.name or "float", "double")
|
|
141
170
|
if spec.kind == "udt" and spec.name:
|
|
142
171
|
# Drawing handle structs (P1): map BEFORE the _udt_defs check so
|
|
143
172
|
# array<line> -> std::vector<Line> and scalar line -> Line instead
|
|
144
173
|
# of the old collapse to double / unknown-type-name.
|
|
145
174
|
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
146
175
|
return DRAWING_TYPE_TO_CPP[spec.name]
|
|
147
|
-
return
|
|
176
|
+
return (self._safe_name(spec.name)
|
|
177
|
+
if spec.name in self._udt_defs else "double")
|
|
148
178
|
if spec.kind == "array":
|
|
149
179
|
return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
|
|
150
180
|
if spec.kind == "map":
|
|
@@ -172,7 +202,7 @@ class TypeInferer:
|
|
|
172
202
|
"""Generated arena specialization for one user-defined object type."""
|
|
173
203
|
return (
|
|
174
204
|
f"{getattr(self, '_udt_arena_template_cpp_name', '_PFUdtArena')}<"
|
|
175
|
-
f"{type_name}, "
|
|
205
|
+
f"{self._safe_name(type_name)}, "
|
|
176
206
|
f"{self._udt_record_cpp_type(type_name)}>"
|
|
177
207
|
)
|
|
178
208
|
|
|
@@ -182,6 +212,227 @@ class TypeInferer:
|
|
|
182
212
|
self, "_checkpoint_traits_cpp_name", "_PFCheckpointTraits"
|
|
183
213
|
)
|
|
184
214
|
|
|
215
|
+
def _udt_array_field_cpp(self, spec: TypeSpec | None,
|
|
216
|
+
type_name: str | None = None) -> str | None:
|
|
217
|
+
"""The record type of an array field of the UDT ``type_name``
|
|
218
|
+
(``_PFArrayField<T>``, base.UDT_ARRAY_FIELD_CPP) when the type is
|
|
219
|
+
bar-local (``_udt_bar_local_types``), else None: its field stays the
|
|
220
|
+
``std::vector<T>`` copy every earlier build stored."""
|
|
221
|
+
if spec is None or spec.kind != "array" or spec.element is None:
|
|
222
|
+
return None
|
|
223
|
+
if type_name not in self._udt_bar_local_types():
|
|
224
|
+
return None
|
|
225
|
+
return f"_PFArrayField<{self._type_spec_to_cpp(spec.element)}>"
|
|
226
|
+
|
|
227
|
+
def _udt_has_array_fields(self) -> bool:
|
|
228
|
+
"""Whether any emitted UDT record holds a ``_PFArrayField``."""
|
|
229
|
+
return any(
|
|
230
|
+
self._udt_array_field_cpp(spec, type_name) is not None
|
|
231
|
+
for type_name, specs in self._udt_field_type_specs.items()
|
|
232
|
+
if type_name in self._udt_defs
|
|
233
|
+
for name, spec in specs.items()
|
|
234
|
+
if name not in self._udt_omitted_fields.get(type_name, set())
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def _udt_array_field_target(self, node) -> str | None:
|
|
238
|
+
"""The ``_PFArrayField<T>`` type of the UDT field ``node`` reads, when
|
|
239
|
+
it is one, else None."""
|
|
240
|
+
if not isinstance(node, MemberAccess) or self._is_omitted_udt_field(node):
|
|
241
|
+
return None
|
|
242
|
+
owner_spec = self._type_spec_from_expr(node.object)
|
|
243
|
+
if (owner_spec is None or owner_spec.kind != "udt"
|
|
244
|
+
or owner_spec.name not in self._udt_defs):
|
|
245
|
+
return None
|
|
246
|
+
return self._udt_array_field_cpp(
|
|
247
|
+
self._udt_field_type_specs.get(owner_spec.name, {}).get(node.member),
|
|
248
|
+
owner_spec.name)
|
|
249
|
+
|
|
250
|
+
# Builtin namespaces whose calls never return a user object.
|
|
251
|
+
_NO_USER_OBJECT_NAMESPACES = frozenset({
|
|
252
|
+
"ta", "math", "str", "color", "input", "timeframe", "syminfo",
|
|
253
|
+
"strategy", "table", "line", "label", "box", "linefill", "polyline",
|
|
254
|
+
"chart", "session", "ticker", "log", "runtime", "alert",
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
_PRIMITIVE_RESULTS = frozenset({
|
|
258
|
+
PineType.INT, PineType.FLOAT, PineType.BOOL, PineType.STRING,
|
|
259
|
+
PineType.COLOR,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
def _may_hold_user_object(self, expr, seen: frozenset = frozenset()) -> bool:
|
|
263
|
+
"""Whether ``expr`` can evaluate to a user-defined object: not a
|
|
264
|
+
literal, an operator's result, a builtin that returns a primitive or
|
|
265
|
+
a drawing, or a user function whose result is one of those (its UDT
|
|
266
|
+
or drawing result, a primitive result type, else what its last
|
|
267
|
+
expression can hold: ``pick(o) => o`` can return an object)."""
|
|
268
|
+
if expr is None or isinstance(expr, (
|
|
269
|
+
NumberLiteral, StringLiteral, BoolLiteral, ColorLiteral,
|
|
270
|
+
NaLiteral, UnaryOp, BinOp)):
|
|
271
|
+
return False
|
|
272
|
+
if isinstance(expr, FuncCall):
|
|
273
|
+
func_name, namespace = self._resolve_callee(expr.callee)
|
|
274
|
+
if namespace is None and func_name in self._func_info_map:
|
|
275
|
+
info = self._func_info_map[func_name]
|
|
276
|
+
returned = getattr(info, "udt_return_type", None)
|
|
277
|
+
if returned is not None:
|
|
278
|
+
return returned in self._udt_defs
|
|
279
|
+
if getattr(info, "return_type", None) in self._PRIMITIVE_RESULTS:
|
|
280
|
+
return False
|
|
281
|
+
body = getattr(getattr(info, "node", None), "body", None) or ()
|
|
282
|
+
if not body or func_name in seen:
|
|
283
|
+
return True
|
|
284
|
+
terminal = body[-1]
|
|
285
|
+
if isinstance(terminal, ExprStmt):
|
|
286
|
+
terminal = terminal.expr
|
|
287
|
+
return self._may_hold_user_object(terminal, seen | {func_name})
|
|
288
|
+
return namespace not in self._NO_USER_OBJECT_NAMESPACES
|
|
289
|
+
if isinstance(expr, Ternary):
|
|
290
|
+
return (self._may_hold_user_object(expr.true_val, seen)
|
|
291
|
+
or self._may_hold_user_object(expr.false_val, seen))
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
def _udt_bar_local_types(self) -> set[str]:
|
|
295
|
+
"""User types none of whose objects outlives the bar that creates it.
|
|
296
|
+
|
|
297
|
+
TradingView keeps a ``var`` array's value per bar: an object kept from
|
|
298
|
+
an earlier bar reads the array as it was then, while one built this
|
|
299
|
+
bar holds the array itself (lab tv pf-cgs2-udt-array-fields: a push
|
|
300
|
+
through ``h.xs`` reaches ``a``; a record kept from the first bar reads
|
|
301
|
+
one element forever). An object nothing keeps past its bar can alias
|
|
302
|
+
the array it is built from, which is exact and copies nothing; any
|
|
303
|
+
other object keeps the copy every earlier build stored. A type escapes
|
|
304
|
+
its bar when a ``var`` / ``varip`` declaration can hold one, a
|
|
305
|
+
collection's element type or another type's field is it, a history
|
|
306
|
+
read reads one, or a ``var`` declaration of a type this cannot tell
|
|
307
|
+
exists. Cached."""
|
|
308
|
+
cached = getattr(self, "_udt_bar_local_cache", None)
|
|
309
|
+
if cached is not None:
|
|
310
|
+
return cached
|
|
311
|
+
types = set(self._udt_defs)
|
|
312
|
+
self._udt_bar_local_cache = set()
|
|
313
|
+
escaping: set[str] = set()
|
|
314
|
+
|
|
315
|
+
def udt_of_hint(hint) -> set[str]:
|
|
316
|
+
if not hint:
|
|
317
|
+
return set()
|
|
318
|
+
return {name for name in types
|
|
319
|
+
if re.search(rf"(?<![A-Za-z0-9_.]){re.escape(name)}(?![A-Za-z0-9_])",
|
|
320
|
+
str(hint))}
|
|
321
|
+
|
|
322
|
+
def udt_of_spec(spec) -> set[str]:
|
|
323
|
+
found = set()
|
|
324
|
+
pending = [spec]
|
|
325
|
+
while pending:
|
|
326
|
+
current = pending.pop()
|
|
327
|
+
if current is None:
|
|
328
|
+
continue
|
|
329
|
+
if current.kind == "udt" and current.name in types:
|
|
330
|
+
found.add(current.name)
|
|
331
|
+
pending.extend((current.element, current.key, current.value))
|
|
332
|
+
return found
|
|
333
|
+
|
|
334
|
+
for type_name, fields in self._udt_defs.items():
|
|
335
|
+
for field in fields or ():
|
|
336
|
+
escaping |= udt_of_hint(getattr(field, "type_name", None))
|
|
337
|
+
ast = getattr(self.ctx, "ast", None)
|
|
338
|
+
for node in self._walk_ast(ast):
|
|
339
|
+
if isinstance(node, VarDecl):
|
|
340
|
+
hint = str(node.type_hint or "")
|
|
341
|
+
if any(mark in hint for mark in ("<", "[")):
|
|
342
|
+
# A collection of objects keeps them past their bar.
|
|
343
|
+
escaping |= udt_of_hint(hint)
|
|
344
|
+
if not (node.is_var or node.is_varip):
|
|
345
|
+
continue
|
|
346
|
+
if hint:
|
|
347
|
+
escaping |= udt_of_hint(hint)
|
|
348
|
+
continue
|
|
349
|
+
try:
|
|
350
|
+
spec = self._type_spec_from_expr(node.value)
|
|
351
|
+
except Exception as exc: # an unresolvable value may hold any type
|
|
352
|
+
if getattr(exc, "limit", False):
|
|
353
|
+
raise
|
|
354
|
+
spec = None
|
|
355
|
+
if spec is None and isinstance(node.value, FuncCall):
|
|
356
|
+
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
357
|
+
info = (self._func_info_map.get(func_name)
|
|
358
|
+
if namespace is None else None)
|
|
359
|
+
returned = getattr(info, "udt_return_type", None)
|
|
360
|
+
if returned in types:
|
|
361
|
+
escaping.add(returned)
|
|
362
|
+
continue
|
|
363
|
+
if spec is None and self._may_hold_user_object(node.value):
|
|
364
|
+
return self._udt_bar_local_cache
|
|
365
|
+
escaping |= udt_of_spec(spec)
|
|
366
|
+
elif isinstance(node, FuncCall):
|
|
367
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
368
|
+
if namespace in ("array", "map", "matrix") or func_name in (
|
|
369
|
+
"push", "unshift", "insert", "set", "fill", "put"):
|
|
370
|
+
for template in self._template_args_from_call(node) or ():
|
|
371
|
+
escaping |= udt_of_hint(template)
|
|
372
|
+
for arg in [*node.args, *node.kwargs.values()]:
|
|
373
|
+
try:
|
|
374
|
+
escaping |= udt_of_spec(self._type_spec_from_expr(arg))
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
if getattr(exc, "limit", False):
|
|
377
|
+
raise
|
|
378
|
+
continue
|
|
379
|
+
elif isinstance(node, Subscript):
|
|
380
|
+
try:
|
|
381
|
+
escaping |= udt_of_spec(self._type_spec_from_expr(node.object))
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
if getattr(exc, "limit", False):
|
|
384
|
+
raise
|
|
385
|
+
continue
|
|
386
|
+
self._udt_bar_local_cache = types - escaping
|
|
387
|
+
return self._udt_bar_local_cache
|
|
388
|
+
|
|
389
|
+
def _stable_var_array_names(self) -> set[str]:
|
|
390
|
+
"""Top-level ``var`` arrays no statement rebinds (``:=``): one array
|
|
391
|
+
object for the whole run, whose member storage keeps that identity,
|
|
392
|
+
so a UDT field can alias it. Cached."""
|
|
393
|
+
cached = getattr(self, "_stable_var_array_cache", None)
|
|
394
|
+
if cached is not None:
|
|
395
|
+
return cached
|
|
396
|
+
ast = getattr(self.ctx, "ast", None)
|
|
397
|
+
top_level_vars = {
|
|
398
|
+
stmt.name for stmt in getattr(ast, "body", None) or ()
|
|
399
|
+
if isinstance(stmt, VarDecl) and stmt.is_var and not stmt.is_varip
|
|
400
|
+
}
|
|
401
|
+
declarations: dict[str, int] = {}
|
|
402
|
+
rebound: set[str] = set()
|
|
403
|
+
for node in self._walk_ast(ast):
|
|
404
|
+
if isinstance(node, VarDecl):
|
|
405
|
+
declarations[node.name] = declarations.get(node.name, 0) + 1
|
|
406
|
+
elif (isinstance(node, Assignment)
|
|
407
|
+
and isinstance(node.target, Identifier)):
|
|
408
|
+
rebound.add(node.target.name)
|
|
409
|
+
names = set()
|
|
410
|
+
for name in top_level_vars:
|
|
411
|
+
# Declared once in the whole program, so no local shadows it.
|
|
412
|
+
if declarations.get(name) != 1 or name in rebound:
|
|
413
|
+
continue
|
|
414
|
+
spec = self._collection_spec_for_name_raw(name)
|
|
415
|
+
if spec is not None and spec.kind == "array":
|
|
416
|
+
names.add(name)
|
|
417
|
+
self._stable_var_array_cache = names
|
|
418
|
+
return names
|
|
419
|
+
|
|
420
|
+
def _udt_array_field_value(self, field_cpp: str, value_node, value_cpp: str) -> str:
|
|
421
|
+
"""The ``_PFArrayField<T>`` a UDT array field takes from ``value_node``:
|
|
422
|
+
an alias of a stable script ``var`` array (``_stable_var_array_names``)
|
|
423
|
+
read at the script's level of names, else the value itself, moved or
|
|
424
|
+
copied in (the snapshot every earlier build stored)."""
|
|
425
|
+
name = value_node.name if isinstance(value_node, Identifier) else None
|
|
426
|
+
if (name is not None
|
|
427
|
+
and name in self._stable_var_array_names()
|
|
428
|
+
and name not in (getattr(self, "_current_func_param_types", {}) or {})
|
|
429
|
+
and name not in (getattr(self, "_current_func_local_types", {}) or {})
|
|
430
|
+
and name not in (getattr(self, "_current_func_locals", ()) or ())
|
|
431
|
+
and name not in (getattr(self, "_current_loop_vars", ()) or ())
|
|
432
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
433
|
+
return f"{field_cpp}::alias({value_cpp})"
|
|
434
|
+
return f"{field_cpp}({value_cpp})"
|
|
435
|
+
|
|
185
436
|
def _udt_direct_array_fields(self, type_name: str) -> tuple[str, ...]:
|
|
186
437
|
"""Direct array-valued fields whose UDT copy semantics are unsupported.
|
|
187
438
|
|
|
@@ -261,7 +512,7 @@ class TypeInferer:
|
|
|
261
512
|
# (Line{} = na handle), NOT the lowercase Pine name (line{}).
|
|
262
513
|
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
263
514
|
return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
|
|
264
|
-
return f"{spec.name}{{}}"
|
|
515
|
+
return f"{self._safe_name(spec.name)}{{}}"
|
|
265
516
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
266
517
|
if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
|
|
267
518
|
return f"{cpp_type}()"
|
|
@@ -294,8 +545,14 @@ class TypeInferer:
|
|
|
294
545
|
def _int_fits_int32(value: int) -> bool:
|
|
295
546
|
return -(1 << 31) <= value < (1 << 31)
|
|
296
547
|
|
|
297
|
-
|
|
298
|
-
|
|
548
|
+
@staticmethod
|
|
549
|
+
def _int_fits_int64(value: int) -> bool:
|
|
550
|
+
return -(1 << 63) <= value < (1 << 63)
|
|
551
|
+
|
|
552
|
+
def _pure_int_literal_value(self, node, names: bool = True,
|
|
553
|
+
global_scope: bool = False) -> int | None:
|
|
554
|
+
"""Exact value of an expression built only from int literals, the
|
|
555
|
+
int constants ``_visit_ident`` inlines as literals (``names``), and
|
|
299
556
|
``+ - *`` / unary minus, else ``None``.
|
|
300
557
|
|
|
301
558
|
Pine ``int`` is 64-bit: ``90 * 24 * 60 * 60 * 1000`` (three months in
|
|
@@ -303,21 +560,30 @@ class TypeInferer:
|
|
|
303
560
|
literal arithmetic the same product overflows (wraps to -813 934 592),
|
|
304
561
|
so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8
|
|
305
562
|
family U: latibonit15 execution-signals-confluence, six lanes; lab tv
|
|
306
|
-
u-lati-levels-nq15 vs the engine, 2026-09-05).
|
|
307
|
-
|
|
563
|
+
u-lati-levels-nq15 vs the engine, 2026-09-05). ``400 * MS`` over
|
|
564
|
+
``const int MS = 7200000`` is 2 880 000 000 too (lab tv
|
|
565
|
+
pf-cgs-int64-const, 2026-09-28): the constant is emitted as its
|
|
566
|
+
literal, so the product was the same C++ ``int`` overflow. Folding
|
|
567
|
+
the subtree in Python keeps the exact value; the caller emits it as a
|
|
308
568
|
64-bit literal only when it does not fit ``int32`` so every in-range
|
|
309
|
-
expression is byte-identical to before.
|
|
569
|
+
expression is byte-identical to before. ``global_scope`` reads names
|
|
570
|
+
as the script's top level binds them, whatever is being emitted.
|
|
310
571
|
"""
|
|
311
572
|
if isinstance(node, NumberLiteral):
|
|
312
573
|
return node.value if isinstance(node.value, int) and not isinstance(node.value, bool) else None
|
|
574
|
+
if isinstance(node, Identifier):
|
|
575
|
+
if not names:
|
|
576
|
+
return None
|
|
577
|
+
return (self._global_int_constant(node.name) if global_scope
|
|
578
|
+
else self._inlined_int_constant(node))
|
|
313
579
|
if isinstance(node, UnaryOp) and node.op == "-":
|
|
314
|
-
inner = self._pure_int_literal_value(node.operand)
|
|
580
|
+
inner = self._pure_int_literal_value(node.operand, names, global_scope)
|
|
315
581
|
return -inner if inner is not None else None
|
|
316
582
|
if isinstance(node, BinOp) and node.op in ("+", "-", "*"):
|
|
317
|
-
left = self._pure_int_literal_value(node.left)
|
|
583
|
+
left = self._pure_int_literal_value(node.left, names, global_scope)
|
|
318
584
|
if left is None:
|
|
319
585
|
return None
|
|
320
|
-
right = self._pure_int_literal_value(node.right)
|
|
586
|
+
right = self._pure_int_literal_value(node.right, names, global_scope)
|
|
321
587
|
if right is None:
|
|
322
588
|
return None
|
|
323
589
|
if node.op == "+":
|
|
@@ -327,10 +593,528 @@ class TypeInferer:
|
|
|
327
593
|
return left * right
|
|
328
594
|
return None
|
|
329
595
|
|
|
330
|
-
def
|
|
331
|
-
|
|
596
|
+
def _inlined_int_constant(self, node: Identifier) -> int | None:
|
|
597
|
+
"""The int a name is emitted as when ``_visit_ident`` inlines it as a
|
|
598
|
+
literal (a never-reassigned constant: ``const int MS = 7200000``,
|
|
599
|
+
``int step = 2 * 60 * 60 * 1000``), else None. Every read that
|
|
600
|
+
``_visit_expr`` / ``_visit_ident`` renders otherwise is None: a
|
|
601
|
+
dropped no-data request, a ``request.security`` fallback frame, a
|
|
602
|
+
parameter, a bar field or built-in, an input, and a local that
|
|
603
|
+
shadows the constant."""
|
|
604
|
+
if node.annotations and UNPINNED_ANNOTATION in node.annotations:
|
|
605
|
+
return None
|
|
606
|
+
if getattr(self, "_security_fallback_frame", None) is not None:
|
|
607
|
+
return None
|
|
608
|
+
name = node.name
|
|
609
|
+
if name == "na" or name in getattr(self, "_pending_decl_outer_alias", {}):
|
|
610
|
+
return None
|
|
611
|
+
if (name in getattr(self, "_current_func_series_params", ())
|
|
612
|
+
or name in getattr(self, "_current_func_param_types", ())
|
|
613
|
+
or name in BAR_FIELDS or name in BAR_BUILTINS):
|
|
614
|
+
return None
|
|
615
|
+
known = getattr(self, "_known_vars", {})
|
|
616
|
+
if (name not in known
|
|
617
|
+
or name in getattr(self, "_input_backed_vars", ())
|
|
618
|
+
or self._known_var_is_lexically_shadowed(name)):
|
|
619
|
+
return None
|
|
620
|
+
value = known[name]
|
|
621
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
622
|
+
return None
|
|
623
|
+
return value
|
|
624
|
+
|
|
625
|
+
def _global_int_constant(self, name: str) -> int | None:
|
|
626
|
+
"""The int constant a top-level name is inlined as (``_known_vars``,
|
|
627
|
+
not an input, a bar field or a built-in), read at the top level."""
|
|
628
|
+
if name == "na" or name in BAR_FIELDS or name in BAR_BUILTINS:
|
|
629
|
+
return None
|
|
630
|
+
known = getattr(self, "_known_vars", {})
|
|
631
|
+
if name not in known or name in getattr(self, "_input_backed_vars", ()):
|
|
632
|
+
return None
|
|
633
|
+
value = known[name]
|
|
634
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
635
|
+
return None
|
|
636
|
+
return value
|
|
637
|
+
|
|
638
|
+
def _literal_overflows_int32(self, node, names: bool = False) -> bool:
|
|
639
|
+
value = self._pure_int_literal_value(node, names)
|
|
332
640
|
return value is not None and not self._int_fits_int32(value)
|
|
333
641
|
|
|
642
|
+
def _literal_wide_global(self, name: str, decl=None) -> bool:
|
|
643
|
+
"""Whether the top-level declaration of ``name`` gives it an int
|
|
644
|
+
constant past int32 -- a literal (``g = 3000000000``) or constant
|
|
645
|
+
arithmetic (``var int e = 300 * MS``): its slot is ``int64_t``,
|
|
646
|
+
reassigned or not. Only that slot: the width is not a provenance
|
|
647
|
+
that travels by spelling through copies, parameters, arrays and
|
|
648
|
+
helper state (``_wide_int_provenance``), whose consumers keep their
|
|
649
|
+
types; a callable's or a block's declaration keeps its type too, and
|
|
650
|
+
so does a callable's local or parameter of that name where it is in
|
|
651
|
+
scope. With ``decl``, only when ``decl`` is that declaration."""
|
|
652
|
+
widths = getattr(self, "_top_level_constant_widths", None)
|
|
653
|
+
if widths is None:
|
|
654
|
+
widths = self._top_level_constant_widths = {}
|
|
655
|
+
ast = getattr(self.ctx, "ast", None)
|
|
656
|
+
for stmt in getattr(ast, "body", None) or ():
|
|
657
|
+
if (isinstance(stmt, VarDecl) and stmt.value is not None
|
|
658
|
+
and stmt.name not in widths):
|
|
659
|
+
value = self._pure_int_literal_value(stmt.value, global_scope=True)
|
|
660
|
+
widths[stmt.name] = (
|
|
661
|
+
stmt, value is not None and not self._int_fits_int32(value))
|
|
662
|
+
top = widths.get(name)
|
|
663
|
+
if top is None or not top[1]:
|
|
664
|
+
return False
|
|
665
|
+
if decl is not None:
|
|
666
|
+
return decl is top[0]
|
|
667
|
+
return not (
|
|
668
|
+
self._known_var_is_lexically_shadowed(name)
|
|
669
|
+
or name in getattr(self, "_current_func_param_types", ())
|
|
670
|
+
or name in getattr(self, "_current_func_series_params", ())
|
|
671
|
+
or name in getattr(self, "_current_func_locals", ())
|
|
672
|
+
or name in getattr(self, "_current_func_local_types", {})
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# C++ text of an ``int`` expression built from decimal literals only.
|
|
676
|
+
_INT_LITERAL_CPP = re.compile(r"[\s()0-9+\-*]+")
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def _int32_literal_cpp_value(cls, cpp: str) -> int | None:
|
|
680
|
+
"""Exact value of C++ text built from decimal literals, ``+ - *`` and
|
|
681
|
+
parentheses that C++ evaluates in ``int`` (every literal and every
|
|
682
|
+
partial result fits int32), else None."""
|
|
683
|
+
if not cls._INT_LITERAL_CPP.fullmatch(cpp or ""):
|
|
684
|
+
return None
|
|
685
|
+
import ast as _ast
|
|
686
|
+
try:
|
|
687
|
+
tree = _ast.parse(cpp.strip(), mode="eval")
|
|
688
|
+
except SyntaxError:
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
def value(node):
|
|
692
|
+
if isinstance(node, _ast.Constant) and type(node.value) is int:
|
|
693
|
+
result = node.value
|
|
694
|
+
elif isinstance(node, _ast.UnaryOp) and isinstance(node.op, (_ast.USub, _ast.UAdd)):
|
|
695
|
+
inner = value(node.operand)
|
|
696
|
+
if inner is None:
|
|
697
|
+
return None
|
|
698
|
+
result = -inner if isinstance(node.op, _ast.USub) else inner
|
|
699
|
+
elif isinstance(node, _ast.BinOp) and isinstance(
|
|
700
|
+
node.op, (_ast.Add, _ast.Sub, _ast.Mult)):
|
|
701
|
+
left, right = value(node.left), value(node.right)
|
|
702
|
+
if left is None or right is None:
|
|
703
|
+
return None
|
|
704
|
+
result = (left + right if isinstance(node.op, _ast.Add)
|
|
705
|
+
else left - right if isinstance(node.op, _ast.Sub)
|
|
706
|
+
else left * right)
|
|
707
|
+
else:
|
|
708
|
+
return None
|
|
709
|
+
return result if cls._int_fits_int32(result) else None
|
|
710
|
+
|
|
711
|
+
return value(tree.body)
|
|
712
|
+
|
|
713
|
+
def _fold_int32_overflow_cpp(self, op: str, left_cpp: str, right_cpp: str) -> str | None:
|
|
714
|
+
"""``left op right`` as a 64-bit literal when both sides are C++
|
|
715
|
+
``int`` literal arithmetic whose exact result leaves int32 (Pine's
|
|
716
|
+
``int`` is 64-bit; C++ would overflow), else None. The emitted text
|
|
717
|
+
is what decides: a name inlined as its literal, a global expanded
|
|
718
|
+
into its declaration or a helper parameter bound to a literal all
|
|
719
|
+
read as literals here."""
|
|
720
|
+
if op not in ("+", "-", "*"):
|
|
721
|
+
return None
|
|
722
|
+
left = self._int32_literal_cpp_value(left_cpp)
|
|
723
|
+
if left is None:
|
|
724
|
+
return None
|
|
725
|
+
right = self._int32_literal_cpp_value(right_cpp)
|
|
726
|
+
if right is None:
|
|
727
|
+
return None
|
|
728
|
+
result = left + right if op == "+" else left - right if op == "-" else left * right
|
|
729
|
+
if self._int_fits_int32(result):
|
|
730
|
+
return None
|
|
731
|
+
return f"static_cast<int64_t>({result}LL)"
|
|
732
|
+
|
|
733
|
+
# A runtime integer the C++ holds in 32 bits (bar_index, a counter, a loop
|
|
734
|
+
# binder, an input without a declared range) is taken to stay within 2**24
|
|
735
|
+
# (16 777 216: more bars than thirty years of one-minute data).
|
|
736
|
+
_RUNTIME_INT_BOUND = 1 << 24
|
|
737
|
+
|
|
738
|
+
def _int_arith_leaves_int32(self, node, owner_info=None) -> bool:
|
|
739
|
+
"""Whether ``node`` is an integer ``+ - *`` the C++ would compute in
|
|
740
|
+
32-bit ``int`` arithmetic -- both operands are 32-bit integers
|
|
741
|
+
(``_narrow_int_bound``) -- while its magnitude can leave int32.
|
|
742
|
+
|
|
743
|
+
Pine's ``int`` is 64-bit: ``days * 86400000`` over ``days =
|
|
744
|
+
input.int(30)`` is 2592000000 on TradingView, and ``bar_index *
|
|
745
|
+
7200000`` passes int32 at bar 299 (lab tv pf-cgs2-int64-products).
|
|
746
|
+
Such a node is computed in 64 bits (``_visit_binop``) and its value
|
|
747
|
+
is wide wherever it is stored (``_expr_returns_wide_int``). A node
|
|
748
|
+
whose magnitude fits int32 keeps its spelling."""
|
|
749
|
+
if not (isinstance(node, BinOp) and node.op in ("+", "-", "*")):
|
|
750
|
+
return False
|
|
751
|
+
bound = self._int_arith_bound(node, owner_info)
|
|
752
|
+
return bound is not None and bound > (1 << 31) - 1
|
|
753
|
+
|
|
754
|
+
def _int_arith_bound(self, node: BinOp, owner_info=None) -> int | None:
|
|
755
|
+
"""The bound on ``|value|`` of a ``+ - *`` over two 32-bit integer
|
|
756
|
+
operands, uncapped; None when an operand is not one."""
|
|
757
|
+
left = self._narrow_int_bound(node.left, owner_info)
|
|
758
|
+
if left is None:
|
|
759
|
+
return None
|
|
760
|
+
right = self._narrow_int_bound(node.right, owner_info)
|
|
761
|
+
if right is None:
|
|
762
|
+
return None
|
|
763
|
+
return left * right if node.op == "*" else left + right
|
|
764
|
+
|
|
765
|
+
def _narrow_int_bound(self, node, owner_info=None) -> int | None:
|
|
766
|
+
"""A bound on ``|value|`` of an integer expression the C++ holds in a
|
|
767
|
+
32-bit ``int``, else None (a double, a 64-bit integer, or a shape this
|
|
768
|
+
does not know). Literals and inlined constants are exact, an
|
|
769
|
+
``input.int`` is its declared range (its default and 2**24 without
|
|
770
|
+
one), and any other runtime integer is ``_RUNTIME_INT_BOUND``."""
|
|
771
|
+
if isinstance(node, NumberLiteral):
|
|
772
|
+
value = node.value
|
|
773
|
+
if type(value) is int and self._int_fits_int32(value):
|
|
774
|
+
return abs(value)
|
|
775
|
+
return None
|
|
776
|
+
if isinstance(node, UnaryOp):
|
|
777
|
+
if node.op != "-":
|
|
778
|
+
return None
|
|
779
|
+
return self._narrow_int_bound(node.operand, owner_info)
|
|
780
|
+
if isinstance(node, BinOp):
|
|
781
|
+
if node.op not in ("+", "-", "*"):
|
|
782
|
+
return None
|
|
783
|
+
bound = self._int_arith_bound(node, owner_info)
|
|
784
|
+
# A node past int32 is computed in 64 bits: no longer 32-bit.
|
|
785
|
+
return bound if bound is not None and bound <= (1 << 31) - 1 else None
|
|
786
|
+
if isinstance(node, Ternary):
|
|
787
|
+
arms = [self._narrow_int_bound(arm, owner_info)
|
|
788
|
+
for arm in (node.true_val, node.false_val)]
|
|
789
|
+
return None if None in arms else max(arms)
|
|
790
|
+
if isinstance(node, Identifier):
|
|
791
|
+
return self._narrow_int_name_bound(node.name, owner_info)
|
|
792
|
+
return None
|
|
793
|
+
|
|
794
|
+
@contextlib.contextmanager
|
|
795
|
+
def _int_width_scan(self, epoch_only: bool | None = None):
|
|
796
|
+
"""The scope of a whole-script width cache computation: names resolve
|
|
797
|
+
against the scanned code's owner, not the callable being emitted
|
|
798
|
+
(``_narrow_int_name_info``); ``epoch_only`` sets the epoch-only mode
|
|
799
|
+
(``_wide_int_array_names``) for its duration."""
|
|
800
|
+
self._int_width_scan_depth = getattr(self, "_int_width_scan_depth", 0) + 1
|
|
801
|
+
saved = getattr(self, "_wide_int_epoch_only", False)
|
|
802
|
+
if epoch_only is not None:
|
|
803
|
+
self._wide_int_epoch_only = epoch_only
|
|
804
|
+
try:
|
|
805
|
+
yield
|
|
806
|
+
finally:
|
|
807
|
+
self._int_width_scan_depth -= 1
|
|
808
|
+
self._wide_int_epoch_only = saved
|
|
809
|
+
|
|
810
|
+
def _narrow_int_name_bound(self, name: str, owner_info=None) -> int | None:
|
|
811
|
+
"""``_narrow_int_bound`` of a bare name (``_narrow_int_name_info``)."""
|
|
812
|
+
info = self._narrow_int_name_info(name, owner_info)
|
|
813
|
+
return info[0] if info is not None else None
|
|
814
|
+
|
|
815
|
+
def _narrow_int_name_info(self, name: str,
|
|
816
|
+
owner_info=None) -> tuple[int, bool] | None:
|
|
817
|
+
"""``(bound, may_be_na)`` of a bare name the C++ holds in a 32-bit
|
|
818
|
+
``int``: a parameter or local of the callable being emitted (or
|
|
819
|
+
``owner_info``'s), a counted loop's binder, an inlined int constant,
|
|
820
|
+
an input, ``bar_index`` / ``last_bar_index``, or a script variable
|
|
821
|
+
whose slot is a C++ ``int``; None for any other name. A parameter, a
|
|
822
|
+
local and a script variable can hold ``na``; a binder, a constant, an
|
|
823
|
+
input and the bar index cannot.
|
|
824
|
+
|
|
825
|
+
While a width cache is computed (``_int_width_scan``) the callable
|
|
826
|
+
being emitted is not the one the scanned code belongs to, so only
|
|
827
|
+
``owner_info`` resolves a callable's names."""
|
|
828
|
+
runtime = self._RUNTIME_INT_BOUND
|
|
829
|
+
# ``owner_info`` names the callable the name belongs to: resolve it
|
|
830
|
+
# there, whichever callable is being emitted.
|
|
831
|
+
emitting = (getattr(self, "_int_width_scan_depth", 0) == 0
|
|
832
|
+
and owner_info is None)
|
|
833
|
+
owner_node = getattr(owner_info, "node", None)
|
|
834
|
+
param_types = (getattr(self, "_current_func_param_types", {}) or {}
|
|
835
|
+
if emitting else {})
|
|
836
|
+
if name in param_types or (emitting and name in (
|
|
837
|
+
getattr(self, "_current_func_series_params", ()) or ())):
|
|
838
|
+
return (runtime, True) if param_types.get(name) == "int" else None
|
|
839
|
+
if owner_node is not None and name in owner_node.params:
|
|
840
|
+
index = owner_node.params.index(name)
|
|
841
|
+
if (getattr(owner_info, "is_udt_method", False) and index == 0) \
|
|
842
|
+
or name in self.ctx.func_series_vars.get(owner_info.name, set()) \
|
|
843
|
+
or (owner_info.name, index) in self._wide_int_provenance()[1]:
|
|
844
|
+
return None
|
|
845
|
+
declared = list(getattr(self.ctx, "func_declared_param_type_specs",
|
|
846
|
+
{}).get(owner_info.name, ()))
|
|
847
|
+
spec = declared[index] if index < len(declared) else None
|
|
848
|
+
if spec is None:
|
|
849
|
+
specs = list(getattr(owner_info, "param_type_specs", ()) or ())
|
|
850
|
+
spec = specs[index] if index < len(specs) else None
|
|
851
|
+
if (spec is not None and spec.kind == "primitive"
|
|
852
|
+
and spec.name == "int"):
|
|
853
|
+
return runtime, True
|
|
854
|
+
if (spec is None and self._func_param_int_cpp_type(
|
|
855
|
+
owner_info, index, None) == "int"):
|
|
856
|
+
# An untyped parameter whose calls carry no TypeSpec
|
|
857
|
+
# (``toMs(year(time))``) is the emitter's ``int`` still.
|
|
858
|
+
return runtime, True
|
|
859
|
+
return None
|
|
860
|
+
if emitting and name in (
|
|
861
|
+
getattr(self, "_current_counted_loop_vars", ()) or ()):
|
|
862
|
+
# A counted loop's binder is ``for (int i = ...)``.
|
|
863
|
+
return runtime, False
|
|
864
|
+
if emitting and name in (getattr(self, "_current_loop_vars", ()) or ()):
|
|
865
|
+
# A ``for ... in`` binder: an element, a map entry or an index,
|
|
866
|
+
# 32-bit only when its spec says ``int`` (a float element is a
|
|
867
|
+
# double), and an element can be na.
|
|
868
|
+
spec = (getattr(self, "_current_loop_var_specs", {}) or {}).get(name)
|
|
869
|
+
if (spec is not None and spec.kind == "primitive"
|
|
870
|
+
and spec.name == "int"):
|
|
871
|
+
return runtime, True
|
|
872
|
+
return None
|
|
873
|
+
local_types = (getattr(self, "_current_func_local_types", {}) or {}
|
|
874
|
+
if emitting else {})
|
|
875
|
+
if name in local_types or (emitting and name in (
|
|
876
|
+
getattr(self, "_current_func_locals", ()) or ())):
|
|
877
|
+
return (runtime, True) if local_types.get(name) == "int" else None
|
|
878
|
+
if owner_node is not None:
|
|
879
|
+
hints = {str(child.type_hint or "") for child in
|
|
880
|
+
self._walk_ast_list(owner_node.body)
|
|
881
|
+
if isinstance(child, VarDecl) and child.name == name}
|
|
882
|
+
if hints:
|
|
883
|
+
# A local of ``owner_info`` read outside its emission (its
|
|
884
|
+
# result type is decided before its locals are registered):
|
|
885
|
+
# a declared ``int`` is 32-bit, anything else unknown.
|
|
886
|
+
return (runtime, True) if hints == {"int"} else None
|
|
887
|
+
if name in BAR_FIELDS:
|
|
888
|
+
return None
|
|
889
|
+
if name in ("bar_index", "last_bar_index"):
|
|
890
|
+
return runtime, False
|
|
891
|
+
if name in BAR_BUILTINS or name in self._INTEGRAL_BUILTINS:
|
|
892
|
+
return None
|
|
893
|
+
if owner_node is None and self._known_var_is_lexically_shadowed(name):
|
|
894
|
+
# A block's local of that spelling: its type is not the global's.
|
|
895
|
+
return None
|
|
896
|
+
known = getattr(self, "_known_vars", {})
|
|
897
|
+
if name in getattr(self, "_input_backed_vars", ()):
|
|
898
|
+
bound = self._input_int_bound(name)
|
|
899
|
+
return (bound, False) if bound is not None else None
|
|
900
|
+
if name in known:
|
|
901
|
+
value = known[name]
|
|
902
|
+
if type(value) is int and self._int_fits_int32(value):
|
|
903
|
+
return abs(value), False
|
|
904
|
+
return None
|
|
905
|
+
if name in self._wide_int_provenance()[0]:
|
|
906
|
+
return None
|
|
907
|
+
if (getattr(self, "_int_width_scan_depth", 0) == 0
|
|
908
|
+
and self._literal_wide_global(name)):
|
|
909
|
+
# A top-level int constant past int32 initializes an int64_t
|
|
910
|
+
# slot (CGINT5a), reassigned or not: where it is emitted it is no
|
|
911
|
+
# 32-bit operand. A width scan keeps it one: it cannot tell the
|
|
912
|
+
# block local that may shadow it there (``int g = bar_index``),
|
|
913
|
+
# and a product it reads is 64-bit either way.
|
|
914
|
+
return None
|
|
915
|
+
return ((runtime, True)
|
|
916
|
+
if self._slot_scalar_cpp_type(name) == "int" else None)
|
|
917
|
+
|
|
918
|
+
def _int_operand_may_be_na(self, node, owner_info=None) -> bool:
|
|
919
|
+
"""Whether a 32-bit int operand ``_narrow_int_bound`` accepts can be
|
|
920
|
+
``na``: a leaf that is a parameter, a local or a script variable
|
|
921
|
+
(``_narrow_int_name_info``). Literals, inlined constants, inputs, a
|
|
922
|
+
counted loop's binder and the bar index cannot."""
|
|
923
|
+
if isinstance(node, NumberLiteral):
|
|
924
|
+
return False
|
|
925
|
+
if isinstance(node, UnaryOp):
|
|
926
|
+
return self._int_operand_may_be_na(node.operand, owner_info)
|
|
927
|
+
if isinstance(node, BinOp):
|
|
928
|
+
return (self._int_operand_may_be_na(node.left, owner_info)
|
|
929
|
+
or self._int_operand_may_be_na(node.right, owner_info))
|
|
930
|
+
if isinstance(node, Ternary):
|
|
931
|
+
return (self._int_operand_may_be_na(node.true_val, owner_info)
|
|
932
|
+
or self._int_operand_may_be_na(node.false_val, owner_info))
|
|
933
|
+
if isinstance(node, Identifier):
|
|
934
|
+
info = self._narrow_int_name_info(node.name, owner_info)
|
|
935
|
+
return info is None or info[1]
|
|
936
|
+
return True
|
|
937
|
+
|
|
938
|
+
def _na_aware_wide_is_double(self, node) -> bool:
|
|
939
|
+
"""Whether ``node`` is a 64-bit ``+ - *`` (``_int_arith_leaves_int32``)
|
|
940
|
+
over an operand that can be na, which ``_wide_int_arith_cpp`` emits
|
|
941
|
+
as a double."""
|
|
942
|
+
return (isinstance(node, BinOp) and self._int_arith_leaves_int32(node)
|
|
943
|
+
and self._int_operand_may_be_na(node))
|
|
944
|
+
|
|
945
|
+
def _holds_na_aware_wide_double(self, expr) -> bool:
|
|
946
|
+
"""Whether ``expr`` holds such a double-valued product: an integer
|
|
947
|
+
store of it narrows na-preserving (quirk 9), not implicitly."""
|
|
948
|
+
return expr is not None and any(
|
|
949
|
+
isinstance(sub, BinOp) and self._na_aware_wide_is_double(sub)
|
|
950
|
+
for sub, _depth in iter_ast_nodes(expr))
|
|
951
|
+
|
|
952
|
+
def _wide_int_arith_cpp(self, node, left: str, right: str, lower) -> str:
|
|
953
|
+
"""The C++ of an integer ``+ - *`` computed in 64 bits
|
|
954
|
+
(``_int_arith_leaves_int32``): ``lower`` over the left operand cast to
|
|
955
|
+
``int64_t``. An operand that can be ``na`` makes the value ``na``, as
|
|
956
|
+
Pine's arithmetic does: the widening ``static_cast`` reads the
|
|
957
|
+
sentinel ``na<int>()`` as the number -2147483648 (quirk 9). That form
|
|
958
|
+
is a ``double`` (``_emitted_value_is_double``): the slot such a value
|
|
959
|
+
lands in is a double as often as an ``int64_t`` (a global holding
|
|
960
|
+
integer arithmetic is declared from ``_infer_type``), and a double
|
|
961
|
+
keeps ``na`` where ``na<int64_t>()`` reads -9.2e18; an integer slot
|
|
962
|
+
narrows it na-preserving (every integer store of one does: arrays,
|
|
963
|
+
map and matrix values, parameters). A double is exact up to 2**53, a
|
|
964
|
+
product past it rounds."""
|
|
965
|
+
if not self._int_operand_may_be_na(node):
|
|
966
|
+
return lower(f"static_cast<int64_t>({left})", right)
|
|
967
|
+
return (f"[&]() -> double {{ auto _pf_wide_l = ({left}); "
|
|
968
|
+
f"auto _pf_wide_r = ({right}); "
|
|
969
|
+
f"return (is_na(_pf_wide_l) || is_na(_pf_wide_r)) "
|
|
970
|
+
f"? na<double>() : static_cast<double>("
|
|
971
|
+
f"{lower('static_cast<int64_t>(_pf_wide_l)', '_pf_wide_r')}); }}()")
|
|
972
|
+
|
|
973
|
+
def _input_int_bound(self, name: str) -> int | None:
|
|
974
|
+
"""A bound on an int input's value: its declared range when it has
|
|
975
|
+
both ends, else its default and ``_RUNTIME_INT_BOUND`` (an override
|
|
976
|
+
can take any value). None for an input that is not an int."""
|
|
977
|
+
call = getattr(self, "_input_var_to_call", {}).get(name)
|
|
978
|
+
default = getattr(self, "_known_vars", {}).get(name)
|
|
979
|
+
if call is None or type(default) is not int:
|
|
980
|
+
return None
|
|
981
|
+
func_name, namespace = self._resolve_callee(call.callee)
|
|
982
|
+
if not ((namespace == "input" and func_name == "int")
|
|
983
|
+
or (namespace is None and func_name == "input")):
|
|
984
|
+
return None
|
|
985
|
+
ends = []
|
|
986
|
+
for index, key in ((2, "minval"), (3, "maxval")):
|
|
987
|
+
node = (call.args[index] if namespace == "input"
|
|
988
|
+
and len(call.args) > index else call.kwargs.get(key))
|
|
989
|
+
# An input is a top-level declaration: a named bound reads the
|
|
990
|
+
# top-level constant, whichever callable is being emitted.
|
|
991
|
+
value = (self._pure_int_literal_value(node, global_scope=True)
|
|
992
|
+
if node is not None else None)
|
|
993
|
+
if value is not None:
|
|
994
|
+
ends.append(abs(value))
|
|
995
|
+
if len(ends) == 2:
|
|
996
|
+
return max(abs(default), *ends)
|
|
997
|
+
return max(abs(default), self._RUNTIME_INT_BOUND, *ends)
|
|
998
|
+
|
|
999
|
+
def _holds_wide_int_constant(self, expr) -> bool:
|
|
1000
|
+
"""Whether ``expr`` is, or selects, an int constant past int32
|
|
1001
|
+
(``3000000000``, ``c ? 0 : 400 * 7200000``): the slot holding its
|
|
1002
|
+
value -- a tuple element, a function's or method's result -- is
|
|
1003
|
+
``int64_t``. Only that slot: the width does not travel through the
|
|
1004
|
+
spelling-keyed provenance (``_wide_int_provenance``)."""
|
|
1005
|
+
if isinstance(expr, ExprStmt):
|
|
1006
|
+
expr = expr.expr
|
|
1007
|
+
if isinstance(expr, NumberLiteral):
|
|
1008
|
+
return (type(expr.value) is int
|
|
1009
|
+
and not self._int_fits_int32(expr.value))
|
|
1010
|
+
if isinstance(expr, (BinOp, UnaryOp)):
|
|
1011
|
+
return self._literal_overflows_int32(expr)
|
|
1012
|
+
if isinstance(expr, Ternary):
|
|
1013
|
+
return (self._holds_wide_int_constant(expr.true_val)
|
|
1014
|
+
or self._holds_wide_int_constant(expr.false_val))
|
|
1015
|
+
return False
|
|
1016
|
+
|
|
1017
|
+
def _widen_int_slot(self, cpp_type: str, expr, owner_info=None) -> str:
|
|
1018
|
+
"""``int64_t`` for an ``int`` slot ``expr`` gives a 64-bit value."""
|
|
1019
|
+
if cpp_type == "int" and (
|
|
1020
|
+
self._holds_wide_int_constant(expr)
|
|
1021
|
+
or self._expr_returns_wide_int(expr, owner_info, set(), None)):
|
|
1022
|
+
return "int64_t"
|
|
1023
|
+
return cpp_type
|
|
1024
|
+
|
|
1025
|
+
def _udt_receiver_type_name(self, receiver, owner_info=None) -> str | None:
|
|
1026
|
+
"""The user type of a field read's receiver: a method's own receiver
|
|
1027
|
+
parameter by its declared type, anything else by its TypeSpec."""
|
|
1028
|
+
owner_node = getattr(owner_info, "node", None)
|
|
1029
|
+
if (getattr(owner_info, "is_udt_method", False)
|
|
1030
|
+
and owner_node is not None and owner_node.params
|
|
1031
|
+
and isinstance(receiver, Identifier)
|
|
1032
|
+
and receiver.name == owner_node.params[0]):
|
|
1033
|
+
specs = list(getattr(owner_info, "param_type_specs", ()) or ())
|
|
1034
|
+
return (method_receiver_type_name(specs[0] if specs else None)
|
|
1035
|
+
or getattr(owner_info, "udt_type_name", None))
|
|
1036
|
+
try:
|
|
1037
|
+
spec = self._type_spec_from_expr(receiver)
|
|
1038
|
+
except Exception as exc: # an unresolvable receiver holds no wide field
|
|
1039
|
+
if getattr(exc, "limit", False):
|
|
1040
|
+
raise
|
|
1041
|
+
return None
|
|
1042
|
+
if spec is not None and spec.kind == "udt":
|
|
1043
|
+
return spec.name
|
|
1044
|
+
return None
|
|
1045
|
+
|
|
1046
|
+
def _wide_udt_int_fields(self) -> set[tuple[str, str]]:
|
|
1047
|
+
"""``(type, field)`` of the UDT ``int`` fields holding a 64-bit value:
|
|
1048
|
+
a default or a write (``T.new(...)``, ``obj.field := v``) that is a
|
|
1049
|
+
constant past int32 or a wide value (``_expr_returns_wide_int``).
|
|
1050
|
+
The field is ``int64_t`` storage already; its reads carry the width
|
|
1051
|
+
(``method big(U this) => this.v * 2`` over ``int v = 3000000000``
|
|
1052
|
+
is 6000000000 on TradingView). Iterated to a fixpoint and cached."""
|
|
1053
|
+
cached = getattr(self, "_wide_udt_int_field_cache", None)
|
|
1054
|
+
if cached is not None:
|
|
1055
|
+
return cached
|
|
1056
|
+
fields: set[tuple[str, str]] = set()
|
|
1057
|
+
self._wide_udt_int_field_cache = fields
|
|
1058
|
+
udt_defs = getattr(self, "_udt_defs", {}) or {}
|
|
1059
|
+
int_fields = {
|
|
1060
|
+
(type_name, field.name): index
|
|
1061
|
+
for type_name, decl_fields in udt_defs.items()
|
|
1062
|
+
for index, field in enumerate(decl_fields or ())
|
|
1063
|
+
if getattr(field, "type_name", "") == "int"
|
|
1064
|
+
}
|
|
1065
|
+
if not int_fields:
|
|
1066
|
+
return fields
|
|
1067
|
+
writes: list = []
|
|
1068
|
+
for (type_name, name), index in int_fields.items():
|
|
1069
|
+
default = udt_defs[type_name][index].default
|
|
1070
|
+
if default is not None:
|
|
1071
|
+
writes.append(((type_name, name), default, None))
|
|
1072
|
+
|
|
1073
|
+
def collect(node, owner) -> None:
|
|
1074
|
+
if (isinstance(node, FuncCall)
|
|
1075
|
+
and isinstance(node.callee, MemberAccess)
|
|
1076
|
+
and isinstance(node.callee.object, Identifier)
|
|
1077
|
+
and node.callee.member == "new"
|
|
1078
|
+
and node.callee.object.name in udt_defs):
|
|
1079
|
+
type_name = node.callee.object.name
|
|
1080
|
+
names = [f.name for f in udt_defs[type_name] or ()]
|
|
1081
|
+
bound = list(zip(names, node.args))
|
|
1082
|
+
bound += [(key, value) for key, value in node.kwargs.items()
|
|
1083
|
+
if key in names]
|
|
1084
|
+
for name, value in bound:
|
|
1085
|
+
if (type_name, name) in int_fields:
|
|
1086
|
+
writes.append(((type_name, name), value, owner))
|
|
1087
|
+
elif (isinstance(node, Assignment)
|
|
1088
|
+
and isinstance(node.target, MemberAccess)):
|
|
1089
|
+
type_name = self._udt_receiver_type_name(
|
|
1090
|
+
node.target.object, owner)
|
|
1091
|
+
if (type_name, node.target.member) in int_fields:
|
|
1092
|
+
writes.append(((type_name, node.target.member),
|
|
1093
|
+
node.value, owner))
|
|
1094
|
+
|
|
1095
|
+
for info in getattr(self.ctx, "func_infos", ()):
|
|
1096
|
+
node = getattr(info, "node", None)
|
|
1097
|
+
if node is not None:
|
|
1098
|
+
for child in self._walk_ast_list(node.body):
|
|
1099
|
+
collect(child, info)
|
|
1100
|
+
ast = getattr(self.ctx, "ast", None)
|
|
1101
|
+
for stmt in getattr(ast, "body", None) or ():
|
|
1102
|
+
if isinstance(stmt, (FuncDef, MethodDef)):
|
|
1103
|
+
continue
|
|
1104
|
+
for child in self._walk_ast(stmt):
|
|
1105
|
+
collect(child, None)
|
|
1106
|
+
changed = True
|
|
1107
|
+
with self._int_width_scan(epoch_only=False):
|
|
1108
|
+
while changed:
|
|
1109
|
+
changed = False
|
|
1110
|
+
for key, value, owner in writes:
|
|
1111
|
+
if key not in fields and (
|
|
1112
|
+
self._holds_wide_int_constant(value)
|
|
1113
|
+
or self._expr_returns_wide_int(value, owner, set(), None)):
|
|
1114
|
+
fields.add(key)
|
|
1115
|
+
changed = True
|
|
1116
|
+
return fields
|
|
1117
|
+
|
|
334
1118
|
def _array_receiver_and_value(self, call):
|
|
335
1119
|
"""``(receiver_name, value_node)`` of an element-writing array call in
|
|
336
1120
|
either form (``array.push(a, v)`` / ``a.push(v)``), else ``None``."""
|
|
@@ -364,7 +1148,15 @@ class TypeInferer:
|
|
|
364
1148
|
``set`` / ``insert`` / ``fill``, or is built by ``array.new_int`` /
|
|
365
1149
|
``array.new<int>`` / ``array.from`` from one. Their element type is
|
|
366
1150
|
``int64_t`` (TypeSpec primitive ``int64``): Pine's ``int`` holds the
|
|
367
|
-
epoch, ``std::vector<int>`` truncates it. Cached on the instance.
|
|
1151
|
+
epoch, ``std::vector<int>`` truncates it. Cached on the instance.
|
|
1152
|
+
|
|
1153
|
+
Only an epoch widens an array (the scan runs in the epoch-only mode):
|
|
1154
|
+
a declared ``array<int>`` parameter, result or field stays
|
|
1155
|
+
``std::vector<int>``, which a widened argument does not bind to, so a
|
|
1156
|
+
value that is wide only by 64-bit arithmetic or a constant past int32
|
|
1157
|
+
(``bars.push(bar_index * 1000)``) keeps the array's C++ ``int``, is
|
|
1158
|
+
narrowed na-preserving where it is stored and warns
|
|
1159
|
+
(``_array_init_value_expr``)."""
|
|
368
1160
|
cached = getattr(self, "_wide_int_array_cache", None)
|
|
369
1161
|
if cached is not None:
|
|
370
1162
|
return cached
|
|
@@ -415,16 +1207,17 @@ class TypeInferer:
|
|
|
415
1207
|
names.add(target)
|
|
416
1208
|
|
|
417
1209
|
ast = getattr(self.ctx, "ast", None)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1210
|
+
with self._int_width_scan(epoch_only=True):
|
|
1211
|
+
for _round in range(8):
|
|
1212
|
+
before = len(names)
|
|
1213
|
+
if ast is not None:
|
|
1214
|
+
scan(self._walk_ast(ast), None)
|
|
1215
|
+
for info in getattr(self.ctx, "func_infos", ()):
|
|
1216
|
+
node = getattr(info, "node", None)
|
|
1217
|
+
if node is not None:
|
|
1218
|
+
scan(self._walk_ast_list(node.body), info)
|
|
1219
|
+
if len(names) == before:
|
|
1220
|
+
break
|
|
428
1221
|
return names
|
|
429
1222
|
|
|
430
1223
|
def _widen_array_spec_for_name(self, name, spec):
|
|
@@ -592,6 +1385,11 @@ class TypeInferer:
|
|
|
592
1385
|
return TypeSpec.primitive("int64")
|
|
593
1386
|
return TypeSpec.primitive("int")
|
|
594
1387
|
return None
|
|
1388
|
+
if self._is_color_value(node):
|
|
1389
|
+
# A color is a packed-ARGB ``int64_t``: ``array.from(color.red,
|
|
1390
|
+
# color.new(c, 50))`` is ``std::vector<int64_t>``, which a
|
|
1391
|
+
# ``double`` vector cannot be braced from.
|
|
1392
|
+
return TypeSpec.primitive("color")
|
|
595
1393
|
spec = self._type_spec_from_expr(node)
|
|
596
1394
|
if spec is not None:
|
|
597
1395
|
return spec
|
|
@@ -611,6 +1409,40 @@ class TypeInferer:
|
|
|
611
1409
|
return TypeSpec.primitive(primitive_name)
|
|
612
1410
|
return None
|
|
613
1411
|
|
|
1412
|
+
def _is_color_value(self, node, _depth: int = 0) -> bool:
|
|
1413
|
+
"""Whether ``node`` is a color value: a literal, a ``color.*``
|
|
1414
|
+
constant, a ``color.new`` / ``rgb`` / ``from_gradient`` call, a
|
|
1415
|
+
conditional selecting one, or a script variable declared ``color`` or
|
|
1416
|
+
bound to one (``c1 = color.new(...)``, which the analyzer types
|
|
1417
|
+
float)."""
|
|
1418
|
+
if _depth > 16 or node is None:
|
|
1419
|
+
return False
|
|
1420
|
+
if isinstance(node, ColorLiteral):
|
|
1421
|
+
return True
|
|
1422
|
+
if isinstance(node, MemberAccess):
|
|
1423
|
+
return (isinstance(node.object, Identifier)
|
|
1424
|
+
and node.object.name == "color"
|
|
1425
|
+
and node.member in COLOR_CONST_MAP)
|
|
1426
|
+
if isinstance(node, FuncCall):
|
|
1427
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
1428
|
+
return namespace == "color" and func_name in (
|
|
1429
|
+
"new", "rgb", "from_gradient")
|
|
1430
|
+
if isinstance(node, Ternary):
|
|
1431
|
+
return (self._is_color_value(node.true_val, _depth + 1)
|
|
1432
|
+
or self._is_color_value(node.false_val, _depth + 1))
|
|
1433
|
+
if isinstance(node, Identifier):
|
|
1434
|
+
if self._slot_scalar_cpp_type(node.name) != "int64_t":
|
|
1435
|
+
return False
|
|
1436
|
+
sym = self._variable_symbol(node.name)
|
|
1437
|
+
if sym is not None and sym.pine_type == PineType.COLOR:
|
|
1438
|
+
return True
|
|
1439
|
+
if (node.name in getattr(self, "_current_func_local_types", {})
|
|
1440
|
+
or node.name in getattr(self, "_current_func_param_types", {})):
|
|
1441
|
+
return False
|
|
1442
|
+
value = getattr(self.ctx, "global_expr_map", {}).get(node.name)
|
|
1443
|
+
return self._is_color_value(value, _depth + 1)
|
|
1444
|
+
return False
|
|
1445
|
+
|
|
614
1446
|
@staticmethod
|
|
615
1447
|
def _selection_terminal_expr(body: list[ASTNode] | None) -> ASTNode | None:
|
|
616
1448
|
"""Return one if/switch branch's value expression, if present."""
|
|
@@ -650,6 +1482,13 @@ class TypeInferer:
|
|
|
650
1482
|
|
|
651
1483
|
Returns ``None`` when the node's type cannot be narrowed beyond
|
|
652
1484
|
the runtime default (most callers fall back to ``double``)."""
|
|
1485
|
+
budget = getattr(self, "_budget", None)
|
|
1486
|
+
if budget is not None:
|
|
1487
|
+
# A method chain re-infers each receiver more than once, so this
|
|
1488
|
+
# recursion can outgrow the visitor checkpoints.
|
|
1489
|
+
self._budget_visit_count += 1
|
|
1490
|
+
if self._budget_visit_count % 128 == 0:
|
|
1491
|
+
budget.check(getattr(node, "loc", None), Phase.CODEGEN)
|
|
653
1492
|
if isinstance(node, NumberLiteral):
|
|
654
1493
|
return TypeSpec.primitive("float" if isinstance(node.value, float) else "int")
|
|
655
1494
|
if isinstance(node, BoolLiteral):
|
|
@@ -929,6 +1768,23 @@ class TypeInferer:
|
|
|
929
1768
|
receiver_spec = self._type_spec_from_expr(receiver_node)
|
|
930
1769
|
if receiver_spec is not None and receiver_spec.kind == "matrix":
|
|
931
1770
|
return receiver_spec
|
|
1771
|
+
# ``matrix.row(m, i)`` / ``matrix.col`` / ``matrix.eigenvalues``
|
|
1772
|
+
# return arrays, as their method forms below do; an untyped
|
|
1773
|
+
# ``r = matrix.row(m, 0)`` was declared a double.
|
|
1774
|
+
if namespace == "matrix" and func_name in ("row", "col", "eigenvalues"):
|
|
1775
|
+
receiver_node = node.args[0] if node.args else node.kwargs.get("id")
|
|
1776
|
+
receiver_spec = self._type_spec_from_expr(receiver_node)
|
|
1777
|
+
if receiver_spec is not None and receiver_spec.kind == "matrix":
|
|
1778
|
+
if func_name == "eigenvalues":
|
|
1779
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
1780
|
+
return TypeSpec.array(receiver_spec.element)
|
|
1781
|
+
# array.abs / standardize / sort_indices build a new array, which
|
|
1782
|
+
# their templates (``ARRAY_METHODS``) emit as a vector of doubles.
|
|
1783
|
+
if namespace == "array" and func_name in ARRAY_RESULT_METHODS:
|
|
1784
|
+
receiver_node = node.args[0] if node.args else node.kwargs.get("id")
|
|
1785
|
+
receiver_spec = self._type_spec_from_expr(receiver_node)
|
|
1786
|
+
if receiver_spec is not None and receiver_spec.kind == "array":
|
|
1787
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
932
1788
|
if namespace == "map" and func_name == "new":
|
|
933
1789
|
key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
934
1790
|
val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
@@ -967,6 +1823,8 @@ class TypeInferer:
|
|
|
967
1823
|
return recv_spec.element
|
|
968
1824
|
if member_name in ("copy", "slice"):
|
|
969
1825
|
return recv_spec
|
|
1826
|
+
if member_name in ARRAY_RESULT_METHODS:
|
|
1827
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
970
1828
|
if recv_spec is not None and recv_spec.kind == "map":
|
|
971
1829
|
if member_name in ("put", "get", "remove"):
|
|
972
1830
|
return recv_spec.value
|
|
@@ -1124,7 +1982,8 @@ class TypeInferer:
|
|
|
1124
1982
|
)
|
|
1125
1983
|
|
|
1126
1984
|
def _array_method_expr(
|
|
1127
|
-
self, array_expr: str, method: str, args: list[str],
|
|
1985
|
+
self, array_expr: str, method: str, args: list[str],
|
|
1986
|
+
spec: TypeSpec | None = None, node: ASTNode | None = None,
|
|
1128
1987
|
) -> str:
|
|
1129
1988
|
"""Lower ``arr.method(...)`` to its C++ form, validating numeric requirements."""
|
|
1130
1989
|
spec = spec or TypeSpec.array(TypeSpec.primitive("float"))
|
|
@@ -1133,14 +1992,33 @@ class TypeInferer:
|
|
|
1133
1992
|
if method == "copy":
|
|
1134
1993
|
lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
|
|
1135
1994
|
elif method == "slice":
|
|
1995
|
+
# TradingView aliases primitive source elements bidirectionally.
|
|
1996
|
+
# Our std::vector lowering owns a copy; a true view needs a
|
|
1997
|
+
# lifetime-safe collection representation across all array lanes.
|
|
1998
|
+
# Keep the compiling lowering and surface the measured divergence.
|
|
1999
|
+
warned = getattr(self, "_slice_alias_warned_nodes", set())
|
|
2000
|
+
if node is None or id(node) not in warned:
|
|
2001
|
+
self._codegen_warning(
|
|
2002
|
+
node,
|
|
2003
|
+
"array.slice returns a copy in PineForge; TradingView slices "
|
|
2004
|
+
"share source elements, so writes through either array can diverge.",
|
|
2005
|
+
hint="Avoid mutating a slice or its source while the slice is used.",
|
|
2006
|
+
)
|
|
2007
|
+
if node is not None:
|
|
2008
|
+
warned.add(id(node))
|
|
2009
|
+
self._slice_alias_warned_nodes = warned
|
|
1136
2010
|
# Bounds-checked in the shared helper; the element type stays
|
|
1137
2011
|
# caller-supplied so the typed lane keeps its own vector type.
|
|
1138
2012
|
lower_receiver = lambda recv: checked_array_slice(
|
|
1139
2013
|
recv, args, result_type=arr_cpp_type
|
|
1140
2014
|
)
|
|
1141
2015
|
elif method == "join" and elem_cpp == "std::string":
|
|
1142
|
-
|
|
1143
|
-
lower_receiver = lambda recv:
|
|
2016
|
+
join_args = list(args) or ['std::string(",")']
|
|
2017
|
+
lower_receiver = lambda recv: evaluate_args_once(
|
|
2018
|
+
join_args, ARRAY_ARGS_READ_REPEATEDLY["join"],
|
|
2019
|
+
lambda a: f"[&](){{ std::string __pf_r; for(size_t __pf_i=0;__pf_i<{recv}.size();__pf_i++){{ if(__pf_i>0)__pf_r+={a[0]}; __pf_r+={recv}[__pf_i]; }} return __pf_r; }}()",
|
|
2020
|
+
"_pf_array_a",
|
|
2021
|
+
)
|
|
1144
2022
|
else:
|
|
1145
2023
|
numeric_only = {
|
|
1146
2024
|
"sum", "avg", "min", "max", "range", "stdev", "variance", "median",
|
|
@@ -1191,8 +2069,15 @@ class TypeInferer:
|
|
|
1191
2069
|
arg_bindings.append((token, args[arg_index]))
|
|
1192
2070
|
self._array_arg_counter = counter
|
|
1193
2071
|
|
|
2072
|
+
table = (V5_ARRAY_INDEX_METHODS
|
|
2073
|
+
if self._pine_v5_body and method in V5_ARRAY_INDEX_METHODS
|
|
2074
|
+
else ARRAY_METHODS)
|
|
2075
|
+
|
|
1194
2076
|
def lower_receiver(recv: str) -> str:
|
|
1195
|
-
lowered =
|
|
2077
|
+
lowered = evaluate_args_once(
|
|
2078
|
+
bound_args, ARRAY_ARGS_READ_REPEATEDLY.get(method, ()),
|
|
2079
|
+
lambda a: table[method](recv, a), "_pf_array_a",
|
|
2080
|
+
)
|
|
1196
2081
|
for token, original in reversed(arg_bindings):
|
|
1197
2082
|
lowered = (
|
|
1198
2083
|
f"[&](){{ auto {token}=({original}); "
|
|
@@ -1258,7 +2143,7 @@ class TypeInferer:
|
|
|
1258
2143
|
owner,
|
|
1259
2144
|
set(),
|
|
1260
2145
|
getattr(self, "_active_call_site_idx", None),
|
|
1261
|
-
) or self._is_int64_builtin_init(node.name):
|
|
2146
|
+
) or self._is_int64_builtin_init(node.name, node):
|
|
1262
2147
|
return "int64_t"
|
|
1263
2148
|
return cpp_type
|
|
1264
2149
|
|
|
@@ -1319,13 +2204,76 @@ class TypeInferer:
|
|
|
1319
2204
|
# int64_t buffer: epoch-ms overflow int32 and the na sentinel would be
|
|
1320
2205
|
# misdetected. ``_is_int64_builtin_init`` only matches user vars whose
|
|
1321
2206
|
# init RHS is such a builtin, so also match the builtin name directly.
|
|
1322
|
-
if name in INT64_BUILTINS
|
|
2207
|
+
if name in INT64_BUILTINS:
|
|
2208
|
+
return "int64_t"
|
|
2209
|
+
sym = self._variable_symbol(name)
|
|
2210
|
+
# A float or bool an epoch reaches keeps its type: the analyzer types
|
|
2211
|
+
# every request.security value float (a double holds an epoch
|
|
2212
|
+
# exactly), and a name is keyed by spelling across scopes.
|
|
2213
|
+
if self._is_int64_builtin_init(name) and (
|
|
2214
|
+
sym is None
|
|
2215
|
+
or sym.pine_type not in (PineType.FLOAT, PineType.BOOL, PineType.STRING)
|
|
2216
|
+
):
|
|
2217
|
+
# So does a function's local, which the global scope does not
|
|
2218
|
+
# hold: ``float x = time`` is a float series (``x - x[1]`` is na
|
|
2219
|
+
# on the first bar, not a difference with the integer sentinel).
|
|
2220
|
+
local = self._callable_series_local_symbol(name) if sym is None else None
|
|
2221
|
+
if local is not None and local.pine_type == PineType.FLOAT:
|
|
2222
|
+
return "double"
|
|
1323
2223
|
return "int64_t"
|
|
1324
|
-
sym = self.ctx.symbols.resolve(name)
|
|
1325
2224
|
if sym is not None:
|
|
1326
2225
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
1327
2226
|
return "double"
|
|
1328
2227
|
|
|
2228
|
+
def _variable_symbol(self, name: str):
|
|
2229
|
+
"""The symbol the variable ``name`` resolves to.
|
|
2230
|
+
|
|
2231
|
+
The symbol table keeps one name per scope, and a user function is
|
|
2232
|
+
defined in the global scope under its name, typed by what it
|
|
2233
|
+
returns. A function-local variable of the same spelling -- the
|
|
2234
|
+
``float f`` series of another function beside ``f(x) =>
|
|
2235
|
+
str.tostring(x)`` -- resolved there once the analyzer had left its
|
|
2236
|
+
scope. Inside a function the variable is that function's own
|
|
2237
|
+
symbol; outside one, a series is the symbol of the function that
|
|
2238
|
+
keeps it (``func_series_vars``).
|
|
2239
|
+
"""
|
|
2240
|
+
sym = self.ctx.symbols.resolve(name)
|
|
2241
|
+
if name not in getattr(self, "_func_names", ()):
|
|
2242
|
+
return sym
|
|
2243
|
+
active = getattr(self, "_active_func_name", None)
|
|
2244
|
+
if active:
|
|
2245
|
+
scope_names = {f"func_{active}"}
|
|
2246
|
+
else:
|
|
2247
|
+
scope_names = {
|
|
2248
|
+
f"func_{owner}"
|
|
2249
|
+
for owner, names in getattr(self.ctx, "func_series_vars", {}).items()
|
|
2250
|
+
if name in names
|
|
2251
|
+
}
|
|
2252
|
+
for scope in self.ctx.symbols.all_scopes:
|
|
2253
|
+
if scope.name in scope_names and name in scope.symbols:
|
|
2254
|
+
return scope.symbols[name]
|
|
2255
|
+
return sym
|
|
2256
|
+
|
|
2257
|
+
def _callable_series_local_symbol(self, name: str):
|
|
2258
|
+
"""The symbol of the function-local series ``name``: the one every
|
|
2259
|
+
function or method keeping it as a series (``func_series_vars``)
|
|
2260
|
+
declares in its scope, else None when they disagree or none does."""
|
|
2261
|
+
scope_names = {
|
|
2262
|
+
# A method ``Type.name`` scopes its body as ``method_Type_name``.
|
|
2263
|
+
f"method_{owner.replace('.', '_', 1)}" if "." in owner
|
|
2264
|
+
else f"func_{owner}"
|
|
2265
|
+
for owner, names in getattr(self.ctx, "func_series_vars", {}).items()
|
|
2266
|
+
if name in names
|
|
2267
|
+
}
|
|
2268
|
+
found = [
|
|
2269
|
+
scope.symbols[name]
|
|
2270
|
+
for scope in self.ctx.symbols.all_scopes
|
|
2271
|
+
if scope.name in scope_names and name in scope.symbols
|
|
2272
|
+
]
|
|
2273
|
+
if not found or any(s.pine_type != found[0].pine_type for s in found):
|
|
2274
|
+
return None
|
|
2275
|
+
return found[0]
|
|
2276
|
+
|
|
1329
2277
|
def _series_param_element_cpp_type(
|
|
1330
2278
|
self,
|
|
1331
2279
|
func_info,
|
|
@@ -1364,7 +2312,7 @@ class TypeInferer:
|
|
|
1364
2312
|
if spec.name == "bool":
|
|
1365
2313
|
return "bool"
|
|
1366
2314
|
if spec.name == "color":
|
|
1367
|
-
return "
|
|
2315
|
+
return "int64_t"
|
|
1368
2316
|
if spec.name == "float":
|
|
1369
2317
|
return "double"
|
|
1370
2318
|
|
|
@@ -1382,7 +2330,7 @@ class TypeInferer:
|
|
|
1382
2330
|
if pine_type == PineType.BOOL:
|
|
1383
2331
|
return "bool"
|
|
1384
2332
|
if pine_type == PineType.COLOR:
|
|
1385
|
-
return "
|
|
2333
|
+
return "int64_t"
|
|
1386
2334
|
if pine_type == PineType.FLOAT:
|
|
1387
2335
|
return "double"
|
|
1388
2336
|
|
|
@@ -1394,7 +2342,7 @@ class TypeInferer:
|
|
|
1394
2342
|
if spec.name == "bool":
|
|
1395
2343
|
return "bool"
|
|
1396
2344
|
if spec.name == "color":
|
|
1397
|
-
return "
|
|
2345
|
+
return "int64_t"
|
|
1398
2346
|
if spec.name == "float":
|
|
1399
2347
|
return "double"
|
|
1400
2348
|
param_types = list(getattr(func_info, "param_types", ()) or ())
|
|
@@ -1404,7 +2352,7 @@ class TypeInferer:
|
|
|
1404
2352
|
if pine_type == PineType.BOOL:
|
|
1405
2353
|
return "bool"
|
|
1406
2354
|
if pine_type == PineType.COLOR:
|
|
1407
|
-
return "
|
|
2355
|
+
return "int64_t"
|
|
1408
2356
|
return "double"
|
|
1409
2357
|
|
|
1410
2358
|
def _callsite_callable_return_pine_type(
|
|
@@ -1464,10 +2412,17 @@ class TypeInferer:
|
|
|
1464
2412
|
terminal = node.body[-1]
|
|
1465
2413
|
if isinstance(terminal, ExprStmt):
|
|
1466
2414
|
terminal = terminal.expr
|
|
2415
|
+
if (not getattr(self, "_wide_int_epoch_only", False)
|
|
2416
|
+
and self._holds_wide_int_constant(terminal)):
|
|
2417
|
+
return True
|
|
1467
2418
|
return self._expr_returns_wide_int(
|
|
1468
2419
|
terminal, func_info, seen, call_site_idx
|
|
1469
2420
|
)
|
|
1470
2421
|
|
|
2422
|
+
_NON_INTEGER_BINOPS = frozenset({
|
|
2423
|
+
"/", "==", "!=", "<", ">", "<=", ">=", "and", "or",
|
|
2424
|
+
})
|
|
2425
|
+
|
|
1471
2426
|
def _expr_returns_wide_int(
|
|
1472
2427
|
self,
|
|
1473
2428
|
expr,
|
|
@@ -1489,7 +2444,12 @@ class TypeInferer:
|
|
|
1489
2444
|
node = getattr(owner_info, "node", None)
|
|
1490
2445
|
if node is not None:
|
|
1491
2446
|
if expr.name in node.params:
|
|
1492
|
-
|
|
2447
|
+
# A parameter is wide when a written call feeds it a wide
|
|
2448
|
+
# value (``_wide_int_provenance``).
|
|
2449
|
+
return (
|
|
2450
|
+
getattr(owner_info, "name", ""),
|
|
2451
|
+
node.params.index(expr.name),
|
|
2452
|
+
) in self._wide_int_provenance()[1]
|
|
1493
2453
|
local_key = (
|
|
1494
2454
|
f"{getattr(owner_info, 'name', '')}:local:{expr.name}"
|
|
1495
2455
|
)
|
|
@@ -1521,15 +2481,32 @@ class TypeInferer:
|
|
|
1521
2481
|
call_site_idx,
|
|
1522
2482
|
):
|
|
1523
2483
|
return True
|
|
1524
|
-
|
|
2484
|
+
# A name some declaration or reassignment makes wide carries the
|
|
2485
|
+
# epoch wherever it is read (``_wide_int_provenance``).
|
|
2486
|
+
return (self._expr_is_int64_builtin(expr)
|
|
2487
|
+
or expr.name in self._wide_int_provenance()[0])
|
|
1525
2488
|
if self._expr_is_int64_builtin(expr):
|
|
1526
2489
|
return True
|
|
1527
2490
|
if isinstance(expr, (BinOp, UnaryOp)) and self._literal_overflows_int32(expr):
|
|
1528
2491
|
# ``90 * 24 * 60 * 60 * 1000``: an int-literal product beyond
|
|
1529
2492
|
# int32 is a 64-bit value in Pine (``_pure_int_literal_value``).
|
|
1530
2493
|
return True
|
|
2494
|
+
epoch_only = getattr(self, "_wide_int_epoch_only", False)
|
|
2495
|
+
if isinstance(expr, MemberAccess):
|
|
2496
|
+
if epoch_only:
|
|
2497
|
+
return False
|
|
2498
|
+
# A UDT ``int`` field is ``int64_t`` storage; a read of one some
|
|
2499
|
+
# write fills with a wide value keeps it (``_wide_udt_int_fields``).
|
|
2500
|
+
type_name = self._udt_receiver_type_name(expr.object, owner_info)
|
|
2501
|
+
return (type_name, expr.member) in self._wide_udt_int_fields()
|
|
1531
2502
|
if isinstance(expr, FuncCall):
|
|
1532
2503
|
func_name, namespace = self._resolve_callee(expr.callee)
|
|
2504
|
+
if namespace == "request" and func_name == "security":
|
|
2505
|
+
# The requested bar's value of the payload keeps its
|
|
2506
|
+
# provenance: ``request.security(t, "60", time)`` is an epoch.
|
|
2507
|
+
return self._expr_returns_wide_int(
|
|
2508
|
+
self._security_payload_node(expr), owner_info, seen, call_site_idx
|
|
2509
|
+
)
|
|
1533
2510
|
if func_name in self._WIDE_ARRAY_ELEMENT_READS:
|
|
1534
2511
|
# An element read off a wide int array (``array.get(times, i)``
|
|
1535
2512
|
# / ``times.get(i)``) carries the epoch: the destination must
|
|
@@ -1607,6 +2584,13 @@ class TypeInferer:
|
|
|
1607
2584
|
)
|
|
1608
2585
|
)
|
|
1609
2586
|
if isinstance(expr, BinOp):
|
|
2587
|
+
if expr.op in self._NON_INTEGER_BINOPS:
|
|
2588
|
+
# Pine v6 ``/`` is a float and a comparison a bool: no
|
|
2589
|
+
# integer slot holds them, whatever the operands carry.
|
|
2590
|
+
return False
|
|
2591
|
+
if not epoch_only and self._int_arith_leaves_int32(expr, owner_info):
|
|
2592
|
+
# ``days * 86400000``: computed in 64 bits (``_visit_binop``).
|
|
2593
|
+
return True
|
|
1610
2594
|
return (
|
|
1611
2595
|
self._expr_returns_wide_int(
|
|
1612
2596
|
expr.left, owner_info, seen, call_site_idx
|
|
@@ -1616,6 +2600,8 @@ class TypeInferer:
|
|
|
1616
2600
|
)
|
|
1617
2601
|
)
|
|
1618
2602
|
if isinstance(expr, UnaryOp):
|
|
2603
|
+
if expr.op == "not":
|
|
2604
|
+
return False
|
|
1619
2605
|
return self._expr_returns_wide_int(
|
|
1620
2606
|
expr.operand, owner_info, seen, call_site_idx
|
|
1621
2607
|
)
|
|
@@ -1697,44 +2683,164 @@ class TypeInferer:
|
|
|
1697
2683
|
return "int64_t"
|
|
1698
2684
|
return cpp_type
|
|
1699
2685
|
|
|
1700
|
-
def
|
|
1701
|
-
"""Names
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2686
|
+
def _wide_int_provenance(self) -> tuple[set[str], set[tuple[str, int]]]:
|
|
2687
|
+
"""Names, and integer callable parameters, that carry an epoch.
|
|
2688
|
+
|
|
2689
|
+
A name is wide when its declaration or any ``:=``/``=`` gives it a
|
|
2690
|
+
value ``_expr_returns_wide_int`` traces to a wide source; an integer
|
|
2691
|
+
parameter (declared ``int``, or untyped) is wide when a written call
|
|
2692
|
+
passes it one. A wide name or parameter is itself a wide source, so
|
|
2693
|
+
the width travels through copies: ``int sel = switch k => E2`` over
|
|
2694
|
+
``const int E2 = timestamp(...)``, ``copy := first`` over
|
|
2695
|
+
``first := time``, and ``f(int t)`` called as ``f(time)``. Pine
|
|
2696
|
+
``int`` keeps the whole epoch in each (lab tv pf-krunerr-int64-probe1,
|
|
2697
|
+
2026-09-26: 1743468300000 where 32-bit slots read -288422176); the
|
|
2698
|
+
runtime stores it, and the na sentinel, in 64 bits, so each slot is
|
|
2699
|
+
``int64_t``. Iterated to a fixpoint and cached. Names are keyed by
|
|
2700
|
+
spelling across scopes, as the reassignment scan before this one was:
|
|
2701
|
+
a same-spelled narrow name widens with a wide one, which only drops a
|
|
2702
|
+
narrowing.
|
|
2703
|
+
"""
|
|
2704
|
+
# The epoch-only mode (``_wide_int_array_names``) keeps its own sets.
|
|
2705
|
+
cache_attr = ("_wide_int_epoch_provenance_cache"
|
|
2706
|
+
if getattr(self, "_wide_int_epoch_only", False)
|
|
2707
|
+
else "_wide_int_provenance_cache")
|
|
2708
|
+
cached = getattr(self, cache_attr, None)
|
|
1706
2709
|
if cached is not None:
|
|
1707
2710
|
return cached
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
#
|
|
1711
|
-
#
|
|
2711
|
+
names: set[str] = set()
|
|
2712
|
+
params: set[tuple[str, int]] = set()
|
|
2713
|
+
# Published before the scan: a lookup made while the fixpoint runs
|
|
2714
|
+
# reads the sets as they grow instead of starting a second scan.
|
|
2715
|
+
setattr(self, cache_attr, (names, params))
|
|
2716
|
+
bindings: list = []
|
|
2717
|
+
calls: list = []
|
|
2718
|
+
|
|
2719
|
+
def collect(node, owner) -> None:
|
|
2720
|
+
if isinstance(node, VarDecl) and node.value is not None:
|
|
2721
|
+
bindings.append((node.name, node.value, owner))
|
|
2722
|
+
elif (isinstance(node, Assignment)
|
|
2723
|
+
and isinstance(node.target, Identifier)):
|
|
2724
|
+
bindings.append((node.target.name, node.value, owner))
|
|
2725
|
+
elif isinstance(node, TupleAssign):
|
|
2726
|
+
# ``[t0, t1] = request.security(sym, tf, f())``: each name is
|
|
2727
|
+
# bound to the element its tuple evaluates.
|
|
2728
|
+
for index, name in enumerate(node.names):
|
|
2729
|
+
element = (
|
|
2730
|
+
self._tuple_element_value(node.value, index, owner)
|
|
2731
|
+
if name and name != "_" else None
|
|
2732
|
+
)
|
|
2733
|
+
if element is not None:
|
|
2734
|
+
bindings.append((name, *element))
|
|
2735
|
+
elif (isinstance(node, FuncCall)
|
|
2736
|
+
and isinstance(node.callee, Identifier)
|
|
2737
|
+
and node.callee.name in self._func_info_map):
|
|
2738
|
+
calls.append((node, owner))
|
|
2739
|
+
|
|
2740
|
+
# Callable bodies resolve their locals and parameters against their
|
|
2741
|
+
# lexical owner; everything outside them is global.
|
|
1712
2742
|
for info in getattr(self.ctx, "func_infos", ()):
|
|
1713
2743
|
node = getattr(info, "node", None)
|
|
1714
2744
|
if node is None:
|
|
1715
2745
|
continue
|
|
1716
2746
|
for child in self._walk_ast_list(node.body):
|
|
1717
|
-
|
|
1718
|
-
isinstance(child, Assignment)
|
|
1719
|
-
and isinstance(child.target, Identifier)
|
|
1720
|
-
and self._expr_returns_wide_int(
|
|
1721
|
-
child.value, info, set(), None
|
|
1722
|
-
)
|
|
1723
|
-
):
|
|
1724
|
-
targets.add(child.target.name)
|
|
2747
|
+
collect(child, info)
|
|
1725
2748
|
ast = getattr(self.ctx, "ast", None)
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
2749
|
+
for stmt in getattr(ast, "body", None) or ():
|
|
2750
|
+
if isinstance(stmt, (FuncDef, MethodDef)):
|
|
2751
|
+
continue
|
|
2752
|
+
for child in self._walk_ast(stmt):
|
|
2753
|
+
collect(child, None)
|
|
2754
|
+
|
|
2755
|
+
changed = True
|
|
2756
|
+
with self._int_width_scan():
|
|
2757
|
+
while changed:
|
|
2758
|
+
changed = False
|
|
2759
|
+
for name, value, owner in bindings:
|
|
2760
|
+
if name not in names and self._expr_returns_wide_int(
|
|
2761
|
+
value, owner, set(), None):
|
|
2762
|
+
names.add(name)
|
|
2763
|
+
changed = True
|
|
2764
|
+
for call, owner in calls:
|
|
2765
|
+
callee = self._func_info_map[call.callee.name]
|
|
2766
|
+
callee_params = list(getattr(callee.node, "params", ()) or ())
|
|
2767
|
+
bound = list(enumerate(call.args))
|
|
2768
|
+
bound += [
|
|
2769
|
+
(callee_params.index(key), arg)
|
|
2770
|
+
for key, arg in call.kwargs.items()
|
|
2771
|
+
if key in callee_params
|
|
2772
|
+
]
|
|
2773
|
+
for index, arg in bound:
|
|
2774
|
+
key = (callee.name, index)
|
|
2775
|
+
if (key not in params
|
|
2776
|
+
and self._param_is_integer_scalar(callee, index)
|
|
2777
|
+
and self._expr_returns_wide_int(
|
|
2778
|
+
arg, owner, set(), None)):
|
|
2779
|
+
params.add(key)
|
|
2780
|
+
changed = True
|
|
2781
|
+
return names, params
|
|
2782
|
+
|
|
2783
|
+
@staticmethod
|
|
2784
|
+
def _security_payload_node(call: FuncCall):
|
|
2785
|
+
"""The expression argument of a ``request.security`` call."""
|
|
2786
|
+
if len(call.args) > 2:
|
|
2787
|
+
return call.args[2]
|
|
2788
|
+
return call.kwargs.get("expression")
|
|
2789
|
+
|
|
2790
|
+
def _tuple_element_value(self, value, index: int, owner, depth: int = 0):
|
|
2791
|
+
"""``(expression, lexical owner)`` element ``index`` of a tuple value
|
|
2792
|
+
evaluates: a tuple literal's element, through a ``request.security``
|
|
2793
|
+
payload and a user function's final tuple; None otherwise."""
|
|
2794
|
+
if depth > 32:
|
|
2795
|
+
return None
|
|
2796
|
+
if isinstance(value, TupleLiteral):
|
|
2797
|
+
if index < len(value.elements):
|
|
2798
|
+
return value.elements[index], owner
|
|
2799
|
+
return None
|
|
2800
|
+
if not isinstance(value, FuncCall):
|
|
2801
|
+
return None
|
|
2802
|
+
func_name, namespace = self._resolve_callee(value.callee)
|
|
2803
|
+
if namespace == "request" and func_name == "security":
|
|
2804
|
+
return self._tuple_element_value(
|
|
2805
|
+
self._security_payload_node(value), index, owner, depth + 1)
|
|
2806
|
+
info = self._func_info_map.get(func_name) if namespace is None else None
|
|
2807
|
+
node = getattr(info, "node", None)
|
|
2808
|
+
if node is None or not node.body:
|
|
2809
|
+
return None
|
|
2810
|
+
terminal = node.body[-1]
|
|
2811
|
+
if isinstance(terminal, ExprStmt):
|
|
2812
|
+
terminal = terminal.expr
|
|
2813
|
+
return self._tuple_element_value(terminal, index, info, depth + 1)
|
|
2814
|
+
|
|
2815
|
+
def _param_is_integer_scalar(self, fi, index: int) -> bool:
|
|
2816
|
+
"""Whether parameter ``index`` of ``fi`` is a scalar integer slot:
|
|
2817
|
+
declared ``int`` or untyped, not a history parameter (those are
|
|
2818
|
+
``Series<int64_t>`` already) and not a method receiver."""
|
|
2819
|
+
node = getattr(fi, "node", None)
|
|
2820
|
+
if node is None or index >= len(node.params or ()):
|
|
2821
|
+
return False
|
|
2822
|
+
if getattr(fi, "is_udt_method", False) and index == 0:
|
|
2823
|
+
return False
|
|
2824
|
+
if node.params[index] in self.ctx.func_series_vars.get(fi.name, set()):
|
|
2825
|
+
return False
|
|
2826
|
+
declared = list(
|
|
2827
|
+
getattr(self.ctx, "func_declared_param_type_specs", {}).get(
|
|
2828
|
+
fi.name, ()
|
|
2829
|
+
)
|
|
2830
|
+
)
|
|
2831
|
+
spec = declared[index] if index < len(declared) else None
|
|
2832
|
+
if spec is None:
|
|
2833
|
+
return True
|
|
2834
|
+
return spec.kind == "primitive" and spec.name == "int"
|
|
2835
|
+
|
|
2836
|
+
def _wide_declared_int_param(self, fi, index: int, cpp_type: str) -> str:
|
|
2837
|
+
"""``int64_t`` for a declared ``int`` parameter a call feeds an epoch
|
|
2838
|
+
(``_wide_int_provenance``), else ``cpp_type`` unchanged."""
|
|
2839
|
+
if cpp_type == "int" and (fi.name, index) in self._wide_int_provenance()[1]:
|
|
2840
|
+
return "int64_t"
|
|
2841
|
+
return cpp_type
|
|
2842
|
+
|
|
2843
|
+
def _is_int64_builtin_init(self, name: str, decl=None) -> bool:
|
|
1738
2844
|
"""True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
|
|
1739
2845
|
RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
|
|
1740
2846
|
``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
|
|
@@ -1742,7 +2848,9 @@ class TypeInferer:
|
|
|
1742
2848
|
(and the full epoch-ms value, which overflows int32) in 64 bits, so
|
|
1743
2849
|
storing into ``int`` silently corrupts both the value and na detection.
|
|
1744
2850
|
A reassignment like ``var int entryTime = na`` then ``entryTime := time``
|
|
1745
|
-
must promote even though the *initializer* alone is ``na
|
|
2851
|
+
must promote even though the *initializer* alone is ``na``, and so
|
|
2852
|
+
must every name a copy of such a value reaches
|
|
2853
|
+
(``_wide_int_provenance``).
|
|
1746
2854
|
"""
|
|
1747
2855
|
expr = (
|
|
1748
2856
|
self.ctx.global_expr_map.get(name)
|
|
@@ -1750,7 +2858,565 @@ class TypeInferer:
|
|
|
1750
2858
|
)
|
|
1751
2859
|
if self._expr_is_int64_builtin(expr):
|
|
1752
2860
|
return True
|
|
1753
|
-
|
|
2861
|
+
# A script-level int constant past int32 (``_literal_wide_global``;
|
|
2862
|
+
# ``decl`` restricts it to that declaration).
|
|
2863
|
+
if self._literal_wide_global(name, decl):
|
|
2864
|
+
return True
|
|
2865
|
+
return name in self._wide_int_provenance()[0]
|
|
2866
|
+
|
|
2867
|
+
# ------------------------------------------------------------------
|
|
2868
|
+
# na-preserving double -> int narrowing
|
|
2869
|
+
# ------------------------------------------------------------------
|
|
2870
|
+
|
|
2871
|
+
# Pine functions whose declared return type is ``int`` but whose C++
|
|
2872
|
+
# lowering is a ``double``-valued expression, so the Pine-facing type is
|
|
2873
|
+
# not enough to decide whether a narrowing happens.
|
|
2874
|
+
_DOUBLE_EMITTING_INT_CALLS = {
|
|
2875
|
+
("math", "round"), ("math", "floor"), ("math", "ceil"),
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
# Built-ins whose lowering is an integer-valued engine expression, in
|
|
2879
|
+
# either the bare-variable or the call form: ``pine_bar_index()`` and the
|
|
2880
|
+
# ``pine_<field>()`` calendar helpers return ``int``, the epoch-ms readings
|
|
2881
|
+
# return ``int64_t``. ``_infer_type`` reports ``double`` for all of them
|
|
2882
|
+
# (they are ``BAR_BUILTINS``), which would wrap an already-integral value.
|
|
2883
|
+
_INTEGRAL_BUILTINS = frozenset({
|
|
2884
|
+
"bar_index", "last_bar_index",
|
|
2885
|
+
"year", "month", "dayofmonth", "dayofweek",
|
|
2886
|
+
"hour", "minute", "second", "weekofyear",
|
|
2887
|
+
"time", "timenow", "time_close", "last_bar_time", "time_tradingday",
|
|
2888
|
+
})
|
|
2889
|
+
|
|
2890
|
+
# ``strategy.closedtrades`` / ``strategy.opentrades`` read the trade-book
|
|
2891
|
+
# sizes, emitted as ``((int)trades_.size())``.
|
|
2892
|
+
_INTEGRAL_STRATEGY_MEMBERS = frozenset({"closedtrades", "opentrades"})
|
|
2893
|
+
|
|
2894
|
+
# Only an integer literal is width-independent. An integer ``na`` value,
|
|
2895
|
+
# input read, or prior guarded cast can still lose its sentinel when it
|
|
2896
|
+
# crosses into the other integer width, so those must be checked again.
|
|
2897
|
+
_INTEGRAL_CPP_TEXT = re.compile(
|
|
2898
|
+
r"^\s*(?:\(\s*)*[-+]?(?:0[xX][0-9a-fA-F]+|\d+)(?:LL|ULL|L|U)?"
|
|
2899
|
+
r"(?:\s*\))*\s*$"
|
|
2900
|
+
)
|
|
2901
|
+
|
|
2902
|
+
def _emitted_value_is_double(self, node) -> bool:
|
|
2903
|
+
"""True when the C++ expression emitted for ``node`` has type ``double``.
|
|
2904
|
+
|
|
2905
|
+
This is deliberately *not* ``_infer_type(node) == "double"``.
|
|
2906
|
+
``_infer_type`` answers "what type does this slot hold", which for
|
|
2907
|
+
``math.round(x)`` is Pine's ``int`` even though the emission is
|
|
2908
|
+
``std::round(x)``, a ``double`` — and which falls back to ``double``
|
|
2909
|
+
for any shape it cannot resolve, including integer arithmetic. Only
|
|
2910
|
+
this function's answer decides whether a value reaching an integer
|
|
2911
|
+
slot needs the na-preserving cast.
|
|
2912
|
+
"""
|
|
2913
|
+
if node is None:
|
|
2914
|
+
return False
|
|
2915
|
+
if isinstance(node, NumberLiteral):
|
|
2916
|
+
return isinstance(node.value, float)
|
|
2917
|
+
if isinstance(node, (BoolLiteral, StringLiteral, ColorLiteral)):
|
|
2918
|
+
# A colour literal lowers to a packed-ARGB ``int64_t`` literal.
|
|
2919
|
+
return False
|
|
2920
|
+
if isinstance(node, Subscript) and isinstance(node.object, Identifier):
|
|
2921
|
+
# A history read keeps the series' element type.
|
|
2922
|
+
base = node.object.name
|
|
2923
|
+
if base in self._INTEGRAL_BUILTINS:
|
|
2924
|
+
return False
|
|
2925
|
+
if base in self.ctx.series_vars or base in self.ctx.series_bar_fields:
|
|
2926
|
+
return self._series_type_for(base) == "double"
|
|
2927
|
+
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
2928
|
+
# color.* lowers to a packed-ARGB integer constant; session.*
|
|
2929
|
+
# lowers to a ``pine_session_*`` predicate (bool).
|
|
2930
|
+
if node.object.name in ("color", "session"):
|
|
2931
|
+
return False
|
|
2932
|
+
if (node.object.name == "strategy"
|
|
2933
|
+
and node.member in self._INTEGRAL_STRATEGY_MEMBERS):
|
|
2934
|
+
return False
|
|
2935
|
+
if isinstance(node, NaLiteral):
|
|
2936
|
+
return True
|
|
2937
|
+
if isinstance(node, UnaryOp):
|
|
2938
|
+
if node.op in ("not", "!"):
|
|
2939
|
+
return False
|
|
2940
|
+
return self._emitted_value_is_double(node.operand)
|
|
2941
|
+
if isinstance(node, BinOp):
|
|
2942
|
+
if node.op in _BOOL_RESULT_BINOPS:
|
|
2943
|
+
return False
|
|
2944
|
+
if node.op in ("/", "%"):
|
|
2945
|
+
# Both lower through an explicit double form
|
|
2946
|
+
# (``(double)a / (double)b`` / ``std::fmod``).
|
|
2947
|
+
return True
|
|
2948
|
+
if self._na_aware_wide_is_double(node):
|
|
2949
|
+
# ``_wide_int_arith_cpp``'s na-aware form.
|
|
2950
|
+
return True
|
|
2951
|
+
return (self._emitted_value_is_double(node.left)
|
|
2952
|
+
or self._emitted_value_is_double(node.right))
|
|
2953
|
+
if isinstance(node, Ternary):
|
|
2954
|
+
# C++ gives a mixed int/double conditional the common type
|
|
2955
|
+
# ``double``, so one double arm narrows the whole expression.
|
|
2956
|
+
return (self._emitted_value_is_double(node.true_val)
|
|
2957
|
+
or self._emitted_value_is_double(node.false_val))
|
|
2958
|
+
if isinstance(node, Identifier):
|
|
2959
|
+
name = node.name
|
|
2960
|
+
if name in getattr(self, "_current_loop_vars", set()):
|
|
2961
|
+
# A counted-loop binder is emitted as ``for (int i = ...)``,
|
|
2962
|
+
# whatever ``_infer_type`` reports for the Pine binding.
|
|
2963
|
+
return False
|
|
2964
|
+
if (name in self._INTEGRAL_BUILTINS
|
|
2965
|
+
and name not in self._current_func_param_types
|
|
2966
|
+
and name not in self._known_vars):
|
|
2967
|
+
return False
|
|
2968
|
+
# Builtins, constants and parameters are resolved precisely by
|
|
2969
|
+
# ``_infer_type``; a user variable's *storage* is what the read
|
|
2970
|
+
# emits, and that can be a double where the Pine type is an int
|
|
2971
|
+
# (a hoisted global is declared from its initializer).
|
|
2972
|
+
if not (name in BAR_FIELDS or name in BAR_BUILTINS
|
|
2973
|
+
or name in self._known_vars
|
|
2974
|
+
or name in getattr(
|
|
2975
|
+
self, "_current_func_series_param_types", {})
|
|
2976
|
+
or name in self._current_func_param_types):
|
|
2977
|
+
declared = self._slot_scalar_cpp_type(name)
|
|
2978
|
+
if declared is not None:
|
|
2979
|
+
return declared == "double"
|
|
2980
|
+
if isinstance(node, FuncCall):
|
|
2981
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
2982
|
+
if (namespace, func_name) in self._DOUBLE_EMITTING_INT_CALLS:
|
|
2983
|
+
# The 2-arg precision overload is a float in Pine as well and
|
|
2984
|
+
# falls through to the generic check.
|
|
2985
|
+
return len(node.args) + len(node.kwargs or {}) == 1
|
|
2986
|
+
if namespace is None and func_name in ("int", "bool"):
|
|
2987
|
+
# Already spelled as an explicit, na-preserving cast.
|
|
2988
|
+
return False
|
|
2989
|
+
if namespace is None and func_name == "nz" and node.args:
|
|
2990
|
+
# ``nz(x)`` / ``nz(x, y)`` keep the replaced value's type.
|
|
2991
|
+
return self._emitted_value_is_double(node.args[0])
|
|
2992
|
+
if namespace == "timeframe" and func_name == "in_seconds":
|
|
2993
|
+
return False
|
|
2994
|
+
if namespace == "session":
|
|
2995
|
+
return False
|
|
2996
|
+
if namespace is None and func_name in self._INTEGRAL_BUILTINS:
|
|
2997
|
+
# ``year(t)`` / ``hour(t, tz)`` lower to the same integer
|
|
2998
|
+
# ``pine_<field>()`` helpers as their bare-variable forms.
|
|
2999
|
+
return False
|
|
3000
|
+
if namespace == "color":
|
|
3001
|
+
# Every color.* lowering is an integer (packed ARGB, or a
|
|
3002
|
+
# channel read).
|
|
3003
|
+
return False
|
|
3004
|
+
if namespace == "ta":
|
|
3005
|
+
# Every ta.* call site lowers to a ta:: class
|
|
3006
|
+
# ``compute()``/``recompute()``, which returns a double even
|
|
3007
|
+
# where Pine's type is an int (``ta.highestbars``,
|
|
3008
|
+
# ``ta.barssince``, ...).
|
|
3009
|
+
return func_name not in TA_RETURNS_BOOL
|
|
3010
|
+
return self._infer_type(node) == "double"
|
|
3011
|
+
|
|
3012
|
+
def _slot_scalar_cpp_type(self, name: str) -> str | None:
|
|
3013
|
+
"""The C++ type of the storage a write to the scalar ``name`` lands in.
|
|
3014
|
+
|
|
3015
|
+
Reads the declaration sites in the order the emitter itself uses:
|
|
3016
|
+
``var`` member, function-local, parameter, then hoisted global member.
|
|
3017
|
+
Unlike ``_na_reassign_cpp_type`` the global case mirrors
|
|
3018
|
+
``base.generate``'s member layout (``_infer_type`` of the initializer)
|
|
3019
|
+
rather than the analyzer's ``PineType``: the two disagree for
|
|
3020
|
+
e.g. ``x = math.round(...)``, and the declaration is the real storage.
|
|
3021
|
+
"""
|
|
3022
|
+
for vname, ptype, _init in self.ctx.var_members:
|
|
3023
|
+
if vname == name:
|
|
3024
|
+
return PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
3025
|
+
local = getattr(self, "_current_func_local_types", {}).get(name)
|
|
3026
|
+
if local is not None:
|
|
3027
|
+
return local
|
|
3028
|
+
param = getattr(self, "_current_func_param_types", {}).get(name)
|
|
3029
|
+
if param is not None:
|
|
3030
|
+
return param
|
|
3031
|
+
for gname, gptype in self.ctx.global_var_decls:
|
|
3032
|
+
if gname != name:
|
|
3033
|
+
continue
|
|
3034
|
+
if self._global_color_hint(name):
|
|
3035
|
+
return "int64_t"
|
|
3036
|
+
if self._global_bool_hint(name):
|
|
3037
|
+
return "bool"
|
|
3038
|
+
if (name in getattr(self, "_direct_program_tuple_binding_names", ())
|
|
3039
|
+
and gptype == PineType.BOOL):
|
|
3040
|
+
return "bool"
|
|
3041
|
+
if (gptype == PineType.STRING
|
|
3042
|
+
and name in self._security_tuple_binding_names()):
|
|
3043
|
+
return "std::string"
|
|
3044
|
+
if name in self._nonfinite_int_names():
|
|
3045
|
+
return "double"
|
|
3046
|
+
expr = getattr(self.ctx, "global_expr_map", {}).get(name)
|
|
3047
|
+
if expr is not None:
|
|
3048
|
+
return self._infer_type(expr)
|
|
3049
|
+
return PINE_TYPE_TO_CPP.get(gptype, "double")
|
|
3050
|
+
return None
|
|
3051
|
+
|
|
3052
|
+
# ------------------------------------------------------------------
|
|
3053
|
+
# A Pine int TradingView holds as +-Infinity
|
|
3054
|
+
# ------------------------------------------------------------------
|
|
3055
|
+
|
|
3056
|
+
_NONFINITE_INT_READ_OPS = frozenset({"<", ">", "<=", ">=", "==", "!="})
|
|
3057
|
+
NONFINITE_INT_ORDERING_OPS = frozenset({"<", ">", "<=", ">="})
|
|
3058
|
+
|
|
3059
|
+
def _nonfinite_int_names(self) -> frozenset[str]:
|
|
3060
|
+
"""Plain global ``int`` names stored as ``double`` to keep an infinity.
|
|
3061
|
+
|
|
3062
|
+
``math.floor(100 / 0)`` is +Infinity in TradingView, even in an ``int``
|
|
3063
|
+
variable: ``>``/``>=`` against any number hold and ``<``/``<=`` do not,
|
|
3064
|
+
while ``na()`` reads it as na, ``==``/``!=`` are false, ``str.tostring``
|
|
3065
|
+
prints NaN and ``strategy.entry(qty = ...)`` trades the default quantity
|
|
3066
|
+
(lab tv tailc-a-na-compare, tailc-a-divzero-readout,
|
|
3067
|
+
tailc-a-qty-nonfinite2; R5 lane TAIL-C). A C++ ``int`` narrows it to
|
|
3068
|
+
``na<int>()``, which orders as na. So a name is stored as the ``double``
|
|
3069
|
+
its value already is when every binding of it -- a global declaration
|
|
3070
|
+
or ``:=`` -- is a one-argument ``math.floor``/``math.ceil``/
|
|
3071
|
+
``math.round``, some read of it is an ordering comparison's operand --
|
|
3072
|
+
the one read whose answer an infinity changes -- and every read of it
|
|
3073
|
+
is an operand of a comparison, the argument of ``na``/``nz``/
|
|
3074
|
+
``str.tostring`` or the quantity of ``strategy.entry``/
|
|
3075
|
+
``strategy.order``. Its reads keep the int's old answers
|
|
3076
|
+
(``_nonfinite_int_read``): an infinity reads na, except as the operand
|
|
3077
|
+
of an ordering comparison, which reads it as the number.
|
|
3078
|
+
"""
|
|
3079
|
+
cached = getattr(self, "_nonfinite_int_names_cache", None)
|
|
3080
|
+
if cached is not None:
|
|
3081
|
+
return cached
|
|
3082
|
+
self._nonfinite_int_names_cache = frozenset()
|
|
3083
|
+
ast = getattr(self.ctx, "ast", None)
|
|
3084
|
+
if ast is None:
|
|
3085
|
+
return self._nonfinite_int_names_cache
|
|
3086
|
+
# Read off the program and the analyzer alone, so every pass of the
|
|
3087
|
+
# emitter, the first included, gets the same answer.
|
|
3088
|
+
candidates = {
|
|
3089
|
+
name for name, _ptype in self.ctx.global_var_decls
|
|
3090
|
+
if name not in self.ctx.series_vars
|
|
3091
|
+
}
|
|
3092
|
+
if not candidates:
|
|
3093
|
+
return self._nonfinite_int_names_cache
|
|
3094
|
+
bound: set[str] = set()
|
|
3095
|
+
rejected: set[str] = set()
|
|
3096
|
+
ordered: set[str] = set()
|
|
3097
|
+
|
|
3098
|
+
def floor_call(value) -> bool:
|
|
3099
|
+
if not isinstance(value, FuncCall) or value.kwargs or len(value.args) != 1:
|
|
3100
|
+
return False
|
|
3101
|
+
func_name, namespace = self._resolve_callee(value.callee)
|
|
3102
|
+
return (namespace, func_name) in self._DOUBLE_EMITTING_INT_CALLS
|
|
3103
|
+
|
|
3104
|
+
def allowed_read(parent, slot: str, index: int) -> bool:
|
|
3105
|
+
if isinstance(parent, BinOp):
|
|
3106
|
+
return parent.op in self._NONFINITE_INT_READ_OPS
|
|
3107
|
+
if not isinstance(parent, FuncCall):
|
|
3108
|
+
return False
|
|
3109
|
+
func_name, namespace = self._resolve_callee(parent.callee)
|
|
3110
|
+
if slot == "args" and index == 0 and (
|
|
3111
|
+
(namespace is None and func_name in ("na", "nz"))
|
|
3112
|
+
or (namespace, func_name) == ("str", "tostring")):
|
|
3113
|
+
return True
|
|
3114
|
+
return (namespace == "strategy" and func_name in ("entry", "order")
|
|
3115
|
+
and (slot == "kwargs:qty" or (slot == "args" and index == 2)))
|
|
3116
|
+
|
|
3117
|
+
def children(node):
|
|
3118
|
+
for slot, value in vars(node).items():
|
|
3119
|
+
if isinstance(value, ASTNode):
|
|
3120
|
+
yield slot, 0, value
|
|
3121
|
+
elif isinstance(value, dict):
|
|
3122
|
+
for key, item in value.items():
|
|
3123
|
+
if isinstance(item, ASTNode):
|
|
3124
|
+
yield f"{slot}:{key}", 0, item
|
|
3125
|
+
elif isinstance(value, (list, tuple)):
|
|
3126
|
+
for index, item in enumerate(value):
|
|
3127
|
+
if isinstance(item, ASTNode):
|
|
3128
|
+
yield slot, index, item
|
|
3129
|
+
elif isinstance(item, (list, tuple)):
|
|
3130
|
+
for inner in item:
|
|
3131
|
+
if isinstance(inner, ASTNode):
|
|
3132
|
+
yield slot, index, inner
|
|
3133
|
+
elif isinstance(inner, list):
|
|
3134
|
+
for leaf in inner:
|
|
3135
|
+
if isinstance(leaf, ASTNode):
|
|
3136
|
+
yield slot, index, leaf
|
|
3137
|
+
|
|
3138
|
+
pending = [(ast, False)]
|
|
3139
|
+
while pending:
|
|
3140
|
+
node, in_callable = pending.pop()
|
|
3141
|
+
if isinstance(node, (FuncDef, MethodDef)):
|
|
3142
|
+
# A parameter or a local of that spelling would read through
|
|
3143
|
+
# the global's rule.
|
|
3144
|
+
rejected.update(set(getattr(node, "params", None) or ()) & candidates)
|
|
3145
|
+
in_callable = True
|
|
3146
|
+
if isinstance(node, VarDecl) and node.name in candidates:
|
|
3147
|
+
if (in_callable or node.is_var or node.is_varip
|
|
3148
|
+
or node.type_hint not in (None, "int")
|
|
3149
|
+
or not floor_call(node.value)):
|
|
3150
|
+
rejected.add(node.name)
|
|
3151
|
+
bound.add(node.name)
|
|
3152
|
+
elif isinstance(node, TupleAssign):
|
|
3153
|
+
rejected.update(set(node.names) & candidates)
|
|
3154
|
+
elif (isinstance(node, Assignment) and isinstance(node.target, Identifier)
|
|
3155
|
+
and node.target.name in candidates):
|
|
3156
|
+
if node.op != ":=" or not floor_call(node.value):
|
|
3157
|
+
rejected.add(node.target.name)
|
|
3158
|
+
bound.add(node.target.name)
|
|
3159
|
+
for slot, index, child in children(node):
|
|
3160
|
+
if isinstance(child, Identifier) and child.name in candidates:
|
|
3161
|
+
binding_target = (isinstance(node, Assignment) and slot == "target")
|
|
3162
|
+
if not binding_target and not allowed_read(node, slot, index):
|
|
3163
|
+
rejected.add(child.name)
|
|
3164
|
+
if (isinstance(node, BinOp)
|
|
3165
|
+
and node.op in self.NONFINITE_INT_ORDERING_OPS):
|
|
3166
|
+
ordered.add(child.name)
|
|
3167
|
+
pending.append((child, in_callable))
|
|
3168
|
+
self._nonfinite_int_names_cache = frozenset((bound & ordered) - rejected)
|
|
3169
|
+
return self._nonfinite_int_names_cache
|
|
3170
|
+
|
|
3171
|
+
def _global_color_hint(self, name: str) -> bool:
|
|
3172
|
+
"""A top-level ``color`` declaration must keep its 64-bit na sentinel."""
|
|
3173
|
+
return any(
|
|
3174
|
+
isinstance(stmt, VarDecl)
|
|
3175
|
+
and stmt.name == name
|
|
3176
|
+
and stmt.type_hint == "color"
|
|
3177
|
+
for stmt in self.ctx.ast.body
|
|
3178
|
+
)
|
|
3179
|
+
|
|
3180
|
+
def _global_bool_hint(self, name: str) -> bool:
|
|
3181
|
+
"""A top-level declared bool keeps its type when initialized by na."""
|
|
3182
|
+
return any(
|
|
3183
|
+
isinstance(stmt, VarDecl)
|
|
3184
|
+
and stmt.name == name
|
|
3185
|
+
and stmt.type_hint == "bool"
|
|
3186
|
+
for stmt in self.ctx.ast.body
|
|
3187
|
+
)
|
|
3188
|
+
|
|
3189
|
+
def _int_slot_cpp_type(self, name: str | None,
|
|
3190
|
+
declared: str | None = None) -> str | None:
|
|
3191
|
+
"""``"int"``/``"int64_t"`` when a write to this slot narrows, else ``None``.
|
|
3192
|
+
|
|
3193
|
+
``declared`` short-circuits the lookup for an emitter that already
|
|
3194
|
+
knows the storage type it is about to spell (a local declaration).
|
|
3195
|
+
"""
|
|
3196
|
+
if declared is not None:
|
|
3197
|
+
resolved = declared
|
|
3198
|
+
elif name is None:
|
|
3199
|
+
return None
|
|
3200
|
+
else:
|
|
3201
|
+
resolved = self._slot_scalar_cpp_type(name)
|
|
3202
|
+
if (resolved == "int" and name is not None
|
|
3203
|
+
and self._is_int64_builtin_init(name)):
|
|
3204
|
+
resolved = "int64_t"
|
|
3205
|
+
return resolved if resolved in (*NA_PRESERVING_INT_TYPES, "bool") else None
|
|
3206
|
+
|
|
3207
|
+
def _func_param_int_cpp_type(self, fi, index: int,
|
|
3208
|
+
call_site_idx: int | None) -> str | None:
|
|
3209
|
+
"""``"int"``/``"int64_t"`` when parameter ``index`` of ``fi`` is an integer.
|
|
3210
|
+
|
|
3211
|
+
Mirrors the parameter-type branches of ``emit_top._emit_func_def`` in
|
|
3212
|
+
the same order, keeping only the outcomes that are integers: a value
|
|
3213
|
+
crossing into one of those parameters narrows, and Pine's ``color`` is
|
|
3214
|
+
one of them (the engine packs a color as an integer). History
|
|
3215
|
+
parameters become ``const Series<T>&`` and are not a narrowing slot.
|
|
3216
|
+
"""
|
|
3217
|
+
node = getattr(fi, "node", None)
|
|
3218
|
+
if node is None or index >= len(node.params or ()):
|
|
3219
|
+
return None
|
|
3220
|
+
param = node.params[index]
|
|
3221
|
+
if getattr(fi, "is_udt_method", False) and index == 0:
|
|
3222
|
+
return None
|
|
3223
|
+
if fi.name == "isInSession" and index < 2:
|
|
3224
|
+
return None
|
|
3225
|
+
if param in self.ctx.func_series_vars.get(fi.name, set()):
|
|
3226
|
+
return None
|
|
3227
|
+
declared = list(
|
|
3228
|
+
getattr(self.ctx, "func_declared_param_type_specs", {}).get(
|
|
3229
|
+
fi.name, ()
|
|
3230
|
+
)
|
|
3231
|
+
)
|
|
3232
|
+
variant = (
|
|
3233
|
+
getattr(self.ctx, "func_callsite_param_types", {}).get(
|
|
3234
|
+
(fi.name, call_site_idx), ()
|
|
3235
|
+
)
|
|
3236
|
+
if call_site_idx is not None
|
|
3237
|
+
else ()
|
|
3238
|
+
)
|
|
3239
|
+
if (index >= len(declared) or declared[index] is None) and index < len(variant):
|
|
3240
|
+
return {
|
|
3241
|
+
PineType.INT: "int64_t",
|
|
3242
|
+
PineType.COLOR: "int64_t",
|
|
3243
|
+
}.get(variant[index])
|
|
3244
|
+
specs = getattr(fi, "param_type_specs", []) or []
|
|
3245
|
+
if index < len(specs) and specs[index] is not None:
|
|
3246
|
+
cpp_t = self._wide_declared_int_param(
|
|
3247
|
+
fi, index, self._type_spec_to_cpp(specs[index]))
|
|
3248
|
+
return cpp_t if cpp_t in NA_PRESERVING_INT_TYPES else None
|
|
3249
|
+
if index < len(fi.param_types):
|
|
3250
|
+
cpp_t = self._wide_declared_int_param(
|
|
3251
|
+
fi, index, PINE_TYPE_TO_CPP.get(fi.param_types[index], "double"))
|
|
3252
|
+
return cpp_t if cpp_t in NA_PRESERVING_INT_TYPES else None
|
|
3253
|
+
return None
|
|
3254
|
+
|
|
3255
|
+
def _func_param_is_bool(self, fi, index: int,
|
|
3256
|
+
call_site_idx: int | None = None) -> bool:
|
|
3257
|
+
"""Whether one emitted user-function parameter is Pine ``bool``."""
|
|
3258
|
+
node = getattr(fi, "node", None)
|
|
3259
|
+
if node is None or index >= len(node.params or ()):
|
|
3260
|
+
return False
|
|
3261
|
+
declared = list(
|
|
3262
|
+
getattr(self.ctx, "func_declared_param_type_specs", {}).get(
|
|
3263
|
+
fi.name, ()
|
|
3264
|
+
)
|
|
3265
|
+
)
|
|
3266
|
+
if index < len(declared) and declared[index] is not None:
|
|
3267
|
+
spec = declared[index]
|
|
3268
|
+
return spec.kind == "primitive" and spec.name == "bool"
|
|
3269
|
+
variant = (
|
|
3270
|
+
getattr(self.ctx, "func_callsite_param_types", {}).get(
|
|
3271
|
+
(fi.name, call_site_idx), ()
|
|
3272
|
+
)
|
|
3273
|
+
if call_site_idx is not None
|
|
3274
|
+
else ()
|
|
3275
|
+
)
|
|
3276
|
+
if index < len(variant):
|
|
3277
|
+
return variant[index] == PineType.BOOL
|
|
3278
|
+
specs = getattr(fi, "param_type_specs", []) or []
|
|
3279
|
+
if index < len(specs) and specs[index] is not None:
|
|
3280
|
+
spec = specs[index]
|
|
3281
|
+
return spec.kind == "primitive" and spec.name == "bool"
|
|
3282
|
+
return index < len(fi.param_types) and fi.param_types[index] == PineType.BOOL
|
|
3283
|
+
|
|
3284
|
+
def _udt_field_int_cpp_type(self, target_node) -> str | None:
|
|
3285
|
+
"""Integer C++ type of a UDT field write target, else ``None``.
|
|
3286
|
+
|
|
3287
|
+
Mirrors the record layout in ``base.generate``, which widens a Pine
|
|
3288
|
+
``int`` field to ``int64_t`` so epoch values and the na sentinel fit.
|
|
3289
|
+
"""
|
|
3290
|
+
if not isinstance(target_node, MemberAccess):
|
|
3291
|
+
return None
|
|
3292
|
+
owner = self._type_spec_from_expr(target_node.object)
|
|
3293
|
+
if owner is None or owner.kind != "udt" or not owner.name:
|
|
3294
|
+
return None
|
|
3295
|
+
spec = (self._udt_field_type_specs.get(owner.name) or {}).get(
|
|
3296
|
+
target_node.member
|
|
3297
|
+
)
|
|
3298
|
+
if spec is None:
|
|
3299
|
+
return None
|
|
3300
|
+
cpp_type = self._type_spec_to_cpp(spec)
|
|
3301
|
+
if cpp_type == "int":
|
|
3302
|
+
cpp_type = "int64_t"
|
|
3303
|
+
return cpp_type if cpp_type in (*NA_PRESERVING_INT_TYPES, "bool") else None
|
|
3304
|
+
|
|
3305
|
+
def _coerce_int_slot(self, cpp_val: str, node, target_cpp_type: str | None,
|
|
3306
|
+
*, value_is_double: bool | None = None) -> str:
|
|
3307
|
+
"""Route a value into an ``int``/``int64_t`` slot without losing ``na``.
|
|
3308
|
+
|
|
3309
|
+
A double NaN cannot be narrowed implicitly, and an integer na
|
|
3310
|
+
sentinel cannot cross into the other integer width without losing
|
|
3311
|
+
its sentinel. Same-width integral slots keep their established C++
|
|
3312
|
+
spelling; width changes check the original source value first.
|
|
3313
|
+
``value_is_double`` overrides the AST judgement for emitters that
|
|
3314
|
+
build their own expression text.
|
|
3315
|
+
"""
|
|
3316
|
+
if target_cpp_type == "bool":
|
|
3317
|
+
return self._coerce_bool_expr(cpp_val, node)
|
|
3318
|
+
if target_cpp_type not in NA_PRESERVING_INT_TYPES:
|
|
3319
|
+
return cpp_val
|
|
3320
|
+
if cpp_val in {"na<double>()", "na<int>()", "na<int64_t>()"}:
|
|
3321
|
+
# A bare na needs no round trip through the source width.
|
|
3322
|
+
return f"na<{target_cpp_type}>()"
|
|
3323
|
+
if self._INTEGRAL_CPP_TEXT.match(cpp_val):
|
|
3324
|
+
return cpp_val
|
|
3325
|
+
if ((target_cpp_type == "int" and cpp_val.startswith("get_input_int("))
|
|
3326
|
+
or (target_cpp_type == "int64_t"
|
|
3327
|
+
and cpp_val.startswith("get_input_int64("))):
|
|
3328
|
+
# Engine ABI declarations return exactly these widths, and their
|
|
3329
|
+
# integer na sentinel already matches the destination slot.
|
|
3330
|
+
return cpp_val
|
|
3331
|
+
if value_is_double is None:
|
|
3332
|
+
value_is_double = self._emitted_value_is_double(node)
|
|
3333
|
+
if value_is_double:
|
|
3334
|
+
return na_preserving_int_cast(cpp_val, target_cpp_type)
|
|
3335
|
+
if node is None:
|
|
3336
|
+
return cpp_val
|
|
3337
|
+
source_cpp_type = self._infer_type(node)
|
|
3338
|
+
if (isinstance(node, Identifier)
|
|
3339
|
+
and self._is_int64_builtin_init(node.name)):
|
|
3340
|
+
source_cpp_type = "int64_t"
|
|
3341
|
+
if (source_cpp_type in NA_PRESERVING_INT_TYPES
|
|
3342
|
+
and source_cpp_type != target_cpp_type):
|
|
3343
|
+
return na_preserving_int_cast(cpp_val, target_cpp_type)
|
|
3344
|
+
return cpp_val
|
|
3345
|
+
|
|
3346
|
+
def _coerce_int_slot_with_cast(
|
|
3347
|
+
self, cpp_val: str, node, target_cpp_type: str
|
|
3348
|
+
) -> str:
|
|
3349
|
+
"""Preserve an explicit integral cast when no Na conversion is needed.
|
|
3350
|
+
|
|
3351
|
+
A few public textual contracts intentionally show the destination cast
|
|
3352
|
+
(drawing ABI coordinates and security history offsets). Keep that
|
|
3353
|
+
spelling for values already proven integral; double-valued expressions
|
|
3354
|
+
still take `_coerce_int_slot`'s na-preserving lambda.
|
|
3355
|
+
"""
|
|
3356
|
+
rendered = self._coerce_int_slot(cpp_val, node, target_cpp_type)
|
|
3357
|
+
if (rendered == cpp_val
|
|
3358
|
+
and (self._INTEGRAL_CPP_TEXT.match(cpp_val)
|
|
3359
|
+
or not self._emitted_value_is_double(node))):
|
|
3360
|
+
return f"({target_cpp_type})({cpp_val})"
|
|
3361
|
+
return rendered
|
|
3362
|
+
|
|
3363
|
+
def _coerce_bool_expr(self, cpp_val: str, node=None) -> str:
|
|
3364
|
+
"""Lower a Pine scalar used in a boolean context.
|
|
3365
|
+
|
|
3366
|
+
Pine treats ``na`` as false when a numeric expression is consumed by
|
|
3367
|
+
``if``, a ternary, ``and``/``or``, or a boolean parameter. A C++
|
|
3368
|
+
conversion does not: NaN and the integer ``na`` sentinel are both
|
|
3369
|
+
truthy. Keep compile-time literals and expressions already typed as
|
|
3370
|
+
``bool`` byte-identical; every other numeric scalar goes through the
|
|
3371
|
+
one single-evaluation runtime helper.
|
|
3372
|
+
|
|
3373
|
+
The helper deliberately does not reject an unresolved shape. The
|
|
3374
|
+
analyzer has already admitted the expression, and preserving its
|
|
3375
|
+
existing lowering with a warning is preferable to introducing a new
|
|
3376
|
+
refusal for a population script.
|
|
3377
|
+
"""
|
|
3378
|
+
# Text-only helper paths (notably security registration) do not carry
|
|
3379
|
+
# an AST node from which to prove the scalar type. Keep their
|
|
3380
|
+
# established spelling; the typed chart/security visitors pass the
|
|
3381
|
+
# original node and take the exact path below.
|
|
3382
|
+
if node is None:
|
|
3383
|
+
return cpp_val
|
|
3384
|
+
if cpp_val.strip() in {"true", "false", "na<bool>()"}:
|
|
3385
|
+
return cpp_val
|
|
3386
|
+
if cpp_val.strip() in {
|
|
3387
|
+
"is_first_tick()", "is_last_tick_", "barstate_islast_",
|
|
3388
|
+
"tf_change(prev_bar_timestamp_, current_bar_.timestamp)",
|
|
3389
|
+
}:
|
|
3390
|
+
return cpp_val
|
|
3391
|
+
if isinstance(node, FuncCall):
|
|
3392
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
3393
|
+
if namespace == "timeframe" and func_name == "change":
|
|
3394
|
+
return cpp_val
|
|
3395
|
+
if namespace == "session":
|
|
3396
|
+
return cpp_val
|
|
3397
|
+
if (isinstance(node, MemberAccess)
|
|
3398
|
+
and isinstance(node.object, Identifier)
|
|
3399
|
+
and node.object.name in {
|
|
3400
|
+
"barstate", "chart", "session", "timeframe"
|
|
3401
|
+
}
|
|
3402
|
+
and self._infer_type(node) == "bool"):
|
|
3403
|
+
return cpp_val
|
|
3404
|
+
inferred = self._infer_type(node)
|
|
3405
|
+
if inferred == "bool":
|
|
3406
|
+
return cpp_val
|
|
3407
|
+
if inferred not in {"double", "int", "int64_t"}:
|
|
3408
|
+
# Non-numeric values cannot carry Pine's numeric na sentinel. The
|
|
3409
|
+
# ordinary C++ conversion is the established fallback for handles
|
|
3410
|
+
# and other engine values.
|
|
3411
|
+
return cpp_val
|
|
3412
|
+
if isinstance(node, (NumberLiteral, BoolLiteral, ColorLiteral)):
|
|
3413
|
+
return cpp_val
|
|
3414
|
+
if (isinstance(node, Identifier)
|
|
3415
|
+
and node.name in self._known_vars
|
|
3416
|
+
and node.name not in self._input_backed_vars
|
|
3417
|
+
and not self._known_var_is_lexically_shadowed(node.name)):
|
|
3418
|
+
return cpp_val
|
|
3419
|
+
return pine_truth_cast(cpp_val)
|
|
1754
3420
|
|
|
1755
3421
|
def _na_reassign_cpp_type(self, name: str) -> str | None:
|
|
1756
3422
|
"""Declared scalar C++ type of a ``:=`` reassignment target ``name``, so a
|
|
@@ -1794,7 +3460,11 @@ class TypeInferer:
|
|
|
1794
3460
|
if cpp_type is None:
|
|
1795
3461
|
for gname, gptype in self.ctx.global_var_decls:
|
|
1796
3462
|
if gname == name:
|
|
1797
|
-
cpp_type =
|
|
3463
|
+
cpp_type = (
|
|
3464
|
+
"int64_t" if self._global_color_hint(name)
|
|
3465
|
+
else "bool" if self._global_bool_hint(name)
|
|
3466
|
+
else PINE_TYPE_TO_CPP.get(gptype, "double")
|
|
3467
|
+
)
|
|
1798
3468
|
break
|
|
1799
3469
|
if cpp_type is None:
|
|
1800
3470
|
return None
|
|
@@ -1935,6 +3605,12 @@ class TypeInferer:
|
|
|
1935
3605
|
and ternaries / if / switch expressions. Returns the string
|
|
1936
3606
|
``"double"`` as the safe fallback when no narrower type can be
|
|
1937
3607
|
determined."""
|
|
3608
|
+
if isinstance(node, Subscript) and isinstance(node.object, Identifier):
|
|
3609
|
+
name = node.object.name
|
|
3610
|
+
if name in self.ctx.series_vars:
|
|
3611
|
+
return self._series_type_for(name)
|
|
3612
|
+
if name in self.ctx.series_bar_fields:
|
|
3613
|
+
return "double"
|
|
1938
3614
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
1939
3615
|
# A callable-result history read keeps the callable's scalar
|
|
1940
3616
|
# family. Collection subscripts follow separate element-type paths.
|
|
@@ -1943,6 +3619,8 @@ class TypeInferer:
|
|
|
1943
3619
|
return "double" if isinstance(node.value, float) else "int"
|
|
1944
3620
|
if isinstance(node, BoolLiteral):
|
|
1945
3621
|
return "bool"
|
|
3622
|
+
if isinstance(node, ColorLiteral):
|
|
3623
|
+
return "int64_t"
|
|
1946
3624
|
if isinstance(node, StringLiteral):
|
|
1947
3625
|
return "std::string"
|
|
1948
3626
|
if isinstance(node, NaLiteral):
|
|
@@ -1973,7 +3651,7 @@ class TypeInferer:
|
|
|
1973
3651
|
return self._current_func_local_types[node.name]
|
|
1974
3652
|
if node.name in getattr(self, "_current_loop_vars", set()):
|
|
1975
3653
|
return "double"
|
|
1976
|
-
sym = self.
|
|
3654
|
+
sym = self._variable_symbol(node.name)
|
|
1977
3655
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
1978
3656
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
1979
3657
|
if sym is not None and sym.pine_type != PineType.UNKNOWN:
|
|
@@ -1981,6 +3659,11 @@ class TypeInferer:
|
|
|
1981
3659
|
return "double"
|
|
1982
3660
|
if isinstance(node, FuncCall):
|
|
1983
3661
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
3662
|
+
if namespace == "color":
|
|
3663
|
+
return "int64_t" if func_name in {"new", "rgb", "from_gradient"} else "int"
|
|
3664
|
+
if (namespace == "request" and func_name == "security"
|
|
3665
|
+
and self._security_call_returns_string(node)):
|
|
3666
|
+
return "std::string"
|
|
1984
3667
|
# Nested trade-accessor calls bypass the flat namespace signature
|
|
1985
3668
|
# table. Their textual metadata accessors return std::string from
|
|
1986
3669
|
# the runtime, so hintless locals must not use the double fallback.
|
|
@@ -2005,7 +3688,9 @@ class TypeInferer:
|
|
|
2005
3688
|
return "int64_t"
|
|
2006
3689
|
if func_name == "na":
|
|
2007
3690
|
return "bool"
|
|
2008
|
-
if namespace
|
|
3691
|
+
if namespace is None and func_name == "input":
|
|
3692
|
+
return self._generic_input_cpp_type(node)
|
|
3693
|
+
if namespace == "input":
|
|
2009
3694
|
if func_name in ("string", "timeframe", "session", "symbol", "text_area"):
|
|
2010
3695
|
return "std::string"
|
|
2011
3696
|
if func_name == "bool":
|
|
@@ -2102,18 +3787,30 @@ class TypeInferer:
|
|
|
2102
3787
|
return "bool"
|
|
2103
3788
|
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
2104
3789
|
ename = node.object.name
|
|
3790
|
+
if ename == "color":
|
|
3791
|
+
return "int64_t"
|
|
2105
3792
|
if ename in self._enum_defs and node.member in self._enum_defs[ename]:
|
|
2106
3793
|
return "int"
|
|
2107
3794
|
# format.* constants emit std::string literals (consumed by
|
|
2108
3795
|
# pine_str_tostring); bare reads must declare std::string.
|
|
2109
3796
|
if ename == "format":
|
|
2110
3797
|
return "std::string"
|
|
3798
|
+
if ename == "alert" and node.member in ALERT_FREQ_VALUES:
|
|
3799
|
+
return "std::string"
|
|
2111
3800
|
if ename == "timeframe":
|
|
2112
3801
|
if node.member in ("period", "main_period"):
|
|
2113
3802
|
return "std::string"
|
|
2114
3803
|
if node.member == "multiplier":
|
|
2115
3804
|
return "int"
|
|
2116
3805
|
return "bool"
|
|
3806
|
+
# chart.is_* are Pine bools (the emitter lowers them to C++
|
|
3807
|
+
# ``true``/``false``): str.tostring(chart.is_standard) reads "true"
|
|
3808
|
+
# on TradingView (lab tv pf-krunerr-env-facts, 2026-09-26), not the
|
|
3809
|
+
# numeric "1".
|
|
3810
|
+
if ename == "chart" and node.member in (
|
|
3811
|
+
"is_standard", "is_heikinashi", "is_kagi", "is_linebreak",
|
|
3812
|
+
"is_pnf", "is_range", "is_renko"):
|
|
3813
|
+
return "bool"
|
|
2117
3814
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
2118
3815
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
2119
3816
|
if ename == "syminfo":
|
|
@@ -2193,6 +3890,16 @@ class TypeInferer:
|
|
|
2193
3890
|
local_types[stmt.name] = self._type_spec_to_cpp(spec)
|
|
2194
3891
|
continue
|
|
2195
3892
|
local_types[stmt.name] = self._infer_type(stmt.value)
|
|
3893
|
+
elif (isinstance(stmt, TupleAssign)
|
|
3894
|
+
and isinstance(stmt.value, (IfStmt, SwitchStmt))):
|
|
3895
|
+
for name, cpp_t in zip(
|
|
3896
|
+
stmt.names,
|
|
3897
|
+
self._infer_selection_tuple_types(
|
|
3898
|
+
stmt.value, len(stmt.names), local_types
|
|
3899
|
+
),
|
|
3900
|
+
):
|
|
3901
|
+
if name != "_":
|
|
3902
|
+
local_types[name] = cpp_t
|
|
2196
3903
|
|
|
2197
3904
|
last_stmt = func_node.body[-1]
|
|
2198
3905
|
expr = None
|
|
@@ -2200,12 +3907,109 @@ class TypeInferer:
|
|
|
2200
3907
|
expr = last_stmt.expr
|
|
2201
3908
|
elif isinstance(last_stmt, TupleLiteral):
|
|
2202
3909
|
expr = last_stmt
|
|
3910
|
+
request = last_stmt.expr if isinstance(last_stmt, ExprStmt) else last_stmt
|
|
3911
|
+
if (isinstance(request, FuncCall)
|
|
3912
|
+
and self._resolve_callee(request.callee) == ("security", "request")):
|
|
3913
|
+
# A helper returning a request's tuple returns it as the value
|
|
3914
|
+
# read stores it (``_req_sec_N`` or its per-element members).
|
|
3915
|
+
item = self._security_call_for_request(request)
|
|
3916
|
+
if item is not None and item.get("returns_tuple"):
|
|
3917
|
+
payload = item["expr_node"]
|
|
3918
|
+
if isinstance(payload, TupleLiteral):
|
|
3919
|
+
return ["double" if item.get("foreign") and cpp_t == "int" else cpp_t
|
|
3920
|
+
for cpp_t in map(self._infer_cpp_type_for_security_elem,
|
|
3921
|
+
payload.elements)]
|
|
3922
|
+
return self._security_tuple_element_cpp_types(
|
|
3923
|
+
count, item.get("tuple_element_types", ()))
|
|
2203
3924
|
if expr is not None:
|
|
3925
|
+
owner_info = next(
|
|
3926
|
+
(info for info in self._func_info_map.values()
|
|
3927
|
+
if getattr(info, "node", None) is func_node), None)
|
|
2204
3928
|
result: list[str] = []
|
|
2205
3929
|
for e in expr.elements:
|
|
2206
3930
|
if isinstance(e, Identifier) and e.name in local_types:
|
|
2207
|
-
|
|
3931
|
+
cpp_t = local_types[e.name]
|
|
2208
3932
|
else:
|
|
2209
|
-
|
|
3933
|
+
cpp_t = self._infer_type(e)
|
|
3934
|
+
result.append(self._widen_int_slot(cpp_t, e, owner_info))
|
|
2210
3935
|
return result
|
|
3936
|
+
if isinstance(last_stmt, (IfStmt, SwitchStmt)):
|
|
3937
|
+
return self._infer_selection_tuple_types(
|
|
3938
|
+
last_stmt, count, local_types
|
|
3939
|
+
)
|
|
3940
|
+
if (isinstance(last_stmt, TupleAssign)
|
|
3941
|
+
and isinstance(last_stmt.value, FuncCall)
|
|
3942
|
+
and isinstance(last_stmt.value.callee, Identifier)):
|
|
3943
|
+
# ``[p, q] = pair(v)`` last: the names are the callee's elements.
|
|
3944
|
+
callee = self._func_info_map.get(last_stmt.value.callee.name)
|
|
3945
|
+
callee_node = getattr(callee, "node", None)
|
|
3946
|
+
if isinstance(callee_node, FuncDef) and callee_node is not func_node:
|
|
3947
|
+
return self._infer_tuple_types(callee_node, count)
|
|
2211
3948
|
return ["double"] * count
|
|
3949
|
+
|
|
3950
|
+
def _infer_selection_tuple_types(
|
|
3951
|
+
self, node, count: int, local_types: dict[str, str] | None = None,
|
|
3952
|
+
) -> list[str]:
|
|
3953
|
+
"""C++ element types of a tuple-valued if/switch.
|
|
3954
|
+
|
|
3955
|
+
Each arm ends in a ``[a, b]`` literal, a tuple-returning user
|
|
3956
|
+
function call or a nested selection (the analyzer's
|
|
3957
|
+
``_selection_tuple_shape``). Arms agreeing on a position keep that
|
|
3958
|
+
type; numeric arms that differ widen to ``double``; any other
|
|
3959
|
+
disagreement keeps the first arm's type.
|
|
3960
|
+
"""
|
|
3961
|
+
local_types = local_types or {}
|
|
3962
|
+
if isinstance(node, IfStmt):
|
|
3963
|
+
arms = [node.body] + ([node.else_body] if node.else_body else [])
|
|
3964
|
+
elif isinstance(node, SwitchStmt):
|
|
3965
|
+
arms = [body for _case, body in node.cases]
|
|
3966
|
+
if node.default_body:
|
|
3967
|
+
arms.append(node.default_body)
|
|
3968
|
+
else:
|
|
3969
|
+
return ["double"] * count
|
|
3970
|
+
per_arm: list[list[str]] = []
|
|
3971
|
+
for body in arms:
|
|
3972
|
+
if not body:
|
|
3973
|
+
continue
|
|
3974
|
+
terminal = body[-1]
|
|
3975
|
+
expr = terminal.expr if isinstance(terminal, ExprStmt) else terminal
|
|
3976
|
+
if isinstance(expr, TupleLiteral) and len(expr.elements) == count:
|
|
3977
|
+
per_arm.append([
|
|
3978
|
+
local_types[e.name]
|
|
3979
|
+
if isinstance(e, Identifier) and e.name in local_types
|
|
3980
|
+
else self._infer_type(e)
|
|
3981
|
+
for e in expr.elements
|
|
3982
|
+
])
|
|
3983
|
+
elif isinstance(expr, FuncCall) and isinstance(expr.callee, Identifier):
|
|
3984
|
+
fi = self._func_info_map.get(expr.callee.name)
|
|
3985
|
+
if (fi is not None and fi.node is not None
|
|
3986
|
+
and getattr(fi, "returns_tuple", False)
|
|
3987
|
+
and fi.tuple_element_count == count):
|
|
3988
|
+
per_arm.append(self._infer_tuple_types(fi.node, count))
|
|
3989
|
+
elif isinstance(expr, (IfStmt, SwitchStmt)):
|
|
3990
|
+
per_arm.append(
|
|
3991
|
+
self._infer_selection_tuple_types(expr, count, local_types)
|
|
3992
|
+
)
|
|
3993
|
+
if not per_arm:
|
|
3994
|
+
return ["double"] * count
|
|
3995
|
+
result: list[str] = []
|
|
3996
|
+
for position in range(count):
|
|
3997
|
+
found = [types[position] for types in per_arm]
|
|
3998
|
+
if len(set(found)) == 1:
|
|
3999
|
+
result.append(found[0])
|
|
4000
|
+
elif set(found) <= {"double", "int", "int64_t"}:
|
|
4001
|
+
result.append("double")
|
|
4002
|
+
else:
|
|
4003
|
+
result.append(found[0])
|
|
4004
|
+
return result
|
|
4005
|
+
|
|
4006
|
+
def _tuple_default_expr(self, types: list[str]) -> str:
|
|
4007
|
+
"""``std::tuple<...>`` of na elements: a tuple selection no arm of
|
|
4008
|
+
which runs yields na in every position."""
|
|
4009
|
+
values = [
|
|
4010
|
+
f"na<{cpp_t}>()"
|
|
4011
|
+
if cpp_t in ("double", "int", "int64_t", "bool", "std::string")
|
|
4012
|
+
else self._default_for_type(cpp_t)
|
|
4013
|
+
for cpp_t in types
|
|
4014
|
+
]
|
|
4015
|
+
return f"std::tuple<{', '.join(types)}>({', '.join(values)})"
|