steelix 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.
Files changed (48) hide show
  1. steelix-0.1.0/LICENSE +28 -0
  2. steelix-0.1.0/PKG-INFO +148 -0
  3. steelix-0.1.0/README.md +137 -0
  4. steelix-0.1.0/pyproject.toml +50 -0
  5. steelix-0.1.0/pyproject.toml.orig +39 -0
  6. steelix-0.1.0/src/steelix/__init__.py +9 -0
  7. steelix-0.1.0/src/steelix/__main__.py +3 -0
  8. steelix-0.1.0/src/steelix/cli.py +430 -0
  9. steelix-0.1.0/src/steelix/coverage.py +226 -0
  10. steelix-0.1.0/src/steelix/diagnostics.py +43 -0
  11. steelix-0.1.0/src/steelix/dist.py +407 -0
  12. steelix-0.1.0/src/steelix/emit/__init__.py +1 -0
  13. steelix-0.1.0/src/steelix/emit/crate.py +493 -0
  14. steelix-0.1.0/src/steelix/frontend/__init__.py +8 -0
  15. steelix-0.1.0/src/steelix/frontend/bindings.py +1407 -0
  16. steelix-0.1.0/src/steelix/frontend/booleans.py +112 -0
  17. steelix-0.1.0/src/steelix/frontend/callgraph.py +79 -0
  18. steelix-0.1.0/src/steelix/frontend/classinfo.py +59 -0
  19. steelix-0.1.0/src/steelix/frontend/desugar.py +148 -0
  20. steelix-0.1.0/src/steelix/frontend/fallback.py +317 -0
  21. steelix-0.1.0/src/steelix/frontend/parse.py +54 -0
  22. steelix-0.1.0/src/steelix/frontend/regions.py +95 -0
  23. steelix-0.1.0/src/steelix/frontend/resolve.py +123 -0
  24. steelix-0.1.0/src/steelix/frontend/scope.py +476 -0
  25. steelix-0.1.0/src/steelix/frontend/subset.py +426 -0
  26. steelix-0.1.0/src/steelix/lower/__init__.py +9 -0
  27. steelix-0.1.0/src/steelix/lower/classes.py +236 -0
  28. steelix-0.1.0/src/steelix/lower/comprehension.py +425 -0
  29. steelix-0.1.0/src/steelix/lower/context.py +250 -0
  30. steelix-0.1.0/src/steelix/lower/docstrings.py +37 -0
  31. steelix-0.1.0/src/steelix/lower/expr.py +1261 -0
  32. steelix-0.1.0/src/steelix/lower/function.py +555 -0
  33. steelix-0.1.0/src/steelix/lower/handling.py +719 -0
  34. steelix-0.1.0/src/steelix/lower/module.py +320 -0
  35. steelix-0.1.0/src/steelix/lower/names.py +108 -0
  36. steelix-0.1.0/src/steelix/lower/rustflow.py +164 -0
  37. steelix-0.1.0/src/steelix/lower/stmt.py +813 -0
  38. steelix-0.1.0/src/steelix/lower/temps.py +194 -0
  39. steelix-0.1.0/src/steelix/project.py +471 -0
  40. steelix-0.1.0/src/steelix/rustast/__init__.py +7 -0
  41. steelix-0.1.0/src/steelix/rustast/cleanup.py +361 -0
  42. steelix-0.1.0/src/steelix/rustast/fmt.py +38 -0
  43. steelix-0.1.0/src/steelix/rustast/nodes.py +342 -0
  44. steelix-0.1.0/src/steelix/rustast/printer.py +387 -0
  45. steelix-0.1.0/src/steelix/rustast/pyo3.py +101 -0
  46. steelix-0.1.0/src/steelix/rustast/rewrite.py +59 -0
  47. steelix-0.1.0/src/steelix/rustast/rustfmt.toml +2 -0
  48. steelix-0.1.0/src/steelix/rustast/verify.py +438 -0
steelix-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, An Long
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
steelix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: steelix
3
+ Version: 0.1.0
4
+ Summary: Transpile Python into readable Rust code built on PyO3
5
+ Author: An Long
6
+ Author-email: An Long <aisk1988@gmail.com>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ # steelix
13
+
14
+ steelix transpiles Python into Rust code that calls libpython through [PyO3](https://pyo3.rs).
15
+
16
+ It is meant to help move Python projects to Rust. You transpile the project once and get a Rust program that behaves like the original. Semantics and error messages stay the same, the standard library and third party packages are still imported from Python, and tracebacks still point at the Python source. You can deploy that program right away, then replace its functions with ordinary Rust one at a time, keeping it working after every step.
17
+
18
+ It is a bit like [Nuitka](https://nuitka.net), but the output is Rust instead of C, and the goal is readability rather than speed. Nuitka's C is meant for a compiler; steelix's Rust is meant to be read and edited by people, and becomes the starting point of the port. It is plain PyO3 plus a small runtime crate.
19
+
20
+ ## Example
21
+
22
+ ```python
23
+ import json
24
+
25
+
26
+ class Item:
27
+ def __init__(self, name, price, qty=1):
28
+ self.name = name
29
+ self.price = price
30
+ self.qty = qty
31
+
32
+ def total(self):
33
+ return self.price * self.qty
34
+
35
+
36
+ def load(text):
37
+ try:
38
+ rows = json.loads(text)
39
+ except ValueError as e:
40
+ print(f"bad input: {e}")
41
+ return []
42
+ return [Item(**row) for row in rows if row["qty"] > 0]
43
+
44
+
45
+ items = load('[{"name": "pen", "price": 2, "qty": 3}]')
46
+ print(sum(item.total() for item in items))
47
+ ```
48
+
49
+ `steelix transpile inventory.py -o out/` writes a Cargo crate. An excerpt of `out/src/inventory.rs`:
50
+
51
+ ```rust
52
+ // inventory.py:14
53
+ pub fn load<'py>(m: &rt::Module<'py>, text: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
54
+ let py = m.py();
55
+ rt::frame!(m, "load", 14, |frame| {
56
+ let rows = match (|| m.global("json")?.method1("loads", (text,)))() {
57
+ Ok(value) => value,
58
+ Err(err) if err.is_instance_of::<PyValueError>(py) => {
59
+ let e = frame.catch(err);
60
+ let _handling = rt::Handling::new(&e);
61
+ rt::builtins::print(
62
+ py,
63
+ (rt::fstring(py, ("bad input: ", rt::format(&e, "")?))?,),
64
+ )?;
65
+ return Ok(PyList::empty(py).into_any());
66
+ }
67
+ Err(err) => return Err(err),
68
+ };
69
+ let list = PyList::empty(py);
70
+ for row in rows.try_iter()? {
71
+ let row = row?;
72
+ if row.get_item("qty")?.gt(0)? {
73
+ list.append(rt::call!(m.global("Item")?, **row)?)?;
74
+ }
75
+ py.check_signals()?;
76
+ }
77
+ Ok(list.into_any())
78
+ })
79
+ }
80
+
81
+ pub mod item {
82
+ use super::*;
83
+
84
+ // inventory.py:5
85
+ pub fn __init__<'py>(
86
+ m: &rt::Module<'py>,
87
+ slf: &Bound<'py, PyAny>,
88
+ name: &Bound<'py, PyAny>,
89
+ price: &Bound<'py, PyAny>,
90
+ qty: &Bound<'py, PyAny>,
91
+ ) -> PyResult<Bound<'py, PyAny>> {
92
+ rt::frame!(m, "Item.__init__", 5, |_| {
93
+ slf.set_attr("name", name)?;
94
+ slf.set_attr("price", price)?;
95
+ slf.set_attr("qty", qty)?;
96
+ Ok(rt::none(m.py()))
97
+ })
98
+ }
99
+
100
+ // inventory.py:10
101
+ pub fn total<'py>(m: &rt::Module<'py>, slf: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
102
+ rt::frame!(m, "Item.total", 10, |_| {
103
+ slf.attr("price")?.mul(slf.attr("qty")?)
104
+ })
105
+ }
106
+ }
107
+ ```
108
+
109
+ `cargo build` in `out/` produces an executable that embeds libpython and prints `6`, like `python inventory.py`. The class is a real Python class, `load` is a real Python function (`inspect.signature`, pickling and monkeypatching still work), and the generator expression is evaluated lazily as in CPython.
110
+
111
+ ## Usage
112
+
113
+ steelix needs Python 3.14 and a Rust toolchain. With [uv](https://docs.astral.sh/uv/):
114
+
115
+ ```sh
116
+ uv run steelix transpile app.py -o out/ # a crate for app.py and the project modules it imports
117
+ uv run steelix transpile -m pkg --root src/ -o out/ # the same for `python -m pkg`
118
+ uv run steelix build app.py # transpile, build and assemble a relocatable dist/app/
119
+ uv run steelix coverage app.py # how much of the code becomes Rust
120
+ ```
121
+
122
+ When building a generated crate yourself, point PyO3 at the interpreter whose packages the program uses: `PYO3_PYTHON=/path/to/venv/bin/python cargo build`.
123
+
124
+ `steelix build` bundles the Python runtime and the site-packages of the build environment next to the executable, so the result runs on a machine without Python (Linux, glibc at least as new as the build machine's).
125
+
126
+ ## Rewriting
127
+
128
+ Each generated function keeps its Python calling convention, so it can be rewritten on its own: keep the signature as an adapter, move the logic into plain Rust, convert the arguments at the boundary. The rest of the program, including Python code that calls the function, does not notice. [`examples/logsum`](examples/logsum) shows a small CLI before and after rewriting some of its functions.
129
+
130
+ Python containers can stay as they are at first (a `PyDict` is fine in Rust code), so the first step only changes the logic, not the data model.
131
+
132
+ ## What is missing
133
+
134
+ - **async/await.** A coroutine would have to become a hand written state machine, which is the opposite of readable. Async functions are kept as Python.
135
+ - **Generators** (`yield`) are kept as Python for the same reason. Generator expressions are supported.
136
+ - **`match`** statements and a few rarer constructs (custom metaclasses, `exec`/`eval` without a namespace, ...) are kept as Python.
137
+ - **Executables for Windows and macOS** are not supported yet, only Linux.
138
+ - **Free-threaded Python** is not supported.
139
+
140
+ "Kept as Python" means the statement or function stays Python source inside the generated Rust and runs in the same module globals, so any program CPython runs can be transpiled. `steelix transpile` and `steelix coverage` report what was kept and why.
141
+
142
+ ## Status
143
+
144
+ Early, but tested against CPython: every test case, six command line tools of the standard library, the standard library `json` package and a small Scheme interpreter run with the same output and the same tracebacks as under CPython, and 154 standard library modules transpile into crates that compile without warnings.
145
+
146
+ ## License
147
+
148
+ BSD 3-Clause, see [LICENSE](LICENSE).
@@ -0,0 +1,137 @@
1
+ # steelix
2
+
3
+ steelix transpiles Python into Rust code that calls libpython through [PyO3](https://pyo3.rs).
4
+
5
+ It is meant to help move Python projects to Rust. You transpile the project once and get a Rust program that behaves like the original. Semantics and error messages stay the same, the standard library and third party packages are still imported from Python, and tracebacks still point at the Python source. You can deploy that program right away, then replace its functions with ordinary Rust one at a time, keeping it working after every step.
6
+
7
+ It is a bit like [Nuitka](https://nuitka.net), but the output is Rust instead of C, and the goal is readability rather than speed. Nuitka's C is meant for a compiler; steelix's Rust is meant to be read and edited by people, and becomes the starting point of the port. It is plain PyO3 plus a small runtime crate.
8
+
9
+ ## Example
10
+
11
+ ```python
12
+ import json
13
+
14
+
15
+ class Item:
16
+ def __init__(self, name, price, qty=1):
17
+ self.name = name
18
+ self.price = price
19
+ self.qty = qty
20
+
21
+ def total(self):
22
+ return self.price * self.qty
23
+
24
+
25
+ def load(text):
26
+ try:
27
+ rows = json.loads(text)
28
+ except ValueError as e:
29
+ print(f"bad input: {e}")
30
+ return []
31
+ return [Item(**row) for row in rows if row["qty"] > 0]
32
+
33
+
34
+ items = load('[{"name": "pen", "price": 2, "qty": 3}]')
35
+ print(sum(item.total() for item in items))
36
+ ```
37
+
38
+ `steelix transpile inventory.py -o out/` writes a Cargo crate. An excerpt of `out/src/inventory.rs`:
39
+
40
+ ```rust
41
+ // inventory.py:14
42
+ pub fn load<'py>(m: &rt::Module<'py>, text: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
43
+ let py = m.py();
44
+ rt::frame!(m, "load", 14, |frame| {
45
+ let rows = match (|| m.global("json")?.method1("loads", (text,)))() {
46
+ Ok(value) => value,
47
+ Err(err) if err.is_instance_of::<PyValueError>(py) => {
48
+ let e = frame.catch(err);
49
+ let _handling = rt::Handling::new(&e);
50
+ rt::builtins::print(
51
+ py,
52
+ (rt::fstring(py, ("bad input: ", rt::format(&e, "")?))?,),
53
+ )?;
54
+ return Ok(PyList::empty(py).into_any());
55
+ }
56
+ Err(err) => return Err(err),
57
+ };
58
+ let list = PyList::empty(py);
59
+ for row in rows.try_iter()? {
60
+ let row = row?;
61
+ if row.get_item("qty")?.gt(0)? {
62
+ list.append(rt::call!(m.global("Item")?, **row)?)?;
63
+ }
64
+ py.check_signals()?;
65
+ }
66
+ Ok(list.into_any())
67
+ })
68
+ }
69
+
70
+ pub mod item {
71
+ use super::*;
72
+
73
+ // inventory.py:5
74
+ pub fn __init__<'py>(
75
+ m: &rt::Module<'py>,
76
+ slf: &Bound<'py, PyAny>,
77
+ name: &Bound<'py, PyAny>,
78
+ price: &Bound<'py, PyAny>,
79
+ qty: &Bound<'py, PyAny>,
80
+ ) -> PyResult<Bound<'py, PyAny>> {
81
+ rt::frame!(m, "Item.__init__", 5, |_| {
82
+ slf.set_attr("name", name)?;
83
+ slf.set_attr("price", price)?;
84
+ slf.set_attr("qty", qty)?;
85
+ Ok(rt::none(m.py()))
86
+ })
87
+ }
88
+
89
+ // inventory.py:10
90
+ pub fn total<'py>(m: &rt::Module<'py>, slf: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
91
+ rt::frame!(m, "Item.total", 10, |_| {
92
+ slf.attr("price")?.mul(slf.attr("qty")?)
93
+ })
94
+ }
95
+ }
96
+ ```
97
+
98
+ `cargo build` in `out/` produces an executable that embeds libpython and prints `6`, like `python inventory.py`. The class is a real Python class, `load` is a real Python function (`inspect.signature`, pickling and monkeypatching still work), and the generator expression is evaluated lazily as in CPython.
99
+
100
+ ## Usage
101
+
102
+ steelix needs Python 3.14 and a Rust toolchain. With [uv](https://docs.astral.sh/uv/):
103
+
104
+ ```sh
105
+ uv run steelix transpile app.py -o out/ # a crate for app.py and the project modules it imports
106
+ uv run steelix transpile -m pkg --root src/ -o out/ # the same for `python -m pkg`
107
+ uv run steelix build app.py # transpile, build and assemble a relocatable dist/app/
108
+ uv run steelix coverage app.py # how much of the code becomes Rust
109
+ ```
110
+
111
+ When building a generated crate yourself, point PyO3 at the interpreter whose packages the program uses: `PYO3_PYTHON=/path/to/venv/bin/python cargo build`.
112
+
113
+ `steelix build` bundles the Python runtime and the site-packages of the build environment next to the executable, so the result runs on a machine without Python (Linux, glibc at least as new as the build machine's).
114
+
115
+ ## Rewriting
116
+
117
+ Each generated function keeps its Python calling convention, so it can be rewritten on its own: keep the signature as an adapter, move the logic into plain Rust, convert the arguments at the boundary. The rest of the program, including Python code that calls the function, does not notice. [`examples/logsum`](examples/logsum) shows a small CLI before and after rewriting some of its functions.
118
+
119
+ Python containers can stay as they are at first (a `PyDict` is fine in Rust code), so the first step only changes the logic, not the data model.
120
+
121
+ ## What is missing
122
+
123
+ - **async/await.** A coroutine would have to become a hand written state machine, which is the opposite of readable. Async functions are kept as Python.
124
+ - **Generators** (`yield`) are kept as Python for the same reason. Generator expressions are supported.
125
+ - **`match`** statements and a few rarer constructs (custom metaclasses, `exec`/`eval` without a namespace, ...) are kept as Python.
126
+ - **Executables for Windows and macOS** are not supported yet, only Linux.
127
+ - **Free-threaded Python** is not supported.
128
+
129
+ "Kept as Python" means the statement or function stays Python source inside the generated Rust and runs in the same module globals, so any program CPython runs can be transpiled. `steelix transpile` and `steelix coverage` report what was kept and why.
130
+
131
+ ## Status
132
+
133
+ Early, but tested against CPython: every test case, six command line tools of the standard library, the standard library `json` package and a small Scheme interpreter run with the same output and the same tracebacks as under CPython, and 154 standard library modules transpile into crates that compile without warnings.
134
+
135
+ ## License
136
+
137
+ BSD 3-Clause, see [LICENSE](LICENSE).
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "steelix"
3
+ version = "0.1.0"
4
+ description = "Transpile Python into readable Rust code built on PyO3"
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.14"
9
+ dependencies = []
10
+
11
+ [[project.authors]]
12
+ name = "An Long"
13
+ email = "aisk1988@gmail.com"
14
+
15
+ [project.scripts]
16
+ steelix = "steelix:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.13.5,<0.14.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pytest>=9.2.0",
25
+ "ruff>=0.17.3",
26
+ ]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ markers = [
31
+ "e2e: end-to-end tests that build generated crates with cargo (slow)",
32
+ "cargo: tests that run cargo check on golden snapshots",
33
+ ]
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ extend-exclude = [
38
+ "tests/cases",
39
+ "tests/apps",
40
+ "tests/projects",
41
+ ]
42
+
43
+ [tool.ruff.lint]
44
+ select = [
45
+ "E",
46
+ "F",
47
+ "I",
48
+ "UP",
49
+ "B",
50
+ ]
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "steelix"
3
+ version = "0.1.0"
4
+ description = "Transpile Python into readable Rust code built on PyO3"
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "An Long", email = "aisk1988@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.14"
12
+ dependencies = []
13
+
14
+ [project.scripts]
15
+ steelix = "steelix:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.13.5,<0.14.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=9.2.0",
24
+ "ruff>=0.17.3",
25
+ ]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+ markers = [
30
+ "e2e: end-to-end tests that build generated crates with cargo (slow)",
31
+ "cargo: tests that run cargo check on golden snapshots",
32
+ ]
33
+
34
+ [tool.ruff]
35
+ line-length = 100
36
+ extend-exclude = ["tests/cases", "tests/apps", "tests/projects"]
37
+
38
+ [tool.ruff.lint]
39
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,9 @@
1
+ """Steelix: transpile Python source into readable Rust code built on PyO3."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+
6
+ def main() -> None:
7
+ from steelix.cli import main as cli_main
8
+
9
+ raise SystemExit(cli_main())
@@ -0,0 +1,3 @@
1
+ from steelix import main
2
+
3
+ main()