-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
This proposal assumes #19754; in particular, it assumes that packed union
s have a backing integer which can be explicitly specified, much like packed struct
s do.
Background
Consider this snippet:
/// This long definition has 256 fields so that the inferred integer tag type
/// is `u8`. We need to do that by having 256 fields because in Zig you can't
/// set enum tag values (`foo = 255`) without an explicitly specified backing
/// integer, which we are intentionally avoiding here.
const Enum = enum {
// zig fmt: off
_00, _01, _02, _03, _04, _05, _06, _07, _08, _09, _0a, _0b, _0c, _0d, _0e, _0f,
_10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _1a, _1b, _1c, _1d, _1e, _1f,
_20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _2a, _2b, _2c, _2d, _2e, _2f,
_30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _3a, _3b, _3c, _3d, _3e, _3f,
_40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _4a, _4b, _4c, _4d, _4e, _4f,
_50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _5a, _5b, _5c, _5d, _5e, _5f,
_60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _6a, _6b, _6c, _6d, _6e, _6f,
_70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _7a, _7b, _7c, _7d, _7e, _7f,
_80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _8a, _8b, _8c, _8d, _8e, _8f,
_90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _9a, _9b, _9c, _9d, _9e, _9f,
_a0, _a1, _a2, _a3, _a4, _a5, _a6, _a7, _a8, _a9, _aa, _ab, _ac, _ad, _ae, _af,
_b0, _b1, _b2, _b3, _b4, _b5, _b6, _b7, _b8, _b9, _ba, _bb, _bc, _bd, _be, _bf,
_c0, _c1, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _ca, _cb, _cc, _cd, _ce, _cf,
_d0, _d1, _d2, _d3, _d4, _d5, _d6, _d7, _d8, _d9, _da, _db, _dc, _dd, _de, _df,
_e0, _e1, _e2, _e3, _e4, _e5, _e6, _e7, _e8, _e9, _ea, _eb, _ec, _ed, _ee, _ef,
_f0, _f1, _f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _fa, _fb, _fc, _fd, _fe, _ff,
// zig fmt: on
};
/// The inferred backing type is `u8`.
const PackedStruct = packed struct { x: u8 };
/// The inferred backing type is `u8`.
const PackedUnion = packed union { x: u8 };
comptime {
assert(@typeInfo(Enum).@"enum".tag_type == u8);
assert(@typeInfo(PackedStruct).@"struct".backing_integer.? == u8);
// `std.builtin.Type.Union.backing_integer` doesn't currently exist, but it
// will under the accepted proposal #19754
//assert(@typeInfo(PackedUnion).@"union".backing_integer.? == u8);
}
const assert = @import("std").debug.assert;
This code is fine, and the assertions pass.
Here's the thing, though. These three types all have an inferred unsigned integer backing type based on their bit count (all u8
). That type changes based on the type's fields, so is at best pretty implicit. In addition, the user has not explicitly specified that the backing type should be unsigned, and the signedness of the backing type has ABI implications (C calling conventions may differ in how they pass signed and unsigned types). As such, it's kind of problematic that this code compiles with the above definitions:
export fn foo(a: Enum, b: PackedStruct, c: PackedUnion) void {
bar(a, b, c);
}
extern fn bar(a: Enum, b: PackedStruct, c: PackedUnion) void;
The ABI behavior here is implicit. Even worse, unsigned and signed types have the same parameter passing convention in most calling conventions, so if this code is wrong, the bug is unlikely to be noticed. Users coming from C may also expect to be able to turn a C enum { ... }
into a Zig enum { ... }
, but the layout is significantly different (c_int
vs a small uint), so passing such a type over ABI boundaries is a potential footgun for new users in particular.
Proposal
enum
s, packed struct
s, and packed union
s should only be allowed in extern
contexts if the backing type was explicitly specified.
So, the previous snippet should emit compile errors which look something like this:
error: parameter of type 'Enum' not allowed in function with calling convention 'c'
note: enum must have explicit backing type to determine its ABI representation
Packability
Another thing worth briefly discussing is whether these types are packable, meaning they are allowed as fields of packed struct
s and packed union
s. The status quo behavior is that enum
s with implicit tag types are not packable, while packed struct
s and packed union
s with implicit backing types are packable. This behavior might feel inconsistent at first, but it probably makes sense: the number of bits of an enum with an implicit integer tag type is not obvious (you'd need to count the fields!), while the number of bits of a packed struct
or packed union
is fairly obvious even with an implicit backing type: you just add up its fields. As such, status quo semantics here are probably reasonable. If we did want to consider any change to them, that would be a separate proposal.