@pineforge/codegen-pyodide 1.0.0 → 1.0.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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +189 -1
- package/pineforge_codegen/analyzer/contracts.py +3 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +32 -5
- package/pineforge_codegen/codegen/helpers.py +5 -1
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -42,6 +42,7 @@ from ..ast_nodes import (
|
|
|
42
42
|
MemberAccess, MethodDef, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
|
|
43
43
|
Subscript, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
|
|
44
44
|
)
|
|
45
|
+
from ..collection_history import history_annotation
|
|
45
46
|
from ..errors import Phase
|
|
46
47
|
from ..external_requests import UNPINNED_ANNOTATION
|
|
47
48
|
from ..limits import iter_ast_nodes
|
|
@@ -1545,14 +1546,26 @@ class TypeInferer:
|
|
|
1545
1546
|
return TypeSpec.primitive(primitive_name)
|
|
1546
1547
|
return None
|
|
1547
1548
|
if isinstance(node, Subscript):
|
|
1548
|
-
# History access preserves the
|
|
1549
|
-
# is
|
|
1550
|
-
#
|
|
1549
|
+
# History access preserves the drawing-handle and user-object
|
|
1550
|
+
# type: ``c[1]`` is the reference ``c`` held one bar back
|
|
1551
|
+
# (fixtures/udt_history_tv). This is intentionally not
|
|
1552
|
+
# generalized to arrays/maps: their subscripting rules are handled
|
|
1553
|
+
# by the collection paths below.
|
|
1551
1554
|
receiver_spec = self._type_spec_from_expr(node.object)
|
|
1552
1555
|
if (receiver_spec is not None
|
|
1553
1556
|
and receiver_spec.kind == "udt"
|
|
1554
|
-
and receiver_spec.name in DRAWING_TYPE_TO_CPP
|
|
1557
|
+
and (receiver_spec.name in DRAWING_TYPE_TO_CPP
|
|
1558
|
+
or receiver_spec.name in self._udt_defs)):
|
|
1555
1559
|
return receiver_spec
|
|
1560
|
+
annotation = history_annotation(node)
|
|
1561
|
+
if annotation is not None:
|
|
1562
|
+
# An array's or a matrix's history is a collection of its
|
|
1563
|
+
# type (pineforge_codegen/collection_history.py), the
|
|
1564
|
+
# analyzer's when no declaration registered one yet.
|
|
1565
|
+
if (receiver_spec is not None
|
|
1566
|
+
and receiver_spec.kind in ("array", "matrix")):
|
|
1567
|
+
return receiver_spec
|
|
1568
|
+
return annotation["spec"]
|
|
1556
1569
|
return None
|
|
1557
1570
|
if isinstance(node, Ternary):
|
|
1558
1571
|
true_spec = self._type_spec_from_expr(node.true_val)
|
|
@@ -1851,13 +1864,18 @@ class TypeInferer:
|
|
|
1851
1864
|
if return_spec is not None:
|
|
1852
1865
|
return return_spec
|
|
1853
1866
|
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
1854
|
-
|
|
1867
|
+
# A method on a matrix's history names its method only
|
|
1868
|
+
# in the member (``_resolve_callee`` reports none).
|
|
1869
|
+
method = (member_name
|
|
1870
|
+
if history_annotation(node.callee.object) is not None
|
|
1871
|
+
else func_name)
|
|
1872
|
+
if method in MATRIX_RETURNING_METHODS:
|
|
1855
1873
|
return recv_spec
|
|
1856
|
-
if
|
|
1874
|
+
if method in ("row", "col"):
|
|
1857
1875
|
return TypeSpec.array(recv_spec.element)
|
|
1858
|
-
if
|
|
1876
|
+
if method == "get":
|
|
1859
1877
|
return recv_spec.element
|
|
1860
|
-
if
|
|
1878
|
+
if method == "eigenvalues":
|
|
1861
1879
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
1862
1880
|
# Drawing method-form: ``a.copy()`` -> same handle type;
|
|
1863
1881
|
# ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
|
|
@@ -2206,6 +2224,9 @@ class TypeInferer:
|
|
|
2206
2224
|
# init RHS is such a builtin, so also match the builtin name directly.
|
|
2207
2225
|
if name in INT64_BUILTINS:
|
|
2208
2226
|
return "int64_t"
|
|
2227
|
+
handle = self._series_handle_cpp_type(name)
|
|
2228
|
+
if handle is not None:
|
|
2229
|
+
return handle
|
|
2209
2230
|
sym = self._variable_symbol(name)
|
|
2210
2231
|
# A float or bool an epoch reaches keeps its type: the analyzer types
|
|
2211
2232
|
# every request.security value float (a double holds an epoch
|
|
@@ -2225,6 +2246,51 @@ class TypeInferer:
|
|
|
2225
2246
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
2226
2247
|
return "double"
|
|
2227
2248
|
|
|
2249
|
+
def _series_handle_type_name(self, name: str) -> str | None:
|
|
2250
|
+
"""The user-defined or drawing type (``Cell``, ``box``) of the
|
|
2251
|
+
history-read variable ``name``, else None. Such a variable holds a
|
|
2252
|
+
reference, and its history the references it held: ``(c[1]).v`` reads
|
|
2253
|
+
the object ``c`` held one bar back as it is now (fixtures/
|
|
2254
|
+
udt_history_tv; Pine v6 User Manual, "Type system": value vs.
|
|
2255
|
+
reference types). A buffer is keyed by spelling across scopes, so
|
|
2256
|
+
every scope's history-read variable of that spelling must hold the
|
|
2257
|
+
same type; any other variable keeps the buffer it always had.
|
|
2258
|
+
Cached per name."""
|
|
2259
|
+
cache = self.__dict__.setdefault("_series_handle_type_cache", {})
|
|
2260
|
+
if name in cache:
|
|
2261
|
+
return cache[name]
|
|
2262
|
+
found: set[str] = set()
|
|
2263
|
+
for scope in self.ctx.symbols.all_scopes:
|
|
2264
|
+
sym = scope.symbols.get(name)
|
|
2265
|
+
if sym is None or not getattr(sym, "is_series", False):
|
|
2266
|
+
continue
|
|
2267
|
+
spec = getattr(sym, "type_spec", None)
|
|
2268
|
+
if (spec is None or spec.kind != "udt"
|
|
2269
|
+
or not (spec.name in DRAWING_TYPE_TO_CPP
|
|
2270
|
+
or spec.name in self._udt_defs)):
|
|
2271
|
+
found = set()
|
|
2272
|
+
break
|
|
2273
|
+
found.add(spec.name)
|
|
2274
|
+
cache[name] = next(iter(found)) if len(found) == 1 else None
|
|
2275
|
+
return cache[name]
|
|
2276
|
+
|
|
2277
|
+
def _series_handle_cpp_type(self, name: str) -> str | None:
|
|
2278
|
+
"""The handle such a variable stores per bar (``Series<Cell>``,
|
|
2279
|
+
``Series<Box>``; a na reference is the handle's ``T{}``), else
|
|
2280
|
+
None (``_series_handle_type_name``)."""
|
|
2281
|
+
udt = self._series_handle_type_name(name)
|
|
2282
|
+
if udt is None:
|
|
2283
|
+
return None
|
|
2284
|
+
return DRAWING_TYPE_TO_CPP.get(udt) or self._safe_name(udt)
|
|
2285
|
+
|
|
2286
|
+
def _series_handle_target(self, name: str) -> str | None:
|
|
2287
|
+
"""``_visit_rhs_value``'s target for a value stored in such a
|
|
2288
|
+
variable's history: the drawing handle, or the user type's name."""
|
|
2289
|
+
udt = self._series_handle_type_name(name)
|
|
2290
|
+
if udt is None:
|
|
2291
|
+
return None
|
|
2292
|
+
return DRAWING_TYPE_TO_CPP.get(udt, udt)
|
|
2293
|
+
|
|
2228
2294
|
def _variable_symbol(self, name: str):
|
|
2229
2295
|
"""The symbol the variable ``name`` resolves to.
|
|
2230
2296
|
|
|
@@ -2303,6 +2369,18 @@ class TypeInferer:
|
|
|
2303
2369
|
declared_spec = (
|
|
2304
2370
|
declared_specs[index] if index < len(declared_specs) else None
|
|
2305
2371
|
)
|
|
2372
|
+
# A parameter holding a user-defined object or a drawing keeps the
|
|
2373
|
+
# references its calls passed (``_series_handle_cpp_type``).
|
|
2374
|
+
inferred_specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
2375
|
+
for candidate in (
|
|
2376
|
+
declared_spec,
|
|
2377
|
+
inferred_specs[index] if index < len(inferred_specs) else None,
|
|
2378
|
+
):
|
|
2379
|
+
if (candidate is not None and candidate.kind == "udt"
|
|
2380
|
+
and (candidate.name in DRAWING_TYPE_TO_CPP
|
|
2381
|
+
or candidate.name in self._udt_defs)):
|
|
2382
|
+
return (DRAWING_TYPE_TO_CPP.get(candidate.name)
|
|
2383
|
+
or self._safe_name(candidate.name))
|
|
2306
2384
|
# A declared type is authoritative across every written call. Only a
|
|
2307
2385
|
# truly untyped slot may consult the per-callsite specialization map.
|
|
2308
2386
|
spec = declared_spec
|
|
@@ -3302,6 +3380,19 @@ class TypeInferer:
|
|
|
3302
3380
|
cpp_type = "int64_t"
|
|
3303
3381
|
return cpp_type if cpp_type in (*NA_PRESERVING_INT_TYPES, "bool") else None
|
|
3304
3382
|
|
|
3383
|
+
def _udt_field_is_double(self, target_node) -> bool:
|
|
3384
|
+
"""Whether a UDT field write target is a ``float`` field (a C++
|
|
3385
|
+
``double``)."""
|
|
3386
|
+
if not isinstance(target_node, MemberAccess):
|
|
3387
|
+
return False
|
|
3388
|
+
owner = self._type_spec_from_expr(target_node.object)
|
|
3389
|
+
if owner is None or owner.kind != "udt" or not owner.name:
|
|
3390
|
+
return False
|
|
3391
|
+
spec = (self._udt_field_type_specs.get(owner.name) or {}).get(
|
|
3392
|
+
target_node.member
|
|
3393
|
+
)
|
|
3394
|
+
return spec is not None and self._type_spec_to_cpp(spec) == "double"
|
|
3395
|
+
|
|
3305
3396
|
def _coerce_int_slot(self, cpp_val: str, node, target_cpp_type: str | None,
|
|
3306
3397
|
*, value_is_double: bool | None = None) -> str:
|
|
3307
3398
|
"""Route a value into an ``int``/``int64_t`` slot without losing ``na``.
|
|
@@ -3343,6 +3434,30 @@ class TypeInferer:
|
|
|
3343
3434
|
return na_preserving_int_cast(cpp_val, target_cpp_type)
|
|
3344
3435
|
return cpp_val
|
|
3345
3436
|
|
|
3437
|
+
def _coerce_double_slot(self, cpp_val: str, node) -> str:
|
|
3438
|
+
"""Route a value into a ``double`` that a designated initializer
|
|
3439
|
+
fills (a user-defined record's ``float`` field: ``Cell.new(v =
|
|
3440
|
+
bar_index)``). C++ refuses to narrow a non-constant ``int`` or
|
|
3441
|
+
``int64_t`` there, which did not compile; TradingView holds the
|
|
3442
|
+
number, an integer na as na (fixtures/array_history_tv uctor_float).
|
|
3443
|
+
A double, an integer literal or an expression of literals (a constant
|
|
3444
|
+
a double holds exactly narrows) and a bare na keep their spelling."""
|
|
3445
|
+
constant = self._pure_int_literal_value(node)
|
|
3446
|
+
if (cpp_val == "na<double>()"
|
|
3447
|
+
or self._INTEGRAL_CPP_TEXT.match(cpp_val)
|
|
3448
|
+
or (constant is not None and abs(constant) <= 2 ** 53)
|
|
3449
|
+
or self._emitted_value_is_double(node)):
|
|
3450
|
+
return cpp_val
|
|
3451
|
+
if cpp_val in {"na<int>()", "na<int64_t>()"}:
|
|
3452
|
+
return "na<double>()"
|
|
3453
|
+
# A bool (TradingView refuses one here: CE10123, CE10173) is 1 or 0,
|
|
3454
|
+
# never na, whatever is_na() of a bool resolves to.
|
|
3455
|
+
return (f"[&](){{ auto _pf_w = ({cpp_val}); "
|
|
3456
|
+
f"if constexpr (std::is_floating_point_v<decltype(_pf_w)> "
|
|
3457
|
+
f"|| std::is_same_v<decltype(_pf_w), bool>) "
|
|
3458
|
+
f"return (double)_pf_w; "
|
|
3459
|
+
f"else return is_na(_pf_w) ? na<double>() : (double)_pf_w; }}()")
|
|
3460
|
+
|
|
3346
3461
|
def _coerce_int_slot_with_cast(
|
|
3347
3462
|
self, cpp_val: str, node, target_cpp_type: str
|
|
3348
3463
|
) -> str:
|
|
@@ -3605,12 +3720,25 @@ class TypeInferer:
|
|
|
3605
3720
|
and ternaries / if / switch expressions. Returns the string
|
|
3606
3721
|
``"double"`` as the safe fallback when no narrower type can be
|
|
3607
3722
|
determined."""
|
|
3723
|
+
if isinstance(node, Subscript) and history_annotation(node) is not None:
|
|
3724
|
+
# An array's or a matrix's history is a collection of its type
|
|
3725
|
+
# (collection_history.py): a loop's local bound to it is one.
|
|
3726
|
+
spec = self._type_spec_from_expr(node)
|
|
3727
|
+
if spec is not None:
|
|
3728
|
+
return self._type_spec_to_cpp(spec)
|
|
3608
3729
|
if isinstance(node, Subscript) and isinstance(node.object, Identifier):
|
|
3609
3730
|
name = node.object.name
|
|
3610
3731
|
if name in self.ctx.series_vars:
|
|
3611
3732
|
return self._series_type_for(name)
|
|
3612
3733
|
if name in self.ctx.series_bar_fields:
|
|
3613
3734
|
return "double"
|
|
3735
|
+
if (isinstance(node, Subscript)
|
|
3736
|
+
and isinstance(node.object, (FuncCall, Ternary, MemberAccess))):
|
|
3737
|
+
# The history of an expression whose value is an object or a
|
|
3738
|
+
# drawing is a reference of its type (``_history_value_cpp_type``).
|
|
3739
|
+
reference = self._reference_cpp_type(node.object)
|
|
3740
|
+
if reference is not None:
|
|
3741
|
+
return reference
|
|
3614
3742
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
3615
3743
|
# A callable-result history read keeps the callable's scalar
|
|
3616
3744
|
# family. Collection subscripts follow separate element-type paths.
|
|
@@ -143,11 +143,13 @@ from ..ast_nodes import (
|
|
|
143
143
|
MemberAccess,
|
|
144
144
|
NaLiteral,
|
|
145
145
|
NumberLiteral,
|
|
146
|
+
Subscript,
|
|
146
147
|
Ternary,
|
|
147
148
|
TupleLiteral,
|
|
148
149
|
StringLiteral,
|
|
149
150
|
VarDecl,
|
|
150
151
|
)
|
|
152
|
+
from ..collection_history import history_annotation, literal_offset
|
|
151
153
|
from ..external_requests import RECORDED_KEY_ANNOTATION
|
|
152
154
|
from ..limits import iter_ast_nodes
|
|
153
155
|
from ..security_contexts import ticker_symbol_arg
|
|
@@ -583,7 +585,9 @@ class CallVisitor:
|
|
|
583
585
|
for arg in rest_nodes
|
|
584
586
|
)
|
|
585
587
|
)) or self._binds_fresh_array_to_reference(
|
|
586
|
-
method_info, rest_nodes, first_param=1)
|
|
588
|
+
method_info, rest_nodes, first_param=1)
|
|
589
|
+
or self._binds_temporary_drawing_to_reference(
|
|
590
|
+
method_info, [receiver_node, *rest_nodes]),
|
|
587
591
|
)
|
|
588
592
|
|
|
589
593
|
def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None:
|
|
@@ -1211,6 +1215,12 @@ class CallVisitor:
|
|
|
1211
1215
|
returns its argument or a global) or a selection of a held and a
|
|
1212
1216
|
fresh array, and ``"keep"`` for every other argument, which keeps the
|
|
1213
1217
|
lowering it had: a variable, a field, ``na``, any other built-in."""
|
|
1218
|
+
annotation = history_annotation(node)
|
|
1219
|
+
if annotation is not None:
|
|
1220
|
+
# A copy of an array's history for the callee (collection_history.py);
|
|
1221
|
+
# at a zero offset, the variable itself.
|
|
1222
|
+
return ("keep" if literal_offset(node) == 0 or annotation["use"] != "copy"
|
|
1223
|
+
else "fresh")
|
|
1214
1224
|
if isinstance(node, Ternary):
|
|
1215
1225
|
kinds = {self._array_arg_kind(node.true_val, depth),
|
|
1216
1226
|
self._array_arg_kind(node.false_val, depth)}
|
|
@@ -1306,6 +1316,63 @@ class CallVisitor:
|
|
|
1306
1316
|
stage = stage or kind == "fresh"
|
|
1307
1317
|
return stage
|
|
1308
1318
|
|
|
1319
|
+
# The drawing types a callable takes as ``T&`` (emit_top: a parameter of
|
|
1320
|
+
# a type outside the user types is a reference).
|
|
1321
|
+
_REFERENCE_DRAWING_PARAMS = frozenset({"line", "box", "label", "linefill", "chart.point"})
|
|
1322
|
+
|
|
1323
|
+
def _drawing_arg_is_temporary(self, arg) -> bool:
|
|
1324
|
+
"""Whether the C++ of the drawing argument ``arg`` is a prvalue no
|
|
1325
|
+
``T&`` takes: a history read or the current slot of a history-read
|
|
1326
|
+
variable (``b[1]``, ``b[0]``: ``Series::operator[]`` returns a copy),
|
|
1327
|
+
a new or copied drawing or chart point, a user call's result, or a
|
|
1328
|
+
selection with such an arm. A variable, a field and a collection
|
|
1329
|
+
element are lvalues, which compiled before and keep their C++."""
|
|
1330
|
+
if isinstance(arg, Subscript):
|
|
1331
|
+
return True
|
|
1332
|
+
if isinstance(arg, Identifier):
|
|
1333
|
+
return self._identifier_reads_series(arg)
|
|
1334
|
+
if isinstance(arg, Ternary):
|
|
1335
|
+
return (self._drawing_arg_is_temporary(arg.true_val)
|
|
1336
|
+
or self._drawing_arg_is_temporary(arg.false_val))
|
|
1337
|
+
if not isinstance(arg, FuncCall):
|
|
1338
|
+
return False
|
|
1339
|
+
func_name, namespace = self._resolve_callee(arg.callee)
|
|
1340
|
+
if namespace in DRAWING_NS and func_name in ("new", "copy"):
|
|
1341
|
+
return True
|
|
1342
|
+
if self._is_chart_point_callee(arg.callee):
|
|
1343
|
+
return True
|
|
1344
|
+
if namespace is None and func_name in self._func_info_map:
|
|
1345
|
+
return True
|
|
1346
|
+
return (isinstance(arg.callee, MemberAccess)
|
|
1347
|
+
and self._typed_user_method_info(
|
|
1348
|
+
arg.callee.object, arg.callee.member)[1] is not None)
|
|
1349
|
+
|
|
1350
|
+
def _binds_temporary_drawing_to_reference(self, func_info, arg_nodes: list,
|
|
1351
|
+
first_param: int = 0) -> bool:
|
|
1352
|
+
"""Whether a call passes a temporary drawing (``_drawing_arg_is_temporary``)
|
|
1353
|
+
to a drawing parameter its callee takes as ``T&``: staging the call
|
|
1354
|
+
(``_ordered_user_call_expr``) binds it to a named forwarding
|
|
1355
|
+
reference. The handle is an arena id and Pine forbids reassigning a
|
|
1356
|
+
parameter, so the callee reads the drawing the argument names
|
|
1357
|
+
(udth_drawparam). A history-read parameter takes a Series bridge
|
|
1358
|
+
instead."""
|
|
1359
|
+
if func_info is None:
|
|
1360
|
+
return False
|
|
1361
|
+
specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
1362
|
+
params = (list(func_info.node.params)
|
|
1363
|
+
if getattr(func_info, "node", None) is not None else [])
|
|
1364
|
+
series = self.ctx.func_series_vars.get(func_info.name, set())
|
|
1365
|
+
for offset, arg in enumerate(arg_nodes):
|
|
1366
|
+
index = first_param + offset
|
|
1367
|
+
spec = specs[index] if index < len(specs) else None
|
|
1368
|
+
if (arg is None or spec is None or spec.kind != "udt"
|
|
1369
|
+
or spec.name not in self._REFERENCE_DRAWING_PARAMS
|
|
1370
|
+
or (index < len(params) and params[index] in series)):
|
|
1371
|
+
continue
|
|
1372
|
+
if self._drawing_arg_is_temporary(arg):
|
|
1373
|
+
return True
|
|
1374
|
+
return False
|
|
1375
|
+
|
|
1309
1376
|
def _ordered_user_call_expr(
|
|
1310
1377
|
self,
|
|
1311
1378
|
call_head: str,
|
|
@@ -1584,6 +1651,17 @@ class CallVisitor:
|
|
|
1584
1651
|
args = self._array_method_args(callee.member, arg_nodes, recv_spec)
|
|
1585
1652
|
return self._array_method_expr(recv, callee.member, args, recv_spec, node)
|
|
1586
1653
|
|
|
1654
|
+
# A matrix method on a matrix's history (``(m[1]).get(0, 0)``:
|
|
1655
|
+
# collection_history.py), which no other receiver path reaches.
|
|
1656
|
+
if (isinstance(callee, MemberAccess)
|
|
1657
|
+
and isinstance(callee.object, Subscript)
|
|
1658
|
+
and history_annotation(callee.object) is not None
|
|
1659
|
+
and callee.member in MATRIX_METHODS):
|
|
1660
|
+
recv_spec = self._type_spec_from_expr(callee.object)
|
|
1661
|
+
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
1662
|
+
return self._matrix_history_method_expr(
|
|
1663
|
+
callee.object, callee.member, recv_spec, node)
|
|
1664
|
+
|
|
1587
1665
|
# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
|
|
1588
1666
|
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
|
|
1589
1667
|
# which would otherwise mis-treat ``chart.point`` as a receiver object
|
|
@@ -1823,6 +1901,9 @@ class CallVisitor:
|
|
|
1823
1901
|
drawing = self._drawing_na_expr(node.args[0])
|
|
1824
1902
|
if drawing is not None:
|
|
1825
1903
|
return drawing
|
|
1904
|
+
history = self._collection_history_na(node.args[0])
|
|
1905
|
+
if history is not None:
|
|
1906
|
+
return history
|
|
1826
1907
|
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
1827
1908
|
return f"is_na({args})"
|
|
1828
1909
|
|
|
@@ -2564,9 +2645,13 @@ class CallVisitor:
|
|
|
2564
2645
|
if func_name in MATRIX_METHODS and node.args:
|
|
2565
2646
|
from ..ast_nodes import Identifier as _Ident
|
|
2566
2647
|
if func_name in MATRIX_NUMERIC_ONLY:
|
|
2567
|
-
|
|
2648
|
+
recv_node = node.args[0]
|
|
2649
|
+
if history_annotation(recv_node) is not None:
|
|
2650
|
+
# A matrix's history is a matrix of its type.
|
|
2651
|
+
recv_node = recv_node.object
|
|
2652
|
+
if not isinstance(recv_node, _Ident):
|
|
2568
2653
|
self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
|
|
2569
|
-
recv_name =
|
|
2654
|
+
recv_name = recv_node.name
|
|
2570
2655
|
recv_spec = self._collection_spec_for_name(recv_name)
|
|
2571
2656
|
if recv_spec is None or recv_spec.kind != "matrix":
|
|
2572
2657
|
self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
|
|
@@ -2684,6 +2769,8 @@ class CallVisitor:
|
|
|
2684
2769
|
val = "na<int64_t>()"
|
|
2685
2770
|
elif f_cpp_type == "bool":
|
|
2686
2771
|
val = self._coerce_bool_expr(val, value_node)
|
|
2772
|
+
elif f_cpp_type == "double":
|
|
2773
|
+
val = self._coerce_double_slot(val, value_node)
|
|
2687
2774
|
elif (self._is_nullable_collection_cpp_type(f_cpp_type)
|
|
2688
2775
|
and val == "na<double>()"):
|
|
2689
2776
|
val = f"{f_cpp_type}{{}}"
|
|
@@ -2958,6 +3045,8 @@ class CallVisitor:
|
|
|
2958
3045
|
all_args,
|
|
2959
3046
|
source_order_nodes=[*node.args, *node.kwargs.values()],
|
|
2960
3047
|
force_stage=self._binds_fresh_array_to_reference(
|
|
3048
|
+
self._func_info_map.get(func_name), ordered_arg_nodes)
|
|
3049
|
+
or self._binds_temporary_drawing_to_reference(
|
|
2961
3050
|
self._func_info_map.get(func_name), ordered_arg_nodes),
|
|
2962
3051
|
)
|
|
2963
3052
|
return f"{call_head}({', '.join(all_args)})"
|
|
@@ -88,6 +88,7 @@ classes from ``..ast_nodes``.
|
|
|
88
88
|
from __future__ import annotations
|
|
89
89
|
|
|
90
90
|
from ..errors import Phase
|
|
91
|
+
from ..symbols import TypeSpec
|
|
91
92
|
from ..external_requests import UNPINNED_ANNOTATION
|
|
92
93
|
from ..ast_nodes import (
|
|
93
94
|
ASTNode,
|
|
@@ -503,6 +504,18 @@ class ExprVisitor:
|
|
|
503
504
|
return (f'([&]() {{ pine_runtime_error(std::string("{self._cpp_string_escape(marker)}")); '
|
|
504
505
|
f"return {value}; }}())")
|
|
505
506
|
|
|
507
|
+
def _identifier_reads_series(self, node: Identifier) -> bool:
|
|
508
|
+
"""Whether ``_visit_ident`` lowers ``node`` to its Series' current slot
|
|
509
|
+
(``x[0]``, a copy): a history-read parameter or variable."""
|
|
510
|
+
name = node.name
|
|
511
|
+
if name in self._current_func_series_params:
|
|
512
|
+
return True
|
|
513
|
+
if (name in self._current_func_param_types or name in BAR_FIELDS
|
|
514
|
+
or name in BAR_BUILTINS):
|
|
515
|
+
return False
|
|
516
|
+
return self._binding_is_series(
|
|
517
|
+
name, self._call_site_var_name(node, self._safe_name(name)))
|
|
518
|
+
|
|
506
519
|
def _visit_ident(self, node: Identifier) -> str:
|
|
507
520
|
name = node.name
|
|
508
521
|
# Bare 'na' identifier → na<double>()
|
|
@@ -1270,7 +1283,66 @@ class ExprVisitor:
|
|
|
1270
1283
|
return self._emit_na_relational(op, left_cpp, right_cpp)
|
|
1271
1284
|
return f"({left_cpp} {op} {right_cpp})"
|
|
1272
1285
|
|
|
1286
|
+
# The reference types TradingView compares with == and != (lab tv
|
|
1287
|
+
# pf-udth-line-eq: by identity, a na reference included); every other
|
|
1288
|
+
# object or drawing type is CE10123 there.
|
|
1289
|
+
_IDENTITY_COMPARED_REFERENCES = frozenset({"line", "label"})
|
|
1290
|
+
|
|
1291
|
+
def _reference_operand_kind(self, operand) -> str | None:
|
|
1292
|
+
"""The type of a user-defined object or drawing reference operand
|
|
1293
|
+
(``Cell``, ``box``, ``chart.point``, ...), else None."""
|
|
1294
|
+
spec = self._type_spec_from_expr(operand)
|
|
1295
|
+
if spec is None or spec.kind != "udt" or not spec.name:
|
|
1296
|
+
return None
|
|
1297
|
+
if spec.name not in DRAWING_TYPE_TO_CPP and spec.name not in self._udt_defs:
|
|
1298
|
+
return None
|
|
1299
|
+
if self._infer_type(operand) in (
|
|
1300
|
+
"double", "int", "int64_t", "bool", "std::string"):
|
|
1301
|
+
return None
|
|
1302
|
+
return spec.name
|
|
1303
|
+
|
|
1304
|
+
def _reference_equality_cpp(self, node: BinOp) -> str | None:
|
|
1305
|
+
"""``==`` / ``!=`` with a reference operand: a line or a label equals
|
|
1306
|
+
the same line or label, na included (TradingView compares their
|
|
1307
|
+
references; a deleted drawing keeps its own), the handles' ids here.
|
|
1308
|
+
TradingView refuses the comparison of any other object or drawing
|
|
1309
|
+
(CE10123) and of a reference with na (CE10187), which never compiled
|
|
1310
|
+
here either."""
|
|
1311
|
+
kinds = (self._reference_operand_kind(node.left),
|
|
1312
|
+
self._reference_operand_kind(node.right))
|
|
1313
|
+
if kinds == (None, None):
|
|
1314
|
+
return None
|
|
1315
|
+
other = node.right if kinds[0] is not None else node.left
|
|
1316
|
+
kind = kinds[0] or kinds[1]
|
|
1317
|
+
if isinstance(other, NaLiteral) or (
|
|
1318
|
+
isinstance(other, Identifier) and other.name == "na"):
|
|
1319
|
+
self._codegen_error(
|
|
1320
|
+
node,
|
|
1321
|
+
f"{node.op} na on a {kind} reference: TradingView refuses a "
|
|
1322
|
+
"comparison with na (CE10187: \"Cannot compare a value to "
|
|
1323
|
+
"\\\"na\\\" directly\").",
|
|
1324
|
+
hint="Test the reference with na(...).",
|
|
1325
|
+
)
|
|
1326
|
+
if kinds[0] != kinds[1] or kind not in self._IDENTITY_COMPARED_REFERENCES:
|
|
1327
|
+
self._codegen_error(
|
|
1328
|
+
node,
|
|
1329
|
+
f"{node.op} on a {kind} reference: TradingView compares only "
|
|
1330
|
+
"line and label references (CE10123: \"Cannot call "
|
|
1331
|
+
f"'operator {node.op}'\" with an argument of the {kind} type).",
|
|
1332
|
+
hint="Compare the fields or getter values the objects hold, "
|
|
1333
|
+
"or test na(...).",
|
|
1334
|
+
)
|
|
1335
|
+
left = self._visit_expr(node.left)
|
|
1336
|
+
right = self._visit_expr(node.right)
|
|
1337
|
+
return self._left_operand_first(
|
|
1338
|
+
node, left, right,
|
|
1339
|
+
lambda left, right: f"(({left}).id {node.op} ({right}).id)")
|
|
1340
|
+
|
|
1273
1341
|
def _visit_binop(self, node: BinOp) -> str:
|
|
1342
|
+
if node.op in ("==", "!="):
|
|
1343
|
+
identity = self._reference_equality_cpp(node)
|
|
1344
|
+
if identity is not None:
|
|
1345
|
+
return identity
|
|
1274
1346
|
# An int-literal-only ``+ - *`` tree whose exact value leaves int32 is
|
|
1275
1347
|
# a 64-bit Pine int (``90 * 24 * 60 * 60 * 1000`` = 7 776 000 000);
|
|
1276
1348
|
# C++ ``int`` literal arithmetic would wrap it. Fold it here and spell
|
|
@@ -1723,7 +1795,119 @@ class ExprVisitor:
|
|
|
1723
1795
|
idx_int = f"({idx_int})"
|
|
1724
1796
|
return idx_int
|
|
1725
1797
|
|
|
1798
|
+
def _warn_untracked_reference_history(self, node: Subscript) -> None:
|
|
1799
|
+
"""A history read of an object or drawing reference that lowers to
|
|
1800
|
+
the current reference, which keeps the lowering it compiled to: the
|
|
1801
|
+
analyzer registered no history for it here, while TradingView reads
|
|
1802
|
+
the reference the variable held k bars back (fixtures/
|
|
1803
|
+
udt_history_tv)."""
|
|
1804
|
+
if isinstance(node.index, NumberLiteral) and node.index.value == 0:
|
|
1805
|
+
return
|
|
1806
|
+
spec = self._type_spec_from_expr(node.object)
|
|
1807
|
+
if spec is not None and spec.kind in ("array", "matrix"):
|
|
1808
|
+
# A parameter's array or matrix history (collection_history.py:
|
|
1809
|
+
# the earlier lowering, kept where it compiled).
|
|
1810
|
+
self._codegen_warning(
|
|
1811
|
+
node,
|
|
1812
|
+
f"{node.object.name}[...] reads the current {spec.kind} in "
|
|
1813
|
+
"PineForge: no history of this parameter is kept, where "
|
|
1814
|
+
"TradingView reads the copy the call that many calls back "
|
|
1815
|
+
"left (fixtures/array_history_tv ahist_fn).",
|
|
1816
|
+
)
|
|
1817
|
+
return
|
|
1818
|
+
if (spec is None or spec.kind != "udt"
|
|
1819
|
+
or not (spec.name in DRAWING_TYPE_TO_CPP
|
|
1820
|
+
or spec.name in self._udt_defs)):
|
|
1821
|
+
return
|
|
1822
|
+
self._codegen_warning(
|
|
1823
|
+
node,
|
|
1824
|
+
f"{node.object.name}[...] reads the current {spec.name} reference "
|
|
1825
|
+
"in PineForge: no history of this variable is kept here, where "
|
|
1826
|
+
"TradingView reads the reference it held that many bars back.",
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
def _chart_point_field_written_names(self) -> set[str]:
|
|
1830
|
+
"""The chart.point variables a field of which the script assigns
|
|
1831
|
+
(``p.price := x``). Cached."""
|
|
1832
|
+
cached = getattr(self, "_chart_point_writes_cache", None)
|
|
1833
|
+
if cached is not None:
|
|
1834
|
+
return cached
|
|
1835
|
+
point = TypeSpec.udt("chart.point")
|
|
1836
|
+
names: set[str] = set()
|
|
1837
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
1838
|
+
if (isinstance(node, Assignment)
|
|
1839
|
+
and isinstance(node.target, MemberAccess)
|
|
1840
|
+
and isinstance(node.target.object, Identifier)
|
|
1841
|
+
and any(getattr(scope.symbols.get(node.target.object.name),
|
|
1842
|
+
"type_spec", None) == point
|
|
1843
|
+
for scope in self.ctx.symbols.all_scopes)):
|
|
1844
|
+
names.add(node.target.object.name)
|
|
1845
|
+
self._chart_point_writes_cache = names
|
|
1846
|
+
return names
|
|
1847
|
+
|
|
1848
|
+
def _refuse_mutable_chart_point_history(self, node: Subscript) -> None:
|
|
1849
|
+
"""History of a chart.point variable the script changes a field of.
|
|
1850
|
+
TradingView's chart.point is an object (its history holds references,
|
|
1851
|
+
read as they are now: fixtures/udt_history_tv); PineForge holds one
|
|
1852
|
+
as a value, so its history would read a changed point's old value,
|
|
1853
|
+
and the field write on its history did not compile. A point no field
|
|
1854
|
+
write changes reads alike either way."""
|
|
1855
|
+
if isinstance(node.index, NumberLiteral) and node.index.value == 0:
|
|
1856
|
+
return
|
|
1857
|
+
if not (isinstance(node.object, Identifier)
|
|
1858
|
+
and node.object.name in self._chart_point_field_written_names()
|
|
1859
|
+
and self._type_spec_from_expr(node.object)
|
|
1860
|
+
== TypeSpec.udt("chart.point")):
|
|
1861
|
+
return
|
|
1862
|
+
self._codegen_error(
|
|
1863
|
+
node,
|
|
1864
|
+
f"History of the chart.point {node.object.name} is not supported "
|
|
1865
|
+
"in PineForge when the script changes its fields: PineForge holds "
|
|
1866
|
+
"a chart.point as a value, where TradingView's history reads the "
|
|
1867
|
+
"point object as it is now.",
|
|
1868
|
+
hint="Keep the field in a variable of its own and read that "
|
|
1869
|
+
"variable's history, or build a new point instead of "
|
|
1870
|
+
"changing a field.",
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
def _refuse_field_value_history(self, node: Subscript) -> None:
|
|
1874
|
+
"""History of a user-defined object's field that holds a value:
|
|
1875
|
+
TradingView refuses ``c.v[1]`` and ``(c.v)[1]`` alike ("Cannot use the
|
|
1876
|
+
history-referencing operator on fields of user-defined types",
|
|
1877
|
+
CE10290) and takes ``(c[1]).v``; the C++ subscripted the field's
|
|
1878
|
+
scalar, which did not compile. A field holding an object or a drawing
|
|
1879
|
+
is a reference, whose history TradingView keeps (udth_expr); the
|
|
1880
|
+
analyzer refuses a collection field's history (CE10290 too:
|
|
1881
|
+
``_refuse_collection_field_history``)."""
|
|
1882
|
+
field = node.object
|
|
1883
|
+
if not isinstance(field, MemberAccess):
|
|
1884
|
+
return
|
|
1885
|
+
owner = self._type_spec_from_expr(field.object)
|
|
1886
|
+
if (owner is None or owner.kind != "udt"
|
|
1887
|
+
or owner.name not in self._udt_defs
|
|
1888
|
+
or self._reference_cpp_type(field) is not None):
|
|
1889
|
+
return
|
|
1890
|
+
field_spec = self._udt_field_type_specs.get(owner.name, {}).get(field.member)
|
|
1891
|
+
if field_spec is not None and field_spec.kind in ("array", "map", "matrix"):
|
|
1892
|
+
return
|
|
1893
|
+
receiver = (field.object.name if isinstance(field.object, Identifier)
|
|
1894
|
+
else "object")
|
|
1895
|
+
self._codegen_error(
|
|
1896
|
+
node,
|
|
1897
|
+
f"{receiver}.{field.member}[...]: TradingView refuses the "
|
|
1898
|
+
"history-referencing operator on fields of user-defined types "
|
|
1899
|
+
"(CE10290).",
|
|
1900
|
+
hint=f"Read the field of the object's history: "
|
|
1901
|
+
f"({receiver}[1]).{field.member}.",
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1726
1904
|
def _visit_subscript(self, node: Subscript) -> str:
|
|
1905
|
+
# An array's or a matrix's history (collection_history.py).
|
|
1906
|
+
collection_history = self._lower_collection_history(node)
|
|
1907
|
+
if collection_history is not None:
|
|
1908
|
+
return collection_history
|
|
1909
|
+
self._refuse_mutable_chart_point_history(node)
|
|
1910
|
+
self._refuse_field_value_history(node)
|
|
1727
1911
|
idx = self._visit_expr(node.index)
|
|
1728
1912
|
# Series::operator[] accepts C++ int. A Pine int can be backed by an
|
|
1729
1913
|
# int64_t timestamp slot; implicitly narrowing its na sentinel to int
|
|
@@ -1747,6 +1931,7 @@ class ExprVisitor:
|
|
|
1747
1931
|
return f"{self._safe_name(name)}[{series_idx}]"
|
|
1748
1932
|
# Function parameters are scalars — src[0] → src, src[N>0] → src
|
|
1749
1933
|
if name in self._current_func_param_types:
|
|
1934
|
+
self._warn_untracked_reference_history(node)
|
|
1750
1935
|
return self._safe_name(name)
|
|
1751
1936
|
if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
|
|
1752
1937
|
# Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
|
|
@@ -1757,6 +1942,9 @@ class ExprVisitor:
|
|
|
1757
1942
|
if self._binding_is_series(name, safe):
|
|
1758
1943
|
# Same Pine [k] semantics as Series in runtime/series.hpp
|
|
1759
1944
|
return f"{safe}[{series_idx}]"
|
|
1945
|
+
# An array's history the analyzer annotated lowers above
|
|
1946
|
+
# (collection_history.py); this legacy element read is left for
|
|
1947
|
+
# one it never met.
|
|
1760
1948
|
spec = self._collection_spec_for_name(name)
|
|
1761
1949
|
if spec is not None and spec.kind in ("array", "map"):
|
|
1762
1950
|
self._codegen_warning(
|
|
@@ -1804,6 +1992,10 @@ class ExprVisitor:
|
|
|
1804
1992
|
cpp_t = self._infer_type(node.object)
|
|
1805
1993
|
if cpp_t not in ("double", "int", "int64_t", "bool"):
|
|
1806
1994
|
cpp_t = "double"
|
|
1995
|
+
# A call's object or drawing result keeps the references it
|
|
1996
|
+
# returned (``_history_value_cpp_type``), in the Series the
|
|
1997
|
+
# prepass declared.
|
|
1998
|
+
cpp_t = self._registered_history_handle(node) or cpp_t
|
|
1807
1999
|
member = self._inline_history_member("hist_call", node)
|
|
1808
2000
|
ta_site = self._get_ta_site(node.object)
|
|
1809
2001
|
ta_name = (
|
|
@@ -1870,12 +2062,18 @@ class ExprVisitor:
|
|
|
1870
2062
|
self._inline_history_member_by_key.get(
|
|
1871
2063
|
("hist_call", id(node), self._current_instance_name)
|
|
1872
2064
|
)
|
|
1873
|
-
if isinstance(node.object, (BinOp, UnaryOp, Ternary))
|
|
2065
|
+
if isinstance(node.object, (BinOp, UnaryOp, Ternary, MemberAccess))
|
|
1874
2066
|
else None
|
|
1875
2067
|
)
|
|
2068
|
+
hoisted_member = self._hoisted_hist_reads.get(id(node))
|
|
2069
|
+
if hoisted_member is not None:
|
|
2070
|
+
# A reference's history below a lazy edge, pushed on every
|
|
2071
|
+
# execution before the statement (``_emit_lazy_call_history_hoists``).
|
|
2072
|
+
return f"{hoisted_member}[{self._history_offset_cpp(idx, node.index)}]"
|
|
1876
2073
|
if compound_member is not None:
|
|
1877
|
-
|
|
1878
|
-
|
|
2074
|
+
reference_cpp = self._registered_history_handle(node)
|
|
2075
|
+
cpp_t = reference_cpp or self._infer_type(node.object)
|
|
2076
|
+
if reference_cpp is not None or cpp_t in ("double", "int", "int64_t", "bool"):
|
|
1879
2077
|
inner = self._visit_expr(node.object)
|
|
1880
2078
|
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1881
2079
|
return (
|
|
@@ -1893,6 +2091,7 @@ class ExprVisitor:
|
|
|
1893
2091
|
if (name not in BAR_FIELDS and name not in BAR_SERIES_PUSH
|
|
1894
2092
|
and name not in self.ctx.series_vars
|
|
1895
2093
|
and name not in self._var_names):
|
|
2094
|
+
self._warn_untracked_reference_history(node)
|
|
1896
2095
|
return obj
|
|
1897
2096
|
idx_int = self._history_offset_cpp(idx, node.index)
|
|
1898
2097
|
return f"{obj}[{idx_int}]"
|