@pineforge/codegen-pyodide 0.10.0 → 0.10.2
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/package.json +1 -1
- package/pineforge_codegen/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Strict, source-order-preserving binding for typed Pine methods.
|
|
2
|
+
|
|
3
|
+
Typed extension methods are inventoried before semantic analysis so a call may
|
|
4
|
+
precede its declaration without falling through to a same-named builtin. This
|
|
5
|
+
module deliberately contains no analyzer or codegen state: both phases consume
|
|
6
|
+
the same signature and binding result.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from .ast_nodes import ASTNode, FuncCall, MethodDef, Program
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class MethodSignature:
|
|
18
|
+
key: str
|
|
19
|
+
param_names: tuple[str, ...]
|
|
20
|
+
param_defaults: tuple[ASTNode | None, ...]
|
|
21
|
+
param_type_hints: tuple[str | None, ...]
|
|
22
|
+
declaration: MethodDef | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class BoundMethodArgs:
|
|
27
|
+
"""Non-receiver arguments in parameter and evaluation order."""
|
|
28
|
+
|
|
29
|
+
args_by_param: tuple[ASTNode, ...]
|
|
30
|
+
evaluation_order: tuple[ASTNode, ...]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MethodBindError(ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def signature_from_method_def(node: MethodDef) -> MethodSignature:
|
|
38
|
+
annotations = node.annotations or {}
|
|
39
|
+
defaults = list(annotations.get("param_defaults", ()))
|
|
40
|
+
hints = list(annotations.get("param_type_hints", ()))
|
|
41
|
+
while len(defaults) < len(node.params):
|
|
42
|
+
defaults.append(None)
|
|
43
|
+
while len(hints) < len(node.params):
|
|
44
|
+
hints.append(None)
|
|
45
|
+
return MethodSignature(
|
|
46
|
+
key=f"{node.type_name}.{node.name}",
|
|
47
|
+
param_names=tuple(node.params[1:]),
|
|
48
|
+
param_defaults=tuple(defaults[1:len(node.params)]),
|
|
49
|
+
param_type_hints=tuple(hints[1:len(node.params)]),
|
|
50
|
+
declaration=node,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def signature_from_callable(
|
|
55
|
+
key: str,
|
|
56
|
+
param_names: list[str] | tuple[str, ...],
|
|
57
|
+
param_defaults: list[ASTNode | None] | tuple[ASTNode | None, ...],
|
|
58
|
+
param_type_hints: list[str | None] | tuple[str | None, ...] = (),
|
|
59
|
+
) -> MethodSignature:
|
|
60
|
+
"""Build a non-receiver signature from an analyzed method FuncInfo."""
|
|
61
|
+
|
|
62
|
+
names = tuple(param_names)
|
|
63
|
+
defaults = list(param_defaults)
|
|
64
|
+
hints = list(param_type_hints)
|
|
65
|
+
while len(defaults) < len(names):
|
|
66
|
+
defaults.append(None)
|
|
67
|
+
while len(hints) < len(names):
|
|
68
|
+
hints.append(None)
|
|
69
|
+
return MethodSignature(
|
|
70
|
+
key=key,
|
|
71
|
+
param_names=names,
|
|
72
|
+
param_defaults=tuple(defaults[:len(names)]),
|
|
73
|
+
param_type_hints=tuple(hints[:len(names)]),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def inventory_method_signatures(program: Program) -> dict[str, MethodSignature]:
|
|
78
|
+
"""Inventory direct method declarations without analyzing their bodies."""
|
|
79
|
+
|
|
80
|
+
result: dict[str, MethodSignature] = {}
|
|
81
|
+
for stmt in program.body:
|
|
82
|
+
if isinstance(stmt, MethodDef):
|
|
83
|
+
signature = signature_from_method_def(stmt)
|
|
84
|
+
# Preserve the analyzer's historical first-declaration lookup. This
|
|
85
|
+
# prepass does not introduce method overloading or duplicate policy.
|
|
86
|
+
result.setdefault(signature.key, signature)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _written_actuals(call: FuncCall) -> list[ASTNode]:
|
|
91
|
+
"""Recover written order lost by the parser's args/kwargs split."""
|
|
92
|
+
|
|
93
|
+
fallback = [*call.args, *call.kwargs.values()]
|
|
94
|
+
recorded = (call.annotations or {}).get("call_arg_order", ())
|
|
95
|
+
if (
|
|
96
|
+
len(recorded) == len(fallback)
|
|
97
|
+
and {id(node) for node in recorded} == {id(node) for node in fallback}
|
|
98
|
+
):
|
|
99
|
+
return list(recorded)
|
|
100
|
+
if not fallback or any(getattr(node, "loc", None) is None for node in fallback):
|
|
101
|
+
return fallback
|
|
102
|
+
indexed = list(enumerate(fallback))
|
|
103
|
+
indexed.sort(
|
|
104
|
+
key=lambda item: (
|
|
105
|
+
item[1].loc.line,
|
|
106
|
+
item[1].loc.col,
|
|
107
|
+
item[0],
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
return [node for _index, node in indexed]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def bind_method_call(
|
|
114
|
+
signature: MethodSignature,
|
|
115
|
+
call: FuncCall,
|
|
116
|
+
) -> BoundMethodArgs:
|
|
117
|
+
"""Bind one method call strictly, excluding the receiver argument."""
|
|
118
|
+
|
|
119
|
+
names = signature.param_names
|
|
120
|
+
if len(call.args) > len(names):
|
|
121
|
+
raise MethodBindError(
|
|
122
|
+
f"{signature.key}: too many positional arguments "
|
|
123
|
+
f"(expected {len(names)}, got {len(call.args)})"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for name in call.kwargs:
|
|
127
|
+
if name not in names:
|
|
128
|
+
raise MethodBindError(
|
|
129
|
+
f"{signature.key}: unknown keyword argument '{name}'"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
bound: list[ASTNode | None] = [None] * len(names)
|
|
133
|
+
for index, value in enumerate(call.args):
|
|
134
|
+
bound[index] = value
|
|
135
|
+
for name, value in call.kwargs.items():
|
|
136
|
+
index = names.index(name)
|
|
137
|
+
if bound[index] is not None:
|
|
138
|
+
raise MethodBindError(
|
|
139
|
+
f"{signature.key}: argument '{name}' passed both "
|
|
140
|
+
"positionally and by keyword"
|
|
141
|
+
)
|
|
142
|
+
bound[index] = value
|
|
143
|
+
|
|
144
|
+
inserted_defaults: list[ASTNode] = []
|
|
145
|
+
for index, name in enumerate(names):
|
|
146
|
+
if bound[index] is not None:
|
|
147
|
+
continue
|
|
148
|
+
default = (
|
|
149
|
+
signature.param_defaults[index]
|
|
150
|
+
if index < len(signature.param_defaults)
|
|
151
|
+
else None
|
|
152
|
+
)
|
|
153
|
+
if default is None:
|
|
154
|
+
raise MethodBindError(
|
|
155
|
+
f"{signature.key}: missing required argument '{name}'"
|
|
156
|
+
)
|
|
157
|
+
bound[index] = default
|
|
158
|
+
inserted_defaults.append(default)
|
|
159
|
+
|
|
160
|
+
return BoundMethodArgs(
|
|
161
|
+
args_by_param=tuple(value for value in bound if value is not None),
|
|
162
|
+
evaluation_order=tuple([*_written_actuals(call), *inserted_defaults]),
|
|
163
|
+
)
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import re
|
|
15
15
|
|
|
16
16
|
from .lexer import Token, TokenType
|
|
17
|
-
from .errors import SourceLocation
|
|
17
|
+
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
|
|
18
18
|
from .ast_nodes import (
|
|
19
19
|
ASTNode,
|
|
20
20
|
Program, StrategyDecl, ImportStmt,
|
|
@@ -56,6 +56,7 @@ class Parser:
|
|
|
56
56
|
self.pos = 0
|
|
57
57
|
self._source = source
|
|
58
58
|
self._filename = filename
|
|
59
|
+
self._recovery_count = 0
|
|
59
60
|
|
|
60
61
|
# ------------------------------------------------------------------
|
|
61
62
|
# Helpers
|
|
@@ -143,6 +144,9 @@ class Parser:
|
|
|
143
144
|
self._recover()
|
|
144
145
|
self._skip_newlines()
|
|
145
146
|
|
|
147
|
+
if self._recovery_count:
|
|
148
|
+
prog.annotations = dict(prog.annotations or {})
|
|
149
|
+
prog.annotations["parse_recovery_count"] = self._recovery_count
|
|
146
150
|
return prog
|
|
147
151
|
|
|
148
152
|
def _extract_version(self) -> int | None:
|
|
@@ -156,6 +160,7 @@ class Parser:
|
|
|
156
160
|
|
|
157
161
|
def _recover(self) -> None:
|
|
158
162
|
"""Skip tokens until next NEWLINE or EOF for error recovery."""
|
|
163
|
+
self._recovery_count += 1
|
|
159
164
|
while not self._at_end() and not self._check(TokenType.NEWLINE):
|
|
160
165
|
self._advance()
|
|
161
166
|
if self._check(TokenType.NEWLINE):
|
|
@@ -400,10 +405,13 @@ class Parser:
|
|
|
400
405
|
start_tok = self._advance() # consume 'strategy' or 'indicator'
|
|
401
406
|
# Parse arguments as a function call, then convert to StrategyDecl
|
|
402
407
|
self._consume(TokenType.LPAREN)
|
|
403
|
-
args, kwargs = self._parse_call_args()
|
|
408
|
+
args, kwargs, call_arg_order = self._parse_call_args()
|
|
404
409
|
self._consume(TokenType.RPAREN)
|
|
405
410
|
node = StrategyDecl(args=args, kwargs=kwargs)
|
|
406
|
-
node.annotations = {
|
|
411
|
+
node.annotations = {
|
|
412
|
+
"decl_kind": start_tok.value,
|
|
413
|
+
"call_arg_order": call_arg_order,
|
|
414
|
+
}
|
|
407
415
|
return self._set_loc(node, start_tok)
|
|
408
416
|
|
|
409
417
|
def _parse_import_stmt(self) -> ImportStmt:
|
|
@@ -786,8 +794,16 @@ class Parser:
|
|
|
786
794
|
start_tok = self._advance() # consume 'method'
|
|
787
795
|
name = self._consume(TokenType.IDENT).value
|
|
788
796
|
self._consume(TokenType.LPAREN)
|
|
789
|
-
# First param is the type + self
|
|
790
|
-
|
|
797
|
+
# First param is the required receiver type + self name. Receiver
|
|
798
|
+
# types use the same complete Pine hint grammar as ordinary typed
|
|
799
|
+
# parameters: primitives lex as TYPE_* tokens, while collections may
|
|
800
|
+
# carry nested ``<...>`` arguments or postfix ``[]``. Consuming only
|
|
801
|
+
# IDENT here silently dropped valid declarations such as
|
|
802
|
+
# ``method id(int self)`` and ``method push(array<int> self, ...)``
|
|
803
|
+
# through the parser's top-level recovery path.
|
|
804
|
+
if self._current().type not in TYPE_KEYWORDS | {TokenType.IDENT}:
|
|
805
|
+
self._consume(TokenType.IDENT) # raises a located ParseError
|
|
806
|
+
type_name = self._parse_type_hint_string()
|
|
791
807
|
params = [self._consume(TokenType.IDENT).value] # 'self' or user's name
|
|
792
808
|
param_type_hints = [type_name]
|
|
793
809
|
# Preserve per-param default expressions so codegen can substitute
|
|
@@ -1212,15 +1228,17 @@ class Parser:
|
|
|
1212
1228
|
"""Parse (args, kwargs) after callee expression."""
|
|
1213
1229
|
start_tok = self._current()
|
|
1214
1230
|
self._consume(TokenType.LPAREN)
|
|
1215
|
-
args, kwargs = self._parse_call_args()
|
|
1231
|
+
args, kwargs, call_arg_order = self._parse_call_args()
|
|
1216
1232
|
self._consume(TokenType.RPAREN)
|
|
1217
1233
|
node = FuncCall(callee=callee, args=args, kwargs=kwargs)
|
|
1234
|
+
node.annotations = {"call_arg_order": call_arg_order}
|
|
1218
1235
|
return self._set_loc(node, start_tok)
|
|
1219
1236
|
|
|
1220
|
-
def _parse_call_args(self) -> tuple[list, dict]:
|
|
1237
|
+
def _parse_call_args(self) -> tuple[list, dict, list]:
|
|
1221
1238
|
"""Parse function call arguments and keyword arguments."""
|
|
1222
1239
|
args: list = []
|
|
1223
1240
|
kwargs: dict = {}
|
|
1241
|
+
call_arg_order: list = []
|
|
1224
1242
|
|
|
1225
1243
|
while not self._check(TokenType.RPAREN) and not self._at_end():
|
|
1226
1244
|
# Detect kwargs: IDENT = value (but not IDENT == value)
|
|
@@ -1228,15 +1246,36 @@ class Parser:
|
|
|
1228
1246
|
and self._peek().type == TokenType.EQUALS
|
|
1229
1247
|
and self._peek(2).type != TokenType.EQUALS):
|
|
1230
1248
|
key_tok = self._advance()
|
|
1249
|
+
if key_tok.value in kwargs:
|
|
1250
|
+
raise CompileError(
|
|
1251
|
+
[
|
|
1252
|
+
Diagnostic(
|
|
1253
|
+
level=Level.ERROR,
|
|
1254
|
+
phase=Phase.PARSER,
|
|
1255
|
+
location=self._loc(key_tok),
|
|
1256
|
+
message=(
|
|
1257
|
+
"duplicate keyword argument "
|
|
1258
|
+
f"'{key_tok.value}'"
|
|
1259
|
+
),
|
|
1260
|
+
hint=(
|
|
1261
|
+
f"Remove one '{key_tok.value}=' binding; "
|
|
1262
|
+
"a keyword argument may be specified only once."
|
|
1263
|
+
),
|
|
1264
|
+
)
|
|
1265
|
+
]
|
|
1266
|
+
)
|
|
1231
1267
|
self._advance() # consume =
|
|
1232
1268
|
val = self._parse_expression()
|
|
1233
1269
|
kwargs[key_tok.value] = val
|
|
1270
|
+
call_arg_order.append(val)
|
|
1234
1271
|
else:
|
|
1235
|
-
|
|
1272
|
+
val = self._parse_expression()
|
|
1273
|
+
args.append(val)
|
|
1274
|
+
call_arg_order.append(val)
|
|
1236
1275
|
|
|
1237
1276
|
self._match(TokenType.COMMA)
|
|
1238
1277
|
|
|
1239
|
-
return args, kwargs
|
|
1278
|
+
return args, kwargs, call_arg_order
|
|
1240
1279
|
|
|
1241
1280
|
# -- Primary expressions --
|
|
1242
1281
|
|
|
@@ -229,7 +229,7 @@ _math("round",
|
|
|
229
229
|
_sig([("x", F)], ret=I), # round(x) → int
|
|
230
230
|
_sig([("x", F), ("precision", I)])) # round(x, n) → float
|
|
231
231
|
_math("round_to_mintick", _sig([("x", F)]))
|
|
232
|
-
_math("sign", _sig([("x", F)], ret=
|
|
232
|
+
_math("sign", _sig([("x", F)], ret=F))
|
|
233
233
|
_math("max",
|
|
234
234
|
_sig([("x", F), ("y", F)]), # 2-arg
|
|
235
235
|
_sig([("x", F), ("y", F), ("z", F)]), # 3-arg
|
|
@@ -307,13 +307,13 @@ _strat("exit", _sig([
|
|
|
307
307
|
_strat("close", _sig([
|
|
308
308
|
("id", S), ("comment", S, None),
|
|
309
309
|
("qty", F, None), ("qty_percent", F, None),
|
|
310
|
-
("alert_message", S, None), ("
|
|
311
|
-
("
|
|
310
|
+
("alert_message", S, None), ("immediately", B, False),
|
|
311
|
+
("disable_alert", B, None),
|
|
312
312
|
], ret=VOID))
|
|
313
313
|
|
|
314
314
|
_strat("close_all", _sig([
|
|
315
315
|
("comment", S, None), ("alert_message", S, None),
|
|
316
|
-
("
|
|
316
|
+
("immediately", B, False), ("disable_alert", B, None),
|
|
317
317
|
], ret=VOID))
|
|
318
318
|
|
|
319
319
|
_strat("cancel", _sig([("id", S)], ret=VOID))
|