2022-05-04: Zig - First Steps

I have started to experiment with Zig.

Installing

On Windows 10, I downloaded the zip file, unpacked it at "C:\\zig" and added "C:\\zig" to PATH. Opening Powershell:

> zig version
0.10.0-dev.2033+3679d737f
> zig zen

 * Communicate intent precisely.
 * Edge cases matter.
 * Favor reading code over writing code.
 * Only one obvious way to do things.
 * Runtime crashes are better than bugs.
 * Compile errors are better than runtime crashes.
 * Incremental improvements.
 * Avoid local maximums.
 * Reduce the amount one must remember.
 * Focus on code rather than style.
 * Resource allocation may fail; resource deallocation must succeed.
 * Memory is a resource.
 * Together we serve the users.

Compiling Hello World

From the documentation, we have the "Hello World" program:

const std = @import("std");                       // <1>

pub fn main() !void {                             // <2>
    const stdout = std.io.getStdOut().writer();   // <3>
    try stdout.print("Hello World", .{});         // <4>
}
  1. The "std" library is imported, and named std.
  2. The return type has an "!" as the function can give an error.
  3. Accesses and names stdout through the std library.
  4. Uses print on stdout. As this can fail, we use a try expression which will make any error abort the function.

Saved into a file "hello.zig", this can be run directly:

> zig run hello.zig
Hello World

One of my favourite features is that we can compile programs into an executable:

> zig build-exe hello.zig
> dir


    Directory: C:\Users\peter\projects\zig


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        04/05/2022     16:47                zig-cache
-a----        04/05/2022     16:47         417280 hello.exe
-a----        04/05/2022     16:47        1011712 hello.pdb
-a----        04/05/2022     16:46            149 hello.zig

The executable size or speed can be controlled using different build modes. These are "Debug", the default, and then three "Release" modes - fast, safe or small. Using the small mode reduces the executable size to kilo-bytes:

> zig build-exe hello.zig -O ReleaseSmall
> dir


    Directory: C:\Users\peter\projects\zig


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        04/05/2022     16:47                zig-cache
-a----        04/05/2022     16:49           7168 hello.exe
-a----        04/05/2022     16:49          86016 hello.pdb
-a----        04/05/2022     16:46            149 hello.zig

User Input: Guessing Game

This is the classic "guess the number" game, where the computer picks a number:

const std = @import("std");

// Displays greeting to stdout
fn printGreeting() !void {
    const stdout = std.io.getStdOut().writer();                             // <1>
    try stdout.print("Guess the Number (from 1 to 100)\n", .{});
}

// Returns a random number from 1 to 100, inclusive
fn chooseTarget() u32 {
    const seed = @intCast(u64, std.time.milliTimestamp());                  // <2>
    var rnd = std.rand.DefaultPrng.init(seed);                              // <3>
    return 1 + rnd.random().uintLessThan(u32, 100);                         // <4>
}

// Return a number from stdin or an error if no number found
fn readNumber() !u32 {
    var number: u32 = 0;
    const stdin = std.io.getStdIn().reader();
    var buf: [10]u8 = undefined;

    if (try stdin.readUntilDelimiterOrEof(buf[0..], '\n')) |input_line| {   // <5>
        var line = input_line;
        // for windows, check if penultimate character is \r
        // if so, remove it
        if (line[line.len - 1] == '\r') {                                   // <6>
            line = line[0..(line.len - 1)];
        }
        number = try std.fmt.parseInt(u32, line, 10);                       // <7>
    }

    return number;
}

// Main function, runs the game
pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    const target = chooseTarget();
    var guess: u32 = 0;

    try printGreeting();

    while (target != guess) {
        try stdout.print("What is your guess? ", .{});
        guess = readNumber() catch {                                        // <8>
            // not a number, so exit
            try stdout.print("Exiting game\n", .{});
            break;
        };

        if (target == guess) {
            try stdout.print("Correct!\n", .{});
            break;
        } else if (target < guess) {
            try stdout.print("Your guess is too high\n", .{});
        } else {
            try stdout.print("Your guess is too low\n", .{});
        }
    }

    try stdout.print("I chose {d}\n", .{target});
}
  1. Separate function to print greetings - still returns an error, and requires stdout writer to be extracted.
  2. Getting a seed relies on the useful @intCast to safely cast between incompatible int types. (NB: on version 0.11.0, the syntax for @intCast has changed, so rewrite this line as @as(u64, @intCast(std.time.milliTimestamp())).)
  3. Creates a random number generator with given seed.
  4. uintLessThan returns a number less than 100, from 0 to 99.
  5. Reads from stdin into buf until the given delimiter is reached - platform specific.
  6. Tidy up the extra delimiter in Windows.
  7. The line can then be parsed, returning an error if it's not a number.
  8. Catch error from readNumber, as any non-numbers are a reason to exit.

Running and interacting looks like:

> .\guess-number.exe
Guess the Number (from 1 to 100)
What is your guess? 50
Your guess is too low
What is your guess? 75
Your guess is too high
What is your guess? 63
Your guess is too high
What is your guess? 56
Your guess is too low
What is your guess? 60
Your guess is too low
What is your guess? 61
Correct!
I chose 61

> .\guess-number.exe
Guess the Number (from 1 to 100)
What is your guess? 50
Your guess is too high
What is your guess? quit
Exiting game
I chose 28

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.