Rust Coding
User-level fallback for Rust style and discipline. Apply when working in a
Rust repository that does not provide its own rust-coding-skill (or
equivalent). When a repo-local Rust skill exists, prefer it — it carries
project-specific conventions this skill cannot.
A starter template that mirrors this content with extension stubs for
project-specific rules lives at
copilot/templates/rust-coding-skill/SKILL.md in tclem/dotfiles. Use
the template when bootstrapping a new repo's Rust skill; use this skill
when no repo skill exists yet.
When to use
Use this when changing or reviewing Rust code in any repo that does not
provide a narrower repo-local Rust skill.
Do not use this to override repository style guides, generated code
conventions, or project-specific API patterns.
Priority hierarchy
From most important to least important:
- Readable code — every line should earn its place.
- Correct code — especially in concurrent and async contexts.
- Performant code — think about allocations, data structures, hot paths.
Error handling
thiserror for typed error enums at module and library boundaries. Name
variants by what the caller should do (ShardNotFound, AuthRequired,
IndexCorrupted), not by where the error came from (DbError, ParseError,
HttpError).
anyhow only at the binary boundary — top-level handlers, CLI entry
points, server commands. Never in library code.
- Avoid
Box<dyn std::error::Error + Send + Sync> as a return type. It
erases type information, prevents matching on specific failures, and produces
opaque messages with no causal chain. Use a thiserror enum.
- No
unwrap() in production code. Use ? to propagate, let-else for
early returns, if let / match for branches, or expect("specific invariant") for truly infallible cases. unwrap() is for tests.
- Don't write
is_none() / is_err() followed by unwrap() — use if let or match. The separate check is redundant and easy to desync.
- Destructure multiple
Options together: let (Some(a), Some(b)) = (x, y) else { return Err(...); };.
panic! only for unrecoverable invariants where the whole application is
broken. Not for localized failures.
#[error(transparent)] for wrapped errors that should pass through
unchanged.
- Prefer errors over
Option when absence indicates a non-recoverable
violation rather than an expected missing value.
Async and concurrency
- Prefer awaited futures over spawning tasks. Futures get cancelled with
the parent and can borrow locals. Spawn only when work must outlive the
caller, then track the
JoinHandle or document the lifecycle owner.
- Rayon for CPU-bound parallelism, Tokio for async I/O fan-out.
- Blocking sync I/O:
tokio::task::spawn_blocking for short operations
(the blocking pool is bounded — don't saturate it with multi-second work).
Long-lived workers use a dedicated thread.
- Never hold a lock across
.await. Use channels (tokio::sync::mpsc /
oneshot) or restructure so the critical section finishes before the await.
- Lock choice:
parking_lot::Mutex — common default; short critical sections that never
touch .await.
std::sync::Mutex — use when you want poisoning for invariant-critical
state.
tokio::sync::Mutex — last resort, only when the lock genuinely must be
held across .await.
std::sync::LazyLock for lazy statics (stable since Rust 1.80). Do not
introduce once_cell::Lazy in new code.
- No module-level mutable statics. Make state a field on the owning
struct and thread
&self through. Immutable LazyLock<Regex> /
LazyLock<Config> are fine.
- Bound fan-out:
stream::iter(...).buffer_unordered(N) over unbounded
join_all on dynamic collections.
- Fix large futures directly. Hoist join branches into
Box::pin locals;
don't #[allow] clippy::large_futures and don't raise thread stack size
as a workaround.
Traits and conversions
- Avoid custom traits when possible. They break code navigation. Prefer
plain functions on the concrete type. Standard derives (
Iterator,
Future, Display, Debug, Clone, Default, PartialEq, Hash,
Serialize/Deserialize, thiserror::Error) are fine. Real extension
seams where the consumer must be pluggable are also fine — document why.
- No
From/Into for contextual conversions. Prefer named methods:
to_record, into_parts, from_row, as_string. From/Into can't
carry context, can't fail expressively, and produce un-navigable call
sites where the target type is implicit.
- Iterators replace visitor patterns.
- Closures over ~10 lines become named functions. Anonymous closures are
invisible in stack traces and hard to navigate.
- Avoid multiple generic
F: Fn(...) parameters.
Tracing and logging
Static messages with structured fields, never interpolated dynamic data:
// Good
tracing::info!(shard_id = %id, path = %path.display(), "shard indexed");
// Bad
tracing::info!("shard {} indexed at {}", id, path.display());
Prefer manual error_span! over #[tracing::instrument]. The attribute
is easy to misuse: wrong level by default, captures all args, hides span
lifetime from readers.
Default to error_span!, not info_span!. Span level controls the
minimum filter at which the span is recorded. Under a warn/error
filter, an info_span is dropped, taking its fields and parent context
with it.
Never hold a span.enter() guard across .await. Use
.instrument(span) instead.
Testing
- Avoid mock testing. Depend on real implementations, spin up lightweight
versions, or split side-effectful functions into a pure core (takes values)
plus a thin wrapper (fetches them).
assert_eq!(actual, expected) — actual first for readable diffs.
unwrap() is fine in tests — not in production.
- Tests must run concurrently. Unique test data, temp directories,
serial_test only when truly necessary.
- Mark slow or integration tests with
#[ignore].
Imports
All use statements at the top of the file. Never import items inline
within function bodies.
Prefer top-level imports over fully-qualified paths in expressions and
match arms.
One use per line, group items from the same module with braces:
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
Code organization
- One concept per file.
- Extract repeated patterns into helpers on the third usage. Don't DRY
prematurely — avoid extracting the wrong abstraction.
pub(crate) by default. Only pub when the API is truly public.
- Prefer
#[expect(...)] over #[allow(...)] for lint suppression.
Apply on individual fields, not whole structs.
- Reserve blank lines for semantic section boundaries — between
setup / execute / respond phases.
- Avoid trivial temporary
let bindings.
- Inline format args:
println!("{value:?}") not println!("{:?}", value).
- No
..Self::default() in production struct constructors. Explicitly
list all fields so adding a new field causes a compile error at every
construction site.
Pattern matching
- Prefer exhaustive matches over
_ wildcards. When an enum gains a
variant, a wildcard arm silently swallows it. Exhaustive matches force
every call site to handle the new case.
Build speed
- Minimize dependency feature flags. Audit with
cargo tree; find unused
deps with cargo machete.
- Iterate with
cargo check, not cargo build.
Comments
Comments explain why, never what. Engineers can read code. Decision
test before writing any comment: "If I delete this, what is lost that can't
be recovered by reading the code, types, names, and one navigation jump?"
If the answer is "nothing," don't write it.
Anti-patterns: doc comments enumerating function body, block comments that
paraphrase the next line, "this is used by X" pointers, flow-narrating
bullets inside a function.
Common mistakes
- Treating this skill as permission to reformat or rewrite unrelated Rust.
Apply the smallest coherent change that improves the code you are touching.
- Adding a trait because several types have similar methods. Start with
functions and concrete types; introduce a trait only when callers need
polymorphism.
- Wrapping errors in strings too early. Preserve typed errors inside
libraries; add human context at the application boundary.
- Spawning a task to solve ownership or lifetime friction. Prefer
restructuring so the parent future owns cancellation and error propagation.
- Adding defensive fallbacks for impossible states. Encode the invariant in
types or return a real error.
- Loading this skill instead of a repo-local rust-coding-skill. If the repo
has one, that one wins — its project-specific rules trump anything here.
Source: tclem/dotfiles — distributed by TomeVault.
1---2name: rust-coding3description: Use when editing or reviewing Rust files (*.rs) and the repository has no rust-coding skill of its own.4---56# Rust Coding78User-level fallback for Rust style and discipline. Apply when working in a9Rust repository that does not provide its own `rust-coding-skill` (or10equivalent). When a repo-local Rust skill exists, prefer it — it carries11project-specific conventions this skill cannot.1213A starter template that mirrors this content with extension stubs for14project-specific rules lives at15`copilot/templates/rust-coding-skill/SKILL.md` in `tclem/dotfiles`. Use16the template when bootstrapping a new repo's Rust skill; use this skill17when no repo skill exists yet.1819## When to use2021Use this when changing or reviewing Rust code in any repo that does not22provide a narrower repo-local Rust skill.2324Do not use this to override repository style guides, generated code25conventions, or project-specific API patterns.2627## Priority hierarchy2829From most important to least important:30311. **Readable code** — every line should earn its place.322. **Correct code** — especially in concurrent and async contexts.333. **Performant code** — think about allocations, data structures, hot paths.3435## Error handling3637- **`thiserror` for typed error enums** at module and library boundaries. Name38 variants by what the caller should do (`ShardNotFound`, `AuthRequired`,39 `IndexCorrupted`), not by where the error came from (`DbError`, `ParseError`,40 `HttpError`).41- **`anyhow` only at the binary boundary** — top-level handlers, CLI entry42 points, server commands. Never in library code.43- **Avoid `Box<dyn std::error::Error + Send + Sync>` as a return type.** It44 erases type information, prevents matching on specific failures, and produces45 opaque messages with no causal chain. Use a `thiserror` enum.46- **No `unwrap()` in production code.** Use `?` to propagate, `let-else` for47 early returns, `if let` / `match` for branches, or `expect("specific48 invariant")` for truly infallible cases. `unwrap()` is for tests.49- **Don't write `is_none()` / `is_err()` followed by `unwrap()`** — use `if50 let` or `match`. The separate check is redundant and easy to desync.51- **Destructure multiple `Option`s together:** `let (Some(a), Some(b)) = (x, y)52 else { return Err(...); };`.53- **`panic!` only for unrecoverable invariants** where the whole application is54 broken. Not for localized failures.55- **`#[error(transparent)]`** for wrapped errors that should pass through56 unchanged.57- **Prefer errors over `Option`** when absence indicates a non-recoverable58 violation rather than an expected missing value.5960## Async and concurrency6162- **Prefer awaited futures over spawning tasks.** Futures get cancelled with63 the parent and can borrow locals. Spawn only when work must outlive the64 caller, then track the `JoinHandle` or document the lifecycle owner.65- **Rayon for CPU-bound parallelism, Tokio for async I/O fan-out.**66- **Blocking sync I/O:** `tokio::task::spawn_blocking` for short operations67 (the blocking pool is bounded — don't saturate it with multi-second work).68 Long-lived workers use a dedicated thread.69- **Never hold a lock across `.await`.** Use channels (`tokio::sync::mpsc` /70 `oneshot`) or restructure so the critical section finishes before the await.71- **Lock choice:**72 - `parking_lot::Mutex` — common default; short critical sections that never73 touch `.await`.74 - `std::sync::Mutex` — use when you want poisoning for invariant-critical75 state.76 - `tokio::sync::Mutex` — last resort, only when the lock genuinely must be77 held across `.await`.78- **`std::sync::LazyLock` for lazy statics** (stable since Rust 1.80). Do not79 introduce `once_cell::Lazy` in new code.80- **No module-level mutable statics.** Make state a field on the owning81 struct and thread `&self` through. Immutable `LazyLock<Regex>` /82 `LazyLock<Config>` are fine.83- **Bound fan-out:** `stream::iter(...).buffer_unordered(N)` over unbounded84 `join_all` on dynamic collections.85- **Fix large futures directly.** Hoist join branches into `Box::pin` locals;86 don't `#[allow]` `clippy::large_futures` and don't raise thread stack size87 as a workaround.8889## Traits and conversions9091- **Avoid custom traits when possible.** They break code navigation. Prefer92 plain functions on the concrete type. Standard derives (`Iterator`,93 `Future`, `Display`, `Debug`, `Clone`, `Default`, `PartialEq`, `Hash`,94 `Serialize`/`Deserialize`, `thiserror::Error`) are fine. Real extension95 seams where the consumer must be pluggable are also fine — document why.96- **No `From`/`Into` for contextual conversions.** Prefer named methods:97 `to_record`, `into_parts`, `from_row`, `as_string`. `From`/`Into` can't98 carry context, can't fail expressively, and produce un-navigable call99 sites where the target type is implicit.100- **Iterators replace visitor patterns.**101- **Closures over ~10 lines become named functions.** Anonymous closures are102 invisible in stack traces and hard to navigate.103- **Avoid multiple generic `F: Fn(...)` parameters.**104105## Tracing and logging106107- **Static messages with structured fields**, never interpolated dynamic data:108109 ```rust110 // Good111 tracing::info!(shard_id = %id, path = %path.display(), "shard indexed");112 // Bad113 tracing::info!("shard {} indexed at {}", id, path.display());114 ```115116- **Prefer manual `error_span!` over `#[tracing::instrument]`.** The attribute117 is easy to misuse: wrong level by default, captures all args, hides span118 lifetime from readers.119- **Default to `error_span!`, not `info_span!`.** Span level controls the120 minimum filter at which the span is recorded. Under a `warn`/`error`121 filter, an `info_span` is dropped, taking its fields and parent context122 with it.123- **Never hold a `span.enter()` guard across `.await`.** Use124 `.instrument(span)` instead.125126## Testing127128- **Avoid mock testing.** Depend on real implementations, spin up lightweight129 versions, or split side-effectful functions into a pure core (takes values)130 plus a thin wrapper (fetches them).131- **`assert_eq!(actual, expected)`** — actual first for readable diffs.132- **`unwrap()` is fine in tests** — not in production.133- **Tests must run concurrently.** Unique test data, temp directories,134 `serial_test` only when truly necessary.135- **Mark slow or integration tests with `#[ignore]`.**136137## Imports138139- **All `use` statements at the top of the file.** Never import items inline140 within function bodies.141- **Prefer top-level imports over fully-qualified paths** in expressions and142 match arms.143- **One `use` per line, group items from the same module with braces:**144145 ```rust146 use std::collections::{HashMap, HashSet};147 use std::sync::Arc;148 ```149150## Code organization151152- **One concept per file.**153- **Extract repeated patterns into helpers on the third usage.** Don't DRY154 prematurely — avoid extracting the wrong abstraction.155- **`pub(crate)` by default.** Only `pub` when the API is truly public.156- **Prefer `#[expect(...)]` over `#[allow(...)]`** for lint suppression.157 Apply on individual fields, not whole structs.158- **Reserve blank lines for semantic section boundaries** — between159 setup / execute / respond phases.160- **Avoid trivial temporary `let` bindings.**161- **Inline format args:** `println!("{value:?}")` not `println!("{:?}", value)`.162- **No `..Self::default()` in production struct constructors.** Explicitly163 list all fields so adding a new field causes a compile error at every164 construction site.165166## Pattern matching167168- **Prefer exhaustive matches over `_` wildcards.** When an enum gains a169 variant, a wildcard arm silently swallows it. Exhaustive matches force170 every call site to handle the new case.171172## Build speed173174- **Minimize dependency feature flags.** Audit with `cargo tree`; find unused175 deps with `cargo machete`.176- **Iterate with `cargo check`**, not `cargo build`.177178## Comments179180Comments explain **why**, never **what**. Engineers can read code. Decision181test before writing any comment: "If I delete this, what is lost that can't182be recovered by reading the code, types, names, and one navigation jump?"183If the answer is "nothing," don't write it.184185Anti-patterns: doc comments enumerating function body, block comments that186paraphrase the next line, "this is used by X" pointers, flow-narrating187bullets inside a function.188189## Common mistakes190191- Treating this skill as permission to reformat or rewrite unrelated Rust.192 Apply the smallest coherent change that improves the code you are touching.193- Adding a trait because several types have similar methods. Start with194 functions and concrete types; introduce a trait only when callers need195 polymorphism.196- Wrapping errors in strings too early. Preserve typed errors inside197 libraries; add human context at the application boundary.198- Spawning a task to solve ownership or lifetime friction. Prefer199 restructuring so the parent future owns cancellation and error propagation.200- Adding defensive fallbacks for impossible states. Encode the invariant in201 types or return a real error.202- Loading this skill instead of a repo-local rust-coding-skill. If the repo203 has one, that one wins — its project-specific rules trump anything here.204205---206> Source: [tclem/dotfiles](https://github.com/tclem/dotfiles) — distributed by [TomeVault](https://tomevault.io).207<!-- tomevault:4.0:skill_md:2026-05-23 -->