logos: Lexer produces wrong tokens when more input is provided
My logos lexer implementation somehow does not match the TK_NOT
token when there is more input (like a whitespace) after it. Instead it matches the TK_WORD
token in that case, which should be wrong when it has a lower priority.
Reproducible example with tests:
use logos::Logos;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Logos)]
#[allow(non_camel_case_types)]
pub enum SyntaxKind {
#[regex(r"[ \t]+", priority = 1)]
TK_WHITESPACE = 0,
#[regex(r"[a-zA-Z][a-zA-Z0-9]*", priority = 1)]
TK_WORD,
#[token("not", priority = 50)]
TK_NOT,
#[token("not in", priority = 60)]
TK_NOT_IN,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_not_works() {
let mut lexer = SyntaxKind::lexer("not");
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_NOT)));
}
#[test]
fn word_then_not_works() {
let mut lexer = SyntaxKind::lexer("word not");
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_WORD)));
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_WHITESPACE)));
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_NOT)));
}
#[test]
fn but_this_does_not_work() {
let mut lexer = SyntaxKind::lexer("not word");
// FAILED because
// Left: Some(Ok(TK_WORD)
// Right: Some(Ok(TK_NOT)
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_NOT)));
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_WHITESPACE)));
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_WORD)));
}
#[test]
fn this_is_fine() {
let mut lexer = SyntaxKind::lexer("not in ");
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_NOT_IN)));
assert_eq!(lexer.next(), Some(Ok(SyntaxKind::TK_WHITESPACE)));
}
}
I know that the situation with TK_NOT
and TK_NOT_IN
is maybe not ideal (if I remove the latter it works again). But for my parser it would be way better to have these tokens rather than two separate TK_NOT
and TK_IN
tokens. I would be thankfully for any suggestions that don’t require me to remove either of TK_WORD
or TK_NOT_IN
to make the test but_this_does_not_work
run.
About this issue
- Original URL
- State: open
- Created 2 years ago
- Comments: 20 (1 by maintainers)
I had been running into what seemed like a similar issue, where a longer match would fail and it would backtrack to the wrong token. What was also strange was that the
&str
(ie.lex.slice()
) contained within the mismatched token was actually the slice for what should have been matched. So to hack around the issue I had simply created a callback that tooklex.slice()
and created a new lexer to re-parse slice on its own and correct the token.The code to reproduce my issue is a lot simpler as I was able to reduce it down to not actually perform any regex matching at all, although it still needed a
#[regex(...)]
token to trigger the bug seemingly due to differences in how the graph is generated for regex vs text tokens.Potential Fix?
I took some time today to look into it and the issue appeared to be in the
generator
, which would explain whypriority
had no effect (both for me and @MalteJanz), as that’s only used in the steps beforegenerator
. The issue appears to be with context tracking and that the backtrack point wasn’t being updated, even though the slice was getting bumped correctly.I was able to resolve my issue by adding
|| meta.min_read == 0
to theif
logic at https://github.com/maciejhirsz/logos/blob/v0.14/logos-codegen/src/generator/mod.rs#L115-L119, as that appears to be what causes the slice to be bumped correctly in thematch
block following theif
. After that change I was able to remove my re-lexing hack and all my tests still pass.The change to the
if
logic also appears to fix the issue in the code originally posted by @MalteJanz, but strangely not in the newly posted minimal reproduction case.I’m not familiar enough with the logos internals to say if this is the correct fix though, as it doesn’t fix the issue with @MalteJanz’s minimal case, although perhaps that’s actually a separate bug.
The case originally posted by @MalteJanz does seem very similar to what I was doing, where I was matching a separator token (
TK_WHITESPACE
) and a sequence followed by a separator ("not in "
) would match incorrectly.hi @jameshurst Thanks for your contribution! It has solved the issue that was blocking me for several days. However, I noticed that the PR still does not address the following case:
The PR works well when the leading
-?
is removed. I think the root cause isFork.merge
fails to take the priority settings into consideration when merging two Leaf node. I try to fix it with some ugly code and it works fine in all my own cases. However, my fix will breakcolors::match_colors
,crunch::crunch
,priority_disambiguate_1::priority_abc
as well as thecss::test_letter_spacing
mentioned above.In my scenario, I assume that ‘priority’ should take precedence over ‘longest match’. I’m curious about the design principle of this library: does ‘longest match’ or ‘priority’ prevail? If priority is supposed to prevail, I could find some time to refine the code and submit a PR.
After staring at the code some more, I believe the proper fix may actually be to just remove the
if
logic alltogether and always performctx.switch(self.graph[id].miss())
.Sidenote: After removing the
if
statement, thectx.switch
function can be simplified to not performctx.bump
and no longer return a token stream. Thematch
logic below theif
statement can then be simplified to onlyif enters_loop || meta.min_read == 0
.From what I can see
Generator::goto
will be called to generate:_
branch for amatch
statementmatch
statementFor each of these cases I would expect
self.graph[id].miss()
to indicate the correct backtrack position, orNone
which would causectx.switch
to keep the existing backtrack.I tested this change against my own library, the original code for this issue, and https://github.com/maciejhirsz/logos/issues/160 and they are all passing.
I’d be interested to hear your thoughts on this @jeertmans (and @maciejhirsz’s if available).