Hacking on future_form #
"In nova fert animus mutatas dicere formas corpora."
("I intend to speak of forms changed into new bodies.")
— Ovid, Metamorphoses
Nix Flake #
The project uses a Nix flake for reproducible development. Enter the dev shell:
nix develop
This gives you:
- Rust 1.90.0 (pinned via
rust-overlay) - Cross-compilation targets: macOS (aarch64/x86_64), Linux musl (aarch64/x86_64), Wasm, thumbv6m (embedded)
- Cargo tools:
cargo-expand,cargo-deny,cargo-outdated,cargo-udeps,cargo-sort,cargo-component - Formatters:
nixpkgs-fmt,alejandra,taplo
Commands #
On shell entry, a menu displays available commands:
| Command | Description |
|---|---|
test |
Run tests with cargo-watch |
lint |
Run clippy |
fmt |
Format code |
build |
Build the project |
doc |
Generate docs |
watch |
Watch for changes |
audit |
Security audit dependencies |
semver |
Check semver compatibility |
ci |
Run full CI checks |
Debugging Macros #
cargo-expand is included for inspecting macro output:
cargo expand --package future_form --test macro_tests
Project Structure #
future_form/
├── future_form/ # Core library
│ ├── src/lib.rs # FutureForm, Sendable, Local, FromFuture
│ └── tests/
│ ├── macro_tests.rs # Unit tests for the macro
│ └── ui/ # Compile-fail tests (trybuild)
├── future_form_macros/ # Proc macro crate
│ └── src/lib.rs # #[future_form] attribute macro
└── Cargo.toml # Workspace configuration
Building #
cargo build
Testing #
# Run all tests
cargo test
# Run only macro unit tests
cargo test --test macro_tests
# Run only compile-fail UI tests
cargo test --test compile_fail
# Update UI test expected output after intentional changes
TRYBUILD=overwrite cargo test --test compile_fail
How the Macro Works #
┌─────────────────────────────────┐
│ #[future_form(Sendable, Local)]│
│ impl<K: FutureForm> Trait<K> │
│ for Type<K> { ... } │
└───────────────┬─────────────────┘
│
┌─────────────────┴─────────────────┐
│ syn::parse │
│ Extract K parameter │
└─────────────────┬─────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────┐
│ For each variant in │ │ Clone the impl block │ │ VisitMut walks AST │
│ (Sendable, Local, ...) │ │ and transform: │ │ replacing K only │
│ │ │ │ │ in type positions │
└─────────────────────────┘ └─────────────────────────┘ └─────────────────────┘
│ │ │
└─────────────────────────┼─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ │
│ impl Trait<Sendable> for Type<Sendable> { ... } │
│ impl Trait<Local> for Type<Local> { ... } │
│ impl Trait<Custom> for Type<Custom> { ... } │
│ │
└─────────────────────────────────────────────────────┘
The #[future_form(Sendable, Local)] macro transforms a generic impl:
#[future_form(Sendable, Local)]
impl<K: FutureForm> Trait<K> for Type<K> {
fn method(&self) -> K::Future<'_, T> {
K::from_future(async { ... })
}
}
Into concrete impls for each variant:
impl Trait<Sendable> for Type<Sendable> {
fn method(&self) -> BoxFuture<'_, T> {
Sendable::from_future(async { ... })
}
}
impl Trait<Local> for Type<Local> {
fn method(&self) -> LocalBoxFuture<'_, T> {
Local::from_future(async { ... })
}
}
Key transformations:
- Replace
Ktype parameter with concrete type (Sendable/Local) - Replace
K::Future<'a, T>withBoxFuture<'a, T>/LocalBoxFuture<'a, T> - Replace
K::from_future(...)withSendable::from_future(...)/Local::from_future(...) - Merge variant-specific
whereclauses into the impl
The macro uses syn::visit_mut::VisitMut to walk the AST and replace identifiers only in type positions, avoiding mangling identifiers that contain the type parameter as a substring (e.g., WorksOK when K is the parameter).
Adding a New Test #
Unit test — Add to future_form/tests/macro_tests.rs:
mod my_feature {
use super::*;
trait MyTrait<K: FutureForm> { ... }
#[future_form(Sendable, Local)]
impl<K: FutureForm> MyTrait<K> for MyType { ... }
#[test]
fn test_my_feature_sendable() { ... }
#[test]
fn test_my_feature_local() { ... }
}
Compile-fail test — Add two files to future_form/tests/ui/:
my_error.rs— Code that should fail to compilemy_error.stderr— Expected compiler output (generate withTRYBUILD=overwrite)
Release Checklist #
- Update version in workspace
Cargo.toml - Run
cargo test - Run
cargo publish --dry-run -p future_form_macros - Run
cargo publish --dry-run -p future_form - Commit and tag
cargo publish -p future_form_macros(first — main crate depends on it)cargo publish -p future_form