3 个不稳定版本
新功能 0.2.0 | 2024 年 8 月 19 日 |
---|---|
0.1.1 | 2024 年 8 月 17 日 |
0.1.0 | 2024 年 8 月 17 日 |
#220 在 算法
每月 317 次下载
180KB
3.5K SLoC
关于 scnr
此包提供了一个具有足够正则表达式支持和最小编译时间的扫描器/词法分析器。扫描器默认支持多种扫描器模式。扫描器模式类似于 Lex/Flex 中的开始条件。
它目前仍处于早期阶段,尚未准备好用于生产。早期采用者可以相当安全地使用它。如果在使用过程中发现任何错误,请报告。
如何使用它
use scnr::ScannerBuilder;
static PATTERNS: &[&str] = &[
r";", // Semicolon
r"0|[1-9][0-9]*", // Number
r"//.*(\r\n|\r|\n)", // Line comment
r"/\*([.\r\n--*]|\*[^/])*\*/", // Block comment
r"[a-zA-Z_]\w*", // Identifier
r"=", // Assignment
];
const INPUT: &str = r#"
// This is a comment
a = 10;
b = 20;
/* This is a block comment
that spans multiple lines */
c = a;
"#;
fn main() {
let scanner = ScannerBuilder::new()
.add_patterns(PATTERNS)
.build()
.expect("ScannerBuilder error");
let find_iter = scanner.find_iter(INPUT);
for ma in find_iter {
println!("Match: {:?}: '{}'", ma, &INPUT[ma.span().range()]);
}
}
安全带
- 扫描器应快速构建。
- 扫描器仅基于 DFA,未实现回溯
- 扫描器可能永远不会支持
u8
,即模式类型可转换为&str
,输入类型可转换为&str
。我们专注于编程语言而不是字节序列。
不支持的正则表达式功能
我们不支持 锚定匹配,即 ^、$、\b、\B、\A、\z 等不可用。这通常可以忍受,因为扫描器整体属性,特别是最长匹配将获胜,这减轻了对此类锚点的需求。
详细说明
假设您有一个关键字 'if' 的模式和一个标识符模式 /[a-zA-Z_][a-zA-Z0-9_]*/。这两个模式都可能匹配 'if',但只要关键字模式插入在标识符模式之前,关键字就会获胜。如果扫描器遇到类似 'ifi' 的输入,则标识符将匹配,因为最长匹配规则。有了这些保证,简单地声明关键字 'if' 附带单词边界 (\b) 就是不必要的。
我们还目前不支持 标志(i、m、s、R、U、u、x),如 r"(?i)a+(?-i)b+"
。我需要评估这是否是问题,但目前我相信这是可以容忍的。
在标记匹配的上下文中,不需要 捕获组,因此我认为不需要实现此功能。
不支持的 Flex 功能
除了锚点^和$之外,尾随上下文,例如在ab/cd
中,目前不支持,因为这需要在字符迭代器的常规前进循环之外提供前瞻。尽管已经做好了准备,但我们将会推迟这个功能,直到有强烈的需求出现。
重复的贪婪性
关于贪婪性的一些说明。
POSIX中Lex/Flex的正常匹配是贪婪的。它在一定程度上遵循最长匹配规则,但在扫描器的运行时回溯过程中会产生一些开销。
由于scnr
只与最小化的DFA一起工作(当前情况,可能改变),它始终非贪婪地匹配*和+等重复。
重复的退出条件
但在这个意义上,您必须非常具体地说明重复表达式的内容,即从重复表达式到正则表达式后续部分的转换应该是明确的。
让我们看看这个正则表达式,其中有一个中间的重复表达式.
。
/\*.*\*/
这个DFA看起来像这样
关键是状态3,它取决于输入是否继续重复还是继续进行后续部分,这里的状态1。但是.
也匹配*
,这引入了与确定性有限自动机的常见概念相矛盾的不确定性。如何解决这取决于扫描器运行时的实现。这应该明确避免。
所以,我们可以做的第一件事是更精确地说明重复表达式的内容。我们可以从.
中移除*
。
/\*[.--*]*\*/
这看起来更确定,但现在我们揭示了一个问题,这个问题实际上已经在第一个版本中存在。
扫描以下输入将搞乱匹配
/* a* */
扫描器在读取*
(在a
之后)时进入状态1,并在匹配空格时失败,而不是期望匹配/
。原因是重复表达式不考虑其后的部分。
所以,我们也需要在这方面更加具体
/\\*([.--*]|\\*[^/])*\\*/
这意味着重复表达式可以是任何字符,除了*
,或者是一个*
后面跟着一个不是/
的字符。
这个解决方案将完美地完成任务,因为它的自动机能够在退出条件失败时返回重复。
扫描器模式
对于退出条件上的歧义以及处理重复表达式中的后续表达式等问题,一种更灵活但稍微复杂的方法是引入第二种扫描器模式,该模式在/\\*
的注释开始时进入,然后处理注释中的所有令牌,并在\\*/
的注释结束时再次进入INITIAL模式。
扫描器模式可以用例如json来定义
[
{
"name": "INITIAL",
"patterns": [["/\\*", 1]],
"transitions": [[1, 1]]
},
{
"name": "COMMENT",
"patterns": [
["\\*/", 2],
["[.\\r\\n]", 3]
],
"transitions": [[2, 0]]
}
]
在这里您可以看到两种模式。扫描器始终以模式0启动,通常是初始模式。当遇到类型1的标记(注释开始)时,它切换到模式1,即注释模式。在这里,类型2的标记(注释结束)比代码 [.\\r\\n]
具有更高的优先级,仅仅因为其在模式切片中的索引更低。在类型2上,它再次切换到初始模式。所有其他标记都由类型3,即注释内容覆盖。
在这种情况下,您可以想象解析器知道类型3是注释内容,并可以相应地处理它。
依赖项
~1.6-2.9MB
~71K SLoC