Six crates. Two carry the engine and its Python bindings; two are the proc-macro crates that generate the boilerplate each of those would otherwise need; two more carry the browser side.
wingfoil the engine — ops, fluent wiring, adapters, runtime core
└── wingfoil-derive nitro! and #[op]
wingfoil-python the Python extension module (`import wingfoil`)
└── wingfoil-python-derive #[pyop]
wingfoil-wire-types wire format shared by the web adapter and the browser
└── wingfoil-wasm the browser-side codec (own workspace, wasm32 target)
| Crate | What it is |
|---|---|
wingfoil |
The dual-mode stream-processing engine: the Op trait and interpreter, the fluent wiring layer, the op catalog, the I/O adapters, and the shared runtime core. |
wingfoil-derive |
nitro! — one wiring function expands to interpreted, compiled and nested runners. #[op] — an Op impl gains its fluent builder method and the forwarders nitro! dispatches through. |
wingfoil-python |
The PyO3 bindings, built with maturin. Importable as wingfoil. |
wingfoil-python-derive |
#[pyop] — derives a Python-callable function from an Op impl, so a new op reaches Python without hand-written glue. |
wingfoil-wire-types |
The wire-format types shared by the web adapter and the browser client — one definition, so the two ends cannot drift. |
wingfoil-wasm |
The browser-side WASM codec behind @wingfoil/client. Excluded from the default workspace: it targets wasm32-unknown-unknown. |
The TypeScript client that consumes the last two is js/ — an npm
package rather than a Cargo crate, which is why it sits outside this
directory.
Proc-macro crates must be their own compilation unit — that is a Rust
requirement, not a design choice. But the split earns its keep: it is what lets a
user-defined op take exactly the same path as a built-in one. #[op]
generates the interpreted builder method and the naming-convention forwarders
that compiled emission dispatches through, so there is no per-op table inside
nitro! that would need editing to admit a new op.
The same holds on the Python side: #[pyop] means adding an op does not mean
also hand-writing its binding.
Engine time, run bounds, the time queue, Burst, the Kernel and the latency
data layer live in wingfoil/src/runtime/, which
wingfoil re-exports at its historical path. That module exists because the
legacy engine depended on this crate and re-exported the same core from it
during the port — the arrangement that made the cutover a deletion rather than
an unpick. See
../docs/planning/cutover-plan.md.
- Using the engine →
wingfoil/examples/, and../README.mdfor the overview. - Adding an op → the
/new-opskill, and../docs/adding-an-op.md. - Adding an adapter → the
/new-adapterskill; then/bind-adapterfor its Python bindings. - Understanding the design →
../docs/— the port plan, the cutover plan, and the design decision records.