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,969 @@
|
|
|
1
|
+
"""Class lowering and typing-annotation machinery.
|
|
2
|
+
|
|
3
|
+
`ClassesMixin` collects `class` statements (dataclasses, enums, synthetic
|
|
4
|
+
tuple/deque/OrderedDict shapes), lowers method signatures, and resolves
|
|
5
|
+
typing annotations. Moved verbatim from `lower.py` via `lower/__init__.py`;
|
|
6
|
+
no signature or behavior change.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import copy
|
|
13
|
+
from typing import Any, NoReturn
|
|
14
|
+
|
|
15
|
+
from pyscriptc.builtins import EXCEPTIONS
|
|
16
|
+
from pyscriptc.errors import span_of
|
|
17
|
+
from pyscriptc.ir import type_key, types_equal
|
|
18
|
+
from pyscriptc.lower._core import (
|
|
19
|
+
_DICT_FORMS,
|
|
20
|
+
_LIST_FORMS,
|
|
21
|
+
_SET_FORMS,
|
|
22
|
+
Fence,
|
|
23
|
+
_name,
|
|
24
|
+
_show,
|
|
25
|
+
)
|
|
26
|
+
from pyscriptc.lower._stmt import _always_returns, _is_docstring
|
|
27
|
+
from pyscriptc.restlower import alias_name, path_attr
|
|
28
|
+
from pyscriptc.restlower import method_spec as synth_method
|
|
29
|
+
from pyscriptc.stdmore import ALWAYS_CLASSES, CLASS_FIELDS
|
|
30
|
+
|
|
31
|
+
_UNWRAP_FORMS = frozenset({"Final", "ClassVar", "Annotated", "Required", "NotRequired"})
|
|
32
|
+
_DYNAMIC_ANNOTATIONS = frozenset({"object", "Any", "Protocol", "TypeVar", "Generic", "Callable", "ABC"})
|
|
33
|
+
_IO_HANDLES = {"BinaryIO": "BytesIO", "TextIO": "TextIO"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ClassesMixin:
|
|
37
|
+
def _install_class_export(self, local: str, item: dict[str, Any], stmt: ast.AST, fences: list[Fence]) -> None:
|
|
38
|
+
name = str(item["name"])
|
|
39
|
+
if name in self.classes and not self.classes[name].get("imported"):
|
|
40
|
+
fences.append(Fence("PS2002", f"{local} is already bound", span_of(stmt, self.filename), None))
|
|
41
|
+
return
|
|
42
|
+
info = copy.deepcopy(item["info"])
|
|
43
|
+
info["imported"] = True
|
|
44
|
+
self.classes[name] = info
|
|
45
|
+
self.class_aliases[local] = name
|
|
46
|
+
self.known_classes.add(local)
|
|
47
|
+
self.known_classes.add(name)
|
|
48
|
+
self.declared.add(local)
|
|
49
|
+
self.declared.add(name)
|
|
50
|
+
|
|
51
|
+
def _install_synthetics(self, class_nodes: list[ast.ClassDef], fences: list[Fence]) -> None:
|
|
52
|
+
self.synthetics.update(ALWAYS_CLASSES)
|
|
53
|
+
specs: dict[str, list[dict[str, Any]]] = {}
|
|
54
|
+
if "Pattern" in self.synthetics:
|
|
55
|
+
specs["Pattern"] = [{"name": "pattern", "type": {"kind": "str"}}]
|
|
56
|
+
if "SplitResult" in self.synthetics:
|
|
57
|
+
specs["SplitResult"] = [
|
|
58
|
+
{"name": name, "type": {"kind": "str"}} for name in ("scheme", "netloc", "path", "query", "fragment")
|
|
59
|
+
]
|
|
60
|
+
for name in self.synthetics:
|
|
61
|
+
if name in CLASS_FIELDS:
|
|
62
|
+
specs[name] = CLASS_FIELDS[name]
|
|
63
|
+
user_names = {node.name for node in class_nodes}
|
|
64
|
+
for name, fields in specs.items():
|
|
65
|
+
if name in user_names or name in self.declared:
|
|
66
|
+
clash = next((node for node in class_nodes if node.name == name), None)
|
|
67
|
+
fences.append(
|
|
68
|
+
Fence(
|
|
69
|
+
"PS2002",
|
|
70
|
+
f"{name} clashes with a lowered stdlib type",
|
|
71
|
+
span_of(clash if clash is not None else ast.Pass(), self.filename),
|
|
72
|
+
None,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
self.declared.add(name)
|
|
77
|
+
self.classes[name] = {
|
|
78
|
+
"fields": fields,
|
|
79
|
+
"own_fields": list(fields),
|
|
80
|
+
"methods": {},
|
|
81
|
+
"method_kind": {},
|
|
82
|
+
"method_owner": {},
|
|
83
|
+
"synthetic": True,
|
|
84
|
+
"base": None,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def _collect_class(self, node: ast.ClassDef) -> None:
|
|
88
|
+
if node.keywords:
|
|
89
|
+
self.fail(node, "PS1203", "class keywords are not lowered")
|
|
90
|
+
if len(node.bases) > 1:
|
|
91
|
+
self.fail(node, "PS1203", "multiple inheritance is not lowered")
|
|
92
|
+
if node.name in self.bindings or node.name in self.sigs or node.name in self.declared:
|
|
93
|
+
self.fail(node, "PS2002", f"{node.name} is already bound")
|
|
94
|
+
deco = self._class_decorator(node)
|
|
95
|
+
# class C(object) and class C(object,) are the same class as class C.
|
|
96
|
+
# object.__init__ is not emitted. Two or more bases already fenced above.
|
|
97
|
+
base = None
|
|
98
|
+
if node.bases and not self._is_builtin_object(node.bases[0]):
|
|
99
|
+
base = self._resolve_base(node.bases[0])
|
|
100
|
+
if deco == "dataclass" and base is not None:
|
|
101
|
+
self.fail(node, "PS2002", "a dataclass has no base")
|
|
102
|
+
if base is not None and base["kind"] == "enum":
|
|
103
|
+
self._collect_enum(node, str(base["name"]))
|
|
104
|
+
return
|
|
105
|
+
self.declared.add(node.name)
|
|
106
|
+
parent_name = base["name"] if base is not None else None
|
|
107
|
+
parent = self.classes.get(parent_name) if parent_name and base and base["kind"] == "class" else None
|
|
108
|
+
inherited = list(parent["fields"]) if parent is not None else []
|
|
109
|
+
inherited_names = {field["name"] for field in inherited}
|
|
110
|
+
fields: list[dict[str, Any]] = []
|
|
111
|
+
methods: list[tuple[ast.FunctionDef, str]] = []
|
|
112
|
+
seen: set[str] = set()
|
|
113
|
+
seen_default = False
|
|
114
|
+
for stmt in node.body:
|
|
115
|
+
if _is_docstring(stmt) or isinstance(stmt, ast.Pass):
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
|
|
119
|
+
if stmt.value is not None and deco != "dataclass":
|
|
120
|
+
self.fail(stmt, "PS1204", "class fields are declared with an annotation and no value")
|
|
121
|
+
if stmt.target.id in seen or stmt.target.id in inherited_names:
|
|
122
|
+
self.fail(stmt, "PS2002", f"duplicate field {stmt.target.id}")
|
|
123
|
+
typ = self._annotation(stmt.annotation)
|
|
124
|
+
if typ["kind"] == "bool":
|
|
125
|
+
self.fail(stmt, "PS2002", "bool fields are not lowered")
|
|
126
|
+
field: dict[str, Any] = {"name": stmt.target.id, "type": typ}
|
|
127
|
+
if stmt.value is not None:
|
|
128
|
+
saved_prelude = self.prelude
|
|
129
|
+
self.prelude = []
|
|
130
|
+
try:
|
|
131
|
+
default = self._expr(stmt.value, typ)
|
|
132
|
+
finally:
|
|
133
|
+
self.prelude = saved_prelude
|
|
134
|
+
self._expect(stmt, default["type"], typ, stmt.target.id)
|
|
135
|
+
if default["kind"] != "const":
|
|
136
|
+
self.fail(stmt, "PS2002", "a dataclass default must be a constant")
|
|
137
|
+
field["default"] = default
|
|
138
|
+
seen_default = True
|
|
139
|
+
elif seen_default:
|
|
140
|
+
self.fail(stmt, "PS2002", "a non-default field follows a default")
|
|
141
|
+
seen.add(stmt.target.id)
|
|
142
|
+
fields.append(field)
|
|
143
|
+
continue
|
|
144
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
145
|
+
kind = self._method_decorator(stmt)
|
|
146
|
+
if stmt.name == "__init__" and kind != "method":
|
|
147
|
+
self.fail(stmt, "PS1201", "decorators are not lowered")
|
|
148
|
+
methods.append((stmt, kind))
|
|
149
|
+
continue
|
|
150
|
+
self.fail(stmt, "PS1204", f"{type(stmt).__name__} in a class body is not lowered")
|
|
151
|
+
except Fence as exc:
|
|
152
|
+
self.caught.append(exc)
|
|
153
|
+
method_sigs: dict[str, tuple[list[dict[str, Any]], dict[str, Any]]] = {}
|
|
154
|
+
kinds: dict[str, str] = {}
|
|
155
|
+
owners: dict[str, str] = {}
|
|
156
|
+
if parent is not None:
|
|
157
|
+
method_sigs.update(parent["methods"])
|
|
158
|
+
kinds.update(parent["method_kind"])
|
|
159
|
+
owners.update(parent["method_owner"])
|
|
160
|
+
wrote_init = False
|
|
161
|
+
# A field fence leaves the field set unknown; a method signature fence
|
|
162
|
+
# only drops that one method. Count each kind so the catalog pass can
|
|
163
|
+
# tell them apart.
|
|
164
|
+
body_fences = len(self.caught)
|
|
165
|
+
sig_fences = 0
|
|
166
|
+
for method, kind in methods:
|
|
167
|
+
try:
|
|
168
|
+
method_sigs[method.name] = self._signature(method, node.name, kind)
|
|
169
|
+
kinds[method.name] = kind
|
|
170
|
+
owners[method.name] = node.name
|
|
171
|
+
if method.name == "__init__":
|
|
172
|
+
wrote_init = True
|
|
173
|
+
except Fence as exc:
|
|
174
|
+
self.caught.append(exc)
|
|
175
|
+
sig_fences += 1
|
|
176
|
+
# The catalog pass still exports the shape when only method signatures
|
|
177
|
+
# fenced: the fenced methods drop out and the rest resolves, the same
|
|
178
|
+
# rule a fenced module function follows (it does not export, its
|
|
179
|
+
# siblings still do). Without this, a mixin or base class with one
|
|
180
|
+
# unlowerable annotation is invisible to every module that subclasses
|
|
181
|
+
# it, and the import fences as "not exported by this package". The
|
|
182
|
+
# lowering pass keeps the strict abort so an emitted class stays
|
|
183
|
+
# all-or-nothing.
|
|
184
|
+
if len(self.caught) > body_fences + sig_fences or (sig_fences and not self.cataloguing):
|
|
185
|
+
return
|
|
186
|
+
generated: str | None = None
|
|
187
|
+
if deco == "dataclass":
|
|
188
|
+
if wrote_init:
|
|
189
|
+
self.fail(node, "PS2002", "a dataclass does not take a written __init__")
|
|
190
|
+
params = [{"name": "self", "type": {"kind": "class", "name": node.name}}]
|
|
191
|
+
for field in fields:
|
|
192
|
+
param: dict[str, Any] = {"name": field["name"], "type": field["type"]}
|
|
193
|
+
if "default" in field:
|
|
194
|
+
param["default"] = field["default"]
|
|
195
|
+
params.append(param)
|
|
196
|
+
method_sigs["__init__"] = (params, {"kind": "none"})
|
|
197
|
+
kinds["__init__"] = "method"
|
|
198
|
+
owners["__init__"] = node.name
|
|
199
|
+
generated = "dataclass"
|
|
200
|
+
elif parent is not None and not wrote_init and "__init__" in parent["methods"]:
|
|
201
|
+
if fields:
|
|
202
|
+
self.fail(node, "PS1001", f"{node.name} needs __init__")
|
|
203
|
+
parent_params = parent["methods"]["__init__"][0]
|
|
204
|
+
params = [{"name": "self", "type": {"kind": "class", "name": node.name}}, *parent_params[1:]]
|
|
205
|
+
method_sigs["__init__"] = (params, {"kind": "none"})
|
|
206
|
+
kinds["__init__"] = "method"
|
|
207
|
+
owners["__init__"] = node.name
|
|
208
|
+
generated = "forward"
|
|
209
|
+
elif parent is not None and fields and not wrote_init:
|
|
210
|
+
self.fail(node, "PS1001", f"{node.name} needs __init__")
|
|
211
|
+
exception = bool(base and (base["kind"] == "exception" or (parent and parent.get("exception"))))
|
|
212
|
+
info: dict[str, Any] = {
|
|
213
|
+
"fields": [*inherited, *fields],
|
|
214
|
+
"own_fields": fields,
|
|
215
|
+
"methods": method_sigs,
|
|
216
|
+
"method_kind": kinds,
|
|
217
|
+
"method_owner": owners,
|
|
218
|
+
"base": parent_name,
|
|
219
|
+
"node": node,
|
|
220
|
+
}
|
|
221
|
+
if generated is not None:
|
|
222
|
+
info["generated_init"] = generated
|
|
223
|
+
if exception:
|
|
224
|
+
info["exception"] = True
|
|
225
|
+
info["exc_code"] = self.exc_n
|
|
226
|
+
self.exc_n += 1
|
|
227
|
+
self.classes[node.name] = info
|
|
228
|
+
|
|
229
|
+
def _class_decorator(self, node: ast.ClassDef) -> str | None:
|
|
230
|
+
if not node.decorator_list:
|
|
231
|
+
return None
|
|
232
|
+
if len(node.decorator_list) != 1 or not self._is_dataclass_deco(node.decorator_list[0]):
|
|
233
|
+
self.fail(node, "PS1201", "decorators are not lowered")
|
|
234
|
+
return "dataclass"
|
|
235
|
+
|
|
236
|
+
def _is_dataclass_deco(self, dec: ast.expr) -> bool:
|
|
237
|
+
target = dec
|
|
238
|
+
if isinstance(dec, ast.Call):
|
|
239
|
+
if dec.args or dec.keywords:
|
|
240
|
+
return False
|
|
241
|
+
target = dec.func
|
|
242
|
+
if isinstance(target, ast.Name):
|
|
243
|
+
binding = self.bindings.get(target.id)
|
|
244
|
+
return binding is not None and binding.module == "dataclasses" and binding.attr == "dataclass"
|
|
245
|
+
if isinstance(target, ast.Attribute):
|
|
246
|
+
resolved = self._resolve_module(target)
|
|
247
|
+
return resolved is not None and resolved[1] == "dataclasses" and resolved[2] == "dataclass"
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def _method_decorator(self, fn: ast.FunctionDef) -> str:
|
|
251
|
+
if not fn.decorator_list:
|
|
252
|
+
return "method"
|
|
253
|
+
if len(fn.decorator_list) != 1:
|
|
254
|
+
self.fail(fn, "PS1201", "decorators are not lowered")
|
|
255
|
+
dec = fn.decorator_list[0]
|
|
256
|
+
target = dec
|
|
257
|
+
if isinstance(dec, ast.Call):
|
|
258
|
+
if dec.args or dec.keywords:
|
|
259
|
+
self.fail(fn, "PS1201", "decorators are not lowered")
|
|
260
|
+
target = dec.func
|
|
261
|
+
if isinstance(target, ast.Name) and target.id in {"property", "staticmethod", "classmethod"}:
|
|
262
|
+
return {"property": "property", "staticmethod": "static", "classmethod": "class"}[target.id]
|
|
263
|
+
self.fail(fn, "PS1201", "decorators are not lowered")
|
|
264
|
+
raise AssertionError("unreachable")
|
|
265
|
+
|
|
266
|
+
def _is_builtin_object(self, node: ast.expr) -> bool:
|
|
267
|
+
if not isinstance(node, ast.Name) or node.id != "object":
|
|
268
|
+
return False
|
|
269
|
+
if node.id in self.classes or node.id in self.bindings or node.id in self.sigs or node.id in self.constants:
|
|
270
|
+
return False
|
|
271
|
+
if node.id in self.declared:
|
|
272
|
+
return False
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
def _resolve_base(self, node: ast.expr) -> dict[str, str]:
|
|
276
|
+
enum = self._enum_base_name(node)
|
|
277
|
+
if enum is not None:
|
|
278
|
+
return {"kind": "enum", "name": enum}
|
|
279
|
+
if isinstance(node, ast.Name) and node.id in EXCEPTIONS:
|
|
280
|
+
return {"kind": "exception", "name": node.id}
|
|
281
|
+
if isinstance(node, ast.Name) and node.id in self.classes:
|
|
282
|
+
info = self.classes[node.id]
|
|
283
|
+
if info.get("synthetic"):
|
|
284
|
+
self.fail(node, "PS2002", "subclassing a stdlib type is not lowered")
|
|
285
|
+
if info.get("enum"):
|
|
286
|
+
self.fail(node, "PS2002", "an enum is not a base")
|
|
287
|
+
return {"kind": "class", "name": node.id}
|
|
288
|
+
self.fail(node, "PS2002", "this class base is not lowered")
|
|
289
|
+
raise AssertionError("unreachable")
|
|
290
|
+
|
|
291
|
+
def _enum_base_name(self, node: ast.expr) -> str | None:
|
|
292
|
+
if isinstance(node, ast.Name):
|
|
293
|
+
binding = self.bindings.get(node.id)
|
|
294
|
+
if binding is not None and binding.module == "enum" and binding.attr in {"Enum", "IntEnum"}:
|
|
295
|
+
return binding.attr
|
|
296
|
+
return None
|
|
297
|
+
if isinstance(node, ast.Attribute):
|
|
298
|
+
resolved = self._resolve_module(node)
|
|
299
|
+
if resolved is not None and resolved[1] == "enum" and resolved[2] in {"Enum", "IntEnum"}:
|
|
300
|
+
return resolved[2]
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def _collect_enum(self, node: ast.ClassDef, which: str) -> None:
|
|
304
|
+
self.declared.add(node.name)
|
|
305
|
+
members: dict[str, int] = {}
|
|
306
|
+
for stmt in node.body:
|
|
307
|
+
if _is_docstring(stmt) or isinstance(stmt, ast.Pass):
|
|
308
|
+
continue
|
|
309
|
+
try:
|
|
310
|
+
target, value = self._enum_assign(stmt)
|
|
311
|
+
number = _const_int_index(value)
|
|
312
|
+
if number is None:
|
|
313
|
+
self.fail(stmt, "PS2002", "an enum member is an integer constant")
|
|
314
|
+
if target in members:
|
|
315
|
+
self.fail(stmt, "PS2002", f"duplicate enum member {target}")
|
|
316
|
+
members[target] = number
|
|
317
|
+
except Fence as exc:
|
|
318
|
+
self.caught.append(exc)
|
|
319
|
+
if self.caught:
|
|
320
|
+
return
|
|
321
|
+
if not members:
|
|
322
|
+
self.fail(node, "PS2002", "an enum needs a member")
|
|
323
|
+
self.classes[node.name] = {
|
|
324
|
+
"fields": [],
|
|
325
|
+
"own_fields": [],
|
|
326
|
+
"methods": {},
|
|
327
|
+
"method_kind": {},
|
|
328
|
+
"method_owner": {},
|
|
329
|
+
"base": which,
|
|
330
|
+
"enum": True,
|
|
331
|
+
"members": members,
|
|
332
|
+
"node": node,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
def _enum_assign(self, stmt: ast.stmt) -> tuple[str, ast.expr]:
|
|
336
|
+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
|
|
337
|
+
return stmt.targets[0].id, stmt.value
|
|
338
|
+
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name) and stmt.value is not None:
|
|
339
|
+
return stmt.target.id, stmt.value
|
|
340
|
+
self.fail(stmt, "PS2002", "an enum body is integer constants")
|
|
341
|
+
raise AssertionError("unreachable")
|
|
342
|
+
|
|
343
|
+
def _lower_class(self, node: ast.ClassDef) -> dict[str, Any] | None:
|
|
344
|
+
info = self.classes[node.name]
|
|
345
|
+
if info.get("enum"):
|
|
346
|
+
return None
|
|
347
|
+
own_names = {field["name"] for field in info.get("own_fields", info["fields"])}
|
|
348
|
+
methods: list[dict[str, Any]] = []
|
|
349
|
+
for stmt in node.body:
|
|
350
|
+
if not isinstance(stmt, ast.FunctionDef):
|
|
351
|
+
continue
|
|
352
|
+
if info["method_owner"].get(stmt.name) != node.name:
|
|
353
|
+
continue
|
|
354
|
+
self.function = f"{node.name}.{stmt.name}"
|
|
355
|
+
self.temp_n = 0
|
|
356
|
+
self.prelude = []
|
|
357
|
+
self.fn_globals = self._globals_for(stmt.body)
|
|
358
|
+
params, ret = info["methods"][stmt.name]
|
|
359
|
+
kind = info["method_kind"].get(stmt.name, "method")
|
|
360
|
+
self.locals = {param["name"]: param["type"] for param in params}
|
|
361
|
+
self.frozen_names = set()
|
|
362
|
+
self.fn_body = list(stmt.body)
|
|
363
|
+
if kind in {"method", "property"}:
|
|
364
|
+
self.locals["self"] = {"kind": "class", "name": node.name}
|
|
365
|
+
body = self._body(list(stmt.body))
|
|
366
|
+
if self.caught:
|
|
367
|
+
continue
|
|
368
|
+
if stmt.name == "__init__":
|
|
369
|
+
assigned = _assigned_fields(stmt.body)
|
|
370
|
+
missing = sorted(own_names - assigned)
|
|
371
|
+
if missing:
|
|
372
|
+
self.fail(stmt, "PS1001", f"__init__ does not assign {', '.join(missing)}")
|
|
373
|
+
if ret["kind"] != "none" and not _always_returns(body):
|
|
374
|
+
self.fail(stmt, "PS1004", f"{stmt.name} can fall off the end and return None")
|
|
375
|
+
methods.append(self._method_ir(stmt.name, params, ret, body, span_of(stmt, self.filename)))
|
|
376
|
+
if self.caught:
|
|
377
|
+
return None
|
|
378
|
+
generated = self._generated_method(node, info)
|
|
379
|
+
if generated is not None:
|
|
380
|
+
methods.append(generated)
|
|
381
|
+
return {
|
|
382
|
+
"name": node.name,
|
|
383
|
+
"fields": info["fields"],
|
|
384
|
+
"methods": methods,
|
|
385
|
+
"span": span_of(node, self.filename),
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
def _method_ir(
|
|
389
|
+
self,
|
|
390
|
+
name: str,
|
|
391
|
+
params: list[dict[str, Any]],
|
|
392
|
+
ret: dict[str, Any],
|
|
393
|
+
body: list[dict[str, Any]],
|
|
394
|
+
span: dict[str, Any],
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
return {
|
|
397
|
+
"name": name,
|
|
398
|
+
"params": params,
|
|
399
|
+
"returnType": ret,
|
|
400
|
+
"locals": [{"name": local, "type": typ} for local, typ in self.locals.items()]
|
|
401
|
+
if self.locals
|
|
402
|
+
else [{"name": param["name"], "type": param["type"]} for param in params],
|
|
403
|
+
"body": body,
|
|
404
|
+
"span": span,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def _generated_method(self, node: ast.ClassDef, info: dict[str, Any]) -> dict[str, Any] | None:
|
|
408
|
+
mode = info.get("generated_init")
|
|
409
|
+
if mode is None:
|
|
410
|
+
return None
|
|
411
|
+
params, ret = info["methods"]["__init__"]
|
|
412
|
+
span = span_of(node, self.filename)
|
|
413
|
+
self_type = {"kind": "class", "name": node.name}
|
|
414
|
+
if mode == "dataclass":
|
|
415
|
+
body = [
|
|
416
|
+
{
|
|
417
|
+
"kind": "setattr",
|
|
418
|
+
"object": _name("self", self_type, span),
|
|
419
|
+
"name": field["name"],
|
|
420
|
+
"value": _name(field["name"], field["type"], span),
|
|
421
|
+
"span": span,
|
|
422
|
+
}
|
|
423
|
+
for field in info["own_fields"]
|
|
424
|
+
]
|
|
425
|
+
else:
|
|
426
|
+
parent = info["base"]
|
|
427
|
+
args = [_name(param["name"], param["type"], span) for param in params]
|
|
428
|
+
body = [
|
|
429
|
+
{
|
|
430
|
+
"kind": "expr",
|
|
431
|
+
"expr": {
|
|
432
|
+
"kind": "call",
|
|
433
|
+
"callee": {"kind": "function", "name": f"cls.{parent}.__init__"},
|
|
434
|
+
"args": args,
|
|
435
|
+
"type": {"kind": "none"},
|
|
436
|
+
"span": span,
|
|
437
|
+
},
|
|
438
|
+
"span": span,
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
self.locals = {param["name"]: param["type"] for param in params}
|
|
442
|
+
return self._method_ir("__init__", params, ret, body, span)
|
|
443
|
+
|
|
444
|
+
def _signature(
|
|
445
|
+
self, fn: ast.FunctionDef, class_name: str | None, kind: str = "method"
|
|
446
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
447
|
+
if fn.args.kwarg or fn.args.kwonlyargs or fn.args.posonlyargs:
|
|
448
|
+
self.fail(fn, "PS1202", f"{fn.name} uses a parameter form that is not lowered")
|
|
449
|
+
args = list(fn.args.args)
|
|
450
|
+
params: list[dict[str, Any]] = []
|
|
451
|
+
if kind == "property":
|
|
452
|
+
if fn.args.vararg is not None or len(args) != 1 or args[0].arg != "self":
|
|
453
|
+
self.fail(fn, "PS2002", "a property's only parameter is self")
|
|
454
|
+
if args[0].annotation is not None:
|
|
455
|
+
self.fail(args[0], "PS2002", "self is not annotated")
|
|
456
|
+
if class_name is None:
|
|
457
|
+
self.fail(fn, "PS1201", "decorators are not lowered")
|
|
458
|
+
params.append({"name": "self", "type": {"kind": "class", "name": class_name}})
|
|
459
|
+
args = []
|
|
460
|
+
elif kind == "class":
|
|
461
|
+
if not args or args[0].arg != "cls":
|
|
462
|
+
self.fail(fn, "PS2002", "a classmethod's first parameter must be cls")
|
|
463
|
+
if args[0].annotation is not None:
|
|
464
|
+
self.fail(args[0], "PS2002", "cls is not annotated")
|
|
465
|
+
# cls is not a value. Class.method(...) does not pass the class object.
|
|
466
|
+
args = args[1:]
|
|
467
|
+
elif kind == "static":
|
|
468
|
+
pass
|
|
469
|
+
elif class_name is not None:
|
|
470
|
+
if not args or args[0].arg != "self":
|
|
471
|
+
self.fail(fn, "PS2002", "a method's first parameter must be self")
|
|
472
|
+
if args[0].annotation is not None:
|
|
473
|
+
self.fail(args[0], "PS2002", "self is not annotated")
|
|
474
|
+
params.append({"name": "self", "type": {"kind": "class", "name": class_name}})
|
|
475
|
+
args = args[1:]
|
|
476
|
+
defaults = list(fn.args.defaults)
|
|
477
|
+
missing = len(args) - len(defaults)
|
|
478
|
+
for index, arg in enumerate(args):
|
|
479
|
+
param = _param(self, arg)
|
|
480
|
+
if index >= missing:
|
|
481
|
+
default_node = defaults[index - missing]
|
|
482
|
+
value = self._expr(default_node, param["type"])
|
|
483
|
+
self._expect(default_node, value["type"], param["type"], arg.arg)
|
|
484
|
+
if value["kind"] != "const":
|
|
485
|
+
self.fail(default_node, "PS2002", "a default must be a constant")
|
|
486
|
+
param["default"] = value
|
|
487
|
+
params.append(param)
|
|
488
|
+
if fn.args.vararg is not None:
|
|
489
|
+
vararg = fn.args.vararg
|
|
490
|
+
if vararg.annotation is None:
|
|
491
|
+
self.fail(vararg, "PS1001", f"parameter {vararg.arg} needs an annotation")
|
|
492
|
+
elem = self._annotation(vararg.annotation)
|
|
493
|
+
if elem["kind"] == "bool":
|
|
494
|
+
self.fail(vararg, "PS2002", "list[bool] is not lowered")
|
|
495
|
+
params.append({"name": vararg.arg, "type": {"kind": "list", "elem": elem}, "vararg": True})
|
|
496
|
+
if fn.returns is None:
|
|
497
|
+
self.fail(fn, "PS1001", f"{fn.name} needs a return annotation")
|
|
498
|
+
ret = self._annotation(fn.returns)
|
|
499
|
+
if class_name is not None and fn.name == "__init__" and ret["kind"] != "none":
|
|
500
|
+
self.fail(fn, "PS1003", "__init__ returns None")
|
|
501
|
+
return params, ret
|
|
502
|
+
|
|
503
|
+
def _construct(self, node: ast.Call, span: dict[str, Any], name: str) -> dict[str, Any]:
|
|
504
|
+
info = self.classes[name]
|
|
505
|
+
init = info["methods"].get("__init__")
|
|
506
|
+
if init is None:
|
|
507
|
+
if node.args or node.keywords:
|
|
508
|
+
self.fail(node, "PS2003", f"{name} takes no arguments")
|
|
509
|
+
return {
|
|
510
|
+
"kind": "call",
|
|
511
|
+
"callee": {"kind": "class", "name": name},
|
|
512
|
+
"args": [],
|
|
513
|
+
"type": {"kind": "class", "name": name},
|
|
514
|
+
"span": span,
|
|
515
|
+
}
|
|
516
|
+
params = [param for param in init[0] if param["name"] != "self"]
|
|
517
|
+
args = self._fill_args(node, params, name)
|
|
518
|
+
return {
|
|
519
|
+
"kind": "call",
|
|
520
|
+
"callee": {"kind": "class", "name": name},
|
|
521
|
+
"args": args,
|
|
522
|
+
"type": {"kind": "class", "name": name},
|
|
523
|
+
"span": span,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
def _reject_banned_type(self, node: ast.AST, typ: dict[str, Any]) -> None:
|
|
527
|
+
if typ["kind"] == "list" and typ["elem"]["kind"] == "bool":
|
|
528
|
+
self.fail(node, "PS2002", "list[bool] is not lowered")
|
|
529
|
+
if typ["kind"] == "dict":
|
|
530
|
+
if typ["key"]["kind"] not in {"int", "str"}:
|
|
531
|
+
self.fail(node, "PS2002", "dict keys are int or str")
|
|
532
|
+
if typ["value"]["kind"] == "bool":
|
|
533
|
+
self.fail(node, "PS2002", "dict values of bool are not lowered")
|
|
534
|
+
|
|
535
|
+
def _annotation(self, node: ast.AST) -> dict[str, Any]:
|
|
536
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
537
|
+
self.fail(node, "PS1101", "union types are not in the IR")
|
|
538
|
+
formed = self._typing_annotation(node)
|
|
539
|
+
if formed is not None:
|
|
540
|
+
return formed
|
|
541
|
+
if isinstance(node, ast.Name) and node.id in {"int", "float", "bool", "str", "bytes"}:
|
|
542
|
+
return {"kind": node.id}
|
|
543
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
544
|
+
return {"kind": "none"}
|
|
545
|
+
if isinstance(node, ast.Name) and (node.id in self.declared or node.id in self.known_classes):
|
|
546
|
+
return {"kind": "class", "name": self.class_aliases.get(node.id, node.id)}
|
|
547
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "list":
|
|
548
|
+
typ = {"kind": "list", "elem": self._annotation(node.slice)}
|
|
549
|
+
self._reject_banned_type(node, typ)
|
|
550
|
+
return typ
|
|
551
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "set":
|
|
552
|
+
elem = self._annotation(node.slice)
|
|
553
|
+
if elem["kind"] not in {"int", "str"}:
|
|
554
|
+
self.fail(node, "PS2002", "set elements are int or str")
|
|
555
|
+
return {"kind": "list", "elem": elem, "set": True}
|
|
556
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "tuple":
|
|
557
|
+
return self._tuple_annotation(node)
|
|
558
|
+
aliased = self._aliased_annotation(node)
|
|
559
|
+
if aliased is not None:
|
|
560
|
+
return aliased
|
|
561
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "dict":
|
|
562
|
+
if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != 2:
|
|
563
|
+
self.fail(node, "PS1001", "dict annotations are dict[key, value]")
|
|
564
|
+
typ = {
|
|
565
|
+
"kind": "dict",
|
|
566
|
+
"key": self._annotation(node.slice.elts[0]),
|
|
567
|
+
"value": self._annotation(node.slice.elts[1]),
|
|
568
|
+
}
|
|
569
|
+
self._reject_banned_type(node, typ)
|
|
570
|
+
return typ
|
|
571
|
+
named = self._annotation_class(node)
|
|
572
|
+
if named is not None:
|
|
573
|
+
return {"kind": "class", "name": named}
|
|
574
|
+
self._fail_annotation(node)
|
|
575
|
+
|
|
576
|
+
def _fail_annotation(self, node: ast.AST) -> NoReturn:
|
|
577
|
+
"""Fence an annotation the subset does not lower, with the code that names why."""
|
|
578
|
+
text = ast.unparse(node)
|
|
579
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
580
|
+
self.fail(node, "PS1105", f"the forward reference {text} is not lowered")
|
|
581
|
+
if isinstance(node, ast.Subscript):
|
|
582
|
+
self.fail(node, "PS1103", f"the generic annotation {text} is not lowered")
|
|
583
|
+
tail = text.rsplit(".", 1)[-1]
|
|
584
|
+
if tail in _DYNAMIC_ANNOTATIONS:
|
|
585
|
+
self.fail(node, "PS1104", f"{text} is a dynamic or structural type and is not lowered")
|
|
586
|
+
if isinstance(node, (ast.Name, ast.Attribute)):
|
|
587
|
+
self.fail(node, "PS1106", f"the annotation {text} names a type that is not lowered")
|
|
588
|
+
self.fail(node, "PS1001", "this annotation is not in the lowered subset")
|
|
589
|
+
|
|
590
|
+
def _class_binary(
|
|
591
|
+
self, node: ast.BinOp, left: dict[str, Any], right: dict[str, Any], span: dict[str, Any]
|
|
592
|
+
) -> dict[str, Any]:
|
|
593
|
+
op = type(node.op)
|
|
594
|
+
left_name = left["type"].get("name")
|
|
595
|
+
right_name = right["type"].get("name")
|
|
596
|
+
if op in {ast.Add, ast.Sub}:
|
|
597
|
+
sign = "+" if op is ast.Add else "-"
|
|
598
|
+
if left_name in {"Date", "DateTime"} and right_name == "TimeDelta":
|
|
599
|
+
return {"kind": "binary", "op": sign, "left": left, "right": right, "type": left["type"], "span": span}
|
|
600
|
+
if left_name == "TimeDelta" and right_name == "TimeDelta":
|
|
601
|
+
return {"kind": "binary", "op": sign, "left": left, "right": right, "type": left["type"], "span": span}
|
|
602
|
+
if op is ast.Sub and left_name == right_name and left_name in {"Date", "DateTime"}:
|
|
603
|
+
return {
|
|
604
|
+
"kind": "binary",
|
|
605
|
+
"op": "-",
|
|
606
|
+
"left": left,
|
|
607
|
+
"right": right,
|
|
608
|
+
"type": {"kind": "class", "name": "TimeDelta"},
|
|
609
|
+
"span": span,
|
|
610
|
+
}
|
|
611
|
+
self.fail(node, "PS2004", f"{op.__name__} on {left_name} and {right_name} is not lowered")
|
|
612
|
+
raise AssertionError("unreachable")
|
|
613
|
+
|
|
614
|
+
def _lookup_attr(self, obj: dict[str, Any], name: str, span: dict[str, Any], node: ast.AST) -> dict[str, Any]:
|
|
615
|
+
if obj["type"]["kind"] != "class":
|
|
616
|
+
self.fail(node, "PS2001", f"attribute access is not lowered ({name})")
|
|
617
|
+
class_name = obj["type"]["name"]
|
|
618
|
+
info = self.classes.get(class_name)
|
|
619
|
+
if info is None:
|
|
620
|
+
self.fail(node, "PS2001", f"{class_name} has no fields")
|
|
621
|
+
for field in info["fields"]:
|
|
622
|
+
if field["name"] == name:
|
|
623
|
+
return {"kind": "attr", "value": obj, "name": name, "type": field["type"], "span": span}
|
|
624
|
+
if info.get("method_kind", {}).get(name) == "property":
|
|
625
|
+
sig = info["methods"][name]
|
|
626
|
+
owner = info.get("method_owner", {}).get(name, class_name)
|
|
627
|
+
return {
|
|
628
|
+
"kind": "call",
|
|
629
|
+
"callee": {"kind": "function", "name": f"cls.{owner}.{name}"},
|
|
630
|
+
"args": [obj],
|
|
631
|
+
"type": sig[1],
|
|
632
|
+
"span": span,
|
|
633
|
+
}
|
|
634
|
+
attr = path_attr(name) if class_name == "Path" else None
|
|
635
|
+
if attr is not None:
|
|
636
|
+
typ, symbol = attr
|
|
637
|
+
return self._abi_call("pathlib", name, [obj], typ, symbol, span)
|
|
638
|
+
if synth_method(class_name, name) is not None:
|
|
639
|
+
self.fail(node, "PS2002", f"{class_name}.{name} is a function and is not a value")
|
|
640
|
+
self.fail(node, "PS2001", f"{class_name}.{name} is not a field")
|
|
641
|
+
raise AssertionError("unreachable")
|
|
642
|
+
|
|
643
|
+
def _has_attr(self, obj: dict[str, Any], name: str) -> bool:
|
|
644
|
+
if obj["type"]["kind"] != "class":
|
|
645
|
+
return False
|
|
646
|
+
class_name = obj["type"]["name"]
|
|
647
|
+
info = self.classes.get(class_name)
|
|
648
|
+
if info is None:
|
|
649
|
+
return False
|
|
650
|
+
if any(field["name"] == name for field in info["fields"]):
|
|
651
|
+
return True
|
|
652
|
+
if info.get("method_kind", {}).get(name) == "property":
|
|
653
|
+
return True
|
|
654
|
+
if class_name == "Path" and path_attr(name) is not None:
|
|
655
|
+
return True
|
|
656
|
+
return synth_method(class_name, name) is not None
|
|
657
|
+
|
|
658
|
+
def _isinstance_type(self, node: ast.expr) -> dict[str, Any]:
|
|
659
|
+
if isinstance(node, ast.Name) and node.id in {"int", "float", "bool", "str", "bytes"}:
|
|
660
|
+
return {"kind": node.id}
|
|
661
|
+
if isinstance(node, ast.Name) and node.id in {"list", "dict"}:
|
|
662
|
+
return {"kind": node.id, "bare": True}
|
|
663
|
+
if isinstance(node, ast.Name) and (
|
|
664
|
+
node.id in self.classes or node.id in self.class_aliases or node.id in EXCEPTIONS
|
|
665
|
+
):
|
|
666
|
+
return {"kind": "class", "name": self.class_aliases.get(node.id, node.id)}
|
|
667
|
+
if isinstance(node, ast.Name):
|
|
668
|
+
binding = self.bindings.get(node.id)
|
|
669
|
+
if binding is not None:
|
|
670
|
+
alias = alias_name(binding.module, binding.attr)
|
|
671
|
+
if alias is not None:
|
|
672
|
+
return {"kind": "class", "name": alias}
|
|
673
|
+
if isinstance(node, ast.Attribute):
|
|
674
|
+
resolved = self._resolve_module(node)
|
|
675
|
+
if resolved is not None:
|
|
676
|
+
alias = alias_name(resolved[1], resolved[2])
|
|
677
|
+
if alias is not None:
|
|
678
|
+
return {"kind": "class", "name": alias}
|
|
679
|
+
self.fail(node, "PS2001", "isinstance type is not in the lowered subset")
|
|
680
|
+
raise AssertionError("unreachable")
|
|
681
|
+
|
|
682
|
+
def _isinstance_holds(self, got: dict[str, Any], want: dict[str, Any]) -> bool:
|
|
683
|
+
if want.get("bare") and got["kind"] == want["kind"]:
|
|
684
|
+
return True
|
|
685
|
+
if want["kind"] == "int" and got["kind"] == "bool":
|
|
686
|
+
return True
|
|
687
|
+
if want["kind"] == "class" and got["kind"] == "class":
|
|
688
|
+
got_name = got.get("name")
|
|
689
|
+
want_name = want.get("name")
|
|
690
|
+
if isinstance(got_name, str) and isinstance(want_name, str) and self._is_subclass_name(got_name, want_name):
|
|
691
|
+
return True
|
|
692
|
+
return types_equal(got, want)
|
|
693
|
+
|
|
694
|
+
def _is_subclass_name(self, got: str, want: str) -> bool:
|
|
695
|
+
current: str | None = got
|
|
696
|
+
seen: set[str] = set()
|
|
697
|
+
while current and current not in seen:
|
|
698
|
+
if current == want:
|
|
699
|
+
return True
|
|
700
|
+
seen.add(current)
|
|
701
|
+
info = self.classes.get(current)
|
|
702
|
+
if info is None:
|
|
703
|
+
return False
|
|
704
|
+
parent = info.get("base")
|
|
705
|
+
current = parent if isinstance(parent, str) else None
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
def _exception_codes(self, name: str) -> list[int]:
|
|
709
|
+
found: list[int] = []
|
|
710
|
+
if name in EXCEPTIONS:
|
|
711
|
+
found.append(EXCEPTIONS[name])
|
|
712
|
+
else:
|
|
713
|
+
info = self.classes.get(name)
|
|
714
|
+
code = info.get("exc_code") if info is not None else None
|
|
715
|
+
if isinstance(code, int):
|
|
716
|
+
found.append(code)
|
|
717
|
+
for class_name, info in self.classes.items():
|
|
718
|
+
code = info.get("exc_code")
|
|
719
|
+
if class_name == name or not isinstance(code, int):
|
|
720
|
+
continue
|
|
721
|
+
if self._is_subclass_name(class_name, name):
|
|
722
|
+
found.append(code)
|
|
723
|
+
return found
|
|
724
|
+
|
|
725
|
+
def _aliased_annotation(self, node: ast.AST) -> dict[str, Any] | None:
|
|
726
|
+
if not isinstance(node, ast.Subscript):
|
|
727
|
+
return None
|
|
728
|
+
alias = self._annotation_alias(node.value)
|
|
729
|
+
if alias == "Deque":
|
|
730
|
+
elem = self._annotation(node.slice)
|
|
731
|
+
if elem["kind"] == "bool":
|
|
732
|
+
self.fail(node, "PS2002", "list[bool] is not lowered")
|
|
733
|
+
return {"kind": "class", "name": "Deque", "elem": elem}
|
|
734
|
+
if alias == "OrderedDict":
|
|
735
|
+
if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != 2:
|
|
736
|
+
self.fail(node, "PS1001", "OrderedDict annotations are OrderedDict[key, value]")
|
|
737
|
+
typ = {
|
|
738
|
+
"kind": "dict",
|
|
739
|
+
"key": self._annotation(node.slice.elts[0]),
|
|
740
|
+
"value": self._annotation(node.slice.elts[1]),
|
|
741
|
+
}
|
|
742
|
+
self._reject_banned_type(node, typ)
|
|
743
|
+
return typ
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
def _annotation_class(self, node: ast.AST) -> str | None:
|
|
747
|
+
if isinstance(node, ast.Name):
|
|
748
|
+
binding = self.bindings.get(node.id)
|
|
749
|
+
if binding is None:
|
|
750
|
+
return None
|
|
751
|
+
return alias_name(binding.module, binding.attr)
|
|
752
|
+
if isinstance(node, ast.Attribute):
|
|
753
|
+
resolved = self._resolve_module(node)
|
|
754
|
+
if resolved is None:
|
|
755
|
+
return None
|
|
756
|
+
if resolved[3] == "erased":
|
|
757
|
+
return _IO_HANDLES.get(resolved[2])
|
|
758
|
+
return alias_name(resolved[1], resolved[2])
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
def _annotation_alias(self, node: ast.AST) -> str | None:
|
|
762
|
+
if isinstance(node, ast.Name):
|
|
763
|
+
binding = self.bindings.get(node.id)
|
|
764
|
+
if binding is None:
|
|
765
|
+
return None
|
|
766
|
+
if binding.module == "collections" and binding.attr == "deque":
|
|
767
|
+
return "Deque"
|
|
768
|
+
if binding.module == "collections" and binding.attr == "OrderedDict":
|
|
769
|
+
return "OrderedDict"
|
|
770
|
+
return alias_name(binding.module, binding.attr)
|
|
771
|
+
if isinstance(node, ast.Attribute):
|
|
772
|
+
resolved = self._resolve_module(node)
|
|
773
|
+
if resolved is None or resolved[3] != "lowered":
|
|
774
|
+
return None
|
|
775
|
+
if resolved[1] == "collections" and resolved[2] == "deque":
|
|
776
|
+
return "Deque"
|
|
777
|
+
if resolved[1] == "collections" and resolved[2] == "OrderedDict":
|
|
778
|
+
return "OrderedDict"
|
|
779
|
+
return None
|
|
780
|
+
|
|
781
|
+
def _typing_annotation(self, node: ast.AST) -> dict[str, Any] | None:
|
|
782
|
+
if isinstance(node, ast.Name) and node.id in self.type_aliases:
|
|
783
|
+
alias = self.type_aliases[node.id]
|
|
784
|
+
handle = _IO_HANDLES.get(alias)
|
|
785
|
+
if handle is not None:
|
|
786
|
+
return {"kind": "class", "name": handle}
|
|
787
|
+
code = "PS1104" if alias in _DYNAMIC_ANNOTATIONS else "PS1103"
|
|
788
|
+
self.fail(node, code, f"{alias} is not a value type in the IR")
|
|
789
|
+
if not isinstance(node, ast.Subscript):
|
|
790
|
+
return None
|
|
791
|
+
alias = self._typing_alias(node.value)
|
|
792
|
+
if alias is None:
|
|
793
|
+
return None
|
|
794
|
+
if alias == "IO":
|
|
795
|
+
handle = _io_parameter(node.slice)
|
|
796
|
+
if handle is None:
|
|
797
|
+
self.fail(node, "PS1001", "IO is not a value type in the IR")
|
|
798
|
+
return {"kind": "class", "name": handle}
|
|
799
|
+
if alias in _LIST_FORMS:
|
|
800
|
+
typ = {"kind": "list", "elem": self._annotation(node.slice)}
|
|
801
|
+
self._reject_banned_type(node, typ)
|
|
802
|
+
return typ
|
|
803
|
+
if alias in _SET_FORMS:
|
|
804
|
+
elem = self._annotation(node.slice)
|
|
805
|
+
if elem["kind"] not in {"int", "str"}:
|
|
806
|
+
self.fail(node, "PS2002", "set elements are int or str")
|
|
807
|
+
return {"kind": "list", "elem": elem, "set": True}
|
|
808
|
+
if alias in _DICT_FORMS:
|
|
809
|
+
if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != 2:
|
|
810
|
+
self.fail(node, "PS1001", "Dict annotations are Dict[key, value]")
|
|
811
|
+
typ = {
|
|
812
|
+
"kind": "dict",
|
|
813
|
+
"key": self._annotation(node.slice.elts[0]),
|
|
814
|
+
"value": self._annotation(node.slice.elts[1]),
|
|
815
|
+
}
|
|
816
|
+
self._reject_banned_type(node, typ)
|
|
817
|
+
return typ
|
|
818
|
+
if alias == "Tuple":
|
|
819
|
+
return self._tuple_annotation(node)
|
|
820
|
+
if alias in _UNWRAP_FORMS:
|
|
821
|
+
inner = node.slice
|
|
822
|
+
if alias == "Annotated":
|
|
823
|
+
if isinstance(inner, ast.Tuple) and inner.elts:
|
|
824
|
+
inner = inner.elts[0]
|
|
825
|
+
else:
|
|
826
|
+
self.fail(node, "PS1001", "Annotated needs a type")
|
|
827
|
+
return self._annotation(inner)
|
|
828
|
+
if alias == "Optional":
|
|
829
|
+
self.fail(node, "PS1102", "optional types are not in the IR")
|
|
830
|
+
if alias == "Union":
|
|
831
|
+
self.fail(node, "PS1101", "union types are not in the IR")
|
|
832
|
+
self.fail(node, "PS1103", f"{alias} is not a value type in the IR")
|
|
833
|
+
|
|
834
|
+
def _typing_alias(self, node: ast.AST) -> str | None:
|
|
835
|
+
if isinstance(node, ast.Name) and node.id in self.type_aliases:
|
|
836
|
+
return self.type_aliases[node.id]
|
|
837
|
+
if isinstance(node, ast.Attribute):
|
|
838
|
+
resolved = self._resolve_module(node)
|
|
839
|
+
if resolved is not None and resolved[3] == "erased":
|
|
840
|
+
return resolved[2]
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
def _synthetic_classes(self, seen: set[str]) -> list[dict[str, Any]]:
|
|
844
|
+
found: list[dict[str, Any]] = []
|
|
845
|
+
for name, info in self.classes.items():
|
|
846
|
+
if name in seen or not info.get("synthetic"):
|
|
847
|
+
continue
|
|
848
|
+
found.append(
|
|
849
|
+
{
|
|
850
|
+
"name": name,
|
|
851
|
+
"fields": info["fields"],
|
|
852
|
+
"methods": [],
|
|
853
|
+
"span": {"file": self.filename, "line": 1, "col": 1, "endLine": 1, "endCol": 1},
|
|
854
|
+
}
|
|
855
|
+
)
|
|
856
|
+
return found
|
|
857
|
+
|
|
858
|
+
def _tuple_annotation(self, node: ast.Subscript) -> dict[str, Any]:
|
|
859
|
+
elts = list(node.slice.elts) if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
860
|
+
if any(_ellipsis_const(elt) for elt in elts):
|
|
861
|
+
# tuple[T, ...] is a homogeneous sequence of zero or more T. That is the
|
|
862
|
+
# list[T] struct.unpack already returns. A synthetic class would be a new
|
|
863
|
+
# shape. Mixed or nested ellipsis stays the current fence.
|
|
864
|
+
if len(elts) == 2 and _ellipsis_const(elts[1]) and not _node_has_ellipsis(elts[0]):
|
|
865
|
+
elem = self._annotation(elts[0])
|
|
866
|
+
typ = {"kind": "list", "elem": elem}
|
|
867
|
+
self._reject_banned_type(node, typ)
|
|
868
|
+
return typ
|
|
869
|
+
self.fail(node, "PS1001", "tuple[T, ...] is not in the IR")
|
|
870
|
+
if len(elts) < 2:
|
|
871
|
+
self.fail(node, "PS1001", "tuples need at least two fields")
|
|
872
|
+
return self._tuple_type([self._annotation(elt) for elt in elts], node)
|
|
873
|
+
|
|
874
|
+
def _tuple_type(self, fields: list[dict[str, Any]], node: ast.AST) -> dict[str, Any]:
|
|
875
|
+
label = "tuple[" + ", ".join(_show(field) for field in fields) + "]"
|
|
876
|
+
name = "tuple_" + str(len(fields)) + "_" + "_".join(_type_slug(field) for field in fields)
|
|
877
|
+
existing = self.classes.get(name)
|
|
878
|
+
if existing is None:
|
|
879
|
+
if name in self.declared:
|
|
880
|
+
self.fail(node, "PS2002", f"{name} clashes with a tuple type")
|
|
881
|
+
self.classes[name] = {
|
|
882
|
+
"fields": [{"name": f"_{index}", "type": field} for index, field in enumerate(fields)],
|
|
883
|
+
"methods": {},
|
|
884
|
+
"synthetic": True,
|
|
885
|
+
"tuple": True,
|
|
886
|
+
}
|
|
887
|
+
self.declared.add(name)
|
|
888
|
+
elif not existing.get("tuple"):
|
|
889
|
+
self.fail(node, "PS2002", f"{name} clashes with a tuple type")
|
|
890
|
+
return {"kind": "class", "name": name, "label": label}
|
|
891
|
+
|
|
892
|
+
def _tuple_expr(self, node: ast.Tuple, span: dict[str, Any]) -> dict[str, Any]:
|
|
893
|
+
if len(node.elts) < 2:
|
|
894
|
+
self.fail(node, "PS2002", "tuples need at least two fields")
|
|
895
|
+
elems = [self._expr(elt) for elt in node.elts]
|
|
896
|
+
return self._materialize_tuple(elems, span, node)
|
|
897
|
+
|
|
898
|
+
def _tuple_subscript(self, node: ast.Subscript, value: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
|
|
899
|
+
index = _const_int_index(node.slice)
|
|
900
|
+
if index is None:
|
|
901
|
+
self.fail(node, "PS2002", "a tuple index must be a constant")
|
|
902
|
+
fields = self.classes[value["type"]["name"]]["fields"]
|
|
903
|
+
if index < 0:
|
|
904
|
+
index += len(fields)
|
|
905
|
+
if index < 0 or index >= len(fields):
|
|
906
|
+
self.fail(node, "PS2002", "tuple index is out of range")
|
|
907
|
+
field = fields[index]
|
|
908
|
+
return {"kind": "attr", "value": value, "name": field["name"], "type": field["type"], "span": span}
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def _param(lowerer: Any, arg: ast.arg) -> dict[str, Any]:
|
|
912
|
+
if arg.annotation is None:
|
|
913
|
+
lowerer.fail(arg, "PS1001", f"parameter {arg.arg} needs an annotation")
|
|
914
|
+
return {"name": arg.arg, "type": lowerer._annotation(arg.annotation)}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _ellipsis_const(node: ast.AST) -> bool:
|
|
918
|
+
return isinstance(node, ast.Constant) and node.value is Ellipsis
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _node_has_ellipsis(node: ast.AST) -> bool:
|
|
922
|
+
return any(_ellipsis_const(child) for child in ast.walk(node))
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _io_parameter(node: ast.AST) -> str | None:
|
|
926
|
+
"""IO[bytes] and IO[str] are the handles open() already returns. Any other parameter stays fenced."""
|
|
927
|
+
|
|
928
|
+
if isinstance(node, ast.Name) and node.id == "bytes":
|
|
929
|
+
return "BytesIO"
|
|
930
|
+
if isinstance(node, ast.Name) and node.id == "str":
|
|
931
|
+
return "TextIO"
|
|
932
|
+
return None
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _assigned_fields(body: list[ast.stmt]) -> set[str]:
|
|
936
|
+
found: set[str] = set()
|
|
937
|
+
for stmt in body:
|
|
938
|
+
target = None
|
|
939
|
+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
|
|
940
|
+
target = stmt.targets[0]
|
|
941
|
+
elif isinstance(stmt, ast.AugAssign):
|
|
942
|
+
target = stmt.target
|
|
943
|
+
elif isinstance(stmt, ast.AnnAssign):
|
|
944
|
+
target = stmt.target
|
|
945
|
+
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == "self":
|
|
946
|
+
found.add(target.attr)
|
|
947
|
+
for child in ast.iter_child_nodes(stmt):
|
|
948
|
+
if isinstance(child, ast.stmt):
|
|
949
|
+
found |= _assigned_fields([child])
|
|
950
|
+
return found
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _const_int_index(node: ast.expr) -> int | None:
|
|
954
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
|
|
955
|
+
return node.value
|
|
956
|
+
if (
|
|
957
|
+
isinstance(node, ast.UnaryOp)
|
|
958
|
+
and isinstance(node.op, ast.USub)
|
|
959
|
+
and isinstance(node.operand, ast.Constant)
|
|
960
|
+
and isinstance(node.operand.value, int)
|
|
961
|
+
and not isinstance(node.operand.value, bool)
|
|
962
|
+
):
|
|
963
|
+
return -node.operand.value
|
|
964
|
+
return None
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _type_slug(typ: dict[str, Any]) -> str:
|
|
968
|
+
raw = type_key(typ)
|
|
969
|
+
return "".join(ch if ch.isalnum() else "_" for ch in raw).strip("_")
|