How-To: Language Learning
A condensed guide to learning Rust — specifically for the person who will be directing AI agents to write it. You don't need to be a daily Rust programmer. You need to understand enough to review, guide, and unblock AI-generated code.
Your job is not to become a Rust expert. Your job is to be able to:
The mental model: Think of yourself as a senior reviewer who can read code, ask good questions, and direct the builder. You don't need to lay every brick — you need to know when the walls are straight.
Rust trades developer convenience for runtime safety and zero-cost concurrency. The compiler prevents entire classes of bugs before the code runs. For AI-generated code, this means the compiler is your first line of defense — if it compiles, it's generally correct.
This is actually an advantage when working with AI agents. The compiler catches the bugs that LLMs are most likely to introduce: null pointer dereferences, data races, buffer overflows, use-after-free.
High performance needed (APIs, inference runtimes, data pipelines)
Concurrency is important (async services, parallel processing)
System control matters — CPU, memory, safety guarantees
Long-lived services that need to run for months without memory leaks
FFI boundaries — Rust as a bridge between Python/ML code and systems-level operations
Rapid prototyping — Python for research, Rust for production
Simple scripts — bash/Python are faster for one-off tasks
Data science itself — Rust is a poor fit for exploratory analysis
Small internal tools — the compile time and learning curve aren't worth it
When you only have AI agents — Rust's strict compiler requires more human oversight than Python
These are Rust's core concepts. They're the "weird" part that makes Rust hard. For your use case (guiding agents), you need to understand what they are and why they exist, not memorize every edge case.
Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped — deallocated immediately, no garbage collector. This is what gives Rust its memory guarantees.
// String is owned — when `s` goes out of scope, memory is freed
let s = String::from("hello");
// Transfer ownership — `s` is now invalid (move semantics)
let s2 = s; // s cannot be used here anymore
// Copy types (i32, bool, f64, etc.) are duplicated, not moved
let x = 42;
let y = x; // x is still valid — Copy trait
🔴 What to look for in AI-generated code: If the agent tries to use a variable after it's been "moved," the compiler will reject it. This is the #1 error patterns you'll encounter. The fix is usually: use references (&T) instead of transferring ownership.
Rust lets you create references to data without taking ownership. There are two kinds:
// Immutable borrow — many readers allowed
let ref1 = &s;
let ref2 = &s; // Totally fine
// Mutable borrow — only ONE, and no other borrows at the same time
let mut_ref = &mut s; // Unique mutable access — no other refs allowed
The "borrow checker" is the part of the compiler you'll argue with most. But for your role, think of it as Rust's way of asking: "Are you sure this reference won't outlive the data it points to?" — the compiler answers that for you.
// The compiler tracks lifetimes automatically in most cases
// When it can't figure it out, you annotate with 'a
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// Compiler infers that the return value's lifetime
// is the shorter of x's and y's lifetime
🟡 Agent pitfall: AI agents sometimes struggle with lifetimes and will try to work around them with .clone() (duplicating data unnecessarily) or String::from() everywhere. Ask the agent: "Can this use references instead of cloning? What's the lifetime situation here?"
// Primitives
let x: i32 = 42; // signed 32-bit int
let y: u64 = 1_000_000; // unsigned 64-bit
let z: f64 = 3.14; // float
let b: bool = true;
let ch: char = 'A';
let s: String = String::from("heap");
let slice: &str = "stack"; // &str is a reference slice
// Collections
let mut vec: Vec<i32> = vec![1, 2, 3];
let map: HashMap<String, i32> = HashMap::new();
let set: HashSet<String> = HashSet::new();
// Enums (the powerful one)
enum Result<T, E> {
Ok(T),
Err(E),
}
// Match — Rust's main control flow (exhaustive!)
match result {
Ok(value) => println!("Got: {value}"),
Err(e) => eprintln!("Error: {e}"),
}
// If let — when you only care about one case
if let Ok(value) = result {
println!("Success: {value}");
}
// For loop over iterators
for item in vec.iter() { // borrows each element
println!("{item}");
}
for item in vec.into_iter() { // takes ownership (consumes the vec)
println!("{item}");
}
// Regular struct
struct Config {
host: String,
port: u16,
debug: bool,
}
// Implementing methods
impl Config {
fn new(host: String, port: u16) -> Self {
Config { host, port, debug: false }
}
// Taking &self = immutable borrow of self
fn as_url(&self) -> String {
format!("http://{}:{}", self.host, self.port)
}
}
// Define a trait (like an interface)
trait AIReadable {
fn interpret(&self) -> String;
}
// Implement it for a type
impl AIReadable for Config {
fn interpret(&self) -> String {
format!("Config: {}:{}", self.host, self.port)
}
}
Key point: Traits are how Rust achieves polymorphism. When reviewing AI-generated code, look for well-defined traits that describe behavior (e.g., From<String> for MyType, Display for formatting). These make code composable and easier to reason about.
Rust doesn't have exceptions. It has Result and Option. AI agents usually get this right, but watch for these patterns:
// The standard pattern — no try/catch
let result: Result<String, std::io::Error> = std::fs::read_to_string("file.txt");
// Propagate errors with ? (the "unwrap with responsibility" operator)
fn read_config() -> Result<Config, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("config.json")?; // returns on Err
let config = serde_json::from_str(&content)?; // another ?
Ok(config)
}
// Option for nullable values
let maybe_value: Option<String> = map.get("key").cloned();
match maybe_value {
Some(v) => println!("Found: {v}"),
None => println!("Key not present"),
}
🟡 Watch out: AI agents sometimes overuse .unwrap() or .expect() — these will panic at runtime. Ask: "Should this error be propagated with ? instead?" Production Rust rarely unwraps externally-provided data.
Cargo is Rust's build tool and package manager (like pip + make combined).
// Project structure
my-crate/
Cargo.toml // dependencies, package metadata
src/
main.rs // binary entry point
lib.rs // library code (if this is a library)
lib/ // module subdirectory (mod.rs files)
// Cargo.toml — this is what you'll care about for dependency reviews
[package]
name = "my-service"
version = "0.1.0"
edition = "2021" // always this unless AI uses old Rust
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.11"
serde_json = "1"
cargo build // compile (slow — this is the check)
cargo run // build + run
cargo check // compile only (fast, for CI)
cargo test // run all tests
cargo clippy // linter — catches style + logic issues
cargo fmt // auto-format (standardized code)
cargo add <crate> // add dependency (e.g., cargo add serde)
🔴 Key crates to know for AI agent work:
tokio — async runtime (everything async uses this)
serde — serialization/deserialization (JSON, config, etc.)
tokio-serde — serde + tokio integration
reqwest — HTTP client (for AI agent API calls)
tracing — structured logging/tracing (for debugging agents)
tonic — gRPC (common in AI/ML infrastructure)
burn, candle — Rust ML frameworks (emerging)
candle — specifically from Hugging Face, good for inference
This is where your day-to-day work happens. Here are prompts that work well:
You are a senior Rust engineer. I need you to write Rust code following these rules:
- Use clippy and cargo fmt standards
- Prefer Result over unwrap() for fallible operations
- Use tokio for async code — never mix blocking and async unnecessarily
- Use serde for all serialization, derive derives manually
- Add #[derive(Debug, Clone, Serialize, Deserialize)] on public structs
- Use thiserror for custom error types
- Include async/await only where concurrency is needed
- Write tests for non-trivial logic
- Comment the why, not the what
- Use meaningful function names — no one-letter variable names
This Rust code was generated by an AI. Review it for:
- Borrow checker correctness (moved vs borrowed, lifetime issues)
- Error handling (are unwraps justified? should any be propagated?)
- Concurrency (tokio task spawning, race conditions, blocking in async)
- Performance (unnecessary clones, allocations in hot paths, wrong collection types)
- Security (input validation, unsafe blocks, dependency trustworthiness)
- Idioms (does it use standard patterns or look like Python translated to Rust?)
List issues by severity: [Critical], [Important], [Style].
This Rust code doesn't compile. The error is:
[COPY THE ERROR FROM `cargo check`]
The code is [...]. Fix the issue and explain what went wrong so I can avoid it in future agent prompts.
I'm building a Rust-based AI agent runner. It needs to:
- Spawn multiple agent sessions concurrently (tokio)
- Handle HTTP API endpoints (axum or actix)
- Read config from JSON/YAML (serde)
- Log with structured tracing
- Be testable and well-organized
Suggest the project structure, crate layout, and key dependency choices. Explain trade-offs between common patterns (e.g., async runtime, web framework choice).
unsafe unnecessarily — AI sometimes throws in `unsafe { }` blocks when there's a safe alternative. Always ask: "Is there a safe way to do this?".unwrap() on external data — database reads, API responses, file I/O. These should propagate with ?Send/Sync bounds on tokio tasksstd::fs::read inside async fn blocks the executor. Should use tokio::fsString everywhere instead of using &str referencesVec when a HashSet or BTreeMap is appropriateOption where Result is correctclippy warnings — these catch real issues in idiomatic RustDon't read The Book cover to cover. Focus on what enables you to guide agents effectively:
cargo check output — the compiler error messages are the most important skill. Learn to parse them and translate them into fixes for the agent.Don't worry about: Complex lifetime annotations, unsafe code, FFI (unless building Python bindings), metaprogramming (procedural macros), or advanced type-level programming. These are rare in typical agent-related code.
Copy-paste these into your prompts when working with AI agents on Rust projects:
Rust standards:
- Edition 2021+, never 2018
- Format: cargo fmt
- Lint: clippy (default + pedantic where safe)
- Error handling: thiserror for custom errors, no unwrap() on external data
- Async: tokio runtime, tokio::fs not std::fs in async
- Serialization: serde with derives on all public types
- Logging: tracing, not println! in production code
- Dependencies: pin versions, audit with cargo audit
This Rust code compiles but feels unidiomatic. Review it specifically for:
1. Are we moving values when borrowing would work?
2. Should any String references be &str?
3. Is the error handling consistent (no mix of unwrap/expect with ?)
4. Are async functions actually async, or could they be synchronous?
5. Are tokio task spawns using tokio::spawn with proper bounds?
6. Is clippy happy with this code?
You're building compiler judgment, not Rust fluency. When you review agent-generated code, your job is to recognize patterns and ask the right questions:
unwrap() justified, or should the error propagate?"Run cargo check and cargo clippy after every agent change. If they pass clean, you're in good shape. If they don't, paste those errors back to the agent and iterate.
Rust compiles slowly and fails loudly — that's the tradeoff. But for AI agent systems where reliability matters (agent runners, inference servers, API gateways), Rust's guarantees are hard to beat.