@pineforge/codegen-pyodide 1.0.0 → 1.0.1

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.
@@ -0,0 +1,200 @@
1
+ """The C++ of an array's or a matrix's history (``a[k]``, ``m[k]``).
2
+
3
+ The analyzer annotates every such read with how its value is used and lists
4
+ the variables whose history the script reads (``ctx.collection_history``;
5
+ pineforge_codegen/collection_history.py). Such a variable keeps its own
6
+ ``std::vector`` or matrix member, so every other operation on it keeps its
7
+ C++; beside it, ``_pf_collection_hist_<name>`` keeps the copies its
8
+ executions left. Each execution of the declaration -- each bar, for a top
9
+ level ``var`` -- opens a slot (``open``) and the end of the bar closes it with
10
+ a copy (``close``), so ``a[1]`` is the array as the previous bar left it,
11
+ TradingView's read-only history (fixtures/array_history_tv).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from ..collection_history import (
17
+ COLLECTION_HISTORY_CLASS_CPP,
18
+ COLLECTION_HISTORY_CPP,
19
+ COLLECTION_HISTORY_GENERIC_MATRIX_CPP,
20
+ COLLECTION_HISTORY_MATRIX_CPP,
21
+ history_annotation,
22
+ literal_offset,
23
+ )
24
+ from ..ast_nodes import FuncCall, Identifier, MemberAccess
25
+ from ..symbols import TypeSpec
26
+
27
+
28
+ class CollectionHistoryEmitter:
29
+ """CodeGen mixin: the history members of array and matrix variables."""
30
+
31
+ def _collection_history_variables(self) -> list:
32
+ """The declarations whose history the script reads, in key order."""
33
+ history = getattr(self.ctx, "collection_history", None) or {}
34
+ return [history[key] for key in sorted(history)]
35
+
36
+ def _collection_history_member(self, variable) -> str:
37
+ """``_pf_collection_hist_<name>``; a later declaration of the same
38
+ name (a sibling block's) gets ``_pf_collection_hist_<ordinal>_<name>``,
39
+ which no Pine name (none starts with a digit) can spell: ``x``'s
40
+ second declaration beside a variable ``x_1``."""
41
+ prefix = f"{variable.ordinal}_" if variable.ordinal else ""
42
+ return f"_pf_collection_hist_{prefix}{self._safe_name(variable.name)}"
43
+
44
+ def _collection_history_of(self, annotation):
45
+ history = getattr(self.ctx, "collection_history", None) or {}
46
+ return history.get(annotation.get("member"))
47
+
48
+ def _collection_history_cpp_type(self, variable) -> str:
49
+ """The history's element type: the variable member's own C++ type,
50
+ whatever its declaration spells (a wide int array included)."""
51
+ return f"decltype({self._safe_name(variable.name)})"
52
+
53
+ def _collection_history_capacity(self, variable) -> str:
54
+ """The ring's size: the largest literal offset read plus one, or the
55
+ Series default (a ``max_bars_back`` cap) for a dynamic offset."""
56
+ if variable.capacity is None:
57
+ return self._series_decl_suffix()
58
+ return f"{{{max(2, variable.capacity)}}}"
59
+
60
+ def _emit_collection_history_helper(self, lines: list[str]) -> None:
61
+ variables = self._collection_history_variables()
62
+ if not variables:
63
+ return
64
+ float_spec = TypeSpec.primitive("float")
65
+ matrices = [v.spec for v in variables if v.kind == "matrix"]
66
+ lines.append(COLLECTION_HISTORY_CPP.strip("\n"))
67
+ if any(spec.element == float_spec for spec in matrices):
68
+ lines.append(COLLECTION_HISTORY_MATRIX_CPP.strip("\n"))
69
+ if any(spec.element != float_spec for spec in matrices):
70
+ lines.append(COLLECTION_HISTORY_GENERIC_MATRIX_CPP.strip("\n"))
71
+ lines.append(COLLECTION_HISTORY_CLASS_CPP.strip("\n"))
72
+ lines.append("")
73
+
74
+ def _collection_history_open(self, variable) -> str:
75
+ """The statement opening a declaration's slot."""
76
+ return (f"{self._collection_history_member(variable)}.open("
77
+ f"{self._safe_name(variable.name)}, history_advances_new_bar());")
78
+
79
+ def _collection_history_close(self, variable) -> str:
80
+ return (f"{self._collection_history_member(variable)}.close("
81
+ f"{self._safe_name(variable.name)});")
82
+
83
+ def _collection_history_decl_statements(self, node) -> list[str]:
84
+ """What an execution of a declaration does first: close the history
85
+ of every other declaration writing the same member (a sibling
86
+ block's ``x``, which keeps the array its block left), then open its
87
+ own slot when it is not a ``var`` (a block's on the bars it runs)."""
88
+ statements = []
89
+ own = None
90
+ for variable in self._collection_history_variables():
91
+ if id(node) in variable.closed_by:
92
+ statements.append(self._collection_history_close(variable))
93
+ elif variable.decl_node_id == id(node):
94
+ own = variable
95
+ if own is not None and not (getattr(node, "is_var", False)
96
+ or getattr(node, "is_varip", False)):
97
+ statements.append(self._collection_history_open(own))
98
+ return statements
99
+
100
+ def _emit_collection_history_var_opens(self, lines: list[str], pad: str) -> None:
101
+ """A top-level ``var``'s slot opens on every bar, after its first
102
+ bar's initialization."""
103
+ for variable in self._collection_history_variables():
104
+ if variable.is_var:
105
+ lines.append(f"{pad}{self._collection_history_open(variable)}")
106
+
107
+ def _emit_collection_history_closes(self, lines: list[str], pad: str) -> None:
108
+ """The bar's end: every open slot keeps a copy of its variable."""
109
+ for variable in self._collection_history_variables():
110
+ lines.append(f"{pad}{self._collection_history_close(variable)}")
111
+
112
+ @staticmethod
113
+ def _roots_at_collection_history(value) -> bool:
114
+ """Whether ``value`` is an array's or a matrix's history or a
115
+ built-in's call on one (``m[1]``, ``(m[1]).copy()``,
116
+ ``matrix.transpose(m[1])``)."""
117
+ if history_annotation(value) is not None:
118
+ return True
119
+ if not isinstance(value, FuncCall):
120
+ return False
121
+ callee = value.callee
122
+ if isinstance(callee, MemberAccess) and history_annotation(callee.object) is not None:
123
+ return True
124
+ first = value.args[0] if value.args else None
125
+ return (isinstance(callee, MemberAccess)
126
+ and isinstance(callee.object, Identifier)
127
+ and callee.object.name in ("array", "matrix")
128
+ and first is not None and history_annotation(first) is not None)
129
+
130
+ def _collection_history_offset_cpp(self, node) -> str:
131
+ offset = literal_offset(node)
132
+ if offset is not None:
133
+ return str(offset)
134
+ return self._history_offset_cpp(self._visit_expr(node.index), node.index)
135
+
136
+ def _lower_collection_history(self, node) -> str | None:
137
+ """The C++ of an annotated array or matrix history read, by its use:
138
+ a ``const`` copy a built-in reads (``at``), a copy for a variable or a
139
+ parameter (``value``), the receiver of a built-in that changes it
140
+ (``changed``: the run stops past a zero offset), the variable itself
141
+ for a for...in loop (TradingView iterates the current array) and at a
142
+ zero offset. None for any other read."""
143
+ annotation = history_annotation(node)
144
+ if annotation is None:
145
+ return None
146
+ current = self._visit_expr(node.object)
147
+ use = annotation["use"]
148
+ offset = literal_offset(node)
149
+ if use == "loop" or offset == 0:
150
+ return current
151
+ member = self._collection_history_member(self._collection_history_of(annotation))
152
+ index = self._collection_history_offset_cpp(node)
153
+ dynamic = offset is None
154
+ if use == "change":
155
+ return f"{member}.changed({index}, {current})"
156
+ if use == "copy":
157
+ return (f"{member}.value({index}, {current})" if dynamic
158
+ else f"{member}.value({index})")
159
+ return (f"{member}.at({index}, {current})" if dynamic
160
+ else f"{member}.at({index})")
161
+
162
+ def _collection_history_na(self, node) -> str | None:
163
+ """``na(a[k])``: whether the history holds no copy there (an array
164
+ variable itself is never na in PineForge; a matrix may be)."""
165
+ annotation = history_annotation(node)
166
+ if annotation is None or annotation["use"] != "na":
167
+ return None
168
+ current = self._visit_expr(node.object)
169
+ offset = literal_offset(node)
170
+ if offset == 0:
171
+ return f"is_na({current})" if annotation["kind"] == "matrix" else "false"
172
+ member = self._collection_history_member(self._collection_history_of(annotation))
173
+ index = self._collection_history_offset_cpp(node)
174
+ if offset is None:
175
+ return f"{member}.is_na({index}, {current})"
176
+ return f"{member}.is_na({index})"
177
+
178
+ def _matrix_history_method_expr(self, receiver, method: str, spec, node) -> str:
179
+ """A matrix method called on a matrix's history, lowered as the
180
+ variable's own method call is (visit_call: a matrix variable's
181
+ method)."""
182
+ from .tables import MATRIX_METHOD_KWARGS, MATRIX_METHODS, _merge_kwargs
183
+
184
+ recv = self._visit_expr(receiver)
185
+ self._check_matrix_method_allowed(method, spec, node)
186
+ param_names = MATRIX_METHOD_KWARGS.get(method)
187
+ raw_args = (
188
+ _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
189
+ if param_names and node.kwargs else list(node.args)
190
+ )
191
+ args = self._matrix_bool_value_args(
192
+ method, [self._visit_expr(a) for a in raw_args], raw_args, spec)
193
+ try:
194
+ return MATRIX_METHODS[method](recv, args)
195
+ except IndexError:
196
+ self._codegen_error(
197
+ node,
198
+ f"matrix.{method}: wrong number of arguments",
199
+ hint="Check Pine v6 matrix method signature (positional vs keyword).",
200
+ )
@@ -1721,7 +1721,12 @@ class TopLevelEmitter:
1721
1721
  name,
1722
1722
  target_cpp_type=udt_name,
1723
1723
  )
