@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.
@@ -42,6 +42,7 @@ from ..ast_nodes import (
42
42
  MemberAccess, MethodDef, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
43
43
  Subscript, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
44
44
  )
45
+ from ..collection_history import history_annotation
45
46
  from ..errors import Phase
46
47
  from ..external_requests import UNPINNED_ANNOTATION
47
48
  from ..limits import iter_ast_nodes
@@ -1545,14 +1546,26 @@ class TypeInferer:
1545
1546
  return TypeSpec.primitive(primitive_name)
1546
1547
  return None
1547
1548
  if isinstance(node, Subscript):
1548
- # History access preserves the scalar drawing-handle type. This
1549
- # is intentionally not generalized to arrays/maps: their
1550
- # subscripting rules are handled by the collection paths below.
1549
+ # History access preserves the drawing-handle and user-object
1550
+ # type: ``c[1]`` is the reference ``c`` held one bar back
1551
+ # (fixtures/udt_history_tv). This is intentionally not
1552
+ # generalized to arrays/maps: their subscripting rules are handled
1553
+ # by the collection paths below.
1551
1554
  receiver_spec = self._type_spec_from_expr(node.object)
1552
1555
  if (receiver_spec is not None
1553
1556
  and receiver_spec.kind == "udt"
1554
- and receiver_spec.name in DRAWING_TYPE_TO_CPP):
1557
+ and (receiver_spec.name in DRAWING_TYPE_TO_CPP
1558
+ or receiver_spec.name in self._udt_defs)):
1555
1559
  return receiver_spec
1560
+ annotation = history_annotation(node)
1561
+ if annotation is not None:
1562
+ # An array's or a matrix's history is a collection of its
1563
+ # type (pineforge_codegen/collection_history.py), the
1564
+ # analyzer's when no declaration registered one yet.
1565
+ if (receiver_spec is not None
1566
+ and receiver_spec.kind in ("array", "matrix")):
1567
+ return receiver_spec
1568
+ return annotation["spec"]
1556
1569
  return None
1557
1570
  if isinstance(node, Ternary):
1558
1571
  true_spec = self._type_spec_from_expr(node.true_val)
@@ -1851,13 +1864,18 @@ class TypeInferer:
1851
1864
  if return_spec is not None:
1852
1865
  return return_spec
1853
1866
  if recv_spec is not None and recv_spec.kind == "matrix":
1854
- if func_name in MATRIX_RETURNING_METHODS:
1867
+ # A method on a matrix's history names its method only
1868
+ # in the member (``_resolve_callee`` reports none).
1869
+ method = (member_name
1870
+ if history_annotation(node.callee.object) is not None
1871
+ else func_name)
1872
+ if method in MATRIX_RETURNING_METHODS:
1855
1873
  return recv_spec
1856
- if func_name in ("row", "col"):
1874
+ if method in ("row", "col"):
1857
1875
  return TypeSpec.array(recv_spec.element)
1858
- if func_name == "get":
1876
+ if method == "get":
1859
1877
  return recv_spec.element
1860
- if func_name == "eigenvalues":
1878
+ if method == "eigenvalues":
1861
1879
  return TypeSpec.array(TypeSpec.primitive("float"))
1862
1880
  # Drawing method-form: ``a.copy()`` -> same handle type;
1863
1881
  # ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
@@ -2206,6 +2224,9 @@ class TypeInferer:
2206
2224
  # init RHS is such a builtin, so also match the builtin name directly.
2207
2225
  if name in INT64_BUILTINS:
2208
2226
  return "int64_t"
2227
+ handle = self._series_handle_cpp_type(name)
2228
+ if handle is not None:
2229
+ return handle
2209
2230
  sym = self._variable_symbol(name)
2210
2231
  # A float or bool an epoch reaches keeps its type: the analyzer types
2211
2232
  # every request.security value float (a double holds an epoch
@@ -2225,6 +2246,51 @@ class TypeInferer:
2225
2246
  return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
2226
2247
  return "double"
2227
2248
 
