rust-borrow-semantics
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when a Rust lock guard or temporary lives longer or shorter than the code suggests, such as a hang on a second lock() while a match scrutinee still holds the guard, or a borrow that depends on expression form (temporary lifetime extension, the drop scope of a temporary, two-phase borrows and autoref, place versus value). Includes edition 2024 if_let_rescope and tail_expr_drop_order changes, and E0716 there. Not for general borrow-checker triage (use rust-compiler-errors).
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 9 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 9
Keep the exact source expression until you know every borrow and drop point.
- Very specific: line 53
Do not classify from the type.
- Distinctive: line 94
When a resource guard must release before later work, bind it and make the drop point explicit with braces.
- Distinctive: line 187
Do not explain the first program as evaluation order alone.
- Distinctive: line 206
Run the two migration lints on the crate while it is still on edition 2021.
- Distinctive: line 225
Do not use an edition migration as a lock-lifetime refactor.
- Distinctive: line 234
Use the smallest proof that decides the question:
- Distinctive: line 236
Put the exact expression in a standalone file.
- Very specific: line 243
Use a compile_fail probe for a rejected borrow, and name the expected error code.
- Very specific: line 244
Use a runtime assertion, for example try_lock().is_ok(), when the question is a drop point or a destructor order.
- Distinctive: line 246
Run the real workspace tests after the probe.
- Distinctive: line 249
Do not use Miri to answer whether code type-checks.
≈ “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.
Files
Rust Borrow Semantics
Keep the exact source expression until you know every borrow and drop point. A manual desugaring, an added helper call, a macro, or an edition change can move those points while the types stay the same.
Start with four facts
Record these facts before you propose a fix:
- Is each operand a place expression or a value expression?
- Which expression creates each temporary?
- Which borrow is explicit, and which borrow does autoref or reborrow insert?
- Which edition and which minimum supported Rust version (MSRV) apply? Temporary scope and lifetime extension rules changed in several releases from 1.79 to 1.98.
Then write the expected reservation, activation, and drop points. Prove them with a probe (see Verification).
Triage
| Symptom | Likely cause | First action |
| Method call compiles, UFCS rewrite fails with E0502 | The rewrite removed an implicit two-phase borrow | Restore the method call, or compute the other argument first |
| E0716 after you add a helper call | A function call does not extend the temporary | Name the owner in a let before the call |
Some(&temp()) or Wrapper(&temp()) compiles on stable, fails with E0716 on the MSRV lane | Constructor lifetime extension needs Rust 1.89 | Name the owner, or raise the MSRV on purpose |
| New E0716 at a block tail after a move to edition 2024 | The tail temporary now drops at the end of the block | Move the block value into a named let |
After a move to edition 2024, a guard releases earlier: a deadlock disappears, a race appears, or try_lock succeeds in else | The if let scrutinee or the tail temporary scope narrowed | Bind the guard to a named local in an explicit block |
A lock stays held through a whole match, or a second lock() later in the same statement deadlocks | A match scrutinee temporary lives through every arm on every edition. It drops at the end of the enclosing statement, or before the block's locals when the match is a block tail on edition 2024 | Read the value out in a block that drops the guard (let v = { let g = m.lock().expect("m mutex poisoned"); g.clone() };), then match on v. For if let guards (1.95+), see the rust-pattern-semantics skill |
E0502 or E0499 at a call to a function that returns impl Trait, only on edition 2024, with the note "may capture more lifetimes than intended" | The opaque return type captures the argument lifetime. This is a signature rule, not a temporary scope | Fix the callee signature, not the caller. See the rust-compiler-errors skill |
E0716 or a moved drop point in a pin!, format_args!, write!, writeln!, or assert_eq! call after a rewrite, a toolchain change, or on the MSRV lane | Macro temporaries have their own rules, changed in 1.89-1.98 | Probe the exact macro call on the MSRV and on stable |
| Adding braces changes whether code compiles | The block changed a temporary or borrow scope | Mark the exact creation and drop points |
| A clone makes the error disappear | Ownership was duplicated, not explained | See Clone is not a scope operator |
Place expressions and value expressions
A place expression identifies storage. A value expression produces a value. The difference decides whether an operation moves from existing storage or creates a temporary.
| Shape | Usually a place | Usually a value |
| Local, static, dereference, index, field | Yes | No |
| Literal, arithmetic result, function call, constructor | No | Yes |
| Parentheses | Keep the inner classification | Keep the inner classification |
| Block tail | Depends on the tail expression | Depends on the tail expression |
Do not classify from the type. A String can be a local place or the value that a call returns.
Temporary scopes
A temporary drops at the end of its enclosing temporary scope. The important scopes are the statement, a condition, a match arm, and a block tail.
Edition 2024 narrows two scopes. It drops if let scrutinee temporaries before the else
block, and it drops block-tail temporaries before the block's locals. Both changes release a
guard earlier, never later. A match scrutinee temporary still lives through every arm.
This probe runs on edition 2024. On edition 2021 the first assertion fails:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(None::<u32>);
let free_in_else = if let Some(v) = *m.lock().unwrap() {
v == 0
} else {
m.try_lock().is_ok()
};
assert!(free_in_else);
let free_in_arm = match *m.lock().unwrap() {
Some(v) => v == 0,
None => m.try_lock().is_ok(),
};
assert!(!free_in_arm);
}The narrowing can also reject code that compiled on edition 2021:
fn main() {
let len = { &String::from("1234") }.len();
println!("{len}");
}When a resource guard must release before later work, bind it and make the drop point explicit with braces. The drop point is then the same on every edition:
use std::sync::Mutex;
let state = Mutex::new(vec![1]);
let first = {
let guard = state.lock().expect("state mutex poisoned");
guard[0]
};
assert_eq!(first, 1);
assert!(state.try_lock().is_ok());Temporary lifetime extension
Some let patterns (ref bindings) and initializers extend a borrowed temporary to the end of
the block. The rule is syntactic. It is not general lifetime inference. Since Rust 1.89 the
extension also passes through the arguments of a tuple struct or tuple variant constructor. On an
older MSRV the two constructor lines below fail with E0716:
fn make() -> String { String::from("alive") }
struct Wrapper<'a>(&'a String);
fn main() {
let value = &make();
let some = Some(&make());
let wrapped = Wrapper(&make());
assert_eq!(value, "alive");
assert_eq!(some.map(String::as_str), Some("alive"));
assert_eq!(wrapped.0, "alive");
}An ordinary function call does not extend the temporary:
fn make() -> String { String::from("short") }
fn keep(value: &String) -> &String { value }
fn main() {
let value = keep(&make());
println!("{value}");
}Fix this shape. Name the owner before the borrow:
fn make() -> String { String::from("long") }
fn keep(value: &String) -> &String { value }
let owner = make();
let value = keep(&owner);
assert_eq!(value, "long");Two-phase borrows
A two-phase mutable borrow starts as a reservation and becomes exclusive at activation. Shared access can occur between those points. Rust creates two-phase borrows only for these implicit borrows:
| Implicit shape | Typical example |
| Autoref of a mutable method receiver | values.push(values.len()) |
| Mutable reborrow in a function argument | Passing an existing &mut T to another call |
| Mutable borrow for overloaded compound assignment | place += rhs |
The standard method-call shape compiles:
fn main() {
let mut values = vec![10, 20];
values.push(values.len());
assert_eq!(values, [10, 20, 2]);
}An explicit &mut borrow is not two-phase. A manual UFCS rewrite can therefore fail even when
the method call compiles:
fn main() {
let mut values = vec![10, 20];
let receiver = &mut values;
Vec::push(receiver, values.len());
}Do not explain the first program as evaluation order alone. Record these points:
- The receiver autoref reserves
&mut values. - Rust evaluates the other arguments. Shared reads can occur during the reservation.
- The call activates the mutable borrow.
- The borrow ends after the call, unless the result keeps it alive.
Rust still rejects a conflicting write between reservation and activation.
Clone is not a scope operator
A clone that makes a borrow error disappear duplicates ownership. It does not explain the borrow. If the cause is a temporary scope, name the owner or add a block. If the cause is a two-phase borrow, keep the method-call shape or compute the argument first. Clone only when an independent copy matches the domain, for example a small value or shared read-only state. Do not clone a resource handle or a unique buffer to escape a scope.
Edition 2024 review
Run the two migration lints on the crate while it is still on edition 2021. They go quiet after the edition changes, because the behavior has already changed:
cargo clippy --locked -p <crate> --all-targets --all-features -- -W if_let_rescope -W tail_expr_drop_orderThe lints see only compiled code. Run the command once for each supported feature set and each
shipping target with cfg-gated code (--target <triple>).
Both lints are allow-by-default members of rust_2024_compatibility. Interpret the results:
cargo fix --editionrewrites anif_let_rescopesite into amatch. Thematchkeeps the edition 2021 lock hold, and any deadlock that came with it. Keep that hold only when theelsebranch must run while the lock is still held. Then prefer a named guard in an explicit block over the generatedmatch, because the block shows the hold. Otherwise restore theif letand accept the earlier drop.cargo fix --editionreportstail_expr_drop_ordersites but changes nothing. Inspect each site, and bind the temporary to a named local when its drop order matters.- Do not use an edition migration as a lock-lifetime refactor. Run behavior tests for code whose temporaries own locks, transactions, file descriptors, or other guards.
The feature-set and target rule, the migration workflow, the staged per-crate order, and the full
edition 2024 change list live in the cargo-workflows skill, when it is installed. Without it,
run cargo fix --edition once per supported feature set and once per shipping --target.
Verification
Use the smallest proof that decides the question:
- Put the exact expression in a standalone file. Do not simplify it first.
- Compile the probe on the workspace edition
(
rustc --edition <edition> --emit=metadata probe.rs). When the fix relies on a rule that is newer than the MSRV, compile the probe withrustc +<msrv>and the real fix withcargo +<msrv> check --locked. Examples:if/matchlifetime extension (1.79+),use<..>(1.82+; in a trait, 1.87+), constructor lifetime extension (1.89+), and macro temporaries (changed in 1.89-1.98). - Use a
compile_failprobe for a rejected borrow, and name the expected error code. - Use a runtime assertion, for example
try_lock().is_ok(), when the question is a drop point or a destructor order. - Run the real workspace tests after the probe. The probe proves the language rule, not the application invariant.
Do not use Miri to answer whether code type-checks. Use Miri after compilation when unsafe aliasing or provenance is also in scope.
Related skills
Each applies when it is installed.
| Skill | Boundary |
rust-compiler-errors | General E0499, E0502, E0597, and E0716 triage |
rust-pattern-semantics | Binding modes, match guards, if let guards, partial moves, and scrutinee behavior |
rust-callback-bounds | Closure inference and higher-ranked callable bounds |
rust-variance | Lifetime subtyping and coercion through type constructors |
cargo-workflows | Edition migration workflow and MSRV verification |
rust-unsafe | Aliasing, invalid values, and provenance after safe borrow checking ends |
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
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.

