agentleFS
po4ykapo4yka/rust-skills/skills/rust-pattern-semantics/SKILL.md

rust-pattern-semantics

Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub

Use when a Rust pattern, match guard, or if let guard decides what moves, borrows, or drops, or when an edition 2024 pattern or let chain fails to compile. Triggers on "match guard", "if let guard", "partial move", "binding mode", "match ergonomics", "ref pattern", "scrutinee lifetime", "let chains", "implicitly-borrowing pattern", "guard runs twice", E0510, E0594, or E0596 in a guard.

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 4 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When the guard runs, and whether it can run more than once.

    line 20
  2. Very specific:

    Do not propose a clone, a wildcard arm, or a ref keyword before this pass.

    line 23
  3. Very specific:

    Do not add a #![feature] gate for an E0658 from this table.

    line 35
  4. Very specific:

    Use Option::take, mem::replace, or an explicit consuming method when a resource owner must release one field before its destructor.

    line 126
  5. Distinctive:

    If a guard needs expensive or stateful work, match first and run ordinary control flow in the arm.

    line 154
  6. Very specific:

    Do not change a scrutinee between a place and a value (a named guard versus an inline lock()), and do not rewrite match into if let or the …

    line 248
  7. Distinctive:

    Every alternative must bind the same names with the same types and binding modes.

    line 255
  8. Very specific:

    Do not add _ => {} only to satisfy rustc.

    line 261
  9. Distinctive:

    Do not copy a pattern from an edition 2021 example into an edition 2024 crate without compiling it in the target crate.

    line 297

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Pattern Semantics

Analysis order

For the pattern that the question or the error names, determine:

  1. The crate edition and the MSRV (rust-version in Cargo.toml). They decide which syntax compiles. See Version gates.
  2. Whether the scrutinee is a place or a value expression.
  3. The alternatives of each or-pattern.
  4. The default binding mode at each nesting level.
  5. Whether each binding is by value, ref, or ref mut, and which fields a partial move leaves initialized.
  6. When the guard runs, and whether it can run more than once.
  7. Where the scrutinee temporaries, the guard temporaries, and the bindings drop.

Do not propose a clone, a wildcard arm, or a ref keyword before this pass. Each one can compile and change what the code moves, borrows, or matches.

Version gates

FeatureNeedsError when the gate is not met
Let chains in if and while (if let A = a && cond)Edition 2024 and rustc 1.88+Edition 2021: let chains are only allowed in Rust 2024 or later (no error code). Edition 2024 on rustc 1.85-1.87: E0658 `let` expressions in this position are unstable (measured on rustc 1.87.0)
if let guards, and a let chain inside a guard (if let P = e && cond)rustc 1.95+, every editionE0658 `if let` guards are experimental (measured on rustc 1.94.0)
std::assert_matches!rustc 1.96+E0658 use of unstable library feature `assert_matches` on rustc 1.95, and for the use form on older toolchains. E0433 or cannot find macro for the path form on rustc 1.94 and older (measured on rustc 1.87.0-1.95.0)
Match ergonomics reservationsEdition 2024Uncoded errors, see Edition 2024 match ergonomics

Do not add a #![feature] gate for an E0658 from this table. Raise rust-version, or use the fallback: a nested if let, or assert!(matches!(..)). The rust-compiler-errors skill, when it is installed, lists the other compile-error repair hazards.

A let chain inside a match guard compiles on edition 2021 (measured on rustc 1.98.1). A chain in an if or while condition does not.

Verify

ClaimCheckWhat a pass does not prove
Binding type or modelet () = binding; and read the E0308 label; or a typed call as in Binding modesDrop timing or guard repetition
Guard count, drop point, lock holdA #[test] or a doctest (it runs by default) that asserts on a counter or on Mutex::try_lock, built with the crate's editionBehavior on another edition
Pattern shape in a teststd::assert_matches!(value, Some(n) if n > 2); on MSRV 1.96+; else assert!(matches!(..))Fields that the pattern does not name
A pattern must not compileA compile_fail doctest that holds only the probe code: compile_fail,E0xxx when rustc prints a code (E0004, E0509, E0510, E0594, E0596), a bare compile_fail for the uncoded edition 2024 errors. To prove the code on stable, run cargo check --message-format=short on the probe and find error[E0xxx], or use a trybuild case, which compares the full stderrThe reason for the failure. Stable rustdoc accepts any compile error and ignores a named code; only nightly rustdoc checks it
Syntax builds on the MSRVcargo +<msrv> check --locked for a let chain, an if let guard, or assert_matches!Behavior, and code behind other features or targets

std::assert_matches! is not in the prelude. Call it by path, or import it with use std::assert_matches;. use std::assert_matches::assert_matches; fails with E0432.

Triage

SymptomLikely causeFirst action
Value is partly usable after destructuringThe pattern made a partial moveList moved and initialized fields
Partial move fails only after adding DropThe destructor needs the whole valueAdd a consuming method or Option::take
Guard side effect occurs twiceOverlapping or-pattern alternatives both matchedRemove the side effect or split the arms
E0510, or E0594/E0596 with "immutable for the pattern guard"The guard mutates or mutably borrows the scrutinee or a pattern bindingMove the mutation into the arm body. A signature change does not help
Lock or borrow stays held inside the armThe lock is in the match scrutinee or an if let guardCopy or clone the value out before the match, or bind the guard in a braced block that returns an owned value
Binding became &T after a refactorMatch ergonomics changed the default binding modeProbe the binding type in the target edition
... within an implicitly-borrowing patternEdition 2024 reservationApply the fix from the edition 2024 table
let chains are only allowed in Rust 2024 or laterLet chain in an edition 2021 if or whileNest the if let and repeat the else branch at each level, or migrate to edition 2024 with MSRV 1.88+ (the cargo-workflows skill)
E0658 on a let chain, an if let guard, or assert_matches!The toolchain is older than the version gateRaise rust-version or use the fallback. Do not add #![feature]
Wildcard fixes exhaustiveness but tests failThe missing variant needed a semantic policyAdd an explicit arm and behavior test

Binding modes

A non-reference pattern binds by value. When a non-reference pattern matches a reference, match ergonomics switches the default binding mode to ref or ref mut, and the inner bindings borrow:

fn shared(_: &String) {}
fn mutable(_: &mut String) {}

let mut value = Some(String::from("x"));
if let Some(text) = &value { shared(text); }
if let Some(text) = &mut value { mutable(text); }

A function argument coerces &mut T to &T, so shared(text) proves only that text is some reference. To see the exact type, write let () = text;. The E0308 label says this expression has type `&String` . Remove the line after you read it.

Edition 2024 restricts explicit mut, ref, ref mut, and & inside a pattern that borrows implicitly. See Edition 2024 match ergonomics.

Partial moves

A pattern can move one field and borrow another. The original value then cannot be used as a whole, but fields that were not moved remain usable.

struct Record {
    name: String,
    count: u32,
}

fn main() {
    let record = Record { name: String::from("job"), count: 3 };
    let Record { name, ref count } = record;
    assert_eq!(name, "job");
    assert_eq!(*count, 3);
    assert_eq!(record.count, 3);
}

A type with a Drop implementation cannot be partially moved because drop must receive the whole value:

struct Resource { name: String }
impl Drop for Resource { fn drop(&mut self) {} }

fn main() {
    let resource = Resource { name: String::from("owned") };
    let Resource { name } = resource;
    println!("{name}");
}

Use Option::take, mem::replace, or an explicit consuming method when a resource owner must release one field before its destructor.

Match guards

A guard runs after the pattern matches and before the arm body. The bindings are shared borrows while the guard runs. A by-value binding moves or copies only after the guard succeeds.

A guard on an or-pattern can run more than once

The guard applies to the complete or-pattern. It runs once for each alternative that matches until one succeeds. Do not put metrics, mutation, I/O, or one-shot work in it.

fn main() {
    let mut calls = 0;
    match 1 {
        1 | 1 if {
            calls += 1;
            false
        } => unreachable!(),
        1 => {}
        _ => unreachable!(),
    }
    assert_eq!(calls, 2);
}

If a guard needs expensive or stateful work, match first and run ordinary control flow in the arm.

Guard mutation

The example above mutates calls, a local that the pattern does not bind. That is legal. rustc rejects two other mutations:

  • Assigning to or mutably borrowing the scrutinee gives E0510 (cannot assign ... in match guard, cannot mutably borrow ... in match guard).
  • Assignment through a pattern binding gives E0594. A &mut method call through it, such as v.push(2), gives E0596. Both carry the note "variables bound in patterns are immutable until the end of the pattern guard". This applies to ref mut bindings too.
fn main() {
    let mut slot = Some(1);
    match slot {
        Some(_) if { slot = None; false } => {}
        _ => {}
    }
}
fn main() {
    let mut slot = Some(1);
    match slot {
        Some(ref mut n) if { *n += 1; false } => {}
        _ => {}
    }
}

Move the mutation into the arm body, or compute a boolean before the match. When rustc rejects a guard, keep the exact pattern in the probe. A rewrite into an if proves a different program.

Temporaries: scrutinee, if let guard, boolean guard

The three positions drop temporaries at different points. A lock() in the wrong position holds the lock through the arm body:

