Suggestions for generating source code via templates

my use-case has a zig program generating source-code artifacts (including other zig files)… most of these files are relatively small (<50 lines), but do require some amount of simple interpolation of values when writing the output…

i can certainly use print from the std library, especially in combination with a comptime multi-line string that allows me to easily see the forest from the trees… i then pass this string as my fmt to print, along with my args tuple… interpolation is handled with appropriate {...} specifiers in the string…

the problem, of course, is that much of what i generate is source-code full of braces!!!  to use print i have to (tediously) use {{ and }} as an escape… obviously i can live with this – though the compiler errors are quite strange when you forget!!!

before i even consider re-writing the fmt string at comptime, adding the proper escapes, i thought i’d ask what others are doing in these sorts of situations…

i’ve seen some existing templating mechanisms (eg., mustache) used with zig, though i have no particular legacy use with any of these…

were i to attempt something, it would be along the line of “backtick” substitution used in javascript, the shell, etc… details aside, each instance of a “backticked” expression could be moved to the args tuple, while replacing it with some appropriate {..} format specifier…

to simply the problem, i’m fine if all interpolated values can work with {s}… not sure if there is a “generic” specifier other than {any} which does a reasonable job with scalars… i suppose i could assume my interpolated values are scalar, and then “json stringify” them as a first approximation…

whew!!!  not looking to boil the ocean here, just warm-up my bathtub a little :wink:

It depends on what you want to do with those templates, whether you just want to use something pre-existing or something custom.

I think I would pick a character that is illegal in normal Zig syntax and doesn’t occur in any code I want to write in those code templates, something like then you can have that followed by something else for example ◊|name| or ◊(name) that way you can easily pluck it out / replace it as a template parameter.

Then you can transform those template files into Zig files that implement it via Zig code, or you can just directly read, interprete and evaluate those template files depending on what makes more sense.

Transforming the template files to zig modules that just contain functions expecting the parameters, as a build step may be nice, then later build steps can probably use those as generated modules.

1 Like

good suggestion… i’ll probably go with the “on-the-fly” transformation of the template string + args into another string (which i’ll then write to a file)…

a simple example of using this approach would be something like:

const result = applyTemplate("my name is ◊(name)...", .{.name = "bob"});

cleaner in many respects that general interpolation of expressions, in effect creating a local namespace where the bindings are explicit…

Just a note here since I’ve done quite a bit of this type of code generation and run into the same tediousness with doubling the braces. At least for me, I found it less tedious to split the generation into parts that I can write with writer.writeAll which just prints the literal string without requiring any escaping of braces and then only use writer.print for the parts that need interpolation of values.

try writer.print("fn {s}", .{func_name});
try writer.writeAll("() void {\n");
try writer.print("{s}\n", .{func_body});
try writer.writeAll("}");

EDIT: Actually, that last one can be a try writer.writeByte('}') which is more efficient. :^)

4 Likes

agreed… in fact, you’ve suggested a basic design for a simple templated file write:

  • do a writer.writeAll() of the slice starting from the current position, up to the next occurance of the template meta-character;

  • process the slice containing the next interpolation and writer.print() the results to the file; and then

  • rinse-and-repeat

2 Likes

If you are just doing strings, why not make all those writeAll?

try writer.writeAll("fn ");
try writer.writeAll(func_name);
try writer.writeAll("() void {\n");
try writer.writeAll(func_body);
try writer.writeAll("\n}");

if you are concerned about the performance of writeByte vs writeAll, this might be even faster? (runtime shouldnt be much different, but comptime will definitely be faster).

2 Likes