google / google/zerocopy

Parse Anneal attributes using `chumsky`

Open
#3,086 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2.6k
Forks
179
Avg merge
1d 19h
Merged PRs (30d)
29

Description

Use a parsing framework instead of a hand-rolled parser.

Here's a prototype parser using `chumsky`:

```rust
use chumsky::{inspector::SimpleState, prelude::*};

// --- AST Definitions ---

#[derive(Debug, Clone, PartialEq)]
pub enum HeaderType {
Requires,
Ensures,
Proof,
}

#[derive(Debug, Clone, PartialEq)]
pub enum SectionHeader<'a> {
/// `foo:` or `foo(bar):`
Header { typ: HeaderType, ident: Option<&'a str> },
/// `isValid foo :=`
IsValid { ident: &'a str },
/// `isSafe :`
IsSafe,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Section<'a> {
pub header: SectionHeader<'a>,
pub content: &'a str,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Block<'a> {
pub doc_strings: &'a str,
pub sections: Vec>,
}

// --- Types ---
type ParserError<'a> = extra::Full, SimpleState, ()>;

// --- Parsers ---

/// Consumes zero or more inline spaces
fn inline_ws<'a>() -> impl Parser<'a, &'a str, (), ParserError<'a>> {
just(' ').repeated().ignored()
}

fn ident<'a>() -> impl Parser<'a, &'a str, &'a str, ParserError<'a>> {
any()
.filter(|c: &char| c.is_alphanumeric() || *c == '_' || *c == '\'')
.repeated()
.at_least(1)
.to_slice()
}

fn keyword<'a>() -> impl Parser<'a, &'a str, HeaderType, ParserError<'a>> {
any().filter(|c: &char| c.is_ascii_alphabetic()).repeated().at_least(1).to_slice().try_map(
|s: &str, span| match s {
"requires" => Ok(HeaderType::Requires),
"ensures" => Ok(HeaderType::Ensures),
"proof" => Ok(HeaderType::Proof),
_ => Err(Rich::custom(span, format!("Unknown header type: '{}'", s))),
},
)
}

/// Parses the section headers at exactly 1-level indentation
fn header<'a>() -> impl Parser<'a, &'a str, SectionHeader<'a>, ParserError<'a>> {
let is_valid = just("isValid")
.then_ignore(inline_ws())
.ignore_then(ident()) // Drops "isValid", keeps ident
.then_ignore(inline_ws())
.then_ignore(just(":="))
.map(|ident| SectionHeader::IsValid { ident });

let is_safe = just("isSafe")
.then_ignore(inline_ws())
.then_ignore(just(':'))
.map(|_| SectionHeader::IsSafe);

let kw_with_id = keyword()
.then_ignore(inline_ws())
.then_ignore(just('('))
.then_ignore(inline_ws())
.then(ident()) // Combines kw and ident into a tuple
.then_ignore(inline_ws())
.then_ignore(just(')'))
.then_ignore(inline_ws())
.then_ignore(just(':'))
.map(|(kw, id)| SectionHeader::Header { typ: kw, ident: Some(id) });

let kw_only = keyword()
.then_ignore(inline_ws())
.then_ignore(just(':'))
.map(|kw| SectionHeader::Header { typ: kw, ident: None });

// Require exactly 1 space of indentation (doc comment base)
just(' ').ignore_then(choice((is_valid, kw_with_id, kw_only)))
}

/// Parses a single line of section content.
fn content_line<'a>() -> impl Parser<'a, &'a str, String, ParserError<'a>> {
// Indented line starts with 2 spaces (1 for doc comment, 1 for section indent)
let indented = just(" ")
.ignore_then(none_of("\r\n").repeated().to_slice())
.then(just('\r').or_not())
.then(just('\n').or_not())
.map(|((text, cr), nl)| {
// We reconstruct the string, preserving the base 2-space indentation
let mut s = format!(" {}", text);
if cr.is_some() {
s.push('\r');
}
if nl.is_some() {
s.push('\n');
}
s
});

// Empty line: optional single doc comment space, then newline
let empty = just(' ')
.or_not()
.ignore_then(just('\r').or_not())
.then(just('\n'))
.map(|(cr, _)| if cr.is_some() { "\r\n".to_string() } else { "\n".to_string() });

choice((indented, empty))
}

fn section<'a>() -> impl Parser<'a, &'a str, Section<'a>, ParserError<'a>> {
header()
.then_ignore(inline_ws())
.then(
none_of("\r\n")
.repeated()
.ignored()
.then_ignore(just('\r').or_not().then(just('\n')))
.then_ignore(content_line().repeated())
.to_slice(),
)
.map(|(hdr, content)| Section { header: hdr, content })
}

pub fn block_parser<'a>() -> impl Parser<'a, &'a str, Block<'a>, ParserError<'a>> {
// Dynamically count opening backticks and store in state
let open_backticks = just('`').repeated().at_least(3).count().map_with(|count, e| {
*e.state() = SimpleState(count);
count
});

// Read state and validate closing backticks match exactly
let close_backticks = just('`').repeated().count().try_map_with(|count, e| {
let state: &mut SimpleState = e.state();
let state: usize = state.0;
if count == state {
Ok(count)
} else {
Err(Rich::custom(e.span(), format!("Expected {} backticks", state)))
}
});

let doc_strings =
none_of("\r\n").repeated().to_slice().then_ignore(just('\r').or_not().then(just('\n')));

open_backticks
.ignore_then(doc_strings)
.then(section().repeated().collect::>())
.then_ignore(close_backticks)
.map(|(doc_strings, sections)| Block { doc_strings, sections })
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.