-
Notifications
You must be signed in to change notification settings - Fork 42
Add docs about multiple start rules. #649
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
Open
ratmice
wants to merge
2
commits into
softdevteam:master
Choose a base branch
from
ratmice:docs_multistart
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| # Multiple start rules | ||
|
|
||
| The LR parsing algorithm starts with a single start rule, occasionally | ||
| people find that they want to generate a parser which starts in the middle of some parse rule. | ||
| For example, their parser may take a `Sequence` of `Statement` rules. But they also want a version of their | ||
| parser to which they can pass a single `Expression`. | ||
|
|
||
| ## An initial attempt | ||
|
|
||
| One of the solutions to this problem is to just copy the parser but change the start rule to the | ||
| `Expression` rule. This is a maintenance burden as you end up having multiple copies of your parser. | ||
| One way to make this easier to maintain, is to programatically change the start rule. This can be done | ||
| with the [clone_and_change_start_rule](https://docs.rs/cfgrammar/latest/cfgrammar/yacc/ast/struct.ASTWithValidityInfo.html#method.clone_and_change_start_rule) function. | ||
|
|
||
| ### Shortcomings | ||
|
|
||
| Each time we clone the grammar rule, we're also duplicating the data parsing data tables used by the algorithm. | ||
| This can add up, so at the cost of some complexity we can try another approach to work around the problem. | ||
|
|
||
| ## A more efficient solution. | ||
|
|
||
| At the cost of some complexity to the grammar, we can try a different approach. | ||
| To avoid duplicating the internal parser data tables. We'll attempt to use some synthetic tokens, not produced | ||
| by the lexer. Then we'll pass these into the parser to kick start the process. This token can be used to select | ||
| which subset of the parser we want to start parsing. We will use the `%tokens START_A START_B`, for these | ||
| synthetic tokens. | ||
|
|
||
| ### First steps: | ||
|
|
||
| First create a new project, add the dependencies and build dependencies. | ||
|
|
||
| ```console | ||
| cargo new multistart | ||
| cd multistart | ||
| cargo add lrlex lrpar --build | ||
| cargo add lrlex lrpar cfgrammar | ||
| ``` | ||
|
|
||
| Here is our grammar `src/multistart.y` | ||
|
|
||
| ``` | ||
| %grmtools{yacckind: Grmtools} | ||
| %token START_A START_B | ||
| %start meta_start | ||
| %% | ||
| meta_start -> Result<String, DefaultLexeme> : | ||
| START_A ARule { $2 } | ||
| | START_B BRule { $2 } | ||
| ; | ||
|
|
||
| ARule -> Result<String, DefaultLexeme> | ||
| : 'A' { Ok($lexer.span_str($1?.span()).to_string()) } | ||
| | BRule { $1 } | ||
| ; | ||
|
|
||
| BRule -> Result<String, DefaultLexeme> | ||
| : 'B' { Ok($lexer.span_str($1?.span()).to_string()) } | ||
| ; | ||
| %% | ||
| use lrlex::DefaultLexeme; | ||
| ``` | ||
|
|
||
| We've declared a `meta_start` as our start rule, and it branches to each of the rules which we want to start parsing from. | ||
| Branching out based on the initial token. | ||
|
|
||
| ### Jump starting the parser. | ||
|
|
||
| There are a few ways we can try and achieve adding these initial tokens | ||
|
|
||
| 1. As we've seen previously in the manual, we can write a manual lexer. | ||
| 2. Use an `lrlex` lexer but wrap the output, adding the initial token. | ||
|
|
||
| Lets just build both in one project. The build scripts would be a little bit simpler | ||
| if we built them in separate projects. Just refer to the other examples in this book | ||
| for a cleaner project structure. | ||
|
|
||
| In the future we may explore extensions which make these easier or more efficient. | ||
|
|
||
| ### With a manual lexer | ||
|
|
||
| Intial `build.rs` script. | ||
|
|
||
| ```rust | ||
| use lrlex::{CTTokenMapBuilder, DefaultLexerTypes}; | ||
| use lrpar::CTParserBuilder; | ||
| use std::path::PathBuf; | ||
|
|
||
| fn main() { | ||
| let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); | ||
|
|
||
| // 1. Build a token_map for a manual lexer. | ||
| let ctp = CTParserBuilder::<DefaultLexerTypes>::new() | ||
| .grammar_in_src_dir("multistart.y").unwrap() | ||
| .mod_name("multi_manual_y") | ||
| .output_path(out_dir.join("multi_manual.y.rs")) | ||
| .build() | ||
| .unwrap(); | ||
| CTTokenMapBuilder::new("token_map", ctp.token_map()).build().unwrap(); | ||
|
|
||
| // 2. We'll be adding a wrapped lexer here. | ||
| } | ||
| ``` | ||
|
|
||
| Calling the parser with a manual lexer: | ||
|
|
||
| This is based off the [Hand-written Lexer](manuallexer.md) section, lets start off with the first rule. | ||
|
|
||
| ``` | ||
| use cfgrammar::NewlineCache; | ||
| use lrlex::{lrlex_mod, DefaultLexeme, DefaultLexerTypes, LRNonStreamingLexer}; | ||
| use lrpar::{lrpar_mod, Lexeme}; | ||
|
|
||
| lrpar_mod!("multi_manual.y"); | ||
| lrlex_mod!("token_map"); | ||
|
|
||
| use token_map::*; | ||
|
|
||
| // For simplicty rather than writing a lexer, lets just build a hard coded list of tokens | ||
| // A real example would actually lex things. This just returns a vector of tokens with | ||
| // an initial `START_B` token | ||
| fn a_rule_lex(s: &str) -> LRNonStreamingLexer<'_, '_, DefaultLexerTypes> { | ||
| let mut lexemes = Vec::new(); | ||
| let newlines = NewlineCache::new(); | ||
|
|
||
| lexemes.push(Ok(DefaultLexeme::new(T_START_A, 0, 0))); | ||
| lexemes.push(Ok(DefaultLexeme::new(T_A, 0, 1))); | ||
| LRNonStreamingLexer::new(s, lexemes, newlines) | ||
| } | ||
|
|
||
| fn with_manual_lexer() { | ||
| println!("{:?}", multi_manual_y::parse(&a_rule_lex("A"))); | ||
|
|
||
| } | ||
|
|
||
| fn main() { | ||
| with_manual_lexer(); | ||
| } | ||
| ``` | ||
|
|
||
| Now we can just modify these rules to call into it with the other start token. | ||
|
|
||
| ``` | ||
| // Add a second function modified for the `T_START_B` rule. | ||
| fn b_rule_lex(s: &str) -> LRNonStreamingLexer<'_, '_, DefaultLexerTypes> { | ||
| let mut lexemes = Vec::new(); | ||
| let newlines = NewlineCache::new(); | ||
|
|
||
| lexemes.push(Ok(DefaultLexeme::new(T_START_B, 0, 0))); | ||
| lexemes.push(Ok(DefaultLexeme::new(T_B, 0, 1))); | ||
| LRNonStreamingLexer::new(s, lexemes, newlines) | ||
| } | ||
|
|
||
|
|
||
| fn with_manual_lexer() { | ||
| println!("{:?}", multi_manual_y::parse(&a_rule_lex("A"))); | ||
| // Update to add the second entry point. | ||
| println!("{:?}", multi_manual_y::parse(&b_rule_lex("B"))); | ||
| } | ||
| ``` | ||
|
|
||
| ### With a wrapper around lrlex | ||
|
|
||
| First we need a lexer. We'll include the `START_A` and `START_B` tokens in the grammar, using rules that | ||
| cannot match any input characters. | ||
|
|
||
| `src/multistart.l` | ||
| ``` | ||
| %% | ||
| A "A" | ||
| B "B" | ||
| [\ \n\t] ; | ||
| [^\p{any}] "START_A" | ||
| [^\p{any}] "START_B" | ||
| ``` | ||
|
|
||
|
|
||
| Additions to build.rs | ||
|
|
||
| ``` | ||
| use lrlex::CTLexerBuilder; | ||
|
|
||
| // 2. Lets try the same grammar using a lrlex lexer. | ||
| CTLexerBuilder::new() | ||
|
|
||
| .lrpar_config(|ctp| { | ||
| ctp.grammar_in_src_dir("multistart.y") | ||
| .unwrap() | ||
| .mod_name("multi_lrlex_y") | ||
| .output_path(out_dir.join("multi_lrlex.y.rs")) | ||
| }) | ||
| .lexer_in_src_dir("multistart.l") | ||
| .unwrap() | ||
| .mod_name("multi_lrlex_l") | ||
| .output_path(out_dir.join("multi_lrlex.l.rs")) | ||
| .build() | ||
| .unwrap(); | ||
| ``` | ||
|
|
||
| The approach we will take is to run the lrlex lexer, and get a list of tokens. | ||
| then append that to a vector which contains the start rule. | ||
|
|
||
| Here is our function `lrlex_wrapper` which takes an input string, and a start token, | ||
| It produces the vector with the start rule as the initial token, followed by each subsequent | ||
| token. From that vector it constructs a final `LRNonStreamingLexer` which we can pass to our parser. | ||
|
|
||
| Import the modules we generated, and other types and traits we'll need. | ||
|
|
||
| ``` | ||
| use lrlex::LRLexError; | ||
| use lrpar::Lexer as _; | ||
|
|
||
| lrpar_mod!("multi_lrlex.l"); | ||
| lrpar_mod!("multi_lrlex.y"); | ||
| ``` | ||
|
|
||
| Here is our wrapper function: | ||
|
|
||
| ``` | ||
| fn lrlex_wrapper(s: &str, start_token: u32) -> LRNonStreamingLexer<'_, '_, DefaultLexerTypes> { | ||
| let nl_cache = NewlineCache::new(); | ||
| let lexerdef = multi_lrlex_l::lexerdef(); | ||
| let mut tokens: Vec<Result<DefaultLexeme, LRLexError>> = vec![Ok(DefaultLexeme::new(start_token, 0, 0))]; | ||
| let lexer = lexerdef.lexer(s); | ||
| tokens.extend(lexer.iter()); | ||
| LRNonStreamingLexer::new(s, tokens, nl_cache) | ||
| } | ||
| ``` | ||
|
|
||
| Modify the main function to call it with some data, and print the results. | ||
|
|
||
| ``` | ||
| fn with_lrlex_wrapper() { | ||
| println!("{:?}", multi_lrlex_y::parse(&lrlex_wrapper("A", T_START_A))); | ||
| println!("{:?}", multi_lrlex_y::parse(&lrlex_wrapper("B", T_START_B))); | ||
| // This should parse okay, B is a subset of A, taking either "A", or "B" as input. | ||
| println!("{:?}", multi_lrlex_y::parse(&lrlex_wrapper("B", T_START_A))); | ||
|
|
||
| // This should be an error, as `START_B` only takes "B" as input. | ||
| println!("{:?}", multi_lrlex_y::parse(&lrlex_wrapper("A", T_START_B))); | ||
| } | ||
|
|
||
| fn main() { | ||
| with_manual_lexer(); | ||
|
|
||
| with_lrlex_wrapper(); | ||
| } | ||
| ``` | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are mostly just to silence the
missing from lexerwarning, it seemed like the easiest way at the time.There is also
allow_missing_terms_in_lexer, though that doesn't appear to allow you to say set a list of terms.Although these regex should be very fast, I suppose their existence in the list of lex rules, still means you're
going to check the empty regex for each character.
I don't know in practice how much of a difference that makes, for however many synthetic start tokens people have, over each unmatched character. I suppose that if they're at the very end, they'll only match unmatched characters in practice.
As such, I should probably move the next line above these rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is at least improved in c9b377c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I wonder if we should add an equivalent of lrpar's
%expect-unusedto lrlex? Arguably we could then remove the Rust APIallow_missing_terms_in_lexerentirely (or, perhaps, make it a no-op and deprecated for 0.15.0?).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like a good idea, I'll try work on this some more mid week.