Description
NLL is built on MIR-borrowck. MIR-borrowck operates on the control-flow graph encoded in the MIR.
We didn't (and still do not) want to let borrowck depend on details of how we choose to codegen the MIR from a given AST (in particular for match
expressions), and so we put some effort into trying to obscure those details from the view of MIR-borrowck.
But there was a case that we (potentially) overlooked: MIR encodes the order in which the variables a
and b
are dropped in let (a, b);
, and now NLL/MIR-borrowck takes advantage of that knowledge when checking code.
- I say "potentially" here because, as @nikomatsakis pointed out to me, the dynamic drop order was of course always observable (unlike say the particular details of the order in which we consider match arms when compiling
match
). So ths arguably does not fall into the same bucket as the previous motivation forFalseEdges
in the MIR.
In particular, in the following example (adapted from ui/dropck-eyepatch.rs):
#![feature(nll)]
// The types in this file follow a pattern, D{t,r}, where:
//
// - D means "I implement Drop"
//
// - t suffix is used when the first generic is a type
//
// - r suffix is used when the first generic is a lifetime.
use std::fmt;
struct Dt<A: fmt::Debug>(&'static str, A);
struct Dr<'a, B:'a+fmt::Debug>(&'static str, &'a B);
impl<A: fmt::Debug> Drop for Dt<A> {
fn drop(&mut self) { println!("drop {} {:?}", self.0, self.1); }
}
impl<'a, B: fmt::Debug> Drop for Dr<'a, B> {
fn drop(&mut self) { println!("drop {} {:?}", self.0, self.1); }
}
fn main() {
use std::cell::Cell;
let c_long;
let (c, mut dt, mut dr): (Cell<_>, Dt<_>, Dr<_>);
c_long = Cell::new(1);
c = Cell::new(1);
// No error: sufficiently long-lived state can be referenced in dtors
dt = Dt("dt", &c_long);
dr = Dr("dr", &c_long);
println!("{:?}", (dt.0, dr.0));
// Error: destructor order imprecisely modelled
dt = Dt("dt", &c);
//~^ ERROR `c` does not live long enough
dr = Dr("dr", &c);
//~^ ERROR `c` does not live long enough
println!("{:?}", (dt.0, dr.0));
}
Using AST-borrowck (by commenting out the #![feature(nll)]
at the top) emits the following errors:
error[E0597]: `c` does not live long enough
--> src/main.rs:36:20
|
36 | dt = Dt("dt", &c);
| ^ borrowed value does not live long enough
...
41 | }
| - `c` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
error[E0597]: `c` does not live long enough
--> src/main.rs:38:20
|
38 | dr = Dr("dr", &c);
| ^ borrowed value does not live long enough
...
41 | }
| - `c` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
Using NLL compiles successfully, because NLL/MIR-borrowck only sees a more precise model of the relative drop order of the variables declared via let (c, dt, dr);
, and takes advantage of it.
The question is: Are we okay with this side-effect of NLL?
I don't think it was explicitly documented as an intended effect.
However, I think the fact that we used to use an imprecise model for the drop-order in let (c, dt, dr);
previously was more due to the weaknesses of AST-borrowck. Under NLL, we can encode the more precise relationships needed to check the borrowing behavior in this program and validate that it is safe to let dt
and dr
hold references to c
here.