Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ source .env314/bin/activate # or whichever venv the user picked
pip install -e .[test] # editable install with test deps
pytest -vv # run full suite
pip install -e .[linting] # linting deps
flake8 src/ test/ # lint check
flake8 src/ test/ examples/ # lint check
```

The private `bocpy._internal_test` C extension (used by
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
uses: pypa/cibuildwheel@v3.4.0
env:
CIBW_BUILD: ${{ matrix.python }}-win*
CIBW_TEST_REQUIRES: "pytest setuptools wheel"
CIBW_ENVIRONMENT: "BOCPY_TEST_WHEEL=1"
CIBW_TEST_COMMAND: "pytest {project}/test/test_public_c_abi.py -v"
with:
package-dir: ${{github.workspace}}

Expand Down Expand Up @@ -58,6 +61,9 @@ jobs:
env:
CIBW_BUILD: ${{ matrix.python }}-macosx*
CIBW_ARCHS_MACOS: arm64
CIBW_TEST_REQUIRES: "pytest setuptools wheel"
CIBW_ENVIRONMENT: "BOCPY_TEST_WHEEL=1"
CIBW_TEST_COMMAND: "pytest {project}/test/test_public_c_abi.py -v"
with:
package-dir: ${{github.workspace}}

Expand Down Expand Up @@ -95,6 +101,9 @@ jobs:
CIBW_BUILD: ${{ matrix.python }}-macosx*
CIBW_ARCHS_MACOS: x86_64
MACOSX_DEPLOYMENT_TARGET: 14.8.1
CIBW_TEST_REQUIRES: "pytest setuptools wheel"
CIBW_ENVIRONMENT: "BOCPY_TEST_WHEEL=1"
CIBW_TEST_COMMAND: "pytest {project}/test/test_public_c_abi.py -v"
with:
package-dir: ${{github.workspace}}

Expand All @@ -121,6 +130,9 @@ jobs:
env:
CIBW_BUILD: ${{ matrix.python }}-manylinux*
CIBW_BEFORE_ALL: yum makecache
CIBW_TEST_REQUIRES: "pytest setuptools wheel"
CIBW_ENVIRONMENT: "BOCPY_TEST_WHEEL=1"
CIBW_TEST_COMMAND: "pytest {project}/test/test_public_c_abi.py -v"
with:
package-dir: ${{github.workspace}}

Expand All @@ -147,6 +159,9 @@ jobs:
uses: pypa/cibuildwheel@v3.4.0
env:
CIBW_BUILD: ${{ matrix.python }}-musllinux*
CIBW_TEST_REQUIRES: "pytest setuptools wheel"
CIBW_ENVIRONMENT: "BOCPY_TEST_WHEEL=1"
CIBW_TEST_COMMAND: "pytest {project}/test/test_public_c_abi.py -v"
with:
package-dir: ${{github.workspace}}

Expand Down
100 changes: 99 additions & 1 deletion .github/workflows/pr_gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,112 @@ jobs:
run: pip install -e .[linting]

- name: Run flake8
run: flake8 src/bocpy test
run: flake8 src/bocpy test examples

docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Use Python 3.14
uses: actions/setup-python@v6
with:
python-version: 3.14

- name: Install docs deps
run: pip install -e .[docs]

- name: Build Sphinx docs (warnings as errors)
run: sphinx-build -W -b html sphinx/source sphinx/build/html

c-abi-consumer:
# Build a standalone downstream extension against the bocpy public
# C ABI (templates/c_abi_consumer/) and run its pytest suite. This
# exercises ``bocpy.get_include()`` / ``bocpy.get_sources()`` and
# the ``<bocpy/bocpy.h>`` umbrella from a fresh process that does
# not share build flags with bocpy itself. The Windows leg also
# compiles ``bocpy_msvc.c`` (returned by ``get_sources()`` on
# win32) end-to-end.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Use Python ${{matrix.python_version}}
uses: actions/setup-python@v6
with:
python-version: ${{matrix.python_version}}

- name: Install bocpy
# Scrub the workflow-level BOCPY_BUILD_INTERNAL_TESTS=1 here:
# this job never invokes the internal-test extension, so
# building it is pure overhead.
env:
BOCPY_BUILD_INTERNAL_TESTS: ""
run: pip install -e .[test] --verbose

- name: Build downstream consumer extension
run: pip install --no-build-isolation ./templates/c_abi_consumer

- name: Run consumer tests
run: pytest -vv templates/c_abi_consumer/test

sdist:
runs-on: ubuntu-latest
# The new sdist must build cleanly without the internal-test
# opt-in; setuptools excludes those sources from the tarball, so
# leaving the workflow-level env in scope would attempt to build
# missing files via PEP 517.
env:
BOCPY_BUILD_INTERNAL_TESTS: ""
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Use Python 3.14
uses: actions/setup-python@v6
with:
python-version: 3.14

- name: Install build tool
run: pip install build

- name: Build sdist
run: python -m build --sdist

- name: Install from sdist
run: pip install dist/*.tar.gz --verbose

- name: Smoke-test import
run: python -c "import bocpy; print(bocpy.__name__)"

- name: Smoke-test example with data file
run: |
python -c "import importlib, importlib.resources as r; \
m = importlib.import_module('bocpy.examples'); \
assert (r.files(m) / 'cheese.txt').is_file(), 'cheese.txt missing'; \
assert (r.files(m) / 'menu.txt').is_file(), 'menu.txt missing'"

- name: Wheel allow-list (no internal C/H ships)
env:
BOCPY_TEST_WHEEL: "1"
run: |
pip install pytest
pytest -vv test/test_public_c_abi.py

cpp-format:
runs-on: ubuntu-latest
strategy:
matrix:
path:
- check: src/bocpy
- check: templates/c_abi_consumer/src
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
## 2026-05-10 - Version 0.6.0
Public C ABI for downstream extensions, enabling C-level participation
in behavior-oriented concurrency across worker sub-interpreters.

**New Features**

- **Decorator composition with ``@when``** — decorators stacked below
``@when`` are now preserved on the generated behavior function and
compose with the behavior body on the worker. Decorators placed
above ``@when`` raise a ``SyntaxError`` at transpile time with
actionable guidance. ``async def`` functions with ``@when`` are
also explicitly rejected.
- **Public C ABI (`<bocpy/bocpy.h>`)** — downstream C extensions can
now link against bocpy to register custom Python types as
cross-interpreter shareable so :class:`Cown` can carry instances of
them across worker interpreters. The header is C-only, version-gated
via the ``BOCPY_ABI`` macro, and bumped on any incompatible change
to ``bocpy.h`` or ``xidata.h``. Wheels remain CPython-version-tagged
so a runtime ABI mismatch cannot occur.
- **`bocpy.get_include()` / `bocpy.get_sources()`** — Python-level
helpers that downstream ``setup.py`` files use to locate the bocpy
headers and the small set of C sources that must be compiled into
the consuming extension.
- **`templates/c_abi_consumer/`** — a ready-to-copy template for
building a C extension against the bocpy ABI, including a
``setup.py``, a probe extension exercising the public surface, and
a pytest suite (``test_public_c_abi.py``) that validates the ABI
end-to-end.
- **C source reorganisation** — the per-subsystem translation units
introduced in 0.5.0 have been renamed with a ``boc_`` prefix
(``boc_compat.[ch]``, ``boc_sched.[ch]``, ``boc_tags.[ch]``,
``boc_terminator.[ch]``, ``boc_noticeboard.[ch]``, ``boc_cown.h``)
to give the public ABI a stable, namespaced identity. ``xidata.h``
has moved under ``include/bocpy/`` alongside ``bocpy.h``.

**Documentation**

- New :doc:`c_abi`, :doc:`messaging`, and :doc:`noticeboard` pages
in the Sphinx site; the API reference has been expanded to cover
the public ABI surface.

**Breaking Changes**

- **`noticeboard_version` removed** — the global monotonic version
counter introduced in 0.4.0 has been removed. It exposed an
implementation detail of the snapshot cache that did not survive
the C ABI review and had no use case that was not better served
by ``notice_sync`` plus an explicit ``noticeboard()`` read.

## 2026-04-29 - Version 0.5.0
Verona-RT-style work-stealing scheduler, C source split into per-subsystem
translation units, and a portable atomics / threading layer.
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ authors:
given-names: "Matthew Alastair"
orcid: "https://orcid.org/0000-0002-1019-8036"
title: "bocpy"
version: 0.5.0
date-released: 2026-04-29
version: 0.6.0
date-released: 2026-05-10
url: "https://github.com/microsoft/bocpy"
7 changes: 7 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include src/bocpy/*.h
include src/bocpy/*.c
include src/bocpy/include/bocpy/*.h
include src/bocpy/include/bocpy/*.c
include src/bocpy/py.typed
include src/bocpy/*.pyi
recursive-include examples *.txt
58 changes: 39 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,11 @@ xychart-beta
title "bocpy speedup vs. worker count (chain-ring benchmark, CPython 3.14)"
x-axis "Workers" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "Speedup vs. 1 worker" 0 --> 9
bar [1.00, 1.97, 2.94, 3.90, 4.87, 5.82, 6.75, 7.54]
line [1, 2, 3, 4, 5, 6, 7, 8]
line [1.00, 1.97, 2.94, 3.90, 4.87, 5.82, 6.75, 7.54]
```
<!-- pypi-skip-end -->

The line is the ideal `y = x` reference; the bars are measured speedup. Up
to 8 workers, BOC delivers roughly linear scaling on this microbenchmark
Up to 8 workers, BOC delivers roughly linear scaling on this microbenchmark
(≈7.5× at 8 workers). Real applications carry serial costs that this
benchmark deliberately strips out — see the docstring at the top of
[examples/benchmark.py](examples/benchmark.py) for the load-bearing
Expand Down Expand Up @@ -280,27 +278,32 @@ We provide a few examples to show different ways of using BOC in a program:


## Why BOC for Python?
For many Python programmers, the GIL has established a programming model in which
they do not have to think about the many potential issues that are introduced by
concurrency, in particular data races. One of the best features of BOC is that, due
to the way behaviors interact with concurrently owned data (*cowns*), each behavior
can operate over its data without a need to change this familiar programming model.
Even in a free-threading context, BOC will reduce contention on locks and provide
programs which are data-race free by construction. Our initial research and experiments
with BOC have shown near linear scaling over cores, with up to 32 concurrent worker
sub-interpreters.
Python has always had data races — compound operations like `x += 1` are not
atomic, even under the GIL — and with the arrival of free-threaded builds
(Python 3.13t+) the surface area for concurrency bugs is only growing. BOC
eliminates these problems by construction: because behaviors interact with
shared data exclusively through *cowns*, each behavior operates over its data
as if it were single-threaded. There is no lock ordering to get right, no
forgotten `acquire()`/`release()`, and no possibility of deadlock. This holds
whether your program runs under the GIL, on per-interpreter GIL (3.12+), or
on a free-threaded interpreter.

### This library
Our implementation is built on top of the sub-interpreters mechanism and the
Cross-Interpreter Data (`XIData`) API. As of Python 3.12 each sub-interpreter
has its own GIL, so behaviors scheduled by `bocpy` run truly in parallel.

In addition to the `when` function decorator, the library also exposes
low-level Erlang-style `send` and selective `receive` functions which enable
lock-free communication across threads and sub-interpreters. See the
[`bocpy-primes`](https://github.com/microsoft/bocpy/blob/main/src/bocpy/examples/primes.py) and
[`bocpy-calculator`](https://github.com/microsoft/bocpy/blob/main/src/bocpy/examples/calculator.py)
examples for the usage of these lower-level functions.
The core scheduling engine is written in C — it is **not** a wrapper around
locks, message queues, or `asyncio`. Each `Cown` is backed by a C-level
capsule that embeds an MCS-style queue of pending behaviors. When you call
`@when(a, b)`, the runtime performs **two-phase locking** (2PL) over the
sorted cown IDs entirely in C (releasing the GIL across the lock-free link
loops). Once all cowns in a behavior's request set are acquired, the behavior
is dispatched directly to a worker — there is no central scheduler thread and
no OS-level lock acquisition on the fast path. Releasing a cown unlinks the
MCS node and hands ownership to the next waiting behavior in O(1), which is
then dispatched without touching any shared queue. This gives bocpy the same
deadlock-freedom-by-construction guarantee as the original Verona runtime.

For cross-behavior data sharing that does not warrant a `Cown`, the library
also provides a small **noticeboard** — a global key-value store of up to 64
Expand All @@ -310,6 +313,23 @@ read a frozen snapshot via `noticeboard()` / `notice_read()`. The
[`bocpy-prime-factor`](https://github.com/microsoft/bocpy/blob/main/src/bocpy/examples/prime_factor.py)
example uses it to coordinate early termination across worker behaviors.

The library also includes lower-level Erlang-style messaging primitives
(`send` / `receive`) for channel-based communication patterns; see the
[API documentation](https://microsoft.github.io/bocpy/messaging.html) for
details.

### Waiting for completion

Call `wait()` after scheduling all your behaviors. It blocks the calling
thread until every scheduled behavior has finished, then tears down the
runtime (joins workers, closes the noticeboard). The next `@when` call will
spin up a fresh runtime automatically.

```python
wait() # block indefinitely
wait(timeout=5) # raise TimeoutError if not done in 5 s
```

### Additional Info
BOC is built on a solid foundation of serious scholarship and engineering. For further reading, please see:
1. [When Concurrency Matters: Behaviour-Oriented Concurrency](https://dl.acm.org/doi/10.1145/3622852)
Expand Down
19 changes: 0 additions & 19 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,3 @@ for a result before doing a batch of trial divisions. When any lane finds a
factor it writes to the noticeboard, and the remaining lanes see the result on
their next check and stop early. Demonstrates the "behavior loop" pattern and
cross-behavior coordination via the noticeboard.

## Send/Receive
In addition to exposing the higher-level behavior primitives (*i.e.*,
`when`, `Cown`, `wait`), the library also exposes the lower-level functions
[`send`](http://microsoft.github.io/bocpy/sphinx/api.html#bocpy.send) and
[`receive`](http://microsoft.github.io/bocpy/sphinx/api.html#bocpy.receive), which provide
lock-free Erlang-style send and selective receive. As this paradigm may be
unfamiliar, we provide a few examples for this lower-level API as well.

### Calculator
In this example, several clients send arithmetic commands concurrently in
parallel to a calculator server, which performs the operations and prints the
result. Shows basic `send`/`receive` functionality and how to provide timeout
information.

### Primes
In this example, you have a coordination thread producing work (in this case,
batches of integers) and worker threads doing work (here, counting primes).
Shows how to use `send`/`receive` to share work across multiple worker threads.
6 changes: 6 additions & 0 deletions examples/bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ def do_transfer(src: Cown[Account], dst: Cown[Account]):
else:
print("failure")

# Schedule follow-up behaviors that each acquire only a single
# account cown. These demonstrate that a behavior body can
# schedule further behaviors on a subset of its cowns — the
# inner behaviors will run after the outer one releases. The
# two inner behaviors are independent (they hold disjoint
# cowns) and may run in either order or in parallel.
@when(src)
def _(a: Cown[Account]):
print("src (after transfer):", a.value)
Expand Down
8 changes: 4 additions & 4 deletions examples/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@
"""

import argparse
from dataclasses import asdict, dataclass, field
from datetime import datetime
import json
import os
import socket
import statistics
import subprocess
import sys
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Optional

from bocpy import (Cown, Matrix, noticeboard, notice_write, receive, send,
start, wait, when)
from bocpy import _core
from bocpy import (Cown, Matrix, notice_write, noticeboard, receive, send,
start, wait, when)

# Sentinels for the parent/child JSON protocol. Uppercase so the
# transpiler keeps them as module-level constants in the worker export.
Expand Down
Loading
Loading