Commit 6faa4cc7e6

mlugg <mlugg@mlugg.co.uk>
2024-08-12 12:02:18
Zcu: construct full reference graph
This commit updates `Zcu.resolveReferences` to traverse the graph of `AnalUnit` references (starting from the 1-3 roots of analysis) in order to determine which `AnalUnit`s are referenced in an update. Errors for unreferenced entities are omitted from the error bundle. However, note that unreferenced `Nav`s are not removed from the binary.
1 parent b65865b
Changed files (3)
src/Compilation.zig
@@ -2264,13 +2264,19 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
             }
         }
 
+        zcu.analysis_roots.resize(0) catch unreachable;
+
         try comp.queueJob(.{ .analyze_mod = std_mod });
+        zcu.analysis_roots.appendAssumeCapacity(std_mod);
+
         if (comp.config.is_test) {
             try comp.queueJob(.{ .analyze_mod = zcu.main_mod });
+            zcu.analysis_roots.appendAssumeCapacity(zcu.main_mod);
         }
 
         if (zcu.root_mod.deps.get("compiler_rt")) |compiler_rt_mod| {
             try comp.queueJob(.{ .analyze_mod = compiler_rt_mod });
+            zcu.analysis_roots.appendAssumeCapacity(compiler_rt_mod);
         }
     }
 
@@ -3059,6 +3065,9 @@ pub fn totalErrorCount(comp: *Compilation) u32 {
     if (comp.module) |zcu| {
         const ip = &zcu.intern_pool;
 
+        var all_references = try zcu.resolveReferences();
+        defer all_references.deinit(zcu.gpa);
+
         total += zcu.failed_exports.count();
         total += zcu.failed_embed_files.count();
 
@@ -3079,6 +3088,7 @@ pub fn totalErrorCount(comp: *Compilation) u32 {
         // the previous parse success, including compile errors, but we cannot
         // emit them until the file succeeds parsing.
         for (zcu.failed_analysis.keys()) |anal_unit| {
+            if (!all_references.contains(anal_unit)) continue;
             const file_index = switch (anal_unit.unwrap()) {
                 .cau => |cau| zcu.namespacePtr(ip.getCau(cau).namespace).file_scope,
                 .func => |ip_index| (zcu.funcInfo(ip_index).zir_body_inst.resolveFull(ip) orelse continue).file,
@@ -3116,12 +3126,6 @@ pub fn totalErrorCount(comp: *Compilation) u32 {
         }
     }
 
-    if (comp.module) |zcu| {
-        if (total == 0 and zcu.transitive_failed_analysis.count() > 0) {
-            @panic("Transitive analysis errors, but none actually emitted");
-        }
-    }
-
     return @intCast(total);
 }
 
@@ -3167,12 +3171,13 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
             .msg = try bundle.addString("memory allocation failure"),
         });
     }
