-
Notifications
You must be signed in to change notification settings - Fork 13.7k
don't apply temporary lifetime extension rules to non-extended super let
#145838
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
This comment has been minimized.
This comment has been minimized.
0542d4f
to
a35548f
Compare
Copied from #145784 (comment), since I think this is a notable caveat of this PR and worth considering before approving it: This comes with a bit of a gotcha in terms of temporary lifetimes: it might be strange that the non_extending({ let x = { &temp() }; f(x) }); // ok than in non_extending({ super let x = { &temp() }; f(x) }); // error Though the case for |
Also copied since it motivates this PR: I think something like this may be necessary for the identity &EXPR === { super let x = &EXPR; x } to hold in both extending and non-extending contexts without changing how block tail expression scopes work. Substituting, e.g. &{ &temp() } drops { super let x = &{ &temp() }; x } would extend it to outlive |
To confirm, with this PR, does this behavior hold (in Rust 2024)?: fn f<T>(_: LogDrop<'_>, x: T) -> T { x }
// These two should be the same.
assert_drop_order(1..=3, |e| {
let _v = f(e.log(2), &{ &raw const *&e.log(1) });
drop(e.log(3));
});
assert_drop_order(1..=3, |e| {
let _v = f(e.log(2), {
super let v = &{ &raw const *&e.log(1) };
v
});
drop(e.log(3));
});
// These two should be the same.
assert_drop_order(1..=3, |e| {
let _v = f(e.log(1), &&raw const *&e.log(2));
drop(e.log(3));
});
assert_drop_order(1..=3, |e| {
let _v = f(e.log(1), {
super let v = &&raw const *&e.log(2);
v
});
drop(e.log(3));
});
// These two should be the same.
assert_drop_order(1..=2, |e| {
let _v = &{ &raw const *&e.log(2) };
drop(e.log(1));
});
assert_drop_order(1..=2, |e| {
let _v = {
super let v = &{ &raw const *&e.log(2) };
v
};
drop(e.log(1));
}); (If any of these are missing, please add them as tests.) |
Does this PR affect any edition 2021 code? |
a35548f
to
f0c43cf
Compare
Those all hold under this PR, yes. I've added them all as tests (with some additional versioning to account for the Edition-dependent drop order in the first one); thanks!
Not that I'm aware of. That detail matters in Edition 2024, since the nested |
This comment has been minimized.
This comment has been minimized.
f0c43cf
to
387cfa5
Compare
// We have extending borrow expressions within the initializer | ||
// expression. |
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.
// We have extending borrow expressions within the initializer | |
// expression. | |
// We have extending borrow expressions within a non-extending | |
// expression within the initializer expression. |
(Revising my earlier text here.)
Team member @traviscross has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
cc @m-ou-se @rust-lang/libs-api |
387cfa5
to
23caea2
Compare
The real-looking failures seem to involve something like
There's a lot (186071) of prepare-fail crates, yeah. I think at least one of the real regressions is in there, since it showed up as a faulty dep but not in the normal regressed list. Edit: Looking at the test failures, it seems like there's a lot of tests for nondeterministic behavior (lots in particular relying on timing or random numbers). I don't have time to go through them all at the moment, but I can try opening a PR against the denylist later (or a few if breaking them up would help in reviewing). I imagine these are the sorts of things that should be |
This may be on the severe side, but instead of the approach this PR takes, I wonder if it would make sense to apply some form of lifetime extension to extending borrow expressions within block tail expressions, regardless of whether they appear in Is there any case where Slightly more precisely, I'm proposing that the grammar for extending expressions could serve two purposes:
I feel like this fits naturally into Rust's existing model of temporary lifetimes. In both cases, it's about extending the result of the temporary to match its use when the result value is known syntactically to only be okay to use2 if the reference is live. It also parallels the two cases for Sometime later I'll try throwing together a PR to experiment with this. Logistics-wise, I think that approach wouldn't need to be backported. I'll have to experiment of course (and make sure those crates that regressed under this PR don't regress under it), but my hope is it wouldn't break real code, so it could be left to cook as long as needed without accumulating regressed crates. Footnotes
|
Interesting point. Our current order here is rather challenging to explain because one can't work from the inside out. Relatedly, can you explain the behavior of "3"?: assert_drop_order(1..=3, |e| { //~ 1.
&({ &raw const *&e.log(1) }, drop(e.log(2)));
drop(e.log(3));
});
assert_drop_order(1..=3, |e| { //~ 2.
{ let _x; _x = &({ &raw const *&e.log(1) }, drop(e.log(2))); }
drop(e.log(3));
});
assert_drop_order(1..=3, |e| { //~ 3.
_ = &({ &raw const *&e.log(2) }, drop(e.log(1)));
drop(e.log(3));
}); |
I believe that's due to destructuring assignment expression desugaring. Since a pattern is being assigned to in an assignment expression, it actually turns into a assert_drop_order(#[lang = "range_inclusive_new"](1, 3), //~ 3.
|e|
{
{ let _ = &({ &raw const *&e.log(2) }, drop(e.log(1))); };
drop(e.log(3));
}); It's wrapped in a block, as above, which prevents temporaries from living past the end of the desugared destructuring assignment, but it does extend |
Interesting, makes sense. E.g., it does the same thing with: assert_drop_order(1..=3, |e| { //~ 5.
let _x; let _y;
(_x, _y) = ({ &raw const *&e.log(2) }, drop(e.log(1)));
drop(e.log(3));
}); We do document the desugaring of destructing assignments in the Reference, but we don't note anywhere the effect this has on drop order. Probably we should do that. cc @rust-lang/lang-docs This is, I think, an argument in favor of the semantic that you're proposing, as that would make the drop order between these cases more similar (as I understand your proposal). What do you think, would it make the drop order of temporaries in |
I think they'd be the exactly the same. The same rules would be used to determine the temporaries that live to the end of the block in |
Retrying the prepared-fails. |
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
We'd be breaking, e.g.: fn f(x: *const u8, y: *mut u8) -> u8 {
unsafe { y.write(42); x.read() }
}
fn main() {
let x = core::cell::RefCell::new((0u8, 0u8));
// Sound under TB.
f({ &raw const x.borrow().0 }, &raw mut x.borrow_mut().1);
} ...not that anyone would defend that code, I hope. I wonder if it'd break anything more defensible. |
Yes. Please do feel free to submit |
I'd suggest to go ahead and still submit PRs to the affected projects. Even if we might want to consider this new semantic, we may in the meanwhile not want to be accumulating code in the ecosystem that relies on this accidental divergence between the intended and the current behavior of |
Oh, true. I'll have to think on that. I'm not that familiar with what sorts of things are realistic in unsafe code though, so I may not be cut out to evaluate it entirely on my own. Speaking of breakages due to lifetime changes, I don't think it's been mentioned yet, but I think this PR can silently introduce UB in cases where raw pointers' pointee lifespans are shortened. e.g. if anyone wrote // Implicitly coerce to sneakily create a dangling pointer with `&`:
non_extending(pin!(if cond { raw_ptr } else { &temp() })); or non_extending(pin!({ Struct { raw_ptr: &temp() } }));
let pin1 = pin!(if cond { raw_ptr } else { &temp() });
let pin2 = pin!({ Struct { raw_ptr: &temp() } }); We've talked about the case where this shortening causes compilation failures (due to borrow-checking), but it totally slipped my mind that people could be relying on subtle lifetime extension behavior in unsafe code like that. |
First batch of flaky tests from the crater run at rust-lang/rust#145838 (comment)
I've reviewed 13 of the test failures in #145838 (comment) (top-to-bottom from |
Saethlin told me that I should wait for the retry before checking the test failures. |
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
🎉 Experiment
Footnotes
|
At a glance, the "build compiler error" failures look real, following the pattern of using In the mean time, I've implemented my lifetime extension suggestion for block tails. It's not ready for review yet, but it lives at #146098 now for testing/discussion purposes. |
Retrying since there's a lot of "cannot find directory"s. |
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
Reference PR: rust-lang/reference#1980
This changes the semantics for
super let
(and macros implemented in terms of it, such aspin!
,format_args!
,write!
, andprintln!
) as suggested by @theemathas in #145784 (comment), makingsuper let
initializers only count as extending expressions when thesuper let
itself is within an extending block. Sincesuper 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:f(pin!({ &temp() }))
droptemp()
at the end of the block in Rust 2024, whereas previously it would live until after the call tof
because syntactically thetemp()
was in an extending position as a result ofsuper let
inpin!
's expansion.super let
nested within a non-extendedsuper let
is no longer extended. i.e. a normallet
is required to treatsuper let
s as extending (in which case nestedsuper 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