@pineforge/codegen-pyodide 0.10.2 → 0.10.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -5,6 +5,7 @@ results from AnalyzerContext instead of walking the AST to collect info.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
8
+ import re
8
9
 
9
10
  from dataclasses import dataclass
10
11
 
@@ -309,6 +310,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
309
310
  # Set of var/series member names that belong to user functions (need cloning)
310
311
  self._func_var_members_set: set[str] = set()
311
312
  self._precalc_loop_active: bool = False
313
+ # Top-level lazy-edge TA sites hoisted to every-bar evaluation for the
314
+ # statement currently being lowered: FuncCall id -> local name, and
315
+ # Subscript id -> ``_hist_call_*`` member (see ``ta.py``).
316
+ self._hoisted_ta_values: dict[int, str] = {}
317
+ self._hoisted_hist_reads: dict[int, str] = {}
312
318
  # Names of ``var`` members that live in a callable scope (not global).
313
319
  # Their exact declaration statements own initialization; they must not
314
320
  # be initialized by the constructor or the global on_bar preamble.
@@ -934,6 +940,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
934
940
  self._register_global_aggregate_member_types()
935
941
  self._uses_map = self._detect_map_usage()
936
942
  self._uses_matrix = self._detect_matrix_usage()
943
+ self._uses_pine_time = self._detect_pine_time_usage()
944
+ self._uses_vwap_anchor = self._detect_vwap_usage()
937
945
  # Drawing-objects-as-data: gate all new emission (drawing.hpp include +
938
946
  # the per-type arenas) on this flag so non-drawing strategies stay
939
947
  # byte-identical. Caps come from the strategy() header max_*_count.
@@ -2110,6 +2118,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2110
2118
  return True
2111
2119
  return False
2112
2120
 
2121
+ def _detect_pine_time_usage(self) -> bool:
2122
+ """True if emitted C++ calls the engine's pine_time()/pine_time_close().
2123
+
2124
+ Gates the PF_PINE_TIME_SESSION_DAY_ARGS prelude shim (emit_top.py) so
2125
+ scripts that never call ``time(tf[, session[, tz]])`` /
2126
+ ``time_close(...)`` stay byte-identical. A ``var`` initializer is
2127
+ also text-scanned, mirroring ``_detect_matrix_usage``.
2128
+ """
2129
+ pat = re.compile(r"(?<![\w.])time(?:_close)?\s*\(")
2130
+ for _, _, init_str in self.ctx.var_members:
2131
+ if init_str and pat.search(str(init_str)):
2132
+ return True
2133
+ for node in self._walk_ast(self.ctx.ast):
2134
+ if isinstance(node, FuncCall) and (node.args or node.kwargs):
2135
+ fn, ns = self._resolve_callee(node.callee)
2136
+ if ns is None and fn in ("time", "time_close"):
2137
+ return True
2138
+ return False
2139
+
2140
+ def _detect_vwap_usage(self) -> bool:
2141
+ """True if emitted C++ calls ta::VWAP / ta::VWAPBands compute().
2142
+
2143
+ Gates the PF_VWAP_SESSION_ANCHOR_ARGS prelude shim (emit_top.py):
2144
+ ``ta.vwap(...)`` calls, the bare ``ta.vwap`` property read, and
2145
+ ``var`` initializers are covered.
2146
+ """
2147
+ for _, _, init_str in self.ctx.var_members:
2148
+ if init_str and "ta.vwap" in str(init_str):
2149
+ return True
2150
+ for node in self._walk_ast(self.ctx.ast):
2151
+ if isinstance(node, FuncCall):
2152
+ fn, ns = self._resolve_callee(node.callee)
2153
+ if ns == "ta" and fn == "vwap":
2154
+ return True
2155
+ elif isinstance(node, MemberAccess) and node.member == "vwap":
2156
+ if getattr(node.object, "name", None) == "ta":
2157
+ return True
2158
+ return False
2159
+
2113
2160
  def _detect_generic_matrix_usage(self) -> bool:
2114
2161
  """True if emitted C++ mentions a non-float matrix specialization."""
2115
2162
  float_spec = TypeSpec.primitive("float")
@@ -3760,7 +3807,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3760
3807
  # request.security helper-call results read at a history offset
