leqx-macro 0.0.1

Proc macro for leqx (q.v.)
Documentation
// SPDX-License-Identifier: MIT
//! Macro for leqx
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::token::Brace;
use syn::{Attribute, Expr, Receiver, Token, braced, parenthesized, parse_macro_input};

mod kw {
    syn::custom_keyword!(mode);
    syn::custom_keyword!(sparse);
    syn::custom_keyword!(dense);
}

#[derive(Copy, Clone)]
enum DfaType {
    Sparse,
    Dense,
}

struct Pattern {
    // meta: Vec<Attribute>,
    // mode: Vec<syn::Ident>,
    pattern_tok: syn::LitStr,
    expr: syn::Expr,
}

struct Mode {
    vis: syn::Visibility,
    #[allow(dead_code)]
    mode_kw: kw::mode,
    name: syn::Ident,
    embed: bool,
    meta: Vec<Attribute>,
    dfa_type: DfaType,
    receiver: Receiver,
    raw_ident: Option<syn::Ident>,
    typ: syn::Type,
    brace: Brace,
    patterns: Vec<Pattern>,
}

struct Input {
    context: syn::ItemStruct,
    modes: Vec<Mode>,
}

impl Parse for DfaType {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(kw::dense) {
            input.parse::<kw::dense>().map(|_| DfaType::Dense)
        } else if input.peek(kw::sparse) {
            input.parse::<kw::sparse>().map(|_| DfaType::Sparse)
        } else {
            Err(input.error("Invalid DFA type; must be dense or sparse"))
        }
    }
}

impl Parse for Mode {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let meta = input.call(Attribute::parse_outer)?;
        let mut saved_meta = vec![];
        let mut embed = true;
        let mut dfa_type = DfaType::Sparse;
        for attr in meta {
            if !attr.path().is_ident("leqxer") {
                saved_meta.push(attr);
                continue;
            }
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("embed") {
                    embed = meta.value()?.parse::<syn::LitBool>()?.value;
                    Ok(())
                } else if meta.path.is_ident("dfa") {
                    dfa_type = meta.value()?.parse()?;
                    Ok(())
                } else {
                    Err(meta.error("Invalid leqxer option. Valid options are dfa={sparse,dense}, embed={true,false}"))
                }
            })?;
        }
        let vis = input.parse()?;
        let mode_kw = input.parse()?;
        let name = input.parse()?;

        let (receiver, raw_ident) = if input.peek(syn::token::Paren) {
            let content;
            parenthesized!(content in input);

            let receiver = content.parse()?;
            content.parse::<Token![,]>()?;
            (receiver, Some(content.parse()?))
        } else {
            (syn::parse_quote!(&self), None)
        };

        input.parse::<Token![->]>()?;

        let typ = input.parse()?;

        let content;
        let brace = braced!(content in input);
        let mut patterns = vec![];
        while !content.is_empty() {
            patterns.push(content.parse()?)
        }

        Ok(Mode {
            meta: saved_meta,
            vis,
            mode_kw,
            name,
            receiver,
            raw_ident,
            typ,
            brace,
            patterns,
            dfa_type,
            embed,
        })
    }
}

impl Parse for Pattern {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let pattern_tok = <syn::LitStr as Parse>::parse(input)?;
        <Token![=>]>::parse(input)?;
        let expr = syn::Expr::parse(input)?;
        let requires_comma = matches!(
            expr,
            Expr::If(_)
                | Expr::Match(_)
                | Expr::Block(_)
                | Expr::Unsafe(_)
                | Expr::While(_)
                | Expr::Loop(_)
                | Expr::ForLoop(_)
                | Expr::TryBlock(_)
                | Expr::Const(_)
        );

        if (requires_comma && !input.is_empty()) || input.peek(Token![,]) {
            input.parse::<Token![,]>()?;
        }
        Ok(Pattern { pattern_tok, expr })
    }
}

impl Parse for Input {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut result = Input {
            context: input.parse()?,
            modes: vec![],
        };

        while !input.is_empty() {
            result.modes.push(input.parse()?);
        }

        Ok(result)
    }
}

fn leqxer_impl(input: Input) -> Result<TokenStream, syn::Error> {
    let mut mode_fns = proc_macro2::TokenStream::new();
    for mode in input.modes.iter() {
        let mode_span = mode.brace.span.join();
        let input_sym = syn::Ident::new("input", proc_macro2::Span::mixed_site());
        let mut pats = proc_macro2::TokenStream::new();
        let mut pat_strs = vec![];
        let mut matches = proc_macro2::TokenStream::new();
        let extract = if let Some(ref ident) = mode.raw_ident {
            quote! {
                let #ident = std::str::from_utf8(&#input_sym.haystack()[start..end]).unwrap();
            }
        } else {
            quote!()
        };

        for (i, pat) in mode.patterns.iter().enumerate() {
            let re = &pat.pattern_tok;
            let expr = &pat.expr;
            pats.extend(quote! {#re,});
            pat_strs.push(pat.pattern_tok.value());
            matches.extend(quote! {#i => #expr,});
        }

        let (dfa, dfa_ty) = match mode.dfa_type {
            DfaType::Sparse => {
                let dfa = {
                    use regex_automata::dfa::{StartKind, dense::DFA};
                    DFA::builder()
                        .configure(DFA::config().start_kind(StartKind::Anchored))
                        .syntax(::regex_automata::util::syntax::Config::new()
                            .dot_matches_new_line(true))
                        .build_many(pat_strs.as_slice())
                        .and_then(|dfa| dfa.to_sparse())
                        .map_err(|err| syn::Error::new(mode.brace.span.join(), err))?
                };
                eprintln!("mode ({mode_name}) sparse size (le={le}. be={be})",
                    mode_name = mode.name,
                    le = dfa.to_bytes_little_endian().len(),
                    be = dfa.to_bytes_big_endian().len(),
                );
                let construct = if mode.embed {
                    let bstr_le =
                        syn::LitByteStr::new(dfa.to_bytes_little_endian().as_slice(), mode_span);
                    let bstr_be =
                        syn::LitByteStr::new(dfa.to_bytes_big_endian().as_slice(), mode_span);
                    quote! {
                        unsafe{ ::regex_automata::dfa::sparse::DFA::from_bytes_unchecked(if cfg!(target_endian="big") { #bstr_be } else { #bstr_le }).unwrap().0 }
                    }
                } else {
                    quote! {{
                        use regex_automata::dfa::{dense::DFA, StartKind};
                        DFA::builder()
                            .configure(DFA::config().start_kind(StartKind::Anchored))
                            .syntax(::regex_automata::util::syntax::Config::new()
                                .dot_matches_new_line(true))
                            .build_many(pat_strs.as_slice())
                            .and_then(|dfa| dfa.to_sparse())
                            .unwrap()
                    }}
                };
                (construct, quote!{ ::regex_automata::dfa::sparse::DFA<&[u8]> })
            }
            DfaType::Dense => {
                let dfa = {
                    use regex_automata::dfa::{StartKind, dense::DFA};
                    DFA::builder()
                        .configure(DFA::config().start_kind(StartKind::Anchored))
                        .syntax(::regex_automata::util::syntax::Config::new()
                            .dot_matches_new_line(true))
                        .build_many(pat_strs.as_slice())
                        .map_err(|err| syn::Error::new(mode.brace.span.join(), err))
                }?;
                eprintln!("mode ({mode_name}) dense size (le={le}. be={be})",
                          mode_name = mode.name,
                          le = dfa.to_bytes_little_endian().0.len(),
                          be = dfa.to_bytes_big_endian().0.len(),
                );

                let construct = if mode.embed {
                    let bstr_le =
                        syn::LitByteStr::new(dfa.to_bytes_little_endian().0.as_slice(), mode_span);
                    let bstr_be =
                        syn::LitByteStr::new(dfa.to_bytes_big_endian().0.as_slice(), mode_span);
                    quote! {
                        unsafe{ ::regex_automata::dfa::dense::DFA::from_bytes_unchecked(if cfg!(target_endian="big") { #bstr_be } else { #bstr_le }) }.unwrap().0
                    }
                } else {
                    quote! {{
                        use regex_automata::dfa::{dense::DFA, StartKind};
                        DFA::builder()
                            .configure(DFA::config().start_kind(StartKind::Anchored))
                            .syntax(::regex_automata::util::syntax::Config::new()
                                .dot_matches_new_line(true))
                            .build_many(pat_strs.as_slice())
                            .map_err(|err| syn::Error::new(mode.brace.span.join(), err))
                            .unwrap()
                    }}
                };
                (construct, quote!{ ::regex_automata::dfa::dense::DFA<&[u32]> })
            }
        };

        let vis = &mode.vis;
        let mode_name = &mode.name;
        let mode_receiver = &mode.receiver;
        let mode_ty = &mode.typ;
        let mut mode_meta = proc_macro2::TokenStream::new();
        for meta in &mode.meta {
            meta.to_tokens(&mut mode_meta);
        }
        mode_fns.extend(quote! {
            #mode_meta
            #vis fn #mode_name(#mode_receiver, #input_sym: &mut ::leqx::__private::Input) -> Option<#mode_ty> {
                use ::regex_automata::dfa::Automaton;
                static RE_SET: ::leqx::__private::LazyLock<#dfa_ty> = ::leqx::__private::LazyLock::new(|| #dfa);
                #input_sym.set_anchored(::regex_automata::Anchored::Yes);
                let res = RE_SET.try_search_fwd(#input_sym).unwrap()?;
                let start = #input_sym.start();
                let end = res.offset();
                #input_sym.set_start(end);
                #extract
                Some(match res.pattern().as_usize() {
                    #matches
                    _ => unreachable!(),
                })
            }
        });
    }

    let struct_decl = &input.context;
    let (struct_impl, struct_type_params, struct_where) = input.context.generics.split_for_impl();
    let struct_name = &input.context.ident;
    Ok(quote! {
        #struct_decl
        impl #struct_impl #struct_name #struct_type_params #struct_where {
            #mode_fns
        }
    }
    .into())
}

#[proc_macro]
pub fn leqxer(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as Input);
    let result = leqxer_impl(input).unwrap_or_else(|err| err.into_compile_error().into());

    result
}