Lifetime of arena in long-running web server

55 Views Asked by At

I'm working on a compiler pipeline and am not sure how to properly model ownership of ASTs with arenas.

I have types inside the AST represented as references into arenas (quite like rustc's own TyCtxt), using typed_arena. As a dummy example (but happy to provide more details):

use typed_arena::Arena;

enum Ty<'ty> {
  Int,
  List(&'ty Ty<'ty>),
}

enum Expr<'ty> {
  Literal(isize),
  List(Vec<Expr<'ty>>),
  TypeAnnotated(Expr<'ty>, &'ty Ty<'ty>)
}

fn parse_and_type_check<'ty>(input: String, ty_arena: &'ty Arena<Ty<'ty>>) -> Expr<'ty> {
  todo!()
}

This works fine for running the compile pipeline by itself, but now I'd like to use this in a language server, where I want Expr<'ty> to live for as long as needed:

struct LanguageServerState<'ty> {
  latest_program: Option<(Arena<'ty>, Expr<'ty>)>,
}

fn main() {


  let mut state = LanguageServerState { latest_program: None };

  // in reality, this is accepting connections and selecting receiving messages from an LSP client
  loop {
     // ...on a new program
     let new_program = "[1, [2, 3]]".to_owned();
     let fresh_arena = Arena::new();
     let new_program = parse_and_type_check(new_program, &fresh_arena);
     state = LanguageServerState { latest_program: Some((fresh_arena, new_program)) };

     // ...on some LSP request I may need to do something using `latest_program`

  }
}

It makes sense that this won't pass the borrow checker - as written the code would allow me to drop the Arena in LanguageServerState without dropping Expr<'ty>, which would leave dangling pointers.

I'm just not sure: what is the right way to model the LanguageServerState in a situation like this? I really need to be able to reclaim memory from the arena when the program itself is dropped, but I need the whole program + arena to live until the net version of the program has been parsed and type checked.

1

There are 1 best solutions below

0
Chayim Friedman On

You'll have hard times. You're trying to create a self-referential struct.

The easiest way will probably be to use one of the self-referential crates readily available.

For example, with yoke (note I switched the typed arena to an untyped arena, since yoke has problems with the lifetime in the arena. This can lead to problems if there you allocate types that need to be dropped. The advantage is you can also allocate Exprs there, though):

use bumpalo::Bump;

#[derive(yoke::Yokeable)]
enum Ty<'ty> {
    Int,
    List(&'ty [Ty<'ty>]),
}

#[derive(yoke::Yokeable)]
enum Expr<'ty> {
    Literal(isize),
    List(Vec<Expr<'ty>>),
    TypeAnnotated(Box<Expr<'ty>>, &'ty Ty<'ty>),
}

fn parse_and_type_check<'ty>(input: String, ty_arena: &'ty Bump) -> Expr<'ty> {
    todo!()
}

struct LanguageServerState {
    latest_program: Option<yoke::Yoke<Expr<'static>, Box<Bump>>>,
}

fn main() {
    let mut state = LanguageServerState {
        latest_program: None,
    };

    // in reality, this is accepting connections and selecting receiving messages from an LSP client
    loop {
        // ...on a new program
        let new_program = "[1, [2, 3]]".to_owned();
        let fresh_arena = Bump::new();
        state = LanguageServerState {
            latest_program: Some(yoke::Yoke::attach_to_cart(
                Box::new(fresh_arena),
                |fresh_arena| parse_and_type_check(new_program, fresh_arena),
            )),
        };

        // ...on some LSP request I may need to do something using `latest_program`
    }
}