Rust-specific readability guidance for the carmack agent including: - let...else example for shallow control flow - Async: don't block the runtime (tokio::fs, spawn_blocking, Send) - Visibility: prefer pub(crate), private fields with accessors - Generics: impl Trait over explicit params, avoid complex where clauses - Improved iterator guidance: if you need a comment, use a loop - UTF-8 string slicing warnings - Ownership/lifetime pragmatism - Anti-patterns: no macros/typestate/proc-macros unless already in repo Also adds Rust detection to LANGUAGE_PROMPTS (empty base prompt, agent-specific prompts handle the guidance).
3.1 KiB
3.1 KiB
Prefer obvious, readable Rust over cleverness.
Keep control flow shallow
let Some(user) = get_user(id) else {
return Err(anyhow!("user not found"));
};
- Prefer early returns (
return,?) over deep nesting. - Use
let ... else { ... }to reduce indentation. - Prefer
matchwhen it clarifies exhaustiveness; preferif letfor single-arm cases.
Name things for humans
- Use concrete, intention-revealing names:
bytes,chars,graphemes(don't call everythingdata)request,response,state,config,opts*_path,*_dir,*_id,*_idx(be explicit about units)
- If something is a char index,
*_char_idx, etc.
Make invariants explicit in types when cheap
- Prefer newtypes for confusing primitives (
UserId,SessionId) when it reduces mistakes. - Prefer enums over magic strings/ints for state.
Prefer small helper functions over mega-blocks
- Extract helpers when a block:
- has >1 responsibility
- has nested matches/loops
- repeats subtle conditions
- Helpers should have crisp names and minimal parameter lists.
Error handling: clarity > cleverness
- Avoid
unwrap()/expect()in production paths. - Use
anyhow/thiserrorpatterns already present in the repo; don't introduce new error stacks casually. - Add context where it matters (
.context("...")) but don't spray context everywhere.
Iterators vs loops
- Prefer a
forloop when it's clearer than an iterator chain. - If you need a comment to explain an iterator chain, use a loop instead.
- Use
collect::<Result<Vec<_>, _>>()?when it's idiomatic AND readable; otherwise a loop withpushis fine.
Strings are UTF-8: do not do byte slicing
- Never slice
String/&strusing byte indices (s[a..b]) unless you can prove ASCII. - Prefer:
char_indices()for char-aware operationsunicode-segmentationgraphemes when user-perceived characters matter- helper functions in-repo (if present) for safe truncation/slicing
Ownership/lifetimes: avoid "lifetime gymnastics"
- Prefer owned values at module boundaries if lifetimes complicate readability.
- Avoid returning references tied to complex internal state unless there's a strong perf reason.
Async: don't block the runtime
- Never call blocking I/O (
std::fs,std::net) in async functions withoutspawn_blocking. - Prefer
tokio::fsoverstd::fsin async contexts. - Keep futures
Sendunless you have a specific reason not to.
Visibility: minimize pub surface
- Prefer
pub(crate)overpubfor internal APIs. - Keep struct fields private with accessor methods when invariants matter.
Generics: prefer simplicity
- Prefer
fn process(items: impl Iterator<Item = T>)overfn process<I: Iterator<Item = T>>(items: I)when there's only one generic parameter. - Avoid trait bounds that require reading three lines of
whereclauses.
What NOT to do
- Do not introduce macros or advanced patterns (typestate, proc-macros, async trait hacks) unless the repo already uses them and it clearly improves readability.
- Do not add
dbg!()calls in committed code. - Do not create "god files"—split by responsibility when a module grows unwieldy.