Generating unique id at comptime

The following is some code I’ve been playing around with. The key function is
getUniqueId, which returns a unique id for any given type:

const std = @import("std");

const Error = error{failure_is_success};

fn fail(comptime _: type) !void {
    return Error.failure_is_success;
}

fn getUniqueNumber(comptime T: type) usize {
    fail(T) catch {
        if (@errorReturnTrace()) |trace| {
            return trace.instruction_addresses[0];
        }
    };
    unreachable;
}

const base_opaque = opaque {};

fn getUniqueId(comptime T: type) usize {
    const base = getUniqueNumber(base_opaque);
    return getUniqueNumber(T) - base;
}

fn Point(comptime T: type) type {
    return struct {
        x: T,
        y: T,
    };
}

pub fn main() void {
    const types = .{
        u32,
        i32,
        []u8,
        [*:0]u8,
        Point(f32),
        Point(f64),
        @TypeOf(.{ .evil_turtles = "on the loose", .stage = 1 }),
        @TypeOf(.{ .evil_turtles = "on the loose", .stage = 2 }),
        @TypeOf(Point),
        opaque {},
        opaque {},
    };
    inline for (1..3) |round| {
        std.debug.print("Round {d}:\n", .{round});
        inline for (types) |T| {
            const id = getUniqueId(T);
            std.debug.print("{s} => {d}\n", .{ @typeName(T), id });
        }
        std.debug.print("\n", .{});
    }
}
Round 1:
u32 => 16
i32 => 352
[]u8 => 688
[*:0]u8 => 1024
opaque.Point(f32) => 1360
opaque.Point(f64) => 1696
struct{comptime evil_turtles: *const [12:0]u8 = "on the loose", comptime stage: comptime_int = 1} => 2032
struct{comptime evil_turtles: *const [12:0]u8 = "on the loose", comptime stage: comptime_int = 2} => 2368
fn (comptime type) type => 2704
opaque.main__opaque_3394 => 3040
opaque.main__opaque_3395 => 3376

Round 2:
u32 => 16
i32 => 352
[]u8 => 688
[*:0]u8 => 1024
opaque.Point(f32) => 1360
opaque.Point(f64) => 1696
struct{comptime evil_turtles: *const [12:0]u8 = "on the loose", comptime stage: comptime_int = 1} => 2032
struct{comptime evil_turtles: *const [12:0]u8 = "on the loose", comptime stage: comptime_int = 2} => 2368
fn (comptime type) type => 2704
opaque.main__opaque_3394 => 3040
opaque.main__opaque_3395 => 3376

Totally hackish but seems to work. Got any idea on how to accomplish the same thing in a cleaner manner?

Cursed. How to do it cleaner? Require that the type gets explicitly declared somewhere, together with other allowed types and then for example use the index. Error if the type wasn't declared. Don't abuse comptime for things it isn't meant for. Another possibility would be to research/work on examples/arguments/code for this proposal for having a type-id https://github.com/ziglang/zig/issues/5459

–

Hmmm, that should be a viable approach. Just recursively collect all the types in to an array first, then do the lookup. Actually, we only need to know the number of types beforehand so we have a large enough array.

–

Maybe merge tuples with ++ and at the end use the tuple to initialize the array with inferred size, or use it directly to convert to the index. Possibly adding a dedupe step that makes sure that duplicate types are removed.

–

1 Answer

1

Since I started this thread, I guess I’m obliged in some way to detail the eventual solution I came up with. So the basic challenge is assigning a unique integer id to a type (for identifying the type outside of Zig). What I did is create a comptime struct that collects information about all the types that my program will encounter:

const TypeDataCollector = struct {
    types: ComptimeList(TypeData),
    functions: ComptimeList(type),
    next_slot: usize = 0,

TypeData is a struct that holds info about a type, including the integer id in question (which I call “slot”):

const TypeData = struct {
    Type: type,
    slot: ?usize = null,
    attrs: TypeAttributes = .{},
};

Plus some important info about the type:

const TypeAttributes = packed struct {
    is_supported: bool = false,
    is_comptime_only: bool = false,
    has_pointer: bool = false,
    known: bool = false,
};

ComptimeList is basically a variable-length array in comptime:

fn ComptimeList(comptime T: type) type {
    return struct {
        entries: []T,
        len: comptime_int,

        fn init(comptime capacity: comptime_int) @This() {
            comptime var entries: [capacity]T = undefined;
            return .{ .entries = &entries, .len = 0 };
        }

        fn concat(comptime self: @This(), comptime value: T) @This() {
            if (self.len < self.entries.len) {
                self.entries[self.len] = value;
                return .{ .entries = self.entries, .len = self.len + 1 };
            } else {
                // need new array
                const capacity = if (self.entries.len > 0) 2 * self.entries.len else 1;
                comptime var entries: [capacity]T = undefined;
                inline for (self.entries, 0..) |entry, index| {
                    entries[index] = entry;
                }
                entries[self.len] = value;
                return .{ .entries = &entries, .len = self.len + 1 };
            }
        }

        fn slice(comptime self: @This()) []T {
            return self.entries[0..self.len];
        }
    };
}

It’s a comptime structure that grows in size as needed. Thanks to it I don’t need to worry about how many types I’m dealing with. It’s used in the following manner:

    fn add(comptime self: *@This(), comptime T: type) void {
        if (self.indexOf(T)) |index| {
            return;
        }
        self.types = self.types.concat(.{ .Type = T });
        switch (@typeInfo(T)) {
           // add child types here
        }
    }

Since TypeDataCollector contains fields with pointers to comptime var, it’s a comptime-only structure. It can only be employed in code that runs only in comptime. You cannot pass it to a function that accepts both comptime and runtime arguments.

In order to actually make use of the info collected by TypeDataCollector, I need a separate struct type:

fn TypeDatabase(comptime len: comptime_int) type {
    return struct {
        entries: [len]TypeData,

        fn get(comptime self: @This(), comptime T: type) TypeData {
            for (self.entries) |entry| {
                if (entry.Type == T) {
                    return entry;
                }
            } else {
                @compileError("No type data for " ++ @typeName(T));
            }
        }
    };
}

TypeDatabase does not have pointers so it’s usable in contexts requiring comptime-known values. Here’s the function in TypeDataCollector that creates an instance of it:

    fn createDatabase(comptime self: *const @This()) TypeDatabase(self.types.len) {
        comptime var tdb: TypeDatabase(self.types.len) = undefined;
        inline for (self.types.slice(), 0..) |td, index| {
            tdb.entries[index] = td;
        }
        return tdb;
    }

Basically, we’re making the data we’ve collected thus far constant from this point on (in comptime). We can therefore pass this struct to functions expecting comptime known variables.

1 Like