Comptime gensym

Lisp has a gensym function that gives me unique names each time I call it.

I tried to implement the same in Zig, after searching for comptime counter solutions. The following doesn’t work but is there a way to make it work?

const std = @import("std");

const Error = std.fmt.BufPrintError;

pub fn gensym(comptime name: []const u8) []const u8 {
    comptime var counter: struct {
        value: usize = 0,

        fn update(self: *@This()) void {
            self.value += 1;
        }
    } = .{};
    comptime counter.update();
    return std.fmt.comptimePrint("{s}{d}", .{ name, counter.value });
}

pub fn main() void {
    inline for (0..10) |_| {
        const name = gensym("foo");
        std.debug.print("value: {s}\n", .{name});
    }
}

you could make random symbols with std.Random.DefaultPrng which can be used at comptime. maybe like this:

const std = @import("std");

pub fn gensym(comptime rand: std.Random, comptime len: u16) [len]u8 {
    comptime {
        var buf: [len]u8 = undefined;
        const alphabet = "abcdefghijklmnopqurstuvwxyz";
        for (&buf) |*c| c.* = alphabet[rand.intRangeLessThan(u8, 0, alphabet.len)];
        return buf;
    }
}

pub fn main() void {
    comptime var prng = std.Random.DefaultPrng.init(0);
    const rand = comptime prng.random();
    const iters = 100;
    @setEvalBranchQuota(iters * 300);
    inline for (0..iters) |_| {
        const name = comptime gensym(rand, rand.intRangeAtMost(u16, 2, 10));
        std.debug.print("value: {s}\n", .{name});
    }
}
1 Like

related:

I think easiest would be to use a unique prefix for every callsite and then use the index from the for loop:

const std = @import("std");

pub fn gensym(comptime name: []const u8, count: comptime_int, comptime src: ?std.builtin.SourceLocation) []const u8 {
    return if (src) |loc|
        std.fmt.comptimePrint("{s}{d}@{s}:{s}:{d}:{d}", .{ name, count, loc.file, loc.fn_name, loc.line, loc.column })
    else
        std.fmt.comptimePrint("{s}{d}", .{ name, count });
}

pub fn main() void {
    inline for (0..3) |i| {
        const name = gensym("foo", i, null);
        std.debug.print("value: {s}\n", .{name});
    }

    inline for (0..5) |i| {
        const name = gensym("bar", i, @src());
        std.debug.print("value: {s}\n", .{name});
    }
}
value: foo0
value: foo1
value: foo2
value: bar0@gensym.zig:main:17:39
value: bar1@gensym.zig:main:17:39
value: bar2@gensym.zig:main:17:39
value: bar3@gensym.zig:main:17:39
value: bar4@gensym.zig:main:17:39

If you have multiple layers of calls you could pass in the start index as a comptime parameter and return the end index, then thread the value through or you could have a locally scoped comptime var counter = 0 that gets increased for every nested call.

Or you even could use 1 “parent” gensym that is used as name to generate new ones. For example if you add a delimiter between the name and the count you could generate names like: foo_1, foo_2 and then use that to generate names foo_1_1, foo_1_2, foo_1_3, foo_2_1, etc.

Or if you use @src even names that contain multiple source locations and indizies. In the best case the name would describe in which nested inline for loop it was created to make debugging easier.