@pineforge/codegen-pyodide 0.8.1 → 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.
@@ -255,7 +255,7 @@ class DrawingVisitor:
255
255
  vals = self._merge_drawing_args(node, ["price"])
256
256
  price = (self._visit_expr(vals["price"]) if vals.get("price") is not None
257
257
  else "current_bar_.close")
258
- return (f"ChartPoint{{ .index=bar_index_, "
258
+ return (f"ChartPoint{{ .index=(int64_t)(pine_bar_index()), "
259
259
  f".time=(int64_t)current_bar_.timestamp, .price=({price}) }}")
260
260
  if func_name == "from_index":
261
261
  vals = self._merge_drawing_args(node, ["index", "price"])
@@ -118,6 +118,8 @@ class TopLevelEmitter:
118
118
  lines.append("#include <string>")
119
119
  lines.append("#include <vector>")
120
120
  lines.append("#include <tuple>")
121
+ lines.append("#include <optional>")
122
+ lines.append("#include <type_traits>")
121
123
  lines.append("#include <memory>")
122
124
  lines.append("#include <mutex>")
123
125
  lines.append("#include <unordered_map>")
@@ -192,6 +194,139 @@ class TopLevelEmitter:
192
194
  return True
193
195
  return False
194
196
 
197
+ @staticmethod
198
+ def _script_state_member_name(decl_line: str) -> str | None:
199
+ """Extract a generated class-member name from one declaration line.
200
+
201
+ ``CodeGen.generate`` emits the complete persistent script state as a
202
+ contiguous block of one-line declarations before the constructor. The
203
+ rollback checkpoint is derived from that block rather than from a
204
+ second, hand-maintained inventory: adding a new TA/helper/collection
205
+ member therefore automatically makes it part of Pine's historical
206
+ execution rollback.
207
+
208
+ The supported declaration shapes are the only shapes emitted in that
209
+ block today::
210
+
211
+ Type name;
212
+ Type name = value;
213
+ Type name(args);
214
+ Type name{args};
215
+
216
+ A declaration that does not match fails generation loudly. Silently
217
+ omitting an unfamiliar member would be materially worse: it would let
218
+ state leak between ``calc_on_order_fills`` executions.
219
+ """
220
+ text = decl_line.strip()
221
+ if not text:
222
+ return None
223
+ if not text.endswith(";"):
224
+ raise AssertionError(
225
+ f"unexpected generated script-state declaration: {decl_line!r}"
226
+ )
227
+ text = text[:-1].rstrip()
228
+ if " = " in text:
229
+ text = text.split(" = ", 1)[0].rstrip()
230
+ else:
231
+ # Series<T> members can carry a max_bars_back ctor suffix and
232
+ # drawing arenas use brace initialization. Both suffixes start in
233
+ # the final declarator token; C++ types emitted here never contain
234
+ # parentheses or braces.
235
+ last_space = text.rfind(" ")
236
+ if last_space < 0:
237
+ raise AssertionError(
238
+ f"missing type in generated script-state declaration: {decl_line!r}"
239
+ )
240
+ declarator = text[last_space + 1:]
241
+ cut = len(declarator)
242
+ for marker in ("(", "{"):
243
+ pos = declarator.find(marker)
244
+ if pos >= 0:
245
+ cut = min(cut, pos)
246
+ text = text[:last_space + 1] + declarator[:cut]
247
+
248
+ name = text.rsplit(" ", 1)[-1].strip()
249
+ if not name or not (name[0].isalpha() or name[0] == "_") \
250
+ or not all(ch.isalnum() or ch == "_" for ch in name):
251
+ raise AssertionError(
252
+ f"cannot identify generated script-state member: {decl_line!r}"
253
+ )
254
+ return name
255
+
256
+ def _collect_script_state_members(self, declaration_lines: list[str]) -> list[str]:
257
+ """Return every rollback-relevant generated member in declaration order.
258
+
259
+ Precalculated TA result vectors and their mode flag are immutable once
260
+ the engine starts its broker walk. Copying an O(number-of-bars) cache
261
+ before every COOF execution would be both unnecessary and catastrophic,
262
+ so those implementation caches are the sole exclusions. The live TA
263
+ objects themselves remain captured because dynamic/magnifier runs call
264
+ ``compute`` and mutate them.
265
+ """
266
+ members: list[str] = []
267
+ seen: set[str] = set()
268
+ for line in declaration_lines:
269
+ name = self._script_state_member_name(line)
270
+ if name is None:
271
+ continue
272
+ if name == "_use_precalc" or name.startswith("_precalc_"):
273
+ continue
274
+ if name in seen:
275
+ raise AssertionError(f"duplicate generated script-state member: {name}")
276
+ seen.add(name)
277
+ members.append(name)
278
+ return members
279
+
280
+ def _emit_script_state_hooks(self, lines: list[str], members: list[str]) -> None:
281
+ """Emit the engine's Pine rollback checkpoint hook implementation.
282
+
283
+ The checkpoint owns value copies of all generated mutable state. Every
284
+ runtime container used by generated code (Series, std::vector/map,
285
+ PineMatrix/generic matrices, UDTs and drawing arenas) has value
286
+ semantics, so copying recursively preserves data without retaining
287
+ pointers into live state. Drawing handles themselves are stable ids;
288
+ their arenas are captured in the same checkpoint.
289
+
290
+ The static assertions deliberately turn any future non-copyable member
291
+ into a compile failure instead of a nominal, shallow rollback. Engine
292
+ broker/order state lives in the base class and is intentionally absent:
293
+ fills must survive while Pine script variables roll back.
294
+ """
295
+ lines.append(" struct _PFScriptState {")
296
+ for idx, name in enumerate(members):
297
+ lines.append(
298
+ f" decltype(GeneratedStrategy::{name}) _pf_value_{idx};"
299
+ )
300
+ lines.append(" };")
301
+ lines.append(
302
+ " static_assert(std::is_copy_constructible_v<_PFScriptState>, "
303
+ '"generated Pine state must be deep-copy constructible");'
304
+ )
305
+ lines.append(
306
+ " static_assert(std::is_copy_assignable_v<_PFScriptState>, "
307
+ '"generated Pine state must be deep-copy assignable");'
308
+ )
309
+ lines.append(" std::optional<_PFScriptState> _pf_script_state_checkpoint_;")
310
+ lines.append("")
311
+ lines.append(" void snapshot_script_state() override {")
312
+ lines.append(" _pf_script_state_checkpoint_.emplace(_PFScriptState{")
313
+ for name in members:
314
+ lines.append(f" {name},")
315
+ lines.append(" });")
316
+ lines.append(" }")
317
+ lines.append("")
318
+ lines.append(" void restore_script_state() override {")
319
+ lines.append(" if (!_pf_script_state_checkpoint_) return;")
320
+ for idx, name in enumerate(members):
321
+ lines.append(
322
+ f" this->{name} = _pf_script_state_checkpoint_->_pf_value_{idx};"
323
+ )
324
+ lines.append(" }")
325
+ lines.append("")
326
+ lines.append(" void commit_script_state() override {")
327
+ lines.append(" snapshot_script_state();")
328
+ lines.append(" }")
329
+
195
330
  def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
196
331
  """Re-type a bare ``na<double>()`` initializer to match a non-double
197
332
  member's C++ type. A ``var int x = na`` resolves its RHS to
@@ -212,7 +347,11 @@ class TopLevelEmitter:
212
347
  def _emit_constructor(self, lines: list[str]) -> None:
213
348
  init_parts: list[str] = []
214
349
  # TA members with ctor args
215
- for site in self.ctx.ta_call_sites:
350
+ for ta_idx, site in enumerate(self.ctx.ta_call_sites):
351
+ # Skip dead-code function TA sites entirely — their buffers never
352
+ # run and their ctor args (bare param names) can never be sized.
353
+ if ta_idx in self._dead_ta_indices:
354
+ continue
216
355
  if site.ctor_args:
217
356
  # If a ctor arg is neither a compile-time literal nor expandable
218
357
  # to an input-backed runtime expression, the old code silently
@@ -296,6 +435,9 @@ class TopLevelEmitter:
296
435
  if sp.get("process_orders_on_close") is True:
297
436
  ctor_body.append(" process_orders_on_close_ = true;")
298
437
 
438
+ if sp.get("calc_on_order_fills") is True:
439
+ ctor_body.append(" calc_on_order_fills_ = true;")
440
+
299
441
  if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
300
442
  ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
301
443
 
@@ -380,6 +522,7 @@ class TopLevelEmitter:
380
522
  lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
381
523
  lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
382
524
  lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
525
+ lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
383
526
  lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
384
527
  lines.append(' if (key == "default_qty_type") {')
385
528
  lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
@@ -448,14 +591,45 @@ class TopLevelEmitter:
448
591
  "initial_capital": "initial_capital_",
449
592
  }
450
593
 
594
+ @staticmethod
595
+ def _emit_history_series_write(
596
+ lines: list[str], pad: str, member: str, value: str) -> None:
597
+ """Emit one Pine-series write without conflating history with isnew.
598
+
599
+ Historical fill recalculations keep ``barstate.isnew`` true, but a
600
+ post-close recalculation restored from the completed ordinary-close
601
+ checkpoint must replace that bar's current history slot rather than
602
+ append a duplicate slot. The engine exposes those independent facts
603
+ as ``is_first_tick_`` and ``history_advances_new_bar()`` respectively.
604
+ """
605
+ lines.append(
606
+ f"{pad}if (history_advances_new_bar()) {member}.push({value});"
607
+ )
608
+ lines.append(f"{pad}else {member}.update({value});")
609
+
451
610
  def _emit_on_bar(self, lines: list[str]) -> None:
452
611
  lines.append(" void on_bar(const Bar& bar) override {")
453
612
 
613
+ # reset_run_state() owns engine/broker state, while these generated
614
+ # Series members belong to the strategy object. Clear all of them on
615
+ # the first genuine history slot of bar zero, unconditionally: a site
616
+ # may live behind a branch that does not execute on bar zero. This is a
617
+ # narrow synthetic-buffer reset, not a promise that every generated
618
+ # member/init latch supports full same-handle reruns. The post-C rollback
619
+ # execution has history_advances_new_bar()==false, so it preserves the
620
+ # committed slot.
621
+ for info in self._inline_history_members:
622
+ lines.append(
623
+ " if (history_advances_new_bar() && bar_index_ == 0) "
624
+ f"{info['member_name']}.clear();"
625
+ )
626
+
454
627
  # a. Push bar field series (with bar magnifier support)
455
628
  for field_name in sorted(self.ctx.series_bar_fields):
456
629
  push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
457
- lines.append(f" if (is_first_tick_) _s_{field_name}.push({push_expr});")
458
- lines.append(f" else _s_{field_name}.update({push_expr});")
630
+ self._emit_history_series_write(
631
+ lines, " ", f"_s_{field_name}", push_expr
632
+ )
459
633
 
460
634
  # a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
461
635
  # hl2[n], …). They land in ``series_vars`` and are declared as Series
@@ -470,17 +644,16 @@ class TopLevelEmitter:
470
644
  if _bname in self._var_names:
471
645
  continue
472
646
  _bexpr = BAR_BUILTINS.get(_bname)
473
- if _bexpr is None or f"{_bname}(" in _bexpr:
647
+ if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
474
648
  continue
475
649
  _bsafe = self._safe_name(_bname)
476
- lines.append(f" if (is_first_tick_) {_bsafe}.push({_bexpr});")
477
- lines.append(f" else {_bsafe}.update({_bexpr});")
650
+ self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
478
651
 
479
652
  # a2. Push strategy series
480
653
  for svar in sorted(self._strategy_series_vars):
481
654
  member = svar.replace("_strat_", "")
482
655
  push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
483
- lines.append(f" {svar}.push({push_expr});")
656
+ self._emit_history_series_write(lines, " ", svar, push_expr)
484
657
 
485
658
  # b. Var init / carry-forward
486
659
  if self.ctx.var_members:
@@ -552,7 +725,9 @@ class TopLevelEmitter:
552
725
  if name in self._array_vars:
553
726
  continue
554
727
  if name in self.ctx.series_vars:
555
- lines.append(f" if (is_first_tick_) {safe}.push({safe}[0]);")
728
+ self._emit_history_series_write(
729
+ lines, " ", safe, f"{safe}[0]"
730
+ )
556
731
  # Also carry-forward cloned copies for per-call-site function variants
557
732
  carry_emitted: set[str] = set()
558
733
  for (fname, cs_idx), remap in self._func_cs_var_remap.items():
@@ -562,7 +737,9 @@ class TopLevelEmitter:
562
737
  cloned = remap[safe]
563
738
  if cloned not in carry_emitted:
564
739
  carry_emitted.add(cloned)
565
- lines.append(f" if (is_first_tick_) {cloned}.push({cloned}[0]);")
740
+ self._emit_history_series_write(
741
+ lines, " ", cloned, f"{cloned}[0]"
742
+ )
566
743
  lines.append(" }")
567
744
 
568
745
  # c. Push non-var series (they start fresh each bar with a push)
@@ -776,10 +953,12 @@ class TopLevelEmitter:
776
953
  # Determine param types and set context for type inference inside body
777
954
  param_strs = []
778
955
  self._current_func_param_types = {}
956
+ self._current_func_param_specs = {}
779
957
  self._current_func_series_params = set()
780
958
  self._udt_param_udt = {}
781
959
  func_sv = self.ctx.func_series_vars.get(fi.name, set())
782
960
  for i, p in enumerate(node.params):
961
+ spec = None
783
962
  if is_udt and i == 0 and fi.udt_type_name:
784
963
  # A method receiver whose type is a drawing primitive
785
964
  # (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
@@ -821,6 +1000,9 @@ class TopLevelEmitter:
821
1000
  cpp_t = "double"
822
1001
  param_strs.append(f"{cpp_t} {self._safe_name(p)}")
823
1002
  self._current_func_param_types[p] = cpp_t
1003
+ if spec is not None:
1004
+ self._current_func_param_specs[p] = spec
1005
+ self._current_func_param_specs[self._safe_name(p)] = spec
824
1006
 
825
1007
  # Determine return type: tuple, UDT, or scalar.
826
1008
  # The UDT branch handles user functions whose body is ``T.new(...)``;
@@ -852,6 +1034,7 @@ class TopLevelEmitter:
852
1034
  func_name = instance["name"]
853
1035
  self._active_ta_remap = instance["ta_remap"]
854
1036
  self._active_var_remap = instance["var_remap"]
1037
+ self._active_fixnan_remap = instance.get("fixnan_remap", {})
855
1038
  self._in_ta_func_variant = True
856
1039
  self._active_call_site_idx = None
857
1040
  self._current_instance_name = instance["name"]
@@ -862,17 +1045,24 @@ class TopLevelEmitter:
862
1045
  self._active_ta_remap = remap
863
1046
  var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
864
1047
  self._active_var_remap = var_remap
1048
+ self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
865
1049
  self._in_ta_func_variant = True
866
1050
  self._active_call_site_idx = call_site_idx
867
- self._current_instance_name = f"{self._func_safe_name(fi.name)}_cs{call_site_idx}"
1051
+ # Use the actual emitted name. Plain UDFs are unchanged; UDT
1052
+ # methods carry their `_udt_Type_method` prefix. This identity is
1053
+ # shared with _build_func_instances and synthetic-history member
1054
+ # registration, so method call paths can dispatch independently.
1055
+ self._current_instance_name = func_name
868
1056
  else:
869
1057
  self._active_ta_remap = {}
870
1058
  self._active_var_remap = {}
1059
+ self._active_fixnan_remap = {}
871
1060
  self._in_ta_func_variant = False
872
1061
  self._active_call_site_idx = None
873
1062
  self._current_instance_name = None
874
1063
 
875
1064
  prev_func_locals = self._current_func_locals
1065
+ prev_func_local_types = self._current_func_local_types
876
1066
  prev_func_body = getattr(self, "_current_func_body", None)
877
1067
  prev_func_name = getattr(self, "_active_func_name", None)
878
1068
  # The function body is the lexical scope used by the UDT-alias analysis
@@ -886,6 +1076,7 @@ class TopLevelEmitter:
886
1076
  prev_ptr_alias = self._udt_ptr_alias_locals
887
1077
  self._udt_ptr_alias_locals = set()
888
1078
  self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
1079
+ self._current_func_local_types = {}
889
1080
  # Plain (non-persistent) scalar locals are emitted inline and live in
890
1081
  # no other set; collect them so the unknown-identifier guard in
891
1082
  # _visit_ident does not mistake them for undeclared symbols.
@@ -956,14 +1147,17 @@ class TopLevelEmitter:
956
1147
 
957
1148
  lines.append(" }")
958
1149
  self._current_func_param_types = {}
1150
+ self._current_func_param_specs = {}
959
1151
  self._current_func_series_params = set()
960
1152
  self._udt_param_udt = {}
961
1153
  self._current_func_locals = prev_func_locals
1154
+ self._current_func_local_types = prev_func_local_types
962
1155
  self._current_func_body = prev_func_body
963
1156
  self._active_func_name = prev_func_name
964
1157
  self._udt_ptr_alias_locals = prev_ptr_alias
965
1158
  self._active_ta_remap = {}
966
1159
  self._active_var_remap = {}
1160
+ self._active_fixnan_remap = {}
967
1161
  self._in_ta_func_variant = False
968
1162
  self._active_call_site_idx = None
969
1163
  self._current_instance_name = None
@@ -1031,24 +1225,44 @@ class TopLevelEmitter:
1031
1225
  lines.append(" }")
1032
1226
 
1033
1227
  def _emit_precalculate_and_run(self, lines: list[str]) -> None:
1034
- has_static_ta = any(getattr(site, "is_static", False) for site in self.ctx.ta_call_sites)
1228
+ has_static_ta = any(
1229
+ self._ta_site_uses_precalc(site)
1230
+ for _ti, site in enumerate(self.ctx.ta_call_sites)
1231
+ if _ti not in self._dead_ta_indices
1232
+ )
1035
1233
  if not has_static_ta:
1036
1234
  return
1037
1235
 
1236
+ replayed_source_series: list[str] = []
1237
+ for stmt in self.ctx.ast.body:
1238
+ if not isinstance(stmt, VarDecl):
1239
+ continue
1240
+ if stmt.name not in self._global_member_vars:
1241
+ continue
1242
+ if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
1243
+ continue
1244
+ if stmt.name in self.ctx.series_vars:
1245
+ replayed_source_series.append(self._safe_name(stmt.name))
1246
+ replayed_source_series = sorted(set(replayed_source_series))
1247
+
1038
1248
  lines.append(" void precalculate(const Bar* bars, int n) {")
1039
1249
  lines.append(" _use_precalc = false;")
1040
1250
  lines.append(" if (n <= 0 || bars == nullptr) return;")
1041
1251
  lines.append("")
1042
1252
 
1043
1253
  # Resize precalculated vectors
1044
- for site in self.ctx.ta_call_sites:
1045
- if getattr(site, "is_static", False):
1254
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1255
+ if _ti in self._dead_ta_indices:
1256
+ continue
1257
+ if self._ta_site_uses_precalc(site):
1046
1258
  lines.append(f" _precalc_{site.member_name}.resize(n);")
1047
1259
 
1048
1260
  # Reset indicators to clean slate
1049
1261
  lines.append("")
1050
- for site in self.ctx.ta_call_sites:
1051
- if getattr(site, "is_static", False):
1262
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1263
+ if _ti in self._dead_ta_indices:
1264
+ continue
1265
+ if self._ta_site_uses_precalc(site):
1052
1266
  resolved = [self._resolve_known(a) for a in site.ctor_args]
1053
1267
  safe_resolved = []
1054
1268
  for r in resolved:
@@ -1059,6 +1273,13 @@ class TopLevelEmitter:
1059
1273
  lines.append("")
1060
1274
  for field_name in sorted(self.ctx.series_bar_fields):
1061
1275
  lines.append(f" _s_{field_name}.clear();")
1276
+ for safe in replayed_source_series:
1277
+ lines.append(f" {safe}.clear();")
1278
+ if self._script_has_input_source():
1279
+ lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
1280
+ lines.append(" _src_close_.clear(); _src_volume_.clear();")
1281
+ lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
1282
+ lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
1062
1283
 
1063
1284
  # Start precalculation loop
1064
1285
  lines.append("")
@@ -1081,8 +1302,8 @@ class TopLevelEmitter:
1081
1302
  # entire precalculation, silently corrupting its precalculated
1082
1303
  # values (e.g. a Bollinger Band's stdev collapsing to 0). Gated on
1083
1304
  # ``_src_series_active_`` to stay a no-op for scripts with no
1084
- # input.source() usage; cleared again before the real run begins
1085
- # (BacktestEngine::run() clears every _src_*_ series unconditionally).
1305
+ # input.source() usage; cleared before and after the precalc pass so
1306
+ # replayed source history cannot leak into the real run.
1086
1307
  lines.append(" if (_src_series_active_) {")
1087
1308
  lines.append(" const double _pc_o = bars[i].open;")
1088
1309
  lines.append(" const double _pc_h = bars[i].high;")
@@ -1136,8 +1357,10 @@ class TopLevelEmitter:
1136
1357
  # Set _precalc_loop_active = True
1137
1358
  self._precalc_loop_active = True
1138
1359
  try:
1139
- for site in self.ctx.ta_call_sites:
1140
- if getattr(site, "is_static", False):
1360
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1361
+ if _ti in self._dead_ta_indices:
1362
+ continue
1363
+ if self._ta_site_uses_precalc(site):
1141
1364
  compute_args = self._ta_compute_args_for_site(site)
1142
1365
  compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
1143
1366
  lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
@@ -1148,8 +1371,10 @@ class TopLevelEmitter:
1148
1371
 
1149
1372
  # Reset indicators and series for the real backtest run
1150
1373
  lines.append("")
1151
- for site in self.ctx.ta_call_sites:
1152
- if getattr(site, "is_static", False):
1374
+ for _ti, site in enumerate(self.ctx.ta_call_sites):
1375
+ if _ti in self._dead_ta_indices:
1376
+ continue
1377
+ if self._ta_site_uses_precalc(site):
1153
1378
  resolved = [self._resolve_known(a) for a in site.ctor_args]
1154
1379
  safe_resolved = []
1155
1380
  for r in resolved:
@@ -1158,6 +1383,13 @@ class TopLevelEmitter:
1158
1383
 
1159
1384
  for field_name in sorted(self.ctx.series_bar_fields):
1160
1385
  lines.append(f" _s_{field_name}.clear();")
1386
+ for safe in replayed_source_series:
1387
+ lines.append(f" {safe}.clear();")
1388
+ if self._script_has_input_source():
1389
+ lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
1390
+ lines.append(" _src_close_.clear(); _src_volume_.clear();")
1391
+ lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
1392
+ lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
1161
1393
 
1162
1394
  lines.append("")
1163
1395
  lines.append(" _use_precalc = true;")
@@ -206,6 +206,23 @@ class InputHelper:
206
206
  default = self._get_input_default(node)
207
207
  default_cpp = self._visit_expr(default) if default is not None else "0"
208
208
  getter = self._input_type_to_getter(func_name, namespace)
209
+ # The generic ``input(...)`` overload is typed by its defval in Pine
210
+ # v6 (an int default yields an int input). The static getter table
211
+ # cannot see the default, so infer the getter from the default's
212
+ # literal type here. This matters for TA lengths: ``input(15)`` must
213
+ # route to ``get_input_int`` so the RMA/EMA ctor receives an int.
214
+ if func_name == "input" and namespace is None:
215
+ if isinstance(default, BoolLiteral):
216
+ getter = "get_input_bool"
217
+ elif isinstance(default, NumberLiteral):
218
+ if isinstance(default.value, bool):
219
+ getter = "get_input_bool"
220
+ elif isinstance(default.value, int):
221
+ getter = "get_input_int"
222
+ else:
223
+ getter = "get_input_double"
224
+ elif isinstance(default, StringLiteral):
225
+ getter = "get_input_string"
209
226
  default_cpp = self._coerce_string_input_default(getter, default_cpp)
210
227
  return f'{getter}("{title}", {default_cpp})'
211
228