zls icon indicating copy to clipboard operation
zls copied to clipboard

Type inference through structs containing types

Open dnut opened this issue 1 year ago • 0 comments

Sometimes it is useful to express a combination of generic types as a struct. For example, I use a struct like this in a key-value database that supports arbitrary types.

const ColumnFamily = struct {
    Key: type,
    Value: type,
    name: []const u8,
};

I wrote some generic functions that accept a ColumnFamily as an input, and use its contained types as the types of the parameters and return of the function. A simplified example:

pub fn get(db: Database, cf: ColumnFamily, key: cf.Key) ?cf.Value {
    return db.getStore(cf)).get(key);
}

This works great in zig, but zls has trouble inferring the type of any variable whose type is specified via ColumnFamily.

What Does Work?

Note: I have measured "successful type inference by zls" by checking that VS code shows a type hint for the variable.

ZLS has no trouble inferring types through layers of indirection, as long as they are supported forms of indirection.

type variable

You can assign a concrete type to a variable T of type type, and zls will successfully infer the concrete type of any variables that have their type specified as T

test "indirect type inference" {
    const T: type = u8;
    const start: T = 123;
    const item = start; // zls infers item as u8
    try std.testing.expect(u8 == @TypeOf(item));
}

generic function with type parameter

ZLS can infer the type returned from a generic function when the type is passed as a parameter to the function.

fn identity(comptime T: type, item: T) T {
    return item;
}

test "double indirect type inference via type param" {
    const T: type = u8;
    const item = identity(T, 123); // zls infers item as u8
    try std.testing.expect(u8 == @TypeOf(item));
}

generic function with anytype

The same also works when the parameter's type is inferred rather than being specified with a type parameter.

fn anyIdentity(item: anytype) @TypeOf(item) {
    return item;
}

test "indirect type inferece via anytype fn" {
    const start: u8 = 123;
    const item = anyIdentity(start); // zls infers item as u8
    try std.testing.expect(u8 == @TypeOf(item));
}

What Doesn't Work?

If all you do is wrap a type in a struct and directly specify it as the type of a variable (similar to the first example, but now with a wrapper struct), ZLS cannot infer the type:

const TypeSpec = struct { T: type };

test TypeSpec {
    const spec = TypeSpec{ .T = u8 };
    const start: spec.T = 123;
    const item = start;  // zls is unable to infer item's type here
    try std.testing.expect(u8 == @TypeOf(item));
}

Likewise, it does not work when passed through a function, either.

const TypeSpec = struct { T: type };

fn specIdentity(comptime spec: TypeSpec, item: spec.T) spec.T {
    return item;
}

test specIdentity {
    const spec = TypeSpec{ .T = u8 };
    const item = specIdentity(spec, 123);  // zls is unable to infer item's type here
    try std.testing.expect(u8 == @TypeOf(item));
}

dnut avatar Aug 07 '24 19:08 dnut