Skip to content
返回

在 Rust 里写解析器:Chumsky、Pest 以及其他选择

发布于

解析器在 Rust 里的几条路径

Rust 生态的解析器库大致可以分成几类:

本文主要对比 Chumsky vs Pest,再顺带盘点一下其他常见选项,给一个比较清晰的心智地图。

Chumsky vs Pest:两种完全不同的哲学

核心理念对比

ChumskyPest
类型Parser CombinatorPEG Grammar 文件
语法定义直接写在 Rust 代码里独立的 .pest 文法文件
错误恢复✅ 强调错误恢复与容错❌ 主要做“失败就报错”,恢复较弱
学习曲线中等:要习惯组合子风格较低:看起来像 BNF,容易上手
类型安全✅ 直接产出强类型 AST⚠️ 返回通用 Pair 树,需要手工映射
生态绑定常与 Ariadne 搭配做漂亮错误信息本身专注在解析,错误展示需自己处理

可以简单理解为:

示例:用 Pest 解析简单算术表达式

目标:解析 1 + 2 - 3 这样的表达式,把它识别成一串 numberop

calc.pest

number = @{ ASCII_DIGIT+ }
op     =  { "+" | "-" }
expr   =  { number ~ (op ~ number)* }
WHITESPACE = _{ " " }

main.rs

use pest::Parser;
use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "calc.pest"]
struct CalcParser;

fn main() {
    let pairs = CalcParser::parse(Rule::expr, "1 + 2 - 3").unwrap();

    for pair in pairs {
        for inner in pair.into_inner() {
            println!("{:?}: {}", inner.as_rule(), inner.as_str());
        }
    }
}

输出:

number: "1"
op: "+"
number: "2"
op: "-"
number: "3"

你拿到的是一个通用的 Pair 树,里面只有“这是一个 Rule::numberRule::op,它的文本是啥”。接下来通常要做两件事:

优点是:

代价是:

示例:用 Chumsky 直接构建强类型 AST

同样的目标,解析 1 + 2 - 3,这次我们想直接拿到一个带结构信息的 AST。

use chumsky::prelude::*;

#[derive(Debug)]
enum Expr {
    Num(i64),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
}

fn parser() -> impl Parser<char, Expr, Error = Simple<char>> {
    let num = text::int(10)
        .map(|s: String| Expr::Num(s.parse().unwrap()))
        .padded();

    let op = just('+').or(just('-')).padded();

    num.then(op.then(num).repeated())
        .foldl(|lhs, (op, rhs)| match op {
            '+' => Expr::Add(Box::new(lhs), Box::new(rhs)),
            '-' => Expr::Sub(Box::new(lhs), Box::new(rhs)),
            _   => unreachable!(),
        })
}

fn main() {
    let result = parser().parse("1 + 2 - 3");
    println!("{:#?}", result);
}

输出:

Ok(
    Sub(
        Add(Num(1), Num(2)),
        Num(3),
    )
)

这里的几个关键点:

相比 Pest:

错误恢复与 IDE 友好性:Chumsky 的优势在哪里?

传统很多解析库都把重点放在“成功解析一份合法输入”,而对“如何优雅地处理不合法输入”关注较少。对于编译器前端、LSP、静态分析工具来说,这个优先级其实是反过来的——现实世界的代码大部分时间是“不合法”的。

Chumsky 的设计基本上是沿着这条思路来的:

如果你想在语义层(名字解析、类型检查等)继续报漂亮的错误,核心做法就是:

相比之下,Pest 本身并不围绕“永不失败式解析”和 IDE 场景设计,你需要更多手工工作来:

除了 Chumsky 和 Pest 还能选什么?

nom / winnow:偏向性能和二进制协议的组合子

如果你是做 网络协议解析、日志格式解析 之类的工作,nom / winnow 依然是非常好的选择;但如果是做 语言前端 / LSP,一般会更推荐 Chumsky 这种更偏“语言工具链”的库。

LALRPOP:LR 风格的文法生成器

如果你更习惯 Yacc/Bison 风味,而又希望使用 Rust,LALRPOP 是值得认真看一眼的。

Rowan 与基于事件的增量解析

核心思想大致是:

这套东西本身并不是一个通用解析库,而更像“解析完成之后,怎么把结果组织得对 IDE 友好”。

在实践中,可以考虑:

这类架构适合你打算做一个“有 IDE 等级支持”的语言。

Tree-sitter 以及 Rust 绑定

对 Rust 来说,更常见的用法是:

如果你的目标是“我有一门语言,想让它在现有编辑器表现得像一级公民”,Tree-sitter 是一个非常务实的选项。

Logos:只做 Token 的快速词法器

通常的组合是:

如果你对语法层已经比较满意,只是想把词法阶段从手写状态里解放出来,可以单独考虑 Logos。

选型建议:你到底该用哪个?

可以根据自己的场景反推:

更现实一点的答案是:真正的项目往往会“混搭”这些工具

参考资料


建议修改

下一篇
Egglog 快速入门(三):复数定义与内置类型