A really nice introduction to comptime reflection / metaprogramming in zig.
Zig’s comptime is still a world onto itself
I’m still not very comfortable with comptime
Oh, yeah… The only thing about comptime
I personally was able to grasp on the fly was it’s use for constructing generic data types (when a function returns a struct).
I found comptime quite intuitive but struggled with some anyopaque and interface patterns at first.
Theres also a pretty funny discord thread from my early low level journey where I asked if recursive data structures can just “get memory” because all I saw was that the linkedlist api doesnt require an allocator.
My main lang is C, so type-erased (“generic”) pointers are things I’m very comfortable with (and it’s not about meta programming and reflection). Just in case, there was a topic where we discussed C’s void*
and Zig’s *anyopaque
types.
Yea by now I understand them too.
My background is first a year of Java and then half a year of go before I started with Zig. I learned Java at High School. Idk why but I guess I just didnt code for long enough to have “mental barriers” in terms of what I assumed would usually be possible in programming, making me question comptime less.
By now it feels super intuitive, my favourite use so far was probably generic parsers and serializers!
Functions that begin with
@
are builtin functions
BTW, If I understand right, there are at least two kinds of builtin functions.
Some of them (@memcpy
, @memset
, @rem
…) are executed in runtime
(or a code the compiler generated when processing these calls is executed in runtime)
and some (@Type
, @field
…) are executed in compile time only. Is that true? If so it would be nice if the solid list of all builtin functions in official language documentation was split into some subcategories.
From my understanding, the builtins are all run at compile time. The difference is what they produce. @memcpy
and friends are replaced with runtime code with the correct behavior. @Type
and friends provide comptime informatiom used by the compiler to generate code.
I think @intFromPtr
can only be runtime because you can’t know the assigned memory address at compile time.
But I think it’s has zero run time, it’s just a type cast, an instruction to compiler.
The general question is “can builtin functions be classified somehow?”
For ex.
@typeOf()
and friends are stuff for (comptime) reflection / metaprogramming- things like
@bitCast()
are definitely not for metaprogramming.
I’ve looked through Karl’s post one more time and collected builtin functions related to type reflection, here they are:
@hasField()
@hasDecl()
@field()
@typeInfo()
@typeName()
@TypeOf()
Anything else?
Anyhow this is one of the categories of builtin functions, “Type reflection / metaprogramming” or so.
I am considering as part of the reflection/metaprogramming
also: @offsetOf, @sizeOf, @bitSizeOf, @alignOf, @src, etc.
There are a lot of bit arithmetic (bit count, shift) and arithmetic/mathematic on integers, floats and vectors.
There are a lot of casting and convertion
.
There are C helpers (for variable arguments, etc).
There are for atomics
(@atomicLoad, @fence, etc)
There are string
manipulation (@memcpy, @memset)
There are debugging
(@panic, @breakpoint, @compileLog, etc)
There are the calling ones: @export, @extern, @call
The @import is in its own category
And the @workGroupId, @workGroupSize, @workItemId is in the WhatIsThis? category (looks like OpenCL concepts but I really don’t know what these do).
Yesterday I looked at these and did not understand what are they for.
I would add these:
@This() // Reflects the constructed container type
@unionInit() // Equivalent of @field for union variants
@tagName() // Useful with @unionInit
There are a handful of others which are useful in reflection/metaprogramming contexts, it’s hard to know where to draw the line. Those three I would say it’s their primary purpose.
usual situation when trying to classify some things
I’d put @embedFile
together with @import
and @cInclude
for interacting with other files.
That’s an interesting exercise. I would classify them as:
- Reflection - people have mentioned all of these already.
- Casting
- Compilation - error ang log, but also the
import
,extern
,export
,cDefine
andcInclude
- Low-level instructions. These are intended to translate one-to-one to a CPU instruction. Can be subdivided into:
- Optimizing - popcount, bitshifts.
- Debugging - breakpoint (translates to an interrupt).
- Atomics
memcpy
and memset
might fit into the low-level → optimizing category, because they are intended to leverage low level instructions to move large amounts of data quickly.
I don’t know why panic
is a builtin. We could just call the platform specific code for ending the process.
These can also be subdivided (or even be put into it’s own category).
- memory re-interpretation (
@bitsCast
,@ptrFromInt
(?),@ptrCast
(?)) - real conversion / transformation (
@intFromFloat
) - ?
@panic
is a built-in so that it can call a program-defined panic function if one is provided:
Invokes the panic handler function. By default the panic handler function calls the public
panic
function exposed in the root source file, or if there is not one specified, thestd.builtin.default_panic
function fromstd/builtin.zig
.Generally it is better to use
@import("std").debug.panic
. However,@panic
can be useful for 2 scenarios:
- From library code, calling the programmer’s panic function if they exposed one in the root source file.
- When mixing C and Zig code, calling the canonical panic implementation across multiple .o files.