3761
3808
  # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
3762
3809
  self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
3763
- self._prepare_lazy_saturated_roc3_sites()
3810
+ self._prepare_lazy_source_clock_sites()
3764
3811
 
3765
3812
  lines: list[str] = []
3766
3813
 
@@ -3993,11 +4040,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3993
4040
  )
3994
4041
  lines.append("")
3995
4042
 
3996
- # Source-shaped lazy ROC call clocks are generated support types, not
3997
- # script state themselves. Their per-callsite instances are declared
3998
- # below inside GeneratedStrategy and therefore join the automatic COOF
3999
- # checkpoint inventory.
4000
- self._emit_lazy_saturated_roc3_helper(lines)
4043
+ # Hold-last source clocks (change/mom/roc below a top-level lazy edge)
4044
+ # are generated support types, not script state themselves. Their
4045
+ # per-callsite instances are declared below inside GeneratedStrategy
4046
+ # and therefore join the automatic COOF checkpoint inventory.
4047
+ self._emit_lazy_source_clock_helper(lines)
4001
4048
 
4002
4049
  # 2. Open class
4003
4050
  lines.append("class GeneratedStrategy : public BacktestEngine {")
@@ -4153,20 +4200,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4153
4200
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
4154
4201
  lines.append(" bool _use_precalc = false;")
4155
4202
 
4156
- for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
4157
- lines.append(
4158
- f" {self._lazy_saturated_roc3_type_name} {clock_name};"
4159
- )
4160
- if self._lazy_saturated_roc3_clock_by_node:
4161
- # Dedicated eager close[3] fallback. Its fixed four-slot capacity
4162
- # is independent of the user's max_bars_back directive, which may
4163
- # legitimately be smaller than the offset this generated route
4164
- # requires. It is ordinary copyable script state and therefore
4165
- # joins the automatic COOF checkpoint below.
4203
+ for info in self._lazy_source_clock_by_node.values():
4166
4204
  lines.append(
4167
- " Series<double> "
4168
- f"{self._lazy_saturated_roc3_history_name}{{4}};"
4205
+ f" {self._lazy_source_clock_type_name} {info['clock']};"
4169
4206
  )
4207
+ # Held-source history on the chart clock. A literal length sizes
4208
+ # it exactly (independent of the user's max_bars_back directive,
4209
+ # which may legitimately be smaller than this generated route
4210
+ # needs); a runtime length uses the Series default capacity. Both
4211
+ # are ordinary copyable script state and join the automatic COOF
4212
+ # checkpoint below.
4213
+ literal = info["length_literal"]
4214
+ capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
4215
+ lines.append(f" Series<double> {info['hist']}{capacity};")
4216
+ if info["chart"] is not None:
4217
+ lines.append(f" Series<double> {info['chart']}{capacity};")
4170
4218
 
4171
4219
  # Security evaluator TA members (cloned from expression dependencies)
4172
4220
  # Skip for user function call expressions — their TA deps are internal to the function
@@ -136,6 +136,28 @@ class TopLevelEmitter:
136
136
  lines.append('#include <pineforge/log.hpp>')
137
137
  lines.append('#include <pineforge/str_utils.hpp>')
138
138
  lines.append('#include <pineforge/session_time.hpp>')
139
+ # Feature-guarded argument tails (lab experiment cells build the
140
+ # candidate codegen against the BASE engine too): the symbol clock
141
+ # (syminfo tz + session) reaches time()/time_close() and ta.vwap only
142
+ # when the engine header advertises the overloads; otherwise the
143
+ # macros expand to nothing and the calls collapse to the legacy
144
+ # 5-arg pine_time / 3-arg VWAP::compute shapes (session filter in
145
+ # UTC, UTC-day VWAP anchor — the honest "factor absent" behaviour).
146
+ # Gated on the pre-scans so scripts without such calls stay
147
+ # byte-identical. The expansion carries no extra parentheses so the
148
+ # preprocessed call is byte-identical to the plain 7-arg form.
149
+ if getattr(self, "_uses_pine_time", False):
150
+ lines.append("#ifdef PF_PINE_TIME_HAS_SESSION_DAY")
151
+ lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess) , tz, sess")
152
+ lines.append("#else")
153
+ lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess)")
154
+ lines.append("#endif")
155
+ if getattr(self, "_uses_vwap_anchor", False):
156
+ lines.append("#ifdef PF_VWAP_HAS_SESSION_ANCHOR")
157
+ lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess) , tz, sess")
158
+ lines.append("#else")
159
+ lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess)")
160
+ lines.append("#endif")
139
161
  if self._uses_matrix:
