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

            
23
use proc_macro::TokenStream;
24
use std::collections::HashMap;
25

            
26
use itertools::Itertools;
27
use quote::quote;
28
use 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

            
36
struct RemoveCompatibleAttrs;
37
impl VisitMut for RemoveCompatibleAttrs {
38
115
    fn visit_variant_mut(&mut self, i: &mut Variant) {
39
115
        // 1. generate docstring for variant
40
115
        // Supported by: minion, sat ...
41
115
        //
42
115
        // 2. delete #[compatible] attributes
43
115

            
44
115
        let mut solvers: Vec<String> = vec![];
45
120
        for attr in i.attrs.iter() {
46
120
            if !attr.path().is_ident("compatible") {
47
25
                continue;
48
95
            }
49
95
            let nested = attr
50
95
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
51
95
                .unwrap();
52
225
            for arg in nested {
53
130
                let ident = arg.path().require_ident().unwrap();
54
130
                let solver_name = ident.to_string();
55
130
                solvers.push(solver_name);
56
130
            }
57
        }
58

            
59
115
        if !solvers.is_empty() {
60
95
            let solver_list: String = solvers.into_iter().intersperse(", ".into()).collect();
61
95
            let doc_string: String = format!("**Supported by:** {}.\n", solver_list);
62
95
            let doc_attr: Attribute = parse_quote!(#[doc = #doc_string]);
63
95
            i.attrs.push(doc_attr);
64
95
        }
65

            
66
215
        i.attrs.retain(|attr| !attr.path().is_ident("compatible"));
67
115
    }
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]
131
5
pub fn document_compatibility(_attr: TokenStream, input: TokenStream) -> TokenStream {
132
    // Parse the input tokens into a syntax tree
133
5
    let mut input = parse_macro_input!(input as ItemEnum);
134
5
    let mut nodes_supported_by_solver: HashMap<String, Vec<syn::Ident>> = HashMap::new();
135

            
136
    // process each item inside the enum.
137
115
    for variant in input.variants.iter() {
138
115
        let variant_ident = variant.ident.clone();
139
120
        for attr in variant.attrs.iter() {
140
120
            if !attr.path().is_ident("compatible") {
141
25
                continue;
142
95
            }
143
95

            
144
95
            let nested = attr
145
95
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
146
95
                .unwrap();
147
225
            for arg in nested {
148
130
                let ident = arg.path().require_ident().unwrap();
149
130
                let solver_name = ident.to_string();
150
130
                match nodes_supported_by_solver.get_mut(&solver_name) {
151
15
                    None => {
152
15
                        nodes_supported_by_solver.insert(solver_name, vec![variant_ident.clone()]);
153
15
                    }
154
115
                    Some(a) => {
155
115
                        a.push(variant_ident.clone());
156
115
                    }
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
5
    RemoveCompatibleAttrs.visit_item_enum_mut(&mut input);
165
5

            
166
5
    // Build the doc string.
167
5

            
168
5
    // Note that quote wants us to build the doc message first, as it cannot interpolate doc
169
5
    // comments well.
170
5
    // https://docs.rs/quote/latest/quote/macro.quote.html#interpolating-text-inside-of-doc-comments
171
5
    let mut doc_msg: String = "# Compatability\n".into();
172
15
    for solver in nodes_supported_by_solver.keys() {
173
        // a nice title
174
15
        doc_msg.push_str(&format!("## {}\n", solver));
175

            
176
        // list all the ast nodes for this solver
177
130
        for node in nodes_supported_by_solver
178
15
            .get(solver)
179
15
            .unwrap()
180
15
            .iter()
181
130
            .map(|x| x.to_string())
182
15
            .sorted()
183
130
        {
184
130
            doc_msg.push_str(&format!("* [`{}`]({}::{})\n", node, input.ident, node));
185
130
        }
186

            
187
        // end list
188
15
        doc_msg.push('\n');
189
    }
190

            
191
5
    input.attrs.push(parse_quote!(#[doc = #doc_msg]));
192
5
    let expanded = quote! {
193
5
        #input
194
5
    };
195
5

            
196
5
    TokenStream::from(expanded)
197
5
}