enum_compatability_macro/
lib.rs

1//! A macro to document enum variants with the things that they are compatible with.
2//!
3//!
4//! As well as documenting each variant, this macro also generates lists of all compatible variants
5//! for each "thing".
6//!
7//! # Motivation
8//!
9//! This macro is used in Conjure-Oxide, a constraint modelling tool with support for multiple
10//! backend solvers (e.g. Minion, SAT).
11//!
12//! The Conjure-Oxide AST is used as the singular representation for constraints models throughout
13//! its crate. A consequence of this is that the AST must contain all possible supported
14//! expressions for all solvers, as well as the high level Essence language it takes as input.
15//! Therefore, only a small subset of AST nodes are useful for a particular solver.
16//!
17//! The documentation this generates helps rewrite rule implementers determine which AST nodes are
18//! used for which backends by grouping AST nodes per solver.
19
20#![allow(clippy::unwrap_used)]
21#![allow(unstable_name_collisions)]
22
23use proc_macro::TokenStream;
24use std::collections::HashMap;
25
26use itertools::Itertools;
27use quote::quote;
28use syn::{
29    parse_macro_input, parse_quote, punctuated::Punctuated, visit_mut::VisitMut, Attribute,
30    ItemEnum, Meta, Token, Variant,
31};
32
33// A nice S.O answer that helped write the syn code :)
34// https://stackoverflow.com/a/65182902
35
36struct RemoveCompatibleAttrs;
37impl VisitMut for RemoveCompatibleAttrs {
38    fn visit_variant_mut(&mut self, i: &mut Variant) {
39        // 1. generate docstring for variant
40        // Supported by: minion, sat ...
41        //
42        // 2. delete #[compatible] attributes
43
44        let mut solvers: Vec<String> = vec![];
45        for attr in i.attrs.iter() {
46            if !attr.path().is_ident("compatible") {
47                continue;
48            }
49            let nested = attr
50                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
51                .unwrap();
52            for arg in nested {
53                let ident = arg.path().require_ident().unwrap();
54                let solver_name = ident.to_string();
55                solvers.push(solver_name);
56            }
57        }
58
59        if !solvers.is_empty() {
60            let solver_list: String = solvers.into_iter().intersperse(", ".into()).collect();
61            let doc_string: String = format!("**Supported by:** {}.\n", solver_list);
62            let doc_attr: Attribute = parse_quote!(#[doc = #doc_string]);
63            i.attrs.push(doc_attr);
64        }
65
66        i.attrs.retain(|attr| !attr.path().is_ident("compatible"));
67    }
68}
69
70/// A macro to document enum variants by the things that they are compatible with.
71///
72/// # Examples
73///
74/// ```
75/// use enum_compatability_macro::document_compatibility;
76///
77/// #[document_compatibility]
78/// pub enum Expression {
79///    #[compatible(Minion)]
80///    ConstantInt(i32),
81///    // ...
82///    #[compatible(Chuffed)]
83///    #[compatible(Minion)]
84///    Sum(Vec<Expression>)
85///    }
86/// ```
87///
88/// The Expression type will have the following lists appended to its documentation:
89///
90///```text
91/// ## Supported by `minion`
92///    ConstantInt(i32)
93///    Sum(Vec<Expression>)
94///
95///
96/// ## Supported by `chuffed`
97///    ConstantInt(i32)
98///    Sum(Vec<Expression>)
99/// ```
100///
101/// Two equivalent syntaxes exist for specifying supported solvers:
102///
103/// ```
104///# use enum_compatability_macro::document_compatibility;
105///#
106///# #[document_compatibility]
107///# pub enum Expression {
108///#    #[compatible(Minion)]
109///#    ConstantInt(i32),
110///#    // ...
111///     #[compatible(Chuffed)]
112///     #[compatible(Minion)]
113///     Sum(Vec<Expression>)
114///#    }
115/// ```
116///
117/// ```
118///# use enum_compatability_macro::document_compatibility;
119///#
120///# #[document_compatibility]
121///# pub enum Expression {
122///#    #[compatible(Minion)]
123///#    ConstantInt(i32),
124///#    // ...
125///     #[compatible(Minion,Chuffed)]
126///     Sum(Vec<Expression>)
127///#    }
128/// ```
129///
130#[proc_macro_attribute]
131pub fn document_compatibility(_attr: TokenStream, input: TokenStream) -> TokenStream {
132    // Parse the input tokens into a syntax tree
133    let mut input = parse_macro_input!(input as ItemEnum);
134    let mut nodes_supported_by_solver: HashMap<String, Vec<syn::Ident>> = HashMap::new();
135
136    // process each item inside the enum.
137    for variant in input.variants.iter() {
138        let variant_ident = variant.ident.clone();
139        for attr in variant.attrs.iter() {
140            if !attr.path().is_ident("compatible") {
141                continue;
142            }
143
144            let nested = attr
145                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
146                .unwrap();
147            for arg in nested {
148                let ident = arg.path().require_ident().unwrap();
149                let solver_name = ident.to_string();
150                match nodes_supported_by_solver.get_mut(&solver_name) {
151                    None => {
152                        nodes_supported_by_solver.insert(solver_name, vec![variant_ident.clone()]);
153                    }
154                    Some(a) => {
155                        a.push(variant_ident.clone());
156                    }
157                };
158            }
159        }
160    }
161
162    // we must remove all references to #[compatible] before we finish expanding the macro,
163    // as it does not exist outside of the context of this macro.
164    RemoveCompatibleAttrs.visit_item_enum_mut(&mut input);
165
166    // Build the doc string.
167
168    // Note that quote wants us to build the doc message first, as it cannot interpolate doc
169    // comments well.
170    // https://docs.rs/quote/latest/quote/macro.quote.html#interpolating-text-inside-of-doc-comments
171    let mut doc_msg: String = "# Compatability\n".into();
172    for solver in nodes_supported_by_solver.keys() {
173        // a nice title
174        doc_msg.push_str(&format!("## {}\n", solver));
175
176        // list all the ast nodes for this solver
177        for node in nodes_supported_by_solver
178            .get(solver)
179            .unwrap()
180            .iter()
181            .map(|x| x.to_string())
182            .sorted()
183        {
184            doc_msg.push_str(&format!("* [`{}`]({}::{})\n", node, input.ident, node));
185        }
186
187        // end list
188        doc_msg.push('\n');
189    }
190
191    input.attrs.push(parse_quote!(#[doc = #doc_msg]));
192    let expanded = quote! {
193        #input
194    };
195
196    TokenStream::from(expanded)
197}