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
pyscriptc/restlower.py ADDED
@@ -0,0 +1,1008 @@
1
+ """Call lowering for the modules in `stdmore`.
2
+
3
+ The lowerer evaluates arguments and reports fences. This module decides which
4
+ calls are in the closed surface and which symbol implements them.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from pyscriptc.stdmore import (
13
+ CALL_FENCES,
14
+ CLASS_METHODS,
15
+ CTYPES,
16
+ FOR_ONLY,
17
+ OPERATORS,
18
+ PATH_ATTRS,
19
+ SIZEOF,
20
+ WITH_ONLY,
21
+ fixed_spec,
22
+ slot_size,
23
+ type_alias,
24
+ )
25
+
26
+ _CMP = {"eq": ast.Eq, "ne": ast.NotEq, "lt": ast.Lt, "le": ast.LtE, "gt": ast.Gt, "ge": ast.GtE}
27
+ _BIN = {"add": ast.Add, "sub": ast.Sub, "mul": ast.Mult, "floordiv": ast.FloorDiv}
28
+ _OPEN = {"r": 1, "w": 2, "rb": 3, "wb": 4, "a": 5, "ab": 6}
29
+
30
+
31
+ def rest_call(
32
+ lo: Any,
33
+ node: ast.Call,
34
+ span: dict[str, Any],
35
+ module: str,
36
+ attr: str,
37
+ expect: dict[str, Any] | None,
38
+ ) -> dict[str, Any]:
39
+ if module == "sys" and attr == "exit":
40
+ return _exit(lo, node, span)
41
+ if module == "importlib.resources" and attr == "as_file":
42
+ return _as_file_value(lo, node, span)
43
+ message = CALL_FENCES.get((module, attr))
44
+ if message:
45
+ lo.fail(node, "PS2001", message)
46
+ if (module, attr) in FOR_ONLY:
47
+ lo.fail(node, "PS2002", f"{module}.{attr} is only lowered as the iterator of a for")
48
+ if (module, attr) in WITH_ONLY:
49
+ lo.fail(node, "PS2002", f"{module}.{attr} is only lowered as a with context")
50
+ if module == "ctypes" and attr in CTYPES:
51
+ lo.fail(node, "PS2001", f"ctypes.{attr} is not callable in this subset")
52
+ spec = fixed_spec(module, attr)
53
+ if spec is not None:
54
+ return lo._fixed_call(node, span, module, attr, spec)
55
+ if module == "datetime" and attr == "date":
56
+ return _ints(lo, node, span, "datetime", "date", "ps_date_ymd", "Date", 3, ["year", "month", "day"])
57
+ if module == "datetime" and attr == "datetime":
58
+ return _ints(
59
+ lo,
60
+ node,
61
+ span,
62
+ "datetime",
63
+ "datetime",
64
+ "ps_dt_new",
65
+ "DateTime",
66
+ 3,
67
+ ["year", "month", "day", "hour", "minute", "second", "microsecond"],
68
+ )
69
+ if module == "datetime" and attr == "time":
70
+ return _ints(
71
+ lo, node, span, "datetime", "time", "ps_tod_new", "Time", 0, ["hour", "minute", "second", "microsecond"]
72
+ )
73
+ if module == "datetime" and attr == "timedelta":
74
+ return _ints(
75
+ lo,
76
+ node,
77
+ span,
78
+ "datetime",
79
+ "timedelta",
80
+ "ps_delta_new",
81
+ "TimeDelta",
82
+ 0,
83
+ ["days", "seconds", "microseconds"],
84
+ )
85
+ if module == "collections" and attr == "OrderedDict":
86
+ return _ordered(lo, node, span, expect)
87
+ lo._reject_keywords(node)
88
+ if module == "operator" and attr in OPERATORS:
89
+ return _operator(lo, node, span, attr)
90
+ if module == "bisect" and attr in {"bisect_left", "bisect_right"}:
91
+ return _bisect(lo, node, span, attr)
92
+ if module == "time" and attr == "sleep":
93
+ return _sleep(lo, node, span)
94
+ if module == "time" and attr in {"gmtime", "localtime"}:
95
+ return _clock(lo, node, span, attr)
96
+ if module == "collections" and attr == "deque":
97
+ return _deque(lo, node, span, expect)
98
+ if module == "functools" and attr == "reduce":
99
+ return _reduce(lo, node, span)
100
+ if module == "posixpath" and attr == "join":
101
+ return _join(lo, node, span)
102
+ if module == "pathlib" and attr == "Path":
103
+ return _one(
104
+ lo, node, span, "pathlib", "Path", "ps_path_new", {"kind": "str"}, {"kind": "class", "name": "Path"}
105
+ )
106
+ if module == "io" and attr == "StringIO":
107
+ return _buffer(lo, node, span, "io", "StringIO", "ps_io_string", "str", "TextIO")
108
+ if module == "io" and attr == "BytesIO":
109
+ return _buffer(lo, node, span, "io", "BytesIO", "ps_io_bytes", "bytes", "BytesIO")
110
+ if module == "ctypes" and attr == "sizeof":
111
+ return _sizeof(lo, node, span)
112
+ if module == "hmac" and attr in {"digest", "new"}:
113
+ return _hmac(lo, node, span, attr)
114
+ if module == "hmac" and attr == "compare_digest":
115
+ return _hmac_compare(lo, node, span)
116
+ if module == "unicodedata" and attr == "name":
117
+ return _unicode_name(lo, node, span)
118
+ if module == "importlib.resources" and attr == "files":
119
+ return _resource_files(lo, node, span)
120
+ if module == "atexit" and attr == "register":
121
+ return _atexit_register(lo, node, span)
122
+ lo.fail(node, "PS2001", f"{module}.{attr} is not in the lowered subset of {module}")
123
+ raise AssertionError("unreachable")
124
+
125
+
126
+ def builtin_call(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any] | None:
127
+ assert isinstance(node.func, ast.Name)
128
+ name = node.func.id
129
+ if name == "open":
130
+ return _open(lo, node, span)
131
+ if name == "print":
132
+ return _print(lo, node, span)
133
+ if node.keywords and name in {
134
+ "isinstance",
135
+ "getattr",
136
+ "hasattr",
137
+ "abs",
138
+ "min",
139
+ "max",
140
+ "sum",
141
+ "ord",
142
+ "chr",
143
+ "round",
144
+ }:
145
+ lo._reject_keywords(node)
146
+ if name == "isinstance":
147
+ return _isinstance(lo, node, span)
148
+ if name == "getattr":
149
+ return _getattr(lo, node, span)
150
+ if name == "hasattr":
151
+ return _hasattr(lo, node, span)
152
+ if name == "abs":
153
+ return _abs(lo, node, span)
154
+ if name in {"min", "max"}:
155
+ return _minmax(lo, node, span, name)
156
+ if name == "sum":
157
+ return _sum(lo, node, span)
158
+ if name == "sorted":
159
+ return _sorted(lo, node, span)
160
+ if name == "ord":
161
+ return _ord(lo, node, span)
162
+ if name == "chr":
163
+ return _chr(lo, node, span)
164
+ if name == "round":
165
+ return _round(lo, node, span)
166
+ return None
167
+
168
+
169
+ def _const_int(value: int, span: dict[str, Any]) -> dict[str, Any]:
170
+ return {"kind": "const", "value": str(value), "type": {"kind": "int"}, "span": span}
171
+
172
+
173
+ _PRINT_KINDS = {"str", "int", "float", "bool", "bytes"}
174
+
175
+
176
+ def _print(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
177
+ """`print` lowers to the builtin of the same name; the emitter writes stdout.
178
+
179
+ Subset: positional values of the compiler's scalar set (str, int, float,
180
+ bool, bytes), `sep=`/`end=` str keywords; `file=`/`flush=` stay fenced.
181
+ The keyword exprs ride on the call dict so the emitter can splice them.
182
+ """
183
+ extras: dict[str, Any] = {}
184
+ for kw in node.keywords:
185
+ if kw.arg is None:
186
+ lo.fail(kw, "PS2002", "**kwargs are not lowered")
187
+ if kw.arg not in {"sep", "end"}:
188
+ lo.fail(
189
+ kw,
190
+ "PS2003",
191
+ f"print keyword argument {kw.arg} is not lowered; print supports sep= and end= only",
192
+ )
193
+ value = lo._expr(kw.value)
194
+ if value["type"]["kind"] != "str":
195
+ lo.fail(kw, "PS1003", f"print {kw.arg}= takes a str")
196
+ extras[kw.arg] = value
197
+ if any(isinstance(arg, ast.Starred) for arg in node.args):
198
+ lo.fail(node, "PS2002", "starred calls are not lowered")
199
+ args = [lo._expr(arg) for arg in node.args]
200
+ for arg in args:
201
+ if arg["type"]["kind"] not in _PRINT_KINDS:
202
+ lo.fail(
203
+ node,
204
+ "PS2003",
205
+ f"print is lowered for str, int, float, bool, and bytes values, not {arg['type']['kind']};"
206
+ " wrap it in str(...)",
207
+ )
208
+ expr = {
209
+ "kind": "call",
210
+ "callee": {"kind": "builtin", "name": "print"},
211
+ "args": args,
212
+ "type": {"kind": "none"},
213
+ "span": span,
214
+ }
215
+ expr.update(extras)
216
+ return expr
217
+
218
+
219
+ def _const_text(value: str, kind: str, span: dict[str, Any]) -> dict[str, Any]:
220
+ return {"kind": "const", "value": value, "type": {"kind": kind}, "span": span}
221
+
222
+
223
+ def _class(name: str) -> dict[str, Any]:
224
+ return {"kind": "class", "name": name}
225
+
226
+
227
+ def _ints(
228
+ lo: Any,
229
+ node: ast.Call,
230
+ span: dict[str, Any],
231
+ module: str,
232
+ attr: str,
233
+ symbol: str,
234
+ cls: str,
235
+ minimum: int,
236
+ names: list[str],
237
+ ) -> dict[str, Any]:
238
+ params: list[dict[str, Any]] = []
239
+ for index, name in enumerate(names):
240
+ param: dict[str, Any] = {"name": name, "type": {"kind": "int"}}
241
+ if index >= minimum:
242
+ param["default"] = _const_int(0, span)
243
+ params.append(param)
244
+ args = lo._fill_args(node, params, f"{module}.{attr}")
245
+ return lo._abi_call(module, attr, args, _class(cls), symbol, span)
246
+
247
+
248
+ def _bisect(lo: Any, node: ast.Call, span: dict[str, Any], attr: str) -> dict[str, Any]:
249
+ lo._reject_keywords(node)
250
+ if len(node.args) != 2:
251
+ lo.fail(node, "PS2003", f"bisect.{attr} expects 2 argument(s)")
252
+ seq = lo._expr(node.args[0])
253
+ key = lo._expr(node.args[1])
254
+ if seq["type"]["kind"] != "list" or seq["type"]["elem"]["kind"] != "int":
255
+ lo.fail(node, "PS1003", f"bisect.{attr} takes a list[int]")
256
+ if key["type"]["kind"] != "int":
257
+ lo.fail(node, "PS1003", f"bisect.{attr} key is int")
258
+ symbol = "ps_bisect_left" if attr == "bisect_left" else "ps_bisect_right"
259
+ return lo._abi_call("bisect", attr, [seq, key], {"kind": "int"}, symbol, span)
260
+
261
+
262
+ def _as_float(lo: Any, node: ast.AST, arg: dict[str, Any], span: dict[str, Any], what: str) -> dict[str, Any]:
263
+ if arg["type"]["kind"] == "int":
264
+ return {"kind": "cast", "op": "int_to_float", "value": arg, "type": {"kind": "float"}, "span": span}
265
+ if arg["type"]["kind"] != "float":
266
+ lo.fail(node, "PS1003", f"{what} argument is {arg['type']['kind']}, expected float")
267
+ return arg
268
+
269
+
270
+ def _exit(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
271
+ if len(node.args) != 1:
272
+ lo.fail(node, "PS2003", "sys.exit expects 1 argument(s)")
273
+ arg = lo._expr(node.args[0])
274
+ if arg["type"]["kind"] != "int":
275
+ lo.fail(node, "PS1003", "sys.exit argument is an int")
276
+ return lo._abi_call("sys", "exit", [arg], {"kind": "none"}, "exit", span)
277
+
278
+
279
+ def _sleep(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
280
+ if len(node.args) != 1:
281
+ lo.fail(node, "PS2003", "time.sleep expects 1 argument(s)")
282
+ arg = _as_float(lo, node, lo._expr(node.args[0]), span, "time.sleep")
283
+ return lo._abi_call("time", "sleep", [arg], {"kind": "none"}, "ps_time_sleep", span)
284
+
285
+
286
+ def _clock(lo: Any, node: ast.Call, span: dict[str, Any], attr: str) -> dict[str, Any]:
287
+ if len(node.args) > 1:
288
+ lo.fail(node, "PS2003", f"time.{attr} expects 0 or 1 argument(s)")
289
+ if node.args:
290
+ arg = _as_float(lo, node, lo._expr(node.args[0]), span, f"time.{attr}")
291
+ else:
292
+ arg = lo._abi_call("time", "time", [], {"kind": "float"}, "ps_time_time", span)
293
+ symbol = "ps_time_gm" if attr == "gmtime" else "ps_time_local"
294
+ return lo._abi_call("time", attr, [arg], _class("StructTime"), symbol, span)
295
+
296
+
297
+ def _deque(lo: Any, node: ast.Call, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
298
+ if len(node.args) > 1:
299
+ lo.fail(node, "PS2003", "deque expects 0 or 1 argument(s)")
300
+ if not node.args:
301
+ if expect is None or expect.get("name") != "Deque" or "elem" not in expect:
302
+ lo.fail(node, "PS1001", "deque() needs a deque annotation")
303
+ elem = expect["elem"]
304
+ if elem["kind"] == "bool":
305
+ lo.fail(node, "PS2002", "list[bool] is not lowered")
306
+ size = _const_int(slot_size(elem), span)
307
+ return lo._abi_call("collections", "deque", [size], expect, "ps_deque_empty", span)
308
+ arg = lo._expr(node.args[0])
309
+ if arg["type"]["kind"] != "list":
310
+ lo.fail(node, "PS1003", "deque() takes a list")
311
+ elem = arg["type"]["elem"]
312
+ if elem["kind"] == "bool":
313
+ lo.fail(node, "PS2002", "list[bool] is not lowered")
314
+ typ = {"kind": "class", "name": "Deque", "elem": elem}
315
+ if expect is not None and expect.get("name") == "Deque":
316
+ lo._expect(node, typ, expect, "deque")
317
+ size = _const_int(slot_size(elem), span)
318
+ return lo._abi_call("collections", "deque", [size, arg], typ, "ps_deque_from", span)
319
+
320
+
321
+ def _ordered(lo: Any, node: ast.Call, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
322
+ """`OrderedDict()` with no arguments is the empty dict the compiler already builds.
323
+
324
+ A positional iterable and keyword arguments stay fenced. The `ordered` mark
325
+ is not part of the type key, so an annotated `dict` assignment still matches.
326
+ """
327
+ if node.args:
328
+ lo.fail(node, "PS2001", "an OrderedDict iterable is not lowered")
329
+ if node.keywords:
330
+ lo.fail(node, "PS2001", "OrderedDict keyword arguments are not lowered")
331
+ if expect is not None and expect["kind"] != "dict":
332
+ lo.fail(node, "PS1001", "OrderedDict needs a dict annotation")
333
+ base = expect if expect is not None else {"kind": "dict", "key": {"kind": "str"}, "value": {"kind": "int"}}
334
+ return {"kind": "dict", "pairs": [], "type": {**base, "ordered": True}, "span": span}
335
+
336
+
337
+ def _reduce(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
338
+ """`functools.reduce(operator.add, xs)` is a loop of the existing int `+`.
339
+
340
+ A direct function name stays the existing call. `operator.add` has no
341
+ function value, so that spelling stores the list once and adds each later
342
+ element. An initializer on `operator.add` stays fenced. An empty list
343
+ raises ValueError; this subset has no TypeError.
344
+ """
345
+ lo._reject_keywords(node)
346
+ if len(node.args) not in {2, 3}:
347
+ lo.fail(node, "PS2003", "functools.reduce expects 2 or 3 argument(s)")
348
+ fn = node.args[0]
349
+ if _is_operator_add(lo, fn):
350
+ if len(node.args) == 3:
351
+ lo.fail(node, "PS2002", "functools.reduce initializer is not lowered")
352
+ return _reduce_add(lo, node, span)
353
+ if not isinstance(fn, ast.Name) or fn.id not in lo.sigs:
354
+ lo.fail(node, "PS2001", "functools.reduce takes a direct function name")
355
+ params, ret = lo.sigs[fn.id]
356
+ if len(params) != 2 or not _same(params[0]["type"], params[1]["type"]) or not _same(params[0]["type"], ret):
357
+ lo.fail(node, "PS2001", "functools.reduce function is (T, T) -> T")
358
+ seq = lo._expr(node.args[1])
359
+ if seq["type"]["kind"] != "list" or not _same(seq["type"]["elem"], ret):
360
+ lo.fail(node, "PS1003", "functools.reduce takes a list of the function's type")
361
+ args = [seq]
362
+ if len(node.args) == 3:
363
+ init = lo._expr(node.args[2], ret)
364
+ lo._expect(node, init["type"], ret, "functools.reduce initializer")
365
+ args.append(init)
366
+ expr = lo._abi_call("functools", "reduce", args, ret, "", span)
367
+ expr.pop("symbol", None)
368
+ expr["fn"] = fn.id
369
+ return expr
370
+
371
+
372
+ def _reduce_add(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
373
+ seq = lo._expr(node.args[1])
374
+ if seq["type"]["kind"] != "list" or seq["type"].get("set") or seq["type"]["elem"]["kind"] != "int":
375
+ lo.fail(node, "PS1003", "functools.reduce takes a list of int")
376
+ int_t = {"kind": "int"}
377
+ list_t = seq["type"]
378
+ src = lo._temp("reds")
379
+ acc = lo._temp("reda")
380
+ idx = lo._temp("redi")
381
+ length = lo._temp("redn")
382
+ lo.locals[src] = list_t
383
+ lo.locals[acc] = int_t
384
+ lo.locals[idx] = int_t
385
+ lo.locals[length] = int_t
386
+ src_n = _nm(src, list_t, span)
387
+ acc_n = _nm(acc, int_t, span)
388
+ idx_n = _nm(idx, int_t, span)
389
+ length_n = _nm(length, int_t, span)
390
+ item = {
391
+ "kind": "subscript",
392
+ "value": src_n,
393
+ "index": idx_n,
394
+ "type": int_t,
395
+ "span": span,
396
+ }
397
+ lo.prelude.extend(
398
+ [
399
+ _asg(src, seq, span),
400
+ _asg(
401
+ length,
402
+ {
403
+ "kind": "call",
404
+ "callee": {"kind": "builtin", "name": "len"},
405
+ "args": [src_n],
406
+ "type": int_t,
407
+ "span": span,
408
+ },
409
+ span,
410
+ ),
411
+ {
412
+ "kind": "if",
413
+ "cond": _cmp("==", length_n, _int(0, span), span),
414
+ "then": [{"kind": "raise", "exc": "ValueError", "message": None, "span": span}],
415
+ "else": [],
416
+ "span": span,
417
+ },
418
+ _asg(
419
+ acc,
420
+ {"kind": "subscript", "value": src_n, "index": _int(0, span), "type": int_t, "span": span},
421
+ span,
422
+ ),
423
+ _asg(idx, _int(1, span), span),
424
+ {
425
+ "kind": "while",
426
+ "cond": _cmp("<", idx_n, length_n, span),
427
+ "body": [
428
+ _asg(
429
+ acc,
430
+ {
431
+ "kind": "binary",
432
+ "op": "+",
433
+ "left": acc_n,
434
+ "right": item,
435
+ "type": int_t,
436
+ "span": span,
437
+ },
438
+ span,
439
+ ),
440
+ _asg(
441
+ idx,
442
+ {
443
+ "kind": "binary",
444
+ "op": "+",
445
+ "left": idx_n,
446
+ "right": _int(1, span),
447
+ "type": int_t,
448
+ "span": span,
449
+ },
450
+ span,
451
+ ),
452
+ ],
453
+ "span": span,
454
+ },
455
+ ]
456
+ )
457
+ return _nm(acc, int_t, span)
458
+
459
+
460
+ def _is_operator_add(lo: Any, node: ast.expr) -> bool:
461
+ if isinstance(node, ast.Attribute):
462
+ resolved = lo._resolve_module(node)
463
+ return resolved is not None and resolved[1] == "operator" and resolved[2] == "add"
464
+ if isinstance(node, ast.Name):
465
+ binding = lo.bindings.get(node.id)
466
+ return binding is not None and binding.module == "operator" and binding.attr == "add"
467
+ return False
468
+
469
+
470
+ def _nm(name: str, typ: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
471
+ return {"kind": "name", "id": name, "type": typ, "span": span}
472
+
473
+
474
+ def _int(value: int, span: dict[str, Any]) -> dict[str, Any]:
475
+ return {"kind": "const", "value": str(value), "type": {"kind": "int"}, "span": span}
476
+
477
+
478
+ def _asg(target: str, value: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
479
+ return {"kind": "assign", "target": target, "value": value, "span": span}
480
+
481
+
482
+ def _cmp(op: str, left: dict[str, Any], right: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
483
+ return {"kind": "compare", "op": op, "left": left, "right": right, "type": {"kind": "bool"}, "span": span}
484
+
485
+
486
+ def _join(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
487
+ if not 1 <= len(node.args) <= 8:
488
+ lo.fail(node, "PS2003", "posixpath.join expects 1 to 8 argument(s)")
489
+ args = []
490
+ for arg_node in node.args:
491
+ arg = lo._expr(arg_node)
492
+ if arg["type"]["kind"] != "str":
493
+ lo.fail(node, "PS1003", "posixpath.join arguments are str")
494
+ args.append(arg)
495
+ return lo._abi_call("posixpath", "join", args, {"kind": "str"}, "ps_path_join", span)
496
+
497
+
498
+ def _one(
499
+ lo: Any,
500
+ node: ast.Call,
501
+ span: dict[str, Any],
502
+ module: str,
503
+ attr: str,
504
+ symbol: str,
505
+ want: dict[str, Any],
506
+ ret: dict[str, Any],
507
+ ) -> dict[str, Any]:
508
+ if len(node.args) != 1:
509
+ lo.fail(node, "PS2003", f"{module}.{attr} expects 1 argument(s)")
510
+ arg = lo._expr(node.args[0])
511
+ lo._expect(node, arg["type"], want, f"{module}.{attr}")
512
+ return lo._abi_call(module, attr, [arg], ret, symbol, span)
513
+
514
+
515
+ def _buffer(
516
+ lo: Any, node: ast.Call, span: dict[str, Any], module: str, attr: str, symbol: str, kind: str, cls: str
517
+ ) -> dict[str, Any]:
518
+ if len(node.args) > 1:
519
+ lo.fail(node, "PS2003", f"{module}.{attr} expects 0 or 1 argument(s)")
520
+ if node.args:
521
+ arg = lo._expr(node.args[0])
522
+ if arg["type"]["kind"] != kind:
523
+ lo.fail(node, "PS1003", f"{module}.{attr} argument is {kind}")
524
+ else:
525
+ arg = _const_text("", kind, span)
526
+ return lo._abi_call(module, attr, [arg], _class(cls), symbol, span)
527
+
528
+
529
+ def _sizeof(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
530
+ if len(node.args) != 1:
531
+ lo.fail(node, "PS2003", "ctypes.sizeof expects 1 argument(s)")
532
+ name = _ctype(lo, node.args[0])
533
+ if name is None:
534
+ lo.fail(node, "PS2001", "ctypes.sizeof argument is not a lowered ctype")
535
+ return {"kind": "const", "value": str(SIZEOF[name]), "type": {"kind": "int"}, "span": span}
536
+
537
+
538
+ def _ctype(lo: Any, node: ast.expr) -> str | None:
539
+ if isinstance(node, ast.Attribute):
540
+ resolved = lo._resolve_module(node)
541
+ if resolved is not None and resolved[1] == "ctypes" and resolved[2] in SIZEOF:
542
+ return str(resolved[2])
543
+ if isinstance(node, ast.Name):
544
+ binding = lo.bindings.get(node.id)
545
+ if binding is not None and binding.module == "ctypes" and binding.attr in SIZEOF:
546
+ return str(binding.attr)
547
+ return None
548
+
549
+
550
+ def _operator(lo: Any, node: ast.Call, span: dict[str, Any], attr: str) -> dict[str, Any]:
551
+ if attr == "neg":
552
+ if len(node.args) != 1:
553
+ lo.fail(node, "PS2003", "operator.neg expects 1 argument(s)")
554
+ fake = ast.UnaryOp(op=ast.USub(), operand=node.args[0])
555
+ ast.copy_location(fake, node)
556
+ return lo._unary(fake, span)
557
+ if attr in _CMP:
558
+ if len(node.args) != 2:
559
+ lo.fail(node, "PS2003", f"operator.{attr} expects 2 argument(s)")
560
+ fake = ast.Compare(left=node.args[0], ops=[_CMP[attr]()], comparators=[node.args[1]])
561
+ ast.copy_location(fake, node)
562
+ return lo._compare(fake, span)
563
+ if attr in _BIN:
564
+ if len(node.args) != 2:
565
+ lo.fail(node, "PS2003", f"operator.{attr} expects 2 argument(s)")
566
+ fake = ast.BinOp(left=node.args[0], op=_BIN[attr](), right=node.args[1])
567
+ ast.copy_location(fake, node)
568
+ return lo._binary(fake, span)
569
+ if attr == "concat":
570
+ if len(node.args) != 2:
571
+ lo.fail(node, "PS2003", "operator.concat expects 2 argument(s)")
572
+ left = lo._expr(node.args[0])
573
+ right = lo._expr(node.args[1])
574
+ if left["type"]["kind"] == right["type"]["kind"] and left["type"]["kind"] in {"str", "bytes"}:
575
+ return {
576
+ "kind": "binary",
577
+ "op": "+",
578
+ "left": left,
579
+ "right": right,
580
+ "type": left["type"],
581
+ "span": span,
582
+ }
583
+ lo.fail(node, "PS2004", "operator.concat takes str or bytes")
584
+ lo.fail(node, "PS2001", f"operator.{attr} is not in the lowered subset of operator")
585
+ raise AssertionError("unreachable")
586
+
587
+
588
+ def _open(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
589
+ slots = lo._place_nodes(node, ["file", "mode"])
590
+ if slots[0] is None:
591
+ lo.fail(node, "PS2003", "open expects a path and an optional mode literal")
592
+ path = lo._expr(slots[0])
593
+ if path["type"]["kind"] != "str":
594
+ lo.fail(node, "PS1003", "open path is str")
595
+ mode = "r"
596
+ if slots[1] is not None:
597
+ mode_node = slots[1]
598
+ if not isinstance(mode_node, ast.Constant) or not isinstance(mode_node.value, str):
599
+ lo.fail(node, "PS2001", "open mode must be a string literal")
600
+ mode = mode_node.value
601
+ if mode not in _OPEN:
602
+ lo.fail(node, "PS2001", "open mode is not lowered")
603
+ cls = "BytesIO" if "b" in mode else "TextIO"
604
+ code = _const_int(_OPEN[mode], span)
605
+ return lo._abi_call("io", "open", [path, code], _class(cls), "ps_io_open", span)
606
+
607
+
608
+ def _isinstance(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
609
+ if len(node.args) != 2:
610
+ lo.fail(node, "PS2003", "isinstance expects 2 argument(s)")
611
+ obj = lo._expr(node.args[0])
612
+ want = lo._isinstance_type(node.args[1])
613
+ flag = lo._isinstance_holds(obj["type"], want)
614
+ return {"kind": "const", "value": flag, "type": {"kind": "bool"}, "span": span}
615
+
616
+
617
+ def _getattr(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
618
+ if len(node.args) != 2:
619
+ lo.fail(node, "PS2003", "getattr expects 2 argument(s)")
620
+ obj = lo._expr(node.args[0])
621
+ name = _literal_name(lo, node)
622
+ return lo._lookup_attr(obj, name, span, node)
623
+
624
+
625
+ def _hasattr(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
626
+ if len(node.args) != 2:
627
+ lo.fail(node, "PS2003", "hasattr expects 2 argument(s)")
628
+ obj = lo._expr(node.args[0])
629
+ name = _literal_name(lo, node)
630
+ return {"kind": "const", "value": lo._has_attr(obj, name), "type": {"kind": "bool"}, "span": span}
631
+
632
+
633
+ def _literal_name(lo: Any, node: ast.Call) -> str:
634
+ arg = node.args[1]
635
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and arg.value.isidentifier():
636
+ if any(ord(ch) > 127 for ch in arg.value):
637
+ lo.fail(node, "PS2005", "non-ASCII string literals are not lowered")
638
+ return arg.value
639
+ lo.fail(node, "PS2001", "the attribute name is a string literal")
640
+ raise AssertionError("unreachable")
641
+
642
+
643
+ def _abs(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
644
+ if len(node.args) != 1:
645
+ lo.fail(node, "PS2003", "abs expects 1 argument(s)")
646
+ arg = lo._expr(node.args[0])
647
+ kind = arg["type"]["kind"]
648
+ if kind == "int":
649
+ return lo._abi_call("builtins", "abs", [arg], {"kind": "int"}, "ps_abs_i64", span)
650
+ if kind == "float":
651
+ return lo._abi_call("builtins", "abs", [arg], {"kind": "float"}, "ps_abs_f64", span)
652
+ lo.fail(node, "PS2003", "abs is lowered for int and float")
653
+ raise AssertionError("unreachable")
654
+
655
+
656
+ def _minmax(lo: Any, node: ast.Call, span: dict[str, Any], name: str) -> dict[str, Any]:
657
+ if len(node.args) != 2:
658
+ lo.fail(node, "PS2003", f"{name} expects 2 argument(s)")
659
+ left = lo._expr(node.args[0])
660
+ right = lo._expr(node.args[1])
661
+ if left["type"]["kind"] != right["type"]["kind"] or left["type"]["kind"] not in {"int", "float", "str", "bytes"}:
662
+ lo.fail(node, "PS2003", f"{name} takes two values of the same scalar type")
663
+ return {
664
+ "kind": "call",
665
+ "callee": {"kind": "builtin", "name": name},
666
+ "args": [left, right],
667
+ "type": left["type"],
668
+ "span": span,
669
+ }
670
+
671
+
672
+ def _sum(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
673
+ if len(node.args) != 1:
674
+ lo.fail(node, "PS2003", "sum expects 1 argument(s)")
675
+ arg = lo._expr(node.args[0])
676
+ if arg["type"]["kind"] != "list" or arg["type"]["elem"]["kind"] not in {"int", "float"}:
677
+ lo.fail(node, "PS2003", "sum takes a list of int or float")
678
+ kind = arg["type"]["elem"]["kind"]
679
+ symbol = "ps_sum_i64" if kind == "int" else "ps_sum_f64"
680
+ return lo._abi_call("builtins", "sum", [arg], {"kind": kind}, symbol, span)
681
+
682
+
683
+ _SORT_ELEM = {"int", "float", "str", "bytes"}
684
+
685
+
686
+ def _sorted(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
687
+ if len(node.args) != 1:
688
+ lo.fail(node, "PS2003", "sorted expects 1 argument(s)")
689
+ if node.keywords:
690
+ if any(kw.arg is None for kw in node.keywords):
691
+ lo.fail(node, "PS2002", "**kwargs are not lowered")
692
+ if len(node.keywords) == 1 and node.keywords[0].arg == "key":
693
+ lam = node.keywords[0].value
694
+ if not isinstance(lam, ast.Lambda):
695
+ lo.fail(lam, "PS2003", "sorted key= takes an inline lambda")
696
+ args = lam.args
697
+ params = args.posonlyargs + args.args
698
+ variadic = args.vararg or args.kwarg or args.kwonlyargs or args.defaults or any(args.kw_defaults)
699
+ if len(params) != 1 or variadic:
700
+ lo.fail(lam, "PS2003", "a sorted key= lambda takes one positional parameter")
701
+ for sub in ast.walk(lam.body):
702
+ if isinstance(sub, (ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
703
+ lo.fail(lam, "PS2003", "a sorted key= lambda is a plain expression")
704
+
705
+ arg = lo._expr(node.args[0])
706
+ kind = arg["type"]["kind"]
707
+ if kind == "dict":
708
+ elem = arg["type"]["key"]
709
+ elems_expr = "{s}.keys()"
710
+ elif kind == "list":
711
+ elem = arg["type"]["elem"]
712
+ elems_expr = "{s}"
713
+ else:
714
+ lo.fail(node.args[0], "PS2003", "sorted key= takes a list or dict")
715
+ raise AssertionError("unreachable")
716
+ if elem["kind"] not in _SORT_ELEM:
717
+ lo.fail(node.args[0], "PS2003", "sorted key= iterates int, float, str, or bytes elements")
718
+
719
+ w = params[0].arg
720
+ fields = lam.body.elts if isinstance(lam.body, ast.Tuple) else [lam.body]
721
+
722
+ saved_w = lo.locals.get(w)
723
+ lo.locals[w] = elem
724
+ probes = [lo._expr(field) for field in fields]
725
+ if saved_w is None:
726
+ del lo.locals[w]
727
+ else:
728
+ lo.locals[w] = saved_w
729
+ if any(p["type"]["kind"] not in _SORT_ELEM for p in probes):
730
+ lo.fail(lam, "PS2003", "a sorted key= lambda returns scalars or a tuple of scalars")
731
+
732
+ def temp(hint: str) -> str:
733
+ return lo._temp(hint).replace(".", "_")
734
+
735
+ s = temp("seq")
736
+ e = temp("elems")
737
+ keys = [temp(f"k{i}") for i in range(len(fields))]
738
+ n = temp("n")
739
+ idx = temp("idx")
740
+ i = temp("i")
741
+ j = temp("j")
742
+ v = temp("v")
743
+ out = temp("out")
744
+
745
+ lo.locals[s] = arg["type"]
746
+ lo.prelude.append({"kind": "assign", "target": s, "value": arg, "span": span})
747
+ lo.locals[e] = {"kind": "list", "elem": elem}
748
+ for p, k in zip(probes, keys, strict=True):
749
+ lo.locals[k] = {"kind": "list", "elem": p["type"]}
750
+ for t in (n, i, j, v):
751
+ lo.locals[t] = {"kind": "int"}
752
+ lo.locals[idx] = {"kind": "list", "elem": {"kind": "int"}}
753
+ lo.locals[out] = {"kind": "list", "elem": elem}
754
+
755
+ def less(field: int) -> str:
756
+ jb = f"{idx}[{j} - 1]"
757
+ lt = f"{keys[field]}[{v}] < {keys[field]}[{jb}]"
758
+ if field == len(keys) - 1:
759
+ return lt
760
+ return f"{lt} or ({keys[field]}[{v}] == {keys[field]}[{jb}] and ({less(field + 1)}))"
761
+
762
+ # The block reuses the lambda's own parameter name as the loop
763
+ # variable; a pre-existing local is pinned and restored after.
764
+ lines = [f"{e} = {elems_expr.format(s=s)}"]
765
+ if saved_w is not None:
766
+ save_w = temp("save")
767
+ lo.locals[save_w] = elem
768
+ lines.append(f"{save_w} = {w}")
769
+ for k in keys:
770
+ lines.append(f"{k} = []")
771
+ lines.append(f"for {w} in {e}:")
772
+ for pos, k in enumerate(keys):
773
+ lines.append(f" {k}.append({ast.unparse(fields[pos])})")
774
+ if saved_w is not None:
775
+ lines.append(f"{w} = {save_w}")
776
+ lines += [
777
+ f"{n} = len({keys[0]})",
778
+ f"{idx} = []",
779
+ f"{i} = 0",
780
+ f"while {i} < {n}:",
781
+ f" {idx}.append({i})",
782
+ f" {i} = {i} + 1",
783
+ f"{i} = 1",
784
+ f"while {i} < {n}:",
785
+ f" {v} = {idx}[{i}]",
786
+ f" {j} = {i}",
787
+ f" while {j} > 0 and ({less(0)}):",
788
+ f" {idx}[{j}] = {idx}[{j} - 1]",
789
+ f" {j} = {j} - 1",
790
+ f" {idx}[{j}] = {v}",
791
+ f" {i} = {i} + 1",
792
+ f"{out} = []",
793
+ f"{i} = 0",
794
+ f"while {i} < {n}:",
795
+ f" {out}.append({e}[{idx}[{i}]])",
796
+ f" {i} = {i} + 1",
797
+ ]
798
+ for stmt in ast.parse("\n".join(lines)).body:
799
+ for sub in ast.walk(stmt):
800
+ sub.lineno, sub.col_offset = node.lineno, node.col_offset
801
+ sub.end_lineno, sub.end_col_offset = node.end_lineno, node.end_col_offset
802
+ lo.prelude.extend(lo._body([stmt]))
803
+ return {"kind": "name", "id": out, "type": {"kind": "list", "elem": elem}, "span": span}
804
+ lo._reject_keywords(node)
805
+ arg = lo._expr(node.args[0])
806
+ if arg["type"]["kind"] != "list" or arg["type"]["elem"]["kind"] not in _SORT_ELEM:
807
+ lo.fail(node, "PS2003", "sorted takes a list of int, float, str, or bytes")
808
+ folded = _const_sorted(arg, span)
809
+ if folded is not None:
810
+ return folded
811
+ kind = arg["type"]["elem"]["kind"]
812
+ symbol = {"int": "ps_sorted_i64", "float": "ps_sorted_f64", "str": "ps_sorted_buf", "bytes": "ps_sorted_buf"}[kind]
813
+ return lo._abi_call("builtins", "sorted", [arg], arg["type"], symbol, span)
814
+
815
+
816
+ def _const_sorted(arg: dict[str, Any], span: dict[str, Any]) -> dict[str, Any] | None:
817
+ """sorted() of a constant list is that list. Int constants sort as integers, not their digits."""
818
+
819
+ if arg.get("kind") != "list":
820
+ return None
821
+ elems = arg["elems"]
822
+ if any(item.get("kind") != "const" for item in elems):
823
+ return None
824
+ kind = arg["type"]["elem"]["kind"]
825
+ if kind == "int":
826
+ ordered = sorted(elems, key=lambda item: int(item["value"]))
827
+ elif kind in {"str", "bytes"}:
828
+ ordered = sorted(elems, key=lambda item: item["value"])
829
+ else:
830
+ return None
831
+ return {"kind": "list", "elems": ordered, "type": arg["type"], "span": span}
832
+
833
+
834
+ def _ord(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
835
+ if len(node.args) != 1:
836
+ lo.fail(node, "PS2003", "ord expects 1 argument(s)")
837
+ arg = lo._expr(node.args[0])
838
+ if arg["type"]["kind"] != "str":
839
+ lo.fail(node, "PS1003", "ord argument is str")
840
+ return lo._abi_call("builtins", "ord", [arg], {"kind": "int"}, "ps_ord", span)
841
+
842
+
843
+ def _chr(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
844
+ if len(node.args) != 1:
845
+ lo.fail(node, "PS2003", "chr expects 1 argument(s)")
846
+ arg = lo._expr(node.args[0])
847
+ if arg["type"]["kind"] != "int":
848
+ lo.fail(node, "PS1003", "chr argument is int")
849
+ return lo._abi_call("builtins", "chr", [arg], {"kind": "str"}, "ps_chr", span)
850
+
851
+
852
+ def _round(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
853
+ if len(node.args) not in {1, 2}:
854
+ lo.fail(node, "PS2003", "round expects 1 or 2 argument(s)")
855
+ arg = lo._expr(node.args[0])
856
+ if len(node.args) == 1 and arg["type"]["kind"] == "int":
857
+ return arg
858
+ if arg["type"]["kind"] != "float":
859
+ lo.fail(node, "PS1003", "round argument is float")
860
+ if len(node.args) == 1:
861
+ return lo._abi_call("builtins", "round", [arg], {"kind": "int"}, "ps_round_i64", span)
862
+ digits = lo._expr(node.args[1])
863
+ if digits["type"]["kind"] != "int":
864
+ lo.fail(node, "PS1003", "round ndigits is int")
865
+ return lo._abi_call("builtins", "round", [arg, digits], {"kind": "float"}, "ps_round_nd", span)
866
+
867
+
868
+ def _same(a: dict[str, Any], b: dict[str, Any]) -> bool:
869
+ from pyscriptc.ir import types_equal
870
+
871
+ return types_equal(a, b)
872
+
873
+
874
+ def method_spec(class_name: str, name: str) -> dict[str, Any] | None:
875
+ return CLASS_METHODS.get(class_name, {}).get(name)
876
+
877
+
878
+ def path_attr(name: str) -> tuple[dict[str, Any], str] | None:
879
+ return PATH_ATTRS.get(name)
880
+
881
+
882
+ _HMAC_ALGS = {"md5": 1, "sha1": 2, "sha256": 3, "sha384": 4, "sha512": 5}
883
+
884
+
885
+ def _hmac_alg(lo: Any, node: ast.expr) -> int | None:
886
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
887
+ return _HMAC_ALGS.get(node.value.lower())
888
+ member = ""
889
+ if isinstance(node, ast.Attribute):
890
+ resolved = lo._resolve_module(node)
891
+ if resolved is not None and resolved[1] == "hashlib":
892
+ member = resolved[2]
893
+ elif isinstance(node, ast.Name):
894
+ binding = lo.bindings.get(node.id)
895
+ if binding is not None and binding.kind == "func" and binding.module == "hashlib":
896
+ member = binding.attr
897
+ if not member:
898
+ return None
899
+ return _HMAC_ALGS.get(member)
900
+
901
+
902
+ def _hmac(lo: Any, node: ast.Call, span: dict[str, Any], attr: str) -> dict[str, Any]:
903
+ if len(node.args) != 3:
904
+ lo.fail(node, "PS2003", f"hmac.{attr} expects 3 argument(s)")
905
+ key = lo._expr(node.args[0])
906
+ msg = lo._expr(node.args[1])
907
+ if key["type"]["kind"] != "bytes" or msg["type"]["kind"] != "bytes":
908
+ lo.fail(node, "PS1003", f"hmac.{attr} key and message are bytes")
909
+ alg = _hmac_alg(lo, node.args[2])
910
+ if alg is None:
911
+ lo.fail(node, "PS2001", "hmac algorithm is a literal md5, sha1, sha256, sha384, or sha512")
912
+ alg_expr = _const_int(alg, span)
913
+ if attr == "digest":
914
+ return lo._abi_call("hmac", "digest", [key, msg, alg_expr], {"kind": "bytes"}, "ps_hmac_digest", span)
915
+ return lo._abi_call("hmac", "new", [key, msg, alg_expr], {"kind": "class", "name": "Hmac"}, "ps_hmac_new", span)
916
+
917
+
918
+ def _hmac_compare(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
919
+ if len(node.args) != 2:
920
+ lo.fail(node, "PS2003", "hmac.compare_digest expects 2 argument(s)")
921
+ left = lo._expr(node.args[0])
922
+ right = lo._expr(node.args[1])
923
+ kind = left["type"]["kind"]
924
+ if kind not in {"str", "bytes"} or right["type"]["kind"] != kind:
925
+ lo.fail(node, "PS1003", "hmac.compare_digest takes two str or two bytes")
926
+ return lo._abi_call("hmac", "compare_digest", [left, right], {"kind": "bool"}, "ps_hmac_compare", span)
927
+
928
+
929
+ def _unicode_name(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
930
+ if not 1 <= len(node.args) <= 2:
931
+ lo.fail(node, "PS2003", "unicodedata.name expects 1 or 2 argument(s)")
932
+ char = lo._expr(node.args[0])
933
+ if char["type"]["kind"] != "str":
934
+ lo.fail(node, "PS1003", "unicodedata.name argument is str")
935
+ if len(node.args) == 2:
936
+ default = lo._expr(node.args[1])
937
+ if default["type"]["kind"] != "str":
938
+ lo.fail(node, "PS1003", "unicodedata.name default is str")
939
+ has = _const_int(1, span)
940
+ else:
941
+ default = _const_text("", "str", span)
942
+ has = _const_int(0, span)
943
+ return lo._abi_call("unicodedata", "name", [char, has, default], {"kind": "str"}, "ps_unicode_name", span)
944
+
945
+
946
+ def _resource_search() -> str:
947
+ import os
948
+ import sys
949
+
950
+ found: list[str] = []
951
+ seen: set[str] = set()
952
+ for entry in sys.path:
953
+ if not entry:
954
+ entry = os.getcwd()
955
+ if not os.path.isdir(entry):
956
+ continue
957
+ path = os.path.abspath(entry)
958
+ if path in seen:
959
+ continue
960
+ seen.add(path)
961
+ found.append(path)
962
+ return "\n".join(found)
963
+
964
+
965
+ def _as_file_value(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
966
+ """as_file of a directory resource is that path. __enter__ returns it; __exit__ is a no-op."""
967
+
968
+ if len(node.args) != 1 or node.keywords:
969
+ lo.fail(node, "PS2003", "as_file expects 1 argument(s)")
970
+ opened = lo._expr(node.args[0])
971
+ if opened["type"]["kind"] != "class" or opened["type"].get("name") != "Path":
972
+ lo.fail(node, "PS1003", "as_file takes a resource path")
973
+ return opened
974
+
975
+
976
+ def _resource_files(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
977
+ if len(node.args) != 1:
978
+ lo.fail(node, "PS2003", "importlib.resources.files expects 1 argument(s)")
979
+ package = lo._expr(node.args[0])
980
+ if package["type"]["kind"] != "str":
981
+ lo.fail(node, "PS1003", "importlib.resources.files package is str")
982
+ search = _const_text(_resource_search(), "str", span)
983
+ return lo._abi_call(
984
+ "importlib.resources",
985
+ "files",
986
+ [package, search],
987
+ {"kind": "class", "name": "Path"},
988
+ "ps_res_files",
989
+ span,
990
+ )
991
+
992
+
993
+ def _atexit_register(lo: Any, node: ast.Call, span: dict[str, Any]) -> dict[str, Any]:
994
+ if len(node.args) != 1 or not isinstance(node.args[0], ast.Name):
995
+ lo.fail(node, "PS2001", "atexit.register takes a no-argument function")
996
+ name = node.args[0].id
997
+ sig = lo.sigs.get(name)
998
+ if sig is None:
999
+ lo.fail(node, "PS2001", "atexit.register takes a no-argument function")
1000
+ params, _ret = sig
1001
+ if params:
1002
+ lo.fail(node, "PS2001", "atexit.register takes a no-argument function")
1003
+ target = _const_text(lo._ir_name(name), "str", span)
1004
+ return lo._abi_call("atexit", "register", [target], {"kind": "none"}, "", span)
1005
+
1006
+
1007
+ def alias_name(module: str, member: str) -> str | None:
1008
+ return type_alias(module, member)