1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! A flexible, yet simple quote-based code generator for creating beautiful Rust code

#![warn(missing_docs)]

/// Configuration related items
pub mod config;
/// Configuration variable related items
pub mod var;

use std::collections::HashMap;
use std::io::Write;
use std::{fs, io};

use flexstr::SharedStr;
use heck::ToSnakeCase;
use proc_macro2::TokenStream;
use quote::quote;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use rust_format::{Formatter, PostProcess, PrettyPlease};
use use_builder::{UseBuilder, UseItems};

use crate::config::{Config, FragmentItem};
use crate::var::TokenVars;

#[doc(hidden)]
#[inline]
pub fn make_key(s: &'static str) -> SharedStr {
    SharedStr::from_ref(&s.to_snake_case())
}

/// Register code fragments in preparation for code generation
#[macro_export]
macro_rules! register_fragments {
    (%item%, $v:ident) => { () };
    (%count%, $($v:ident),+) => { [$($crate::register_fragments!(%item%, $v)),+].len() };
    // Allow trailing comma
    ($($fragment:ident,)+) => { $crate::register_fragments!($($fragment),+) };
    ($($fragment:ident),+) => {
        {
            let cap = $crate::register_fragments!(%count%, $($fragment),+);
            let mut map = $crate::CodeFragments::with_capacity(cap);

            $(
                map.insert($crate::make_key(stringify!($fragment)), &$fragment);
            )+
            map
        }
    };
}

// *** Error ***

