#lexer #parser-generator #parser #generator #scanner #tokenizer

plex

用于编写词法和解析器的语法扩展

11个版本

0.3.1 2024年6月4日
0.3.0 2023年10月28日
0.2.5 2019年6月16日
0.2.3 2018年8月20日
0.0.2 2016年5月1日

#1590过程宏

Download history 244/week @ 2024-05-02 276/week @ 2024-05-09 174/week @ 2024-05-16 130/week @ 2024-05-23 269/week @ 2024-05-30 196/week @ 2024-06-06 116/week @ 2024-06-13 110/week @ 2024-06-20 59/week @ 2024-06-27 69/week @ 2024-07-04 283/week @ 2024-07-11 113/week @ 2024-07-18 84/week @ 2024-07-25 78/week @ 2024-08-01 98/week @ 2024-08-08 66/week @ 2024-08-15

342 每月下载
5 个crate中使用 (直接使用2个)

MIT/Apache

45KB
1K SLoC

plex,一个解析器和词法分析器生成器

此crate提供了一些语法扩展

  • lexer!,它创建一个基于DFA的词法分析器,使用最大 munch。它的工作方式类似于lex工具。你编写正则表达式来定义你的令牌,以及从输入切片中创建你的令牌的Rust表达式。
  • parser!,它创建一个LALR(1)解析器。它的工作方式类似于yacc。你编写一个上下文无关文法,以及每个规则的表达式。你给每个非终结符分配一个Rust类型,允许你递归地构建AST。它还支持范围,提供方便的源位置报告。

你可以在examples/demo.rs中找到一个示例。

用法

首先,包含plex宏。

use plex::{lexer, parser};

创建词法分析器

要定义词法分析器,使用lexer!宏。

lexer! {
    fn take_token(tok: 'a) -> Token<'a>;

首先声明函数的名称、你将在词法分析器内部访问的令牌的名称以及你的词法分析器的返回类型。你也可以可选地声明你接受的字符串的生存期(这里,'a)。

请注意,这将声明一个具有实际签名的函数:fn take_token<'a>(text: &mut &'a str) -> Option<Token<'a>>。词法分析器将修改 text 切片以移除已消费的文本。这是为了便于从字符串切片中创建 Token 迭代器。

    r"[ \t\r\n]" => Token::Whitespace,
    "[0-9]+" => Token::IntegerLiteral(tok.parse().unwrap()),
    r#""[^"]*""# => Token::StringLiteral(&tok[1..tok.len()-1]),

您的词法分析器其余部分应包含规则。左侧应与正则表达式对应的字面字符串(原始字符串字面量是可以的)。您可以使用典型的正则表达式语法,包括用于分组的括号、用于字符类的方括号以及通常的 .|*+。(? 目前不受支持。)您还可以使用一些额外的运算符,如 ~ 用于否定和 & 用于合取

    r"/\*~(.*\*/.*)\*/" => Token::Comment(tok),

上述正则表达式将匹配具有 /* */ 分隔符的 C 风格注释,但不会允许在注释中出现 */。(.*\*/.* 匹配包含 */ 的任何字符串,~(.*\*/.*) 匹配不包含任何内容的字符串。)这很重要,因为词法分析器使用最大 munch。如果您只是简单地写出 r"/\*.*\*/",那么词法分析器将消费最长的匹配子串。这将解释 /* comment */ not comment? /* comment */ 为一个大的注释。

    "let" => Token::Let,
    "[a-zA-Z]+" => Token::Ident(tok),
    "." => panic!("unexpected character"),
}

请注意,如果有多个规则可以适用,则首先声明的规则获胜。这使得您可以通过将它们放在第一位来声明关键字(关键字具有比标识符更高的优先级)。

创建解析器

plex 使用解析器的 LALR(1) 构造。本节和 plex 在一般上假设您理解 LR 解析及其相关词汇。

要定义解析器,请使用 parser! 宏。

parser! {
    fn parse(Token, Span);

这里声明了解析器的名称(在本例中为 parse)以及它所接受的输入类型。在本例中,parse 将接受任何对((Token, Span))的迭代器。标记类型必须是一个有效的枚举类型(其变体处于作用域内)。(这是 plex 的当前限制,可能会在以后修复。)这些变体是语法中的终结符。plex 生成的解析器还会跟踪输入到其中的源位置(“跨度”),因此您需要在这里提及您的跨度类型。如果您不想跟踪源位置,可以使用单位类型 ()

接下来,告诉 plex 如何组合两个跨度

    (a, b) {
        Span {
            lo: a.lo,
            hi: b.hi,
        }
    }

在这里,abSpan。在本例中,我们已将 Span 定义为一个包含两个字段的结构,即 lohi,分别表示跨度的起始和结束字节的偏移量。请注意,这里需要额外的括号:函数体的内容必须是一个块。

现在您编写您的语法。对于每个非终结符,写上它的名称和类型。这表示非终结符解析成的数据类型。

    statements: Vec<Expr> {

请注意,第一个非终结符是特殊的:它是语法的起始符号,其类型是解析器的返回类型(大致上)。

然后为这个非终结符编写规则。(每个规则的左侧隐含为 statements。)

        statements[mut st] expr[e] Semi => {
            st.push(e);
            st
        }

编写规则的右侧,即一个箭头 =>,以及处理此规则的代码。右侧是一系列要匹配的非终结符或终结符。在这里,statementsexpr 是非终结符。方括号将一个模式分配给非终结符的结果,允许我们使用该非终结符返回的数据。终结符必须是引入作用域的枚举变体。表达式必须评估为左侧的类型:在本例中,为 Vec<Expr>

        => vec![],
    }

允许空规则:只需在箭头之前不写任何内容。

如果终结符(即标记)是一个类似元组的枚举变体,并且包含数据,您应该使用圆括号对其进行解构

    expr: Expr {
        Ident(s) => Expr::Var(span!(), s)
    }
}

在规则内部,span!() 宏计算为当前右侧的跨度。然而,这只在至少匹配了一个标记时才有效。如果规则匹配了一个空序列,span!() 将引发恐慌,因此在可空规则中避免使用它。

此解析器的返回类型为 Result<Vec<Expr>, (Option<(Token, Span)>, &'static str)>。错误类型是一个由意外的标记(或 None 表示的 EOF)和描述预期标记的消息组成的对。

依赖关系

~240–720KB
~17K SLoC