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

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.