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.
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)