doc: update for next release
This commit is contained in:
1 parent
a13f148778
commit
766555bef4
24 files changed
+224
-878
No files matched your search
+11
-20
@@ -1,7 +1,6 @@
|
||||
//! Task builder with type-state pattern for dependency tracking.
|
||||
//!
|
||||
//! TaskBuilder uses compile-time type checking to ensure dependencies are wired correctly.
|
||||
//! The type parameter tracks whether dependencies have been specified yet.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
@@ -14,10 +13,8 @@ use tracing::debug;
|
||||
|
||||
/// Node builder that tracks dependency completion via type state.
|
||||
///
|
||||
/// A `TaskBuilder<Tk, Deps>` is returned from [`DagRunner::add_task`] and uses the type-state
|
||||
/// pattern to ensure dependencies are wired correctly at compile time:
|
||||
/// - `TaskBuilder<Tk, Pending>`: No dependencies specified yet
|
||||
/// - `TaskBuilder<Tk, Tk::Input>`: Dependencies complete; acts as a [`TaskHandle<Tk::Output>`]
|
||||
/// A `TaskBuilder<Tk, Input>` is returned from [`DagRunner::add_task`] for tasks with non-unit inputs.
|
||||
/// After calling [`TaskBuilder::depends_on`], it becomes a [`TaskHandle`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -47,13 +44,11 @@ use tracing::debug;
|
||||
/// # async {
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct with initial value (tuple struct)
|
||||
/// let a = dag.add_task(Constant(10));
|
||||
/// // a is TaskBuilder<_, Pending> since no dependencies needed (Input = ())
|
||||
/// // a is TaskHandle since no dependencies needed (Input = ())
|
||||
///
|
||||
/// // Construct with ::new() pattern
|
||||
/// let b = dag.add_task(Multiplier::new(2));
|
||||
/// // b is TaskBuilder<_, Pending> until we call depends_on()
|
||||
/// // b is TaskBuilder until we call depends_on()
|
||||
///
|
||||
/// let b = b.depends_on(a);
|
||||
/// // Now b is a TaskHandle<i32>
|
||||
@@ -81,9 +76,8 @@ where
|
||||
/// Provide all dependencies exactly once as a tuple.
|
||||
///
|
||||
/// The dependencies must match the task's `Input` type exactly:
|
||||
/// - `Input = ()`: Do **not** call `depends_on`
|
||||
/// - `Input = T`: Pass `&TaskHandle` or `(&TaskHandle,)`
|
||||
/// - `Input = (A, B, ...)`: Pass `(&TaskHandle<A>, &TaskHandle<B>, ...)`
|
||||
/// - `Input = A`: Pass `TaskHandle<A>`
|
||||
/// - `Input = (A, B, ...)`: Pass `(TaskHandle<A>, TaskHandle<B>, ...)`
|
||||
///
|
||||
/// The order of dependencies in the tuple must match the order in `Input`.
|
||||
///
|
||||
@@ -119,14 +113,14 @@ where
|
||||
/// # async {
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// let x = dag.add_task(Value(2)).into();
|
||||
/// let x = dag.add_task(Value(2));
|
||||
/// let y = dag.add_task(Value(3));
|
||||
///
|
||||
/// // Single dependency
|
||||
/// let double = dag.add_task(Scale(2)).depends_on(&x);
|
||||
/// let double = dag.add_task(Scale(2)).depends_on(x);
|
||||
///
|
||||
/// // Multiple dependencies: tuple form
|
||||
/// let sum = dag.add_task(Add).depends_on((&x, y));
|
||||
/// let sum = dag.add_task(Add).depends_on((x, y));
|
||||
///
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// # };
|
||||
@@ -177,8 +171,6 @@ pub struct NodeId(pub u32);
|
||||
/// 1. Pass it to [`crate::TaskBuilder::depends_on`] to wire up dependencies
|
||||
/// 2. Use it with [`crate::DagOutput::get`] to retrieve the output after [`crate::DagRunner::run`]
|
||||
///
|
||||
/// Handles are cheap to clone and copy.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
@@ -215,9 +207,8 @@ impl<T> Copy for TaskHandle<T> {}
|
||||
/// Takes a task and converts it to either a TaskBuilder or a TaskHandle,
|
||||
/// depending on whether it has inputs or not.
|
||||
///
|
||||
/// This is useful to enforce at compile time that a task with unit input never has
|
||||
/// depends_on called, and that it can be used directly as a dependency
|
||||
/// without converting it manually to a TaskHandle.
|
||||
/// This is useful to enforce at compile time that a TaskBuilder is never created for task with unit input,
|
||||
/// and that it can be used directly as a dependency without converting it manually to a TaskHandle.
|
||||
pub trait TaskWire<Input>: Task<Input> + Sync + 'static
|
||||
where
|
||||
Input: Send + Sync + 'static,
|
||||
|
||||
+10
-17
@@ -1,11 +1,7 @@
|
||||
//! Dependency tuple trait and implementations.
|
||||
//!
|
||||
//! This module defines the DepsTuple trait that allows specifying dependencies in
|
||||
//! various forms (handles, builders, tuples). It uses macro-generated implementations
|
||||
//! for different tuple sizes.
|
||||
//!
|
||||
//! This trait is internal and not meant for external implementation. Users should only
|
||||
//! use the provided implementations.
|
||||
//! various combinations of handles for tuples of different sizes.
|
||||
|
||||
use crate::builder::{NodeId, TaskHandle};
|
||||
|
||||
@@ -29,9 +25,6 @@ impl<O> DepsTuple<(O,)> for TaskHandle<O> {
|
||||
|
||||
/// Macro to implement DepsTuple for different tuple sizes.
|
||||
///
|
||||
/// This macro exists because Rust lacks variadic generics - we need separate implementations
|
||||
/// for each tuple size.
|
||||
///
|
||||
/// This allows flexible dependency specification:
|
||||
/// ```ignore
|
||||
/// // Using handles
|
||||
@@ -41,7 +34,7 @@ impl<O> DepsTuple<(O,)> for TaskHandle<O> {
|
||||
/// task.depends_on((dag.add_task(TaskA), dag.add_task(TaskB)))
|
||||
/// ```
|
||||
macro_rules! impl_deps_tuple {
|
||||
($($T:ident : $O:ident),+) => {
|
||||
($($O:ident),+) => {
|
||||
impl<$($O),+> DepsTuple<($($O,)+)> for ($(TaskHandle<$O>,)+)
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
@@ -55,14 +48,14 @@ macro_rules! impl_deps_tuple {
|
||||
|
||||
// Generate DepsTuple implementations for tuples of size 1-8.
|
||||
// Supporting up to 8 elements covers the vast majority of use cases.
|
||||
impl_deps_tuple!(T1:O1);
|
||||
impl_deps_tuple!(T1:O1, T2:O2);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7);
|
||||
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7, T8:O8);
|
||||
impl_deps_tuple!(O1);
|
||||
impl_deps_tuple!(O1, O2);
|
||||
impl_deps_tuple!(O1, O2, O3);
|
||||
impl_deps_tuple!(O1, O2, O3, O4);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6, O7);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6, O7, O8);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+19
-316
@@ -10,15 +10,8 @@
|
||||
//! - **Compile-time type safety**: Dependencies are validated at compile time through the type
|
||||
//! system. The public API is fully type-safe with no runtime type errors. Internal execution
|
||||
//! uses type erasure for heterogeneous task storage, but this is never exposed to users.
|
||||
//! - **Works with ANY type**: Custom types work automatically with the `#[task]` macro—just
|
||||
//! implement `Send + Sync`, no trait implementations needed! The macro generates
|
||||
//! type-specific extraction logic for seamless integration.
|
||||
//! - **Runtime-agnostic**: Works with any async runtime (Tokio, async-std, smol, Embassy, etc.)
|
||||
//! - **Type-state pattern**: The API guides you with compile-time errors if you wire dependencies
|
||||
//! incorrectly. This is what prevents cycles at compile time.
|
||||
//! - **Runtime-agnostic**: Works with any async runtime (Tokio, smol, Embassy, etc.)
|
||||
//! - **Optimal execution**: Topological scheduling with maximum safe parallelism
|
||||
//! - **Zero-cost abstractions**: Leverages generics and monomorphization for minimal overhead
|
||||
//! - **Error handling**: Result-based error handling with [`DagResult<T>`] for validation
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
@@ -46,10 +39,10 @@
|
||||
//!
|
||||
//! // Add source tasks
|
||||
//! let x = dag.add_task(Value(2));
|
||||
//! let y = dag.add_task(Value(3)).into(); // Allows y to be reused as a dependency
|
||||
//! let y = dag.add_task(Value(3));
|
||||
//!
|
||||
//! // Add task with dependencies
|
||||
//! let sum = dag.add_task(Add).depends_on((x, &y));
|
||||
//! let sum = dag.add_task(Add).depends_on((x, y));
|
||||
//!
|
||||
//! // Execute and retrieve results
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
@@ -66,10 +59,9 @@
|
||||
//! that task executes inline rather than being spawned. This eliminates spawning overhead,
|
||||
//! and context switching.
|
||||
//!
|
||||
//! **Panic handling**: To maintain consistent behavior between inline and spawned execution,
|
||||
//! panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to
|
||||
//! `DagError::TaskPanicked`. This matches the behavior of async runtimes like Tokio, async-std,
|
||||
//! and smol, which catch panics in spawned tasks and convert them to errors.
|
||||
//! **Panic handling**: To maintain consistent behavior between runtimes,
|
||||
//! panics in tasks are caught using `FutureExt::catch_unwind()` and converted to
|
||||
//! `DagError::TaskPanicked`. This matches the behavior of async runtimes like Tokio.
|
||||
//!
|
||||
//! **What this means for you**:
|
||||
//! - Tasks behave identically whether executed inline or spawned
|
||||
@@ -78,8 +70,6 @@
|
||||
//! - The optimization is completely transparent - no code changes needed
|
||||
//!
|
||||
//! **When inline execution happens**:
|
||||
//! - Single-task layers (e.g., long sequential chains)
|
||||
//! - Linear pipelines (A→B→C→D)
|
||||
//! - Any layer where `layer.len() == 1` after topological ordering
|
||||
//!
|
||||
//! ## Dependency Limits
|
||||
@@ -89,71 +79,11 @@
|
||||
//! 2. Use intermediate aggregation tasks
|
||||
//! 3. Consider if 8+ dependencies indicates a design issue
|
||||
//!
|
||||
//! ## Task Output Limitations
|
||||
//!
|
||||
//! **IMPORTANT**: Tasks **cannot return bare tuples** as output types. This is a technical
|
||||
//! limitation of the current implementation. If you need to return multiple values, use one
|
||||
//! of these workarounds:
|
||||
//!
|
||||
//! ### Workaround 1: Wrap in Result (Recommended for Fallible Operations)
|
||||
//!
|
||||
//! ```
|
||||
//! use dagx::{task, Task};
|
||||
//!
|
||||
//! struct ProcessData;
|
||||
//! #[task]
|
||||
//! impl ProcessData {
|
||||
//! async fn run(input: &String) -> Result<(String, i32, bool), String> {
|
||||
//! // Wrap the tuple in Result - this works!
|
||||
//! Ok(("Alice".to_string(), 30, true))
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Workaround 2: Use a Struct (Recommended for Most Cases)
|
||||
//!
|
||||
//! ```
|
||||
//! use dagx::{task, Task};
|
||||
//!
|
||||
//! // Define a struct to hold your multiple values
|
||||
//! struct UserData {
|
||||
//! name: String,
|
||||
//! age: i32,
|
||||
//! active: bool,
|
||||
//! }
|
||||
//!
|
||||
//! struct ProcessDataStruct;
|
||||
//! #[task]
|
||||
//! impl ProcessDataStruct {
|
||||
//! async fn run(input: &String) -> UserData {
|
||||
//! // Return the struct - clean and self-documenting
|
||||
//! UserData {
|
||||
//! name: "Alice".to_string(),
|
||||
//! age: 30,
|
||||
//! active: true,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Why Use Structs Over `Result<(...)>`?
|
||||
//!
|
||||
//! While both workarounds solve the technical limitation, **structs are the better choice**
|
||||
//! for most cases:
|
||||
//!
|
||||
//! - **Self-documenting**: `user.name` is clearer than `user.0`
|
||||
//! - **Refactorable**: Easy to add/remove fields without breaking all dependencies
|
||||
//! - **Type-safe**: Named fields prevent field order mistakes
|
||||
//! - **Semantic clarity**: `Result` should indicate fallibility, not just wrap data
|
||||
//!
|
||||
//! Use `Result<(...), E>` only when your operation can genuinely fail and you need to
|
||||
//! return multiple values on success
|
||||
//!
|
||||
//! # Core Concepts
|
||||
//!
|
||||
//! ## Task
|
||||
//!
|
||||
//! A [`Task`] is a unit of async work with typed inputs and outputs. Use the `#[task]` macro.
|
||||
//! A [`Task`] is a unit of async work with typed inputs and outputs. Use the `#[task]` macro to generate an implementation.
|
||||
//!
|
||||
//! ### Task Patterns
|
||||
//!
|
||||
@@ -220,7 +150,7 @@
|
||||
//!
|
||||
//! ### No Dependencies (Source Tasks)
|
||||
//!
|
||||
//! Tasks with no dependencies don't call `depends_on()`:
|
||||
//! Tasks with no dependencies return a [`TaskHandle`] directly and don't call `depends_on()`:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
@@ -258,6 +188,8 @@
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! To manually implement [`Task`] for a single-dependency type, you must implement [`Task<(T,)>`], not [`Task<T>`].
|
||||
//!
|
||||
//! ### Multiple Dependencies
|
||||
//!
|
||||
//! Tasks with multiple dependencies receive separate reference parameters (order matters!):
|
||||
@@ -319,7 +251,7 @@
|
||||
//! # async {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! let base = dag.add_task(Value(10)).into();
|
||||
//! let base = dag.add_task(Value(10));
|
||||
//! let plus1 = dag.add_task(Add(1)).depends_on(base);
|
||||
//! let times2 = dag.add_task(Scale(2)).depends_on(base);
|
||||
//!
|
||||
@@ -386,109 +318,10 @@
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! ## Many-to-Many Pattern (m ↔ n)
|
||||
//!
|
||||
//! Complex DAGs with multiple layers and dependencies:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use dagx::{task, DagRunner, Task};
|
||||
//!
|
||||
//!
|
||||
//! // Source task (tuple struct)
|
||||
//! struct Value(i32);
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Value {
|
||||
//! async fn run(&self) -> i32 { self.0 }
|
||||
//! }
|
||||
//!
|
||||
//! // Stateless addition (unit struct)
|
||||
//! struct Add;
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Add {
|
||||
//! async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
||||
//! }
|
||||
//!
|
||||
//! // Stateless multiplication (unit struct)
|
||||
//! struct Multiply;
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Multiply {
|
||||
//! async fn run(a: &i32, b: &i32) -> i32 { a * b }
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! // Layer 1: Sources
|
||||
//! let x = dag.add_task(Value(2));
|
||||
//! let y = dag.add_task(Value(3)).into();
|
||||
//! let z = dag.add_task(Value(5));
|
||||
//!
|
||||
//! // Layer 2: Intermediate computations
|
||||
//! let sum_xy = dag.add_task(Add).depends_on((x, &y)); // 2 + 3 = 5
|
||||
//! let prod_yz = dag.add_task(Multiply).depends_on((&y, z)); // 3 * 5 = 15
|
||||
//!
|
||||
//! // Layer 3: Final result
|
||||
//! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20
|
||||
//!
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(output.get(total).unwrap(), 20);
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! ## Custom Types
|
||||
//!
|
||||
//! **dagx works with ANY type automatically!** As long as your type is `Send + Sync + 'static`,
|
||||
//! the `#[task]` macro generates the necessary extraction logic:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
//! #
|
||||
//!
|
||||
//! struct User {
|
||||
//! name: String,
|
||||
//! age: u32,
|
||||
//! }
|
||||
//!
|
||||
//! struct CreateUser;
|
||||
//! #[task]
|
||||
//! impl CreateUser {
|
||||
//! async fn run() -> User {
|
||||
//! User { name: "Alice".to_string(), age: 30 }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! struct FormatUser;
|
||||
//! #[task]
|
||||
//! impl FormatUser {
|
||||
//! async fn run(user: &User) -> String {
|
||||
//! format!("{} is {} years old", user.name, user.age)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! let user = dag.add_task(CreateUser);
|
||||
//! let formatted = dag.add_task(FormatUser).depends_on(user);
|
||||
//!
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(output.get(formatted).unwrap(), "Alice is 30 years old");
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! **No trait implementations needed!** Works with nested structs, collections, enums, and any
|
||||
//! other custom type.
|
||||
//!
|
||||
//! # Runtime Agnostic
|
||||
//!
|
||||
//! dagx works with any async runtime. The library has been tested with:
|
||||
//! - **Tokio** - Most popular async runtime
|
||||
//! - **async-std** - Alternative async runtime
|
||||
//! - **smol** - Lightweight async runtime
|
||||
//!
|
||||
//! Examples with different runtimes:
|
||||
@@ -498,14 +331,8 @@
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! }
|
||||
//!
|
||||
//! // With async-std
|
||||
//! #[async_std::main]
|
||||
//! async fn main() {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! // ... build DAG
|
||||
//! let result = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await;
|
||||
//! }
|
||||
//!
|
||||
//! // With smol
|
||||
@@ -513,6 +340,7 @@
|
||||
//! smol::block_on(async {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! let result = dag.run(|fut| smol::spawn(fut)).await;
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
@@ -527,8 +355,6 @@
|
||||
//! - [`DagOutput::get`] returns `DagResult<T>` and can fail if the task hasn't executed or
|
||||
//! the handle is invalid
|
||||
//!
|
||||
//! You can handle errors explicitly or use `.unwrap()` for simple cases:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
//! #
|
||||
@@ -552,25 +378,6 @@
|
||||
//! }
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! # Documentation Accuracy
|
||||
//!
|
||||
//! This library maintains documentation accuracy through:
|
||||
//!
|
||||
//! 1. **Doctests**: All code examples are tested via `cargo test --doc`
|
||||
//! 2. **Examples**: All files in `examples/` are tested via `cargo build --examples`
|
||||
//! 3. **Type signatures**: Documentation reflects actual API return types
|
||||
//! 4. **Error handling**: All examples show proper `DagResult<T>` handling
|
||||
//! 5. **Safety claims**: All safety guarantees are verified by tests
|
||||
//!
|
||||
//! Last audited: 2025-10-06 for v0.1.0
|
||||
//!
|
||||
//! Changes that require documentation updates:
|
||||
//! - API signature changes (`run()`, `get()`, etc.)
|
||||
//! - New error types or error handling changes
|
||||
//! - Safety guarantee changes
|
||||
//! - Example code updates
|
||||
//!
|
||||
//! # API Design Principles
|
||||
//!
|
||||
//! dagx follows these API design principles:
|
||||
@@ -578,48 +385,13 @@
|
||||
//! 1. **Type Safety**: Dependencies validated at compile time via type-state pattern
|
||||
//! 2. **Builder Pattern**: Fluent interface with `add_task().depends_on()`
|
||||
//! 3. **Error Handling**: All fallible operations return `DagResult<T>`
|
||||
//! 4. **Minimal Surface**: Small, focused API with 4 core types and 1 trait
|
||||
//! 5. **Zero Cost**: Leverages generics for monomorphization
|
||||
//! 6. **Consistency**:
|
||||
//! - All types are PascalCase
|
||||
//! - All methods are snake_case
|
||||
//! - Mutable operations take `&mut self`
|
||||
//! - Builder methods consume `self`
|
||||
//! - Results are always `DagResult<T>`
|
||||
//! 4. **Minimal Surface**: Small, focused API
|
||||
//!
|
||||
//! # Performance Characteristics
|
||||
//!
|
||||
//! dagx is designed for minimal overhead and optimal parallel execution.
|
||||
//!
|
||||
//! ## Benchmarks
|
||||
//!
|
||||
//! Run benchmarks with:
|
||||
//! ```bash
|
||||
//! cargo bench
|
||||
//! ```
|
||||
//!
|
||||
//! View the detailed HTML reports:
|
||||
//! ```bash
|
||||
//! # macOS
|
||||
//! open target/criterion/report/index.html
|
||||
//!
|
||||
//! # Linux
|
||||
//! xdg-open target/criterion/report/index.html
|
||||
//!
|
||||
//! # Windows
|
||||
//! start target/criterion/report/index.html
|
||||
//!
|
||||
//! # Or manually open target/criterion/report/index.html in your browser
|
||||
//! ```
|
||||
//!
|
||||
//! Typical performance on modern hardware (AMD 7840U - Zen 4 laptop CPU):
|
||||
//! - **DAG creation**: ~20ns empty DAG, ~96ns per task added
|
||||
//! - **Execution overhead**: ~119ns per task (framework coordination only)
|
||||
//! - **Total overhead** (construction + execution): ~223ns per task
|
||||
//! - **Scaling**: Linear overhead with task count - 100 tasks in ~123µs, 1000 tasks in ~1.15ms
|
||||
//!
|
||||
//! For real-world workloads where tasks perform meaningful work (I/O, computation), the framework
|
||||
//! overhead is typically well under 1% of total execution time.
|
||||
//! See the [repository](https://github.com/swaits/dagx) for up-to-date benchmarks.
|
||||
//!
|
||||
//! ## Performance Tips
|
||||
//!
|
||||
@@ -653,7 +425,7 @@
|
||||
//!
|
||||
//! # async {
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! let data = dag.add_task(FetchData).into();
|
||||
//! let data = dag.add_task(FetchData);
|
||||
//!
|
||||
//! // All three tasks get efficient Arc-wrapped sharing automatically
|
||||
//! let task1 = dag.add_task(ProcessData).depends_on(data);
|
||||
@@ -667,77 +439,20 @@
|
||||
//! **How it works:**
|
||||
//! - Your task outputs `T` (e.g., `Vec<String>`)
|
||||
//! - Framework wraps it in `Arc<T>` internally
|
||||
//! - For fan-out (1→N), Arc is cloned N times (just atomic pointer increments - O(1))
|
||||
//! - For fan-out (1→N), Arc is cloned N times
|
||||
//! - Each downstream task receives `&T` after extracting from Arc
|
||||
//! - When you call `output.get()`, the Arc is cloned and inner value extracted
|
||||
//!
|
||||
//! **Performance characteristics:**
|
||||
//! - **Heap types** (Vec, String, HashMap): Arc overhead is negligible (~few ns)
|
||||
//! - **Copy types** (i32, usize): Small Arc overhead but usually still negligible
|
||||
//! - See `cargo bench` for actual measurements on your hardware
|
||||
//!
|
||||
//! **Advanced - Zero-copy optimization:**
|
||||
//! If you want true zero-copy sharing (no extraction), output `Arc<T>` explicitly:
|
||||
//! ```
|
||||
//! # use dagx::{task, Task};
|
||||
//! # use std::sync::Arc;
|
||||
//! struct ZeroCopyData;
|
||||
//! #[task]
|
||||
//! impl ZeroCopyData {
|
||||
//! // Output Arc<T> directly
|
||||
//! async fn run() -> Arc<Vec<String>> {
|
||||
//! Arc::new(vec!["data".to_string(); 10_000])
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! struct ZeroCopyProcess;
|
||||
//! #[task]
|
||||
//! impl ZeroCopyProcess {
|
||||
//! // Receive &Arc<T> - just clones the Arc pointer, no data extraction
|
||||
//! async fn run(data: &Arc<Vec<String>>) -> usize {
|
||||
//! data.len()
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! This becomes `Arc<Arc<T>>` internally, but ExtractInput unwraps one layer automatically.
|
||||
//! Use this when you have many dependents AND want to avoid the clone() of inner data
|
||||
//!
|
||||
//! ### Other Tips
|
||||
//!
|
||||
//! 1. **Minimize task count**: Combine small operations into larger tasks
|
||||
//! 2. **Use appropriate granularity**: Don't create tasks for trivial work (< 1µs)
|
||||
//! 3. **Parallel execution**: dagx automatically maximizes parallelism
|
||||
//!
|
||||
//! Run `cargo bench` to see comprehensive benchmarks including basic operations, scaling
|
||||
//! characteristics, common patterns (fan-out, diamond), and realistic workloads.
|
||||
//!
|
||||
//! # Optional Tracing Support
|
||||
//!
|
||||
//! dagx provides optional observability through the `tracing` crate with **zero runtime overhead
|
||||
//! when disabled**. The tracing instrumentation is conditionally compiled using feature flags.
|
||||
//!
|
||||
//! ## Enabling Tracing
|
||||
//!
|
||||
//! Add the `tracing` feature to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! dagx = { version = "0.2", features = ["tracing"] }
|
||||
//! tracing-subscriber = "0.3"
|
||||
//! ```
|
||||
//!
|
||||
//! Then initialize a tracing subscriber in your application:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use tracing_subscriber::{fmt, EnvFilter};
|
||||
//!
|
||||
//! fmt()
|
||||
//! .with_env_filter(
|
||||
//! EnvFilter::try_from_default_env()
|
||||
//! .unwrap_or_else(|_| EnvFilter::new("dagx=info"))
|
||||
//! )
|
||||
//! .init();
|
||||
//! ```
|
||||
//! dagx provides optional observability through the `tracing` crate. It can be enabled with the `tracing` feature flag.
|
||||
//!
|
||||
//! ## Log Levels
|
||||
//!
|
||||
@@ -753,21 +468,9 @@
|
||||
//! RUST_LOG=dagx=debug cargo run # Task and layer details
|
||||
//! RUST_LOG=dagx=trace cargo run # All execution details
|
||||
//! ```
|
||||
//!
|
||||
//! ## Zero-Cost Guarantee
|
||||
//!
|
||||
//! When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
|
||||
//! - Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
|
||||
//! - No branches, no function calls, nothing in the compiled binary
|
||||
//! - The `tracing` crate isn't even linked
|
||||
//! - Benchmarks verify identical performance with/without the feature
|
||||
//!
|
||||
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
|
||||
//! Rust async libraries.
|
||||
|
||||
extern crate self as dagx;
|
||||
|
||||
// Module declarations
|
||||
mod builder;
|
||||
mod deps;
|
||||
mod error;
|
||||
|
||||
+3
-8
@@ -27,8 +27,6 @@ type ExecuteFuture =
|
||||
///
|
||||
/// This is the ONLY place where type erasure occurs - tasks are created with full type
|
||||
/// information and only erased at this trait boundary.
|
||||
///
|
||||
/// This is an implementation detail and not part of the public API.
|
||||
pub(crate) trait ExecutableNode: Send {
|
||||
/// Execute the task with type-erased inputs and output
|
||||
///
|
||||
@@ -43,14 +41,14 @@ pub(crate) trait ExecutableNode: Send {
|
||||
/// Fully-typed node storage for a single task.
|
||||
///
|
||||
/// Stores a task of type T. Implements ExecutableNode for type erasure,
|
||||
/// allowing heterogeneous tasks to be stored in a single Vec.
|
||||
/// allowing heterogeneous tasks to be stored together.
|
||||
///
|
||||
/// # Ownership Model
|
||||
///
|
||||
/// The task is owned directly (no Mutex needed) and consumed during execution.
|
||||
/// Each task executes exactly once, taking ownership and producing an output.
|
||||
///
|
||||
/// The output is also returned from execute_with_deps for central storage.
|
||||
/// The output is also returned from execute_with_deps to be passed to dependents and returned as an output
|
||||
pub(crate) struct TypedNode<Input, T>
|
||||
where
|
||||
Input: Send + Sync + 'static,
|
||||
@@ -84,14 +82,11 @@ where
|
||||
) -> ExecuteFuture {
|
||||
Box::pin(async move {
|
||||
// Use the task's run method which has inline extraction logic
|
||||
// generated by the #[task] macro. This allows ANY type to work without
|
||||
// requiring ExtractInput trait implementations.
|
||||
// generated by the #[task] macro.
|
||||
let output = self.task.run(TaskInput::new(dependencies.iter())).await;
|
||||
|
||||
// Arc Wrapping Strategy:
|
||||
// Wrap output in Arc ONCE for efficient fan-out sharing.
|
||||
// For 1:N dependencies, this enables O(1) sharing (Arc clone = atomic increment)
|
||||
// instead of O(N) cloning (N deep copies of output data).
|
||||
// Downstream tasks receive &T after extraction from Arc.
|
||||
let arc_output = Arc::new(output);
|
||||
|
||||
|
||||
+3
-4
@@ -12,13 +12,14 @@ impl DagOutput {
|
||||
/// # Behavior
|
||||
///
|
||||
/// All task outputs are stored after execution and can be retrieved via get().
|
||||
/// Each task's output can only be taken once, after which it will return `DagError::ResultNotFound`.
|
||||
/// Each task's output can only be retrieved once, after which it will return `DagError::ResultNotFound`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `DagError::ResultNotFound` if:
|
||||
/// - The task hasn't been executed yet
|
||||
/// - The handle is invalid
|
||||
/// - The output has already been retrieved
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -46,14 +47,12 @@ impl DagOutput {
|
||||
/// // Construct task with specific setting value
|
||||
/// let task = dag.add_task(Configuration::new(42));
|
||||
///
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(output.get(task).unwrap(), 42);
|
||||
/// # };
|
||||
/// ```
|
||||
pub fn get<T: 'static + Send + Sync>(&mut self, handle: TaskHandle<T>) -> DagResult<T> {
|
||||
let handle: TaskHandle<T> = handle.into();
|
||||
|
||||
let arc_output = self
|
||||
.outputs
|
||||
.remove(&handle.id)
|
||||
|
||||
+17
-53
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! Provides DagRunner for building and executing directed acyclic graphs of async tasks
|
||||
//! with compile-time type-safe dependencies.
|
||||
//!
|
||||
//! Uses Mutex for interior mutability to enable builder pattern (`&self` instead of `&mut self`).
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -23,6 +21,7 @@ use crate::error::{DagError, DagResult};
|
||||
use crate::node::{ExecutableNode, TypedNode};
|
||||
use crate::DagOutput;
|
||||
|
||||
/// Fast hasher using values as hashes
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct PassThroughHasher {
|
||||
hash: u64,
|
||||
@@ -82,7 +81,6 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
||||
/// async fn run(&mut self) -> i32 { self.value }
|
||||
/// }
|
||||
///
|
||||
/// // Unit struct - no fields needed
|
||||
/// struct Add;
|
||||
///
|
||||
/// #[task]
|
||||
@@ -103,17 +101,12 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
||||
/// assert_eq!(output.get(sum).unwrap(), 5);
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// Uses Mutex for interior mutability to enable the builder pattern (`&self` not `&mut self`).
|
||||
/// This allows fluent chaining of `add_task()` calls.
|
||||
///
|
||||
/// Nodes use Option to allow taking ownership during execution.
|
||||
/// Outputs are Arc-wrapped and stored separately for retrieval via get().
|
||||
/// Arc enables efficient sharing during fanout without cloning data.
|
||||
pub struct DagRunner {
|
||||
pub(crate) nodes: Vec<Option<Box<dyn ExecutableNode + Sync>>>,
|
||||
pub(crate) edges: PassThroughHashMap<NodeId, Vec<NodeId>>, // node -> dependencies
|
||||
pub(crate) dependents: PassThroughHashMap<NodeId, Vec<NodeId>>, // node -> tasks that depend on it
|
||||
/// node -> dependencies
|
||||
pub(crate) edges: PassThroughHashMap<NodeId, Vec<NodeId>>,
|
||||
/// node -> tasks that depend on it
|
||||
pub(crate) dependents: PassThroughHashMap<NodeId, Vec<NodeId>>,
|
||||
}
|
||||
|
||||
impl Default for DagRunner {
|
||||
@@ -143,8 +136,8 @@ impl DagRunner {
|
||||
/// Add a task instance to the DAG, returning a node builder for wiring dependencies.
|
||||
///
|
||||
/// If the task has no dependencies, a [`crate::TaskHandle`] will be returned.
|
||||
/// If not, the returned [`crate::TaskBuilder`] can be used to
|
||||
/// specify dependencies via [`crate::TaskBuilder::depends_on`].
|
||||
/// If not, dependencies should be specified for the returned [`crate::TaskBuilder`]
|
||||
/// using [`crate::TaskBuilder::depends_on`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -220,11 +213,9 @@ impl DagRunner {
|
||||
|
||||
/// Run the entire DAG to completion using the provided spawner.
|
||||
///
|
||||
/// This method:
|
||||
/// - Executes tasks in topological order (respecting dependencies)
|
||||
/// - Runs ready tasks with maximum parallelism (executor-limited)
|
||||
/// - Executes each task at most once
|
||||
/// - Waits for **all sinks** (tasks with no dependents) to complete
|
||||
/// - Is runtime-agnostic via the spawner function
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -233,13 +224,11 @@ impl DagRunner {
|
||||
/// and returns a handle to the task. This is the only way to run tasks on separate threads. Examples:
|
||||
/// - Tokio: `|fut| { tokio::spawn(fut).await.unwrap() }`
|
||||
/// - Smol: `|fut| { smol::spawn(fut) }`
|
||||
/// - Async-std: `|fut| { async_std::task::spawn(fut) }`
|
||||
/// - Single-threaded: `|fut| fut`
|
||||
/// - For computationally light tasks, concurrency without parallelism can be significantly faster.
|
||||
/// - Single-threaded on invoking runtime: `|fut| fut`
|
||||
/// - Can be faster in situations where waiting time dominates
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - Returns `DagError::ConcurrentExecution` if the DAG is already executing
|
||||
/// - Returns `DagError::TaskPanicked` if any task panics during execution
|
||||
///
|
||||
/// # Examples
|
||||
@@ -298,36 +287,24 @@ impl DagRunner {
|
||||
HashMap::with_capacity_and_hasher(total_tasks, PassThroughHasher::default());
|
||||
let mut first_error = None;
|
||||
|
||||
// Panic handling is required to maintain behavioral consistency, as
|
||||
// different async runtimes (Tokio, async-std, smol, embassy-rs) handle panics in
|
||||
// spawned tasks differently.
|
||||
//
|
||||
// This ensures tasks behave in the same way whether executed inline or on the spawner
|
||||
// across every runtime.
|
||||
for layer in layers {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
debug!(task_count = layer.len(), "executing layer");
|
||||
}
|
||||
// ============================================================================
|
||||
// PERFORMANCE OPTIMIZATION: Inline execution for single-task layers
|
||||
// ============================================================================
|
||||
// Performance optimization: Inline execution for single-task layers
|
||||
//
|
||||
// When a layer contains exactly one task (common in deep chains, linear
|
||||
// pipelines), we execute it inline rather than spawning it. This provides
|
||||
// 10-100x performance improvements for sequential workloads by eliminating:
|
||||
// - Task spawning overhead
|
||||
// - Context switching to/from the runtime
|
||||
//
|
||||
// CRITICAL: Panic handling is required to maintain behavioral consistency.
|
||||
// All async runtimes (Tokio, async-std, smol, embassy-rs) catch panics in
|
||||
// spawned tasks and convert them to errors. We must do the same for inline
|
||||
// execution to ensure tasks behave identically regardless of execution path.
|
||||
//
|
||||
// Without panic catching:
|
||||
// - Spawned task panics → caught by runtime → becomes error (expected)
|
||||
// - Inline task panics → bubbles up → crashes program (surprising!)
|
||||
//
|
||||
// With panic catching (current implementation):
|
||||
// - Spawned task panics → caught by runtime → becomes error
|
||||
// - Inline task panics → caught by us → becomes error (consistent!)
|
||||
//
|
||||
// This ensures the optimization is transparent to users.
|
||||
// ============================================================================
|
||||
if layer.len() == 1 {
|
||||
let node_id = layer[0];
|
||||
|
||||
@@ -348,11 +325,6 @@ impl DagRunner {
|
||||
.collect();
|
||||
|
||||
// Execute inline with panic handling.
|
||||
//
|
||||
// FutureExt::catch_unwind() ensures panics are caught and converted to
|
||||
// DagError::TaskPanicked, matching the behavior of async runtimes when
|
||||
// they spawn tasks. This guarantees consistent error handling whether
|
||||
// a task executes inline (single-task layer) or spawned (multi-task layer).
|
||||
let result = AssertUnwindSafe(node.execute_with_deps(dependencies))
|
||||
.catch_unwind()
|
||||
.await
|
||||
@@ -461,7 +433,7 @@ impl DagRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Return first error if any
|
||||
// Return first error if any, aborting execution after this layer
|
||||
if let Some(err) = first_error {
|
||||
#[cfg(feature = "tracing")]
|
||||
error!(?err, "DAG execution failed");
|
||||
@@ -475,7 +447,6 @@ impl DagRunner {
|
||||
Ok(DagOutput::new(outputs))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compute_layers(&self) -> DagResult<Vec<Vec<NodeId>>> {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("computing topological layers");
|
||||
@@ -541,10 +512,6 @@ impl DagRunner {
|
||||
//
|
||||
// This eliminates the need for runtime cycle detection, providing both
|
||||
// safety guarantees and performance benefits (no validation overhead).
|
||||
|
||||
// Compiler optimization barrier: ensures visited set is fully populated.
|
||||
// This assertion compiles to nothing in release builds but guides the optimizer's
|
||||
// understanding of the HashSet state, improving subsequent code generation.
|
||||
debug_assert!(!visited.is_empty() || layers.is_empty());
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -554,8 +521,5 @@ impl DagRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We cannot implement Default for DagRunner anymore since new() returns Arc<Self>.
|
||||
// Users should call DagRunner::new() directly.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+16
-66
@@ -7,72 +7,14 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}
|
||||
|
||||
/// A unit of async work with typed inputs and outputs.
|
||||
///
|
||||
/// Use the [`task`](crate::task) macro to implement this trait. The macro automatically
|
||||
/// Use the [`crate::task`] macro to implement this trait. The macro automatically
|
||||
/// derives `Input` and `Output` types from your `run()` method signature and generates
|
||||
/// type-specific extraction logic in `run()`.
|
||||
/// extraction logic in `run()`.
|
||||
///
|
||||
/// **Custom types work automatically!** As long as your output type implements
|
||||
/// `Send + Sync + 'static`, the macro handles everything. No trait implementations needed!
|
||||
/// `Send + Sync + 'static`, the macro handles everything.
|
||||
///
|
||||
/// # Task Patterns
|
||||
///
|
||||
/// dagx supports three main task patterns:
|
||||
///
|
||||
/// ## 1. Stateless Tasks (Pure Functions)
|
||||
///
|
||||
/// Tasks with no internal state - just transform inputs to outputs:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Add;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Add {
|
||||
/// async fn run(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 2. Stateful Tasks with Shared State (`&self`)
|
||||
///
|
||||
/// Tasks that read from internal state but don't modify it:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Multiplier {
|
||||
/// factor: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Multiplier {
|
||||
/// async fn run(&self, input: &i32) -> i32 {
|
||||
/// input * self.factor
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 3. Stateful Tasks with Mutable State (`&mut self`)
|
||||
///
|
||||
/// Tasks that modify internal state during execution:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, increment: &i32) -> i32 {
|
||||
/// self.count += increment;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// **Note:** When implementing this trait manually for a task with a single dependency, the Input type must be (T,) instead of T.
|
||||
///
|
||||
/// # Input Patterns
|
||||
///
|
||||
@@ -80,10 +22,6 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}
|
||||
/// - `&T`: Single dependency
|
||||
/// - `&A, &B, &C, ...`: Multiple dependencies (up to 8, order matters)
|
||||
///
|
||||
/// # The Underlying Trait
|
||||
///
|
||||
/// The `#[task]` macro generates an implementation of this trait:
|
||||
///
|
||||
/// # Output Wrapping
|
||||
///
|
||||
/// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out.
|
||||
@@ -101,6 +39,17 @@ where
|
||||
fn run(self, input: TaskInput<Input>) -> impl Future<Output = Self::Output> + Send;
|
||||
}
|
||||
|
||||
/// Linear type providing a [`Task`] access to its dependencies.
|
||||
///
|
||||
/// The framework ensures that all dependencies will be present when this task runs, and
|
||||
/// can be retrieved with [`TaskInput::next`]. The nth call to `next` consumes this TaskInput instances and returns a
|
||||
/// (&T, TaskInput) tuple with the nth dependency specified in the call to [`crate::TaskBuilder::depends_on`] and a
|
||||
/// TaskInput with the remaining dependencies.
|
||||
///
|
||||
/// No dependencies can be retrieved from an empty TaskInput (Input = ()).
|
||||
///
|
||||
/// **Note:** Calling [`std::mem::forget`] on a `TaskInput` instance will make it impossible to retrieve outputs of dependencies.
|
||||
/// There's no reason to ever do this.
|
||||
pub struct TaskInput<'inputs, Input: Send> {
|
||||
inputs: Iter<'inputs, Arc<dyn Any + Send + Sync + 'static>>,
|
||||
phantom: PhantomData<Input>,
|
||||
@@ -118,6 +67,7 @@ impl<'inputs, Input: Send> TaskInput<'inputs, Input> {
|
||||
macro_rules! impl_task_input {
|
||||
($First:ident $(, $T:ident)*) => {
|
||||
impl<'inputs, $First: 'static + Send, $($T: 'static + Send),*> TaskInput<'inputs, ($First, $($T,)*)> {
|
||||
/// Get a reference to the next dependency from this TaskInput, and a [`TaskInput`] instance with the remaining dependencies.
|
||||
#[must_use]
|
||||
pub fn next(mut self) -> (&'inputs $First, TaskInput<'inputs, ($($T,)*)>) {
|
||||
let value = self.inputs.next().and_then(|value| value.downcast_ref()).unwrap();
|
||||
|
||||
Reference in new issue
Block a user