Skip to content

Conversation

dianne
Copy link
Contributor

@dianne dianne commented Aug 26, 2025

Reference PR for rust-lang/rust#145838, given the format_args! change in rust-lang/rust#145882. cc @m-ou-se

Based on #1979; the first commit is the commit from that PR.

@rustbot rustbot added the S-waiting-on-review Status: The marked PR is awaiting review from a maintainer label Aug 26, 2025
@dianne dianne force-pushed the extending-macros branch 3 times, most recently from 1c9c73c to 53de4ae Compare August 28, 2025 06:18
Comment on lines 444 to 501
r[destructors.scope.lifetime-extension.exprs.borrow]
The operand of any extending borrow expression has its temporary scope
extended.

r[destructors.scope.lifetime-extension.exprs.macros]
The built-in macros [`pin!`] and [`format_args!`] create temporaries.
Any extending [`pin!`] or [`format_args!`] [macro invocation] expression has an extended temporary scope.
Copy link
Contributor Author

@dianne dianne Aug 28, 2025

Choose a reason for hiding this comment

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

Fourth pass: I've broken up destructors.scope.lifetime-extension.exprs to match destructors.scope.lifetime-extension.patterns and to put the rule for built-in macros' temporaries in a subsection. I've also moved the rule for arguments' extension back into the the newly-delimited rule destructors.scope.lifetime-extension.exprs.extending. I'm not satisfied with the wording yet, but structurally I think it's an improvement.

I'm doing a bit of conflation here. The "temporaries" here are both:

  • super let bindings; since they have (extended) temporary scopes, I feel like referring to them as "temporaries" is most fitting for the moment.
  • The borrowed temporaries created when a value expression is passed to format_args!.

Let me know if it needs further clarification. My hope is that it's a suitable level of detail for how these macros behave, to avoid specifying their exact expansion.

Copy link
Contributor

Choose a reason for hiding this comment

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

My reading is that this rule may not be needed at all. The "extending based on expressions" section states a recursive algorithm for determining, starting on the outside and working inward, which expressions are extending. This definition then serves the following rule, which is the one that has language effect:

The operand of any extending borrow expression has its temporary scope extended.

That's the whole game, right there, I think, is deciding whether a particular borrow expression is extending.

In that context, then, the only thing that matters with respect to these built-in macros is whether expressions in their argument positions are extending expressions.

(I'll push a revision to drop this rule.)

Copy link
Contributor Author

@dianne dianne Sep 8, 2025

Choose a reason for hiding this comment

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

In that context, then, the only thing that matters with respect to these built-in macros is whether expressions in their argument positions are extending expressions.

Unless I'm missing something, I don't think that's sufficient to describe the behavior of pin! and format_args!. I was struggling to write out the missing rule though, since it can't quite be expressed precisely with stable terminology: the bindings of a super let statement in an extending block expression have their scopes extended1. In that way, super let bindings are scoped like borrowed temporaries. The compiler implementation is spread across here and here.

This can be observed through pin! and format_args!:

In pin!($expr), the result of $expr is moved into a super let binding, giving it the same scope a borrowed temporary would have: if the pin! invocation is extending, its scope is extended, and otherwise, it's dropped in the enclosing temporary scope. Since pin! moves its argument, this can't simply be described as it borrowing it, but the Pin doesn't own it either. To enforced pinnedness, it has to treat its argument as a value, but it also needs to scope it like it's borrowed; this (as I understand it) is why super let is needed in Rust 20242.

format_args! does borrow its arguments, but super let's unique scoping can be observed there too: even if format_args! is borrowing from long-lived places, the super let bindings created to store the arguments have the scope a borrowed temporary would: they have extended scopes if the format_args! invocation is extending, and otherwise they're dropped in the enclosing temporary scope.

Footnotes

  1. Arguments to extending pin! and format_args! invocations being extending covers a separate super let property: the initializer of super let in an extending block is extending. This is what don't apply temporary lifetime extension rules to non-extended super let rust#145838 affects.

  2. Likewise the reason Pin { __pinned: &mut { $expr } } works is it forces $expr to evaluate to a temporary in the appropriate scope (on account of fields and block tails of extending struct and block expressions being extending). Effectively, the super let-based implementation captures that property without the block tail scope being a problem in Rust 2024.

Copy link
Contributor

@traviscross traviscross Sep 8, 2025

Choose a reason for hiding this comment

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

Right. The mental model that I think is correct is that pin!($expr) is, in this regard, exactly like &pin mut $expr, which is to say that the argument/operand is a place expression context, that the operand is an extending expression when the borrow is, and that the operand of such an extending borrow has its temporary scope extended.

If that's right, the cleanest way I can think of to express this is to create a concept of a "borrow macro call expression", and then to reframe the rules for extending based on expressions to work with these.

Let me know if that looks right.

Copy link
Contributor Author

@dianne dianne Sep 8, 2025

Choose a reason for hiding this comment

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

With regard to pin!, I think the operand is a value expression. In the current implementation, the super let mut pinned = $value; binds the operand by value, effectively evaluating it to a temporary1. In the old implementation it uses a block tail to force a value expression context. Functionally, pin! has to move out of its operand, to ensure that its operand can't be moved after being pinned: https://doc.rust-lang.org/nightly/std/pin/macro.pin.html#remarks. If its operand was just borrowed, the place would be able to be moved from after the Pin was no longer in use, violating Pin's invariant.

format_args!'s non-format-string arguments are place expressions and implicitly borrowed, so the inclusion as a borrow macro call expression works2. I don't think it's sufficient to describe its behavior though, since the returned fmt::Arguments also borrows from temporaries created by format_args!, which may have shorter scopes than its operands (playground).

In both cases, the best I was able to come up with was that pin! and format_args! themselves create temporaries3, the scopes of which can be extended4. This is especially clear in the old implementation of pin!: in Pin { __pointer: &mut { $value } }, { $value } evaluates to a temporary that may be extended because it's the operand of a borrow expression.

Footnotes

  1. Technically, the initializer of a super let is a place expression, but then creating the mut pinned binding moves out of that place, effectively treating it like it like a value expression.

  2. A wording complexity though: format_args!("{x}") borrows x despite it not appearing "after" the format string.

  3. Currently implemented as super let bindings.

  4. Because the bindings of a super let in an extending block have extended scopes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right. This is I think the key:

I was struggling to write out the missing rule though, since it can't quite be expressed precisely with stable terminology...

It's just too hard to be precise here without introducing terms, so I've pushed a revision that does go ahead and introduce new terms.

Let me know what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This looks right to me1. I think introducing terms is the right call; that helped a lot with precision and clarity. Thanks for writing that out!

Footnotes

  1. Though I think one of the examples is left over from an old revision? I'll leave a comment on it.

@rustbot

This comment has been minimized.

@traviscross traviscross added S-waiting-on-stabilization Waiting for a stabilization PR to be merged in the main Rust repository and removed S-waiting-on-review Status: The marked PR is awaiting review from a maintainer labels Sep 8, 2025
@traviscross traviscross changed the title specify lifetime extension of pin! and format_args! arguments Specify lifetime extension of pin! and format_args! arguments Sep 8, 2025
@traviscross traviscross force-pushed the extending-macros branch 12 times, most recently from 3b2e90f to 4095838 Compare September 8, 2025 10:35
@traviscross traviscross force-pushed the extending-macros branch 6 times, most recently from 58e85ee to 899ab5e Compare September 10, 2025 05:17
jhpratt added a commit to jhpratt/rust that referenced this pull request Sep 17, 2025
…jackh726,traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by `@theemathas` in rust-lang#145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes rust-lang#145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in rust-lang#145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside rust-lang#145784: `@rustbot` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: rust-lang#139076
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2025
…jackh726,traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by ``@theemathas`` in rust-lang#145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes rust-lang#145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in rust-lang#145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside rust-lang#145784: ``@rustbot`` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: rust-lang#139076
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2025
…jackh726,traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by ```@theemathas``` in rust-lang#145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes rust-lang#145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in rust-lang#145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside rust-lang#145784: ```@rustbot``` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: rust-lang#139076
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 17, 2025
…jackh726,traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by ````@theemathas```` in rust-lang#145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes rust-lang#145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in rust-lang#145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside rust-lang#145784: ````@rustbot```` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: rust-lang#139076
rust-timer added a commit to rust-lang/rust that referenced this pull request Sep 17, 2025
Rollup merge of #145838 - dianne:non-extending-super-let, r=jackh726,traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by ````@theemathas```` in #145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes #145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in #145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside #145784: ````@rustbot```` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: #139076
@traviscross traviscross force-pushed the extending-macros branch 4 times, most recently from 8c15a25 to 2404e93 Compare September 20, 2025 00:35
Rather than discussing the built-in macros directly in the context of
extending expressions, let's define "super macros", "super operands",
and "super temporaries".  It's unfortunate to have to introduce so
many terms, but it still seems a bit clearer as the terms help to
disentangle the many different things at play.

Since the fix to `format_args!` hasn't landed yet, we'll state the
intended rule and leave a note about the current situation.
@traviscross
Copy link
Contributor

I've updated this to account for

not having merged yet so that we can merge this.

@traviscross traviscross added this pull request to the merge queue Sep 20, 2025
@traviscross traviscross removed the S-waiting-on-stabilization Waiting for a stabilization PR to be merged in the main Rust repository label Sep 20, 2025
Merged via the queue into rust-lang:master with commit 268200d Sep 20, 2025
5 checks passed
github-actions bot pushed a commit to rust-lang/rustc-dev-guide that referenced this pull request Sep 22, 2025
…traviscross

don't apply temporary lifetime extension rules to non-extended `super let`

Reference PR: rust-lang/reference#1980

This changes the semantics for `super let` (and macros implemented in terms of it, such as `pin!`, `format_args!`, `write!`, and `println!`) as suggested by ````@theemathas```` in rust-lang/rust#145784 (comment), making `super let` initializers only count as [extending expressions](https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions) when the `super let` itself is within an extending block. Since `super let` initializers aren't temporary drop scopes, their temporaries outside of inner temporary scopes are effectively always extended, even when not in extending positions; this only affects two cases as far as I can tell:
- Block tail expressions in Rust 2024. This PR makes `f(pin!({ &temp() }))` drop `temp()` at the end of the block in Rust 2024, whereas previously it would live until after the call to `f` because syntactically the `temp()` was in an extending position as a result of `super let` in `pin!`'s expansion.
- `super let` nested within a non-extended `super let` is no longer extended. i.e. a normal `let` is required to treat `super let`s as extending (in which case nested `super let`s will also be extending).

Closes rust-lang/rust#145784

This is a breaking change. Both static and dynamic semantics are affected. The most likely breakage is for programs to stop compiling, but it's technically possible for drop order to silently change as well (as in rust-lang/rust#145784). Since this affects stable macros, it probably would need a crater run.

Nominating for discussion alongside rust-lang/rust#145784: ````@rustbot```` label +I-lang-nominated +I-libs-api-nominated

Tracking issue for `super let`: rust-lang/rust#139076
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Sep 22, 2025
Update books

## rust-lang/book

1 commits in 3e9dc46aa563ca0c53ec826c41b05f10c5915925..33f1af40cc44dde7e3e892f7a508e6f427d2cbc6
2025-09-15 16:10:14 UTC to 2025-09-15 16:10:14 UTC

- Release trpl 0.3 (rust-lang/book#4505)

## rust-lang/reference

9 commits in b3ce60628c6f55ab8ff3dba9f3d20203df1c0dee..cc7247d8dfaef4c39000bb12c55c32ba5b5ba976
2025-09-20 10:26:26 UTC to 2025-09-08 18:07:29 UTC

- Document temporary scoping for destructuring assignments (rust-lang/reference#1992)
- Specify lifetime extension of `pin!` and `format_args!` arguments (rust-lang/reference#1980)
- update for more ABIs supporting c-variadics (rust-lang/reference#1936)
- Fix incorrect span tag (rust-lang/reference#1995)
- Remove strike attribute (rust-lang/reference#1997)
- Specify the target limits for target-specific ABIs (rust-lang/reference#2000)
- Remove tuple index carve out (rust-lang/reference#1966)
- Enable folding of chapter listing in navigation sidebar (rust-lang/reference#1988)
- Add support to grammar for single line comments (rust-lang/reference#1993)

## rust-lang/rust-by-example

1 commits in dd26bc8e726dc2e73534c8972d4dccd1bed7495f..2c9b490d70e535cf166bf17feba59e594579843f
2025-09-18 22:28:52 UTC to 2025-09-18 22:28:52 UTC

- Update unit testing output for additional test (rust-lang/rust-by-example#1958)
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 23, 2025
Update books

## rust-lang/book

1 commits in 3e9dc46aa563ca0c53ec826c41b05f10c5915925..33f1af40cc44dde7e3e892f7a508e6f427d2cbc6
2025-09-15 16:10:14 UTC to 2025-09-15 16:10:14 UTC

- Release trpl 0.3 (rust-lang/book#4505)

## rust-lang/reference

9 commits in b3ce60628c6f55ab8ff3dba9f3d20203df1c0dee..cc7247d8dfaef4c39000bb12c55c32ba5b5ba976
2025-09-20 10:26:26 UTC to 2025-09-08 18:07:29 UTC

- Document temporary scoping for destructuring assignments (rust-lang/reference#1992)
- Specify lifetime extension of `pin!` and `format_args!` arguments (rust-lang/reference#1980)
- update for more ABIs supporting c-variadics (rust-lang/reference#1936)
- Fix incorrect span tag (rust-lang/reference#1995)
- Remove strike attribute (rust-lang/reference#1997)
- Specify the target limits for target-specific ABIs (rust-lang/reference#2000)
- Remove tuple index carve out (rust-lang/reference#1966)
- Enable folding of chapter listing in navigation sidebar (rust-lang/reference#1988)
- Add support to grammar for single line comments (rust-lang/reference#1993)

## rust-lang/rust-by-example

1 commits in dd26bc8e726dc2e73534c8972d4dccd1bed7495f..2c9b490d70e535cf166bf17feba59e594579843f
2025-09-18 22:28:52 UTC to 2025-09-18 22:28:52 UTC

- Update unit testing output for additional test (rust-lang/rust-by-example#1958)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants