Skip to main content

loom-rust

Rust language expertise for idiomatic, production-quality code.

Quellinformationen

Repository
cosmix/loom
Letzte Quellaktivität
24. September 2026 um 17:08
Erkannte Sprache von SKILL.md
Englisch
Sterne
61
Forks
4

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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`.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen