> Can you do a lot better? I don't think so and it wouldn't help that much.
I think Rust could do a lot better inferring lifetimes if the compiler would be allowed to peek into called function instead of stopping at the function signature - e.g. if it had a complete picture of the control flow of the entire code base (maybe be up to a point that manual lifetime annotations could be completely eliminated?).
IMHO it's not unrealistic to treat the entire codebase as a single compilation unit, Zig does this for instance - it just doesn't do much so far with the additional information that could be gained.
It's a dangerous option: Rust already has long compile times, expanding the space it has to analyze would only increase that. Not to mention you'd be much more dependent on the implementation details of a given function, and it'd become very messy. The fact that lifetimes have a specifiable interface is probably one of the main things that makes Rust's approach work at all.
Rust has similar rules about type inference (of which lifetimes are a subset) at the function level as well. I think this was a lesson learned the hard way by Haskell, which does allow whole-program type inference, and how programmers working in it quickly learned you really want to specify the types at the function level anyway
> Not to mention you'd be much more dependent on the implementation details of a given function
Hmm, but wouldn't that already be the case since the manual lifetime annotation must match what the function actually does? E.g. I would expect compiler errors if the 'internal' lifetime details of a function no longer match its manual lifetime annotations (is it actually possible to create incorrect lifetime annoatations in Rust without the compiler noticing?)
Higher compile times would be bad of course, but I wonder how much it would add in practice. It's a similar problem as LTO, just earlier in the compile process. E.g. maybe some time consuming tasks can be moved around instead of added on top.
> is it actually possible to create incorrect lifetime annoatations in Rust without the compiler noticing?
In safe rust, no.
Full inference is one of those things that seems like a no brainer, but there are a number of other more subtle tradeoffs that make it a not great idea. Speed was already mentioned, but it’s really downstream from tractibility, IMHO. That is, lifetime checking is effectively instantaneous today, and that’s because you only need to confirm that the body matches the signature, which is a very small and local problem. Once you allow inference, you end up needing to check not just the body, but also the bodies of every function called in your body, recursively, since you no longer know their signatures up front. We tend to think of compiler passes as “speed” in the sense of it’s nice to have fast compile times, but it also matters in the sense of what can practically be checked in a reasonable time. The cheaper a check, the more checks we can do. Furthermore, remember that Rust supports separate compilation, which is a major hindrance to full program analysis, which is what you need to truly infer lifetimes.
Beyond complexity arguments, there’s also more practical ones: error messages would get way worse. More valid programs would be rejected if the inference can’t figure out an answer. Semver is harder to maintain, because a change in the body now changes the signature, and you may break your callers in ways you don’t realize at first.
I would kill for Rust to spend some time figuring out what the ownership rule should be when I get the ownership wrong - compile cycles are cheap and inexpensive compared to me sitting & trying different approaches or running an LLM to try to help me figure it out (hint: they largely fail miserably and cause me to waste more time). I was fighting one function in my codebase and couldn’t figure out how to get the compiler to be happy despite me seemingly having a correct definition, so I just broke down and won the impasse by using unsafe which isn’t what I wanted to do. I know it sometimes recommends, but not in all cases and not in this particular case.
Another thing I’ll point out is that TypeScript does full program inference and while type checking performance is a huge problem, it does a pretty good job. That obviously doesn’t necessarily map to Rust and the problem domain it’s solving (& maybe TS codebases naturally are smaller than Rust) but just putting that out there. Rust has made certain opinionated choices but that doesn’t mean that other choices weren’t equally valid and available. SemVer is easily solvable - don’t allow inference for pub APIs exported from the crate which also neatly largely solves the locality issue.
Did you check your unsafe with Miri? It's possible you were trying to do something that isn't actually possible, locally speaking.
> I’ll point out is that TypeScript does full program inference
Do you have a citation for this? I don't believe this is the case, though I could be wrong. I actually spent some time trying to find a definitive answer here and couldn't. That said,
> Rust has made certain opinionated choices but that doesn’t mean that other choices weren’t equally valid and available.
This is true for sure; for example, TypeScript is deliberately unsound, and that's a great choice for it, but does not make sense for Rust.
> SemVer is easily solvable - don’t allow inference for pub APIs exported from the crate which also neatly largely solves the locality issue.
It helps with locality but doesn't solve it, as it's still a non-local analysis. The same problems fundamentally remain, even if the scope is a bit reduced.
Have you ever had success with Miri on non-trivial programs? Here's a reduced test case which does show it's safe under Miri but for the life of me I can't figure out how to get rid of the unsafe: https://play.rust-lang.org/?version=stable&mode=debug&editio...
> Do you have a citation for this? I don't believe this is the case, though I could be wrong. I actually spent some time trying to find a definitive answer here and couldn't. That said,
No and thinking about it more I'm not sure about the specific requirements that constitutes full program inference so it's possible it's not. However, I do know that it infers the return type signatures of functions from the bodies.
> This is true for sure; for example, TypeScript is deliberately unsound, and that's a great choice for it, but does not make sense for Rust
Sure but I think we can agree that the deliberately unsound is for ergonomic and pragmatic compatibility with JS, not because of the choice of inference.
I'm not arguing Rust should change it's inference strategy. Of all the things, I'd rate this quite low on my wishlist of "what would I change about how Rust works if I could wave a magic wand".
Note that by creating the reference to a local and passing it up through the callback, you are using a fresh region that can’t possibly outlive any of one of the ones you are generic over. Fundamentally, that callback could stash the reference you pass it into state somewhere and now the pointer has escaped, invalidated as soon as that iteration of the loops ends.
See that the definition of Group is tying those together. Instead, you can split them apart and maybe use HRTB to ensure the closure _must_ be able to treat the lifetime as fresh? But then you’ll probably have other issues…
… which can largely be circumvented simply by pinning, in your reduced example, which probably doesn’t retain enough detail.
But why does pinning solve the issue? Fundamentally the lifetime of the future is unchanged as far as the compiler is concerned so in theory the callback should be capable of doing the same stashing, no?
The lifetime _is_ changed; this lets you use the lifetime from the HRTB instead of the function generics. It’s not so much the pinning itself that does it, for the type system, but using the trait object enables referring to that HRTB to require true lifetime generic (and then pinning comes along for the ride).
> Have you ever had success with Miri on non-trivial programs?
The key is to isolate the unsafe code and test it directly, so you're not really doing it with whole programs. At least that's what I try to do. Anyway, was just curious!
(I don't have anything to say about the specific code here that cmr didn't already say)
> Sure but I think we can agree that the deliberately unsound is for ergonomic and pragmatic compatibility with JS,
Oh absolutely, all I meant was that because they're starting from different goals, they can make different choices.
I think Rust could do a lot better inferring lifetimes if the compiler would be allowed to peek into called function instead of stopping at the function signature - e.g. if it had a complete picture of the control flow of the entire code base (maybe be up to a point that manual lifetime annotations could be completely eliminated?).
IMHO it's not unrealistic to treat the entire codebase as a single compilation unit, Zig does this for instance - it just doesn't do much so far with the additional information that could be gained.