@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",
@@ -32,6 +32,10 @@ from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase
32
32
  from ..limits import TimeBudget, iter_ast_nodes
33
33
  from ..security_contexts import PASS_WARNINGS_ANNOTATION, context_key, reads_bar_series
34
34
  from ..session_reads import emitted_session_reads
35
+ from ..collection_history import (
36
+ COLLECTION_HISTORY_ANNOTATION, LEGACY, REFUSED, SUPPORTED,
37
+ CollectionHistoryChecker, HistoryRead,
38
+ )
35
39
  from ..method_binding import (
36
40
  BoundMethodArgs,
37
41
  MethodBindError,
@@ -431,6 +435,13 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
431
435
  # pass can tell borrowed clones apart from a dead function's own
432
436
  # sites (see contracts.TACallSite.owner_func).
433
437
  self._enclosing_func_names: list[str] = []
438
+ # Array and matrix history reads (``a[1]``), each decided where the
439
+ # visit meets it (pineforge_codegen/collection_history.py), and the
440
+ # declarations writing each class member, by name (a sibling block's
441
+ # declaration closes another one's history).
442
+ self._collection_history_reads: list[HistoryRead] = []
443
+ self._collection_history_checker: CollectionHistoryChecker | None = None
444
+ self._member_writer_decls: dict[str, set[int]] = {}
434
445
  # Exact TA targets borrowed through nested callable edges while visiting
435
446
  # the current callable body. Constructor templates are tracked
436
447
  # separately in ``_func_ta_ctor_args`` because state ownership also
@@ -552,6 +563,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
552
563
  self._ensure_pine_v6()
553
564
  self._check_direct_terminal_array_element_callee_shadows()
554
565
  self._visit(self._ast)
566
+ collection_history = self._check_collection_history()
555
567
  self._check_direct_terminal_array_temporary_cycles()
556
568
  self._register_resolved_direct_terminal_array_forward_calls()
557
569
  self._refresh_direct_terminal_array_temporary_returns()
@@ -652,6 +664,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
652
664
  for owner, names in self._func_var_storage_names.items()
653
665
  },
654
666
  func_series_vars=self._func_series_vars,
667
+ collection_history=collection_history,
655
668
  func_global_history_reads={
656
669
  owner: list(names)
657
670
  for owner, names in self._func_global_history_reads.items()
@@ -4345,6 +4358,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
4345
4358
  self._symbols.define(sym)
4346
4359
  setattr(sym, "_pf_decl_node_id", id(node))
4347
4360
  setattr(sym, "_pf_decl_binding_name", node.name)
4361
+ self._note_member_writer(sym, node.name, node)
4348
4362
  if (type_spec is None
4349
4363
  and self._collection_scope_stack
4350
4364
  and self._block_node_stack
@@ -4647,6 +4661,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
4647
4661
  self._symbols.define(sym)
4648
4662
  setattr(sym, "_pf_decl_node_id", id(node))
4649
4663
  setattr(sym, "_pf_decl_binding_name", name)
4664
+ self._note_member_writer(sym, name, node)
4650
4665
 
4651
4666
  if (self._collection_scope_stack
4652
4667
  and self._block_node_stack
@@ -6174,6 +6189,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
6174
6189
  if self._type_spec_contains_map(object_spec):
6175
6190
  assert object_spec is not None
6176
6191
  self._reject_unsupported_map_history(object_spec, node)
6192
+ if (object_spec is not None
6193
+ and object_spec.kind in ("array", "matrix")):
6194
+ # A parameter given an array or a matrix: its history is a
6195
+ # parameter's, which PineForge does not keep; the earlier
6196
+ # lowering stays where it compiled (collection_history.py).
6197
+ # A selection of one is an expression's history.
6198
+ checker = self._history_checker()
6199
+ if isinstance(node.object, Identifier):
6200
+ decision = checker.decide_parameter_history(node, object_spec)
6201
+ if decision.outcome == REFUSED:
6202
+ self._collection_history_raise(decision, node)
6203
+ else:
6204
+ self._refuse_collection_expression_history(node, object_spec)
6177
6205
 
6178
6206
  # Propagate concrete caller specs through wrapper calls. Each edge is
6179
6207
  # identity-keyed from the definition pass, so a local that shadows a
@@ -6428,8 +6456,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
6428
6456
  def _visit_Subscript(self, node: Subscript) -> PineType:
6429
6457
  obj_type = self._visit(node.object)
6430
6458
  self._visit(node.index)
6459
+ self._refuse_collection_field_history(node)
6431
6460
 
6432
6461
  object_spec = self._history_receiver_type_spec(node.object)
6462
+ self._refuse_collection_expression_history(node, object_spec)
6433
6463
  if self._type_spec_contains_map(object_spec):
6434
6464
  assert object_spec is not None
6435
6465
  self._reject_unsupported_map_history(object_spec, node)
@@ -6461,7 +6491,17 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
6461
6491
  "Series buffer for that binding.",
6462
6492
  node.loc,
6463
6493
  )
6464
- if getattr(sym, "type_spec", None) is None or sym.type_spec.kind not in ("array", "map"):
6494
+ # An array's or a matrix's history: copies kept beside
6495
+ # the variable, not a Series of it (collection_history.py);
6496
+ # a legacy matrix parameter's read keeps the registration
6497
+ # below, as it had.
6498
+ keeps_series = not (
6499
+ getattr(sym, "type_spec", None) is not None
6500
+ and sym.type_spec.kind in ("array", "matrix")
6501
+ and not self._note_collection_history_read(node, name, sym)
6502
+ )
6503
+ if keeps_series and (getattr(sym, "type_spec", None) is None
6504
+ or sym.type_spec.kind not in ("array", "map")):
6465
6505
  exact_member = getattr(sym, "_pf_var_member_name", None)
6466
6506
  if exact_member is not None:
6467
6507
  self._series_var_members.add(exact_member)
@@ -6510,6 +6550,150 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
6510
6550
 
6511
6551
  return obj_type
6512
6552
 
6553
+ def _refuse_collection_field_history(self, node: Subscript) -> None:
6554
+ """The history of an object's array, matrix or map field: TradingView
6555
+ refuses ``h.xs[1]`` and ``(h.xs)[1]`` alike ("Cannot use the
6556
+ history-referencing operator on fields of user-defined types",
6557
+ CE10290: fixtures/array_history_tv ahist_field_noparen,
6558
+ ahist_field_paren, ahist_mfield) and takes ``(h[1]).xs``. The C++
6559
+ indexed the field's current collection."""
6560
+ field = node.object
6561
+ if not isinstance(field, MemberAccess):
6562
+ return
6563
+ owner = self._history_receiver_type_spec(field.object)
6564
+ if (owner is None or owner.kind != "udt"
6565
+ or owner.name not in self._udt_fields):
6566
+ return
6567
+ spec = (self._udt_field_type_specs.get(owner.name) or {}).get(field.member)
6568
+ if spec is None or spec.kind not in ("array", "matrix", "map"):
6569
+ return
6570
+ receiver = (field.object.name if isinstance(field.object, Identifier)
6571
+ else "object")
6572
+ raise CompileError([Diagnostic(
6573
+ level=Level.ERROR,
6574
+ phase=Phase.ANALYZER,
6575
+ location=node.loc or SourceLocation(self._filename, 1, 1, 1),
6576
+ message=(f"{receiver}.{field.member}[...]: TradingView refuses the "
6577
+ "history-referencing operator on fields of user-defined "
6578
+ "types (CE10290)."),
6579
+ hint=(f"Read the field of the object's history: "
6580
+ f"({receiver}[1]).{field.member}."),
6581
+ )])
6582
+
6583
+ def _history_checker(self) -> CollectionHistoryChecker:
6584
+ """The script's array and matrix history checker, built when the
6585
+ visit first meets such a read."""
6586
+ if self._collection_history_checker is None:
6587
+ self._collection_history_checker = CollectionHistoryChecker(
6588
+ self._ast, self._collection_history_name_kind)
6589
+ return self._collection_history_checker
6590
+
6591
+ def _collection_history_name_kind(self, name: str) -> str | None:
6592
+ """``array`` / ``matrix`` / ``map`` / ``scalar`` for a script
6593
+ variable, else None (collection_history.CollectionHistoryChecker)."""
6594
+ sym = self._symbols.global_scope.symbols.get(name)
6595
+ if sym is None:
6596
+ return None
6597
+ spec = getattr(sym, "type_spec", None)
6598
+ if spec is not None and spec.kind in ("array", "matrix", "map"):
6599
+ return spec.kind
6600
+ if sym.pine_type in (PineType.INT, PineType.FLOAT, PineType.BOOL,
6601
+ PineType.STRING, PineType.COLOR):
6602
+ return "scalar"
6603
+ return None
6604
+
6605
+ def _note_member_writer(self, sym, name: str, node) -> None:
6606
+ """A declaration writing the class member of its name (the script's
6607
+ top level and its blocks): a sibling block's declaration of the name
6608
+ closes another one's array history before it writes
6609
+ (codegen/collection_history.py)."""
6610
+ if sym.scope == "global":
6611
+ self._member_writer_decls.setdefault(name, set()).add(id(node))
6612
+
6613
+ def _collection_history_raise(self, decision, node) -> None:
6614
+ raise CompileError([Diagnostic(
6615
+ level=Level.ERROR,
6616
+ phase=Phase.ANALYZER,
6617
+ location=(getattr(decision.node, "loc", None) or node.loc
6618
+ or SourceLocation(self._filename, 1, 1, 1)),
6619
+ message=decision.message,
6620
+ hint=decision.hint,
6621
+ )])
6622
+
6623
+ def _refuse_collection_expression_history(self, node: Subscript, spec) -> None:
6624
+ """The history of an expression whose value is an array or a matrix
6625
+ (a call's result, a selection): TradingView reads the copy the
6626
+ expression produced k executions back (fixtures/array_history_tv
6627
+ ahist_fn ``c1``), which PineForge does not keep. A use whose earlier
6628
+ lowering compiled keeps it; one only a collection fits never compiled
6629
+ and is refused (collection_history.py)."""
6630
+ if (isinstance(node.object, Identifier) or spec is None
6631
+ or spec.kind not in ("array", "matrix")):
6632
+ return
6633
+ checker = self._history_checker()
6634
+ decided = checker.decision_for(node) is not None
6635
+ decision = checker.decide_expression(node, spec)
6636
+ if decision.outcome == REFUSED:
6637
+ self._collection_history_raise(decision, node)
6638
+ if decision.outcome == LEGACY and decision.use == "element" and not decided:
6639
+ self._warn(
6640
+ "(...)[...] reads an element of the selected current array in "
6641
+ "PineForge: no history of a selection is kept, where TradingView "
6642
+ "reads the array it selected that many bars back.",
6643
+ node.loc,
6644
+ )
6645
+
6646
+ def _note_collection_history_read(self, node: Subscript, name: str, sym) -> bool:
6647
+ """Decide ``a[k]`` of an array or a matrix variable
6648
+ (collection_history.py): annotate a supported read, refuse a refused
6649
+ one, and keep a legacy one's earlier lowering. True where that
6650
+ lowering registered a series (a matrix parameter's), which the caller
6651
+ then does as it did."""
6652
+ decl_node_id = getattr(sym, "_pf_decl_node_id", None)
6653
+ read = HistoryRead(
6654
+ node=node,
6655
+ name=name,
6656
+ kind=sym.type_spec.kind,
6657
+ spec=sym.type_spec,
6658
+ in_callable=bool(self._enclosing_func_names),
6659
+ is_parameter=sym.scope != "global" and decl_node_id is None,
6660
+ is_global=sym.scope == "global",
6661
+ is_var=bool(sym.is_var),
6662
+ decl_node_id=decl_node_id,
6663
+ scope_name=sym.scope or "",
6664
+ )
6665
+ decision = self._history_checker().decide(read)
6666
+ if node.annotations and COLLECTION_HISTORY_ANNOTATION in node.annotations:
6667
+ node.annotations = dict(node.annotations)
6668
+ del node.annotations[COLLECTION_HISTORY_ANNOTATION]
6669
+ if decision.outcome == REFUSED:
6670
+ self._collection_history_raise(decision, node)
6671
+ if decision.outcome == SUPPORTED:
6672
+ node.annotations = dict(node.annotations or {})
6673
+ node.annotations[COLLECTION_HISTORY_ANNOTATION] = {
6674
+ "var": name, "kind": read.kind, "use": decision.use,
6675
+ "spec": read.spec, "member": None,
6676
+ }
6677
+ if not any(r.node is node for r in self._collection_history_reads[-1:]):
6678
+ self._collection_history_reads.append(read)
6679
+ return False
6680
+ return decision.outcome == LEGACY and decision.use == "series"
6681
+
6682
+ def _collection_history_is_supported(self, node: Subscript) -> bool:
6683
+ """Whether the checker supports the history read ``node`` (decided
6684
+ where the visit met it; a read not met yet keeps its old typing)."""
6685
+ checker = self._collection_history_checker
6686
+ decision = checker.decision_for(node) if checker is not None else None
6687
+ return decision is not None and decision.outcome == SUPPORTED
6688
+
6689
+ def _check_collection_history(self) -> dict:
6690
+ """The declarations whose history the supported reads need, keyed
6691
+ by their members (pineforge_codegen/collection_history.py)."""
6692
+ if not self._collection_history_reads:
6693
+ return {}
6694
+ return self._history_checker().history_variables(
6695
+ self._collection_history_reads, self._member_writer_decls)
6696
+
6513
6697
  def _note_function_global_history_read(
6514
6698
  self, node: Subscript, name: str, sym) -> None:
6515
6699
  """Record ``x[k]`` in a plain UDF body on a script variable.
@@ -6807,6 +6991,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
6807
6991
  if owner_spec is not None and owner_spec.kind == "udt" and owner_spec.name:
6808
6992
  field_spec = (self._udt_field_type_specs.get(owner_spec.name) or {}).get(node.member)
6809
6993
  if field_spec is not None:
6994
+ # The receiver is read too: ``(o[1]).inner.v`` reads the
6995
+ # history of ``o`` (fixtures/udt_history_tv udth_ref),
6996
+ # which only its visit registers.
6997
+ self._visit(node.object)
6810
6998
  return self._type_hint_to_pine(str(field_spec))
6811
6999
  self._visit(node.object)
6812
7000
  # strategy.closedtrades.* and strategy.opentrades.* return types
@@ -337,6 +337,9 @@ class AnalyzerContext:
337
337
  # per read. TradingView keeps that history per call site, so codegen gives
338
338
  # each emitted body its own chart-clocked buffer of the variable.
339
339
  func_global_history_reads: dict = field(default_factory=dict)
340
+ # Array and matrix variables whose history the script reads, by name
341
+ # (pineforge_codegen/collection_history.py HistoryVariable).
342
+ collection_history: dict = field(default_factory=dict)
340
343
  func_global_history_nodes: dict = field(default_factory=dict)
341
344
  # FuncDef owner -> declaration-bound non-persistent history-local names.
342
345
  # Parameters are deliberately excluded so a same-named qualified
@@ -347,14 +347,25 @@ class TypeHelper:
347
347
  if isinstance(value, Subscript):
348
348
  # Pine's history operator preserves the value type: a
349
349
  # ``Series<line>`` read such as ``h[1]`` is a scalar ``line``
350
- # handle, not the legacy numeric fallback. Keep this refinement
351
- # drawing-only; collection subscripts have separate array/map
352
- # semantics and primitive history inference already flows through
353
- # PineType in ``_visit_Subscript``.
350
+ # handle and ``c[1]`` of a user-defined object the reference ``c``
351
+ # held (fixtures/udt_history_tv), not the legacy numeric fallback.
352
+ # Collection subscripts have separate array/map semantics and
353
+ # primitive history inference already flows through PineType in
354
+ # ``_visit_Subscript``.
354
355
  receiver_spec = self._type_spec_from_expr(value.object)
355
356
  if (receiver_spec is not None
356
357
  and receiver_spec.kind == "udt"
357
- and receiver_spec.name in _DRAWING_TYPE_NAMES):
358
+ and (receiver_spec.name in _DRAWING_TYPE_NAMES
359
+ or receiver_spec.name in self._udt_fields)):
360
+ return receiver_spec
361
+ if (receiver_spec is not None
362
+ and receiver_spec.kind in ("array", "matrix")
363
+ and isinstance(value.object, Identifier)
364
+ and self._collection_history_is_supported(value)):
365
+ # An array's or a matrix's history is a collection of its
366
+ # type (fixtures/array_history_tv; pineforge_codegen/
367
+ # collection_history.py); a legacy read keeps the element
368
+ # typing it had.
358
369
  return receiver_spec
359
370
  return None
360
371
  if isinstance(value, IfStmt):
@@ -6,6 +6,13 @@ from typing import Any
6
6
  from pineforge_codegen.errors import SourceLocation
7
7
 
8
8
 
9
+ # A MemberAccess (a field, or a method call's callee) written straight after a
10
+ # history reference, ``c[1].v`` / ``b[1].get_top()``: TradingView refuses it
11
+ # (CE10011 / CE10010) and takes ``(c[1]).v``, which parses to the same nodes.
12
+ # Set by the parser, refused by the support checker.
13
+ HISTORY_MEMBER_ANNOTATION = "pf_history_member"
14
+
15
+
9
16
  # ---------------------------------------------------------------------------
10
17
  # Base class
11
18
  # ---------------------------------------------------------------------------
@@ -145,6 +145,7 @@ TA_TUPLE_RESULT_TYPES = {
145
145
 
146
146
  # CPP_RESERVED + the NamingHelper mixin are pulled in from helpers.py so the
147
147
  # small naming/walk utilities can be shared with future visitor mixins.
148
+ from .collection_history import CollectionHistoryEmitter
148
149
  from .helpers import (
149
150
  CPP_RESERVED, INLINE_HISTORY_KINDS, SESSION_FLAG_MEMBERS, NamingHelper,
150
151
  cpp_code_only, na_preserving_int_cast, pine_truth_cast,
@@ -194,7 +195,7 @@ from .drawing import DrawingVisitor
194
195
  # CodeGen class
195
196
  # ---------------------------------------------------------------------------
196
197
 
197
- class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
198
+ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, CollectionHistoryEmitter, NamingHelper):
198
199
  """Generate C++ from an AnalyzerContext (visitor pattern).
199
200
 
200
201
  Mixin chain (Python MRO is left-to-right; method names are
@@ -1945,7 +1946,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1945
1946
  )
1946
1947
  elif (orig_safe in self._series_var_member_names
1947
1948
  or vname in self.ctx.series_vars):
1948
- lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
1949
+ # A history-read var object keeps its references, as
1950
+ # its base member does.
1951
+ udt_t = udt_spec.name if udt_spec is not None else member_udt_type
1952
+ element = (self._safe_name(udt_t) if udt_t in self._udt_defs
1953
+ else cpp_type)
1954
+ lines.append(f" Series<{element}> {cloned_safe}{series_suffix};")
1949
1955
  elif collection_spec is not None:
1950
1956
  lines.append(
1951
1957
  f" {self._type_spec_to_cpp(collection_spec)} {cloned_safe};"
@@ -1987,8 +1993,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1987
1993
  lines.append(f" {cpp_type} {cloned_safe};")
1988
1994
  return
1989
1995
  # Non-var series var
1990
- if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
1991
- cpp_type = self._series_type_for(orig_safe)
1996
+ raw_names = [n for n in self.ctx.series_vars if self._safe_name(n) == orig_safe]
1997
+ if raw_names:
1998
+ # A history-read object or drawing (a receiver ``this`` spelled
1999
+ # ``pf_safe_this``) holds handles, as its base member does.
2000
+ handles = {self._series_handle_cpp_type(n) for n in raw_names}
2001
+ cpp_type = (handles.pop() if len(handles) == 1 and None not in handles
2002
+ else self._series_type_for(orig_safe))
1992
2003
  lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
1993
2004
  else:
1994
2005
  lines.append(f" double {cloned_safe} = 0.0;")
@@ -2112,6 +2123,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2112
2123
  declared_spec = self._type_spec_from_hint_name(stmt.type_hint)
2113
2124
  elif isinstance(stmt.value, (Ternary, IfStmt, SwitchStmt)):
2114
2125
  declared_spec = self._type_spec_from_expr(stmt.value)
2126
+ elif self._roots_at_collection_history(stmt.value):
2127
+ # A copy of an array's or a matrix's history, or a built-in's
2128
+ # result on one (``pm = m[1]``, ``c = (m[1]).copy()``).
2129
+ declared_spec = self._type_spec_from_expr(stmt.value)
2130
+ if declared_spec is not None and declared_spec.kind == "array":
2131
+ self._array_vars.add(stmt.name)
2132
+ self._collection_types.setdefault(stmt.name, declared_spec)
2133
+ continue
2115
2134
  else:
2116
2135
  continue
2117
2136
  if declared_spec is None or declared_spec.kind != "matrix":
@@ -2225,10 +2244,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2225
2244
 
2226
2245
  Walks chained ``FuncCall`` receivers (e.g. ``m.transpose().copy()``)
2227
2246
  until it finds an ``Identifier`` so the source matrix's TypeSpec can
2228
- be propagated through fluent call chains.
2247
+ be propagated through fluent call chains. A history read (``m[1]``)
2248
+ is the variable's: its copy has the variable's type.
2229
2249
  """
2230
2250
  if not isinstance(call_node, FuncCall):
2231
2251
  return None
2252
+
2253
+ def unwrap(node):
2254
+ if isinstance(node, Subscript) and isinstance(node.object, Identifier):
2255
+ return node.object
2256
+ return node
2257
+
2232
2258
  callee = call_node.callee
2233
2259
  # Method form: m.method(...) — possibly chained: m.foo().bar()
2234
2260
  if isinstance(callee, MemberAccess):
@@ -2240,12 +2266,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2240
2266
  obj = inner_callee.object
2241
2267
  else:
2242
2268
  break
2269
+ obj = unwrap(obj)
2243
2270
  if isinstance(obj, Identifier):
2244
2271
  if obj.name != "matrix":
2245
2272
  return obj.name
2246
2273
  # matrix.method(m, ...) functional form
2247
2274
  if call_node.args:
2248
- first = call_node.args[0]
2275
+ first = unwrap(call_node.args[0])
2249
2276
  if isinstance(first, Identifier):
2250
2277
  return first.name
2251
2278
  return None
@@ -3623,13 +3650,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3623
3650
  if len(states) > 1
3624
3651
  }
3625
3652
 
3653
+ # A history parameter holding a user-defined object or a drawing keeps
3654
+ # the references its calls passed (``_series_param_element_cpp_type``).
3655
+ handle_cpp_types = {
3656
+ *DRAWING_TYPE_TO_CPP.values(),
3657
+ *(self._safe_name(name) for name in self._udt_defs),
3658
+ }
3659
+
3626
3660
  def register_one(
3627
3661
  kind: str,
3628
3662
  source_key: tuple,
3629
3663
  cpp_type: str,
3630
3664
  context: str | None,
3631
3665
  ) -> None:
3632
- if cpp_type not in ("double", "int", "int64_t", "bool"):
3666
+ if (cpp_type not in ("double", "int", "int64_t", "bool")
3667
+ and cpp_type not in handle_cpp_types):
3633
3668
  cpp_type = "double"
3634
3669
  key = (kind, *source_key, context)
3635
3670
  if key in self._inline_history_member_by_key:
@@ -3721,6 +3756,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3721
3756
  lexical.update(self._func_collection_types.get(owner, {}))
3722
3757
  return lexical
3723
3758
 
3759
+ def owner_typed(owner: str | None, compute):
3760
+ """``compute()`` with the owner's parameter types bound, as the
3761
+ emitter binds them: a history read of a typed parameter's field
3762
+ (``f(Outer p) => p.inner[1]``) types its Series by the field."""
3763
+ previous = self.__dict__.get("_current_func_param_specs")
3764
+ self._current_func_param_specs = owner_lexical_specs(owner)
3765
+ try:
3766
+ return compute()
3767
+ finally:
3768
+ if previous is None:
3769
+ self.__dict__.pop("_current_func_param_specs", None)
3770
+ else:
3771
+ self._current_func_param_specs = previous
3772
+
3773
+ def history_cpp_type(expr, owner: str | None) -> str:
3774
+ """``_history_value_cpp_type`` of ``expr``, a reference's handle
3775
+ resolved with the owner's parameters bound; any other value keeps
3776
+ the inference every earlier build registered."""
3777
+ return (owner_typed(owner, lambda: self._reference_cpp_type(expr))
3778
+ or self._infer_type(expr))
3779
+
3724
3780
  def plain_udf_info(call: FuncCall):
3725
3781
  if not isinstance(call.callee, Identifier):
3726
3782
  return None
@@ -3846,7 +3902,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3846
3902
  owner = owner_by_node.get(id(node))
3847
3903
  if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
3848
3904
  register(
3849
- "hist_call", (id(node),), self._infer_type(node.object), owner
3905
+ "hist_call", (id(node),), history_cpp_type(node.object, owner),
3906
+ owner,
3850
3907
  )
3851
3908
  elif (isinstance(node, Subscript)
3852
3909
  and self._is_session_flag(node.object)):
@@ -3865,9 +3922,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
3865
3922
  self._session_call_flags.setdefault(owner, set()).add(flag)
3866
3923
  register("session_call", (owner, flag), "bool", owner)
3867
3924
  elif (isinstance(node, Subscript)
3868
- and self._is_compound_history_object(node.object)):
3925
+ and (self._is_compound_history_object(node.object)
3926
+ or (isinstance(node.object, (Ternary, MemberAccess))
3927
+ and owner_typed(owner, lambda: self._reference_cpp_type(
3928
+ node.object)) is not None))):
3869
3929
  register(
3870
- "hist_call", (id(node),), self._infer_type(node.object), owner
3930
+ "hist_call", (id(node),), history_cpp_type(node.object, owner),
3931
+ owner,
3871
3932
  )
3872
3933
 
3873
3934
  if not isinstance(node, FuncCall):
@@ -4021,6 +4082,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4021
4082
  ("fn_global_hist", id(fi.node), name, self._current_instance_name)
4022
4083
  )
4023
4084
 
4085
+ def _reference_cpp_type(self, expr) -> str | None:
4086
+ """The handle type of an expression whose value is a user-defined
4087
+ object or a drawing reference (``Cell``, ``Box``), else None."""
4088
+ spec = self._type_spec_from_expr(expr)
4089
+ if (spec is None or spec.kind != "udt"
4090
+ or not (spec.name in DRAWING_TYPE_TO_CPP
4091
+ or spec.name in self._udt_defs)):
4092
+ return None
4093
+ return DRAWING_TYPE_TO_CPP.get(spec.name) or self._safe_name(spec.name)
4094
+
4095
+ def _registered_history_handle(self, node: Subscript) -> str | None:
4096
+ """The handle type of the synthetic history Series the prepass
4097
+ registered for ``node`` (``_prepare_inline_history_members``), else
4098
+ None: emission reads the type the member was declared with."""
4099
+ member = self._inline_history_member_by_key.get(
4100
+ ("hist_call", id(node), self._current_instance_name))
4101
+ if member is None:
4102
+ return None
4103
+ handles = {*DRAWING_TYPE_TO_CPP.values(),
4104
+ *(self._safe_name(name) for name in self._udt_defs)}
4105
+ for info in self._inline_history_members:
4106
+ if info["member_name"] == member:
4107
+ return info["cpp_type"] if info["cpp_type"] in handles else None
4108
+ return None
4109
+
4110
+ def _history_value_cpp_type(self, expr) -> str:
4111
+ """The element type of the synthetic Series that keeps ``expr``'s
4112
+ history: a reference's handle (TradingView's history of such an
4113
+ expression is the reference it produced at its previous evaluation:
4114
+ fixtures/udt_history_tv udth_expr), else the scalar it infers."""
4115
+ return self._reference_cpp_type(expr) or self._infer_type(expr)
4116
+
4024
4117
  def _is_compound_history_object(self, node) -> bool:
4025
4118
  """Whether ``node[k]`` is history on an operator expression or a
4026
4119
  ``session.*`` flag.
@@ -4038,6 +4131,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4038
4131
  """
4039
4132
  if self._is_session_flag(node):
4040
4133
  return True
4134
+ if (isinstance(node, (Ternary, MemberAccess))
4135
+ and self._reference_cpp_type(node) is not None):
4136
+ # ``(c ? a : b)[1]`` and an object-typed field's ``o.inner[1]``:
4137
+ # the reference the expression produced at its previous
4138
+ # evaluation (fixtures/udt_history_tv udth_expr).
4139
+ return True
4041
4140
  return (isinstance(node, (BinOp, UnaryOp, Ternary))
4042
4141
  and self._infer_type(node) in ("double", "int", "int64_t", "bool"))
4043
4142
 
@@ -4614,6 +4713,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4614
4713
  # per-callsite instances are declared below inside GeneratedStrategy
4615
4714
  # and therefore join the automatic COOF checkpoint inventory.
4616
4715
  self._emit_lazy_source_clock_helper(lines)
4716
+ # The history of an array or a matrix variable: its support types
4717
+ # (pineforge_codegen/collection_history.py), emitted only for a script
4718
+ # that reads one.
4719
+ self._emit_collection_history_helper(lines)
4617
4720
  # The chart-bar session type, inserted here once the class is lowered
4618
4721
  # and known to call it (codegen/session_market.py).
4619
4722
  _session_market_at = len(lines)
@@ -4847,9 +4950,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4847
4950
  continue
4848
4951
  callable_udt_spec = self._callable_var_udt_spec(name)
4849
4952
  if callable_udt_spec is not None:
4850
- lines.append(
4851
- f" {self._type_spec_to_cpp(callable_udt_spec)} {safe};"
4852
- )
4953
+ handle_cpp = self._type_spec_to_cpp(callable_udt_spec)
4954
+ if safe in self._series_var_member_names:
4955
+ # A history-read var object: its history holds the
4956
+ # references it held (``_series_handle_cpp_type``).
4957
+ lines.append(f" Series<{handle_cpp}> {safe}{_mbb};")
4958
+ else:
4959
+ lines.append(f" {handle_cpp} {safe};")
4853
4960
  continue
4854
4961
  # Detect array vars from init expression. Guard the substring
4855
4962
  # heuristic against a UDT constructor that merely WRAPS array.new /
@@ -4941,7 +5048,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4941
5048
  udt_type = udt_name
4942
5049
  break
4943
5050
  if udt_type:
4944
- lines.append(f" {self._safe_name(udt_type)} {safe};")
5051
+ if safe in self._series_var_member_names:
5052
+ # A history-read var object: its history holds the
5053
+ # references it held (``_series_handle_cpp_type``).
5054
+ lines.append(
5055
+ f" Series<{self._safe_name(udt_type)}> {safe}{_mbb};")
5056
+ else:
5057
+ lines.append(f" {self._safe_name(udt_type)} {safe};")
4945
5058
  continue
4946
5059
  cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
4947
5060
  # Promote int->int64_t when init RHS is an int64-returning builtin
@@ -5150,6 +5263,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5150
5263
  lines.append(" DrawingArena<LabelRec> _pf_labels_{_PF_DRAWING_UNBOUNDED};")
5151
5264
  lines.append(" DrawingArena<LinefillRec> _pf_linefills_{_PF_DRAWING_UNBOUNDED};")
5152
5265
 
5266
+ # 8e. The copies an array or a matrix variable's executions left
5267
+ # (pineforge_codegen/collection_history.py).
5268
+ for variable in self._collection_history_variables():
5269
+ lines.append(
5270
+ f" _PFCollectionHistory<{self._collection_history_cpp_type(variable)}> "
5271
+ f"{self._collection_history_member(variable)}"
5272
+ f"{self._collection_history_capacity(variable)};"
5273
+ )
5274
+
5153
5275
  # 9. _var_initialized flag
5154
5276
  if self.ctx.var_members:
5155
5277
  lines.append(" bool _var_initialized = false;")