diff --git a/Cargo.lock b/Cargo.lock index 400a3cc..848be2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anes" version = "0.1.6" @@ -10,13 +19,14 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "aoc-main" -version = "0.4.0" +version = "0.5.0" dependencies = [ "attohttpc", "clap 4.0.29", "colored", "criterion", "dirs", + "regex", ] [[package]] @@ -500,6 +510,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "memoffset" version = "0.7.1" @@ -716,6 +732,8 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] diff --git a/Cargo.toml b/Cargo.toml index 280466a..b2fed7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ attohttpc = { version = "0.24", default_features = false, features = ["tls"] } clap = { version = "4", features = ["string"] } colored = "2" dirs = "4" +regex = "1" # Optional dependencies criterion = { version = "0.4", optional = true } diff --git a/src/bin/example.rs b/src/bin/example.rs index b73711b..74bd1ec 100644 --- a/src/bin/example.rs +++ b/src/bin/example.rs @@ -46,7 +46,7 @@ mod day2 { } mod day3 { - pub fn generator(_: &str) -> Option<&str> { + pub fn generator(_: String) -> Option { None } @@ -56,7 +56,7 @@ mod day3 { } mod day4 { - pub fn generator(_: &str) -> Result { + pub fn generator(_: String) -> Result { "five".parse() } diff --git a/src/input.rs b/src/input.rs index 486c2ed..bc488f6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,21 +1,38 @@ //! Tools used to fetch input contents from adventofcode.com. -use std::error::Error; -use std::fs::{create_dir_all, read_to_string, File}; -use std::io::Write; +use std::fs::{self, create_dir_all, read_to_string, File}; +use std::io::{self, Write}; use std::io::{stdin, stdout}; use std::path::{Path, PathBuf}; use std::time::Instant; use attohttpc::header::{COOKIE, USER_AGENT}; +use regex::Regex; +use crate::params::InputChoice; use crate::utils::Line; +use crate::{input, Day, Error, Year}; const BASE_URL: &str = "https://adventofcode.com"; const USER_AGENT_VALUE: &str = "github.com/remi-dupre/aoc by remi@dupre.io"; fn input_path(year: u16, day: u8) -> PathBuf { - format!("input/{}/day{}.txt", year, day).into() + format!("input/{year}/day{day}.txt").into() +} + +fn output_path(year: u16, day: u8, part: u8) -> PathBuf { + format!("output/{year}/day{day}-{part}.txt").into() +} + +pub fn get_input_data(day: Day, year: Year, input: &InputChoice) -> String { + match input { + InputChoice::Download => input::get_input(year, day).expect("could not fetch input"), + InputChoice::File(file) => fs::read_to_string(file).expect("failed to read from stdin"), + InputChoice::Stdin => { + let input = io::stdin().lock(); + io::read_to_string(input).expect("failed to read specified file") + } + } } fn token_path() -> PathBuf { @@ -27,35 +44,69 @@ fn token_path() -> PathBuf { .unwrap_or_else(|| ".token".into()) } -pub fn get_input(year: u16, day: u8) -> Result> { - let mut result = get_from_path_or_else(&input_path(year, day), || { +fn session_cookie() -> Result { + let token = get_conn_token()?; + Ok(format!("session={token}")) +} + +fn get_input(year: u16, day: u8) -> Result { + let url = format!("{}/{}/day/{}/input", BASE_URL, year, day); + + let fetch_from_web = move || { let start = Instant::now(); - let url = format!("{}/{}/day/{}/input", BASE_URL, year, day); - let session_cookie = format!("session={}", get_conn_token()?); let resp = attohttpc::get(&url) - .header(COOKIE, session_cookie) + .header(COOKIE, session_cookie()?) .header(USER_AGENT, USER_AGENT_VALUE) .send()?; let elapsed = start.elapsed(); + let mut result = resp.text()?; - println!( - " - {}", - Line::new("downloaded input file").with_duration(elapsed) - ); + Line::new("download input file") + .with_duration(elapsed) + .println(); - resp.text() - })?; + if result.ends_with('\n') { + result.pop(); + } - if result.ends_with('\n') { - result.pop(); - } + Ok(result) + }; - Ok(result) + get_from_path_or_else(&input_path(year, day), fetch_from_web) } -fn get_conn_token() -> Result { +pub fn get_expected(year: u16, day: u8, part: u8) -> Result, Error> { + let pattern = + Regex::new(r"Your puzzle answer was (.*)\.").expect("could no build pattern"); + + let url = format!("https://adventofcode.com/{year}/day/{day}"); + + let fetch_from_web = move || { + let start = Instant::now(); + + let resp = attohttpc::get(&url) + .header(COOKIE, session_cookie()?) + .header(USER_AGENT, USER_AGENT_VALUE) + .send()?; + + let elapsed = start.elapsed(); + let body = resp.text()?; + Line::new("get expected").with_duration(elapsed).println(); + + let Some(found) = pattern.captures_iter(&body).nth(usize::from(part) - 1) else { + return Ok(None); + }; + + let expected = found.get(1).expect("no capture in pattern").as_str(); + Ok(Some(expected.to_string())) + }; + + try_get_from_path_or_else(&output_path(year, day, part), fetch_from_web) +} + +fn get_conn_token() -> Result { get_from_path_or_else(&token_path(), || { let mut stdout = stdout(); write!(&mut stdout, "Write your connection token: ")?; @@ -67,20 +118,44 @@ fn get_conn_token() -> Result { }) } -fn get_from_path_or_else( +fn get_from_path_or_else( path: &Path, - fallback: impl FnOnce() -> Result, -) -> Result { + fallback: impl FnOnce() -> Result, +) -> Result { let from_path = read_to_string(path); if let Ok(res) = from_path { Ok(res) } else { let res = fallback()?; + create_dir_all(path.parent().expect("no parent directory")) .and_then(|_| File::create(path)) .and_then(|mut file| file.write_all(res.as_bytes())) .unwrap_or_else(|err| eprintln!("could not write {}: {}", path.display(), err)); + + Ok(res) + } +} + +fn try_get_from_path_or_else( + path: &Path, + fallback: impl FnOnce() -> Result, Error>, +) -> Result, Error> { + let from_path = read_to_string(path); + + if let Ok(res) = from_path { + Ok(Some(res)) + } else { + let res = fallback()?; + + if let Some(res) = &res { + create_dir_all(path.parent().expect("no parent directory")) + .and_then(|_| File::create(path)) + .and_then(|mut file| file.write_all(res.as_bytes())) + .unwrap_or_else(|err| eprintln!("could not write {}: {}", path.display(), err)); + } + Ok(res) } } diff --git a/src/lib.rs b/src/lib.rs index 495cb04..e5c0f79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,160 +1,278 @@ pub mod input; -pub mod parse; +pub mod params; +pub mod step; pub mod utils; -// Reexport some crates for the generated main -pub use clap; -pub use colored; - -#[cfg(feature = "bench")] -pub use criterion; - -use clap::{Arg, ArgAction, Command, ValueHint}; - -pub fn args(year: u16) -> Command { - Command::new(format!("Advent of Code {year}")) - .about(format!( - "Main page of the event: https://adventofcode.com/{year}/" - )) - .arg( - Arg::new("stdin") - .short('i') - .long("stdin") - .action(ArgAction::SetTrue) - .conflicts_with("file") - .help("Read input from stdin instead of downloading it"), - ) - .arg( - Arg::new("file") - .short('f') - .long("file") - .conflicts_with("stdin") - .value_hint(ValueHint::FilePath) - .help("Read input from file instead of downloading it"), - ) - .arg( - Arg::new("days") - .short('d') - .long("day") - .value_name("day num") - .help("Days to execute. By default all implemented days will run"), - ) - .arg( - Arg::new("bench") - .short('b') - .long("bench") - .action(ArgAction::SetTrue) - .help("Run criterion benchmarks"), - ) - .arg( - Arg::new("all") - .short('a') - .long("all") - .action(ArgAction::SetTrue) - .conflicts_with("days") - .help("Run all days"), - ) +use std::borrow::Borrow; +use std::fmt::Display; +use std::time::Instant; + +use colored::Colorize; + +use crate::input::{get_expected, get_input_data}; +use crate::params::{get_params, DayChoice, InputChoice, Params}; +use crate::step::Step; +use crate::utils::{leak, Line, Status}; + +pub type Error = Box; + +type Day = u8; +type Year = u16; + +pub struct Solution { + ident: &'static str, + implem: Box>, } -#[macro_export] -macro_rules! base_main { - ( year $year: expr; $( $tail: tt )* ) => { - use std::fs::read_to_string; - use std::io::Read; - use std::time::Instant; +pub struct DaySolutions { + year: Year, + day: Day, + generator: Box>, + solutions: Vec>, +} - use $crate::{bench_day, extract_day, parse, run_day}; +impl DaySolutions { + pub fn new(year: Year, day: Day, generator: G) -> Self + where + G: Step<&'static mut String, I> + 'static, + { + let generator = Box::new(generator); - const YEAR: u16 = $year; + Self { + year, + day, + generator, + solutions: Vec::new(), + } + } - fn main() { - let mut opt = $crate::args(YEAR).get_matches(); - - let days: Vec<_> = { - if let Some(opt_days) = opt.get_many::("days") { - let opt_days: Vec<&str> = opt_days.map(|s| s.as_str()).collect(); - let days = parse! { extract_day {}; $( $tail )* }; - - let ignored_days: Vec<_> = opt_days - .iter() - .filter(|day| !days.contains(&format!("day{day}").as_str())) - .copied() - .collect(); - - if !ignored_days.is_empty() { - eprintln!(r"/!\ Ignoring unimplemented days: {}", ignored_days.join(", ")); - } - - opt_days - .into_iter() - .filter(|day| days.contains(&format!("day{}", day).as_str())) - .collect() - } else if opt.get_flag("all") { - parse!(extract_day {}; $( $tail )*) - .iter() - .map(|s| &s[3..]) - .collect() - } else { - // Get most recent day, assuming the days are sorted - vec![parse!(extract_day {}; $( $tail )*) - .iter() - .map(|s| &s[3..]) - .last() - .expect("No day implemenations found")] + pub fn with_solution(mut self, ident: &'static str, implem: F) -> Self + where + V: ?Sized + 'static, + I: Borrow + 'static, + F: Step<&'static V, O> + 'static, + O: Display + 'static, + { + let implem = + Box::new(move |input: &'static I| implem.run(input.borrow()).map(|x| x.to_string())); + + self.solutions.push(Solution { ident, implem }); + self + } +} + +pub trait DayTrait { + fn day(&self) -> Day; + fn run(&self, params: &Params); + + #[cfg(feature = "bench")] + fn bench(&self, params: &Params, criterion: &mut criterion::Criterion); +} + +impl DayTrait for DaySolutions { + fn day(&self) -> Day { + self.day + } + + fn run(&self, params: &Params) { + println!("Day {}:", self.day); + let data = leak(get_input_data(self.day, self.year, ¶ms.input)); + let extract_part = regex::Regex::new(r"part(\d+)").unwrap(); + + let input = { + let start = Instant::now(); + let res = self.generator.run(data); + let line = Line::new("generator").with_duration(start.elapsed()); + + match res { + Ok(x) => { + line.println(); + leak(x) + } + Err(err) => { + line.with_output(err.red()).println(); + return; } + } + }; + + for solution in &self.solutions { + let start = Instant::now(); + let res = solution.implem.run(input); + let mut line = Line::new(solution.ident).with_duration(start.elapsed()); + + let get_expected = || { + let part: u8 = extract_part + .captures(solution.ident)? + .get(1)? + .as_str() + .parse() + .ok()?; + + // TODO: display errors + get_expected(self.year, self.day(), part) + .map_err(|err| eprintln!("{err}")) + .ok() + .flatten() }; - if opt.get_flag("bench") { - bench(days); - } else { - if days.len() > 1 && (opt.contains_id("stdin") || opt.contains_id("file")) { - eprintln!(r"/!\ You are using a personalized output over several days which can"); - eprintln!(r" be missleading. If you only intend to run solutions for a"); - eprintln!(r" specific day, you can specify it by using the `-d DAY_NUM` flag."); - } + if params.check { + line = line.with_status(match get_expected() { + None => Status::Warn, + x if x.as_ref() == res.as_ref().ok() => Status::Ok, + _ => Status::Err, + }); + } + + match res { + Ok(x) => line.with_output(x.normal()).println(), + Err(err) => line.with_output(err.red()).println(), + } + } + } + + #[cfg(feature = "bench")] + fn bench(&self, params: &Params, criterion: &mut criterion::Criterion) { + let mut group = criterion.benchmark_group(format!("day{}", self.day)); + let data = leak(get_input_data(self.day, self.year, ¶ms.input)); + + let input = { + let res = self.generator.run(data); - for (i, day) in days.iter().enumerate() { - parse! { - run_day { i, format!("day{}", day), YEAR, opt }; - $( $tail )* - }; + match res { + Ok(x) => x, + Err(err) => { + eprintln!( + r"/!\ Skipping day {} because generator failed: {err}", + self.day, + ); + + return; } } + }; + + let input = leak(input); + + for solution in &self.solutions { + group.bench_function(solution.ident, |b| b.iter(|| solution.implem.run(input))); + // group.bench_with_input(solution.ident, input, |b, input| { + // b.iter(move || solution.implem.run(input)) + // }); } + + group.finish(); } } -#[cfg(feature = "bench")] -#[macro_export] -macro_rules! main { - ( year $year: expr; $( $tail: tt )* ) => { - $crate::base_main! { year $year; $( $tail )* } +pub fn parse_day_ident(ident: &str) -> Result { + let day = ident + .strip_prefix("day") + .ok_or_else(|| "day modules should be in the form dayXX".to_string())?; + + day.parse() + .map_err(|err| format!("invalid day {day}: {err}")) +} + +pub fn run_main(year: Year, days: &[Box]) { + let params = get_params(year); + + if !matches!(params.input, InputChoice::Download) && params.days.has_multiple_choices() { + eprintln!(r"/!\ You are using a personalized output over several days which can"); + eprintln!(r" be missleading. If you only intend to run solutions for a"); + eprintln!(r" specific day, you can specify it by using the `-d DAY_NUM` flag."); + eprintln!(); + } - use $crate::criterion::Criterion; + let days: Vec<_> = match params.days { + DayChoice::All => days.iter().collect(), + DayChoice::Latest => days.last().into_iter().collect(), + DayChoice::Select(ref selected) => selected + .iter() + .filter_map(|selected_day| days.iter().find(|day| day.day() == *selected_day)) + .collect(), + }; - fn bench(days: Vec<&str>) { - let mut criterion = Criterion::default().with_output_color(true); + if params.bench { + #[cfg(feature = "bench")] + { + let mut criterion = criterion::Criterion::default() + .with_output_color(true) + .warm_up_time(std::time::Duration::from_millis(200)) + .measurement_time(std::time::Duration::from_millis(1000)); - for day in days.into_iter() { - parse! { - bench_day { &mut criterion, format!("day{}", day), YEAR }; - $( $tail )* - }; + for day in days { + day.bench(¶ms, &mut criterion); } criterion.final_summary(); } + #[cfg(not(feature = "bench"))] + { + eprintln!(r"/!\ You are using option --bench but the 'bench' feature is disabled,"); + eprintln!(r" please update dependancy to aoc-main in your Cargo.toml."); + } + } else { + for day in days { + day.run(¶ms); + } } } -#[cfg(not(feature = "bench"))] +#[macro_export] +macro_rules! with_fallback { + ( $true: expr, $false: expr ) => { + $true + }; + ( , $false: expr ) => { + $false + }; +} + #[macro_export] macro_rules! main { - ( year $year: expr; $( $tail: tt )* ) => { - $crate::base_main! { year $year; $( $tail )* } + ( + year $year: expr; + $( + $day: ident + $( : $generator: ident $( ? $([$($_gen:tt)* $opt_gen:tt])? )? )? + => $( $part: ident $( ? $([$($_imp:tt)* $opt_imp:tt])? )? ),+ ; )* + ) => { + use $crate::{DaySolutions, DayTrait, parse_day_ident, run_main}; + use $crate::step::{GeneratorInput, InfaillibleStep, Step}; + + const YEAR: u16 = $year; + + fn main() { + let days: &[Box] = &[ + $(Box::new( + DaySolutions::new( + YEAR, + parse_day_ident(&stringify!($day)).expect("failed to parse day"), + $crate::with_fallback!( + $(|mut input| { + $crate::with_fallback!( + $( $($opt_gen)? $day::$generator )?, + InfaillibleStep($day::$generator) + ).run(GeneratorInput::take_buffer(input)) + })?, + InfaillibleStep(|x: &'static mut String| std::mem::take(x)) + ) + ) + + $( + .with_solution( + stringify!($part), + $crate::with_fallback!( + $( $($opt_imp)? $day::$part )?, + InfaillibleStep($day::$part) + ), + ) + )* + )),+ + ]; - fn bench(days: Vec<&str>) { - println!("Benchmarks not available, please enable `bench` feature for cargo-main."); + run_main(YEAR, &days); } } } diff --git a/src/params.rs b/src/params.rs new file mode 100644 index 0000000..7d15a8f --- /dev/null +++ b/src/params.rs @@ -0,0 +1,112 @@ +use std::path::PathBuf; + +use clap::{value_parser, Arg, ArgAction, Command, ValueHint}; + +use crate::Day; + +pub enum InputChoice { + Download, + Stdin, + File(PathBuf), +} + +pub enum DayChoice { + All, + Latest, + Select(Vec), +} + +impl DayChoice { + pub fn has_multiple_choices(&self) -> bool { + match self { + DayChoice::All => true, + DayChoice::Latest => false, + DayChoice::Select(days) => days.len() > 1, + } + } +} + +pub struct Params { + pub input: InputChoice, + pub days: DayChoice, + pub check: bool, + pub bench: bool, +} + +pub fn get_params(year: u16) -> Params { + let args = Command::new(format!("Advent of Code {year}")) + .about(format!( + "Main page of the event: https://adventofcode.com/{year}/" + )) + .arg( + Arg::new("stdin") + .short('i') + .long("stdin") + .action(ArgAction::SetTrue) + .conflicts_with("file") + .help("Read input from stdin instead of downloading it"), + ) + .arg( + Arg::new("file") + .short('f') + .long("file") + .conflicts_with("stdin") + .value_hint(ValueHint::FilePath) + .help("Read input from file instead of downloading it"), + ) + .arg( + Arg::new("days") + .short('d') + .long("day") + .value_name("day num") + .value_parser(value_parser!(Day)) + .help("Days to execute. By default only latest days will run"), + ) + .arg( + Arg::new("check") + .short('n') + .long("no-check") + .action(ArgAction::SetFalse) + .help("Don't perform result validation"), + ) + .arg( + Arg::new("all") + .short('a') + .long("all") + .action(ArgAction::SetTrue) + .conflicts_with("days") + .help("Run all days"), + ) + .arg( + Arg::new("bench") + .short('b') + .long("bench") + .action(ArgAction::SetTrue) + .help("Run criterion benchmarks"), + ) + .get_matches(); + + let input = match (args.get_flag("stdin"), args.get_one::("file")) { + (false, None) => InputChoice::Download, + (true, None) => InputChoice::Stdin, + (false, Some(path)) => InputChoice::File(path.clone()), + (true, Some(_)) => panic!("You can't specify both stdin and file inputs"), + }; + + let days = match (args.get_many::("days"), args.get_flag("all")) { + (None, false) => DayChoice::Latest, + (None, true) => DayChoice::All, + (Some(days), false) => DayChoice::Select(days.copied().collect()), + (Some(_), true) => panic!("You can't specify days with the --all option"), + }; + + let check = args.get_flag("check"); + let bench = args.get_flag("bench"); + + Params { + input, + days, + check, + bench, + } +} diff --git a/src/parse/gen_bench.rs b/src/parse/gen_bench.rs deleted file mode 100644 index 80bf100..0000000 --- a/src/parse/gen_bench.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Generate code for criterion benchmarks. - -#[macro_export] -macro_rules! bench_day { - ( - { $criterion: expr, $curr_day: expr, $year: expr }, - { day $day: ident { $gen: tt { $( $sol: tt )* } } } - ) => {{ - if stringify!($day) == $curr_day { - let day = $curr_day[3..].parse().expect("days must be integers"); - let data = $crate::input::get_input($year, day).expect("could not fetch input"); - let input = $crate::bench_gen!($day, &data, $gen); - - let mut group = $criterion.benchmark_group(stringify!($day)); - $( $crate::bench_sol!(&mut group, $day, &input, $sol); )+ - group.finish(); - } - }} -} - -// This is just a silent version of run_gen with more agressive exceptions. -#[macro_export] -macro_rules! bench_gen { - ( $day: ident, $data: expr, { gen_default } ) => {{ - $data - }}; - ( $day: ident, $data: expr, { gen $generator: ident } ) => {{ - $day::$generator($data) - }}; - ( $day: ident, $data: expr, { gen_fallible $generator: ident } ) => {{ - use std::fmt::*; - $day::$generator($data).expect("failed to parse input") - }}; -} - -#[macro_export] -macro_rules! bench_sol { - ( $group: expr, $day: ident, $input: expr, { $kind: tt $solution: ident } ) => {{ - $group.bench_function(stringify!($solution), |b| { - b.iter(|| $day::$solution($input)) - }); - }}; -} diff --git a/src/parse/gen_run.rs b/src/parse/gen_run.rs deleted file mode 100644 index b60480c..0000000 --- a/src/parse/gen_run.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Generate code to run solutions. - -#[macro_export] -macro_rules! run_day { - ( - { $i: expr, $curr_day: expr, $year: expr, $opt: expr }, - { day $day: ident { $gen: tt { $( $sol: tt )* } } } - ) => {{ - if stringify!($day) == $curr_day { - if $i != 0 { println!() } - let day = $curr_day[3..].parse().expect("days must be integers"); - println!("Day {}", day); - - let data = { - if $opt.get_flag("stdin") { - let mut data = String::new(); - std::io::stdin().read_to_string(&mut data) - .expect("failed to read from stdin"); - data - } else if let Some(path) = $opt.get_one::("file") { - read_to_string(path) - .expect("failed to read specified file") - } else { - $crate::input::get_input($year, day).expect("could not fetch input") - } - }; - - if let Some(input) = $crate::run_gen!($day, &data, $gen) { - $( $crate::run_sol!($day, &input, $sol); )+ - } else { - $( $crate::skip_sol!($sol); )+ - } - } - }} -} - -#[macro_export] -macro_rules! run_gen { - // No generator is needed: default behavior is to just pass input &str - ( $day: ident, $data: expr, { gen_default } ) => {{ - Some($data) - }}; - - // Run generator - ( $day: ident, $data: expr, { gen $generator: ident } ) => {{ - use $crate::utils::Line; - - let start = Instant::now(); - let input = $day::$generator($data); - let elapsed = start.elapsed(); - println!(" - {}", Line::new("generator").with_duration(elapsed)); - Some(input) - }}; - - // Run fallible generator - ( $day: ident, $data: expr, { gen_fallible $generator: ident } ) => {{ - use $crate::colored::*; - use $crate::utils::{Line, TryUnwrap}; - - let start = Instant::now(); - let result = $day::$generator($data); - let elapsed = start.elapsed(); - - match result.try_unwrap() { - Ok(input) => { - println!(" - {}", Line::new("generator").with_duration(elapsed)); - Some(input) - } - Err(msg) => { - println!( - " - {}", - Line::new("generator") - .with_duration(elapsed) - .with_state(msg.red()) - ); - None - } - } - }}; -} - -#[macro_export] -macro_rules! run_sol { - // Run solution - ( $day: ident, $input: expr, { sol $solution: ident } ) => {{ - use $crate::colored::*; - use $crate::utils::Line; - - let start = Instant::now(); - let response = $day::$solution($input); - let elapsed = start.elapsed(); - - println!( - " - {}", - Line::new(stringify!($solution)) - .with_duration(elapsed) - .with_state(format!("{}", response).normal()) - ); - }}; - - // Run fallible solution - ( $day: ident, $input: expr, { sol_fallible $solution: ident } ) => {{ - use $crate::colored::*; - use $crate::utils::{Line, TryUnwrap}; - - let start = Instant::now(); - let response = $day::$solution($input); - let elapsed = start.elapsed(); - let line = Line::new(stringify!($solution)).with_duration(elapsed); - - println!( - " - {}", - match response.try_unwrap() { - Ok(response) => { - line.with_state(format!("{}", response).normal()) - } - Err(msg) => { - line.with_state(msg.red()) - } - } - ); - }}; -} - -#[macro_export] -macro_rules! skip_sol { - ({ $kind: tt $solution: ident }) => {{ - use $crate::colored::*; - use $crate::utils::Line; - - println!( - " - {}", - Line::new(stringify!($solution)).with_state("skipped".bright_black()) - ); - }}; -} diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index 527b90b..0000000 --- a/src/parse/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Macro used to parse input tokens. - -mod gen_bench; -mod gen_run; - -// Call `apply` macro on this generated form of token tree; -// $ctx, { day DAY { { gen GENERATOR } { { sol SOLUTION } { sol SOLUTION } } } } -#[macro_export] -macro_rules! parse { - // Read day: default generator - ( - day $apply: ident, $ctx: tt, $val: expr; - $day: ident => $( $tail: tt )* - ) => { - $crate::parse!( - sol $apply, $ctx, $val; - { day $day { { gen_default } { } } }; $( $tail )* - ) - }; - - // Read day: regular generator - ( - day $apply: ident, $ctx: tt, $val: expr; - $day: ident : $generator: ident => $( $tail: tt )* - ) => { - $crate::parse!( - sol $apply, $ctx, $val; - { day $day { { gen $generator } { } } }; $( $tail )* - ) - }; - - // Read day: fallible generator - ( - day $apply: ident, $ctx: tt, $val: expr; - $day: ident : $generator: ident ? => $( $tail: tt )* - ) => { - $crate::parse!( - sol $apply, $ctx, $val; - { day $day { { gen_fallible $generator } { } } }; $( $tail )* - ) - }; - - // Empty rules - ( day $apply: ident, $ctx: tt, $val: expr; ) => {}; - - // Read fallible solution - ( - sol $apply: ident, $ctx: tt, $val: expr; - { day $day: tt { $gen: tt { $( $acc: tt )* } } } ; - $sol: ident ? $( $tail: tt )* - ) => { - $crate::parse!( - post_sol $apply, $ctx, $val; - { day $day { $gen { $( $acc )* { sol_fallible $sol } } } }; $( $tail )* - ) - }; - - // Read solution - ( - sol $apply: ident, $ctx: tt, $val: expr; - { day $day: tt { $gen: tt { $( $acc: tt )* } } } ; - $sol: ident $( $tail: tt )* - ) => { - $crate::parse!( - post_sol $apply, $ctx, $val; - { day $day { $gen { $( $acc )* { sol $sol } } } }; $( $tail )* - ) - }; - - // After solution: there is new solutions - ( - post_sol $apply: ident, $ctx: tt, $val: expr; - $curr: tt ; , $( $tail: tt )* - ) => { - $crate::parse!(sol $apply, $ctx, $val; $curr; $( $tail )* ) - }; - - // After solution: end of day - ( - post_sol $apply: ident, $ctx: tt, $val: expr; - $curr: tt ; ; $( $tail: tt )* - ) => {{ - $val.push($apply!{ $ctx, $curr }); - $crate::parse!( day $apply, $ctx, $val; $( $tail )* ); - }}; - - // Initialization - ( $apply: ident $ctx: tt; $( $tt: tt )* ) => {{ - let mut val = Vec::new(); - $crate::parse!( day $apply, $ctx, val; $( $tt )* ); - val - }}; -} - -// Extract day names from a parsed token tree -#[macro_export] -macro_rules! extract_day { - ({}, { day $day: ident $other: tt }) => { - stringify!($day) - }; -} diff --git a/src/step.rs b/src/step.rs new file mode 100644 index 0000000..c7e8ea2 --- /dev/null +++ b/src/step.rs @@ -0,0 +1,81 @@ +use std::fmt::Display; + +/// A step that can't return an error +pub struct InfaillibleStep(pub F); + +/// A step that can convert buffer input +pub struct GeneratorStep(pub F); + +pub trait Step { + fn run(&self, input: I) -> Result; +} + +impl Step for F +where + F: Fn(I) -> FO + 'static, + FO: StepResult, +{ + fn run(&self, input: I) -> Result { + self(input).into_result() + } +} + +impl Step for InfaillibleStep +where + F: Fn(I) -> O + 'static, +{ + fn run(&self, input: I) -> Result { + Ok((self.0)(input)) + } +} + +/// Represent a feasible generator input +pub trait GeneratorInput { + fn take_buffer(buffer: &'static mut String) -> Self; +} + +impl GeneratorInput for &'static str { + fn take_buffer(buffer: &'static mut String) -> Self { + buffer + } +} + +impl GeneratorInput for &'static [u8] { + fn take_buffer(buffer: &'static mut String) -> Self { + buffer.as_bytes() + } +} + +impl GeneratorInput for String { + fn take_buffer(buffer: &mut String) -> Self { + std::mem::take(buffer) + } +} + +impl GeneratorInput for Vec { + fn take_buffer(buffer: &mut String) -> Self { + std::mem::take(buffer).into_bytes() + } +} + +/// Represent a faillible result that can be extracted in a generic way. +pub trait StepResult { + type Val; + fn into_result(self) -> Result; +} + +impl StepResult for Option { + type Val = T; + + fn into_result(self) -> Result { + self.ok_or_else(|| "no output".to_string()) + } +} + +impl StepResult for Result { + type Val = T; + + fn into_result(self) -> Result { + self.map_err(|err| err.to_string()) + } +} diff --git a/src/utils.rs b/src/utils.rs index 73e3176..9c16b5b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,10 @@ use std::cmp::min; use std::fmt; use std::time::Duration; +pub fn leak(x: T) -> &'static mut T { + Box::leak(Box::new(x)) +} + // --- // --- TryUnwrap: wrapper for Option and Result // --- @@ -39,14 +43,23 @@ impl TryUnwrap for Result { // --- Line: helper struct for printing // --- -const DEFAULT_WIDTH: usize = 30; +const PREFIX: &str = " "; +const DEFAULT_WIDTH: usize = 40; + +#[derive(Clone, Copy)] +pub enum Status { + Ok, + Err, + Warn, +} /// Simple helper struct used to display lines with a dotted separator. -/// For example: "line text (1.2 ms) .............. status". +/// For example: " - line text (1.2 ms) .............. status". pub struct Line { text: String, duration: Option, - state: Option, + output: Option, + status: Option, } impl Line { @@ -54,7 +67,8 @@ impl Line { Self { text: text.into(), duration: None, - state: None, + output: None, + status: None, } } @@ -63,10 +77,19 @@ impl Line { self } - pub fn with_state(mut self, state: impl Into) -> Self { - self.state = Some(state.into()); + pub fn with_output(mut self, state: impl Into) -> Self { + self.output = Some(state.into()); + self + } + + pub fn with_status(mut self, status: Status) -> Self { + self.status = Some(status); self } + + pub fn println(&self) { + println!("{self}"); + } } impl fmt::Display for Line { @@ -78,13 +101,28 @@ impl fmt::Display for Line { .map(|duration| format!(" ({:.2?})", duration)) .unwrap_or_else(String::new); - write!(f, "{}{}", self.text, duration.bright_black())?; + write!(f, "{PREFIX}{}{}", self.text, duration.bright_black())?; - if let Some(state) = &self.state { + let show_status = |f: &mut fmt::Formatter| { + if let Some(status) = self.status { + let status_str = match status { + Status::Ok => "✓".green(), + Status::Err => "✗".red(), + Status::Warn => "⁉".yellow(), + }; + + write!(f, " {}", status_str) + } else { + Ok(()) + } + }; + + if let Some(state) = &self.output { let width = self.text.chars().count() + 1 + duration.chars().count(); let dots = display_width - min(display_width - 5, width) - 2; - let dots = ".".repeat(dots); - write!(f, " {}", dots.bright_black())?; + let dots = ".".repeat(dots).bright_black(); + write!(f, " {dots}")?; + show_status(f)?; if state.contains('\n') { for line in state.trim_matches('\n').lines() { @@ -93,6 +131,8 @@ impl fmt::Display for Line { } else { write!(f, " {}", state.clone().bold())?; } + } else { + show_status(f)?; } Ok(())