> For the complete documentation index, see [llms.txt](https://wiki.nushackers.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.nushackers.org/hackerschool/introduction-to-zig/language-basics.md).

# Language Basics

The basics of the Zig language are quite straightforward. Given here are examples of each basic concept, which should be picked up and experimented upon.

{% hint style="info" %}
For code examples that don't define a `main`function, please define your own `main`function and paste the code inside in order to run it.
{% endhint %}

## Hello, world!

We have to import the standard library here using `@import("std")`, which we then store in the `std`variable. We can access functions (and types) from the standard library using the `.`syntax.

```zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello, world!\n", .{});
}

```

## Primitives

```zig
// ints
const my_32_bit_int: i32 = -42;
const my_64_bit_int: i64 = -323;
const my_32_bit_unsigned_int: u32 = 3424;
const my_64_bit_unsigned_int: u64 = 34;

// more ints ...?
const my_17_bit_int: i17 = 17;
const my_38_bit_unsigned_int: i38 = 38;

// floats
const my_32_bit_float: f32 = 3.14;
const my_64_bit_float: f64 = 3.14159;

// bool
const my_bool: bool = true;

// string
const my_string: []const u8 = "Hello, world!";

std.debug.print("32-bit int: {}\n", .{my_32_bit_int});
std.debug.print("64-bit int: {}\n", .{my_64_bit_int});
std.debug.print("32-bit unsigned int: {}\n", .{my_32_bit_unsigned_int});
std.debug.print("64-bit unsigned int: {}\n", .{my_64_bit_unsigned_int});
std.debug.print("17-bit int: {}\n", .{my_17_bit_int});
std.debug.print("38-bit unsigned int: {}\n", .{my_38_bit_unsigned_int});
std.debug.print("32-bit float: {}\n", .{my_32_bit_float});
std.debug.print("64-bit float: {}\n", .{my_64_bit_float});
std.debug.print("bool: {}\n", .{my_bool});
std.debug.print("string: {s}\n", .{my_string});
```

## Arrays, Pointers & Slices

### Arrays

Arrays in Zig have a fixed size (defined in the type of the array). There aren't many differences between Zig arrays and those found in C, C++, Java or Go besides syntax.

```zig
// Arrays have a fixed size.
var my_int_array = [5]i32{ 1, 2, 3, 4, 5 };

// You can use the `_` character to have the compiler infer the size.
const my_other_int_array = [_]i32{ 1, 2, 3, 4, 5 };

std.debug.print("my_int_array: {any}\n", .{my_int_array});
std.debug.print("my_other_int_array: {any}\n", .{my_other_int_array});

// `len` is the only field of an array.
std.debug.print("length of my_int_array: {any}\n", .{my_int_array.len});

// Access and modify items within an array using the `[]` syntax.
std.debug.print("my_int_array[1]: {}\n", .{my_int_array[1]});

// Arrays are copied by default.
const my_copied_int_array = my_int_array;
my_int_array[2] = 33;
std.debug.print("my_copied_int_array: {any}\n", .{my_copied_int_array});

// Arrays can have sentinel values.
const my_int_sentinel_array = [5:0]i32{ 1, 2, 3, 4, 5 };

std.debug.print("my_int_sentinel_array: {any}\n", .{my_int_sentinel_array});
std.debug.print("my_int_sentinel_array.len: {any}\n", .{my_int_sentinel_array.len});
std.debug.print("my_int_sentinel_array (with sentinel): {any}\n", .{@as([6]i32, @bitCast(my_int_sentinel_array))});
```

### Pointers

Zig defines two kind of pointers: pointers to a single value, and pointers to multiple values. This is a departure from C and C++, where a pointer to an array with 1000000 integer looks the same as a pointer to a single integer (`int*`).

```zig
// Pointers work similarly to C or C++.
var some_int: i32 = 42;

const some_int_pointer: *i32 = &some_int;

std.debug.print("some_int_pointer: {*}\n", .{some_int_pointer});

// Zig distinguishes between pointers to single values and pointers to multiple values (C-style arrays).
var some_int_array = [3]i32{ 1, 2, 3 };

const single_int_pointer: *i32 = &some_int_array[0];
const many_int_pointer: [*]i32 = &some_int_array;

std.debug.print("single_int_pointer: {*}\n", .{single_int_pointer});
std.debug.print("many_int_pointer: {*}\n", .{many_int_pointer});
std.debug.print("single_int_pointer == many_int_pointer: {}\n", .{@intFromPtr(single_int_pointer) == @intFromPtr(many_int_pointer)});

// Just like arrays, pointers to multiple values can have sentinel values.
var some_int_sentinel_array = [3:0]i32{ 1, 2, 3 };

const many_int_sentinel_pointer: [*:0]i32 = &some_int_sentinel_array;

std.debug.print("many_int_sentinel_pointer[3]: {}\n", .{many_int_sentinel_pointer[3]});
```

{% hint style="info" %}
Notice that the `[*:0]u8`type is perfect for representing strings in C, since they terminate in `\0`and we keep track of a pointer to their first character.
{% endhint %}

### Slices

```zig
var some_array = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Slices are a view into an array.
const some_slice: []i32 = some_array[2..6];
std.debug.print("some_slice: {any}\n", .{some_slice});

// Slices are represented by a pointer to the first element and a length.
std.debug.print("some_slice.ptr: {*}\n", .{some_slice.ptr});
std.debug.print("some_slice.len: {}\n", .{some_slice.len});
std.debug.print("some_slice.ptr == &some_array[2]: {}\n", .{@intFromPtr(some_slice.ptr) == @intFromPtr(&some_array[2])});

// Slices should be treated as pointers to arrays. Modifying the slice modifies the original array.
some_slice[2] = 33;
std.debug.print("some_slice: {any}\n", .{some_slice});
std.debug.print("some_array: {any}\n", .{some_array});

// Slices can be sliced further.
const some_subslice = some_slice[1..3];
std.debug.print("some_subslice: {any}\n", .{some_subslice});

// Just like arrays, slices can have sentinel values.
var some_sentinel_array = [10:0]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

const some_sentinel_slice: [:0]i32 = some_sentinel_array[2..10];
std.debug.print("some_sentinel_slice: {any}\n", .{some_sentinel_slice});
std.debug.print("some_sentinel_slice[8]: {}\n", .{some_sentinel_slice[8]});
```

## Control Flow

### If/else

```zig
const x = 5;
if (x > 7) {
    std.debug.print("x is greater than 7!\n", .{});
} else {
    std.debug.print("x is smaller than or equal to 7...\n", .{});
}

// If-else can also be used as expressions rather than statements.
const y = if (x > 4) 10 else 20;
std.debug.print("y: {}\n", .{y});
```

### Switch

```zig
const x = 34;
switch (x) {
    1...5 => {
        std.debug.print("x is between 1 and 5!\n", .{});
    },
    6...10 => {
        std.debug.print("x is between 6 and 10!\n", .{});
    },
    else => {
        std.debug.print("x is not between 1 and 10...\n", .{});
    },
}

// Switch can also be used as an expression rather than a statement.
const y = switch (x) {
    1...5 => 10,
    6...10 => 20,
    else => 30,
};
std.debug.print("y: {}\n", .{y});
```

### While

```zig
var x: i32 = 0;
while (x < 32) {
    std.debug.print("x: {}\n", .{x});
    x += 1;
}

// You can also pass an expression to perform each iteration.
var y: i32 = 0;
while (y < 32) : (y += 1) {
    std.debug.print("y: {}\n", .{y});
}
```

### For

```zig
var some_array = [_]i32{ 1, 2, 3, 4, 5 };
for (some_array) |item| {
    std.debug.print("array item: {}\n", .{item});
}

// Slices work too!
const some_slice = some_array[1..4];
for (some_slice) |item| {
    std.debug.print("slice item: {}\n", .{item});
}

// You can also iterate over pointers to each element rather than the value.
for (some_slice) |*item| {
    std.debug.print("slice item pointer: {*}\n", .{item});
}

std.debug.print("{any}\n", .{some_slice});
```

## Structs

```zig
const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

const Rect = struct {
    top_left: Point,
    bottom_right: Point,

    // You can define methods in structs.
    fn area(self: Rect) i32 {
        return (self.bottom_right.x - self.top_left.x) * (self.bottom_right.y - self.top_left.y);
    }
};

pub fn main() !void {
    var point1 = Point{ .x = 32, .y = 32 };
    const point2 = Point{ .x = 99, .y = 44 };
    const rect = Rect{ .top_left = point1, .bottom_right = point2 };
    std.debug.print("point1: {any}\n", .{point1});
    std.debug.print("point2: {any}\n", .{point2});
    std.debug.print("rect: {any}\n", .{rect});

    // You can access struct members using `.`. Works for nesting too.
    std.debug.print("point1.x: {}\n", .{point1.x});
    std.debug.print("rect.bottom_right.y: {}\n", .{rect.bottom_right.y});

    // Methods are accessed in a similar way.
    std.debug.print("rect.area(): {}\n", .{rect.area()});

    // Structs are copied.
    point1.x = 99;
    std.debug.print("point1.x: {}\n", .{point1.x});
    std.debug.print("rect.top_left.x: {}\n", .{rect.top_left.x});
}

```

## Enums

```zig
const std = @import("std");

const Color = enum {
    red,
    green,
    blue,
    yellow,
    brown,
    // ...
};

// The integer representation of enums can be overrided.
const Operation = enum(u8) {
    add = 0,
    sub = 1,
    mul = 2,
    div = 3,
    rem = 4,
    shift_left = 5,
    shift_right = 6,

    // Enums can have methods too!
    fn name(self: Operation) []const u8 {
        switch (self) {
            .add => return "add",
            .sub => return "sub",
            .mul => return "mul",
            .div => return "div",
            .rem => return "rem",
            .shift_left => return "shift_left",
            .shift_right => return "shift_right",
        }
    }
};

pub fn main() !void {
    std.debug.print("red: {any}\n", .{Color.red});
    std.debug.print("blue: {any}\n", .{Color.blue});

    std.debug.print("mul: {any}\n", .{Operation.mul});
    std.debug.print("mul (tag value): {}\n", .{@intFromEnum(Operation.mul)});
    std.debug.print("shift_left: {any}\n", .{Operation.shift_left});
    std.debug.print("shift_left (tag value): {}\n", .{@intFromEnum(Operation.shift_left)});

    // You can also use enums in switch statements.
    const some_color = Color.red;
    switch (some_color) {
        .red => std.debug.print("some_color is red\n", .{}),
        .green => std.debug.print("some_color is green\n", .{}),
        .blue => std.debug.print("some_color is blue\n", .{}),
        .yellow => std.debug.print("some_color is yellow\n", .{}),
        .brown => std.debug.print("some_color is brown\n", .{}), // try removing this and compiling
    }
}
```

## Unions

Unions allow you to store one of their members at a time, instead of all at once like in a struct. Zig unions can be treated similarly to C unions, except that they do throw a runtime error if you access the incorrect member (at the cost of a larger runtime size due to storing extra info).

```zig
const std = @import("std");

// We could use an enum to represent a shape, but we can't store any
// shape data within it.
const ShapeEnum = enum {
    circle,
    rectangle,
    square,
};

// We can use a union instead. A union is like a struct, but it can only
// store one of its members at a time, rather than all at once.
const ShapeUnion = union {
    circle: struct { radius: f32 },
    rectangle: struct { width: f32, height: f32 },
    square: struct { size: f32 },
};

const TaggedShapeUnion = union(ShapeEnum) {
    circle: struct { radius: f32 },
    rectangle: struct { width: f32, height: f32 },
    square: struct { size: f32 },
};

// We can also use an automatic enum to tag the union.
const TaggedShapeUnionAutomatic = union(enum) {
    circle: struct { radius: f32 },
    rectangle: struct { width: f32, height: f32 },
    square: struct { size: f32 },
};

pub fn main() !void {
    // We can access members of a union directly.
    const some_rectangle = ShapeUnion{ .rectangle = .{ .width = 3.14, .height = 2.71 } };
    std.debug.print("some_rectangle has width {} and height {}\n", .{ some_rectangle.rectangle.width, some_rectangle.rectangle.height });

    // But what if we do the following???
    //                ------------ try to uncomment the code below -------------
    // std.debug.print("some_rectangle has radius {}\n", .{some_rectangle.circle.radius});

    // Notice how given a shape of type ShapeUnion, we don't know which kind
    // shape it is? We can't switch on it...
    //                ------------ try to uncomment the code below -------------
    // const some_shape = ShapeUnion{ .circle = .{ .radius = 3.14 } };
    // switch (some_shape) {
    //     .circle => std.debug.print("some_shape is a circle of radius {}\n", .{some_shape.circle.radius}),
    //     .rectangle => std.debug.print("some_shape is a rectangle of width {} and height {}\n", .{ some_shape.rectangle.width, some_shape.rectangle.height }),
    //     .square => std.debug.print("some_shape is a square of size {}\n", .{some_shape.square.size}),
    // }

    // We must "tag" the union with an enum to know which kind of shape it is.
    const some_shape = TaggedShapeUnion{ .circle = .{ .radius = 3.14 } };
    switch (some_shape) {
        .circle => std.debug.print("some_shape is a circle of radius {}\n", .{some_shape.circle.radius}),
        .rectangle => std.debug.print("some_shape is a rectangle of width {} and height {}\n", .{ some_shape.rectangle.width, some_shape.rectangle.height }),
        .square => std.debug.print("some_shape is a square of size {}\n", .{some_shape.square.size}),
    }
}
```

## Functions

```zig
const std = @import("std");

pub fn foo(x: i32, y: f32) f32 {
    std.debug.print("inside the foo function... x: {}, y: {}\n", .{ x, y });
    return @as(f32, @floatFromInt(x + 2)) * y;
}

pub fn main() !void {
    std.debug.print("foo(3, 5.34) returned {}\n", .{foo(3, 5.34)});
}
```

## Optionals

```zig
// Zig values can never be `null`, unless they are explicitly marked as optional.
const some_int: i32 = 34;
const some_optional_int: ?i32 = 34;
const some_optional_int_null: ?i32 = null;

std.debug.print("some_int: {}\n", .{some_int});
std.debug.print("some_optional_int: {any}\n", .{some_optional_int});
std.debug.print("some_optional_int_null: {any}\n", .{some_optional_int_null});

// You can check if an optional is null using an if-else, and unwrap it at the same time.
if (some_optional_int) |an_int| {
    std.debug.print("some_optional_int is not null: {}\n", .{an_int});
} else {
    std.debug.print("some_optional_int is null\n", .{});
}

// You can also unwrap it with a default value.
const an_int = some_optional_int orelse 0;
std.debug.print("an_int: {}\n", .{an_int});

// If you know an optional is definitely not null, you can unwrap it using `.?`.
std.debug.print("some_optional_int is definitely not null: {}\n", .{some_optional_int.?});

// But this will crash if you attempt to unwrap a null.
//                ------------ try to uncomment the code below -------------
// std.debug.print("some_optional_int_null is definitely not null: {}\n", .{some_optional_int_null.?});

// Optionals aren't for free. They take up more space.
std.debug.print("size of i32: {}\n", .{@sizeOf(i32)});
std.debug.print("size of ?i32: {}\n", .{@sizeOf(?i32)});

// However, they're free if the underlying value is a pointer! Any guesses why?
std.debug.print("size of *i32: {}\n", .{@sizeOf(*i32)});
std.debug.print("size of ?*i32: {}\n", .{@sizeOf(?*i32)});
```
