From 77d3b684cb467ad4e06c211518e2d2d1c7346ad1 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 22:28:37 +0200 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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