#语法树 #树结构 #内存 #存储

no-std syntree

为语言开发者提供内存高效的语法树

28 个版本

0.17.2 2024年8月14日
0.16.0 2024年7月29日
0.14.5 2023年3月22日
0.12.1 2022年11月16日
0.11.1 2022年1月9日

#117 in 解析器实现

Download history 176/week @ 2024-05-02 153/week @ 2024-05-09 148/week @ 2024-05-16 114/week @ 2024-05-23 300/week @ 2024-05-30 245/week @ 2024-06-06 362/week @ 2024-06-13 471/week @ 2024-06-20 144/week @ 2024-06-27 80/week @ 2024-07-04 140/week @ 2024-07-11 130/week @ 2024-07-18 344/week @ 2024-07-25 507/week @ 2024-08-01 619/week @ 2024-08-08 292/week @ 2024-08-15

1,771 每月下载量
15 个 crate 中使用 (5 直接)

MIT/Apache

165KB
2K SLoC

syntree

github crates.io docs.rs build status

一个内存高效的语法树。

这个 crate 提供了一个在内存中总是连续存储和操作的树结构。它提供了类似于 rowan 的 API,并旨在作为它的有效替代(更多信息见下文)。

只要实现了 Copy,就可以在树中存储任何内容。


用法

syntree 添加到您的 crate

syntree = "0.17.2"

如果您想查看如何使用 syntree 进行解析的完整示例,请参阅 计算器示例


语法树

这个 crate 提供了一种高效建模 抽象语法树 的方法。树中的节点通常由枚举中的变体表示,但 也可以是任何您想要的东西

每个树由 节点标记 组成。兄弟是树中的中间元素,它们封装了零个或多个其他节点或标记,而标记是表示精确源位置的叶元素。

对于简单的表达式 256 / 2 + 64 * 2 的示例树可以这样表示

[email protected]
  [email protected]
    [email protected] "256"
  [email protected] " "
  [email protected]
    [email protected] "/"
  [email protected] " "
  [email protected]
    [email protected] "2"
  [email protected] " "
  [email protected]
    [email protected] "+"
  [email protected] " "
  [email protected]
    [email protected]
      [email protected] "64"
    [email protected] " "
    [email protected]
      [email protected] "*"
    [email protected] " "
    [email protected]
      [email protected] "2"

自己尝试一下

cargo run --example calculator -- "256 / 2 + 64 * 2"

syntreerowan 之间的主要区别在于我们不将原始源存储在语法树中。相反,库的使用者负责在必要时提供它。就像调用 print_with_source 一样。

通过 Builder 提供 API 来构建语法树,它实现了流式构建方法。内部,构建器被表示为一个连续的内存块。一旦构建了树,就可以通过 Tree 类型查询树的架构。

请注意,syntree::tree! 只是一个辅助宏,用于简化构建示例树。它完全对应于在 openclosetoken 上对 Builder 进行指定的调用。

use syntree::{Builder, Span};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Syntax {
    Number,
    Lit,
    Nested,
}

use Syntax::*;

let mut tree = Builder::new();

tree.open(Number)?;
tree.token(Lit, 1)?;
tree.token(Lit, 3)?;

tree.open(Nested)?;
tree.token(Lit, 1)?;
tree.close()?;

tree.close()?;

let tree = tree.build()?;

let expected = syntree::tree! {
    Number => {
        (Lit, 1),
        (Lit, 3),
        Nested => {
            (Lit, 1)
        }
    }
};

assert_eq!(tree, expected);

let number = tree.first().ok_or("missing number")?;
assert_eq!(number.span(), Span::new(0, 5));

注意,对于 Number 的结果 Span 如何对应其 Lit 子元素的完整范围。包括 Nested 内的元素。

树通常是通过解析输入来构建的。这个库鼓励使用手写的 Pratt 解析器。请参阅 计算器示例 以获取完整用例。


紧凑或空范围

默认情况下,范围使用基于 u32 的索引和基于 usize 的指针,这可以通过使用 Builder::new_with 构造函数从其默认值更改

use syntree::{Builder, Span, Tree};

let mut tree = Builder::<_, usize, u16>::new_with();

tree.open("root")?;
tree.open("child")?;
tree.token("token", 100)?;
tree.close()?;
tree.open("child2")?;
tree.close()?;
tree.close()?;

let tree = tree.build()?;

let expected: Tree<_, usize, u32> = syntree::tree_with! {
    "root" => {
        "child" => { ("token", 100) },
        "child2" => {}
    }
};

assert_eq!(tree, expected);
assert_eq!(tree.span(), Span::new(0, 100));

结合 Empty,这允许在不使用范围的情况下构建树,如果您想这样做的话

use syntree::{Builder, Empty, Tree};

let mut tree = Builder::<_, Empty, u32>::new_with();

tree.open("root")?;
tree.open("child")?;
tree.token("token", Empty)?;
tree.close()?;
tree.open("child2")?;
tree.close()?;
tree.close()?;

let tree = tree.build()?;

let expected: Tree<_, Empty, usize> = syntree::tree_with! {
    "root" => {
        "child" => { "token" },
        "child2" => {}
    }
};

assert_eq!(tree, expected);
assert!(tree.span().is_empty());

为什么不使用 rowan 呢?

我喜欢 rowan。这就是我开始这个项目的原因。但这个 crate 仍然存在,因为它存在一些哲学上的差异,这些差异在 rowan 中很难直接调和。

rowan 只支持添加可以以某种方式强制转换为 repr(u16) 的类型的语法树的一部分。我认为这个决定是合理的,但它阻止了您设计包含除了源引用以外的任何内容的树,而不必执行某种形式的间接查找。这是将 Rune 移动到无损语法树所需的东西(参见 Kind::Str 变体的表示)。

为了说明这种情况,请考虑以下语法

#[derive(Debug, Clone, Copy)]
enum Syntax {
    /// A string referenced somewhere else using the provided ID.
    Synthetic(usize),
    /// A literal string from the source.
    Lit,
    /// Whitespace.
    Whitespace,
    /// A lexer error.
    Error,
}

您可以在 完整的 synthetic_strings 示例 中查看如何使用它。但是,Synthetic 标记不仅可以对应某个源位置(因为它应该从其中一个扩展而来!),它还直接表示它 不是 指向源位置的字符串字面量。

Rune 中,当我们开始 展开宏 时,这成为必需的。因为宏扩展到不引用源位置的东西,所以我们需要某种其他机制来包含标记表示的内容在语法树中。

您可以在 synthetic_strings 示例 中尝试一个 非常 简单的词法时间变量展开器。

cargo run --example synthetic_strings -- "Hello $world"

这将输出

Tree:
[email protected] "Hello"
[email protected] " "
Synthetic(0)@6..12 "$world"
Eval:
0 = "Hello"
1 = "Earth"

所以从本质上讲,syntree 不认为您需要在树本身中存储字符串。即使您想进行字符串存储的简化。所有这些都可以在一边完成,并按您的意愿编码到语法树中。


错误而不是恐慌

这个crate与其他crate的不同之处还在于,当API使用不当时,我们依赖于在树构建过程中传播一个Error,而不是直接恐慌。

表面上这可能只是对编程错误是否应该是错误的一种微小的观点差异。根据我的经验,解析器往往是大型项目中crate的一部分。由用户提供的输入中的边缘情况触发的错误通常可以避免。

比如说,Rune被嵌入到OxidizeBot中,用户提供的脚本中的一段代码触发了rune编译器的错误。这反过来又导致构建了一个非法的树。如果树构建直接恐慌,整个机器人将崩溃。但如果我们传播一个错误,这将需要在OxidizeBot中处理,如果它愿意的话可以恐慌。在这种情况下,简单地记录错误并卸载脚本(并希望收到一个错误报告!)比让机器人崩溃更合适。

Rust有很好的诊断功能来指示结果应该被处理,虽然处理结果比简单地恐慌要尴尬一些,但我相信最终结果是更健壮的软件。


性能和内存使用

在性能方面,唯一的目标是尽可能与rowan一样高效。初步测试表明,在合成工作负载上,syntree稍微快一些,这可能是由于将整个树存储和操作在单个连续内存区域中的原因。这可能在将来改变,具体取决于syntree的需求是否改变。虽然性能很重要,但它不是主要关注点。

我还期望(但尚未验证)syntree可能最终会有与rowan相似的较低内存占用,后者执行节点去重。唯一的限制是它取决于原始源是如何存储和查询的。这是rowan为你解决的问题,但syntree将其留给了读者去练习。

无运行时依赖