+
+    var all_references = if (comp.module) |zcu| try zcu.resolveReferences() else undefined;
+    defer if (comp.module != null) all_references.deinit(gpa);
+
     if (comp.module) |zcu| {
         const ip = &zcu.intern_pool;
 
-        var all_references = try zcu.resolveReferences();
-        defer all_references.deinit(gpa);
-
         for (zcu.failed_files.keys(), zcu.failed_files.values()) |file, error_msg| {
             if (error_msg) |msg| {
                 try addModuleErrorMsg(zcu, &bundle, msg.*, &all_references);
@@ -3220,6 +3225,8 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
             if (err) |e| return e;
         }
         for (zcu.failed_analysis.keys(), zcu.failed_analysis.values()) |anal_unit, error_msg| {
+            if (!all_references.contains(anal_unit)) continue;
+
             const file_index = switch (anal_unit.unwrap()) {
                 .cau => |cau| zcu.namespacePtr(ip.getCau(cau).namespace).file_scope,
                 .func => |ip_index| (zcu.funcInfo(ip_index).zir_body_inst.resolveFull(ip) orelse continue).file,
@@ -3313,9 +3320,6 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
 
     if (comp.module) |zcu| {
         if (bundle.root_list.items.len == 0 and zcu.compile_log_sources.count() != 0) {
-            var all_references = try zcu.resolveReferences();
-            defer all_references.deinit(gpa);
-
             const values = zcu.compile_log_sources.values();
             // First one will be the error; subsequent ones will be notes.
             const src_loc = values[0].src();
@@ -3339,6 +3343,17 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
 
     assert(comp.totalErrorCount() == bundle.root_list.items.len);
 
+    if (comp.module) |zcu| {
+        if (bundle.root_list.items.len == 0) {
+            const should_have_error = for (zcu.transitive_failed_analysis.keys()) |failed_unit| {
+                if (all_references.contains(failed_unit)) break true;
+            } else false;
+            if (should_have_error) {
+                @panic("referenced transitive analysis errors, but none actually emitted");
+            }
+        }
+    }
+
     const compile_log_text = if (comp.module) |m| m.compile_log_text.items else "";
     return bundle.toOwnedBundle(compile_log_text);
 }
@@ -3393,7 +3408,7 @@ pub fn addModuleErrorMsg(
     mod: *Zcu,
     eb: *ErrorBundle.Wip,
     module_err_msg: Zcu.ErrorMsg,
-    all_references: *const std.AutoHashMapUnmanaged(InternPool.AnalUnit, Zcu.ResolvedReference),
+    all_references: *const std.AutoHashMapUnmanaged(InternPool.AnalUnit, ?Zcu.ResolvedReference),
 ) !void {
     const gpa = eb.gpa;
     const ip = &mod.intern_pool;
@@ -3423,7 +3438,8 @@ pub fn addModuleErrorMsg(
         const max_references = mod.comp.reference_trace orelse Sema.default_reference_trace_len;
 
         var referenced_by = rt_root;
-        while (all_references.get(referenced_by)) |ref| {
+        while (all_references.get(referenced_by)) |maybe_ref| {
+            const ref = maybe_ref orelse break;
             const gop = try seen.getOrPut(gpa, ref.referencer);
             if (gop.found_existing) break;
             if (ref_traces.items.len < max_references) {
src/Sema.zig
@@ -110,6 +110,7 @@ exports: std.ArrayListUnmanaged(Zcu.Export) = .{},
 /// of data stored in `Zcu.all_references`. It exists to avoid adding references to
 /// a given `AnalUnit` multiple times.
 references: std.AutoArrayHashMapUnmanaged(AnalUnit, void) = .{},
+type_references: std.AutoArrayHashMapUnmanaged(InternPool.Index, void) = .{},
 
 const MaybeComptimeAlloc = struct {
     /// The runtime index of the `alloc` instruction.
@@ -877,6 +878,7 @@ pub fn deinit(sema: *Sema) void {
     sema.comptime_allocs.deinit(gpa);
     sema.exports.deinit(gpa);
     sema.references.deinit(gpa);
+    sema.type_references.deinit(gpa);
     sema.* = undefined;
 }
 
@@ -2809,7 +2811,11 @@ fn zirStructDecl(
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getStructType(gpa, pt.tid, struct_init)) {
         .existing => |ty| wip: {
-            if (!try sema.maybeRemoveOutdatedType(ty)) return Air.internedToRef(ty);
+            if (!try sema.maybeRemoveOutdatedType(ty)) {
+                try sema.declareDependency(.{ .interned = ty });
+                try sema.addTypeReferenceEntry(src, ty);
+                return Air.internedToRef(ty);
+            }
             break :wip (try ip.getStructType(gpa, pt.tid, struct_init)).wip;
         },
         .wip => |wip| wip,
@@ -2850,8 +2856,8 @@ fn zirStructDecl(
         if (block.ownerModule().strip) break :codegen_type;
         try mod.comp.queueJob(.{ .codegen_type = wip_ty.index });
     }
-    try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .cau = new_cau_index }));
     try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     return Air.internedToRef(wip_ty.finish(ip, new_cau_index.toOptional(), new_namespace_index));
 }
 
@@ -3031,7 +3037,11 @@ fn zirEnumDecl(
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getEnumType(gpa, pt.tid, enum_init)) {
         .existing => |ty| wip: {
-            if (!try sema.maybeRemoveOutdatedType(ty)) return Air.internedToRef(ty);
+            if (!try sema.maybeRemoveOutdatedType(ty)) {
+                try sema.declareDependency(.{ .interned = ty });
+                try sema.addTypeReferenceEntry(src, ty);
+                return Air.internedToRef(ty);
+            }
             break :wip (try ip.getEnumType(gpa, pt.tid, enum_init)).wip;
         },
         .wip => |wip| wip,
@@ -3071,8 +3081,8 @@ fn zirEnumDecl(
 
     try pt.scanNamespace(new_namespace_index, decls);
 
-    try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .cau = new_cau_index }));
     try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
 
     // We've finished the initial construction of this type, and are about to perform analysis.
     // Set the Cau and namespace appropriately, and don't destroy anything on failure.
@@ -3297,7 +3307,11 @@ fn zirUnionDecl(
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getUnionType(gpa, pt.tid, union_init)) {
         .existing => |ty| wip: {
-            if (!try sema.maybeRemoveOutdatedType(ty)) return Air.internedToRef(ty);
+            if (!try sema.maybeRemoveOutdatedType(ty)) {
+                try sema.declareDependency(.{ .interned = ty });
+                try sema.addTypeReferenceEntry(src, ty);
+                return Air.internedToRef(ty);
+            }
             break :wip (try ip.getUnionType(gpa, pt.tid, union_init)).wip;
         },
         .wip => |wip| wip,
@@ -3338,8 +3352,8 @@ fn zirUnionDecl(
         if (block.ownerModule().strip) break :codegen_type;
         try mod.comp.queueJob(.{ .codegen_type = wip_ty.index });
     }
-    try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .cau = new_cau_index }));
     try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     return Air.internedToRef(wip_ty.finish(ip, new_cau_index.toOptional(), new_namespace_index));
 }
 
@@ -3388,7 +3402,10 @@ fn zirOpaqueDecl(
     // No `wrapWipTy` needed as no std.builtin types are opaque.
     const wip_ty = switch (try ip.getOpaqueType(gpa, pt.tid, opaque_init)) {
         // No `maybeRemoveOutdatedType` as opaque types are never outdated.
-        .existing => |ty| return Air.internedToRef(ty),
+        .existing => |ty| {
+            try sema.addTypeReferenceEntry(src, ty);
+            return Air.internedToRef(ty);
+        },
         .wip => |wip| wip,
     };
     errdefer wip_ty.cancel(ip, pt.tid);
@@ -3416,6 +3433,7 @@ fn zirOpaqueDecl(
         if (block.ownerModule().strip) break :codegen_type;
         try mod.comp.queueJob(.{ .codegen_type = wip_ty.index });
     }
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     return Air.internedToRef(wip_ty.finish(ip, .none, new_namespace_index));
 }
 
@@ -21820,7 +21838,10 @@ fn zirReify(
                     .zir_index = try block.trackZir(inst),
                 } },
             })) {
-                .existing => |ty| return Air.internedToRef(ty),
+                .existing => |ty| {
+                    try sema.addTypeReferenceEntry(src, ty);
+                    return Air.internedToRef(ty);
+                },
                 .wip => |wip| wip,
             };
             errdefer wip_ty.cancel(ip, pt.tid);
@@ -21839,6 +21860,7 @@ fn zirReify(
                 .file_scope = block.getFileScopeIndex(mod),
             });
 
+            try sema.addTypeReferenceEntry(src, wip_ty.index);
             return Air.internedToRef(wip_ty.finish(ip, .none, new_namespace_index));
         },
         .Union => {
@@ -22020,7 +22042,11 @@ fn reifyEnum(
         } },
     })) {
         .wip => |wip| wip,
-        .existing => |ty| return Air.internedToRef(ty),
+        .existing => |ty| {
+            try sema.declareDependency(.{ .interned = ty });
+            try sema.addTypeReferenceEntry(src, ty);
+            return Air.internedToRef(ty);
+        },
     };
     errdefer wip_ty.cancel(ip, pt.tid);
 
@@ -22044,6 +22070,8 @@ fn reifyEnum(
 
     const new_cau_index = try ip.createTypeCau(gpa, pt.tid, tracked_inst, new_namespace_index, wip_ty.index);
 
+    try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     wip_ty.prepare(ip, new_cau_index, new_namespace_index);
     wip_ty.setTagTy(ip, tag_ty.toIntern());
 
@@ -22182,7 +22210,11 @@ fn reifyUnion(
         } },
     })) {
         .wip => |wip| wip,
-        .existing => |ty| return Air.internedToRef(ty),
+        .existing => |ty| {
+            try sema.declareDependency(.{ .interned = ty });
+            try sema.addTypeReferenceEntry(src, ty);
+            return Air.internedToRef(ty);
+        },
     };
     errdefer wip_ty.cancel(ip, pt.tid);
 
@@ -22347,7 +22379,8 @@ fn reifyUnion(
         if (block.ownerModule().strip) break :codegen_type;
         try mod.comp.queueJob(.{ .codegen_type = wip_ty.index });
     }
-    try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .cau = new_cau_index }));
+    try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     return Air.internedToRef(wip_ty.finish(ip, new_cau_index.toOptional(), new_namespace_index));
 }
 
@@ -22447,7 +22480,11 @@ fn reifyStruct(
         } },
     })) {
         .wip => |wip| wip,
-        .existing => |ty| return Air.internedToRef(ty),
+        .existing => |ty| {
+            try sema.declareDependency(.{ .interned = ty });
+            try sema.addTypeReferenceEntry(src, ty);
+            return Air.internedToRef(ty);
+        },
     };
     errdefer wip_ty.cancel(ip, pt.tid);
 
@@ -22625,7 +22662,8 @@ fn reifyStruct(
         if (block.ownerModule().strip) break :codegen_type;
         try mod.comp.queueJob(.{ .codegen_type = wip_ty.index });
     }
-    try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .cau = new_cau_index }));
+    try sema.declareDependency(.{ .interned = wip_ty.index });
+    try sema.addTypeReferenceEntry(src, wip_ty.index);
     return Air.internedToRef(wip_ty.finish(ip, new_cau_index.toOptional(), new_namespace_index));
 }
 
@@ -32231,6 +32269,18 @@ fn addReferenceEntry(
     try zcu.addUnitReference(sema.owner, referenced_unit, src);
 }
 
