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
118
    fn visit_variant_mut(&mut self, i: &mut Variant) {
39
118
        // 1. generate docstring for variant
40
118
        // Supported by: minion, sat ...
41
118
        //
42
118
        // 2. delete #[compatible] attributes
43
118

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

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

            
66
278
        i.attrs.retain(|attr| !attr.path().is_ident("compatible"));
67
118
    }
68
}
69

            
70
/// A macro to document enum variants by the things that they are compatible with.
71
///
72
/// # Examples
73
///
74
/// ```
75
1
/// 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
1
///
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
1
///# 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
1
///
117
/// ```
118
1
///# 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
1
///
130
#[proc_macro_attribute]
131
7
pub fn document_compatibility(_attr: TokenStream, input: TokenStream) -> TokenStream {
132
    // Parse the input tokens into a syntax tree
133
7
    let mut input = parse_macro_input!(input as ItemEnum);
134
7
    let mut nodes_supported_by_solver: HashMap<String, Vec<syn::Ident>> = HashMap::new();
135

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

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

            
166
7
    // Build the doc string.
167
7

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

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

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

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

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