pyir 0.1.0__tar.gz
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.
- pyir-0.1.0/LICENSE +21 -0
- pyir-0.1.0/PKG-INFO +251 -0
- pyir-0.1.0/README.md +226 -0
- pyir-0.1.0/pyproject.toml +43 -0
- pyir-0.1.0/setup.cfg +4 -0
- pyir-0.1.0/src/pyir/__init__.py +78 -0
- pyir-0.1.0/src/pyir/__main__.py +47 -0
- pyir-0.1.0/src/pyir/errors.py +38 -0
- pyir-0.1.0/src/pyir/interp.py +110 -0
- pyir-0.1.0/src/pyir/ir.py +412 -0
- pyir-0.1.0/src/pyir/lower.py +523 -0
- pyir-0.1.0/src/pyir/passes.py +390 -0
- pyir-0.1.0/src/pyir/printer.py +32 -0
- pyir-0.1.0/src/pyir/py.typed +0 -0
- pyir-0.1.0/src/pyir.egg-info/PKG-INFO +251 -0
- pyir-0.1.0/src/pyir.egg-info/SOURCES.txt +21 -0
- pyir-0.1.0/src/pyir.egg-info/dependency_links.txt +1 -0
- pyir-0.1.0/src/pyir.egg-info/entry_points.txt +2 -0
- pyir-0.1.0/src/pyir.egg-info/top_level.txt +1 -0
- pyir-0.1.0/tests/test_interp_and_printer.py +154 -0
- pyir-0.1.0/tests/test_lowering.py +176 -0
- pyir-0.1.0/tests/test_passes.py +260 -0
- pyir-0.1.0/tests/test_semantics.py +128 -0
pyir-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nehz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyir-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyir
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python Intermediate Representation: lower a Python subset to basic-block IR, optimise it and interpret it.
|
|
5
|
+
Author: nehz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: compiler,ir,intermediate representation,ast,optimization,interpreter,education
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
19
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# pyir — Python Intermediate Representation
|
|
27
|
+
|
|
28
|
+
`pyir` turns ordinary Python functions into a small, readable compiler IR: three-address code organised into basic blocks with an explicit control-flow graph. You can print it, optimise it and run it. It is meant for learning, teaching and experimenting with compiler ideas while staying in Python, without LLVM or a C toolchain.
|
|
29
|
+
|
|
30
|
+
Every stage is plain Python with no dependencies. The front end uses the stdlib `ast` module. The IR is a handful of dataclasses. The passes are textbook dataflow analyses. An interpreter executes the IR, so you can check a transformation by comparing its result against CPython running the original source.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Front end**: lowers a useful subset of Python via `ast`. Supported: functions, local variables, `if`/`elif`/`else`, `while`, `for x in range(...)`, `break`/`continue`, `return`, tuple assignment (`a, b = b, a % b`), augmented assignment, arithmetic, bitwise and comparison operators (including chained comparisons), short-circuit `and`/`or` with Python's operand-returning semantics, conditional expressions, recursion and calls between functions, and a few builtins (`abs`, `min`, `max`, `int`, `float`, `bool`, `str`, `round`, `print`).
|
|
35
|
+
- **IR**: three-address code (`x = a + b`, `branch c, L1, L2`, `jump L`, `return v`, `call f(...)`) in basic blocks, plus helpers for predecessors, reachability (reverse post-order) and structural verification.
|
|
36
|
+
- **Pretty-printer**: a stable, diff-friendly text format.
|
|
37
|
+
- **Optimisation passes**:
|
|
38
|
+
- `constant_fold`: global constant propagation and folding, which also turns branches on known conditions into jumps.
|
|
39
|
+
- `eliminate_dead_code`: removes unreachable blocks and unused assignments, driven by liveness analysis.
|
|
40
|
+
- `simplify_cfg`: threads jumps through empty blocks and merges straight-line block chains.
|
|
41
|
+
- `optimize`: runs the passes to a fixed point.
|
|
42
|
+
- **Interpreter**: executes IR with a step budget. Python exceptions (`ZeroDivisionError`, ...) propagate unchanged, so results can be compared with CPython one-to-one.
|
|
43
|
+
- **CLI**: `python -m pyir file.py [-O] [--run FUNC ARG...]`.
|
|
44
|
+
- Standard library only. Typed (`py.typed`). Python 3.10+.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pyir # once published
|
|
50
|
+
# or, from a checkout:
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import pyir
|
|
58
|
+
|
|
59
|
+
source = '''
|
|
60
|
+
def clamp_sum(n):
|
|
61
|
+
limit = 4 * 25
|
|
62
|
+
debug = False
|
|
63
|
+
total = 0
|
|
64
|
+
for i in range(n):
|
|
65
|
+
if debug:
|
|
66
|
+
print(i)
|
|
67
|
+
total += i
|
|
68
|
+
return total if total < limit else limit
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
module = pyir.compile_source(source) # Python source -> IR Module
|
|
72
|
+
print(pyir.format_module(module)) # or simply print(module)
|
|
73
|
+
|
|
74
|
+
pyir.optimize(module) # constant folding + DCE + CFG cleanup, in place
|
|
75
|
+
print(module)
|
|
76
|
+
|
|
77
|
+
assert pyir.interpret(module, "clamp_sum", 10) == 45
|
|
78
|
+
assert pyir.interpret(module, "clamp_sum", 100) == 100
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can also lower a live function, as long as its source is available to `inspect`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def triple_plus_one(x):
|
|
85
|
+
return 3 * x + 1
|
|
86
|
+
|
|
87
|
+
module = pyir.compile_function(triple_plus_one)
|
|
88
|
+
pyir.interpret(module, "triple_plus_one", 4) # 13
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
From the command line:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pyir examples.py # print IR
|
|
95
|
+
python -m pyir -O examples.py # print optimised IR
|
|
96
|
+
python -m pyir examples.py --run gcd 48 18
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What the IR looks like
|
|
100
|
+
|
|
101
|
+
`clamp_sum` above, straight out of the front end. Python locals keep their names and compiler temporaries are written `%tN`. The `for` loop runs on a hidden counter (`%t2`), so reassigning `i` in the body cannot change the iteration.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
function clamp_sum(n):
|
|
105
|
+
entry:
|
|
106
|
+
limit = 4 * 25
|
|
107
|
+
debug = False
|
|
108
|
+
total = 0
|
|
109
|
+
%t2 = 0
|
|
110
|
+
%t3 = n
|
|
111
|
+
jump for.cond.1
|
|
112
|
+
for.cond.1:
|
|
113
|
+
%t4 = %t2 < %t3
|
|
114
|
+
branch %t4, for.body.2, for.end.4
|
|
115
|
+
for.body.2:
|
|
116
|
+
i = %t2
|
|
117
|
+
branch debug, if.then.5, if.end.6
|
|
118
|
+
if.then.5:
|
|
119
|
+
%t5 = call print(i)
|
|
120
|
+
jump if.end.6
|
|
121
|
+
if.end.6:
|
|
122
|
+
total = total + i
|
|
123
|
+
jump for.step.3
|
|
124
|
+
for.step.3:
|
|
125
|
+
%t2 = %t2 + 1
|
|
126
|
+
jump for.cond.1
|
|
127
|
+
for.end.4:
|
|
128
|
+
%t7 = total < limit
|
|
129
|
+
branch %t7, ifexp.then.7, ifexp.else.8
|
|
130
|
+
ifexp.then.7:
|
|
131
|
+
%t6 = total
|
|
132
|
+
jump ifexp.end.9
|
|
133
|
+
ifexp.else.8:
|
|
134
|
+
%t6 = limit
|
|
135
|
+
jump ifexp.end.9
|
|
136
|
+
ifexp.end.9:
|
|
137
|
+
return %t6
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
After `pyir.optimize(module)`, `limit` is folded to `100` and propagated. The `if debug:` branch is folded, which leaves the `print` call unreachable, so it is deleted. The dead `limit`/`debug` assignments are removed and the loop body's blocks are merged:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
function clamp_sum(n):
|
|
144
|
+
entry:
|
|
145
|
+
total = 0
|
|
146
|
+
%t2 = 0
|
|
147
|
+
%t3 = n
|
|
148
|
+
jump for.cond.1
|
|
149
|
+
for.cond.1:
|
|
150
|
+
%t4 = %t2 < %t3
|
|
151
|
+
branch %t4, for.body.2, for.end.4
|
|
152
|
+
for.body.2:
|
|
153
|
+
i = %t2
|
|
154
|
+
total = total + i
|
|
155
|
+
%t2 = %t2 + 1
|
|
156
|
+
jump for.cond.1
|
|
157
|
+
for.end.4:
|
|
158
|
+
%t7 = total < 100
|
|
159
|
+
branch %t7, ifexp.then.7, ifexp.else.8
|
|
160
|
+
ifexp.then.7:
|
|
161
|
+
%t6 = total
|
|
162
|
+
jump ifexp.end.9
|
|
163
|
+
ifexp.else.8:
|
|
164
|
+
%t6 = 100
|
|
165
|
+
jump ifexp.end.9
|
|
166
|
+
ifexp.end.9:
|
|
167
|
+
return %t6
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The IR is three-address code, not SSA: a variable may be assigned in several places, as `%t6` is above. The passes use classic dataflow analyses that do not depend on single assignment.
|
|
171
|
+
|
|
172
|
+
## API overview
|
|
173
|
+
|
|
174
|
+
Everything below can be imported from the top-level `pyir` package.
|
|
175
|
+
|
|
176
|
+
### Front end (`pyir.lower`)
|
|
177
|
+
|
|
178
|
+
| Name | Description |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `compile_source(source: str) -> Module` | Parse source (top-level `def`s only, plus an optional module docstring) and lower every function. Raises `UnsupportedSyntaxError` for constructs outside the subset. |
|
|
181
|
+
| `compile_function(fn) -> Module` | Lower one live function via `inspect.getsource`. It may call itself and builtins. |
|
|
182
|
+
|
|
183
|
+
### IR (`pyir.ir`)
|
|
184
|
+
|
|
185
|
+
| Name | Description |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `Module(functions: dict[str, Function])` | Container. Supports `module["name"]`, `in`, iteration over functions, `.verify()` and `str()`. |
|
|
188
|
+
| `Function(name, params, blocks)` | `blocks` is an ordered `dict[str, BasicBlock]`, and the first block is the entry. Provides `.entry`, `.predecessors()`, `.reachable()` (labels in reverse post-order), `.instruction_count()`, `.verify()` and `str()`. |
|
|
189
|
+
| `BasicBlock(label, instructions, terminator)` | Provides `.successors()`. |
|
|
190
|
+
| `Const(value)`, `Var(name)` | Operands. `Const` equality is type-aware: `Const(1) != Const(True)`. `Var.is_temp` is true for `%tN` temporaries. |
|
|
191
|
+
| `Copy(dest, src)` | `dest = src` |
|
|
192
|
+
| `BinOp(dest, op, left, right)` | `op` is one of `+ - * / // % ** << >> & \| ^ == != < <= > >= is` or `is not` |
|
|
193
|
+
| `UnaryOp(dest, op, operand)` | `op` is one of `- + ~ not` |
|
|
194
|
+
| `Call(dest, func, args)` | Calls a module function, or else an entry of `BUILTINS` |
|
|
195
|
+
| `Jump(target)`, `Branch(cond, if_true, if_false)`, `Return(value)` | Terminators |
|
|
196
|
+
| `BUILTINS` | Name-to-callable map of the builtins that IR may call |
|
|
197
|
+
|
|
198
|
+
Each instruction and terminator has `.uses()`, which lists the operands it reads.
|
|
199
|
+
|
|
200
|
+
### Printing (`pyir.printer`)
|
|
201
|
+
|
|
202
|
+
`format_module(module)`, `format_function(function)` and `format_block(block)` each return a `str`.
|
|
203
|
+
|
|
204
|
+
### Optimisation (`pyir.passes`)
|
|
205
|
+
|
|
206
|
+
| Name | Description |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `constant_fold(fn) -> bool` | Constant propagation and folding, plus constant-branch folding |
|
|
209
|
+
| `eliminate_dead_code(fn) -> bool` | Removes unreachable blocks and dead assignments |
|
|
210
|
+
| `simplify_cfg(fn) -> bool` | Jump threading, `branch c, L, L` → `jump L`, and block merging |
|
|
211
|
+
| `optimize(target, passes=None, max_rounds=10)` | Runs `passes` (default `DEFAULT_PASSES`) on a `Module` or `Function` until nothing changes. Works in place and returns `target`. |
|
|
212
|
+
| `DEFAULT_PASSES` | `(constant_fold, eliminate_dead_code, simplify_cfg)` |
|
|
213
|
+
|
|
214
|
+
A pass is any callable `Function -> bool` that mutates the function in place and returns whether it changed anything.
|
|
215
|
+
|
|
216
|
+
### Execution (`pyir.interp`)
|
|
217
|
+
|
|
218
|
+
| Name | Description |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `interpret(module, name, *args, max_steps=10_000_000)` | Run a function and return its result |
|
|
221
|
+
| `Interpreter(module, max_steps=...)` | `.run(name, *args)`. `.steps` holds the number of instructions executed by the last run. |
|
|
222
|
+
|
|
223
|
+
### Errors (`pyir.errors`)
|
|
224
|
+
|
|
225
|
+
`PyIRError` is the base class. Its subclasses are:
|
|
226
|
+
|
|
227
|
+
- `UnsupportedSyntaxError` (has `.lineno`)
|
|
228
|
+
- `IRVerificationError`
|
|
229
|
+
- `IRRuntimeError`: unknown function, variable read before assignment, or step budget exhausted
|
|
230
|
+
|
|
231
|
+
## Semantics notes and limitations
|
|
232
|
+
|
|
233
|
+
- Values are dynamically typed Python objects. Operators are executed with the `operator` module, so the IR's semantics are exactly CPython's for the supported operations.
|
|
234
|
+
- Constant folding never folds an operation that raises (for example `1 // 0`). It also leaves alone `is` comparisons between objects other than `None`/`True`/`False`, and results that would be huge (very large ints or long strings).
|
|
235
|
+
- Dead code elimination always keeps calls. It also keeps arithmetic that commonly fails on ordinary numbers: division or modulo by a non-constant or zero, and shifts or `**` with a non-constant or negative right operand. Other dead arithmetic is assumed not to raise. For example, `unused = a + b` is removed even if `a` and `b` happen to have incompatible types.
|
|
236
|
+
- Reading a local before it is assigned raises `IRRuntimeError`, where CPython would raise `UnboundLocalError`.
|
|
237
|
+
- Not supported: classes, closures, lambdas, globals, containers and subscripts, attributes, keyword arguments, `try`, `with`, and `for` loops over anything but `range` (whose optional step must be an integer literal).
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
243
|
+
# or, with pytest installed:
|
|
244
|
+
python3 -m pytest
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The test suite runs a set of sample programs and 300 randomly generated programs three ways (CPython, IR, optimised IR) and checks that results and exception types agree.
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
pyir-0.1.0/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# pyir — Python Intermediate Representation
|
|
2
|
+
|
|
3
|
+
`pyir` turns ordinary Python functions into a small, readable compiler IR: three-address code organised into basic blocks with an explicit control-flow graph. You can print it, optimise it and run it. It is meant for learning, teaching and experimenting with compiler ideas while staying in Python, without LLVM or a C toolchain.
|
|
4
|
+
|
|
5
|
+
Every stage is plain Python with no dependencies. The front end uses the stdlib `ast` module. The IR is a handful of dataclasses. The passes are textbook dataflow analyses. An interpreter executes the IR, so you can check a transformation by comparing its result against CPython running the original source.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Front end**: lowers a useful subset of Python via `ast`. Supported: functions, local variables, `if`/`elif`/`else`, `while`, `for x in range(...)`, `break`/`continue`, `return`, tuple assignment (`a, b = b, a % b`), augmented assignment, arithmetic, bitwise and comparison operators (including chained comparisons), short-circuit `and`/`or` with Python's operand-returning semantics, conditional expressions, recursion and calls between functions, and a few builtins (`abs`, `min`, `max`, `int`, `float`, `bool`, `str`, `round`, `print`).
|
|
10
|
+
- **IR**: three-address code (`x = a + b`, `branch c, L1, L2`, `jump L`, `return v`, `call f(...)`) in basic blocks, plus helpers for predecessors, reachability (reverse post-order) and structural verification.
|
|
11
|
+
- **Pretty-printer**: a stable, diff-friendly text format.
|
|
12
|
+
- **Optimisation passes**:
|
|
13
|
+
- `constant_fold`: global constant propagation and folding, which also turns branches on known conditions into jumps.
|
|
14
|
+
- `eliminate_dead_code`: removes unreachable blocks and unused assignments, driven by liveness analysis.
|
|
15
|
+
- `simplify_cfg`: threads jumps through empty blocks and merges straight-line block chains.
|
|
16
|
+
- `optimize`: runs the passes to a fixed point.
|
|
17
|
+
- **Interpreter**: executes IR with a step budget. Python exceptions (`ZeroDivisionError`, ...) propagate unchanged, so results can be compared with CPython one-to-one.
|
|
18
|
+
- **CLI**: `python -m pyir file.py [-O] [--run FUNC ARG...]`.
|
|
19
|
+
- Standard library only. Typed (`py.typed`). Python 3.10+.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install pyir # once published
|
|
25
|
+
# or, from a checkout:
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import pyir
|
|
33
|
+
|
|
34
|
+
source = '''
|
|
35
|
+
def clamp_sum(n):
|
|
36
|
+
limit = 4 * 25
|
|
37
|
+
debug = False
|
|
38
|
+
total = 0
|
|
39
|
+
for i in range(n):
|
|
40
|
+
if debug:
|
|
41
|
+
print(i)
|
|
42
|
+
total += i
|
|
43
|
+
return total if total < limit else limit
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
module = pyir.compile_source(source) # Python source -> IR Module
|
|
47
|
+
print(pyir.format_module(module)) # or simply print(module)
|
|
48
|
+
|
|
49
|
+
pyir.optimize(module) # constant folding + DCE + CFG cleanup, in place
|
|
50
|
+
print(module)
|
|
51
|
+
|
|
52
|
+
assert pyir.interpret(module, "clamp_sum", 10) == 45
|
|
53
|
+
assert pyir.interpret(module, "clamp_sum", 100) == 100
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can also lower a live function, as long as its source is available to `inspect`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def triple_plus_one(x):
|
|
60
|
+
return 3 * x + 1
|
|
61
|
+
|
|
62
|
+
module = pyir.compile_function(triple_plus_one)
|
|
63
|
+
pyir.interpret(module, "triple_plus_one", 4) # 13
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
From the command line:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pyir examples.py # print IR
|
|
70
|
+
python -m pyir -O examples.py # print optimised IR
|
|
71
|
+
python -m pyir examples.py --run gcd 48 18
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What the IR looks like
|
|
75
|
+
|
|
76
|
+
`clamp_sum` above, straight out of the front end. Python locals keep their names and compiler temporaries are written `%tN`. The `for` loop runs on a hidden counter (`%t2`), so reassigning `i` in the body cannot change the iteration.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
function clamp_sum(n):
|
|
80
|
+
entry:
|
|
81
|
+
limit = 4 * 25
|
|
82
|
+
debug = False
|
|
83
|
+
total = 0
|
|
84
|
+
%t2 = 0
|
|
85
|
+
%t3 = n
|
|
86
|
+
jump for.cond.1
|
|
87
|
+
for.cond.1:
|
|
88
|
+
%t4 = %t2 < %t3
|
|
89
|
+
branch %t4, for.body.2, for.end.4
|
|
90
|
+
for.body.2:
|
|
91
|
+
i = %t2
|
|
92
|
+
branch debug, if.then.5, if.end.6
|
|
93
|
+
if.then.5:
|
|
94
|
+
%t5 = call print(i)
|
|
95
|
+
jump if.end.6
|
|
96
|
+
if.end.6:
|
|
97
|
+
total = total + i
|
|
98
|
+
jump for.step.3
|
|
99
|
+
for.step.3:
|
|
100
|
+
%t2 = %t2 + 1
|
|
101
|
+
jump for.cond.1
|
|
102
|
+
for.end.4:
|
|
103
|
+
%t7 = total < limit
|
|
104
|
+
branch %t7, ifexp.then.7, ifexp.else.8
|
|
105
|
+
ifexp.then.7:
|
|
106
|
+
%t6 = total
|
|
107
|
+
jump ifexp.end.9
|
|
108
|
+
ifexp.else.8:
|
|
109
|
+
%t6 = limit
|
|
110
|
+
jump ifexp.end.9
|
|
111
|
+
ifexp.end.9:
|
|
112
|
+
return %t6
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
After `pyir.optimize(module)`, `limit` is folded to `100` and propagated. The `if debug:` branch is folded, which leaves the `print` call unreachable, so it is deleted. The dead `limit`/`debug` assignments are removed and the loop body's blocks are merged:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
function clamp_sum(n):
|
|
119
|
+
entry:
|
|
120
|
+
total = 0
|
|
121
|
+
%t2 = 0
|
|
122
|
+
%t3 = n
|
|
123
|
+
jump for.cond.1
|
|
124
|
+
for.cond.1:
|
|
125
|
+
%t4 = %t2 < %t3
|
|
126
|
+
branch %t4, for.body.2, for.end.4
|
|
127
|
+
for.body.2:
|
|
128
|
+
i = %t2
|
|
129
|
+
total = total + i
|
|
130
|
+
%t2 = %t2 + 1
|
|
131
|
+
jump for.cond.1
|
|
132
|
+
for.end.4:
|
|
133
|
+
%t7 = total < 100
|
|
134
|
+
branch %t7, ifexp.then.7, ifexp.else.8
|
|
135
|
+
ifexp.then.7:
|
|
136
|
+
%t6 = total
|
|
137
|
+
jump ifexp.end.9
|
|
138
|
+
ifexp.else.8:
|
|
139
|
+
%t6 = 100
|
|
140
|
+
jump ifexp.end.9
|
|
141
|
+
ifexp.end.9:
|
|
142
|
+
return %t6
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The IR is three-address code, not SSA: a variable may be assigned in several places, as `%t6` is above. The passes use classic dataflow analyses that do not depend on single assignment.
|
|
146
|
+
|
|
147
|
+
## API overview
|
|
148
|
+
|
|
149
|
+
Everything below can be imported from the top-level `pyir` package.
|
|
150
|
+
|
|
151
|
+
### Front end (`pyir.lower`)
|
|
152
|
+
|
|
153
|
+
| Name | Description |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `compile_source(source: str) -> Module` | Parse source (top-level `def`s only, plus an optional module docstring) and lower every function. Raises `UnsupportedSyntaxError` for constructs outside the subset. |
|
|
156
|
+
| `compile_function(fn) -> Module` | Lower one live function via `inspect.getsource`. It may call itself and builtins. |
|
|
157
|
+
|
|
158
|
+
### IR (`pyir.ir`)
|
|
159
|
+
|
|
160
|
+
| Name | Description |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `Module(functions: dict[str, Function])` | Container. Supports `module["name"]`, `in`, iteration over functions, `.verify()` and `str()`. |
|
|
163
|
+
| `Function(name, params, blocks)` | `blocks` is an ordered `dict[str, BasicBlock]`, and the first block is the entry. Provides `.entry`, `.predecessors()`, `.reachable()` (labels in reverse post-order), `.instruction_count()`, `.verify()` and `str()`. |
|
|
164
|
+
| `BasicBlock(label, instructions, terminator)` | Provides `.successors()`. |
|
|
165
|
+
| `Const(value)`, `Var(name)` | Operands. `Const` equality is type-aware: `Const(1) != Const(True)`. `Var.is_temp` is true for `%tN` temporaries. |
|
|
166
|
+
| `Copy(dest, src)` | `dest = src` |
|
|
167
|
+
| `BinOp(dest, op, left, right)` | `op` is one of `+ - * / // % ** << >> & \| ^ == != < <= > >= is` or `is not` |
|
|
168
|
+
| `UnaryOp(dest, op, operand)` | `op` is one of `- + ~ not` |
|
|
169
|
+
| `Call(dest, func, args)` | Calls a module function, or else an entry of `BUILTINS` |
|
|
170
|
+
| `Jump(target)`, `Branch(cond, if_true, if_false)`, `Return(value)` | Terminators |
|
|
171
|
+
| `BUILTINS` | Name-to-callable map of the builtins that IR may call |
|
|
172
|
+
|
|
173
|
+
Each instruction and terminator has `.uses()`, which lists the operands it reads.
|
|
174
|
+
|
|
175
|
+
### Printing (`pyir.printer`)
|
|
176
|
+
|
|
177
|
+
`format_module(module)`, `format_function(function)` and `format_block(block)` each return a `str`.
|
|
178
|
+
|
|
179
|
+
### Optimisation (`pyir.passes`)
|
|
180
|
+
|
|
181
|
+
| Name | Description |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `constant_fold(fn) -> bool` | Constant propagation and folding, plus constant-branch folding |
|
|
184
|
+
| `eliminate_dead_code(fn) -> bool` | Removes unreachable blocks and dead assignments |
|
|
185
|
+
| `simplify_cfg(fn) -> bool` | Jump threading, `branch c, L, L` → `jump L`, and block merging |
|
|
186
|
+
| `optimize(target, passes=None, max_rounds=10)` | Runs `passes` (default `DEFAULT_PASSES`) on a `Module` or `Function` until nothing changes. Works in place and returns `target`. |
|
|
187
|
+
| `DEFAULT_PASSES` | `(constant_fold, eliminate_dead_code, simplify_cfg)` |
|
|
188
|
+
|
|
189
|
+
A pass is any callable `Function -> bool` that mutates the function in place and returns whether it changed anything.
|
|
190
|
+
|
|
191
|
+
### Execution (`pyir.interp`)
|
|
192
|
+
|
|
193
|
+
| Name | Description |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `interpret(module, name, *args, max_steps=10_000_000)` | Run a function and return its result |
|
|
196
|
+
| `Interpreter(module, max_steps=...)` | `.run(name, *args)`. `.steps` holds the number of instructions executed by the last run. |
|
|
197
|
+
|
|
198
|
+
### Errors (`pyir.errors`)
|
|
199
|
+
|
|
200
|
+
`PyIRError` is the base class. Its subclasses are:
|
|
201
|
+
|
|
202
|
+
- `UnsupportedSyntaxError` (has `.lineno`)
|
|
203
|
+
- `IRVerificationError`
|
|
204
|
+
- `IRRuntimeError`: unknown function, variable read before assignment, or step budget exhausted
|
|
205
|
+
|
|
206
|
+
## Semantics notes and limitations
|
|
207
|
+
|
|
208
|
+
- Values are dynamically typed Python objects. Operators are executed with the `operator` module, so the IR's semantics are exactly CPython's for the supported operations.
|
|
209
|
+
- Constant folding never folds an operation that raises (for example `1 // 0`). It also leaves alone `is` comparisons between objects other than `None`/`True`/`False`, and results that would be huge (very large ints or long strings).
|
|
210
|
+
- Dead code elimination always keeps calls. It also keeps arithmetic that commonly fails on ordinary numbers: division or modulo by a non-constant or zero, and shifts or `**` with a non-constant or negative right operand. Other dead arithmetic is assumed not to raise. For example, `unused = a + b` is removed even if `a` and `b` happen to have incompatible types.
|
|
211
|
+
- Reading a local before it is assigned raises `IRRuntimeError`, where CPython would raise `UnboundLocalError`.
|
|
212
|
+
- Not supported: classes, closures, lambdas, globals, containers and subscripts, attributes, keyword arguments, `try`, `with`, and `for` loops over anything but `range` (whose optional step must be an integer literal).
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
218
|
+
# or, with pytest installed:
|
|
219
|
+
python3 -m pytest
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The test suite runs a set of sample programs and 300 randomly generated programs three ways (CPython, IR, optimised IR) and checks that results and exception types agree.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyir"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python Intermediate Representation: lower a Python subset to basic-block IR, optimise it and interpret it."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "nehz" }]
|
|
14
|
+
keywords = ["compiler", "ir", "intermediate representation", "ast", "optimization", "interpreter", "education"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Compilers",
|
|
27
|
+
"Topic :: Software Development :: Interpreters",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
pyir = "pyir.__main__:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
pyir = ["py.typed"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
pythonpath = ["src", "tests"]
|
|
43
|
+
testpaths = ["tests"]
|
pyir-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""pyir - Python Intermediate Representation.
|
|
2
|
+
|
|
3
|
+
Lower a small, statically-tractable subset of Python into a three-address-code
|
|
4
|
+
IR organised in basic blocks, print it, optimise it and interpret it.
|
|
5
|
+
|
|
6
|
+
Typical use::
|
|
7
|
+
|
|
8
|
+
import pyir
|
|
9
|
+
|
|
10
|
+
module = pyir.compile_source(source)
|
|
11
|
+
print(pyir.format_module(module))
|
|
12
|
+
pyir.optimize(module)
|
|
13
|
+
result = pyir.interpret(module, "gcd", 48, 18)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .errors import IRRuntimeError, IRVerificationError, PyIRError, UnsupportedSyntaxError
|
|
19
|
+
from .interp import Interpreter, interpret
|
|
20
|
+
from .ir import (
|
|
21
|
+
BUILTINS,
|
|
22
|
+
BasicBlock,
|
|
23
|
+
BinOp,
|
|
24
|
+
Branch,
|
|
25
|
+
Call,
|
|
26
|
+
Const,
|
|
27
|
+
Copy,
|
|
28
|
+
Function,
|
|
29
|
+
Jump,
|
|
30
|
+
Module,
|
|
31
|
+
Return,
|
|
32
|
+
UnaryOp,
|
|
33
|
+
Var,
|
|
34
|
+
)
|
|
35
|
+
from .lower import compile_function, compile_source
|
|
36
|
+
from .passes import DEFAULT_PASSES, constant_fold, eliminate_dead_code, optimize, simplify_cfg
|
|
37
|
+
from .printer import format_block, format_function, format_module
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"__version__",
|
|
43
|
+
# front end
|
|
44
|
+
"compile_source",
|
|
45
|
+
"compile_function",
|
|
46
|
+
# IR
|
|
47
|
+
"Module",
|
|
48
|
+
"Function",
|
|
49
|
+
"BasicBlock",
|
|
50
|
+
"Const",
|
|
51
|
+
"Var",
|
|
52
|
+
"Copy",
|
|
53
|
+
"BinOp",
|
|
54
|
+
"UnaryOp",
|
|
55
|
+
"Call",
|
|
56
|
+
"Jump",
|
|
57
|
+
"Branch",
|
|
58
|
+
"Return",
|
|
59
|
+
"BUILTINS",
|
|
60
|
+
# printing
|
|
61
|
+
"format_module",
|
|
62
|
+
"format_function",
|
|
63
|
+
"format_block",
|
|
64
|
+
# optimisation
|
|
65
|
+
"optimize",
|
|
66
|
+
"constant_fold",
|
|
67
|
+
"eliminate_dead_code",
|
|
68
|
+
"simplify_cfg",
|
|
69
|
+
"DEFAULT_PASSES",
|
|
70
|
+
# execution
|
|
71
|
+
"interpret",
|
|
72
|
+
"Interpreter",
|
|
73
|
+
# errors
|
|
74
|
+
"PyIRError",
|
|
75
|
+
"UnsupportedSyntaxError",
|
|
76
|
+
"IRVerificationError",
|
|
77
|
+
"IRRuntimeError",
|
|
78
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Command line interface: ``python -m pyir FILE [-O] [--run FUNC ARG...]``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ast
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from .errors import PyIRError
|
|
11
|
+
from .interp import interpret
|
|
12
|
+
from .lower import compile_source
|
|
13
|
+
from .passes import optimize
|
|
14
|
+
from .printer import format_module
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
18
|
+
"""Entry point for ``python -m pyir`` and the ``pyir`` console script."""
|
|
19
|
+
parser = argparse.ArgumentParser(prog="pyir", description="Lower Python functions to pyir IR.")
|
|
20
|
+
parser.add_argument("file", help="Python source file containing only function definitions ('-' for stdin)")
|
|
21
|
+
parser.add_argument("-O", "--optimize", action="store_true", help="run the default optimisation passes")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--run",
|
|
24
|
+
nargs="+",
|
|
25
|
+
metavar=("FUNC", "ARG"),
|
|
26
|
+
help="interpret FUNC with literal arguments (e.g. --run gcd 48 18) instead of printing IR",
|
|
27
|
+
)
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
|
|
30
|
+
source = sys.stdin.read() if args.file == "-" else open(args.file, encoding="utf-8").read()
|
|
31
|
+
try:
|
|
32
|
+
module = compile_source(source)
|
|
33
|
+
if args.optimize:
|
|
34
|
+
optimize(module)
|
|
35
|
+
if args.run:
|
|
36
|
+
func, *raw = args.run
|
|
37
|
+
print(repr(interpret(module, func, *(ast.literal_eval(a) for a in raw))))
|
|
38
|
+
else:
|
|
39
|
+
print(format_module(module))
|
|
40
|
+
except (PyIRError, SyntaxError) as exc:
|
|
41
|
+
print(f"pyir: error: {exc}", file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
sys.exit(main())
|