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
Edition 2024: don't special-case diverging blocks #123590
base: master
Are you sure you want to change the base?
Conversation
6164863
to
6ccb844
Compare
6ccb844
to
c39e26c
Compare
I wonder about maybe special-casing keyword control flow here. Trying to push a "no, you can't write Given that we could change let Some(x) = foo else {
return;
}; everywhere, especially since we've been pushing people to that. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
@rfcbot merge We discussed this in @rust-lang/lang today. My takeaways from the meeting (and I think roughly meeting consensus) was...
Immediate next step being merged here: land this on nightly so that we can begin experimenting and make sure there aren't cases we've overlooked where this rule is important. |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members: Concerns:
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. |
|
||
/// Returns the edition-based default diverging block behavior | ||
fn default_diverging_block_behavior(tcx: TyCtxt<'_>) -> DivergingBlockBehavior { | ||
if tcx.sess.edition().at_least_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.
Is this right? Couldn't the block come from an older-edition macro?
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 is not right, good point. I was thinking so much about the never type fallback change (which has to be crate-global) that I forgot we can have more local things 🙈
Will change it to check the edition per-block.
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.
Should we put this behind a feature gate? Especially if it's for experimenting. We can make a "stabilization PR" that removes the need for the gate once it's settled.
@rfcbot concern return-churn Given that we're been encouraging let … else {
return;
}; in the current edition, I think jumping directly to "nope, hard error" on an edition edge is too severe. Since as I read this it would make this part of the 2024 edition and I think right now that I'd block on that going to stable, I think a concern is procedurally right here? I don't want to get to Sept and raise a concern then on the whole edition. I'm absolutely in favour of experimenting with this in nightly, but maybe as a (Or, as described above in #123590 (comment) , I'd also be happy with a less strong version of this that doesn't break keyworded divergence.) |
@scottmcm I think calling functions returning The reason I think this makes sense as an edition change is that the fix is so easy. Both in "I want to prepare for the next edition"2 and in "I changed the edition, now my code doesn't compile" ways3. So while this will affect many, I think the impact is very light. Also of note that the change will only make a difference when the type of the block is expected to be something, so not all blocks with the If you have a specific experiment or a question we could test on nightly, I'm happy to do that. But otherwise, I'm not sure what we can do here. (as an aside, I think the concern is 100% procedurally right) Footnotes
|
I agree that t-style getting pulled into approval was undesirable, but this does seem to involve rustfmt & style to a significant enough extent that I'd like to ensure both teams are aware and following the conversation cc @rust-lang/rustfmt for awareness |
I don't think this is waiting on review... It's waiting either on fcp/team or blocked on rustfmt work, namely rust-lang/rustfmt#6149. I'm going to mark this as the latter. @rustbot blocked |
Just putting in my two cents here, as a lang advisor I guess: I think this is one of those cases where on paper the motivation makes sense, but in practice it's pretty weird. First, I would expect this change to be much bigger than the examples given in this thread already. The let else example @scottmcm gave also applies to Today, what is the type of a block where there is dead code after a diverging statement without a semicolon? If the answer is |
The "hard edge" problem sounds like it's resolved, so that it's possible to check in rustfmt-compliant code that compiles before and after the edition, with the (We might get a new one from the discussion in triage today, but I want to acknowledge the work done already.) |
@rfcbot concern is it worth the churn I'd like to get an idea of how much churn there is, so we can make a decision about the tradeoffs. So far this feels like more of a theoretical improvement, but one that has very concrete churn (though it is automated at least). I'm not sure if that churn is worth it. In the lang team meeting today we discussed using the standard library as a testing ground.
Most tests are not using edition 2024, which is one reason the change wouldn't be large. |
@rustbot labels -I-lang-nominated +S-waiting-on-author As tmandry mentioned, we discussed this in the lang call on 2024-04-24. The specific next step to move this forward is to convert the standard library (and perhaps There was discussion that if this seems like too much for Rust 2024, then perhaps we might plot a course for doing this in the edition after, as there seemed to be a general feeling that if we had a time machine, we would do this. Please renominate when the diff is posted. Thanks to @WaffleLapkin for pushing this forward. |
@jackh726 the answer is indeed |
@tmandry here is a commit which fixes the standard library: 6e09358 (I'm not pushing it here, since it currently fails rustfmt). Summary:
The last part is the only annoying thing in my opinion. Here is an example: fn f() -> u8 {
return helper();
fn helper() -> u8 { 1 }
} the fn f() -> u8 {
fn helper() -> u8 { 1 }
helper()
} I think this looks fine, there aren't that many changes (git statistic looks bad, because it doesn't understand moves), given how big the standard libraries are and given that almost all changes can be auto-applied. That being said I'm not sure how often helper functions at the end of an outer function like the above are used. @traviscross I did not change the compiler, because there is currently no way to change the behavior in all |
So, consistent is maybe not the completely correct word. I'm trying to come up with good examples to try to show my thoughts, but the biggest thing to me is that in every other piece of code, when we write a This is also kind of true for returns at the end of functions (even though this isn't recommended style): fn foo() -> u32 {
return 0;
} Under this PR, you could expect that with the fn foo() -> u32 {
return 0;
()
} (under the logic that a new statement of
Thinking about it, the last point above is sort of why this is a thing: because we don't expect that a return statement in a function to add an implicit I guess, all in all, today we treat I can see where the logic of this PR comes from: "blocks without a expression at the end get an implicit I think if we wanted to make this change and it not be extremely disruptive (not necessarily just code, but to the way we think about writing Rust), we should probably do it in steps (similar to |
Realizing that I didn't connect my thoughts back to my original question (and realizing that the phrasing of my question was nonsense anyways). And, the answer was in the OP the whole time anyways. One thing to note is that with the changes here, I'm not sure it's possible to write a block with dead code after a diverging expression without also making the final expression be the real type or also diverging. It's very minor, but can make a debugging development cycle a bit more difficult. E.g. you can no longer stick a That being said, my above comment better explains my thoughts on consistency here anways. |
Inserting I do expect a
I'm not sure what this means.
Again, not sure what this means,
I don't think we need this and I don't think this can happen. rustfmt should not change semantics, and with this PR removing a semicolon does change things. So rustfmt should only preserve the existence or lack of the semicolon. Adding a lint alike |
I'm well aware of where the implicit empty tuple expression comes from. What I mean is that when somebody has written
It's not that it must be different, it's that writing
If you think about a return expression at the end of a fn block versus a non-diverging expression: typically, the type of the last expression is the type of the block. So, diverging expressions also make the block diverging. But this not true of return: the type of the expression is diverging but the type of the block is not. Similar for break in other blocks.
Not sure what I meant here.
Today they do mean the same thing though. And the code after this PR more closely matches the code without a semicolon.
But I don't think that criteria goes far enough: then you end up in a situation where some returns have a semicolon and some don't. |
We discussed this in the lang triage call today. Seeing the migration of the standard library was helpful. Thanks to @WaffleLapkin for that. Many people found compelling the changes that looked like this: pub fn abort() -> ! {
- crate::sys::abort_internal();
+ crate::sys::abort_internal()
} let Some(id) = last.checked_add(1) else {
- exhausted();
+ exhausted()
}; ...and wanted to accept the churn for these. However, people remain uncertain about cases like this: } else {
- return false; // other is empty
+ return false // other is empty
}; In particular, the concern here is a feeling of whiplash given that, due to In the meeting, interest converged around what we called Option 2, which is:
To solidify this tentative consensus, we wanted to first lay out and review some examples. Those examples follow. Examples for further discussionRust currently rejects these: fn foo(x: bool) {
let true = x else {
return; // or break, continue
()
};
//~^ ERROR `else` clause of `let...else` does not diverge
} fn foo(x: bool) {
let true = x else {
();
};
//~^ ERROR `else` clause of `let...else` does not diverge
} Rust currently accepts these: fn foo(x: bool) {
let true = x else {
return // or any diverging expression
};
} fn foo(x: bool) {
let true = x else {
return; // or any diverging expression
};
} fn foo(x: bool) {
let true = x else {
return; // or any diverging expression
();
};
} fn foo(x: bool) {
let true = x else {
panic!(); // or unimplemented!(), todo!(), etc.
};
} fn foo() -> ! {
foo();
} In Rust 2024, under Option 2, we would reject these: fn foo(x: bool) {
let true = x else {
return; // or `continue` or `break`
();
//~^ ERROR when the block must diverge, any dead code that
//~| follows `return` must also diverge
};
} fn foo() -> ! {
foo();
//~^ ERROR type mismatch
} fn foo() -> ! {
foo();
();
//~^ ERROR type mismatch
} In Rust 2024, we would still of course allow: fn foo(x: bool) {
let true = x else {
return; // or any diverging expression
();
todo!()
};
} And in Rust 2024, we would continue to allow: fn foo(x: bool) {
let true = x else {
return; // or `continue` or `break`
};
} ...and: fn foo(x: bool) {
let true = x else {
panic!(); // or unimplemented!(), todo!(), etc.
};
} Trailing itemsIndependently of this, but prompted by the observation that some cases of where updates in the standard library were needed were where items followed the trailing expression in a function (rather than being at the top of the function), we discussed whether we should just make that work. E.g., we might want to allow: fn foo() {
return
fn bar() {} // or other items, e.g., `const C: u32 = ..`
} fn foo() -> u8 {
42
fn bar() {} // or other items, e.g., `const C: u32 = ..`
} Next stepsWe'll pick this back up in our next meeting. |
I honestly think the conclusion drawn in the lang meeting make a lot of sense and probably result in a net positive all around. I could elaborate more, but I think my comments above probably say all that I could here anyways. I'm happy with the conclusions made. |
Normally, when a block has no tail-expression its type is
()
:However, this is not the case when the block is known to diverge, in which case its type becomes
!
1:I think that this is a useless special case and unnecessary complicates the language. I propose that we remove it in the next edition.
Note that you can always fix the code that used this special case by removing a
;
(and removing dead-code, if there is any). We already have a machine-applicable fix (as can be seen in the test added in this PR).Also note that while this is related to the never type, this change is independent of the work on its stabilization. I personally think that if we are going to stabilize
!
, we should make it less weird and this is one of the ways to do that; but, this is not required for!
stabilization and is a completely separate cleanup.The only hard part of this change is that
rustfmt
currently adds;
toreturn
s which are at the end of blocks. We'll have to change rustfmt style in this regard in order to support the next edition. Out options are;
, but not add;
(this won't break any existing users);
in the edition<=2021 and remove;
in the edition>=2024 (this will automatically fix code when porting to the new edition) (does rustfmt even support editions?...);
(this will break all existing formatting, I don't think that's desirable)r? compiler-errors
Footnotes
it then immediately decays to
()
due to current never type fallback, but it does not matter -- you can still specify any type and the block will be coerced to that ↩