@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.
Files changed (31) hide show
  1. package/glue.py +2 -1
  2. package/package.json +1 -1
  3. package/pineforge_codegen/__init__.py +24 -9
  4. package/pineforge_codegen/analyzer/base.py +190 -1
  5. package/pineforge_codegen/analyzer/call_handlers.py +2 -0
  6. package/pineforge_codegen/analyzer/contracts.py +6 -0
  7. package/pineforge_codegen/analyzer/types.py +16 -5
  8. package/pineforge_codegen/ast_nodes.py +7 -0
  9. package/pineforge_codegen/codegen/base.py +136 -14
  10. package/pineforge_codegen/codegen/checked_settings.py +272 -0
  11. package/pineforge_codegen/codegen/collection_history.py +200 -0
  12. package/pineforge_codegen/codegen/emit_top.py +76 -14
  13. package/pineforge_codegen/codegen/helpers.py +12 -2
  14. package/pineforge_codegen/codegen/host_members.py +8 -0
  15. package/pineforge_codegen/codegen/input.py +36 -3
  16. package/pineforge_codegen/codegen/security.py +56 -1
  17. package/pineforge_codegen/codegen/ta.py +65 -1
  18. package/pineforge_codegen/codegen/types.py +136 -8
  19. package/pineforge_codegen/codegen/visit_call.py +92 -3
  20. package/pineforge_codegen/codegen/visit_expr.py +202 -3
  21. package/pineforge_codegen/codegen/visit_stmt.py +19 -4
  22. package/pineforge_codegen/collection_history.py +1595 -0
  23. package/pineforge_codegen/external_requests.py +3 -0
  24. package/pineforge_codegen/parser.py +19 -4
  25. package/pineforge_codegen/request_discovery.py +506 -0
  26. package/pineforge_codegen/support_checker.py +37 -0
  27. package/pineforge_codegen-1.1.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +1 -1
  30. package/transpile.worker.mjs +2 -1
  31. 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
- cpp_t = self._infer_type(node.object)
1878
- if cpp_t in ("double", "int", "int64_t", "bool"):
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 None
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=self._drawing_target_cpp_type(
1150
- target_name,
1151
- None,
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()))