1724
- lines.append(f" {safe} = {cpp_val};")
1724
+ if self._binding_is_series(name, safe):
1725
+ # A history-read var object: its first
1726
+ # bar's slot holds the reference.
1727
+ lines.append(f" {safe}.push({cpp_val});")
1728
+ else:
1729
+ lines.append(f" {safe} = {cpp_val};")
1725
1730
  break
1726
1731
  continue
1727
1732
  if self._binding_is_series(name, safe):
@@ -1797,6 +1802,10 @@ class TopLevelEmitter:
1797
1802
  # c. Push non-var series (they start fresh each bar with a push)
1798
1803
  # (actual push happens in visit_VarDecl when the decl is visited)
1799
1804
 
1805
+ # c1. A top-level var array's or matrix's history slot opens on every
1806
+ # bar (pineforge_codegen/collection_history.py).
1807
+ self._emit_collection_history_var_opens(lines, " ")
1808
+
1800
1809
  # c2. First-bar TA resize: rebuild any TA object whose ctor args come
1801
1810
  # from input-backed variables so strategy_set_input() actually changes
1802
1811
  # the circular-buffer sizes. Emits nothing when no TA site depends on
@@ -1815,6 +1824,10 @@ class TopLevelEmitter:
1815
1824
  finally:
1816
1825
  self._clear_lazy_edge_ta_hoists()
