@pineforge/codegen-pyodide 0.10.3 → 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 +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- 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 +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -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 +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- 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/symbols.py +4 -2
- 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.3.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
|
|
|
@@ -135,15 +164,17 @@ class TypeInferer:
|
|
|
135
164
|
if spec is None:
|
|
136
165
|
return "double"
|
|
137
166
|
if spec.kind == "primitive":
|
|
138
|
-
return {"float": "double", "int": "int", "
|
|
139
|
-
"
|
|
167
|
+
return {"float": "double", "int": "int", "int64": "int64_t",
|
|
168
|
+
"bool": "bool", "string": "std::string",
|
|
169
|
+
"color": "int64_t"}.get(spec.name or "float", "double")
|
|
140
170
|
if spec.kind == "udt" and spec.name:
|
|
141
171
|
# Drawing handle structs (P1): map BEFORE the _udt_defs check so
|
|
142
172
|
# array<line> -> std::vector<Line> and scalar line -> Line instead
|
|
143
173
|
# of the old collapse to double / unknown-type-name.
|
|
144
174
|
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
145
175
|
return DRAWING_TYPE_TO_CPP[spec.name]
|
|
146
|
-
return
|
|
176
|
+
return (self._safe_name(spec.name)
|
|
177
|
+
if spec.name in self._udt_defs else "double")
|
|
147
178
|
if spec.kind == "array":
|
|
148
179
|
return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
|
|
149
180
|
if spec.kind == "map":
|
|
@@ -171,7 +202,7 @@ class TypeInferer:
|
|
|
171
202
|
"""Generated arena specialization for one user-defined object type."""
|
|
172
203
|
return (
|
|
173
204
|
f"{getattr(self, '_udt_arena_template_cpp_name', '_PFUdtArena')}<"
|
|
174
|
-
f"{type_name}, "
|
|
205
|
+
f"{self._safe_name(type_name)}, "
|
|
175
206
|
f"{self._udt_record_cpp_type(type_name)}>"
|
|
176
207
|
)
|
|
177
208
|
|
|
@@ -181,6 +212,227 @@ class TypeInferer:
|
|
|
181
212
|
self, "_checkpoint_traits_cpp_name", "_PFCheckpointTraits"
|
|
182
213
|
)
|
|
183
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
|
+
|
|
184
436
|
def _udt_direct_array_fields(self, type_name: str) -> tuple[str, ...]:
|
|
185
437
|
"""Direct array-valued fields whose UDT copy semantics are unsupported.
|
|
186
438
|
|
|
@@ -260,7 +512,7 @@ class TypeInferer:
|
|
|
260
512
|
# (Line{} = na handle), NOT the lowercase Pine name (line{}).
|
|
261
513
|
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
262
514
|
return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
|
|
263
|
-
return f"{spec.name}{{}}"
|
|
515
|
+
return f"{self._safe_name(spec.name)}{{}}"
|
|
264
516
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
265
517
|
if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
|
|
266
518
|
return f"{cpp_type}()"
|
|
@@ -269,6 +521,717 @@ class TypeInferer:
|
|
|
269
521
|
return self._default_for_type(cpp_type)
|
|
270
522
|
|
|
271
523
|
def _collection_spec_for_name(self, name: str) -> TypeSpec | None:
|
|
524
|
+
"""``_collection_spec_for_name_raw`` with the wide-int element applied
|
|
525
|
+
(``_widen_array_spec_for_name``): an ``array<int>`` that stores epoch
|
|
526
|
+
milliseconds resolves as ``std::vector<int64_t>`` everywhere it is
|
|
527
|
+
read, declared or passed."""
|
|
528
|
+
return self._widen_array_spec_for_name(
|
|
529
|
+
name, self._collection_spec_for_name_raw(name)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Element-returning array reads whose result carries the receiver's wide
|
|
533
|
+
# provenance (``_expr_returns_wide_int``).
|
|
534
|
+
_WIDE_ARRAY_ELEMENT_READS = frozenset({
|
|
535
|
+
"get", "first", "last", "pop", "shift", "remove", "max", "min",
|
|
536
|
+
})
|
|
537
|
+
# Writers whose VALUE argument decides whether an int array is wide:
|
|
538
|
+
# name -> (functional-form value index, method-form value index).
|
|
539
|
+
_ARRAY_ELEMENT_WRITERS = {
|
|
540
|
+
"push": (1, 0), "unshift": (1, 0), "set": (2, 1), "insert": (2, 1),
|
|
541
|
+
"fill": (1, 0),
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def _int_fits_int32(value: int) -> bool:
|
|
546
|
+
return -(1 << 31) <= value < (1 << 31)
|
|
547
|
+
|
|
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
|
|
556
|
+
``+ - *`` / unary minus, else ``None``.
|
|
557
|
+
|
|
558
|
+
Pine ``int`` is 64-bit: ``90 * 24 * 60 * 60 * 1000`` (three months in
|
|
559
|
+
milliseconds) is 7 776 000 000 on TradingView. Emitted as C++ ``int``
|
|
560
|
+
literal arithmetic the same product overflows (wraps to -813 934 592),
|
|
561
|
+
so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8
|
|
562
|
+
family U: latibonit15 execution-signals-confluence, six lanes; lab tv
|
|
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
|
|
568
|
+
64-bit literal only when it does not fit ``int32`` so every in-range
|
|
569
|
+
expression is byte-identical to before. ``global_scope`` reads names
|
|
570
|
+
as the script's top level binds them, whatever is being emitted.
|
|
571
|
+
"""
|
|
572
|
+
if isinstance(node, NumberLiteral):
|
|
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))
|
|
579
|
+
if isinstance(node, UnaryOp) and node.op == "-":
|
|
580
|
+
inner = self._pure_int_literal_value(node.operand, names, global_scope)
|
|
581
|
+
return -inner if inner is not None else None
|
|
582
|
+
if isinstance(node, BinOp) and node.op in ("+", "-", "*"):
|
|
583
|
+
left = self._pure_int_literal_value(node.left, names, global_scope)
|
|
584
|
+
if left is None:
|
|
585
|
+
return None
|
|
586
|
+
right = self._pure_int_literal_value(node.right, names, global_scope)
|
|
587
|
+
if right is None:
|
|
588
|
+
return None
|
|
589
|
+
if node.op == "+":
|
|
590
|
+
return left + right
|
|
591
|
+
if node.op == "-":
|
|
592
|
+
return left - right
|
|
593
|
+
return left * right
|
|
594
|
+
return None
|
|
595
|
+
|
|
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)
|
|
640
|
+
return value is not None and not self._int_fits_int32(value)
|
|
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
|
+
|
|
1118
|
+
def _array_receiver_and_value(self, call):
|
|
1119
|
+
"""``(receiver_name, value_node)`` of an element-writing array call in
|
|
1120
|
+
either form (``array.push(a, v)`` / ``a.push(v)``), else ``None``."""
|
|
1121
|
+
if not isinstance(call, FuncCall):
|
|
1122
|
+
return None
|
|
1123
|
+
func_name, namespace = self._resolve_callee(call.callee)
|
|
1124
|
+
if func_name not in self._ARRAY_ELEMENT_WRITERS:
|
|
1125
|
+
return None
|
|
1126
|
+
functional_idx, method_idx = self._ARRAY_ELEMENT_WRITERS[func_name]
|
|
1127
|
+
if namespace == "array":
|
|
1128
|
+
receiver = call.args[0] if call.args else call.kwargs.get("id")
|
|
1129
|
+
value = (call.args[functional_idx]
|
|
1130
|
+
if len(call.args) > functional_idx
|
|
1131
|
+
else call.kwargs.get("value"))
|
|
1132
|
+
elif (isinstance(call.callee, MemberAccess)
|
|
1133
|
+
and isinstance(call.callee.object, Identifier)):
|
|
1134
|
+
receiver = call.callee.object
|
|
1135
|
+
value = (call.args[method_idx]
|
|
1136
|
+
if len(call.args) > method_idx
|
|
1137
|
+
else call.kwargs.get("value"))
|
|
1138
|
+
else:
|
|
1139
|
+
return None
|
|
1140
|
+
if not isinstance(receiver, Identifier) or value is None:
|
|
1141
|
+
return None
|
|
1142
|
+
return receiver.name, value
|
|
1143
|
+
|
|
1144
|
+
def _wide_int_array_names(self) -> set[str]:
|
|
1145
|
+
"""Names of int arrays that hold a wide integer: one that receives an
|
|
1146
|
+
epoch-millisecond value (``time``, ``time_close``, ``timestamp(...)``,
|
|
1147
|
+
a wide callable result, ...) through ``array.push`` / ``unshift`` /
|
|
1148
|
+
``set`` / ``insert`` / ``fill``, or is built by ``array.new_int`` /
|
|
1149
|
+
``array.new<int>`` / ``array.from`` from one. Their element type is
|
|
1150
|
+
``int64_t`` (TypeSpec primitive ``int64``): Pine's ``int`` holds the
|
|
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``)."""
|
|
1160
|
+
cached = getattr(self, "_wide_int_array_cache", None)
|
|
1161
|
+
if cached is not None:
|
|
1162
|
+
return cached
|
|
1163
|
+
# The scan below asks ``_expr_returns_wide_int``, whose receiver typing
|
|
1164
|
+
# resolves collection specs through ``_collection_spec_for_name`` and so
|
|
1165
|
+
# back here: publish the (growing) set first so the recursion reads the
|
|
1166
|
+
# partial answer instead of re-entering, then iterate to the fixpoint so
|
|
1167
|
+
# an array filled from another wide array's elements widens too.
|
|
1168
|
+
names: set[str] = set()
|
|
1169
|
+
self._wide_int_array_cache = names
|
|
1170
|
+
|
|
1171
|
+
def scan(nodes, owner_info):
|
|
1172
|
+
for child in nodes:
|
|
1173
|
+
if isinstance(child, FuncCall):
|
|
1174
|
+
pair = self._array_receiver_and_value(child)
|
|
1175
|
+
if pair is not None and self._expr_returns_wide_int(
|
|
1176
|
+
pair[1], owner_info, set(), None
|
|
1177
|
+
):
|
|
1178
|
+
names.add(pair[0])
|
|
1179
|
+
continue
|
|
1180
|
+
target = None
|
|
1181
|
+
value = None
|
|
1182
|
+
if isinstance(child, VarDecl):
|
|
1183
|
+
target, value = child.name, child.value
|
|
1184
|
+
elif (isinstance(child, Assignment)
|
|
1185
|
+
and isinstance(child.target, Identifier)):
|
|
1186
|
+
target, value = child.target.name, child.value
|
|
1187
|
+
if target is None or not isinstance(value, FuncCall):
|
|
1188
|
+
continue
|
|
1189
|
+
fn, ns = self._resolve_callee(value.callee)
|
|
1190
|
+
if ns != "array":
|
|
1191
|
+
continue
|
|
1192
|
+
candidates = []
|
|
1193
|
+
if fn in ("new_int", "new"):
|
|
1194
|
+
if fn == "new":
|
|
1195
|
+
targs = (self._template_args_from_call(value)
|
|
1196
|
+
if hasattr(value, "annotations") else [])
|
|
1197
|
+
if not targs or targs[0] != "int":
|
|
1198
|
+
continue
|
|
1199
|
+
if len(value.args) > 1:
|
|
1200
|
+
candidates.append(value.args[1])
|
|
1201
|
+
if "initial_value" in value.kwargs:
|
|
1202
|
+
candidates.append(value.kwargs["initial_value"])
|
|
1203
|
+
elif fn == "from":
|
|
1204
|
+
candidates = list(value.args)
|
|
1205
|
+
if any(self._expr_returns_wide_int(c, owner_info, set(), None)
|
|
1206
|
+
for c in candidates):
|
|
1207
|
+
names.add(target)
|
|
1208
|
+
|
|
1209
|
+
ast = getattr(self.ctx, "ast", None)
|
|
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
|
|
1221
|
+
return names
|
|
1222
|
+
|
|
1223
|
+
def _widen_array_spec_for_name(self, name, spec):
|
|
1224
|
+
"""``array<int>`` -> ``array<int64>`` when ``name`` is a wide int
|
|
1225
|
+
array; every other spec (and ``None``) passes through unchanged."""
|
|
1226
|
+
if (spec is None or spec.kind != "array" or spec.element is None
|
|
1227
|
+
or spec.element.kind != "primitive"
|
|
1228
|
+
or spec.element.name != "int"):
|
|
1229
|
+
return spec
|
|
1230
|
+
if name in self._wide_int_array_names():
|
|
1231
|
+
return TypeSpec.array(TypeSpec.primitive("int64"))
|
|
1232
|
+
return spec
|
|
1233
|
+
|
|
1234
|
+
def _collection_spec_for_name_raw(self, name: str) -> TypeSpec | None:
|
|
272
1235
|
"""Resolve collection metadata with lexical precedence.
|
|
273
1236
|
|
|
274
1237
|
Source-ordered callable locals shadow loop bindings and parameters once
|
|
@@ -417,9 +1380,16 @@ class TypeInferer:
|
|
|
417
1380
|
return TypeSpec.primitive("string")
|
|
418
1381
|
if node.op == "/" or left.name == "float" or right.name == "float":
|
|
419
1382
|
return TypeSpec.primitive("float")
|
|
420
|
-
if left.name
|
|
1383
|
+
if left.name in ("int", "int64") and right.name in ("int", "int64"):
|
|
1384
|
+
if "int64" in (left.name, right.name):
|
|
1385
|
+
return TypeSpec.primitive("int64")
|
|
421
1386
|
return TypeSpec.primitive("int")
|
|
422
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")
|
|
423
1393
|
spec = self._type_spec_from_expr(node)
|
|
424
1394
|
if spec is not None:
|
|
425
1395
|
return spec
|
|
@@ -439,6 +1409,40 @@ class TypeInferer:
|
|
|
439
1409
|
return TypeSpec.primitive(primitive_name)
|
|
440
1410
|
return None
|
|
441
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
|
+
|
|
442
1446
|
@staticmethod
|
|
443
1447
|
def _selection_terminal_expr(body: list[ASTNode] | None) -> ASTNode | None:
|
|
444
1448
|
"""Return one if/switch branch's value expression, if present."""
|
|
@@ -478,6 +1482,13 @@ class TypeInferer:
|
|
|
478
1482
|
|
|
479
1483
|
Returns ``None`` when the node's type cannot be narrowed beyond
|
|
480
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)
|
|
481
1492
|
if isinstance(node, NumberLiteral):
|
|
482
1493
|
return TypeSpec.primitive("float" if isinstance(node.value, float) else "int")
|
|
483
1494
|
if isinstance(node, BoolLiteral):
|
|
@@ -757,6 +1768,23 @@ class TypeInferer:
|
|
|
757
1768
|
receiver_spec = self._type_spec_from_expr(receiver_node)
|
|
758
1769
|
if receiver_spec is not None and receiver_spec.kind == "matrix":
|
|
759
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"))
|
|
760
1788
|
if namespace == "map" and func_name == "new":
|
|
761
1789
|
key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
762
1790
|
val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
@@ -795,6 +1823,8 @@ class TypeInferer:
|
|
|
795
1823
|
return recv_spec.element
|
|
796
1824
|
if member_name in ("copy", "slice"):
|
|
797
1825
|
return recv_spec
|
|
1826
|
+
if member_name in ARRAY_RESULT_METHODS:
|
|
1827
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
798
1828
|
if recv_spec is not None and recv_spec.kind == "map":
|
|
799
1829
|
if member_name in ("put", "get", "remove"):
|
|
800
1830
|
return recv_spec.value
|
|
@@ -952,7 +1982,8 @@ class TypeInferer:
|
|
|
952
1982
|
)
|
|
953
1983
|
|
|
954
1984
|
def _array_method_expr(
|
|
955
|
-
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,
|
|
956
1987
|
) -> str:
|
|
957
1988
|
"""Lower ``arr.method(...)`` to its C++ form, validating numeric requirements."""
|
|
958
1989
|
spec = spec or TypeSpec.array(TypeSpec.primitive("float"))
|
|
@@ -961,14 +1992,33 @@ class TypeInferer:
|
|
|
961
1992
|
if method == "copy":
|
|
962
1993
|
lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
|
|
963
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
|
|
964
2010
|
# Bounds-checked in the shared helper; the element type stays
|
|
965
2011
|
# caller-supplied so the typed lane keeps its own vector type.
|
|
966
2012
|
lower_receiver = lambda recv: checked_array_slice(
|
|
967
2013
|
recv, args, result_type=arr_cpp_type
|
|
968
2014
|
)
|
|
969
2015
|
elif method == "join" and elem_cpp == "std::string":
|
|
970
|
-
|
|
971
|
-
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
|
+
)
|
|
972
2022
|
else:
|
|
973
2023
|
numeric_only = {
|
|
974
2024
|
"sum", "avg", "min", "max", "range", "stdev", "variance", "median",
|
|
@@ -976,7 +2026,7 @@ class TypeInferer:
|
|
|
976
2026
|
"percentrank", "abs", "standardize", "covariance", "binary_search",
|
|
977
2027
|
"binary_search_leftmost", "binary_search_rightmost", "sort_indices",
|
|
978
2028
|
}
|
|
979
|
-
if method in numeric_only and elem_cpp not in ("double", "int"):
|
|
2029
|
+
if method in numeric_only and elem_cpp not in ("double", "int", "int64_t"):
|
|
980
2030
|
self._codegen_error(
|
|
981
2031
|
None,
|
|
982
2032
|
f"array.{method} requires a numeric array",
|
|
@@ -1019,8 +2069,15 @@ class TypeInferer:
|
|
|
1019
2069
|
arg_bindings.append((token, args[arg_index]))
|
|
1020
2070
|
self._array_arg_counter = counter
|
|
1021
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
|
+
|
|
1022
2076
|
def lower_receiver(recv: str) -> str:
|
|
1023
|
-
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
|
+
)
|
|
1024
2081
|
for token, original in reversed(arg_bindings):
|
|
1025
2082
|
lowered = (
|
|
1026
2083
|
f"[&](){{ auto {token}=({original}); "
|
|
@@ -1086,7 +2143,7 @@ class TypeInferer:
|
|
|
1086
2143
|
owner,
|
|
1087
2144
|
set(),
|
|
1088
2145
|
getattr(self, "_active_call_site_idx", None),
|
|
1089
|
-
) or self._is_int64_builtin_init(node.name):
|
|
2146
|
+
) or self._is_int64_builtin_init(node.name, node):
|
|
1090
2147
|
return "int64_t"
|
|
1091
2148
|
return cpp_type
|
|
1092
2149
|
|
|
@@ -1147,13 +2204,76 @@ class TypeInferer:
|
|
|
1147
2204
|
# int64_t buffer: epoch-ms overflow int32 and the na sentinel would be
|
|
1148
2205
|
# misdetected. ``_is_int64_builtin_init`` only matches user vars whose
|
|
1149
2206
|
# init RHS is such a builtin, so also match the builtin name directly.
|
|
1150
|
-
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"
|
|
1151
2223
|
return "int64_t"
|
|
1152
|
-
sym = self.ctx.symbols.resolve(name)
|
|
1153
2224
|
if sym is not None:
|
|
1154
2225
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
1155
2226
|
return "double"
|
|
1156
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
|
+
|
|
1157
2277
|
def _series_param_element_cpp_type(
|
|
1158
2278
|
self,
|
|
1159
2279
|
func_info,
|
|
@@ -1187,12 +2307,12 @@ class TypeInferer:
|
|
|
1187
2307
|
# truly untyped slot may consult the per-callsite specialization map.
|
|
1188
2308
|
spec = declared_spec
|
|
1189
2309
|
if spec is not None and spec.kind == "primitive":
|
|
1190
|
-
if spec.name
|
|
2310
|
+
if spec.name in ("int", "int64"):
|
|
1191
2311
|
return "int64_t"
|
|
1192
2312
|
if spec.name == "bool":
|
|
1193
2313
|
return "bool"
|
|
1194
2314
|
if spec.name == "color":
|
|
1195
|
-
return "
|
|
2315
|
+
return "int64_t"
|
|
1196
2316
|
if spec.name == "float":
|
|
1197
2317
|
return "double"
|
|
1198
2318
|
|
|
@@ -1210,19 +2330,19 @@ class TypeInferer:
|
|
|
1210
2330
|
if pine_type == PineType.BOOL:
|
|
1211
2331
|
return "bool"
|
|
1212
2332
|
if pine_type == PineType.COLOR:
|
|
1213
|
-
return "
|
|
2333
|
+
return "int64_t"
|
|
1214
2334
|
if pine_type == PineType.FLOAT:
|
|
1215
2335
|
return "double"
|
|
1216
2336
|
|
|
1217
2337
|
specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
1218
2338
|
spec = specs[index] if index < len(specs) else None
|
|
1219
2339
|
if spec is not None and spec.kind == "primitive":
|
|
1220
|
-
if spec.name
|
|
2340
|
+
if spec.name in ("int", "int64"):
|
|
1221
2341
|
return "int64_t"
|
|
1222
2342
|
if spec.name == "bool":
|
|
1223
2343
|
return "bool"
|
|
1224
2344
|
if spec.name == "color":
|
|
1225
|
-
return "
|
|
2345
|
+
return "int64_t"
|
|
1226
2346
|
if spec.name == "float":
|
|
1227
2347
|
return "double"
|
|
1228
2348
|
param_types = list(getattr(func_info, "param_types", ()) or ())
|
|
@@ -1232,7 +2352,7 @@ class TypeInferer:
|
|
|
1232
2352
|
if pine_type == PineType.BOOL:
|
|
1233
2353
|
return "bool"
|
|
1234
2354
|
if pine_type == PineType.COLOR:
|
|
1235
|
-
return "
|
|
2355
|
+
return "int64_t"
|
|
1236
2356
|
return "double"
|
|
1237
2357
|
|
|
1238
2358
|
def _callsite_callable_return_pine_type(
|
|
@@ -1292,10 +2412,17 @@ class TypeInferer:
|
|
|
1292
2412
|
terminal = node.body[-1]
|
|
1293
2413
|
if isinstance(terminal, ExprStmt):
|
|
1294
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
|
|
1295
2418
|
return self._expr_returns_wide_int(
|
|
1296
2419
|
terminal, func_info, seen, call_site_idx
|
|
1297
2420
|
)
|
|
1298
2421
|
|
|
2422
|
+
_NON_INTEGER_BINOPS = frozenset({
|
|
2423
|
+
"/", "==", "!=", "<", ">", "<=", ">=", "and", "or",
|
|
2424
|
+
})
|
|
2425
|
+
|
|
1299
2426
|
def _expr_returns_wide_int(
|
|
1300
2427
|
self,
|
|
1301
2428
|
expr,
|
|
@@ -1317,7 +2444,12 @@ class TypeInferer:
|
|
|
1317
2444
|
node = getattr(owner_info, "node", None)
|
|
1318
2445
|
if node is not None:
|
|
1319
2446
|
if expr.name in node.params:
|
|
1320
|
-
|
|
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]
|
|
1321
2453
|
local_key = (
|
|
1322
2454
|
f"{getattr(owner_info, 'name', '')}:local:{expr.name}"
|
|
1323
2455
|
)
|
|
@@ -1349,11 +2481,45 @@ class TypeInferer:
|
|
|
1349
2481
|
call_site_idx,
|
|
1350
2482
|
):
|
|
1351
2483
|
return True
|
|
1352
|
-
|
|
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])
|
|
1353
2488
|
if self._expr_is_int64_builtin(expr):
|
|
1354
2489
|
return True
|
|
2490
|
+
if isinstance(expr, (BinOp, UnaryOp)) and self._literal_overflows_int32(expr):
|
|
2491
|
+
# ``90 * 24 * 60 * 60 * 1000``: an int-literal product beyond
|
|
2492
|
+
# int32 is a 64-bit value in Pine (``_pure_int_literal_value``).
|
|
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()
|
|
1355
2502
|
if isinstance(expr, FuncCall):
|
|
1356
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
|
+
)
|
|
2510
|
+
if func_name in self._WIDE_ARRAY_ELEMENT_READS:
|
|
2511
|
+
# An element read off a wide int array (``array.get(times, i)``
|
|
2512
|
+
# / ``times.get(i)``) carries the epoch: the destination must
|
|
2513
|
+
# not narrow it (``_wide_int_array_names``).
|
|
2514
|
+
receiver = None
|
|
2515
|
+
if namespace == "array":
|
|
2516
|
+
receiver = expr.args[0] if expr.args else expr.kwargs.get("id")
|
|
2517
|
+
elif (isinstance(expr.callee, MemberAccess)
|
|
2518
|
+
and isinstance(expr.callee.object, Identifier)):
|
|
2519
|
+
receiver = expr.callee.object
|
|
2520
|
+
if (isinstance(receiver, Identifier)
|
|
2521
|
+
and receiver.name in self._wide_int_array_names()):
|
|
2522
|
+
return True
|
|
1357
2523
|
if namespace is None and func_name in {
|
|
1358
2524
|
"int",
|
|
1359
2525
|
"float",
|
|
@@ -1418,6 +2584,13 @@ class TypeInferer:
|
|
|
1418
2584
|
)
|
|
1419
2585
|
)
|
|
1420
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
|
|
1421
2594
|
return (
|
|
1422
2595
|
self._expr_returns_wide_int(
|
|
1423
2596
|
expr.left, owner_info, seen, call_site_idx
|
|
@@ -1427,6 +2600,8 @@ class TypeInferer:
|
|
|
1427
2600
|
)
|
|
1428
2601
|
)
|
|
1429
2602
|
if isinstance(expr, UnaryOp):
|
|
2603
|
+
if expr.op == "not":
|
|
2604
|
+
return False
|
|
1430
2605
|
return self._expr_returns_wide_int(
|
|
1431
2606
|
expr.operand, owner_info, seen, call_site_idx
|
|
1432
2607
|
)
|
|
@@ -1508,44 +2683,164 @@ class TypeInferer:
|
|
|
1508
2683
|
return "int64_t"
|
|
1509
2684
|
return cpp_type
|
|
1510
2685
|
|
|
1511
|
-
def
|
|
1512
|
-
"""Names
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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)
|
|
1517
2709
|
if cached is not None:
|
|
1518
2710
|
return cached
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
#
|
|
1522
|
-
#
|
|
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.
|
|
1523
2742
|
for info in getattr(self.ctx, "func_infos", ()):
|
|
1524
2743
|
node = getattr(info, "node", None)
|
|
1525
2744
|
if node is None:
|
|
1526
2745
|
continue
|
|
1527
2746
|
for child in self._walk_ast_list(node.body):
|
|
1528
|
-
|
|
1529
|
-
isinstance(child, Assignment)
|
|
1530
|
-
and isinstance(child.target, Identifier)
|
|
1531
|
-
and self._expr_returns_wide_int(
|
|
1532
|
-
child.value, info, set(), None
|
|
1533
|
-
)
|
|
1534
|
-
):
|
|
1535
|
-
targets.add(child.target.name)
|
|
2747
|
+
collect(child, info)
|
|
1536
2748
|
ast = getattr(self.ctx, "ast", None)
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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:
|
|
1549
2844
|
"""True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
|
|
1550
2845
|
RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
|
|
1551
2846
|
``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
|
|
@@ -1553,7 +2848,9 @@ class TypeInferer:
|
|
|
1553
2848
|
(and the full epoch-ms value, which overflows int32) in 64 bits, so
|
|
1554
2849
|
storing into ``int`` silently corrupts both the value and na detection.
|
|
1555
2850
|
A reassignment like ``var int entryTime = na`` then ``entryTime := time``
|
|
1556
|
-
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``).
|
|
1557
2854
|
"""
|
|
1558
2855
|
expr = (
|
|
1559
2856
|
self.ctx.global_expr_map.get(name)
|
|
@@ -1561,7 +2858,565 @@ class TypeInferer:
|
|
|
1561
2858
|
)
|
|
1562
2859
|
if self._expr_is_int64_builtin(expr):
|
|
1563
2860
|
return True
|
|
1564
|
-
|
|
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)
|
|
1565
3420
|
|
|
1566
3421
|
def _na_reassign_cpp_type(self, name: str) -> str | None:
|
|
1567
3422
|
"""Declared scalar C++ type of a ``:=`` reassignment target ``name``, so a
|
|
@@ -1605,7 +3460,11 @@ class TypeInferer:
|
|
|
1605
3460
|
if cpp_type is None:
|
|
1606
3461
|
for gname, gptype in self.ctx.global_var_decls:
|
|
1607
3462
|
if gname == name:
|
|
1608
|
-
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
|
+
)
|
|
1609
3468
|
break
|
|
1610
3469
|
if cpp_type is None:
|
|
1611
3470
|
return None
|
|
@@ -1746,6 +3605,12 @@ class TypeInferer:
|
|
|
1746
3605
|
and ternaries / if / switch expressions. Returns the string
|
|
1747
3606
|
``"double"`` as the safe fallback when no narrower type can be
|
|
1748
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"
|
|
1749
3614
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
1750
3615
|
# A callable-result history read keeps the callable's scalar
|
|
1751
3616
|
# family. Collection subscripts follow separate element-type paths.
|
|
@@ -1754,6 +3619,8 @@ class TypeInferer:
|
|
|
1754
3619
|
return "double" if isinstance(node.value, float) else "int"
|
|
1755
3620
|
if isinstance(node, BoolLiteral):
|
|
1756
3621
|
return "bool"
|
|
3622
|
+
if isinstance(node, ColorLiteral):
|
|
3623
|
+
return "int64_t"
|
|
1757
3624
|
if isinstance(node, StringLiteral):
|
|
1758
3625
|
return "std::string"
|
|
1759
3626
|
if isinstance(node, NaLiteral):
|
|
@@ -1784,7 +3651,7 @@ class TypeInferer:
|
|
|
1784
3651
|
return self._current_func_local_types[node.name]
|
|
1785
3652
|
if node.name in getattr(self, "_current_loop_vars", set()):
|
|
1786
3653
|
return "double"
|
|
1787
|
-
sym = self.
|
|
3654
|
+
sym = self._variable_symbol(node.name)
|
|
1788
3655
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
1789
3656
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
1790
3657
|
if sym is not None and sym.pine_type != PineType.UNKNOWN:
|
|
@@ -1792,6 +3659,11 @@ class TypeInferer:
|
|
|
1792
3659
|
return "double"
|
|
1793
3660
|
if isinstance(node, FuncCall):
|
|
1794
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"
|
|
1795
3667
|
# Nested trade-accessor calls bypass the flat namespace signature
|
|
1796
3668
|
# table. Their textual metadata accessors return std::string from
|
|
1797
3669
|
# the runtime, so hintless locals must not use the double fallback.
|
|
@@ -1816,7 +3688,9 @@ class TypeInferer:
|
|
|
1816
3688
|
return "int64_t"
|
|
1817
3689
|
if func_name == "na":
|
|
1818
3690
|
return "bool"
|
|
1819
|
-
if namespace
|
|
3691
|
+
if namespace is None and func_name == "input":
|
|
3692
|
+
return self._generic_input_cpp_type(node)
|
|
3693
|
+
if namespace == "input":
|
|
1820
3694
|
if func_name in ("string", "timeframe", "session", "symbol", "text_area"):
|
|
1821
3695
|
return "std::string"
|
|
1822
3696
|
if func_name == "bool":
|
|
@@ -1913,18 +3787,30 @@ class TypeInferer:
|
|
|
1913
3787
|
return "bool"
|
|
1914
3788
|
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
1915
3789
|
ename = node.object.name
|
|
3790
|
+
if ename == "color":
|
|
3791
|
+
return "int64_t"
|
|
1916
3792
|
if ename in self._enum_defs and node.member in self._enum_defs[ename]:
|
|
1917
3793
|
return "int"
|
|
1918
3794
|
# format.* constants emit std::string literals (consumed by
|
|
1919
3795
|
# pine_str_tostring); bare reads must declare std::string.
|
|
1920
3796
|
if ename == "format":
|
|
1921
3797
|
return "std::string"
|
|
3798
|
+
if ename == "alert" and node.member in ALERT_FREQ_VALUES:
|
|
3799
|
+
return "std::string"
|
|
1922
3800
|
if ename == "timeframe":
|
|
1923
3801
|
if node.member in ("period", "main_period"):
|
|
1924
3802
|
return "std::string"
|
|
1925
3803
|
if node.member == "multiplier":
|
|
1926
3804
|
return "int"
|
|
1927
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"
|
|
1928
3814
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
1929
3815
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
1930
3816
|
if ename == "syminfo":
|
|
@@ -2004,6 +3890,16 @@ class TypeInferer:
|
|
|
2004
3890
|
local_types[stmt.name] = self._type_spec_to_cpp(spec)
|
|
2005
3891
|
continue
|
|
2006
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
|
|
2007
3903
|
|
|
2008
3904
|
last_stmt = func_node.body[-1]
|
|
2009
3905
|
expr = None
|
|
@@ -2011,12 +3907,109 @@ class TypeInferer:
|
|
|
2011
3907
|
expr = last_stmt.expr
|
|
2012
3908
|
elif isinstance(last_stmt, TupleLiteral):
|
|
2013
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", ()))
|
|
2014
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)
|
|
2015
3928
|
result: list[str] = []
|
|
2016
3929
|
for e in expr.elements:
|
|
2017
3930
|
if isinstance(e, Identifier) and e.name in local_types:
|
|
2018
|
-
|
|
3931
|
+
cpp_t = local_types[e.name]
|
|
2019
3932
|
else:
|
|
2020
|
-
|
|
3933
|
+
cpp_t = self._infer_type(e)
|
|
3934
|
+
result.append(self._widen_int_slot(cpp_t, e, owner_info))
|
|
2021
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)
|
|
2022
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)})"
|