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.
- pyscriptc/__init__.py +3 -0
- pyscriptc/__main__.py +3 -0
- pyscriptc/_posthog_token.py +1 -0
- pyscriptc/builtins.py +156 -0
- pyscriptc/canonical.py +167 -0
- pyscriptc/check.py +49 -0
- pyscriptc/cli.py +271 -0
- pyscriptc/coverage.py +388 -0
- pyscriptc/debug.py +120 -0
- pyscriptc/diagnostics.py +179 -0
- pyscriptc/errors.py +56 -0
- pyscriptc/ir.py +365 -0
- pyscriptc/llvm.py +2129 -0
- pyscriptc/log.py +127 -0
- pyscriptc/lower/__init__.py +1071 -0
- pyscriptc/lower/_calls.py +1499 -0
- pyscriptc/lower/_classes.py +969 -0
- pyscriptc/lower/_core.py +301 -0
- pyscriptc/lower/_expr.py +886 -0
- pyscriptc/lower/_stdlib.py +491 -0
- pyscriptc/lower/_stdlib_shared.py +93 -0
- pyscriptc/lower/_stmt.py +1173 -0
- pyscriptc/package.py +249 -0
- pyscriptc/restlower.py +1008 -0
- pyscriptc/runtime/ps_argv.c +28 -0
- pyscriptc/runtime/ps_b64.c +168 -0
- pyscriptc/runtime/ps_coll.c +133 -0
- pyscriptc/runtime/ps_dt.c +388 -0
- pyscriptc/runtime/ps_hash.c +411 -0
- pyscriptc/runtime/ps_help.c +219 -0
- pyscriptc/runtime/ps_hmac.c +120 -0
- pyscriptc/runtime/ps_io.c +198 -0
- pyscriptc/runtime/ps_island.c +272 -0
- pyscriptc/runtime/ps_json.c +402 -0
- pyscriptc/runtime/ps_mix.c +47 -0
- pyscriptc/runtime/ps_more.c +1089 -0
- pyscriptc/runtime/ps_osmore.c +447 -0
- pyscriptc/runtime/ps_path.c +547 -0
- pyscriptc/runtime/ps_re.c +1102 -0
- pyscriptc/runtime/ps_rt.c +1175 -0
- pyscriptc/runtime/ps_rt.h +189 -0
- pyscriptc/runtime/ps_struct.c +291 -0
- pyscriptc/runtime/ps_time.c +207 -0
- pyscriptc/runtime/ps_unicode.c +1521 -0
- pyscriptc/runtime/ps_url.c +327 -0
- pyscriptc/stdlib_queue.py +191 -0
- pyscriptc/stdmore.py +1596 -0
- pyscriptc/telemetry.py +219 -0
- pyscriptc-0.1.0.dist-info/METADATA +187 -0
- pyscriptc-0.1.0.dist-info/RECORD +54 -0
- pyscriptc-0.1.0.dist-info/WHEEL +5 -0
- pyscriptc-0.1.0.dist-info/entry_points.txt +2 -0
- pyscriptc-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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()
|