-
-
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
Proposal: Decl Literals #9938
Comments
As another data point, 2A would significantly reduce the verbosity of using the type-safe binding for libwayland's wl_listener I came up with. The status quo code is https://github.com/ifreund/river/blob/4b94b9c0839eb75e5a8d3eeaf26e85e516a89015/river/XdgToplevel.zig#L47-L64 destroy: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleDestroy),
map: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleMap),
unmap: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleUnmap),
new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
// Listeners that are only active while the view is mapped
ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure),
commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
request_fullscreen: wl.Listener(*wlr.XdgToplevel.event.SetFullscreen) =
wl.Listener(*wlr.XdgToplevel.event.SetFullscreen).init(handleRequestFullscreen),
request_move: wl.Listener(*wlr.XdgToplevel.event.Move) =
wl.Listener(*wlr.XdgToplevel.event.Move).init(handleRequestMove),
request_resize: wl.Listener(*wlr.XdgToplevel.event.Resize) =
wl.Listener(*wlr.XdgToplevel.event.Resize).init(handleRequestResize),
set_title: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleSetTitle),
set_app_id: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleSetAppId), |
I like the idea, a few technical questions: const z = .z;
var a: SomeType = z; not sure if this should work, although the syntax is the same. What is @typeof(.f())? does this even work like enum literals? |
Although not explicitly stated, assuming this implicitly would also extend to the declarations in enums, this could also enable slightly better cohesion between normal enum values, and enum value "aliases", which are common in C APIs like Vulkan. E.g. const std = @import("std");
const VkResult = enum(i32) {
VK_SUCCESS = 0,
VK_NOT_READY = 1,
VK_TIMEOUT = 2,
VK_EVENT_SET = 3,
VK_EVENT_RESET = 4,
VK_INCOMPLETE = 5,
VK_ERROR_OUT_OF_HOST_MEMORY = -1,
VK_ERROR_OUT_OF_DEVICE_MEMORY = -2,
VK_ERROR_INITIALIZATION_FAILED = -3,
VK_ERROR_DEVICE_LOST = -4,
VK_ERROR_MEMORY_MAP_FAILED = -5,
VK_ERROR_LAYER_NOT_PRESENT = -6,
VK_ERROR_EXTENSION_NOT_PRESENT = -7,
VK_ERROR_FEATURE_NOT_PRESENT = -8,
VK_ERROR_INCOMPATIBLE_DRIVER = -9,
VK_ERROR_TOO_MANY_OBJECTS = -10,
VK_ERROR_FORMAT_NOT_SUPPORTED = -11,
VK_ERROR_FRAGMENTED_POOL = -12,
VK_ERROR_UNKNOWN = -13,
VK_ERROR_OUT_OF_POOL_MEMORY = -1000069000,
VK_ERROR_INVALID_EXTERNAL_HANDLE = -1000072003,
VK_ERROR_FRAGMENTATION = -1000161000,
VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS = -1000257000,
VK_ERROR_SURFACE_LOST_KHR = -1000000000,
VK_ERROR_NATIVE_WINDOW_IN_USE_KHR = -1000000001,
VK_SUBOPTIMAL_KHR = 1000001003,
VK_ERROR_OUT_OF_DATE_KHR = -1000001004,
VK_ERROR_INCOMPATIBLE_DISPLAY_KHR = -1000003001,
VK_ERROR_VALIDATION_FAILED_EXT = -1000011001,
VK_ERROR_INVALID_SHADER_NV = -1000012000,
VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT = -1000158000,
VK_ERROR_NOT_PERMITTED_EXT = -1000174001,
VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT = -1000255000,
VK_THREAD_IDLE_KHR = 1000268000,
VK_THREAD_DONE_KHR = 1000268001,
VK_OPERATION_DEFERRED_KHR = 1000268002,
VK_OPERATION_NOT_DEFERRED_KHR = 1000268003,
VK_PIPELINE_COMPILE_REQUIRED_EXT = 1000297000,
pub const VK_ERROR_OUT_OF_POOL_MEMORY_KHR: @This() = .VK_ERROR_OUT_OF_POOL_MEMORY;
pub const VK_ERROR_INVALID_EXTERNAL_HANDLE_KHR: @This() = .VK_ERROR_INVALID_EXTERNAL_HANDLE;
pub const VK_ERROR_FRAGMENTATION_EXT: @This() = .VK_ERROR_FRAGMENTATION;
pub const VK_ERROR_INVALID_DEVICE_ADDRESS_EXT: @This() = .VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS;
pub const VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS_KHR: @This() = .VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS;
pub const VK_ERROR_PIPELINE_COMPILE_REQUIRED_EXT: @This() = .VK_PIPELINE_COMPILE_REQUIRED_EXT;
};
test {
const expected_result: VkResult = .VK_ERROR_PIPELINE_COMPILE_REQUIRED_EXT;
const actual_result: VkResult = .VK_PIPELINE_COMPILE_REQUIRED_EXT;
try std.testing.expectEqual(expected_result, actual_result);
} But then, would this also enable switching on the alias literals? Obviously, switching on the actual value and the alias would be a compile error, the same as having duplicate switch cases. But it's worth considering. |
This proposal renames enum literals to decl literals, so they are already one and the same. A decl literal will resolve to an enum value when coerced to an enum type with a matching field name.
This is a compile error because there is no result type to bind
Yes, for the same reason switching on enum literals works now. The switch target expressions are coerced to the target type (which would now see aliases), they are all calculated at compile time, and then they are checked for uniqueness and exhaustiveness. So if decl literals are implemented they should "just work" in switches with no extra work. |
I would also argue that the syntax const tag = if (comptime something()) .Fn else .BoundFn;
t = tag{ kind of undermines the explicit-ness required by a regular union assignment. One other idea would be to extend this to also the left-hand side of struct literals: const tag = if (comptime somethin()) .Fn else .BoundFn;
t = .{tag = .{...}}; but i don't think that is very nice either. |
I think that while this proposal is a good idea per se, i really dislike it for my vision of the Zig language. I find the code examples using this proposal way less clear and require way more knowledge of the whole codebase. Imho, this proposal contradicts
Status quo syntax usually answers these questions by looking at the same source file, as i either have a qualified name init ( I feel like this is a step away from the goals of Zig |
@MasterQ32 Do you feel the same way about part 1, or is it just 2A that bothers you? Personally, I could take or leave 2A, it's really only fixing a minor inconvenience. But I think part 1 is really important for the ergonomics of any library that makes heavy use of bit flags. In defense of 2A though, IMO the information that is removed is not relevant to the locations from which it has been removed. Specifically, the language makes no attempt to specify field types on struct initializers. For example: some_struct = .{ // no indication of the type of some_struct
.num_items = 4, // no idea what kind of number this is
.dispatch_type = .disable, // no idea what enum this is
.extra_value = getExtra(), // no indication of the type of extra_value. In case of coercion, it may not even match the return type of getExtra().
.tag = util.makeTag(), // false positive: .tag is not of type `util`.
}; There are many existing cases where actual types are not apparent within struct initializers, so I don't feel that it's inconsistent or a major loss to drop that information in a few more cases. It's not really important to the initializer whether or not However, your example of status quo also does not necessarily indicate what type .work_queue = @as(ArrayList(u8), ArrayList(u8).init(gpa)); If indicating the type is your goal, this proposal makes that easier: .work_queue = @as(ArrayList(u8), .init(gpa)); |
After typing some code with this proposal in mind, i have found some very nice use cases indeed for Part 1. So i revisit my thing and say: Let's do Part 1 for sure |
This doesn’t quite sit right with me. It requires a result type for resolution, but decls aren’t required to be of the same type as their container, so it only works in the specific case where they happen to be coercible. Say I write some code using this feature, then I refactor so the decl is now of a different type; now every occurrence of this feature is broken, so I rewrite into type-on-the-right style. If, later, I decide this was a bad change for whatever reason, all of the existing code still works, and I see no reason to change them as they are not any less neat like that. This “hysteresis” of correct style under refactorings just seems very un-Zig-like to me. And while I’m all about ergonomics enforcing correctness, the cases presented in 1 and 2A (ignoring 2B because I agree with your assessment of it) are only necessarily unergonomic if a type-on-the-left style is enforced, which it is not in general in Zig. That said, I am curious to hear xq’s cases, and I’m not certain how to address ifreund’s case (though I’m not certain it needs to be addressed — I’m not aware of any possible sketchy shortcuts). Just that to me, this feature would need to meet a very high threshold of utility to be justified. |
There are two ways to look at this feature:
From the first point of view, we should probably do it; from the second, we probably shouldn't. Personally, I still lean towards 1., but not very strongly. The "style hysteresis" issue is an interesting point too, though it strikes me as somewhat theoretical. How often would such a change really happen? In the intended use cases (constructor methods and pre-configured structs) the type is what it is by construction. One particular exception I can think of is changing the type of a constructor from |
One thing I'd like to pick up on here is the support for late binding coercion, rather than this being a specific syntax form. Is this support really necessary? I feel that in any scenario where you could use this, the intention would start becoming unclear, to the point where it would be preferable to write the code as you would today, i.e. probably with In that case, rather than changing how the type currently known as Thus, I propose to simply special-case the syntactic form of enum (/decl) literals when a result type is provided. So: const S = struct {
z: u32,
const default_val: S = .{ .z = 123 };
fn init() S {
return default_val;
}
};
// this works
const x0: S = .default_val;
// this does not
const x_lit = .default_val;
const x1: S = lit;
// this works
const y0: S = .init();
// this does not
const y_lit = .init;
const y1: S = y_lit(); I feel that this is a rather less sweeping language change: it's a relatively simple extension of our general preference for type annotations over explicitly typed expressions ( |
If this proposal gets implemented, you can special case "constructor" syntax for functions with no name so we can get rid of the
|
@DerpMcDerp that seems like rather confusing syntax, and I don't really see any benefit over Also, sometimes you have multiple init functions (eg. |
Zero-length identifiers are currently illegal, so that wouldn't even work in status quo; even if it were to become legal again, this proposal does not pose any changes to the rules around accessing declarations, so your example would most likely in all actuality be: const Vec2 = struct {
x: f32,
y: f32,
pub inline fn @""(x: f32, y: f32) Vec2 {
return .{ .x = x, .y = y };
}
};
const pt1 = Vec2.@""(1, 2);
const pt2: Vec2 = .@""(3, 4); |
A few examples of how real Zig APIs (mostly from
All of these cases would result in improved clarity and less possibility for bugs from this proposal, similar to how |
In addition, here are a few cases I hit frequently in the compiler itself.
Here's another point (no longer related to the compiler implementation): this solves an issue which could return if we bring back return value RLS paired with pinned types. Let me elaborate. Return value RLS (whose fate is undecided) alongside #7769 (accepted) gives us the ability to directly [1]: this is a necessary restriction, because PTR on the final alloc type (in the case of multiple initialization peers) could result in RLS demanding an impossible coercion. For instance, in the code |
In a language design meeting on 2024/05/18 between myself, @andrewrk, @SpexGuy, and @thejoshwolfe, the variant of this proposal described in my previous comment was accepted, but with an important caveat. I'll slap on the label for now, but please read this comment for details. The main point of contention for acceptance of this proposal was the issue of namespace ambiguity in enums (and unions). Today, the syntax As such, the following conclusion was reached. We will attempt to introduce the following rule: fields and declarations of a container share a namespace, and thus cannot have the same name. It is as yet undecided if this rule will apply to all containers, or just unions/enums (i.e. it is undecided whether it will apply to structs). If this rule can be introduced without decreasing the quality of Zig code in the wild, then this rule will become a part of the language specification, and the Decl Literals proposal is accepted (for now). Otherwise, this proposal may have to be tweaked or thrown out entirely. Now that I've outlined the state of affairs, I'll quickly summarize the other points discussed during the meeting. I unfortunately didn't take notes, so this is all from memory (if any attendees want to add any points I forgot to mention, by all means do!).
|
First question: if namespace exclusion is to apply to structs, will shadowing rules apply too? const Vec3 = struct {
x: f32, y: f32, z: f32, // occupies Vec3.x, Vec3.y, Vec3.z
pub fn init(x: f32, y: f32, z: f32) Vec3 { // error: function parameter shadows declaration
return .{ .x=x, .y=y, .z=z };
}
}; Second question: how much of the decl usecase can be achieved by instead providing a my_field: ArrayListUnmanaged(u32) = @Here().empty startDelorean(@Here().time_travel); var array: std.ArrayList(u32) = @Here().init(allocator); |
The compiler actually doesn't need any functional changes for this: Sema does reification based on the tag indices of `std.builtin.Type` already! So, no zig1.wasm update is necessary. This change is necessary to disallow name clashes between fields and decls on a type, which is a prerequisite of ziglang#9938.
The compiler actually doesn't need any functional changes for this: Sema does reification based on the tag indices of `std.builtin.Type` already! So, no zig1.wasm update is necessary. This change is necessary to disallow name clashes between fields and decls on a type, which is a prerequisite of ziglang#9938.
The compiler actually doesn't need any functional changes for this: Sema does reification based on the tag indices of `std.builtin.Type` already! So, no zig1.wasm update is necessary. This change is necessary to disallow name clashes between fields and decls on a type, which is a prerequisite of ziglang#9938.
This is a mini-proposal which is accepted as part of ziglang#9938. This compiler and standard library need some changes to obey this rule.
This is a mini-proposal which is accepted as part of ziglang#9938. This compiler and standard library need some changes to obey this rule.
This is a mini-proposal which is accepted as part of ziglang#9938. This compiler and standard library need some changes to obey this rule.
This is a mini-proposal which is accepted as part of ziglang#9938. This compiler and standard library need some changes to obey this rule.
This is mainly useful in conjunction with Decl Literals (ziglang#9938). Resolves: ziglang#19777
This is mainly useful in conjunction with Decl Literals (ziglang#9938). Resolves: ziglang#19777
The compiler actually doesn't need any functional changes for this: Sema does reification based on the tag indices of `std.builtin.Type` already! So, no zig1.wasm update is necessary. This change is necessary to disallow name clashes between fields and decls on a type, which is a prerequisite of ziglang#9938.
This is a mini-proposal which is accepted as part of ziglang#9938. This compiler and standard library need some changes to obey this rule.
This is mainly useful in conjunction with Decl Literals (ziglang#9938). Resolves: ziglang#19777
Enum literals are a powerful and extremely useful feature of Zig. This proposal changes their definition slightly to make them more useful in a wider variety of cases, and renames them to "Decl Literals". I'll start with a thorough description of the feature, and then end with a discussion of the tradeoffs of this proposal.
Description
Part 1: Decl Literals
The current procedure for enum literal casting is to look in the target type for an enum field matching the name of the literal. I propose to generalize that to look instead for a type field (decl or enum/union tag) matching the name of the literal. With this change, decl literals can be coerced to any namespace type. This can be especially useful for modeling default values and common presets. For example:
Part 2: Operations on Decl Literals
We can further define a couple of operations on decl literals, to take advantage of their ability to infer a namespace:
2A:
.decl_literal()
Calling a decl literal does the following operations:
This can remove repetition in initialization code:
2B:
.decl_literal{ .field = value, ... }
Instantiating a decl literal with this syntax does the following:
This extends the current special-case initialization for void tags to work for struct tags as well.
Discussion
1: Decl Literals
An extremely common pattern in C when building a bitfield enum is to create extra named constants for common sets of flags. These defaults often behave like a de-facto enum, with custom specifications being very uncommon. Zig's solution to bitfields is to use packed structs. However, a packed struct can have only one default (
.{}
), which in the case of a bitfield is usually reserved for the zero value. You can declare default values as decls in the bitfield namespace, but in doing so you lose a lot of the ergonomics that those decls might provide. (obj.foo(some.deeply.nested.package.FooFlags.flushCpuCaches)
).This friction causes a conflict when specifying field defaults. You can either specify defaults so that
.{}
is a useful value, or specify defaults so that fields must be correctly initialized. These two things are often not the same. The second one is safer, but the first is often more ergonomic. With decl literals, there is an ergonomic alternative for useful default values which lets.{}
syntax be reserved for intentional initialization.There is an additional tradeoff between modeling such a structure as a packed struct or an extensible enum. In theory, the packed struct is better on nearly all metrics. It documents the bit meanings, reflection code can understand it, and it's clearer and easier to make custom variants. But in the current language, the common case of using a preset is much less ergonomic with a packed struct than an enum. This feature solves that tradeoff, making packed struct the clear choice.
The std lib and stage 2 compiler don't make heavy use of this sort of bitfield API, but it's common in C/C++ libraries and their zig bindings. Some examples:
https://github.com/SpexGuy/Zig-ImGui/blob/1469da84a3d90e9d96a87690f0202475b0f875df/zig/imgui.zig#L53-L97
https://github.com/MasterQ32/SDL.zig/blob/f3a3384e6a7b268eccb4aa566e952b05ff7eebfc/src/wrapper/sdl.zig#L43-L56
I don't believe that this pattern comes from the language design of C, but instead from the high information density of bitfields. This property carries over to Zig, so there shouldn't be any reason that these sorts of APIs wouldn't be desirable in Zig. I suspect the current lack of them comes from the lack of ergonomics surrounding these features, not because there are "better" patterns that we choose to use instead.
2A: Call syntax
I really like this syntax for initialization, and I think it's a consistent extension of the
var x: T = .{}
syntax. With the current pattern,The reader does not necessarily know that the type of
value
ispackage.SomeType
. This is usually true by convention, but careful readers and editor tools cannot know for sure. In contrast, with the new syntax:The reader and tools now know for sure that
value
must be of typepackage.SomeType
. This syntax conveys extra information, and is consistent with a preference forx: T = .{}
overx = T{}
.Examples of code this would affect are everywhere, but here are some examples from the std lib and stage 2:
zig/lib/std/bit_set.zig
Lines 428 to 430 in f42725c
zig/src/Compilation.zig
Lines 1445 to 1451 in f42725c
zig/src/codegen/spirv.zig
Lines 247 to 261 in f42725c
zig/lib/std/os/linux/bpf.zig
Lines 751 to 759 in f42725c
There may be an argument that this is too implicit, and removes information that would have previously been available. However, it is still clear where to look for the relevant function, and it's clear that a function call is being made. It's also clearer now what the return type of the function is, where that was not known before. So I think this change is still reasonable.
2B: Union struct init syntax
This syntax could be used in a large number of places in the std lib and stage 2 compiler. Search for the regex
\.\{ \.\w+ = \.\{
to find them. Some examples for convenience:zig/src/AstGen.zig
Lines 701 to 704 in f42725c
zig/src/AstGen.zig
Lines 6079 to 6082 in f42725c
Because the void tag syntax works, I intuitively expected the proposed syntax to work as well. So I think this feature has a certain amount of consistency on its side. However, it also has some significant drawbacks:
There are alternatives, but I don't like them either:
- The above but also
.tag{ value }
initializestag
tovalue
-
const u: U = .tag = value;
.{}
. Also it's difficult to read, and it's a new syntactic form which would now be allowed in non-typechecked code.-
const u: U = .tag: value;
:
specifies types in all other situations, not values.-
const u: U = .tag value;
val = .tag.{ .x = 4, .y = 6 };
. But we don't use bare word order like this anywhere else in the language. It's probably ambiguous with something.-
const u: U = .tag(init_expr);
Because of this, I don't think 2B should be accepted. But I wanted to put it out there anyway for completeness.
The text was updated successfully, but these errors were encountered: