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            if let Some(machine) = input
46                .machines
47                .iter()
48                .find(|machine| !machine.event_generics.params.is_empty())
49            {
50                return syn::Error::new(
51                    machine.name.as_ref().map_or_else(
52                        proc_macro2::Span::call_site,
53                        proc_macro2::Ident::span,
54                    ),
55                    "generic event declarations are currently supported by flat `sml!` tables; composite tables do not yet have dispatch-scoped generic event enums",
56                )
57                .to_compile_error()
58                .into();
59            }
60            return match composite_codegen::generate_code(&input.machines) {
61                Ok(code) => code.into(),
62                Err(error) => error.to_compile_error().into(),
63            };
64        }
65        let mut output = proc_macro2::TokenStream::new();
66        for machine in input.machines {
67            output.extend(proc_macro2::TokenStream::from(expand(machine)));
68        }
69        return output.into();
70    }
71    let machine = input
72        .machines
73        .into_iter()
74        .next()
75        .expect("parser requires a machine");
76    if machine
77        .transitions
78        .iter()
79        .filter(|transition| transition.in_state.start)
80        .count()
81        > 1
82    {
83        if !machine.event_generics.params.is_empty() {
84            return syn::Error::new(
85                machine.name.as_ref().map_or_else(
86                    proc_macro2::Span::call_site,
87                    proc_macro2::Ident::span,
88                ),
89                "generic event declarations are currently supported by flat `sml!` tables; orthogonal tables do not yet have dispatch-scoped generic event enums",
90            )
91            .to_compile_error()
92            .into();
93        }
94        return match orthogonal_codegen::generate_code(&machine) {
95            Ok(code) => code.into(),
96            Err(error) => error.to_compile_error().into(),
97        };
98    }
99    expand(machine)
100}
101
102fn expand(input: parser::state_machine::StateMachine) -> proc_macro::TokenStream {
103    match parser::ParsedStateMachine::new(input) {
104        // Generate code and hand the output tokens back to the compiler
105        Ok(sm) => {
106            #[cfg(feature = "graphviz")]
107            {
108                use std::hash::{Hash, Hasher};
109                use std::io::Write;
110
111                // Generate DOT syntax for the state machine.
112                let diagram = diagramgen::generate_diagram(&sm);
113                let diagram_name = if let Some(name) = &sm.name {
114                    name.to_string()
115                } else {
116                    let mut diagram_hasher = std::collections::hash_map::DefaultHasher::new();
117                    diagram.hash(&mut diagram_hasher);
118                    format!("sml{:010x}", diagram_hasher.finish())
119                };
120
121                // Render SVG when Graphviz is available. Otherwise retain the
122                // DOT source instead of making compilation depend on a host
123                // executable.
124                let svg_name = format!("sml_{diagram_name}.svg");
125                let rendered = std::process::Command::new("dot")
126                    .args(["-Tsvg", "-o", &svg_name])
127                    .stdin(std::process::Stdio::piped())
128                    .spawn()
129                    .ok()
130                    .map(|mut process| {
131                        let wrote_input = process
132                            .stdin
133                            .as_mut()
134                            .map(|stdin| stdin.write_all(diagram.as_bytes()).is_ok())
135                            .unwrap_or(false);
136                        wrote_input
137                            && process
138                                .wait()
139                                .map(|status| status.success())
140                                .unwrap_or(false)
141                    })
142                    .unwrap_or(false);
143
144                if !rendered {
145                    let _ = std::fs::remove_file(svg_name);
146                    let dot_name = format!("sml_{diagram_name}.dot");
147                    let dot_path = std::env::var_os("OUT_DIR")
148                        .map(std::path::PathBuf::from)
149                        .map(|directory| directory.join(&dot_name))
150                        .unwrap_or_else(|| std::env::temp_dir().join(dot_name));
151                    let _ = std::fs::write(dot_path, diagram.as_bytes());
152                }
153            }
154
155            // Validate the parsed state machine before generating code.
156            if let Err(e) = validation::validate(&sm) {
157                return e.to_compile_error().into();
158            }
159
160            codegen::generate_code(&sm).into()
161        }
162        Err(error) => error.to_compile_error().into(),
163    }
164}