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/package.py ADDED
@@ -0,0 +1,249 @@
1
+ """Compile a directory as one module and resolve relative imports.
2
+
3
+ Each ``.py`` file becomes a dotted module under the directory name.
4
+ ``from .util import add`` and ``from . import VERSION`` bind a function, a
5
+ module-level constant, or a class from another file in that tree. A class
6
+ export is the class the defining module already emits, including a subclass
7
+ of a class exported by another module in the tree. ``__main__.py`` is the
8
+ ``python -m`` script and is not an importable module, so it is skipped and
9
+ the compile says so. A single file still refuses a relative import.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ import copy
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from pyscriptc.errors import IRError
20
+ from pyscriptc.ir import SUBSET, cpython_version
21
+ from pyscriptc.lower import Lowerer
22
+
23
+
24
+ def lower_directory(root: Path) -> tuple[dict[str, Any], str]:
25
+ units, skipped = _discover(root)
26
+ exports, failed = _catalog(units)
27
+
28
+ functions: list[dict[str, Any]] = []
29
+ classes: list[dict[str, Any]] = []
30
+ fences: list[dict[str, Any]] = []
31
+ class_names: dict[str, dict[str, Any]] = {}
32
+ exc_n = 8
33
+ for name, path, is_package in units:
34
+ text = path.read_text(encoding="utf-8")
35
+ tree = ast.parse(text, filename=str(path))
36
+ lowerer = Lowerer(str(path), text)
37
+ lowerer.module_name = name
38
+ lowerer.is_package = is_package
39
+ lowerer.package = exports
40
+ lowerer.exc_n = exc_n
41
+ module = lowerer.module(tree)
42
+ exc_n = lowerer.exc_n
43
+ for fn in module["functions"]:
44
+ functions.append(fn)
45
+ for cls in module["classes"]:
46
+ previous = class_names.get(cls["name"])
47
+ if previous is not None:
48
+ if previous["fields"] == cls["fields"] and previous["methods"] == cls["methods"]:
49
+ continue
50
+ fences.append(
51
+ {
52
+ "code": "PS2002",
53
+ "message": f"class {cls['name']} is defined in more than one module",
54
+ "span": cls["span"],
55
+ "function": None,
56
+ }
57
+ )
58
+ continue
59
+ class_names[cls["name"]] = cls
60
+ classes.append(cls)
61
+ fences.extend(module["fences"])
62
+ fences = _promote_definition_fences(fences, failed)
63
+ merged = {
64
+ "irVersion": 1,
65
+ "subset": SUBSET,
66
+ "cpython": cpython_version(),
67
+ "sourceFile": str(root),
68
+ "entry": functions[0]["name"] if functions else None,
69
+ "functions": functions,
70
+ "classes": classes,
71
+ "fences": fences,
72
+ }
73
+ if skipped:
74
+ merged["_notes"] = [_skip_note(root, rel) for rel in skipped]
75
+ return merged, ""
76
+
77
+
78
+ def _catalog(
79
+ units: list[tuple[str, Path, bool]],
80
+ ) -> tuple[dict[str, dict[str, dict[str, Any]]], dict[str, dict[str, dict[str, Any]]]]:
81
+ """Repeat until a subclass can see a base exported by an earlier pass."""
82
+
83
+ exports: dict[str, dict[str, dict[str, Any]]] = {}
84
+ failed: dict[str, dict[str, dict[str, Any]]] = {}
85
+ for _ in range(len(units) + 1):
86
+ nxt, nxt_failed = _catalog_once(units, exports)
87
+ if nxt == exports:
88
+ return nxt, nxt_failed
89
+ exports, failed = nxt, nxt_failed
90
+ return exports, failed
91
+
92
+
93
+ def _catalog_once(
94
+ units: list[tuple[str, Path, bool]], exports: dict[str, dict[str, dict[str, Any]]]
95
+ ) -> tuple[dict[str, dict[str, dict[str, Any]]], dict[str, dict[str, dict[str, Any]]]]:
96
+ found: dict[str, dict[str, dict[str, Any]]] = {}
97
+ failed: dict[str, dict[str, dict[str, Any]]] = {}
98
+ exc_n = 8
99
+ for name, path, is_package in units:
100
+ text = path.read_text(encoding="utf-8")
101
+ tree = ast.parse(text, filename=str(path))
102
+ lowerer = Lowerer(str(path), text)
103
+ lowerer.module_name = name
104
+ lowerer.is_package = is_package
105
+ lowerer.cataloguing = True
106
+ lowerer.package = exports
107
+ lowerer.exc_n = exc_n
108
+ module = lowerer.module(tree)
109
+ exc_n = lowerer.exc_n
110
+ found[name] = _exports_of(name, lowerer)
111
+ missed = _unexported_definitions(tree, lowerer, module["fences"])
112
+ if missed:
113
+ failed[name] = missed
114
+ return found, failed
115
+
116
+
117
+ def _exports_of(module_name: str, lowerer: Lowerer) -> dict[str, dict[str, Any]]:
118
+ bucket: dict[str, dict[str, Any]] = {}
119
+ for fn, (params, ret) in lowerer.sigs.items():
120
+ bucket[fn] = {
121
+ "kind": "func",
122
+ "ir_name": f"{module_name}.{fn}",
123
+ "params": copy.deepcopy(params),
124
+ "return": copy.deepcopy(ret),
125
+ }
126
+ for const_name, value in lowerer.constants.items():
127
+ bucket[const_name] = {"kind": "const", "value": copy.deepcopy(value)}
128
+ for class_name, info in lowerer.classes.items():
129
+ if info.get("synthetic") or info.get("imported"):
130
+ continue
131
+ bucket[class_name] = {"kind": "class", "name": class_name, "info": _class_export(info)}
132
+ return bucket
133
+
134
+
135
+ def _class_export(info: dict[str, Any]) -> dict[str, Any]:
136
+ """Class shape without the AST node. The defining module still emits the class."""
137
+
138
+ exported: dict[str, Any] = {
139
+ "fields": copy.deepcopy(info.get("fields", [])),
140
+ "own_fields": copy.deepcopy(info.get("own_fields", [])),
141
+ "methods": copy.deepcopy(info.get("methods", {})),
142
+ "method_kind": dict(info.get("method_kind", {})),
143
+ "method_owner": dict(info.get("method_owner", {})),
144
+ "base": info.get("base"),
145
+ "imported": True,
146
+ }
147
+ if info.get("generated_init") is not None:
148
+ exported["generated_init"] = info["generated_init"]
149
+ if info.get("exception"):
150
+ exported["exception"] = True
151
+ exported["exc_code"] = info.get("exc_code")
152
+ if info.get("enum"):
153
+ exported["enum"] = True
154
+ exported["members"] = dict(info.get("members") or {})
155
+ return exported
156
+
157
+
158
+ def _discover(root: Path) -> tuple[list[tuple[str, Path, bool]], list[str]]:
159
+ root = root.resolve()
160
+ package = root.name
161
+ if not package.isidentifier():
162
+ raise IRError(f"package directory {package} is not an identifier")
163
+ found: list[tuple[str, Path, bool]] = []
164
+ skipped: list[str] = []
165
+ for path in sorted(root.rglob("*.py")):
166
+ relative = path.relative_to(root)
167
+ if any(part.startswith(".") or part == "__pycache__" for part in relative.parts):
168
+ continue
169
+ if path.name == "__main__.py":
170
+ skipped.append(relative.as_posix())
171
+ continue
172
+ if path.name == "__init__.py":
173
+ parts = relative.parts[:-1]
174
+ name = ".".join((package, *parts)) if parts else package
175
+ found.append((name, path, True))
176
+ continue
177
+ stem = relative.with_suffix("")
178
+ if not all(part.isidentifier() for part in stem.parts):
179
+ raise IRError(f"{relative} is not a module name")
180
+ found.append(((".".join((package, *stem.parts))), path, False))
181
+ if not found:
182
+ raise IRError(f"no Python files in {root}")
183
+ return found, skipped
184
+
185
+
186
+ def _skip_note(root: Path, relative: str) -> str:
187
+ return f"note: skipped {root.name}/{relative}; __main__.py is the python -m script, not an importable module"
188
+
189
+
190
+ _NOT_EXPORTED = " is not exported by this package"
191
+
192
+
193
+ def _promote_definition_fences(
194
+ fences: list[dict[str, Any]], failed: dict[str, dict[str, dict[str, Any]]]
195
+ ) -> list[dict[str, Any]]:
196
+ """Report the fence inside a class or function before the import that missed it."""
197
+
198
+ by_qual = {f"{module}.{name}": fence for module, names in failed.items() for name, fence in names.items()}
199
+ if not by_qual:
200
+ return fences
201
+
202
+ def ident(fence: dict[str, Any]) -> tuple[Any, ...]:
203
+ span = fence.get("span") or {}
204
+ return (fence.get("code"), fence.get("message"), span.get("file"), span.get("line"), span.get("col"))
205
+
206
+ wanted = {ident(fence) for fence in by_qual.values()}
207
+ emitted: set[tuple[Any, ...]] = set()
208
+ out: list[dict[str, Any]] = []
209
+ for fence in fences:
210
+ message = fence.get("message") or ""
211
+ if message.endswith(_NOT_EXPORTED):
212
+ defined = by_qual.get(message[: -len(_NOT_EXPORTED)])
213
+ if defined is not None:
214
+ mark = ident(defined)
215
+ if mark not in emitted:
216
+ out.append(defined)
217
+ emitted.add(mark)
218
+ mark = ident(fence)
219
+ if mark in emitted and mark in wanted:
220
+ continue
221
+ out.append(fence)
222
+ if mark in wanted:
223
+ emitted.add(mark)
224
+ return out
225
+
226
+
227
+ def _unexported_definitions(
228
+ tree: ast.Module, lowerer: Lowerer, fences: list[dict[str, Any]]
229
+ ) -> dict[str, dict[str, Any]]:
230
+ exported = set(lowerer.sigs)
231
+ for class_name, info in lowerer.classes.items():
232
+ if info.get("synthetic") or info.get("imported"):
233
+ continue
234
+ exported.add(class_name)
235
+ found: dict[str, dict[str, Any]] = {}
236
+ for node in tree.body:
237
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
238
+ continue
239
+ if node.name in exported:
240
+ continue
241
+ start = node.lineno
242
+ decorators = getattr(node, "decorator_list", None) or []
243
+ if decorators:
244
+ start = min(start, min(item.lineno for item in decorators))
245
+ end = node.end_lineno or node.lineno
246
+ inside = [fence for fence in fences if start <= fence["span"]["line"] <= end]
247
+ if inside:
248
+ found[node.name] = inside[0]
249
+ return found