2249
+ def _series_handle_type_name(self, name: str) -> str | None:
2250
+ """The user-defined or drawing type (``Cell``, ``box``) of the
2251
+ history-read variable ``name``, else None. Such a variable holds a
2252
+ reference, and its history the references it held: ``(c[1]).v`` reads
2253
+ the object ``c`` held one bar back as it is now (fixtures/
2254
+ udt_history_tv; Pine v6 User Manual, "Type system": value vs.
2255
+ reference types). A buffer is keyed by spelling across scopes, so
2256
+ every scope's history-read variable of that spelling must hold the
2257
+ same type; any other variable keeps the buffer it always had.
2258
+ Cached per name."""
2259
+ cache = self.__dict__.setdefault("_series_handle_type_cache", {})
2260
+ if name in cache:
2261
+ return cache[name]
2262
+ found: set[str] = set()
2263
+ for scope in self.ctx.symbols.all_scopes:
2264
+ sym = scope.symbols.get(name)
2265
+ if sym is None or not getattr(sym, "is_series", False):
2266
+ continue
2267
+ spec = getattr(sym, "type_spec", None)
2268
+ if (spec is None or spec.kind != "udt"
2269
+ or not (spec.name in DRAWING_TYPE_TO_CPP
2270
+ or spec.name in self._udt_defs)):
2271
+ found = set()
2272
+ break
2273
+ found.add(spec.name)
2274
+ cache[name] = next(iter(found)) if len(found) == 1 else None
2275
+ return cache[name]
2276
+
2277
+ def _series_handle_cpp_type(self, name: str) -> str | None:
2278
+ """The handle such a variable stores per bar (``Series<Cell>``,
2279
+ ``Series<Box>``; a na reference is the handle's ``T{}``), else
2280
+ None (``_series_handle_type_name``)."""
2281
+ udt = self._series_handle_type_name(name)
2282
+ if udt is None:
2283
+ return None
2284
+ return DRAWING_TYPE_TO_CPP.get(udt) or self._safe_name(udt)
2285
+
2286
+ def _series_handle_target(self, name: str) -> str | None:
2287
+ """``_visit_rhs_value``'s target for a value stored in such a
2288
+ variable's history: the drawing handle, or the user type's name."""
2289
+ udt = self._series_handle_type_name(name)
2290
+ if udt is None:
2291
+ return None
2292
+ return DRAWING_TYPE_TO_CPP.get(udt, udt)
2293
+
2228
2294
  def _variable_symbol(self, name: str):
2229
2295
  """The symbol the variable ``name`` resolves to.
2230
2296
 
@@ -2303,6 +2369,18 @@ class TypeInferer:
2303
2369
  declared_spec = (
2304
2370
  declared_specs[index] if index < len(declared_specs) else None
2305
2371
  )
2372
+ # A parameter holding a user-defined object or a drawing keeps the
2373
+ # references its calls passed (``_series_handle_cpp_type``).
2374
+ inferred_specs = list(getattr(func_info, "param_type_specs", ()) or ())
2375
+ for candidate in (
2376
+ declared_spec,
2377
+ inferred_specs[index] if index < len(inferred_specs) else None,
2378
+ ):
2379
+ if (candidate is not None and candidate.kind == "udt"
2380
+ and (candidate.name in DRAWING_TYPE_TO_CPP
2381
+ or candidate.name in self._udt_defs)):
2382
+ return (DRAWING_TYPE_TO_CPP.get(candidate.name)
2383
+ or self._safe_name(candidate.name))
2306
2384
  # A declared type is authoritative across every written call. Only a
2307
2385
  # truly untyped slot may consult the per-callsite specialization map.
2308
2386
  spec = declared_spec
@@ -3302,6 +3380,19 @@ class TypeInferer:
3302
3380
  cpp_type = "int64_t"
3303
3381
  return cpp_type if cpp_type in (*NA_PRESERVING_INT_TYPES, "bool") else None
3304
3382
 
3383
+ def _udt_field_is_double(self, target_node) -> bool:
3384
+ """Whether a UDT field write target is a ``float`` field (a C++
3385
+ ``double``)."""
3386
+ if not isinstance(target_node, MemberAccess):
3387
+ return False
3388
+ owner = self._type_spec_from_expr(target_node.object)
3389
+ if owner is None or owner.kind != "udt" or not owner.name:
3390
+ return False
3391
+ spec = (self._udt_field_type_specs.get(owner.name) or {}).get(
3392
+ target_node.member
3393
+ )
3394
+ return spec is not None and self._type_spec_to_cpp(spec) == "double"
3395
+
3305
3396
  def _coerce_int_slot(self, cpp_val: str, node, target_cpp_type: str | None,
3306
3397
  *, value_is_double: bool | None = None) -> str:
3307
3398
  """Route a value into an ``int``/``int64_t`` slot without losing ``na``.
@@ -3343,6 +3434,30 @@ class TypeInferer:
3343
3434
  return na_preserving_int_cast(cpp_val, target_cpp_type)
3344
3435
  return cpp_val
3345
3436
 
