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 4 times, most recently from afa6088 to 9efbc95 Compare September 10, 2025 04:20
@traviscross traviscross force-pushed the extending-macros branch 3 times, most recently from 58e85ee to 899ab5e Compare September 10, 2025 05:17
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-stabilization Waiting for a stabilization PR to be merged in the main Rust repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants