From 77d3b684cb467ad4e06c211518e2d2d1c7346ad1 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 22:28:37 +0200 Subject: revival of the async-io functions --- exercises/084_async.zig | 78 +++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 44 deletions(-) (limited to 'exercises/084_async.zig') diff --git a/exercises/084_async.zig b/exercises/084_async.zig index 56c9969..48bda2b 100644 --- a/exercises/084_async.zig +++ b/exercises/084_async.zig @@ -1,58 +1,48 @@ // -// Six Facts: +// In previous versions of Zig, async/await used special keywords +// like 'suspend', 'resume', and 'async' that operated on stackframes +// directly. Those keywords no longer exist! // -// 1. The memory space allocated to your program for the -// invocation of a function and all of its data is called a -// "stack frame". +// Zig 0.16 replaced them with a unified I/O interface: std.Io. +// This interface uses a VTable pattern - a struct of function pointers - +// to abstract over different concurrency backends: // -// 2. The 'return' keyword "pops" the current function -// invocation's frame off of the stack (it is no longer needed) -// and returns control to the place where the function was -// called. +// * Threaded - classic thread-pool based I/O +// * Uring - Linux io_uring +// * Kqueue - BSD/macOS +// * Dispatch - macOS Grand Central Dispatch // -// fn foo() void { -// return; // Pop the frame and return control -// } -// -// 3. Like 'return', the 'suspend' keyword returns control to the -// place where the function was called BUT the function -// invocation's frame remains so that it can regain control again -// at a later time. Functions which do this are "async" -// functions. -// -// fn fooThatSuspends() void { -// suspend {} // return control, but leave the frame alone -// } +// The Io struct itself is tiny: // -// 4. To call any function in async context and get a reference -// to its frame for later use, use the 'async' keyword: +// const Io = struct { +// userdata: ?*anyopaque, // opaque state of the backend +// vtable: *const VTable, // table of function pointers +// }; // -// var foo_frame = async fooThatSuspends(); +// Your code receives an Io value and calls methods on it. +// The backend is chosen at initialization time - your code doesn't +// need to know which one it is! // -// 5. If you call an async function without the 'async' keyword, -// the function FROM WHICH you called the async function itself -// becomes async! In this example, the bar() function is now -// async because it calls fooThatSuspends(), which is async. +// In Zig 0.16, main() receives a std.process.Init struct to opt +// into I/O and concurrency support: // -// fn bar() void { -// fooThatSuspends(); +// pub fn main(init: std.process.Init) !void { +// const io = init.io; +// // ... use io ... // } // -// 6. The main() function cannot be async! +// Let's start simple. Fix the main function to extract the Io +// interface from init, then use it to get the current time. // -// Given facts 3 and 4, how do we fix this program (broken by facts -// 5 and 6)? -// -const print = @import("std").debug.print; +const std = @import("std"); -pub fn main() void { - // Additional Hint: you can assign things to '_' when you - // don't intend to do anything with them. - foo(); -} +pub fn main(init: std.process.Init) !void { + const io = init.???; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). + const timestamp = std.Io.Timestamp.now(io, .real); -fn foo() void { - print("foo() A\n", .{}); - suspend {} - print("foo() B\n", .{}); + // Print the timestamp in seconds since the Unix epoch. + std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); } -- cgit v1.2.3 From 1c6487c1e79cbe0d59a39b483af8ec44b59c586e Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 18:11:00 +0200 Subject: added async-io quiz --- build.zig | 46 +++++---- exercises/084_async.zig | 48 --------- exercises/084_interfaces.zig | 127 ++++++++++++++++++++++++ exercises/085_async.zig | 48 +++++++++ exercises/085_async2.zig | 48 --------- exercises/086_async2.zig | 48 +++++++++ exercises/086_async3.zig | 50 ---------- exercises/087_async3.zig | 50 ++++++++++ exercises/087_async4.zig | 50 ---------- exercises/088_async4.zig | 50 ++++++++++ exercises/088_async5.zig | 61 ------------ exercises/089_async5.zig | 61 ++++++++++++ exercises/089_async6.zig | 71 ------------- exercises/090_async6.zig | 71 +++++++++++++ exercises/090_async7.zig | 57 ----------- exercises/091_async7.zig | 57 +++++++++++ exercises/091_async8.zig | 62 ------------ exercises/092_async8.zig | 62 ++++++++++++ exercises/092_async9.zig | 57 ----------- exercises/093_async10.zig | 67 ------------- exercises/093_async9.zig | 57 +++++++++++ exercises/094_async10.zig | 67 +++++++++++++ exercises/095_interfaces.zig | 127 ------------------------ exercises/095_quiz_async.zig | 186 +++++++++++++++++++++++++++++++++++ patches/patches/084_interfaces.patch | 11 +++ patches/patches/085_async.patch | 11 +++ patches/patches/086_async2.patch | 14 +++ patches/patches/087_async3.patch | 18 ++++ patches/patches/088_async4.patch | 11 +++ patches/patches/089_async5.patch | 11 +++ patches/patches/090_async6.patch | 11 +++ patches/patches/091_async7.patch | 13 +++ patches/patches/092_async8.patch | 11 +++ patches/patches/093_async9.patch | 11 +++ patches/patches/094_async10.patch | 13 +++ patches/patches/095_quiz_async.patch | 52 ++++++++++ 36 files changed, 1097 insertions(+), 718 deletions(-) delete mode 100644 exercises/084_async.zig create mode 100644 exercises/084_interfaces.zig create mode 100644 exercises/085_async.zig delete mode 100644 exercises/085_async2.zig create mode 100644 exercises/086_async2.zig delete mode 100644 exercises/086_async3.zig create mode 100644 exercises/087_async3.zig delete mode 100644 exercises/087_async4.zig create mode 100644 exercises/088_async4.zig delete mode 100644 exercises/088_async5.zig create mode 100644 exercises/089_async5.zig delete mode 100644 exercises/089_async6.zig create mode 100644 exercises/090_async6.zig delete mode 100644 exercises/090_async7.zig create mode 100644 exercises/091_async7.zig delete mode 100644 exercises/091_async8.zig create mode 100644 exercises/092_async8.zig delete mode 100644 exercises/092_async9.zig delete mode 100644 exercises/093_async10.zig create mode 100644 exercises/093_async9.zig create mode 100644 exercises/094_async10.zig delete mode 100644 exercises/095_interfaces.zig create mode 100644 exercises/095_quiz_async.zig create mode 100644 patches/patches/084_interfaces.patch create mode 100644 patches/patches/085_async.patch create mode 100644 patches/patches/086_async2.patch create mode 100644 patches/patches/087_async3.patch create mode 100644 patches/patches/088_async4.patch create mode 100644 patches/patches/089_async5.patch create mode 100644 patches/patches/090_async6.patch create mode 100644 patches/patches/091_async7.patch create mode 100644 patches/patches/092_async8.patch create mode 100644 patches/patches/093_async9.patch create mode 100644 patches/patches/094_async10.patch create mode 100644 patches/patches/095_quiz_async.patch (limited to 'exercises/084_async.zig') diff --git a/build.zig b/build.zig index b9aaac3..beb0d72 100644 --- a/build.zig +++ b/build.zig @@ -1109,20 +1109,29 @@ const exercises = [_]Exercise{ .main_file = "083_anonymous_lists.zig", .output = "I say hello!", }, + .{ + .main_file = "084_interfaces.zig", + .output = + \\Daily Insect Report: + \\Ant is alive. + \\Bee visited 17 flowers. + \\Grasshopper hopped 32 meters. + , // pay attention to the comma + }, // Skipped because of https://github.com/ratfactor/ziglings/issues/163 // direct link: https://github.com/ziglang/zig/issues/6025 .{ - .main_file = "084_async.zig", + .main_file = "085_async.zig", .output = "Current time: s since epoch", .timestamp = true, }, .{ - .main_file = "085_async2.zig", + .main_file = "086_async2.zig", .output = "Computing... the answer is: 42", }, .{ - .main_file = "086_async3.zig", + .main_file = "087_async3.zig", .output = \\1 + 2 = 3 \\6 * 7 = 42 @@ -1130,7 +1139,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "087_async4.zig", + .main_file = "088_async4.zig", .output = \\Task 1 done. \\Task 2 done. @@ -1139,7 +1148,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "088_async5.zig", + .main_file = "089_async5.zig", .output = \\Starting long computation... \\Canceling slow task... @@ -1148,19 +1157,19 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "089_async6.zig", + .main_file = "090_async6.zig", .output = "Hare: I'm fast!", }, .{ - .main_file = "090_async7.zig", + .main_file = "091_async7.zig", .output = "Counter: 400", }, .{ - .main_file = "091_async8.zig", + .main_file = "092_async8.zig", .output = "Sum of 1..10 = 55", }, .{ - .main_file = "092_async9.zig", + .main_file = "093_async9.zig", .output = \\Main thread continues... \\Computing on a separate thread! @@ -1169,7 +1178,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "093_async10.zig", + .main_file = "094_async10.zig", .output = \\Starting critical section... \\Critical section completed safely. @@ -1177,17 +1186,14 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "094_async_quiz.zig", - .output = "", - .skip = true, - }, - .{ - .main_file = "095_interfaces.zig", + .main_file = "095_quiz_async.zig", .output = - \\Daily Insect Report: - \\Ant is alive. - \\Bee visited 17 flowers. - \\Grasshopper hopped 32 meters. + \\=== Doctor Zoraptera's Garden Report === + \\Temperature : 23C + \\Humidity : 63% + \\Wind : 13 km/h + \\Readings : 9 + \\Bee-friendly conditions! Expect high pollination. , // pay attention to the comma }, .{ diff --git a/exercises/084_async.zig b/exercises/084_async.zig deleted file mode 100644 index 48bda2b..0000000 --- a/exercises/084_async.zig +++ /dev/null @@ -1,48 +0,0 @@ -// -// In previous versions of Zig, async/await used special keywords -// like 'suspend', 'resume', and 'async' that operated on stackframes -// directly. Those keywords no longer exist! -// -// Zig 0.16 replaced them with a unified I/O interface: std.Io. -// This interface uses a VTable pattern - a struct of function pointers - -// to abstract over different concurrency backends: -// -// * Threaded - classic thread-pool based I/O -// * Uring - Linux io_uring -// * Kqueue - BSD/macOS -// * Dispatch - macOS Grand Central Dispatch -// -// The Io struct itself is tiny: -// -// const Io = struct { -// userdata: ?*anyopaque, // opaque state of the backend -// vtable: *const VTable, // table of function pointers -// }; -// -// Your code receives an Io value and calls methods on it. -// The backend is chosen at initialization time - your code doesn't -// need to know which one it is! -// -// In Zig 0.16, main() receives a std.process.Init struct to opt -// into I/O and concurrency support: -// -// pub fn main(init: std.process.Init) !void { -// const io = init.io; -// // ... use io ... -// } -// -// Let's start simple. Fix the main function to extract the Io -// interface from init, then use it to get the current time. -// -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.???; - - // Get the current wall-clock time using the Io interface. - // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). - const timestamp = std.Io.Timestamp.now(io, .real); - - // Print the timestamp in seconds since the Unix epoch. - std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); -} diff --git a/exercises/084_interfaces.zig b/exercises/084_interfaces.zig new file mode 100644 index 0000000..7775dd5 --- /dev/null +++ b/exercises/084_interfaces.zig @@ -0,0 +1,127 @@ +// +// Remember our ant and bee simulator constructed with unions +// back in exercises 55 and 56? There, we demonstrated that +// unions allow us to treat different data types in a uniform +// manner. +// +// One neat feature was using tagged unions to create a single +// function to print a status for ants *or* bees by switching: +// +// switch (insect) { +// .still_alive => ... // (print ant stuff) +// .flowers_visited => ... // (print bee stuff) +// } +// +// Well, that simulation was running just fine until a new insect +// arrived in the virtual garden, a grasshopper! +// +// Doctor Zoraptera started to add grasshopper code to the +// program, but then she backed away from her keyboard with an +// angry hissing sound. She had realized that having code for +// each insect in one place and code to print each insect in +// another place was going to become unpleasant to maintain when +// the simulation expanded to hundreds of different insects. +// +// Thankfully, Zig has another comptime feature we can use +// to get out of this dilemma called the 'inline else'. +// +// We can replace this redundant code: +// +// switch (thing) { +// .a => |a| special(a), +// .b => |b| normal(b), +// .c => |c| normal(c), +// .d => |d| normal(d), +// .e => |e| normal(e), +// ... +// } +// +// With: +// +// switch (thing) { +// .a => |a| special(a), +// inline else => |t| normal(t), +// } +// +// We can have special handling of some cases and then Zig +// handles the rest of the matches for us. +// +// With this feature, you decide to make an Insect union with a +// single uniform 'print()' function. All of the insects can +// then be responsible for printing themselves. And Doctor +// Zoraptera can calm down and stop gnawing on the furniture. +// +const std = @import("std"); + +const Ant = struct { + still_alive: bool, + + pub fn print(self: Ant) void { + std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); + } +}; + +const Bee = struct { + flowers_visited: u16, + + pub fn print(self: Bee) void { + std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); + } +}; + +// Here's the new grasshopper. Notice how we've also added print +// methods to each insect. +const Grasshopper = struct { + distance_hopped: u16, + + pub fn print(self: Grasshopper) void { + std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); + } +}; + +const Insect = union(enum) { + ant: Ant, + bee: Bee, + grasshopper: Grasshopper, + + // Thanks to 'inline else', we can think of this print() as + // being an interface method. Any member of this union with + // a print() method can be treated uniformly by outside + // code without needing to know any other details. Cool! + pub fn print(self: Insect) void { + switch (self) { + inline else => |case| return case.print(), + } + } +}; + +pub fn main() !void { + const my_insects = [_]Insect{ + Insect{ .ant = Ant{ .still_alive = true } }, + Insect{ .bee = Bee{ .flowers_visited = 17 } }, + Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, + }; + + std.debug.print("Daily Insect Report:\n", .{}); + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. + ??? + } +} + +// Our print() method in the Insect union above demonstrates +// something very similar to the object-oriented concept of an +// abstract data type. That is, the Insect type doesn't contain +// the underlying data, and the print() function doesn't +// actually do the printing. +// +// The point of an interface is to support generic programming: +// the ability to treat different things as if they were the +// same to cut down on clutter and conceptual complexity. +// +// The Daily Insect Report doesn't need to worry about *which* +// insects are in the report - they all print the same way via +// the interface! +// +// Doctor Zoraptera loves it. diff --git a/exercises/085_async.zig b/exercises/085_async.zig new file mode 100644 index 0000000..48bda2b --- /dev/null +++ b/exercises/085_async.zig @@ -0,0 +1,48 @@ +// +// In previous versions of Zig, async/await used special keywords +// like 'suspend', 'resume', and 'async' that operated on stackframes +// directly. Those keywords no longer exist! +// +// Zig 0.16 replaced them with a unified I/O interface: std.Io. +// This interface uses a VTable pattern - a struct of function pointers - +// to abstract over different concurrency backends: +// +// * Threaded - classic thread-pool based I/O +// * Uring - Linux io_uring +// * Kqueue - BSD/macOS +// * Dispatch - macOS Grand Central Dispatch +// +// The Io struct itself is tiny: +// +// const Io = struct { +// userdata: ?*anyopaque, // opaque state of the backend +// vtable: *const VTable, // table of function pointers +// }; +// +// Your code receives an Io value and calls methods on it. +// The backend is chosen at initialization time - your code doesn't +// need to know which one it is! +// +// In Zig 0.16, main() receives a std.process.Init struct to opt +// into I/O and concurrency support: +// +// pub fn main(init: std.process.Init) !void { +// const io = init.io; +// // ... use io ... +// } +// +// Let's start simple. Fix the main function to extract the Io +// interface from init, then use it to get the current time. +// +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.???; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). + const timestamp = std.Io.Timestamp.now(io, .real); + + // Print the timestamp in seconds since the Unix epoch. + std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); +} diff --git a/exercises/085_async2.zig b/exercises/085_async2.zig deleted file mode 100644 index 1f1c4c8..0000000 --- a/exercises/085_async2.zig +++ /dev/null @@ -1,48 +0,0 @@ -// -// Now that we know how to get an Io value, let's use it for -// asynchronous execution! -// -// io.async() launches a function and returns a Future. The result -// won't necessarily be available until you call .await() on it: -// -// var future = io.async(someFunction, .{ arg1, arg2 }); -// // ... do other work here ... -// const result = future.await(io); -// -// The function *may* run immediately or on another thread - -// your code doesn't need to care! That's the beauty of the -// Io abstraction. (In the Threaded backend, if no thread is -// available, the function runs synchronously right away and -// .await() just returns the already-computed result.) -// -// io.async() returns a Future(T) where T is the return type -// of the function you passed in. Future has two key methods: -// -// .await(io) - block until the result is ready, return it -// .cancel(io) - request cancellation, then return the result -// -// Fix this program so that computeAnswer runs asynchronously -// and its result is properly awaited. -// -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch computeAnswer asynchronously. - // io.async() takes a function and a tuple of its arguments. - var future = io.async(computeAnswer, .{ 6, 7 }); - - // Meanwhile, print something to show we're not blocked. - std.debug.print("Computing... ", .{}); - - // Now collect the result. What method on Future gives us - // the value, blocking if it isn't ready yet? - const answer = future.???(io); - - std.debug.print("The answer is: {}\n", .{answer}); -} - -fn computeAnswer(a: u32, b: u32) u32 { - return a * b; -} diff --git a/exercises/086_async2.zig b/exercises/086_async2.zig new file mode 100644 index 0000000..1f1c4c8 --- /dev/null +++ b/exercises/086_async2.zig @@ -0,0 +1,48 @@ +// +// Now that we know how to get an Io value, let's use it for +// asynchronous execution! +// +// io.async() launches a function and returns a Future. The result +// won't necessarily be available until you call .await() on it: +// +// var future = io.async(someFunction, .{ arg1, arg2 }); +// // ... do other work here ... +// const result = future.await(io); +// +// The function *may* run immediately or on another thread - +// your code doesn't need to care! That's the beauty of the +// Io abstraction. (In the Threaded backend, if no thread is +// available, the function runs synchronously right away and +// .await() just returns the already-computed result.) +// +// io.async() returns a Future(T) where T is the return type +// of the function you passed in. Future has two key methods: +// +// .await(io) - block until the result is ready, return it +// .cancel(io) - request cancellation, then return the result +// +// Fix this program so that computeAnswer runs asynchronously +// and its result is properly awaited. +// +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch computeAnswer asynchronously. + // io.async() takes a function and a tuple of its arguments. + var future = io.async(computeAnswer, .{ 6, 7 }); + + // Meanwhile, print something to show we're not blocked. + std.debug.print("Computing... ", .{}); + + // Now collect the result. What method on Future gives us + // the value, blocking if it isn't ready yet? + const answer = future.???(io); + + std.debug.print("The answer is: {}\n", .{answer}); +} + +fn computeAnswer(a: u32, b: u32) u32 { + return a * b; +} diff --git a/exercises/086_async3.zig b/exercises/086_async3.zig deleted file mode 100644 index 07221e9..0000000 --- a/exercises/086_async3.zig +++ /dev/null @@ -1,50 +0,0 @@ -// -// The real power of async shows when you launch MULTIPLE tasks! -// -// With io.async(), you can start several operations, then await -// them all. The Io backend may run them concurrently: -// -// var f1 = io.async(taskA, .{}); -// var f2 = io.async(taskB, .{}); -// -// // Both tasks may be running now! -// const a = f1.await(io); -// const b = f2.await(io); -// -// There's also io.concurrent() which provides a STRONGER guarantee: -// it ensures the function gets its own unit of concurrency (e.g. a -// real OS thread). But it can fail with error.ConcurrencyUnavailable -// if resources are exhausted. -// -// io.async() is more portable: if no thread is available, it simply -// runs the function synchronously. This makes it the right default -// for most code. -// -// Fix this program to launch both tasks and collect their results. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch both tasks asynchronously. - var future_a = io.async(slowAdd, .{ 10, 20 }); - var future_b = ???(slowMul, .{ 6, 7 }); - - // Await both results. - const sum = future_a.await(io); - const product = future_b.???(io); - - print("{} + {} = {}\n", .{ 1, 2, sum }); - print("{} * {} = {}\n", .{ 6, 7, product }); - print("Total: {}\n", .{sum + product}); -} - -fn slowAdd(a: u32, b: u32) u32 { - return a + b; -} - -fn slowMul(a: u32, b: u32) u32 { - return a * b; -} diff --git a/exercises/087_async3.zig b/exercises/087_async3.zig new file mode 100644 index 0000000..07221e9 --- /dev/null +++ b/exercises/087_async3.zig @@ -0,0 +1,50 @@ +// +// The real power of async shows when you launch MULTIPLE tasks! +// +// With io.async(), you can start several operations, then await +// them all. The Io backend may run them concurrently: +// +// var f1 = io.async(taskA, .{}); +// var f2 = io.async(taskB, .{}); +// +// // Both tasks may be running now! +// const a = f1.await(io); +// const b = f2.await(io); +// +// There's also io.concurrent() which provides a STRONGER guarantee: +// it ensures the function gets its own unit of concurrency (e.g. a +// real OS thread). But it can fail with error.ConcurrencyUnavailable +// if resources are exhausted. +// +// io.async() is more portable: if no thread is available, it simply +// runs the function synchronously. This makes it the right default +// for most code. +// +// Fix this program to launch both tasks and collect their results. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch both tasks asynchronously. + var future_a = io.async(slowAdd, .{ 10, 20 }); + var future_b = ???(slowMul, .{ 6, 7 }); + + // Await both results. + const sum = future_a.await(io); + const product = future_b.???(io); + + print("{} + {} = {}\n", .{ 1, 2, sum }); + print("{} * {} = {}\n", .{ 6, 7, product }); + print("Total: {}\n", .{sum + product}); +} + +fn slowAdd(a: u32, b: u32) u32 { + return a + b; +} + +fn slowMul(a: u32, b: u32) u32 { + return a * b; +} diff --git a/exercises/087_async4.zig b/exercises/087_async4.zig deleted file mode 100644 index 50829fc..0000000 --- a/exercises/087_async4.zig +++ /dev/null @@ -1,50 +0,0 @@ -// -// When you have many tasks that don't return individual values, -// use a Group! A Group is an unordered set of tasks that can -// only be awaited or canceled as a whole: -// -// var group: std.Io.Group = .init; -// group.async(io, myTask, .{arg1}); -// group.async(io, myTask, .{arg2}); -// try group.await(io); // blocks until ALL tasks finish -// -// Important rules: -// * The return type of functions spawned in a group must be -// coercible to Cancelable!void (i.e. void, or error{Canceled}!void). -// * Once you call group.async(), you MUST eventually call -// group.await() or group.cancel() to release resources. -// * group.cancel() requests cancellation on ALL members, -// then waits for them to finish. -// -// Unlike Future, Group tasks don't return values to the caller. -// They're ideal for parallel work that communicates through -// shared state or side effects (like printing). -// -// Fix this program to await all tasks in the group. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var group: std.Io.Group = .init; - - // Spawn 3 tasks in any order. Each sleeps for (id * 1) seconds - // before printing, so the output order is deterministic. - group.async(io, doWork, .{ io, 1 }); - group.async(io, doWork, .{ io, 3 }); - group.async(io, doWork, .{ io, 2 }); - - // Wait for all tasks to finish. - // What Group method blocks until all tasks complete? - try group.??? - - print("All tasks finished!\n", .{}); -} - -fn doWork(io: std.Io, id: u32) void { - // Sleep ensures deterministic output order. - io.sleep(std.Io.Duration.fromSeconds(id), .awake) catch return; - print("Task {} done.\n", .{id}); -} diff --git a/exercises/088_async4.zig b/exercises/088_async4.zig new file mode 100644 index 0000000..50829fc --- /dev/null +++ b/exercises/088_async4.zig @@ -0,0 +1,50 @@ +// +// When you have many tasks that don't return individual values, +// use a Group! A Group is an unordered set of tasks that can +// only be awaited or canceled as a whole: +// +// var group: std.Io.Group = .init; +// group.async(io, myTask, .{arg1}); +// group.async(io, myTask, .{arg2}); +// try group.await(io); // blocks until ALL tasks finish +// +// Important rules: +// * The return type of functions spawned in a group must be +// coercible to Cancelable!void (i.e. void, or error{Canceled}!void). +// * Once you call group.async(), you MUST eventually call +// group.await() or group.cancel() to release resources. +// * group.cancel() requests cancellation on ALL members, +// then waits for them to finish. +// +// Unlike Future, Group tasks don't return values to the caller. +// They're ideal for parallel work that communicates through +// shared state or side effects (like printing). +// +// Fix this program to await all tasks in the group. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var group: std.Io.Group = .init; + + // Spawn 3 tasks in any order. Each sleeps for (id * 1) seconds + // before printing, so the output order is deterministic. + group.async(io, doWork, .{ io, 1 }); + group.async(io, doWork, .{ io, 3 }); + group.async(io, doWork, .{ io, 2 }); + + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? + try group.??? + + print("All tasks finished!\n", .{}); +} + +fn doWork(io: std.Io, id: u32) void { + // Sleep ensures deterministic output order. + io.sleep(std.Io.Duration.fromSeconds(id), .awake) catch return; + print("Task {} done.\n", .{id}); +} diff --git a/exercises/088_async5.zig b/exercises/088_async5.zig deleted file mode 100644 index 4fb8d76..0000000 --- a/exercises/088_async5.zig +++ /dev/null @@ -1,61 +0,0 @@ -// -// One of the most important features of the new Io system is -// structured cancellation! -// -// Every Future has a .cancel() method that: -// 1. Requests the task to stop (via error.Canceled at the -// next "cancellation point") -// 2. Waits for the task to actually finish -// 3. Returns whatever result the task produced -// -// A "cancellation point" is any Io function that can return -// error.Canceled - most commonly io.sleep(): -// -// fn myTask(io: std.Io) u32 { -// io.sleep(...) catch |err| switch (err) { -// error.Canceled => return 0, // handle gracefully -// }; -// return 42; -// } -// -// This is fundamentally different from killing a thread - -// the task gets a chance to clean up and return a value! -// -// Fix this program: the slow task would take 10 seconds, -// but we cancel it after 1 second. The task should detect -// the cancellation and return early. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var future = io.async(slowTask, .{io}); - - // Wait 1 second, then cancel instead of waiting the full 10. - io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; - - print("Canceling slow task...\n", .{}); - - // We don't want to wait 10 seconds! - // Which Future method requests cancellation AND returns the result? - const result = ???; - - print("Task returned: {}\n", .{result}); -} - -fn slowTask(io: std.Io) u32 { - print("Starting long computation...\n", .{}); - - // Try to sleep for 10 seconds - but we might get canceled! - io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch |err| switch (err) { - error.Canceled => { - print("Task was canceled, cleaning up.\n", .{}); - return 0; - }, - }; - - print("Task completed normally.\n", .{}); - return 42; -} diff --git a/exercises/089_async5.zig b/exercises/089_async5.zig new file mode 100644 index 0000000..4fb8d76 --- /dev/null +++ b/exercises/089_async5.zig @@ -0,0 +1,61 @@ +// +// One of the most important features of the new Io system is +// structured cancellation! +// +// Every Future has a .cancel() method that: +// 1. Requests the task to stop (via error.Canceled at the +// next "cancellation point") +// 2. Waits for the task to actually finish +// 3. Returns whatever result the task produced +// +// A "cancellation point" is any Io function that can return +// error.Canceled - most commonly io.sleep(): +// +// fn myTask(io: std.Io) u32 { +// io.sleep(...) catch |err| switch (err) { +// error.Canceled => return 0, // handle gracefully +// }; +// return 42; +// } +// +// This is fundamentally different from killing a thread - +// the task gets a chance to clean up and return a value! +// +// Fix this program: the slow task would take 10 seconds, +// but we cancel it after 1 second. The task should detect +// the cancellation and return early. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var future = io.async(slowTask, .{io}); + + // Wait 1 second, then cancel instead of waiting the full 10. + io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; + + print("Canceling slow task...\n", .{}); + + // We don't want to wait 10 seconds! + // Which Future method requests cancellation AND returns the result? + const result = ???; + + print("Task returned: {}\n", .{result}); +} + +fn slowTask(io: std.Io) u32 { + print("Starting long computation...\n", .{}); + + // Try to sleep for 10 seconds - but we might get canceled! + io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch |err| switch (err) { + error.Canceled => { + print("Task was canceled, cleaning up.\n", .{}); + return 0; + }, + }; + + print("Task completed normally.\n", .{}); + return 42; +} diff --git a/exercises/089_async6.zig b/exercises/089_async6.zig deleted file mode 100644 index eab03c9..0000000 --- a/exercises/089_async6.zig +++ /dev/null @@ -1,71 +0,0 @@ -// -// Sometimes you want to race multiple tasks and act on whichever -// finishes first. That's what Select is for! -// -// Select is like a Group, but lets you receive individual results -// as tasks complete — one at a time: -// -// const Race = std.Io.Select(union(enum) { -// fast: u32, -// slow: u32, -// }); -// -// var buffer: [2]Race.Union = undefined; -// var sel = Race.init(io, &buffer); -// -// sel.async(.fast, fastFn, .{io}); -// sel.async(.slow, slowFn, .{io}); -// -// const winner = try sel.await(); // returns first completed -// switch (winner) { -// .fast => |val| ..., -// .slow => |val| ..., -// } -// sel.cancelDiscard(); // cancel remaining, discard results -// -// The buffer must be large enough for all tasks that might -// complete before you call cancelDiscard(). -// -// Fix this program to receive the winner of the race. -// -const std = @import("std"); -const print = std.debug.print; - -const RaceResult = std.Io.Select(union(enum) { - hare: []const u8, - tortoise: []const u8, -}); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var buffer: [2]RaceResult.Union = undefined; - var sel = RaceResult.init(io, &buffer); - - sel.async(.hare, runHare, .{io}); - sel.async(.tortoise, runTortoise, .{io}); - - // Wait for the first finisher. - // What Select method returns the first completed result? - const winner = ???; - - switch (winner) { - .hare => |msg| print("Hare: {s}\n", .{msg}), - .tortoise => |msg| print("Tortoise: {s}\n", .{msg}), - } - - // Clean up the loser — we don't need their result. - sel.cancelDiscard(); -} - -fn runHare(io: std.Io) []const u8 { - // The hare is fast — only 1 second! - io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch return "I got canceled!"; - return "I'm fast!"; -} - -fn runTortoise(io: std.Io) []const u8 { - // The tortoise is slow — 10 seconds. - io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch return "I got canceled!"; - return "Slow and steady..."; -} diff --git a/exercises/090_async6.zig b/exercises/090_async6.zig new file mode 100644 index 0000000..eab03c9 --- /dev/null +++ b/exercises/090_async6.zig @@ -0,0 +1,71 @@ +// +// Sometimes you want to race multiple tasks and act on whichever +// finishes first. That's what Select is for! +// +// Select is like a Group, but lets you receive individual results +// as tasks complete — one at a time: +// +// const Race = std.Io.Select(union(enum) { +// fast: u32, +// slow: u32, +// }); +// +// var buffer: [2]Race.Union = undefined; +// var sel = Race.init(io, &buffer); +// +// sel.async(.fast, fastFn, .{io}); +// sel.async(.slow, slowFn, .{io}); +// +// const winner = try sel.await(); // returns first completed +// switch (winner) { +// .fast => |val| ..., +// .slow => |val| ..., +// } +// sel.cancelDiscard(); // cancel remaining, discard results +// +// The buffer must be large enough for all tasks that might +// complete before you call cancelDiscard(). +// +// Fix this program to receive the winner of the race. +// +const std = @import("std"); +const print = std.debug.print; + +const RaceResult = std.Io.Select(union(enum) { + hare: []const u8, + tortoise: []const u8, +}); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var buffer: [2]RaceResult.Union = undefined; + var sel = RaceResult.init(io, &buffer); + + sel.async(.hare, runHare, .{io}); + sel.async(.tortoise, runTortoise, .{io}); + + // Wait for the first finisher. + // What Select method returns the first completed result? + const winner = ???; + + switch (winner) { + .hare => |msg| print("Hare: {s}\n", .{msg}), + .tortoise => |msg| print("Tortoise: {s}\n", .{msg}), + } + + // Clean up the loser — we don't need their result. + sel.cancelDiscard(); +} + +fn runHare(io: std.Io) []const u8 { + // The hare is fast — only 1 second! + io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch return "I got canceled!"; + return "I'm fast!"; +} + +fn runTortoise(io: std.Io) []const u8 { + // The tortoise is slow — 10 seconds. + io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch return "I got canceled!"; + return "Slow and steady..."; +} diff --git a/exercises/090_async7.zig b/exercises/090_async7.zig deleted file mode 100644 index bfe6ffd..0000000 --- a/exercises/090_async7.zig +++ /dev/null @@ -1,57 +0,0 @@ -// -// When multiple async tasks access shared data, you need -// synchronization! Io provides a Mutex for this: -// -// var mutex: std.Io.Mutex = .init; -// -// // In a task: -// try mutex.lock(io); // blocks until lock is acquired -// defer mutex.unlock(); -// // ... critical section: safe to modify shared data ... -// -// Without the mutex, concurrent tasks could read and write the -// same memory simultaneously, causing a data race — the result -// would be unpredictable. -// -// mutex.lock() is a cancellation point — it can return -// error.Canceled. There's also tryLock() which returns -// immediately (true if acquired, false if not). -// -// Fix this program so the counter is correctly synchronized. -// Without the fix, the final count would be unpredictable. -// With it, four tasks incrementing 100 times each = 400. -// -const std = @import("std"); -const print = std.debug.print; - -const SharedState = struct { - counter: u32 = 0, - mutex: std.Io.Mutex = .init, -}; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - var state = SharedState{}; - - var group: std.Io.Group = .init; - - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - - try group.await(io); - - print("Counter: {}\n", .{state.counter}); -} - -fn increment(io: std.Io, state: *SharedState, times: u32) void { - for (0..times) |_| { - // Acquire the lock before modifying shared state. - // What Mutex method blocks until the lock is acquired? - state.mutex.??? catch return; - defer state.mutex.unlock(); // <-- what's missing here? - - state.counter += 1; - } -} diff --git a/exercises/091_async7.zig b/exercises/091_async7.zig new file mode 100644 index 0000000..bfe6ffd --- /dev/null +++ b/exercises/091_async7.zig @@ -0,0 +1,57 @@ +// +// When multiple async tasks access shared data, you need +// synchronization! Io provides a Mutex for this: +// +// var mutex: std.Io.Mutex = .init; +// +// // In a task: +// try mutex.lock(io); // blocks until lock is acquired +// defer mutex.unlock(); +// // ... critical section: safe to modify shared data ... +// +// Without the mutex, concurrent tasks could read and write the +// same memory simultaneously, causing a data race — the result +// would be unpredictable. +// +// mutex.lock() is a cancellation point — it can return +// error.Canceled. There's also tryLock() which returns +// immediately (true if acquired, false if not). +// +// Fix this program so the counter is correctly synchronized. +// Without the fix, the final count would be unpredictable. +// With it, four tasks incrementing 100 times each = 400. +// +const std = @import("std"); +const print = std.debug.print; + +const SharedState = struct { + counter: u32 = 0, + mutex: std.Io.Mutex = .init, +}; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + var state = SharedState{}; + + var group: std.Io.Group = .init; + + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + + try group.await(io); + + print("Counter: {}\n", .{state.counter}); +} + +fn increment(io: std.Io, state: *SharedState, times: u32) void { + for (0..times) |_| { + // Acquire the lock before modifying shared state. + // What Mutex method blocks until the lock is acquired? + state.mutex.??? catch return; + defer state.mutex.unlock(); // <-- what's missing here? + + state.counter += 1; + } +} diff --git a/exercises/091_async8.zig b/exercises/091_async8.zig deleted file mode 100644 index 10921c3..0000000 --- a/exercises/091_async8.zig +++ /dev/null @@ -1,62 +0,0 @@ -// -// Tasks often need to communicate! Io provides Queue for this — -// a bounded, thread-safe channel for passing data between tasks: -// -// var backing: [16]u32 = undefined; -// var queue: std.Io.Queue(u32) = .init(&backing); -// -// // Producer task: -// try queue.putOne(io, value); // blocks if queue is full -// -// // Consumer task: -// const val = try queue.getOne(io); // blocks if queue is empty -// -// When the producer is done, it calls queue.close(io) to signal -// that no more data is coming. After that, getOne() will return -// error.Closed once the queue is drained. -// -// This is the classic producer/consumer pattern — one task -// generates work, another processes it, and the queue handles -// all the synchronization automatically. -// -// Fix this program: the producer sends numbers 1..10, the -// consumer sums them up. The expected sum is 55. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var backing: [4]u32 = undefined; - var queue: std.Io.Queue(u32) = .init(&backing); - - var group: std.Io.Group = .init; - - group.async(io, producer, .{ io, &queue }); - group.async(io, consumer, .{ io, &queue }); - - try group.await(io); -} - -fn producer(io: std.Io, queue: *std.Io.Queue(u32)) void { - // Send numbers 1 through 10 into the queue. - for (1..11) |i| { - // What Queue method sends a single element, blocking if full? - queue.???(io, @intCast(i)) catch return; - } - // Signal that we're done sending. - queue.close(io); -} - -fn consumer(io: std.Io, queue: *std.Io.Queue(u32)) void { - var sum: u32 = 0; - while (true) { - const value = queue.getOne(io) catch |err| switch (err) { - error.Closed => break, - error.Canceled => return, - }; - sum += value; - } - print("Sum of 1..10 = {}\n", .{sum}); -} diff --git a/exercises/092_async8.zig b/exercises/092_async8.zig new file mode 100644 index 0000000..10921c3 --- /dev/null +++ b/exercises/092_async8.zig @@ -0,0 +1,62 @@ +// +// Tasks often need to communicate! Io provides Queue for this — +// a bounded, thread-safe channel for passing data between tasks: +// +// var backing: [16]u32 = undefined; +// var queue: std.Io.Queue(u32) = .init(&backing); +// +// // Producer task: +// try queue.putOne(io, value); // blocks if queue is full +// +// // Consumer task: +// const val = try queue.getOne(io); // blocks if queue is empty +// +// When the producer is done, it calls queue.close(io) to signal +// that no more data is coming. After that, getOne() will return +// error.Closed once the queue is drained. +// +// This is the classic producer/consumer pattern — one task +// generates work, another processes it, and the queue handles +// all the synchronization automatically. +// +// Fix this program: the producer sends numbers 1..10, the +// consumer sums them up. The expected sum is 55. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var backing: [4]u32 = undefined; + var queue: std.Io.Queue(u32) = .init(&backing); + + var group: std.Io.Group = .init; + + group.async(io, producer, .{ io, &queue }); + group.async(io, consumer, .{ io, &queue }); + + try group.await(io); +} + +fn producer(io: std.Io, queue: *std.Io.Queue(u32)) void { + // Send numbers 1 through 10 into the queue. + for (1..11) |i| { + // What Queue method sends a single element, blocking if full? + queue.???(io, @intCast(i)) catch return; + } + // Signal that we're done sending. + queue.close(io); +} + +fn consumer(io: std.Io, queue: *std.Io.Queue(u32)) void { + var sum: u32 = 0; + while (true) { + const value = queue.getOne(io) catch |err| switch (err) { + error.Closed => break, + error.Canceled => return, + }; + sum += value; + } + print("Sum of 1..10 = {}\n", .{sum}); +} diff --git a/exercises/092_async9.zig b/exercises/092_async9.zig deleted file mode 100644 index ad30dcf..0000000 --- a/exercises/092_async9.zig +++ /dev/null @@ -1,57 +0,0 @@ -// -// We've been using io.async() to launch tasks. But there's a -// stronger variant: io.concurrent(). -// -// The difference: -// -// io.async(): -// * The function MAY run on another thread, or it may run -// immediately on the current thread (synchronously). -// * Never fails — if no thread is available, it just runs -// the function right away. -// * More portable, works with all Io backends. -// -// io.concurrent(): -// * GUARANTEES a separate unit of concurrency (a real thread -// in the Threaded backend). -// * Can fail with error.ConcurrencyUnavailable if resources -// are exhausted or the backend doesn't support it. -// * Use when you NEED true parallelism. -// -// Because concurrent() can fail, you must handle the error: -// -// var future = try io.concurrent(myFn, .{args}); -// const result = future.await(io); -// -// Notice the 'try' — that's the key difference in usage! -// -// Fix this program to launch the computation concurrently. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch with a guaranteed separate thread. - // Which Io method guarantees true concurrency? - // (Hint: unlike io.async, this one can fail!) - var future = try io.???(compute, .{io}); - - print("Main thread continues...\n", .{}); - - // Wait 100 millisecond so the output order is deterministic. - io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {}; - - print("Main thread done waiting.\n", .{}); - - const result = future.await(io); - print("Result: {}\n", .{result}); -} - -fn compute(io: std.Io) u32 { - print("Computing on a separate thread!\n", .{}); - // Simulate some work. - io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0; - return 123; -} diff --git a/exercises/093_async10.zig b/exercises/093_async10.zig deleted file mode 100644 index 6ed229d..0000000 --- a/exercises/093_async10.zig +++ /dev/null @@ -1,67 +0,0 @@ -// -// In exercise 088, we learned that cancellation happens at -// "cancellation points" — any Io function that can return -// error.Canceled. -// -// But sometimes a task has a critical section that MUST NOT -// be interrupted — for example, writing a consistent state -// to disk, or completing a transaction. -// -// Io provides CancelProtection for this: -// -// const old = io.swapCancelProtection(.blocked); -// defer _ = io.swapCancelProtection(old); -// -// // In this block, NO Io function will return error.Canceled. -// // The cancel request is held until protection is restored. -// -// There are two states: -// .unblocked — normal: cancellation points can fire (default) -// .blocked — protected: error.Canceled is never returned -// -// There's also io.checkCancel() — a pure cancellation point -// that does nothing except return error.Canceled if a cancel -// request is pending. Useful in long CPU-bound loops. -// -// And io.recancel() — re-arms a consumed cancel request so -// the NEXT cancellation point will fire again. -// -// Fix this program so the critical section completes even -// when the task is canceled. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var future = io.async(importantTask, .{io}); - - // Give the task time to start and enter its critical section. - io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {}; - - // Cancel while the task is in its protected section. - const result = future.cancel(io); - print("Task result: {s}\n", .{result}); -} - -fn importantTask(io: std.Io) []const u8 { - print("Starting critical section...\n", .{}); - - // Protect this section from cancellation. - // What method swaps the cancel protection state? - const old = io.???(. blocked); - defer _ = io.???(old); - - // This sleep will NOT return error.Canceled even though - // we get canceled during it — protection is active! - io.sleep(std.Io.Duration.fromMilliseconds(600), .awake) catch |err| switch (err) { - error.Canceled => { - // This should never happen while protected! - return "ERROR: canceled during critical section!"; - }, - }; - - print("Critical section completed safely.\n", .{}); - return "All data saved."; -} diff --git a/exercises/093_async9.zig b/exercises/093_async9.zig new file mode 100644 index 0000000..ad30dcf --- /dev/null +++ b/exercises/093_async9.zig @@ -0,0 +1,57 @@ +// +// We've been using io.async() to launch tasks. But there's a +// stronger variant: io.concurrent(). +// +// The difference: +// +// io.async(): +// * The function MAY run on another thread, or it may run +// immediately on the current thread (synchronously). +// * Never fails — if no thread is available, it just runs +// the function right away. +// * More portable, works with all Io backends. +// +// io.concurrent(): +// * GUARANTEES a separate unit of concurrency (a real thread +// in the Threaded backend). +// * Can fail with error.ConcurrencyUnavailable if resources +// are exhausted or the backend doesn't support it. +// * Use when you NEED true parallelism. +// +// Because concurrent() can fail, you must handle the error: +// +// var future = try io.concurrent(myFn, .{args}); +// const result = future.await(io); +// +// Notice the 'try' — that's the key difference in usage! +// +// Fix this program to launch the computation concurrently. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch with a guaranteed separate thread. + // Which Io method guarantees true concurrency? + // (Hint: unlike io.async, this one can fail!) + var future = try io.???(compute, .{io}); + + print("Main thread continues...\n", .{}); + + // Wait 100 millisecond so the output order is deterministic. + io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {}; + + print("Main thread done waiting.\n", .{}); + + const result = future.await(io); + print("Result: {}\n", .{result}); +} + +fn compute(io: std.Io) u32 { + print("Computing on a separate thread!\n", .{}); + // Simulate some work. + io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0; + return 123; +} diff --git a/exercises/094_async10.zig b/exercises/094_async10.zig new file mode 100644 index 0000000..6ed229d --- /dev/null +++ b/exercises/094_async10.zig @@ -0,0 +1,67 @@ +// +// In exercise 088, we learned that cancellation happens at +// "cancellation points" — any Io function that can return +// error.Canceled. +// +// But sometimes a task has a critical section that MUST NOT +// be interrupted — for example, writing a consistent state +// to disk, or completing a transaction. +// +// Io provides CancelProtection for this: +// +// const old = io.swapCancelProtection(.blocked); +// defer _ = io.swapCancelProtection(old); +// +// // In this block, NO Io function will return error.Canceled. +// // The cancel request is held until protection is restored. +// +// There are two states: +// .unblocked — normal: cancellation points can fire (default) +// .blocked — protected: error.Canceled is never returned +// +// There's also io.checkCancel() — a pure cancellation point +// that does nothing except return error.Canceled if a cancel +// request is pending. Useful in long CPU-bound loops. +// +// And io.recancel() — re-arms a consumed cancel request so +// the NEXT cancellation point will fire again. +// +// Fix this program so the critical section completes even +// when the task is canceled. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var future = io.async(importantTask, .{io}); + + // Give the task time to start and enter its critical section. + io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {}; + + // Cancel while the task is in its protected section. + const result = future.cancel(io); + print("Task result: {s}\n", .{result}); +} + +fn importantTask(io: std.Io) []const u8 { + print("Starting critical section...\n", .{}); + + // Protect this section from cancellation. + // What method swaps the cancel protection state? + const old = io.???(. blocked); + defer _ = io.???(old); + + // This sleep will NOT return error.Canceled even though + // we get canceled during it — protection is active! + io.sleep(std.Io.Duration.fromMilliseconds(600), .awake) catch |err| switch (err) { + error.Canceled => { + // This should never happen while protected! + return "ERROR: canceled during critical section!"; + }, + }; + + print("Critical section completed safely.\n", .{}); + return "All data saved."; +} diff --git a/exercises/095_interfaces.zig b/exercises/095_interfaces.zig deleted file mode 100644 index 7775dd5..0000000 --- a/exercises/095_interfaces.zig +++ /dev/null @@ -1,127 +0,0 @@ -// -// Remember our ant and bee simulator constructed with unions -// back in exercises 55 and 56? There, we demonstrated that -// unions allow us to treat different data types in a uniform -// manner. -// -// One neat feature was using tagged unions to create a single -// function to print a status for ants *or* bees by switching: -// -// switch (insect) { -// .still_alive => ... // (print ant stuff) -// .flowers_visited => ... // (print bee stuff) -// } -// -// Well, that simulation was running just fine until a new insect -// arrived in the virtual garden, a grasshopper! -// -// Doctor Zoraptera started to add grasshopper code to the -// program, but then she backed away from her keyboard with an -// angry hissing sound. She had realized that having code for -// each insect in one place and code to print each insect in -// another place was going to become unpleasant to maintain when -// the simulation expanded to hundreds of different insects. -// -// Thankfully, Zig has another comptime feature we can use -// to get out of this dilemma called the 'inline else'. -// -// We can replace this redundant code: -// -// switch (thing) { -// .a => |a| special(a), -// .b => |b| normal(b), -// .c => |c| normal(c), -// .d => |d| normal(d), -// .e => |e| normal(e), -// ... -// } -// -// With: -// -// switch (thing) { -// .a => |a| special(a), -// inline else => |t| normal(t), -// } -// -// We can have special handling of some cases and then Zig -// handles the rest of the matches for us. -// -// With this feature, you decide to make an Insect union with a -// single uniform 'print()' function. All of the insects can -// then be responsible for printing themselves. And Doctor -// Zoraptera can calm down and stop gnawing on the furniture. -// -const std = @import("std"); - -const Ant = struct { - still_alive: bool, - - pub fn print(self: Ant) void { - std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); - } -}; - -const Bee = struct { - flowers_visited: u16, - - pub fn print(self: Bee) void { - std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); - } -}; - -// Here's the new grasshopper. Notice how we've also added print -// methods to each insect. -const Grasshopper = struct { - distance_hopped: u16, - - pub fn print(self: Grasshopper) void { - std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); - } -}; - -const Insect = union(enum) { - ant: Ant, - bee: Bee, - grasshopper: Grasshopper, - - // Thanks to 'inline else', we can think of this print() as - // being an interface method. Any member of this union with - // a print() method can be treated uniformly by outside - // code without needing to know any other details. Cool! - pub fn print(self: Insect) void { - switch (self) { - inline else => |case| return case.print(), - } - } -}; - -pub fn main() !void { - const my_insects = [_]Insect{ - Insect{ .ant = Ant{ .still_alive = true } }, - Insect{ .bee = Bee{ .flowers_visited = 17 } }, - Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, - }; - - std.debug.print("Daily Insect Report:\n", .{}); - for (my_insects) |insect| { - // Almost done! We want to print() each insect with a - // single method call here. - ??? - } -} - -// Our print() method in the Insect union above demonstrates -// something very similar to the object-oriented concept of an -// abstract data type. That is, the Insect type doesn't contain -// the underlying data, and the print() function doesn't -// actually do the printing. -// -// The point of an interface is to support generic programming: -// the ability to treat different things as if they were the -// same to cut down on clutter and conceptual complexity. -// -// The Daily Insect Report doesn't need to worry about *which* -// insects are in the report - they all print the same way via -// the interface! -// -// Doctor Zoraptera loves it. diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig new file mode 100644 index 0000000..fb78e7b --- /dev/null +++ b/exercises/095_quiz_async.zig @@ -0,0 +1,186 @@ +// +// Quiz Time — Async I/O! +// +// Doctor Zoraptera's insect simulation is going well, but she +// realized that her virtual garden needs weather data! Insects +// behave differently depending on temperature, humidity, and +// wind conditions. +// +// She has set up three weather sensors around the garden that +// measure conditions in parallel and report their readings +// through a shared data channel. A collector task gathers the +// readings, and after all sensors have reported, a garden +// report is printed. +// +// But Doctor Z rushed through the code (she was being chased +// by a grasshopper) and left several bugs. Can you fix them? +// +// Here's what the program should do: +// 1. Three sensor tasks run concurrently, each sending +// exactly 3 readings through a Queue +// 2. A collector task receives readings, protected by a Mutex +// 3. After all sensors finish, the queue is closed +// 4. The final report is written in a cancel-protected section +// +// ************************************************************* +// * A NOTE ABOUT THIS EXERCISE * +// * * +// * This quiz uses concepts from exercises 084-093. * +// * There are 6 bugs to fix — look for the ???s! * +// * * +// ************************************************************* +// +const std = @import("std"); +const print = std.debug.print; + +const SensorType = enum { thermometer, hygrometer, anemometer }; + +const Reading = struct { + sensor_type: SensorType, + value: i32, +}; + +const GardenWeather = struct { + temperature: i32 = 0, + humidity: i32 = 0, + wind: i32 = 0, + readings_count: u32 = 0, + mutex: std.Io.Mutex = .init, + + fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { + // Bug 1: The collector needs to lock before modifying + // shared state. What Mutex method acquires the lock? + self.mutex.lock(io) catch return; + self.mutex.???(io) catch return; + + switch (reading.sensor_type) { + .thermometer => self.temperature = reading.value, + .hygrometer => self.humidity = reading.value, + .anemometer => self.wind = reading.value, + } + self.readings_count += 1; + } +}; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var weather = GardenWeather{}; + + var reading_buf: [8]Reading = undefined; + var queue: std.Io.Queue(Reading) = .init(&reading_buf); + + // Sensor group: runs all three sensors to completion. + var sensors: std.Io.Group = .init; + + // Start three sensor tasks. They need GUARANTEED concurrency + // since they each simulate real-time measurement. + // + // Bug 2: io.async doesn't guarantee a separate thread. + // Which Io method guarantees true concurrency? + // (Don't forget: it can fail, so you need 'try'!) + try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); + try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); + try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); + + // Collector group: processes readings from the queue. + var collectors: std.Io.Group = .init; + collectors.async(io, collector, .{ io, &queue, &weather }); + + // Bug 3: Wait for ALL sensors to finish sending their readings. + // What Group method blocks until all tasks complete? + try sensors.await(io); + // try sensors.???(io); + + // All sensors done — close the queue so the collector knows + // there's no more data coming. + queue.close(io); + + // Wait for the collector to drain the queue. + try collectors.await(io); + + // Now write the garden report. This is critical — it must + // NOT be interrupted, even if something tries to cancel us! + // + // Bug 4: Protect this section from cancellation. + // What Io method swaps the cancel protection state? + const old_protection = io.???(.blocked); + defer _ = io.???(old_protection); + + printGardenReport(&weather); +} + +fn sensor( + io: std.Io, + queue: *std.Io.Queue(Reading), + sensor_type: SensorType, + base_value: i32, +) void { + // Each sensor takes exactly 3 measurements. + for (1..4) |i| { + io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch return; + + const reading = Reading{ + .sensor_type = sensor_type, + .value = base_value + @as(i32, @intCast(i)), + }; + + // Bug 5: Send the reading into the queue. + // What Queue method sends a single element? + queue.???(io, reading) catch return; + } +} + +fn collector( + io: std.Io, + queue: *std.Io.Queue(Reading), + weather: *GardenWeather, +) void { + while (true) { + const reading = queue.getOne(io) catch |err| switch (err) { + error.Closed => break, + error.Canceled => return, + }; + weather.addReading(io, reading); + } +} + +fn printGardenReport(weather: *GardenWeather) void { + print("=== Doctor Zoraptera's Garden Report ===\n", .{}); + print("Temperature : {}C\n", .{weather.temperature}); + print("Humidity : {}%\n", .{weather.humidity}); + print("Wind : {} km/h\n", .{weather.wind}); + print("Readings : {}\n", .{weather.readings_count}); + + if (weather.temperature > 20 and weather.wind < 15) { + print("Bee-friendly conditions! Expect high pollination.\n", .{}); + } else { + print("Grasshoppers will be grumpy today.\n", .{}); + } +} + +// Further reading for the curious: +// +// This quiz covered the main async I/O primitives: +// io.async() - launch a task (may run inline) +// io.concurrent() - launch with guaranteed parallelism +// Group.concurrent() - concurrent tasks in a group +// Future.await/cancel - collect or cancel a single task +// Group.async/await/cancel - manage fire-and-forget tasks +// Select.async/await - race tasks, act on first completion +// Queue - bounded channel between tasks +// Mutex - protect shared state +// CancelProtection - shield critical sections +// +// There are more synchronization primitives we didn't cover: +// Condition - wait for a condition to become true +// RwLock - multiple readers OR one writer +// Semaphore - limit concurrent access to a resource +// Futex - low-level wait/wake on a memory address +// Batch - submit multiple I/O operations at once +// +// The key insight: all of these work through the Io VTable, +// so your code is portable across backends (Threaded, Uring, +// Kqueue, Dispatch) without any changes! +// +// Doctor Zoraptera approves. diff --git a/patches/patches/084_interfaces.patch b/patches/patches/084_interfaces.patch new file mode 100644 index 0000000..a1d0628 --- /dev/null +++ b/patches/patches/084_interfaces.patch @@ -0,0 +1,11 @@ +--- exercises/084_interfaces.zig 2025-08-15 15:17:57.839348063 +0200 ++++ answers/084_interfaces.zig 2026-04-03 14:27:32.670756488 +0200 +@@ -106,7 +106,7 @@ + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. +- ??? ++ insect.print(); + } + } + diff --git a/patches/patches/085_async.patch b/patches/patches/085_async.patch new file mode 100644 index 0000000..ca8b102 --- /dev/null +++ b/patches/patches/085_async.patch @@ -0,0 +1,11 @@ +--- exercises/085_async.zig 2026-04-01 20:40:08.904999609 +0200 ++++ answers/085_async.zig 2026-04-01 20:40:05.641933231 +0200 +@@ -37,7 +37,7 @@ + const std = @import("std"); + + pub fn main(init: std.process.Init) !void { +- const io = init.???; ++ const io = init.io; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). diff --git a/patches/patches/086_async2.patch b/patches/patches/086_async2.patch new file mode 100644 index 0000000..7506a69 --- /dev/null +++ b/patches/patches/086_async2.patch @@ -0,0 +1,14 @@ +--- exercises/086_async2.zig 2026-04-01 19:22:50.017227542 +0200 ++++ answers/086_async2.zig 2026-04-01 19:21:57.569158481 +0200 +@@ -38,9 +38,9 @@ + + // Now collect the result. What method on Future gives us + // the value, blocking if it isn't ready yet? +- const answer = future.???(io); ++ const answer = future.await(io); + +- std.debug.print("The answer is: {}\n", .{answer}); ++ std.debug.print("the answer is: {}\n", .{answer}); + } + + fn computeAnswer(a: u32, b: u32) u32 { diff --git a/patches/patches/087_async3.patch b/patches/patches/087_async3.patch new file mode 100644 index 0000000..8365e7a --- /dev/null +++ b/patches/patches/087_async3.patch @@ -0,0 +1,18 @@ +--- exercises/087_async3.zig 2026-04-01 22:51:05.540094851 +0200 ++++ answers/087_async3.zig 2026-04-01 22:50:44.579669189 +0200 +@@ -29,12 +29,12 @@ + const io = init.io; + + // Launch both tasks asynchronously. +- var future_a = io.async(slowAdd, .{ 10, 20 }); +- var future_b = ???(slowMul, .{ 6, 7 }); ++ var future_a = io.async(slowAdd, .{ 1, 2 }); ++ var future_b = io.async(slowMul, .{ 6, 7 }); + + // Await both results. + const sum = future_a.await(io); +- const product = future_b.???(io); ++ const product = future_b.await(io); + + print("{} + {} = {}\n", .{ 1, 2, sum }); + print("{} * {} = {}\n", .{ 6, 7, product }); diff --git a/patches/patches/088_async4.patch b/patches/patches/088_async4.patch new file mode 100644 index 0000000..1faf30e --- /dev/null +++ b/patches/patches/088_async4.patch @@ -0,0 +1,11 @@ +--- exercises/088_async4.zig 2026-04-01 23:17:31.066443941 +0200 ++++ answers/088_async4.zig 2026-04-01 23:17:39.251612131 +0200 +@@ -38,7 +38,7 @@ + + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? +- try group.??? ++ try group.await(io); + + print("All tasks finished!\n", .{}); + } diff --git a/patches/patches/089_async5.patch b/patches/patches/089_async5.patch new file mode 100644 index 0000000..d2baa96 --- /dev/null +++ b/patches/patches/089_async5.patch @@ -0,0 +1,11 @@ +--- exercises/089_async5.zig 2026-04-01 23:40:40.505855238 +0200 ++++ answers/089_async5.zig 2026-04-01 23:40:10.176236971 +0200 +@@ -40,7 +40,7 @@ + + // We don't want to wait 10 seconds! + // Which Future method requests cancellation AND returns the result? +- const result = ???; ++ const result = future.cancel(io); + + print("Task returned: {}\n", .{result}); + } diff --git a/patches/patches/090_async6.patch b/patches/patches/090_async6.patch new file mode 100644 index 0000000..5ac777b --- /dev/null +++ b/patches/patches/090_async6.patch @@ -0,0 +1,11 @@ +--- exercises/090_async6.zig 2026-04-02 10:25:34.016616118 +0200 ++++ answers/090_async6.zig 2026-04-02 10:27:48.827144051 +0200 +@@ -47,7 +47,7 @@ + + // Wait for the first finisher. + // What Select method returns the first completed result? +- const winner = ???; ++ const winner = try sel.await(); + + switch (winner) { + .hare => |msg| print("Hare: {s}\n", .{msg}), diff --git a/patches/patches/091_async7.patch b/patches/patches/091_async7.patch new file mode 100644 index 0000000..b4bab9b --- /dev/null +++ b/patches/patches/091_async7.patch @@ -0,0 +1,13 @@ +--- exercises/091_async7.zig 2026-04-02 10:50:08.142508099 +0200 ++++ answers/091_async7.zig 2026-04-02 10:49:59.629341593 +0200 +@@ -49,8 +49,8 @@ + for (0..times) |_| { + // Acquire the lock before modifying shared state. + // What Mutex method blocks until the lock is acquired? +- state.mutex.??? catch return; +- defer state.mutex.unlock(); // <-- what's missing here? ++ state.mutex.lock(io) catch return; ++ defer state.mutex.unlock(io); + + state.counter += 1; + } diff --git a/patches/patches/092_async8.patch b/patches/patches/092_async8.patch new file mode 100644 index 0000000..0ec9116 --- /dev/null +++ b/patches/patches/092_async8.patch @@ -0,0 +1,11 @@ +--- exercises/092_async8.zig 2026-04-02 10:49:27.925721496 +0200 ++++ answers/092_async8.zig 2026-04-02 10:49:31.694795212 +0200 +@@ -43,7 +43,7 @@ + // Send numbers 1 through 10 into the queue. + for (1..11) |i| { + // What Queue method sends a single element, blocking if full? +- queue.???(io, @intCast(i)) catch return; ++ queue.putOne(io, @intCast(i)) catch return; + } + // Signal that we're done sending. + queue.close(io); diff --git a/patches/patches/093_async9.patch b/patches/patches/093_async9.patch new file mode 100644 index 0000000..f759921 --- /dev/null +++ b/patches/patches/093_async9.patch @@ -0,0 +1,11 @@ +--- exercises/093_async9.zig 2026-04-03 13:44:50.526780809 +0200 ++++ answers/093_async9.zig 2026-04-03 13:44:54.957870294 +0200 +@@ -36,7 +36,7 @@ + // Launch with a guaranteed separate thread. + // Which Io method guarantees true concurrency? + // (Hint: unlike io.async, this one can fail!) +- var future = try io.???(compute, .{io}); ++ var future = try io.concurrent(compute, .{io}); + + print("Main thread continues...\n", .{}); + diff --git a/patches/patches/094_async10.patch b/patches/patches/094_async10.patch new file mode 100644 index 0000000..ae0d26d --- /dev/null +++ b/patches/patches/094_async10.patch @@ -0,0 +1,13 @@ +--- exercises/094_async10.zig 2026-04-03 14:25:16.600025924 +0200 ++++ answers/094_async10.zig 2026-04-03 14:24:56.192615893 +0200 +@@ -50,8 +50,8 @@ + + // Protect this section from cancellation. + // What method swaps the cancel protection state? +- const old = io.???(. blocked); +- defer _ = io.???(old); ++ const old = io.swapCancelProtection(.blocked); ++ defer _ = io.swapCancelProtection(old); + + // This sleep will NOT return error.Canceled even though + // we get canceled during it — protection is active! diff --git a/patches/patches/095_quiz_async.patch b/patches/patches/095_quiz_async.patch new file mode 100644 index 0000000..dbaae07 --- /dev/null +++ b/patches/patches/095_quiz_async.patch @@ -0,0 +1,52 @@ +--- exercises/095_quiz_async.zig 2026-04-03 18:04:53.577391455 +0200 ++++ answers/095_quiz_async.zig 2026-04-03 18:05:42.570392172 +0200 +@@ -51,7 +51,7 @@ + // Bug 1: The collector needs to lock before modifying + // shared state. What Mutex method acquires the lock? + self.mutex.lock(io) catch return; +- self.mutex.???(io) catch return; ++ defer self.mutex.unlock(io); + + switch (reading.sensor_type) { + .thermometer => self.temperature = reading.value, +@@ -79,9 +79,9 @@ + // Bug 2: io.async doesn't guarantee a separate thread. + // Which Io method guarantees true concurrency? + // (Don't forget: it can fail, so you need 'try'!) +- try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); +- try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); +- try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .thermometer, 20 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .hygrometer, 60 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .anemometer, 10 }); + + // Collector group: processes readings from the queue. + var collectors: std.Io.Group = .init; +@@ -90,7 +90,6 @@ + // Bug 3: Wait for ALL sensors to finish sending their readings. + // What Group method blocks until all tasks complete? + try sensors.await(io); +- // try sensors.???(io); + + // All sensors done — close the queue so the collector knows + // there's no more data coming. +@@ -104,8 +103,8 @@ + // + // Bug 4: Protect this section from cancellation. + // What Io method swaps the cancel protection state? +- const old_protection = io.???(.blocked); +- defer _ = io.???(old_protection); ++ const old_protection = io.swapCancelProtection(.blocked); ++ defer _ = io.swapCancelProtection(old_protection); + + printGardenReport(&weather); + } +@@ -127,7 +126,7 @@ + + // Bug 5: Send the reading into the queue. + // What Queue method sends a single element? +- queue.???(io, reading) catch return; ++ queue.putOne(io, reading) catch return; + } + } + -- cgit v1.2.3