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

new scoping rules for safe dtors can yield spurious semi-colon or trailing unit expr #21114

Open
pnkfelix opened this Issue Jan 13, 2015 · 39 comments

Comments

Projects
None yet
@pnkfelix
Member

pnkfelix commented Jan 13, 2015

Spawned off of #21022, #21972

There are a number of situations where a trailing expression like a for-loop ends up being treated by the region lifetime inferencer as requiring a much longer lifetime assignment than what you would intuitively expect.

A simple example of this (from examples embedded in the docs for io::stdio):

    use std::io;

    let mut stdin = io::stdin();
    for line in stdin.lock().lines() {
        println!("{}", line.unwrap());
    };

Another example, this time from an example for alloc::rc (where this time I took the time to add a comment explaining what I encountered):

fn main() {
     let gadget_owner : Rc<Owner> = Rc::new(
            Owner {
                name: "Gadget Man".to_string(),
                gadgets: RefCell::new(Vec::new())
            }
    );

    let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});
    let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});

    gadget_owner.gadgets.borrow_mut().push(gadget1.clone().downgrade());
    gadget_owner.gadgets.borrow_mut().push(gadget2.clone().downgrade());

    for gadget_opt in gadget_owner.gadgets.borrow().iter() {
        let gadget = gadget_opt.upgrade().unwrap();
        println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
    }

    // This is an unfortunate wart that is a side-effect of the implmentation
    // of new destructor semantics: if the above for-loop is the final expression
    // in the function, the borrow-checker treats the gadget_owner as needing to
    // live past the destruction scope of the function (which of course it does not).
    // To work around this, for now I am inserting a dummy value just so the above
    // for-loop is no longer the final expression in the block.
    ()
}

Luckily for #21022, the coincidence of factors here is not very frequent, which is why I'm not planning on blocking #21022 on resolving this, but instead leaving it as something to address after issues for the alpha have been addressed.

(I'm filing this bug before landing the PR so that I can annotation each of the corresponding cases with a FIXME so that I can go back and address them after I get a chance to investigate this properly.)

@pnkfelix pnkfelix changed the title from new scoping rules for safe dtors often yield spurious semi-colon or trailing unit expr to new scoping rules for safe dtors can yield spurious semi-colon or trailing unit expr Jan 13, 2015

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Jan 23, 2015

Member

The solution here seems to be to adjust the infer lifetimes for temporaries generated from the <iter> in for <pat> in <iter> { ... }.

I have a patch to do this on my branch, and so far it seems to work, but @nikomatsakis has pointed out to me that I may be better off waiting until after #20790 lands.

Member

pnkfelix commented Jan 23, 2015

The solution here seems to be to adjust the infer lifetimes for temporaries generated from the <iter> in for <pat> in <iter> { ... }.

I have a patch to do this on my branch, and so far it seems to work, but @nikomatsakis has pointed out to me that I may be better off waiting until after #20790 lands.

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Jan 23, 2015

Member

(Nonetheless, there may be similar or related issues in other syntactic forms, such as if let <pat> = <expr> { ... }. At the very least, I will be able to use this bug as a reference point in FIXME notes.)

Member

pnkfelix commented Jan 23, 2015

(Nonetheless, there may be similar or related issues in other syntactic forms, such as if let <pat> = <expr> { ... }. At the very least, I will be able to use this bug as a reference point in FIXME notes.)

@nikomatsakis nikomatsakis referenced this issue Jan 26, 2015

Closed

New destructor semantics #8861

5 of 7 tasks complete

pnkfelix referenced this issue in pnkfelix/rust Feb 2, 2015

fixes for new rules for lifetimes and destructors.
note that some of these cases may actually be fixes to latent bugs, in
some sense (depending on what our guarantees are about e.g. what a
hashmap should be allowed to access in its own destructor).
@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Feb 11, 2015

Member

(This was largely addressed by #21984. I am not sure whether there is much more we can actually do here. You can search for FIXME's in this commit bdb9f3e for most of the fallout that was caused by this in the end (its mostly with if let, along with one semicolon I had to add to a test; it was not that bad.)

Member

pnkfelix commented Feb 11, 2015

(This was largely addressed by #21984. I am not sure whether there is much more we can actually do here. You can search for FIXME's in this commit bdb9f3e for most of the fallout that was caused by this in the end (its mostly with if let, along with one semicolon I had to add to a test; it was not that bad.)

@ebfull

This comment has been minimized.

Show comment
Hide comment
@ebfull

ebfull Feb 13, 2015

Contributor

I think I'm having a similar issue:

use std::collections::HashMap;
use std::sync::Mutex;

fn i_used_to_be_able_to(foo: &Mutex<HashMap<usize, usize>>) -> Vec<(usize, usize)> {
    let mut foo = foo.lock().unwrap();

    foo.drain().collect()
}

fn but_after_nightly_update_now_i_gotta(foo: &Mutex<HashMap<usize, usize>>) -> Vec<(usize, usize)> {
    let mut foo = foo.lock().unwrap();

    return foo.drain().collect();
}

fn main() {}
Contributor

ebfull commented Feb 13, 2015

I think I'm having a similar issue:

use std::collections::HashMap;
use std::sync::Mutex;

fn i_used_to_be_able_to(foo: &Mutex<HashMap<usize, usize>>) -> Vec<(usize, usize)> {
    let mut foo = foo.lock().unwrap();

    foo.drain().collect()
}

fn but_after_nightly_update_now_i_gotta(foo: &Mutex<HashMap<usize, usize>>) -> Vec<(usize, usize)> {
    let mut foo = foo.lock().unwrap();

    return foo.drain().collect();
}

fn main() {}
@felipesere

This comment has been minimized.

Show comment
Hide comment
@felipesere

felipesere Feb 13, 2015

I get the above mentioned error when running rustc rustc 1.0.0-nightly (3ef8ff1f8 2015-02-12 00:38:24 +0000)
With the following code:

    std::old_io::stdio::stdin().lock().lines().map( |line| {
        line.unwrap().trim().to_string()
    }).collect()

When using rustc 1.0.0-nightly (00df3251f 2015-02-08 23:24:33 +0000) I don't get the error.

felipesere commented Feb 13, 2015

I get the above mentioned error when running rustc rustc 1.0.0-nightly (3ef8ff1f8 2015-02-12 00:38:24 +0000)
With the following code:

    std::old_io::stdio::stdin().lock().lines().map( |line| {
        line.unwrap().trim().to_string()
    }).collect()

When using rustc 1.0.0-nightly (00df3251f 2015-02-08 23:24:33 +0000) I don't get the error.

felipesere added a commit to felipesere/icepick that referenced this issue Feb 13, 2015

Split usage of stdin() into multiple lets
Due to the following bug in rustc lifetime inferrer:
rust-lang/rust#21114
@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Feb 14, 2015

Member

@felipesere Your example, I think, is running afoul of the failure of the region system's model to precisely track the destruction order for r-value temporaries; see #22323.

After rewriting your example to accommodate that short-coming, we have this:

fn lines() -> Vec<String> {
    let mut stdin = std::old_io::stdio::stdin();
    let mut locked = stdin.lock();
    locked.lines().map( |line| {
        line.unwrap().trim().to_string()
    }).collect()
}

which compiles for me. So I do not think this is an instance of this ticket, but rather of #22323

Member

pnkfelix commented Feb 14, 2015

@felipesere Your example, I think, is running afoul of the failure of the region system's model to precisely track the destruction order for r-value temporaries; see #22323.

After rewriting your example to accommodate that short-coming, we have this:

fn lines() -> Vec<String> {
    let mut stdin = std::old_io::stdio::stdin();
    let mut locked = stdin.lock();
    locked.lines().map( |line| {
        line.unwrap().trim().to_string()
    }).collect()
}

which compiles for me. So I do not think this is an instance of this ticket, but rather of #22323

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Feb 14, 2015

Member

@ebfull your example is very interesting. (I am not yet sure whether the underlying cause is the same as the one that led to this ticket, but I definitely would like to fix it.)

cc #22321

Member

pnkfelix commented Feb 14, 2015

@ebfull your example is very interesting. (I am not yet sure whether the underlying cause is the same as the one that led to this ticket, but I definitely would like to fix it.)

cc #22321

@drbawb

This comment has been minimized.

Show comment
Hide comment
@drbawb

drbawb Feb 15, 2015

I think I'm bumping up against this, I have a function that acquires a RwLock in a trailing expression. Though it's a match expression, not a for loop.

The code looks similar to this:

type Context = Arc<RwLock<HashMap<Uuid, User>>>;

fn disconnect_user(ctx: Context, uid: Uuid) -> Result<String, String> {
    // ....
   match ctx.write().unwrap().remove(&uid) {
         Some(user) => { Ok(format!("user removed")) },
         None => { Err(format!("user not removed")) },
    }
}

Which yields this error on the latest nightly (b63cee4a1 2015-02-14):

src/supervisor/handlers/connections.rs:162:8: 162:11 error: `ctx` does not live long enough
src/supervisor/handlers/connections.rs:162      match ctx.users.write().ok().unwrap().remove(&uid) {
                                                      ^~~
src/supervisor/handlers/connections.rs:153:78: 186:2 note: reference must be valid for the destruction scope surrounding block at 153:77...
src/supervisor/handlers/connections.rs:153 pub fn disconnect_user(req: &Request, env: &mut Env) -> Result<String,String>{
src/supervisor/handlers/connections.rs:154      let ctx = env.get::<Arc<Context>>().unwrap().clone();
src/supervisor/handlers/connections.rs:155      let io  = env.get::<Sender<Envelope>>().unwrap().clone();
src/supervisor/handlers/connections.rs:156      let uid = req.uid.clone();
src/supervisor/handlers/connections.rs:157 
src/supervisor/handlers/connections.rs:158      let reason = req.payload.as_ref().and_then(|pl| {
                                           ...
src/supervisor/handlers/connections.rs:154:54: 186:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 154:53
src/supervisor/handlers/connections.rs:154      let ctx = env.get::<Arc<Context>>().unwrap().clone();
src/supervisor/handlers/connections.rs:155      let io  = env.get::<Sender<Envelope>>().unwrap().clone();
src/supervisor/handlers/connections.rs:156      let uid = req.uid.clone();
src/supervisor/handlers/connections.rs:157 
src/supervisor/handlers/connections.rs:158      let reason = req.payload.as_ref().and_then(|pl| {
src/supervisor/handlers/connections.rs:159              json::decode(&pl[]).ok()
                                           ...
error: aborting due to previous error

I seem to be able to work around it in two ways. I can either bind the result of the match exprsession to a variable and return that instead, or I can bind context's lock guard to a variable and perform the match on that.

drbawb commented Feb 15, 2015

I think I'm bumping up against this, I have a function that acquires a RwLock in a trailing expression. Though it's a match expression, not a for loop.

The code looks similar to this:

type Context = Arc<RwLock<HashMap<Uuid, User>>>;

fn disconnect_user(ctx: Context, uid: Uuid) -> Result<String, String> {
    // ....
   match ctx.write().unwrap().remove(&uid) {
         Some(user) => { Ok(format!("user removed")) },
         None => { Err(format!("user not removed")) },
    }
}

Which yields this error on the latest nightly (b63cee4a1 2015-02-14):

src/supervisor/handlers/connections.rs:162:8: 162:11 error: `ctx` does not live long enough
src/supervisor/handlers/connections.rs:162      match ctx.users.write().ok().unwrap().remove(&uid) {
                                                      ^~~
src/supervisor/handlers/connections.rs:153:78: 186:2 note: reference must be valid for the destruction scope surrounding block at 153:77...
src/supervisor/handlers/connections.rs:153 pub fn disconnect_user(req: &Request, env: &mut Env) -> Result<String,String>{
src/supervisor/handlers/connections.rs:154      let ctx = env.get::<Arc<Context>>().unwrap().clone();
src/supervisor/handlers/connections.rs:155      let io  = env.get::<Sender<Envelope>>().unwrap().clone();
src/supervisor/handlers/connections.rs:156      let uid = req.uid.clone();
src/supervisor/handlers/connections.rs:157 
src/supervisor/handlers/connections.rs:158      let reason = req.payload.as_ref().and_then(|pl| {
                                           ...
src/supervisor/handlers/connections.rs:154:54: 186:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 154:53
src/supervisor/handlers/connections.rs:154      let ctx = env.get::<Arc<Context>>().unwrap().clone();
src/supervisor/handlers/connections.rs:155      let io  = env.get::<Sender<Envelope>>().unwrap().clone();
src/supervisor/handlers/connections.rs:156      let uid = req.uid.clone();
src/supervisor/handlers/connections.rs:157 
src/supervisor/handlers/connections.rs:158      let reason = req.payload.as_ref().and_then(|pl| {
src/supervisor/handlers/connections.rs:159              json::decode(&pl[]).ok()
                                           ...
error: aborting due to previous error

I seem to be able to work around it in two ways. I can either bind the result of the match exprsession to a variable and return that instead, or I can bind context's lock guard to a variable and perform the match on that.

@pnkfelix pnkfelix self-assigned this Feb 16, 2015

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Feb 16, 2015

Member

@drbawb yes, I suspect this is the same issue. I doubt I will be fixing this any time soon (i.e., I do not think it is a 1.0 blocker issue), so while I do hope to fix it eventually, I think you will need to live with the workarounds for now.

Member

pnkfelix commented Feb 16, 2015

@drbawb yes, I suspect this is the same issue. I doubt I will be fixing this any time soon (i.e., I do not think it is a 1.0 blocker issue), so while I do hope to fix it eventually, I think you will need to live with the workarounds for now.

@steveklabnik

This comment has been minimized.

Show comment
Hide comment
@steveklabnik

steveklabnik May 24, 2016

Member

A full reproduction of @pnkfelix 's early example compiles today without the ():

use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;

struct Owner {
    name: String,
    gadgets: RefCell<Vec<Weak<Gadget>>>,
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
}

fn main() {
    let gadget_owner : Rc<Owner> = Rc::new(
        Owner { 
            name: "Gadget Man".to_string(),
            gadgets: RefCell::new(Vec::new())
        }
        );

    let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});
    let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});

    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget1.clone()));
    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget2.clone()));

    for gadget_opt in gadget_owner.gadgets.borrow().iter() {
        let gadget = gadget_opt.upgrade().unwrap();
        println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
    }
}

That said, obviously this issue is about a bit more than that, and as he said:

(I'm filing this bug before landing the PR so that I can annotation each of the corresponding cases with a FIXME so that I can go back and address them after I get a chance to investigate this properly.)

@pnkfelix where does this stand today?

Member

steveklabnik commented May 24, 2016

A full reproduction of @pnkfelix 's early example compiles today without the ():

use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;

struct Owner {
    name: String,
    gadgets: RefCell<Vec<Weak<Gadget>>>,
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
}

fn main() {
    let gadget_owner : Rc<Owner> = Rc::new(
        Owner { 
            name: "Gadget Man".to_string(),
            gadgets: RefCell::new(Vec::new())
        }
        );

    let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});
    let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});

    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget1.clone()));
    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget2.clone()));

    for gadget_opt in gadget_owner.gadgets.borrow().iter() {
        let gadget = gadget_opt.upgrade().unwrap();
        println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
    }
}

That said, obviously this issue is about a bit more than that, and as he said:

(I'm filing this bug before landing the PR so that I can annotation each of the corresponding cases with a FIXME so that I can go back and address them after I get a chance to investigate this properly.)

@pnkfelix where does this stand today?

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix May 25, 2016

Member

@steveklabnik it seems like the cases of interest here got fixed via independent changes elsewhere in rustc

It might be interesting to track down which PR did this, but given that we have made a number of refinements to the region system since this was filed, I'm not astounded that it was fixed .... merely mildly surprised

Member

pnkfelix commented May 25, 2016

@steveklabnik it seems like the cases of interest here got fixed via independent changes elsewhere in rustc

It might be interesting to track down which PR did this, but given that we have made a number of refinements to the region system since this was filed, I'm not astounded that it was fixed .... merely mildly surprised

@pnkfelix pnkfelix closed this May 25, 2016

@niconii

This comment has been minimized.

Show comment
Hide comment
@niconii

niconii Jun 24, 2016

Member

This problem seems to still crop up with code like this, which doesn't compile unless ; or an expression is added after the if-let:

use std::sync::Mutex;

fn main() {
    let counter = Mutex::new(0);

    if let Ok(_) = counter.lock() { }
}
Member

niconii commented Jun 24, 2016

This problem seems to still crop up with code like this, which doesn't compile unless ; or an expression is added after the if-let:

use std::sync::Mutex;

fn main() {
    let counter = Mutex::new(0);

    if let Ok(_) = counter.lock() { }
}

@pnkfelix pnkfelix reopened this Sep 20, 2016

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Sep 20, 2016

Member

reopening based on discussion at #22252

Member

pnkfelix commented Sep 20, 2016

reopening based on discussion at #22252

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Apr 25, 2017

Member

(I will try to accumulate a representative set of currently failing examples in the description for ease of reference)

Member

pnkfelix commented Apr 25, 2017

(I will try to accumulate a representative set of currently failing examples in the description for ease of reference)

@Thiez

This comment has been minimized.

Show comment
Hide comment
@Thiez

Thiez May 21, 2018

Contributor

I'd like to amend my struct-only testcases with the following case:

struct S{ n: u8 }
struct T<'a>{ s: &'a S }
impl<'a> Drop for T<'a> { fn drop(&mut self){} }

