I wonder how clear error messages for this kind of liveness checking will be. Will they be able to point to variable usage that causes a variable to be locked?
The Rust team is committed to having high-quality error messages in rustc, and its lifetime error messages are singularly superb, making a complex concept understandable and explaining in very thorough detail when necessary what is causing a problem: “this expected this because of this but found this which would mean that instead” with detailed diagnostics of the points in question, and such things.
This tradition will not be forsaken with non-lexical lifetimes.
I don't think anyone cares more about good error messages for this class of errors than Niko. The error messages are already pretty good today, pointing to precisely where a location was borrowed, and I'm sure it won't be allowed to regress, only improve.
Just yesterday an overhaul of the style of the error messages landed (should be in the next nightly). It doesn't change the actual messages, but should make thinks like scopes clearer. You can find a few screenshots in the PR: https://github.com/rust-lang/rust/pull/32756#issuecomment-20...
If I understand the article, what's already the case is that any reuse of "a" while "b" exists is illegal. Which is simple and uncomplicated and you can work around it like this
let a = 1
begin block scope {
let b = pointer to a
do_stuff(b)
} // end block scope and b is dead
do_other_stuff (a)
What the change would do is make that block scope exist, but it exists implicitly and invisibly. And it invisibly stretches over the call to do_other_stuff(a) by the insertion of the do_more_stuff(b) below it. And suddenly it breaks.
Contrast:
let a = 1
begin block scope {
let b = pointer to a
do_stuff(b)
} // end block scope and b is dead
do_other_stuff (a)
// ... etc etc
do_more_stuff(b) // is obviously broken, b is not in scope
or
let a = 1
begin block scope {
let b = pointer to a
do_stuff(b)
do_other_stuff (a) // is obviously broken, b is not dead
// ... etc etc
do_more_stuff(b)
} // end block scope way down here
Given that the Rust team is committed to introducing non-lexical lifetimes backwards-compatibly, by definition that means that no code that compiles today will be invalidated by the feature's addition. Which means that the only instances of concern being raised here are programs which are already invalid in today's Rust, which is to say that any workarounds for these theoretical problems are already mandatory. There's nothing "breaking" here that isn't already broken. It's a strict reduction in the number of programs that will be rejected by the borrow checker.
I don't think non-lexical lifetimes will make code harder to reason about (beyond the fact that it means there are more valid Rust programs, of course), because it doesn't change behaviour: a program that currently compiles won't have different runtime results with non-lexical lifetimes.
I suppose it will mean a programmer might have to think a little harder if they want to manually preempt compilation errors, but fortunately the compiler is there to catch them if they don't get it right. The example the OP points out is a situation that will likely play out like the following in most circumstances:
- programmer adds new use of variable b.
- compiler points out an existing use of some other value a that could invalidate b, and, importantly, highlights all the moving parts (conflicting use of a, use of b that prolongs its lifetime, etc.) in the error message.
You do this exact thing all the time in non-Rust languages. Rust notices, and then tells you about it. I don't think that makes code harder to reason about... I think that leans more in the "easier" direction.
I don't think you should dock Rust for pointing out bad practices that other languages simply don't notice. I think you can dock it for complaining about code that's really OK, which just happens to be exactly what they're trying to fix here.
So, I don't endorse this reasoning, but I can see it. With Rust you have to think about the possible failure cases of your code up-front and account for them. In other languages, you don't have to.
For instance, I'd bet that most C or C++ apps in production that use concurrency have some concurrency bug somewhere, that's so rare that nobody has hit it yet (or the effect of the bug is minor). Those apps are running in production and serving a valuable purpose. Rust would force you to solve the edge case before deploying the app in production.
Occasionally, it's the right set of engineering tradeoffs to deploy something that produces useful results 99.9% of the time and crashes .1% of the time. (But, I guess you can say, you can always use Rust's 'unsafe' keyword to acknowledge that something isn't quite right but you're deploying it anyway.)
A sort of related issue: while Rust may make you solve it up front, it also helps you _a lot_ with finding the issue in the first place and pointing out what's wrong. Concurrency bugs in other languages often remain because it's hard to actually identify the problem in the first place, and so it's not worth the time to go through that massive effort for a bug that happens rarely.
> Occasionally, it's the right set of engineering tradeoffs to deploy something that produces useful results 99.9% of the time and crashes .1% of the time.
Yes, but unfortunately that doesn't describe the situation we are usually presented with. The developers most likely have not calculated that it runs fine 99.9% of the time and has specific failure cases that account for 0.1% of the time, they just don't know about the error. That is vastly different than making an engineering tradeoff, since the people engineering it don't even know there's a tradeoff being made, and thus can't make an informed decision about it.
In some sense, rust actually enables the situation you are presenting, in that it requires more explicit reasoning about behavior, and if you want to wrap a chunk in unsafe after calculating that it should only negatively affect the program operation 0.1% of the time, well then you actually are making a tradeoff.
This is exactly one example why non-lexical lifetimes aren't a uniform positive: they do make the compiler more accepting of code that people think should "obviously" compile, but comes at the cost of weird situations where making a (somewhat) distant change results in errors.
Fortunately, these sort of errors should be all function-local and are of course all flagged at compile time. Assuming Niko's usual focus on high-quality error messages continues, I would expect this sort of situation to be fairly clearly explained by the compiler.
I don't know, borrow checker error messages are already pretty good at pointing out issues of this sort already.
With a minor addition I think it would remain very comprehensible: "you borrowed a here ^^^ ... gave a away here ^^^^, then used it's borrow again here ^^^^ after having given it away."
I'd submit that stretching "action at a distance" to include "function local" is really taking the idea too far. When we decry "action at a distance" I don't think we generally mean "seventeen lines previous in the same function". I think we also generally mean "action at a distance that produces hard-to-debug runtime effects", not "compiler errors that point out exactly what is wrong on what line" (as I assume will be the case).
Personal opinion, but I would rather be given a way to explicitly define(or in that case "undefined") the lifetime, rather than having the compiler trying to be smart. I.e
let slice = ...
capitalize(slice);
"unlet" slice // <- explicitly make it go out of scope
25 comments
[ 3605 ms ] story [ 2161 ms ] thread[1]: http://smallcultfollowing.com/babysteps/blog/2016/04/27/non-... [2]: https://news.ycombinator.com/item?id=11611436
This tradition will not be forsaken with non-lexical lifetimes.
Contrast:
orI suppose it will mean a programmer might have to think a little harder if they want to manually preempt compilation errors, but fortunately the compiler is there to catch them if they don't get it right. The example the OP points out is a situation that will likely play out like the following in most circumstances:
- programmer adds new use of variable b.
- compiler points out an existing use of some other value a that could invalidate b, and, importantly, highlights all the moving parts (conflicting use of a, use of b that prolongs its lifetime, etc.) in the error message.
- programmer edits code to avoid the problem.
I don't think you should dock Rust for pointing out bad practices that other languages simply don't notice. I think you can dock it for complaining about code that's really OK, which just happens to be exactly what they're trying to fix here.
For instance, I'd bet that most C or C++ apps in production that use concurrency have some concurrency bug somewhere, that's so rare that nobody has hit it yet (or the effect of the bug is minor). Those apps are running in production and serving a valuable purpose. Rust would force you to solve the edge case before deploying the app in production.
Occasionally, it's the right set of engineering tradeoffs to deploy something that produces useful results 99.9% of the time and crashes .1% of the time. (But, I guess you can say, you can always use Rust's 'unsafe' keyword to acknowledge that something isn't quite right but you're deploying it anyway.)
Yes, but unfortunately that doesn't describe the situation we are usually presented with. The developers most likely have not calculated that it runs fine 99.9% of the time and has specific failure cases that account for 0.1% of the time, they just don't know about the error. That is vastly different than making an engineering tradeoff, since the people engineering it don't even know there's a tradeoff being made, and thus can't make an informed decision about it.
In some sense, rust actually enables the situation you are presenting, in that it requires more explicit reasoning about behavior, and if you want to wrap a chunk in unsafe after calculating that it should only negatively affect the program operation 0.1% of the time, well then you actually are making a tradeoff.
Fortunately, these sort of errors should be all function-local and are of course all flagged at compile time. Assuming Niko's usual focus on high-quality error messages continues, I would expect this sort of situation to be fairly clearly explained by the compiler.
With a minor addition I think it would remain very comprehensible: "you borrowed a here ^^^ ... gave a away here ^^^^, then used it's borrow again here ^^^^ after having given it away."
let slice = ...
capitalize(slice);
"unlet" slice // <- explicitly make it go out of scope