Skip to content

Lrpar codegen - #657

Open
ratmice wants to merge 14 commits into
softdevteam:masterfrom
ratmice:lrpar_codegen4
Open

Lrpar codegen#657
ratmice wants to merge 14 commits into
softdevteam:masterfrom
ratmice:lrpar_codegen4

Conversation

@ratmice

@ratmice ratmice commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I think this is probably as reviewable as I'm going to manage to make this large of a patch.
The basic idea behind this patch is to have a kind of "functional pipeline", for doing code generation,

(SrcEnv?, BuildEnvArgs) -> BuildEnv? -> Codegen(SrcEnv, BuildEnv) -> rust_code?

A bit of an oversimplification, as there are some other minor details...

  1. BuildEnvArgs is kind of a minimalist equivalent of the current builder, it's just full of Option values.
  2. BuildEnv is full of derived values, it mostly strips off the Option, but it also contains values like ASTWithValidityInfo that are derived from all the other args.
  3. Codegen then owns the YaccGrammar, StateTable and StateGraphs, which you can take ownership of after generating code.

This may not be totally perfect basis for traits and external usage, for example it currently returns Box<dyn Error> instead of typed errors. But it should be a pretty faithful conversion of the existing process, into a more targeted/self contained module.

Comment thread lrpar/src/lib/codegen.rs
src: &'a str,
// We store the path here so we can generate a module name from it if needed.
// But should never use it for filesystem interaction within this module.
path: &'a Path,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in some future use-cases we might not even have a Path for a given input (e.g. if it's from stdin or a buffer in memory)? If so, I wonder if we should just make this Option<String> for the path? [Maybe not for this PR, but as a simple follow-up.] That might avoid a (borderline-but-not-quite pathological) case where the Path disappears and we don't get a valid pathname out of it later.

@ratmice ratmice Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we kind of need something here stdin, e.g. we derive the default mod_name from it, and error formatting will need some label for error printing, I do think it could be a string though.

Comment thread lrpar/src/lib/codegen.rs
pub(crate) struct ParserBuildEnvArgs<'a> {
/// This allows the parser to originate from from a pre-parsed AST, rather than
/// parsing a grammar definition given as source string into an AST.
ast_originated: Option<&'a ASTWithValidityInfo>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ideally wondered if there are only two routes to us getting the AST: pre-parsed AST or string. I think "there are only those two routes" but perhaps I'm being narrow-minded!

Either way, I wondered if we could maybe tweak this field name a bit. Maybe just ast_validity_info with a docstring like "If the input came from a preparsed AST, this will be Some"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me.

Comment thread lrpar/src/lib/codegen.rs
}

impl<'a> ParserSrcEnv<'a> {
pub(crate) fn new_with_defaults(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there ever be another new method? Should we just call this new?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had it named new, but changed it in case we want a method new like so:

pub fn new(src, path) -> Self {
   self.new_with_defaults(src, path, Header::new())
}

The thought was that this could allow us to make Codegen public without making Header public,
the public API though would only be usable with grammars that have filled out grmtools sections.
And wouldn't allow specifying default values via the header. So a little less flexible than what the builder can do, but something.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method should be new_with_header?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a better name.

Comment thread lrpar/src/lib/codegen.rs
}
}

fn extract_ast_validation(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to prefix these methods with extract_? I'm not saying it's wrong, but I wondered if it has more semantic implication than is immediately obvious to me. I also suspect these should document that they mark headers as used?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had meant to rename these resolve_ but I forgot, is resolve any better?
Essentially these are resolving them to some final/absolute value, from all the possible ways they could have defaults set for them.

Comment thread lrpar/src/lib/codegen.rs
}
}

pub(crate) fn build_env<LexerTypesT>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumb question: could/should this ever be called more than once? If "no" should this consume self?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think calling it more than once would be a waste and inefficient, there is seems no actual harm in it though.
(I should say, there is no reason to, but there is no reason it couldn't either)

However his can't really consume self though:
Codegen::generate() and many functions (all error handling) after build_env require a &ParserSrcEnv reference. I didn't really think about this, and whether BuildEnv should take ownership of the SrcEnv after the call to build_env though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, it looks like Codegen::generate also uses it for error handling.
One thing to note though is what I said in the PR description:

This may not be totally perfect basis for traits and external usage, for example it currently returns Box instead of typed errors.

As an example nimbleparse_lsp wants to send spans directly to the editor, so it wants a totally separate error formatting code than tools like nimbleparse or CTParserBuilder.

Perhaps that changes things.

Comment thread lrpar/src/lib/codegen.rs
}
}

impl<'a, LexerTypesT> ParserBuildEnv<'a, LexerTypesT>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is struct is private to the crate, I wondered if these getter methods are serving a useful purpose?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the only real vital getter that I recall is fn ast_validation it is vital, because that that is how we get the list of warnings, to error from warnings_are_errors in CTParserBuilder.

A lot of the other getters are less vital, the reason they were added was that I couldn't keep into self while codegen was partially migrated from CTParserBuilder to codegen, where we didn't have access to the private variables from outside the module.

But what that does mean, is that if we have an external Codegen trait, that the ParserCodegen struct could be derived from an external source. So if we did make it public, there are getters for all the things needed by the codegen module itself.

I don't exactly know about trait boundaries and the like given that e.g. BuildEnv::code_generator actually returns a CodeGen whether that would make it impossible to use with some external codegen impl.

That was my thinking behind keeping them anyways.

Comment thread lrpar/src/lib/codegen.rs
&self.stable
}

pub(crate) fn take_parser(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's interesting because it consumes self. Why/when do we use it in that way?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the return values to CTParserBuilder::build we construct a CTParser from it in the Ok case.
And I think we need to take ownership of the StateTable and StateGraph for CTConflictsError in some of the error cases too.

Comment thread lrpar/src/lib/codegen.rs
// `lrpar::Node`` is deprecated within the lrpar crate, but not from within this module,
// Once it is removed from `lrpar`, we should move the declaration here entirely.
Some(quote! {
#[allow(unused_imports)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably dedent this a bit (cargo fmt doesn't tend to deal well with macros, so we often have to do it by hand).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants