AI Tools Compared

Rust’s ownership model, borrow checker, and type system create unique challenges for AI code generation. Most models understand Rust syntax but struggle with lifetime annotations, async/await with Send + Sync bounds, and error handling patterns. This guide tests the tools specifically on Rust’s hard parts.

The Rust-Specific Challenge

Rust errors aren’t just runtime bugs — the compiler rejects code that violates ownership rules before it runs. AI tools fail in two ways:

  1. Generates code that doesn’t compile: Incorrect lifetime annotations, borrow checker violations, wrong trait bounds
  2. Generates code that compiles but is wrong: Using clone() to paper over ownership issues instead of restructuring, Arc<Mutex<>> everywhere instead of proper ownership

The second failure is more dangerous because it passes compilation and tests but creates performance problems and potential deadlocks.

Borrow Checker Assistance

The most valuable AI capability in Rust is explaining and fixing borrow checker errors.

Test: Paste a borrow checker error and ask for explanation + fix.

// Code that produces: error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
fn problem() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];      // immutable borrow
    v.push(4);              // mutable borrow -- ERROR
    println!("{}", first);
}

Claude’s explanation: “The borrow checker rejects this because v.push(4) may reallocate the vector’s backing storage, which would invalidate the reference first. Even though first is just &i32, the compiler doesn’t know at compile time whether push will reallocate.

Fix 1 — Use the value before the mutable operation:

fn fixed_1() {
    let mut v = vec![1, 2, 3];
    let first_val = v[0];   // Copy the value, not a reference
    v.push(4);
    println!("{}", first_val);
}

Fix 2 — Clone the slice if you need multiple references:

fn fixed_2() {
    let mut v = vec![1, 2, 3];
    v.push(4);
    let first = &v[0];      // Borrow after mutation
    println!("{}", first);
}

Fix 3 — Use indices if you need to reference elements across mutations:

fn fixed_3() {
    let mut v = vec![1, 2, 3];
    let first_idx = 0;
    v.push(4);
    println!("{}", v[first_idx]);  // Access by index, not stored reference
}

The idiomatic choice depends on your use case: if you just need the value, copy it (Fix 1). If you need to reference after more operations, restructure the access order (Fix 2).”

Claude gave three semantically correct options with reasoning. Copilot in Cursor gave Fix 2 only without explanation.

Async/Await Pattern Generation

Async Rust is notoriously complex. Test: “Write a function that fetches multiple URLs concurrently, with a timeout on each request and a maximum of 10 concurrent requests.”

Copilot’s output:

use tokio::time::timeout;
use std::time::Duration;
use futures::future::join_all;

async fn fetch_urls(urls: Vec<String>) -> Vec<Result<String, String>> {
    let futures = urls.iter().map(|url| {
        let url = url.clone();
        async move {
            timeout(Duration::from_secs(10), reqwest::get(&url))
                .await
                .map_err(|_| "timeout".to_string())
                .and_then(|r| r.map_err(|e| e.to_string()))
                .and_then(|r| /* ... */)
        }
    });
    join_all(futures).await
}

This doesn’t limit concurrency — it spawns all requests simultaneously.

Claude’s output:

use tokio::time::{timeout, Duration};
use futures::{stream, StreamExt};

async fn fetch_urls_concurrent(
    urls: Vec<String>,
    max_concurrent: usize,
    request_timeout: Duration,
) -> Vec<Result<String, FetchError>> {
    stream::iter(urls)
        .map(|url| async move { fetch_single(&url, request_timeout).await })
        .buffer_unordered(max_concurrent)  // Limits concurrency to max_concurrent
        .collect()
        .await
}

async fn fetch_single(url: &str, request_timeout: Duration) -> Result<String, FetchError> {
    let response = timeout(
        request_timeout,
        reqwest::get(url)
    )
    .await
    .map_err(|_| FetchError::Timeout)?
    .map_err(FetchError::Request)?;

    response.text()
        .await
        .map_err(FetchError::Request)
}

#[derive(Debug, thiserror::Error)]
enum FetchError {
    #[error("Request timeout")]
    Timeout,
    #[error("Request failed: {0}")]
    Request(#[from] reqwest::Error),
}

Claude used buffer_unordered from the futures crate which correctly limits concurrency, used thiserror for proper error types, and structured the timeout correctly. This is idiomatic Rust.

Macro Generation

Rust macros (both macro_rules! and proc macros) are notoriously hard. AI tools vary significantly here.

Test: “Create a macro that generates builder patterns for structs.”

// Claude generated this derive macro (proc macro)
// In a separate crate (derive_builder_macro):

use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let builder_name = format_ident!("{}Builder", name);

    let fields = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(f) => &f.named,
            _ => panic!("Builder only works on structs with named fields"),
        },
        _ => panic!("Builder only works on structs"),
    };

    let builder_fields = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        quote! { #name: Option<#ty> }
    });

    let builder_setters = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        quote! {
            pub fn #name(mut self, val: #ty) -> Self {
                self.#name = Some(val);
                self
            }
        }
    });

    let build_fields = fields.iter().map(|f| {
        let name = &f.ident;
        let name_str = name.as_ref().unwrap().to_string();
        quote! {
            #name: self.#name.ok_or_else(|| format!("Field '{}' not set", #name_str))?
        }
    });

    quote! {
        #[derive(Default)]
        pub struct #builder_name {
            #(#builder_fields,)*
        }

        impl #builder_name {
            #(#builder_setters)*

            pub fn build(self) -> Result<#name, String> {
                Ok(#name {
                    #(#build_fields,)*
                })
            }
        }

        impl #name {
            pub fn builder() -> #builder_name {
                #builder_name::default()
            }
        }
    }.into()
}

GPT-4o generated macro_rules! instead of a proc macro, which doesn’t work for this use case. Claude correctly identified this requires a proc macro and generated compilable code.

Tool Rankings for Rust

Task Claude Copilot/Cursor Codeium
Borrow checker explanation Excellent Good OK
Lifetime annotation Good OK Poor
Async/await patterns Excellent Good OK
Proc macro generation Good Poor Poor
Error handling with ? Excellent Good Good
Unsafe code review Good OK Poor

Claude is the strongest for Rust specifically because its reasoning about ownership semantics is more accurate than pattern-matching-only approaches.

Practical Workflow

Use Copilot in rust-analyzer-enabled VS Code or Cursor for:

Use Claude for:

# Useful: pipe rustc errors to Claude
cargo build 2>&1 | pbcopy  # Copy errors, then paste to Claude
# Or use claude-code CLI:
cargo build 2>&1 | claude "explain these Rust errors and provide fixes"

Built by theluckystrike — More at zovo.one