From 77d3b684cb467ad4e06c211518e2d2d1c7346ad1 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Wed, 1 Apr 2026 22:28:37 +0200
Subject: [PATCH 01/25] revival of the async-io functions
---
build.zig | 71 ++++++++++++++++++++++-----------
exercises/084_async.zig | 86 ++++++++++++++++++----------------------
exercises/085_async2.zig | 56 +++++++++++++++++---------
3 files changed, 123 insertions(+), 90 deletions(-)
diff --git a/build.zig b/build.zig
index 968a6b0..fa0dcac 100644
--- a/build.zig
+++ b/build.zig
@@ -75,6 +75,8 @@ pub const Exercise = struct {
/// Hint to the user, why this has been skipped
skip_hint: ?[]const u8 = null,
+ timestamp: bool = false,
+
/// Returns the name of the main file with .zig stripped.
pub fn name(self: Exercise) []const u8 {
return std.fs.path.stem(self.main_file);
@@ -435,11 +437,30 @@ const ZiglingStep = struct {
result.stderr;
// Validate the output.
+ var exercise_output = self.exercise.output;
+
+ // Insert timestamp for exercise 84
+ if (self.exercise.timestamp) {
+ var ts_buf: [20]u8 = undefined;
+ const timestamp = std.fmt.bufPrint(&ts_buf, "{}", .{std.Io.Timestamp.now(io, .real).toSeconds()}) catch unreachable;
+
+ var buf: [100]u8 = undefined;
+ const prefix_len = 14;
+ const placeholder_len = 11;
+ @memcpy(buf[0..prefix_len], exercise_output[0..prefix_len]);
+ @memcpy(buf[prefix_len..][0..timestamp.len], timestamp);
+ const suffix = exercise_output[prefix_len + placeholder_len ..];
+ const suffix_dest_start = prefix_len + timestamp.len;
+ @memcpy(buf[suffix_dest_start..][0..suffix.len], suffix);
+
+ const total_len = prefix_len + timestamp.len + suffix.len;
+ exercise_output = buf[0..total_len];
+ }
+
// NOTE: exercise.output can never contain a CR character.
// See https://ziglang.org/documentation/master/#Source-Encoding.
const output = trimLines(b.allocator, raw_output) catch @panic("OOM");
- const exercise_output = self.exercise.output;
- if (!std.mem.eql(u8, output, self.exercise.output)) {
+ if (!std.mem.eql(u8, output, exercise_output)) {
const red = red_bold_text;
const reset = reset_text;
@@ -698,7 +719,7 @@ const exercises = [_]Exercise{
\\most part, you'll be taking directions from the Zig
\\compiler itself.)
\\
- ,
+ , // pay attention to the comma
},
.{
.main_file = "002_std.zig",
@@ -730,7 +751,7 @@ const exercises = [_]Exercise{
\\Ziggy played guitar
\\Jamming good with Andrew Kelley
\\And the Spiders from Mars
- ,
+ , // pay attention to the comma
.hint = "Please fix the lyrics!",
},
.{
@@ -867,7 +888,7 @@ const exercises = [_]Exercise{
\\ Green
\\ Blue
\\
- ,
+ , // pay attention to the comma
.hint = "I'm feeling blue about this.",
},
.{
@@ -879,7 +900,7 @@ const exercises = [_]Exercise{
.output =
\\Character 1 - G:20 H:100 XP:10
\\Character 2 - G:10 H:100 XP:20
- ,
+ , // pay attention to the comma
},
.{
.main_file = "039_pointers.zig",
@@ -903,7 +924,7 @@ const exercises = [_]Exercise{
.output =
\\Wizard (G:10 H:100 XP:20)
\\ Mentor: Wizard (G:10000 H:100 XP:2340)
- ,
+ , // pay attention to the comma
},
.{
.main_file = "044_quiz5.zig",
@@ -947,7 +968,7 @@ const exercises = [_]Exercise{
.output =
\\Hand1: A 4 K 8
\\Hand2: 5 2 Q J
- ,
+ , // pay attention to the comma
},
.{
.main_file = "053_slices2.zig",
@@ -1043,7 +1064,10 @@ const exercises = [_]Exercise{
.main_file = "073_comptime8.zig",
.output = "My llama value is 25.",
},
- .{ .main_file = "074_comptime9.zig", .output = "My llama value is 2.", .skip = false, .skip_hint = "This is actually correct as it is. :-)" },
+ .{
+ .main_file = "074_comptime9.zig",
+ .output = "My llama value is 2.",
+ },
.{
.main_file = "075_quiz8.zig",
.output = "Archer's Point--2->Bridge--1->Dogwood Grove--3->Cottage--2->East Pond--1->Fox Pond",
@@ -1078,7 +1102,7 @@ const exercises = [_]Exercise{
.main_file = "082_anonymous_structs3.zig",
.output =
\\"0"(bool):true "1"(bool):false "2"(i32):42 "3"(f32):3.141592
- ,
+ , // pay attention to the comma
.hint = "This one is a challenge! But you have everything you need.",
},
.{
@@ -1090,16 +1114,15 @@ const exercises = [_]Exercise{
// direct link: https://github.com/ziglang/zig/issues/6025
.{
.main_file = "084_async.zig",
- .output = "foo() A",
- .hint = "Read the facts. Use the facts.",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output = "Current time: s since epoch",
+ .timestamp = true,
+ // .hint = "Read the facts. Use the facts.",
+ // .skip = true,
+ // .skip_hint = "async has not been implemented in the current compiler version.",
},
.{
.main_file = "085_async2.zig",
- .output = "Hello async!",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output = "Computing... the answer is: 42",
},
.{
.main_file = "086_async3.zig",
@@ -1145,7 +1168,7 @@ const exercises = [_]Exercise{
\\Ant is alive.
\\Bee visited 17 flowers.
\\Grasshopper hopped 32 meters.
- ,
+ , // pay attention to the comma
},
.{
.main_file = "093_hello_c.zig",
@@ -1221,7 +1244,7 @@ const exercises = [_]Exercise{
\\2. Bard (Gold: 11, XP: 17)
\\3. Bard (Gold: 5, XP: 55)
\\4. Warrior (Gold: 7392, XP: 21)
- ,
+ , // pay attention to the comma
},
.{
.main_file = "102_testing.zig",
@@ -1247,7 +1270,7 @@ const exercises = [_]Exercise{
\\and
\\despair
\\This little poem has 15 words!
- ,
+ , // pay attention to the comma
},
.{
.main_file = "104_threading.zig",
@@ -1261,7 +1284,7 @@ const exercises = [_]Exercise{
\\thread 1: finished.
\\thread 3: finished.
\\Zig is cool!
- ,
+ , // pay attention to the comma
},
.{
.main_file = "105_threading2.zig",
@@ -1276,7 +1299,7 @@ const exercises = [_]Exercise{
.output =
\\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
\\Successfully Read 18 bytes: It's zigling time!
- ,
+ , // pay attention to the comma
},
.{
.main_file = "108_labeled_switch.zig",
@@ -1287,7 +1310,7 @@ const exercises = [_]Exercise{
.output =
\\Max difference (old fn): 0.014
\\Max difference (new fn): 0.014
- ,
+ , // pay attention to the comma
},
.{ .main_file = "110_quiz9.zig", .output =
\\Toggle pins with XOR on PORTB
@@ -1334,6 +1357,6 @@ const exercises = [_]Exercise{
\\
\\This is the end for now!
\\We hope you had fun and were able to learn a lot, so visit us again when the next exercises are available.
- ,
+ , // pay attention to the comma
},
};
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
+// 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 ...
// }
//
-// 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.
+// Let's start simple. Fix the main function to extract the Io
+// interface from init, then use it to get the current time.
//
-// fn fooThatSuspends() void {
-// suspend {} // return control, but leave the frame alone
-// }
-//
-// 4. To call any function in async context and get a reference
-// to its frame for later use, use the 'async' keyword:
-//
-// var foo_frame = async fooThatSuspends();
-//
-// 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.
-//
-// fn bar() void {
-// fooThatSuspends();
-// }
-//
-// 6. The main() function cannot be async!
-//
-// 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.???;
-fn foo() void {
- print("foo() A\n", .{});
- suspend {}
- print("foo() B\n", .{});
+ // 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
index 036aefa..1f1c4c8 100644
--- a/exercises/085_async2.zig
+++ b/exercises/085_async2.zig
@@ -1,28 +1,48 @@
//
-// So, 'suspend' returns control to the place from which it was
-// called (the "call site"). How do we give control back to the
-// suspended function?
+// Now that we know how to get an Io value, let's use it for
+// asynchronous execution!
//
-// For that, we have a new keyword called 'resume' which takes an
-// async function invocation's frame and returns control to it.
+// io.async() launches a function and returns a Future. The result
+// won't necessarily be available until you call .await() on it:
//
-// fn fooThatSuspends() void {
-// suspend {}
-// }
+// var future = io.async(someFunction, .{ arg1, arg2 });
+// // ... do other work here ...
+// const result = future.await(io);
//
-// var foo_frame = async fooThatSuspends();
-// resume foo_frame;
+// 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.)
//
-// See if you can make this program print "Hello async!".
+// io.async() returns a Future(T) where T is the return type
+// of the function you passed in. Future has two key methods:
//
-const print = @import("std").debug.print;
+// .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() void {
- var foo_frame = async foo();
+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 foo() void {
- print("Hello ", .{});
- suspend {}
- print("async!\n", .{});
+fn computeAnswer(a: u32, b: u32) u32 {
+ return a * b;
}
From 6d89dcd2dec3654b754012f0411d36975e1dc7b2 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Wed, 1 Apr 2026 22:31:48 +0200
Subject: [PATCH 02/25] revival of the async-io functions
---
patches/patches/084_async.patch | 11 +++++++++++
patches/patches/085_async2.patch | 14 ++++++++++++++
2 files changed, 25 insertions(+)
create mode 100644 patches/patches/084_async.patch
create mode 100644 patches/patches/085_async2.patch
diff --git a/patches/patches/084_async.patch b/patches/patches/084_async.patch
new file mode 100644
index 0000000..2c97f0e
--- /dev/null
+++ b/patches/patches/084_async.patch
@@ -0,0 +1,11 @@
+--- exercises/084_async.zig 2026-04-01 20:40:08.904999609 +0200
++++ answers/084_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/085_async2.patch b/patches/patches/085_async2.patch
new file mode 100644
index 0000000..7583d7c
--- /dev/null
+++ b/patches/patches/085_async2.patch
@@ -0,0 +1,14 @@
+--- exercises/085_async2.zig 2026-04-01 19:22:50.017227542 +0200
++++ answers/085_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 {
From db1fef8b864f1af6f9d2b8a7871e72d77747304d Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Wed, 1 Apr 2026 22:52:04 +0200
Subject: [PATCH 03/25] revival of the async-io functions
---
build.zig | 11 +++---
exercises/086_async3.zig | 61 +++++++++++++++++++++-----------
patches/patches/086_async3.patch | 18 ++++++++++
3 files changed, 64 insertions(+), 26 deletions(-)
create mode 100644 patches/patches/086_async3.patch
diff --git a/build.zig b/build.zig
index fa0dcac..d2ee3aa 100644
--- a/build.zig
+++ b/build.zig
@@ -1116,9 +1116,6 @@ const exercises = [_]Exercise{
.main_file = "084_async.zig",
.output = "Current time: s since epoch",
.timestamp = true,
- // .hint = "Read the facts. Use the facts.",
- // .skip = true,
- // .skip_hint = "async has not been implemented in the current compiler version.",
},
.{
.main_file = "085_async2.zig",
@@ -1126,9 +1123,11 @@ const exercises = [_]Exercise{
},
.{
.main_file = "086_async3.zig",
- .output = "5 4 3 2 1",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output =
+ \\1 + 2 = 3
+ \\6 * 7 = 42
+ \\Total: 45
+ , // pay attention to the comma
},
.{
.main_file = "087_async4.zig",
diff --git a/exercises/086_async3.zig b/exercises/086_async3.zig
index c8f1113..07221e9 100644
--- a/exercises/086_async3.zig
+++ b/exercises/086_async3.zig
@@ -1,29 +1,50 @@
//
-// Because they can suspend and resume, async Zig functions are
-// an example of a more general programming concept called
-// "coroutines". One of the neat things about Zig async functions
-// is that they retain their state as they are suspended and
-// resumed.
+// The real power of async shows when you launch MULTIPLE tasks!
//
-// See if you can make this program print "5 4 3 2 1".
+// With io.async(), you can start several operations, then await
+// them all. The Io backend may run them concurrently:
//
-const print = @import("std").debug.print;
+// 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() void {
- const n = 5;
- var foo_frame = async foo(n);
+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 });
- print("\n", .{});
+ // 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 foo(countdown: u32) void {
- var current = countdown;
-
- while (current > 0) {
- print("{} ", .{current});
- current -= 1;
- suspend {}
- }
+fn slowAdd(a: u32, b: u32) u32 {
+ return a + b;
+}
+
+fn slowMul(a: u32, b: u32) u32 {
+ return a * b;
}
diff --git a/patches/patches/086_async3.patch b/patches/patches/086_async3.patch
new file mode 100644
index 0000000..3a3c2c4
--- /dev/null
+++ b/patches/patches/086_async3.patch
@@ -0,0 +1,18 @@
+--- exercises/086_async3.zig 2026-04-01 22:51:05.540094851 +0200
++++ answers/086_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 });
From fcfb0e80a039d8283f6a298ed67a26c23e3a560a Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Wed, 1 Apr 2026 23:34:16 +0200
Subject: [PATCH 04/25] revival of the async-io functions
---
build.zig | 9 +++--
exercises/087_async4.zig | 58 +++++++++++++++++++----------
exercises/104_threading.zig | 53 +++++++++++---------------
patches/patches/087_async4.patch | 11 ++++++
patches/patches/104_threading.patch | 6 +--
5 files changed, 81 insertions(+), 56 deletions(-)
create mode 100644 patches/patches/087_async4.patch
diff --git a/build.zig b/build.zig
index d2ee3aa..1d40dec 100644
--- a/build.zig
+++ b/build.zig
@@ -1131,9 +1131,12 @@ const exercises = [_]Exercise{
},
.{
.main_file = "087_async4.zig",
- .output = "1 2 3 4 5",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output =
+ \\Task 1 done.
+ \\Task 2 done.
+ \\Task 3 done.
+ \\All tasks finished!
+ , // pay attention to the comma
},
.{
.main_file = "088_async5.zig",
diff --git a/exercises/087_async4.zig b/exercises/087_async4.zig
index bb9c9ec..50829fc 100644
--- a/exercises/087_async4.zig
+++ b/exercises/087_async4.zig
@@ -1,30 +1,50 @@
//
-// It has probably not escaped your attention that we are no
-// longer capturing a return value from foo() because the 'async'
-// keyword returns the frame instead.
+// 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:
//
-// One way to solve this is to use a global variable.
+// 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
//
-// See if you can make this program print "1 2 3 4 5".
+// 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.
//
-const print = @import("std").debug.print;
+// 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;
-var global_counter: i32 = 0;
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
-pub fn main() void {
- var foo_frame = async foo();
+ var group: std.Io.Group = .init;
- while (global_counter <= 5) {
- print("{} ", .{global_counter});
- ???
- }
+ // 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 });
- print("\n", .{});
+ // Wait for all tasks to finish.
+ // What Group method blocks until all tasks complete?
+ try group.???
+
+ print("All tasks finished!\n", .{});
}
-fn foo() void {
- while (true) {
- ???
- ???
- }
+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/104_threading.zig b/exercises/104_threading.zig
index 2b0e6f7..3c3fa21 100644
--- a/exercises/104_threading.zig
+++ b/exercises/104_threading.zig
@@ -1,31 +1,22 @@
//
-// Whenever there is a lot to calculate, the question arises as to how
-// tasks can be carried out simultaneously. We have already learned about
-// one possibility, namely asynchronous processes, in Exercises 84-91.
+// In Exercises 84-91, we learned about Zig's Io interface for
+// concurrent execution: io.async(), Group, Select, and Futures.
+// Under the hood, the Threaded backend manages a pool of real
+// OS threads for you - including scheduling, cancellation, and
+// resource cleanup.
//
-// However, the computing power of the processor is only distributed to
-// the started and running tasks, which always reaches its limits when
-// pure computing power is called up.
+// But sometimes you need direct control over threads:
+// * Long-lived dedicated workers
+// * Specific stack sizes or thread counts
+// * Code that doesn't have an Io interface available
+// * Fine-grained synchronization patterns
//
-// For example, in blockchains based on proof of work, the miners have
-// to find a nonce for a certain character string so that the first m bits
-// in the hash of the character string and the nonce are zeros.
-// As the miner who can solve the task first receives the reward, everyone
-// tries to complete the calculations as quickly as possible.
+// That's where std.Thread comes in. It gives you a raw OS thread
+// that you spawn, manage, and join yourself. No pool, no Futures,
+// no automatic cancellation - but full control.
//
-// This is where multithreading comes into play, where tasks are actually
-// distributed across several cores of the CPU or GPU, which then really
-// means a multiplication of performance.
-//
-// The following diagram roughly illustrates the difference between the
-// various types of process execution.
-// The 'Overall Time' column is intended to illustrate how the time is
-// affected if, instead of one core as in synchronous and asynchronous
-// processing, a second core now helps to complete the work in multithreading.
-//
-// In the ideal case shown, execution takes only half the time compared
-// to the synchronous single thread. And even asynchronous processing
-// is only slightly faster in comparison.
+// The following diagram roughly illustrates the difference between
+// the various types of process execution:
//
//
// Synchronous Asynchronous
@@ -108,7 +99,7 @@ pub fn main() !void {
// they run in parallel and we can still do some work in between.
var io_instance: std.Io.Threaded = .init_single_threaded;
const io = io_instance.io();
- try io.sleep(std.Io.Duration.fromSeconds(4), .awake);
+ try io.sleep(std.Io.Duration.fromMilliseconds(400), .awake);
std.debug.print("Some weird stuff, after starting the threads.\n", .{});
}
// After we have left the closed area, we wait until
@@ -118,17 +109,17 @@ pub fn main() !void {
// This function is started with every thread that we set up.
// In our example, we pass the number of the thread as a parameter.
-fn thread_function(num: usize) !void {
+fn thread_function(id: usize) !void {
var io_instance: std.Io.Threaded = .init_single_threaded;
const io = io_instance.io();
- try io.sleep(std.Io.Duration.fromSeconds(1 * @as(isize, @intCast(num))), .awake);
- std.debug.print("thread {d}: {s}\n", .{ num, "started." });
+ try io.sleep(std.Io.Duration.fromMilliseconds(100 * @as(isize, @intCast(id))), .awake);
+ std.debug.print("thread {d}: {s}\n", .{ id, "started." });
// This timer simulates the work of the thread.
- const work_time = 3 * ((5 - num % 3) - 2);
- try io.sleep(std.Io.Duration.fromSeconds(@intCast(work_time)), .awake);
+ const work_time = 300 * ((5 - id % 3) - 2);
+ try io.sleep(std.Io.Duration.fromMilliseconds(@intCast(work_time)), .awake);
- std.debug.print("thread {d}: {s}\n", .{ num, "finished." });
+ std.debug.print("thread {d}: {s}\n", .{ id, "finished." });
}
// This is the easiest way to run threads in parallel.
// In general, however, more management effort is required,
diff --git a/patches/patches/087_async4.patch b/patches/patches/087_async4.patch
new file mode 100644
index 0000000..4883afd
--- /dev/null
+++ b/patches/patches/087_async4.patch
@@ -0,0 +1,11 @@
+--- exercises/087_async4.zig 2026-04-01 23:17:31.066443941 +0200
++++ answers/087_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/104_threading.patch b/patches/patches/104_threading.patch
index eda25fd..e6fe0f4 100644
--- a/patches/patches/104_threading.patch
+++ b/patches/patches/104_threading.patch
@@ -1,6 +1,6 @@
---- exercises/104_threading.zig 2025-11-28 14:17:31.552529679 +0100
-+++ answers/104_threading.zig 2025-11-28 14:15:36.823931851 +0100
-@@ -97,12 +97,12 @@
+--- exercises/104_threading.zig 2026-04-01 23:31:10.073198955 +0200
++++ answers/104_threading.zig 2026-04-01 23:29:51.314585919 +0200
+@@ -88,12 +88,12 @@
defer handle.join();
// Second thread
From e22748d48899d93c6af2684b33688c91c239596c Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Wed, 1 Apr 2026 23:44:24 +0200
Subject: [PATCH 05/25] revival of the async-io functions
---
README.md | 2 +-
build.zig | 9 ++--
exercises/088_async5.zig | 81 ++++++++++++++++++--------------
patches/patches/088_async5.patch | 11 +++++
4 files changed, 65 insertions(+), 38 deletions(-)
create mode 100644 patches/patches/088_async5.patch
diff --git a/README.md b/README.md
index 8d90e03..35eadd4 100644
--- a/README.md
+++ b/README.md
@@ -193,7 +193,7 @@ Zig Core Language
* [x] Sentinel termination
* [x] Quoted identifiers @""
* [x] Anonymous structs/tuples/lists
-* [ ] Async I/O
+* [x] Async I/O
* [X] Interfaces
* [X] Bit manipulation
* [X] Working with C
diff --git a/build.zig b/build.zig
index 1d40dec..d0f4b52 100644
--- a/build.zig
+++ b/build.zig
@@ -1140,9 +1140,12 @@ const exercises = [_]Exercise{
},
.{
.main_file = "088_async5.zig",
- .output = "Example Title.",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output =
+ \\Starting long computation...
+ \\Canceling slow task...
+ \\Task was canceled, cleaning up.
+ \\Task returned: 0
+ , // pay attention to the comma
},
.{
.main_file = "089_async6.zig",
diff --git a/exercises/088_async5.zig b/exercises/088_async5.zig
index 759a920..4fb8d76 100644
--- a/exercises/088_async5.zig
+++ b/exercises/088_async5.zig
@@ -1,48 +1,61 @@
//
-// Sure, we can solve our async value problem with a global
-// variable. But this hardly seems like an ideal solution.
+// One of the most important features of the new Io system is
+// structured cancellation!
//
-// So how do we REALLY get return values from async functions?
+// 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
//
-// The 'await' keyword waits for an async function to complete
-// and then captures its return value.
+// A "cancellation point" is any Io function that can return
+// error.Canceled - most commonly io.sleep():
//
-// fn foo() u32 {
-// return 5;
+// fn myTask(io: std.Io) u32 {
+// io.sleep(...) catch |err| switch (err) {
+// error.Canceled => return 0, // handle gracefully
+// };
+// return 42;
// }
//
-// var foo_frame = async foo(); // invoke and get frame
-// var value = await foo_frame; // await result using frame
+// This is fundamentally different from killing a thread -
+// the task gets a chance to clean up and return a value!
//
-// The above example is just a silly way to call foo() and get 5
-// back. But if foo() did something more interesting such as wait
-// for a network response to get that 5, our code would pause
-// until the value was ready.
+// 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.
//
-// As you can see, async/await basically splits a function call
-// into two parts:
-//
-// 1. Invoke the function ('async')
-// 2. Getting the return value ('await')
-//
-// Also notice that a 'suspend' keyword does NOT need to exist in
-// a function to be called in an async context.
-//
-// Please use 'await' to get the string returned by
-// getPageTitle().
-//
-const print = @import("std").debug.print;
+const std = @import("std");
+const print = std.debug.print;
-pub fn main() void {
- var myframe = async getPageTitle("http://example.com");
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
- var value = ???
+ var future = io.async(slowTask, .{io});
- print("{s}\n", .{value});
+ // 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 getPageTitle(url: []const u8) []const u8 {
- // Please PRETEND this is actually making a network request.
- _ = url;
- return "Example Title.";
+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/patches/patches/088_async5.patch b/patches/patches/088_async5.patch
new file mode 100644
index 0000000..d88e820
--- /dev/null
+++ b/patches/patches/088_async5.patch
@@ -0,0 +1,11 @@
+--- exercises/088_async5.zig 2026-04-01 23:40:40.505855238 +0200
++++ answers/088_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});
+ }
From 3b22bfd898a5ca93df9e07a3f1816f3b0b08137b Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Thu, 2 Apr 2026 10:28:40 +0200
Subject: [PATCH 06/25] revival of the async-io functions
---
exercises/089_async6.zig | 95 +++++++++++++++++++-------------
patches/patches/089_async6.patch | 11 ++++
2 files changed, 67 insertions(+), 39 deletions(-)
create mode 100644 patches/patches/089_async6.patch
diff --git a/exercises/089_async6.zig b/exercises/089_async6.zig
index 8bf50e9..eab03c9 100644
--- a/exercises/089_async6.zig
+++ b/exercises/089_async6.zig
@@ -1,54 +1,71 @@
//
-// The power and purpose of async/await becomes more apparent
-// when we do multiple things concurrently. Foo and Bar do not
-// depend on each other and can happen at the same time, but End
-// requires that they both be finished.
+// Sometimes you want to race multiple tasks and act on whichever
+// finishes first. That's what Select is for!
//
-// +---------+
-// | Start |
-// +---------+
-// / \
-// / \
-// +---------+ +---------+
-// | Foo | | Bar |
-// +---------+ +---------+
-// \ /
-// \ /
-// +---------+
-// | End |
-// +---------+
+// Select is like a Group, but lets you receive individual results
+// as tasks complete — one at a time:
//
-// We can express this in Zig like so:
+// const Race = std.Io.Select(union(enum) {
+// fast: u32,
+// slow: u32,
+// });
//
-// fn foo() u32 { ... }
-// fn bar() u32 { ... }
+// var buffer: [2]Race.Union = undefined;
+// var sel = Race.init(io, &buffer);
//
-// // Start
+// sel.async(.fast, fastFn, .{io});
+// sel.async(.slow, slowFn, .{io});
//
-// var foo_frame = async foo();
-// var bar_frame = async bar();
+// const winner = try sel.await(); // returns first completed
+// switch (winner) {
+// .fast => |val| ...,
+// .slow => |val| ...,
+// }
+// sel.cancelDiscard(); // cancel remaining, discard results
//
-// var foo_value = await foo_frame;
-// var bar_value = await bar_frame;
+// The buffer must be large enough for all tasks that might
+// complete before you call cancelDiscard().
//
-// // End
+// Fix this program to receive the winner of the race.
//
-// Please await TWO page titles!
-//
-const print = @import("std").debug.print;
+const std = @import("std");
+const print = std.debug.print;
-pub fn main() void {
- var com_frame = async getPageTitle("http://example.com");
- var org_frame = async getPageTitle("http://example.org");
+const RaceResult = std.Io.Select(union(enum) {
+ hare: []const u8,
+ tortoise: []const u8,
+});
- var com_title = com_frame;
- var org_title = org_frame;
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
- print(".com: {s}, .org: {s}.\n", .{ com_title, org_title });
+ 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 getPageTitle(url: []const u8) []const u8 {
- // Please PRETEND this is actually making a network request.
- _ = url;
- return "Example Title";
+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/patches/patches/089_async6.patch b/patches/patches/089_async6.patch
new file mode 100644
index 0000000..4ba0b49
--- /dev/null
+++ b/patches/patches/089_async6.patch
@@ -0,0 +1,11 @@
+--- exercises/089_async6.zig 2026-04-02 10:25:34.016616118 +0200
++++ answers/089_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}),
From ffde357f303e7459a12cfe4b785ae9e8ef9ebe30 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Thu, 2 Apr 2026 10:38:45 +0200
Subject: [PATCH 07/25] revival of the async-io functions, #90
---
build.zig | 8 +--
exercises/090_async7.zig | 116 ++++++++++++-------------------
patches/patches/090_async7.patch | 13 ++++
3 files changed, 58 insertions(+), 79 deletions(-)
create mode 100644 patches/patches/090_async7.patch
diff --git a/build.zig b/build.zig
index d0f4b52..3dc0520 100644
--- a/build.zig
+++ b/build.zig
@@ -1149,15 +1149,11 @@ const exercises = [_]Exercise{
},
.{
.main_file = "089_async6.zig",
- .output = ".com: Example Title, .org: Example Title.",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output = "Hare: I'm fast!",
},
.{
.main_file = "090_async7.zig",
- .output = "beef? BEEF!",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output = "Counter: 400 (expected: 400)",
},
.{
.main_file = "091_async8.zig",
diff --git a/exercises/090_async7.zig b/exercises/090_async7.zig
index 2da3a4a..f914aef 100644
--- a/exercises/090_async7.zig
+++ b/exercises/090_async7.zig
@@ -1,87 +1,57 @@
//
-// Remember how a function with 'suspend' is async and calling an
-// async function without the 'async' keyword makes the CALLING
-// function async?
+// When multiple async tasks access shared data, you need
+// synchronization! Io provides a Mutex for this:
//
-// fn fooThatMightSuspend(maybe: bool) void {
-// if (maybe) suspend {}
-// }
+// var mutex: std.Io.Mutex = .init;
//
-// fn bar() void {
-// fooThatMightSuspend(true); // Now bar() is async!
-// }
+// // In a task:
+// try mutex.lock(io); // blocks until lock is acquired
+// defer mutex.unlock();
+// // ... critical section: safe to modify shared data ...
//
-// But if you KNOW the function won't suspend, you can make a
-// promise to the compiler with the 'nosuspend' keyword:
+// Without the mutex, concurrent tasks could read and write the
+// same memory simultaneously, causing a data race — the result
+// would be unpredictable.
//
-// fn bar() void {
-// nosuspend fooThatMightSuspend(false);
-// }
+// mutex.lock() is a cancellation point — it can return
+// error.Canceled. There's also tryLock() which returns
+// immediately (true if acquired, false if not).
//
-// If the function does suspend and YOUR PROMISE TO THE COMPILER
-// IS BROKEN, the program will panic at runtime, which is
-// probably better than you deserve, you oathbreaker! >:-(
+// 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 print = @import("std").debug.print;
+const std = @import("std");
+const print = std.debug.print;
-pub fn main() void {
+const SharedState = struct {
+ counter: u32 = 0,
+ mutex: std.Io.Mutex = .init,
+};
- // The main() function can not be async. But we know
- // getBeef() will not suspend with this particular
- // invocation. Please make this okay:
- var my_beef = getBeef(0);
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
+ var state = SharedState{};
- print("beef? {X}!\n", .{my_beef});
+ 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: {} (expected: 400)\n", .{state.counter});
}
-fn getBeef(input: u32) u32 {
- if (input == 0xDEAD) {
- suspend {}
+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;
}
-
- return 0xBEEF;
}
-//
-// Going Deeper Into...
-// ...uNdeFiNEd beHAVi0r!
-//
-// We haven't discussed it yet, but runtime "safety" features
-// require some extra instructions in your compiled program.
-// Most of the time, you're going to want to keep these in.
-//
-// But in some programs, when data integrity is less important
-// than raw speed (some games, for example), you can compile
-// without these safety features.
-//
-// Instead of a safe panic when something goes wrong, your
-// program will now exhibit Undefined Behavior (UB), which simply
-// means that the Zig language does not (cannot) define what will
-// happen. The best case is that it will crash, but in the worst
-// case, it will continue to run with the wrong results and
-// corrupt your data or expose you to security risks.
-//
-// This program is a great way to explore UB. Once you get it
-// working, try calling the getBeef() function with the value
-// 0xDEAD so that it will invoke the 'suspend' keyword:
-//
-// getBeef(0xDEAD)
-//
-// Now when you run the program, it will panic and give you a
-// nice stack trace to help debug the problem.
-//
-// zig run exercises/090_async7.zig
-// thread 328 panic: async function called...
-// ...
-//
-// But see what happens when you turn off safety checks by using
-// ReleaseFast mode:
-//
-// zig run -O ReleaseFast exercises/090_async7.zig
-// beef? 0!
-//
-// This is the wrong result. On your computer, you may get a
-// different answer or it might crash! What exactly will happen
-// is UNDEFINED. Your computer is now like a wild animal,
-// reacting to bits and bytes of raw memory with the base
-// instincts of the CPU. It is both terrifying and exhilarating.
-//
diff --git a/patches/patches/090_async7.patch b/patches/patches/090_async7.patch
new file mode 100644
index 0000000..2108d51
--- /dev/null
+++ b/patches/patches/090_async7.patch
@@ -0,0 +1,13 @@
+--- exercises/090_async7.zig 2026-04-02 10:36:42.910708919 +0200
++++ answers/090_async7.zig 2026-04-02 10:36:51.965884223 +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;
+ }
From e0259f43a726f61da14686de802021fcdb9aacd0 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 13:35:56 +0200
Subject: [PATCH 08/25] Insert space for additional async exercises
---
build.zig | 64 ++++++++++-------
exercises/090_async7.zig | 2 +-
exercises/091_async8.zig | 69 +++++++++++++------
...{092_interfaces.zig => 095_interfaces.zig} | 0
.../{093_hello_c.zig => 096_hello_c.zig} | 0
exercises/{094_c_math.zig => 097_c_math.zig} | 0
exercises/{095_for3.zig => 098_for3.zig} | 0
...location.zig => 099_memory_allocation.zig} | 0
...ipulation.zig => 100_bit_manipulation.zig} | 0
...ulation2.zig => 101_bit_manipulation2.zig} | 0
...{099_formatting.zig => 102_formatting.zig} | 0
exercises/{100_for4.zig => 103_for4.zig} | 0
exercises/{101_for5.zig => 104_for5.zig} | 0
.../{102_testing.zig => 105_testing.zig} | 0
..._tokenization.zig => 106_tokenization.zig} | 0
.../{104_threading.zig => 107_threading.zig} | 0
...{105_threading2.zig => 108_threading2.zig} | 0
exercises/{106_files.zig => 109_files.zig} | 0
exercises/{107_files2.zig => 110_files2.zig} | 0
...eled_switch.zig => 111_labeled_switch.zig} | 0
.../{109_vectors.zig => 112_vectors.zig} | 0
exercises/{110_quiz9.zig => 113_quiz9.zig} | 0
exercises/{111_packed.zig => 114_packed.zig} | 0
.../{112_packed2.zig => 115_packed2.zig} | 0
patches/patches/091_async8.patch | 11 +++
..._interfaces.patch => 095_interfaces.patch} | 4 +-
.../{093_hello_c.patch => 096_hello_c.patch} | 4 +-
patches/patches/097_bit_manipulation.patch | 11 ---
.../{094_c_math.patch => 097_c_math.patch} | 4 +-
.../{095_for3.patch => 098_for3.patch} | 4 +-
...tion.patch => 099_memory_allocation.patch} | 4 +-
patches/patches/100_bit_manipulation.patch | 11 +++
...ion2.patch => 101_bit_manipulation2.patch} | 4 +-
..._formatting.patch => 102_formatting.patch} | 4 +-
.../{100_for4.patch => 103_for4.patch} | 4 +-
.../{101_for5.patch => 104_for5.patch} | 4 +-
.../{102_testing.patch => 105_testing.patch} | 4 +-
...enization.patch => 106_tokenization.patch} | 4 +-
...04_threading.patch => 107_threading.patch} | 4 +-
..._threading2.patch => 108_threading2.patch} | 4 +-
.../{106_files.patch => 109_files.patch} | 4 +-
.../{107_files2.patch => 110_files2.patch} | 4 +-
..._switch.patch => 111_labeled_switch.patch} | 6 +-
.../{109_vectors.patch => 112_vectors.patch} | 4 +-
.../{110_quiz9.patch => 113_quiz9.patch} | 16 ++---
.../{111_packed.patch => 114_packed.patch} | 4 +-
.../{112_packed2.patch => 115_packed2.patch} | 4 +-
47 files changed, 156 insertions(+), 106 deletions(-)
rename exercises/{092_interfaces.zig => 095_interfaces.zig} (100%)
rename exercises/{093_hello_c.zig => 096_hello_c.zig} (100%)
rename exercises/{094_c_math.zig => 097_c_math.zig} (100%)
rename exercises/{095_for3.zig => 098_for3.zig} (100%)
rename exercises/{096_memory_allocation.zig => 099_memory_allocation.zig} (100%)
rename exercises/{097_bit_manipulation.zig => 100_bit_manipulation.zig} (100%)
rename exercises/{098_bit_manipulation2.zig => 101_bit_manipulation2.zig} (100%)
rename exercises/{099_formatting.zig => 102_formatting.zig} (100%)
rename exercises/{100_for4.zig => 103_for4.zig} (100%)
rename exercises/{101_for5.zig => 104_for5.zig} (100%)
rename exercises/{102_testing.zig => 105_testing.zig} (100%)
rename exercises/{103_tokenization.zig => 106_tokenization.zig} (100%)
rename exercises/{104_threading.zig => 107_threading.zig} (100%)
rename exercises/{105_threading2.zig => 108_threading2.zig} (100%)
rename exercises/{106_files.zig => 109_files.zig} (100%)
rename exercises/{107_files2.zig => 110_files2.zig} (100%)
rename exercises/{108_labeled_switch.zig => 111_labeled_switch.zig} (100%)
rename exercises/{109_vectors.zig => 112_vectors.zig} (100%)
rename exercises/{110_quiz9.zig => 113_quiz9.zig} (100%)
rename exercises/{111_packed.zig => 114_packed.zig} (100%)
rename exercises/{112_packed2.zig => 115_packed2.zig} (100%)
create mode 100644 patches/patches/091_async8.patch
rename patches/patches/{092_interfaces.patch => 095_interfaces.patch} (62%)
rename patches/patches/{093_hello_c.patch => 096_hello_c.patch} (73%)
delete mode 100644 patches/patches/097_bit_manipulation.patch
rename patches/patches/{094_c_math.patch => 097_c_math.patch} (52%)
rename patches/patches/{095_for3.patch => 098_for3.patch} (80%)
rename patches/patches/{096_memory_allocation.patch => 099_memory_allocation.patch} (64%)
create mode 100644 patches/patches/100_bit_manipulation.patch
rename patches/patches/{098_bit_manipulation2.patch => 101_bit_manipulation2.patch} (55%)
rename patches/patches/{099_formatting.patch => 102_formatting.patch} (71%)
rename patches/patches/{100_for4.patch => 103_for4.patch} (71%)
rename patches/patches/{101_for5.patch => 104_for5.patch} (72%)
rename patches/patches/{102_testing.patch => 105_testing.patch} (81%)
rename patches/patches/{103_tokenization.patch => 106_tokenization.patch} (64%)
rename patches/patches/{104_threading.patch => 107_threading.patch} (82%)
rename patches/patches/{105_threading2.patch => 108_threading2.patch} (73%)
rename patches/patches/{106_files.patch => 109_files.patch} (84%)
rename patches/patches/{107_files2.patch => 110_files2.patch} (89%)
rename patches/patches/{108_labeled_switch.patch => 111_labeled_switch.patch} (83%)
rename patches/patches/{109_vectors.patch => 112_vectors.patch} (70%)
rename patches/patches/{110_quiz9.patch => 113_quiz9.patch} (87%)
rename patches/patches/{111_packed.patch => 114_packed.patch} (91%)
rename patches/patches/{112_packed2.patch => 115_packed2.patch} (87%)
diff --git a/build.zig b/build.zig
index 3dc0520..5e5d3de 100644
--- a/build.zig
+++ b/build.zig
@@ -1153,17 +1153,29 @@ const exercises = [_]Exercise{
},
.{
.main_file = "090_async7.zig",
- .output = "Counter: 400 (expected: 400)",
+ .output = "Counter: 400",
},
.{
.main_file = "091_async8.zig",
- .output = "ABCDEF",
- .skip = true,
- .skip_hint = "async has not been implemented in the current compiler version.",
+ .output = "Sum of 1..10 = 55",
},
-
.{
- .main_file = "092_interfaces.zig",
+ .main_file = "092_async9.zig",
+ .output = "",
+ .skip = true,
+ },
+ .{
+ .main_file = "093_async10.zig",
+ .output = "",
+ .skip = true,
+ },
+ .{
+ .main_file = "094_async_quiz.zig",
+ .output = "",
+ .skip = true,
+ },
+ .{
+ .main_file = "095_interfaces.zig",
.output =
\\Daily Insect Report:
\\Ant is alive.
@@ -1172,33 +1184,33 @@ const exercises = [_]Exercise{
, // pay attention to the comma
},
.{
- .main_file = "093_hello_c.zig",
+ .main_file = "096_hello_c.zig",
.output = "Hello C from Zig! - C result is 17 chars written.",
.link_libc = true,
},
.{
- .main_file = "094_c_math.zig",
+ .main_file = "097_c_math.zig",
.output = "The normalized angle of 765.2 degrees is 45.2 degrees.",
.link_libc = true,
},
.{
- .main_file = "095_for3.zig",
+ .main_file = "098_for3.zig",
.output = "1 2 4 7 8 11 13 14 16 17 19\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15",
},
.{
- .main_file = "096_memory_allocation.zig",
+ .main_file = "099_memory_allocation.zig",
.output = "Running Average: 0.30 0.25 0.20 0.18 0.22",
},
.{
- .main_file = "097_bit_manipulation.zig",
+ .main_file = "100_bit_manipulation.zig",
.output = "x = 1011; y = 1101",
},
.{
- .main_file = "098_bit_manipulation2.zig",
+ .main_file = "101_bit_manipulation2.zig",
.output = "Is this a pangram? true!",
},
.{
- .main_file = "099_formatting.zig",
+ .main_file = "102_formatting.zig",
.output =
\\
\\ X | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@@ -1235,11 +1247,11 @@ const exercises = [_]Exercise{
,
},
.{
- .main_file = "100_for4.zig",
+ .main_file = "103_for4.zig",
.output = "Arrays match!",
},
.{
- .main_file = "101_for5.zig",
+ .main_file = "104_for5.zig",
.output =
\\1. Wizard (Gold: 25, XP: 40)
\\2. Bard (Gold: 11, XP: 17)
@@ -1248,12 +1260,12 @@ const exercises = [_]Exercise{
, // pay attention to the comma
},
.{
- .main_file = "102_testing.zig",
+ .main_file = "105_testing.zig",
.output = "",
.kind = .@"test",
},
.{
- .main_file = "103_tokenization.zig",
+ .main_file = "106_tokenization.zig",
.output =
\\My
\\name
@@ -1274,7 +1286,7 @@ const exercises = [_]Exercise{
, // pay attention to the comma
},
.{
- .main_file = "104_threading.zig",
+ .main_file = "107_threading.zig",
.output =
\\Starting work...
\\thread 1: started.
@@ -1288,32 +1300,32 @@ const exercises = [_]Exercise{
, // pay attention to the comma
},
.{
- .main_file = "105_threading2.zig",
+ .main_file = "108_threading2.zig",
.output = "PI ≈ 3.14159265",
},
.{
- .main_file = "106_files.zig",
+ .main_file = "109_files.zig",
.output = "Successfully wrote 18 bytes.",
},
.{
- .main_file = "107_files2.zig",
+ .main_file = "110_files2.zig",
.output =
\\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
\\Successfully Read 18 bytes: It's zigling time!
, // pay attention to the comma
},
.{
- .main_file = "108_labeled_switch.zig",
+ .main_file = "111_labeled_switch.zig",
.output = "The pull request has been merged.",
},
.{
- .main_file = "109_vectors.zig",
+ .main_file = "112_vectors.zig",
.output =
\\Max difference (old fn): 0.014
\\Max difference (new fn): 0.014
, // pay attention to the comma
},
- .{ .main_file = "110_quiz9.zig", .output =
+ .{ .main_file = "113_quiz9.zig", .output =
\\Toggle pins with XOR on PORTB
\\-----------------------------
\\ 1100 // (initial state of PORTB)
@@ -1345,11 +1357,11 @@ const exercises = [_]Exercise{
\\= 0110
},
.{
- .main_file = "111_packed.zig",
+ .main_file = "114_packed.zig",
.output = "",
},
.{
- .main_file = "112_packed2.zig",
+ .main_file = "115_packed2.zig",
.output = "",
},
.{
diff --git a/exercises/090_async7.zig b/exercises/090_async7.zig
index f914aef..bfe6ffd 100644
--- a/exercises/090_async7.zig
+++ b/exercises/090_async7.zig
@@ -42,7 +42,7 @@ pub fn main(init: std.process.Init) !void {
try group.await(io);
- print("Counter: {} (expected: 400)\n", .{state.counter});
+ print("Counter: {}\n", .{state.counter});
}
fn increment(io: std.Io, state: *SharedState, times: u32) void {
diff --git a/exercises/091_async8.zig b/exercises/091_async8.zig
index cd9c975..10921c3 100644
--- a/exercises/091_async8.zig
+++ b/exercises/091_async8.zig
@@ -1,35 +1,62 @@
//
-// You have doubtless noticed that 'suspend' requires a block
-// expression like so:
+// Tasks often need to communicate! Io provides Queue for this —
+// a bounded, thread-safe channel for passing data between tasks:
//
-// suspend {}
+// var backing: [16]u32 = undefined;
+// var queue: std.Io.Queue(u32) = .init(&backing);
//
-// The suspend block executes when a function suspends. To get
-// sense for when this happens, please make the following
-// program print the string
+// // Producer task:
+// try queue.putOne(io, value); // blocks if queue is full
//
-// "ABCDEF"
+// // Consumer task:
+// const val = try queue.getOne(io); // blocks if queue is empty
//
-const print = @import("std").debug.print;
+// 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() void {
- print("A", .{});
+pub fn main(init: std.process.Init) !void {
+ const io = init.io;
- var frame = async suspendable();
+ var backing: [4]u32 = undefined;
+ var queue: std.Io.Queue(u32) = .init(&backing);
- print("X", .{});
+ var group: std.Io.Group = .init;
- resume frame;
+ group.async(io, producer, .{ io, &queue });
+ group.async(io, consumer, .{ io, &queue });
- print("F", .{});
+ try group.await(io);
}
-fn suspendable() void {
- print("X", .{});
-
- suspend {
- print("X", .{});
+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;
}
-
- print("X", .{});
+ // 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_interfaces.zig b/exercises/095_interfaces.zig
similarity index 100%
rename from exercises/092_interfaces.zig
rename to exercises/095_interfaces.zig
diff --git a/exercises/093_hello_c.zig b/exercises/096_hello_c.zig
similarity index 100%
rename from exercises/093_hello_c.zig
rename to exercises/096_hello_c.zig
diff --git a/exercises/094_c_math.zig b/exercises/097_c_math.zig
similarity index 100%
rename from exercises/094_c_math.zig
rename to exercises/097_c_math.zig
diff --git a/exercises/095_for3.zig b/exercises/098_for3.zig
similarity index 100%
rename from exercises/095_for3.zig
rename to exercises/098_for3.zig
diff --git a/exercises/096_memory_allocation.zig b/exercises/099_memory_allocation.zig
similarity index 100%
rename from exercises/096_memory_allocation.zig
rename to exercises/099_memory_allocation.zig
diff --git a/exercises/097_bit_manipulation.zig b/exercises/100_bit_manipulation.zig
similarity index 100%
rename from exercises/097_bit_manipulation.zig
rename to exercises/100_bit_manipulation.zig
diff --git a/exercises/098_bit_manipulation2.zig b/exercises/101_bit_manipulation2.zig
similarity index 100%
rename from exercises/098_bit_manipulation2.zig
rename to exercises/101_bit_manipulation2.zig
diff --git a/exercises/099_formatting.zig b/exercises/102_formatting.zig
similarity index 100%
rename from exercises/099_formatting.zig
rename to exercises/102_formatting.zig
diff --git a/exercises/100_for4.zig b/exercises/103_for4.zig
similarity index 100%
rename from exercises/100_for4.zig
rename to exercises/103_for4.zig
diff --git a/exercises/101_for5.zig b/exercises/104_for5.zig
similarity index 100%
rename from exercises/101_for5.zig
rename to exercises/104_for5.zig
diff --git a/exercises/102_testing.zig b/exercises/105_testing.zig
similarity index 100%
rename from exercises/102_testing.zig
rename to exercises/105_testing.zig
diff --git a/exercises/103_tokenization.zig b/exercises/106_tokenization.zig
similarity index 100%
rename from exercises/103_tokenization.zig
rename to exercises/106_tokenization.zig
diff --git a/exercises/104_threading.zig b/exercises/107_threading.zig
similarity index 100%
rename from exercises/104_threading.zig
rename to exercises/107_threading.zig
diff --git a/exercises/105_threading2.zig b/exercises/108_threading2.zig
similarity index 100%
rename from exercises/105_threading2.zig
rename to exercises/108_threading2.zig
diff --git a/exercises/106_files.zig b/exercises/109_files.zig
similarity index 100%
rename from exercises/106_files.zig
rename to exercises/109_files.zig
diff --git a/exercises/107_files2.zig b/exercises/110_files2.zig
similarity index 100%
rename from exercises/107_files2.zig
rename to exercises/110_files2.zig
diff --git a/exercises/108_labeled_switch.zig b/exercises/111_labeled_switch.zig
similarity index 100%
rename from exercises/108_labeled_switch.zig
rename to exercises/111_labeled_switch.zig
diff --git a/exercises/109_vectors.zig b/exercises/112_vectors.zig
similarity index 100%
rename from exercises/109_vectors.zig
rename to exercises/112_vectors.zig
diff --git a/exercises/110_quiz9.zig b/exercises/113_quiz9.zig
similarity index 100%
rename from exercises/110_quiz9.zig
rename to exercises/113_quiz9.zig
diff --git a/exercises/111_packed.zig b/exercises/114_packed.zig
similarity index 100%
rename from exercises/111_packed.zig
rename to exercises/114_packed.zig
diff --git a/exercises/112_packed2.zig b/exercises/115_packed2.zig
similarity index 100%
rename from exercises/112_packed2.zig
rename to exercises/115_packed2.zig
diff --git a/patches/patches/091_async8.patch b/patches/patches/091_async8.patch
new file mode 100644
index 0000000..865292f
--- /dev/null
+++ b/patches/patches/091_async8.patch
@@ -0,0 +1,11 @@
+--- exercises/091_async8.zig 2026-04-02 10:49:27.925721496 +0200
++++ answers/091_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/092_interfaces.patch b/patches/patches/095_interfaces.patch
similarity index 62%
rename from patches/patches/092_interfaces.patch
rename to patches/patches/095_interfaces.patch
index 36d8784..cb5fd62 100644
--- a/patches/patches/092_interfaces.patch
+++ b/patches/patches/095_interfaces.patch
@@ -1,5 +1,5 @@
---- exercises/092_interfaces.zig 2023-10-03 22:15:22.125574535 +0200
-+++ answers/092_interfaces.zig 2023-10-05 20:04:07.259437354 +0200
+--- exercises/095_interfaces.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/095_interfaces.zig 2026-04-03 13:09:13.722917764 +0200
@@ -106,7 +106,7 @@
for (my_insects) |insect| {
// Almost done! We want to print() each insect with a
diff --git a/patches/patches/093_hello_c.patch b/patches/patches/096_hello_c.patch
similarity index 73%
rename from patches/patches/093_hello_c.patch
rename to patches/patches/096_hello_c.patch
index fe1f853..f5fe06f 100644
--- a/patches/patches/093_hello_c.patch
+++ b/patches/patches/096_hello_c.patch
@@ -1,5 +1,5 @@
---- exercises/093_hello_c.zig 2023-10-03 22:15:22.125574535 +0200
-+++ answers/093_hello_c.zig 2023-10-05 20:04:07.262770750 +0200
+--- exercises/096_hello_c.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/096_hello_c.zig 2026-04-03 13:09:26.195163128 +0200
@@ -54,7 +54,7 @@
//
// In this exercise we use 'write' to output 17 chars,
diff --git a/patches/patches/097_bit_manipulation.patch b/patches/patches/097_bit_manipulation.patch
deleted file mode 100644
index 19ba876..0000000
--- a/patches/patches/097_bit_manipulation.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/097_bit_manipulation.zig 2025-05-12 21:25:03.395385743 +0200
-+++ answers/097_bit_manipulation.zig 2025-05-12 21:22:57.472986976 +0200
-@@ -80,7 +80,7 @@
- y ^= x;
-
- // What must be written here?
-- ???;
-+ x ^= y;
-
- print("x = {b}; y = {b}\n", .{ x, y });
- }
diff --git a/patches/patches/094_c_math.patch b/patches/patches/097_c_math.patch
similarity index 52%
rename from patches/patches/094_c_math.patch
rename to patches/patches/097_c_math.patch
index f8c7620..917fe6b 100644
--- a/patches/patches/094_c_math.patch
+++ b/patches/patches/097_c_math.patch
@@ -1,5 +1,5 @@
---- exercises/094_c_math.zig 2024-02-28 12:50:35.789939935 +0100
-+++ answers/094_c_math.zig 2024-02-28 12:53:57.910309471 +0100
+--- exercises/097_c_math.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/097_c_math.zig 2026-04-03 13:09:32.059278502 +0200
@@ -26,7 +26,7 @@
const c = @cImport({
diff --git a/patches/patches/095_for3.patch b/patches/patches/098_for3.patch
similarity index 80%
rename from patches/patches/095_for3.patch
rename to patches/patches/098_for3.patch
index a158b31..c280bc6 100644
--- a/patches/patches/095_for3.patch
+++ b/patches/patches/098_for3.patch
@@ -1,5 +1,5 @@
---- exercises/095_for3.zig 2026-02-27 19:33:59
-+++ answers/095_for3.zig 2026-02-27 19:33:38
+--- exercises/098_for3.zig 2026-03-20 19:23:48.873150100 +0100
++++ answers/098_for3.zig 2026-04-03 13:09:39.916433087 +0200
@@ -56,7 +56,7 @@
// I want to print every number between 1 and 20 that is NOT
diff --git a/patches/patches/096_memory_allocation.patch b/patches/patches/099_memory_allocation.patch
similarity index 64%
rename from patches/patches/096_memory_allocation.patch
rename to patches/patches/099_memory_allocation.patch
index c26eeeb..d83a0ae 100644
--- a/patches/patches/096_memory_allocation.patch
+++ b/patches/patches/099_memory_allocation.patch
@@ -1,5 +1,5 @@
---- exercises/096_memory_allocation.zig 2023-11-21 14:55:33.805678390 +0100
-+++ answers/096_memory_allocation.zig 2023-11-21 14:56:00.236163484 +0100
+--- exercises/099_memory_allocation.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/099_memory_allocation.zig 2026-04-03 13:09:47.403580391 +0200
@@ -64,7 +64,7 @@
const allocator = arena.allocator();
diff --git a/patches/patches/100_bit_manipulation.patch b/patches/patches/100_bit_manipulation.patch
new file mode 100644
index 0000000..f70cc41
--- /dev/null
+++ b/patches/patches/100_bit_manipulation.patch
@@ -0,0 +1,11 @@
+--- exercises/100_bit_manipulation.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/100_bit_manipulation.zig 2026-04-02 10:51:15.795831343 +0200
+@@ -80,7 +80,7 @@
+ y ^= x;
+
+ // What must be written here?
+- ???;
++ x ^= y;
+
+ print("x = {b}; y = {b}\n", .{ x, y });
+ }
diff --git a/patches/patches/098_bit_manipulation2.patch b/patches/patches/101_bit_manipulation2.patch
similarity index 55%
rename from patches/patches/098_bit_manipulation2.patch
rename to patches/patches/101_bit_manipulation2.patch
index 5354a7b..5179272 100644
--- a/patches/patches/098_bit_manipulation2.patch
+++ b/patches/patches/101_bit_manipulation2.patch
@@ -1,5 +1,5 @@
---- exercises/098_bit_manipulation2.zig 2023-10-03 22:15:22.125574535 +0200
-+++ answers/098_bit_manipulation2.zig 2023-10-05 20:04:07.286104520 +0200
+--- exercises/101_bit_manipulation2.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/101_bit_manipulation2.zig 2026-04-02 10:51:15.797831382 +0200
@@ -60,5 +60,5 @@
// and if so, we know the given string is a pangram
//
diff --git a/patches/patches/099_formatting.patch b/patches/patches/102_formatting.patch
similarity index 71%
rename from patches/patches/099_formatting.patch
rename to patches/patches/102_formatting.patch
index a56b556..dba8aef 100644
--- a/patches/patches/099_formatting.patch
+++ b/patches/patches/102_formatting.patch
@@ -1,5 +1,5 @@
---- exercises/099_formatting.zig 2024-11-07 21:45:10.459123650 +0100
-+++ answers/099_formatting.zig 2024-11-07 21:43:55.154345991 +0100
+--- exercises/102_formatting.zig 2026-03-20 19:23:48.873150100 +0100
++++ answers/102_formatting.zig 2026-04-02 10:51:15.799831421 +0200
@@ -131,7 +131,7 @@
for (0..size) |b| {
// What formatting is needed here to make our columns
diff --git a/patches/patches/100_for4.patch b/patches/patches/103_for4.patch
similarity index 71%
rename from patches/patches/100_for4.patch
rename to patches/patches/103_for4.patch
index ad73e9a..7a7e161 100644
--- a/patches/patches/100_for4.patch
+++ b/patches/patches/103_for4.patch
@@ -1,5 +1,5 @@
---- exercises/100_for4.zig 2023-10-03 22:15:22.125574535 +0200
-+++ answers/100_for4.zig 2023-10-05 20:04:07.296104707 +0200
+--- exercises/103_for4.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/103_for4.zig 2026-04-02 10:51:15.801831460 +0200
@@ -39,7 +39,7 @@
const hex_nums = [_]u8{ 0xb, 0x2a, 0x77 };
const dec_nums = [_]u8{ 11, 42, 119 };
diff --git a/patches/patches/101_for5.patch b/patches/patches/104_for5.patch
similarity index 72%
rename from patches/patches/101_for5.patch
rename to patches/patches/104_for5.patch
index 00f19df..ef77013 100644
--- a/patches/patches/101_for5.patch
+++ b/patches/patches/104_for5.patch
@@ -1,5 +1,5 @@
---- exercises/101_for5.zig 2023-10-03 22:15:22.125574535 +0200
-+++ answers/101_for5.zig 2023-10-05 20:04:07.299438103 +0200
+--- exercises/104_for5.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/104_for5.zig 2026-04-02 10:51:15.803831499 +0200
@@ -51,7 +51,7 @@
// We would like to number our list starting with 1, not 0.
diff --git a/patches/patches/102_testing.patch b/patches/patches/105_testing.patch
similarity index 81%
rename from patches/patches/102_testing.patch
rename to patches/patches/105_testing.patch
index 6d18bf6..069b72b 100644
--- a/patches/patches/102_testing.patch
+++ b/patches/patches/105_testing.patch
@@ -1,5 +1,5 @@
---- exercises/102_testing.zig 2025-10-24 12:54:56
-+++ answers/102_testing.zig 2025-10-24 12:56:33
+--- exercises/105_testing.zig 2026-03-20 19:23:48.873150100 +0100
++++ answers/105_testing.zig 2026-04-02 10:51:15.805831538 +0200
@@ -71,7 +71,7 @@
// The corresponding test is not much different from the previous one. Except
// that it contains an error that you need to correct.
diff --git a/patches/patches/103_tokenization.patch b/patches/patches/106_tokenization.patch
similarity index 64%
rename from patches/patches/103_tokenization.patch
rename to patches/patches/106_tokenization.patch
index 941ca3b..4792df6 100644
--- a/patches/patches/103_tokenization.patch
+++ b/patches/patches/106_tokenization.patch
@@ -1,5 +1,5 @@
---- exercises/103_tokenization.zig 2026-02-27 19:25:11
-+++ answers/103_tokenization.zig 2026-02-27 19:26:04
+--- exercises/106_tokenization.zig 2026-03-20 19:23:48.873150100 +0100
++++ answers/106_tokenization.zig 2026-04-02 10:51:15.807831578 +0200
@@ -134,7 +134,7 @@
;
diff --git a/patches/patches/104_threading.patch b/patches/patches/107_threading.patch
similarity index 82%
rename from patches/patches/104_threading.patch
rename to patches/patches/107_threading.patch
index e6fe0f4..3f65ef4 100644
--- a/patches/patches/104_threading.patch
+++ b/patches/patches/107_threading.patch
@@ -1,5 +1,5 @@
---- exercises/104_threading.zig 2026-04-01 23:31:10.073198955 +0200
-+++ answers/104_threading.zig 2026-04-01 23:29:51.314585919 +0200
+--- exercises/107_threading.zig 2026-04-01 23:31:10.073198955 +0200
++++ answers/107_threading.zig 2026-04-02 10:51:15.809831617 +0200
@@ -88,12 +88,12 @@
defer handle.join();
diff --git a/patches/patches/105_threading2.patch b/patches/patches/108_threading2.patch
similarity index 73%
rename from patches/patches/105_threading2.patch
rename to patches/patches/108_threading2.patch
index dfa5613..d71f254 100644
--- a/patches/patches/105_threading2.patch
+++ b/patches/patches/108_threading2.patch
@@ -1,5 +1,5 @@
---- exercises/105_threading2.zig 2024-03-23 16:35:14.754540802 +0100
-+++ answers/105_threading2.zig 2024-03-23 16:38:00.577539733 +0100
+--- exercises/108_threading2.zig 2025-08-15 15:17:57.839348063 +0200
++++ answers/108_threading2.zig 2026-04-02 10:51:15.811831656 +0200
@@ -81,8 +81,8 @@
defer handle1.join();
diff --git a/patches/patches/106_files.patch b/patches/patches/109_files.patch
similarity index 84%
rename from patches/patches/106_files.patch
rename to patches/patches/109_files.patch
index 27bcb56..ac59d70 100644
--- a/patches/patches/106_files.patch
+++ b/patches/patches/109_files.patch
@@ -1,5 +1,5 @@
---- exercises/106_files.zig 2026-01-09 22:41:19.373872684 +0100
-+++ answers/106_files.zig 2026-01-09 22:41:44.518372910 +0100
+--- exercises/109_files.zig 2026-03-20 19:23:48.874150121 +0100
++++ answers/109_files.zig 2026-04-02 10:51:15.813831695 +0200
@@ -41,7 +41,7 @@
// by doing nothing
//
diff --git a/patches/patches/107_files2.patch b/patches/patches/110_files2.patch
similarity index 89%
rename from patches/patches/107_files2.patch
rename to patches/patches/110_files2.patch
index 6820742..64dc193 100644
--- a/patches/patches/107_files2.patch
+++ b/patches/patches/110_files2.patch
@@ -1,5 +1,5 @@
---- exercises/107_files2.zig 2026-01-09 22:43:15.211177186 +0100
-+++ answers/107_files2.zig 2026-01-09 22:42:48.943654602 +0100
+--- exercises/110_files2.zig 2026-03-20 19:23:48.874150121 +0100
++++ answers/110_files2.zig 2026-04-02 10:51:15.815831734 +0200
@@ -39,7 +39,7 @@
// initialize an array of u8 with all letter 'A'
// we need to pick the size of the array, 64 seems like a good number
diff --git a/patches/patches/108_labeled_switch.patch b/patches/patches/111_labeled_switch.patch
similarity index 83%
rename from patches/patches/108_labeled_switch.patch
rename to patches/patches/111_labeled_switch.patch
index fa9dd67..0fb872d 100644
--- a/patches/patches/108_labeled_switch.patch
+++ b/patches/patches/111_labeled_switch.patch
@@ -1,6 +1,6 @@
---- exercises/108_labeled_switch.zig 2024-09-20 12:09:24.370066539 +0200
-+++ answers/108_labeled_switch.zig 2024-09-20 12:09:06.499711739 +0200
-@@ -65,13 +65,13 @@
+--- exercises/111_labeled_switch.zig 2025-08-15 15:17:57.840348083 +0200
++++ answers/111_labeled_switch.zig 2026-04-02 10:51:15.817831773 +0200
+@@ -67,13 +67,13 @@
// how would you fix it?
pr: switch (PullRequestState.Draft) {
PullRequestState.Draft => continue :pr PullRequestState.InReview,
diff --git a/patches/patches/109_vectors.patch b/patches/patches/112_vectors.patch
similarity index 70%
rename from patches/patches/109_vectors.patch
rename to patches/patches/112_vectors.patch
index bf18cc0..5bf684b 100644
--- a/patches/patches/109_vectors.patch
+++ b/patches/patches/112_vectors.patch
@@ -1,5 +1,5 @@
---- exercises/109_vectors.zig 2024-11-07 14:57:09.673383618 +0100
-+++ answers/109_vectors.zig 2024-11-07 14:22:59.069150138 +0100
+--- exercises/112_vectors.zig 2025-08-15 15:17:57.840348083 +0200
++++ answers/112_vectors.zig 2026-04-02 10:51:15.819831812 +0200
@@ -121,8 +121,8 @@
const Vec4 = @Vector(4, f32);
diff --git a/patches/patches/110_quiz9.patch b/patches/patches/113_quiz9.patch
similarity index 87%
rename from patches/patches/110_quiz9.patch
rename to patches/patches/113_quiz9.patch
index 9d9b864..03bd572 100644
--- a/patches/patches/110_quiz9.patch
+++ b/patches/patches/113_quiz9.patch
@@ -1,6 +1,6 @@
---- exercises/110_quiz9.zig 2025-02-08 13:19:48.522641785 -0800
-+++ answers/110_quiz9.zig 2025-02-10 17:42:04.525004335 -0800
-@@ -108,7 +108,7 @@
+--- exercises/113_quiz9.zig 2025-08-15 15:17:57.840348083 +0200
++++ answers/113_quiz9.zig 2026-04-02 10:51:15.821831851 +0200
+@@ -84,7 +84,7 @@
PORTB = 0b1100;
print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB});
print("^ {b:0>4} // (bitmask)\n", .{0b0101});
@@ -9,7 +9,7 @@
checkAnswer(0b1001, PORTB);
newline();
-@@ -116,7 +116,7 @@
+@@ -92,7 +92,7 @@
PORTB = 0b1100;
print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB});
print("^ {b:0>4} // (bitmask)\n", .{0b0011});
@@ -18,7 +18,7 @@
checkAnswer(0b1111, PORTB);
newline();
-@@ -170,7 +170,7 @@
+@@ -103,7 +103,7 @@
PORTB = 0b1001; // reset PORTB
print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB});
print("| {b:0>4} // (bitmask)\n", .{0b0100});
@@ -27,7 +27,7 @@
checkAnswer(0b1101, PORTB);
newline();
-@@ -178,7 +178,7 @@
+@@ -111,7 +111,7 @@
PORTB = 0b1001; // reset PORTB
print(" {b:0>4} // (reset state)\n", .{PORTB});
print("| {b:0>4} // (bitmask)\n", .{0b0100});
@@ -36,7 +36,7 @@
checkAnswer(0b1101, PORTB);
newline();
-@@ -269,7 +269,7 @@
+@@ -122,7 +122,7 @@
PORTB = 0b1110; // reset PORTB
print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB});
print("& {b:0>4} // (bitmask)\n", .{0b1011});
@@ -45,7 +45,7 @@
checkAnswer(0b1010, PORTB);
newline();
-@@ -277,7 +277,7 @@
+@@ -130,7 +130,7 @@
PORTB = 0b0111; // reset PORTB
print(" {b:0>4} // (reset state)\n", .{PORTB});
print("& {b:0>4} // (bitmask)\n", .{0b1110});
diff --git a/patches/patches/111_packed.patch b/patches/patches/114_packed.patch
similarity index 91%
rename from patches/patches/111_packed.patch
rename to patches/patches/114_packed.patch
index d38ac68..3c79f18 100644
--- a/patches/patches/111_packed.patch
+++ b/patches/patches/114_packed.patch
@@ -1,5 +1,5 @@
---- exercises/111_packed.zig 2026-03-13 11:18:44
-+++ answers/111_packed.zig 2026-03-13 11:18:57
+--- exercises/114_packed.zig 2026-03-20 19:23:48.874150121 +0100
++++ answers/114_packed.zig 2026-04-02 10:51:15.824831910 +0200
@@ -41,7 +41,7 @@
const PackedStruct = packed struct {
diff --git a/patches/patches/112_packed2.patch b/patches/patches/115_packed2.patch
similarity index 87%
rename from patches/patches/112_packed2.patch
rename to patches/patches/115_packed2.patch
index 9b01eb3..81c32ab 100644
--- a/patches/patches/112_packed2.patch
+++ b/patches/patches/115_packed2.patch
@@ -1,5 +1,5 @@
---- exercises/112_packed2.zig 2026-03-13 11:14:08
-+++ answers/112_packed2.zig 2026-03-13 11:14:16
+--- exercises/115_packed2.zig 2026-03-20 19:23:48.874150121 +0100
++++ answers/115_packed2.zig 2026-04-02 10:51:15.826831949 +0200
@@ -13,9 +13,9 @@
const s: S = .{ .a = true, .b = -1 };
switch (s) {
From 903c33cd0a6dfdc050e8575f64aa60a9a09001e0 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 13:46:35 +0200
Subject: [PATCH 09/25] new async exercise
---
exercises/092_async9.zig | 57 ++++++++++++++++++++++++++++++++
patches/patches/092_async9.patch | 11 ++++++
2 files changed, 68 insertions(+)
create mode 100644 exercises/092_async9.zig
create mode 100644 patches/patches/092_async9.patch
diff --git a/exercises/092_async9.zig b/exercises/092_async9.zig
new file mode 100644
index 0000000..ad30dcf
--- /dev/null
+++ b/exercises/092_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/patches/patches/092_async9.patch b/patches/patches/092_async9.patch
new file mode 100644
index 0000000..b7c8de9
--- /dev/null
+++ b/patches/patches/092_async9.patch
@@ -0,0 +1,11 @@
+--- exercises/092_async9.zig 2026-04-03 13:44:50.526780809 +0200
++++ answers/092_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", .{});
+
From 25009361533be5e45cf59d9840edf5d13cfb8d6d Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 14:28:19 +0200
Subject: [PATCH 10/25] new async exercise
---
build.zig | 15 +++++--
exercises/093_async10.zig | 67 +++++++++++++++++++++++++++++++
patches/patches/093_async10.patch | 13 ++++++
3 files changed, 91 insertions(+), 4 deletions(-)
create mode 100644 exercises/093_async10.zig
create mode 100644 patches/patches/093_async10.patch
diff --git a/build.zig b/build.zig
index 5e5d3de..b9aaac3 100644
--- a/build.zig
+++ b/build.zig
@@ -1161,13 +1161,20 @@ const exercises = [_]Exercise{
},
.{
.main_file = "092_async9.zig",
- .output = "",
- .skip = true,
+ .output =
+ \\Main thread continues...
+ \\Computing on a separate thread!
+ \\Main thread done waiting.
+ \\Result: 123
+ , // pay attention to the comma
},
.{
.main_file = "093_async10.zig",
- .output = "",
- .skip = true,
+ .output =
+ \\Starting critical section...
+ \\Critical section completed safely.
+ \\Task result: All data saved.
+ , // pay attention to the comma
},
.{
.main_file = "094_async_quiz.zig",
diff --git a/exercises/093_async10.zig b/exercises/093_async10.zig
new file mode 100644
index 0000000..6ed229d
--- /dev/null
+++ b/exercises/093_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/patches/patches/093_async10.patch b/patches/patches/093_async10.patch
new file mode 100644
index 0000000..69f7518
--- /dev/null
+++ b/patches/patches/093_async10.patch
@@ -0,0 +1,13 @@
+--- exercises/093_async10.zig 2026-04-03 14:25:16.600025924 +0200
++++ answers/093_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!
From 1c6487c1e79cbe0d59a39b483af8ec44b59c586e Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 18:11:00 +0200
Subject: [PATCH 11/25] added async-io quiz
---
build.zig | 46 +++--
...{095_interfaces.zig => 084_interfaces.zig} | 0
exercises/{084_async.zig => 085_async.zig} | 0
exercises/{085_async2.zig => 086_async2.zig} | 0
exercises/{086_async3.zig => 087_async3.zig} | 0
exercises/{087_async4.zig => 088_async4.zig} | 0
exercises/{088_async5.zig => 089_async5.zig} | 0
exercises/{089_async6.zig => 090_async6.zig} | 0
exercises/{090_async7.zig => 091_async7.zig} | 0
exercises/{091_async8.zig => 092_async8.zig} | 0
exercises/{092_async9.zig => 093_async9.zig} | 0
.../{093_async10.zig => 094_async10.zig} | 0
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 +++++
25 files changed, 399 insertions(+), 20 deletions(-)
rename exercises/{095_interfaces.zig => 084_interfaces.zig} (100%)
rename exercises/{084_async.zig => 085_async.zig} (100%)
rename exercises/{085_async2.zig => 086_async2.zig} (100%)
rename exercises/{086_async3.zig => 087_async3.zig} (100%)
rename exercises/{087_async4.zig => 088_async4.zig} (100%)
rename exercises/{088_async5.zig => 089_async5.zig} (100%)
rename exercises/{089_async6.zig => 090_async6.zig} (100%)
rename exercises/{090_async7.zig => 091_async7.zig} (100%)
rename exercises/{091_async8.zig => 092_async8.zig} (100%)
rename exercises/{092_async9.zig => 093_async9.zig} (100%)
rename exercises/{093_async10.zig => 094_async10.zig} (100%)
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
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/095_interfaces.zig b/exercises/084_interfaces.zig
similarity index 100%
rename from exercises/095_interfaces.zig
rename to exercises/084_interfaces.zig
diff --git a/exercises/084_async.zig b/exercises/085_async.zig
similarity index 100%
rename from exercises/084_async.zig
rename to exercises/085_async.zig
diff --git a/exercises/085_async2.zig b/exercises/086_async2.zig
similarity index 100%
rename from exercises/085_async2.zig
rename to exercises/086_async2.zig
diff --git a/exercises/086_async3.zig b/exercises/087_async3.zig
similarity index 100%
rename from exercises/086_async3.zig
rename to exercises/087_async3.zig
diff --git a/exercises/087_async4.zig b/exercises/088_async4.zig
similarity index 100%
rename from exercises/087_async4.zig
rename to exercises/088_async4.zig
diff --git a/exercises/088_async5.zig b/exercises/089_async5.zig
similarity index 100%
rename from exercises/088_async5.zig
rename to exercises/089_async5.zig
diff --git a/exercises/089_async6.zig b/exercises/090_async6.zig
similarity index 100%
rename from exercises/089_async6.zig
rename to exercises/090_async6.zig
diff --git a/exercises/090_async7.zig b/exercises/091_async7.zig
similarity index 100%
rename from exercises/090_async7.zig
rename to exercises/091_async7.zig
diff --git a/exercises/091_async8.zig b/exercises/092_async8.zig
similarity index 100%
rename from exercises/091_async8.zig
rename to exercises/092_async8.zig
diff --git a/exercises/092_async9.zig b/exercises/093_async9.zig
similarity index 100%
rename from exercises/092_async9.zig
rename to exercises/093_async9.zig
diff --git a/exercises/093_async10.zig b/exercises/094_async10.zig
similarity index 100%
rename from exercises/093_async10.zig
rename to exercises/094_async10.zig
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;
+ }
+ }
+
From 7fae6e0607854831422115674f9cc02893ee217d Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 19:07:10 +0200
Subject: [PATCH 12/25] improved timestamp comparison
---
build.zig | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/build.zig b/build.zig
index beb0d72..7bde273 100644
--- a/build.zig
+++ b/build.zig
@@ -436,17 +436,29 @@ const ZiglingStep = struct {
else
result.stderr;
+ // NOTE: exercise.output can never contain a CR character.
+ // See https://ziglang.org/documentation/master/#Source-Encoding.
+ const output = trimLines(b.allocator, raw_output) catch @panic("OOM");
+
// Validate the output.
var exercise_output = self.exercise.output;
- // Insert timestamp for exercise 84
+ // Insert timestamp for exercise 85
if (self.exercise.timestamp) {
- var ts_buf: [20]u8 = undefined;
- const timestamp = std.fmt.bufPrint(&ts_buf, "{}", .{std.Io.Timestamp.now(io, .real).toSeconds()}) catch unreachable;
+ // Compare timestamp from exercise with now, diff < 5 seconds is valid
+ var ts_buf: [20]u8 = undefined;
+ const ts_slice = output[14..24];
+ const ts_value = try std.fmt.parseInt(i64, ts_slice, 10);
+ const ts_build = std.Io.Timestamp.now(io, .real).toSeconds();
+ const ts_diff = @abs(ts_build - ts_value);
+ const timestamp = std.fmt.bufPrint(&ts_buf, "{}", .{if (ts_diff < 5) ts_value else ts_build}) catch unreachable;
+
+ // Insert timestamp into check string
var buf: [100]u8 = undefined;
const prefix_len = 14;
const placeholder_len = 11;
+
@memcpy(buf[0..prefix_len], exercise_output[0..prefix_len]);
@memcpy(buf[prefix_len..][0..timestamp.len], timestamp);
const suffix = exercise_output[prefix_len + placeholder_len ..];
@@ -457,9 +469,6 @@ const ZiglingStep = struct {
exercise_output = buf[0..total_len];
}
- // NOTE: exercise.output can never contain a CR character.
- // See https://ziglang.org/documentation/master/#Source-Encoding.
- const output = trimLines(b.allocator, raw_output) catch @panic("OOM");
if (!std.mem.eql(u8, output, exercise_output)) {
const red = red_bold_text;
const reset = reset_text;
From f6a6798c8b6b813bd2ceee81db276e05327a76e0 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 19:28:12 +0200
Subject: [PATCH 13/25] improved report design
---
build.zig | 2 +-
exercises/084_interfaces.zig | 2 +-
patches/patches/084_interfaces.patch | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/build.zig b/build.zig
index 7bde273..d3d91ee 100644
--- a/build.zig
+++ b/build.zig
@@ -1121,7 +1121,7 @@ const exercises = [_]Exercise{
.{
.main_file = "084_interfaces.zig",
.output =
- \\Daily Insect Report:
+ \\=== Doctor Zoraptera's Insect Report ===
\\Ant is alive.
\\Bee visited 17 flowers.
\\Grasshopper hopped 32 meters.
diff --git a/exercises/084_interfaces.zig b/exercises/084_interfaces.zig
index 7775dd5..fd61ead 100644
--- a/exercises/084_interfaces.zig
+++ b/exercises/084_interfaces.zig
@@ -102,7 +102,7 @@ pub fn main() !void {
Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } },
};
- std.debug.print("Daily Insect Report:\n", .{});
+ std.debug.print("=== Doctor Zoraptera's Insect Report ===\n", .{});
for (my_insects) |insect| {
// Almost done! We want to print() each insect with a
// single method call here.
diff --git a/patches/patches/084_interfaces.patch b/patches/patches/084_interfaces.patch
index a1d0628..d3a45fd 100644
--- a/patches/patches/084_interfaces.patch
+++ b/patches/patches/084_interfaces.patch
@@ -1,5 +1,5 @@
---- exercises/084_interfaces.zig 2025-08-15 15:17:57.839348063 +0200
-+++ answers/084_interfaces.zig 2026-04-03 14:27:32.670756488 +0200
+--- exercises/084_interfaces.zig 2026-04-03 19:24:51.764327692 +0200
++++ answers/084_interfaces.zig 2026-04-03 19:27:31.552579474 +0200
@@ -106,7 +106,7 @@
for (my_insects) |insect| {
// Almost done! We want to print() each insect with a
From 366b597519e3aac3ead922f3925dab6fcbc99ff7 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 19:47:02 +0200
Subject: [PATCH 14/25] removed unnecessary patches
---
patches/patches/085_async2.patch | 14 --------------
patches/patches/086_async3.patch | 18 ------------------
patches/patches/087_async4.patch | 11 -----------
patches/patches/088_async5.patch | 11 -----------
patches/patches/089_async6.patch | 11 -----------
patches/patches/090_async7.patch | 13 -------------
patches/patches/091_async8.patch | 11 -----------
patches/patches/092_async9.patch | 11 -----------
patches/patches/093_async10.patch | 13 -------------
patches/patches/095_interfaces.patch | 11 -----------
10 files changed, 124 deletions(-)
delete mode 100644 patches/patches/085_async2.patch
delete mode 100644 patches/patches/086_async3.patch
delete mode 100644 patches/patches/087_async4.patch
delete mode 100644 patches/patches/088_async5.patch
delete mode 100644 patches/patches/089_async6.patch
delete mode 100644 patches/patches/090_async7.patch
delete mode 100644 patches/patches/091_async8.patch
delete mode 100644 patches/patches/092_async9.patch
delete mode 100644 patches/patches/093_async10.patch
delete mode 100644 patches/patches/095_interfaces.patch
diff --git a/patches/patches/085_async2.patch b/patches/patches/085_async2.patch
deleted file mode 100644
index 7583d7c..0000000
--- a/patches/patches/085_async2.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- exercises/085_async2.zig 2026-04-01 19:22:50.017227542 +0200
-+++ answers/085_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/086_async3.patch b/patches/patches/086_async3.patch
deleted file mode 100644
index 3a3c2c4..0000000
--- a/patches/patches/086_async3.patch
+++ /dev/null
@@ -1,18 +0,0 @@
---- exercises/086_async3.zig 2026-04-01 22:51:05.540094851 +0200
-+++ answers/086_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/087_async4.patch b/patches/patches/087_async4.patch
deleted file mode 100644
index 4883afd..0000000
--- a/patches/patches/087_async4.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/087_async4.zig 2026-04-01 23:17:31.066443941 +0200
-+++ answers/087_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/088_async5.patch b/patches/patches/088_async5.patch
deleted file mode 100644
index d88e820..0000000
--- a/patches/patches/088_async5.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/088_async5.zig 2026-04-01 23:40:40.505855238 +0200
-+++ answers/088_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/089_async6.patch b/patches/patches/089_async6.patch
deleted file mode 100644
index 4ba0b49..0000000
--- a/patches/patches/089_async6.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/089_async6.zig 2026-04-02 10:25:34.016616118 +0200
-+++ answers/089_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/090_async7.patch b/patches/patches/090_async7.patch
deleted file mode 100644
index 2108d51..0000000
--- a/patches/patches/090_async7.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- exercises/090_async7.zig 2026-04-02 10:36:42.910708919 +0200
-+++ answers/090_async7.zig 2026-04-02 10:36:51.965884223 +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/091_async8.patch b/patches/patches/091_async8.patch
deleted file mode 100644
index 865292f..0000000
--- a/patches/patches/091_async8.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/091_async8.zig 2026-04-02 10:49:27.925721496 +0200
-+++ answers/091_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/092_async9.patch b/patches/patches/092_async9.patch
deleted file mode 100644
index b7c8de9..0000000
--- a/patches/patches/092_async9.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/092_async9.zig 2026-04-03 13:44:50.526780809 +0200
-+++ answers/092_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/093_async10.patch b/patches/patches/093_async10.patch
deleted file mode 100644
index 69f7518..0000000
--- a/patches/patches/093_async10.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- exercises/093_async10.zig 2026-04-03 14:25:16.600025924 +0200
-+++ answers/093_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_interfaces.patch b/patches/patches/095_interfaces.patch
deleted file mode 100644
index cb5fd62..0000000
--- a/patches/patches/095_interfaces.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- exercises/095_interfaces.zig 2025-08-15 15:17:57.839348063 +0200
-+++ answers/095_interfaces.zig 2026-04-03 13:09:13.722917764 +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();
- }
- }
-
From 0206db8129e6dba87100958aa8d31e7c58628d2e Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 21:27:55 +0200
Subject: [PATCH 15/25] fixed missing uppercase letter
---
build.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.zig b/build.zig
index d3d91ee..3cddb61 100644
--- a/build.zig
+++ b/build.zig
@@ -1137,7 +1137,7 @@ const exercises = [_]Exercise{
},
.{
.main_file = "086_async2.zig",
- .output = "Computing... the answer is: 42",
+ .output = "Computing... The answer is: 42",
},
.{
.main_file = "087_async3.zig",
From 261c12d6a22f34c08249fad4cd81bcd90eafd02b Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Fri, 3 Apr 2026 21:30:49 +0200
Subject: [PATCH 16/25] fixed missing uppercase letter
---
patches/patches/086_async2.patch | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/patches/patches/086_async2.patch b/patches/patches/086_async2.patch
index 7506a69..9a672a6 100644
--- a/patches/patches/086_async2.patch
+++ b/patches/patches/086_async2.patch
@@ -1,14 +1,11 @@
---- 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 @@
+--- exercises/086_async2.zig 2026-04-03 19:42:15.274532915 +0200
++++ answers/086_async2.zig 2026-04-03 21:30:18.180019206 +0200
+@@ -38,7 +38,7 @@
// 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});
+ std.debug.print("The answer is: {}\n", .{answer});
}
-
- fn computeAnswer(a: u32, b: u32) u32 {
From 58f8df66d57fec4b0d8d69df7ac26624194b86ad Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Sat, 4 Apr 2026 16:05:35 +0200
Subject: [PATCH 17/25] improvements for async-io
---
exercises/085_async.zig | 9 +++++----
patches/patches/085_async.patch | 6 +++---
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/exercises/085_async.zig b/exercises/085_async.zig
index 48bda2b..1d885a5 100644
--- a/exercises/085_async.zig
+++ b/exercises/085_async.zig
@@ -7,10 +7,11 @@
// 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
+// * Threaded - thread-pool based I/O
+// * Evented - chooses the best event-loop backend for your OS:
+// * Uring on Linux (io_uring)
+// * Kqueue on BSD/macOS
+// * Dispatch on macOS (Grand Central Dispatch)
//
// The Io struct itself is tiny:
//
diff --git a/patches/patches/085_async.patch b/patches/patches/085_async.patch
index ca8b102..108eae1 100644
--- a/patches/patches/085_async.patch
+++ b/patches/patches/085_async.patch
@@ -1,6 +1,6 @@
---- 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 @@
+--- exercises/085_async.zig 2026-04-04 16:01:01.509555724 +0200
++++ answers/085_async.zig 2026-04-04 16:00:58.541495688 +0200
+@@ -38,7 +38,7 @@
const std = @import("std");
pub fn main(init: std.process.Init) !void {
From 2acf19277544ded4f56b525240be856094085009 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Sun, 5 Apr 2026 12:42:42 +0200
Subject: [PATCH 18/25] improvements for async-io
---
exercises/086_async2.zig | 32 +++++++++++++++++++-------------
patches/patches/086_async2.patch | 10 +++++-----
2 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/exercises/086_async2.zig b/exercises/086_async2.zig
index 1f1c4c8..cf376e2 100644
--- a/exercises/086_async2.zig
+++ b/exercises/086_async2.zig
@@ -6,41 +6,47 @@
// 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 abstraction.
//
-// io.async() returns a Future(T) where T is the return type
-// of the function you passed in. Future has two key methods:
+// IMPORTANT: Every Future MUST be either .await()ed or .cancel()ed.
+// Failing to do so leaks resources! A safe pattern is:
//
-// .await(io) - block until the result is ready, return it
-// .cancel(io) - request cancellation, then return the result
+// var future = io.async(myFn, .{});
+// defer _ = future.cancel(io); // safety net
+// // ... later, if we want the result:
+// const result = future.await(io);
+// // (await after cancel is fine — it just returns the result)
+//
+// Both .await() and .cancel() block until the task finishes and
+// return the result. The difference is that .cancel() also
+// requests the task to stop at its next cancellation point.
+// Calling either one more than once is safe — subsequent calls
+// just return a copy of the result.
//
// Fix this program so that computeAnswer runs asynchronously
// and its result is properly awaited.
//
const std = @import("std");
+const print = std.debug.print;
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 });
+ defer _ = future.cancel(io); // always clean up!
- // Meanwhile, print something to show we're not blocked.
- std.debug.print("Computing... ", .{});
+ print("Computing... ", .{});
// Now collect the result. What method on Future gives us
- // the value, blocking if it isn't ready yet?
+ // the value, blocking until it's ready?
const answer = future.???(io);
- std.debug.print("The answer is: {}\n", .{answer});
+ print("The answer is: {}\n", .{answer});
}
fn computeAnswer(a: u32, b: u32) u32 {
diff --git a/patches/patches/086_async2.patch b/patches/patches/086_async2.patch
index 9a672a6..1738089 100644
--- a/patches/patches/086_async2.patch
+++ b/patches/patches/086_async2.patch
@@ -1,11 +1,11 @@
---- exercises/086_async2.zig 2026-04-03 19:42:15.274532915 +0200
-+++ answers/086_async2.zig 2026-04-03 21:30:18.180019206 +0200
-@@ -38,7 +38,7 @@
+--- exercises/086_async2.zig 2026-04-05 12:41:11.350626443 +0200
++++ answers/086_async2.zig 2026-04-05 12:42:00.879791167 +0200
+@@ -44,7 +44,7 @@
// Now collect the result. What method on Future gives us
- // the value, blocking if it isn't ready yet?
+ // the value, blocking until it's ready?
- const answer = future.???(io);
+ const answer = future.await(io);
- std.debug.print("The answer is: {}\n", .{answer});
+ print("The answer is: {}\n", .{answer});
}
From 5e474ea5d1483cdca15c3b29295f0886950e4d94 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Sun, 5 Apr 2026 16:13:42 +0200
Subject: [PATCH 19/25] improvements for async-io
---
exercises/087_async3.zig | 23 +++++++++++------------
patches/patches/087_async3.patch | 19 ++++++-------------
2 files changed, 17 insertions(+), 25 deletions(-)
diff --git a/exercises/087_async3.zig b/exercises/087_async3.zig
index 07221e9..d10052c 100644
--- a/exercises/087_async3.zig
+++ b/exercises/087_async3.zig
@@ -5,20 +5,17 @@
// them all. The Io backend may run them concurrently:
//
// var f1 = io.async(taskA, .{});
+// defer _ = f1.cancel(io);
// var f2 = io.async(taskB, .{});
-//
-// // Both tasks may be running now!
+// defer _ = f2.cancel(io);
// 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.
+// Notice the defer pattern: each async call is immediately
+// followed by a defer cancel. This ensures cleanup even if
+// we return early or hit an error before reaching await.
+// Since await/cancel are idempotent, the defer is harmless
+// if we've already awaited.
//
// Fix this program to launch both tasks and collect their results.
//
@@ -29,12 +26,14 @@ 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_a = io.async(slowAdd, .{ 1, 2 });
+ defer _ = future_a.cancel(io);
var future_b = ???(slowMul, .{ 6, 7 });
+ defer _ = future_b.cancel(io);
// 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/087_async3.patch b/patches/patches/087_async3.patch
index 8365e7a..91ba9af 100644
--- a/patches/patches/087_async3.patch
+++ b/patches/patches/087_async3.patch
@@ -1,18 +1,11 @@
---- 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;
-
+--- exercises/087_async3.zig 2026-04-05 16:12:48.317265515 +0200
++++ answers/087_async3.zig 2026-04-05 16:12:52.269343030 +0200
+@@ -28,7 +28,7 @@
// Launch both tasks asynchronously.
-- var future_a = io.async(slowAdd, .{ 10, 20 });
+ var future_a = io.async(slowAdd, .{ 1, 2 });
+ defer _ = future_a.cancel(io);
- var future_b = ???(slowMul, .{ 6, 7 });
-+ var future_a = io.async(slowAdd, .{ 1, 2 });
+ var future_b = io.async(slowMul, .{ 6, 7 });
+ defer _ = future_b.cancel(io);
// 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 });
From 446da3ce5a1d5ea12bffe5a8b12eaad94e8afeee Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 12:22:41 +0200
Subject: [PATCH 20/25] improvements for async-io
---
exercises/088_async4.zig | 4 ++--
patches/patches/088_async4.patch | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/exercises/088_async4.zig b/exercises/088_async4.zig
index 50829fc..8298ca1 100644
--- a/exercises/088_async4.zig
+++ b/exercises/088_async4.zig
@@ -14,7 +14,7 @@
// * 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.
+// then blocks until they all finish.
//
// Unlike Future, Group tasks don't return values to the caller.
// They're ideal for parallel work that communicates through
@@ -38,7 +38,7 @@ pub fn main(init: std.process.Init) !void {
// Wait for all tasks to finish.
// What Group method blocks until all tasks complete?
- try group.???
+ try group.???(io);
print("All tasks finished!\n", .{});
}
diff --git a/patches/patches/088_async4.patch b/patches/patches/088_async4.patch
index 1faf30e..6cf549f 100644
--- a/patches/patches/088_async4.patch
+++ b/patches/patches/088_async4.patch
@@ -1,10 +1,10 @@
---- exercises/088_async4.zig 2026-04-01 23:17:31.066443941 +0200
-+++ answers/088_async4.zig 2026-04-01 23:17:39.251612131 +0200
+--- exercises/088_async4.zig 2026-04-06 12:22:06.643385622 +0200
++++ answers/088_async4.zig 2026-04-06 12:22:11.820491035 +0200
@@ -38,7 +38,7 @@
// Wait for all tasks to finish.
// What Group method blocks until all tasks complete?
-- try group.???
+- try group.???(io);
+ try group.await(io);
print("All tasks finished!\n", .{});
From 63e506586fad9458c8a881d003f57942542f2cde Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 16:57:48 +0200
Subject: [PATCH 21/25] improvements for async-io
---
exercises/089_async5.zig | 12 +++++++++---
patches/patches/089_async5.patch | 8 ++++----
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/exercises/089_async5.zig b/exercises/089_async5.zig
index 4fb8d76..0c00c1f 100644
--- a/exercises/089_async5.zig
+++ b/exercises/089_async5.zig
@@ -5,7 +5,7 @@
// 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
+// 2. BLOCKS until the task actually finishes
// 3. Returns whatever result the task produced
//
// A "cancellation point" is any Io function that can return
@@ -13,7 +13,7 @@
//
// fn myTask(io: std.Io) u32 {
// io.sleep(...) catch |err| switch (err) {
-// error.Canceled => return 0, // handle gracefully
+// error.Canceled => return 0, // error handle
// };
// return 42;
// }
@@ -21,6 +21,11 @@
// This is fundamentally different from killing a thread -
// the task gets a chance to clean up and return a value!
//
+// Remember: both .await() and .cancel() block and return the
+// result. The only difference is that .cancel() also sends
+// the cancellation request. And both are idempotent — calling
+// either one again just returns the same result.
+//
// 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.
@@ -32,6 +37,7 @@ pub fn main(init: std.process.Init) !void {
const io = init.io;
var future = io.async(slowTask, .{io});
+ defer _ = future.cancel(io); // safety net
// Wait 1 second, then cancel instead of waiting the full 10.
io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {};
@@ -40,7 +46,7 @@ pub fn main(init: std.process.Init) !void {
// We don't want to wait 10 seconds!
// Which Future method requests cancellation AND returns the result?
- const result = ???;
+ const result = future.???(io);
print("Task returned: {}\n", .{result});
}
diff --git a/patches/patches/089_async5.patch b/patches/patches/089_async5.patch
index d2baa96..3cea4e9 100644
--- a/patches/patches/089_async5.patch
+++ b/patches/patches/089_async5.patch
@@ -1,10 +1,10 @@
---- 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 @@
+--- exercises/089_async5.zig 2026-04-06 14:38:54.443726849 +0200
++++ answers/089_async5.zig 2026-04-06 14:38:39.945438309 +0200
+@@ -46,7 +46,7 @@
// We don't want to wait 10 seconds!
// Which Future method requests cancellation AND returns the result?
-- const result = ???;
+- const result = future.???(io);
+ const result = future.cancel(io);
print("Task returned: {}\n", .{result});
From aeeb18931da3c8444fc85db4da43e72a0ad2a46a Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 18:50:57 +0200
Subject: [PATCH 22/25] improvements for async-io
---
exercises/090_async6.zig | 7 ++++++-
patches/patches/090_async6.patch | 8 ++++----
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/exercises/090_async6.zig b/exercises/090_async6.zig
index eab03c9..16fb75f 100644
--- a/exercises/090_async6.zig
+++ b/exercises/090_async6.zig
@@ -23,6 +23,11 @@
// }
// sel.cancelDiscard(); // cancel remaining, discard results
//
+// As with all async primitives: tasks spawned in a Select MUST
+// be cleaned up. Use sel.cancel() to get remaining results one
+// by one (for resource cleanup), or sel.cancelDiscard() if you
+// don't need them.
+//
// The buffer must be large enough for all tasks that might
// complete before you call cancelDiscard().
//
@@ -47,7 +52,7 @@ pub fn main(init: std.process.Init) !void {
// Wait for the first finisher.
// What Select method returns the first completed result?
- const winner = ???;
+ const winner = try sel.???();
switch (winner) {
.hare => |msg| print("Hare: {s}\n", .{msg}),
diff --git a/patches/patches/090_async6.patch b/patches/patches/090_async6.patch
index 5ac777b..6289708 100644
--- a/patches/patches/090_async6.patch
+++ b/patches/patches/090_async6.patch
@@ -1,10 +1,10 @@
---- 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 @@
+--- exercises/090_async6.zig 2026-04-06 18:49:37.232023422 +0200
++++ answers/090_async6.zig 2026-04-06 18:49:22.189720687 +0200
+@@ -52,7 +52,7 @@
// Wait for the first finisher.
// What Select method returns the first completed result?
-- const winner = ???;
+- const winner = try sel.???();
+ const winner = try sel.await();
switch (winner) {
From 09bae6a70e51fc1313297bc98c67c252e8c32d3e Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 19:30:56 +0200
Subject: [PATCH 23/25] improvements for async-io
---
build.zig | 6 ++---
exercises/093_async9.zig | 42 ++++++++++++++++++++------------
patches/patches/093_async9.patch | 14 +++++------
3 files changed, 37 insertions(+), 25 deletions(-)
diff --git a/build.zig b/build.zig
index 3cddb61..dcafe14 100644
--- a/build.zig
+++ b/build.zig
@@ -1180,9 +1180,9 @@ const exercises = [_]Exercise{
.{
.main_file = "093_async9.zig",
.output =
- \\Main thread continues...
- \\Computing on a separate thread!
- \\Main thread done waiting.
+ \\Computing concurrently!
+ \\Main continues...
+ \\Main done waiting.
\\Result: 123
, // pay attention to the comma
},
diff --git a/exercises/093_async9.zig b/exercises/093_async9.zig
index ad30dcf..4a41544 100644
--- a/exercises/093_async9.zig
+++ b/exercises/093_async9.zig
@@ -5,22 +5,29 @@
// 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.
+// * The function MAY run on a separate unit of concurrency,
+// or it may run immediately on the caller (synchronously).
+// * Never fails — if no concurrency 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).
+// * GUARANTEES a separate unit of concurrency.
// * Can fail with error.ConcurrencyUnavailable if resources
// are exhausted or the backend doesn't support it.
-// * Use when you NEED true parallelism.
+// * Use when you NEED the task to run independently of the
+// caller.
+//
+// What is a "unit of concurrency"? That depends on the backend!
+// The Threaded backend uses OS threads. But the Evented backends
+// (Uring, Kqueue, Dispatch) use M:N green threads / fibers,
+// which can provide concurrency even on a SINGLE OS thread.
+// Your code doesn't need to know the difference.
//
// Because concurrent() can fail, you must handle the error:
//
// var future = try io.concurrent(myFn, .{args});
+// defer _ = future.cancel(io);
// const result = future.await(io);
//
// Notice the 'try' — that's the key difference in usage!
@@ -33,25 +40,30 @@ 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?
+ // Launch with a guaranteed separate unit of concurrency.
+ // Which Io method guarantees this?
// (Hint: unlike io.async, this one can fail!)
var future = try io.???(compute, .{io});
+ defer _ = future.cancel(io);
- print("Main thread continues...\n", .{});
-
- // Wait 100 millisecond so the output order is deterministic.
+ // Note: All breaks in this excercise (using sleep)
+ // are only necessary for a deterministic result.
io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {};
- print("Main thread done waiting.\n", .{});
+ print("Main continues...\n", .{});
+
+ // Wait 1 second for the output order.
+ io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch {};
+
+ print("Main 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", .{});
+ print("Computing concurrently!\n", .{});
// Simulate some work.
- io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0;
+ io.sleep(std.Io.Duration.fromMilliseconds(400), .awake) catch return 0;
return 123;
}
diff --git a/patches/patches/093_async9.patch b/patches/patches/093_async9.patch
index f759921..ebdfce2 100644
--- a/patches/patches/093_async9.patch
+++ b/patches/patches/093_async9.patch
@@ -1,11 +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?
+--- exercises/093_async9.zig 2026-04-06 19:26:11.388025362 +0200
++++ answers/093_async9.zig 2026-04-06 19:18:36.242931688 +0200
+@@ -43,7 +43,7 @@
+ // Launch with a guaranteed separate unit of concurrency.
+ // Which Io method guarantees this?
// (Hint: unlike io.async, this one can fail!)
- var future = try io.???(compute, .{io});
+ var future = try io.concurrent(compute, .{io});
+ defer _ = future.cancel(io);
- print("Main thread continues...\n", .{});
-
+ // Note: All breaks in this excercise (using sleep)
From 55a4841b07ac5d20fdd9f272fdbdd1875a7b4431 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 19:38:19 +0200
Subject: [PATCH 24/25] improvements for async-io
---
exercises/094_async10.zig | 11 ++++++-----
patches/patches/094_async10.patch | 8 ++++----
2 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/exercises/094_async10.zig b/exercises/094_async10.zig
index 6ed229d..c561d37 100644
--- a/exercises/094_async10.zig
+++ b/exercises/094_async10.zig
@@ -1,5 +1,5 @@
//
-// In exercise 088, we learned that cancellation happens at
+// In exercise 089, we learned that cancellation happens at
// "cancellation points" — any Io function that can return
// error.Canceled.
//
@@ -11,7 +11,7 @@
//
// 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.
//
@@ -36,9 +36,10 @@ pub fn main(init: std.process.Init) !void {
const io = init.io;
var future = io.async(importantTask, .{io});
+ defer _ = future.cancel(io);
// Give the task time to start and enter its critical section.
- io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {};
+ io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch {};
// Cancel while the task is in its protected section.
const result = future.cancel(io);
@@ -50,12 +51,12 @@ fn importantTask(io: std.Io) []const u8 {
// Protect this section from cancellation.
// What method swaps the cancel protection state?
- const old = io.???(. blocked);
+ 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) {
+ io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch |err| switch (err) {
error.Canceled => {
// This should never happen while protected!
return "ERROR: canceled during critical section!";
diff --git a/patches/patches/094_async10.patch b/patches/patches/094_async10.patch
index ae0d26d..e721485 100644
--- a/patches/patches/094_async10.patch
+++ b/patches/patches/094_async10.patch
@@ -1,10 +1,10 @@
---- 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 @@
+--- exercises/094_async10.zig 2026-04-06 19:36:59.873966580 +0200
++++ answers/094_async10.zig 2026-04-06 19:37:12.416216872 +0200
+@@ -51,8 +51,8 @@
// Protect this section from cancellation.
// What method swaps the cancel protection state?
-- const old = io.???(. blocked);
+- const old = io.???(.blocked);
- defer _ = io.???(old);
+ const old = io.swapCancelProtection(.blocked);
+ defer _ = io.swapCancelProtection(old);
From 882c6aa0ab174f0d7a28a7804531611fcef98476 Mon Sep 17 00:00:00 2001
From: Chris Boesch
Date: Mon, 6 Apr 2026 19:57:32 +0200
Subject: [PATCH 25/25] improvements for async-io
---
exercises/095_quiz_async.zig | 60 ++++++++++++++--------------
patches/patches/095_quiz_async.patch | 52 +++++++++++++-----------
2 files changed, 59 insertions(+), 53 deletions(-)
diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig
index fb78e7b..b116fc9 100644
--- a/exercises/095_quiz_async.zig
+++ b/exercises/095_quiz_async.zig
@@ -16,16 +16,17 @@
// 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
+// 1. Three sensor tasks send exactly 3 readings each through
+// a Queue
+// 2. A collector task receives readings concurrently,
+// 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. *
+// * This quiz uses concepts from exercises 085-094. *
// * There are 6 bugs to fix — look for the ???s! *
// * *
// *************************************************************
@@ -50,8 +51,8 @@ const GardenWeather = struct {
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;
+ defer self.mutex.unlock(io);
switch (reading.sensor_type) {
.thermometer => self.temperature = reading.value,
@@ -70,39 +71,40 @@ pub fn main(init: std.process.Init) !void {
var reading_buf: [8]Reading = undefined;
var queue: std.Io.Queue(Reading) = .init(&reading_buf);
- // Sensor group: runs all three sensors to completion.
+ // The collector must run concurrently so it can process
+ // readings while the sensors are still sending.
+ // Start it FIRST to ensure its concurrency unit is reserved.
+ //
+ // Bug 2: The collector needs guaranteed concurrency.
+ // What method ensures a separate unit of concurrency?
+ // (Don't forget: it can fail!)
+ var collector_future = try io.???(collector, .{ io, &queue, &weather });
+ defer _ = collector_future.cancel(io);
+
+ // Sensor group: the sensors can use async — they just need
+ // to run, and async is more portable.
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 });
+ sensors.async(io, sensor, .{ io, &queue, .thermometer, 20 });
+ sensors.async(io, sensor, .{ io, &queue, .hygrometer, 60 });
+ sensors.async(io, sensor, .{ io, &queue, .anemometer, 10 });
// 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);
+ 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);
+ // Wait for the collector to drain the remaining queue.
+ _ = collector_future.await(io);
+ // _ = collector_future.???(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.
+ // Bug 5: Protect this section from cancellation.
// What Io method swaps the cancel protection state?
const old_protection = io.???(.blocked);
defer _ = io.???(old_protection);
@@ -125,7 +127,7 @@ fn sensor(
.value = base_value + @as(i32, @intCast(i)),
};
- // Bug 5: Send the reading into the queue.
+ // Bug 6: Send the reading into the queue.
// What Queue method sends a single element?
queue.???(io, reading) catch return;
}
@@ -163,8 +165,7 @@ fn printGardenReport(weather: *GardenWeather) void {
//
// 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
+// io.concurrent() - guaranteed unit of concurrency
// 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
@@ -180,7 +181,8 @@ fn printGardenReport(weather: *GardenWeather) void {
// 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!
+// so your code is portable across backends — whether Threaded
+// (OS thread pool), or Evented (M:N green threads / fibers
+// that can provide concurrency even on a single OS thread).
//
// Doctor Zoraptera approves.
diff --git a/patches/patches/095_quiz_async.patch b/patches/patches/095_quiz_async.patch
index dbaae07..e3d2a79 100644
--- a/patches/patches/095_quiz_async.patch
+++ b/patches/patches/095_quiz_async.patch
@@ -1,38 +1,42 @@
---- 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
+--- exercises/095_quiz_async.zig 2026-04-06 19:55:17.111817364 +0200
++++ answers/095_quiz_async.zig 2026-04-06 19:56:16.063974543 +0200
@@ -51,7 +51,7 @@
+ 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;
-+ defer self.mutex.unlock(io);
++ self.mutex.lock(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 });
+@@ -78,7 +78,7 @@
+ // Bug 2: The collector needs guaranteed concurrency.
+ // What method ensures a separate unit of concurrency?
+ // (Don't forget: it can fail!)
+- var collector_future = try io.???(collector, .{ io, &queue, &weather });
++ var collector_future = try io.concurrent(collector, .{ io, &queue, &weather });
+ defer _ = collector_future.cancel(io);
+
+ // Sensor group: the sensors can use async — they just need
+@@ -91,7 +91,7 @@
- // 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);
+- try sensors.???(io);
++ try sensors.await(io);
// All sensors done — close the queue so the collector knows
// there's no more data coming.
-@@ -104,8 +103,8 @@
+@@ -99,15 +99,14 @@
+
+ // Wait for the collector to drain the remaining queue.
+ _ = collector_future.await(io);
+- // _ = collector_future.???(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.
+ // Bug 5: Protect this section from cancellation.
// What Io method swaps the cancel protection state?
- const old_protection = io.???(.blocked);
- defer _ = io.???(old_protection);
@@ -41,9 +45,9 @@
printGardenReport(&weather);
}
-@@ -127,7 +126,7 @@
+@@ -129,7 +128,7 @@
- // Bug 5: Send the reading into the queue.
+ // Bug 6: Send the reading into the queue.
// What Queue method sends a single element?
- queue.???(io, reading) catch return;
+ queue.putOne(io, reading) catch return;