browser-py 0.1.0__py3-none-any.whl
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.
- browser_compile.py +27 -0
- browser_py-0.1.0.dist-info/METADATA +53 -0
- browser_py-0.1.0.dist-info/RECORD +12 -0
- browser_py-0.1.0.dist-info/WHEEL +5 -0
- browser_py-0.1.0.dist-info/entry_points.txt +2 -0
- browser_py-0.1.0.dist-info/top_level.txt +7 -0
- browser_source.py +598 -0
- compiler_command.py +53 -0
- javascript_output.py +9 -0
- numeric_ir.py +315 -0
- numeric_source.py +266 -0
- webgpu_bindings.py +308 -0
browser_source.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
from ast import (
|
|
2
|
+
AST,
|
|
3
|
+
Add,
|
|
4
|
+
And,
|
|
5
|
+
AnnAssign,
|
|
6
|
+
Assign,
|
|
7
|
+
AsyncFunctionDef,
|
|
8
|
+
Attribute,
|
|
9
|
+
AugAssign,
|
|
10
|
+
Await,
|
|
11
|
+
BinOp,
|
|
12
|
+
BitAnd,
|
|
13
|
+
BitOr,
|
|
14
|
+
BitXor,
|
|
15
|
+
BoolOp,
|
|
16
|
+
Break,
|
|
17
|
+
Call,
|
|
18
|
+
ClassDef,
|
|
19
|
+
Compare,
|
|
20
|
+
Constant,
|
|
21
|
+
Continue,
|
|
22
|
+
Dict,
|
|
23
|
+
Div,
|
|
24
|
+
Eq,
|
|
25
|
+
ExceptHandler,
|
|
26
|
+
Expr,
|
|
27
|
+
For,
|
|
28
|
+
FunctionDef,
|
|
29
|
+
Gt,
|
|
30
|
+
GtE,
|
|
31
|
+
If,
|
|
32
|
+
IfExp,
|
|
33
|
+
ImportFrom,
|
|
34
|
+
Is,
|
|
35
|
+
IsNot,
|
|
36
|
+
List,
|
|
37
|
+
Lt,
|
|
38
|
+
LtE,
|
|
39
|
+
Mult,
|
|
40
|
+
Name,
|
|
41
|
+
Not,
|
|
42
|
+
NotEq,
|
|
43
|
+
Pass,
|
|
44
|
+
Raise,
|
|
45
|
+
Return,
|
|
46
|
+
Store,
|
|
47
|
+
Sub,
|
|
48
|
+
Subscript,
|
|
49
|
+
Try,
|
|
50
|
+
Tuple,
|
|
51
|
+
UAdd,
|
|
52
|
+
UnaryOp,
|
|
53
|
+
USub,
|
|
54
|
+
While,
|
|
55
|
+
expr,
|
|
56
|
+
parse,
|
|
57
|
+
stmt,
|
|
58
|
+
walk,
|
|
59
|
+
)
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from json import dumps
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
|
|
64
|
+
from numeric_source import require_acyclic
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Emission:
|
|
69
|
+
node: stmt | None
|
|
70
|
+
text: str
|
|
71
|
+
depth: int
|
|
72
|
+
exception: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class BrowserScope:
|
|
77
|
+
names: set[str]
|
|
78
|
+
arities: dict[str, int]
|
|
79
|
+
constructors: set[str]
|
|
80
|
+
arrays: set[str]
|
|
81
|
+
native: set[str]
|
|
82
|
+
asynchronous: bool
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def annotation_shape(node: expr | None, constructors: set[str]) -> str:
|
|
86
|
+
if node is None:
|
|
87
|
+
return "native"
|
|
88
|
+
if isinstance(node, Constant) and node.value is None:
|
|
89
|
+
return "void"
|
|
90
|
+
if isinstance(node, Name):
|
|
91
|
+
if node.id in ("int", "float", "str", "bool"):
|
|
92
|
+
return "value"
|
|
93
|
+
if node.id in constructors:
|
|
94
|
+
return "record"
|
|
95
|
+
if isinstance(node, Subscript) and isinstance(node.value, Name):
|
|
96
|
+
element = node.slice
|
|
97
|
+
if node.value.id == "tuple" and isinstance(element, Tuple) and len(element.elts) == 2:
|
|
98
|
+
if isinstance(element.elts[1], Constant) and element.elts[1].value is Ellipsis:
|
|
99
|
+
element = element.elts[0]
|
|
100
|
+
else:
|
|
101
|
+
raise NotImplementedError("Browser tuple annotations require one repeated element type")
|
|
102
|
+
if node.value.id in ("list", "tuple") and isinstance(element, Name):
|
|
103
|
+
if element.id in ("int", "float", "str", "bool") or element.id in constructors:
|
|
104
|
+
return "array"
|
|
105
|
+
raise NotImplementedError("Unsupported browser annotation")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def operator_text(node: BinOp | AugAssign | Compare | UnaryOp) -> str:
|
|
109
|
+
operator = node.ops[0] if isinstance(node, Compare) else node.op
|
|
110
|
+
if isinstance(operator, Add):
|
|
111
|
+
return "+"
|
|
112
|
+
if isinstance(operator, Sub):
|
|
113
|
+
return "-"
|
|
114
|
+
if isinstance(operator, Mult):
|
|
115
|
+
return "*"
|
|
116
|
+
if isinstance(operator, Div):
|
|
117
|
+
return "/"
|
|
118
|
+
if isinstance(operator, BitAnd):
|
|
119
|
+
return "&"
|
|
120
|
+
if isinstance(operator, BitOr):
|
|
121
|
+
return "|"
|
|
122
|
+
if isinstance(operator, BitXor):
|
|
123
|
+
return "^"
|
|
124
|
+
if isinstance(operator, (Eq, Is)):
|
|
125
|
+
return "==="
|
|
126
|
+
if isinstance(operator, (NotEq, IsNot)):
|
|
127
|
+
return "!=="
|
|
128
|
+
if isinstance(operator, Lt):
|
|
129
|
+
return "<"
|
|
130
|
+
if isinstance(operator, LtE):
|
|
131
|
+
return "<="
|
|
132
|
+
if isinstance(operator, Gt):
|
|
133
|
+
return ">"
|
|
134
|
+
if isinstance(operator, GtE):
|
|
135
|
+
return ">="
|
|
136
|
+
if isinstance(operator, Not):
|
|
137
|
+
return "!"
|
|
138
|
+
if isinstance(operator, USub):
|
|
139
|
+
return "-"
|
|
140
|
+
if isinstance(operator, UAdd):
|
|
141
|
+
return "+"
|
|
142
|
+
raise NotImplementedError("Unsupported browser operator: " + type(operator).__name__)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def expression_text(value: expr, scope: BrowserScope) -> str:
|
|
146
|
+
text: dict[expr, str] = {}
|
|
147
|
+
shapes: dict[expr, str] = {}
|
|
148
|
+
nodes = list(walk(value))
|
|
149
|
+
targets = {item.func for item in nodes if isinstance(item, Call)}
|
|
150
|
+
for node in reversed(nodes):
|
|
151
|
+
if not isinstance(node, expr):
|
|
152
|
+
continue
|
|
153
|
+
shape = "value"
|
|
154
|
+
if isinstance(node, Constant):
|
|
155
|
+
if node.value is None or isinstance(node.value, (str, bool, int, float)):
|
|
156
|
+
result = dumps(node.value, ensure_ascii=True, allow_nan=False)
|
|
157
|
+
else:
|
|
158
|
+
raise NotImplementedError("Unsupported browser literal")
|
|
159
|
+
elif isinstance(node, Name):
|
|
160
|
+
if node.id in scope.names:
|
|
161
|
+
result = node.id if node.id in ("window", "document") else "p_" + node.id
|
|
162
|
+
if node.id in scope.arrays:
|
|
163
|
+
shape = "array"
|
|
164
|
+
elif node.id in scope.native:
|
|
165
|
+
shape = "native"
|
|
166
|
+
elif node.id in ("len", "int", "str", "ValueError") and node in targets:
|
|
167
|
+
result = node.id
|
|
168
|
+
else:
|
|
169
|
+
raise NotImplementedError("Unknown browser name: " + node.id)
|
|
170
|
+
if node.id in scope.arities and node not in targets:
|
|
171
|
+
raise NotImplementedError("Browser functions must be called directly")
|
|
172
|
+
elif isinstance(node, (List, Tuple)):
|
|
173
|
+
result = "[" + ", ".join(text[item] for item in node.elts) + "]"
|
|
174
|
+
shape = "array"
|
|
175
|
+
elif isinstance(node, Dict):
|
|
176
|
+
fields = []
|
|
177
|
+
for i in range(len(node.keys)):
|
|
178
|
+
key = node.keys[i]
|
|
179
|
+
if isinstance(key, Constant) and isinstance(key.value, str):
|
|
180
|
+
fields.append(text[key] + ": " + text[node.values[i]])
|
|
181
|
+
else:
|
|
182
|
+
raise NotImplementedError("Browser descriptors require literal string keys")
|
|
183
|
+
result = "({" + ", ".join(fields) + "})"
|
|
184
|
+
shape = "native"
|
|
185
|
+
elif isinstance(node, Attribute):
|
|
186
|
+
if node.attr.startswith("_") and node.attr != "__new__":
|
|
187
|
+
raise NotImplementedError("Private browser attributes are unsupported")
|
|
188
|
+
if node.attr == "append":
|
|
189
|
+
if shapes[node.value] == "array" and node in targets:
|
|
190
|
+
result = text[node.value] + ".push"
|
|
191
|
+
else:
|
|
192
|
+
raise NotImplementedError("append requires a known array and a direct call")
|
|
193
|
+
else:
|
|
194
|
+
result = "(" + text[node.value] + ")." + node.attr
|
|
195
|
+
shape = "native"
|
|
196
|
+
elif isinstance(node, Subscript):
|
|
197
|
+
result = "(" + text[node.value] + ")[" + text[node.slice] + "]"
|
|
198
|
+
shape = "native"
|
|
199
|
+
elif isinstance(node, BinOp):
|
|
200
|
+
if shapes[node.left] == "array" or shapes[node.right] == "array":
|
|
201
|
+
raise NotImplementedError("Browser arrays do not support arithmetic")
|
|
202
|
+
result = "(" + text[node.left] + " " + operator_text(node) + " " + text[node.right] + ")"
|
|
203
|
+
elif isinstance(node, UnaryOp):
|
|
204
|
+
result = "(" + operator_text(node) + text[node.operand] + ")"
|
|
205
|
+
elif isinstance(node, BoolOp):
|
|
206
|
+
symbol = " && " if isinstance(node.op, And) else " || "
|
|
207
|
+
result = "(" + symbol.join(text[item] for item in node.values) + ")"
|
|
208
|
+
elif isinstance(node, Compare) and len(node.ops) == 1:
|
|
209
|
+
result = "(" + text[node.left] + " " + operator_text(node) + " " + text[node.comparators[0]] + ")"
|
|
210
|
+
elif isinstance(node, IfExp):
|
|
211
|
+
result = "(" + text[node.test] + " ? " + text[node.body] + " : " + text[node.orelse] + ")"
|
|
212
|
+
if shapes[node.body] == shapes[node.orelse]:
|
|
213
|
+
shape = shapes[node.body]
|
|
214
|
+
elif isinstance(node, Await) and scope.asynchronous:
|
|
215
|
+
result = "(await " + text[node.value] + ")"
|
|
216
|
+
shape = "native"
|
|
217
|
+
elif isinstance(node, Call):
|
|
218
|
+
if node.keywords:
|
|
219
|
+
raise NotImplementedError("Browser calls require positional arguments")
|
|
220
|
+
args = ", ".join(text[item] for item in node.args)
|
|
221
|
+
if isinstance(node.func, Name):
|
|
222
|
+
name = node.func.id
|
|
223
|
+
if name in ("len", "int", "str", "ValueError") and len(node.args) == 1:
|
|
224
|
+
if name == "len":
|
|
225
|
+
result = "(" + args + ").length"
|
|
226
|
+
elif name == "int":
|
|
227
|
+
result = "Math.trunc(" + args + ")"
|
|
228
|
+
elif name == "str":
|
|
229
|
+
result = "String(" + args + ")"
|
|
230
|
+
else:
|
|
231
|
+
result = "new Error(" + args + ")"
|
|
232
|
+
elif name in scope.arities and len(node.args) == scope.arities[name]:
|
|
233
|
+
prefix = "new " if name in scope.constructors else ""
|
|
234
|
+
result = prefix + text[node.func] + "(" + args + ")"
|
|
235
|
+
if name in scope.arrays:
|
|
236
|
+
shape = "array"
|
|
237
|
+
elif name in scope.native:
|
|
238
|
+
shape = "native"
|
|
239
|
+
else:
|
|
240
|
+
raise NotImplementedError("Unsupported browser call or arity: " + name)
|
|
241
|
+
elif isinstance(node.func, Attribute) and node.func.attr == "__new__":
|
|
242
|
+
if shapes[node.func.value] == "native":
|
|
243
|
+
result = "new (" + text[node.func.value] + ")(" + args + ")"
|
|
244
|
+
shape = "native"
|
|
245
|
+
else:
|
|
246
|
+
raise NotImplementedError("Native constructor required")
|
|
247
|
+
elif isinstance(node.func, (Attribute, Subscript)):
|
|
248
|
+
result = text[node.func] + "(" + args + ")"
|
|
249
|
+
shape = "native"
|
|
250
|
+
else:
|
|
251
|
+
raise NotImplementedError("Browser call must name a function or native method")
|
|
252
|
+
else:
|
|
253
|
+
raise NotImplementedError("Unsupported browser expression: " + type(node).__name__)
|
|
254
|
+
text[node] = result
|
|
255
|
+
shapes[node] = shape
|
|
256
|
+
return text[value]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def emit_statements(body: list[stmt], scope: BrowserScope) -> str:
|
|
260
|
+
stack = [Emission(node, "", 0, "") for node in reversed(body)]
|
|
261
|
+
lines = []
|
|
262
|
+
serial = 0
|
|
263
|
+
while stack:
|
|
264
|
+
task = stack.pop()
|
|
265
|
+
node = task.node
|
|
266
|
+
depth = task.depth
|
|
267
|
+
exception = task.exception
|
|
268
|
+
if node is None:
|
|
269
|
+
lines.append(" " * depth + task.text)
|
|
270
|
+
continue
|
|
271
|
+
text = ""
|
|
272
|
+
if isinstance(node, (Assign, AnnAssign)):
|
|
273
|
+
target = node.targets[0] if isinstance(node, Assign) and len(node.targets) == 1 else None
|
|
274
|
+
if isinstance(node, AnnAssign):
|
|
275
|
+
target = node.target
|
|
276
|
+
if isinstance(target, (Name, Attribute, Subscript)) and node.value is not None:
|
|
277
|
+
text = expression_text(target, scope) + " = " + expression_text(node.value, scope) + ";"
|
|
278
|
+
else:
|
|
279
|
+
raise NotImplementedError("Browser assignments require one explicit target/value")
|
|
280
|
+
elif isinstance(node, AugAssign) and isinstance(node.target, (Name, Attribute, Subscript)):
|
|
281
|
+
text = (
|
|
282
|
+
expression_text(node.target, scope)
|
|
283
|
+
+ " "
|
|
284
|
+
+ operator_text(node)
|
|
285
|
+
+ "= "
|
|
286
|
+
+ expression_text(node.value, scope)
|
|
287
|
+
+ ";"
|
|
288
|
+
)
|
|
289
|
+
elif isinstance(node, Expr):
|
|
290
|
+
if isinstance(node.value, Constant) and isinstance(node.value.value, str):
|
|
291
|
+
continue
|
|
292
|
+
if isinstance(node.value, (Call, Await)):
|
|
293
|
+
text = expression_text(node.value, scope) + ";"
|
|
294
|
+
else:
|
|
295
|
+
raise NotImplementedError("Browser expression statements must be calls")
|
|
296
|
+
elif isinstance(node, Return):
|
|
297
|
+
text = (
|
|
298
|
+
"return " + (expression_text(node.value, scope) if node.value is not None else "undefined") + ";"
|
|
299
|
+
)
|
|
300
|
+
elif isinstance(node, Raise):
|
|
301
|
+
if node.cause is not None:
|
|
302
|
+
raise NotImplementedError("Browser exception causes are unsupported")
|
|
303
|
+
if node.exc is not None:
|
|
304
|
+
text = "throw " + expression_text(node.exc, scope) + ";"
|
|
305
|
+
elif exception:
|
|
306
|
+
text = "throw " + exception + ";"
|
|
307
|
+
else:
|
|
308
|
+
raise NotImplementedError("Bare raise requires a catch scope")
|
|
309
|
+
elif isinstance(node, If):
|
|
310
|
+
text = "if (" + expression_text(node.test, scope) + ") {"
|
|
311
|
+
stack.append(Emission(None, "}", depth, exception))
|
|
312
|
+
if node.orelse:
|
|
313
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.orelse))
|
|
314
|
+
stack.append(Emission(None, "} else {", depth, exception))
|
|
315
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.body))
|
|
316
|
+
elif isinstance(node, (For, While)):
|
|
317
|
+
if node.orelse:
|
|
318
|
+
raise NotImplementedError("Browser loop else clauses are unsupported")
|
|
319
|
+
if isinstance(node, While):
|
|
320
|
+
text = "while (" + expression_text(node.test, scope) + ") {"
|
|
321
|
+
elif isinstance(node.target, Name):
|
|
322
|
+
variable = expression_text(node.target, scope)
|
|
323
|
+
iterator = node.iter
|
|
324
|
+
if isinstance(iterator, Call) and isinstance(iterator.func, Name) and iterator.func.id == "range":
|
|
325
|
+
if iterator.keywords or len(iterator.args) not in (1, 2, 3):
|
|
326
|
+
raise NotImplementedError("Expected range with one to three positional arguments")
|
|
327
|
+
args = [expression_text(item, scope) for item in iterator.args]
|
|
328
|
+
start = args[0] if len(args) > 1 else "0"
|
|
329
|
+
stop = args[1] if len(args) > 1 else args[0]
|
|
330
|
+
step = args[2] if len(args) == 3 else "1"
|
|
331
|
+
if len(args) == 3 and not (
|
|
332
|
+
isinstance(iterator.args[2], Constant)
|
|
333
|
+
and type(iterator.args[2].value) is int
|
|
334
|
+
and iterator.args[2].value > 0
|
|
335
|
+
):
|
|
336
|
+
raise NotImplementedError("Browser ranges require a positive literal step")
|
|
337
|
+
serial += 1
|
|
338
|
+
initial = "bp_start_" + str(serial)
|
|
339
|
+
limit = "bp_limit_" + str(serial)
|
|
340
|
+
index = "bp_index_" + str(serial)
|
|
341
|
+
lines.append(" " * depth + "const " + initial + " = " + start + ";")
|
|
342
|
+
lines.append(" " * depth + "const " + limit + " = " + stop + ";")
|
|
343
|
+
text = f"for (let {index} = {initial}; {index} < {limit}; {index} += {step}) {{"
|
|
344
|
+
stack.append(Emission(None, "}", depth, exception))
|
|
345
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.body))
|
|
346
|
+
stack.append(Emission(None, variable + " = " + index + ";", depth + 1, exception))
|
|
347
|
+
lines.append(" " * depth + text)
|
|
348
|
+
continue
|
|
349
|
+
elif isinstance(iterator, (List, Tuple)) or (
|
|
350
|
+
isinstance(iterator, Name) and iterator.id in scope.arrays
|
|
351
|
+
):
|
|
352
|
+
text = "for (" + variable + " of " + expression_text(iterator, scope) + ") {"
|
|
353
|
+
else:
|
|
354
|
+
raise NotImplementedError("Browser iteration requires range or a known array")
|
|
355
|
+
else:
|
|
356
|
+
raise NotImplementedError("Browser loops require a single named target")
|
|
357
|
+
stack.append(Emission(None, "}", depth, exception))
|
|
358
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.body))
|
|
359
|
+
elif isinstance(node, Try):
|
|
360
|
+
if node.orelse or len(node.handlers) > 1:
|
|
361
|
+
raise NotImplementedError("Browser try supports one native catch and/or finally")
|
|
362
|
+
text = "try {"
|
|
363
|
+
stack.append(Emission(None, "}", depth, exception))
|
|
364
|
+
if node.finalbody:
|
|
365
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.finalbody))
|
|
366
|
+
stack.append(Emission(None, "} finally {", depth, exception))
|
|
367
|
+
if node.handlers:
|
|
368
|
+
handler = node.handlers[0]
|
|
369
|
+
if handler.type is not None and not (
|
|
370
|
+
isinstance(handler.type, Name) and handler.type.id == "Exception"
|
|
371
|
+
):
|
|
372
|
+
raise NotImplementedError(
|
|
373
|
+
"Browser catches handle native exceptions without Python type dispatch"
|
|
374
|
+
)
|
|
375
|
+
serial += 1
|
|
376
|
+
caught = "bp_error_" + str(serial)
|
|
377
|
+
stack.extend(Emission(item, "", depth + 1, caught) for item in reversed(handler.body))
|
|
378
|
+
if handler.name is not None:
|
|
379
|
+
stack.append(
|
|
380
|
+
Emission(None, "const p_" + handler.name + " = " + caught + ";", depth + 1, caught)
|
|
381
|
+
)
|
|
382
|
+
stack.append(Emission(None, "} catch (" + caught + ") {", depth, caught))
|
|
383
|
+
stack.extend(Emission(item, "", depth + 1, exception) for item in reversed(node.body))
|
|
384
|
+
elif isinstance(node, Pass):
|
|
385
|
+
continue
|
|
386
|
+
elif isinstance(node, Break):
|
|
387
|
+
text = "break;"
|
|
388
|
+
elif isinstance(node, Continue):
|
|
389
|
+
text = "continue;"
|
|
390
|
+
else:
|
|
391
|
+
raise NotImplementedError("Unsupported browser statement: " + type(node).__name__)
|
|
392
|
+
lines.append(" " * depth + text)
|
|
393
|
+
return "\n".join(lines)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def compile_browser_source(source: Path) -> str:
|
|
397
|
+
tree = parse(source.read_text())
|
|
398
|
+
names = set()
|
|
399
|
+
arities: dict[str, int] = {}
|
|
400
|
+
constructors = set()
|
|
401
|
+
arrays = set()
|
|
402
|
+
native = {"window", "document"}
|
|
403
|
+
functions: list[FunctionDef | AsyncFunctionDef] = []
|
|
404
|
+
lines = []
|
|
405
|
+
exports = []
|
|
406
|
+
entry: list[stmt] = []
|
|
407
|
+
dataclass_imported = False
|
|
408
|
+
imports_open = True
|
|
409
|
+
model_names = {item.name for item in tree.body if isinstance(item, ClassDef)}
|
|
410
|
+
for node in tree.body:
|
|
411
|
+
if isinstance(node, Expr) and isinstance(node.value, Constant) and isinstance(node.value.value, str):
|
|
412
|
+
continue
|
|
413
|
+
if isinstance(node, ImportFrom) and imports_open:
|
|
414
|
+
if node.level or node.module is None or not node.module.isidentifier():
|
|
415
|
+
raise NotImplementedError("Browser imports require direct module names")
|
|
416
|
+
symbols = [item.name for item in node.names]
|
|
417
|
+
if any(
|
|
418
|
+
item.asname is not None
|
|
419
|
+
or item.name == "*"
|
|
420
|
+
or item.name in names
|
|
421
|
+
or item.name in ("len", "str", "int", "range", "ValueError")
|
|
422
|
+
for item in node.names
|
|
423
|
+
):
|
|
424
|
+
raise NotImplementedError("Browser imports require unique unaliased symbols")
|
|
425
|
+
names.update(symbols)
|
|
426
|
+
if node.module == "browser":
|
|
427
|
+
if set(symbols) <= {"window", "document"}:
|
|
428
|
+
continue
|
|
429
|
+
raise NotImplementedError("Browser imports expose only native document and window")
|
|
430
|
+
if node.module == "dataclasses":
|
|
431
|
+
if symbols == ["dataclass"]:
|
|
432
|
+
dataclass_imported = True
|
|
433
|
+
continue
|
|
434
|
+
raise NotImplementedError("Browser dataclass imports expose only the decorator")
|
|
435
|
+
if node.module.startswith("__"):
|
|
436
|
+
raise NotImplementedError("Browser entry modules cannot be imported")
|
|
437
|
+
target = source.parent / (node.module + ".py")
|
|
438
|
+
if not target.is_file():
|
|
439
|
+
raise NotImplementedError("Browser imports must name local source modules: " + node.module)
|
|
440
|
+
definitions = {
|
|
441
|
+
item.name: item for item in parse(target.read_text()).body if isinstance(item, FunctionDef)
|
|
442
|
+
}
|
|
443
|
+
for symbol in symbols:
|
|
444
|
+
if symbol not in definitions:
|
|
445
|
+
raise NotImplementedError("Browser imports must name directly defined functions: " + symbol)
|
|
446
|
+
definition = definitions[symbol]
|
|
447
|
+
arities[symbol] = len(definition.args.args)
|
|
448
|
+
shape = annotation_shape(definition.returns, set())
|
|
449
|
+
if shape == "array":
|
|
450
|
+
arrays.add(symbol)
|
|
451
|
+
elif shape == "native":
|
|
452
|
+
native.add(symbol)
|
|
453
|
+
bindings = ", ".join(symbol + " as p_" + symbol for symbol in symbols)
|
|
454
|
+
lines.append("import { " + bindings + " } from './" + node.module + ".js';")
|
|
455
|
+
continue
|
|
456
|
+
imports_open = False
|
|
457
|
+
if isinstance(node, (FunctionDef, AsyncFunctionDef, ClassDef)):
|
|
458
|
+
if (
|
|
459
|
+
node.name in names
|
|
460
|
+
or node.name in native
|
|
461
|
+
or node.name in ("len", "str", "int", "range", "ValueError")
|
|
462
|
+
):
|
|
463
|
+
raise NotImplementedError("Duplicate/reserved browser name: " + node.name)
|
|
464
|
+
names.add(node.name)
|
|
465
|
+
exports.append("p_" + node.name + " as " + node.name)
|
|
466
|
+
if isinstance(node, (FunctionDef, AsyncFunctionDef)):
|
|
467
|
+
args = node.args
|
|
468
|
+
if (
|
|
469
|
+
node.decorator_list
|
|
470
|
+
or node.type_params
|
|
471
|
+
or args.defaults
|
|
472
|
+
or args.kw_defaults
|
|
473
|
+
or args.vararg
|
|
474
|
+
or args.kwarg
|
|
475
|
+
or args.kwonlyargs
|
|
476
|
+
or args.posonlyargs
|
|
477
|
+
):
|
|
478
|
+
raise NotImplementedError("Browser functions require plain positional parameters")
|
|
479
|
+
arities[node.name] = len(args.args)
|
|
480
|
+
for parameter in args.args:
|
|
481
|
+
annotation_shape(parameter.annotation, model_names)
|
|
482
|
+
shape = annotation_shape(node.returns, model_names)
|
|
483
|
+
if shape == "array":
|
|
484
|
+
arrays.add(node.name)
|
|
485
|
+
elif shape == "native":
|
|
486
|
+
native.add(node.name)
|
|
487
|
+
functions.append(node)
|
|
488
|
+
elif isinstance(node, ClassDef):
|
|
489
|
+
if not (
|
|
490
|
+
dataclass_imported
|
|
491
|
+
and len(node.decorator_list) == 1
|
|
492
|
+
and isinstance(node.decorator_list[0], Name)
|
|
493
|
+
and node.decorator_list[0].id == "dataclass"
|
|
494
|
+
and not node.bases
|
|
495
|
+
and not node.keywords
|
|
496
|
+
and not node.type_params
|
|
497
|
+
):
|
|
498
|
+
raise NotImplementedError("Browser classes must be plain required-field dataclasses")
|
|
499
|
+
fields = []
|
|
500
|
+
for field in node.body:
|
|
501
|
+
if isinstance(field, AnnAssign) and isinstance(field.target, Name) and field.value is None:
|
|
502
|
+
if field.target.id in fields:
|
|
503
|
+
raise NotImplementedError("Duplicate browser dataclass field")
|
|
504
|
+
if annotation_shape(field.annotation, model_names) in ("value", "record", "array"):
|
|
505
|
+
fields.append(field.target.id)
|
|
506
|
+
else:
|
|
507
|
+
raise NotImplementedError("Browser dataclass fields require explicit value annotations")
|
|
508
|
+
else:
|
|
509
|
+
raise NotImplementedError("Browser dataclasses require annotation-only fields")
|
|
510
|
+
constructors.add(node.name)
|
|
511
|
+
arities[node.name] = len(fields)
|
|
512
|
+
lines.append(
|
|
513
|
+
"class p_" + node.name + " {\nconstructor(" + ", ".join("p_" + item for item in fields) + ") {"
|
|
514
|
+
)
|
|
515
|
+
for field_name in fields:
|
|
516
|
+
lines.append("this." + field_name + " = p_" + field_name + ";")
|
|
517
|
+
lines.append("}\n}")
|
|
518
|
+
elif isinstance(node, (Expr, Try)):
|
|
519
|
+
entry.append(node)
|
|
520
|
+
else:
|
|
521
|
+
raise NotImplementedError("Browser module scope permits imports, definitions, and entry calls only")
|
|
522
|
+
calls = {}
|
|
523
|
+
function_names = {item.name for item in functions}
|
|
524
|
+
for function in functions:
|
|
525
|
+
calls[function.name] = {
|
|
526
|
+
item.func.id
|
|
527
|
+
for statement in function.body
|
|
528
|
+
for item in walk(statement)
|
|
529
|
+
if isinstance(item, Call) and isinstance(item.func, Name) and item.func.id in function_names
|
|
530
|
+
}
|
|
531
|
+
require_acyclic(calls, "Browser calls")
|
|
532
|
+
for function in functions:
|
|
533
|
+
parameters = {item.arg for item in function.args.args}
|
|
534
|
+
if len(parameters) != len(function.args.args):
|
|
535
|
+
raise NotImplementedError("Browser parameters must have unique names")
|
|
536
|
+
locals_ = set(parameters)
|
|
537
|
+
local_arrays = set(arrays)
|
|
538
|
+
local_native = set(native)
|
|
539
|
+
nodes: list[AST] = []
|
|
540
|
+
for parameter in function.args.args:
|
|
541
|
+
shape = annotation_shape(parameter.annotation, model_names)
|
|
542
|
+
if shape == "array":
|
|
543
|
+
local_arrays.add(parameter.arg)
|
|
544
|
+
elif shape == "native":
|
|
545
|
+
local_native.add(parameter.arg)
|
|
546
|
+
for statement in function.body:
|
|
547
|
+
nodes.extend(walk(statement))
|
|
548
|
+
for item in nodes:
|
|
549
|
+
if isinstance(item, (FunctionDef, AsyncFunctionDef, ClassDef, ImportFrom)):
|
|
550
|
+
raise NotImplementedError("Browser functions cannot contain nested definitions/imports")
|
|
551
|
+
if isinstance(item, Name) and isinstance(item.ctx, Store):
|
|
552
|
+
locals_.add(item.id)
|
|
553
|
+
if isinstance(item, AnnAssign):
|
|
554
|
+
annotation_shape(item.annotation, model_names)
|
|
555
|
+
if isinstance(item, ExceptHandler) and item.name is not None:
|
|
556
|
+
locals_.add(item.name)
|
|
557
|
+
if isinstance(item, Assign) and len(item.targets) == 1 and isinstance(item.targets[0], Name):
|
|
558
|
+
if isinstance(item.value, (List, Tuple)) or (
|
|
559
|
+
isinstance(item.value, IfExp)
|
|
560
|
+
and isinstance(item.value.body, (List, Tuple))
|
|
561
|
+
and isinstance(item.value.orelse, (List, Tuple))
|
|
562
|
+
):
|
|
563
|
+
local_arrays.add(item.targets[0].id)
|
|
564
|
+
else:
|
|
565
|
+
local_native.add(item.targets[0].id)
|
|
566
|
+
if locals_ & (set(arities) | {"window", "document", "len", "str", "int", "range", "ValueError"}):
|
|
567
|
+
raise NotImplementedError("Browser locals cannot shadow callables or native globals")
|
|
568
|
+
scope = BrowserScope(
|
|
569
|
+
names | locals_,
|
|
570
|
+
arities,
|
|
571
|
+
constructors,
|
|
572
|
+
local_arrays,
|
|
573
|
+
local_native,
|
|
574
|
+
isinstance(function, AsyncFunctionDef),
|
|
575
|
+
)
|
|
576
|
+
declaration = "async function " if scope.asynchronous else "function "
|
|
577
|
+
signature = ", ".join("p_" + item.arg for item in function.args.args)
|
|
578
|
+
lines.append(declaration + "p_" + function.name + "(" + signature + ") {")
|
|
579
|
+
declared = sorted(locals_ - parameters)
|
|
580
|
+
if declared:
|
|
581
|
+
lines.append("let " + ", ".join("p_" + item for item in declared) + ";")
|
|
582
|
+
lines.append(emit_statements(function.body, scope))
|
|
583
|
+
lines.append("}")
|
|
584
|
+
if entry:
|
|
585
|
+
for statement in entry:
|
|
586
|
+
for item in walk(statement):
|
|
587
|
+
if isinstance(item, Name) and isinstance(item.ctx, Store):
|
|
588
|
+
raise NotImplementedError("Browser entry state belongs inside a function")
|
|
589
|
+
caught_names = {
|
|
590
|
+
item.name
|
|
591
|
+
for statement in entry
|
|
592
|
+
for item in walk(statement)
|
|
593
|
+
if isinstance(item, ExceptHandler) and item.name is not None
|
|
594
|
+
}
|
|
595
|
+
scope = BrowserScope(names | caught_names, arities, constructors, arrays, native, True)
|
|
596
|
+
lines.append(emit_statements(entry, scope))
|
|
597
|
+
lines.append("export { " + ", ".join(exports) + " };\n")
|
|
598
|
+
return "\n".join(lines)
|
compiler_command.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
from os import environ
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from subprocess import run
|
|
5
|
+
from sys import executable
|
|
6
|
+
from tempfile import mkdtemp
|
|
7
|
+
|
|
8
|
+
from browser_compile import compile_modules
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
parser = ArgumentParser(prog="browser-py", description="Compile Python for the browser")
|
|
13
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
compile_command = commands.add_parser(
|
|
15
|
+
"compile", help="Compile a typed numeric Python module and its imports"
|
|
16
|
+
)
|
|
17
|
+
compile_command.add_argument("source", type=Path)
|
|
18
|
+
compile_command.add_argument(
|
|
19
|
+
"--unminified", action="store_true", help="Keep generated modules uncompressed"
|
|
20
|
+
)
|
|
21
|
+
compile_command.add_argument("--output", type=Path, help="Write compiled files to this directory")
|
|
22
|
+
commands.add_parser("validate", help="Run synchronous regression checks from a source checkout")
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
root = Path(__file__).resolve().parents[1]
|
|
25
|
+
if args.command == "compile":
|
|
26
|
+
output = args.output
|
|
27
|
+
if output is None:
|
|
28
|
+
output = Path(mkdtemp(prefix="browser-py-compile-", dir="/tmp"))
|
|
29
|
+
entry = compile_modules(args.source, output, not args.unminified)
|
|
30
|
+
print(f"Compiled {entry}", flush=True)
|
|
31
|
+
print("Orchestration: command -> source admission -> mypyc IR -> browser-py emission -> ES modules")
|
|
32
|
+
return
|
|
33
|
+
if args.command == "validate":
|
|
34
|
+
if not (root / "pyproject.toml").is_file() or not (root / "tests").is_dir():
|
|
35
|
+
parser.error("validate requires a source checkout; run python -B browser_py.py validate there")
|
|
36
|
+
environment = {**environ, "PYTHONPATH": str(root / "source"), "PYTHONDONTWRITEBYTECODE": "1"}
|
|
37
|
+
run(
|
|
38
|
+
[executable, "-B", "-m", "ruff", "check", str(root / "source"), str(root / "tests")],
|
|
39
|
+
env=environment,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
run(
|
|
43
|
+
[executable, "-B", "-m", "ruff", "format", "--check", str(root / "source"), str(root / "tests")],
|
|
44
|
+
env=environment,
|
|
45
|
+
check=True,
|
|
46
|
+
)
|
|
47
|
+
run(
|
|
48
|
+
[executable, "-B", "-m", "mypy", str(root / "source"), str(root / "tests")],
|
|
49
|
+
env=environment,
|
|
50
|
+
check=True,
|
|
51
|
+
)
|
|
52
|
+
run([executable, "-B", "-m", "pytest", str(root / "tests")], env=environment, check=True)
|
|
53
|
+
print("Orchestration: command -> Ruff -> mypy -> regression tests")
|