@pineforge/codegen-pyodide 1.0.0 → 1.1.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.
Files changed (31) hide show
  1. package/glue.py +2 -1
  2. package/package.json +1 -1
  3. package/pineforge_codegen/__init__.py +24 -9
  4. package/pineforge_codegen/analyzer/base.py +190 -1
  5. package/pineforge_codegen/analyzer/call_handlers.py +2 -0
  6. package/pineforge_codegen/analyzer/contracts.py +6 -0
  7. package/pineforge_codegen/analyzer/types.py +16 -5
  8. package/pineforge_codegen/ast_nodes.py +7 -0
  9. package/pineforge_codegen/codegen/base.py +136 -14
  10. package/pineforge_codegen/codegen/checked_settings.py +272 -0
  11. package/pineforge_codegen/codegen/collection_history.py +200 -0
  12. package/pineforge_codegen/codegen/emit_top.py +76 -14
  13. package/pineforge_codegen/codegen/helpers.py +12 -2
  14. package/pineforge_codegen/codegen/host_members.py +8 -0
  15. package/pineforge_codegen/codegen/input.py +36 -3
  16. package/pineforge_codegen/codegen/security.py +56 -1
  17. package/pineforge_codegen/codegen/ta.py +65 -1
  18. package/pineforge_codegen/codegen/types.py +136 -8
  19. package/pineforge_codegen/codegen/visit_call.py +92 -3
  20. package/pineforge_codegen/codegen/visit_expr.py +202 -3
  21. package/pineforge_codegen/codegen/visit_stmt.py +19 -4
  22. package/pineforge_codegen/collection_history.py +1595 -0
  23. package/pineforge_codegen/external_requests.py +3 -0
  24. package/pineforge_codegen/parser.py +19 -4
  25. package/pineforge_codegen/request_discovery.py +506 -0
  26. package/pineforge_codegen/support_checker.py +37 -0
  27. package/pineforge_codegen-1.1.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +1 -1
  30. package/transpile.worker.mjs +2 -1
  31. package/pineforge_codegen-1.0.0.tar.gz +0 -0
@@ -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
@@ -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)})"