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}