maplit icon indicating copy to clipboard operation
maplit copied to clipboard

Extend lifetime of temporaries

Open dtolnay opened this issue 6 years ago • 5 comments

The behavior of maplit's macros when temporaries are involved is inconsistent with vec! and inconsistent with how temporary lifetimes usually work. Temporaries are supposed to outlive the current statement i.e. live "until the semicolon".

use maplit::hashset;
use std::collections::HashSet;
use std::path::Path;

fn v(_: Vec<&str>) {}
fn s(_: HashSet<&str>) {}

fn main() {
    let path = Path::new("/");

    // fine
    v(vec![path.to_string_lossy().as_ref()]);

    // does not compile
    s(hashset![path.to_string_lossy().as_ref()]);
}
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:15:16
   |
15 |     s(hashset![path.to_string_lossy().as_ref()]);
   |       ---------^^^^^^^^^^^^^^^^^^^^^^----------
   |       |        |
   |       |        creates a temporary which is freed while still in use
   |       temporary value is freed at the end of this statement
   |       borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

As a proof of concept, an expansion like this would behave correctly with lifetimes of temporaries:

s({
    match (path.to_string_lossy().as_ref(), /* ... */) {
        (x0, /* ... */) => {
            let mut set = HashSet::new();
            set.insert(x0);
            /* ... */
            set
        }
    }
});

dtolnay avatar Sep 30 '19 21:09 dtolnay

Interesting. Do you rank this as a curiosity, feature request, or bug?

That expansion is interesting but I don't see how we achieve it (need identifiers for the patterns - is there a recursive macro rule-trick for that, that will work here)?

bluss avatar Oct 01 '19 21:10 bluss

75% feature request + 25% bug. I think this is worth fixing if someone (or I) were to send a PR that addresses this without making the implementation or the generated code inscrutable.

dtolnay avatar Oct 01 '19 22:10 dtolnay

Sounds good. We will also check that compile time for large maps remains in the same ballpark.

bluss avatar Oct 06 '19 19:10 bluss

What comes to mind is to make it a "builder expression", chained methods, that way the macro can result in mostly a single expression.

The half baked poc is like this

trait SetInsert<K> {
    fn maplit_insert(self, k: K) -> Self;
}

impl<K> SetInsert<K> for HashSet<K> where K: Hash + Eq {
    fn maplit_insert(mut self, k: K) -> Self { self.insert(k); self }
}

macro_rules! hashset {
    (@single $($x:tt)*) => (());
    (@count $($rest:expr),*) => (<[()]>::len(&[$(hashset!(@single $rest)),*]));

    ($($key:expr,)+) => { hashset!($($key),+) };
    ($($key:expr),*) => {
        {
            let _cap = hashset!(@count $($key),*);
            ::std::collections::HashSet::with_capacity(_cap)
            $(
                .maplit_insert($key)
            )*
        }
    };
}

bluss avatar Oct 06 '19 19:10 bluss

Would something like

macro_rules! hashset {
    (@single $($x:tt)*) => (());
    (@count $($rest:expr),*) => (<[()]>::len(&[$(hashset!(@single $rest)),*]));

    (@r [$($ks:expr,)*] $k:expr, $($xs:tt)*) => {
        {
            match $k {
                k => hashset!(@r [$($ks,)* k,] $($xs)*)
            }
        }
    };
    (@r [$($key:expr,)*]) => {
        {
            let _cap = hashset!(@count $($key),*);
            let mut _set = ::std::collections::HashSet::with_capacity(_cap);
            $(
                let _ = _set.insert($key);
            )*
            _set
        }
    };

    ($($key:expr,)+) => { hashset!($($key),+) };
    ($($key:expr),*) => { hashset!(@r [] $($key,)*) };
}

https://github.com/SeeSpring/maplit/commit/d842582f46bd286871a88526a790910fe95754f4 work? I'm not sure how much this would affect compile times though.

SeeSpring avatar Jun 11 '20 02:06 SeeSpring