Skip to content

Cromulent Copy closure captures - #4011

Open
Jules-Bertholet wants to merge 13 commits into
rust-lang:mainfrom
Jules-Bertholet:cromulent-copy-closure-captures
Open

Jules-Bertholet wants to merge 13 commits into
rust-lang:mainfrom
Jules-Bertholet:cromulent-copy-closure-captures

Conversation

@Jules-Bertholet

@Jules-Bertholet Jules-Bertholet commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Rendered

Change closure capture inference rules for Copy captures, allowing more code to compile without changing semantics for existing closures.

Some examples of closures that would start compiling:

fn callable(c: char) -> impl Fn() -> char {
    || c
}
fn main() {
    let cs: u32 = (0..0xCC).flat_map(|c| (0..c).map(|cc| c + cc)).sum();
    assert_eq!(cs, 4203318);
}

Implementation: rust-lang/rust#163717

@rustbot label T-lang A-closures

Important

Since RFCs involve many conversations at once that can be difficult to follow, please use review comment threads on the text changes instead of direct comments on the RFC.

If you don't have a particular section of the RFC to comment on, you can click on the "Comment on this file" button on the top-right corner of the diff, to the right of the "Viewed" checkbox. This will create a separate thread even if others have commented on the file too.

@Jules-Bertholet Jules-Bertholet changed the title RFC 4011: Cromulent Copy closure captures Cromulent Copy closure captures Oct 1, 2026
@rustbot rustbot added A-closures Proposals relating to closures / lambdas. T-lang Relevant to the language team, which will review and decide on the RFC. labels Oct 1, 2026
> 4. Mutable borrow (`MutBorrow`) --- The place expression is captured as a [mutable reference](https://doc.rust-lang.org/reference/types/pointer.html#mutable-references-mut).
> 5. Move (`ByValue`) --- The place expression is captured by [moving the value](https://doc.rust-lang.org/reference/expressions.html#moved-and-copied-types) into the closure.
>
> Place expressions from the environment are captured from the first mode that is compatible with how the captured value is used inside the closure body. The mode is not affected by the code surrounding the closure, such as the lifetimes of involved variables or fields, or of the closure itself.

@ds84182 ds84182 Oct 2, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be aware of lifetimes, otherwise it'll be easy to accidentally bloat the size of closures that previously borrowed instead of copied. If some place outlives the closure, capture by reference.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Borrowck happens after we decided on closure captures. We need MIR to borrowck, but MIR building needs the set of closure captures.

@Jules-Bertholet

Copy link
Copy Markdown
Contributor Author

I've added a note about potential breakage around the auto traits a closure is inferred to implement, and how we could resolve that breakage.

@Jules-Bertholet
Jules-Bertholet force-pushed the cromulent-copy-closure-captures branch from d0d566b to e417f07 Compare October 2, 2026 21:20
@clarfonthey

Copy link
Copy Markdown
Contributor

Thank you for creating an RFC that taught me what the word cromulent means. By which I mean you did not provide this information, but upon seeing the RFC, I looked it up.

@Jules-Bertholet

Copy link
Copy Markdown
Contributor Author

I've updated the RFC to clarify that capturing values behind a reference still happens by-reference:

let x = &([0; 1024],);
let c = || {
    let y = x.0; // x.0 captured by ImmBorrow
};

These aren't actually a problem;
if `&T` implements them, so does T.
@Jules-Bertholet

Copy link
Copy Markdown
Contributor Author

I've now implemented this RFC: rust-lang/rust#163717

The RFC text has been updated with lessons from the implementation.

@Jules-Bertholet
Jules-Bertholet force-pushed the cromulent-copy-closure-captures branch from 7bbe2d2 to c4d2536 Compare October 8, 2026 16:48
@Jules-Bertholet

Copy link
Copy Markdown
Contributor Author

I've added a note about potential breakage around the auto traits a closure is inferred to implement, and how we could resolve that breakage.

I've now gotten rid of this breakage entirely.

@Jules-Bertholet

Jules-Bertholet commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

I've added a special case for the comparison operators (==, !=, <, <=, >=, >) in order to resolve an issue fond during the Crater run.

@eddyb

eddyb commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

(only saw this due to looking at the list of most recent RFCs by accident, while looking something else up)

This reminds me of an idea @nikomatsakis and I have discussed at some point, but which may not be written down anywhere AFAIK:

Here, a and b are both captured by-ref but b doesn't live long enough:

let a = "a".to_string();
let concat_a_b = {
    let b = "b".to_string();
    || format!("{a}{b}")
}; // <- `b` drops here today, causing an error.
println!("{}", concat_a_b());

// Ideally, this would also work
// (i.e. `a` wouldn't need to be moved into the closure).
println!("{a}");

The error is even quite precise about what's going on:

closure may outlive the current block, but it borrows b, which is owned by the current block

Capture modes need to be known strictly before borrowck, and also, in the past, there have been decisions made against borrowck information controlling behavior (which is why let x = &foo(); rules were initially a list of special cases etc.).

But even during typeck, typeof(concat_a_b) (aka typeof({...; || ... }) aka typeof(|| ...)) is known to contain either typeof(b) or typeof(&b) (depending on the eventually-decided capture mode), simply because that closure captures b.

In many cases, slapping the move keyword on all affected closures is enough, but sometimes explicit references are needed, for the other captures (a in the example above) to not also get moved into the closure.


So a potential simple ruleset for a conservatively correct approximation of "guaranteed to fail borrowck because of scoping" (modulo dead code? not sure what the actual limits are), may be:

  1. expression E's type contains (i.e. is covariant/invariant wrt) closure C's type
    (if E is the function body, its type would need to be the function return type)
  2. closure C captures variable V
  3. variable V is syntactically outlived by E
    (e.g. E contains the parent block/match, that declares V)

As the expectation is that borrowck would fail, those rules could be used to determine that capture mode should be move, for every such variable V, without changing the behavior of any code that already compiles successfully.


Whether that kind of change should be made, is a different matter entirely, and there's a chance it's been proposed elsewhere already (and rejected/postponed), I have not looked into whether that had happened.

@Nadrieril

Copy link
Copy Markdown
Member

I'd say you're cheating on the "borrowck shouldn't influence captures" rule by morally doing a bit of lexical borrow-checking on HIR :p Interesting angle nevertheless.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-closures Proposals relating to closures / lambdas. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants