#ast #grammar #tokens #ast-parser #parse #language #parse-tree

sprout

A rust 包用于生成简单而美观的 AST 树 🌳

5 个版本 (1 个稳定版本)

1.0.0 2022 年 11 月 6 日
0.2.1 2022 年 11 月 5 日
0.2.0+1 2022 年 11 月 5 日
0.1.1 2022 年 11 月 3 日
0.1.0 2022 年 11 月 3 日

#95 in 解析工具

MIT 许可证

120KB
2K SLoC

sprout 是一个 rust 包,可以将文本解析为 AST(抽象语法树),给定由这些标记构成的标记和语法的定义。

使用方法

您可以从导入 sprout 预导入开始。对于大多数项目来说,这将是足够的。

use sprout::prelude::*;

示例

首先,为您的标记定义一个枚举。它应该派生出以下必要特质。

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Token {
   Number,
   Word,
   Space
}

为您的标记枚举实现 std::fmt::Display。这将用于生成可读的错误消息。

use std::fmt;

impl fmt::Display for Token {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      match self {
         Self::Number => write!(f, "number"),
         Self::Word => write!(f, "word"),
         Self::Space => write!(f, "space")
      }
   }
}

现在,您可以使用 alphabet 宏通过正则表达式语法的子集为您的标记提供定义,包括括号(())、方括号([])、范围([a-z]),以及运算符 *+?

use Token::*;

let alphabet = alphabet! {
   Number => "[0-9]+";
   Word => "[a-z]+";
   Space => " "
};

接下来,您定义您的语法,以“过程”或“procs”的形式。它们是您的语言的抽象部分,最终将形成您的语法树的节点。

首先,您再次创建一个枚举

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Proc {
   TwoOrThreeWords,
   WordOrNumber,
   Sequence
}

然后,再次实现 std::fmt::Display

impl fmt::Display for Proc {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      match self {
         Self::TwoOrThreeWords => write!(f, "two or three words"),
         Self::WordOrNumber => write!(f, "word or number"),
         Self::Sequence => write!(f, "word/number sequence")
      }
   }
}

最后,使用 grammar 宏定义您的过程。

use Token::*;
use Proc::*;

let grammar = grammar! {
   #TwoOrThreeWords => Word, Space, Word, (Space, Word)?;
   #!WordOrNumber => [Word; Number];
   #?Sequence => ([#TwoOrThreeWords; #WordOrNumber]){Space}+,
};

正如您在这个示例中所见,在语法定义中,过程名称总是以 # 为前缀。

过程定义上的 #! 前缀意味着该过程是 原生的原生 过程被视为您语言的 简单构建块,这意味着错误消息将根据它们的名称而不是它们的组合部分来引用它们。您应该在那些构造可以被认为是实现细节并且与用户无关的低级过程中使用 #!

在过程定义中使用 #? 前缀表示该过程是 隐藏的,这意味着它们永远不会以名称出现在错误信息中。这样做可以确保你的错误信息不会过于抽象。你应该在那些高级且抽象、与本地化解析错误无关的过程上使用 #?

标记的名称通常保持不变,但你也可以通过在它们之前添加 @ 将标记指定为 签名。签名标记会使当前过程优先于其他过程,即使它只匹配到那个点。这很方便,如果你想定义一个特定的结构,迫使解析器以某种方式解释文本,即使这会导致错误。使用签名来尝试捕捉到某个过程“显然”是打算使用的直觉是有益的,即使其结构不正确;这有助于生成更有帮助的错误信息。

标记/过程的序列以逗号分隔。

你可以在过程定义中使用以下特殊语法来定义更复杂的模式

语法 描述
(<A>)* 重复 <A> 零次或多次
(<A>)+ 重复 <A> 一次或多次
(<A>){<B>}* 重复 <A> 零次或多次,由 <B> 分隔
(<A>){<B>}+ 重复 <A> 一次或多次,由 <B> 分隔
(<A>)? <A> 是可选的
[<A>; <B>; ...] 选择 <A><B> 等。

现在,最后,从你的字母表和语法中,你可以构建一个解析器

let parser = Parser::new(alphabet, grammar);

这个解析器现在将简单地输出一个 AST(或解析错误)对于你扔给它的任何字符串!例如这个输入

let tree = parser.parse(Proc::Sequence, "abc ab 123 xyz 69".to_string());

将产生如下 AST

Sequence["abc ab 123 xyz 69"](
   TwoOrThreeWords["abc ab"]
   WordOrNumber["123"]
   WordOrNumber["xyz"]
   WordOrNumber["69"]
)

具体来说,输出类型 ASTtrees::Tree 的别称,其中 ASTNode 有以下字段

名称 类型 描述
proc Proc(或你给它取的名字) 与该节点对应的过程
text 字符串 该过程实例中的文本
pos TextPosition(具有 linechar 字段) 实例在文本中的起始位置

有关 trees::Tree API 的更多信息,请参阅 trees 文档

还有...

是的,我知道这个包的描述成为 RAS 综合征的受害者,但我不在乎 :)

依赖关系

~2-12MB
~75K SLoC