// The `if-else` makes it work...
fn test_if_else() -> u8 {
    let s = S{ n: 5 };
    if true {
        T{ s: &s }.s.n
    } else {
        T{ s: &s }.s.n
    }
}

This compiles, but replacing the entire if-else with T{ s: &s }.s.n will give an error, also on nightly with NLL enabled. Error without NLL:

error[E0597]: `s` does not live long enough
  --> src/main.rs:17:16
   |
17 |         T{ s: &s }.s.n
   |                ^ borrowed value does not live long enough
18 |     
19 | }
   | - `s` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

With NLL:

error[E0597]: `s` does not live long enough
  --> src/main.rs:17:15
   |
17 |         T{ s: &s }.s.n
   |         ------^^--
   |         |     |
   |         |     borrowed value does not live long enough
   |         borrow may end up in a temporary, created here
18 |     
19 | }
   | -
   | |
   | borrowed value only lives until here
   | temporary later dropped here, potentially using the reference
Contributor

Thiez commented May 21, 2018

I'd like to amend my struct-only testcases with the following case:

struct S{ n: u8 }
struct T<'a>{ s: &'a S }
impl<'a> Drop for T<'a> { fn drop(&mut self){} }

// The `if-else` makes it work...
fn test_if_else() -> u8 {
    let s = S{ n: 5 };
    if true {
        T{ s: &s }.s.n
    } else {
        T{ s: &s }.s.n
    }
}

This compiles, but replacing the entire if-else with T{ s: &s }.s.n will give an error, also on nightly with NLL enabled. Error without NLL:

error[E0597]: `s` does not live long enough
  --> src/main.rs:17:16
   |
17 |         T{ s: &s }.s.n
   |                ^ borrowed value does not live long enough
18 |     
19 | }
   | - `s` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

With NLL:

error[E0597]: `s` does not live long enough
  --> src/main.rs:17:15
   |
17 |         T{ s: &s }.s.n
   |         ------^^--
   |         |     |
   |         |     borrowed value does not live long enough
   |         borrow may end up in a temporary, created here
18 |     
19 | }
   | -
   | |
   | borrowed value only lives until here
   | temporary later dropped here, potentially using the reference
@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Sep 13, 2018

Member

This issue was discussed (or at least alluded to) in today's compiler team meeting. I'm going to treat addressing it as part of overall potential NLL work, so I'm 1. tagging with A-NLL, and 2. putting on the Release milestone.

Member

pnkfelix commented Sep 13, 2018

This issue was discussed (or at least alluded to) in today's compiler team meeting. I'm going to treat addressing it as part of overall potential NLL work, so I'm 1. tagging with A-NLL, and 2. putting on the Release milestone.

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Sep 25, 2018

Member

(Shifting effort into making a diagnostic to address this pain point; see #54556.)

WIth that shift, this bug is dedicated to the hypothetical attempt to "fix" this problem in some way that isn't just changing diagnostics.

But also with that shift, this bug is no longer part of the NLL work, nor on the Rust 2018 release milestone.

Member

pnkfelix commented Sep 25, 2018

(Shifting effort into making a diagnostic to address this pain point; see #54556.)

WIth that shift, this bug is dedicated to the hypothetical attempt to "fix" this problem in some way that isn't just changing diagnostics.

But also with that shift, this bug is no longer part of the NLL work, nor on the Rust 2018 release milestone.

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Oct 4, 2018

Member

Some months ago @Thiez wrote an interesting note that pointed out an oddity where a tail expression with if _ { EXPR } else { EXPR } in the block tail position was compiling, while just EXPR in the tail expression (which you would think has the same dynamic semantics) was not compiling.

I investigated, and discovered something interesting: Apparently the two forms do not have the same dynamic semantics, because the temporaries for the EXPR in if _ { EXPR } else { EXPR } are dropped sooner than for EXPR alone.

Here is a concrete demonstration illustrating this (play:

#![feature(nll)]

struct T(&'static str, u8);
impl Drop for T {
    fn drop(&mut self){ println!("Dropping T {:?}", self.0); }
}

fn ends_with_if_else() -> u8 {
    println!("entered `ends_with_if_else`");
    let _tmp = T("temp", 0);
    if true { T("expr", 1).1 } else { T("expr", 2).1 }
}

fn ends_with_tail() -> u8 {
    println!("entered `ends_with_tail`");
    let _tmp = T("temp", 3);
    T("expr", 4).1
}

fn main() {
    {
        let _outer1 = T("outer1", 5);
        println!("invoke method `ends_with_if_else`");
        ends_with_if_else();
        println!("returned from `ends_with_if_else`");
    }

    println!("");

    {
        let _outer2 = T("outer2", 6);
        println!("invoke method `ends_with_tail`");
        ends_with_tail();
        println!("returned from `ends_with_tail`");
    }
}

This prints:

invoke method `ends_with_if_else`
entered `ends_with_if_else`
Dropping T "expr"
Dropping T "temp"
returned from `ends_with_if_else`
Dropping T "outer1"

invoke method `ends_with_tail`
entered `ends_with_tail`
Dropping T "temp"
Dropping T "expr"
returned from `ends_with_tail`
Dropping T "outer2"

The implication is that our current temporary r-value rules do not let temporaries from the tails of if/else expressions bubble out to the if/else itself.

This discrepancy is perhaps unfortunate, but I don't know if it would make sense to try to change it. If we were to try to change it, we should probably attach such a change to a particular Rust edition shift (and we're too close to the release of the 2018 edition to attempt it there.)

Member

pnkfelix commented Oct 4, 2018

Some months ago @Thiez wrote an interesting note that pointed out an oddity where a tail expression with if _ { EXPR } else { EXPR } in the block tail position was compiling, while just EXPR in the tail expression (which you would think has the same dynamic semantics) was not compiling.

I investigated, and discovered something interesting: Apparently the two forms do not have the same dynamic semantics, because the temporaries for the EXPR in if _ { EXPR } else { EXPR } are dropped sooner than for EXPR alone.

Here is a concrete demonstration illustrating this (play:

#![feature(nll)]

struct T(&'static str, u8);
impl Drop for T {
    fn drop(&mut self){ println!("Dropping T {:?}", self.0); }
}

fn ends_with_if_else() -> u8 {
    println!("entered `ends_with_if_else`");
    let _tmp = T("temp", 0);
    if true { T("expr", 1).1 } else { T("expr", 2).1 }
}

fn ends_with_tail() -> u8 {
    println!("entered `ends_with_tail`");
    let _tmp = T("temp", 3);
    T("expr", 4).1
}

fn main() {
    {
        let _outer1 = T("outer1", 5);
        println!("invoke method `ends_with_if_else`");
        ends_with_if_else();
        println!("returned from `ends_with_if_else`");
    }

    println!("");

    {
        let _outer2 = T("outer2", 6);
        println!("invoke method `ends_with_tail`");
        ends_with_tail();
        println!("returned from `ends_with_tail`");
    }
}

This prints:

invoke method `ends_with_if_else`
entered `ends_with_if_else`
Dropping T "expr"
Dropping T "temp"
returned from `ends_with_if_else`
Dropping T "outer1"

invoke method `ends_with_tail`
entered `ends_with_tail`
Dropping T "temp"
Dropping T "expr"
returned from `ends_with_tail`
Dropping T "outer2"

The implication is that our current temporary r-value rules do not let temporaries from the tails of if/else expressions bubble out to the if/else itself.

This discrepancy is perhaps unfortunate, but I don't know if it would make sense to try to change it. If we were to try to change it, we should probably attach such a change to a particular Rust edition shift (and we're too close to the release of the 2018 edition to attempt it there.)

@kwanyyoss

This comment has been minimized.

Show comment
Hide comment
@kwanyyoss

kwanyyoss Oct 7, 2018

The implication is that our current temporary r-value rules do not let temporaries from the tails of if/else expressions bubble out to the if/else itself.

I am confused. I thought the behavior of "ends_with_if_else" is the good one.

This discrepancy is perhaps unfortunate, but I don't know if it would make sense to try to change it. If we were to try to change it, we should probably attach such a change to a particular Rust edition shift (and we're too close to the release of the 2018 edition to attempt it there.)

I added "let ret = ..." to the last statements and ended with a naked "ret" as returned value from both "ends_with_tail" and "ends_with_if_else". The behavior of the new forms looks completely correct to me: Playground

invoke method `ends_with_if_else`
entered `ends_with_if_else`
Dropping T "expr"
result obtained
Dropping T "temp"
returned from `ends_with_if_else`
Dropping T "outer1"

invoke method `ends_with_tail`
entered `ends_with_tail`
Dropping T "expr"
result obtained
Dropping T "temp"
returned from `ends_with_tail`
Dropping T "outer2"

"expr" was dropped even before "result obtained" as it should be. I suppose this was also what @Thiez wanted.

Why is let ret = T("expr", 4).1; ret not the same as T("expr", 4).1? I even tried let ret = &T("expr", 4).1; *ret and let ret = &T("expr", 4); ret.1. "expr" was dropped before "temp" in all 3 cases with "let ret =".

kwanyyoss commented Oct 7, 2018

The implication is that our current temporary r-value rules do not let temporaries from the tails of if/else expressions bubble out to the if/else itself.

I am confused. I thought the behavior of "ends_with_if_else" is the good one.

This discrepancy is perhaps unfortunate, but I don't know if it would make sense to try to change it. If we were to try to change it, we should probably attach such a change to a particular Rust edition shift (and we're too close to the release of the 2018 edition to attempt it there.)

I added "let ret = ..." to the last statements and ended with a naked "ret" as returned value from both "ends_with_tail" and "ends_with_if_else". The behavior of the new forms looks completely correct to me: Playground

invoke method `ends_with_if_else`
entered `ends_with_if_else`
Dropping T "expr"
result obtained
Dropping T "temp"
returned from `ends_with_if_else`
Dropping T "outer1"

invoke method `ends_with_tail`
entered `ends_with_tail`
Dropping T "expr"
result obtained
Dropping T "temp"
returned from `ends_with_tail`
Dropping T "outer2"

"expr" was dropped even before "result obtained" as it should be. I suppose this was also what @Thiez wanted.

Why is let ret = T("expr", 4).1; ret not the same as T("expr", 4).1? I even tried let ret = &T("expr", 4).1; *ret and let ret = &T("expr", 4); ret.1. "expr" was dropped before "temp" in all 3 cases with "let ret =".

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Oct 9, 2018

Member

@kwanyyoss wrote:

Why is let ret = T("expr", 4).1; ret not the same as T("expr", 4).1?

Because the former is assigning its result into a let-bound variable of the block, while the latter is passing its result back outside of the block directly, without going through a let-statement.

For better or for worse, the temporary r-value lifetime rules we have chosen dictate that the temporaries are dropped at different times for these two scenarios.

There is much background discussion of this, as well as proposals for revising our semantics here. See e.g.

Member

pnkfelix commented Oct 9, 2018

@kwanyyoss wrote:

Why is let ret = T("expr", 4).1; ret not the same as T("expr", 4).1?

Because the former is assigning its result into a let-bound variable of the block, while the latter is passing its result back outside of the block directly, without going through a let-statement.

For better or for worse, the temporary r-value lifetime rules we have chosen dictate that the temporaries are dropped at different times for these two scenarios.

There is much background discussion of this, as well as proposals for revising our semantics here. See e.g.

@pnkfelix pnkfelix closed this Oct 9, 2018

@pnkfelix pnkfelix reopened this Oct 9, 2018

@kwanyyoss

This comment has been minimized.

Show comment
Hide comment
@kwanyyoss

kwanyyoss Oct 16, 2018

Thanks @pnkfelix for the reply and references!

I think the problem with this case is that the return value is just a "u8". As long as the type is "Copy", it is hard to see why the lifetime of any temporary where the final value came from needs to be extended. Along this line, I wonder if extending the lifetime of the entire tail expression may be too broad. Does it make sense to extended (the lifetime of) only the last object created by the tail expression rather than the whole expression?

I can find descriptions of how C++ deals with return objects and temporaries using copy constructors. The way I understand it is that it is basically doing a "placement new" into an ancestor stack frame/scope. Is there a similar treatise of how Rust returns objects or bubbles up the value of a {} block? I doubt whether "Clone" is involved.

I think there is still a common belief that return <expr>; at the end of a function is the same as <expr>. I hope this issue will bring forth a section in Rust documentation on why they are not the same.

kwanyyoss commented Oct 16, 2018

Thanks @pnkfelix for the reply and references!

I think the problem with this case is that the return value is just a "u8". As long as the type is "Copy", it is hard to see why the lifetime of any temporary where the final value came from needs to be extended. Along this line, I wonder if extending the lifetime of the entire tail expression may be too broad. Does it make sense to extended (the lifetime of) only the last object created by the tail expression rather than the whole expression?

I can find descriptions of how C++ deals with return objects and temporaries using copy constructors. The way I understand it is that it is basically doing a "placement new" into an ancestor stack frame/scope. Is there a similar treatise of how Rust returns objects or bubbles up the value of a {} block? I doubt whether "Clone" is involved.

I think there is still a common belief that return <expr>; at the end of a function is the same as <expr>. I hope this issue will bring forth a section in Rust documentation on why they are not the same.

@Havvy

This comment has been minimized.

Show comment
Hide comment
@Havvy

Havvy Oct 16, 2018

Contributor

There's discussion about temporary lifetimes in the Reference but having an explicit section on this would make sense.

Edit: Filed rust-lang-nursery/reference#452

Contributor

Havvy commented Oct 16, 2018

There's discussion about temporary lifetimes in the Reference but having an explicit section on this would make sense.

Edit: Filed rust-lang-nursery/reference#452

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Oct 16, 2018

Member

@kwanyyoss wrote:

I think the problem with this case is that the return value is just a "u8". As long as the type is "Copy", it is hard to see why the lifetime of any temporary where the final value came from needs to be extended.

Part of the goal here is to have temporary r-value rules that are uniform, in the sense that they come directly from the syntactic structure from the program, and not from e.g. the types that have been inferred nor the results of the borrow-check analysis.

Here's further elaboration of this point:

Does it make sense to extended (the lifetime of) only the last object created by the tail expression rather than the whole expression?

I don't know. My most immediate reaction to that is that it would really complicate encoding the cases where you need those intermediate values to survive long enough.

(And of course, the interpretation of your question depends very much on how one interprets the phrase "the last object created" ...)

Member

pnkfelix commented Oct 16, 2018

@kwanyyoss wrote:

I think the problem with this case is that the return value is just a "u8". As long as the type is "Copy", it is hard to see why the lifetime of any temporary where the final value came from needs to be extended.

Part of the goal here is to have temporary r-value rules that are uniform, in the sense that they come directly from the syntactic structure from the program, and not from e.g. the types that have been inferred nor the results of the borrow-check analysis.

Here's further elaboration of this point:

Does it make sense to extended (the lifetime of) only the last object created by the tail expression rather than the whole expression?

I don't know. My most immediate reaction to that is that it would really complicate encoding the cases where you need those intermediate values to survive long enough.

(And of course, the interpretation of your question depends very much on how one interprets the phrase "the last object created" ...)

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Oct 16, 2018

Member

I wrote up above:

My most immediate reaction to that is that it would really complicate encoding the cases where you need those intermediate values to survive long enough.

As an attempt to illustrate this: people already complain about the fact that they are forced to add extra let-bindings of intermediate results in cases like this:

fn does_not_work() {
    let x = &std::env::args().collect::<Vec<_>>().as_slice()[1..];
    println!("{:?}", x);
}

fn works() {
    println!("{:?}", &std::env::args().collect::<Vec<_>>().as_slice()[1..]);
}

(See #15023 )

I suspect your proposal would further exacerbate this already present problem. But I have not done any experiment to validate that suspicion.

Member

pnkfelix commented Oct 16, 2018

I wrote up above:

My most immediate reaction to that is that it would really complicate encoding the cases where you need those intermediate values to survive long enough.

As an attempt to illustrate this: people already complain about the fact that they are forced to add extra let-bindings of intermediate results in cases like this:

fn does_not_work() {
    let x = &std::env::args().collect::<Vec<_>>().as_slice()[1..];
    println!("{:?}", x);
}

fn works() {
    println!("{:?}", &std::env::args().collect::<Vec<_>>().as_slice()[1..]);
}

(See #15023 )

I suspect your proposal would further exacerbate this already present problem. But I have not done any experiment to validate that suspicion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment