Skip to main content

scnr2_generate/
pattern.rs

1//! A pattern as a data structure that is used during the construction of the NFA.
2//! It contains the pattern string and the associated metadata.
3//! Metadata includes the terminal type and a possibly empty lookahead constraint.
4use crate::{
5    Result,
6    dfa::{Dfa, DfaWithNumberOfCharacterClasses},
7    ids::{TerminalID, TerminalIDBase},
8    nfa::Nfa,
9};
10use proc_macro2::TokenStream;
11use quote::{ToTokens, quote};
12
13macro_rules! parse_ident {
14    ($input:ident, $name:ident) => {
15        $input.parse().map_err(|e| {
16            syn::Error::new(
17                e.span(),
18                concat!("expected identifier `", stringify!($name), "`"),
19            )
20        })?
21    };
22}
23
24/// The type of the automaton, which can be either an NFA or a DFA.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AutomatonType {
27    /// The NFA type of the automaton.
28    Nfa(Nfa),
29    /// The DFA type of the automaton.
30    Dfa(Dfa),
31}
32
33/// The lookahead constraint is used to ensure that the pattern matches only if it is followed by a
34/// specific regex pattern, a so called positive lookahead. It is also possible to demand that the
35/// pattern is not followed by a specific regex pattern. In this case the lookahead is negative.
36#[derive(Debug, Default, Clone, PartialEq, Eq)]
37pub enum Lookahead {
38    /// No lookahead constraint is applied. This is used when no specific lookahead is required.
39    #[default]
40    None,
41    /// A positive lookahead constraint that requires the pattern to be followed by a specific regex
42    /// pattern.
43    Positive(AutomatonType),
44    /// A negative lookahead constraint that requires the pattern to not be followed by a specific
45    /// regex pattern.
46    Negative(AutomatonType),
47}
48
49impl Lookahead {
50    /// Creates a new positive lookahead constraint with the given regex pattern.
51    ///
52    /// # Arguments
53    /// * `pattern` - The regex pattern that must follow the main pattern.
54    pub fn positive(pattern: String) -> Result<Self> {
55        // Convert the string pattern into an NFA.
56        // The `usize::MAX` is used to indicate that the pattern has no associated terminal type.
57        let nfa = Nfa::build(&Pattern::new(pattern, TerminalIDBase::MAX.into()))
58            .map_err(|e| format!("Failed to create NFA from regex pattern: {e}"))?;
59        Ok(Lookahead::Positive(AutomatonType::Nfa(nfa)))
60    }
61
62    /// Creates a new negative lookahead constraint with the given regex pattern.
63    ///
64    /// # Arguments
65    /// * `pattern` - The regex pattern that must not follow the main pattern.
66    pub fn negative(pattern: String) -> Result<Self> {
67        // Convert the string pattern into an NFA.
68        // The `usize::MAX` is used to indicate that the pattern has no associated terminal type.
69        let nfa = Nfa::build(&Pattern::new(pattern, TerminalIDBase::MAX.into()))
70            .map_err(|e| format!("Failed to create NFA from regex pattern: {e}"))?;
71        Ok(Lookahead::Negative(AutomatonType::Nfa(nfa)))
72    }
73}
74
75/// This is used to create a lookahead from a part of a macro input.
76/// The macro input looks like this:
77/// ```text
78/// followed by r"!";
79/// ```
80/// for positive lookahead
81/// or
82/// ```text
83/// not followed by r"!";
84/// ```
85/// for negative lookahead.
86impl syn::parse::Parse for Lookahead {
87    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
88        let followed_or_not: syn::Ident = parse_ident!(input, followed_or_not);
89        if followed_or_not != "followed" && followed_or_not != "not" {
90            return Err(input.error("expected 'followed' or 'not'"));
91        }
92        let mut is_positive = true;
93        if followed_or_not == "not" {
94            is_positive = false;
95            let followed: syn::Ident = parse_ident!(input, followed);
96            if followed != "followed" {
97                return Err(input.error("expected 'followed'"));
98            }
99        }
100        // Otherwise followed_or_not is "followed" and we are in the positive case.
101        // Now we have to parse the "by" keyword.
102        let by: syn::Ident = parse_ident!(input, by);
103        if by != "by" {
104            return Err(input.error("expected 'by'"));
105        }
106        // And finally the pattern.
107        let pattern: syn::LitStr = input.parse().map_err(|e| {
108            syn::Error::new(
109                e.span(),
110                "expected a string literal for the lookahead pattern",
111            )
112        })?;
113        let pattern = pattern.value();
114        Ok(if is_positive {
115            Lookahead::positive(pattern).map_err(|e| {
116                syn::Error::new(
117                    input.span(),
118                    format!("Failed to create positive lookahead: {e}"),
119                )
120            })?
121        } else {
122            Lookahead::negative(pattern).map_err(|e| {
123                syn::Error::new(
124                    input.span(),
125                    format!("Failed to create negative lookahead: {e}"),
126                )
127            })?
128        })
129    }
130}
131
132#[derive(Debug)]
133pub(crate) struct LookaheadWithNumberOfCharacterClasses {
134    /// The lookahead constraint.
135    pub lookahead: Lookahead,
136    /// The number of character classes in the lookahead automaton.
137    pub character_classes: usize,
138}
139
140impl LookaheadWithNumberOfCharacterClasses {
141    /// Creates a new `LookaheadWithNumberOfCharacterClasses` with the given lookahead and character
142    /// classes.
143    pub fn new(lookahead: Lookahead, character_classes: usize) -> Self {
144        Self {
145            lookahead,
146            character_classes,
147        }
148    }
149}
150
151impl ToTokens for LookaheadWithNumberOfCharacterClasses {
152    fn to_tokens(&self, tokens: &mut TokenStream) {
153        let lookahead_tokens = match &self.lookahead {
154            Lookahead::None => quote! { Lookahead::None },
155            Lookahead::Positive(AutomatonType::Dfa(dfa)) => {
156                let dfa_with_classes =
157                    DfaWithNumberOfCharacterClasses::new(dfa, self.character_classes);
158                quote! { Lookahead::Positive(#dfa_with_classes) }
159            }
160            Lookahead::Negative(AutomatonType::Dfa(dfa)) => {
161                let dfa_with_classes =
162                    DfaWithNumberOfCharacterClasses::new(dfa, self.character_classes);
163                quote! { Lookahead::Negative(#dfa_with_classes) }
164            }
165            _ => panic!("Unexpected lookahead type in Lookahead: {self:?}"),
166        };
167        tokens.extend(lookahead_tokens);
168    }
169}
170
171/// A pattern is a data structure that is used during the construction of the NFA.
172#[derive(Debug, Default, Clone, PartialEq, Eq)]
173pub struct Pattern {
174    /// The pattern string that is used to match input tokens.
175    pub pattern: String,
176    /// The terminal type associated with the pattern.
177    pub terminal_type: TerminalID,
178    /// The priority of the pattern, used to resolve conflicts between patterns.
179    /// Patterns with priority lower value are preferred over those with higher priority value.
180    pub priority: usize,
181    /// The lookahead constraint for the pattern, which can be positive, negative, or none.
182    pub lookahead: Lookahead,
183}
184
185impl Pattern {
186    /// Creates a new pattern with the given pattern string, terminal type, and optional lookahead.
187    ///
188    /// # Arguments
189    /// * `pattern` - The pattern string.
190    /// * `terminal_type` - The terminal type associated with the pattern.
191    pub fn new(pattern: String, terminal_type: TerminalID) -> Self {
192        const DEFAULT_PRIORITY: usize = 0;
193        Self {
194            pattern,
195            terminal_type,
196            priority: DEFAULT_PRIORITY,
197            lookahead: Lookahead::None,
198        }
199    }
200
201    /// Sets the lookahead constraint for the pattern while consuming the current pattern.
202    /// # Arguments
203    /// * `lookahead` - The lookahead constraint to set.
204    pub fn with_lookahead(mut self, lookahead: Lookahead) -> Self {
205        self.lookahead = lookahead;
206        self
207    }
208
209    /// Sets the priority of the pattern.
210    /// # Arguments
211    /// * `priority` - The priority to set.
212    pub fn with_priority(mut self, priority: usize) -> Self {
213        self.priority = priority;
214        self
215    }
216}
217
218/// This is used to create a pattern from a part of a macro input.
219/// The macro input looks like this:
220/// ```text
221/// token r"World" followed by r"!" => 11;
222/// ```
223/// where the lookahead part can be either
224/// ```text
225/// followed by r"!";
226/// ```text
227/// or
228/// ```text
229/// not followed by r"!";
230/// ```text
231/// or it can be omitted completely.
232///
233/// The lookahead part should be parsed with the help of the `Lookahead` struct's `parse` method.
234///
235/// Note that the `token` keyword is not part of the pattern, but it is used to identify the
236/// pattern.
237impl syn::parse::Parse for Pattern {
238    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
239        let pattern: syn::LitStr = input.parse().map_err(|e| {
240            syn::Error::new(
241                e.span(),
242                format!("expected a string literal for the pattern: {input:?}"),
243            )
244        })?;
245        let pattern = pattern.value();
246        let mut lookahead: Option<Lookahead> = None;
247        // Check if there is a lookahead and parse it.
248        if input.peek(syn::Ident) {
249            // The parse implementation of the Lookahead struct will check if the ident is
250            // `followed` or `not`.
251            // If it is neither, it will return an error.
252            lookahead = Some(input.parse()?);
253        }
254        input.parse::<syn::Token![=>]>()?;
255        let token_type: syn::LitInt = input.parse()?;
256        let token_type: TerminalIDBase = token_type.base10_parse()?;
257        let mut pattern = Pattern::new(pattern, token_type.into());
258        // Parse the semicolon at the end of the pattern.
259        if input.peek(syn::Token![;]) {
260            input.parse::<syn::Token![;]>()?;
261        } else {
262            return Err(input.error("expected ';'"));
263        }
264
265        // If a lookahead was parsed, set it on the pattern.
266        let lookahead = lookahead.unwrap_or(Lookahead::None);
267        pattern = pattern.with_lookahead(lookahead);
268
269        Ok(pattern)
270    }
271}
272
273#[derive(Debug)]
274pub(crate) struct PatternWithNumberOfCharacterClasses<'a> {
275    /// The pattern itself.
276    pub pattern: &'a Pattern,
277    /// The number of character classes in the pattern.
278    pub character_classes: usize,
279}
280
281impl<'a> PatternWithNumberOfCharacterClasses<'a> {
282    /// Creates a new `PatternWithNumberOfCharacterClasses` with the given pattern and character
283    /// classes.
284    pub fn new(pattern: &'a Pattern, character_classes: usize) -> Self {
285        Self {
286            pattern,
287            character_classes,
288        }
289    }
290}
291
292impl ToTokens for PatternWithNumberOfCharacterClasses<'_> {
293    fn to_tokens(&self, tokens: &mut TokenStream) {
294        let PatternWithNumberOfCharacterClasses {
295            pattern,
296            character_classes,
297        } = self;
298        let terminal_type = pattern.terminal_type.as_usize().to_token_stream();
299        let priority = pattern.priority.to_token_stream();
300        let lookahead_with_number_of_character_classes = LookaheadWithNumberOfCharacterClasses::new(
301            pattern.lookahead.clone(),
302            *character_classes,
303        );
304        tokens.extend(quote! {
305            AcceptData {
306                token_type: #terminal_type,
307                priority: #priority,
308                lookahead: #lookahead_with_number_of_character_classes,
309            }
310        });
311    }
312}