-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Unsound projection when late-bound-region appears only in return type #32330
Comments
Just make these lifetimes early-bound. Why would you want a |
Right, I have that implemented now and am working out the kinks.
I don't want a binder over every type. I want fn lb<'a>(x: &'a u32) -> &'a u32 { x } then the type of |
What do you mean by that? Aren't signatures already contained in the fn def type? Types that are not determined by the function parameters need to be "early-bound" (because you need to be able to project out) - because |
Note the problem is not specific to bare functions, but can be reproduced with closures, which also currently permit late-bound regions in just the return type: #![feature(unboxed_closures)]
fn foo<'a>() -> &'a u32 { loop { } }
fn bar<T>(t: T, x: T::Output) -> T::Output
where T: FnOnce<()>
{
x
}
fn main() {
let x = &22;
let z: &'static u32 = bar(|| -> &u32 { loop { } }, x); // <-- Effectively casts stack pointer to static pointer
} |
So implementing this change by making the regions early-bound breaks a few bits of code in rustc. It also breaks parsell. The problem in parsell boils down to the fact that if you bind the type Another example from the compiler is that we have functions like this: fn expand<'cx>(&mut Foo) -> &'cx Result { ... } which are being used in a context that requires The galling thing about both of these examples is that the original code was correctly typed. I think that the latter case, at least, could probably be fixed by improving the code in I am not sure if this fix would scale up to parsell, because of the layers of traits involved, which require exact types. But it might. It's a bit hard to say without giving it a try. |
Yes. Very concretely, whereas today all function types have an implicit binder, I am talking about refactoring to pull the binder out. So that instead of having In any case, I'm more concerned about trying to ammeloriate the problems I mentioned in my previous comment, which I think are probably orthogonal to this question. |
So the problem is that |
Actually, this compiles: fn foo<'a, 'b>(_a: &'a str) -> &'b str where &'b (): Sized { "hello" }
fn main() {
let f: fn(&str) -> &str = foo::<'static>;
} because we special-case |
@arielb1 In my branch I reworked the interaction with The change I made is basically to be more sensitive to subtyping. Whereas today This has some interesting consequences. For example, |
I am worried about that consequence - it means that By the way, how is that implied? We have
Don't the edges here go in the wrong direction? I thought the change was about |
This is interesting. I certainly agree with making the implicit arguments and the universal quantifier (the binder) explicit; it is the approach I am taking in my formalization. Traits and thus associated types are not yet covered by my formalization, and won't be for the next few months, at least. My rough plan, should I ever get there, was to not leave anything implicit -- to make trait resolution explicit. So, when a function has a trait bound, that would actually translate to an additional argument passed to the function, which provides all the trait methods. (Think of this as the vtable passed next to the other arguments rather than in a fat pointer; monomorphization is then merely partial evaluation given that the vtable pointer argument is hard-coded at the call site.) I assume associated types correspond to type-level functions, which I guess means I have to switch the type system to System F_\omega. I will have to read up on how far my vtable approach scales here ;-) struct foo;
impl<'a> foo_FnOnce : FnOnce<()> for foo {
type Output = &'a u32;
...
} This reveals the problem at hand: From your discussion, I take it that Rust independently checks that |
|
That's because we already discussed the problem (with explicit impls) in rust-lang/rfcs#447.
The compiler assumes that
That would be non-trivial, as the following code is legal (and nothing forces pub struct Holder {
pub data: Option<<for<'a> Iterator<Item=&'a u32> as Iterator>::Item>
}
fn put<'a, 'b>(h: &'a mut Holder, d: Option<&'b u32>) {
h.data = d;
}
fn get<'a, 'b>(h: &'a Holder) -> Option<&'b u32> {
h.data
}
fn main() {
let mut h = Holder {
data: None
};
{
let x = 4;
put(&mut h, Some(&x));
}
println!("{:?}", get(&h));
} |
BTW Rust already uses "coercion" for a different thing. I would use |
Yes, that is a consequence.
Well, let's talk it out, make sure I got it right. First, to be clear, I think that in your example the alpha is a variable, whereas So the algorithm begins by replacing
where each edge represents the So this constraint graph kind of shows what I was getting at: you can consider Put another way, consider that each of the two functions can freely call each other: fn foo<'a>(x: &'a u32, y: &'a u32) {
bar(x, y) // OK: 'b and 'c can both be bound to `'a` (or, in fact, the call site)
}
fn bar<'b, 'c>(x: &'b u32, y: &'c u32) {
foo(x, y) // OK: 'a is inferred to the call site
} Note that this is not the case if one of the regions appears in the output type: fn foo<'a>(x: &'a u32, y: &'a u32) -> &'a u32 { bar(x, y) }
fn bar<'b, 'c>(x: &'b u32, y: &'c u32) -> &'b u32 { foo(x, y) } //~ ERROR E0495 |
Wait, I am confused here. I can certainly write down traits that, when used as a trait object, the compiler complains about for not being object safe. So, there are object-unsafe traits that even are useful. Are you referring to a different notion of object safety?
Okay. I will probably eventually dig into old discussions to figure out the reasons behind this design decision.
I do not understand the type |
You can have an object-unsafe trait (e.g.
Associated type projection is required to be a (partial) function. That's basically the point of associated types, not?
That's because it is an incoherent notion. The |
This is a step towards fixing rust-lang#32330. The full fix would be a breaking change, so we begin by issuing warnings for scenarios that will break.
I never saw associated types as a partial function; I saw them as additional, type-level data that is extracted from the inferred trait implementation; similar to how value-level data (like associated constants or method implementations) are extracted. If you see it as a function, then what is the domain of that function? As you said yourself, I guess I am rather puzzled that this ( |
It's time to fix issue rust-lang#32330. cc rust-lang#33685
@arielb1 still an issue on latest nightly. https://is.gd/I1QNWn (thanks to @niconii) https://is.gd/RpOnJm (shortened) |
Working on doing a complete fix for this now. |
@nikomatsakis will this also solve #26325 or #20304? |
This is the full and proper fix for rust-lang#32330. This also makes some effort to give a nice error message (as evidenced by the `ui` test), sending users over to the tracking issue for a full explanation.
make lifetimes that only appear in return type early-bound This is the full and proper fix for #32330. This also makes some effort to give a nice error message (as evidenced by the `ui` test), sending users over to the tracking issue for a fuller explanation and offering a `--explain` message in some cases. This needs a crater run before we land. r? @arielb1
So I believe this issue is now fixed thanks to #38897. @DemiMarie, in answer to your question (sorry, missed that before): My change does not affect #26325. It does help with #20304 -- but we were already doing some caching. I do have plans to improve the approach much further that I think are enabled by closing this bug. There may also be some amount of simplification I can do, I have to check. |
While working on a cache for normalization, I came across this problem in the way we currently handle fn items:
That program compiles, but clearly it should not. What's going on is kind of in the grotty details of how we handle a function like:
Currently, that is typed as if it was roughly equivalent to:
However, if you actually tried to write that impl above, you'd get an error:
And the reason is precisely because of this kind of unsoundness. Effectively, the way we handle the function
foo
above allowsOutput
to have different lifetimes each time it is projected.I believe this problem is specific to named lifetimes like
'a
that only appear in the return type and do not appear in any where clauses. This is because:FnOnce
trait's argument parameter. (Assuming it appears in a constrained position; that is, not as an input to an associated type projection.)struct foo<'a>
).The current treatment of early- vs late-bound is something that hasn't really scaled up well to the more complex language we have now. It is also central to #25860, for example. I think it is feasible to actually revamp how we handle things in the compiler in a largely backwards compatible way to close this hole -- and the refactoring would even take us closer to HKT. Basically we'd be pulling the
Binder
out ofFnSig
and object types and moving to be around types in general.However, in the short term, I think we could just reclassify named lifetimes that do not appear in the argument types as being early-bound. I've got an experimental branch doing that right now.
This problem is the root of what caused my projection cache not to work: I was assuming in that code that
T::Output
would always be the same ifT
is the same, and that is not currently the case!cc @rust-lang/lang
cc @arielb1
cc @RalfJung
The text was updated successfully, but these errors were encountered: