@pineforge/codegen-pyodide 0.10.2 → 0.10.4

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.4",
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",
@@ -5973,7 +5973,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
5973
5973
  callee=MemberAccess(object=Identifier(name="ta"), member=node.member),
5974
5974
  args=[], kwargs={},
5975
5975
  )
5976
- return self._handle_ta_call(node.member, synthetic_call)
5976
+ before = len(self._ta_call_sites)
5977
+ result = self._handle_ta_call(node.member, synthetic_call)
5978
+ # The site belongs to THIS read: key it on the AST node the
5979
+ # codegen will meet (the MemberAccess), not on the synthetic
5980
+ # call nobody else holds. Without this a bare ``ta.vwap``
5981
+ # inside request.security() could not find its own site and
5982
+ # was lowered to the first live CHART member (advanced by the
5983
+ # chart bar AND every requested sub-bar, read through
5984
+ # history_advances_new_bar()); a second top-level read bound
5985
+ # to the first read's member as well.
5986
+ if len(self._ta_call_sites) > before:
5987
+ self._ta_call_sites[-1].node = node
5988
+ return result
5977
5989
  return PineType.FLOAT
5978
5990
 
5979
5991
  # math.* properties
@@ -437,9 +437,15 @@ class CallHandlers:
437
437
  all_args = [default_arg]
438
438
 
439
439
  if func_name == "vwap" and not all_args:
440
- default_src = Identifier(name="close")
440
+ # The bare ``ta.vwap`` property is the VWAP of hlc3 (Pine v6
441
+ # reference: "It uses hlc3 as its source series"); TradingView's
442
+ # own read equals hlc3 on a daily bar (lab tv
443
+ # notrade-session-vwap-f1d, NYSE:F 1D 2025-07-01..08-31, 42/42,
444
+ # 2026-09-05), where the engine used to read the bar's close.
445
+ default_src = Identifier(name="hlc3")
441
446
  self._visit(default_src)
442
- self._series_bar_fields.add("close")
447
+ for field in ("high", "low", "close"):
448
+ self._series_bar_fields.add(field)
443
449
  all_args = [default_src]
444
450
 
445
451
  # Handle ta.highest(length) / ta.lowest(length) with 1 arg:
@@ -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.
@@ -1906,7 +1914,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1906
1914
  self._collection_types[name] = spec
1907
1915
  elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
1908
1916
  self._array_vars.add(name)
1909
- spec = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
1917
+ spec = self._widen_array_spec_for_name(
1918
+ name,
1919
+ self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
1920
+ )
1910
1921
  self._collection_types[name] = spec
1911
1922
  elif ns == "map" and fn == "new":
1912
1923
  self._map_vars.add(name)
@@ -1926,7 +1937,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1926
1937
  fn2, ns2 = self._resolve_callee(expr.callee)
1927
1938
  if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
1928
1939
  self._array_vars.add(name)
1929
- spec2 = self._type_spec_from_expr(expr) or self._array_spec_for_name(name)
1940
+ spec2 = self._widen_array_spec_for_name(
1941
+ name,
1942
+ self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
1943
+ )
1930
1944
  self._collection_types[name] = spec2
1931
1945
  continue
1932
1946
  if name in self._matrix_specs:
@@ -2110,6 +2124,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2110
2124
  return True
2111
2125
  return False
2112
2126
 
