/home/huy/everyday

everyday: Development Log

01.06.2022 - Zig/Comptime

One of the unique features of Zig is the ability to let us control what happens in our code, during both runtime and compile time.

For optimization, Zig implicitly performs some evaluations at compile-time, for example, in the case of const.

All numeric literals const is of type comptime_int or comptime_float, and when we use these variables in the program, the values will be inserted at compile time instead of the variable itself.

So we can declare numeric constants without the need to specify their type:

const foo = 1234;
const bar = 12.34;

For mutable variables, defined with var, you can't do this, the following definition will lead to compile error:

var foo = 1234;
// error: variable of type 'comptime_int' must be a const or comptime
foo = 2234;

It's because Zig assumes that an identifier declared with var will be modified at runtime. And for some variables to be modified at runtime, Zig needs to allocate some memory area for it. An exact memory size needed for the allocation is required, so we must specify a runtime type (i32 or f32,...) for that variable.

var foo: i32 = 1234;
foo = 2234;

We can explicitly request Zig to compute the value of this variable (and all the modifications) at compile time instead of runtime, by appending comptime keyword before the declaration:

comptime var foo = 1234;
print("{}\n", .{foo});
foo = 2234;
print("{}\n", .{foo});

In the above example, the value of foo is now calculated and modified during compile time, any usage of foo in the code will be replaced by its value at each usage time.

// foo after compile
print("{}\n", .{1234})
print("{}\n", .{2234});

We can also use comptime in function parameters, this opened up a lot of possibilities.

For example, let's take a look at this impractical example, just to see the concept, we can catch some logic errors at compile time!

The discount function returns the discounted price from an original price and the discount percent. We can use comptime to prevent the discount function to be called with any percent larger than 100%:

fn discount(price: f32, comptime percent: f32) f32 {
    if (percent > 100) @compileError("Are you nut?");
  return price - (price * percent / 100);
}

Calling this function with some unreasonable discount percent will throw compile error!

var price: f32 = discount(250, 125);
// error: Are you nut?

Another use case of comptime in function parameters is to pass a type into a function, it's actually how Zig implements generics:

fn makeArray(comptime T: type, size: usize) []T {
  ...
}

const arr1 = makeArray(u8, 10);
const arr2 = makeArray(f32, 5);
const arr3 = makeArray(i32, 10);

For every type T passed to the makeArray function, a separate copy of makeArray for that type will be generated at compile time.

Since type is also first-class in Zig, we can define generic data structures by create a function that returns a type:

fn List(comptime T: type) type {
  return struct {
    items: []T,
    len: usize,
  };
}

List is called a generic data structure, and we can create new list types by calling List(i32), List(u8),...

const NumberList = List(i32);
var list: NumberList = ...;

For a more in-depth discussion about comptime, be sure that you check out Zig's documentation.

Read more