3437
+ def _coerce_double_slot(self, cpp_val: str, node) -> str:
3438
+ """Route a value into a ``double`` that a designated initializer
3439
+ fills (a user-defined record's ``float`` field: ``Cell.new(v =
3440
+ bar_index)``). C++ refuses to narrow a non-constant ``int`` or
3441
+ ``int64_t`` there, which did not compile; TradingView holds the
3442
+ number, an integer na as na (fixtures/array_history_tv uctor_float).
3443
+ A double, an integer literal or an expression of literals (a constant
3444
+ a double holds exactly narrows) and a bare na keep their spelling."""
3445
+ constant = self._pure_int_literal_value(node)
3446
+ if (cpp_val == "na<double>()"
3447
+ or self._INTEGRAL_CPP_TEXT.match(cpp_val)
3448
+ or (constant is not None and abs(constant) <= 2 ** 53)
3449
+ or self._emitted_value_is_double(node)):
3450
+ return cpp_val
3451
+ if cpp_val in {"na<int>()", "na<int64_t>()"}:
3452
+ return "na<double>()"
3453
+ # A bool (TradingView refuses one here: CE10123, CE10173) is 1 or 0,
3454
+ # never na, whatever is_na() of a bool resolves to.
3455
+ return (f"[&](){{ auto _pf_w = ({cpp_val}); "
3456
+ f"if constexpr (std::is_floating_point_v<decltype(_pf_w)> "
3457
+ f"|| std::is_same_v<decltype(_pf_w), bool>) "
3458
+ f"return (double)_pf_w; "
3459
+ f"else return is_na(_pf_w) ? na<double>() : (double)_pf_w; }}()")
3460
+
3346
3461
  def _coerce_int_slot_with_cast(
3347
3462
  self, cpp_val: str, node, target_cpp_type: str
3348
3463
  ) -> str:
@@ -3605,12 +3720,25 @@ class TypeInferer:
3605
3720
  and ternaries / if / switch expressions. Returns the string
3606
3721
  ``"double"`` as the safe fallback when no narrower type can be
3607
3722
  determined."""
3723
+ if isinstance(node, Subscript) and history_annotation(node) is not None:
3724
+ # An array's or a matrix's history is a collection of its type
3725
+ # (collection_history.py): a loop's local bound to it is one.
3726
+ spec = self._type_spec_from_expr(node)
3727
+ if spec is not None:
3728
+ return self._type_spec_to_cpp(spec)
3608
3729
  if isinstance(node, Subscript) and isinstance(node.object, Identifier):
3609
3730
  name = node.object.name
3610
3731
  if name in self.ctx.series_vars:
3611
3732
  return self._series_type_for(name)
3612
3733
  if name in self.ctx.series_bar_fields:
3613
3734
  return "double"
3735
+ if (isinstance(node, Subscript)
3736
+ and isinstance(node.object, (FuncCall, Ternary, MemberAccess))):
3737
+ # The history of an expression whose value is an object or a
3738
+ # drawing is a reference of its type (``_history_value_cpp_type``).
3739
+ reference = self._reference_cpp_type(node.object)
3740
+ if reference is not None:
3741
+ return reference
3614
3742
  if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
3615
3743
  # A callable-result history read keeps the callable's scalar
3616
3744
  # family. Collection subscripts follow separate element-type paths.
@@ -143,11 +143,13 @@ from ..ast_nodes import (
143
143
  MemberAccess,
144
144
  NaLiteral,
145
145
  NumberLiteral,
146
+ Subscript,
146
147
  Ternary,
147
148
  TupleLiteral,
148
149
  StringLiteral,
149
150
  VarDecl,
150
151
  )
152
+ from ..collection_history import history_annotation, literal_offset
151
153
  from ..external_requests import RECORDED_KEY_ANNOTATION
152
154
  from ..limits import iter_ast_nodes
153
155
  from ..security_contexts import ticker_symbol_arg
@@ -583,7 +585,9 @@ class CallVisitor:
583
585
  for arg in rest_nodes
584
586
  )
585
587
  )) or self._binds_fresh_array_to_reference(
586
- method_info, rest_nodes, first_param=1),
588
+ method_info, rest_nodes, first_param=1)
589
+ or self._binds_temporary_drawing_to_reference(
590
+ method_info, [receiver_node, *rest_nodes]),
587
591
  )
588
592
 
589
593
  def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None:
@@ -1211,6 +1215,12 @@ class CallVisitor:
1211
1215
  returns its argument or a global) or a selection of a held and a
1212
1216
  fresh array, and ``"keep"`` for every other argument, which keeps the
1213
1217
  lowering it had: a variable, a field, ``na``, any other built-in."""
1218
+ annotation = history_annotation(node)
1219
+ if annotation is not None:
1220
+ # A copy of an array's history for the callee (collection_history.py);
1221
+ # at a zero offset, the variable itself.
1222
+ return ("keep" if literal_offset(node) == 0 or annotation["use"] != "copy"
1223
+ else "fresh")
1214
1224
  if isinstance(node, Ternary):
1215
1225
  kinds = {self._array_arg_kind(node.true_val, depth),
1216
1226
  self._array_arg_kind(node.false_val, depth)}
@@ -1306,6 +1316,63 @@ class CallVisitor:
1306
1316
  stage = stage or kind == "fresh"
1307
1317
  return stage
1308
1318
 
1319
+ # The drawing types a callable takes as ``T&`` (emit_top: a parameter of
1320
+ # a type outside the user types is a reference).
1321
+ _REFERENCE_DRAWING_PARAMS = frozenset({"line", "box", "label", "linefill", "chart.point"})
1322
+
1323
+ def _drawing_arg_is_temporary(self, arg) -> bool:
1324
+ """Whether the C++ of the drawing argument ``arg`` is a prvalue no
1325
+ ``T&`` takes: a history read or the current slot of a history-read
1326
+ variable (``b[1]``, ``b[0]``: ``Series::operator[]`` returns a copy),
1327
+ a new or copied drawing or chart point, a user call's result, or a
1328
+ selection with such an arm. A variable, a field and a collection
1329
+ element are lvalues, which compiled before and keep their C++."""
1330
+ if isinstance(arg, Subscript):
1331
+ return True
1332
+ if isinstance(arg, Identifier):
1333
+ return self._identifier_reads_series(arg)
1334
+ if isinstance(arg, Ternary):
1335
+ return (self._drawing_arg_is_temporary(arg.true_val)
1336
+ or self._drawing_arg_is_temporary(arg.false_val))
1337
+ if not isinstance(arg, FuncCall):
1338
+ return False
1339
+ func_name, namespace = self._resolve_callee(arg.callee)
1340
+ if namespace in DRAWING_NS and func_name in ("new", "copy"):
1341
+ return True
1342
+ if self._is_chart_point_callee(arg.callee):
1343
+ return True
1344
+ if namespace is None and func_name in self._func_info_map:
1345
+ return True
1346
+ return (isinstance(arg.callee, MemberAccess)
1347
+ and self._typed_user_method_info(
1348
+ arg.callee.object, arg.callee.member)[1] is not None)
1349
+
1350
+ def _binds_temporary_drawing_to_reference(self, func_info, arg_nodes: list,
1351
+ first_param: int = 0) -> bool:
1352
+ """Whether a call passes a temporary drawing (``_drawing_arg_is_temporary``)
1353
+ to a drawing parameter its callee takes as ``T&``: staging the call
1354
+ (``_ordered_user_call_expr``) binds it to a named forwarding
1355
+ reference. The handle is an arena id and Pine forbids reassigning a
1356
+ parameter, so the callee reads the drawing the argument names
1357
+ (udth_drawparam). A history-read parameter takes a Series bridge
1358
+ instead."""
1359
+ if func_info is None:
1360
+ return False
1361
+ specs = list(getattr(func_info, "param_type_specs", ()) or ())
1362
+ params = (list(func_info.node.params)
1363
+ if getattr(func_info, "node", None) is not None else [])
1364
+ series = self.ctx.func_series_vars.get(func_info.name, set())
1365
+ for offset, arg in enumerate(arg_nodes):
1366
+ index = first_param + offset
1367
+ spec = specs[index] if index < len(specs) else None
1368
+ if (arg is None or spec is None or spec.kind != "udt"
1369
+ or spec.name not in self._REFERENCE_DRAWING_PARAMS
1370
+ or (index < len(params) and params[index] in series)):
1371
+ continue
1372
+ if self._drawing_arg_is_temporary(arg):
1373
+ return True
1374
+ return False
1375
+
1309
1376
  def _ordered_user_call_expr(
1310
1377
  self,
1311
1378
  call_head: str,
@@ -1584,6 +1651,17 @@ class CallVisitor:
1584
1651
  args = self._array_method_args(callee.member, arg_nodes, recv_spec)
1585
1652
  return self._array_method_expr(recv, callee.member, args, recv_spec, node)
1586
1653
 
1654
+ # A matrix method on a matrix's history (``(m[1]).get(0, 0)``:
1655
+ # collection_history.py), which no other receiver path reaches.
1656
+ if (isinstance(callee, MemberAccess)
1657
+ and isinstance(callee.object, Subscript)
1658
+ and history_annotation(callee.object) is not None
1659
+ and callee.member in MATRIX_METHODS):
1660
+ recv_spec = self._type_spec_from_expr(callee.object)
1661
+ if recv_spec is not None and recv_spec.kind == "matrix":
1662
+ return self._matrix_history_method_expr(
1663
+ callee.object, callee.member, recv_spec, node)
1664
+
1587
1665
  # chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
1588
1666
  # aggregate). Routed here BEFORE the obj.field.method receiver logic,
1589
1667
  # which would otherwise mis-treat ``chart.point`` as a receiver object
@@ -1823,6 +1901,9 @@ class CallVisitor:
1823
1901
  drawing = self._drawing_na_expr(node.args[0])
1824
1902
  if drawing is not None:
1825
1903
  return drawing
1904
+ history = self._collection_history_na(node.args[0])
1905
+ if history is not None:
1906
+ return history
1826
1907
  args = ", ".join(self._visit_expr(a) for a in node.args)
1827
1908
  return f"is_na({args})"
1828
1909
 
@@ -2564,9 +2645,13 @@ class CallVisitor:
2564
2645
  if func_name in MATRIX_METHODS and node.args:
2565
2646
  from ..ast_nodes import Identifier as _Ident
2566
2647
  if func_name in MATRIX_NUMERIC_ONLY:
2567
- if not isinstance(node.args[0], _Ident):
2648
+ recv_node = node.args[0]
2649
+ if history_annotation(recv_node) is not None:
2650
+ # A matrix's history is a matrix of its type.
2651
+ recv_node = recv_node.object
2652
+ if not isinstance(recv_node, _Ident):
2568
2653
  self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
2569
- recv_name = node.args[0].name
2654
+ recv_name = recv_node.name
2570
2655
  recv_spec = self._collection_spec_for_name(recv_name)
2571
2656
  if recv_spec is None or recv_spec.kind != "matrix":
2572
2657
  self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
@@ -2684,6 +2769,8 @@ class CallVisitor:
2684
2769
  val = "na<int64_t>()"
2685
2770
  elif f_cpp_type == "bool":
2686
2771
  val = self._coerce_bool_expr(val, value_node)
2772
+ elif f_cpp_type == "double":
2773
+ val = self._coerce_double_slot(val, value_node)
2687
2774
  elif (self._is_nullable_collection_cpp_type(f_cpp_type)
2688
2775
  and val == "na<double>()"):
2689
2776
  val = f"{f_cpp_type}{{}}"
@@ -2958,6 +3045,8 @@ class CallVisitor:
2958
3045
  all_args,
2959
3046
  source_order_nodes=[*node.args, *node.kwargs.values()],
2960
3047
  force_stage=self._binds_fresh_array_to_reference(
3048
+ self._func_info_map.get(func_name), ordered_arg_nodes)
3049
+ or self._binds_temporary_drawing_to_reference(
2961
3050
  self._func_info_map.get(func_name), ordered_arg_nodes),
2962
3051
  )
2963
3052
  return f"{call_head}({', '.join(all_args)})"
@@ -88,6 +88,7 @@ classes from ``..ast_nodes``.
88
88
  from __future__ import annotations
89
89
 
90
90
  from ..errors import Phase
91
+ from ..symbols import TypeSpec
91
92
  from ..external_requests import UNPINNED_ANNOTATION
92
93
  from ..ast_nodes import (
93
94
  ASTNode,
@@ -503,6 +504,18 @@ class ExprVisitor:
503
504
  return (f'([&]() {{ pine_runtime_error(std::string("{self._cpp_string_escape(marker)}")); '
504
505
  f"return {value}; }}())")
505
506
 
507
+ def _identifier_reads_series(self, node: Identifier) -> bool:
508
+ """Whether ``_visit_ident`` lowers ``node`` to its Series' current slot
509
+ (``x[0]``, a copy): a history-read parameter or variable."""
510
+ name = node.name
511
+ if name in self._current_func_series_params:
512
+ return True
513
+ if (name in self._current_func_param_types or name in BAR_FIELDS
514
+ or name in BAR_BUILTINS):
515
+ return False
516
+ return self._binding_is_series(
517
+ name, self._call_site_var_name(node, self._safe_name(name)))
518
+
506
519
  def _visit_ident(self, node: Identifier) -> str:
507
520
  name = node.name
508
521
  # Bare 'na' identifier → na<double>()
@@ -1270,7 +1283,66 @@ class ExprVisitor:
1270
1283
  return self._emit_na_relational(op, left_cpp, right_cpp)
1271
1284
  return f"({left_cpp} {op} {right_cpp})"
1272
1285
 
1286
+ # The reference types TradingView compares with == and != (lab tv
1287
+ # pf-udth-line-eq: by identity, a na reference included); every other
1288
+ # object or drawing type is CE10123 there.
1289
+ _IDENTITY_COMPARED_REFERENCES = frozenset({"line", "label"})
1290
+
1291
+ def _reference_operand_kind(self, operand) -> str | None:
1292
+ """The type of a user-defined object or drawing reference operand
1293
+ (``Cell``, ``box``, ``chart.point``, ...), else None."""
1294
+ spec = self._type_spec_from_expr(operand)
1295
+ if spec is None or spec.kind != "udt" or not spec.name:
1296
+ return None
1297
+ if spec.name not in DRAWING_TYPE_TO_CPP and spec.name not in self._udt_defs:
1298
+ return None
1299
+ if self._infer_type(operand) in (
1300
+ "double", "int", "int64_t", "bool", "std::string"):
1301
+ return None
1302
+ return spec.name
1303
+
1304
+ def _reference_equality_cpp(self, node: BinOp) -> str | None:
1305
+ """``==`` / ``!=`` with a reference operand: a line or a label equals
1306
+ the same line or label, na included (TradingView compares their
1307
+ references; a deleted drawing keeps its own), the handles' ids here.
1308
+ TradingView refuses the comparison of any other object or drawing
1309
+ (CE10123) and of a reference with na (CE10187), which never compiled
1310
+ here either."""
1311
+ kinds = (self._reference_operand_kind(node.left),
1312
+ self._reference_operand_kind(node.right))
1313
+ if kinds == (None, None):
1314
+ return None
1315
+ other = node.right if kinds[0] is not None else node.left
1316
+ kind = kinds[0] or kinds[1]
1317
+ if isinstance(other, NaLiteral) or (
1318
+ isinstance(other, Identifier) and other.name == "na"):
1319
+ self._codegen_error(
1320
+ node,
1321
+ f"{node.op} na on a {kind} reference: TradingView refuses a "
1322
+ "comparison with na (CE10187: \"Cannot compare a value to "
1323
+ "\\\"na\\\" directly\").",
1324
+ hint="Test the reference with na(...).",
1325
+ )
1326
+ if kinds[0] != kinds[1] or kind not in self._IDENTITY_COMPARED_REFERENCES:
1327
+ self._codegen_error(
1328
+ node,
1329
+ f"{node.op} on a {kind} reference: TradingView compares only "
1330
+ "line and label references (CE10123: \"Cannot call "
1331
+ f"'operator {node.op}'\" with an argument of the {kind} type).",
1332
+ hint="Compare the fields or getter values the objects hold, "
1333
+ "or test na(...).",
1334
+ )
1335
+ left = self._visit_expr(node.left)
1336
+ right = self._visit_expr(node.right)
1337
+ return self._left_operand_first(
1338
+ node, left, right,
1339
+ lambda left, right: f"(({left}).id {node.op} ({right}).id)")
1340
+
1273
1341
  def _visit_binop(self, node: BinOp) -> str:
1342
+ if node.op in ("==", "!="):
1343
+ identity = self._reference_equality_cpp(node)
1344
+ if identity is not None:
1345
+ return identity
1274
1346
  # An int-literal-only ``+ - *`` tree whose exact value leaves int32 is
1275
1347
  # a 64-bit Pine int (``90 * 24 * 60 * 60 * 1000`` = 7 776 000 000);
1276
1348
  # C++ ``int`` literal arithmetic would wrap it. Fold it here and spell
@@ -1723,7 +1795,119 @@ class ExprVisitor:
1723
1795
  idx_int = f"({idx_int})"
1724
1796
  return idx_int
1725
1797
 
1798
+ def _warn_untracked_reference_history(self, node: Subscript) -> None:
1799
+ """A history read of an object or drawing reference that lowers to
1800
+ the current reference, which keeps the lowering it compiled to: the
1801
+ analyzer registered no history for it here, while TradingView reads
1802
+ the reference the variable held k bars back (fixtures/
1803
+ udt_history_tv)."""
1804
+ if isinstance(node.index, NumberLiteral) and node.index.value == 0:
1805
+ return
1806
+ spec = self._type_spec_from_expr(node.object)
1807
+ if spec is not None and spec.kind in ("array", "matrix"):
1808
+ # A parameter's array or matrix history (collection_history.py:
1809
+ # the earlier lowering, kept where it compiled).
1810
+ self._codegen_warning(
1811
+ node,
1812
+ f"{node.object.name}[...] reads the current {spec.kind} in "
1813
+ "PineForge: no history of this parameter is kept, where "
1814
+ "TradingView reads the copy the call that many calls back "
1815
+ "left (fixtures/array_history_tv ahist_fn).",
1816
+ )
1817
+ return
1818
+ if (spec is None or spec.kind != "udt"
1819
+ or not (spec.name in DRAWING_TYPE_TO_CPP
1820
+ or spec.name in self._udt_defs)):
1821
+ return
1822
+ self._codegen_warning(
1823
+ node,
1824
+ f"{node.object.name}[...] reads the current {spec.name} reference "
1825
+ "in PineForge: no history of this variable is kept here, where "
1826
+ "TradingView reads the reference it held that many bars back.",
1827
+ )
1828
+
1829
+ def _chart_point_field_written_names(self) -> set[str]:
1830
+ """The chart.point variables a field of which the script assigns
1831
+ (``p.price := x``). Cached."""
1832
+ cached = getattr(self, "_chart_point_writes_cache", None)
1833
+ if cached is not None:
1834
+ return cached
1835
+ point = TypeSpec.udt("chart.point")
1836
+ names: set[str] = set()
1837
+ for node in self._walk_ast(self.ctx.ast):
1838
+ if (isinstance(node, Assignment)
1839
+ and isinstance(node.target, MemberAccess)
1840
+ and isinstance(node.target.object, Identifier)
1841
+ and any(getattr(scope.symbols.get(node.target.object.name),
1842
+ "type_spec", None) == point
1843
+ for scope in self.ctx.symbols.all_scopes)):
1844
+ names.add(node.target.object.name)
1845
+ self._chart_point_writes_cache = names
1846
+ return names
1847
+
1848
+ def _refuse_mutable_chart_point_history(self, node: Subscript) -> None:
1849
+ """History of a chart.point variable the script changes a field of.
1850
+ TradingView's chart.point is an object (its history holds references,
1851
+ read as they are now: fixtures/udt_history_tv); PineForge holds one
1852
+ as a value, so its history would read a changed point's old value,
1853
+ and the field write on its history did not compile. A point no field
1854
+ write changes reads alike either way."""
1855
+ if isinstance(node.index, NumberLiteral) and node.index.value == 0:
1856
+ return
1857
+ if not (isinstance(node.object, Identifier)
1858
+ and node.object.name in self._chart_point_field_written_names()
1859
+ and self._type_spec_from_expr(node.object)
1860
+ == TypeSpec.udt("chart.point")):
1861
+ return
1862
+ self._codegen_error(
1863
+ node,
1864
+ f"History of the chart.point {node.object.name} is not supported "
1865
+ "in PineForge when the script changes its fields: PineForge holds "
1866
+ "a chart.point as a value, where TradingView's history reads the "
1867
+ "point object as it is now.",
1868
+ hint="Keep the field in a variable of its own and read that "
1869
+ "variable's history, or build a new point instead of "
1870
+ "changing a field.",
1871
+ )
1872
+
1873
+ def _refuse_field_value_history(self, node: Subscript) -> None:
1874
+ """History of a user-defined object's field that holds a value:
1875
+ TradingView refuses ``c.v[1]`` and ``(c.v)[1]`` alike ("Cannot use the
1876
+ history-referencing operator on fields of user-defined types",
1877
+ CE10290) and takes ``(c[1]).v``; the C++ subscripted the field's
1878
+ scalar, which did not compile. A field holding an object or a drawing
1879
+ is a reference, whose history TradingView keeps (udth_expr); the
1880
+ analyzer refuses a collection field's history (CE10290 too:
1881
+ ``_refuse_collection_field_history``)."""
1882
+ field = node.object
1883
+ if not isinstance(field, MemberAccess):
1884
+ return
1885
+ owner = self._type_spec_from_expr(field.object)
1886
+ if (owner is None or owner.kind != "udt"
1887
+ or owner.name not in self._udt_defs
1888
+ or self._reference_cpp_type(field) is not None):
1889
+ return
1890
+ field_spec = self._udt_field_type_specs.get(owner.name, {}).get(field.member)
1891
+ if field_spec is not None and field_spec.kind in ("array", "map", "matrix"):
1892
+ return
1893
+ receiver = (field.object.name if isinstance(field.object, Identifier)
1894
+ else "object")
1895
+ self._codegen_error(
1896
+ node,
1897
+ f"{receiver}.{field.member}[...]: TradingView refuses the "
1898
+ "history-referencing operator on fields of user-defined types "
1899
+ "(CE10290).",
1900
+ hint=f"Read the field of the object's history: "
1901
+ f"({receiver}[1]).{field.member}.",
1902
+ )
1903
+
1726
1904
  def _visit_subscript(self, node: Subscript) -> str:
1905
+ # An array's or a matrix's history (collection_history.py).
1906
+ collection_history = self._lower_collection_history(node)
1907
+ if collection_history is not None:
1908
+ return collection_history
1909
+ self._refuse_mutable_chart_point_history(node)
1910
+ self._refuse_field_value_history(node)
1727
1911
  idx = self._visit_expr(node.index)
1728
1912
  # Series::operator[] accepts C++ int. A Pine int can be backed by an
1729
1913
  # int64_t timestamp slot; implicitly narrowing its na sentinel to int
@@ -1747,6 +1931,7 @@ class ExprVisitor:
1747
1931
  return f"{self._safe_name(name)}[{series_idx}]"
1748
1932
  # Function parameters are scalars — src[0] → src, src[N>0] → src
1749
1933
  if name in self._current_func_param_types:
1934
+ self._warn_untracked_reference_history(node)
1750
1935
  return self._safe_name(name)
1751
1936
  if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
1752
1937
  # Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
@@ -1757,6 +1942,9 @@ class ExprVisitor:
1757
1942
  if self._binding_is_series(name, safe):
1758
1943
  # Same Pine [k] semantics as Series in runtime/series.hpp
1759
1944
  return f"{safe}[{series_idx}]"
1945
+ # An array's history the analyzer annotated lowers above
1946
+ # (collection_history.py); this legacy element read is left for
1947
+ # one it never met.
1760
1948
  spec = self._collection_spec_for_name(name)
1761
1949
  if spec is not None and spec.kind in ("array", "map"):
1762
1950
  self._codegen_warning(
@@ -1804,6 +1992,10 @@ class ExprVisitor:
1804
1992
  cpp_t = self._infer_type(node.object)
1805
1993
  if cpp_t not in ("double", "int", "int64_t", "bool"):
1806
1994
  cpp_t = "double"
1995
+ # A call's object or drawing result keeps the references it
1996
+ # returned (``_history_value_cpp_type``), in the Series the
1997
+ # prepass declared.
1998
+ cpp_t = self._registered_history_handle(node) or cpp_t
1807
1999
  member = self._inline_history_member("hist_call", node)
1808
2000
  ta_site = self._get_ta_site(node.object)
1809
2001
  ta_name = (
@@ -1870,12 +2062,18 @@ class ExprVisitor:
1870
2062
  self._inline_history_member_by_key.get(
1871
2063
  ("hist_call", id(node), self._current_instance_name)
1872
2064
  )
1873
- if isinstance(node.object, (BinOp, UnaryOp, Ternary))
2065
+ if isinstance(node.object, (BinOp, UnaryOp, Ternary, MemberAccess))
1874
2066
  else None
1875
2067
  )
2068
+ hoisted_member = self._hoisted_hist_reads.get(id(node))
2069
+ if hoisted_member is not None:
2070
+ # A reference's history below a lazy edge, pushed on every
2071
+ # execution before the statement (``_emit_lazy_call_history_hoists``).
2072
+ return f"{hoisted_member}[{self._history_offset_cpp(idx, node.index)}]"
1876
2073
  if compound_member is not None:
1877
- cpp_t = self._infer_type(node.object)
1878
- if cpp_t in ("double", "int", "int64_t", "bool"):
2074
+ reference_cpp = self._registered_history_handle(node)
2075
+ cpp_t = reference_cpp or self._infer_type(node.object)
2076
+ if reference_cpp is not None or cpp_t in ("double", "int", "int64_t", "bool"):
1879
2077
  inner = self._visit_expr(node.object)
1880
2078
  idx_int = self._history_offset_cpp(idx, node.index)
1881
2079
  return (
@@ -1893,6 +2091,7 @@ class ExprVisitor:
1893
2091
  if (name not in BAR_FIELDS and name not in BAR_SERIES_PUSH
1894
2092
  and name not in self.ctx.series_vars
1895
2093
  and name not in self._var_names):
2094
+ self._warn_untracked_reference_history(node)
1896
2095
  return obj
1897
2096
  idx_int = self._history_offset_cpp(idx, node.index)
1898
2097
  return f"{obj}[{idx_int}]"