diff options
Diffstat (limited to 'exercises')
| -rw-r--r-- | exercises/084_async.zig | 58 | ||||
| -rw-r--r-- | exercises/084_interfaces.zig (renamed from exercises/092_interfaces.zig) | 2 | ||||
| -rw-r--r-- | exercises/085_async.zig | 48 | ||||
| -rw-r--r-- | exercises/085_async2.zig | 28 | ||||
| -rw-r--r-- | exercises/086_async2.zig | 48 | ||||
| -rw-r--r-- | exercises/086_async3.zig | 29 | ||||
| -rw-r--r-- | exercises/087_async3.zig | 50 | ||||
| -rw-r--r-- | exercises/087_async4.zig | 30 | ||||
| -rw-r--r-- | exercises/088_async4.zig | 50 | ||||
| -rw-r--r-- | exercises/088_async5.zig | 48 | ||||
| -rw-r--r-- | exercises/089_async5.zig | 61 | ||||
| -rw-r--r-- | exercises/089_async6.zig | 54 | ||||
| -rw-r--r-- | exercises/090_async6.zig | 71 | ||||
| -rw-r--r-- | exercises/090_async7.zig | 87 | ||||
| -rw-r--r-- | exercises/091_async7.zig | 57 | ||||
| -rw-r--r-- | exercises/091_async8.zig | 35 | ||||
| -rw-r--r-- | exercises/092_async8.zig | 62 | ||||
| -rw-r--r-- | exercises/093_async9.zig | 57 | ||||
| -rw-r--r-- | exercises/094_async10.zig | 67 | ||||
| -rw-r--r-- | exercises/095_quiz_async.zig | 186 | ||||
| -rw-r--r-- | exercises/096_hello_c.zig (renamed from exercises/093_hello_c.zig) | 0 | ||||
| -rw-r--r-- | exercises/097_c_math.zig (renamed from exercises/094_c_math.zig) | 0 | ||||
| -rw-r--r-- | exercises/098_for3.zig (renamed from exercises/095_for3.zig) | 0 | ||||
| -rw-r--r-- | exercises/099_memory_allocation.zig (renamed from exercises/096_memory_allocation.zig) | 0 | ||||
| -rw-r--r-- | exercises/100_bit_manipulation.zig (renamed from exercises/097_bit_manipulation.zig) | 0 | ||||
| -rw-r--r-- | exercises/101_bit_manipulation2.zig (renamed from exercises/098_bit_manipulation2.zig) | 0 | ||||
| -rw-r--r-- | exercises/102_formatting.zig (renamed from exercises/099_formatting.zig) | 0 | ||||
| -rw-r--r-- | exercises/103_for4.zig (renamed from exercises/100_for4.zig) | 0 | ||||
| -rw-r--r-- | exercises/104_for5.zig (renamed from exercises/101_for5.zig) | 0 | ||||
| -rw-r--r-- | exercises/105_testing.zig (renamed from exercises/102_testing.zig) | 0 | ||||
| -rw-r--r-- | exercises/106_tokenization.zig (renamed from exercises/103_tokenization.zig) | 0 | ||||
| -rw-r--r-- | exercises/107_threading.zig (renamed from exercises/104_threading.zig) | 53 | ||||
| -rw-r--r-- | exercises/108_threading2.zig (renamed from exercises/105_threading2.zig) | 0 | ||||
| -rw-r--r-- | exercises/109_files.zig (renamed from exercises/106_files.zig) | 0 | ||||
| -rw-r--r-- | exercises/110_files2.zig (renamed from exercises/107_files2.zig) | 0 | ||||
| -rw-r--r-- | exercises/111_labeled_switch.zig (renamed from exercises/108_labeled_switch.zig) | 0 | ||||
| -rw-r--r-- | exercises/112_vectors.zig (renamed from exercises/109_vectors.zig) | 0 | ||||
| -rw-r--r-- | exercises/113_quiz9.zig (renamed from exercises/110_quiz9.zig) | 0 | ||||
| -rw-r--r-- | exercises/114_packed.zig (renamed from exercises/111_packed.zig) | 0 | ||||
| -rw-r--r-- | exercises/115_packed2.zig (renamed from exercises/112_packed2.zig) | 0 |
40 files changed, 780 insertions, 401 deletions
diff --git a/exercises/084_async.zig b/exercises/084_async.zig deleted file mode 100644 index 56c9969..0000000 --- a/exercises/084_async.zig +++ /dev/null @@ -1,58 +0,0 @@ -// -// Six Facts: -// -// 1. The memory space allocated to your program for the -// invocation of a function and all of its data is called a -// "stack frame". -// -// 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. -// -// 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 -// } -// -// 4. To call any function in async context and get a reference -// to its frame for later use, use the 'async' keyword: -// -// var foo_frame = async fooThatSuspends(); -// -// 5. If you call an async function without the 'async' keyword, -// the function FROM WHICH you called the async function itself -// becomes async! In this example, the bar() function is now -// async because it calls fooThatSuspends(), which is async. -// -// fn bar() void { -// fooThatSuspends(); -// } -// -// 6. The main() function cannot be async! -// -// Given facts 3 and 4, how do we fix this program (broken by facts -// 5 and 6)? -// -const print = @import("std").debug.print; - -pub fn main() void { - // Additional Hint: you can assign things to '_' when you - // don't intend to do anything with them. - foo(); -} - -fn foo() void { - print("foo() A\n", .{}); - suspend {} - print("foo() B\n", .{}); -} diff --git a/exercises/092_interfaces.zig b/exercises/084_interfaces.zig index 7775dd5..fd61ead 100644 --- a/exercises/092_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/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 036aefa..0000000 --- a/exercises/085_async2.zig +++ /dev/null @@ -1,28 +0,0 @@ -// -// 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? -// -// For that, we have a new keyword called 'resume' which takes an -// async function invocation's frame and returns control to it. -// -// fn fooThatSuspends() void { -// suspend {} -// } -// -// var foo_frame = async fooThatSuspends(); -// resume foo_frame; -// -// See if you can make this program print "Hello async!". -// -const print = @import("std").debug.print; - -pub fn main() void { - var foo_frame = async foo(); -} - -fn foo() void { - print("Hello ", .{}); - suspend {} - print("async!\n", .{}); -} 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 c8f1113..0000000 --- a/exercises/086_async3.zig +++ /dev/null @@ -1,29 +0,0 @@ -// -// 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. -// -// See if you can make this program print "5 4 3 2 1". -// -const print = @import("std").debug.print; - -pub fn main() void { - const n = 5; - var foo_frame = async foo(n); - - ??? - - print("\n", .{}); -} - -fn foo(countdown: u32) void { - var current = countdown; - - while (current > 0) { - print("{} ", .{current}); - current -= 1; - suspend {} - } -} 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 bb9c9ec..0000000 --- a/exercises/087_async4.zig +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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. -// -// One way to solve this is to use a global variable. -// -// See if you can make this program print "1 2 3 4 5". -// -const print = @import("std").debug.print; - -var global_counter: i32 = 0; - -pub fn main() void { - var foo_frame = async foo(); - - while (global_counter <= 5) { - print("{} ", .{global_counter}); - ??? - } - - print("\n", .{}); -} - -fn foo() void { - while (true) { - ??? - ??? - } -} 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 759a920..0000000 --- a/exercises/088_async5.zig +++ /dev/null @@ -1,48 +0,0 @@ -// -// 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; -// } -// -// 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') -// -// Also notice that a 'suspend' keyword does NOT need to exist in -// a function to be called in an async context. -// -// Please use 'await' to get the string returned by -// getPageTitle(). -// -const print = @import("std").debug.print; - -pub fn main() void { - var myframe = async getPageTitle("http://example.com"); - - var value = ??? - - print("{s}\n", .{value}); -} - -fn getPageTitle(url: []const u8) []const u8 { - // Please PRETEND this is actually making a network request. - _ = url; - return "Example Title."; -} 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 8bf50e9..0000000 --- a/exercises/089_async6.zig +++ /dev/null @@ -1,54 +0,0 @@ -// -// 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. -// -// +---------+ -// | Start | -// +---------+ -// / \ -// / \ -// +---------+ +---------+ -// | Foo | | Bar | -// +---------+ +---------+ -// \ / -// \ / -// +---------+ -// | End | -// +---------+ -// -// We can express this in Zig like so: -// -// fn foo() u32 { ... } -// fn bar() u32 { ... } -// -// // Start -// -// var foo_frame = async foo(); -// var bar_frame = async bar(); -// -// var foo_value = await foo_frame; -// var bar_value = await bar_frame; -// -// // End -// -// Please await TWO page titles! -// -const print = @import("std").debug.print; - -pub fn main() void { - var com_frame = async getPageTitle("http://example.com"); - var org_frame = async getPageTitle("http://example.org"); - - var com_title = com_frame; - var org_title = org_frame; - - print(".com: {s}, .org: {s}.\n", .{ com_title, org_title }); -} - -fn getPageTitle(url: []const u8) []const u8 { - // Please PRETEND this is actually making a network request. - _ = url; - return "Example Title"; -} 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 2da3a4a..0000000 --- a/exercises/090_async7.zig +++ /dev/null @@ -1,87 +0,0 @@ -// -// Remember how a function with 'suspend' is async and calling an -// async function without the 'async' keyword makes the CALLING -// function async? -// -// fn fooThatMightSuspend(maybe: bool) void { -// if (maybe) suspend {} -// } -// -// fn bar() void { -// fooThatMightSuspend(true); // Now bar() is async! -// } -// -// But if you KNOW the function won't suspend, you can make a -// promise to the compiler with the 'nosuspend' keyword: -// -// fn bar() void { -// nosuspend fooThatMightSuspend(false); -// } -// -// 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! >:-( -// -const print = @import("std").debug.print; - -pub fn main() void { - - // 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); - - print("beef? {X}!\n", .{my_beef}); -} - -fn getBeef(input: u32) u32 { - if (input == 0xDEAD) { - suspend {} - } - - return 0xBEEF; -} -// -// Going Deeper Into... -// ...uNdeFiNEd beHAVi0r! -// -// We haven't discussed it yet, but runtime "safety" features -// require some extra instructions in your compiled program. -// Most of the time, you're going to want to keep these in. -// -// But in some programs, when data integrity is less important -// than raw speed (some games, for example), you can compile -// without these safety features. -// -// Instead of a safe panic when something goes wrong, your -// program will now exhibit Undefined Behavior (UB), which simply -// means that the Zig language does not (cannot) define what will -// happen. The best case is that it will crash, but in the worst -// case, it will continue to run with the wrong results and -// corrupt your data or expose you to security risks. -// -// This program is a great way to explore UB. Once you get it -// working, try calling the getBeef() function with the value -// 0xDEAD so that it will invoke the 'suspend' keyword: -// -// getBeef(0xDEAD) -// -// Now when you run the program, it will panic and give you a -// nice stack trace to help debug the problem. -// -// zig run exercises/090_async7.zig -// thread 328 panic: async function called... -// ... -// -// But see what happens when you turn off safety checks by using -// ReleaseFast mode: -// -// zig run -O ReleaseFast exercises/090_async7.zig -// beef? 0! -// -// This is the wrong result. On your computer, you may get a -// different answer or it might crash! What exactly will happen -// is UNDEFINED. Your computer is now like a wild animal, -// reacting to bits and bytes of raw memory with the base -// instincts of the CPU. It is both terrifying and exhilarating. -// diff --git a/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 cd9c975..0000000 --- a/exercises/091_async8.zig +++ /dev/null @@ -1,35 +0,0 @@ -// -// You have doubtless noticed that 'suspend' requires a block -// expression like so: -// -// suspend {} -// -// The suspend block executes when a function suspends. To get -// sense for when this happens, please make the following -// program print the string -// -// "ABCDEF" -// -const print = @import("std").debug.print; - -pub fn main() void { - print("A", .{}); - - var frame = async suspendable(); - - print("X", .{}); - - resume frame; - - print("F", .{}); -} - -fn suspendable() void { - print("X", .{}); - - suspend { - print("X", .{}); - } - - print("X", .{}); -} 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/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_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/exercises/093_hello_c.zig b/exercises/096_hello_c.zig index 182e0b0..182e0b0 100644 --- a/exercises/093_hello_c.zig +++ b/exercises/096_hello_c.zig diff --git a/exercises/094_c_math.zig b/exercises/097_c_math.zig index ec59a86..ec59a86 100644 --- a/exercises/094_c_math.zig +++ b/exercises/097_c_math.zig diff --git a/exercises/095_for3.zig b/exercises/098_for3.zig index 77a1b56..77a1b56 100644 --- a/exercises/095_for3.zig +++ b/exercises/098_for3.zig diff --git a/exercises/096_memory_allocation.zig b/exercises/099_memory_allocation.zig index 58de7b0..58de7b0 100644 --- a/exercises/096_memory_allocation.zig +++ b/exercises/099_memory_allocation.zig diff --git a/exercises/097_bit_manipulation.zig b/exercises/100_bit_manipulation.zig index 0e64ad7..0e64ad7 100644 --- a/exercises/097_bit_manipulation.zig +++ b/exercises/100_bit_manipulation.zig diff --git a/exercises/098_bit_manipulation2.zig b/exercises/101_bit_manipulation2.zig index 8b51265..8b51265 100644 --- a/exercises/098_bit_manipulation2.zig +++ b/exercises/101_bit_manipulation2.zig diff --git a/exercises/099_formatting.zig b/exercises/102_formatting.zig index be16978..be16978 100644 --- a/exercises/099_formatting.zig +++ b/exercises/102_formatting.zig diff --git a/exercises/100_for4.zig b/exercises/103_for4.zig index c8a1161..c8a1161 100644 --- a/exercises/100_for4.zig +++ b/exercises/103_for4.zig diff --git a/exercises/101_for5.zig b/exercises/104_for5.zig index 200e71d..200e71d 100644 --- a/exercises/101_for5.zig +++ b/exercises/104_for5.zig diff --git a/exercises/102_testing.zig b/exercises/105_testing.zig index 0dec9b9..0dec9b9 100644 --- a/exercises/102_testing.zig +++ b/exercises/105_testing.zig diff --git a/exercises/103_tokenization.zig b/exercises/106_tokenization.zig index d0abb5c..d0abb5c 100644 --- a/exercises/103_tokenization.zig +++ b/exercises/106_tokenization.zig diff --git a/exercises/104_threading.zig b/exercises/107_threading.zig index 2b0e6f7..3c3fa21 100644 --- a/exercises/104_threading.zig +++ b/exercises/107_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/exercises/105_threading2.zig b/exercises/108_threading2.zig index 374391a..374391a 100644 --- a/exercises/105_threading2.zig +++ b/exercises/108_threading2.zig diff --git a/exercises/106_files.zig b/exercises/109_files.zig index bf4de9b..bf4de9b 100644 --- a/exercises/106_files.zig +++ b/exercises/109_files.zig diff --git a/exercises/107_files2.zig b/exercises/110_files2.zig index c363086..c363086 100644 --- a/exercises/107_files2.zig +++ b/exercises/110_files2.zig diff --git a/exercises/108_labeled_switch.zig b/exercises/111_labeled_switch.zig index 897fcf5..897fcf5 100644 --- a/exercises/108_labeled_switch.zig +++ b/exercises/111_labeled_switch.zig diff --git a/exercises/109_vectors.zig b/exercises/112_vectors.zig index 96892ca..96892ca 100644 --- a/exercises/109_vectors.zig +++ b/exercises/112_vectors.zig diff --git a/exercises/110_quiz9.zig b/exercises/113_quiz9.zig index 8f5cb61..8f5cb61 100644 --- a/exercises/110_quiz9.zig +++ b/exercises/113_quiz9.zig diff --git a/exercises/111_packed.zig b/exercises/114_packed.zig index 1058f76..1058f76 100644 --- a/exercises/111_packed.zig +++ b/exercises/114_packed.zig diff --git a/exercises/112_packed2.zig b/exercises/115_packed2.zig index 2b6b558..2b6b558 100644 --- a/exercises/112_packed2.zig +++ b/exercises/115_packed2.zig |