2127
+ def _detect_pine_time_usage(self) -> bool:
2128
+ """True if emitted C++ calls the engine's pine_time()/pine_time_close().
2129
+
2130
+ Gates the PF_PINE_TIME_SESSION_DAY_ARGS prelude shim (emit_top.py) so
2131
+ scripts that never call ``time(tf[, session[, tz]])`` /
2132
+ ``time_close(...)`` stay byte-identical. A ``var`` initializer is
2133
+ also text-scanned, mirroring ``_detect_matrix_usage``.
2134
+ """
2135
+ pat = re.compile(r"(?<![\w.])time(?:_close)?\s*\(")
2136
+ for _, _, init_str in self.ctx.var_members:
2137
+ if init_str and pat.search(str(init_str)):
2138
+ return True
2139
+ for node in self._walk_ast(self.ctx.ast):
2140
+ if isinstance(node, FuncCall) and (node.args or node.kwargs):
2141
+ fn, ns = self._resolve_callee(node.callee)
2142
+ if ns is None and fn in ("time", "time_close"):
2143
+ return True
2144
+ return False
2145
+
2146
+ def _detect_vwap_usage(self) -> bool:
2147
+ """True if emitted C++ calls ta::VWAP / ta::VWAPBands compute().
2148
+
2149
+ Gates the PF_VWAP_SESSION_ANCHOR_ARGS prelude shim (emit_top.py):
2150
+ ``ta.vwap(...)`` calls, the bare ``ta.vwap`` property read, and
2151
+ ``var`` initializers are covered.
2152
+ """
2153
+ for _, _, init_str in self.ctx.var_members:
2154
+ if init_str and "ta.vwap" in str(init_str):
2155
+ return True
2156
+ for node in self._walk_ast(self.ctx.ast):
2157
+ if isinstance(node, FuncCall):
2158
+ fn, ns = self._resolve_callee(node.callee)
2159
+ if ns == "ta" and fn == "vwap":
2160
+ return True
2161
+ elif isinstance(node, MemberAccess) and node.member == "vwap":
2162
+ if getattr(node.object, "name", None) == "ta":
2163
+ return True
2164
+ return False
2165
+
2113
2166
  def _detect_generic_matrix_usage(self) -> bool:
2114
2167
  """True if emitted C++ mentions a non-float matrix specialization."""
2115
2168
  float_spec = TypeSpec.primitive("float")
@@ -3760,7 +3813,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3760
3813
  # request.security helper-call results read at a history offset
3761
3814
  # (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
3762
3815
  self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
3763
- self._prepare_lazy_saturated_roc3_sites()
3816
+ self._prepare_lazy_source_clock_sites()
3764
3817
 
3765
3818
  lines: list[str] = []
3766
3819
 
@@ -3993,11 +4046,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3993
4046
  )
3994
4047
  lines.append("")
3995
4048
 
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)
4049
+ # Hold-last source clocks (change/mom/roc below a top-level lazy edge)
4050
+ # are generated support types, not script state themselves. Their
4051
+ # per-callsite instances are declared below inside GeneratedStrategy
4052
+ # and therefore join the automatic COOF checkpoint inventory.
4053
+ self._emit_lazy_source_clock_helper(lines)
4001
4054
 
4002
4055
  # 2. Open class
4003
4056
  lines.append("class GeneratedStrategy : public BacktestEngine {")
@@ -4153,20 +4206,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4153
4206
  lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
4154
4207
  lines.append(" bool _use_precalc = false;")
4155
4208
 
4156
- for clock_name in self._lazy_saturated_roc3_clock_by_node.values():
4209
+ for info in self._lazy_source_clock_by_node.values():
4157
4210
  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.
4166
- lines.append(
4167
- " Series<double> "
4168
- f"{self._lazy_saturated_roc3_history_name}{{4}};"
4211
+ f" {self._lazy_source_clock_type_name} {info['clock']};"
4169
4212
  )
4213
+ # Held-source history on the chart clock. A literal length sizes
4214
+ # it exactly (independent of the user's max_bars_back directive,
4215
+ # which may legitimately be smaller than this generated route
4216
+ # needs); a runtime length uses the Series default capacity. Both
4217
+ # are ordinary copyable script state and join the automatic COOF
4218
+ # checkpoint below.
4219
+ literal = info["length_literal"]
4220
+ capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
4221
+ lines.append(f" Series<double> {info['hist']}{capacity};")
4222
+ if info["chart"] is not None:
4223
+ lines.append(f" Series<double> {info['chart']}{capacity};")
4170
4224
 
