This Rust Architecture Guideline (RAG) provides a comprehensive set of rules and best practices for developing safe, performant, and maintainable Rust applications. AI systems (e.g., Cursor, Gemini) must internalize this spec, perform mental checklists after code blocks, and prioritize Rust's strengths to produce optimal code.
- Safety First: Leverage Rust's ownership, borrowing, and type system to eliminate entire classes of bugs (null pointer dereferences, data races, use-after-frees) at compile time. Safety is not optional.
- Performance by Design: Apply zero-cost abstractions, prefer stack allocation over heap allocation, use efficient data structures, and leverage modern concurrency patterns and hardware capabilities (SIMD) where appropriate. Performance should be idiomatic, not an afterthought.
- Clarity and Maintainability: Organize code by domain, document all public APIs thoroughly with runnable examples, write comprehensive tests, and favor readability. Code is read far more often than it is written.
- Ergonomic API Design: Build APIs that are difficult to misuse. Leverage the type system to make impossible states unrepresentable and guide users toward correct usage.
- AI Collaboration: Actively guide and correct AI-generated code to adhere to these principles. Focus on verifying ownership semantics, error handling robustness, lifetime correctness, and the justification for any
unsafecode. - Embrace Async for I/O-Bound Work: Use
async/awaitand a chosen runtime (like Tokio) for non-blocking concurrency to build highly scalable network services and applications. - Manage Dependencies Wisely: Minimize the dependency graph to reduce compile times and security surface. Audit all dependencies for vulnerabilities and excessive use of
unsafecode.
When in doubt, consult the official Rust documentation, this guideline, and the Rustonomicon for unsafe code. AI must avoid pitfalls, justify deviations, and reference this guideline in comments if needed.
- Ownership Fundamentals
- Borrowing Rules
- Lifetimes
- Memory Safety
- Error Handling
- Concurrency
- Async Programming
- Type System
- Traits and Generics
- Macros and Metaprogramming
- Unsafe Rust
- Unsafe Rust and FFI
- AI-Specific Guidelines
- Common Anti-Patterns
- Module Organization
- Performance Optimizations
- Best Practices
- Testing Strategies
- Testing Principles
- Dependency Management
- Security Best Practices
- Build, CI, and Tooling
- Migration and Maintenance
- Checklist
- Expected Benefits
- Every value has a single variable that is its owner. When the owner goes out of scope, the value is dropped, and its memory is freed. This is the core of Rust's RAII (Resource Acquisition Is Initialization) pattern.
- Ownership is transferred on assignment, function calls, or returns. This is a "move."
- AI Note: Critically trace ownership paths. In loops, do not move a value that is needed in a subsequent iteration. The AI should prefer borrowing within loops or iterating over references.
fn process(data: Vec<i32>) {
// data owned here
}
let numbers = vec![1, 2, 3];
process(numbers); // Moved- Prefer passing references (
&T,&mut T) to avoid expensive deep copies. - Implement the
Copytrait for small, plain-old-data (POD) types that can be duplicated with a cheap bitwise copy.Copyis a superset ofClone. - Use
.clone()only when you explicitly need to duplicate data and create a new owner. Profile code in hot paths before liberally adding clones. - AI Note: Justify every single
.clone()call. Ask: "Can I use a borrow? Can I useArcfor shared ownership? Am I cloning inside a hot loop?"
fn len(s: &str) -> usize { s.len() }
let s = String::from("hello");
let length = len(&s); // Borrow, no clone- Moving large data structures (like a
Vec<T>) is extremely cheap, as it only involves copying a few stack-allocated pointers and metadata, not the heap data itself. - Design functions to take ownership of large data structures when they need to consume or transform them. Use traits like
IntoIteratorto be generic over owned values and references.
fn process_large(data: Vec<LargeStruct>) {
// Process data
}
let data = create_large_data();
process_large_data(data); // Efficient move- This rule is checked at compile time and prevents data races. You cannot have both at the same time in the same scope.
- A mutable borrow is exclusive. While it exists, no other borrows of the object are allowed.
- Note on Non-Lexical Lifetimes (NLL): The borrow checker is smart. A borrow ends when the reference is last used, not necessarily at the end of the lexical scope (
}).
let mut x = 5;
let y = &mut x; // Mutable borrow starts
*y += 1;
// Mutable borrow y is last used here, so it ends.
println!("The value of x is: {}", x); // Now we can immutably borrow x again.- The compiler can infer lifetimes in many situations (lifetime elision), but you must be explicit when reference relationships are not obvious.
- Rule of thumb: If a function takes multiple references as input and returns one, you must annotate lifetimes to tell the compiler which input reference the output reference is tied to.
- Use
'staticsparingly. It means the reference is valid for the entire duration of the program. It's common for string literals or global constants. - AI Note: When generating a function with reference inputs and outputs, double-check if lifetime annotations are required. If the compiler complains, add the necessary
'a,'b, etc. annotations.
fn process_slice(slice: &[i32]) {
// Process slice
}
let vec = vec![1, 2, 3];
let first = &vec[0];
process_slice(&vec);
println!("{}", first);- If a struct holds a reference to data it does not own, its definition must be annotated with a lifetime parameter. This ensures that no instance of the struct can outlive the data it refers to.
// This Context holds a reference to some string data.
// The 'a ensures the Context cannot outlive the data.
struct Context<'a> {
data: &'a str,
}- Use 'static sparingly
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}- Annotate if >1 input ref
fn get_first<'a>(s: &'a str) -> &'a str {
&s[0..1]
}struct Context<'a> {
data: &'a str,
}- For closures or traits with refs
fn for_any_lifetime<F>(f: F) where F: for<'a> FnOnce(&'a str) {}- Rust does not have null pointers. For values that can be absent, use the
Option<T>enum, which can beSome(T)orNone. - This forces you to handle the
Nonecase at compile time, preventing null pointer dereferences.
fn find_item(haystack: &[i32], needle: i32) -> Option<usize> {
haystack.iter().position(|&x| x == needle)
}- The
Droptrait is Rust's equivalent of a destructor. ImplementDropfor any type that needs to perform cleanup (e.g., closing a file, releasing a lock, closing a network connection). The compiler will automatically insert calls todropwhen the value goes out of scope.
fn as_string() -> String {
String::from("hello")
}Box<T>: For heap allocation of a single value. Provides unique ownership.Rc<T>: For single-threaded shared ownership. Uses reference counting.Arc<T>: For thread-safe shared ownership. Uses Atomic Reference Counting. It is the thread-safe equivalent ofRc<T>.RefCell<T>: For enforcing borrowing rules at runtime instead of compile time (interior mutability) in a single-threaded context. A panic will occur if borrowing rules are violated.Mutex<T>/RwLock<T>: For thread-safe interior mutability.Mutexprovides exclusive access, whileRwLockallows multiple readers or one writer, making it more performant for read-heavy workloads.
let shared = Arc::new(data);- If an operation can fail but the failure is expected and handleable, the function must return
Result<T, E>. - Use the
?operator for clean, concise error propagation. It unwraps aResultor returns theErrvariant from the function early. - AI Note: Never generate code that calls
.unwrap()or.expect()onResultorOptiontypes in production logic. These should only be used in tests, examples, or in cases where a panic is the absolutely correct and desired outcome (e.g., a critical, unrecoverable initialization failure).
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}- For libraries, define custom error enums that represent all possible failure modes.
- Use the
thiserrorcrate to easily derivestd::error::Errorand create rich, expressive error types with source information. This is ideal for libraries. - For applications, use the
anyhowcrate for simple, flexible error handling with context and backtraces. It is excellent for the top layers of an application (main.rs, request handlers, etc.).
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Network error: {0}")]
NetworkError(#[from] reqwest::Error),
}panic!should be reserved for truly exceptional circumstances that indicate a bug in the program (e.g., a violated invariant, an out-of-bounds access on an index you thought was safe). A panic unwinds the stack and crashes the thread.
let value = map.get("key").ok_or_else(|| AppError::KeyMissing("key".to_string()))?;- Use eyre for stack traces
fn func() -> eyre::Result<()> {
// Implementation
Ok(())
}- Prefer channels (mpsc)
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("Hello from thread").unwrap();
});
println!("{}", rx.recv().unwrap());- Prefer RwLock for read-heavy
use std::sync::{Arc, Mutex, RwLock};
let counter = Arc::new(Mutex::new(0));
let data = Arc::new(RwLock::new(0));- Use Sync bounds
fn process<T: Sync>(data: &T) {
// Thread-safe
}- Prefer Tokio or async-std
async fn fetch() -> Result<String, Error> {
reqwest::get("url").await?.text().await
}- Use tokio::spawn for CPU-bound
use tokio::task;
task::spawn_blocking(|| heavy_computation()).await?;- Use select! for racing
use tokio::select;
select! {
_ = task1() => {},
_ = task2() => {}
}- Wrap primitive types (like
i32orString) in a tuple struct to create a new, distinct type. This allows the compiler to enforce domain-specific invariants.
struct UserId(i32);
struct ProductId(String);
// This function cannot accidentally be called with a ProductId.
fn get_user_by_id(id: UserId) { /* ... */ }- Use enums and struct layouts to model your domain in a way that invalid states cannot even be created.
// BAD: Boolean blindness and invalid states are possible (e.g., connected AND error_message is Some)
struct Connection {
is_connected: bool,
is_connecting: bool,
since: Option<std::time::Instant>,
error_message: Option<String>,
}
// GOOD: Each state is distinct and holds only the data relevant to it.
enum ConnectionState {
Disconnected,
Connecting,
Connected { since: std::time::Instant },
Failed { error: String },
}use std::marker::PhantomData;
struct Degrees<T> {
value: f64,
phantom: PhantomData<T>,
}
struct Celsius;
struct Fahrenheit;
impl Degrees<Celsius> {
fn to_fahrenheit(&self) -> Degrees<Fahrenheit> {
Degrees {
value: self.value * 9.0/5.0 + 32.0,
phantom: PhantomData,
}
}
}- For related types
trait Graph {
type Node;
fn nodes(&self) -> Vec<Self::Node>;
}- Define behavior via traits
trait Drawable {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self) { /* ... */ }
}
impl Drawable for Rectangle {
fn draw(&self) { /* ... */ }
}
fn draw_all<T: Drawable>(shapes: &[T]) {
for shape in shapes {
shape.draw();
}
}fn draw_shapes(shapes: &Vec<Box<dyn Drawable>>) {
for shape in shapes {
shape.draw();
}
}- Use derive where possible
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}fn add(a: i32, b: i32) -> i32 {
a + b
}macro_rules! create_vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(temp_vec.push($x);)*
temp_vec
}
};
}use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
// Implementation
}- The
unsafekeyword does not turn off the borrow checker; it only allows five extra capabilities (dereferencing raw pointers, calling unsafe functions, etc.). - You, the programmer, are responsible for manually upholding Rust's safety invariants within an
unsafeblock. - Wrap
unsafeblocks in a safe abstraction. The goal is to have a safe public API, even if the internal implementation requires a small amount ofunsafefor performance or interoperability.
struct SafeVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> SafeVec<T> {
pub fn new() -> Self {
SafeVec {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
pub fn push(&mut self, item: T) {
// Safe implementation
}
}- Every
unsafeblock must be accompanied by a// SAFETY:comment that explains why the code is safe and what invariants must be upheld by the surrounding code for it to be correct. - AI Note: An AI must never generate
unsafecode without a corresponding// SAFETY:comment. It should also be challenged to provide a safe alternative first.
/// # Safety
/// 1. `ptr` is non-null and aligned
/// 2. `ptr` points to valid memory
/// 3. Memory not mutated elsewhere
unsafe fn read_memory(ptr: *const u8, len: usize) -> Vec<u8> {
// Implementation
}#[test]
fn test_safe_abstraction() {
let mut vec = SafeVec::new();
vec.push(42);
assert_eq!(vec.get(0), Some(&42));
}- Use
#[repr(C)]on structs passed across the FFI boundary to ensure a stable memory layout. - Use types from
std::os::raw(likec_char) or thelibccrate for C-compatible types. - Never panic across an FFI boundary. This is undefined behavior. Wrap FFI entry points in
std::panic::catch_unwindand return an error code instead.
#[repr(C)]
pub struct CStruct {
pub field: i32,
}
#[no_mangle]
pub extern "C" fn safe_ffi_function() -> i32 {
std::panic::catch_unwind(|| {
// Safe Rust code here
42
}).unwrap_or(-1) // Return error code on panic
}- Mental Checklist:
- Is
.clone()used where a borrow&would suffice? This is the most common AI error. - Is a value being moved inside a loop, causing a "use of moved value" error on the second iteration?
- Could
Arcbe used for more efficient shared ownership instead of deep cloning?
- Is
- Action: Instruct the AI to refactor to use borrows, iterators over references, or appropriate smart pointers.
fn process(data: Vec<i32>) -> Vec<i32> {
let processed = data.iter().map(|x| x * 2).collect();
processed
}- Mental Checklist:
- Is
.unwrap()or.expect()used on aResultorOption?
- Is
- Action: Immediately instruct the AI to replace it with proper error handling (
match,if let,?). Provide the context for a proper error type (thiserrorenum oranyhow::Result).
fn first_and_last(s: &str) -> (&str, &str) {
let first = &s[0..1];
let last = &s[s.len()-1..];
(first, last)
}- Mental Checklist:
- Is this
unsafeblock absolutely necessary? Can it be achieved with safe Rust? - Is there a
// SAFETY:comment explaining the invariants?
- Is this
- Action: Challenge the AI to provide a safe alternative. If
unsafeis required, ensure it is documented correctly and isolated in the smallest possible scope.
fn get_unchecked(vec: &Vec<i32>, index: usize) -> Option<&i32> {
if index < vec.len() {
Some(unsafe { vec.get_unchecked(index) })
} else {
None
}
}- Mental Checklist:
- Is the AI using C-style
for i in 0..len { vec[i] }loops instead of iterators? - Is it using boolean flags instead of enums for state?
- Is it using
Stringin function arguments where&strwould be more flexible?
- Is the AI using C-style
- Action: Instruct the AI to refactor using iterators (
.iter(),.map(),.filter()), enums, and appropriate string types (&str,Path,OsStr).
fn find<T: PartialEq>(vec: &Vec<T>, item: &T) -> Option<usize> {
vec.iter().position(|x| x == item)
}- Action: Always require the AI to generate
rustdoccomments (///) for all public functions, structs, and enums. These comments should include a brief explanation, sections for# Examples,# Panics, and# Errors. - Action: For any non-trivial function, instruct the AI to also generate a
#[cfg(test)] mod tests { ... }module with unit tests covering both success and failure cases. - Action: Instruct the AI to follow Test-Driven Development (TDD) principles, writing tests that validate real functionality rather than just trying to make tests pass.
- Action: Require the AI to avoid stubs and mocks except when absolutely necessary, preferring real implementations and real data in tests.
/// Adds two numbers together.
///
/// # Examples
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}struct Counter {
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
}- Redesign code to align with ownership
enum Color { Red, Green, Blue }
fn set_color(color: Color) { /* ... */ }let mut vec = vec![1, 2, 3, 4];
vec.retain(|&x| x % 2 != 0);- String literals are prone to typos and make refactoring difficult. They provide no type safety and can lead to runtime errors.
- Use enums, constants, or newtype patterns for domain-specific string values.
- This approach prevents accidental misspellings, enables compile-time checking, and improves code documentation.
// BAD: Stringly-typed API with literals
fn set_status(status: &str) -> Result<(), Error> {
match status {
"active" => { /* ... */ },
"inactive" => { /* ... */ },
"pending" => { /* ... */ },
_ => return Err(Error::InvalidStatus),
}
Ok(())
}
// GOOD: Type-safe enum
#[derive(Debug, Clone, Copy, PartialEq)]
enum Status {
Active,
Inactive,
Pending,
}
fn set_status(status: Status) -> Result<(), Error> {
match status {
Status::Active => { /* ... */ },
Status::Inactive => { /* ... */ },
Status::Pending => { /* ... */ },
}
Ok(())
}
// ALSO GOOD: Constants for fixed string values
const STATUS_ACTIVE: &str = "active";
const STATUS_INACTIVE: &str = "inactive";
const STATUS_PENDING: &str = "pending";
// BEST: Newtype pattern with validation
#[derive(Debug, Clone, PartialEq)]
struct Status(String);
impl Status {
pub fn active() -> Self { Status(STATUS_ACTIVE.to_string()) }
pub fn inactive() -> Self { Status(STATUS_INACTIVE.to_string()) }
pub fn pending() -> Self { Status(STATUS_PENDING.to_string()) }
pub fn from_str(s: &str) -> Result<Self, Error> {
match s {
STATUS_ACTIVE => Ok(Status::active()),
STATUS_INACTIVE => Ok(Status::inactive()),
STATUS_PENDING => Ok(Status::pending()),
_ => Err(Error::InvalidStatus),
}
}
}src/
├── core/
│ ├── error.rs
│ └── mod.rs
├── media/
│ ├── audio/
│ ├── video/
│ └── mod.rs
├── timeline/
│ ├── models.rs
│ └── operations.rs
└── lib.rsmod project {
pub mod settings;
pub mod resolution;
}
mod media {
pub mod info;
pub mod thumbnail;
}pub mod api {
pub fn public_function() { /* ... */ }
fn internal_helper() { /* ... */ }
}
mod internal {
pub(crate) fn crate_visible() { /* ... */ }
fn private() { /* ... */ }
}#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Resolution {
pub width: u32,
pub height: u32,
}
impl Resolution {
#[inline]
pub fn aspect_ratio(&self) -> f64 {
self.width as f64 / self.height as f64
}
}pub fn process_path(path: &str) -> Result<(), Error> {
// Process without cloning
Ok(())
}
use std::borrow::Cow;
pub struct Metadata {
pub name: Cow<'static, str>,
}use std::collections::BTreeMap;
use smallvec::SmallVec;
pub struct Timeline {
pub clips: SmallVec<[ClipData; 8]>,
pub markers: BTreeMap<Time, Marker>,
}#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
pub fn process_audio(samples: &mut [f32]) {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx") {
return unsafe { process_audio_avx(samples) };
}
}
// Fallback
}pub struct ClipPool {
pool: Vec<ClipData>,
}
impl ClipPool {
pub fn get(&mut self) -> ClipData {
self.pool.pop().unwrap_or_default()
}
pub fn return_clip(&mut self, clip: ClipData) {
self.pool.push(clip);
}
}pub struct VideoClip {
pub duration: Duration,
pub frame_rate: FrameRate,
}
pub fn calculate_timeline(timeline: &Timeline) -> Result<Duration, Error> {
Ok(Duration::new(0, 0))
}
pub const MAX_RESOLUTION: Resolution = Resolution {
width: 3840,
height: 2160,
};/// Represents a video clip.
///
/// # Examples
/// ```
/// let clip = VideoClip::new(Duration::from_secs(10), FrameRate::fps(30));
/// assert_eq!(clip.duration(), Duration::from_secs(10));
/// ```
pub struct VideoClip {
// Fields
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clip_creation() {
let clip = VideoClip::new(Duration::from_secs(10), FrameRate::fps(30));
assert_eq!(clip.duration(), Duration::from_secs(10));
}
}
#[bench]
fn bench_timeline_processing(b: &mut test::Bencher) {
let timeline = create_test_timeline();
b.iter(|| process_timeline(&timeline));
}pub fn safe_vector_access(vec: &Vec<i32>, index: usize) -> Option<&i32> {
vec.get(index)
}- Run cargo clippy, cargo fmt
// In CI
// cargo clippy -- -D warnings
// cargo fmt- Cover happy/error paths
#[test]
fn test() {
assert_eq!(func(), expected);
}- With proptest
proptest! {
#[test]
fn prop(input: i32) {
// Test properties
}
}- Use cargo fuzz
// cargo fuzz init- If there is code, there must be corresponding tests.
- All functionality, whether critical or non-critical, must have regression tests.
- Tests must be written before or alongside the code, following Test-Driven Development (TDD) principles.
- Stubs and mocks should only be used in rare occasions and for temporary reasons.
- Prefer real implementations and real data in tests whenever possible.
- If a test requires a stub or mock, clearly document why it's necessary.
- Do not retrofit tests just to make them pass.
- If a test fails, it should fail for a valid reason that indicates a real issue.
- The core idea of testing is not to simply pass tests, but to validate functionality so we can fix actual code.
- A failing test is better than a test with stub or mock data.
- Always test against real data as much as possible.
- Follow TDD and industry standards for test quality.
- Audit with cargo audit
// cargo.toml
[dependencies]
# Minimal set- Use Cargo.lock
// Update Cargo.lock
// cargo update- For embedded systems
#![no_std]- Use validators
fn validate_input(input: &str) -> Result<(), Error> {
if input.contains("<") { Err(Error::InvalidInput) } else { Ok(()) }
}- Use serde safely
use serde::Deserialize;
#[derive(Deserialize)]
struct SafeData {
#[serde(deserialize_with = "validate")]
field: String,
}- rand with crypto backends
use rand::rngs::OsRng;
let value = OsRng.gen::<u32>();- Your CI pipeline must, at a minimum, run:
cargo fmt --check: Ensures all code conforms to the standard Rust style.cargo clippy -- -D warnings: Runs the linter and fails the build on any warnings. This catches common mistakes and anti-patterns.cargo test: Runs all unit and integration tests.
- Regularly run
cargo auditto check for security vulnerabilities in your dependencies. - Use
cargo-denyorcargo-geigerin CI to check for disallowed licenses or high concentrations ofunsafecode in your dependency tree.
- Pin the exact Rust toolchain version for your project by adding a
rust-toolchain.tomlfile to your repository root. This ensures all developers and the CI system use the exact same compiler version, leading to reproducible builds.
[toolchain]
channel = "1.75.0"
components = ["rustfmt", "clippy"]// Phase 1: Create new module structure
// Phase 2: Move domain logic
// Phase 3: Optimize paths
// Phase 4: Update APIs#[bench]
fn bench_old(b: &mut test::Bencher) {
b.iter(|| old_process(&data));
}
#[bench]
fn bench_new(b: &mut test::Bencher) {
b.iter(|| new_process(&data));
}// Checklist
// - [ ] All public APIs documented
// - [ ] No unwrap() in production- No
.unwrap()/.expect()in production code. - Error handling uses
Result<T, E>and?. - Domain-driven module structure is used.
- Public APIs are documented with
///and runnable examples. - Clones are intentional and justified; borrows are preferred.
-
&stris used for function parameters instead ofStringwhere possible. - Type system is used to prevent invalid states (e.g., enums over booleans).
-
unsafeblocks are minimal, isolated, and have// SAFETY:comments. - CI checks for formatting (
fmt), linting (clippy), and tests. - Dependencies are audited (
cargo audit). - Iterators are used instead of manual C-style index loops.
- Appropriate smart pointers (
Box,Arc,Rc) are used. - Benchmarks exist for performance-critical code.
-
[inline]is used judiciously for small, hot functions. - Follows standard Rust naming conventions (snake_case for functions/variables, PascalCase for types).
- Domain-specific string values use enums or constants instead of string literals.
- Every code addition has corresponding tests.
- All critical and non-critical functionality has regression tests.
- Stubs and mocks are avoided except when absolutely necessary.
- Tests validate real functionality, not just pass for the sake of passing.
- Failing tests indicate real issues that need to be fixed.
- Tests use real data whenever possible instead of stub/mock data.
- Follows Test-Driven Development (TDD) principles.
-
Ownership Fundamentals (Rules 1.1-1.3)
- Each value has exactly one owner
- Clone only when ownership is required
- Prefer move semantics for large data
-
Borrowing Rules (Rules 2.1-2.3)
- Either multiple immutable borrows OR one mutable borrow
- Explicit lifetimes when ambiguous
- Struct lifetime annotations for stored references
-
Lifetimes (Rules 3.1-3.4)
- Explicit lifetimes when references outlive scope
- Avoid lifetime elision ambiguity
- Struct lifetime annotations for stored references
- Higher-ranked trait bounds for flexible lifetimes
-
Memory Safety (Rules 4.1-4.3)
- Eliminate null pointers with
Option<T> - Leverage RAII for resource management
- Use appropriate smart pointers
- Eliminate null pointers with
-
Error Handling (Rules 5.1-5.4)
- Use
Result<T, E>for recoverable errors - Use specific, custom error types
- Differentiate recoverable vs unrecoverable errors
- Implement error tracing
- Use
-
Concurrency (Rules 6.1-6.3)
- Use message passing
- Use mutexes for shared state
- Avoid data races
-
Async Programming (Rules 7.1-7.3)
- Use async/await for I/O
- Avoid blocking in async contexts
- Manage cancellation
-
Type System (Rules 8.1-8.4)
- Leverage newtype pattern
- Make impossible states unrepresentable
- Use phantom types
- Use associated types
-
Traits and Generics (Rules 9.1-9.3)
- Use traits for polymorphism
- Prefer trait objects over enums
- Implement standard traits
-
Macros and Metaprogramming (Rules 10.1-10.3)
- Prefer functions over macros
- Use
macro_rules!for declarative macros - Use procedural macros
-
Unsafe Rust (Rules 11.1-11.3)
- Minimize and isolate unsafe code
- Document safety invariants
- Validate with tests
-
Unsafe Rust and FFI (Rule 11.3)
- Follow FFI best practices
-
AI-Specific Guidelines (Rules 12.1-12.5)
- Verify ownership and borrowing
- Enforce robust error handling
- Scrutinize unsafe code
- Demand idiomatic Rust
- Generate documentation and tests
-
Common Anti-Patterns (Rules 13.1-13.5)
- Avoid RefCell overuse
- Don't fight the borrow checker
- Avoid stringly-typed APIs
- Prevent iterator invalidation
- Avoid string literals for domain values
-
Module Organization (Rules 14.1-14.3)
- Follow domain-driven structure
- Separate concerns
- Clear module boundaries
-
Performance Optimizations (Rules 15.1-15.5)
- Zero-cost abstractions
- Optimize string handling
- Cache-friendly data structures
- SIMD acceleration
- Memory pools
-
Best Practices (Rules 16.1-16.5)
- Naming conventions
- Document public APIs
- Integration tests and benchmarks
- Memory safety
- Linting and formatting
-
Testing Strategies (Rules 17.1-17.3)
- Unit and integration tests
- Property-based testing
- Fuzz testing
-
Testing Principles (Rules 17.4-17.6)
- Every code addition requires tests
- Avoid stubs and mocks
- Validate real functionality
-
Dependency Management (Rules 18.1-18.3)
- Minimize dependencies
- Pin versions
- Prefer no-std when possible
-
Security Best Practices (Rules 19.1-19.3)
- Sanitize inputs
- Avoid deserialization vulnerabilities
- Use secure randomness
-
Build, CI, and Tooling (Rules 18.1-18.3)
- Enforce code quality with CI
- Audit dependencies
- Use rust-toolchain.toml
- ✅ Easy to find related code
- ✅ Clear domain boundaries
- ✅ Follows Rust conventions
- ✅ Better IDE support
- ✅ Comprehensive documentation
- ✅ AI-friendly patterns
- ✅ Fewer allocations
- ✅ Better cache locality
- ✅ SIMD acceleration
- ✅ Predictable performance
- ✅ Efficient memory usage
- ✅ Async efficiency
- ✅ Clear separation of concerns
- ✅ Easy to test individual domains
- ✅ Scalable architecture
- ✅ Future-proof design
- ✅ Incremental migration path
- ✅ Automated linting
- ✅ Compile-time error prevention
- ✅ Memory safety guarantees
- ✅ Thread-safe concurrency
- ✅ Minimal unsafe code
- ✅ Comprehensive validation
- ✅ Secure dependencies