zig icon indicating copy to clipboard operation
zig copied to clipboard

Stage2: false dependency loop

Open Vexu opened this issue 3 years ago • 16 comments

const S = struct {
    a: *[@sizeOf(S)]u8,
};
test {
    var s: S = undefined;
    _ = s;
}
a.zig:19:10: error: struct 'a.S' depends on itself
    a: *[@sizeOf(S)]u8,
         ^~~~~~~~~~

Should be fixed by implementing lazy pointer types, lazy array types, or both.

Vexu avatar Aug 03 '22 16:08 Vexu

This also fails:

pub const DeviceCallback = *const fn (*Device) void;
pub const Device = struct {
    callback: DeviceCallback,
};
test {
    _ = DeviceCallback;
}

error: dependency loop detected

@Vexu Do you think this is the same bug or should I open a new issue?

michal-z avatar Aug 11 '22 21:08 michal-z

It's the same bug.

Vexu avatar Aug 12 '22 08:08 Vexu

Slight variation on this issue, this example straight-up crashes the compiler:

const Foo = struct {
    ptr: *[1]Foo,
};

test {
    const x: Foo = undefined;
    _ = x;
}

Weirdly, _ = @as(Foo, undefined) works fine - this example seems specifically related to assignment.

mlugg avatar Sep 07 '22 16:09 mlugg

That is a separate bug in the LLVM backend's debug info creation, adding either --fno-LLVM or --strip avoids the crash.

Vexu avatar Sep 07 '22 16:09 Vexu

Ah okay, thank you. Is there an open issue for that bug (just so I can reference it in a comment)?

mlugg avatar Sep 07 '22 16:09 mlugg

Not that I know of.

Vexu avatar Sep 07 '22 16:09 Vexu

Note as a workaround for this I'm just hand-modifying the cimport.zig to just replace the pointers with anyopaque pointers. I've only had this issue with C imports (probably due to the style of C this sort of reference is common -- I know you can get this in pure Zig too I just haven't seen or written code in practice that does). This works for now!

mitchellh avatar Oct 15 '22 18:10 mitchellh

The following also fails:

const std = @import("std");

const node_size = 16;

const Tag = enum(u8) {
    none,
    some
};

pub const Maybe = extern union {
    none: extern struct {
        tag: Tag = .none,
        padding: [node_size - @sizeOf(Tag)]u8 = undefined,
    },
    some: Some,
};

const Body = extern struct {
    maybe: Maybe = Maybe{.none = .{}},
};

const Some = extern struct {
    const padding_len = node_size - (@sizeOf(Tag) + @sizeOf(usize));

    tag: Tag = .some,
    padding: [padding_len]u8 = [_]u8{0} ** padding_len,
    body: *Body,
};


pub fn main() !void {
    std.debug.print("{any}\n", .{@sizeOf(Maybe)});
}

Interestingly when body: *Body, is replaced with body: *Maybe, it compiles fine.

somethingelseentirely avatar Oct 25 '22 12:10 somethingelseentirely

That looks very similar to my original case that I was unable to reduce.

Vexu avatar Oct 25 '22 12:10 Vexu

i'm having a similar issue. is this the same thing? if not is there already an issue for this or should i make a one?

really wanting this feature for generated code in my protobuf-zig lib so that i don't have to resort to generating duplicate .c,/h files to achieve this.

// /tmp/tmp.zig
pub const A = extern struct { // same happens w/ non-extern struct
    b: *const B,
};

pub const B = extern struct {
    a: *const A,
};

const a = A{ .b = &b };
const b = B{ .a = &a };

test {
    _ = a;
}
$ zig test /tmp/tmp.zig
/tmp/tmp.zig:11:1: error: dependency loop detected
const a = A{ .b = &b };
^~~~~~~~~~~~~~~~~~~~~~

travisstaloch avatar Feb 01 '23 21:02 travisstaloch

This issue is about types incorrectly depending on themselves while yours is caused by declarations depending on each others address recursively and reduces to:

const a = &b;
const b = &a;
test {
    _ = a;
}

Vexu avatar Feb 02 '23 11:02 Vexu

This issue is about types incorrectly depending on themselves while yours is caused by declarations depending on each others address recursively and reduces to:

Thanks. Created #14517

travisstaloch avatar Feb 02 '23 18:02 travisstaloch

This also fails:

pub const DeviceCallback = *const fn (*Device) void;
pub const Device = struct {
    callback: DeviceCallback,
};
test {
    _ = DeviceCallback;
}

error: dependency loop detected

I hit this as well, in basically the same situation. Eventually I resorted to using *anyopaque instead of my equivalent of *Device and casting it in each callback. Is there a better workaround than this?

pub const DeviceCallback = *const fn (*anyopaque) void;

fn someCallback(p: *anyopaque) void {
    const device: *Device = @ptrCast(@alignCast(p));
}

rhoot avatar Oct 09 '23 06:10 rhoot

Running into this in Bun, but there’s no stack/return trace other than the comptime function generating the type, which does not point to the line causing a dependency loop

image

note: the MultiArrayList error is unrelated & usually means UB in zig’s compiler. It happens when any compiler error occurs in a a non-root module within the build

Jarred-Sumner avatar Jan 28 '24 21:01 Jarred-Sumner

Just in case, one more example.

This one does not compile:

const std = @import("std");

const Foo = struct {

    const FnPtr = *const fn (*Foo, u8) void;
    data: u8,
    cb: FooCallBack,

    const FooCallBack = struct {
        fptr: FnPtr,
        data: u8,
    };

    fn init(fptr: FnPtr) Foo {
        return .{
            .data = 7,
            .cb = .{.fptr = fptr, .data = 8},
        };
    }

    fn call(foo: *Foo, data: u8) void {
        foo.cb.fptr(foo, data);
    }
};

fn bar(foo: *Foo, data: u8) void {
    std.debug.print (
        "data-1 = {}, data-2 = {}, data-3 = {}\n",
        .{foo.data, foo.cb.data, data}
    );
}

pub fn main() !void {
    var foo = Foo.init(&bar);
    foo.call(9);
}

Get

222-b.zig:6:5: error: dependency loop detected
    const FnPtr = *const fn (*Foo, u8) void;
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

But this one (without "construtor") compiles:

const std = @import("std");

const Foo = struct {

    const FnPtr = *const fn (*Foo, u8) void;
    data: u8,
    cb: FooCallBack,

    const FooCallBack = struct {
        fptr: FnPtr,
        data: u8,
    };

    pub fn call(foo: *Foo, data: u8) void {
        foo.cb.fptr(foo, data);
    }
};

fn bar(foo: *Foo, data: u8) void {
    std.debug.print (
        "data-1 = {}, data-2 = {}, data-3 = {}\n",
        .{foo.data, foo.cb.data, data}
    );
}

pub fn main() !void {
    var foo: Foo = .{
        .data = 7,
        .cb = .{.fptr = &bar, .data = 8},
    };
    foo.call(9);
}

dee0xeed avatar Jun 13 '24 13:06 dee0xeed

I'm facing a very similar issue with dependency loops with regard to function pointers, although I'm not sure it's the exact same (tested in 0.13 and 0.12.1):


const std = @import("std");

const A = struct {
    field: std.meta.Tuple(&.{ i32, B }),
};

const B = union(enum) {
    thing: *const fn (a: *A) void,
};

pub fn main() void {
    const b: B = undefined;
    _ = b;
}

Weirdly enough, the dependency loop error does not occur when using an ordinary struct instead std.meta.Tuple in A, or when B is a struct instead of a tagged union.

cosineblast avatar Aug 04 '24 23:08 cosineblast

the recent changes to tuples (suspected cause) now make the following code error (tested on 0.14.0-dev.2335+8594f179f):

test {
    _ = Foo;
}

const Foo = struct {
    //      ^~~~~~
    // error: struct 'main.Foo' depends on itself
    field: *struct { Foo },
};

the code compiles properly on 0.13.0

Fri3dNstuff avatar Nov 29 '24 07:11 Fri3dNstuff

I've encountered a similar issue with std.ArrayListAligned. The code:

const std = @import("std");

const Node = struct {
    child: ?*NodeList,
};
const NodeList = std.ArrayListAligned(Node, @alignOf(Node));

pub fn main() void {
    var test_node = Node{
        .child = null,
    };
    test_node = test_node;
}

fails to compile with

example.zig:3:14: error: struct 'example.Node' depends on itself

See example at Godbolt

It works fine when I replace child: ?*NodeList on line 4 with child: ?*Node.

The error occurs with every version of Zig I've tested, so I'm not sure if I'm just doing it wrong.

clemenssielaff avatar Jan 20 '25 20:01 clemenssielaff

Another example. I've been struggling with dependency loops forever, and here is the most recent:

const execute = struct {
    const ThreadedFn = packed struct {
        f: Fn,
        const Fn = *const fn (
            process: *Process,
        ) void;
    };
};
const Process = struct {
    m: [@sizeOf(P)]u8,
    const P = extern struct {
        h: Fields,
        const Fields = extern struct {
            debugFn: execute.ThreadedFn,
        };
    };
};
fn f(_: *Process) void {}
test "die" {
    const tfn = execute.ThreadedFn{.f = &f};
    var p: Process = undefined;
    tfn.f(&p);
}

The m field and the P struct are part of my effort to break the dependency loop. I have a workaround, as the size of the Process actually is a constant, and I tune the size of the P struct to match it. The h:Fields is just to mimic my code... the loop remains with debugFn directly in P. This is probably just a more complex example of the top post problem.

dvmason avatar Feb 17 '25 15:02 dvmason

Categorizing under #24637 for the moment, but I'm pretty certain we're going to want to just accept this restriction in the language. Solving this would introduce a lot of language complexity.

mlugg avatar Aug 10 '25 19:08 mlugg

This problem specifically could be solved by assuming pointers to always be sized @sizeOf(usize) bytes. This currently holds true even for @sizeOf(*void). @sizeOf(SomeStruct) doesn't have to fully resolve the type. Am I missing something?

Inve1951 avatar Aug 11 '25 20:08 Inve1951

I've recently abandoned having the named type

const Fn = *const fn (
            process: *Process,
        ) void;

and just put in the whole *const fn... stuff everywhere. Ugly, but it works.

dvmason avatar Aug 11 '25 23:08 dvmason

This problem specifically could be solved by assuming pointers to always be sized @sizeOf(usize) bytes. This currently holds true even for @sizeOf(*void). @sizeOf(SomeStruct) doesn't have to fully resolve the type. Am I missing something?

It is true that this is logically possible. Unfortunately, type resolution is an area where Zig's comptime forces us to make some design compromises. Typical languages like C kind of have this easy, because types are their own thing with a limited grammar; they can simply see that the type is a pointer without "looking at" the whole type. That doesn't work in Zig: field types are expressions, and the only way to figure out the value of that expression is to evaluate it. After all, instead of this:

const Foo = struct {
    ptr: *[@sizeOf(Foo)]u8,
};

I could have written this:

const Foo = struct {
    ptr: (T: {
        const n = @sizeOf(Foo);
        const Array = [n]u8;
        if (true) break :T *Array;
        @compileError("you can't get here!");
    }),
};

...which clearly means the exact same thing by Zig's semantics, but I hope it's clear that handling that makes the job much, much harder. Bear in mind that this labeled block could do anything between const Array = ... and the break, so if you were trying to evaluate the type "lazily", you'd need to make every single operation which could touch Array check whether it needs to resolve that type. We have systems like this in the compiler to try and make type resolution more lazy -- in practice they cause lots of annoying bugs.

The way type resolution works in Zig today is held together with duct tape and string: it's extremely tied to the implementation, essentially impossible to codify into a language specification, and is chock-full of bugs (see sub-issues of #24637), some of which are very serious and essentially impossible to solve. I'm currently working on a potential change which sacrifices a couple of small capabilities, but makes the behavior consistent, and allows a few things which clearly should be allowed, alongside fixing some issues with incremental compilation. If that branch ends up making it into master, it will not allow this.

By the way, if you're hitting this issue, you can just use a [*]u8 field instead! You can even slice it (bytes[0..@sizeOf(T)]) when you use it to get the same behavior. Obviously it would be preferable for it to work directly, but it's pretty easy to work around this limitation.

mlugg avatar Aug 12 '25 13:08 mlugg

@mlugg In this potential change to the type system, will the following snippet work ?

pub const DeviceCallback = *const fn (*Device) void;
pub const Device = struct {
    callback: DeviceCallback,
};
test {
    _ = DeviceCallback;
}

I think it's an extremely common pattern (much more than the one described in the issue itself) and currently it doesn't work, even though every programming language with function pointers allow this kind of use.

zenith391 avatar Aug 18 '25 17:08 zenith391

@zenith391 yep, that snippet works fine on my in-progress branch -- it's one of the cases in #24637 which I specifically wanted to make sure I solved.

mlugg avatar Aug 18 '25 18:08 mlugg