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/llvm.py ADDED
@@ -0,0 +1,2129 @@
1
+ """Lower spike IR to textual LLVM IR and a host binary.
2
+
3
+ `int` stays a Python int in the IR. This backend uses i64 only when the value
4
+ fits, and checked arithmetic traps on overflow instead of wrapping. A literal
5
+ that does not fit is refused: it is not truncated.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import struct
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from pyscriptc.builtins import EXCEPTIONS, USER_EXC_BASE
16
+ from pyscriptc.errors import FitError, IRError
17
+ from pyscriptc.ir import assert_same_cpython, fits_i64
18
+ from pyscriptc.stdlib_queue import MATH_FUNCTIONS
19
+
20
+ _ICMP = {"<": "slt", "<=": "sle", ">": "sgt", ">=": "sge", "==": "eq", "!=": "ne"}
21
+ _FOP = {"+": "fadd", "-": "fsub", "*": "fmul"}
22
+ _INT_HELPER = {"+": "iadd", "-": "isub", "*": "imul", "//": "floordiv"}
23
+
24
+
25
+ def emit_llvm(
26
+ module: dict[str, Any],
27
+ call: str | None = None,
28
+ args: list[Any] | None = None,
29
+ standalone: bool = False,
30
+ ) -> str:
31
+ """With `standalone` and no `call`, main does nothing: module level code is not lowered."""
32
+ assert_same_cpython(module)
33
+ if module["fences"]:
34
+ raise IRError("refusing to emit LLVM while fences are present")
35
+ if not module["functions"]:
36
+ raise IRError("module has no functions to emit")
37
+ return Emitter(module).emit(call, [] if args is None else args, standalone)
38
+
39
+
40
+ def link_binary(
41
+ llvm_path: Path,
42
+ output: Path,
43
+ driver: str = "clang",
44
+ target: str | None = None,
45
+ record: list[list[str]] | None = None,
46
+ ) -> None:
47
+ """Link `.ll` text plus the C runtime into `output`.
48
+
49
+ Two-stage: `.ll -> .o`, then objects + runtime `*.c` link. Required for
50
+ `zig cc` on macOS (a `.ll` on the link line writes an empty binary there)
51
+ and keeps per-target artifacts clean. `driver` is `clang` or `zig`
52
+ (`zig cc`); `target` is a zig-style triple and requires the zig driver —
53
+ it bundles the per-target sysroot that `clang --target` does not.
54
+ """
55
+ output.parent.mkdir(parents=True, exist_ok=True)
56
+ if target is not None and driver != "zig":
57
+ raise IRError("--target requires --driver zig (bundled sysroot per target)")
58
+ cc = "zig cc" if driver == "zig" else driver
59
+ runtime = Path(__file__).resolve().parent / "runtime"
60
+ text = llvm_path.read_text(encoding="utf-8")
61
+ sources = [str(path) for path in sorted(runtime.glob("*.c"))]
62
+ target_flags = ["-target", target] if target is not None else []
63
+ tail = ["-lm", "-o", str(output)]
64
+ if "ps_island_" in text:
65
+ if target is not None:
66
+ raise IRError(
67
+ "island fallback embeds the host CPython; "
68
+ "cross-compiling it needs a cross-built libpython (explain-only)"
69
+ )
70
+ import sysconfig
71
+
72
+ include = sysconfig.get_path("include")
73
+ libdir = sysconfig.get_config_var("LIBDIR")
74
+ version = sysconfig.get_config_var("VERSION")
75
+ target_flags.extend(["-I", include])
76
+ tail = [f"-L{libdir}", f"-lpython{version}", f"-Wl,-rpath,{libdir}", *tail]
77
+ else:
78
+ sources = [source for source in sources if not source.endswith("ps_island.c")]
79
+ # zig cannot compile C sources inside a link invocation for Mach-O, so
80
+ # every unit goes to .o first and the link line carries objects only.
81
+ units = [llvm_path, *(Path(source) for source in sources)]
82
+ objects = [output.with_name(f"{output.name}.{unit.stem}.o") for unit in units]
83
+ proc: subprocess.CompletedProcess[str] | None = None
84
+ for unit, obj in zip(units, objects, strict=True):
85
+ kind = ["-x", "ir"] if driver == "clang" and unit == llvm_path else []
86
+ argv = [*cc.split(), "-O2", *kind, "-c", str(unit), "-o", str(obj), *target_flags]
87
+ if record is not None:
88
+ record.append([str(arg) for arg in argv])
89
+ proc = subprocess.run(argv, capture_output=True, text=True)
90
+ if proc.returncode != 0:
91
+ break
92
+ if proc is not None and proc.returncode == 0:
93
+ # no -O on the link line: it is a compile flag and zig's Mach-O
94
+ # driver crashes on it (0.16.0)
95
+ argv = [*cc.split(), *objects, *target_flags, *tail]
96
+ if record is not None:
97
+ record.append([str(arg) for arg in argv])
98
+ proc = subprocess.run(argv, capture_output=True, text=True)
99
+ if proc is None or proc.returncode != 0:
100
+ detail = (proc.stderr or proc.stdout).strip() if proc is not None else ""
101
+ raise IRError(f"{cc} failed to assemble LLVM IR: {detail}")
102
+
103
+
104
+ class Emitter:
105
+ def __init__(self, module: dict[str, Any]) -> None:
106
+ self.module = module
107
+ self.functions = {fn["name"]: fn for fn in module["functions"]}
108
+ self.out: list[str] = []
109
+ self.globals: list[str] = []
110
+ self.needed: set[str] = set()
111
+ self.string_data: list[bytes] = []
112
+ self.tid = 0
113
+ self.lid = 0
114
+ self.slots: dict[str, tuple[str, dict[str, Any]]] = {}
115
+ self.terminated = False
116
+ self.try_depth = 0
117
+ self.outline_depth = 0
118
+ self.outline_n = 0
119
+ self.outline_lines: list[str] = []
120
+ self.slot_order: list[str] = []
121
+ self.slot_types: dict[str, dict[str, Any]] = {}
122
+ self.ret_ptr: str | None = None
123
+ self.ret_llvm: str | None = None
124
+ self.flow_ptr: str | None = None
125
+ self.class_index: dict[str, dict[str, Any]] = {}
126
+ self.json_n = 0
127
+ self.atexit_n = 0
128
+ self.abi_decls: dict[str, tuple[str, list[str]]] = {}
129
+
130
+ def emit(self, call: str | None, args: list[Any], standalone: bool = False) -> str:
131
+ self.class_index = {cls["name"]: cls for cls in self.module.get("classes", [])}
132
+ for cls in self.module.get("classes", []):
133
+ body = "i8" if not cls["fields"] else ", ".join(_llvm_type(field["type"]) for field in cls["fields"])
134
+ self.globals.append(f"%cls.{cls['name']} = type {{ {body} }}")
135
+ for method in cls["methods"]:
136
+ symbol = f"cls.{cls['name']}.{method['name']}"
137
+ self.functions[symbol] = {**method, "name": symbol}
138
+ for fn in self.module["functions"]:
139
+ self.emit_function(fn)
140
+ for cls in self.module.get("classes", []):
141
+ for method in cls["methods"]:
142
+ self.emit_function(self.functions[f"cls.{cls['name']}.{method['name']}"])
143
+ if call is not None:
144
+ self.emit_main(call, args)
145
+ elif standalone:
146
+ # A script's synthesized entry (E1 item 1) is the program: run it
147
+ # in source order. A definition-only module keeps the no-op main.
148
+ entry = self.module.get("entry")
149
+ entry_fn = next((fn for fn in self.module["functions"] if fn["name"] == entry), None)
150
+ if entry_fn is not None and entry_fn["name"] == "main" and not entry_fn["params"]:
151
+ self.emit_main("main", [])
152
+ else:
153
+ self.out.extend(["define i32 @main(i32 %argc, ptr %argv) {", "entry:", " ret i32 0", "}", ""])
154
+ self.out.extend(self.outline_lines)
155
+ header = ["%ps.str = type { i64, ptr }", ""]
156
+ header.extend(self.globals)
157
+ if self.globals:
158
+ header.append("")
159
+ decls = _declarations(self.needed)
160
+ decls.extend(self._abi_lines())
161
+ header.extend(decls)
162
+ if decls:
163
+ header.append("")
164
+ header.extend(_helpers(self.needed))
165
+ return "\n".join(header + self.out).rstrip() + "\n"
166
+
167
+ def emit_function(self, fn: dict[str, Any]) -> None:
168
+ ret = _llvm_type(fn["returnType"])
169
+ params = ", ".join(f"{_llvm_type(p['type'])} %{_safe(p['name'])}" for p in fn["params"])
170
+ self.out.append(f"define {ret} @{_fn_symbol(fn['name'])}({params}) {{")
171
+ self._reset_function()
172
+ self.block("entry")
173
+ self.slot_order = []
174
+ self.slot_types = {}
175
+ for local in fn["locals"]:
176
+ if local["type"]["kind"] == "none":
177
+ # None is not a runtime value. The local is the singleton, so it has no slot.
178
+ self.slots[local["name"]] = ("", local["type"])
179
+ continue
180
+ ptr = self.tmp()
181
+ self.line(f"{ptr} = alloca {_llvm_type(local['type'])}")
182
+ self.slots[local["name"]] = (ptr, local["type"])
183
+ self.slot_order.append(local["name"])
184
+ self.slot_types[local["name"]] = local["type"]
185
+ self.ret_llvm = None if fn["returnType"]["kind"] == "none" else _llvm_type(fn["returnType"])
186
+ self.ret_ptr = None if self.ret_llvm is None else self._alloca(self.ret_llvm)
187
+ self.flow_ptr = self._alloca("i32")
188
+ for param in fn["params"]:
189
+ self.store(param["name"], f"%{_safe(param['name'])}")
190
+ self.emit_body(fn["body"])
191
+ if not self.terminated:
192
+ if fn["returnType"]["kind"] == "none":
193
+ self.line("ret void")
194
+ else:
195
+ self.line("unreachable")
196
+ self.out.append("}")
197
+ self.out.append("")
198
+
199
+ def emit_main(self, name: str, args: list[Any]) -> None:
200
+ fn = self.functions.get(name)
201
+ if fn is None:
202
+ raise IRError(f"no function {name} to call")
203
+ params = fn["params"]
204
+ if len(args) > len(params):
205
+ raise FitError(f"{name} expects {len(params)} argument(s), got {len(args)}")
206
+ filled: list[Any] = list(args)
207
+ for param in params[len(args) :]:
208
+ default = param.get("default")
209
+ if not isinstance(default, dict) or default.get("kind") != "const":
210
+ raise FitError(f"{name} expects {len(params)} argument(s), got {len(args)}")
211
+ filled.append(_ir_const_value(param["type"], default["value"]))
212
+ prepared = [_prepare_arg(param, value) for param, value in zip(params, filled, strict=True)]
213
+ self.out.append("define i32 @main(i32 %argc, ptr %argv) {")
214
+ self._reset_function()
215
+ self.block("entry")
216
+ self._abi_decl("ps_argv_init", "void", ["i32", "ptr"])
217
+ self._call_void("ps_argv_init", ["i32 %argc", "ptr %argv"])
218
+ values = [self.materialize_arg(param["type"], value) for param, value in zip(params, prepared, strict=True)]
219
+ result = self.call_function(fn, values)
220
+ if fn["returnType"]["kind"] != "none":
221
+ self.print_value(fn["returnType"], result)
222
+ self.line("ret i32 0")
223
+ self.out.append("}")
224
+ self.out.append("")
225
+
226
+ def emit_body(self, body: list[dict[str, Any]]) -> None:
227
+ for stmt in body:
228
+ if self.terminated:
229
+ break
230
+ self.emit_stmt(stmt)
231
+
232
+ def emit_stmt(self, stmt: dict[str, Any]) -> None:
233
+ kind = stmt["kind"]
234
+ if kind == "assign":
235
+ if stmt["value"]["type"]["kind"] == "none":
236
+ if stmt["value"]["kind"] != "const":
237
+ self.emit_expr(stmt["value"])
238
+ return
239
+ self.store(stmt["target"], self.emit_expr(stmt["value"]))
240
+ elif kind == "expr":
241
+ if stmt["expr"]["kind"] == "const" and stmt["expr"]["type"]["kind"] == "none":
242
+ return
243
+ self.emit_expr(stmt["expr"])
244
+ elif kind == "return":
245
+ self._emit_return(stmt)
246
+ elif kind == "pass":
247
+ return
248
+ elif kind == "setitem":
249
+ self.emit_setitem(stmt)
250
+ elif kind == "setattr":
251
+ self.emit_setattr(stmt)
252
+ elif kind == "raise":
253
+ self.emit_raise(stmt)
254
+ self.terminated = True
255
+ elif kind == "try":
256
+ self.emit_try(stmt)
257
+ elif kind == "if":
258
+ self.emit_if(stmt)
259
+ elif kind == "while":
260
+ self.emit_while(stmt)
261
+ else:
262
+ raise IRError(f"cannot emit statement {kind}")
263
+
264
+ def emit_if(self, stmt: dict[str, Any]) -> None:
265
+ cond = self.emit_expr(stmt["cond"])
266
+ then_l = self.fresh("then")
267
+ else_l = self.fresh("else")
268
+ merge_l = self.fresh("merge")
269
+ self.line(f"br i1 {cond}, label %{then_l}, label %{else_l}")
270
+ self.terminated = True
271
+ self.block(then_l)
272
+ self.emit_body(stmt["then"])
273
+ then_dead = self.terminated
274
+ if not then_dead:
275
+ self.line(f"br label %{merge_l}")
276
+ self.terminated = True
277
+ self.block(else_l)
278
+ self.emit_body(stmt["else"])
279
+ else_dead = self.terminated
280
+ if not else_dead:
281
+ self.line(f"br label %{merge_l}")
282
+ self.terminated = True
283
+ if then_dead and else_dead:
284
+ self.terminated = True
285
+ return
286
+ self.block(merge_l)
287
+
288
+ def emit_while(self, stmt: dict[str, Any]) -> None:
289
+ cond_l = self.fresh("while.cond")
290
+ body_l = self.fresh("while.body")
291
+ end_l = self.fresh("while.end")
292
+ self.line(f"br label %{cond_l}")
293
+ self.terminated = True
294
+ self.block(cond_l)
295
+ cond = self.emit_expr(stmt["cond"])
296
+ self.line(f"br i1 {cond}, label %{body_l}, label %{end_l}")
297
+ self.terminated = True
298
+ self.block(body_l)
299
+ self.emit_body(stmt["body"])
300
+ if not self.terminated:
301
+ self.line(f"br label %{cond_l}")
302
+ self.terminated = True
303
+ self.block(end_l)
304
+
305
+ def emit_try(self, stmt: dict[str, Any]) -> None:
306
+ self.needed.add("exc")
307
+ outline = self._emit_outline(stmt["body"])
308
+ env = self._pack_env()
309
+ if self.flow_ptr is None:
310
+ raise IRError("try has no flow slot")
311
+ code = self._call_ret("i32", "ps_try_call", [f"ptr @{outline}", f"ptr {env}", f"ptr {self.flow_ptr}"])
312
+ raised = self.tmp()
313
+ self.line(f"{raised} = icmp ne i32 {code}, 0")
314
+ hand_l = self.fresh("try.handler")
315
+ cont_l = self.fresh("try.cont")
316
+ self.line(f"br i1 {raised}, label %{hand_l}, label %{cont_l}")
317
+ self.terminated = True
318
+ self.block(hand_l)
319
+ merge_l = self.fresh("try.merge")
320
+ ret_l = self.fresh("try.ret")
321
+ ecode = self._call_ret("i32", "ps_exc_code", [])
322
+ for index, handler in enumerate(stmt["handlers"]):
323
+ take_l = self.fresh(f"try.h{index}")
324
+ miss_l = self.fresh("try.miss")
325
+ cond = self._exc_match(ecode, handler)
326
+ self.line(f"br i1 {cond}, label %{take_l}, label %{miss_l}")
327
+ self.terminated = True
328
+ self.block(take_l)
329
+ self.emit_body(handler["body"])
330
+ if not self.terminated:
331
+ self.line(f"br label %{merge_l}")
332
+ self.terminated = True
333
+ self.block(miss_l)
334
+ self.line("call void @ps_reraise() noreturn")
335
+ self.line("unreachable")
336
+ self.terminated = True
337
+ self.block(cont_l)
338
+ flow = self._load("i32", self.flow_ptr)
339
+ leave = self.tmp()
340
+ self.line(f"{leave} = icmp eq i32 {flow}, 1")
341
+ self.line(f"br i1 {leave}, label %{ret_l}, label %{merge_l}")
342
+ self.terminated = True
343
+ self.block(ret_l)
344
+ self._emit_propagated_return()
345
+ self.block(merge_l)
346
+
347
+ def _emit_return(self, stmt: dict[str, Any]) -> None:
348
+ kind = stmt["value"]["type"]["kind"]
349
+ if self.outline_depth:
350
+ if kind != "none":
351
+ value = self.emit_expr(stmt["value"])
352
+ if self.ret_ptr is None or self.ret_llvm is None:
353
+ raise IRError("return inside try has no slot")
354
+ self.line(f"store {self.ret_llvm} {value}, ptr {self.ret_ptr}")
355
+ elif stmt["value"]["kind"] != "const":
356
+ self.emit_expr(stmt["value"])
357
+ if self.terminated:
358
+ return
359
+ self.line("ret i32 1")
360
+ elif kind == "none":
361
+ if stmt["value"]["kind"] != "const":
362
+ self.emit_expr(stmt["value"])
363
+ if self.terminated:
364
+ return
365
+ self.line("ret void")
366
+ else:
367
+ value = self.emit_expr(stmt["value"])
368
+ self.line(f"ret {_llvm_type(stmt['value']['type'])} {value}")
369
+ self.terminated = True
370
+
371
+ def _emit_propagated_return(self) -> None:
372
+ if self.outline_depth:
373
+ self.line("ret i32 1")
374
+ elif self.ret_llvm is None:
375
+ self.line("ret void")
376
+ else:
377
+ if self.ret_ptr is None:
378
+ raise IRError("return has no slot")
379
+ value = self._load(self.ret_llvm, self.ret_ptr)
380
+ self.line(f"ret {self.ret_llvm} {value}")
381
+ self.terminated = True
382
+
383
+ def _emit_outline(self, body: list[dict[str, Any]]) -> str:
384
+ name = f"ps.try.{self.outline_n}"
385
+ self.outline_n += 1
386
+ saved_out = self.out
387
+ saved_slots = self.slots
388
+ saved_flow = self.flow_ptr
389
+ saved_ret = self.ret_ptr
390
+ saved_term = self.terminated
391
+ saved_lid = self.lid
392
+ saved_tid = self.tid
393
+ saved_depth = self.outline_depth
394
+ self.out = []
395
+ self.out.append(f"define internal i32 @{name}(ptr %env) {{")
396
+ self.tid = 0
397
+ self.lid = 0
398
+ self.terminated = False
399
+ self.outline_depth = saved_depth + 1
400
+ self.block("entry")
401
+ self._bind_env("%env")
402
+ self.flow_ptr = self._alloca("i32")
403
+ self.emit_body(body)
404
+ if not self.terminated:
405
+ self.line("ret i32 0")
406
+ self.out.append("}")
407
+ self.out.append("")
408
+ self.outline_lines.extend(self.out)
409
+ self.out = saved_out
410
+ self.slots = saved_slots
411
+ self.flow_ptr = saved_flow
412
+ self.ret_ptr = saved_ret
413
+ self.terminated = saved_term
414
+ self.lid = saved_lid
415
+ self.tid = saved_tid
416
+ self.outline_depth = saved_depth
417
+ return name
418
+
419
+ def _env_pointers(self) -> list[str]:
420
+ ptrs = [self.slots[name][0] for name in self.slot_order]
421
+ if self.ret_ptr is not None:
422
+ ptrs.append(self.ret_ptr)
423
+ return ptrs
424
+
425
+ def _pack_env(self) -> str:
426
+ ptrs = self._env_pointers()
427
+ if not ptrs:
428
+ return "null"
429
+ env = self._alloca(f"[{len(ptrs)} x ptr]")
430
+ for index, ptr in enumerate(ptrs):
431
+ dest = env if index == 0 else self.tmp()
432
+ if index != 0:
433
+ self.line(f"{dest} = getelementptr ptr, ptr {env}, i64 {index}")
434
+ self.line(f"store ptr {ptr}, ptr {dest}")
435
+ return env
436
+
437
+ def _bind_env(self, env: str) -> None:
438
+ self.slots = {}
439
+ for index, name in enumerate(self.slot_order):
440
+ loaded = self._load_env_ptr(env, index)
441
+ self.slots[name] = (loaded, self.slot_types[name])
442
+ if self.ret_llvm is not None:
443
+ self.ret_ptr = self._load_env_ptr(env, len(self.slot_order))
444
+
445
+ def _load_env_ptr(self, env: str, index: int) -> str:
446
+ if index == 0:
447
+ return self._load("ptr", env)
448
+ gep = self.tmp()
449
+ self.line(f"{gep} = getelementptr ptr, ptr {env}, i64 {index}")
450
+ return self._load("ptr", gep)
451
+
452
+ def emit_raise(self, stmt: dict[str, Any]) -> None:
453
+ self.needed.add("exc")
454
+ code = stmt["code"] if "code" in stmt else EXCEPTIONS[stmt["exc"]]
455
+ if stmt.get("reraise"):
456
+ self.line("call void @ps_reraise() noreturn")
457
+ elif stmt.get("message") is None:
458
+ self.line(f"call void @ps_raise_code(i32 {code}) noreturn")
459
+ else:
460
+ message = self.emit_expr(stmt["message"])
461
+ length, ptr = self._buf_parts(message)
462
+ self.line(f"call void @ps_raise_msg(i32 {code}, i64 {length}, ptr {ptr}) noreturn")
463
+ self.line("unreachable")
464
+
465
+ def emit_setitem(self, stmt: dict[str, Any]) -> None:
466
+ container = self.emit_expr(stmt["container"])
467
+ index = self.emit_expr(stmt["index"])
468
+ value = self.emit_expr(stmt["value"])
469
+ kind = stmt["container"]["type"]["kind"]
470
+ if kind == "list":
471
+ self.needed.add("seq")
472
+ slot = self._stash(stmt["value"]["type"], value)
473
+ self.line(f"call void @ps_seq_set(ptr {container}, i64 {index}, ptr {slot})")
474
+ return
475
+ if kind == "dict":
476
+ self.needed.add("map")
477
+ key = self._stash(stmt["index"]["type"], index)
478
+ slot = self._stash(stmt["value"]["type"], value)
479
+ self.line(f"call void @ps_map_put(ptr {container}, ptr {key}, ptr {slot})")
480
+ return
481
+ raise IRError(f"cannot assign into {kind}")
482
+
483
+ def emit_setattr(self, stmt: dict[str, Any]) -> None:
484
+ obj = self.emit_expr(stmt["object"])
485
+ value = self.emit_expr(stmt["value"])
486
+ field = self._field(stmt["object"]["type"]["name"], stmt["name"])
487
+ slot = self._field_ptr(stmt["object"]["type"]["name"], obj, field)
488
+ self.line(f"store {_llvm_type(field['type'])} {value}, ptr {slot}")
489
+
490
+ def _exc_match(self, code: str, handler: dict[str, Any] | list[str]) -> str:
491
+ names = handler if isinstance(handler, list) else handler["exc"]
492
+ checks: list[str] = []
493
+ for name in names:
494
+ if name not in EXCEPTIONS:
495
+ continue
496
+ wanted = EXCEPTIONS[name]
497
+ call = self._call_ret("i32", "ps_exc_isa", [f"i32 {code}", f"i32 {wanted}"])
498
+ checks.append(self._flag(call))
499
+ if name == "Exception":
500
+ # User codes have no runtime parent chain yet; every user
501
+ # exception inherits a lowered builtin, so all of them are
502
+ # Exception descendants.
503
+ reg = self.tmp()
504
+ self.line(f"{reg} = icmp sge i32 {code}, {USER_EXC_BASE}")
505
+ checks.append(reg)
506
+ numbers = handler.get("codes") if isinstance(handler, dict) else None
507
+ if not numbers:
508
+ numbers = [EXCEPTIONS[name] for name in names if name in EXCEPTIONS]
509
+ for number in numbers:
510
+ if number < USER_EXC_BASE:
511
+ continue
512
+ reg = self.tmp()
513
+ self.line(f"{reg} = icmp eq i32 {code}, {number}")
514
+ checks.append(reg)
515
+ acc: str | None = None
516
+ for reg in checks:
517
+ if acc is None:
518
+ acc = reg
519
+ continue
520
+ nxt = self.tmp()
521
+ self.line(f"{nxt} = or i1 {acc}, {reg}")
522
+ acc = nxt
523
+ if acc is None:
524
+ raise IRError("except handler has no exception name")
525
+ return acc
526
+
527
+ def emit_expr(self, expr: dict[str, Any]) -> str:
528
+ kind = expr["kind"]
529
+ if kind == "const":
530
+ return self.emit_const(expr)
531
+ if kind == "name":
532
+ return self.load(expr["id"])
533
+ if kind == "unary":
534
+ operand = self.emit_expr(expr["operand"])
535
+ if expr["op"] == "-" and expr["type"]["kind"] == "int":
536
+ return self.helper_call("ineg", "i64", [operand])
537
+ if expr["op"] == "-" and expr["type"]["kind"] == "float":
538
+ reg = self.tmp()
539
+ self.line(f"{reg} = fneg double {operand}")
540
+ return reg
541
+ if expr["op"] == "-" and expr["type"]["kind"] == "class":
542
+ mem = self._alloc_class("TimeDelta")
543
+ self._abi_decl("ps_delta_neg", "void", ["ptr", "ptr"])
544
+ self._call_void("ps_delta_neg", [f"ptr {operand}", f"ptr {mem}"])
545
+ return mem
546
+ if expr["op"] == "not":
547
+ reg = self.tmp()
548
+ self.line(f"{reg} = xor i1 {operand}, true")
549
+ return reg
550
+ raise IRError(f"cannot emit unary {expr['op']}")
551
+ if kind == "binary":
552
+ return self.emit_binary(expr, self.emit_expr(expr["left"]), self.emit_expr(expr["right"]))
553
+ if kind == "compare":
554
+ return self.emit_compare(expr)
555
+ if kind == "call":
556
+ return self.emit_call(expr)
557
+ if kind == "list":
558
+ return self.emit_list(expr)
559
+ if kind == "dict":
560
+ return self.emit_dict(expr)
561
+ if kind == "subscript":
562
+ return self.emit_subscript(expr)
563
+ if kind == "attr":
564
+ return self.emit_attr(expr)
565
+ if kind == "cast":
566
+ value = self.emit_expr(expr["value"])
567
+ reg = self.tmp()
568
+ self.line(f"{reg} = sitofp i64 {value} to double")
569
+ return reg
570
+ if kind == "contains":
571
+ return self.emit_contains(expr)
572
+ raise IRError(f"cannot emit expression {kind}")
573
+
574
+ def emit_const(self, expr: dict[str, Any]) -> str:
575
+ kind = expr["type"]["kind"]
576
+ value = expr["value"]
577
+ if kind == "int":
578
+ number = int(value)
579
+ if not fits_i64(number):
580
+ raise FitError(f"integer literal {value} does not fit in i64; refusing to truncate")
581
+ return str(number)
582
+ if kind == "bool":
583
+ return "true" if value else "false"
584
+ if kind == "float":
585
+ return _llvm_double(float(value))
586
+ if kind == "str":
587
+ return self.string_value(value)
588
+ if kind == "bytes":
589
+ return self.string_value(value, binary=True)
590
+ if kind == "none":
591
+ raise IRError("None is not a runtime value")
592
+ raise IRError(f"cannot emit constant of type {kind}")
593
+
594
+ def emit_binary(self, expr: dict[str, Any], left: str, right: str) -> str:
595
+ kind = expr["type"]["kind"]
596
+ op = expr["op"]
597
+ if kind == "int":
598
+ return self.helper_call(_INT_HELPER[op], "i64", [left, right])
599
+ if kind == "float":
600
+ reg = self.tmp()
601
+ self.line(f"{reg} = {_FOP[op]} double {left}, {right}")
602
+ return reg
603
+ if kind in {"str", "bytes"} and op == "+":
604
+ return self.helper_call("strconcat", "%ps.str", [left, right], arg_type="%ps.str")
605
+ if expr["left"]["type"]["kind"] == "class":
606
+ return self._emit_class_binary(expr, left, right)
607
+ raise IRError(f"cannot emit {op} on {kind}")
608
+
609
+ def emit_compare(self, expr: dict[str, Any]) -> str:
610
+ left_kind = expr["left"]["type"]["kind"]
611
+ right_kind = expr["right"]["type"]["kind"]
612
+ op = expr["op"]
613
+ if left_kind == "none" or right_kind == "none":
614
+ if op not in {"==", "!="}:
615
+ raise IRError(f"cannot compare None with {op}")
616
+ self._emit_none_side(expr["left"])
617
+ self._emit_none_side(expr["right"])
618
+ # None equals only None. A str, int, or other value is never None.
619
+ same = left_kind == "none" and right_kind == "none"
620
+ truth = same if op == "==" else not same
621
+ reg = self.tmp()
622
+ bit = "true" if truth else "false"
623
+ self.line(f"{reg} = icmp eq i1 {bit}, true")
624
+ return reg
625
+ left = self.emit_expr(expr["left"])
626
+ right = self.emit_expr(expr["right"])
627
+ kind = left_kind
628
+ reg = self.tmp()
629
+ if kind in {"int", "bool"}:
630
+ self.line(f"{reg} = icmp {_ICMP[op]} {'i64' if kind == 'int' else 'i1'} {left}, {right}")
631
+ return reg
632
+ if kind == "float":
633
+ pred = {"==": "oeq", "!=": "une", "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge"}[op]
634
+ self.line(f"{reg} = fcmp {pred} double {left}, {right}")
635
+ return reg
636
+ if kind in {"str", "bytes"}:
637
+ cmp = self._buf_cmp(left, right)
638
+ pred = {"==": "eq", "!=": "ne", "<": "slt", "<=": "sle", ">": "sgt", ">=": "sge"}[op]
639
+ self.line(f"{reg} = icmp {pred} i32 {cmp}, 0")
640
+ return reg
641
+ if kind == "class":
642
+ return self._emit_class_cmp(expr, left, right)
643
+ raise IRError(f"cannot compare {kind}")
644
+
645
+ def _emit_none_side(self, expr: dict[str, Any]) -> None:
646
+ """Emit a call that returns None. A None constant or a None local is not a register."""
647
+ if expr["type"]["kind"] != "none":
648
+ self.emit_expr(expr)
649
+ return
650
+ if expr["kind"] == "const" or expr["kind"] == "name":
651
+ return
652
+ self.emit_expr(expr)
653
+
654
+ def emit_call(self, expr: dict[str, Any]) -> str:
655
+ callee = expr["callee"]
656
+ values = [self.emit_expr(arg) for arg in expr["args"]]
657
+ if callee["kind"] == "builtin":
658
+ return self.emit_builtin(callee["name"], values, [arg["type"] for arg in expr["args"]], expr)
659
+ if callee["kind"] == "module":
660
+ return self.emit_module_call(expr)
661
+ if callee["kind"] == "method":
662
+ return self.emit_method(expr, values)
663
+ if callee["kind"] == "class":
664
+ return self.emit_constructor(callee["name"], values)
665
+ return self.call_function(self.functions[callee["name"]], values)
666
+
667
+ def emit_builtin(self, name: str, values: list[str], arg_types: list[dict[str, Any]], expr: dict[str, Any]) -> str:
668
+ if name == "print":
669
+ self._emit_print(values, arg_types, expr)
670
+ return ""
671
+ if name == "len" and arg_types[0]["kind"] in {"str", "bytes"}:
672
+ reg = self.tmp()
673
+ self.line(f"{reg} = extractvalue %ps.str {values[0]}, 0")
674
+ return reg
675
+ if name == "len" and arg_types[0]["kind"] == "list":
676
+ self.needed.add("seq")
677
+ return self._call_ret("i64", "ps_seq_len", [f"ptr {values[0]}"])
678
+ if name == "len" and arg_types[0]["kind"] == "dict":
679
+ self.needed.add("map")
680
+ return self._call_ret("i64", "ps_map_len", [f"ptr {values[0]}"])
681
+ if name == "len" and arg_types[0]["kind"] == "class" and arg_types[0].get("name") == "Deque":
682
+ self._abi_decl("ps_deque_len", "i64", ["ptr"])
683
+ return self._call_ret("i64", "ps_deque_len", [f"ptr {values[0]}"])
684
+ if name in {"min", "max"}:
685
+ return self._emit_minmax(name, values, arg_types)
686
+ if name == "int":
687
+ if arg_types[0]["kind"] in {"bytes", "str"}:
688
+ self._abi_decl("ps_int_from_bytes", "i64", ["i64", "ptr"])
689
+ length, ptr = self._buf_parts(values[0])
690
+ return self._call_ret("i64", "ps_int_from_bytes", [f"i64 {length}", f"ptr {ptr}"])
691
+ self.needed.add("math")
692
+ return self._call_ret("i64", "ps_float_to_int", [f"double {values[0]}"])
693
+ if name == "float" and arg_types[0]["kind"] == "str":
694
+ self._abi_decl("ps_float_from_str", "double", ["i64", "ptr"])
695
+ length, ptr = self._buf_parts(values[0])
696
+ return self._call_ret("double", "ps_float_from_str", [f"i64 {length}", f"ptr {ptr}"])
697
+ if name == "str":
698
+ kind = arg_types[0]["kind"]
699
+ if kind == "str":
700
+ return values[0]
701
+ if kind == "int":
702
+ self._abi_decl("ps_i64_str", "void", ["i64", "ptr"])
703
+ return self._buf_out("ps_i64_str", [f"i64 {values[0]}"])
704
+ if kind == "float":
705
+ self._abi_decl("ps_f64_str", "void", ["double", "ptr"])
706
+ return self._buf_out("ps_f64_str", [f"double {values[0]}"])
707
+ if kind == "bool":
708
+ t_len = self.tmp()
709
+ t_ptr = self.tmp()
710
+ self.line(f"{t_len} = select i1 {values[0]}, i64 4, i64 5")
711
+ self.line(
712
+ f"{t_ptr} = select i1 {values[0]}, ptr {self.format_global('boolT', b'True')}, "
713
+ f"ptr {self.format_global('boolF', b'False')}"
714
+ )
715
+ agg = self.tmp()
716
+ self.line(f"{agg} = insertvalue %ps.str poison, i64 {t_len}, 0")
717
+ ret = self.tmp()
718
+ self.line(f"{ret} = insertvalue %ps.str {agg}, ptr {t_ptr}, 1")
719
+ return ret
720
+ if kind == "bytes":
721
+ self._abi_decl("ps_repr_bytes", "void", ["i64", "ptr", "ptr"])
722
+ length, ptr = self._buf_parts(values[0])
723
+ return self._buf_out("ps_repr_bytes", [f"i64 {length}", f"ptr {ptr}"])
724
+ if kind == "class" and arg_types[0].get("name") == "Path":
725
+ self._abi_decl("ps_path_posix", "void", ["ptr", "ptr"])
726
+ return self._buf_out("ps_path_posix", [f"ptr {values[0]}"])
727
+ raise IRError(f"no LLVM lowering for builtin {name}")
728
+
729
+ def emit_module_call(self, expr: dict[str, Any]) -> str:
730
+ callee = expr["callee"]
731
+ module = callee["module"]
732
+ name = callee["name"]
733
+ args = expr["args"]
734
+ values = [self.emit_expr(arg) for arg in args]
735
+ if expr.get("fn"):
736
+ return self._emit_reduce(expr)
737
+ if name == "exit":
738
+ return self._emit_exit(values[0])
739
+ if module == "atexit" and name == "register":
740
+ return self._emit_atexit(expr)
741
+ if expr.get("symbol") == "ps_path_join":
742
+ return self._emit_join(values)
743
+ if expr.get("symbol") in {"ps_deque_append", "ps_deque_appendleft"}:
744
+ return self._emit_deque_store(expr, values)
745
+ if expr.get("symbol") in {"ps_deque_pop", "ps_deque_popleft"}:
746
+ return self._emit_deque_load(expr, values)
747
+ if expr.get("symbol") == "ps_list_push":
748
+ return self._emit_list_push(expr, values)
749
+ if expr.get("symbol") == "ps_map_del":
750
+ self.needed.add("map")
751
+ key = self._stash(expr["args"][1]["type"], values[1])
752
+ self._abi_decl("ps_map_del", "void", ["ptr", "ptr"])
753
+ self._call_void("ps_map_del", [f"ptr {values[0]}", f"ptr {key}"])
754
+ return ""
755
+ if expr.get("symbol") == "ps_dict_items":
756
+ # The element is a synthetic tuple_N class; the C side packs key and
757
+ # value cells at byte offsets read off the class layout.
758
+ tname = expr["type"]["elem"]["name"]
759
+ size_ptr = self.tmp()
760
+ size = self.tmp()
761
+ self.line(f"{size_ptr} = getelementptr %cls.{tname}, ptr null, i64 1")
762
+ self.line(f"{size} = ptrtoint ptr {size_ptr} to i64")
763
+ voff_ptr = self.tmp()
764
+ voff = self.tmp()
765
+ self.line(f"{voff_ptr} = getelementptr %cls.{tname}, ptr null, i32 0, i32 1")
766
+ self.line(f"{voff} = ptrtoint ptr {voff_ptr} to i64")
767
+ self._abi_decl("ps_dict_items", "ptr", ["ptr", "i64", "i64"])
768
+ return self._call_ret("ptr", "ps_dict_items", [f"ptr {values[0]}", f"i64 {voff}", f"i64 {size}"])
769
+ if expr.get("symbol"):
770
+ return self._emit_abi(expr, values)
771
+ if module == "math":
772
+ return self._emit_math(name, values)
773
+ if module == "re":
774
+ self.needed.add("re")
775
+ return self._emit_re(name, values)
776
+ if module == "json":
777
+ self.needed.add("json")
778
+ if name == "loads":
779
+ return self._emit_json_loads(values[0], expr["type"])
780
+ return self._emit_json_dumps(values[0], args[0]["type"])
781
+ if module == "urllib.parse":
782
+ self.needed.add("url")
783
+ return self._emit_url(name, values)
784
+ if module == "base64":
785
+ self.needed.add("b64")
786
+ return self._emit_b64(name, values)
787
+ if module == "hashlib":
788
+ self.needed.add("hash")
789
+ return self._emit_hash(name, values)
790
+ if module == "struct":
791
+ self.needed.add("struct")
792
+ return self._emit_struct(name, values, args, expr["type"])
793
+ raise IRError(f"no LLVM lowering for {module}.{name}")
794
+
795
+ def _emit_math(self, name: str, values: list[str]) -> str:
796
+ spec = MATH_FUNCTIONS[name]
797
+ if name == "isnan":
798
+ reg = self.tmp()
799
+ self.line(f"{reg} = fcmp uno double {values[0]}, 0.0")
800
+ return reg
801
+ if name == "isinf":
802
+ abs_reg = self.tmp()
803
+ self.line(f"{abs_reg} = call double @llvm.fabs.f64(double {values[0]})")
804
+ self.needed.add("fabs")
805
+ reg = self.tmp()
806
+ self.line(f"{reg} = fcmp oeq double {abs_reg}, 0x7FF0000000000000")
807
+ return reg
808
+ self.needed.add("math")
809
+ return self._call_ret(spec["llvm"], spec["symbol"], [f"double {value}" for value in values])
810
+
811
+ def _emit_re(self, name: str, values: list[str]) -> str:
812
+ if name == "compile":
813
+ return self._emit_pattern(values[0])
814
+ if name == "escape":
815
+ length, ptr = self._buf_parts(values[0])
816
+ return self._buf_out("ps_re_escape", [f"i64 {length}", f"ptr {ptr}"])
817
+ if name == "findall":
818
+ pattern_len, pattern_ptr = self._buf_parts(values[0])
819
+ text_len, text_ptr = self._buf_parts(values[1])
820
+ return self._call_ret(
821
+ "ptr",
822
+ "ps_re_findall",
823
+ [f"i64 {pattern_len}", f"ptr {pattern_ptr}", f"i64 {text_len}", f"ptr {text_ptr}"],
824
+ )
825
+ if name == "split":
826
+ pattern_len, pattern_ptr = self._buf_parts(values[0])
827
+ text_len, text_ptr = self._buf_parts(values[1])
828
+ return self._call_ret(
829
+ "ptr",
830
+ "ps_re_split",
831
+ [f"i64 {pattern_len}", f"ptr {pattern_ptr}", f"i64 {text_len}", f"ptr {text_ptr}"],
832
+ )
833
+ if name == "sub":
834
+ pattern_len, pattern_ptr = self._buf_parts(values[0])
835
+ repl_len, repl_ptr = self._buf_parts(values[1])
836
+ text_len, text_ptr = self._buf_parts(values[2])
837
+ return self._buf_out(
838
+ "ps_re_sub",
839
+ [
840
+ f"i64 {pattern_len}",
841
+ f"ptr {pattern_ptr}",
842
+ f"i64 {repl_len}",
843
+ f"ptr {repl_ptr}",
844
+ f"i64 {text_len}",
845
+ f"ptr {text_ptr}",
846
+ ],
847
+ )
848
+ raise IRError(f"no LLVM lowering for re.{name}")
849
+
850
+ def _emit_pattern(self, pattern: str) -> str:
851
+ self.needed.add("alloc")
852
+ size_ptr = self.tmp()
853
+ size = self.tmp()
854
+ self.line(f"{size_ptr} = getelementptr %cls.Pattern, ptr null, i64 1")
855
+ self.line(f"{size} = ptrtoint ptr {size_ptr} to i64")
856
+ mem = self._call_ret("ptr", "ps_alloc", [f"i64 {size}"])
857
+ slot = self.tmp()
858
+ self.line(f"{slot} = getelementptr %cls.Pattern, ptr {mem}, i32 0, i32 0")
859
+ self.line(f"store %ps.str {pattern}, ptr {slot}")
860
+ return mem
861
+
862
+ def _emit_b64(self, name: str, values: list[str]) -> str:
863
+ symbols = {
864
+ "b64encode": "ps_b64_std_encode",
865
+ "standard_b64encode": "ps_b64_std_encode",
866
+ "urlsafe_b64encode": "ps_b64_url_encode",
867
+ "b64decode": "ps_b64_std_decode",
868
+ "standard_b64decode": "ps_b64_std_decode",
869
+ "urlsafe_b64decode": "ps_b64_url_decode",
870
+ }
871
+ length, ptr = self._buf_parts(values[0])
872
+ return self._buf_out(symbols[name], [f"i64 {length}", f"ptr {ptr}"])
873
+
874
+ def _emit_hash(self, name: str, values: list[str]) -> str:
875
+ symbols = {
876
+ "md5": "ps_hash_md5",
877
+ "sha1": "ps_hash_sha1",
878
+ "sha256": "ps_hash_sha256",
879
+ "sha384": "ps_hash_sha384",
880
+ "sha512": "ps_hash_sha512",
881
+ }
882
+ if name in symbols:
883
+ length, ptr = self._buf_parts(values[0])
884
+ return self._call_ret("ptr", symbols[name], [f"i64 {length}", f"ptr {ptr}"])
885
+ if name == "hexdigest":
886
+ return self._buf_out("ps_hash_hex", [f"ptr {values[0]}"])
887
+ if name == "digest":
888
+ return self._buf_out("ps_hash_digest", [f"ptr {values[0]}"])
889
+ raise IRError(f"no LLVM lowering for hashlib.{name}")
890
+
891
+ def _emit_url(self, name: str, values: list[str]) -> str:
892
+ if name in {"quote", "quote_plus"}:
893
+ text_len, text_ptr = self._buf_parts(values[0])
894
+ safe_len, safe_ptr = self._buf_parts(values[1])
895
+ symbol = "ps_url_quote" if name == "quote" else "ps_url_quote_plus"
896
+ return self._buf_out(symbol, [f"i64 {text_len}", f"ptr {text_ptr}", f"i64 {safe_len}", f"ptr {safe_ptr}"])
897
+ if name in {"unquote", "unquote_plus"}:
898
+ length, ptr = self._buf_parts(values[0])
899
+ symbol = "ps_url_unquote" if name == "unquote" else "ps_url_unquote_plus"
900
+ return self._buf_out(symbol, [f"i64 {length}", f"ptr {ptr}"])
901
+ if name == "urlencode":
902
+ return self._buf_out("ps_url_encode", [f"ptr {values[0]}"])
903
+ if name == "parse_qs":
904
+ length, ptr = self._buf_parts(values[0])
905
+ return self._call_ret("ptr", "ps_url_parse_qs", [f"i64 {length}", f"ptr {ptr}"])
906
+ if name == "urlsplit":
907
+ return self._emit_urlsplit(values[0])
908
+ raise IRError(f"no LLVM lowering for urllib.parse.{name}")
909
+
910
+ def _emit_urlsplit(self, value: str) -> str:
911
+ self.needed.add("alloc")
912
+ size_ptr = self.tmp()
913
+ size = self.tmp()
914
+ self.line(f"{size_ptr} = getelementptr %cls.SplitResult, ptr null, i64 1")
915
+ self.line(f"{size} = ptrtoint ptr {size_ptr} to i64")
916
+ mem = self._call_ret("ptr", "ps_alloc", [f"i64 {size}"])
917
+ length, ptr = self._buf_parts(value)
918
+ self.line(f"call void @ps_url_split(i64 {length}, ptr {ptr}, ptr {mem})")
919
+ return mem
920
+
921
+ def _emit_struct(self, name: str, values: list[str], args: list[dict[str, Any]], typ: dict[str, Any]) -> str:
922
+ if name == "pack":
923
+ return self._emit_struct_pack(values, args)
924
+ elem = typ["elem"]["kind"]
925
+ want = {"int": 1, "float": 2, "bytes": 3}[elem]
926
+ fmt_len, fmt_ptr = self._buf_parts(values[0])
927
+ data_len, data_ptr = self._buf_parts(values[1])
928
+ return self._call_ret(
929
+ "ptr",
930
+ "ps_struct_unpack",
931
+ [f"i64 {fmt_len}", f"ptr {fmt_ptr}", f"i64 {data_len}", f"ptr {data_ptr}", f"i32 {want}"],
932
+ )
933
+
934
+ def _emit_struct_pack(self, values: list[str], args: list[dict[str, Any]]) -> str:
935
+ fmt_len, fmt_ptr = self._buf_parts(values[0])
936
+ payload = list(zip(values[1:], args[1:], strict=True))
937
+ if not payload:
938
+ arg_ptr = "null"
939
+ else:
940
+ self._ensure_arg_type()
941
+ count = len(payload)
942
+ slot = self.tmp()
943
+ self.line(f"{slot} = alloca [{count} x %ps.arg]")
944
+ for index, (value, arg) in enumerate(payload):
945
+ base = self.tmp()
946
+ self.line(f"{base} = getelementptr [{count} x %ps.arg], ptr {slot}, i64 0, i64 {index}")
947
+ self._store_ps_arg(base, value, arg["type"])
948
+ arg_ptr = self.tmp()
949
+ self.line(f"{arg_ptr} = getelementptr [{count} x %ps.arg], ptr {slot}, i64 0, i64 0")
950
+ return self._buf_out(
951
+ "ps_struct_pack", [f"i64 {fmt_len}", f"ptr {fmt_ptr}", f"ptr {arg_ptr}", f"i64 {len(payload)}"]
952
+ )
953
+
954
+ def _ensure_arg_type(self) -> None:
955
+ line = "%ps.arg = type { i32, i32, i64, double, %ps.str }"
956
+ if line not in self.globals:
957
+ self.globals.append(line)
958
+
959
+ def _store_ps_arg(self, base: str, value: str, typ: dict[str, Any]) -> None:
960
+ kind = typ["kind"]
961
+ code = 1 if kind == "int" else 2 if kind == "float" else 3
962
+ kind_ptr = self.tmp()
963
+ self.line(f"{kind_ptr} = getelementptr %ps.arg, ptr {base}, i32 0, i32 0")
964
+ self.line(f"store i32 {code}, ptr {kind_ptr}")
965
+ if kind == "int":
966
+ slot = self.tmp()
967
+ self.line(f"{slot} = getelementptr %ps.arg, ptr {base}, i32 0, i32 2")
968
+ self.line(f"store i64 {value}, ptr {slot}")
969
+ return
970
+ if kind == "float":
971
+ slot = self.tmp()
972
+ self.line(f"{slot} = getelementptr %ps.arg, ptr {base}, i32 0, i32 3")
973
+ self.line(f"store double {value}, ptr {slot}")
974
+ return
975
+ slot = self.tmp()
976
+ self.line(f"{slot} = getelementptr %ps.arg, ptr {base}, i32 0, i32 4")
977
+ self.line(f"store %ps.str {value}, ptr {slot}")
978
+
979
+ def _emit_json_loads(self, text: str, typ: dict[str, Any]) -> str:
980
+ symbol, root = self._json_global(typ)
981
+ length, ptr = self._buf_parts(text)
982
+ slot = self._json_alloca(typ)
983
+ self.line(f"call void @ps_json_loads(i64 {length}, ptr {ptr}, ptr {symbol}, i32 {root}, ptr {slot})")
984
+ return self._json_read(typ, slot)
985
+
986
+ def _emit_json_dumps(self, value: str, typ: dict[str, Any]) -> str:
987
+ symbol, root = self._json_global(typ)
988
+ slot = self._json_write(typ, value)
989
+ return self._buf_out("ps_json_dumps", [f"ptr {symbol}", f"i32 {root}", f"ptr {slot}"])
990
+
991
+ def _json_global(self, typ: dict[str, Any]) -> tuple[str, int]:
992
+ entries: list[tuple[int, int] | None] = []
993
+
994
+ def add(node: dict[str, Any]) -> int:
995
+ idx = len(entries)
996
+ entries.append(None)
997
+ kind = node["kind"]
998
+ if kind == "int":
999
+ entries[idx] = (1, 0)
1000
+ elif kind == "bool":
1001
+ entries[idx] = (2, 0)
1002
+ elif kind == "str":
1003
+ entries[idx] = (3, 0)
1004
+ elif kind == "list":
1005
+ child = add(node["elem"])
1006
+ entries[idx] = (4, child)
1007
+ elif kind == "dict":
1008
+ child = add(node["value"])
1009
+ entries[idx] = (5, child)
1010
+ else:
1011
+ raise IRError(f"no JSON type for {kind}")
1012
+ return idx
1013
+
1014
+ root = add(typ)
1015
+ typed = ", ".join(f"%ps.json.ty {{ i32 {kind}, i32 {child} }}" for kind, child in entries if kind is not None)
1016
+ symbol = f"@ps.json.{self.json_n}"
1017
+ self.json_n += 1
1018
+ if "%ps.json.ty = type { i32, i32 }" not in self.globals:
1019
+ self.globals.append("%ps.json.ty = type { i32, i32 }")
1020
+ self.globals.append(f"{symbol} = private constant [{len(entries)} x %ps.json.ty] [{typed}]")
1021
+ return symbol, root
1022
+
1023
+ def _json_alloca(self, typ: dict[str, Any]) -> str:
1024
+ kind = typ["kind"]
1025
+ if kind == "bool":
1026
+ return self._alloca("i32")
1027
+ if kind in {"list", "dict"}:
1028
+ return self._alloca("ptr")
1029
+ return self._alloca(_llvm_type(typ))
1030
+
1031
+ def _json_read(self, typ: dict[str, Any], slot: str) -> str:
1032
+ kind = typ["kind"]
1033
+ if kind == "bool":
1034
+ raw = self._load("i32", slot)
1035
+ reg = self.tmp()
1036
+ self.line(f"{reg} = trunc i32 {raw} to i1")
1037
+ return reg
1038
+ if kind in {"list", "dict"}:
1039
+ return self._load("ptr", slot)
1040
+ return self._load(_llvm_type(typ), slot)
1041
+
1042
+ def _json_write(self, typ: dict[str, Any], value: str) -> str:
1043
+ slot = self._json_alloca(typ)
1044
+ kind = typ["kind"]
1045
+ if kind == "bool":
1046
+ wide = self.tmp()
1047
+ self.line(f"{wide} = zext i1 {value} to i32")
1048
+ self.line(f"store i32 {wide}, ptr {slot}")
1049
+ return slot
1050
+ if kind in {"list", "dict"}:
1051
+ self.line(f"store ptr {value}, ptr {slot}")
1052
+ return slot
1053
+ self.line(f"store {_llvm_type(typ)} {value}, ptr {slot}")
1054
+ return slot
1055
+
1056
+ def _buf_out(self, symbol: str, args: list[str]) -> str:
1057
+ out = self._alloca("%ps.str")
1058
+ self.line(f"call void @{symbol}({', '.join([*args, f'ptr {out}'])})")
1059
+ return self._load("%ps.str", out)
1060
+
1061
+ def _abi_decl(self, symbol: str, ret: str, params: list[str]) -> None:
1062
+ self.abi_decls[symbol] = (ret, params)
1063
+
1064
+ def _abi_lines(self) -> list[str]:
1065
+ lines = []
1066
+ for symbol, (ret, params) in self.abi_decls.items():
1067
+ if symbol == "exit":
1068
+ lines.append("declare void @exit(i32) noreturn")
1069
+ continue
1070
+ rendered = ", ".join(params)
1071
+ lines.append(f"declare {ret} @{symbol}({rendered})")
1072
+ return lines
1073
+
1074
+ def _call_void(self, name: str, args: list[str]) -> None:
1075
+ self.line(f"call void @{name}({', '.join(args)})")
1076
+
1077
+ def _flag(self, raw: str) -> str:
1078
+ reg = self.tmp()
1079
+ self.line(f"{reg} = icmp ne i32 {raw}, 0")
1080
+ return reg
1081
+
1082
+ def _alloc_class(self, name: str) -> str:
1083
+ self.needed.add("alloc")
1084
+ size_ptr = self.tmp()
1085
+ size = self.tmp()
1086
+ self.line(f"{size_ptr} = getelementptr %cls.{name}, ptr null, i64 1")
1087
+ self.line(f"{size} = ptrtoint ptr {size_ptr} to i64")
1088
+ return self._call_ret("ptr", "ps_alloc", [f"i64 {size}"])
1089
+
1090
+ def _c_pieces(self, value: str, typ: dict[str, Any]) -> list[tuple[str, str]]:
1091
+ kind = typ["kind"]
1092
+ if kind == "int":
1093
+ return [(f"i64 {value}", "i64")]
1094
+ if kind == "float":
1095
+ return [(f"double {value}", "double")]
1096
+ if kind in {"str", "bytes"}:
1097
+ length, ptr = self._buf_parts(value)
1098
+ return [(f"i64 {length}", "i64"), (f"ptr {ptr}", "ptr")]
1099
+ if kind in {"list", "dict", "class"}:
1100
+ return [(f"ptr {value}", "ptr")]
1101
+ raise IRError(f"cannot pass {kind} to a runtime call")
1102
+
1103
+ def _emit_abi(self, expr: dict[str, Any], values: list[str]) -> str:
1104
+ symbol = expr["symbol"]
1105
+ rendered: list[str] = []
1106
+ types: list[str] = []
1107
+ for value, arg in zip(values, expr["args"], strict=True):
1108
+ for piece, typ in self._c_pieces(value, arg["type"]):
1109
+ rendered.append(piece)
1110
+ types.append(typ)
1111
+ ret = expr["type"]
1112
+ kind = ret["kind"]
1113
+ if kind in {"str", "bytes"}:
1114
+ self._abi_decl(symbol, "void", [*types, "ptr"])
1115
+ return self._buf_out(symbol, rendered)
1116
+ if kind == "int":
1117
+ self._abi_decl(symbol, "i64", types)
1118
+ return self._call_ret("i64", symbol, rendered)
1119
+ if kind == "float":
1120
+ self._abi_decl(symbol, "double", types)
1121
+ return self._call_ret("double", symbol, rendered)
1122
+ if kind == "bool":
1123
+ self._abi_decl(symbol, "i32", types)
1124
+ return self._flag(self._call_ret("i32", symbol, rendered))
1125
+ if kind == "none":
1126
+ self._abi_decl(symbol, "void", types)
1127
+ self._call_void(symbol, rendered)
1128
+ return ""
1129
+ if kind == "list" or (kind == "class" and ret.get("name") in {"Deque", "TextIO", "BytesIO"}):
1130
+ self._abi_decl(symbol, "ptr", types)
1131
+ return self._call_ret("ptr", symbol, rendered)
1132
+ if kind == "class":
1133
+ self._abi_decl(symbol, "void", [*types, "ptr"])
1134
+ mem = self._alloc_class(ret["name"])
1135
+ self._call_void(symbol, [*rendered, f"ptr {mem}"])
1136
+ return mem
1137
+ raise IRError(f"cannot emit {symbol}")
1138
+
1139
+ def _emit_list_push(self, expr: dict[str, Any], values: list[str]) -> str:
1140
+ seq, elem = values
1141
+ slot = self._stash(expr["args"][1]["type"], elem)
1142
+ self._abi_decl("ps_seq_push", "void", ["ptr", "ptr"])
1143
+ self._call_void("ps_seq_push", [f"ptr {seq}", f"ptr {slot}"])
1144
+ return ""
1145
+
1146
+ def _emit_atexit(self, expr: dict[str, Any]) -> str:
1147
+ name = expr["args"][0]["value"]
1148
+ fn = self.functions.get(name)
1149
+ if fn is None:
1150
+ raise IRError(f"atexit.register target {name} was not lowered")
1151
+ symbol = _fn_symbol(name)
1152
+ wrap = f"ps.atexit.{self.atexit_n}"
1153
+ self.atexit_n += 1
1154
+ ret = fn["returnType"]
1155
+ if ret["kind"] == "none":
1156
+ body = f" call void @{symbol}()\n ret void\n"
1157
+ else:
1158
+ body = f" call {_llvm_type(ret)} @{symbol}()\n ret void\n"
1159
+ self.outline_lines.append(f"define internal void @{wrap}() {{\nentry:\n{body}}}")
1160
+ self._abi_decl("atexit", "i32", ["ptr"])
1161
+ reg = self.tmp()
1162
+ self.line(f"{reg} = call i32 @atexit(ptr @{wrap})")
1163
+ return ""
1164
+
1165
+ def _emit_exit(self, value: str) -> str:
1166
+ self._abi_decl("exit", "void", ["i32"])
1167
+ narrow = self.tmp()
1168
+ self.line(f"{narrow} = trunc i64 {value} to i32")
1169
+ self._call_void("exit", [f"i32 {narrow}"])
1170
+ self.line("unreachable")
1171
+ self.terminated = True
1172
+ return ""
1173
+
1174
+ def _emit_join(self, values: list[str]) -> str:
1175
+ count = len(values)
1176
+ slot = self.tmp()
1177
+ self.line(f"{slot} = alloca [{count} x %ps.str]")
1178
+ for index, value in enumerate(values):
1179
+ dest = self.tmp()
1180
+ self.line(f"{dest} = getelementptr [{count} x %ps.str], ptr {slot}, i64 0, i64 {index}")
1181
+ self.line(f"store %ps.str {value}, ptr {dest}")
1182
+ first = self.tmp()
1183
+ self.line(f"{first} = getelementptr [{count} x %ps.str], ptr {slot}, i64 0, i64 0")
1184
+ self._abi_decl("ps_path_join", "void", ["ptr", "i64", "ptr"])
1185
+ return self._buf_out("ps_path_join", [f"ptr {first}", f"i64 {count}"])
1186
+
1187
+ def _emit_deque_store(self, expr: dict[str, Any], values: list[str]) -> str:
1188
+ slot = self._stash(expr["args"][1]["type"], values[1])
1189
+ symbol = expr["symbol"]
1190
+ self._abi_decl(symbol, "void", ["ptr", "ptr"])
1191
+ self._call_void(symbol, [f"ptr {values[0]}", f"ptr {slot}"])
1192
+ return ""
1193
+
1194
+ def _emit_deque_load(self, expr: dict[str, Any], values: list[str]) -> str:
1195
+ out = self._alloca(_llvm_type(expr["type"]))
1196
+ symbol = expr["symbol"]
1197
+ self._abi_decl(symbol, "void", ["ptr", "ptr"])
1198
+ self._call_void(symbol, [f"ptr {values[0]}", f"ptr {out}"])
1199
+ return self._load(_llvm_type(expr["type"]), out)
1200
+
1201
+ def _emit_class_cmp(self, expr: dict[str, Any], left: str, right: str) -> str:
1202
+ name = expr["left"]["type"]["name"]
1203
+ op = expr["op"]
1204
+ if name == "Path":
1205
+ left_ptr = self.tmp()
1206
+ right_ptr = self.tmp()
1207
+ self.line(f"{left_ptr} = getelementptr %cls.Path, ptr {left}, i32 0, i32 0")
1208
+ self.line(f"{right_ptr} = getelementptr %cls.Path, ptr {right}, i32 0, i32 0")
1209
+ cmp = self._buf_cmp(self._load("%ps.str", left_ptr), self._load("%ps.str", right_ptr))
1210
+ else:
1211
+ count = len(self.class_index[name]["fields"])
1212
+ self._abi_decl("ps_i64_lex", "i32", ["ptr", "ptr", "i64"])
1213
+ cmp = self._call_ret("i32", "ps_i64_lex", [f"ptr {left}", f"ptr {right}", f"i64 {count}"])
1214
+ pred = {"==": "eq", "!=": "ne", "<": "slt", "<=": "sle", ">": "sgt", ">=": "sge"}[op]
1215
+ reg = self.tmp()
1216
+ self.line(f"{reg} = icmp {pred} i32 {cmp}, 0")
1217
+ return reg
1218
+
1219
+ def _emit_class_binary(self, expr: dict[str, Any], left: str, right: str) -> str:
1220
+ left_name = expr["left"]["type"]["name"]
1221
+ right_name = expr["right"]["type"]["name"]
1222
+ out_name = expr["type"]["name"]
1223
+ mem = self._alloc_class(out_name)
1224
+ sign = "1" if expr["op"] == "+" else "-1"
1225
+ if left_name in {"Date", "DateTime"} and right_name == "TimeDelta":
1226
+ symbol = "ps_date_add" if left_name == "Date" else "ps_dt_add"
1227
+ self._abi_decl(symbol, "void", ["ptr", "ptr", "i32", "ptr"])
1228
+ self._call_void(symbol, [f"ptr {left}", f"ptr {right}", f"i32 {sign}", f"ptr {mem}"])
1229
+ return mem
1230
+ if left_name == "TimeDelta" and right_name == "TimeDelta":
1231
+ self._abi_decl("ps_delta_add", "void", ["ptr", "ptr", "i32", "ptr"])
1232
+ self._call_void("ps_delta_add", [f"ptr {left}", f"ptr {right}", f"i32 {sign}", f"ptr {mem}"])
1233
+ return mem
1234
+ if expr["op"] == "-" and left_name == right_name and left_name in {"Date", "DateTime"}:
1235
+ symbol = "ps_date_diff" if left_name == "Date" else "ps_dt_diff"
1236
+ self._abi_decl(symbol, "void", ["ptr", "ptr", "ptr"])
1237
+ self._call_void(symbol, [f"ptr {left}", f"ptr {right}", f"ptr {mem}"])
1238
+ return mem
1239
+ raise IRError(f"cannot emit {expr['op']} on {left_name}")
1240
+
1241
+ def _emit_minmax(self, name: str, values: list[str], arg_types: list[dict[str, Any]]) -> str:
1242
+ kind = arg_types[0]["kind"]
1243
+ left, right = values
1244
+ if kind == "int":
1245
+ pred = "sle" if name == "min" else "sge"
1246
+ cmp = self.tmp()
1247
+ self.line(f"{cmp} = icmp {pred} i64 {left}, {right}")
1248
+ reg = self.tmp()
1249
+ self.line(f"{reg} = select i1 {cmp}, i64 {left}, i64 {right}")
1250
+ return reg
1251
+ if kind == "float":
1252
+ pred = "ole" if name == "min" else "oge"
1253
+ cmp = self.tmp()
1254
+ self.line(f"{cmp} = fcmp {pred} double {left}, {right}")
1255
+ reg = self.tmp()
1256
+ self.line(f"{reg} = select i1 {cmp}, double {left}, double {right}")
1257
+ return reg
1258
+ cmp = self._buf_cmp(left, right)
1259
+ pred = "sle" if name == "min" else "sge"
1260
+ flag = self.tmp()
1261
+ self.line(f"{flag} = icmp {pred} i32 {cmp}, 0")
1262
+ reg = self.tmp()
1263
+ self.line(f"{reg} = select i1 {flag}, %ps.str {left}, %ps.str {right}")
1264
+ return reg
1265
+
1266
+ def _emit_reduce(self, expr: dict[str, Any]) -> str:
1267
+ seq = self.emit_expr(expr["args"][0])
1268
+ elem = expr["args"][0]["type"]["elem"]
1269
+ llvm_t = _llvm_type(elem)
1270
+ self.needed.add("seq")
1271
+ length = self._call_ret("i64", "ps_seq_len", [f"ptr {seq}"])
1272
+ acc_slot = self._alloca(llvm_t)
1273
+ idx_slot = self._alloca("i64")
1274
+ if len(expr["args"]) == 2:
1275
+ init = self.emit_expr(expr["args"][1])
1276
+ self.line(f"store {llvm_t} {init}, ptr {acc_slot}")
1277
+ self.line(f"store i64 0, ptr {idx_slot}")
1278
+ else:
1279
+ empty = self.tmp()
1280
+ self.line(f"{empty} = icmp eq i64 {length}, 0")
1281
+ bad = self.fresh("red.empty")
1282
+ ok = self.fresh("red.start")
1283
+ self.line(f"br i1 {empty}, label %{bad}, label %{ok}")
1284
+ self.terminated = True
1285
+ self.block(bad)
1286
+ self.needed.add("exc")
1287
+ self.line("call void @ps_raise_code(i32 1) noreturn")
1288
+ self.line("unreachable")
1289
+ self.terminated = True
1290
+ self.block(ok)
1291
+ first = self._seq_at(seq, "0", elem)
1292
+ self.line(f"store {llvm_t} {first}, ptr {acc_slot}")
1293
+ self.line(f"store i64 1, ptr {idx_slot}")
1294
+ cond_l = self.fresh("red.cond")
1295
+ body_l = self.fresh("red.body")
1296
+ end_l = self.fresh("red.end")
1297
+ self.line(f"br label %{cond_l}")
1298
+ self.terminated = True
1299
+ self.block(cond_l)
1300
+ idx = self._load("i64", idx_slot)
1301
+ cmp = self.tmp()
1302
+ self.line(f"{cmp} = icmp slt i64 {idx}, {length}")
1303
+ self.line(f"br i1 {cmp}, label %{body_l}, label %{end_l}")
1304
+ self.terminated = True
1305
+ self.block(body_l)
1306
+ current = self._load(llvm_t, acc_slot)
1307
+ item = self._seq_at(seq, idx, elem)
1308
+ nxt = self.call_function(self.functions[expr["fn"]], [current, item])
1309
+ self.line(f"store {llvm_t} {nxt}, ptr {acc_slot}")
1310
+ bumped = self.tmp()
1311
+ self.line(f"{bumped} = add i64 {idx}, 1")
1312
+ self.line(f"store i64 {bumped}, ptr {idx_slot}")
1313
+ self.line(f"br label %{cond_l}")
1314
+ self.terminated = True
1315
+ self.block(end_l)
1316
+ return self._load(llvm_t, acc_slot)
1317
+
1318
+ def _seq_at(self, seq: str, index: str, elem: dict[str, Any]) -> str:
1319
+ self.needed.add("seq")
1320
+ out = self._alloca(_llvm_type(elem))
1321
+ self.line(f"call void @ps_seq_get(ptr {seq}, i64 {index}, ptr {out})")
1322
+ return self._load(_llvm_type(elem), out)
1323
+
1324
+ def emit_method(self, expr: dict[str, Any], values: list[str]) -> str:
1325
+ callee = expr["callee"]
1326
+ name = callee["name"]
1327
+ kind = str(callee.get("receiver", "str"))
1328
+ self.needed.add("str")
1329
+ length, ptr = self._buf_parts(values[0])
1330
+ if name in {"strip", "lstrip", "rstrip", "upper", "lower"}:
1331
+ out = self._alloca("%ps.str")
1332
+ self.line(f"call void @ps_str_{name}(i64 {length}, ptr {ptr}, ptr {out})")
1333
+ return self._load("%ps.str", out)
1334
+ if name in {"title", "capitalize", "casefold", "swapcase"}:
1335
+ self._abi_decl(f"ps_str_{name}", "void", ["i64", "ptr", "ptr"])
1336
+ return self._buf_out(f"ps_str_{name}", [f"i64 {length}", f"ptr {ptr}"])
1337
+ if name in {"isdigit", "isalpha", "isalnum", "isspace", "isupper", "islower"}:
1338
+ self._abi_decl(f"ps_str_{name}", "i32", ["i64", "ptr"])
1339
+ return self._flag(self._call_ret("i32", f"ps_str_{name}", [f"i64 {length}", f"ptr {ptr}"]))
1340
+ if name == "encode":
1341
+ self._abi_decl("ps_utf8_encode", "void", ["i64", "ptr", "ptr"])
1342
+ return self._buf_out("ps_utf8_encode", [f"i64 {length}", f"ptr {ptr}"])
1343
+ if name == "decode":
1344
+ self._abi_decl("ps_utf8_decode", "void", ["i64", "ptr", "ptr"])
1345
+ return self._buf_out("ps_utf8_decode", [f"i64 {length}", f"ptr {ptr}"])
1346
+ if name == "splitlines":
1347
+ self._abi_decl("ps_str_splitlines", "ptr", ["i64", "ptr", "i64"])
1348
+ return self._call_ret("ptr", "ps_str_splitlines", [f"i64 {length}", f"ptr {ptr}", "i64 0"])
1349
+ if name == "expandtabs":
1350
+ tabsize = values[1] if len(values) > 1 else "8"
1351
+ self._abi_decl("ps_str_expandtabs", "void", ["i64", "ptr", "i64", "ptr"])
1352
+ return self._buf_out("ps_str_expandtabs", [f"i64 {length}", f"ptr {ptr}", f"i64 {tabsize}"])
1353
+ if name == "zfill":
1354
+ self._abi_decl("ps_str_zfill", "void", ["i64", "ptr", "i64", "ptr"])
1355
+ return self._buf_out("ps_str_zfill", [f"i64 {length}", f"ptr {ptr}", f"i64 {values[1]}"])
1356
+ if name in {"ljust", "rjust", "center"}:
1357
+ side = {"ljust": "0", "rjust": "1", "center": "2"}[name]
1358
+ if len(values) > 2:
1359
+ fill_len, fill_ptr = self._buf_parts(values[2])
1360
+ else:
1361
+ fill_len, fill_ptr = "1", self.format_global("fill", b" ")
1362
+ self._abi_decl("ps_str_justify", "void", ["i64", "ptr", "i64", "i64", "ptr", "i64", "ptr"])
1363
+ return self._buf_out(
1364
+ "ps_str_justify",
1365
+ [
1366
+ f"i64 {length}",
1367
+ f"ptr {ptr}",
1368
+ f"i64 {values[1]}",
1369
+ f"i64 {fill_len}",
1370
+ f"ptr {fill_ptr}",
1371
+ f"i64 {side}",
1372
+ ],
1373
+ )
1374
+ if name in {"partition", "rpartition"}:
1375
+ sep_len, sep_ptr = self._buf_parts(values[1])
1376
+ tname = callee["tuple"]
1377
+ mem = self._alloc_class(tname)
1378
+ right = "1" if name == "rpartition" else "0"
1379
+ self._abi_decl("ps_str_partition", "void", ["i64", "ptr", "i64", "ptr", "i64", "ptr"])
1380
+ self._call_void(
1381
+ "ps_str_partition",
1382
+ [
1383
+ f"i64 {length}",
1384
+ f"ptr {ptr}",
1385
+ f"i64 {sep_len}",
1386
+ f"ptr {sep_ptr}",
1387
+ f"i64 {right}",
1388
+ f"ptr {mem}",
1389
+ ],
1390
+ )
1391
+ return mem
1392
+ if name in {"split", "rsplit"}:
1393
+ if len(values) == 1:
1394
+ sep_len, sep_ptr = "0", "null"
1395
+ mode = "2" if kind == "bytes" else "1"
1396
+ maxsplit = "-1"
1397
+ else:
1398
+ sep_len, sep_ptr = self._buf_parts(values[1])
1399
+ mode = "0"
1400
+ maxsplit = values[2] if len(values) > 2 else "-1"
1401
+ right = "1" if name == "rsplit" else "0"
1402
+ self._abi_decl("ps_str_split_m", "ptr", ["i64", "ptr", "i64", "ptr", "i64", "i64", "i64"])
1403
+ return self._call_ret(
1404
+ "ptr",
1405
+ "ps_str_split_m",
1406
+ [
1407
+ f"i64 {length}",
1408
+ f"ptr {ptr}",
1409
+ f"i64 {sep_len}",
1410
+ f"ptr {sep_ptr}",
1411
+ f"i64 {mode}",
1412
+ f"i64 {maxsplit}",
1413
+ f"i64 {right}",
1414
+ ],
1415
+ )
1416
+ if name == "join":
1417
+ self._abi_decl("ps_str_join", "void", ["i64", "ptr", "ptr", "ptr"])
1418
+ return self._buf_out("ps_str_join", [f"i64 {length}", f"ptr {ptr}", f"ptr {values[1]}"])
1419
+ arg_len, arg_ptr = self._buf_parts(values[1])
1420
+ if name in {"removeprefix", "removesuffix"}:
1421
+ self._abi_decl(f"ps_str_{name}", "void", ["i64", "ptr", "i64", "ptr", "ptr"])
1422
+ return self._buf_out(f"ps_str_{name}", [f"i64 {length}", f"ptr {ptr}", f"i64 {arg_len}", f"ptr {arg_ptr}"])
1423
+ if name == "replace":
1424
+ new_len, new_ptr = self._buf_parts(values[2])
1425
+ out = self._alloca("%ps.str")
1426
+ self.line(
1427
+ "call void @ps_str_replace("
1428
+ f"i64 {length}, ptr {ptr}, i64 {arg_len}, ptr {arg_ptr}, i64 {new_len}, ptr {new_ptr}, ptr {out})"
1429
+ )
1430
+ return self._load("%ps.str", out)
1431
+ if name in {"find", "count", "index", "rfind"}:
1432
+ symbol = {"find": "ps_str_find", "count": "ps_str_count", "index": "ps_str_index", "rfind": "ps_str_rfind"}[
1433
+ name
1434
+ ]
1435
+ # ps_str_find is already declared by the "str" runtime block.
1436
+ if name != "find":
1437
+ self._abi_decl(symbol, "i64", ["i64", "ptr", "i64", "ptr"])
1438
+ return self._call_ret("i64", symbol, [f"i64 {length}", f"ptr {ptr}", f"i64 {arg_len}", f"ptr {arg_ptr}"])
1439
+ if name in {"startswith", "endswith"}:
1440
+ flag = self._call_ret(
1441
+ "i32", f"ps_str_{name}", [f"i64 {length}", f"ptr {ptr}", f"i64 {arg_len}", f"ptr {arg_ptr}"]
1442
+ )
1443
+ reg = self.tmp()
1444
+ self.line(f"{reg} = icmp ne i32 {flag}, 0")
1445
+ return reg
1446
+ raise IRError(f"no LLVM lowering for method {name}")
1447
+
1448
+ def emit_constructor(self, name: str, values: list[str]) -> str:
1449
+ self.needed.add("alloc")
1450
+ size_ptr = self.tmp()
1451
+ size = self.tmp()
1452
+ self.line(f"{size_ptr} = getelementptr %cls.{name}, ptr null, i64 1")
1453
+ self.line(f"{size} = ptrtoint ptr {size_ptr} to i64")
1454
+ mem = self._call_ret("ptr", "ps_alloc", [f"i64 {size}"])
1455
+ init = f"cls.{name}.__init__"
1456
+ if init in self.functions:
1457
+ fn = self.functions[init]
1458
+ self.call_function(fn, [mem, *values])
1459
+ return mem
1460
+
1461
+ def emit_list(self, expr: dict[str, Any]) -> str:
1462
+ self.needed.add("seq")
1463
+ elem = expr["type"]["elem"]
1464
+ values = [self.emit_expr(item) for item in expr["elems"]]
1465
+ reg = self._call_ret("ptr", "ps_seq_new", [f"i64 {_elem_size(elem)}", f"i64 {len(values)}"])
1466
+ for index, value in enumerate(values):
1467
+ slot = self._stash(elem, value)
1468
+ self.line(f"call void @ps_seq_set(ptr {reg}, i64 {index}, ptr {slot})")
1469
+ return reg
1470
+
1471
+ def emit_dict(self, expr: dict[str, Any]) -> str:
1472
+ self.needed.add("map")
1473
+ key_t = expr["type"]["key"]
1474
+ val_t = expr["type"]["value"]
1475
+ key_kind = 1 if key_t["kind"] == "int" else 2
1476
+ stored = [(self.emit_expr(pair["key"]), self.emit_expr(pair["value"])) for pair in expr["pairs"]]
1477
+ reg = self._call_ret(
1478
+ "ptr",
1479
+ "ps_map_new",
1480
+ [
1481
+ f"i32 {key_kind}",
1482
+ f"i64 {_elem_size(key_t)}",
1483
+ f"i64 {_elem_size(val_t)}",
1484
+ f"i64 {len(stored)}",
1485
+ ],
1486
+ )
1487
+ for key_v, val_v in stored:
1488
+ key = self._stash(key_t, key_v)
1489
+ value = self._stash(val_t, val_v)
1490
+ self.line(f"call void @ps_map_put(ptr {reg}, ptr {key}, ptr {value})")
1491
+ return reg
1492
+
1493
+ def emit_subscript(self, expr: dict[str, Any]) -> str:
1494
+ value = self.emit_expr(expr["value"])
1495
+ index = self.emit_expr(expr["index"])
1496
+ kind = expr["value"]["type"]["kind"]
1497
+ if kind == "list":
1498
+ self.needed.add("seq")
1499
+ out = self._alloca(_llvm_type(expr["type"]))
1500
+ self.line(f"call void @ps_seq_get(ptr {value}, i64 {index}, ptr {out})")
1501
+ return self._load(_llvm_type(expr["type"]), out)
1502
+ if kind == "dict":
1503
+ self.needed.add("map")
1504
+ key = self._stash(expr["index"]["type"], index)
1505
+ out = self._alloca(_llvm_type(expr["type"]))
1506
+ self.line(f"call void @ps_map_get(ptr {value}, ptr {key}, ptr {out})")
1507
+ return self._load(_llvm_type(expr["type"]), out)
1508
+ if kind == "str":
1509
+ self.needed.add("str")
1510
+ length, ptr = self._buf_parts(value)
1511
+ out = self._alloca("%ps.str")
1512
+ self.line(f"call void @ps_str_get(i64 {length}, ptr {ptr}, i64 {index}, ptr {out})")
1513
+ return self._load("%ps.str", out)
1514
+ if kind == "bytes":
1515
+ self.needed.add("str")
1516
+ length, ptr = self._buf_parts(value)
1517
+ return self._call_ret("i64", "ps_buf_at", [f"i64 {length}", f"ptr {ptr}", f"i64 {index}"])
1518
+ if kind == "class" and expr["value"]["type"].get("name") == "Deque":
1519
+ out = self._alloca(_llvm_type(expr["type"]))
1520
+ self._abi_decl("ps_deque_get", "void", ["ptr", "i64", "ptr"])
1521
+ self._call_void("ps_deque_get", [f"ptr {value}", f"i64 {index}", f"ptr {out}"])
1522
+ return self._load(_llvm_type(expr["type"]), out)
1523
+ raise IRError(f"cannot index {kind}")
1524
+
1525
+ def emit_attr(self, expr: dict[str, Any]) -> str:
1526
+ obj = self.emit_expr(expr["value"])
1527
+ field = self._field(expr["value"]["type"]["name"], expr["name"])
1528
+ slot = self._field_ptr(expr["value"]["type"]["name"], obj, field)
1529
+ return self._load(_llvm_type(field["type"]), slot)
1530
+
1531
+ def emit_contains(self, expr: dict[str, Any]) -> str:
1532
+ needle = self.emit_expr(expr["needle"])
1533
+ hay = self.emit_expr(expr["hay"])
1534
+ hay_kind = expr["hay"]["type"]["kind"]
1535
+ if hay_kind in {"str", "bytes"}:
1536
+ self.needed.add("str")
1537
+ hay_len, hay_ptr = self._buf_parts(hay)
1538
+ needle_len, needle_ptr = self._buf_parts(needle)
1539
+ found = self._call_ret(
1540
+ "i64", "ps_str_find", [f"i64 {hay_len}", f"ptr {hay_ptr}", f"i64 {needle_len}", f"ptr {needle_ptr}"]
1541
+ )
1542
+ flag = self.tmp()
1543
+ self.line(f"{flag} = icmp sge i64 {found}, 0")
1544
+ elif hay_kind == "list":
1545
+ self.needed.add("seq")
1546
+ elem = expr["hay"]["type"]["elem"]["kind"]
1547
+ if elem == "int":
1548
+ flag = self._call_ret("i32", "ps_seq_has_i64", [f"ptr {hay}", f"i64 {needle}"])
1549
+ elif elem == "float":
1550
+ flag = self._call_ret("i32", "ps_seq_has_f64", [f"ptr {hay}", f"double {needle}"])
1551
+ else:
1552
+ length, ptr = self._buf_parts(needle)
1553
+ flag = self._call_ret("i32", "ps_seq_has_buf", [f"ptr {hay}", f"i64 {length}", f"ptr {ptr}"])
1554
+ bit = self.tmp()
1555
+ self.line(f"{bit} = icmp ne i32 {flag}, 0")
1556
+ flag = bit
1557
+ elif hay_kind == "dict":
1558
+ self.needed.add("map")
1559
+ key = self._stash(expr["needle"]["type"], needle)
1560
+ raw = self._call_ret("i32", "ps_map_has", [f"ptr {hay}", f"ptr {key}"])
1561
+ flag = self.tmp()
1562
+ self.line(f"{flag} = icmp ne i32 {raw}, 0")
1563
+ else:
1564
+ raise IRError(f"cannot emit containment for {hay_kind}")
1565
+ if not expr["negated"]:
1566
+ return flag
1567
+ reg = self.tmp()
1568
+ self.line(f"{reg} = xor i1 {flag}, true")
1569
+ return reg
1570
+
1571
+ def _field(self, class_name: str, name: str) -> dict[str, Any]:
1572
+ for index, field in enumerate(self.class_index[class_name]["fields"]):
1573
+ if field["name"] == name:
1574
+ return {**field, "index": index}
1575
+ raise IRError(f"{class_name}.{name} is not a field")
1576
+
1577
+ def _field_ptr(self, class_name: str, obj: str, field: dict[str, Any]) -> str:
1578
+ reg = self.tmp()
1579
+ self.line(f"{reg} = getelementptr %cls.{class_name}, ptr {obj}, i32 0, i32 {field['index']}")
1580
+ return reg
1581
+
1582
+ def _stash(self, typ: dict[str, Any], value: str) -> str:
1583
+ slot = self._alloca(_llvm_type(typ))
1584
+ self.line(f"store {_llvm_type(typ)} {value}, ptr {slot}")
1585
+ return slot
1586
+
1587
+ def _alloca(self, llvm_type: str) -> str:
1588
+ slot = self.tmp()
1589
+ self.line(f"{slot} = alloca {llvm_type}")
1590
+ return slot
1591
+
1592
+ def _load(self, llvm_type: str, slot: str) -> str:
1593
+ reg = self.tmp()
1594
+ self.line(f"{reg} = load {llvm_type}, ptr {slot}")
1595
+ return reg
1596
+
1597
+ def _call_ret(self, ret: str, name: str, args: list[str]) -> str:
1598
+ reg = self.tmp()
1599
+ self.line(f"{reg} = call {ret} @{name}({', '.join(args)})")
1600
+ return reg
1601
+
1602
+ def _buf_parts(self, value: str) -> tuple[str, str]:
1603
+ length = self.tmp()
1604
+ ptr = self.tmp()
1605
+ self.line(f"{length} = extractvalue %ps.str {value}, 0")
1606
+ self.line(f"{ptr} = extractvalue %ps.str {value}, 1")
1607
+ return length, ptr
1608
+
1609
+ def _buf_cmp(self, left: str, right: str) -> str:
1610
+ self.needed.add("str")
1611
+ left_len, left_ptr = self._buf_parts(left)
1612
+ right_len, right_ptr = self._buf_parts(right)
1613
+ return self._call_ret(
1614
+ "i32", "ps_str_cmp", [f"i64 {left_len}", f"ptr {left_ptr}", f"i64 {right_len}", f"ptr {right_ptr}"]
1615
+ )
1616
+
1617
+ def call_function(self, fn: dict[str, Any], values: list[str]) -> str:
1618
+ ret = _llvm_type(fn["returnType"])
1619
+ rendered = ", ".join(
1620
+ f"{_llvm_type(param['type'])} {value}" for param, value in zip(fn["params"], values, strict=True)
1621
+ )
1622
+ symbol = _fn_symbol(fn["name"])
1623
+ if ret == "void":
1624
+ self.line(f"call void @{symbol}({rendered})")
1625
+ return ""
1626
+ reg = self.tmp()
1627
+ self.line(f"{reg} = call {ret} @{symbol}({rendered})")
1628
+ return reg
1629
+
1630
+ def helper_call(self, name: str, ret: str, args: list[str], arg_type: str = "i64") -> str:
1631
+ self.needed.add(name)
1632
+ rendered = ", ".join(f"{arg_type} {arg}" for arg in args)
1633
+ reg = self.tmp()
1634
+ self.line(f"{reg} = call {ret} @ps.{name}({rendered})")
1635
+ return reg
1636
+
1637
+ def string_value(self, text: str, binary: bool = False) -> str:
1638
+ data = text.encode("latin1")
1639
+ idx = len(self.string_data)
1640
+ self.string_data.append(data)
1641
+ symbol = f"@ps.str.{idx}"
1642
+ self.globals.append(f'{symbol} = private unnamed_addr constant [{len(data) + 1} x i8] c"{_llvm_bytes(data)}"')
1643
+ first = self.tmp()
1644
+ second = self.tmp()
1645
+ self.line(f"{first} = insertvalue %ps.str poison, i64 {len(data)}, 0")
1646
+ self.line(f"{second} = insertvalue %ps.str {first}, ptr {symbol}, 1")
1647
+ return second
1648
+
1649
+ def materialize_arg(self, typ: dict[str, Any], value: Any) -> str:
1650
+ kind = typ["kind"]
1651
+ if kind == "int":
1652
+ return str(value)
1653
+ if kind == "float":
1654
+ return _llvm_double(value)
1655
+ if kind == "bool":
1656
+ return "true" if value else "false"
1657
+ if kind == "str":
1658
+ return self.string_value(value)
1659
+ raise FitError(f"cannot pass a {kind} argument into main")
1660
+
1661
+ def print_value(self, typ: dict[str, Any], value: str) -> None:
1662
+ kind = typ["kind"]
1663
+ self.needed.add("printf")
1664
+ if kind == "int":
1665
+ fmt = self.format_global("int", b"%lld\n")
1666
+ self.line(f"call i32 (ptr, ...) @printf(ptr {fmt}, i64 {value})")
1667
+ return
1668
+ if kind == "float":
1669
+ fmt = self.format_global("float", b"%.17g\n")
1670
+ self.line(f"call i32 (ptr, ...) @printf(ptr {fmt}, double {value})")
1671
+ return
1672
+ if kind == "str":
1673
+ fmt = self.format_global("str", b"%.*s\n")
1674
+ length = self.tmp()
1675
+ ptr = self.tmp()
1676
+ self.line(f"{length} = extractvalue %ps.str {value}, 0")
1677
+ self.line(f"{ptr} = extractvalue %ps.str {value}, 1")
1678
+ self.line(f"call i32 (ptr, ...) @printf(ptr {fmt}, i64 {length}, ptr {ptr})")
1679
+ return
1680
+ raise IRError(f"cannot print {kind}")
1681
+
1682
+ def _emit_print(self, values: list[str], arg_types: list[dict[str, Any]], expr: dict[str, Any]) -> None:
1683
+ """`print` with CPython defaults: space separator, newline terminator.
1684
+
1685
+ `sep=`/`end=` str arguments ride on the call dict; when absent the
1686
+ constants hold. Each value writes through `printf`, mirroring
1687
+ `print_value`'s int and str pieces. Floats go through `ps_f64_str` so
1688
+ the text is the CPython `str(float)` spelling (shortest round-trip,
1689
+ `.0`, `1e+16`), which a raw `%g` conversion cannot reproduce. Bools
1690
+ select between the `True` and `False` constants, not the runtime's
1691
+ lowercase literals. Bytes print as their `b'...'` repr.
1692
+ """
1693
+ self.needed.add("printf")
1694
+ sep = self.emit_expr(expr["sep"]) if expr.get("sep") else None
1695
+ end = self.emit_expr(expr["end"]) if expr.get("end") else None
1696
+ for index, (value, typ) in enumerate(zip(values, arg_types, strict=True)):
1697
+ if index:
1698
+ if sep is None:
1699
+ self._print_fmt("space", b" ")
1700
+ else:
1701
+ slen, sptr = self._buf_parts(sep)
1702
+ self._print_fmt("sep", b"%.*s", [f"i64 {slen}", f"ptr {sptr}"])
1703
+ kind = typ["kind"]
1704
+ if kind == "int":
1705
+ self._print_fmt("int", b"%lld", [f"i64 {value}"])
1706
+ elif kind == "float":
1707
+ out = self._buf_out("ps_f64_str", [f"double {value}"])
1708
+ length, ptr = self._buf_parts(out)
1709
+ self._abi_decl("ps_f64_str", "void", ["double", "ptr"])
1710
+ self._print_fmt("str", b"%.*s", [f"i64 {length}", f"ptr {ptr}"])
1711
+ elif kind == "bool":
1712
+ picked = self.tmp()
1713
+ self.line(
1714
+ f"{picked} = select i1 {value}, ptr {self.format_global('boolT', b'True')}, "
1715
+ f"ptr {self.format_global('boolF', b'False')}"
1716
+ )
1717
+ self._print_fmt("s", b"%s", [f"ptr {picked}"])
1718
+ elif kind == "bytes":
1719
+ self._abi_decl("ps_repr_bytes", "void", ["i64", "ptr", "ptr"])
1720
+ rlen, rptr = self._buf_parts(value)
1721
+ out = self._buf_out("ps_repr_bytes", [f"i64 {rlen}", f"ptr {rptr}"])
1722
+ length, ptr = self._buf_parts(out)
1723
+ self._print_fmt("str", b"%.*s", [f"i64 {length}", f"ptr {ptr}"])
1724
+ else:
1725
+ length, ptr = self._buf_parts(value)
1726
+ self._print_fmt("str", b"%.*s", [f"i64 {length}", f"ptr {ptr}"])
1727
+ if end is None:
1728
+ self._print_fmt("nl", b"\n")
1729
+ else:
1730
+ elen, eptr = self._buf_parts(end)
1731
+ self._print_fmt("end", b"%.*s", [f"i64 {elen}", f"ptr {eptr}"])
1732
+
1733
+ def _print_fmt(self, name: str, data: bytes, args: list[str] | None = None) -> None:
1734
+ fmt = self.format_global(name, data)
1735
+ rendered = ", ".join([f"ptr {fmt}", *(args or [])])
1736
+ self.line(f"call i32 (ptr, ...) @printf({rendered})")
1737
+
1738
+ def format_global(self, name: str, data: bytes) -> str:
1739
+ base = f"@ps.fmt.{name}"
1740
+ body = f'private unnamed_addr constant [{len(data) + 1} x i8] c"{_llvm_bytes(data)}"'
1741
+ defined = {g.split(" = ", 1)[0] for g in self.globals}
1742
+ symbol = base
1743
+ suffix = 0
1744
+ while symbol in defined and f"{symbol} = {body}" not in self.globals:
1745
+ suffix += 1
1746
+ symbol = f"{base}.{suffix}"
1747
+ if symbol not in defined:
1748
+ self.globals.append(f"{symbol} = {body}")
1749
+ return symbol
1750
+
1751
+ def store(self, name: str, value: str) -> None:
1752
+ ptr, typ = self.slots[name]
1753
+ self.line(f"store {_llvm_type(typ)} {value}, ptr {ptr}")
1754
+
1755
+ def load(self, name: str) -> str:
1756
+ ptr, typ = self.slots[name]
1757
+ reg = self.tmp()
1758
+ self.line(f"{reg} = load {_llvm_type(typ)}, ptr {ptr}")
1759
+ return reg
1760
+
1761
+ def block(self, name: str) -> None:
1762
+ self.out.append(f"{name}:")
1763
+ self.terminated = False
1764
+
1765
+ def fresh(self, hint: str) -> str:
1766
+ self.lid += 1
1767
+ return f"L{self.lid}.{hint}"
1768
+
1769
+ def tmp(self) -> str:
1770
+ self.tid += 1
1771
+ return f"%t{self.tid}"
1772
+
1773
+ def line(self, text: str) -> None:
1774
+ self.out.append(f" {text}")
1775
+
1776
+ def _reset_function(self) -> None:
1777
+ self.tid = 0
1778
+ self.lid = 0
1779
+ self.slots = {}
1780
+ self.terminated = False
1781
+ self.try_depth = 0
1782
+ self.outline_depth = 0
1783
+
1784
+
1785
+ def _fn_symbol(name: str) -> str:
1786
+ """The process entry point is ``@main``. A user function of that name gets another symbol."""
1787
+ if name == "main":
1788
+ return "ps.user.main"
1789
+ return name
1790
+
1791
+
1792
+ def _ir_const_value(typ: dict[str, Any], value: Any) -> Any:
1793
+ kind = typ["kind"]
1794
+ if kind == "int":
1795
+ return int(value)
1796
+ if kind == "float":
1797
+ return float(value)
1798
+ return value
1799
+
1800
+
1801
+ def _prepare_arg(param: dict[str, Any], value: Any) -> Any:
1802
+ kind = param["type"]["kind"]
1803
+ name = param["name"]
1804
+ if kind == "int":
1805
+ if isinstance(value, bool) or not isinstance(value, int):
1806
+ raise FitError(f"argument {name} must be an int")
1807
+ if not fits_i64(value):
1808
+ raise FitError(f"argument {name} does not fit in i64; refusing to truncate")
1809
+ return value
1810
+ if kind == "float":
1811
+ if isinstance(value, bool) or not isinstance(value, float):
1812
+ raise FitError(f"argument {name} must be a float")
1813
+ return value
1814
+ if kind == "bool":
1815
+ if not isinstance(value, bool):
1816
+ raise FitError(f"argument {name} must be a bool")
1817
+ return value
1818
+ if kind == "str":
1819
+ if not isinstance(value, str):
1820
+ raise FitError(f"argument {name} must be a str")
1821
+ if any(ord(ch) > 255 for ch in value):
1822
+ raise FitError(f"argument {name} is outside Latin-1 and is not lowered")
1823
+ return value
1824
+ raise FitError(f"argument {name} has type {kind}, which main cannot pass")
1825
+
1826
+
1827
+ def _llvm_double(value: float) -> str:
1828
+ """IEEE-754 bits. Clang 18 rejects the 0x1.0p-1 spelling in call arguments."""
1829
+
1830
+ bits = struct.unpack("<Q", struct.pack("<d", value))[0]
1831
+ return f"0x{bits:016X}"
1832
+
1833
+
1834
+ def _llvm_type(typ: dict[str, Any]) -> str:
1835
+ kind = typ["kind"]
1836
+ if kind == "int":
1837
+ return "i64"
1838
+ if kind == "bool":
1839
+ return "i1"
1840
+ if kind == "float":
1841
+ return "double"
1842
+ if kind in {"str", "bytes"}:
1843
+ return "%ps.str"
1844
+ if kind in {"list", "dict", "class"}:
1845
+ return "ptr"
1846
+ if kind == "none":
1847
+ return "void"
1848
+ raise IRError(f"no LLVM type for {kind}")
1849
+
1850
+
1851
+ def _elem_size(typ: dict[str, Any]) -> int:
1852
+ """Bytes copied into a list or dict slot. Matches the LLVM layout on this ABI."""
1853
+
1854
+ kind = typ["kind"]
1855
+ if kind in {"int", "float"}:
1856
+ return 8
1857
+ if kind in {"str", "bytes"}:
1858
+ return 16
1859
+ if kind in {"list", "dict", "class"}:
1860
+ return 8
1861
+ raise IRError(f"no element size for {kind}")
1862
+
1863
+
1864
+ def _safe(name: str) -> str:
1865
+ return name.replace(".", "_")
1866
+
1867
+
1868
+ def _llvm_bytes(data: bytes) -> str:
1869
+ """LLVM ``c"..."`` escapes. A quote is ``\\22``; ``\\"`` ends the literal early."""
1870
+
1871
+ parts: list[str] = []
1872
+ for byte in data:
1873
+ if byte == ord("\\"):
1874
+ parts.append("\\\\")
1875
+ elif 32 <= byte < 127 and byte != ord('"'):
1876
+ parts.append(chr(byte))
1877
+ else:
1878
+ parts.append(f"\\{byte:02X}")
1879
+ parts.append("\\00")
1880
+ return "".join(parts)
1881
+
1882
+
1883
+ def _declarations(needed: set[str]) -> list[str]:
1884
+ lines: list[str] = []
1885
+ if "printf" in needed:
1886
+ lines.append("declare i32 @printf(ptr, ...)")
1887
+ overflow = needed & {"iadd", "isub", "imul", "ineg", "strconcat"}
1888
+ if overflow or "floordiv" in needed:
1889
+ lines.append("declare void @llvm.trap() noreturn nounwind")
1890
+ if "iadd" in needed or "strconcat" in needed:
1891
+ lines.append("declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64)")
1892
+ if "isub" in needed or "ineg" in needed:
1893
+ lines.append("declare { i64, i1 } @llvm.ssub.with.overflow.i64(i64, i64)")
1894
+ if "imul" in needed:
1895
+ lines.append("declare { i64, i1 } @llvm.smul.with.overflow.i64(i64, i64)")
1896
+ if "strconcat" in needed:
1897
+ lines.append("declare ptr @malloc(i64)")
1898
+ lines.append("declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1)")
1899
+ lines.extend(_runtime_declarations(needed))
1900
+ return lines
1901
+
1902
+
1903
+ def _runtime_declarations(needed: set[str]) -> list[str]:
1904
+ lines: list[str] = []
1905
+ if "exc" in needed:
1906
+ lines.append("declare i32 @ps_try_call(ptr, ptr, ptr)")
1907
+ lines.append("declare void @ps_raise(i32) noreturn")
1908
+ lines.append("declare void @ps_raise_code(i32) noreturn")
1909
+ lines.append("declare void @ps_raise_msg(i32, i64, ptr) noreturn")
1910
+ lines.append("declare void @ps_reraise() noreturn")
1911
+ lines.append("declare i32 @ps_exc_code()")
1912
+ lines.append("declare i32 @ps_exc_isa(i32, i32)")
1913
+ if "alloc" in needed:
1914
+ lines.append("declare ptr @ps_alloc(i64)")
1915
+ if "seq" in needed:
1916
+ lines.extend(
1917
+ [
1918
+ "declare ptr @ps_seq_new(i64, i64)",
1919
+ "declare i64 @ps_seq_len(ptr)",
1920
+ "declare void @ps_seq_get(ptr, i64, ptr)",
1921
+ "declare void @ps_seq_set(ptr, i64, ptr)",
1922
+ "declare i32 @ps_seq_has_i64(ptr, i64)",
1923
+ "declare i32 @ps_seq_has_f64(ptr, double)",
1924
+ "declare i32 @ps_seq_has_buf(ptr, i64, ptr)",
1925
+ ]
1926
+ )
1927
+ if "map" in needed:
1928
+ lines.extend(
1929
+ [
1930
+ "declare ptr @ps_map_new(i32, i64, i64, i64)",
1931
+ "declare i64 @ps_map_len(ptr)",
1932
+ "declare void @ps_map_put(ptr, ptr, ptr)",
1933
+ "declare void @ps_map_get(ptr, ptr, ptr)",
1934
+ "declare i32 @ps_map_has(ptr, ptr)",
1935
+ ]
1936
+ )
1937
+ if "str" in needed:
1938
+ lines.extend(
1939
+ [
1940
+ "declare void @ps_str_get(i64, ptr, i64, ptr)",
1941
+ "declare i64 @ps_buf_at(i64, ptr, i64)",
1942
+ "declare void @ps_str_strip(i64, ptr, ptr)",
1943
+ "declare void @ps_str_lstrip(i64, ptr, ptr)",
1944
+ "declare void @ps_str_rstrip(i64, ptr, ptr)",
1945
+ "declare i32 @ps_str_startswith(i64, ptr, i64, ptr)",
1946
+ "declare i32 @ps_str_endswith(i64, ptr, i64, ptr)",
1947
+ "declare i64 @ps_str_find(i64, ptr, i64, ptr)",
1948
+ "declare i32 @ps_str_cmp(i64, ptr, i64, ptr)",
1949
+ "declare void @ps_str_upper(i64, ptr, ptr)",
1950
+ "declare void @ps_str_lower(i64, ptr, ptr)",
1951
+ "declare void @ps_str_replace(i64, ptr, i64, ptr, i64, ptr, ptr)",
1952
+ ]
1953
+ )
1954
+ if "math" in needed:
1955
+ lines.append("declare i64 @ps_float_to_int(double)")
1956
+ for spec in MATH_FUNCTIONS.values():
1957
+ symbol = spec["symbol"]
1958
+ if not symbol:
1959
+ continue
1960
+ params = ", ".join(["double"] * len(spec["params"]))
1961
+ lines.append(f"declare {spec['llvm']} @{symbol}({params})")
1962
+ if "fabs" in needed:
1963
+ lines.append("declare double @llvm.fabs.f64(double)")
1964
+ if "re" in needed:
1965
+ lines.extend(
1966
+ [
1967
+ "declare ptr @ps_re_findall(i64, ptr, i64, ptr)",
1968
+ "declare void @ps_re_sub(i64, ptr, i64, ptr, i64, ptr, ptr)",
1969
+ "declare ptr @ps_re_split(i64, ptr, i64, ptr)",
1970
+ "declare void @ps_re_escape(i64, ptr, ptr)",
1971
+ ]
1972
+ )
1973
+ if "json" in needed:
1974
+ lines.extend(
1975
+ [
1976
+ "declare void @ps_json_loads(i64, ptr, ptr, i32, ptr)",
1977
+ "declare void @ps_json_dumps(ptr, i32, ptr, ptr)",
1978
+ ]
1979
+ )
1980
+ if "url" in needed:
1981
+ lines.extend(
1982
+ [
1983
+ "declare void @ps_url_quote(i64, ptr, i64, ptr, ptr)",
1984
+ "declare void @ps_url_quote_plus(i64, ptr, i64, ptr, ptr)",
1985
+ "declare void @ps_url_unquote(i64, ptr, ptr)",
1986
+ "declare void @ps_url_unquote_plus(i64, ptr, ptr)",
1987
+ "declare void @ps_url_encode(ptr, ptr)",
1988
+ "declare ptr @ps_url_parse_qs(i64, ptr)",
1989
+ "declare void @ps_url_split(i64, ptr, ptr)",
1990
+ ]
1991
+ )
1992
+ if "b64" in needed:
1993
+ lines.extend(
1994
+ [
1995
+ "declare void @ps_b64_std_encode(i64, ptr, ptr)",
1996
+ "declare void @ps_b64_std_decode(i64, ptr, ptr)",
1997
+ "declare void @ps_b64_url_encode(i64, ptr, ptr)",
1998
+ "declare void @ps_b64_url_decode(i64, ptr, ptr)",
1999
+ ]
2000
+ )
2001
+ if "hash" in needed:
2002
+ lines.extend(
2003
+ [
2004
+ "declare ptr @ps_hash_md5(i64, ptr)",
2005
+ "declare ptr @ps_hash_sha1(i64, ptr)",
2006
+ "declare ptr @ps_hash_sha256(i64, ptr)",
2007
+ "declare ptr @ps_hash_sha384(i64, ptr)",
2008
+ "declare ptr @ps_hash_sha512(i64, ptr)",
2009
+ "declare void @ps_hash_hex(ptr, ptr)",
2010
+ "declare void @ps_hash_digest(ptr, ptr)",
2011
+ ]
2012
+ )
2013
+ if "struct" in needed:
2014
+ lines.extend(
2015
+ [
2016
+ "declare void @ps_struct_pack(i64, ptr, ptr, i64, ptr)",
2017
+ "declare ptr @ps_struct_unpack(i64, ptr, i64, ptr, i32)",
2018
+ ]
2019
+ )
2020
+ return lines
2021
+
2022
+
2023
+ def _helpers(needed: set[str]) -> list[str]:
2024
+ chunks: list[str] = []
2025
+ if "iadd" in needed:
2026
+ chunks.append(_overflow_helper("iadd", "sadd"))
2027
+ if "isub" in needed:
2028
+ chunks.append(_overflow_helper("isub", "ssub"))
2029
+ if "imul" in needed:
2030
+ chunks.append(_overflow_helper("imul", "smul"))
2031
+ if "ineg" in needed:
2032
+ chunks.append(
2033
+ """define internal i64 @ps.ineg(i64 %a) {
2034
+ entry:
2035
+ %r = call { i64, i1 } @llvm.ssub.with.overflow.i64(i64 0, i64 %a)
2036
+ %ov = extractvalue { i64, i1 } %r, 1
2037
+ br i1 %ov, label %trap, label %ok
2038
+ ok:
2039
+ %v = extractvalue { i64, i1 } %r, 0
2040
+ ret i64 %v
2041
+ trap:
2042
+ call void @llvm.trap()
2043
+ unreachable
2044
+ }
2045
+ """
2046
+ )
2047
+ if "floordiv" in needed:
2048
+ chunks.append(
2049
+ """define internal i64 @ps.floordiv(i64 %a, i64 %b) {
2050
+ entry:
2051
+ %zero = icmp eq i64 %b, 0
2052
+ br i1 %zero, label %trap, label %nonzero
2053
+ nonzero:
2054
+ %min = icmp eq i64 %a, -9223372036854775808
2055
+ %neg1 = icmp eq i64 %b, -1
2056
+ %bad = and i1 %min, %neg1
2057
+ br i1 %bad, label %trap, label %divide
2058
+ divide:
2059
+ %q = sdiv i64 %a, %b
2060
+ %r = srem i64 %a, %b
2061
+ %rnz = icmp ne i64 %r, 0
2062
+ %aneg = icmp slt i64 %a, 0
2063
+ %bneg = icmp slt i64 %b, 0
2064
+ %diff = icmp ne i1 %aneg, %bneg
2065
+ %adjust = and i1 %rnz, %diff
2066
+ br i1 %adjust, label %dec, label %keep
2067
+ dec:
2068
+ %q2 = sub i64 %q, 1
2069
+ ret i64 %q2
2070
+ keep:
2071
+ ret i64 %q
2072
+ trap:
2073
+ call void @llvm.trap()
2074
+ unreachable
2075
+ }
2076
+ """
2077
+ )
2078
+ if "strconcat" in needed:
2079
+ chunks.append(
2080
+ """define internal %ps.str @ps.strconcat(%ps.str %a, %ps.str %b) {
2081
+ entry:
2082
+ %na = extractvalue %ps.str %a, 0
2083
+ %pa = extractvalue %ps.str %a, 1
2084
+ %nb = extractvalue %ps.str %b, 0
2085
+ %pb = extractvalue %ps.str %b, 1
2086
+ %sum = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %na, i64 %nb)
2087
+ %ov = extractvalue { i64, i1 } %sum, 1
2088
+ br i1 %ov, label %trap, label %sized
2089
+ sized:
2090
+ %n = extractvalue { i64, i1 } %sum, 0
2091
+ %atmax = icmp eq i64 %n, 9223372036854775807
2092
+ br i1 %atmax, label %trap, label %alloc
2093
+ alloc:
2094
+ %n1 = add i64 %n, 1
2095
+ %mem = call ptr @malloc(i64 %n1)
2096
+ %null = icmp eq ptr %mem, null
2097
+ br i1 %null, label %trap, label %copy
2098
+ copy:
2099
+ call void @llvm.memcpy.p0.p0.i64(ptr %mem, ptr %pa, i64 %na, i1 false)
2100
+ %dst2 = getelementptr i8, ptr %mem, i64 %na
2101
+ call void @llvm.memcpy.p0.p0.i64(ptr %dst2, ptr %pb, i64 %nb, i1 false)
2102
+ %end = getelementptr i8, ptr %mem, i64 %n
2103
+ store i8 0, ptr %end
2104
+ %s0 = insertvalue %ps.str poison, i64 %n, 0
2105
+ %s1 = insertvalue %ps.str %s0, ptr %mem, 1
2106
+ ret %ps.str %s1
2107
+ trap:
2108
+ call void @llvm.trap()
2109
+ unreachable
2110
+ }
2111
+ """
2112
+ )
2113
+ return chunks
2114
+
2115
+
2116
+ def _overflow_helper(name: str, intrinsic: str) -> str:
2117
+ return f"""define internal i64 @ps.{name}(i64 %a, i64 %b) {{
2118
+ entry:
2119
+ %r = call {{ i64, i1 }} @llvm.{intrinsic}.with.overflow.i64(i64 %a, i64 %b)
2120
+ %ov = extractvalue {{ i64, i1 }} %r, 1
2121
+ br i1 %ov, label %trap, label %ok
2122
+ ok:
2123
+ %v = extractvalue {{ i64, i1 }} %r, 0
2124
+ ret i64 %v
2125
+ trap:
2126
+ call void @llvm.trap()
2127
+ unreachable
2128
+ }}
2129
+ """