Zig Meta Programming

published: [nandalism home] (dark light)

Zig Meta Programming

I will skip the intro defining meta-programming (see my previous meta-programming post). Unlike Lisp (and Nim) Zig doesn't explicitly have compile-time code generation. However, zig has something unique, which, while type-based is very different from C++/D's template programming. zig calls this comptime. I would describe it as very aggressive constant folding in the compiler.

If zig can evaluate something at compile time it will be evaluated. The resulting programming style blends the compile time code and the run time code together, in a way which, for me at least, at first hid the true power of zig comptime.

Generating SQL Prepare Statements

This example is not as difficult as my previous meta-programming example. I have made progress on a version of that for zig but so far it's restricted to comptime values i.e. values known completely at compile-time (constants).

Instead the motivating example this time is generating sql prepare statements for a long list of sql. Prepare? Briefly, database api's separate the pre-processing/parsing of sql-language statements and their use later, much like regex libraries allow one to pre-compile regex patterns rather than parsing/interpreting them every time they are matched.

I aim do the following:

This can be quite tedious for a long list of sql statements and I usually define a macro to help me. Here is the desired interface.

const statements = .{
// variable-name, sql-statement
.{"add_it", "insert into t1 (x,y,z) values (?,?,?)"},
.{"list_special", "select id,a,b,c from t1 where a>?"},
.{"del_all", "delete from t1"},
.{"del_special", "delete from t1 where a>?"},
...
};

The macro should generate a set of variables and two functions, say prepare_all and finalize_all.

The Zig Macro

Generating top level code doesn't seem to be possible with zig comptime, so instead of generating a set of global variables, I generate a struct-type, with a field where I would have had a global variable. Then I manually instance that struct (a single line which is unchanged when the statement list is changed).

I also use 2 separate function templates for prepare_all and finalize_all. They are not completely generated by the macro. So it's more like 3 macros. I have put the main macro in a separate file to hide it from the user and I could probably move the 2 function templates into that separate file too (with some awkwardness).

So the final code I use (apart from the statement list above) looks like this:

const AllStatements = macros.varcontainer_struct(sl.Stmt, statements);
var st: AllStatements = undefined;

fn prepare_all() !void {
  inline for(statements) |x| {
    @field(st, x[0]) = try sl.prepare(x[1]);
  }
}

fn finalize_all() !void {
  inline for(statements) |x| {
    try sl.finalize(@field(st, x[0]));
  }
}

The inline for above gives a glimpse into comptime possibilities. The sl prefix is for my sqlite3 module and isn't really relevant.

Finally the scary part. This is the code which generates a struct with one field per statement each of type sqlite3.Stmt, those fields which are used above in prepare_all/finalize_all.

pub fn varcontainer_struct(comptime FieldType: type, comptime pairs: anytype) type {
    const ze: std.builtin.Type.StructField = undefined;
    var fields = [_]std.builtin.Type.StructField{ze}**pairs.len;
    inline for(pairs) |x,i| {
      fields[i] = .{ .name = x[0], .type = FieldType, .default_value = null, .is_comptime = false, .alignment = 0 };
    }
    var Decl = std.builtin.Type{
      .Struct = .{
        .layout = .Auto,
        .fields = &fields,
        .decls = &[_]std.builtin.Type.Declaration{},
        .is_tuple = false,
      },
    };
    return @Type(Decl);
}

Yes, indeed... An overview of what happens here may help allay your frights.

It was a fascinating, intellectual puzzle, going from my pre-conceived notion of generating a list of global variables and two functions to the final, working, zig implementation, which was quite different but achieves basically what I wanted.

Finally, the generated variables (struct fields) are used as follows:

  if(try sl.query_one(st.list_one, .{id})) |row| {...

site built using mf technology