Skip to main content

aoc_core_macros/
lib.rs

1#![warn(missing_docs)]
2
3//! Implementation details of the `bin_setup` procedural macro for my Advent of Code `aoc-core` library crate.
4//!
5//! You probably don't want to use this crate directly,
6//! although it can be useful if you use your own library.
7
8use proc_macro::TokenStream;
9
10use quote::quote;
11use syn::{ItemFn, parse_macro_input};
12
13mod input;
14
15use input::BinSetupInput;
16
17/// This macro can be used to generate my standard approach to Advent of Code binaries.
18///
19/// It can only be attached to `fn main()`.\
20/// Required arguments:
21/// - `puzzles_count`, number of puzzles in the binary
22/// - `resources_directory`, path to inputs and answers files, relative to `src`
23/// - `input_extension`, input files extension
24/// - `answers_file`, file containing the puzzle answers for checking execution
25///
26/// A simple `pretty_solution_2` macro is provided to:
27/// - embed input strings
28/// - execute solutions
29/// - measure execution time
30/// - verify output against provided answers
31///
32/// The generated main function provides `args` handling to execute only specific days, e.g. `$ executable 1 3 5`
33///
34/// Example usage:
35/// ```
36/// use aoc_core_macros::bin_setup;
37///
38/// #[bin_setup(5, "../resources", ".in", "Answers.out")]
39/// fn main() {
40///     pretty_solution_2!(5, "PuzzleX", solution1, solution2);
41/// }
42///
43/// fn solution1(input: &str) -> u8 {
44///     input.lines().map(|n| n.parse::<u8>().unwrap()).sum()
45/// }
46///
47/// fn solution2(input: &str) -> u8 {
48///     input.lines().map(|n| n.parse::<u8>().unwrap()).product()
49/// }
50///
51/// // ../resources/PuzzleX.in
52/// // 2
53/// // 3
54///
55/// // ../resources/Answers.out
56/// // PuzzleX 5 6
57/// ```
58///
59/// Generated code looks like this:
60/// ```
61/// #[allow(clippy::items_after_statements)]
62/// fn main() {
63///     let puzzle_answers: rustc_hash::FxHashMap<&'static str, [&'static str; 2]> =
64///         include_str!(concat!("../resources", "/", "Answers.out"))
65///             .lines()
66///             .map(|line| {
67///                 let parts: Vec<_> = line.split_ascii_whitespace().collect();
68///
69///                 (parts[0], [parts[1], parts[2]])
70///             })
71///             .collect();
72///
73///     let selected_puzzles: [bool; 5] = {
74///         let args: Vec<_> = std::env::args().collect();
75///
76///         if args.len() == 1 {
77///             [true; 5]
78///         } else {
79///             std::array::from_fn(|day| args.contains(&(day + 1).to_string()))
80///         }
81///     };
82///
83///     #[inline]
84///     fn pretty_solution<R>(
85///         puzzle: &str,
86///         part: usize,
87///         solution: fn(&str) -> R,
88///         input: &str,
89///         answer: &str,
90///     ) where
91///         R: std::fmt::Display + PartialEq,
92///     {
93///         let now = std::time::Instant::now();
94///         let solution = solution(input);
95///         let microseconds = now.elapsed().as_micros();
96///
97///         assert!(
98///             solution.to_string() == answer,
99///             "Wrong solution for {puzzle} part {part}: expected {answer}, but got {solution}"
100///         );
101///
102///         println!("{part} -> {answer} ({microseconds}μs)");
103///     }
104///
105///     macro_rules! pretty_solution_2 {
106///         ($day:literal, $puzzle: literal, $solution1:ident $(,$solution2:ident)?) => {
107///             if selected_puzzles[$day - 1] {
108///                 println!("Day {}: {}", $day, $puzzle);
109///
110///                 const INPUT: &str =
111///                     include_str!(concat!("../resources", "/", $puzzle, ".in"));
112///                 let answers = puzzle_answers.get($puzzle).expect("Puzzle answer not found");
113///
114///                 pretty_solution($puzzle, 1, $solution1, INPUT, answers[0]);
115///
116///                 $(pretty_solution($puzzle, 2, $solution2, INPUT, answers[1]);)?
117///
118///                 println!();
119///             }
120///         };
121///     }
122///
123///     pretty_solution_2!(5, "PuzzleX", solution1, solution2);
124/// }
125///
126/// # fn solution1(input: &str) -> u8 {
127/// #     input.lines().map(|n| n.parse::<u8>().unwrap()).sum()
128/// # }
129/// #
130/// # fn solution2(input: &str) -> u8 {
131/// #     input.lines().map(|n| n.parse::<u8>().unwrap()).product()
132/// # }
133/// ```
134#[proc_macro_attribute]
135pub fn bin_setup(attr: TokenStream, item: TokenStream) -> TokenStream {
136    let BinSetupInput {
137        puzzles_count,
138        resources_directory,
139        input_extension,
140        answers_file,
141    } = parse_macro_input!(attr as BinSetupInput);
142
143    let input_fn = parse_macro_input!(item as ItemFn);
144
145    if input_fn.sig.ident != "main" {
146        return syn::Error::new_spanned(
147            &input_fn.sig.ident,
148            format!(
149                "#[{}] can only be applied to `fn main()`",
150                stringify!(bin_setup)
151            ),
152        )
153        .into_compile_error()
154        .into();
155    }
156
157    let input_fn = input_fn.block;
158
159    quote! {
160        #[allow(clippy::items_after_statements)]
161        fn main() {
162            let puzzle_answers: rustc_hash::FxHashMap<&'static str, [&'static str; 2]> =
163                include_str!(concat!(#resources_directory, "/", #answers_file))
164                    .lines()
165                    .map(|line| {
166                        let parts: Vec<_> = line.split_ascii_whitespace().collect();
167
168                        (parts[0], [parts[1], parts[2]])
169                    })
170                    .collect();
171
172            let selected_puzzles: [bool; #puzzles_count] = {
173                let args: Vec<_> = std::env::args().collect();
174
175                if args.len() == 1 {
176                    [true; #puzzles_count]
177                } else {
178                    std::array::from_fn(|day| args.contains(&(day + 1).to_string()))
179                }
180            };
181
182            #[inline]
183            fn pretty_solution<R>(
184                puzzle: &str,
185                part: usize,
186                solution: fn(&str) -> R,
187                input: &str,
188                answer: &str,
189            ) where
190                R: std::fmt::Display + PartialEq,
191            {
192                let now = std::time::Instant::now();
193                let solution = solution(input);
194                let microseconds = now.elapsed().as_micros();
195
196                assert!(
197                    solution.to_string() == answer,
198                    "Wrong solution for {puzzle} part {part}: expected {answer}, but got {solution}"
199                );
200
201                println!("{part} -> {answer} ({microseconds}μs)");
202            }
203
204            macro_rules! pretty_solution_2 {
205                ($day:literal, $puzzle: literal, $solution1:ident $(,$solution2:ident)?) => {
206                    if selected_puzzles[$day - 1] {
207                        println!("Day {}: {}", $day, $puzzle);
208
209                        const INPUT: &str =
210                            include_str!(concat!(#resources_directory, "/", $puzzle, #input_extension));
211                        let answers = puzzle_answers.get($puzzle).expect("Puzzle answer not found");
212
213                        pretty_solution($puzzle, 1, $solution1, INPUT, answers[0]);
214
215                        $(pretty_solution($puzzle, 2, $solution2, INPUT, answers[1]);)?
216
217                        println!();
218                    }
219                };
220            }
221
222            #input_fn
223        }
224    }
225    .into()
226}