+fn addTypeReferenceEntry(
+    sema: *Sema,
+    src: LazySrcLoc,
+    referenced_type: InternPool.Index,
+) !void {
+    const zcu = sema.pt.zcu;
+    if (zcu.comp.reference_trace == 0) return;
+    const gop = try sema.type_references.getOrPut(sema.gpa, referenced_type);
+    if (gop.found_existing) return;
+    try zcu.addTypeReference(sema.owner, referenced_type, src);
+}
+
 pub fn ensureNavResolved(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav.Index) CompileError!void {
     const pt = sema.pt;
     const zcu = pt.zcu;
src/Zcu.zig
@@ -168,6 +168,10 @@ outdated_ready: std.AutoArrayHashMapUnmanaged(AnalUnit, void) = .{},
 /// it as outdated.
 retryable_failures: std.ArrayListUnmanaged(AnalUnit) = .{},
 
+/// These are the modules which we initially queue for analysis in `Compilation.update`.
+/// `resolveReferences` will use these as the root of its reachability traversal.
+analysis_roots: std.BoundedArray(*Package.Module, 3) = .{},
+
 stage1_flags: packed struct {
     have_winmain: bool = false,
     have_wwinmain: bool = false,
@@ -186,7 +190,7 @@ global_assembly: std.AutoArrayHashMapUnmanaged(InternPool.Cau.Index, []u8) = .{}
 
 /// Key is the `AnalUnit` *performing* the reference. This representation allows
 /// incremental updates to quickly delete references caused by a specific `AnalUnit`.
-/// Value is index into `all_reference` of the first reference triggered by the unit.
+/// Value is index into `all_references` of the first reference triggered by the unit.
 /// The `next` field on the `Reference` forms a linked list of all references
 /// triggered by the key `AnalUnit`.
 reference_table: std.AutoArrayHashMapUnmanaged(AnalUnit, u32) = .{},
@@ -194,6 +198,16 @@ all_references: std.ArrayListUnmanaged(Reference) = .{},
 /// Freelist of indices in `all_references`.
 free_references: std.ArrayListUnmanaged(u32) = .{},
 
+/// Key is the `AnalUnit` *performing* the reference. This representation allows
+/// incremental updates to quickly delete references caused by a specific `AnalUnit`.
+/// Value is index into `all_type_reference` of the first reference triggered by the unit.
+/// The `next` field on the `TypeReference` forms a linked list of all type references
+/// triggered by the key `AnalUnit`.
+type_reference_table: std.AutoArrayHashMapUnmanaged(AnalUnit, u32) = .{},
+all_type_references: std.ArrayListUnmanaged(TypeReference) = .{},
+/// Freelist of indices in `all_type_references`.
+free_type_references: std.ArrayListUnmanaged(u32) = .{},
+
 panic_messages: [PanicId.len]InternPool.Nav.Index.Optional = .{.none} ** PanicId.len,
 /// The panic function body.
 panic_func_index: InternPool.Index = .none,
@@ -302,6 +316,16 @@ pub const Reference = struct {
     src: LazySrcLoc,
 };
 
+pub const TypeReference = struct {
+    /// The container type which was referenced.
+    referenced: InternPool.Index,
+    /// Index into `all_type_references` of the next `TypeReference` triggered by the same `AnalUnit`.
+    /// `std.math.maxInt(u32)` is the sentinel.
+    next: u32,
+    /// The source location of the reference.
+    src: LazySrcLoc,
+};
+
 /// The container that structs, enums, unions, and opaques have.
 pub const Namespace = struct {
     parent: OptionalIndex,
@@ -2155,6 +2179,10 @@ pub fn deinit(zcu: *Zcu) void {
     zcu.all_references.deinit(gpa);
     zcu.free_references.deinit(gpa);
 
+    zcu.type_reference_table.deinit(gpa);
+    zcu.all_type_references.deinit(gpa);
+    zcu.free_type_references.deinit(gpa);
+
     zcu.intern_pool.deinit(gpa);
 }
 
@@ -2660,16 +2688,32 @@ pub fn deleteUnitExports(zcu: *Zcu, anal_unit: AnalUnit) void {
 pub fn deleteUnitReferences(zcu: *Zcu, anal_unit: AnalUnit) void {
     const gpa = zcu.gpa;
 
-    const kv = zcu.reference_table.fetchSwapRemove(anal_unit) orelse return;
-    var idx = kv.value;
+    unit_refs: {
+        const kv = zcu.reference_table.fetchSwapRemove(anal_unit) orelse return;
+        var idx = kv.value;
 
-    while (idx != std.math.maxInt(u32)) {
-        zcu.free_references.append(gpa, idx) catch {
-            // This space will be reused eventually, so we need not propagate this error.
-            // Just leak it for now, and let GC reclaim it later on.
-            return;
-        };
-        idx = zcu.all_references.items[idx].next;
+        while (idx != std.math.maxInt(u32)) {
+            zcu.free_references.append(gpa, idx) catch {
+                // This space will be reused eventually, so we need not propagate this error.
+                // Just leak it for now, and let GC reclaim it later on.
+                break :unit_refs;
+            };
+            idx = zcu.all_references.items[idx].next;
+        }
+    }
+
+    type_refs: {
+        const kv = zcu.type_reference_table.fetchSwapRemove(anal_unit) orelse return;
+        var idx = kv.value;
+
+        while (idx != std.math.maxInt(u32)) {
+            zcu.free_type_references.append(gpa, idx) catch {
+                // This space will be reused eventually, so we need not propagate this error.
+                // Just leak it for now, and let GC reclaim it later on.
+                break :type_refs;
+            };
+            idx = zcu.all_type_references.items[idx].next;
+        }
     }
 }
 
@@ -2696,6 +2740,29 @@ pub fn addUnitReference(zcu: *Zcu, src_unit: AnalUnit, referenced_unit: AnalUnit
     gop.value_ptr.* = @intCast(ref_idx);
 }
 
+pub fn addTypeReference(zcu: *Zcu, src_unit: AnalUnit, referenced_type: InternPool.Index, ref_src: LazySrcLoc) Allocator.Error!void {
+    const gpa = zcu.gpa;
+
+    try zcu.type_reference_table.ensureUnusedCapacity(gpa, 1);
+
+    const ref_idx = zcu.free_type_references.popOrNull() orelse idx: {
+        _ = try zcu.all_type_references.addOne(gpa);
+        break :idx zcu.all_type_references.items.len - 1;
+    };
+
+    errdefer comptime unreachable;
+
+    const gop = zcu.type_reference_table.getOrPutAssumeCapacity(src_unit);
+
+    zcu.all_type_references.items[ref_idx] = .{
+        .referenced = referenced_type,
+        .next = if (gop.found_existing) gop.value_ptr.* else std.math.maxInt(u32),
+        .src = ref_src,
+    };
+
+    gop.value_ptr.* = @intCast(ref_idx);
+}
+
 pub fn errorSetBits(mod: *Zcu) u16 {
     if (mod.error_limit == 0) return 0;
     return @as(u16, std.math.log2_int(ErrorInt, mod.error_limit)) + 1;
@@ -2990,28 +3057,175 @@ pub const ResolvedReference = struct {
 };
 
 /// Returns a mapping from an `AnalUnit` to where it is referenced.
-/// TODO: in future, this must be adapted to traverse from roots of analysis. That way, we can
-/// use the returned map to determine which units have become unreferenced in an incremental update.
-pub fn resolveReferences(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ResolvedReference) {
+/// If the value is `null`, the `AnalUnit` is a root of analysis.
+/// If an `AnalUnit` is not in the returned map, it is unreferenced.
+pub fn resolveReferences(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) {
     const gpa = zcu.gpa;
+    const comp = zcu.comp;
+    const ip = &zcu.intern_pool;
 
-    var result: std.AutoHashMapUnmanaged(AnalUnit, ResolvedReference) = .{};
+    var result: std.AutoHashMapUnmanaged(AnalUnit, ?ResolvedReference) = .{};
     errdefer result.deinit(gpa);
 
+    var checked_types: std.AutoArrayHashMapUnmanaged(InternPool.Index, void) = .{};
+    var type_queue: std.AutoArrayHashMapUnmanaged(InternPool.Index, ?ResolvedReference) = .{};
+    var unit_queue: std.AutoArrayHashMapUnmanaged(AnalUnit, ?ResolvedReference) = .{};
+    defer {
+        checked_types.deinit(gpa);
+        type_queue.deinit(gpa);
+        unit_queue.deinit(gpa);
+    }
+
     // This is not a sufficient size, but a lower bound.
     try result.ensureTotalCapacity(gpa, @intCast(zcu.reference_table.count()));
 
-    for (zcu.reference_table.keys(), zcu.reference_table.values()) |referencer, first_ref_idx| {
-        assert(first_ref_idx != std.math.maxInt(u32));
-        var ref_idx = first_ref_idx;
-        while (ref_idx != std.math.maxInt(u32)) {
-            const ref = zcu.all_references.items[ref_idx];
-            const gop = try result.getOrPut(gpa, ref.referenced);
-            if (!gop.found_existing) {
-                gop.value_ptr.* = .{ .referencer = referencer, .src = ref.src };
+    try type_queue.ensureTotalCapacity(gpa, zcu.analysis_roots.len);
+    for (zcu.analysis_roots.slice()) |mod| {
+        // Logic ripped from `Zcu.PerThread.importPkg`.
+        // TODO: this is silly, `Module` should just store a reference to its root `File`.
+        const resolved_path = try std.fs.path.resolve(gpa, &.{
+            mod.root.root_dir.path orelse ".",
+            mod.root.sub_path,
+            mod.root_src_path,
+        });
+        defer gpa.free(resolved_path);
+        const file = zcu.import_table.get(resolved_path).?;
+        if (zcu.fileByIndex(file).status != .success_zir) continue;
+        const root_ty = zcu.fileRootType(file);
+        if (root_ty == .none) continue;
+        type_queue.putAssumeCapacityNoClobber(root_ty, null);
+    }
+
+    while (true) {
+        if (type_queue.popOrNull()) |kv| {
+            const ty = kv.key;
+            const referencer = kv.value;
+            try checked_types.putNoClobber(gpa, ty, {});
+
+            // If this type has a `Cau` for resolution, it's automatically referenced.
+            const resolution_cau: InternPool.Cau.Index.Optional = switch (ip.indexToKey(ty)) {
+                .struct_type => ip.loadStructType(ty).cau,
+                .union_type => ip.loadUnionType(ty).cau.toOptional(),
+                .enum_type => ip.loadEnumType(ty).cau,
+                .opaque_type => .none,
+                else => unreachable,
+            };
+            if (resolution_cau.unwrap()) |cau| {
+                // this should only be referenced by the type
+                const unit = AnalUnit.wrap(.{ .cau = cau });
+                assert(!result.contains(unit));
+                try unit_queue.putNoClobber(gpa, unit, referencer);
+            }
+
+            // If this is a union with a generated tag, its tag type is automatically referenced.
+            // We don't add this reference for non-generated tags, as those will already be referenced via the union's `Cau`, with a better source location.
+            if (zcu.typeToUnion(Type.fromInterned(ty))) |union_obj| {
+                const tag_ty = union_obj.enum_tag_ty;
+                if (tag_ty != .none) {
+                    if (ip.indexToKey(tag_ty).enum_type == .generated_tag) {
+                        if (!checked_types.contains(tag_ty)) {
+                            try type_queue.put(gpa, tag_ty, referencer);
+                        }
+                    }
+                }
+            }
+
+            // Queue any decls within this type which would be automatically analyzed.
+            // Keep in sync with analysis queueing logic in `Zcu.PerThread.ScanDeclIter.scanDecl`.
+            const ns = Type.fromInterned(ty).getNamespace(zcu).unwrap() orelse continue;
+            for (zcu.namespacePtr(ns).other_decls.items) |cau| {
+                // These are `comptime` and `test` declarations.
+                // `comptime` decls are always analyzed; `test` declarations are analyzed depending on the test filter.
+                const inst_info = ip.getCau(cau).zir_index.resolveFull(ip) orelse continue;
+                const file = zcu.fileByIndex(inst_info.file);
+                const zir = file.zir;
+                const declaration = zir.getDeclaration(inst_info.inst)[0];
+                const want_analysis = switch (declaration.name) {
+                    .@"usingnamespace" => unreachable,
+                    .@"comptime" => true,
+                    else => a: {
+                        if (!comp.config.is_test) break :a false;
+                        if (file.mod != zcu.main_mod) break :a false;
+                        if (declaration.name.isNamedTest(zir) or declaration.name == .decltest) {
+                            const nav = ip.getCau(cau).owner.unwrap().nav;
+                            const fqn_slice = ip.getNav(nav).fqn.toSlice(ip);
+                            for (comp.test_filters) |test_filter| {
+                                if (std.mem.indexOf(u8, fqn_slice, test_filter) != null) break;
+                            } else break :a false;
+                        }
+                        break :a true;
+                    },
+                };
+                if (want_analysis) {
+                    const unit = AnalUnit.wrap(.{ .cau = cau });
+                    if (!result.contains(unit)) try unit_queue.put(gpa, unit, referencer);
+                }
+            }
+            for (zcu.namespacePtr(ns).pub_decls.keys()) |nav| {
+                // These are named declarations. They are analyzed only if marked `export`.
+                const cau = ip.getNav(nav).analysis_owner.unwrap().?;
+                const inst_info = ip.getCau(cau).zir_index.resolveFull(ip) orelse continue;
+                const declaration = zcu.fileByIndex(inst_info.file).zir.getDeclaration(inst_info.inst)[0];
+                if (declaration.flags.is_export) {
+                    const unit = AnalUnit.wrap(.{ .cau = cau });
+                    if (!result.contains(unit)) try unit_queue.put(gpa, unit, referencer);
+                }
+            }
+            for (zcu.namespacePtr(ns).priv_decls.keys()) |nav| {
+                // These are named declarations. They are analyzed only if marked `export`.
+                const cau = ip.getNav(nav).analysis_owner.unwrap().?;
+                const inst_info = ip.getCau(cau).zir_index.resolveFull(ip) orelse continue;
+                const declaration = zcu.fileByIndex(inst_info.file).zir.getDeclaration(inst_info.inst)[0];
+                if (declaration.flags.is_export) {
+                    const unit = AnalUnit.wrap(.{ .cau = cau });
+                    if (!result.contains(unit)) try unit_queue.put(gpa, unit, referencer);
+                }
+            }
+            // Incremental compilation does not support `usingnamespace`.
+            // These are only included to keep good reference traces in non-incremental updates.
+            for (zcu.namespacePtr(ns).pub_usingnamespace.items) |nav| {
+                const cau = ip.getNav(nav).analysis_owner.unwrap().?;
+                const unit = AnalUnit.wrap(.{ .cau = cau });
+                if (!result.contains(unit)) try unit_queue.put(gpa, unit, referencer);
             }
-            ref_idx = ref.next;
+            for (zcu.namespacePtr(ns).priv_usingnamespace.items) |nav| {
+                const cau = ip.getNav(nav).analysis_owner.unwrap().?;
+                const unit = AnalUnit.wrap(.{ .cau = cau });
+                if (!result.contains(unit)) try unit_queue.put(gpa, unit, referencer);
+            }
+            continue;
+        }
+        if (unit_queue.popOrNull()) |kv| {
+            const unit = kv.key;
+            try result.putNoClobber(gpa, unit, kv.value);
+
+            if (zcu.reference_table.get(unit)) |first_ref_idx| {
+                assert(first_ref_idx != std.math.maxInt(u32));
+                var ref_idx = first_ref_idx;
+                while (ref_idx != std.math.maxInt(u32)) {
+                    const ref = zcu.all_references.items[ref_idx];
+                    if (!result.contains(ref.referenced)) try unit_queue.put(gpa, ref.referenced, .{
+                        .referencer = unit,
+                        .src = ref.src,
+                    });
+                    ref_idx = ref.next;
+                }
+            }
+            if (zcu.type_reference_table.get(unit)) |first_ref_idx| {
+                assert(first_ref_idx != std.math.maxInt(u32));
+                var ref_idx = first_ref_idx;
+                while (ref_idx != std.math.maxInt(u32)) {
+                    const ref = zcu.all_type_references.items[ref_idx];
+                    if (!checked_types.contains(ref.referenced)) try type_queue.put(gpa, ref.referenced, .{
+                        .referencer = unit,
+                        .src = ref.src,
+                    });
+                    ref_idx = ref.next;
+                }
+            }
+            continue;
         }
+        break;
     }
 
     return result;