1
use std::collections::HashMap;
2

            
3
use itertools::Itertools;
4
use proc_macro::TokenStream;
5
use quote::quote;
6
use syn::{
7
    parse_macro_input, parse_quote, punctuated::Punctuated, visit_mut::VisitMut, Attribute,
8
    ItemEnum, Meta, Token, Variant,
9
};
10

            
11
// A nice S.O answer that helped write the syn code :)
12
// https://stackoverflow.com/a/65182902
13

            
14
struct RemoveSolverAttrs;
15
impl VisitMut for RemoveSolverAttrs {
16
51
    fn visit_variant_mut(&mut self, i: &mut Variant) {
17
51
        // 1. generate docstring for variant
18
51
        // Supported by: minion, sat ...
19
51
        //
20
51
        // 2. delete #[solver] attributes
21
51

            
22
51
        let mut solvers: Vec<String> = vec![];
23
51
        for attr in i.attrs.iter() {
24
27
            if !attr.path().is_ident("solver") {
25
3
                continue;
26
24
            }
27
24
            let nested = attr
28
24
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
29
24
                .unwrap();
30
51
            for arg in nested {
31
27
                let ident = arg.path().require_ident().unwrap();
32
27
                let solver_name = ident.to_string();
33
27
                solvers.push(solver_name);
34
27
            }
35
        }
36

            
37
51
        if !solvers.is_empty() {
38
24
            let solver_list: String = solvers.into_iter().intersperse(", ".into()).collect();
39
24
            let doc_string: String = format!("**Supported by:** {}.\n", solver_list);
40
24
            let doc_attr: Attribute = parse_quote!(#[doc = #doc_string]);
41
24
            i.attrs.push(doc_attr);
42
27
        }
43

            
44
51
        i.attrs = i
45
51
            .attrs
46
51
            .iter()
47
51
            .filter(|attr| !attr.path().is_ident("solver"))
48
51
            .map(|attr| attr.clone())
49
51
            .collect();
50
51

            
51
51
        return;
52
51
    }
53
}
54

            
55
/// A macro to document the AST's variants by the solvers they support.
56
///
57
/// The Conjure-Oxide AST is used as the singular intermediate language between input and solvers.
58
/// A consequence of this is that the AST contains all possible supported expressions for all
59
/// solvers, as well as the high level Essence language we take as input. A given
60
/// solver only "supports" a small subset of the AST, and will reject the rest.
61
///
62
/// The documentation this generates helps determine which AST nodes are used for which backends,
63
/// to help rule writers.
64
///
65
/// # Example
66
///
67
/// ```
68
/// use doc_solver_support::doc_solver_support;
69
///
70
/// #[doc_solver_support]
71
/// pub enum Expression {
72
///    #[solver(Minion)]
73
///    ConstantInt(i32),
74
///    // ...
75
///    #[solver(Chuffed)]
76
///    #[solver(Minion)]
77
///    Sum(Vec<Expression>)
78
///    }
79
/// ```
80
///
81
/// The Expression type will have the following lists appended to its documentation:
82
///
83
///```text
84
/// ## Supported by `minion`
85
///    ConstantInt(i32)
86
///    Sum(Vec<Expression>)
87
///
88
///
89
/// ## Supported by `chuffed`
90
///    ConstantInt(i32)
91
///    Sum(Vec<Expression>)
92
/// ```
93
///
94
/// Two equivalent syntaxes exist for specifying supported solvers:
95
///
96
/// ```
97
///# use doc_solver_support::doc_solver_support;
98
///#
99
///# #[doc_solver_support]
100
///# pub enum Expression {
101
///#    #[solver(Minion)]
102
///#    ConstantInt(i32),
103
///#    // ...
104
///     #[solver(Chuffed)]
105
///     #[solver(Minion)]
106
///     Sum(Vec<Expression>)
107
///#    }
108
/// ```
109
///
110
/// ```
111
///# use doc_solver_support::doc_solver_support;
112
///#
113
///# #[doc_solver_support]
114
///# pub enum Expression {
115
///#    #[solver(Minion)]
116
///#    ConstantInt(i32),
117
///#    // ...
118
///     #[solver(Minion,Chuffed)]
119
///     Sum(Vec<Expression>)
120
///#    }
121
/// ```
122
///
123
#[proc_macro_attribute]
124
3
pub fn doc_solver_support(_attr: TokenStream, input: TokenStream) -> TokenStream {
125
    // Parse the input tokens into a syntax tree
126
3
    let mut input = parse_macro_input!(input as ItemEnum);
127
3
    let mut nodes_supported_by_solver: HashMap<String, Vec<syn::Ident>> = HashMap::new();
128

            
129
    // process each item inside the enum.
130
51
    for variant in input.variants.iter() {
131
51
        let variant_ident = variant.ident.clone();
132
51
        for attr in variant.attrs.iter() {
133
27
            if !attr.path().is_ident("solver") {
134
3
                continue;
135
24
            }
136
24

            
137
24
            let nested = attr
138
24
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
139
24
                .unwrap();
140
51
            for arg in nested {
141
27
                let ident = arg.path().require_ident().unwrap();
142
27
                let solver_name = ident.to_string();
143
27
                match nodes_supported_by_solver.get_mut(&solver_name) {
144
                    None => {
145
6
                        nodes_supported_by_solver.insert(solver_name, vec![variant_ident.clone()]);
146
6
                        ()
147
                    }
148
21
                    Some(a) => {
149
21
                        a.push(variant_ident.clone());
150
21
                        ()
151
                    }
152
                };
153
            }
154
        }
155
    }
156

            
157
    // we must remove all references to #[solver] before we finish expanding the macro,
158
    // as it does not exist outside of the context of this macro.
159
3
    RemoveSolverAttrs.visit_item_enum_mut(&mut input);
160
3

            
161
3
    // Build the doc string.
162
3

            
163
3
    // Note that quote wants us to build the doc message first, as it cannot interpolate doc
164
3
    // comments well.
165
3
    // https://docs.rs/quote/latest/quote/macro.quote.html#interpolating-text-inside-of-doc-comments
166
3
    let mut doc_msg: String = "# Solver Support\n".into();
167
6
    for solver in nodes_supported_by_solver.keys() {
168
        // a nice title
169
6
        doc_msg.push_str(&format!("## {}\n", solver));
170

            
171
        // list all the ast nodes for this solver
172
27
        for node in nodes_supported_by_solver
173
6
            .get(solver)
174
6
            .unwrap()
175
6
            .iter()
176
27
            .map(|x| x.to_string())
177
6
            .sorted()
178
27
        {
179
27
            doc_msg.push_str(&format!("* [`{}`]({}::{})\n", node, input.ident, node));
180
27
        }
181

            
182
        // end list
183
6
        doc_msg.push_str("\n");
184
    }
185

            
186
3
    input.attrs.push(parse_quote!(#[doc = #doc_msg]));
187
3
    let expanded = quote! {
188
3
        #input
189
3
    };
190
3

            
191
3
    TokenStream::from(expanded)
192
3
}