140
162
  # Unconditional include when matrix API is used — do not gate on
141
163
  # __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
@@ -912,28 +934,38 @@ class TopLevelEmitter:
912
934
 
913
935
  # A GeneratedStrategy handle may execute multiple batch runs or
914
936
  # streaming lifecycles. BacktestEngine resets broker/base state, but
915
- # generated members survive. Reset source-shaped lazy ROC clocks and
916
- # their forced eager-fallback close history at the first genuine slot
917
- # of each lifecycle. This lives in on_bar rather than a generated run
918
- # wrapper because stream_begin() enters the base run path directly.
919
- # COOF post-close recalculations have history_advances_new_bar()==false
920
- # and therefore preserve the current committed clock/base.
921
- if self._lazy_saturated_roc3_clock_by_node:
937
+ # generated members survive. Reset the hold-last source clocks
938
+ # (change/mom/roc below a top-level lazy edge) and their held-source
939
+ # Series at the first genuine slot of each lifecycle, then freeze this
940
+ # bar's base and record it once per chart bar. This lives in on_bar
941
+ # rather than a generated run wrapper because stream_begin() enters the
942
+ # base run path directly. COOF post-close recalculations have
943
+ # history_advances_new_bar()==false and therefore keep the same base.
944
+ if self._lazy_source_clock_by_node:
922
945
  lines.append(
923
946
  " if (history_advances_new_bar() && bar_index_ == 0) {"
924
947
  )
925
- for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
926
- lines.append(f" {clock_name}.reset();")
927
- lines.append(
928
- f" {self._lazy_saturated_roc3_history_name}.clear();"
929
- )
948
+ for info in self._lazy_source_clock_by_node.values():
949
+ lines.append(f" {info['clock']}.reset();")
950
+ lines.append(f" {info['hist']}.clear();")
951
+ if info["chart"] is not None:
952
+ lines.append(f" {info['chart']}.clear();")
930
953
  lines.append(" }")
931
- self._emit_history_series_write(
932
- lines,
933
- " ",
934
- self._lazy_saturated_roc3_history_name,
935
- "current_bar_.close",
936
- )
954
+ for info in self._lazy_source_clock_by_node.values():
955
+ lines.append(f" {info['clock']}.begin_bar(bar_index_);")
956
+ self._emit_history_series_write(
957
+ lines,
958
+ " ",
959
+ info["hist"],
960
+ f"{info['clock']}.bar_base_source",
961
+ )
962
+ if info["chart"] is not None:
963
+ self._emit_history_series_write(
964
+ lines,
965
+ " ",
966
+ info["chart"],
967
+ self._visit_expr(info["chart_source"]),
968
+ )
937
969
 
938
970
  # reset_run_state() owns engine/broker state, while these generated
939
971
  # Series members belong to the strategy object. Clear all of them on
@@ -1196,9 +1228,15 @@ class TopLevelEmitter:
1196
1228
  # an input (the default-sized construction already matches Pine).
1197
1229
  self._emit_ta_runtime_reset(lines, indent=2)
1198
1230
 
1199
- # d. Visit each statement
1231
+ # d. Visit each statement. A stateful ``ta.*`` site below a lazy
1232
+ # ``and``/``or`` RHS or ternary arm of a top-level statement is
1233
+ # evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
1200
1234
  for stmt in self.ctx.ast.body:
1201
- self._visit_stmt(stmt, lines, indent=2)
1235
+ self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
1236
+ try:
1237
+ self._visit_stmt(stmt, lines, indent=2)
1238
+ finally:
1239
+ self._clear_lazy_edge_ta_hoists()
1202
1240
 
1203
1241
  # e. ``// @pf-trace`` pragma block — emitted last so trace values
1204
1242
  # reflect every assignment / strategy call made earlier in the