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,886 @@
1
+ """Expression lowering for the lowerer.
2
+
3
+ Split out of `__init__.py` as E0 move 2. `Lowerer` composes this mixin in.
4
+ The area owns the `_expr` dispatch and its expression-form methods: names,
5
+ constants, unary/binary/compare (including chains and membership), list and
6
+ dict displays, subscripts and slices, attributes, calls, `and`/`or`,
7
+ conditionals, f-strings and `%` formatting.
8
+
9
+ Tables shared with the statement, call, and typing paths (`_CMP`, `_SCALAR`,
10
+ `_TRUTHY`, `_LIST_FORMS`, `_DICT_FORMS`, `_SET_FORMS`) live in `_core.py`;
11
+ `_INT_BIN`/`_FLOAT_BIN` are expression-only and stay here. The boundary calls
12
+ in both directions stay `self._method()` so the move is behavior-identical.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ast
18
+ from typing import Any
19
+
20
+ from pyscriptc.builtins import EXCEPTIONS
21
+ from pyscriptc.errors import span_of
22
+ from pyscriptc.ir import types_equal
23
+ from pyscriptc.lower._core import (
24
+ _CMP,
25
+ _SCALAR,
26
+ _TRUTHY,
27
+ _assign,
28
+ _binary_int,
29
+ _compare,
30
+ _concat_str,
31
+ _const_bool,
32
+ _const_int,
33
+ _const_list_concat,
34
+ _const_none,
35
+ _const_str_join,
36
+ _len_call,
37
+ _name,
38
+ _percent_pieces,
39
+ _plain_const_kind,
40
+ _show,
41
+ _static_int,
42
+ )
43
+ from pyscriptc.stdlib_queue import (
44
+ MATH_CONSTANTS,
45
+ MATH_FUNCTIONS,
46
+ SPECIAL_FUNCTIONS,
47
+ wave_function,
48
+ )
49
+ from pyscriptc.stdmore import COMPARE_CLASSES, member_callable, module_constant
50
+
51
+ _INT_BIN = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.FloorDiv: "//"}
52
+ _FLOAT_BIN = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*"}
53
+
54
+
55
+ class ExprMixin:
56
+ def _expr(self, node: ast.expr, expect: dict[str, Any] | None = None) -> dict[str, Any]:
57
+ span = span_of(node, self.filename)
58
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
59
+ return self._name(node, span)
60
+ if isinstance(node, ast.Constant):
61
+ return self._const(node, span)
62
+ if isinstance(node, ast.UnaryOp):
63
+ return self._unary(node, span)
64
+ if isinstance(node, ast.BinOp):
65
+ return self._binary(node, span)
66
+ if isinstance(node, ast.Compare):
67
+ return self._compare(node, span)
68
+ if isinstance(node, ast.Call):
69
+ return self._call(node, span, expect)
70
+ if isinstance(node, ast.Attribute):
71
+ return self._attribute(node, span)
72
+ if isinstance(node, ast.List):
73
+ return self._list(node, span, expect)
74
+ if isinstance(node, ast.Set):
75
+ return self._set_display(node, span, expect)
76
+ if isinstance(node, ast.Dict):
77
+ return self._dict(node, span, expect)
78
+ if isinstance(node, ast.Tuple):
79
+ return self._tuple_expr(node, span)
80
+ if isinstance(node, ast.Subscript):
81
+ return self._subscript(node, span)
82
+ if isinstance(node, ast.BoolOp):
83
+ return self._boolop(node, span)
84
+ if isinstance(node, ast.JoinedStr):
85
+ return self._joined(node, span)
86
+ if isinstance(node, ast.ListComp):
87
+ return self._listcomp(node, span, expect)
88
+ if isinstance(node, ast.IfExp):
89
+ return self._ifexp(node, span)
90
+ self.fail(node, "PS2002", f"{type(node).__name__} is not lowered")
91
+ raise AssertionError("unreachable")
92
+
93
+ def _name(self, node: ast.Name, span: dict[str, Any]) -> dict[str, Any]:
94
+ if node.id in self.locals:
95
+ return _name(node.id, self.locals[node.id], span)
96
+ slot = self.global_slots.get(node.id)
97
+ if slot is not None and slot.get("active"):
98
+ return self._global_get(node.id, span)
99
+ if node.id in self.constants:
100
+ found = self.constants[node.id]
101
+ if found.get("kind") == "tuple-const":
102
+ return self._materialize_tuple(found["elems"], span)
103
+ return {**found, "span": span}
104
+ if node.id in self.type_aliases:
105
+ self.fail(node, "PS2002", f"{node.id} is a typing name and is not a value")
106
+ binding = self.bindings.get(node.id)
107
+ if binding is not None:
108
+ if binding.queued:
109
+ self._queued(node, f"{binding.module}.{binding.attr}" if binding.attr else binding.module)
110
+ if binding.kind == "erased":
111
+ self.fail(node, "PS2002", f"{node.id} is a typing name and is not a value")
112
+ if binding.kind == "const":
113
+ if binding.module == "math":
114
+ value: Any = MATH_CONSTANTS[binding.attr]
115
+ typ: dict[str, Any] = {"kind": "float"}
116
+ else:
117
+ found = module_constant(binding.module, binding.attr)
118
+ if found is None:
119
+ self.fail(node, "PS2001", f"{binding.module}.{binding.attr} is not a constant")
120
+ value, typ = found
121
+ return {"kind": "const", "value": value, "type": typ, "span": span}
122
+ if binding.kind in {"module", "pkg"}:
123
+ self.fail(node, "PS2002", f"module {node.id} is not a value in the static subset")
124
+ self.fail(node, "PS2002", f"{node.id} is a function and is not a value")
125
+ if node.id in self.classes:
126
+ self.fail(node, "PS2002", f"class {node.id} is not a value")
127
+ if node.id in EXCEPTIONS:
128
+ self.fail(node, "PS2002", f"{node.id} is only lowered in raise and except")
129
+ self.fail(node, "PS2002", f"{node.id} is not a local or parameter")
130
+ raise AssertionError("unreachable")
131
+
132
+ def _const(self, node: ast.Constant, span: dict[str, Any]) -> dict[str, Any]:
133
+ value = node.value
134
+ if isinstance(value, bool):
135
+ return {"kind": "const", "value": value, "type": {"kind": "bool"}, "span": span}
136
+ if isinstance(value, int):
137
+ return {"kind": "const", "value": str(value), "type": {"kind": "int"}, "span": span}
138
+ if isinstance(value, float):
139
+ return {"kind": "const", "value": repr(value), "type": {"kind": "float"}, "span": span}
140
+ if isinstance(value, str):
141
+ if any(ord(ch) > 255 for ch in value):
142
+ self.fail(node, "PS2005", "string literals outside Latin-1 are not lowered")
143
+ return {"kind": "const", "value": value, "type": {"kind": "str"}, "span": span}
144
+ if isinstance(value, bytes):
145
+ return {"kind": "const", "value": value.decode("latin1"), "type": {"kind": "bytes"}, "span": span}
146
+ if value is None:
147
+ return _const_none(span)
148
+ self.fail(node, "PS2002", f"literal {type(value).__name__} is not lowered")
149
+ raise AssertionError("unreachable")
150
+
151
+ def _unary(self, node: ast.UnaryOp, span: dict[str, Any]) -> dict[str, Any]:
152
+ operand = self._expr(node.operand)
153
+ kind = operand["type"]["kind"]
154
+ if isinstance(node.op, ast.USub) and kind == "int":
155
+ return {"kind": "unary", "op": "-", "operand": operand, "type": {"kind": "int"}, "span": span}
156
+ if isinstance(node.op, ast.USub) and kind == "float":
157
+ return {"kind": "unary", "op": "-", "operand": operand, "type": {"kind": "float"}, "span": span}
158
+ if isinstance(node.op, ast.Not) and kind == "bool":
159
+ return {"kind": "unary", "op": "not", "operand": operand, "type": {"kind": "bool"}, "span": span}
160
+ if isinstance(node.op, ast.Not) and kind in _TRUTHY:
161
+ inner = self._truthy(operand, span)
162
+ return {"kind": "unary", "op": "not", "operand": inner, "type": {"kind": "bool"}, "span": span}
163
+ if isinstance(node.op, ast.USub) and kind == "class" and operand["type"].get("name") == "TimeDelta":
164
+ return {"kind": "unary", "op": "-", "operand": operand, "type": operand["type"], "span": span}
165
+ self.fail(node, "PS2004", f"unary {type(node.op).__name__} on {kind} is not lowered")
166
+ raise AssertionError("unreachable")
167
+
168
+ def _binary(self, node: ast.BinOp, span: dict[str, Any]) -> dict[str, Any]:
169
+ if isinstance(node.op, ast.Mod):
170
+ return self._percent(node, span)
171
+ left = self._expr(node.left)
172
+ right = self._expr(node.right)
173
+ left_kind = left["type"]["kind"]
174
+ right_kind = right["type"]["kind"]
175
+ if left_kind == "bool" or right_kind == "bool":
176
+ self.fail(node, "PS2004", "bool arithmetic is not lowered (bool is a subclass of int in Python)")
177
+ if isinstance(node.op, ast.Pow):
178
+ return self._pow(node, left, right, span)
179
+ op_type = type(node.op)
180
+ if left_kind == "int" and right_kind == "int" and op_type in _INT_BIN:
181
+ return _binary_int(_INT_BIN[op_type], left, right, span)
182
+ if left_kind == "float" and right_kind == "float" and op_type in _FLOAT_BIN:
183
+ return {
184
+ "kind": "binary",
185
+ "op": _FLOAT_BIN[op_type],
186
+ "left": left,
187
+ "right": right,
188
+ "type": {"kind": "float"},
189
+ "span": span,
190
+ }
191
+ if left_kind == right_kind and left_kind in {"str", "bytes"} and isinstance(node.op, ast.Add):
192
+ return {
193
+ "kind": "binary",
194
+ "op": "+",
195
+ "left": left,
196
+ "right": right,
197
+ "type": {"kind": left_kind},
198
+ "span": span,
199
+ }
200
+ if left_kind == "class" or right_kind == "class":
201
+ return self._class_binary(node, left, right, span)
202
+ joined = _const_list_concat(left, right, span)
203
+ if joined is not None and isinstance(node.op, ast.Add):
204
+ return joined
205
+ if (
206
+ isinstance(node.op, ast.Add)
207
+ and left_kind == "list"
208
+ and right_kind == "list"
209
+ and not left["type"].get("set")
210
+ and not right["type"].get("set")
211
+ and types_equal(left["type"]["elem"], right["type"]["elem"])
212
+ ):
213
+ # `xs + ys` builds a fresh list; `xs += ys` reaches the same node
214
+ # through the aug-assign rewrite, which makes += copy, not mutate.
215
+ return self._abi_call(
216
+ "builtins",
217
+ "concat",
218
+ [left, right],
219
+ {"kind": "list", "elem": left["type"]["elem"]},
220
+ "ps_seq_concat",
221
+ span,
222
+ )
223
+ if isinstance(node.op, ast.Mult):
224
+ if left_kind == "list" and right_kind == "int" and not left["type"].get("set"):
225
+ return self._abi_call("builtins", "repeat", [left, right], left["type"], "ps_seq_repeat", span)
226
+ if left_kind == "int" and right_kind == "list" and not right["type"].get("set"):
227
+ return self._abi_call("builtins", "repeat", [right, left], right["type"], "ps_seq_repeat", span)
228
+ self.fail(node, "PS2004", f"{op_type.__name__} on {left_kind} and {right_kind} is not lowered")
229
+ raise AssertionError("unreachable")
230
+
231
+ def _pow(
232
+ self, node: ast.BinOp, left: dict[str, Any], right: dict[str, Any], span: dict[str, Any]
233
+ ) -> dict[str, Any]:
234
+ """`base ** exp` is a loop of the existing int `*`.
235
+
236
+ Both operands are ints. The base and the exponent are stored once, then
237
+ the accumulator starts at 1 and is multiplied `exp` times. `0 ** 0` is
238
+ that 1. A negative exponent stays fenced: a constant is refused here,
239
+ and a computed negative raises ValueError instead of becoming a float.
240
+ """
241
+ if left["type"]["kind"] != "int" or right["type"]["kind"] != "int":
242
+ self.fail(node, "PS2004", "** is only lowered for int")
243
+ static = _static_int(right)
244
+ if static is not None and static < 0:
245
+ self.fail(node, "PS2004", "a negative exponent is not lowered")
246
+ int_t = {"kind": "int"}
247
+ base = self._temp("powb")
248
+ exp = self._temp("powe")
249
+ acc = self._temp("powa")
250
+ idx = self._temp("powi")
251
+ for name in (base, exp, acc, idx):
252
+ self.locals[name] = int_t
253
+ base_n = _name(base, int_t, span)
254
+ exp_n = _name(exp, int_t, span)
255
+ acc_n = _name(acc, int_t, span)
256
+ idx_n = _name(idx, int_t, span)
257
+ self.prelude.extend(
258
+ [
259
+ _assign(base, left, span),
260
+ _assign(exp, right, span),
261
+ {
262
+ "kind": "if",
263
+ "cond": _compare("<", exp_n, _const_int(0, span), span),
264
+ "then": [{"kind": "raise", "exc": "ValueError", "message": None, "span": span}],
265
+ "else": [],
266
+ "span": span,
267
+ },
268
+ _assign(acc, _const_int(1, span), span),
269
+ _assign(idx, _const_int(0, span), span),
270
+ {
271
+ "kind": "while",
272
+ "cond": _compare("<", idx_n, exp_n, span),
273
+ "body": [
274
+ _assign(acc, _binary_int("*", acc_n, base_n, span), span),
275
+ _assign(idx, _binary_int("+", idx_n, _const_int(1, span), span), span),
276
+ ],
277
+ "span": span,
278
+ },
279
+ ]
280
+ )
281
+ return _name(acc, int_t, span)
282
+
283
+ def _compare(self, node: ast.Compare, span: dict[str, Any]) -> dict[str, Any]:
284
+ if len(node.ops) != 1 or len(node.comparators) != 1:
285
+ return self._compare_chain(node, span)
286
+ if isinstance(node.ops[0], (ast.In, ast.NotIn)):
287
+ return self._contains(node, span, isinstance(node.ops[0], ast.NotIn))
288
+ if isinstance(node.ops[0], (ast.Is, ast.IsNot)):
289
+ return self._is_none(node, span, isinstance(node.ops[0], ast.IsNot))
290
+ op = _CMP.get(type(node.ops[0]))
291
+ if op is None:
292
+ self.fail(node, "PS2004", f"{type(node.ops[0]).__name__} is not lowered")
293
+ left = self._expr(node.left)
294
+ right = self._expr(node.comparators[0])
295
+ if left["type"].get("ordered") or right["type"].get("ordered"):
296
+ self.fail(node, "PS2004", "OrderedDict equality is not lowered")
297
+ if (
298
+ left["type"]["kind"] == "class"
299
+ and right["type"]["kind"] == "class"
300
+ and left["type"].get("name") == right["type"].get("name")
301
+ and left["type"].get("name") in COMPARE_CLASSES
302
+ ):
303
+ return _compare(op, left, right, span)
304
+ if left["type"]["kind"] != right["type"]["kind"] or left["type"]["kind"] not in {
305
+ "int",
306
+ "float",
307
+ "bool",
308
+ "str",
309
+ "bytes",
310
+ }:
311
+ self.fail(node, "PS2004", "comparisons need two values of the same scalar type")
312
+ if left["type"]["kind"] == "bool" and op not in {"==", "!="}:
313
+ self.fail(node, "PS2004", "bool ordering is not lowered")
314
+ return _compare(op, left, right, span)
315
+
316
+ def _compare_chain(self, node: ast.Compare, span: dict[str, Any]) -> dict[str, Any]:
317
+ """`a < b < c` is the existing compares joined by the existing `and`.
318
+
319
+ Evaluate the left, then the first middle, and compare. Each later
320
+ middle is stored once, inside the `and` branch, so a false link does
321
+ not evaluate the rest. `is` stays fenced. A link the one-op compare
322
+ rejects still fences.
323
+ """
324
+ if len(node.ops) != len(node.comparators) or len(node.ops) < 2:
325
+ self.fail(node, "PS2004", "chained comparisons are not lowered")
326
+ if any(isinstance(op, (ast.Is, ast.IsNot)) for op in node.ops):
327
+ self.fail(node, "PS2004", "is is not lowered")
328
+ bool_t = {"kind": "bool"}
329
+ result = self._temp("chain")
330
+ self.locals[result] = bool_t
331
+ left_ref = self._chain_pin(node.left)
332
+ prev_ref: ast.expr = self._chain_pin(node.comparators[0])
333
+ first = self._one_link(node, node.ops[0], left_ref, prev_ref)
334
+ self.prelude.append(_assign(result, first, span))
335
+ for index in range(1, len(node.ops)):
336
+ saved = self.prelude
337
+ self.prelude = []
338
+ try:
339
+ operand = node.comparators[index]
340
+ if index + 1 < len(node.ops):
341
+ right_ref: ast.expr = self._chain_pin(operand)
342
+ else:
343
+ right_ref = operand
344
+ link = self._one_link(node, node.ops[index], prev_ref, right_ref)
345
+ if index + 1 < len(node.ops):
346
+ prev_ref = right_ref
347
+ finally:
348
+ inner = self.prelude
349
+ self.prelude = saved
350
+ inner.append(_assign(result, link, span))
351
+ cond = self._truthy(_name(result, bool_t, span), span)
352
+ self.prelude.append({"kind": "if", "cond": cond, "then": inner, "else": [], "span": span})
353
+ return _name(result, bool_t, span)
354
+
355
+ def _chain_pin(self, expr: ast.expr) -> ast.Name:
356
+ value = self._expr(expr)
357
+ temp = self._temp("cmp")
358
+ self.locals[temp] = value["type"]
359
+ pinned = ast.Name(id=temp, ctx=ast.Load())
360
+ ast.copy_location(pinned, expr)
361
+ self.prelude.append(_assign(temp, value, span_of(expr, self.filename)))
362
+ return pinned
363
+
364
+ def _one_link(self, origin: ast.Compare, op: ast.cmpop, left: ast.expr, right: ast.expr) -> dict[str, Any]:
365
+ compare = ast.Compare(left=left, ops=[op], comparators=[right])
366
+ ast.copy_location(compare, origin)
367
+ return self._compare(compare, span_of(origin, self.filename))
368
+
369
+ def _contains(self, node: ast.Compare, span: dict[str, Any], negated: bool) -> dict[str, Any]:
370
+ folded = self._const_tuple_membership(node, span, negated)
371
+ if folded is not None:
372
+ return folded
373
+ needle = self._expr(node.left)
374
+ hay = self._expr(node.comparators[0])
375
+ hay_kind = hay["type"]["kind"]
376
+ if hay_kind in {"str", "bytes"}:
377
+ if needle["type"]["kind"] != hay_kind:
378
+ self.fail(node, "PS1003", f"in on {hay_kind} takes {hay_kind}")
379
+ elif hay_kind == "list":
380
+ if hay["type"]["elem"]["kind"] not in _SCALAR or not types_equal(needle["type"], hay["type"]["elem"]):
381
+ self.fail(node, "PS1003", "in on a list takes an element of that list")
382
+ elif hay_kind == "dict":
383
+ if not types_equal(needle["type"], hay["type"]["key"]):
384
+ self.fail(node, "PS1003", "in on a dict takes a key")
385
+ else:
386
+ self.fail(node, "PS2002", "in is lowered for str, bytes, list, and dict")
387
+ if hay_kind == "list" and not hay["type"].get("set") and hay["type"]["elem"]["kind"] == "float":
388
+ return self._float_list_membership(needle, hay, span, negated)
389
+ return {
390
+ "kind": "contains",
391
+ "hay": hay,
392
+ "needle": needle,
393
+ "negated": negated,
394
+ "type": {"kind": "bool"},
395
+ "span": span,
396
+ }
397
+
398
+ def _float_list_membership(
399
+ self, needle: dict[str, Any], hay: dict[str, Any], span: dict[str, Any], negated: bool
400
+ ) -> dict[str, Any]:
401
+ """Float list membership is ==, so NaN is not a member of a list of NaNs.
402
+
403
+ Int and str lists stay on the contains helper. A set stays there too.
404
+ """
405
+ int_t = {"kind": "int"}
406
+ float_t = {"kind": "float"}
407
+ bool_t = {"kind": "bool"}
408
+ held = self._temp("fneedle")
409
+ src = self._temp("fseq")
410
+ idx = self._temp("fidx")
411
+ length = self._temp("flen")
412
+ found = self._temp("fhas")
413
+ item = self._temp("felt")
414
+ self.locals[held] = float_t
415
+ self.locals[src] = hay["type"]
416
+ self.locals[idx] = int_t
417
+ self.locals[length] = int_t
418
+ self.locals[found] = bool_t
419
+ self.locals[item] = float_t
420
+ self.prelude.extend(
421
+ [
422
+ _assign(held, needle, span),
423
+ _assign(src, hay, span),
424
+ _assign(length, _len_call(_name(src, hay["type"], span), span), span),
425
+ _assign(idx, _const_int(0, span), span),
426
+ _assign(found, _const_bool(False, span), span),
427
+ {
428
+ "kind": "while",
429
+ "cond": _compare("<", _name(idx, int_t, span), _name(length, int_t, span), span),
430
+ "body": [
431
+ _assign(
432
+ item,
433
+ {
434
+ "kind": "subscript",
435
+ "value": _name(src, hay["type"], span),
436
+ "index": _name(idx, int_t, span),
437
+ "type": float_t,
438
+ "span": span,
439
+ },
440
+ span,
441
+ ),
442
+ {
443
+ "kind": "if",
444
+ "cond": _compare("==", _name(item, float_t, span), _name(held, float_t, span), span),
445
+ "then": [_assign(found, _const_bool(True, span), span)],
446
+ "else": [],
447
+ "span": span,
448
+ },
449
+ _assign(idx, _binary_int("+", _name(idx, int_t, span), _const_int(1, span), span), span),
450
+ ],
451
+ "span": span,
452
+ },
453
+ ]
454
+ )
455
+ result = _name(found, bool_t, span)
456
+ if negated:
457
+ return {"kind": "unary", "op": "not", "operand": result, "type": bool_t, "span": span}
458
+ return result
459
+
460
+ def _const_tuple_membership(self, node: ast.Compare, span: dict[str, Any], negated: bool) -> dict[str, Any] | None:
461
+ """`value in (c0, c1, ...)` of one const type is `==` joined by `or`.
462
+
463
+ The tuple is not a class. Empty is false. One element is one `==`.
464
+ `not in` is `not` of that test. A name, a list, a mix, or a non-constant
465
+ element stays on the ordinary path.
466
+ """
467
+ hay = node.comparators[0]
468
+ if not isinstance(hay, ast.Tuple) or any(isinstance(elt, ast.Starred) for elt in hay.elts):
469
+ return None
470
+ kinds = [_plain_const_kind(elt) for elt in hay.elts]
471
+ if any(kind is None for kind in kinds) or len(set(kinds)) > 1:
472
+ return None
473
+ needle = self._expr(node.left)
474
+ if not hay.elts:
475
+ self.prelude.append({"kind": "expr", "expr": needle, "span": span})
476
+ return _const_bool(negated, span)
477
+ elem = kinds[0]
478
+ if needle["type"]["kind"] != elem:
479
+ if len(hay.elts) < 2:
480
+ self.fail(hay, "PS2002", "tuples need at least two fields")
481
+ self.fail(node, "PS2002", "in is lowered for str, bytes, list, and dict")
482
+ temp = self._temp("in")
483
+ self.locals[temp] = needle["type"]
484
+ self.prelude.append(_assign(temp, needle, span))
485
+ compares: list[ast.expr] = []
486
+ for elt in hay.elts:
487
+ left = ast.Name(id=temp, ctx=ast.Load())
488
+ ast.copy_location(left, node.left)
489
+ compare = ast.Compare(left=left, ops=[ast.Eq()], comparators=[elt])
490
+ ast.copy_location(compare, node)
491
+ compares.append(compare)
492
+ if len(compares) == 1:
493
+ test = self._expr(compares[0])
494
+ else:
495
+ chain = ast.BoolOp(op=ast.Or(), values=compares)
496
+ ast.copy_location(chain, node)
497
+ test = self._boolop(chain, span)
498
+ if negated:
499
+ return {"kind": "unary", "op": "not", "operand": test, "type": {"kind": "bool"}, "span": span}
500
+ return test
501
+
502
+ def _list(self, node: ast.List, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
503
+ if not node.elts:
504
+ if expect is None or expect["kind"] != "list":
505
+ self.fail(node, "PS1001", "an empty list needs a list annotation")
506
+ return {"kind": "list", "elems": [], "type": expect, "span": span}
507
+ elem_expect = expect["elem"] if expect and expect["kind"] == "list" else None
508
+ elems = [self._expr(elt, elem_expect) for elt in node.elts]
509
+ elem = elems[0]["type"]
510
+ if elem["kind"] == "bool":
511
+ self.fail(node, "PS2002", "list[bool] is not lowered")
512
+ for item in elems[1:]:
513
+ if not types_equal(item["type"], elem):
514
+ self.fail(node, "PS1003", "list elements have one type")
515
+ typ = {"kind": "list", "elem": elem}
516
+ if expect and expect["kind"] == "list":
517
+ self._expect(node, typ, expect, "list")
518
+ return {"kind": "list", "elems": elems, "type": typ, "span": span}
519
+
520
+ def _dict(self, node: ast.Dict, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
521
+ if not node.keys or any(key is None for key in node.keys):
522
+ if expect is None or expect["kind"] != "dict":
523
+ self.fail(node, "PS1001", "an empty dict needs a dict annotation")
524
+ if any(key is None for key in node.keys):
525
+ self.fail(node, "PS2002", "dict unpacking is not lowered")
526
+ return {"kind": "dict", "pairs": [], "type": expect, "span": span}
527
+ key_expect = expect["key"] if expect and expect["kind"] == "dict" else None
528
+ val_expect = expect["value"] if expect and expect["kind"] == "dict" else None
529
+ pairs = []
530
+ for key_node, val_node in zip(node.keys, node.values, strict=True):
531
+ assert key_node is not None
532
+ key = self._expr(key_node, key_expect)
533
+ value = self._expr(val_node, val_expect)
534
+ pairs.append({"key": key, "value": value})
535
+ key_t = pairs[0]["key"]["type"]
536
+ val_t = pairs[0]["value"]["type"]
537
+ if key_t["kind"] not in {"int", "str"}:
538
+ self.fail(node, "PS2002", "dict keys are int or str")
539
+ if val_t["kind"] == "bool":
540
+ self.fail(node, "PS2002", "dict values of bool are not lowered")
541
+ for pair in pairs[1:]:
542
+ if not types_equal(pair["key"]["type"], key_t) or not types_equal(pair["value"]["type"], val_t):
543
+ self.fail(node, "PS1003", "dict entries have one key type and one value type")
544
+ typ = {"kind": "dict", "key": key_t, "value": val_t}
545
+ if expect and expect["kind"] == "dict":
546
+ self._expect(node, typ, expect, "dict")
547
+ return {"kind": "dict", "pairs": pairs, "type": typ, "span": span}
548
+
549
+ def _subscript(self, node: ast.Subscript, span: dict[str, Any]) -> dict[str, Any]:
550
+ if isinstance(node.slice, ast.Slice):
551
+ return self._slice(node, span)
552
+ value = self._expr(node.value)
553
+ index = self._expr(node.slice)
554
+ kind = value["type"]["kind"]
555
+ if kind == "class" and self.classes.get(value["type"]["name"], {}).get("tuple"):
556
+ return self._tuple_subscript(node, value, span)
557
+ if kind == "list" and value["type"].get("set"):
558
+ self.fail(node, "PS2002", "a set has no subscript")
559
+ if kind == "list":
560
+ if index["type"]["kind"] != "int":
561
+ self.fail(node, "PS1003", "a list index is int")
562
+ result = value["type"]["elem"]
563
+ elif kind == "dict":
564
+ self._expect(node, index["type"], value["type"]["key"], "dict key")
565
+ result = value["type"]["value"]
566
+ elif kind == "str":
567
+ if index["type"]["kind"] != "int":
568
+ self.fail(node, "PS1003", "a str index is int")
569
+ result = {"kind": "str"}
570
+ elif kind == "bytes":
571
+ if index["type"]["kind"] != "int":
572
+ self.fail(node, "PS1003", "a bytes index is int")
573
+ result = {"kind": "int"}
574
+ elif kind == "class" and value["type"].get("name") == "Deque":
575
+ if index["type"]["kind"] != "int":
576
+ self.fail(node, "PS1003", "a deque index is int")
577
+ if "elem" not in value["type"]:
578
+ self.fail(node, "PS2002", "this deque has no element type")
579
+ result = value["type"]["elem"]
580
+ else:
581
+ self.fail(node, "PS2002", "subscript is lowered for list, dict, str, and bytes")
582
+ return {"kind": "subscript", "value": value, "index": index, "type": result, "span": span}
583
+
584
+ def _attribute(self, node: ast.Attribute, span: dict[str, Any]) -> dict[str, Any]:
585
+ if (
586
+ isinstance(node.value, ast.Call)
587
+ and node.attr == "name"
588
+ and self._is_mod(node.value, "threading", "current_thread")
589
+ ):
590
+ return self._abi_call(
591
+ "threading", "current_thread.name", [], {"kind": "str"}, "ps_island_thread_name", span
592
+ )
593
+ resolved = self._resolve_module(node)
594
+ if resolved is not None:
595
+ dotted, module, member, status = resolved
596
+ if status == "erased":
597
+ self.fail(node, "PS2002", f"{dotted} is a typing name and is not a value")
598
+ if status == "pkg":
599
+ item = self._pkg_item(node, module, member)
600
+ if item["kind"] == "const":
601
+ value = item["value"]
602
+ if value.get("kind") == "tuple-const":
603
+ return self._materialize_tuple(value["elems"], span)
604
+ return {**value, "span": span}
605
+ if item["kind"] == "class":
606
+ self.fail(node, "PS2002", f"class {dotted} is not a value")
607
+ self.fail(node, "PS2002", f"{dotted} is a function and is not a value")
608
+ if status == "queued":
609
+ self._queued(node, dotted)
610
+ if status == "value":
611
+ self.fail(node, "PS2002", f"module {dotted} is not a value in the static subset")
612
+ if module == "math" and member in MATH_CONSTANTS:
613
+ return {"kind": "const", "value": MATH_CONSTANTS[member], "type": {"kind": "float"}, "span": span}
614
+ constant = module_constant(module, member)
615
+ if constant is not None:
616
+ value, typ = constant
617
+ return {"kind": "const", "value": value, "type": typ, "span": span}
618
+ if module == "sys" and member == "argv":
619
+ return self._abi_call("sys", "argv", [], {"kind": "list", "elem": {"kind": "str"}}, "ps_argv_get", span)
620
+ if (module == "math" and member in MATH_FUNCTIONS) or wave_function(module, member) is not None:
621
+ self.fail(node, "PS2002", f"{dotted} is a function and is not a value")
622
+ if member in SPECIAL_FUNCTIONS.get(module, frozenset()) or member_callable(module, member):
623
+ self.fail(node, "PS2002", f"{dotted} is a function and is not a value")
624
+ self.fail(node, "PS2001", f"{dotted} is not in the lowered subset of {module}")
625
+ class_name = self.class_aliases.get(node.value.id, node.value.id) if isinstance(node.value, ast.Name) else None
626
+ if class_name is not None and class_name in self.classes:
627
+ info = self.classes[class_name]
628
+ if info.get("enum"):
629
+ member = info["members"].get(node.attr)
630
+ if member is None:
631
+ self.fail(node, "PS2001", f"{node.value.id}.{node.attr} is not an enum member")
632
+ return {"kind": "const", "value": str(member), "type": {"kind": "int"}, "span": span}
633
+ value = self._expr(node.value)
634
+ return self._lookup_attr(value, node.attr, span, node)
635
+
636
+ def _call(self, node: ast.Call, span: dict[str, Any], expect: dict[str, Any] | None = None) -> dict[str, Any]:
637
+ if isinstance(node.func, ast.Name):
638
+ return self._call_name(node, span, expect)
639
+ if isinstance(node.func, ast.Attribute):
640
+ return self._call_attr(node, span, expect)
641
+ self.fail(node, "PS2002", "this call is not lowered")
642
+ raise AssertionError("unreachable")
643
+
644
+ def _truthy(self, expr: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
645
+ kind = expr["type"]["kind"]
646
+ if kind == "bool":
647
+ return expr
648
+ if kind == "int":
649
+ return _compare("!=", expr, _const_int(0, span), span)
650
+ if kind == "float":
651
+ zero = {"kind": "const", "value": "0.0", "type": {"kind": "float"}, "span": span}
652
+ return _compare("!=", expr, zero, span)
653
+ if kind in {"str", "bytes", "list", "dict"}:
654
+ length = {
655
+ "kind": "call",
656
+ "callee": {"kind": "builtin", "name": "len"},
657
+ "args": [expr],
658
+ "type": {"kind": "int"},
659
+ "span": span,
660
+ }
661
+ return _compare("!=", length, _const_int(0, span), span)
662
+ self.fail(ast.Pass(), "PS2004", f"and/or on {kind} is not lowered")
663
+ raise AssertionError("unreachable")
664
+
665
+ def _mixed_cond_kind(self, kind: str) -> bool:
666
+ return kind == "bool" or kind in _TRUTHY
667
+
668
+ def _bool_operand(self, node: ast.expr, as_cond: bool) -> dict[str, Any]:
669
+ if as_cond and isinstance(node, ast.BoolOp):
670
+ return self._boolop(node, span_of(node, self.filename), as_cond=True)
671
+ return self._expr(node)
672
+
673
+ def _boolop(self, node: ast.BoolOp, span: dict[str, Any], *, as_cond: bool = False) -> dict[str, Any]:
674
+ """Same-type `and`/`or` still yields the operand.
675
+
676
+ A mixed `and`/`or` lowers as the truthiness of each side joined by bool
677
+ `and`/`or`, so the result is always a bool. That diverges from CPython,
678
+ where `a or b` returns the deciding operand with its own type; the IR
679
+ has no tagged union for `T_a | T_b` (epic E3c). The right side is
680
+ evaluated only when the left does not decide it.
681
+ """
682
+ op_and = isinstance(node.op, ast.And)
683
+ first = self._bool_operand(node.values[0], as_cond)
684
+ typ = first["type"]
685
+ if typ["kind"] not in _TRUTHY | {"bool", "float"}:
686
+ self.fail(node, "PS2004", f"and/or on {typ['kind']} is not lowered")
687
+ temp = self._temp("boolop")
688
+ self.locals[temp] = typ
689
+ self.prelude.append(_assign(temp, first, span))
690
+ for value_node in node.values[1:]:
691
+ saved = self.prelude
692
+ self.prelude = []
693
+ right = self._bool_operand(value_node, as_cond)
694
+ inner = self.prelude
695
+ self.prelude = saved
696
+ if not types_equal(right["type"], typ):
697
+ left_ok = self._mixed_cond_kind(typ["kind"])
698
+ right_ok = self._mixed_cond_kind(right["type"]["kind"])
699
+ if not left_ok or not right_ok:
700
+ self.fail(node, "PS2004", "and/or across two types is not lowered")
701
+ if typ["kind"] != "bool":
702
+ bool_temp = self._temp("cond")
703
+ self.locals[bool_temp] = {"kind": "bool"}
704
+ self.prelude.append(_assign(bool_temp, self._truthy(_name(temp, typ, span), span), span))
705
+ temp = bool_temp
706
+ typ = {"kind": "bool"}
707
+ inner.append(_assign(temp, self._truthy(right, span), span))
708
+ else:
709
+ inner.append(_assign(temp, right, span))
710
+ cond = self._truthy(_name(temp, typ, span), span)
711
+ if not op_and:
712
+ cond = {"kind": "unary", "op": "not", "operand": cond, "type": {"kind": "bool"}, "span": span}
713
+ self.prelude.append({"kind": "if", "cond": cond, "then": inner, "else": [], "span": span})
714
+ return _name(temp, typ, span)
715
+
716
+ def _ifexp(self, node: ast.IfExp, span: dict[str, Any]) -> dict[str, Any]:
717
+ """`a if cond else b` is the if statement BoolOp already uses.
718
+
719
+ The condition is whatever `if` already accepts. Both operands lower to
720
+ one type. Each operand's statements sit in its own branch, so the
721
+ untaken call does not run. A nested conditional is the same desugar.
722
+ """
723
+ cond = self._bool_cond(node.test)
724
+ saved = self.prelude
725
+ self.prelude = []
726
+ try:
727
+ body = self._expr(node.body)
728
+ body_pre = self.prelude
729
+ self.prelude = []
730
+ orelse = self._expr(node.orelse)
731
+ orelse_pre = self.prelude
732
+ finally:
733
+ self.prelude = saved
734
+ if not types_equal(body["type"], orelse["type"]):
735
+ self.fail(node, "PS2004", "conditional expression across two types is not lowered")
736
+ typ = body["type"]
737
+ then_body = self._ifexp_branch(body_pre, body, span)
738
+ else_body = self._ifexp_branch(orelse_pre, orelse, span)
739
+ if typ["kind"] == "none":
740
+ # None is not a register. The if still runs the taken operand.
741
+ self.prelude.append({"kind": "if", "cond": cond, "then": then_body, "else": else_body, "span": span})
742
+ return _const_none(span)
743
+ temp = self._temp("ifexp")
744
+ self.locals[temp] = typ
745
+ then_body.append(_assign(temp, body, span))
746
+ else_body.append(_assign(temp, orelse, span))
747
+ self.prelude.append({"kind": "if", "cond": cond, "then": then_body, "else": else_body, "span": span})
748
+ return _name(temp, typ, span)
749
+
750
+ def _ifexp_branch(
751
+ self, prelude: list[dict[str, Any]], value: dict[str, Any], span: dict[str, Any]
752
+ ) -> list[dict[str, Any]]:
753
+ if value["type"]["kind"] != "none":
754
+ return list(prelude)
755
+ body = list(prelude)
756
+ if value["kind"] != "const":
757
+ body.append({"kind": "expr", "expr": value, "span": span})
758
+ return body
759
+
760
+ def _joined(self, node: ast.JoinedStr, span: dict[str, Any]) -> dict[str, Any]:
761
+ parts: list[dict[str, Any]] = []
762
+ for value in node.values:
763
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
764
+ parts.append(self._const(value, span_of(value, self.filename)))
765
+ continue
766
+ if isinstance(value, ast.FormattedValue):
767
+ if value.conversion != -1 or value.format_spec is not None:
768
+ self.fail(value, "PS2002", "f-string format specs are not lowered")
769
+ parts.append(self._as_str(self._expr(value.value), value))
770
+ continue
771
+ self.fail(value, "PS2002", "f-strings are not lowered")
772
+ if self.function is None:
773
+ folded = _const_str_join(parts, span)
774
+ if folded is not None:
775
+ return folded
776
+ return _concat_str(parts, span)
777
+
778
+ def _as_str(self, expr: dict[str, Any], node: ast.AST) -> dict[str, Any]:
779
+ kind = expr["type"]["kind"]
780
+ span = expr["span"]
781
+ if kind == "str":
782
+ return expr
783
+ if kind in {"int", "float"}:
784
+ return {
785
+ "kind": "call",
786
+ "callee": {"kind": "builtin", "name": "str"},
787
+ "args": [expr],
788
+ "type": {"kind": "str"},
789
+ "span": span,
790
+ }
791
+ self.fail(node, "PS2002", f"formatting {_show(expr['type'])} is not lowered")
792
+ raise AssertionError("unreachable")
793
+
794
+ def _percent(self, node: ast.BinOp, span: dict[str, Any]) -> dict[str, Any]:
795
+ if not isinstance(node.left, ast.Constant) or not isinstance(node.left.value, str):
796
+ self.fail(node, "PS2004", "a format string must be a literal")
797
+ if any(ord(ch) > 255 for ch in node.left.value):
798
+ self.fail(node, "PS2005", "string literals outside Latin-1 are not lowered")
799
+ pieces = _percent_pieces(node.left.value)
800
+ if pieces is None:
801
+ self.fail(node, "PS2004", "% formatting is lowered for %s and %d")
802
+ specs = [kind for kind, _text in pieces if kind != "lit"]
803
+ if isinstance(node.right, ast.Tuple):
804
+ values = list(node.right.elts)
805
+ else:
806
+ values = [node.right]
807
+ if len(values) != len(specs):
808
+ self.fail(node, "PS2003", "format string and values disagree")
809
+ rendered: list[dict[str, Any]] = []
810
+ index = 0
811
+ for kind, text in pieces:
812
+ if kind == "lit":
813
+ if text:
814
+ rendered.append({"kind": "const", "value": text, "type": {"kind": "str"}, "span": span})
815
+ continue
816
+ expr = self._expr(values[index])
817
+ index += 1
818
+ if kind == "s":
819
+ if expr["type"]["kind"] != "str":
820
+ self.fail(node, "PS1003", "%s takes a str")
821
+ rendered.append(expr)
822
+ else:
823
+ if expr["type"]["kind"] != "int":
824
+ self.fail(node, "PS1003", "%d takes an int")
825
+ rendered.append(self._as_str(expr, node))
826
+ return _concat_str(rendered, span)
827
+
828
+ def _slice(self, node: ast.Subscript, span: dict[str, Any]) -> dict[str, Any]:
829
+ slc = node.slice
830
+ assert isinstance(slc, ast.Slice)
831
+ value = self._expr(node.value)
832
+ kind = value["type"]["kind"]
833
+ if kind == "list" and value["type"].get("set"):
834
+ self.fail(node, "PS2002", "a set has no subscript")
835
+ if kind not in {"str", "bytes", "list"}:
836
+ self.fail(node, "PS2002", "slices are lowered for str, bytes, and list")
837
+ start, has_start = self._slice_bound(slc.lower, span)
838
+ stop, has_stop = self._slice_bound(slc.upper, span)
839
+ step = self._slice_step(slc.step, span)
840
+ start = self._pin_slice_int(start, span)
841
+ stop = self._pin_slice_int(stop, span)
842
+ step = self._pin_slice_int(step, span)
843
+ symbol = "ps_seq_slice" if kind == "list" else "ps_str_slice"
844
+ result = value["type"] if kind != "list" else value["type"]
845
+ if kind == "str":
846
+ result = {"kind": "str"}
847
+ elif kind == "bytes":
848
+ result = {"kind": "bytes"}
849
+ return self._abi_call(
850
+ "builtins",
851
+ "slice",
852
+ [value, start, stop, _const_int(has_start, span), _const_int(has_stop, span), step],
853
+ result,
854
+ symbol,
855
+ span,
856
+ )
857
+
858
+ def _slice_bound(self, node: ast.expr | None, span: dict[str, Any]) -> tuple[dict[str, Any], int]:
859
+ if node is None:
860
+ return _const_int(0, span), 0
861
+ expr = self._expr(node, {"kind": "int"})
862
+ if expr["type"]["kind"] != "int":
863
+ self.fail(node, "PS1003", "a slice bound is int")
864
+ return expr, 1
865
+
866
+ def _slice_step(self, node: ast.expr | None, span: dict[str, Any]) -> dict[str, Any]:
867
+ """Any nonzero step lowers; the runtime walks backwards for step < 0.
868
+
869
+ Step 0 is the runtime's ValueError. The step expression is one operand,
870
+ so it runs once.
871
+ """
872
+ if node is None:
873
+ return _const_int(1, span)
874
+ expr = self._expr(node, {"kind": "int"})
875
+ if expr["type"]["kind"] != "int":
876
+ self.fail(node, "PS1003", "a slice step is int")
877
+ return expr
878
+
879
+ def _pin_slice_int(self, expr: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
880
+ """Store a slice operand once. A module call emits its arguments twice."""
881
+ if expr["kind"] in {"const", "name"}:
882
+ return expr
883
+ temp = self._temp("sl")
884
+ self.locals[temp] = {"kind": "int"}
885
+ self.prelude.append(_assign(temp, expr, span))
886
+ return _name(temp, {"kind": "int"}, span)