zig icon indicating copy to clipboard operation
zig copied to clipboard

Allow declarations in @Type for Struct/Union/Enum/Opaque

Open tadeokondrak opened this issue 5 years ago • 16 comments

The compiler currently errors when the .decls slice is non-empty.

More context at https://github.com/ziglang/zig/issues/383.

tadeokondrak avatar Oct 17 '20 06:10 tadeokondrak

One (imho strong) argument to allow this is that we can make really convenient interface implementations:

// Definition of an interface:
const Allocator = std.meta.Interface(struct {
    pub const Error = error{OutOfMemory};
    alloc: fn (self: *std.meta.Self, len: usize) Error![]u8,
    free: fn(self: *std.meta.Self, ptr: []u8) void,
});

// Usage is just taking the interface type, it's a "fat pointer":
fn process(allocator: Allocator, items: []const u8) !void {
    // Just call functions on the interface like on normal objects.
    // this is provided via created functions from .decls
    const buf = try allocator.alloc(items.len);
    defer allocator.free(buf);
    
    …
}

// Implementing interfaces is still duck typing:
const FixedBufferAllocator = struct {
    const Self = @This();

    buffer: []u8,
    allocated: bool = false,
    
    pub fn init(buf: []u8) Self {
        return Self { .buffer = buf };
    }

                                          // access public symbols of the struct we pass to std.meta.Interface
                                          // as we can usingnamespace them
    pub fn alloc(self: *Self, size: usize) Allocator.Error!void {
        if(self.allocated)
            return error.OutOfMemory;
        if(size > self.buffer.len)
            return error.OutOfMemory;
        self.allocated = true;
        return self.buffer[0..size];
    }

    pub fn free(self: *Self, ptr: []u8) void {
        std.debug.assert(ptr.ptr == self.buffer.ptr);
        self.allocated = false;
    }
};

fn main() !void {
    var fba = FixedBufferAllocator.init(&some_array);
    
    // Interface().get(ptr) will duck-type match the interface into a fat pointer
    try process(Allocator.get(&fba), "Hello, Zig");
}

To make the convenient function calls like allocator.free(…) we need TypeInfo.Struct.decls.

The neat thing is: The example code above does feel native, like if interfaces would be a part of the language. But they are just a clever use of comptime. This would kinda also close #130 as "implemented"

ikskuh avatar Oct 20 '20 22:10 ikskuh

A minor usecase - when translate-c can't handle something I make a wrapper function instead:

[nix-shell:~/bluetron/blinky]$ cat wrapper.c
#include "stdbool.h"
#include "stdint.h"
#include "nrf_delay.h"
#include "boards.h"

void bluetron_nrf_delay_ms(uint32_t ms_time)
{
  nrf_delay_ms(ms_time);
}

It would be nice to be able to wrap my @cImport to automatically rename bluetron_foo to foo, replacing the original declaration.

This is kind of similar to --wrap=foo in ld.

jamii avatar Oct 31 '20 03:10 jamii

This would also allow merging structs, opening the door to having template structs that can be composed to build others:

// a and b must be structs.
fn mergeStructs(comptime a: type, comptime b: type) type {
    const ti_a = @typeInfo(a);
    const ti_b = @typeInfo(b);
    std.debug.assert(ti_a == .Struct);
    std.debug.assert(ti_b == .Struct);

    return @Type(.{ .Struct = .{
        .layout = .Auto,
        .fields = ti_a.Struct.fields ++ ti_b.Struct.fields,
        .decls = ti_a.Struct.decls ++ ti_b.Struct.decls,
        .is_tuple = false,
    } });
}

jecolon avatar May 04 '21 14:05 jecolon

One (imho strong) argument to allow this is that we can make really convenient interface implementations

There's a bit of a problem with this example actually. In order to build the type, the functions used need to reference self: @This(). They have no way to do this, so even with .decls I don't think Interface can be implemented the way you are imagining.

SpexGuy avatar May 18 '21 18:05 SpexGuy

One (imho strong) argument to allow this is that we can make really convenient interface implementations

There's a bit of a problem with this example actually. In order to build the type, the functions used need to reference self: @This(). They have no way to do this, so even with .decls I don't think Interface can be implemented the way you are imagining.

I just realized that this might not be an issue, if those functions use anytype instead of @This(). Since the functions are meant to be generic anyway, I don't think this is a problem

bayo-code avatar Jun 13 '22 13:06 bayo-code

I didn't realize this was an open issue.

I think this should be closed; I like the fact that declarations have to be written out by the programmer, it makes them very easy to track down, and understand.

And besides making it very easy for people to understand, it also makes it very easy for machines to understand, namely LSPs. E.g. ZLS is unable to figure out fields/members in types created using @Type, but that's supplemented by the fact that it can nearly always figure out declarations.

Being able to construct data layouts at comptime is great, and I can understand why one might want to be able to apply the same ideas to declarations, but I feel that it goes against the apparent sentiment of "Concrete types over abstract types", which, reified declarations would overly-facilitate the latter.

InKryption avatar Jun 13 '22 14:06 InKryption

I'm at a point where I kinda depend on this to implement something where I want a specific struct to be configurable based on a given configuration. I could still implement it if I type out the same struct for every single possible configuration and usingnamespace in the decls, which is extremely tedious, hard to maintain, and error-prone. In some cases this error just makes something so much harder than it has to be. I think this is a pretty limiting issue especially for libraries and I hope we can remove this error soon.

wooster0 avatar Sep 28 '22 20:09 wooster0

this is very unlikely

nektro avatar Sep 28 '22 22:09 nektro

For such an specific case I would say you should write Zig code to write Zig code

davidgmbb avatar Sep 28 '22 22:09 davidgmbb

Yeah so as an update, I thought about my use case a bit more overnight and realized: another actually simpler way to solve this is if I add the requirement of the user usernamespaceing the decls I provide into the struct (instead of the library doing that) and then they pass that struct to me and then I could even still check if they indeed usernamespaced it etc. So to reiterate, these were the previous solutions I thought of:

  1. secretly add decls to the struct they pass to me.
  2. being able to configure the struct with a Config struct of bool fields or something and automatically having the decls be in that struct that I create using @Type based on that Config.

Both depend on this to be allowed.

But so with the usingnamespace solution, the configuration that the library was supposed to do, the user now does it themself, explicitly. This is actually the simplest solution as opposed to fiddling around with @typeInfo and @Type etc.

So I think because of the existence of usingnamespace it is often fine for this to not be allowed, so maybe ultimately we will close this issue. usingnamespace is already a very powerful construct I think so it may end up eliminating the need of this to be allowed.

For such an specific case I would say you should write Zig code to write Zig code

Yeah, I was thinking that as well. Luckily that's not possible in Zig, and shouldn't be.

wooster0 avatar Sep 29 '22 05:09 wooster0

I get your points but it's unfortunate that you cannot generate tagged union from struct fields, and to also provide some methods there.

Context: Being able to accept a chunk of prop: value declarations, to be applied to a struct. These props are parsed. So you have a "script" of changes to be applied to a struct.

Being able to provide decls would make it possible to generate both .format() and .parse() methods, which could then fit nicely into the rest of the "framework".

cztomsik avatar Mar 30 '23 09:03 cztomsik

For such an specific case I would say you should write Zig code to write Zig code

That is always an alternative; I just think it would be an arbitrary shortcoming of Zig's regular comptime metaprogramming. You would need to make another executable, manage the generated files, and to transform code precisely, it would need to depend on the entire compiler frontend.

tau-dev avatar May 05 '23 11:05 tau-dev

Note, accepted #10710 to replace @Type with: @Int, @Float, @Pointer, @Array, @Struct, @Enum, @Union

SeriousBusiness100 avatar Aug 26 '23 19:08 SeriousBusiness100

this would be very useful for my com-like interfaces, though i suppose not strictly essential - without this my code just gets a lot less clean and i end up doing a lot more comptime generation of other parts of the setup or the need to generate zig code rather than using the metaprogramming tools

Khitiara avatar Jan 16 '24 16:01 Khitiara

InKryption: Being able to construct data layouts at comptime is great, and I can understand why one might want to be able to apply the same ideas to declarations, but I feel that it goes against the apparent sentiment of "Concrete types over abstract types", which, reified declarations would overly-facilitate the latter.

I feel the same way. Came to this issue to see if it's being worked on, now I think that the current implementation should remain. It's probably better to restructure the interface or use a different approach. Also, an implemention patch was already closed, this probably should be to. If you really need it, this is already supported and (I think necessarily) obtuse to do:

Code example

this works but no promises this is idomatic.

const std = @import("std");

//just call it what it is, inheritance
test "inherit" {
    const p = Programmer{
        //lsp is blind in here, no better than an anonymous struct
        .name = "Ziggy",
        .language = "Zig",
    };
    //doesn't exist, Programmer has no decls remember?
    //p.speak();
    //prints "Ziggy <3 Zig"
    p.child.speak(p);
    //prints "Ziggy"
    p.child.as_person(p).speak();
    //prints "Ziggy" too
    p.parent.speak(p.child.as_person(p));
    try std.testing.expect(p.parent == Person);
}

