Add special matchers, lookahead operators

This commit is contained in:
2025-05-28 00:13:03 -07:00
parent ee5451e2b5
commit df99d78a64
2 changed files with 97 additions and 5 deletions
+62 -2
View File
@@ -1,5 +1,6 @@
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use kw::{ANY, EOI};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{ use syn::{
@@ -20,16 +21,25 @@ struct Rule {
#[derive(Debug)] #[derive(Debug)]
enum Term { enum Term {
AnyChar,
Choice(Vec<Term>), Choice(Vec<Term>),
EOI,
Literal(String), Literal(String),
NegLookahead(Box<Term>),
Optional(Box<Term>), Optional(Box<Term>),
Plus(Box<Term>), Plus(Box<Term>),
PosLookahead(Box<Term>),
Range(RangeInclusive<char>), Range(RangeInclusive<char>),
Rule(Ident), Rule(Ident),
Sequence(Vec<Term>), Sequence(Vec<Term>),
Star(Box<Term>), Star(Box<Term>),
} }
mod kw {
syn::custom_keyword!(ANY);
syn::custom_keyword!(EOI);
}
impl Parse for Grammar { impl Parse for Grammar {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Grammar { Ok(Grammar {
@@ -59,7 +69,13 @@ impl Parse for Term {
fn parse_atom(input: ParseStream) -> syn::Result<Term> { fn parse_atom(input: ParseStream) -> syn::Result<Term> {
let look = input.lookahead1(); let look = input.lookahead1();
if look.peek(Ident) { if look.peek(Ident) {
if input.parse::<ANY>().is_ok() {
Ok(Term::AnyChar)
} else if input.parse::<EOI>().is_ok() {
Ok(Term::EOI)
} else {
input.parse().map(Term::Rule) input.parse().map(Term::Rule)
}
} else if look.peek(LitStr) { } else if look.peek(LitStr) {
input.parse::<LitStr>().map(|x| Term::Literal(x.value())) input.parse::<LitStr>().map(|x| Term::Literal(x.value()))
} else if look.peek(LitChar) { } else if look.peek(LitChar) {
@@ -89,10 +105,20 @@ impl Parse for Term {
Ok(result) Ok(result)
} }
fn parse_lookahead(input: ParseStream) -> syn::Result<Term> {
if input.parse::<Token![!]>().is_ok() {
parse_repeat(input).map(|x| Term::NegLookahead(x.into()))
} else if input.parse::<Token![&]>().is_ok() {
parse_repeat(input).map(|x| Term::PosLookahead(x.into()))
} else {
parse_repeat(input)
}
}
fn parse_sequence(input: ParseStream) -> syn::Result<Term> { fn parse_sequence(input: ParseStream) -> syn::Result<Term> {
let mut terms = vec![parse_repeat(input)?]; let mut terms = vec![parse_lookahead(input)?];
while !input.is_empty() && !input.peek(Token![/]) && !input.peek(Token![;]) { while !input.is_empty() && !input.peek(Token![/]) && !input.peek(Token![;]) {
terms.push(parse_repeat(input)?); terms.push(parse_lookahead(input)?);
} }
if terms.len() == 1 { if terms.len() == 1 {
Ok(terms.pop().unwrap()) Ok(terms.pop().unwrap())
@@ -121,6 +147,12 @@ impl Parse for Term {
impl Term { impl Term {
fn generate_code(&self) -> proc_macro2::TokenStream { fn generate_code(&self) -> proc_macro2::TokenStream {
match self { match self {
Term::AnyChar => quote! {
p.any()
},
Term::EOI => quote! {
p.eoi()
},
Term::Rule(ident) => quote! { Term::Rule(ident) => quote! {
#ident(p) #ident(p)
}, },
@@ -186,6 +218,34 @@ impl Term {
p.range(#lo..=#hi) 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
}
}
}
}
} }
} }
} }
+34 -2
View File
@@ -45,6 +45,19 @@ impl ParseState {
} }
false 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)] #[cfg(test)]
@@ -54,7 +67,7 @@ mod tests {
use super::*; use super::*;
grammar! { grammar! {
fake_csv = line "\n" line; fake_csv = line "\n" line EOI;
line = item "," item; line = item "," item;
item = "foo" / "bar"; item = "foo" / "bar";
@@ -69,11 +82,15 @@ mod tests {
words = "(" (plain_word ("," plain_word)*)? ")"; words = "(" (plain_word ("," plain_word)*)? ")";
nested_choice = "(" ("a" / "b") ("c" / "d") ")"; nested_choice = "(" ("a" / "b") ("c" / "d") ")";
dq = "\"";
quoted = dq ("\\" ANY / !dq ANY)* dq;
} }
fn parse<T: Fn(&mut ParseState) -> bool>(rule: T, s: &str) -> bool { fn parse<T: Fn(&mut ParseState) -> bool>(rule: T, s: &str) -> bool {
println!("About to parse: {}", s);
let mut p = ParseState::new(s); let mut p = ParseState::new(s);
rule(&mut p) rule(&mut p) && p.eoi()
} }
#[test] #[test]
@@ -82,6 +99,8 @@ mod tests {
assert!(fake_csv(&mut p)); assert!(fake_csv(&mut p));
let mut p = ParseState::new("foo,foo\nfoo;foo"); let mut p = ParseState::new("foo,foo\nfoo;foo");
assert!(!fake_csv(&mut p)); assert!(!fake_csv(&mut p));
let mut p = ParseState::new("foo,foo\nfoo,foo\njunk at end");
assert!(!fake_csv(&mut p));
} }
#[test] #[test]
@@ -131,4 +150,17 @@ mod tests {
assert!(!parse(nested_choice, "(c)")); assert!(!parse(nested_choice, "(c)"));
assert!(!parse(nested_choice, "(d)")); 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 \""#));
}
} }