-
-
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
remove u0 from zig #1530
Comments
u0 can still be created with |
In Rust, structs with 0 members can be used as a global "tag" or marker. One excellent trait about structs is that you can apply traits to them. trait DoThing {
fn do_thing(&self) -> u32;
}
struct X;
struct Y;
impl DoThing for X {
fn do_thing(&self) -> i32 { 1 }
}
impl DoThing for Y {
fn do_thing(&self) -> i32 { 2 }
}
fn main() {
let box: Box<dyn DoThing> = Box::new(X);
println!(box.do_thing());
} This is a basic version of what I was doing. I wanted to store these markers and use certain functions on them. Now, my actual code generated the structs & implementations using a macro, but I still have a use case for 0 members in a struct. I could have implemented this by generating functions instead and storing the function pointer, but that's not as clean. The Rust docs say:
I don't know how well this applies to Zig, as it doesn't have a whole lot of polymorphism yet (iirc), but implementing methods / traits onto these markers was a use case that fit my situation. |
@kristate what's your explanation for why u0 shouldn't be allowed? Here's why it should be allowed:
|
@andrewrk thanks for the thought exercise.
No,
I cannot imagine such an edge case, and if one should occur, I would imagine that this would be a fault in the language design and/or implementation. Furthermore, after writing my patch (#1531) for this bug in zig, I discovered that LLVM does not support integers of zero bits and that there is an upper limit of Likewise, no part of the zig language or standard library requires I also searched for a language that allows initialization of a zero-bit-length integer type and could not find one. In conclusion:
This is not a proposal, but a bug in the language. |
My 2 cents:
assert(@(typeId(u0) == TypeId.Int); // true
// Works fine // Causes a seg fault in compler and is a bug, if allowed u0 can be used in calculations.
A mistake in LLVM?
That could change.
Doesn't seem like a strong argument
Arguable |
Just a point of clarity- LLVM does not have the concept of 0-bit types. Zig does. Zig simply avoids emitting instructions for zero bit types, since the values are all compile time known by definition. In the above crashing example, there is missing code in zig to detect that we knew the value of a u0 at compile-time, and so we incorrectly try to use a 0 bit type in LLVM. With the fix, any u0 value would always be comptime known to be zero. This is the same as what we do for structs with all void fields, and enums with only 1 tag. |
For what it's worth, I am not against 0-bit types. I am saying that 0-bit integers (a data type that represents some range of mathematical integers) don't exist and are a misnomer. There is no range/integral from zero to zero. |
It's sound according to mathematics. |
@andrewrk how would you store a 0-bit integer? |
When you store any integer, there is the bits that you place in the storage, and there is the type information (that it is an integer, the size of the integer, that it is stored in twos complement form, whether it is unsigned) which is known but not actually stored. When you store a u0, you know that there is only one possible value. So it takes up 0 bits of storage. It's brilliant, you don't actually store anything. When you load it, same thing. You know that there is only one value. So when you load it, you just know that the value is 0 and don't actually do any loading. |
Well, if we're going to have u0, we should at least implement is properly. printing a u0 does not work: const std = @import("std");
test "print u0" {
var uz: u0 = 0;
std.debug.warn("uz: {}", uz);
}
|
@winksaville just for clarification, |
The math checks out. Unfortunately, computers can only store bits, not half bits. |
I'm sorry I don't follow your argument. Where does a half bit come in to play? |
@kristate, I never doubted you that it was backed by void, but it seems from the users point of view it isn't. |
Should a bug be filed for:
|
Here are the integers u8, u7, u6, u5, u4, u3, u2, u1, u0 and their relationship with log base 2:
pow(2, 0) == 1 everything is fine |
I think the confusion here can be demonstrated with an analogy to enums: const Foo = enum { One }; This enum has 1 possible state. It can only be the value const Bar = enum {}; This is a compile error. This type is impossible, because there are zero possible states. Note that this is not analogous to |
So, one problem I see is that structs You cannot store and retrieve zero bytes. const bob = struct {
a: u0,
b: u0,
};
const jill = struct {
a: u0,
b: u0,
c: u0,
}; |
How is this different than const bob = struct {
a: u8,
};
const jill = struct {
a: u8,
}; either way, you don't know if you got a bob or a jill without metadata. |
@andrewrk it's completely different? the members of those structs are the same. In my example, |
I'll re-open this if there is a sufficiently convincing argument. As it stands I am strongly convinced that u0 should remain a valid integer type in Zig. |
This comment has been minimized.
This comment has been minimized.
Here are some current properties of u0: @sizeof(u0) is 0
Address of
Appears you can dereference a null address
Empty struct behaves like u0:
|
All zero bit types have the serialization problem ( Idk exactly the use case for Btw, have you always been able to compare none optional pointers with null (@winksaville example)? Seems like a bug. |
It is. Thanks @winksaville for the example. I filed #1539 |
For what it is worth, the code I have to read and write arbitrary types doesn't have any trouble writing a u0 without modification (it outputs nothing). The read function fails because Also of note, |
@Hejsil a Just like everyone has seen with this type, if it were a real integer type, we would not be having all of these faults. Instead, in order to support u0, we have to do all of these extra hacks on the language to support a type that has no use. |
@kristate I agreed with the fact that all zero sized types make coder harder to reason about on a machine code level, because, well, there is no machine code to reason about. I don't see how
and:
What
This is the same as how |
@Hejsil presumably a serialization library would serialize the
This seems like the right answer, since the programmer is taking care of the special
Yes, I am perfectly fine accepting |
A If you were going to serialize a The same JSON deserialization code that works for any integer type would handle |
Just for clarification, how would removing |
I just wrote some code that demonstrates uses of It's conceivable that a data structure like that could be useful in some situation. (It just occurred to me that I didn't document the idea of the data structure. I'll do that here.) You have integer keys to a hashtable, but it's not a typical hashtable implementation. It's sharded on the top A typical case for this data structure might be sharding a u32 key based on the top 8 bits (into 256 shards). But the corner cases are where this gets interesting.
In the case that you shard on the top Another strange case is if you shard on the top 1 bit of a I admit that the corner cases that demonstrate the use of The one part of I think I'm going to write a proposal for some enhancements to shift-by-comptime_int semantics now... |
Also, under
Idk if this is a good idea. Maybe Should I open a proposal for this? Edit: Never written lock free code, so I might be missing details that make this impossible |
Feel free to do that. Reading your comment though I have to agree that probably atomicrmw should be defined to always load/store memory since that is its main purpose. |
@Hejsil @andrewrk Hejsil makes a good point here. These are the kind of mistakes that I think that |
I reread that HN thread and found the comment I was after[0]:
I still do not know what the clear win is for |
I personally can't come up with runtime vulnerabilities that
@tgschultz (i think this is MajorLag on IRC) have pointed out many times how it is not clear when something happens at comptime vs runtime. @andrewrk are you sure closing and rejecting this issue is a good idea, when the design and implementation of |
name resolution happens in pass1, so here's an updated example: test "u0" {
generic(u0, 0, 0);
}
fn generic(comptime T: type, a: T, b: T) void {
if (a + b != 0) {
ssss();
}
}
fn ssss() void {
@compileError("this was analyzed");
} This passes. Here's a similar test that passes, that uses u32: test "u0" {
generic(u32, 0);
}
fn generic(comptime T: type, a: T) void {
if (a < 0) {
ssss();
}
}
fn ssss() void {
@compileError("this was analyzed");
} There is precedent for expressions to potentially result in a comptime value, and that is fine. The only time it matters whether something is comptime or not is when you need it to be comptime, and in this case you can force it to be with the
It seems fine to me - zig already has the concept of comptime operations, and it already has the concept of 0 bit types. u0 fits in like a perfect puzzle piece. There was some missing compiler code in the comptime implementation, but that was also true for enums with 1 tag, void, structs with no fields, etc. It should be pretty stable now. My take on this issue is that I have been very patient in hearing out the case for removing u0, and the case is weak, and the matter is settled. It's time to move on to other issues. |
I have no opinion on this subject, just some information and comments. Many of you are doubting whether u0 is a valid type, and theoretically, it is valid. From a type theory's perspective, u0 is the "unit type", which is always constructible, but carries no information. void, empty tuple, empty struct, enum with a single element are all the same. u0, u1, u2, ... are closely related by "product of types": u1 = u0 * onebit, u2 = u1 * onebit, ... For a high-level functional language like Haskell or Ocaml, not including u0 would probably be a mistake. But now the problem is, Zig is a low-level language, and cares about machine representations and such. u0 and u1, u2, ... differs in a fundamental way that u0 has no memory representation. Including u0 in Zig breaks the assumption that all integers have a memory representation, which in turn may break things if handled carelessly. I am disappointed with the design decision process. I agree with @Hejsil and think @andrewrk has rejected the case too early. In a language with robustness as the top priority, we should have thoroughly investigated for potential pitfalls, before deciding to include u0 in the language. |
Anyone is welcome to make new arguments and proposals regarding the status quo. The closed status of this issue corresponds to the status quo plan; it does not mean my mind is closed. So if you want it to be reconsidered, you need to provide either a problematic use case, or a fully formed new proposal. |
u0
is not actually an integer type (it's backed byvoid
) and does not hold anything.void
already accomplishes whatu0
sets out to do. (it's not like you can useu0
for any sort of calculation)LLVMIntType()
only supports from1
to(1<<24)-1
bits.u0
outside of testingu0
.I don't believe that
u0
should be a thing. @thejoshwolfe seems to agree:The text was updated successfully, but these errors were encountered: