#lexer #regex #generator #rule #proc-macro #implemented #semantic

macro lexgen

一个功能齐全的词法分析器生成器,实现为过程宏

18个版本 (破坏性更新)

0.15.0 2023年9月3日
0.14.0 2023年4月23日
0.13.0 2023年4月10日
0.12.0 2022年8月12日
0.4.0 2021年5月30日

#325 in 解析工具

Download history 26/week @ 2024-04-08 69/week @ 2024-04-15 146/week @ 2024-04-22 169/week @ 2024-04-29 67/week @ 2024-05-06 41/week @ 2024-05-13 357/week @ 2024-05-20 21/week @ 2024-05-27 45/week @ 2024-06-03 28/week @ 2024-06-10 69/week @ 2024-06-17 180/week @ 2024-06-24 71/week @ 2024-07-01 249/week @ 2024-07-08 244/week @ 2024-07-15 122/week @ 2024-07-22

每月721次下载
6 个crate中(直接使用3个) 使用

MIT 许可证

250KB
8K SLoC

lexgen: 一个功能齐全的词法分析器生成器,实现为过程宏

lexer! {
    // First line specifies name of the lexer and the token type returned by
    // semantic actions
    Lexer -> Token;

    // Regular expressions can be named with `let` syntax
    let init = ['a'-'z'];
    let subseq = $init | ['A'-'Z' '0'-'9' '-' '_'];

    // Rule sets have names. Each rule set is compiled to a separate DFA.
    // Switching between rule sets is done explicitly in semantic actions.
    rule Init {
        // Rules without a right-hand side for skipping whitespace,
        // comments, etc.
        [' ' '\t' '\n']+,

        // Rule for matching identifiers
        $init $subseq* => |lexer| {
            let token = Token::Id(lexer.match_().to_owned());
            lexer.return_(token)
        },
    }
}

// The token type
#[derive(Debug, PartialEq, Eq)]
enum Token {
    // An identifier
    Id(String),
}

// Generated lexers are initialized with a `&str` for the input
let mut lexer = Lexer::new(" abc123Q-t  z9_9");

// Lexers implement `Iterator<Item=Result<(Loc, T, Loc), LexerError>>`,
// where `T` is the token type specified in the lexer definition (`Token` in
// this case), and `Loc`s indicate line, column, and byte indices of
// beginning and end of the lexemes.
assert_eq!(
    lexer.next(),
    Some(Ok((
        Loc { line: 0, col: 1, byte_idx: 1 },
        Token::Id("abc123Q-t".to_owned()),
        Loc { line: 0, col: 10, byte_idx: 10 }
    )))
);
assert_eq!(
    lexer.next(),
    Some(Ok((
        Loc { line: 0, col: 12, byte_idx: 12 },
        Token::Id("z9_9".to_owned()),
        Loc { line: 0, col: 16, byte_idx: 16 }
    )))
);
assert_eq!(lexer.next(), None);

另请参阅

动机

实现词法分析通常是(与解析一起)实现语言中最枯燥的部分。词法分析生成器使这变得更加容易,但在Rust中现有的词法分析生成器缺少实际使用中的基本功能,并且/或者需要构建时的预处理步骤。

lexgen的目标是拥有一个功能齐全且易于使用的词法分析器生成器。

用法

lexgen不需要构建步骤。在您的Cargo.toml中将相同版本的lexgenlexgen_util添加为依赖项。

词法分析器语法

lexgen词法分析器以生成的词法分析器结构的名称开头,可选的用户状态部分,以及令牌类型(语义动作返回的值类型)。示例

lexer! {
    Lexer(LexerState) -> Token;
    ...
}

这里生成的词法分析器类型将被命名为Lexer。用户状态类型是LexerState(此类型应由用户定义)。令牌类型是Token

在词法分析器名称、用户状态和令牌类型之后,我们定义规则

rule Init {
    ...
}

rule SomeOtherRule {
    ...
}

第一个规则集将定义词法分析器的初始状态,需要命名为Init

rule块的主体中,我们定义该词法状态的规则。规则的语法为:<regex> => <语义动作>,。正则表达式的语法描述如下。语义动作是任何具有类型fn(LexerHandle) -> LexerAction的Rust代码,其中LexerHandleLexerAction是源自词法器名称(例如,我们示例中的Lexer)生成的名称。下面将详细介绍这些类型。

可以使用let <name> = <regex>;语法为正则表达式命名。例如

let init = ['a'-'z'];
let subseq = $init | ['A'-'Z' '0'-'9' '-' '_'];

// Named regexes can be used with the `$` prefix
$init $subseq* => |lexer| { ... }

如果您不需要规则集,可以省略rule Init { ... }部分,并将所有规则放在顶层。

总之

  • 第一行形式为<词法器名称>(<用户状态类型类型>) -> <令牌类型名称>。对于无状态的词法器,可以省略(<用户状态类型类型>)部分。

  • 接下来是规则集。应该至少有一个名为Init的规则集,这是初始状态的名字。

  • 可以在顶层或在rule中添加let绑定。

正则表达式语法

正则表达式语法可以用于let绑定右侧和规则左侧。语法是

  • $var用于在let绑定部分定义的变量。变量在使用前需要定义。

  • $$var用于内置正则表达式(见下文“内置正则表达式”部分)。

  • Rust字符语法用于字符,例如'a'

  • Rust字符串语法用于字符串,例如"abc"

  • [...]用于字符集。在括号内可以有一个或多个

    • 字符
    • 字符范围:例如'a'-'z'

    以下是一个ASCII字母数字字符集的示例:['a'-'z' 'A'-'Z' '0'-'9']

  • _用于匹配任何字符

  • $用于匹配输入结束

  • <regex>*用于匹配0次或多次重复的<regex>

  • <regex>+用于匹配1次或多次重复的<regex>

  • <regex>?用于匹配0次或1次重复的<regex>

  • <regex> <regex>用于连接

  • <regex> | <regex>用于选择:匹配第一个或第二个。

  • <regex> # <regex>用于差集:匹配第一个正则表达式中的字符,但不包含在第二个正则表达式中。注意,在#左右的正则表达式应该是“字符集”,即*+?"..."$和连接是不允许的。绑定到字符集的变量是允许的。

绑定优先级(从高到低)

  • *, +, ?
  • #
  • 连接
  • |

您可以使用括号进行分组,例如:('a' | 'b')*

示例:'a' 'b' | 'c'+ 等价于 (('a' 'b') | ('c'+))

正确上下文(前瞻)

规则集中的一条规则可以使用> <regex>语法跟随另一个正则表达式,用于正确上下文。正确上下文基本上是前瞻的有限形式:它们只能出现在规则顶层正则表达式之后。它们不能在正则表达式中嵌套使用。

例如,规则左侧'a' > (_ # 'b')'a'匹配,只要其后不是'b'

有关更多示例,请参阅正确上下文测试

内置正则表达式

lexgen自带一组内置正则表达式。以下列出的正则表达式与它们的Rust对应物匹配相同的字符集。例如,$$alphabetic与Rust的char::is_alphabetic匹配相同的字符集。

  • $$alphabetic
  • $$alphanumeric
  • $$ascii
  • $$ascii_alphabetic
  • $$ascii_alphanumeric
  • $$ascii_control
  • $$ascii_digit
  • $$ascii_graphic
  • $$ascii_hexdigit
  • $$ascii_lowercase
  • $$ascii_punctuation
  • $$ascii_uppercase
  • $$ascii_whitespace
  • $$control
  • $$lowercase
  • $$numeric
  • $$uppercase
  • $$whitespace

(请注意,在生成的代码中,我们不使用Rust char方法。对于简单的案例,如$$ascii,我们生成简单的范围检查。对于更复杂的案例,如$$lowercase,我们生成一个二分搜索表,并在检查字符时执行二分搜索)

此外,这两个内置正则表达式匹配Unicode XID_Start和XID_Continue

  • $$XID_Start
  • $$XID_Continue

规则语法

  • <regex> => <语义动作>,:如下所述的 <regex> 语法。 <语义动作> 是任何具有类型 fn(&mut Lexer) -> SemanticActionResult<Token> 的 Rust 代码。有关 SemanticActionResult 类型更详细的内容,请参阅下一节。

  • <regex> =? <语义动作>,:可能失败的动作。此语法类似于上面的语法,但 <语义动作> 的类型为 fn(&mut Lexer) -> LexerAction<Result<Token, UserError>>。使用此类规则时,需要在词法分析器的开头声明错误类型,使用 type Error = UserError; 语法。

    当此类规则返回错误时,错误会被返回到词法分析器 next 方法的调用者。

  • <regex>,:省略了 <regex> => |lexer| { lexer.reset_match(); lexer.continue_() }, 的语法糖。对于跳过字符(例如空白字符)非常有用。

  • <regex> = <token>,:省略了 <regex> => |lexer| lexer.return_(<token>), 的语法糖。对于匹配关键字、标点(运算符)和分隔符(括号、方括号)非常有用。

规则集中的输入结束处理

Init 规则集在输入结束(即 lexer.next() 返回 None)时成功结束词法分析。其他规则集在输入结束时失败(即返回 Some(<Err(<...)<))。这是因为通常除了初始状态之外的状态是为复杂标记(字符串、原始字符串、多行注释)而设计的,这些标记需要结束和处理,而这些状态中的输入结束通常意味着标记没有正确结束。

(要在规则集中处理输入结束,可以使用如上所述的“正则表达式语法”部分中描述的 $。)

处理、规则、错误和动作类型

lexer 宏生成一个结构体,其名称由用户在词法分析定义的第一行指定。在开头的示例中(Lexer <-> Token;),结构体的名称是 Lexer

将此类型的可变引用传递给语义动作函数。在语义动作的实现中,您应使用以下方法之一驱动词法分析器并返回标记

  • fn match_(&self) -> &str:返回当前匹配。请注意,当使用 new_from_iternew_from_iter_with_state 构造词法分析器时,此方法会引发恐慌。它仅在用 newnew_with_state 初始化词法分析器时调用。
  • fn match_loc(&self) -> (lexgen_util::Loc, lexgen_util::Loc):返回当前匹配的边界
  • fn peek(&mut self) -> Option<char>:向前看一个字符
  • fn state(&mut self) -> &mut <user state type>:返回对用户状态的可变引用
  • fn return_(&self, token: <用户令牌类型>) -> SemanticActionResult:返回传递的令牌作为匹配项。
  • fn continue_(&self) -> SemanticActionResult:忽略当前匹配项并继续在同一词法分析器状态下进行词法分析。用于跳过字符。
  • fn switch(&mut self, rule: LexerRule) -> SemanticActionResult:用于在词法分析器状态之间切换。`LexerRule`(其中`Lexer`部分是用户指定的词法分析器名称)是一个枚举,具有每个规则集名称的变体,例如,`LexerRule::Init`。请参见下面的状态性词法分析器示例。
  • fn switch_and_return(&mut self, rule: LexerRule, token: <用户令牌类型>) -> SemanticActionResult:切换到给定的词法分析器状态并返回给定的令牌。
  • fn reset_match(&mut self):重置当前匹配项。例如,如果在调用`reset_match()`后立即调用`match_()`,它将返回一个空字符串。

语义动作函数应返回从上述方法之一获得的`SemanticActionResult`值。

初始化词法分析器

lexgen生成4个构造函数

  • fn new(input: &str) -> Self:当词法分析器没有用户状态或用户状态实现Default时使用。

  • fn new_with_state(input: &str, user_state: S) -> Self:当词法分析器具有不实现Default的用户状态,或者您希望使用非默认值初始化状态时使用。 S是在词法分析器定义中指定的用户状态类型。请参阅下面的状态化词法分析器示例。

  • fn new_from_iter<I: Iterator<Item = char> + Clone>(iter: I) -> Self:当输入不是平面字符串,而是类似绳索或拉链的东西时使用。请注意,当使用此构造函数时,match_方法会恐慌。相反,使用match_loc来获取当前匹配的位置。

  • fn new_from_iter_with_state<I: Iterator<Item = char> + Clone, S>(iter: I, user_state: S) -> Self:与上面相同,但不需要用户状态实现Default

状态化词法分析器示例

这是一个示例词法分析器,用于计算两个=之间出现的数量

lexer! {
    // `usize` in parenthesis is the user state type, `usize` after the arrow
    // is the token type
    Lexer(usize) -> usize;

    rule Init {
        $$ascii_whitespace,                             // line 7

        '[' => |lexer| {
            *lexer.state() = 0;                         // line 10
            lexer.switch(LexerRule::Count)              // line 11
        },
    }

    rule Count {
        '=' => |lexer| {
            *lexer.state() += 1;                        // line 17
            lexer.continue_()                           // line 18
        },

        '[' => |lexer| {
            let n = *lexer.state();
            lexer.switch_and_return(LexerRule::Init, n) // line 23
        },
    }
}

let mut lexer = Lexer::new("[[ [=[ [==[");
assert_eq!(
    lexer.next(),
    Some(Ok((
        Loc { line: 0, col: 0, byte_idx: 0 },
        0,
        Loc { line: 0, col: 2, byte_idx: 2 },
    )))
);
assert_eq!(
    lexer.next(),
    Some(Ok((
        Loc { line: 0, col: 3, byte_idx: 3 },
        1,
        Loc { line: 0, col: 6, byte_idx: 6 },
    )))
);
assert_eq!(
    lexer.next(),
    Some(Ok((
        Loc { line: 0, col: 7, byte_idx: 7 },
        2,
        Loc { line: 0, col: 11, byte_idx: 11 },
    )))
);
assert_eq!(lexer.next(), None);

最初(Init规则集)我们跳过空格(第7行)。当我们看到[时,我们初始化用户状态(第10行)并切换到Count状态(第11行)。在Count中,每个=将用户状态增加1(第17行)并跳过匹配(第18行)。在Count状态中的[返回当前数量并切换到Init状态(第23行)。

依赖关系

~270–710KB
~17K SLoC