Static comptime variable? (preserve state between two comptime function calls)

Is it possible to preserve some comptime state between comptime function calls?

What I am trying to do is to create a logger wrapper that provides IDs to log messages based on their name in the struct.

const UnnamedLogger = struct {
    logger: *Logger,
};
fn namedLog(This: type) LogFn(This) {
    comptime var LastT: type = This;
    const Static = struct {
        var i: u8 = 0;
    };
    if (This != LastT) {
        Static.i = 0;
        LastT = This;
    }
    const info = @typeInfo(This).@"struct";
    Static.i += 1;

    return (struct {
        const name = info.decls[Static.i].name;
        fn log(
            self: *const This,
            comptime level: LogLevel,
            comptime format: []const u8,
            args: anytype,
        ) void {
            const logger: *const UnnamedLogger = @ptrCast(@alignCast(self));
            logger.logger.log(level, This.name ++ "." ++ name ++ " " ++ format, args);
        }
    }).log;
}

And then usage would be

const Idk = struct {
    logger: *Logger,
    pub const name = "idk";
    pub const streamingBody = namedLog(@This());
    pub const streamingBody2 = namedLog(@This());
};

The issue here is that the stuff with Static.i does not work (and the LastT has the same issue), even though similiar with comptime var works, but the state can not be preserved between calls. And I can’t seem to be able to make any variation on Static.i work.

Is it impossible, or am I just missing something?

PS: I only really want to construct it this way because it is ZLS friendly. I think I should be able to construct my logger wrapper with @Struct but I think I would lose the ZLS autocompletion. So if you know of any zls friendly alternatives, please do let me know. The second best think I can think of is to change the API to

    pub const streamingBody = namedLog(@This(), 1);
    pub const streamingBody2 = namedLog(@This(), 2);

Which works, but maybe just a tiny bit annoying.

related

1 Like

Well, the post talks about capturing comptime var reference at runtime. I can see how something I was doing could be considered that, but check this.

fn Idk() type {
    comptime var counter: struct {
        i: u8 = 0,
        fn get(self: *@This()) u8 {
            self.i += 1;
            return self.i;
        }
    } = .{};
    return struct {
        logger: *Logger,
        pub const name = "idk";
        pub const streamingBody = namedLog(@This(), counter.get());
        pub const streamingBody2 = namedLog(@This(), counter.get());
    };
}

And compiler still says “captured value contains reference to comptime var”. No runtime involved. No comptime pointer being referenced from the created struct (or the factory function). I thought I could somehow manage with @src, but it can’t actually be used in struct declaration even if it is actually in a function. Non comptime var does not work either as it says the counter is not accessible, even though the context should always be comptime.

So I guess I am out of luck, and I will have to manually manage this. At least until there is LSP that undestands types created with @Struct. Which is unfortunate, because this feels like it should have been easy…

Is there really no way at all to find from within the namedLog what it is being assigned to? Any way at all (without manually providing the information).

declarations are evaluated in order of when they are seen used during analysis, so this wouldnt work even if you could have mutable comptime global state.

also this already exists as std.log.scoped(.name)

std.log.scoped(.name) is not nearly sufficient for my needs

yea i figured so after the fact

That is pretty good insight though, now it kinda makes sense why it is not possible to do it that way. Any clue why I can’t use @src in that function creating a structure? That would have probably allowed me to parse the name that is being assigned to.

There’s also one more rule that makes global comptime state impossible.

This does indeed lead to many comptime use cases not being met, but the upside is that all compilation units are independent of order.

@src can only be used in function bodies

I feel like the last example can be hardly considered global comptime state… It certainly feels pretty well contained. When I thought of that I thought it would work for sure… it still feels counterintuitive that the same thing that would likely be trivial with @Struct can’t really be expressed in this more verbose way. Oh well…

I mean, it IS in a function body… Sure, there is a container in between, but still…

even if that was allowed it would give you the function name, not the name of the decl of the struct within the function

Fair, didn’t realize that. Never actually used it and always thought it points to where the @src was called. So there is no way at all to be able to determine what name the value is being assigned to, without actually providing some information manually.

that is correct

Actually the @src does point to the place where it was called. Multiple @src in the same function do give different results. So if the @src was usable there, it would allow me to determine what it is being assigned to.

no thats because monomorphisation changes the function name, i.e its the comptime parameters not the place its being assigned to.

Well, I thought the file field it returns is actually the file content so that I would be able to parse the decl name. Now that I am looking at it its just file name. Still, I would have probably been able to do something like

    const start = @src();
    return struct {
        logger: *Logger,
        pub const name = "idk";
        pub const streamingBody = namedLog(@This(), start, @src());
        pub const streamingBody2 = namedLog(@This(), start, @src());
    };

And infer the decl index from the line numbers. But damn, would that be hacky and fragile.

So I ended up duplicating the decl name into the function factory and adding a comptime test for that. The comptime test was very tricky to get right, took me maybe 2 hours, because there is some weirdness in how strings are compared in comptime and so the newly created function in the test was different from the function created outside of the test. The solution was to do this in the factory function: instead of using provided name directly, find the decl with that name from @This(), and use the decl.name. If I had gone the index based way I wouldn’t have to deal with that gotcha but there are issues with index based that I was unhappy with.

fn testLogger(This: type) void {
    comptime {
        const info = @typeInfo(This).@"struct";

        for (info.decls[1..]) |decl| {
            if (@field(This, decl.name) != namedLogStr(This, decl.name)) {
                @compileError(This.name ++ "." ++ decl.name ++ " has incorrectly specified id. Must be ." ++ decl.name);
            }
        }
    }
}
const Idk = struct {
    logger: *Logger,
    pub const name = "idk";
    pub const streamingBody = namedLog(@This(), .streamingBody);
    pub const streamingBody2 = namedLog(@This(), .streamingBody); // error: idk.streamingBody2 has incorrectly specified id. Must be .streamingBody2
    comptime {
        testLogger(@This());
    }
};