Manual lifetimes

I really think we need some kind of a macro or an operator for telling the compiler to lift lifetimes. For example:

'outer: {
    let x = {
        let y = "foo".to_owned();
        unsafe {
            set_lifetime!(y, 'outer);
        }
        &y
    };
    println!("{x}");
    // y is dropped here
};

I know many people here will dislike this for some reasons, but I think it can be very useful.

It can also be used to, for example, pass stuff like this

fn readln<'a>() -> std::io::Result<&'a str> {
    let mut i = String::new();
    std::io::stdin().read_line(&mut i)?;
    unsafe {
        set_lifetime!(i, 'a);
    }
    Ok(i.trim_end())
}

fn main() -> std::io::Result<()> {
    let input = readln()?;
    println!("{input}");
    Ok(()) // i is dropped here
}

This concept is very raw, so i want to discuss it on URLO firstly.

Lifetimes are just annotations, they do not change at all when a value is going to be dropped. Allowing them to be changed arbitrarily (as you propose) would defeat their whole purpose of preventing use-after-free and similar bugs.

The second example makes even less sense when you remember that the caller can fill in generic type arguments. So they can call readln::<'static>() and get a static reference to a string, out of thin air. This does not make sense and is completely incompatible with how Rust works. What would even be the benefit over just returning the String?

9 Likes

Your idea violates the fundamental rule/promise in Rust:

Didn't convince me. Elaborate on that plz. I don't see how useful it can be.
Instead, just return owned y and i in your case, then you won't bother with lifetime parameters :slight_smile:

1 Like

This can be done by moving variable declaration into another scope, no need to introduce new syntax to do so.

let y;
let x = {
    y = "foo".to_owned();
    &y
};
println!("{x}");
// y is dropped here

The second example is inherently impossible in Rust (unless you are willing to leak a String I guess), what you may actually want is to create a self-referential structure that would keep track of when to destroy an element, for instance using ouroboros crate (well, in this case it's probably more practical to simply return a String, but sometimes self-referential structures may be more useful):

use ouroboros::self_referencing;

#[self_referencing]
struct Line {
    full: String,
    #[borrows(full)]
    trimmed: &'this str,
}

fn readln() -> std::io::Result<Line> {
    let mut i = String::new();
    std::io::stdin().read_line(&mut i)?;
    Ok(LineBuilder {
        full: i,
        trimmed_builder: |full| full.trim_end(),
    }
    .build())
}

fn main() -> std::io::Result<()> {
    let input = readln()?;
    println!("{}", input.borrow_trimmed());
    Ok(())
}
3 Likes

Lifetimes are not the right tool for the job. Lifetimes in Rust just exist to check your code, not to influence its behavior. Lifetimes are erased fairly early in compilation and will always have zero effect on program behavior.

If you want to influence the place where your values are dropped, you need to make sure those values are owned by some variable that’s dropped at the appropriate time.

The first example can just use a local variable in the outer scope:

'outer: {
    let y;
    let x = {
        y = "foo".to_owned();
        &y
    };
    println!("{x}");
    // y is dropped here
};

The second example would need to pass in some kind of “slot” to populate; an easy option would be to use an (empty) Option<String> as in[1]

fn readln<'a>(slot: &'a mut Option<String>) -> std::io::Result<&'a str> {
    let mut i = slot.insert(String::new());
    std::io::stdin().read_line(&mut i)?;
    Ok(i.trim_end())
}

fn main() -> std::io::Result<()> {
    let mut i_slot = None;
    let input = readln(&mut i_slot)?;
    println!("{input}");
    Ok(()) // i is dropped here
}

I don’t see any need for new language features here, as IMO all this is fairly straightforward already ^^


  1. I’m going for minimal change in the code… technically you could skip passing &mut i to the read_linewhich will get dereferenced implicitly anyways, and do it with i directly instead, as clippy will, too, kindly point out :slight_smile: ↩︎

3 Likes

You all misunderstood me a little. I meant it should be just syntactic sugar for forget and drop_in_place.

I think your main misunderstanding is that lifetimes are actually the duration in which something is borrowed! It makes no sense to "set" the lifetime of y in your original snippet because it isn't borrowed, so it has no lifetime.

2 Likes

"Lifetime" can mean different things. Obviously OP is talking about the lifetime of a value stored in a variable, not about the lifetime annotation attached to a reference. Much of the criticism misses the point.

Nevertheless, the proposal isn't very practical because it would require a completely different memory allocation mechanism for those local variables. Local variable memory allocation is efficient because they are allocated in LIFO (last in, first out) manner, which allows using a stack. This would have to be very different with the proposal.

4 Likes

Practically speaking the only thing that may ever work with that proposal is full-blown GC (at least refcounting GC or maybe even full-blown tracing GC).

At that point all that dance with lifetimes stops being useful because they stop providing any guarantees: you never know if you are dealing with affine types or relaxed-GC-based types.

In a sense it's an attempt to return to the solution which Rust have already tried and, quite explicitly and consciously, rejected.

And for good reason, too: if you combine these two things the end result is combining not strengths of two approaches, but their weaknesses: now you can not make memory management S.E.P. because you have affine types, yet, at the same time, you can not rely on RAII for resource management because GC-based types make it unreliable.