@pineforge/codegen-pyodide 0.10.1 → 0.10.3

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.
@@ -516,11 +516,20 @@ class SupportChecker:
516
516
  # then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
517
517
  # syminfo.tickerid``. First binding wins (closest to declaration).
518
518
  self._scalar_defs: dict[str, ASTNode] = {}
519
+ # EVERY reassignment value bound to a scalar name (``name := <expr>``
520
+ # and the compound forms). A declaration alone does not pin the value a
521
+ # ``request.security`` symbol argument actually carries: the engine is
522
+ # registered with no symbol at all, so an accepted-but-divergent rebind
523
+ # silently backtests the CHART feed. Collected in a whole-AST pre-pass
524
+ # (below) as well as during the visit, so a rebind that lexically
525
+ # FOLLOWS the request.security call cannot slip past.
526
+ self._scalar_rebinds: dict[str, list[ASTNode]] = {}
519
527
 
520
528
  # -- Public API --
521
529
 
522
530
  def check(self) -> list[Diagnostic]:
523
531
  self._collect_user_definitions(self._ast)
532
+ self._collect_scalar_rebinds(self._ast)
524
533
  for stmt in self._ast.body:
525
534
  self._visit(stmt)
526
535
  return self._diagnostics
@@ -533,6 +542,42 @@ class SupportChecker:
533
542
 
534
543
  # -- Setup --
535
544
 
545
+ def _record_scalar_rebind(self, node: Assignment) -> None:
546
+ """Remember ``name := <expr>`` (and the compound forms) by name.
547
+
548
+ A compound op (``+=``, ``*=``, …) records its right-hand operand, which
549
+ is never a current-symbol expression — so a string-built symbol is
550
+ rejected by the same rule that rejects a plain divergent ``:=``.
551
+ """
552
+ target = node.target
553
+ if not isinstance(target, Identifier) or node.value is None:
554
+ return
555
+ recorded = self._scalar_rebinds.setdefault(target.name, [])
556
+ if not any(value is node.value for value in recorded):
557
+ recorded.append(node.value)
558
+
559
+ def _collect_scalar_rebinds(self, node: ASTNode) -> None:
560
+ """Pre-pass: collect every scalar rebind anywhere in the AST.
561
+
562
+ The visit walks top-level statements in source order, so a rebind that
563
+ FOLLOWS a ``request.security`` call would otherwise never be seen by
564
+ that call's symbol check. Collecting up front is strictly fail-closed:
565
+ it can only add rejections, never remove one.
566
+ """
567
+ if isinstance(node, Assignment):
568
+ self._record_scalar_rebind(node)
569
+ for value in vars(node).values():
570
+ if isinstance(value, ASTNode):
571
+ self._collect_scalar_rebinds(value)
572
+ elif isinstance(value, list):
573
+ for item in value:
574
+ if isinstance(item, ASTNode):
575
+ self._collect_scalar_rebinds(item)
576
+ elif isinstance(value, dict):
577
+ for item in value.values():
578
+ if isinstance(item, ASTNode):
579
+ self._collect_scalar_rebinds(item)
580
+
536
581
  def _collect_user_definitions(self, ast: Program) -> None:
537
582
  for stmt in ast.body:
538
583
  if isinstance(stmt, TypeDecl):
@@ -1001,6 +1046,14 @@ class SupportChecker:
1001
1046
  )
1002
1047
  self._visit_children(node)
1003
1048
 
1049
+ def _visit_Assignment(self, node: Assignment) -> None:
1050
+ # ``_scalar_defs`` records only the DECLARATION, so a later ``:=``
1051
+ # rebind used to be invisible to the request.security symbol check.
1052
+ # (``check`` also pre-collects these; recording here keeps the visit
1053
+ # self-consistent for any caller that drives visitors directly.)
1054
+ self._record_scalar_rebind(node)
1055
+ self._visit_children(node)
1056
+
1004
1057
  def _visit_TupleAssign(self, node: TupleAssign) -> None:
1005
1058
  drawing_mask = self._tuple_assign_drawing_mask(node.value)
1006
1059
  if drawing_mask:
@@ -1723,10 +1776,21 @@ class SupportChecker:
1723
1776
  or self._is_current_symbol_expr(node.false_val, _seen))
1724
1777
  # Def-use: resolve a bare identifier through its declaration value so an
1725
1778
  # aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
1726
- # ``request.security(haTicker, ...)``). Name-cycle-guarded.
1727
- if isinstance(node, Identifier) and node.name in self._scalar_defs and node.name not in _seen:
1728
- _seen.add(node.name)
1729
- return self._is_current_symbol_expr(self._scalar_defs[node.name], _seen)
1779
+ # ``request.security(haTicker, ...)``). Every ``:=`` rebind of the name
1780
+ # must resolve to the chart symbol too: the declaration alone does not
1781
+ # pin the value the call actually receives. Name-cycle-guarded.
1782
+ if isinstance(node, Identifier) and node.name not in _seen:
1783
+ definition = self._scalar_defs.get(node.name)
1784
+ rebinds = self._scalar_rebinds.get(node.name)
1785
+ if definition is not None or rebinds:
1786
+ _seen.add(node.name)
1787
+ if rebinds and not all(
1788
+ self._is_current_symbol_expr(value, _seen) for value in rebinds
1789
+ ):
1790
+ return False
1791
+ if definition is None:
1792
+ return False
1793
+ return self._is_current_symbol_expr(definition, _seen)
1730
1794
  return False
1731
1795
 
1732
1796
  # -- Pine timeframe-literal validation --
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "0.10.1",
2
+ "codegen": "0.10.3",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "e0d1ec121b40e4be39c958e338900789768fb1e6ec39b99b4cb3301c80f13a89"
6
+ "sha256": "50f545e8a008c23ec01a752a5e8bd00e885dda9dcd2bb417e2af18d680c22a6e"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "0.10.1",
2
+ "CODEGEN_VERSION": "0.10.3",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
Binary file