Configuration: implement UnionList storage

This commit is contained in:
Andrew Kelley 2026-02-26 18:44:23 -08:00
parent f45c4925a5
commit e911add06f
5 changed files with 301 additions and 66 deletions

View file

@ -15,6 +15,7 @@ pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void {
std.log.err("TODO also print unlazy deps", .{});
std.log.err("TODO also print system integrations", .{});
std.log.err("TODO also print available options", .{});
std.log.err("TODO also print modules", .{});
const c = &sc.configuration;
var serializer: Serializer = .{ .writer = w };
var s = try serializer.beginStruct(.{});
@ -83,6 +84,7 @@ fn printValue(sc: *const ScannedConfig, s: *Serializer, comptime Field: type, fi
.flag_optional => comptime unreachable,
.flag_length_prefixed_list => comptime unreachable,
.enum_optional => comptime unreachable,
.union_list => comptime unreachable,
} else if (std.enums.tagName(Field, field_value)) |name| {
try s.ident(name);
} else {
@ -105,6 +107,7 @@ fn printValue(sc: *const ScannedConfig, s: *Serializer, comptime Field: type, fi
try printValue(sc, s, @TypeOf(field_value.slice), field_value.slice);
},
.extended => @compileError("TODO"),
.union_list => @compileError("TODO"),
},
else => @compileError("not implemented: " ++ @typeName(Field)),
},

View file

@ -224,6 +224,8 @@ const Serialize = struct {
wc: *Configuration.Wip,
module_map: std.AutoArrayHashMapUnmanaged(*std.Build.Module, Configuration.Module.Index) = .empty,
package_map: std.AutoArrayHashMapUnmanaged(*std.Build, Configuration.Package.Index) = .empty,
/// Index corresponds to `Configuration.steps` index.
step_map: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty,
fn builderToPackage(s: *Serialize, b: *std.Build) !Configuration.Package.Index {
if (b.pkg_hash.len == 0) return .root;
@ -291,6 +293,56 @@ const Serialize = struct {
return if (opt_slice) |slice| try s.wc.addString(slice) else null;
}
fn addSystemLib(s: *Serialize, sl: *const std.Build.Module.SystemLib) !Configuration.SystemLib.Index {
log.err("TODO deduplicate addSystemLib", .{});
const wc = s.wc;
return @enumFromInt(try wc.addExtra(@as(Configuration.SystemLib, .{
.flags = .{
.needed = sl.needed,
.weak = sl.weak,
.use_pkg_config = sl.use_pkg_config,
.preferred_link_mode = sl.preferred_link_mode,
.search_strategy = sl.search_strategy,
},
.name = try wc.addString(sl.name),
})));
}
fn addCSourceFile(s: *Serialize, csf: *const std.Build.Module.CSourceFile) !Configuration.CSourceFile.Index {
log.err("TODO addCSourceFile trailing data", .{});
const wc = s.wc;
return @enumFromInt(try wc.addExtra(@as(Configuration.CSourceFile, .{
.flags = .{
.args_len = @intCast(csf.flags.len),
.lang = .init(csf.language),
},
.file = try addLazyPath(s, csf.file),
})));
}
fn addCSourceFiles(s: *Serialize, csf: *const std.Build.Module.CSourceFiles) !Configuration.CSourceFiles.Index {
log.err("TODO addCSourceFiles trailing data", .{});
const wc = s.wc;
return @enumFromInt(try wc.addExtra(@as(Configuration.CSourceFiles, .{
.flags = .{
.args_len = @intCast(csf.flags.len),
.lang = .init(csf.language),
},
.root = try addLazyPath(s, csf.root),
.files_len = @intCast(csf.files.len),
})));
}
fn addRcSourceFile(s: *Serialize, rsf: *const std.Build.Module.RcSourceFile) !Configuration.RcSourceFile.Index {
log.err("TODO addRcSourceFile trailing data", .{});
const wc = s.wc;
return @enumFromInt(try wc.addExtra(@as(Configuration.RcSourceFile, .{
.file = try addLazyPath(s, rsf.file),
.args_len = @intCast(rsf.flags.len),
.include_paths_len = @intCast(rsf.include_paths.len),
})));
}
fn initStringList(s: *Serialize, list: []const []const u8) ![]const Configuration.String {
const wc = s.wc;
const result = try s.arena.alloc(Configuration.String, list.len);
@ -312,6 +364,35 @@ const Serialize = struct {
const arena = s.arena;
const gpa = wc.gpa;
const include_dirs = try arena.alloc(Configuration.Module.IncludeDir, m.include_dirs.items.len);
for (include_dirs, m.include_dirs.items) |*dest, src| dest.* = switch (src) {
.path => |lp| .{ .path = try addLazyPath(s, lp) },
.path_system => |lp| .{ .path_system = try addLazyPath(s, lp) },
.path_after => |lp| .{ .path_after = try addLazyPath(s, lp) },
.framework_path => |lp| .{ .framework_path = try addLazyPath(s, lp) },
.framework_path_system => |lp| .{ .framework_path_system = try addLazyPath(s, lp) },
.embed_path => |lp| .{ .embed_path = try addLazyPath(s, lp) },
.other_step => |cs| .{ .other_step = stepIndex(s, &cs.step) },
.config_header_step => |chs| .{ .config_header_step = stepIndex(s, &chs.step) },
};
const rpaths = try arena.alloc(Configuration.Module.RPath, m.rpaths.items.len);
for (rpaths, m.rpaths.items) |*dest, src| dest.* = switch (src) {
.lazy_path => |lp| .{ .lazy_path = try addLazyPath(s, lp) },
.special => |slice| .{ .special = try wc.addString(slice) },
};
const link_objects = try arena.alloc(Configuration.Module.LinkObject, m.link_objects.items.len);
for (link_objects, m.link_objects.items) |*dest, *src| dest.* = switch (src.*) {
.static_path => |lp| .{ .static_path = try addLazyPath(s, lp) },
.other_step => |cs| .{ .other_step = stepIndex(s, &cs.step) },
.system_lib => |*sl| .{ .system_lib = try addSystemLib(s, sl) },
.assembly_file => |lp| .{ .assembly_file = try addLazyPath(s, lp) },
.c_source_file => |csf| .{ .c_source_file = try addCSourceFile(s, csf) },
.c_source_files => |csf| .{ .c_source_files = try addCSourceFiles(s, csf) },
.win32_resource_file => |wrf| .{ .win32_resource_file = try addRcSourceFile(s, wrf) },
};
const lib_paths = try arena.alloc(Configuration.LazyPath, m.lib_paths.items.len);
for (lib_paths, m.lib_paths.items) |*dest, src| dest.* = try addLazyPath(s, src);
@ -352,11 +433,11 @@ const Serialize = struct {
.fuzz = .init(m.strip),
.code_model = m.code_model,
.c_macros = c_macros.len != 0,
.include_dirs = m.include_dirs.items.len != 0,
.include_dirs = include_dirs.len != 0,
.lib_paths = lib_paths.len != 0,
.rpaths = m.rpaths.items.len != 0,
.rpaths = rpaths.len != 0,
.frameworks = m.frameworks.entries.len != 0,
.link_objects = m.link_objects.items.len != 0,
.link_objects = link_objects.len != 0,
.export_symbol_names = export_symbol_names.len != 0,
},
.flags2 = .{
@ -376,6 +457,9 @@ const Serialize = struct {
.c_macros = .{ .slice = c_macros },
.lib_paths = .{ .slice = lib_paths },
.export_symbol_names = .{ .slice = export_symbol_names },
.include_dirs = .init(include_dirs),
.rpaths = .init(rpaths),
.link_objects = .init(link_objects),
})));
log.err("TODO serialize the trailing Module data", .{});
@ -384,6 +468,10 @@ const Serialize = struct {
return module_index;
}
fn stepIndex(s: *const Serialize, step: *Step) Configuration.Step.Index {
return @enumFromInt(s.step_map.getIndex(step).?);
}
};
fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
@ -396,34 +484,32 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
// Starting from all top-level steps in `b`, traverse the entire step graph
// and add all step dependencies implied by module graphs.
const top_level_steps = b.top_level_steps.values();
// Index corresponds to `Configuration.steps` index.
var step_map: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
try step_map.ensureUnusedCapacity(arena, top_level_steps.len);
try s.step_map.ensureUnusedCapacity(arena, top_level_steps.len);
for (top_level_steps) |tls| {
step_map.putAssumeCapacityNoClobber(&tls.step, {});
s.step_map.putAssumeCapacityNoClobber(&tls.step, {});
}
{
while (wc.steps.items.len < step_map.count()) {
const step = step_map.keys()[wc.steps.items.len];
while (wc.steps.items.len < s.step_map.count()) {
const step = s.step_map.keys()[wc.steps.items.len];
// Set up any implied dependencies for this step. It's important that we do this first, so
// that the loop below discovers steps implied by the module graph.
try createModuleDependenciesForStep(step);
try step_map.ensureUnusedCapacity(arena, step.dependencies.items.len);
try s.step_map.ensureUnusedCapacity(arena, step.dependencies.items.len);
for (step.dependencies.items) |other_step| {
step_map.putAssumeCapacity(other_step, {});
s.step_map.putAssumeCapacity(other_step, {});
}
// Add and then de-duplicate dependencies.
const deps = d: {
const deps: Configuration.Deps = @enumFromInt(wc.extra.items.len);
for (try wc.reserveLengthPrefixed(step.dependencies.items.len), step.dependencies.items) |*dep, dep_step|
dep.* = @intCast(step_map.getIndex(dep_step).?);
dep.* = @intCast(s.step_map.getIndex(dep_step).?);
break :d try wc.dedupeDeps(deps);
};
try wc.steps.ensureTotalCapacity(gpa, step_map.entries.capacity);
try wc.steps.ensureTotalCapacity(gpa, s.step_map.entries.capacity);
wc.steps.appendAssumeCapacity(.{
.name = try wc.addString(step.name),
.owner = try s.builderToPackage(step.owner),
@ -613,7 +699,7 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
.emitted_pdb = try s.addOptionalLazyPathEnum(ia.emitted_pdb),
.h_dir = try addInstallDir(wc, ia.h_dir),
.emitted_h = try s.addOptionalLazyPathEnum(ia.emitted_h),
.artifact = stepIndex(&step_map, &ia.artifact.step),
.artifact = s.stepIndex(&ia.artifact.step),
})));
},
.install_file => @panic("TODO"),
@ -688,7 +774,7 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
}
try wc.write(writer, .{
.default_step = stepIndex(&step_map, b.default_step),
.default_step = s.stepIndex(b.default_step),
});
}
@ -714,10 +800,6 @@ fn addInstallDir(wc: *Configuration.Wip, install_dir: ?std.Build.InstallDir) !Co
}
}
fn stepIndex(step_map: *const std.AutoArrayHashMapUnmanaged(*Step, void), step: *Step) Configuration.Step.Index {
return @enumFromInt(step_map.getIndex(step).?);
}
/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {

View file

@ -73,18 +73,8 @@ pub const SystemLib = struct {
preferred_link_mode: std.builtin.LinkMode,
search_strategy: SystemLib.SearchStrategy,
pub const UsePkgConfig = enum {
/// Don't use pkg-config, just pass -lfoo where foo is name.
no,
/// Try to get information on how to link the library from pkg-config.
/// If that fails, fall back to passing -lfoo where foo is name.
yes,
/// Try to get information on how to link the library from pkg-config.
/// If that fails, error out.
force,
};
pub const SearchStrategy = enum { paths_first, mode_first, no_fallback };
pub const UsePkgConfig = std.Build.Configuration.SystemLib.UsePkgConfig;
pub const SearchStrategy = std.Build.Configuration.SystemLib.SearchStrategy;
};
pub const CSourceLanguage = enum {

View file

@ -863,7 +863,7 @@ pub const OutputMode = enum {
/// This data structure is used by the Zig language code generation and
/// therefore must be kept in sync with the compiler implementation.
pub const LinkMode = enum {
pub const LinkMode = enum(u1) {
static,
dynamic,
};

View file

@ -1071,9 +1071,6 @@ pub const Package = struct {
/// Trailing:
/// * frameworks: FlagsPrefixedList(FrameworkFlags), // if flag is set
/// * include_dirs: UnionList(IncludeDir), // if flag is set
/// * rpaths: UnionList(RPath), // if flag is set
/// * link_objects: UnionList(LinkObject), // if flag is set
pub const Module = struct {
flags: Flags,
flags2: Flags2,
@ -1084,6 +1081,9 @@ pub const Module = struct {
c_macros: Storage.FlagLengthPrefixedList(.flags, .c_macros, String),
lib_paths: Storage.FlagLengthPrefixedList(.flags, .lib_paths, LazyPath),
export_symbol_names: Storage.FlagLengthPrefixedList(.flags, .export_symbol_names, String),
include_dirs: Storage.UnionList(.flags, .include_dirs, IncludeDir),
rpaths: Storage.UnionList(.flags, .rpaths, RPath),
link_objects: Storage.UnionList(.flags, .link_objects, LinkObject),
pub const Optimize = enum(u3) {
debug,
@ -1204,7 +1204,7 @@ pub const Module = struct {
static_path: LazyPath,
/// Always `Step.Tag.compile`.
other_step: Step.Index,
system_lib: SystemLib,
system_lib: SystemLib.Index,
assembly_file: LazyPath,
c_source_file: CSourceFile.Index,
c_source_files: CSourceFiles.Index,
@ -1328,8 +1328,18 @@ pub const SystemLib = struct {
_,
};
pub const UsePkgConfig = enum(u2) { no, yes, force };
pub const LinkMode = enum { static, dynamic };
pub const UsePkgConfig = enum(u2) {
/// Don't use pkg-config, just pass -lfoo where foo is name.
no,
/// Try to get information on how to link the library from pkg-config.
/// If that fails, fall back to passing -lfoo where foo is name.
yes,
/// Try to get information on how to link the library from pkg-config.
/// If that fails, error out.
force,
};
pub const LinkMode = std.builtin.LinkMode;
pub const Flags = packed struct(u32) {
needed: bool,
@ -1337,18 +1347,19 @@ pub const SystemLib = struct {
use_pkg_config: UsePkgConfig,
preferred_link_mode: LinkMode,
search_strategy: SearchStrategy,
_: u25 = 0,
};
pub const SearchStrategy = enum(u2) { paths_first, mode_first, no_fallback };
};
/// Trailing:
/// * flag: String, // for each flags_len
/// * arg: String, // for each args_len
/// * sub_path: String, // for each files_len
pub const CSourceFiles = struct {
flags: Flags,
root: LazyPath,
files_len: u32,
flags: Flags,
pub const Index = enum(u32) {
_,
@ -1356,16 +1367,16 @@ pub const CSourceFiles = struct {
pub const Flags = packed struct(u32) {
/// C compiler CLI flags.
flags_len: u29,
args_len: u29,
lang: OptionalCSourceLanguage,
};
};
/// Trailing:
/// * flag: String, // for each flags_len
/// * arg: String, // for each args_len
pub const CSourceFile = struct {
file: LazyPath,
flags: Flags,
file: LazyPath,
pub const Index = enum(u32) {
_,
@ -1373,11 +1384,24 @@ pub const CSourceFile = struct {
pub const Flags = packed struct(u32) {
/// C compiler CLI flags.
flags_len: u29,
args_len: u29,
lang: OptionalCSourceLanguage,
};
};
/// Trailing:
/// * arg: String, // for each args_len
/// * include_path: String, // for each include_paths_len
pub const RcSourceFile = struct {
file: LazyPath,
args_len: u32,
include_paths_len: u32,
pub const Index = enum(u32) {
_,
};
};
pub const OptionalCSourceLanguage = enum(u3) {
c,
cpp,
@ -1386,29 +1410,17 @@ pub const OptionalCSourceLanguage = enum(u3) {
assembly,
assembly_with_preprocessor,
default,
};
pub const RcSourceFile = struct {
file: LazyPath,
/// Any option that rc.exe accepts will work here, with the exception of:
/// - `/fo`: The output filename is set by the build system
/// - `/p`: Only running the preprocessor is not supported in this context
/// - `/:no-preprocess` (non-standard option): Not supported in this context
/// - Any MUI-related option
/// https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
///
/// Implicitly defined options:
/// /x (ignore the INCLUDE environment variable)
/// /D_DEBUG or /DNDEBUG depending on the optimization mode
flags: []const []const u8 = &.{},
/// Include paths that may or may not exist yet and therefore need to be
/// specified as a LazyPath. Each path will be appended to the flags
/// as `/I <resolved path>`.
include_paths: []const LazyPath = &.{},
pub const Index = enum(u32) {
_,
};
pub fn init(x: ?std.Build.Module.CSourceLanguage) @This() {
return switch (x orelse return .default) {
.c => .c,
.cpp => .cpp,
.objective_c => .objective_c,
.objective_cpp => .objective_cpp,
.assembly => .assembly,
.assembly_with_preprocessor => .assembly_with_preprocessor,
};
}
};
pub const ResolvedTarget = struct {
@ -1691,6 +1703,7 @@ pub const Storage = enum {
enum_optional,
extended,
flag_length_prefixed_list,
union_list,
/// The presence of the field is determined by a boolean within a packed
/// struct.
@ -1769,6 +1782,63 @@ pub const Storage = enum {
};
}
/// `UnionArg` is a tagged union with a small integer for the enum tag.
///
/// A field in flags determines whether the metadata is present.
///
/// The metadata is bit-packed consecutive packed struct which is the
/// `UnionArg` enum tag combined with a "last" marker boolean field.
/// When "last" is true, the element is the last one, providing
/// the length of the list.
///
/// Following is each element of the list; each bitcastable to u32.
pub fn UnionList(
comptime flags_arg: @EnumLiteral(),
comptime flag_arg: @EnumLiteral(),
comptime UnionArg: type,
) type {
return struct {
/// When serializing it is UnionArg slice pointer.
/// When deserializing it is extra index of first UnionArg element.
data: ?*const anyopaque,
len: usize,
pub const storage: Storage = .union_list;
pub const flags = flags_arg;
pub const flag = flag_arg;
pub const Union = UnionArg;
pub const Tag = @typeInfo(Union).@"union".tag_type.?;
pub const MetaInt = @Int(.unsigned, @bitSizeOf(Tag) + 1);
pub const Meta = packed struct(MetaInt) {
tag: Tag,
last: bool,
};
/// Valid to call only when serializing.
pub fn init(slice: []const Union) @This() {
return .{ .data = slice.ptr, .len = slice.len };
}
/// Valid to call only when deserializing.
pub fn get(this: *const @This(), extra: []const u32) []const u32 {
return extra[@intFromPtr(this.data)..][0..this.len];
}
/// Valid to call only when deserializing.
pub fn tag(this: *const @This(), extra: []const u32, i: usize) Tag {
_ = this;
_ = extra;
_ = i;
@panic("TODO implement UnionList.tag");
}
fn extraLen(len: usize) usize {
return len + (len * @bitSizeOf(Meta) + 31) / 32;
}
};
}
pub fn dataLength(buffer: []const u32, i: usize, comptime S: type) usize {
var end = i;
_ = data(buffer, &end, S);
@ -1848,6 +1918,23 @@ pub const Storage = enum {
defer i.* = data_start + len;
return .{ .slice = @ptrCast(buffer[data_start..][0..len]) };
},
.union_list => {
const flags = @field(container, @tagName(Field.flags));
const flag = @field(flags, @tagName(Field.flag));
if (!flag) return .{ .data = null, .len = 0 };
const meta_start = i.*;
const meta_buffer = buffer[meta_start..];
var len: u32 = 0;
var bit_offset: usize = 0;
while (true) : (bit_offset += @bitSizeOf(Field.Meta)) {
const meta = loadBits(u32, meta_buffer, bit_offset, Field.Meta);
len += 1;
if (meta.last) break;
}
const end = meta_start + Field.extraLen(len);
i.* = end;
return .{ .data = end - len, .len = len };
},
},
},
.@"extern" => comptime unreachable,
@ -1884,6 +1971,7 @@ pub const Storage = enum {
.auto => switch (Field.storage) {
.flag_optional, .enum_optional, .extended => 1,
.flag_length_prefixed_list => field.slice.len + 1,
.union_list => Field.extraLen(field.len),
},
.@"extern" => comptime unreachable,
},
@ -1947,6 +2035,33 @@ pub const Storage = enum {
@memcpy(buffer[i + 1 ..][0..len], @as([]const u32, @ptrCast(value.slice)));
return len + 1;
},
.union_list => {
if (value.len == 0) return 0;
const Tag = @typeInfo(Field.Union).@"union".tag_type.?;
const slice_ptr: [*]const Field.Union = @ptrCast(@alignCast(value.data));
const slice = slice_ptr[0..value.len];
const meta_buffer = buffer[i..][0 .. (slice.len * @bitSizeOf(Field.Meta) + 31) / 32];
for (slice[0 .. slice.len - 1], 0..) |elem, elem_index| {
const union_tag: Tag = elem;
storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{
.tag = union_tag,
.last = false,
}));
} else {
const elem_index = slice.len - 1;
const elem = slice[elem_index];
const union_tag: Tag = elem;
storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{
.tag = union_tag,
.last = true,
}));
}
var total: usize = meta_buffer.len;
for (i + meta_buffer.len.., slice) |elem_index, src| switch (src) {
inline else => |x| total += setExtraField(buffer, elem_index, @TypeOf(x), x),
};
return total;
},
},
},
.@"extern" => comptime unreachable,
@ -2000,3 +2115,48 @@ pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration {
try reader.readVecAll(&vecs);
return result;
}
pub fn loadBits(comptime Int: type, buffer: []const Int, bit_offset: usize, comptime Result: type) Result {
const index = bit_offset / @bitSizeOf(Int);
const small_bit_offset = bit_offset % @bitSizeOf(Int);
const ResultInt = @Int(.unsigned, @bitSizeOf(Result));
const result: ResultInt = @truncate(buffer[index] >> @intCast(small_bit_offset));
const available_bits = @bitSizeOf(Int) - small_bit_offset;
if (available_bits >= @bitSizeOf(ResultInt)) return @bitCast(result);
const missing_bits = @bitSizeOf(ResultInt) - available_bits;
const upper: ResultInt = @truncate(buffer[index + 1] & ((@as(usize, 1) << @intCast(missing_bits)) - 1));
return @bitCast(result | (upper << @intCast(available_bits)));
}
pub fn storeBits(comptime Int: type, buffer: []Int, bit_offset: usize, value: anytype) void {
const Value = @TypeOf(value);
const ValueInt = @Int(.unsigned, @bitSizeOf(Value));
const value_int: ValueInt = @bitCast(value);
const index = bit_offset / @bitSizeOf(Int);
const small_bit_offset = bit_offset % @bitSizeOf(Int);
const available_bits = @bitSizeOf(Int) - small_bit_offset;
if (available_bits >= @bitSizeOf(ValueInt)) {
buffer[index] &= ~(((@as(Int, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset));
buffer[index] |= @as(Int, value_int) << @intCast(small_bit_offset);
} else {
const DoubleInt = @Int(.unsigned, @bitSizeOf(Int) * 2);
const ptr: *align(@alignOf(Int)) DoubleInt = @ptrCast(buffer[index..][0..2]);
ptr.* &= ~(((@as(DoubleInt, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset));
ptr.* |= @as(DoubleInt, value_int) << @intCast(small_bit_offset);
}
}
test "loadBits and storeBits" {
var buffer: [2]u32 = .{
0b01111111000000001111111100000000,
0b11111111000000001111111100000100,
};
try std.testing.expectEqual(0b100, loadBits(u32, &buffer, 6, u3));
try std.testing.expectEqual(0b100011, loadBits(u32, &buffer, 29, u6));
storeBits(u32, &buffer, 6, @as(u3, 0b010));
storeBits(u32, &buffer, 29, @as(u6, 0b010010));
try std.testing.expectEqual(0b010, loadBits(u32, &buffer, 6, u3));
try std.testing.expectEqual(0b010010, loadBits(u32, &buffer, 29, u6));
}