#![recursion_limit = "512"]
extern crate proc_macro;
mod codegen;
mod composite_codegen;
#[cfg(feature = "graphviz")]
mod diagramgen;
mod orthogonal_codegen;
mod parser;
mod validation;
use syn::parse_macro_input;
#[proc_macro]
pub fn sml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as parser::cpp::SmlDefinitions);
if input.machines.len() > 1 {
let machine_names = input
.machines
.iter()
.filter_map(|machine| machine.name.as_ref())
.collect::<Vec<_>>();
let has_composite = input.machines.iter().any(|machine| {
machine.transitions.iter().any(|transition| {
transition
.in_state
.composite
.as_ref()
.or(transition.out_state.composite.as_ref())
.is_some_and(|reference| machine_names.contains(&reference))
})
});
if has_composite {
if let Some(machine) = input
.machines
.iter()
.find(|machine| !machine.event_generics.params.is_empty())
{
return syn::Error::new(
machine.name.as_ref().map_or_else(
proc_macro2::Span::call_site,
proc_macro2::Ident::span,
),
"generic event declarations are currently supported by flat `sml!` tables; composite tables do not yet have dispatch-scoped generic event enums",
)
.to_compile_error()
.into();
}
return match composite_codegen::generate_code(&input.machines) {
Ok(code) => code.into(),
Err(error) => error.to_compile_error().into(),
};
}
let mut output = proc_macro2::TokenStream::new();
for machine in input.machines {
output.extend(proc_macro2::TokenStream::from(expand(machine)));
}
return output.into();
}
let machine = input
.machines
.into_iter()
.next()
.expect("parser requires a machine");
if machine
.transitions
.iter()
.filter(|transition| transition.in_state.start)
.count()
> 1
{
if !machine.event_generics.params.is_empty() {
return syn::Error::new(
machine.name.as_ref().map_or_else(
proc_macro2::Span::call_site,
proc_macro2::Ident::span,
),
"generic event declarations are currently supported by flat `sml!` tables; orthogonal tables do not yet have dispatch-scoped generic event enums",
)
.to_compile_error()
.into();
}
return match orthogonal_codegen::generate_code(&machine) {
Ok(code) => code.into(),
Err(error) => error.to_compile_error().into(),
};
}
expand(machine)
}
fn expand(input: parser::state_machine::StateMachine) -> proc_macro::TokenStream {
match parser::ParsedStateMachine::new(input) {
Ok(sm) => {
#[cfg(feature = "graphviz")]
{
use std::hash::{Hash, Hasher};
use std::io::Write;
let diagram = diagramgen::generate_diagram(&sm);
let diagram_name = if let Some(name) = &sm.name {
name.to_string()
} else {
let mut diagram_hasher = std::collections::hash_map::DefaultHasher::new();
diagram.hash(&mut diagram_hasher);
format!("sml{:010x}", diagram_hasher.finish())
};
let svg_name = format!("sml_{diagram_name}.svg");
let rendered = std::process::Command::new("dot")
.args(["-Tsvg", "-o", &svg_name])
.stdin(std::process::Stdio::piped())
.spawn()
.ok()
.map(|mut process| {
let wrote_input = process
.stdin
.as_mut()
.map(|stdin| stdin.write_all(diagram.as_bytes()).is_ok())
.unwrap_or(false);
wrote_input
&& process
.wait()
.map(|status| status.success())
.unwrap_or(false)
})
.unwrap_or(false);
if !rendered {
let _ = std::fs::remove_file(svg_name);
let dot_name = format!("sml_{diagram_name}.dot");
let dot_path = std::env::var_os("OUT_DIR")
.map(std::path::PathBuf::from)
.map(|directory| directory.join(&dot_name))
.unwrap_or_else(|| std::env::temp_dir().join(dot_name));
let _ = std::fs::write(dot_path, diagram.as_bytes());
}
}
if let Err(e) = validation::validate(&sm) {
return e.to_compile_error().into();
}
codegen::generate_code(&sm).into()
}
Err(error) => error.to_compile_error().into(),
}
}