Zig Metaprogramming#
Overview#
Zig's metaprogramming is driven by a few basic concepts:
- Types are valid values at compile-time
- most runtime code will also work at compile-time
- struct field evaluation is compile-time duck-typed
- the zig standard library gives you tools to perform compile-time reflection
- examples:
-
multiple dispatch
Zigconst std = @import("std"); fn foo(x : anytype) @TypeOf(x) { // note that this if statement happens at compile-time, not runtime. if (@TypeOf(x) == i64) { return x + 2; } else { return 2 * x; } } pub fn main() void { var x: i64 = 47; var y: i32 = 47; std.debug.print("i64-foo: {}\n", .{foo(x)}); std.debug.print("i32-foo: {}\n", .{foo(y)}); } -
generic Types
Zigfn Vec2Of(comptime T: type) type { return struct{ x: T, y: T }; } const V2i64 = Vec2Of(i64); const V2f64 = Vec2Of(f64); pub fn main() void { var vi = V2i64{.x = 47, .y = 47}; var vf = V2f64{.x = 47.0, .y = 47.0}; std.debug.print("i64 vector: {}\n", .{vi}); std.debug.print("f64 vector: {}\n", .{vf}); }
Compile Time Execution#
- Blocks of code may be forcibly executed at compile time using the
comptimekeyword. In this example, the variables x and y are equivalent.
test "comptime blocks" { var x = comptime fibonacci(10); var y = comptime blk: { break :blk fibonacci(10); }; } - Integer literals are of the type
comptime_int. These are special in that they have no size (they cannot be used at runtime!), and they have arbitrary precision.comptime_intvalues coerce to any integer type that can hold them. They also coerce to floats. Character literals are of this type.
test "comptime_int" { const a = 12; const b = a + 10; const c: u4 = a; const d: f32 = b; } -
comptime_floatis also available, which internally is anf128. These cannot be coerced to integers, even if they hold an integer value. -
function parameters in Zig can be tagged as being
comptime, meaning value passed must be known at compile time
fn Matrix( comptime T: type, comptime width: comptime_int, comptime height: comptime_int, ) type { return [height][width]T; } test "returning a type" { expect(Matrix(f32, 4, 4) == [4][4]f32); } Gotchas/Surprises#
- no peer type resolution in comptime execution
- all comptime values do not obey usual lifetime rules;
- have "static" lifetimes (can think of values as garbage collected)
anytypestruct fields are allowed- turns the struct into a comptime type
-
NOTE: allows the type of the field to be mutable
Zigconst ArgTuple = struct { tuple: anytype = .{}, }; var arg_list = ArgTuple{}; for (args) |arg| { if (@TypeOf(arg) == ?u21) { if (arg) |cp| { arg_list.tuple = arg_list.tuple ++ .{ctUtf8EncodeChar(cp)}; } else { arg_list.tuple = arg_list.tuple ++ .{"null"}; } } else if (@TypeOf(arg) == u21) { arg_list.tuple = arg_list.tuple ++ .{ctUtf8EncodeChar(arg)}; } else { arg_list.tuple = arg_list.tuple ++ .{arg}; } } -
can use
comptime varto create compile time closures - uses anonymous struct literals to avoid compiler caching
- can be buggy in complex cases
- Look at zorrow, a simple rust-like borrow checker implemented using this technique
Reflection#
- Types in Zig are values of the type
type, only available at compile time
test "branching on types" { const a = 5; const b: if (a < 10) f32 else i32 = 5; } -
can reflect upon types using the built-in
@typeInfo, which takes in atypeand returns a tagged union. -
tagged union type can be found in
std.builtin.TypeInfo(info on how to make use of imports and std later)
fn addSmallInts(comptime T: type, a: T, b: T) T { return switch (@typeInfo(T)) { .ComptimeInt => a + b, .Int => |info| if (info.bits <= 16) a + b else @compileError("ints too large"), else => @compileError("only ints accepted"), }; } test "typeinfo switch" { const x = addSmallInts(u16, 20, 30); expect(@TypeOf(x) == u16); expect(x == 50); } -
can use the
@Typefunction to create a type/reify from a@typeInfo.@Typeis implemented for most types but is notably unimplemented for enums, unions, functions -
anonymous struct syntax is used with
.{}, because theTinT{}can be inferred. In this example we will get a compile error if theInttag isn’t set
fn GetBiggerInt(comptime T: type) type { return @Type(.{ .Int = .{ .bits = @typeInfo(T).Int.bits + 1, .signedness = @typeInfo(T).Int.signedness, }, }); } test "@Type" { expect(GetBiggerInt(u8) == u9); expect(GetBiggerInt(i31) == i32); } Generic Types#
-
Generic types are specified through explicit parametric type constructor functions
-
Returning a struct type is how you make generic data structures in Zig. The usage of
@Thisis required here, which gets the type of the innermost struct, union, or enum. Herestd.mem.eqlis also used which compares two slices.
fn Vec( comptime count: comptime_int, comptime T: type, ) type { return struct { data: [count]T, const Self = @This(); fn abs(self: Self) Self { var tmp = Self{ .data = undefined }; for (self.data) |elem, i| { tmp.data[i] = if (elem < 0) -elem else elem; } return tmp; } fn init(data: [count]T) Self { return Self{ .data = data }; } }; } const eql = @import("std").mem.eql; test "generic vector" { const x = Vec(3, f32).init([_]f32{ 10, -10, 5 }); const y = x.abs(); expect(eql(f32, &y.data, &[_]f32{ 10, 10, 5 })); } - The types of function parameters can also be inferred by using
anytypein place of a type.@TypeOfcan then be used on the parameter.
fn plusOne(x: anytype) @TypeOf(x) { return x + 1; } test "inferred function parameter" { expect(plusOne(@as(u32, 1)) == 2); } - Comptime also introduces the operators
++and**for concatenating and repeating arrays and slices. These operators do not work at runtime.
test "++" { const x: [4]u8 = undefined; const y = x[0..]; const a: [6]u8 = undefined; const b = a[0..]; const new = y ++ b; expect(new.len == 10); } test "**" { const pattern = [_]u8{ 0xCC, 0xAA }; const memory = pattern ** 3; expect(eql( u8, &memory, &[_]u8{ 0xCC, 0xAA, 0xCC, 0xAA, 0xCC, 0xAA } )); } Examples#
Generic Types through Functors#
-
The function returns a
type, which means it can only be called at comptime. It defines two structs:
Zigfn LinkedList(comptime T: type) type { return struct { pub const Node = struct { prev: ?*Node = null, next: ?*Node = null, data: T, }; first: ?*Node = null, last: ?*Node = null, len: usize = 0, }; } -
main
LinkedListstruct Nodestruct, namespaced inside the main struct- structs can namespace functions and variables
- useful for introspection when creating composite types
Zig// To try this code, paste both definitions in the same file. const PointList = LinkedList(Point); const p = Point{ .x = 0, .y = 2, .z = 8 }; var my_list = PointList{}; // A complete implementation would offer an `append` method. // For now let's add the new node manually. var node = PointList.Node{ .data = p }; my_list.first = &node; my_list.last = &node; my_list.len = 1;
Dynamic specialization#
- type
anytypebinds to anything
fn makeCoupleOf(x: anytype) [2]@TypeOf(x) { return [2]@TypeOf(x){ x, x }; } - allows specialization based on call types
fn ReturnType(comptime T: type) type { comptime var info = @typeInfo(T); if (info == .Int) { info.Int.bits /= 2; return @Type(info); } else { return T; } } pub fn sqrt(x: anytype) ReturnType(@TypeOf(x)) { const T = @TypeOf(x); switch (@typeInfo(T)) { .ComptimeFloat, .Float => return @sqrt(x), .ComptimeInt => { if (x < 0) { @compileError("sqrt on negative number"); } return T(sqrtInt(u128, x)); }, .Int => return sqrtInt(T, x), else => @compileError("not implemented for " ++ @typeName(T)), } }