pyscriptc 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.
Files changed (54) hide show
  1. pyscriptc/__init__.py +3 -0
  2. pyscriptc/__main__.py +3 -0
  3. pyscriptc/_posthog_token.py +1 -0
  4. pyscriptc/builtins.py +156 -0
  5. pyscriptc/canonical.py +167 -0
  6. pyscriptc/check.py +49 -0
  7. pyscriptc/cli.py +271 -0
  8. pyscriptc/coverage.py +388 -0
  9. pyscriptc/debug.py +120 -0
  10. pyscriptc/diagnostics.py +179 -0
  11. pyscriptc/errors.py +56 -0
  12. pyscriptc/ir.py +365 -0
  13. pyscriptc/llvm.py +2129 -0
  14. pyscriptc/log.py +127 -0
  15. pyscriptc/lower/__init__.py +1071 -0
  16. pyscriptc/lower/_calls.py +1499 -0
  17. pyscriptc/lower/_classes.py +969 -0
  18. pyscriptc/lower/_core.py +301 -0
  19. pyscriptc/lower/_expr.py +886 -0
  20. pyscriptc/lower/_stdlib.py +491 -0
  21. pyscriptc/lower/_stdlib_shared.py +93 -0
  22. pyscriptc/lower/_stmt.py +1173 -0
  23. pyscriptc/package.py +249 -0
  24. pyscriptc/restlower.py +1008 -0
  25. pyscriptc/runtime/ps_argv.c +28 -0
  26. pyscriptc/runtime/ps_b64.c +168 -0
  27. pyscriptc/runtime/ps_coll.c +133 -0
  28. pyscriptc/runtime/ps_dt.c +388 -0
  29. pyscriptc/runtime/ps_hash.c +411 -0
  30. pyscriptc/runtime/ps_help.c +219 -0
  31. pyscriptc/runtime/ps_hmac.c +120 -0
  32. pyscriptc/runtime/ps_io.c +198 -0
  33. pyscriptc/runtime/ps_island.c +272 -0
  34. pyscriptc/runtime/ps_json.c +402 -0
  35. pyscriptc/runtime/ps_mix.c +47 -0
  36. pyscriptc/runtime/ps_more.c +1089 -0
  37. pyscriptc/runtime/ps_osmore.c +447 -0
  38. pyscriptc/runtime/ps_path.c +547 -0
  39. pyscriptc/runtime/ps_re.c +1102 -0
  40. pyscriptc/runtime/ps_rt.c +1175 -0
  41. pyscriptc/runtime/ps_rt.h +189 -0
  42. pyscriptc/runtime/ps_struct.c +291 -0
  43. pyscriptc/runtime/ps_time.c +207 -0
  44. pyscriptc/runtime/ps_unicode.c +1521 -0
  45. pyscriptc/runtime/ps_url.c +327 -0
  46. pyscriptc/stdlib_queue.py +191 -0
  47. pyscriptc/stdmore.py +1596 -0
  48. pyscriptc/telemetry.py +219 -0
  49. pyscriptc-0.1.0.dist-info/METADATA +187 -0
  50. pyscriptc-0.1.0.dist-info/RECORD +54 -0
  51. pyscriptc-0.1.0.dist-info/WHEEL +5 -0
  52. pyscriptc-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyscriptc-0.1.0.dist-info/licenses/LICENSE +201 -0
  54. pyscriptc-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,491 @@
1
+ """Stdlib import binding and module dispatch for the lowerer.
2
+
3
+ Split out of `__init__.py` as the first E0 move commit. `Lowerer` composes
4
+ this mixin in. The area owns the closed import table: the `_bind_*` family,
5
+ module call dispatch, `_resolve_module`, and the fences that keep queued
6
+ stdlib modules out of the IR.
7
+
8
+ Shared module-level helpers (`Fence`, `Binding`, `_package_base`,
9
+ `_is_none_expr`) live in `_core.py`; binding-side helpers shared with the
10
+ static-statement walk live in `_stdlib_shared.py`. The boundary calls in both
11
+ directions stay `self._method()` so the move is behavior-identical.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import ast
17
+ import os
18
+ from typing import Any, NoReturn
19
+
20
+ from pyscriptc.errors import span_of
21
+ from pyscriptc.lower._core import _FUTURE_OK, Binding, Fence, _is_none_expr, _package_base
22
+ from pyscriptc.lower._stdlib_shared import _ERASED_MODULES
23
+ from pyscriptc.restlower import alias_name, rest_call
24
+ from pyscriptc.stdlib_queue import (
25
+ MATH_CONSTANTS,
26
+ MATH_FUNCTIONS,
27
+ SPECIAL_FUNCTIONS,
28
+ module_status,
29
+ wave_function,
30
+ )
31
+ from pyscriptc.stdmore import (
32
+ MODULE_CLASSES,
33
+ REST_MODULES,
34
+ member_callable,
35
+ module_constant,
36
+ )
37
+
38
+
39
+ class StdlibDispatchMixin:
40
+ def _bind_import(self, stmt: ast.Import, fences: list[Fence]) -> None:
41
+ for alias in stmt.names:
42
+ root = alias.name.split(".", 1)[0]
43
+ if root in _ERASED_MODULES:
44
+ self.bindings[alias.asname or root] = Binding("erased", root, "", False)
45
+ continue
46
+ if alias.name == "nativeprobe" and os.environ.get("PYSCRIPTC_NATIVEPROBE") == "1":
47
+ # Benchmark probe, not a stdlib module. Off unless the native
48
+ # benchmark sets the env var, so a normal compile still fences.
49
+ self.bindings[alias.asname or "nativeprobe"] = Binding("module", "nativeprobe", "", False)
50
+ continue
51
+ if self._is_own_package(alias.name):
52
+ self._bind_own_import(alias, stmt, fences)
53
+ continue
54
+ status = module_status(alias.name)
55
+ if status is None:
56
+ fences.append(
57
+ Fence(
58
+ "PS2002",
59
+ f"{alias.name} is not in the closed import table",
60
+ span_of(stmt, self.filename),
61
+ None,
62
+ )
63
+ )
64
+ continue
65
+ parts = alias.name.split(".")
66
+ if alias.asname or len(parts) == 1:
67
+ bound = alias.asname or parts[0]
68
+ rooted = False
69
+ else:
70
+ bound = parts[0]
71
+ rooted = True
72
+ self.bindings[bound] = Binding("module", alias.name, "", status == "queued", rooted=rooted)
73
+ self._note_module(alias.name)
74
+
75
+ def _bind_from(self, stmt: ast.ImportFrom, fences: list[Fence]) -> None:
76
+ if stmt.module in _ERASED_MODULES and not stmt.level:
77
+ for alias in stmt.names:
78
+ if alias.name == "*":
79
+ fences.append(Fence("PS2002", "star import is not lowered", span_of(stmt, self.filename), None))
80
+ continue
81
+ self.type_aliases[alias.asname or alias.name] = alias.name
82
+ return
83
+ if stmt.module == "__future__" and not stmt.level:
84
+ for alias in stmt.names:
85
+ if alias.name not in _FUTURE_OK:
86
+ fences.append(
87
+ Fence(
88
+ "PS2002",
89
+ f"__future__ import {alias.name} is not a type import",
90
+ span_of(stmt, self.filename),
91
+ None,
92
+ )
93
+ )
94
+ return
95
+ if stmt.level or not stmt.module:
96
+ if self.package is None:
97
+ fences.append(Fence("PS2002", "relative import is not lowered", span_of(stmt, self.filename), None))
98
+ return
99
+ self._bind_package_import(stmt, fences)
100
+ return
101
+ if self._is_own_package(stmt.module):
102
+ self._bind_own_from(stmt, fences)
103
+ return
104
+ status = module_status(stmt.module)
105
+ if status is None:
106
+ fences.append(
107
+ Fence(
108
+ "PS2002",
109
+ f"{stmt.module} is not in the closed import table",
110
+ span_of(stmt, self.filename),
111
+ None,
112
+ )
113
+ )
114
+ return
115
+ for alias in stmt.names:
116
+ if alias.name == "*":
117
+ fences.append(Fence("PS2002", "star import is not lowered", span_of(stmt, self.filename), None))
118
+ continue
119
+ local = alias.asname or alias.name
120
+ if status == "queued":
121
+ self.bindings[local] = Binding("func", stmt.module, alias.name, True)
122
+ continue
123
+ if stmt.module == "math" and alias.name in MATH_FUNCTIONS:
124
+ self.bindings[local] = Binding(
125
+ "func", stmt.module, alias.name, False, MATH_FUNCTIONS[alias.name]["return"]
126
+ )
127
+ continue
128
+ if stmt.module == "math" and alias.name in MATH_CONSTANTS:
129
+ self.bindings[local] = Binding("const", stmt.module, alias.name, False, {"kind": "float"})
130
+ continue
131
+ if wave_function(stmt.module, alias.name) is not None or alias.name in SPECIAL_FUNCTIONS.get(
132
+ stmt.module, frozenset()
133
+ ):
134
+ self.bindings[local] = Binding("func", stmt.module, alias.name, False)
135
+ self._note_module(stmt.module)
136
+ continue
137
+ constant = module_constant(stmt.module, alias.name)
138
+ if constant is not None:
139
+ self.bindings[local] = Binding("const", stmt.module, alias.name, False, constant[1])
140
+ continue
141
+ if member_callable(stmt.module, alias.name):
142
+ self.bindings[local] = Binding("func", stmt.module, alias.name, False)
143
+ self._note_module(stmt.module)
144
+ continue
145
+ child = f"{stmt.module}.{alias.name}"
146
+ child_status = module_status(child)
147
+ if child_status is not None:
148
+ self.bindings[local] = Binding("module", child, "", child_status == "queued")
149
+ self._note_module(child)
150
+ continue
151
+ fences.append(
152
+ Fence(
153
+ "PS2001",
154
+ f"{stmt.module}.{alias.name} is not in the lowered subset of {stmt.module}",
155
+ span_of(stmt, self.filename),
156
+ None,
157
+ )
158
+ )
159
+
160
+ def _bind_package_import(self, stmt: ast.ImportFrom, fences: list[Fence]) -> None:
161
+ base = _package_base(self.module_name, self.is_package, stmt.level)
162
+ if base is None:
163
+ fences.append(Fence("PS2002", "relative import is not lowered", span_of(stmt, self.filename), None))
164
+ return
165
+ if stmt.module:
166
+ module = ".".join([*base, *stmt.module.split(".")]) if base else stmt.module
167
+ for alias in stmt.names:
168
+ if alias.name == "*":
169
+ fences.append(Fence("PS2002", "star import is not lowered", span_of(stmt, self.filename), None))
170
+ continue
171
+ self._bind_one_export(module, alias.name, alias.asname or alias.name, stmt, fences)
172
+ return
173
+ for alias in stmt.names:
174
+ if alias.name == "*":
175
+ fences.append(Fence("PS2002", "star import is not lowered", span_of(stmt, self.filename), None))
176
+ continue
177
+ local = alias.asname or alias.name
178
+ child = ".".join([*base, alias.name]) if base else alias.name
179
+ package = self.package or {}
180
+ if child in package:
181
+ self._bind_pkg_module(local, child, stmt, fences)
182
+ continue
183
+ owner = ".".join(base)
184
+ if owner and alias.name in package.get(owner, {}):
185
+ self._bind_one_export(owner, alias.name, local, stmt, fences)
186
+ continue
187
+ fences.append(
188
+ Fence(
189
+ "PS2002",
190
+ f"{child} is not a module in this package",
191
+ span_of(stmt, self.filename),
192
+ None,
193
+ )
194
+ )
195
+
196
+ def _bind_pkg_module(self, local: str, module: str, stmt: ast.AST, fences: list[Fence]) -> None:
197
+ if local in self.bindings or local in self.constants or local in self.declared:
198
+ fences.append(Fence("PS2002", f"{local} is already bound", span_of(stmt, self.filename), None))
199
+ return
200
+ self.bindings[local] = Binding("pkg", module, "", False)
201
+ self.declared.add(local)
202
+
203
+ def _bind_own_import(self, alias: ast.alias, stmt: ast.Import, fences: list[Fence]) -> None:
204
+ package = self.package or {}
205
+ root = alias.name.split(".", 1)[0]
206
+ if alias.asname:
207
+ if alias.name not in package:
208
+ fences.append(
209
+ Fence(
210
+ "PS2002",
211
+ f"{alias.name} is not a module in this package",
212
+ span_of(stmt, self.filename),
213
+ None,
214
+ )
215
+ )
216
+ return
217
+ self._bind_pkg_module(alias.asname, alias.name, stmt, fences)
218
+ return
219
+ if root not in package or (alias.name != root and alias.name not in package):
220
+ missing = alias.name if alias.name != root else root
221
+ fences.append(
222
+ Fence(
223
+ "PS2002",
224
+ f"{missing} is not a module in this package",
225
+ span_of(stmt, self.filename),
226
+ None,
227
+ )
228
+ )
229
+ return
230
+ self._bind_pkg_module(root, root, stmt, fences)
231
+
232
+ def _bind_own_from(self, stmt: ast.ImportFrom, fences: list[Fence]) -> None:
233
+ module = stmt.module or ""
234
+ package = self.package or {}
235
+ for alias in stmt.names:
236
+ if alias.name == "*":
237
+ fences.append(Fence("PS2002", "star import is not lowered", span_of(stmt, self.filename), None))
238
+ continue
239
+ local = alias.asname or alias.name
240
+ child = f"{module}.{alias.name}"
241
+ if child in package:
242
+ self._bind_pkg_module(local, child, stmt, fences)
243
+ continue
244
+ if module in package:
245
+ self._bind_one_export(module, alias.name, local, stmt, fences)
246
+ continue
247
+ fences.append(
248
+ Fence(
249
+ "PS2002",
250
+ f"{module} is not a module in this package",
251
+ span_of(stmt, self.filename),
252
+ None,
253
+ )
254
+ )
255
+
256
+ def _note_module(self, module: str) -> None:
257
+ if module == "re" or module.startswith("re."):
258
+ if module_status("re") == "lowered":
259
+ self.synthetics.add("Pattern")
260
+ if module == "urllib" or module == "urllib.parse" or module.startswith("urllib."):
261
+ if module_status("urllib.parse") == "lowered":
262
+ self.synthetics.add("SplitResult")
263
+ for name, classes in MODULE_CLASSES.items():
264
+ if module == name or module.startswith(name + "."):
265
+ if module_status(name) == "lowered":
266
+ self.synthetics.update(classes)
267
+
268
+ def _accept_module_function(self, fn: ast.FunctionDef, fences: list[Fence]) -> bool:
269
+ """Module `@staticmethod` is the function the class decorator already emits.
270
+
271
+ The call passes no instance. Other module decorators stay fenced.
272
+ """
273
+ message = self._module_decorator_fence(fn)
274
+ if message is None:
275
+ return True
276
+ fences.append(Fence("PS2002", message, span_of(fn, self.filename), fn.name))
277
+ return False
278
+
279
+ def _is_module_staticmethod(self, fn: ast.FunctionDef) -> bool:
280
+ return self._module_decorator_fence(fn) is None and bool(fn.decorator_list)
281
+
282
+ def _module_decorator_fence(self, fn: ast.FunctionDef) -> str | None:
283
+ if not fn.decorator_list:
284
+ return None
285
+ if len(fn.decorator_list) != 1:
286
+ return "stacked decorators are not lowered"
287
+ dec = fn.decorator_list[0]
288
+ if not isinstance(dec, ast.Name):
289
+ return "a decorator expression is not lowered"
290
+ if dec.id == "staticmethod":
291
+ return None
292
+ if dec.id == "property":
293
+ return "a module property is not lowered"
294
+ if dec.id == "classmethod":
295
+ return "a module classmethod is not lowered"
296
+ return "user decorators are not lowered"
297
+
298
+ def _module_call(
299
+ self,
300
+ node: ast.Call,
301
+ span: dict[str, Any],
302
+ module: str,
303
+ attr: str,
304
+ expect: dict[str, Any] | None,
305
+ ) -> dict[str, Any]:
306
+ if module == "nativeprobe":
307
+ return self._nativeprobe_call(node, span, attr)
308
+ if module == "math":
309
+ self._reject_keywords(node)
310
+ return self._math_call(node, span, attr)
311
+ if module in REST_MODULES:
312
+ return rest_call(self, node, span, module, attr, expect)
313
+ if module == "json" and attr == "loads":
314
+ self._reject_keywords(node)
315
+ return self._json_loads(node, span, expect)
316
+ if module == "json" and attr == "dumps":
317
+ self._reject_keywords(node)
318
+ return self._json_dumps(node, span)
319
+ if module == "struct" and attr in {"pack", "unpack"}:
320
+ self._reject_keywords(node)
321
+ return self._struct_call(node, span, attr)
322
+ if module == "re" and attr == "findall":
323
+ ret, symbol = self._group_tuple(node, node.args[0] if node.args else None)
324
+ expr = self._fixed_call(node, span, module, attr, wave_function(module, attr))
325
+ expr["type"] = ret
326
+ if symbol:
327
+ expr["symbol"] = symbol
328
+ return expr
329
+ spec = wave_function(module, attr)
330
+ if spec is None:
331
+ self.fail(node, "PS2001", f"{module}.{attr} is not in the lowered subset of {module}")
332
+ return self._fixed_call(node, span, module, attr, spec)
333
+
334
+ def _resolve_module(self, node: ast.Attribute) -> tuple[str, str, str, str] | None:
335
+ """Dotted spelling, module, member, and status for an import chain.
336
+
337
+ Status is `lowered`, `queued`, `missing`, or `value` when the chain
338
+ names the module object itself.
339
+ """
340
+
341
+ attrs: list[str] = []
342
+ cur: ast.AST = node
343
+ while isinstance(cur, ast.Attribute):
344
+ attrs.append(cur.attr)
345
+ cur = cur.value
346
+ if not isinstance(cur, ast.Name):
347
+ return None
348
+ binding = self.bindings.get(cur.id)
349
+ if binding is None:
350
+ return None
351
+ attrs.reverse()
352
+ dotted = ".".join([cur.id, *attrs])
353
+ if binding.kind == "func" and alias_name(binding.module, binding.attr) is not None:
354
+ if not attrs:
355
+ return None
356
+ member = ".".join([binding.attr, *attrs])
357
+ status = "lowered" if member_callable(binding.module, member) else "missing"
358
+ return self._finish_resolve(dotted, binding.module, member, status)
359
+ if binding.kind == "erased":
360
+ member = ".".join(attrs)
361
+ return dotted, binding.module, member, "erased"
362
+ if binding.kind == "pkg":
363
+ module = binding.module
364
+ package = self.package or {}
365
+ for attr in attrs[:-1]:
366
+ child = f"{module}.{attr}"
367
+ if child not in package:
368
+ return dotted, module, ".".join(attrs), "missing"
369
+ module = child
370
+ if not attrs:
371
+ return dotted, module, "", "value"
372
+ return dotted, module, attrs[-1], "pkg"
373
+ if binding.kind != "module":
374
+ return None
375
+ if binding.rooted:
376
+ prefix = binding.module.split(".")[1:]
377
+ if attrs[: len(prefix)] != prefix:
378
+ head = binding.module.split(".")[0]
379
+ module = ".".join([head, *attrs[:-1]]) if len(attrs) > 1 else head
380
+ member = attrs[-1] if attrs else ""
381
+ if not member:
382
+ return dotted, module, "", "value"
383
+ return self._finish_resolve(dotted, module, member, self._use_status(module))
384
+ rest = attrs[len(prefix) :]
385
+ if not rest:
386
+ return dotted, binding.module, "", "value"
387
+ member = rest[-1]
388
+ extra = rest[:-1]
389
+ module = binding.module if not extra else binding.module + "." + ".".join(extra)
390
+ return self._finish_resolve(dotted, module, member, self._use_status(module))
391
+ if not attrs:
392
+ return dotted, binding.module, "", "value"
393
+ member = attrs[-1]
394
+ extra = attrs[:-1]
395
+ module = binding.module if not extra else binding.module + "." + ".".join(extra)
396
+ return self._finish_resolve(dotted, module, member, self._use_status(module))
397
+
398
+ def _queued(self, node: ast.AST, dotted: str) -> NoReturn:
399
+ self.fail(node, "PS2101", f"{dotted} is queued in the stdlib subset and is not lowered yet")
400
+
401
+ def _is_mod(self, node: ast.expr, module: str, name: str) -> bool:
402
+ if not isinstance(node, ast.Call) or node.keywords:
403
+ return False
404
+ func = node.func
405
+ if isinstance(func, ast.Name):
406
+ binding = self.bindings.get(func.id)
407
+ return (
408
+ binding is not None
409
+ and binding.kind == "func"
410
+ and binding.module == module
411
+ and binding.attr == name
412
+ and not binding.queued
413
+ )
414
+ if isinstance(func, ast.Attribute):
415
+ resolved = self._resolve_module(func)
416
+ return resolved is not None and resolved[1] == module and resolved[2] == name and resolved[3] == "lowered"
417
+ return False
418
+
419
+ def _bind_module_assign(self, stmt: ast.stmt) -> None:
420
+ try:
421
+ if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
422
+ if stmt.value is None:
423
+ self.fail(stmt, "PS1001", f"{stmt.target.id} needs an initial value")
424
+ typ = self._annotation(stmt.annotation)
425
+ if isinstance(stmt.value, ast.Set):
426
+ folded = self._module_set_literal(stmt.value)
427
+ if folded is not None:
428
+ self._expect(stmt, folded["type"], typ, stmt.target.id)
429
+ self._store_constant(stmt.target.id, folded, stmt)
430
+ return
431
+ value = self._expr(stmt.value, typ)
432
+ self._expect(stmt, value["type"], typ, stmt.target.id)
433
+ self._store_constant(stmt.target.id, value, stmt)
434
+ return
435
+ if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
436
+ target = stmt.targets[0].id
437
+ if target in self.global_slots and _is_none_expr(stmt.value):
438
+ self._activate_slot(target, stmt)
439
+ return
440
+ if isinstance(stmt.value, ast.Tuple):
441
+ self._store_tuple_constant(target, stmt.value, stmt)
442
+ return
443
+ if isinstance(stmt.value, ast.Set):
444
+ folded = self._module_set_literal(stmt.value)
445
+ if folded is not None:
446
+ self._store_constant(target, folded, stmt)
447
+ return
448
+ value = self._expr(stmt.value)
449
+ # re.compile of one str constant is the Pattern call a function
450
+ # already lowers. Store that call and re-emit it at each read.
451
+ # It is not a const kind and not a runtime global. Flags, extra
452
+ # arguments, and a non-constant pattern stay fenced here.
453
+ if value["kind"] not in {"const", "list", "dict"} and not self._one_str_pattern(stmt.value, value):
454
+ self.fail(stmt, "PS2002", "a module assignment must be a constant")
455
+ self._store_constant(target, value, stmt)
456
+ return
457
+ self.fail(stmt, "PS2002", "module assignment is not lowered")
458
+ except Fence as exc:
459
+ self.caught.append(exc)
460
+
461
+ def _module_set_literal(self, node: ast.Set) -> dict[str, Any] | None:
462
+ """A set of const str or const int is the set list a module list constant already inlines.
463
+
464
+ Any other element, a starred element, or a non-constant stays on the ordinary path.
465
+ Duplicates keep the first occurrence. Each use copies the list, so it is a new object.
466
+ """
467
+ if not node.elts or any(isinstance(elt, ast.Starred) for elt in node.elts):
468
+ return None
469
+ kinds: list[str] = []
470
+ for elt in node.elts:
471
+ if not isinstance(elt, ast.Constant) or isinstance(elt.value, bool):
472
+ return None
473
+ if isinstance(elt.value, str):
474
+ kinds.append("str")
475
+ elif isinstance(elt.value, int):
476
+ kinds.append("int")
477
+ else:
478
+ return None
479
+ if len(set(kinds)) != 1:
480
+ return None
481
+ elems: list[dict[str, Any]] = []
482
+ seen: list[Any] = []
483
+ for elt in node.elts:
484
+ value = self._expr(elt)
485
+ if value["value"] in seen:
486
+ continue
487
+ seen.append(value["value"])
488
+ elems.append(value)
489
+ span = span_of(node, self.filename)
490
+ typ = {"kind": "list", "elem": {"kind": kinds[0]}, "set": True}
491
+ return {"kind": "list", "elems": elems, "type": typ, "span": span}
@@ -0,0 +1,93 @@
1
+ """Module-level helpers for the stdlib dispatch area.
2
+
3
+ `_release_erased_import` and `_module_assigned` are shared by the binding
4
+ mixin and the static-statement walk in `__init__.py`; they live on a leaf
5
+ module so both sides import them without a cycle.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ from typing import Any
12
+
13
+ _ERASED_MODULES = frozenset({"typing", "typing_extensions"})
14
+
15
+
16
+ def _release_erased_import(stmt: ast.Import | ast.ImportFrom, rebound: set[str]) -> None:
17
+ """An erased import binds the typing name again. A previous user assignment no longer hides it."""
18
+ if isinstance(stmt, ast.ImportFrom):
19
+ if stmt.module in _ERASED_MODULES and not stmt.level:
20
+ for alias in stmt.names:
21
+ if alias.name == "TYPE_CHECKING":
22
+ rebound.discard(alias.asname or alias.name)
23
+ return
24
+ for alias in stmt.names:
25
+ root = alias.name.split(".", 1)[0]
26
+ if root in _ERASED_MODULES:
27
+ rebound.discard(alias.asname or root)
28
+
29
+
30
+ def _module_assigned(stmt: ast.stmt) -> set[str]:
31
+ """Names this module statement binds. Function and class bodies do not bind module names."""
32
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
33
+ return {stmt.name}
34
+ if isinstance(stmt, ast.Assign):
35
+ names: set[str] = set()
36
+ for target in stmt.targets:
37
+ names.update(_store_names(target))
38
+ return names
39
+ if isinstance(stmt, ast.AnnAssign):
40
+ return _store_names(stmt.target)
41
+ if isinstance(stmt, ast.AugAssign):
42
+ return _store_names(stmt.target)
43
+ if isinstance(stmt, (ast.For, ast.AsyncFor)):
44
+ return _store_names(stmt.target)
45
+ if isinstance(stmt, (ast.With, ast.AsyncWith)):
46
+ names = set()
47
+ for item in stmt.items:
48
+ if item.optional_vars is not None:
49
+ names.update(_store_names(item.optional_vars))
50
+ return names
51
+ if isinstance(stmt, ast.If):
52
+ names = set()
53
+ for part in (*stmt.body, *stmt.orelse):
54
+ names.update(_module_assigned(part))
55
+ return names
56
+ return set()
57
+
58
+
59
+ def _synth_module(class_name: str) -> str:
60
+ if class_name in {"Date", "DateTime", "Time", "TimeDelta"}:
61
+ return "datetime"
62
+ if class_name == "Path":
63
+ return "pathlib"
64
+ if class_name in {"TextIO", "BytesIO"}:
65
+ return "io"
66
+ if class_name == "Deque":
67
+ return "collections"
68
+ if class_name == "Hmac":
69
+ return "hmac"
70
+ return "builtins"
71
+
72
+
73
+ def _mod_call(
74
+ module: str, name: str, args: list[dict[str, Any]], typ: dict[str, Any], span: dict[str, Any]
75
+ ) -> dict[str, Any]:
76
+ return {
77
+ "kind": "call",
78
+ "callee": {"kind": "module", "module": module, "name": name},
79
+ "args": args,
80
+ "type": typ,
81
+ "span": span,
82
+ }
83
+
84
+
85
+ def _store_names(target: ast.expr) -> set[str]:
86
+ if isinstance(target, ast.Name):
87
+ return {target.id}
88
+ if isinstance(target, (ast.Tuple, ast.List)):
89
+ names: set[str] = set()
90
+ for elt in target.elts:
91
+ names.update(_store_names(elt))
92
+ return names
93
+ return set()