Skip to content
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

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

Closed
tadeokondrak opened this issue Oct 17, 2020 · 20 comments
Closed

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

tadeokondrak opened this issue Oct 17, 2020 · 20 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@tadeokondrak
Copy link
Contributor

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

More context at #383.

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Oct 17, 2020
@andrewrk andrewrk added this to the 0.8.0 milestone Oct 17, 2020
@ikskuh
Copy link
Contributor

ikskuh commented Oct 20, 2020

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"

@jamii
Copy link

jamii commented Oct 31, 2020

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.

@jecolon
Copy link

jecolon commented May 4, 2021

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,
    } });
}

@SpexGuy
Copy link
Contributor

SpexGuy commented May 18, 2021

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.

@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 May 19, 2021
@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 23, 2021
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@bayo-code
Copy link

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

@InKryption
Copy link
Contributor

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.

@wooster0
Copy link
Contributor

wooster0 commented Sep 28, 2022

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.

@nektro
Copy link
Contributor

nektro commented Sep 28, 2022

this is very unlikely

@david4r4
Copy link
Contributor

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

@wooster0
Copy link
Contributor

wooster0 commented Sep 29, 2022

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.

@cztomsik
Copy link

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".

@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@tau-dev
Copy link
Contributor

tau-dev commented May 5, 2023

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.

@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jul 9, 2023
@SeriousBusiness100
Copy link

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

greytdepression added a commit to greytdepression/traitor that referenced this issue Aug 30, 2023
GATs currently do not work in zig 0.11 due to
ziglang/zig#6709.
@Khitiara
Copy link

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

@eastmancr
Copy link

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.

@TeamPuzel
Copy link

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.

@deanveloper
Copy link

deanveloper commented Jun 25, 2024

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,
        // ...
    });
}

@andrewrk andrewrk closed this as not planned Won't fix, can't repro, duplicate, stale Jul 14, 2024
@andrewrk
Copy link
Member

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.

@deanveloper
Copy link

deanveloper commented Jul 15, 2024

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);
}

@cztomsik
Copy link

cztomsik commented Aug 9, 2024

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)
#20663

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests