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
numeric_ir.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from mypy.build import build
|
|
5
|
+
from mypyc.build import get_mypy_config
|
|
6
|
+
from mypyc.errors import Errors
|
|
7
|
+
from mypyc.ir.func_ir import FuncIR, all_values
|
|
8
|
+
from mypyc.ir.module_ir import ModuleIR
|
|
9
|
+
from mypyc.ir.ops import (
|
|
10
|
+
Assign,
|
|
11
|
+
BasicBlock,
|
|
12
|
+
Box,
|
|
13
|
+
Branch,
|
|
14
|
+
Call,
|
|
15
|
+
CallC,
|
|
16
|
+
Cast,
|
|
17
|
+
ComparisonOp,
|
|
18
|
+
Float,
|
|
19
|
+
FloatComparisonOp,
|
|
20
|
+
FloatNeg,
|
|
21
|
+
FloatOp,
|
|
22
|
+
GetAttr,
|
|
23
|
+
Goto,
|
|
24
|
+
Integer,
|
|
25
|
+
IntOp,
|
|
26
|
+
KeepAlive,
|
|
27
|
+
LoadLiteral,
|
|
28
|
+
Op,
|
|
29
|
+
PrimitiveOp,
|
|
30
|
+
RaiseStandardError,
|
|
31
|
+
Return,
|
|
32
|
+
SetAttr,
|
|
33
|
+
Unbox,
|
|
34
|
+
Unreachable,
|
|
35
|
+
Value,
|
|
36
|
+
)
|
|
37
|
+
from mypyc.ir.pprint import format_func
|
|
38
|
+
from mypyc.irbuild.main import build_ir
|
|
39
|
+
from mypyc.irbuild.mapper import Mapper
|
|
40
|
+
from mypyc.options import CompilerOptions
|
|
41
|
+
|
|
42
|
+
from numeric_source import read_numeric_source
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Region:
|
|
47
|
+
block: BasicBlock | None
|
|
48
|
+
stop: BasicBlock | None
|
|
49
|
+
active: frozenset[BasicBlock]
|
|
50
|
+
text: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_ir(inputs: tuple[Path, ...]) -> dict[str, ModuleIR]:
|
|
54
|
+
compiler_options = CompilerOptions()
|
|
55
|
+
config = get_mypy_config([str(path) for path in inputs], None, compiler_options, None)
|
|
56
|
+
sources = config[0]
|
|
57
|
+
all_sources = config[1]
|
|
58
|
+
options = config[2]
|
|
59
|
+
options.cache_dir = "/tmp/python-js-mypy-cache"
|
|
60
|
+
result = build(all_sources, options)
|
|
61
|
+
if result.errors:
|
|
62
|
+
raise RuntimeError("\n".join(result.errors))
|
|
63
|
+
errors = Errors(options)
|
|
64
|
+
modules = []
|
|
65
|
+
for item in sources:
|
|
66
|
+
tree = result.graph[item.module].tree
|
|
67
|
+
if tree is not None:
|
|
68
|
+
modules.append(tree)
|
|
69
|
+
else:
|
|
70
|
+
raise RuntimeError("mypy did not produce a syntax tree for " + item.module)
|
|
71
|
+
modules_ir = build_ir(modules, result.graph, result.types, Mapper({}), compiler_options, errors)
|
|
72
|
+
if errors.num_errors:
|
|
73
|
+
raise RuntimeError("\n".join(errors.new_messages()))
|
|
74
|
+
return modules_ir
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def value_text(value: Value, names: dict[Value, str]) -> str:
|
|
78
|
+
if isinstance(value, Integer):
|
|
79
|
+
return str(value.numeric_value())
|
|
80
|
+
if isinstance(value, Float):
|
|
81
|
+
return repr(value.value)
|
|
82
|
+
return names[value]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def arguments(values: list[Value], names: dict[Value, str]) -> list[str]:
|
|
86
|
+
result = []
|
|
87
|
+
for value in values:
|
|
88
|
+
result.append(value_text(value, names))
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def statement(op: Op, names: dict[Value, str], constructors: frozenset[str]) -> str:
|
|
93
|
+
if isinstance(op, Assign):
|
|
94
|
+
return f"{value_text(op.dest, names)} = {value_text(op.src, names)};"
|
|
95
|
+
if isinstance(op, (FloatOp, FloatComparisonOp, ComparisonOp)):
|
|
96
|
+
symbol = op.op_str[op.op]
|
|
97
|
+
if symbol not in {"+", "-", "*", "/", "%", "==", "!=", "<", ">", "<=", ">="}:
|
|
98
|
+
raise NotImplementedError(symbol)
|
|
99
|
+
expr = f"{value_text(op.lhs, names)} {symbol} {value_text(op.rhs, names)}"
|
|
100
|
+
elif isinstance(op, FloatNeg):
|
|
101
|
+
expr = f"-{value_text(op.src, names)}"
|
|
102
|
+
elif isinstance(op, GetAttr):
|
|
103
|
+
expr = f"{value_text(op.obj, names)}.{op.attr}"
|
|
104
|
+
elif isinstance(op, SetAttr):
|
|
105
|
+
expr = f"({value_text(op.obj, names)}.{op.attr} = {value_text(op.src, names)}, true)"
|
|
106
|
+
elif isinstance(op, LoadLiteral) and isinstance(op.value, (int, float)):
|
|
107
|
+
expr = repr(op.value)
|
|
108
|
+
elif isinstance(op, (Box, Unbox, Cast)):
|
|
109
|
+
expr = value_text(op.src, names)
|
|
110
|
+
elif isinstance(op, KeepAlive):
|
|
111
|
+
return ""
|
|
112
|
+
elif isinstance(op, RaiseStandardError):
|
|
113
|
+
return f"throw new Error({op.class_name!r});"
|
|
114
|
+
elif isinstance(op, IntOp):
|
|
115
|
+
if (
|
|
116
|
+
op.op == IntOp.LEFT_SHIFT
|
|
117
|
+
and isinstance(op.rhs, Integer)
|
|
118
|
+
and op.rhs.numeric_value() == 1
|
|
119
|
+
and str(op.lhs.type) == "native_int"
|
|
120
|
+
and str(op.type) == "short_int"
|
|
121
|
+
):
|
|
122
|
+
# mypyc tags len() integers; JavaScript keeps the untagged numeric length.
|
|
123
|
+
expr = value_text(op.lhs, names)
|
|
124
|
+
else:
|
|
125
|
+
raise NotImplementedError("Unsupported representation-level IntOp")
|
|
126
|
+
elif isinstance(op, PrimitiveOp):
|
|
127
|
+
name = op.desc.name
|
|
128
|
+
args = arguments(op.args, names)
|
|
129
|
+
comparisons = {"int_lt": "<", "int_le": "<=", "int_gt": ">", "int_eq": "=="}
|
|
130
|
+
if name in comparisons:
|
|
131
|
+
expr = f"{args[0]} {comparisons[name]} {args[1]}"
|
|
132
|
+
elif name == "var_object_size":
|
|
133
|
+
expr = f"{args[0]}.length"
|
|
134
|
+
elif name == "list_items":
|
|
135
|
+
expr = args[0]
|
|
136
|
+
elif name == "buf_init_item":
|
|
137
|
+
return f"{args[0]}[{args[1]}] = {args[2]};"
|
|
138
|
+
else:
|
|
139
|
+
raise NotImplementedError(f"primitive {name}")
|
|
140
|
+
elif isinstance(op, CallC):
|
|
141
|
+
name = op.function_name
|
|
142
|
+
args = arguments(op.args, names)
|
|
143
|
+
symbols = {"CPyTagged_Add": "+", "CPyTagged_Subtract": "-", "CPyTagged_Multiply": "*"}
|
|
144
|
+
if name in symbols:
|
|
145
|
+
expr = f"{args[0]} {symbols[name]} {args[1]}"
|
|
146
|
+
elif name == "CPyTagged_Remainder":
|
|
147
|
+
expr = f"((({args[0]} % {args[1]}) + {args[1]}) % {args[1]})"
|
|
148
|
+
elif name == "CPyFloat_FromTagged":
|
|
149
|
+
expr = args[0]
|
|
150
|
+
elif name == "PyList_New":
|
|
151
|
+
expr = f"new Array({args[0]})"
|
|
152
|
+
elif name in {"CPyList_GetItem", "CPyList_GetItemShort"}:
|
|
153
|
+
expr = f"{args[0]}[{args[1]}]"
|
|
154
|
+
elif name == "CPyList_SetItem":
|
|
155
|
+
expr = f"({args[0]}[{args[1]}] = {args[2]}, true)"
|
|
156
|
+
elif name == "PyList_Append":
|
|
157
|
+
expr = f"({args[0]}.push({args[1]}), 0)"
|
|
158
|
+
elif name == "PyNumber_InPlaceAdd":
|
|
159
|
+
expr = f"{args[0]} + {args[1]}"
|
|
160
|
+
elif name == "copysign":
|
|
161
|
+
expr = (
|
|
162
|
+
f"((({args[1]}) < 0 || Object.is({args[1]}, -0)) ? -Math.abs({args[0]}) : Math.abs({args[0]}))"
|
|
163
|
+
)
|
|
164
|
+
elif name == "PyTuple_Pack":
|
|
165
|
+
expr = f"[{', '.join(args[1:])}]"
|
|
166
|
+
elif name == "PyDict_New":
|
|
167
|
+
expr = "{}"
|
|
168
|
+
else:
|
|
169
|
+
raise NotImplementedError(f"C primitive {name}")
|
|
170
|
+
elif isinstance(op, Call) and op.fn.class_name is None:
|
|
171
|
+
if op.fn.name in constructors:
|
|
172
|
+
# mypyc passes dataclass positional/keyword containers; native JS consumes the positional fields.
|
|
173
|
+
expr = f"new {op.fn.name}(...{value_text(op.args[0], names)})"
|
|
174
|
+
else:
|
|
175
|
+
expr = f"{op.fn.name}({', '.join(arguments(op.args, names))})"
|
|
176
|
+
else:
|
|
177
|
+
raise NotImplementedError(f"unsupported IR {type(op).__name__}")
|
|
178
|
+
return f"{value_text(op, names)} = {expr};" if op in names else f"{expr};"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def condition(branch: Branch, names: dict[Value, str]) -> str:
|
|
182
|
+
if branch.op == Branch.BOOL:
|
|
183
|
+
return ("!" if branch.negated else "") + value_text(branch.value, names)
|
|
184
|
+
raise NotImplementedError("Error-value branches are not supported")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def emit_function(fn: FuncIR, constructors: frozenset[str]) -> str:
|
|
188
|
+
values = all_values(fn.arg_regs, fn.blocks)
|
|
189
|
+
names = {value: f"v{i}" for i, value in enumerate(values)}
|
|
190
|
+
blocks = fn.blocks
|
|
191
|
+
exceptional = set()
|
|
192
|
+
for block in blocks:
|
|
193
|
+
for op in block.ops:
|
|
194
|
+
if isinstance(op, RaiseStandardError):
|
|
195
|
+
exceptional.add(block)
|
|
196
|
+
successors: dict[BasicBlock, list[BasicBlock]] = {}
|
|
197
|
+
for block in blocks:
|
|
198
|
+
term = block.ops[-1]
|
|
199
|
+
successors[block] = []
|
|
200
|
+
if isinstance(term, (Goto, Branch)):
|
|
201
|
+
for target in term.targets():
|
|
202
|
+
if target not in exceptional:
|
|
203
|
+
successors[block].append(target)
|
|
204
|
+
# Postdominators reconcile each branch with its closest shared continuation before emission.
|
|
205
|
+
post = {block: set(blocks) if successors[block] else {block} for block in blocks}
|
|
206
|
+
while True:
|
|
207
|
+
updated = {}
|
|
208
|
+
for block in blocks:
|
|
209
|
+
shared = set(blocks) if successors[block] else set()
|
|
210
|
+
for target in successors[block]:
|
|
211
|
+
shared &= post[target]
|
|
212
|
+
updated[block] = {block} | shared
|
|
213
|
+
if updated == post:
|
|
214
|
+
break
|
|
215
|
+
post = updated
|
|
216
|
+
index = {block: i for i, block in enumerate(blocks)}
|
|
217
|
+
headers = set()
|
|
218
|
+
for block in blocks:
|
|
219
|
+
for target in successors[block]:
|
|
220
|
+
if index[target] <= index[block]:
|
|
221
|
+
headers.add(target)
|
|
222
|
+
signature = ", ".join(arguments(list(fn.arg_regs), names))
|
|
223
|
+
lines = [f"export function {fn.name}({signature}) {{"]
|
|
224
|
+
locals_ = [names[value] for value in values if value not in fn.arg_regs]
|
|
225
|
+
if locals_:
|
|
226
|
+
lines.append(f"let {', '.join(locals_)};")
|
|
227
|
+
emitted: set[BasicBlock] = set()
|
|
228
|
+
stack = [Region(blocks[0], None, frozenset(), "")]
|
|
229
|
+
while stack:
|
|
230
|
+
region = stack.pop()
|
|
231
|
+
if region.block is None:
|
|
232
|
+
lines.append(region.text)
|
|
233
|
+
continue
|
|
234
|
+
block = region.block
|
|
235
|
+
while block is not region.stop:
|
|
236
|
+
if block in emitted:
|
|
237
|
+
raise NotImplementedError(f"{fn.name}: unstructured CFG")
|
|
238
|
+
emitted.add(block)
|
|
239
|
+
term = block.ops[-1]
|
|
240
|
+
body = [statement(op, names, constructors) for op in block.ops[:-1]]
|
|
241
|
+
if block in headers and block not in region.active:
|
|
242
|
+
if isinstance(term, Branch) and block in post[term.true]:
|
|
243
|
+
lines.extend(["while (true) {", *body, f"if (!({condition(term, names)})) break;"])
|
|
244
|
+
stack.append(Region(term.false, region.stop, region.active, ""))
|
|
245
|
+
stack.append(Region(None, None, frozenset(), "}"))
|
|
246
|
+
stack.append(Region(term.true, block, region.active | {block}, ""))
|
|
247
|
+
break
|
|
248
|
+
raise NotImplementedError("Unsupported loop shape")
|
|
249
|
+
lines.extend(body)
|
|
250
|
+
if isinstance(term, Unreachable) and block in exceptional:
|
|
251
|
+
break
|
|
252
|
+
if isinstance(term, Return):
|
|
253
|
+
lines.append(f"return {value_text(term.value, names)};")
|
|
254
|
+
break
|
|
255
|
+
if isinstance(term, Goto):
|
|
256
|
+
block = term.label
|
|
257
|
+
continue
|
|
258
|
+
if isinstance(term, Branch):
|
|
259
|
+
if term.true in exceptional or term.false in exceptional:
|
|
260
|
+
error = term.true if term.true in exceptional else term.false
|
|
261
|
+
normal = term.false if error is term.true else term.true
|
|
262
|
+
cond = condition(term, names) if error is term.true else f"!({condition(term, names)})"
|
|
263
|
+
lines.append(f"if ({cond}) {{")
|
|
264
|
+
stack.append(Region(normal, region.stop, region.active, ""))
|
|
265
|
+
stack.append(Region(None, None, frozenset(), "}"))
|
|
266
|
+
stack.append(Region(error, None, frozenset(), ""))
|
|
267
|
+
break
|
|
268
|
+
common = post[term.true] & post[term.false]
|
|
269
|
+
if not common:
|
|
270
|
+
raise NotImplementedError("Conditional without a merge")
|
|
271
|
+
join = blocks[0]
|
|
272
|
+
depth = -1
|
|
273
|
+
for candidate in blocks:
|
|
274
|
+
if candidate in common and len(post[candidate]) > depth:
|
|
275
|
+
join = candidate
|
|
276
|
+
depth = len(post[candidate])
|
|
277
|
+
lines.append(f"if ({condition(term, names)}) {{")
|
|
278
|
+
stack.append(Region(join, region.stop, region.active, ""))
|
|
279
|
+
stack.append(Region(None, None, frozenset(), "}"))
|
|
280
|
+
stack.append(Region(term.false, join, region.active, ""))
|
|
281
|
+
stack.append(Region(None, None, frozenset(), "} else {"))
|
|
282
|
+
stack.append(Region(term.true, join, region.active, ""))
|
|
283
|
+
break
|
|
284
|
+
raise NotImplementedError(type(term).__name__)
|
|
285
|
+
return "\n".join([*lines, "}"])
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def emit_module(module: ModuleIR, fields: dict[str, list[str]]) -> tuple[str, str]:
|
|
289
|
+
functions = [fn for fn in module.functions if fn.name != "__top_level__"]
|
|
290
|
+
constructors = frozenset(fields)
|
|
291
|
+
imports: dict[str, set[str]] = {}
|
|
292
|
+
for fn in functions:
|
|
293
|
+
for block in fn.blocks:
|
|
294
|
+
for op in block.ops:
|
|
295
|
+
if isinstance(op, Call) and op.fn.module_name != module.fullname:
|
|
296
|
+
imports.setdefault(op.fn.module_name, set()).add(op.fn.name)
|
|
297
|
+
js = ""
|
|
298
|
+
for name, symbols in sorted(imports.items()):
|
|
299
|
+
js += f"import {{ {', '.join(sorted(symbols))} }} from './{name}.js';\n"
|
|
300
|
+
for cls in module.classes:
|
|
301
|
+
attributes = fields[cls.name]
|
|
302
|
+
js += f"export class {cls.name} {{\nconstructor({', '.join(attributes)}) {{\n"
|
|
303
|
+
for attribute in attributes:
|
|
304
|
+
js += f"this.{attribute} = {attribute};\n"
|
|
305
|
+
js += "}\n}\n"
|
|
306
|
+
for fn in functions:
|
|
307
|
+
js += emit_function(fn, constructors) + "\n"
|
|
308
|
+
dump = "\n\n".join("\n".join(format_func(fn)) for fn in module.functions)
|
|
309
|
+
return js, dump
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def compile_js(source: Path) -> tuple[str, str]:
|
|
313
|
+
parsed = read_numeric_source(source)
|
|
314
|
+
module = load_ir(tuple(parsed.trees))[source.stem]
|
|
315
|
+
return emit_module(module, parsed.schemas[source])
|
numeric_source.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from ast import (
|
|
2
|
+
AnnAssign,
|
|
3
|
+
AsyncFunctionDef,
|
|
4
|
+
Attribute,
|
|
5
|
+
Await,
|
|
6
|
+
Call,
|
|
7
|
+
ClassDef,
|
|
8
|
+
Constant,
|
|
9
|
+
Expr,
|
|
10
|
+
FunctionDef,
|
|
11
|
+
Global,
|
|
12
|
+
Import,
|
|
13
|
+
ImportFrom,
|
|
14
|
+
Lambda,
|
|
15
|
+
Module,
|
|
16
|
+
Name,
|
|
17
|
+
Nonlocal,
|
|
18
|
+
Starred,
|
|
19
|
+
Subscript,
|
|
20
|
+
expr,
|
|
21
|
+
parse,
|
|
22
|
+
walk,
|
|
23
|
+
)
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
BUILTIN_ARITIES = (
|
|
28
|
+
("int", (0, 1)),
|
|
29
|
+
("float", (0, 1)),
|
|
30
|
+
("len", (1,)),
|
|
31
|
+
("range", (1, 2, 3)),
|
|
32
|
+
# Admission keeps these names for types/decorators; backends receive literals and generated constructors.
|
|
33
|
+
("list", ()),
|
|
34
|
+
("dataclass", ()),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Source dataclasses define field order; both backends consume the same admitted model before emission.
|
|
39
|
+
@dataclass
|
|
40
|
+
class NumericSource:
|
|
41
|
+
trees: dict[Path, Module]
|
|
42
|
+
schemas: dict[Path, dict[str, list[str]]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def require_acyclic(edges: dict[str, set[str]], subject: str) -> None:
|
|
46
|
+
remaining = {name: len(edges[name]) for name in edges}
|
|
47
|
+
callers: dict[str, list[str]] = {name: [] for name in edges}
|
|
48
|
+
for name in edges:
|
|
49
|
+
for target in edges[name]:
|
|
50
|
+
callers[target].append(name)
|
|
51
|
+
ready = [name for name in remaining if remaining[name] == 0]
|
|
52
|
+
while ready:
|
|
53
|
+
name = ready.pop()
|
|
54
|
+
del remaining[name]
|
|
55
|
+
for caller in callers[name]:
|
|
56
|
+
remaining[caller] -= 1
|
|
57
|
+
if remaining[caller] == 0:
|
|
58
|
+
ready.append(caller)
|
|
59
|
+
if remaining:
|
|
60
|
+
raise NotImplementedError(subject + " must be acyclic: " + ", ".join(remaining))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def numeric_annotation(node: expr | None, classes: set[str]) -> bool:
|
|
64
|
+
if isinstance(node, Constant):
|
|
65
|
+
return isinstance(node.value, str) and node.value in classes
|
|
66
|
+
if isinstance(node, Name):
|
|
67
|
+
return node.id in ("int", "float") or node.id in classes
|
|
68
|
+
if isinstance(node, Subscript):
|
|
69
|
+
return (
|
|
70
|
+
isinstance(node.value, Name)
|
|
71
|
+
and node.value.id == "list"
|
|
72
|
+
and isinstance(node.slice, Name)
|
|
73
|
+
and (node.slice.id in ("int", "float") or node.slice.id in classes)
|
|
74
|
+
)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def function_calls(
|
|
79
|
+
node: FunctionDef,
|
|
80
|
+
classes: dict[str, list[str]],
|
|
81
|
+
functions: dict[str, FunctionDef],
|
|
82
|
+
imports: dict[str, FunctionDef],
|
|
83
|
+
) -> set[str]:
|
|
84
|
+
args = node.args
|
|
85
|
+
if (
|
|
86
|
+
node.decorator_list
|
|
87
|
+
or node.type_params
|
|
88
|
+
or args.defaults
|
|
89
|
+
or args.kw_defaults
|
|
90
|
+
or args.posonlyargs
|
|
91
|
+
or args.vararg
|
|
92
|
+
or args.kwarg
|
|
93
|
+
or args.kwonlyargs
|
|
94
|
+
):
|
|
95
|
+
raise NotImplementedError("Numeric functions require plain positional parameters")
|
|
96
|
+
class_names = set(classes)
|
|
97
|
+
annotations = [arg.annotation for arg in args.args] + [node.returns]
|
|
98
|
+
for annotation in annotations:
|
|
99
|
+
if numeric_annotation(annotation, class_names):
|
|
100
|
+
continue
|
|
101
|
+
raise NotImplementedError("Explicit int/float or numeric-list signatures required")
|
|
102
|
+
arities = dict(BUILTIN_ARITIES)
|
|
103
|
+
for name in functions:
|
|
104
|
+
arities[name] = (len(functions[name].args.args),)
|
|
105
|
+
for name in imports:
|
|
106
|
+
arities[name] = (len(imports[name].args.args),)
|
|
107
|
+
for name in classes:
|
|
108
|
+
arities[name] = (len(classes[name]),)
|
|
109
|
+
for arg in args.args:
|
|
110
|
+
if arg.arg in arities:
|
|
111
|
+
raise NotImplementedError("Numeric parameters cannot shadow callables: " + arg.arg)
|
|
112
|
+
nodes = list(walk(node))
|
|
113
|
+
for item in nodes:
|
|
114
|
+
if isinstance(item, AnnAssign):
|
|
115
|
+
if numeric_annotation(item.annotation, class_names):
|
|
116
|
+
annotations.append(item.annotation)
|
|
117
|
+
else:
|
|
118
|
+
raise NotImplementedError("Only numeric local annotations are supported")
|
|
119
|
+
annotation_names: set[Name] = set()
|
|
120
|
+
for annotation in annotations:
|
|
121
|
+
if annotation is not None:
|
|
122
|
+
for item in walk(annotation):
|
|
123
|
+
if isinstance(item, Name):
|
|
124
|
+
annotation_names.add(item)
|
|
125
|
+
targets = {item.func for item in nodes if isinstance(item, Call)}
|
|
126
|
+
dependencies = set()
|
|
127
|
+
for item in nodes:
|
|
128
|
+
if isinstance(item, (Import, ImportFrom, AsyncFunctionDef, Await, Lambda)) or (
|
|
129
|
+
isinstance(item, (FunctionDef, ClassDef)) and item is not node
|
|
130
|
+
):
|
|
131
|
+
raise NotImplementedError("Numeric functions cannot contain imports, closures, or async code")
|
|
132
|
+
if isinstance(item, (Global, Nonlocal)):
|
|
133
|
+
raise NotImplementedError("Numeric functions cannot declare nonlocal or global state")
|
|
134
|
+
if isinstance(item, Name) and item.id in arities:
|
|
135
|
+
if item in targets or item in annotation_names:
|
|
136
|
+
pass
|
|
137
|
+
else:
|
|
138
|
+
raise NotImplementedError("Numeric callables must be called directly")
|
|
139
|
+
if isinstance(item, Call):
|
|
140
|
+
if item.keywords or any(isinstance(argument, Starred) for argument in item.args):
|
|
141
|
+
raise NotImplementedError("Numeric calls require positional arguments")
|
|
142
|
+
if isinstance(item.func, Name) and item.func.id in arities:
|
|
143
|
+
if len(item.args) in arities[item.func.id]:
|
|
144
|
+
if item.func.id in functions:
|
|
145
|
+
dependencies.add(item.func.id)
|
|
146
|
+
else:
|
|
147
|
+
raise NotImplementedError("Unsupported numeric positional arity: " + item.func.id)
|
|
148
|
+
elif isinstance(item.func, Attribute) and item.func.attr == "append":
|
|
149
|
+
if len(item.args) == 1:
|
|
150
|
+
continue
|
|
151
|
+
raise NotImplementedError("List append requires positional arity one")
|
|
152
|
+
else:
|
|
153
|
+
raise NotImplementedError("Only direct numeric calls and list append are supported")
|
|
154
|
+
return dependencies
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_numeric_source(source: Path) -> NumericSource:
|
|
158
|
+
pending = [source]
|
|
159
|
+
trees: dict[Path, Module] = {}
|
|
160
|
+
schemas: dict[Path, dict[str, list[str]]] = {}
|
|
161
|
+
definitions: dict[Path, dict[str, FunctionDef]] = {}
|
|
162
|
+
dependencies: dict[str, set[str]] = {}
|
|
163
|
+
reserved = {item[0] for item in BUILTIN_ARITIES}
|
|
164
|
+
while pending:
|
|
165
|
+
path = pending.pop()
|
|
166
|
+
if path in trees:
|
|
167
|
+
continue
|
|
168
|
+
tree = parse(path.read_text())
|
|
169
|
+
trees[path] = tree
|
|
170
|
+
fields: dict[str, list[str]] = {}
|
|
171
|
+
schemas[path] = fields
|
|
172
|
+
dependencies[path.stem] = set()
|
|
173
|
+
classes = {node.name for node in tree.body if isinstance(node, ClassDef)}
|
|
174
|
+
definitions[path] = {node.name: node for node in tree.body if isinstance(node, FunctionDef)}
|
|
175
|
+
symbols = set()
|
|
176
|
+
dataclass_imported = False
|
|
177
|
+
imports_open = True
|
|
178
|
+
for node in tree.body:
|
|
179
|
+
if isinstance(node, Expr) and isinstance(node.value, Constant) and isinstance(node.value.value, str):
|
|
180
|
+
continue
|
|
181
|
+
if isinstance(node, ImportFrom):
|
|
182
|
+
if not imports_open:
|
|
183
|
+
raise NotImplementedError("Numeric imports must precede definitions")
|
|
184
|
+
if node.level == 0 and node.module == "dataclasses" and len(node.names) == 1:
|
|
185
|
+
alias = node.names[0]
|
|
186
|
+
if alias.name == "dataclass" and alias.asname is None:
|
|
187
|
+
if alias.name in symbols:
|
|
188
|
+
raise NotImplementedError("Duplicate numeric symbol: " + alias.name)
|
|
189
|
+
symbols.add(alias.name)
|
|
190
|
+
dataclass_imported = True
|
|
191
|
+
continue
|
|
192
|
+
if (
|
|
193
|
+
node.level == 0
|
|
194
|
+
and node.module is not None
|
|
195
|
+
and node.module.isidentifier()
|
|
196
|
+
and not node.module.startswith("__")
|
|
197
|
+
and node.module != "dataclasses"
|
|
198
|
+
and (path.parent / f"{node.module}.py").is_file()
|
|
199
|
+
and all(alias.asname is None and alias.name != "*" for alias in node.names)
|
|
200
|
+
):
|
|
201
|
+
dependency = path.parent / f"{node.module}.py"
|
|
202
|
+
dependencies[path.stem].add(dependency.stem)
|
|
203
|
+
pending.append(dependency)
|
|
204
|
+
for alias in node.names:
|
|
205
|
+
if alias.name in reserved:
|
|
206
|
+
raise NotImplementedError("Import shadows a reserved numeric name: " + alias.name)
|
|
207
|
+
if alias.name in symbols:
|
|
208
|
+
raise NotImplementedError("Duplicate numeric symbol: " + alias.name)
|
|
209
|
+
symbols.add(alias.name)
|
|
210
|
+
continue
|
|
211
|
+
raise NotImplementedError("Only direct local imports of compiled functions supported")
|
|
212
|
+
imports_open = False
|
|
213
|
+
if isinstance(node, (ClassDef, FunctionDef)):
|
|
214
|
+
if node.name in reserved:
|
|
215
|
+
raise NotImplementedError("Definition shadows a reserved numeric name: " + node.name)
|
|
216
|
+
if node.name in symbols:
|
|
217
|
+
raise NotImplementedError("Duplicate numeric symbol: " + node.name)
|
|
218
|
+
symbols.add(node.name)
|
|
219
|
+
if isinstance(node, ClassDef):
|
|
220
|
+
if (
|
|
221
|
+
node.bases
|
|
222
|
+
or node.keywords
|
|
223
|
+
or node.type_params
|
|
224
|
+
or len(node.decorator_list) != 1
|
|
225
|
+
or not isinstance(node.decorator_list[0], Name)
|
|
226
|
+
or node.decorator_list[0].id != "dataclass"
|
|
227
|
+
or not dataclass_imported
|
|
228
|
+
):
|
|
229
|
+
raise NotImplementedError("Only plain required-field dataclasses are supported")
|
|
230
|
+
fields[node.name] = []
|
|
231
|
+
for item in node.body:
|
|
232
|
+
if (
|
|
233
|
+
isinstance(item, AnnAssign)
|
|
234
|
+
and isinstance(item.target, Name)
|
|
235
|
+
and item.value is None
|
|
236
|
+
and numeric_annotation(item.annotation, classes)
|
|
237
|
+
):
|
|
238
|
+
if item.target.id in fields[node.name]:
|
|
239
|
+
raise NotImplementedError("Duplicate numeric dataclass field: " + item.target.id)
|
|
240
|
+
fields[node.name].append(item.target.id)
|
|
241
|
+
continue
|
|
242
|
+
raise NotImplementedError("Only required numeric annotation-only dataclass fields supported")
|
|
243
|
+
continue
|
|
244
|
+
if isinstance(node, FunctionDef):
|
|
245
|
+
continue
|
|
246
|
+
raise NotImplementedError(
|
|
247
|
+
"Only typed dataclasses/functions/imports/docstrings allowed at module scope"
|
|
248
|
+
)
|
|
249
|
+
require_acyclic(dependencies, "Numeric imports")
|
|
250
|
+
for path in trees:
|
|
251
|
+
imports: dict[str, FunctionDef] = {}
|
|
252
|
+
for node in trees[path].body:
|
|
253
|
+
if isinstance(node, ImportFrom) and node.module != "dataclasses":
|
|
254
|
+
exports = definitions[path.parent / f"{node.module}.py"]
|
|
255
|
+
for alias in node.names:
|
|
256
|
+
if alias.name in exports:
|
|
257
|
+
imports[alias.name] = exports[alias.name]
|
|
258
|
+
else:
|
|
259
|
+
raise NotImplementedError(
|
|
260
|
+
f"Numeric imports must name functions defined in {node.module}: {alias.name}"
|
|
261
|
+
)
|
|
262
|
+
calls = {}
|
|
263
|
+
for name in definitions[path]:
|
|
264
|
+
calls[name] = function_calls(definitions[path][name], schemas[path], definitions[path], imports)
|
|
265
|
+
require_acyclic(calls, "Numeric calls")
|
|
266
|
+
return NumericSource(trees, schemas)
|