From df99d78a64df159d947e2619a10573868727bc42 Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Wed, 28 May 2025 00:13:03 -0700 Subject: [PATCH] Add special matchers, lookahead operators --- peg_macro/src/lib.rs | 66 ++++++++++++++++++++++++++++++++++++++++++-- src/peg/peg.rs | 36 ++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/peg_macro/src/lib.rs b/peg_macro/src/lib.rs index 493386f..d2ea7fc 100644 --- a/peg_macro/src/lib.rs +++ b/peg_macro/src/lib.rs @@ -1,5 +1,6 @@ use std::ops::RangeInclusive; +use kw::{ANY, EOI}; use proc_macro::TokenStream; use quote::quote; use syn::{ @@ -20,16 +21,25 @@ struct Rule { #[derive(Debug)] enum Term { + AnyChar, Choice(Vec), + EOI, Literal(String), + NegLookahead(Box), Optional(Box), Plus(Box), + PosLookahead(Box), Range(RangeInclusive), Rule(Ident), Sequence(Vec), Star(Box), } +mod kw { + syn::custom_keyword!(ANY); + syn::custom_keyword!(EOI); +} + impl Parse for Grammar { fn parse(input: ParseStream) -> syn::Result { Ok(Grammar { @@ -59,7 +69,13 @@ impl Parse for Term { fn parse_atom(input: ParseStream) -> syn::Result { let look = input.lookahead1(); if look.peek(Ident) { - input.parse().map(Term::Rule) + if input.parse::().is_ok() { + Ok(Term::AnyChar) + } else if input.parse::().is_ok() { + Ok(Term::EOI) + } else { + input.parse().map(Term::Rule) + } } else if look.peek(LitStr) { input.parse::().map(|x| Term::Literal(x.value())) } else if look.peek(LitChar) { @@ -89,10 +105,20 @@ impl Parse for Term { Ok(result) } + fn parse_lookahead(input: ParseStream) -> syn::Result { + if input.parse::().is_ok() { + parse_repeat(input).map(|x| Term::NegLookahead(x.into())) + } else if input.parse::().is_ok() { + parse_repeat(input).map(|x| Term::PosLookahead(x.into())) + } else { + parse_repeat(input) + } + } + fn parse_sequence(input: ParseStream) -> syn::Result { - let mut terms = vec![parse_repeat(input)?]; + let mut terms = vec![parse_lookahead(input)?]; while !input.is_empty() && !input.peek(Token![/]) && !input.peek(Token![;]) { - terms.push(parse_repeat(input)?); + terms.push(parse_lookahead(input)?); } if terms.len() == 1 { Ok(terms.pop().unwrap()) @@ -121,6 +147,12 @@ impl Parse for Term { impl Term { fn generate_code(&self) -> proc_macro2::TokenStream { match self { + Term::AnyChar => quote! { + p.any() + }, + Term::EOI => quote! { + p.eoi() + }, Term::Rule(ident) => quote! { #ident(p) }, @@ -186,6 +218,34 @@ impl Term { p.range(#lo..=#hi) } } + Term::NegLookahead(term) => { + let code = term.generate_code(); + quote! { + { + let save = p.save(); + if #code { + p.restore(save); + false + } else { + true + } + } + } + } + Term::PosLookahead(term) => { + let code = term.generate_code(); + quote! { + { + let save = p.save(); + if #code { + p.restore(save); + true + } else { + false + } + } + } + } } } } diff --git a/src/peg/peg.rs b/src/peg/peg.rs index 1716457..ce8be35 100644 --- a/src/peg/peg.rs +++ b/src/peg/peg.rs @@ -45,6 +45,19 @@ impl ParseState { } false } + + pub fn any(&mut self) -> bool { + if let Some(c) = self.input[self.offset..].chars().next() { + self.offset += c.len_utf8(); + true + } else { + false + } + } + + pub fn eoi(&self) -> bool { + self.offset >= self.input.len() + } } #[cfg(test)] @@ -54,7 +67,7 @@ mod tests { use super::*; grammar! { - fake_csv = line "\n" line; + fake_csv = line "\n" line EOI; line = item "," item; item = "foo" / "bar"; @@ -69,11 +82,15 @@ mod tests { words = "(" (plain_word ("," plain_word)*)? ")"; nested_choice = "(" ("a" / "b") ("c" / "d") ")"; + + dq = "\""; + quoted = dq ("\\" ANY / !dq ANY)* dq; } fn parse bool>(rule: T, s: &str) -> bool { + println!("About to parse: {}", s); let mut p = ParseState::new(s); - rule(&mut p) + rule(&mut p) && p.eoi() } #[test] @@ -82,6 +99,8 @@ mod tests { 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] @@ -131,4 +150,17 @@ mod tests { 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#""no end"#)); + assert!(!parse(quoted, r#""foo " bar""#)); + assert!(!parse(quoted, r#""false end \""#)); + } }