Why do enum fields create dependency loop errors in situations where decls don't?

Basically, I had a struct with only one field and decided to turn it into an enum instead, because I had quite a few constants of type @This() which I thought could be more clear and terse as enum fields, but I ran into a problem turning the constants into fields. Below is a minimal reproduction of the problem I ran into. Why do these fields create circular dependency errors when the constant declarations don’t? Because this is an enum, aren’t they practically the same? Is it better to use a struct with a single field instead of a non-exhaustive enum?

test.zig:

pub const E = enum(u32) {
    /// What this function does doesn't matter
    pub fn foo(x: E) u32 {
        return @intFromEnum(x) + 100;
    }

    pub const this_works: E = @enumFromInt(foo(.zero));
    pub const this_works_too: E = @enumFromInt(foo(.one));

    pub const zero: E = @enumFromInt(0);
    one = 1,

    this_doesnt_work = foo(.zero),
    this_doesnt_work_either = foo(.one),
    _,
};

test {
    @import("std").testing.refAllDecls(E);
}

console output:

$ zig test test.zig

error: dependency loop with length 2
    test.zig:13:24: note: type 'test.E' uses value of declaration 'test.E.foo' here
        this_doesnt_work = foo(.zero),
                           ^~~
    test.zig:3:16: note: value of declaration 'test.E.foo' depends on type 'test.E' for function parameter declared here
        pub fn foo(x: E) u32 {
                   ^~~~
    note: eliminate any one of these dependencies to break the loop

because these are fields, they are a part of the types “shape”

Those fields numeric values depend on calling foo
foo depends on the enum type itself
so by extension those fields depend on their own type.

I can see that, my question is why the same rule doesn’t apply to declarations which are also part of a type. I really don’t see why this shouldn’t be allowed anyway

They work because the type doesn’t depend on them
if, however, the type did depend on them (and they depended on the type), you would get the same error

const E = enum(u8) {
    foo = @intFromEnum(E.baz), // error cause baz depends on type
    bar = safe_baz, // works cause safe_baz does not depend on the typ
    _,

    const baz: E = @enumFromInt(1);
    const safe_baz = 2;
};

FYI your code also has examples of this: foo (functions are declarations) and zero whose type is E.

You didn’t get an error for zero simply because foo was analysed first.

..but the declarations DO depend on the rest of the type. They depend on foo.

Sorry mistyped, yes they depend on the type, but the type doesn’t depend on them

A non-exhaustive enum doesn’t depend on the fields any more than it depends on the declarations though? The only thing that influences the structure of the type is the backing integer type

You are thinking of memory structure, at the point this error happens is analysing semantic structure, the enum fields and their values absolutely do matter.

Perhaps that will change perhaps it won’t.

At least I prefer it this way as it is consistent with other types.

Maybe this pair of examples will be useful:

const T = struct {
    foo: u32
    bar: [@sizeOf(T)]u8
};
const T = struct {
    foo: u32, 

    const bar: [@sizeOf(T)]u8 = @splat(0);
};

The first can’t work, as that’s a diverging recursion. The second one is fine though. This sort of thing is what motivates some difference.

It is debatable where exactly the boundary lies. In particular, default field values could be made independent, and, IIRC, that’s how it used to work, and then that was simplified (for incremental compilation) to “field definition as a whole can’t depend on the type”, I think.

1 Like

On the other hand this is a non-exhaustive enum. So the amount of possible values is already given via different means.

Your example would be more similar to this which one should think should be able to work since the types size is given by the u8:

const T = packed struct(u8) {
    first_half: @Int(.unsigned, half),
    second_half: @Int(.unsigned, @sizeOf(T) * 8 - half),
    const half = @sizeOf(T) / 2 * 8;
};

and changing it to this does:

const T = packed struct(T.Backing) {
    first_half: @Int(.unsigned, half),
    second_half: @Int(.unsigned, @sizeOf(Backing) * 8 - half),
    const half = @sizeOf(Backing) / 2 * 8;
    const Backing = u8;
};
1 Like