Cliff的工作室
自定义解析器2

上一篇只讲了基础概念,这一篇我来用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\]*

这里说明几点:

  1. 这里的<word>是正则表达式,表示以字母开头,后续是字母和数字的变量名。注意这里我没允许下划线等等特殊字符,纯粹是为了正则表达式的简单化。
  2. <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定义的数据结构进行分析了。

2019 年 08 月 30 日 21 时 29 分