@pineforge/codegen-pyodide 0.10.0 → 0.10.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.
@@ -166,10 +166,11 @@ _BUILTIN_NAMESPACE_NAMES: frozenset[str] = frozenset({
166
166
  })
167
167
 
168
168
 
169
- # KI-71: Pine relational comparisons (``==`` ``!=`` ``<`` ``>`` ``<=`` ``>=``)
170
- # with an ``na`` operand evaluate *falsy*. Naive C++ relationals do not honour
171
- # this for the engine's na sentinels, so these ops route through na-aware
172
- # lowering when an operand can be na (see ``_visit_binop`` / ``_operand_na_kind``).
169
+ # KI-71/KI-73: Pine relational comparisons (``==`` ``!=`` ``<`` ``>``
170
+ # ``<=`` ``>=``) with an ``na`` operand evaluate *falsy*. Pine also treats
171
+ # finite float operands at most 1e-10 apart as equal, independent of their
172
+ # magnitude. Naive C++ relationals honour neither rule completely, so numeric
173
+ # comparisons route through the shared lowering below.
173
174
  _RELATIONAL_OPS: frozenset[str] = frozenset({"==", "!=", "<", ">", "<=", ">="})
174
175
 
175
176
  # C++ scalar types carrying a detectable ``na`` sentinel via ``is_na``:
@@ -221,7 +222,21 @@ class ExprVisitor:
221
222
  if isinstance(node, Subscript):
222
223
  return self._visit_subscript(node)
223
224
  if isinstance(node, TupleLiteral):
224
- elems = ", ".join(self._visit_expr(e) for e in node.elements)
225
+ elems = []
226
+ for element in node.elements:
227
+ element_spec = self._type_spec_from_expr(element)
228
+ element_target = None
229
+ if (element_spec is not None
230
+ and element_spec.kind == "udt"
231
+ and element_spec.name in DRAWING_TYPE_TO_CPP):
232
+ element_target = self._type_spec_to_cpp(element_spec)
233
+ elems.append(
234
+ self._visit_rhs_value(
235
+ element,
236
+ target_cpp_type=element_target,
237
+ )
238
+ )
239
+ elems = ", ".join(elems)
225
240
  return f"std::make_tuple({elems})"
226
241
  return "/* unknown */"
227
242
 
@@ -233,34 +248,132 @@ class ExprVisitor:
233
248
  return (isinstance(node, NaLiteral)
234
249
  or (isinstance(node, Identifier) and node.name == "na"))
235
250
 
