Commit d656c2a7ab

Jacob Young <jacobly0@users.noreply.github.com>
2024-02-25 14:04:06
test: rework how filtering works
* make test names contain the fully qualified name * make test filters match the fully qualified name * allow multiple test filters, where a test is skipped if it does not match any of the specified filters
1 parent 429e542
doc/langref.html.in
@@ -988,13 +988,13 @@ fn addOne(number: i32) i32 {
         printed to standard error by the default test runner:
       </p>
       <dl>
-        <dt><samp>Test [1/2] test.expect addOne adds one to 41...</samp></dt>
+        <dt><samp>1/2 testing_introduction.test.expect addOne adds one to 41...</samp></dt>
         <dd>Lines like this indicate which test, out of the total number of tests, is being run.
-          In this case, <samp>[1/2]</samp> indicates that the first test, out of a total of
-          two test, is being run. Note that, when the test runner program's standard error is output
+          In this case, <samp>1/2</samp> indicates that the first test, out of a total of two tests,
+          is being run. Note that, when the test runner program's standard error is output
           to the terminal, these lines are cleared when a test succeeds.
         </dd>
-        <dt><samp>Test [2/2] decltest.addOne...</samp></dt>
+        <dt><samp>2/2 testing_introduction.decltest.addOne...</samp></dt>
         <dd>When the test name is an identifier, the default test runner uses the text
           decltest instead of test.
         </dd>
lib/std/Build/Step/Compile.zig
@@ -54,7 +54,7 @@ global_base: ?u64 = null,
 /// Set via options; intended to be read-only after that.
 zig_lib_dir: ?LazyPath,
 exec_cmd_args: ?[]const ?[]const u8,
-filter: ?[]const u8,
+filters: []const []const u8,
 test_runner: ?[]const u8,
 test_server_mode: bool,
 wasi_exec_model: ?std.builtin.WasiExecModel = null,
@@ -223,7 +223,7 @@ pub const Options = struct {
     linkage: ?Linkage = null,
     version: ?std.SemanticVersion = null,
     max_rss: usize = 0,
-    filter: ?[]const u8 = null,
+    filters: []const []const u8 = &.{},
     test_runner: ?[]const u8 = null,
     use_llvm: ?bool = null,
     use_lld: ?bool = null,
@@ -310,7 +310,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
         .installed_headers = ArrayList(*Step).init(owner.allocator),
         .zig_lib_dir = null,
         .exec_cmd_args = null,
-        .filter = options.filter,
+        .filters = options.filters,
         .test_runner = options.test_runner,
         .test_server_mode = options.test_runner == null,
         .rdynamic = false,
@@ -1297,7 +1297,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
         try zig_args.append(b.fmt("0x{x}", .{image_base}));
     }
 
-    if (self.filter) |filter| {
+    for (self.filters) |filter| {
         try zig_args.append("--test-filter");
         try zig_args.append(filter);
     }
lib/std/Build.zig
@@ -855,7 +855,9 @@ pub const TestOptions = struct {
     optimize: std.builtin.OptimizeMode = .Debug,
     version: ?std.SemanticVersion = null,
     max_rss: usize = 0,
+    /// deprecated: use `.filters = &.{filter}` instead of `.filter = filter`.
     filter: ?[]const u8 = null,
+    filters: []const []const u8 = &.{},
     test_runner: ?[]const u8 = null,
     link_libc: ?bool = null,
     single_threaded: ?bool = null,
@@ -888,7 +890,12 @@ pub fn addTest(b: *Build, options: TestOptions) *Step.Compile {
             .error_tracing = options.error_tracing,
         },
         .max_rss = options.max_rss,
-        .filter = options.filter,
+        .filters = if (options.filter != null and options.filters.len > 0) filters: {
+            const filters = b.allocator.alloc([]const u8, 1 + options.filters.len) catch @panic("OOM");
+            filters[0] = b.dupe(options.filter.?);
+            for (filters[1..], options.filters) |*dest, source| dest.* = b.dupe(source);
+            break :filters filters;
+        } else b.dupeStrings(if (options.filter) |filter| &.{filter} else options.filters),
         .test_runner = options.test_runner,
         .use_llvm = options.use_llvm,
         .use_lld = options.use_lld,
@@ -993,9 +1000,7 @@ pub fn dupe(self: *Build, bytes: []const u8) []u8 {
 /// Duplicates an array of strings without the need to handle out of memory.
 pub fn dupeStrings(self: *Build, strings: []const []const u8) [][]u8 {
     const array = self.allocator.alloc([]u8, strings.len) catch @panic("OOM");
-    for (strings, 0..) |s, i| {
-        array[i] = self.dupe(s);
-    }
+    for (array, strings) |*dest, source| dest.* = self.dupe(source);
     return array;
 }
 
src/arch/wasm/CodeGen.zig
@@ -7223,7 +7223,7 @@ fn getTagNameFunction(func: *CodeGen, enum_ty: Type) InnerError!u32 {
     defer arena_allocator.deinit();
     const arena = arena_allocator.allocator();
 
-    const fqn = ip.stringToSlice(try mod.declPtr(enum_decl_index).getFullyQualifiedName(mod));
+    const fqn = ip.stringToSlice(try mod.declPtr(enum_decl_index).fullyQualifiedName(mod));
     const func_name = try std.fmt.allocPrintZ(arena, "__zig_tag_name_{s}", .{fqn});
 
     // check if we already generated code for this.
src/codegen/llvm.zig
@@ -1163,7 +1163,7 @@ pub const Object = struct {
                         const fwd_ref = self.debug_unresolved_namespace_scopes.values()[i];
 
                         const namespace = self.module.namespacePtr(namespace_index);
-                        const debug_type = try self.lowerDebugType(namespace.ty);
+                        const debug_type = try self.lowerDebugType(namespace.getType(self.module));
 
                         self.builder.debugForwardReferenceSetType(fwd_ref, debug_type);
                     }
@@ -1797,7 +1797,7 @@ pub const Object = struct {
             return updateExportedGlobal(self, mod, global_index, exports);
         } else {
             const fqn = try self.builder.string(
-                mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod)),
+                mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod)),
             );
             try global_index.rename(fqn, &self.builder);
             global_index.setLinkage(.internal, &self.builder);
@@ -2835,15 +2835,13 @@ pub const Object = struct {
 
         const builtin_str = try mod.intern_pool.getOrPutString(mod.gpa, "builtin");
         const std_namespace = mod.namespacePtr(mod.declPtr(std_file.root_decl.unwrap().?).src_namespace);
-        const builtin_decl = std_namespace.decls
-            .getKeyAdapted(builtin_str, Module.DeclAdapter{ .mod = mod }).?;
+        const builtin_decl = std_namespace.decls.getKeyAdapted(builtin_str, Module.DeclAdapter{ .zcu = mod }).?;
 
         const stack_trace_str = try mod.intern_pool.getOrPutString(mod.gpa, "StackTrace");
         // buffer is only used for int_type, `builtin` is a struct.
         const builtin_ty = mod.declPtr(builtin_decl).val.toType();
         const builtin_namespace = builtin_ty.getNamespace(mod).?;
-        const stack_trace_decl_index = builtin_namespace.decls
-            .getKeyAdapted(stack_trace_str, Module.DeclAdapter{ .mod = mod }).?;
+        const stack_trace_decl_index = builtin_namespace.decls.getKeyAdapted(stack_trace_str, Module.DeclAdapter{ .zcu = mod }).?;
         const stack_trace_decl = mod.declPtr(stack_trace_decl_index);
 
         // Sema should have ensured that StackTrace was analyzed.
@@ -2886,7 +2884,7 @@ pub const Object = struct {
             try o.builder.string(ip.stringToSlice(if (is_extern)
                 decl.name
             else
-                try decl.getFullyQualifiedName(zcu))),
+                try decl.fullyQualifiedName(zcu))),
             toLlvmAddressSpace(decl.@"addrspace", target),
         );
         gop.value_ptr.* = function_index.ptrConst(&o.builder).global;
@@ -3100,7 +3098,7 @@ pub const Object = struct {
 
         const variable_index = try o.builder.addVariable(
             try o.builder.string(mod.intern_pool.stringToSlice(
-                if (is_extern) decl.name else try decl.getFullyQualifiedName(mod),
+                if (is_extern) decl.name else try decl.fullyQualifiedName(mod),
             )),
             try o.lowerType(decl.ty),
             toLlvmGlobalAddressSpace(decl.@"addrspace", mod.getTarget()),
@@ -3325,7 +3323,7 @@ pub const Object = struct {
                     }
 
                     const name = try o.builder.string(ip.stringToSlice(
-                        try mod.declPtr(struct_type.decl.unwrap().?).getFullyQualifiedName(mod),
+                        try mod.declPtr(struct_type.decl.unwrap().?).fullyQualifiedName(mod),
                     ));
 
                     var llvm_field_types = std.ArrayListUnmanaged(Builder.Type){};
@@ -3481,7 +3479,7 @@ pub const Object = struct {
                     }
 
                     const name = try o.builder.string(ip.stringToSlice(
-                        try mod.declPtr(union_obj.decl).getFullyQualifiedName(mod),
+                        try mod.declPtr(union_obj.decl).fullyQualifiedName(mod),
                     ));
 
                     const aligned_field_ty = Type.fromInterned(union_obj.field_types.get(ip)[layout.most_aligned_field]);
@@ -4599,7 +4597,7 @@ pub const Object = struct {
 
         const usize_ty = try o.lowerType(Type.usize);
         const ret_ty = try o.lowerType(Type.slice_const_u8_sentinel_0);
-        const fqn = try zcu.declPtr(enum_type.decl).getFullyQualifiedName(zcu);
+        const fqn = try zcu.declPtr(enum_type.decl).fullyQualifiedName(zcu);
         const target = zcu.root_mod.resolved_target.result;
         const function_index = try o.builder.addFunction(
             try o.builder.fnType(ret_ty, &.{try o.lowerType(Type.fromInterned(enum_type.tag_ty))}, .normal),
@@ -6613,7 +6611,7 @@ pub const FuncGen = struct {
             .base_line = self.base_line,
         });
 
-        const fqn = try decl.getFullyQualifiedName(zcu);
+        const fqn = try decl.fullyQualifiedName(zcu);
 
         const is_internal_linkage = !zcu.decl_exports.contains(decl_index);
         const fn_ty = try zcu.funcType(.{
@@ -9643,7 +9641,7 @@ pub const FuncGen = struct {
         if (gop.found_existing) return gop.value_ptr.*;
         errdefer assert(o.named_enum_map.remove(enum_type.decl));
 
-        const fqn = try zcu.declPtr(enum_type.decl).getFullyQualifiedName(zcu);
+        const fqn = try zcu.declPtr(enum_type.decl).fullyQualifiedName(zcu);
         const target = zcu.root_mod.resolved_target.result;
         const function_index = try o.builder.addFunction(
             try o.builder.fnType(.i1, &.{try o.lowerType(Type.fromInterned(enum_type.tag_ty))}, .normal),
src/codegen/spirv.zig
@@ -2019,7 +2019,7 @@ const DeclGen = struct {
             // Append the actual code into the functions section.
             try self.spv.addFunction(spv_decl_index, self.func);
 
-            const fqn = ip.stringToSlice(try decl.getFullyQualifiedName(self.module));
+            const fqn = ip.stringToSlice(try decl.fullyQualifiedName(self.module));
             try self.spv.debugName(decl_id, fqn);
 
             // Temporarily generate a test kernel declaration if this is a test function.
@@ -2055,7 +2055,7 @@ const DeclGen = struct {
                 .id_result = decl_id,
                 .storage_class = actual_storage_class,
             });
-            const fqn = ip.stringToSlice(try decl.getFullyQualifiedName(self.module));
+            const fqn = ip.stringToSlice(try decl.fullyQualifiedName(self.module));
             try self.spv.debugName(decl_id, fqn);
 
             if (opt_init_val) |init_val| {
src/link/Elf/ZigObject.zig
@@ -903,7 +903,7 @@ fn updateDeclCode(
     const gpa = elf_file.base.comp.gpa;
     const mod = elf_file.base.comp.module.?;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateDeclCode {s}{*}", .{ decl_name, decl });
 
@@ -1001,7 +1001,7 @@ fn updateTlv(
     const gpa = elf_file.base.comp.gpa;
     const mod = elf_file.base.comp.module.?;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateTlv {s} ({*})", .{ decl_name, decl });
 
@@ -1300,7 +1300,7 @@ pub fn lowerUnnamedConst(
     }
     const unnamed_consts = gop.value_ptr;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
     const index = unnamed_consts.items.len;
     const name = try std.fmt.allocPrint(gpa, "__unnamed_{s}_{d}", .{ decl_name, index });
     defer gpa.free(name);
@@ -1482,7 +1482,7 @@ pub fn updateDeclLineNumber(
     defer tracy.end();
 
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateDeclLineNumber {s}{*}", .{ decl_name, decl });
 
src/link/MachO/ZigObject.zig
@@ -792,7 +792,7 @@ fn updateDeclCode(
     const gpa = macho_file.base.comp.gpa;
     const mod = macho_file.base.comp.module.?;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateDeclCode {s}{*}", .{ decl_name, decl });
 
@@ -876,7 +876,7 @@ fn updateTlv(
 ) !void {
     const mod = macho_file.base.comp.module.?;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateTlv {s} ({*})", .{ decl_name, decl });
 
@@ -1079,7 +1079,7 @@ pub fn lowerUnnamedConst(
     }
     const unnamed_consts = gop.value_ptr;
     const decl = mod.declPtr(decl_index);
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
     const index = unnamed_consts.items.len;
     const name = try std.fmt.allocPrint(gpa, "__unnamed_{s}_{d}", .{ decl_name, index });
     defer gpa.free(name);
src/link/Coff.zig
@@ -1176,7 +1176,7 @@ pub fn lowerUnnamedConst(self: *Coff, tv: TypedValue, decl_index: InternPool.Dec
         gop.value_ptr.* = .{};
     }
     const unnamed_consts = gop.value_ptr;
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
     const index = unnamed_consts.items.len;
     const sym_name = try std.fmt.allocPrint(gpa, "__unnamed_{s}_{d}", .{ decl_name, index });
     defer gpa.free(sym_name);
@@ -1427,7 +1427,7 @@ fn updateDeclCode(self: *Coff, decl_index: InternPool.DeclIndex, code: []u8, com
     const mod = self.base.comp.module.?;
     const decl = mod.declPtr(decl_index);
 
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     log.debug("updateDeclCode {s}{*}", .{ decl_name, decl });
     const required_alignment: u32 = @intCast(decl.getAlignment(mod).toByteUnits(0));
src/link/Dwarf.zig
@@ -1082,7 +1082,7 @@ pub fn initDeclState(self: *Dwarf, mod: *Module, decl_index: InternPool.DeclInde
     defer tracy.end();
 
     const decl = mod.declPtr(decl_index);
-    const decl_linkage_name = try decl.getFullyQualifiedName(mod);
+    const decl_linkage_name = try decl.fullyQualifiedName(mod);
 
     log.debug("initDeclState {}{*}", .{ decl_linkage_name.fmt(&mod.intern_pool), decl });
 
src/link/Plan9.zig
@@ -478,7 +478,7 @@ pub fn lowerUnnamedConst(self: *Plan9, tv: TypedValue, decl_index: InternPool.De
     }
     const unnamed_consts = gop.value_ptr;
 
-    const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
     const index = unnamed_consts.items.len;
     // name is freed when the unnamed const is freed
src/link/Wasm.zig
@@ -662,7 +662,7 @@ pub fn getOrCreateAtomForDecl(wasm: *Wasm, decl_index: InternPool.DeclIndex) !At
         const symbol = atom.symbolLoc().getSymbol(wasm);
         const mod = wasm.base.comp.module.?;
         const decl = mod.declPtr(decl_index);
-        const full_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+        const full_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
         symbol.name = try wasm.string_table.put(gpa, full_name);
     }
     return gop.value_ptr.*;
@@ -1598,7 +1598,7 @@ pub fn updateDeclLineNumber(wasm: *Wasm, mod: *Module, decl_index: InternPool.De
         defer tracy.end();
 
         const decl = mod.declPtr(decl_index);
-        const decl_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+        const decl_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
 
         log.debug("updateDeclLineNumber {s}{*}", .{ decl_name, decl });
         try dw.updateDeclLineNumber(mod, decl_index);
@@ -1612,7 +1612,7 @@ fn finishUpdateDecl(wasm: *Wasm, decl_index: InternPool.DeclIndex, code: []const
     const atom_index = wasm.decls.get(decl_index).?;
     const atom = wasm.getAtomPtr(atom_index);
     const symbol = &wasm.symbols.items[atom.sym_index];
-    const full_name = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const full_name = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
     symbol.name = try wasm.string_table.put(gpa, full_name);
     symbol.tag = symbol_tag;
     try atom.code.appendSlice(gpa, code);
@@ -1678,7 +1678,7 @@ pub fn lowerUnnamedConst(wasm: *Wasm, tv: TypedValue, decl_index: InternPool.Dec
     const parent_atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
     const parent_atom = wasm.getAtom(parent_atom_index);
     const local_index = parent_atom.locals.items.len;
-    const fqn = mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod));
+    const fqn = mod.intern_pool.stringToSlice(try decl.fullyQualifiedName(mod));
     const name = try std.fmt.allocPrintZ(gpa, "__unnamed_{s}_{d}", .{
         fqn, local_index,
     });
src/Compilation.zig
@@ -217,7 +217,7 @@ libcxx_abi_version: libcxx.AbiVersion = libcxx.AbiVersion.default,
 /// This mutex guards all `Compilation` mutable state.
 mutex: std.Thread.Mutex = .{},
 
-test_filter: ?[]const u8,
+test_filters: []const []const u8,
 test_name_prefix: ?[]const u8,
 
 emit_asm: ?EmitLoc,
@@ -1097,7 +1097,7 @@ pub const CreateOptions = struct {
     native_system_include_paths: []const []const u8 = &.{},
     clang_preprocessor_mode: ClangPreprocessorMode = .no,
     reference_trace: ?u32 = null,
-    test_filter: ?[]const u8 = null,
+    test_filters: []const []const u8 = &.{},
     test_name_prefix: ?[]const u8 = null,
     test_runner_path: ?[]const u8 = null,
     subsystem: ?std.Target.SubSystem = null,
@@ -1506,7 +1506,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .formatted_panics = formatted_panics,
             .time_report = options.time_report,
             .stack_report = options.stack_report,
-            .test_filter = options.test_filter,
+            .test_filters = options.test_filters,
             .test_name_prefix = options.test_name_prefix,
             .debug_compiler_runtime_libs = options.debug_compiler_runtime_libs,
             .debug_compile_errors = options.debug_compile_errors,
@@ -1613,7 +1613,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
                 hash.add(options.config.use_lib_llvm);
                 hash.add(options.config.dll_export_fns);
                 hash.add(options.config.is_test);
-                hash.addOptionalBytes(options.test_filter);
+                hash.addListOfBytes(options.test_filters);
                 hash.addOptionalBytes(options.test_name_prefix);
                 hash.add(options.skip_linker_dependencies);
                 hash.add(formatted_panics);
@@ -2475,7 +2475,7 @@ fn addNonIncrementalStuffToCacheManifest(
         try addModuleTableToCacheHash(gpa, arena, &man.hash, mod.root_mod, mod.main_mod, .{ .files = man });
 
         // Synchronize with other matching comments: ZigOnlyHashStuff
-        man.hash.addOptionalBytes(comp.test_filter);
+        man.hash.addListOfBytes(comp.test_filters);
         man.hash.addOptionalBytes(comp.test_name_prefix);
         man.hash.add(comp.skip_linker_dependencies);
         man.hash.add(comp.formatted_panics);
src/InternPool.zig
@@ -7904,7 +7904,7 @@ pub fn destroyNamespace(ip: *InternPool, gpa: Allocator, index: NamespaceIndex)
     ip.namespacePtr(index).* = .{
         .parent = undefined,
         .file_scope = undefined,
-        .ty = undefined,
+        .decl_index = undefined,
     };
     ip.namespaces_free_list.append(gpa, index) catch {
         // In order to keep `destroyNamespace` a non-fallible function, we ignore memory
src/main.zig
@@ -596,7 +596,7 @@ const usage_build_generic =
     \\  --export=[value]               (WebAssembly) Force a symbol to be exported
     \\
     \\Test Options:
-    \\  --test-filter [text]           Skip tests that do not match filter
+    \\  --test-filter [text]           Skip tests that do not match any filter
     \\  --test-name-prefix [text]      Add prefix to all tests
     \\  --test-cmd [arg]               Specify test execution command one arg at a time
     \\  --test-cmd-bin                 Appends test binary path to test cmd args
@@ -869,7 +869,7 @@ fn buildOutputType(
     var link_emit_relocs = false;
     var build_id: ?std.zig.BuildId = null;
     var runtime_args_start: ?usize = null;
-    var test_filter: ?[]const u8 = null;
+    var test_filters: std.ArrayListUnmanaged([]const u8) = .{};
     var test_name_prefix: ?[]const u8 = null;
     var test_runner_path: ?[]const u8 = null;
     var override_local_cache_dir: ?[]const u8 = try EnvVar.ZIG_LOCAL_CACHE_DIR.get(arena);
@@ -909,7 +909,7 @@ fn buildOutputType(
     var rc_source_files_owner_index: usize = 0;
 
     // null means replace with the test executable binary
-    var test_exec_args = std.ArrayList(?[]const u8).init(arena);
+    var test_exec_args: std.ArrayListUnmanaged(?[]const u8) = .{};
 
     // These get set by CLI flags and then snapshotted when a `--mod` flag is
     // encountered.
@@ -1278,13 +1278,13 @@ fn buildOutputType(
                     } else if (mem.eql(u8, arg, "--libc")) {
                         create_module.libc_paths_file = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--test-filter")) {
-                        test_filter = args_iter.nextOrFatal();
+                        try test_filters.append(arena, args_iter.nextOrFatal());
                     } else if (mem.eql(u8, arg, "--test-name-prefix")) {
                         test_name_prefix = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--test-runner")) {
                         test_runner_path = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--test-cmd")) {
-                        try test_exec_args.append(args_iter.nextOrFatal());
+                        try test_exec_args.append(arena, args_iter.nextOrFatal());
                     } else if (mem.eql(u8, arg, "--cache-dir")) {
                         override_local_cache_dir = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--global-cache-dir")) {
@@ -1334,7 +1334,7 @@ fn buildOutputType(
                     } else if (mem.eql(u8, arg, "-fno-each-lib-rpath")) {
                         create_module.each_lib_rpath = false;
                     } else if (mem.eql(u8, arg, "--test-cmd-bin")) {
-                        try test_exec_args.append(null);
+                        try test_exec_args.append(arena, null);
                     } else if (mem.eql(u8, arg, "--test-no-exec")) {
                         test_no_exec = true;
                     } else if (mem.eql(u8, arg, "-ftime-report")) {
@@ -3246,7 +3246,7 @@ fn buildOutputType(
         .time_report = time_report,
         .stack_report = stack_report,
         .build_id = build_id,
-        .test_filter = test_filter,
+        .test_filters = test_filters.items,
         .test_name_prefix = test_name_prefix,
         .test_runner_path = test_runner_path,
         .disable_lld_caching = disable_lld_caching,
@@ -3369,16 +3369,15 @@ fn buildOutputType(
         const c_code_path = try fs.path.join(arena, &[_][]const u8{
             c_code_directory.path orelse ".", c_code_loc.basename,
         });
-        try test_exec_args.append(self_exe_path);
-        try test_exec_args.append("run");
+        try test_exec_args.appendSlice(arena, &.{ self_exe_path, "run" });
         if (zig_lib_directory.path) |p| {
-            try test_exec_args.appendSlice(&.{ "-I", p });
+            try test_exec_args.appendSlice(arena, &.{ "-I", p });
         }
 
         if (create_module.resolved_options.link_libc) {
-            try test_exec_args.append("-lc");
+            try test_exec_args.append(arena, "-lc");
         } else if (target.os.tag == .windows) {
-            try test_exec_args.appendSlice(&.{
+            try test_exec_args.appendSlice(arena, &.{
                 "--subsystem", "console",
                 "-lkernel32",  "-lntdll",
             });
@@ -3386,17 +3385,15 @@ fn buildOutputType(
 
         const first_cli_mod = create_module.modules.values()[0];
         if (first_cli_mod.target_arch_os_abi) |triple| {
-            try test_exec_args.append("-target");
-            try test_exec_args.append(triple);
+            try test_exec_args.appendSlice(arena, &.{ "-target", triple });
         }
         if (first_cli_mod.target_mcpu) |mcpu| {
-            try test_exec_args.append(try std.fmt.allocPrint(arena, "-mcpu={s}", .{mcpu}));
+            try test_exec_args.append(arena, try std.fmt.allocPrint(arena, "-mcpu={s}", .{mcpu}));
         }
         if (create_module.dynamic_linker) |dl| {
-            try test_exec_args.append("--dynamic-linker");
-            try test_exec_args.append(dl);
+            try test_exec_args.appendSlice(arena, &.{ "--dynamic-linker", dl });
         }
-        try test_exec_args.append(c_code_path);
+        try test_exec_args.append(arena, c_code_path);
     }
 
     const run_or_test = switch (arg_mode) {
src/Module.zig
@@ -411,15 +411,15 @@ pub const Decl = struct {
         /// This state detects dependency loops.
         in_progress,
         /// The file corresponding to this Decl had a parse error or ZIR error.
-        /// There will be a corresponding ErrorMsg in Module.failed_files.
+        /// There will be a corresponding ErrorMsg in Zcu.failed_files.
         file_failure,
         /// This Decl might be OK but it depends on another one which did not
         /// successfully complete semantic analysis.
         dependency_failure,
         /// Semantic analysis failure.
-        /// There will be a corresponding ErrorMsg in Module.failed_decls.
+        /// There will be a corresponding ErrorMsg in Zcu.failed_decls.
         sema_failure,
-        /// There will be a corresponding ErrorMsg in Module.failed_decls.
+        /// There will be a corresponding ErrorMsg in Zcu.failed_decls.
         codegen_failure,
         /// Sematic analysis and constant value codegen of this Decl has
         /// succeeded. However, the Decl may be outdated due to an in-progress
@@ -494,77 +494,45 @@ pub const Decl = struct {
         return LazySrcLoc.nodeOffset(decl.nodeIndexToRelative(node_index));
     }
 
-    pub fn srcLoc(decl: Decl, mod: *Module) SrcLoc {
-        return decl.nodeOffsetSrcLoc(0, mod);
+    pub fn srcLoc(decl: Decl, zcu: *Zcu) SrcLoc {
+        return decl.nodeOffsetSrcLoc(0, zcu);
     }
 
-    pub fn nodeOffsetSrcLoc(decl: Decl, node_offset: i32, mod: *Module) SrcLoc {
+    pub fn nodeOffsetSrcLoc(decl: Decl, node_offset: i32, zcu: *Zcu) SrcLoc {
         return .{
-            .file_scope = decl.getFileScope(mod),
+            .file_scope = decl.getFileScope(zcu),
             .parent_decl_node = decl.src_node,
             .lazy = LazySrcLoc.nodeOffset(node_offset),
         };
     }
 
-    pub fn srcToken(decl: Decl, mod: *Module) Ast.TokenIndex {
-        const tree = &decl.getFileScope(mod).tree;
+    pub fn srcToken(decl: Decl, zcu: *Zcu) Ast.TokenIndex {
+        const tree = &decl.getFileScope(zcu).tree;
         return tree.firstToken(decl.src_node);
     }
 
-    pub fn srcByteOffset(decl: Decl, mod: *Module) u32 {
-        const tree = &decl.getFileScope(mod).tree;
+    pub fn srcByteOffset(decl: Decl, zcu: *Zcu) u32 {
+        const tree = &decl.getFileScope(zcu).tree;
         return tree.tokens.items(.start)[decl.srcToken()];
     }
 
-    pub fn renderFullyQualifiedName(decl: Decl, mod: *Module, writer: anytype) !void {
+    pub fn renderFullyQualifiedName(decl: Decl, zcu: *Zcu, writer: anytype) !void {
         if (decl.name_fully_qualified) {
-            try writer.print("{}", .{decl.name.fmt(&mod.intern_pool)});
+            try writer.print("{}", .{decl.name.fmt(&zcu.intern_pool)});
         } else {
-            try mod.namespacePtr(decl.src_namespace).renderFullyQualifiedName(mod, decl.name, writer);
+            try zcu.namespacePtr(decl.src_namespace).renderFullyQualifiedName(zcu, decl.name, writer);
         }
     }
 
-    pub fn renderFullyQualifiedDebugName(decl: Decl, mod: *Module, writer: anytype) !void {
-        return mod.namespacePtr(decl.src_namespace).renderFullyQualifiedDebugName(mod, decl.name, writer);
+    pub fn renderFullyQualifiedDebugName(decl: Decl, zcu: *Zcu, writer: anytype) !void {
+        return zcu.namespacePtr(decl.src_namespace).renderFullyQualifiedDebugName(zcu, decl.name, writer);
     }
 
-    pub fn getFullyQualifiedName(decl: Decl, mod: *Module) !InternPool.NullTerminatedString {
-        if (decl.name_fully_qualified) return decl.name;
-
-        const ip = &mod.intern_pool;
-        const count = count: {
-            var count: usize = ip.stringToSlice(decl.name).len + 1;
-            var ns: Namespace.Index = decl.src_namespace;
-            while (true) {
-                const namespace = mod.namespacePtr(ns);
-                const ns_decl = mod.declPtr(namespace.getDeclIndex(mod));
-                count += ip.stringToSlice(ns_decl.name).len + 1;
-                ns = namespace.parent.unwrap() orelse {
-                    count += namespace.file_scope.sub_file_path.len;
-                    break :count count;
-                };
-            }
-        };
-
-        const gpa = mod.gpa;
-        const start = ip.string_bytes.items.len;
-        // Protects reads of interned strings from being reallocated during the call to
-        // renderFullyQualifiedName.
-        try ip.string_bytes.ensureUnusedCapacity(gpa, count);
-        decl.renderFullyQualifiedName(mod, ip.string_bytes.writer(gpa)) catch unreachable;
-
-        // Sanitize the name for nvptx which is more restrictive.
-        // TODO This should be handled by the backend, not the frontend. Have a
-        // look at how the C backend does it for inspiration.
-        const cpu_arch = mod.root_mod.resolved_target.result.cpu.arch;
-        if (cpu_arch.isNvptx()) {
-            for (ip.string_bytes.items[start..]) |*byte| switch (byte.*) {
-                '{', '}', '*', '[', ']', '(', ')', ',', ' ', '\'' => byte.* = '_',
-                else => {},
-            };
-        }
-
-        return ip.getOrPutTrailingString(gpa, ip.string_bytes.items.len - start);
+    pub fn fullyQualifiedName(decl: Decl, zcu: *Zcu) !InternPool.NullTerminatedString {
+        return if (decl.name_fully_qualified)
+            decl.name
+        else
+            zcu.namespacePtr(decl.src_namespace).fullyQualifiedName(zcu, decl.name);
     }
 
     pub fn typedValue(decl: Decl) error{AnalysisFail}!TypedValue {
@@ -572,38 +540,38 @@ pub const Decl = struct {
         return TypedValue{ .ty = decl.ty, .val = decl.val };
     }
 
-    pub fn internValue(decl: *Decl, mod: *Module) Allocator.Error!InternPool.Index {
+    pub fn internValue(decl: *Decl, zcu: *Zcu) Allocator.Error!InternPool.Index {
         assert(decl.has_tv);
-        const ip_index = try decl.val.intern(decl.ty, mod);
+        const ip_index = try decl.val.intern(decl.ty, zcu);
         decl.val = Value.fromInterned(ip_index);
         return ip_index;
     }
 
-    pub fn isFunction(decl: Decl, mod: *const Module) !bool {
+    pub fn isFunction(decl: Decl, zcu: *const Zcu) !bool {
         const tv = try decl.typedValue();
-        return tv.ty.zigTypeTag(mod) == .Fn;
+        return tv.ty.zigTypeTag(zcu) == .Fn;
     }
 
     /// If the Decl owns its value and it is a struct, return it,
     /// otherwise null.
-    pub fn getOwnedStruct(decl: Decl, mod: *Module) ?InternPool.Key.StructType {
+    pub fn getOwnedStruct(decl: Decl, zcu: *Zcu) ?InternPool.Key.StructType {
         if (!decl.owns_tv) return null;
         if (decl.val.ip_index == .none) return null;
-        return mod.typeToStruct(decl.val.toType());
+        return zcu.typeToStruct(decl.val.toType());
     }
 
     /// If the Decl owns its value and it is a union, return it,
     /// otherwise null.
-    pub fn getOwnedUnion(decl: Decl, mod: *Module) ?InternPool.UnionType {
+    pub fn getOwnedUnion(decl: Decl, zcu: *Zcu) ?InternPool.UnionType {
         if (!decl.owns_tv) return null;
         if (decl.val.ip_index == .none) return null;
-        return mod.typeToUnion(decl.val.toType());
+        return zcu.typeToUnion(decl.val.toType());
     }
 
-    pub fn getOwnedFunction(decl: Decl, mod: *Module) ?InternPool.Key.Func {
+    pub fn getOwnedFunction(decl: Decl, zcu: *Zcu) ?InternPool.Key.Func {
         const i = decl.getOwnedFunctionIndex();
         if (i == .none) return null;
-        return switch (mod.intern_pool.indexToKey(i)) {
+        return switch (zcu.intern_pool.indexToKey(i)) {
             .func => |func| func,
             else => null,
         };
@@ -616,24 +584,24 @@ pub const Decl = struct {
 
     /// If the Decl owns its value and it is an extern function, returns it,
     /// otherwise null.
-    pub fn getOwnedExternFunc(decl: Decl, mod: *Module) ?InternPool.Key.ExternFunc {
-        return if (decl.owns_tv) decl.val.getExternFunc(mod) else null;
+    pub fn getOwnedExternFunc(decl: Decl, zcu: *Zcu) ?InternPool.Key.ExternFunc {
+        return if (decl.owns_tv) decl.val.getExternFunc(zcu) else null;
     }
 
     /// If the Decl owns its value and it is a variable, returns it,
     /// otherwise null.
-    pub fn getOwnedVariable(decl: Decl, mod: *Module) ?InternPool.Key.Variable {
-        return if (decl.owns_tv) decl.val.getVariable(mod) else null;
+    pub fn getOwnedVariable(decl: Decl, zcu: *Zcu) ?InternPool.Key.Variable {
+        return if (decl.owns_tv) decl.val.getVariable(zcu) else null;
     }
 
     /// Gets the namespace that this Decl creates by being a struct, union,
     /// enum, or opaque.
-    pub fn getInnerNamespaceIndex(decl: Decl, mod: *Module) Namespace.OptionalIndex {
+    pub fn getInnerNamespaceIndex(decl: Decl, zcu: *Zcu) Namespace.OptionalIndex {
         if (!decl.has_tv) return .none;
         return switch (decl.val.ip_index) {
             .empty_struct_type => .none,
             .none => .none,
-            else => switch (mod.intern_pool.indexToKey(decl.val.toIntern())) {
+            else => switch (zcu.intern_pool.indexToKey(decl.val.toIntern())) {
                 .opaque_type => |opaque_type| opaque_type.namespace.toOptional(),
                 .struct_type => |struct_type| struct_type.namespace,
                 .union_type => |union_type| union_type.namespace.toOptional(),
@@ -644,19 +612,19 @@ pub const Decl = struct {
     }
 
     /// Like `getInnerNamespaceIndex`, but only returns it if the Decl is the owner.
-    pub fn getOwnedInnerNamespaceIndex(decl: Decl, mod: *Module) Namespace.OptionalIndex {
+    pub fn getOwnedInnerNamespaceIndex(decl: Decl, zcu: *Zcu) Namespace.OptionalIndex {
         if (!decl.owns_tv) return .none;
-        return decl.getInnerNamespaceIndex(mod);
+        return decl.getInnerNamespaceIndex(zcu);
     }
 
     /// Same as `getOwnedInnerNamespaceIndex` but additionally obtains the pointer.
-    pub fn getOwnedInnerNamespace(decl: Decl, mod: *Module) ?*Namespace {
-        return mod.namespacePtrUnwrap(decl.getOwnedInnerNamespaceIndex(mod));
+    pub fn getOwnedInnerNamespace(decl: Decl, zcu: *Zcu) ?*Namespace {
+        return zcu.namespacePtrUnwrap(decl.getOwnedInnerNamespaceIndex(zcu));
     }
 
     /// Same as `getInnerNamespaceIndex` but additionally obtains the pointer.
-    pub fn getInnerNamespace(decl: Decl, mod: *Module) ?*Namespace {
-        return mod.namespacePtrUnwrap(decl.getInnerNamespaceIndex(mod));
+    pub fn getInnerNamespace(decl: Decl, zcu: *Zcu) ?*Namespace {
+        return zcu.namespacePtrUnwrap(decl.getInnerNamespaceIndex(zcu));
     }
 
     pub fn dump(decl: *Decl) void {
@@ -674,27 +642,27 @@ pub const Decl = struct {
         std.debug.print("\n", .{});
     }
 
-    pub fn getFileScope(decl: Decl, mod: *Module) *File {
-        return mod.namespacePtr(decl.src_namespace).file_scope;
+    pub fn getFileScope(decl: Decl, zcu: *Zcu) *File {
+        return zcu.namespacePtr(decl.src_namespace).file_scope;
     }
 
-    pub fn getExternDecl(decl: Decl, mod: *Module) OptionalIndex {
+    pub fn getExternDecl(decl: Decl, zcu: *Zcu) OptionalIndex {
         assert(decl.has_tv);
-        return switch (mod.intern_pool.indexToKey(decl.val.toIntern())) {
+        return switch (zcu.intern_pool.indexToKey(decl.val.toIntern())) {
             .variable => |variable| if (variable.is_extern) variable.decl.toOptional() else .none,
             .extern_func => |extern_func| extern_func.decl.toOptional(),
             else => .none,
         };
     }
 
-    pub fn isExtern(decl: Decl, mod: *Module) bool {
-        return decl.getExternDecl(mod) != .none;
+    pub fn isExtern(decl: Decl, zcu: *Zcu) bool {
+        return decl.getExternDecl(zcu) != .none;
     }
 
-    pub fn getAlignment(decl: Decl, mod: *Module) Alignment {
+    pub fn getAlignment(decl: Decl, zcu: *Zcu) Alignment {
         assert(decl.has_tv);
         if (decl.alignment != .none) return decl.alignment;
-        return decl.ty.abiAlignment(mod);
+        return decl.ty.abiAlignment(zcu);
     }
 };
 
@@ -704,7 +672,7 @@ pub const EmitH = struct {
 };
 
 pub const DeclAdapter = struct {
-    mod: *Module,
+    zcu: *Zcu,
 
     pub fn hash(self: @This(), s: InternPool.NullTerminatedString) u32 {
         _ = self;
@@ -713,8 +681,7 @@ pub const DeclAdapter = struct {
 
     pub fn eql(self: @This(), a: InternPool.NullTerminatedString, b_decl_index: Decl.Index, b_index: usize) bool {
         _ = b_index;
-        const b_decl = self.mod.declPtr(b_decl_index);
-        return a == b_decl.name;
+        return a == self.zcu.declPtr(b_decl_index).name;
     }
 };
 
@@ -723,7 +690,7 @@ pub const Namespace = struct {
     parent: OptionalIndex,
     file_scope: *File,
     /// Will be a struct, enum, union, or opaque.
-    ty: Type,
+    decl_index: Decl.Index,
     /// Direct children of the namespace.
     /// Declaration order is preserved via entry order.
     /// These are only declarations named directly by the AST; anonymous
@@ -739,7 +706,7 @@ pub const Namespace = struct {
     const OptionalIndex = InternPool.OptionalNamespaceIndex;
 
     const DeclContext = struct {
-        module: *Module,
+        zcu: *Zcu,
 
         pub fn hash(ctx: @This(), decl_index: Decl.Index) u32 {
             const decl = ctx.module.declPtr(decl_index);
@@ -757,39 +724,87 @@ pub const Namespace = struct {
     // This renders e.g. "std.fs.Dir.OpenOptions"
     pub fn renderFullyQualifiedName(
         ns: Namespace,
-        mod: *Module,
+        zcu: *Zcu,
         name: InternPool.NullTerminatedString,
         writer: anytype,
     ) @TypeOf(writer).Error!void {
         if (ns.parent.unwrap()) |parent| {
-            const decl = mod.declPtr(ns.getDeclIndex(mod));
-            try mod.namespacePtr(parent).renderFullyQualifiedName(mod, decl.name, writer);
+            try zcu.namespacePtr(parent).renderFullyQualifiedName(
+                zcu,
+                zcu.declPtr(ns.decl_index).name,
+                writer,
+            );
         } else {
             try ns.file_scope.renderFullyQualifiedName(writer);
         }
-        if (name != .empty) try writer.print(".{}", .{name.fmt(&mod.intern_pool)});
+        if (name != .empty) try writer.print(".{}", .{name.fmt(&zcu.intern_pool)});
     }
 
     /// This renders e.g. "std/fs.zig:Dir.OpenOptions"
     pub fn renderFullyQualifiedDebugName(
         ns: Namespace,
-        mod: *Module,
+        zcu: *Zcu,
         name: InternPool.NullTerminatedString,
         writer: anytype,
     ) @TypeOf(writer).Error!void {
-        const separator_char: u8 = if (ns.parent.unwrap()) |parent| sep: {
-            const decl = mod.declPtr(ns.getDeclIndex(mod));
-            try mod.namespacePtr(parent).renderFullyQualifiedDebugName(mod, decl.name, writer);
+        const sep: u8 = if (ns.parent.unwrap()) |parent| sep: {
+            try zcu.namespacePtr(parent).renderFullyQualifiedDebugName(
+                zcu,
+                zcu.declPtr(ns.decl_index).name,
+                writer,
+            );
             break :sep '.';
         } else sep: {
             try ns.file_scope.renderFullyQualifiedDebugName(writer);
             break :sep ':';
         };
-        if (name != .empty) try writer.print("{c}{}", .{ separator_char, name.fmt(&mod.intern_pool) });
+        if (name != .empty) try writer.print("{c}{}", .{ sep, name.fmt(&zcu.intern_pool) });
     }
 
-    pub fn getDeclIndex(ns: Namespace, mod: *Module) Decl.Index {
-        return ns.ty.getOwnerDecl(mod);
+    pub fn fullyQualifiedName(
+        ns: Namespace,
+        zcu: *Zcu,
+        name: InternPool.NullTerminatedString,
+    ) !InternPool.NullTerminatedString {
+        const ip = &zcu.intern_pool;
+        const count = count: {
+            var count: usize = ip.stringToSlice(name).len + 1;
+            var cur_ns = &ns;
+            while (true) {
+                const decl = zcu.declPtr(cur_ns.decl_index);
+                count += ip.stringToSlice(decl.name).len + 1;
+                cur_ns = zcu.namespacePtr(cur_ns.parent.unwrap() orelse {
+                    count += ns.file_scope.sub_file_path.len;
+                    break :count count;
+                });
+            }
+        };
+
+        const gpa = zcu.gpa;
+        const start = ip.string_bytes.items.len;
+        // Protects reads of interned strings from being reallocated during the call to
+        // renderFullyQualifiedName.
+        try ip.string_bytes.ensureUnusedCapacity(gpa, count);
+        ns.renderFullyQualifiedName(zcu, name, ip.string_bytes.writer(gpa)) catch unreachable;
+
+        // Sanitize the name for nvptx which is more restrictive.
+        // TODO This should be handled by the backend, not the frontend. Have a
+        // look at how the C backend does it for inspiration.
+        const cpu_arch = zcu.root_mod.resolved_target.result.cpu.arch;
+        if (cpu_arch.isNvptx()) {
+            for (ip.string_bytes.items[start..]) |*byte| switch (byte.*) {
+                '{', '}', '*', '[', ']', '(', ')', ',', ' ', '\'' => byte.* = '_',
+                else => {},
+            };
+        }
+
+        return ip.getOrPutTrailingString(gpa, ip.string_bytes.items.len - start);
+    }
+
+    pub fn getType(ns: Namespace, zcu: *Zcu) Type {
+        const decl = zcu.declPtr(ns.decl_index);
+        assert(decl.has_tv);
+        return decl.val.toType();
     }
 };
 
@@ -2559,9 +2574,8 @@ pub fn namespacePtrUnwrap(mod: *Module, index: Namespace.OptionalIndex) ?*Namesp
 pub fn declIsRoot(mod: *Module, decl_index: Decl.Index) bool {
     const decl = mod.declPtr(decl_index);
     const namespace = mod.namespacePtr(decl.src_namespace);
-    if (namespace.parent != .none)
-        return false;
-    return decl_index == namespace.getDeclIndex(mod);
+    if (namespace.parent != .none) return false;
+    return decl_index == namespace.decl_index;
 }
 
 fn freeExportList(gpa: Allocator, export_list: *ArrayListUnmanaged(*Export)) void {
@@ -3592,7 +3606,7 @@ pub fn ensureFuncBodyAnalyzed(zcu: *Zcu, func_index: InternPool.Index) SemaError
     defer liveness.deinit(gpa);
 
     if (dump_air) {
-        const fqn = try decl.getFullyQualifiedName(zcu);
+        const fqn = try decl.fullyQualifiedName(zcu);
         std.debug.print("# Begin Function AIR: {}:\n", .{fqn.fmt(ip)});
         @import("print_air.zig").dump(zcu, air, liveness);
         std.debug.print("# End Function AIR: {}\n\n", .{fqn.fmt(ip)});
@@ -3738,7 +3752,7 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
     // InternPool index.
     const new_namespace_index = try mod.createNamespace(.{
         .parent = .none,
-        .ty = undefined,
+        .decl_index = undefined,
         .file_scope = file,
     });
     const new_namespace = mod.namespacePtr(new_namespace_index);
@@ -3749,6 +3763,7 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
     errdefer @panic("TODO error handling");
 
     file.root_decl = new_decl_index.toOptional();
+    new_namespace.decl_index = new_decl_index;
 
     new_decl.name = try file.fullyQualifiedName(mod);
     new_decl.name_fully_qualified = true;
@@ -3808,7 +3823,6 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
         _ = try decl.internValue(mod);
     }
 
-    new_namespace.ty = Type.fromInterned(struct_ty);
     new_decl.val = Value.fromInterned(struct_ty);
     new_decl.has_tv = true;
     new_decl.owns_tv = true;
@@ -3881,7 +3895,7 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !SemaDeclResult {
         const std_decl = mod.declPtr(std_file.root_decl.unwrap().?);
         const std_namespace = std_decl.getInnerNamespace(mod).?;
         const builtin_str = try ip.getOrPutString(gpa, "builtin");
-        const builtin_decl = mod.declPtr(std_namespace.decls.getKeyAdapted(builtin_str, DeclAdapter{ .mod = mod }) orelse break :blk .none);
+        const builtin_decl = mod.declPtr(std_namespace.decls.getKeyAdapted(builtin_str, DeclAdapter{ .zcu = mod }) orelse break :blk .none);
         const builtin_namespace = builtin_decl.getInnerNamespaceIndex(mod).unwrap() orelse break :blk .none;
         if (decl.src_namespace != builtin_namespace) break :blk .none;
         // We're in builtin.zig. This could be a builtin we need to add to a specific InternPool index.
@@ -4576,8 +4590,8 @@ fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void
     const gop = try namespace.decls.getOrPutContextAdapted(
         gpa,
         decl_name,
-        DeclAdapter{ .mod = zcu },
-        Namespace.DeclContext{ .module = zcu },
+        DeclAdapter{ .zcu = zcu },
+        Namespace.DeclContext{ .zcu = zcu },
     );
     const comp = zcu.comp;
     if (!gop.found_existing) {
@@ -4600,12 +4614,11 @@ fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void
             .@"test" => a: {
                 if (!comp.config.is_test) break :a false;
                 if (decl_mod != zcu.main_mod) break :a false;
-                if (is_named_test) {
-                    if (comp.test_filter) |test_filter| {
-                        if (mem.indexOf(u8, ip.stringToSlice(decl_name), test_filter) == null) {
-                            break :a false;
-                        }
-                    }
+                if (is_named_test and comp.test_filters.len > 0) {
+                    const decl_fqn = ip.stringToSlice(try namespace.fullyQualifiedName(zcu, decl_name));
+                    for (comp.test_filters) |test_filter| {
+                        if (mem.indexOf(u8, decl_fqn, test_filter)) |_| break;
+                    } else break :a false;
                 }
                 try zcu.test_functions.put(gpa, new_decl_index, {});
                 break :a true;
@@ -5622,7 +5635,7 @@ pub fn populateTestFunctions(
     const test_functions_str = try ip.getOrPutString(gpa, "test_functions");
     const decl_index = builtin_namespace.decls.getKeyAdapted(
         test_functions_str,
-        DeclAdapter{ .mod = mod },
+        DeclAdapter{ .zcu = mod },
     ).?;
     {
         // We have to call `ensureDeclAnalyzed` here in case `builtin.test_functions`
@@ -5646,8 +5659,7 @@ pub fn populateTestFunctions(
 
         for (test_fn_vals, mod.test_functions.keys()) |*test_fn_val, test_decl_index| {
             const test_decl = mod.declPtr(test_decl_index);
-            // TODO: write something like getCoercedInts to avoid needing to dupe
-            const test_decl_name = try gpa.dupe(u8, ip.stringToSlice(test_decl.name));
+            const test_decl_name = try gpa.dupe(u8, ip.stringToSlice(try test_decl.fullyQualifiedName(mod)));
             defer gpa.free(test_decl_name);
             const test_name_decl_index = n: {
                 const test_name_decl_ty = try mod.arrayType(.{
@@ -6359,17 +6371,13 @@ pub fn opaqueSrcLoc(mod: *Module, opaque_type: InternPool.Key.OpaqueType) SrcLoc
 }
 
 pub fn opaqueFullyQualifiedName(mod: *Module, opaque_type: InternPool.Key.OpaqueType) !InternPool.NullTerminatedString {
-    return mod.declPtr(opaque_type.decl).getFullyQualifiedName(mod);
+    return mod.declPtr(opaque_type.decl).fullyQualifiedName(mod);
 }
 
 pub fn declFileScope(mod: *Module, decl_index: Decl.Index) *File {
     return mod.declPtr(decl_index).getFileScope(mod);
 }
 
-pub fn namespaceDeclIndex(mod: *Module, namespace_index: Namespace.Index) Decl.Index {
-    return mod.namespacePtr(namespace_index).getDeclIndex(mod);
-}
-
 /// Returns null in the following cases:
 /// * `@TypeOf(.{})`
 /// * A struct which has no fields (`struct {}`).
src/Sema.zig
@@ -2801,10 +2801,9 @@ fn zirStructDecl(
 
     const new_namespace_index = try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
-        .ty = undefined,
+        .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
     });
-    const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer mod.destroyNamespace(new_namespace_index);
 
     const struct_ty = ty: {
@@ -2821,7 +2820,6 @@ fn zirStructDecl(
 
     new_decl.ty = Type.type;
     new_decl.val = Value.fromInterned(struct_ty);
-    new_namespace.ty = Type.fromInterned(struct_ty);
 
     const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
@@ -2990,10 +2988,9 @@ fn zirEnumDecl(
 
     const new_namespace_index = try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
-        .ty = undefined,
+        .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
     });
-    const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer if (!done) mod.destroyNamespace(new_namespace_index);
 
     const decls = sema.code.bodySlice(extra_index, decls_len);
@@ -3036,7 +3033,6 @@ fn zirEnumDecl(
 
     new_decl.ty = Type.type;
     new_decl.val = Value.fromInterned(incomplete_enum.index);
-    new_namespace.ty = Type.fromInterned(incomplete_enum.index);
 
     const decl_val = try sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
@@ -3248,10 +3244,9 @@ fn zirUnionDecl(
 
     const new_namespace_index = try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
-        .ty = undefined,
+        .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
     });
-    const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer mod.destroyNamespace(new_namespace_index);
 
     const union_ty = ty: {
@@ -3292,7 +3287,6 @@ fn zirUnionDecl(
 
     new_decl.ty = Type.type;
     new_decl.val = Value.fromInterned(union_ty);
-    new_namespace.ty = Type.fromInterned(union_ty);
 
     const decls = sema.code.bodySlice(extra_index, decls_len);
     try mod.scanNamespace(new_namespace_index, decls, new_decl);
@@ -3346,10 +3340,9 @@ fn zirOpaqueDecl(
 
     const new_namespace_index = try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
-        .ty = undefined,
+        .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
     });
-    const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer mod.destroyNamespace(new_namespace_index);
 
     const opaque_ty = try mod.intern(.{ .opaque_type = .{
@@ -3362,7 +3355,6 @@ fn zirOpaqueDecl(
 
     new_decl.ty = Type.type;
     new_decl.val = Value.fromInterned(opaque_ty);
-    new_namespace.ty = Type.fromInterned(opaque_ty);
 
     const decls = sema.code.bodySlice(extra_index, decls_len);
     try mod.scanNamespace(new_namespace_index, decls, new_decl);
@@ -4834,7 +4826,7 @@ fn validateStructInit(
         if (root_msg) |msg| {
             if (mod.typeToStruct(struct_ty)) |struct_type| {
                 const decl = mod.declPtr(struct_type.decl.unwrap().?);
-                const fqn = try decl.getFullyQualifiedName(mod);
+                const fqn = try decl.fullyQualifiedName(mod);
                 try mod.errNoteNonLazy(
                     decl.srcLoc(mod),
                     msg,
@@ -4961,7 +4953,7 @@ fn validateStructInit(
     if (root_msg) |msg| {
         if (mod.typeToStruct(struct_ty)) |struct_type| {
             const decl = mod.declPtr(struct_type.decl.unwrap().?);
-            const fqn = try decl.getFullyQualifiedName(mod);
+            const fqn = try decl.fullyQualifiedName(mod);
             try mod.errNoteNonLazy(
                 decl.srcLoc(mod),
                 msg,
@@ -5355,7 +5347,7 @@ fn failWithBadStructFieldAccess(
     const mod = sema.mod;
     const gpa = sema.gpa;
     const decl = mod.declPtr(struct_type.decl.unwrap().?);
-    const fqn = try decl.getFullyQualifiedName(mod);
+    const fqn = try decl.fullyQualifiedName(mod);
 
     const msg = msg: {
         const msg = try sema.errMsg(
@@ -5382,7 +5374,7 @@ fn failWithBadUnionFieldAccess(
     const gpa = sema.gpa;
 
     const decl = mod.declPtr(union_obj.decl);
-    const fqn = try decl.getFullyQualifiedName(mod);
+    const fqn = try decl.fullyQualifiedName(mod);
 
     const msg = msg: {
         const msg = try sema.errMsg(
@@ -6504,8 +6496,7 @@ fn lookupInNamespace(
     const mod = sema.mod;
 
     const namespace = mod.namespacePtr(namespace_index);
-    const namespace_decl_index = namespace.getDeclIndex(mod);
-    const namespace_decl = mod.declPtr(namespace_decl_index);
+    const namespace_decl = mod.declPtr(namespace.decl_index);
     if (namespace_decl.analysis == .file_failure) {
         return error.AnalysisFail;
     }
@@ -6526,7 +6517,7 @@ fn lookupInNamespace(
 
         while (check_i < checked_namespaces.count()) : (check_i += 1) {
             const check_ns = checked_namespaces.keys()[check_i];
-            if (check_ns.decls.getKeyAdapted(ident_name, Module.DeclAdapter{ .mod = mod })) |decl_index| {
+            if (check_ns.decls.getKeyAdapted(ident_name, Module.DeclAdapter{ .zcu = mod })) |decl_index| {
                 // Skip decls which are not marked pub, which are in a different
                 // file than the `a.b`/`@hasDecl` syntax.
                 const decl = mod.declPtr(decl_index);
@@ -6584,7 +6575,7 @@ fn lookupInNamespace(
                 return sema.failWithOwnedErrorMsg(block, msg);
             },
         }
-    } else if (namespace.decls.getKeyAdapted(ident_name, Module.DeclAdapter{ .mod = mod })) |decl_index| {
+    } else if (namespace.decls.getKeyAdapted(ident_name, Module.DeclAdapter{ .zcu = mod })) |decl_index| {
         return decl_index;
     }
 
@@ -17210,7 +17201,7 @@ fn zirThis(
     extended: Zir.Inst.Extended.InstData,
 ) CompileError!Air.Inst.Ref {
     const mod = sema.mod;
-    const this_decl_index = mod.namespaceDeclIndex(block.namespace);
+    const this_decl_index = mod.namespacePtr(block.namespace).decl_index;
     const src = LazySrcLoc.nodeOffset(@bitCast(extended.operand));
     return sema.analyzeDeclVal(block, src, this_decl_index);
 }
@@ -20075,7 +20066,7 @@ fn finishStructInit(
     if (root_msg) |msg| {
         if (mod.typeToStruct(struct_ty)) |struct_type| {
             const decl = mod.declPtr(struct_type.decl.unwrap().?);
-            const fqn = try decl.getFullyQualifiedName(mod);
+            const fqn = try decl.fullyQualifiedName(mod);
             try mod.errNoteNonLazy(
                 decl.srcLoc(mod),
                 msg,
@@ -21404,10 +21395,9 @@ fn zirReify(
 
             const new_namespace_index = try mod.createNamespace(.{
                 .parent = block.namespace.toOptional(),
-                .ty = undefined,
+                .decl_index = new_decl_index,
                 .file_scope = block.getFileScope(mod),
             });
-            const new_namespace = mod.namespacePtr(new_namespace_index);
             errdefer mod.destroyNamespace(new_namespace_index);
 
             const opaque_ty = try mod.intern(.{ .opaque_type = .{
@@ -21420,7 +21410,6 @@ fn zirReify(
 
             new_decl.ty = Type.type;
             new_decl.val = Value.fromInterned(opaque_ty);
-            new_namespace.ty = Type.fromInterned(opaque_ty);
 
             const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
             try mod.finalizeAnonDecl(new_decl_index);
@@ -21614,10 +21603,9 @@ fn zirReify(
 
             const new_namespace_index = try mod.createNamespace(.{
                 .parent = block.namespace.toOptional(),
-                .ty = undefined,
+                .decl_index = new_decl_index,
                 .file_scope = block.getFileScope(mod),
             });
-            const new_namespace = mod.namespacePtr(new_namespace_index);
             errdefer mod.destroyNamespace(new_namespace_index);
 
             const union_ty = try ip.getUnionType(gpa, .{
@@ -21649,7 +21637,6 @@ fn zirReify(
 
             new_decl.ty = Type.type;
             new_decl.val = Value.fromInterned(union_ty);
-            new_namespace.ty = Type.fromInterned(union_ty);
 
             const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
             try mod.finalizeAnonDecl(new_decl_index);
@@ -37260,7 +37247,7 @@ fn generateUnionTagTypeNumbered(
     const src_decl = mod.declPtr(block.src_decl);
     const new_decl_index = try mod.allocateNewDecl(block.namespace, src_decl.src_node, block.wip_capture_scope);
     errdefer mod.destroyDecl(new_decl_index);
-    const fqn = try decl.getFullyQualifiedName(mod);
+    const fqn = try decl.fullyQualifiedName(mod);
     const name = try ip.getOrPutStringFmt(gpa, "@typeInfo({}).Union.tag_type.?", .{fqn.fmt(ip)});
     try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, .{
         .ty = Type.noreturn,
@@ -37269,7 +37256,6 @@ fn generateUnionTagTypeNumbered(
     errdefer mod.abortAnonDecl(new_decl_index);
 
     const new_decl = mod.declPtr(new_decl_index);
-    new_decl.name_fully_qualified = true;
     new_decl.owns_tv = true;
     new_decl.name_fully_qualified = true;
 
@@ -37310,7 +37296,7 @@ fn generateUnionTagTypeSimple(
                 .val = Value.@"unreachable",
             });
         };
-        const fqn = try mod.declPtr(decl_index).getFullyQualifiedName(mod);
+        const fqn = try mod.declPtr(decl_index).fullyQualifiedName(mod);
         const src_decl = mod.declPtr(block.src_decl);
         const new_decl_index = try mod.allocateNewDecl(block.namespace, src_decl.src_node, block.wip_capture_scope);
         errdefer mod.destroyDecl(new_decl_index);
test/src/Cases.zig
@@ -537,7 +537,7 @@ pub fn lowerToBuildSteps(
     self: *Cases,
     b: *std.Build,
     parent_step: *std.Build.Step,
-    opt_test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     cases_dir_path: []const u8,
     incremental_exe: *std.Build.Step.Compile,
 ) void {
@@ -552,9 +552,9 @@ pub fn lowerToBuildSteps(
             // compilation is in a happier state.
             continue;
         }
-        if (opt_test_filter) |test_filter| {
-            if (std.mem.indexOf(u8, incr_case.base_path, test_filter) == null) continue;
-        }
+        for (test_filters) |test_filter| {
+            if (std.mem.indexOf(u8, incr_case.base_path, test_filter)) |_| break;
+        } else if (test_filters.len > 0) continue;
         const case_base_path_with_dir = std.fs.path.join(b.allocator, &.{
             cases_dir_path, incr_case.base_path,
         }) catch @panic("OOM");
@@ -573,9 +573,9 @@ pub fn lowerToBuildSteps(
         assert(case.updates.items.len == 1);
         const update = case.updates.items[0];
 
-        if (opt_test_filter) |test_filter| {
-            if (std.mem.indexOf(u8, case.name, test_filter) == null) continue;
-        }
+        for (test_filters) |test_filter| {
+            if (std.mem.indexOf(u8, case.name, test_filter)) |_| break;
+        } else if (test_filters.len > 0) continue;
 
         const writefiles = b.addWriteFiles();
         var file_sources = std.StringHashMap(std.Build.LazyPath).init(b.allocator);
@@ -685,9 +685,9 @@ pub fn lowerToBuildSteps(
     for (self.translate.items) |case| switch (case.kind) {
         .run => |output| {
             const annotated_case_name = b.fmt("run-translated-c  {s}", .{case.name});
-            if (opt_test_filter) |filter| {
-                if (std.mem.indexOf(u8, annotated_case_name, filter) == null) continue;
-            }
+            for (test_filters) |test_filter| {
+                if (std.mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+            } else if (test_filters.len > 0) continue;
             if (!std.process.can_spawn) {
                 std.debug.print("Unable to spawn child processes on {s}, skipping test.\n", .{@tagName(builtin.os.tag)});
                 continue; // Pass test.
@@ -721,9 +721,9 @@ pub fn lowerToBuildSteps(
         },
         .translate => |output| {
             const annotated_case_name = b.fmt("zig translate-c {s}", .{case.name});
-            if (opt_test_filter) |filter| {
-                if (std.mem.indexOf(u8, annotated_case_name, filter) == null) continue;
-            }
+            for (test_filters) |test_filter| {
+                if (std.mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+            } else if (test_filters.len > 0) continue;
 
             const write_src = b.addWriteFiles();
             const file_source = write_src.add("tmp.c", case.input);
@@ -1440,9 +1440,9 @@ fn runCases(self: *Cases, zig_exe_path: []const u8) !void {
 
             assert(case.backend != .stage1);
 
-            if (build_options.test_filter) |test_filter| {
-                if (std.mem.indexOf(u8, case.name, test_filter) == null) continue;
-            }
+            for (build_options.test_filters) |test_filter| {
+                if (std.mem.indexOf(u8, case.name, test_filter)) |_| break;
+            } else if (build_options.test_filters.len > 0) continue;
 
             var prg_node = root_node.start(case.name, case.updates.items.len);
             prg_node.activate();
test/src/CompareOutput.zig
@@ -4,7 +4,7 @@
 b: *std.Build,
 step: *std.Build.Step,
 test_index: usize,
-test_filter: ?[]const u8,
+test_filters: []const []const u8,
 optimize_modes: []const OptimizeMode,
 
 const Special = enum {
@@ -90,9 +90,9 @@ pub fn addCase(self: *CompareOutput, case: TestCase) void {
             const annotated_case_name = fmt.allocPrint(self.b.allocator, "run assemble-and-link {s}", .{
                 case.name,
             }) catch @panic("OOM");
-            if (self.test_filter) |filter| {
-                if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-            }
+            for (self.test_filters) |test_filter| {
+                if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+            } else if (self.test_filters.len > 0) return;
 
             const exe = b.addExecutable(.{
                 .name = "test",
@@ -113,9 +113,9 @@ pub fn addCase(self: *CompareOutput, case: TestCase) void {
                 const annotated_case_name = fmt.allocPrint(self.b.allocator, "run compare-output {s} ({s})", .{
                     case.name, @tagName(optimize),
                 }) catch @panic("OOM");
-                if (self.test_filter) |filter| {
-                    if (mem.indexOf(u8, annotated_case_name, filter) == null) continue;
-                }
+                for (self.test_filters) |test_filter| {
+                    if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+                } else if (self.test_filters.len > 0) return;
 
                 const exe = b.addExecutable(.{
                     .name = "test",
@@ -139,9 +139,9 @@ pub fn addCase(self: *CompareOutput, case: TestCase) void {
             // TODO iterate over self.optimize_modes and test this in both
             // debug and release safe mode
             const annotated_case_name = fmt.allocPrint(self.b.allocator, "run safety {s}", .{case.name}) catch @panic("OOM");
-            if (self.test_filter) |filter| {
-                if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-            }
+            for (self.test_filters) |test_filter| {
+                if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+            } else if (self.test_filters.len > 0) return;
 
             const exe = b.addExecutable(.{
                 .name = "test",
test/src/run_translated_c.zig
@@ -1,106 +0,0 @@
-// This is the implementation of the test harness for running translated
-// C code. For the actual test cases, see test/run_translated_c.zig.
-const std = @import("std");
-const ArrayList = std.ArrayList;
-const fmt = std.fmt;
-const mem = std.mem;
-const fs = std.fs;
-
-pub const RunTranslatedCContext = struct {
-    b: *std.Build,
-    step: *std.Build.Step,
-    test_index: usize,
-    test_filter: ?[]const u8,
-    target: std.Build.ResolvedTarget,
-
-    const TestCase = struct {
-        name: []const u8,
-        sources: ArrayList(SourceFile),
-        expected_stdout: []const u8,
-        allow_warnings: bool,
-
-        const SourceFile = struct {
-            filename: []const u8,
-            source: []const u8,
-        };
-
-        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
-            self.sources.append(SourceFile{
-                .filename = filename,
-                .source = source,
-            }) catch unreachable;
-        }
-    };
-
-    pub fn create(
-        self: *RunTranslatedCContext,
-        allow_warnings: bool,
-        filename: []const u8,
-        name: []const u8,
-        source: []const u8,
-        expected_stdout: []const u8,
-    ) *TestCase {
-        const tc = self.b.allocator.create(TestCase) catch unreachable;
-        tc.* = TestCase{
-            .name = name,
-            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
-            .expected_stdout = expected_stdout,
-            .allow_warnings = allow_warnings,
-        };
-
-        tc.addSourceFile(filename, source);
-        return tc;
-    }
-
-    pub fn add(
-        self: *RunTranslatedCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_stdout: []const u8,
-    ) void {
-        const tc = self.create(false, "source.c", name, source, expected_stdout);
-        self.addCase(tc);
-    }
-
-    pub fn addAllowWarnings(
-        self: *RunTranslatedCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_stdout: []const u8,
-    ) void {
-        const tc = self.create(true, "source.c", name, source, expected_stdout);
-        self.addCase(tc);
-    }
-
-    pub fn addCase(self: *RunTranslatedCContext, case: *const TestCase) void {
-        const b = self.b;
-
-        const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {s}", .{case.name}) catch unreachable;
-        if (self.test_filter) |filter| {
-            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-        }
-
-        const write_src = b.addWriteFiles();
-        for (case.sources.items) |src_file| {
-            _ = write_src.add(src_file.filename, src_file.source);
-        }
-        const translate_c = b.addTranslateC(.{
-            .root_source_file = write_src.files.items[0].getPath(),
-            .target = b.host,
-            .optimize = .Debug,
-        });
-
-        translate_c.step.name = b.fmt("{s} translate-c", .{annotated_case_name});
-        const exe = translate_c.addExecutable(.{});
-        exe.step.name = b.fmt("{s} build-exe", .{annotated_case_name});
-        exe.linkLibC();
-        const run = b.addRunArtifact(exe);
-        run.step.name = b.fmt("{s} run", .{annotated_case_name});
-        if (!case.allow_warnings) {
-            run.expectStdErrEqual("");
-        }
-        run.expectStdOutEqual(case.expected_stdout);
-
-        self.step.dependOn(&run.step);
-    }
-};
test/src/RunTranslatedC.zig
@@ -0,0 +1,103 @@
+b: *std.Build,
+step: *std.Build.Step,
+test_index: usize,
+test_filters: []const []const u8,
+target: std.Build.ResolvedTarget,
+
+const TestCase = struct {
+    name: []const u8,
+    sources: ArrayList(SourceFile),
+    expected_stdout: []const u8,
+    allow_warnings: bool,
+
+    const SourceFile = struct {
+        filename: []const u8,
+        source: []const u8,
+    };
+
+    pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
+        self.sources.append(SourceFile{
+            .filename = filename,
+            .source = source,
+        }) catch unreachable;
+    }
+};
+
+pub fn create(
+    self: *RunTranslatedCContext,
+    allow_warnings: bool,
+    filename: []const u8,
+    name: []const u8,
+    source: []const u8,
+    expected_stdout: []const u8,
+) *TestCase {
+    const tc = self.b.allocator.create(TestCase) catch unreachable;
+    tc.* = TestCase{
+        .name = name,
+        .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
+        .expected_stdout = expected_stdout,
+        .allow_warnings = allow_warnings,
+    };
+
+    tc.addSourceFile(filename, source);
+    return tc;
+}
+
+pub fn add(
+    self: *RunTranslatedCContext,
+    name: []const u8,
+    source: []const u8,
+    expected_stdout: []const u8,
+) void {
+    const tc = self.create(false, "source.c", name, source, expected_stdout);
+    self.addCase(tc);
+}
+
+pub fn addAllowWarnings(
+    self: *RunTranslatedCContext,
+    name: []const u8,
+    source: []const u8,
+    expected_stdout: []const u8,
+) void {
+    const tc = self.create(true, "source.c", name, source, expected_stdout);
+    self.addCase(tc);
+}
+
+pub fn addCase(self: *RunTranslatedCContext, case: *const TestCase) void {
+    const b = self.b;
+
+    const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {s}", .{case.name}) catch unreachable;
+    for (self.test_filters) |test_filter| {
+        if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+    } else if (self.test_filters.len > 0) return;
+
+    const write_src = b.addWriteFiles();
+    for (case.sources.items) |src_file| {
+        _ = write_src.add(src_file.filename, src_file.source);
+    }
+    const translate_c = b.addTranslateC(.{
+        .root_source_file = write_src.files.items[0].getPath(),
+        .target = b.host,
+        .optimize = .Debug,
+    });
+
+    translate_c.step.name = b.fmt("{s} translate-c", .{annotated_case_name});
+    const exe = translate_c.addExecutable(.{});
+    exe.step.name = b.fmt("{s} build-exe", .{annotated_case_name});
+    exe.linkLibC();
+    const run = b.addRunArtifact(exe);
+    run.step.name = b.fmt("{s} run", .{annotated_case_name});
+    if (!case.allow_warnings) {
+        run.expectStdErrEqual("");
+    }
+    run.expectStdOutEqual(case.expected_stdout);
+
+    self.step.dependOn(&run.step);
+}
+
+const RunTranslatedCContext = @This();
+const std = @import("std");
+const ArrayList = std.ArrayList;
+const fmt = std.fmt;
+const mem = std.mem;
+const fs = std.fs;
test/src/StackTrace.zig
@@ -1,7 +1,7 @@
 b: *std.Build,
 step: *Step,
 test_index: usize,
-test_filter: ?[]const u8,
+test_filters: []const []const u8,
 optimize_modes: []const OptimizeMode,
 check_exe: *std.Build.Step.Compile,
 
@@ -47,9 +47,9 @@ fn addExpect(
     const annotated_case_name = fmt.allocPrint(b.allocator, "check {s} ({s})", .{
         name, @tagName(optimize_mode),
     }) catch @panic("OOM");
-    if (self.test_filter) |filter| {
-        if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-    }
+    for (self.test_filters) |test_filter| {
+        if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+    } else if (self.test_filters.len > 0) return;
 
     const write_src = b.addWriteFile("source.zig", source);
     const exe = b.addExecutable(.{
test/src/translate_c.zig
@@ -1,121 +0,0 @@
-// This is the implementation of the test harness.
-// For the actual test cases, see test/translate_c.zig.
-const std = @import("std");
-const ArrayList = std.ArrayList;
-const fmt = std.fmt;
-const mem = std.mem;
-const fs = std.fs;
-
-pub const TranslateCContext = struct {
-    b: *std.Build,
-    step: *std.Build.Step,
-    test_index: usize,
-    test_filter: ?[]const u8,
-
-    const TestCase = struct {
-        name: []const u8,
-        sources: ArrayList(SourceFile),
-        expected_lines: ArrayList([]const u8),
-        allow_warnings: bool,
-        target: std.Target.Query = .{},
-
-        const SourceFile = struct {
-            filename: []const u8,
-            source: []const u8,
-        };
-
-        pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
-            self.sources.append(SourceFile{
-                .filename = filename,
-                .source = source,
-            }) catch unreachable;
-        }
-
-        pub fn addExpectedLine(self: *TestCase, text: []const u8) void {
-            self.expected_lines.append(text) catch unreachable;
-        }
-    };
-
-    pub fn create(
-        self: *TranslateCContext,
-        allow_warnings: bool,
-        filename: []const u8,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) *TestCase {
-        const tc = self.b.allocator.create(TestCase) catch unreachable;
-        tc.* = TestCase{
-            .name = name,
-            .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
-            .expected_lines = ArrayList([]const u8).init(self.b.allocator),
-            .allow_warnings = allow_warnings,
-        };
-
-        tc.addSourceFile(filename, source);
-        var arg_i: usize = 0;
-        while (arg_i < expected_lines.len) : (arg_i += 1) {
-            tc.addExpectedLine(expected_lines[arg_i]);
-        }
-        return tc;
-    }
-
-    pub fn add(
-        self: *TranslateCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) void {
-        const tc = self.create(false, "source.h", name, source, expected_lines);
-        self.addCase(tc);
-    }
-
-    pub fn addWithTarget(
-        self: *TranslateCContext,
-        name: []const u8,
-        target: std.Target.Query,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) void {
-        const tc = self.create(false, "source.h", name, source, expected_lines);
-        tc.target = target;
-        self.addCase(tc);
-    }
-
-    pub fn addAllowWarnings(
-        self: *TranslateCContext,
-        name: []const u8,
-        source: []const u8,
-        expected_lines: []const []const u8,
-    ) void {
-        const tc = self.create(true, "source.h", name, source, expected_lines);
-        self.addCase(tc);
-    }
-
-    pub fn addCase(self: *TranslateCContext, case: *const TestCase) void {
-        const b = self.b;
-
-        const translate_c_cmd = "translate-c";
-        const annotated_case_name = fmt.allocPrint(self.b.allocator, "{s} {s}", .{ translate_c_cmd, case.name }) catch unreachable;
-        if (self.test_filter) |filter| {
-            if (mem.indexOf(u8, annotated_case_name, filter) == null) return;
-        }
-
-        const write_src = b.addWriteFiles();
-        for (case.sources.items) |src_file| {
-            _ = write_src.add(src_file.filename, src_file.source);
-        }
-
-        const translate_c = b.addTranslateC(.{
-            .root_source_file = write_src.files.items[0].getPath(),
-            .target = b.resolveTargetQuery(case.target),
-            .optimize = .Debug,
-        });
-
-        translate_c.step.name = annotated_case_name;
-
-        const check_file = translate_c.addCheckFile(case.expected_lines.items);
-
-        self.step.dependOn(&check_file.step);
-    }
-};
test/src/TranslateC.zig
@@ -0,0 +1,118 @@
+b: *std.Build,
+step: *std.Build.Step,
+test_index: usize,
+test_filters: []const []const u8,
+
+const TestCase = struct {
+    name: []const u8,
+    sources: ArrayList(SourceFile),
+    expected_lines: ArrayList([]const u8),
+    allow_warnings: bool,
+    target: std.Target.Query = .{},
+
+    const SourceFile = struct {
+        filename: []const u8,
+        source: []const u8,
+    };
+
+    pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void {
+        self.sources.append(SourceFile{
+            .filename = filename,
+            .source = source,
+        }) catch unreachable;
+    }
+
+    pub fn addExpectedLine(self: *TestCase, text: []const u8) void {
+        self.expected_lines.append(text) catch unreachable;
+    }
+};
+
+pub fn create(
+    self: *TranslateCContext,
+    allow_warnings: bool,
+    filename: []const u8,
+    name: []const u8,
+    source: []const u8,
+    expected_lines: []const []const u8,
+) *TestCase {
+    const tc = self.b.allocator.create(TestCase) catch unreachable;
+    tc.* = TestCase{
+        .name = name,
+        .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator),
+        .expected_lines = ArrayList([]const u8).init(self.b.allocator),
+        .allow_warnings = allow_warnings,
+    };
+
+    tc.addSourceFile(filename, source);
+    var arg_i: usize = 0;
+    while (arg_i < expected_lines.len) : (arg_i += 1) {
+        tc.addExpectedLine(expected_lines[arg_i]);
+    }
+    return tc;
+}
+
+pub fn add(
+    self: *TranslateCContext,
+    name: []const u8,
+    source: []const u8,
+    expected_lines: []const []const u8,
+) void {
+    const tc = self.create(false, "source.h", name, source, expected_lines);
+    self.addCase(tc);
+}
+
+pub fn addWithTarget(
+    self: *TranslateCContext,
+    name: []const u8,
+    target: std.Target.Query,
+    source: []const u8,
+    expected_lines: []const []const u8,
+) void {
+    const tc = self.create(false, "source.h", name, source, expected_lines);
+    tc.target = target;
+    self.addCase(tc);
+}
+
+pub fn addAllowWarnings(
+    self: *TranslateCContext,
+    name: []const u8,
+    source: []const u8,
+    expected_lines: []const []const u8,
+) void {
+    const tc = self.create(true, "source.h", name, source, expected_lines);
+    self.addCase(tc);
+}
+
+pub fn addCase(self: *TranslateCContext, case: *const TestCase) void {
+    const b = self.b;
+
+    const translate_c_cmd = "translate-c";
+    const annotated_case_name = fmt.allocPrint(self.b.allocator, "{s} {s}", .{ translate_c_cmd, case.name }) catch unreachable;
+    for (self.test_filters) |test_filter| {
+        if (mem.indexOf(u8, annotated_case_name, test_filter)) |_| break;
+    } else if (self.test_filters.len > 0) return;
+
+    const write_src = b.addWriteFiles();
+    for (case.sources.items) |src_file| {
+        _ = write_src.add(src_file.filename, src_file.source);
+    }
+
+    const translate_c = b.addTranslateC(.{
+        .root_source_file = write_src.files.items[0].getPath(),
+        .target = b.resolveTargetQuery(case.target),
+        .optimize = .Debug,
+    });
+
+    translate_c.step.name = annotated_case_name;
+
+    const check_file = translate_c.addCheckFile(case.expected_lines.items);
+
+    self.step.dependOn(&check_file.step);
+}
+
+const TranslateCContext = @This();
+const std = @import("std");
+const ArrayList = std.ArrayList;
+const fmt = std.fmt;
+const mem = std.mem;
+const fs = std.fs;
test/tests.zig
@@ -15,8 +15,8 @@ const run_translated_c = @import("run_translated_c.zig");
 const link = @import("link.zig");
 
 // Implementations
-pub const TranslateCContext = @import("src/translate_c.zig").TranslateCContext;
-pub const RunTranslatedCContext = @import("src/run_translated_c.zig").RunTranslatedCContext;
+pub const TranslateCContext = @import("src/TranslateC.zig");
+pub const RunTranslatedCContext = @import("src/RunTranslatedC.zig");
 pub const CompareOutputContext = @import("src/CompareOutput.zig");
 pub const StackTracesContext = @import("src/StackTrace.zig");
 
@@ -619,7 +619,7 @@ const c_abi_targets = [_]CAbiTarget{
 
 pub fn addCompareOutputTests(
     b: *std.Build,
-    test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     optimize_modes: []const OptimizeMode,
 ) *Step {
     const cases = b.allocator.create(CompareOutputContext) catch @panic("OOM");
@@ -627,7 +627,7 @@ pub fn addCompareOutputTests(
         .b = b,
         .step = b.step("test-compare-output", "Run the compare output tests"),
         .test_index = 0,
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .optimize_modes = optimize_modes,
     };
 
@@ -638,7 +638,7 @@ pub fn addCompareOutputTests(
 
 pub fn addStackTraceTests(
     b: *std.Build,
-    test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     optimize_modes: []const OptimizeMode,
 ) *Step {
     const check_exe = b.addExecutable(.{
@@ -653,7 +653,7 @@ pub fn addStackTraceTests(
         .b = b,
         .step = b.step("test-stack-traces", "Run the stack trace tests"),
         .test_index = 0,
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .optimize_modes = optimize_modes,
         .check_exe = check_exe,
     };
@@ -983,13 +983,13 @@ pub fn addCliTests(b: *std.Build) *Step {
     return step;
 }
 
-pub fn addAssembleAndLinkTests(b: *std.Build, test_filter: ?[]const u8, optimize_modes: []const OptimizeMode) *Step {
+pub fn addAssembleAndLinkTests(b: *std.Build, test_filters: []const []const u8, optimize_modes: []const OptimizeMode) *Step {
     const cases = b.allocator.create(CompareOutputContext) catch @panic("OOM");
     cases.* = CompareOutputContext{
         .b = b,
         .step = b.step("test-asm-link", "Run the assemble and link tests"),
         .test_index = 0,
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .optimize_modes = optimize_modes,
     };
 
@@ -998,13 +998,13 @@ pub fn addAssembleAndLinkTests(b: *std.Build, test_filter: ?[]const u8, optimize
     return cases.step;
 }
 
-pub fn addTranslateCTests(b: *std.Build, test_filter: ?[]const u8) *Step {
+pub fn addTranslateCTests(b: *std.Build, test_filters: []const []const u8) *Step {
     const cases = b.allocator.create(TranslateCContext) catch @panic("OOM");
     cases.* = TranslateCContext{
         .b = b,
         .step = b.step("test-translate-c", "Run the C translation tests"),
         .test_index = 0,
-        .test_filter = test_filter,
+        .test_filters = test_filters,
     };
 
     translate_c.addCases(cases);
@@ -1014,7 +1014,7 @@ pub fn addTranslateCTests(b: *std.Build, test_filter: ?[]const u8) *Step {
 
 pub fn addRunTranslatedCTests(
     b: *std.Build,
-    test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     target: std.Build.ResolvedTarget,
 ) *Step {
     const cases = b.allocator.create(RunTranslatedCContext) catch @panic("OOM");
@@ -1022,7 +1022,7 @@ pub fn addRunTranslatedCTests(
         .b = b,
         .step = b.step("test-run-translated-c", "Run the Run-Translated-C tests"),
         .test_index = 0,
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .target = target,
     };
 
@@ -1032,7 +1032,7 @@ pub fn addRunTranslatedCTests(
 }
 
 const ModuleTestOptions = struct {
-    test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     root_src: []const u8,
     name: []const u8,
     desc: []const u8,
@@ -1115,7 +1115,7 @@ pub fn addModuleTests(b: *std.Build, options: ModuleTestOptions) *Step {
             .optimize = test_target.optimize_mode,
             .target = resolved_target,
             .max_rss = max_rss,
-            .filter = options.test_filter,
+            .filters = options.test_filters,
             .link_libc = test_target.link_libc,
             .single_threaded = test_target.single_threaded,
             .use_llvm = test_target.use_llvm,
@@ -1291,7 +1291,7 @@ pub fn addCAbiTests(b: *std.Build, skip_non_native: bool, skip_release: bool) *S
 pub fn addCases(
     b: *std.Build,
     parent_step: *Step,
-    opt_test_filter: ?[]const u8,
+    test_filters: []const []const u8,
     check_case_exe: *std.Build.Step.Compile,
     build_options: @import("cases.zig").BuildOptions,
 ) !void {
@@ -1310,7 +1310,7 @@ pub fn addCases(
     cases.lowerToBuildSteps(
         b,
         parent_step,
-        opt_test_filter,
+        test_filters,
         cases_dir_path,
         check_case_exe,
     );
build.zig
@@ -390,7 +390,7 @@ pub fn build(b: *std.Build) !void {
         }
     }
 
-    const test_filter = b.option([]const u8, "test-filter", "Skip tests that do not match filter");
+    const test_filters = b.option([]const []const u8, "test-filter", "Skip tests that do not match any filter") orelse &[0][]const u8{};
 
     const test_cases_options = b.addOptions();
     check_case_exe.root_module.addOptions("build_options", test_cases_options);
@@ -418,7 +418,7 @@ pub fn build(b: *std.Build) !void {
     test_cases_options.addOption(?[]const u8, "glibc_runtimes_dir", b.glibc_runtimes_dir);
     test_cases_options.addOption([:0]const u8, "version", version);
     test_cases_options.addOption(std.SemanticVersion, "semver", semver);
-    test_cases_options.addOption(?[]const u8, "test_filter", test_filter);
+    test_cases_options.addOption([]const []const u8, "test_filters", test_filters);
 
     var chosen_opt_modes_buf: [4]builtin.OptimizeMode = undefined;
     var chosen_mode_index: usize = 0;
@@ -454,7 +454,7 @@ pub fn build(b: *std.Build) !void {
     }).step);
 
     const test_cases_step = b.step("test-cases", "Run the main compiler test cases");
-    try tests.addCases(b, test_cases_step, test_filter, check_case_exe, .{
+    try tests.addCases(b, test_cases_step, test_filters, check_case_exe, .{
         .enable_llvm = enable_llvm,
         .llvm_has_m68k = llvm_has_m68k,
         .llvm_has_csky = llvm_has_csky,
@@ -464,7 +464,7 @@ pub fn build(b: *std.Build) !void {
     test_step.dependOn(test_cases_step);
 
     test_step.dependOn(tests.addModuleTests(b, .{
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .root_src = "test/behavior.zig",
         .name = "behavior",
         .desc = "Run the behavior tests",
@@ -477,7 +477,7 @@ pub fn build(b: *std.Build) !void {
     }));
 
     test_step.dependOn(tests.addModuleTests(b, .{
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .root_src = "test/c_import.zig",
         .name = "c-import",
         .desc = "Run the @cImport tests",
@@ -489,7 +489,7 @@ pub fn build(b: *std.Build) !void {
     }));
 
     test_step.dependOn(tests.addModuleTests(b, .{
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .root_src = "lib/compiler_rt.zig",
         .name = "compiler-rt",
         .desc = "Run the compiler_rt tests",
@@ -501,7 +501,7 @@ pub fn build(b: *std.Build) !void {
     }));
 
     test_step.dependOn(tests.addModuleTests(b, .{
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .root_src = "lib/c.zig",
         .name = "universal-libc",
         .desc = "Run the universal libc tests",
@@ -512,7 +512,7 @@ pub fn build(b: *std.Build) !void {
         .skip_libc = true,
     }));
 
-    test_step.dependOn(tests.addCompareOutputTests(b, test_filter, optimization_modes));
+    test_step.dependOn(tests.addCompareOutputTests(b, test_filters, optimization_modes));
     test_step.dependOn(tests.addStandaloneTests(
         b,
         optimization_modes,
@@ -523,16 +523,16 @@ pub fn build(b: *std.Build) !void {
     ));
     test_step.dependOn(tests.addCAbiTests(b, skip_non_native, skip_release));
     test_step.dependOn(tests.addLinkTests(b, enable_macos_sdk, enable_ios_sdk, false, enable_symlinks_windows));
-    test_step.dependOn(tests.addStackTraceTests(b, test_filter, optimization_modes));
+    test_step.dependOn(tests.addStackTraceTests(b, test_filters, optimization_modes));
     test_step.dependOn(tests.addCliTests(b));
-    test_step.dependOn(tests.addAssembleAndLinkTests(b, test_filter, optimization_modes));
-    test_step.dependOn(tests.addTranslateCTests(b, test_filter));
+    test_step.dependOn(tests.addAssembleAndLinkTests(b, test_filters, optimization_modes));
+    test_step.dependOn(tests.addTranslateCTests(b, test_filters));
     if (!skip_run_translated_c) {
-        test_step.dependOn(tests.addRunTranslatedCTests(b, test_filter, target));
+        test_step.dependOn(tests.addRunTranslatedCTests(b, test_filters, target));
     }
 
     test_step.dependOn(tests.addModuleTests(b, .{
-        .test_filter = test_filter,
+        .test_filters = test_filters,
         .root_src = "lib/std/std.zig",
         .name = "std",
         .desc = "Run the standard library tests",