bend-python 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.
- bend_python-0.1.0/AGENTS.md +30 -0
- bend_python-0.1.0/CHANGELOG.md +37 -0
- bend_python-0.1.0/LICENSE +21 -0
- bend_python-0.1.0/MANIFEST.in +3 -0
- bend_python-0.1.0/PKG-INFO +262 -0
- bend_python-0.1.0/README.md +230 -0
- bend_python-0.1.0/bend/LAWS.bend +85 -0
- bend_python-0.1.0/bend/PROOF.bend +141 -0
- bend_python-0.1.0/bend/THREAD_LAWS.bend +199 -0
- bend_python-0.1.0/bend/THREAD_PROOF.bend +362 -0
- bend_python-0.1.0/bend/python.bend +218 -0
- bend_python-0.1.0/bend/python.c +817 -0
- bend_python-0.1.0/bend/thread_state.bend +225 -0
- bend_python-0.1.0/pyproject.toml +54 -0
- bend_python-0.1.0/scripts/bootstrap.sh +22 -0
- bend_python-0.1.0/setup.cfg +4 -0
- bend_python-0.1.0/setup.py +11 -0
- bend_python-0.1.0/src/bend_python/__init__.py +24 -0
- bend_python-0.1.0/src/bend_python/__main__.py +26 -0
- bend_python-0.1.0/src/bend_python/build.py +368 -0
- bend_python-0.1.0/src/bend_python/library.py +52 -0
- bend_python-0.1.0/src/bend_python.egg-info/PKG-INFO +262 -0
- bend_python-0.1.0/src/bend_python.egg-info/SOURCES.txt +26 -0
- bend_python-0.1.0/src/bend_python.egg-info/dependency_links.txt +1 -0
- bend_python-0.1.0/src/bend_python.egg-info/top_level.txt +1 -0
- bend_python-0.1.0/tests/test_bindings.py +684 -0
- bend_python-0.1.0/tests/test_build.py +640 -0
- bend_python-0.1.0/tests/test_release.py +61 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Bend/Python bindings
|
|
2
|
+
|
|
3
|
+
- Target Victor Taelin's **Bend 2**, pinned **2.0.28** (`.tools/bend/bin/bend` or `BEND`); never use Bend 1/HVM instructions.
|
|
4
|
+
- Linux x86_64, Clang 14+, CPython **3.10–3.14 and 3.14t**. Development venv: `bash scripts/bootstrap.sh`; `uv pip install --python .venv/bin/python --no-build-isolation -e .`.
|
|
5
|
+
- The SDK is a pure wheel; `examples/` is a separate project built with it. Build: `cd examples && CC=clang ../.venv/bin/python setup.py build_ext --inplace`. Test: `.venv/bin/python -m unittest discover -s tests`.
|
|
6
|
+
- `bend/{PROOF,THREAD_PROOF}.bend` and `examples/PROOF.bend` are build gates (`scripts/check-proofs.sh`). Lint: `uvx prek run --all-files` (ruff, hygiene, proofs). Docker/GitHub wheel recipe and usage are in README.
|
|
7
|
+
- Mutation-check every new law: break the code it covers and confirm the gate fails.
|
|
8
|
+
- Keep application logic, export adapters, and proved invariants in Bend. C owns CPython/runtime integration; Python owns build orchestration. Canonical `bend/` files are bundled in the SDK wheel; do not duplicate tracked copies (`examples/bend/` is an untracked vendored copy).
|
|
9
|
+
|
|
10
|
+
## Language reminders
|
|
11
|
+
|
|
12
|
+
- [Pinned guide](https://github.com/bendlang/bend/blob/v2.0.28/guide/GUIDE.md): pure affine dependent types; `+` permits reuse of Data. Closures remain affine.
|
|
13
|
+
- Binder quantities affect function types: adapt `square(+x:U32)` to `U32 -> U32` with `x => square(x)`.
|
|
14
|
+
- Templates `~f` must be closed and call only earlier templates. Export helpers specialize them into captureless callbacks.
|
|
15
|
+
- Imported constructors need the alias (`Python.PyCall`). Computed match scrutinees need a helper. Recursive arguments decrease left-to-right.
|
|
16
|
+
- `{==}` proves definitional equality; erased equality expressions may mention affine inputs repeatedly. Laws about pure parsers do not prove their C callers.
|
|
17
|
+
- [Foreign effects](https://github.com/bendlang/bend/blob/v2.0.28/guide/EFFECTS.md): C is spliced after the runtime; CID/FID names resolve relative to the importer. No stable ABI. Erased generic arguments never enter the C argument array.
|
|
18
|
+
|
|
19
|
+
## Runtime boundary
|
|
20
|
+
|
|
21
|
+
- [Pinned compiler](https://github.com/bendlang/bend/blob/v2.0.28/bend2/comp.ts): patched CPU globals live in exclusive per-invocation runtime contexts, selected through TLS. Captureless closures can cross contexts; captured environments are consumed.
|
|
22
|
+
- Generic `Call -> IO(Object)` callbacks preserve every Python object's identity/precision through per-invocation strong-reference arenas. Never retain/forge handles across calls.
|
|
23
|
+
- **Object wrappers may be packed TAG_PAK or heap TAG_CTR**; both must unbox correctly. This matters after Bend copies/reboxes handles.
|
|
24
|
+
- Handles are sealed per invocation (`bp_object`/`bp_get`); never pack or accept a raw arena index. Bend has no private constructors, so sealing is what rejects forged `PyObject{n}`.
|
|
25
|
+
- Detach for the short idle-cache lock and optionally during pure work (always on free-threaded builds); reattach before Python effects/refcounts. Concurrent and reentrant calls own separate contexts. No Python API inside native evaluation. Each call uses one worker; upstream pool entry is rejected.
|
|
26
|
+
- Free-threaded Python needs attachment plus thread-safe APIs, not a GIL. C boundaries check attachment; traditional builds check GIL ownership too. Main interpreter only, including checks on every call.
|
|
27
|
+
- THREAD_PROOF proves attachment and context-lease models, **not C correspondence or CPython**. Lease laws assume distinct allocations and atomic cache operations; do not claim end-to-end GIL/refcount verification.
|
|
28
|
+
- `_patch_runtime` zero-initializes forwarded registers, so it also disables `preserve_none`: together they exhaust registers on Clang 19+ for wide segments. The example's `reverse5` keeps a wide segment under CI's Clang.
|
|
29
|
+
- Avoid Bend's signal-installing pool_stack/io_loop. Patched err_fail raises a Python exception and discards that runtime instance; unrelated calls remain usable. Ordinary Python exceptions only abort their invocation.
|
|
30
|
+
- Native faults still follow host signal handling. Each context reserves about 8 GiB heap + 2 GiB stack virtually, not resident RAM. Cache at most eight idle contexts whose heap stayed within 32 MiB; unmap failed/large/excess contexts. Never cap active leases: callbacks can nest.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `bend-python` are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/). A release is published to PyPI only
|
|
6
|
+
when its version has a section below.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `BendExtension` and `BendBuildExt`: setuptools integration that proves a
|
|
15
|
+
program's laws and the library's own, generates C with the pinned Bend 2.0.28
|
|
16
|
+
compiler, and compiles it with Clang. Failures end in one `error:` line.
|
|
17
|
+
- Automatic, checksum-verified download of the pinned Bend compiler (`BEND`
|
|
18
|
+
overrides it; `BEND_PYTHON_DOWNLOAD=0` disables it), and automatic vendoring
|
|
19
|
+
of the Bend library when a program imports a missing copy.
|
|
20
|
+
- `python -m bend_python vendor` to copy the Bend library by hand.
|
|
21
|
+
- The Bend `Python` interface: generic `export` of `Call -> IO(Object)`; typed
|
|
22
|
+
`export_u32`, `export_f32`, `export_bool`, `export_string` and
|
|
23
|
+
`export_binary_u32`; conversions (`to_u32`/`from_u32`, `to_f32`/`from_f32`,
|
|
24
|
+
`to_bool`/`from_bool`, `to_string`/`from_string`, `from_nat`,
|
|
25
|
+
`to_bytes`/`from_bytes`, `truthy`); and Python operations (`get_item`,
|
|
26
|
+
`set_item`, `getattr`, `len`, `tuple`, `list`, `dict`, `call`, `invoke`,
|
|
27
|
+
`builtins`, `construct`, `import_module`).
|
|
28
|
+
- Concurrent calls in isolated runtime instances, with a per-export GIL policy;
|
|
29
|
+
free-threaded CPython 3.14t never enables the GIL.
|
|
30
|
+
- Sealed object handles detect accidental fabrication or reuse from another
|
|
31
|
+
call probabilistically, raising `ValueError` on an invalid decoded handle.
|
|
32
|
+
- Proved models of the thread-attachment and runtime-lease protocol, and laws
|
|
33
|
+
for the typed adapters' argument checks.
|
|
34
|
+
- Linux x86_64 support for CPython 3.10–3.14 and 3.14t.
|
|
35
|
+
|
|
36
|
+
[Unreleased]: https://github.com/lucaswiman/bend-python/compare/v0.1.0...HEAD
|
|
37
|
+
[0.1.0]: https://github.com/lucaswiman/bend-python/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucas Wiman
|
|
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.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bend-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write CPython extensions in Bend 2, with laws proved at build time
|
|
5
|
+
Author: Lucas Wiman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lucaswiman/bend-python
|
|
8
|
+
Project-URL: Source, https://github.com/lucaswiman/bend-python
|
|
9
|
+
Project-URL: Issues, https://github.com/lucaswiman/bend-python/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/lucaswiman/bend-python/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: bend,extension,native,setuptools,proof,dependent types
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Setuptools Plugin
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: C
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
27
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# bend-python
|
|
34
|
+
|
|
35
|
+
Write CPython extensions in [Bend 2](https://github.com/bendlang/bend), a pure,
|
|
36
|
+
dependently typed language that compiles to native code, and state laws about
|
|
37
|
+
your code that the build **proves before it compiles**. A false law is a build
|
|
38
|
+
error, not a failing test.
|
|
39
|
+
|
|
40
|
+
```bend
|
|
41
|
+
# fast.bend
|
|
42
|
+
import Base
|
|
43
|
+
import ./bend/python.bend as Python
|
|
44
|
+
|
|
45
|
+
def square(+x: U32) -> U32:
|
|
46
|
+
(x * x : U32)
|
|
47
|
+
|
|
48
|
+
def main() -> IO(Unit):
|
|
49
|
+
Python.export_u32(~(x => square(x)), "square", True{})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```bend
|
|
53
|
+
# LAWS.bend: the claim
|
|
54
|
+
import Base
|
|
55
|
+
import ./fast.bend as Fast
|
|
56
|
+
|
|
57
|
+
law square_wraps:
|
|
58
|
+
{Fast.square(65536) == 0 : U32}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bend
|
|
62
|
+
# PROOF.bend: its proof, checked by every build
|
|
63
|
+
import Base
|
|
64
|
+
import ./LAWS.bend as Laws
|
|
65
|
+
|
|
66
|
+
def Laws.square_wraps():
|
|
67
|
+
{==}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
>>> import fast
|
|
72
|
+
>>> fast.square(12)
|
|
73
|
+
144
|
|
74
|
+
>>> fast.square(2**16) # U32 arithmetic wraps, as the law says
|
|
75
|
+
0
|
|
76
|
+
>>> fast.square(-1)
|
|
77
|
+
OverflowError: can't convert negative value to unsigned int
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`True{}` releases the GIL while Bend computes, so threads calling `square` run
|
|
81
|
+
in parallel. Installed wheels need neither Bend nor a compiler.
|
|
82
|
+
|
|
83
|
+
**Status: experimental.** Linux x86_64, CPython 3.10–3.14 including free-threaded
|
|
84
|
+
3.14t, Bend pinned to 2.0.28. Bend 2 is young: its strings are linked lists of
|
|
85
|
+
characters, so text-heavy code is slow and memory-hungry. Benchmark before
|
|
86
|
+
relying on it.
|
|
87
|
+
|
|
88
|
+
## Build an extension
|
|
89
|
+
|
|
90
|
+
You need Clang and the `bend-python` SDK in your build environment. The first
|
|
91
|
+
build downloads the pinned Bend compiler (checksum-verified, cached under
|
|
92
|
+
`~/.cache/bend-python`); set `BEND` to use your own, or `BEND_PYTHON_DOWNLOAD=0`
|
|
93
|
+
to forbid the download.
|
|
94
|
+
|
|
95
|
+
`fast.bend` imports `./bend/python.bend`. If that directory is missing, the build
|
|
96
|
+
copies the SDK's Bend library there; `python -m bend_python vendor bend` does
|
|
97
|
+
the same by hand, for editors and `bend --check-only`.
|
|
98
|
+
|
|
99
|
+
`setup.py` (setuptools is the only supported build backend):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from setuptools import setup
|
|
103
|
+
from bend_python import BendBuildExt, BendExtension
|
|
104
|
+
|
|
105
|
+
setup(
|
|
106
|
+
name="fast",
|
|
107
|
+
ext_modules=[BendExtension("fast", "fast.bend", proofs=["PROOF.bend"])],
|
|
108
|
+
cmdclass={"build_ext": BendBuildExt},
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`pyproject.toml`:
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[build-system]
|
|
116
|
+
requires = ["setuptools>=80", "bend-python==0.1.0"]
|
|
117
|
+
build-backend = "setuptools.build_meta"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then `python -m build` or `pip install .`. The build proves the library's laws
|
|
121
|
+
and yours, generates C, and compiles it; a false law ends the build with
|
|
122
|
+
`error: Bend proof check failed: PROOF.bend` after Bend's diagnostic. Dotted
|
|
123
|
+
names such as `mypackage._native` work. Ship your `.bend` files in the sdist
|
|
124
|
+
(`include *.bend` in `MANIFEST.in`).
|
|
125
|
+
[`examples/`](https://github.com/lucaswiman/bend-python/tree/main/examples) is a
|
|
126
|
+
complete project built this way.
|
|
127
|
+
|
|
128
|
+
## The Python interface
|
|
129
|
+
|
|
130
|
+
An export receives a `Python.Call` (positional arguments and a kwargs dict) and
|
|
131
|
+
returns `IO(Python.Object)`. A `Python.Object` is a handle to any Python object:
|
|
132
|
+
passing one through Bend preserves identity, aliases, cycles and precision,
|
|
133
|
+
with no serialization.
|
|
134
|
+
|
|
135
|
+
```bend
|
|
136
|
+
def echo(request: Python.Call) -> IO(Python.Object):
|
|
137
|
+
Python.unary(request) # exactly one positional argument
|
|
138
|
+
|
|
139
|
+
# in main: Python.export("echo", echo, False{})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Bend API | Does |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `export(name, function, release_gil)` | Expose `Call -> IO(Object)`; `function` must not capture variables |
|
|
145
|
+
| `export_u32` / `export_f32` / `export_bool` / `export_string` / `export_binary_u32` | Typed exports from a template `~f`: argument checks and conversions included. Binary templates are curried: `~(x => y => add(x, y))` |
|
|
146
|
+
| `unary`, `binary` | Require exactly one/two positional arguments and no keywords |
|
|
147
|
+
| `to_u32` `from_u32` `to_f32` `from_f32` `to_bool` `from_bool` `to_string` `from_string` `from_nat` | Convert between Python objects and Bend values |
|
|
148
|
+
| `to_bytes`, `from_bytes` | Any contiguous buffer (`bytes`, `bytearray`, `memoryview`, ...) to `List<U32>` of bytes, and back to `bytes` |
|
|
149
|
+
| `truthy` | Python truthiness, as `bool(value)` |
|
|
150
|
+
| `get_item` `set_item` `getattr` `len` | The Python operations, raising Python's exceptions |
|
|
151
|
+
| `tuple` `list` `dict` `empty_dict` `none` | Build Python objects |
|
|
152
|
+
| `call(f, args, kwargs)`, `invoke(f, arguments)` | Call Python, including callbacks that reenter Bend |
|
|
153
|
+
| `builtins(name)`, `construct(name, arguments)` | Look up or call a builtin such as `"int"` or `"dict"` |
|
|
154
|
+
| `import_module(name)` | Import a module, such as `"operator"` |
|
|
155
|
+
|
|
156
|
+
Conversions are strict. `to_u32` accepts only `int` in `[0, 2**32)`, not `bool`;
|
|
157
|
+
`to_bool` only `bool` (use `truthy` for anything else). `to_f32` accepts only
|
|
158
|
+
`float` and rounds to single precision (overflowing to infinity). Strings convert
|
|
159
|
+
codepoint by codepoint, lone surrogates included; they are linked lists in Bend,
|
|
160
|
+
so prefer `to_bytes` for binary data.
|
|
161
|
+
`to_bytes` snapshots a direct `bytearray` while holding its lock on free-threaded
|
|
162
|
+
Python. For other mutable buffer exporters, including memoryviews of mutable
|
|
163
|
+
storage, callers must prevent concurrent writes while conversion runs.
|
|
164
|
+
Wrong types raise `TypeError`, out-of-range values `OverflowError`, and wrong
|
|
165
|
+
arity or unexpected keywords in typed exports `TypeError`. Python exceptions
|
|
166
|
+
raised inside a call propagate unchanged.
|
|
167
|
+
|
|
168
|
+
Handles are valid only during the call that created them. The bridge seals each
|
|
169
|
+
handle with a per-call key and rejects invalid decoded handles with `ValueError`.
|
|
170
|
+
This detects accidental fabrication or reuse from another call probabilistically;
|
|
171
|
+
it is not an absolute guarantee or a security boundary.
|
|
172
|
+
|
|
173
|
+
## Threads and the GIL
|
|
174
|
+
|
|
175
|
+
Every call runs in its own Bend runtime instance (heap, stack and allocator),
|
|
176
|
+
so threads call the same extension concurrently, and callbacks can reenter it.
|
|
177
|
+
The third argument to `export` sets the GIL policy: `True{}` releases the GIL
|
|
178
|
+
during pure Bend evaluation, `False{}` holds it. Free-threaded builds always
|
|
179
|
+
detach during evaluation and never enable the GIL. The bridge reattaches before
|
|
180
|
+
every Python operation. Each call uses one CPU core; Bend's own parallel
|
|
181
|
+
scheduler and GPU backends are not enabled.
|
|
182
|
+
|
|
183
|
+
## What is proved, and what is trusted
|
|
184
|
+
|
|
185
|
+
The build refuses to compile unless every law checks:
|
|
186
|
+
|
|
187
|
+
- **Argument checks.** Typed exports accept exactly the right number of
|
|
188
|
+
arguments and request a `TypeError` for every other count
|
|
189
|
+
([`bend/LAWS.bend`](https://github.com/lucaswiman/bend-python/blob/main/bend/LAWS.bend)).
|
|
190
|
+
- **Thread protocol.** A model of the C driver proves that Python operations
|
|
191
|
+
happen only while attached and never during native evaluation, that every
|
|
192
|
+
call returns attached (failures included), and that free-threaded builds
|
|
193
|
+
detach ([`bend/THREAD_LAWS.bend`](https://github.com/lucaswiman/bend-python/blob/main/bend/THREAD_LAWS.bend)).
|
|
194
|
+
- **Runtime leases.** In the same model, a call with any number of evaluation
|
|
195
|
+
rounds owns one instance and changes no other, a lease cannot be returned
|
|
196
|
+
twice, a native failure in any round poisons it, and only healthy, small
|
|
197
|
+
instances are cached for reuse.
|
|
198
|
+
|
|
199
|
+
Each model was mutation-tested: breaking any of these rules fails a law. The
|
|
200
|
+
laws are about models and pure Bend code. **Nothing proves that the C bridge
|
|
201
|
+
implements the model**, or anything about CPython, reference counting, or the
|
|
202
|
+
generated runtime; those are covered by tests. The table maps each invariant to
|
|
203
|
+
its evidence:
|
|
204
|
+
|
|
205
|
+
| Invariant | Evidence |
|
|
206
|
+
|---|---|
|
|
207
|
+
| Python API only while attached, never during evaluation | Model proved (`native_effect_rejected`, `native_only_leaves`); C asserts attachment (and the GIL on GIL builds) at every boundary |
|
|
208
|
+
| Every call returns attached, including after native failures | Model proved for any slot, rounds and outcomes (`native_returns_attached`, `invocation_returns_attached`); C's `setjmp` placement trusted |
|
|
209
|
+
| One call per runtime instance | Model proved (`acquisition_requires_available`, `leased_invocation_rejected`, `double_return_rejected`); C's pool mutex trusted; concurrency test |
|
|
210
|
+
| A native failure affects only its instance | Model proved (`failure_isolated`); C unmaps poisoned instances; test |
|
|
211
|
+
| Failed or large instances are never reused | Model proved (`failure_poisons_lease`, `reusable_requires_*`); test for memory release |
|
|
212
|
+
| Wrong arity is a `TypeError` | Proved for the parsers and wrappers (`*_exact_arity`, `*_complete`, `*_rejected`); keyword rejection tested |
|
|
213
|
+
| Accidental forged or stale handles are detected | Sealing in C; tests. Rejection is probabilistic: 32-bit collisions remain possible. Hostile Bend code can import its own C |
|
|
214
|
+
| Exports cannot capture variables | Checked by C at import; test |
|
|
215
|
+
| Objects cross unchanged; conversions are strict | C; tests |
|
|
216
|
+
|
|
217
|
+
## Limits
|
|
218
|
+
|
|
219
|
+
- Main interpreter only; subinterpreters are rejected.
|
|
220
|
+
- A native Bend failure (such as `Nat` overflow) raises `RuntimeError` and
|
|
221
|
+
discards that runtime instance; other calls are unaffected. Native crashes and
|
|
222
|
+
stack exhaustion still kill the process.
|
|
223
|
+
- Each active instance reserves about 10 GiB of *virtual* memory, so `ulimit -v`
|
|
224
|
+
or strict overcommit can make calls fail. Up to eight idle instances are
|
|
225
|
+
cached; an instance whose heap grew past 32 MiB is released instead.
|
|
226
|
+
- Bend's C runtime is private API. The build pins 2.0.28 and refuses to patch a
|
|
227
|
+
runtime that has changed.
|
|
228
|
+
|
|
229
|
+
## Developing this repository
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
bash scripts/bootstrap.sh # Python 3.14, uv venv, checksum-verified Bend
|
|
233
|
+
uv pip install --python .venv/bin/python --no-build-isolation -e .
|
|
234
|
+
(cd examples && CC=clang ../.venv/bin/python setup.py build_ext --inplace)
|
|
235
|
+
.venv/bin/python -m unittest discover -s tests
|
|
236
|
+
uvx prek install # ruff, file hygiene, and proof gates on commit
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
[`docker/Dockerfile`](https://github.com/lucaswiman/bend-python/blob/main/docker/Dockerfile)
|
|
240
|
+
builds the pure SDK wheel once, then builds, `auditwheel`-repairs and tests the
|
|
241
|
+
example for CPython 3.10–3.14 and 3.14t with the image's current Clang. Select
|
|
242
|
+
versions with `-e PYTHON_TAGS=cp314-cp314t`.
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
docker build -f docker/Dockerfile -t bend-python-manylinux .
|
|
246
|
+
docker run --rm --user "$(id -u):$(id -g)" \
|
|
247
|
+
-v "$PWD:/io:ro" -v "$PWD/wheelhouse:/wheelhouse" bend-python-manylinux
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
[CI](https://github.com/lucaswiman/bend-python/blob/main/.github/workflows/wheels.yml)
|
|
251
|
+
runs that recipe per version, `prek`, and `twine check` on the SDK. Record changes
|
|
252
|
+
under "Unreleased" in [`CHANGELOG.md`](https://github.com/lucaswiman/bend-python/blob/main/CHANGELOG.md).
|
|
253
|
+
To release, move them to a dated `## [X.Y.Z] - YYYY-MM-DD` section matching the
|
|
254
|
+
project version and publish a GitHub release tagged `vX.Y.Z`; the
|
|
255
|
+
[release workflow](https://github.com/lucaswiman/bend-python/blob/main/.github/workflows/release.yml)
|
|
256
|
+
checks both, then publishes the SDK to PyPI by trusted publishing, with
|
|
257
|
+
provenance attestations. [`AGENTS.md`](https://github.com/lucaswiman/bend-python/blob/main/AGENTS.md)
|
|
258
|
+
has notes for contributors and coding agents.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
[MIT](https://github.com/lucaswiman/bend-python/blob/main/LICENSE).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# bend-python
|
|
2
|
+
|
|
3
|
+
Write CPython extensions in [Bend 2](https://github.com/bendlang/bend), a pure,
|
|
4
|
+
dependently typed language that compiles to native code, and state laws about
|
|
5
|
+
your code that the build **proves before it compiles**. A false law is a build
|
|
6
|
+
error, not a failing test.
|
|
7
|
+
|
|
8
|
+
```bend
|
|
9
|
+
# fast.bend
|
|
10
|
+
import Base
|
|
11
|
+
import ./bend/python.bend as Python
|
|
12
|
+
|
|
13
|
+
def square(+x: U32) -> U32:
|
|
14
|
+
(x * x : U32)
|
|
15
|
+
|
|
16
|
+
def main() -> IO(Unit):
|
|
17
|
+
Python.export_u32(~(x => square(x)), "square", True{})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bend
|
|
21
|
+
# LAWS.bend: the claim
|
|
22
|
+
import Base
|
|
23
|
+
import ./fast.bend as Fast
|
|
24
|
+
|
|
25
|
+
law square_wraps:
|
|
26
|
+
{Fast.square(65536) == 0 : U32}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bend
|
|
30
|
+
# PROOF.bend: its proof, checked by every build
|
|
31
|
+
import Base
|
|
32
|
+
import ./LAWS.bend as Laws
|
|
33
|
+
|
|
34
|
+
def Laws.square_wraps():
|
|
35
|
+
{==}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
>>> import fast
|
|
40
|
+
>>> fast.square(12)
|
|
41
|
+
144
|
|
42
|
+
>>> fast.square(2**16) # U32 arithmetic wraps, as the law says
|
|
43
|
+
0
|
|
44
|
+
>>> fast.square(-1)
|
|
45
|
+
OverflowError: can't convert negative value to unsigned int
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`True{}` releases the GIL while Bend computes, so threads calling `square` run
|
|
49
|
+
in parallel. Installed wheels need neither Bend nor a compiler.
|
|
50
|
+
|
|
51
|
+
**Status: experimental.** Linux x86_64, CPython 3.10–3.14 including free-threaded
|
|
52
|
+
3.14t, Bend pinned to 2.0.28. Bend 2 is young: its strings are linked lists of
|
|
53
|
+
characters, so text-heavy code is slow and memory-hungry. Benchmark before
|
|
54
|
+
relying on it.
|
|
55
|
+
|
|
56
|
+
## Build an extension
|
|
57
|
+
|
|
58
|
+
You need Clang and the `bend-python` SDK in your build environment. The first
|
|
59
|
+
build downloads the pinned Bend compiler (checksum-verified, cached under
|
|
60
|
+
`~/.cache/bend-python`); set `BEND` to use your own, or `BEND_PYTHON_DOWNLOAD=0`
|
|
61
|
+
to forbid the download.
|
|
62
|
+
|
|
63
|
+
`fast.bend` imports `./bend/python.bend`. If that directory is missing, the build
|
|
64
|
+
copies the SDK's Bend library there; `python -m bend_python vendor bend` does
|
|
65
|
+
the same by hand, for editors and `bend --check-only`.
|
|
66
|
+
|
|
67
|
+
`setup.py` (setuptools is the only supported build backend):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from setuptools import setup
|
|
71
|
+
from bend_python import BendBuildExt, BendExtension
|
|
72
|
+
|
|
73
|
+
setup(
|
|
74
|
+
name="fast",
|
|
75
|
+
ext_modules=[BendExtension("fast", "fast.bend", proofs=["PROOF.bend"])],
|
|
76
|
+
cmdclass={"build_ext": BendBuildExt},
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`pyproject.toml`:
|
|
81
|
+
|
|
82
|
+
```toml
|
|
83
|
+
[build-system]
|
|
84
|
+
requires = ["setuptools>=80", "bend-python==0.1.0"]
|
|
85
|
+
build-backend = "setuptools.build_meta"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then `python -m build` or `pip install .`. The build proves the library's laws
|
|
89
|
+
and yours, generates C, and compiles it; a false law ends the build with
|
|
90
|
+
`error: Bend proof check failed: PROOF.bend` after Bend's diagnostic. Dotted
|
|
91
|
+
names such as `mypackage._native` work. Ship your `.bend` files in the sdist
|
|
92
|
+
(`include *.bend` in `MANIFEST.in`).
|
|
93
|
+
[`examples/`](https://github.com/lucaswiman/bend-python/tree/main/examples) is a
|
|
94
|
+
complete project built this way.
|
|
95
|
+
|
|
96
|
+
## The Python interface
|
|
97
|
+
|
|
98
|
+
An export receives a `Python.Call` (positional arguments and a kwargs dict) and
|
|
99
|
+
returns `IO(Python.Object)`. A `Python.Object` is a handle to any Python object:
|
|
100
|
+
passing one through Bend preserves identity, aliases, cycles and precision,
|
|
101
|
+
with no serialization.
|
|
102
|
+
|
|
103
|
+
```bend
|
|
104
|
+
def echo(request: Python.Call) -> IO(Python.Object):
|
|
105
|
+
Python.unary(request) # exactly one positional argument
|
|
106
|
+
|
|
107
|
+
# in main: Python.export("echo", echo, False{})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Bend API | Does |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `export(name, function, release_gil)` | Expose `Call -> IO(Object)`; `function` must not capture variables |
|
|
113
|
+
| `export_u32` / `export_f32` / `export_bool` / `export_string` / `export_binary_u32` | Typed exports from a template `~f`: argument checks and conversions included. Binary templates are curried: `~(x => y => add(x, y))` |
|
|
114
|
+
| `unary`, `binary` | Require exactly one/two positional arguments and no keywords |
|
|
115
|
+
| `to_u32` `from_u32` `to_f32` `from_f32` `to_bool` `from_bool` `to_string` `from_string` `from_nat` | Convert between Python objects and Bend values |
|
|
116
|
+
| `to_bytes`, `from_bytes` | Any contiguous buffer (`bytes`, `bytearray`, `memoryview`, ...) to `List<U32>` of bytes, and back to `bytes` |
|
|
117
|
+
| `truthy` | Python truthiness, as `bool(value)` |
|
|
118
|
+
| `get_item` `set_item` `getattr` `len` | The Python operations, raising Python's exceptions |
|
|
119
|
+
| `tuple` `list` `dict` `empty_dict` `none` | Build Python objects |
|
|
120
|
+
| `call(f, args, kwargs)`, `invoke(f, arguments)` | Call Python, including callbacks that reenter Bend |
|
|
121
|
+
| `builtins(name)`, `construct(name, arguments)` | Look up or call a builtin such as `"int"` or `"dict"` |
|
|
122
|
+
| `import_module(name)` | Import a module, such as `"operator"` |
|
|
123
|
+
|
|
124
|
+
Conversions are strict. `to_u32` accepts only `int` in `[0, 2**32)`, not `bool`;
|
|
125
|
+
`to_bool` only `bool` (use `truthy` for anything else). `to_f32` accepts only
|
|
126
|
+
`float` and rounds to single precision (overflowing to infinity). Strings convert
|
|
127
|
+
codepoint by codepoint, lone surrogates included; they are linked lists in Bend,
|
|
128
|
+
so prefer `to_bytes` for binary data.
|
|
129
|
+
`to_bytes` snapshots a direct `bytearray` while holding its lock on free-threaded
|
|
130
|
+
Python. For other mutable buffer exporters, including memoryviews of mutable
|
|
131
|
+
storage, callers must prevent concurrent writes while conversion runs.
|
|
132
|
+
Wrong types raise `TypeError`, out-of-range values `OverflowError`, and wrong
|
|
133
|
+
arity or unexpected keywords in typed exports `TypeError`. Python exceptions
|
|
134
|
+
raised inside a call propagate unchanged.
|
|
135
|
+
|
|
136
|
+
Handles are valid only during the call that created them. The bridge seals each
|
|
137
|
+
handle with a per-call key and rejects invalid decoded handles with `ValueError`.
|
|
138
|
+
This detects accidental fabrication or reuse from another call probabilistically;
|
|
139
|
+
it is not an absolute guarantee or a security boundary.
|
|
140
|
+
|
|
141
|
+
## Threads and the GIL
|
|
142
|
+
|
|
143
|
+
Every call runs in its own Bend runtime instance (heap, stack and allocator),
|
|
144
|
+
so threads call the same extension concurrently, and callbacks can reenter it.
|
|
145
|
+
The third argument to `export` sets the GIL policy: `True{}` releases the GIL
|
|
146
|
+
during pure Bend evaluation, `False{}` holds it. Free-threaded builds always
|
|
147
|
+
detach during evaluation and never enable the GIL. The bridge reattaches before
|
|
148
|
+
every Python operation. Each call uses one CPU core; Bend's own parallel
|
|
149
|
+
scheduler and GPU backends are not enabled.
|
|
150
|
+
|
|
151
|
+
## What is proved, and what is trusted
|
|
152
|
+
|
|
153
|
+
The build refuses to compile unless every law checks:
|
|
154
|
+
|
|
155
|
+
- **Argument checks.** Typed exports accept exactly the right number of
|
|
156
|
+
arguments and request a `TypeError` for every other count
|
|
157
|
+
([`bend/LAWS.bend`](https://github.com/lucaswiman/bend-python/blob/main/bend/LAWS.bend)).
|
|
158
|
+
- **Thread protocol.** A model of the C driver proves that Python operations
|
|
159
|
+
happen only while attached and never during native evaluation, that every
|
|
160
|
+
call returns attached (failures included), and that free-threaded builds
|
|
161
|
+
detach ([`bend/THREAD_LAWS.bend`](https://github.com/lucaswiman/bend-python/blob/main/bend/THREAD_LAWS.bend)).
|
|
162
|
+
- **Runtime leases.** In the same model, a call with any number of evaluation
|
|
163
|
+
rounds owns one instance and changes no other, a lease cannot be returned
|
|
164
|
+
twice, a native failure in any round poisons it, and only healthy, small
|
|
165
|
+
instances are cached for reuse.
|
|
166
|
+
|
|
167
|
+
Each model was mutation-tested: breaking any of these rules fails a law. The
|
|
168
|
+
laws are about models and pure Bend code. **Nothing proves that the C bridge
|
|
169
|
+
implements the model**, or anything about CPython, reference counting, or the
|
|
170
|
+
generated runtime; those are covered by tests. The table maps each invariant to
|
|
171
|
+
its evidence:
|
|
172
|
+
|
|
173
|
+
| Invariant | Evidence |
|
|
174
|
+
|---|---|
|
|
175
|
+
| Python API only while attached, never during evaluation | Model proved (`native_effect_rejected`, `native_only_leaves`); C asserts attachment (and the GIL on GIL builds) at every boundary |
|
|
176
|
+
| Every call returns attached, including after native failures | Model proved for any slot, rounds and outcomes (`native_returns_attached`, `invocation_returns_attached`); C's `setjmp` placement trusted |
|
|
177
|
+
| One call per runtime instance | Model proved (`acquisition_requires_available`, `leased_invocation_rejected`, `double_return_rejected`); C's pool mutex trusted; concurrency test |
|
|
178
|
+
| A native failure affects only its instance | Model proved (`failure_isolated`); C unmaps poisoned instances; test |
|
|
179
|
+
| Failed or large instances are never reused | Model proved (`failure_poisons_lease`, `reusable_requires_*`); test for memory release |
|
|
180
|
+
| Wrong arity is a `TypeError` | Proved for the parsers and wrappers (`*_exact_arity`, `*_complete`, `*_rejected`); keyword rejection tested |
|
|
181
|
+
| Accidental forged or stale handles are detected | Sealing in C; tests. Rejection is probabilistic: 32-bit collisions remain possible. Hostile Bend code can import its own C |
|
|
182
|
+
| Exports cannot capture variables | Checked by C at import; test |
|
|
183
|
+
| Objects cross unchanged; conversions are strict | C; tests |
|
|
184
|
+
|
|
185
|
+
## Limits
|
|
186
|
+
|
|
187
|
+
- Main interpreter only; subinterpreters are rejected.
|
|
188
|
+
- A native Bend failure (such as `Nat` overflow) raises `RuntimeError` and
|
|
189
|
+
discards that runtime instance; other calls are unaffected. Native crashes and
|
|
190
|
+
stack exhaustion still kill the process.
|
|
191
|
+
- Each active instance reserves about 10 GiB of *virtual* memory, so `ulimit -v`
|
|
192
|
+
or strict overcommit can make calls fail. Up to eight idle instances are
|
|
193
|
+
cached; an instance whose heap grew past 32 MiB is released instead.
|
|
194
|
+
- Bend's C runtime is private API. The build pins 2.0.28 and refuses to patch a
|
|
195
|
+
runtime that has changed.
|
|
196
|
+
|
|
197
|
+
## Developing this repository
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
bash scripts/bootstrap.sh # Python 3.14, uv venv, checksum-verified Bend
|
|
201
|
+
uv pip install --python .venv/bin/python --no-build-isolation -e .
|
|
202
|
+
(cd examples && CC=clang ../.venv/bin/python setup.py build_ext --inplace)
|
|
203
|
+
.venv/bin/python -m unittest discover -s tests
|
|
204
|
+
uvx prek install # ruff, file hygiene, and proof gates on commit
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
[`docker/Dockerfile`](https://github.com/lucaswiman/bend-python/blob/main/docker/Dockerfile)
|
|
208
|
+
builds the pure SDK wheel once, then builds, `auditwheel`-repairs and tests the
|
|
209
|
+
example for CPython 3.10–3.14 and 3.14t with the image's current Clang. Select
|
|
210
|
+
versions with `-e PYTHON_TAGS=cp314-cp314t`.
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
docker build -f docker/Dockerfile -t bend-python-manylinux .
|
|
214
|
+
docker run --rm --user "$(id -u):$(id -g)" \
|
|
215
|
+
-v "$PWD:/io:ro" -v "$PWD/wheelhouse:/wheelhouse" bend-python-manylinux
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
[CI](https://github.com/lucaswiman/bend-python/blob/main/.github/workflows/wheels.yml)
|
|
219
|
+
runs that recipe per version, `prek`, and `twine check` on the SDK. Record changes
|
|
220
|
+
under "Unreleased" in [`CHANGELOG.md`](https://github.com/lucaswiman/bend-python/blob/main/CHANGELOG.md).
|
|
221
|
+
To release, move them to a dated `## [X.Y.Z] - YYYY-MM-DD` section matching the
|
|
222
|
+
project version and publish a GitHub release tagged `vX.Y.Z`; the
|
|
223
|
+
[release workflow](https://github.com/lucaswiman/bend-python/blob/main/.github/workflows/release.yml)
|
|
224
|
+
checks both, then publishes the SDK to PyPI by trusted publishing, with
|
|
225
|
+
provenance attestations. [`AGENTS.md`](https://github.com/lucaswiman/bend-python/blob/main/AGENTS.md)
|
|
226
|
+
has notes for contributors and coding agents.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
[MIT](https://github.com/lucaswiman/bend-python/blob/main/LICENSE).
|