1817
1826
 
1827
+ # d2. The bar's end: each array or matrix variable whose history the
1828
+ # script reads keeps a copy of the value it ends the bar with.
1829
+ self._emit_collection_history_closes(lines, " ")
1830
+
1818
1831
  # e. ``// @pf-trace`` pragma block — emitted last so trace values
1819
1832
  # reflect every assignment / strategy call made earlier in the
1820
1833
  # bar. The block is wrapped in ``if (trace_enabled_)`` so cost
@@ -2103,11 +2116,16 @@ class TopLevelEmitter:
2103
2116
 
2104
2117
  if (
2105
2118
  receiver_spec is not None
2106
- and receiver_spec.kind == "primitive"
2119
+ and (receiver_spec.kind == "primitive"
2120
+ or (receiver_spec.kind == "udt"
2121
+ and (receiver_spec.name in DRAWING_TYPE_TO_CPP
2122
+ or receiver_spec.name in self._udt_defs)))
2107
2123
  and p in func_sv
2108
2124
  ):
2109
2125
  # A primitive receiver used with history is a Series boundary,
2110
- # just like an ordinary history-bearing UDF parameter.
2126
+ # just like an ordinary history-bearing UDF parameter; so is
2127
+ # an object or drawing receiver (``this[1]`` is the receiver of
2128
+ # the previous call: fixtures/udt_history_tv udth_method).
2111
2129
  elem_cpp_t = self._series_param_element_cpp_type(
2112
2130
  fi, i, call_site_idx
2113
2131
  )
