From 77d3b684cb467ad4e06c211518e2d2d1c7346ad1 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 22:28:37 +0200 Subject: revival of the async-io functions --- exercises/084_async.zig | 78 +++++++++++++++++++++--------------------------- exercises/085_async2.zig | 56 +++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 62 deletions(-) (limited to 'exercises') diff --git a/exercises/084_async.zig b/exercises/084_async.zig index 56c9969..48bda2b 100644 --- a/exercises/084_async.zig +++ b/exercises/084_async.zig @@ -1,58 +1,48 @@ // -// Six Facts: +// In previous versions of Zig, async/await used special keywords +// like 'suspend', 'resume', and 'async' that operated on stackframes +// directly. Those keywords no longer exist! // -// 1. The memory space allocated to your program for the -// invocation of a function and all of its data is called a -// "stack frame". +// Zig 0.16 replaced them with a unified I/O interface: std.Io. +// This interface uses a VTable pattern - a struct of function pointers - +// to abstract over different concurrency backends: // -// 2. The 'return' keyword "pops" the current function -// invocation's frame off of the stack (it is no longer needed) -// and returns control to the place where the function was -// called. +// * Threaded - classic thread-pool based I/O +// * Uring - Linux io_uring +// * Kqueue - BSD/macOS +// * Dispatch - macOS Grand Central Dispatch // -// fn foo() void { -// return; // Pop the frame and return control -// } -// -// 3. Like 'return', the 'suspend' keyword returns control to the -// place where the function was called BUT the function -// invocation's frame remains so that it can regain control again -// at a later time. Functions which do this are "async" -// functions. -// -// fn fooThatSuspends() void { -// suspend {} // return control, but leave the frame alone -// } +// The Io struct itself is tiny: // -// 4. To call any function in async context and get a reference -// to its frame for later use, use the 'async' keyword: +// const Io = struct { +// userdata: ?*anyopaque, // opaque state of the backend +// vtable: *const VTable, // table of function pointers +// }; // -// var foo_frame = async fooThatSuspends(); +// Your code receives an Io value and calls methods on it. +// The backend is chosen at initialization time - your code doesn't +// need to know which one it is! // -// 5. If you call an async function without the 'async' keyword, -// the function FROM WHICH you called the async function itself -// becomes async! In this example, the bar() function is now -// async because it calls fooThatSuspends(), which is async. +// In Zig 0.16, main() receives a std.process.Init struct to opt +// into I/O and concurrency support: // -// fn bar() void { -// fooThatSuspends(); +// pub fn main(init: std.process.Init) !void { +// const io = init.io; +// // ... use io ... // } // -// 6. The main() function cannot be async! +// Let's start simple. Fix the main function to extract the Io +// interface from init, then use it to get the current time. // -// Given facts 3 and 4, how do we fix this program (broken by facts -// 5 and 6)? -// -const print = @import("std").debug.print; +const std = @import("std"); -pub fn main() void { - // Additional Hint: you can assign things to '_' when you - // don't intend to do anything with them. - foo(); -} +pub fn main(init: std.process.Init) !void { + const io = init.???; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). + const timestamp = std.Io.Timestamp.now(io, .real); -fn foo() void { - print("foo() A\n", .{}); - suspend {} - print("foo() B\n", .{}); + // Print the timestamp in seconds since the Unix epoch. + std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); } 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(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); -pub fn main() void { - var foo_frame = async foo(); + 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; } -- cgit v1.2.3 From db1fef8b864f1af6f9d2b8a7871e72d77747304d Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 22:52:04 +0200 Subject: revival of the async-io functions --- build.zig | 11 ++++---- exercises/086_async3.zig | 59 +++++++++++++++++++++++++++------------- patches/patches/086_async3.patch | 18 ++++++++++++ 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 patches/patches/086_async3.patch (limited to 'exercises') 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(init: std.process.Init) !void { + const io = init.io; -pub fn main() void { - const n = 5; - var foo_frame = async foo(n); + // Launch both tasks asynchronously. + var future_a = io.async(slowAdd, .{ 10, 20 }); + var future_b = ???(slowMul, .{ 6, 7 }); - ??? + // Await both results. + const sum = future_a.await(io); + const product = future_b.???(io); - print("\n", .{}); + print("{} + {} = {}\n", .{ 1, 2, sum }); + print("{} * {} = {}\n", .{ 6, 7, product }); + print("Total: {}\n", .{sum + product}); } -fn foo(countdown: u32) void { - var current = countdown; +fn slowAdd(a: u32, b: u32) u32 { + return a + b; +} - while (current > 0) { - print("{} ", .{current}); - current -= 1; - suspend {} - } +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 }); -- cgit v1.2.3 From fcfb0e80a039d8283f6a298ed67a26c23e3a560a Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 23:34:16 +0200 Subject: 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 (limited to 'exercises') 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; + +pub fn main(init: std.process.Init) !void { + const io = init.io; -var global_counter: i32 = 0; + var group: std.Io.Group = .init; -pub fn main() void { - var foo_frame = async foo(); + // 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 }); - while (global_counter <= 5) { - print("{} ", .{global_counter}); - ??? - } + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? + try group.??? - print("\n", .{}); + 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 -- cgit v1.2.3 From e22748d48899d93c6af2684b33688c91c239596c Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Wed, 1 Apr 2026 23:44:24 +0200 Subject: revival of the async-io functions --- README.md | 2 +- build.zig | 9 +++-- exercises/088_async5.zig | 87 +++++++++++++++++++++++----------------- patches/patches/088_async5.patch | 11 +++++ 4 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 patches/patches/088_async5.patch (limited to 'exercises') 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. -// -// So how do we REALLY get return values from async functions? -// -// The 'await' keyword waits for an async function to complete -// and then captures its return value. -// -// fn foo() u32 { -// return 5; +// One of the most important features of the new Io system is +// structured cancellation! +// +// Every Future has a .cancel() method that: +// 1. Requests the task to stop (via error.Canceled at the +// next "cancellation point") +// 2. Waits for the task to actually finish +// 3. Returns whatever result the task produced +// +// A "cancellation point" is any Io function that can return +// error.Canceled - most commonly io.sleep(): +// +// fn myTask(io: std.Io) u32 { +// io.sleep(...) catch |err| switch (err) { +// error.Canceled => return 0, // handle gracefully +// }; +// return 42; // } // -// var foo_frame = async foo(); // invoke and get frame -// var value = await foo_frame; // await result using frame -// -// 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. -// -// 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') +// This is fundamentally different from killing a thread - +// the task gets a chance to clean up and return a value! // -// Also notice that a 'suspend' keyword does NOT need to exist in -// a function to be called in an async context. +// 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. // -// 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(init: std.process.Init) !void { + const io = init.io; + + var future = io.async(slowTask, .{io}); -pub fn main() void { - var myframe = async getPageTitle("http://example.com"); + // Wait 1 second, then cancel instead of waiting the full 10. + io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; - var value = ??? + print("Canceling slow task...\n", .{}); - print("{s}\n", .{value}); + // 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}); + } -- cgit v1.2.3 From 3b22bfd898a5ca93df9e07a3f1816f3b0b08137b Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Thu, 2 Apr 2026 10:28:40 +0200 Subject: 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 (limited to 'exercises') 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; + +const RaceResult = std.Io.Select(union(enum) { + hare: []const u8, + tortoise: []const u8, +}); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var buffer: [2]RaceResult.Union = undefined; + var sel = RaceResult.init(io, &buffer); -pub fn main() void { - var com_frame = async getPageTitle("http://example.com"); - var org_frame = async getPageTitle("http://example.org"); + sel.async(.hare, runHare, .{io}); + sel.async(.tortoise, runTortoise, .{io}); - var com_title = com_frame; - var org_title = org_frame; + // 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(); +} - print(".com: {s}, .org: {s}.\n", .{ com_title, org_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 getPageTitle(url: []const u8) []const u8 { - // Please PRETEND this is actually making a network request. - _ = url; - return "Example Title"; +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}), -- cgit v1.2.3 From ffde357f303e7459a12cfe4b785ae9e8ef9ebe30 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Thu, 2 Apr 2026 10:38:45 +0200 Subject: 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 (limited to 'exercises') 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? - return 0xBEEF; + state.counter += 1; + } } -// -// 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; + } -- cgit v1.2.3 From e0259f43a726f61da14686de802021fcdb9aacd0 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 13:35:56 +0200 Subject: Insert space for additional async exercises --- build.zig | 62 ++-- exercises/090_async7.zig | 2 +- exercises/091_async8.zig | 67 ++-- exercises/092_interfaces.zig | 127 -------- exercises/093_hello_c.zig | 68 ---- exercises/094_c_math.zig | 41 --- exercises/095_for3.zig | 81 ----- exercises/095_interfaces.zig | 127 ++++++++ exercises/096_hello_c.zig | 68 ++++ exercises/096_memory_allocation.zig | 78 ----- exercises/097_bit_manipulation.zig | 95 ------ exercises/097_c_math.zig | 41 +++ exercises/098_bit_manipulation2.zig | 64 ---- exercises/098_for3.zig | 81 +++++ exercises/099_formatting.zig | 140 -------- exercises/099_memory_allocation.zig | 78 +++++ exercises/100_bit_manipulation.zig | 95 ++++++ exercises/100_for4.zig | 62 ---- exercises/101_bit_manipulation2.zig | 64 ++++ exercises/101_for5.zig | 126 -------- exercises/102_formatting.zig | 140 ++++++++ exercises/102_testing.zig | 96 ------ exercises/103_for4.zig | 62 ++++ exercises/103_tokenization.zig | 148 --------- exercises/104_for5.zig | 126 ++++++++ exercises/104_threading.zig | 129 -------- exercises/105_testing.zig | 96 ++++++ exercises/105_threading2.zig | 107 ------ exercises/106_files.zig | 102 ------ exercises/106_tokenization.zig | 148 +++++++++ exercises/107_files2.zig | 61 ---- exercises/107_threading.zig | 129 ++++++++ exercises/108_labeled_switch.zig | 79 ----- exercises/108_threading2.zig | 107 ++++++ exercises/109_files.zig | 102 ++++++ exercises/109_vectors.zig | 147 --------- exercises/110_files2.zig | 61 ++++ exercises/110_quiz9.zig | 484 ---------------------------- exercises/111_labeled_switch.zig | 79 +++++ exercises/111_packed.zig | 175 ---------- exercises/112_packed2.zig | 78 ----- exercises/112_vectors.zig | 147 +++++++++ exercises/113_quiz9.zig | 484 ++++++++++++++++++++++++++++ exercises/114_packed.zig | 175 ++++++++++ exercises/115_packed2.zig | 78 +++++ patches/patches/091_async8.patch | 11 + patches/patches/092_interfaces.patch | 11 - patches/patches/093_hello_c.patch | 11 - patches/patches/094_c_math.patch | 11 - patches/patches/095_for3.patch | 20 -- patches/patches/095_interfaces.patch | 11 + patches/patches/096_hello_c.patch | 11 + patches/patches/096_memory_allocation.patch | 11 - patches/patches/097_bit_manipulation.patch | 11 - patches/patches/097_c_math.patch | 11 + patches/patches/098_bit_manipulation2.patch | 9 - patches/patches/098_for3.patch | 20 ++ patches/patches/099_formatting.patch | 11 - patches/patches/099_memory_allocation.patch | 11 + patches/patches/100_bit_manipulation.patch | 11 + patches/patches/100_for4.patch | 11 - patches/patches/101_bit_manipulation2.patch | 9 + patches/patches/101_for5.patch | 11 - patches/patches/102_formatting.patch | 11 + patches/patches/102_testing.patch | 18 -- patches/patches/103_for4.patch | 11 + patches/patches/103_tokenization.patch | 11 - patches/patches/104_for5.patch | 11 + patches/patches/104_threading.patch | 17 - patches/patches/105_testing.patch | 18 ++ patches/patches/105_threading2.patch | 13 - patches/patches/106_files.patch | 20 -- patches/patches/106_tokenization.patch | 11 + patches/patches/107_files2.patch | 26 -- patches/patches/107_threading.patch | 17 + patches/patches/108_labeled_switch.patch | 18 -- patches/patches/108_threading2.patch | 13 + patches/patches/109_files.patch | 20 ++ patches/patches/109_vectors.patch | 13 - patches/patches/110_files2.patch | 26 ++ patches/patches/110_quiz9.patch | 56 ---- patches/patches/111_labeled_switch.patch | 18 ++ patches/patches/111_packed.patch | 57 ---- patches/patches/112_packed2.patch | 32 -- patches/patches/112_vectors.patch | 13 + patches/patches/113_quiz9.patch | 56 ++++ patches/patches/114_packed.patch | 57 ++++ patches/patches/115_packed2.patch | 32 ++ 88 files changed, 2982 insertions(+), 2932 deletions(-) delete mode 100644 exercises/092_interfaces.zig delete mode 100644 exercises/093_hello_c.zig delete mode 100644 exercises/094_c_math.zig delete mode 100644 exercises/095_for3.zig create mode 100644 exercises/095_interfaces.zig create mode 100644 exercises/096_hello_c.zig delete mode 100644 exercises/096_memory_allocation.zig delete mode 100644 exercises/097_bit_manipulation.zig create mode 100644 exercises/097_c_math.zig delete mode 100644 exercises/098_bit_manipulation2.zig create mode 100644 exercises/098_for3.zig delete mode 100644 exercises/099_formatting.zig create mode 100644 exercises/099_memory_allocation.zig create mode 100644 exercises/100_bit_manipulation.zig delete mode 100644 exercises/100_for4.zig create mode 100644 exercises/101_bit_manipulation2.zig delete mode 100644 exercises/101_for5.zig create mode 100644 exercises/102_formatting.zig delete mode 100644 exercises/102_testing.zig create mode 100644 exercises/103_for4.zig delete mode 100644 exercises/103_tokenization.zig create mode 100644 exercises/104_for5.zig delete mode 100644 exercises/104_threading.zig create mode 100644 exercises/105_testing.zig delete mode 100644 exercises/105_threading2.zig delete mode 100644 exercises/106_files.zig create mode 100644 exercises/106_tokenization.zig delete mode 100644 exercises/107_files2.zig create mode 100644 exercises/107_threading.zig delete mode 100644 exercises/108_labeled_switch.zig create mode 100644 exercises/108_threading2.zig create mode 100644 exercises/109_files.zig delete mode 100644 exercises/109_vectors.zig create mode 100644 exercises/110_files2.zig delete mode 100644 exercises/110_quiz9.zig create mode 100644 exercises/111_labeled_switch.zig delete mode 100644 exercises/111_packed.zig delete mode 100644 exercises/112_packed2.zig create mode 100644 exercises/112_vectors.zig create mode 100644 exercises/113_quiz9.zig create mode 100644 exercises/114_packed.zig create mode 100644 exercises/115_packed2.zig create mode 100644 patches/patches/091_async8.patch delete mode 100644 patches/patches/092_interfaces.patch delete mode 100644 patches/patches/093_hello_c.patch delete mode 100644 patches/patches/094_c_math.patch delete mode 100644 patches/patches/095_for3.patch create mode 100644 patches/patches/095_interfaces.patch create mode 100644 patches/patches/096_hello_c.patch delete mode 100644 patches/patches/096_memory_allocation.patch delete mode 100644 patches/patches/097_bit_manipulation.patch create mode 100644 patches/patches/097_c_math.patch delete mode 100644 patches/patches/098_bit_manipulation2.patch create mode 100644 patches/patches/098_for3.patch delete mode 100644 patches/patches/099_formatting.patch create mode 100644 patches/patches/099_memory_allocation.patch create mode 100644 patches/patches/100_bit_manipulation.patch delete mode 100644 patches/patches/100_for4.patch create mode 100644 patches/patches/101_bit_manipulation2.patch delete mode 100644 patches/patches/101_for5.patch create mode 100644 patches/patches/102_formatting.patch delete mode 100644 patches/patches/102_testing.patch create mode 100644 patches/patches/103_for4.patch delete mode 100644 patches/patches/103_tokenization.patch create mode 100644 patches/patches/104_for5.patch delete mode 100644 patches/patches/104_threading.patch create mode 100644 patches/patches/105_testing.patch delete mode 100644 patches/patches/105_threading2.patch delete mode 100644 patches/patches/106_files.patch create mode 100644 patches/patches/106_tokenization.patch delete mode 100644 patches/patches/107_files2.patch create mode 100644 patches/patches/107_threading.patch delete mode 100644 patches/patches/108_labeled_switch.patch create mode 100644 patches/patches/108_threading2.patch create mode 100644 patches/patches/109_files.patch delete mode 100644 patches/patches/109_vectors.patch create mode 100644 patches/patches/110_files2.patch delete mode 100644 patches/patches/110_quiz9.patch create mode 100644 patches/patches/111_labeled_switch.patch delete mode 100644 patches/patches/111_packed.patch delete mode 100644 patches/patches/112_packed2.patch create mode 100644 patches/patches/112_vectors.patch create mode 100644 patches/patches/113_quiz9.patch create mode 100644 patches/patches/114_packed.patch create mode 100644 patches/patches/115_packed2.patch (limited to 'exercises') 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", + .output = "Sum of 1..10 = 55", + }, + .{ + .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, - .skip_hint = "async has not been implemented in the current compiler version.", }, - .{ - .main_file = "092_interfaces.zig", + .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; } + // Signal that we're done sending. + queue.close(io); +} - print("X", .{}); +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/092_interfaces.zig deleted file mode 100644 index 7775dd5..0000000 --- a/exercises/092_interfaces.zig +++ /dev/null @@ -1,127 +0,0 @@ -// -// Remember our ant and bee simulator constructed with unions -// back in exercises 55 and 56? There, we demonstrated that -// unions allow us to treat different data types in a uniform -// manner. -// -// One neat feature was using tagged unions to create a single -// function to print a status for ants *or* bees by switching: -// -// switch (insect) { -// .still_alive => ... // (print ant stuff) -// .flowers_visited => ... // (print bee stuff) -// } -// -// Well, that simulation was running just fine until a new insect -// arrived in the virtual garden, a grasshopper! -// -// Doctor Zoraptera started to add grasshopper code to the -// program, but then she backed away from her keyboard with an -// angry hissing sound. She had realized that having code for -// each insect in one place and code to print each insect in -// another place was going to become unpleasant to maintain when -// the simulation expanded to hundreds of different insects. -// -// Thankfully, Zig has another comptime feature we can use -// to get out of this dilemma called the 'inline else'. -// -// We can replace this redundant code: -// -// switch (thing) { -// .a => |a| special(a), -// .b => |b| normal(b), -// .c => |c| normal(c), -// .d => |d| normal(d), -// .e => |e| normal(e), -// ... -// } -// -// With: -// -// switch (thing) { -// .a => |a| special(a), -// inline else => |t| normal(t), -// } -// -// We can have special handling of some cases and then Zig -// handles the rest of the matches for us. -// -// With this feature, you decide to make an Insect union with a -// single uniform 'print()' function. All of the insects can -// then be responsible for printing themselves. And Doctor -// Zoraptera can calm down and stop gnawing on the furniture. -// -const std = @import("std"); - -const Ant = struct { - still_alive: bool, - - pub fn print(self: Ant) void { - std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); - } -}; - -const Bee = struct { - flowers_visited: u16, - - pub fn print(self: Bee) void { - std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); - } -}; - -// Here's the new grasshopper. Notice how we've also added print -// methods to each insect. -const Grasshopper = struct { - distance_hopped: u16, - - pub fn print(self: Grasshopper) void { - std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); - } -}; - -const Insect = union(enum) { - ant: Ant, - bee: Bee, - grasshopper: Grasshopper, - - // Thanks to 'inline else', we can think of this print() as - // being an interface method. Any member of this union with - // a print() method can be treated uniformly by outside - // code without needing to know any other details. Cool! - pub fn print(self: Insect) void { - switch (self) { - inline else => |case| return case.print(), - } - } -}; - -pub fn main() !void { - const my_insects = [_]Insect{ - Insect{ .ant = Ant{ .still_alive = true } }, - Insect{ .bee = Bee{ .flowers_visited = 17 } }, - Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, - }; - - std.debug.print("Daily Insect Report:\n", .{}); - for (my_insects) |insect| { - // Almost done! We want to print() each insect with a - // single method call here. - ??? - } -} - -// Our print() method in the Insect union above demonstrates -// something very similar to the object-oriented concept of an -// abstract data type. That is, the Insect type doesn't contain -// the underlying data, and the print() function doesn't -// actually do the printing. -// -// The point of an interface is to support generic programming: -// the ability to treat different things as if they were the -// same to cut down on clutter and conceptual complexity. -// -// The Daily Insect Report doesn't need to worry about *which* -// insects are in the report - they all print the same way via -// the interface! -// -// Doctor Zoraptera loves it. diff --git a/exercises/093_hello_c.zig b/exercises/093_hello_c.zig deleted file mode 100644 index 182e0b0..0000000 --- a/exercises/093_hello_c.zig +++ /dev/null @@ -1,68 +0,0 @@ -// -// When Andrew Kelley announced the idea of a new programming language -// - namely Zig - in his blog on February 8, 2016, he also immediately -// stated his ambitious goal: to replace the C language! -// -// In order to be able to achieve this goal at all, Zig should be -// as compatible as possible with its "predecessor". -// Only if it is possible to exchange individual modules in existing -// C programs without having to use complicated wrappers, -// the undertaking has a chance of success. -// -// So it is not surprising that calling C functions and vice versa -// is extremely "smooth". -// -// To call C functions in Zig, you only need to specify the library -// that contains said function. For this purpose there is a built-in -// function corresponding to the well-known @import(): -// -// @cImport() -// -// All required libraries can now be included in the usual Zig notation: -// -// const c = @cImport({ -// @cInclude("stdio.h"); -// @cInclude("..."); -// }); -// -// Now a function can be called via the (in this example) constant 'c': -// -// c.puts("Hello world!"); -// -// By the way, most C functions have return values in the form of an -// integer value. Errors can then be evaluated (return < 0) or other -// information can be obtained. For example, 'puts' returns the number -// of characters output. -// -// So that all this does not remain a dry theory now, let's just start -// and call a C function out of Zig. - -// our well-known "import" for Zig -const std = @import("std"); - -// and here the new import for C -const c = @cImport({ - @cInclude("unistd.h"); -}); - -pub fn main() void { - - // In order to output text that can be evaluated by the - // Zig Builder, we need to write it to the Error output. - // In Zig, we do this with "std.debug.print" and in C we can - // specify a file descriptor i.e. 2 for error console. - // - // In this exercise we use 'write' to output 17 chars, - // but something is still missing... - const c_res = write(2, "Hello C from Zig!", 17); - - // let's see what the result from C is: - std.debug.print(" - C result is {d} chars written.\n", .{c_res}); -} -// -// Something must be considered when compiling with C functions. -// Namely that the Zig compiler knows that it should include -// corresponding libraries. For this purpose we call the compiler -// with the parameter "lc" for such a program, -// e.g. "zig run -lc hello_c.zig". -// diff --git a/exercises/094_c_math.zig b/exercises/094_c_math.zig deleted file mode 100644 index ec59a86..0000000 --- a/exercises/094_c_math.zig +++ /dev/null @@ -1,41 +0,0 @@ -// -// Often, C functions are used where no equivalent Zig function exists -// yet. Okay, that's getting less and less. ;-) -// -// Since the integration of a C function is very simple, as already -// seen in the last exercise, it naturally offers itself to use the -// very large variety of C functions for our own programs. -// As an example: -// -// Let's say we have a given angle of 765.2 degrees. If we want to -// normalize that, it means that we have to subtract X * 360 degrees -// to get the correct angle. -// How could we do that? A good method is to use the modulo function. -// But if we write "765.2 % 360", it only works with float values -// that are known at compile time. -// In Zig, we would use @mod(a, b) instead. -// -// Let us now assume that we cannot do this in Zig, but only with -// a C function from the standard library. In the library "math", -// there is a function called "fmod"; the "f" stands for floating -// and means that we can solve modulo for real numbers. With this -// function, it should be possible to normalize our angle. -// Let's go. - -const std = @import("std"); - -const c = @cImport({ - // What do we need here? - ??? -}); - -pub fn main() !void { - const angle = 765.2; - const circle = 360; - - // Here we call the C function 'fmod' to get our normalized angle. - const result = c.fmod(angle, circle); - - // We use formatters for the desired precision and to truncate the decimal places - std.debug.print("The normalized angle of {d: >3.1} degrees is {d: >3.1} degrees.\n", .{ angle, result }); -} diff --git a/exercises/095_for3.zig b/exercises/095_for3.zig deleted file mode 100644 index 77a1b56..0000000 --- a/exercises/095_for3.zig +++ /dev/null @@ -1,81 +0,0 @@ -// -// The Zig language is in rapid development and continuously -// improves the language constructs. Ziglings evolves with it. -// -// Until version 0.11, Zig's 'for' loops did not directly -// replicate the functionality of the C-style: "for(a;b;c)" -// which are so well suited for iterating over a numeric -// sequence. -// -// Instead, 'while' loops with counters clumsily stood in their -// place: -// -// var i: usize = 0; -// while (i < 10) : (i += 1) { -// // Here variable 'i' will have each value 0 to 9. -// } -// -// But here we are in the glorious future and Zig's 'for' loops -// can now take this form: -// -// for (0..10) |i| { -// // Here variable 'i' will have each value 0 to 9. -// } -// -// The key to understanding this example is to know that '0..9' -// uses the new range syntax: -// -// 0..10 is a range from 0 to 9 -// 1..4 is a range from 1 to 3 -// -// Crucially, the end value is EXCLUSIVE. -// -// At the moment, ranges in loops are only supported in 'for' loops. -// -// Perhaps you recall Exercise 13? We were printing a numeric -// sequence like so: -// -// var n: u32 = 1; -// -// // I want to print every number between 1 and 20 that is NOT -// // divisible by 3 or 5. -// while (n <= 20) : (n += 1) { -// // The '%' symbol is the "modulo" operator and it -// // returns the remainder after division. -// if (n % 3 == 0) continue; -// if (n % 5 == 0) continue; -// std.debug.print("{} ", .{n}); -// } -// -// Let's try out the new form of 'for' to re-implement that -// exercise: -// -const std = @import("std"); - -pub fn main() void { - - // I want to print every number between 1 and 20 that is NOT - // divisible by 3 or 5. - for (???) |n| { - - // The '%' symbol is the "modulo" operator and it - // returns the remainder after division. - if (n % 3 == 0) continue; - if (n % 5 == 0) continue; - std.debug.print("{} ", .{n}); - } - - std.debug.print("\n", .{}); - - // Let's also print every number from 1 through 15 - for (???) |n| { - std.debug.print("{} ", .{n}); - } - std.debug.print("\n", .{}); -} -// -// That's a bit nicer, right? -// -// Of course, both 'while' and 'for' have different advantages. -// Exercises 11, 12, and 14 would NOT be simplified by switching -// a 'while' for a 'for'. diff --git a/exercises/095_interfaces.zig b/exercises/095_interfaces.zig new file mode 100644 index 0000000..7775dd5 --- /dev/null +++ b/exercises/095_interfaces.zig @@ -0,0 +1,127 @@ +// +// Remember our ant and bee simulator constructed with unions +// back in exercises 55 and 56? There, we demonstrated that +// unions allow us to treat different data types in a uniform +// manner. +// +// One neat feature was using tagged unions to create a single +// function to print a status for ants *or* bees by switching: +// +// switch (insect) { +// .still_alive => ... // (print ant stuff) +// .flowers_visited => ... // (print bee stuff) +// } +// +// Well, that simulation was running just fine until a new insect +// arrived in the virtual garden, a grasshopper! +// +// Doctor Zoraptera started to add grasshopper code to the +// program, but then she backed away from her keyboard with an +// angry hissing sound. She had realized that having code for +// each insect in one place and code to print each insect in +// another place was going to become unpleasant to maintain when +// the simulation expanded to hundreds of different insects. +// +// Thankfully, Zig has another comptime feature we can use +// to get out of this dilemma called the 'inline else'. +// +// We can replace this redundant code: +// +// switch (thing) { +// .a => |a| special(a), +// .b => |b| normal(b), +// .c => |c| normal(c), +// .d => |d| normal(d), +// .e => |e| normal(e), +// ... +// } +// +// With: +// +// switch (thing) { +// .a => |a| special(a), +// inline else => |t| normal(t), +// } +// +// We can have special handling of some cases and then Zig +// handles the rest of the matches for us. +// +// With this feature, you decide to make an Insect union with a +// single uniform 'print()' function. All of the insects can +// then be responsible for printing themselves. And Doctor +// Zoraptera can calm down and stop gnawing on the furniture. +// +const std = @import("std"); + +const Ant = struct { + still_alive: bool, + + pub fn print(self: Ant) void { + std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); + } +}; + +const Bee = struct { + flowers_visited: u16, + + pub fn print(self: Bee) void { + std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); + } +}; + +// Here's the new grasshopper. Notice how we've also added print +// methods to each insect. +const Grasshopper = struct { + distance_hopped: u16, + + pub fn print(self: Grasshopper) void { + std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); + } +}; + +const Insect = union(enum) { + ant: Ant, + bee: Bee, + grasshopper: Grasshopper, + + // Thanks to 'inline else', we can think of this print() as + // being an interface method. Any member of this union with + // a print() method can be treated uniformly by outside + // code without needing to know any other details. Cool! + pub fn print(self: Insect) void { + switch (self) { + inline else => |case| return case.print(), + } + } +}; + +pub fn main() !void { + const my_insects = [_]Insect{ + Insect{ .ant = Ant{ .still_alive = true } }, + Insect{ .bee = Bee{ .flowers_visited = 17 } }, + Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, + }; + + std.debug.print("Daily Insect Report:\n", .{}); + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. + ??? + } +} + +// Our print() method in the Insect union above demonstrates +// something very similar to the object-oriented concept of an +// abstract data type. That is, the Insect type doesn't contain +// the underlying data, and the print() function doesn't +// actually do the printing. +// +// The point of an interface is to support generic programming: +// the ability to treat different things as if they were the +// same to cut down on clutter and conceptual complexity. +// +// The Daily Insect Report doesn't need to worry about *which* +// insects are in the report - they all print the same way via +// the interface! +// +// Doctor Zoraptera loves it. diff --git a/exercises/096_hello_c.zig b/exercises/096_hello_c.zig new file mode 100644 index 0000000..182e0b0 --- /dev/null +++ b/exercises/096_hello_c.zig @@ -0,0 +1,68 @@ +// +// When Andrew Kelley announced the idea of a new programming language +// - namely Zig - in his blog on February 8, 2016, he also immediately +// stated his ambitious goal: to replace the C language! +// +// In order to be able to achieve this goal at all, Zig should be +// as compatible as possible with its "predecessor". +// Only if it is possible to exchange individual modules in existing +// C programs without having to use complicated wrappers, +// the undertaking has a chance of success. +// +// So it is not surprising that calling C functions and vice versa +// is extremely "smooth". +// +// To call C functions in Zig, you only need to specify the library +// that contains said function. For this purpose there is a built-in +// function corresponding to the well-known @import(): +// +// @cImport() +// +// All required libraries can now be included in the usual Zig notation: +// +// const c = @cImport({ +// @cInclude("stdio.h"); +// @cInclude("..."); +// }); +// +// Now a function can be called via the (in this example) constant 'c': +// +// c.puts("Hello world!"); +// +// By the way, most C functions have return values in the form of an +// integer value. Errors can then be evaluated (return < 0) or other +// information can be obtained. For example, 'puts' returns the number +// of characters output. +// +// So that all this does not remain a dry theory now, let's just start +// and call a C function out of Zig. + +// our well-known "import" for Zig +const std = @import("std"); + +// and here the new import for C +const c = @cImport({ + @cInclude("unistd.h"); +}); + +pub fn main() void { + + // In order to output text that can be evaluated by the + // Zig Builder, we need to write it to the Error output. + // In Zig, we do this with "std.debug.print" and in C we can + // specify a file descriptor i.e. 2 for error console. + // + // In this exercise we use 'write' to output 17 chars, + // but something is still missing... + const c_res = write(2, "Hello C from Zig!", 17); + + // let's see what the result from C is: + std.debug.print(" - C result is {d} chars written.\n", .{c_res}); +} +// +// Something must be considered when compiling with C functions. +// Namely that the Zig compiler knows that it should include +// corresponding libraries. For this purpose we call the compiler +// with the parameter "lc" for such a program, +// e.g. "zig run -lc hello_c.zig". +// diff --git a/exercises/096_memory_allocation.zig b/exercises/096_memory_allocation.zig deleted file mode 100644 index 58de7b0..0000000 --- a/exercises/096_memory_allocation.zig +++ /dev/null @@ -1,78 +0,0 @@ -// -// In most of the examples so far, the inputs are known at compile -// time, thus the amount of memory used by the program is fixed. -// However, if responding to input whose size is not known at compile -// time, such as: -// - user input via command-line arguments -// - inputs from another program -// -// You'll need to request memory for your program to be allocated by -// your operating system at runtime. -// -// Zig provides several different allocators. In the Zig -// documentation, it recommends the Arena allocator for simple -// programs which allocate once and then exit: -// -// const std = @import("std"); -// -// // memory allocation can fail, so the return type is !void -// pub fn main() !void { -// -// var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); -// defer arena.deinit(); -// -// const allocator = arena.allocator(); -// -// const ptr = try allocator.create(i32); -// std.debug.print("ptr={*}\n", .{ptr}); -// -// const slice_ptr = try allocator.alloc(f64, 5); -// std.debug.print("slice_ptr={*}\n", .{slice_ptr}); -// } - -// Instead of a simple integer or a slice with a constant size, -// this program requires allocating a slice that is the same size -// as an input array. - -// Given a series of numbers, take the running average. In other -// words, each item N should contain the average of the last N -// elements. - -const std = @import("std"); - -fn runningAverage(arr: []const f64, avg: []f64) void { - var sum: f64 = 0; - - for (0.., arr) |index, val| { - sum += val; - const f_index: f64 = @floatFromInt(index + 1); - avg[index] = sum / f_index; - } -} - -pub fn main() !void { - // pretend this was defined by reading in user input - const arr: []const f64 = &[_]f64{ 0.3, 0.2, 0.1, 0.1, 0.4 }; - - // initialize the allocator - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - - // free the memory on exit - defer arena.deinit(); - - // initialize the allocator - const allocator = arena.allocator(); - - // allocate memory for this array - const avg: []f64 = ???; - - runningAverage(arr, avg); - std.debug.print("Running Average: ", .{}); - for (avg) |val| { - std.debug.print("{d:.2} ", .{val}); - } - std.debug.print("\n", .{}); -} - -// For more details on memory allocation and the different types of -// memory allocators, see https://www.youtube.com/watch?v=vHWiDx_l4V0 diff --git a/exercises/097_bit_manipulation.zig b/exercises/097_bit_manipulation.zig deleted file mode 100644 index 0e64ad7..0000000 --- a/exercises/097_bit_manipulation.zig +++ /dev/null @@ -1,95 +0,0 @@ -// -// Bit manipulation is a very powerful tool, also from Zig. -// Since the dawn of the computer age, numerous algorithms have been -// developed that solve tasks solely by moving, setting, or logically -// combining bits. -// -// Zig also uses direct bit manipulation in its standard library for -// functions where possible. And it is often possible with calculations -// based on integers. -// -// At first glance, it is often not easy to understand what exactly these -// algorithms do when only "numbers" in memory areas change outwardly. -// However, it should never be forgotten that the numbers only represent -// the interpretation of the bit sequences. -// -// Quasi the reversed case we have otherwise, namely that we represent -// numbers in bit sequences. -// -// We remember: 1 byte = 8 bits = 11111111 = 255 decimal = FF hex. -// -// Zig provides all the necessary functions to change the bits inside -// a variable. It is distinguished whether the bit change leads to an -// overflow or not. The details are in the Zig documentation in section -// "Table of Operators". -// -// Here are some examples of how the bits of variables can be changed: -// -// const numOne: u8 = 15; // = 0000 1111 -// const numTwo: u8 = 245; // = 1111 0101 -// -// const res1 = numOne >> 4; // = 0000 0000 - shift right -// const res2 = numOne << 4; // = 1111 0000 - shift left -// const res3 = numOne & numTwo; // = 0000 0101 - and -// const res4 = numOne | numTwo; // = 1111 1111 - or -// const res5 = numOne ^ numTwo; // = 1111 1010 - xor -// -// -// To familiarize ourselves with bit manipulation, we start with a simple -// but often underestimated function and then add other exercises in -// loose order. -// -// The following text contains excerpts from Wikipedia. -// -// Swap -// In computer programming, the act of swapping two variables refers to -// mutually exchanging the values of the variables. Usually, this is -// done with the data in memory. For example, in a program, two variables -// may be defined thus (in pseudocode): -// -// data_item x := 1 -// data_item y := 0 -// -// swap (x, y); -// -// After swap() is performed, x will contain the value 0 and y will -// contain 1; their values have been exchanged. The simplest and probably -// most widely used method to swap two variables is to use a third temporary -// variable: -// -// define swap (x, y) -// temp := x -// x := y -// y := temp -// -// However, with integers we can also achieve the swap function simply by -// bit manipulation. To do this, the variables are mutually linked with xor -// and the result is the same. - -const std = @import("std"); -const print = std.debug.print; - -pub fn main() !void { - - // Let us use 1101 and 1011 as values for x and y - var x: u8 = 0b1101; - var y: u8 = 0b1011; - - // Now we swap the values of the two variables by doing xor on them - x ^= y; - y ^= x; - - // What must be written here? - ???; - - print("x = {b}; y = {b}\n", .{ x, y }); -} - -// This variable swap takes advantage of the fact that the value resulting -// from the xor of two values contains both of these values. -// This circumstance was (and still is) sometimes used for encryption. -// Value XOR Key = Crypto. => Crypto XOR Key = Value. -// Since this can be swapped arbitrarily, you can swap two variables in this way. -// -// For Crypto it is better not to use this, but in sorting algorithms like -// Bubble Sort it works very well. diff --git a/exercises/097_c_math.zig b/exercises/097_c_math.zig new file mode 100644 index 0000000..ec59a86 --- /dev/null +++ b/exercises/097_c_math.zig @@ -0,0 +1,41 @@ +// +// Often, C functions are used where no equivalent Zig function exists +// yet. Okay, that's getting less and less. ;-) +// +// Since the integration of a C function is very simple, as already +// seen in the last exercise, it naturally offers itself to use the +// very large variety of C functions for our own programs. +// As an example: +// +// Let's say we have a given angle of 765.2 degrees. If we want to +// normalize that, it means that we have to subtract X * 360 degrees +// to get the correct angle. +// How could we do that? A good method is to use the modulo function. +// But if we write "765.2 % 360", it only works with float values +// that are known at compile time. +// In Zig, we would use @mod(a, b) instead. +// +// Let us now assume that we cannot do this in Zig, but only with +// a C function from the standard library. In the library "math", +// there is a function called "fmod"; the "f" stands for floating +// and means that we can solve modulo for real numbers. With this +// function, it should be possible to normalize our angle. +// Let's go. + +const std = @import("std"); + +const c = @cImport({ + // What do we need here? + ??? +}); + +pub fn main() !void { + const angle = 765.2; + const circle = 360; + + // Here we call the C function 'fmod' to get our normalized angle. + const result = c.fmod(angle, circle); + + // We use formatters for the desired precision and to truncate the decimal places + std.debug.print("The normalized angle of {d: >3.1} degrees is {d: >3.1} degrees.\n", .{ angle, result }); +} diff --git a/exercises/098_bit_manipulation2.zig b/exercises/098_bit_manipulation2.zig deleted file mode 100644 index 8b51265..0000000 --- a/exercises/098_bit_manipulation2.zig +++ /dev/null @@ -1,64 +0,0 @@ -// -// Another useful application for bit manipulation is setting bits as flags. -// This is especially useful when processing lists of something and storing -// the states of the entries, e.g. a list of numbers and for each prime -// number a flag is set. -// -// As an example, let's take the Pangram exercise from Exercism: -// https://exercism.org/tracks/zig/exercises/pangram -// -// A pangram is a sentence using every letter of the alphabet at least once. -// It is case insensitive, so it doesn't matter if a letter is lower-case -// or upper-case. The best known English pangram is: -// -// "The quick brown fox jumps over the lazy dog." -// -// There are several ways to select the letters that appear in the pangram -// (and it doesn't matter if they appear once or several times). -// -// For example, you could take an array of bool and set the value to 'true' -// for each letter in the order of the alphabet (a=0; b=1; etc.) found in -// the sentence. However, this is neither memory efficient nor particularly -// fast. Instead we choose a simpler approach that is very similar in principle: -// We define a variable with at least 26 bits (e.g. u32) and set the bit for -// each letter that is found in the corresponding position. -// -// Zig provides functions for this in the standard library, but we prefer to -// solve it without these extras, after all we want to learn something. -// -const std = @import("std"); -const ascii = std.ascii; -const print = std.debug.print; - -pub fn main() !void { - // let's check the pangram - print("Is this a pangram? {}!\n", .{isPangram("The quick brown fox jumps over the lazy dog.")}); -} - -fn isPangram(str: []const u8) bool { - // first we check if the string has at least 26 characters - if (str.len < 26) return false; - - // we use a 32 bit variable of which we need 26 bits - var bits: u32 = 0; - - // loop about all characters in the string - for (str) |c| { - // if the character is an alphabetical character - if (ascii.isAscii(c) and ascii.isAlphabetic(c)) { - // then we set the bit at the position - // - // to do this, we use a little trick: - // since the letters in the ASCII table start at 65 - // and are numbered sequentially, we simply subtract the - // first letter (in this case the 'a') from the character - // found, and thus get the position of the desired bit - bits |= @as(u32, 1) << @truncate(ascii.toLower(c) - 'a'); - } - } - // last we return the comparison if all 26 bits are set, - // and if so, we know the given string is a pangram - // - // but what do we have to compare? - return bits == 0x..???; -} diff --git a/exercises/098_for3.zig b/exercises/098_for3.zig new file mode 100644 index 0000000..77a1b56 --- /dev/null +++ b/exercises/098_for3.zig @@ -0,0 +1,81 @@ +// +// The Zig language is in rapid development and continuously +// improves the language constructs. Ziglings evolves with it. +// +// Until version 0.11, Zig's 'for' loops did not directly +// replicate the functionality of the C-style: "for(a;b;c)" +// which are so well suited for iterating over a numeric +// sequence. +// +// Instead, 'while' loops with counters clumsily stood in their +// place: +// +// var i: usize = 0; +// while (i < 10) : (i += 1) { +// // Here variable 'i' will have each value 0 to 9. +// } +// +// But here we are in the glorious future and Zig's 'for' loops +// can now take this form: +// +// for (0..10) |i| { +// // Here variable 'i' will have each value 0 to 9. +// } +// +// The key to understanding this example is to know that '0..9' +// uses the new range syntax: +// +// 0..10 is a range from 0 to 9 +// 1..4 is a range from 1 to 3 +// +// Crucially, the end value is EXCLUSIVE. +// +// At the moment, ranges in loops are only supported in 'for' loops. +// +// Perhaps you recall Exercise 13? We were printing a numeric +// sequence like so: +// +// var n: u32 = 1; +// +// // I want to print every number between 1 and 20 that is NOT +// // divisible by 3 or 5. +// while (n <= 20) : (n += 1) { +// // The '%' symbol is the "modulo" operator and it +// // returns the remainder after division. +// if (n % 3 == 0) continue; +// if (n % 5 == 0) continue; +// std.debug.print("{} ", .{n}); +// } +// +// Let's try out the new form of 'for' to re-implement that +// exercise: +// +const std = @import("std"); + +pub fn main() void { + + // I want to print every number between 1 and 20 that is NOT + // divisible by 3 or 5. + for (???) |n| { + + // The '%' symbol is the "modulo" operator and it + // returns the remainder after division. + if (n % 3 == 0) continue; + if (n % 5 == 0) continue; + std.debug.print("{} ", .{n}); + } + + std.debug.print("\n", .{}); + + // Let's also print every number from 1 through 15 + for (???) |n| { + std.debug.print("{} ", .{n}); + } + std.debug.print("\n", .{}); +} +// +// That's a bit nicer, right? +// +// Of course, both 'while' and 'for' have different advantages. +// Exercises 11, 12, and 14 would NOT be simplified by switching +// a 'while' for a 'for'. diff --git a/exercises/099_formatting.zig b/exercises/099_formatting.zig deleted file mode 100644 index be16978..0000000 --- a/exercises/099_formatting.zig +++ /dev/null @@ -1,140 +0,0 @@ -// -// Terminals have come a long way over the years. Starting with -// monochrome lines on flickering CRT monitors and continuously -// improving to today's modern terminal emulators with sharp -// images, true color, fonts, ligatures, and characters in every -// known language. -// -// Formatting our results to be appealing and allow quick visual -// comprehension of the information is what users desire. <3 -// -// C set string formatting standards over the years, and Zig is -// following suit and growing daily. Due to this growth, there is -// no official documentation for standard library features such -// as string formatting. -// -// Therefore, the comments for the format() function are the only -// way to definitively learn how to format strings in Zig: -// -// https://codeberg.org/ziglang/zig/src/branch/master/lib/std/Io/Writer.zig#L537 -// -// Zig already has a very nice selection of formatting options. -// These can be used in different ways, but generally to convert -// numerical values into various text representations. The results -// can be used for direct output to a terminal or stored for -// later use or written to a file. The latter is useful when -// large amounts of data are to be processed by other programs. -// -// In Ziglings, we are concerned with the output to the console. -// But since the formatting instructions for files are the same, -// what you learn applies universally. -// -// Since we write to "debug" output in Ziglings, our answers -// usually look something like this: -// -// print("Text {placeholder} another text \n", .{foo}); -// -// In addition to being replaced with foo in this example, the -// {placeholder} in the string can also have formatting applied. -// How does that work? -// -// This actually happens in several stages. In one stage, escape -// sequences are evaluated. The one we've seen the most -// (including the example above) is "\n" which means "line feed". -// Whenever this statement is found, a new line is started in the -// output. Escape sequences can also be written one after the -// other, e.g. "\n\n" will cause two line feeds. -// -// By the way, the result of these escape sequences is passed -// directly to the terminal program. Other than translating them -// into control codes, escape sequences have nothing to do with -// Zig. Zig knows nothing about "line feeds" or "tabs" or -// "bells". -// -// The formatting that Zig *does* perform itself is found in the -// curly brackets: "{placeholder}". Formatting instructions in -// the placeholder will determine how the corresponding value, -// e.g. foo, is displayed. -// -// And this is where it gets exciting, because format() accepts a -// variety of formatting instructions. It's basically a tiny -// language of its own. Here's a numeric example: -// -// print("Catch-0x{x:0>4}.", .{twenty_two}); -// -// This formatting instruction outputs a hexadecimal number with -// leading zeros: -// -// Catch-0x0016. -// -// Or you can center-align a string like so: -// -// print("{s:*^20}\n", .{"Hello!"}); -// -// Output: -// -// *******Hello!******* -// -// Let's try making use of some formatting. We've decided that -// the one thing missing from our lives is a multiplication table -// for all numbers from 1-15. We want the table to be nice and -// neat, with numbers in straight columns like so: -// -// X | 1 2 3 4 5 ... -// ---+---+---+---+---+---+ -// 1 | 1 2 3 4 5 -// -// 2 | 2 4 6 8 10 -// -// 3 | 3 6 9 12 15 -// -// 4 | 4 8 12 16 20 -// -// 5 | 5 10 15 20 25 -// -// ... -// -// Without string formatting, this would be a more challenging -// assignment because the number of digits in the numbers varies -// from 1 to 3. But formatting can help us with that. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main() !void { - // Max number to multiply - const size = 15; - - // Print the header: - // - // We start with a single 'X' for the diagonal. - print("\n X |", .{}); - - // Header row with all numbers from 1 to size. - for (0..size) |n| { - print("{d:>3} ", .{n + 1}); - } - print("\n", .{}); - - // Header column rule line. - var n: u8 = 0; - while (n <= size) : (n += 1) { - print("---+", .{}); - } - print("\n", .{}); - - // Now the actual table. (Is there anything more beautiful - // than a well-formatted table?) - for (0..size) |a| { - print("{d:>2} |", .{a + 1}); - - for (0..size) |b| { - // What formatting is needed here to make our columns - // nice and straight? - print("{???} ", .{(a + 1) * (b + 1)}); - } - - // After each row we use double line feed: - print("\n\n", .{}); - } -} diff --git a/exercises/099_memory_allocation.zig b/exercises/099_memory_allocation.zig new file mode 100644 index 0000000..58de7b0 --- /dev/null +++ b/exercises/099_memory_allocation.zig @@ -0,0 +1,78 @@ +// +// In most of the examples so far, the inputs are known at compile +// time, thus the amount of memory used by the program is fixed. +// However, if responding to input whose size is not known at compile +// time, such as: +// - user input via command-line arguments +// - inputs from another program +// +// You'll need to request memory for your program to be allocated by +// your operating system at runtime. +// +// Zig provides several different allocators. In the Zig +// documentation, it recommends the Arena allocator for simple +// programs which allocate once and then exit: +// +// const std = @import("std"); +// +// // memory allocation can fail, so the return type is !void +// pub fn main() !void { +// +// var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +// defer arena.deinit(); +// +// const allocator = arena.allocator(); +// +// const ptr = try allocator.create(i32); +// std.debug.print("ptr={*}\n", .{ptr}); +// +// const slice_ptr = try allocator.alloc(f64, 5); +// std.debug.print("slice_ptr={*}\n", .{slice_ptr}); +// } + +// Instead of a simple integer or a slice with a constant size, +// this program requires allocating a slice that is the same size +// as an input array. + +// Given a series of numbers, take the running average. In other +// words, each item N should contain the average of the last N +// elements. + +const std = @import("std"); + +fn runningAverage(arr: []const f64, avg: []f64) void { + var sum: f64 = 0; + + for (0.., arr) |index, val| { + sum += val; + const f_index: f64 = @floatFromInt(index + 1); + avg[index] = sum / f_index; + } +} + +pub fn main() !void { + // pretend this was defined by reading in user input + const arr: []const f64 = &[_]f64{ 0.3, 0.2, 0.1, 0.1, 0.4 }; + + // initialize the allocator + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + + // free the memory on exit + defer arena.deinit(); + + // initialize the allocator + const allocator = arena.allocator(); + + // allocate memory for this array + const avg: []f64 = ???; + + runningAverage(arr, avg); + std.debug.print("Running Average: ", .{}); + for (avg) |val| { + std.debug.print("{d:.2} ", .{val}); + } + std.debug.print("\n", .{}); +} + +// For more details on memory allocation and the different types of +// memory allocators, see https://www.youtube.com/watch?v=vHWiDx_l4V0 diff --git a/exercises/100_bit_manipulation.zig b/exercises/100_bit_manipulation.zig new file mode 100644 index 0000000..0e64ad7 --- /dev/null +++ b/exercises/100_bit_manipulation.zig @@ -0,0 +1,95 @@ +// +// Bit manipulation is a very powerful tool, also from Zig. +// Since the dawn of the computer age, numerous algorithms have been +// developed that solve tasks solely by moving, setting, or logically +// combining bits. +// +// Zig also uses direct bit manipulation in its standard library for +// functions where possible. And it is often possible with calculations +// based on integers. +// +// At first glance, it is often not easy to understand what exactly these +// algorithms do when only "numbers" in memory areas change outwardly. +// However, it should never be forgotten that the numbers only represent +// the interpretation of the bit sequences. +// +// Quasi the reversed case we have otherwise, namely that we represent +// numbers in bit sequences. +// +// We remember: 1 byte = 8 bits = 11111111 = 255 decimal = FF hex. +// +// Zig provides all the necessary functions to change the bits inside +// a variable. It is distinguished whether the bit change leads to an +// overflow or not. The details are in the Zig documentation in section +// "Table of Operators". +// +// Here are some examples of how the bits of variables can be changed: +// +// const numOne: u8 = 15; // = 0000 1111 +// const numTwo: u8 = 245; // = 1111 0101 +// +// const res1 = numOne >> 4; // = 0000 0000 - shift right +// const res2 = numOne << 4; // = 1111 0000 - shift left +// const res3 = numOne & numTwo; // = 0000 0101 - and +// const res4 = numOne | numTwo; // = 1111 1111 - or +// const res5 = numOne ^ numTwo; // = 1111 1010 - xor +// +// +// To familiarize ourselves with bit manipulation, we start with a simple +// but often underestimated function and then add other exercises in +// loose order. +// +// The following text contains excerpts from Wikipedia. +// +// Swap +// In computer programming, the act of swapping two variables refers to +// mutually exchanging the values of the variables. Usually, this is +// done with the data in memory. For example, in a program, two variables +// may be defined thus (in pseudocode): +// +// data_item x := 1 +// data_item y := 0 +// +// swap (x, y); +// +// After swap() is performed, x will contain the value 0 and y will +// contain 1; their values have been exchanged. The simplest and probably +// most widely used method to swap two variables is to use a third temporary +// variable: +// +// define swap (x, y) +// temp := x +// x := y +// y := temp +// +// However, with integers we can also achieve the swap function simply by +// bit manipulation. To do this, the variables are mutually linked with xor +// and the result is the same. + +const std = @import("std"); +const print = std.debug.print; + +pub fn main() !void { + + // Let us use 1101 and 1011 as values for x and y + var x: u8 = 0b1101; + var y: u8 = 0b1011; + + // Now we swap the values of the two variables by doing xor on them + x ^= y; + y ^= x; + + // What must be written here? + ???; + + print("x = {b}; y = {b}\n", .{ x, y }); +} + +// This variable swap takes advantage of the fact that the value resulting +// from the xor of two values contains both of these values. +// This circumstance was (and still is) sometimes used for encryption. +// Value XOR Key = Crypto. => Crypto XOR Key = Value. +// Since this can be swapped arbitrarily, you can swap two variables in this way. +// +// For Crypto it is better not to use this, but in sorting algorithms like +// Bubble Sort it works very well. diff --git a/exercises/100_for4.zig b/exercises/100_for4.zig deleted file mode 100644 index c8a1161..0000000 --- a/exercises/100_for4.zig +++ /dev/null @@ -1,62 +0,0 @@ -// -// We've seen that the 'for' loop can let us perform some action -// for every item in an array or slice. -// -// More recently, we discovered that it supports ranges to -// iterate over number sequences. -// -// This is part of a more general capability of the `for` loop: -// looping over one or more "objects" where an object is an -// array, slice, or range. -// -// In fact, we *did* use multiple objects way back in Exercise -// 016 where we iterated over an array and also a numeric index. -// It didn't always work exactly this way, so the exercise had to -// be retroactively modified a little bit. -// -// for (bits, 0..) |bit, i| { ... } -// -// The general form of a 'for' loop with two lists is: -// -// for (list_a, list_b) |a, b| { -// // Here we have the first item from list_a and list_b, -// // then the second item from each, then the third and -// // so forth... -// } -// -// What's really beautiful about this is that we don't have to -// keep track of an index or advancing a memory pointer for -// *either* of these lists. That error-prone stuff is all taken -// care of for us by the compiler. -// -// Below, we have a program that is supposed to compare two -// arrays. Please make it work! -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main() void { - const hex_nums = [_]u8{ 0xb, 0x2a, 0x77 }; - const dec_nums = [_]u8{ 11, 42, 119 }; - - for (hex_nums, ???) |hn, ???| { - if (hn != dn) { - print("Uh oh! Found a mismatch: {d} vs {d}\n", .{ hn, dn }); - return; - } - } - - print("Arrays match!\n", .{}); -} -// -// You are perhaps wondering what happens if one of the two lists -// is longer than the other? Try it! -// -// By the way, congratulations for making it to Exercise 100! -// -// +-------------+ -// | Celebration | -// | Area * * * | -// +-------------+ -// -// Please keep your celebrating within the area provided. diff --git a/exercises/101_bit_manipulation2.zig b/exercises/101_bit_manipulation2.zig new file mode 100644 index 0000000..8b51265 --- /dev/null +++ b/exercises/101_bit_manipulation2.zig @@ -0,0 +1,64 @@ +// +// Another useful application for bit manipulation is setting bits as flags. +// This is especially useful when processing lists of something and storing +// the states of the entries, e.g. a list of numbers and for each prime +// number a flag is set. +// +// As an example, let's take the Pangram exercise from Exercism: +// https://exercism.org/tracks/zig/exercises/pangram +// +// A pangram is a sentence using every letter of the alphabet at least once. +// It is case insensitive, so it doesn't matter if a letter is lower-case +// or upper-case. The best known English pangram is: +// +// "The quick brown fox jumps over the lazy dog." +// +// There are several ways to select the letters that appear in the pangram +// (and it doesn't matter if they appear once or several times). +// +// For example, you could take an array of bool and set the value to 'true' +// for each letter in the order of the alphabet (a=0; b=1; etc.) found in +// the sentence. However, this is neither memory efficient nor particularly +// fast. Instead we choose a simpler approach that is very similar in principle: +// We define a variable with at least 26 bits (e.g. u32) and set the bit for +// each letter that is found in the corresponding position. +// +// Zig provides functions for this in the standard library, but we prefer to +// solve it without these extras, after all we want to learn something. +// +const std = @import("std"); +const ascii = std.ascii; +const print = std.debug.print; + +pub fn main() !void { + // let's check the pangram + print("Is this a pangram? {}!\n", .{isPangram("The quick brown fox jumps over the lazy dog.")}); +} + +fn isPangram(str: []const u8) bool { + // first we check if the string has at least 26 characters + if (str.len < 26) return false; + + // we use a 32 bit variable of which we need 26 bits + var bits: u32 = 0; + + // loop about all characters in the string + for (str) |c| { + // if the character is an alphabetical character + if (ascii.isAscii(c) and ascii.isAlphabetic(c)) { + // then we set the bit at the position + // + // to do this, we use a little trick: + // since the letters in the ASCII table start at 65 + // and are numbered sequentially, we simply subtract the + // first letter (in this case the 'a') from the character + // found, and thus get the position of the desired bit + bits |= @as(u32, 1) << @truncate(ascii.toLower(c) - 'a'); + } + } + // last we return the comparison if all 26 bits are set, + // and if so, we know the given string is a pangram + // + // but what do we have to compare? + return bits == 0x..???; +} diff --git a/exercises/101_for5.zig b/exercises/101_for5.zig deleted file mode 100644 index 200e71d..0000000 --- a/exercises/101_for5.zig +++ /dev/null @@ -1,126 +0,0 @@ -// -// The 'for' loop is not just limited to looping over one or two -// items. Let's try an example with a whole bunch! -// -// But first, there's one last thing we've avoided mentioning -// until now: The special range that leaves off the last value: -// -// for ( things, 0.. ) |t, i| { ... } -// -// That's how we tell Zig that we want to get a numeric value for -// every item in "things", starting with 0. -// -// A nice feature of these index ranges is that you can have them -// start with any number you choose. The first value of "i" in -// this example will be 500, then 501, 502, etc.: -// -// for ( things, 500.. ) |t, i| { ... } -// -// Remember our RPG characters? They had the following -// properties, which we stored in a struct type: -// -// class -// gold -// experience -// -// What we're going to do now is store the same RPG character -// data, but in a separate array for each property. -// -// It might look a little awkward, but let's bear with it. -// -// We've started writing a program to print a numbered list of -// characters with each of their properties, but it needs a -// little help: -// -const std = @import("std"); -const print = std.debug.print; - -// This is the same character role enum we've seen before. -const Role = enum { - wizard, - thief, - bard, - warrior, -}; - -pub fn main() void { - // Here are the three "property" arrays: - const roles = [4]Role{ .wizard, .bard, .bard, .warrior }; - const gold = [4]u16{ 25, 11, 5, 7392 }; - const experience = [4]u8{ 40, 17, 55, 21 }; - - // We would like to number our list starting with 1, not 0. - // How do we do that? - for (roles, gold, experience, ???) |c, g, e, i| { - const role_name = switch (c) { - .wizard => "Wizard", - .thief => "Thief", - .bard => "Bard", - .warrior => "Warrior", - }; - - std.debug.print("{d}. {s} (Gold: {d}, XP: {d})\n", .{ - i, - role_name, - g, - e, - }); - } -} -// -// By the way, storing our character data in arrays like this -// isn't *just* a silly way to demonstrate multi-object 'for' -// loops. -// -// It's *also* a silly way to introduce a concept called -// "data-oriented design". -// -// Let's use a metaphor to build up an intuition for what this is -// all about: -// -// Let's say you've been tasked with grabbing three glass -// marbles, three spoons, and three feathers from a magic bag. -// But you can't use your hands to grab them. Instead, you must -// use a marble scoop, spoon magnet, and feather tongs to grab -// each type of object. -// -// Now, would you rather use the magic bag: -// -// A. Grouped the items in clusters so you have to pick up one -// marble, then one spoon, then one feather? -// -// OR -// -// B. Grouped the items by type so you can pick up all of the -// marbles at once, then all the spoons, then all of the -// feathers? -// -// If this metaphor is working, hopefully, it's clear that the 'B' -// option would be much more efficient. -// -// Well, it probably comes as little surprise that storing and -// using data in a sequential and uniform fashion is also more -// efficient for modern CPUs. -// -// Decades of OOP practices have steered people towards grouping -// different data types together into mixed-type "objects" with -// the intent that these are easier on the human mind. -// Data-oriented design groups data by type in a way that is -// easier on the computer. -// -// With clever language design, maybe we can have both. -// -// In the Zig community, you may see the difference in groupings -// presented with the terms "Array of Structs" (AoS) versus -// "Struct of Arrays" (SoA). -// -// To envision these two designs in action, imagine an array of -// RPG character structs, each containing three different data -// types (AoS) versus a single RPG character struct containing -// three arrays of one data type each, like those in the exercise -// above (SoA). -// -// For a more practical application of "data-oriented design" -// watch the following talk from Andrew Kelley, the creator of Zig: -// https://vimeo.com/649009599 -// diff --git a/exercises/102_formatting.zig b/exercises/102_formatting.zig new file mode 100644 index 0000000..be16978 --- /dev/null +++ b/exercises/102_formatting.zig @@ -0,0 +1,140 @@ +// +// Terminals have come a long way over the years. Starting with +// monochrome lines on flickering CRT monitors and continuously +// improving to today's modern terminal emulators with sharp +// images, true color, fonts, ligatures, and characters in every +// known language. +// +// Formatting our results to be appealing and allow quick visual +// comprehension of the information is what users desire. <3 +// +// C set string formatting standards over the years, and Zig is +// following suit and growing daily. Due to this growth, there is +// no official documentation for standard library features such +// as string formatting. +// +// Therefore, the comments for the format() function are the only +// way to definitively learn how to format strings in Zig: +// +// https://codeberg.org/ziglang/zig/src/branch/master/lib/std/Io/Writer.zig#L537 +// +// Zig already has a very nice selection of formatting options. +// These can be used in different ways, but generally to convert +// numerical values into various text representations. The results +// can be used for direct output to a terminal or stored for +// later use or written to a file. The latter is useful when +// large amounts of data are to be processed by other programs. +// +// In Ziglings, we are concerned with the output to the console. +// But since the formatting instructions for files are the same, +// what you learn applies universally. +// +// Since we write to "debug" output in Ziglings, our answers +// usually look something like this: +// +// print("Text {placeholder} another text \n", .{foo}); +// +// In addition to being replaced with foo in this example, the +// {placeholder} in the string can also have formatting applied. +// How does that work? +// +// This actually happens in several stages. In one stage, escape +// sequences are evaluated. The one we've seen the most +// (including the example above) is "\n" which means "line feed". +// Whenever this statement is found, a new line is started in the +// output. Escape sequences can also be written one after the +// other, e.g. "\n\n" will cause two line feeds. +// +// By the way, the result of these escape sequences is passed +// directly to the terminal program. Other than translating them +// into control codes, escape sequences have nothing to do with +// Zig. Zig knows nothing about "line feeds" or "tabs" or +// "bells". +// +// The formatting that Zig *does* perform itself is found in the +// curly brackets: "{placeholder}". Formatting instructions in +// the placeholder will determine how the corresponding value, +// e.g. foo, is displayed. +// +// And this is where it gets exciting, because format() accepts a +// variety of formatting instructions. It's basically a tiny +// language of its own. Here's a numeric example: +// +// print("Catch-0x{x:0>4}.", .{twenty_two}); +// +// This formatting instruction outputs a hexadecimal number with +// leading zeros: +// +// Catch-0x0016. +// +// Or you can center-align a string like so: +// +// print("{s:*^20}\n", .{"Hello!"}); +// +// Output: +// +// *******Hello!******* +// +// Let's try making use of some formatting. We've decided that +// the one thing missing from our lives is a multiplication table +// for all numbers from 1-15. We want the table to be nice and +// neat, with numbers in straight columns like so: +// +// X | 1 2 3 4 5 ... +// ---+---+---+---+---+---+ +// 1 | 1 2 3 4 5 +// +// 2 | 2 4 6 8 10 +// +// 3 | 3 6 9 12 15 +// +// 4 | 4 8 12 16 20 +// +// 5 | 5 10 15 20 25 +// +// ... +// +// Without string formatting, this would be a more challenging +// assignment because the number of digits in the numbers varies +// from 1 to 3. But formatting can help us with that. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main() !void { + // Max number to multiply + const size = 15; + + // Print the header: + // + // We start with a single 'X' for the diagonal. + print("\n X |", .{}); + + // Header row with all numbers from 1 to size. + for (0..size) |n| { + print("{d:>3} ", .{n + 1}); + } + print("\n", .{}); + + // Header column rule line. + var n: u8 = 0; + while (n <= size) : (n += 1) { + print("---+", .{}); + } + print("\n", .{}); + + // Now the actual table. (Is there anything more beautiful + // than a well-formatted table?) + for (0..size) |a| { + print("{d:>2} |", .{a + 1}); + + for (0..size) |b| { + // What formatting is needed here to make our columns + // nice and straight? + print("{???} ", .{(a + 1) * (b + 1)}); + } + + // After each row we use double line feed: + print("\n\n", .{}); + } +} diff --git a/exercises/102_testing.zig b/exercises/102_testing.zig deleted file mode 100644 index 0dec9b9..0000000 --- a/exercises/102_testing.zig +++ /dev/null @@ -1,96 +0,0 @@ -// -// A big advantage of Zig is the integration of its own test system. -// This allows the philosophy of Test Driven Development (TDD) to be -// implemented perfectly. Zig even goes one step further than other -// languages, the tests can be included directly in the source file. -// -// This has several advantages. On the one hand it is much clearer to -// have everything in one file, both the source code and the associated -// test code. On the other hand, it is much easier for third parties -// to understand what exactly a function is supposed to do if they can -// simply look at the test inside the source and compare both. -// -// Especially if you want to understand how e.g. the standard library -// of Zig works, this approach is very helpful. Furthermore it is very -// practical, if you want to report a bug to the Zig community, to -// illustrate it with a small example including a test. -// -// Therefore, in this exercise we will deal with the basics of testing -// in Zig. Basically, tests work as follows: you pass certain parameters -// to a function, for which you get a return - the result. This is then -// compared with the EXPECTED value. If both values match, the test is -// passed, otherwise an error message is displayed. -// -// testing.expect(foo(param1, param2) == expected); -// -// Also other comparisons are possible, deviations or also errors can -// be provoked, which must lead to an appropriate behavior of the -// function, so that the test is passed. -// -// Tests can be run via Zig build system or applied directly to -// individual modules using "zig test xyz.zig". -// -// Both can be used script-driven to execute tests automatically, e.g. -// after checking into a Git repository. Something we also make extensive -// use of here at Ziglings. -// -const std = @import("std"); -const testing = std.testing; - -// This is a simple function that builds a sum from the passed parameters and -// returns. -fn add(a: f16, b: f16) f16 { - return a + b; -} - -// The associated test. It always starts with the keyword "test", followed by a -// description of the tasks of the test. This is followed by the test cases in -// curly brackets. -test "add" { - - // The first test checks if the sum of '41' and '1' gives '42', which is - // correct. - try testing.expect(add(41, 1) == 42); - - // Another way to perform this test is as follows: - try testing.expectEqual(42, add(41, 1)); - - // This time a test with the addition of a negative number: - try testing.expect(add(5, -4) == 1); - - // And a floating point operation: - try testing.expect(add(1.5, 1.5) == 3); -} - -// Another simple function that returns the result of subtracting the two -// parameters. -fn sub(a: f16, b: f16) f16 { - return a - b; -} - -// The corresponding test is not much different from the previous one. Except -// that it contains an error that you need to correct. -test "sub" { - try testing.expect(sub(10, 5) == 6); - - try testing.expect(sub(3, 1.5) == 1.5); -} - -// This function divides the numerator by the denominator. Here it is important -// that the denominator must not be zero. This is checked and if it occurs an -// error is returned. -fn divide(a: f16, b: f16) !f16 { - if (b == 0) return error.DivisionByZero; - return a / b; -} - -test "divide" { - try testing.expect(divide(2, 2) catch unreachable == 1); - try testing.expect(divide(-1, -1) catch unreachable == 1); - try testing.expect(divide(10, 2) catch unreachable == 5); - try testing.expect(divide(1, 3) catch unreachable == 0.3333333333333333); - - // Now we test if the function returns an error if we pass a zero as - // denominator. But which error needs to be tested? - try testing.expectError(error.???, divide(15, 0)); -} diff --git a/exercises/103_for4.zig b/exercises/103_for4.zig new file mode 100644 index 0000000..c8a1161 --- /dev/null +++ b/exercises/103_for4.zig @@ -0,0 +1,62 @@ +// +// We've seen that the 'for' loop can let us perform some action +// for every item in an array or slice. +// +// More recently, we discovered that it supports ranges to +// iterate over number sequences. +// +// This is part of a more general capability of the `for` loop: +// looping over one or more "objects" where an object is an +// array, slice, or range. +// +// In fact, we *did* use multiple objects way back in Exercise +// 016 where we iterated over an array and also a numeric index. +// It didn't always work exactly this way, so the exercise had to +// be retroactively modified a little bit. +// +// for (bits, 0..) |bit, i| { ... } +// +// The general form of a 'for' loop with two lists is: +// +// for (list_a, list_b) |a, b| { +// // Here we have the first item from list_a and list_b, +// // then the second item from each, then the third and +// // so forth... +// } +// +// What's really beautiful about this is that we don't have to +// keep track of an index or advancing a memory pointer for +// *either* of these lists. That error-prone stuff is all taken +// care of for us by the compiler. +// +// Below, we have a program that is supposed to compare two +// arrays. Please make it work! +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main() void { + const hex_nums = [_]u8{ 0xb, 0x2a, 0x77 }; + const dec_nums = [_]u8{ 11, 42, 119 }; + + for (hex_nums, ???) |hn, ???| { + if (hn != dn) { + print("Uh oh! Found a mismatch: {d} vs {d}\n", .{ hn, dn }); + return; + } + } + + print("Arrays match!\n", .{}); +} +// +// You are perhaps wondering what happens if one of the two lists +// is longer than the other? Try it! +// +// By the way, congratulations for making it to Exercise 100! +// +// +-------------+ +// | Celebration | +// | Area * * * | +// +-------------+ +// +// Please keep your celebrating within the area provided. diff --git a/exercises/103_tokenization.zig b/exercises/103_tokenization.zig deleted file mode 100644 index d0abb5c..0000000 --- a/exercises/103_tokenization.zig +++ /dev/null @@ -1,148 +0,0 @@ -// -// The functionality of the standard library is becoming increasingly -// important in Zig. First of all, it is helpful to take a look at how -// the individual functions are implemented. Because this is wonderfully -// suitable as a template for your own functions. In addition, these -// standard functions are part of the basic configuration of Zig. -// -// This means that they are always available on every system. -// Therefore it is worthwhile to deal with them also in Ziglings. -// It's a great way to learn important skills. For example, it is -// often necessary to process large amounts of data from files. -// And for this sequential reading and processing, Zig provides some -// useful functions, which we will take a closer look at in the coming -// exercises. -// -// A nice example of this has been published on the Zig homepage, -// replacing the somewhat dusty 'Hello world! -// -// Nothing against 'Hello world!', but it just doesn't do justice -// to the elegance of Zig and that's a pity, if someone takes a short, -// first look at the homepage and doesn't get 'enchanted'. And for that -// the present example is simply better suited and we will therefore -// use it as an introduction to tokenizing, because it is wonderfully -// suited to understand the basic principles. -// -// In the following exercises we will also read and process data from -// large files, it will then be clearer to you how useful all this is. -// -// Let's start with the analysis of the example from the Zig homepage -// and explain the most important things. -// -// const std = @import("std"); -// -// // Here a function from the Standard library is defined, -// // which transfers numbers from a string into the respective -// // integer values. -// const parseInt = std.fmt.parseInt; -// -// // Defining a test case -// test "parse integers" { -// -// // Four numbers are passed in a string. -// // Please note that the individual values are separated -// // either by a space or a comma. -// const input = "123 67 89,99"; -// -// // In order to be able to process the input values, -// // memory is required. An allocator is defined here for -// // this purpose. -// const gpa = std.testing.allocator; -// -// // An array into which the numbers are stored is initialized. -// var list: std.ArrayList(u32) = .empty; -// -// // This way you can never forget what is urgently needed -// // and the compiler doesn't grumble either. -// defer list.deinit(gpa); -// -// // Now it gets exciting: -// // A standard tokenizer is called (Zig has several) and -// // used to locate the positions of the respective separators -// // (we remember, space and comma) and pass them to an iterator. -// var it = std.mem.tokenizeAny(u8, input, " ,"); -// -// // The iterator can now be processed in a loop and the -// // individual numbers can be transferred. -// while (it.next()) |num| { -// // But be careful: The numbers are still only available -// // as strings. This is where the integer parser comes -// // into play, converting them into real integer values. -// const n = try parseInt(u32, num, 10); -// -// // Finally the individual values are stored in the array. -// try list.append(gpa, n); -// } -// -// // For the subsequent test, a second static array is created, -// // which is directly filled with the expected values. -// const expected = [_]u32{ 123, 67, 89, 99 }; -// -// // Now the numbers converted from the string can be compared -// // with the expected ones, so that the test is completed -// // successfully. -// for (expected, list.items) |exp, actual| { -// try std.testing.expectEqual(exp, actual); -// } -// } -// -// So much for the example from the homepage. -// Let's summarize the basic steps again: -// -// - We have a set of data in sequential order, separated from each other -// by means of various characters. -// -// - For further processing, for example in an array, this data must be -// read in, separated and, if necessary, converted into the target format. -// -// - We need a buffer that is large enough to hold the data. -// -// - This buffer can be created either statically at compile time, if the -// amount of data is already known, or dynamically at runtime by using -// a memory allocator. -// -// - The data are divided by means of Tokenizer at the respective -// separators and stored in the reserved memory. This usually also -// includes conversion to the target format. -// -// - Now the data can be conveniently processed further in the correct format. -// -// These steps are basically always the same. -// Whether the data is read from a file or entered by the user via the -// keyboard, for example, is irrelevant. Only subtleties are distinguished -// and that's why Zig has different tokenizers. But more about this in -// later exercises. -// -// Now we also want to write a small program to tokenize some data, -// after all we need some practice. Suppose we want to count the words -// of this little poem: -// -// My name is Ozymandias, King of Kings; -// Look on my Works, ye Mighty, and despair! -// by Percy Bysshe Shelley -// -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main() !void { - - // our input - const poem = - \\My name is Ozymandias, King of Kings; - \\Look on my Works, ye Mighty, and despair! - ; - - // now the tokenizer, but what do we need here? - var it = std.mem.tokenizeAny(u8, poem, ???); - - // print all words and count them - var cnt: usize = 0; - while (it.next()) |word| { - cnt += 1; - print("{s}\n", .{word}); - } - - // print the result - print("This little poem has {d} words!\n", .{cnt}); -} diff --git a/exercises/104_for5.zig b/exercises/104_for5.zig new file mode 100644 index 0000000..200e71d --- /dev/null +++ b/exercises/104_for5.zig @@ -0,0 +1,126 @@ +// +// The 'for' loop is not just limited to looping over one or two +// items. Let's try an example with a whole bunch! +// +// But first, there's one last thing we've avoided mentioning +// until now: The special range that leaves off the last value: +// +// for ( things, 0.. ) |t, i| { ... } +// +// That's how we tell Zig that we want to get a numeric value for +// every item in "things", starting with 0. +// +// A nice feature of these index ranges is that you can have them +// start with any number you choose. The first value of "i" in +// this example will be 500, then 501, 502, etc.: +// +// for ( things, 500.. ) |t, i| { ... } +// +// Remember our RPG characters? They had the following +// properties, which we stored in a struct type: +// +// class +// gold +// experience +// +// What we're going to do now is store the same RPG character +// data, but in a separate array for each property. +// +// It might look a little awkward, but let's bear with it. +// +// We've started writing a program to print a numbered list of +// characters with each of their properties, but it needs a +// little help: +// +const std = @import("std"); +const print = std.debug.print; + +// This is the same character role enum we've seen before. +const Role = enum { + wizard, + thief, + bard, + warrior, +}; + +pub fn main() void { + // Here are the three "property" arrays: + const roles = [4]Role{ .wizard, .bard, .bard, .warrior }; + const gold = [4]u16{ 25, 11, 5, 7392 }; + const experience = [4]u8{ 40, 17, 55, 21 }; + + // We would like to number our list starting with 1, not 0. + // How do we do that? + for (roles, gold, experience, ???) |c, g, e, i| { + const role_name = switch (c) { + .wizard => "Wizard", + .thief => "Thief", + .bard => "Bard", + .warrior => "Warrior", + }; + + std.debug.print("{d}. {s} (Gold: {d}, XP: {d})\n", .{ + i, + role_name, + g, + e, + }); + } +} +// +// By the way, storing our character data in arrays like this +// isn't *just* a silly way to demonstrate multi-object 'for' +// loops. +// +// It's *also* a silly way to introduce a concept called +// "data-oriented design". +// +// Let's use a metaphor to build up an intuition for what this is +// all about: +// +// Let's say you've been tasked with grabbing three glass +// marbles, three spoons, and three feathers from a magic bag. +// But you can't use your hands to grab them. Instead, you must +// use a marble scoop, spoon magnet, and feather tongs to grab +// each type of object. +// +// Now, would you rather use the magic bag: +// +// A. Grouped the items in clusters so you have to pick up one +// marble, then one spoon, then one feather? +// +// OR +// +// B. Grouped the items by type so you can pick up all of the +// marbles at once, then all the spoons, then all of the +// feathers? +// +// If this metaphor is working, hopefully, it's clear that the 'B' +// option would be much more efficient. +// +// Well, it probably comes as little surprise that storing and +// using data in a sequential and uniform fashion is also more +// efficient for modern CPUs. +// +// Decades of OOP practices have steered people towards grouping +// different data types together into mixed-type "objects" with +// the intent that these are easier on the human mind. +// Data-oriented design groups data by type in a way that is +// easier on the computer. +// +// With clever language design, maybe we can have both. +// +// In the Zig community, you may see the difference in groupings +// presented with the terms "Array of Structs" (AoS) versus +// "Struct of Arrays" (SoA). +// +// To envision these two designs in action, imagine an array of +// RPG character structs, each containing three different data +// types (AoS) versus a single RPG character struct containing +// three arrays of one data type each, like those in the exercise +// above (SoA). +// +// For a more practical application of "data-oriented design" +// watch the following talk from Andrew Kelley, the creator of Zig: +// https://vimeo.com/649009599 +// diff --git a/exercises/104_threading.zig b/exercises/104_threading.zig deleted file mode 100644 index 3c3fa21..0000000 --- a/exercises/104_threading.zig +++ /dev/null @@ -1,129 +0,0 @@ -// -// 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. -// -// 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 -// -// 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. -// -// The following diagram roughly illustrates the difference between -// the various types of process execution: -// -// -// Synchronous Asynchronous -// Processing Processing Multithreading -// ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -// │ Thread 1 │ │ Thread 1 │ │ Thread 1 │ │ Thread 2 │ -// ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ Overall Time -// └──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┴──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┬───────┬───────┬── -// ├───┤ ├───┤ ├───┤ ├───┤ │ │ │ -// │ T │ │ T │ │ T │ │ T │ │ │ │ -// │ a │ │ a │ │ a │ │ a │ │ │ │ -// │ s │ │ s │ │ s │ │ s │ │ │ │ -// │ k │ │ k │ │ k │ │ k │ │ │ │ -// │ │ │ │ │ │ │ │ │ │ │ -// │ 1 │ │ 1 │ │ 1 │ │ 3 │ │ │ │ -// └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │ │ -// │ │ │ │ 5 Sec │ │ -// ┌────┴───┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ │ │ │ -// │Blocking│ │ T │ │ T │ │ T │ │ │ │ -// └────┬───┘ │ a │ │ a │ │ a │ │ │ │ -// │ │ s │ │ s │ │ s │ │ 8 Sec │ -// ┌─┴─┐ │ k │ │ k │ │ k │ │ │ │ -// │ T │ │ │ │ │ │ │ │ │ │ -// │ a │ │ 2 │ │ 2 │ │ 4 │ │ │ │ -// │ s │ └─┬─┘ ├───┤ ├───┤ │ │ │ -// │ k │ │ │┼┼┼│ │┼┼┼│ ▼ │ 10 Sec -// │ │ ┌─┴─┐ └───┴────────┴───┴───────── │ │ -// │ 1 │ │ T │ │ │ -// └─┬─┘ │ a │ │ │ -// │ │ s │ │ │ -// ┌─┴─┐ │ k │ │ │ -// │ T │ │ │ │ │ -// │ a │ │ 1 │ │ │ -// │ s │ ├───┤ │ │ -// │ k │ │┼┼┼│ ▼ │ -// │ │ └───┴──────────────────────────────────────────── │ -// │ 2 │ │ -// ├───┤ │ -// │┼┼┼│ ▼ -// └───┴──────────────────────────────────────────────────────────────── -// -// -// The diagram was modeled on the one in a blog in which the differences -// between asynchronous processing and multithreading are explained in detail: -// https://blog.devgenius.io/multi-threading-vs-asynchronous-programming-what-is-the-difference-3ebfe1179a5 -// -// Our exercise is essentially about clarifying the approach in Zig and -// therefore we try to keep it as simple as possible. -// Multithreading in itself is already difficult enough. ;-) -// -const std = @import("std"); - -pub fn main() !void { - // This is where the preparatory work takes place - // before the parallel processing begins. - std.debug.print("Starting work...\n", .{}); - - // These curly brackets are very important, they are necessary - // to enclose the area where the threads are called. - // Without these brackets, the program would not wait for the - // end of the threads and they would continue to run beyond the - // end of the program. - { - // Now we start the first thread, with the number as parameter - const handle = try std.Thread.spawn(.{}, thread_function, .{1}); - - // Waits for the thread to complete, - // then deallocates any resources created on `spawn()`. - defer handle.join(); - - // Second thread - const handle2 = try std.Thread.spawn(.{}, thread_function, .{-4}); // that can't be right? - defer handle2.join(); - - // Third thread - const handle3 = try std.Thread.spawn(.{}, thread_function, .{3}); - defer ??? // <-- something is missing - - // After the threads have been started, - // 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.fromMilliseconds(400), .awake); - std.debug.print("Some weird stuff, after starting the threads.\n", .{}); - } - // After we have left the closed area, we wait until - // the threads have run through, if this has not yet been the case. - std.debug.print("Zig is cool!\n", .{}); -} - -// 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(id: usize) !void { - var io_instance: std.Io.Threaded = .init_single_threaded; - const io = io_instance.io(); - 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 = 300 * ((5 - id % 3) - 2); - try io.sleep(std.Io.Duration.fromMilliseconds(@intCast(work_time)), .awake); - - 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, -// e.g. by setting up a pool and allowing the threads to communicate -// with each other using semaphores. -// -// But that's a topic for another exercise. diff --git a/exercises/105_testing.zig b/exercises/105_testing.zig new file mode 100644 index 0000000..0dec9b9 --- /dev/null +++ b/exercises/105_testing.zig @@ -0,0 +1,96 @@ +// +// A big advantage of Zig is the integration of its own test system. +// This allows the philosophy of Test Driven Development (TDD) to be +// implemented perfectly. Zig even goes one step further than other +// languages, the tests can be included directly in the source file. +// +// This has several advantages. On the one hand it is much clearer to +// have everything in one file, both the source code and the associated +// test code. On the other hand, it is much easier for third parties +// to understand what exactly a function is supposed to do if they can +// simply look at the test inside the source and compare both. +// +// Especially if you want to understand how e.g. the standard library +// of Zig works, this approach is very helpful. Furthermore it is very +// practical, if you want to report a bug to the Zig community, to +// illustrate it with a small example including a test. +// +// Therefore, in this exercise we will deal with the basics of testing +// in Zig. Basically, tests work as follows: you pass certain parameters +// to a function, for which you get a return - the result. This is then +// compared with the EXPECTED value. If both values match, the test is +// passed, otherwise an error message is displayed. +// +// testing.expect(foo(param1, param2) == expected); +// +// Also other comparisons are possible, deviations or also errors can +// be provoked, which must lead to an appropriate behavior of the +// function, so that the test is passed. +// +// Tests can be run via Zig build system or applied directly to +// individual modules using "zig test xyz.zig". +// +// Both can be used script-driven to execute tests automatically, e.g. +// after checking into a Git repository. Something we also make extensive +// use of here at Ziglings. +// +const std = @import("std"); +const testing = std.testing; + +// This is a simple function that builds a sum from the passed parameters and +// returns. +fn add(a: f16, b: f16) f16 { + return a + b; +} + +// The associated test. It always starts with the keyword "test", followed by a +// description of the tasks of the test. This is followed by the test cases in +// curly brackets. +test "add" { + + // The first test checks if the sum of '41' and '1' gives '42', which is + // correct. + try testing.expect(add(41, 1) == 42); + + // Another way to perform this test is as follows: + try testing.expectEqual(42, add(41, 1)); + + // This time a test with the addition of a negative number: + try testing.expect(add(5, -4) == 1); + + // And a floating point operation: + try testing.expect(add(1.5, 1.5) == 3); +} + +// Another simple function that returns the result of subtracting the two +// parameters. +fn sub(a: f16, b: f16) f16 { + return a - b; +} + +// The corresponding test is not much different from the previous one. Except +// that it contains an error that you need to correct. +test "sub" { + try testing.expect(sub(10, 5) == 6); + + try testing.expect(sub(3, 1.5) == 1.5); +} + +// This function divides the numerator by the denominator. Here it is important +// that the denominator must not be zero. This is checked and if it occurs an +// error is returned. +fn divide(a: f16, b: f16) !f16 { + if (b == 0) return error.DivisionByZero; + return a / b; +} + +test "divide" { + try testing.expect(divide(2, 2) catch unreachable == 1); + try testing.expect(divide(-1, -1) catch unreachable == 1); + try testing.expect(divide(10, 2) catch unreachable == 5); + try testing.expect(divide(1, 3) catch unreachable == 0.3333333333333333); + + // Now we test if the function returns an error if we pass a zero as + // denominator. But which error needs to be tested? + try testing.expectError(error.???, divide(15, 0)); +} diff --git a/exercises/105_threading2.zig b/exercises/105_threading2.zig deleted file mode 100644 index 374391a..0000000 --- a/exercises/105_threading2.zig +++ /dev/null @@ -1,107 +0,0 @@ -// -// Now that we are familiar with the principles of multi-threading, -// let's boldly venture into a practical example from mathematics. -// We will determine the circle number PI with sufficient accuracy. -// -// There are different methods for this, and some of them are several -// hundred years old. For us, the dusty procedures are surprisingly well -// suited to our exercise. Because the mathematicians of the time didn't -// have fancy computers with which we can calculate something like this -// in seconds today. -// Whereby, of course, it depends on the accuracy, i.e. how many digits -// after the decimal point we are interested in. -// But these old procedures can still be tackled with paper and pencil, -// which is why they are easier for us to understand. -// At least for me. ;-) -// -// So let's take a mental leap back a few years. -// Around 1672 (if you want to know and read about it in detail, you can -// do so on Wikipedia, for example), various mathematicians once again -// discovered a method of approaching the circle number PI. -// There were the Scottish mathematician Gregory and the German -// mathematician Leibniz, and even a few hundred years earlier the Indian -// mathematician Madhava. All of them independently developed the same -// formula, which was published by Leibniz in 1682 in the journal -// "Acta Eruditorum". -// This is why this method has become known as the "Leibniz series", -// although the other names are also often used today. -// We will not go into the formula and its derivation in detail, but -// will deal with the series straight away: -// -// 4 4 4 4 4 -// PI = --- - --- + --- - --- + --- ... -// 1 3 5 7 9 -// -// As you can clearly see, the series starts with the whole number 4 and -// approaches the circle number by subtracting and adding smaller and -// smaller parts of 4. Pretty much everyone has learned PI = 3.14 at school, -// but very few people remember other digits, and this is rarely necessary -// in practice. Because either you don't need the precision, or you use a -// calculator in which the number is stored as a very precise constant. -// But at some point this constant was calculated and we are doing the same -// now. The question at this point is, how many partial values do we have -// to calculate for which accuracy? -// -// The answer is chewing, to get 8 digits after the decimal point we need -// 1,000,000,000 partial values. And for each additional digit we have to -// add a zero. -// Even fast computers - and I mean really fast computers - get a bit warmer -// on the CPU when it comes to really many digits. But the 8 digits are -// enough for us for now, because we want to understand the principle and -// nothing more, right? -// -// As we have already discovered, the Leibniz series is a series with a -// fixed distance of 2 between the individual partial values. This makes -// it easy to apply a simple loop to it, because if we start with n = 1 -// (which is not necessarily useful now) we always have to add 2 in each -// round. -// But wait! The partial values are alternately added and subtracted. -// This could also be achieved with one loop, but not very elegantly. -// It also makes sense to split this between two CPUs, one calculates -// the positive values and the other the negative values. And so we can -// simply start two threads and add everything up at the end and we're -// done. -// We just have to remember that if only the positive or negative values -// are calculated, the distances are twice as large, i.e. 4. -// -// So that the whole thing has a real learning effect, the first thread -// call is specified and you have to make the second. -// But don't worry, it will work out. :-) -// -const std = @import("std"); - -pub fn main() !void { - const count = 1_000_000_000; - var pi_plus: f64 = 0; - var pi_minus: f64 = 0; - - { - // First thread to calculate the plus numbers. - const handle1 = try std.Thread.spawn(.{}, thread_pi, .{ &pi_plus, 5, count }); - defer handle1.join(); - - // Second thread to calculate the minus numbers. - ??? - - } - // Here we add up the results. - std.debug.print("PI ≈ {d:.8}\n", .{4 + pi_plus - pi_minus}); -} - -fn thread_pi(pi: *f64, begin: u64, end: u64) !void { - var n: u64 = begin; - while (n < end) : (n += 4) { - pi.* += 4 / @as(f64, @floatFromInt(n)); - } -} -// If you wish, you can increase the number of loop passes, which -// improves the number of digits. -// -// But be careful: -// In order for parallel processing to really show its strengths, -// the compiler must be given the "-O ReleaseFast" flag when it -// is created. Otherwise the debug functions slow down the speed -// to such an extent that seconds become minutes during execution. -// -// And you should remove the formatting restriction in "print", -// otherwise you will not be able to see the additional digits. diff --git a/exercises/106_files.zig b/exercises/106_files.zig deleted file mode 100644 index bf4de9b..0000000 --- a/exercises/106_files.zig +++ /dev/null @@ -1,102 +0,0 @@ -// -// Until now, we've only been printing our output in the console, -// which is good enough for fighting aliens and hermit bookkeeping. -// -// However, many other tasks require some interaction with the file system, -// which is the underlying structure for organizing files on your computer. -// -// The file system provides a hierarchical structure for storing files -// by organizing them into directories, which hold files and other directories, -// thus creating a tree structure that can be navigated. -// -// Fortunately, the Zig Standard Library provides a simple API for interacting -// with the file system, see the detail documentation here: -// -// https://ziglang.org/documentation/master/std/#std.Io -// -// In this exercise, we'll try to: -// - create a new directory, -// - open a file in the directory, -// - write to the file. -// -// Note: For simplicity, we write byte-by-byte without buffering. -// In real applications, you'd typically use a buffer for better -// performance. We'll learn about buffered I/O in a later exercise. -// -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - // default I/O implementation - const io = init.io; - - // first we get the current working directory - const cwd: std.Io.Dir = std.Io.Dir.cwd(); - - // then we'll try to make a new directory /output/ - // to store our output files. - cwd.createDir(io, "output", .default_dir) catch |e| switch (e) { - // there is a chance you might want to run this - // program more than once and the path might already - // have been created, so we'll have to handle this error - // by doing nothing - // - // we want to catch error.PathAlreadyExists and do nothing - ??? => {}, - // if there's any other unexpected error we just propagate it through - else => return e, - }; - - // then we'll try to open our freshly created directory - // wait a minute... - // opening a directory might fail! - // what should we do here? - var output_dir: std.Io.Dir = try cwd.openDir(io, "output", .{}); - defer output_dir.close(io); - - // we try to open the file `zigling.txt`, - // and propagate any error up - const file: std.Io.File = try output_dir.createFile(io, "zigling.txt", .{}); - // it is a good habit to close a file after you are done with it - // so that other programs can read it and prevent data corruption - // but here we are not yet done writing to the file - // if only there were a keyword in Zig that - // allowed you to "defer" code execution to the end of the scope... - file.close(io); - - // you are not allowed to move these lines above the file closing line! - var file_writer = file.writer(io, &.{}); - const writer = &file_writer.interface; - - const byte_written = try writer.write("It's zigling time!"); - std.debug.print("Successfully wrote {d} bytes.\n", .{byte_written}); -} -// to check if you actually write to the file, you can either, -// 1. open the file in your text editor, or -// 2. print the content of the file in the console with one of these commands -// Linux/macOS: >> cat ./output/zigling.txt -// Windows (CMD): >> type .\output\zigling.txt -// -// -// More on Creating files -// -// notice in: -// ... try output_dir.createFile(io, "zigling.txt", .{}); -// ^^^ -// we passed this anonymous struct to the function call -// -// this is the struct `CreateFlag` with default fields -// { -// read: bool = false, -// truncate: bool = true, -// exclusive: bool = false, -// lock: Lock = .none, -// lock_nonblocking: bool = false, -// mode: Mode = default_mode -// } -// -// Question: -// - what should you do if you want to also read the file after opening it? -// - go to the documentation of the struct `std.Io.Dir` here: -// https://ziglang.org/documentation/master/std/#std.Io.Dir -// - can you find a function for opening a file? how about deleting a file? -// - what kind of options can you use with those functions? diff --git a/exercises/106_tokenization.zig b/exercises/106_tokenization.zig new file mode 100644 index 0000000..d0abb5c --- /dev/null +++ b/exercises/106_tokenization.zig @@ -0,0 +1,148 @@ +// +// The functionality of the standard library is becoming increasingly +// important in Zig. First of all, it is helpful to take a look at how +// the individual functions are implemented. Because this is wonderfully +// suitable as a template for your own functions. In addition, these +// standard functions are part of the basic configuration of Zig. +// +// This means that they are always available on every system. +// Therefore it is worthwhile to deal with them also in Ziglings. +// It's a great way to learn important skills. For example, it is +// often necessary to process large amounts of data from files. +// And for this sequential reading and processing, Zig provides some +// useful functions, which we will take a closer look at in the coming +// exercises. +// +// A nice example of this has been published on the Zig homepage, +// replacing the somewhat dusty 'Hello world! +// +// Nothing against 'Hello world!', but it just doesn't do justice +// to the elegance of Zig and that's a pity, if someone takes a short, +// first look at the homepage and doesn't get 'enchanted'. And for that +// the present example is simply better suited and we will therefore +// use it as an introduction to tokenizing, because it is wonderfully +// suited to understand the basic principles. +// +// In the following exercises we will also read and process data from +// large files, it will then be clearer to you how useful all this is. +// +// Let's start with the analysis of the example from the Zig homepage +// and explain the most important things. +// +// const std = @import("std"); +// +// // Here a function from the Standard library is defined, +// // which transfers numbers from a string into the respective +// // integer values. +// const parseInt = std.fmt.parseInt; +// +// // Defining a test case +// test "parse integers" { +// +// // Four numbers are passed in a string. +// // Please note that the individual values are separated +// // either by a space or a comma. +// const input = "123 67 89,99"; +// +// // In order to be able to process the input values, +// // memory is required. An allocator is defined here for +// // this purpose. +// const gpa = std.testing.allocator; +// +// // An array into which the numbers are stored is initialized. +// var list: std.ArrayList(u32) = .empty; +// +// // This way you can never forget what is urgently needed +// // and the compiler doesn't grumble either. +// defer list.deinit(gpa); +// +// // Now it gets exciting: +// // A standard tokenizer is called (Zig has several) and +// // used to locate the positions of the respective separators +// // (we remember, space and comma) and pass them to an iterator. +// var it = std.mem.tokenizeAny(u8, input, " ,"); +// +// // The iterator can now be processed in a loop and the +// // individual numbers can be transferred. +// while (it.next()) |num| { +// // But be careful: The numbers are still only available +// // as strings. This is where the integer parser comes +// // into play, converting them into real integer values. +// const n = try parseInt(u32, num, 10); +// +// // Finally the individual values are stored in the array. +// try list.append(gpa, n); +// } +// +// // For the subsequent test, a second static array is created, +// // which is directly filled with the expected values. +// const expected = [_]u32{ 123, 67, 89, 99 }; +// +// // Now the numbers converted from the string can be compared +// // with the expected ones, so that the test is completed +// // successfully. +// for (expected, list.items) |exp, actual| { +// try std.testing.expectEqual(exp, actual); +// } +// } +// +// So much for the example from the homepage. +// Let's summarize the basic steps again: +// +// - We have a set of data in sequential order, separated from each other +// by means of various characters. +// +// - For further processing, for example in an array, this data must be +// read in, separated and, if necessary, converted into the target format. +// +// - We need a buffer that is large enough to hold the data. +// +// - This buffer can be created either statically at compile time, if the +// amount of data is already known, or dynamically at runtime by using +// a memory allocator. +// +// - The data are divided by means of Tokenizer at the respective +// separators and stored in the reserved memory. This usually also +// includes conversion to the target format. +// +// - Now the data can be conveniently processed further in the correct format. +// +// These steps are basically always the same. +// Whether the data is read from a file or entered by the user via the +// keyboard, for example, is irrelevant. Only subtleties are distinguished +// and that's why Zig has different tokenizers. But more about this in +// later exercises. +// +// Now we also want to write a small program to tokenize some data, +// after all we need some practice. Suppose we want to count the words +// of this little poem: +// +// My name is Ozymandias, King of Kings; +// Look on my Works, ye Mighty, and despair! +// by Percy Bysshe Shelley +// +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main() !void { + + // our input + const poem = + \\My name is Ozymandias, King of Kings; + \\Look on my Works, ye Mighty, and despair! + ; + + // now the tokenizer, but what do we need here? + var it = std.mem.tokenizeAny(u8, poem, ???); + + // print all words and count them + var cnt: usize = 0; + while (it.next()) |word| { + cnt += 1; + print("{s}\n", .{word}); + } + + // print the result + print("This little poem has {d} words!\n", .{cnt}); +} diff --git a/exercises/107_files2.zig b/exercises/107_files2.zig deleted file mode 100644 index c363086..0000000 --- a/exercises/107_files2.zig +++ /dev/null @@ -1,61 +0,0 @@ -// -// Prerequisite : -// - exercise/106_files.zig, or -// - create a file {project_root}/output/zigling.txt -// with content `It's zigling time!`(18 bytes total) -// -// Now there's no point in writing to a file if we don't read from it, am I right? -// Let's write a program to read the content of the file that we just created. -// -// I am assuming that you've created the appropriate files for this to work. -// -// Alright, bud, lean in close. Here's the game plan. -// - First, we open the {project_root}/output/ directory -// - Secondly, we open file `zigling.txt` in that directory -// - Then, we initialize an array of characters with all letter 'A', and print it -// - After that, we read the content of the file into the array -// - Finally, we print out the content we just read -// -// Note: For simplicity, we read byte-by-byte without buffering. -// In real applications, you'd typically use a buffer for better -// performance. We'll learn about buffered I/O in a later exercise. - -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Get the current working directory - const cwd = std.Io.Dir.cwd(); - - // try to open ./output assuming you did your 106_files exercise - var output_dir = try cwd.openDir(io, "output", .{}); - defer output_dir.close(io); - - // try to open the file - const file = try output_dir.openFile(io, "zigling.txt", .{}); - defer file.close(io); - - // initialize an array of u8 with all letter 'A' - // we need to pick the size of the array, 64 seems like a good number - // fix the initialization below - var content = ['A']*64; - // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` - std.debug.print("{s}\n", .{content}); - - var file_reader = file.reader(io, &.{}); - const reader = &file_reader.interface; - - // okay, seems like a threat of violence is not the answer in this case - // can you go here to find a way to read the content? - // https://ziglang.org/documentation/master/std/#std.Io.Reader - // hint: look for a method that reads into a slice - const bytes_read = zig_read_the_file_or_i_will_fight_you(&content); - - // Woah, too screamy. I know you're excited for zigling time but tone it down a bit. - // Can you print only what we read from the file? - std.debug.print("Successfully Read {d} bytes: {s}\n", .{ - bytes_read, - content, // change this line only - }); -} diff --git a/exercises/107_threading.zig b/exercises/107_threading.zig new file mode 100644 index 0000000..3c3fa21 --- /dev/null +++ b/exercises/107_threading.zig @@ -0,0 +1,129 @@ +// +// 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. +// +// 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 +// +// 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. +// +// The following diagram roughly illustrates the difference between +// the various types of process execution: +// +// +// Synchronous Asynchronous +// Processing Processing Multithreading +// ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +// │ Thread 1 │ │ Thread 1 │ │ Thread 1 │ │ Thread 2 │ +// ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ Overall Time +// └──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┴──┼┼┼┼┼───┴─┴──┼┼┼┼┼───┴──┬───────┬───────┬── +// ├───┤ ├───┤ ├───┤ ├───┤ │ │ │ +// │ T │ │ T │ │ T │ │ T │ │ │ │ +// │ a │ │ a │ │ a │ │ a │ │ │ │ +// │ s │ │ s │ │ s │ │ s │ │ │ │ +// │ k │ │ k │ │ k │ │ k │ │ │ │ +// │ │ │ │ │ │ │ │ │ │ │ +// │ 1 │ │ 1 │ │ 1 │ │ 3 │ │ │ │ +// └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │ │ +// │ │ │ │ 5 Sec │ │ +// ┌────┴───┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ │ │ │ +// │Blocking│ │ T │ │ T │ │ T │ │ │ │ +// └────┬───┘ │ a │ │ a │ │ a │ │ │ │ +// │ │ s │ │ s │ │ s │ │ 8 Sec │ +// ┌─┴─┐ │ k │ │ k │ │ k │ │ │ │ +// │ T │ │ │ │ │ │ │ │ │ │ +// │ a │ │ 2 │ │ 2 │ │ 4 │ │ │ │ +// │ s │ └─┬─┘ ├───┤ ├───┤ │ │ │ +// │ k │ │ │┼┼┼│ │┼┼┼│ ▼ │ 10 Sec +// │ │ ┌─┴─┐ └───┴────────┴───┴───────── │ │ +// │ 1 │ │ T │ │ │ +// └─┬─┘ │ a │ │ │ +// │ │ s │ │ │ +// ┌─┴─┐ │ k │ │ │ +// │ T │ │ │ │ │ +// │ a │ │ 1 │ │ │ +// │ s │ ├───┤ │ │ +// │ k │ │┼┼┼│ ▼ │ +// │ │ └───┴──────────────────────────────────────────── │ +// │ 2 │ │ +// ├───┤ │ +// │┼┼┼│ ▼ +// └───┴──────────────────────────────────────────────────────────────── +// +// +// The diagram was modeled on the one in a blog in which the differences +// between asynchronous processing and multithreading are explained in detail: +// https://blog.devgenius.io/multi-threading-vs-asynchronous-programming-what-is-the-difference-3ebfe1179a5 +// +// Our exercise is essentially about clarifying the approach in Zig and +// therefore we try to keep it as simple as possible. +// Multithreading in itself is already difficult enough. ;-) +// +const std = @import("std"); + +pub fn main() !void { + // This is where the preparatory work takes place + // before the parallel processing begins. + std.debug.print("Starting work...\n", .{}); + + // These curly brackets are very important, they are necessary + // to enclose the area where the threads are called. + // Without these brackets, the program would not wait for the + // end of the threads and they would continue to run beyond the + // end of the program. + { + // Now we start the first thread, with the number as parameter + const handle = try std.Thread.spawn(.{}, thread_function, .{1}); + + // Waits for the thread to complete, + // then deallocates any resources created on `spawn()`. + defer handle.join(); + + // Second thread + const handle2 = try std.Thread.spawn(.{}, thread_function, .{-4}); // that can't be right? + defer handle2.join(); + + // Third thread + const handle3 = try std.Thread.spawn(.{}, thread_function, .{3}); + defer ??? // <-- something is missing + + // After the threads have been started, + // 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.fromMilliseconds(400), .awake); + std.debug.print("Some weird stuff, after starting the threads.\n", .{}); + } + // After we have left the closed area, we wait until + // the threads have run through, if this has not yet been the case. + std.debug.print("Zig is cool!\n", .{}); +} + +// 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(id: usize) !void { + var io_instance: std.Io.Threaded = .init_single_threaded; + const io = io_instance.io(); + 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 = 300 * ((5 - id % 3) - 2); + try io.sleep(std.Io.Duration.fromMilliseconds(@intCast(work_time)), .awake); + + 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, +// e.g. by setting up a pool and allowing the threads to communicate +// with each other using semaphores. +// +// But that's a topic for another exercise. diff --git a/exercises/108_labeled_switch.zig b/exercises/108_labeled_switch.zig deleted file mode 100644 index 897fcf5..0000000 --- a/exercises/108_labeled_switch.zig +++ /dev/null @@ -1,79 +0,0 @@ -// -// You've heard of while loops in exercises 011,012,013 and 014 -// You've also heard of switch expressions in exercises 030 and 31. -// You've also seen how labels can be used in exercise 063. -// -// By combining while loops and switch statements with continue and break statements -// one can create very concise State Machines. -// -// One such example would be: -// -// pub fn main() void { -// var op: u8 = 1; -// while (true) { -// switch (op) { -// 1 => { op = 2; continue; }, -// 2 => { op = 3; continue; }, -// 3 => return, -// else => {}, -// } -// break; -// } -// std.debug.print("This statement cannot be reached\n", .{}); -// } -// -// By combining all we've learned so far, we can now proceed with a labeled switch. -// -// A labeled switch is some extra syntactic sugar, which comes with all sorts of -// candy (performance benefits). Don't believe me? Directly to source https://github.com/ziglang/zig/pull/21367 -// -// Here is the previous excerpt implemented as a labeled switch instead: -// -// pub fn main() void { -// foo: switch (@as(u8, 1)) { -// 1 => continue :foo 2, -// 2 => continue :foo 3, -// 3 => return, -// else => {}, -// } -// std.debug.print("This statement cannot be reached\n", .{}); -// } -// -// The flow of execution on this second case is: -// 1. The switch starts with value '1'; -// 2. The switch evaluates to case '1' which in turn uses the continue statement -// to re-evaluate the labeled switch again, now providing the value '2'; -// 3. In the case '2' we repeat the same pattern as case '1' -// but instead the value to be evaluated is now '3'; -// 4. Finally we get to case '3', where we return from the function as a whole, -// so the debug statement is never executed. -// 5. In this example, since the input does not have clear, exhaustive patterns and -// can essentially be any 'u8' integer, we need to handle all cases not explicitly -// covered by using the 'else => {}' branch as the default case. -// -// -const std = @import("std"); - -const PullRequestState = enum(u8) { - Draft, - InReview, - Approved, - Rejected, - Merged, -}; - -pub fn main() void { - // Oh no, your pull request keeps being rejected, - // how would you fix it? - pr: switch (PullRequestState.Draft) { - PullRequestState.Draft => continue :pr PullRequestState.InReview, - PullRequestState.InReview => continue :pr PullRequestState.Rejected, - PullRequestState.Approved => continue :pr PullRequestState.Merged, - PullRequestState.Rejected => { - std.debug.print("The pull request has been rejected.\n", .{}); - return; - }, - PullRequestState.Merged => break, // Would you know where to break to? - } - std.debug.print("The pull request has been merged.\n", .{}); -} diff --git a/exercises/108_threading2.zig b/exercises/108_threading2.zig new file mode 100644 index 0000000..374391a --- /dev/null +++ b/exercises/108_threading2.zig @@ -0,0 +1,107 @@ +// +// Now that we are familiar with the principles of multi-threading, +// let's boldly venture into a practical example from mathematics. +// We will determine the circle number PI with sufficient accuracy. +// +// There are different methods for this, and some of them are several +// hundred years old. For us, the dusty procedures are surprisingly well +// suited to our exercise. Because the mathematicians of the time didn't +// have fancy computers with which we can calculate something like this +// in seconds today. +// Whereby, of course, it depends on the accuracy, i.e. how many digits +// after the decimal point we are interested in. +// But these old procedures can still be tackled with paper and pencil, +// which is why they are easier for us to understand. +// At least for me. ;-) +// +// So let's take a mental leap back a few years. +// Around 1672 (if you want to know and read about it in detail, you can +// do so on Wikipedia, for example), various mathematicians once again +// discovered a method of approaching the circle number PI. +// There were the Scottish mathematician Gregory and the German +// mathematician Leibniz, and even a few hundred years earlier the Indian +// mathematician Madhava. All of them independently developed the same +// formula, which was published by Leibniz in 1682 in the journal +// "Acta Eruditorum". +// This is why this method has become known as the "Leibniz series", +// although the other names are also often used today. +// We will not go into the formula and its derivation in detail, but +// will deal with the series straight away: +// +// 4 4 4 4 4 +// PI = --- - --- + --- - --- + --- ... +// 1 3 5 7 9 +// +// As you can clearly see, the series starts with the whole number 4 and +// approaches the circle number by subtracting and adding smaller and +// smaller parts of 4. Pretty much everyone has learned PI = 3.14 at school, +// but very few people remember other digits, and this is rarely necessary +// in practice. Because either you don't need the precision, or you use a +// calculator in which the number is stored as a very precise constant. +// But at some point this constant was calculated and we are doing the same +// now. The question at this point is, how many partial values do we have +// to calculate for which accuracy? +// +// The answer is chewing, to get 8 digits after the decimal point we need +// 1,000,000,000 partial values. And for each additional digit we have to +// add a zero. +// Even fast computers - and I mean really fast computers - get a bit warmer +// on the CPU when it comes to really many digits. But the 8 digits are +// enough for us for now, because we want to understand the principle and +// nothing more, right? +// +// As we have already discovered, the Leibniz series is a series with a +// fixed distance of 2 between the individual partial values. This makes +// it easy to apply a simple loop to it, because if we start with n = 1 +// (which is not necessarily useful now) we always have to add 2 in each +// round. +// But wait! The partial values are alternately added and subtracted. +// This could also be achieved with one loop, but not very elegantly. +// It also makes sense to split this between two CPUs, one calculates +// the positive values and the other the negative values. And so we can +// simply start two threads and add everything up at the end and we're +// done. +// We just have to remember that if only the positive or negative values +// are calculated, the distances are twice as large, i.e. 4. +// +// So that the whole thing has a real learning effect, the first thread +// call is specified and you have to make the second. +// But don't worry, it will work out. :-) +// +const std = @import("std"); + +pub fn main() !void { + const count = 1_000_000_000; + var pi_plus: f64 = 0; + var pi_minus: f64 = 0; + + { + // First thread to calculate the plus numbers. + const handle1 = try std.Thread.spawn(.{}, thread_pi, .{ &pi_plus, 5, count }); + defer handle1.join(); + + // Second thread to calculate the minus numbers. + ??? + + } + // Here we add up the results. + std.debug.print("PI ≈ {d:.8}\n", .{4 + pi_plus - pi_minus}); +} + +fn thread_pi(pi: *f64, begin: u64, end: u64) !void { + var n: u64 = begin; + while (n < end) : (n += 4) { + pi.* += 4 / @as(f64, @floatFromInt(n)); + } +} +// If you wish, you can increase the number of loop passes, which +// improves the number of digits. +// +// But be careful: +// In order for parallel processing to really show its strengths, +// the compiler must be given the "-O ReleaseFast" flag when it +// is created. Otherwise the debug functions slow down the speed +// to such an extent that seconds become minutes during execution. +// +// And you should remove the formatting restriction in "print", +// otherwise you will not be able to see the additional digits. diff --git a/exercises/109_files.zig b/exercises/109_files.zig new file mode 100644 index 0000000..bf4de9b --- /dev/null +++ b/exercises/109_files.zig @@ -0,0 +1,102 @@ +// +// Until now, we've only been printing our output in the console, +// which is good enough for fighting aliens and hermit bookkeeping. +// +// However, many other tasks require some interaction with the file system, +// which is the underlying structure for organizing files on your computer. +// +// The file system provides a hierarchical structure for storing files +// by organizing them into directories, which hold files and other directories, +// thus creating a tree structure that can be navigated. +// +// Fortunately, the Zig Standard Library provides a simple API for interacting +// with the file system, see the detail documentation here: +// +// https://ziglang.org/documentation/master/std/#std.Io +// +// In this exercise, we'll try to: +// - create a new directory, +// - open a file in the directory, +// - write to the file. +// +// Note: For simplicity, we write byte-by-byte without buffering. +// In real applications, you'd typically use a buffer for better +// performance. We'll learn about buffered I/O in a later exercise. +// +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + // default I/O implementation + const io = init.io; + + // first we get the current working directory + const cwd: std.Io.Dir = std.Io.Dir.cwd(); + + // then we'll try to make a new directory /output/ + // to store our output files. + cwd.createDir(io, "output", .default_dir) catch |e| switch (e) { + // there is a chance you might want to run this + // program more than once and the path might already + // have been created, so we'll have to handle this error + // by doing nothing + // + // we want to catch error.PathAlreadyExists and do nothing + ??? => {}, + // if there's any other unexpected error we just propagate it through + else => return e, + }; + + // then we'll try to open our freshly created directory + // wait a minute... + // opening a directory might fail! + // what should we do here? + var output_dir: std.Io.Dir = try cwd.openDir(io, "output", .{}); + defer output_dir.close(io); + + // we try to open the file `zigling.txt`, + // and propagate any error up + const file: std.Io.File = try output_dir.createFile(io, "zigling.txt", .{}); + // it is a good habit to close a file after you are done with it + // so that other programs can read it and prevent data corruption + // but here we are not yet done writing to the file + // if only there were a keyword in Zig that + // allowed you to "defer" code execution to the end of the scope... + file.close(io); + + // you are not allowed to move these lines above the file closing line! + var file_writer = file.writer(io, &.{}); + const writer = &file_writer.interface; + + const byte_written = try writer.write("It's zigling time!"); + std.debug.print("Successfully wrote {d} bytes.\n", .{byte_written}); +} +// to check if you actually write to the file, you can either, +// 1. open the file in your text editor, or +// 2. print the content of the file in the console with one of these commands +// Linux/macOS: >> cat ./output/zigling.txt +// Windows (CMD): >> type .\output\zigling.txt +// +// +// More on Creating files +// +// notice in: +// ... try output_dir.createFile(io, "zigling.txt", .{}); +// ^^^ +// we passed this anonymous struct to the function call +// +// this is the struct `CreateFlag` with default fields +// { +// read: bool = false, +// truncate: bool = true, +// exclusive: bool = false, +// lock: Lock = .none, +// lock_nonblocking: bool = false, +// mode: Mode = default_mode +// } +// +// Question: +// - what should you do if you want to also read the file after opening it? +// - go to the documentation of the struct `std.Io.Dir` here: +// https://ziglang.org/documentation/master/std/#std.Io.Dir +// - can you find a function for opening a file? how about deleting a file? +// - what kind of options can you use with those functions? diff --git a/exercises/109_vectors.zig b/exercises/109_vectors.zig deleted file mode 100644 index 96892ca..0000000 --- a/exercises/109_vectors.zig +++ /dev/null @@ -1,147 +0,0 @@ -// So far in Ziglings, we've seen how for loops can be used to -// repeat calculations across an array in several ways. -// -// For loops are generally great for this kind of task, but -// sometimes they don't fully utilize the capabilities of the -// CPU. -// -// Most modern CPUs can execute instructions in which SEVERAL -// calculations are performed WITHIN registers at the SAME TIME. -// These are known as "single instruction, multiple data" (SIMD) -// instructions. SIMD instructions can make code significantly -// more performant. -// -// To see why, imagine we have a program in which we take the -// square root of four (changing) f32 floats. -// -// A simple compiler would take the program and produce machine code -// which calculates each square root sequentially. Most registers on -// modern CPUs have 64 bits, so we could imagine that each float moves -// into a 64-bit register, and the following happens four times: -// -// 32 bits 32 bits -// +-------------------+ -// register | 0 | x | -// +-------------------+ -// -// | -// [SQRT instruction] -// V -// -// +-------------------+ -// | 0 | sqrt(x) | -// +-------------------+ -// -// Notice that half of the register contains blank data to which -// nothing happened. What a waste! What if we were able to use -// that space instead? This is the idea at the core of SIMD. -// -// Most modern CPUs contain specialized registers with at least 128 bits -// for performing SIMD instructions. On a machine with 128-bit SIMD -// registers, a smart compiler would probably NOT issue four sqrt -// instructions as above, but instead pack the floats into a single -// 128-bit register, then execute a single "packed" sqrt -// instruction to do ALL the square root calculations at once. -// -// For example: -// -// -// 32 bits 32 bits 32 bits 32 bits -// +---------------------------------------+ -// register | 4.0 | 9.0 | 25.0 | 49.0 | -// +---------------------------------------+ -// -// | -// [SIMD SQRT instruction] -// V -// -// +---------------------------------------+ -// register | 2.0 | 3.0 | 5.0 | 7.0 | -// +---------------------------------------+ -// -// Pretty cool, right? -// -// Code with SIMD instructions is usually more performant than code -// without SIMD instructions. Zig cares a lot about performance, -// so it has built-in support for SIMD! It has a data structure that -// directly supports SIMD instructions: -// -// +-----------+ -// | Vectors | -// +-----------+ -// -// Operations performed on vectors in Zig will be done in parallel using -// SIMD instructions, whenever possible. -// -// Defining vectors in Zig is straightforwards. No library import is needed. -const v1 = @Vector(3, i32){ 1, 10, 100 }; -const v2 = @Vector(3, f32){ 2.0, 3.0, 5.0 }; - -// Vectors support the same builtin operators as their underlying base types. -const v3 = v1 + v1; // { 2, 20, 200}; -const v4 = v2 * v2; // { 4.0, 9.0, 25.0}; - -// Intrinsics that apply to base types usually extend to vectors. -const v5: @Vector(3, f32) = @floatFromInt(v3); // { 2.0, 20.0, 200.0} -const v6 = v4 - v5; // { 2.0, -11.0, -175.0} -const v7 = @abs(v6); // { 2.0, 11.0, 175.0} - -// We can make constant vectors, and reduce vectors. -const v8: @Vector(4, u8) = @splat(2); // { 2, 2, 2, 2} -const v8_sum = @reduce(.Add, v8); // 8 -const v8_min = @reduce(.Min, v8); // 2 - -// Fixed-length arrays can be automatically assigned to vectors (and vice-versa). -const single_digit_primes = [4]i8{ 2, 3, 5, 7 }; -const prime_vector: @Vector(4, i8) = single_digit_primes; - -// Now let's use vectors to simplify and optimize some code! -// -// Ewa is writing a program in which they frequently want to compare -// two lists of four f32s. Ewa expects the lists to be similar, and -// wants to determine the largest pairwise difference between the lists. -// -// Ewa wrote the following function to figure this out. - -fn calcMaxPairwiseDiffOld(list1: [4]f32, list2: [4]f32) f32 { - var max_diff: f32 = 0; - for (list1, list2) |n1, n2| { - const abs_diff = @abs(n1 - n2); - if (abs_diff > max_diff) { - max_diff = abs_diff; - } - } - return max_diff; -} - -// Ewa heard about vectors in Zig, and started writing a new vector -// version of the function, but has got stuck! -// -// Help Ewa finish the vector version! The examples above should help. - -const Vec4 = @Vector(4, f32); -fn calcMaxPairwiseDiffNew(a: Vec4, b: Vec4) f32 { - const abs_diff_vec = ???; - const max_diff = @reduce(???, abs_diff_vec); - return max_diff; -} - -// Quite the simplification! We could even write the function in one line -// and it would still be readable. -// -// Since the entire function is now expressed in terms of vector operations, -// the Zig compiler will easily be able to compile it down to machine code -// which utilizes the all-powerful SIMD instructions and does a lot of the -// computation in parallel. - -const std = @import("std"); -const print = std.debug.print; - -pub fn main() void { - const l1 = [4]f32{ 3.141, 2.718, 0.577, 1.000 }; - const l2 = [4]f32{ 3.154, 2.707, 0.591, 0.993 }; - const mpd_old = calcMaxPairwiseDiffOld(l1, l2); - const mpd_new = calcMaxPairwiseDiffNew(l1, l2); - print("Max difference (old fn): {d: >5.3}\n", .{mpd_old}); - print("Max difference (new fn): {d: >5.3}\n", .{mpd_new}); -} diff --git a/exercises/110_files2.zig b/exercises/110_files2.zig new file mode 100644 index 0000000..c363086 --- /dev/null +++ b/exercises/110_files2.zig @@ -0,0 +1,61 @@ +// +// Prerequisite : +// - exercise/106_files.zig, or +// - create a file {project_root}/output/zigling.txt +// with content `It's zigling time!`(18 bytes total) +// +// Now there's no point in writing to a file if we don't read from it, am I right? +// Let's write a program to read the content of the file that we just created. +// +// I am assuming that you've created the appropriate files for this to work. +// +// Alright, bud, lean in close. Here's the game plan. +// - First, we open the {project_root}/output/ directory +// - Secondly, we open file `zigling.txt` in that directory +// - Then, we initialize an array of characters with all letter 'A', and print it +// - After that, we read the content of the file into the array +// - Finally, we print out the content we just read +// +// Note: For simplicity, we read byte-by-byte without buffering. +// In real applications, you'd typically use a buffer for better +// performance. We'll learn about buffered I/O in a later exercise. + +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Get the current working directory + const cwd = std.Io.Dir.cwd(); + + // try to open ./output assuming you did your 106_files exercise + var output_dir = try cwd.openDir(io, "output", .{}); + defer output_dir.close(io); + + // try to open the file + const file = try output_dir.openFile(io, "zigling.txt", .{}); + defer file.close(io); + + // initialize an array of u8 with all letter 'A' + // we need to pick the size of the array, 64 seems like a good number + // fix the initialization below + var content = ['A']*64; + // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` + std.debug.print("{s}\n", .{content}); + + var file_reader = file.reader(io, &.{}); + const reader = &file_reader.interface; + + // okay, seems like a threat of violence is not the answer in this case + // can you go here to find a way to read the content? + // https://ziglang.org/documentation/master/std/#std.Io.Reader + // hint: look for a method that reads into a slice + const bytes_read = zig_read_the_file_or_i_will_fight_you(&content); + + // Woah, too screamy. I know you're excited for zigling time but tone it down a bit. + // Can you print only what we read from the file? + std.debug.print("Successfully Read {d} bytes: {s}\n", .{ + bytes_read, + content, // change this line only + }); +} diff --git a/exercises/110_quiz9.zig b/exercises/110_quiz9.zig deleted file mode 100644 index 8f5cb61..0000000 --- a/exercises/110_quiz9.zig +++ /dev/null @@ -1,484 +0,0 @@ -// ---------------------------------------------------------------------------- -// Quiz Time: Toggling, Setting, and Clearing Bits -// ---------------------------------------------------------------------------- -// -// Another exciting thing about Zig is its suitability for embedded -// programming. Your Zig code doesn't have to remain on your laptop; you can -// also deploy your code to microcontrollers! This means you can write Zig to -// drive your next robot or greenhouse climate control system! Ready to enter -// the exciting world of embedded programming? Let's get started! -// -// ---------------------------------------------------------------------------- -// Some Background -// ---------------------------------------------------------------------------- -// -// A common activity in microcontroller programming is setting and clearing -// bits on input and output pins. This lets you control LEDs, sensors, motors -// and more! In a previous exercise (097_bit_manipulation.zig) you learned how -// to swap two bytes using the ^ (XOR - exclusive or) operator. This quiz will -// test your knowledge of bit manipulation in Zig while giving you a taste of -// what it's like to control registers in a real microcontroller. Included at -// the end are some helper functions that demonstrate how we might make our -// code a little more readable. -// -// Below is a pinout diagram for the famous ATmega328 AVR microcontroller used -// as the primary microchip on popular microcontroller platforms like the -// Arduino UNO. -// -// ============ PINOUT DIAGRAM FOR ATMEGA328 MICROCONTROLLER ============ -// _____ _____ -// | U | -// (RESET) PC6 --| 1 28 |-- PC5 -// PD0 --| 2 27 |-- PC4 -// PD1 --| 3 26 |-- PC3 -// PD2 --| 4 25 |-- PC2 -// PD3 --| 5 24 |-- PC1 -// PD4 --| 6 23 |-- PC0 -// VCC --| 7 22 |-- GND -// GND --| 8 21 |-- AREF -// |-- PB6 --| 9 20 |-- AVCC -// |-- PB7 --| 10 19 |-- PB5 --| -// | PD5 --| 11 18 |-- PB4 --| -// | PD6 --| 12 17 |-- PB3 --| -// | PD7 --| 13 16 |-- PB2 --| -// |-- PB0 --| 14 15 |-- PB1 --| -// | |___________| | -// \_______________________________/ -// | -// PORTB -// -// Drawing inspiration from this diagram, we'll use the pins for PORTB as our -// mental model for this quiz on bit manipulation. It should be noted that -// in the following problems we are using ordinary variables, one of which we -// have named PORTB, to simulate modifying the bits of real hardware registers. -// But in actual microcontroller code, PORTB would be defined something like -// this: -// pub const PORTB = @as(*volatile u8, @ptrFromInt(0x25)); -// -// This lets the compiler know not to make any optimizations to PORTB so that -// the IO pins are properly mapped to our code. -// -// NOTE : To keep things simple, the following problems are given using type -// u4, so applying the output to PORTB would only affect the lower four pins -// PB0..PB3. Of course, there is nothing to prevent you from swapping the u4 -// with a u8 so you can control all 8 of PORTB's IO pins. - -const std = @import("std"); -const print = std.debug.print; -const testing = std.testing; - -pub fn main() !void { - var PORTB: u4 = 0b0000; // only 4 bits wide for simplicity - - // ------------------------------------------------------------------------ - // Quiz - // ------------------------------------------------------------------------ - - // See if you can solve the following problems. The last two problems throw - // you a bit of a curve ball. Try solving them on your own. If you need - // help, scroll to the bottom of main to see some in depth explanations on - // toggling, setting, and clearing bits in Zig. - - print("Toggle pins with XOR on PORTB\n", .{}); - print("-----------------------------\n", .{}); - PORTB = 0b1100; - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("^ {b:0>4} // (bitmask)\n", .{0b0101}); - PORTB ^= (1 << 1) | (1 << 0); // What's wrong here? - checkAnswer(0b1001, PORTB); - - newline(); - - PORTB = 0b1100; - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("^ {b:0>4} // (bitmask)\n", .{0b0011}); - PORTB ^= (1 << 1) & (1 << 0); // What's wrong here? - checkAnswer(0b1111, PORTB); - - newline(); - - print("Set pins with OR on PORTB\n", .{}); - print("-------------------------\n", .{}); - - PORTB = 0b1001; // reset PORTB - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("| {b:0>4} // (bitmask)\n", .{0b0100}); - PORTB = PORTB ??? (1 << 2); // What's missing here? - checkAnswer(0b1101, PORTB); - - newline(); - - PORTB = 0b1001; // reset PORTB - print(" {b:0>4} // (reset state)\n", .{PORTB}); - print("| {b:0>4} // (bitmask)\n", .{0b0100}); - PORTB ??? (1 << 2); // What's missing here? - checkAnswer(0b1101, PORTB); - - newline(); - - print("Clear pins with AND and NOT on PORTB\n", .{}); - print("------------------------------------\n", .{}); - - PORTB = 0b1110; // reset PORTB - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("& {b:0>4} // (bitmask)\n", .{0b1011}); - PORTB = PORTB & ???@as(u4, 1 << 2); // What character is missing here? - checkAnswer(0b1010, PORTB); - - newline(); - - PORTB = 0b0111; // reset PORTB - print(" {b:0>4} // (reset state)\n", .{PORTB}); - print("& {b:0>4} // (bitmask)\n", .{0b1110}); - PORTB &= ~(1 << 0); // What's missing here? - checkAnswer(0b0110, PORTB); - - newline(); - newline(); -} - -// ************************************************************************ -// IN-DEPTH EXPLANATIONS BELOW -// ************************************************************************ -// -// -// -// -// -// -// -// -// -// -// -// ------------------------------------------------------------------------ -// Toggling bits with XOR: -// ------------------------------------------------------------------------ -// XOR stands for "exclusive or". We can toggle bits with the ^ (XOR) -// bitwise operator, like so: -// -// -// In order to output a 1, the logic of an XOR operation requires that the -// two input bits are of different values. Therefore, 0 ^ 1 and 1 ^ 0 will -// both yield a 1 but 0 ^ 0 and 1 ^ 1 will output 0. XOR's unique behavior -// of outputting a 0 when both inputs are 1s is what makes it different from -// the OR operator; it also gives us the ability to toggle bits by putting -// 1s into our bitmask. -// -// - 1s in our bitmask operand, can be thought of as causing the -// corresponding bits in the other operand to flip to the opposite value. -// - 0s cause no change. -// -// The 0s in our bitmask preserve these values -// -XOR op- ---expanded--- in the output. -// _______________/ -// / / -// 1100 1 1 0 0 -// ^ 0101 0 1 0 1 (bitmask) -// ------ - - - - -// = 1001 1 0 0 1 <- This bit was already cleared. -// \_______\ -// \ -// We can think of these bits having flipped -// because of the presence of 1s in those columns -// of our bitmask. -// -// Now let's take a look at setting bits with the | operator. -// -// -// -// -// -// ------------------------------------------------------------------------ -// Setting bits with OR: -// ------------------------------------------------------------------------ -// We can set bits on PORTB with the | (OR) operator, like so: -// -// var PORTB: u4 = 0b1001; -// PORTB = PORTB | 0b0010; -// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011 -// -// -OR op- ---expanded--- -// _ Set only this bit. -// / -// 1001 1 0 0 1 -// | 0010 0 0 1 0 (bitmask) -// ------ - - - - -// = 1011 1 0 1 1 -// \___\_______\ -// \ -// These bits remain untouched because OR-ing with -// a 0 effects no change. -// -// ------------------------------------------------------------------------ -// To create a bitmask like 0b0010 used above: -// -// 1. First, shift the value 1 over one place with the bitwise << (shift -// left) operator as indicated below: -// 1 << 0 -> 0001 -// 1 << 1 -> 0010 <-- Shift 1 one place to the left -// 1 << 2 -> 0100 -// 1 << 3 -> 1000 -// -// This allows us to rewrite the above code like this: -// -// var PORTB: u4 = 0b1001; -// PORTB = PORTB | (1 << 1); -// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011 -// -// Finally, as in the C language, Zig allows us to use the |= operator, so -// we can rewrite our code again in an even more compact and idiomatic -// form: PORTB |= (1 << 1) - -// So now we've covered how to toggle and set bits. What about clearing -// them? Well, this is where Zig throws us a curve ball. Don't worry we'll -// go through it step by step. -// -// -// -// -// -// ------------------------------------------------------------------------ -// Clearing bits with AND and NOT: -// ------------------------------------------------------------------------ -// We can clear bits with the & (AND) bitwise operator, like so: - -// PORTB = 0b1110; // reset PORTB -// PORTB = PORTB & 0b1011; -// print("PORTB: {b:0>4}\n", .{PORTB}); // output -> 1010 -// -// - 0s clear bits when used in conjunction with a bitwise AND. -// - 1s do nothing, thus preserving the original bits. -// -// -AND op- ---expanded--- -// __________ Clear only this bit. -// / -// 1110 1 1 1 0 -// & 1011 1 0 1 1 (bitmask) -// ------ - - - - -// = 1010 1 0 1 0 <- This bit was already cleared. -// \_______\ -// \ -// These bits remain untouched because AND-ing with a -// 1 preserves the original bit value whether 0 or 1. -// -// ------------------------------------------------------------------------ -// We can use the ~ (NOT) operator to easily create a bitmask like 1011: -// -// 1. First, shift the value 1 over two places with the bit-wise << (shift -// left) operator as indicated below: -// 1 << 0 -> 0001 -// 1 << 1 -> 0010 -// 1 << 2 -> 0100 <- The 1 has been shifted two places to the left -// 1 << 3 -> 1000 -// -// 2. The second step in creating our bitmask is to invert the bits -// ~0100 -> 1011 -// in C we would write this as: -// ~(1 << 2) -> 1011 -// -// But if we try to compile ~(1 << 2) in Zig, we'll get an error: -// unable to perform binary not operation on type 'comptime_int' -// -// Before Zig can invert our bits, it needs to know the number of -// bits it's being asked to invert. -// -// We do this with the @as (cast as) built-in like this: -// @as(u4, 1 << 2) -> 0100 -// -// Finally, we can invert our new mask by placing the NOT ~ operator -// before our expression, like this: -// ~@as(u4, 1 << 2) -> 1011 -// -// If you are offput by the fact that you can't simply invert bits like -// you can in languages such as C without casting to a particular size -// of integer, you're not alone. However, this is actually another -// instance where Zig is really helpful because it protects you from -// difficult to debug integer overflow bugs that can have you tearing -// your hair out. In the interest of keeping things sane, Zig requires -// you simply to tell it the size of number you are inverting. In the -// words of Andrew Kelley, "If you want to invert the bits of an -// integer, zig has to know how many bits there are." -// -// For more insight into the Zig team's position on why the language -// takes the approach it does with the ~ operator, take a look at -// Andrew's comments on the following github issue: -// https://github.com/ziglang/zig/issues/1382#issuecomment-414459529 -// -// Whew, so after all that what we end up with is: -// PORTB = PORTB & ~@as(u4, 1 << 2); -// -// We can shorten this with the &= combined AND and assignment operator, -// which applies the AND operator on PORTB and then reassigns PORTB. Here's -// what that looks like: -// PORTB &= ~@as(u4, 1 << 2); -// - -// ------------------------------------------------------------------------ -// Conclusion -// ------------------------------------------------------------------------ -// -// While the examples in this quiz have used only 4-bit wide variables, -// working with 8 bits is no different. Here's an example where we set -// every other bit beginning with the two's place: - -// var PORTD: u8 = 0b0000_0000; -// print("PORTD: {b:0>8}\n", .{PORTD}); -// PORTD |= (1 << 1); -// PORTD = setBit(u8, PORTD, 3); -// PORTD |= (1 << 5) | (1 << 7); -// print("PORTD: {b:0>8} // set every other bit\n", .{PORTD}); -// PORTD = ~PORTD; -// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD}); -// newline(); -// -// // Here we clear every other bit beginning with the two's place. -// -// PORTD = 0b1111_1111; -// print("PORTD: {b:0>8}\n", .{PORTD}); -// PORTD &= ~@as(u8, 1 << 1); -// PORTD = clearBit(u8, PORTD, 3); -// PORTD &= ~@as(u8, (1 << 5) | (1 << 7)); -// print("PORTD: {b:0>8} // clear every other bit\n", .{PORTD}); -// PORTD = ~PORTD; -// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD}); -// newline(); - -// ---------------------------------------------------------------------------- -// Here are some helper functions for manipulating bits -// ---------------------------------------------------------------------------- - -// Functions for setting, clearing, and toggling a single bit -fn setBit(comptime T: type, byte: T, comptime bit_pos: T) !T { - return byte | (1 << bit_pos); -} - -test "setBit" { - try testing.expectEqual(setBit(u8, 0b0000_0000, 3), 0b0000_1000); -} - -fn clearBit(comptime T: type, byte: T, comptime bit_pos: T) T { - return byte & ~@as(T, (1 << bit_pos)); -} - -test "clearBit" { - try testing.expectEqual(clearBit(u8, 0b1111_1111, 0), 0b1111_1110); -} - -fn toggleBit(comptime T: type, byte: T, comptime bit_pos: T) T { - return byte ^ (1 << bit_pos); -} - -test "toggleBit" { - var byte = toggleBit(u8, 0b0000_0000, 0); - try testing.expectEqual(byte, 0b0000_0001); - byte = toggleBit(u8, byte, 0); - try testing.expectEqual(byte, 0b0000_0000); -} - -// ---------------------------------------------------------------------------- -// Some additional functions for setting, clearing, and toggling multiple bits -// at once with a tuple because, hey, why not? -// ---------------------------------------------------------------------------- -// - -fn createBitmask(comptime T: type, comptime bits: anytype) !T { - comptime var bitmask: T = 0; - inline for (bits) |bit| { - if (bit >= @bitSizeOf(T)) return error.BitPosTooLarge; - if (bit < 0) return error.BitPosTooSmall; - - bitmask |= (1 << bit); - } - return bitmask; -} - -test "creating bitmasks from a tuple" { - try testing.expectEqual(createBitmask(u8, .{0}), 0b0000_0001); - try testing.expectEqual(createBitmask(u8, .{1}), 0b0000_0010); - try testing.expectEqual(createBitmask(u8, .{2}), 0b0000_0100); - try testing.expectEqual(createBitmask(u8, .{3}), 0b0000_1000); - // - try testing.expectEqual(createBitmask(u8, .{ 0, 4 }), 0b0001_0001); - try testing.expectEqual(createBitmask(u8, .{ 1, 5 }), 0b0010_0010); - try testing.expectEqual(createBitmask(u8, .{ 2, 6 }), 0b0100_0100); - try testing.expectEqual(createBitmask(u8, .{ 3, 7 }), 0b1000_1000); - - try testing.expectError(error.BitPosTooLarge, createBitmask(u4, .{4})); -} - -fn setBits(byte: u8, bits: anytype) !u8 { - const bitmask = try createBitmask(u8, bits); - return byte | bitmask; -} - -test "setBits" { - try testing.expectEqual(setBits(0b0000_0000, .{0}), 0b0000_0001); - try testing.expectEqual(setBits(0b0000_0000, .{7}), 0b1000_0000); - - try testing.expectEqual(setBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); - try testing.expectEqual(setBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); - - try testing.expectEqual(setBits(0b0000_0000, .{ 2, 3, 4, 5 }), 0b0011_1100); - - try testing.expectError(error.BitPosTooLarge, setBits(0b1111_1111, .{8})); - try testing.expectError(error.BitPosTooSmall, setBits(0b1111_1111, .{-1})); -} - -fn clearBits(comptime byte: u8, comptime bits: anytype) !u8 { - const bitmask: u8 = try createBitmask(u8, bits); - return byte & ~@as(u8, bitmask); -} - -test "clearBits" { - try testing.expectEqual(clearBits(0b1111_1111, .{0}), 0b1111_1110); - try testing.expectEqual(clearBits(0b1111_1111, .{7}), 0b0111_1111); - - try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); - try testing.expectEqual(clearBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); - - try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 6, 7 }), 0b0011_1100); - - try testing.expectError(error.BitPosTooLarge, clearBits(0b1111_1111, .{8})); - try testing.expectError(error.BitPosTooSmall, clearBits(0b1111_1111, .{-1})); -} - -fn toggleBits(comptime byte: u8, comptime bits: anytype) !u8 { - const bitmask = try createBitmask(u8, bits); - return byte ^ bitmask; -} - -test "toggleBits" { - try testing.expectEqual(toggleBits(0b0000_0000, .{0}), 0b0000_0001); - try testing.expectEqual(toggleBits(0b0000_0000, .{7}), 0b1000_0000); - - try testing.expectEqual(toggleBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); - try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); - - try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_0000); - try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3 }), 0b0000_0000); - - try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 2, 4, 6 }), 0b0101_0101); - - try testing.expectError(error.BitPosTooLarge, toggleBits(0b1111_1111, .{8})); - try testing.expectError(error.BitPosTooSmall, toggleBits(0b1111_1111, .{-1})); -} - -// ---------------------------------------------------------------------------- -// Utility functions -// ---------------------------------------------------------------------------- - -fn newline() void { - print("\n", .{}); -} - -fn checkAnswer(expected: u4, answer: u4) void { - if (expected != answer) { - print("*************************************************************\n", .{}); - print("= {b:0>4} <- INCORRECT! THE EXPECTED OUTPUT IS {b:0>4}\n", .{ answer, expected }); - print("*************************************************************\n", .{}); - } else { - print("= {b:0>4}", .{answer}); - } - newline(); -} diff --git a/exercises/111_labeled_switch.zig b/exercises/111_labeled_switch.zig new file mode 100644 index 0000000..897fcf5 --- /dev/null +++ b/exercises/111_labeled_switch.zig @@ -0,0 +1,79 @@ +// +// You've heard of while loops in exercises 011,012,013 and 014 +// You've also heard of switch expressions in exercises 030 and 31. +// You've also seen how labels can be used in exercise 063. +// +// By combining while loops and switch statements with continue and break statements +// one can create very concise State Machines. +// +// One such example would be: +// +// pub fn main() void { +// var op: u8 = 1; +// while (true) { +// switch (op) { +// 1 => { op = 2; continue; }, +// 2 => { op = 3; continue; }, +// 3 => return, +// else => {}, +// } +// break; +// } +// std.debug.print("This statement cannot be reached\n", .{}); +// } +// +// By combining all we've learned so far, we can now proceed with a labeled switch. +// +// A labeled switch is some extra syntactic sugar, which comes with all sorts of +// candy (performance benefits). Don't believe me? Directly to source https://github.com/ziglang/zig/pull/21367 +// +// Here is the previous excerpt implemented as a labeled switch instead: +// +// pub fn main() void { +// foo: switch (@as(u8, 1)) { +// 1 => continue :foo 2, +// 2 => continue :foo 3, +// 3 => return, +// else => {}, +// } +// std.debug.print("This statement cannot be reached\n", .{}); +// } +// +// The flow of execution on this second case is: +// 1. The switch starts with value '1'; +// 2. The switch evaluates to case '1' which in turn uses the continue statement +// to re-evaluate the labeled switch again, now providing the value '2'; +// 3. In the case '2' we repeat the same pattern as case '1' +// but instead the value to be evaluated is now '3'; +// 4. Finally we get to case '3', where we return from the function as a whole, +// so the debug statement is never executed. +// 5. In this example, since the input does not have clear, exhaustive patterns and +// can essentially be any 'u8' integer, we need to handle all cases not explicitly +// covered by using the 'else => {}' branch as the default case. +// +// +const std = @import("std"); + +const PullRequestState = enum(u8) { + Draft, + InReview, + Approved, + Rejected, + Merged, +}; + +pub fn main() void { + // Oh no, your pull request keeps being rejected, + // how would you fix it? + pr: switch (PullRequestState.Draft) { + PullRequestState.Draft => continue :pr PullRequestState.InReview, + PullRequestState.InReview => continue :pr PullRequestState.Rejected, + PullRequestState.Approved => continue :pr PullRequestState.Merged, + PullRequestState.Rejected => { + std.debug.print("The pull request has been rejected.\n", .{}); + return; + }, + PullRequestState.Merged => break, // Would you know where to break to? + } + std.debug.print("The pull request has been merged.\n", .{}); +} diff --git a/exercises/111_packed.zig b/exercises/111_packed.zig deleted file mode 100644 index 1058f76..0000000 --- a/exercises/111_packed.zig +++ /dev/null @@ -1,175 +0,0 @@ -// -// We've already learned plenty about bit manipulation using bitwise operations -// in exercices 097 and 098 and in quiz 110. The techniques we already know work -// just fine, but creating masks and shifting individual bits around can become -// quite tedious and unwieldy pretty quickly. -// What if there was a better, a more convenient way to control invidivual bits? -// -// Luckily, Zig has a keyword for exactly this purpose: -// -// packed -// -// It doesn't do anything on its own, to unlock its potential (and to get our -// program to compile) we have to attach it either to a struct or to a union -// declaration: -// -// const Foo = packed struct { ... }; -// const Bar = packed union { ... }; -// -// Now, what does this keyword even do? -// To answer this question we first have to talk about *container layouts*. -// -// Plain structs and unions use the `auto` layout; it gives no guarantees about -// their size or the order of the fields they contain, both are fully up to the -// compiler (though both size and field order *are* guaranteed to be the same -// across any single compilation unit). -// -// Attaching the `packed` keyword to a container makes it use `packed` layout: -// Suddenly, all of its fields are *packed* together tightly without any padding -// in between and their order is guaranteed to be the same as the one specified -// in our source code. For structs, the size of the container is guaranteed to -// be the sum of the (bit-)sizes of all of its fields. For unions, all fields -// have to have the exact same (bit-)size (no padding allowed!); the union itself -// is also guaranteed to be exactly of this size. -// -// If you're familiar with C, you might have already heard of structure packing -// in a different context: arranging fields in a way that minimizes the amount -// of alignment padding between them (or having the compiler do it for you). -// This is *not* what Zig's `packed` keyword is for! -// -// Try to make the comptime assertions below pass: - -const PackedStruct = packed struct { - a: u2, - b: u?, -}; - -comptime { - assert(@bitSizeOf(PackedStruct) == 6); -} - -const PackedUnion = packed union { - a: bool, - b: u?, -}; - -comptime { - assert(@bitSizeOf(PackedUnion) == 1); -} - -// Now, how can we use this new knowledge to manipulate some bits? -// -// As you might have already guessed, `packed` containers are very useful for -// representing bitflags or other tightly packed collections of bit-sized values -// often found in file headers and network protocols. -// -// Let's take a look at a real-life example: -// The LZ4 compression format (†) specifies a frame format to describe compressed -// data. Each LZ4 frame has a descriptor, and each descriptor contains a 'FLG' -// byte that specifies the contents of its frame: - -/// | BitNb | 7-6 | 5 | 4 | 3 | 2 | 1 | 0 | -/// | ------- |-------|-------|----------|------|----------|--------|------| -/// |FieldName|Version|B.Indep|B.Checksum|C.Size|C.Checksum|Reserved|DictID| -/// -const FLG = packed struct(u8) { - dict_id: bool, - reserved: u1 = 0, - content_checksum: bool, - content_size: bool, - block_checksum: bool, - block_indepencence: bool, - version: u2, -}; - -// Wait, what's with the `(u8)` after the `struct` keyword? What do integers have -// to do with all of this? -// Well, this is a good opportunity to come clear about something: -// packed structs and packed unions aren't actually structs or unions at all... -// They are merely integers in disguise! For all intents and purposes, their -// fields are just convenient names for ranges of their underlying bits. To make -// it easier to enforce size requirements for packed containers, Zig allows us -// to specify a *backing integer* for them, just like for enums. -// -// In the case of `FLG`, we want our struct to occupy exactly a single byte, so -// we specify `u8` as the backing integer. It's safe to convert between a packed -// container and its backing integer using the builtin `@bitCast`. -// The LZ4 spec also mandates that reserved bits must always be zero, so it's -// good practice to set `0` as a default value for `reserved`. -// -// The fields of a packed struct start at the least significant bit of its backing -// integer and end at its most significant bit. This is the case no matter what -// endianness our target has. -// -// Try to silence the complaints below: - -const Bits = packed struct(u4) { - a: u1 = 0, - b: u1 = 0, - c: u1 = 0, - d: u1 = 0, -}; - -pub fn main() void { - { - const expected: Bits = @bitCast(@as(u4, 0b1000)); - const my_bits: Bits = .{}; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0001)); - const my_bits: Bits = .{}; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0010)); - const my_bits: Bits = .{}; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0011)); - const my_bits: Bits = .{}; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b1101)); - const my_bits: Bits = .{}; - if (my_bits != expected) complain(my_bits, expected, @src()); - } -} - -// As we can see, equality comparisons (`==` and `!=`) work for packed structs. -// They also work for packed unions. However, since packed containers are not -// naturally ordered, we can't use any other comparison operators on them. -// -// It's also possible to use packed containers in `switch` statements, which we -// will cover in the next exercise! -// -// Since packed containers make very strong guarantees about their memory layout, -// only a handful of types are eligible to be part of them. -// The following types are allowed as field types: -// -// - integers -// - floats -// - bool -// - void -// - enums with explicit backing integers -// - packed unions -// - packed structs -// - -const std = @import("std"); -const assert = std.debug.assert; - -fn complain(my_bits: Bits, expected: Bits, src_loc: std.builtin.SourceLocation) void { - std.debug.print( - "That's not quite right! You've got 0b{b:0>4}, but we want 0b{b:0>4} in line {d}.\n", - .{ @as(u4, @bitCast(my_bits)), @as(u4, @bitCast(expected)), src_loc.line }, - ); -} - -// (†) https://github.com/lz4/lz4/blob/5c4c1fb2354133e1f3b087a341576985f8114bd5/doc/lz4_Frame_format.md#frame-descriptor diff --git a/exercises/112_packed2.zig b/exercises/112_packed2.zig deleted file mode 100644 index 2b6b558..0000000 --- a/exercises/112_packed2.zig +++ /dev/null @@ -1,78 +0,0 @@ -// -// We've already learned about switch statements in exercises 030, 031 and 108. -// They also work with packed containers: - -const S = packed struct(u2) { - a: bool, - b: i1, -}; - -// Try to make it compile without adding an `else` prong! - -comptime { - const s: S = .{ .a = true, .b = -1 }; - switch (s) { - .{ .a = true, .b = -1 } => {}, // ok! - .{ .a = true, .b = ??? }, - .{ .a = ???, .b = 0 }, - .{ .a = ???, .b = ??? }, - => @compileError("We don't want to end up here!"), - } -} - -// As we can see, switching on packed structs is pretty straightforward. -// When switching on packed unions however, we'll realize that a packed -// union never keeps track of its active tag, not even in debug mode! This -// means that packed unions compare solely by their bit pattern (again, just -// like integers). - -const U = packed union(u2) { - a: u2, - b: i2, -}; - -// Find and remove the duplicate case! - -comptime { - const u: U = .{ .a = 3 }; - switch (u) { - .{ .a = 3 } => {}, // ok! - .{ .a = 2 }, - .{ .b = 1 }, - .{ .b = -1 }, - .{ .a = 0 }, - => @compileError("We don't want to end up here!"), - } -} - -// Since packed unions don't have the concept of an active tag, it's always legal -// to access any of their fields. This can be useful to view the same data from -// different perspectives seamlessly. -// -// Try to make the float below negative: - -/// IEEE 754 half precision float -const Float = packed union(u16) { - value: f16, - bits: packed struct(u16) { - mantissa: u10, - exponent: u5, - sign: u1, - }, -}; - -pub fn main() void { - // Reminder: if the sign bit of a float is set, the number is negative! - - var number: Float = .{ .value = 2.34 }; - number.bits.??? = ???; - if (number.value != -2.34) { - std.debug.print("Make it negative!\n", .{}); - } -} - -// This concludes our introduction to packed containers. The next time you need -// control over individual bits, keep them in mind as a potent alternative! -// - -const std = @import("std"); diff --git a/exercises/112_vectors.zig b/exercises/112_vectors.zig new file mode 100644 index 0000000..96892ca --- /dev/null +++ b/exercises/112_vectors.zig @@ -0,0 +1,147 @@ +// So far in Ziglings, we've seen how for loops can be used to +// repeat calculations across an array in several ways. +// +// For loops are generally great for this kind of task, but +// sometimes they don't fully utilize the capabilities of the +// CPU. +// +// Most modern CPUs can execute instructions in which SEVERAL +// calculations are performed WITHIN registers at the SAME TIME. +// These are known as "single instruction, multiple data" (SIMD) +// instructions. SIMD instructions can make code significantly +// more performant. +// +// To see why, imagine we have a program in which we take the +// square root of four (changing) f32 floats. +// +// A simple compiler would take the program and produce machine code +// which calculates each square root sequentially. Most registers on +// modern CPUs have 64 bits, so we could imagine that each float moves +// into a 64-bit register, and the following happens four times: +// +// 32 bits 32 bits +// +-------------------+ +// register | 0 | x | +// +-------------------+ +// +// | +// [SQRT instruction] +// V +// +// +-------------------+ +// | 0 | sqrt(x) | +// +-------------------+ +// +// Notice that half of the register contains blank data to which +// nothing happened. What a waste! What if we were able to use +// that space instead? This is the idea at the core of SIMD. +// +// Most modern CPUs contain specialized registers with at least 128 bits +// for performing SIMD instructions. On a machine with 128-bit SIMD +// registers, a smart compiler would probably NOT issue four sqrt +// instructions as above, but instead pack the floats into a single +// 128-bit register, then execute a single "packed" sqrt +// instruction to do ALL the square root calculations at once. +// +// For example: +// +// +// 32 bits 32 bits 32 bits 32 bits +// +---------------------------------------+ +// register | 4.0 | 9.0 | 25.0 | 49.0 | +// +---------------------------------------+ +// +// | +// [SIMD SQRT instruction] +// V +// +// +---------------------------------------+ +// register | 2.0 | 3.0 | 5.0 | 7.0 | +// +---------------------------------------+ +// +// Pretty cool, right? +// +// Code with SIMD instructions is usually more performant than code +// without SIMD instructions. Zig cares a lot about performance, +// so it has built-in support for SIMD! It has a data structure that +// directly supports SIMD instructions: +// +// +-----------+ +// | Vectors | +// +-----------+ +// +// Operations performed on vectors in Zig will be done in parallel using +// SIMD instructions, whenever possible. +// +// Defining vectors in Zig is straightforwards. No library import is needed. +const v1 = @Vector(3, i32){ 1, 10, 100 }; +const v2 = @Vector(3, f32){ 2.0, 3.0, 5.0 }; + +// Vectors support the same builtin operators as their underlying base types. +const v3 = v1 + v1; // { 2, 20, 200}; +const v4 = v2 * v2; // { 4.0, 9.0, 25.0}; + +// Intrinsics that apply to base types usually extend to vectors. +const v5: @Vector(3, f32) = @floatFromInt(v3); // { 2.0, 20.0, 200.0} +const v6 = v4 - v5; // { 2.0, -11.0, -175.0} +const v7 = @abs(v6); // { 2.0, 11.0, 175.0} + +// We can make constant vectors, and reduce vectors. +const v8: @Vector(4, u8) = @splat(2); // { 2, 2, 2, 2} +const v8_sum = @reduce(.Add, v8); // 8 +const v8_min = @reduce(.Min, v8); // 2 + +// Fixed-length arrays can be automatically assigned to vectors (and vice-versa). +const single_digit_primes = [4]i8{ 2, 3, 5, 7 }; +const prime_vector: @Vector(4, i8) = single_digit_primes; + +// Now let's use vectors to simplify and optimize some code! +// +// Ewa is writing a program in which they frequently want to compare +// two lists of four f32s. Ewa expects the lists to be similar, and +// wants to determine the largest pairwise difference between the lists. +// +// Ewa wrote the following function to figure this out. + +fn calcMaxPairwiseDiffOld(list1: [4]f32, list2: [4]f32) f32 { + var max_diff: f32 = 0; + for (list1, list2) |n1, n2| { + const abs_diff = @abs(n1 - n2); + if (abs_diff > max_diff) { + max_diff = abs_diff; + } + } + return max_diff; +} + +// Ewa heard about vectors in Zig, and started writing a new vector +// version of the function, but has got stuck! +// +// Help Ewa finish the vector version! The examples above should help. + +const Vec4 = @Vector(4, f32); +fn calcMaxPairwiseDiffNew(a: Vec4, b: Vec4) f32 { + const abs_diff_vec = ???; + const max_diff = @reduce(???, abs_diff_vec); + return max_diff; +} + +// Quite the simplification! We could even write the function in one line +// and it would still be readable. +// +// Since the entire function is now expressed in terms of vector operations, +// the Zig compiler will easily be able to compile it down to machine code +// which utilizes the all-powerful SIMD instructions and does a lot of the +// computation in parallel. + +const std = @import("std"); +const print = std.debug.print; + +pub fn main() void { + const l1 = [4]f32{ 3.141, 2.718, 0.577, 1.000 }; + const l2 = [4]f32{ 3.154, 2.707, 0.591, 0.993 }; + const mpd_old = calcMaxPairwiseDiffOld(l1, l2); + const mpd_new = calcMaxPairwiseDiffNew(l1, l2); + print("Max difference (old fn): {d: >5.3}\n", .{mpd_old}); + print("Max difference (new fn): {d: >5.3}\n", .{mpd_new}); +} diff --git a/exercises/113_quiz9.zig b/exercises/113_quiz9.zig new file mode 100644 index 0000000..8f5cb61 --- /dev/null +++ b/exercises/113_quiz9.zig @@ -0,0 +1,484 @@ +// ---------------------------------------------------------------------------- +// Quiz Time: Toggling, Setting, and Clearing Bits +// ---------------------------------------------------------------------------- +// +// Another exciting thing about Zig is its suitability for embedded +// programming. Your Zig code doesn't have to remain on your laptop; you can +// also deploy your code to microcontrollers! This means you can write Zig to +// drive your next robot or greenhouse climate control system! Ready to enter +// the exciting world of embedded programming? Let's get started! +// +// ---------------------------------------------------------------------------- +// Some Background +// ---------------------------------------------------------------------------- +// +// A common activity in microcontroller programming is setting and clearing +// bits on input and output pins. This lets you control LEDs, sensors, motors +// and more! In a previous exercise (097_bit_manipulation.zig) you learned how +// to swap two bytes using the ^ (XOR - exclusive or) operator. This quiz will +// test your knowledge of bit manipulation in Zig while giving you a taste of +// what it's like to control registers in a real microcontroller. Included at +// the end are some helper functions that demonstrate how we might make our +// code a little more readable. +// +// Below is a pinout diagram for the famous ATmega328 AVR microcontroller used +// as the primary microchip on popular microcontroller platforms like the +// Arduino UNO. +// +// ============ PINOUT DIAGRAM FOR ATMEGA328 MICROCONTROLLER ============ +// _____ _____ +// | U | +// (RESET) PC6 --| 1 28 |-- PC5 +// PD0 --| 2 27 |-- PC4 +// PD1 --| 3 26 |-- PC3 +// PD2 --| 4 25 |-- PC2 +// PD3 --| 5 24 |-- PC1 +// PD4 --| 6 23 |-- PC0 +// VCC --| 7 22 |-- GND +// GND --| 8 21 |-- AREF +// |-- PB6 --| 9 20 |-- AVCC +// |-- PB7 --| 10 19 |-- PB5 --| +// | PD5 --| 11 18 |-- PB4 --| +// | PD6 --| 12 17 |-- PB3 --| +// | PD7 --| 13 16 |-- PB2 --| +// |-- PB0 --| 14 15 |-- PB1 --| +// | |___________| | +// \_______________________________/ +// | +// PORTB +// +// Drawing inspiration from this diagram, we'll use the pins for PORTB as our +// mental model for this quiz on bit manipulation. It should be noted that +// in the following problems we are using ordinary variables, one of which we +// have named PORTB, to simulate modifying the bits of real hardware registers. +// But in actual microcontroller code, PORTB would be defined something like +// this: +// pub const PORTB = @as(*volatile u8, @ptrFromInt(0x25)); +// +// This lets the compiler know not to make any optimizations to PORTB so that +// the IO pins are properly mapped to our code. +// +// NOTE : To keep things simple, the following problems are given using type +// u4, so applying the output to PORTB would only affect the lower four pins +// PB0..PB3. Of course, there is nothing to prevent you from swapping the u4 +// with a u8 so you can control all 8 of PORTB's IO pins. + +const std = @import("std"); +const print = std.debug.print; +const testing = std.testing; + +pub fn main() !void { + var PORTB: u4 = 0b0000; // only 4 bits wide for simplicity + + // ------------------------------------------------------------------------ + // Quiz + // ------------------------------------------------------------------------ + + // See if you can solve the following problems. The last two problems throw + // you a bit of a curve ball. Try solving them on your own. If you need + // help, scroll to the bottom of main to see some in depth explanations on + // toggling, setting, and clearing bits in Zig. + + print("Toggle pins with XOR on PORTB\n", .{}); + print("-----------------------------\n", .{}); + PORTB = 0b1100; + print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); + print("^ {b:0>4} // (bitmask)\n", .{0b0101}); + PORTB ^= (1 << 1) | (1 << 0); // What's wrong here? + checkAnswer(0b1001, PORTB); + + newline(); + + PORTB = 0b1100; + print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); + print("^ {b:0>4} // (bitmask)\n", .{0b0011}); + PORTB ^= (1 << 1) & (1 << 0); // What's wrong here? + checkAnswer(0b1111, PORTB); + + newline(); + + print("Set pins with OR on PORTB\n", .{}); + print("-------------------------\n", .{}); + + PORTB = 0b1001; // reset PORTB + print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); + print("| {b:0>4} // (bitmask)\n", .{0b0100}); + PORTB = PORTB ??? (1 << 2); // What's missing here? + checkAnswer(0b1101, PORTB); + + newline(); + + PORTB = 0b1001; // reset PORTB + print(" {b:0>4} // (reset state)\n", .{PORTB}); + print("| {b:0>4} // (bitmask)\n", .{0b0100}); + PORTB ??? (1 << 2); // What's missing here? + checkAnswer(0b1101, PORTB); + + newline(); + + print("Clear pins with AND and NOT on PORTB\n", .{}); + print("------------------------------------\n", .{}); + + PORTB = 0b1110; // reset PORTB + print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); + print("& {b:0>4} // (bitmask)\n", .{0b1011}); + PORTB = PORTB & ???@as(u4, 1 << 2); // What character is missing here? + checkAnswer(0b1010, PORTB); + + newline(); + + PORTB = 0b0111; // reset PORTB + print(" {b:0>4} // (reset state)\n", .{PORTB}); + print("& {b:0>4} // (bitmask)\n", .{0b1110}); + PORTB &= ~(1 << 0); // What's missing here? + checkAnswer(0b0110, PORTB); + + newline(); + newline(); +} + +// ************************************************************************ +// IN-DEPTH EXPLANATIONS BELOW +// ************************************************************************ +// +// +// +// +// +// +// +// +// +// +// +// ------------------------------------------------------------------------ +// Toggling bits with XOR: +// ------------------------------------------------------------------------ +// XOR stands for "exclusive or". We can toggle bits with the ^ (XOR) +// bitwise operator, like so: +// +// +// In order to output a 1, the logic of an XOR operation requires that the +// two input bits are of different values. Therefore, 0 ^ 1 and 1 ^ 0 will +// both yield a 1 but 0 ^ 0 and 1 ^ 1 will output 0. XOR's unique behavior +// of outputting a 0 when both inputs are 1s is what makes it different from +// the OR operator; it also gives us the ability to toggle bits by putting +// 1s into our bitmask. +// +// - 1s in our bitmask operand, can be thought of as causing the +// corresponding bits in the other operand to flip to the opposite value. +// - 0s cause no change. +// +// The 0s in our bitmask preserve these values +// -XOR op- ---expanded--- in the output. +// _______________/ +// / / +// 1100 1 1 0 0 +// ^ 0101 0 1 0 1 (bitmask) +// ------ - - - - +// = 1001 1 0 0 1 <- This bit was already cleared. +// \_______\ +// \ +// We can think of these bits having flipped +// because of the presence of 1s in those columns +// of our bitmask. +// +// Now let's take a look at setting bits with the | operator. +// +// +// +// +// +// ------------------------------------------------------------------------ +// Setting bits with OR: +// ------------------------------------------------------------------------ +// We can set bits on PORTB with the | (OR) operator, like so: +// +// var PORTB: u4 = 0b1001; +// PORTB = PORTB | 0b0010; +// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011 +// +// -OR op- ---expanded--- +// _ Set only this bit. +// / +// 1001 1 0 0 1 +// | 0010 0 0 1 0 (bitmask) +// ------ - - - - +// = 1011 1 0 1 1 +// \___\_______\ +// \ +// These bits remain untouched because OR-ing with +// a 0 effects no change. +// +// ------------------------------------------------------------------------ +// To create a bitmask like 0b0010 used above: +// +// 1. First, shift the value 1 over one place with the bitwise << (shift +// left) operator as indicated below: +// 1 << 0 -> 0001 +// 1 << 1 -> 0010 <-- Shift 1 one place to the left +// 1 << 2 -> 0100 +// 1 << 3 -> 1000 +// +// This allows us to rewrite the above code like this: +// +// var PORTB: u4 = 0b1001; +// PORTB = PORTB | (1 << 1); +// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011 +// +// Finally, as in the C language, Zig allows us to use the |= operator, so +// we can rewrite our code again in an even more compact and idiomatic +// form: PORTB |= (1 << 1) + +// So now we've covered how to toggle and set bits. What about clearing +// them? Well, this is where Zig throws us a curve ball. Don't worry we'll +// go through it step by step. +// +// +// +// +// +// ------------------------------------------------------------------------ +// Clearing bits with AND and NOT: +// ------------------------------------------------------------------------ +// We can clear bits with the & (AND) bitwise operator, like so: + +// PORTB = 0b1110; // reset PORTB +// PORTB = PORTB & 0b1011; +// print("PORTB: {b:0>4}\n", .{PORTB}); // output -> 1010 +// +// - 0s clear bits when used in conjunction with a bitwise AND. +// - 1s do nothing, thus preserving the original bits. +// +// -AND op- ---expanded--- +// __________ Clear only this bit. +// / +// 1110 1 1 1 0 +// & 1011 1 0 1 1 (bitmask) +// ------ - - - - +// = 1010 1 0 1 0 <- This bit was already cleared. +// \_______\ +// \ +// These bits remain untouched because AND-ing with a +// 1 preserves the original bit value whether 0 or 1. +// +// ------------------------------------------------------------------------ +// We can use the ~ (NOT) operator to easily create a bitmask like 1011: +// +// 1. First, shift the value 1 over two places with the bit-wise << (shift +// left) operator as indicated below: +// 1 << 0 -> 0001 +// 1 << 1 -> 0010 +// 1 << 2 -> 0100 <- The 1 has been shifted two places to the left +// 1 << 3 -> 1000 +// +// 2. The second step in creating our bitmask is to invert the bits +// ~0100 -> 1011 +// in C we would write this as: +// ~(1 << 2) -> 1011 +// +// But if we try to compile ~(1 << 2) in Zig, we'll get an error: +// unable to perform binary not operation on type 'comptime_int' +// +// Before Zig can invert our bits, it needs to know the number of +// bits it's being asked to invert. +// +// We do this with the @as (cast as) built-in like this: +// @as(u4, 1 << 2) -> 0100 +// +// Finally, we can invert our new mask by placing the NOT ~ operator +// before our expression, like this: +// ~@as(u4, 1 << 2) -> 1011 +// +// If you are offput by the fact that you can't simply invert bits like +// you can in languages such as C without casting to a particular size +// of integer, you're not alone. However, this is actually another +// instance where Zig is really helpful because it protects you from +// difficult to debug integer overflow bugs that can have you tearing +// your hair out. In the interest of keeping things sane, Zig requires +// you simply to tell it the size of number you are inverting. In the +// words of Andrew Kelley, "If you want to invert the bits of an +// integer, zig has to know how many bits there are." +// +// For more insight into the Zig team's position on why the language +// takes the approach it does with the ~ operator, take a look at +// Andrew's comments on the following github issue: +// https://github.com/ziglang/zig/issues/1382#issuecomment-414459529 +// +// Whew, so after all that what we end up with is: +// PORTB = PORTB & ~@as(u4, 1 << 2); +// +// We can shorten this with the &= combined AND and assignment operator, +// which applies the AND operator on PORTB and then reassigns PORTB. Here's +// what that looks like: +// PORTB &= ~@as(u4, 1 << 2); +// + +// ------------------------------------------------------------------------ +// Conclusion +// ------------------------------------------------------------------------ +// +// While the examples in this quiz have used only 4-bit wide variables, +// working with 8 bits is no different. Here's an example where we set +// every other bit beginning with the two's place: + +// var PORTD: u8 = 0b0000_0000; +// print("PORTD: {b:0>8}\n", .{PORTD}); +// PORTD |= (1 << 1); +// PORTD = setBit(u8, PORTD, 3); +// PORTD |= (1 << 5) | (1 << 7); +// print("PORTD: {b:0>8} // set every other bit\n", .{PORTD}); +// PORTD = ~PORTD; +// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD}); +// newline(); +// +// // Here we clear every other bit beginning with the two's place. +// +// PORTD = 0b1111_1111; +// print("PORTD: {b:0>8}\n", .{PORTD}); +// PORTD &= ~@as(u8, 1 << 1); +// PORTD = clearBit(u8, PORTD, 3); +// PORTD &= ~@as(u8, (1 << 5) | (1 << 7)); +// print("PORTD: {b:0>8} // clear every other bit\n", .{PORTD}); +// PORTD = ~PORTD; +// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD}); +// newline(); + +// ---------------------------------------------------------------------------- +// Here are some helper functions for manipulating bits +// ---------------------------------------------------------------------------- + +// Functions for setting, clearing, and toggling a single bit +fn setBit(comptime T: type, byte: T, comptime bit_pos: T) !T { + return byte | (1 << bit_pos); +} + +test "setBit" { + try testing.expectEqual(setBit(u8, 0b0000_0000, 3), 0b0000_1000); +} + +fn clearBit(comptime T: type, byte: T, comptime bit_pos: T) T { + return byte & ~@as(T, (1 << bit_pos)); +} + +test "clearBit" { + try testing.expectEqual(clearBit(u8, 0b1111_1111, 0), 0b1111_1110); +} + +fn toggleBit(comptime T: type, byte: T, comptime bit_pos: T) T { + return byte ^ (1 << bit_pos); +} + +test "toggleBit" { + var byte = toggleBit(u8, 0b0000_0000, 0); + try testing.expectEqual(byte, 0b0000_0001); + byte = toggleBit(u8, byte, 0); + try testing.expectEqual(byte, 0b0000_0000); +} + +// ---------------------------------------------------------------------------- +// Some additional functions for setting, clearing, and toggling multiple bits +// at once with a tuple because, hey, why not? +// ---------------------------------------------------------------------------- +// + +fn createBitmask(comptime T: type, comptime bits: anytype) !T { + comptime var bitmask: T = 0; + inline for (bits) |bit| { + if (bit >= @bitSizeOf(T)) return error.BitPosTooLarge; + if (bit < 0) return error.BitPosTooSmall; + + bitmask |= (1 << bit); + } + return bitmask; +} + +test "creating bitmasks from a tuple" { + try testing.expectEqual(createBitmask(u8, .{0}), 0b0000_0001); + try testing.expectEqual(createBitmask(u8, .{1}), 0b0000_0010); + try testing.expectEqual(createBitmask(u8, .{2}), 0b0000_0100); + try testing.expectEqual(createBitmask(u8, .{3}), 0b0000_1000); + // + try testing.expectEqual(createBitmask(u8, .{ 0, 4 }), 0b0001_0001); + try testing.expectEqual(createBitmask(u8, .{ 1, 5 }), 0b0010_0010); + try testing.expectEqual(createBitmask(u8, .{ 2, 6 }), 0b0100_0100); + try testing.expectEqual(createBitmask(u8, .{ 3, 7 }), 0b1000_1000); + + try testing.expectError(error.BitPosTooLarge, createBitmask(u4, .{4})); +} + +fn setBits(byte: u8, bits: anytype) !u8 { + const bitmask = try createBitmask(u8, bits); + return byte | bitmask; +} + +test "setBits" { + try testing.expectEqual(setBits(0b0000_0000, .{0}), 0b0000_0001); + try testing.expectEqual(setBits(0b0000_0000, .{7}), 0b1000_0000); + + try testing.expectEqual(setBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); + try testing.expectEqual(setBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); + + try testing.expectEqual(setBits(0b0000_0000, .{ 2, 3, 4, 5 }), 0b0011_1100); + + try testing.expectError(error.BitPosTooLarge, setBits(0b1111_1111, .{8})); + try testing.expectError(error.BitPosTooSmall, setBits(0b1111_1111, .{-1})); +} + +fn clearBits(comptime byte: u8, comptime bits: anytype) !u8 { + const bitmask: u8 = try createBitmask(u8, bits); + return byte & ~@as(u8, bitmask); +} + +test "clearBits" { + try testing.expectEqual(clearBits(0b1111_1111, .{0}), 0b1111_1110); + try testing.expectEqual(clearBits(0b1111_1111, .{7}), 0b0111_1111); + + try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); + try testing.expectEqual(clearBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); + + try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 6, 7 }), 0b0011_1100); + + try testing.expectError(error.BitPosTooLarge, clearBits(0b1111_1111, .{8})); + try testing.expectError(error.BitPosTooSmall, clearBits(0b1111_1111, .{-1})); +} + +fn toggleBits(comptime byte: u8, comptime bits: anytype) !u8 { + const bitmask = try createBitmask(u8, bits); + return byte ^ bitmask; +} + +test "toggleBits" { + try testing.expectEqual(toggleBits(0b0000_0000, .{0}), 0b0000_0001); + try testing.expectEqual(toggleBits(0b0000_0000, .{7}), 0b1000_0000); + + try testing.expectEqual(toggleBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000); + try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111); + + try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_0000); + try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3 }), 0b0000_0000); + + try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 2, 4, 6 }), 0b0101_0101); + + try testing.expectError(error.BitPosTooLarge, toggleBits(0b1111_1111, .{8})); + try testing.expectError(error.BitPosTooSmall, toggleBits(0b1111_1111, .{-1})); +} + +// ---------------------------------------------------------------------------- +// Utility functions +// ---------------------------------------------------------------------------- + +fn newline() void { + print("\n", .{}); +} + +fn checkAnswer(expected: u4, answer: u4) void { + if (expected != answer) { + print("*************************************************************\n", .{}); + print("= {b:0>4} <- INCORRECT! THE EXPECTED OUTPUT IS {b:0>4}\n", .{ answer, expected }); + print("*************************************************************\n", .{}); + } else { + print("= {b:0>4}", .{answer}); + } + newline(); +} diff --git a/exercises/114_packed.zig b/exercises/114_packed.zig new file mode 100644 index 0000000..1058f76 --- /dev/null +++ b/exercises/114_packed.zig @@ -0,0 +1,175 @@ +// +// We've already learned plenty about bit manipulation using bitwise operations +// in exercices 097 and 098 and in quiz 110. The techniques we already know work +// just fine, but creating masks and shifting individual bits around can become +// quite tedious and unwieldy pretty quickly. +// What if there was a better, a more convenient way to control invidivual bits? +// +// Luckily, Zig has a keyword for exactly this purpose: +// +// packed +// +// It doesn't do anything on its own, to unlock its potential (and to get our +// program to compile) we have to attach it either to a struct or to a union +// declaration: +// +// const Foo = packed struct { ... }; +// const Bar = packed union { ... }; +// +// Now, what does this keyword even do? +// To answer this question we first have to talk about *container layouts*. +// +// Plain structs and unions use the `auto` layout; it gives no guarantees about +// their size or the order of the fields they contain, both are fully up to the +// compiler (though both size and field order *are* guaranteed to be the same +// across any single compilation unit). +// +// Attaching the `packed` keyword to a container makes it use `packed` layout: +// Suddenly, all of its fields are *packed* together tightly without any padding +// in between and their order is guaranteed to be the same as the one specified +// in our source code. For structs, the size of the container is guaranteed to +// be the sum of the (bit-)sizes of all of its fields. For unions, all fields +// have to have the exact same (bit-)size (no padding allowed!); the union itself +// is also guaranteed to be exactly of this size. +// +// If you're familiar with C, you might have already heard of structure packing +// in a different context: arranging fields in a way that minimizes the amount +// of alignment padding between them (or having the compiler do it for you). +// This is *not* what Zig's `packed` keyword is for! +// +// Try to make the comptime assertions below pass: + +const PackedStruct = packed struct { + a: u2, + b: u?, +}; + +comptime { + assert(@bitSizeOf(PackedStruct) == 6); +} + +const PackedUnion = packed union { + a: bool, + b: u?, +}; + +comptime { + assert(@bitSizeOf(PackedUnion) == 1); +} + +// Now, how can we use this new knowledge to manipulate some bits? +// +// As you might have already guessed, `packed` containers are very useful for +// representing bitflags or other tightly packed collections of bit-sized values +// often found in file headers and network protocols. +// +// Let's take a look at a real-life example: +// The LZ4 compression format (†) specifies a frame format to describe compressed +// data. Each LZ4 frame has a descriptor, and each descriptor contains a 'FLG' +// byte that specifies the contents of its frame: + +/// | BitNb | 7-6 | 5 | 4 | 3 | 2 | 1 | 0 | +/// | ------- |-------|-------|----------|------|----------|--------|------| +/// |FieldName|Version|B.Indep|B.Checksum|C.Size|C.Checksum|Reserved|DictID| +/// +const FLG = packed struct(u8) { + dict_id: bool, + reserved: u1 = 0, + content_checksum: bool, + content_size: bool, + block_checksum: bool, + block_indepencence: bool, + version: u2, +}; + +// Wait, what's with the `(u8)` after the `struct` keyword? What do integers have +// to do with all of this? +// Well, this is a good opportunity to come clear about something: +// packed structs and packed unions aren't actually structs or unions at all... +// They are merely integers in disguise! For all intents and purposes, their +// fields are just convenient names for ranges of their underlying bits. To make +// it easier to enforce size requirements for packed containers, Zig allows us +// to specify a *backing integer* for them, just like for enums. +// +// In the case of `FLG`, we want our struct to occupy exactly a single byte, so +// we specify `u8` as the backing integer. It's safe to convert between a packed +// container and its backing integer using the builtin `@bitCast`. +// The LZ4 spec also mandates that reserved bits must always be zero, so it's +// good practice to set `0` as a default value for `reserved`. +// +// The fields of a packed struct start at the least significant bit of its backing +// integer and end at its most significant bit. This is the case no matter what +// endianness our target has. +// +// Try to silence the complaints below: + +const Bits = packed struct(u4) { + a: u1 = 0, + b: u1 = 0, + c: u1 = 0, + d: u1 = 0, +}; + +pub fn main() void { + { + const expected: Bits = @bitCast(@as(u4, 0b1000)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0001)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0010)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0011)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b1101)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } +} + +// As we can see, equality comparisons (`==` and `!=`) work for packed structs. +// They also work for packed unions. However, since packed containers are not +// naturally ordered, we can't use any other comparison operators on them. +// +// It's also possible to use packed containers in `switch` statements, which we +// will cover in the next exercise! +// +// Since packed containers make very strong guarantees about their memory layout, +// only a handful of types are eligible to be part of them. +// The following types are allowed as field types: +// +// - integers +// - floats +// - bool +// - void +// - enums with explicit backing integers +// - packed unions +// - packed structs +// + +const std = @import("std"); +const assert = std.debug.assert; + +fn complain(my_bits: Bits, expected: Bits, src_loc: std.builtin.SourceLocation) void { + std.debug.print( + "That's not quite right! You've got 0b{b:0>4}, but we want 0b{b:0>4} in line {d}.\n", + .{ @as(u4, @bitCast(my_bits)), @as(u4, @bitCast(expected)), src_loc.line }, + ); +} + +// (†) https://github.com/lz4/lz4/blob/5c4c1fb2354133e1f3b087a341576985f8114bd5/doc/lz4_Frame_format.md#frame-descriptor diff --git a/exercises/115_packed2.zig b/exercises/115_packed2.zig new file mode 100644 index 0000000..2b6b558 --- /dev/null +++ b/exercises/115_packed2.zig @@ -0,0 +1,78 @@ +// +// We've already learned about switch statements in exercises 030, 031 and 108. +// They also work with packed containers: + +const S = packed struct(u2) { + a: bool, + b: i1, +}; + +// Try to make it compile without adding an `else` prong! + +comptime { + const s: S = .{ .a = true, .b = -1 }; + switch (s) { + .{ .a = true, .b = -1 } => {}, // ok! + .{ .a = true, .b = ??? }, + .{ .a = ???, .b = 0 }, + .{ .a = ???, .b = ??? }, + => @compileError("We don't want to end up here!"), + } +} + +// As we can see, switching on packed structs is pretty straightforward. +// When switching on packed unions however, we'll realize that a packed +// union never keeps track of its active tag, not even in debug mode! This +// means that packed unions compare solely by their bit pattern (again, just +// like integers). + +const U = packed union(u2) { + a: u2, + b: i2, +}; + +// Find and remove the duplicate case! + +comptime { + const u: U = .{ .a = 3 }; + switch (u) { + .{ .a = 3 } => {}, // ok! + .{ .a = 2 }, + .{ .b = 1 }, + .{ .b = -1 }, + .{ .a = 0 }, + => @compileError("We don't want to end up here!"), + } +} + +// Since packed unions don't have the concept of an active tag, it's always legal +// to access any of their fields. This can be useful to view the same data from +// different perspectives seamlessly. +// +// Try to make the float below negative: + +/// IEEE 754 half precision float +const Float = packed union(u16) { + value: f16, + bits: packed struct(u16) { + mantissa: u10, + exponent: u5, + sign: u1, + }, +}; + +pub fn main() void { + // Reminder: if the sign bit of a float is set, the number is negative! + + var number: Float = .{ .value = 2.34 }; + number.bits.??? = ???; + if (number.value != -2.34) { + std.debug.print("Make it negative!\n", .{}); + } +} + +// This concludes our introduction to packed containers. The next time you need +// control over individual bits, keep them in mind as a potent alternative! +// + +const std = @import("std"); 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/092_interfaces.patch deleted file mode 100644 index 36d8784..0000000 --- a/patches/patches/092_interfaces.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- exercises/092_interfaces.zig 2023-10-03 22:15:22.125574535 +0200 -+++ answers/092_interfaces.zig 2023-10-05 20:04:07.259437354 +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/093_hello_c.patch b/patches/patches/093_hello_c.patch deleted file mode 100644 index fe1f853..0000000 --- a/patches/patches/093_hello_c.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- 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 -@@ -54,7 +54,7 @@ - // - // In this exercise we use 'write' to output 17 chars, - // but something is still missing... -- const c_res = write(2, "Hello C from Zig!", 17); -+ const c_res = c.write(2, "Hello C from Zig!", 17); - - // let's see what the result from C is: - std.debug.print(" - C result is {d} chars written.\n", .{c_res}); diff --git a/patches/patches/094_c_math.patch b/patches/patches/094_c_math.patch deleted file mode 100644 index f8c7620..0000000 --- a/patches/patches/094_c_math.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- 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 -@@ -26,7 +26,7 @@ - - const c = @cImport({ - // What do we need here? -- ??? -+ @cInclude("math.h"); - }); - - pub fn main() !void { diff --git a/patches/patches/095_for3.patch b/patches/patches/095_for3.patch deleted file mode 100644 index a158b31..0000000 --- a/patches/patches/095_for3.patch +++ /dev/null @@ -1,20 +0,0 @@ ---- exercises/095_for3.zig 2026-02-27 19:33:59 -+++ answers/095_for3.zig 2026-02-27 19:33:38 -@@ -56,7 +56,7 @@ - - // I want to print every number between 1 and 20 that is NOT - // divisible by 3 or 5. -- for (???) |n| { -+ for (1..21) |n| { - - // The '%' symbol is the "modulo" operator and it - // returns the remainder after division. -@@ -68,7 +68,7 @@ - std.debug.print("\n", .{}); - - // Let's also print every number from 1 through 15 -- for (???) |n| { -+ for (1..16) |n| { - std.debug.print("{} ", .{n}); - } - std.debug.print("\n", .{}); diff --git a/patches/patches/095_interfaces.patch b/patches/patches/095_interfaces.patch new file mode 100644 index 0000000..cb5fd62 --- /dev/null +++ b/patches/patches/095_interfaces.patch @@ -0,0 +1,11 @@ +--- exercises/095_interfaces.zig 2025-08-15 15:17:57.839348063 +0200 ++++ answers/095_interfaces.zig 2026-04-03 13:09:13.722917764 +0200 +@@ -106,7 +106,7 @@ + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. +- ??? ++ insect.print(); + } + } + diff --git a/patches/patches/096_hello_c.patch b/patches/patches/096_hello_c.patch new file mode 100644 index 0000000..f5fe06f --- /dev/null +++ b/patches/patches/096_hello_c.patch @@ -0,0 +1,11 @@ +--- 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, + // but something is still missing... +- const c_res = write(2, "Hello C from Zig!", 17); ++ const c_res = c.write(2, "Hello C from Zig!", 17); + + // let's see what the result from C is: + std.debug.print(" - C result is {d} chars written.\n", .{c_res}); diff --git a/patches/patches/096_memory_allocation.patch b/patches/patches/096_memory_allocation.patch deleted file mode 100644 index c26eeeb..0000000 --- a/patches/patches/096_memory_allocation.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- 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 -@@ -64,7 +64,7 @@ - const allocator = arena.allocator(); - - // allocate memory for this array -- const avg: []f64 = ???; -+ const avg: []f64 = try allocator.alloc(f64, arr.len); - - runningAverage(arr, avg); - std.debug.print("Running Average: ", .{}); 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/097_c_math.patch b/patches/patches/097_c_math.patch new file mode 100644 index 0000000..917fe6b --- /dev/null +++ b/patches/patches/097_c_math.patch @@ -0,0 +1,11 @@ +--- 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({ + // What do we need here? +- ??? ++ @cInclude("math.h"); + }); + + pub fn main() !void { diff --git a/patches/patches/098_bit_manipulation2.patch b/patches/patches/098_bit_manipulation2.patch deleted file mode 100644 index 5354a7b..0000000 --- a/patches/patches/098_bit_manipulation2.patch +++ /dev/null @@ -1,9 +0,0 @@ ---- 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 -@@ -60,5 +60,5 @@ - // and if so, we know the given string is a pangram - // - // but what do we have to compare? -- return bits == 0x..???; -+ return bits == 0x3ffffff; - } diff --git a/patches/patches/098_for3.patch b/patches/patches/098_for3.patch new file mode 100644 index 0000000..c280bc6 --- /dev/null +++ b/patches/patches/098_for3.patch @@ -0,0 +1,20 @@ +--- 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 + // divisible by 3 or 5. +- for (???) |n| { ++ for (1..21) |n| { + + // The '%' symbol is the "modulo" operator and it + // returns the remainder after division. +@@ -68,7 +68,7 @@ + std.debug.print("\n", .{}); + + // Let's also print every number from 1 through 15 +- for (???) |n| { ++ for (1..16) |n| { + std.debug.print("{} ", .{n}); + } + std.debug.print("\n", .{}); diff --git a/patches/patches/099_formatting.patch b/patches/patches/099_formatting.patch deleted file mode 100644 index a56b556..0000000 --- a/patches/patches/099_formatting.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- exercises/099_formatting.zig 2024-11-07 21:45:10.459123650 +0100 -+++ answers/099_formatting.zig 2024-11-07 21:43:55.154345991 +0100 -@@ -131,7 +131,7 @@ - for (0..size) |b| { - // What formatting is needed here to make our columns - // nice and straight? -- print("{???} ", .{(a + 1) * (b + 1)}); -+ print("{d:>3} ", .{(a + 1) * (b + 1)}); - } - - // After each row we use double line feed: diff --git a/patches/patches/099_memory_allocation.patch b/patches/patches/099_memory_allocation.patch new file mode 100644 index 0000000..d83a0ae --- /dev/null +++ b/patches/patches/099_memory_allocation.patch @@ -0,0 +1,11 @@ +--- 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(); + + // allocate memory for this array +- const avg: []f64 = ???; ++ const avg: []f64 = try allocator.alloc(f64, arr.len); + + runningAverage(arr, avg); + std.debug.print("Running Average: ", .{}); 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/100_for4.patch b/patches/patches/100_for4.patch deleted file mode 100644 index ad73e9a..0000000 --- a/patches/patches/100_for4.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- exercises/100_for4.zig 2023-10-03 22:15:22.125574535 +0200 -+++ answers/100_for4.zig 2023-10-05 20:04:07.296104707 +0200 -@@ -39,7 +39,7 @@ - const hex_nums = [_]u8{ 0xb, 0x2a, 0x77 }; - const dec_nums = [_]u8{ 11, 42, 119 }; - -- for (hex_nums, ???) |hn, ???| { -+ for (hex_nums, dec_nums) |hn, dn| { - if (hn != dn) { - print("Uh oh! Found a mismatch: {d} vs {d}\n", .{ hn, dn }); - return; diff --git a/patches/patches/101_bit_manipulation2.patch b/patches/patches/101_bit_manipulation2.patch new file mode 100644 index 0000000..5179272 --- /dev/null +++ b/patches/patches/101_bit_manipulation2.patch @@ -0,0 +1,9 @@ +--- 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 + // + // but what do we have to compare? +- return bits == 0x..???; ++ return bits == 0x3ffffff; + } diff --git a/patches/patches/101_for5.patch b/patches/patches/101_for5.patch deleted file mode 100644 index 00f19df..0000000 --- a/patches/patches/101_for5.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- exercises/101_for5.zig 2023-10-03 22:15:22.125574535 +0200 -+++ answers/101_for5.zig 2023-10-05 20:04:07.299438103 +0200 -@@ -51,7 +51,7 @@ - - // We would like to number our list starting with 1, not 0. - // How do we do that? -- for (roles, gold, experience, ???) |c, g, e, i| { -+ for (roles, gold, experience, 1..) |c, g, e, i| { - const role_name = switch (c) { - .wizard => "Wizard", - .thief => "Thief", diff --git a/patches/patches/102_formatting.patch b/patches/patches/102_formatting.patch new file mode 100644 index 0000000..dba8aef --- /dev/null +++ b/patches/patches/102_formatting.patch @@ -0,0 +1,11 @@ +--- 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 + // nice and straight? +- print("{???} ", .{(a + 1) * (b + 1)}); ++ print("{d:>3} ", .{(a + 1) * (b + 1)}); + } + + // After each row we use double line feed: diff --git a/patches/patches/102_testing.patch b/patches/patches/102_testing.patch deleted file mode 100644 index 6d18bf6..0000000 --- a/patches/patches/102_testing.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- exercises/102_testing.zig 2025-10-24 12:54:56 -+++ answers/102_testing.zig 2025-10-24 12:56:33 -@@ -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. - test "sub" { -- try testing.expect(sub(10, 5) == 6); -+ try testing.expect(sub(10, 5) == 5); - - try testing.expect(sub(3, 1.5) == 1.5); - } -@@ -92,5 +92,5 @@ - - // Now we test if the function returns an error if we pass a zero as - // denominator. But which error needs to be tested? -- try testing.expectError(error.???, divide(15, 0)); -+ try testing.expectError(error.DivisionByZero, divide(15, 0)); - } diff --git a/patches/patches/103_for4.patch b/patches/patches/103_for4.patch new file mode 100644 index 0000000..7a7e161 --- /dev/null +++ b/patches/patches/103_for4.patch @@ -0,0 +1,11 @@ +--- 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 }; + +- for (hex_nums, ???) |hn, ???| { ++ for (hex_nums, dec_nums) |hn, dn| { + if (hn != dn) { + print("Uh oh! Found a mismatch: {d} vs {d}\n", .{ hn, dn }); + return; diff --git a/patches/patches/103_tokenization.patch b/patches/patches/103_tokenization.patch deleted file mode 100644 index 941ca3b..0000000 --- a/patches/patches/103_tokenization.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- exercises/103_tokenization.zig 2026-02-27 19:25:11 -+++ answers/103_tokenization.zig 2026-02-27 19:26:04 -@@ -134,7 +134,7 @@ - ; - - // now the tokenizer, but what do we need here? -- var it = std.mem.tokenizeAny(u8, poem, ???); -+ var it = std.mem.tokenizeAny(u8, poem, " ,;!\n"); - - // print all words and count them - var cnt: usize = 0; diff --git a/patches/patches/104_for5.patch b/patches/patches/104_for5.patch new file mode 100644 index 0000000..ef77013 --- /dev/null +++ b/patches/patches/104_for5.patch @@ -0,0 +1,11 @@ +--- 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. + // How do we do that? +- for (roles, gold, experience, ???) |c, g, e, i| { ++ for (roles, gold, experience, 1..) |c, g, e, i| { + const role_name = switch (c) { + .wizard => "Wizard", + .thief => "Thief", diff --git a/patches/patches/104_threading.patch b/patches/patches/104_threading.patch deleted file mode 100644 index e6fe0f4..0000000 --- a/patches/patches/104_threading.patch +++ /dev/null @@ -1,17 +0,0 @@ ---- 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 -- const handle2 = try std.Thread.spawn(.{}, thread_function, .{-4}); // that can't be right? -+ const handle2 = try std.Thread.spawn(.{}, thread_function, .{2}); - defer handle2.join(); - - // Third thread - const handle3 = try std.Thread.spawn(.{}, thread_function, .{3}); -- defer ??? // <-- something is missing -+ defer handle3.join(); - - // After the threads have been started, - // they run in parallel and we can still do some work in between. diff --git a/patches/patches/105_testing.patch b/patches/patches/105_testing.patch new file mode 100644 index 0000000..069b72b --- /dev/null +++ b/patches/patches/105_testing.patch @@ -0,0 +1,18 @@ +--- 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. + test "sub" { +- try testing.expect(sub(10, 5) == 6); ++ try testing.expect(sub(10, 5) == 5); + + try testing.expect(sub(3, 1.5) == 1.5); + } +@@ -92,5 +92,5 @@ + + // Now we test if the function returns an error if we pass a zero as + // denominator. But which error needs to be tested? +- try testing.expectError(error.???, divide(15, 0)); ++ try testing.expectError(error.DivisionByZero, divide(15, 0)); + } diff --git a/patches/patches/105_threading2.patch b/patches/patches/105_threading2.patch deleted file mode 100644 index dfa5613..0000000 --- a/patches/patches/105_threading2.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- exercises/105_threading2.zig 2024-03-23 16:35:14.754540802 +0100 -+++ answers/105_threading2.zig 2024-03-23 16:38:00.577539733 +0100 -@@ -81,8 +81,8 @@ - defer handle1.join(); - - // Second thread to calculate the minus numbers. -- ??? -- -+ const handle2 = try std.Thread.spawn(.{}, thread_pi, .{ &pi_minus, 3, count }); -+ defer handle2.join(); - } - // Here we add up the results. - std.debug.print("PI ≈ {d:.8}\n", .{4 + pi_plus - pi_minus}); diff --git a/patches/patches/106_files.patch b/patches/patches/106_files.patch deleted file mode 100644 index 27bcb56..0000000 --- a/patches/patches/106_files.patch +++ /dev/null @@ -1,20 +0,0 @@ ---- exercises/106_files.zig 2026-01-09 22:41:19.373872684 +0100 -+++ answers/106_files.zig 2026-01-09 22:41:44.518372910 +0100 -@@ -41,7 +41,7 @@ - // by doing nothing - // - // we want to catch error.PathAlreadyExists and do nothing -- ??? => {}, -+ error.PathAlreadyExists => {}, - // if there's any other unexpected error we just propagate it through - else => return e, - }; -@@ -61,7 +61,7 @@ - // but here we are not yet done writing to the file - // if only there were a keyword in Zig that - // allowed you to "defer" code execution to the end of the scope... -- file.close(io); -+ defer file.close(io); - - // you are not allowed to move these lines above the file closing line! - var file_writer = file.writer(io, &.{}); diff --git a/patches/patches/106_tokenization.patch b/patches/patches/106_tokenization.patch new file mode 100644 index 0000000..4792df6 --- /dev/null +++ b/patches/patches/106_tokenization.patch @@ -0,0 +1,11 @@ +--- 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 @@ + ; + + // now the tokenizer, but what do we need here? +- var it = std.mem.tokenizeAny(u8, poem, ???); ++ var it = std.mem.tokenizeAny(u8, poem, " ,;!\n"); + + // print all words and count them + var cnt: usize = 0; diff --git a/patches/patches/107_files2.patch b/patches/patches/107_files2.patch deleted file mode 100644 index 6820742..0000000 --- a/patches/patches/107_files2.patch +++ /dev/null @@ -1,26 +0,0 @@ ---- exercises/107_files2.zig 2026-01-09 22:43:15.211177186 +0100 -+++ answers/107_files2.zig 2026-01-09 22:42:48.943654602 +0100 -@@ -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 - // fix the initialization below -- var content = ['A']*64; -+ var content = [_]u8{'A'} ** 64; - // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` - std.debug.print("{s}\n", .{content}); - -@@ -50,12 +50,12 @@ - // can you go here to find a way to read the content? - // https://ziglang.org/documentation/master/std/#std.Io.Reader - // hint: look for a method that reads into a slice -- const bytes_read = zig_read_the_file_or_i_will_fight_you(&content); -+ const bytes_read = try reader.readSliceShort(&content); - - // Woah, too screamy. I know you're excited for zigling time but tone it down a bit. - // Can you print only what we read from the file? - std.debug.print("Successfully Read {d} bytes: {s}\n", .{ - bytes_read, -- content, // change this line only -+ content[0..bytes_read], // change this line only - }); - } diff --git a/patches/patches/107_threading.patch b/patches/patches/107_threading.patch new file mode 100644 index 0000000..3f65ef4 --- /dev/null +++ b/patches/patches/107_threading.patch @@ -0,0 +1,17 @@ +--- 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(); + + // Second thread +- const handle2 = try std.Thread.spawn(.{}, thread_function, .{-4}); // that can't be right? ++ const handle2 = try std.Thread.spawn(.{}, thread_function, .{2}); + defer handle2.join(); + + // Third thread + const handle3 = try std.Thread.spawn(.{}, thread_function, .{3}); +- defer ??? // <-- something is missing ++ defer handle3.join(); + + // After the threads have been started, + // they run in parallel and we can still do some work in between. diff --git a/patches/patches/108_labeled_switch.patch b/patches/patches/108_labeled_switch.patch deleted file mode 100644 index fa9dd67..0000000 --- a/patches/patches/108_labeled_switch.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- 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 @@ - // how would you fix it? - pr: switch (PullRequestState.Draft) { - PullRequestState.Draft => continue :pr PullRequestState.InReview, -- PullRequestState.InReview => continue :pr PullRequestState.Rejected, -+ PullRequestState.InReview => continue :pr PullRequestState.Approved, - PullRequestState.Approved => continue :pr PullRequestState.Merged, - PullRequestState.Rejected => { - std.debug.print("The pull request has been rejected.\n", .{}); - return; - }, -- PullRequestState.Merged => break, // Would you know where to break to? -+ PullRequestState.Merged => break :pr, // Would you know where to break to? - } - std.debug.print("The pull request has been merged.\n", .{}); - } diff --git a/patches/patches/108_threading2.patch b/patches/patches/108_threading2.patch new file mode 100644 index 0000000..d71f254 --- /dev/null +++ b/patches/patches/108_threading2.patch @@ -0,0 +1,13 @@ +--- 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(); + + // Second thread to calculate the minus numbers. +- ??? +- ++ const handle2 = try std.Thread.spawn(.{}, thread_pi, .{ &pi_minus, 3, count }); ++ defer handle2.join(); + } + // Here we add up the results. + std.debug.print("PI ≈ {d:.8}\n", .{4 + pi_plus - pi_minus}); diff --git a/patches/patches/109_files.patch b/patches/patches/109_files.patch new file mode 100644 index 0000000..ac59d70 --- /dev/null +++ b/patches/patches/109_files.patch @@ -0,0 +1,20 @@ +--- 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 + // + // we want to catch error.PathAlreadyExists and do nothing +- ??? => {}, ++ error.PathAlreadyExists => {}, + // if there's any other unexpected error we just propagate it through + else => return e, + }; +@@ -61,7 +61,7 @@ + // but here we are not yet done writing to the file + // if only there were a keyword in Zig that + // allowed you to "defer" code execution to the end of the scope... +- file.close(io); ++ defer file.close(io); + + // you are not allowed to move these lines above the file closing line! + var file_writer = file.writer(io, &.{}); diff --git a/patches/patches/109_vectors.patch b/patches/patches/109_vectors.patch deleted file mode 100644 index bf18cc0..0000000 --- a/patches/patches/109_vectors.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- exercises/109_vectors.zig 2024-11-07 14:57:09.673383618 +0100 -+++ answers/109_vectors.zig 2024-11-07 14:22:59.069150138 +0100 -@@ -121,8 +121,8 @@ - - const Vec4 = @Vector(4, f32); - fn calcMaxPairwiseDiffNew(a: Vec4, b: Vec4) f32 { -- const abs_diff_vec = ???; -- const max_diff = @reduce(???, abs_diff_vec); -+ const abs_diff_vec = @abs(a - b); -+ const max_diff = @reduce(.Max, abs_diff_vec); - return max_diff; - } - diff --git a/patches/patches/110_files2.patch b/patches/patches/110_files2.patch new file mode 100644 index 0000000..64dc193 --- /dev/null +++ b/patches/patches/110_files2.patch @@ -0,0 +1,26 @@ +--- 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 + // fix the initialization below +- var content = ['A']*64; ++ var content = [_]u8{'A'} ** 64; + // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` + std.debug.print("{s}\n", .{content}); + +@@ -50,12 +50,12 @@ + // can you go here to find a way to read the content? + // https://ziglang.org/documentation/master/std/#std.Io.Reader + // hint: look for a method that reads into a slice +- const bytes_read = zig_read_the_file_or_i_will_fight_you(&content); ++ const bytes_read = try reader.readSliceShort(&content); + + // Woah, too screamy. I know you're excited for zigling time but tone it down a bit. + // Can you print only what we read from the file? + std.debug.print("Successfully Read {d} bytes: {s}\n", .{ + bytes_read, +- content, // change this line only ++ content[0..bytes_read], // change this line only + }); + } diff --git a/patches/patches/110_quiz9.patch b/patches/patches/110_quiz9.patch deleted file mode 100644 index 9d9b864..0000000 --- a/patches/patches/110_quiz9.patch +++ /dev/null @@ -1,56 +0,0 @@ ---- 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 @@ - PORTB = 0b1100; - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("^ {b:0>4} // (bitmask)\n", .{0b0101}); -- PORTB ^= (1 << 1) | (1 << 0); // What's wrong here? -+ PORTB ^= (1 << 2) | (1 << 0); - checkAnswer(0b1001, PORTB); - - newline(); -@@ -116,7 +116,7 @@ - PORTB = 0b1100; - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("^ {b:0>4} // (bitmask)\n", .{0b0011}); -- PORTB ^= (1 << 1) & (1 << 0); // What's wrong here? -+ PORTB ^= (1 << 1) | (1 << 0); - checkAnswer(0b1111, PORTB); - - newline(); -@@ -170,7 +170,7 @@ - PORTB = 0b1001; // reset PORTB - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("| {b:0>4} // (bitmask)\n", .{0b0100}); -- PORTB = PORTB ??? (1 << 2); // What's missing here? -+ PORTB = PORTB | (1 << 2); - checkAnswer(0b1101, PORTB); - - newline(); -@@ -178,7 +178,7 @@ - PORTB = 0b1001; // reset PORTB - print(" {b:0>4} // (reset state)\n", .{PORTB}); - print("| {b:0>4} // (bitmask)\n", .{0b0100}); -- PORTB ??? (1 << 2); // What's missing here? -+ PORTB |= (1 << 2); - checkAnswer(0b1101, PORTB); - - newline(); -@@ -269,7 +269,7 @@ - PORTB = 0b1110; // reset PORTB - print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); - print("& {b:0>4} // (bitmask)\n", .{0b1011}); -- PORTB = PORTB & ???@as(u4, 1 << 2); // What character is missing here? -+ PORTB = PORTB & ~@as(u4, 1 << 2); - checkAnswer(0b1010, PORTB); - - newline(); -@@ -277,7 +277,7 @@ - PORTB = 0b0111; // reset PORTB - print(" {b:0>4} // (reset state)\n", .{PORTB}); - print("& {b:0>4} // (bitmask)\n", .{0b1110}); -- PORTB &= ~(1 << 0); // What's missing here? -+ PORTB &= ~@as(u4, 1 << 0); - checkAnswer(0b0110, PORTB); - - newline(); diff --git a/patches/patches/111_labeled_switch.patch b/patches/patches/111_labeled_switch.patch new file mode 100644 index 0000000..0fb872d --- /dev/null +++ b/patches/patches/111_labeled_switch.patch @@ -0,0 +1,18 @@ +--- 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, +- PullRequestState.InReview => continue :pr PullRequestState.Rejected, ++ PullRequestState.InReview => continue :pr PullRequestState.Approved, + PullRequestState.Approved => continue :pr PullRequestState.Merged, + PullRequestState.Rejected => { + std.debug.print("The pull request has been rejected.\n", .{}); + return; + }, +- PullRequestState.Merged => break, // Would you know where to break to? ++ PullRequestState.Merged => break :pr, // Would you know where to break to? + } + std.debug.print("The pull request has been merged.\n", .{}); + } diff --git a/patches/patches/111_packed.patch b/patches/patches/111_packed.patch deleted file mode 100644 index d38ac68..0000000 --- a/patches/patches/111_packed.patch +++ /dev/null @@ -1,57 +0,0 @@ ---- exercises/111_packed.zig 2026-03-13 11:18:44 -+++ answers/111_packed.zig 2026-03-13 11:18:57 -@@ -41,7 +41,7 @@ - - const PackedStruct = packed struct { - a: u2, -- b: u?, -+ b: u4, - }; - - comptime { -@@ -50,7 +50,7 @@ - - const PackedUnion = packed union { - a: bool, -- b: u?, -+ b: u1, - }; - - comptime { -@@ -113,31 +113,31 @@ - pub fn main() void { - { - const expected: Bits = @bitCast(@as(u4, 0b1000)); -- const my_bits: Bits = .{}; -+ const my_bits: Bits = .{ .d = 1 }; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0001)); -- const my_bits: Bits = .{}; -+ const my_bits: Bits = .{ .a = 1 }; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0010)); -- const my_bits: Bits = .{}; -+ const my_bits: Bits = .{ .b = 1 }; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b0011)); -- const my_bits: Bits = .{}; -+ const my_bits: Bits = .{ .a = 1, .b = 1 }; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - - { - const expected: Bits = @bitCast(@as(u4, 0b1101)); -- const my_bits: Bits = .{}; -+ const my_bits: Bits = .{ .a = 1, .c = 1, .d = 1 }; - if (my_bits != expected) complain(my_bits, expected, @src()); - } - } diff --git a/patches/patches/112_packed2.patch b/patches/patches/112_packed2.patch deleted file mode 100644 index 9b01eb3..0000000 --- a/patches/patches/112_packed2.patch +++ /dev/null @@ -1,32 +0,0 @@ ---- exercises/112_packed2.zig 2026-03-13 11:14:08 -+++ answers/112_packed2.zig 2026-03-13 11:14:16 -@@ -13,9 +13,9 @@ - const s: S = .{ .a = true, .b = -1 }; - switch (s) { - .{ .a = true, .b = -1 } => {}, // ok! -- .{ .a = true, .b = ??? }, -- .{ .a = ???, .b = 0 }, -- .{ .a = ???, .b = ??? }, -+ .{ .a = true, .b = 0 }, -+ .{ .a = false, .b = 0 }, -+ .{ .a = false, .b = -1 }, - => @compileError("We don't want to end up here!"), - } - } -@@ -39,7 +39,6 @@ - .{ .a = 3 } => {}, // ok! - .{ .a = 2 }, - .{ .b = 1 }, -- .{ .b = -1 }, - .{ .a = 0 }, - => @compileError("We don't want to end up here!"), - } -@@ -65,7 +64,7 @@ - // Reminder: if the sign bit of a float is set, the number is negative! - - var number: Float = .{ .value = 2.34 }; -- number.bits.??? = ???; -+ number.bits.sign = 1; - if (number.value != -2.34) { - std.debug.print("Make it negative!\n", .{}); - } diff --git a/patches/patches/112_vectors.patch b/patches/patches/112_vectors.patch new file mode 100644 index 0000000..5bf684b --- /dev/null +++ b/patches/patches/112_vectors.patch @@ -0,0 +1,13 @@ +--- 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); + fn calcMaxPairwiseDiffNew(a: Vec4, b: Vec4) f32 { +- const abs_diff_vec = ???; +- const max_diff = @reduce(???, abs_diff_vec); ++ const abs_diff_vec = @abs(a - b); ++ const max_diff = @reduce(.Max, abs_diff_vec); + return max_diff; + } + diff --git a/patches/patches/113_quiz9.patch b/patches/patches/113_quiz9.patch new file mode 100644 index 0000000..03bd572 --- /dev/null +++ b/patches/patches/113_quiz9.patch @@ -0,0 +1,56 @@ +--- 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}); +- PORTB ^= (1 << 1) | (1 << 0); // What's wrong here? ++ PORTB ^= (1 << 2) | (1 << 0); + checkAnswer(0b1001, PORTB); + + newline(); +@@ -92,7 +92,7 @@ + PORTB = 0b1100; + print(" {b:0>4} // (initial state of PORTB)\n", .{PORTB}); + print("^ {b:0>4} // (bitmask)\n", .{0b0011}); +- PORTB ^= (1 << 1) & (1 << 0); // What's wrong here? ++ PORTB ^= (1 << 1) | (1 << 0); + checkAnswer(0b1111, PORTB); + + newline(); +@@ -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}); +- PORTB = PORTB ??? (1 << 2); // What's missing here? ++ PORTB = PORTB | (1 << 2); + checkAnswer(0b1101, PORTB); + + newline(); +@@ -111,7 +111,7 @@ + PORTB = 0b1001; // reset PORTB + print(" {b:0>4} // (reset state)\n", .{PORTB}); + print("| {b:0>4} // (bitmask)\n", .{0b0100}); +- PORTB ??? (1 << 2); // What's missing here? ++ PORTB |= (1 << 2); + checkAnswer(0b1101, PORTB); + + newline(); +@@ -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}); +- PORTB = PORTB & ???@as(u4, 1 << 2); // What character is missing here? ++ PORTB = PORTB & ~@as(u4, 1 << 2); + checkAnswer(0b1010, PORTB); + + newline(); +@@ -130,7 +130,7 @@ + PORTB = 0b0111; // reset PORTB + print(" {b:0>4} // (reset state)\n", .{PORTB}); + print("& {b:0>4} // (bitmask)\n", .{0b1110}); +- PORTB &= ~(1 << 0); // What's missing here? ++ PORTB &= ~@as(u4, 1 << 0); + checkAnswer(0b0110, PORTB); + + newline(); diff --git a/patches/patches/114_packed.patch b/patches/patches/114_packed.patch new file mode 100644 index 0000000..3c79f18 --- /dev/null +++ b/patches/patches/114_packed.patch @@ -0,0 +1,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 { + a: u2, +- b: u?, ++ b: u4, + }; + + comptime { +@@ -50,7 +50,7 @@ + + const PackedUnion = packed union { + a: bool, +- b: u?, ++ b: u1, + }; + + comptime { +@@ -113,31 +113,31 @@ + pub fn main() void { + { + const expected: Bits = @bitCast(@as(u4, 0b1000)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .d = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0001)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0010)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .b = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0011)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1, .b = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b1101)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1, .c = 1, .d = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + } diff --git a/patches/patches/115_packed2.patch b/patches/patches/115_packed2.patch new file mode 100644 index 0000000..81c32ab --- /dev/null +++ b/patches/patches/115_packed2.patch @@ -0,0 +1,32 @@ +--- 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) { + .{ .a = true, .b = -1 } => {}, // ok! +- .{ .a = true, .b = ??? }, +- .{ .a = ???, .b = 0 }, +- .{ .a = ???, .b = ??? }, ++ .{ .a = true, .b = 0 }, ++ .{ .a = false, .b = 0 }, ++ .{ .a = false, .b = -1 }, + => @compileError("We don't want to end up here!"), + } + } +@@ -39,7 +39,6 @@ + .{ .a = 3 } => {}, // ok! + .{ .a = 2 }, + .{ .b = 1 }, +- .{ .b = -1 }, + .{ .a = 0 }, + => @compileError("We don't want to end up here!"), + } +@@ -65,7 +64,7 @@ + // Reminder: if the sign bit of a float is set, the number is negative! + + var number: Float = .{ .value = 2.34 }; +- number.bits.??? = ???; ++ number.bits.sign = 1; + if (number.value != -2.34) { + std.debug.print("Make it negative!\n", .{}); + } -- cgit v1.2.3 From 903c33cd0a6dfdc050e8575f64aa60a9a09001e0 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 13:46:35 +0200 Subject: 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 (limited to 'exercises') 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", .{}); + -- cgit v1.2.3 From 25009361533be5e45cf59d9840edf5d13cfb8d6d Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 14:28:19 +0200 Subject: 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 (limited to 'exercises') 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! -- cgit v1.2.3 From 1c6487c1e79cbe0d59a39b483af8ec44b59c586e Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 18:11:00 +0200 Subject: added async-io quiz --- build.zig | 46 +++++---- exercises/084_async.zig | 48 --------- exercises/084_interfaces.zig | 127 ++++++++++++++++++++++++ exercises/085_async.zig | 48 +++++++++ exercises/085_async2.zig | 48 --------- exercises/086_async2.zig | 48 +++++++++ exercises/086_async3.zig | 50 ---------- exercises/087_async3.zig | 50 ++++++++++ exercises/087_async4.zig | 50 ---------- exercises/088_async4.zig | 50 ++++++++++ exercises/088_async5.zig | 61 ------------ exercises/089_async5.zig | 61 ++++++++++++ exercises/089_async6.zig | 71 ------------- exercises/090_async6.zig | 71 +++++++++++++ exercises/090_async7.zig | 57 ----------- exercises/091_async7.zig | 57 +++++++++++ exercises/091_async8.zig | 62 ------------ exercises/092_async8.zig | 62 ++++++++++++ exercises/092_async9.zig | 57 ----------- exercises/093_async10.zig | 67 ------------- exercises/093_async9.zig | 57 +++++++++++ exercises/094_async10.zig | 67 +++++++++++++ exercises/095_interfaces.zig | 127 ------------------------ exercises/095_quiz_async.zig | 186 +++++++++++++++++++++++++++++++++++ patches/patches/084_interfaces.patch | 11 +++ patches/patches/085_async.patch | 11 +++ patches/patches/086_async2.patch | 14 +++ patches/patches/087_async3.patch | 18 ++++ patches/patches/088_async4.patch | 11 +++ patches/patches/089_async5.patch | 11 +++ patches/patches/090_async6.patch | 11 +++ patches/patches/091_async7.patch | 13 +++ patches/patches/092_async8.patch | 11 +++ patches/patches/093_async9.patch | 11 +++ patches/patches/094_async10.patch | 13 +++ patches/patches/095_quiz_async.patch | 52 ++++++++++ 36 files changed, 1097 insertions(+), 718 deletions(-) delete mode 100644 exercises/084_async.zig create mode 100644 exercises/084_interfaces.zig create mode 100644 exercises/085_async.zig delete mode 100644 exercises/085_async2.zig create mode 100644 exercises/086_async2.zig delete mode 100644 exercises/086_async3.zig create mode 100644 exercises/087_async3.zig delete mode 100644 exercises/087_async4.zig create mode 100644 exercises/088_async4.zig delete mode 100644 exercises/088_async5.zig create mode 100644 exercises/089_async5.zig delete mode 100644 exercises/089_async6.zig create mode 100644 exercises/090_async6.zig delete mode 100644 exercises/090_async7.zig create mode 100644 exercises/091_async7.zig delete mode 100644 exercises/091_async8.zig create mode 100644 exercises/092_async8.zig delete mode 100644 exercises/092_async9.zig delete mode 100644 exercises/093_async10.zig create mode 100644 exercises/093_async9.zig create mode 100644 exercises/094_async10.zig delete mode 100644 exercises/095_interfaces.zig create mode 100644 exercises/095_quiz_async.zig create mode 100644 patches/patches/084_interfaces.patch create mode 100644 patches/patches/085_async.patch create mode 100644 patches/patches/086_async2.patch create mode 100644 patches/patches/087_async3.patch create mode 100644 patches/patches/088_async4.patch create mode 100644 patches/patches/089_async5.patch create mode 100644 patches/patches/090_async6.patch create mode 100644 patches/patches/091_async7.patch create mode 100644 patches/patches/092_async8.patch create mode 100644 patches/patches/093_async9.patch create mode 100644 patches/patches/094_async10.patch create mode 100644 patches/patches/095_quiz_async.patch (limited to 'exercises') diff --git a/build.zig b/build.zig index b9aaac3..beb0d72 100644 --- a/build.zig +++ b/build.zig @@ -1109,20 +1109,29 @@ const exercises = [_]Exercise{ .main_file = "083_anonymous_lists.zig", .output = "I say hello!", }, + .{ + .main_file = "084_interfaces.zig", + .output = + \\Daily Insect Report: + \\Ant is alive. + \\Bee visited 17 flowers. + \\Grasshopper hopped 32 meters. + , // pay attention to the comma + }, // Skipped because of https://github.com/ratfactor/ziglings/issues/163 // direct link: https://github.com/ziglang/zig/issues/6025 .{ - .main_file = "084_async.zig", + .main_file = "085_async.zig", .output = "Current time: s since epoch", .timestamp = true, }, .{ - .main_file = "085_async2.zig", + .main_file = "086_async2.zig", .output = "Computing... the answer is: 42", }, .{ - .main_file = "086_async3.zig", + .main_file = "087_async3.zig", .output = \\1 + 2 = 3 \\6 * 7 = 42 @@ -1130,7 +1139,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "087_async4.zig", + .main_file = "088_async4.zig", .output = \\Task 1 done. \\Task 2 done. @@ -1139,7 +1148,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "088_async5.zig", + .main_file = "089_async5.zig", .output = \\Starting long computation... \\Canceling slow task... @@ -1148,19 +1157,19 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "089_async6.zig", + .main_file = "090_async6.zig", .output = "Hare: I'm fast!", }, .{ - .main_file = "090_async7.zig", + .main_file = "091_async7.zig", .output = "Counter: 400", }, .{ - .main_file = "091_async8.zig", + .main_file = "092_async8.zig", .output = "Sum of 1..10 = 55", }, .{ - .main_file = "092_async9.zig", + .main_file = "093_async9.zig", .output = \\Main thread continues... \\Computing on a separate thread! @@ -1169,7 +1178,7 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "093_async10.zig", + .main_file = "094_async10.zig", .output = \\Starting critical section... \\Critical section completed safely. @@ -1177,17 +1186,14 @@ const exercises = [_]Exercise{ , // pay attention to the comma }, .{ - .main_file = "094_async_quiz.zig", - .output = "", - .skip = true, - }, - .{ - .main_file = "095_interfaces.zig", + .main_file = "095_quiz_async.zig", .output = - \\Daily Insect Report: - \\Ant is alive. - \\Bee visited 17 flowers. - \\Grasshopper hopped 32 meters. + \\=== Doctor Zoraptera's Garden Report === + \\Temperature : 23C + \\Humidity : 63% + \\Wind : 13 km/h + \\Readings : 9 + \\Bee-friendly conditions! Expect high pollination. , // pay attention to the comma }, .{ diff --git a/exercises/084_async.zig b/exercises/084_async.zig deleted file mode 100644 index 48bda2b..0000000 --- a/exercises/084_async.zig +++ /dev/null @@ -1,48 +0,0 @@ -// -// In previous versions of Zig, async/await used special keywords -// like 'suspend', 'resume', and 'async' that operated on stackframes -// directly. Those keywords no longer exist! -// -// Zig 0.16 replaced them with a unified I/O interface: std.Io. -// This interface uses a VTable pattern - a struct of function pointers - -// to abstract over different concurrency backends: -// -// * Threaded - classic thread-pool based I/O -// * Uring - Linux io_uring -// * Kqueue - BSD/macOS -// * Dispatch - macOS Grand Central Dispatch -// -// The Io struct itself is tiny: -// -// const Io = struct { -// userdata: ?*anyopaque, // opaque state of the backend -// vtable: *const VTable, // table of function pointers -// }; -// -// Your code receives an Io value and calls methods on it. -// The backend is chosen at initialization time - your code doesn't -// need to know which one it is! -// -// In Zig 0.16, main() receives a std.process.Init struct to opt -// into I/O and concurrency support: -// -// pub fn main(init: std.process.Init) !void { -// const io = init.io; -// // ... use io ... -// } -// -// Let's start simple. Fix the main function to extract the Io -// interface from init, then use it to get the current time. -// -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.???; - - // Get the current wall-clock time using the Io interface. - // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). - const timestamp = std.Io.Timestamp.now(io, .real); - - // Print the timestamp in seconds since the Unix epoch. - std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); -} diff --git a/exercises/084_interfaces.zig b/exercises/084_interfaces.zig new file mode 100644 index 0000000..7775dd5 --- /dev/null +++ b/exercises/084_interfaces.zig @@ -0,0 +1,127 @@ +// +// Remember our ant and bee simulator constructed with unions +// back in exercises 55 and 56? There, we demonstrated that +// unions allow us to treat different data types in a uniform +// manner. +// +// One neat feature was using tagged unions to create a single +// function to print a status for ants *or* bees by switching: +// +// switch (insect) { +// .still_alive => ... // (print ant stuff) +// .flowers_visited => ... // (print bee stuff) +// } +// +// Well, that simulation was running just fine until a new insect +// arrived in the virtual garden, a grasshopper! +// +// Doctor Zoraptera started to add grasshopper code to the +// program, but then she backed away from her keyboard with an +// angry hissing sound. She had realized that having code for +// each insect in one place and code to print each insect in +// another place was going to become unpleasant to maintain when +// the simulation expanded to hundreds of different insects. +// +// Thankfully, Zig has another comptime feature we can use +// to get out of this dilemma called the 'inline else'. +// +// We can replace this redundant code: +// +// switch (thing) { +// .a => |a| special(a), +// .b => |b| normal(b), +// .c => |c| normal(c), +// .d => |d| normal(d), +// .e => |e| normal(e), +// ... +// } +// +// With: +// +// switch (thing) { +// .a => |a| special(a), +// inline else => |t| normal(t), +// } +// +// We can have special handling of some cases and then Zig +// handles the rest of the matches for us. +// +// With this feature, you decide to make an Insect union with a +// single uniform 'print()' function. All of the insects can +// then be responsible for printing themselves. And Doctor +// Zoraptera can calm down and stop gnawing on the furniture. +// +const std = @import("std"); + +const Ant = struct { + still_alive: bool, + + pub fn print(self: Ant) void { + std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); + } +}; + +const Bee = struct { + flowers_visited: u16, + + pub fn print(self: Bee) void { + std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); + } +}; + +// Here's the new grasshopper. Notice how we've also added print +// methods to each insect. +const Grasshopper = struct { + distance_hopped: u16, + + pub fn print(self: Grasshopper) void { + std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); + } +}; + +const Insect = union(enum) { + ant: Ant, + bee: Bee, + grasshopper: Grasshopper, + + // Thanks to 'inline else', we can think of this print() as + // being an interface method. Any member of this union with + // a print() method can be treated uniformly by outside + // code without needing to know any other details. Cool! + pub fn print(self: Insect) void { + switch (self) { + inline else => |case| return case.print(), + } + } +}; + +pub fn main() !void { + const my_insects = [_]Insect{ + Insect{ .ant = Ant{ .still_alive = true } }, + Insect{ .bee = Bee{ .flowers_visited = 17 } }, + Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, + }; + + std.debug.print("Daily Insect Report:\n", .{}); + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. + ??? + } +} + +// Our print() method in the Insect union above demonstrates +// something very similar to the object-oriented concept of an +// abstract data type. That is, the Insect type doesn't contain +// the underlying data, and the print() function doesn't +// actually do the printing. +// +// The point of an interface is to support generic programming: +// the ability to treat different things as if they were the +// same to cut down on clutter and conceptual complexity. +// +// The Daily Insect Report doesn't need to worry about *which* +// insects are in the report - they all print the same way via +// the interface! +// +// Doctor Zoraptera loves it. diff --git a/exercises/085_async.zig b/exercises/085_async.zig new file mode 100644 index 0000000..48bda2b --- /dev/null +++ b/exercises/085_async.zig @@ -0,0 +1,48 @@ +// +// In previous versions of Zig, async/await used special keywords +// like 'suspend', 'resume', and 'async' that operated on stackframes +// directly. Those keywords no longer exist! +// +// Zig 0.16 replaced them with a unified I/O interface: std.Io. +// This interface uses a VTable pattern - a struct of function pointers - +// to abstract over different concurrency backends: +// +// * Threaded - classic thread-pool based I/O +// * Uring - Linux io_uring +// * Kqueue - BSD/macOS +// * Dispatch - macOS Grand Central Dispatch +// +// The Io struct itself is tiny: +// +// const Io = struct { +// userdata: ?*anyopaque, // opaque state of the backend +// vtable: *const VTable, // table of function pointers +// }; +// +// Your code receives an Io value and calls methods on it. +// The backend is chosen at initialization time - your code doesn't +// need to know which one it is! +// +// In Zig 0.16, main() receives a std.process.Init struct to opt +// into I/O and concurrency support: +// +// pub fn main(init: std.process.Init) !void { +// const io = init.io; +// // ... use io ... +// } +// +// Let's start simple. Fix the main function to extract the Io +// interface from init, then use it to get the current time. +// +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.???; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). + const timestamp = std.Io.Timestamp.now(io, .real); + + // Print the timestamp in seconds since the Unix epoch. + std.debug.print("Current time: {}s since epoch\n", .{timestamp.toSeconds()}); +} diff --git a/exercises/085_async2.zig b/exercises/085_async2.zig deleted file mode 100644 index 1f1c4c8..0000000 --- a/exercises/085_async2.zig +++ /dev/null @@ -1,48 +0,0 @@ -// -// Now that we know how to get an Io value, let's use it for -// asynchronous execution! -// -// io.async() launches a function and returns a Future. The result -// won't necessarily be available until you call .await() on it: -// -// var future = io.async(someFunction, .{ arg1, arg2 }); -// // ... do other work here ... -// const result = future.await(io); -// -// The function *may* run immediately or on another thread - -// your code doesn't need to care! That's the beauty of the -// Io abstraction. (In the Threaded backend, if no thread is -// available, the function runs synchronously right away and -// .await() just returns the already-computed result.) -// -// io.async() returns a Future(T) where T is the return type -// of the function you passed in. Future has two key methods: -// -// .await(io) - block until the result is ready, return it -// .cancel(io) - request cancellation, then return the result -// -// Fix this program so that computeAnswer runs asynchronously -// and its result is properly awaited. -// -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch computeAnswer asynchronously. - // io.async() takes a function and a tuple of its arguments. - var future = io.async(computeAnswer, .{ 6, 7 }); - - // Meanwhile, print something to show we're not blocked. - std.debug.print("Computing... ", .{}); - - // Now collect the result. What method on Future gives us - // the value, blocking if it isn't ready yet? - const answer = future.???(io); - - std.debug.print("The answer is: {}\n", .{answer}); -} - -fn computeAnswer(a: u32, b: u32) u32 { - return a * b; -} diff --git a/exercises/086_async2.zig b/exercises/086_async2.zig new file mode 100644 index 0000000..1f1c4c8 --- /dev/null +++ b/exercises/086_async2.zig @@ -0,0 +1,48 @@ +// +// Now that we know how to get an Io value, let's use it for +// asynchronous execution! +// +// io.async() launches a function and returns a Future. The result +// won't necessarily be available until you call .await() on it: +// +// var future = io.async(someFunction, .{ arg1, arg2 }); +// // ... do other work here ... +// const result = future.await(io); +// +// The function *may* run immediately or on another thread - +// your code doesn't need to care! That's the beauty of the +// Io abstraction. (In the Threaded backend, if no thread is +// available, the function runs synchronously right away and +// .await() just returns the already-computed result.) +// +// io.async() returns a Future(T) where T is the return type +// of the function you passed in. Future has two key methods: +// +// .await(io) - block until the result is ready, return it +// .cancel(io) - request cancellation, then return the result +// +// Fix this program so that computeAnswer runs asynchronously +// and its result is properly awaited. +// +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch computeAnswer asynchronously. + // io.async() takes a function and a tuple of its arguments. + var future = io.async(computeAnswer, .{ 6, 7 }); + + // Meanwhile, print something to show we're not blocked. + std.debug.print("Computing... ", .{}); + + // Now collect the result. What method on Future gives us + // the value, blocking if it isn't ready yet? + const answer = future.???(io); + + std.debug.print("The answer is: {}\n", .{answer}); +} + +fn computeAnswer(a: u32, b: u32) u32 { + return a * b; +} diff --git a/exercises/086_async3.zig b/exercises/086_async3.zig deleted file mode 100644 index 07221e9..0000000 --- a/exercises/086_async3.zig +++ /dev/null @@ -1,50 +0,0 @@ -// -// The real power of async shows when you launch MULTIPLE tasks! -// -// With io.async(), you can start several operations, then await -// them all. The Io backend may run them concurrently: -// -// var f1 = io.async(taskA, .{}); -// var f2 = io.async(taskB, .{}); -// -// // Both tasks may be running now! -// const a = f1.await(io); -// const b = f2.await(io); -// -// There's also io.concurrent() which provides a STRONGER guarantee: -// it ensures the function gets its own unit of concurrency (e.g. a -// real OS thread). But it can fail with error.ConcurrencyUnavailable -// if resources are exhausted. -// -// io.async() is more portable: if no thread is available, it simply -// runs the function synchronously. This makes it the right default -// for most code. -// -// Fix this program to launch both tasks and collect their results. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch both tasks asynchronously. - var future_a = io.async(slowAdd, .{ 10, 20 }); - var future_b = ???(slowMul, .{ 6, 7 }); - - // Await both results. - const sum = future_a.await(io); - const product = future_b.???(io); - - print("{} + {} = {}\n", .{ 1, 2, sum }); - print("{} * {} = {}\n", .{ 6, 7, product }); - print("Total: {}\n", .{sum + product}); -} - -fn slowAdd(a: u32, b: u32) u32 { - return a + b; -} - -fn slowMul(a: u32, b: u32) u32 { - return a * b; -} diff --git a/exercises/087_async3.zig b/exercises/087_async3.zig new file mode 100644 index 0000000..07221e9 --- /dev/null +++ b/exercises/087_async3.zig @@ -0,0 +1,50 @@ +// +// The real power of async shows when you launch MULTIPLE tasks! +// +// With io.async(), you can start several operations, then await +// them all. The Io backend may run them concurrently: +// +// var f1 = io.async(taskA, .{}); +// var f2 = io.async(taskB, .{}); +// +// // Both tasks may be running now! +// const a = f1.await(io); +// const b = f2.await(io); +// +// There's also io.concurrent() which provides a STRONGER guarantee: +// it ensures the function gets its own unit of concurrency (e.g. a +// real OS thread). But it can fail with error.ConcurrencyUnavailable +// if resources are exhausted. +// +// io.async() is more portable: if no thread is available, it simply +// runs the function synchronously. This makes it the right default +// for most code. +// +// Fix this program to launch both tasks and collect their results. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch both tasks asynchronously. + var future_a = io.async(slowAdd, .{ 10, 20 }); + var future_b = ???(slowMul, .{ 6, 7 }); + + // Await both results. + const sum = future_a.await(io); + const product = future_b.???(io); + + print("{} + {} = {}\n", .{ 1, 2, sum }); + print("{} * {} = {}\n", .{ 6, 7, product }); + print("Total: {}\n", .{sum + product}); +} + +fn slowAdd(a: u32, b: u32) u32 { + return a + b; +} + +fn slowMul(a: u32, b: u32) u32 { + return a * b; +} diff --git a/exercises/087_async4.zig b/exercises/087_async4.zig deleted file mode 100644 index 50829fc..0000000 --- a/exercises/087_async4.zig +++ /dev/null @@ -1,50 +0,0 @@ -// -// When you have many tasks that don't return individual values, -// use a Group! A Group is an unordered set of tasks that can -// only be awaited or canceled as a whole: -// -// var group: std.Io.Group = .init; -// group.async(io, myTask, .{arg1}); -// group.async(io, myTask, .{arg2}); -// try group.await(io); // blocks until ALL tasks finish -// -// Important rules: -// * The return type of functions spawned in a group must be -// coercible to Cancelable!void (i.e. void, or error{Canceled}!void). -// * Once you call group.async(), you MUST eventually call -// group.await() or group.cancel() to release resources. -// * group.cancel() requests cancellation on ALL members, -// then waits for them to finish. -// -// Unlike Future, Group tasks don't return values to the caller. -// They're ideal for parallel work that communicates through -// shared state or side effects (like printing). -// -// Fix this program to await all tasks in the group. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var group: std.Io.Group = .init; - - // Spawn 3 tasks in any order. Each sleeps for (id * 1) seconds - // before printing, so the output order is deterministic. - group.async(io, doWork, .{ io, 1 }); - group.async(io, doWork, .{ io, 3 }); - group.async(io, doWork, .{ io, 2 }); - - // Wait for all tasks to finish. - // What Group method blocks until all tasks complete? - try group.??? - - print("All tasks finished!\n", .{}); -} - -fn doWork(io: std.Io, id: u32) void { - // Sleep ensures deterministic output order. - io.sleep(std.Io.Duration.fromSeconds(id), .awake) catch return; - print("Task {} done.\n", .{id}); -} diff --git a/exercises/088_async4.zig b/exercises/088_async4.zig new file mode 100644 index 0000000..50829fc --- /dev/null +++ b/exercises/088_async4.zig @@ -0,0 +1,50 @@ +// +// When you have many tasks that don't return individual values, +// use a Group! A Group is an unordered set of tasks that can +// only be awaited or canceled as a whole: +// +// var group: std.Io.Group = .init; +// group.async(io, myTask, .{arg1}); +// group.async(io, myTask, .{arg2}); +// try group.await(io); // blocks until ALL tasks finish +// +// Important rules: +// * The return type of functions spawned in a group must be +// coercible to Cancelable!void (i.e. void, or error{Canceled}!void). +// * Once you call group.async(), you MUST eventually call +// group.await() or group.cancel() to release resources. +// * group.cancel() requests cancellation on ALL members, +// then waits for them to finish. +// +// Unlike Future, Group tasks don't return values to the caller. +// They're ideal for parallel work that communicates through +// shared state or side effects (like printing). +// +// Fix this program to await all tasks in the group. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var group: std.Io.Group = .init; + + // Spawn 3 tasks in any order. Each sleeps for (id * 1) seconds + // before printing, so the output order is deterministic. + group.async(io, doWork, .{ io, 1 }); + group.async(io, doWork, .{ io, 3 }); + group.async(io, doWork, .{ io, 2 }); + + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? + try group.??? + + print("All tasks finished!\n", .{}); +} + +fn doWork(io: std.Io, id: u32) void { + // Sleep ensures deterministic output order. + io.sleep(std.Io.Duration.fromSeconds(id), .awake) catch return; + print("Task {} done.\n", .{id}); +} diff --git a/exercises/088_async5.zig b/exercises/088_async5.zig deleted file mode 100644 index 4fb8d76..0000000 --- a/exercises/088_async5.zig +++ /dev/null @@ -1,61 +0,0 @@ -// -// One of the most important features of the new Io system is -// structured cancellation! -// -// Every Future has a .cancel() method that: -// 1. Requests the task to stop (via error.Canceled at the -// next "cancellation point") -// 2. Waits for the task to actually finish -// 3. Returns whatever result the task produced -// -// A "cancellation point" is any Io function that can return -// error.Canceled - most commonly io.sleep(): -// -// fn myTask(io: std.Io) u32 { -// io.sleep(...) catch |err| switch (err) { -// error.Canceled => return 0, // handle gracefully -// }; -// return 42; -// } -// -// This is fundamentally different from killing a thread - -// the task gets a chance to clean up and return a value! -// -// Fix this program: the slow task would take 10 seconds, -// but we cancel it after 1 second. The task should detect -// the cancellation and return early. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var future = io.async(slowTask, .{io}); - - // Wait 1 second, then cancel instead of waiting the full 10. - io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; - - print("Canceling slow task...\n", .{}); - - // We don't want to wait 10 seconds! - // Which Future method requests cancellation AND returns the result? - const result = ???; - - print("Task returned: {}\n", .{result}); -} - -fn slowTask(io: std.Io) u32 { - print("Starting long computation...\n", .{}); - - // Try to sleep for 10 seconds - but we might get canceled! - io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch |err| switch (err) { - error.Canceled => { - print("Task was canceled, cleaning up.\n", .{}); - return 0; - }, - }; - - print("Task completed normally.\n", .{}); - return 42; -} diff --git a/exercises/089_async5.zig b/exercises/089_async5.zig new file mode 100644 index 0000000..4fb8d76 --- /dev/null +++ b/exercises/089_async5.zig @@ -0,0 +1,61 @@ +// +// One of the most important features of the new Io system is +// structured cancellation! +// +// Every Future has a .cancel() method that: +// 1. Requests the task to stop (via error.Canceled at the +// next "cancellation point") +// 2. Waits for the task to actually finish +// 3. Returns whatever result the task produced +// +// A "cancellation point" is any Io function that can return +// error.Canceled - most commonly io.sleep(): +// +// fn myTask(io: std.Io) u32 { +// io.sleep(...) catch |err| switch (err) { +// error.Canceled => return 0, // handle gracefully +// }; +// return 42; +// } +// +// This is fundamentally different from killing a thread - +// the task gets a chance to clean up and return a value! +// +// Fix this program: the slow task would take 10 seconds, +// but we cancel it after 1 second. The task should detect +// the cancellation and return early. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var future = io.async(slowTask, .{io}); + + // Wait 1 second, then cancel instead of waiting the full 10. + io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; + + print("Canceling slow task...\n", .{}); + + // We don't want to wait 10 seconds! + // Which Future method requests cancellation AND returns the result? + const result = ???; + + print("Task returned: {}\n", .{result}); +} + +fn slowTask(io: std.Io) u32 { + print("Starting long computation...\n", .{}); + + // Try to sleep for 10 seconds - but we might get canceled! + io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch |err| switch (err) { + error.Canceled => { + print("Task was canceled, cleaning up.\n", .{}); + return 0; + }, + }; + + print("Task completed normally.\n", .{}); + return 42; +} diff --git a/exercises/089_async6.zig b/exercises/089_async6.zig deleted file mode 100644 index eab03c9..0000000 --- a/exercises/089_async6.zig +++ /dev/null @@ -1,71 +0,0 @@ -// -// Sometimes you want to race multiple tasks and act on whichever -// finishes first. That's what Select is for! -// -// Select is like a Group, but lets you receive individual results -// as tasks complete — one at a time: -// -// const Race = std.Io.Select(union(enum) { -// fast: u32, -// slow: u32, -// }); -// -// var buffer: [2]Race.Union = undefined; -// var sel = Race.init(io, &buffer); -// -// sel.async(.fast, fastFn, .{io}); -// sel.async(.slow, slowFn, .{io}); -// -// const winner = try sel.await(); // returns first completed -// switch (winner) { -// .fast => |val| ..., -// .slow => |val| ..., -// } -// sel.cancelDiscard(); // cancel remaining, discard results -// -// The buffer must be large enough for all tasks that might -// complete before you call cancelDiscard(). -// -// Fix this program to receive the winner of the race. -// -const std = @import("std"); -const print = std.debug.print; - -const RaceResult = std.Io.Select(union(enum) { - hare: []const u8, - tortoise: []const u8, -}); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var buffer: [2]RaceResult.Union = undefined; - var sel = RaceResult.init(io, &buffer); - - sel.async(.hare, runHare, .{io}); - sel.async(.tortoise, runTortoise, .{io}); - - // Wait for the first finisher. - // What Select method returns the first completed result? - const winner = ???; - - switch (winner) { - .hare => |msg| print("Hare: {s}\n", .{msg}), - .tortoise => |msg| print("Tortoise: {s}\n", .{msg}), - } - - // Clean up the loser — we don't need their result. - sel.cancelDiscard(); -} - -fn runHare(io: std.Io) []const u8 { - // The hare is fast — only 1 second! - io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch return "I got canceled!"; - return "I'm fast!"; -} - -fn runTortoise(io: std.Io) []const u8 { - // The tortoise is slow — 10 seconds. - io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch return "I got canceled!"; - return "Slow and steady..."; -} diff --git a/exercises/090_async6.zig b/exercises/090_async6.zig new file mode 100644 index 0000000..eab03c9 --- /dev/null +++ b/exercises/090_async6.zig @@ -0,0 +1,71 @@ +// +// Sometimes you want to race multiple tasks and act on whichever +// finishes first. That's what Select is for! +// +// Select is like a Group, but lets you receive individual results +// as tasks complete — one at a time: +// +// const Race = std.Io.Select(union(enum) { +// fast: u32, +// slow: u32, +// }); +// +// var buffer: [2]Race.Union = undefined; +// var sel = Race.init(io, &buffer); +// +// sel.async(.fast, fastFn, .{io}); +// sel.async(.slow, slowFn, .{io}); +// +// const winner = try sel.await(); // returns first completed +// switch (winner) { +// .fast => |val| ..., +// .slow => |val| ..., +// } +// sel.cancelDiscard(); // cancel remaining, discard results +// +// The buffer must be large enough for all tasks that might +// complete before you call cancelDiscard(). +// +// Fix this program to receive the winner of the race. +// +const std = @import("std"); +const print = std.debug.print; + +const RaceResult = std.Io.Select(union(enum) { + hare: []const u8, + tortoise: []const u8, +}); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var buffer: [2]RaceResult.Union = undefined; + var sel = RaceResult.init(io, &buffer); + + sel.async(.hare, runHare, .{io}); + sel.async(.tortoise, runTortoise, .{io}); + + // Wait for the first finisher. + // What Select method returns the first completed result? + const winner = ???; + + switch (winner) { + .hare => |msg| print("Hare: {s}\n", .{msg}), + .tortoise => |msg| print("Tortoise: {s}\n", .{msg}), + } + + // Clean up the loser — we don't need their result. + sel.cancelDiscard(); +} + +fn runHare(io: std.Io) []const u8 { + // The hare is fast — only 1 second! + io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch return "I got canceled!"; + return "I'm fast!"; +} + +fn runTortoise(io: std.Io) []const u8 { + // The tortoise is slow — 10 seconds. + io.sleep(std.Io.Duration.fromSeconds(10), .awake) catch return "I got canceled!"; + return "Slow and steady..."; +} diff --git a/exercises/090_async7.zig b/exercises/090_async7.zig deleted file mode 100644 index bfe6ffd..0000000 --- a/exercises/090_async7.zig +++ /dev/null @@ -1,57 +0,0 @@ -// -// When multiple async tasks access shared data, you need -// synchronization! Io provides a Mutex for this: -// -// var mutex: std.Io.Mutex = .init; -// -// // In a task: -// try mutex.lock(io); // blocks until lock is acquired -// defer mutex.unlock(); -// // ... critical section: safe to modify shared data ... -// -// Without the mutex, concurrent tasks could read and write the -// same memory simultaneously, causing a data race — the result -// would be unpredictable. -// -// mutex.lock() is a cancellation point — it can return -// error.Canceled. There's also tryLock() which returns -// immediately (true if acquired, false if not). -// -// Fix this program so the counter is correctly synchronized. -// Without the fix, the final count would be unpredictable. -// With it, four tasks incrementing 100 times each = 400. -// -const std = @import("std"); -const print = std.debug.print; - -const SharedState = struct { - counter: u32 = 0, - mutex: std.Io.Mutex = .init, -}; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - var state = SharedState{}; - - var group: std.Io.Group = .init; - - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - group.async(io, increment, .{ io, &state, 100 }); - - try group.await(io); - - print("Counter: {}\n", .{state.counter}); -} - -fn increment(io: std.Io, state: *SharedState, times: u32) void { - for (0..times) |_| { - // Acquire the lock before modifying shared state. - // What Mutex method blocks until the lock is acquired? - state.mutex.??? catch return; - defer state.mutex.unlock(); // <-- what's missing here? - - state.counter += 1; - } -} diff --git a/exercises/091_async7.zig b/exercises/091_async7.zig new file mode 100644 index 0000000..bfe6ffd --- /dev/null +++ b/exercises/091_async7.zig @@ -0,0 +1,57 @@ +// +// When multiple async tasks access shared data, you need +// synchronization! Io provides a Mutex for this: +// +// var mutex: std.Io.Mutex = .init; +// +// // In a task: +// try mutex.lock(io); // blocks until lock is acquired +// defer mutex.unlock(); +// // ... critical section: safe to modify shared data ... +// +// Without the mutex, concurrent tasks could read and write the +// same memory simultaneously, causing a data race — the result +// would be unpredictable. +// +// mutex.lock() is a cancellation point — it can return +// error.Canceled. There's also tryLock() which returns +// immediately (true if acquired, false if not). +// +// Fix this program so the counter is correctly synchronized. +// Without the fix, the final count would be unpredictable. +// With it, four tasks incrementing 100 times each = 400. +// +const std = @import("std"); +const print = std.debug.print; + +const SharedState = struct { + counter: u32 = 0, + mutex: std.Io.Mutex = .init, +}; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + var state = SharedState{}; + + var group: std.Io.Group = .init; + + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + group.async(io, increment, .{ io, &state, 100 }); + + try group.await(io); + + print("Counter: {}\n", .{state.counter}); +} + +fn increment(io: std.Io, state: *SharedState, times: u32) void { + for (0..times) |_| { + // Acquire the lock before modifying shared state. + // What Mutex method blocks until the lock is acquired? + state.mutex.??? catch return; + defer state.mutex.unlock(); // <-- what's missing here? + + state.counter += 1; + } +} diff --git a/exercises/091_async8.zig b/exercises/091_async8.zig deleted file mode 100644 index 10921c3..0000000 --- a/exercises/091_async8.zig +++ /dev/null @@ -1,62 +0,0 @@ -// -// Tasks often need to communicate! Io provides Queue for this — -// a bounded, thread-safe channel for passing data between tasks: -// -// var backing: [16]u32 = undefined; -// var queue: std.Io.Queue(u32) = .init(&backing); -// -// // Producer task: -// try queue.putOne(io, value); // blocks if queue is full -// -// // Consumer task: -// const val = try queue.getOne(io); // blocks if queue is empty -// -// When the producer is done, it calls queue.close(io) to signal -// that no more data is coming. After that, getOne() will return -// error.Closed once the queue is drained. -// -// This is the classic producer/consumer pattern — one task -// generates work, another processes it, and the queue handles -// all the synchronization automatically. -// -// Fix this program: the producer sends numbers 1..10, the -// consumer sums them up. The expected sum is 55. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var backing: [4]u32 = undefined; - var queue: std.Io.Queue(u32) = .init(&backing); - - var group: std.Io.Group = .init; - - group.async(io, producer, .{ io, &queue }); - group.async(io, consumer, .{ io, &queue }); - - try group.await(io); -} - -fn producer(io: std.Io, queue: *std.Io.Queue(u32)) void { - // Send numbers 1 through 10 into the queue. - for (1..11) |i| { - // What Queue method sends a single element, blocking if full? - queue.???(io, @intCast(i)) catch return; - } - // Signal that we're done sending. - queue.close(io); -} - -fn consumer(io: std.Io, queue: *std.Io.Queue(u32)) void { - var sum: u32 = 0; - while (true) { - const value = queue.getOne(io) catch |err| switch (err) { - error.Closed => break, - error.Canceled => return, - }; - sum += value; - } - print("Sum of 1..10 = {}\n", .{sum}); -} diff --git a/exercises/092_async8.zig b/exercises/092_async8.zig new file mode 100644 index 0000000..10921c3 --- /dev/null +++ b/exercises/092_async8.zig @@ -0,0 +1,62 @@ +// +// Tasks often need to communicate! Io provides Queue for this — +// a bounded, thread-safe channel for passing data between tasks: +// +// var backing: [16]u32 = undefined; +// var queue: std.Io.Queue(u32) = .init(&backing); +// +// // Producer task: +// try queue.putOne(io, value); // blocks if queue is full +// +// // Consumer task: +// const val = try queue.getOne(io); // blocks if queue is empty +// +// When the producer is done, it calls queue.close(io) to signal +// that no more data is coming. After that, getOne() will return +// error.Closed once the queue is drained. +// +// This is the classic producer/consumer pattern — one task +// generates work, another processes it, and the queue handles +// all the synchronization automatically. +// +// Fix this program: the producer sends numbers 1..10, the +// consumer sums them up. The expected sum is 55. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var backing: [4]u32 = undefined; + var queue: std.Io.Queue(u32) = .init(&backing); + + var group: std.Io.Group = .init; + + group.async(io, producer, .{ io, &queue }); + group.async(io, consumer, .{ io, &queue }); + + try group.await(io); +} + +fn producer(io: std.Io, queue: *std.Io.Queue(u32)) void { + // Send numbers 1 through 10 into the queue. + for (1..11) |i| { + // What Queue method sends a single element, blocking if full? + queue.???(io, @intCast(i)) catch return; + } + // Signal that we're done sending. + queue.close(io); +} + +fn consumer(io: std.Io, queue: *std.Io.Queue(u32)) void { + var sum: u32 = 0; + while (true) { + const value = queue.getOne(io) catch |err| switch (err) { + error.Closed => break, + error.Canceled => return, + }; + sum += value; + } + print("Sum of 1..10 = {}\n", .{sum}); +} diff --git a/exercises/092_async9.zig b/exercises/092_async9.zig deleted file mode 100644 index ad30dcf..0000000 --- a/exercises/092_async9.zig +++ /dev/null @@ -1,57 +0,0 @@ -// -// We've been using io.async() to launch tasks. But there's a -// stronger variant: io.concurrent(). -// -// The difference: -// -// io.async(): -// * The function MAY run on another thread, or it may run -// immediately on the current thread (synchronously). -// * Never fails — if no thread is available, it just runs -// the function right away. -// * More portable, works with all Io backends. -// -// io.concurrent(): -// * GUARANTEES a separate unit of concurrency (a real thread -// in the Threaded backend). -// * Can fail with error.ConcurrencyUnavailable if resources -// are exhausted or the backend doesn't support it. -// * Use when you NEED true parallelism. -// -// Because concurrent() can fail, you must handle the error: -// -// var future = try io.concurrent(myFn, .{args}); -// const result = future.await(io); -// -// Notice the 'try' — that's the key difference in usage! -// -// Fix this program to launch the computation concurrently. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - // Launch with a guaranteed separate thread. - // Which Io method guarantees true concurrency? - // (Hint: unlike io.async, this one can fail!) - var future = try io.???(compute, .{io}); - - print("Main thread continues...\n", .{}); - - // Wait 100 millisecond so the output order is deterministic. - io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {}; - - print("Main thread done waiting.\n", .{}); - - const result = future.await(io); - print("Result: {}\n", .{result}); -} - -fn compute(io: std.Io) u32 { - print("Computing on a separate thread!\n", .{}); - // Simulate some work. - io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0; - return 123; -} diff --git a/exercises/093_async10.zig b/exercises/093_async10.zig deleted file mode 100644 index 6ed229d..0000000 --- a/exercises/093_async10.zig +++ /dev/null @@ -1,67 +0,0 @@ -// -// In exercise 088, we learned that cancellation happens at -// "cancellation points" — any Io function that can return -// error.Canceled. -// -// But sometimes a task has a critical section that MUST NOT -// be interrupted — for example, writing a consistent state -// to disk, or completing a transaction. -// -// Io provides CancelProtection for this: -// -// const old = io.swapCancelProtection(.blocked); -// defer _ = io.swapCancelProtection(old); -// -// // In this block, NO Io function will return error.Canceled. -// // The cancel request is held until protection is restored. -// -// There are two states: -// .unblocked — normal: cancellation points can fire (default) -// .blocked — protected: error.Canceled is never returned -// -// There's also io.checkCancel() — a pure cancellation point -// that does nothing except return error.Canceled if a cancel -// request is pending. Useful in long CPU-bound loops. -// -// And io.recancel() — re-arms a consumed cancel request so -// the NEXT cancellation point will fire again. -// -// Fix this program so the critical section completes even -// when the task is canceled. -// -const std = @import("std"); -const print = std.debug.print; - -pub fn main(init: std.process.Init) !void { - const io = init.io; - - var future = io.async(importantTask, .{io}); - - // Give the task time to start and enter its critical section. - io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {}; - - // Cancel while the task is in its protected section. - const result = future.cancel(io); - print("Task result: {s}\n", .{result}); -} - -fn importantTask(io: std.Io) []const u8 { - print("Starting critical section...\n", .{}); - - // Protect this section from cancellation. - // What method swaps the cancel protection state? - const old = io.???(. blocked); - defer _ = io.???(old); - - // This sleep will NOT return error.Canceled even though - // we get canceled during it — protection is active! - io.sleep(std.Io.Duration.fromMilliseconds(600), .awake) catch |err| switch (err) { - error.Canceled => { - // This should never happen while protected! - return "ERROR: canceled during critical section!"; - }, - }; - - print("Critical section completed safely.\n", .{}); - return "All data saved."; -} diff --git a/exercises/093_async9.zig b/exercises/093_async9.zig new file mode 100644 index 0000000..ad30dcf --- /dev/null +++ b/exercises/093_async9.zig @@ -0,0 +1,57 @@ +// +// We've been using io.async() to launch tasks. But there's a +// stronger variant: io.concurrent(). +// +// The difference: +// +// io.async(): +// * The function MAY run on another thread, or it may run +// immediately on the current thread (synchronously). +// * Never fails — if no thread is available, it just runs +// the function right away. +// * More portable, works with all Io backends. +// +// io.concurrent(): +// * GUARANTEES a separate unit of concurrency (a real thread +// in the Threaded backend). +// * Can fail with error.ConcurrencyUnavailable if resources +// are exhausted or the backend doesn't support it. +// * Use when you NEED true parallelism. +// +// Because concurrent() can fail, you must handle the error: +// +// var future = try io.concurrent(myFn, .{args}); +// const result = future.await(io); +// +// Notice the 'try' — that's the key difference in usage! +// +// Fix this program to launch the computation concurrently. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // Launch with a guaranteed separate thread. + // Which Io method guarantees true concurrency? + // (Hint: unlike io.async, this one can fail!) + var future = try io.???(compute, .{io}); + + print("Main thread continues...\n", .{}); + + // Wait 100 millisecond so the output order is deterministic. + io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {}; + + print("Main thread done waiting.\n", .{}); + + const result = future.await(io); + print("Result: {}\n", .{result}); +} + +fn compute(io: std.Io) u32 { + print("Computing on a separate thread!\n", .{}); + // Simulate some work. + io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0; + return 123; +} diff --git a/exercises/094_async10.zig b/exercises/094_async10.zig new file mode 100644 index 0000000..6ed229d --- /dev/null +++ b/exercises/094_async10.zig @@ -0,0 +1,67 @@ +// +// In exercise 088, we learned that cancellation happens at +// "cancellation points" — any Io function that can return +// error.Canceled. +// +// But sometimes a task has a critical section that MUST NOT +// be interrupted — for example, writing a consistent state +// to disk, or completing a transaction. +// +// Io provides CancelProtection for this: +// +// const old = io.swapCancelProtection(.blocked); +// defer _ = io.swapCancelProtection(old); +// +// // In this block, NO Io function will return error.Canceled. +// // The cancel request is held until protection is restored. +// +// There are two states: +// .unblocked — normal: cancellation points can fire (default) +// .blocked — protected: error.Canceled is never returned +// +// There's also io.checkCancel() — a pure cancellation point +// that does nothing except return error.Canceled if a cancel +// request is pending. Useful in long CPU-bound loops. +// +// And io.recancel() — re-arms a consumed cancel request so +// the NEXT cancellation point will fire again. +// +// Fix this program so the critical section completes even +// when the task is canceled. +// +const std = @import("std"); +const print = std.debug.print; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var future = io.async(importantTask, .{io}); + + // Give the task time to start and enter its critical section. + io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {}; + + // Cancel while the task is in its protected section. + const result = future.cancel(io); + print("Task result: {s}\n", .{result}); +} + +fn importantTask(io: std.Io) []const u8 { + print("Starting critical section...\n", .{}); + + // Protect this section from cancellation. + // What method swaps the cancel protection state? + const old = io.???(. blocked); + defer _ = io.???(old); + + // This sleep will NOT return error.Canceled even though + // we get canceled during it — protection is active! + io.sleep(std.Io.Duration.fromMilliseconds(600), .awake) catch |err| switch (err) { + error.Canceled => { + // This should never happen while protected! + return "ERROR: canceled during critical section!"; + }, + }; + + print("Critical section completed safely.\n", .{}); + return "All data saved."; +} diff --git a/exercises/095_interfaces.zig b/exercises/095_interfaces.zig deleted file mode 100644 index 7775dd5..0000000 --- a/exercises/095_interfaces.zig +++ /dev/null @@ -1,127 +0,0 @@ -// -// Remember our ant and bee simulator constructed with unions -// back in exercises 55 and 56? There, we demonstrated that -// unions allow us to treat different data types in a uniform -// manner. -// -// One neat feature was using tagged unions to create a single -// function to print a status for ants *or* bees by switching: -// -// switch (insect) { -// .still_alive => ... // (print ant stuff) -// .flowers_visited => ... // (print bee stuff) -// } -// -// Well, that simulation was running just fine until a new insect -// arrived in the virtual garden, a grasshopper! -// -// Doctor Zoraptera started to add grasshopper code to the -// program, but then she backed away from her keyboard with an -// angry hissing sound. She had realized that having code for -// each insect in one place and code to print each insect in -// another place was going to become unpleasant to maintain when -// the simulation expanded to hundreds of different insects. -// -// Thankfully, Zig has another comptime feature we can use -// to get out of this dilemma called the 'inline else'. -// -// We can replace this redundant code: -// -// switch (thing) { -// .a => |a| special(a), -// .b => |b| normal(b), -// .c => |c| normal(c), -// .d => |d| normal(d), -// .e => |e| normal(e), -// ... -// } -// -// With: -// -// switch (thing) { -// .a => |a| special(a), -// inline else => |t| normal(t), -// } -// -// We can have special handling of some cases and then Zig -// handles the rest of the matches for us. -// -// With this feature, you decide to make an Insect union with a -// single uniform 'print()' function. All of the insects can -// then be responsible for printing themselves. And Doctor -// Zoraptera can calm down and stop gnawing on the furniture. -// -const std = @import("std"); - -const Ant = struct { - still_alive: bool, - - pub fn print(self: Ant) void { - std.debug.print("Ant is {s}.\n", .{if (self.still_alive) "alive" else "dead"}); - } -}; - -const Bee = struct { - flowers_visited: u16, - - pub fn print(self: Bee) void { - std.debug.print("Bee visited {} flowers.\n", .{self.flowers_visited}); - } -}; - -// Here's the new grasshopper. Notice how we've also added print -// methods to each insect. -const Grasshopper = struct { - distance_hopped: u16, - - pub fn print(self: Grasshopper) void { - std.debug.print("Grasshopper hopped {} meters.\n", .{self.distance_hopped}); - } -}; - -const Insect = union(enum) { - ant: Ant, - bee: Bee, - grasshopper: Grasshopper, - - // Thanks to 'inline else', we can think of this print() as - // being an interface method. Any member of this union with - // a print() method can be treated uniformly by outside - // code without needing to know any other details. Cool! - pub fn print(self: Insect) void { - switch (self) { - inline else => |case| return case.print(), - } - } -}; - -pub fn main() !void { - const my_insects = [_]Insect{ - Insect{ .ant = Ant{ .still_alive = true } }, - Insect{ .bee = Bee{ .flowers_visited = 17 } }, - Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, - }; - - std.debug.print("Daily Insect Report:\n", .{}); - for (my_insects) |insect| { - // Almost done! We want to print() each insect with a - // single method call here. - ??? - } -} - -// Our print() method in the Insect union above demonstrates -// something very similar to the object-oriented concept of an -// abstract data type. That is, the Insect type doesn't contain -// the underlying data, and the print() function doesn't -// actually do the printing. -// -// The point of an interface is to support generic programming: -// the ability to treat different things as if they were the -// same to cut down on clutter and conceptual complexity. -// -// The Daily Insect Report doesn't need to worry about *which* -// insects are in the report - they all print the same way via -// the interface! -// -// Doctor Zoraptera loves it. diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig new file mode 100644 index 0000000..fb78e7b --- /dev/null +++ b/exercises/095_quiz_async.zig @@ -0,0 +1,186 @@ +// +// Quiz Time — Async I/O! +// +// Doctor Zoraptera's insect simulation is going well, but she +// realized that her virtual garden needs weather data! Insects +// behave differently depending on temperature, humidity, and +// wind conditions. +// +// She has set up three weather sensors around the garden that +// measure conditions in parallel and report their readings +// through a shared data channel. A collector task gathers the +// readings, and after all sensors have reported, a garden +// report is printed. +// +// But Doctor Z rushed through the code (she was being chased +// by a grasshopper) and left several bugs. Can you fix them? +// +// Here's what the program should do: +// 1. Three sensor tasks run concurrently, each sending +// exactly 3 readings through a Queue +// 2. A collector task receives readings, protected by a Mutex +// 3. After all sensors finish, the queue is closed +// 4. The final report is written in a cancel-protected section +// +// ************************************************************* +// * A NOTE ABOUT THIS EXERCISE * +// * * +// * This quiz uses concepts from exercises 084-093. * +// * There are 6 bugs to fix — look for the ???s! * +// * * +// ************************************************************* +// +const std = @import("std"); +const print = std.debug.print; + +const SensorType = enum { thermometer, hygrometer, anemometer }; + +const Reading = struct { + sensor_type: SensorType, + value: i32, +}; + +const GardenWeather = struct { + temperature: i32 = 0, + humidity: i32 = 0, + wind: i32 = 0, + readings_count: u32 = 0, + mutex: std.Io.Mutex = .init, + + fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { + // Bug 1: The collector needs to lock before modifying + // shared state. What Mutex method acquires the lock? + self.mutex.lock(io) catch return; + self.mutex.???(io) catch return; + + switch (reading.sensor_type) { + .thermometer => self.temperature = reading.value, + .hygrometer => self.humidity = reading.value, + .anemometer => self.wind = reading.value, + } + self.readings_count += 1; + } +}; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var weather = GardenWeather{}; + + var reading_buf: [8]Reading = undefined; + var queue: std.Io.Queue(Reading) = .init(&reading_buf); + + // Sensor group: runs all three sensors to completion. + var sensors: std.Io.Group = .init; + + // Start three sensor tasks. They need GUARANTEED concurrency + // since they each simulate real-time measurement. + // + // Bug 2: io.async doesn't guarantee a separate thread. + // Which Io method guarantees true concurrency? + // (Don't forget: it can fail, so you need 'try'!) + try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); + try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); + try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); + + // Collector group: processes readings from the queue. + var collectors: std.Io.Group = .init; + collectors.async(io, collector, .{ io, &queue, &weather }); + + // Bug 3: Wait for ALL sensors to finish sending their readings. + // What Group method blocks until all tasks complete? + try sensors.await(io); + // try sensors.???(io); + + // All sensors done — close the queue so the collector knows + // there's no more data coming. + queue.close(io); + + // Wait for the collector to drain the queue. + try collectors.await(io); + + // Now write the garden report. This is critical — it must + // NOT be interrupted, even if something tries to cancel us! + // + // Bug 4: Protect this section from cancellation. + // What Io method swaps the cancel protection state? + const old_protection = io.???(.blocked); + defer _ = io.???(old_protection); + + printGardenReport(&weather); +} + +fn sensor( + io: std.Io, + queue: *std.Io.Queue(Reading), + sensor_type: SensorType, + base_value: i32, +) void { + // Each sensor takes exactly 3 measurements. + for (1..4) |i| { + io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch return; + + const reading = Reading{ + .sensor_type = sensor_type, + .value = base_value + @as(i32, @intCast(i)), + }; + + // Bug 5: Send the reading into the queue. + // What Queue method sends a single element? + queue.???(io, reading) catch return; + } +} + +fn collector( + io: std.Io, + queue: *std.Io.Queue(Reading), + weather: *GardenWeather, +) void { + while (true) { + const reading = queue.getOne(io) catch |err| switch (err) { + error.Closed => break, + error.Canceled => return, + }; + weather.addReading(io, reading); + } +} + +fn printGardenReport(weather: *GardenWeather) void { + print("=== Doctor Zoraptera's Garden Report ===\n", .{}); + print("Temperature : {}C\n", .{weather.temperature}); + print("Humidity : {}%\n", .{weather.humidity}); + print("Wind : {} km/h\n", .{weather.wind}); + print("Readings : {}\n", .{weather.readings_count}); + + if (weather.temperature > 20 and weather.wind < 15) { + print("Bee-friendly conditions! Expect high pollination.\n", .{}); + } else { + print("Grasshoppers will be grumpy today.\n", .{}); + } +} + +// Further reading for the curious: +// +// This quiz covered the main async I/O primitives: +// io.async() - launch a task (may run inline) +// io.concurrent() - launch with guaranteed parallelism +// Group.concurrent() - concurrent tasks in a group +// Future.await/cancel - collect or cancel a single task +// Group.async/await/cancel - manage fire-and-forget tasks +// Select.async/await - race tasks, act on first completion +// Queue - bounded channel between tasks +// Mutex - protect shared state +// CancelProtection - shield critical sections +// +// There are more synchronization primitives we didn't cover: +// Condition - wait for a condition to become true +// RwLock - multiple readers OR one writer +// Semaphore - limit concurrent access to a resource +// Futex - low-level wait/wake on a memory address +// Batch - submit multiple I/O operations at once +// +// The key insight: all of these work through the Io VTable, +// so your code is portable across backends (Threaded, Uring, +// Kqueue, Dispatch) without any changes! +// +// Doctor Zoraptera approves. diff --git a/patches/patches/084_interfaces.patch b/patches/patches/084_interfaces.patch new file mode 100644 index 0000000..a1d0628 --- /dev/null +++ b/patches/patches/084_interfaces.patch @@ -0,0 +1,11 @@ +--- exercises/084_interfaces.zig 2025-08-15 15:17:57.839348063 +0200 ++++ answers/084_interfaces.zig 2026-04-03 14:27:32.670756488 +0200 +@@ -106,7 +106,7 @@ + for (my_insects) |insect| { + // Almost done! We want to print() each insect with a + // single method call here. +- ??? ++ insect.print(); + } + } + diff --git a/patches/patches/085_async.patch b/patches/patches/085_async.patch new file mode 100644 index 0000000..ca8b102 --- /dev/null +++ b/patches/patches/085_async.patch @@ -0,0 +1,11 @@ +--- exercises/085_async.zig 2026-04-01 20:40:08.904999609 +0200 ++++ answers/085_async.zig 2026-04-01 20:40:05.641933231 +0200 +@@ -37,7 +37,7 @@ + const std = @import("std"); + + pub fn main(init: std.process.Init) !void { +- const io = init.???; ++ const io = init.io; + + // Get the current wall-clock time using the Io interface. + // Hint: Timestamp.now() takes an Io and a Clock type (.real = wall clock). diff --git a/patches/patches/086_async2.patch b/patches/patches/086_async2.patch new file mode 100644 index 0000000..7506a69 --- /dev/null +++ b/patches/patches/086_async2.patch @@ -0,0 +1,14 @@ +--- exercises/086_async2.zig 2026-04-01 19:22:50.017227542 +0200 ++++ answers/086_async2.zig 2026-04-01 19:21:57.569158481 +0200 +@@ -38,9 +38,9 @@ + + // Now collect the result. What method on Future gives us + // the value, blocking if it isn't ready yet? +- const answer = future.???(io); ++ const answer = future.await(io); + +- std.debug.print("The answer is: {}\n", .{answer}); ++ std.debug.print("the answer is: {}\n", .{answer}); + } + + fn computeAnswer(a: u32, b: u32) u32 { diff --git a/patches/patches/087_async3.patch b/patches/patches/087_async3.patch new file mode 100644 index 0000000..8365e7a --- /dev/null +++ b/patches/patches/087_async3.patch @@ -0,0 +1,18 @@ +--- exercises/087_async3.zig 2026-04-01 22:51:05.540094851 +0200 ++++ answers/087_async3.zig 2026-04-01 22:50:44.579669189 +0200 +@@ -29,12 +29,12 @@ + const io = init.io; + + // Launch both tasks asynchronously. +- var future_a = io.async(slowAdd, .{ 10, 20 }); +- var future_b = ???(slowMul, .{ 6, 7 }); ++ var future_a = io.async(slowAdd, .{ 1, 2 }); ++ var future_b = io.async(slowMul, .{ 6, 7 }); + + // Await both results. + const sum = future_a.await(io); +- const product = future_b.???(io); ++ const product = future_b.await(io); + + print("{} + {} = {}\n", .{ 1, 2, sum }); + print("{} * {} = {}\n", .{ 6, 7, product }); diff --git a/patches/patches/088_async4.patch b/patches/patches/088_async4.patch new file mode 100644 index 0000000..1faf30e --- /dev/null +++ b/patches/patches/088_async4.patch @@ -0,0 +1,11 @@ +--- exercises/088_async4.zig 2026-04-01 23:17:31.066443941 +0200 ++++ answers/088_async4.zig 2026-04-01 23:17:39.251612131 +0200 +@@ -38,7 +38,7 @@ + + // Wait for all tasks to finish. + // What Group method blocks until all tasks complete? +- try group.??? ++ try group.await(io); + + print("All tasks finished!\n", .{}); + } diff --git a/patches/patches/089_async5.patch b/patches/patches/089_async5.patch new file mode 100644 index 0000000..d2baa96 --- /dev/null +++ b/patches/patches/089_async5.patch @@ -0,0 +1,11 @@ +--- exercises/089_async5.zig 2026-04-01 23:40:40.505855238 +0200 ++++ answers/089_async5.zig 2026-04-01 23:40:10.176236971 +0200 +@@ -40,7 +40,7 @@ + + // We don't want to wait 10 seconds! + // Which Future method requests cancellation AND returns the result? +- const result = ???; ++ const result = future.cancel(io); + + print("Task returned: {}\n", .{result}); + } diff --git a/patches/patches/090_async6.patch b/patches/patches/090_async6.patch new file mode 100644 index 0000000..5ac777b --- /dev/null +++ b/patches/patches/090_async6.patch @@ -0,0 +1,11 @@ +--- exercises/090_async6.zig 2026-04-02 10:25:34.016616118 +0200 ++++ answers/090_async6.zig 2026-04-02 10:27:48.827144051 +0200 +@@ -47,7 +47,7 @@ + + // Wait for the first finisher. + // What Select method returns the first completed result? +- const winner = ???; ++ const winner = try sel.await(); + + switch (winner) { + .hare => |msg| print("Hare: {s}\n", .{msg}), diff --git a/patches/patches/091_async7.patch b/patches/patches/091_async7.patch new file mode 100644 index 0000000..b4bab9b --- /dev/null +++ b/patches/patches/091_async7.patch @@ -0,0 +1,13 @@ +--- exercises/091_async7.zig 2026-04-02 10:50:08.142508099 +0200 ++++ answers/091_async7.zig 2026-04-02 10:49:59.629341593 +0200 +@@ -49,8 +49,8 @@ + for (0..times) |_| { + // Acquire the lock before modifying shared state. + // What Mutex method blocks until the lock is acquired? +- state.mutex.??? catch return; +- defer state.mutex.unlock(); // <-- what's missing here? ++ state.mutex.lock(io) catch return; ++ defer state.mutex.unlock(io); + + state.counter += 1; + } diff --git a/patches/patches/092_async8.patch b/patches/patches/092_async8.patch new file mode 100644 index 0000000..0ec9116 --- /dev/null +++ b/patches/patches/092_async8.patch @@ -0,0 +1,11 @@ +--- exercises/092_async8.zig 2026-04-02 10:49:27.925721496 +0200 ++++ answers/092_async8.zig 2026-04-02 10:49:31.694795212 +0200 +@@ -43,7 +43,7 @@ + // Send numbers 1 through 10 into the queue. + for (1..11) |i| { + // What Queue method sends a single element, blocking if full? +- queue.???(io, @intCast(i)) catch return; ++ queue.putOne(io, @intCast(i)) catch return; + } + // Signal that we're done sending. + queue.close(io); diff --git a/patches/patches/093_async9.patch b/patches/patches/093_async9.patch new file mode 100644 index 0000000..f759921 --- /dev/null +++ b/patches/patches/093_async9.patch @@ -0,0 +1,11 @@ +--- exercises/093_async9.zig 2026-04-03 13:44:50.526780809 +0200 ++++ answers/093_async9.zig 2026-04-03 13:44:54.957870294 +0200 +@@ -36,7 +36,7 @@ + // Launch with a guaranteed separate thread. + // Which Io method guarantees true concurrency? + // (Hint: unlike io.async, this one can fail!) +- var future = try io.???(compute, .{io}); ++ var future = try io.concurrent(compute, .{io}); + + print("Main thread continues...\n", .{}); + diff --git a/patches/patches/094_async10.patch b/patches/patches/094_async10.patch new file mode 100644 index 0000000..ae0d26d --- /dev/null +++ b/patches/patches/094_async10.patch @@ -0,0 +1,13 @@ +--- exercises/094_async10.zig 2026-04-03 14:25:16.600025924 +0200 ++++ answers/094_async10.zig 2026-04-03 14:24:56.192615893 +0200 +@@ -50,8 +50,8 @@ + + // Protect this section from cancellation. + // What method swaps the cancel protection state? +- const old = io.???(. blocked); +- defer _ = io.???(old); ++ const old = io.swapCancelProtection(.blocked); ++ defer _ = io.swapCancelProtection(old); + + // This sleep will NOT return error.Canceled even though + // we get canceled during it — protection is active! diff --git a/patches/patches/095_quiz_async.patch b/patches/patches/095_quiz_async.patch new file mode 100644 index 0000000..dbaae07 --- /dev/null +++ b/patches/patches/095_quiz_async.patch @@ -0,0 +1,52 @@ +--- exercises/095_quiz_async.zig 2026-04-03 18:04:53.577391455 +0200 ++++ answers/095_quiz_async.zig 2026-04-03 18:05:42.570392172 +0200 +@@ -51,7 +51,7 @@ + // Bug 1: The collector needs to lock before modifying + // shared state. What Mutex method acquires the lock? + self.mutex.lock(io) catch return; +- self.mutex.???(io) catch return; ++ defer self.mutex.unlock(io); + + switch (reading.sensor_type) { + .thermometer => self.temperature = reading.value, +@@ -79,9 +79,9 @@ + // Bug 2: io.async doesn't guarantee a separate thread. + // Which Io method guarantees true concurrency? + // (Don't forget: it can fail, so you need 'try'!) +- try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); +- try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); +- try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .thermometer, 20 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .hygrometer, 60 }); ++ try sensors.concurrent(io, sensor, .{ io, &queue, .anemometer, 10 }); + + // Collector group: processes readings from the queue. + var collectors: std.Io.Group = .init; +@@ -90,7 +90,6 @@ + // Bug 3: Wait for ALL sensors to finish sending their readings. + // What Group method blocks until all tasks complete? + try sensors.await(io); +- // try sensors.???(io); + + // All sensors done — close the queue so the collector knows + // there's no more data coming. +@@ -104,8 +103,8 @@ + // + // Bug 4: Protect this section from cancellation. + // What Io method swaps the cancel protection state? +- const old_protection = io.???(.blocked); +- defer _ = io.???(old_protection); ++ const old_protection = io.swapCancelProtection(.blocked); ++ defer _ = io.swapCancelProtection(old_protection); + + printGardenReport(&weather); + } +@@ -127,7 +126,7 @@ + + // Bug 5: Send the reading into the queue. + // What Queue method sends a single element? +- queue.???(io, reading) catch return; ++ queue.putOne(io, reading) catch return; + } + } + -- cgit v1.2.3 From f6a6798c8b6b813bd2ceee81db276e05327a76e0 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Fri, 3 Apr 2026 19:28:12 +0200 Subject: improved report design --- build.zig | 2 +- exercises/084_interfaces.zig | 2 +- patches/patches/084_interfaces.patch | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'exercises') 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 -- cgit v1.2.3 From 58f8df66d57fec4b0d8d69df7ac26624194b86ad Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Sat, 4 Apr 2026 16:05:35 +0200 Subject: improvements for async-io --- exercises/085_async.zig | 9 +++++---- patches/patches/085_async.patch | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'exercises') diff --git a/exercises/085_async.zig b/exercises/085_async.zig index 48bda2b..1d885a5 100644 --- a/exercises/085_async.zig +++ b/exercises/085_async.zig @@ -7,10 +7,11 @@ // This interface uses a VTable pattern - a struct of function pointers - // to abstract over different concurrency backends: // -// * Threaded - classic thread-pool based I/O -// * Uring - Linux io_uring -// * Kqueue - BSD/macOS -// * Dispatch - macOS Grand Central Dispatch +// * Threaded - thread-pool based I/O +// * Evented - chooses the best event-loop backend for your OS: +// * Uring on Linux (io_uring) +// * Kqueue on BSD/macOS +// * Dispatch on macOS (Grand Central Dispatch) // // The Io struct itself is tiny: // diff --git a/patches/patches/085_async.patch b/patches/patches/085_async.patch index ca8b102..108eae1 100644 --- a/patches/patches/085_async.patch +++ b/patches/patches/085_async.patch @@ -1,6 +1,6 @@ ---- exercises/085_async.zig 2026-04-01 20:40:08.904999609 +0200 -+++ answers/085_async.zig 2026-04-01 20:40:05.641933231 +0200 -@@ -37,7 +37,7 @@ +--- exercises/085_async.zig 2026-04-04 16:01:01.509555724 +0200 ++++ answers/085_async.zig 2026-04-04 16:00:58.541495688 +0200 +@@ -38,7 +38,7 @@ const std = @import("std"); pub fn main(init: std.process.Init) !void { -- cgit v1.2.3 From 2acf19277544ded4f56b525240be856094085009 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Sun, 5 Apr 2026 12:42:42 +0200 Subject: improvements for async-io --- exercises/086_async2.zig | 32 +++++++++++++++++++------------- patches/patches/086_async2.patch | 10 +++++----- 2 files changed, 24 insertions(+), 18 deletions(-) (limited to 'exercises') diff --git a/exercises/086_async2.zig b/exercises/086_async2.zig index 1f1c4c8..cf376e2 100644 --- a/exercises/086_async2.zig +++ b/exercises/086_async2.zig @@ -6,41 +6,47 @@ // won't necessarily be available until you call .await() on it: // // var future = io.async(someFunction, .{ arg1, arg2 }); -// // ... do other work here ... // const result = future.await(io); // // The function *may* run immediately or on another thread - // your code doesn't need to care! That's the beauty of the -// Io abstraction. (In the Threaded backend, if no thread is -// available, the function runs synchronously right away and -// .await() just returns the already-computed result.) +// Io abstraction. // -// io.async() returns a Future(T) where T is the return type -// of the function you passed in. Future has two key methods: +// IMPORTANT: Every Future MUST be either .await()ed or .cancel()ed. +// Failing to do so leaks resources! A safe pattern is: // -// .await(io) - block until the result is ready, return it -// .cancel(io) - request cancellation, then return the result +// var future = io.async(myFn, .{}); +// defer _ = future.cancel(io); // safety net +// // ... later, if we want the result: +// const result = future.await(io); +// // (await after cancel is fine — it just returns the result) +// +// Both .await() and .cancel() block until the task finishes and +// return the result. The difference is that .cancel() also +// requests the task to stop at its next cancellation point. +// Calling either one more than once is safe — subsequent calls +// just return a copy of the result. // // Fix this program so that computeAnswer runs asynchronously // and its result is properly awaited. // const std = @import("std"); +const print = std.debug.print; pub fn main(init: std.process.Init) !void { const io = init.io; // Launch computeAnswer asynchronously. - // io.async() takes a function and a tuple of its arguments. var future = io.async(computeAnswer, .{ 6, 7 }); + defer _ = future.cancel(io); // always clean up! - // Meanwhile, print something to show we're not blocked. - std.debug.print("Computing... ", .{}); + print("Computing... ", .{}); // Now collect the result. What method on Future gives us - // the value, blocking if it isn't ready yet? + // the value, blocking until it's ready? const answer = future.???(io); - std.debug.print("The answer is: {}\n", .{answer}); + print("The answer is: {}\n", .{answer}); } fn computeAnswer(a: u32, b: u32) u32 { diff --git a/patches/patches/086_async2.patch b/patches/patches/086_async2.patch index 9a672a6..1738089 100644 --- a/patches/patches/086_async2.patch +++ b/patches/patches/086_async2.patch @@ -1,11 +1,11 @@ ---- exercises/086_async2.zig 2026-04-03 19:42:15.274532915 +0200 -+++ answers/086_async2.zig 2026-04-03 21:30:18.180019206 +0200 -@@ -38,7 +38,7 @@ +--- exercises/086_async2.zig 2026-04-05 12:41:11.350626443 +0200 ++++ answers/086_async2.zig 2026-04-05 12:42:00.879791167 +0200 +@@ -44,7 +44,7 @@ // Now collect the result. What method on Future gives us - // the value, blocking if it isn't ready yet? + // the value, blocking until it's ready? - const answer = future.???(io); + const answer = future.await(io); - std.debug.print("The answer is: {}\n", .{answer}); + print("The answer is: {}\n", .{answer}); } -- cgit v1.2.3 From 5e474ea5d1483cdca15c3b29295f0886950e4d94 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Sun, 5 Apr 2026 16:13:42 +0200 Subject: improvements for async-io --- exercises/087_async3.zig | 23 +++++++++++------------ patches/patches/087_async3.patch | 19 ++++++------------- 2 files changed, 17 insertions(+), 25 deletions(-) (limited to 'exercises') diff --git a/exercises/087_async3.zig b/exercises/087_async3.zig index 07221e9..d10052c 100644 --- a/exercises/087_async3.zig +++ b/exercises/087_async3.zig @@ -5,20 +5,17 @@ // them all. The Io backend may run them concurrently: // // var f1 = io.async(taskA, .{}); +// defer _ = f1.cancel(io); // var f2 = io.async(taskB, .{}); -// -// // Both tasks may be running now! +// defer _ = f2.cancel(io); // const a = f1.await(io); // const b = f2.await(io); // -// There's also io.concurrent() which provides a STRONGER guarantee: -// it ensures the function gets its own unit of concurrency (e.g. a -// real OS thread). But it can fail with error.ConcurrencyUnavailable -// if resources are exhausted. -// -// io.async() is more portable: if no thread is available, it simply -// runs the function synchronously. This makes it the right default -// for most code. +// Notice the defer pattern: each async call is immediately +// followed by a defer cancel. This ensures cleanup even if +// we return early or hit an error before reaching await. +// Since await/cancel are idempotent, the defer is harmless +// if we've already awaited. // // Fix this program to launch both tasks and collect their results. // @@ -29,12 +26,14 @@ pub fn main(init: std.process.Init) !void { const io = init.io; // Launch both tasks asynchronously. - var future_a = io.async(slowAdd, .{ 10, 20 }); + var future_a = io.async(slowAdd, .{ 1, 2 }); + defer _ = future_a.cancel(io); var future_b = ???(slowMul, .{ 6, 7 }); + defer _ = future_b.cancel(io); // Await both results. const sum = future_a.await(io); - const product = future_b.???(io); + const product = future_b.await(io); print("{} + {} = {}\n", .{ 1, 2, sum }); print("{} * {} = {}\n", .{ 6, 7, product }); diff --git a/patches/patches/087_async3.patch b/patches/patches/087_async3.patch index 8365e7a..91ba9af 100644 --- a/patches/patches/087_async3.patch +++ b/patches/patches/087_async3.patch @@ -1,18 +1,11 @@ ---- exercises/087_async3.zig 2026-04-01 22:51:05.540094851 +0200 -+++ answers/087_async3.zig 2026-04-01 22:50:44.579669189 +0200 -@@ -29,12 +29,12 @@ - const io = init.io; - +--- exercises/087_async3.zig 2026-04-05 16:12:48.317265515 +0200 ++++ answers/087_async3.zig 2026-04-05 16:12:52.269343030 +0200 +@@ -28,7 +28,7 @@ // Launch both tasks asynchronously. -- var future_a = io.async(slowAdd, .{ 10, 20 }); + var future_a = io.async(slowAdd, .{ 1, 2 }); + defer _ = future_a.cancel(io); - var future_b = ???(slowMul, .{ 6, 7 }); -+ var future_a = io.async(slowAdd, .{ 1, 2 }); + var future_b = io.async(slowMul, .{ 6, 7 }); + defer _ = future_b.cancel(io); // Await both results. - const sum = future_a.await(io); -- const product = future_b.???(io); -+ const product = future_b.await(io); - - print("{} + {} = {}\n", .{ 1, 2, sum }); - print("{} * {} = {}\n", .{ 6, 7, product }); -- cgit v1.2.3 From 446da3ce5a1d5ea12bffe5a8b12eaad94e8afeee Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 12:22:41 +0200 Subject: improvements for async-io --- exercises/088_async4.zig | 4 ++-- patches/patches/088_async4.patch | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'exercises') diff --git a/exercises/088_async4.zig b/exercises/088_async4.zig index 50829fc..8298ca1 100644 --- a/exercises/088_async4.zig +++ b/exercises/088_async4.zig @@ -14,7 +14,7 @@ // * Once you call group.async(), you MUST eventually call // group.await() or group.cancel() to release resources. // * group.cancel() requests cancellation on ALL members, -// then waits for them to finish. +// then blocks until they all finish. // // Unlike Future, Group tasks don't return values to the caller. // They're ideal for parallel work that communicates through @@ -38,7 +38,7 @@ pub fn main(init: std.process.Init) !void { // Wait for all tasks to finish. // What Group method blocks until all tasks complete? - try group.??? + try group.???(io); print("All tasks finished!\n", .{}); } diff --git a/patches/patches/088_async4.patch b/patches/patches/088_async4.patch index 1faf30e..6cf549f 100644 --- a/patches/patches/088_async4.patch +++ b/patches/patches/088_async4.patch @@ -1,10 +1,10 @@ ---- exercises/088_async4.zig 2026-04-01 23:17:31.066443941 +0200 -+++ answers/088_async4.zig 2026-04-01 23:17:39.251612131 +0200 +--- exercises/088_async4.zig 2026-04-06 12:22:06.643385622 +0200 ++++ answers/088_async4.zig 2026-04-06 12:22:11.820491035 +0200 @@ -38,7 +38,7 @@ // Wait for all tasks to finish. // What Group method blocks until all tasks complete? -- try group.??? +- try group.???(io); + try group.await(io); print("All tasks finished!\n", .{}); -- cgit v1.2.3 From 63e506586fad9458c8a881d003f57942542f2cde Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 16:57:48 +0200 Subject: improvements for async-io --- exercises/089_async5.zig | 12 +++++++++--- patches/patches/089_async5.patch | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) (limited to 'exercises') diff --git a/exercises/089_async5.zig b/exercises/089_async5.zig index 4fb8d76..0c00c1f 100644 --- a/exercises/089_async5.zig +++ b/exercises/089_async5.zig @@ -5,7 +5,7 @@ // Every Future has a .cancel() method that: // 1. Requests the task to stop (via error.Canceled at the // next "cancellation point") -// 2. Waits for the task to actually finish +// 2. BLOCKS until the task actually finishes // 3. Returns whatever result the task produced // // A "cancellation point" is any Io function that can return @@ -13,7 +13,7 @@ // // fn myTask(io: std.Io) u32 { // io.sleep(...) catch |err| switch (err) { -// error.Canceled => return 0, // handle gracefully +// error.Canceled => return 0, // error handle // }; // return 42; // } @@ -21,6 +21,11 @@ // This is fundamentally different from killing a thread - // the task gets a chance to clean up and return a value! // +// Remember: both .await() and .cancel() block and return the +// result. The only difference is that .cancel() also sends +// the cancellation request. And both are idempotent — calling +// either one again just returns the same result. +// // Fix this program: the slow task would take 10 seconds, // but we cancel it after 1 second. The task should detect // the cancellation and return early. @@ -32,6 +37,7 @@ pub fn main(init: std.process.Init) !void { const io = init.io; var future = io.async(slowTask, .{io}); + defer _ = future.cancel(io); // safety net // Wait 1 second, then cancel instead of waiting the full 10. io.sleep(std.Io.Duration.fromSeconds(1), .awake) catch {}; @@ -40,7 +46,7 @@ pub fn main(init: std.process.Init) !void { // We don't want to wait 10 seconds! // Which Future method requests cancellation AND returns the result? - const result = ???; + const result = future.???(io); print("Task returned: {}\n", .{result}); } diff --git a/patches/patches/089_async5.patch b/patches/patches/089_async5.patch index d2baa96..3cea4e9 100644 --- a/patches/patches/089_async5.patch +++ b/patches/patches/089_async5.patch @@ -1,10 +1,10 @@ ---- exercises/089_async5.zig 2026-04-01 23:40:40.505855238 +0200 -+++ answers/089_async5.zig 2026-04-01 23:40:10.176236971 +0200 -@@ -40,7 +40,7 @@ +--- exercises/089_async5.zig 2026-04-06 14:38:54.443726849 +0200 ++++ answers/089_async5.zig 2026-04-06 14:38:39.945438309 +0200 +@@ -46,7 +46,7 @@ // We don't want to wait 10 seconds! // Which Future method requests cancellation AND returns the result? -- const result = ???; +- const result = future.???(io); + const result = future.cancel(io); print("Task returned: {}\n", .{result}); -- cgit v1.2.3 From aeeb18931da3c8444fc85db4da43e72a0ad2a46a Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 18:50:57 +0200 Subject: improvements for async-io --- exercises/090_async6.zig | 7 ++++++- patches/patches/090_async6.patch | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'exercises') diff --git a/exercises/090_async6.zig b/exercises/090_async6.zig index eab03c9..16fb75f 100644 --- a/exercises/090_async6.zig +++ b/exercises/090_async6.zig @@ -23,6 +23,11 @@ // } // sel.cancelDiscard(); // cancel remaining, discard results // +// As with all async primitives: tasks spawned in a Select MUST +// be cleaned up. Use sel.cancel() to get remaining results one +// by one (for resource cleanup), or sel.cancelDiscard() if you +// don't need them. +// // The buffer must be large enough for all tasks that might // complete before you call cancelDiscard(). // @@ -47,7 +52,7 @@ pub fn main(init: std.process.Init) !void { // Wait for the first finisher. // What Select method returns the first completed result? - const winner = ???; + const winner = try sel.???(); switch (winner) { .hare => |msg| print("Hare: {s}\n", .{msg}), diff --git a/patches/patches/090_async6.patch b/patches/patches/090_async6.patch index 5ac777b..6289708 100644 --- a/patches/patches/090_async6.patch +++ b/patches/patches/090_async6.patch @@ -1,10 +1,10 @@ ---- exercises/090_async6.zig 2026-04-02 10:25:34.016616118 +0200 -+++ answers/090_async6.zig 2026-04-02 10:27:48.827144051 +0200 -@@ -47,7 +47,7 @@ +--- exercises/090_async6.zig 2026-04-06 18:49:37.232023422 +0200 ++++ answers/090_async6.zig 2026-04-06 18:49:22.189720687 +0200 +@@ -52,7 +52,7 @@ // Wait for the first finisher. // What Select method returns the first completed result? -- const winner = ???; +- const winner = try sel.???(); + const winner = try sel.await(); switch (winner) { -- cgit v1.2.3 From 09bae6a70e51fc1313297bc98c67c252e8c32d3e Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 19:30:56 +0200 Subject: improvements for async-io --- build.zig | 6 +++--- exercises/093_async9.zig | 42 ++++++++++++++++++++++++++-------------- patches/patches/093_async9.patch | 14 +++++++------- 3 files changed, 37 insertions(+), 25 deletions(-) (limited to 'exercises') diff --git a/build.zig b/build.zig index 3cddb61..dcafe14 100644 --- a/build.zig +++ b/build.zig @@ -1180,9 +1180,9 @@ const exercises = [_]Exercise{ .{ .main_file = "093_async9.zig", .output = - \\Main thread continues... - \\Computing on a separate thread! - \\Main thread done waiting. + \\Computing concurrently! + \\Main continues... + \\Main done waiting. \\Result: 123 , // pay attention to the comma }, diff --git a/exercises/093_async9.zig b/exercises/093_async9.zig index ad30dcf..4a41544 100644 --- a/exercises/093_async9.zig +++ b/exercises/093_async9.zig @@ -5,22 +5,29 @@ // The difference: // // io.async(): -// * The function MAY run on another thread, or it may run -// immediately on the current thread (synchronously). -// * Never fails — if no thread is available, it just runs -// the function right away. +// * The function MAY run on a separate unit of concurrency, +// or it may run immediately on the caller (synchronously). +// * Never fails — if no concurrency is available, it just +// runs the function right away. // * More portable, works with all Io backends. // // io.concurrent(): -// * GUARANTEES a separate unit of concurrency (a real thread -// in the Threaded backend). +// * GUARANTEES a separate unit of concurrency. // * Can fail with error.ConcurrencyUnavailable if resources // are exhausted or the backend doesn't support it. -// * Use when you NEED true parallelism. +// * Use when you NEED the task to run independently of the +// caller. +// +// What is a "unit of concurrency"? That depends on the backend! +// The Threaded backend uses OS threads. But the Evented backends +// (Uring, Kqueue, Dispatch) use M:N green threads / fibers, +// which can provide concurrency even on a SINGLE OS thread. +// Your code doesn't need to know the difference. // // Because concurrent() can fail, you must handle the error: // // var future = try io.concurrent(myFn, .{args}); +// defer _ = future.cancel(io); // const result = future.await(io); // // Notice the 'try' — that's the key difference in usage! @@ -33,25 +40,30 @@ const print = std.debug.print; pub fn main(init: std.process.Init) !void { const io = init.io; - // Launch with a guaranteed separate thread. - // Which Io method guarantees true concurrency? + // Launch with a guaranteed separate unit of concurrency. + // Which Io method guarantees this? // (Hint: unlike io.async, this one can fail!) var future = try io.???(compute, .{io}); + defer _ = future.cancel(io); - print("Main thread continues...\n", .{}); - - // Wait 100 millisecond so the output order is deterministic. + // Note: All breaks in this excercise (using sleep) + // are only necessary for a deterministic result. io.sleep(std.Io.Duration.fromMilliseconds(100), .awake) catch {}; - print("Main thread done waiting.\n", .{}); + print("Main continues...\n", .{}); + + // Wait 1 second for the output order. + io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch {}; + + print("Main done waiting.\n", .{}); const result = future.await(io); print("Result: {}\n", .{result}); } fn compute(io: std.Io) u32 { - print("Computing on a separate thread!\n", .{}); + print("Computing concurrently!\n", .{}); // Simulate some work. - io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch return 0; + io.sleep(std.Io.Duration.fromMilliseconds(400), .awake) catch return 0; return 123; } diff --git a/patches/patches/093_async9.patch b/patches/patches/093_async9.patch index f759921..ebdfce2 100644 --- a/patches/patches/093_async9.patch +++ b/patches/patches/093_async9.patch @@ -1,11 +1,11 @@ ---- exercises/093_async9.zig 2026-04-03 13:44:50.526780809 +0200 -+++ answers/093_async9.zig 2026-04-03 13:44:54.957870294 +0200 -@@ -36,7 +36,7 @@ - // Launch with a guaranteed separate thread. - // Which Io method guarantees true concurrency? +--- exercises/093_async9.zig 2026-04-06 19:26:11.388025362 +0200 ++++ answers/093_async9.zig 2026-04-06 19:18:36.242931688 +0200 +@@ -43,7 +43,7 @@ + // Launch with a guaranteed separate unit of concurrency. + // Which Io method guarantees this? // (Hint: unlike io.async, this one can fail!) - var future = try io.???(compute, .{io}); + var future = try io.concurrent(compute, .{io}); + defer _ = future.cancel(io); - print("Main thread continues...\n", .{}); - + // Note: All breaks in this excercise (using sleep) -- cgit v1.2.3 From 55a4841b07ac5d20fdd9f272fdbdd1875a7b4431 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 19:38:19 +0200 Subject: improvements for async-io --- exercises/094_async10.zig | 11 ++++++----- patches/patches/094_async10.patch | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) (limited to 'exercises') diff --git a/exercises/094_async10.zig b/exercises/094_async10.zig index 6ed229d..c561d37 100644 --- a/exercises/094_async10.zig +++ b/exercises/094_async10.zig @@ -1,5 +1,5 @@ // -// In exercise 088, we learned that cancellation happens at +// In exercise 089, we learned that cancellation happens at // "cancellation points" — any Io function that can return // error.Canceled. // @@ -11,7 +11,7 @@ // // const old = io.swapCancelProtection(.blocked); // defer _ = io.swapCancelProtection(old); -// + // // In this block, NO Io function will return error.Canceled. // // The cancel request is held until protection is restored. // @@ -36,9 +36,10 @@ pub fn main(init: std.process.Init) !void { const io = init.io; var future = io.async(importantTask, .{io}); + defer _ = future.cancel(io); // Give the task time to start and enter its critical section. - io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch {}; + io.sleep(std.Io.Duration.fromMilliseconds(200), .awake) catch {}; // Cancel while the task is in its protected section. const result = future.cancel(io); @@ -50,12 +51,12 @@ fn importantTask(io: std.Io) []const u8 { // Protect this section from cancellation. // What method swaps the cancel protection state? - const old = io.???(. blocked); + const old = io.???(.blocked); defer _ = io.???(old); // This sleep will NOT return error.Canceled even though // we get canceled during it — protection is active! - io.sleep(std.Io.Duration.fromMilliseconds(600), .awake) catch |err| switch (err) { + io.sleep(std.Io.Duration.fromMilliseconds(300), .awake) catch |err| switch (err) { error.Canceled => { // This should never happen while protected! return "ERROR: canceled during critical section!"; diff --git a/patches/patches/094_async10.patch b/patches/patches/094_async10.patch index ae0d26d..e721485 100644 --- a/patches/patches/094_async10.patch +++ b/patches/patches/094_async10.patch @@ -1,10 +1,10 @@ ---- exercises/094_async10.zig 2026-04-03 14:25:16.600025924 +0200 -+++ answers/094_async10.zig 2026-04-03 14:24:56.192615893 +0200 -@@ -50,8 +50,8 @@ +--- exercises/094_async10.zig 2026-04-06 19:36:59.873966580 +0200 ++++ answers/094_async10.zig 2026-04-06 19:37:12.416216872 +0200 +@@ -51,8 +51,8 @@ // Protect this section from cancellation. // What method swaps the cancel protection state? -- const old = io.???(. blocked); +- const old = io.???(.blocked); - defer _ = io.???(old); + const old = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(old); -- cgit v1.2.3 From 882c6aa0ab174f0d7a28a7804531611fcef98476 Mon Sep 17 00:00:00 2001 From: Chris Boesch Date: Mon, 6 Apr 2026 19:57:32 +0200 Subject: improvements for async-io --- exercises/095_quiz_async.zig | 60 +++++++++++++++++++----------------- patches/patches/095_quiz_async.patch | 54 +++++++++++++++++--------------- 2 files changed, 60 insertions(+), 54 deletions(-) (limited to 'exercises') diff --git a/exercises/095_quiz_async.zig b/exercises/095_quiz_async.zig index fb78e7b..b116fc9 100644 --- a/exercises/095_quiz_async.zig +++ b/exercises/095_quiz_async.zig @@ -16,16 +16,17 @@ // by a grasshopper) and left several bugs. Can you fix them? // // Here's what the program should do: -// 1. Three sensor tasks run concurrently, each sending -// exactly 3 readings through a Queue -// 2. A collector task receives readings, protected by a Mutex +// 1. Three sensor tasks send exactly 3 readings each through +// a Queue +// 2. A collector task receives readings concurrently, +// protected by a Mutex // 3. After all sensors finish, the queue is closed // 4. The final report is written in a cancel-protected section // // ************************************************************* // * A NOTE ABOUT THIS EXERCISE * // * * -// * This quiz uses concepts from exercises 084-093. * +// * This quiz uses concepts from exercises 085-094. * // * There are 6 bugs to fix — look for the ???s! * // * * // ************************************************************* @@ -50,8 +51,8 @@ const GardenWeather = struct { fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { // Bug 1: The collector needs to lock before modifying // shared state. What Mutex method acquires the lock? - self.mutex.lock(io) catch return; self.mutex.???(io) catch return; + defer self.mutex.unlock(io); switch (reading.sensor_type) { .thermometer => self.temperature = reading.value, @@ -70,39 +71,40 @@ pub fn main(init: std.process.Init) !void { var reading_buf: [8]Reading = undefined; var queue: std.Io.Queue(Reading) = .init(&reading_buf); - // Sensor group: runs all three sensors to completion. - var sensors: std.Io.Group = .init; - - // Start three sensor tasks. They need GUARANTEED concurrency - // since they each simulate real-time measurement. + // The collector must run concurrently so it can process + // readings while the sensors are still sending. + // Start it FIRST to ensure its concurrency unit is reserved. // - // Bug 2: 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 }); + // Bug 2: The collector needs guaranteed concurrency. + // What method ensures a separate unit of concurrency? + // (Don't forget: it can fail!) + var collector_future = try io.???(collector, .{ io, &queue, &weather }); + defer _ = collector_future.cancel(io); + + // Sensor group: the sensors can use async — they just need + // to run, and async is more portable. + var sensors: std.Io.Group = .init; - // Collector group: processes readings from the queue. - var collectors: std.Io.Group = .init; - collectors.async(io, collector, .{ io, &queue, &weather }); + sensors.async(io, sensor, .{ io, &queue, .thermometer, 20 }); + sensors.async(io, sensor, .{ io, &queue, .hygrometer, 60 }); + sensors.async(io, sensor, .{ io, &queue, .anemometer, 10 }); // Bug 3: Wait for ALL sensors to finish sending their readings. // What Group method blocks until all tasks complete? - try sensors.await(io); - // try sensors.???(io); + try sensors.???(io); // All sensors done — close the queue so the collector knows // there's no more data coming. queue.close(io); - // Wait for the collector to drain the queue. - try collectors.await(io); + // Wait for the collector to drain the remaining queue. + _ = collector_future.await(io); + // _ = collector_future.???(io); // Now write the garden report. This is critical — it must // NOT be interrupted, even if something tries to cancel us! // - // Bug 4: Protect this section from cancellation. + // Bug 5: Protect this section from cancellation. // What Io method swaps the cancel protection state? const old_protection = io.???(.blocked); defer _ = io.???(old_protection); @@ -125,7 +127,7 @@ fn sensor( .value = base_value + @as(i32, @intCast(i)), }; - // Bug 5: Send the reading into the queue. + // Bug 6: Send the reading into the queue. // What Queue method sends a single element? queue.???(io, reading) catch return; } @@ -163,8 +165,7 @@ fn printGardenReport(weather: *GardenWeather) void { // // This quiz covered the main async I/O primitives: // io.async() - launch a task (may run inline) -// io.concurrent() - launch with guaranteed parallelism -// Group.concurrent() - concurrent tasks in a group +// io.concurrent() - guaranteed unit of concurrency // Future.await/cancel - collect or cancel a single task // Group.async/await/cancel - manage fire-and-forget tasks // Select.async/await - race tasks, act on first completion @@ -180,7 +181,8 @@ fn printGardenReport(weather: *GardenWeather) void { // Batch - submit multiple I/O operations at once // // The key insight: all of these work through the Io VTable, -// so your code is portable across backends (Threaded, Uring, -// Kqueue, Dispatch) without any changes! +// so your code is portable across backends — whether Threaded +// (OS thread pool), or Evented (M:N green threads / fibers +// that can provide concurrency even on a single OS thread). // // Doctor Zoraptera approves. diff --git a/patches/patches/095_quiz_async.patch b/patches/patches/095_quiz_async.patch index dbaae07..e3d2a79 100644 --- a/patches/patches/095_quiz_async.patch +++ b/patches/patches/095_quiz_async.patch @@ -1,38 +1,42 @@ ---- exercises/095_quiz_async.zig 2026-04-03 18:04:53.577391455 +0200 -+++ answers/095_quiz_async.zig 2026-04-03 18:05:42.570392172 +0200 +--- exercises/095_quiz_async.zig 2026-04-06 19:55:17.111817364 +0200 ++++ answers/095_quiz_async.zig 2026-04-06 19:56:16.063974543 +0200 @@ -51,7 +51,7 @@ + fn addReading(self: *GardenWeather, io: std.Io, reading: Reading) void { // Bug 1: The collector needs to lock before modifying // shared state. What Mutex method acquires the lock? - self.mutex.lock(io) catch return; - self.mutex.???(io) catch return; -+ defer self.mutex.unlock(io); ++ self.mutex.lock(io) catch return; + defer self.mutex.unlock(io); switch (reading.sensor_type) { - .thermometer => self.temperature = reading.value, -@@ -79,9 +79,9 @@ - // Bug 2: io.async doesn't guarantee a separate thread. - // Which Io method guarantees true concurrency? - // (Don't forget: it can fail, so you need 'try'!) -- try sensors.???(io, sensor, .{ io, &queue, .thermometer, 20 }); -- try sensors.???(io, sensor, .{ io, &queue, .hygrometer, 60 }); -- try sensors.???(io, sensor, .{ io, &queue, .anemometer, 10 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .thermometer, 20 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .hygrometer, 60 }); -+ try sensors.concurrent(io, sensor, .{ io, &queue, .anemometer, 10 }); - - // Collector group: processes readings from the queue. - var collectors: std.Io.Group = .init; -@@ -90,7 +90,6 @@ +@@ -78,7 +78,7 @@ + // Bug 2: The collector needs guaranteed concurrency. + // What method ensures a separate unit of concurrency? + // (Don't forget: it can fail!) +- var collector_future = try io.???(collector, .{ io, &queue, &weather }); ++ var collector_future = try io.concurrent(collector, .{ io, &queue, &weather }); + defer _ = collector_future.cancel(io); + + // Sensor group: the sensors can use async — they just need +@@ -91,7 +91,7 @@ + // Bug 3: Wait for ALL sensors to finish sending their readings. // What Group method blocks until all tasks complete? - try sensors.await(io); -- // try sensors.???(io); +- try sensors.???(io); ++ try sensors.await(io); // All sensors done — close the queue so the collector knows // there's no more data coming. -@@ -104,8 +103,8 @@ +@@ -99,15 +99,14 @@ + + // Wait for the collector to drain the remaining queue. + _ = collector_future.await(io); +- // _ = collector_future.???(io); + + // Now write the garden report. This is critical — it must + // NOT be interrupted, even if something tries to cancel us! // - // Bug 4: Protect this section from cancellation. + // Bug 5: Protect this section from cancellation. // What Io method swaps the cancel protection state? - const old_protection = io.???(.blocked); - defer _ = io.???(old_protection); @@ -41,9 +45,9 @@ printGardenReport(&weather); } -@@ -127,7 +126,7 @@ +@@ -129,7 +128,7 @@ - // Bug 5: Send the reading into the queue. + // Bug 6: Send the reading into the queue. // What Queue method sends a single element? - queue.???(io, reading) catch return; + queue.putOne(io, reading) catch return; -- cgit v1.2.3