#traits #parser #macro #interpreter #type #file-info

nightly 可解析的

一个易于解析数据结构的特性

3 个版本

0.1.2 2022年5月24日
0.1.1 2022年5月24日
0.1.0 2022年5月23日

1071数据结构

每月 24 次下载

MIT 协议

34KB
647

安装

[dependencies]
parsable = "0.1"

示例

实现一个仅适用于正整数且不包含操作符优先级的简单操作解释器。

use parsable::{parsable, Parsable, ParseOptions};

#[parsable]
struct Operation {
    first_operand: Operand,
    other_operands: Vec<(Operator, Operand)>
}

impl Operation {
    fn process(&self) -> i32 {
        let mut result = self.first_operand.process();

        for (operator, operand) in &self.other_operands {
            let value = operand.process();

            result = match operator {
                Operator::Plus => result + value,
                Operator::Minus => result - value,
                Operator::Mult => result * value,
                Operator::Div => result / value,
                Operator::Mod => result % value,
            }
        }

        result
    }
}

#[parsable]
enum Operand {
    Number(NumberLiteral),
    Wrapped(WrappedOperation)
}

impl Operand {
    fn process(&self) -> i32 {
        match self {
            Operand::Number(number) => number.process(),
            Operand::Wrapped(wrapped) => wrapped.process(),
        }
    }
}

#[parsable]
struct NumberLiteral {
    #[parsable(regex=r"\d+")]
    value: String
}

impl NumberLiteral {
    fn process(&self) -> i32 {
        self.value.parse().unwrap()
    }
}

#[parsable]
struct WrappedOperation {
    #[parsable(brackets="()")]
    operation: Box<Operation>
}

impl WrappedOperation {
    fn process(&self) -> i32 {
        self.operation.process()
    }
}

#[parsable]
enum Operator {
    Plus = "+",
    Minus = "-",
    Mult = "*",
    Div = "/",
    Mod = "%"
}

fn main() {
    let operation_string = "3 + (4 * 5)".to_string();
    let parse_options = ParseOptions::default();
    
    match Operation::parse(operation_string, parse_options) {
        Ok(operation) => {
            println!("result: {}", operation.process());
        },
        Err(error) => {
            dbg!(error);
        }
    }
}

#[parsable]

使用 #[parsable] 宏标记结构体或枚举将实现项目的 Parsable 特性,条件是所有字段也必须实现 Parsable 特性。它还可以用于字段以调整其解析方式。

结构体

  • 所有字段逐个解析。只有当所有字段都成功解析时,解析才会成功。

枚举

  • 解析会在成功解析的第一个变体上停止。
  • 如果变体包含多个字段,它们将依次解析,并且必须全部成功才能匹配变体。
  • 如果变体不包含字段,则必须指定一个字符串以指示如何解析它。
#[parsable]
enum MyOperation {
    BinaryOperation(NumerLiteral, Operator, NumerLiteral),
    Number(NumberLiteral),
    Zero = "zero"
}

// If the first two variants are swapped, then the parsing will never reach the `SimpleOperation` variant

内置类型

字符串

字符串字段必须使用 #[parsable(regex="<pattern>")]#[parsable(value="<pattern>")] 宏选项指定如何解析。

// Matches at least one digit
#[parsable]
struct NumberLiteral {
    #[parsable(regex=r"\d+")]
    value: String
}

#[parsable]
// Only matches the string "+"
struct PlusSign {
    #[parsable(value="+")]
    value: String
}

可选<T>

匹配 T。如果失败,则返回 None,但字段的解析仍然被认为是成功的。

#[parsable]
enum Sign {
    Plus = "+",
    Minus = "-"
}

// Matches a number with an optional sign
#[parsable]
struct NumberLiteral {
    sign: Option<Sign>,
    #[parsable(regex=r"\d+")]
    value: String
}

Vec<T>

尽可能多地连续匹配 T。以下选项可以指定:

  • min=X:只有解析至少 X 项时解析才有效。
  • separator=<string>:在每一项之后,解析器将尝试消费分隔符。如果没有找到分隔符,则解析失败。
// Matches a non-empty list of numbers separated by a comma
#[parsable]
struct NumberList {
    #[parsable(separator=",", min=1)]
    numbers: Vec<NumberLiteral>
}

其他类型

  • ():不匹配任何内容,总是成功。
  • (T, U):先匹配 T,然后匹配 U
  • Box<T>:匹配 T

运行解析器

Parsable 特性提供了一个 parse() 方法,它接受两个参数

  • content: String:要解析的字符串
  • options: ParseOptions:解析选项

ParseOptions 类型有以下字段

  • comment_start: Option<&'static str>:当匹配到指定的模式时,忽略该行其余部分。常见的实例是 "//""#"
  • file_path: Option<String>:正在解析的字符串的文件路径。
  • package_root_path: Option<String>:包含正在解析的文件的包或模块的根路径。

字段 file_pathpackage_root_path 被转发到 FileInfo 结构中,并且库实际上从不使用它们。

在解析过程中始终忽略空白字符(空格、换行和制表符)。

FileInfo

FileInfo 结构在整个库中都被使用。它有以下字段

  • content: String:正在解析的字符串
  • path: String:正在解析的文件的路径,如 ParseOptions 中指定
  • package_root_path: String:包含正在解析的文件的包的路径,如 ParseOptions 中指定

它还提供了以下方法

  • get_line_col(index: usize) -> Option<(usize, usize)>: 返回与指定字符索引相关联的行和列号(从1开始)。此方法假设每个字符占用一个字节,因此当文件包含非ASCII字符时不能正常工作。

ItemLocation

使用 #[parsable] 标注结构体将在其中添加类型为 ItemLocationlocation 字段,并具有以下字段和方法

  • file: Rc<FileInfo>: 包含项目的文件信息
  • start: usize: 项在文件中的起始索引
  • end: usize: 项在文件中的结束索引
  • get_start_line_col() -> (usize, usize): 获取位置起始的行和列号(从1开始)

Parsable 特性还提供了一个结构体上的 location() 方法

  • 枚举上的 location() 方法返回其 location 字段
  • 枚举上的 location() 方法返回匹配的变体的 location() 方法
  • 在没有任何字段的变体上调用 location() 会引发恐慌

为了防止恐慌,可以将具有单元变体的枚举封装在结构体中

#[parsable]
enum Operator {
    Plus = "+",
    Minus = "-",
    Mult = "*",
    Div = "/",
    Mod = "%"
}

#[parsable]
struct WrappedOperator {
    operator: Operator
}

fn main() {
    let string = "+".to_string();
    let options = ParseOptions::default();
    let result = WrappedOperator::parse(string, options).unwrap();

    dbg!(result.location()); // It works!
}

ParseError

在失败时,Parsable::parse() 返回 Err(ParseError)。此结构体具有以下字段

  • file: Rc<FileInfo>: 错误发生的文件。
  • index: usize: 错误发生的索引。
  • expected: Vec<String>: 在此索引处期望的项名称列表。

宏选项

根属性

  • located=<bool>: 在结构体上指示是否应生成 location 字段。默认值:true
  • cascade=<bool>:如果在结构体上为 true,则表示如果 Option 字段不匹配,则解析器不应尝试匹配其他 Option 字段。这不会使整体结构解析无效。默认值:false
  • name=<string>:指示结构体或枚举的名称,当发生解析错误时使用。默认值:结构体或枚举的名称。
#[parsable(located=false)] // The `location` field will not be added
struct Operation {
    first_operand: Operand,
    other_operands: Vec<(Operator, Operand)>
}

字段属性

  • prefix=<string>:在解析字段之前尝试解析指定的字符串。如果前缀解析失败,则字段解析失败。
  • suffix=<string>:在解析字段之后尝试解析指定的字符串。如果后缀解析失败,则字段解析失败。
  • brackets=<string>:通过使用指定字符串的前两个字符来同时指定前缀和后缀的快捷方式。
  • exclude=<string>:指示只有当项目不匹配指定的正则表达式时,解析才是有效的。
  • followed_by=<string>:指示只有当项目后面跟有指定的正则表达式时,解析才是有效的。
  • not_followed_by=<string>:指示只有当项目后面不跟有指定的正则表达式时,解析才是有效的。
  • value=<string>:在一个 String 字段上,指示该字段仅匹配指定的字符串。
  • regex=<string>:在一个 String 字段上,指示该字段仅匹配具有指定模式的正则表达式(使用 regex crate)。
  • separator=<string>:在一个 Vec 字段上,指定项目之间的分隔符。
  • min=<integer>:在一个 Vec 字段上,指定解析有效的最小项目数。
  • cascade=false:指示该字段忽略根 cascade 选项

手动实现 Parsable 特性

有时 #[parsable] 不足以实现自己的解析机制,您需要实现自己的解析机制。这通过实现 parse_itemget_item_namelocation 方法来完成。

use parsable::{Parsable, StringReader};

struct MyInteger {
    value: u32,
    location: ItemLocation,
}

impl Parsable for MyInteger {
    fn parse_item(reader: &mut StringReader) -> Option<Self> {
        let start = reader.get_index();

        match reader.read_regex(r"\d+") {
            Some(string) => Some(MyInteger {
                value: string.parse().unwrap(),
                location: reader.get_item_location(start),
            }),
            None => None,
        }
    }

    // Only used in errors
    fn get_item_name() -> String {
        "integer".to_string()
    }

    // Not required, but convenient
    fn location(&self) -> &ItemLocation {
        &self.location
    }
}

fn main() {
    let number_string = "56";
    let number = MyInteger::parse(number_string.to_string(), ParseOptions::default()).unwrap();
    println!("{}", number.value);
}

StringReader 使用索引封装要解析的字符串,随着解析的进行,索引会增加。它具有以下方法

  • content() -> &str:返回整个字符串
  • get_index() -> usize:返回字符串中的当前索引
  • set_index(index: usize) -> usize:设置字符串中的当前索引
  • as_str() -> &str:返回尚未解析的字符串部分(等同于 &self.content()[self.get_index()..]
  • as_char() -> char:返回当前字符(等同于 &self.content().as_bytes()[self.get_index()]
  • is_finished() -> bool:指示是否已到达字符串的末尾
  • advance(length: usize) -> Option<&str>:将当前索引向前推进 length,并返回相应的子字符串。如果 length0,则返回 None
  • eat_spaces():将当前索引向前推进,直到遇到非空白和非注释字符
  • read_string(string: &str) -> Option<&str>:如果字符串以 string 开头,则将当前索引向前推进 string 的长度,并返回它,否则返回 None
  • read_regex(pattern: &'static str) -> Option<&str>:如果字符串以指定的正则表达式模式开头,则将当前索引向前移动到解析字符串的长度,并返回它,否则返回 None
  • peek_regex(pattern: &'static str) -> bool:指示字符串是否以指定的正则表达式模式开头,而不移动当前索引

如果 parse_item 返回 None,则它必须确保函数退出时的索引与开始时的索引相同。

许可证

MIT

依赖关系

~3.5–4.5MB
~89K SLoC