diff --git a/README.md b/README.md index e62353a..f42d9dd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # Ziglings -# ⚠️ (My solutions, not the [original exercises](https://codeberg.org/ziglings/exercises)) +# ⚠️ My solutions, not the [original exercises](https://codeberg.org/ziglings/exercises) diff --git a/build.zig b/build.zig index 71ea66f..8319bb4 100644 --- a/build.zig +++ b/build.zig @@ -15,7 +15,7 @@ const print = std.debug.print; // 1) Getting Started // 2) Version Changes comptime { - const required_zig = "0.12.0-dev.2618"; + const required_zig = "0.12.0-dev.3397"; const current_zig = builtin.zig_version; const min_zig = std.SemanticVersion.parse(required_zig) catch unreachable; if (current_zig.order(min_zig) == .lt) { @@ -119,7 +119,7 @@ pub const logo = ; pub fn build(b: *Build) !void { - if (!validate_exercises()) std.os.exit(2); + if (!validate_exercises()) std.process.exit(2); use_color_escapes = false; if (std.io.getStdErr().supportsAnsiEscapeCodes()) { @@ -172,7 +172,7 @@ pub fn build(b: *Build) !void { // Named build mode: verifies a single exercise. if (n == 0 or n > exercises.len - 1) { print("unknown exercise number: {}\n", .{n}); - std.os.exit(2); + std.process.exit(2); } const ex = exercises[n - 1]; @@ -262,7 +262,7 @@ const ZiglingStep = struct { print("\n{s}Ziglings hint: {s}{s}", .{ bold_text, hint, reset_text }); self.help(); - std.os.exit(2); + std.process.exit(2); }; self.run(exe_path.?, prog_node) catch { @@ -272,7 +272,7 @@ const ZiglingStep = struct { print("\n{s}Ziglings hint: {s}{s}", .{ bold_text, hint, reset_text }); self.help(); - std.os.exit(2); + std.process.exit(2); }; // Print possible warning/debug messages. @@ -939,7 +939,7 @@ const exercises = [_]Exercise{ .{ .main_file = "082_anonymous_structs3.zig", .output = - \\"0"(bool):true "1"(bool):false "2"(i32):42 "3"(f32):3.14159202e+00 + \\"0"(bool):true "1"(bool):false "2"(i32):42 "3"(f32):3.141592e0 , .hint = "This one is a challenge! But you have everything you need.", }, @@ -1103,6 +1103,24 @@ const exercises = [_]Exercise{ \\This little poem has 15 words! , }, + .{ + .main_file = "104_threading.zig", + .output = + \\Starting work... + \\thread 1: started. + \\thread 2: started. + \\thread 3: started. + \\Some weird stuff, after starting the threads. + \\thread 2: finished. + \\thread 1: finished. + \\thread 3: finished. + \\Zig is cool! + , + }, + .{ + .main_file = "105_threading2.zig", + .output = "PI ≈ 3.14159265", + }, .{ .main_file = "999_the_end.zig", .output = diff --git a/exercises/051_values.zig b/exercises/051_values.zig index 848b6a8..debd521 100644 --- a/exercises/051_values.zig +++ b/exercises/051_values.zig @@ -141,9 +141,20 @@ pub fn main() void { // // Moving along... // - // Passing arguments to functions is pretty much exactly like - // making an assignment to a const (since Zig enforces that ALL - // function parameters are const). + // When arguments are passed to a function, + // they are ALWAYS passed as constants within the function, + // regardless of how they were declared in the calling function. + // + // Example: + // fn foo(arg: u8) void { + // arg = 42; // Error, 'arg' is const! + // } + // + // fn bar() void { + // var arg: u8 = 12; + // foo(arg); + // ... + // } // // Knowing this, see if you can make levelUp() work as expected - // it should add the specified amount to the supplied character's diff --git a/exercises/059_integers.zig b/exercises/059_integers.zig index 39e077c..ae65790 100644 --- a/exercises/059_integers.zig +++ b/exercises/059_integers.zig @@ -2,12 +2,12 @@ // Zig lets you express integer literals in several convenient // formats. These are all the same value: // -// const a1: u8 = 65; // decimal -// const a2: u8 = 0x41; // hexadecimal -// const a3: u8 = 0o101; // octal -// const a4: u8 = 0b1000001; // binary -// const a5: u8 = 'A'; // ASCII code point literal -// const a6: u16 = 'Ȁ'; // Unicode code points can take up to 21 bits +// const a1: u8 = 65; // decimal +// const a2: u8 = 0x41; // hexadecimal +// const a3: u8 = 0o101; // octal +// const a4: u8 = 0b1000001; // binary +// const a5: u8 = 'A'; // ASCII code point literal +// const a6: u16 = '\u{0041}'; // Unicode code points can take up to 21 bits // // You can also place underscores in numbers to aid readability: // diff --git a/exercises/094_c_math.zig b/exercises/094_c_math.zig index e650f6e..61e2c7b 100644 --- a/exercises/094_c_math.zig +++ b/exercises/094_c_math.zig @@ -1,19 +1,26 @@ // // Often, C functions are used where no equivalent Zig function exists -// yet. Since the integration of a C function is very simple, as already +// 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 won't -// work, because the standard modulo function works only with integer -// values. In the C 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. +// 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"); diff --git a/exercises/104_threading.zig b/exercises/104_threading.zig new file mode 100644 index 0000000..8aeb683 --- /dev/null +++ b/exercises/104_threading.zig @@ -0,0 +1,129 @@ +// +// 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. +// +// However, the computing power of the processor is only distributed to +// the started tasks, which always reaches its limits when pure computing +// power is called up. +// +// 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. +// +// 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. +// +// +// 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. + std.time.sleep((1) * std.time.ns_per_s); + 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(num: usize) !void { + std.debug.print("thread {d}: {s}\n", .{ num, "started." }); + std.time.sleep((5 - num % 3) * std.time.ns_per_s); + std.debug.print("thread {d}: {s}\n", .{ num, "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_threading2.zig b/exercises/105_threading2.zig new file mode 100644 index 0000000..1330999 --- /dev/null +++ b/exercises/105_threading2.zig @@ -0,0 +1,107 @@ +// +// Now that we are familiar with the principles of multi threading, we +// 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 Leibnitz in 1682 in the journal +// "Acta Eruditorum". +// This is why this method has become known as the "Leibnitz 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 diggits. 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 Leibnitz 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 diggits. diff --git a/patches/patches/051_values.patch b/patches/patches/051_values.patch index bb65525..53d73be 100644 --- a/patches/patches/051_values.patch +++ b/patches/patches/051_values.patch @@ -1,5 +1,5 @@ ---- exercises/051_values.zig 2023-10-03 22:15:22.122241138 +0200 -+++ answers/051_values.zig 2023-10-05 20:04:07.072767194 +0200 +--- exercises/051_values.zig 2024-03-14 23:25:42.695020607 +0100 ++++ answers/051_values.zig 2024-03-14 23:28:34.525109174 +0100 @@ -87,7 +87,7 @@ // Let's assign the std.debug.print function to a const named // "print" so that we can use this new name later! @@ -9,7 +9,7 @@ // Now let's look at assigning and pointing to values in Zig. // -@@ -152,13 +152,13 @@ +@@ -163,13 +163,13 @@ print("XP before:{}, ", .{glorp.experience}); // Fix 1 of 2 goes here: diff --git a/patches/patches/094_c_math.patch b/patches/patches/094_c_math.patch index 67da7e8..f8c7620 100644 --- a/patches/patches/094_c_math.patch +++ b/patches/patches/094_c_math.patch @@ -1,6 +1,6 @@ ---- exercises/094_c_math.zig 2023-10-22 14:00:02.909379696 +0200 -+++ answers/094_c_math.zig 2023-10-22 14:02:46.709025235 +0200 -@@ -19,7 +19,7 @@ +--- 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? diff --git a/patches/patches/104_threading.patch b/patches/patches/104_threading.patch new file mode 100644 index 0000000..58410a8 --- /dev/null +++ b/patches/patches/104_threading.patch @@ -0,0 +1,17 @@ +--- exercises/104_threading.zig 2024-03-05 09:09:04.013974229 +0100 ++++ answers/104_threading.zig 2024-03-05 09:12:03.987162883 +0100 +@@ -97,12 +97,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_threading2.patch b/patches/patches/105_threading2.patch new file mode 100644 index 0000000..dfa5613 --- /dev/null +++ b/patches/patches/105_threading2.patch @@ -0,0 +1,13 @@ +--- 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});