fn inherit(comptime parent: type, comptime child: type) type {
    const new_f = [_]std.builtin.Type.StructField{
        //are these alignments allowed to be different? i don't know but it works here
        .{ .name = "parent", .type = type, .default_value = &parent, .is_comptime = true, .alignment = @alignOf(parent) },
        .{ .name = "child", .type = type, .default_value = &child, .is_comptime = true, .alignment = @alignOf(child) },
    };
    return @Type(std.builtin.Type{ .Struct = .{
        .layout = .auto,
        .fields = std.meta.fields(parent) ++ std.meta.fields(child) ++ new_f,
        .decls = &[_]std.builtin.Type.Declaration{},
        .is_tuple = false,
    } });
}

const Person = struct {
    name: []const u8 = "",
    pub fn speak(self: @This()) void {
        std.debug.print("{s}\n", .{self.name});
    }
};

const Programmer: type = inherit(Person, struct {
    language: []const u8 = "",
    //cannot use @This() since the final type doesn't exist yet
    pub fn speak(self: Programmer) void {
        std.debug.print("{s} <3 {s}\n", .{self.name, self.language});
    }
    fn as_person(self: Programmer) Person {
        return Person{
            .name = self.name,
        };
    }
});

Double inheritance is a mess since the p.child type can't reflect to its implementing type Programmer. Even if you prevent the name collision by filtering out .parent and .child, the decls you were trying to save are now lost without even more convoluted reflection.

eastmancr avatar Apr 08 '24 09:04 eastmancr

This would be useful for automating runtime linking code 🙂

I was trying to iterate function declarations in an imported header and generate appropriate function pointers (and a function to load them with dlsym), but I found that this is not possible. It would be a little sad to miss out on this cool language feature because it can be abused to get discount interfaces.

TeamPuzel avatar May 23 '24 19:05 TeamPuzel

One real-world use-case I have for this is that I'm currently trying to do some JSON encoding, and I'd like to construct what I thought would be a fairly simple function that adds custom jsonStringify, jsonParse, and jsonParseFromValue functions for the type passed in.

pub fn FlagStruct(comptime Flags: type) type {
    // ...
}

Then, I could define a type like

pub const MyType = struct{
    some_field: Flags,
   
    // automatically gets jsonStringify/jsonParse/jsonParseFromValue
    pub const Flags = FlagStruct(packed struct {
        flag1: bool = false,
        flag2: bool = false,
        // ...
    });
}

deanveloper avatar Jun 25 '24 00:06 deanveloper

I intentionally made this not possible, because this kind of functionality tends to be abused, and is generally not needed to solve any problems elegantly. I personally never want to have to read any Zig code that creates declarations in compile-time logic, I don't want to have to implement such features in the compiler, and I don't want to toil through codifying such behavior into a language specification. I am quite satisfied with the relatively simpler language that we enjoy today, without this feature. I consider this decision unlikely to be reversed.

andrewrk avatar Jul 15 '24 02:07 andrewrk

One real-world use-case I have for this is that I'm currently trying to do some JSON encoding, and I'd like to construct what I thought would be a fairly simple function that adds custom jsonStringify, jsonParse, and jsonParseFromValue functions for the type passed in.

One way to get around this is with usingnamespace:

pub fn FlagsJsonDecls(comptime FlagsT: type) type {
    return struct {
        const Self = This();
        pub fn jsonStringify(self: FlagsT, jw: anytype) !void { ... }
        pub fn jsonParse(alloc: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Self { ... }
        pub fn jsonParseFromValue(alloc: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !Self { ... } 
    }
}

pub const Flags = packed struct {
        flag1: bool = false,
        flag2: bool = false,
        // ...

        pub usingnamespace FlagsJsonDecls(Flags);
}

deanveloper avatar Jul 15 '24 23:07 deanveloper

FYI, found a loop-hole via comptime fields:

const builtin = @import("builtin");
const std = @import("std");

fn Mixin(comptime T: type, M: type) type {
    var fields = @typeInfo(T).Struct.fields;

    for (@typeInfo(M).Struct.decls) |d| {
        const F = @TypeOf(@field(M, d.name));
        fields = fields ++ [_]std.builtin.Type.StructField{.{
            .name = d.name,
            .type = F,
            .default_value = @field(M, d.name),
            .is_comptime = true,
            .alignment = @alignOf(F),
        }};
    }

    return @Type(.{ .Struct = .{
        .layout = .auto,
        .is_tuple = false,
        .fields = fields,
        .decls = &.{},
    } });
}

const Foo = Mixin(struct { name: []const u8 }, struct {
    pub fn sayHello(self: anytype) void {
        std.debug.print("Hello, {s}\n", .{self.name});
    }
});

pub fn main() !void {
    const foo = Foo{ .name = "world" };
    foo.sayHello(foo);
}

It only works for functions (not methods), so it's not that useful, and it's obviously a hack, but if this was supposed to be impossible, then there's a hole in the current design...

Also, it's a bit related to this, because it could be used for dynamic re-exports (so it might be similar issue to incremental compilation) https://github.com/ziglang/zig/issues/20663

cztomsik avatar Aug 09 '24 09:08 cztomsik

I don't think this is bad for the language, and, as shown, it already can be done. Tho Zig should not implement things just to solve someone's specific problem (like C++), this is something that could be implemented in std.meta and if recursive types are ever supported, extended to methods.

Mulling avatar Jan 24 '25 01:01 Mulling

I have a set of structs that all have a function, let's say func and I want a struct that contains all of those functions, named by the struct names. The structs come from a variety of sources files using usingnamespace, and I'd like them to automatically show up. So:

const combined = struct {
   usingnamespace @import("foo.zig");
   usingnamespace @import("bar.zig");
};
const Functions = Extract("func",combined);

and I'm almost there using the idea from @cztomsik, except I get a compile error:

const builtin = @import("builtin");
const std = @import("std");

fn Extract(funcName: [:0]const u8, M: type) type {
    var fields = @typeInfo(struct{}).@"struct".fields;
    for (@typeInfo(M).@"struct".decls) |d| {
        const F = @TypeOf(@field(@field(M, d.name),funcName));
        const F2 = @Type(.{.pointer = .{
            .size= .One,
            .is_const= true,
            .is_volatile= false,
            .alignment= @alignOf(F),
            .address_space= .generic,
            .child= F,
            .is_allowzero= false,
            .sentinel= null,
        }});
        fields = fields ++ [_]std.builtin.Type.StructField{.{
            .name = d.name,
            .type = F2,
            .default_value = @field(@field(M, d.name),funcName),
            .is_comptime = true,
            .alignment = @alignOf(F),
        }};
    }
    @compileLog(fields);
    
    return @Type(.{ .@"struct" = .{
        .layout = .auto,
        .is_tuple = false,
        .fields = fields,
        .decls = &.{},
    } });
}

const combined = struct {
    pub const struct1 = struct {
        pub fn func() void {
            std.debug.print("Hello, struct1\n", .{});
        }
    };
    pub const struct2 = struct {
        pub fn func() void {
            std.debug.print("Goodbye, struct2\n", .{});
        }
    };
};
    
const Foo = Extract("func",combined);

pub fn main() !void {
    const foo = Foo{ };
    foo.struct1();
    foo.struct2();
}

The @compileLog appears to show everything as I expect it to be, but I still get the error: bar.zig:28:12: error: comptime dereference requires 'fn () void' to have a well-defined layout

Any suggestions? Thanks.

dvmason avatar Mar 03 '25 06:03 dvmason

@dvmason I think this does not work because you are mixing the types F and F2. I managed to get it to work on master:

const builtin = @import("builtin");
const std = @import("std");

fn addDecl(comptime name: []const u8, comptime T: type, d: anytype) std.builtin.Type.StructField {
    const F = @TypeOf(@field(T, d.name));

    return .{
        .name = name ++ "_" ++ d.name,
        .type = F,
        .default_value_ptr = @field(T, d.name),
        .is_comptime = true,
        .alignment = @alignOf(F),
    };
}

fn Extract(
    comptime name: []const u8,
    comptime p_name: []const u8,
    comptime P: type,
    comptime q_name: []const u8,
    comptime Q: type,
) type {
    comptime var fields: []const std.builtin.Type.StructField = std.meta.fields(P) ++ std.meta.fields(Q);

    for (std.meta.declarations(P)) |d| {
        if (std.mem.eql(u8, name, d.name)) {
            fields = fields ++ &[_]std.builtin.Type.StructField{addDecl(p_name, P, d)};
        }
    }

    for (std.meta.declarations(Q)) |d| {
        if (std.mem.eql(u8, name, d.name)) {
            fields = fields ++ &[_]std.builtin.Type.StructField{addDecl(q_name, Q, d)};
        }
    }

    return @Type(.{ .@"struct" = .{
        .layout = .auto,
        .is_tuple = false,
        .fields = fields,
        .decls = &.{},
    } });
}

pub const struct1 = struct {
    pub fn func() void {
        std.debug.print("Hello, struct1\n", .{});
    }
};
pub const struct2 = struct {
    pub fn func() void {
        std.debug.print("Goodbye, struct2\n", .{});
    }
};

const Foo = Extract("func", "struct1", struct1, "struct2", struct2);

pub fn main() !void {
    const foo = Foo{};
    foo.struct1_func();
    foo.struct2_func();
}

Mulling avatar Mar 03 '25 17:03 Mulling

Sad that this is'nt planned, this would be so powerfull

HaraldWik avatar May 16 '25 18:05 HaraldWik