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.
webgpu_bindings.py ADDED
@@ -0,0 +1,308 @@
1
+ from ast import (
2
+ Assign,
3
+ Attribute,
4
+ Call,
5
+ Constant,
6
+ Dict,
7
+ Expr,
8
+ FunctionDef,
9
+ ImportFrom,
10
+ List,
11
+ Load,
12
+ Module,
13
+ Name,
14
+ Return,
15
+ Subscript,
16
+ Tuple,
17
+ arg,
18
+ arguments,
19
+ expr,
20
+ fix_missing_locations,
21
+ parse,
22
+ stmt,
23
+ unparse,
24
+ walk,
25
+ )
26
+ from pathlib import Path
27
+
28
+ # wgpu structs define field types; the lowerer validates this subset and emits browser field names.
29
+ DESCRIPTORS = (
30
+ ("ShaderModuleDescriptor", (("code", "code", "str"),)),
31
+ (
32
+ "RenderPipelineDescriptor",
33
+ (
34
+ ("layout", "layout", "str"),
35
+ ("vertex", "vertex", "VertexState"),
36
+ ("fragment", "fragment", "FragmentState"),
37
+ ("primitive", "primitive", "PrimitiveState"),
38
+ ),
39
+ ),
40
+ ("VertexState", (("module", "module", "GPUShaderModule"), ("entry_point", "entryPoint", "str"))),
41
+ (
42
+ "FragmentState",
43
+ (
44
+ ("module", "module", "GPUShaderModule"),
45
+ ("entry_point", "entryPoint", "str"),
46
+ ("targets", "targets", "ColorTargetState[]"),
47
+ ),
48
+ ),
49
+ ("PrimitiveState", (("topology", "topology", "str"),)),
50
+ ("ColorTargetState", (("format", "format", "str"),)),
51
+ ("RenderPassDescriptor", (("color_attachments", "colorAttachments", "RenderPassColorAttachment[]"),)),
52
+ (
53
+ "RenderPassColorAttachment",
54
+ (
55
+ ("view", "view", "GPUTextureView"),
56
+ ("clear_value", "clearValue", "Color"),
57
+ ("load_op", "loadOp", "str"),
58
+ ("store_op", "storeOp", "str"),
59
+ ),
60
+ ),
61
+ )
62
+
63
+
64
+ def method_binding(owner: str, name: str) -> tuple[str, str, str | None, tuple[str, ...]]:
65
+ if owner == "GPUDevice":
66
+ if name == "create_shader_module":
67
+ return "createShaderModule", "GPUShaderModule", "ShaderModuleDescriptor", ()
68
+ if name == "create_render_pipeline":
69
+ return "createRenderPipeline", "GPURenderPipeline", "RenderPipelineDescriptor", ()
70
+ if name == "create_command_encoder":
71
+ return "createCommandEncoder", "GPUCommandEncoder", None, ()
72
+ elif owner == "GPUCommandEncoder":
73
+ if name == "begin_render_pass":
74
+ return "beginRenderPass", "GPURenderPassEncoder", "RenderPassDescriptor", ()
75
+ if name == "finish":
76
+ return "finish", "GPUCommandBuffer", None, ()
77
+ elif owner == "GPUTexture" and name == "create_view":
78
+ return "createView", "GPUTextureView", None, ()
79
+ elif owner == "GPURenderPassEncoder":
80
+ if name == "set_pipeline":
81
+ return "setPipeline", "None", None, ("GPURenderPipeline",)
82
+ if name == "draw":
83
+ return "draw", "None", None, ("int", "int", "int", "int")
84
+ if name == "end":
85
+ return "end", "None", None, ()
86
+ elif owner == "GPUQueue" and name == "submit":
87
+ return "submit", "None", None, ("GPUCommandBuffer[]",)
88
+ raise NotImplementedError(f"Unsupported WebGPU method: {owner}.{name}")
89
+
90
+
91
+ def compatible_type(actual: str, expected: str) -> bool:
92
+ return actual == expected or (actual == "empty_sequence" and expected.endswith("[]"))
93
+
94
+
95
+ def descriptor_binding(
96
+ names: tuple[str | None, ...], types: tuple[str, ...], expected: str | None
97
+ ) -> tuple[str, tuple[str, ...]]:
98
+ for descriptor in DESCRIPTORS:
99
+ fields = descriptor[1]
100
+ if (
101
+ (expected is None or descriptor[0] == expected)
102
+ and len(names) == len(fields)
103
+ and set(names) == {field[0] for field in fields}
104
+ ):
105
+ browser_names = []
106
+ for i in range(len(names)):
107
+ for field in fields:
108
+ if names[i] == field[0]:
109
+ if compatible_type(types[i], field[2]):
110
+ browser_names.append(field[1])
111
+ break
112
+ raise NotImplementedError(
113
+ f"Expected {field[2]} for {descriptor[0]}.{field[0]}, got {types[i]}"
114
+ )
115
+ return descriptor[0], tuple(browser_names)
116
+ subject = expected if expected is not None else "supported wgpu struct"
117
+ raise NotImplementedError(f"Unsupported WebGPU descriptor fields for {subject}: {names}")
118
+
119
+
120
+ def sequence_type(types: tuple[str, ...]) -> str:
121
+ if len(types) == 0:
122
+ return "empty_sequence"
123
+ if (
124
+ len(types) == 4
125
+ and types[0] in ("int", "float")
126
+ and types[1] in ("int", "float")
127
+ and types[2] in ("int", "float")
128
+ and types[3] in ("int", "float")
129
+ ):
130
+ return "Color"
131
+ if types[0] in ("GPUCommandBuffer", "ColorTargetState", "RenderPassColorAttachment"):
132
+ for kind in types:
133
+ if kind == types[0]:
134
+ continue
135
+ raise NotImplementedError("WebGPU sequences require one element type")
136
+ return types[0] + "[]"
137
+ raise NotImplementedError("Unsupported WebGPU sequence element types: " + str(types))
138
+
139
+
140
+ def lower_expression(value: expr, variables: dict[str, str]) -> tuple[expr, str]:
141
+ lowered: dict[expr, expr] = {}
142
+ types: dict[expr, str] = {}
143
+ nodes = list(walk(value))
144
+ targets = {node.func for node in nodes if isinstance(node, Call)}
145
+ for node in reversed(nodes):
146
+ if not isinstance(node, expr):
147
+ continue
148
+ if isinstance(node, Name):
149
+ if node.id in variables:
150
+ lowered[node] = Name(id=node.id, ctx=Load())
151
+ types[node] = variables[node.id]
152
+ else:
153
+ raise NotImplementedError("Unknown WebGPU value: " + node.id)
154
+ elif isinstance(node, Constant):
155
+ lowered[node] = node
156
+ types[node] = "None" if node.value is None else type(node.value).__name__
157
+ elif isinstance(node, (List, Tuple)):
158
+ lowered[node] = List(elts=[lowered[item] for item in node.elts], ctx=Load())
159
+ types[node] = sequence_type(tuple(types[item] for item in node.elts))
160
+ elif isinstance(node, Dict):
161
+ names = []
162
+ for key in node.keys:
163
+ if isinstance(key, Constant) and isinstance(key.value, str):
164
+ names.append(key.value)
165
+ else:
166
+ raise NotImplementedError("WebGPU descriptors require literal field names")
167
+ descriptor = descriptor_binding(tuple(names), tuple(types[item] for item in node.values), None)
168
+ keys: list[expr | None] = [Constant(value=name) for name in descriptor[1]]
169
+ lowered[node] = Dict(keys=keys, values=[lowered[item] for item in node.values])
170
+ types[node] = descriptor[0]
171
+ elif isinstance(node, Attribute):
172
+ owner = types[node.value]
173
+ if owner == "GPUDevice" and node.attr == "queue":
174
+ lowered[node] = Attribute(value=lowered[node.value], attr="queue", ctx=Load())
175
+ types[node] = "GPUQueue"
176
+ elif node in targets:
177
+ binding = method_binding(owner, node.attr)
178
+ # The lowerer emits native method keys; the browser emitter preserves them without runtime bindings.
179
+ lowered[node] = Subscript(value=lowered[node.value], slice=Constant(value=binding[0]), ctx=Load())
180
+ types[node] = "method"
181
+ else:
182
+ raise NotImplementedError("WebGPU methods must be called directly")
183
+ elif isinstance(node, Call) and isinstance(node.func, Attribute):
184
+ owner = types[node.func.value]
185
+ binding = method_binding(owner, node.func.attr)
186
+ if binding[2] is not None:
187
+ if node.args:
188
+ raise NotImplementedError(f"Expected keyword descriptor fields for {owner}.{node.func.attr}")
189
+ descriptor = descriptor_binding(
190
+ tuple(item.arg for item in node.keywords),
191
+ tuple(types[item.value] for item in node.keywords),
192
+ binding[2],
193
+ )
194
+ keys = [Constant(value=name) for name in descriptor[1]]
195
+ args: list[expr] = [Dict(keys=keys, values=[lowered[item.value] for item in node.keywords])]
196
+ elif len(node.args) == len(binding[3]) and not node.keywords:
197
+ for i in range(len(node.args)):
198
+ actual = types[node.args[i]]
199
+ if compatible_type(actual, binding[3][i]):
200
+ continue
201
+ raise NotImplementedError(
202
+ f"Expected {binding[3][i]} for {owner}.{node.func.attr} argument {i + 1}, got {actual}"
203
+ )
204
+ args = [lowered[item] for item in node.args]
205
+ else:
206
+ raise NotImplementedError(
207
+ f"Expected {len(binding[3])} positional arguments for {owner}.{node.func.attr}"
208
+ )
209
+ lowered[node] = Call(func=lowered[node.func], args=args, keywords=[])
210
+ types[node] = binding[1]
211
+ else:
212
+ raise NotImplementedError("Unsupported WebGPU expression: " + type(node).__name__)
213
+ return lowered[value], types[value]
214
+
215
+
216
+ def lower_webgpu(source: Path) -> str:
217
+ tree = parse(source.read_text())
218
+ gpu_types = (
219
+ "GPUDevice",
220
+ "GPUShaderModule",
221
+ "GPURenderPipeline",
222
+ "GPUCommandEncoder",
223
+ "GPURenderPassEncoder",
224
+ "GPUTexture",
225
+ "GPUTextureView",
226
+ "GPUCommandBuffer",
227
+ "GPUQueue",
228
+ )
229
+ imported = set()
230
+ functions: list[stmt] = []
231
+ imports_open = True
232
+ for node in tree.body:
233
+ if isinstance(node, ImportFrom) and imports_open:
234
+ if node.level or node.module != "wgpu.classes":
235
+ raise NotImplementedError("WebGPU bindings accept only top-level wgpu type imports")
236
+ for alias in node.names:
237
+ if alias.asname or alias.name not in gpu_types:
238
+ raise NotImplementedError("Unsupported WebGPU import: " + alias.name)
239
+ imported.add(alias.name)
240
+ continue
241
+ imports_open = False
242
+ if not isinstance(node, FunctionDef):
243
+ raise NotImplementedError("WebGPU modules contain only typed functions")
244
+ if (
245
+ node.decorator_list
246
+ or node.type_params
247
+ or node.args.defaults
248
+ or node.args.kw_defaults
249
+ or node.args.vararg
250
+ or node.args.kwarg
251
+ or node.args.kwonlyargs
252
+ or node.args.posonlyargs
253
+ ):
254
+ raise NotImplementedError("WebGPU functions require plain positional parameters")
255
+ variables: dict[str, str] = {}
256
+ for parameter in node.args.args:
257
+ if isinstance(parameter.annotation, Name) and parameter.annotation.id in imported | {"str"}:
258
+ variables[parameter.arg] = parameter.annotation.id
259
+ else:
260
+ raise NotImplementedError("Explicit WebGPU parameter types required")
261
+ if isinstance(node.returns, Name) and node.returns.id in imported:
262
+ result_type = node.returns.id
263
+ elif isinstance(node.returns, Constant) and node.returns.value is None:
264
+ result_type = "None"
265
+ else:
266
+ raise NotImplementedError("Explicit WebGPU return type required")
267
+ body: list[stmt] = []
268
+ returned = False
269
+ for operation in node.body:
270
+ if returned:
271
+ raise NotImplementedError("Unreachable WebGPU statement")
272
+ if (
273
+ isinstance(operation, Assign)
274
+ and len(operation.targets) == 1
275
+ and isinstance(operation.targets[0], Name)
276
+ ):
277
+ value = lower_expression(operation.value, variables)
278
+ variables[operation.targets[0].id] = value[1]
279
+ body.append(Assign(targets=operation.targets, value=value[0]))
280
+ elif isinstance(operation, Expr) and isinstance(operation.value, Call):
281
+ value = lower_expression(operation.value, variables)
282
+ body.append(Expr(value=value[0]))
283
+ elif isinstance(operation, Return) and operation.value is not None:
284
+ value = lower_expression(operation.value, variables)
285
+ if value[1] != result_type:
286
+ raise NotImplementedError("WebGPU return type does not match its value")
287
+ body.append(Return(value=value[0]))
288
+ returned = True
289
+ else:
290
+ raise NotImplementedError("Unsupported WebGPU statement: " + type(operation).__name__)
291
+ if result_type != "None" and not returned:
292
+ raise NotImplementedError("WebGPU function must return its declared result")
293
+ functions.append(
294
+ FunctionDef(
295
+ name=node.name,
296
+ args=arguments(
297
+ posonlyargs=[],
298
+ args=[arg(arg=item.arg) for item in node.args.args],
299
+ kwonlyargs=[],
300
+ kw_defaults=[],
301
+ defaults=[],
302
+ ),
303
+ body=body,
304
+ decorator_list=[],
305
+ type_params=[],
306
+ )
307
+ )
308
+ return unparse(fix_missing_locations(Module(body=functions, type_ignores=[]))) + "\n"