#parser-combinator #parser #operator-overloading #peg #parse-tree

rushell_deps_pom

不使用宏的运算符重载的PEG解析器组合器

2个版本

3.2.0-jeff.22021年5月7日

#94 in 解析工具


用于 rushell

MIT 许可证

170KB
991

pom

Crates.io Build Status Docs Discord

使用运算符重载不使用宏创建的PEG解析器组合器。

文档

什么是PEG?

PEG代表解析表达式语法,是一种分析形式语法,即它通过一组识别语言中字符串的规则来描述形式语言。与CFG不同,PEG不能有歧义;如果一个字符串可以解析,它只有一个有效的解析树。每个解析函数在概念上接受一个输入字符串作为其参数,并返回以下结果之一

  • 成功,在这种情况下,函数可以选择性地向前移动或消费其提供的输入字符串中的一个或多个字符,或者
  • 失败,在这种情况下,不消耗任何输入。

维基百科上了解更多。

什么是解析器组合器?

解析器组合器是一种接受多个解析器作为输入并返回新解析器作为其输出的高阶函数。解析器组合器启用递归下降解析策略,这有助于模块化分段构建和测试。

使用组合器构建的解析器易于构建、易于阅读、模块化、结构良好且易于维护。通过运算符重载,解析器组合器可以采取中缀运算符的形式,用于将不同的解析器粘合在一起形成完整的规则。因此,解析器组合器使解析器能够以嵌入式的方式定义,代码结构与形式语法的规则相似。代码比宏更容易调试。

主要优点是不需要通过任何类型的代码生成步骤,您始终使用底层标准语言。除了构建问题(以及通常关于错误消息和可调试性的问题,公平地说,宏和代码生成的问题大致相同),通常更容易自由混合语法表达式和普通代码。

预定义解析器和组合器列表

基本解析器 描述
empty() 总是成功,不消耗输入。
end() 匹配输入的末尾。
any() 匹配任何符号并返回该符号。
sym(t) 匹配单个终结符号 t
seq(s) 匹配符号序列。
list(p,s) 匹配由 s 分隔的 p 列表。
one_of(set) 当当前输入符号是集合中的任何一个时成功。
none_of(set) 当当前输入符号不是集合中的任何一个时成功。
is_a(predicate) 当谓词在当前输入符号上返回 true 时成功。
not_a(predicate) 当谓词在当前输入符号上返回 false 时成功。
take(n) 读取 n 个符号。
skip(n) 跳过 n 个符号。
call(pf) 调用解析器工厂,可用于创建递归解析器。
解析器组合器 描述
p | q 匹配 p 或 q,返回第一个成功的结果。
p + q 匹配 p 和 q,如果两者都成功,则返回结果的元组。
p - q 匹配 p 和 q,如果两者都成功,则返回 p 的结果。
p * q 匹配 p 和 q,如果两者都成功,则返回 q 的结果。
p >> q 解析 p 并获取结果 P,然后解析 q 并返回 q(P) 的结果。
-p 当 p 成功时成功,不消耗输入。
!p 当 p 失败时成功,不消耗输入。
p.opt() 使解析器可选。返回一个 Option
p.repeat(m..n) p.repeat(0..) 重复 p 零次或多次
p.repeat(1..) 重复 p 一次或多次
p.repeat(1..4) 至少匹配 p 1 次,最多 3 次
p.repeat(5) 重复 p 精确 5 次
p.map(f) 将解析器结果转换为所需值。
p.convert(f) 将解析器结果转换为所需值,转换错误时失败。
p.pos() 获取匹配 p 后的输入位置。
p.collect() 收集所有匹配的输入符号。
p.discard() 丢弃解析器输出。
p.name(_) 为解析器提供一个名称以识别解析错误。
p.expect(_) 将解析器标记为期望值,在有序选择失败时提前终止。

操作符的选择由它们的运算符优先级、算术和“意义”确定。使用 * 忽略表达式开始时第一个操作数的结果,+- 可以满足表达式其余部分的需求。

例如,A * B * C - D + E - F 将返回 C 和 E 的结果作为一对。

示例代码

extern crate pom;
use pom::parser::*;

let input = b"abcde";
let parser = sym(b'a') * none_of(b"AB") - sym(b'c') + seq(b"de");
let output = parser.parse(input);
assert_eq!(output, Ok( (b'b', vec![b'd', b'e']) ) );

示例 JSON 解析器

extern crate pom;
use pom::parser::*;
use pom::Parser;

use std::collections::HashMap;
use std::str::{self, FromStr};

#[derive(Debug, PartialEq)]
pub enum JsonValue {
	Null,
	Bool(bool),
	Str(String),
	Num(f64),
	Array(Vec<JsonValue>),
	Object(HashMap<String,JsonValue>)
}

fn space() -> Parser<u8, ()> {
	one_of(b" \t\r\n").repeat(0..).discard()
}

fn number() -> Parser<u8, f64> {
	let integer = one_of(b"123456789") - one_of(b"0123456789").repeat(0..) | sym(b'0');
	let frac = sym(b'.') + one_of(b"0123456789").repeat(1..);
	let exp = one_of(b"eE") + one_of(b"+-").opt() + one_of(b"0123456789").repeat(1..);
	let number = sym(b'-').opt() + integer + frac.opt() + exp.opt();
	number.collect().convert(str::from_utf8).convert(|s|f64::from_str(&s))
}

fn string() -> Parser<u8, String> {
	let special_char = sym(b'\\') | sym(b'/') | sym(b'"')
		| sym(b'b').map(|_|b'\x08') | sym(b'f').map(|_|b'\x0C')
		| sym(b'n').map(|_|b'\n') | sym(b'r').map(|_|b'\r') | sym(b't').map(|_|b'\t');
	let escape_sequence = sym(b'\\') * special_char;
	let string = sym(b'"') * (none_of(b"\\\"") | escape_sequence).repeat(0..) - sym(b'"');
	string.convert(String::from_utf8)
}

fn array() -> Parser<u8, Vec<JsonValue>> {
	let elems = list(call(value), sym(b',') * space());
	sym(b'[') * space() * elems - sym(b']')
}

fn object() -> Parser<u8, HashMap<String, JsonValue>> {
	let member = string() - space() - sym(b':') - space() + call(value);
	let members = list(member, sym(b',') * space());
	let obj = sym(b'{') * space() * members - sym(b'}');
	obj.map(|members|members.into_iter().collect::<HashMap<_,_>>())
}

fn value() -> Parser<u8, JsonValue> {
	( seq(b"null").map(|_|JsonValue::Null)
	| seq(b"true").map(|_|JsonValue::Bool(true))
	| seq(b"false").map(|_|JsonValue::Bool(false))
	| number().map(|num|JsonValue::Num(num))
	| string().map(|text|JsonValue::Str(text))
	| array().map(|arr|JsonValue::Array(arr))
	| object().map(|obj|JsonValue::Object(obj))
	) - space()
}

pub fn json() -> Parser<u8, JsonValue> {
	space() * value() - end()
}

fn main() {
	let input = br#"
	{
        "Image": {
            "Width":  800,
            "Height": 600,
            "Title":  "View from 15th Floor",
            "Thumbnail": {
                "Url":    "http://www.example.com/image/481989943",
                "Height": 125,
                "Width":  100
            },
            "Animated" : false,
            "IDs": [116, 943, 234, 38793]
        }
    }"#;

	println!("{:?}", json().parse(input));
}

您可以使用以下命令运行此示例

cargo run --example json

基准测试

解析器 解析相同JSON文件的时间
pom: json_byte 621,319 ns/iter (+/- 20,318)
pom: json_char 627,110 ns/iter (+/- 11,463)
pest: json_char 13,359 ns/iter (+/- 811)

生命周期和文件

字符串字面量具有静态生命周期,因此它们可以与从 pom::Parser 导入的解析器的静态版本一起工作。从文件中读取的输入具有较短的生存期。在这种情况下,你应该导入 pom::parser::Parser 并在解析器函数上声明生命周期。所以

fn space() -> Parser<u8, ()> {
    one_of(b" \t\r\n").repeat(0..).discard()
}

将变成

fn space<'a>() -> Parser<'a, u8, ()> {
    one_of(b" \t\r\n").repeat(0..).discard()
}

无运行时依赖