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 解析器工具
5,191 每月下载量
140KB
2K SLoC
RustyLR
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-hash
的FxHashMap
。
示例
// 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)
- 此错误信息是由构建脚本工具生成的,而不是过程宏。
特性
- 纯Rust实现
- 可读错误信息,包括语法构建和解析
- 从CFGs编译时构建DFA
- 可自定义的还原动作
- 解决歧义语法的冲突
- 部分支持正则表达式模式
- 与
build.rs
集成的工具
内容
过程宏
以下提供了以下过程宏
lr1!
: 生成LR(1)解析器lalr1!
: 生成LALR(1)解析器
这些宏将生成结构体
Parser
: 包含DFA表和产生式规则ParseError
:Error
的类型别名,从feed()
返回Context
: 包含当前状态和数据栈枚举 NonTerminals
: 非终结符号列表Rule
: 产生式规则的类型别名State
: DFA状态类型别名
上述所有结构体都以 <StartSymbol>
为前缀。在大多数情况下,你需要的可能是 Parser
和 ParseError
结构体,而其他则是内部使用。
与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动作可能被多次调用,即使结果在将来会被丢弃。
- 每个
RuleType
和Term
都必须实现Clone
特质。
- 每个
- 用户必须注意shift/reduce或reduce/reduce冲突发生的位置。每当解析器发生分歧,计算成本都会增加。
语法
要开始编写上下文无关文法,首先需要定义必要的指令。这是过程宏的语法。
lr1! {
// %directives
// %directives
// ...
// %directives
// NonTerminalSymbol(RuleType): ProductionRules
// NonTerminalSymbol(RuleType): ProductionRules
// ...
}
lr1!
宏将生成具有LR(1)有向图表的解析器结构。如果您想生成LALR(1)解析器,请使用lalr1!
宏。宏中的每一行都必须遵循以下语法。
引导,扩展引导是理解语法和生成代码的好例子。这是用RustyLR本身编写的RustyLR语法解析器。
快速参考
- 产生式规则
- 正则表达式模式
- 规则类型
- Reduce动作
- 在Reduce动作中访问令牌数据
- 感叹号
!
%tokentype
%令牌
%起始
%eof
%用户数据
%left
,%right
%err
,%error
%derive
%derive
%glr
产生式规则
每个产生式规则都有基本形式
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 / term
、P / [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>
您仍然可以通过使用模式的基名来访问 Vec
或 Option
。
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
不实现任何特质。但在某些情况下,您可能希望派生特质,如 Clone
、Debug
或 Serialize
、Deserialize
(来自 serde
)。
在这种情况下,用户必须确保 Context
的每个成员都实现了该特质。目前,Context
保留堆栈数据,这是状态堆栈的 Vec<usize>
,以及语法中每个 RuleType
的 Vec<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