Parse Anneal attributes using `chumsky`
- 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
Assessment
This issue has not been assessed yet.