summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Boesch <chrboesch@noreply.codeberg.org>2026-03-13 18:18:50 +0100
committerChris Boesch <chrboesch@noreply.codeberg.org>2026-03-13 18:18:50 +0100
commite6d93d731a6d3b3366e6cb80df3b1a4cf22a81f3 (patch)
treeb1d2c108a0fcb0c88c4f388d44aa9bb769bb12bc
parent1de4e140963df3a038330ab9efb74cc747ae5219 (diff)
parent16a794fbeef0c1d98069dc825e55af38945b5165 (diff)
Merge pull request 'Add exercises for packed structs/unions' (#374) from justusk/ziglings:packed into main
Reviewed-on: https://codeberg.org/ziglings/exercises/pulls/374
-rw-r--r--build.zig10
-rw-r--r--exercises/111_packed.zig175
-rw-r--r--exercises/112_packed2.zig78
-rw-r--r--patches/patches/111_packed.patch57
-rw-r--r--patches/patches/112_packed2.patch32
5 files changed, 351 insertions, 1 deletions
diff --git a/build.zig b/build.zig
index 1099135..f4b4405 100644
--- a/build.zig
+++ b/build.zig
@@ -265,7 +265,7 @@ pub fn build(b: *Build) !void {
const progress_file_size = try progress_file.length(io);
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const contents = try allocator.alloc(u8, progress_file_size);
@@ -1309,6 +1309,14 @@ const exercises = [_]Exercise{
\\= 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", .{});
+ }