@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.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- 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.
|
|
2847
|
-
# catches "1.5", "-15", "abc",
|
|
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
|
-
|
|
2863
|
-
if
|
|
2945
|
+
refusal = invalid_tf_literal(tf_node.value, fn_label)
|
|
2946
|
+
if refusal is None:
|
|
2864
2947
|
return
|
|
2865
|
-
|
|
2866
|
-
|
|
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
|
+
"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": "
|
|
6
|
+
"sha256": "fe674a25ca188d1d6902b01f81d040fc62e6ad9ee325afae7287d990248e1f97"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "1.
|
|
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
|
-
|
|
1124
|
+
"syminfo.prefix": [
|
|
1125
|
+
"symbol"
|
|
1126
|
+
],
|
|
1127
|
+
"syminfo.ticker": [
|
|
1128
|
+
"symbol"
|
|
1129
|
+
],
|
|
1126
1130
|
"na": [
|
|
1127
1131
|
"x"
|
|
1128
1132
|
],
|
|
Binary file
|