4171
4225
  # Security evaluator TA members (cloned from expression dependencies)
4172
4226
  # Skip for user function call expressions — their TA deps are internal to the function
@@ -4227,7 +4281,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4227
4281
  )
4228
4282
  ):
4229
4283
  self._array_vars.add(name)
4230
- array_spec = exact_member_spec or self._array_spec_for_name(name)
4284
+ array_spec = self._widen_array_spec_for_name(
4285
+ name, exact_member_spec or self._array_spec_for_name(name)
4286
+ )
4231
4287
  lines.append(
4232
4288
  f" {self._type_spec_to_cpp(array_spec)} {safe};"
4233
4289
  )
@@ -89,6 +89,7 @@ from ..analyzer import FuncInfo
89
89
  from ..symbols import PineType, method_receiver_cpp_token
90
90
  from .tables import (
91
91
  BAR_SERIES_PUSH,
92
+ TA_CHART_PREV_CLOSE_ARG,
92
93
  DRAWING_TYPE_TO_CPP,
93
94
  PINE_TYPE_TO_CPP,
94
95
  RUNTIME_REGISTER_SECURITY_EVAL_FN,
@@ -136,6 +137,28 @@ class TopLevelEmitter:
136
137
  lines.append('#include <pineforge/log.hpp>')
137
138
  lines.append('#include <pineforge/str_utils.hpp>')
138
139
  lines.append('#include <pineforge/session_time.hpp>')
140
+ # Feature-guarded argument tails (lab experiment cells build the
141
+ # candidate codegen against the BASE engine too): the symbol clock
142
+ # (syminfo tz + session) reaches time()/time_close() and ta.vwap only
143
+ # when the engine header advertises the overloads; otherwise the
144
+ # macros expand to nothing and the calls collapse to the legacy
145
+ # 5-arg pine_time / 3-arg VWAP::compute shapes (session filter in
146
+ # UTC, UTC-day VWAP anchor — the honest "factor absent" behaviour).
147
+ # Gated on the pre-scans so scripts without such calls stay
148
+ # byte-identical. The expansion carries no extra parentheses so the
149
+ # preprocessed call is byte-identical to the plain 7-arg form.
150
+ if getattr(self, "_uses_pine_time", False):
151
+ lines.append("#ifdef PF_PINE_TIME_HAS_SESSION_DAY")
152
+ lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess) , tz, sess")
153
+ lines.append("#else")
154
+ lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess)")
155
+ lines.append("#endif")
156
+ if getattr(self, "_uses_vwap_anchor", False):
157
+ lines.append("#ifdef PF_VWAP_HAS_SESSION_ANCHOR")
158
+ lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess) , tz, sess")
159
+ lines.append("#else")
160
+ lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess)")
161
+ lines.append("#endif")
139
162
  if self._uses_matrix:
140
163
  # Unconditional include when matrix API is used — do not gate on
141
164
  # __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
@@ -912,28 +935,38 @@ class TopLevelEmitter:
912
935
 
913
936
  # A GeneratedStrategy handle may execute multiple batch runs or
914
937
  # 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:
938
+ # generated members survive. Reset the hold-last source clocks
939
+ # (change/mom/roc below a top-level lazy edge) and their held-source
940
+ # Series at the first genuine slot of each lifecycle, then freeze this
941
+ # bar's base and record it once per chart bar. This lives in on_bar
942
+ # rather than a generated run wrapper because stream_begin() enters the
943
+ # base run path directly. COOF post-close recalculations have
944
+ # history_advances_new_bar()==false and therefore keep the same base.
945
+ if self._lazy_source_clock_by_node:
922
946
  lines.append(
923
947
  " if (history_advances_new_bar() && bar_index_ == 0) {"
924
948
  )
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
- )
949
+ for info in self._lazy_source_clock_by_node.values():
950
+ lines.append(f" {info['clock']}.reset();")
951
+ lines.append(f" {info['hist']}.clear();")
952
+ if info["chart"] is not None:
953
+ lines.append(f" {info['chart']}.clear();")
930
954
  lines.append(" }")
931
- self._emit_history_series_write(
932
- lines,
933
- " ",
934
- self._lazy_saturated_roc3_history_name,
935
- "current_bar_.close",
936
- )
955
+ for info in self._lazy_source_clock_by_node.values():
956
+ lines.append(f" {info['clock']}.begin_bar(bar_index_);")
957
+ self._emit_history_series_write(
958
+ lines,
959
+ " ",
960
+ info["hist"],
961
+ f"{info['clock']}.bar_base_source",
962
+ )
963
+ if info["chart"] is not None:
964
+ self._emit_history_series_write(
965
+ lines,
966
+ " ",
967
+ info["chart"],
968
+ self._visit_expr(info["chart_source"]),
969
+ )
937
970
 
938
971
  # reset_run_state() owns engine/broker state, while these generated
939
972
  # Series members belong to the strategy object. Clear all of them on
@@ -1003,6 +1036,11 @@ class TopLevelEmitter:
1003
1036
  # declaration statements, not in the global on_bar preamble.
1004
1037
  if name in getattr(self, "_func_local_var_names", ()):
1005
1038
  continue
1039
+ # A wide int array member (``std::vector<int64_t>``, see
1040
+ # ``_wide_int_array_names``) needs its first-bar constructor
1041
+ # spelled with the same element type: name the target for
1042
+ # visit_call's ``array.new_*`` / ``array.from`` lowering.
1043
+ self._array_ctor_target_name = name
1006
1044
  safe = self._safe_name(name)
1007
1045
  runtime_info = self._runtime_scalar_var_init_by_member.get(name)
1008
1046
  if (runtime_info is not None
@@ -1099,6 +1137,7 @@ class TopLevelEmitter:
1099
1137
  if cloned not in init_emitted:
1100
1138
  init_emitted.add(cloned)
1101
1139
  lines.append(f" {cloned}.push({cpp_val});")
1140
+ self._array_ctor_target_name = None
1102
1141
  lines.append(" _var_initialized = true;")
1103
1142
  lines.append(" } else {")
1104
1143
  for name, _, _ in self.ctx.var_members:
@@ -1196,9 +1235,15 @@ class TopLevelEmitter:
1196
1235
  # an input (the default-sized construction already matches Pine).
1197
1236
  self._emit_ta_runtime_reset(lines, indent=2)
1198
1237
 
1199
- # d. Visit each statement
1238
+ # d. Visit each statement. A stateful ``ta.*`` site below a lazy
1239
+ # ``and``/``or`` RHS or ternary arm of a top-level statement is
1240
+ # evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
1200
1241
  for stmt in self.ctx.ast.body:
1201
- self._visit_stmt(stmt, lines, indent=2)
1242
+ self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
1243
+ try:
1244
+ self._visit_stmt(stmt, lines, indent=2)
1245
+ finally:
1246
+ self._clear_lazy_edge_ta_hoists()
1202
1247
 
1203
1248
  # e. ``// @pf-trace`` pragma block — emitted last so trace values
1204
1249
  # reflect every assignment / strategy call made earlier in the
@@ -1938,6 +1983,14 @@ class TopLevelEmitter:
1938
1983
  if self._ta_site_uses_precalc(site):
1939
1984
  compute_args = self._ta_compute_args_for_site(site)
1940
1985
  compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
1986
+ # issue #178: the precalc pre-pass walks the bar array
1987
+ # itself, so the previous CHART close is bars[i - 1]
1988
+ # (the engine's prev_chart_close() tracker only advances
1989
+ # at on_bar dispatch).
1990
+ compute_args_bars = compute_args_bars.replace(
1991
+ TA_CHART_PREV_CLOSE_ARG,
1992
+ "(i > 0 ? bars[i - 1].close : na<double>())",
1993
+ )
1941
1994
  lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
1942
1995
  finally:
1943
1996
  self._precalc_loop_active = False