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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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())