惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
博客园 - 叶小钗
P
Privacy International News Feed
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
IT之家
IT之家
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
罗磊的独立博客
I
Intezer
S
Schneier on Security
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Ivan on Containers, Kubernetes, and Server-Side

A grounded take on agentic coding for production environments Server-Side Playgrounds Reimagined: Build, Boot, and Network Your Own Virtual Labs [not a] Kubernetes 101 - Pods, Deployments, and Services As an Attempt To Automate Age-Old Infra Patterns JavaScript or TypeScript? How To Benefit From the Dichotomy On Software Design... and Good Writing Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes How To Publish a Port of a Running Container What Actually Happens When You Publish a Container Port A Visual Guide to SSH Tunnels: Local and Remote Port Forwarding Debugging Containers Like a Pro Docker: How To Debug Distroless And Slim Containers How To Extract Container Image Filesystem Using Docker | iximiuz Labs In Pursuit of Better Container Images: Alpine, Distroless, Apko, Chisel, DockerSlim, oh my! How To Start Programming In Go: Advice For Fellow DevOps Engineers Kubernetes Ephemeral Containers and kubectl debug Command How To Develop Kubernetes CLIs Like a Pro Docker Container Commands Explained: Understand, Don't Memorize | iximiuz Labs Learning Docker with Docker - Toying With DinD For Fun And Profit How To Extend Kubernetes API - Kubernetes vs. Django The Influence of Plumbing on Programming How To Call Kubernetes API from Go - Types and Common Machinery How To Call Kubernetes API using Simple HTTP Client Kubernetes API Basics - Resources, Kinds, and Objects OpenFaaS - Run Containerized Functions On Your Own Terms Learning Containers From The Bottom Up Docker Containers vs. Kubernetes Pods - Taking a Deeper Look | iximiuz Labs Learn-by-Doing Platforms for Dev, DevOps, and SRE Folks How HTTP Keep-Alive can cause TCP race condition How to Work with Container Images Using ctr | iximiuz Labs Multiple Containers, Same Port, no Reverse Proxy... Exploring Go net/http Package - On How Not To Set Socket Options Disposable Local Development Environments with Vagrant, Docker, and Arkade DevOps, SRE, and Platform Engineering My Choice of Programming Languages Prometheus Is Not a TSDB How to learn PromQL with Prometheus Playground Prometheus Cheat Sheet - Basics (Metrics, Labels, Time Series, Scraping) pq - parse and query log files as time series Prometheus Cheat Sheet - Moving Average, Max, Min, etc (Aggregation Over Time) Prometheus Cheat Sheet - How to Join Multiple Metrics (Vector Matching) The Need For Slimmer Containers Understanding Rust Privacy and Visibility Model Bridge vs. Switch: Takeaways from a Real Data Center Tour | iximiuz Labs From LAN to VXLAN: Networking Basics for Non-Network Engineers | iximiuz Labs KiND - How I Wasted a Day Loading Local Docker Images Go, HTTP handlers, panic, and deadlocks Exploring Kubernetes Operator Pattern Making Sense Out Of Cloud Native Buzz Service Discovery in Kubernetes: Combining the Best of Two Worlds API Developers Never REST How Container Networking Works: Building a Bridge Network From Scratch | iximiuz Labs Traefik: canary deployments with weighted load balancing Service Proxy, Pod, Sidecar, oh my! You Need Containers To Build Images You Don't Need an Image To Run a Container Not Every Container Has an Operating System Inside Working with container images in Go Master Go While Learning Containers Implementing Container Runtime Shim: Interactive Containers How to use Flask with gevent (uWSGI and Gunicorn editions) My 10 Years of Programming Experience Implementing Container Runtime Shim: First Code Implementing Container Runtime Shim: runc Kubernetes Repository On Flame Dealing with process termination in Linux (with Rust examples) conman - [the] Container Manager: Inception Journey From Containerization To Orchestration And Beyond Linux PTY - How docker attach and docker exec Commands Work Inside Illustrated introduction to Linux iptables From Docker Container to Bootable Linux Disk Image Пишем свой веб-сервер на Python: протокол HTTP 9001 способ создать веб-сервер на Python Explaining async/await in 200 lines of code Explaining event loop in 100 lines of code Save the day with gevent Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O Truly optional scalar types in protobuf3 (with Go examples) Node.js Writable streams distilled Node.js Readable streams distilled How to on starting processes (mostly in Linux) Дайджест интересных ссылок – Июль 2016 Пишем свой веб-сервер на Python: сокеты Наследование в JavaScript Мастерить!
Rust - Writing Parsers With nom Parser Combinator Framework
Ivan Velichko · 2021-07-22 · via Ivan on Containers, Kubernetes, and Server-Side

I've been working on my new Rust side-project for several months now, and I've got some learnings to share. The project is called pq - it's a command-line tool to parse and query log files as time series. It comes with its own domain-specific language that is highly influenced by PromQL. A typical pq usage may look like this:

tail -f /var/log/nginx/access.log | pq '
/...some fancy regex.../
| map {
    .0 as ip,
    .1:ts,
    .2 as method,
    .3:str as status_code,
    .4 as content_len
  }
| select topk(
      10,
      sum(
          count_over_time(
              __line__{method="GET", status_code="200"}[1s]
          )
      ) by (ip)
  )
| to_json
'

pq has many components, including various log parsing strategies and a pretty sophisticated query execution engine. But surprisingly or not, about half of the time I've put into this project so far was dedicated to writing the parser of the pq's own query language. To be honest, when I was starting the project, I didn't see that coming...

nom logo

Luckily, writing a parser in Rust was mostly a pleasant experience, thanks to a crate concisely named nom. Although learning how to write parsers with nom wasn't completely seamless. So here is my journey.

Level up your server-side game — join 20,000 engineers getting insightful learning materials straight to their inbox.

Parser combinators

As I've been taught in the university, writing a parser starts from defining a formal grammar 🥱

The grammar describes a language in a rather declarative way. Yay, everybody loves functional programming! The grammar is then passed to a parser generator software like Yacc or Bison that produces a code of the new, fully-generated parser. For instance, here is the real PromQL grammar (looks fine) and the corresponding generated parser (looks terrifying).

Well, for some reason, I've never felt excited about this approach...

On the other end of the spectrum resides an entirely manual way of writing parsers. First, the input text needs to be tokenized into keywords, identifiers, number and string literals, etc. A particular sequence of tokens can then be recognized as a certain expression, such as a function call or a binary operation. While this technique appeals more to me, it would require writing a substantial amount of low-level tokenization routines. The parser logic itself would also have quite some boilerplate code for matching various combinations of tokens into higher-layer constructs - syntax tree nodes.

Parser combinators to the rescue!

Apparently, there are libraries that free you from reimplementing the low-level repeating logic over and over again. They provide some primitive functions like take_next_5_bytes(), recognize_a_float(), or recognize_a_word() for tokenization and other functions like pair(f1, f2), or alt(opt1, opt2, opt3, ...) for recursive combination of other parsing routines.

A parser developer can pack one or many such parsers and/or combinators into a domain-specific function giving it a meaningful name. A combination of such higher-level functions forms a language grammar. But the grammar is expressed in the target code now! In the case of Rust, nom is one of such parser combinator frameworks. From the very real pq code:

use nom::{
    branch::alt,
    bytes::complete::{tag, tag_no_case},
    character::complete::{alpha1, alphanumeric1},
    combinator::recognize,
    multi::many0,
    sequence::pair,
    IResult
};

/// Parses a label matching clause into a syntax tree node `LabelMatching`:
///  - on()
///  - on(label1, label2, label3, ...)
///  - ignoring()
///  - ignoring(label1, label2, label3, ...)
fn label_matching(input: &str) -> IResult<LabelMatching> {
    let (rest, kind) = alt((tag_no_case("on"), tag_no_case("ignoring")))(input)?;
    let (rest, labels) = grouping_labels(rest)?;
    Ok((rest, LabelMatching::new(kind, labels)))
}

fn grouping_labels(input: &str) -> IResult<Vec<String>> {
    separated_list(
        '(',          // opener
        ')',          // closer
        ',',          // delimiter
        label_identifier,
    )(input)
}

/// Parses strings like [a-zA-Z_][a-zA-Z0-9_]*
fn label_identifier(input: &str) -> IResult<String> {
    // The true power of combination!
    let (rest, m) = recognize(pair(
        alt((alpha1, tag("_"))),
        many0(alt((alphanumeric1, tag("_")))),
    ))(input)?;
    Ok((rest, String::from(*m.fragment())))
}

As you can see, the resulting code looks pretty declarative. However, nom doesn't restrict you from writing imperative logic when it'd simplify the implementation. For instance, I couldn't find a better way to specify a regular expression in pq programs than going with /.../ syntax. But some regular expressions may contain slashes. So, I had to use escaping. And then I cut the corner with the following hack:

use nom::{bytes::complete::take, character::complete::char, IResult};

/// Parses a /<regex>/ into a string:
///  - /\d+/ becomes String::new("\d+")
///  - /\w+\/some\/path\s/ becomes String::new("\w+/some/path\s")
fn decoder_regex(input: &str) -> IResult<String> {
    let (rest, _) = char('/')(input)?;

    // Some imperative ad-hoc parsing logic...
    if let Some(end_pos) = find_first_unescaped_slash(*rest) {
        let (rest, regex) = take(end_pos)(rest)?;
        let (rest, _) = char('/')(rest)?;
        return Ok((rest, (*regex).replace(r#"\/"#, "/")));
    }

    Err(nom::Err::Failure(ParseError::partial(
        "regex",
        "closing '/' symbol",
        rest,
    )))
}

Nom parser example

Ok, I hope it's enough to sell you the beautifulness of the parser combinator approach. Now let's try to write a tiny parser using nom and see what problems we may stumble upon on the way.

The high-level idea of combinators is probably clear at the moment, but it's always good to understand the lower-level details before jumping to the actual parser implementation. Oversimplifying, a typical nom's routine can be described as follows:

  • it takes in a string that needs to be parsed
  • it attempts to match the beginning of the string with some pattern
  • if a match is found, it returns Ok((remainder, parsed_value))
  • if no match is found, it returns an Err(), so alternate parsers, if any, could have a try.

Example with a simple word match. The parsed value here is a substring:

use nom::{bytes::complete::tag, IResult};

fn foo(s: &str) -> IResult<&str, &str> {
    tag("foo")(s)
}

fn main() {
    assert_eq!(foo("foo bar"), Ok((" bar", "foo")));
    assert!(foo("1234567").is_err());
}

Building on the idea - adding a combinator separated_pair. The parsed value becomes a tuple of substrings:

use nom::{
    bytes::complete::tag,
    character::complete::char,
    sequence::separated_pair,
    IResult
};

fn foo(s: &str) -> IResult<&str, &str> {
    tag("foo")(s)
}

fn bar(s: &str) -> IResult<&str, &str> {
    tag("bar")(s)
}

fn foo_bar(s: &str) -> IResult<&str, (&str, &str)> {
    separated_pair(foo, char(' '), bar)(s)
}

fn main() {
    assert_eq!(foo_bar("foo bar"), Ok(("", ("foo", "bar"))));
    assert!(foo_bar("1234567").is_err());
}

Adding even more combinators:

use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::{char, digit1},
    combinator::recognize,
    sequence::separated_pair,
    IResult,
};

fn foo(s: &str) -> IResult<&str, &str> {
    tag("foo")(s)
}

fn bar(s: &str) -> IResult<&str, &str> {
    tag("bar")(s)
}

fn foo_bar(s: &str) -> IResult<&str, (&str, &str)> {
    separated_pair(foo, char(' '), bar)(s)
}

fn foo_bar_or_number(s: &str) -> IResult<&str, &str> {
    alt((recognize(foo_bar), recognize(digit1)))(s)
}

fn main() {
    assert_eq!(foo_bar_or_number("foo bar"), Ok(("", "foo bar")));
    assert_eq!(foo_bar_or_number("1234567"), Ok(("", "1234567")));
}

The foo_bar_or_number() parser alternates between the foo_bar() and digit1 subparsers. But since the first one returns a tuple ("foo", "bar") while the second one returns just a single substring "1234567", the result needs to coalesce to a common type. recognize() utility does the trick here - it discards the actual parsed value and returns the matched substring as a result.

Well, that's probably it for the basics!

As usual, there is more than one way to skin a cat - nom comes with lots of such small parsers and combinators, and they can be combined in many ways to express the same parsing logic. Personally, I find it pretty rewarding to think of various ways certain parsers can be put together to parse a given grammar. But I cannot imagine doing it without this super-helpful page listing all the most frequently used routines by groups.

Pitfall - nom error handling

Ok, enough of the made-up stuff. Let's mess with a real-world problem. Much like pq does, I'll try to parse a simplified subset of PromQL here. Or, more precisely, a vector selector expression that looks like this:

http_requests_total{method="GET", status_code=~"2.."}

First, a code to parse an identifier. It'll be used to match metric and label names in the vector selector:

/// Parses an identifier (in the most common sense).
///
/// ```
/// # use nom_parser_example::identifier;
/// #
/// # fn main() {
/// assert_eq!(identifier("foo bar"), Ok((" bar", "foo")));
/// assert_eq!(identifier("_12 bar"), Ok((" bar", "_12")));
/// assert_eq!(identifier("012 bar").is_err(), true);
/// # }
/// ```
pub fn identifier(input: &str) -> IResult<&str, &str> {
    // [a-zA-Z_][a-zA-Z0-9_]*
    let (rest, m) = recognize(pair(
        alt((alpha1, tag("_"))),
        many0(alt((alphanumeric1, tag("_")))),
    ))(input)?;
    Ok((rest, m))
}

Now, a naive string literal parsing. Don't use this code in production - it doesn't handle escape sequences! (checkout this nom example for more)

/// Parses a quoted string.
///
/// ```
/// # use nom_parser_example::naive::string_literal;
/// #
/// # fn main() {
/// assert_eq!(string_literal(r#""" baz"#), Ok((" baz", "")));
/// assert_eq!(string_literal(r#""foo bar" baz"#), Ok((" baz", "foo bar")));
/// assert_eq!(string_literal("qux").is_err(), true);
/// # }
/// ```
pub fn string_literal(input: &str) -> IResult<&str, &str> {
    let (rest, m) = recognize(
        delimited(char('"'), many0(is_not("\"")), char('"'))
    )(input)?;
    Ok((rest, &m[1..m.len() - 1]))
}

Parsing a label matching operator with a handy value mapper:

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MatchOp {
    Eql,   // ==
    Neq,   // !=
    EqlRe, // =~
    NeqRe, // !~
}

/// Parses a label matching operator.
///
/// ```
/// # use nom_parser_example::naive::{match_op, MatchOp};
/// #
/// # fn main() {
/// assert_eq!(match_op("=="), Ok(("", MatchOp::Eql)));
/// assert_eq!(match_op("!="), Ok(("", MatchOp::Neq)));
/// assert_eq!(match_op("=~"), Ok(("", MatchOp::EqlRe)));
/// assert_eq!(match_op("!~"), Ok(("", MatchOp::NeqRe)));
/// # }
/// ```
pub fn match_op(input: &str) -> IResult<&str, MatchOp> {
    alt((
        value(MatchOp::Eql, tag("==")),
        value(MatchOp::Neq, tag("!=")),
        value(MatchOp::EqlRe, tag("=~")),
        value(MatchOp::NeqRe, tag("!~")),
    ))(input)
}

Note that the match_op() parser also uses a custom struct for the result type.

One more beast, bear with me:

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LabelMatch<'a> {
    pub label: &'a str,
    pub value: &'a str,
    pub op: MatchOp,
}

/// Parses a label matching operation.
///
/// ```
/// # use nom_parser_example::naive::{label_match, LabelMatch, MatchOp};
/// #
/// # fn main() {
/// assert_eq!(
///     label_match(r#"foo=="bar""#),
///     Ok(("", LabelMatch { label: "foo", value: "bar", op: MatchOp::Eql }))
/// );
/// assert_eq!(
///     label_match(r#"foo!~"2..""#),
///     Ok(("", LabelMatch { label: "foo", value: "2..", op: MatchOp::NeqRe }))
/// );
/// # }
/// ```
pub fn label_match(input: &str) -> IResult<&str, LabelMatch> {
    let (rest, label) = identifier(input)?;
    let (rest, op) = match_op(rest)?;
    let (rest, value) = string_literal(rest)?;
    Ok((rest, LabelMatch { label, value, op }))
}

Last but not least, putting all the above parsers together to match a vector selector expression:

#[derive(Clone, Debug, PartialEq)]
pub struct VectorSelector<'a> {
    pub metric: &'a str,
    pub labels: Vec<LabelMatch<'a>>,
}

/// Parses a vector selector.
///
/// ```
/// # use nom_parser_example::naive::vector_selector;
/// #
/// # fn main() {
/// assert_eq!(vector_selector(r#"foo{bar=="baz",qux!="123"}"#).is_ok(), true);
/// # }
/// ```
pub fn vector_selector(input: &str) -> IResult<&str, VectorSelector> {
    let (rest, metric) = identifier(input)?;
    let (rest, _) = char('{')(rest)?;
    let (rest, labels) = separated_list0(char(','), label_match)(rest)?;
    let (rest, _) = char('}')(rest)?;
    Ok((rest, VectorSelector { metric, labels }))
}

Lovely! Every individual parser looks nice and readable, and the compound vector_selector parser also seems pretty concise. So, where is the pitfall?

Try running the following code:

println!("{:?}", vector_selector(r#"foo {bar=="baz",qux="123",abc="mux"}"#));

Yes, I intentionally skipped the whitespace handling here. But when I was writing the real parser, I actually truly forgot about it 🙈 The above snippet produces the following error:

Err(Error(Error { input: " {bar==\"baz\",qux=\"123\",abc=\"mux\"}", code: Char }))

Well, for me, it was super hard to decipher it. The message contains some part of the input string and an obscure code: Char part. I'd guess that Char comes from the char() parser. But we actually used it in several places, so which one of them is failing? Parser errors are supposed to be user-friendly. It's an end-user who will face them most of the time. However, this error is not even developer-friendly...

Error position with nom_locate

Having an exact error position in the message would definitely simplify the parser development. But apparently, nom doesn't track it. Luckily, there is a tiny crate called nom_locate that does the trick.

The input type of the nom parsers and combinators is actually generic. By default, it uses &str, and it seems to work well for demo purposes. But I quickly found it too limited. nom_locate introduces a LocatedSpan type that can be used as an input type for the parsers. Apparently, it's just a thin wrapper that augments the original type with the location information.

The required code adjustment is actually quite small. First, add some type aliases for the sake of brevity:

use nom_locate::LocatedSpan;

pub type Span<'a> = LocatedSpan<&'a str>;

pub type IResult<'a, O> = nom::IResult<Span<'a>, O>;

And then simply adjust the input and output types of the parsers. Example:

fn identifier(input: Span) -> IResult<&str> {}

fn string_literal(input: Span) -> IResult<&str> {}

fn match_op(input: Span) -> IResult<MatchOp> {}

fn label_match(input: Span) -> IResult<LabelMatch> {}

fn vector_selector(input: Span) -> IResult<VectorSelector> {}

The LocatedSpan values can be defer-ed back to the enclosed type. So wherever the back conversion from LocatedSpan to &str is needed, a simple *value can be used.

And, the moment of truth:

println!("{:#?}", vector_selector(Span::new(r#"foo {bar=="baz",qux!="123"}"#)));

// Outputs
// Err(
//     Error(
//         Error {
//             input: LocatedSpan {
//                 offset: 3,
//                 line: 1,
//                 fragment: " {bar==\"baz\",qux!=\"123\"}",
//                 extra: (),
//             },
//             code: Char,
//         },
//     ),
// )

Yay! 🎉 The exact position where the parser failed is in the error message now!

User-friendly message with custom error type

Much like the input type, the error type is also generic. The above message allows targeting the error position quickly. However, it's still by no means a user-friendly error. Without looking at the code, it's not clear which of the parsers is actually failed. Also, there is no room for the suggested input changes.

That's how a custom nom error can be added:

use nom;

#[derive(Debug, PartialEq)]
pub struct ParseError<'a> {
    span: Span<'a>,
    message: Option<String>,
}

impl<'a> ParseError<'a> {
    pub fn new(message: String, span: Span<'a>) -> Self {
        Self { span, message: Some(message) }
    }

    pub fn span(&self) -> &Span { &self.span }

    pub fn line(&self) -> u32 { self.span().location_line() }

    pub fn offset(&self) -> usize { self.span().location_offset() }
}

// That's what makes it nom-compatible.
impl<'a> nom::error::ParseError<Span<'a>> for ParseError<'a> {
    fn from_error_kind(input: Span<'a>, kind: nom::error::ErrorKind) -> Self {
        Self::new(format!("parse error {:?}", kind), input)
    }

    fn append(_input: Span<'a>, _kind: nom::error::ErrorKind, other: Self) -> Self {
        other
    }

    fn from_char(input: Span<'a>, c: char) -> Self {
        Self::new(format!("unexpected character '{}'", c), input)
    }
}

The result type can also be slightly adjusted:

pub type IResult<'a, O> = nom::IResult<Span<'a>, O, ParseError<'a>>;

So, now it's possible to have a code like that:

pub fn vector_selector(input: Span) -> IResult<VectorSelector> {
    let (rest, metric) = identifier(input).map_err(|_| {
        nom::Err::Error(ParseError::new(
            "vector_selector must start from a metric name".to_owned(),
            input,
        ))
    })?;
    let (rest, _) = char('{')(rest)?;
    let (rest, labels) = separated_list0(char(','), label_match)(rest)?;
    let (rest, _) = char('}')(rest)?;
    Ok((rest, VectorSelector { metric, labels }))
}

Partial parsing error

nom parsing approach resembles the depth-first search. The combination of parsers forms a search tree that is greedily traversed from the root node, trying to find the longest possible match in the input string. If a parser in the current branch returns an error, nom backtracks trying to find an alternate parser on every step back. While backtracking, nom simply discards error messages of the failed parsers. If all the parsers fail, only the error message produced by the top-most level parser will be returned to the user.

For instance, if we are parsing a string like http_requests_total{foo=="bar", qux="123"}, the error will be about the malformed vector selector as a whole. Although as a human being, we can identify the problem much more precisely here - one of the label matches has an incomplete match operation =.

Maybe the label_match() parser even detected it and returned a proper error message. However, by default nom would just discard it while backtracking. Luckily, there is a way to improve the parser and make it properly reporting partial results.

nom's error type is actually compound:

/// The `Err` enum indicates the parser was not successful
///
/// It has three cases:
///
/// * `Incomplete` indicates that more data is needed to decide. The `Needed` enum
/// can contain how many additional bytes are necessary. If you are sure your parser
/// is working on full data, you can wrap your parser with the `complete` combinator
/// to transform that case in `Error`
/// * `Error` means some parser did not succeed, but another one might (as an example,
/// when testing different branches of an `alt` combinator)
/// * `Failure` indicates an unrecoverable error. As an example, if you recognize a prefix
/// to decide on the next parser to apply, and that parser fails, you know there's no need
/// to try other parsers, you were already in the right branch, so the data is invalid
///
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_doc_code_examples)]
pub enum Err<E> {
  /// There was not enough data
  Incomplete(Needed),
  /// The parser had an error (recoverable)
  Error(E),
  /// The parser had an unrecoverable error: we got to the right
  /// branch and we know other branches won't work, so backtrack
  /// as fast as possible
  Failure(E),
}

By default, parsers return nom::Err::Error(<actual error>). However, it's possible to use nom::Err::Failure(<actual error>) to make nom backtrack immediately right up to the root. Example:

pub fn label_match(input: Span) -> IResult<LabelMatch> {
    // If it doesn't start from an identifier, it's simply not a label matching.
    let (rest, label) = identifier(input)?;

    // But here it's already a partial match error. We are quite certain that
    // it's a label matching, but probably it's malformed or incomplete.
    let (rest, op) = match match_op(rest) {
        Ok((rest, op)) => (rest, op),
        Err(nom::Err::Error(_)) => {
            return Err(nom::Err::Failure(ParseError::new(
                "label matching - expected on of '==', '!=', '=~', '!~'".to_owned(),
                rest,
            )))
        }
        Err(e) => return Err(e),
    };

    // Same as above, but we are even more certain.
    let (rest, value) = match string_literal(rest) {
        Ok((rest, value)) => (rest, value),
        Err(nom::Err::Error(_)) => {
            return Err(nom::Err::Failure(ParseError::new(
                "label matching - expected string literal".to_owned(),
                rest,
            )))
        }
        Err(e) => return Err(e),
    };

    Ok((rest, LabelMatch { label, value, op }))
}

The upper-level parsers need to be aware of that trick as well. I find the following error handling pattern quite handy:

    // ...somewhere in the middle of a parser

    let (rest, something) = match subparser(rest) {
        Ok((rest, x)) => (rest, x),
        Err(nom::Err::Error(_)) => {
            // Complain about partial match.
        }
        // That's highly likely a lower-level Failure.
        // Just propagate it.
        Err(e) => return Err(e),
    };

Loosing this Err(e) => return Err(e) match arm periodically costed me countless hours of debugging 🤭

Instead of conclusion

That's probably already too many code snippets for a single article. But I hope it covers the most needed parts to kickstart the parser development with nom.

While working on pq, I faced another interesting parsing problem. PromQL's binary operation grammar seems to be left-recursive. For instance, an addition is defined as <expr> := <exrp> + <expr>, where left-hand- and right-hand sides both in turn can be additions as well. Thus, to define an expression, one needs to define an expression... Implementing such grammar in nom naively immediately leads to an infinite recursion that, of course, results in stack overflow 🤯 But that's a totally different story...

P.S. All the code snippets from the article can be found here.

Level up your server-side game — join 20,000 engineers getting insightful learning materials straight to their inbox: