My notes on zigling and zig-book.:w
// 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
printCharacter(&glorp);
// wskaznik do tej struktury
What is Undefined?
There are 4 ways to tell ziggi that variable/const or something does not have any value. ("no value")
- Str8 forward
var skibi: u8 = undefined;
it value of the variable. Or should be seen like one, can be used as some placeholder. You are telling that you are not giving it value yet its var
means you can change it later.
- Close your eyes bro. What do you see? .... Nothing.
var skibi: ?u8 = null;
Imagine that there is nothing. You are not giving it value. It is not defined. It is not undefined. It is null. It is a promise that this is a no_value value. Now skibidi is something like:
I'm u8 or I'm nothig, but to be nothing you need to be a thing. Holds value which is saying,
no value
- ERROR Imagine that something went wrong and you are not able to give it value. You got 2 options, a) you can ignore it and let your program crash b). -> tests/test_050_error.zig
var toilet: u8!MyError = SomeError;
It was said that they are very similar to null
, these two holds value
- void
This is a type not a value. (hold semantic value == dont take any space),
It's much more common to see void as the return type of a function that returns nothing.
var skibidi: void ={};
BRIEFLY WE GOT:
// * 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 stored here
Now I am doing the code:
//obv i need to do undefined bc we already have defined type and size (*const [16]u8);
//thats why i will use undefined (maybe later a change my mind? who knows)
var first_line1: *const [16]u8 = undefined;
//there is nothing to say i was explained before why i used rn a
// Error,
var first_line2: Err!*const [21]u8 = Err.Cthulhu;
// null
var second_line2: ?*const [18]u8 = null;
I added a 1 commend close to first_line2, to show how error looks like,
??? computer's molten core??? wtf is this.
the ziggi it saying that:
const std = @import("std");
Is a structure that holds a lot of different values. I think the whole program will tell us what are those crazy bits and bytes.
Struct is a very convenient way to deal with memory <- thats a quote from the exercise. All the values of the struct are stored in memmory, if you add size of all the particular values you will get the size of struct as a whole.
// if the instance of the struct is const, all the values inside are const
const Toilet = struct{
toilet_paper: u32 = 3,
toilet_paper_roll: u32 = 1,
};
const skibdi_new= Toilet{
.toilet_paper = 31,
.toilet_paper_roll = 10,
};
// but you can create the instance of the struct that is not const
var free_skibidi = Toilet{};
a function is a instruction code at a particular address. FUNCTION PARAMETERS IN Z I G ARE ALWAYS IMMUTABLE
oh, struct can be written like it was a size/type ex.
//its a number
var number: u32 =3;
var nmumber2: u32 = someother_number;
// but with struct?
var skib: *Toilet = &skibidi_new;
Remember Timmie that *Toilet
is a pointer to the struct, and &skibidi_new
is a reference to thevalue.
Values inside the function are initated as the const.
fn function(arg: u32) void{
arg =3; //error
}
but... We will now look at different ways of assigning existing variables to new ones. When do we pass thesame object in memory? When do we make a copy?
var glorp_access1: Character = glorp;
glorp_access1.gold = 111;
Above creates a copy. You can see it by changing a value after assigning to new name. The two variables will have different values if you change the value for one of them.
var glorp_access2: *Character = &glorp;
glorp_access2.gold = 222;
Now we created a proper link to an object. This variable is nothing more than just the original glorp in disguise. If you now change the value of some field, it will be changed for the original glorp as well.
const glorp_access3: *Character = &glorp;
glorp_access3.gold = 222;
Now we get back to the pointer differences we've talked when doing pointer exercises. const pointer variable means that we can't change what the pointer variable is pointing at, but we can still change the values of the variable we point at.
const glorp_access4: *const Character = &glorp;
glorp_access4 = 111;
part of this code above was copied from -> https://github.com/Laz4rz/ziglings
Answer
//in leverUp we needed to change arguments to a values.
levelUp(&glorp, reward_xp); //original wal levelUp(glorp, reward_xp);
//later on I needed to see that the argument `character_access` is imported as a const, I needed a pointer to check sum.
fn levelUp(const character_access: *Character, xp: u32) void{
};
Alright, Zigers, let's talk about arrays and how they can be moved around. Remember that function definition from quiz 3? Yeah, the one that only worked with arrays of exactly 4 u16s? That's the core issue with arrays – their size is baked right into their type. Once you declare an array like:
var digits = [10]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
, it's stuck being an array of 10 u8s forever. Not super flexible, right?
Enter slices. Think of slices as a way to peek into an existing array without being tied down by its full size. They're like dynamic windows into an array(you come inside, see what you want and go home), letting you specify a starting point and a length.
Let's break down those examples:
const foo = digits[0..1];
This creates a slice that starts at the very first element (index 0) and includes elements up to, but not including, index 1. So, foo
will contain just 0.
const bar = digits[3..9];
This slice starts at index 3 and goes up to (but not including) index 9. So, bar will hold 3, 4, 5, 6, 7, 8
const skibidi = digits[5..9];
Similar to bar
, this starts at index 5 and goes up to index 8. skibidi
will contain 5, 6, 7, 8.
const all = digits[0..];
This is a cool shortcut. By leaving off the end index, you're telling Zig, "Give me everything from the starting index to the end of the array." So, all will contain all the digits: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
.
The really neat thing is the type of these slices. If you have an array of u8's, a slice of that array will have the type []u8
.
Noticed the empty brackets? Of course you did smart fella. I love you Timmie
That signifies that the length of the slice isn't fixed at compile time. It can vary depending on where you're slicing from and how much you're slicing.
So, slices are a game-changer when you want to work with portions of arrays without being constrained by their full, static size. They make passing data around and writing flexible functions much, much easier. You'll be seeing slices a lot in Zig, so getting comfortable with them is key!
Answer nothing special, just remember that cards[0..4]
will give you A4K8 because they are indexed inside like 0,1,2,3,4
and cards[0..4]
stop just before element with index 4. (de facto 5th element)
zig run exercises/052_slices.zig
Hand1: A 4 K 8
Hand2: 5 2 Q J
your output should be like this.
Welcome, todays Subject is: Slicing... again, Are you interested in slicing of a string? You get to the proper place lad.
- String is just an array of characters. (string)
- Timmie, you need to remember that in Zig, strings literals, are immutable (you cant change them). {const}
- Thats why if you want to take slice of a string, you need to use
[]const u8
type, because output needs to be the same type as input.
If you forgot about this you will get,
exercises/053_slices2.zig:22:34: error: expected type '[]u8', found '*const [16:0]u8'
const base3: []u8 = scrambled[32..];
~~~~~~~~~^~~~~~
I added simple Error, when word in justice1 == "for"
fn printPhrase(part1: []const u8, part2: []const u8, part3: []const u8) !void {
const stdout = std.io.getStdOut().writer();
if (std.mem.eql(u8, part1, "for")) {
return error.Word_For;
} else return try stdout.print("'{s} {s} {s}.' ", .{ part1, part2, part3 });
}
const MyError = error{Word_For};
/Ziglings/exercises/053_slices2.zig:37:9: 0x10de1f8 in printPhrase (053_slices2)
return error.Word_For;
/Ziglings/exercises/053_slices2.zig:28:5: 0x10de54f in main (053_slices2)
try printPhrase(justice1, justice2, justice3);
Pointers are a way to reference a value in memory. I can do them to multiple items without using slicing.
good to hear fr fr
//examples from code
var foo: [4]u8 = [4]u8{ 1, 2, 3, 4 };
var foo_slice: []u8 = foo[0..]; //slice has a known length,
var foo_ptr: [*]u8 = &foo; // pointer doesn't,
//Its your duty to keep track
//of the number of u8 foo_ptr points to
var foo_slice_from_ptr: []u8 = foo_ptr[0..4];
- Definition: An array in Zig has a fixed size known at compile time. The type
[N]T
represents an array ofN
elements of typeT
. - Example:
var foo: [4]u8 = [4]u8{ 1, 2, 3, 4 };
defines an array namedfoo
that holds exactly 4u8
values. - Nature: Arrays often behave like value types. When assigned or passed by value, the entire content might be copied (depending on context and size). The size
N
is part of the array's type.
const std = @import("std");
pub fn main() void {
// Define an array of 3 integers
var numbers: [3]i32 = .{ 10, 20, 30 };
// Access an element
std.debug.print("Second number: {d}\n", .{numbers[1]}); // Output: Second number: 20
// The size is fixed and known at compile time
std.debug.print("Array size: {d}\n", .{numbers.len}); // Output: Array size: 3
// Attempting to resize or assign different size array will cause compile error
// numbers = .{ 1, 2, 3, 4 }; // COMPILE ERROR: expected type '[3]i32', found '.{ 1, 2, 3, 4 }'
}
- Definition: A slice is a view or a section into an array(memory). It doesn't have a fixed size known at compile time. The type
[]T
represents a slice of elements of typeT
. But is stored with the slice and is know at RUNTIME - Example:
var foo_slice: []u8 = foo[0..];
creates a slicefoo_slice
that points to the beginning of thefoo
array and knows its length is 4. Slices can represent a part of an array too:foo[1..3]
would be a slice of length 2 containing{ 2, 3 }
. - Key Feature: Slices always know their length. This makes them safer to work with for iteration and bounds checking. They are often used for function arguments that accept sequences of unknown length at compile time.
Example :
const std = @import("std");
pub fn main() void {
var data: [5]u8 = .{ 'Z', 'i', 'g', '!', '!' };
// Create a slice covering the whole array
var full_slice: []u8 = data[0..];
std.debug.print("Full slice length: {d}\n", .{full_slice.len}); // Output: Full slice length: 5
// Create a slice covering part of the array
var partial_slice: []u8 = data[1..3]; // Includes elements at index 1 and 2 ('i', 'g')
std.debug.print("Partial slice length: {d}\n", .{partial_slice.len}); // Output: Partial slice length: 2
std.debug.print("Partial slice content: {s}\n", .{partial_slice}); // Output: Partial slice content: ig
// Modify data through the slice (slices point to the original data)
partial_slice[0] = 'I';
std.debug.print("Original data modified: {s}\n", .{data[0..]}); // Output: Original data modified: ZIg!!
}
- Definition: A many-item pointer (
[*]T
) is a pointer to one or more items of typeT
, but it does not store the length. It's essentially just a memory address pointing to the start of a sequence. - Analogy: This is similar to how pointers often work in C/C++, where you have a pointer to the first element but need a separate mechanism (like a null terminator or an explicit length variable) to know where the sequence ends.
- Example:
var foo_ptr: [*]u8 = &foo;
creates a many-item pointerfoo_ptr
that points to the start of thefoo
array's data. Crucially, the type[*]u8
itself does not retain the information that there are 4 elements. - Responsibility: When using a
[*]T
, you (the programmer) are responsible for keeping track of the number of valid items this pointer points to. Accessing beyond the actual bounds leads to undefined behavior. These are often used for C interoperability or low-level memory manipulation where length tracking is handled separately.
const std = @import("std");
pub fn main() void {
var values: [4]f32 = .{ 1.1, 2.2, 3.3, 4.4 };
const len: usize = values.len; // We MUST store the length separately
// Get a many-item pointer to the start of the array
var values_ptr: [*]f32 = &values;
// We CANNOT get the length from the pointer itself
// std.debug.print("Pointer length: {d}\n", .{values_ptr.len}); // COMPILE ERROR: '[*]f32' has no member named 'len'
// Accessing elements requires knowing the valid range (using our stored 'len')
std.debug.print("First value via ptr: {d}\n", .{values_ptr[0]}); // Output: First value via ptr: 1.1
std.debug.print("Third value via ptr: {d}\n", .{values_ptr[2]}); // Output: Third value via ptr: 3.3
// To use it like a slice, we need to combine the pointer and the length
var values_slice: []f32 = values_ptr[0..len];
std.debug.print("Slice created from ptr length: {d}\n", .{values_slice.len}); // Output: Slice created from ptr length: 4
}
-
Definition: This is a pointer to a specific, fixed-size array type. The type
*[N]T
is a pointer to one instance of[N]T
. -
Example:
var foo_ptr_to_array: *[4]u8 = &foo;
-
Difference from
[*]T
: This pointer type knows it points to an array of exactlyN
items becauseN
is part of the type itself (*[N]T
). You can get the size by dereferencing the pointer and accessing the.len
of the underlying array type.[*]T
loses this compile-time size information. -
Code Snippet:
const std = @import("std");
pub fn main() void {
var matrix_data: [2][2]i32 = .{ .{1, 2}, .{3, 4} };
// Pointer to the specific array type [2][2]i32
var matrix_ptr: *[2][2]i32 = &matrix_data;
// Access elements by first dereferencing the pointer (*)
std.debug.print("Element [0][1]: {d}\n", .{matrix_ptr.*[0][1]}); // Output: Element [0][1]: 2
// We can get the length (number of rows in this case) from the dereferenced array
std.debug.print("Matrix rows: {d}\n", .{matrix_ptr.*.len}); // Output: Matrix rows: 2
// Modify data through the pointer
matrix_ptr.*[1][1] = 42;
std.debug.print("Modified element [1][1]: {d}\n", .{matrix_data[1][1]}); // Output: Modified element [1][1]: 42
}
Free cheatsheet!!!!
// 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 |
// +---------------+----------------------------------------------+
It wasnt that difficult, was it?
The Big Zig told me few things about unions. Sounds cool. But What is this??
// imagine a struct like this which can hold either a u8 or a u16 or something else.
const MyUnion = union {
x: u8,
y: u16,
z: f32,
a: bool,
//etc.
}
Unions are a way to store different types of data in the same memory location. Realy good for ecolodgy of memory. You can reuse the same memory shelf.
var some_union: MyUnion{ .x = 42 };
some_union.y = 1234; // gives error
some_union = MyUnion{ .y = 1234 }; // works
some_union.a = True; // gives error
// How to make it work??
Its important to remember that you can only access the field that was last written to.
exercise link First look at the code.
const BigEnumStruct = enum{small, medium, large};
const MyUnion = union {
x: u8,
y: u16,
z: f32,
};
var skibidi = MyUnion{ .x = 42 };
By using swtich state I can change/act on my taged union skibidi
. For example like this,
switch (skibidi) {
.x => |argument| do_something_function(argument),
.y => |argument_2| function2(argument_2),
.z => |argument_3| function3(argument_3) ,
}
Optional Values are basically
null unions
, and Erros are using "error union types".
And it's possible to create own unions depends on the needs:
const MyUnion = union {
x: u8,
y: u16,
z: f32,
a: bool,
b: BigEnumStruct,
function: void,
};
exercise link
Tagged unions are fun (probably).
damn, instead of creating enums I can just use enum
and union
together. toUse this as strucs or something to store data
I can create a union like this:
const Insect = union(enum) {
flowers_visited: u16,
still_alive: bool,
};
Instead of having to write it the longer way with separate enum definition (which I was doing before):
const InsectStat = enum {
flowers_visited,
still_alive,
};
const Insect = union(InsectStat) {
flowers_visited: u16,
still_alive: bool,
};
Cool Things I Discovered 🌟
- I can use union(enum) to make my code shorter
- It automatically creates the enum types from the union fields
- It's really useful for making different types of data work together (like my ants and bees example!) Note to self: This is a really neat way to handle different types of data in one structure!
Damn, I was like 30 minutes to do some zigging, you know. git commit
and see you later. Wtf is this
// quote from the exercise
//
// 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
I'm just going to show you how things were(?) done by me.
// 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 => |p| print("{s}", .{p.name}), // It was needed to add a |p|
.path => |p| print("--{}->", .{p.dist}),
}
}
};
if (place == entry.*.?.place) return &entry.*.?;
// Try to make your answer this long:__________;
}
return null;
}
Last: In function get trip to change void
-> !void
ex skip that
//
// 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
//
In next one(060_floats.zig) I found new notes
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,200const a2: f32 = 1.2e+3;
// 1,200const b1: f32 = -500_000.0;
// -500,000const b2: f32 = -5.0e+5;
// -500,000
Zig's type coercion allows for automatic conversion between certain types, making the code cleaner and more concise. Here's a breakdown of the key rules:
Core Principles:
- Restriction: Types can always be coerced to a more restrictive type (e.g., mutable to immutable).
- Expansion: Numeric types can be coerced to larger types (e.g.,
u8
tou16
,f32
tof64
).
Key Coercion Rules:
- Pointer to Array: Single-item pointers to arrays coerce to slices (
[]T
) and many-item pointers ([*]T
). - Single-Item to Array Pointer: Single-item pointers (
*T
) can coerce to pointers to a one-element array (*[1]T
). This allows treating a single value as if it were in an array. - Optional Types: Values and
null
coerce to optional types (?T
). - Error Unions: Values and errors coerce to error unions (
Error!T
). - Undefined:
undefined
coerces to any type. This is essential for uninitialized variables.
Example: Coercing a Pointer
Let's say you have var x: u8 = 10;
and var ptr: *u8 = &x;
.
ptr
can coerce to*const u8
(mutable to immutable).ptr
can coerce to*[1]u8
(single-item pointer to array pointer).ptr
can coerce to?*const [1]u8
(optional pointer to a const array pointer).
Important Considerations:
- Coercion must not lead to data loss. You cannot coerce a
u32
to au8
directly (explicit casting needed). - Understanding coercion rules is crucial for writing safe and efficient Zig code. The compiler will help by enforcing these rules and reporting errors when coercion is not possible.
Simple example of using loop expressions in Zig. Loop expressions are a powerful feature that allows you to iterate over collections and perform operations in a concise manner.
Just add else statement on the end
Zig utilizes labels primarily for fine-grained control over flow in loops and blocks. They allow you to specify exactly which loop or block a break
or continue
statement should affect, especially in nested situations.
Labels can be attached to while
and for
loops.
break :label;
: Exits the specific loop identified bylabel
, even from within nested loops.break :label value;
: Exits the specific loop identified bylabel
and makes the loop expression evaluate tovalue
. (Useful when the loop is used as an expression).continue :label;
: Skips the rest of the current iteration and proceeds to the next iteration of the specific loop identified bylabel
.
const std = @import("std");
pub fn main() !void {
var i: u3 = 0;
outer_loop: while (i < 5) : (i += 1) {
std.debug.print("Outer: {d}\n", .{i});
var j: u3 = 0;
inner_loop: while (j < 5) : (j += 1) {
std.debug.print(" Inner: {d}\n", .{j});
if (i == 2 and j == 1) {
std.debug.print(" -> Breaking outer_loop from inner!\n", .{});
break :outer_loop; // Exit the loop labeled 'outer_loop'
}
if (i == 0 and j == 3) {
// This would only break the inner_loop if uncommented
// break :inner_loop;
}
}
// This line is skipped when break :outer_loop is hit
std.debug.print("End of outer iteration {d}\n", .{i});
}
std.debug.print("After loops.\n", .{});
// Output stops after "Outer: 2", " Inner: 0", " Inner: 1", " -> Breaking outer_loop from inner!"
}
Labels can also be applied to regular blocks ({ ... }
). This turns the block into an expression that can be exited early using break :label value;
.
break :label value;
: Immediately exits the block identified bylabel
, and the entire block expression evaluates tovalue
.
Example: Using a labeled block as an expression with early exit.
const std = @import("std");
pub fn main() !void {
const input: ?u32 = 10; // Try changing to null, 0, or 101
const result_message = check_value: {
const value = input orelse {
// If input is null, exit the block early
break :check_value "Input is missing.";
};
if (value == 0) {
break :check_value "Input is zero.";
} else if (value > 100) {
break :check_value "Input is too large.";
}
// If we reach here, the input is valid and within range
// The final expression of the block is its value if no break occurred
// but using break :label is often clearer for the "success" case too.
break :check_value "Input is valid.";
};
std.debug.print("Check result: {s}\n", .{result_message});
// With input = 10, Output: Check result: Input is valid.
// With input = null, Output: Check result: Input is missing.
// With input = 0, Output: Check result: Input is zero.
// With input = 101, Output: Check result: Input is too large.
}
Block labels are particularly useful for complex initializations or validation logic where you might need to return a specific result from multiple points within the block.
Zig provides a set of built-in functions that can be used to perform various operations. These built-ins are available in the @
namespace and can be used without any imports.
Some of the most commonly used built-ins include:
@intToPtr
: Converts an integer to a pointer type.@ptrToInt
: Converts a pointer to an integer type.@sizeOf
: Returns the size of a type in bytes.@alignOf
: Returns the alignment of a type in bytes.@typeInfo
: Returns information about a type, including its size, alignment, and whether it is a pointer or an array.
Juz read the exercise I added the checking skibidi
to see something
Okay Timmie, let's look at this Zig code like it's about building blocks and asking questions!
Imagine you have a special blueprint for a toy box called Narcissus
.
-
The
Narcissus
Box Blueprint:- This blueprint says the box should have two spots inside called
me
andmyself
. These spots are special because they should hold instructions pointing back to the very same box! Like looking in a mirror. - It also has a spot called
echo
, but this spot is like an empty space, it doesn't hold anything real (that's whatvoid
means here - nothing!). - Inside the blueprint, there's a little helper instruction called
fetchTheMostBeautifulType
.
- This blueprint says the box should have two spots inside called
-
Building the Box:
- In the
main
part, we build one actual toy box using theNarcissus
blueprint:var narcissus: Narcissus = Narcissus{};
. - Uh oh! The spots
me
andmyself
are empty! The code fixes this by putting instructions in them that point back to ournarcissus
box:narcissus.me = &narcissus;
(Put a pointer tonarcissus
in theme
spot)narcissus.myself = &narcissus;
(Put a pointer tonarcissus
in themyself
spot)
- In the
-
Asking Magic Questions (The Built-ins!):
-
@TypeOf(...)
- What kind of toy is this? Imagine you show the computer yournarcissus
box, the instruction inme
(which points to the box), and the instruction inmyself
(which also points to the box).@TypeOf(narcissus, narcissus.me.*, narcissus.myself.*)
asks the computer: "Hey, if you look at all these things, what kind of toy are they all related to?" The computer is smart and says: "They are all related to theNarcissus
blueprint!" So,Type1
becomes the typeNarcissus
. -
@This()
- What blueprint am I inside? ThefetchTheMostBeautifulType
helper inside the blueprint uses@This()
. This is like the helper asking itself: "What's the name of the blueprint I am written inside of?" The answer is: "You are inside theNarcissus
blueprint!" Tiny tricky part: The code callsnarcissus.fetchTheMostBeautifulType()
. It's asking the actual box to run the helper. The helper still knows it belongs to theNarcissus
blueprint. So,Type2
also becomes the typeNarcissus
. -
@typeInfo(...)
- Tell me about this blueprint! This is like asking the computer to be a detective and look really closely at theNarcissus
blueprint (not the actual box, but the idea of the box).@typeInfo(Narcissus)
asks: "Tell me all the parts listed in theNarcissus
blueprint!" The computer gives back a list (fields
) saying: "Okay, the blueprint has a part namedme
, a part namedmyself
, and a part namedecho
."
-
-
Checking the Parts:
- The code then looks at the list of parts the detective (
@typeInfo
) found. - For each part (
me
,myself
,echo
), it checks: "Is this part an empty space (void
)?" if (fields[0].type != void)
: Is the first part (me
) NOT an empty space? Yes! So print "me".if (fields[1].type != void)
: Is the second part (myself
) NOT an empty space? Yes! So print "myself".if (fields[2].type != void)
: Is the third part (echo
) NOT an empty space? No, it is an empty space (void
)! So, don't print "echo".
- The code then looks at the list of parts the detective (
-
maximumNarcissism
Helper:- Sometimes the computer gives a long name like
"filename.Narcissus"
. This little helper function just cleans it up and gives you back the main part,"Narcissus"
. It makes the final printout look nicer.
- Sometimes the computer gives a long name like
So, Timmie, what does the code do?
It makes a Narcissus
box that points to itself. Then it uses magic Zig questions (@TypeOf
, @This
, @typeInfo
) to learn about the type of the box (Narcissus
) and the parts inside its blueprint (me
, myself
, echo
). Finally, it prints the important parts (me
, myself
) but skips the empty one (echo
). It's all about looking at things and asking questions about what they are and what they're made of!
Comptime is a powerful feature in Zig that allows you to execute code at compile timeThis means you can generate values, types, and even functions before your program runs, leading to more efficient and flexible code.
We can do things like this:
const skibidi = 123;
const toilet = 3.21;
I Dont need to precise the size
of the const value
. Now they are comptime_int
and comptime_float
If we use variables we need to know how much memory we need to reserve for this. Which means this code is wrong
var skibidi_var = 123;
var toilet_var = 3.21;
It's needed to add the size of variable:
var skibidi_var: u32 = 321;
// . . . o . . * . . .
// . * | . . . . . . * . .
// --o-- comptime * | .. .
// * | * . . . . --*-- . * .
// . . . . . . . . . | . . .
when I use comptime before declaration of variable, zig will perform every usage of this variable in comptime
comptime var skibidi: u32 = 123; //comptime
var toilet: f32 = 2.11; //no comptime == RUNtime and no error
var toilet2 = 2123; //error (no declaration of size)
But they said that the using of comptime
is A CRIME if I use this on unassigned size variablee. fun?
When we write something like this:
fn scaleMe(self: *Schooner, comptime scale: u32) void {}
We are telling compilator that comptime scale
is a compile-time variable:
this variable its need to be known at compile time. So, when we call this function, we need to pass a value that is known at compile time.
This is useful for things like array sizes, loop counts, or any other value that can be determined before the program runs.
-
Your blocks
You have three blocks: 🔴 (red), 🟢 (green), 🔵 (blue) -
A normal loop (“for”)
- It’s like telling your toy robot:
- “Pick up one block, draw it”
- “Pick up the next block, draw it”
- “Pick up the next block, draw it”
- The robot thinks each time it picks a block (this happens at runtime).
- It’s like telling your toy robot:
-
inline for
loops- You tell your robot before playtime exactly what to do with each block.
- The robot writes down three steps on its plan:
- Draw the red block đź”´
- Draw the green block 🟢
- Draw the blue block 🔵
- When playtime starts, the robot just follows the steps—no thinking needed!
```zig
const std = @import("std");
pub fn main() void {
const blocks = [_]u8{ 10, 20, 30 };
for (blocks) |value, index| {
std.debug.print("Block {} has size {}\n", .{index, value});
}
}
At runtime, the robot thinks:
- “Pick up block 0…”
- “Pick up block 1…”
- “Pick up block 2…”
- Inline (
inline for
):
const std = @import("std");
pub fn main() void {
const blocks = [_]u8{ 10, 20, 30 };
inline for (blocks, 0..) |value, index| {
std.debug.print("Block {} has size {}\n", .{index, value});
}
}
- At compile time, the robot thinks:
std.debug.print("Block {} has size {}\n", .{0, 10});
std.debug.print("Block {} has size {}\n", .{1, 20});
std.debug.print("Block {} has size {}\n", .{2, 30});
At runtime, your program just runs those three lines—no looping needed!
- When data is known at compile time. If you're looping over an array, enum, or range that’s fully known during compilation, inline for lets Zig unroll the loop at compile time.
const primes = [_]u32{2, 3, 5, 7, 11};
inline for (primes) |p| {
comptime std.debug.assert(isPrime(p));
}
Each assertion here runs during compilation. If one fails, you get a compile-time error.
-
When you want to eliminate loop overhead at runtime
inline for
expands into individual statements, so there's no runtime cost for iterating. This can matter in performance-critical code like graphics, DSP, or low-level systems. -
When generating code (metaprogramming)
inline for
is great for generating repetitive code safely and cleanly:
const Colors = enum { Red, Green, Blue };
comptime {
inline for (@typeInfo(Colors).Enum.fields) |f| {
pub const handler_@{f.name} = generateHandler(f.name);
}
}
- When initializing complex compile-time structures
const N = 4;
comptime var matrix: [N][N]f32 = undefined;
inline for (0..N) |i| {
inline for (0..N) |j| {
matrix[i][j] = computeEntry(i, j);
}
}
- When you want maintainable and DRY code Instead of copy-pasting similar lines, you keep your code clean by looping over a list and generating code for each item.
When NOT to Use inline for
-
Huge loops (many elements) inline for will literally duplicate the code for every element, which can lead to large binaries and longer compile times.
-
When working with runtime data If the data you're looping over is only available at runtime (e.g., user input or file data), you must use a regular for.
-
When performance is not critical For simple logic where runtime cost is negligible, normal for loops are easier to read and generate smaller binaries.
đź§ Summary Use inline for when:
- All loop data is known at compile time,
- You want the fastest possible code with no runtime overhead,
- You're generating code or doing comptime metaprogramming.
Avoid it when:
- Your loop range is large (e.g., 1000+ items),
- You're working with data only known at runtime,
- You care about binary size or compilation speed more than raw performance.
//
// In addition to knowing when to use the 'comptime' keyword,
// it's also good to know when you DON'T need it.
//
// The following contexts are already IMPLICITLY evaluated at
// compile time, and adding the 'comptime' keyword would be
// superfluous, redundant, and smelly:
//
// * The container-level scope (outside of any function in a source file)
// * Type declarations of:
// * Variables
// * Functions (types of parameters and return values)
// * Structs
// * Unions
// * Enums
// * The test expressions in inline for and while loops
// * An expression passed to the @cImport() builtin
//
// Work with Zig for a while, and you'll start to develop an
// intuition for these contexts. Let's work on that now.
//
// You have been given just one 'comptime' statement to use in
// the program below. Here it is:
//
// comptime
//
// Just one is all it takes. Use it wisely!
//
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";