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}