#lalr-parser #context-free-grammar #lr #compiler #error-message #bison #proc-macro

rusty_lr

具有自定义还原动作的GLR、LR(1)和LALR(1)解析器生成器

51个版本 (22个稳定版本)

新版本 2.5.0 2024年8月24日
1.6.1 2024年8月13日
0.12.3 2024年8月7日
0.5.2 2024年7月29日

#44 in 解析器工具

Download history 141/week @ 2024-07-12 723/week @ 2024-07-19 535/week @ 2024-07-26 2112/week @ 2024-08-02 1307/week @ 2024-08-09 1147/week @ 2024-08-16

5,191 每月下载量

MIT 许可证

140KB
2K SLoC

RustyLR

crates.io docs.rs

Rust的GLR、LR(1)和LALR(1)解析器生成器。

RustyLR提供了过程宏构建脚本工具来生成GLR、LR(1)和LALR(1)解析器。生成的解析器将是纯Rust代码,DFA的计算将在编译时完成。还原动作可以编写在Rust中,错误信息是可读和详细的。对于大型和复杂的语法,建议使用构建脚本

features in Cargo.toml

  • build : 启用构建脚本工具。
  • fxhash : 在解析器表中,将 std::collections::HashMap 替换为来自 rustc-hashFxHashMap

示例

// this define `EParser` struct
// where `E` is the start symbol
lr1! {
    %userdata i32;           // userdata type
    %tokentype char;         // token type
    %start E;                // start symbol
    %eof '\0';               // eof token

    // token definition
    %token zero '0';
    %token one '1';
    %token two '2';
    %token three '3';
    %token four '4';
    %token five '5';
    %token six '6';
    %token seven '7';
    %token eight '8';
    %token nine '9';
    %token plus '+';
    %token star '*';
    %token lparen '(';
    %token rparen ')';
    %token space ' ';

    // conflict resolving
    %left [plus star];                  // reduce first for token 'plus', 'star'

    // context-free grammars
    Digit(char): [zero-nine];           // character set '0' to '9'

    Number(i32)                         // type assigned to production rule `Number`
        : space* Digit+ space*          // regex pattern
    { Digit.into_iter().collect::<String>().parse().unwrap() }; 
    //    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this will be the value of `Number`
                                        // reduce action written in Rust code

    A(f32): A plus a2=A {
        *data += 1;                     // access userdata by `data`
        println!( "{:?} {:?} {:?}", A, plus, a2 );
        A + a2
    }
        | M
        ;

    M(f32): M star m2=M { M * m2 }
        | P
        ;

    P(f32): Number { Number as f32 }
        | space* lparen E rparen space* { E }
        ;

    E(f32) : A ;
}
let parser = EParser::new();         // generate `EParser`
let mut context = parser.begin();    // create context
let mut userdata: i32 = 0;           // define userdata

let input_sequence = "1 + 2 * ( 3 + 4 )";

// start feeding tokens
for token in input_sequence.chars() {
    match parser.feed(&mut context, token, &mut userdata) {
        //                          ^^^^^   ^^^^^^^^^^^^ userdata passed here as `&mut i32`
        //                          feed token
        Ok(_) => {}
        Err(e) => {
            match e {
                EParseError::InvalidTerminal(invalid_terminal) => {
                    ...
                }
                EParseError::ReduceAction(error_from_reduce_action) => {
                    ...
                }
            }
            println!("{}", e);
            // println!( "{}", e.long_message( &parser, &context ) );
            return;
        }
    }
}
parser.feed(&mut context, '\0', &mut userdata).unwrap();    // feed `eof` token

let res = context.accept();   // get the value of start symbol
println!("{}", res);
println!("userdata: {}", userdata);

可读错误信息(带有 codespan

images/error1.png images/error2.png

  • 此错误信息是由构建脚本工具生成的,而不是过程宏。

特性

  • 纯Rust实现
  • 可读错误信息,包括语法构建和解析
  • 从CFGs编译时构建DFA
  • 可自定义的还原动作
  • 解决歧义语法的冲突
  • 部分支持正则表达式模式
  • build.rs集成的工具

内容

过程宏

以下提供了以下过程宏

  • lr1! : 生成LR(1)解析器
  • lalr1! : 生成LALR(1)解析器

这些宏将生成结构体

  • Parser : 包含DFA表和产生式规则
  • ParseError : Error的类型别名,从feed()返回
  • Context : 包含当前状态和数据栈
  • 枚举 NonTerminals : 非终结符号列表
  • Rule : 产生式规则的类型别名
  • State : DFA状态类型别名

上述所有结构体都以 <StartSymbol> 为前缀。在大多数情况下,你需要的可能是 ParserParseError 结构体,而其他则是内部使用。

build.rs集成

此构建脚本工具将提供比过程宏更详细、格式化的错误消息。如果你正在编写一个庞大、复杂的语法,建议使用构建脚本而不是过程宏。生成的代码将包含与过程宏相同的结构和函数。在你的实际源代码中,你可以 include! 生成的文件。

与过程宏不同,程序在输入文件中搜索 %%,而不是 lr1!lalr1! 宏。在 %% 之前的内容将原样复制到输出文件中。上下文无关语法必须跟随 %%

// parser.rs
use some_crate::some_module::SomeStruct;

enum SomeTypeDef {
    A,
    B,
    C,
}

%% // <-- input file splitted here

%tokentype u8;
%start E;
%eof b'\0';

%token a b'a';
%token lparen b'(';
%token rparen b')';

E: lparen E rparen
 | P
 ;

P: a;

你必须启用 build 功能才能在构建脚本中使用。

[build-dependencies]
rusty_lr = { version = "...", features = ["build"] }
// build.rs
use rusty_lr::build;

fn main() {
    println!("cargo::rerun-if-changed=src/parser.rs");

    let output = format!("{}/parser.rs", std::env::var("OUT_DIR").unwrap());
    build::Builder::new()
        .file("src/parser.rs") // path to the input file
    //  .lalr()                // to generate LALR(1) parser
        .build(&output);       // path to the output file
}

在你的源代码中,包含生成的文件。

include!(concat!(env!("OUT_DIR"), "/parser.rs"));

开始解析

Parser 结构体有以下函数

  • new() : 创建新的解析器
  • begin(&self) : 创建新的上下文
  • feed(&self, &mut Context, TerminalType, &mut UserData) -> Result<(), ParseError> : 将标记馈送到解析器

请注意,如果未定义 %userdata,则省略了参数 &mut UserData。你所需要做的就是调用 new() 来生成解析器,并调用 begin() 来创建上下文。然后,你可以使用 feed() 函数逐个输入序列。一旦输入序列被馈送(包括 eof 标记),如果没有错误,你可以通过调用 context.accept() 获取起始符号的值。

let parser = Parser::new();
let context = parser.begin();
for token in input_sequence {
    match parser.feed(&context, token) {
        Ok(_) => {}
        Err(e) => { // e: ParseError
            println!("{}", e);
            return;
        }
    }
}
let start_symbol_value = context.accept();

GLR解析器

可以通过语法中的 %glr; 指令生成 GLR(广义 LR 解析器)。

// generate GLR parser;
// from now on, shift/reduce, reduce/reduce conflicts will not be treated as errors
%glr; 
...

GLR 解析器可以处理 LR(1) 或 LALR(1) 解析器无法处理的歧义语法。在解析过程中遇到任何类型的冲突时,解析器将进入多个状态,并尝试所有路径直到失败。当然,在解析结束时(你在其中馈送 eof 标记的点)必须留下唯一的路径。

解决歧义

您可以通过reduce动作来解决歧义。简单地,从reduce动作返回Result::Err(Error)将撤销当前路径。可以通过%err指令定义Error变体类型。

关于GLR解析器的说明

  • 仍在开发中,尚未经过充分测试(欢迎提交补丁!)。
  • 由于存在多个路径,reduce动作可能被多次调用,即使结果在将来会被丢弃。
    • 每个RuleTypeTerm都必须实现Clone特质。
  • 用户必须注意shift/reduce或reduce/reduce冲突发生的位置。每当解析器发生分歧,计算成本都会增加。

语法

要开始编写上下文无关文法,首先需要定义必要的指令。这是过程宏的语法。

lr1! {
// %directives
// %directives
// ...
// %directives

// NonTerminalSymbol(RuleType): ProductionRules
// NonTerminalSymbol(RuleType): ProductionRules
// ...
}

lr1!宏将生成具有LR(1)有向图表的解析器结构。如果您想生成LALR(1)解析器,请使用lalr1!宏。宏中的每一行都必须遵循以下语法。

引导扩展引导是理解语法和生成代码的好例子。这是用RustyLR本身编写的RustyLR语法解析器。

快速参考


产生式规则

每个产生式规则都有基本形式

NonTerminalName
    : Pattern1 Pattern2 ... PatternN { ReduceAction }
    | Pattern1 Pattern2 ... PatternN { ReduceAction }
   ...
    ;

每个Pattern遵循以下语法

  • name:语法中定义的非终结符或终结符符号name
  • [term1 term_start-term_last][^term1 term_start-term_last]:终结符符号的集合。eof将自动从终结符集合中移除。
  • P*:零次或多次重复P
  • P+:一次或多次重复P
  • P?:零次或一次重复P
  • (P1 P2 P3):模式的分组。
  • P / termP / [term1 term_start-term_last]P / [^term1 term_start-term_last]:前瞻;P后面跟着给定的终结符集中的一项。前瞻不会被消耗。

注意

当使用范围模式 [first-last] 时,范围是由 %token 指令的顺序构建的,而不是由实际值构建的。如果您按以下顺序定义令牌

%token one '1';
%token two '2';
...
%token zero '0';
%token nine '9';

范围 [zero-nine] 将是 ['0', '9'],而不是 ['0'-'9']


RuleType (可选)

可以为每个非终结符号分配一个值。在 reduce action 中,您可以访问每个模式持有的值,并可以分配新值给当前的符号。请参阅下面的 ReduceAction在 ReduceAction 中访问令牌数据 部分。解析结束时,起始符号的值将是解析的结果。默认情况下,终结符号保留通过 feed() 函数传递的 %tokentype 的值。

struct MyType<T> {
    ...
}
E(MyType<i32>) : ... Patterns ... { <This will be new value of E> } ;

ReduceAction (可选)

reduce action 可以用 Rust 代码编写。在规则匹配并归约时执行。

  • 如果为当前非终结符号定义了 RuleType,则 ReduceAction 本身必须是 RuleType 的值(即语句末尾没有分号)。

  • 如果

    • RuleType 未定义,则可以省略 ReduceAction
    • 生产规则中只保留一个令牌的值。
  • Result<(),Error> 可以从 ReduceAction 返回。

    • 返回的 Error 将传递给 feed() 函数的调用者。
    • ErrorType 可以通过 %err%error 指令定义。请参阅 错误类型 部分。
NoRuleType: ... ;

RuleTypeI32(i32): ... { 0 } ;

// RuleTypeI32 will be chosen
E(i32): NoRuleType NoRuleType RuleTypeI32 NoRuleType;
// set Err variant type to String
%err String;

%token div '/';

E(i32): A div a2=A {
    if a2 == 0 {
        return Err("Division by zero".to_string());
    }

    A / a2
};

A(i32): ... ;

在Reduce动作中访问令牌数据

可以在 ReduceAction 中使用 预定义变量

  • data&mut UserData):传递给 feed() 函数的用户数据。
  • lookahead ( &Term ) : 产生规约动作的预查标记。

要访问每个标记的数据,您可以直接使用标记的名称作为变量。

  • 对于非终结符号,变量的类型是 RuleType
  • 对于终结符号,变量的类型是 %tokentype
  • 如果定义了多个同名的变量,则最前面的变量将被使用。
  • 您可以使用 = 操作符重新映射变量名。
E(i32) : A plus a2=A {
    println!("Value of A: {:?}", A);
    println!("Value of plus: {:?}", plus);
    println!("Value of a2: {:?}", a2);

    A + a2 // new value of E
};

对于某些正则表达式模式,变量的类型将按以下方式修改

  • P* : Vec<P>
  • P+ : Vec<P>
  • P? : Option<P>

您仍然可以通过使用模式的基名来访问 VecOption

E(i32) : A* {
    println!( "Value of A: {:?}", A ); // Vec<A>
};

对于终结集合 [term1 term_start-term_end][^term1 term_start-term_end],没有预定义的变量名。您必须显式定义变量名。

E: digit=[zero-nine] {
    println!( "Value of digit: {:?}", digit ); // %tokentype
};

对于组 (P1 P2 P3)

  • 如果没有任何模式持有值,则该组本身将不持有任何值。
  • 如果只有一个模式持有值,则该组将持有该模式的值。变量名将与模式相同。(即如果 P1 持有值,而其他都不持有,则 (P1 P2 P3) 将持有 P1 的值,并通过名称 P1 访问)
  • 如果有多个模式持有值,则该组将持有值的 Tuple。组没有默认变量名,您必须通过 = 操作符显式定义变量名。
NoRuleType: ... ;

I(i32): ... ;

// I will be chosen
A: (NoRuleType I NoRuleType) {
    println!( "Value of I: {:?}", I ); // can access by 'I'
    I
};

// ( i32, i32 )
B: i2=( I NoRuleType I ) {
    println!( "Value of I: {:?}", i2 ); // must explicitly define the variable name
};


感叹号!

感叹号 ! 可以直接跟在标记后,以忽略标记的值。标记将被视为不持有任何值。

A(i32) : ... ;

// A in the middle will be chosen, since other A's are ignored
E(i32) : A! A A!;

标记类型 (必须定义)

%tokentype <RustType> ;

定义终结符号的类型。 <RustType> 必须在调用宏的点上是可访问的。

enum MyTokenType<Generic> {
    Digit,
    Ident,
    ...
    VariantWithGeneric<Generic>
}

lr! {
...
%tokentype MyTokenType<i32>;
}

标记定义 (必须定义)

%token name <RustExpr> ;

将终结符号 name 映射到实际值 <RustExpr><RustExpr> 必须在调用宏的点上是可访问的。

%tokentype u8;

%token zero b'0';
%token one b'1';

...

// 'zero' and 'one' will be replaced by b'0' and b'1' respectively
E: zero one;

开始符号 (必须定义)

%start NonTerminalName ;

将语法的开始符号设置为 NonTerminalName

%start E;
// this internally generate augmented rule <Augmented> -> E eof

E: ... ;

EOF 符号 (必须定义)

%eof <RustExpr> ;

定义终结符 eof。必须确保在宏调用处可以访问 <RustExpr>。'eof' 终结符将被自动添加到语法中。

%eof b'\0';
// you can access eof terminal symbol by 'eof' in the grammar
// without %token eof ...;

用户数据类型 (可选)

%userdata <RustType> ;

定义传递给 feed() 函数的用户数据类型。

struct MyUserData { ... }

...

%userdata MyUserData;

...

fn main() {
    ...
    let mut userdata = MyUserData { ... };
    parser.feed( ..., token, &mut userdata); // <-- userdata feed here
}

减少类型 (可选)

// reduce first
%left term1 ;
%left [term1 term_start-term_last] ;

// shift first
%right term1 ;
%right [term1 term_start-term_last] ;

设置终结符的移位/减少优先级。 %left 可以缩写为 %reduce%l,而 %right 可以缩写为 %shift%r

// define tokens
%token plus '+';
%token hat '^';


// reduce first for token 'plus'
%left plus;

// shift first for token 'hat'
%right hat;

错误类型 (可选)

%err <RustType> ;
%error <RustType> ;

定义从 ReduceAction 返回的 Err 变量的类型。如果没有定义,将使用 DefaultReduceActionError

enum MyErrorType<T> {
    ErrVar1,
    ErrVar2,
    ErrVar3(T),
}

...


%err MyErrorType<GenericType> ;

...

match parser.feed( ... ) {
    Ok(_) => {}
    Err(err) => {
        match err {
            ParseError::ReduceAction( err ) => {
                // do something with err
            }
            _ => {}
        }
    }
}

派生 (可选)

指定生成 Context 结构体的派生属性。默认情况下,生成的 Context 不实现任何特质。但在某些情况下,您可能希望派生特质,如 CloneDebugSerializeDeserialize(来自 serde)。

在这种情况下,用户必须确保 Context 的每个成员都实现了该特质。目前,Context 保留堆栈数据,这是状态堆栈的 Vec<usize>,以及语法中每个 RuleTypeVec<T>

%derive Clone, Debug, serde::Serialize ;
// here, #[derive(Clone,Debug)] will be added to the generated `Context` struct
%derive Clone, Debug;

...

let mut context = parser.begin();
// do something with context...

println!( "{:?}", context );          // debug-print context
let cloned_context = context.clone(); // clone context, you can re-feed the input sequence using cloned context

GLR 解析器生成

%glr;

切换到 GLR 解析器生成。

如果您想生成 GLR 解析器,请在语法中添加 %glr; 指令。使用此指令,任何移位/减少、减少/减少冲突都不会被视为错误。

有关详细信息,请参阅 GLR 解析器 部分。

依赖项

~0.3–7.5MB
~43K SLoC