Names of types from generating functions

The name of a type returned from a function depends on too much. If I build a type from the return value of a function, I expect the type to only capture that return value, but it seem to capture the entire function.

For example, if I wanted to build a Boxed(u32) and I have a function to generate it that uses the return value of a function so I can bury the type deduction inside the generating functiion, it shouldn’t matter what the function is only what it returns.

Taking this example a step more, if I wanted to generate the Boxed(type) from a common type of two values (find a common coercion), it makes the order you supply to the generator important when the actual type returned is the exact same fn gen_common_boxed_type(x: anytype, y:anytype) type depends on irrelevant argument order.

const std = @import("std");

fn aident(T: type) type {
    return T;

fn bident(T: type) type {
    return T;

fn Sized(comptime f: fn(type) type, T: type) type {
    return struct {
        pub const S = f(T);
        x: S,

pub fn main() void {
    const atype = Sized(aident, u32);
    const aatype = Sized(aident, u32);
    const btype = Sized(bident, u32);

    const a = @typeName(atype);
    const b = @typeName(btype);

    const pr = std.debug.print;
    pr("a -> {s}\n", .{a});
    pr("b -> {s}\n", .{b});
    pr("A == B -> {}\n", .{atype == btype});
    pr("A == AA -> {}\n", .{atype == aatype});


a -> example.Sized((function 'aident'),u32)
b -> example.Sized((function 'bident'),u32)
A == B -> false
A == AA -> true

this actually doesn’t seem impossible to fix either. The struct being created just needs to track its captures as opposed to the function tracking its arguments.

By changing the types to:

    const atype = aident(u32);
    const aatype = aident(u32);
    const btype = bident(u32);

I am getting:

a -> u32
b -> u32
A == B -> true
A == AA -> true

It is not the function.
Sized returns a new struct type each time it is called.

1 Like

then why is A == AA true?


fn Sized(T: type) type {
    return struct {
        pub const S = T;
        x: S,
    const atype = Sized(aident(u32));
    const aatype = Sized(aident(u32));
    const btype = Sized(bident(u32));


a -> example.Sized(u32)
b -> example.Sized(u32)
A == B -> true
A == AA -> true

Zig is caching the result of the comptime calls of the functions that have the same arguments.

I get that. It just means that “Sized returns a new struct type each time it is called” is a little off to say. Caching shouldn’t affect the outcode or correctness of code (and I understand th argument is that well it is only called once – just in the presence of a cache layer that changes semantics, its a incomplete statement). It would seem more approapriatge to say “Sized returns a new struct each time it is called with different arguments.”

I can see how that caching layer is necessary, but it doesn’t answer the underlying question of why the types should be different. Cache away, but also when different arguments come in, the capture lists might be the same as another generated struct. Right now the cache defines the type, and I’m not sure that is correct.

Yes, you are right.

Different functions are always considered different arguments, even if the have the same body. In practice this is not a problem (why have two different identity functions?).

They were just stand-ins. The could have easily been structs or types even. I just need something where the argument isn’t the actual capture. It could even be a single functtion that say rounds up int types to 64 bit boundaries so func(u32) and func(u16) result in the same true capture.\

Different functions are always considered different arguments

I understand that, but I’m just asking why that decision because it seem to be too general. One of the problems with generic code can sometimes be that the types are off a little and can result in two types that are indentical generating two different function bodies and can’t be used isomorphically like they should be able to, and that seems to be the case here.

It not a bug, but clearly a design decision to not track the the struct captures better, but I’m not sure why.

This came about in an actual use case for the SSO lib I have where I was tryging to use a function to gererate the Small String bufferfsize and two types that both had the same size but a different argument wound up being typed differently. It caused me to have to move code around to only the final size was in the generating function argument list.

Then you have weird behavior like this:

const std = @import("std");

fn NS(comptime _: type) type {
    return struct {
        pub var number: u32 = 0;

pub fn main() void {
    const a = NS(bool);
    const b = NS(bool);
    const c = NS(void);
    a.number += 1;
    b.number += 1;
    c.number += 1;
    std.debug.print("{d} {d} {d}\n", .{ a.number, b.number, c.number });
3 3 3

I was trying to clarify the issue by switching the const to a var but now I’m really puzzled myself ¯\_(ツ)_/¯

The output is from the 0.12.0 compiler. The 0.11.0 compiler produces 2 2 1, which is what I was expecting. The 0.11.0 compiler would also complain if you leave out comptime while 0.12.0 doesn’t.

1 Like

Zig 0.12 ignores the unused argument!


I think you can force different struct types by including the argument as a declaration. I don’t quite know where I read it, but to paraphrase I think the explanation was something along the lines of:
“Zig tracks the set of incoming dependencies to the struct type and uses those to decide whether a struct type is identical or not”

I also think there were some reasons for this, I think it has benefits for going towards incremental compilation and possibly making the comptime memoization more of an implementation detail, where the external dependencies are the ground truth that causes the struct type to be instantiated separately or shared, rather than the memoization / caching being semantically relevant.

1 Like

Perhaps in @mlugg responses to How Zig incremental compilation is implemented internally?

1 Like

In the hack I use to pick the function name back out, I make a type generating function and slice the returned structs name up to parse out the argument name.

To get that to work, I had to include a const x = arg in the generated struct or else it overwrote the last value. The capture has to be used, but I think the capture is too course. A more fine grained capture might help speed up compilation since multiple structs would be able to map to just their true captures (but it might slow it down bc that could be more difficult to determine).