Concrete v Bound Lifetime Parameter in fn Argument

Hi,

I'm porting Chapter 17 from Crafting Interpreters. The Pratt Parser uses a table of function pointers. So I have

type ParseFn = fn(&mut Compiler) -> ();

struct ParseRule {
    prefix: Option<ParseFn>,
    infix: Option<ParseFn>,
    precedence: Precedence,
}

pub struct Compiler<'a> {
    scanner: Scanner<'a>,
    ...,
    parse_rules: HashMap<TokenType, ParseRule>,
}

impl<'a> Compiler<'a> {
    pub fn new() -> Self {
        let parse_rules: HashMap<TokenType, ParseRule> = {
            let mut m: HashMap<TokenType, ParseRule> = HashMap::new();
            m.insert(
                TokenType::LeftParen,
                ParseRule::new(Some(Compiler::grouping), None, Precedence::None),
            );
            m
        };

        Compiler {
            ...,
            parse_rules: parse_rules,
        }
    }

    fn grouping(&mut self) { ... }
}

I plan to use it as follows

let rule = get_rule(toke_type);
rule.infix(&mut self);

However, the compiler complains with

error[E0308]: mismatched types
  --> bytecode/src/compiler.rs:94:37
   |
94 |                 ParseRule::new(Some(Compiler::grouping), None, Precedence::None),
   |                                     ^^^^^^^^^^^^^^^^^^ expected concrete lifetime, found bound lifetime parameter
   |
   = note: expected type `for<'r, 's> fn(&'r mut compiler::Compiler<'s>)`
              found type `for<'r> fn(&'r mut compiler::Compiler<'_>) {compiler::Compiler::<'_>::grouping}`

Here is my pull request.

What is the difference between a concrete and a bound lifetime? How can I define ParseFn so that it works?

Thanks for your help!

When you type Compiler::grouping, it first has to figure out which Compiler you want. There are many of them: One for each lifetime. Therefore it inserts a lifetime like this Compiler::<'b>::grouping with a lifetime I will call 'b, and then type checking will proceed to attempt to figure out what value 'b should have. However it will now notice that there is a type mismatch, because your ParseRule needs a function that works for any lifetime 'a and 'b, but yours only works for one specific 'b.

To fix this, you can try to use a closure, it might work, but otherwise try this:

// outside the impl block
fn grouping<'a, 'b>(compiler: &'a mut Compiler<'b>) {
    compiler.grouping();
}

Basically the issue is that impl<'a> { fn grouping() ... } will create a separate grouping function for every choice of generic parameter on the impl block.

Wow. Thanks @alice. I was always wondering why lifetime parameters where part of the generic parameters. I did not know that Rust would create a function for each lifetime parameter. I solved it this way now

type ParseFn<'r> = fn(&mut Compiler<'r>) -> ();

struct ParseRule<'r> {
    prefix: Option<ParseFn<'r>>,
    infix: Option<ParseFn<'r>>,
    precedence: Precedence,
}

pub struct Compiler<'a> {
    parser: Parser<'a>,
    ...
    parse_rules: HashMap<TokenType, ParseRule<'a>>,
}

So ParseFn should get the same lifetime.

They probably won't end up being different in the compiled binary, but they are different in the type system :slight_smile: .

Lifetimes never end up in codegen. Lifetimes are erased, not monomorphozed, so there will only be 1 instance for all possible choices of lifetimes. In most other ways, they behave just like generic types