@pineforge/codegen-pyodide 0.8.0 → 0.9.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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -28,7 +28,9 @@ from __future__ import annotations
|
|
|
28
28
|
from typing import TYPE_CHECKING
|
|
29
29
|
|
|
30
30
|
from ..ast_nodes import (
|
|
31
|
-
Assignment, BinOp,
|
|
31
|
+
Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall,
|
|
32
|
+
Identifier, MemberAccess, NaLiteral, NumberLiteral, StringLiteral,
|
|
33
|
+
Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
32
34
|
)
|
|
33
35
|
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
|
|
34
36
|
|
|
@@ -221,6 +223,90 @@ class TaSiteHelper:
|
|
|
221
223
|
|
|
222
224
|
return ""
|
|
223
225
|
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
# Precalculation safety
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
_PRECALC_BAR_IDENTIFIERS = {
|
|
231
|
+
"open", "high", "low", "close", "volume",
|
|
232
|
+
"hl2", "hlc3", "ohlc4", "hlcc4",
|
|
233
|
+
"time", "time_close", "bar_index",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def _is_precalc_replayed_source_var(self, name: str) -> bool:
|
|
237
|
+
"""True for top-level ``x = input.source(...)`` variables replayed in
|
|
238
|
+
``precalculate()``.
|
|
239
|
+
|
|
240
|
+
The precompute loop explicitly advances native source series and then
|
|
241
|
+
replays those source-input assignments before computing static TA
|
|
242
|
+
sites. Other user aliases, even when they are statically derived from
|
|
243
|
+
bar data (``src = close`` / ``ha_close = close``), are not replayed
|
|
244
|
+
there and must therefore use the normal per-bar TA path."""
|
|
245
|
+
ast = getattr(self.ctx, "ast", None)
|
|
246
|
+
for stmt in getattr(ast, "body", ()):
|
|
247
|
+
if (
|
|
248
|
+
isinstance(stmt, VarDecl)
|
|
249
|
+
and stmt.name == name
|
|
250
|
+
and isinstance(stmt.value, FuncCall)
|
|
251
|
+
and self._is_source_input(stmt.value)
|
|
252
|
+
):
|
|
253
|
+
return True
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _expr_safe_for_ta_precalc(self, expr) -> bool:
|
|
257
|
+
if expr is None:
|
|
258
|
+
return True
|
|
259
|
+
if isinstance(expr, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral)):
|
|
260
|
+
return True
|
|
261
|
+
if isinstance(expr, Identifier):
|
|
262
|
+
if expr.name in self._PRECALC_BAR_IDENTIFIERS:
|
|
263
|
+
return True
|
|
264
|
+
if self._is_precalc_replayed_source_var(expr.name):
|
|
265
|
+
return True
|
|
266
|
+
if expr.name in getattr(self.ctx, "series_vars", set()):
|
|
267
|
+
return False
|
|
268
|
+
return expr.name in getattr(self, "_static_vars", set())
|
|
269
|
+
if isinstance(expr, MemberAccess):
|
|
270
|
+
if isinstance(expr.object, Identifier) and (
|
|
271
|
+
expr.object.name.startswith("input") or expr.object.name in getattr(self, "_enum_defs", {})
|
|
272
|
+
):
|
|
273
|
+
return True
|
|
274
|
+
return self._expr_safe_for_ta_precalc(expr.object)
|
|
275
|
+
if isinstance(expr, BinOp):
|
|
276
|
+
return self._expr_safe_for_ta_precalc(expr.left) and self._expr_safe_for_ta_precalc(expr.right)
|
|
277
|
+
if isinstance(expr, UnaryOp):
|
|
278
|
+
return self._expr_safe_for_ta_precalc(expr.operand)
|
|
279
|
+
if isinstance(expr, Ternary):
|
|
280
|
+
return (
|
|
281
|
+
self._expr_safe_for_ta_precalc(expr.condition)
|
|
282
|
+
and self._expr_safe_for_ta_precalc(expr.true_val)
|
|
283
|
+
and self._expr_safe_for_ta_precalc(expr.false_val)
|
|
284
|
+
)
|
|
285
|
+
if isinstance(expr, Subscript):
|
|
286
|
+
return self._expr_safe_for_ta_precalc(expr.object) and self._expr_safe_for_ta_precalc(expr.index)
|
|
287
|
+
if isinstance(expr, TupleLiteral):
|
|
288
|
+
return all(self._expr_safe_for_ta_precalc(elem) for elem in expr.elements)
|
|
289
|
+
if isinstance(expr, FuncCall):
|
|
290
|
+
if isinstance(expr.callee, MemberAccess) and isinstance(expr.callee.object, Identifier):
|
|
291
|
+
if expr.callee.object.name in ("math", "str", "color"):
|
|
292
|
+
return all(self._expr_safe_for_ta_precalc(arg) for arg in expr.args)
|
|
293
|
+
return False
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def _ta_site_uses_precalc(self, site: "TACallSite") -> bool:
|
|
297
|
+
"""Whether a static TA site can safely read from ``_precalc_*``.
|
|
298
|
+
|
|
299
|
+
Static-ness from the analyzer means the expression can be represented
|
|
300
|
+
from bar data and constants, but the standalone precompute loop only
|
|
301
|
+
replays a narrow subset of per-bar assignments. A user alias such as
|
|
302
|
+
``ha_close = close`` is static in that analyzer sense, yet its Series is
|
|
303
|
+
empty during precompute, so ``ta.stdev(ha_close, 20)`` precalculates as
|
|
304
|
+
all-``na``. Opting that site out preserves correctness; it simply uses
|
|
305
|
+
the ordinary stateful TA object during ``on_bar``."""
|
|
306
|
+
if not getattr(site, "is_static", False):
|
|
307
|
+
return False
|
|
308
|
+
return all(self._expr_safe_for_ta_precalc(arg) for arg in site.compute_args)
|
|
309
|
+
|
|
224
310
|
def _security_ta_compute_args_for_site(
|
|
225
311
|
self,
|
|
226
312
|
sec_id: int,
|
|
@@ -55,7 +55,7 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
|
|
|
55
55
|
"""
|
|
56
56
|
return (
|
|
57
57
|
"[&]() -> int { "
|
|
58
|
-
f"std::string _tz = ({tz_arg}); "
|
|
58
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz_arg})); "
|
|
59
59
|
f"time_t _secs = (time_t)(({ts_arg}) / 1000); "
|
|
60
60
|
"struct tm tm_buf; "
|
|
61
61
|
"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") { "
|
|
@@ -76,11 +76,11 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
BAR_BUILTINS = {
|
|
79
|
-
"bar_index": "
|
|
79
|
+
"bar_index": "pine_bar_index()",
|
|
80
80
|
"time": "current_bar_.timestamp",
|
|
81
81
|
"time_close": "time_close()",
|
|
82
82
|
"timenow": "current_bar_.timestamp",
|
|
83
|
-
"last_bar_index": "
|
|
83
|
+
"last_bar_index": "pine_last_bar_index()",
|
|
84
84
|
"last_bar_time": "last_bar_time_",
|
|
85
85
|
# time_tradingday: Unix-ms of the session-open of the trading day that
|
|
86
86
|
# contains the current bar. Backed by pine_time_tradingday() in the engine.
|
|
@@ -96,10 +96,12 @@ BAR_BUILTINS = {
|
|
|
96
96
|
# ``_bar_hour()`` helpers. For UTC-exchange data (the crypto corpus,
|
|
97
97
|
# SymInfo's constructor default) the lambda takes the gmtime_r fast
|
|
98
98
|
# path and is value-identical to the old emission.
|
|
99
|
+
# Route through the engine's cached pine_hour/pine_minute/... (session_time.hpp)
|
|
100
|
+
# instead of an inline per-call setenv+tzset lambda — the lambda's TZ churn
|
|
101
|
+
# caused a macOS tzset()->notifyd IPC storm (KI-35). The engine helpers are
|
|
102
|
+
# value-identical (same tm fields + Pine offsets) but tzset-churn-free.
|
|
99
103
|
**{
|
|
100
|
-
name:
|
|
101
|
-
TIME_FIELD_EXPRS[name], "current_bar_.timestamp", "syminfo_.timezone"
|
|
102
|
-
)
|
|
104
|
+
name: f"pine_{name}(current_bar_.timestamp, syminfo_.timezone)"
|
|
103
105
|
for name in ("hour", "minute", "second", "dayofmonth", "dayofweek",
|
|
104
106
|
"month", "year", "weekofyear")
|
|
105
107
|
},
|
|
@@ -113,7 +115,27 @@ BAR_SERIES_PUSH = {
|
|
|
113
115
|
"ohlc4": "((current_bar_.open + current_bar_.high + current_bar_.low + current_bar_.close) / 4.0)",
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
#
|
|
118
|
+
# Bar identifiers that refer to the *security* (HTF) bar inside
|
|
119
|
+
# ``request.security()``. ``time`` is the HTF bar-open timestamp.
|
|
120
|
+
SECURITY_BAR_FIELD_EXPRS = {
|
|
121
|
+
"open": "bar.open",
|
|
122
|
+
"high": "bar.high",
|
|
123
|
+
"low": "bar.low",
|
|
124
|
+
"close": "bar.close",
|
|
125
|
+
"volume": "bar.volume",
|
|
126
|
+
"time": "bar.timestamp",
|
|
127
|
+
}
|
|
128
|
+
SECURITY_BAR_FIELD_TYPES = {
|
|
129
|
+
"open": "double",
|
|
130
|
+
"high": "double",
|
|
131
|
+
"low": "double",
|
|
132
|
+
"close": "double",
|
|
133
|
+
"volume": "double",
|
|
134
|
+
"time": "int64_t",
|
|
135
|
+
}
|
|
136
|
+
SECURITY_BAR_FIELDS = frozenset(SECURITY_BAR_FIELD_EXPRS)
|
|
137
|
+
|
|
138
|
+
# Backwards-compatible name for consumers that only need the OHLCV subset.
|
|
117
139
|
SECURITY_OHLC_BAR_FIELDS = frozenset({"open", "high", "low", "close", "volume"})
|
|
118
140
|
|
|
119
141
|
# Generated C++ runtime function names referenced by the codegen as string
|
|
@@ -255,7 +277,12 @@ TA_TUPLE_FIELDS = {
|
|
|
255
277
|
# must be promoted to ``int64_t`` so the ``na`` sentinel (``INT64_MIN``)
|
|
256
278
|
# survives — narrowing to 32-bit ``int`` collapses it to ``0`` and breaks
|
|
257
279
|
# ``is_na<int>`` detection.
|
|
258
|
-
INT64_BUILTINS = {"time", "time_close", "timestamp", "time_tradingday"}
|
|
280
|
+
INT64_BUILTINS = {"time", "time_close", "timenow", "timestamp", "time_tradingday"}
|
|
281
|
+
|
|
282
|
+
# Subset of INT64_BUILTINS spelled as a bare identifier (no call args) in Pine.
|
|
283
|
+
# ``entryTime := time`` parses the RHS as an ``Identifier`` named ``time`` (not a
|
|
284
|
+
# ``FuncCall``), so int64 promotion must match these by identifier name too.
|
|
285
|
+
INT64_BUILTIN_IDENTIFIERS = {"time", "time_close", "timenow"}
|
|
259
286
|
|
|
260
287
|
PINE_TYPE_TO_CPP = {
|
|
261
288
|
"int": "int", "float": "double", "bool": "bool", "string": "std::string",
|
|
@@ -263,15 +290,66 @@ PINE_TYPE_TO_CPP = {
|
|
|
263
290
|
PineType.STRING: "std::string", PineType.NA: "double",
|
|
264
291
|
PineType.UNKNOWN: "double", PineType.VOID: "double",
|
|
265
292
|
PineType.COLOR: "int",
|
|
293
|
+
# Drawing-objects-as-data: the value-view handle structs (see
|
|
294
|
+
# drawing.hpp). Explicit-hint decls (``var line x``), UDT-method drawing
|
|
295
|
+
# params, and ``_type_for_decl`` resolve through here.
|
|
296
|
+
"line": "Line", "box": "Box", "label": "Label",
|
|
297
|
+
"linefill": "Linefill", "chart.point": "ChartPoint",
|
|
266
298
|
}
|
|
267
299
|
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# Drawing-objects-as-data (spec §4.1)
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Pine drawing type name -> C++ handle struct (pineforge/drawing.hpp).
|
|
304
|
+
# ``table`` / ``polyline`` are DELIBERATELY absent — they stay 100% no-op
|
|
305
|
+
# (kept in SKIP_NAMESPACES / SKIP_VAR_TYPES / _DRAWING_TYPES_INIT).
|
|
306
|
+
DRAWING_TYPE_TO_CPP = {
|
|
307
|
+
"line": "Line", "box": "Box", "label": "Label",
|
|
308
|
+
"linefill": "Linefill", "chart.point": "ChartPoint",
|
|
309
|
+
}
|
|
310
|
+
# Function-call namespaces routed to the per-type arena dispatch.
|
|
311
|
+
DRAWING_NS = {"line", "box", "label", "linefill"}
|
|
312
|
+
# Constant member reads (``line.style_solid`` -> "0"). chart.point/linefill
|
|
313
|
+
# carry no style constants.
|
|
314
|
+
DRAWING_STYLE_NS = {"line", "box", "label"}
|
|
315
|
+
# Per-type arena member name on the generated strategy.
|
|
316
|
+
DRAWING_ARENA = {
|
|
317
|
+
"line": "_pf_lines_", "box": "_pf_boxes_",
|
|
318
|
+
"label": "_pf_labels_", "linefill": "_pf_linefills_",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Pine typed array constructors for drawing handles — ``array.new_line(...)``
|
|
322
|
+
# is the typed alias of ``array.new<line>(...)`` (and likewise for box/label/
|
|
323
|
+
# linefill). Maps the constructor name to the drawing element type name, so the
|
|
324
|
+
# element TypeSpec resolves to the C++ handle struct (Line/Box/Label/Linefill).
|
|
325
|
+
ARRAY_DRAWING_NEW_CTORS = {
|
|
326
|
+
"new_line": "line", "new_box": "box",
|
|
327
|
+
"new_label": "label", "new_linefill": "linefill",
|
|
328
|
+
}
|
|
329
|
+
# Pine array methods whose C++ lowering is void (or a non-scalar iterator) and
|
|
330
|
+
# whose Pine semantics is mutate-in-place: a function whose body ends in one of
|
|
331
|
+
# these returns void, so the codegen must emit it as a statement + default
|
|
332
|
+
# return rather than ``return arr.clear();`` / ``return arr.insert(...);``.
|
|
333
|
+
ARRAY_VOID_METHODS = frozenset({
|
|
334
|
+
"push", "unshift", "insert", "clear", "set", "fill",
|
|
335
|
+
"sort", "reverse", "concat",
|
|
336
|
+
})
|
|
337
|
+
# All recognised ``array.new_*`` scalar+drawing constructor names (``new`` itself
|
|
338
|
+
# is template-form and handled separately).
|
|
339
|
+
ARRAY_NEW_CTORS = frozenset(
|
|
340
|
+
{"new_float", "new_int", "new_bool", "new_string"} | set(ARRAY_DRAWING_NEW_CTORS)
|
|
341
|
+
)
|
|
342
|
+
|
|
268
343
|
SKIP_FUNC_NAMES = {
|
|
269
344
|
"plot", "plotshape", "plotchar", "plotcandle", "plotbar", "plotarrow",
|
|
270
345
|
"fill", "hline", "barcolor", "bgcolor", "alert", "alertcondition",
|
|
271
346
|
}
|
|
347
|
+
# line / box / label / linefill REMOVED — they now route to real drawing
|
|
348
|
+
# codegen (gated on _uses_drawing). table / polyline / chart / display / size
|
|
349
|
+
# / position stay no-op.
|
|
272
350
|
SKIP_NAMESPACES = {
|
|
273
|
-
"table", "
|
|
274
|
-
"
|
|
351
|
+
"table", "polyline", "chart",
|
|
352
|
+
"display", "size", "position",
|
|
275
353
|
}
|
|
276
354
|
SKIP_VAR_TYPES = {"table"}
|
|
277
355
|
|
|
@@ -578,6 +656,33 @@ MATRIX_METHODS = {
|
|
|
578
656
|
# Math / String dispatch
|
|
579
657
|
# ---------------------------------------------------------------------------
|
|
580
658
|
|
|
659
|
+
def _math_minmax_na_expr(func_name: str, args: list[str]) -> str:
|
|
660
|
+
"""Emit Pine-compatible math.min/math.max with na propagation.
|
|
661
|
+
|
|
662
|
+
``std::min``/``std::max`` do not propagate NaN consistently because their
|
|
663
|
+
comparison is specified in terms of ``operator<``. Pine math.min/max return
|
|
664
|
+
``na`` when any operand is ``na``, so generated clamp-style expressions
|
|
665
|
+
like ``math.max(-1, math.min(1, na))`` must stay ``na``.
|
|
666
|
+
"""
|
|
667
|
+
if not args:
|
|
668
|
+
return "na<double>()"
|
|
669
|
+
if len(args) == 1:
|
|
670
|
+
return f"(double)({args[0]})"
|
|
671
|
+
op = "std::max" if func_name == "max" else "std::min"
|
|
672
|
+
decls = " ".join(
|
|
673
|
+
f"double _v{i} = (double)({arg});" for i, arg in enumerate(args)
|
|
674
|
+
)
|
|
675
|
+
guard = " || ".join(f"is_na(_v{i})" for i in range(len(args)))
|
|
676
|
+
updates = " ".join(
|
|
677
|
+
f"_out = {op}(_out, _v{i});" for i in range(1, len(args))
|
|
678
|
+
)
|
|
679
|
+
return (
|
|
680
|
+
f"([&]() -> double {{ {decls} "
|
|
681
|
+
f"if ({guard}) return na<double>(); "
|
|
682
|
+
f"double _out = _v0; {updates} return _out; }}())"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
581
686
|
MATH_FUNC_MAP = {
|
|
582
687
|
"abs": "std::abs", "max": "std::max", "min": "std::min",
|
|
583
688
|
"ceil": "std::ceil", "floor": "std::floor", "round": "std::round",
|