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

Tags #1099

Open
BraedonWooding opened this issue Jun 12, 2018 · 32 comments
Open

Tags #1099

BraedonWooding opened this issue Jun 12, 2018 · 32 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@BraedonWooding
Copy link
Contributor

BraedonWooding commented Jun 12, 2018

This was discussed originally here, and elsewhere :).

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;

const Foo = struct {
    x: u32 @("anything") @(bar()),
};

fn bar() i32 {
    return 1234;
}

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.

const Foo = struct {
  x: u32 @tag("A") @tag("B"),
};

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;

fn X() void @tag("MyFunc") {
    ...
}

You would access it like;

// For Struct
for (@typeInfo(Foo).Struct.fields) |field| {
    for (field.Tags) |tag| {
       // Do whatever with tag
    }
}

// For function
for (@typeInfo(X).Fn) |tag| {
    // Do whatever with tag
}

You could also query all objects that have a certain tag like;

comptime const query = @withTag("A"); // returning typeInfo

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

  • Syntax Change: allow @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
  • Builtin.zig change (autogen that is); add 'tags' field to StructField, and Fn that is a string slice (u8 const)
  • Add @tag(tagName: []const u8) and @withTag(tagName: []const u8)->[]const @import("builtin").TypeInfo
    • Note: @tag is not grouped under attributes perhaps but rather is separate as it doesn't have a return type
@andrewrk andrewrk added this to the 0.4.0 milestone Jun 12, 2018
@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jun 12, 2018
@bheads
Copy link

bheads commented Jun 12, 2018

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

@ghost
Copy link

ghost commented Jun 12, 2018

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

@PavelVozenilek
Copy link

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.

@BraedonWooding
Copy link
Contributor Author

@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 @tag("SetName:Bar"), the important thing is to keep it simple because else it gets really difficult to use because the types don't match and because annoying to go through and iterate tags.

@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 @tag("Hide") flag to hide a certain field from serialisation.

@BraedonWooding
Copy link
Contributor Author

BraedonWooding commented Jun 13, 2018

@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 @throws, @doesThisWeirdThing and so on to describe how something behaves, in this tag system it would ONLY be used for allowing the user to do comptime reflection to perform certain actions such as setup serialisation scripts (as above), and perhaps you want to put all functions that have the @tag("Console") into a dictionary for runtime access so you would have something 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.

@ghost
Copy link

ghost commented Jun 13, 2018

okay so a tag is essentially just a string, I'd agree that this is actually easy to read.

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 @tag("Only: 1, 0, -1") should really be an enum or in other cases a custom type (-> enforced by compiler) though and this @tag("many") does not really contain any meaning?.

