rust

How to annotate lifetimes for multiple arguments in a bumpalo arena?


In my action_recursive function, I am creating a vec which will act like a stack. I will insert all the commands I have encountered and then do some stuff. All are allocated in a bumpalo arena.

But I get the following error on line 27:

use bumpalo::{collections::Vec, Bump};

enum Command<'a> {
    Add(Vec<'a, Command<'a>>),
    Sub(Vec<'a, Command<'a>>),
    Break,
}

fn main() {
    let arena = Bump::new();
    let mut cmds = Vec::new_in(&arena);
    action(&arena, &mut cmds);
}

fn action<'a>(arena: &'a Bump, cmds: &mut Vec<'a, Command<'a>>) {
    let mut cmd_stack = Vec::new_in(arena);
    action_recursive(arena, cmds, &mut cmd_stack);
}

fn action_recursive<'a>(
    arena: &'a Bump,
    cmds: &mut Vec<'a, Command<'a>>,
    cmd_stack: &mut Vec<'a, Vec<'a, &Command<'a>>>,
) {
    let mut breaks = Vec::new_in(arena);

    cmds.iter().for_each(|cmd| {
        if matches!(cmd, Command::Break) {
            breaks.push(cmd)
        }
    });

    cmd_stack.push(breaks)
}
add explicit lifetime `'a` to the type of `cmds`: `&'a mut bumpalo::collections::Vec<'a, Command<'a>>``

If I do what it is saying me to do, I get more errors in the main function.

`cmds` does not live long enough borrowed value does not live long enough`

I have this in a sample repo since I couldn't create a playground link due to missing crates.


Solution

  • You've attempted to scatter-shot the lifetimes at action_recursive to satisfy the compiler. Which to be honest often works, but you have a net of lifetimes that are interconnected and just using 'a for all of them unfortunately ties them in knots. The big foul is that 'a on the right-hand side of a &mut makes it invariant which means the compiler can't manipulate it as well as other contexts.

    Often it is better to leave everything as inferred and fill-in the blanks when the compiler get fussy. But the error suggestions here are a bit ambitious so we'll have to use our brains.

    Our first link needs to tie the arena to the breaks that we are putting in cmd_stack:

    fn action_recursive<'a>(
        arena: &'a Bump,
             // \------------------------------\
        cmds: &'_ mut Vec<'_, Command<'_>>, // |
                                 // v----------/
        cmd_stack: &mut Vec<'_, Vec<'a, &'_ Command<'_>>>,
    )
    

    Our next link is to bind the lifetime from cmds to where it is stored in cmd_stack:

    fn action_recursive<'a, 'b>(
        arena: &'a Bump,
        cmds: &'b mut Vec<'_, Command<'_>>,
            // \-------------------------v
        cmd_stack: &mut Vec<'_, Vec<'a, &'b Command<'_>>>,
    )
    

    And out last link is to simply communicate that the lifetime within the Commands are the same:

    fn action_recursive<'a, 'b, 'c>(
        arena: &'a Bump,
        cmds: &'b mut Vec<'_, Command<'c>>,
                                   // \-------------v
        cmd_stack: &mut Vec<'_, Vec<'a, &'b Command<'c>>>,
    )
    

    And you'll find that it compiles.

    The key disconnect here is that we don't want the top level Vec<'_, ...> lifetimes to be bound to arena. We don't actually care about how long they are using the arena. But the reason it was a problem is that Bumpalo's Vec requires that the elements outlive the arena:

    pub struct Vec<'bump, T: 'bump>
    

    This can still allow the arena to store shorter-lived values, but only if we allow the compiler to shorten the reference to the arena (immutable references are naturally covariant). As mentioned above, if we left those all connected then the arena would be forced to have its original lifetime and thus not allow shorter-lived references within itself.

    Note that other arenas may not have this limitation: your code nearly-verbatim mapped converted to the bump-scope crate will compile since there is no link between elements and the arena lifetime there.