pyscriptc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. pyscriptc/__init__.py +3 -0
  2. pyscriptc/__main__.py +3 -0
  3. pyscriptc/_posthog_token.py +1 -0
  4. pyscriptc/builtins.py +156 -0
  5. pyscriptc/canonical.py +167 -0
  6. pyscriptc/check.py +49 -0
  7. pyscriptc/cli.py +271 -0
  8. pyscriptc/coverage.py +388 -0
  9. pyscriptc/debug.py +120 -0
  10. pyscriptc/diagnostics.py +179 -0
  11. pyscriptc/errors.py +56 -0
  12. pyscriptc/ir.py +365 -0
  13. pyscriptc/llvm.py +2129 -0
  14. pyscriptc/log.py +127 -0
  15. pyscriptc/lower/__init__.py +1071 -0
  16. pyscriptc/lower/_calls.py +1499 -0
  17. pyscriptc/lower/_classes.py +969 -0
  18. pyscriptc/lower/_core.py +301 -0
  19. pyscriptc/lower/_expr.py +886 -0
  20. pyscriptc/lower/_stdlib.py +491 -0
  21. pyscriptc/lower/_stdlib_shared.py +93 -0
  22. pyscriptc/lower/_stmt.py +1173 -0
  23. pyscriptc/package.py +249 -0
  24. pyscriptc/restlower.py +1008 -0
  25. pyscriptc/runtime/ps_argv.c +28 -0
  26. pyscriptc/runtime/ps_b64.c +168 -0
  27. pyscriptc/runtime/ps_coll.c +133 -0
  28. pyscriptc/runtime/ps_dt.c +388 -0
  29. pyscriptc/runtime/ps_hash.c +411 -0
  30. pyscriptc/runtime/ps_help.c +219 -0
  31. pyscriptc/runtime/ps_hmac.c +120 -0
  32. pyscriptc/runtime/ps_io.c +198 -0
  33. pyscriptc/runtime/ps_island.c +272 -0
  34. pyscriptc/runtime/ps_json.c +402 -0
  35. pyscriptc/runtime/ps_mix.c +47 -0
  36. pyscriptc/runtime/ps_more.c +1089 -0
  37. pyscriptc/runtime/ps_osmore.c +447 -0
  38. pyscriptc/runtime/ps_path.c +547 -0
  39. pyscriptc/runtime/ps_re.c +1102 -0
  40. pyscriptc/runtime/ps_rt.c +1175 -0
  41. pyscriptc/runtime/ps_rt.h +189 -0
  42. pyscriptc/runtime/ps_struct.c +291 -0
  43. pyscriptc/runtime/ps_time.c +207 -0
  44. pyscriptc/runtime/ps_unicode.c +1521 -0
  45. pyscriptc/runtime/ps_url.c +327 -0
  46. pyscriptc/stdlib_queue.py +191 -0
  47. pyscriptc/stdmore.py +1596 -0
  48. pyscriptc/telemetry.py +219 -0
  49. pyscriptc-0.1.0.dist-info/METADATA +187 -0
  50. pyscriptc-0.1.0.dist-info/RECORD +54 -0
  51. pyscriptc-0.1.0.dist-info/WHEEL +5 -0
  52. pyscriptc-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyscriptc-0.1.0.dist-info/licenses/LICENSE +201 -0
  54. pyscriptc-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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