-
Notifications
You must be signed in to change notification settings - Fork 550
Specify lifetime extension of pin!
and format_args!
arguments
#1980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
9adbc21
to
64644b2
Compare
64644b2
to
43c81b3
Compare
1c9c73c
to
53de4ae
Compare
src/destructors.md
Outdated
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
-
Arguments to extending
pin!
andformat_args!
invocations being extending covers a separatesuper let
property: the initializer ofsuper let
in an extending block is extending. This is what don't apply temporary lifetime extension rules to non-extendedsuper let
rust#145838 affects. ↩ -
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, thesuper let
-based implementation captures that property without the block tail scope being a problem in Rust 2024. ↩
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
-
Technically, the initializer of a
super let
is a place expression, but then creating themut pinned
binding moves out of that place, effectively treating it like it like a value expression. ↩ -
A wording complexity though:
format_args!("{x}")
borrowsx
despite it not appearing "after" the format string. ↩ -
Currently implemented as
super let
bindings. ↩ -
Because the bindings of a
super let
in an extending block have extended scopes. ↩
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment has been minimized.
This comment has been minimized.
53de4ae
to
64f24fb
Compare
pin!
and format_args!
argumentspin!
and format_args!
arguments
3b2e90f
to
4095838
Compare
afa6088
to
9efbc95
Compare
58e85ee
to
899ab5e
Compare
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.
899ab5e
to
29cafd4
Compare
Reference PR for rust-lang/rust#145838, given the
format_args!
change in rust-lang/rust#145882. cc @m-ou-seBased on #1979; the first commit is the commit from that PR.