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.
- pyscriptc/__init__.py +3 -0
- pyscriptc/__main__.py +3 -0
- pyscriptc/_posthog_token.py +1 -0
- pyscriptc/builtins.py +156 -0
- pyscriptc/canonical.py +167 -0
- pyscriptc/check.py +49 -0
- pyscriptc/cli.py +271 -0
- pyscriptc/coverage.py +388 -0
- pyscriptc/debug.py +120 -0
- pyscriptc/diagnostics.py +179 -0
- pyscriptc/errors.py +56 -0
- pyscriptc/ir.py +365 -0
- pyscriptc/llvm.py +2129 -0
- pyscriptc/log.py +127 -0
- pyscriptc/lower/__init__.py +1071 -0
- pyscriptc/lower/_calls.py +1499 -0
- pyscriptc/lower/_classes.py +969 -0
- pyscriptc/lower/_core.py +301 -0
- pyscriptc/lower/_expr.py +886 -0
- pyscriptc/lower/_stdlib.py +491 -0
- pyscriptc/lower/_stdlib_shared.py +93 -0
- pyscriptc/lower/_stmt.py +1173 -0
- pyscriptc/package.py +249 -0
- pyscriptc/restlower.py +1008 -0
- pyscriptc/runtime/ps_argv.c +28 -0
- pyscriptc/runtime/ps_b64.c +168 -0
- pyscriptc/runtime/ps_coll.c +133 -0
- pyscriptc/runtime/ps_dt.c +388 -0
- pyscriptc/runtime/ps_hash.c +411 -0
- pyscriptc/runtime/ps_help.c +219 -0
- pyscriptc/runtime/ps_hmac.c +120 -0
- pyscriptc/runtime/ps_io.c +198 -0
- pyscriptc/runtime/ps_island.c +272 -0
- pyscriptc/runtime/ps_json.c +402 -0
- pyscriptc/runtime/ps_mix.c +47 -0
- pyscriptc/runtime/ps_more.c +1089 -0
- pyscriptc/runtime/ps_osmore.c +447 -0
- pyscriptc/runtime/ps_path.c +547 -0
- pyscriptc/runtime/ps_re.c +1102 -0
- pyscriptc/runtime/ps_rt.c +1175 -0
- pyscriptc/runtime/ps_rt.h +189 -0
- pyscriptc/runtime/ps_struct.c +291 -0
- pyscriptc/runtime/ps_time.c +207 -0
- pyscriptc/runtime/ps_unicode.c +1521 -0
- pyscriptc/runtime/ps_url.c +327 -0
- pyscriptc/stdlib_queue.py +191 -0
- pyscriptc/stdmore.py +1596 -0
- pyscriptc/telemetry.py +219 -0
- pyscriptc-0.1.0.dist-info/METADATA +187 -0
- pyscriptc-0.1.0.dist-info/RECORD +54 -0
- pyscriptc-0.1.0.dist-info/WHEEL +5 -0
- pyscriptc-0.1.0.dist-info/entry_points.txt +2 -0
- pyscriptc-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyscriptc-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
"""Lower CPython's ast into the spike IR.
|
|
2
|
+
|
|
3
|
+
Three gates are lowered: a closed import table and calls, containers
|
|
4
|
+
(`list`, `dict`, `str`/`bytes` methods, `for`, exceptions), and annotated
|
|
5
|
+
classes. `for` is desugared to `while` so the iterator is consumed once and
|
|
6
|
+
a write to the loop target does not affect the next step. The codec wave
|
|
7
|
+
(`re`, `json`, `urllib.parse`, `base64`, `hashlib`, `struct`) lowers a
|
|
8
|
+
closed set of calls. Unknown syntax and queued stdlib modules stay fences.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any, NoReturn
|
|
17
|
+
|
|
18
|
+
from pyscriptc.errors import format_diagnostic, span_of
|
|
19
|
+
from pyscriptc.ir import SUBSET, cpython_version, types_equal
|
|
20
|
+
from pyscriptc.lower._calls import CallsMixin
|
|
21
|
+
from pyscriptc.lower._classes import ClassesMixin
|
|
22
|
+
from pyscriptc.lower._core import (
|
|
23
|
+
_CMP,
|
|
24
|
+
_TRUTHY,
|
|
25
|
+
Binding,
|
|
26
|
+
Fence,
|
|
27
|
+
_compare,
|
|
28
|
+
_const_none,
|
|
29
|
+
_declared_globals,
|
|
30
|
+
_dotted,
|
|
31
|
+
_is_none_expr,
|
|
32
|
+
_name,
|
|
33
|
+
_show,
|
|
34
|
+
_tuple_assign,
|
|
35
|
+
)
|
|
36
|
+
from pyscriptc.lower._expr import ExprMixin
|
|
37
|
+
from pyscriptc.lower._stdlib import StdlibDispatchMixin
|
|
38
|
+
from pyscriptc.lower._stdlib_shared import (
|
|
39
|
+
_ERASED_MODULES,
|
|
40
|
+
_module_assigned,
|
|
41
|
+
_release_erased_import,
|
|
42
|
+
)
|
|
43
|
+
from pyscriptc.lower._stmt import (
|
|
44
|
+
StmtMixin,
|
|
45
|
+
_always_returns,
|
|
46
|
+
_is_docstring,
|
|
47
|
+
_is_main_guard,
|
|
48
|
+
_stmt_breaks_this_loop,
|
|
49
|
+
_stmt_continues_this_loop,
|
|
50
|
+
)
|
|
51
|
+
from pyscriptc.stdlib_queue import module_status
|
|
52
|
+
from pyscriptc.stdmore import member_callable, module_constant
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Lowerer(StdlibDispatchMixin, ExprMixin, StmtMixin, CallsMixin, ClassesMixin):
|
|
56
|
+
def __init__(self, filename: str, source: str) -> None:
|
|
57
|
+
self.filename = filename
|
|
58
|
+
self.source = source
|
|
59
|
+
self.function: str | None = None
|
|
60
|
+
self.sigs: dict[str, tuple[list[dict[str, Any]], dict[str, Any]]] = {}
|
|
61
|
+
self.locals: dict[str, dict[str, Any]] = {}
|
|
62
|
+
self.bindings: dict[str, Binding] = {}
|
|
63
|
+
self.classes: dict[str, dict[str, Any]] = {}
|
|
64
|
+
self.declared: set[str] = set()
|
|
65
|
+
self.known_classes: set[str] = set()
|
|
66
|
+
self.synthetics: set[str] = set()
|
|
67
|
+
self.temp_n = 0
|
|
68
|
+
self.caught: list[Fence] = []
|
|
69
|
+
self.prelude: list[dict[str, Any]] = []
|
|
70
|
+
self.constants: dict[str, dict[str, Any]] = {}
|
|
71
|
+
self.type_aliases: dict[str, str] = {}
|
|
72
|
+
self.comp_elt: ast.expr | None = None
|
|
73
|
+
self.comp_dest: str | None = None
|
|
74
|
+
self.comp_init: dict[str, Any] | None = None
|
|
75
|
+
self.comp_expect: dict[str, Any] | None = None
|
|
76
|
+
self.comp_ready = False
|
|
77
|
+
self.comp_if: ast.expr | None = None
|
|
78
|
+
self.break_flag: str | None = None
|
|
79
|
+
self.continue_flag: str | None = None
|
|
80
|
+
self.module_name = ""
|
|
81
|
+
self.is_package = False
|
|
82
|
+
self.cataloguing = False
|
|
83
|
+
self.package: dict[str, dict[str, dict[str, Any]]] | None = None
|
|
84
|
+
self.pkg_funcs: dict[str, dict[str, Any]] = {}
|
|
85
|
+
self.class_aliases: dict[str, str] = {}
|
|
86
|
+
self.global_slots: dict[str, dict[str, Any]] = {}
|
|
87
|
+
self.fn_globals: set[str] = set()
|
|
88
|
+
# `global` declarations per function body, cached by body-list identity:
|
|
89
|
+
# the scan pass and the lowering pass would otherwise walk each function
|
|
90
|
+
# tree twice for the same `ast.Global` nodes. Bodies are not mutated
|
|
91
|
+
# after parsing, so identity is a stable key within one lower_file run.
|
|
92
|
+
self._globals_cache: dict[int, frozenset[str]] = {}
|
|
93
|
+
# The function body being lowered, so `del name` can see a later load.
|
|
94
|
+
self.fn_body: list[ast.stmt] = []
|
|
95
|
+
# Names bound to frozenset(...). The value is still the set list. .add and
|
|
96
|
+
# .remove on these names stay fenced. A plain set name is not in this set.
|
|
97
|
+
self.frozen_names: set[str] = set()
|
|
98
|
+
# User exception classes take codes after the builtin table (1–7);
|
|
99
|
+
# builtins.USER_EXC_BASE pins the same floor.
|
|
100
|
+
self.exc_n = 8
|
|
101
|
+
|
|
102
|
+
def fail(self, node: ast.AST, code: str, message: str) -> NoReturn:
|
|
103
|
+
raise Fence(code, message, span_of(node, self.filename), self.function)
|
|
104
|
+
|
|
105
|
+
def _desugar(self, tree: ast.Module) -> ast.Module:
|
|
106
|
+
"""Pre-lowering syntax pass. Runs once, before any statement is
|
|
107
|
+
lowered (called from `module`).
|
|
108
|
+
|
|
109
|
+
A desugar pass receives the parsed module tree and returns the tree
|
|
110
|
+
to lower: a replacement `ast.Module` whose statements the lowering
|
|
111
|
+
below accepts. It must not lower anything itself, touch IR state
|
|
112
|
+
(`self.bindings`, `self.prelude`, `self.sigs`, ...), or collect
|
|
113
|
+
fences; those belong to the lowering passes that run after it.
|
|
114
|
+
|
|
115
|
+
Today lowering happens inline (`for` becomes `while` inside
|
|
116
|
+
`_for`/`_for_range`, not before it), so this hook is the identity.
|
|
117
|
+
E1 replaces the body with the first real AST-to-AST pass.
|
|
118
|
+
"""
|
|
119
|
+
return tree
|
|
120
|
+
|
|
121
|
+
def module(self, tree: ast.Module) -> dict[str, Any]:
|
|
122
|
+
tree = self._desugar(tree)
|
|
123
|
+
fences: list[Fence] = []
|
|
124
|
+
functions: list[ast.FunctionDef] = []
|
|
125
|
+
class_nodes: list[ast.ClassDef] = []
|
|
126
|
+
live = self._live_statements(list(tree.body), fences)
|
|
127
|
+
self.global_slots = self._scan_global_slots(live)
|
|
128
|
+
entry_stmts = self._split_entry(live, functions, class_nodes, fences)
|
|
129
|
+
entry_fn = self._entry_function(entry_stmts, functions)
|
|
130
|
+
entry_name = entry_fn[0].name if entry_fn else None
|
|
131
|
+
functions.extend(entry_fn)
|
|
132
|
+
self._install_synthetics(class_nodes, fences)
|
|
133
|
+
imported = {name for name, info in self.classes.items() if info.get("imported")}
|
|
134
|
+
imported.update(self.class_aliases)
|
|
135
|
+
self.known_classes = {node.name for node in class_nodes}
|
|
136
|
+
self.known_classes.update(name for name, info in self.classes.items() if info.get("synthetic"))
|
|
137
|
+
self.known_classes.update(imported)
|
|
138
|
+
for node in class_nodes:
|
|
139
|
+
self.function = node.name
|
|
140
|
+
self.caught = []
|
|
141
|
+
try:
|
|
142
|
+
self._collect_class(node)
|
|
143
|
+
except Fence as exc:
|
|
144
|
+
fences.append(exc)
|
|
145
|
+
fences.extend(self.caught)
|
|
146
|
+
for fn in functions:
|
|
147
|
+
self.function = fn.name
|
|
148
|
+
if fn.name in self.declared:
|
|
149
|
+
fences.append(Fence("PS2002", f"{fn.name} is already bound", span_of(fn, self.filename), fn.name))
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
kind = "static" if self._is_module_staticmethod(fn) else "method"
|
|
153
|
+
self.sigs[fn.name] = self._signature(fn, None, kind)
|
|
154
|
+
except Fence as exc:
|
|
155
|
+
fences.append(exc)
|
|
156
|
+
if self.cataloguing:
|
|
157
|
+
self.function = None
|
|
158
|
+
return {
|
|
159
|
+
"irVersion": 1,
|
|
160
|
+
"subset": SUBSET,
|
|
161
|
+
"cpython": cpython_version(),
|
|
162
|
+
"sourceFile": self.filename,
|
|
163
|
+
"entry": None,
|
|
164
|
+
"functions": [],
|
|
165
|
+
"classes": [],
|
|
166
|
+
"fences": [fence.as_dict() for fence in fences],
|
|
167
|
+
}
|
|
168
|
+
lowered_classes: list[dict[str, Any]] = []
|
|
169
|
+
for node in class_nodes:
|
|
170
|
+
if node.name not in self.classes or self.classes[node.name].get("synthetic"):
|
|
171
|
+
continue
|
|
172
|
+
self.function = node.name
|
|
173
|
+
self.caught = []
|
|
174
|
+
try:
|
|
175
|
+
lowered = self._lower_class(node)
|
|
176
|
+
except Fence as exc:
|
|
177
|
+
fences.append(exc)
|
|
178
|
+
fences.extend(self.caught)
|
|
179
|
+
continue
|
|
180
|
+
fences.extend(self.caught)
|
|
181
|
+
if lowered is not None and not self.caught:
|
|
182
|
+
lowered_classes.append(lowered)
|
|
183
|
+
lowered_classes.extend(self._synthetic_classes({item["name"] for item in lowered_classes}))
|
|
184
|
+
lowered: list[dict[str, Any]] = []
|
|
185
|
+
for fn in functions:
|
|
186
|
+
if fn.name not in self.sigs:
|
|
187
|
+
continue
|
|
188
|
+
self.function = fn.name
|
|
189
|
+
self.temp_n = 0
|
|
190
|
+
self.prelude = []
|
|
191
|
+
self.caught = []
|
|
192
|
+
self.fn_globals = self._globals_for(fn.body)
|
|
193
|
+
params, ret = self.sigs[fn.name]
|
|
194
|
+
self.locals = {param["name"]: param["type"] for param in params}
|
|
195
|
+
self.frozen_names = set()
|
|
196
|
+
self.fn_body = list(fn.body)
|
|
197
|
+
try:
|
|
198
|
+
body = self._body(list(fn.body))
|
|
199
|
+
if self.caught:
|
|
200
|
+
fences.extend(self.caught)
|
|
201
|
+
# The entry is not a user function: its fences are already
|
|
202
|
+
# in the module list and block emission, so the partial
|
|
203
|
+
# body still carries the flip signal in the IR.
|
|
204
|
+
if entry_name is None or fn.name != entry_name:
|
|
205
|
+
continue
|
|
206
|
+
if ret["kind"] != "none" and not _always_returns(body):
|
|
207
|
+
self.fail(fn, "PS1004", f"{fn.name} can fall off the end and return None")
|
|
208
|
+
lowered.append(
|
|
209
|
+
{
|
|
210
|
+
"name": self._ir_name(fn.name),
|
|
211
|
+
"params": params,
|
|
212
|
+
"returnType": ret,
|
|
213
|
+
"locals": [{"name": name, "type": typ} for name, typ in self.locals.items()],
|
|
214
|
+
"body": body,
|
|
215
|
+
"span": span_of(fn, self.filename),
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
except Fence as exc:
|
|
219
|
+
fences.append(exc)
|
|
220
|
+
lowered_classes.extend(self._synthetic_classes({item["name"] for item in lowered_classes}))
|
|
221
|
+
self.function = None
|
|
222
|
+
names = [item["name"] for item in lowered]
|
|
223
|
+
return {
|
|
224
|
+
"irVersion": 1,
|
|
225
|
+
"subset": SUBSET,
|
|
226
|
+
"cpython": cpython_version(),
|
|
227
|
+
"sourceFile": self.filename,
|
|
228
|
+
# A definition-only module keeps the old shape: entry is the first
|
|
229
|
+
# function. Only a synthesized entry overrides that.
|
|
230
|
+
"entry": entry_name if entry_name in names else lowered[0]["name"] if lowered else None,
|
|
231
|
+
"functions": lowered,
|
|
232
|
+
"classes": lowered_classes,
|
|
233
|
+
"fences": [fence.as_dict() for fence in fences],
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def _split_entry(
|
|
237
|
+
self,
|
|
238
|
+
live: list[ast.stmt],
|
|
239
|
+
functions: list[ast.FunctionDef],
|
|
240
|
+
class_nodes: list[ast.ClassDef],
|
|
241
|
+
fences: list[Fence],
|
|
242
|
+
) -> list[ast.stmt]:
|
|
243
|
+
"""Walk the live statements: definitions stay where they are, script
|
|
244
|
+
statements collect into the entry body, in source order.
|
|
245
|
+
|
|
246
|
+
A module assignment tries the constant binding first. When the module
|
|
247
|
+
also carries live script statements the failed binding rolls back and
|
|
248
|
+
the assignment lowers inside the entry instead; a definition-only
|
|
249
|
+
module keeps the constant fence it always raised. `pass` is inert
|
|
250
|
+
(C3) and never makes a script.
|
|
251
|
+
"""
|
|
252
|
+
entry_stmts: list[ast.stmt] = []
|
|
253
|
+
failed_assigns: list[tuple[ast.stmt, int]] = []
|
|
254
|
+
definitions = (ast.Import, ast.ImportFrom, ast.Pass, ast.FunctionDef, ast.ClassDef, ast.AnnAssign, ast.Assign)
|
|
255
|
+
scriptish = any(not isinstance(stmt, definitions) and not _is_docstring(stmt) for stmt in live)
|
|
256
|
+
divert = self._diverted_literals(live) if scriptish else set()
|
|
257
|
+
for stmt in live:
|
|
258
|
+
if isinstance(stmt, (ast.Import, ast.ImportFrom, ast.Pass)):
|
|
259
|
+
continue
|
|
260
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
261
|
+
if not self._accept_module_function(stmt, fences):
|
|
262
|
+
continue
|
|
263
|
+
functions.append(stmt)
|
|
264
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
265
|
+
class_nodes.append(stmt)
|
|
266
|
+
elif isinstance(stmt, (ast.AnnAssign, ast.Assign)):
|
|
267
|
+
targets = stmt.targets if isinstance(stmt, ast.Assign) else [stmt.target]
|
|
268
|
+
if any(isinstance(t, ast.Name) and t.id in divert for t in targets):
|
|
269
|
+
entry_stmts.append(stmt)
|
|
270
|
+
continue
|
|
271
|
+
snapshot = (dict(self.constants), set(self.declared), len(fences))
|
|
272
|
+
self.caught = []
|
|
273
|
+
self._bind_module_assign(stmt)
|
|
274
|
+
# A failed binding rolls back at once, so later assignments
|
|
275
|
+
# bind against the pre-statement state. Its fences stay for
|
|
276
|
+
# now; when the module turns out to be a script they are
|
|
277
|
+
# retracted and the statement lowers inside the entry.
|
|
278
|
+
if self.caught:
|
|
279
|
+
self.constants = snapshot[0]
|
|
280
|
+
self.declared = snapshot[1]
|
|
281
|
+
fences.extend(self.caught)
|
|
282
|
+
failed_assigns.append((stmt, snapshot[2]))
|
|
283
|
+
self.caught = []
|
|
284
|
+
elif _is_docstring(stmt):
|
|
285
|
+
continue
|
|
286
|
+
else:
|
|
287
|
+
entry_stmts.append(stmt)
|
|
288
|
+
if entry_stmts:
|
|
289
|
+
# This module is a script: retract the fences the failed constant
|
|
290
|
+
# bindings raised; those assignments lower inside the entry
|
|
291
|
+
# instead, in source order. Bindings that did not fence stay
|
|
292
|
+
# bound - a script may still carry true constants.
|
|
293
|
+
retract = min((at for _, at in failed_assigns), default=len(fences))
|
|
294
|
+
if retract < len(fences):
|
|
295
|
+
del fences[retract:]
|
|
296
|
+
entry_stmts.extend(stmt for stmt, _ in failed_assigns)
|
|
297
|
+
entry_stmts.sort(key=lambda stmt: stmt.lineno)
|
|
298
|
+
return entry_stmts
|
|
299
|
+
|
|
300
|
+
def _diverted_literals(self, live: list[ast.stmt]) -> set[str]:
|
|
301
|
+
"""Names bound to container literals whose entry mutates them by subscript.
|
|
302
|
+
|
|
303
|
+
Module constants re-emit their literal on every read, so a script's
|
|
304
|
+
`counts[k] = v` would write into a throwaway dict; the binding lowers
|
|
305
|
+
inside the entry instead unless the name hides inside a def/class.
|
|
306
|
+
"""
|
|
307
|
+
bound: set[str] = set()
|
|
308
|
+
for stmt in live:
|
|
309
|
+
if isinstance(stmt, (ast.Assign, ast.AnnAssign)) and isinstance(stmt.value, (ast.Dict, ast.List, ast.Set)):
|
|
310
|
+
targets = stmt.targets if isinstance(stmt, ast.Assign) else [stmt.target]
|
|
311
|
+
bound.update(t.id for t in targets if isinstance(t, ast.Name))
|
|
312
|
+
if not bound:
|
|
313
|
+
return set()
|
|
314
|
+
hidden: set[str] = set()
|
|
315
|
+
for stmt in live:
|
|
316
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
317
|
+
hidden.update(n.id for n in ast.walk(stmt) if isinstance(n, ast.Name) and n.id in bound)
|
|
318
|
+
mutated: set[str] = set()
|
|
319
|
+
for stmt in live:
|
|
320
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
321
|
+
continue
|
|
322
|
+
for n in ast.walk(stmt):
|
|
323
|
+
if (
|
|
324
|
+
isinstance(n, ast.Subscript)
|
|
325
|
+
and isinstance(n.value, ast.Name)
|
|
326
|
+
and n.value.id in bound
|
|
327
|
+
and isinstance(n.ctx, (ast.Store, ast.Del))
|
|
328
|
+
):
|
|
329
|
+
mutated.add(n.value.id)
|
|
330
|
+
return mutated - hidden
|
|
331
|
+
|
|
332
|
+
def _entry_function(self, stmts: list[ast.stmt], functions: list[ast.FunctionDef]) -> list[ast.FunctionDef]:
|
|
333
|
+
"""The synthesized entry for module-level statements.
|
|
334
|
+
|
|
335
|
+
A definition-only module (imports, constants, functions, classes,
|
|
336
|
+
docstrings) synthesizes nothing: the IR is exactly what it was before
|
|
337
|
+
entry lowering existed. Scripts get one function named `main`, or
|
|
338
|
+
`__entry__` when the module defines its own `main` - the user's name
|
|
339
|
+
always wins - carrying the live statements in source order.
|
|
340
|
+
|
|
341
|
+
Package units never synthesize one: a package is a library, its
|
|
342
|
+
modules are namespaced, and `sigs` feeds the export table.
|
|
343
|
+
"""
|
|
344
|
+
if not stmts or self.module_name or self.cataloguing:
|
|
345
|
+
return []
|
|
346
|
+
taken = {fn.name for fn in functions} | self.declared
|
|
347
|
+
name = "main" if "main" not in taken else "__entry__"
|
|
348
|
+
fn = ast.FunctionDef(
|
|
349
|
+
name=name,
|
|
350
|
+
args=ast.arguments(
|
|
351
|
+
posonlyargs=[],
|
|
352
|
+
args=[],
|
|
353
|
+
vararg=None,
|
|
354
|
+
kwonlyargs=[],
|
|
355
|
+
kw_defaults=[],
|
|
356
|
+
kwarg=None,
|
|
357
|
+
defaults=[],
|
|
358
|
+
),
|
|
359
|
+
body=stmts,
|
|
360
|
+
decorator_list=[],
|
|
361
|
+
returns=ast.Constant(value=None),
|
|
362
|
+
type_comment=None,
|
|
363
|
+
type_params=[],
|
|
364
|
+
)
|
|
365
|
+
for stmt in stmts:
|
|
366
|
+
ast.copy_location(fn, stmt)
|
|
367
|
+
fn.lineno = stmts[0].lineno
|
|
368
|
+
fn.col_offset = 0
|
|
369
|
+
fn.end_lineno = stmts[-1].end_lineno
|
|
370
|
+
fn.end_col_offset = 0
|
|
371
|
+
return [fn]
|
|
372
|
+
|
|
373
|
+
def _ir_name(self, name: str) -> str:
|
|
374
|
+
if self.module_name:
|
|
375
|
+
return f"{self.module_name}.{name}"
|
|
376
|
+
return name
|
|
377
|
+
|
|
378
|
+
def _bind_one_export(self, module: str, name: str, local: str, stmt: ast.AST, fences: list[Fence]) -> None:
|
|
379
|
+
package = self.package or {}
|
|
380
|
+
item = package.get(module, {}).get(name)
|
|
381
|
+
if item is None:
|
|
382
|
+
fences.append(
|
|
383
|
+
Fence(
|
|
384
|
+
"PS2002",
|
|
385
|
+
f"{module}.{name} is not exported by this package",
|
|
386
|
+
span_of(stmt, self.filename),
|
|
387
|
+
None,
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
return
|
|
391
|
+
if local in self.bindings or local in self.constants or local in self.declared or local in self.sigs:
|
|
392
|
+
fences.append(Fence("PS2002", f"{local} is already bound", span_of(stmt, self.filename), None))
|
|
393
|
+
return
|
|
394
|
+
if item["kind"] == "const":
|
|
395
|
+
self.constants[local] = item["value"]
|
|
396
|
+
self.declared.add(local)
|
|
397
|
+
return
|
|
398
|
+
if item["kind"] == "func":
|
|
399
|
+
self.pkg_funcs[local] = item
|
|
400
|
+
self.bindings[local] = Binding("pyfunc", module, name, False, item["return"])
|
|
401
|
+
self.declared.add(local)
|
|
402
|
+
return
|
|
403
|
+
if item["kind"] == "class":
|
|
404
|
+
self._install_class_export(local, item, stmt, fences)
|
|
405
|
+
return
|
|
406
|
+
fences.append(
|
|
407
|
+
Fence("PS2002", f"{module}.{name} is not a function or constant", span_of(stmt, self.filename), None)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _package_root(self) -> str | None:
|
|
411
|
+
if self.package is None or not self.module_name:
|
|
412
|
+
return None
|
|
413
|
+
return self.module_name.split(".", 1)[0]
|
|
414
|
+
|
|
415
|
+
def _is_own_package(self, module: str | None) -> bool:
|
|
416
|
+
root = self._package_root()
|
|
417
|
+
if root is None or not module:
|
|
418
|
+
return False
|
|
419
|
+
return module == root or module.startswith(root + ".")
|
|
420
|
+
|
|
421
|
+
def _pkg_item(self, node: ast.AST, module: str, member: str) -> dict[str, Any]:
|
|
422
|
+
item = (self.package or {}).get(module, {}).get(member)
|
|
423
|
+
if item is None:
|
|
424
|
+
self.fail(node, "PS2001", f"{module}.{member} is not exported by this package")
|
|
425
|
+
return item
|
|
426
|
+
|
|
427
|
+
def _pkg_export(self, node: ast.AST, module: str, member: str) -> dict[str, Any]:
|
|
428
|
+
item = self._pkg_item(node, module, member)
|
|
429
|
+
if item["kind"] not in {"func", "const"}:
|
|
430
|
+
self.fail(node, "PS2001", f"{module}.{member} is not exported by this package")
|
|
431
|
+
return item
|
|
432
|
+
|
|
433
|
+
def _linked_call(self, node: ast.Call, span: dict[str, Any], item: dict[str, Any], what: str) -> dict[str, Any]:
|
|
434
|
+
if item["kind"] != "func":
|
|
435
|
+
self.fail(node, "PS2002", f"{what} is not a function")
|
|
436
|
+
args = self._fill_args(node, item["params"], what)
|
|
437
|
+
return {
|
|
438
|
+
"kind": "call",
|
|
439
|
+
"callee": {"kind": "function", "name": item["ir_name"]},
|
|
440
|
+
"args": args,
|
|
441
|
+
"type": item["return"],
|
|
442
|
+
"span": span,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
def _body(self, statements: list[ast.stmt]) -> list[dict[str, Any]]:
|
|
446
|
+
body: list[dict[str, Any]] = []
|
|
447
|
+
for index, stmt in enumerate(statements):
|
|
448
|
+
if _is_docstring(stmt):
|
|
449
|
+
continue
|
|
450
|
+
try:
|
|
451
|
+
lowered = self._stmt(stmt)
|
|
452
|
+
except Fence as exc:
|
|
453
|
+
self.caught.append(exc)
|
|
454
|
+
continue
|
|
455
|
+
body.extend(lowered)
|
|
456
|
+
skips = (self.break_flag is not None and _stmt_breaks_this_loop(stmt)) or (
|
|
457
|
+
self.continue_flag is not None and _stmt_continues_this_loop(stmt)
|
|
458
|
+
)
|
|
459
|
+
if skips:
|
|
460
|
+
rest = self._body(statements[index + 1 :])
|
|
461
|
+
if rest:
|
|
462
|
+
body.append(self._skip_rest(rest, span_of(stmt, self.filename)))
|
|
463
|
+
return body
|
|
464
|
+
return body
|
|
465
|
+
|
|
466
|
+
def _setitem(self, stmt: ast.Assign, span: dict[str, Any]) -> dict[str, Any]:
|
|
467
|
+
target = stmt.targets[0]
|
|
468
|
+
assert isinstance(target, ast.Subscript)
|
|
469
|
+
if isinstance(target.slice, ast.Slice):
|
|
470
|
+
self.fail(target, "PS2002", "slices are not lowered")
|
|
471
|
+
container = self._expr(target.value)
|
|
472
|
+
index = self._expr(target.slice)
|
|
473
|
+
kind = container["type"]["kind"]
|
|
474
|
+
if kind == "list" and container["type"].get("set"):
|
|
475
|
+
self.fail(target, "PS2002", "a set has no item assignment")
|
|
476
|
+
if kind == "list":
|
|
477
|
+
if index["type"]["kind"] != "int":
|
|
478
|
+
self.fail(target, "PS1003", "a list index is int")
|
|
479
|
+
value = self._expr(stmt.value, container["type"]["elem"])
|
|
480
|
+
self._expect(stmt, value["type"], container["type"]["elem"], "list item")
|
|
481
|
+
elif kind == "dict":
|
|
482
|
+
self._expect(target, index["type"], container["type"]["key"], "dict key")
|
|
483
|
+
value = self._expr(stmt.value, container["type"]["value"])
|
|
484
|
+
self._expect(stmt, value["type"], container["type"]["value"], "dict value")
|
|
485
|
+
else:
|
|
486
|
+
self.fail(target, "PS2002", "item assignment targets a list or dict")
|
|
487
|
+
return {"kind": "setitem", "container": container, "index": index, "value": value, "span": span}
|
|
488
|
+
|
|
489
|
+
def _setattr(self, stmt: ast.Assign, span: dict[str, Any]) -> dict[str, Any]:
|
|
490
|
+
target = stmt.targets[0]
|
|
491
|
+
assert isinstance(target, ast.Attribute)
|
|
492
|
+
obj = self._expr(target.value)
|
|
493
|
+
if obj["type"]["kind"] != "class":
|
|
494
|
+
self.fail(target, "PS2001", f"attribute assignment is not lowered ({_dotted(target)})")
|
|
495
|
+
class_name = obj["type"]["name"]
|
|
496
|
+
field = next((item for item in self.classes[class_name]["fields"] if item["name"] == target.attr), None)
|
|
497
|
+
if field is None:
|
|
498
|
+
self.fail(target, "PS2001", f"{class_name}.{target.attr} is not a field")
|
|
499
|
+
value = self._expr(stmt.value, field["type"])
|
|
500
|
+
self._expect(stmt, value["type"], field["type"], target.attr)
|
|
501
|
+
return {"kind": "setattr", "object": obj, "name": target.attr, "value": value, "span": span}
|
|
502
|
+
|
|
503
|
+
def _bool_cond(self, node: ast.expr) -> dict[str, Any]:
|
|
504
|
+
if isinstance(node, ast.BoolOp):
|
|
505
|
+
cond = self._boolop(node, span_of(node, self.filename), as_cond=True)
|
|
506
|
+
else:
|
|
507
|
+
cond = self._expr(node)
|
|
508
|
+
kind = cond["type"]["kind"]
|
|
509
|
+
if kind == "bool":
|
|
510
|
+
return cond
|
|
511
|
+
if kind in _TRUTHY:
|
|
512
|
+
return self._truthy(cond, span_of(node, self.filename))
|
|
513
|
+
self.fail(node, "PS2004", "condition must be a bool, not a truthy value")
|
|
514
|
+
raise AssertionError("unreachable")
|
|
515
|
+
|
|
516
|
+
def _expect(self, node: ast.AST, got: dict[str, Any], want: dict[str, Any], what: str) -> None:
|
|
517
|
+
if not types_equal(got, want):
|
|
518
|
+
self.fail(node, "PS1003", f"{what} is {_show(got)}, annotation says {_show(want)}")
|
|
519
|
+
|
|
520
|
+
def _use_status(self, module: str) -> str:
|
|
521
|
+
if module == "nativeprobe" and os.environ.get("PYSCRIPTC_NATIVEPROBE") == "1":
|
|
522
|
+
return "lowered"
|
|
523
|
+
status = module_status(module)
|
|
524
|
+
if status == "lowered":
|
|
525
|
+
return "lowered"
|
|
526
|
+
if status == "queued":
|
|
527
|
+
return "queued"
|
|
528
|
+
parts = module.split(".")
|
|
529
|
+
for index in range(len(parts) - 1, 0, -1):
|
|
530
|
+
parent = ".".join(parts[:index])
|
|
531
|
+
parent_status = module_status(parent)
|
|
532
|
+
if parent_status == "queued":
|
|
533
|
+
return "queued"
|
|
534
|
+
if parent_status == "lowered":
|
|
535
|
+
return "missing"
|
|
536
|
+
return "missing"
|
|
537
|
+
|
|
538
|
+
def _finish_resolve(self, dotted: str, module: str, member: str, status: str) -> tuple[str, str, str, str]:
|
|
539
|
+
if status != "missing":
|
|
540
|
+
return dotted, module, member, status
|
|
541
|
+
parts = module.split(".")
|
|
542
|
+
best = 0
|
|
543
|
+
for index in range(1, len(parts) + 1):
|
|
544
|
+
parent = ".".join(parts[:index])
|
|
545
|
+
if module_status(parent) == "lowered":
|
|
546
|
+
best = index
|
|
547
|
+
if best == 0:
|
|
548
|
+
return dotted, module, member, status
|
|
549
|
+
parent = ".".join(parts[:best])
|
|
550
|
+
rest = parts[best:]
|
|
551
|
+
if member:
|
|
552
|
+
rest.append(member)
|
|
553
|
+
joined = ".".join(rest)
|
|
554
|
+
if not joined:
|
|
555
|
+
return dotted, parent, "", "value"
|
|
556
|
+
if member_callable(parent, joined) or module_constant(parent, joined) is not None:
|
|
557
|
+
return dotted, parent, joined, "lowered"
|
|
558
|
+
return dotted, parent, joined, "missing"
|
|
559
|
+
|
|
560
|
+
def _set_display(self, node: ast.Set, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
|
|
561
|
+
if not node.elts:
|
|
562
|
+
self.fail(node, "PS1001", "an empty set needs a set annotation")
|
|
563
|
+
if any(isinstance(elt, ast.Starred) for elt in node.elts):
|
|
564
|
+
self.fail(node, "PS2002", "set unpacking is not lowered")
|
|
565
|
+
elem_expect = expect["elem"] if expect and expect.get("set") else None
|
|
566
|
+
first = self._expr(node.elts[0], elem_expect)
|
|
567
|
+
elem = first["type"]
|
|
568
|
+
if elem["kind"] not in {"int", "str"}:
|
|
569
|
+
self.fail(node, "PS2002", "set elements are int or str")
|
|
570
|
+
typ = {"kind": "list", "elem": elem, "set": True}
|
|
571
|
+
if expect and expect.get("set"):
|
|
572
|
+
self._expect(node, typ, expect, "set")
|
|
573
|
+
dest = self._temp("set")
|
|
574
|
+
self.locals[dest] = typ
|
|
575
|
+
self.prelude.append(_assign(dest, {"kind": "list", "elems": [], "type": typ, "span": span}, span))
|
|
576
|
+
self.prelude.extend(self._set_insert_stmts(dest, typ, first, span))
|
|
577
|
+
for elt in node.elts[1:]:
|
|
578
|
+
value = self._expr(elt, elem)
|
|
579
|
+
if not types_equal(value["type"], elem):
|
|
580
|
+
self.fail(node, "PS1003", "set elements have one type")
|
|
581
|
+
self.prelude.extend(self._set_insert_stmts(dest, typ, value, span))
|
|
582
|
+
return _name(dest, typ, span)
|
|
583
|
+
|
|
584
|
+
def _listcomp(self, node: ast.ListComp, span: dict[str, Any], expect: dict[str, Any] | None) -> dict[str, Any]:
|
|
585
|
+
if any(gen.is_async for gen in node.generators):
|
|
586
|
+
self.fail(node, "PS2002", "async comprehensions are not lowered")
|
|
587
|
+
if any(gen.ifs and index < len(node.generators) - 1 for index, gen in enumerate(node.generators)):
|
|
588
|
+
self.fail(node, "PS2002", "an if before the for is not lowered")
|
|
589
|
+
if len(node.generators) != 1:
|
|
590
|
+
self.fail(node, "PS2002", "list comprehensions have one for")
|
|
591
|
+
gen = node.generators[0]
|
|
592
|
+
if len(gen.ifs) > 1:
|
|
593
|
+
self.fail(node, "PS2002", "list comprehensions have one if")
|
|
594
|
+
if not isinstance(gen.target, ast.Name):
|
|
595
|
+
self.fail(node, "PS2002", "for targets a single name")
|
|
596
|
+
dest = self._temp("comp")
|
|
597
|
+
saved = (
|
|
598
|
+
self.comp_elt,
|
|
599
|
+
self.comp_dest,
|
|
600
|
+
self.comp_init,
|
|
601
|
+
self.comp_expect,
|
|
602
|
+
self.comp_ready,
|
|
603
|
+
self.comp_if,
|
|
604
|
+
)
|
|
605
|
+
self.comp_elt = node.elt
|
|
606
|
+
self.comp_dest = dest
|
|
607
|
+
self.comp_init = None
|
|
608
|
+
self.comp_expect = expect
|
|
609
|
+
self.comp_ready = False
|
|
610
|
+
self.comp_if = gen.ifs[0] if gen.ifs else None
|
|
611
|
+
loop = ast.For(target=gen.target, iter=gen.iter, body=[ast.Pass()], orelse=[], type_comment=None)
|
|
612
|
+
ast.copy_location(loop, node)
|
|
613
|
+
try:
|
|
614
|
+
lowered = self._for(loop, span)
|
|
615
|
+
finally:
|
|
616
|
+
init = self.comp_init
|
|
617
|
+
self.comp_elt, self.comp_dest, self.comp_init, self.comp_expect, self.comp_ready, self.comp_if = saved
|
|
618
|
+
if init is None:
|
|
619
|
+
self.fail(node, "PS2002", "list comprehension is not lowered")
|
|
620
|
+
self.prelude.append(init)
|
|
621
|
+
self.prelude.extend(lowered)
|
|
622
|
+
return _name(dest, self.locals[dest], span)
|
|
623
|
+
|
|
624
|
+
def _store_constant(self, name: str, value: dict[str, Any], node: ast.AST) -> None:
|
|
625
|
+
if name in self.bindings or name in self.declared or name in self.constants or name in self.type_aliases:
|
|
626
|
+
self.fail(node, "PS2002", f"{name} is already bound")
|
|
627
|
+
self.constants[name] = value
|
|
628
|
+
self.declared.add(name)
|
|
629
|
+
|
|
630
|
+
def _store_tuple_constant(self, name: str, node: ast.Tuple, stmt: ast.AST) -> None:
|
|
631
|
+
if len(node.elts) < 2:
|
|
632
|
+
self.fail(node, "PS2002", "tuples need at least two fields")
|
|
633
|
+
elems: list[dict[str, Any]] = []
|
|
634
|
+
for elt in node.elts:
|
|
635
|
+
value = self._expr(elt)
|
|
636
|
+
if value["kind"] != "const":
|
|
637
|
+
self.fail(stmt, "PS2002", "a module assignment must be a constant")
|
|
638
|
+
elems.append(value)
|
|
639
|
+
self._tuple_type([elem["type"] for elem in elems], node)
|
|
640
|
+
self._store_constant(name, {"kind": "tuple-const", "elems": elems}, stmt)
|
|
641
|
+
|
|
642
|
+
def _activate_slot(self, name: str, node: ast.AST) -> None:
|
|
643
|
+
slot = self.global_slots[name]
|
|
644
|
+
if slot.get("conflict") or slot.get("type") is None:
|
|
645
|
+
self.fail(node, "PS1001", f"{name} needs a type")
|
|
646
|
+
if name in self.bindings or name in self.declared or name in self.constants or name in self.type_aliases:
|
|
647
|
+
self.fail(node, "PS2002", f"{name} is already bound")
|
|
648
|
+
slot["active"] = True
|
|
649
|
+
self.declared.add(name)
|
|
650
|
+
|
|
651
|
+
def _assign_global(self, stmt: ast.Assign, target: str, span: dict[str, Any]) -> list[dict[str, Any]]:
|
|
652
|
+
slot = self.global_slots.get(target)
|
|
653
|
+
if slot is None or not slot.get("active"):
|
|
654
|
+
self.fail(stmt, "PS2002", f"global {target} is not a module slot")
|
|
655
|
+
value = self._expr(stmt.value, slot["type"])
|
|
656
|
+
self._expect(stmt, value["type"], slot["type"], target)
|
|
657
|
+
return [self._global_set(target, value, span)]
|
|
658
|
+
|
|
659
|
+
def _slot_key(self, name: str) -> str:
|
|
660
|
+
if self.module_name:
|
|
661
|
+
return f"{self.module_name}.{name}"
|
|
662
|
+
return name
|
|
663
|
+
|
|
664
|
+
def _global_get(self, name: str, span: dict[str, Any]) -> dict[str, Any]:
|
|
665
|
+
slot = self.global_slots[name]
|
|
666
|
+
typ = slot["type"]
|
|
667
|
+
key = _text_ir(self._slot_key(name), "str", span)
|
|
668
|
+
if typ["kind"] == "str":
|
|
669
|
+
return self._abi_call("builtins", "global_get", [key], typ, "ps_global_get_str", span)
|
|
670
|
+
if typ["kind"] == "int":
|
|
671
|
+
return self._abi_call("builtins", "global_get", [key], typ, "ps_global_get_i64", span)
|
|
672
|
+
if typ["kind"] == "class" and typ.get("name") == "Path":
|
|
673
|
+
return self._abi_call("builtins", "global_get", [key], typ, "ps_global_get_path", span)
|
|
674
|
+
self.fail(ast.Pass(), "PS2002", f"global {name} of {_show(typ)} is not lowered")
|
|
675
|
+
raise AssertionError("unreachable")
|
|
676
|
+
|
|
677
|
+
def _global_set(self, name: str, value: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
|
|
678
|
+
typ = value["type"]
|
|
679
|
+
key = _text_ir(self._slot_key(name), "str", span)
|
|
680
|
+
if typ["kind"] == "str":
|
|
681
|
+
symbol = "ps_global_set_str"
|
|
682
|
+
elif typ["kind"] == "int":
|
|
683
|
+
symbol = "ps_global_set_i64"
|
|
684
|
+
elif typ["kind"] == "class" and typ.get("name") == "Path":
|
|
685
|
+
symbol = "ps_global_set_path"
|
|
686
|
+
else:
|
|
687
|
+
self.fail(ast.Pass(), "PS2002", f"global {name} of {_show(typ)} is not lowered")
|
|
688
|
+
return {
|
|
689
|
+
"kind": "expr",
|
|
690
|
+
"expr": self._abi_call("builtins", "global_set", [key, value], {"kind": "none"}, symbol, span),
|
|
691
|
+
"span": span,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
def _global_is_set(self, name: str, span: dict[str, Any]) -> dict[str, Any]:
|
|
695
|
+
key = _text_ir(self._slot_key(name), "str", span)
|
|
696
|
+
return self._abi_call("builtins", "global_is_set", [key], {"kind": "bool"}, "ps_global_is_set", span)
|
|
697
|
+
|
|
698
|
+
def _is_none(self, node: ast.Compare, span: dict[str, Any], negated: bool) -> dict[str, Any]:
|
|
699
|
+
left, right = node.left, node.comparators[0]
|
|
700
|
+
# A process slot's `is None` is the unset flag. Every other None literal
|
|
701
|
+
# is the existing == / != compare against none.
|
|
702
|
+
if _is_none_expr(right):
|
|
703
|
+
name = self._slot_read_name(left)
|
|
704
|
+
if name is not None:
|
|
705
|
+
return self._slot_is_unset(name, span, negated)
|
|
706
|
+
return self._compare_none(node, self._expr(left), negated, span, none_on_left=False)
|
|
707
|
+
if _is_none_expr(left):
|
|
708
|
+
name = self._slot_read_name(right)
|
|
709
|
+
if name is not None:
|
|
710
|
+
return self._slot_is_unset(name, span, negated)
|
|
711
|
+
return self._compare_none(node, self._expr(right), negated, span, none_on_left=True)
|
|
712
|
+
self.fail(node, "PS2004", "is is not lowered")
|
|
713
|
+
raise AssertionError("unreachable")
|
|
714
|
+
|
|
715
|
+
def _slot_is_unset(self, name: str, span: dict[str, Any], negated: bool) -> dict[str, Any]:
|
|
716
|
+
flag = self._global_is_set(name, span)
|
|
717
|
+
if negated:
|
|
718
|
+
return flag
|
|
719
|
+
return {"kind": "unary", "op": "not", "operand": flag, "type": {"kind": "bool"}, "span": span}
|
|
720
|
+
|
|
721
|
+
def _compare_none(
|
|
722
|
+
self,
|
|
723
|
+
node: ast.AST,
|
|
724
|
+
value: dict[str, Any],
|
|
725
|
+
negated: bool,
|
|
726
|
+
span: dict[str, Any],
|
|
727
|
+
*,
|
|
728
|
+
none_on_left: bool,
|
|
729
|
+
) -> dict[str, Any]:
|
|
730
|
+
kind = value["type"]["kind"]
|
|
731
|
+
if kind not in {"none", "int", "float", "bool", "str", "bytes"}:
|
|
732
|
+
self.fail(node, "PS2004", f"{_show(value['type'])} cannot compare with None")
|
|
733
|
+
none = _const_none(span)
|
|
734
|
+
op = "!=" if negated else "=="
|
|
735
|
+
left, right = (none, value) if none_on_left else (value, none)
|
|
736
|
+
return _compare(op, left, right, span)
|
|
737
|
+
|
|
738
|
+
def _slot_read_name(self, node: ast.expr) -> str | None:
|
|
739
|
+
if not isinstance(node, ast.Name) or node.id in self.locals:
|
|
740
|
+
return None
|
|
741
|
+
slot = self.global_slots.get(node.id)
|
|
742
|
+
if slot is None or not slot.get("active"):
|
|
743
|
+
return None
|
|
744
|
+
return node.id
|
|
745
|
+
|
|
746
|
+
def _materialize_tuple(
|
|
747
|
+
self, elems: list[dict[str, Any]], span: dict[str, Any], node: ast.AST | None = None
|
|
748
|
+
) -> dict[str, Any]:
|
|
749
|
+
typ = self._tuple_type([elem["type"] for elem in elems], node or ast.Pass())
|
|
750
|
+
temp = self._temp("tuple")
|
|
751
|
+
self.locals[temp] = typ
|
|
752
|
+
self.prelude.extend(_tuple_assign(temp, typ, elems, span))
|
|
753
|
+
return _name(temp, typ, span)
|
|
754
|
+
|
|
755
|
+
def _live_statements(
|
|
756
|
+
self,
|
|
757
|
+
statements: list[ast.stmt],
|
|
758
|
+
fences: list[Fence],
|
|
759
|
+
rebound: set[str] | None = None,
|
|
760
|
+
) -> list[ast.stmt]:
|
|
761
|
+
# Names assigned by user code. A later `if TYPE_CHECKING` is the typing
|
|
762
|
+
# constant only while that name is still the erased import.
|
|
763
|
+
if rebound is None:
|
|
764
|
+
rebound = set()
|
|
765
|
+
live: list[ast.stmt] = []
|
|
766
|
+
for stmt in statements:
|
|
767
|
+
if isinstance(stmt, ast.Import):
|
|
768
|
+
self._bind_import(stmt, fences)
|
|
769
|
+
live.append(stmt)
|
|
770
|
+
_release_erased_import(stmt, rebound)
|
|
771
|
+
continue
|
|
772
|
+
if isinstance(stmt, ast.ImportFrom):
|
|
773
|
+
self._bind_from(stmt, fences)
|
|
774
|
+
live.append(stmt)
|
|
775
|
+
_release_erased_import(stmt, rebound)
|
|
776
|
+
continue
|
|
777
|
+
if _is_main_guard(stmt):
|
|
778
|
+
# C3: the canonical guard's body is live script text. It joins
|
|
779
|
+
# the entry in source order; an `else` under a `__name__` test
|
|
780
|
+
# never runs as `__main__`, so it stays dropped.
|
|
781
|
+
live.extend(self._live_statements(list(stmt.body), fences, rebound))
|
|
782
|
+
continue
|
|
783
|
+
if _is_docstring(stmt):
|
|
784
|
+
continue
|
|
785
|
+
if isinstance(stmt, ast.If):
|
|
786
|
+
taken = self._static_branch(stmt, rebound)
|
|
787
|
+
if taken is None:
|
|
788
|
+
live.append(stmt)
|
|
789
|
+
rebound.update(_module_assigned(stmt))
|
|
790
|
+
continue
|
|
791
|
+
# The other branch is not lowered. Its imports and annotations stay unread.
|
|
792
|
+
live.extend(self._live_statements(taken, fences, rebound))
|
|
793
|
+
continue
|
|
794
|
+
live.append(stmt)
|
|
795
|
+
rebound.update(_module_assigned(stmt))
|
|
796
|
+
return live
|
|
797
|
+
|
|
798
|
+
def _static_branch(self, stmt: ast.If, rebound: set[str]) -> list[ast.stmt] | None:
|
|
799
|
+
held = self._version_holds(stmt.test)
|
|
800
|
+
if held is None:
|
|
801
|
+
held = self._type_checking_value(stmt.test, rebound)
|
|
802
|
+
if held is None:
|
|
803
|
+
return None
|
|
804
|
+
return list(stmt.body if held else stmt.orelse)
|
|
805
|
+
|
|
806
|
+
def _type_checking_value(self, node: ast.expr, rebound: set[str]) -> bool | None:
|
|
807
|
+
"""typing.TYPE_CHECKING is False. `not TYPE_CHECKING` is True. Anything else is not this fold."""
|
|
808
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
809
|
+
inner = self._type_checking_value(node.operand, rebound)
|
|
810
|
+
if inner is None:
|
|
811
|
+
return None
|
|
812
|
+
return not inner
|
|
813
|
+
if isinstance(node, ast.Name):
|
|
814
|
+
if node.id in rebound:
|
|
815
|
+
return None
|
|
816
|
+
if self.type_aliases.get(node.id) == "TYPE_CHECKING":
|
|
817
|
+
return False
|
|
818
|
+
return None
|
|
819
|
+
if (
|
|
820
|
+
isinstance(node, ast.Attribute)
|
|
821
|
+
and node.attr == "TYPE_CHECKING"
|
|
822
|
+
and isinstance(node.value, ast.Name)
|
|
823
|
+
and node.value.id not in rebound
|
|
824
|
+
):
|
|
825
|
+
binding = self.bindings.get(node.value.id)
|
|
826
|
+
if binding is not None and binding.kind == "erased" and binding.module in _ERASED_MODULES:
|
|
827
|
+
return False
|
|
828
|
+
return None
|
|
829
|
+
|
|
830
|
+
def _version_holds(self, test: ast.expr) -> bool | None:
|
|
831
|
+
if not isinstance(test, ast.Compare) or len(test.ops) != 1 or len(test.comparators) != 1:
|
|
832
|
+
return None
|
|
833
|
+
op = _CMP.get(type(test.ops[0]))
|
|
834
|
+
if op is None:
|
|
835
|
+
return None
|
|
836
|
+
left = self._version_operand(test.left)
|
|
837
|
+
right = self._version_operand(test.comparators[0])
|
|
838
|
+
if left == "version" and isinstance(right, tuple):
|
|
839
|
+
return _cmp_version(op, right)
|
|
840
|
+
if right == "version" and isinstance(left, tuple):
|
|
841
|
+
flipped = {">=": "<=", "<=": ">=", ">": "<", "<": ">", "==": "==", "!=": "!="}[op]
|
|
842
|
+
return _cmp_version(flipped, left)
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
def _version_operand(self, node: ast.expr) -> str | tuple[int, ...] | None:
|
|
846
|
+
if self._is_version_info(node):
|
|
847
|
+
return "version"
|
|
848
|
+
if isinstance(node, ast.Tuple) and node.elts and all(_is_int_const(elt) for elt in node.elts):
|
|
849
|
+
return tuple(int(elt.value) for elt in node.elts if isinstance(elt, ast.Constant))
|
|
850
|
+
return None
|
|
851
|
+
|
|
852
|
+
def _is_version_info(self, node: ast.expr) -> bool:
|
|
853
|
+
if not isinstance(node, ast.Attribute) or node.attr != "version_info" or not isinstance(node.value, ast.Name):
|
|
854
|
+
return False
|
|
855
|
+
binding = self.bindings.get(node.value.id)
|
|
856
|
+
return (
|
|
857
|
+
binding is not None
|
|
858
|
+
and binding.kind == "module"
|
|
859
|
+
and binding.module == "sys"
|
|
860
|
+
and not binding.rooted
|
|
861
|
+
and not binding.queued
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
def _globals_for(self, body: list[ast.stmt]) -> frozenset[str]:
|
|
865
|
+
"""`global`-declared names for one function body, walked once.
|
|
866
|
+
|
|
867
|
+
`_scan_global_slots` and the per-function lowering pass see the same
|
|
868
|
+
body objects, so the result is cached by body-list identity. Bodies
|
|
869
|
+
are immutable after parsing, so identity is a stable key within one
|
|
870
|
+
`lower_file` run; the cache lives on the `Lowerer` and dies with it.
|
|
871
|
+
"""
|
|
872
|
+
key = id(body)
|
|
873
|
+
cached = self._globals_cache.get(key)
|
|
874
|
+
if cached is None:
|
|
875
|
+
cached = frozenset(_declared_globals(list(body)))
|
|
876
|
+
self._globals_cache[key] = cached
|
|
877
|
+
return cached
|
|
878
|
+
|
|
879
|
+
def _scan_global_slots(self, statements: list[ast.stmt]) -> dict[str, dict[str, Any]]:
|
|
880
|
+
slots: dict[str, dict[str, Any]] = {}
|
|
881
|
+
for stmt in statements:
|
|
882
|
+
if not isinstance(stmt, ast.FunctionDef):
|
|
883
|
+
continue
|
|
884
|
+
declared: set[str] = set()
|
|
885
|
+
assigns: list[ast.Assign] = []
|
|
886
|
+
for node in ast.walk(stmt):
|
|
887
|
+
if isinstance(node, ast.Global):
|
|
888
|
+
declared.update(node.names)
|
|
889
|
+
elif isinstance(node, ast.Assign):
|
|
890
|
+
assigns.append(node)
|
|
891
|
+
for name in declared:
|
|
892
|
+
slots.setdefault(name, {"type": None, "active": False})
|
|
893
|
+
# Prime the lowering-time cache: `declared` equals
|
|
894
|
+
# `_declared_globals(list(stmt.body))` because a `global` statement
|
|
895
|
+
# can only appear in a body, and this walk covers the same body.
|
|
896
|
+
self._globals_cache[id(stmt.body)] = frozenset(declared)
|
|
897
|
+
for node in assigns:
|
|
898
|
+
if len(node.targets) != 1:
|
|
899
|
+
continue
|
|
900
|
+
target = node.targets[0]
|
|
901
|
+
if not isinstance(target, ast.Name) or target.id not in declared:
|
|
902
|
+
continue
|
|
903
|
+
typ = self._infer_slot_type(node.value)
|
|
904
|
+
if typ is None:
|
|
905
|
+
continue
|
|
906
|
+
existing = slots[target.id]["type"]
|
|
907
|
+
if existing is not None and not types_equal(existing, typ):
|
|
908
|
+
slots[target.id]["conflict"] = True
|
|
909
|
+
else:
|
|
910
|
+
slots[target.id]["type"] = typ
|
|
911
|
+
return slots
|
|
912
|
+
|
|
913
|
+
def _infer_slot_type(self, node: ast.expr) -> dict[str, Any] | None:
|
|
914
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
915
|
+
return {"kind": "str"}
|
|
916
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool):
|
|
917
|
+
return {"kind": "int"}
|
|
918
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "str":
|
|
919
|
+
return {"kind": "str"}
|
|
920
|
+
if self._is_mod(node, "importlib.resources", "as_file"):
|
|
921
|
+
return {"kind": "class", "name": "Path"}
|
|
922
|
+
return None
|
|
923
|
+
|
|
924
|
+
def _temp(self, hint: str) -> str:
|
|
925
|
+
self.temp_n += 1
|
|
926
|
+
return f".ps_{hint}_{self.temp_n}"
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def lower_source(source: str, filename: str = "<stdin>") -> dict[str, Any]:
|
|
930
|
+
tree = ast.parse(source, filename=filename)
|
|
931
|
+
return Lowerer(filename, source).module(tree)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def lower_file(path: str) -> tuple[dict[str, Any], str]:
|
|
935
|
+
text = open(path, encoding="utf-8").read()
|
|
936
|
+
return lower_source(text, path), text
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def render_fences(module: dict[str, Any], source: str) -> str:
|
|
940
|
+
parts: list[str] = []
|
|
941
|
+
for fence in module["fences"]:
|
|
942
|
+
text = source
|
|
943
|
+
path = fence.get("span", {}).get("file")
|
|
944
|
+
if path and path != module.get("sourceFile"):
|
|
945
|
+
try:
|
|
946
|
+
text = open(path, encoding="utf-8").read()
|
|
947
|
+
except OSError:
|
|
948
|
+
text = source
|
|
949
|
+
parts.append(format_diagnostic(fence, text))
|
|
950
|
+
return "\n".join(parts)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _assign(target: str, value: dict[str, Any], span: dict[str, Any]) -> dict[str, Any]:
|
|
954
|
+
return {"kind": "assign", "target": target, "value": value, "span": span}
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _const_str_join(parts: list[dict[str, Any]], span: dict[str, Any]) -> dict[str, Any] | None:
|
|
958
|
+
"""Module-level f-string pieces that are already str constants are one str constant."""
|
|
959
|
+
|
|
960
|
+
if any(part.get("kind") != "const" or part.get("type", {}).get("kind") != "str" for part in parts):
|
|
961
|
+
return None
|
|
962
|
+
text = "".join(str(part["value"]) for part in parts)
|
|
963
|
+
return {"kind": "const", "value": text, "type": {"kind": "str"}, "span": span}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _concat_str(parts: list[dict[str, Any]], span: dict[str, Any]) -> dict[str, Any]:
|
|
967
|
+
if not parts:
|
|
968
|
+
return {"kind": "const", "value": "", "type": {"kind": "str"}, "span": span}
|
|
969
|
+
acc = parts[0]
|
|
970
|
+
for part in parts[1:]:
|
|
971
|
+
acc = {"kind": "binary", "op": "+", "left": acc, "right": part, "type": {"kind": "str"}, "span": span}
|
|
972
|
+
return acc
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _percent_pieces(fmt: str) -> list[tuple[str, str]] | None:
|
|
976
|
+
pieces: list[tuple[str, str]] = []
|
|
977
|
+
buf: list[str] = []
|
|
978
|
+
index = 0
|
|
979
|
+
size = len(fmt)
|
|
980
|
+
while index < size:
|
|
981
|
+
if fmt[index] != "%":
|
|
982
|
+
buf.append(fmt[index])
|
|
983
|
+
index += 1
|
|
984
|
+
continue
|
|
985
|
+
if index + 1 >= size:
|
|
986
|
+
return None
|
|
987
|
+
nxt = fmt[index + 1]
|
|
988
|
+
if nxt == "%":
|
|
989
|
+
buf.append("%")
|
|
990
|
+
index += 2
|
|
991
|
+
continue
|
|
992
|
+
if nxt not in {"s", "d"}:
|
|
993
|
+
return None
|
|
994
|
+
pieces.append(("lit", "".join(buf)))
|
|
995
|
+
buf = []
|
|
996
|
+
pieces.append((nxt, ""))
|
|
997
|
+
index += 2
|
|
998
|
+
pieces.append(("lit", "".join(buf)))
|
|
999
|
+
return pieces
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _const_list_concat(left: dict[str, Any], right: dict[str, Any], span: dict[str, Any]) -> dict[str, Any] | None:
|
|
1003
|
+
"""Two constant lists of one element type are one list. A runtime list stays fenced."""
|
|
1004
|
+
|
|
1005
|
+
if left.get("kind") != "list" or right.get("kind") != "list":
|
|
1006
|
+
return None
|
|
1007
|
+
if left["type"]["kind"] != "list" or right["type"]["kind"] != "list":
|
|
1008
|
+
return None
|
|
1009
|
+
if left["type"].get("set") or right["type"].get("set"):
|
|
1010
|
+
return None
|
|
1011
|
+
if not types_equal(left["type"]["elem"], right["type"]["elem"]):
|
|
1012
|
+
return None
|
|
1013
|
+
return {
|
|
1014
|
+
"kind": "list",
|
|
1015
|
+
"elems": [*left["elems"], *right["elems"]],
|
|
1016
|
+
"type": {"kind": "list", "elem": left["type"]["elem"]},
|
|
1017
|
+
"span": span,
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _static_int(expr: dict[str, Any]) -> int | None:
|
|
1022
|
+
if expr.get("kind") == "const" and expr.get("type", {}).get("kind") == "int":
|
|
1023
|
+
value = expr.get("value")
|
|
1024
|
+
if isinstance(value, str) and _int_text(value):
|
|
1025
|
+
return int(value)
|
|
1026
|
+
if expr.get("kind") == "unary" and expr.get("op") == "-" and isinstance(expr.get("operand"), dict):
|
|
1027
|
+
inner = _static_int(expr["operand"])
|
|
1028
|
+
if inner is not None:
|
|
1029
|
+
return -inner
|
|
1030
|
+
return None
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _int_text(value: str) -> bool:
|
|
1034
|
+
body = value[1:] if value.startswith("-") else value
|
|
1035
|
+
return bool(body) and body.isdigit()
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _text_ir(value: str, kind: str, span: dict[str, Any]) -> dict[str, Any]:
|
|
1039
|
+
return {"kind": "const", "value": value, "type": {"kind": kind}, "span": span}
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _is_int_const(node: ast.expr) -> bool:
|
|
1043
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def _cmp_version(op: str, parts: tuple[int, ...]) -> bool:
|
|
1047
|
+
current = sys.version_info
|
|
1048
|
+
if op == ">=":
|
|
1049
|
+
return current >= parts
|
|
1050
|
+
if op == ">":
|
|
1051
|
+
return current > parts
|
|
1052
|
+
if op == "<=":
|
|
1053
|
+
return current <= parts
|
|
1054
|
+
if op == "<":
|
|
1055
|
+
return current < parts
|
|
1056
|
+
if op == "==":
|
|
1057
|
+
return current == parts
|
|
1058
|
+
if op == "!=":
|
|
1059
|
+
return current != parts
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _plain_const_kind(node: ast.expr) -> str | None:
|
|
1064
|
+
"""A str constant or an int constant. Bool is not an int here."""
|
|
1065
|
+
if not isinstance(node, ast.Constant) or isinstance(node.value, bool):
|
|
1066
|
+
return None
|
|
1067
|
+
if isinstance(node.value, str):
|
|
1068
|
+
return "str"
|
|
1069
|
+
if isinstance(node.value, int):
|
|
1070
|
+
return "int"
|
|
1071
|
+
return None
|