3.2 KiB
Compile-Time Cycle Prevention
dagx guarantees acyclic graphs at compile time with zero runtime overhead. Unlike other DAG libraries that detect cycles at runtime, dagx uses Rust's type system to make cycles impossible to express in the first place.
Why This Matters
Most DAG libraries check for cycles when you execute the graph:
// Other libraries: cycle detected at RUNTIME
let dag = DagRunner::new();
let a = dag.add_task(TaskA);
let b = dag.add_task(TaskB);
a.depends_on(b);
b.depends_on(a); // Compiles fine...
dag.run().await?; // ❌ Error: "Cycle detected!" (at runtime)
dagx prevents this at compile time:
// dagx: cycle prevented at COMPILE TIME
let dag = DagRunner::new();
let a_builder = dag.add_task(TaskA);
let b_builder = dag.add_task(TaskB);
// To make A depend on B, we need B as a TaskHandle.
// But calling depends_on() consumes the builder!
let b_handle = b_builder.depends_on(&some_source);
// Now we can't make B depend on A because b_builder was moved!
// This won't compile:
// a_builder.depends_on(&b_handle); // ✓ OK: A→B
// b_handle.depends_on(&a_handle); // ❌ ERROR: TaskHandle has no depends_on() method!
How It Works: The Type-State Pattern
dagx uses two types to enforce acyclic structure:
-
TaskBuilder<T, Deps>: Mutable builder that can have dependencies added viadepends_on()- Consumed (moved) when you call
depends_on()or convert toTaskHandle - Can only be used once for wiring
- Consumed (moved) when you call
-
TaskHandle<T>: Immutable reference to a finalized task- Copy type (just wraps a task ID)
- Can be used as a dependency for other tasks
- Has no
depends_on()method - can't be modified after creation
This creates an impossible ordering constraint for cycles:
// Attempting a cycle A→B→A:
let a_builder = dag.add_task(TaskA); // TaskBuilder
let b_builder = dag.add_task(TaskB); // TaskBuilder
// To wire A→B, we need B as a TaskHandle
let b_handle = b_builder.into(); // Convert B to TaskHandle (consumes builder)
// To wire B→A, we need A as a TaskHandle
let a_handle = a_builder.depends_on(&b_handle); // A now depends on B
// Now if we try to complete the cycle:
// a_handle.depends_on(&...); // ❌ ERROR: no method named `depends_on` found
The type system enforces strict topological ordering! You can't create a TaskHandle until all its dependencies exist as handles, which prevents cycles.
Proof & Examples
See src/cycle_prevention.rs for comprehensive documentation with compile_fail tests proving various cycle attempts don't compile.
See tests/cycle_prevention.rs for runtime tests demonstrating the type-state pattern in action.
Benefits
- ✅ Zero runtime cost: No cycle detection code to execute
- ✅ Catches errors early: Cycles are caught at compile time, not in production
- ✅ Compiler-verified: The type system proves your graph is acyclic
- ✅ No mental overhead: You can't accidentally create cycles even if you try
This is a true zero-cost abstraction—the type system provides safety guarantees without any runtime checks or performance penalty.