236
- def _drawing_na_default(self, target_name: str | None) -> str | None:
237
- """If ``target_name`` is a drawing-handle variable (line/box/label/
238
- linefill/chart.point), return its na default literal (e.g. ``Box{}`` —
239
- a default-constructed handle whose id == -1 == na); otherwise None."""
251
+ def _drawing_target_cpp_type(
252
+ self,
253
+ target_name: str | None,
254
+ target_cpp_type: str | None,
255
+ ) -> str | None:
256
+ """Resolve a drawing target without leaking a shadowed global type.
257
+
258
+ An explicit contextual type always wins. For reassignments, where the
259
+ caller may not have one, consult function-local and parameter types
260
+ before the analyzer's flat global drawing registry. A same-named
261
+ scalar local/parameter must therefore block the global fallback.
262
+ """
263
+ if target_cpp_type is not None:
264
+ return (
265
+ target_cpp_type
266
+ if target_cpp_type in DRAWING_TYPE_TO_CPP.values()
267
+ else None
268
+ )
269
+ if not target_name:
270
+ return None
271
+ if target_name in self._lexical_drawing_types:
272
+ return self._lexical_drawing_types[target_name]
273
+ param_spec = getattr(self, "_current_func_param_specs", {}).get(
274
+ target_name
275
+ )
276
+ if (param_spec is not None
277
+ and param_spec.kind == "udt"
278
+ and param_spec.name in DRAWING_TYPE_TO_CPP):
279
+ return DRAWING_TYPE_TO_CPP[param_spec.name]
280
+ for scoped_types in (
281
+ getattr(self, "_current_func_local_types", {}),
282
+ getattr(self, "_current_func_param_types", {}),
283
+ ):
284
+ if target_name in scoped_types:
285
+ scoped_type = scoped_types[target_name].removesuffix("&")
286
+ return (
287
+ scoped_type
288
+ if scoped_type in DRAWING_TYPE_TO_CPP.values()
289
+ else None
290
+ )
291
+ # Only declarations already emitted into the current lexical path may
292
+ # shadow a global. Scanning the whole future function body makes a
293
+ # later local declaration pre-shadow earlier statements. Exact
294
+ # top-level types are captured separately because the analyzer's flat
295
+ # raw-name registry can be overwritten by a same-named local.
296
+ return self._global_drawing_cpp_types.get(target_name)
297
+
298
+ def _udt_target_cpp_type(
299
+ self,
300
+ *,
301
+ target_name: str | None = None,
302
+ target_node=None,
303
+ type_hint: str | None = None,
304
+ ) -> str | None:
305
+ """Return the exact authored UDT type for a contextual RHS target.
306
+
307
+ Pine's bare ``na`` is target typed. Arbitrary UDTs represent their na
308
+ ID with a default-constructed ``T{}``, just as generated drawing
309
+ handles do, but the drawing-only registry cannot safely answer for
310
+ same-spelled ordinary UDT declarations in sibling blocks/functions.
311
+ Prefer explicit/node context, then the source-ordered lexical map, and
312
+ consult legacy raw-name metadata only when no lexical binding exists.
313
+ """
314
+ spec = self._type_spec_from_hint_name(type_hint) if type_hint else None
315
+ if spec is None and target_node is not None:
316
+ spec = self._type_spec_from_expr(target_node)
317
+ if spec is not None and spec.kind == "udt" and spec.name in self._udt_defs:
318
+ return spec.name
240
319
  if not target_name:
241
320
  return None
242
- udt = self._udt_var_types.get(target_name)
243
- if udt in DRAWING_TYPE_TO_CPP:
244
- return f"{DRAWING_TYPE_TO_CPP[udt]}{{}}"
245
- return None
321
+ udt_name = self._identifier_udt_type(target_name)
322
+ return udt_name if udt_name in self._udt_defs else None
246
323
 
247
324
  def _visit_rhs_value(self, value_node, target_name: str | None = None,
248
325
  target_cpp_type: str | None = None) -> str:
249
326
  """Visit an assignment / declaration RHS.
250
327
 
251
328
  A bare ``na`` lowers to a type-appropriate initializer for the target
252
- instead of ``na<double>()``: drawing handles brace-init to their na
253
- handle (``Box{}`` / ``Line{}`` / …); ``std::string``/``int``/``int64_t``/
254
- ``bool`` use ``na<T>()``. Without this, ``Box b = na;`` and
255
- ``string s = na;`` would both emit ``na<double>()`` and fail to compile
256
- (no viable ``operator=`` / conversion). Every other RHS lowers unchanged.
329
+ instead of ``na<double>()``: drawing and authored UDT handles
330
+ brace-init to their na object (``Box{}`` / ``State{}``), while
331
+ ``std::string``/``int``/``int64_t``/``bool`` use ``na<T>()``. Without
332
+ this, ``State s = na;`` and ``string x = na;`` would both emit
333
+ ``na<double>()`` and fail to compile (no viable assignment/conversion).
334
+ Every other RHS lowers unchanged.
257
335
  """
258
- if target_name and self._is_na_expr(value_node):
259
- draw_default = self._drawing_na_default(target_name)
260
- if draw_default is not None:
261
- return draw_default
336
+ drawing_target = self._drawing_target_cpp_type(
337
+ target_name,
338
+ target_cpp_type,
339
+ )
340
+ if self._is_na_expr(value_node):
341
+ if drawing_target is not None:
342
+ return f"{drawing_target}{{}}"
343
+ if target_cpp_type in self._udt_defs:
344
+ return f"{target_cpp_type}{{}}"
345
+ if self._is_nullable_collection_cpp_type(target_cpp_type):
346
+ # Maps and matrices use default construction for a typed
347
+ # ``na`` ID. Their ``*.new`` factories create a valid ID,
348
+ # including a valid empty collection.
349
+ return f"{target_cpp_type}{{}}"
262
350
  if target_cpp_type in ("std::string", "int", "int64_t", "bool"):
