diff --git a/build.zig b/build.zig index 02ad592..f4b4405 100644 --- a/build.zig +++ b/build.zig @@ -1308,6 +1308,14 @@ const exercises = [_]Exercise{ \\& 1110 // (bitmask) \\= 0110 }, + .{ + .main_file = "111_packed.zig", + .output = "", + }, + .{ + .main_file = "112_packed2.zig", + .output = "", + }, .{ .main_file = "999_the_end.zig", .output = diff --git a/exercises/111_packed.zig b/exercises/111_packed.zig new file mode 100644 index 0000000..1058f76 --- /dev/null +++ b/exercises/111_packed.zig @@ -0,0 +1,175 @@ +// +// We've already learned plenty about bit manipulation using bitwise operations +// in exercices 097 and 098 and in quiz 110. The techniques we already know work +// just fine, but creating masks and shifting individual bits around can become +// quite tedious and unwieldy pretty quickly. +// What if there was a better, a more convenient way to control invidivual bits? +// +// Luckily, Zig has a keyword for exactly this purpose: +// +// packed +// +// It doesn't do anything on its own, to unlock its potential (and to get our +// program to compile) we have to attach it either to a struct or to a union +// declaration: +// +// const Foo = packed struct { ... }; +// const Bar = packed union { ... }; +// +// Now, what does this keyword even do? +// To answer this question we first have to talk about *container layouts*. +// +// Plain structs and unions use the `auto` layout; it gives no guarantees about +// their size or the order of the fields they contain, both are fully up to the +// compiler (though both size and field order *are* guaranteed to be the same +// across any single compilation unit). +// +// Attaching the `packed` keyword to a container makes it use `packed` layout: +// Suddenly, all of its fields are *packed* together tightly without any padding +// in between and their order is guaranteed to be the same as the one specified +// in our source code. For structs, the size of the container is guaranteed to +// be the sum of the (bit-)sizes of all of its fields. For unions, all fields +// have to have the exact same (bit-)size (no padding allowed!); the union itself +// is also guaranteed to be exactly of this size. +// +// If you're familiar with C, you might have already heard of structure packing +// in a different context: arranging fields in a way that minimizes the amount +// of alignment padding between them (or having the compiler do it for you). +// This is *not* what Zig's `packed` keyword is for! +// +// Try to make the comptime assertions below pass: + +const PackedStruct = packed struct { + a: u2, + b: u?, +}; + +comptime { + assert(@bitSizeOf(PackedStruct) == 6); +} + +const PackedUnion = packed union { + a: bool, + b: u?, +}; + +comptime { + assert(@bitSizeOf(PackedUnion) == 1); +} + +// Now, how can we use this new knowledge to manipulate some bits? +// +// As you might have already guessed, `packed` containers are very useful for +// representing bitflags or other tightly packed collections of bit-sized values +// often found in file headers and network protocols. +// +// Let's take a look at a real-life example: +// The LZ4 compression format (†) specifies a frame format to describe compressed +// data. Each LZ4 frame has a descriptor, and each descriptor contains a 'FLG' +// byte that specifies the contents of its frame: + +/// | BitNb | 7-6 | 5 | 4 | 3 | 2 | 1 | 0 | +/// | ------- |-------|-------|----------|------|----------|--------|------| +/// |FieldName|Version|B.Indep|B.Checksum|C.Size|C.Checksum|Reserved|DictID| +/// +const FLG = packed struct(u8) { + dict_id: bool, + reserved: u1 = 0, + content_checksum: bool, + content_size: bool, + block_checksum: bool, + block_indepencence: bool, + version: u2, +}; + +// Wait, what's with the `(u8)` after the `struct` keyword? What do integers have +// to do with all of this? +// Well, this is a good opportunity to come clear about something: +// packed structs and packed unions aren't actually structs or unions at all... +// They are merely integers in disguise! For all intents and purposes, their +// fields are just convenient names for ranges of their underlying bits. To make +// it easier to enforce size requirements for packed containers, Zig allows us +// to specify a *backing integer* for them, just like for enums. +// +// In the case of `FLG`, we want our struct to occupy exactly a single byte, so +// we specify `u8` as the backing integer. It's safe to convert between a packed +// container and its backing integer using the builtin `@bitCast`. +// The LZ4 spec also mandates that reserved bits must always be zero, so it's +// good practice to set `0` as a default value for `reserved`. +// +// The fields of a packed struct start at the least significant bit of its backing +// integer and end at its most significant bit. This is the case no matter what +// endianness our target has. +// +// Try to silence the complaints below: + +const Bits = packed struct(u4) { + a: u1 = 0, + b: u1 = 0, + c: u1 = 0, + d: u1 = 0, +}; + +pub fn main() void { + { + const expected: Bits = @bitCast(@as(u4, 0b1000)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0001)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0010)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0011)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b1101)); + const my_bits: Bits = .{}; + if (my_bits != expected) complain(my_bits, expected, @src()); + } +} + +// As we can see, equality comparisons (`==` and `!=`) work for packed structs. +// They also work for packed unions. However, since packed containers are not +// naturally ordered, we can't use any other comparison operators on them. +// +// It's also possible to use packed containers in `switch` statements, which we +// will cover in the next exercise! +// +// Since packed containers make very strong guarantees about their memory layout, +// only a handful of types are eligible to be part of them. +// The following types are allowed as field types: +// +// - integers +// - floats +// - bool +// - void +// - enums with explicit backing integers +// - packed unions +// - packed structs +// + +const std = @import("std"); +const assert = std.debug.assert; + +fn complain(my_bits: Bits, expected: Bits, src_loc: std.builtin.SourceLocation) void { + std.debug.print( + "That's not quite right! You've got 0b{b:0>4}, but we want 0b{b:0>4} in line {d}.\n", + .{ @as(u4, @bitCast(my_bits)), @as(u4, @bitCast(expected)), src_loc.line }, + ); +} + +// (†) https://github.com/lz4/lz4/blob/5c4c1fb2354133e1f3b087a341576985f8114bd5/doc/lz4_Frame_format.md#frame-descriptor diff --git a/exercises/112_packed2.zig b/exercises/112_packed2.zig new file mode 100644 index 0000000..2b6b558 --- /dev/null +++ b/exercises/112_packed2.zig @@ -0,0 +1,78 @@ +// +// We've already learned about switch statements in exercises 030, 031 and 108. +// They also work with packed containers: + +const S = packed struct(u2) { + a: bool, + b: i1, +}; + +// Try to make it compile without adding an `else` prong! + +comptime { + const s: S = .{ .a = true, .b = -1 }; + switch (s) { + .{ .a = true, .b = -1 } => {}, // ok! + .{ .a = true, .b = ??? }, + .{ .a = ???, .b = 0 }, + .{ .a = ???, .b = ??? }, + => @compileError("We don't want to end up here!"), + } +} + +// As we can see, switching on packed structs is pretty straightforward. +// When switching on packed unions however, we'll realize that a packed +// union never keeps track of its active tag, not even in debug mode! This +// means that packed unions compare solely by their bit pattern (again, just +// like integers). + +const U = packed union(u2) { + a: u2, + b: i2, +}; + +// Find and remove the duplicate case! + +comptime { + const u: U = .{ .a = 3 }; + switch (u) { + .{ .a = 3 } => {}, // ok! + .{ .a = 2 }, + .{ .b = 1 }, + .{ .b = -1 }, + .{ .a = 0 }, + => @compileError("We don't want to end up here!"), + } +} + +// Since packed unions don't have the concept of an active tag, it's always legal +// to access any of their fields. This can be useful to view the same data from +// different perspectives seamlessly. +// +// Try to make the float below negative: + +/// IEEE 754 half precision float +const Float = packed union(u16) { + value: f16, + bits: packed struct(u16) { + mantissa: u10, + exponent: u5, + sign: u1, + }, +}; + +pub fn main() void { + // Reminder: if the sign bit of a float is set, the number is negative! + + var number: Float = .{ .value = 2.34 }; + number.bits.??? = ???; + if (number.value != -2.34) { + std.debug.print("Make it negative!\n", .{}); + } +} + +// This concludes our introduction to packed containers. The next time you need +// control over individual bits, keep them in mind as a potent alternative! +// + +const std = @import("std"); diff --git a/patches/patches/111_packed.patch b/patches/patches/111_packed.patch new file mode 100644 index 0000000..d38ac68 --- /dev/null +++ b/patches/patches/111_packed.patch @@ -0,0 +1,57 @@ +--- exercises/111_packed.zig 2026-03-13 11:18:44 ++++ answers/111_packed.zig 2026-03-13 11:18:57 +@@ -41,7 +41,7 @@ + + const PackedStruct = packed struct { + a: u2, +- b: u?, ++ b: u4, + }; + + comptime { +@@ -50,7 +50,7 @@ + + const PackedUnion = packed union { + a: bool, +- b: u?, ++ b: u1, + }; + + comptime { +@@ -113,31 +113,31 @@ + pub fn main() void { + { + const expected: Bits = @bitCast(@as(u4, 0b1000)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .d = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0001)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0010)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .b = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b0011)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1, .b = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + + { + const expected: Bits = @bitCast(@as(u4, 0b1101)); +- const my_bits: Bits = .{}; ++ const my_bits: Bits = .{ .a = 1, .c = 1, .d = 1 }; + if (my_bits != expected) complain(my_bits, expected, @src()); + } + } diff --git a/patches/patches/112_packed2.patch b/patches/patches/112_packed2.patch new file mode 100644 index 0000000..9b01eb3 --- /dev/null +++ b/patches/patches/112_packed2.patch @@ -0,0 +1,32 @@ +--- exercises/112_packed2.zig 2026-03-13 11:14:08 ++++ answers/112_packed2.zig 2026-03-13 11:14:16 +@@ -13,9 +13,9 @@ + const s: S = .{ .a = true, .b = -1 }; + switch (s) { + .{ .a = true, .b = -1 } => {}, // ok! +- .{ .a = true, .b = ??? }, +- .{ .a = ???, .b = 0 }, +- .{ .a = ???, .b = ??? }, ++ .{ .a = true, .b = 0 }, ++ .{ .a = false, .b = 0 }, ++ .{ .a = false, .b = -1 }, + => @compileError("We don't want to end up here!"), + } + } +@@ -39,7 +39,6 @@ + .{ .a = 3 } => {}, // ok! + .{ .a = 2 }, + .{ .b = 1 }, +- .{ .b = -1 }, + .{ .a = 0 }, + => @compileError("We don't want to end up here!"), + } +@@ -65,7 +64,7 @@ + // Reminder: if the sign bit of a float is set, the number is negative! + + var number: Float = .{ .value = 2.34 }; +- number.bits.??? = ???; ++ number.bits.sign = 1; + if (number.value != -2.34) { + std.debug.print("Make it negative!\n", .{}); + }