aoc-core-macros 0.1.2

Procedural macros used by aoc-core when enabling the binary feature.
Documentation
#![warn(missing_docs)]

//! Implementation details of the `bin_setup` procedural macro for my Advent of Code `aoc-core` library crate.
//!
//! You probably don't want to use this crate directly,
//! although it can be useful if you use your own library.

use proc_macro::TokenStream;

use quote::quote;
use syn::{ItemFn, parse_macro_input};

mod input;

use input::BinSetupInput;

/// This macro can be used to generate my standard approach to Advent of Code binaries.
///
/// It can only be attached to `fn main()`.\
/// Required arguments:
/// - `puzzles_count`, number of puzzles in the binary
/// - `resources_directory`, path to inputs and answers files, relative to `src`
/// - `input_extension`, input files extension
/// - `answers_file`, file containing the puzzle answers for checking execution
///
/// A simple `pretty_solution_2` macro is provided to:
/// - embed input strings
/// - execute solutions
/// - measure execution time
/// - verify output against provided answers
///
/// The generated main function provides `args` handling to execute only specific days, e.g. `$ executable 1 3 5`
///
/// Example usage:
/// ```
/// use aoc_core_macros::bin_setup;
///
/// #[bin_setup(5, "../resources", ".in", "Answers.out")]
/// fn main() {
///     pretty_solution_2!(5, "PuzzleX", solution1, solution2);
/// }
///
/// fn solution1(input: &str) -> u8 {
///     input.lines().map(|n| n.parse::<u8>().unwrap()).sum()
/// }
///
/// fn solution2(input: &str) -> u8 {
///     input.lines().map(|n| n.parse::<u8>().unwrap()).product()
/// }
///
/// // ../resources/PuzzleX.in
/// // 2
/// // 3
///
/// // ../resources/Answers.out
/// // PuzzleX 5 6
/// ```
///
/// Generated code looks like this:
/// ```
/// #[allow(clippy::items_after_statements)]
/// fn main() {
///     let puzzle_answers: rustc_hash::FxHashMap<&'static str, [&'static str; 2]> =
///         include_str!(concat!("../resources", "/", "Answers.out"))
///             .lines()
///             .map(|line| {
///                 let parts: Vec<_> = line.split_ascii_whitespace().collect();
///
///                 (parts[0], [parts[1], parts[2]])
///             })
///             .collect();
///
///     let selected_puzzles: [bool; 5] = {
///         let args: Vec<_> = std::env::args().collect();
///
///         if args.len() == 1 {
///             [true; 5]
///         } else {
///             std::array::from_fn(|day| args.contains(&(day + 1).to_string()))
///         }
///     };
///
///     #[inline]
///     fn pretty_solution<R>(
///         puzzle: &str,
///         part: usize,
///         solution: fn(&str) -> R,
///         input: &str,
///         answer: &str,
///     ) where
///         R: std::fmt::Display + PartialEq,
///     {
///         let now = std::time::Instant::now();
///         let solution = solution(input);
///         let microseconds = now.elapsed().as_micros();
///
///         assert!(
///             solution.to_string() == answer,
///             "Wrong solution for {puzzle} part {part}: expected {answer}, but got {solution}"
///         );
///
///         println!("{part} -> {answer} ({microseconds}μs)");
///     }
///
///     macro_rules! pretty_solution_2 {
///         ($day:literal, $puzzle: literal, $solution1:ident $(,$solution2:ident)?) => {
///             if selected_puzzles[$day - 1] {
///                 println!("Day {}: {}", $day, $puzzle);
///
///                 const INPUT: &str =
///                     include_str!(concat!("../resources", "/", $puzzle, ".in"));
///                 let answers = puzzle_answers.get($puzzle).expect("Puzzle answer not found");
///
///                 pretty_solution($puzzle, 1, $solution1, INPUT, answers[0]);
///
///                 $(pretty_solution($puzzle, 2, $solution2, INPUT, answers[1]);)?
///
///                 println!();
///             }
///         };
///     }
///
///     pretty_solution_2!(5, "PuzzleX", solution1, solution2);
/// }
///
/// # fn solution1(input: &str) -> u8 {
/// #     input.lines().map(|n| n.parse::<u8>().unwrap()).sum()
/// # }
/// #
/// # fn solution2(input: &str) -> u8 {
/// #     input.lines().map(|n| n.parse::<u8>().unwrap()).product()
/// # }
/// ```
#[proc_macro_attribute]
pub fn bin_setup(attr: TokenStream, item: TokenStream) -> TokenStream {
    let BinSetupInput {
        puzzles_count,
        resources_directory,
        input_extension,
        answers_file,
    } = parse_macro_input!(attr as BinSetupInput);

    let input_fn = parse_macro_input!(item as ItemFn);

    if input_fn.sig.ident != "main" {
        return syn::Error::new_spanned(
            &input_fn.sig.ident,
            format!(
                "#[{}] can only be applied to `fn main()`",
                stringify!(bin_setup)
            ),
        )
        .into_compile_error()
        .into();
    }

    let input_fn = input_fn.block;

    quote! {
        #[allow(clippy::items_after_statements)]
        fn main() {
            let puzzle_answers: rustc_hash::FxHashMap<&'static str, [&'static str; 2]> =
                include_str!(concat!(#resources_directory, "/", #answers_file))
                    .lines()
                    .map(|line| {
                        let parts: Vec<_> = line.split_ascii_whitespace().collect();

                        (parts[0], [parts[1], parts[2]])
                    })
                    .collect();

            let selected_puzzles: [bool; #puzzles_count] = {
                let args: Vec<_> = std::env::args().collect();

                if args.len() == 1 {
                    [true; #puzzles_count]
                } else {
                    std::array::from_fn(|day| args.contains(&(day + 1).to_string()))
                }
            };

            #[inline]
            fn pretty_solution<R>(
                puzzle: &str,
                part: usize,
                solution: fn(&str) -> R,
                input: &str,
                answer: &str,
            ) where
                R: std::fmt::Display + PartialEq,
            {
                let now = std::time::Instant::now();
                let solution = solution(input);
                let microseconds = now.elapsed().as_micros();

                assert!(
                    solution.to_string() == answer,
                    "Wrong solution for {puzzle} part {part}: expected {answer}, but got {solution}"
                );

                println!("{part} -> {answer} ({microseconds}μs)");
            }

            macro_rules! pretty_solution_2 {
                ($day:literal, $puzzle: literal, $solution1:ident $(,$solution2:ident)?) => {
                    if selected_puzzles[$day - 1] {
                        println!("Day {}: {}", $day, $puzzle);

                        const INPUT: &str =
                            include_str!(concat!(#resources_directory, "/", $puzzle, #input_extension));
                        let answers = puzzle_answers.get($puzzle).expect("Puzzle answer not found");

                        pretty_solution($puzzle, 1, $solution1, INPUT, answers[0]);

                        $(pretty_solution($puzzle, 2, $solution2, INPUT, answers[1]);)?

                        println!();
                    }
                };
            }

            #input_fn
        }
    }
    .into()
}