-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Comments
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 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 |
A minor usecase - when translate-c can't handle something I make a wrapper function instead:
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 |
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,
} });
} |
There's a bit of a problem with this example actually. In order to build the type, the functions used need to reference |
I just realized that this might not be an issue, if those functions use |
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 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'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. |
this is very unlikely |
For such an specific case I would say you should write Zig code to write Zig code |
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
Both depend on this to be allowed. But so with the So I think because of the existence of
Yeah, I was thinking that as well. Luckily that's not possible in Zig, and shouldn't be. |
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 Being able to provide decls would make it possible to generate both |
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. |
Note, accepted #10710 to replace |
GATs currently do not work in zig 0.11 due to ziglang/zig#6709.
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 |
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. Code examplethis 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 |
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 |
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
Then, I could define a type like
|
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. |
One way to get around this is with 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);
} |
FYI, found a loop-hole via comptime fields:
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) |
The compiler currently errors when the
.decls
slice is non-empty.More context at #383.
The text was updated successfully, but these errors were encountered: