#nom #error #parser #tracking #input #spans #context

无 std kh5ase

nom 解析器的跟踪和更好的错误处理

10 个稳定版本

3.0.5 2023年11月16日
3.0.4 2023年3月26日
2.0.0 2023年2月24日
1.1.0 2023年2月5日
1.0.1 2023年1月29日

#168解析器实现

Download history 23/week @ 2024-04-08 19/week @ 2024-04-15 23/week @ 2024-04-22 25/week @ 2024-04-29 28/week @ 2024-05-06 37/week @ 2024-05-13 33/week @ 2024-05-20 22/week @ 2024-05-27 62/week @ 2024-06-03 65/week @ 2024-06-10 42/week @ 2024-06-17 51/week @ 2024-06-24 9/week @ 2024-07-01 38/week @ 2024-07-08 55/week @ 2024-07-15 31/week @ 2024-07-22

137 每月下载次数
用于 6 个crate (3 直接)

MIT/Apache

185KB
4.5K SLoC

kh5ase

nom 解析器的附加组件。

  • 错误码的特质代码。

  • ParserError 用于完整错误收集,TokenizerError 用于快速内部循环。

  • 解析器执行的跟踪/日志记录。

  • Builder 风格的测试,可以进行从简单的 ok/err 测试到对结果进行深度检查。

  • 具有可插拔的报告功能。

  • 为解析器提供扩展的后缀适配器集。灵感来自 nom_supreme,但与该crate的错误码和错误类型集成。

  • SourceStr 和 SourceBytes 用于获取span的上下文信息。

    • 行/列信息
    • 上下文源行。
    • 无需 LocatedSpan。
  • 默认情况下,跟踪功能仅在调试模式下激活。

  • 在发布模式下,所有跟踪都会编译为空。

完整代码可在 examples/example1.rs 中找到。

// := a* b
fn parse_a_star_b(input: ExSpan<'_>) -> ExParserResult<'_, AstAstarB> {
    track(
        ExAstarB,
        tuple((many0(parse_a), parse_b))
            .map(|(a, b)| AstAstarB { a, b }),
    )
    .err_into()
    .parse(input)
}

// := ( a | b )*
fn parse_a_b_star(input: ESpan<'_>) -> EResult<'_, AstABstar> {
    Context.enter(EABstar, input);

    let mut loop_rest = input;
    let mut res = AstABstar {
        a: vec![],
        b: vec![],
    };
    let mut err = None;

    loop {
        let rest2 = loop_rest;

        let rest2 = match parse_a(rest2) {
            Ok((rest3, a)) => {
                res.a.push(a);
                rest3
            }
            Err(e) => match parse_b(rest2) {
                Ok((rest3, b)) => {
                    res.b.push(b);
                    rest3
                }
                Err(e2) => {
                    err.append(e)?;
                    err.append(e2)?;
                    rest2
                }
            },
        };

        if let Some(err) = err {
            return Context.err(err);
        }
        if rest2.is_empty() {
            break;
        }

        loop_rest = rest2;
    }

    Context.ok(loop_rest, input, res)
}

基础知识

预览

有一个用于所有常用特质的预览。

错误码

定义错误码枚举。错误码用于实际的错误报告以及在跟踪解析器执行时作为标记。

所有 nom errorkind 都映射到一个解析器错误。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ECode {
    ENomError,

    ETagA,
    ETagB,
    ENumber,

    EAthenB,
    EAoptB,
    EAstarB,
    EABstar,
    EAorB,
    EABNum,
}

impl Code for ECode {
    const NOM_ERROR: Self = Self::ENomError;
}

该crate在类型变量上非常重。以下建议使用以下类型别名。

define_span!(ExSpan = ExCode, str);
pub type ExParserResult<'s, O> = ParserResult<ExCode, ExSpan<'s>, O>;
pub type ExTokenizerResult<'s, O> = TokenizerResult<ExCode, ExSpan<'s>, O>;
pub type ExParserError<'s> = ParserError<ExCode, ExSpan<'s>>;
pub type ExTokenizerError<'s> = TokenizerError<ExCode, ExSpan<'s>>;

define_span 创建一个数据类型别名。它的结果在调试和发布模式下不同。

ParserError 可以包含多个错误码和各种额外数据。另一方面,TokenizerError 只有一个错误码和一个范围,以最小化其大小。

AST

根据您的需求定义解析器的输出。这里没有限制。

#[derive(Debug)]
struct AstNumber<'s> {
    pub number: u32,
    pub span: ESpan<'s>,
}

解析器函数

解析器函数与普通的 nom 解析器相同,只是使用不同的输入和错误类型

fn token_number(i: ExSpan<'_>) -> ExParserResult<'_, AstNumber<'_>> {
    nom_number
        .map(|(span, number)| AstNumber { number, span })
        .parse(i)
}

IResult

ParserError 和 TokenizerError 实现 nom::error::ParseError,它们可以用作 nom::error::Error 的替代。

错误处理

err_into()

使用 From 特质的错误转换。

parse_from_str()

使用FromStr特性行解析。接收错误代码以在失败时创建错误。

with_code()

更改错误的错误代码。旧错误代码保留为预期代码。此函数作为组合函数和为Result<>定义的函数可用。

解析跟踪

在解析器内部

跟踪器作为LocatedSpan.extra字段添加,这种方式不需要额外的参数。

要访问跟踪器,请使用Track结构。

fn parse_a(input: ESpan<'_>) -> EResult<'_, AstA> {
    Track.enter(ETagA, input);
    let (rest, tok) = nom_parse_a(input).track()?;

    if false {
        return Track.err(EParserError::new(EAorB, input));
    }

    Track.ok(rest, tok, AstA { span: tok })
}

enter()、ok()和err()捕获解析器的正常控制流。

track()对Result进行操作,以便轻松传播错误。

注意:有track_as(code: ExCode)来更改错误代码。

调用解析器

创建StdTracker并使用带注解的跨度调用解析器。

根据调试/发布模式,Track.span()返回LocatedSpan或原始文本。

fn main() {
    for txt in env::args() {
        let trk = Track.new_tracker();
        let span = Track.span(trk, txt.as_str());

        match parse_a_b_star(span) {
            Ok((rest, val)) => {}
            Err(e) => {
                println!("{:?}", trk.results());
                println!("{:?}", e);
            }
        }
    }
}

获取跟踪数据

StdTracker::results()的调用返回跟踪数据。

测试解析器

测试模块有几个函数可以运行针对单个解析器函数的测试并评估结果。

str_parse()运行解析器并返回一个具有各种构建器函数的Test结构,以检查结果。如果任何检查失败,q()调用将报告为失败的测试。

q()接受一个参数,该参数定义了实际完成的报告。CheckTrace是其中之一,它转储跟踪和错误并引发恐慌。

#[test]
fn test_1() {
    str_parse(&mut None, "", parse_ab).err_any().q(Timing(1));
    str_parse(&mut None, "ab", parse_ab).ok_any().q(Timing(1));
    str_parse(&mut None, "aba", parse_ab).rest("a").q(Timing(1));
}

结果如下。

FAIL: Expected ok, but was an error.

when parsing LocatedSpan { offset: 0, line: 1, fragment: "aabc", extra:  } in 43.4µs =>
trace
  (A | B)*: enter with "aabc"
    a: enter with "aabc"
    a: ok -> [ "a", "abc" ]
    a: enter with "abc"
    a: ok -> [ "a", "bc" ]
    a: enter with "bc"
    a: err ENomError  errorkind Tag for span LocatedSpan { offset: 2, line: 1, fragment: "bc", extra:  } 
    b: enter with "bc"
    b: ok -> [ "b", "c" ]
    a: enter with "c"
    a: err ENomError  errorkind Tag for span LocatedSpan { offset: 3, line: 1, fragment: "c", extra:  } 
    b: enter with "c"
    b: err ENomError  errorkind Tag for span LocatedSpan { offset: 3, line: 1, fragment: "c", extra:  } 
  (A | B)*: err ENomError  errorkind Tag for span LocatedSpan { offset: 3, line: 1, fragment: "c", extra:  } 

error
ParserError nom for LocatedSpan { offset: 3, line: 1, fragment: "c", extra:  }
errorkind=Tag

组合器

一些我一直缺少的东西。

track()

跟踪对子解析器的调用。在子解析器前后调用Track.enter()和Track.ok()/Track.err()。

pchar()

类似于nom的char函数,但名称更容易,并返回输入类型而不是char。

err_into()

使用 From 特质的错误转换。

with_code()

更改错误代码。

separated_list_trailing0和1

类似于separated_list,但允许有尾部分隔符。

错误报告

SpanUnion

这个特性行为一种解析撤销。它作为输入值的成员方法调用,并接受两个解析片段。然后返回一个覆盖两个输入片段的新片段。

nom有consume()和recognize()用于此,它们也工作得很好。

SpanFragment

通过特性行提供nom_locate的fragment()函数。
这样它对&str也有效。在切换位于Span和普通&str之间时这是必要的。

SourceStr和SourceBytes

它们可以用Track.source_str()/Track.source_bytes()创建。

它们可以将任何解析片段映射到行/列索引,并可以提取周围的文本行。

ParserError与TokenizerError

由于包含所有额外数据的Vec,ParserError是TokenizerError的两倍大小。

我使用TokenizerError进行低级解析器,并在需要额外信息的地方切换到ParserError。

使用err_into()这并不太烦人。

依赖项

~1MB
~21K SLoC