Releases: romac/corophage
v0.4.1
Fixed
- Public macros no longer require downstream crates to depend on
frunk_coredirectly
Effects!, empty effect sets, spread-based#[effectful]signatures, and internal runner macro expansions now route throughcorophage's hiddenfrunk_corere-export.
Added
- Expanded examples and website docs
Added saga-style order processing, stepwise debugger, and choreographic programming examples, plus website pages for the saga and debugger examples.
v0.4.0
Fixed
-
invoke!inside#[effectful(..., send)]functions — previously failed to compile due to a compiler limitation (rust-lang/rust#100013) where the compiler could not prove thatForwardEffects::forward's return type wasSend. Internally replaced with synchronous coproduct conversions (EmbedEffect/ProjectResume) that the compiler handles correctly. -
Sequential
invoke!calls with mutable borrows —invoke!previously required sub-programs to borrow for the entire outer computation's lifetime, preventing patterns likeinvoke!(sub(&mut state)); state.mutate(); invoke!(sub(&mut state));. Sub-programs can now borrow from shorter-lived data than the outer program. -
invoke!with overlapping resume types — fixed an index inference bug where multiple effects sharing the same resume type (e.g., both resuming with()) caused incorrect dispatch.
Changed
-
Manual
Effectimpls requireshorten_resume— theEffecttrait now includes ashorten_resumemethod that witnesses the covariance ofResume<'r>in'r. This is needed byinvoketo safely shorten resume lifetimes when composing programs. The body is always justresume. The#[effect]macro generates it automatically, so this only affects hand-writtenimpl Effectblocks. -
ForwardEffectstrait removed — replaced internally byEmbedEffectandProjectResume. This trait was not part of the public API.
v0.3.1
Changed
Efftype alias renamed toEffectful— theEff<'a, Effs, R, L>type alias for an unhandledProgramhas been renamed toEffectful<'a, Effs, R, L>for clarity.
The#[effectful]macro now generatesEffectful<...>as its return type.unsafe { unreachable_unchecked() }replaced with debug-checked macro — all internal uses ofcore::hint::unreachable_unchecked()are now wrapped in a
debug_unreachable!macro that panics with a descriptive message in debug builds, while preserving the optimization hint in release builds. This makes invariant violations
easier to diagnose during development.
Removed
declare_effect!macro removed — use the#[effect(ResumeType)]attribute macro instead, which supports all the same features (lifetimes, generics, named fields, GAT
resume types) plus visibility modifiers, multiple trait bounds,#[derive(...)], and arbitrary attributes.
EOF
v0.3.0
Added
-
Program composition via
Yielder::invoke— invoke a sub-program from within another program, forwarding the inner program's effects through the outer yielder. The sub-program's effects must be a subset of the outer program's effects. This enables reusable effectful computations that can be composed together.#[effectful(Ask, Print)] fn greet() { let name: &str = yield_!(Ask("name?")); yield_!(Print(format!("Hello, {name}!"))); } #[effectful(Ask, Print, Log)] fn main_program() { yield_!(Log("Starting...")); invoke!(greet()); yield_!(Log("Done!")); }
-
invoke!()macro — call a sub-program inside an#[effectful]function. Expands to__y.invoke(expr).await. Outside#[effectful], emits a compile error. For the manualProgram::newAPI, usey.invoke(program).awaitdirectly. -
ForwardEffectstrait — internal trait used byYielder::invoketo forward each effect variant from a sub-program's coproduct through the outer yielder. -
#[effect(ResumeType)]proc macro — derive anEffectimpl by annotating a struct. Supports lifetimes, generics, and GAT resume types via'r.#[effect(bool)] pub struct Ask(i32); #[effect(&'r str)] pub struct GetConfig; #[effect(())] pub struct Log<'a>(pub &'a str);
-
#[effectful(Eff1, Eff2, ...)]proc macro — mark a function as an effectful computation. The macro transforms the return type toEff<...>, wraps the body inProgram::new, and enablesyield_!(expr)syntax inside the function.#[effectful(Ask, Log<'a>)] fn my_prog<'a>(msg: &'a str) -> bool { yield_!(Log(msg)); yield_!(Ask(42)) }
Supports a
sendflag forSend-able programs (#[effectful(Ask, send)]), automatic lifetime inference, and explicit lifetime annotation as the first argument. -
yield_!()fallback macro — emits a clear compile error when used outside an#[effectful]function. -
corophage-macroscrate — new proc-macro crate, added as a workspace member. Re-exported fromcorophagebehind themacrosfeature (enabled by default). -
macrosfeature flag — controls whether the proc macros are available. Enabled by default; disable withdefault-features = falseto opt out. -
Control<R>— new return type for effect handlers, parameterized by the resume typeRinstead of the full effect set. Handlers now returnControl::resume(value)orControl::cancel(), making them reusable across different effect sets.// Before: handler was coupled to the full effect set |_: Counter| CoControl::resume(42u64) |_: Ask| CoControl::<'static, Effects![Counter, Ask]>::cancel() // After: handler only knows its own resume type |_: Counter| Control::resume(42u64) |_: Ask| Control::<&str>::cancel()
-
Program::handle_all— attach multiple handlers at once from an HList. Handlers can cover any subset of the remaining effects, in any order.let handlers = hlist![ |_: Counter| Control::resume(42u64), |_: Ask| Control::resume("yes"), ]; Program::new(|y: Yielder<'_, Effects![Other, Counter, Ask]>| async move { ... }) .handle_all(handlers) .handle(|_: Other| Control::resume(())) .run_sync()
-
HandlersToEffectsimpls for stateful handlers —Fn(&mut S, E) -> Control<E::Resume<'a>>andAsyncFn(&mut S, E) -> Control<E::Resume<'a>>closures are now recognized byHandlersToEffects, enabling stateful handlers to work withhandle()andhandle_all().
Changed
- Removed
Send + Syncbounds fromEffect::Resume<'r>— theEffecttrait no longer requires resume types to beSend + Sync. This allows effects with non-Sendresume types (e.g.,Rc<str>) when used with non-Sendcoroutines (Co/Program::new). The bounds are still enforced forSend-able coroutines (CoSend/Program::new_send) via the existingfor<'r> Resumes<'r, ...>: Send + Syncconstraint. The#[effect]proc macro anddeclare_effect!macro no longer addSend + Syncbounds on generic type parameters used in the resume type. Program::handle()is now order-independent —.handle()now usesCoproductSubsetter(like.handle_all()) to remove the handled effect from the remaining set, so handlers can be attached in any order. Handlers passed to the low-levelsync::run/asynk::runfunctions must still match theEffects![...]order.CoControlis now internal — replaced byControl<R>in user-facing code.CoControlis still used internally by the runner loop but is no longer exported.- Prelude updated —
CoControlremoved from prelude,Controladded.
v0.2.0
Added
-
Programtype — a builder-style API for assembling and running effect-handled computations. This is now the recommended way to use the library for most users.The key feature is incremental handler attachment: handlers are added one at a time via
.handle(), which means a partially-handled program is a first-class value you can pass around, store, or extend later. The compiler tracks which effects are still unhandled and only permits running the computation once all effects have a handler.// Define a computation once... let program = Program::new(|yielder: Yielder<'_, Effs>| async move { let n = yielder.yield_(Counter).await; let answer = yielder.yield_(Ask("question")).await; (answer, n) }); // ...attach handlers incrementally, e.g. in different modules or call sites... let program = program.handle(|_: Counter| CoControl::resume(42u64)); let program = program.handle(|_: Ask| CoControl::resume("yes")); // ...and run only when all effects are handled. let result = program.run_sync();
Available constructors and methods:
Program::new(f)— creates a program from an async closureProgram::new_send(f)— creates aSend-able program (for use withtokio::spawn)Program::from_co(co)— wraps an existingCo/CoSendcoroutine.handle(handler)— attaches the next handler (in effect declaration order); type-checked at compile time.run_sync()— executes synchronously, returnsResult<R, Cancelled>.run_sync_stateful(&mut state)— executes synchronously with shared mutable state.run()— executes asynchronously, returnsResult<R, Cancelled>.run_stateful(&mut state)— executes asynchronously with shared mutable state
-
handlefree function — functional alternative to.handle(), useful when incrementally building up a program across call sites without method chaining:let p = handle(p, |_: Counter| CoControl::resume(42u64)); let p = handle(p, |_: Ask| CoControl::resume("yes")); p.run_sync()
-
All public items are now documented.
Full Changelog: v0.1.0...v0.2.0