fn foo(dst: &mut i32, a: i32) {
*dst = a;
}
fn bar() {
let f = foo as fn(_, _);
loop {
let mut dst = 0;
f(&mut dst, 1);
}
}
rustc fails with the following error message
error[E0597]: `dst` does not live long enough
--> <source>:9:11
|
8 | let mut dst = 0;
| ------- binding `dst` declared here
9 | f(&mut dst, 1);
| - ^^^^^^^^ borrowed value does not live long enough
| |
| borrow later used here
10 | }
| - `dst` dropped here while still borrowed
But I can't make any sense of the message -- the dst parameter of f should be instantiated with a smaller lifetime than the lifetime of the dst variable. It says dst is dropped while still borrowed, but I can't figure out where the borrow is. Afaiu f cannot keep the borrow because it is instantiated with a smaller lifetime.
Also, this only occurs with the fn cast and some sort of loop (loop, for, while all work)
yes, the function needs to have a parameter type of higher ranked lifetime, but each placeholder _ can only be inferred as a single type. and because the type is declared in the outer scope, a reference to a variable in the inner scope cannot satisfy the constraint.
it's enough to tell the compiler the first parameter type is in the shape of a reference:
so, iiuc, casting foo to fn(_,_) automatically lifetime-monomorphizes it (for the lack of a better word), so it cannot satisfy all the different lifetimes at once. That's weird, I thought it would instantiate it to something like foo(&'a mut i32, i32) for <'a>
You're not alone, it's quite a common frustration/friction point when dealing with function/closure type inference, at this particular point in time. Still more intuitive than the whole state of async, imo.
Default closure inference does have similar limitations.
// Will have similar problems as `fn(_, _)`.
let f = |_, _| {}
// A similar fix.
let f = |_: &mut _, _| {};
Though it's arguably worse with closures as they don't follow the same elision-like rules when it comes to the return type.
fn quz(dst: &mut i32, a: i32) -> &i32 { .. }
// Works as it infers `for<'a> fn(&'a mut _, _) -> &'a mut`.
let f = quz as fn(&mut _, _) -> &mut _;
// Closure itself does not compile as it infers a concrete lifetimes for
// the return type only, in contrast with the example above.
let f = |dst: &mut _, _| -> &mut _ { dst };
// Similar inference failure; does not compile due to "non-primitive cast".
let f = quz as fn(&mut _, _) -> _;
A workaround for closures is to funnel them through an identify function with the desired signature as a Fn* bound, because those bounds override the default closure inference. (You have to define the closure in the call to the function.)
// Identity function. The bound follows the higher-ranked
// elision rules we want.
fn funnel<F: Fn(&mut i32, i32) -> &i32>(f: F) -> F { f }
// Works.
let f = funnel(|dst, _| dst);
But this workaround falls apart in the face of unnameable types, like compiler generated futures or other opaque types.
I feel there's room for improvement, but it's hard to change the default inference without causing lots of breakage. Perhaps acceptable over an edition if there's an easy path to get the old behavior. There will probably always be some rough edges; as I understand it, higher-ranked inference is NP completeundecidable.
Are you sure existing one is not? In my experience it takes surprisingly little to turn anything into NP-complete task in unrestricted case. But usually there are something acceptable if not perfect with adequate complexity nearby.
You mean, am I sure the current type inference has polynomial complexity? No, I'm not. I do know that parts of the compiler such as the trait solver (which can impact inference) have non-polynomial complexity (mitigated somewhat by recursion limits). Oh, and exhaustiveness checking is NP complete, and the compiler does that for matches.
Anyway, turns out I mispoke. I was thinking of some specific citation in an issue I had seen. I found the comment I was thinking of, and the actual citation is that higher-ranked type inference is undecidable (vs. NP-complete).