From 02a3e9b26725c4e1bd90b43ef3dce7b4409998ce Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Mon, 26 May 2025 22:59:17 -0700 Subject: [PATCH] Add grammar! matching on character ranges --- peg_macro/src/lib.rs | 24 +++++++++++++++++++++--- src/peg/peg.rs | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/peg_macro/src/lib.rs b/peg_macro/src/lib.rs index f2500eb..4b644a4 100644 --- a/peg_macro/src/lib.rs +++ b/peg_macro/src/lib.rs @@ -1,7 +1,9 @@ +use std::ops::RangeInclusive; + use proc_macro::TokenStream; use quote::quote; use syn::{ - Ident, LitStr, Token, + Ident, LitChar, LitStr, Token, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, @@ -20,10 +22,11 @@ enum Term { Choice(Vec), Literal(LitStr), Optional(Box), + Plus(Box), + Range(RangeInclusive), Rule(Ident), Sequence(Vec), Star(Box), - Plus(Box), } impl Parse for Grammar { @@ -45,12 +48,21 @@ impl Parse for Rule { impl Parse for Term { fn parse(input: ParseStream) -> syn::Result { + fn parse_range(input: ParseStream) -> syn::Result { + let start = input.parse::()?.value(); + input.parse::()?; + let end = input.parse::()?.value(); + Ok(Term::Range(start..=end)) + } + fn parse_atom(input: ParseStream) -> syn::Result { let look = input.lookahead1(); if look.peek(Ident) { input.parse().map(Term::Rule) } else if look.peek(LitStr) { input.parse().map(Term::Literal) + } else if look.peek(LitChar) { + parse_range(input) } else { Err(look.error()) } @@ -74,7 +86,7 @@ impl Parse for Term { fn parse_sequence(input: ParseStream) -> syn::Result { let mut terms = vec![parse_repeat(input)?]; - while input.peek(Ident) || input.peek(LitStr) { + while input.peek(Ident) || input.peek(LitStr) || input.peek(LitChar) { terms.push(parse_repeat(input)?); } if terms.len() == 1 { @@ -158,6 +170,12 @@ impl Term { } } } + Term::Range(range) => { + let (lo, hi) = (range.start(), range.end()); + quote! { + p.range(#lo..=#hi) + } + } } } } diff --git a/src/peg/peg.rs b/src/peg/peg.rs index 624f14a..723418f 100644 --- a/src/peg/peg.rs +++ b/src/peg/peg.rs @@ -1,3 +1,5 @@ +use std::ops::RangeInclusive; + pub struct ParseState { pub input: String, pub offset: usize, @@ -33,6 +35,16 @@ impl ParseState { false } } + + pub fn range(&mut self, r: RangeInclusive) -> bool { + if let Some(next) = self.input[self.offset..].chars().next() { + if r.contains(&next) { + self.offset += next.len_utf8(); + return true; + } + } + false + } } #[cfg(test)] @@ -49,6 +61,10 @@ mod tests { option = "(" "x"? ")"; plus = "(" "x"+ ")"; star = "(" "x"* ")"; + + word_head = "_" / 'A'..'Z' / 'a'..'z'; + word_tail = word_head / '0'..'9'; + word = "(" word_head word_tail* ")"; } fn parse bool>(rule: T, s: &str) -> bool { @@ -81,4 +97,12 @@ mod tests { 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)")); + } }