Skip to content

Latest commit

 

History

History
221 lines (141 loc) · 13.4 KB

File metadata and controls

221 lines (141 loc) · 13.4 KB

Changelog

Unreleased

Changed

  • MSRV raised to Rust 1.93 — corophage now requires Rust 1.93 or later. The newer compiler can prove that recursive async handler dispatch futures are Send, removing an internal unchecked Send assertion and its associated wrapper plumbing.

v0.5.0 (2026-07-16)

Fixed

  • Explicit #[effectful] lifetimes parse correctly — a sole named lifetime no longer requires a trailing comma, 'static remains concrete instead of becoming an invalid generic parameter, and '_ uses normal lifetime inference.

  • Co and CoSend are now available from the crate root — the documented corophage::{Co, CoSend} imports now compile without routing through the coroutine module.

  • Proc-macro expansion is now hygienic#[effect] and #[effectful] honor Cargo dependency renames and no longer collide with user parameters named __y or effect lifetimes named 'r.

  • Overlapping Yielder operations can no longer cause undefined behavioryield_ and invoke now require exclusive &mut self access, so safe Rust rejects starting a second operation before awaiting the first. Manual Program::new and Co::new closures must bind their Yielder parameter as mutable. Incorrect explicit dispatch indices also produce a checked panic in release builds.

  • Invalid resume projections no longer cause undefined behavior — directly invoking the hidden ProjectResume helper with an unmatched resume now produces a checked panic in all build profiles.

  • Async Sendable programs can now be spawned — the future returned by Program::run for Program::new_send and #[effectful(..., send)] computations is now Send when the result and handler futures are Send, making it compatible with tokio::spawn.

  • #[effectful] now accepts ordinary borrowed and generic arguments — elided references, &self receivers, generic parameters, and argument-position impl Trait are tied to the generated program lifetime automatically, without artificial lifetime annotations or outlives bounds.

  • Async computation bodies can now await ordinary futuresProgram::run, asynk::run, and invoked subprograms now drive non-effect suspension points instead of panicking. Synchronous runners continue to support effect suspension and now report a clear error directing callers to an async runner when a computation awaits a pending non-effect future.

v0.4.1 (2026-05-18)

Fixed

  • Public macros no longer require downstream crates to depend on frunk_core directlyEffects!, empty effect sets, spread-based #[effectful] signatures, and internal runner macro expansions now route through corophage's hidden frunk_core re-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 (2026-03-28)

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 that ForwardEffects::forward's return type was Send. Internally replaced with synchronous coproduct conversions (EmbedEffect/ProjectResume) that the compiler handles correctly.

  • Sequential invoke! calls with mutable borrowsinvoke! previously required sub-programs to borrow for the entire outer computation's lifetime, preventing patterns like invoke!(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 Effect impls require shorten_resume — the Effect trait now includes a shorten_resume method that witnesses the covariance of Resume<'r> in 'r. This is needed by invoke to safely shorten resume lifetimes when composing programs. The body is always just resume. The #[effect] macro generates it automatically, so this only affects hand-written impl Effect blocks.

  • ForwardEffects trait removed — replaced internally by EmbedEffect and ProjectResume. This trait was not part of the public API.

v0.3.2 (2026-03-27)

Added

  • Eff added to the prelude — the deprecated Eff type alias (renamed to Effectful in v0.3.0) is now re-exported from corophage::prelude for convenience during migration.

  • ...Spread syntax in #[effectful] and Effects! — reference a pre-defined effects type alias with ...Alias, using the same spread syntax as frunk's Coprod!(...Tail). Works in both #[effectful(Extra, ...MyEffs)] and Effects![Extra, ...MyEffs]. Inline effects can precede the spread, and send/lifetime arguments compose naturally: #[effectful('a, Extra, ...MyEffs<'a>, send)].

v0.3.1 (2026-03-26)

Changed

  • Eff type alias renamed to Effectful — the Eff<'a, Effs, R, L> type alias for an unhandled Program has been renamed to Effectful<'a, Effs, R, L> for clarity. The #[effectful] macro now generates Effectful<...> as its return type.
  • unsafe { unreachable_unchecked() } replaced with debug-checked macro — all internal uses of core::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.

v0.3.0 (2026-03-11)

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 manual Program::new API, use y.invoke(program).await directly.

  • ForwardEffects trait — internal trait used by Yielder::invoke to forward each effect variant from a sub-program's coproduct through the outer yielder.

  • #[effect(ResumeType)] proc macro — derive an Effect impl 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 to Eff<...>, wraps the body in Program::new, and enables yield_!(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 send flag for Send-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-macros crate — new proc-macro crate, added as a workspace member. Re-exported from corophage behind the macros feature (enabled by default).

  • macros feature flag — controls whether the proc macros are available. Enabled by default; disable with default-features = false to opt out.

  • Control<R> — new return type for effect handlers, parameterized by the resume type R instead of the full effect set. Handlers now return Control::resume(value) or Control::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()
  • HandlersToEffects impls for stateful handlersFn(&mut S, E) -> Control<E::Resume<'a>> and AsyncFn(&mut S, E) -> Control<E::Resume<'a>> closures are now recognized by HandlersToEffects, enabling stateful handlers to work with handle() and handle_all().

Changed

  • Removed Send + Sync bounds from Effect::Resume<'r> — the Effect trait no longer requires resume types to be Send + Sync. This allows effects with non-Send resume types (e.g., Rc<str>) when used with non-Send coroutines (Co/Program::new). The bounds are still enforced for Send-able coroutines (CoSend/Program::new_send) via the existing for<'r> Resumes<'r, ...>: Send + Sync constraint. The #[effect] proc macro and declare_effect! macro no longer add Send + Sync bounds on generic type parameters used in the resume type.
  • Program::handle() is now order-independent.handle() now uses CoproductSubsetter (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-level sync::run/asynk::run functions must still match the Effects![...] order.
  • CoControl is now internal — replaced by Control<R> in user-facing code. CoControl is still used internally by the runner loop but is no longer exported.
  • Prelude updatedCoControl removed from prelude, Control added.

v0.2.0 (2026-03-08)

Added

  • Program type — 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 closure
    • Program::new_send(f) — creates a Send-able program (for use with tokio::spawn)
    • Program::from_co(co) — wraps an existing Co/CoSend coroutine
    • .handle(handler) — attaches the next handler (in effect declaration order); type-checked at compile time
    • .run_sync() — executes synchronously, returns Result<R, Cancelled>
    • .run_sync_stateful(&mut state) — executes synchronously with shared mutable state
    • .run() — executes asynchronously, returns Result<R, Cancelled>
    • .run_stateful(&mut state) — executes asynchronously with shared mutable state
  • handle free 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.

Changed

  • run_with renamed to run_stateful everywhere — affects sync::run_stateful and asynk::run_stateful (previously sync::run_with / run_with at crate root).

  • asynk module is now public — async runner functions asynk::run and asynk::run_stateful are now accessed via corophage::asynk::* instead of being re-exported at the crate root.

  • Prelude narroweduse corophage::prelude::* now re-exports only Effect, Effects!, CoControl, Cancelled, Never, Program, Yielder, frunk, and hlist. Co, CoSend, sync, and asynk are no longer in the prelude but remain public.

v0.1.0 (2026-03-02)

Initial release.