@pineforge/codegen-pyodide 1.2.0 → 1.4.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 (34) hide show
  1. package/LICENSE +19 -3
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +6 -5
  5. package/pineforge_codegen/analyzer/base.py +3 -3
  6. package/pineforge_codegen/analyzer/types.py +7 -3
  7. package/pineforge_codegen/codegen/base.py +36 -17
  8. package/pineforge_codegen/codegen/capabilities.py +143 -10
  9. package/pineforge_codegen/codegen/checked_settings.py +161 -4
  10. package/pineforge_codegen/codegen/collection_history.py +163 -12
  11. package/pineforge_codegen/codegen/emit_top.py +29 -13
  12. package/pineforge_codegen/codegen/helpers.py +25 -1
  13. package/pineforge_codegen/codegen/host_members.py +1 -0
  14. package/pineforge_codegen/codegen/input.py +23 -8
  15. package/pineforge_codegen/codegen/order_shapes.py +356 -0
  16. package/pineforge_codegen/codegen/run_stops.py +52 -0
  17. package/pineforge_codegen/codegen/security.py +66 -10
  18. package/pineforge_codegen/codegen/tables.py +144 -28
  19. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  20. package/pineforge_codegen/codegen/types.py +85 -4
  21. package/pineforge_codegen/codegen/visit_call.py +235 -55
  22. package/pineforge_codegen/codegen/visit_expr.py +40 -8
  23. package/pineforge_codegen/codegen/visit_stmt.py +5 -4
  24. package/pineforge_codegen/collection_history.py +122 -20
  25. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  26. package/pineforge_codegen/external_requests.py +30 -9
  27. package/pineforge_codegen/matrix_overloads.py +7 -0
  28. package/pineforge_codegen/parser.py +7 -2
  29. package/pineforge_codegen/signatures.py +2 -2
  30. package/pineforge_codegen/support_checker.py +101 -10
  31. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  32. package/release.json +2 -2
  33. package/tables.json +7 -3
  34. package/pineforge_codegen-1.2.0.tar.gz +0 -0
@@ -549,6 +549,20 @@ def _qualified_name(callee: ASTNode) -> tuple[str | None, str | None]:
549
549
  return None, None
550
550
 
551
551
 
552
+ # The ``array.*`` functions whose value is a new array.
553
+ _ARRAY_VALUED_FUNCTIONS = frozenset({"new", "from", "copy", "slice"})
554
+
555
+
556
+ def _is_array_valued_call(node) -> bool:
557
+ """Whether ``node`` is a call of ``array.new``, ``array.new_<type>``,
558
+ ``array.from``, ``array.copy`` or ``array.slice``."""
559
+ if not isinstance(node, FuncCall):
560
+ return False
561
+ namespace, function = _qualified_name(node.callee)
562
+ return namespace == "array" and function is not None and (
563
+ function in _ARRAY_VALUED_FUNCTIONS or function.startswith("new_"))
564
+
565
+
552
566
  def _resolve_member_chain(node: ASTNode) -> str | None:
553
567
  """Flatten a MemberAccess/Identifier chain into a dotted name."""
554
568
  if isinstance(node, Identifier):
@@ -661,6 +675,11 @@ class SupportChecker:
661
675
  # then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
662
676
  # syminfo.tickerid``. First binding wins (closest to declaration).
663
677
  self._scalar_defs: dict[str, ASTNode] = {}
678
+ # Variables declared an array (by an array-valued call or an
679
+ # ``array<...>`` type), and the names the script binds exactly once:
680
+ # the receivers ``_check_bound_array_concat`` resolves by name.
681
+ self._array_names: set[str] = set()
682
+ self._bound_once: set[str] | None = None
664
683
  # EVERY reassignment value bound to a scalar name (``name := <expr>``
665
684
  # and the compound forms). A declaration alone does not pin the value a
666
685
  # ``request.security`` symbol argument actually carries: the engine is
@@ -1335,6 +1354,10 @@ class SupportChecker:
1335
1354
 
1336
1355
  def _visit_VarDecl(self, node: VarDecl) -> None:
1337
1356
  self._check_tuple_literal_value(node.value)
1357
+ self._check_bound_array_concat(node.value)
1358
+ if node.name and (_is_array_valued_call(node.value)
1359
+ or (node.type_hint or "").startswith("array<")):
1360
+ self._array_names.add(node.name)
1338
1361
  if node.name and node.value is not None:
1339
1362
  self._scalar_defs.setdefault(node.name, node.value)
1340
1363
  if node.name and (node.is_var or node.is_varip):
@@ -1367,6 +1390,7 @@ class SupportChecker:
1367
1390
  self._visit_children(node)
1368
1391
 
1369
1392
  def _visit_Assignment(self, node: Assignment) -> None:
1393
+ self._check_bound_array_concat(node.value)
1370
1394
  if isinstance(node.target, TupleLiteral):
1371
1395
  # ``[p, q] := f()`` reached the C++ as an assignment to a
1372
1396
  # temporary tuple, a silent no-op. TradingView rejects it
@@ -1390,6 +1414,64 @@ class SupportChecker:
1390
1414
  self._record_scalar_rebind(node)
1391
1415
  self._visit_children(node)
1392
1416
 
1417
+ def _check_bound_array_concat(self, value) -> None:
1418
+ """Refuse the value of ``array.concat``: PineForge lowers the call as
1419
+ the statement that appends to the first array (``a.concat(b)`` or
1420
+ ``array.concat(a, b)``), which has no value to bind. The namespace
1421
+ call is refused wherever its value is bound; the method form only on a
1422
+ receiver known to be an array (a call that builds one, or a variable
1423
+ declared as one that the script binds exactly once, so that a
1424
+ parameter or a local of another type never shares the name), and never
1425
+ when the script defines a method of that name."""
1426
+ if not isinstance(value, FuncCall):
1427
+ return
1428
+ if isinstance(value.callee, MemberAccess):
1429
+ # ``_qualified_name`` names no call receiver (``array.copy(a).concat(b)``).
1430
+ method = value.callee.member
1431
+ namespace = (value.callee.object.name
1432
+ if isinstance(value.callee.object, Identifier) else None)
1433
+ else:
1434
+ namespace, method = _qualified_name(value.callee)
1435
+ if method != "concat":
1436
+ return
1437
+ if namespace != "array":
1438
+ if method in self._user_methods or not isinstance(value.callee, MemberAccess):
1439
+ return
1440
+ receiver = value.callee.object
1441
+ while isinstance(receiver, Subscript):
1442
+ receiver = receiver.object
1443
+ if not (_is_array_valued_call(receiver)
1444
+ or (isinstance(receiver, Identifier)
1445
+ and receiver.name in self._array_names
1446
+ and receiver.name in self._names_bound_once())):
1447
+ return
1448
+ name = method
1449
+ self._err(value, f"array.{name}(...) is not implemented in PineForge runtime.")
1450
+
1451
+ def _names_bound_once(self) -> set[str]:
1452
+ """The names the script binds exactly once: by one declaration, with
1453
+ no function or method parameter, loop variable, tuple element or
1454
+ second declaration (a local beside a global) sharing the name."""
1455
+ if self._bound_once is None:
1456
+ counts: dict[str, int] = {}
1457
+ for item in _walk_nodes(self._ast):
1458
+ if isinstance(item, VarDecl):
1459
+ names = (item.name,)
1460
+ elif isinstance(item, TupleAssign):
1461
+ names = tuple(item.names)
1462
+ elif isinstance(item, (FuncDef, MethodDef)):
1463
+ names = tuple(getattr(param, "name", param) for param in item.params)
1464
+ elif isinstance(item, ForStmt):
1465
+ names = (item.var,)
1466
+ elif isinstance(item, ForInStmt):
1467
+ names = tuple(item.vars or ()) + ((item.var,) if item.var else ())
1468
+ else:
1469
+ continue
1470
+ for name in names:
1471
+ counts[name] = counts.get(name, 0) + 1
1472
+ self._bound_once = {name for name, count in counts.items() if count == 1}
1473
+ return self._bound_once
1474
+
1393
1475
  def _check_tuple_literal_value(self, value) -> None:
1394
1476
  """Refuse a tuple literal or a ternary of tuples as a declaration's
1395
1477
  or reassignment's value.
@@ -2843,8 +2925,9 @@ class SupportChecker:
2843
2925
  # Bare suffix shorthand — Pine treats "D"/"W"/"M"/"S" as 1<suffix>.
2844
2926
  # Accept the same shorthand here.
2845
2927
  return None
2846
- if not digits.isdigit():
2847
- # catches "1.5", "-15", "abc", etc.
2928
+ if not digits.isdecimal():
2929
+ # catches "1.5", "-15", "abc", and digits int() does not read
2930
+ # ("²"), etc.
2848
2931
  return f"non-integer timeframe magnitude '{digits}'"
2849
2932
  n = int(digits)
2850
2933
  if n <= 0:
@@ -2859,15 +2942,11 @@ class SupportChecker:
2859
2942
  return
2860
2943
  if not isinstance(tf_node, StringLiteral):
2861
2944
  return
2862
- err = self._validate_pine_tf_literal(tf_node.value)
2863
- if err is None:
2945
+ refusal = invalid_tf_literal(tf_node.value, fn_label)
2946
+ if refusal is None:
2864
2947
  return
2865
- self._err(
2866
- tf_node,
2867
- f"{fn_label}: invalid timeframe literal '{tf_node.value}'. "
2868
- "Expected Pine TF format like '1', '15', '1H', '1D', '15S'.",
2869
- hint=err,
2870
- )
2948
+ message, hint = refusal
2949
+ self._err(tf_node, message, hint=hint)
2871
2950
 
2872
2951
  def _check_request_security_lower_tf_symbol(self, node: FuncCall) -> None:
2873
2952
  """``request.security_lower_tf`` reads the chart's intrabars: the
@@ -2912,6 +2991,18 @@ class SupportChecker:
2912
2991
  self._check_tf_literal(tf_node, "request.security_lower_tf")
2913
2992
 
2914
2993
 
2994
+ def invalid_tf_literal(tf_str: str, fn_label: str) -> tuple[str, str] | None:
2995
+ """``(message, hint)`` refusing ``tf_str`` as the timeframe of
2996
+ ``fn_label`` (``request.security`` / ``request.security_lower_tf``), or
2997
+ None when it is a Pine timeframe. The codegen refuses a timeframe string
2998
+ it resolves from a constant or a helper parameter with the same text."""
2999
+ err = SupportChecker._validate_pine_tf_literal(tf_str)
3000
+ if err is None:
3001
+ return None
3002
+ return (f"{fn_label}: invalid timeframe literal '{tf_str}'. "
3003
+ "Expected Pine TF format like '1', '15', '1H', '1D', '15S'.", err)
3004
+
3005
+
2915
3006
  # ---------------------------------------------------------------------------
2916
3007
  # Convenience entry point
2917
3008
  # ---------------------------------------------------------------------------
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "1.2.0",
2
+ "codegen": "1.4.0",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "7c9654f79d188babd5034d3ebedd2cdd60a8ec2866b70d8ad863a9f06341420e"
6
+ "sha256": "fe674a25ca188d1d6902b01f81d040fc62e6ad9ee325afae7287d990248e1f97"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "1.2.0",
2
+ "CODEGEN_VERSION": "1.4.0",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
@@ -1121,8 +1121,12 @@
1121
1121
  "id",
1122
1122
  "id2"
1123
1123
  ],
1124
- "syminfo.prefix": [],
1125
- "syminfo.ticker": [],
1124
+ "syminfo.prefix": [
1125
+ "symbol"
1126
+ ],
1127
+ "syminfo.ticker": [
1128
+ "symbol"
1129
+ ],
1126
1130
  "na": [
1127
1131
  "x"
1128
1132
  ],
Binary file