263
351
  return f"na<{target_cpp_type}>()"
352
+ if (isinstance(value_node, Ternary)
353
+ and (self._is_nullable_collection_cpp_type(target_cpp_type)
354
+ or drawing_target is not None
355
+ or target_cpp_type in self._udt_defs)):
356
+ # C++ cannot deduce a common type for ``na<double>()`` and a
357
+ # collection/drawing handle. Pine's ternary is target typed, so
358
+ # propagate the exact declared/reassignment target into both arms.
359
+ # Arrays and scalar ternaries retain the established generic path;
360
+ # target typing is safe only for collections with nullable runtime
361
+ # representations.
362
+ branch_target = drawing_target or target_cpp_type
363
+ condition = self._visit_expr(value_node.condition)
364
+ true_value = self._visit_rhs_value(
365
+ value_node.true_val,
366
+ target_name,
367
+ target_cpp_type=branch_target,
368
+ )
369
+ false_value = self._visit_rhs_value(
370
+ value_node.false_val,
371
+ target_name,
372
+ target_cpp_type=branch_target,
373
+ )
374
+ return (
375
+ f"(({condition}) ? ({true_value}) : ({false_value}))"
376
+ )
264
377
  return self._visit_expr(value_node)
265
378
 
266
379
  def _visit_ident(self, node: Identifier) -> str:
@@ -268,6 +381,9 @@ class ExprVisitor:
268
381
  # Bare 'na' identifier → na<double>()
269
382
  if name == "na":
270
383
  return "na<double>()"
384
+ pending_outer = getattr(self, "_pending_decl_outer_alias", {})
385
+ if name in pending_outer:
386
+ return pending_outer[name]
271
387
  # Function parameters that are series — read current value
272
388
  if name in self._current_func_series_params:
273
389
  return f"{self._safe_name(name)}[0]"
@@ -281,7 +397,9 @@ class ExprVisitor:
281
397
  # Inline known constants (literals). Input-backed names are NOT inlined
282
398
  # because strategy_set_input() can override them at runtime; emit the
283
399
  # variable name and let get_input_*() produce the current value.
284
- if name in self._known_vars and name not in self._input_backed_vars:
400
+ if (name in self._known_vars
401
+ and name not in self._input_backed_vars
402
+ and not self._known_var_is_lexically_shadowed(name)):
285
403
  val = self._known_vars[name]
286
404
  if isinstance(val, bool):
287
405
  return "true" if val else "false"
@@ -316,7 +434,7 @@ class ExprVisitor:
316
434
  # Apply per-call-site var remap (for function-local vars)
317
435
  if self._active_var_remap and safe in self._active_var_remap:
318
436
  safe = self._active_var_remap[safe]
319
- if name in self.ctx.series_vars:
437
+ if self._binding_is_series(name, safe):
320
438
  return f"{safe}[0]"
321
439
  # Safety net: by here the name resolved to none of the builtins,
322
440
  # constants, parameters, or declared variables handled above, so
@@ -377,6 +495,16 @@ class ExprVisitor:
377
495
  return True
378
496
  return False
379
497
 
498
+ def _visit_mutable_expr(self, node: ASTNode) -> str:
499
+ """Lower an assignment target with UDT journal capture enabled."""
500
+
501
+ previous = getattr(self, "_udt_mutable_expr_depth", 0)
502
+ self._udt_mutable_expr_depth = previous + 1
503
+ try:
504
+ return self._visit_expr(node)
505
+ finally:
506
+ self._udt_mutable_expr_depth = previous
507
+
380
508
  def _visit_member_access(self, node: MemberAccess) -> str:
381
509
  # UDT field whose type was a drawing primitive (label/line/box/
382
510
  # linefill/polyline/table/chart.point) — the field is dropped from