what confused me in the beginning was this part @(bar()

const Foo = struct {
   x: u32 @("anything") @(bar()),
                         ^~~~
};

what was this supposed to do in the old proposal?

@BraedonWooding
Copy link
Contributor Author

@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 -s a -s b -s c and this was just chosen to simulate that; a better name could be chosen but I was just trying to keep to that. The case of Only actually maybe indicates the sufferings of using a single type ([]u8) to represent all situations as yes a better case would be enum but how they can be implemented efficiently on the builtin side is the difficult position, unless we want something 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; @tag("only", i32(-1), i32(0), i32(1)), or something like that, the data field could even represent anything really a float or whatever; we would take in the arguments as var args storing each argument in the data array, which would be 4*3 (12) bytes; this would be annoying to handle however and is very prone to errors and I don't particularly like this.

I think perhaps the best way to approach this is just encourage string tags that are like hide or many and discourage ones like only: -1, 0, 1. Then later on (or now) if someone has a better idea to allow for the extra types then that can be implemented.

@tgschultz
Copy link
Contributor

tgschultz commented Jun 13, 2018

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 void type.

const Foo = struct {
    x: u32,    _note_anything: void, _func_bar: void,
    
};

fn bar() i32 {
    return 1234;
}

Now Foo will have these names in @typeInfo(Foo).Struct.Fields and we can identify them by their void type and leading _. They can be associated to the x field by their ordering. One issue with this method is that you can't have two fields named identically, so this kind of thing has to happen:

const Pos = struct {
    x: f32, _x_pack_fp2dot14: void, //or _pack_fp2dot14_0
    y: f32, _y_pack_fp2dot14: void,  //or _pack_fp2dot14_1
};

This could potentially be remedied by allowing field names of void type to be repeated, but I'm not sure what consequences might result elsewhere from that. This also couldn't be used to annotate things that aren't fields (although it does make multiple tags simple).

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.

const fp2dot14 = f32;

const Pos = struct {
    .x: fp2dot14,
    .y: fp2dot14,
};

Now in comptime code I could look at the alias with something like @aliasNameOf(T) and know how to serialize the field for use in the netcode. Still no way to apply it to a function though, since we can declare a type alias for a function type, but we can't define a function using it.

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:

const Pos = struct {
    .x: @tag(f32, "fp2dot14"),
    .y: @tag(f32, "fp2dot14"),
};

//or we can make a type alias of the tagged type:
const u32be = @tag(u32, builtin.Endian.Big);

//and have multiple tags
const u12be_lsb = @tag(u12, builtin.Endian.Big, BitPacking.LSBFirst);

//functions don't quite work the same, but:
fn add(x: i32, y: i32) i32 {
    return x + y;
}
@fnTag(add, Console.TwoArgs);

@Hejsil
Copy link
Contributor

Hejsil commented Jun 13, 2018

It was discussed in #676, that we could allow _ as a name in structs which could be used for padding and such. That, together with zero sized types, could be used for everything tags would do:

const S = struct {
    _: json.Ignore,
    ignore_me: u8,

    _: json.CustomFormatter(formatEnum),
    some_enum_field: E,
};

We can ever expand this to all definitions if we allow _ in global scope.

const _ = ConsoleCommand{};
fn someConsoleCommand(c: *Console) void {
    c.print("Hello World\n");
}

With #1047, the @withTag could even be implemented in userland, though it would only operate on type instead of the whole project (which I don't think is realistically possible anyway).

// meta.zig
pub fn withTag(comptime Namespace: type, comptime TagType: type) []const TypeInfo.Definition {
    ...
}

// commands.zig
const commands = comptime meta.withTag(this, ConsoleCommand);

@tgschultz
Copy link
Contributor

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.

@bheads
Copy link

bheads commented Jun 13, 2018

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
json_id would change the mapping, json_type changes parsing types, ect...

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

@thejoshwolfe
Copy link
Contributor

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.

@ghost
Copy link

ghost commented Jun 13, 2018

@BraedonWooding

The bar was to do with the old proposal (under the Clap library issues).

is this something that would be impossible now? I wanted to ask because maybe the current way would miss that (important?) use case.

@bheads
Copy link

bheads commented Jun 13, 2018

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.

@alexnask
Copy link
Contributor

alexnask commented Jun 14, 2018

I also prefer a userspace solution to this problem.
Something roughly along the lines of:

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

@Hejsil
Copy link
Contributor

Hejsil commented Jun 14, 2018

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

  • Libraries could end up searching for tags with the same string, and do something unintended.
  • Parsing the values from the strings is a lot of unnecessary boilerplate and may even make tag compile times slow for large projects.

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

const CustomTag = struct {
    a: u8,
    b: []const u8,
};
const S = struct {
    // I prefer tags to be prefixed just like 'pub' and those.
    // This also makes the language easier to parse
    @tag(CustomTag{ .a = 2, .b = "Hello World" })
    a: u8,

    @tag(CustomTag{ .a = 4, .b = "Hello b" })
    fn b() void {}
};

test "" {
    inline for (@typeInfo(S).Struct.fields) |field| {
        const TagT = field.tag.T;
        const tag = @ptrCast(*const TagT, field.tag.data).*;
        if (TagT == CustomTag) {
            // Do stuff with fields of tag
        }
    }
}

@bheads
Copy link

bheads commented Jun 14, 2018

@Hejsil Like this even better

@BraedonWooding
Copy link
Contributor Author

BraedonWooding commented Jun 15, 2018

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)

@alexnask
Copy link
Contributor

@BraedonWooding

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.

@tgschultz
Copy link
Contributor

tgschultz commented Jun 15, 2018

Pre/postfix @tag() feels very un-Zig-like to me. It doesn't seem to fit well with the rest of the language. All other builtin functions act like functions, but the proposed @tag() doesn't.

For example:

id: u64 @tag("json_required") @tag("json_id", "request id") @tag("json_type", "string"),

What is @tag() here? It isn't a type, which is the only thing expected between the field name and the comma.

args: [][]u8, @tag("json_optional")

If one assumes the above isn't a typo, you'd expect @tag() to be a new definition of some kind.

@tag(CustomTag{ .a = 2, .b = "Hello World" })
    a: u8,

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 @tag() an actual function that returns a type with annotations in its @typeInfo() (which admittedly raises questions regarding inference and type comparisons), it could be a keyword like const or fn, and use the angle bracket syntax:

const Thing = struct {
    a: tag<json.Required, json.Id> [][]u8,
    b: tag<endian.Big> u16,

   tag<std.fmt.Formatter> fn format(...) ... {
       ....
   }
};

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.

@andrewrk andrewrk modified the milestones: 0.4.0, 0.5.0 Sep 28, 2018
@andrewrk andrewrk modified the milestones: 0.5.0, 0.6.0 May 3, 2019
@daurnimator
Copy link
Contributor

daurnimator commented Jul 23, 2019

@hryx told me I should mention that @field currently works as an lvalue which may obliviate the need for tags. Especially when combined with #2937.

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

@GoNZooo
Copy link

GoNZooo commented Jul 23, 2019

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.

@fengb
Copy link
Contributor

fengb commented Feb 20, 2020

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

@pgruenbacher
Copy link

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.

type T struct {
    f1     string "f one"
    f2     string
    f3     string `f three`
    f4, f5 int64  `f four and five`
}
func main() {
    t := reflect.TypeOf(T{})
    f1, _ := t.FieldByName("f1")
    fmt.Println(f1.Tag) // f one
    f4, _ := t.FieldByName("f4")
    fmt.Println(f4.Tag) // f four and five
    f5, _ := t.FieldByName("f5")
    fmt.Println(f5.Tag) // f four and five
}

@ghost ghost mentioned this issue Apr 22, 2020
@ghost
Copy link

ghost commented Apr 28, 2020

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:

  • Key-value or plain tag?
    • Offer both: Make the (tag typedef) config payload a union of key-value pairs ([_][2][]const u8) and plain strings ([_][]const u8), with corresponding std lib functions hasTag(..) and getTagValue(..)
    • hasTag(TaggedTypedef: type, txt: []const u8) bool
    • getTagValue(TaggedTypedef: type, key: []const u8) ?[]const u8
  • Typedef nesting:
    • Might want to allow appending tags at will
    • Might want the possibility of "clearing" existing tags if the typedef base is a tagged typedef itself
    • If a tag typedef is itself nested by another typedef, the nested tag payload must still be available through reflection. This will require "traversing" the typedef hierarchy downwards until either a tag payload is found, or none was found before the plain base type was reached.
  • Typedef coercion:
    • A tag typedef Tt is comptime distinct from its base type/typedef Tb (in terms of their typedef IDs), but for most purposes this distinctness be transparent. We are rather interested in whether a tag payload is attached or not.
    • Automatic coercion between Tt and Tb must work in both directions, and any typedef that would automatically coerce to Tb should coerce automatically to Tt as well.
  • Restrict for simplicity?
    • Forcing typedef tags to wrap pure primitive values (disable nesting) would serve most(?) use cases while being quicker to implement and having less edge cases.
  • Have 4 members of the tag payload union: txt, txtarr, keyvalue, keyvaluearr ?
    • To avoid all the redundant curly braces when the tag payload is a single tag or a single keyvalue pair.

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

Tag appends tags, ReTag "clears" all tags below in the typedef hierarchy, and appends the given tag payload. Instead of erasing payloads directly, ReTag would just act as a stop marker, so that a function searching for a tag or key will stop searching if it reaches a ReTag payload.

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:

  • typedef distincts (as described in Proposal: 'typedef' for zig #5132) have standard behavior where they can automatically coerce down one level in expressions that expect the distinct base type, but typedef tags should not be counted in this regard. distinct(tag(primitive)) or distinct(tag(tag(primitive))) should both coerce down to the first non-tag in expressions. In a sense, tags shouldn't interfere with comptime type checking.

On note 2 in the code comments:

  • Very permissive coercion to typedef tags is beneficial elsewhere, but not when the typedef tag is a function argument type. The "automatic tagging" of any value that is passed to the function is not desirable. The base type or a typedef distinct should be used instead.

@codehz
Copy link
Contributor

codehz commented Oct 2, 2020

@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"),
}

@andrewrk andrewrk modified the milestones: 0.7.0, 0.8.0 Oct 10, 2020
@slimsag
Copy link
Contributor

slimsag commented Feb 7, 2021

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:

There is no hidden control flow, no hidden memory allocations, no preprocessor, and no macros.

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.

@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 May 19, 2021
@Hadron67
Copy link
Contributor

Hadron67 commented May 22, 2021

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

@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
@ThadThompson
Copy link

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 []u8 in the serializer. Currently it looks at the data and takes its best guess:

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 std.fmt and std.json.

@16hournaps
Copy link

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.

@therealcisse
Copy link

therealcisse commented Jun 1, 2024

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

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