/// This error will be returned if any issues arise during the code generation process
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// A variable specified in [import_vars] could not be found
    #[error("The specified variable '{0}' was missing.")]
    MissingVar(SharedStr),

    /// A fragment specified by the [CodeFragments] could not be found
    #[error("These code fragments from the configuration are missing: {0:?}")]
    MissingFragments(Vec<SharedStr>),

    /// The fragment list specified in the file section doesn't exist
    #[error("The fragment list '{0}' referenced by file '{1}' doesn't exist")]
    MissingFragmentList(SharedStr, SharedStr),

    /// The fragment list exceptions specified by the file don't exist
    #[error("These fragment list exceptions referenced by file '{1}' don't exist: {0:?}")]
    MissingFragmentListExceptions(Vec<SharedStr>, SharedStr),

    /// The file section requested from the [Config](config::Config) doesn't exist
    #[error("The configuration file item '{0}' doesn't exist")]
    FileNotFound(SharedStr),

    /// The fragment list requested from the [Config](config::Config) doesn't exist
    #[error("The configuration fragment list item '{0}' doesn't exist")]
    FragmentListNotFound(SharedStr),

    /// A nested list of execution errors occurred while trying to generate source code
    #[error("Errors occurred during execution: {0:?}")]
    ExecutionErrors(Vec<Error>),

    /// The item imported was of the wrong type (either single when a list was needed or vice versa)
    #[error("The specified item was a 'list' instead of a 'single' item (or vice versa)")]
    WrongItem,

    /// Unable to parse source code value from variable
    #[error("The code item could not be parsed: {0}")]
    UnrecognizedCodeItem(#[from] syn::Error),

    /// The code item variable data was in an unknown format
    #[error("The item did not match any known code item prefix: {0}")]
    NotCodeItem(SharedStr),

    /// An error occurred while deserializing the [Config](config::Config)
    #[error("There was an error while deserializing: {0}")]
    DeserializeError(String),

    /// An error occurred while formatting
    #[error(transparent)]
    FormatError(#[from] rust_format::Error),

    /// A general I/O error occurred
    #[error(transparent)]
    IOError(#[from] io::Error),

    /// A TOML syntax error occurred
    #[error(transparent)]
    TOMLError(#[from] toml::de::Error),

    /// An error occurred while parsing use sections
    #[error(transparent)]
    UseBuilderError(#[from] use_builder::Error),
}

// *** Execute ***

struct FileGenerator<'exec> {
    name: &'exec SharedStr,
    vars: TokenVars,
    fragments: &'exec CodeFragments,
    config: &'exec Config,
}

impl<'exec> FileGenerator<'exec> {
    fn new(
        name: &'exec SharedStr,
        fragments: &'exec CodeFragments,
        config: &'exec Config,
    ) -> Result<Self, Error> {
        // Get merged vars
        let vars = config.vars(name)?;

        Ok(Self {
            name,
            vars,
            fragments,
            config,
        })
    }

    fn assemble_source(
        &self,
        results: Vec<TokenStream>,
        top_results: Vec<TokenStream>,
        uses: Vec<UseItems>,
    ) -> Result<String, Error> {
        // Would be nice to make this a constant, but _comment_! marker needs a literal
        let comment = quote! {
            _comment_!("\nWARNING: This file has been auto-generated using flexgen (https://github.com/nu11ptr/flexgen).");
            _comment_!("Any manual modifications to this file will be overwritten the next time this file is generated.\n\n");
        };

        let builder = UseBuilder::from_uses(uses);
        let (std_uses, ext_uses, crate_uses) = builder.into_items_sections()?;

        let tokens = quote! {
            #comment
            _blank_!();

            #( #top_results )*

            #( #std_uses )*
            _blank_!();
            #( #ext_uses )*
            _blank_!();
            #( #crate_uses )*
            _blank_!();

            #( #results )*
        };

        let config = rust_format::Config::new_str().post_proc(PostProcess::ReplaceMarkers);
        let formatter = PrettyPlease::from_config(config);
        let source = formatter.format_tokens(tokens)?;

        // Either return after PrettyPlease format or do one last final RustFmt run
        Ok(match self.config.build_rust_fmt() {
            Some(rust_fmt) => rust_fmt.format_str(source)?,
            None => source,
        })
    }

    fn build_source(
        &self,
        fragments: &[FragmentItem],
        exceptions: &[SharedStr],
        results: &mut Vec<TokenStream>,
        top_results: &mut Vec<TokenStream>,
        use_trees: &mut Vec<UseItems>,
    ) -> Result<(), Error> {
        for (idx, fragment) in fragments.iter().enumerate() {
            match fragment {
                FragmentItem::FragmentListRef(name) => {
                    if exceptions.contains(name) {
                        continue;
                    }

                    let fragments = self.config.fragment_list(name)?;
                    return self.build_source(
                        fragments,
                        exceptions,
                        results,
                        top_results,
                        use_trees,
                    );
                }
                FragmentItem::Fragment(name) => {
                    if exceptions.contains(name) {
                        continue;
                    }

                    // Panic safety: This was pre-validated
                    let fragment = self.fragments[name];
                    let tokens = fragment.generate(&self.vars)?;
                    if !tokens.is_empty() {
                        results.push(tokens);
                    }

                    let top_tokens = fragment.generate_top(&self.vars)?;
                    if !top_tokens.is_empty() {
                        top_results.push(top_tokens);
                    }

                    // Store the use tree, if we had one
                    let use_tokens = fragment.uses(&self.vars)?;
                    if !use_tokens.is_empty() {
                        use_trees.push(syn::parse2(use_tokens)?)
                    }

                    // Push a blank line on all but the last fragment in the list
                    if idx < fragments.len() - 1 {
                        results.push(quote! { _blank_!(); })
                    }
                }
            }
        }

        Ok(())
    }

    fn generate_string(&self) -> Result<(SharedStr, String), Error> {
        // TODO: Combine into one call?
        let fragments = self.config.file_fragment_list(self.name)?;
        let exceptions = self.config.file_fragment_exceptions(self.name)?;

        // TODO: What capacity? (we could have nested lists, etc.)
        let mut results = Vec::with_capacity(self.fragments.len() * 2);
        let mut top_results = Vec::with_capacity(3);
        // Random choice based on a typical file
        let mut uses = Vec::with_capacity(10);

        self.build_source(
            fragments,
            exceptions,
            &mut results,
            &mut top_results,
            &mut uses,
        )?;
        let source = self.assemble_source(results, top_results, uses)?;

        Ok((self.name.clone(), source))
    }

    fn generate_file(&self) -> Result<(), Error> {
        let (_, source) = self.generate_string()?;

        let mut file = fs::File::create(self.config.file_path(self.name)?)?;
        file.write_all(source.as_bytes())?;
        Ok(())
    }
}

/// The actual code generator
pub struct CodeGenerator {
    code: CodeFragments,
    config: Config,
}

impl CodeGenerator {
    /// Create a new instance of the `CodeGenerator`. It will validate the [Config] and return
    /// an [Error] if there are any issues
    #[inline]
    pub fn new(code: CodeFragments, mut config: Config) -> Result<Self, Error> {
        config.build_and_validate(&code)?;
        Ok(Self { code, config })
    }

    fn parse_results<T>(results: Vec<Result<T, Error>>) -> Result<Vec<T>, Error> {
        let mut errors = Vec::with_capacity(results.len());
        let mut source = Vec::with_capacity(results.len());

        for result in results {
            match result {
                Ok(result) => source.push(result),
                Err(err) => errors.push(err),
            }
        }

        if errors.is_empty() {
            Ok(source)
        } else {
            Err(Error::ExecutionErrors(errors))
        }
    }

    fn generate(&self, to_file: bool) -> Result<HashMap<SharedStr, String>, Error> {
        let names = self.config.file_names();

        Ok(if to_file {
            let results: Vec<Result<_, _>> = names
                .par_iter()
                .map(|&name| FileGenerator::new(name, &self.code, &self.config)?.generate_file())
                .collect();

            Self::parse_results(results)?;
            HashMap::new()
        } else {
            let results: Vec<Result<_, _>> = names
                .par_iter()
                .map(|&name| FileGenerator::new(name, &self.code, &self.config)?.generate_string())
                .collect();
            let results: HashMap<_, _> = Self::parse_results(results)?.into_iter().collect();
            results
        })
    }

    /// Generate the files listed in the [Config], but return them as a map of strings instead of
    /// actually writing them to he filesystem
    #[inline]
    pub fn generate_strings(&self) -> Result<HashMap<SharedStr, String>, Error> {
        self.generate(false)
    }

    /// Generate the files listed in the [Config]
    #[inline]
    pub fn generate_files(&self) -> Result<(), Error> {
        self.generate(true).map(|_| ())
    }
}

// *** Misc. Types ***

/// A map of all registered code fragments. Is is returned by a call to [register_fragments]
pub type CodeFragments = HashMap<SharedStr, &'static (dyn CodeFragment + Send + Sync)>;

/// A single code fragment - the smallest unit of work
#[allow(unused_variables)]
pub trait CodeFragment {
    /// Generate the `use` sections of the file, if any. The returned `TokenStream` can ONLY
    /// be varius `use` items. They will be deduplicated and grouped before inclusion in the file.
    #[inline]
    fn uses(&self, vars: &TokenVars) -> Result<TokenStream, Error> {
        Ok(quote! {})
    }

    /// Generate any portion of the source file that must be on the top (such as `#![]` style attributes).
    /// Each snippet will be collected in order and combined into the whole
    #[inline]
    fn generate_top(&self, vars: &TokenVars) -> Result<TokenStream, Error> {
        Ok(quote! {})
    }

    /// Generate the general body of the source file. Each snippet will be collected in order and
    /// combined into the whole
    #[inline]
    fn generate(&self, vars: &TokenVars) -> Result<TokenStream, Error> {
        Ok(quote! {})
    }
}