@@ -385,6 +513,34 @@ class ExprVisitor:
385
513
  # See: pineforge-codegen issue #10.
386
514
  if self._is_omitted_udt_field(node):
387
515
  return "/* drawing field omitted */ 0"
516
+
517
+ # User-defined objects are numeric handles into a per-type arena. The
518
+ # arena lookup returns a real record lvalue, so this one lowering serves
519
+ # reads, assignment targets, nested field chains and method receivers.
520
+ owner_spec = self._type_spec_from_expr(node.object)
521
+ if (
522
+ owner_spec is not None
523
+ and owner_spec.kind == "udt"
524
+ and owner_spec.name in self._udt_defs
525
+ ):
526
+ owner = self._visit_expr(node.object)
527
+ arena = self._udt_arena_member_name(owner_spec.name)
528
+ field_spec = (
529
+ self._udt_field_type_specs.get(owner_spec.name, {}).get(
530
+ node.member
531
+ )
532
+ )
533
+ mutable_collection = (
534
+ field_spec is not None
535
+ and field_spec.kind in {"array", "map", "matrix"}
536
+ )
537
+ access = (
538
+ "get"
539
+ if getattr(self, "_udt_mutable_expr_depth", 0)
540
+ or mutable_collection
541
+ else "read"
542
+ )
543
+ return f"{arena}.{access}({owner}).{node.member}"
388
544
  if isinstance(node.object, Identifier):
389
545
  ns = node.object.name
390
546
  if ns == "strategy":
@@ -764,10 +920,6 @@ class ExprVisitor:
764
920
  safe = self._safe_name(name)
765
921
  if self._active_var_remap and safe in self._active_var_remap:
766
922
  safe = self._active_var_remap[safe]
767
- # Pointer-aliased UDT local (BUG C, rebinding case): field access
768
- # goes through ``->`` since the local holds ``UDT*``.
769
- if name in self._udt_ptr_alias_locals:
770
- return f"{safe}->{node.member}"
771
923
  return f"{safe}.{node.member}"
772
924
  if name not in self.ctx.series_vars:
773
925
  # Unknown identifier — likely an enum value
@@ -803,7 +955,8 @@ class ExprVisitor:
803
955
  # Inlined compile-time constants (non-input known vars) never hold na.
804
956
  if (isinstance(node, Identifier)
805
957
  and node.name in self._known_vars
806
- and node.name not in self._input_backed_vars):
958
+ and node.name not in self._input_backed_vars
959
+ and not self._known_var_is_lexically_shadowed(node.name)):
807
960
  return None
808
961
  return "int" if cpp_type in ("int", "int64_t") else "float"
809
962
 
@@ -820,29 +973,62 @@ class ExprVisitor:
820
973
  f"return !is_na(_pna_l) && !is_na(_pna_r) && "
821
974
  f"(_pna_l {op} _pna_r); }}())")
822
975
 
976
+ def _emit_float_relational(self, op: str, left: str, right: str) -> str:
977
+ """Emit Pine's na-aware float comparator with its fixed equality band.
978
+
979
+ Pine evaluates each operand once, rejects ``na``, and converts an int
980
+ to float when paired with a float. TradingView oracle probes pin a
981
+ magnitude-independent comparison band: finite operands with
982
+ ``abs(left - right) <= 1e-10`` compare equal. Every ordered operator is
983
+ derived from that same predicate, so strict ``<``/``>`` are suppressed
984
+ inside the band while ``<=``/``>=`` accept it.
985
+
986
+ Exact equality is checked first so equal infinities remain equal;
987
+ opposite infinities and finite/infinite pairs remain normally ordered.
988
+ The subtraction can overflow only to infinity, which is safely outside
989
+ the equality band. The preceding ``is_na`` guard keeps every NaN
990
+ comparison falsy, including ``!=``.
991
+ """
992
+ compare = {
993
+ "==": "_pfc_eq",
994
+ "!=": "!_pfc_eq",
995
+ "<": "(_pfc_l < _pfc_r) && !_pfc_eq",
996
+ ">": "(_pfc_l > _pfc_r) && !_pfc_eq",
997
+ "<=": "(_pfc_l < _pfc_r) || _pfc_eq",
998
+ ">=": "(_pfc_l > _pfc_r) || _pfc_eq",
999
+ }[op]
1000
+ return (
1001
+ f"([&]{{ auto _pna_l = ({left}); auto _pna_r = ({right}); "
1002
+ "double _pfc_l = static_cast<double>(_pna_l); "
1003
+ "double _pfc_r = static_cast<double>(_pna_r); "
1004
+ "bool _pfc_eq = (_pfc_l == _pfc_r) || "
1005
+ "(std::isfinite(_pfc_l) && std::isfinite(_pfc_r) && "
1006
+ "std::fabs(_pfc_l - _pfc_r) <= 1e-10); "
1007
+ f"return !is_na(_pna_l) && !is_na(_pna_r) && "
1008
+ f"({compare}); }}())"
1009
+ )
1010
+
823
1011
  def _lower_relational(self, op: str, left_node, right_node,
824
1012
  left_cpp: str, right_cpp: str) -> str:
825
- """Lower a Pine relational to C++, applying KI-71 na-aware wrapping.
1013
+ """Lower a Pine relational with Pine's na and float-precision rules.
826
1014
 
827
1015
  Shared by ``_visit_binop`` and the ``request.security`` expression
828
- builder so EVERY relational emission site honours Pine's falsy-on-na
829
- rule. Wraps only the diverging cells: any na-capable *integer* operand
830
- (the INT_MIN sentinel poisons all six operators) or a ``!=`` with an
831
- na-capable *float* operand (the sole IEEE-diverging float cell). Pure
832
- ``double`` ``==`` ``<`` ``>`` ``<=`` ``>=`` keep the naive emission —
833
- IEEE is already falsy-on-NaN there, so wrapping would be a pure no-op.
834
- Non-relational ``op`` (or non-scalar operands with no ``is_na``) fall
835
- through to the naive ``(left op right)`` form unchanged.
1016
+ builder so EVERY relational emission site honours Pine's rules. Any
1017
+ numeric comparison involving an inferred float gets the fixed-band
1018
+ wrapper for all six operators. Pure integer comparisons get the smaller
1019
+ KI-71 wrapper only when an operand can carry the INT_MIN ``na`` sentinel.
1020
+ Non-relational operators and nonnumeric operands fall through unchanged.
836
1021
  """
837
1022
  if op in _RELATIONAL_OPS:
838
1023
  lt = self._infer_type(left_node)
839
1024
  rt = self._infer_type(right_node)
840
1025
  if lt in _NA_SCALAR_CPP and rt in _NA_SCALAR_CPP:
1026
+ if "double" in (lt, rt):
1027
+ return self._emit_float_relational(op, left_cpp, right_cpp)
841
1028
  lk = self._operand_na_kind(left_node, lt)
842
1029
  rk = self._operand_na_kind(right_node, rt)
843
1030
  int_na = "int" in (lk, rk)
844
- float_na = "float" in (lk, rk)
845
- if int_na or (op == "!=" and float_na):
1031
+ if int_na:
846
1032
  return self._emit_na_relational(op, left_cpp, right_cpp)
847
1033
  return f"({left_cpp} {op} {right_cpp})"
848
1034
 
@@ -893,16 +1079,17 @@ class ExprVisitor:
893
1079
  if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
894
1080
  # Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
895
1081
  return f"_s_{name}[{idx}]"
896
- if name in self.ctx.series_vars:
897
- safe = self._safe_name(name)
898
- # Apply per-call-site var remap
899
- if self._active_var_remap and safe in self._active_var_remap:
900
- safe = self._active_var_remap[safe]
1082
+ safe = self._safe_name(name)
1083
+ # Apply per-call-site / exact block-member remap before deciding
1084
+ # whether the current lexical binding is a Series.
1085
+ if self._active_var_remap and safe in self._active_var_remap:
1086
+ safe = self._active_var_remap[safe]
1087
+ if self._binding_is_series(name, safe):
901
1088
  # Same Pine [k] semantics as Series in runtime/series.hpp
902
1089
  return f"{safe}[{idx}]"
903
- spec = self._collection_types.get(name)
1090
+ spec = self._collection_spec_for_name(name)
904
1091
  if spec is not None and spec.kind in ("array", "map"):
905
- return f"{self._safe_name(name)}[{idx}]"
1092
+ return f"{self._collection_receiver_expr(name)}[{idx}]"
906
1093
  # Handle strategy.* history access (e.g., strategy.position_size[1])
907
1094
  if isinstance(node.object, MemberAccess):
908
1095
  if isinstance(node.object.object, Identifier):
@@ -929,9 +1116,57 @@ class ExprVisitor:
929
1116
  if isinstance(node.object, FuncCall):
930
1117
  inner = self._visit_expr(node.object)
931
1118
  cpp_t = self._infer_type(node.object)
932
- if cpp_t not in ("double", "int", "bool"):
1119
+ if cpp_t not in ("double", "int", "int64_t", "bool"):
933
1120
  cpp_t = "double"
934
1121
  member = self._inline_history_member("hist_call", node)
1122
+ ta_site = self._get_ta_site(node.object)
1123
+ ta_name = (
1124
+ self._ta_name_from_site(ta_site)
1125
+ if ta_site is not None
1126
+ else ""
1127
+ )
1128
+ if (
1129
+ ta_site is not None
1130
+ and ta_name in {"highest", "lowest"}
1131
+ and self._ta_site_uses_precalc(ta_site)
1132
+ ):
1133
+ # Static chart Highest/Lowest sites already advance in the
1134
+ # full-bar precalculation pass, including when the source call
1135
+ # sits behind a Pine-v6 lazy boolean edge. Their direct history
1136
+ # operator must use that same chart-bar clock. Advancing only
1137
+ # ``_hist_call`` when the lazy RHS is reached changes ``[1]``
1138
+ # into "previous evaluation" and can permanently miss a first
1139
+ # qualifying signal (Filter V2's source-bound v6 oracle).
1140
+ #
1141
+ # Keep evaluation lazy: this branch only reads the immutable
1142
+ # precalculated result when the expression is reached. Dynamic
1143
+ # runs, UDF sites, and non-precalculated calls retain the
1144
+ # established call-local history fallback below.
1145
+ ta_mem = self._ta_member_name(ta_site)
1146
+ precalc = f"_precalc_{ta_mem}"
1147
+ return (
1148
+ f"([&]() -> {cpp_t} {{ "
1149
+ f"if (_use_precalc) {{ "
1150
+ f"auto _pf_hist_offset_raw = ({idx}); "
1151
+ f"if (is_na(_pf_hist_offset_raw)) return na<{cpp_t}>(); "
1152
+ f"long double _pf_hist_offset_numeric = "
1153
+ f"static_cast<long double>(_pf_hist_offset_raw); "
1154
+ f"if (bar_index_ < 0 || _pf_hist_offset_numeric < 0.0L || "
1155
+ f"_pf_hist_offset_numeric > "
1156
+ f"static_cast<long double>(bar_index_)) "
1157
+ f"return na<{cpp_t}>(); "
1158
+ f"std::size_t _pf_hist_offset = "
1159
+ f"static_cast<std::size_t>(_pf_hist_offset_numeric); "
1160
+ f"std::size_t _pf_hist_bar = "
1161
+ f"static_cast<std::size_t>(bar_index_) - _pf_hist_offset; "
1162
+ f"if (_pf_hist_bar >= {precalc}.size()) "
1163
+ f"return na<{cpp_t}>(); "
1164
+ f"return {precalc}[(std::size_t)_pf_hist_bar]; }} "
1165
+ f"{cpp_t} _hv = ({inner}); "
1166
+ f"if (history_advances_new_bar()) {member}.push(_hv); "
1167
+ f"else {member}.update(_hv); "
1168
+ f"return {member}[(int)({idx})]; }}())"
1169
+ )
935
1170
  return (
936
1171
  f"([&]() -> {cpp_t} {{ "
937
1172
  f"{cpp_t} _hv = ({inner}); "