-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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: allow inferred-size array in type annotations #19674
Comments
Related: #9056 proposed a similar idea of a pseudo-/placeholder type The ideas seem compatible to me, but maybe their discussions should be kept separate (not sure).
That seems like an unfortunate limitation - it means users have to spell out the size again if they move one branch into its own function.
Again I would be in favor of this for (my perception of) regularity/consistency. |
Yes, I think these proposals deserve to be considered independently of one another.
I don't think this is the case. If you're moving one branch into a function for some reason, just move the type annotation too: const foo = if (xyz) computeFoo() else foo: {
const foo: [_]u32 = ...;
break :foo foo;
}; Seems a little odd, sure, but I would raise the point that this seems like a pretty rare thing to need anyway: it's okay for it to be a little non-trivial to write.
I think it is more conceptually straightforward too to be honest -- but perhaps that's just me being used to status quo and trying to treat this as a "translation" of that concept. The proposal could be amended to support other rvalues here, that wouldn't be too difficult AFAICT -- as long as there was at least one array initialization so that the length was trivially known. Removing that rule would make this far more complex. Thus, I believe the proposal as stated is more reasonable.
Personally, I lean a little towards not allowing it -- it feels dirtier to me to special-case a builtin parameter as opposed to a pure syntax construct -- but I don't really have a strong opinion. To be clear, if accepted right now the proposal would not apply this to
Why could you not take a slice here? I am aware it impacts memoization, but as of the recent type equivalence change, memoization should not have any impact on language semantics. |
I'm not sure this would be beneficial without #5038. Without #5038 this is a different way of doing the same thing. It's also a form of special syntax for type annotations which seems unprecedented.
Why is it discouraged? |
What about blockers like const a: [*:null]const ?[*:0]const u8 = &.{ "a", "b", "c" }; // doesn't compile Are they related/could be covered by this proposal somehow? Or would they need the use of |
Equally,
Sure, but I don't see why it's any worse than the existing special case for
It allows for RLS, is easier for tooling to interpret, and more consistent for humans to read.
This code snippet is completely unrelated to this proposal. |
What about function return types? I would love to get rid of this: https://codeberg.org/kiesel-js/kiesel/src/commit/48e2ebc8f8a5f618d2e89960be9ce7e245941755/src/builtins/global.zig#L32-L36 |
The existing special case applies only in a single, easily understandable situation. Moving the cost x = @as([_]u8, .{ 1, 2, 3 });
cost y = .{ 1, 2, 3 };
cost z = @as([_]u8, y);
const T = struct {
v: [_]u8 = .{ 1, 2, 3 }
};
fn foo() [_]u8 {
return .{ 1, 2, 3};
}
fn bar(comptime x: [_]u8) void {
...
}
// bar(.{ 1 2 3 }); All of these could work in principle... or they could be disallowed, but then you have to explain why. No such questions can arise with the literal notation. |
The following example from the previous post looks rather far fetched IMHO const T = struct {
v: [_]u8 = .{ 1, 2, 3 }
}; but if it's accepted I'd argue that Zig also should allow further generalization of the same, where not only the array dimension can be inferred from the default initializer, but also the entire type. Not sure what the syntax would be, probably the following is not the best idea: const T = struct {
v = T1.init(0)
}; |
Would be good to add some examples where the array in question is an unnamed temporary, rather an initializer for a bindings. One of the more frequent usages for pub fn main() void {
for ([_][]const u8{ "hello", "world" }) |s| {
std.debug.print("s = {s}\n", .{s});
}
} how that would look under this proposal? It seems that I'd have to add a named temporary here, no? |
The example you give wouldn't work with this proposal even if it were permitted for return types. This isn't proposing anything that relies on semantic analysis: to infer the array length of an expression, it must be a trivial array initialization expression. The use of @zzyxyzz Why would you assume that permitting EDIT: it was also just pointed out to me that function parameters are already a special case thanks to the existence of I get that it's an extension of what itself is an extension of my proposal, but what you propose is a completely unrelated issue. Feel free to open your own proposal, but I'm almost 100% confident it would quickly be rejected: there's no reason to allow omitting struct field types like this. To be clear, this proposal does not necessarily depend on #5038. It is possible that this proposal is accepted and #5038 is rejected, in which case that snippet continues to work. However, this proposal does fit best alongside #5038, so assuming both are accepted, then indeed, that snippet will begin to fail. If you really want to do that, the easiest way will indeed be to introduce a temporary. I don't think this is a big loss. Now, a sincere question: can you give an example of a few places where you've written this? I can count the number of times I've used that pattern on one hand (it's, erm, two). It's incredibly rare that I need to loop, at runtime, over a comptime-known fixed set of values, which is not already a constant somewhere. (I say "at runtime" because |
I think that could be pretty reasonable, but it's a separate proposal IMO. That adds a (tiny) bit more complexity to RLS since you don't have the type being iterated over as a result type, so it's another case to consider. Here's what I think of each of the uses you link:
|
Having to write const T = struct {
field: FieldType = FieldType.init(arguments),
.... sometimes gets annoying, reaching its peak when |
I think #9938 is what you really want. You're targeting the wrong bit of syntax: Zig is generally migrating towards having more type annotations, not less. |
I guess I just do, hard to say why exactly :) Jokes aside, my point was that your proposal is not really a trivial rewrite anymore (unlike method calls or Overall, I see potential for complication in this proposal that is not commensurate with the benefit, which purely stylistic when considered apart from #5038, AFAICT. It's how I feel about #5038 in general -- it may be slightly nicer in the 90% case, but it creates enough complexities and inconvenience in the remaining 10 to be no longer worthwhile. My 2c. |
I originally wrote up this proposal in a comment on #5038. I've remained a fan of it since then, so thought it was worth promoting to a proper proposal.
The biggest blocker to eliminating
T{ ... }
syntax from the language (which is a broadly discouraged syntax form) is the existence of inferred-size array literals. Today, the syntax[_]T{ ... }
defines an array whose length is inferred from the number of elements provided. There is no way to achieve the same thing with type annotations, since[_]T
is not an actual type (arrays in Zig always have a fixed length encoded in the type).I propose that we permit the syntax
[_]T
in a type annotation onconst
andvar
(both local and container scope). It could optionally also be allowed as the operand to@as
. Like the[_]T{ ... }
syntax, this results in a normal-fixed size array, and is a specific syntax form: for instance,const x: ([_]T) = ...
is disallowed, just as([_]T){ ... }
is disallowed today. When this "type" is used, the expression with this type is given a new kind of result location. All peers of this expression must be array initialization literals with lengths matching the expected length.I think the best way to get an idea of how this would work is to look at the implementation. The new form of result location would be implemented in
AstGen
like this:When we encounter the first peer array initialization, its length is written to
chosen_len
. Later peers will check that their length matches the other length, and emit an error if not. Thevar
/const
decl will create a stack allocation whose length in the ZIR is retroactively rewritten to match the length of the initialization expressions.This kind of result location will immediately trigger an error when encountered for any expression other than array initializers, such as struct inits and any syntax form which calls
AstGen.rvalue
.Here is what the proposal looks like in practice:
Implementing this proposal would solve the primary blocker for accepting #5038. I personally believe that proposal to be the right direction for the language, but even if it is not accepted, I feel that this proposal is beneficial, because it brings the language further in line with our preference for direct type annotations over explictly-typed expressions.
The text was updated successfully, but these errors were encountered: