#ruby #mri #parse #lib #interface #byte #tokens

bin+lib lib-ruby-parser

Ruby 解析器

40 个版本 (20 个稳定版)

4.0.6+ruby-3.1.22024年4月2日
4.0.5+ruby-3.1.22023年7月20日
4.0.4+ruby-3.1.12022年10月17日
4.0.3+ruby-3.1.12022年6月21日
0.7.0 2020年11月19日

#71解析器实现

Download history 356/week @ 2024-04-20 252/week @ 2024-04-27 265/week @ 2024-05-04 290/week @ 2024-05-11 531/week @ 2024-05-18 943/week @ 2024-05-25 418/week @ 2024-06-01 513/week @ 2024-06-08 419/week @ 2024-06-15 315/week @ 2024-06-22 99/week @ 2024-06-29 280/week @ 2024-07-06 331/week @ 2024-07-13 846/week @ 2024-07-20 809/week @ 2024-07-27 726/week @ 2024-08-03

2,761 每月下载量
用于 6 个包 (4 个直接使用)

MIT 许可

1MB
25K SLoC

lib-ruby-parser

test unsafe forbidden Crates.io codecov MIT Licence dependency status Docs

lib-ruby-parser 是用 Rust 编写的 Ruby 解析器。

基本用法

use lib_ruby_parser::{Parser, ParserOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let options = ParserOptions {
        buffer_name: "(eval)".to_string(),
        ..Default::default()
    };
    let mut parser = Parser::new(b"2 + 2".to_vec(), options);

    println!("{:#?}", parser.do_parse());

    Ok(())
}

完整文档

特性

简单来说;它速度快,精度高,并且界面美观。

Ripper/RubyVM::AST 的比较

  1. 它基于 MRI 的 parse.y,因此返回 完全相同 的令牌序列。
  2. 它已在下载量最高的 300 个 gem(约 4M 行代码)上进行了测试,以及 rubyspecruby/ruby 仓库上进行了测试,并且与 Ripper.lex 没有差异。
  3. 它比 Ripper 快 5 倍左右,Ripper 解析 4M 行代码需要约 24 秒,而 lib-ruby-parser 需要约 4.5 秒。即约为 950K 行代码/秒。您可以在 bench/ 目录中找到基准测试,它们不包括任何 I/O 或 GC。
  4. 它有一个非常好的接口。AST 是强类型的并且有良好的文档。
  5. 它不会丢弃关于令牌的信息。所有节点都有关于它们源位置的信息。

whitequark/parser 的比较

  1. 它快得多(在相同的机器上,4M 行代码的语料库可以在 245 秒内被解析)
  2. 它有一个非常相似的接口(从 AST 结构和错误报告方面来说)
  3. 然而,AST 是强类型的,因此如果某个东西是可空的,则它将被明确定义和记录。
  4. 重要的是,它不依赖于 Ruby

测试语料库有 4,176,379 行代码和 170,114,575 字节,因此在我本地机器上的近似解析速度为

解析器 总时间 每秒字节 每秒行数
lib-ruby-parser ~4.4s ~38,000,000 ~950,000
ripper ~24秒 ~7,000,000 ~175,000
whitequark/parser ~245秒 ~700,000 ~17,000

语法版本管理

lib-ruby-parser 遵循 MRI/master。目前没有计划支持像 whitequark/parser 那样支持多个版本的方案。

库版本管理

Ruby版本 lib-ruby-parser版本
3.0.0 3.0.0+
3.1.0 4.0.0+ruby-3.1.0

4.0.0开始,lib-ruby-parser遵循SemVer。基本版本根据API更改而增加,而元数据与当前Ruby版本相匹配,即 X.Y.Z+ruby-A.B.C 表示

  • X.Y.Z 基础版本
  • 它解析Ruby A.B.C

两个版本分别增加。

编码

默认情况下,lib-ruby-parser 只能解析编码为 UTF-8ASCII-8BIT/BINARY 的源文件。

可以在ParserOptions中传递一个decoder函数,该函数接收由库识别的编码和字节数组。它必须返回UTF-8编码的字节数组或错误

use lib_ruby_parser::source::{InputError, Decoder, DecoderResult};
use lib_ruby_parser::{Parser, ParserOptions, ParserResult, LocExt};

fn decode(encoding: String, input: Vec<u8>) -> DecoderResult {
    if "US-ASCII" == encoding.to_uppercase() {
        // reencode and return Ok(result)
        return DecoderResult::Ok(b"# encoding: us-ascii\ndecoded".to_vec());
    }
    DecoderResult::Err(InputError::DecodingError(
        "only us-ascii is supported".to_string(),
    ))
}

let options = ParserOptions {
    decoder: Some(Decoder::new(Box::new(decode))),
    ..Default::default()
};
let mut parser = Parser::new(b"# encoding: us-ascii\n3 + 3".to_vec(), options);
let ParserResult { ast, input, .. } = parser.do_parse();

assert_eq!(ast.unwrap().expression().source(&input).unwrap(), "decoded".to_string())

无效字符串值

Ruby不要求字符串字面量在它们的编码中有效。这就是为什么以下代码是有效的

# encoding: utf-8

"\xFF"

UTF-8中字节序列 255 是无效的,但MRI忽略了它。

但并非所有语言都支持它,这就是为什么字符串和符号节点封装了一个自定义的 StringValue 而不是普通的 String

如果你的语言支持无效字符串,你可以使用这个StringValue的原始 .bytes。例如,这个库的Ruby包装器可以这样做。

如果你的语言不支持它,最好调用 .to_string_lossy(),该函数将所有不受支持的字符替换为特殊的 U+FFFD REPLACEMENT CHARACTER ()

正则表达式

Ruby在解析过程中从字面量构建正则表达式以

  1. 验证它们
  2. 如果正则表达式用于匹配并且包含命名捕获,则声明局部变量

为了反映这种行为,lib-ruby-parser 使用 Onigurama 编译、验证和解析正则表达式字面量。

默认情况下,此功能是禁用的,但您可以通过启用 "onig" 功能来添加它。

Bison

lib-ruby-parser 的语法是使用一个为该项目编写的自定义 Bison框架 构建的。

对于开发,您需要在本地安装Bison的最新版本。当然,对于crates.io的发布构建不是必需的(因为编译的 parser.rs 已包含在发布构建中,并且 build.rs 转换它的代码已被排除)。

如果您直接从GitHub使用它,还需要Bison(因为 parser.rs 在gitignore中)

其他语言的绑定

性能分析

您可以使用 parse 示例

$ cargo run --bin parse --features=bin-parse -- --print=N --run-profiler --glob "blob/**/*.rb"

基准测试

使用 download.rb 脚本可以生成 4M 行代码的代码库

$ ruby gems/download.rb

然后,运行一个比较 Ripperlib-ruby-parser 的脚本(附带的成果来自 2024 年 3 月)

$ ./scripts/bench.sh
Running lib-ruby-parser
Run 1:
Time taken: 4.4287733330 (total files: 17895)
Run 2:
Time taken: 4.4292764170 (total files: 17895)
Run 3:
Time taken: 4.4460961250 (total files: 17895)
Run 4:
Time taken: 4.4284508330 (total files: 17895)
Run 5:
Time taken: 4.4695665830 (total files: 17895)
--------
Running MRI/ripper
Run 1:
Time taken: 24.790103999897838 (total files: 17894)
Run 2:
Time taken: 23.145863000303507 (total files: 17894)
Run 3:
Time taken: 25.50493900012225 (total files: 17894)
Run 4:
Time taken: 24.570900999940932 (total files: 17894)
Run 5:
Time taken: 26.0963700003922 (total files: 17894)

模糊测试

首先,请确保切换到夜间模式

$ rustup default nightly

然后安装 cargo-fuzz

$ cargo install cargo-fuzz

并运行模糊器(根据需要更改 --jobs 的数量或删除它以仅运行一个并行进程)

$ RUST_BACKTRACE=1 cargo fuzz run parse --jobs=8 -- -max_len=50

依赖关系

~0.5–11MB
~133K SLoC