PositionTemporaries drop
match scrutineeAt the end of the enclosing temporary scope (usually the statement), after every arm
if let guard scrutineeWhen the arm body ends. If the guard fails, before the next arm
Boolean guard (if cond)As soon as the condition is evaluated
use std::sync::Mutex;

fn main() {
    let state = Mutex::new(Some(1));

    match *state.lock().unwrap() {
        Some(_) => assert!(state.try_lock().is_err()),
        None => unreachable!(),
    }

    match 0 {
        _ if let Some(_) = *state.lock().unwrap() => assert!(state.try_lock().is_err()),
        _ => unreachable!(),
    }

    match 0 {
        _ if state.lock().unwrap().is_some() => assert!(state.try_lock().is_ok()),
        _ => unreachable!(),
    }
}

To release the lock before the arm body, copy or clone the value out before the match (let v = state.lock().expect("state mutex poisoned").clone();), or bind the guard inside a braced block that returns an owned value. Do not bind the guard itself to a local in the same block: it then lives to the end of the block.

An arm with any guard does not count toward exhaustiveness. This includes an if let guard whose pattern cannot fail. rustc still reports E0004 for the value that the guard covers.

Scrutinee place versus value

Matching a place inspects existing storage. Matching a value expression creates a temporary. The match scrutinee is not a temporary scope, so that temporary lives until the end of the enclosing temporary scope. That is usually the statement. For a match that is a block tail on edition 2024, it is right after the tail expression.

ScrutineeOwnership question
match localWhich fields move from or borrow the local?
match &localWhich bindings inherit a reference binding mode?
match make()The temporary lives through every arm. Does it hold a lock or borrow?
match *pointerIs the dereference a safe place, and what can move from it?
if let ... = make()Edition 2024 drops the temporary before else. Edition 2021 keeps it through else

Do not change a scrutinee between a place and a value (a named guard versus an inline lock()), and do not rewrite match into if let or the reverse, without checking destructor and borrow timing. The rust-borrow-semantics skill, when it is installed, has the full temporary-scope analysis.

Or-patterns

Every alternative must bind the same names with the same types and binding modes. Prefer one arm per case when alternatives need different guards or side effects. Use an or-pattern when the alternatives have the same binding contract and the same body.

Match exhaustiveness is a behavior decision

Do not add _ => {} only to satisfy rustc. A new variant then takes the wildcard path silently. For a closed enum this crate owns, list every variant. The rust-discipline skill, when it is installed, holds the exhaustiveness policy and the wildcard_enum_match_arm lint.

Edition 2024 match ergonomics

Edition 2024 adds two rules:

  • A mut, ref, or ref mut modifier on a binding is an error when the default binding mode is ref or ref mut.
  • A reference pattern (& or &mut) can match only while the default binding mode is move.

The errors have no error code. Do not invent one for a probe.

MessageExample on value: &Option<T>Fix
cannot explicitly borrow within an implicitly-borrowing patternSome(ref x)Remove ref. x already borrows. On &mut Option<T>, write &mut Some(ref x) to keep a shared borrow (rustc's help)
cannot mutably bind by value within an implicitly-borrowing patternSome(mut n)For a Copy type, write &Some(mut n). Otherwise remove mut and clone into a new let mut in the body
cannot explicitly dereference within an implicitly-borrowing patternSome(&n) on &Option<&u32>Write &Some(&n)

In edition 2021, mut on a binding resets the default binding mode to move. Some(mut n) on &Option<u32> compiles there and makes n a u32 copy, not a &u32. On &Option<String> it fails with E0507.

To migrate:

  1. List the affected patterns with cargo clippy --locked --all-targets --all-features -- -W rust_2024_incompatible_pat. Use --all-features only when the features are additive. Otherwise run it once per supported feature set. Repeat it once per shipping --target <triple>, as the cargo-workflows skill describes when it is installed.
  2. cargo fix --edition applies this lint, which is part of rust-2024-compatibility. It rewrites each pattern to a fully explicit form with the same meaning in every edition.
  3. Review each rewrite. The explicit form keeps the 2021 meaning, including a silent copy. Decide per site whether a borrow was intended.

Do not copy a pattern from an edition 2021 example into an edition 2024 crate without compiling it in the target crate.

Related skills

Each applies when it is installed.

SkillBoundary
rust-borrow-semanticsTemporary scopes, place expressions, and two-phase borrows
rust-compiler-errorsGeneral E0382, E0507, E0509, E0499, and E0502 triage
rust-disciplineExhaustiveness policy and public API review
rust-callback-boundsClosure patterns and higher-ranked callback bounds
cargo-workflowsEdition migration and compatibility lints

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.