Make LazyCell<T, F> and LazyLock<T, F> covariant in both T and F - #159838
WaffleLapkin wants to merge 2 commits into
Conversation
LazyCell<T, F> and `LazyLock<T, F> covariant in both T and F LazyCell<T, F> and LazyLock<T, F> covariant in both T and F
5da2332 to
01615ea
Compare
This comment has been minimized.
This comment has been minimized.
| // | ||
| // **NOTE**: the important invariants here are that | ||
| // 1. `T` can only be written through `&LazyCell<T, F>` by being returned from `F` | ||
| // 2. `F` cannot be overwritten after `LazyCell` creation |
There was a problem hiding this comment.
This two assumptions are both fragile. We might want to provide an API in the future to clear a LazyCell similar to OnceCell, and we might want to provide an API to set the value directly without the callback. Making this type covariant in T will prohibit both.
I think the safe thing to do is to make it covariant in F but leave it invariant in T.
There was a problem hiding this comment.
What do you mean by "API [...] to clear a LazyCell similar to OnceCell"?
If you are talking about take, then it would not be a problem for these assumptions1. For LazyCell you would need something like fn take(&mut self, new_initializer: F) -> Result<T, F>. Notably since it would have to take &mut self, it does not interact with variance2.
Setting the value without callback would require LazyCell to be invariant in T. If we want to add such an API we should keep T invariant.
Footnotes
There was a problem hiding this comment.
Alright, maybe take() isn't a problem, but fn set(&self, value: T) -> Result<(), T> is.
This comment has been minimized.
This comment has been minimized.
01615ea to
4454dd6
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
r? libs |
|
I think this probably is better reviewed by types -- and in an ideal world, types(?) would provide us some way to assert the API shape guarantees the properties required, since I don't really trust us to get that right as the API evolves... I think we'll also need libs FCP on this (@rustbot label: +needs-fcp) because this change is insta-stable AFAICT. r? types |
|
Error: Parsing relabel command in comment failed: a label delta when parsing: +needs-fcp[!]) because Please file an issue on GitHub at triagebot if there's a problem with this bot, or reach out on #triagebot on Zulip. |
|
I don't think types necessarily has to review/sign off, but you're definitely right that it should have an FCP due to it being instantly stable. Speaking of which, does semver-checks catch that? @bors try |
This comment has been minimized.
This comment has been minimized.
Make `LazyCell<T, F>` and `LazyLock<T, F>` covariant in both `T` and `F`
|
@bors try jobs=test-x86_64-gnu-stdlib-semver-check |
This comment has been minimized.
This comment has been minimized.
Make `LazyCell<T, F>` and `LazyLock<T, F>` covariant in both `T` and `F` try-job: test-x86_64-gnu-stdlib-semver-check
I agree that types doesn’t necessarily have to sign off, but if types doesn’t then we need someone on libs comfortable reviewing this to do so (I am not that person). Types also seems best placed to answer how hard a lint / enforcement of the guarantees needed to make this guaranteed correct (rather than hoping humans catch bugs) in the compiler would be. |
|
FWIW I was mostly commenting since I figured this would need an FCP that probably it should just be libs, not types. In terms of review, I would not expect most people to be comfortable with reviewing variance changes since they're highly counterintuitive. |
|
I'm not sure if this is sound, let me take a look at this. |
|
I'm trying to build a counter-example, but I'm getting kinda stuck on rustc seemingly not considering |
|
This is very technically a breaking change. See #153607 |
|
Anyway.. I managed to simulate covariance with an unsafe wrapper to demonstrate potential soundness issues. struct CovariantLazyCell<T, F> {
data: *mut (),
marker: PhantomData<fn() -> (T, F)>,
}
impl<T, F> CovariantLazyCell<T, F> {
fn new(b: Box<LazyCell<T, F>>) -> Self {
Self {
data: Box::into_raw(b) as _,
marker: PhantomData,
}
}
fn as_ref(&self) -> &LazyCell<T, F> {
unsafe {
self.data
.cast::<LazyCell<T, F>>()
.cast_const()
.as_ref_unchecked()
}
}
}
impl<T, F> Drop for CovariantLazyCell<T, F> {
fn drop(&mut self) {
let inner: *mut LazyCell<T, F> = self.data.cast();
unsafe { drop(Box::from_raw(inner)) }
}
}In terms of soundness issue, here's what I could come up with: Once people can (eventually) manually implement #![feature(unboxed_closures)]
#![feature(fn_traits)]
use std::marker::PhantomData;
struct MyCustomFn<T>(PhantomData<T>);
type A = for<'a> fn(&'a str);
type B = fn(&'static str);
impl FnOnce<()> for MyCustomFn<A> {
type Output = A;
extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
println!("A");
|_| {}
}
}
static STRING: Mutex<&'static str> = Mutex::new("");
impl FnOnce<()> for MyCustomFn<B> {
type Output = B;
extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
println!("B");
|s| {
*STRING.lock().unwrap() = s;
}
}
}
use std::cell::LazyCell;
use std::sync::Mutex;
fn main() {
let r_a: &CovariantLazyCell<A, MyCustomFn<A>> =
&CovariantLazyCell::new(Box::new(LazyCell::new(MyCustomFn::<A>(PhantomData))));
let r_b: &CovariantLazyCell<B, MyCustomFn<B>> = r_a;
let f_b: &B = LazyCell::force(r_b.as_ref());
let f_a: &A = LazyCell::force(r_a.as_ref());
let string = String::from("Hello, World");
f_a(&string);
let s: &'static str = *STRING.lock().unwrap();
println!("{s}");
drop(string);
println!("{s}");
}The problem is that coercing Reading the result from the original On the other hand, at least: AFAICT, the argument for why making |
|
People have just found out that It's not covariant. |
|
Filed #163051 to make |
|
Here is the same soundness issue demonstration as above, but without the #![feature(unboxed_closures)]
#![feature(fn_traits)]
use std::marker::PhantomData;
struct MyCustomFn<T>(PhantomData<T>);
type A = for<'a> fn(&'a str);
type B = fn(&'static str);
impl FnOnce<()> for MyCustomFn<A> {
type Output = A;
extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
println!("A");
|_| {}
}
}
static STRING: Mutex<&'static str> = Mutex::new("");
impl FnOnce<()> for MyCustomFn<B> {
type Output = B;
extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
println!("B");
|s| {
*STRING.lock().unwrap() = s;
}
}
}
use std::cell::LazyCell;
use std::sync::Mutex;
fn main() {
let r_a: &LazyCell<A, MyCustomFn<A>> = &LazyCell::new(MyCustomFn::<A>(PhantomData));
let r_b: &LazyCell<B, MyCustomFn<B>> = r_a; // <- coercion using covariance happens here
let f_b: &B = LazyCell::force(r_b);
let f_a: &A = LazyCell::force(r_a);
let string = String::from("Hello, World");
f_a(&string);
let s: &'static str = *STRING.lock().unwrap();
println!("{s}");
drop(string);
println!("{s}");
} |
View all comments
See explanation for why it's valid that I wrote as a comment in
LazyCell:rust/library/core/src/cell/lazy.rs
Lines 55 to 91 in 5da2332
r? libs-api