@pineforge/codegen-pyodide 1.0.0 → 1.1.0
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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -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/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- 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/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -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}]"
|
|
@@ -469,6 +469,11 @@ class StmtVisitor:
|
|
|
469
469
|
return self._type_spec_to_cpp(spec)
|
|
470
470
|
|
|
471
471
|
def _visit_var_decl(self, node: VarDecl, lines: list[str], pad: str) -> None:
|
|
472
|
+
# An array or a matrix whose history the script reads: this execution
|
|
473
|
+
# of its declaration opens a slot, after closing a sibling
|
|
474
|
+
# declaration's (codegen/collection_history.py).
|
|
475
|
+
for statement in self._collection_history_decl_statements(node):
|
|
476
|
+
lines.append(f"{pad}{statement}")
|
|
472
477
|
member_meta = getattr(
|
|
473
478
|
self.ctx, "var_member_metadata_by_node", {}
|
|
474
479
|
).get(id(node))
|
|
@@ -830,7 +835,7 @@ class StmtVisitor:
|
|
|
830
835
|
target_cpp_type=(
|
|
831
836
|
target_cpp_type
|
|
832
837
|
if target_cpp_type in DRAWING_TYPE_TO_CPP.values()
|
|
833
|
-
else
|
|
838
|
+
else self._series_handle_target(node.name)
|
|
834
839
|
),
|
|
835
840
|
)
|
|
836
841
|
cpp_val = self._coerce_int_slot(
|
|
@@ -1122,6 +1127,11 @@ class StmtVisitor:
|
|
|
1122
1127
|
f"{self._udt_array_field_value(array_field, node.value, val_cpp)};")
|
|
1123
1128
|
elif node.op == ":=":
|
|
1124
1129
|
val_cpp = self._coerce_int_slot(val_cpp, node.value, field_int)
|
|
1130
|
+
if self._udt_field_is_double(node.target):
|
|
1131
|
+
# An int reaching a float field keeps its na (the
|
|
1132
|
+
# constructor's conversion: fixtures/array_history_tv
|
|
1133
|
+
# uctor_float).
|
|
1134
|
+
val_cpp = self._coerce_double_slot(val_cpp, node.value)
|
|
1125
1135
|
lines.append(f"{pad}{target_cpp} = {val_cpp};")
|
|
1126
1136
|
else:
|
|
1127
1137
|
rhs = self._compound_assign_rhs(target_cpp, node.op, val_cpp)
|
|
@@ -1146,9 +1156,9 @@ class StmtVisitor:
|
|
|
1146
1156
|
val_cpp = self._visit_rhs_value(
|
|
1147
1157
|
node.value,
|
|
1148
1158
|
target_name,
|
|
1149
|
-
target_cpp_type=
|
|
1150
|
-
target_name,
|
|
1151
|
-
|
|
1159
|
+
target_cpp_type=(
|
|
1160
|
+
self._drawing_target_cpp_type(target_name, None)
|
|
1161
|
+
or self._series_handle_target(target_name)
|
|
1152
1162
|
),
|
|
1153
1163
|
)
|
|
1154
1164
|
elem_int = self._int_slot_cpp_type(
|
|
@@ -1241,6 +1251,11 @@ class StmtVisitor:
|
|
|
1241
1251
|
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
1242
1252
|
|
|
1243
1253
|
def _visit_tuple_assign(self, node: TupleAssign, lines: list[str], pad: str) -> None:
|
|
1254
|
+
# A tuple declaration writing a member whose history a sibling
|
|
1255
|
+
# block's declaration keeps closes that history first
|
|
1256
|
+
# (codegen/collection_history.py).
|
|
1257
|
+
for statement in self._collection_history_decl_statements(node):
|
|
1258
|
+
lines.append(f"{pad}{statement}")
|
|
1244
1259
|
is_top_level = any(id(node) == id(stmt) for stmt in self.ctx.ast.body)
|
|
1245
1260
|
global_targets = (
|
|
1246
1261
|
set(getattr(self.ctx, "ordinary_global_binding_names", set()))
|