🦀🚀 Abstract over `Send` and `!Send` traits crates.io/crates/future_form
future_form HACKING.md
7.3 kB
Markdown

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:

  1. Replace K type parameter with concrete type (Sendable/Local)
  2. Replace K::Future<'a, T> with BoxFuture<'a, T>/LocalBoxFuture<'a, T>
  3. Replace K::from_future(...) with Sendable::from_future(...)/Local::from_future(...)
  4. Merge variant-specific where clauses 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/:

  1. my_error.rs — Code that should fail to compile
  2. my_error.stderr — Expected compiler output (generate with TRYBUILD=overwrite)

Release Checklist #

  1. Update version in workspace Cargo.toml
  2. Run cargo test
  3. Run cargo publish --dry-run -p future_form_macros
  4. Run cargo publish --dry-run -p future_form
  5. Commit and tag
  6. cargo publish -p future_form_macros (first — main crate depends on it)
  7. cargo publish -p future_form