Zig compiler segfaults when accessing struct with function pointer

zig build-exe segfaults when compiling the following example. zig build-lib and zig build-obj also shows same fault. Build succeeds when -fllvm is passed. Also only Debug builds have this issue - no fault when -O ReleaseSafe is used.

I think this is a regression in 0.15.1 compiler as the same works fine in 0.14.1. I am not sure if there is already a issue for the same, but I could not find one.

Source

const std = @import("std");

const ISpeaker = struct {
    speak: fn () void,
};

const Human = struct {
    speaker: ISpeaker,
};

fn say_hello() void {
    std.debug.print("Hello\n", .{});
}

pub fn speak(speaker: *const ISpeaker) void {
    speaker.speak();
}

pub fn main() !void {
    const h = Human{
        .speaker = .{
            .speak = say_hello,
        },
    };

    // This fails
    speak(&h.speaker);

    // This works
    h.speaker.speak();

    // This works
    const i = ISpeaker{ .speak = say_hello };
    speak(&i);
}

Bulid command

$ zig build-exe -O Debug main.zig
[1]    156500 segmentation fault  zig build-exe -O Debug main.zig

Expected behavior
Build finish with no errors.

The segfault is definitely a regression though I don’t think it should compile regardless, function body types are compile time only, the runtime counterpart function pointers which you probably meant to use are *const fn(args) ret.

Thanks. You are right I should have used speak: *const fn () void and this works (no fault after this change). But this raises few questions.

  1. What does speak: fn () void even mean? Is it still a pointer?
  2. So the regression is the compiler not reporting the a syntax/semantic error. If this is so then the same exists in version 0.14.1 as well.

more like a compile time handle to a function of that signature, kinda like a pointer ig, one where the compiler nows 100% what function It’s referring so will likely be inlined and optimised.

the regression is the segfault, but yes it should have compile errored in 0.14, that could also be a regression, but I can’t be bothered to chase that right now.

To clarify, the part that should not compile is passing a/asking for a runtime pointer to a type that has a field with compile time only type.

You could force the argument to be comptime or maybe even call the function with comptime, not sure about the latter, and the compiler would try to chase the pointer down at comptime, and it could work, or throw an error if it can’t. That might be the part that segfaulted, though it shouldn’t have gotten there without a comptime somewhere.

I raised an GitHub issue (`zig build-exe` segfaulting on Debug builds without `-fllvm` Ā· Issue #25060 Ā· ziglang/zig Ā· GitHub). If this is bogus, they can close it, if as you said there could multiple regressions here.

What does function body types mean? Documentation also does not say much

There is a difference between a function body and a function pointer. Function bodies are comptime-only types while function Pointers may be runtime-known.

I think you can basically think of function body types as the type that defines a specific function signature and the function body is like a handle that refers to a specific function that has this function signature.

You can think of it as a reference to a function that is only valid at comptime, because it refers to a function that is stored somewhere in the running compiler, once the compiler generates runtime code, that code can’t contain any function bodies, because otherwise the code that you are compiling wouldn’t be independent from the compiler that is compiling it.
Instead the functions which are actually used get compiled into the program and then you can use function pointers to refer to them.

Also you can do things with function body types that you can’t do with ā€œnormalā€ functions or function pointers, for example they allow you to do comptime higher order programming, having function bodies and function pointers as separate types makes a clearer distinction between what you can/can’t do with those types and when.

Function bodies are something you can use in comptime meta programming, while function pointers are more general and also work at runtime.

4 Likes

I’m not sure that this should actually be treated as runtime-known. It’s a const in the main function, so if I understand things correctly, it can propagate on a comptime-known basis. Example: indexing out-of-bounds with a const-declared usize is a compile error, to trigger it at runtime one has to use a var.

I’m not sure one way or the other if this premise applies to function bodies, however. I think we can agree that function pointers are what is intended by the code in question, it’s whether a pointer-to-constant-function-body can be deferenced to get a function body back under these circumstances which I’m fuzzy on.

Thanks. @Sze you might already know this, but for some FYI, I found this forum post Function Pointers - #7 by dimdin which explained this same topic. It has a link to the release when this distinction appeared in the compiler 0.10.0 Release Notes ⚔ The Zig Programming Language.

1 Like

Getting a pointer to that type is ofc fine, it’s the passing it to a function which isn’t being called in comptime and the function accepting a runtime pointer to that type which can only exist at comptime, I would expect an error requiring the parameter to be marked comptime or the function to be called in comptime.

There is also the option of the parameter being implicitly comptime, but I don’t like that. With type or fn body parameters that make sense, but for a non language primitive type I would prefer to have to explicitly mark it as comptime either the call or the parameter. I think the latter makes more sense.

Right, this is an important aspect of the coming months and years: things like this need to migrate from ā€œthis is how things workā€ to ā€œthis is how things must workā€, and with enough ā€˜why’ to make it hold together. It’s no too surprising that there’s a compiler regression here, because ā€œcall function body by passing constant pointer to a struct type which can only exist at comptimeā€ is a weird thing to do.

I’m inclined to think that a function body parameter should just be implicitly comptime, like an enum literal or a type. They can’t be passed around at any other time, so labeling them is redundant: if you try to pass a function pointer as a function body, that won’t compile.

There’s a subtle distinction here between ā€œin comptimeā€ and comptime known, though. It’s totally legit to call a function with comptime parameters at runtime, as long as all the comptime parameter are comptime known. Totally ordinary in fact, most of std.mem receives a type as the first parameter, and the rest are allowed to be runtime-known (whether they happen to be or not).

This example isn’t that simple, of course, because we’re talking about a comptime-known field type of a struct. But it’s still the case that we’re dealing with a type which can only exist in comptime, and it’s tricky (for me at least) to come up with a nice tight rule which would require that, but not e.g. a type, to be annotated with comptime.

I’ve never personally made a struct with a field of type type, but I have to presume the same thing happens: the composite gains ā€œcomptime taintā€, and can only be instantiated by assigning that field using comptime-known information. Kinda has to be, since runtime-known types do not exist.

In principle, I see no difference between a function where some parameters must be comptime-known, and a struct where some fields must be comptime-known. The latter could even be useful in calling the former!

2 Likes

Not to nitpick, but to show that it’s a totally normal thing - while you may not have created your own struct type with a field of type type, you’ve surely instantiated many such, such as std.builtin.Type.StructField, etc.