use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::parse2;
use crate::{
character_classes::CharacterClasses,
dfa::{Dfa, DfaStateWithNumberOfCharacterClasses},
nfa::Nfa,
scanner_data::{ScannerData, TransitionToNumericMode},
scanner_mode::ScannerMode,
};
pub fn generate(input: TokenStream) -> TokenStream {
let scanner_data: ScannerData = parse2(input).expect("Failed to parse input");
let scanner_modes: Vec<ScannerMode> = scanner_data
.build_scanner_modes()
.expect("Failed to build scanner modes");
let mut nfas = scanner_modes
.iter()
.map(|mode| {
Nfa::build_from_patterns(&mode.patterns).expect("Failed to build NFA for pattern")
})
.collect::<Vec<_>>();
let mut character_classes = CharacterClasses::new();
for nfa in &nfas {
nfa.collect_character_classes(&mut character_classes)
}
character_classes.create_disjoint_character_classes();
for nfa in &mut nfas {
nfa.convert_to_disjoint_character_classes(&character_classes);
}
let dfas = nfas
.into_iter()
.try_fold(Vec::new(), |mut acc, nfa| -> Result<Vec<Dfa>, syn::Error> {
let dfa = Dfa::try_from(&nfa).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to convert NFA to DFA: {e}"),
)
})?;
acc.push(dfa);
Ok(acc)
})
.expect("Failed to convert NFAs to DFAs");
let module_name = to_snake_case(&scanner_data.name);
let module_name_ident = syn::Ident::new(&module_name, proc_macro2::Span::call_site());
let scanner_name = syn::Ident::new(&scanner_data.name, proc_macro2::Span::call_site());
let match_function_code = character_classes.generate("match_function");
let number_of_character_classes = character_classes.intervals.len();
let modes = scanner_modes.into_iter().enumerate().map(|(index, mode)| {
let transitions = mode.transitions.iter().map(|transition_to_numeric_mode| {
match transition_to_numeric_mode {
TransitionToNumericMode::SetMode(token_type, new_mode_index) => {
quote! { Transition::SetMode(#token_type, #new_mode_index) }
}
TransitionToNumericMode::PushMode(token_type, new_mode_index) => {
quote! { Transition::PushMode(#token_type, #new_mode_index) }
}
TransitionToNumericMode::PopMode(token_type) => {
quote! { Transition::PopMode(#token_type) }
}
}
});
let states = dfas[index].states.iter().map(|state| {
let dfa_state_with_number_of_character_classes =
DfaStateWithNumberOfCharacterClasses::new(state, number_of_character_classes);
dfa_state_with_number_of_character_classes.to_token_stream()
});
let mode_name = mode.name;
quote! {
ScannerMode {
name: #mode_name,
transitions: &[#(#transitions),*],
dfa: Dfa { states: &[#(#states),*] }
}
}
});
let output = quote! {
pub mod #module_name_ident {
use scnr2::{AcceptData, Dfa, DfaState, DfaTransition, Lookahead, ScannerMode, ScannerImpl, Transition};
pub const MODES: &[ScannerMode] = &[
#(
#modes
),*
];
pub struct #scanner_name {
pub scanner_impl: std::rc::Rc<std::cell::RefCell<ScannerImpl>>,
}
impl #scanner_name {
pub fn new() -> Self {
#scanner_name {
scanner_impl: std::rc::Rc::new(std::cell::RefCell::new(ScannerImpl::new(MODES))),
}
}
#match_function_code
pub fn find_matches<'a>(
&'a self,
input: &'a str,
offset: usize,
) -> scnr2::FindMatches<'a, fn(char) -> Option<usize>> {
ScannerImpl::find_matches(
self.scanner_impl.clone(),
input,
offset,
&(Self::match_function as fn(char) -> Option<usize>)
)
}
pub fn find_matches_with_position<'a>(
&'a self,
input: &'a str,
offset: usize,
) -> scnr2::FindMatchesWithPosition<'a, fn(char) -> Option<usize>> {
ScannerImpl::find_matches_with_position(
self.scanner_impl.clone(),
input,
offset,
&(Self::match_function as fn(char) -> Option<usize>)
)
}
pub fn current_mode_index(&self) -> usize {
self.scanner_impl.borrow().current_mode_index()
}
pub fn mode_name(&self, index: usize) -> Option<&'static str> {
self.scanner_impl.borrow().mode_name(index)
}
pub fn current_mode_name(&self) -> &'static str {
self.scanner_impl.borrow().current_mode_name()
}
}
}
};
output
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
let chars = s.chars().peekable();
for c in chars {
if c.is_uppercase() {
if !result.is_empty() && !result.ends_with('_') {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
use crate::Result;
use std::path::Path;
use std::process::Command;
fn should_update_snapshots() -> bool {
std::env::var("SCNR2_UPDATE_SNAPSHOTS")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
fn try_format(path_to_file: &Path) -> Result<()> {
Command::new("rustfmt")
.args([path_to_file])
.status()
.map(|_| ())
.map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to format file: {e}")).into()
})
}
#[test]
fn test_generate() {
let input = quote::quote! {
TestScanner {
mode INITIAL {
token r"\r\n|\r|\n" => 1;
token r"[\s--\r\n]+" => 2;
token r"//.*(\r\n|\r|\n)?" => 3;
token r"/\*([^*]|\*[^/])*\*/" => 4;
token r#"""# => 8;
token r"Hello" => 9;
token r"World" => 10;
token r"World" followed by r"!" => 11;
token r"!" not followed by r"!" => 12;
token r"[a-zA-Z_]\w*" => 13;
token r"." => 14;
on 8 enter STRING;
}
mode STRING {
token r#"\\[\"\\bfnt]"# => 5;
token r"\\[\s--\r\n]*\r?\n" => 6;
token r#"[^\"\\]+"# => 7;
token r#"""# => 8;
token r"." => 14;
on 8 enter INITIAL;
}
}
};
let code = generate(input).to_string();
let mut temp_file =
tempfile::NamedTempFile::new().expect("Failed to create temporary file");
temp_file
.write_all(code.as_bytes())
.expect("Failed to write to temporary file");
println!("Temporary file created at: {:?}", temp_file.path());
try_format(temp_file.path()).expect("Failed to format the temporary file");
let formatted_code = std::fs::read_to_string(temp_file.path())
.expect("Failed to read the formatted temporary file")
.replace("\r\n", "\n");
if should_update_snapshots() {
std::fs::write("data/expected_generated_code.rs", &formatted_code)
.expect("Failed to write the expected code file");
}
let expected_code = std::fs::read_to_string("data/expected_generated_code.rs")
.expect("Failed to read the expected code file")
.replace("\r\n", "\n");
assert_eq!(formatted_code, expected_code);
}
}