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

Proposal for semantics of u0 and i0 types #1625

Closed
winksaville opened this issue Oct 3, 2018 · 14 comments
Closed

Proposal for semantics of u0 and i0 types #1625

winksaville opened this issue Oct 3, 2018 · 14 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@winksaville
Copy link
Contributor

winksaville commented Oct 3, 2018

Proposal:

All members of TypeId.Int should have well defined semantics and all operations
perform as expected. This is currently true except for u0 and i0. This proposal
attempts is to make u0 and i0 first class members of TypeId.Int. Below u0 and
i0 are referred to as x0.

Discussions about x0:

Version 2

x0: Shall have a value of 0.

@sizeOf(x0): Shall be 0.

Address of var x0 or var s: struct { f: x0 }: Shall be an indeterminate non-null value.

Fields in a struct or packed struct: Shall have an address equal to address of its struct + @byteOffsetOf(struct, "field").

x0 in a struct: Has an indeterminate @byteOffsetOf and @bitOffsetOf.

x0in apacked struct: Shall have the @byteOffsetOfand@bitOffsetOfof the following non-zero length field. If there is no following non-zero length field, it shall have the@byteOffsetOfand@bitOffsetOfas if there was au1` field following.

x0 in a extern struct: Not allowed since an x0 is not supported n a C struct.

Version 1

x0: Shall have a value of 0.

@sizeOf(x0): Shall be 0.

Address of var x0 or var s: struct { f: x0 }: Shall be unique, this can be accomplished
by always allocating 1 byte for each var x0 or var s: struct { f: x0 }. It is allowed
to optimize away the allocation if the address of such a variable is known to never be taken.

x0 in a struct: Has an indeterminate @byteOffsetOf and @bitOffsetOf. But shall be
0 to @sizeOf(struct) inclusive for @bytteOffsetOf and 0 to @sizeOf(struct) * 8 inclusive
for @bitOffsetOf.

x0 in a packed struct: Shall have the @byteOffsetOf and @bitOffsetOf of the
following non-zero length field. If there is no following non-zero length field, it shall have
the @byteOffsetOf and @bitOffsetOf as if there was a u1 field following.

x0 in a extern struct: Not allowed since an x0 is not supported n a C struct.

@BarabasGitHub
Copy link
Contributor

Would the address of f1 and f2 also be unique in this case?

var s: packed struct { f1: x0, f2: x0 } 

If I understand correctly they will have the same address, but then I wonder a bit about the unique address rules. Would it not be simpler to just give x0s the same address?

@winksaville
Copy link
Contributor Author

f1 and f2 would have the same offset (zero in this case) and thus the same address. My thinking was they "must" have an address and that making them unique seemed reasonable. In C++ empty structs are required to be unique so it seemed a good choice. If people don't like the unique requirement then what should the value be, I'd suggest usize.max as 0 wouldn't be a good choice.

@winksaville
Copy link
Contributor Author

So I've explored how C handles struct Empty {}; as its the closest thing C has to x0 behavior. And it seems to "guarantee" that the addresses of variables are unique. So I believe we should probably do the same thing in zig:

$ cat main.c
#include "stddef.h"
#include "stdint.h"
#include "stdio.h"
#include "stdlib.h"
#include "memory.h"

#undef NDEBUG
#include "assert.h"

struct Empty {};

struct NestedEmpty {
  struct Empty e;
};

struct Full {
  struct Empty e1;
  int32_t a;
  struct Empty e2;
};

int main() {
  int32_t a = 0;
  struct Empty s;
  struct NestedEmpty ne;
  struct Full f;

  f.a = 1;
  
  printf("%-10s=%20zu\n", "sizeof(a)", sizeof(a));
  printf("%-10s=%20zu\n", "sizeof(s)", sizeof(s));
  printf("%-10s=%20zu\n", "sizeof(ne)", sizeof(ne));
  printf("%-10s=%20zu\n", "sizeof(f)", sizeof(f));
  assert(sizeof(a) == 4);
  assert(sizeof(s) == 0);
  assert(sizeof(ne) == 0);
  assert(sizeof(f) == 4);

  printf("%-10s=%20p\n", "&a", &a);
  printf("%-10s=%20p\n", "&s", &s);
  printf("%-10s=%20p\n", "&ne", &ne);
  printf("%-10s=%20p\n", "&ne.e", &ne.e);
  printf("%-10s=%20p\n", "&f", &f);
  printf("%-10s=%20p\n", "&f.e1", &f.e1);
  printf("%-10s=%20p\n", "&f.a", &f.a);
  printf("%-10s=%20p\n", "&f.e2", &f.e2);
  assert(&a != NULL);
  assert(&s != NULL);
  assert(&ne != NULL);
  assert(&ne.e != NULL);
  assert(&f.e1 != NULL);
  assert(&f.a != NULL);
  assert(&f.e2 != NULL);
  assert((uintptr_t)&a != (uintptr_t)&s);
  assert((uintptr_t)&s != (uintptr_t)&ne);
  assert((uintptr_t)&ne == (uintptr_t)&ne.e);
  assert((uintptr_t)&f != (uintptr_t)&ne);
  assert((uintptr_t)&f == (uintptr_t)&f.e1);
  assert((uintptr_t)&f.e1 == (uintptr_t)&f.a);
  assert((uintptr_t)&f.a != (uintptr_t)&f.e2);
        
  printf("%-35s=%4zu\n", "offsetof(struct NestedEmpty, e)", offsetof(struct NestedEmpty, e));
  printf("%-35s=%4zu\n", "offsetof(struct Full, e1)", offsetof(struct Full, e1));
  printf("%-35s=%4zu\n", "offsetof(struct Full, a)", offsetof(struct Full, a));
  printf("%-35s=%4zu\n", "offsetof(struct Full, e2)", offsetof(struct Full, e2));
  assert(offsetof(struct NestedEmpty, e) == 0);
  assert(offsetof(struct Full, e1) == 0);
  assert(offsetof(struct Full, a) == 0);
  assert(offsetof(struct Full, e2) == 4);

  return 0;
}

And here is the execution using gcc:

$ make clean && make CC=gcc CFLAGS=-O2 && ./main
rm -rf main *.o *.s
gcc -O2 -c main.c -o main.o
gcc -o main main.o
sizeof(a) =                   4
sizeof(s) =                   0
sizeof(ne)=                   0
sizeof(f) =                   4
&a        =      0x7fff4d231490
&s        =      0x7fff4d23148e
&ne       =      0x7fff4d23148f
&ne.e     =      0x7fff4d23148f
&f        =      0x7fff4d231494
&f.e1     =      0x7fff4d231494
&f.a      =      0x7fff4d231494
&f.e2     =      0x7fff4d231498
offsetof(struct NestedEmpty, e)    =   0
offsetof(struct Full, e1)          =   0
offsetof(struct Full, a)           =   0
offsetof(struct Full, e2)          =   4

As noted in my repo, clang actually give s and ne the same address, 0x7ffe0518bbd8, but the code still behaves as if they are different:

$ make clean && make CC=clang CFLAGS=-O2 && ./main
rm -rf main *.o *.s
clang -O2 -c main.c -o main.o
clang -o main main.o
sizeof(a) =                   4
sizeof(s) =                   0
sizeof(ne)=                   0
sizeof(f) =                   4
&a        =      0x7ffe0518bbd4
&s        =      0x7ffe0518bbd8
&ne       =      0x7ffe0518bbd8
&ne.e     =      0x7ffe0518bbd8
&f        =      0x7ffe0518bbd0
&f.e1     =      0x7ffe0518bbd0
&f.a      =      0x7ffe0518bbd0
&f.e2     =      0x7ffe0518bbd4
offsetof(struct NestedEmpty, e)    =   0
offsetof(struct Full, e1)          =   0
offsetof(struct Full, a)           =   0
offsetof(struct Full, e2)          =   4

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Oct 4, 2018
@andrewrk andrewrk added this to the 0.5.0 milestone Oct 4, 2018
@andrewrk
Copy link
Member

andrewrk commented Oct 4, 2018

x0: Shall have a value of 0.

✔️ status quo

@sizeOf(x0): Shall be 0.

✔️ status quo

Address of var x0 or var s: struct { f: x0 }: Shall be unique, this can be accomplished
by always allocating 1 byte for each var x0 or var s: struct { f: x0 }. It is allowed
to optimize away the allocation if the address of such a variable is known to never be taken.

❌ not planned. To get a unique address, @sizeOf(T) must be nonzero. This decision can be challenged with a Real Life Use Case.

x0 in a struct: Has an indeterminate @byteOffsetOf and @bitOffsetOf. But shall be
0 to @sizeOf(struct) inclusive for @bytteOffsetOf and 0 to @sizeOf(struct) * 8 inclusive
for @bitOffsetOf.

❌ not planned. Just as a memory allocation of 0 bytes gives you a slice of undefined[0..0], the byte offset of and bit offset of a 0-bit field in a non-packed struct is undefined. I would accept a proposal to have these functions return undefined for size 0 fields (that includes x0 as well as void, empty structs, and enums with only 1 tag). In practice it would be a compile error for trying to use this field; in practice the field would not get used because the size is 0. This decision can be challenged with a Real Life Use Case.

x0 in a packed struct: Shall have the @byteOffsetOf and @bitOffsetOf of the
following non-zero length field. If there is no following non-zero length field, it shall have
the @byteOffsetOf and @bitOffsetOf as if there was a u1 field following.

✔️ planned

x0 in a extern struct: Not allowed since an x0 is not supported n a C struct.

✔️ status quo

@thejoshwolfe
Copy link
Contributor

how C handles struct Empty {};

It doesn't. You can explore how GCC or Clang or MSVC handle empty structs, but empty structs are not really allowed in C (or maybe they're allowed but compiler dependent, or maybe it's version specific, or maybe C and C++ differ; it's complicated.). I don't like the idea of looking at the corner case behavior of C implementations to inform language design.

@thejoshwolfe
Copy link
Contributor

@winksaville what do you propose should be the behavior of an array of u0?

var array: [100]u0 = undefined;

What is @sizeOf(array)? What about (&array[0] == &array[1])?

@winksaville
Copy link
Contributor Author

Address of var x0 or var s: struct { f: x0 }: Shall be unique, this can be accomplished
by always allocating 1 byte for each var x0 or var s: struct { f: x0 }. It is allowed
to optimize away the allocation if the address of such a variable is known to never be taken.

x not planned. To get a unique address, @sizeof(T) must be nonzero. This decision can be challenged with a Real Life Use Case.

As I showed C allows an entity with a sizeof(xx) == 0 to have unique addresses. I'm not saying just because it works in C it should work in zig, but it is an example and seems reasonable.

But if you don't like uniqueness, what about:
Address of var x0 or var s: struct { f: x0 }: Shall be an indeterminate non-null value.

Regarding structs, you seem to agree all fields in a packed struct have offsets, including x0 fields. It would stand to reason that all fields in a struct should have offsets. As a RLUC I should be able to change a packed struct to a struct, with the limitation that offsets are indeterminate in a struct (the status quo).

So what about:
x0 in a struct: Has an indeterminate @byteOffsetOf and @bitOffsetOf.

We probaby need another rule:
Address of a struct field: Shall be the address of its struct + @byteOffsetOf(struct, "field").

@winksaville
Copy link
Contributor Author

@thejoshwolfe I propose:

assert(@sizeOf(array) == 0);
assert(&array[0] == &array[1]);

@winksaville
Copy link
Contributor Author

@thejoshwolfe what about a future "packed" arrays? Based on the behavior I saw with packed struct I'd propose:

var array: packed [9]u1;
assert(@sizeOf(@typeOf(array)) == 2);
assert(&array[0] == &array[1]);
assert(&array[0] == &array[8]);
assert(@bitOffsetOf(array, 0) == 0);
assert(@bitOffsetOf(array, 8) == 8);

And an array of u0 would be:

var array: packed [9]u0;
assert(@sizeOf(@typeOf(array)) == 0);
assert(&array[0] == &array[1]);
assert(&array[0] == &array[8]);
assert(@bitOffsetOf(array, 0) == 0);
assert(@bitOffsetOf(array, 8) == 0);

@thejoshwolfe
Copy link
Contributor

I don't think there are any plans for packed arrays. That should probably have its own discussion.

@winksaville
Copy link
Contributor Author

I've updated the original post to contian "Version 2" of the proposed list of rules.

@andrewrk
Copy link
Member

andrewrk commented Oct 5, 2018

It looks good, except instead of "indeterminate" the value should literally be usize(undefined) (for offsets) and (*T)(undefined) (for address-of). This is compile-time known undefined, so the programmer is protected from mistakes such as branching on undefined.

winksaville added a commit to winksaville/zig-x0-proposal that referenced this issue Oct 5, 2018
@winksaville
Copy link
Contributor Author

If offsets are undefined will the following execute properly for all TypeId.Int including x0?

$ cat make-struct.zig 
const std = @import("std");
const warn = std.debug.warn;
const assert = std.debug.assert;

fn MakeStruct(comptime TypeF1: type, comptime TypeF2: type, comptime pack: bool) type {
    if (pack) {
        return packed struct {
            f1: TypeF1,
            f2: TypeF2,
        };
    } else {
        return struct {
            f1: TypeF1,
            f2: TypeF2,
        };
    }
}

fn testMakeStruct(comptime pack: bool) void {
    warn("\n");
    const Struct = MakeStruct(u1, u8, pack);
    var s = Struct {
        .f1 = 0,
        .f2 = 0,
    };
    assert(s.f1 == 0);
    assert(s.f2 == 0);
    assert((@ptrToInt(&s) == @ptrToInt(&s.f1)) or (@ptrToInt(&s) == @ptrToInt(&s.f2)));
    warn("{s30} : {}\n", "s", s);
    warn("{s30} : {*}\n", "&s", &s);
    warn("{s30} : {*}\n", "&s.f1", &s.f1);
    warn("{s30} : {*}\n", "&s.f2", &s.f2);
    warn("{s30} : {} {}\n", "@byteOffsetOf f1 f2",
        @intCast(usize, @byteOffsetOf(Struct, "f1")),
        @intCast(usize, @byteOffsetOf(Struct, "f2")));
    warn("{s30} : {} {}\n", "@bitOffsetOf f1 f2",
        @intCast(usize, @bitOffsetOf(Struct, "f1")),
        @intCast(usize, @bitOffsetOf(Struct, "f2")));
}

test "MakeStruct.u1.u8.not.packed" {
    testMakeStruct(false);
}

test "MakeStruct.u1.u8.packed" {
    testMakeStruct(true);
}

When executed the results are:

$ zig test make-struct.zig 
Test 1/2 MakeStruct.u1.u8.not.packed...
s                              : MakeStruct(u1,u8,false){ .f1 = 0, .f2 = 0 }
&s                             : MakeStruct(u1,u8,false)@7ffe53d29c68
&s.f1                          : u1@7ffe53d29c68
&s.f2                          : u8@7ffe53d29c69
@byteOffsetOf f1 f2            : 0 1
@bitOffsetOf f1 f2             : 0 8
OK
Test 2/2 MakeStruct.u1.u8.packed...
s                              : MakeStruct(u1,u8,true){ .f1 = 0, .f2 = 0 }
&s                             : MakeStruct(u1,u8,true)@7ffe53d29c68
&s.f1                          : u1@7ffe53d29c68
&s.f2                          : u8@7ffe53d29c68
@byteOffsetOf f1 f2            : 0 0
@bitOffsetOf f1 f2             : 0 1
OK
All tests passed.

@winksaville
Copy link
Contributor Author

@andrewrk, thoughts on make-struct.zig above.

@andrewrk andrewrk modified the milestones: 0.5.0, 0.6.0 Jul 3, 2019
@andrewrk andrewrk modified the milestones: 0.6.0, 0.7.0 Oct 17, 2019
@andrewrk andrewrk modified the milestones: 0.7.0, 0.6.0 Mar 29, 2020
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

4 participants