All skills
massimodeluisa avatar

/rust-best-practices

@d521b51

Write and review production Rust using the official API Guidelines, Style Guide, and this engineering standard: allocation contracts, ownership, Result vs panic, debug_assert, Clippy, tests, rustdoc, unsafe, and Cargo CI. Use when writing, reviewing, or refactoring Rust; choosing borrow vs clone; designing crate APIs; handling errors; bounding heap use; configuring clippy or rustfmt; or when the user runs /rust-best-practices. Do not use for other languages. Triggers: rust, rustc, cargo, clippy, rustfmt, ownership, clone, borrow, Result, unwrap, expect, panic, thiserror, anyhow, heapless, no_std, allocation, debug_assert, type-state, Send, Sync, unsafe, FFI, rustdoc, MSRV, rust-best-practices, rust style, rust guidelines

Use this Skill: https://skilld.dev/gh/massimodeluisa/rust-best-practices-skill/rust-best-practices

This session only. Nothing lands on disk.

referencesallocation.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Allocation, bounds, and memory profiles

Every crate, module, and performance-sensitive public operation MUST declare one profile.

Profile Contract Typical use
Strict heapless No heap-backed storage. Prefer #![no_std]. Do not import alloc. Core runtimes, embedded, parsers and kernels that must prove no heap
Allocation-free steady state Init MAY allocate. The named operation and its complete transitive call graph perform zero allocation or reallocation after init Servers, reusable engines, per-request or per-token execution
Allocation-conscious Allocation only at explicit boundaries, justified, bounded where possible, measured when performance-sensitive CLI, compilers, build tools, admin services, offline analysis

Default for runtime, protocol, parser hot paths, deterministic kernels, and repeated request processing: strict heapless or allocation-free steady state.

What a claim actually means

These are different:

  • this function does not call Vec::new()
  • this function performs no allocation on the exercised path
  • this function and every transitive callee perform no allocation for all valid inputs
  • this subsystem never uses heap-backed storage

Only the last is a strict no-heap guarantee.

A function taking &[T] backed by a caller Vec<T> MAY itself be allocation-free while the system is not heapless. A preallocated Vec<T> MAY satisfy steady-state zero-allocation if it never grows; it still uses heap and is not strict heapless.

Core principles

Invalid states

Use the type system: newtypes for ids, offsets, lengths, units, scores, protocol values; enums instead of loosely related booleans or sentinel integers; Option<T> not magic values; Result<T, E> for recoverable failure; type-state when operations depend on lifecycle; fixed-width integers at serialization and FFI; private fields unless access is the contract.

#[repr(transparent)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NodeId(u32);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResolutionStatus {
    Exact,
    BackedOff,
    Unsupported,
}

Explicit control flow

Signatures MUST make visible: allocation, capacity limits, error behavior, ownership transfer, mutation, blocking vs async, determinism, panic conditions, thread-safety.

Avoid APIs whose correctness depends on undocumented global state, implicit init, hidden retries, lazy allocation, or environment-dependent defaults.

Bounded runtime

MUST bound: iterations, candidate counts, recursion depth, queue length, output size, temporary storage, retry count, concurrency, bytes read or written where practical.

On limit: typed error or explicit status. MUST NOT silently allocate more, recurse without a bound, grow an unbounded queue, or switch to an unbounded fallback.

Rich construction, lean execution

Compiler, CLI, migration, test, and offline-analysis code MAY use rich owned structures. Runtime SHOULD consume compact validated views and caller-owned state:

allocating authoring/compiler layer
    -> validates, normalizes, sorts, packs, serializes
immutable packed artifact
    -> borrowed by
a bounded, allocation-free runtime

MUST NOT deserialize a packed artifact into a heap-resident object graph merely for convenience when the runtime can borrow validated slices.

Reference implementation

Optimized implementations MUST stay behaviorally equivalent to a clear, safe reference path. Keep that path readable as: normative semantics, differential-testing oracle, portability fallback, property-test basis, reviewable spec for the optimized code.

Allocation is part of the API

Public runtime APIs SHOULD make allocation unnecessary and obvious from signatures.

Prefer: &T, &mut T, &[T], &mut [T], &str; caller-owned output and scratch; fixed-size arrays for genuinely small bounds; fixed-capacity containers that cannot spill; borrowed views into validated bytes; iterators instead of collected results; returned lengths, ranges, and status instead of owned collections; static or enum dispatch instead of boxed trait objects; small copyable error enums.

Avoid:

fn parse(input: String) -> Vec<Record>;

Prefer (heapless / allocation-free):

fn parse(input: &[u8], output: &mut [Record]) -> Result<usize, ParseError>;

Ordinary library APIs that are allocation-conscious SHOULD still return values (C-NO-OUT). Caller-owned buffers are the exception required by the memory profile, not a default for every crate.

Do not move unbounded work onto the stack

Avoiding the heap is not permission to place arbitrarily large arrays on every thread stack.

Large storage SHOULD be: supplied by the caller; static when lifetime and sync permit; a bounded arena with a documented lifecycle; memory-mapped; partitioned into bounded chunks; a reusable worker-owned scratch region.

Review stack use against the smallest supported thread stack. Avoid unbounded recursion. Recursion is acceptable only when depth is statically or structurally bounded and documented.

Capacity exhaustion MUST be explicit

A fixed-capacity structure MUST report exhaustion deterministically.

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CapacityError {
    Full { capacity: usize },
}

MUST NOT silently: spill into a heap allocation; discard existing entries; overwrite an unrelated entry; retry indefinitely; switch to an unbounded representation.

Output-buffer pattern

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FilterError {
    OutputTooSmall { required: usize, provided: usize },
}

/// Copies even values into `output` and returns the initialized prefix length.
///
/// # Allocation
///
/// This function performs no heap allocation.
pub fn retain_even(input: &[u32], output: &mut [u32]) -> Result<usize, FilterError> {
    let required = input.iter().filter(|value| **value & 1 == 0).count();
    if output.len() < required {
        return Err(FilterError::OutputTooSmall {
            required,
            provided: output.len(),
        });
    }
    debug_assert!(required <= output.len());
    let mut written = 0usize;
    for &value in input {
        if value & 1 == 0 {
            debug_assert!(written < output.len());
            output[written] = value;
            written += 1;
        }
    }
    debug_assert_eq!(written, required);
    Ok(written)
}

The capacity branch is required in all builds (caller-controlled input). Later debug_assert! checks internal invariants already established by that control flow.

Hidden allocation to review

Forbidden in strict heapless code. Also forbidden in an allocation-free operation unless constructed outside it and the exercised path is proven not to grow, allocate, or reallocate:

  • Vec<T>, String, Box<T>, Rc<T>, Arc<T>, PathBuf, OsString
  • HashMap, HashSet, BTreeMap, BTreeSet, VecDeque, BinaryHeap
  • Box::new, Box::pin, Rc::new, Arc::new
  • vec![], format!(), to_vec(), allocating to_owned(), to_string()
  • collect::<Vec<_>>(), collect::<String>(), equivalent owned collections
  • growth through push, insert, extend, reserve, or implicit reallocation
  • boxed trait objects, boxed iterators, boxed futures
  • Cow::into_owned() and APIs that silently go from borrowed to owned
  • spill-capable "small" containers unless spilling is structurally impossible
  • lazy init that creates owned heap data on first use
  • logging, tracing, metrics, serialization, backtrace, and error-context paths that have not been allocation-audited
  • callbacks, trait methods, FFI, or third-party code whose transitive behavior is unknown

Syntax does not prove allocation. Iterators, closures, formatting arguments, trait calls, and async fn are not inherently allocating; a specific adapter, receiver, executor, or impl may be. Review the complete call graph.

Formatting without owned strings

format_args! builds borrowed formatting arguments. The destination decides whether formatting allocates.

use core::fmt::{self, Write as _};

pub fn write_record(sink: &mut impl fmt::Write, id: u32, score: i32) -> fmt::Result {
    write!(sink, "id={id} score={score}")
}

Allocation-free only when the sink is. A String sink may grow; a fixed-capacity sink SHOULD return fmt::Error when full.

Avoid owned diagnostic strings in runtime code. Defer rich formatting to a higher-level adapter after the core returns a typed error.

Sorting and selection

Where unstable order is acceptable, sort_unstable* and select_nth_unstable* are in-place and do not allocate. Do not drop a stability requirement merely to avoid allocation. Define required semantics, then choose or implement a bounded algorithm that matches them.

If deterministic output matters, define: total ordering, tie-breaking, equal keys, architecture-independent integers, canonical output order.

Heapless errors at the core boundary

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParseError {
    OffsetOutOfBounds { offset: usize, input_len: usize },
    OutputTooSmall { required: usize, provided: usize },
    CapacityExceeded { capacity: usize },
    IntegerOverflow,
}

Implement Display by writing to the formatter. Do not store a preformatted String for context. Application code MAY translate a core error into a richer allocating diagnostic after crossing the allocation-free boundary.

Isolate strict code structurally

A strict core crate SHOULD start from:

#![no_std]
#![forbid(unsafe_code)]
#![deny(clippy::disallowed_macros)]
#![deny(clippy::disallowed_types)]

Do not add extern crate alloc to a crate claiming strict no-heap behavior. Filesystem, network, CLI, telemetry, and rich diagnostics belong in adapter crates.

crates/
  project-core/       # no_std, no alloc, bounded algorithms
  project-format/     # packed types and validation
  project-runtime/    # allocation-free repeated execution
  project-std/        # std adapters, I/O, threading, telemetry
  project-cli/        # allocating application boundary

Clippy guardrails for disallowed types: quality.md.

Source: SKILL.md on GitHub

No alerts15d3 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    The skill provides a comprehensive set of Rust engineering standards and best practices for writing and reviewing code. It covers memory allocation, API design, error handling, and performance optimization. No malicious behavior or security risks were identified.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

Signed by skilld at d521b51. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 weeks ago
Other metadata
metadata
{
  "author": "massimodeluisa",
  "version": "1.0.0",
  "website": "https://www.rust-lang.org/"
}

README badge

README badge for massimodeluisa/rust-best-practices-skill