Capabilities and effects
In the new edition, a function's signature says what it may touch: the filesystem, the network, the clock, randomness. You write effects; the compiler passes capabilities. A function that names none is pure, so ra-jit can cache it, sandbox it and run it sync or async without being told.+
No comments yet.
Comments are not set up yet.
Motivation+
No comments yet.
Comments are not set up yet.
Any Rust function can open a file, read the clock or call into C, and nothing in its signature says so. That costs ra-jit three things it wants.+
No comments yet.
Comments are not set up yet.
- Caching. The ra-jit: the function cache can reuse a result only when the function cannot see anything but its arguments. Today it proves that by refusing statics, foreign calls, address exposure and the like, one rule at a time, and asks for
#[ra_jit::cache]. A signature that says pure would make the check one line and the attribute unnecessary.+No comments yet.
Comments are not set up yet.
- Sandboxing. Build scripts and proc macros run with the user's full authority (indexable-inc/ra-jit#147: sandbox build scripts and proc macros). With capabilities, a build script gets read access to its crate and write access to
OUT_DIR, and can do nothing else.+No comments yet.
Comments are not set up yet.
- Sync and async. Rust writes the same logic twice, once blocking and once
async, and libraries pick a runtime for you. If I/O is a capability passed in, the caller chooses how it runs.+No comments yet.
Comments are not set up yet.
It also makes testing honest: a test passes an in-memory filesystem and a fixed clock instead of mocking globals.+
No comments yet.
Comments are not set up yet.
Design sketch+
No comments yet.
Comments are not set up yet.
On the surface, effects: after the return type, / lists what a function uses, as in Effekt (T / Fs, read "T, using Fs"). Several join with +, like trait bounds. Syntax is illustrative; see the open issues.34+
No comments yet.
Comments are not set up yet.
// a crate on the new edition
fn load_config(path: &Path) -> Result<Config, Error> / Fs {
let text = fs::read_to_string(path)?;
parse(&text)
}
// No `/`: pure. ra-jit may cache it, and does.
fn parse(text: &str) -> Result<Config, Error> { /* ... */ }
fn main() / Fs + Net + Clock {
let config = load_config("app.toml".as_ref())?;
// ...
}+No comments yet.
Comments are not set up yet.
Underneath, capabilities: each effect is a hidden parameter. / Fs lowers to a generic parameter bounded by a trait, so load_config takes an F: Fs and calls through it. Generic, not dyn: each caller's choice is monomorphised and inlined, with no vtable on the hot path.54+
No comments yet.
Comments are not set up yet.
fn load_config<F: Fs>(fs: &F, path: &Path) -> Result<Config, Error> {
let text = fs.read_to_string(path)?;
parse(&text)
}+No comments yet.
Comments are not set up yet.
A caller that has a capability passes it on implicitly; one that wants to narrow or replace it says so. A test runs the same code against an in-memory filesystem.+
No comments yet.
Comments are not set up yet.
#[test]
fn reads_defaults() {
let fs = MemFs::from([("app.toml", "port = 80")]);
with fs {
assert_eq!(load_config("app.toml".as_ref()).unwrap().port, 80);
}
}+No comments yet.
Comments are not set up yet.
main receives every capability the platform has. Nothing else is ambient: a function that names no effects cannot reach one, except through unsafe (see the open issues).+
No comments yet.
Comments are not set up yet.
Sync and async from one source+
No comments yet.
Comments are not set up yet.
Zig 0.16 made the same choice from the other side: I/O is an Io value passed in, and the caller picks a blocking or an evented implementation. It dropped the async and await keywords, and code is no longer coloured by how its I/O runs.6789+
No comments yet.
Comments are not set up yet.
Zig's Io is a runtime interface: a vtable. An evented implementation needs a stack per task, or stackless frames, which Zig does not have yet; the proposals for them are still open.101112+
No comments yet.
Comments are not set up yet.
ra-jit owns the compiler, so it can decide at compile time. A function that uses an I/O capability is compiled twice when needed: a direct version, which blocks, and a stackless state machine like the one async fn lowers to today. Which one a call gets follows from the capability the caller passes, the way a generic picks its instance. No keywords in the source, no stacks per task, and no function colouring.+
No comments yet.
Comments are not set up yet.
The library side is the sans-IO pattern made a rule: protocol code that never performs I/O itself, driven by whoever holds the capabilities.131415+
No comments yet.
Comments are not set up yet.
What it buys+
No comments yet.
Comments are not set up yet.
- Automatic caching: a function with no effects, no
unsafeand only pure callees is eligible, with no attribute. One with/ Clockor/ Randis not, visibly.+No comments yet.
Comments are not set up yet.
- Sandboxed build scripts and proc macros, granted only what they need.+
No comments yet.
Comments are not set up yet.
- One source for blocking and async code; the caller picks.+
No comments yet.
Comments are not set up yet.
- Tests without global mocks: pass a fake capability.+
No comments yet.
Comments are not set up yet.
- Review: a diff that adds
/ Netto a parser is visible in the signature, not buried in a call three levels down.+No comments yet.
Comments are not set up yet.
Prior art+
No comments yet.
Comments are not set up yet.
- Zig 0.16.0 release notes, Andrew Kelley, Zig's new async I/O, Loris Cro, Zig's new async I/O, ziglang/zig#25592: std: Introduce Io interface: I/O as an interface the caller passes.+
No comments yet.
Comments are not set up yet.
- cap-std: capability-based filesystem and network APIs for today's Rust.+
No comments yet.
Comments are not set up yet.
- Sans-I/O and Firezone: sans-IO in Rust networking code: protocols that never do I/O themselves.+
No comments yet.
Comments are not set up yet.
- Koka and Effekt: typed effects and handlers, compiled without a runtime for effects; Effekt treats effects as capabilities.+
No comments yet.
Comments are not set up yet.
- Unison abilities: abilities in signatures, and pure code identified and cached by hash (Unison: the big idea).+
No comments yet.
Comments are not set up yet.
- Scala 3 capture checking: types that track which capabilities a value captures.+
No comments yet.
Comments are not set up yet.
- WASI: the host grants capabilities to a wasm module; nothing is ambient.+
No comments yet.
Comments are not set up yet.
- OCaml 5 effect handlers: untyped effect handlers on fibers, the stackful alternative.+
No comments yet.
Comments are not set up yet.
- Rust effects initiative: the Rust project's work on effect generics (
const,async).+No comments yet.
Comments are not set up yet.
Open issues+
No comments yet.
Comments are not set up yet.
How fine are capabilities? Open+
No comments yet.
Comments are not set up yet.
Fs or FsRead and FsWrite? A directory, like cap-std's Dir, or the whole filesystem? Finer is more checkable and more to write; the charter says to pay for checkability, but only for guarantees someone relies on.+
No comments yet.
Comments are not set up yet.
Calling old-edition code Open+
No comments yet.
Comments are not set up yet.
Code in editions 2015 to 2024 has ambient authority: it may do anything. Calling it from the new edition needs a capability that means everything (/ Ambient?), which makes caching and sandboxing impossible through it. How much of std and crates.io can be given precise effects, and how?+
No comments yet.
Comments are not set up yet.
Inference and semver Open+
No comments yet.
Comments are not set up yet.
Must every function spell out its effects, or can private functions infer them? Inferred effects on public functions would make adding a log line a breaking change; explicit ones are noise. One option: explicit at crate boundaries, inferred inside.+
No comments yet.
Comments are not set up yet.
Trait objects and function pointers Open+
No comments yet.
Comments are not set up yet.
A dyn Fn or fn() hides what it uses. Effects have to be part of the type (dyn Fn() / Fs), and a pure fn() must not be able to hold an effectful one.+
No comments yet.
Comments are not set up yet.
Two versions of every I/O function Open+
No comments yet.
Comments are not set up yet.
Compiling a direct and a stackless version doubles code for effectful functions that are used both ways. Only instantiate what is called, as generics do; measure what that costs real programs.+
No comments yet.
Comments are not set up yet.
Is panicking an effect? Open+
No comments yet.
Comments are not set up yet.
Almost everything can panic. Treating it as an effect would be accurate and unusable. Likely answer: panics are not tracked, but a cached function that panics is not cached.+
No comments yet.
Comments are not set up yet.
Is allocation an effect? Open+
No comments yet.
Comments are not set up yet.
Proposed: explicit everywhere, as in Zig. Allocation is a capability like Fs: a function that allocates says / Alloc, and the caller chooses which allocator it gets, so a request can run on an arena, a frame on a bump allocator, a test on a counting allocator. Code with no Alloc cannot allocate at all, which is what real-time audio callbacks, interrupt handlers and kernel paths need and what nothing checks today.7+
No comments yet.
Comments are not set up yet.
Allocation does not make a function impure. A function with only / Alloc gives the same answer for the same input; allocation is observable only through failure and through addresses, and the cache already refuses functions that expose addresses. So Alloc decides which allocator and whether any, not whether a result can be cached.+
No comments yet.
Comments are not set up yet.
The cost is that almost everything says / Alloc, and std's collections take their allocator (Vec<T, A>, as in Rust's unstable allocator API, or Zig's unmanaged containers that take it per call). Open: per collection or per call, how code in old editions gets one (the global allocator, passed in at the edition boundary), and whether Alloc is inferred inside a crate and only written at public items.+
No comments yet.
Comments are not set up yet.
Threads and Send Open+
No comments yet.
Comments are not set up yet.
Spawning is an effect. Can a capability cross threads? Some (Clock) obviously; some (a single-threaded executor's handle) not. This is Send again, for capabilities.+
No comments yet.
Comments are not set up yet.
unsafe and FFI Open+
No comments yet.
Comments are not set up yet.
unsafe code and extern calls can do anything. A function that uses them either declares the effects it really has (trusted, like unsafe impl Send) or counts as ambient.+
No comments yet.
Comments are not set up yet.
Capabilities in cached values Open+
No comments yet.
Comments are not set up yet.
A pure function must not return or capture a capability, or its cached result would carry authority from one run into another. Capability types must be non-cacheable, and the check has to see through generics.+
No comments yet.
Comments are not set up yet.
Syntax Open+
No comments yet.
Comments are not set up yet.
/ Fs after the return type (Effekt's form, the current sketch), uses Fs, an attribute, or bounds on a hidden generic? / reads well and is short, but needs checking against division in const generic and return-type positions. with fs { ... } for handlers, or ordinary arguments? Pick whatever reads best in diffs; nothing is decided.+
No comments yet.
Comments are not set up yet.
References
- ra-jit: the function cache #[ra_jit::cache], and what makes a function ineligible.
- indexable-inc/ra-jit#147: sandbox build scripts and proc macros
- Koka Effect types and handlers, compiled without a runtime for effects.
- Effekt Effect handlers where effects are capabilities passed in, second-class.
- cap-std A capability-oriented version of Rust's std: no ambient filesystem or network.
- Zig 0.16.0 release notes std.Io ships: I/O as an interface value passed in, no async keywords.
- Andrew Kelley, Zig's new async I/O Why the caller picks blocking or evented I/O by passing an Io.
- Loris Cro, Zig's new async I/O The same design from the user's side; the end of function colouring.
- ziglang/zig#6025: async/await/suspend/resume Closed as not planned on 2025-07-15: Zig will not bring back the keywords.
- ziglang/zig#23446: stackless coroutines as low-level primitives Open. How evented I/O gets stackless frames without async/await.
- ziglang/zig#23367: restricted function types Open. Lets the compiler know every target of an indirect call.
- LWN: Zig's new I/O An outside summary of the design and its trade-offs.
- Sans-I/O Protocol code that never does I/O itself; the caller feeds it bytes and time.
- Firezone: sans-IO in Rust networking code The pattern in production Rust.
- quinn QUIC in Rust; quinn-proto is a sans-IO state machine under an async wrapper.
- ziglang/zig#25592: std: Introduce Io interface Merged 2025-10-29.
- Unison abilities Effects in signatures, with handlers; pure code is cached by hash.
- Unison: the big idea Code identified by the hash of its content.
- Scala 3 capture checking Types track which capabilities a value captures.
- WASI System interfaces for wasm, granted as capabilities by the host.
- OCaml 5 effect handlers Untyped effects on fibers: one stack per fiber, no colouring.
- Rust effects initiative The Rust project's work on const, async and other effect generics.
Discussion
Comments on the page as a whole. To comment on one paragraph, use the button beside it.
Comments are not set up yet.