Skip to main content

sml_macros/
lib.rs

1#![recursion_limit = "512"]
2
3extern crate proc_macro;
4
5mod codegen;
6mod composite_codegen;
7#[cfg(feature = "graphviz")]
8mod diagramgen;
9mod orthogonal_codegen;
10mod parser;
11mod validation;
12
13use syn::parse_macro_input;
14
15/// Defines a state machine using sml.cpp-shaped transition-table syntax.
16///
17/// ```ignore
18/// sml! {
19///     Player {
20///         *"empty"_s + event<OpenClose> / open_drawer = "open"_s,
21///          "open"_s + event<OpenClose> / close_drawer = "empty"_s,
22///     }
23/// }
24/// ```
25#[proc_macro]
26pub fn sml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
27    let input = parse_macro_input!(input as parser::cpp::SmlDefinitions);
28    if input.machines.len() > 1 {
29        let machine_names = input
30            .machines
31            .iter()
32            .filter_map(|machine| machine.name.as_ref())
33            .collect::<Vec<_>>();
34        let has_composite = input.machines.iter().any(|machine| {
35            machine.transitions.iter().any(|transition| {
36                transition
37                    .in_state
38                    .composite
39                    .as_ref()
40                    .or(transition.out_state.composite.as_ref())
41                    .is_some_and(|reference| machine_names.contains(&reference))
42            })
43        });
44        if has_composite {
45            return match composite_codegen::generate_code(&input.machines) {
46                Ok(code) => code.into(),
47                Err(error) => error.to_compile_error().into(),
48            };
49        }
50        let mut output = proc_macro2::TokenStream::new();
51        for machine in input.machines {
52            output.extend(proc_macro2::TokenStream::from(expand(machine)));
53        }
54        return output.into();
55    }
56    let machine = input
57        .machines
58        .into_iter()
59        .next()
60        .expect("parser requires a machine");
61    if machine
62        .transitions
63        .iter()
64        .filter(|transition| transition.in_state.start)
65        .count()
66        > 1
67    {
68        return match orthogonal_codegen::generate_code(&machine) {
69            Ok(code) => code.into(),
70            Err(error) => error.to_compile_error().into(),
71        };
72    }
73    expand(machine)
74}
75
76fn expand(input: parser::state_machine::StateMachine) -> proc_macro::TokenStream {
77    match parser::ParsedStateMachine::new(input) {
78        // Generate code and hand the output tokens back to the compiler
79        Ok(sm) => {
80            #[cfg(feature = "graphviz")]
81            {
82                use std::hash::{Hash, Hasher};
83                use std::io::Write;
84
85                // Generate DOT syntax for the state machine.
86                let diagram = diagramgen::generate_diagram(&sm);
87                let diagram_name = if let Some(name) = &sm.name {
88                    name.to_string()
89                } else {
90                    let mut diagram_hasher = std::collections::hash_map::DefaultHasher::new();
91                    diagram.hash(&mut diagram_hasher);
92                    format!("sml{:010x}", diagram_hasher.finish())
93                };
94
95                // Render SVG when Graphviz is available. Otherwise retain the
96                // DOT source instead of making compilation depend on a host
97                // executable.
98                let svg_name = format!("sml_{diagram_name}.svg");
99                let rendered = std::process::Command::new("dot")
100                    .args(["-Tsvg", "-o", &svg_name])
101                    .stdin(std::process::Stdio::piped())
102                    .spawn()
103                    .ok()
104                    .map(|mut process| {
105                        let wrote_input = process
106                            .stdin
107                            .as_mut()
108                            .map(|stdin| stdin.write_all(diagram.as_bytes()).is_ok())
109                            .unwrap_or(false);
110                        wrote_input
111                            && process
112                                .wait()
113                                .map(|status| status.success())
114                                .unwrap_or(false)
115                    })
116                    .unwrap_or(false);
117
118                if !rendered {
119                    let _ = std::fs::remove_file(svg_name);
120                    let dot_name = format!("sml_{diagram_name}.dot");
121                    let dot_path = std::env::var_os("OUT_DIR")
122                        .map(std::path::PathBuf::from)
123                        .map(|directory| directory.join(&dot_name))
124                        .unwrap_or_else(|| std::env::temp_dir().join(dot_name));
125                    let _ = std::fs::write(dot_path, diagram.as_bytes());
126                }
127            }
128
129            // Validate the parsed state machine before generating code.
130            if let Err(e) = validation::validate(&sm) {
131                return e.to_compile_error().into();
132            }
133
134            codegen::generate_code(&sm).into()
135        }
136        Err(error) => error.to_compile_error().into(),
137    }
138}