Issue with opaque {} in inline for

I know that opaque {} types are assigned unique identifiers for different literals. I’m wondering why a literal inside inline for loop is reused?

In this code snippet foo() returns equal type ids, while bar() returns distinct:

const std = @import("std");

fn foo(comptime n: usize) []const type {
    comptime var a: [n]type = undefined;
    inline for (&a) |*t|
        t.* = opaque {};
    return &a;
}

fn bar() []const type {
    return &.{opaque {}, opaque {}, opaque {}};
}

pub fn main() void {
    const f = comptime foo(3);
    const b = comptime bar();
    inline for (f, b) |i, j|
        std.debug.print("foo:{} bar:{}\n", .{i, j});
}

Is there any way to create comptime array of n distinct opaque {} types?

Out of sheer curiosity, why do you need this?

You can use something in the opaque {}, which changes every iteration:

fn foo(comptime n: usize) []const type {
    comptime var a: [n]type = undefined;
    inline for (&a, 0..) |*t, i| {
        t.* = opaque {
            comptime {
                _ = i;
            }
        };
    }
    return &a;
}
1 Like

I am working on my own ECS library that stores all data in static fields of wrapper structs by design. I need a way to ensure that the wrapper function returns a unique type for each user-defined type, so that static fields are not reused. I’m trying to achieve this by using opaque {} as a field.

This will work if foo() is called only once, but I’d like to get globally unique opaques.

Then move the comptime var to the scope at which it needs to be unique.

Whether or not two namespaces (struct, enum, union or opaque) are equivalent is based on the AST nodes they capture. If you have written opaque {} on line 7, column 9, then every usage of that exact span of source text is going to reference the exact same namespace. To get a unique namespace, the namespace needs to capture a unique AST node. This is described in more detail in the 0.12.0 release notes under Namespace Type Equivalence (though it should probably be added to the langref too).

As you noted with @rpkak’s solution, it isn’t enough to just capture a loop index since that would only make each element distinct. To make each call to foo distinct you need to capture an AST node unique to the function call. There is no way to declare this “unique thing” entirely from the inside of your function; it must be passed as a parameter by the caller:

const std = @import("std");

fn foo(comptime n: usize, comptime brand: anytype) []const type {
    // I like to call this parameter used solely as an AST capture a "brand",
    // a term borrowed from a similar concept in TypeScript.
    return comptime a: {
        var a: [n]type = undefined;
        for (&a, 0..) |*t, i| t.* = opaque {
            comptime {
                _ = brand; // unique per function call (if the caller behaves)
                _ = i; // unique per iteration
            }
        };
        break :a &a;
    };
}

pub fn main() void {
    const fa = comptime foo(3, opaque {}); // or '@src()' or anything unique
    const fb = comptime foo(3, opaque {});
    inline for (fa, fb) |i, j|
        std.debug.print("fa:{} fb:{}\n", .{ i, j });
}
fa:z.foo__opaque_687 fb:z.foo__opaque_691
fa:z.foo__opaque_688 fb:z.foo__opaque_692
fa:z.foo__opaque_689 fb:z.foo__opaque_693

opaque {} or @src() are good candidates for “brands”, but anything unique works (and you might want to throw in comptime assertions to help guide your users toward passing a value likely to be unique, instead of e.g. 1 or "abc"). And yes, this means that a naughty caller could do e.g.

const x = opaque {};
const fa = comptime foo(3, x);
const fb = comptime foo(3, x);
inline for (fa, fb) |a, b| assert(a == b);

but that’s their problem to worry about, not yours.

4 Likes

A simple and elegant solution. Thank you! :slight_smile:

1 Like