Skip to main content

loom-rust

Rust language expertise for idiomatic, production-quality code.

Datos de origen

Repositorio
cosmix/loom
Última actividad en el origen
24 de septiembre de 2026 a las 17:08
Idioma detectado de SKILL.md
inglés
Estrellas
61
Forks
4

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
loom-rust
description
Rust language expertise for idiomatic, production-quality code.
triggers
["rust","cargo","rustc","ownership","borrowing","lifetime","trait","impl","struct","enum","Result","Option","async","await","tokio","serde","clap","thiserror","anyhow","Arc","Mutex","RwLock","RefCell","Box","Rc","Vec","HashMap","HashSet","String","derive","macro"]
# Rust Language Expertise ## Overview Idiomatic, production-quality Rust for an engineer who already knows ownership/borrowing. This skill is decision rules, gotchas, and the traps that cost hours: async/`Send` correctness, error-handling architecture, serde pitfalls, and edition-2024 features. Assumes the borrow checker itself is not the problem — using it *well* is. ## Error Handling: anyhow vs thiserror **The decision rule:** will a caller ever `match` on the error to recover? → `thiserror` (concrete, matchable enum). Does the error only bubble up to be logged/reported? → `anyhow` (type-erased, context chains). **Never put `anyhow::Error` in a library's public API** — it erases the type and denies callers any recovery. Libraries expose `thiserror` enums; binaries/apps consume with `anyhow`. ```rust // Library boundary: thiserror. Each variant = a distinct, matchable failure mode. use thiserror::Error; #[derive(Error, Debug)] pub enum ParseError { #[error("IO error: {0}")] Io(#[from] std::io::Error), // #[from] gives `?` conversion for free #[error("invalid syntax at {line}:{column}: {message}")] Syntax { line: usize, column: usize, message: String }, #[error("unexpected token: expected {expected}, found {found}")] Unexpected { expected: String, found: String }, } ``` ```rust // Application code: anyhow. `?` unifies heterogeneous errors; .context adds a chain. use anyhow::{Context, Result, bail, ensure}; fn load_config(path: &str) -> Result<Config> { let content = std::fs::read_to_string(path) .with_context(|| format!("reading config: {path}"))?; // with_context: lazy, use for allocating msgs let config: Config = toml::from_str(&content).context("parsing config TOML")?; // context: eager literal ensure!(!config.name.is_empty(), "config name cannot be empty"); // returns Err on false if config.port == 0 { bail!("port must be non-zero"); } // early return with Err Ok(config) } ``` - `context` (eager, takes a value) vs `with_context(|| ...)` (lazy closure) — use `with_context` whenever the message allocates (`format!`), else you pay the cost on the success path too. - `#[from]` generates `From` for `?`; `#[error(transparent)]` forwards `Display`/`source` to the inner error (use for a pass-through variant). - Option→Result: `.ok_or_else(|| Error::NotFound(id.to_string()))?`. Prefer `ok_or_else` (lazy) over `ok_or` when the error allocates. - Collecting: `iter.map(f).collect::<Result<Vec<_>>>()` stops at the first `Err`; collect into `Vec<Result<_>>` to keep all outcomes. - **`thiserror` 2.0:** must be a **direct** dependency (not transitive); format strings dropped raw-identifier support (`{type}`, not `{r#type}`); field trait bounds no longer inferred when shadowed by a format arg. New: `no_std` via `default-features = false`, out-of-line `#[error(fmt = path)]`, per-variant `#[error(transparent)]`. Pin `thiserror = "2"`. ## Traits, Generics & Lifetimes ```rust // Accept the widest input, return the narrowest: Into<T> in bounds, From impls only. // std's blanket `impl<T, U: From<T>> Into<U> for T` means every From yields Into for free; // implementing Into manually does NOT grant the reverse From. So implement From, bound on Into. fn store(&mut self, id: impl Into<String>) { self.id = id.into(); } // where-clauses for readability on complex bounds fn merge<V>(a: impl IntoIterator<Item = V>, b: impl IntoIterator<Item = V>) -> Vec<V> where V: Ord { let mut v: Vec<V> = a.into_iter().chain(b).collect(); v.sort(); v.dedup(); v } // Blanket impls are legal ONLY when the trait or the type is local to your crate (orphan rule). trait Summary { fn summary(&self) -> String; } impl<T: std::fmt::Display> Summary for T { fn summary(&self) -> String { self.to_string() } } ``` - **Never implement `ToString`** — std ships `impl<T: Display + ?Sized> ToString for T`. A manual impl conflicts with std's and violates the orphan rule (E0119, won't compile). Implement `Display`; `to_string()` comes free. - **`impl Trait`/generics (static) vs `dyn Trait` (dynamic):** generics monomorphize — inlinable, but N copies bloat the binary if instantiated widely. `dyn Trait` is a vtable indirect call (no inlining) but one code path and heterogeneous collections (`Vec<Box<dyn Stage>>`). RPIT (`-> impl Trait`) avoids boxing but fixes one concrete type; `Box<dyn Trait>` allows runtime variation at an allocation. - **Implement `Deref`/`DerefMut` only for smart pointers.** Deref coercion is implicit and transitive; on a plain wrapper it silently exposes the target's entire API (name collisions, unpredictable surface). Write explicit delegation methods for wrappers. - **Name conversions by cost (API Guidelines C-CONV/C-GETTER):** `as_` = free borrow reinterpret (`str::as_bytes`); `to_` = expensive/allocating (`str::to_uppercase`); `into_` = consuming owned→owned (`String::into_bytes`). Getters take the field name with **no** `get_` prefix (`first()`, not `get_first()`). - **Lifetime elision:** `fn get(&self, k: &str) -> Option<&str>` desugars to output borrowing `&self`. Only annotate when multiple input lifetimes make the output ambiguous. - **Borrow-checker fights:** to mutate two struct fields at once, **destructure** rather than call `&mut self` methods: `let Self { a, b } = self;` gives independent borrows. For slices, `split_at_mut`. See also the `entry` API below. ## Iterators Prefer iterator chains over index loops: no bounds checks, no off-by-one, and the optimizer fuses them into tight loops. ```rust let emails: Vec<String> = users.into_iter() .filter(|u| u.active) .filter_map(|u| u.email) // yields T from Option<T>, dropping None .collect(); let (evens, odds): (Vec<_>, Vec<_>) = nums.iter().partition(|&&x| x % 2 == 0); let sum: i32 = nums.iter().sum(); let first_even = nums.iter().find(|&&x| x % 2 == 0); ``` - Use `.flatten()` on an iterator of `Option`/`Result`, not `.filter_map(|x| x)` — the identity closure is a `clippy::filter_map_identity` error under `-D warnings`. (`filter_map` with a *real* closure is idiomatic.) - `.collect::<Result<Vec<_>>>()` short-circuits on the first `Err`; `.collect::<Vec<Result<_>>>()` keeps every outcome. - Reach for `itertools` (`.chunks`, `.group_by`, `.dedup_by`, `.sorted`, `.unique`) before hand-rolling; `.try_fold` for early-exit accumulation. - Custom iterators: implement `Iterator::next` returning `Option<Self::Item>` — you get `map`/`filter`/`collect` etc. for free. ## Serde ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] struct Config { name: String, #[serde(default)] enabled: bool, // missing → Default #[serde(default = "default_timeout")] timeout: u64, // missing → fn value #[serde(skip_serializing_if = "Option::is_none")] description: Option<String>, #[serde(skip)] runtime_state: Option<String>, // never (de)serialized #[serde(rename = "userId")] user_id: String, #[serde(flatten)] extra: std::collections::HashMap<String, String>, // absorbs unknown keys } fn default_timeout() -> u64 { 30 } #[derive(Serialize, Deserialize)] #[serde(rename_all = "snake_case")] // enum variants / fields in one shot enum Status { Active, Inactive, Pending } ``` **Enum tagging — pick deliberately, it defines the wire format:** | Representation | Attribute | Wire shape | | --- | --- | --- | | Externally tagged (default) | *(none)* | `{"Text": {"content": "..."}}` | | Internally tagged | `#[serde(tag = "type")]` | `{"type": "Text", "content": "..."}` | | Adjacently tagged | `#[serde(tag = "t", content = "c")]` | `{"t": "Text", "c": {...}}` | | Untagged | `#[serde(untagged)]` | bare value, variant inferred | ⚠ **`#[serde(untagged)]` silently matches the FIRST variant that deserializes** — declaration order decides which variant wins. An earlier variant with an overlapping shape wins with no ambiguity error; failures produce the useless `data did not match any variant`; it is slow in index formats (bincode). Order variants **most-specific first** and add per-variant round-trip tests. Internally tagged also can't represent newtype-of-non-struct variants. Prefer a real tag whenever you control the format. - Custom (de)serialize: `#[serde(with = "module")]`, or `serialize_with`/`deserialize_with` per field; whole-type via `impl Serialize`/`Deserialize`. `chrono` ships helpers (`chrono::serde::ts_seconds`). - **`serde_yaml` is DEPRECATED** (archived at 0.9.34, 2024-03-25; its `yaml-rust` dep is RUSTSEC-2024-0320). The `serde_yml` fork has an unsoundness advisory (RUSTSEC-2025-0068, ≤0.0.12). Prefer TOML/JSON for Rust-native config; if YAML is mandatory use the maintained `yaml-rust2`, and verify the API (`serde-saphyr` does not implement serde's traits — not a drop-in). ## Async & Concurrency (Tokio) ```rust use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; // std::sync::Mutex for state NOT held across .await (cheaper); tokio's async Mutex/RwLock // ONLY when the guard genuinely must span an await point. struct AppState { counter: Arc<std::sync::Mutex<u64>>, // never crosses .await cache: Arc<RwLock<std::collections::HashMap<String, String>>>, // crosses .await } impl AppState { fn increment(&self) -> u64 { let mut c = self.counter.lock().unwrap(); // std guard, dropped before any await *c += 1; *c } async fn get_cached(&self, key: &str) -> Option<String> { self.cache.read().await.get(key).cloned() // tokio RwLock held across await } } ``` ⚠ **Never hold a `std::sync::MutexGuard` across an `.await`.** The guard is `!Send`, so a future holding one is `!Send` and `tokio::spawn` on the multi-thread runtime rejects it at compile time. Worse: on a single task / `current_thread` it *compiles* but the worker blocks on the lock and can never poll the task that would release it — a **runtime deadlock the compiler does not catch**. Drop the guard in an explicit `{ }` scope before the await, or use `tokio::sync::Mutex` (guard is `Send`, at the cost of async locking) only when it must span the await. ⚠ **The same trap with `RefCell`:** a `Ref`/`RefMut` held across `.await` can hit a `BorrowMutError` **panic** when another task on the runtime re-enters the same cell. `RefCell` is `!Send` so it can't cross into a multi-thread `spawn` anyway — drop the borrow before awaiting. ⚠ **`tokio::spawn` requires `Send + 'static`.** The future must OWN its data (`async move { }`) — borrowed locals, `Rc`, `RefCell` break it. Subtle: a non-`Send` value created *and dropped* within an await-free span can still poison `Send` inference (auto-trait analysis spans the whole async block) — force an early drop with an explicit `{ }` scope. ⚠ **`tokio::select!` drops (cancels) losing branches at their suspension point** — most async ops are NOT cancel-safe, and any state in the loser's future-local vars is silently lost each loop iteration. - **NOT cancel-safe:** `read_exact`, `read_to_end`, `write_all`, `Mutex::lock`, `RwLock::read/write`, `Semaphore::acquire`. - **Cancel-safe:** `mpsc::Receiver::recv`, `TcpListener::accept`, `AsyncReadExt::read` (returns partial). - Put only cancel-safe ops directly in `select!` branches; store resumable state in a struct field, not a future-local. ```rust tokio::select! { msg = rx.recv() => { /* recv is cancel-safe */ } _ = shutdown.cancelled() => return, } ``` ⚠ **`spawn_blocking` is for blocking I/O, not CPU work at scale.** It runs on a separate blocking pool (default max 512, `Builder::max_blocking_threads`). Past the limit calls silently queue; a running blocking task **cannot be aborted**. For CPU-bound work use `rayon` and bridge back via a `oneshot`.
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub