tola-caps
Type-level capability system for Rust
Let the compiler track your state.
tola-caps is a typestate library: encode state into types, let the compiler verify transitions. Each capability is a zero-sized marker type, and a capability set is a type-level collection of these markers.
Type -> Hash -> Point in metric space -> Position in set
You define states (Parsed, Validated), the compiler enforces correct ordering. Zero runtime cost.
Features
- Typestate Pattern: State transitions enforced by the type system
- Zero Runtime Cost: All checks happen at compile time
- Extensible: Define capabilities anywhere, no central registry
- Boolean Logic: Full
&,|,!,()support - Readable Errors: Shows
(A & !B)instead of complex generics - Stable Specialization: Nightly-like specialization on stable Rust
- Trait Detection: Compile-time check if any type implements any trait
- no_std Compatible: Works without std, optional alloc support
Feature Flags
[]
= "0.2" # default: std + specialize + detect
# Or customize:
= { = "0.2", = false, = ["alloc"] }
| Feature | Description |
|---|---|
std (default) |
Standard library support, includes alloc |
alloc |
Alloc types (Vec, Box, String...) without full std |
specialize (default) |
Enable #[specialize] and specialization! macros |
detect (default) |
Std trait detection (caps_check!, AutoCaps) |
Use Cases
- Protocol State Machines: HTTP connections (Connected -> Authenticated -> Active)
- Database Transactions: Track transaction lifecycle (Started -> Modified -> Committed)
- File Handles: Enforce open -> read/write -> close ordering
- Build Pipelines: Ensure compile -> test -> deploy sequence
- Access Control: Type-safe permission tracking (Anonymous -> User -> Admin)
Macro Overview
| Macro | Target | Purpose |
|---|---|---|
#[derive(Capability)] |
struct | Define capability marker |
#[derive(AutoCaps)] |
struct/enum | Enable type for generic context detection |
#[trait_autocaps] |
trait | Enable trait for generic context detection |
#[caps_bound] |
fn/impl | Add capability constraints |
#[specialize] |
impl | Attribute-style specialization |
specialization!{} |
- | Block-style specialization syntax |
caps![] |
- | Build capability set type |
caps_check! |
- | Compile-time trait detection |
Note:
caps_check!for concrete types works with ANY trait directly- Generic contexts (
fn<T>) needT: AutoCapsfor bothcaps_check!andspecialization! - Standard library types already implement
AutoCaps #[derive(AutoCaps)]enables custom types for generic detection and specialization#[trait_autocaps]enables custom traits for detection/specialization
Quick Start
use *;
// 1. Define your states as capabilities
;
;
;
// 2. Use PhantomData to track capabilities at type level
// 3. Add capability constraints to functions
>
>
>
Trait Detection
Check if a type implements any trait at compile time:
use caps_check;
use Debug;
// Concrete types - works with ANY trait, no setup needed
assert!;
assert!;
assert!;
// Custom traits also work on concrete types
assert!;
Generic Context
For generic type parameters, add AutoCaps bound:
use ;
Why AutoCaps is needed for generics:
- Concrete types (e.g.,
String): The compiler knows all trait impls at the call site - Generic
T: The compiler doesn't knowT's traits until instantiation AutoCapsprovides a type-level capability set that enables detection in generic contexts- Standard library types (String, Vec, Box, etc.) implement
AutoCapsautomatically - Custom types need
#[derive(AutoCaps)]
Or use specialization for true type-level dispatch:
```rust
use tola_caps::specialization;
specialization! {
trait Strategy {
fn pick() -> &'static str;
}
impl<T> Strategy for T {
default fn pick() -> &'static str { "move" }
}
impl<T: Clone> Strategy for T {
default fn pick() -> &'static str { "clone" }
}
impl<T: Copy> Strategy for T {
fn pick() -> &'static str { "memcpy" }
}
}
Specialization
tola-caps provides stable Rust specialization using two syntax styles. Both achieve the same goal: select different implementations based on trait bounds.
How it works: The specialization! macro transforms trait bounds into capability queries on Cap<T> (the type's capability set). Selection happens at type-level via SelectCap<Condition, Then, Else>. This requires T: AutoCaps to provide the capability set.
Key insight: T: Clone becomes Cap<T>: Evaluate<IsClone> - dispatching based on whether IsClone exists in T's capability set.
Block Syntax: specialization!
All-in-one macro with default keyword for overridable items:
use specialization;
specialization!
// Usage:
let v: = 42i32.encode; // picks Copy impl
let v: = "hello".to_string.encode; // picks String impl
Attribute Syntax: #[specialize]
Distributed across files, with explicit constraint syntax:
use specialize;
// Fallback impl
// When T: Clone
// Multiple bounds
// Concrete type (highest priority)
Specialization Priority
Resolution order (highest to lowest priority):
- Concrete types:
impl Encode for String - Multiple bounds:
impl<T: Clone + Copy> Encode for T - Single bound:
impl<T: Clone> Encode for T - Default:
impl<T> Encode for Twithdefaultkeyword
Safety Guarantees
tola-caps uses type-level capability dispatch (different from Rust's nightly specialization):
- Mechanism: Dispatches via
Cap<T>capability set lookup, not trait impl ordering - Primary benefit: No lifetime-dependent dispatch - the main source of nightly's unsoundness
- Trade-off: Requires
AutoCapson types, but more reliable on stable Rust
For detailed analysis and caveats, see the Specialization Soundness section.
Limitations
AutoCapsrequired for generic dispatch:- Both
caps_check!ANDspecialization!useCap<T>internally - Concrete types:
caps_check!(String: Clone)works directly (no generics) - Generic
T: NeedsT: AutoCapsforCap<T>to exist - Standard library types already implement
AutoCaps
// Concrete types: no AutoCaps needed assert!; // OK // Generic context: AutoCaps required for Cap<T> // Custom type for generic specialization ; assert_eq!;- Both
- Custom traits: Need
#[trait_autocaps]for generic dispatch// Generates IsMyTrait capability marker - No lifetime-based specialization: By design - lifetimes are erased at codegen
More Examples
Transaction Safety
;
;
Dropping Capabilities
;
>
Set Operations
type Combined = union!;
type Common = intersect!;
Architecture
The Challenge: Type-level set membership when Rust's coherence rules prevent arbitrary type comparison.
The Solution: Hash-based routing + compile-time identity verification.
Capability Name -> FNV-1a Hash -> Trie Path -> Identity Check
"Parsed" -> 0x84... -> [8][4]... -> Unique
Each capability gets a deterministic address in a 16-ary trie. Membership is O(1) path lookup.
Components
1. Routing (Hash -> Path)
FNV-1a (64-bit) hashes the capability name into a HashStream. Each nibble (4 bits) selects a slot in the 16-ary Node16 trie.
2. Identity & Collision Resolution
Hash collisions are unavoidable with 64-bit FNV-1a. tola-caps uses a two-tier identity system:
Tier 1: 64-bit Hash Routing
- FNV-1a hash -> 16 nibbles -> Trie path
- Handles initial routing to leaf nodes
- Fast O(1) lookup
- Note: The codebase also has a 512-bit hash (4×128-bit) system for future collision reduction
Tier 2: Full Path Identity (Guaranteed Uniqueness)
- Complete source path:
name@module::path:file:line:col - Encoded as Finger Tree structure with tiered IList
- Each character stored as compile-time type
- For strings >64 chars: smart sampling (head32 + mid16 + tail16)
// Example identity type structure
type Identity = >>
>;
Collision Handling: When two capabilities hash to the same trie slot:
- Compare full path identities -> guaranteed unique
- If paths match -> compile error (same capability defined twice)
This ensures O(1) routing with 100% collision resolution.
3. Trait Detection (Autoref Fallback)
Rust prefers inherent methods over trait methods:
;
Detect::<String>::IS_CLONE finds the inherent const if String: Clone, else falls back to trait default.
4. Specialization (Cap Dispatch)
Specialization uses Cap<T> (the type's capability set) for type-level selection:
// Generated by specialization! macro:
type Selected = Out;
// SelectCap checks if IsClone exists in Cap<T>:
// - If T: Clone, IsClone is Present -> selects CloneImpl
// - Otherwise, IsClone is Absent -> selects DefaultImpl
This is pure type-level computation - no runtime dispatch.
Complexity
| Operation | Cost |
|---|---|
| Routing | O(1) - fixed 16 levels |
| Collision resolution | O(1) in practice |
| Runtime | Zero - compile-time only |
| Memory | Zero - all PhantomData |
Error Messages
Human-readable errors - shows your capability logic, not compiler internals:
error[E0277]: Capability requirement failed: ((A | B) & !C)
--> src/main.rs:44:19
|
44 | complex_logic(doc_c);
| ^^^^^^^^^^^^^ requirement '((A | B) & !C)' not satisfied
|
= help: ensure the type has capabilities: (A OR B) AND NOT C
Caveats
Specialization Soundness
tola-caps uses type-level capability dispatch, which differs fundamentally from Rust's nightly specialization feature:
Mechanism comparison:
| Aspect | Nightly specialization |
tola-caps |
|---|---|---|
| Dispatch | Trait impl ordering | Cap<T> capability set lookup |
| Decision time | Monomorphization | Type-level (compile-time) |
| Lifetime handling | Problematic (unsound) | Irrelevant (no lifetime dispatch) |
| Associated types | Can be unsound | Not affected |
How tola-caps avoids nightly's unsoundness:
-
No lifetime-dependent dispatch: Selection is based on capability markers (
IsClone,IsCopy), not on lifetime bounds like'static. The SOUNDNESS.md documents that lifetime-dependent specialization is the primary source of unsoundness in nightly. -
No associated type normalization issues: tola-caps generates separate impl structs and selects between them at type-level. There's no "which impl to use" decision that could differ between type-checking and codegen.
-
Capability set is fixed per type:
Cap<MyType>is deterministic - it's built from#[derive(AutoCaps)]at definition time, not inferred from context.
What works safely:
// OK: Trait bound specialization
// IsClone in Cap<T>
// Fallback
// OK: Concrete type override
// Highest priority
What is NOT supported (by design):
// NOT supported: Lifetime-dependent dispatch
// tola-caps cannot distinguish T: 'static from T
Caveats:
- [VERIFIED] Sound for trait-bound-based dispatch (Clone, Copy, Debug, etc.)
- [VERIFIED] No lifetime erasure issues since capabilities don't encode lifetimes
- [LIMITATION] Cannot specialize on
'staticor other lifetime bounds - [LIMITATION] Custom traits need
#[trait_autocaps]for generic dispatch
Practical Limitations
-
AutoCapsis the foundation:- Both
caps_check!andspecialization!useCap<T>for dispatch - Standard library types (String, Vec, i32, etc.) already implement
AutoCaps - Custom types need
#[derive(AutoCaps)]for generic contexts
; // Now works with caps_check! and specialization! - Both
-
#[trait_autocaps]for custom traits:- Registers trait in the capability system
- Enables
caps_check!(T: MyTrait)in generic contexts - Enables specialization based on
MyTraitbound
// Now IsMyTrait capability is available
License
MIT