-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Tags #1099
Comments
I was just thinking that I could use attributes the other day, there are uses for more complicated tags. Ex: you could tag members of a struct for serialization: name, type, optional, required flags ect.. |
I'm not sure I understand the motivation for tags. Could you give a written, practical example maybe? after all I see this sounds easy for you and you want to continue working on harder topics which is great 😃 thanks |
I do not see where this would be more useful than confusing. Java has (or had) something similar, and it was big mess to use. Project guidelines/unwritten rules pushed people to place annotations everywhere, no matter how silly it was. Most even didn't know why they were doing it. Invisibly generated things are evil. For example, serialization is really better to be written down manually, instead of relying on black magic. If there's area where invisibly generated trickery may be of some use, it would be "aspects", and even this mostly to add debugging checks. Aspects, fortunately, do not require tagging things. However, there's one specific case where tags should be allowed, and available for compile time and runtime inspection: in tests. My proposals: #567 and #1010. Also the "this test must fail to compile" feature (#513) could be supported in the same way. |
@bheads what's a use-case for a required flag, couldn't you just state that if the flag isn't given it is false by default and if it is given it is true? Things like names and so on can be implemented maybe even like an arg parser like @monouser7dig a good example would be something like; const OptionsSpec = struct {
h: ?bool @tag("flag"),
fileName: []u8 const,
inputFiles: [][]u8 const @tag("many"),
state: i32 @tag("Only: 1, 0, -1"),
}; Another good use case would be having just a simple |
@PavelVozenilek I definitely understand your plight with Java's annotations, but keep in mind this ISN'T annotations, this is merely extra meta data. For example in java you have stuff like fn ClearScreen() void @tag("Console") { .. } This way you would be able to add all the entire the dictionary with a simple for loop using the query function INSTEAD of having; loadConsoleCommand(Clear);
loadConsoleCommand(Other);
loadConsoleCommand(A);
loadConsoleCommand(B);
loadConsoleCommand(C); Not having 200 lines of 'loads' and just having a simple for loop is a HUGE thing, I've done this so many times in games that it really is an important use-case :). So while I understand your problem with Java, I kind of really want to enforce that the intention is not at all aligned with java and much more with C# attributes (have a look at them if you have time) which are ONLY ever used when the user wants to have a unique attribute for stuff like the function thing I gave above, OR when doing certain stuff like serialization. So I don't see people doing ANY stuff like the Java annotation system, as the only use-case in the std library I can see right now would be for JSON serialization of structs. |
okay so a I see the point for serialization or the game example..although in the game example I think a different overall data structure may be the real answer but I've not dealt with the same situation myself so could be off. The what confused me in the beginning was this part
what was this supposed to do in the old proposal? |
@monouser7dig we needed a string to function dictionary because we wanted to allow users to type commands and have them run, maybe there was a better data structure regardless there was a need to have a tag based system. The bar was to do with the old proposal (under the Clap library issues). Keep in mind that both of those are context sensitive, they make sense in relation to the library for example the clap library allows you to specify that something is a 'many' argument i.e. is like struct Tag {
ptr: usize,
T: type,
} Which would allow you to ptr cast it back into the original value, however this has problems of allocation and requiring unsafe ptr semantics. If you can think of a way to have multiple types in such a way that is safe, the only other way I can think of it is have Tag look something like; struct Tag {
name: []const u8,
data: []const u8,
} You could use the data field to do things like store a float, or an enum or anything and thus the syntax would be; I think perhaps the best way to approach this is just encourage string tags that are like |
I've spent some time thinking about this, and I think it might not even be necessary to introduce a new builtin for most cases. Consider that Zig has a zero-sized
Now
This could potentially be remedied by allowing field names of So from there, my thinking is that what we actually want to annotate is not the field itself, but the type of the field. Currently type aliases have no meaning, they are just different names for the same thing. If we could, at comptime, determine what alias was used for a type then we can just operate on that.
Now in comptime code I could look at the alias with something like My personal preference is not to add a builtin if it can be reasonably avoided, but if we did do that, I propose a small change to the syntax presented:
|
It was discussed in #676, that we could allow
We can ever expand this to all definitions if we allow
With #1047, the
|
I knew I just hadn't spent enough time thinking about it. It didn't occur to me that empty structs are also size zero. |
I was thinking something like this: something like json_required would be a parsing error if not found, or json_option is okay is missing const RpcRequest = struct {
id: u64 @tag("json_required") @tag("json_id", "request id") @tag("json_type", "string"),
method_name: []u8,
args: [][]u8, @tag("json_optional")
};
...
const rpc_request = try json.parse(RpcRequest, some_json_string); |
I don't like the idea of using empty fields to convey something that isn't related to fields. That doesn't convey intent very well. I was recently confused by C++ code that used std:: piecewise_construct as a parameter to a function, even though it doesn't pass any data to the function. A parameter should pass data, and a field should hold data. That's the intent of those constructs. |
is this something that would be impossible now? I wanted to ask because maybe the current way would miss that (important?) use case. |
Using _ to pack data is interesting but relies on someone just knowing that these exists and its not really a solid pattern for tagging fields. |
I also prefer a userspace solution to this problem. const Any = struct {
const Self = this;
_type: type,
value: *const u8,
fn make(val: var) Self {
return Self { ._type=@typeOf(val), .value=@ptrCast(*const u8, &val), };
}
fn get(self: *const Self, comptime T: type) T {
// TODO: Could allow some conversions here, e.g. from [N]T -> []T, []T -> []const T, *T -> *const T, etc.
if (T != self._type) {
@panic("Wrong type.");
}
return @ptrCast(*const T, self.value).*;
}
};
pub const Tag = struct {
const Self = this;
name: []const u8,
value: Any,
pub fn make(name: []const u8, val: var) Self {
return Self { .name=name, .value=Any.make(val), };
}
pub fn get(self: *const Self, comptime T: type) T {
return self.value.get(T);
}
};
pub const TagList = struct {
pub fn make(tuple: var) this {
// ...
}
};
// Checks that the Tags only contain existing fields etc.
pub fn tag_check(comptime T: type) void {
// ...
}
pub fn has_tags(comptime T: type, comptime field: []const u8) bool {
// ...
}
pub fn tagsOf(comptime T: type, comptime field: []const u8) []Tag {
// ...
}
pub fn tagOf(comptime T: type, comptime field: []const u8, comptime tag: []const u8) ?Tag {
// ...
}
pub const RpcRequest = struct {
// With proposed tuple syntax
pub const Tags = TagList.make([
"id", Tag.make("json_required", {}), Tag.make("json_id", "request id"), Tag.make("json_type", "string"),
"args", Tag.make("json_optional", {}),
]);
id: u64,
method_name: []u8,
args: [][]u8,
};
pub fn json_parse(comptime T: type, data_stream: InputStream) T {
comptime tag_check(T);
var instance: T = undefined;
inline for (@typeInfo(T).Struct.fields) |f| {
comptime const is_required = tagOf(T, f.name, "json_required") != null;
// etc etc.
@field(instance, f.name) = read_some_json(f.field_type, data_stream, is_required, ...);
}
return instance;
} |
@thejoshwolfe I agree that intent is not clear. I'd say, if we're going to have tags as a language feature, then it should not use strings.
@BraedonWooding proposed storing the type and a data ptr in the tag, and I think that is the best idea. As for allocation, it is really not an issue, as the compiler already stores the data behind const pointers somewhere without the user needing to worry about allocation.
|
@Hejsil Like this even better |
Wow! Amazing feedback. Most of the proposed 'userland' solutions can be expressed honestly as a bit 'ugly' with @alexnask's probably the least ugly though in that essence it is the least 'tag' like of all of them. I personally prefer the one @Hejsil brought up again (which is honestly is a better derivative of the other idea I had). I think that the issue is quite an important one so it is quite necessary to make it easy to use and not overcomplicated. My only concern is the requirement for a ptrcast though I feel that is less relevant as this is purely compile time code. I'll draft something up this weekend :). I'll do the prefix method as it is easier to code (no changes to the parser if my memory serves me correct), this will just be a first iteration since this idea has quite a bit of contention (often a physical 'variant' helps isolate where the problems are) |
The type erasure code I posted in that comment seems to work in comptime (I did just a couple of tests), so something similar would be fine I think. |
Pre/postfix For example:
What is
If one assumes the above isn't a typo, you'd expect
There's also no precedent for this. Personally, I still feel like userland comptime solutions that don't add new things to the language are a better way to go, but if we're going to add to the language I feel like it should fit better. Aside from my own suggestion of making
The downside of course is that this adds a keyword, and from what I can tell would have the same issue with inference and comparisons as my other suggestion. |
@hryx told me I should mention that This lets you do things like: const ptr = try allocator.alloc(MyConfig);
inline for (@typeInfo(MyConfig).Struct.StructFields) |f| {
@field(ptr, f.name) = try conf.get(f.name, f.field_type);
} Larger sample here |
I think tags make a lot of sense in a system supported by a common set of knowledge and assumptions, but not as a general language feature. It requires more language knowledge, more complicated debugging and more general investigation to figure out what a tag actually means. Being able to reach tags from arbitrary scopes (even cross-library?) would all but guarantee that you can't prove that you've exhaustively figured out what a tag has made happen. To put this into a familiar perspective: One of the examples of zig being a clear language is that it doesn't call functions unless it looks like it. With tags, what you get is arbitrary code being executed arbitrarily many times in arbitrary contexts. Something may or may not be caused by a tag and it's generally speaking hard to guarantee you know exactly what. |
More patterns that are currently usable: const Foo = struct {
const bar__serialize = "varint";
bar: u32,
const baz__serialize = "fixint";
baz: u32,
};
// or maybe
fn Tag(data: var) type {
return packed struct {
pub const tag = data;
};
};
const Foo = struct {
const Varint = Tag("varint");
const Fixint = Tag("fixint");
bar: u32,
bar__serialize: Varint = .{},
baz: u32,
baz__serialize: Fixint = .{},
};
// semi unified
fn Tagged(comptime T: type, tags_: var) type {
return packed struct {
data: T,
tags: tags_,
pub const tags = tags_;
}
}
const Foo = struct {
bar: Tagged(u32, "varint"),
baz: Tagged(u32, "fixint"),
} |
Don't forget golang's spec on this. For the most part I think people are happy with them, but they just don't like that it's inspected at runtime I think... but that's where compile-time inspection would be most useful.
|
Here's an attempt at getting comptime tags with typedef for zig (#5132). It gets a bit verbose, but it is exactly what comptime tags has been described as: a way to attach data to types that are available at comptime through reflection, with no runtime cost. Some things that must be considered with typedef for tags:
Example, key-value tag: const TaggedMyStruct = typedef(MyStruct, .Tag{.keyvalue=.{"key","myvalue"}});
const TagPayloads = struct{
const tag1 : TypedefConfig.Tag = .Tag{.keyvaluearr=.{.{"key","myvalue"}, .{"otherkey","othervalue"}}});
}
const SecondTaggedMyStruct = typedef(MyStruct, TagPayloads.tag1); // can introduce variable for complex tag payloads Example, appending and replacing tags through typedef nesting: const Td1 = typedef(u32, .Tag{.txt=.{"one"}});
const Td2 = typedef(Td1, .Tag{.txt=.{"two"}});
assert(hasTag(Td2,"one") and hasTag(Td2,"two") == true);
_ = typedef(Td2, .Tag{.txt=.{"one"}}); // compile error. cannot append duplicate tag ?
const Td3 = typedef(Td3, .ReTag{.txt=.{"three"}});
assert( (!hasTag(Td3,"one") and hasTag(Td3,"three")) == true);
const Td4 = typedef(u32, .Tag{.keyValue=.{.{"key4","value4"}}});
Example, coercion of typedef tags: const v = MyTagged_u32 = @as(u32,132);
const x = v + 1; // v coerces down to u32, x becomes u32
// MyDistinct_MyTagged = typedef(MyTagged_u32, .Distinct);
const v2 : MyDistinct_MyTagged_u32 = @as(MyDistinct_MyTagged_u32,132);
const x2 : u32 = v2 + 1; // Note 1: should allow v2 to coerce down 2 steps directly to u32?
fn myfunc(value: MyTagged_u32) bool{ ...} // Note 2: typedef tags should not be valid function argument types On note 1 in the code comments:
On note 2 in the code comments:
|
@user00e00 I think they are two different things, this proposal intended to attach metadata into field, your typedef intended to attach metadata into type... The former is actually a decoration to the parent struct, not intended to decorate the type, so they may not useful to create unique types for every field, it will definitely greatly increase the complexity... i.e. const A = struct {
a: u8 @tag("xxx"), // the tag attach to the struct A
b: u8 @tag("yyy"),
} |
It may help to consider the type-safety aspect here, as is being discussed for Go 2: golang/go#23637 More broadly, though, I would argue struct tags and/or more general annotations go against the spirit of:
In Go, struct tags can often cause hidden control flow insofar as the fact that you need to worry about how tags will alter the behavior of your program and it can be difficult to track down what the implication of a struct tag is/isn't:
Tags are clearly useful, but I hope Zig will consider the danger posed by them in the form of tags providing implicit control flow. |
A cleaner pattern available in status quo is taking advantage of the anonymous struct literal: pub const Packet = struct {
id: u32,
data: []const u8,
pub const TAGS = .{
.id = .{ .var_int = true },
.data = .{ .max_length = 567 },
};
}; |
On the intersection of tags, strings, and comptime generation: While studying the standard library's JSON parser to learn the 'Zig Way' of serialization, this issue appears in the question of how to interpret a if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(value)) {
try outputJsonString(value, options, out_stream);
return;
}
// Output value as an array Taking a binary array and examining it at runtime to 'see if it looks like a string' to determine how to send it seems non-deterministically fragile. Since the resolution of #234 was not to create a distinct datatype for strings, an alternative would be to have a tag on a field indicating precisely how it should be serialized: as a string, as an array, or perhaps a base-64 encoded binary string if one so preferred. Which, for better or worse is exactly the kind of code generation / control flow changes to which @slimsag is referring. However, this seems to already be happening extensively in places with comptime evaluation like |
This will allow creation of requirement based testing and requirement tracing framework directly in the code. No more weird external comment-based tools etc. Useful for any serious quality management to straight up ASIL development. Really like this. |
Type attributes in zig would be very useful. const std = @import("std");
const JsonAttr = union(enum) {
name: struct { []const u8 },
ignoreNull,
pub fn name(comptime value: []const u8) JsonAttr.name {
return JsonAttr{.name = value};
}
pub fn ignoreNull() JsonAttr.ignoreNull {
return JsonAttr{.ignoreNull = {}};
}
};
const Person attr(.{1, 2, 3}) = struct {
firstName: []const u8 attr(.{JsonAttr.name("first_name")}),
middleName: ?[]const u8 attr(.{JsonAttr.name("middle_name"), JsonAttr.ignoreNull()}),
lastName: []const u8 attr(.{JsonAttr.name("last_name")}),
age: u32,
};
pub fn main() !void {
// const person_attrs = std.meta.attrs(Person); // @attrsOf(Person)
// .{1, 2, 3}
const fields = std.meta.fields(Person);
std.debug.print("\n", .{});
inline for (fields) |f| {
std.debug.print("name: {s}, type: {s}\n", .{f.name, @typeName(f.type)});
const attrs: anytype = f.attrs;
inline for(attrs) |a| {
switch (a) {
JsonAttr.name => |n| std.debug.print("{s}\n", .{n}),
JsonAttr.ignoreNull => std.debug.print("ignoreNull\n", .{}),
}
std.debug.print("\n", .{});
}
}
_ = p;
}
|
Motivation
I've been looking at implementing lambda functions (by first making all functions anonymous) but I feel like I should begin with something a little easier as to help me understand the codebase; I've recently had need for reflection metadata in a codegen which generates getters/setters (amongst other things) for structs in relation to a stack based system for a markup language that translates to a IR/bytecode representation, anyways it would be nice to allow users to disable/hide this generation with a hide tag (as well as customise such as link getters/setters to other functions). Amongst other tags, regardless this would be a nice feature I'm sure many authors would enjoy. I'll be looking towards implementing this, this upcoming weekend but I wanted to gather opinion on syntax :).
Proposal
The original proposal was this;
However this proposes a problem of how one would access the tags, since they aren't purely the same type; in the majority of cases (specifically I can't think of a case where this isn't true) you just want a string tag not a 'boolean' tag since in reality having something like
@(true)
is meaningless compared to@("Hide")
so I propose the following restriction; all tags are just strings and@("X")
is replaced with@tag("X")
this makes it a little simpler to read and a lot simpler to actually handle.Also tags will be able to be applied to almost anything that is globally scoped that is functions (global), structs (note: variables can't have tags) having something like;
You would access it like;
You could also query all objects that have a certain tag like;
Now this would look through each typeInfo available returning a static array of all that have it, however we could also maintain a map of each tag to a list of typeinfo making this much much more efficient but increase the size of the running compiler (I feel a valid tradeoff).
Actual Changes
@tag(...)
after types in global 'objects' (functions, structs), currently only@tag(...)
but probably add syntactically support for any attribute (in terms of format@<...>(<...>)
but lexically only support tag, this would allow us to expand it later for other attributes if wanted@tag(tagName: []const u8)
and@withTag(tagName: []const u8)->[]const @import("builtin").TypeInfo
@tag
is not grouped under attributes perhaps but rather is separate as it doesn't have a return typeThe text was updated successfully, but these errors were encountered: