Skip to content

Unexpected type dependency loop when declaring a function pointer to a function that returns its own type. #18664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dimdin opened this issue Jan 24, 2024 · 9 comments
Labels
bug Observed behavior contradicts documented or intended behavior

Comments

@dimdin
Copy link
Contributor

dimdin commented Jan 24, 2024

Zig Version

0.11.0, 0.12.0-dev.2327+b0c8a3f31

Steps to Reproduce and Observed Behavior

state.zig:6:1: error: dependency loop detected
const StateFn = *const fn (In, *Out) ?StateFn;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Example program:

const std = @import("std");

const In = struct {};
const Out = struct {};

const StateFn = *const fn (In, *Out) ?StateFn;

// trampoline
fn run(in: In, out: *Out, initial_state: StateFn) void {
    var current_state: StateFn = initial_state;
    while (current_state(in, out)) |state| {
        current_state = state;
    }
}

fn firstState(in: In, out: *Out) ?StateFn {
    _ = in;
    _ = out;
    std.log.debug("in first state", .{});
    return lastState;
}

fn lastState(in: In, out: *Out) ?StateFn {
    _ = in;
    _ = out;
    std.log.debug("in last state", .{});
    return null;
}

test "run state machine" {
    var out: Out = undefined;
    const in: In = undefined;
    run(in, &out, firstState);
}

Expected Behavior

Expecting zig compiler to accept the function pointer declaration of a function that returns itself.
Such declarations are useful in lexer/scanner trampolines.

Example golang code: https://cs.opensource.google/go/go/+/master:src/text/template/parse/lex.go;l=110

type stateFn func(*lexer) stateFn
@dimdin dimdin added the bug Observed behavior contradicts documented or intended behavior label Jan 24, 2024
@nektro
Copy link
Contributor

nektro commented Jan 24, 2024

Duplicate #12325 likely

@nektro
Copy link
Contributor

nektro commented Jan 28, 2024

I get this running it

test.zig:6:43: error: use of undeclared identifier 'StateFn'
    const StateFn = *const fn (In, *Out) ?StateFn;
                                          ^~~~~~~

@dimdin
Copy link
Contributor Author

dimdin commented Jan 28, 2024

With the latest master binaries produced on 2024-01-25, zig version "0.12.0-dev.2341+92211135f" I am still getting "dependency loop detected".
Probably a recent commit changed the behavior.
But the issue is still there.

@dimdin
Copy link
Contributor Author

dimdin commented Feb 25, 2024

Related dependency loop issues #16932 and #131

smaller test case:

const StateFn = *const fn () ?StateFn;

fn done() ?StateFn {
    return null;
}

test {
    _ = done();
}

zig 0.12.0-dev.2928+6fddc9cd3 output:

state.zig:1:1: error: dependency loop detected
const StateFn = *const fn () ?StateFn;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@mlugg
Copy link
Member

mlugg commented Feb 25, 2024

I should note you can work around this by wrapping this data in a struct:

const State = struct {
    evalFn: *const fn () ?State,
};

The error here happens because we only have lazy resolution (which permits recursive definitions) for structs, unions, enums, and opaques. It's not exactly a duplicate, but is heavily related to #12325.

@dimdin
Copy link
Contributor Author

dimdin commented Feb 25, 2024

I should note you can work around this by wrapping this data in a struct:

Thank you very much @mlugg for the workaround

@mlugg mlugg marked this as a duplicate of #22543 Jan 20, 2025
@dvmason
Copy link

dvmason commented Jan 20, 2025

Thank you @mlugg for the prompt workaround to #22543

I had a question about that, but you closed it, so I'll ask here. I changed your workaround to:

const std = @import("std");

const execute = struct {
    const ThreadedFn = struct {
        f: *const fn (
            process: *Process,
            context: *Context,
        ) void,
    };
};
const Process = struct {
    debugFn: ?execute.ThreadedFn,
};
const Context = struct {
    npc: execute.ThreadedFn,
};

test "die" {
    _ = Process{
        .debugFn = null,
    };
}

and that seems to work. Is there a footgun hiding in that solution? (I worry because you said 'This is a little awkward because you can't just have a ?ThreadedFn' but it seems to be OK.)

@mlugg
Copy link
Member

mlugg commented Jan 20, 2025

The issue there is that the optional is no longer represented as a null pointer, but rather with an extra tag stored separately. So, you couldn't e.g. pass that to a C API which expected a plain old nullable pointer. If that isn't a problem for you, then your code works fine (although bear in mind that it uses a bit more memory than an optional pointer).

@dvmason
Copy link

dvmason commented Jan 20, 2025

Right, because then it's an optional struct (even though it's one word), not an optional function pointer. and doing const ThreadedFn = *struct { means I'd actually have to allocate a struct somewhere.

Simplest for my case is to not make it optional, but point to a function that doesn't do anything.

Thanks, again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Observed behavior contradicts documented or intended behavior
Projects
None yet
Development

No branches or pull requests

4 participants