@@ -2118,6 +2136,9 @@ class TopLevelEmitter:
2118
2136
  self._current_func_series_param_types[
2119
2137
  self._safe_name(p)
2120
2138
  ] = elem_cpp_t
2139
+ if receiver_spec.kind == "udt":
2140
+ self._udt_param_udt[self._safe_name(p)] = receiver_spec.name
2141
+ self._udt_param_udt[p] = receiver_spec.name
2121
2142
  elif is_method and i == 0 and fi.udt_type_name:
2122
2143
  # Receiver pass modes follow Pine's value/ID families. A user
2123
2144
  # UDT is itself a numeric object-ID handle, so pass it by value:
@@ -2352,10 +2373,16 @@ class TopLevelEmitter:
2352
2373
  ),
2353
2374
  )
2354
2375
  if (drawing_name in DRAWING_TYPE_TO_CPP
2355
- and self._current_func_param_types.get(
2376
+ and (self._current_func_param_types.get(
2356
2377
  param, ""
2357
2378
  ).removesuffix("&").removesuffix("*")
2358
- == DRAWING_TYPE_TO_CPP[drawing_name])
2379
+ == DRAWING_TYPE_TO_CPP[drawing_name]
2380
+ # A history-read drawing parameter or receiver is a
2381
+ # ``const Series<Box>&``: ``(x[1]).get_top()`` dispatches on
2382
+ # the handle it holds (udth_drawparam).
2383
+ or (param in self._current_func_series_params
2384
+ and self._current_func_series_param_types.get(param)
2385
+ == DRAWING_TYPE_TO_CPP[drawing_name])))
2359
2386
  }
2360
2387
  self._lexical_udt_types = {
2361
2388
  param: (
@@ -345,7 +345,10 @@ CPP_RESERVED = set(
345
345
  # ``_nz_v``) or initialized itself from itself (``_pna_l > close`` did not
346
346
  # compile). ``_safe_name`` escapes an authored spelling of any of them. A
347
347
  # temporary whose name the emitter allocates against the authored spellings
348
- # itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed.
348
+ # itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed. An
349
+ # array's or a matrix's history member (``_pf_collection_hist_<name>``,
350
+ # ``_pf_collection_hist_<n>_<name>``) is reserved the same way: a script
351
+ # variable of that spelling was a second member of one name.
349
352
  CPP_TEMPORARY_NAMES = frozenset("""
350
353
  _nz_v _nz_y _fixnan_v _pna_l _pna_r _pfc_l _pfc_r _pfc_eq _hv _hidx _sv
351
354
  _out _func_ret _pf_v _pf_idx_v _pf_idx_t _pf_bool_v _pf_bool_t
@@ -364,6 +367,7 @@ _CPP_TEMPORARY_PATTERN = re.compile(
364
367
  r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
365
368
  r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
366
369
  r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
370
+ r"|_pf_collection_hist_\w+"
367
371
  )
368
372
 
369
373
 
@@ -88,7 +88,7 @@ from .helpers import na_preserving_int_cast, unary_sign_cpp
88
88
  from ..security_contexts import GLOBAL_ANNOTATION, UNREACHED_ANNOTATION
89
89
  from ..symbols import PineType, method_receiver_type_name
90
90
  from .tables import (
91
- BAR_BUILTINS, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
91
+ BAR_BUILTINS, DRAWING_TYPE_TO_CPP, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
92
92
  SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, TA_TUPLE_FIELDS,
93
93
  _math_minmax_na_expr, _math_round_digits_expr, _merge_kwargs,
94
94
  )
@@ -6127,11 +6127,66 @@ class SecurityEmitter:
6127
6127
  lines.append(" }")
6128
6128
  lines.append("")
6129
6129
 
6130
+ def _refuse_security_reference_history(self, item: dict) -> None:
6131
+ """A request.security expression that reads the history of an
6132
+ object or drawing reference, itself or in a function or method it
6133
+ calls. TradingView evaluates it on the requested bars, where the
6134
+ variable holds the objects of that timeframe's bars; PineForge keeps
6135
+ such a history on the chart's bars only, and the payload read the
6136
+ chart's (it never compiled before: the history was a
6137
+ ``Series<double>``). Refused."""
6138
+ if item["expr_node"] is None:
6139
+ # ``request.security(sym, tf)`` with no expression: lowered to na
6140
+ # as it always was (TradingView refuses it, CE10165).
6141
+ return
6142
+ pending = [item["expr_node"]]
6143
+ seen: set[str] = set()
6144
+ while pending:
6145
+ for node, _depth in iter_ast_nodes(pending.pop()):
6146
+ if isinstance(node, Subscript):
6147
+ receiver = node.object
6148
+ kind = (self._series_handle_type_name(receiver.name)
6149
+ if isinstance(receiver, Identifier) else None)
6150
+ if kind is None:
6151
+ spec = self._type_spec_from_expr(receiver)
6152
+ if (spec is not None and spec.kind == "udt"
6153
+ and (spec.name in DRAWING_TYPE_TO_CPP
6154
+ or spec.name in self._udt_defs)):
6155
+ kind = spec.name
6156
+ if kind is not None:
6157
+ self._codegen_error(
6158
+ node,
6159
+ f"History of a {kind} reference inside a "
6160
+ "request.security expression is not supported in "
6161
+ "PineForge: TradingView reads the references the "
6162
+ "variable held on the requested timeframe's bars, "
6163
+ "which PineForge does not keep.",
6164
+ hint="Request the field values the expression "
6165
+ "needs (request.security(..., obj.field)) "
6166
+ "and read their history on the chart.",
6167
+ )
6168
+ if not isinstance(node, FuncCall):
6169
+ continue
6170
+ func_name, namespace = self._resolve_callee(node.callee)
6171
+ keys = []
6172
+ if namespace is None and func_name in self._func_info_map:
6173
+ keys.append(func_name)
6174
+ if isinstance(node.callee, MemberAccess):
6175
+ keys.extend(key for key in self._func_info_map
6176
+ if key.endswith(f".{node.callee.member}"))
6177
+ for key in keys:
6178
+ body = getattr(self._func_info_map[key], "node", None)
6179
+ if key not in seen and body is not None:
6180
+ seen.add(key)
6181
+ pending.append(body)
6182
+
6130
6183
  def _emit_security_evaluators(self, lines: list[str]) -> None:
6131
6184
  """Emit _eval_security_N() methods and evaluate_security() dispatch."""
6132
6185
  if not self._security_calls:
6133
6186
  return
6134
6187
 
6188
+ for item in self._security_calls:
6189
+ self._refuse_security_reference_history(item)
6135
6190
  for item in self._security_calls:
6136
6191
  self._emit_security_evaluator_requested(item, lines)
6137
6192
 
@@ -1131,6 +1131,65 @@ class TaSiteHelper:
1131
1131
  )
1132
1132
  return cache[id(node)]
1133
1133
 
1134
+ def _lazy_ctor_history_call(self, call) -> bool:
1135
+ """A positional call of the one user function of its name whose body
1136
+ is a single ``T.new(...)`` of a user-defined type over
1137
+ ``_lazy_call_history_pure_expr`` arguments of its parameters: its value
1138
+ is a new object of fixed fields, and TradingView runs it on every
1139
+ execution of its scope below a lazy edge when its history is read
1140
+ (fixtures/udt_history_tv udth_expr: ``(mk(bar_index * 7)[1]).v`` in a
1141
+ ternary's arm reads the previous bar's call)."""
1142
+ if not isinstance(call, FuncCall) or not isinstance(call.callee, Identifier):
1143
+ return False
1144
+ overloads = [fi for fi in self.ctx.func_infos if fi.name == call.callee.name]
1145
+ if call.kwargs or len(overloads) != 1 or overloads[0].node is None:
1146
+ return False
1147
+ node = overloads[0].node
1148
+ body = getattr(node, "body", None) or []
1149
+ if len(call.args) != len(node.params) or len(body) != 1:
1150
+ return False
1151
+ ctor = body[0].expr if isinstance(body[0], ExprStmt) else None
1152
+ if not (isinstance(ctor, FuncCall)
1153
+ and isinstance(ctor.callee, MemberAccess)
1154
+ and isinstance(ctor.callee.object, Identifier)
1155
+ and ctor.callee.object.name in self._udt_defs
1156
+ and ctor.callee.member == "new"):
1157
+ return False
1158
+ params = set(node.params)
1159
+ return all(self._lazy_call_history_pure_expr(arg, params)
1160
+ for arg in [*ctor.args, *ctor.kwargs.values()])
1161
+
1162
+ def _lazy_reference_history_object(self, obj) -> bool:
1163
+ """Whether the history of the object or drawing reference ``obj``
1164
+ below a lazy edge is kept on every execution of its scope, as
1165
+ TradingView keeps it (udth_expr): a pure call or a
1166
+ ``_lazy_ctor_history_call`` over pure arguments, a ternary over
1167
+ names, literals and operators, or a field of a named object (read
1168
+ na while the object is na, ``_lazy_history_read_cpp``)."""
1169
+ if self._reference_cpp_type(obj) is None:
1170
+ return False
1171
+ if isinstance(obj, FuncCall):
1172
+ return ((self._lazy_call_history_pure_call(obj)
1173
+ or self._lazy_ctor_history_call(obj))
1174
+ and all(self._lazy_call_history_pure_expr(arg, None)
1175
+ for arg in obj.args))
1176
+ if isinstance(obj, Ternary):
1177
+ return self._lazy_call_history_pure_expr(obj, None)
1178
+ return (isinstance(obj, MemberAccess)
1179
+ and isinstance(obj.object, Identifier)
1180
+ and self._reference_cpp_type(obj.object) is not None)
1181
+
1182
+ def _lazy_history_read_cpp(self, obj) -> str:
1183
+ """The value a hoisted history read pushes: the expression, a field of
1184
+ a na object reading na (TradingView stops on the read itself)."""
1185
+ if (isinstance(obj, MemberAccess)
1186
+ and self._reference_cpp_type(obj.object) is not None):
1187
+ handle = self._reference_cpp_type(obj)
1188
+ receiver = self._visit_expr(obj.object)
1189
+ return (f"(is_na({receiver}) ? {handle}{{}} : "
1190
+ f"{handle}({self._visit_expr(obj)}))")
1191
+ return self._visit_expr(obj)
1192
+
1134
1193
  def _lazy_call_history_units(self, stmt) -> list:
1135
1194
  """The ``pure_call(...)[k]`` reads below a lazy edge of ``stmt``'s
1136
1195
  roots (``_lazy_edge_statement_roots``), in evaluation order, whose
@@ -1159,6 +1218,10 @@ class TaSiteHelper:
1159
1218
  for arg in call.args)):
1160
1219
  units.append(expr)
1161
1220
  return
1221
+ if (under_lazy and self._registered_history_handle(expr) is not None
1222
+ and self._lazy_reference_history_object(call)):
1223
+ units.append(expr)
1224
+ return
1162
1225
  scan(expr.object, under_lazy)
1163
1226
  scan(expr.index, under_lazy)
1164
1227
  elif isinstance(expr, FuncCall):
@@ -1219,7 +1282,8 @@ class TaSiteHelper:
1219
1282
  hoisted = []
1220
1283
  for read in units:
1221
1284
  member = self._inline_history_member("hist_call", read)
1222
- self._emit_history_series_write(lines, pad, member, self._visit_expr(read.object))
1285
+ self._emit_history_series_write(
1286
+ lines, pad, member, self._lazy_history_read_cpp(read.object))
1223
1287
  self._hoisted_hist_reads[id(read)] = member
1224
1288
  hoisted.append(id(read))
1225
1289
  return hoisted