inconsistent dependency loop error when attemping to reify indirectly self-referential type
Zig Version
0.12.0-dev.1396+f6de3ec96
Steps to Reproduce and Observed Behavior
Create a file bug.zig:
const std = @import("std");
fn Union(comptime T: type) type {
const enum_fields = &[1]std.builtin.Type.EnumField{.{
.name = "x",
.value = 0,
}};
const Enum = @Type(.{ .Enum = .{
.tag_type = u1,
.fields = enum_fields,
.decls = &.{},
.is_exhaustive = true,
} });
const union_fields = &[1]std.builtin.Type.UnionField{.{
.name = "x",
.type = T,
.alignment = @alignOf(T), // This causes the error.
}};
return @Type(.{ .Union = .{
.layout = .Auto,
.tag_type = Enum,
.fields = union_fields,
.decls = &.{},
} });
}
test {
_ = struct {
const A = struct { b: *B };
const B = Union(A);
}.B;
}
zig test bug.zig
bug.zig:33:9: error: dependency loop detected
const B = Union(A);
^~~~~~~~~~~~~~~~~~
This might seem logical at first, as A's alignment seems to be dependent on B.
However, if the implementation of Union is replaced with this:
return union(enum) {
x: T,
};
the compiler can correctly infer the alignment of the x field to be 8.
Considering this works, it seems logical to assume that the @Type-based implementation also does.
Expected Behavior
The code compiles.
In my use-case, this seems to be fixable by setting .alignment = 0, making the compiler infer it. We should probably document that this is a valid value and consider setting the default value of the field to 0.
Possibly related to #17255 - that issue does not use comptime, but does use @alignOf.
Nope, unrelated. This issue isn't really a bug - resolving the alignment of T requires evaluating Union(A), triggering the dependency loop. The behaviour that makes this not work is that reified struct types do not keep laztly alignment values lazy for future resolution, instead resolving the entire reified type immediately. This could be changed, but the merits of doing so are unclear.
However, we should definitely document the fields of std.builtin.Type better.
The following variation of the originally reported code however does compile:
const std = @import("std");
fn Union(comptime T: type) type {
const enum_fields = &[1]std.builtin.Type.EnumField{.{
.name = "x",
.value = 0,
}};
const Enum = @Type(.{ .@"enum" = .{
.tag_type = u1,
.fields = enum_fields,
.decls = &.{},
.is_exhaustive = true,
} });
const union_fields = &[1]std.builtin.Type.UnionField{.{
.name = "x",
.type = T,
.alignment = @alignOf(T), // This causes the error.
}};
return @Type(.{ .@"union" = .{
.layout = .auto,
.tag_type = Enum,
.fields = union_fields,
.decls = &.{},
} });
}
pub fn main() void {
const decls = struct {
const A = struct { b: *B };
const B = Union(A);
};
var t: decls.A = undefined;
const B0 = decls.B; // if this line is shifted up above the preceding one, the error is back
_ = B0;
_ = &t;
}
It however yields a dependency loop error, if the declaration of B0 is put before declaration of t. It looks as if somehow the dependency loop is or is not an issue, depending on at which point it's entered first.
Reduction of the above:
const A = struct { b: *B };
const B = @Type(.{ .@"struct" = .{
.layout = .auto,
.is_tuple = false,
.fields = &.{.{
.name = "x",
.type = A,
.is_comptime = false,
.default_value_ptr = null,
.alignment = @alignOf(A),
}},
.decls = &.{},
} });
comptime {
_ = @as(A, undefined); // try commenting this line
_ = B;
}
build-obj on this file succeeds. If you comment out the marked line, a dependency loop compile error is emitted.
Please notice that in 0.15.2 the .alignment = 0 workaround no longer works, since 0 is not accepted as the alignment value any longer.