自定义解析器2
2019 年 08 月 30 日
上一篇只讲了基础概念,这一篇我来用lalrpop实际解析一些东西。
我们假设要解析这样的字符串:“a: str”,这是一个类似变量定义的字符串,冒号前是变量名,冒号后是类型。对于类型,我们定义一些基础类型,但我们也允许自定义类型。
首先我们分析一下这类字符串的BNF,应该是类似这样的形式(简化后):
<variable> ::= <variable name> ":" <variable type>
<variable name> ::= <word>
<variable type> ::= "int" | "str" | <word>
<word> ::= \[A-Za-z\]\[A-Za-z0-9\]*
这里说明几点:
- 这里的
<word>
是正则表达式,表示以字母开头,后续是字母和数字的变量名。注意这里我没允许下划线等等特殊字符,纯粹是为了正则表达式的简单化。 <variable type>
除了保留的类型,也允许自定义类型。
首先可以把BNF用lalrpop的格式写到一个文件中。这里仅做部分示例,实际写的时候还需要参考lalrpop文档:
// DataType和Variable是ast.rs中定义的数据结构
use crate::ast::{DataType, Variable};
// 从这一句开始,下面都是lalrpop语法
grammar;
// 由于int和str也符合<word>,因此用match将其优先级提高,避免其被解析成<word>
match {
"int",
"str",
} else {
_,
}
// DataType是非终结符,输出是rust中的自定义的DataType格式,是个enum
// 如果匹配到<word>,那么使用特定的enum携带参数
DataType: DataType = {
"int" => DataType::Int,
"str" => DataType::Str,
<t:Word> => DataType::Custom(t),
};
// Word是非终结符,输出是rust中的String格式
// =>箭头符号前面的r""意思是使用正则表达式来匹配,格式按照rust的regex库
Word: String = {
<r"[[:alpha:]][[:alnum:]]*"> => String::from(<>)
};
// Variable是最终我们要解析的数据结构
pub Variable: Variable = {
<name:Word> ":" <data_type: DataType> => Variable {
name: name,
data_type: data_type,
},
};
跟上面文件对应的ast.rs应该这么写:
#[derive(Debug)]
pub enum DataType {
Int,
Str,
Custom(String),
}
#[derive(Debug)]
pub struct Variable {
pub name: String,
pub data_type: DataType,
}
假设上面的文件名是var.sidl,那么main.rs需要这么写
#[macro_use]
extern crate lalrpop_util;
lalrpop_mod!(pub var); // synthesized by LALRPOP
mod ast;
fn main() {
let my_str = r#"varname: vartype"#;
let ret = var::VariableParser::new().parse(my_str).unwrap();
println!("result = {:?}", ret);
}
当然,还有build.rs要参考lalrpop文档,这样就可以把目标字符串解析成Variable结构体。
如果是多行文本需要解析,并且带上comment支持,那么需要调整BNF定义,比如可以做成这样:
use crate::ast::{DataType, Variable};
grammar;
// 注释的优先级需要高一些
match {
r"//.*",
}
// 保留字次之
else {
"int",
"str",
} else {
_,
}
DataType: DataType = {
"int" => DataType::Int,
"str" => DataType::Str,
<t:Word> => DataType::Custom(t),
};
Word: String = {
<r"[[:alpha:]][[:alnum:]]*"> => String::from(<>)
};
// 单独定义Comment
Comment: () = {
<r"//.*"> => ()
}
// 注意Variable现在不需要加pub了,因为我们不用它来解析多行文本
Variable: Variable = {
<name:Word> ":" <data_type: DataType> => Variable {
name: name,
data_type: data_type,
},
};
// 定义了一个Variables表示复数个Variable,注意最后的s
pub Variables: Vec<Variable> = {
<v:Variable> => {
let mut vars:Vec<Variable> = Vec::new();
vars.push(v);
vars
},
<v:Variable> <mut vars:Variables> => {
vars.push(v);
vars
},
// 吃掉Comment
<c:Comment> <vars:Variables> => {
vars
},
}
这样就可以解析多行的变量定义了,并且可以加入//开头的注释。
当然,还有一个更简单的办法,在parse之前加入预处理,用正则表达式的方式,直接把注释删掉也可以。
extern crate regex;
use regex::Regex;
// Delete comments from content
let re = Regex::new(r"//.*").unwrap();
let dehydrated = re.replace_all(content.as_ref(), "");
据我理解,现在这样的做法,对于类似的属性文件格式应该可以处理了。当然解析完成后还有校验问题,那时就要通过拿到的ast定义的数据结构进行分析了。