What's the magic behind the function call inference?

Hi guys,

Have a look at the following code

//! hello.zig
const person = struct {
    pub fn new() person {
        return .{};
    }
};
fn foo(_: person) void {}
pub fn main() void {
    foo(.new());
}

How Zig compiler infered that the .new call is from the person?

The decl literal syntax, named that way because it works for all declarations, not just functions but consts and vars too.

Using your example foo(.new()) is semantically identical to foo(Person.new(), in the same way you can type .{} instead of Person{}, assuming zig can infer the type.

Its just type inference! zig knows the parameters of foo, so it knows what type it should be, so searches that type for a declaration with the name. Of course, the decl type, or return type if you’re calling a function, has to be the same type or one that can coerce.

You’ll probably like to know, zigs type inference is able to see through pointers, optionals etc, so it even works in those cases.

7 Likes

In order to complete what @vulpesx said, there’s a few things to keep in mind that you might stumble upon.

You can’t use them in an expression, because the result location is then different. For example:

const r: Result = .decl.modified(...);

This won’t work because it effectively is this

const r: Result = (.decl).modified(...);

And so .decl doesn’t infer from : Result. Its parent expression is [expr].modified instead of const r: Result = [expr], and so it can’t get the type from here.

The only exceptions to this are the simple call and try call syntaxes, because they’re very useful. You can do .decl(...) or try .decl(...). IMO, it should also work with .decl(...) catch but last time I tried it didn’t.

This also mean that this syntax only works if the return type is the same as the namespace (save for error union). IMO it should also work with noreturn, and the optional (making .decl orelse an exception too), but last time I tried it, this wasn’t the case.

It doesn’t work with catch and orelse because they are branches.

The type of each branch can be different as long as they are compatible (can coerce into one or the other, or into a common type), meaning the type of each branch must be known before the type of the whole expression is.

Since a decl literal depends on knowing the final type, branching expressions can’t support them as it would create circular logic.

But special cases can be supported, like try, no others currently but more could be supported in the future.

they do work for optional result types, just not in .a orelse ....

3 Likes

thanks. I‘ll read the doc.

One would think that try is also a branch since it’s just a shorthand for catch |err| return err.

With RLS one could argue that the result type of each branch of a branching expression must be the same similar to how this is possible (and currently compiles):

const Birthday = extern struct {
    year: i16,
    month: u8,
    day: u8,

    const unknown: Birthday = .{
        .day = 255,
        .month = 255,
        .year = 0,
    };
};

pub export fn create_birthday(timestamp: u64) Birthday {
    const unknown = ~@as(u64, 0);
    return if (timestamp == unknown)
        .unknown
    else
        .{
            // placeholder
            .year = 1900,
            .month = 5,
            .day = 5,
        };
}

I think it is. I think @vulpesx was indicating that it’s a “special supported case”, and that others might follow in the future, too.

1 Like