#scanner #lexer #tokenizer #compile-time

scnr

带有正则表达式模式和多种模式的扫描器/词法分析器

3 个不稳定版本

新功能 0.2.0 2024 年 8 月 19 日
0.1.1 2024 年 8 月 17 日
0.1.0 2024 年 8 月 17 日

#220算法

Download history 317/week @ 2024-08-16

每月 317 次下载

MIT/Apache

180KB
3.5K SLoC

Rust Docs.rs Crates.io

关于 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看起来像这样

CppComments1

关键是状态3,它取决于输入是否继续重复还是继续进行后续部分,这里的状态1。但是.也匹配*,这引入了与确定性有限自动机的常见概念相矛盾的不确定性。如何解决这取决于扫描器运行时的实现。这应该明确避免。

所以,我们可以做的第一件事是更精确地说明重复表达式的内容。我们可以从.中移除*

/\*[.--*]*\*/

CppComments2

这看起来更确定,但现在我们揭示了一个问题,这个问题实际上已经在第一个版本中存在。

扫描以下输入将搞乱匹配

/* a* */

扫描器在读取*(在a之后)时进入状态1,并在匹配空格时失败,而不是期望匹配/。原因是重复表达式不考虑其后的部分。

所以,我们也需要在这方面更加具体

/\\*([.--*]|\\*[^/])*\\*/

这意味着重复表达式可以是任何字符,除了*,或者是一个*后面跟着一个不是/的字符。

CppComments3

这个解决方案将完美地完成任务,因为它的自动机能够在退出条件失败时返回重复。

扫描器模式

对于退出条件上的歧义以及处理重复表达式中的后续表达式等问题,一种更灵活但稍微复杂的方法是引入第二种扫描器模式,该模式在/\\*注释开始时进入,然后处理注释中的所有令牌,并在\\*/注释结束时再次进入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