I'm following along with a book which uses Pratt Parsing when writing a parser in C and have run into exactly the problem you'd expect when writing similar code in Rust. There are of course plenty of other ways to set up a Pratt Parser in Rust, it just feels like it almost works which makes me worry I'm missing a relatively straightforward fix here
The problem: since the Parser (Actor
in the example code) type the method is defined on has a lifetime, there's not a straightforward way to define the static table of fn pointers that is populated with Actor
's methods.
Playground with the full example code
The "obvious" code just ends up making the fn pointer use an Actor<'static>
argument which certainly can't be called with a non-'static
lifetime.
struct Actor<'a> {
context: &'a str,
}
impl<'a> Actor<'a> {
fn act(&mut self) {
println!("{}", self.context);
}
fn lookup_action(&mut self) {
ACTIONS[0](self) // Fails: `ACTIONS` contains a 'static lifetime which is hidden
}
}
type Action<'a> = fn(&mut Actor<'a>);
static ACTIONS: &[Action] = &[Actor::act];
You can use a transmute
but that sort of defeats the point of using the lifetimes in the first place, even if it's not super likely to be a problem in practice. (The building of the table could probably be made safer too, for example with a const fn populating the table from inside the Actor
impl)
pub struct Actor<'a> {
pub context: &'a str,
}
impl<'a> Actor<'a> {
fn act(&mut self) {
println!("{}", self.context);
}
pub fn lookup_action(&mut self) {
ACTIONS[0].act(self)
}
}
struct Lookup(Action<'static>);
impl Lookup {
/// Tie the lifetime of the action to the actor as a safeguard since we're transmuting.
fn act<'a>(&self, c: &mut Actor<'a>) {
let f: Action<'a> = unsafe { std::mem::transmute(self.0) };
f(c)
}
}
type Action<'a> = fn(&mut Actor<'a>);
static ACTIONS: &[Lookup] = &[Lookup(Actor::act)];
You can build the table inside the Actor
impl which dodges the problem by allowing you to actually refer to the proper lifetime. But now the table is no longer static, and you have to hope the compiler gets really clever about optimizations (admittedly this example is the silliest way to do this)
pub struct Actor<'a> {
pub context: &'a str,
}
impl<'a> Actor<'a> {
fn act(&mut self) {
println!("{}", self.context);
}
pub fn lookup_action(&mut self) {
Self::rules()[0](self)
}
fn rules() -> [Action<'a>; 1] {
[Actor::act]
}
}
type Action<'a> = fn(&mut Actor<'a>);
Is there a better option to solve this kind of problem? (besides "stop trying to do that" which I will freely admit is probably the right answer to this problem in most cases)