-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Do not materialise X in [X; 0] when X is unsizing a const #145277
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?
Do not materialise X in [X; 0] when X is unsizing a const #145277
Conversation
r? @nnethercote rustbot has assigned @nnethercote. Use |
This comment has been minimized.
This comment has been minimized.
6a64501
to
b7b9519
Compare
A question to @traviscross ... Should we also need to run this through |
Well, let's see. It looks like this issue does affect the user-visible stable behavior of the language: use core::{mem, ptr, sync::atomic::{AtomicU64, Ordering}};
static COUNT: AtomicU64 = AtomicU64::new(0);
struct S;
impl Drop for S {
fn drop(&mut self) {
COUNT.fetch_add(1, Ordering::Relaxed);
}
}
trait Tr {}
impl Tr for S {}
const X: Box<dyn Tr> = unsafe {
mem::transmute::<_, Box<S>>(ptr::dangling_mut::<S>())
};
fn main() {
assert_eq!(0, COUNT.load(Ordering::Relaxed));
let _a: &'static [Box<dyn Tr>; 0] = &[X; 0];
assert_eq!(1, COUNT.load(Ordering::Relaxed));
//~^ Drop was run.
let _b = [X; 0];
assert_eq!(1, COUNT.load(Ordering::Relaxed));
//~^ Drop wasn't run.
} So, yes, it's ours to fix via FCP. Anyone think we need a crater run? For my part, I could go either way on that, and I do want us to fix this, so let's: @rfcbot fcp merge @rustbot labels +I-lang-nominated +needs-fcp cc @rust-lang/lang @rust-lang/lang-advisors |
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. |
Another fun edge case where this change is visible in user code: This code compiles with this PR, but fails on nightly due to dropping in const use std::rc::Weak;
const X: Weak<i32> = Weak::new();
const Y: [Weak<dyn Send>; 0] = [X; 0]; |
@@ -656,6 +657,24 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { | |||
block.and(rvalue) | |||
} | |||
|
|||
/// Recursively inspect a THIR expression and probe through unsizing | |||
/// operations that can be const-folded today. | |||
fn check_constness(&self, mut ek: &'a ExprKind<'tcx>) -> bool { |
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.
s/ek/kind/
I've never seen ek
used for an ExprKind
, kind
is the normal name.
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.
Thanks! Renamed.
The code changes seem fine, speaking as a reviewer who is not at all familiar with the MIR building and relevant lang details. The example from @theemathas might be worth adding to the test case. |
b7b9519
to
c1ab3ed
Compare
@bors try |
This comment has been minimized.
This comment has been minimized.
…<try> Do not materialise X in [X; 0] when X is unsizing a const
Okay, I will request a crater run ahead of the next meeting. |
I will push a commit to include the second test cases. |
Hello team. I would like to request
for a quick triage. 🙇 |
Signed-off-by: Ding Xiang Fei <dingxiangfei2009@protonmail.ch> Co-authored-by: Theemathas Chirananthavat <theemathas@gmail.com>
I'm not sure, I didn't page this back in -- I just remembered seeing something about empty arrays before. |
In the lang call, we noted (h/t @joshtriplett) that another observable manifestation of this is when a post-mono panic occurs or does not occur, e.g.: use std::rc::Weak;
fn main() {
const X: Weak<dyn Send> = Weak::<u8>::new();
let _a: &'static [Weak<dyn Send>; 0] = &[const { panic!(); X }; 0];
//~^ Panics.
let _b = &[const { panic!(); X }; 0];
//~^ Does not panic.
} |
@rfcbot reviewed My understanding of what is happening here is that, before, we had some special-case code to avoid dropping I don't 100% understand why why |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
@rfcbot reviewed |
Here's the way I understand the problem: When the type is annotated, the compiler inserts a no-op unsize coercion that turns The I think of the fix in this PR as effectively changing the expression to Applying a similar construction to arrays with nonzero lengths would fix #140123. (Not done in this PR.) In that issue, a similar coercion causes the compiler to try to copy a non-Copy type when creating an array with length 2 or more. (See #140123 (comment) for a simplified repro.) |
@@ -656,6 +657,24 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { | |||
block.and(rvalue) | |||
} | |||
|
|||
/// Recursively inspect a THIR expression and probe through unsizing | |||
/// operations that can be const-folded today. |
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.
What does this have to do with const-folding (which usually refers to the constant propagation optimization pass)?
Hm, this doesn't seem very satisfying... this will also treat explicit coercions introduces by an Is it really worth having a special case for I guess this could become tricky in the general case where the array length is generic... |
I think it is fine that
|
Do const X: HasDrop = .....
fn generic<const N: usize>() {
let _ = [X; N];
}
fn one() {
generic::<0>();
}
fn two() {
let _ = [X; 0];
} |
That does not sound fine; the behavior should be the same in generic and monomorphic code. |
struct HasDrop;
impl Drop for HasDrop {
fn drop(&mut self) {
println!("drop!");
}
}
fn main() {
let x = HasDrop;
let y = [x; 0];
println!("after array");
} This prints
so the move+drop of That said, in general if |
I am testing the following snippet. Playground use std::sync::atomic::{AtomicUsize, Ordering};
struct X;
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
impl Drop for X {
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
}
}
fn main() {
one();
assert_eq!(DROP_COUNT.load(Ordering::Relaxed), 0);
}
fn generic<const N: usize>() {
const A: X = X;
let _ = [A; N];
}
fn one() {
generic::<0>();
} This runs without assertion failure on stable. Inspecting the MIR after the runtime optimisation phase,
So, in some sense our generic code is already working differently from monomorphic code on stable today. I have reflected on my earlier comment and I want to retract it. Instead I now agree that we should take the monomorphised code as the golden benchmark. This is what I would think, a better way to solve this. I would like to propose that we build the MIR slightly differently, [ $expr; $repeat ] ... shall be built into a MIR that is roughly equivalent to if $($repeat equals the const zero) {
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This will most likely be optimised away;
// if not, we can investigate further how we could
// optimise harder after monomorphism.
try_const_promotion_orelse_instantiate_and_drop!($expr);
[]
} else {
[ build_and_move($expr); $repeat ]
} Here what I mean by What this PR concerns about is the case when a const literal is behind a pointer coercion, which is promoted into a const today. |
Is it? If I make the code monomorphic, the assertion still passes: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=0c47f27743a2e5b1f8431ea679d8fd20 |
I really want to say, yes it is and , but apparently I cannot write a code that demonstrate this in the runtime because when constant propagation is concerned we can only work with types without More concretely speaking, given My question is, is my proposal in the last comment reasonable? I would like to take this opportunity to make consensus. If the idea is interesting, I can further explain what I mean by |
I am a bit confused by your mention of const promotion... there's no |
I have the example in which as The |
Stable version (no nightly features) of the above inconsistency: use std::mem::transmute;
use std::ptr::dangling_mut;
struct Thing;
impl Drop for Thing {
fn drop(&mut self) {
println!("drop");
}
}
const X: Box<dyn Send> = unsafe { transmute::<*mut Thing, Box<Thing>>(dangling_mut()) };
fn main() {
let _a: &'static [Box<dyn Send>; 0] = &[X; 0];
} |
Oh yeah that's an interesting case. Promotion seems to say "this array is empty, so there's no destructor, so we can promote", but MIR building already inserted a direct call to the destructor... but that call has no data dependency with the actual array so promotion doesn't even see it as related to the array. I think a fundamental part of the problem is that we have various different "modes of use" of array expressions that need to be considered to desugar in different ways. We allow
These conditions can overlap, and specifically when 2 and 3 overlap there's an observable difference since 3, in general, should run the destructor for I feel like we need to sort out the entire desugaring story here, considering not only #143671 but also #140123. It seems what happens is that some early analysis concludes that the array is legal based on case 2, but then later due to the coercion it doesn't look like a case 2 any more, and then it either ICEs (since no case fits) or starts doing weird drop things (since case 3 was used). What does this PR do with #140123 (specifically examples like this)? And how sure are we that |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
Fix #143671
It turns out that MIR builder materialise
X
in[X; 0]
into a temporary local whenX
is unsizing aconst
. This led to a confusing call to destructor ofX
when such a destructor is declared. PlaygroundThis patch may miss out other cases that we should avoid materialisation in case of
[X; 0]
. Suggestions to include is most welcome!