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
pyscriptc/lower/_stmt.py
ADDED
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
"""Statement lowering for the lowerer.
|
|
2
|
+
|
|
3
|
+
Split out of `__init__.py` as E0 move 3. `Lowerer` composes this mixin in.
|
|
4
|
+
The area owns the `_stmt` dispatch and its statement forms: assignments
|
|
5
|
+
(plain, annotated, augmented, global slots), `if`/`while`/`for` (range,
|
|
6
|
+
repeat, chain, enumerate, zip, list), `break`/`continue` plumbing,
|
|
7
|
+
`return`/`raise`/`assert`/`del`, `try` handlers, `with` (suppress,
|
|
8
|
+
nullcontext, `open`, `importlib.resources.as_file`), and `match` on
|
|
9
|
+
literal subjects.
|
|
10
|
+
|
|
11
|
+
Loop-flag plumbing and close injection are statement-shaped and moved with
|
|
12
|
+
the area. Small leaf helpers shared across the package (tables, `_assign`,
|
|
13
|
+
scalar constructors) stay in `_core.py`; the boundary calls in both
|
|
14
|
+
directions stay `self._method()` so the move is behavior-identical.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from pyscriptc.builtins import EXCEPTIONS
|
|
23
|
+
from pyscriptc.errors import span_of
|
|
24
|
+
from pyscriptc.ir import types_equal
|
|
25
|
+
from pyscriptc.lower._core import (
|
|
26
|
+
_SCALAR,
|
|
27
|
+
_assign,
|
|
28
|
+
_binary_int,
|
|
29
|
+
_compare,
|
|
30
|
+
_const_bool,
|
|
31
|
+
_const_int,
|
|
32
|
+
_const_none,
|
|
33
|
+
_len_call,
|
|
34
|
+
_name,
|
|
35
|
+
_show,
|
|
36
|
+
_tuple_assign,
|
|
37
|
+
_zero,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from pyscriptc.lower import Lowerer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StmtMixin:
|
|
45
|
+
def _loop_flag(self, body: list[ast.stmt]) -> str | None:
|
|
46
|
+
if not any(_stmt_breaks_this_loop(stmt) for stmt in body):
|
|
47
|
+
return None
|
|
48
|
+
flag = self._temp("brk")
|
|
49
|
+
self.locals[flag] = {"kind": "bool"}
|
|
50
|
+
return flag
|
|
51
|
+
|
|
52
|
+
def _continue_loop_flag(self, body: list[ast.stmt]) -> str | None:
|
|
53
|
+
if not any(_stmt_continues_this_loop(stmt) for stmt in body):
|
|
54
|
+
return None
|
|
55
|
+
flag = self._temp("cont")
|
|
56
|
+
self.locals[flag] = {"kind": "bool"}
|
|
57
|
+
return flag
|
|
58
|
+
|
|
59
|
+
def _continue_reset(self, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
60
|
+
if self.continue_flag is None:
|
|
61
|
+
return []
|
|
62
|
+
return [_assign(self.continue_flag, _const_bool(False, span), span)]
|
|
63
|
+
|
|
64
|
+
def _skip_rest(self, stmts: list[dict[str, Any]], span: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
"""Skip the rest of this loop body after `break` or `continue`.
|
|
66
|
+
|
|
67
|
+
`break` and `continue` each have a flag. The for-loop step stays outside
|
|
68
|
+
this guard, so `continue` still advances and `break` does not.
|
|
69
|
+
"""
|
|
70
|
+
guarded = stmts
|
|
71
|
+
if self.continue_flag is not None:
|
|
72
|
+
guarded = [
|
|
73
|
+
{"kind": "if", "cond": _not_bool(self.continue_flag, span), "then": guarded, "else": [], "span": span}
|
|
74
|
+
]
|
|
75
|
+
if self.break_flag is not None:
|
|
76
|
+
guarded = [
|
|
77
|
+
{"kind": "if", "cond": _not_bool(self.break_flag, span), "then": guarded, "else": [], "span": span}
|
|
78
|
+
]
|
|
79
|
+
return guarded[0]
|
|
80
|
+
|
|
81
|
+
def _break_init(self, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
82
|
+
if self.break_flag is None:
|
|
83
|
+
return []
|
|
84
|
+
return [_assign(self.break_flag, _const_bool(False, span), span)]
|
|
85
|
+
|
|
86
|
+
def _skip_on_break(self, stmts: list[dict[str, Any]], span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
87
|
+
if self.break_flag is None:
|
|
88
|
+
return stmts
|
|
89
|
+
return [{"kind": "if", "cond": _not_bool(self.break_flag, span), "then": stmts, "else": [], "span": span}]
|
|
90
|
+
|
|
91
|
+
def _user_while(
|
|
92
|
+
self,
|
|
93
|
+
cond: dict[str, Any],
|
|
94
|
+
body: list[dict[str, Any]],
|
|
95
|
+
span: dict[str, Any],
|
|
96
|
+
*,
|
|
97
|
+
init: bool = True,
|
|
98
|
+
cond_pre: list[dict[str, Any]] | None = None,
|
|
99
|
+
) -> list[dict[str, Any]]:
|
|
100
|
+
"""`while cond: body`, and `break` sets a flag the existing while already tests.
|
|
101
|
+
|
|
102
|
+
The flag starts false. The loop runs while it is false and `cond` is
|
|
103
|
+
true. `break` sets the flag and the rest of that body is an `if`.
|
|
104
|
+
A false `cond` sets the flag too, so the condition is not tested again.
|
|
105
|
+
Statements that compute `cond` run at the start of each iteration.
|
|
106
|
+
"""
|
|
107
|
+
flag = self.break_flag
|
|
108
|
+
pre = cond_pre or []
|
|
109
|
+
if flag is None and not pre:
|
|
110
|
+
return [{"kind": "while", "cond": cond, "body": body, "span": span}]
|
|
111
|
+
own_flag = False
|
|
112
|
+
if flag is None:
|
|
113
|
+
flag = self._temp("whl")
|
|
114
|
+
self.locals[flag] = {"kind": "bool"}
|
|
115
|
+
own_flag = True
|
|
116
|
+
loop = {
|
|
117
|
+
"kind": "while",
|
|
118
|
+
"cond": _not_bool(flag, span),
|
|
119
|
+
"body": [
|
|
120
|
+
*pre,
|
|
121
|
+
{
|
|
122
|
+
"kind": "if",
|
|
123
|
+
"cond": cond,
|
|
124
|
+
"then": body,
|
|
125
|
+
"else": [_assign(flag, _const_bool(True, span), span)],
|
|
126
|
+
"span": span,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
"span": span,
|
|
130
|
+
}
|
|
131
|
+
if not init and not own_flag:
|
|
132
|
+
return [loop]
|
|
133
|
+
return [_assign(flag, _const_bool(False, span), span), loop]
|
|
134
|
+
|
|
135
|
+
def _stmt(self, stmt: ast.stmt) -> list[dict[str, Any]]:
|
|
136
|
+
saved = self.prelude
|
|
137
|
+
self.prelude = []
|
|
138
|
+
try:
|
|
139
|
+
produced = self._stmt_core(stmt)
|
|
140
|
+
finally:
|
|
141
|
+
pre = self.prelude
|
|
142
|
+
self.prelude = saved
|
|
143
|
+
return [*pre, *produced]
|
|
144
|
+
|
|
145
|
+
def _stmt_core(self, stmt: ast.stmt) -> list[dict[str, Any]]:
|
|
146
|
+
span = span_of(stmt, self.filename)
|
|
147
|
+
if isinstance(stmt, ast.Pass):
|
|
148
|
+
return [{"kind": "pass", "span": span}]
|
|
149
|
+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Attribute):
|
|
150
|
+
return [self._setattr(stmt, span)]
|
|
151
|
+
if _is_setitem(stmt):
|
|
152
|
+
return [self._setitem(stmt, span)]
|
|
153
|
+
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
|
|
154
|
+
typ = self._annotation(stmt.annotation)
|
|
155
|
+
if stmt.value is None:
|
|
156
|
+
self.fail(stmt, "PS1001", f"{stmt.target.id} needs an initial value")
|
|
157
|
+
value = self._expr(stmt.value, typ)
|
|
158
|
+
self._expect(stmt, value["type"], typ, stmt.target.id)
|
|
159
|
+
self.locals[stmt.target.id] = typ
|
|
160
|
+
self._note_frozen(stmt.target.id, stmt.value)
|
|
161
|
+
return [_assign(stmt.target.id, value, span)]
|
|
162
|
+
if isinstance(stmt, ast.Global):
|
|
163
|
+
for name in stmt.names:
|
|
164
|
+
slot = self.global_slots.get(name)
|
|
165
|
+
if slot is None or not slot.get("active"):
|
|
166
|
+
self.fail(stmt, "PS2002", f"global {name} is not a module slot")
|
|
167
|
+
return [{"kind": "pass", "span": span}]
|
|
168
|
+
if isinstance(stmt, ast.Assign):
|
|
169
|
+
if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
|
|
170
|
+
self.fail(stmt, "PS2002", "only a single name can be assigned")
|
|
171
|
+
target = stmt.targets[0].id
|
|
172
|
+
if target in self.fn_globals:
|
|
173
|
+
return self._assign_global(stmt, target, span)
|
|
174
|
+
if target not in self.locals:
|
|
175
|
+
self.fail(stmt, "PS1001", f"{target} needs an annotation before use")
|
|
176
|
+
value = self._expr(stmt.value, self.locals[target])
|
|
177
|
+
self._expect(stmt, value["type"], self.locals[target], target)
|
|
178
|
+
self._note_frozen(target, stmt.value)
|
|
179
|
+
return [_assign(target, value, span)]
|
|
180
|
+
if isinstance(stmt, ast.AugAssign):
|
|
181
|
+
return self._aug_assign(stmt, span)
|
|
182
|
+
if isinstance(stmt, ast.If):
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
"kind": "if",
|
|
186
|
+
"cond": self._bool_cond(stmt.test),
|
|
187
|
+
"then": self._body(list(stmt.body)),
|
|
188
|
+
"else": self._body(list(stmt.orelse)),
|
|
189
|
+
"span": span,
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
if isinstance(stmt, ast.Assert):
|
|
193
|
+
return self._assert(stmt, span)
|
|
194
|
+
if isinstance(stmt, ast.While):
|
|
195
|
+
if stmt.orelse:
|
|
196
|
+
self.fail(stmt, "PS2002", "while-else is not lowered")
|
|
197
|
+
saved_break = self.break_flag
|
|
198
|
+
saved_cont = self.continue_flag
|
|
199
|
+
self.break_flag = self._loop_flag(list(stmt.body))
|
|
200
|
+
self.continue_flag = self._continue_loop_flag(list(stmt.body))
|
|
201
|
+
try:
|
|
202
|
+
saved_prelude = self.prelude
|
|
203
|
+
self.prelude = []
|
|
204
|
+
try:
|
|
205
|
+
cond = self._bool_cond(stmt.test)
|
|
206
|
+
cond_pre = self.prelude
|
|
207
|
+
finally:
|
|
208
|
+
self.prelude = saved_prelude
|
|
209
|
+
body = [*self._continue_reset(span), *self._body(list(stmt.body))]
|
|
210
|
+
return self._user_while(cond, body, span, cond_pre=cond_pre)
|
|
211
|
+
finally:
|
|
212
|
+
self.break_flag = saved_break
|
|
213
|
+
self.continue_flag = saved_cont
|
|
214
|
+
if isinstance(stmt, ast.Break):
|
|
215
|
+
if self.break_flag is None:
|
|
216
|
+
self.fail(stmt, "PS2002", "break outside a loop is not lowered")
|
|
217
|
+
return [_assign(self.break_flag, _const_bool(True, span), span)]
|
|
218
|
+
if isinstance(stmt, ast.Continue):
|
|
219
|
+
if self.continue_flag is None:
|
|
220
|
+
self.fail(stmt, "PS2002", "continue outside a loop is not lowered")
|
|
221
|
+
return [_assign(self.continue_flag, _const_bool(True, span), span)]
|
|
222
|
+
if isinstance(stmt, ast.For):
|
|
223
|
+
return self._for(stmt, span)
|
|
224
|
+
if isinstance(stmt, ast.Return):
|
|
225
|
+
_params, ret = self.sigs[self.function or ""] if self.function in self.sigs else self._current_method()
|
|
226
|
+
if stmt.value is None or (isinstance(stmt.value, ast.Constant) and stmt.value.value is None):
|
|
227
|
+
if ret["kind"] != "none":
|
|
228
|
+
self.fail(stmt, "PS1003", "bare return is None, and this function does not return None")
|
|
229
|
+
return [{"kind": "return", "value": _const_none(span), "span": span}]
|
|
230
|
+
value = self._expr(stmt.value, ret)
|
|
231
|
+
self._expect(stmt, value["type"], ret, "return")
|
|
232
|
+
return [{"kind": "return", "value": value, "span": span}]
|
|
233
|
+
if isinstance(stmt, ast.Expr):
|
|
234
|
+
return [{"kind": "expr", "expr": self._expr(stmt.value), "span": span}]
|
|
235
|
+
if isinstance(stmt, ast.Raise):
|
|
236
|
+
return [self._raise(stmt, span)]
|
|
237
|
+
if isinstance(stmt, ast.Try):
|
|
238
|
+
return [self._try(stmt, span)]
|
|
239
|
+
if isinstance(stmt, ast.With):
|
|
240
|
+
return self._with(stmt, span)
|
|
241
|
+
if isinstance(stmt, ast.Match):
|
|
242
|
+
return self._match(stmt, span)
|
|
243
|
+
if isinstance(stmt, ast.Delete):
|
|
244
|
+
return self._delete(stmt, span)
|
|
245
|
+
self.fail(stmt, "PS2002", f"{type(stmt).__name__} is not lowered")
|
|
246
|
+
raise AssertionError("unreachable")
|
|
247
|
+
|
|
248
|
+
def _current_method(self) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
249
|
+
if self.function and "." in self.function:
|
|
250
|
+
class_name, method = self.function.split(".", 1)
|
|
251
|
+
return self.classes[class_name]["methods"][method]
|
|
252
|
+
self.fail(ast.Pass(), "PS2002", "return is outside a function")
|
|
253
|
+
raise AssertionError("unreachable")
|
|
254
|
+
|
|
255
|
+
def _match(self, stmt: ast.Match, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
256
|
+
"""`match subject: case literal:` is `if subject == literal`.
|
|
257
|
+
|
|
258
|
+
The subject is stored once. Each later literal case sits in the else of
|
|
259
|
+
the previous one, so a match does not test the rest. Any other pattern
|
|
260
|
+
stays fenced.
|
|
261
|
+
"""
|
|
262
|
+
subject_expr = self._expr(stmt.subject)
|
|
263
|
+
temp = self._temp("subj")
|
|
264
|
+
self.locals[temp] = subject_expr["type"]
|
|
265
|
+
self.prelude.append(_assign(temp, subject_expr, span))
|
|
266
|
+
chain: list[dict[str, Any]] = []
|
|
267
|
+
for case in reversed(stmt.cases):
|
|
268
|
+
cond = self._match_case(case, temp, span)
|
|
269
|
+
chain = [
|
|
270
|
+
{
|
|
271
|
+
"kind": "if",
|
|
272
|
+
"cond": cond,
|
|
273
|
+
"then": self._body(list(case.body)),
|
|
274
|
+
"else": chain,
|
|
275
|
+
"span": span,
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
return chain
|
|
279
|
+
|
|
280
|
+
def _match_case(self, case: ast.match_case, subject: str, span: dict[str, Any]) -> dict[str, Any]:
|
|
281
|
+
if case.guard is not None:
|
|
282
|
+
self.fail(case.guard, "PS2002", "a match guard is not lowered")
|
|
283
|
+
pat = case.pattern
|
|
284
|
+
if isinstance(pat, ast.MatchValue) and isinstance(pat.value, ast.Constant):
|
|
285
|
+
left = ast.Name(id=subject, ctx=ast.Load())
|
|
286
|
+
ast.copy_location(left, pat)
|
|
287
|
+
compare = ast.Compare(left=left, ops=[ast.Eq()], comparators=[pat.value])
|
|
288
|
+
ast.copy_location(compare, pat)
|
|
289
|
+
return self._compare(compare, span)
|
|
290
|
+
self._refuse_pattern(pat)
|
|
291
|
+
raise AssertionError("unreachable")
|
|
292
|
+
|
|
293
|
+
def _refuse_pattern(self, pat: ast.pattern) -> None:
|
|
294
|
+
if isinstance(pat, ast.MatchAs):
|
|
295
|
+
if pat.name is None:
|
|
296
|
+
self.fail(pat, "PS2002", "a wildcard pattern is not lowered")
|
|
297
|
+
self.fail(pat, "PS2002", "a capture pattern is not lowered")
|
|
298
|
+
if isinstance(pat, ast.MatchOr):
|
|
299
|
+
self.fail(pat, "PS2002", "an or pattern is not lowered")
|
|
300
|
+
if isinstance(pat, ast.MatchSequence) or isinstance(pat, ast.MatchStar):
|
|
301
|
+
self.fail(pat, "PS2002", "a sequence pattern is not lowered")
|
|
302
|
+
if isinstance(pat, ast.MatchClass):
|
|
303
|
+
self.fail(pat, "PS2002", "a class pattern is not lowered")
|
|
304
|
+
if isinstance(pat, ast.MatchMapping):
|
|
305
|
+
self.fail(pat, "PS2002", "a mapping pattern is not lowered")
|
|
306
|
+
if isinstance(pat, ast.MatchSingleton):
|
|
307
|
+
self.fail(pat, "PS2002", "a singleton pattern is not lowered")
|
|
308
|
+
if isinstance(pat, ast.MatchValue):
|
|
309
|
+
self.fail(pat, "PS2002", "a match value that is not a literal is not lowered")
|
|
310
|
+
self.fail(pat, "PS2002", f"{type(pat).__name__} is not lowered")
|
|
311
|
+
|
|
312
|
+
def _delete(self, stmt: ast.Delete, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
313
|
+
"""`del` of one unread local is dropped.
|
|
314
|
+
|
|
315
|
+
The name stays in the local table so the earlier assignment still
|
|
316
|
+
type-checks, and nothing reads it. A later load stays fenced: this
|
|
317
|
+
compiler cannot raise UnboundLocalError, and an empty `for` target
|
|
318
|
+
already reads as 0. Other targets stay fenced.
|
|
319
|
+
"""
|
|
320
|
+
if len(stmt.targets) != 1:
|
|
321
|
+
self.fail(stmt, "PS2002", "del of more than one name is not lowered")
|
|
322
|
+
target = stmt.targets[0]
|
|
323
|
+
if isinstance(target, ast.Attribute):
|
|
324
|
+
self.fail(stmt, "PS2002", "del of an attribute is not lowered")
|
|
325
|
+
if isinstance(target, ast.Subscript):
|
|
326
|
+
if isinstance(target.slice, ast.Slice):
|
|
327
|
+
self.fail(stmt, "PS2002", "del of a slice is not lowered")
|
|
328
|
+
# del d[k] lowers to ps_map_del for a declared dict local; every
|
|
329
|
+
# other subscript target keeps the subscript fence.
|
|
330
|
+
if isinstance(target.value, ast.Name) and self.locals.get(target.value.id, {}).get("kind") == "dict":
|
|
331
|
+
container = self._expr(target.value)
|
|
332
|
+
index = self._expr(target.slice)
|
|
333
|
+
self._expect(stmt, index["type"], container["type"]["key"], "dict key")
|
|
334
|
+
call = self._abi_call(
|
|
335
|
+
"builtins",
|
|
336
|
+
"dict.delitem",
|
|
337
|
+
[container, index],
|
|
338
|
+
{"kind": "none"},
|
|
339
|
+
"ps_map_del",
|
|
340
|
+
span,
|
|
341
|
+
)
|
|
342
|
+
return [{"kind": "expr", "expr": call, "span": span}]
|
|
343
|
+
self.fail(stmt, "PS2002", "del of a subscript is not lowered")
|
|
344
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
345
|
+
self.fail(stmt, "PS2002", "del of a tuple of names is not lowered")
|
|
346
|
+
if not isinstance(target, ast.Name):
|
|
347
|
+
self.fail(stmt, "PS2002", "del of this target is not lowered")
|
|
348
|
+
name = target.id
|
|
349
|
+
if name in self.fn_globals:
|
|
350
|
+
self.fail(stmt, "PS2002", "del of a global is not lowered")
|
|
351
|
+
if name not in self.locals:
|
|
352
|
+
self.fail(stmt, "PS2002", "del of an unbound name is not lowered")
|
|
353
|
+
if self._name_read_after(stmt, name):
|
|
354
|
+
self.fail(stmt, "PS2002", "del of a name that is read later is not lowered")
|
|
355
|
+
return [{"kind": "pass", "span": span}]
|
|
356
|
+
|
|
357
|
+
def _name_read_after(self, delete: ast.Delete, name: str) -> bool:
|
|
358
|
+
state = {"seen": False, "hit": False}
|
|
359
|
+
|
|
360
|
+
def visit(node: ast.AST) -> None:
|
|
361
|
+
if state["hit"]:
|
|
362
|
+
return
|
|
363
|
+
if node is delete:
|
|
364
|
+
state["seen"] = True
|
|
365
|
+
return
|
|
366
|
+
if state["seen"] and isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id == name:
|
|
367
|
+
state["hit"] = True
|
|
368
|
+
return
|
|
369
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
370
|
+
return
|
|
371
|
+
for child in ast.iter_child_nodes(node):
|
|
372
|
+
visit(child)
|
|
373
|
+
|
|
374
|
+
for stmt in self.fn_body:
|
|
375
|
+
visit(stmt)
|
|
376
|
+
return (not state["seen"]) or state["hit"]
|
|
377
|
+
|
|
378
|
+
def _aug_assign(self, stmt: ast.AugAssign, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
379
|
+
if not isinstance(stmt.target, ast.Name):
|
|
380
|
+
self.fail(stmt, "PS2002", "augmented assignment is only lowered for a name")
|
|
381
|
+
target = stmt.target.id
|
|
382
|
+
if target not in self.locals:
|
|
383
|
+
self.fail(stmt, "PS1001", f"{target} needs an annotation before use")
|
|
384
|
+
left = ast.Name(id=target, ctx=ast.Load())
|
|
385
|
+
ast.copy_location(left, stmt)
|
|
386
|
+
node = ast.BinOp(left=left, op=stmt.op, right=stmt.value)
|
|
387
|
+
ast.copy_location(node, stmt)
|
|
388
|
+
value = self._expr(node)
|
|
389
|
+
self._expect(stmt, value["type"], self.locals[target], target)
|
|
390
|
+
return [_assign(target, value, span)]
|
|
391
|
+
|
|
392
|
+
def _for(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
393
|
+
if stmt.orelse:
|
|
394
|
+
self.fail(stmt, "PS2002", "for-else is not lowered")
|
|
395
|
+
if not isinstance(stmt.target, ast.Name):
|
|
396
|
+
self.fail(stmt, "PS2002", "for targets a single name")
|
|
397
|
+
saved_break = self.break_flag
|
|
398
|
+
saved_cont = self.continue_flag
|
|
399
|
+
self.break_flag = self._loop_flag(list(stmt.body))
|
|
400
|
+
self.continue_flag = self._continue_loop_flag(list(stmt.body))
|
|
401
|
+
try:
|
|
402
|
+
return self._for_dispatch(stmt, span)
|
|
403
|
+
finally:
|
|
404
|
+
self.break_flag = saved_break
|
|
405
|
+
self.continue_flag = saved_cont
|
|
406
|
+
|
|
407
|
+
def _for_dispatch(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
408
|
+
if self._is_mod(stmt.iter, "itertools", "repeat"):
|
|
409
|
+
return self._for_repeat(stmt, span)
|
|
410
|
+
if self._is_mod(stmt.iter, "itertools", "chain"):
|
|
411
|
+
return self._for_chain(stmt, span)
|
|
412
|
+
if _is_range_call(stmt.iter):
|
|
413
|
+
return self._for_range(stmt, span)
|
|
414
|
+
if _is_call_name(stmt.iter, "enumerate"):
|
|
415
|
+
return self._for_enumerate(stmt, span)
|
|
416
|
+
if _is_call_name(stmt.iter, "zip"):
|
|
417
|
+
return self._for_zip(stmt, span)
|
|
418
|
+
iter_node: ast.AST = stmt.iter
|
|
419
|
+
if isinstance(iter_node, ast.Tuple):
|
|
420
|
+
if not iter_node.elts:
|
|
421
|
+
return []
|
|
422
|
+
# A tuple literal iterates its elements in order; the homogeneous
|
|
423
|
+
# case is the list literal this loop already lowers.
|
|
424
|
+
list_iter = ast.List(elts=iter_node.elts, ctx=ast.Load())
|
|
425
|
+
ast.copy_location(list_iter, iter_node)
|
|
426
|
+
iter_node = list_iter
|
|
427
|
+
iterable = self._expr(iter_node)
|
|
428
|
+
if iterable["type"]["kind"] != "list":
|
|
429
|
+
self.fail(stmt.iter, "PS2002", "for iterates over a list or range")
|
|
430
|
+
elem = iterable["type"]["elem"]
|
|
431
|
+
if elem["kind"] not in _SCALAR and not (
|
|
432
|
+
elem["kind"] == "class" and self.classes.get(elem["name"], {}).get("tuple")
|
|
433
|
+
):
|
|
434
|
+
self.fail(stmt, "PS2002", f"for over list[{_show(elem)}] is not lowered")
|
|
435
|
+
target = stmt.target.id
|
|
436
|
+
fresh = self._bind_loop_target(target, elem, stmt.target)
|
|
437
|
+
seq = self._temp("seq")
|
|
438
|
+
idx = self._temp("idx")
|
|
439
|
+
length = self._temp("len")
|
|
440
|
+
self.locals[seq] = iterable["type"]
|
|
441
|
+
self.locals[idx] = {"kind": "int"}
|
|
442
|
+
self.locals[length] = {"kind": "int"}
|
|
443
|
+
int_t = {"kind": "int"}
|
|
444
|
+
body = self._loop_body(stmt)
|
|
445
|
+
step = [
|
|
446
|
+
_assign(
|
|
447
|
+
target,
|
|
448
|
+
{
|
|
449
|
+
"kind": "subscript",
|
|
450
|
+
"value": _name(seq, iterable["type"], span),
|
|
451
|
+
"index": _name(idx, int_t, span),
|
|
452
|
+
"type": elem,
|
|
453
|
+
"span": span,
|
|
454
|
+
},
|
|
455
|
+
span,
|
|
456
|
+
),
|
|
457
|
+
*body,
|
|
458
|
+
*self._skip_on_break(
|
|
459
|
+
[_assign(idx, _binary_int("+", _name(idx, int_t, span), _const_int(1, span), span), span)],
|
|
460
|
+
span,
|
|
461
|
+
),
|
|
462
|
+
]
|
|
463
|
+
prefix = []
|
|
464
|
+
if fresh:
|
|
465
|
+
prefix.append(_assign(target, _zero(elem, span), span))
|
|
466
|
+
prefix.extend(
|
|
467
|
+
[
|
|
468
|
+
_assign(seq, iterable, span),
|
|
469
|
+
_assign(
|
|
470
|
+
length,
|
|
471
|
+
{
|
|
472
|
+
"kind": "call",
|
|
473
|
+
"callee": {"kind": "builtin", "name": "len"},
|
|
474
|
+
"args": [_name(seq, iterable["type"], span)],
|
|
475
|
+
"type": int_t,
|
|
476
|
+
"span": span,
|
|
477
|
+
},
|
|
478
|
+
span,
|
|
479
|
+
),
|
|
480
|
+
_assign(idx, _const_int(0, span), span),
|
|
481
|
+
*self._user_while(
|
|
482
|
+
_compare("<", _name(idx, int_t, span), _name(length, int_t, span), span),
|
|
483
|
+
step,
|
|
484
|
+
span,
|
|
485
|
+
),
|
|
486
|
+
]
|
|
487
|
+
)
|
|
488
|
+
return prefix
|
|
489
|
+
|
|
490
|
+
def _for_range(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
491
|
+
call = stmt.iter
|
|
492
|
+
assert isinstance(call, ast.Call)
|
|
493
|
+
if call.keywords:
|
|
494
|
+
self.fail(call, "PS2003", "keyword arguments are not lowered")
|
|
495
|
+
args = [self._expr(arg, {"kind": "int"}) for arg in call.args]
|
|
496
|
+
for arg in args:
|
|
497
|
+
if arg["type"]["kind"] != "int":
|
|
498
|
+
self.fail(call, "PS1003", "range arguments are int")
|
|
499
|
+
if len(args) == 1:
|
|
500
|
+
start, stop, step = _const_int(0, span), args[0], _const_int(1, span)
|
|
501
|
+
elif len(args) == 2:
|
|
502
|
+
start, stop, step = args[0], args[1], _const_int(1, span)
|
|
503
|
+
elif len(args) == 3:
|
|
504
|
+
start, stop, step = args
|
|
505
|
+
else:
|
|
506
|
+
self.fail(call, "PS2003", "range takes 1, 2, or 3 arguments")
|
|
507
|
+
target = stmt.target.id
|
|
508
|
+
assert isinstance(stmt.target, ast.Name)
|
|
509
|
+
fresh = self._bind_loop_target(target, {"kind": "int"}, stmt.target)
|
|
510
|
+
cur = self._temp("cur")
|
|
511
|
+
stop_n = self._temp("stop")
|
|
512
|
+
step_n = self._temp("step")
|
|
513
|
+
int_t = {"kind": "int"}
|
|
514
|
+
self.locals[cur] = int_t
|
|
515
|
+
self.locals[stop_n] = int_t
|
|
516
|
+
self.locals[step_n] = int_t
|
|
517
|
+
body = self._loop_body(stmt)
|
|
518
|
+
advance = self._skip_on_break(
|
|
519
|
+
[_assign(cur, _binary_int("+", _name(cur, int_t, span), _name(step_n, int_t, span), span), span)],
|
|
520
|
+
span,
|
|
521
|
+
)
|
|
522
|
+
up = [_assign(target, _name(cur, int_t, span), span), *body, *advance]
|
|
523
|
+
down = [_assign(target, _name(cur, int_t, span), span), *body, *advance]
|
|
524
|
+
prefix = []
|
|
525
|
+
if fresh:
|
|
526
|
+
prefix.append(_assign(target, _const_int(0, span), span))
|
|
527
|
+
prefix.extend(
|
|
528
|
+
[
|
|
529
|
+
*self._break_init(span),
|
|
530
|
+
_assign(cur, start, span),
|
|
531
|
+
_assign(stop_n, stop, span),
|
|
532
|
+
_assign(step_n, step, span),
|
|
533
|
+
{
|
|
534
|
+
"kind": "if",
|
|
535
|
+
"cond": _compare("==", _name(step_n, int_t, span), _const_int(0, span), span),
|
|
536
|
+
"then": [{"kind": "raise", "exc": "ValueError", "message": None, "span": span}],
|
|
537
|
+
"else": [],
|
|
538
|
+
"span": span,
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"kind": "if",
|
|
542
|
+
"cond": _compare(">", _name(step_n, int_t, span), _const_int(0, span), span),
|
|
543
|
+
"then": self._user_while(
|
|
544
|
+
_compare("<", _name(cur, int_t, span), _name(stop_n, int_t, span), span),
|
|
545
|
+
up,
|
|
546
|
+
span,
|
|
547
|
+
init=False,
|
|
548
|
+
),
|
|
549
|
+
"else": self._user_while(
|
|
550
|
+
_compare(">", _name(cur, int_t, span), _name(stop_n, int_t, span), span),
|
|
551
|
+
down,
|
|
552
|
+
span,
|
|
553
|
+
init=False,
|
|
554
|
+
),
|
|
555
|
+
"span": span,
|
|
556
|
+
},
|
|
557
|
+
]
|
|
558
|
+
)
|
|
559
|
+
return prefix
|
|
560
|
+
|
|
561
|
+
def _bind_loop_target(self, name: str, typ: dict[str, Any], node: ast.AST) -> bool:
|
|
562
|
+
if name in self.bindings or name in self.classes:
|
|
563
|
+
self.fail(node, "PS2002", f"{name} is already bound and cannot be a for target")
|
|
564
|
+
if name not in self.locals:
|
|
565
|
+
self.locals[name] = typ
|
|
566
|
+
return True
|
|
567
|
+
if not types_equal(self.locals[name], typ):
|
|
568
|
+
self.fail(node, "PS1003", f"{name} is {_show(self.locals[name])}, and the for target is {_show(typ)}")
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
def _assert(self, stmt: ast.Assert, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
572
|
+
"""`assert cond` is `if not cond: raise Exception`, and a message sits on that raise.
|
|
573
|
+
|
|
574
|
+
The condition is whatever `if` already accepts. AssertionError is not a
|
|
575
|
+
lowered exception, so the failure is the existing `Exception` raise.
|
|
576
|
+
The message is a `str`, same as `raise`, and it is lowered only in the
|
|
577
|
+
failing branch.
|
|
578
|
+
"""
|
|
579
|
+
cond = self._bool_cond(stmt.test)
|
|
580
|
+
then: list[dict[str, Any]] = []
|
|
581
|
+
message: dict[str, Any] | None = None
|
|
582
|
+
if stmt.msg is not None:
|
|
583
|
+
saved = self.prelude
|
|
584
|
+
self.prelude = []
|
|
585
|
+
try:
|
|
586
|
+
message = self._expr(stmt.msg, {"kind": "str"})
|
|
587
|
+
if message["type"]["kind"] != "str":
|
|
588
|
+
self.fail(stmt.msg, "PS1003", "an exception message is a str")
|
|
589
|
+
then = self.prelude
|
|
590
|
+
finally:
|
|
591
|
+
self.prelude = saved
|
|
592
|
+
then.append({"kind": "raise", "exc": "Exception", "message": message, "span": span})
|
|
593
|
+
negated = {"kind": "unary", "op": "not", "operand": cond, "type": {"kind": "bool"}, "span": span}
|
|
594
|
+
return [{"kind": "if", "cond": negated, "then": then, "else": [], "span": span}]
|
|
595
|
+
|
|
596
|
+
def _raise(self, stmt: ast.Raise, span: dict[str, Any]) -> dict[str, Any]:
|
|
597
|
+
if stmt.cause is not None:
|
|
598
|
+
self.fail(stmt, "PS2002", "raise-from is not lowered")
|
|
599
|
+
if stmt.exc is None:
|
|
600
|
+
self.fail(stmt, "PS2002", "bare raise is not lowered")
|
|
601
|
+
exc: ast.expr = stmt.exc
|
|
602
|
+
message: dict[str, Any] | None = None
|
|
603
|
+
if isinstance(exc, ast.Call):
|
|
604
|
+
if exc.keywords:
|
|
605
|
+
self.fail(exc, "PS2003", "keyword arguments are not lowered")
|
|
606
|
+
if len(exc.args) > 1:
|
|
607
|
+
self.fail(exc, "PS2003", "raise takes an exception name and an optional message")
|
|
608
|
+
if exc.args:
|
|
609
|
+
message = self._expr(exc.args[0], {"kind": "str"})
|
|
610
|
+
if message["type"]["kind"] != "str":
|
|
611
|
+
self.fail(exc, "PS1003", "an exception message is a str")
|
|
612
|
+
exc = exc.func
|
|
613
|
+
if not isinstance(exc, ast.Name):
|
|
614
|
+
self.fail(stmt, "PS2002", "raise names a lowered exception")
|
|
615
|
+
if exc.id in EXCEPTIONS:
|
|
616
|
+
return {"kind": "raise", "exc": exc.id, "message": message, "span": span}
|
|
617
|
+
resolved = self.class_aliases.get(exc.id, exc.id)
|
|
618
|
+
info = self.classes.get(resolved)
|
|
619
|
+
if info is not None and info.get("exception"):
|
|
620
|
+
# A user exception is a code plus an optional str. raise does not call __init__.
|
|
621
|
+
return {"kind": "raise", "exc": resolved, "code": info["exc_code"], "message": message, "span": span}
|
|
622
|
+
self.fail(stmt, "PS2002", "raise names a lowered exception")
|
|
623
|
+
raise AssertionError("unreachable")
|
|
624
|
+
|
|
625
|
+
def _try(self, stmt: ast.Try, span: dict[str, Any]) -> dict[str, Any]:
|
|
626
|
+
if stmt.orelse:
|
|
627
|
+
self.fail(stmt, "PS2002", "try-else is not lowered")
|
|
628
|
+
if stmt.finalbody:
|
|
629
|
+
self.fail(stmt, "PS2002", "finally is not lowered")
|
|
630
|
+
handlers = []
|
|
631
|
+
for handler in stmt.handlers:
|
|
632
|
+
if handler.name is not None:
|
|
633
|
+
self.fail(handler, "PS1205", "except-as is not lowered")
|
|
634
|
+
if handler.type is None:
|
|
635
|
+
self.fail(handler, "PS1205", "bare except is not lowered")
|
|
636
|
+
names = _handler_names(self, handler.type)
|
|
637
|
+
codes: list[int] = []
|
|
638
|
+
if "Exception" not in names:
|
|
639
|
+
for name in names:
|
|
640
|
+
codes.extend(self._exception_codes(name))
|
|
641
|
+
handlers.append({"exc": names, "codes": codes, "body": self._body(list(handler.body))})
|
|
642
|
+
return {"kind": "try", "body": self._body(list(stmt.body)), "handlers": handlers, "span": span}
|
|
643
|
+
|
|
644
|
+
def _with(self, stmt: ast.With, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
645
|
+
if len(stmt.items) != 1:
|
|
646
|
+
self.fail(stmt, "PS2002", "with lowers one contextlib item")
|
|
647
|
+
item = stmt.items[0]
|
|
648
|
+
ctx = item.context_expr
|
|
649
|
+
if isinstance(ctx, ast.Call) and isinstance(ctx.func, ast.Name) and ctx.func.id == "open":
|
|
650
|
+
return self._with_open(stmt, item, span)
|
|
651
|
+
if self._is_mod(ctx, "importlib.resources", "as_file"):
|
|
652
|
+
return self._with_as_file(stmt, item, span)
|
|
653
|
+
if item.optional_vars is not None:
|
|
654
|
+
self.fail(stmt, "PS1206", "with-as is not lowered")
|
|
655
|
+
if self._is_mod(ctx, "contextlib", "suppress"):
|
|
656
|
+
assert isinstance(ctx, ast.Call)
|
|
657
|
+
if len(ctx.args) != 1 or not isinstance(ctx.args[0], ast.Name) or ctx.args[0].id not in EXCEPTIONS:
|
|
658
|
+
self.fail(ctx, "PS2002", "suppress takes one lowered exception")
|
|
659
|
+
return [
|
|
660
|
+
{
|
|
661
|
+
"kind": "try",
|
|
662
|
+
"body": self._body(list(stmt.body)),
|
|
663
|
+
"handlers": [{"exc": [ctx.args[0].id], "body": []}],
|
|
664
|
+
"span": span,
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
if self._is_mod(ctx, "contextlib", "nullcontext"):
|
|
668
|
+
assert isinstance(ctx, ast.Call)
|
|
669
|
+
if ctx.args:
|
|
670
|
+
self.fail(ctx, "PS2003", "nullcontext takes no arguments")
|
|
671
|
+
return self._body(list(stmt.body))
|
|
672
|
+
self.fail(
|
|
673
|
+
stmt,
|
|
674
|
+
"PS2002",
|
|
675
|
+
"with is lowered for contextlib.suppress, contextlib.nullcontext, open, and importlib.resources.as_file",
|
|
676
|
+
)
|
|
677
|
+
raise AssertionError("unreachable")
|
|
678
|
+
|
|
679
|
+
def _for_repeat(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
680
|
+
call = stmt.iter
|
|
681
|
+
assert isinstance(call, ast.Call)
|
|
682
|
+
if len(call.args) != 2:
|
|
683
|
+
self.fail(call, "PS2003", "itertools.repeat takes a value and a count")
|
|
684
|
+
value = self._expr(call.args[0])
|
|
685
|
+
count = self._expr(call.args[1])
|
|
686
|
+
if count["type"]["kind"] != "int":
|
|
687
|
+
self.fail(call, "PS1003", "itertools.repeat count is int")
|
|
688
|
+
if value["type"]["kind"] not in _SCALAR:
|
|
689
|
+
self.fail(call, "PS2002", "itertools.repeat takes a scalar")
|
|
690
|
+
target = stmt.target.id
|
|
691
|
+
assert isinstance(stmt.target, ast.Name)
|
|
692
|
+
fresh = self._bind_loop_target(target, value["type"], stmt.target)
|
|
693
|
+
held = self._temp("rep")
|
|
694
|
+
idx = self._temp("idx")
|
|
695
|
+
stop = self._temp("stop")
|
|
696
|
+
int_t = {"kind": "int"}
|
|
697
|
+
self.locals[held] = value["type"]
|
|
698
|
+
self.locals[idx] = int_t
|
|
699
|
+
self.locals[stop] = int_t
|
|
700
|
+
body = self._loop_body(stmt)
|
|
701
|
+
prefix: list[dict[str, Any]] = []
|
|
702
|
+
if fresh:
|
|
703
|
+
prefix.append(_assign(target, _zero(value["type"], span), span))
|
|
704
|
+
prefix.extend(
|
|
705
|
+
[
|
|
706
|
+
_assign(held, value, span),
|
|
707
|
+
_assign(idx, _const_int(0, span), span),
|
|
708
|
+
_assign(stop, count, span),
|
|
709
|
+
*self._user_while(
|
|
710
|
+
_compare("<", _name(idx, int_t, span), _name(stop, int_t, span), span),
|
|
711
|
+
[
|
|
712
|
+
_assign(target, _name(held, value["type"], span), span),
|
|
713
|
+
*body,
|
|
714
|
+
*self._skip_on_break(
|
|
715
|
+
[_assign(idx, _binary_int("+", _name(idx, int_t, span), _const_int(1, span), span), span)],
|
|
716
|
+
span,
|
|
717
|
+
),
|
|
718
|
+
],
|
|
719
|
+
span,
|
|
720
|
+
),
|
|
721
|
+
]
|
|
722
|
+
)
|
|
723
|
+
return prefix
|
|
724
|
+
|
|
725
|
+
def _for_chain(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
726
|
+
call = stmt.iter
|
|
727
|
+
assert isinstance(call, ast.Call)
|
|
728
|
+
if not 2 <= len(call.args) <= 8:
|
|
729
|
+
self.fail(call, "PS2003", "itertools.chain takes 2 to 8 lists")
|
|
730
|
+
lists = [self._expr(arg) for arg in call.args]
|
|
731
|
+
if any(item["type"]["kind"] != "list" for item in lists):
|
|
732
|
+
self.fail(call, "PS2002", "itertools.chain takes lists")
|
|
733
|
+
elem = lists[0]["type"]["elem"]
|
|
734
|
+
if elem["kind"] not in _SCALAR:
|
|
735
|
+
self.fail(call, "PS2002", f"for over list[{_show(elem)}] is not lowered")
|
|
736
|
+
for item in lists[1:]:
|
|
737
|
+
if not types_equal(item["type"]["elem"], elem):
|
|
738
|
+
self.fail(call, "PS1003", "itertools.chain lists have one element type")
|
|
739
|
+
target = stmt.target.id
|
|
740
|
+
assert isinstance(stmt.target, ast.Name)
|
|
741
|
+
fresh = self._bind_loop_target(target, elem, stmt.target)
|
|
742
|
+
body = self._loop_body(stmt)
|
|
743
|
+
prefix: list[dict[str, Any]] = []
|
|
744
|
+
if fresh:
|
|
745
|
+
prefix.append(_assign(target, _zero(elem, span), span))
|
|
746
|
+
prefix.extend(self._break_init(span))
|
|
747
|
+
for item in lists:
|
|
748
|
+
prefix.extend(self._list_loop(target, item, body, span))
|
|
749
|
+
return prefix
|
|
750
|
+
|
|
751
|
+
def _list_loop(
|
|
752
|
+
self, target: str, iterable: dict[str, Any], body: list[dict[str, Any]], span: dict[str, Any]
|
|
753
|
+
) -> list[dict[str, Any]]:
|
|
754
|
+
elem = iterable["type"]["elem"]
|
|
755
|
+
seq = self._temp("seq")
|
|
756
|
+
idx = self._temp("idx")
|
|
757
|
+
length = self._temp("len")
|
|
758
|
+
int_t = {"kind": "int"}
|
|
759
|
+
self.locals[seq] = iterable["type"]
|
|
760
|
+
self.locals[idx] = int_t
|
|
761
|
+
self.locals[length] = int_t
|
|
762
|
+
return [
|
|
763
|
+
_assign(seq, iterable, span),
|
|
764
|
+
_assign(
|
|
765
|
+
length,
|
|
766
|
+
{
|
|
767
|
+
"kind": "call",
|
|
768
|
+
"callee": {"kind": "builtin", "name": "len"},
|
|
769
|
+
"args": [_name(seq, iterable["type"], span)],
|
|
770
|
+
"type": int_t,
|
|
771
|
+
"span": span,
|
|
772
|
+
},
|
|
773
|
+
span,
|
|
774
|
+
),
|
|
775
|
+
_assign(idx, _const_int(0, span), span),
|
|
776
|
+
*self._user_while(
|
|
777
|
+
_compare("<", _name(idx, int_t, span), _name(length, int_t, span), span),
|
|
778
|
+
[
|
|
779
|
+
_assign(
|
|
780
|
+
target,
|
|
781
|
+
{
|
|
782
|
+
"kind": "subscript",
|
|
783
|
+
"value": _name(seq, iterable["type"], span),
|
|
784
|
+
"index": _name(idx, int_t, span),
|
|
785
|
+
"type": elem,
|
|
786
|
+
"span": span,
|
|
787
|
+
},
|
|
788
|
+
span,
|
|
789
|
+
),
|
|
790
|
+
*body,
|
|
791
|
+
*self._skip_on_break(
|
|
792
|
+
[_assign(idx, _binary_int("+", _name(idx, int_t, span), _const_int(1, span), span), span)],
|
|
793
|
+
span,
|
|
794
|
+
),
|
|
795
|
+
],
|
|
796
|
+
span,
|
|
797
|
+
init=False,
|
|
798
|
+
),
|
|
799
|
+
]
|
|
800
|
+
|
|
801
|
+
def _loop_body(self, stmt: ast.For) -> list[dict[str, Any]]:
|
|
802
|
+
if self.comp_elt is None or self.comp_dest is None:
|
|
803
|
+
return [*self._continue_reset(span_of(stmt, self.filename)), *self._body(list(stmt.body))]
|
|
804
|
+
filt_pre: list[dict[str, Any]] = []
|
|
805
|
+
filt_cond: dict[str, Any] | None = None
|
|
806
|
+
if self.comp_if is not None:
|
|
807
|
+
saved = self.prelude
|
|
808
|
+
self.prelude = []
|
|
809
|
+
try:
|
|
810
|
+
filt_cond = self._bool_cond(self.comp_if)
|
|
811
|
+
filt_pre = self.prelude
|
|
812
|
+
finally:
|
|
813
|
+
self.prelude = saved
|
|
814
|
+
saved = self.prelude
|
|
815
|
+
self.prelude = []
|
|
816
|
+
try:
|
|
817
|
+
elt = self._expr(self.comp_elt)
|
|
818
|
+
inner = self.prelude
|
|
819
|
+
finally:
|
|
820
|
+
self.prelude = saved
|
|
821
|
+
elem = elt["type"]
|
|
822
|
+
if elem["kind"] == "bool" or elem["kind"] not in {"int", "float", "str", "bytes", "list", "dict", "class"}:
|
|
823
|
+
self.fail(self.comp_elt, "PS2002", f"list[{_show(elem)}] is not lowered")
|
|
824
|
+
typ = {"kind": "list", "elem": elem}
|
|
825
|
+
if self.comp_expect is not None and self.comp_expect["kind"] == "list":
|
|
826
|
+
self._expect(self.comp_elt, typ, self.comp_expect, "list")
|
|
827
|
+
if not self.comp_ready:
|
|
828
|
+
self.locals[self.comp_dest] = typ
|
|
829
|
+
empty = {"kind": "list", "elems": [], "type": typ, "span": span_of(self.comp_elt, self.filename)}
|
|
830
|
+
self.comp_init = _assign(self.comp_dest, empty, empty["span"])
|
|
831
|
+
self.comp_ready = True
|
|
832
|
+
# The push is a module call, and those arguments are emitted twice.
|
|
833
|
+
# Store the element first so a call in it runs once, and only in this branch.
|
|
834
|
+
slot = self._temp("elt")
|
|
835
|
+
self.locals[slot] = elem
|
|
836
|
+
push = {
|
|
837
|
+
"kind": "expr",
|
|
838
|
+
"expr": {
|
|
839
|
+
"kind": "call",
|
|
840
|
+
"callee": {"kind": "module", "module": "builtins", "name": "list.append"},
|
|
841
|
+
"args": [_name(self.comp_dest, typ, elt["span"]), _name(slot, elem, elt["span"])],
|
|
842
|
+
"symbol": "ps_list_push",
|
|
843
|
+
"type": {"kind": "none"},
|
|
844
|
+
"span": elt["span"],
|
|
845
|
+
},
|
|
846
|
+
"span": elt["span"],
|
|
847
|
+
}
|
|
848
|
+
kept = [*inner, _assign(slot, elt, elt["span"]), push]
|
|
849
|
+
if filt_cond is None:
|
|
850
|
+
return kept
|
|
851
|
+
return [
|
|
852
|
+
*filt_pre,
|
|
853
|
+
{"kind": "if", "cond": filt_cond, "then": kept, "else": [], "span": span_of(self.comp_if, self.filename)},
|
|
854
|
+
]
|
|
855
|
+
|
|
856
|
+
def _with_as_file(self, stmt: ast.With, item: ast.withitem, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
857
|
+
if not isinstance(item.optional_vars, ast.Name):
|
|
858
|
+
self.fail(stmt, "PS2002", "with as_file needs a name")
|
|
859
|
+
ctx = item.context_expr
|
|
860
|
+
assert isinstance(ctx, ast.Call)
|
|
861
|
+
if len(ctx.args) != 1 or ctx.keywords:
|
|
862
|
+
self.fail(ctx, "PS2003", "as_file expects 1 argument(s)")
|
|
863
|
+
opened = self._expr(ctx.args[0])
|
|
864
|
+
if opened["type"]["kind"] != "class" or opened["type"].get("name") != "Path":
|
|
865
|
+
self.fail(ctx, "PS1003", "as_file takes a resource path")
|
|
866
|
+
target = item.optional_vars.id
|
|
867
|
+
if target in self.bindings or target in self.constants:
|
|
868
|
+
self.fail(item.optional_vars, "PS2002", f"{target} is already bound")
|
|
869
|
+
self.locals[target] = opened["type"]
|
|
870
|
+
return [_assign(target, opened, span), *self._body(list(stmt.body))]
|
|
871
|
+
|
|
872
|
+
def _with_open(self, stmt: ast.With, item: ast.withitem, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
873
|
+
if not isinstance(item.optional_vars, ast.Name):
|
|
874
|
+
self.fail(stmt, "PS2002", "with open needs a name")
|
|
875
|
+
assert isinstance(item.context_expr, ast.Call)
|
|
876
|
+
opened = self._expr(item.context_expr)
|
|
877
|
+
target = item.optional_vars.id
|
|
878
|
+
if target in self.bindings or target in self.constants:
|
|
879
|
+
self.fail(item.optional_vars, "PS2002", f"{target} is already bound")
|
|
880
|
+
self.locals[target] = opened["type"]
|
|
881
|
+
close = {
|
|
882
|
+
"kind": "expr",
|
|
883
|
+
"expr": self._abi_call(
|
|
884
|
+
"io", "close", [_name(target, opened["type"], span)], {"kind": "none"}, "ps_io_close", span
|
|
885
|
+
),
|
|
886
|
+
"span": span,
|
|
887
|
+
}
|
|
888
|
+
body = self._inject_close(self._body(list(stmt.body)), close)
|
|
889
|
+
return [
|
|
890
|
+
_assign(target, opened, span),
|
|
891
|
+
{
|
|
892
|
+
"kind": "try",
|
|
893
|
+
"body": [*body, close],
|
|
894
|
+
"handlers": [
|
|
895
|
+
{
|
|
896
|
+
"exc": ["Exception"],
|
|
897
|
+
"body": [
|
|
898
|
+
close,
|
|
899
|
+
{
|
|
900
|
+
"kind": "raise",
|
|
901
|
+
"exc": "Exception",
|
|
902
|
+
"message": None,
|
|
903
|
+
"reraise": True,
|
|
904
|
+
"span": span,
|
|
905
|
+
},
|
|
906
|
+
],
|
|
907
|
+
}
|
|
908
|
+
],
|
|
909
|
+
"span": span,
|
|
910
|
+
},
|
|
911
|
+
]
|
|
912
|
+
|
|
913
|
+
def _for_enumerate(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
914
|
+
call = stmt.iter
|
|
915
|
+
assert isinstance(call, ast.Call)
|
|
916
|
+
if call.keywords:
|
|
917
|
+
self.fail(call, "PS2003", "keyword arguments are not lowered")
|
|
918
|
+
if len(call.args) not in {1, 2}:
|
|
919
|
+
self.fail(call, "PS2003", "enumerate takes 1 or 2 arguments")
|
|
920
|
+
source = self._expr(call.args[0])
|
|
921
|
+
if source["type"]["kind"] != "list" or source["type"].get("set"):
|
|
922
|
+
self.fail(call, "PS2002", "enumerate() takes a list")
|
|
923
|
+
elem = source["type"]["elem"]
|
|
924
|
+
if elem["kind"] not in _SCALAR | {"bool"}:
|
|
925
|
+
self.fail(call, "PS2002", f"enumerate() of list[{_show(elem)}] is not lowered")
|
|
926
|
+
start = _const_int(0, span)
|
|
927
|
+
if len(call.args) == 2:
|
|
928
|
+
start = self._expr(call.args[1], {"kind": "int"})
|
|
929
|
+
if start["type"]["kind"] != "int":
|
|
930
|
+
self.fail(call, "PS1003", "enumerate start is int")
|
|
931
|
+
return self._for_pairs(stmt, span, [source], [elem], start, "enumerate")
|
|
932
|
+
|
|
933
|
+
def _for_zip(self, stmt: ast.For, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
934
|
+
call = stmt.iter
|
|
935
|
+
assert isinstance(call, ast.Call)
|
|
936
|
+
if call.keywords or len(call.args) != 2:
|
|
937
|
+
self.fail(call, "PS2003", "zip takes 2 lists")
|
|
938
|
+
sources = [self._expr(arg) for arg in call.args]
|
|
939
|
+
elems: list[dict[str, Any]] = []
|
|
940
|
+
for source in sources:
|
|
941
|
+
if source["type"]["kind"] != "list" or source["type"].get("set"):
|
|
942
|
+
self.fail(call, "PS2002", "zip() takes lists")
|
|
943
|
+
elem = source["type"]["elem"]
|
|
944
|
+
if elem["kind"] not in _SCALAR | {"bool"}:
|
|
945
|
+
self.fail(call, "PS2002", f"zip() of list[{_show(elem)}] is not lowered")
|
|
946
|
+
elems.append(elem)
|
|
947
|
+
return self._for_pairs(stmt, span, sources, elems, None, "zip")
|
|
948
|
+
|
|
949
|
+
def _for_pairs(
|
|
950
|
+
self,
|
|
951
|
+
stmt: ast.For,
|
|
952
|
+
span: dict[str, Any],
|
|
953
|
+
sources: list[dict[str, Any]],
|
|
954
|
+
elems: list[dict[str, Any]],
|
|
955
|
+
enum_start: dict[str, Any] | None,
|
|
956
|
+
what: str,
|
|
957
|
+
) -> list[dict[str, Any]]:
|
|
958
|
+
if not isinstance(stmt.target, ast.Name):
|
|
959
|
+
self.fail(stmt, "PS2002", "for targets a single name")
|
|
960
|
+
int_t = {"kind": "int"}
|
|
961
|
+
fields = ([{"kind": "int"}, *elems]) if enum_start is not None else elems
|
|
962
|
+
pair = self._tuple_type(fields, stmt.iter)
|
|
963
|
+
target = stmt.target.id
|
|
964
|
+
fresh = self._bind_loop_target(target, pair, stmt.target)
|
|
965
|
+
seqs = [self._temp("seq") for _ in sources]
|
|
966
|
+
idx = self._temp("idx")
|
|
967
|
+
length = self._temp("len")
|
|
968
|
+
enum_i = self._temp("enum")
|
|
969
|
+
for seq, source in zip(seqs, sources, strict=True):
|
|
970
|
+
self.locals[seq] = source["type"]
|
|
971
|
+
self.locals[idx] = int_t
|
|
972
|
+
self.locals[length] = int_t
|
|
973
|
+
if enum_start is not None:
|
|
974
|
+
self.locals[enum_i] = int_t
|
|
975
|
+
body = self._loop_body(stmt)
|
|
976
|
+
values: list[dict[str, Any]] = []
|
|
977
|
+
if enum_start is not None:
|
|
978
|
+
values.append(_name(enum_i, int_t, span))
|
|
979
|
+
for seq, source, elem in zip(seqs, sources, elems, strict=True):
|
|
980
|
+
values.append(
|
|
981
|
+
{
|
|
982
|
+
"kind": "subscript",
|
|
983
|
+
"value": _name(seq, source["type"], span),
|
|
984
|
+
"index": _name(idx, int_t, span),
|
|
985
|
+
"type": elem,
|
|
986
|
+
"span": span,
|
|
987
|
+
}
|
|
988
|
+
)
|
|
989
|
+
tail = [_assign(idx, _binary_int("+", _name(idx, int_t, span), _const_int(1, span), span), span)]
|
|
990
|
+
if enum_start is not None:
|
|
991
|
+
tail.append(_assign(enum_i, _binary_int("+", _name(enum_i, int_t, span), _const_int(1, span), span), span))
|
|
992
|
+
step = [*_tuple_assign(target, pair, values, span), *body, *self._skip_on_break(tail, span)]
|
|
993
|
+
prefix: list[dict[str, Any]] = []
|
|
994
|
+
if fresh:
|
|
995
|
+
zeros = [_zero(field, span) for field in fields]
|
|
996
|
+
prefix.extend(_tuple_assign(target, pair, zeros, span))
|
|
997
|
+
for seq, source in zip(seqs, sources, strict=True):
|
|
998
|
+
prefix.append(_assign(seq, source, span))
|
|
999
|
+
prefix.append(_assign(idx, _const_int(0, span), span))
|
|
1000
|
+
if len(sources) == 1:
|
|
1001
|
+
prefix.append(_assign(length, _len_call(_name(seqs[0], sources[0]["type"], span), span), span))
|
|
1002
|
+
else:
|
|
1003
|
+
left_n = self._temp("n")
|
|
1004
|
+
right_n = self._temp("n")
|
|
1005
|
+
self.locals[left_n] = int_t
|
|
1006
|
+
self.locals[right_n] = int_t
|
|
1007
|
+
prefix.extend(
|
|
1008
|
+
[
|
|
1009
|
+
_assign(left_n, _len_call(_name(seqs[0], sources[0]["type"], span), span), span),
|
|
1010
|
+
_assign(right_n, _len_call(_name(seqs[1], sources[1]["type"], span), span), span),
|
|
1011
|
+
_assign(length, _name(left_n, int_t, span), span),
|
|
1012
|
+
{
|
|
1013
|
+
"kind": "if",
|
|
1014
|
+
"cond": _compare("<", _name(right_n, int_t, span), _name(left_n, int_t, span), span),
|
|
1015
|
+
"then": [_assign(length, _name(right_n, int_t, span), span)],
|
|
1016
|
+
"else": [],
|
|
1017
|
+
"span": span,
|
|
1018
|
+
},
|
|
1019
|
+
]
|
|
1020
|
+
)
|
|
1021
|
+
if enum_start is not None:
|
|
1022
|
+
prefix.append(_assign(enum_i, enum_start, span))
|
|
1023
|
+
prefix.extend(
|
|
1024
|
+
self._user_while(
|
|
1025
|
+
_compare("<", _name(idx, int_t, span), _name(length, int_t, span), span),
|
|
1026
|
+
step,
|
|
1027
|
+
span,
|
|
1028
|
+
)
|
|
1029
|
+
)
|
|
1030
|
+
if what not in {"enumerate", "zip"}:
|
|
1031
|
+
self.fail(stmt, "PS2002", f"{what} is not lowered")
|
|
1032
|
+
return prefix
|
|
1033
|
+
|
|
1034
|
+
def _inject_close(self, body: list[dict[str, Any]], close: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1035
|
+
"""Close after a return value is computed, so ``return handle.read()`` still sees an open file."""
|
|
1036
|
+
out: list[dict[str, Any]] = []
|
|
1037
|
+
for stmt in body:
|
|
1038
|
+
if stmt["kind"] == "return":
|
|
1039
|
+
value = stmt["value"]
|
|
1040
|
+
if value["kind"] == "const" and value["type"]["kind"] == "none":
|
|
1041
|
+
out.append(close)
|
|
1042
|
+
out.append(stmt)
|
|
1043
|
+
continue
|
|
1044
|
+
temp = self._temp("withret")
|
|
1045
|
+
self.locals[temp] = value["type"]
|
|
1046
|
+
out.append(_assign(temp, value, value["span"]))
|
|
1047
|
+
out.append(close)
|
|
1048
|
+
out.append({**stmt, "value": _name(temp, value["type"], value["span"])})
|
|
1049
|
+
continue
|
|
1050
|
+
if stmt["kind"] == "if":
|
|
1051
|
+
out.append(
|
|
1052
|
+
{
|
|
1053
|
+
**stmt,
|
|
1054
|
+
"then": self._inject_close(stmt["then"], close),
|
|
1055
|
+
"else": self._inject_close(stmt["else"], close),
|
|
1056
|
+
}
|
|
1057
|
+
)
|
|
1058
|
+
continue
|
|
1059
|
+
if stmt["kind"] == "while":
|
|
1060
|
+
out.append({**stmt, "body": self._inject_close(stmt["body"], close)})
|
|
1061
|
+
continue
|
|
1062
|
+
if stmt["kind"] == "try":
|
|
1063
|
+
out.append(
|
|
1064
|
+
{
|
|
1065
|
+
**stmt,
|
|
1066
|
+
"body": self._inject_close(stmt["body"], close),
|
|
1067
|
+
"handlers": [
|
|
1068
|
+
{**handler, "body": self._inject_close(handler["body"], close)}
|
|
1069
|
+
for handler in stmt["handlers"]
|
|
1070
|
+
],
|
|
1071
|
+
}
|
|
1072
|
+
)
|
|
1073
|
+
continue
|
|
1074
|
+
out.append(stmt)
|
|
1075
|
+
return out
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _is_call_name(node: ast.expr, name: str) -> bool:
|
|
1079
|
+
return isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == name
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _is_docstring(stmt: ast.stmt) -> bool:
|
|
1083
|
+
return isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _is_main_guard(stmt: ast.stmt) -> bool:
|
|
1087
|
+
if not isinstance(stmt, ast.If) or not isinstance(stmt.test, ast.Compare):
|
|
1088
|
+
return False
|
|
1089
|
+
test = stmt.test
|
|
1090
|
+
return (
|
|
1091
|
+
isinstance(test.left, ast.Name)
|
|
1092
|
+
and test.left.id == "__name__"
|
|
1093
|
+
and len(test.ops) == 1
|
|
1094
|
+
and isinstance(test.ops[0], ast.Eq)
|
|
1095
|
+
and len(test.comparators) == 1
|
|
1096
|
+
and isinstance(test.comparators[0], ast.Constant)
|
|
1097
|
+
and test.comparators[0].value == "__main__"
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _is_range_call(node: ast.expr) -> bool:
|
|
1102
|
+
return isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "range"
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def _is_setitem(stmt: ast.stmt) -> bool:
|
|
1106
|
+
return isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Subscript)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _handler_names(lowerer: Lowerer, node: ast.expr) -> list[str]:
|
|
1110
|
+
names = node.elts if isinstance(node, ast.Tuple) else [node]
|
|
1111
|
+
found: list[str] = []
|
|
1112
|
+
for item in names:
|
|
1113
|
+
if not isinstance(item, ast.Name):
|
|
1114
|
+
lowerer.fail(item, "PS2002", "except names a lowered exception")
|
|
1115
|
+
resolved = lowerer.class_aliases.get(item.id, item.id)
|
|
1116
|
+
info = lowerer.classes.get(resolved)
|
|
1117
|
+
user_exception = info is not None and bool(info.get("exception"))
|
|
1118
|
+
if resolved not in EXCEPTIONS and item.id not in EXCEPTIONS and not user_exception:
|
|
1119
|
+
lowerer.fail(item, "PS2002", "except names a lowered exception")
|
|
1120
|
+
found.append(resolved if user_exception else item.id)
|
|
1121
|
+
return found
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _not_bool(name: str, span: dict[str, Any]) -> dict[str, Any]:
|
|
1125
|
+
return {
|
|
1126
|
+
"kind": "unary",
|
|
1127
|
+
"op": "not",
|
|
1128
|
+
"operand": _name(name, {"kind": "bool"}, span),
|
|
1129
|
+
"type": {"kind": "bool"},
|
|
1130
|
+
"span": span,
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _stmt_jumps_this_loop(stmt: ast.stmt, jump: type[ast.stmt]) -> bool:
|
|
1135
|
+
if isinstance(stmt, jump):
|
|
1136
|
+
return True
|
|
1137
|
+
if isinstance(stmt, (ast.While, ast.For, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
1138
|
+
return False
|
|
1139
|
+
nested: list[ast.stmt] = []
|
|
1140
|
+
if isinstance(stmt, ast.If):
|
|
1141
|
+
nested = [*stmt.body, *stmt.orelse]
|
|
1142
|
+
elif isinstance(stmt, ast.Try):
|
|
1143
|
+
nested = [*stmt.body, *stmt.orelse, *stmt.finalbody]
|
|
1144
|
+
for handler in stmt.handlers:
|
|
1145
|
+
nested.extend(handler.body)
|
|
1146
|
+
elif isinstance(stmt, ast.With):
|
|
1147
|
+
nested = list(stmt.body)
|
|
1148
|
+
elif isinstance(stmt, ast.Match):
|
|
1149
|
+
nested = [child for case in stmt.cases for child in case.body]
|
|
1150
|
+
return any(_stmt_jumps_this_loop(child, jump) for child in nested)
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def _stmt_breaks_this_loop(stmt: ast.stmt) -> bool:
|
|
1154
|
+
return _stmt_jumps_this_loop(stmt, ast.Break)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _stmt_continues_this_loop(stmt: ast.stmt) -> bool:
|
|
1158
|
+
return _stmt_jumps_this_loop(stmt, ast.Continue)
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _always_returns(body: list[dict[str, Any]]) -> bool:
|
|
1162
|
+
for stmt in body:
|
|
1163
|
+
if stmt["kind"] == "return" or stmt["kind"] == "raise":
|
|
1164
|
+
return True
|
|
1165
|
+
if stmt["kind"] == "if" and _always_returns(stmt["then"]) and _always_returns(stmt["else"]):
|
|
1166
|
+
return True
|
|
1167
|
+
if (
|
|
1168
|
+
stmt["kind"] == "try"
|
|
1169
|
+
and _always_returns(stmt["body"])
|
|
1170
|
+
and all(_always_returns(handler["body"]) for handler in stmt["handlers"])
|
|
1171
|
+
):
|
|
1172
|
+
return True
|
|
1173
|
+
return False
|