Comptime unique IDs

i’ve come to appreciate the challenges in maintaining a global comptime counter as well as creating unique IDs for type

in my use-case, i just need a program-wide unique ID – and one whose “seed” is not necessarily a unique type…

'in fact, i’ve been starting to deploy techniques to ensure that instances of comptime-defined structs are indeed unique (even if initialized identically)… in certain situations, i can maintain a local comptime var which i’ll use as a counter… ideally, i’d simply draw upon some comptime unique ID instead… (a μs timestamp would work for me)

what are my options???

This sounds like you are doing something that is not intended to work.

Global counters at comptime if they work somehow, are a bug that you are exploiting. Using such bugs, are counter to the efforts of getting Zig to a state where it can incrementally compile things and compile more things in parallel.

Instead of doing that sort of thing, write code that explicitly registers and collects types in an array and use the index of the type in the array.

I think if you can’t do that, then comptime might not be the right level to implement your features, that is why it isn’t the only way to do meta programming. Instead of exploiting comptime, rethink it, maybe use the buildsystem.

What you describe sounds brittle and like it tries to make things work magically. Instead just write code that actually does things, instead of gluing everything up within the implementation, to then put a pretty interface on top of it.

Throw away the pretty interface and get the pipes simple and clear, write libraries, not frameworks where nobody wants to look at the plumbing.

This is my personal opinion, but this contorting the language into another pseudo language which then is called a framework, seems to bring all kinds of problems.

My 2 cents: either write an actual language implementation or write libraries, not this strange mix. I think you are trying to squeeze your solution into Zig at the wrong level, use buildsteps, or code generation, not a hacked version of comptime.

3 Likes

my problem isn’t unique types but rather unique instances of some common struct type that are otherwise identically initialized…

i’ve tried to use the technique suggested by @AndrewCodeDev in this post… it definitely works for me in some but not all situations…

another approach is to have an .id field within my struct type, which i would then set to a distinct value per instance… this would make it patently clear that each instance of my struct are truly unique in the eyes of the compiler…

but then, i need a unique identifier at comptime :wink:

If those instances are otherwise the same why does it matter whether they are unique or identical?
Do you mutate them at run-time?
Do you use them as keys somewhere?

If you create an array of 5 elements you will get 5 elements.
It is hard to understand why you need this and for what.

It seems like you want to create instances of structs at comptime all scattered around the code base and then use some magic counter to wire them up.

What I am saying is they shouldn’t be scattered all over the place to begin with, that seems like a symptom of trying to make things work through magic, instead of letting the user call some init function that setups some initial state.

yes, i mutate them at runtime… that was the issue that @AndrewCodeDev solved in another post…

but for whatever reason, this technique doesn’t seem to do the trick inside an explicit comptime block within a bulkInit function that is creating many instances of many distinct types…

within the comptime block of bulkInit, i can indeed maintain a local var which i can increment; and all of the instances created within a single call to this function are indeed unique…

my problem is that i call this bulkInit init functions from multiple call-sites – and it’s not at all obvious how to ensure this function’s local comptime var starts off with a unique value on each call…

This feels like playing 20 questions.

Why does it need a unique value?
What does it produce that uses it?

How about passing the function a unique value?
(Instead of having a magical comptime generate next id, you could just have some build options that allow a user to customize upper limits like max_types, max_callbacks, max_foo, then you can pass the thing that creates stuff for types 0, the next gets 0+max_types, the next 0+max_types+max_callbacks, and so on, that is one option if you can have some kind of upper limit, but difficult to suggest alternative solutions without knowing what the actual goal is)

What is the purpose? Are you trying to pack things together into memory?
Are you trying to associate things, etc?

here’s a self-contained example:

const std = @import("std");
const print = std.log.info;

const CA = struct {
    s: *S,
};

const CB = struct {
    s: *S,
    s2: *S2,
};

const S = struct {
    x: u32 = 10,
};

const S2 = struct {
    y: u32 = 5,
};

fn create(T: type, key: anytype) *T {
    return &struct {
        var o = blk: {
            std.mem.doNotOptimizeAway(key);
            break :blk std.mem.zeroInit(T, .{});
        };
    }.o;
}

fn initC(CT: type) CT {
    comptime {
        var new_c: CT = undefined;
        const cti = @typeInfo(CT);
        for (cti.Struct.fields) |fld| {
            const fti = @typeInfo(fld.type);
            const FT = fti.Pointer.child;
            const fval = create(FT, opaque {});
            @field(new_c, fld.name) = fval;
        }
        const res = new_c;
        return res;
    }
}

const ca = initC(CA);
const cb = initC(CB);

pub fn main() void {
    ca.s.x = 20;
    print("ca.s.x = {d}", .{ca.s.x});
    print("cb.s.x = {d}", .{cb.s.x});
    print("cb.2.y = {d}", .{cb.s2.y});
}

and the output is:

info: ca.s.x = 20
info: cb.s.x = 20
info: cb.s2.y = 5

clearly CA and CB are distinct types with different fields… but the create function used to instantiate values for fields of type *S and *S2 (suggested by @AndrewCodeDev in this post) doesn’t appear to yield a distinct instance of the s: *S field in CA and CB

how can ensure that my modification of cb.s.x doesn’t ALSO clobber ca.s.x???

This fixes it:

const std = @import("std");
const print = std.log.info;

const CA = struct {
    s: *S,
};

const CB = struct {
    s: *S,
    s2: *S2,
};

const S = struct {
    x: u32 = 10,
};

const S2 = struct {
    y: u32 = 5,
};

fn create(T: type, name: []const u8, key: anytype) *T {
    return &struct {
        var o = blk: {
            std.mem.doNotOptimizeAway(key);
            std.mem.doNotOptimizeAway(name);
            break :blk std.mem.zeroInit(T, .{});
        };
    }.o;
}

fn initC(CT: type) CT {
    comptime {
        var new_c: CT = undefined;
        const cti = @typeInfo(CT);
        for (cti.Struct.fields, 0..) |fld, i| {
            const fti = @typeInfo(fld.type);
            const FT = fti.Pointer.child;
            const fval = create(FT, @typeName(CT), i);
            @field(new_c, fld.name) = fval;
        }
        const res = new_c;
        return res;
    }
}

const ca = initC(CA);
const cb = initC(CB);

pub fn main() void {
    ca.s.x = 20;
    cb.s.x = 25;
    print("ca.s.x = {d}", .{ca.s.x});
    print("cb.s.x = {d}", .{cb.s.x});
    print("cb.2.y = {d}", .{cb.s2.y});
}

Unfortunately, I don’t have a lot of time to automate this process. The thing is, @Sze is right that this process is starting to look brittle to me. At this level, I would start using file generation instead of comptime.

1 Like

Is that, in fact, combining doNotOptimizeAway with comptime execution?

I have trouble modeling what that even does! I thought doNotOptimizeAway emitted some ASM, how does that, comptime?

I agree that it would be a mistake to expect whatever is going on there to keep working, I’m impressed that you came up with it in the first place.

1 Like

I had the same reaction, it turns out it is an empty return at comptime. I assume the anytype parameter is producing the current behavior, but I have no idea if that is intended to continue.

pub fn doNotOptimizeAway(val: anytype) void {
    if (@inComptime()) return;
1 Like

I suspect that for how things are currently working, it just matters if the arguments are being used at all (we’re depending on the create function to be monomorphized into unique instances based on the supplied comptime arguments). You and @permutationlock are both correct in the fact that it essentially does nothing. I also do not anticipate that this behaviour will continue through future compiler iterations.

1 Like

Having a “counter var” local to a comptime block is absolutely legitimate, but as others have pointed out any “global” counter would be a violation of the model of comptime as it’s being designed currently. People used to leverage leaks in the abstraction to implement a userland borrow checker even :^)

Just like you can’t build a comptime borrow checker anymore, any similar hole will be plugged moving forward.

I’ve toyed with your code sample (the one posted further down in the thread) but wouldn’t find a reasonable way to make it work.

My recommendation is to turn some of that logic into Zig code generated programmatically that then can be imported as a module in your runtime-optimized executable.

If you figure out a not too painful way of getting the code generation part to work, the final integration step should be complete smooth sailing (ie I don’t expect it to have restrictions that catch you by surprise as you try to make it work).

we’re losing sight of my fundamental issue… and i apologize for bringing up “Comptime unique IDs”, as i was already attempting to solve my problem prematurely…

to that end, let’s clean the slate… i just add this NEW post which hopefully states my underlying issue ore clearly :wink: