Submind YouTube summaries
Thumbnail for Zig Intro - Code Examples (Ziglings)

Zig Intro - Code Examples (Ziglings)

Watch on YouTube

Video summary

Bu video, Zig programlama dilinin temel özelliklerini ve benzersiz yeteneklerini, "Ziglings" projesindeki kırık kod örneklerinin düzeltilmiş hallerini inceleyerek tanıtmaktadır. İzleyicilerin C, Rust veya Go gibi dillerle ilgili bir arka planı olduğu varsayılmakta olup, video Zig dilinin standart modülünü nasıl içe aktardığını ve `main` fonksiyonunun yapısını açıklamaya başlar. İlk örneklerde değişken tanımları, sabit sayılar (örneğin pi sayısı) ve debug modülü üzerinden çıktı alma gibi temel işlemler gösterilirken; Zig'in parametre varsayılan değerleri desteklememesi nedeniyle boş yapı literallerinin bile fonksiyon çağrılarında zorunlu olduğu vurgulanır. Ayrıca video, dizi kopyalama yerine `double plus` ve `double asterisk` operatörlerini kullanarak dizileri birleştirme veya tekrar etme yöntemlerini anlatarak dilin bellek yönetimi açısından sunduğu esneklikten bahseder. Video ilerledikçe Zig'in kontrol akışı yapılarını ele alırken, C'deki ternary operatöre benzer şekilde `if` ifadesinin nasıl kullanılabildiği ve döngülerde post-iterasyon ifadelerinin (`while`) işlevi açıklanır. Özellikle hata yönetimi üzerine yoğunlaşarak "error set" türlerinin küresel olarak eşleşen isimlere sahip olması, bu hataların `error union` ile birleştirilmesi ve `catch`, `try` gibi operatörlerin nasıl çalıştığı detaylandırılır. Bu bölümde ayrıca `defer` ifadesinin fonksiyonun çıkışında ertelenmiş işlemlerin (örn. temizlik kodu) otomatik olarak çalıştırılmasını sağladığı, bununla birlikte hata durumunda çalışan "error defer" mekanizması da örneklerle somutlaştırılır. Sonuç bölümünde ise Zig'in derleme zamanı yetenekleri ve ileri düzey yapıları öne çıkarılarak; `comptime` değişkenlerinin derleme sırasında değerlendirilmesi, many-item pointer'ların indekslenmesi ve birleşik türlerin (union) nasıl işlendiği anlatılır. Video ayrıca switch ifadelerinin C'den farklı olarak düşüş yapmaması (`break` gerektirmemesi), yapı içindeki fonksiyon üyelerinin çağrılma biçimleri, opsiyonel tiplerin kullanımı ve hatta C kodunun Zig ile nasıl içe aktarılması gibi konulara değinerek dilin hem güvenliğini hem de esnekliğini bir arada sunan kapsamlı bir ekosistem olduğunu özetler. İzleyicilere bu hızlı taramadan ziyade, kendi başlarına Ziglings alıştırmalarını çözerek dili daha derinden öğrenmeleri tavsiye edilirken; video sonunda thread yönetimi ve test blokları gibi konularla dilin modern programlama ihtiyaçlarını karşılamaya yönelik araç setini tamamlar.
Read the full video transcript
This video is going to introduce the Zig programming language by walking through small code exercises from the Ziglings project. We'll assume the viewer has reasonable familiarity with C or other similar languages such as C++, Rust, Go, or Odin. If you're new to this kind of programming, you may want to first check out my intro to Odin video series linked below. The Ziglings exercises present broken code examples that need fixes to pass their tests, but here we'll present just completed solutions rather than focus on the particular problems being solved and the logic of their solutions, the video commentary and the code comments focus just on the Zig language features introduced by each exercise. Also, understand that we won't cover every single Ziglings exercise. Some exercises are skipped because they are redundant and several others are skipped because they cover async, a feature that isn't yet available in the main Zig compiler. Also, we will skip over most of the exercises that focus on usage of the standard libraries such as reading and writing files. I strongly recommend at some point working through the Ziglings exercises yourself, which generally takes several hours or more. Hopefully, this video gives you an easy quick survey of Zig's unique features, but you'll almost certainly understand and retain them far better after you get your hands dirty. Lastly, before getting started, this video will probably feel like a big list of facts even more so than other videos. So, it might be difficult to maintain full attention if you attempt to watch it in just one sitting. Instead, you'll probably want to watch it in chunks. So, let's start with exercise three. First, at the top, we're importing the standard module with the import function. The function returns the module as a struct value, which we assign to a constant we name STD. By the way, the at symbol indicates that this is a built-in function. Then, we have the definition of the main function, which is marked as public and returns void, meaning it returns nothing. In the function, we're first declaring a local variable n with type U8, meaning an 8-bit unsigned integer. We also declare a local constant named pi with type U32, meaning a 32-bit unsigned integer, and another local constant named negative 11 with type I8, meaning an 8-bit signed integer. The last line calls the print function, which is a member of the debug module, which itself is included as a member of the standard module. So, we can access it through the STD constant we created at the top of the file. The print function takes two arguments, a string and a value of any struct type. In this case, we're passing an anonymous struct denoted by the dot before the opening curly brace. Inside the curly braces, we have three values. And because these values are not given member names, they are treated like positional values in the struct for indexes 0, 1, and 2. What happens in the print function then is that introspection is used to get the members of the struct, and then the values of these members are interpolated into the string replacing the curly braces. So, this print call effectively outputs the value of n, then a space, then the value of pi, then another space, negative 11, and lastly a new line. Next, let's look at exercise five. After importing the standard module like before, we're also aliasing the assert function to a constant in this file. Inside main, we are creating two arrays of U8 values. The square brackets of an array literal contain the size of the array, though for these two arrays, we're using an underscore to indicate that the size should be inferred from the number of elements in the curly braces. So, both of these arrays have a size of two. In the next line, the double plus operator is used to concatenate the two arrays into a new array of length four. The double asterisk operator concatenates multiple instances of its left operand the number of times specified by its right operand. So, in this case, three instances of the U8 array are concatenated together creating an array of 12 U8s. A few lines down, a for loop iterates over every element of the array assigning the element to a variable n in each iteration. Lastly, you may have noticed that many of the print calls in this example pass an empty anonymous struct literal. Despite being empty, the struct is still necessary because Zig does not support default parameter values or variadic functions. So, the print function must always have a struct argument. In exercise six, a string is assigned to a local constant, and then when the index operator is used on the string, we get back a U8. So, this assigns the U8 value at index four of the string to the local constant d. Like with arrays, we can use the double plus and double asterisk operators to concatenate and repeat strings. In the print call at the end, the U and S in the curly braces indicate how the values should be formatted. A U indicates unsigned, and an S indicates string. In exercise seven, the double backslash syntax indicates the start of a multi-line string literal. A multi-line literal runs to the end of the line, and any successive lines beginning with double backslash become part of the same multi-line string. So, this example has one multi-line string literal spread across three lines. In exercise 10, an if is used as an expression rather than a statement. This is basically equivalent to the ternary operator in C. If the condition evaluates true, then the first expression is evaluated. Otherwise, if false, only the second is evaluated. So, here because discount is true, price will be assigned 17. In exercise 12, a while loop is given a post-iteration expression denoted by the colon. This while loop will iterate as long as n is less than 1,000, and after each iteration, n is multiplied by two. In exercise 16, the for loop here iterates over both an array plus a range in tandem. The range denoted by the double dots starts at zero, and the end of the range is left inferred from context. Because the length of the array and range must match, the end of the range is inferred to be the length of the array. Also, inside the loop here, the built-in function int cast is used to cast from one integer type to another. The target integer type returned by int cast is inferred from the calling context. So, here because the assignment target is a U32, this call returns a U32. In exercise 21, an error set type is defined and assigned to the constant my number error. Like an enum, an error set is composed of named members, but the names of error set members are mapped to globally unique IDs such that, say, the name foo in one error set is considered equivalent to the name foo from any other error set, which is not the case with enums. A foo member of one enum would be totally different from a foo member of a different enum. The number fail function defined in this exercise returns the my number error type, meaning it must return one of the members of the error set. In the next exercise 22, the type of the first local variable is an error union as denoted by the exclamation mark. An error union combines an error set on the left and a so-called payload type on the right, which can be any kind of type. In this case, the union is between my number error as the error set and U8 as the payload. What this means is that the variable can be assigned any value of either type, either any member of the my number error error union or any U8 value. So, in initialization, we assign the variable the U8 value five, but in the next line, we assign the variable the my number error value too small. In exercise 23, the catch operator is used, which takes an error union value on the left and a value of its payload type on the right. If the error union value is a value of its error set, then the catch evaluates and returns the right operand. Otherwise, if the error union is a value of its payload type, the catch directly returns the left payload without evaluating the right operand. In this example, the left operand is a call to a function add 20, which returns the error union of my number error and U32. The first call, which is passed 44, will return 64, and so the catch directly returns this value. In the next line, though, the add 20 call is passed four, in which case it returns a too small error, and so the catch evaluates and returns the right operand expression, which is the value 22. In exercise 24, you can ignore most of the code. The part to focus on here is that the catch operator can capture the error value from its left operand to be used in its right operand. Looking at the make just right function, if the left operand of the catch evaluates into an error set value, that value is captured as variable ERR in the right operand, and then this error is returned by the catch. In the fix too big and fix too small functions, again, errors are captured by the catch operations, but in these cases, the right operands are block statements denoted by curly braces. Be clear that any return statement in a block returns from the whole function, not just the block. Though in a later exercise, we'll learn a way to return a value from just the scope of a block. In exercise 25, we see a try operation which is simply a shorthand for a catch operation that captures and returns the error. In the add five function, if the call to detect returns an error, the try immediately returns that error. This is the same as if we used a catch operation that captured the error from the detect call and used the return statement to return the error. In exercise 27, a defer statement is used to defer evaluation of an expression. Here the first call to print is deferred, meaning that it won't execute until execution leaves this scope. In this case, the defer is in the top-level scope of the function, so when execution leaves the function, the deferred print will be executed. Effectively here, the string apple is printed before the string banana. In exercise 29 an error defer statement is used, which defers an expression, but the expression is only evaluated if an error is being returned. In the make number function here, a print statement is error deferred, so it only executes if an error is returned from the function. In exercise 30, we use a switch statement to switch on the value of a U8. Unlike C, a switch case does not fall through to the next, so we don't put a break statement in each case. The default case is denoted by the reserved word else. Exercise 31 demonstrates a switch used as an expression. The switch evaluates into the expression of the executed case. In exercise 32, an unreachable statement is used to denote a code path that should never execute. If executed, unreachable triggers a panic, meaning an unrecoverable termination of the program. Unreachable statements can be useful in development to help guard against unintended code paths. In exercise 33, an if else statement is used to branch on an error union value. If the value is an error, the else branch captures the error and executes. Otherwise, the if branch captures the payload value and executes. In exercise 35, an enum type is defined and assigned to constant ops. This enum has three named values, inc, pow, and dec. In the main function here, a switch is used to branch on the three enum values. In exercise 36, the enum type color is backed by integer type U32, meaning each value of the enum has an associated U32 value. This allows the enum values to be cast to integers with the built-in int from enum function. In exercise 37, we see the definition of an example struct type, which is assigned to constant character. This struct is composed of four fields, a field named role of type role, which is defined above as an enum, a field named gold of type U32, a field named experience of type U32, and a field named health of type U8. Inside the main function, we initialize a variable with a literal of this character struct type, and in the literal, we give a value for each field. Notice that each field name in the literal is prefixed with a dot. After creating the struct instance, we access its fields with the dot operator, just like in C or other similar languages. Next, exercise 39 demonstrates basic usage of pointers. The syntax is very similar to C, except the asterisk for dereferencing is placed in post position and separated by a dot. First here, we create a pointer from variable num1, which we assign to constant num1 pointer, and then we assign the dereference of num1 pointer to variable num2. Also note that we are allowed to assign to the dereference of a pointer, even if that pointer is stored in a constant. Unlike a variable, a constant cannot be assigned a new value after initialization, but dereferencing modifies the location referenced by the pointer, rather than modify the pointer itself. Hence, this assignment is allowed. Next, in exercise 40, the variable P has the type pointer to a constant U8, meaning it stores a pointer to a U8 that does not allow assignment to its dereference. Attempting to assign to the dereference of a pointer to a constant will trigger a compilation error. However, if the location pointed to by the pointer is not itself constant, then it is possible to modify the value directly. Here, P's last assignment is the address of Y, and we subsequently modify the value of Y. When we then dereference P, we get the last value assigned to Y. In exercise 45, the function deep thought returns a so-called optional type as denoted by the question mark prefix before U8. This optional type encompasses all U8 values plus the special value null. So, this function can return either any U8 value or null. Be clear that unlike in other languages, what Zig calls null isn't necessarily related to pointers, as in this case where we have the optional variant of the U8 type. As we'll see in the next exercise though, pointer types can also be made optional. Anyway, in the main function here, deep thought is called as the left operand of the or else operator. When the left operand of an or else evaluates to null, then the or else evaluates and returns its right operand. Otherwise, or else just returns its left operand. So here, because deep thought always returns null, this or else will return 42. The next exercise 46 defines an elephant struct, which contains a field named tail with type optional pointer of elephant. This means that the tail field can be assigned either an elephant pointer or the value null. The parameters of the function link elephants are also optional pointers of elephant. Even if we're certain that an optional pointer value is not null, we still must use an or else operation to get the plain pointer value. For the right operands of the or else operations here, we use unreachable, so if the optional pointers actually are null, these or else operations will trigger panics. Because this pattern is so common, Zig provides a shorthand syntax, question mark after a dot, as demonstrated by the next line, which is equivalent to the prior. Lastly, notice in main that we use the regular address operator for the arguments to the link elephant function. What's happening here is that the address operator returns a regular elephant pointer, but in this context, the compiler will coerce the regular pointer into an optional pointer. In exercise 47, functions are included as members in structs. In the alien struct, there is a member function named hatch, and in the heat ray struct, there's a member function named zap. Function members of a struct belong to the namespace of the struct, but if their first parameter is the enclosing struct type, then they can be called with traditional method call syntax. In this example, the hatch function in the alien struct doesn't take an alien as its first parameter, so it can only be called as alien.hatch. The zap function in the heat ray struct does have heat ray as its first parameter type, so it can be called either as heat ray.zap, or instead, we can write the first argument, then dot and zap, then in the parameter list parentheses, we pass the remaining arguments, just like the syntax of a Java or C# instance method call. Either way, the result is the same. In exercise 50, the first variable is initialized with the special value undefined. This means the initial content of the variable will be whatever happened to reside at that variable's location in memory before it was created. The rest of the code demonstrates that string literals can be cast to pointer to constant arrays of U8s, as long as the array size matches the number of bytes required to store the string. The variable first line one has a type pointer to constant array of 16 U8s, so it can be assigned a string literal with 16 bytes of character data. Then the second variable has an error union type where the payload is a pointer to constant arrays of 21 U8s, so it can be assigned a string literal with 21 bytes of character data. In the next exercise 52, we see examples of slices. A slice in Zig contains a pointer and a length, so it effectively represents a sub range of an array. Here the variable cards is an array of eight U8s, and then constants hand one and hand two are slices of U8s. The range notation inside the square brackets indicates a slice operation, where the first integer is the starting index of the sub range, and the second integer is the end index of the sub range, so the length of the sub range is the second integer minus the first. When the second integer is omitted, it defaults to the length of the array. Hand one is assigned a slice representing the sub range of the cards starting at index zero with length four. And hand two is assigned a slice representing the sub range of the cards starting at index four, also with length four because it runs to the remaining end of the cards array. In exercise 53, a string is sliced. Because the string type is a pointer to a constant array of U8s, slicing a string produces a slice of const U8s. Note that the use of constant type declarations seems a bit inconsistent. There's no such thing in Zig as an array of constants, nor is there such a thing as a constant array, yet Zig does have pointers to constant arrays. For slices, the values of the slice itself can be made constant, which prohibits modifying the values through the slice. Like with arrays, you you cannot create a slice which itself is a constant, but you can create a pointer to a constant slice. Confusingly though, unlike with pointers to constant arrays, you can modify the elements via a pointer to a constant slice. What you can't do via a pointer to a constant slice is modify the slice itself, meaning its pointer and length. This is very confusing, so let me restate it. First, you cannot create an array with a constant element type, but you can create slices with a constant element type. And second, you cannot create arrays or slices which are themselves constant, but you can create pointers to constant arrays or slices. The elements of a pointer to a constant array cannot be modified, but the elements of a pointer to a constant slice can, unless the elements themselves are declared constant. Anyway, const is definitely one of the more confusing aspects of Zig, so don't worry if it takes a while to get straight. The next exercise, 54, contains what Zig calls a many item pointer, which is a pointer that allows for indexing and pointer arithmetic. In this example, the local constant many PTR is a many item pointer of const U8s. The next line reads index five of the pointer, meaning the U8 value that is five U8s up in memory from the location represented by the pointer, just like adding five to a pointer in C. In the line after, the many item pointer is sliced from index zero up to index S.len. In exercise 55, a union type is defined called insect. A union in Zig has members like a struct, but unlike a struct, the fields of a union overlap each other in memory. So, effectively, only one field of a union instance is active at a time. The insect union here in this example has an ant struct field and a bee struct field, and if you assign the one field, you're effectively clobbering the value of the other. Because by default, unions in Zig are not tagged, there's no way to tell from the union itself which of its fields is currently active. So, the code here creates an enum species with values ant and bee to track which of the insect fields is active. In the main function, instances of the ant and bee structs are created, and then an instance of insect is created with its ant field initialized. The insect is passed to the print insect function along with the species.ant enum value to indicate that this insect represents an ant. Then, another instance of insect is created, this time with its bee field initialized, and this instance is passed to the print insect function along with the species.bee enum value. Again, without the enum, the print insect function wouldn't know which field of the insect to use. In exercise 56, again an insect union is defined, but this time the species enum is stored as a tag in the union type itself. Now, when a field of an insect instance is assigned, its tag is set accordingly. Inside the print insect function then, the code can switch directly on the insect value itself. Be clear though that the cases of the switch still correspond to the values of the species enum, not the fields of the union directly. In exercise 57, this time the insect union stores an auto generated enum as a tag, as indicated by the reserved word enum in the parentheses. The auto generated enum type has a value corresponding to every field of the union, so we don't need to create a separately defined enum type. In exercise 62, a for loop is used as an expression. In the loop, a break statement specifies the value produced by the loop. Because the compiler can't know if a loop is guaranteed to break, a for loop expression always requires an else clause to guarantee that the loop produces some value. In this case, the loop looks for the first string that has exactly three bytes, but failing to find any match, it will produce the value null. After the loop, the following if statement has an optional type for its condition. The else clause of this if will execute if the value is null, otherwise the if clause executes with the captured non null value. In exercise 63, an outer loop is given a label, enabling any break and continue statements inside any of its nested loops to break or continue from the outer loop. Inside main here, the outer loop here is given the label food loop, and then a continue statement inside a nested loop specifies this label to continue the outer loop. All other break and continue statements here do not specify a label, so they apply to the loops in which they're directly contained. Note that a label is defined with a colon after the name, but then when used in a break or continue statement, the colon precedes the name. In exercise 64, we see some calls to a couple of Zig's built-in functions, which are denoted by an at symbol prefix. First, the add with overflow function returns the result of adding two numbers, plus also a bit indicating whether the addition triggered overflow. Second, the bit reverse function reverses the values of the bits in an integer value. Exercise 65 demonstrates a few more important built-in functions. The at type function returns the type that it is called inside, in this case the struct narcissus. As you can imagine, this requires special compiler support, hence why it is a built-in function. Inside the type to string function defined here, the at type name function is called. The type name function takes a type as argument and returns its name as a string. Inside main, the at type of function is called. When called with one argument, this returns the type of the argument, but when called with multiple arguments, it returns the best fit type which they can all be coerced to. Zig calls this peer type resolution. In this case though, the arguments are all of the same type narcissus, so that is the type returned. Next, the at type info function is called. This function takes a type argument and returns a struct with its type information. In this example, the code uses type info to print the names of the narcissus fields as strings. Lastly here, note that the field structs is written as a string with an at symbol prefix. This is special syntax for identifiers that otherwise are reserved words in the language. Because struct is a reserved word in Zig, we need this special syntax to access the type info field named struct. Exercise 66 demonstrates that number literals are comptime types. An integer literal is a comptime int, and a float literal is a comptime float. Because these types are expected to exist only at compilation time, we can create constants to store these types, but not variables. However, these types can be coerced into the other numeric types, such as in the following line, where an integer literal is coerced into a U32, and a float literal is coerced into an F32. In exercise 67, a comptime variable is created, meaning a variable which can be modified at compile time, but which functions as a constant at runtime. We need to back up a second though and talk about how to think about Zig's compile time execution. The simplest way to think of it is that as the compiler processes each statement, it asks two questions. One, should the statement be executed right now during compilation? In other words, is it a comptime statement? And two, should the statement be part of the generated code? In other words, is it a runtime statement? Some statements are either just comptime or runtime, but some statements are both. You may wonder though how code can be executed during compilation if it hasn't yet been compiled already. Well, in short, the compile time code is interpreted. The compiler translates a comptime statement directly into action, rather than generated machine code. Or in other words, the compiler reads the statement and does what it says to do. Now, the details of course get more complicated, but this is generally an accurate enough mental model for a user of the language. So anyway, in this example, we have a comptime variable count, which is initialized with the value zero. Because the variable is comptime, any assignment to the variable is also implicitly executed at compile time. However, the value of a comptime variable can still be used in runtime expressions, in which cases it acts like a constant that has whatever value was last assigned to it. In this example, though, count is only used in expressions that can be fully evaluated at compile time. After each time the count is incremented, it is used with a double asterisk operator and a struct literal containing the character A. The end result is that constant A1 is a struct with a single character A, constant A2 is a struct with two characters B, constant A3 is a struct with three characters C, and constant A4 is a struct with four characters D. In exercise 68, the function scale me inside the struct schooner has a compile time parameter. This means the argument passed to this parameter must be a constant or compile time value, and the function is separately compiled for each unique combination of arguments to its compile time parameters. The scale me function in this example is called with three different values for its compile time parameter scale, so it is compiled three times. In exercise 69, the function make sequence has two compile time parameters, one for a type and one for a size. The function then returns an array of this type and size. Inside the function, it initializes the array it will return with increasing integer values. This requires using the built-in function int cast, which casts a value into the target type expected from context, and also the built-in function as, which casts a value into a target type. Because all parameters of this function are compile time, and the function does not depend upon any runtime globals, calls to the function can execute fully at compile time. Also note that because the function casts an integer to the type parameter T, T must be an integer type. Any call to make sequence that tries to pass a non-integer type would trigger a compilation error. In exercise 70, the last function is a duck has a parameter of type any type, meaning the compiler accepts any kind of value passed to this parameter. A function with any type parameters is separately compiled for each unique combination of types passed to its any type parameters. Inside this function, built-in functions are used to get information about the parameter's type. Type of returns the type and has decl returns true if the passed type has a member with a name matching the passed string. So here, walks like duck will be true if the argument has a member named waddle, and quacks like duck will be true if the argument has a member named quack. Note that any if with no runtime expressions in its condition will be evaluated at compile time, and if the condition is false, then the body of the if is omitted from the generated code entirely. So here, when is a duck is called with an argument not having both waddle and quack members, the call will not invoke the quack method of the possible duck, and in fact the generated code will contain no such branch at all. In exercise 71, a for loop has the modifier inline, which means the loop is unrolled at compile time. An unrolled loop is iterated at compile time and generates runtime code for each iteration. Here, the loop iterates over the compile time value fields, so the loop is allowed to be inlined. Inside the loop, the if condition is evaluable at compile time, so the body of the if is only included in the generated code of each iteration when its condition is true at compile time. Be clear, though, that again, this is generally true of if statements, not just if statements inside inline loops. In exercise 72, this time a while loop has the modifier inline. Again, the loop is iterated at compile time and code is generated for each iteration. This is allowed here because both the condition and the post condition of the loop are evaluable at compile time. Inside this loop, the switch is also evaluable at compile time, so each iteration contains generated code for just the single matching case. Like with ifs, this is generally true of all switch statements, not just switch statements inside inline loops. In exercise 73, a compile time statement is used to make an expression evaluable at compile time. The function get llama takes a single compile time parameter, but the function reads from a global non-constant array, so calls to the function must still execute at runtime. However, the call to assert in the function can be made to run at compile time with the compile time reserved word. This way, calls to get llama with an index out of bounds will trigger a compilation error rather than a runtime error. Lastly about compile time, exercise 74 demonstrates that code in the file scope, meaning outside of any function, is always implicitly executed at compile time. The global constant here, llamas, is initialized with a call to make llamas, which takes a compile time argument. Though the expression is not explicitly marked as compile time, it is compile time implicitly. If we do try to add the compile time reserved word here, the compiler will complain that it's redundant. Exercise 76 introduces sentinel-terminated arrays. A sentinel is a special designated value that signals the end of an array. C strings, for example, use a zero byte to denote the end of the string. Here, the variable nums stores a sentinel-terminated array with sentinel value zero, as indicated by the colon and zero inside the square brackets. As usual, the underscore indicates that the length of this array is inferred from the number of values in the literal, in this case six. Because the sentinel itself must be stored at the end of the array, though, this array requires storage for seven contiguous U32s in memory rather than just six. Because the end of the array is supposed to be indicated by the sentinel value zero, the array generally should not contain zero as a normal value. However, Zig does not enforce this restriction. Many item pointers can also be sentinel-terminated, such as this constant PTR, which is a zero-terminated many item pointer of U32s. Concretely, it is still just a pointer, but when indexed, the value zero is expected to indicate the end of the data. Lastly here, the function print sequence uses compile time introspection to print information about these types. In the case of a pointer, the function calls the built-in sentinel to get the sentinel value of the type, which in this case is zero. Exercise 77 demonstrates that the true type of a Zig string literal is a pointer to a constant zero-terminated array of U8s. Such a pointer can be cast or automatically coerced to several different other types, in this case a many item pointer of const U8s. In exercise 78, the built-in function pointer cast is used to cast a multi-pointer of const U8s into a zero-terminated many item pointer of const U8s. Like a few other built-ins we've seen, this built-in infers its return type from the calling context. In exercise 80, the function circle returns a new type. Because types only exist at compile time, that right there tells you that this function can only execute at compile time. The function takes a compile time type parameter, and this type parameter is used in a struct definition that is directly returned from the function. So the function returns a struct type where the fields have the type of the type argument. In the main function here, the circle function is first called with argument I32, so it returns the struct type where T is I32, and then this type is used in a struct literal that is assigned to the local constant circle one. Then the circle function is called again, but this time with argument F32, so it returns the struct type where T is F32, and again this type is used in a struct literal that is assigned to a local constant. In exercise 81, two anonymous struct literals have three fields of the same name but different types. These structs are then passed to the print circle function, which takes an any type parameter, and the function prints out the values of these fields. Because the print circle function doesn't depend upon the types of the fields, print circle can be compiled for both of these anonymous structs. Also, the fact that one struct has an extra field is irrelevant because print circle simply ignores it. In exercise 82, an anonymous struct is created with values that have no field name. These values are implicitly assigned to numbered fields starting from zero. So here, the value true is assigned to field zero, the value false is assigned to field one, the I32 value 42 is assigned to field two, and the F32 value 3.141592 is assigned to field three. In the print tuple function, compile time introspection is then used to print the names, types, and values of these fields. In exercise 83, we see a struct with numbered fields coerced into an array. The struct has five U8 values for fields zero, one, two, three, and four, and so the struct can be coerced into an array of five U8s. In In exercise 92, three struct types, ant, bee, and grasshopper, all have a member function named print that takes a value of their own type. The union type insect has a member for each of these structs and defines its own print function that takes an insect. Inside this function, a switch over the insect value contains just the else case marked with the reserved word inline, which means that compile time a case is generated for each member of insect. So, here then we get three generated cases, each of which invokes the print method of its respective insect member type. This inline else convenience spares us from manually writing out cases for each insect member to invoke their respective print methods. So, in the main function here then, insect values are passed to the insect print method, which switches on the member type and invokes the appropriate struct's print method. Exercise 93 demonstrates how to import C code. The built-in function C.import takes as argument a block which is interpreted as C code. Inside the block, calling the special built-in C.include will transpose the content of the specified file just like the include directive in C. Note that C.import can only be called inside the block passed to C.import. The C.import function itself returns the module of C code as a module type. Then inside the main function here, the imported C print function is invoked. The function takes a C int, an optional pointer of const anyopaque, and a C uint, and the function returns a C uint. In exercise 96, an arena allocator from the standard library is used to manually allocate memory. First, an arena allocator is initialized with its deinitialization deferred immediately in the next line. Then the allocator method is called to get a wrapper type, which has a create method that allocates memory of the specified size. In this case, memory is allocated for an array of F64s. This array is then passed to a function which stores the running averages of values from another array. Note that we don't deallocate the individual allocations from the arena. The whole idea of an arena is that all of its memory is deallocated together. So, here the deferred deinit call on the arena itself is what will deallocate this memory. In exercise 100, a for loop iterates over two arrays in tandem. This is allowed for arrays which have the same length. In exercise 101, a for loop iterates over three arrays and a range. Again, this is allowed as long as all the sequences have the same length. This range starts at one and leaves its end unspecified, so its length will automatically match the other sequences. Exercise 102 demonstrates a few tests. A test block is marked by the word test and its name is specified as a string. Tests are executed by running the zig test subcommand, and each test is effectively like a function that returns an error or void. The expect function of the testing module returns an error if its argument is not true. Expect equal returns an error if its two arguments are not equal, and expect error returns an error if its second argument does not match the error passed as its first argument. In exercise 104, the main thread spawns a few additional threads and waits for them to terminate. To create a thread, we call std.thread.spawn. The first argument is an anonymous struct of config options, which when empty means we're using all the defaults. The second argument is the function to invoke in the thread, and the third argument is an anonymous struct of values to be passed to the function. In this case, the function being called in the threads takes a single integer, and the values one, two, and three are being passed to each of the three threads, respectively. The calls to spawn return handles that represent the threads, and calls to join on these thread handles are immediately deferred after spawning. When we call join in the main thread, the main thread will spin and wait for the join thread to finish execution if it hasn't finished execution by that point already. Thus, after join returns, the join thread is known to have finished execution. Because all these defers are inside a code block, the join calls will run when execution leaves that code block. So, the print statement that is after the block will not run until after the threads have all finished running. But before leaving the block, the main thread also gets an instance of std.io.threaded and calls its sleep function to make the main thread sleep for a minimum of 5 seconds. Note that the expression .initSingleThreaded is shorthand for std.io.threaded.initSingleThreaded, and also the expression .await is shorthand for std.io.clock.await. These namespaces can be left implicit in these cases because they can be inferred from the context. In exercise 108, a switch is given a label so the code in the switch can break out of or continue the switch. To continue a switch means to jump back to the start of the switch with a different value. For example here, the approved case continues with the merged value, so execution effectively jumps from the approved case to the merged case.