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/coverage.py ADDED
@@ -0,0 +1,388 @@
1
+ """Collect-all fence report behind `pyscriptc coverage`.
2
+
3
+ Lowering already keeps going after a fence inside a body, a class, or a module
4
+ statement. This module runs it on a file or a package tree, then classifies
5
+ each fence into a feature family, attaches a refactor hint and a code frame,
6
+ and groups the fences by the definition that holds them. It never changes how
7
+ `compile` treats a fence.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ast
13
+ import re
14
+ from collections import Counter, defaultdict
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from pyscriptc.errors import IRError, format_diagnostic
20
+ from pyscriptc.lower import lower_file
21
+ from pyscriptc.package import _discover, lower_directory
22
+
23
+ SCHEMA_VERSION = 1
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Family:
28
+ key: str
29
+ hint: str
30
+ derived: bool = False
31
+
32
+
33
+ _FAMILIES = {
34
+ family.key: family
35
+ for family in (
36
+ Family(
37
+ "union-types",
38
+ "Narrow the annotation to one arm, or split the function per arm. A tagged union is a compiler epic.",
39
+ ),
40
+ Family(
41
+ "optional-types",
42
+ "Accept or return T and signal absence with a sentinel. Optional[T] needs the tagged-union epic.",
43
+ ),
44
+ Family(
45
+ "generic-alias", "Use list[T], dict[K, V] or tuple[...] in the signature instead of the abstract generic."
46
+ ),
47
+ Family(
48
+ "dynamic-annotation",
49
+ "Annotate with the concrete type. object, Any, Protocol, TypeVar and Callable do not lower.",
50
+ ),
51
+ Family("forward-reference", "Define the class before use and write the annotation unquoted."),
52
+ Family(
53
+ "unlowered-type-name",
54
+ "Replace the alias or imported name with a lowered type, or lower the class it names.",
55
+ ),
56
+ Family("param-inference", "Annotate the parameter. Unannotated parameters need local type inference."),
57
+ Family("local-inference", "Add an annotation where the name is first assigned."),
58
+ Family("return-inference", "Add a return annotation."),
59
+ Family("uninitialized-field", "Give the field an initial value in __init__ or at the assignment."),
60
+ Family("value-type", "Use list[T], dict[K, V] or a lowered class for this annotation."),
61
+ Family(
62
+ "annotation-other",
63
+ "Rewrite the annotation with int, float, bool, str, bytes, list, dict, tuple or a lowered class.",
64
+ ),
65
+ Family("decorators", "Remove the decorator or inline what it does. Only a closed set of decorators lowers."),
66
+ Family(
67
+ "parameter-forms",
68
+ "Use plain positional parameters. Keyword-only, positional-only, *args and **kwargs do not lower.",
69
+ ),
70
+ Family("class-keywords", "Use one lowered base class and no class keywords such as metaclass."),
71
+ Family("class-body", "Declare fields with an annotation and no value and set them in __init__."),
72
+ Family("except-as", "Name a lowered exception and drop the `as` binding or the bare except."),
73
+ Family("with-as", "Use a lowered context manager without `as`, or open and close explicitly."),
74
+ Family("async", "Async definitions do not lower. Use a synchronous definition."),
75
+ Family("enum", "Write the enum as integer constants in a plain class body."),
76
+ Family(
77
+ "module-statements",
78
+ "Module scope holds imports, functions, classes and constant assignments only. "
79
+ "Move the statement into a function.",
80
+ ),
81
+ Family("module-assign", "A module assignment must be a constant the lowering can fold."),
82
+ Family(
83
+ "import-table",
84
+ "The imported module is outside the closed import table. Avoid it or wait for it to be queued.",
85
+ ),
86
+ Family("queued-stdlib", "The stdlib module is queued and not lowered yet."),
87
+ Family("name-resolution", "The name is not a local, a parameter or a lowered global. Declare it or import it."),
88
+ Family("typing-value", "A typing name is used as a value. Keep typing names in annotations only."),
89
+ Family("module-value", "A module is used as a value. Call its lowered functions instead."),
90
+ Family("unlowered-call", "Call a lowered function, or rewrite the call with lowered constructs."),
91
+ Family("keyword-args", "Pass the arguments positionally."),
92
+ Family("call-arity", "Match the lowered arity of the call."),
93
+ Family("operator", "Convert the operands to a supported type pair."),
94
+ Family("non-latin1", "Use Latin-1 text. Other strings do not lower."),
95
+ Family("type-mismatch", "Convert the operand explicitly."),
96
+ Family("fall-off-end", "End every path with a return or a raise."),
97
+ Family("field-lookup", "Access only fields the class declares."),
98
+ Family("construct-other", "Rewrite the statement with lowered constructs."),
99
+ Family(
100
+ "export-cascade", "Follows from a fence in the exporting module. Fix that definition first.", derived=True
101
+ ),
102
+ Family("class-cascade", "Follows from a fence inside the class. Fix the class first.", derived=True),
103
+ )
104
+ }
105
+
106
+
107
+ def _stdlib_family(match: re.Match[str]) -> str:
108
+ return f"stdlib:{match.group(1)}"
109
+
110
+
111
+ # Order matters: the first matching rule wins. A rule is (code or None, message regex, family).
112
+ _RULES: tuple[tuple[str | None, str, str], ...] = (
113
+ ("PS1101", r"", "union-types"),
114
+ ("PS1102", r"", "optional-types"),
115
+ ("PS1103", r"", "generic-alias"),
116
+ ("PS1104", r"", "dynamic-annotation"),
117
+ ("PS1105", r"", "forward-reference"),
118
+ ("PS1106", r"", "unlowered-type-name"),
119
+ ("PS1201", r"", "decorators"),
120
+ ("PS1202", r"", "parameter-forms"),
121
+ ("PS1203", r"", "class-keywords"),
122
+ ("PS1204", r"", "class-body"),
123
+ ("PS1205", r"", "except-as"),
124
+ ("PS1206", r"", "with-as"),
125
+ ("PS1001", r"^parameter .+ needs an annotation$", "param-inference"),
126
+ ("PS1001", r"needs an annotation before use$", "local-inference"),
127
+ ("PS1001", r"needs a return annotation$", "return-inference"),
128
+ ("PS1001", r"needs an initial value$|needs __init__$|does not assign", "uninitialized-field"),
129
+ ("PS1001", r"is not a value type in the IR", "value-type"),
130
+ ("PS1001", r"", "annotation-other"),
131
+ ("PS1003", r"", "type-mismatch"),
132
+ ("PS1004", r"", "fall-off-end"),
133
+ ("PS2001", r"is not a field$|has no fields$", "field-lookup"),
134
+ ("PS2001", r"is not in the lowered subset of ([\w.]+)", "stdlib"),
135
+ ("PS2001", r"", "construct-other"),
136
+ ("PS2002", r"is not exported by this package", "export-cascade"),
137
+ ("PS2002", r"^this class .+ is not lowered", "class-cascade"),
138
+ ("PS2002", r"is not in the closed import table", "import-table"),
139
+ ("PS2002", r"is not a local or parameter", "name-resolution"),
140
+ ("PS2002", r"is a typing name and is not a value", "typing-value"),
141
+ ("PS2002", r"^module .+ is not lowered|module .+ is not a value", "module-value"),
142
+ ("PS2002", r"^call to .+ is not lowered|call of attribute is not lowered", "unlowered-call"),
143
+ ("PS2002", r"AsyncFunctionDef|async ", "async"),
144
+ ("PS2002", r"\benum\b", "enum"),
145
+ ("PS2002", r"at module scope is not lowered", "module-statements"),
146
+ ("PS2002", r"module assignment", "module-assign"),
147
+ ("PS2002", r"decorators? ", "decorators"),
148
+ ("PS2002", r"", "construct-other"),
149
+ ("PS2003", r"keyword arguments", "keyword-args"),
150
+ ("PS2003", r"", "call-arity"),
151
+ ("PS2004", r"", "operator"),
152
+ ("PS2005", r"", "non-latin1"),
153
+ ("PS2101", r"", "queued-stdlib"),
154
+ )
155
+
156
+
157
+ def family_of(code: str, message: str) -> str:
158
+ for rule_code, pattern, family in _RULES:
159
+ if rule_code is not None and rule_code != code:
160
+ continue
161
+ match = re.search(pattern, message)
162
+ if match is None:
163
+ continue
164
+ if family == "stdlib":
165
+ return _stdlib_family(match)
166
+ return family
167
+ return "construct-other"
168
+
169
+
170
+ def hint_for(family: str, message: str) -> str:
171
+ known = _FAMILIES.get(family)
172
+ if known is not None:
173
+ if family == "param-inference":
174
+ name = re.search(r"parameter (\S+) needs", message)
175
+ if name:
176
+ return f"Annotate parameter `{name.group(1)}`. Unannotated parameters need local type inference."
177
+ return known.hint
178
+ if family.startswith("stdlib:"):
179
+ return f"Use a lowered attribute of {family.split(':', 1)[1]}, or hand-write the logic."
180
+ return _FAMILIES["construct-other"].hint
181
+
182
+
183
+ def is_derived(family: str) -> bool:
184
+ known = _FAMILIES.get(family)
185
+ return known is not None and known.derived
186
+
187
+
188
+ @dataclass
189
+ class _Def:
190
+ name: str
191
+ kind: str
192
+ line: int
193
+ end_line: int
194
+ body_line: int
195
+
196
+
197
+ def _definitions(tree: ast.AST) -> list[_Def]:
198
+ found: list[_Def] = []
199
+
200
+ def walk(node: ast.AST, prefix: str) -> None:
201
+ for child in ast.iter_child_nodes(node):
202
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
203
+ name = f"{prefix}{child.name}"
204
+ kind = "class" if isinstance(child, ast.ClassDef) else "function"
205
+ body_line = child.body[0].lineno if child.body else child.lineno
206
+ found.append(_Def(name, kind, child.lineno, child.end_lineno or child.lineno, body_line))
207
+ walk(child, f"{name}.")
208
+ else:
209
+ walk(child, prefix)
210
+
211
+ walk(tree, "")
212
+ return found
213
+
214
+
215
+ def _innermost(defs: list[_Def], line: int) -> _Def | None:
216
+ best: _Def | None = None
217
+ for item in defs:
218
+ if item.line <= line <= item.end_line and (best is None or item.line >= best.line):
219
+ best = item
220
+ return best
221
+
222
+
223
+ def _read(path: str, cache: dict[str, str | None]) -> str | None:
224
+ if path not in cache:
225
+ try:
226
+ cache[path] = Path(path).read_text(encoding="utf-8")
227
+ except OSError:
228
+ cache[path] = None
229
+ return cache[path]
230
+
231
+
232
+ def _lower(target: Path) -> tuple[dict[str, Any], list[str]]:
233
+ if target.is_dir():
234
+ module, _ = lower_directory(target)
235
+ files = [str(path) for _, path, _ in _discover(target)[0]]
236
+ return module, files
237
+ if target.is_file():
238
+ module, _ = lower_file(str(target))
239
+ return module, [str(target)]
240
+ raise IRError(f"source not found: {target}")
241
+
242
+
243
+ def build_report(target: Path) -> dict[str, Any]:
244
+ module, files = _lower(target)
245
+ sources: dict[str, str | None] = {}
246
+ trees: dict[str, list[_Def]] = {}
247
+ total_defs: dict[str, int] = {}
248
+ for path in files:
249
+ text = _read(path, sources)
250
+ if text is None:
251
+ continue
252
+ defs = _definitions(ast.parse(text, filename=path))
253
+ trees[path] = defs
254
+ total_defs[path] = len(defs)
255
+
256
+ seen: set[tuple[Any, ...]] = set()
257
+ entries: list[dict[str, Any]] = []
258
+ raw = sorted(
259
+ module["fences"],
260
+ key=lambda f: (f["span"]["file"], f["span"]["line"], f["span"]["col"], f["code"]),
261
+ )
262
+ for fence in raw:
263
+ span = fence["span"]
264
+ key = (fence["code"], span["file"], span["line"], span["col"], fence["message"])
265
+ if key in seen:
266
+ continue
267
+ seen.add(key)
268
+ path = span["file"]
269
+ text = _read(path, sources)
270
+ defs = trees.get(path, [])
271
+ owner = _innermost(defs, span["line"])
272
+ family = family_of(fence["code"], fence["message"])
273
+ lines = text.splitlines() if text else []
274
+ statement = lines[span["line"] - 1].strip() if 1 <= span["line"] <= len(lines) else ""
275
+ entries.append(
276
+ {
277
+ "code": fence["code"],
278
+ "message": fence["message"],
279
+ "family": family,
280
+ "derived": is_derived(family),
281
+ "file": path,
282
+ "line": span["line"],
283
+ "col": span["col"],
284
+ "endLine": span["endLine"],
285
+ "endCol": span["endCol"],
286
+ "definition": owner.name if owner else None,
287
+ "statement": statement,
288
+ "frame": format_diagnostic(fence, text),
289
+ "hint": hint_for(family, fence["message"]),
290
+ }
291
+ )
292
+
293
+ by_def: dict[tuple[str, str], list[int]] = defaultdict(list)
294
+ for index, entry in enumerate(entries):
295
+ if entry["definition"] is not None:
296
+ by_def[(entry["file"], entry["definition"])].append(index)
297
+ definitions: list[dict[str, Any]] = []
298
+ for (path, name), indexes in sorted(by_def.items()):
299
+ owner = next(d for d in trees[path] if d.name == name)
300
+ # A fence between the def line and the first body line sits in the signature or header.
301
+ header = any(entries[i]["line"] < owner.body_line for i in indexes)
302
+ definitions.append(
303
+ {
304
+ "file": path,
305
+ "name": name,
306
+ "kind": owner.kind,
307
+ "line": owner.line,
308
+ "fences": indexes,
309
+ "bodyAnalyzed": not header,
310
+ }
311
+ )
312
+ module_fences = [i for i, e in enumerate(entries) if e["definition"] is None]
313
+
314
+ return {
315
+ "schemaVersion": SCHEMA_VERSION,
316
+ "target": str(target),
317
+ "summary": _summary(entries, definitions, files, total_defs),
318
+ "fences": entries,
319
+ "definitions": definitions,
320
+ "moduleFences": module_fences,
321
+ "chain": _chain(entries, definitions),
322
+ }
323
+
324
+
325
+ def _summary(
326
+ entries: list[dict[str, Any]],
327
+ definitions: list[dict[str, Any]],
328
+ files: list[str],
329
+ total_defs: dict[str, int],
330
+ ) -> dict[str, Any]:
331
+ fences_by_family = Counter(e["family"] for e in entries)
332
+ return {
333
+ "files": len(files),
334
+ "definitions": sum(total_defs.values()),
335
+ "fencedDefinitions": len(definitions),
336
+ "fences": len(entries),
337
+ "rootFences": sum(1 for e in entries if not e["derived"]),
338
+ "byCode": dict(sorted(Counter(e["code"] for e in entries).items())),
339
+ "byFamily": {
340
+ family: {"fences": count, "derived": is_derived(family)}
341
+ for family, count in sorted(fences_by_family.items(), key=lambda kv: (-kv[1], kv[0]))
342
+ },
343
+ }
344
+
345
+
346
+ def _chain(entries: list[dict[str, Any]], definitions: list[dict[str, Any]]) -> list[dict[str, Any]]:
347
+ """Per family: how many definitions it alone blocks, and how many root fences stay if it is fixed."""
348
+ root_total = sum(1 for e in entries if not e["derived"])
349
+ root_by_family = Counter(e["family"] for e in entries if not e["derived"])
350
+ alone: Counter[str] = Counter()
351
+ for definition in definitions:
352
+ families = {entries[i]["family"] for i in definition["fences"] if not entries[i]["derived"]}
353
+ if len(families) == 1:
354
+ alone[next(iter(families))] += 1
355
+ chain = [
356
+ {
357
+ "family": family,
358
+ "fences": count,
359
+ "definitionsUnblocked": alone[family],
360
+ "rootFencesLeft": root_total - count,
361
+ }
362
+ for family, count in root_by_family.items()
363
+ ]
364
+ chain.sort(key=lambda item: (-item["definitionsUnblocked"], -item["fences"], item["family"]))
365
+ return chain
366
+
367
+
368
+ def render_text(report: dict[str, Any]) -> str:
369
+ summary = report["summary"]
370
+ parts = [
371
+ f"{report['target']}: {summary['fences']} fences "
372
+ f"({summary['rootFences']} root) in {summary['fencedDefinitions']} of {summary['definitions']} definitions"
373
+ ]
374
+ entries = report["fences"]
375
+ for entry in entries:
376
+ parts.append(entry["frame"])
377
+ owner = entry["definition"] or "<module>"
378
+ parts.append(f" family: {entry['family']}{' (derived)' if entry['derived'] else ''} in: {owner}")
379
+ parts.append(f" hint: {entry['hint']}")
380
+ if report["chain"]:
381
+ parts.append("blockers, most definitions first:")
382
+ for item in report["chain"][:10]:
383
+ parts.append(
384
+ f" {item['family']}: {item['fences']} fences, "
385
+ f"fixing it unblocks {item['definitionsUnblocked']} definitions "
386
+ f"and leaves {item['rootFencesLeft']} more root fences"
387
+ )
388
+ return "\n".join(parts)
pyscriptc/debug.py ADDED
@@ -0,0 +1,120 @@
1
+ """Compiler debug mode.
2
+
3
+ `--debug` (or `PYSC_DEBUG`) prints a compact per-stage table to stderr.
4
+ `--debug-dir DIR` additionally writes the intermediate artifacts the agent
5
+ needs to inspect a compile: the lowered module (`ir.json`), the emitted LLVM
6
+ text (`out.ll`), the exact cc command lines (`cc.sh`), and a machine-readable
7
+ `summary.json`. Debug output never touches the IR or the emitted text.
8
+
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ _TOPICS = {"stages", "fences", "cc", "paths", "all"}
20
+
21
+
22
+ def parse_topics(value: str | None) -> set[str]:
23
+ """A `--debug`/`PYSC_DEBUG` value: empty means `stages`, else a comma list."""
24
+ if value is None or not value.strip() or value.strip() in ("1", "true", "on"):
25
+ return {"stages"}
26
+ topics = {topic.strip() for topic in value.split(",") if topic.strip()}
27
+ unknown = topics - _TOPICS
28
+ if unknown:
29
+ raise ValueError(f"unknown debug topic(s): {', '.join(sorted(unknown))}")
30
+ return topics
31
+
32
+
33
+ def resolve(flag: str | None, debug_dir: str | None) -> Debug | None:
34
+ """`--debug` wins over `PYSC_DEBUG`; `--debug-dir` alone also enables debug."""
35
+ value = flag if flag is not None else os.environ.get("PYSC_DEBUG")
36
+ if flag is None and not value and debug_dir is None:
37
+ return None
38
+ topics = parse_topics(value)
39
+ if debug_dir is not None:
40
+ topics.add("paths")
41
+ return Debug(topics, None if debug_dir is None else Path(debug_dir))
42
+
43
+
44
+ class Debug:
45
+ def __init__(self, topics: set[str], debug_dir: Path | None) -> None:
46
+ self.topics = topics
47
+ self.debug_dir = debug_dir
48
+ self.stages: list[tuple[str, float]] = []
49
+ self.commands: list[list[str]] = []
50
+ self._starts: list[float] = []
51
+ self._t0 = time.perf_counter()
52
+
53
+ def wants(self, topic: str) -> bool:
54
+ return "all" in self.topics or topic in self.topics
55
+
56
+ def begin(self, name: str) -> None:
57
+ self._starts.append(time.perf_counter())
58
+
59
+ def end(self, name: str) -> None:
60
+ self.stages.append((name, (time.perf_counter() - self._starts.pop()) * 1000.0))
61
+
62
+ def render(self, module: dict[str, Any] | None, outputs: dict[str, str]) -> str:
63
+ lines: list[str] = []
64
+ if self.wants("stages"):
65
+ lines.append("[debug] stage ms")
66
+ for name, ms in self.stages:
67
+ lines.append(f"[debug] {name:<12}{ms:>8.1f}")
68
+ total = (time.perf_counter() - self._t0) * 1000.0
69
+ lines.append(f"[debug] {'total':<12}{total:>8.1f}")
70
+ if module is not None and self.wants("fences"):
71
+ fences = module.get("fences", [])
72
+ lines.append(f"[debug] fences: {len(fences)}")
73
+ for fence in fences:
74
+ span = fence.get("span", {})
75
+ lines.append(
76
+ f"[debug] {span.get('file')}:{span.get('line')}:{span.get('col')}"
77
+ f" {fence.get('code')} {fence.get('message')}"
78
+ )
79
+ if self.commands and self.wants("cc"):
80
+ lines.append("[debug] cc:")
81
+ for argv in self.commands:
82
+ lines.append(f"[debug] {' '.join(argv)}")
83
+ if outputs and self.wants("paths"):
84
+ for key, value in outputs.items():
85
+ lines.append(f"[debug] {key}: {value}")
86
+ return "\n".join(lines)
87
+
88
+ def summary(self, module: dict[str, Any] | None, outputs: dict[str, str], status: str) -> dict[str, Any]:
89
+ return {
90
+ "stages": [{"name": name, "ms": round(ms, 3)} for name, ms in self.stages],
91
+ "totalMs": round((time.perf_counter() - self._t0) * 1000.0, 3),
92
+ "fences": [] if module is None else module.get("fences", []),
93
+ "functions": 0 if module is None else len(module.get("functions", [])),
94
+ "commands": self.commands,
95
+ "outputs": outputs,
96
+ "status": status,
97
+ }
98
+
99
+ def write_dir(
100
+ self,
101
+ module: dict[str, Any] | None,
102
+ ll_text: str | None,
103
+ outputs: dict[str, str],
104
+ status: str,
105
+ ) -> None:
106
+ if self.debug_dir is None:
107
+ return
108
+ self.debug_dir.mkdir(parents=True, exist_ok=True)
109
+ if module is not None:
110
+ (self.debug_dir / "ir.json").write_text(json.dumps(module, indent=2) + "\n", encoding="utf-8")
111
+ if ll_text is not None:
112
+ (self.debug_dir / "out.ll").write_text(ll_text, encoding="utf-8")
113
+ if self.commands:
114
+ script = "#!/bin/sh\n# exact cc invocations pyscriptc ran\n" + "\n".join(
115
+ " ".join(argv) for argv in self.commands
116
+ )
117
+ (self.debug_dir / "cc.sh").write_text(script + "\n", encoding="utf-8")
118
+ (self.debug_dir / "summary.json").write_text(
119
+ json.dumps(self.summary(module, outputs, status), indent=2) + "\n", encoding="utf-8"
120
+ )
@@ -0,0 +1,179 @@
1
+ """Diagnostic code registry behind `pyscriptc explain`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Diagnostic:
10
+ code: str
11
+ title: str
12
+ why: str
13
+ fix: str
14
+
15
+
16
+ _ENTRIES = (
17
+ Diagnostic(
18
+ "PS0001",
19
+ "invalid input or IR",
20
+ "The source or the generated IR failed a structural check before lowering could finish.",
21
+ "Fix the input path or syntax error named in the message.",
22
+ ),
23
+ Diagnostic(
24
+ "PS1001",
25
+ "annotation missing or not lowered",
26
+ (
27
+ "A parameter, return, or variable needs an annotation the subset can read, "
28
+ "or an annotation form has no more specific code."
29
+ ),
30
+ "Annotate the name with int, float, bool, str, bytes, list[T], dict[K, V], tuple[...], or a lowered class.",
31
+ ),
32
+ Diagnostic(
33
+ "PS1003",
34
+ "type mismatch",
35
+ "An operation received a type the lowered subset does not accept for it.",
36
+ "Convert the operand explicitly, for example int(flag) before math calls.",
37
+ ),
38
+ Diagnostic(
39
+ "PS1004",
40
+ "function can fall off the end",
41
+ "A function with a non-None return type has a path that reaches the end without a return.",
42
+ "End every path with a return or a raise.",
43
+ ),
44
+ Diagnostic(
45
+ "PS1101",
46
+ "union annotation",
47
+ "The IR has no tagged union yet, so `A | B` and `Union[A, B]` have no representation.",
48
+ "Narrow the annotation to one arm, or split the function per arm.",
49
+ ),
50
+ Diagnostic(
51
+ "PS1102",
52
+ "Optional annotation",
53
+ "The IR has no optional value, so `Optional[T]` and `T | None` have no representation.",
54
+ "Return or accept T and signal absence with a sentinel value, or split the function.",
55
+ ),
56
+ Diagnostic(
57
+ "PS1103",
58
+ "generic alias annotation",
59
+ (
60
+ "A subscripted annotation such as Mapping[K, V], Sequence[T], Iterator[T], "
61
+ "Callable[...], or type[T] is not a lowered container."
62
+ ),
63
+ "Use list[T], dict[K, V], or tuple[...] in the signature.",
64
+ ),
65
+ Diagnostic(
66
+ "PS1104",
67
+ "dynamic or structural annotation",
68
+ (
69
+ "object, Any, Protocol, TypeVar, Generic, Callable and ABC bases describe "
70
+ "types the subset cannot represent natively."
71
+ ),
72
+ "Annotate with the concrete type the function really takes.",
73
+ ),
74
+ Diagnostic(
75
+ "PS1105",
76
+ "forward reference annotation",
77
+ "A quoted annotation such as 'Node' is not resolved by the lowering.",
78
+ "Define the class before use and write the name unquoted.",
79
+ ),
80
+ Diagnostic(
81
+ "PS1106",
82
+ "annotation names an unlowered type",
83
+ "The annotation is a plain name that is not a builtin scalar, a lowered class, or a known alias.",
84
+ "Import a lowered class, or replace the alias with the concrete type.",
85
+ ),
86
+ Diagnostic(
87
+ "PS1201",
88
+ "decorator",
89
+ "Only a closed set of decorators lowers. Other decorators change behavior the compiler cannot see.",
90
+ "Remove the decorator, or inline what it does.",
91
+ ),
92
+ Diagnostic(
93
+ "PS1202",
94
+ "parameter form",
95
+ "Positional-only, keyword-only, *args and **kwargs parameters are not lowered.",
96
+ "Use plain positional parameters, with defaults where needed.",
97
+ ),
98
+ Diagnostic(
99
+ "PS1203",
100
+ "class header form",
101
+ "Class keywords (metaclass=...) and multiple inheritance are not lowered.",
102
+ "Use a single lowered base class and no class keywords.",
103
+ ),
104
+ Diagnostic(
105
+ "PS1204",
106
+ "class body statement",
107
+ "A class body holds only annotated fields without values and method definitions.",
108
+ "Move assignments into __init__ and declare fields with an annotation only.",
109
+ ),
110
+ Diagnostic(
111
+ "PS1205",
112
+ "except form",
113
+ "`except E as name` and bare `except:` are not lowered.",
114
+ "Name a lowered exception class and drop the `as` binding.",
115
+ ),
116
+ Diagnostic(
117
+ "PS1206",
118
+ "with-as binding",
119
+ "`with ... as name` is lowered only for a closed set of context managers.",
120
+ "Use a lowered context manager without the binding, or open and close explicitly.",
121
+ ),
122
+ Diagnostic(
123
+ "PS2001",
124
+ "stdlib name not lowered",
125
+ "The module imports, but this attribute is outside its lowered surface.",
126
+ "Use a lowered attribute of the module, or hand-write the logic.",
127
+ ),
128
+ Diagnostic(
129
+ "PS2002",
130
+ "construct not lowered",
131
+ "A statement, expression, import, or definition has no lowering.",
132
+ "Rewrite it with the lowered constructs, as the hint for this fence says.",
133
+ ),
134
+ Diagnostic(
135
+ "PS2003",
136
+ "call arguments not lowered",
137
+ "A call has keyword arguments, or the wrong number of arguments, for its lowered form.",
138
+ "Pass positional arguments in the lowered arity.",
139
+ ),
140
+ Diagnostic(
141
+ "PS2004",
142
+ "operator or operand not lowered",
143
+ "The operator is not lowered for these operand types.",
144
+ "Convert the operands to a supported pair of types.",
145
+ ),
146
+ Diagnostic(
147
+ "PS2005",
148
+ "string literal outside Latin-1",
149
+ "Strings are byte sequences in the runtime, so only Latin-1 text lowers.",
150
+ "Use Latin-1 text, or move the literal to bytes.",
151
+ ),
152
+ Diagnostic(
153
+ "PS2101",
154
+ "queued stdlib module",
155
+ "The module is on the roadmap and is not lowered yet.",
156
+ "Avoid the module, or wait for the queue entry to land.",
157
+ ),
158
+ Diagnostic(
159
+ "PS3001",
160
+ "version mismatch",
161
+ "The running interpreter is not the CPython version the subset targets.",
162
+ "Run under the pinned CPython version.",
163
+ ),
164
+ Diagnostic(
165
+ "PS3002",
166
+ "does not fit",
167
+ "The emitted program exceeds a fixed limit of the backend.",
168
+ "Reduce the offending size or count.",
169
+ ),
170
+ )
171
+
172
+ DIAGNOSTICS: dict[str, Diagnostic] = {entry.code: entry for entry in _ENTRIES}
173
+
174
+
175
+ def explain(code: str) -> str | None:
176
+ entry = DIAGNOSTICS.get(code.upper())
177
+ if entry is None:
178
+ return None
179
+ return f"{entry.code}: {entry.title}\n\nWhy: {entry.why}\n\nFix: {entry.fix}"