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/errors.py ADDED
@@ -0,0 +1,56 @@
1
+ """Diagnostics for lowering and LLVM emission."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class SpikeError(Exception):
9
+ """Base error. `code` is stable; tests and callers match on it."""
10
+
11
+ def __init__(self, code: str, message: str) -> None:
12
+ super().__init__(f"{code}: {message}")
13
+ self.code = code
14
+ self.message = message
15
+
16
+
17
+ class IRError(SpikeError):
18
+ def __init__(self, message: str) -> None:
19
+ super().__init__("PS0001", message)
20
+
21
+
22
+ class VersionError(SpikeError):
23
+ def __init__(self, message: str) -> None:
24
+ super().__init__("PS3001", message)
25
+
26
+
27
+ class FitError(SpikeError):
28
+ def __init__(self, message: str) -> None:
29
+ super().__init__("PS3002", message)
30
+
31
+
32
+ def span_of(node: Any, filename: str) -> dict[str, Any]:
33
+ line = getattr(node, "lineno", 1) or 1
34
+ col0 = getattr(node, "col_offset", 0) or 0
35
+ end_line = getattr(node, "end_lineno", line) or line
36
+ end_col0 = getattr(node, "end_col_offset", col0) or col0
37
+ return {
38
+ "file": filename,
39
+ "line": line,
40
+ "col": col0 + 1,
41
+ "endLine": end_line,
42
+ "endCol": end_col0 + 1,
43
+ }
44
+
45
+
46
+ def format_diagnostic(diag: dict[str, Any], source: str | None = None) -> str:
47
+ span = diag["span"]
48
+ header = f"{span['file']}:{span['line']}:{span['col']}: {diag['code']}: {diag['message']}"
49
+ if source is None:
50
+ return header
51
+ lines = source.splitlines()
52
+ if not 1 <= span["line"] <= len(lines):
53
+ return header
54
+ text = lines[span["line"] - 1]
55
+ caret_at = max(span["col"] - 1, 0)
56
+ return f"{header}\n {text}\n {' ' * caret_at}^"
pyscriptc/ir.py ADDED
@@ -0,0 +1,365 @@
1
+ """JSON IR for the spike subset.
2
+
3
+ The schema is Python's, not scriptc's. `int` is a Python int (stored as a
4
+ decimal string so JSON cannot round it). JavaScript kinds such as `f64` and
5
+ `nullT` are rejected.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import Any
12
+
13
+ from pyscriptc.errors import IRError, VersionError
14
+
15
+ IR_VERSION = 1
16
+ SUBSET = "gates-v1"
17
+
18
+ TYPE_KINDS = {"int", "float", "bool", "none", "str", "bytes", "list", "dict", "class", "unknown"}
19
+ EXPR_KINDS = {
20
+ "name",
21
+ "const",
22
+ "unary",
23
+ "binary",
24
+ "compare",
25
+ "call",
26
+ "list",
27
+ "dict",
28
+ "subscript",
29
+ "attr",
30
+ "cast",
31
+ "contains",
32
+ }
33
+ STMT_KINDS = {"assign", "if", "while", "return", "expr", "pass", "setitem", "setattr", "try", "raise"}
34
+ # Kinds that would import a JavaScript object model. Named so a reused
35
+ # scriptc node fails closed instead of being misread.
36
+ FORBIDDEN_KINDS = {
37
+ "f64",
38
+ "bigint",
39
+ "nullT",
40
+ "undefinedT",
41
+ "jsval",
42
+ "dyn",
43
+ "map",
44
+ "array",
45
+ "httpReq",
46
+ "promise",
47
+ "date",
48
+ }
49
+
50
+ I64_MIN = -9223372036854775808
51
+ I64_MAX = 9223372036854775807
52
+
53
+
54
+ def cpython_version() -> str:
55
+ return sys.version.split()[0]
56
+
57
+
58
+ def type_key(typ: dict[str, Any]) -> str:
59
+ if typ["kind"] == "list":
60
+ head = "set" if typ.get("set") else "list"
61
+ return f"{head}[{type_key(typ['elem'])}]"
62
+ if typ["kind"] == "dict":
63
+ return f"dict[{type_key(typ['key'])}, {type_key(typ['value'])}]"
64
+ if typ["kind"] == "class":
65
+ if "elem" in typ:
66
+ return f"{typ['name']}[{type_key(typ['elem'])}]"
67
+ return str(typ["name"])
68
+ return str(typ["kind"])
69
+
70
+
71
+ def types_equal(a: dict[str, Any], b: dict[str, Any]) -> bool:
72
+ return type_key(a) == type_key(b)
73
+
74
+
75
+ def fits_i64(value: int) -> bool:
76
+ return I64_MIN <= value <= I64_MAX
77
+
78
+
79
+ def _reject_forbidden(kind: str, where: str) -> None:
80
+ if kind in FORBIDDEN_KINDS:
81
+ raise IRError(f"{where}: kind {kind!r} is a JavaScript/scriptc type and is not in {SUBSET}")
82
+
83
+
84
+ def validate_type(typ: Any, where: str) -> None:
85
+ if not isinstance(typ, dict) or "kind" not in typ:
86
+ raise IRError(f"{where}: type must be an object with kind")
87
+ kind = typ["kind"]
88
+ _reject_forbidden(kind, where)
89
+ if kind not in TYPE_KINDS:
90
+ raise IRError(f"{where}: unknown type kind {kind!r}")
91
+ if kind == "list":
92
+ if "elem" not in typ:
93
+ raise IRError(f"{where}: list type needs elem")
94
+ validate_type(typ["elem"], f"{where}.elem")
95
+ elif kind == "dict":
96
+ if "key" not in typ or "value" not in typ:
97
+ raise IRError(f"{where}: dict type needs key and value")
98
+ validate_type(typ["key"], f"{where}.key")
99
+ validate_type(typ["value"], f"{where}.value")
100
+ elif kind == "class":
101
+ if not isinstance(typ.get("name"), str):
102
+ raise IRError(f"{where}: class type needs a name")
103
+ if "elem" in typ:
104
+ validate_type(typ["elem"], f"{where}.elem")
105
+
106
+
107
+ def validate_expr(expr: Any, where: str) -> None:
108
+ if not isinstance(expr, dict) or "kind" not in expr:
109
+ raise IRError(f"{where}: expression must be an object with kind")
110
+ kind = expr["kind"]
111
+ _reject_forbidden(kind, where)
112
+ if kind not in EXPR_KINDS:
113
+ raise IRError(f"{where}: unknown expression kind {kind!r}")
114
+ validate_type(expr.get("type"), f"{where}.type")
115
+ if "span" not in expr:
116
+ raise IRError(f"{where}: expression needs a span")
117
+ if kind == "name":
118
+ if not isinstance(expr.get("id"), str):
119
+ raise IRError(f"{where}: name needs id")
120
+ elif kind == "const":
121
+ if "value" not in expr:
122
+ raise IRError(f"{where}: const needs value")
123
+ elif kind == "unary":
124
+ if expr.get("op") not in {"-", "not"}:
125
+ raise IRError(f"{where}: unsupported unary op")
126
+ validate_expr(expr.get("operand"), f"{where}.operand")
127
+ elif kind == "binary":
128
+ if expr.get("op") not in {"+", "-", "*", "//"}:
129
+ raise IRError(f"{where}: unsupported binary op")
130
+ validate_expr(expr.get("left"), f"{where}.left")
131
+ validate_expr(expr.get("right"), f"{where}.right")
132
+ elif kind == "compare":
133
+ if expr.get("op") not in {"<", "<=", ">", ">=", "==", "!="}:
134
+ raise IRError(f"{where}: unsupported compare")
135
+ validate_expr(expr.get("left"), f"{where}.left")
136
+ validate_expr(expr.get("right"), f"{where}.right")
137
+ elif kind == "call":
138
+ _validate_callee(expr.get("callee"), where)
139
+ args = expr.get("args")
140
+ if not isinstance(args, list):
141
+ raise IRError(f"{where}: call needs args")
142
+ for i, arg in enumerate(args):
143
+ validate_expr(arg, f"{where}.args[{i}]")
144
+ elif kind == "list":
145
+ elems = expr.get("elems")
146
+ if not isinstance(elems, list):
147
+ raise IRError(f"{where}: list needs elems")
148
+ for i, elem in enumerate(elems):
149
+ validate_expr(elem, f"{where}.elems[{i}]")
150
+ elif kind == "dict":
151
+ pairs = expr.get("pairs")
152
+ if not isinstance(pairs, list):
153
+ raise IRError(f"{where}: dict needs pairs")
154
+ for i, pair in enumerate(pairs):
155
+ if not isinstance(pair, dict):
156
+ raise IRError(f"{where}.pairs[{i}] must be an object")
157
+ validate_expr(pair.get("key"), f"{where}.pairs[{i}].key")
158
+ validate_expr(pair.get("value"), f"{where}.pairs[{i}].value")
159
+ elif kind == "subscript":
160
+ validate_expr(expr.get("value"), f"{where}.value")
161
+ validate_expr(expr.get("index"), f"{where}.index")
162
+ elif kind == "attr":
163
+ validate_expr(expr.get("value"), f"{where}.value")
164
+ if not isinstance(expr.get("name"), str):
165
+ raise IRError(f"{where}: attr needs a name")
166
+ elif kind == "cast":
167
+ if expr.get("op") != "int_to_float":
168
+ raise IRError(f"{where}: unsupported cast")
169
+ validate_expr(expr.get("value"), f"{where}.value")
170
+ elif kind == "contains":
171
+ validate_expr(expr.get("hay"), f"{where}.hay")
172
+ validate_expr(expr.get("needle"), f"{where}.needle")
173
+ if not isinstance(expr.get("negated"), bool):
174
+ raise IRError(f"{where}: contains needs negated")
175
+
176
+
177
+ def validate_stmt(stmt: Any, where: str) -> None:
178
+ if not isinstance(stmt, dict) or "kind" not in stmt:
179
+ raise IRError(f"{where}: statement must be an object with kind")
180
+ kind = stmt["kind"]
181
+ _reject_forbidden(kind, where)
182
+ if kind not in STMT_KINDS:
183
+ raise IRError(f"{where}: unknown statement kind {kind!r}")
184
+ if "span" not in stmt:
185
+ raise IRError(f"{where}: statement needs a span")
186
+ if kind == "assign":
187
+ if not isinstance(stmt.get("target"), str):
188
+ raise IRError(f"{where}: assign needs a target name")
189
+ validate_expr(stmt.get("value"), f"{where}.value")
190
+ elif kind == "if":
191
+ validate_expr(stmt.get("cond"), f"{where}.cond")
192
+ _validate_body(stmt.get("then"), f"{where}.then")
193
+ _validate_body(stmt.get("else"), f"{where}.else")
194
+ elif kind == "while":
195
+ validate_expr(stmt.get("cond"), f"{where}.cond")
196
+ _validate_body(stmt.get("body"), f"{where}.body")
197
+ elif kind == "return":
198
+ validate_expr(stmt.get("value"), f"{where}.value")
199
+ elif kind == "expr":
200
+ validate_expr(stmt.get("expr"), f"{where}.expr")
201
+ elif kind == "pass":
202
+ return
203
+ elif kind == "setitem":
204
+ validate_expr(stmt.get("container"), f"{where}.container")
205
+ validate_expr(stmt.get("index"), f"{where}.index")
206
+ validate_expr(stmt.get("value"), f"{where}.value")
207
+ elif kind == "setattr":
208
+ validate_expr(stmt.get("object"), f"{where}.object")
209
+ if not isinstance(stmt.get("name"), str):
210
+ raise IRError(f"{where}: setattr needs a name")
211
+ validate_expr(stmt.get("value"), f"{where}.value")
212
+ elif kind == "raise":
213
+ if not isinstance(stmt.get("exc"), str):
214
+ raise IRError(f"{where}: raise needs an exception name")
215
+ message = stmt.get("message")
216
+ if message is not None:
217
+ validate_expr(message, f"{where}.message")
218
+ elif kind == "try":
219
+ _validate_body(stmt.get("body"), f"{where}.body")
220
+ handlers = stmt.get("handlers")
221
+ if not isinstance(handlers, list) or not handlers:
222
+ raise IRError(f"{where}: try needs handlers")
223
+ for i, handler in enumerate(handlers):
224
+ if not isinstance(handler, dict) or not isinstance(handler.get("exc"), list):
225
+ raise IRError(f"{where}.handlers[{i}] needs exc names")
226
+ _validate_body(handler.get("body"), f"{where}.handlers[{i}].body")
227
+
228
+
229
+ def _validate_callee(callee: Any, where: str) -> None:
230
+ if not isinstance(callee, dict):
231
+ raise IRError(f"{where}: call callee must be an object")
232
+ kind = callee.get("kind")
233
+ if kind in {"function", "builtin", "class"}:
234
+ if not isinstance(callee.get("name"), str):
235
+ raise IRError(f"{where}: call callee needs a name")
236
+ return
237
+ if kind == "module":
238
+ if not isinstance(callee.get("module"), str) or not isinstance(callee.get("name"), str):
239
+ raise IRError(f"{where}: module callee needs module and name")
240
+ return
241
+ if kind == "method":
242
+ if callee.get("receiver") not in {"str", "bytes"} or not isinstance(callee.get("name"), str):
243
+ raise IRError(f"{where}: method callee needs a str or bytes receiver and a name")
244
+ return
245
+ raise IRError(f"{where}: call callee must be function, builtin, module, method, or class")
246
+
247
+
248
+ def _validate_body(body: Any, where: str) -> None:
249
+ if not isinstance(body, list):
250
+ raise IRError(f"{where}: body must be a list")
251
+ for i, stmt in enumerate(body):
252
+ validate_stmt(stmt, f"{where}[{i}]")
253
+
254
+
255
+ def validate_module(module: Any) -> None:
256
+ if not isinstance(module, dict):
257
+ raise IRError("module must be an object")
258
+ if module.get("irVersion") != IR_VERSION:
259
+ raise IRError(f"irVersion must be {IR_VERSION}")
260
+ if module.get("subset") != SUBSET:
261
+ raise IRError(f"subset must be {SUBSET!r}")
262
+ if not isinstance(module.get("cpython"), str):
263
+ raise IRError("cpython version string is required")
264
+ if not isinstance(module.get("sourceFile"), str):
265
+ raise IRError("sourceFile is required")
266
+ functions = module.get("functions")
267
+ if not isinstance(functions, list):
268
+ raise IRError("functions must be a list")
269
+ names: set[str] = set()
270
+ for i, fn in enumerate(functions):
271
+ if not isinstance(fn, dict):
272
+ raise IRError(f"functions[{i}] must be an object")
273
+ name = fn.get("name")
274
+ if not isinstance(name, str) or not _dotted_identifier(name):
275
+ raise IRError(f"functions[{i}] needs an identifier name")
276
+ if name in names:
277
+ raise IRError(f"duplicate function {name}")
278
+ names.add(name)
279
+ validate_type(fn.get("returnType"), f"{name}.returnType")
280
+ params = fn.get("params")
281
+ if not isinstance(params, list):
282
+ raise IRError(f"{name}: params must be a list")
283
+ for j, param in enumerate(params):
284
+ if not isinstance(param, dict) or not isinstance(param.get("name"), str):
285
+ raise IRError(f"{name}.params[{j}] needs a name")
286
+ validate_type(param.get("type"), f"{name}.params[{j}].type")
287
+ locals_ = fn.get("locals")
288
+ if not isinstance(locals_, list):
289
+ raise IRError(f"{name}: locals must be a list")
290
+ for j, local in enumerate(locals_):
291
+ if not isinstance(local, dict) or not isinstance(local.get("name"), str):
292
+ raise IRError(f"{name}.locals[{j}] needs a name")
293
+ validate_type(local.get("type"), f"{name}.locals[{j}].type")
294
+ _validate_body(fn.get("body"), f"{name}.body")
295
+ classes = module.get("classes")
296
+ if not isinstance(classes, list):
297
+ raise IRError("classes must be a list")
298
+ class_names: set[str] = set()
299
+ for i, cls in enumerate(classes):
300
+ if not isinstance(cls, dict) or not isinstance(cls.get("name"), str):
301
+ raise IRError(f"classes[{i}] needs a name")
302
+ if cls["name"] in class_names or cls["name"] in names:
303
+ raise IRError(f"duplicate class {cls['name']}")
304
+ class_names.add(cls["name"])
305
+ fields = cls.get("fields")
306
+ if not isinstance(fields, list):
307
+ raise IRError(f"{cls['name']}: fields must be a list")
308
+ for j, field in enumerate(fields):
309
+ if not isinstance(field, dict) or not isinstance(field.get("name"), str):
310
+ raise IRError(f"{cls['name']}.fields[{j}] needs a name")
311
+ validate_type(field.get("type"), f"{cls['name']}.fields[{j}].type")
312
+ methods = cls.get("methods")
313
+ if not isinstance(methods, list):
314
+ raise IRError(f"{cls['name']}: methods must be a list")
315
+ for j, method in enumerate(methods):
316
+ if not isinstance(method, dict) or not isinstance(method.get("name"), str):
317
+ raise IRError(f"{cls['name']}.methods[{j}] needs a name")
318
+ label = f"{cls['name']}.{method['name']}"
319
+ validate_type(method.get("returnType"), f"{label}.returnType")
320
+ params = method.get("params")
321
+ if not isinstance(params, list):
322
+ raise IRError(f"{label}: params must be a list")
323
+ for k, param in enumerate(params):
324
+ if not isinstance(param, dict) or not isinstance(param.get("name"), str):
325
+ raise IRError(f"{label}.params[{k}] needs a name")
326
+ validate_type(param.get("type"), f"{label}.params[{k}].type")
327
+ locals_ = method.get("locals")
328
+ if not isinstance(locals_, list):
329
+ raise IRError(f"{label}: locals must be a list")
330
+ _validate_body(method.get("body"), f"{label}.body")
331
+ entry = module.get("entry")
332
+ if functions and entry not in names:
333
+ raise IRError("entry must name a function")
334
+ if not functions and entry is not None:
335
+ raise IRError("entry must be null when there are no functions")
336
+ fences = module.get("fences", [])
337
+ if not isinstance(fences, list):
338
+ raise IRError("fences must be a list")
339
+ for i, fence in enumerate(fences):
340
+ if not isinstance(fence, dict) or not isinstance(fence.get("code"), str):
341
+ raise IRError(f"fences[{i}] needs a code")
342
+ if "message" not in fence or "span" not in fence:
343
+ raise IRError(f"fences[{i}] needs a message and span")
344
+
345
+
346
+ def _dotted_identifier(name: str) -> bool:
347
+ parts = name.split(".")
348
+ return bool(parts) and all(part.isidentifier() for part in parts)
349
+
350
+
351
+ def assert_same_cpython(module: dict[str, Any]) -> None:
352
+ """LLVM emission refuses an IR stamped by a different CPython.
353
+
354
+ Re-run the corpus on the new interpreter, then lower again. The stamp is
355
+ not a hint the backend may ignore.
356
+ """
357
+
358
+ validate_module(module)
359
+ running = cpython_version()
360
+ stamped = module["cpython"]
361
+ if stamped != running:
362
+ raise VersionError(
363
+ f"IR was lowered on CPython {stamped}, this process is {running}; "
364
+ "re-lower and re-run the corpus before emitting LLVM"
365
+ )