-
-
Notifications
You must be signed in to change notification settings - Fork 202
Fix #486: Add another recursive parser #894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This commit (hopefully) renders `define` and `declare` for users of the library obselete, by adding a parser which wraps the declaration and definition similar to `recursive`. This `recursive_n` allows for the definition of mutually recursive parsers, resulting in a single parser. This solves the memory leak in a fashion similar to zesterer#664 by keeping all strong references within `RecursiveN` and dropping these all whenever the parser returned by `recursive_n` is dropped. Additionally, `Recursive::declare` and `Recursive::define` are removed from the API.
|
Thanks! A few thoughts:
|
My only concern with that is that the
No, honestly I didn't try. It took me 3 minutes to write the other definitions. If going above 8 though, definitely worth converting it to a macro. |
Oh, I've just realised that I entirely misunderstood the point of this combinator. My assumption is that it did I'm less sure how I feel about a combinator that only gives you back one, although I realise now that it's necessary for there to be a single root of the
Fair enough. I may attempt this at some point, if only to make future maintenance easier. |
Yeah, the drawback of the solution is that it does require a little bit of duplication when you are constructing the final parsers. It happens to work fine for the problem that I have, because each node implements has it's own // with no dedup
fn expr_parser<'a>() -> Parser<'a, Expression> {
recursive_n(|expr, stmt, another, parser| {
let stmt = Statement::parser(/* recursion */);
let expr = Expr::parser(/* recursion */);
let another = Another::parser(/* recursion */);
let parser = Parser::parser(/* recursion */);
(expr, stmt, another, parser)
})
}
fn module_parser<'a>() -> Parser<'a, Module> {
// Notice the recursive `_module` is unused
recursive_n(|_module, stmt, expr, another, parser| {
let stmt = Statement::parser(/* recursion */);
let expr = Expr::parser(/* recursion */);
let another = Another::parser(/* recursion */);
let parser = Parser::parser(/* recursion */);
let module = Module::parser(/* no recursion here */)
(module, stmt, expr, another, parser)
})
}And here would be it again, but with some... slight dedup. fn define_shared_recursion(/* long param list */) -> (/* long tuple */) {
let stmt = Statement::parser(/* recursion */ stmt, expr);
let expr = Expr::parser(/* recursion */ expr, another);
let another = Another::parser(/* recursion */ another, expr, parser);
let parser = Parser::parser(/* recursion */ expr, parser);
(stmt, expr, another, parser)
}
fn expr_parser<'a>() -> Parser<'a, Expression> {
recursive_n(|expr, stmt, another, parser| {
let (stmt, expr, another, parser) = define_shared_recursion(stmt, expr, another, parser);
(expr, stmt, another, parser)
})
}
fn module_parser<'a>() -> Parser<'a, Module> {
// Notice the recursive `_module` is unused
recursive_n(|_module, stmt, expr, another, parser| {
let (stmt, expr, another, parser) = define_shared_recursion(stmt, expr, another, parser);
let module = Module::parser(/* no recursion here */, stmt, expr, another, parser)
(module, stmt, expr, another, parser)
})
}Sorry for the more pseudo-ish Rust. I hope this helps to illustrate that you can still do the design. It's just a matter of shifting where you deduplicate (and the use of a non-recursive |
This might not be that bad. Using the language of #664, with mutually recursive module and expression parsers, the returned parser would be To obtain a signature like |
|
So. As far as I and Miri can tell, this doesn't leak (tested using the sail crates mentioned earlier). Now we just provide the user access to all parsers defined, but stuck behind the This I believe matches your original expectations @zesterer (:
|
turns out I do have the skills, but I just have to sit a wait a little longer
|
Hmm. I do think this is a neat solution for the specific, limited-scope problem the original issue - but I can't help but feel like it doesn't go far enough to be flexible for most use-cases. I think I am going to have a go at prototyping something before merging this: not because I don't think is worth merging, but because my spidey sense is telling me that there's a better solution very close by in the design space. |
|
Sounds like a plan! I totally get wanting to explore the space and seeing if you can find a solution that has a bit more flexibility to it. My solutions are admittedly biased towards my use-case :) I'm intrigued to see what you end up finding. If this does end up being the solution that we want to go with, it may be worth trying to modify the macro to prevent the extra clone of each of the parsers (though it is just an |
|
I've implemented a version of my idea in #902, along with an example/test. It's extremely rough and not at all ready for merge, but hopefully there's enough meat on the bones to demonstrate the idea. It has a few key advantages:
I think it should do this while guaranteeing no leaks either, since the final parsers produced at the end are the only roots of the tree. I'm interested to know what you think: whether you can foresee limitations or annoyances, etc. Usually I am very anti-macro, but in this case (as with |
This PR contains the suggested solution described in #494 for the problem described in #486, based on the solution provided by #664.
Breaking Changes:
Recursive::defineandRecursive::declareremoved from the public APIRecursiveNandrecursive_nadded to the preludeDescription
Introduced to the public API through this PR are the following items:
recursive_n- a function which outputs aRecursiveNparserRecursiveN- a parser which contains a recursive parser, as well as all recursive dependencies as strong referencesRecursiveArgs- a trait which all arguments passed intorecursive_nmust fulfill. It is implemented up to a tuple of 8 parsers, as I hope for everyone that they never have to deal with 8 mutually recursive parsers.These solve the memory leak by only allowing the user to make weak clones of the recursive parsers. When the parsers returned by
recursive_nare dropped, all strong references are dropped. The user has no access to any of the strong references and therefor can't accidentally shoot themselves in the foot.Considerations
recursivebe replaced withrecursive_n? In some cases it would unfortunately require the user to add a: Recursive<_>to their currently existing parsers. I understand why Rust can't determine if the argument is a tuple given a name, or not, but it is surely unfortunate.(Whoops. Looks like a got the wrong number for the commit message. dang it)