Files
marzipan/src/peg/peg.rs
T

425 lines
12 KiB
Rust

use std::{num::NonZero, ops::Range};
pub struct ParseState<T: Clone> {
pub input: String,
pub offset: usize,
captures: Vec<RawCapture<T>>,
}
pub struct Captures<'a, T: Clone> {
input: &'a str,
raw: &'a [RawCapture<T>],
index: usize,
walk: bool,
}
pub struct Capture<'a, T: Clone> {
input: &'a str,
raw: &'a [RawCapture<T>],
}
#[derive(Debug)]
struct RawCapture<T: Clone> {
kind: T,
span: Range<usize>,
subtree_len: Option<NonZero<usize>>,
}
impl<T: Clone> ParseState<T> {
pub fn new(s: &str) -> Self {
Self {
input: s.into(),
offset: 0,
captures: vec![],
}
}
pub fn captures<'a>(&'a self) -> Captures<'a, T> {
Captures {
input: &self.input,
raw: &self.captures,
index: 0,
walk: false,
}
}
pub fn walk_captures<'a>(&'a self) -> Captures<'a, T> {
let mut result = self.captures();
result.walk = true;
result
}
}
pub mod backend {
use std::{num::NonZero, ops::RangeInclusive};
use super::{ParseState, RawCapture};
pub struct Savepoint {
offset: usize,
captures_len: usize,
}
pub trait LowLevel<T: Clone> {
fn any(&mut self) -> bool;
fn begin_capture(&mut self, tag: T) -> Savepoint;
fn commit_capture(&mut self, start: Savepoint);
fn eoi(&self) -> bool;
fn literal(&mut self, s: &str) -> bool;
fn literal_i(&mut self, s: &str) -> bool;
fn range(&mut self, r: RangeInclusive<char>) -> bool;
fn range_i(&mut self, r: RangeInclusive<char>) -> bool;
fn restore(&mut self, save: Savepoint);
fn save(&self) -> Savepoint;
}
impl<T: Clone> LowLevel<T> for ParseState<T> {
fn any(&mut self) -> bool {
if let Some(c) = self.input[self.offset..].chars().next() {
self.offset += c.len_utf8();
true
} else {
false
}
}
fn begin_capture(&mut self, tag: T) -> Savepoint {
let result = self.save();
self.captures.push(RawCapture {
kind: tag,
span: 0..0,
subtree_len: None,
});
result
}
fn commit_capture(&mut self, start: Savepoint) {
let index = start.captures_len;
assert_eq!(self.captures[index].subtree_len, None);
let subtree_len = self.captures.len() - index;
self.captures[index].span = start.offset..self.offset;
self.captures[index].subtree_len = NonZero::new(subtree_len);
}
fn eoi(&self) -> bool {
self.offset >= self.input.len()
}
fn literal(&mut self, s: &str) -> bool {
if self.input[self.offset..].starts_with(s) {
self.offset += s.len();
true
} else {
false
}
}
fn literal_i(&mut self, s: &str) -> bool {
let range = self.offset..(self.offset + s.len());
if self.input.is_char_boundary(range.end) && self.input[range].eq_ignore_ascii_case(s) {
self.offset += s.len();
true
} else {
false
}
}
fn range(&mut self, r: RangeInclusive<char>) -> bool {
if let Some(next) = self.input[self.offset..].chars().next() {
if r.contains(&next) {
self.offset += next.len_utf8();
return true;
}
}
false
}
fn range_i(&mut self, r: RangeInclusive<char>) -> bool {
if let Some(next) = self.input[self.offset..].chars().next() {
if r.contains(&next.to_ascii_lowercase()) || r.contains(&next.to_ascii_uppercase())
{
self.offset += next.len_utf8();
return true;
}
}
false
}
fn restore(&mut self, save: Savepoint) {
self.offset = save.offset;
self.captures.truncate(save.captures_len);
}
fn save(&self) -> Savepoint {
Savepoint {
offset: self.offset,
captures_len: self.captures.len(),
}
}
}
}
impl<'a, T: Clone> Iterator for Captures<'a, T> {
type Item = Capture<'a, T>;
fn next(&mut self) -> Option<Self::Item> {
let head = self.raw.get(self.index)?;
let subtree_len = head.subtree_len.unwrap().get();
let subtree_slice = &self.raw[self.index..self.index + subtree_len];
self.index += if self.walk { 1 } else { subtree_len };
Some(Capture {
input: &self.input,
raw: subtree_slice,
})
}
}
impl<'a, T: Clone> Capture<'a, T> {
pub fn children(&self) -> Captures<'a, T> {
Captures {
input: &self.input,
raw: &self.raw[1..],
index: 0,
walk: false,
}
}
pub fn walk_children(&self) -> Captures<'a, T> {
let mut result = self.children();
result.walk = true;
result
}
pub fn kind(&self) -> T {
self.raw[0].kind.clone()
}
pub fn span(&self) -> Range<usize> {
self.raw[0].span.clone()
}
pub fn text(&self) -> &'a str {
&self.input[self.span()]
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use mzp_peg_macro::grammar;
use super::ParseState;
grammar! {
fake_csv = line "\n" line EOI;
line = item "," item;
item = "foo" / "bar";
option = "(" "x"? ")";
plus = "(" "x"+ ")";
star = "(" "x"* ")";
word_head = "_" / 'A'..'Z' / 'a'..'z';
word_tail = word_head / '0'..'9';
plain_word = word_head word_tail*;
word = "(" plain_word ")";
words = "(" (plain_word ("," plain_word)*)? ")";
nested_choice = "(" ("a" / "b") ("c" / "d") ")";
dq = "\"";
quoted = dq ("\\" ANY / !dq ANY)* dq;
case_sensitivity = "Strict " ('a'..'z')+ ", loose "i ('a'..'z'i)+;
long_icase_str = "foo bar baz"i;
@icase
hex_config = "let " var_name " = 0x" ('a'..'f' / '0'..'9')+;
var_name = "foo" / "bar"; // case must match
@icase
may_contain_foo = ("foo" / ANY)+;
email_text = (!email ANY / email)*;
email = #Email:(#User:user "@" #Domain:domain);
user = ('a'..'z'i)+;
domain = user+ ("." user)+;
}
fn parse<C: Clone, T: Fn(&mut ParseState<C>) -> bool>(rule: T, s: &str) -> bool {
use super::backend::LowLevel;
println!("About to parse: {}", s);
let mut p = ParseState::new(s);
rule(&mut p) && p.eoi()
}
#[test]
fn test_generated() {
let mut p = ParseState::new("foo,bar\nbar,foo");
assert!(fake_csv(&mut p));
let mut p = ParseState::new("foo,foo\nfoo;foo");
assert!(!fake_csv(&mut p));
let mut p = ParseState::new("foo,foo\nfoo,foo\njunk at end");
assert!(!fake_csv(&mut p));
}
#[test]
fn test_repetition_suffixes() {
let zero = "()";
assert!(parse(option, zero));
assert!(parse(star, zero));
assert!(!parse(plus, zero));
let one = "(x)";
assert!(parse(option, one));
assert!(parse(star, one));
assert!(parse(plus, one));
let many = "(xxx)";
assert!(!parse(option, many));
assert!(parse(star, many));
assert!(parse(plus, many));
}
#[test]
fn test_ranges() {
assert!(parse(word, "(_FooBar)"));
assert!(parse(word, "(abc123)"));
assert!(!parse(word, "(123abc)"));
assert!(!parse(word, "(foo-bar)"));
}
#[test]
fn test_groups() {
assert!(parse(words, "()"));
assert!(parse(words, "(foo)"));
assert!(parse(words, "(foo,bar,baz)"));
assert!(!parse(words, "(foo,)"));
assert!(!parse(words, "(,bar)"));
assert!(!parse(words, "(3baz)"));
}
#[test]
fn test_nested_choice() {
assert!(parse(nested_choice, "(ac)"));
assert!(parse(nested_choice, "(ad)"));
assert!(parse(nested_choice, "(bc)"));
assert!(parse(nested_choice, "(bd)"));
assert!(!parse(nested_choice, "(a)"));
assert!(!parse(nested_choice, "(b)"));
assert!(!parse(nested_choice, "(c)"));
assert!(!parse(nested_choice, "(d)"));
}
#[test]
fn test_quoted() {
assert!(parse(quoted, r#""""#));
assert!(parse(quoted, r#""foo""#));
assert!(parse(quoted, r#""\"""#));
assert!(parse(quoted, r#""foo \"bar\" baz""#));
assert!(parse(quoted, r#""C:\\>""#));
assert!(parse(quoted, r#""ñon-äscii""#));
assert!(!parse(quoted, r#""no end"#));
assert!(!parse(quoted, r#""foo " bar""#));
assert!(!parse(quoted, r#""false end \""#));
}
#[test]
fn test_i_suffix() {
assert!(parse(case_sensitivity, "Strict abc, loose xyz"));
assert!(!parse(case_sensitivity, "STRICT abc, loose xyz"));
assert!(!parse(case_sensitivity, "Strict ABC, loose xyz"));
assert!(parse(case_sensitivity, "Strict abc, LOOSE xyz"));
assert!(parse(case_sensitivity, "Strict abc, loose XYZ"));
assert!(parse(case_sensitivity, "Strict abc, LoOsE XyZ"));
}
#[test]
fn test_icase_decorator() {
assert!(parse(hex_config, "let foo = 0xc0ffee"));
assert!(parse(hex_config, "LET bar = 0XCAFE"));
assert!(!parse(hex_config, "let Foo = 0xc0ffee"));
assert!(!parse(hex_config, "let BAR = 0xcafe"));
}
#[test]
fn test_icase_eoi() {
assert!(parse(long_icase_str, "FOO BAR BAZ"));
assert!(!parse(long_icase_str, "FOO BAR BA"));
}
#[test]
fn test_icase_literal_multibyte() {
// Testing failed matches of a icase literal against multibyte characters,
// where the end of the literal doesn't fall on a character boundary.
// First, make sure the rule works for ASCII input:
assert!(parse(may_contain_foo, "==foo=="));
assert!(parse(may_contain_foo, "==bar=="));
// Then force a comparison where a 3-byte slice would split a multibyte char:
assert!(parse(may_contain_foo, "....♥")); // dots force an eventual misalignment
}
#[test]
fn test_captures() {
let mut p = ParseState::new("alice@example.com");
assert!(email(&mut p));
let mut captures = vec![];
for email in p.captures() {
captures.push((email.kind(), email.text()));
for part in email.children() {
captures.push((part.kind(), part.text()));
}
}
assert_debug_snapshot!(captures, @r#"
[
(
Email,
"alice@example.com",
),
(
User,
"alice",
),
(
Domain,
"example.com",
),
]
"#);
}
#[test]
fn test_walk_captures() {
let mut p = ParseState::new("Contact alice@foo.com or bob@bar.net.");
assert!(email_text(&mut p));
let extract_by_tag = |tag| {
p.walk_captures()
.filter_map(|c| {
if c.kind() == tag {
Some(c.text())
} else {
None
}
})
.collect::<Vec<_>>()
};
let users = extract_by_tag(Tag::User);
let domains = extract_by_tag(Tag::Domain);
assert_debug_snapshot!(users, @r#"
[
"alice",
"bob",
]
"#);
assert_debug_snapshot!(domains, @r#"
[
"foo.com",
"bar.net",
]
"#);
}
}