diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 66bc1dfea0..c57bc95dd6 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -188,6 +188,8 @@ bin = [ { name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" }, { name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" }, { name = "conversions5_sol", path = "../solutions/23_conversions/conversions5.rs" }, + { name = "async1", path = "../exercises/24_async/async1.rs" }, + { name = "async1_sol", path = "../solutions/24_async/async1.rs" }, ] [package] @@ -196,6 +198,9 @@ edition = "2024" # Don't publish the exercises on crates.io! publish = false +[dependencies] +tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } + [profile.release] panic = "abort" diff --git a/exercises/24_async/README.md b/exercises/24_async/README.md new file mode 100644 index 0000000000..4e61bc32b2 --- /dev/null +++ b/exercises/24_async/README.md @@ -0,0 +1,13 @@ +# Async + +Asynchronous programming is a model where tasks are delegated to a runtime that executes them concurrently. +It is particularly efficient for applications where many independent IO-operations are performed, e.g. web servers. + +Rust provides the necessary primitives to do asynchronous programming in the language. +However, Rust's standard library does not include a runtime. +For these exercises, we will use the mainstream runtime called `tokio`. + +## Further information + +- [Fundamentals of Asynchronous Programming](https://doc.rust-lang.org/book/ch17-00-async-await.html) +- [Tokio documentation](https://docs.rs/tokio/latest/tokio/) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs new file mode 100644 index 0000000000..aa61ea70e2 --- /dev/null +++ b/exercises/24_async/async1.rs @@ -0,0 +1,44 @@ +// Alice is an elementary school teacher who needs to calculate the mean test +// score for three classes she teaches. Instead of calculating them one after +// the other, she decides to ask her friends Bob and Catherine for help. Working +// together, they can finish the job much faster. +// +// Let's simulate this using asynchronous programming. Each person is +// represented as an asynchronous task, which can be executed concurrently. + +const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; +const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; +const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; + +// Async tasks need to be executed by a "runtime", which is not provided by +// Rust's standard library. Here, we use the mainstream runtime `tokio`. +// The macro `tokio::main` wraps the entire main function in a runtime. +#[tokio::main] +async fn main() { + let mean_score_a = tokio::spawn(calculate_mean_score(SCORES_CLASS_A)); + let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); + let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); + + // TODO: Await the spawned tasks to check their results. + assert_eq!(mean_score_a, 84); // alice + assert_eq!(mean_score_b, 89); // bob + assert_eq!(mean_score_c, 76); // catherine +} + +// TODO: Fix the compiler errors by making the spawned function async. +fn calculate_mean_score(scores_file: &str) -> usize { + // Read the file asynchronously + let file = tokio::fs::read_to_string(scores_file).await.unwrap(); + + // Initialize the sum and the number of scores + let mut sum = 0; + let mut n = 0; + for line in file.lines() { + // Parse every line as a score + let score = line.parse::().unwrap(); + sum += score; + n += 1; + } + + sum / n +} diff --git a/exercises/24_async/scores_class_a.txt b/exercises/24_async/scores_class_a.txt new file mode 100644 index 0000000000..29fc7d8ffb --- /dev/null +++ b/exercises/24_async/scores_class_a.txt @@ -0,0 +1,3 @@ +83 +77 +92 diff --git a/exercises/24_async/scores_class_b.txt b/exercises/24_async/scores_class_b.txt new file mode 100644 index 0000000000..7bbc2e8e22 --- /dev/null +++ b/exercises/24_async/scores_class_b.txt @@ -0,0 +1,3 @@ +84 +88 +96 diff --git a/exercises/24_async/scores_class_c.txt b/exercises/24_async/scores_class_c.txt new file mode 100644 index 0000000000..cda6e81041 --- /dev/null +++ b/exercises/24_async/scores_class_c.txt @@ -0,0 +1,3 @@ +71 +83 +76 diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 3a1cac3537..01c47c4f69 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1211,3 +1211,22 @@ name = "conversions5" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" + +# ASYNC + +[[exercises]] +name = "async1" +dir = "24_async" +test = false +input_files = [ + "scores_class_a.txt", + "scores_class_b.txt", + "scores_class_c.txt", +] +hint = """ +Asynchronous runtimes like tokio can only spawn tasks that are defined as async +functions, not regular ones. Add the "async" keyword before the "fn" keyword of +the functions "tim", "carl" and "nick". + +An async task can wait for another one to complete by "awaiting" it. Add +".await" after the three "task_name" variables in the "block_on" call.""" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index db758d5945..b672b850a2 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -6,6 +6,8 @@ use serde::Deserialize; struct ExerciseInfo<'a> { name: &'a str, dir: &'a str, + #[serde(default)] + input_files: Vec<&'a str>, } #[derive(Deserialize)] @@ -17,9 +19,8 @@ struct InfoFile<'a> { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { let info_file = include_str!("../info.toml"); - let exercises = toml::de::from_str::(info_file) - .expect("Failed to parse `info.toml`") - .exercises; + let info = toml::de::from_str::(info_file).expect("Failed to parse `info.toml`"); + let exercises = info.exercises; let exercise_files = exercises .iter() @@ -42,6 +43,20 @@ pub fn include_files(_: TokenStream) -> TokenStream { *dir_ind = dirs.len() - 1; } + let input_files = exercises.iter().map(|exercise| { + let names = exercise.input_files.iter(); + let paths = exercise + .input_files + .iter() + .map(|f| format!("../exercises/{}/{}", exercise.dir, f)); + quote! { + &[#(InputFile { + name: #names, + content: include_str!(#paths), + }),*] + } + }); + let readmes = dirs .iter() .map(|dir| format!("../exercises/{dir}/README.md")); @@ -49,8 +64,13 @@ pub fn include_files(_: TokenStream) -> TokenStream { quote! { EmbeddedFiles { info_file: #info_file, - exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], - exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] + exercise_files: &[#(ExerciseFiles { + exercise: include_bytes!(#exercise_files), + solution: include_bytes!(#solution_files), + dir_ind: #dir_inds, + input_files: #input_files, + }),*], + exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*], } } .into() diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs new file mode 100644 index 0000000000..97121c7797 --- /dev/null +++ b/solutions/24_async/async1.rs @@ -0,0 +1,42 @@ +// Alice is an elementary school teacher who needs to calculate the mean test +// score for three classes she teaches. Instead of calculating them one after +// the other, she decides to ask her friends Bob and Catherine for help. Working +// together, they can finish the job much faster. +// +// Let's simulate this using asynchronous programming. Each person is +// represented as an asynchronous task, which can be executed concurrently. + +const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; +const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; +const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; + +// Async tasks need to be executed by a "runtime", which is not provided by +// Rust's standard library. Here, we use the mainstream runtime `tokio`. +// The macro `tokio::main` wraps the entire main function in a runtime. +#[tokio::main] +async fn main() { + let mean_score_a = tokio::spawn(calculate_mean_score(SCORES_CLASS_A)); + let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); + let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); + + assert_eq!(mean_score_a.await.unwrap(), 84); // alice + assert_eq!(mean_score_b.await.unwrap(), 89); // bob + assert_eq!(mean_score_c.await.unwrap(), 76); // catherine +} + +async fn calculate_mean_score(scores_file: &str) -> usize { + // Read the file asynchronously + let file = tokio::fs::read_to_string(scores_file).await.unwrap(); + + // Initialize the sum and the number of scores + let mut sum = 0; + let mut n = 0; + for line in file.lines() { + // Parse every line as a score + let score = line.parse::().unwrap(); + sum += score; + n += 1; + } + + sum / n +} diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 2407745218..fcbf0588db 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -110,6 +110,7 @@ mod tests { dir: None, test: true, strict_clippy: true, + input_files: vec![], hint: String::new(), skip_check_unsolved: false, }, @@ -118,6 +119,7 @@ mod tests { dir: Some("d"), test: false, strict_clippy: false, + input_files: vec![], hint: String::new(), skip_check_unsolved: false, }, diff --git a/src/dev/check.rs b/src/dev/check.rs index 58c2a174af..925f1d864b 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -133,19 +133,25 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { file_buf.clear(); - paths.insert(PathBuf::from(path)); + let path = PathBuf::from(path); + + for input_file in &exercise_info.input_files { + paths.insert(path.parent().unwrap().join(input_file)); + } + + paths.insert(path); } Ok(paths) } // Check `dir` for unexpected files. -// Only Rust files in `allowed_rust_files` and `README.md` files are allowed. +// Only files in `allowed_files` and `README.md` files are allowed. // Only one level of directory nesting is allowed. -fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> Result<()> { +fn check_unexpected_files(dir: &str, allowed_files: &HashSet) -> Result<()> { let unexpected_file = |path: &Path| { anyhow!( - "Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", + "Found the file `{}`. Only `README.md`, Rust files and input files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display() ) }; @@ -160,7 +166,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R continue; } - if !allowed_rust_files.contains(&path) { + if !allowed_files.contains(&path) { return Err(unexpected_file(&path)); } @@ -187,7 +193,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R continue; } - if !allowed_rust_files.contains(&path) { + if !allowed_files.contains(&path) { return Err(unexpected_file(&path)); } } diff --git a/src/embedded.rs b/src/embedded.rs index bee4119c2b..36263a9cce 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -17,6 +17,14 @@ struct ExerciseFiles { solution: &'static [u8], // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. dir_ind: usize, + // Files that are read by the exercise. + input_files: &'static [InputFile], +} + +// Input files that may be read by exercises. +pub struct InputFile { + pub name: &'static str, + pub content: &'static str, } fn create_dir_if_not_exists(path: &str) -> Result<()> { @@ -90,6 +98,12 @@ impl EmbeddedFiles { fs::write(&exercise_path, exercise_files.exercise) .with_context(|| format!("Failed to write the exercise file {exercise_path}"))?; + + for InputFile { name, content } in exercise_files.input_files { + let path = format!("{prefix}/{dir_name}/{name}", dir_name = dir.name); + fs::write(&path, content) + .with_context(|| format!("Failed to write the input file {path}"))?; + } } Ok(()) diff --git a/src/info_file.rs b/src/info_file.rs index da4086aa3e..ece40afbb7 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -17,6 +17,9 @@ pub struct ExerciseInfo<'a> { /// Deny all Clippy warnings. #[serde(default)] pub strict_clippy: bool, + // Files that are read by the exercise. + #[serde(default)] + pub input_files: Vec<&'a str>, /// The exercise's hint to be shown to the user on request. pub hint: String, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved.