Exe: 001_hello.zig
//
// Oh no, this is supposed to print "Hello world!" but it needs
// your help.
//
// Zig functions are private by default, but the main() function
// must be public.
//
// A function is made public with the "pub" statement like so:
//
// pub fn foo() void {
// ...
// }
//
// Perhaps knowing this will help solve the errors we're getting
// with this little program?
//
const std = @import("std");
pub fn main() void {
std.debug.print("Hello world!n", .{});
}
Exe: 002_std.zig
//
// Oops! This program is supposed to print a line like our Hello World
// example. But we forgot how to import the Zig Standard Library.
//
// The @import() function is built into Zig. It returns a value which
// represents the imported code. It's a good idea to store the import as
// a constant value with the same name as the import:
//
// const foo = @import("foo");
//
// Please complete the import below:
//
const std = @import("std");
pub fn main() void {
std.debug.print("Standard Library.n", .{});
}
// For the curious: Imports must be declared as constants because they
// can only be used at compile time rather than run time. Zig evaluates
// constant values at compile time. Don't worry, we'll cover imports
// in detail later.
// Also see this answer: https://stackoverflow.com/a/62567550/695615
Exe: 003_assignment.zig
//
// It seems we got a little carried away making everything "const u8"!
//
// "const" values cannot change.
// "u" types are "unsigned" and cannot store negative values.
// "8" means the type is 8 bits in size.
//
// Example: foo cannot change (it is CONSTant)
// bar can change (it is VARiable):
//
// const foo: u8 = 20;
// var bar: u8 = 20;
//
// Example: foo cannot be negative and can hold 0 to 255
// bar CAN be negative and can hold -128 to 127
//
// const foo: u8 = 20;
// const bar: i8 = -20;
//
// Example: foo can hold 8 bits (0 to 255)
// bar can hold 16 bits (0 to 65,535)
//
// const foo: u8 = 20;
// const bar: u16 = 2000;
//
// You can do just about any combination of these that you can think of:
//
// u32 can hold 0 to 4,294,967,295
// i64 can hold -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
//
// Please fix this program so that the types can hold the desired values
// and the errors go away!
//
const std = @import("std");
pub fn main() void {
var n: u8 = 50;
n = n + 5;
const pi: u32 = 314159;
const negative_eleven: i8 = -11;
// There are no errors in the next line, just explanation:
// Perhaps you noticed before that the print function takes two
// parameters. Now it will make more sense: the first parameter
// is a string. The string may contain placeholders '{}', and the
// second parameter is an "anonymous list literal" (don't worry
// about this for now!) with the values to be printed.
std.debug.print("{} {} {}n", .{ n, pi, negative_eleven });
}
Exe: 004_arrays.zig
//
// Let's learn some array basics. Arrays are declared with:
//
// var foo: [3]u32 = [3]u32{ 42, 108, 5423 };
//
// When Zig can infer the size of the array, you can use '_' for the
// size. You can also let Zig infer the type of the value so the
// declaration is much less verbose.
//
// var foo = [_]u32{ 42, 108, 5423 };
//
// Get values of an array using array[index] notation:
//
// const bar = foo[2]; // 5423
//
// Set values of an array using array[index] notation:
//
// foo[2] = 16;
//
// Get the length of an array using the len property:
//
// const length = foo.len;
//
const std = @import("std");
pub fn main() void {
// (Problem 1)
// This "const" is going to cause a problem later - can you see what it is?
// How do we fix it?
var some_primes = [_]u8{ 1, 3, 5, 7, 11, 13, 17, 19 };
// Individual values can be set with '[]' notation.
// Example: This line changes the first prime to 2 (which is correct):
some_primes[0] = 2;
// Individual values can also be accessed with '[]' notation.
// Example: This line stores the first prime in "first":
const first = some_primes[0];
// (Problem 2)
// Looks like we need to complete this expression. Use the example
// above to set "fourth" to the fourth element of the some_primes array:
const fourth = some_primes[3];
// (Problem 3)
// Use the len property to get the length of the array:
const length = some_primes.len;
std.debug.print("First: {}, Fourth: {}, Length: {}n", .{
first, fourth, length,
});
}
Exe: 005_arrays2.zig
//
// Zig has some fun array operators.
//
// You can use '++' to concatenate two arrays:
//
// const a = [_]u8{ 1,2 };
// const b = [_]u8{ 3,4 };
// const c = a ++ b ++ [_]u8{ 5 }; // equals 1 2 3 4 5
//
// You can use '**' to repeat an array:
//
// const d = [_]u8{ 1,2,3 } ** 2; // equals 1 2 3 1 2 3
//
// Note that both '++' and '**' only operate on arrays while your
// program is _being compiled_. This special time is known in Zig
// parlance as "comptime" and we'll learn plenty more about that
// later.
//
const std = @import("std");
pub fn main() void {
const le = [_]u8{ 1, 3 };
const et = [_]u8{ 3, 7 };
// (Problem 1)
// Please set this array concatenating the two arrays above.
// It should result in: 1 3 3 7
const leet = le ++ et;
// (Problem 2)
// Please set this array using repetition.
// It should result in: 1 0 0 1 1 0 0 1 1 0 0 1
const bit_pattern = [_]u8{ 1, 0, 0, 1 } ** 3;
// Okay, that's all of the problems. Let's see the results.
//
// We could print these arrays with leet[0], leet[1],...but let's
// have a little preview of Zig 'for' loops instead:
//
// for (<item array>) |item| { <do something with item> }
//
// Don't worry, we'll cover looping properly in upcoming
// lessons.
//
std.debug.print("LEET: ", .{});
for (leet) |n| {
std.debug.print("{}", .{n});
}
std.debug.print(", Bits: ", .{});
for (bit_pattern) |n| {
std.debug.print("{}", .{n});
}
std.debug.print("n", .{});
}
Exe: 006_strings.zig
//
// Now that we've learned about arrays, we can talk about strings.
//
// We've already seen Zig string literals: "Hello world.n"
//
// Zig stores strings as arrays of bytes.
//
// const foo = "Hello";
//
// Is almost* the same as:
//
// const foo = [_]u8{ 'H', 'e', 'l', 'l', 'o' };
//
// (* We'll see what Zig strings REALLY are in Exercise 77.)
//
// Notice how individual characters use single quotes ('H') and
// strings use double quotes ("H"). These are not interchangeable!
//
const std = @import("std");
pub fn main() void {
const ziggy = "stardust";
// (Problem 1)
// Use array square bracket syntax to get the letter 'd' from
// the string "stardust" above.
const d: u8 = ziggy[4];
// (Problem 2)
// Use the array repeat '**' operator to make "ha ha ha ".
const laugh = "ha " ** 3;
// (Problem 3)
// Use the array concatenation '++' operator to make "Major Tom".
// (You'll need to add a space as well!)
const major = "Major";
const tom = "Tom";
const major_tom = major ++ " " ++ tom;
// That's all the problems. Let's see our results:
std.debug.print("d={u} {s}{s}n", .{ d, laugh, major_tom });
// Keen eyes will notice that we've put 'u' and 's' inside the '{}'
// placeholders in the format string above. This tells the
// print() function to format the values as a UTF-8 character and
// UTF-8 strings respectively. If we didn't do this, we'd see '100',
// which is the decimal number corresponding with the 'd' character
// in UTF-8. (And an error in the case of the strings.)
//
// While we're on this subject, 'c' (ASCII encoded character)
// would work in place for 'u' because the first 128 characters
// of UTF-8 are the same as ASCII!
//
}
Exe: 007_strings2.zig
//
// Here's a fun one: Zig has multi-line strings!
//
// To make a multi-line string, put '\' at the beginning of each
// line just like a code comment but with backslashes instead:
//
// const two_lines =
// \Line One
// \Line Two
// ;
//
// See if you can make this program print some song lyrics.
//
const std = @import("std");
pub fn main() void {
const lyrics =
\Ziggy played guitar
\Jamming good with Andrew Kelley
\And the Spiders from Mars
;
std.debug.print("{s}n", .{lyrics});
}
Exe: 008_quiz.zig
//
// Quiz time! Let's see if you can fix this whole program.
//
// You'll have to think about this one a bit.
//
// Let the compiler tell you what's wrong.
//
// Start at the top.
//
const std = @import("std");
pub fn main() void {
// What is this nonsense? :-)
const letters = "YZhifg";
// Note: usize is an unsigned integer type used for...sizes.
// The exact size of usize depends on the target CPU
// architecture. We could have used a u8 here, but usize is
// the idiomatic type to use for array indexing.
//
// There IS a problem on this line, but 'usize' isn't it.
var x: usize = 1;
// Note: When you want to declare memory (an array in this
// case) without putting anything in it, you can set it to
// 'undefined'. There is no problem on this line.
var lang: [3]u8 = undefined;
// The following lines attempt to put 'Z', 'i', and 'g' into the
// 'lang' array we just created by indexing the array
// 'letters' with the variable 'x'. As you can see above, x=1
// to begin with.
lang[0] = letters[x];
x = 3;
lang[1] = letters[x];
x = 5;
lang[2] = letters[x];
// We want to "Program in Zig!" of course:
std.debug.print("Program in {s}!n", .{lang});
}
Exe: 009_if.zig
//
// Now we get into the fun stuff, starting with the 'if' statement!
//
// if (true) {
// ...
// } else {
// ...
// }
//
// Zig has the "usual" comparison operators such as:
//
// a == b means "a equals b"
// a < b means "a is less than b"
// a > b means "a is greater than b"
// a != b means "a does not equal b"
//
// The important thing about Zig's "if" is that it *only* accepts
// boolean values. It won't coerce numbers or other types of data
// to true and false.
//
const std = @import("std");
pub fn main() void {
const foo = 42;
// Please fix this condition:
if (foo == 42) {
// We want our program to print this message!
std.debug.print("Foo is 42!n", .{});
} else {
std.debug.print("Foo is not 42!n", .{});
}
}
Exe: 010_if2.zig
//
// If statements are also valid expressions:
//
// const foo: u8 = if (a) 2 else 3;
//
const std = @import("std");
pub fn main() void {
const discount = true;
// Please use an if...else expression to set "price".
// If discount is true, the price should be $17, otherwise $20:
const price: u8 = if (discount) 17 else 20;
std.debug.print("With the discount, the price is ${}.n", .{price});
}
Exe: 011_while.zig
//
// Zig 'while' statements create a loop that runs while the
// condition is true. This runs once (at most):
//
// while (condition) {
// condition = false;
// }
//
// Remember that the condition must be a boolean value and
// that we can get a boolean value from conditional operators
// such as:
//
// a == b means "a equals b"
// a < b means "a is less than b"
// a > b means "a is greater than b"
// a != b means "a does not equal b"
//
const std = @import("std");
pub fn main() void {
var n: u32 = 2;
// Please use a condition that is true UNTIL "n" reaches 1024:
while (n < 1024) {
// Print the current number
std.debug.print("{} ", .{n});
// Set n to n multiplied by 2
n *= 2;
}
// Once the above is correct, this will print "n=1024"
std.debug.print("n={}n", .{n});
}
Exe: 012_while2.zig
//
// Zig 'while' statements can have an optional 'continue expression'
// which runs every time the while loop continues (either at the
// end of the loop or when an explicit 'continue' is invoked - we'll
// try those out next):
//
// while (condition) : (continue expression) {
// ...
// }
//
// Example:
//
// var foo = 2;
// while (foo < 10) : (foo += 2) {
// // Do something with even numbers less than 10...
// }
//
// See if you can re-write the last exercise using a continue
// expression:
//
const std = @import("std");
pub fn main() void {
var n: u32 = 2;
// Please set the continue expression so that we get the desired
// results in the print statement below.
while (n < 1024) : (n *= 2) {
// Print the current number
std.debug.print("{} ", .{n});
}
// As in the last exercise, we want this to result in "n=1024"
std.debug.print("n={}n", .{n});
}
Exe: 013_while3.zig
//
// The last two exercises were functionally identical. Continue
// expressions really show their utility when used with 'continue'
// statements!
//
// Example:
//
// while (condition) : (continue expression) {
//
// if (other condition) continue;
//
// }
//
// The "continue expression" executes every single time the loop restarts,
// even when a `continue` statement skips the rest of the loop body.
//
const std = @import("std");
pub fn main() void {
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});
}
std.debug.print("n", .{});
}
Exe: 014_while4.zig
//
// You can force a loop to exit immediately with a "break" statement:
//
// while (condition) : (continue expression) {
//
// if (other condition) break;
//
// }
//
// Continue expressions do NOT execute when a while loop stops
// because of a break!
//
const std = @import("std");
pub fn main() void {
var n: u32 = 1;
// Oh dear! This while loop will go forever?!
// Please fix this so the print statement below gives the desired output.
while (true) : (n += 1) {
if (n == 4) break;
}
// Result: we want n=4
std.debug.print("n={}n", .{n});
}
Exe: 015_for.zig
//
// Behold the 'for' loop! For loops let you execute code for each
// element of an array:
//
// for (items) |item| {
//
// // Do something with item
//
// }
//
const std = @import("std");
pub fn main() void {
const story = [_]u8{ 'h', 'h', 's', 'n', 'h' };
std.debug.print("A Dramatic Story: ", .{});
for (story) |scene| {
if (scene == 'h') std.debug.print(":-) ", .{});
if (scene == 's') std.debug.print(":-( ", .{});
if (scene == 'n') std.debug.print(":-| ", .{});
}
std.debug.print("The End.n", .{});
}
// Note that 'for' loops also work on things called "slices"
// which we'll see later.
//
// Also note that 'for' loops have recently become more flexible
// and powerful (two years after this exercise was written).
// More about that in a moment.
Exe: 016_for2.zig
//
// For loops also let you use the "index" of the iteration, a number
// that counts up with each iteration. To access the index of iteration,
// specify a second condition as well as a second capture value.
//
// for (items, 0..) |item, index| {
//
// // Do something with item and index
//
// }
//
// You can name "item" and "index" anything you want. "i" is a popular
// shortening of "index". The item name is often the singular form of
// the items you're looping through.
//
const std = @import("std");
pub fn main() void {
// Let's store the bits of binary number 1101 in
// 'little-endian' order (least significant byte or bit first):
const bits = [_]u8{ 1, 0, 1, 1 };
var value: u32 = 0;
// Now we'll convert the binary bits to a number value by adding
// the value of the place as a power of two for each bit.
//
// See if you can figure out the missing pieces:
for (bits, 0..) |bit, i| {
// Note that we convert the usize i to a u32 with
// @intCast(), a builtin function just like @import().
// We'll learn about these properly in a later exercise.
const i_u32: u32 = @intCast(i);
const place_value = std.math.pow(u32, 2, i_u32);
value += place_value * bit;
}
std.debug.print("The value of bits '1101': {}.n", .{value});
}
//
// As mentioned in the previous exercise, 'for' loops have gained
// additional flexibility since these early exercises were
// written. As we'll see in later exercises, the above syntax for
// capturing the index is part of a more general ability. Hang in
// there!
Exe: 017_quiz2.zig
//
// Quiz time again! Let's see if you can solve the famous "Fizz Buzz"!
//
// "Players take turns to count incrementally, replacing
// any number divisible by three with the word "fizz",
// and any number divisible by five with the word "buzz".
// - From https://en.wikipedia.org/wiki/Fizz_buzz
//
// Let's go from 1 to 16. This has been started for you, but there
// are some problems. :-(
//
const std = @import("std");
pub fn main() void {
var i: u8 = 1;
const stop_at: u8 = 16;
// What kind of loop is this? A 'for' or a 'while'?
while (i <= stop_at) : (i += 1) {
if (i % 3 == 0) std.debug.print("Fizz", .{});
if (i % 5 == 0) std.debug.print("Buzz", .{});
if (!(i % 3 == 0) and !(i % 5 == 0)) {
std.debug.print("{}", .{i});
}
std.debug.print(", ", .{});
}
std.debug.print("n", .{});
}
Exe: 018_functions.zig
//
// Functions! We've already created lots of functions called 'main()'. Now let's
// do something different:
//
// fn foo(n: u8) u8 {
// return n + 1;
// }
//
// The foo() function above takes a number 'n' and returns a number that is
// larger by one.
//
// Note the input parameter 'n' and return types are both u8.
//
const std = @import("std");
pub fn main() void {
// The new function deepThought() should return the number 42. See below.
const answer: u8 = deepThought();
std.debug.print("Answer to the Ultimate Question: {}n", .{answer});
}
// Please define the deepThought() function below.
//
// We're just missing a couple things. One thing we're NOT missing is the
// keyword "pub", which is not needed here. Can you guess why?
//
fn deepThought() u8 {
return 42; // Number courtesy Douglas Adams
}
Exe: 019_functions2.zig
//
// Now let's create a function that takes a parameter. Here's an
// example that takes two parameters. As you can see, parameters
// are declared just like any other types ("name": "type"):
//
// fn myFunction(number: u8, is_lucky: bool) void {
// ...
// }
//
const std = @import("std");
pub fn main() void {
std.debug.print("Powers of two: {} {} {} {}n", .{
twoToThe(1),
twoToThe(2),
twoToThe(3),
twoToThe(4),
});
}
// Please give this function the correct input parameter(s).
// You'll need to figure out the parameter name and type that we're
// expecting. The output type has already been specified for you.
//
fn twoToThe(my_number: u32) u32 {
return std.math.pow(u32, 2, my_number);
// std.math.pow(type, a, b) takes a numeric type and two
// numbers of that type (or that can coerce to that type) and
// returns "a to the power of b" as that same numeric type.
}
Exe: 020_quiz3.zig
//
// Let's see if we can make use of some of the things we've learned so far.
// We'll create two functions: one that contains a "for" loop and one
// that contains a "while" loop.
//
// Both of these are simply labeled "loop" below.
//
const std = @import("std");
pub fn main() void {
const my_numbers = [4]u16{ 5, 6, 7, 8 };
printPowersOfTwo(my_numbers);
std.debug.print("n", .{});
}
// You won't see this every day: a function that takes an array with
// exactly four u16 numbers. This is not how you would normally pass
// an array to a function. We'll learn about slices and pointers in
// a little while. For now, we're using what we know.
//
// This function prints, but does not return anything.
//
fn printPowersOfTwo(numbers: [4]u16) void {
for (numbers) |n| {
std.debug.print("{} ", .{twoToThe(n)});
}
}
// This function bears a striking resemblance to twoToThe() in the last
// exercise. But don't be fooled! This one does the math without the aid
// of the standard library!
//
fn twoToThe(number: u16) u16 {
var n: u16 = 0;
var total: u16 = 1;
while (n < number) : (n += 1) {
total *= 2;
}
return total;
}
Exe: 021_errors.zig
//
// Believe it or not, sometimes things go wrong in programs.
//
// In Zig, an error is a value. Errors are named so we can identify
// things that can go wrong. Errors are created in "error sets", which
// are just a collection of named errors.
//
// We have the start of an error set, but we're missing the condition
// "TooSmall". Please add it where needed!
const MyNumberError = error{
TooBig,
TooSmall,
TooFour,
};
const std = @import("std");
pub fn main() void {
const nums = [_]u8{ 2, 3, 4, 5, 6 };
for (nums) |n| {
std.debug.print("{}", .{n});
const number_error: MyNumberError = numberFail(n);
if (number_error == MyNumberError.TooBig) {
std.debug.print(">4. ", .{});
}
if (number_error == MyNumberError.TooSmall) {
std.debug.print("<4. ", .{});
}
if (number_error == MyNumberError.TooFour) {
std.debug.print("=4. ", .{});
}
}
std.debug.print("n", .{});
}
// Notice how this function can return any member of the MyNumberError
// error set.
fn numberFail(n: u8) MyNumberError {
if (n > 4) return MyNumberError.TooBig;
if (n < 4) return MyNumberError.TooSmall; // <---- this one is free!
return MyNumberError.TooFour;
}
Exe: 022_errors2.zig
//
// A common case for errors is a situation where we're expecting to
// have a value OR something has gone wrong. Take this example:
//
// var text: Text = getText("foo.txt");
//
// What happens if getText() can't find "foo.txt"? How do we express
// this in Zig?
//
// Zig lets us make what's called an "error union" which is a value
// which could either be a regular value OR an error from a set:
//
// var text: MyErrorSet!Text = getText("foo.txt");
//
// For now, let's just see if we can try making an error union!
//
const std = @import("std");
const MyNumberError = error{TooSmall};
pub fn main() void {
var my_number: MyNumberError!i8 = 5;
// Looks like my_number will need to either store a number OR
// an error. Can you set the type correctly above?
my_number = MyNumberError.TooSmall;
std.debug.print("I compiled!n", .{});
}
Exe: 023_errors3.zig
//
// One way to deal with error unions is to "catch" any error and
// replace it with a default value.
//
// foo = canFail() catch 6;
//
// If canFail() fails, foo will equal 6.
//
const std = @import("std");
const MyNumberError = error{TooSmall};
pub fn main() void {
const a: u32 = addTwenty(44) catch 22;
const b: u32 = addTwenty(4) catch 22;
std.debug.print("a={}, b={}n", .{ a, b });
}
// Please provide the return type from this function.
// Hint: it'll be an error union.
fn addTwenty(n: u32) MyNumberError!u32 {
if (n < 5) {
return MyNumberError.TooSmall;
} else {
return n + 20;
}
}
Exe: 024_errors4.zig
//
// Using `catch` to replace an error with a default value is a bit
// of a blunt instrument since it doesn't matter what the error is.
//
// Catch lets us capture the error value and perform additional
// actions with this form:
//
// canFail() catch |err| {
// if (err == FishError.TunaMalfunction) {
// ...
// }
// };
//
const std = @import("std");
const MyNumberError = error{
TooSmall,
TooBig,
};
pub fn main() void {
// The "catch 0" below is a temporary hack to deal with
// makeJustRight()'s returned error union (for now).
const a: u32 = makeJustRight(44) catch 0;
const b: u32 = makeJustRight(14) catch 0;
const c: u32 = makeJustRight(4) catch 0;
std.debug.print("a={}, b={}, c={}n", .{ a, b, c });
}
// In this silly example we've split the responsibility of making
// a number just right into four (!) functions:
//
// makeJustRight() Calls fixTooBig(), cannot fix any errors.
// fixTooBig() Calls fixTooSmall(), fixes TooBig errors.
// fixTooSmall() Calls detectProblems(), fixes TooSmall errors.
// detectProblems() Returns the number or an error.
//
fn makeJustRight(n: u32) MyNumberError!u32 {
return fixTooBig(n) catch |err| {
return err;
};
}
fn fixTooBig(n: u32) MyNumberError!u32 {
return fixTooSmall(n) catch |err| {
if (err == MyNumberError.TooBig) {
return 20;
}
return err;
};
}
fn fixTooSmall(n: u32) MyNumberError!u32 {
// Oh dear, this is missing a lot! But don't worry, it's nearly
// identical to fixTooBig() above.
//
// If we get a TooSmall error, we should return 10.
// If we get any other error, we should return that error.
// Otherwise, we return the u32 number.
return detectProblems(n) catch |err| {
if (err == MyNumberError.TooSmall) {
return 10;
}
return err;
};
}
fn detectProblems(n: u32) MyNumberError!u32 {
if (n < 10) return MyNumberError.TooSmall;
if (n > 20) return MyNumberError.TooBig;
return n;
}
Exe: 025_errors5.zig
//
// Zig has a handy "try" shortcut for this common error handling pattern:
//
// canFail() catch |err| return err;
//
// which can be more compactly written as:
//
// try canFail();
//
const std = @import("std");
const MyNumberError = error{
TooSmall,
TooBig,
};
pub fn main() void {
const a: u32 = addFive(44) catch 0;
const b: u32 = addFive(14) catch 0;
const c: u32 = addFive(4) catch 0;
std.debug.print("a={}, b={}, c={}n", .{ a, b, c });
}
fn addFive(n: u32) MyNumberError!u32 {
// This function needs to return any error which might come back from detect().
// Please use a "try" statement rather than a "catch".
//
const x = try detect(n);
return x + 5;
}
fn detect(n: u32) MyNumberError!u32 {
if (n < 10) return MyNumberError.TooSmall;
if (n > 20) return MyNumberError.TooBig;
return n;
}
Exe: 026_hello2.zig
//
// Great news! Now we know enough to understand a "real" Hello World
// program in Zig - one that uses the system Standard Out resource...which
// can fail!
//
const std = @import("std");
// Take note that this main() definition now returns "!void" rather
// than just "void". Since there's no specific error type, this means
// that Zig will infer the error type. This is appropriate in the case
// of main(), but can make a function harder (function pointers) or
// even impossible to work with (recursion) in some situations.
//
// You can find more information at:
// https://ziglang.org/documentation/master/#Inferred-Error-Sets
//
pub fn main(init: std.process.Init) !void {
// Instance for input/output operations; we will learn more about this later.
const io = init.io;
// We get a Writer for Standard Out...
var stdout_writer = std.Io.File.stdout().writer(io, &.{});
// ...and extract its interface so we can print() to it.
const stdout = &stdout_writer.interface;
// Unlike std.debug.print(), the Standard Out writer can fail
// with an error. We don't care _what_ the error is, we want
// to be able to pass it up as a return value of main().
//
// We just learned of a single statement which can accomplish this.
try stdout.print("Hello world!n", .{});
}
Exe: 027_defer.zig
//
// You can assign some code to run _after_ a block of code exits by
// deferring it with a "defer" statement:
//
// {
// defer runLater();
// runNow();
// }
//
// In the example above, runLater() will run when the block ({...})
// is finished. So the code above will run in the following order:
//
// runNow();
// runLater();
//
// This feature seems strange at first, but we'll see how it could be
// useful in the next exercise.
const std = @import("std");
pub fn main() void {
// Without changing anything else, please add a 'defer' statement
// to this code so that our program prints "One Twon":
defer std.debug.print("Twon", .{});
std.debug.print("One ", .{});
}
Exe: 028_defer2.zig
//
// Now that you know how "defer" works, let's do something more
// interesting with it.
//
const std = @import("std");
pub fn main() void {
const animals = [_]u8{ 'g', 'c', 'd', 'd', 'g', 'z' };
for (animals) |a| printAnimal(a);
std.debug.print("done.n", .{});
}
// This function is _supposed_ to print an animal name in parentheses
// like "(Goat) ", but we somehow need to print the end parenthesis
// even though this function can return in four different places!
fn printAnimal(animal: u8) void {
std.debug.print("(", .{});
defer std.debug.print(") ", .{}); // <---- how?!
if (animal == 'g') {
std.debug.print("Goat", .{});
return;
}
if (animal == 'c') {
std.debug.print("Cat", .{});
return;
}
if (animal == 'd') {
std.debug.print("Dog", .{});
return;
}
std.debug.print("Unknown", .{});
}
Exe: 029_errdefer.zig
//
// Another common problem is a block of code that could exit in multiple
// places due to an error - but that needs to do something before it
// exits (typically to clean up after itself).
//
// An "errdefer" is a defer that only runs if the block exits with an error:
//
// {
// errdefer cleanup();
// try canFail();
// }
//
// The cleanup() function is called ONLY if the "try" statement returns an
// error produced by canFail().
//
const std = @import("std");
var counter: u32 = 0;
const MyErr = error{ GetFail, IncFail };
pub fn main() void {
// We simply quit the entire program if we fail to get a number:
const a: u32 = makeNumber() catch return;
const b: u32 = makeNumber() catch return;
std.debug.print("Numbers: {}, {}n", .{ a, b });
}
fn makeNumber() MyErr!u32 {
std.debug.print("Getting number...", .{});
// Please make the "failed" message print ONLY if the makeNumber()
// function exits with an error:
errdefer std.debug.print("failed!n", .{});
var num = try getNumber(); // <-- This could fail!
num = try increaseNumber(num); // <-- This could ALSO fail!
std.debug.print("got {}. ", .{num});
return num;
}
fn getNumber() MyErr!u32 {
// I _could_ fail...but I don't!
return 4;
}
fn increaseNumber(n: u32) MyErr!u32 {
// I fail after the first time you run me!
if (counter > 0) return MyErr.IncFail;
// Sneaky, weird global stuff.
counter += 1;
return n + 1;
}
Exe: 030_switch.zig
//
// The "switch" statement lets you match the possible values of an
// expression and perform a different action for each.
//
// This switch:
//
// switch (players) {
// 1 => startOnePlayerGame(),
// 2 => startTwoPlayerGame(),
// else => {
// alert();
// return GameError.TooManyPlayers;
// }
// }
//
// Is equivalent to this if/else:
//
// if (players == 1) startOnePlayerGame();
// else if (players == 2) startTwoPlayerGame();
// else {
// alert();
// return GameError.TooManyPlayers;
// }
//
const std = @import("std");
pub fn main() void {
const lang_chars = [_]u8{ 26, 9, 7, 42 };
for (lang_chars) |c| {
switch (c) {
1 => std.debug.print("A", .{}),
2 => std.debug.print("B", .{}),
3 => std.debug.print("C", .{}),
4 => std.debug.print("D", .{}),
5 => std.debug.print("E", .{}),
6 => std.debug.print("F", .{}),
7 => std.debug.print("G", .{}),
8 => std.debug.print("H", .{}),
9 => std.debug.print("I", .{}),
10 => std.debug.print("J", .{}),
// ... we don't need everything in between ...
25 => std.debug.print("Y", .{}),
26 => std.debug.print("Z", .{}),
// Switch statements must be "exhaustive" (there must be a
// match for every possible value). Please add an "else"
// to this switch to print a question mark "?" when c is
// not one of the existing matches.
else => {
std.debug.print("?", .{});
},
}
}
std.debug.print("n", .{});
}
Exe: 031_switch2.zig
//
// What's really nice is that you can use a switch statement as an
// expression to return a value.
//
// const a = switch (x) {
// 1 => 9,
// 2 => 16,
// 3 => 7,
// ...
// }
//
const std = @import("std");
pub fn main() void {
const lang_chars = [_]u8{ 26, 9, 7, 42 };
for (lang_chars) |c| {
const real_char: u8 = switch (c) {
1 => 'A',
2 => 'B',
3 => 'C',
4 => 'D',
5 => 'E',
6 => 'F',
7 => 'G',
8 => 'H',
9 => 'I',
10 => 'J',
// ...
25 => 'Y',
26 => 'Z',
// As in the last exercise, please add the 'else' clause
// and this time, have it return an exclamation mark '!'.
else => '!',
};
std.debug.print("{c}", .{real_char});
// Note: "{c}" forces print() to display the value as a character.
// Can you guess what happens if you remove the "c"? Try it!
}
std.debug.print("n", .{});
}
Exe: 032_unreachable.zig
//
// Zig has an "unreachable" statement. Use it when you want to tell the
// compiler that a branch of code should never be executed and that the
// mere act of reaching it is an error.
//
// if (true) {
// ...
// } else {
// unreachable;
// }
//
// Here we've made a little virtual machine that performs mathematical
// operations on a single numeric value. It looks great but there's one
// little problem: the switch statement doesn't cover every possible
// value of a u8 number!
//
// WE know there are only three operations but Zig doesn't. Use the
// unreachable statement to make the switch complete. Or ELSE. :-)
//
const std = @import("std");
pub fn main() void {
const operations = [_]u8{ 1, 1, 1, 3, 2, 2 };
var current_value: u32 = 0;
for (operations) |op| {
switch (op) {
1 => {
current_value += 1;
},
2 => {
current_value -= 1;
},
3 => {
current_value *= current_value;
},
else => unreachable,
}
std.debug.print("{} ", .{current_value});
}
std.debug.print("n", .{});
}
Exe: 033_iferror.zig
//
// Let's revisit the very first error exercise. This time, we're going to
// look at an error-handling variation of the "if" statement.
//
// if (foo) |value| {
//
// // foo was NOT an error; value is the non-error value of foo
//
// } else |err| {
//
// // foo WAS an error; err is the error value of foo
//
// }
//
// We'll take it even further and use a switch statement to handle
// the error types.
//
// if (foo) |value| {
// ...
// } else |err| switch (err) {
// ...
// }
//
const MyNumberError = error{
TooBig,
TooSmall,
};
const std = @import("std");
pub fn main() void {
const nums = [_]u8{ 2, 3, 4, 5, 6 };
for (nums) |num| {
std.debug.print("{}", .{num});
const n = numberMaybeFail(num);
if (n) |value| {
std.debug.print("={}. ", .{value});
} else |err| switch (err) {
MyNumberError.TooBig => std.debug.print(">4. ", .{}),
// Please add a match for TooSmall here and have it print: "<4. "
MyNumberError.TooSmall => std.debug.print("<4. ", .{}),
}
}
std.debug.print("n", .{});
}
// This time we'll have numberMaybeFail() return an error union rather
// than a straight error.
fn numberMaybeFail(n: u8) MyNumberError!u8 {
if (n > 4) return MyNumberError.TooBig;
if (n < 4) return MyNumberError.TooSmall;
return n;
}
Exe: 034_quiz4.zig
//
// Quiz time. See if you can make this program work!
//
// Solve this any way you like, just be sure the output is:
//
// my_num=42
//
const std = @import("std");
const NumError = error{IllegalNumber};
pub fn main(init: std.process.Init) !void {
const io = init.io;
var stdout_writer = std.Io.File.stdout().writer(io, &.{});
const stdout = &stdout_writer.interface;
const my_num: u32 = try getNumber();
try stdout.print("my_num={}n", .{my_num});
}
// This function is obviously weird and non-functional. But you will not be changing it for this quiz.
fn getNumber() NumError!u32 {
if (false) return NumError.IllegalNumber;
return 42;
}
Exe: 035_enums.zig
//
// Remember that little mathematical virtual machine we made using the
// "unreachable" statement? Well, there were two problems with the
// way we were using op codes:
//
// 1. Having to remember op codes by number is no good.
// 2. We had to use "unreachable" because Zig had no way of knowing
// how many valid op codes there were.
//
// An "enum" is a Zig construct that lets you give names to numeric
// values and store them in a set. They look a lot like error sets:
//
// const Fruit = enum{ apple, pear, orange };
//
// const my_fruit = Fruit.apple;
//
// Let's use an enum in place of the numbers we were using in the
// previous version!
//
const std = @import("std");
// Please complete the enum!
const Ops = enum { inc, pow, dec };
pub fn main() void {
const operations = [_]Ops{
Ops.inc,
Ops.inc,
Ops.inc,
Ops.pow,
Ops.dec,
Ops.dec,
};
var current_value: u32 = 0;
for (operations) |op| {
switch (op) {
Ops.inc => {
current_value += 1;
},
Ops.dec => {
current_value -= 1;
},
Ops.pow => {
current_value *= current_value;
},
// No "else" needed! Why is that?
}
std.debug.print("{} ", .{current_value});
}
std.debug.print("n", .{});
}
Exe: 036_enums2.zig
//
// Enums are really just a set of numbers. You can leave the
// numbering up to the compiler, or you can assign them
// explicitly. You can even specify the numeric type used.
//
// const Stuff = enum(u8){ foo = 16 };
//
// You can get the integer out with a builtin function,
// @intFromEnum(). We'll learn about builtins properly in a later
// exercise.
//
// const my_stuff: u8 = @intFromEnum(Stuff.foo);
//
// Note how that built-in function starts with "@" just like the
// @import() function we've been using.
//
const std = @import("std");
// Zig lets us write integers in hexadecimal format:
//
// 0xf (is the value 15 in hex)
//
// Web browsers let us specify colors using a hexadecimal
// number where each byte represents the brightness of the
// Red, Green, or Blue component (RGB) where two hex digits
// are one byte with a value range of 0-255:
//
// #RRGGBB
//
// Please define and use a pure blue value Color:
const Color = enum(u32) {
red = 0xff0000,
green = 0x00ff00,
blue = 0x0000FF,
};
pub fn main() void {
// Remember Zig's multi-line strings? Here they are again.
// Also, check out this cool format string:
//
// {x:0>6}
// ^
// x type ('x' is lower-case hexadecimal)
// : separator (needed for format syntax)
// 0 padding character (default is ' ')
// > alignment ('>' aligns right)
// 6 width (use padding to force width)
//
// Please add this formatting to the blue value.
// (Even better, experiment without it, or try parts of it
// to see what prints!)
std.debug.print(
\<p>
\ <span style="color: #{x:0>6}">Red</span>
\ <span style="color: #{x:0>6}">Green</span>
\ <span style="color: #{x:0>6}">Blue</span>
\</p>
\
, .{
@intFromEnum(Color.red),
@intFromEnum(Color.green),
@intFromEnum(Color.blue), // Oops! We're missing something!
});
}
Exe: 037_structs.zig
//
// Being able to group values together lets us turn this:
//
// point1_x = 3;
// point1_y = 16;
// point1_z = 27;
// point2_x = 7;
// point2_y = 13;
// point2_z = 34;
//
// into this:
//
// point1 = Point{ .x=3, .y=16, .z=27 };
// point2 = Point{ .x=7, .y=13, .z=34 };
//
// The Point above is an example of a "struct" (short for "structure").
// Here's how that struct type could have been defined:
//
// const Point = struct{ x: u32, y: u32, z: u32 };
//
// Let's store something fun with a struct: a roleplaying character!
//
const std = @import("std");
// We'll use an enum to specify the character role.
const Role = enum {
wizard,
thief,
bard,
warrior,
};
// Please add a new property to this struct called "health" and make
// it a u8 integer type.
const Character = struct {
role: Role,
gold: u32,
experience: u32,
health: u32,
};
pub fn main() void {
// Please initialize Glorp with 100 health.
var glorp_the_wise = Character{
.role = Role.wizard,
.gold = 20,
.experience = 10,
.health = 100,
};
// Glorp gains some gold.
glorp_the_wise.gold += 5;
// Ouch! Glorp takes a punch!
glorp_the_wise.health -= 10;
std.debug.print("Your wizard has {} health and {} gold.n", .{
glorp_the_wise.health,
glorp_the_wise.gold,
});
}
Exe: 038_structs2.zig
//
// Grouping values in structs is not merely convenient. It also allows
// us to treat the values as a single item when storing them, passing
// them to functions, etc.
//
// This exercise demonstrates how we can store structs in an array and
// how doing so lets us print them using a loop.
//
const std = @import("std");
const Role = enum {
wizard,
thief,
bard,
warrior,
};
const Character = struct {
role: Role,
gold: u32,
health: u8,
experience: u32,
};
pub fn main() void {
var chars: [2]Character = undefined;
// Glorp the Wise
chars[0] = Character{
.role = Role.wizard,
.gold = 20,
.health = 100,
.experience = 10,
};
chars[1] = Character{
.role = Role.bard,
.gold = 10,
.health = 100,
.experience = 20,
};
// Please add "Zump the Loud" with the following properties:
//
// role bard
// gold 10
// health 100
// experience 20
//
// Feel free to run this program without adding Zump. What does
// it do and why?
// Printing all RPG characters in a loop:
for (chars, 0..) |c, num| {
std.debug.print("Character {} - G:{} H:{} XP:{}n", .{
num + 1, c.gold, c.health, c.experience,
});
}
}
// If you tried running the program without adding Zump as mentioned
// above, you get what appear to be "garbage" values. In debug mode
// (which is the default), Zig writes the repeating pattern "10101010"
// in binary (or 0xAA in hex) to all undefined locations to make them
// easier to spot when debugging.
Exe: 039_pointers.zig
//
// Check this out:
//
// var foo: u8 = 5; // foo is 5
// var bar: *u8 = &foo; // bar is a pointer
//
// What is a pointer? It's a reference to a value. In this example,
// bar is a reference to the memory space that currently contains the
// value 5.
//
// A cheatsheet given the above declarations:
//
// u8 the type of a u8 value
// foo the value 5
// *u8 the type of a pointer to a u8 value
// &foo a reference to foo
// bar a pointer to the value at foo
// bar.* the value 5 (the dereferenced value "at" bar)
//
// We'll see why pointers are useful in a moment. For now, see if you
// can make this example work!
//
const std = @import("std");
pub fn main() void {
var num1: u8 = 5;
const num1_pointer: *u8 = &num1;
var num2: u8 = undefined;
// Please make num2 equal 5 using num1_pointer!
// (See the "cheatsheet" above for ideas.)
num2 = num1_pointer.*;
std.debug.print("num1: {}, num2: {}n", .{ num1, num2 });
}
Exe: 040_pointers2.zig
//
// It's important to note that variable pointers and constant pointers
// are different types.
//
// Given:
//
// var foo: u8 = 5;
// const bar: u8 = 5;
//
// Then:
//
// &foo is of type "*u8"
// &bar is of type "*const u8"
//
// You can always make a const pointer to a mutable value (var), but
// you cannot make a var pointer to an immutable value (const).
// This sounds like a logic puzzle, but it just means that once data
// is declared immutable, you can't coerce it to a mutable type.
// Think of mutable data as being volatile or even dangerous. Zig
// always lets you be "more safe" and never "less safe."
//
const std = @import("std");
pub fn main() void {
const a: u8 = 12;
const b: *const u8 = &a; // fix this!
std.debug.print("a: {}, b: {}n", .{ a, b.* });
}
Exe: 041_pointers3.zig
//
// The tricky part is that the pointer's mutability (var vs const) refers
// to the ability to change what the pointer POINTS TO, not the ability
// to change the VALUE at that location!
//
// const locked: u8 = 5;
// var unlocked: u8 = 10;
//
// const p1: *const u8 = &locked;
// var p2: *const u8 = &locked;
//
// Both p1 and p2 point to constant values which cannot change. However,
// p2 can be changed to point to something else and p1 cannot!
//
// const p3: *u8 = &unlocked;
// var p4: *u8 = &unlocked;
// const p5: *const u8 = &unlocked;
// var p6: *const u8 = &unlocked;
//
// Here p3 and p4 can both be used to change the value they point to but
// p3 cannot point at anything else.
// What's interesting is that p5 and p6 act like p1 and p2, but point to
// the value at "unlocked". This is what we mean when we say that we can
// make a constant reference to any value!
//
const std = @import("std");
pub fn main() void {
var foo: u8 = 5;
var bar: u8 = 10;
// Please define pointer "p" so that it can point to EITHER foo or
// bar AND change the value it points to!
var p: *u8 = undefined;
p = &foo;
p.* += 1;
p = &bar;
p.* += 1;
std.debug.print("foo={}, bar={}n", .{ foo, bar });
}
Exe: 042_pointers4.zig
//
// Now let's use pointers to do something we haven't been
// able to do before: pass a value by reference to a function.
//
// Why would we wish to pass a pointer to an integer variable
// rather than the integer value itself? Because then we are
// allowed to *change* the value of the variable!
//
// +-----------------------------------------------+
// | Pass by reference when you want to change the |
// | pointed-to value. Otherwise, pass the value. |
// +-----------------------------------------------+
//
const std = @import("std");
pub fn main() void {
var num: u8 = 1;
var more_nums = [_]u8{ 1, 1, 1, 1 };
// Let's pass the num reference to our function and print it:
makeFive(&num);
std.debug.print("num: {}, ", .{num});
// Now something interesting. Let's pass a reference to a
// specific array value:
makeFive(&more_nums[2]);
// And print the array:
std.debug.print("more_nums: ", .{});
for (more_nums) |n| {
std.debug.print("{} ", .{n});
}
std.debug.print("n", .{});
}
// This function should take a reference to a u8 value and set it
// to 5.
fn makeFive(x: *u8) void {
x.* = 5; // fix me!
}
Exe: 043_pointers5.zig
//
// As with integers, you can pass a pointer to a struct when you
// will wish to modify that struct. Pointers are also useful when
// you need to store a reference to a struct (a "link" to it).
//
// const Vertex = struct{ x: u32, y: u32, z: u32 };
//
// var v1 = Vertex{ .x=3, .y=2, .z=5 };
//
// var pv: *Vertex = &v1; // <-- a pointer to our struct
//
// Note that you don't need to dereference the "pv" pointer to access
// the struct's fields:
//
// YES: pv.x
// NO: pv.*.x
//
// We can write functions that take pointers to structs as
// arguments. This foo() function modifies struct v:
//
// fn foo(v: *Vertex) void {
// v.x += 2;
// v.y += 3;
// v.z += 7;
// }
//
// And call them like so:
//
// foo(&v1);
//
// Let's revisit our RPG example and make a printCharacter() function
// that takes a Character by reference and prints it...*and*
// prints a linked "mentor" Character, if there is one.
//
const std = @import("std");
const Class = enum {
wizard,
thief,
bard,
warrior,
};
const Character = struct {
class: Class,
gold: u32,
health: u8 = 100, // You can provide default values
experience: u32,
// I need to use the '?' here to allow for a null value. But
// I don't explain it until later. Please don't tell anyone.
mentor: ?*Character = null,
};
pub fn main() void {
var mighty_krodor = Character{
.class = Class.wizard,
.gold = 10000,
.experience = 2340,
};
var glorp = Character{ // Glorp!
.class = Class.wizard,
.gold = 10,
.experience = 20,
.mentor = &mighty_krodor, // Glorp's mentor is the Mighty Krodor
};
// FIX ME!
// Please pass Glorp to printCharacter():
printCharacter(&glorp);
}
// Note how this function's "c" parameter is a pointer to a Character struct.
fn printCharacter(c: *Character) void {
// Here's something you haven't seen before: when switching an enum, you
// don't have to write the full enum name. Zig understands that ".wizard"
// means "Class.wizard" when we switch on a Class enum value:
const class_name = switch (c.class) {
.wizard => "Wizard",
.thief => "Thief",
.bard => "Bard",
.warrior => "Warrior",
};
std.debug.print("{s} (G:{} H:{} XP:{})n", .{
class_name,
c.gold,
c.health,
c.experience,
});
// Checking an "optional" value and capturing it will be
// explained later (this pairs with the '?' mentioned above.)
if (c.mentor) |mentor| {
std.debug.print(" Mentor: ", .{});
printCharacter(mentor);
}
}
Exe: 044_quiz5.zig
//
// "Elephants walking
// Along the trails
//
// Are holding hands
// By holding tails."
//
// from Holding Hands
// by Lenore M. Link
//
const std = @import("std");
const Elephant = struct {
letter: u8,
tail: *Elephant = undefined,
visited: bool = false,
};
pub fn main() void {
var elephantA = Elephant{ .letter = 'A' };
// (Please add Elephant B here!)
var elephantB = Elephant{ .letter = 'B' };
var elephantC = Elephant{ .letter = 'C' };
// Link the elephants so that each tail "points" to the next elephant.
// They make a circle: A->B->C->A...
elephantA.tail = &elephantB;
// (Please link Elephant B's tail to Elephant C here!)
elephantB.tail = &elephantC;
elephantC.tail = &elephantA;
visitElephants(&elephantA);
std.debug.print("n", .{});
}
// This function visits all elephants once, starting with the
// first elephant and following the tails to the next elephant.
// If we did not "mark" the elephants as visited (by setting
// visited=true), then this would loop infinitely!
fn visitElephants(first_elephant: *Elephant) void {
var e = first_elephant;
while (!e.visited) {
std.debug.print("Elephant {u}. ", .{e.letter});
e.visited = true;
e = e.tail;
}
}
Exe: 045_optionals.zig
//
// Sometimes you know that a variable might hold a value or
// it might not. Zig has a neat way of expressing this idea
// called Optionals. An optional type just has a '?' like this:
//
// var foo: ?u32 = 10;
//
// Now foo can store a u32 integer OR null (a value storing
// the cosmic horror of a value NOT EXISTING!)
//
// foo = null;
//
// if (foo == null) beginScreaming();
//
// Before we can use the optional value as the non-null type
// (a u32 integer in this case), we need to guarantee that it
// isn't null. One way to do this is to THREATEN IT with the
// "orelse" statement.
//
// var bar = foo orelse 2;
//
// Here, bar will either equal the u32 integer value stored in
// foo, or it will equal 2 if foo was null.
//
const std = @import("std");
pub fn main() void {
const result = deepThought();
// Please threaten the result so that answer is either the
// integer value from deepThought() OR the number 42:
const answer: u8 = result orelse 42;
std.debug.print("The Ultimate Answer: {}.n", .{answer});
}
fn deepThought() ?u8 {
// It seems Deep Thought's output has declined in quality.
// But we'll leave this as-is. Sorry Deep Thought.
return null;
}
// Blast from the past:
//
// Optionals are a lot like error union types which can either
// hold a value or an error. Likewise, the orelse statement is
// like the catch statement used to "unwrap" a value or supply
// a default value:
//
// var maybe_bad: Error!u32 = Error.Evil;
// var number: u32 = maybe_bad catch 0;
//
Exe: 046_optionals2.zig
//
// Now that we have optional types, we can apply them to structs.
// The last time we checked in with our elephants, we had to link
// all three of them together in a "circle" so that the last tail
// linked to the first elephant. This is because we had NO CONCEPT
// of a tail that didn't point to another elephant!
//
// We also introduce the handy `.?` shortcut:
//
// const foo = bar.?;
//
// is the same as
//
// const foo = bar orelse unreachable;
//
// Check out where we use this shortcut below to change control flow
// based on if an optional value exists.
//
// Now let's make those elephant tails optional!
//
const std = @import("std");
const Elephant = struct {
letter: u8,
tail: ?*Elephant = null, // Hmm... tail needs something...
visited: bool = false,
};
pub fn main() void {
var elephantA = Elephant{ .letter = 'A' };
var elephantB = Elephant{ .letter = 'B' };
var elephantC = Elephant{ .letter = 'C' };
// Link the elephants so that each tail "points" to the next.
linkElephants(&elephantA, &elephantB);
linkElephants(&elephantB, &elephantC);
// `linkElephants` will stop the program if you try and link an
// elephant that doesn't exist! Uncomment and see what happens.
// const missingElephant: ?*Elephant = null;
// linkElephants(&elephantC, missingElephant);
visitElephants(&elephantA);
std.debug.print("n", .{});
}
// If e1 and e2 are valid pointers to elephants,
// this function links the elephants so that e1's tail "points" to e2.
fn linkElephants(e1: ?*Elephant, e2: ?*Elephant) void {
e1.?.tail = e2.?;
}
// This function visits all elephants once, starting with the
// first elephant and following the tails to the next elephant.
fn visitElephants(first_elephant: *Elephant) void {
var e = first_elephant;
while (!e.visited) {
std.debug.print("Elephant {u}. ", .{e.letter});
e.visited = true;
// We should stop once we encounter a tail that
// does NOT point to another element. What can
// we put here to make that happen?
// HINT: We want something similar to what `.?` does,
// but instead of ending the program, we want to exit the loop...
e = e.tail orelse return;
}
}
Exe: 047_methods.zig
//
// Help! Evil alien creatures have hidden eggs all over the Earth
// and they're starting to hatch!
//
// Before you jump into battle, you'll need to know three things:
//
// 1. You can attach functions to structs (and other "type definitions"):
//
// const Foo = struct{
// pub fn hello() void {
// std.debug.print("Foo says hello!n", .{});
// }
// };
//
// 2. A function that is a member of a struct is "namespaced" within
// that struct and is called by specifying the "namespace" and then
// using the "dot syntax":
//
// Foo.hello();
//
// 3. The NEAT feature of these functions is that if their first argument
// is an instance of the struct (or a pointer to one) then we can use
// the instance as the namespace instead of the type:
//
// const Bar = struct{
// pub fn a(self: Bar) void {}
// pub fn b(this: *Bar, other: u8) void {}
// pub fn c(bar: *const Bar) void {}
// };
//
// var bar = Bar{};
// bar.a() // is equivalent to Bar.a(bar)
// bar.b(3) // is equivalent to Bar.b(&bar, 3)
// bar.c() // is equivalent to Bar.c(&bar)
//
// Notice that the name of the parameter doesn't matter. Some use
// self, others use a lowercase version of the type name, but feel
// free to use whatever is most appropriate.
//
// Okay, you're armed.
//
// Now, please zap the alien structs until they're all gone or
// the Earth will be doomed!
//
const std = @import("std");
// Look at this hideous Alien struct. Know your enemy!
const Alien = struct {
health: u8,
// We hate this method:
pub fn hatch(strength: u8) Alien {
return Alien{
.health = strength * 5,
};
}
};
// Your trusty weapon. Zap those aliens!
const HeatRay = struct {
damage: u8,
// We love this method:
pub fn zap(self: HeatRay, alien: *Alien) void {
alien.health -= if (self.damage >= alien.health) alien.health else self.damage;
}
};
pub fn main() void {
// Look at all of these aliens of various strengths!
var aliens = [_]Alien{
Alien.hatch(2),
Alien.hatch(1),
Alien.hatch(3),
Alien.hatch(3),
Alien.hatch(5),
Alien.hatch(3),
};
var aliens_alive = aliens.len;
const heat_ray = HeatRay{ .damage = 7 }; // We've been given a heat ray weapon.
// We'll keep checking to see if we've killed all the aliens yet.
while (aliens_alive > 0) {
aliens_alive = 0;
// Loop through every alien by reference (* makes a pointer capture value)
for (&aliens) |*alien| {
// *** Zap the alien with the heat ray here! ***
heat_ray.zap(&alien.*);
// If the alien's health is still above 0, it's still alive.
if (alien.health > 0) aliens_alive += 1;
}
std.debug.print("{} aliens. ", .{aliens_alive});
}
std.debug.print("Earth is saved!n", .{});
}
Exe: 048_methods2.zig
//
// Now that we've seen how methods work, let's see if we can help
// our elephants out a bit more with some Elephant methods.
//
const std = @import("std");
const Elephant = struct {
letter: u8,
tail: ?*Elephant = null,
visited: bool = false,
// New Elephant methods!
pub fn getTail(self: *Elephant) *Elephant {
return self.tail.?; // Remember, this means "orelse unreachable"
}
pub fn hasTail(self: *Elephant) bool {
return (self.tail != null);
}
pub fn visit(self: *Elephant) void {
self.visited = true;
}
pub fn print(self: *Elephant) void {
// Prints elephant letter and [v]isited
const v: u8 = if (self.visited) 'v' else ' ';
std.debug.print("{u}{u} ", .{ self.letter, v });
}
};
pub fn main() void {
var elephantA = Elephant{ .letter = 'A' };
var elephantB = Elephant{ .letter = 'B' };
var elephantC = Elephant{ .letter = 'C' };
// This links the elephants so that each tail "points" to the next.
elephantA.tail = &elephantB;
elephantB.tail = &elephantC;
visitElephants(&elephantA);
std.debug.print("n", .{});
}
// This function visits all elephants once, starting with the
// first elephant and following the tails to the next elephant.
fn visitElephants(first_elephant: *Elephant) void {
var e = first_elephant;
while (true) {
e.print();
e.visit();
// This gets the next elephant or stops:
// which method do we want here?
e = if (e.hasTail()) e.tail.? else break;
}
}
// Zig's enums can also have methods! This comment originally asked
// if anyone could find instances of enum methods in the wild. The
// first five pull requests were accepted and here they are:
//
// 1) drforester - I found one in the Zig source:
// https://github.com/ziglang/zig/blob/041212a41cfaf029dc3eb9740467b721c76f406c/src/Compilation.zig#L2495
//
// 2) bbuccianti - I found one!
// https://github.com/ziglang/zig/blob/6787f163eb6db2b8b89c2ea6cb51d63606487e12/lib/std/debug.zig#L477
//
// 3) GoldsteinE - Found many, here's one
// https://github.com/ziglang/zig/blob/ce14bc7176f9e441064ffdde2d85e35fd78977f2/lib/std/target.zig#L65
//
// 4) SpencerCDixon - Love this language so far :-)
// https://github.com/ziglang/zig/blob/a502c160cd51ce3de80b3be945245b7a91967a85/src/zir.zig#L530
//
// 5) tomkun - here's another enum method
// https://github.com/ziglang/zig/blob/4ca1f4ec2e3ae1a08295bc6ed03c235cb7700ab9/src/codegen/aarch64.zig#L24
Exe: 049_quiz6.zig
//
// "Trunks and tails
// Are handy things"
// from Holding Hands
// by Lenore M. Link
//
// Now that we have tails all figured out, can you implement trunks?
//
const std = @import("std");
const Elephant = struct {
letter: u8,
tail: ?*Elephant = null,
trunk: ?*Elephant = null,
visited: bool = false,
// Elephant tail methods!
pub fn getTail(self: *Elephant) *Elephant {
return self.tail.?; // Remember, this means "orelse unreachable"
}
pub fn hasTail(self: *Elephant) bool {
return (self.tail != null);
}
// Your Elephant trunk methods go here!
// ---------------------------------------------------
pub fn getTrunk(self: *Elephant) *Elephant {
return self.trunk.?;
}
pub fn hasTrunk(self: *Elephant) bool {
return (self.trunk != null);
}
// ---------------------------------------------------
pub fn visit(self: *Elephant) void {
self.visited = true;
}
pub fn print(self: *Elephant) void {
// Prints elephant letter and [v]isited
const v: u8 = if (self.visited) 'v' else ' ';
std.debug.print("{u}{u} ", .{ self.letter, v });
}
};
pub fn main() void {
var elephantA = Elephant{ .letter = 'A' };
var elephantB = Elephant{ .letter = 'B' };
var elephantC = Elephant{ .letter = 'C' };
// We link the elephants so that each tail "points" to the next.
elephantA.tail = &elephantB;
elephantB.tail = &elephantC;
// And link the elephants so that each trunk "points" to the previous.
elephantB.trunk = &elephantA;
elephantC.trunk = &elephantB;
visitElephants(&elephantA);
std.debug.print("n", .{});
}
// This function visits all elephants twice, tails to trunks.
fn visitElephants(first_elephant: *Elephant) void {
var e = first_elephant;
// We follow the tails!
while (true) {
e.print();
e.visit();
// This gets the next elephant or stops.
if (e.hasTail()) {
e = e.getTail();
} else {
break;
}
}
// We follow the trunks!
while (true) {
e.print();
// This gets the previous elephant or stops.
if (e.hasTrunk()) {
e = e.getTrunk();
} else {
break;
}
}
}
Exe: 050_no_value.zig
//
// "We live on a placid island of ignorance in the midst
// of black seas of infinity, and it was not meant that
// we should voyage far."
//
// from The Call of Cthulhu
// by H. P. Lovecraft
//
// Zig has at least four ways of expressing "no value":
//
// * undefined
//
// var foo: u8 = undefined;
//
// "undefined" should not be thought of as a value, but as a way
// of telling the compiler that you are not assigning a value
// _yet_. Any variable may be set to undefined, but attempting to
// read its value before assigning one is _always_ a mistake.
//
// * null
//
// var foo: ?u8 = null;
//
// The "null" primitive value _is_ a value that means "no value".
// This is typically used with optional types as with the ?u8
// shown above. When foo equals null, that's not a value of type
// u8. It means you have assigned foo to have _no value_!
//
// * error
//
// var foo: MyError!u8 = BadError;
//
// Errors are _very_ similar to nulls. They _are_ a value, but
// they usually indicate that the "real value" you were looking
// for does not exist. Instead of "no value", you have an error.
// The example error union type of MyError!u8 means that foo
// either holds a u8 value OR a MyError error.
//
// * void
//
// var foo: void = {};
//
// "void" is a _type_, not a value. It is the most popular of the
// Zero Bit Types (those types which take up absolutely no space
// and have only a semantic value). When compiled to executable
// code, zero bit types generate no code at all. The above example
// shows a variable foo of type void which is assigned the value
// of an empty expression. It's much more common to see void as
// the return type of a function that returns nothing.
//
// Zig has all of these ways of expressing different types of "no value"
// because they each serve a purpose. Briefly:
//
// * undefined - there is no value YET, this cannot be read YET
// * null - there is an explicit value of "no value"
// * errors - there is no value because something went wrong
// * void - there will NEVER be a value here
//
// Please use the correct "no value" for each ??? to make this program
// print out a cursed quote from the Necronomicon. ...If you dare.
//
const std = @import("std");
const Err = error{Cthulhu};
pub fn main() void {
var first_line1: *const [16]u8 = undefined;
first_line1 = "That is not dead";
var first_line2: Err!*const [21]u8 = Err.Cthulhu;
first_line2 = "which can eternal lie";
// Note we need the "{!s}" format for the error union string.
std.debug.print("{s} {!s} / ", .{ first_line1, first_line2 });
printSecondLine();
}
fn printSecondLine() void {
var second_line2: ?*const [18]u8 = null;
second_line2 = "even death may die";
std.debug.print("And with strange aeons {s}.n", .{second_line2.?});
}
Exe: 051_values.zig
//
// If you thought the last exercise was a deep dive, hold onto your
// hat because we are about to descend into the computer's molten
// core.
//
// (Shouting) DOWN HERE, THE BITS AND BYTES FLOW FROM RAM TO THE CPU
// LIKE A HOT, DENSE FLUID. THE FORCES ARE INCREDIBLE. BUT HOW DOES
// ALL OF THIS RELATE TO THE DATA IN OUR ZIG PROGRAMS? LET'S HEAD
// BACK UP TO THE TEXT EDITOR AND FIND OUT.
//
// Ah, that's better. Now we can look at some familiar Zig code.
//
// @import() adds the imported code to your own. In this case, code
// from the standard library is added to your program and compiled
// with it. All of this will be loaded into RAM when it runs. Oh, and
// that thing we name "const std"? That's a struct!
//
const std = @import("std");
// Remember our old RPG Character struct? A struct is really just a
// very convenient way to deal with memory. These fields (gold,
// health, experience) are all values of a particular size. Add them
// together and you have the size of the struct as a whole.
const Character = struct {
gold: u32 = 0,
health: u8 = 100,
experience: u32 = 0,
};
// Here we create a character called "the_narrator" that is a constant
// (immutable) instance of a Character struct. It is stored in your
// program as data, and like the instruction code, it is loaded into
// RAM when your program runs. The relative location of this data in
// memory is hard-coded and neither the address nor the value changes.
var the_narrator = Character{
.gold = 12,
.health = 99,
.experience = 9000,
};
// This "global_wizard" character is very similar. The address for
// this data won't change, but the data itself can since this is a var
// and not a const.
var global_wizard = Character{};
// A function is instruction code at a particular address. Function
// parameters in Zig are always immutable. They are stored in "the
// stack". A stack is a type of data structure and "the stack" is a
// specific bit of RAM reserved for your program. The CPU has special
// support for adding and removing things from "the stack", so it is
// an extremely efficient place for memory storage.
//
// Also, when a function executes, the input arguments are often
// loaded into the beating heart of the CPU itself in registers.
//
// Our main() function here has no input parameters, but it will have
// a stack entry (called a "frame").
pub fn main() void {
// Here, the "glorp" character will be allocated on the stack
// because each instance of glorp is mutable and therefore unique
// to the invocation of this function.
var glorp = Character{
.gold = 30,
};
// The "reward_xp" value is interesting. It's an immutable
// value, so even though it is local, it can be put in global
// data and shared between all invocations. But being such a
// small value, it may also simply be inlined as a literal
// value in your instruction code where it is used. It's up
// to the compiler.
const reward_xp: u32 = 200;
// Now let's circle back around to that "std" struct we imported
// at the top. Since it's just a regular Zig value once it's
// imported, we can also assign new names for its fields and
// declarations. "debug" refers to another struct and "print" is a
// public function namespaced within THAT struct.
//
// Let's assign the std.debug.print function to a const named
// "print" so that we can use this new name later!
const print = std.debug.print;
// Now let's look at assigning and pointing to values in Zig.
//
// We'll try three different ways of making a new name to access
// our glorp Character and change one of its values.
//
// "glorp_access1" is incorrectly named! We asked Zig to set aside
// memory for another Character struct. So when we assign glorp to
// glorp_access1 here, we're actually assigning all of the fields
// to make a copy! Now we have two separate characters.
//
// You don't need to fix this. But notice what gets printed in
// your program's output for this one compared to the other two
// assignments below!
var glorp_access1: Character = glorp;
glorp_access1.gold = 111;
print("1:{}!. ", .{glorp.gold == glorp_access1.gold});
// NOTE:
//
// If we tried to do this with a const Character instead of a
// var, changing the gold field would give us a compiler error
// because const values are immutable!
//
// "glorp_access2" will do what we want. It points to the original
// glorp's address. Also remember that we get one implicit
// dereference with struct fields, so accessing the "gold" field
// from glorp_access2 looks just like accessing it from glorp
// itself.
var glorp_access2: *Character = &glorp;
glorp_access2.gold = 222;
print("2:{}!. ", .{glorp.gold == glorp_access2.gold});
// "glorp_access3" is interesting. It's also a pointer, but it's a
// const. Won't that disallow changing the gold value? No! As you
// may recall from our earlier pointer experiments, a constant
// pointer can't change what it's POINTING AT, but the value at
// the address it points to is still mutable! So we CAN change it.
const glorp_access3: *Character = &glorp;
glorp_access3.gold = 333;
print("3:{}!. ", .{glorp.gold == glorp_access3.gold});
// NOTE:
//
// If we tried to do this with a *const Character pointer,
// that would NOT work and we would get a compiler error
// because the VALUE becomes immutable!
//
// Moving along...
//
// 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
// experience points.
//
print("XP before:{}, ", .{glorp.experience});
// Fix 1 of 2 goes here:
levelUp(&glorp, reward_xp);
print("after:{}.n", .{glorp.experience});
}
// Fix 2 of 2 goes here:
fn levelUp(character_access: *Character, xp: u32) void {
character_access.experience += xp;
}
// And there's more!
//
// Data segments (allocated at compile time) and "the stack"
// (allocated at run time) aren't the only places where program data
// can be stored in memory. They're just the most efficient. Sometimes
// we don't know how much memory our program will need until the
// program is running. Also, there is a limit to the size of stack
// memory allotted to programs (often set by your operating system).
// For these occasions, we have "the heap".
//
// You can use as much heap memory as you like (within physical
// limitations, of course), but it's much less efficient to manage
// because there is no built-in CPU support for adding and removing
// items as we have with the stack. Also, depending on the type of
// allocation, your program MAY have to do expensive work to manage
// the use of heap memory. We'll learn about heap allocators later.
//
// Whew! This has been a lot of information. You'll be pleased to know
// that the next exercise gets us back to learning Zig language
// features we can use right away to do more things!
Exe: 052_slices.zig
//
// We've seen that passing arrays around can be awkward. Perhaps you
// remember a particularly horrendous function definition from quiz3?
// This function can only take arrays that are exactly 4 items long!
//
// fn printPowersOfTwo(numbers: [4]u16) void { ... }
//
// That's the trouble with arrays - their size is part of the data
// type and must be hard-coded into every usage of that type. This
// digits array is a [10]u8 forever and ever:
//
// var digits = [10]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//
// Thankfully, Zig has slices, which let you dynamically point to a
// start item and provide a length. Here are slices of our digit
// array:
//
// const foo = digits[0..1]; // 0
// const bar = digits[3..9]; // 3 4 5 6 7 8
// const baz = digits[5..9]; // 5 6 7 8
// const all = digits[0..]; // 0 1 2 3 4 5 6 7 8 9
//
// As you can see, a slice [x..y] starts with the index of the
// first item at x and the last item at y-1. You can leave the y
// off to get "the rest of the items".
//
// The type of a slice on an array of u8 items is []u8.
//
const std = @import("std");
pub fn main() void {
var cards = [8]u8{ 'A', '4', 'K', '8', '5', '2', 'Q', 'J' };
// Please put the first 4 cards in hand1 and the rest in hand2.
const hand1: []u8 = cards[0..4];
const hand2: []u8 = cards[4..];
std.debug.print("Hand1: ", .{});
printHand(hand1);
std.debug.print("Hand2: ", .{});
printHand(hand2);
}
// Please lend this function a hand. A u8 slice hand, that is.
fn printHand(hand: []u8) void {
for (hand) |h| {
std.debug.print("{u} ", .{h});
}
std.debug.print("n", .{});
}
//
// Fun fact: Under the hood, slices are stored as a pointer to
// the first item and a length.
Exe: 053_slices2.zig
//
// You are perhaps tempted to try slices on strings? They're arrays of
// u8 characters after all, right? Slices on strings work great.
// There's just one catch: don't forget that Zig string literals are
// immutable (const) values. So we need to change the type of slice
// from:
//
// var foo: []u8 = "foobar"[0..3];
//
// to:
//
// var foo: []const u8 = "foobar"[0..3];
//
// See if you can fix this Zero Wing-inspired phrase descrambler:
const std = @import("std");
pub fn main() void {
const scrambled = "great base for all your justice are belong to us";
const base1: []const u8 = scrambled[15..23];
const base2: []const u8 = scrambled[6..10];
const base3: []const u8 = scrambled[32..];
printPhrase(base1, base2, base3);
const justice1: []const u8 = scrambled[11..14];
const justice2: []const u8 = scrambled[0..5];
const justice3: []const u8 = scrambled[24..31];
printPhrase(justice1, justice2, justice3);
std.debug.print("n", .{});
}
fn printPhrase(part1: []const u8, part2: []const u8, part3: []const u8) void {
std.debug.print("'{s} {s} {s}.' ", .{ part1, part2, part3 });
}
Exe: 054_manypointers.zig
//
// You can also make pointers to multiple items without using a slice.
//
// var foo: [4]u8 = [4]u8{ 1, 2, 3, 4 };
// var foo_slice: []u8 = foo[0..];
// var foo_ptr: [*]u8 = &foo;
// var foo_slice_from_ptr: []u8 = foo_ptr[0..4];
//
// The difference between foo_slice and foo_ptr is that the slice has
// a known length. The pointer doesn't. It is up to YOU to keep track
// of the number of u8s foo_ptr points to!
//
const std = @import("std");
pub fn main() void {
// Take a good look at the array type to which we're coercing
// the zen12 string (the REAL nature of strings will be
// revealed when we've learned some additional features):
const zen12: *const [21]u8 = "Memory is a resource.";
//
// It would also have been valid to coerce to a slice:
// const zen12: []const u8 = "...";
//
// Now let's turn this into a "many-item pointer":
const zen_manyptr: [*]const u8 = zen12;
// It's okay to access zen_manyptr just like an array or slice as
// long as you keep track of the length yourself!
//
// A "string" in Zig is a pointer to an array of const u8 values
// (or a slice of const u8 values, as we saw above). So, we could
// treat a "many-item pointer" of const u8 as a string as long as
// we can CONVERT IT TO A SLICE. (Hint: we do know the length!)
//
// Please fix this line so the print statement below can print it:
const zen12_string = zen_manyptr[0..zen12.len];
// Here's the moment of truth!
std.debug.print("{s}n", .{zen12_string});
}
//
// Are all of these pointer types starting to get confusing?
//
// FREE ZIG POINTER CHEATSHEET! (Using u8 as the example type.)
// +---------------+----------------------------------------------+
// | u8 | one u8 |
// | *u8 | pointer to one u8 |
// | [2]u8 | two u8s |
// | [*]u8 | pointer to unknown number of u8s |
// | [*]const u8 | pointer to unknown number of immutable u8s |
// | *[2]u8 | pointer to an array of 2 u8s |
// | *const [2]u8 | pointer to an immutable array of 2 u8s |
// | []u8 | slice of u8s |
// | []const u8 | slice of immutable u8s |
// +---------------+----------------------------------------------+
Exe: 055_unions.zig
//
// A union lets you store different types and sizes of data at
// the same memory address. How is this possible? The compiler
// sets aside enough memory for the largest thing you might want
// to store.
//
// In this example, an instance of Foo always takes up u64 of
// space in memory even if you're currently storing a u8.
//
// const Foo = union {
// small: u8,
// medium: u32,
// large: u64,
// };
//
// The syntax looks just like a struct, but a Foo can only hold a
// small OR a medium OR a large value. Once a field becomes
// active, the other inactive fields cannot be accessed. To
// change active fields, assign a whole new instance:
//
// var f = Foo{ .small = 5 };
// f.small += 5; // OKAY
// f.medium = 5432; // ERROR!
// f = Foo{ .medium = 5432 }; // OKAY
//
// Unions can save space in memory because they let you "re-use"
// a space in memory. They also provide a sort of primitive
// polymorphism. Here fooBar() can take a Foo no matter what size
// of unsigned integer it holds:
//
// fn fooBar(f: Foo) void { ... }
//
// Oh, but how does fooBar() know which field is active? Zig has
// a neat way of keeping track, but for now, we'll just have to
// do it manually.
//
// Let's see if we can get this program working!
//
const std = @import("std");
// We've just started writing a simple ecosystem simulation.
// Insects will be represented by either bees or ants. Bees store
// the number of flowers they've visited that day and ants just
// store whether or not they're still alive.
const Insect = union {
flowers_visited: u16,
still_alive: bool,
};
// Since we need to specify the type of insect, we'll use an
// enum (remember those?).
const AntOrBee = enum { a, b };
pub fn main() void {
// We'll just make one bee and one ant to test them out:
const ant = Insect{ .still_alive = true };
const bee = Insect{ .flowers_visited = 15 };
std.debug.print("Insect report! ", .{});
// Oops! We've made a mistake here.
printInsect(ant, AntOrBee.a);
printInsect(bee, AntOrBee.b);
std.debug.print("n", .{});
}
// Eccentric Doctor Zoraptera says that we can only use one
// function to print our insects. Doctor Z is small and sometimes
// inscrutable but we do not question her.
fn printInsect(insect: Insect, what_it_is: AntOrBee) void {
switch (what_it_is) {
.a => std.debug.print("Ant alive is: {}. ", .{insect.still_alive}),
.b => std.debug.print("Bee visited {} flowers. ", .{insect.flowers_visited}),
}
}
Exe: 056_unions2.zig
//
// It is really quite inconvenient having to manually keep track
// of the active field in our union, isn't it?
//
// Thankfully, Zig also has "tagged unions", which allow us to
// store an enum value within our union representing which field
// is active.
//
// const FooTag = enum{ small, medium, large };
//
// const Foo = union(FooTag) {
// small: u8,
// medium: u32,
// large: u64,
// };
//
// Now we can use a switch directly on the union to act on the
// active field:
//
// var f = Foo{ .small = 10 };
//
// switch (f) {
// .small => |my_small| do_something(my_small),
// .medium => |my_medium| do_something(my_medium),
// .large => |my_large| do_something(my_large),
// }
//
// Let's make our Insects use a tagged union (Doctor Zoraptera
// approves).
//
const std = @import("std");
const InsectStat = enum { flowers_visited, still_alive };
const Insect = union(InsectStat) {
flowers_visited: u16,
still_alive: bool,
};
pub fn main() void {
const ant = Insect{ .still_alive = true };
const bee = Insect{ .flowers_visited = 16 };
std.debug.print("Insect report! ", .{});
// Could it really be as simple as just passing the union?
printInsect(ant);
printInsect(bee);
std.debug.print("n", .{});
}
fn printInsect(insect: Insect) void {
switch (insect) {
.still_alive => |a| std.debug.print("Ant alive is: {}. ", .{a}),
.flowers_visited => |f| std.debug.print("Bee visited {} flowers. ", .{f}),
}
}
// By the way, did unions remind you of optional values and errors?
// Optional values are basically "null unions" and errors use "error
// union types". Now we can add our own unions to the mix to handle
// whatever situations we might encounter:
// union(Tag) { value: u32, toxic_ooze: void }
Exe: 057_unions3.zig
//
// With tagged unions, it gets EVEN BETTER! If you don't have a
// need for a separate enum, you can define an inferred enum with
// your union all in one place. Just use the 'enum' keyword in
// place of the tag type:
//
// const Foo = union(enum) {
// small: u8,
// medium: u32,
// large: u64,
// };
//
// Let's convert Insect. Doctor Zoraptera has already deleted the
// explicit InsectStat enum for you!
//
const std = @import("std");
// const InsectStat = enum {
// flowers_visited,
// still_alive,
// };
const Insect = union(enum) {
flowers_visited: u16,
still_alive: bool,
};
pub fn main() void {
const ant = Insect{ .still_alive = true };
const bee = Insect{ .flowers_visited = 17 };
std.debug.print("Insect report! ", .{});
printInsect(ant);
printInsect(bee);
std.debug.print("n", .{});
}
fn printInsect(insect: Insect) void {
switch (insect) {
.still_alive => |a| std.debug.print("Ant alive is: {}. ", .{a}),
.flowers_visited => |f| std.debug.print("Bee visited {} flowers. ", .{f}),
}
}
// Inferred enums are neat, representing the tip of the iceberg
// in the relationship between enums and unions. You can actually
// coerce a union TO an enum (which gives you the active field
// from the union as an enum). What's even wilder is that you can
// coerce an enum to a union! But don't get too excited, that
// only works when the union type is one of those weird zero-bit
// types like void!
//
// Tagged unions, as with most ideas in computer science, have a
// long history going back to the 1960s. However, they're only
// recently becoming mainstream, particularly in system-level
// programming languages. You might have also seen them called
// "variants", "sum types", or even "enums"!
Exe: 058_quiz7.zig
//
// We've absorbed a lot of information about the variations of types
// we can use in Zig. Roughly, in order we have:
//
// u8 single item
// *u8 single-item pointer
// []u8 slice (size known at runtime)
// [5]u8 array of 5 u8s
// [*]u8 many-item pointer (zero or more)
// enum {a, b} set of unique values a and b
// error {e, f} set of unique error values e and f
// struct {y: u8, z: i32} group of values y and z
// union(enum) {a: u8, b: i32} single value either u8 or i32
//
// Values of any of the above types can be assigned as "var" or "const"
// to allow or disallow changes (mutability) via the assigned name:
//
// const a: u8 = 5; // immutable
// var b: u8 = 5; // mutable
//
// We can also make error unions or optional types from any of
// the above:
//
// var a: E!u8 = 5; // can be u8 or error from set E
// var b: ?u8 = 5; // can be u8 or null
//
// Knowing all of this, maybe we can help out a local hermit. He made
// a little Zig program to help him plan his trips through the woods,
// but it has some mistakes.
//
// *************************************************************
// * A NOTE ABOUT THIS EXERCISE *
// * *
// * You do NOT have to read and understand every bit of this *
// * program. This is a very big example. Feel free to skim *
// * through it and then just focus on the few parts that are *
// * actually broken! *
// * *
// *************************************************************
//
const print = @import("std").debug.print;
// The grue is a nod to Zork.
const TripError = error{ Unreachable, EatenByAGrue };
// Let's start with the Places on the map. Each has a name and a
// distance or difficulty of travel (as judged by the hermit).
//
// Note that we declare the places as mutable (var) because we need to
// assign the paths later. And why is that? Because paths contain
// pointers to places and assigning them now would create a dependency
// loop!
const Place = struct {
name: []const u8,
paths: []const Path = undefined,
};
var a = Place{ .name = "Archer's Point" };
var b = Place{ .name = "Bridge" };
var c = Place{ .name = "Cottage" };
var d = Place{ .name = "Dogwood Grove" };
var e = Place{ .name = "East Pond" };
var f = Place{ .name = "Fox Pond" };
// The hermit's hand-drawn ASCII map
// +---------------------------------------------------+
// | * Archer's Point ~~~~ |
// | ~~~ ~~~~~~~~ |
// | ~~~| |~~~~~~~~~~~~ ~~~~~~~ |
// | Bridge ~~~~~~~~ |
// | ^ ^ ^ |
// | ^ ^ / |
// | ^ ^ ^ ^ |_| Cottage |
// | Dogwood Grove |
// | ^ <boat> |
// | ^ ^ ^ ^ ~~~~~~~~~~~~~ ^ ^ |
// | ^ ~~ East Pond ~~~ |
// | ^ ^ ^ ~~~~~~~~~~~~~~ |
// | ~~ ^ |
// | ^ ~~~ <-- short waterfall |
// | ^ ~~~~~ |
// | ~~~~~~~~~~~~~~~~~ |
// | ~~~~ Fox Pond ~~~~~~~ ^ ^ |
// | ^ ~~~~~~~~~~~~~~~ ^ ^ |
// | ~~~~~ |
// +---------------------------------------------------+
//
// We'll be reserving memory in our program based on the number of
// places on the map. Note that we do not have to specify the type of
// this value because we don't actually use it in our program once
// it's compiled! (Don't worry if this doesn't make sense yet.)
const place_count = 6;
// Now let's create all of the paths between sites. A path goes from
// one place to another and has a distance.
const Path = struct {
from: *const Place,
to: *const Place,
dist: u8,
};
// By the way, if the following code seems like a lot of tedious
// manual labor, you're right! One of Zig's killer features is letting
// us write code that runs at compile time to "automate" repetitive
// code (much like macros in other languages), but we haven't learned
// how to do that yet!
const a_paths = [_]Path{
Path{
.from = &a, // from: Archer's Point
.to = &b, // to: Bridge
.dist = 2,
},
};
const b_paths = [_]Path{
Path{
.from = &b, // from: Bridge
.to = &a, // to: Archer's Point
.dist = 2,
},
Path{
.from = &b, // from: Bridge
.to = &d, // to: Dogwood Grove
.dist = 1,
},
};
const c_paths = [_]Path{
Path{
.from = &c, // from: Cottage
.to = &d, // to: Dogwood Grove
.dist = 3,
},
Path{
.from = &c, // from: Cottage
.to = &e, // to: East Pond
.dist = 2,
},
};
const d_paths = [_]Path{
Path{
.from = &d, // from: Dogwood Grove
.to = &b, // to: Bridge
.dist = 1,
},
Path{
.from = &d, // from: Dogwood Grove
.to = &c, // to: Cottage
.dist = 3,
},
Path{
.from = &d, // from: Dogwood Grove
.to = &f, // to: Fox Pond
.dist = 7,
},
};
const e_paths = [_]Path{
Path{
.from = &e, // from: East Pond
.to = &c, // to: Cottage
.dist = 2,
},
Path{
.from = &e, // from: East Pond
.to = &f, // to: Fox Pond
.dist = 1, // (one-way down a short waterfall!)
},
};
const f_paths = [_]Path{
Path{
.from = &f, // from: Fox Pond
.to = &d, // to: Dogwood Grove
.dist = 7,
},
};
// Once we've plotted the best course through the woods, we'll make a
// "trip" out of it. A trip is a series of Places connected by Paths.
// We use a TripItem union to allow both Places and Paths to be in the
// same array.
const TripItem = union(enum) {
place: *const Place,
path: *const Path,
// This is a little helper function to print the two different
// types of item correctly.
fn printMe(self: TripItem) void {
switch (self) {
// Oops! The hermit forgot how to capture the union values
// in a switch statement. Please capture each value as
// 'p' so the print statements work!
.place => print("{s}", .{self.place.name}),
.path => print("--{}->", .{self.path.dist}),
}
}
};
// The Hermit's Notebook is where all the magic happens. A notebook
// entry is a Place discovered on the map along with the Path taken to
// get there and the distance to reach it from the start point. If we
// find a better Path to reach a Place (shorter distance), we update the
// entry. Entries also serve as a "todo" list which is how we keep
// track of which paths to explore next.
const NotebookEntry = struct {
place: *const Place,
coming_from: ?*const Place,
via_path: ?*const Path,
dist_to_reach: u16,
};
// +------------------------------------------------+
// | ~ Hermit's Notebook ~ |
// +---+----------------+----------------+----------+
// | | Place | From | Distance |
// +---+----------------+----------------+----------+
// | 0 | Archer's Point | null | 0 |
// | 1 | Bridge | Archer's Point | 2 | < next_entry
// | 2 | Dogwood Grove | Bridge | 1 |
// | 3 | | | | < end_of_entries
// | ... |
// +---+----------------+----------------+----------+
//
const HermitsNotebook = struct {
// Remember the array repetition operator `**`? It is no mere
// novelty, it's also a great way to assign multiple items in an
// array without having to list them one by one. Here we use it to
// initialize an array with null values.
entries: [place_count]?NotebookEntry = .{null} ** place_count,
// The next entry keeps track of where we are in our "todo" list.
next_entry: u8 = 0,
// Mark the start of empty space in the notebook.
end_of_entries: u8 = 0,
// We'll often want to find an entry by Place. If one is not
// found, we return null.
fn getEntry(self: *HermitsNotebook, place: *const Place) ?*NotebookEntry {
for (&self.entries, 0..) |*entry, i| {
if (i >= self.end_of_entries) break;
// Here's where the hermit got stuck. We need to return
// an optional pointer to a NotebookEntry.
//
// What we have with "entry" is the opposite: a pointer to
// an optional NotebookEntry!
//
// To get one from the other, we need to dereference
// "entry" (with .*) and get the non-null value from the
// optional (with .?) and return the address of that. The
// if statement provides some clues about how the
// dereference and optional value "unwrapping" look
// together. Remember that you return the address with the
// "&" operator.
if (place == entry.*.?.place) return &entry.*.?;
// Try to make your answer this long:__________;
}
return null;
}
// The checkNote() method is the beating heart of the magical
// notebook. Given a new note in the form of a NotebookEntry
// struct, we check to see if we already have an entry for the
// note's Place.
//
// If we DON'T, we'll add the entry to the end of the notebook
// along with the Path taken and distance.
//
// If we DO, we check to see if the path is "better" (shorter
// distance) than the one we'd noted before. If it is, we
// overwrite the old entry with the new one.
fn checkNote(self: *HermitsNotebook, note: NotebookEntry) void {
const existing_entry = self.getEntry(note.place);
if (existing_entry == null) {
self.entries[self.end_of_entries] = note;
self.end_of_entries += 1;
} else if (note.dist_to_reach < existing_entry.?.dist_to_reach) {
existing_entry.?.* = note;
}
}
// The next two methods allow us to use the notebook as a "todo"
// list.
fn hasNextEntry(self: *HermitsNotebook) bool {
return self.next_entry < self.end_of_entries;
}
fn getNextEntry(self: *HermitsNotebook) *const NotebookEntry {
defer self.next_entry += 1; // Increment after getting entry
return &self.entries[self.next_entry].?;
}
// After we've completed our search of the map, we'll have
// computed the shortest Path to every Place. To collect the
// complete trip from the start to the destination, we need to
// walk backwards from the destination's notebook entry, following
// the coming_from pointers back to the start. What we end up with
// is an array of TripItems with our trip in reverse order.
//
// We need to take the trip array as a parameter because we want
// the main() function to "own" the array memory. What do you
// suppose could happen if we allocated the array in this
// function's stack frame (the space allocated for a function's
// "local" data) and returned a pointer or slice to it?
//
// Looks like the hermit forgot something in the return value of
// this function. What could that be?
fn getTripTo(self: *HermitsNotebook, trip: []?TripItem, dest: *Place) !void {
// We start at the destination entry.
const destination_entry = self.getEntry(dest);
// This function needs to return an error if the requested
// destination was never reached. (This can't actually happen
// in our map since every Place is reachable by every other
// Place.)
if (destination_entry == null) {
return TripError.Unreachable;
}
// Variables hold the entry we're currently examining and an
// index to keep track of where we're appending trip items.
var current_entry = destination_entry.?;
var i: u8 = 0;
// At the end of each looping, a continue expression increments
// our index. Can you see why we need to increment by two?
while (true) : (i += 2) {
trip[i] = TripItem{ .place = current_entry.place };
// An entry "coming from" nowhere means we've reached the
// start, so we're done.
if (current_entry.coming_from == null) break;
// Otherwise, entries have a path.
trip[i + 1] = TripItem{ .path = current_entry.via_path.? };
// Now we follow the entry we're "coming from". If we
// aren't able to find the entry we're "coming from" by
// Place, something has gone horribly wrong with our
// program! (This really shouldn't ever happen. Have you
// checked for grues?)
// Note: you do not need to fix anything here.
const previous_entry = self.getEntry(current_entry.coming_from.?);
if (previous_entry == null) return TripError.EatenByAGrue;
current_entry = previous_entry.?;
}
}
};
pub fn main() void {
// Here's where the hermit decides where he would like to go. Once
// you get the program working, try some different Places on the
// map!
const start = &a; // Archer's Point
const destination = &f; // Fox Pond
// Store each Path array as a slice in each Place. As mentioned
// above, we needed to delay making these references to avoid
// creating a dependency loop when the compiler is trying to
// figure out how to allocate space for each item.
a.paths = a_paths[0..];
b.paths = b_paths[0..];
c.paths = c_paths[0..];
d.paths = d_paths[0..];
e.paths = e_paths[0..];
f.paths = f_paths[0..];
// Now we create an instance of the notebook and add the first
// "start" entry. Note the null values. Read the comments for the
// checkNote() method above to see how this entry gets added to
// the notebook.
var notebook = HermitsNotebook{};
var working_note = NotebookEntry{
.place = start,
.coming_from = null,
.via_path = null,
.dist_to_reach = 0,
};
notebook.checkNote(working_note);
// Get the next entry from the notebook (the first being the
// "start" entry we just added) until we run out, at which point
// we'll have checked every reachable Place.
while (notebook.hasNextEntry()) {
const place_entry = notebook.getNextEntry();
// For every Path that leads FROM the current Place, create a
// new note (in the form of a NotebookEntry) with the
// destination Place and the total distance from the start to
// reach that place. Again, read the comments for the
// checkNote() method to see how this works.
for (place_entry.place.paths) |*path| {
working_note = NotebookEntry{
.place = path.to,
.coming_from = place_entry.place,
.via_path = path,
.dist_to_reach = place_entry.dist_to_reach + path.dist,
};
notebook.checkNote(working_note);
}
}
// Once the loop above is complete, we've calculated the shortest
// path to every reachable Place! What we need to do now is set
// aside memory for the trip and have the hermit's notebook fill
// in the trip from the destination back to the path. Note that
// this is the first time we've actually used the destination!
var trip = [_]?TripItem{null} ** (place_count * 2);
notebook.getTripTo(trip[0..], destination) catch |err| {
print("Oh no! {}n", .{err});
return;
};
// Print the trip with a little helper function below.
printTrip(trip[0..]);
}
// Remember that trips will be a series of alternating TripItems
// containing a Place or Path from the destination back to the start.
// The remaining space in the trip array will contain null values, so
// we need to loop through the items in reverse, skipping nulls, until
// we reach the destination at the front of the array.
fn printTrip(trip: []?TripItem) void {
// We convert the usize length to a u8 with @intCast(), a
// builtin function just like @import(). We'll learn about
// these properly in a later exercise.
var i: u8 = @intCast(trip.len);
while (i > 0) {
i -= 1;
if (trip[i] == null) continue;
trip[i].?.printMe();
}
print("n", .{});
}
// Going deeper:
//
// In computer science terms, our map places are "nodes" or "vertices" and
// the paths are "edges". Together, they form a "weighted, directed
// graph". It is "weighted" because each path has a distance (also
// known as a "cost"). It is "directed" because each path goes FROM
// one place TO another place (undirected graphs allow you to travel
// on an edge in either direction).
//
// Since we append new notebook entries at the end of the list and
// then explore each sequentially from the beginning (like a "todo"
// list), we are treating the notebook as a "First In, First Out"
// (FIFO) queue.
//
// Since we examine all closest paths first before trying further ones
// (thanks to the "todo" queue), we are performing a "Breadth-First
// Search" (BFS).
//
// By tracking "lowest cost" paths, we can also say that we're
// performing a "least-cost search".
//
// Even more specifically, the Hermit's Notebook most closely
// resembles the Shortest Path Faster Algorithm (SPFA), attributed to
// Edward F. Moore. By replacing our simple FIFO queue with a
// "priority queue", we would basically have Dijkstra's algorithm. A
// priority queue retrieves items sorted by "weight" (in our case, it
// would keep the paths with the shortest distance at the front of the
// queue). Dijkstra's algorithm is more efficient because longer paths
// can be eliminated more quickly. (Work it out on paper to see why!)
Exe: 059_integers.zig
//
// 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 = 'u{0041}'; // Unicode code points can take up to 21 bits
//
// You can also place underscores in numbers to aid readability:
//
// const t1: u32 = 14_689_520 // Ford Model T sales 1909-1927
// const t2: u32 = 0xE0_24_F0 // same, in hex pairs
//
// Please fix the message:
const print = @import("std").debug.print;
pub fn main() void {
const zig = [_]u8{
0o132, // octal
0b1101001, // binary
0x67, // hex
};
print("{s} is cool.n", .{zig});
}
Exe: 060_floats.zig
//
// Zig has support for IEEE-754 floating-point numbers in these
// specific sizes: f16, f32, f64, f80, and f128. Floating point
// literals may be written in the same ways as integers but also
// in scientific notation:
//
// const a1: f32 = 1200; // 1,200
// const a2: f32 = 1.2e+3; // 1,200
// const b1: f32 = -500_000.0; // -500,000
// const b2: f32 = -5.0e+5; // -500,000
//
// Hex floats can't use the letter 'e' because that's a hex
// digit, so we use a 'p' instead:
//
// const hex: f16 = 0x2A.F7p+3; // Wow, that's arcane!
//
// Be sure to use a float type that is large enough to store your
// value (both in terms of significant digits and scale).
// Rounding may or may not be okay, but numbers which are too
// large or too small become inf or -inf (positive or negative
// infinity)!
//
// const pi: f16 = 3.1415926535; // rounds to 3.140625
// const av: f16 = 6.02214076e+23; // Avogadro's inf(inity)!
//
// When performing math operations with numeric literals, ensure
// the types match. Zig does not perform unsafe type coercions
// behind your back:
//
// var foo: f16 = 5; // NO ERROR
//
// var foo: u16 = 5; // A literal of a different type
// var bar: f16 = foo; // ERROR
//
// Please fix the two float problems with this program and
// display the result as a whole number.
const print = @import("std").debug.print;
pub fn main() void {
// The approximate weight of the Space Shuttle upon liftoff
// (including boosters and fuel tank) was 4,480,000 lb.
//
// We'll convert this weight from pounds to metric units at a
// conversion of 0.453592 kg to the pound.
const shuttle_weight: f32 = 0.453592 * 4480e3;
// By default, float values are formatted in standard decimal
// notation. Experiment with '{d}' and '{d:.3}' to see how
// decimal formatting works, or try '{e}' and '{e:.3}' for
// scientific notation.
// NOTE: The weight of the shuttle is a huge number, a scientific notation
// may be more appropriate.
print("Shuttle liftoff weight: {e:.3} metric tonsn", .{shuttle_weight / 1e3});
}
// Floating further:
//
// As an example, Zig's f16 is a IEEE 754 "half-precision" binary
// floating-point format ("binary16"), which is stored in memory
// like so:
//
// 0 1 0 0 0 0 1 0 0 1 0 0 1 0 0 0
// | |-------| |-----------------|
// | exponent significand
// |
// sign
//
// This example is the decimal number 3.140625, which happens to
// be the closest representation of Pi we can make with an f16
// due to the way IEEE-754 floating points store digits:
//
// * Sign bit 0 makes the number positive.
// * Exponent bits 10000 are a scale of 16.
// * Significand bits 1001001000 are the decimal value 584.
//
// IEEE-754 saves space by modifying these values: the value
// 01111 is always subtracted from the exponent bits (in our
// case, 10000 - 01111 = 1, so our exponent is 2^1) and our
// significand digits become the decimal value _after_ an
// implicit 1 (so 1.1001001000 or 1.5703125 in decimal)! This
// gives us:
//
// 2^1 * 1.5703125 = 3.140625
//
// Feel free to forget these implementation details immediately.
// The important thing to know is that floating point numbers are
// great at storing big and small values (f64 lets you work with
// numbers on the scale of the number of atoms in the universe),
// but digits may be rounded, leading to results which are less
// precise than integers.
//
// Fun fact: sometimes you'll see the significand labeled as a
// "mantissa" but Donald E. Knuth says not to do that.
//
// C compatibility fact: There is also a Zig floating point type
// specifically for working with C ABIs called c_longdouble.
Exe: 061_coercions.zig
//
// It'll only take us a moment to learn the Zig type coercion
// rules because they're quite logical.
//
// 1. Types can always be made _more_ restrictive.
//
// var foo: u8 = 5;
// var p1: *u8 = &foo;
// var p2: *const u8 = p1; // mutable to immutable
//
// 2. Numeric types can coerce to _larger_ types.
//
// var n1: u8 = 5;
// var n2: u16 = n1; // integer "widening"
//
// var n3: f16 = 42.0;
// var n4: f32 = n3; // float "widening"
//
// 3. Single-item pointers to arrays coerce to slices and
// many-item pointers.
//
// const arr: [3]u8 = [3]u8{5, 6, 7};
// const s: []const u8 = &arr; // to slice
// const p: [*]const u8 = &arr; // to many-item pointer
//
// 4. Single-item mutable pointers can coerce to single-item
// pointers pointing to an array of length 1. (Interesting!)
//
// var five: u8 = 5;
// var a_five: *[1]u8 = &five;
//
// 5. Payload types and null coerce to optionals.
//
// var num: u8 = 5;
// var maybe_num: ?u8 = num; // payload type
// maybe_num = null; // null
//
// 6. Payload types and errors coerce to error unions.
//
// const MyError = error{Argh};
// var char: u8 = 'x';
// var char_or_die: MyError!u8 = char; // payload type
// char_or_die = MyError.Argh; // error
//
// 7. 'undefined' coerces to any type (or it wouldn't work!)
//
// 8. Compile-time numbers coerce to compatible types.
//
// Just about every single exercise program has had an example
// of this, but a full and proper explanation is coming your
// way soon in the third-eye-opening subject of comptime.
//
// 9. Tagged unions coerce to the current tagged enum.
//
// 10. Enums coerce to a tagged union when that tagged field is a
// zero-length type that has only one value (like void).
//
// 11. Zero-bit types (like void) can be coerced into single-item
// pointers.
//
// The last three are fairly esoteric, but you're more than
// welcome to read more about them in the official Zig language
// documentation and write your own experiments.
const print = @import("std").debug.print;
pub fn main() void {
var letter: u8 = 'A';
const my_letter: ?*[1]u8 = &letter;
// ^^^^^^^
// Your type here.
// Must coerce from &letter (which is a *u8).
// Hint: Use coercion Rules 4 and 5.
// When it's right, this will work:
print("Letter: {u}n", .{my_letter.?.*[0]});
}
Exe: 062_loop_expressions.zig
//
// Remember using if/else statements as expressions like this?
//
// var foo: u8 = if (true) 5 else 0;
//
// Zig also lets you use for and while loops as expressions.
//
// Like 'return' for functions, you can return a value from a
// loop block with break:
//
// break true; // return boolean value from block
//
// But what value is returned from a loop if a break statement is
// never reached? We need a default expression. Thankfully, Zig
// loops also have 'else' clauses! As you might have guessed, the
// 'else' clause is evaluated when: 1) a 'while' condition becomes
// false or 2) a 'for' loop runs out of items.
//
// const two: u8 = while (true) break 2 else 0; // 2
// const three: u8 = for ([1]u8{1}) |f| break 3 else 0; // 3
//
// If you do not provide an else clause, an empty one will be
// provided for you, which will evaluate to the void type, which
// is probably not what you want. So consider the else clause
// essential when using loops as expressions.
//
// const four: u8 = while (true) {
// break 4;
// }; // <-- ERROR! Implicit 'else void' here!
//
// With that in mind, see if you can fix the problem with this
// program.
//
const print = @import("std").debug.print;
pub fn main() void {
const langs: [6][]const u8 = .{
"Erlang",
"Algol",
"C",
"OCaml",
"Zig",
"Prolog",
};
// Let's find the first language with a three-letter name and
// return it from the for loop.
const current_lang: ?[]const u8 = for (langs) |lang| {
if (lang.len == 3) break lang;
} else null;
if (current_lang) |cl| {
print("Current language: {s}n", .{cl});
} else {
print("Did not find a three-letter language name. :-(n", .{});
}
}
Exe: 063_labels.zig
//
// Loop bodies are blocks, which are also expressions. We've seen
// how they can be used to evaluate and return values. To further
// expand on this concept, it turns out we can also give names to
// blocks by applying a 'label':
//
// my_label: { ... }
//
// Once you give a block a label, you can use 'break' to exit
// from that block.
//
// outer_block: { // outer block
// while (true) { // inner block
// break :outer_block;
// }
// unreachable;
// }
//
// As we've just learned, you can return a value using a break
// statement. Does that mean you can return a value from any
// labeled block? Yes it does!
//
// const foo = make_five: {
// const five = 1 + 1 + 1 + 1 + 1;
// break :make_five five;
// };
//
// Labels can also be used with loops. Being able to break out of
// nested loops at a specific level is one of those things that
// you won't use every day, but when the time comes, it's
// incredibly convenient. Being able to return a value from an
// inner loop is sometimes so handy, it almost feels like cheating
// (and can help you avoid creating a lot of temporary variables).
//
// const bar: u8 = two_loop: while (true) {
// while (true) {
// break :two_loop 2;
// }
// } else 0;
//
// In the above example, the break exits from the outer loop
// labeled "two_loop" and returns the value 2. The else clause is
// attached to the outer two_loop and would be evaluated if the
// loop somehow ended without the break having been called.
//
// Finally, you can also use block labels with the 'continue'
// statement:
//
// my_while: while (true) {
// continue :my_while;
// }
//
const print = @import("std").debug.print;
// As mentioned before, we'll soon understand why these two
// numbers don't need explicit types. Hang in there!
const ingredients = 4;
const foods = 4;
const Food = struct {
name: []const u8,
requires: [ingredients]bool,
};
// Chili Macaroni Tomato Sauce Cheese
// ------------------------------------------------------
// Mac & Cheese x x
// Chili Mac x x
// Pasta x x
// Cheesy Chili x x
// ------------------------------------------------------
const menu: [foods]Food = [_]Food{
Food{
.name = "Mac & Cheese",
.requires = [ingredients]bool{ false, true, false, true },
},
Food{
.name = "Chili Mac",
.requires = [ingredients]bool{ true, true, false, false },
},
Food{
.name = "Pasta",
.requires = [ingredients]bool{ false, true, true, false },
},
Food{
.name = "Cheesy Chili",
.requires = [ingredients]bool{ true, false, false, true },
},
};
pub fn main() void {
// Welcome to Cafeteria USA! Choose your favorite ingredients
// and we'll produce a delicious meal.
//
// Cafeteria Customer Note: Not all ingredient combinations
// make a meal. The default meal is macaroni and cheese.
//
// Software Developer Note: Hard-coding the ingredient
// numbers (based on array position) will be fine for our
// tiny example, but it would be downright criminal in a real
// application!
const wanted_ingredients = [_]u8{ 0, 3 }; // Chili, Cheese
// Look at each Food on the menu...
const meal = food_loop: for (menu) |food| {
// Now look at each required ingredient for the Food...
for (food.requires, 0..) |required, required_ingredient| {
// This ingredient isn't required, so skip it.
if (!required) continue;
// See if the customer wanted this ingredient.
// (Remember that want_it will be the index number of
// the ingredient based on its position in the
// required ingredient list for each food.)
const found = for (wanted_ingredients) |want_it| {
if (required_ingredient == want_it) break true;
} else false;
// We did not find this required ingredient, so we
// can't make this Food. Continue the outer loop.
if (!found) continue :food_loop;
}
// If we get this far, the required ingredients were all
// wanted for this Food.
//
// Please return this Food from the loop.
break food;
} else menu[0];
// ^ Oops! We forgot to return Mac & Cheese as the default
// Food when the requested ingredients aren't found.
print("Enjoy your {s}!n", .{meal.name});
}
// Challenge: You can also do away with the 'found' variable in
// the inner loop. See if you can figure out how to do that!
Exe: 064_builtins.zig
//
// The Zig compiler provides "builtin" functions. You've already
// gotten used to seeing an @import() at the top of every
// Ziglings exercise.
//
// We've also seen @intCast() in "016_for2.zig", "058_quiz7.zig";
// and @intFromEnum() in "036_enums2.zig".
//
// Builtins are special because they are intrinsic to the Zig
// language itself (as opposed to being provided in the standard
// library). They are also special because they can provide
// functionality that is only possible with help from the
// compiler, such as type introspection (the ability to examine
// type properties from within a program).
//
// Zig contains over 100 builtin functions. We're certainly
// not going to cover them all, but we can look at some
// interesting ones.
//
// Before we begin, know that many builtin functions have
// parameters marked as "comptime". It's probably fairly clear
// what we mean when we say that these parameters need to be
// "known at compile time." But rest assured we'll be doing the
// "comptime" subject real justice soon.
//
const print = @import("std").debug.print;
pub fn main() void {
// The second builtin, alphabetically, is:
// @addWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }
// * 'a' and 'b' are numbers of anytype.
// * The return value is a tuple with the result and a possible overflow bit.
//
// Let's try it with a tiny 4-bit integer size to make it clear:
const a: u4 = 0b1101;
const b: u4 = 0b0101;
const my_result = @addWithOverflow(a, b);
// Check out our fancy formatting! b:0>4 means, "print
// as a binary number, zero-pad right-aligned four digits."
// The print() below will produce: "1101 + 0101 = 0010 (true)".
print("{b:0>4} + {b:0>4} = {b:0>4} ({s})", .{ a, b, my_result[0], if (my_result[1] == 1) "true" else "false" });
// Let's make sense of this answer. The value of 'b' in decimal is 5.
// Let's add 5 to 'a' but go one by one and see where it overflows:
//
// a | b | result | overflowed?
// ----------------------------------
// 1101 + 0001 = 1110 | false
// 1110 + 0001 = 1111 | false
// 1111 + 0001 = 0000 | true (the real answer is 10000)
// 0000 + 0001 = 0001 | false
// 0001 + 0001 = 0010 | false
//
// In the last two lines the value of 'a' is corrupted because there was
// an overflow in line 3, but the operations of lines 4 and 5 themselves
// do not overflow.
// There is a difference between
// - a value, that overflowed at some point and is now corrupted
// - a single operation that overflows and maybe causes subsequent errors
// In practice we usually notice the overflowed value first and have to work
// our way backwards to the operation that caused the overflow.
//
// If there was no overflow at all while adding 5 to a, what value would
// 'my_result' hold? Write the answer in into 'expected_result'.
const expected_result: u8 = 0b00010010;
print(". Without overflow: {b:0>8}. ", .{expected_result});
print("Furthermore, ", .{});
// Here's a fun one:
//
// @bitReverse(integer: anytype) T
// * 'integer' is the value to reverse.
// * The return value will be the same type with the
// value's bits reversed!
//
// Now it's your turn. See if you can fix this attempt to use
// this builtin to reverse the bits of a u8 integer.
const input: u8 = 0b11110000;
const tupni: u8 = @bitReverse(input);
print("{b:0>8} backwards is {b:0>8}.n", .{ input, tupni });
}
Exe: 065_builtins2.zig
//
// Zig has builtins for mathematical operations such as...
//
// @sqrt @sin @cos
// @exp @log @floor
//
// ...and lots of type casting operations such as...
//
// @as @errorFromInt @floatFromInt
// @ptrFromInt @intFromPtr @intFromEnum
//
// Spending part of a rainy day skimming through the complete
// list of builtins in the official Zig documentation wouldn't be
// a bad use of your time. There are some seriously cool features
// in there. Check out @call, @compileLog, @embedFile, and @src!
//
// ...
//
// For now, we're going to complete our examination of builtins
// by exploring just THREE of Zig's MANY introspection abilities:
//
// 1. @This() type
//
// Returns the innermost struct, enum, or union that a function
// call is inside.
//
// 2. @typeInfo(comptime T: type) @import("std").builtin.Type
//
// Returns information about any type in a data structure which
// will contain different information depending on which type
// you're examining.
//
// 3. @TypeOf(...) type
//
// Returns the type common to all input parameters (each of which
// may be any expression). The type is resolved using the same
// "peer type resolution" process the compiler itself uses when
// inferring types.
//
// (Notice how the two functions which return types start with
// uppercase letters? This is a standard naming practice in Zig.)
//
const print = @import("std").debug.print;
const Narcissus = struct {
me: *Narcissus = undefined,
myself: *Narcissus = undefined,
echo: void = undefined, // Alas, poor Echo!
fn fetchTheMostBeautifulType() type {
return @This();
}
};
pub fn main() void {
var narcissus: Narcissus = Narcissus{};
// Oops! We cannot leave the 'me' and 'myself' fields
// undefined. Please set them here:
narcissus.me = &narcissus;
narcissus.myself = &narcissus;
// This determines a "peer type" from three separate
// references (they just happen to all be the same object).
const Type1 = @TypeOf(narcissus, narcissus.me.*, narcissus.myself.*);
// Oh dear, we seem to have done something wrong when calling
// this function. We called it as a method, which would work
// if it had a self parameter. But it doesn't. (See above.)
//
// The fix for this is very subtle, but it makes a big
// difference!
const Type2 = Narcissus.fetchTheMostBeautifulType();
// Now we print a pithy statement about Narcissus.
print("A {s} loves all {s}es. ", .{
maximumNarcissism(Type1),
maximumNarcissism(Type2),
});
// His final words as he was looking in
// those waters he habitually watched
// were these:
// "Alas, my beloved boy, in vain!"
// The place gave every word back in reply.
// He cried:
// "Farewell."
// And Echo called:
// "Farewell!"
//
// --Ovid, The Metamorphoses
// translated by Ian Johnston
print("He has room in his heart for:", .{});
// A StructFields array
const fields = @typeInfo(Narcissus).@"struct".fields;
// 'fields' is a slice of StructFields. Here's the declaration:
//
// pub const StructField = struct {
// name: [:0]const u8,
// type: type,
// default_value_ptr: ?*const anyopaque,
// is_comptime: bool,
// alignment: comptime_int,
//
// defaultValue() ?sf.type // Function that loads the
// // field's default value from
// // `default_value_ptr`
// };
//
// Please complete these 'if' statements so that the field
// name will not be printed if the field is of type 'void'
// (which is a zero-bit type that takes up no space at all!):
if (fields[0].type != void) {
print(" {s}", .{fields[0].name});
}
if (fields[1].type != void) {
print(" {s}", .{fields[1].name});
}
if (fields[2].type != void) {
print(" {s}", .{fields[2].name});
}
// Yuck, look at all that repeated code above! I don't know
// about you, but it makes me itchy.
//
// Alas, we can't use a regular 'for' loop here because
// 'fields' can only be evaluated at compile time. It seems
// like we're overdue to learn about this "comptime" stuff,
// doesn't it? Don't worry, we'll get there.
print(".n", .{});
}
// NOTE: This exercise did not originally include the function below.
// After Zig 0.10.0, `@typeName` began prefixing the returned type name
// with the source file name. For example, "Narcissus" became
// "065_builtins2.Narcissus".
//
// To fix this, we've added this function to strip the filename from
// the front of the type name. (It returns a slice of the type name
// starting just after the ".")
//
// We'll be seeing @typeName again in Exercise 070. For now, you can
// see that it takes a Type and returns a u8 "string".
fn maximumNarcissism(myType: type) []const u8 {
const find = @import("std").mem.find;
// Turn "065_builtins2.Narcissus" into "Narcissus"
const name = @typeName(myType);
return name[find(u8, name, ".").? + 1 ..];
}
Exe: 066_comptime.zig
//
// "Compile time" is a program's environment while it is being
// compiled. In contrast, "run time" is the environment while the
// compiled program is executing (traditionally as machine code
// on a hardware CPU).
//
// Errors make an easy example:
//
// 1. Compile-time error: caught by the compiler, usually
// resulting in a message to the programmer.
//
// 2. Runtime error: either caught by the running program itself
// or by the host hardware or operating system. Could be
// gracefully caught and handled or could cause the computer
// to crash (or halt and catch fire)!
//
// All compiled languages must perform a certain amount of logic
// at compile time in order to analyze the code, maintain a table
// of symbols (such as variable and function names), etc.
//
// Optimizing compilers can also figure out how much of a program
// can be pre-computed or "inlined" at compile time to make the
// resulting program more efficient. Smart compilers can even
// "unroll" loops, turning their logic into a fast linear
// sequence of statements (at the usually very slight expense of
// the increased size of the repeated code).
//
// Zig takes these concepts further by making these optimizations
// an integral part of the language itself!
//
const print = @import("std").debug.print;
pub fn main() void {
// ALL numeric literals in Zig are of type comptime_int or
// comptime_float. They are of arbitrary size (as big or
// little as you need).
//
// Notice how we don't have to specify a size like "u8",
// "i32", or "f64" when we assign identifiers immutably with
// "const".
//
// When we use these identifiers in our program, the VALUES
// are inserted at compile time into the executable code. The
// IDENTIFIERS "const_int" and "const_float" don't exist in
// our compiled application!
const const_int = 12345;
const const_float = 987.654;
print("Immutable: {}, {d:.3}; ", .{ const_int, const_float });
// But something changes when we assign the exact same values
// to identifiers mutably with "var".
//
// The literals are STILL comptime_int and comptime_float,
// but we wish to assign them to identifiers which are
// mutable at runtime.
//
// To be mutable at runtime, these identifiers must refer to
// areas of memory. In order to refer to areas of memory, Zig
// must know exactly how much memory to reserve for these
// values. Therefore, it follows that we just specify numeric
// types with specific sizes. The comptime numbers will be
// coerced (if they'll fit!) into your chosen runtime types.
// For this it is necessary to specify a size, e.g. 32 bit.
var var_int: u32 = 12345;
var var_float: f32 = 987.654;
// We can change what is stored at the areas set aside for
// "var_int" and "var_float" in the running compiled program.
var_int = 54321;
var_float = 456.789;
print("Mutable: {}, {d:.3}; ", .{ var_int, var_float });
// Bonus: Now that we're familiar with Zig's builtins, we can
// also inspect the types to see what they are, no guessing
// needed!
print("Types: {}, {}, {}, {}n", .{
@TypeOf(const_int),
@TypeOf(const_float),
@TypeOf(var_int),
@TypeOf(var_float),
});
}
Exe: 067_comptime2.zig
//
// We've seen that Zig implicitly performs some evaluations at
// compile time. But sometimes you'll want to explicitly request
// compile time evaluation. For that, we have a new keyword:
//
// . . . o . . * . . .
// . * | . . . . . . * . .
// --o-- comptime * | .. .
// * | * . . . . --*-- . * .
// . . . . . . . . . | . . .
// (ASCII art depicting a starry sky with "comptime" as rising star)
//
// When placed before a variable declaration, 'comptime'
// guarantees that every usage of that variable will be performed
// at compile time.
//
// As a simple example, compare these two statements:
//
// var bar1 = 5; // ERROR!
// comptime var bar2 = 5; // OKAY!
//
// The first one gives us an error because Zig assumes mutable
// identifiers (declared with 'var') will be used at runtime and
// we have not assigned a runtime type (like u8 or f32). Trying
// to use a comptime_int of undetermined size at runtime is
// a MEMORY CRIME and you are UNDER ARREST.
//
// The second one is okay because we've told Zig that 'bar2' is
// a compile time variable. Zig will help us ensure this is true
// and let us know if we make a mistake.
//
const print = @import("std").debug.print;
pub fn main() void {
//
// In this contrived example, we've decided to allocate some
// arrays using a variable count! But something's missing...
//
comptime var count = 0;
count += 1;
const a1: [count]u8 = .{'A'} ** count;
count += 1;
const a2: [count]u8 = .{'B'} ** count;
count += 1;
const a3: [count]u8 = .{'C'} ** count;
count += 1;
const a4: [count]u8 = .{'D'} ** count;
print("{s} {s} {s} {s}n", .{ a1, a2, a3, a4 });
// Builtin BONUS!
//
// The @compileLog() builtin is like a print statement that
// ONLY operates at compile time. The Zig compiler treats
// @compileLog() calls as errors, so you'll want to use them
// temporarily to debug compile time logic.
//
// Try uncommenting this line and playing around with it
// (copy it, move it) to see what it does:
// @compileLog("Count at compile time: ", count);
}
Exe: 068_comptime3.zig
//
// You can also put 'comptime' before a function parameter to
// enforce that the argument passed to the function must be known
// at compile time. We've actually been using a function like
// this the entire time, std.debug.print():
//
// fn print(comptime fmt: []const u8, args: anytype) void
//
// Notice that the format string parameter 'fmt' is marked as
// 'comptime'. One of the neat benefits of this is that the
// format string can be checked for errors at compile time rather
// than crashing at runtime.
//
// (The actual formatting is done by std.Io.Writer.print() and it
// contains a complete format string parser that runs entirely at
// compile time!)
//
const print = @import("std").debug.print;
// This struct is the model of a model boat. We can transform it
// to any scale we would like: 1:2 is half-size, 1:32 is
// thirty-two times smaller than the real thing, and so forth.
const Schooner = struct {
name: []const u8,
scale: u32 = 1,
hull_length: u32 = 143,
bowsprit_length: u32 = 34,
mainmast_height: u32 = 95,
fn scaleMe(self: *Schooner, scale: u32) void {
var my_scale = scale;
// We did something neat here: we've anticipated the
// possibility of accidentally attempting to create a
// scale of 1:0. Rather than having this result in a
// divide-by-zero error at runtime, we've turned this
// into a compile error.
//
// This is probably the correct solution most of the
// time. But our model boat model program is very casual
// and we just want it to "do what I mean" and keep
// working.
//
// Please change this so that it sets a 0 scale to 1
// instead.
if (my_scale == 0) my_scale = 1; //@compileError("Scale 1:0 is not valid!");
self.scale = my_scale;
self.hull_length /= my_scale;
self.bowsprit_length /= my_scale;
self.mainmast_height /= my_scale;
}
fn printMe(self: Schooner) void {
print("{s} (1:{}, {} x {})n", .{
self.name,
self.scale,
self.hull_length,
self.mainmast_height,
});
}
};
pub fn main() void {
var whale = Schooner{ .name = "Whale" };
var shark = Schooner{ .name = "Shark" };
var minnow = Schooner{ .name = "Minnow" };
// Hey, we can't just pass this runtime variable as an
// argument to the scaleMe() method. What would let us do
// that?
var scale: u32 = undefined;
scale = 32; // 1:32 scale
minnow.scaleMe(scale);
minnow.printMe();
scale -= 16; // 1:16 scale
shark.scaleMe(scale);
shark.printMe();
scale -= 16; // 1:0 scale (oops, but DON'T FIX THIS!)
whale.scaleMe(scale);
whale.printMe();
}
//
// Going deeper:
//
// What would happen if you DID attempt to build a model in the
// scale of 1:0?
//
// A) You're already done!
// B) You would suffer a mental divide-by-zero error.
// C) You would construct a singularity and destroy the
// planet.
//
// And how about a model in the scale of 0:1?
//
// A) You're already done!
// B) You'd arrange nothing carefully into the form of the
// original nothing but infinitely larger.
// C) You would construct a singularity and destroy the
// planet.
//
// Answers can be found on the back of the Ziglings packaging.
Exe: 069_comptime4.zig
//
// One of the more common uses of 'comptime' function parameters is
// passing a type to a function:
//
// fn foo(comptime MyType: type) void { ... }
//
// In fact, types are ONLY available at compile time, so the
// 'comptime' keyword is required here.
//
// Please take a moment to put on the wizard hat which has been
// provided for you. We're about to use this ability to implement
// a generic function.
//
const print = @import("std").debug.print;
pub fn main() void {
// Here we declare arrays of three different types and sizes
// at compile time from a function call. Neat!
const s1 = makeSequence(u8, 3); // creates a [3]u8
const s2 = makeSequence(u32, 5); // creates a [5]u32
const s3 = makeSequence(i64, 7); // creates a [7]i64
print("s1={any}, s2={any}, s3={any}n", .{ s1, s2, s3 });
}
// This function is pretty wild because it executes at runtime
// and is part of the final compiled program. The function is
// compiled with unchanging data sizes and types.
//
// And yet it ALSO allows for different sizes and types. This
// seems paradoxical. How could both things be true?
//
// To accomplish this, the Zig compiler actually generates a
// separate copy of the function for every size/type combination!
// So in this case, three different functions will be generated
// for you, each with machine code that handles that specific
// data size and type.
//
// Please fix this function so that the 'size' parameter:
//
// 1) Is guaranteed to be known at compile time.
// 2) Sets the size of the array of type T (which is the
// sequence we're creating and returning).
//
fn makeSequence(comptime T: type, comptime size: usize) [size]T {
var sequence: [size]T = undefined;
var i: usize = 0;
while (i < size) : (i += 1) {
sequence[i] = @as(T, @intCast(i)) + 1;
}
return sequence;
}
Exe: 070_comptime5.zig
//
// Being able to pass types to functions at compile time lets us
// generate code that works with multiple types. But it doesn't
// help us pass VALUES of different types to a function.
//
// For that, we have the 'anytype' placeholder, which tells Zig
// to infer the actual type of a parameter at compile time.
//
// fn foo(thing: anytype) void { ... }
//
// Then we can use builtins such as @TypeOf(), @typeInfo(),
// @typeName(), @hasDecl(), and @hasField() to determine more
// about the type that has been passed in. All of this logic will
// be performed entirely at compile time.
//
const print = @import("std").debug.print;
// Let's define three structs: Duck, RubberDuck, and Duct. Notice
// that Duck and RubberDuck both contain waddle() and quack()
// methods declared in their namespace (also known as "decls").
const Duck = struct {
eggs: u8,
loudness: u8,
location_x: i32 = 0,
location_y: i32 = 0,
fn waddle(self: *Duck, x: i16, y: i16) void {
self.location_x += x;
self.location_y += y;
}
fn quack(self: Duck) void {
if (self.loudness < 4) {
print(""Quack." ", .{});
} else {
print(""QUACK!" ", .{});
}
}
};
const RubberDuck = struct {
in_bath: bool = false,
location_x: i32 = 0,
location_y: i32 = 0,
fn waddle(self: *RubberDuck, x: i16, y: i16) void {
self.location_x += x;
self.location_y += y;
}
fn quack(self: RubberDuck) void {
// Assigning an expression to '_' allows us to safely
// "use" the value while also ignoring it.
_ = self;
print(""Squeek!" ", .{});
}
fn listen(self: RubberDuck, dev_talk: []const u8) void {
// Listen to developer talk about programming problem.
// Silently contemplate problem. Emit helpful sound.
_ = dev_talk;
self.quack();
}
};
const Duct = struct {
diameter: u32,
length: u32,
galvanized: bool,
connection: ?*Duct = null,
fn connect(self: *Duct, other: *Duct) !void {
if (self.diameter == other.diameter) {
self.connection = other;
} else {
return DuctError.UnmatchedDiameters;
}
}
};
const DuctError = error{UnmatchedDiameters};
pub fn main() void {
// This is a real duck!
const ducky1 = Duck{
.eggs = 0,
.loudness = 3,
};
// This is not a real duck, but it has quack() and waddle()
// abilities, so it's still a "duck".
const ducky2 = RubberDuck{
.in_bath = false,
};
// This is not even remotely a duck.
const ducky3 = Duct{
.diameter = 17,
.length = 165,
.galvanized = true,
};
print("ducky1: {}, ", .{isADuck(ducky1)});
print("ducky2: {}, ", .{isADuck(ducky2)});
print("ducky3: {}n", .{isADuck(ducky3)});
}
// This function has a single parameter which is inferred at
// compile time. It uses builtins @TypeOf() and @hasDecl() to
// perform duck typing ("if it walks like a duck and it quacks
// like a duck, then it must be a duck") to determine if the type
// is a "duck".
fn isADuck(possible_duck: anytype) bool {
// We'll use @hasDecl() to determine if the type has
// everything needed to be a "duck".
//
// In this example, 'has_increment' will be true if type Foo
// has an increment() method:
//
// const has_increment = @hasDecl(Foo, "increment");
//
// Please make sure MyType has both waddle() and quack()
// methods:
const MyType = @TypeOf(possible_duck);
const walks_like_duck = @hasDecl(MyType, "waddle");
const quacks_like_duck = @hasDecl(MyType, "quack");
const is_duck = walks_like_duck and quacks_like_duck;
if (is_duck) {
// We also call the quack() method here to prove that Zig
// allows us to perform duck actions on anything
// sufficiently duck-like.
//
// Because all of the checking and inference is performed
// at compile time, we still have complete type safety:
// attempting to call the quack() method on a struct that
// doesn't have it (like Duct) would result in a compile
// error, not a runtime panic or crash!
possible_duck.quack();
}
return is_duck;
}
Exe: 071_comptime6.zig
//
// There have been several instances where it would have been
// nice to use loops in our programs, but we couldn't because the
// things we were trying to do could only be done at compile
// time. We ended up having to do those things MANUALLY, like
// NORMAL people. Bah! We are PROGRAMMERS! The computer should be
// doing this work.
//
// An 'inline for' is performed at compile time, allowing you to
// programmatically loop through a series of items in situations
// like those mentioned above where a regular runtime 'for' loop
// wouldn't be allowed:
//
// inline for (.{ u8, u16, u32, u64 }) |T| {
// print("{} ", .{@typeInfo(T).int.bits});
// }
//
// In the above example, we're looping over a list of types,
// which are available only at compile time.
//
const print = @import("std").debug.print;
// Remember Narcissus from exercise 065 where we used builtins
// for reflection? He's back and loving it.
const Narcissus = struct {
me: *Narcissus = undefined,
myself: *Narcissus = undefined,
echo: void = undefined,
};
pub fn main() void {
print("Narcissus has room in his heart for:", .{});
// Last time we examined the Narcissus struct, we had to
// manually access each of the three fields. Our 'if'
// statement was repeated three times almost verbatim. Yuck!
//
// Please use an 'inline for' to implement the block below
// for each field in the slice 'fields'!
const fields = @typeInfo(Narcissus).@"struct".fields;
inline for (fields) |field| {
if (field.type != void) {
print(" {s}", .{field.name});
}
}
// Once you've got that, go back and take a look at exercise
// 065 and compare what you've written to the abomination we
// had there!
print(".n", .{});
}
Exe: 072_comptime7.zig
//
// There is also an 'inline while'. Just like 'inline for', it
// loops at compile time, allowing you to do all sorts of
// interesting things not possible at runtime. See if you can
// figure out what this rather bonkers example prints:
//
// const foo = [3]*const [5]u8{ "~{s}~", "<{s}>", "d{s}b" };
// comptime var i = 0;
//
// inline while ( i < foo.len ) : (i += 1) {
// print(foo[i] ++ "n", .{foo[i]});
// }
//
// You haven't taken off that wizard hat yet, have you?
//
const print = @import("std").debug.print;
pub fn main() void {
// Here is a string containing a series of arithmetic
// operations and single-digit decimal values. Let's call
// each operation and digit pair an "instruction".
const instructions = "+3 *5 -2 *2";
// Here is a u32 variable that will keep track of our current
// value in the program at runtime. It starts at 0, and we
// will get the final value by performing the sequence of
// instructions above.
var value: u32 = 0;
// This "index" variable will only be used at compile time in
// our loop.
comptime var i = 0;
// Here we wish to loop over each "instruction" in the string
// at compile time.
//
// Please fix this to loop once per "instruction":
inline while (i < instructions.len) : (i += 3) {
// This gets the digit from the "instruction". Can you
// figure out why we subtract '0' from it?
const digit = instructions[i + 1] - '0';
// This 'switch' statement contains the actual work done
// at runtime. At first, this doesn't seem exciting...
switch (instructions[i]) {
'+' => value += digit,
'-' => value -= digit,
'*' => value *= digit,
else => unreachable,
}
// ...But it's quite a bit more exciting than it first appears.
// The 'inline while' no longer exists at runtime and neither
// does anything else not touched directly by runtime
// code. The 'instructions' string, for example, does not
// appear anywhere in the compiled program because it's
// not used by it!
//
// So in a very real sense, this loop actually converts
// the instructions contained in a string into runtime
// code at compile time. Guess we're compiler writers
// now. See? The wizard hat was justified after all.
}
print("{}n", .{value});
}
Exe: 073_comptime8.zig
//
// As a matter of fact, you can put 'comptime' in front of any
// expression to force it to be run at compile time.
//
// Execute a function:
//
// comptime llama();
//
// Get a value:
//
// bar = comptime baz();
//
// Execute a whole block:
//
// comptime {
// bar = baz + biff();
// llama(bar);
// }
//
// Get a value from a block:
//
// var llama = comptime bar: {
// const baz = biff() + bonk();
// break :bar baz;
// }
//
const print = @import("std").debug.print;
const llama_count = 5;
const llamas = [llama_count]u32{ 5, 10, 15, 20, 25 };
pub fn main() void {
// We meant to fetch the last llama. Please fix this simple
// mistake so the assertion no longer fails.
const my_llama = getLlama(4);
print("My llama value is {}.n", .{my_llama});
}
fn getLlama(comptime i: usize) u32 {
// We've put a guard assert() at the top of this function to
// prevent mistakes. The 'comptime' keyword here means that
// the mistake will be caught when we compile!
//
// Without 'comptime', this would still work, but the
// assertion would fail at runtime with a PANIC, and that's
// not as nice.
//
// Unfortunately, we're going to get an error right now
// because the 'i' parameter needs to be guaranteed to be
// known at compile time. What can you do with the 'i'
// parameter above to make this so?
comptime assert(i < llama_count);
return llamas[i];
}
// Fun fact: this assert() function is identical to
// std.debug.assert() from the Zig Standard Library.
fn assert(ok: bool) void {
if (!ok) unreachable;
}
//
// Bonus fun fact: I accidentally replaced all instances of 'foo'
// with 'llama' in this exercise and I have no regrets!
Exe: 074_comptime9.zig
const print = @import("std").debug.print;
// We're going to (ab)use the power of Zig to make animal hybrid creatures!
// What do you think a GatorMouse would look like? Eek.
//
// Let's try a MouseLlama instead.
//
// We'll make a function that runs at comptime and takes a short code describing
// the desired creature. A Mouse is represented by "m" and a Llama is "lm".
// A MouseLlama hybrid, then, would be represented by "mlm".
const Animal = enum {
Mouse,
Llama,
Gator,
};
// makeCreature takes the count of animals making up the hybrid creature (so we
// know how big a pen we'll need) and a format string, like the "mlm" for
// MouseLlama.
fn makeCreature(comptime count: usize, comptime fmt: []const u8) [count]Animal {
// Since not every animal is represented by a single character, we need to
// track the state of things as we move along. For example, if we see an
// "m", is that a new Mouse or the end of a Llama?
const State = enum {
start, // Ready to start a new animal.
l, // This means we've seen an "l", so if we see an "m", we know it's a Llama.
};
comptime var state = State.start;
comptime var next_animal: usize = 0;
// We return an array of animals representing the creature. (This is why we
// really needed the 'count' parameter. Arrays need a size.)
var animals: [count]Animal = .{undefined} ** count;
inline for (fmt) |char| {
// This is a good spot to add a @compileLog() call if you need to debug
// any variables... (Come back here after you see main().)
switch (state) {
.start => switch (char) {
// We've seen the start of a Llama.
'l' => state = .l,
// Mice are smaller. An "m" is a full Mouse.
'm' => {
animals[next_animal] = .Mouse;
next_animal += 1;
},
// @compileError lets us stop the build immediately if something
// is wrong. It's like @compileLog but it prints a message
// instead of inspecting values.
//
// What do you think happens with Gators? Do they join with
// other animals or is this an error?
'g' => {
animals[next_animal] = .Gator;
next_animal += 1;
},
else => @compileError("No animal starts with '" ++ char ++ "'!"),
},
.l => switch (char) {
// We've seen the end of a Llama.
'm' => {
animals[next_animal] = .Llama;
next_animal += 1;
// Something is missing here. After we finish a Llama, we
// need to be ready to _start_ over with a new animal...
state = .start;
},
else => @compileError("Only llamas start with 'l'!"),
},
}
}
if (state != .start) {
@compileError("Oh no, an incomplete llama!");
}
if (next_animal != count) {
@compileError("Creature is missing an animal (format string too short).");
}
return animals;
}
pub fn main() void {
// Once you've fixed the ??? marks above, this makeCreature call will still
// only succeed if you move it outside of main, so it will run at comptime.
//
// With the call here, Zig will try to make the creature at runtime, and
// you'll get an interesting error.
//
// You may think the state got mixed up, but if you use @compileLog to check
// some variables in makeCreature, you'll see that Zig is trying to compare
// comptime values with "[runtime value]", which will never match.
//
// You can solve this by adding "comptime" to two of the variables in
// makeCreature...
const creature = makeCreature(2, "mlm");
for (creature) |animal| {
// @tagName gives us a string representing which variant of an enum we
// have. This lets us print the names of animals without repeating them
// here.
print("{s}", .{@tagName(animal)});
}
print(" joins the crew!", .{});
}
Exe: 075_quiz8.zig
//
// Quiz Time!
//
// Let's revisit the Hermit's Map from Quiz 7.
//
// Oh, don't worry, it's not nearly as big without all the
// explanatory comments. And we're only going to change one part
// of it.
//
const print = @import("std").debug.print;
const TripError = error{ Unreachable, EatenByAGrue };
const Place = struct {
name: []const u8,
paths: []const Path = undefined,
};
var a = Place{ .name = "Archer's Point" };
var b = Place{ .name = "Bridge" };
var c = Place{ .name = "Cottage" };
var d = Place{ .name = "Dogwood Grove" };
var e = Place{ .name = "East Pond" };
var f = Place{ .name = "Fox Pond" };
// Remember how we didn't have to declare the numeric type of the
// place_count because it is only used at compile time? That
// probably makes a lot more sense now. :-)
const place_count = 6;
const Path = struct {
from: *const Place,
to: *const Place,
dist: u8,
};
// Okay, so as you may recall, we had to create each Path struct
// by hand and each one took 5 lines of code to define:
//
// Path{
// .from = &a, // from: Archer's Point
// .to = &b, // to: Bridge
// .dist = 2,
// },
//
// Well, armed with the knowledge that we can run code at compile
// time, we can perhaps shorten this a bit with a simple function
// instead.
//
// Please fill in the body of this function!
fn makePath(from: *Place, to: *Place, dist: u8) Path {
return Path{
.from = from,
.to = to,
.dist = dist,
};
}
// Using our new function, these path definitions take up considerably less
// space in our program now!
const a_paths = [_]Path{makePath(&a, &b, 2)};
const b_paths = [_]Path{ makePath(&b, &a, 2), makePath(&b, &d, 1) };
const c_paths = [_]Path{ makePath(&c, &d, 3), makePath(&c, &e, 2) };
const d_paths = [_]Path{ makePath(&d, &b, 1), makePath(&d, &c, 3), makePath(&d, &f, 7) };
const e_paths = [_]Path{ makePath(&e, &c, 2), makePath(&e, &f, 1) };
const f_paths = [_]Path{makePath(&f, &d, 7)};
//
// But is it more readable? That could be argued either way.
//
// We've seen that it is possible to parse strings at compile
// time, so the sky's really the limit on how fancy we could get
// with this.
//
// For example, we could create our own "path language" and
// create Paths from that. Something like this, perhaps:
//
// a -> (b[2])
// b -> (a[2] d[1])
// c -> (d[3] e[2])
// ...
//
// Feel free to implement something like that as a SUPER BONUS EXERCISE!
const TripItem = union(enum) {
place: *const Place,
path: *const Path,
fn printMe(self: TripItem) void {
switch (self) {
.place => |p| print("{s}", .{p.name}),
.path => |p| print("--{}->", .{p.dist}),
}
}
};
const NotebookEntry = struct {
place: *const Place,
coming_from: ?*const Place,
via_path: ?*const Path,
dist_to_reach: u16,
};
const HermitsNotebook = struct {
entries: [place_count]?NotebookEntry = .{null} ** place_count,
next_entry: u8 = 0,
end_of_entries: u8 = 0,
fn getEntry(self: *HermitsNotebook, place: *const Place) ?*NotebookEntry {
for (&self.entries, 0..) |*entry, i| {
if (i >= self.end_of_entries) break;
if (place == entry.*.?.place) return &entry.*.?;
}
return null;
}
fn checkNote(self: *HermitsNotebook, note: NotebookEntry) void {
const existing_entry = self.getEntry(note.place);
if (existing_entry == null) {
self.entries[self.end_of_entries] = note;
self.end_of_entries += 1;
} else if (note.dist_to_reach < existing_entry.?.dist_to_reach) {
existing_entry.?.* = note;
}
}
fn hasNextEntry(self: *HermitsNotebook) bool {
return self.next_entry < self.end_of_entries;
}
fn getNextEntry(self: *HermitsNotebook) *const NotebookEntry {
defer self.next_entry += 1;
return &self.entries[self.next_entry].?;
}
fn getTripTo(self: *HermitsNotebook, trip: []?TripItem, dest: *Place) TripError!void {
const destination_entry = self.getEntry(dest);
if (destination_entry == null) {
return TripError.Unreachable;
}
var current_entry = destination_entry.?;
var i: u8 = 0;
while (true) : (i += 2) {
trip[i] = TripItem{ .place = current_entry.place };
if (current_entry.coming_from == null) break;
trip[i + 1] = TripItem{ .path = current_entry.via_path.? };
const previous_entry = self.getEntry(current_entry.coming_from.?);
if (previous_entry == null) return TripError.EatenByAGrue;
current_entry = previous_entry.?;
}
}
};
pub fn main() void {
const start = &a; // Archer's Point
const destination = &f; // Fox Pond
// We could either have this:
//
// a.paths = a_paths[0..];
// b.paths = b_paths[0..];
// c.paths = c_paths[0..];
// d.paths = d_paths[0..];
// e.paths = e_paths[0..];
// f.paths = f_paths[0..];
//
// or this comptime wizardry:
//
const letters = [_][]const u8{ "a", "b", "c", "d", "e", "f" };
inline for (letters) |letter| {
@field(@This(), letter).paths = @field(@This(), letter ++ "_paths")[0..];
}
var notebook = HermitsNotebook{};
var working_note = NotebookEntry{
.place = start,
.coming_from = null,
.via_path = null,
.dist_to_reach = 0,
};
notebook.checkNote(working_note);
while (notebook.hasNextEntry()) {
const place_entry = notebook.getNextEntry();
for (place_entry.place.paths) |*path| {
working_note = NotebookEntry{
.place = path.to,
.coming_from = place_entry.place,
.via_path = path,
.dist_to_reach = place_entry.dist_to_reach + path.dist,
};
notebook.checkNote(working_note);
}
}
var trip = [_]?TripItem{null} ** (place_count * 2);
notebook.getTripTo(trip[0..], destination) catch |err| {
print("Oh no! {}n", .{err});
return;
};
printTrip(trip[0..]);
}
fn printTrip(trip: []?TripItem) void {
var i: u8 = @intCast(trip.len);
while (i > 0) {
i -= 1;
if (trip[i] == null) continue;
trip[i].?.printMe();
}
print("n", .{});
}
Exe: 076_sentinels.zig
//
// A sentinel value indicates the end of data. Let's imagine a
// sequence of lowercase letters where uppercase 'S' is the
// sentinel, indicating the end of the sequence:
//
// abcdefS
//
// If our sequence also allows for uppercase letters, 'S' would
// make a terrible sentinel since it could no longer be a regular
// value in the sequence:
//
// abcdQRST
// ^-- Oops! The last letter in the sequence is R!
//
// A popular choice for indicating the end of a string is the
// value 0. ASCII and Unicode call this the "Null Character".
//
// Zig supports sentinel-terminated arrays, slices, and pointers:
//
// const a: [4:0]u32 = [4:0]u32{1, 2, 3, 4};
// const b: [:0]const u32 = &[4:0]u32{1, 2, 3, 4};
// const c: [*:0]const u32 = &[4:0]u32{1, 2, 3, 4};
//
// Array 'a' stores 5 u32 values, the last of which is 0.
// However the compiler takes care of this housekeeping detail
// for you. You can treat 'a' as a normal array with just 4
// items.
//
// Slice 'b' is only allowed to point to zero-terminated arrays
// but otherwise works just like a normal slice.
//
// Pointer 'c' is exactly like the many-item pointers we learned
// about in exercise 054, but it is guaranteed to end in 0.
// Because of this guarantee, we can safely find the end of this
// many-item pointer without knowing its length. (We CAN'T do
// that with regular many-item pointers!).
//
// Important: the sentinel value must be of the same type as the
// data being terminated!
//
const print = @import("std").debug.print;
const sentinel = @import("std").meta.sentinel;
pub fn main() void {
// Here's a zero-terminated array of u32 values:
var nums = [_:0]u32{ 1, 2, 3, 4, 5, 6 };
// And here's a zero-terminated many-item pointer:
const ptr: [*:0]u32 = &nums;
// For fun, let's replace the value at position 3 with the
// sentinel value 0. This seems kind of naughty.
nums[3] = 0;
// So now we have a zero-terminated array and a many-item
// pointer that reference the same data: a sequence of
// numbers that both ends in and CONTAINS the sentinel value.
//
// Attempting to loop through and print both of these should
// demonstrate how they are similar and different.
//
// (It turns out that the array prints completely, including
// the sentinel 0 in the middle. The many-item pointer stops
// at the first sentinel value.)
printSequence(nums);
printSequence(ptr);
print("n", .{});
}
// Here's our generic sequence printing function. It's nearly
// complete, but there are a couple of missing bits. Please fix
// them!
fn printSequence(my_seq: anytype) void {
const my_typeinfo = @typeInfo(@TypeOf(my_seq));
// The TypeInfo contained in my_typeinfo is a union. We use
// a switch to handle printing the Array or Pointer fields,
// depending on which type of my_seq was passed in:
switch (my_typeinfo) {
.array => {
print("Array:", .{});
// Loop through the items in my_seq.
for (my_seq) |s| {
print("{}", .{s});
}
},
.pointer => {
// Check this out - it's pretty cool:
const my_sentinel = sentinel(@TypeOf(my_seq));
print("Many-item pointer:", .{});
// Loop through the items in my_seq until we hit the
// sentinel value.
var i: usize = 0;
while (my_seq[i] != my_sentinel) {
print("{}", .{my_seq[i]});
i += 1;
}
},
else => unreachable,
}
print(". ", .{});
}
Exe: 077_sentinels2.zig
//
// ------------------------------------------------------------
// TOP SECRET TOP SECRET TOP SECRET TOP SECRET TOP SECRET
// ------------------------------------------------------------
//
// Are you ready for the THE TRUTH about Zig string literals?
//
// Here it is:
//
// @TypeOf("foo") == *const [3:0]u8
//
// Which means a string literal is a "constant pointer to a
// zero-terminated (null-terminated) fixed-size array of u8".
//
// Now you know. You've earned it. Welcome to the secret club!
//
// ------------------------------------------------------------
//
// Why do we bother using a zero/null sentinel to terminate
// strings in Zig when we already have a known length?
//
// Versatility! Zig strings are compatible with C strings (which
// are null-terminated) AND can be coerced to a variety of other
// Zig types:
//
// const a: [5]u8 = "array".*;
// const b: *const [16]u8 = "pointer to array";
// const c: []const u8 = "slice";
// const d: [:0]const u8 = "slice with sentinel";
// const e: [*:0]const u8 = "many-item pointer with sentinel";
// const f: [*]const u8 = "many-item pointer";
//
// All but 'f' may be printed. (A many-item pointer without a
// sentinel is not safe to print because we don't know where it
// ends!)
//
const print = @import("std").debug.print;
const WeirdContainer = struct {
data: [*]const u8,
length: usize,
};
pub fn main() void {
// WeirdContainer is an awkward way to house a string.
//
// Being a many-item pointer (with no sentinel termination),
// the 'data' field "loses" the length information AND the
// sentinel termination of the string literal "Weird Data!".
//
// Luckily, the 'length' field makes it possible to still
// work with this value.
const foo = WeirdContainer{
.data = "Weird Data!",
.length = 11,
};
// How do we get a printable value from 'foo'? One way is to
// turn it into something with a known length. We do have a
// length... You've actually solved this problem before!
//
// Here's a big hint: do you remember how to take a slice?
const printable = foo.data[0..foo.length];
print("{s}n", .{printable});
}
Exe: 078_sentinels3.zig
//
// We were able to get a printable string out of a many-item
// pointer by using a slice to assert a specific length.
//
// But can we ever GO BACK to a sentinel-terminated pointer
// after we've "lost" the sentinel in a coercion?
//
// Yes, we can. Zig's @ptrCast() builtin can do this. Check out
// the signature:
//
// @ptrCast(value: anytype) anytype
//
// See if you can use it to solve the same many-item pointer
// problem, but without needing a length!
//
const print = @import("std").debug.print;
pub fn main() void {
// Again, we've coerced the sentinel-terminated string to a
// many-item pointer, which has no length or sentinel.
const data: [*]const u8 = "Weird Data!";
// Please cast 'data' to 'printable':
const printable: [*:0]const u8 = @ptrCast(data);
print("{s}n", .{printable});
}
Exe: 079_quoted_identifiers.zig
//
// Sometimes you need to create an identifier that will not, for
// whatever reason, play by the naming rules:
//
// const 55_cows: i32 = 55; // ILLEGAL: starts with a number
// const isn't true: bool = false; // ILLEGAL: what even?!
//
// If you try to create either of these under normal
// circumstances, a special Program Identifier Syntax Security
// Team (PISST) will come to your house and take you away.
//
// Thankfully, Zig has a way to sneak these wacky identifiers
// past the authorities: the @"" identifier quoting syntax.
//
// @"foo"
//
// Please help us safely smuggle these fugitive identifiers into
// our program:
//
const print = @import("std").debug.print;
pub fn main() void {
const @"55_cows": i32 = 55;
const @"isn't true": bool = false;
print("Sweet freedom: {}, {}.n", .{
@"55_cows",
@"isn't true",
});
}
Exe: 080_anonymous_structs.zig
//
// Struct types are always "anonymous" until we give them a name:
//
// struct {};
//
// So far, we've been giving struct types a name like so:
//
// const Foo = struct {};
//
// * The value of @typeName(Foo) is "<filename>.Foo".
//
// A struct is also given a name when you return it from a
// function:
//
// fn Bar() type {
// return struct {};
// }
//
// const MyBar = Bar(); // store the struct type
// const bar = Bar() {}; // create instance of the struct
//
// * The value of @typeName(Bar()) is "<filename>.Bar()".
// * The value of @typeName(MyBar) is "<filename>.Bar()".
// * The value of @typeName(@TypeOf(bar)) is "<filename>.Bar()".
//
// You can also have completely anonymous structs. The value
// of @typeName(struct {}) is "<filename>.<function>__struct_<nnn>".
//
const print = @import("std").debug.print;
// This function creates a generic data structure by returning an
// anonymous struct type (which will no longer be anonymous AFTER
// it's returned from the function).
fn Circle(comptime T: type) type {
return struct {
center_x: T,
center_y: T,
radius: T,
};
}
pub fn main() void {
//
// See if you can complete these two variable initialization
// expressions to create instances of circle struct types
// which can hold these values:
//
// * circle1 should hold i32 integers
// * circle2 should hold f32 floats
//
const circle1 = Circle(i32){
.center_x = 25,
.center_y = 70,
.radius = 15,
};
const circle2 = Circle(f32){
.center_x = 25.234,
.center_y = 70.999,
.radius = 15.714,
};
print("[{s}: {},{},{}] ", .{
stripFname(@typeName(@TypeOf(circle1))),
circle1.center_x,
circle1.center_y,
circle1.radius,
});
print("[{s}: {d:.1},{d:.1},{d:.1}]n", .{
stripFname(@typeName(@TypeOf(circle2))),
circle2.center_x,
circle2.center_y,
circle2.radius,
});
}
// Perhaps you remember the "narcissistic fix" for the type name
// in Ex. 065? We're going to do the same thing here: use a hard-
// coded slice to return the type name. That's just so our output
// looks prettier. Indulge your vanity. Programmers are beautiful.
fn stripFname(mytype: []const u8) []const u8 {
return mytype[22..];
}
// The above would be an instant red flag in a "real" program.
Exe: 081_anonymous_structs2.zig
//
// An anonymous struct value LITERAL (not to be confused with a
// struct TYPE) uses '.{}' syntax:
//
// .{
// .center_x = 15,
// .center_y = 12,
// .radius = 6,
// }
//
// These literals are always evaluated entirely at compile-time.
// The example above could be coerced into the i32 variant of the
// "circle struct" from the last exercise.
//
// Or you can let them remain entirely anonymous as in this
// example:
//
// fn bar(foo: anytype) void {
// print("a:{} b:{}n", .{foo.a, foo.b});
// }
//
// bar(.{
// .a = true,
// .b = false,
// });
//
// The example above prints "a:true b:false".
//
const print = @import("std").debug.print;
pub fn main() void {
printCircle(.{
.center_x = @as(u32, 205),
.center_y = @as(u32, 187),
.radius = @as(u32, 12),
});
}
// Please complete this function which prints an anonymous struct
// representing a circle.
fn printCircle(circle: anytype) void {
print("x:{} y:{} radius:{}n", .{
circle.center_x,
circle.center_y,
circle.radius,
});
}
Exe: 082_anonymous_structs3.zig
//
// You can even create anonymous struct literals without field
// names:
//
// .{
// false,
// @as(u32, 15),
// @as(f64, 67.12)
// }
//
// We call these "tuples", which is a term used by many
// programming languages for a data type with fields referenced
// by index order rather than name. To make this possible, the Zig
// compiler automatically assigns numeric field names 0, 1, 2,
// etc. to the struct.
//
// Since bare numbers are not legal identifiers (foo.0 is a
// syntax error), we have to quote them with the @"" syntax.
// Example:
//
// const foo = .{ true, false };
//
// print("{} {}n", .{foo.@"0", foo.@"1"});
//
// The example above prints "true false".
//
// Hey, WAIT A SECOND...
//
// If a .{} thing is what the print function wants, do we need to
// break our "tuple" apart and put it in another one? No! It's
// redundant! This will print the same thing:
//
// print("{} {}n", foo);
//
// Aha! So now we know that print() takes a "tuple". Things are
// really starting to come together now.
//
const print = @import("std").debug.print;
pub fn main() void {
// A "tuple":
const foo = .{
true,
false,
@as(i32, 42),
@as(f32, 3.141592),
};
// We'll be implementing this:
printTuple(foo);
// This is just for fun, because we can:
const nothing = .{};
print("n", nothing);
}
// Let's make our own generic "tuple" printer. This should take a
// "tuple" and print out each field in the following format:
//
// "name"(type):value
//
// Example:
//
// "0"(bool):true
//
// You'll be putting this together. But don't worry, everything
// you need is documented in the comments.
fn printTuple(tuple: anytype) void {
// 1. Get a list of fields in the input 'tuple'
// parameter. You'll need:
//
// @TypeOf() - takes a value, returns its type.
//
// @typeInfo() - takes a type, returns a TypeInfo union
// with fields specific to that type.
//
// The list of a struct type's fields can be found in
// TypeInfo's @"struct".fields.
//
// Example:
//
// @typeInfo(Circle).@"struct".fields
//
// This will be an array of StructFields.
const fields = @typeInfo(@TypeOf(tuple)).@"struct".fields;
// 2. Loop through each field. This must be done at compile
// time.
//
// Hint: remember 'inline' loops?
//
inline for (fields) |field| {
// 3. Print the field's name, type, and value.
//
// Each 'field' in this loop is one of these:
//
// pub const StructField = struct {
// name: [:0]const u8,
// type: type,
// default_value_ptr: ?*const anyopaque,
// is_comptime: bool,
// alignment: comptime_int,
// };
//
// Note we will learn about 'anyopaque' type later
//
// You'll need this builtin:
//
// @field(lhs: anytype, comptime field_name: []const u8)
//
// The first parameter is the value to be accessed,
// the second parameter is a string with the name of
// the field you wish to access. The value of the
// field is returned.
//
// Example:
//
// @field(foo, "x"); // returns the value at foo.x
//
// The first field should print as: "0"(bool):true
//
// Hint: Be careful! If your 'lhs' is a type, @field() looks
// for declarations. If it's a value, it looks for data.
//
print(""{s}"({any}):{any} ", .{
field.name,
field.type,
@field(tuple, field.name),
});
}
}
Exe: 083_anonymous_lists.zig
//
// Anonymous struct literal syntax can also be used to compose an
// "anonymous list" with an array type destination:
//
// const foo: [3]u32 = .{10, 20, 30};
//
// Otherwise it's a "tuple":
//
// const bar = .{10, 20, 30};
//
// The only difference is the destination type.
//
const print = @import("std").debug.print;
pub fn main() void {
// Please make 'hello' a string-like array of u8 WITHOUT
// changing the value literal.
//
// Don't change this part:
//
// = .{ 'h', 'e', 'l', 'l', 'o' };
//
const hello: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' };
print("I say {s}!n", .{hello});
}
Exe: 084_interfaces.zig
//
// 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("=== Doctor Zoraptera's Insect Report ===n", .{});
for (my_insects) |insect| {
// Almost done! We want to print() each insect with a
// single method call here.
insect.print();
}
}
// 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.
Exe: 085_async.zig
//
// 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 - 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:
//
// 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.io;
// 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 epochn", .{timestamp.toSeconds()});
}
Exe: 086_async2.zig
//
// 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 });
// 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.
//
// IMPORTANT: Every Future MUST be either .await()ed or .cancel()ed.
// Failing to do so leaks resources! A safe pattern is:
//
// 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.
var future = io.async(computeAnswer, .{ 6, 7 });
defer _ = future.cancel(io); // always clean up!
print("Computing... ", .{});
// Now collect the result. What method on Future gives us
// the value, blocking until it's ready?
const answer = future.await(io);
print("The answer is: {}n", .{answer});
}
fn computeAnswer(a: u32, b: u32) u32 {
return a * b;
}
Exe: 087_async3.zig
//
// 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, .{});
// defer _ = f1.cancel(io);
// var f2 = io.async(taskB, .{});
// defer _ = f2.cancel(io);
// const a = f1.await(io);
// const b = f2.await(io);
//
// 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.
//
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, .{ 1, 2 });
defer _ = future_a.cancel(io);
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.await(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;
}
Exe: 088_async4.zig
//
// 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 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
// 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.await(io);
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});
}
Exe: 089_async5.zig
//
// 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. BLOCKS until the task actually finishes
// 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, // error handle
// };
// return 42;
// }
//
// 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.
//
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});
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 {};
print("Canceling slow task...n", .{});
// We don't want to wait 10 seconds!
// Which Future method requests cancellation AND returns the result?
const result = future.cancel(io);
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;
}
Exe: 090_async6.zig
//
// 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
//
// 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().
//
// 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 = try sel.await();
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...";
}
Exe: 091_async7.zig
//
// 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.lock(io) catch return;
defer state.mutex.unlock(io); // <-- what's missing here?
state.counter += 1;
}
}
Exe: 092_async8.zig
//
// 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.putOne(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});
}
Exe: 093_async9.zig
//
// 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 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.
// * Can fail with error.ConcurrencyUnavailable if resources
// are exhausted or the backend doesn't support it.
// * 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);
//
// Let's try a slightly simplified example from signal processing:
// Suppose we're looking for the beginning of a signal above the noise
// level. To do this, we compare each entry from beginning to end with
// the threshold. To speed things up a bit, we split the signal into
// two halves and have two parallel workers search for them.
// Who finds the beginning first "wins" and thus ends the other one.
//
// As I said, this is a simplified explanation,
// but in practice it's done more or less like this.
//
const std = @import("std");
const Io = std.Io;
const print = std.debug.print;
const SearchResult = struct {
found: bool,
worker_id: u8 = 0,
index: usize = 0,
};
pub fn main(init: std.process.Init) !void {
const io = init.io;
const data = [_]u32{ 10, 23, 45, 67, 12, 69, 3, 54, 69, 42, 68, 56, 71, 79, 79, 75, 70, 77 };
const threshold = 70;
const mid = data.len / 2;
// A queue with space for one result.
var buf: [1]SearchResult = undefined;
var queue = Io.Queue(SearchResult).init(&buf);
// Launch two workers, each searching half the array.
// Remember, we want them to be guaranteed separate units of concurrency.
var f1 = try io.concurrent(searchThreshold, .{ io, data[0..mid], threshold, 0, 0, &queue });
defer _ = f1.cancel(io);
var f2 = try io.concurrent(searchThreshold, .{ io, data[mid..], threshold, mid, 1, &queue });
defer _ = f2.cancel(io);
// Wait for the first result.
const result = try queue.getOne(io);
if (result.found)
print("Worker {} found signal start over threshold at index {}!n", .{ result.worker_id, result.index });
}
fn searchThreshold(
io: Io,
slice: []const u32,
threshold: u32,
base_offset: usize,
worker_id: u8,
queue: *Io.Queue(SearchResult),
) void {
for (slice, 0..) |val, i| {
// This pause is necessary so that the process can be canceled
// if another one has already finished. Without this pause,
// all workers would continue until the end.
io.sleep(Io.Duration.fromMilliseconds(1), .awake) catch return;
// To test this, you can uncomment this to view the work of the workers
// and then comment out the pause.
// print("id: {} - val: {}n", .{ worker_id, val });
if (val >= threshold) {
queue.putOne(io, .{
.found = true,
.worker_id = worker_id,
.index = base_offset + i,
}) catch return;
return;
}
}
// Nothing found
queue.putOneUncancelable(io, .{ .found = false }) catch return;
}
Exe: 094_async10.zig
//
// In exercise 089, 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});
defer _ = future.cancel(io);
// Give the task time to start and enter its critical section.
io.sleep(std.Io.Duration.fromMilliseconds(200), .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.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(old);
// This sleep will NOT return error.Canceled even though
// we get canceled during it — protection is active!
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!";
},
};
print("Critical section completed safely.n", .{});
return "All data saved.";
}
Exe: 095_quiz_async.zig
//
// 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 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 085-094. *
// * 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;
defer self.mutex.unlock(io);
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);
// The collector must run concurrently so it can process
// readings while the sensors are still sending.
// Start it FIRST to ensure its concurrency unit is reserved.
//
// Bug 2: The collector needs guaranteed concurrency.
// What method ensures a separate unit of concurrency?
// (Don't forget: it can fail!)
var collector_future = try io.concurrent(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;
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);
// All sensors done — close the queue so the collector knows
// there's no more data coming.
queue.close(io);
// Bug 4: How do we wait for the collector to drain the remaining queue?
_ = collector_future.await(io);
// Now write the garden report. This is critical — it must
// NOT be interrupted, even if something tries to cancel us!
//
// Bug 5: Protect this section from cancellation.
// What Io method swaps the cancel protection state?
const old_protection = io.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(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 6: Send the reading into the queue.
// What Queue method sends a single element?
queue.putOne(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 : {}Cn", .{weather.temperature});
print("Humidity : {}%n", .{weather.humidity});
print("Wind : {} km/hn", .{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() - 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
// 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 — 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.
Exe: 096_hello_c.zig
//
// 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 = 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});
}
//
// 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".
//
Exe: 097_c_math.zig
//
// 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?
@cInclude("math.h");
});
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 });
}
Exe: 098_for3.zig
//
// 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 (1..21) |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 (1..16) |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'.
Exe: 099_memory_allocation.zig
//
// 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 = try allocator.alloc(f64, arr.len);
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
Exe: 100_bit_manipulation.zig
//
// 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?
x ^= y;
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.
Exe: 101_bit_manipulation2.zig
//
// 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 == 0x03FFFFFF;
}
Exe: 102_formatting.zig
//
// 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 print() function are the only
// way to definitively learn how to format strings in Zig:
//
// https://ziglang.org/documentation/master/std/#std.Io.Writer.print
//
// 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. "nn" 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 print() 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("{d:>3} ", .{(a + 1) * (b + 1)});
}
// After each row we use double line feed:
print("nn", .{});
}
}
Exe: 103_for4.zig
//
// 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, dec_nums) |hn, dn| {
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.
Exe: 104_for5.zig
//
// 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, 1..) |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
//
Exe: 105_testing.zig
//
// 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, 4) == 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.DivisionByZero, divide(15, 0));
}
Exe: 106_tokenization.zig
//
// 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, "n ,;!");
// 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});
}
Exe: 107_threading.zig
//
// 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, .{2}); // that can't be right?
defer handle2.join();
// Third thread
const handle3 = try std.Thread.spawn(.{}, thread_function, .{3});
defer handle3.join(); // <-- 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: isize) !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 - @rem(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.
Exe: 108_threading2.zig
//
// 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.
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});
}
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.
//
// If count = 10_000_000_000_000 you should see the following:
// 3.141592653589
Exe: 109_files.zig
//
// 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
error.PathAlreadyExists => {},
// 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...
defer 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 .outputzigling.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?
Exe: 110_files2.zig
//
// 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 = [_]u8{'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 = 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[0..bytes_read], // change this line only
});
}
Exe: 111_labeled_switch.zig
//
// 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 reachedn", .{});
// }
//
// 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 reachedn", .{});
// }
//
// 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", .{});
}
Exe: 112_vectors.zig
// 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});
}
Exe: 113_quiz9.zig
// ----------------------------------------------------------------------------
// 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 PORTBn", .{});
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 PORTBn", .{});
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 PORTBn", .{});
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 bitn", .{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 bitn", .{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();
}
Exe: 114_packed.zig
//
// 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
Exe: 115_packed2.zig
//
// 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");
Exe: 999_the_end.zig
// This is the end for now!
// More exercises will follow...
const print = @import("std").debug.print;
pub fn main() void {
print("nThis is the end for now!nWe hope you had fun and were able to learn a lot, so visit us again when the next exercises are available.n", .{});
}