name: hdb:rust-dev description: Develop Rust code efficiently by minimizing compile cycles and batching work
hdb:rust-dev
Develop Rust code with practices that minimize compile-wait time and maximize throughput in AI-assisted workflows.
Usage
/hdb:rust-dev <task description>
Description
Implements Rust code using a batch-first workflow optimized for AI-assisted development. Instead of the naive write-one-file-compile-fix loop, this skill writes internally consistent code across multiple files before triggering a single compile pass, then fixes all errors in one batch. This approach eliminates the dominant time cost in AI-assisted Rust development: waiting for the compiler.
Instructions
When the user invokes /hdb:rust-dev <task description>:
Phase 1: Understand the task
Read relevant existing code. Before writing anything, read every file that will be modified or that the new code depends on. Understand the types, traits, module structure, and error handling patterns already in use.
Identify the full scope. List all files that need to be created or modified. Group them by dependency order:
- Leaf modules — types, models, data structures (no internal dependencies)
- Core logic — algorithms, business logic (depends on leaf modules)
- Integration points — handlers, CLI wiring, tests (depends on core logic)
Verify third-party crate APIs before writing code that uses them. For any crate you haven't used recently or any unfamiliar feature (template filters, integration crates, macro attributes):
- Check the docs for your exact version combination — e.g.,
askama 0.12+axum 0.8may not be compatible withaskama_axum 0.4 - If an integration crate bridges two dependencies, verify all three versions are compatible before writing any handlers or templates
- When in doubt, write a minimal standalone example (
examples/smoke.rs) andcargo checkit before building on the API
- Check the docs for your exact version combination — e.g.,
Identify domain-specific constraints and edge cases. Before writing core logic, document the domain invariants that the compiler cannot check:
- Sign conventions and ordering of operands in domain formulas
- Numerical edge cases (division by zero, trig inputs outside valid ranges, limits as values approach zero or infinity)
- Unit conversions and coordinate systems
- Business rules or domain constraints that produce wrong answers (not compiler errors) when violated
These domain bugs are invisible to the compiler and typically cost more debugging time than type errors.
Phase 2: Batch write
Write all code before compiling. Generate all files in dependency order (leaves first, integration last). Ensure internal consistency across files:
- Type names, field names, and method signatures match at every call site
- Imports reference the correct module paths
- Trait implementations satisfy all required methods
- Error types propagate consistently through
?chains - Lifetimes and ownership are correct at API boundaries
Do not run
cargo checkorcargo buildbetween files. The goal is zero intermediate compilations.Self-review before compiling. Before triggering the first compile, scan the generated code for these common issues:
Rust-specific:
- Missing
useimports - Mismatched
&strvsStringat function boundaries moveclosures that should borrow, or borrows that needclone()- Missing
deriveattributes (Debug, Clone, Serialize, etc.) asyncfunctions that need.awaitor missingSendbounds- Public vs private visibility (
pub,pub(crate))
Domain-specific:
- Do formulas match the reference specification? (sign conventions, operand order, edge cases)
- Are trig/math inputs clamped to valid ranges? (e.g.,
acosargument within[-1, 1]) - Are division-by-zero and degenerate cases handled? (e.g., guard against zero denominators)
- Do string format specifiers match the template engine's actual syntax? (e.g., Askama filter syntax vs
format!syntax)
- Missing
Phase 3: Compile and fix
Use
cargo checkfor the first pass, notcargo build.cargo checkskips codegen and linking, running 2-3x faster. It catches all type errors, borrow errors, and lifetime issues.cargo check 2>&1Fix all errors in a single batch. Read the full compiler output, identify every error, and fix them all before recompiling. Do not fix one error and recompile — that wastes a full compile cycle on partial progress.
Common batch-fix patterns:
- If multiple files have the same import error, fix them all at once with parallel edits
- If a type rename caused errors across 5 files, fix all 5 before recompiling
- If the borrow checker rejects a pattern, fix the API design (not just the one call site) to prevent cascading errors
Iterate until clean. Repeat the check-fix cycle. Each cycle should resolve multiple errors. If a cycle fixes only one error, you are being too incremental — look for the root cause.
Run
cargo buildonly whencargo checkis clean and you need to execute the binary or run tests.Run
cargo testto verify correctness. If tests fail, fix the failures and re-run. Usecargo test -- --nocapturewhen you need to see output from failing tests.
Phase 4: Validate
Run clippy for lint issues.
cargo clippy 2>&1Fix any warnings. Clippy catches idiomatic issues that
cargo checkmisses.Run
cargo fmt --checkto verify formatting. Applycargo fmtif needed.
Build Optimization Reference
Apply these project-level optimizations when setting up a new Rust project or when build times become painful:
Fast linker (macOS Apple Silicon)
Add to .cargo/config.toml:
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/opt/homebrew/bin/ld64.lld"]
Requires: brew install lld. On macOS the linker must be invoked as ld64.lld (not lld), which is the Mach-O compatible driver. Using plain lld will fail with "Invoke ld64.lld (macOS) instead". Cuts link time 50-80% on incremental builds.
Compilation caching
cargo install sccache
export RUSTC_WRAPPER=sccache
Caches compiled crates across builds. Saves time when switching branches, after cargo clean, or across projects sharing dependencies.
Workspace splitting
For projects with independent subsystems, split into a Cargo workspace:
[workspace]
members = ["core", "web", "cli"]
Benefits:
- Independent crates compile in parallel across CPU cores
- Only the changed crate recompiles on incremental builds
- Enforces clean API boundaries between subsystems
Split when: the project has 3+ modules with no circular dependencies and build times exceed 30 seconds.
Check tests without running them
cargo check --tests
Validates that test code compiles without building the test harness or running tests. Useful during the write phase when you want to verify test code is structurally correct.
Continuous checking during manual development
cargo watch -x check
Reruns cargo check on every file save. Useful when the developer is editing code manually between AI-assisted sessions.
Release Profile
For production binaries, add this to Cargo.toml to produce small, optimized, stripped binaries:
[profile.release]
codegen-units = 1 # Better optimization, slower compile
debug = false
lto = true
opt-level = "z" # Optimize for size
panic = "abort" # Don't include unwinding code
strip = true # Strip symbols from binary
What each setting does:
codegen-units = 1— Allows LLVM to optimize across the entire crate as one unit. Produces faster/smaller code at the cost of slower release builds. Only affectscargo build --release.lto = true— Link-Time Optimization across all crates. Eliminates dead code and inlines across crate boundaries. Significant size reduction.opt-level = "z"— Optimize aggressively for binary size over speed. Use"3"instead if runtime performance matters more than binary size.panic = "abort"— Removes unwinding machinery (~10-20% size reduction). Panics terminate immediately. Incompatible withcatch_unwind()— only use in applications, not libraries.strip = true— Strips debug symbols and symbol tables from the final binary.
When to use: CLI tools, web servers, deployable binaries. Do not apply panic = "abort" to library crates that may be used by others.
Rust-Specific Patterns
Error handling
- Use
anyhow::Resultfor application code and CLI tools - Use
thiserrorfor library crates that expose typed errors - Propagate with
?rather than.unwrap()in non-test code - In tests,
.unwrap()is acceptable — it produces clear panic messages with line numbers
anyhow = "1.0"
thiserror = "2"
API design
- Use enums instead of boolean flags or boolean tuples. Replace
(bool, bool)parameter pairs with a named enum.ScrapeTargets::Bothis self-documenting;(true, false)is not. - Use
StatusCodewith error responses in web handlers. Don't return error HTML without a corresponding HTTP status code.
Ownership at API boundaries
Design function signatures to minimize ownership friction:
- Accept
&strnotStringwhen the function doesn't need to store the value - Accept
impl Into<String>when the function stores the value and callers might have either&strorString - Return owned types (
String,Vec<T>) from functions — let the caller decide to borrow - Use
Cow<'_, str>only when profiling shows the clone matters
Module organization
- Use
lib.rs+main.rssplit for all non-trivial projects. Put all logic inlib.rs(and its submodules);main.rsonly parses args and calls into the library. This is the single most impactful structural decision: it enables integration tests intests/, which cannot import from a binary crate. - One
mod.rs(ormodule_name.rs) per logical subsystem - Re-export the public API from
mod.rsso callers use short paths (e.g.,use crate::bemt::design_propellernotuse crate::bemt::optimizer::design_propeller) - Keep
mod.rsfiles thin — orchestration and re-exports, not implementation - Unit tests go in the same file as the code they test (
#[cfg(test)] mod tests) - Integration tests go in
tests/. These test the public API throughuse your_crate::.... Use test fixtures (files intests/fixtures/) for data-driven tests. This is only possible with thelib.rssplit.
Dependency management
- Pin major versions in
Cargo.toml(e.g.,serde = "1"notserde = "*") - Use
featuressparingly — only enable what you need (e.g.,tokio = { version = "1", features = ["rt-multi-thread", "macros"] }notfeatures = ["full"]) - Prefer
bundledfeature for C library bindings (e.g.,rusqlite = { features = ["bundled"] }) to avoid system dependency issues - Run
cargo updateperiodically to pick up patch releases
Preferred Crates by Domain
When the project has no existing precedent for a dependency, prefer these crates:
Command-line utilities
clap = { version = "4.3", features = ["derive"] } # Argument parsing with derive macros
dirs = "5.0" # Platform-standard directories (~/.config, etc.)
glob = "0.3" # File path glob matching
regex = "1.8" # Regular expressions
clapwithderivefeature for declarative argument definitions. Avoid hand-parsingstd::env::args.dirsfor locating config/data/cache directories portably. Never hardcode~/.config— it differs on macOS and Windows.globfor file pattern matching (e.g.,"src/**/*.rs").regexis the standard regex engine. Compiles patterns to efficient automata. UseRegexSetwhen matching against multiple patterns.
Web applications
axum = "0.8" # Web framework (async, tower-based)
tokio = { version = "1.40", features = ["full"] } # Async runtime
tower-http = { version = "0.6", features = ["fs"] } # HTTP middleware (static files, CORS, etc.)
reqwest = { version = "0.12", features = ["rustls-tls"] } # HTTP client
askama = "0.12" # Compile-time HTML templates
- Avoid
askama_axumand similar integration crates that lag behind framework releases. Instead, render templates manually and returnHtml:
This avoids version coupling between the template engine and the web framework.let html = template.render().map_err(|e| /* error handling */)?; Ok(Html(html))
Asynchronous operation
tokio = { version = "1.40", features = ["full"] } # Async runtime, timers, I/O, channels
features = ["full"]enables everything (runtime, macros, net, fs, time, sync). For libraries, enable only what you need:["rt-multi-thread", "macros"].- Prefer
tokio::spawnfor concurrent tasks,tokio::select!for racing futures. - Use
tokio::sync::Mutex(notstd::sync::Mutex) when holding a lock across.awaitpoints.
System code with hashing and parallel execution
blake3 = { version = "1.8", features = ["rayon"] } # Fast cryptographic hashing (SIMD-accelerated)
rayon = "1.10" # Data parallelism (parallel iterators)
memmap2 = "0.9" # Memory-mapped file I/O
blake3withrayonfeature enables multi-threaded hashing of large files. Faster than SHA-256 for all input sizes.rayonturns.iter()into.par_iter()for trivial parallelism. Use for CPU-bound work over collections. Do not mix withtokio— rayon has its own thread pool.memmap2for zero-copy access to large files. Avoids reading entire files into memory.
WASM (WebAssembly)
yew = { version = "0.21", features = ["csr"] } # Component framework (React-like)
patternfly-yew = "0.6" # PatternFly UI components for Yew
yewwithcsr(client-side rendering) for browser-targeted WASM applications.patternfly-yewprovides pre-built UI components (tables, forms, navigation) following the PatternFly design system.- Build with
trunk servefor development,trunk build --releasefor production.
Serialization and deserialization
serde = { version = "1", features = ["derive"] } # Serialization framework
serde_json = "1" # JSON
serde_yaml = "0.9" # YAML
toml = "0.8" # TOML (config files)
csv = "1.3" # CSV reading/writing
chrono = { version = "0.4", features = ["serde"] } # DateTime with serde support
- Always enable
serde'sderivefeature. Use#[derive(Serialize, Deserialize)]on all data types that cross serialization boundaries. chronowithserdefeature for serializable timestamps. Usechrono::DateTime<Utc>as the standard time type.- For TOML config files, prefer
tomlcrate overserde_toml.
Terminal / TUI applications
ratatui = "0.29" # TUI framework (widgets, layout, rendering)
crossterm = "0.28" # Terminal manipulation backend
ratatuiis the actively maintained fork oftui-rs. Provides widgets (tables, lists, charts, paragraphs) and a layout system.crosstermis the cross-platform terminal backend. Use with ratatui:ratatui::prelude::CrosstermBackend.- Pattern: initialize terminal in
main(), restore on exit (including panic). Usestd::panic::set_hookto ensure terminal cleanup.
Git operations
git2 = "0.19" # libgit2 bindings
git2provides full git operations (clone, commit, diff, log, blame) without shelling out togit.- Requires
libgit2(bundled by default vialibgit2-sys). No system dependency needed. - For simple operations (status, add, commit), shelling out to
gitviastd::process::Commandis simpler and avoids the compile-time cost ofgit2.
Crate Compatibility
When using multiple crates that integrate with each other, verify version compatibility before writing application code:
- Integration crates (e.g.,
askama_axum,tower-http,sqlxwith runtime features) bridge two or more dependencies. All bridged versions must be compatible. Check the integration crate'sCargo.tomlfor its dependency version requirements. - Test compatibility early. After adding a new integration crate, run
cargo checkon a minimal use before writing handlers or business logic. Discovering incompatibility after writing 500 lines of handler code wastes the entire batch. - When an integration crate lags behind its dependencies, drop it and implement the glue manually. For example, if a template integration crate doesn't support the latest version of your web framework, render templates manually and wrap the output. A few lines of manual glue is better than pinning to an old framework version.
- Pin integration crate versions explicitly (e.g.,
askama_axum = "=0.4.0") when you need a specific compatible combination, to preventcargo updatefrom breaking it.
Testing Strategies
Golden-value tests for numerical and domain code
For code that computes numerical results (solvers, financial calculations, data transformations), compile-time correctness is necessary but not sufficient — the code can compile and produce wrong answers. Use golden-value tests:
- Obtain reference values from a known-good source (published tables, reference implementation, manual calculation)
- Create test fixtures with input data and expected outputs
- Assert with tolerances — use approximate comparison for floating-point results:
assert!((result - expected).abs() < 1e-6, "expected {expected}, got {result}"); - Test edge cases explicitly — zero inputs, boundary values, degenerate cases that are valid but extreme
Integration tests with fixtures
For code that processes external data (HTML, files, API responses):
- Store representative fixtures in
tests/fixtures/— real-world examples, not hand-crafted minimal inputs - Test the public API end-to-end — parse, transform, and verify the output in a single test
- Include malformed inputs — test that bad data produces clear errors, not panics
Web application state
Prefer simpler state patterns that avoid ownership complexity:
- Pass configuration (not connections) in web state. For example, store a database path as a
Stringand open a connection per request, rather than sharingArc<Mutex<Connection>>across handlers. This eliminates lock contention and simplifies ownership. - Use
Arc<T>for truly shared read-only state (config, compiled templates, static data) - Use per-request resources for anything with mutable state or cleanup requirements
Guidelines
- Batch over incremental. The single most impactful practice is writing more code before compiling. Each compile cycle costs 10-30 seconds; eliminating 10 unnecessary cycles saves 2-5 minutes per task.
- Read before writing. Never modify a file you haven't read. The compiler errors from misunderstanding existing types cost more time than reading the file would have.
- Fix root causes, not symptoms. If the borrow checker rejects a pattern in 3 places, the API design is wrong — fix the signature, not the call sites.
- Keep the dependency tree shallow. Every new crate dependency adds compile time. Check if the standard library or an existing dependency already provides the functionality.
- Use the type system, don't fight it. If you're writing a lot of
.clone(),Rc, orunsafe, step back and reconsider the data ownership model. - Verify crate APIs before committing to them. The cost of discovering an API mismatch after writing 10 handlers is far higher than testing one minimal example first. This applies especially to template engines, integration crates, and anything with macro-based DSLs.
- Domain bugs cost more than type bugs. The compiler catches type errors, borrow errors, and lifetime issues. It cannot catch wrong formulas, incorrect sign conventions, or numerical edge cases. Invest verification effort proportional to the risk: domain-critical code needs golden-value tests, not just
cargo check. - Split
lib.rsfrommain.rsby default. This is a one-time structural decision that enables integration testing, benchmarking, and reuse. Do it at project creation, not as a refactor later. - Respect the user's CLAUDE.md. The user's global instructions override defaults. Check for project-specific conventions before applying generic Rust patterns.
Other
- Some AI LLMs suggest changing the Rust edition in Cargo.toml to 2021 from 2024. Their reasoning is mistaken: they are not up to date.The latest Rust edition is 2024 and Cargo.toml files with this should not be changed.