Commit 84c2ebd6c6

mlugg <mlugg@mlugg.co.uk>
2024-08-16 13:46:52
frontend: incremental compilation progress
Another big commit, sorry! This commit makes all fixes necessary for incremental updates of the compiler itself (specifically, adding a breakpoint to `zirCompileLog`) to succeed, at least on the frontend. The biggest change here is a reform to how types are handled. It works like this: * When a type is first created in `zirStructDecl` etc, its namespace is scanned. If the type requires resolution, an `interned` dependency is declared for the containing `AnalUnit`. * `zirThis` also declared an `interned` dependency for its `AnalUnit` on the namespace's owner type. * If the type's namespace changes, the surrounding source declaration changes hash, so `zirStructDecl` etc will be hit again. We check whether the namespace has been scanned this generation, and re-scan it if not. * Namespace lookups also check whether the namespace in question requires a re-scan based on the generation. This is because there's no guarantee that the `zirStructDecl` is re-analyzed before the namespace lookup is re-analyzed. * If a type's structure (essentially its fields) change, then the type's `Cau` is considered outdated. When the type is re-analyzed due to being outdated, or the `zirStructDecl` is re-analyzed by being transitively outdated, or a corresponding `zirThis` is re-analyzed by being transitively outdated, the struct type is recreated at a new `InternPool` index. The namespace's owner is updated (but not re-scanned, since that is handled by the mechanisms above), and the old type, while remaining a valid `Index`, is removed from the map metadata so it will never be found by lookups. `zirStructDecl` and `zirThis` store an `interned` dependency on the *new* type.
1 parent 65cbdef
src/Zcu/PerThread.zig
@@ -485,10 +485,7 @@ pub fn updateZirRefs(pt: Zcu.PerThread) Allocator.Error!void {
 pub fn ensureFileAnalyzed(pt: Zcu.PerThread, file_index: Zcu.File.Index) Zcu.SemaError!void {
     const file_root_type = pt.zcu.fileRootType(file_index);
     if (file_root_type != .none) {
-        // The namespace is already up-to-date thanks to the `updateFileNamespace` calls at the
-        // start of this update. We just have to check whether the type itself is okay!
-        const file_root_type_cau = pt.zcu.intern_pool.loadStructType(file_root_type).cau.unwrap().?;
-        return pt.ensureCauAnalyzed(file_root_type_cau);
+        _ = try pt.ensureTypeUpToDate(file_root_type, false);
     } else {
         return pt.semaFile(file_index);
     }
@@ -505,10 +502,10 @@ pub fn ensureCauAnalyzed(pt: Zcu.PerThread, cau_index: InternPool.Cau.Index) Zcu
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
 
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .cau = cau_index });
+    const anal_unit = AnalUnit.wrap(.{ .cau = cau_index });
     const cau = ip.getCau(cau_index);
 
-    //log.debug("ensureCauAnalyzed {d}", .{@intFromEnum(cau_index)});
+    log.debug("ensureCauAnalyzed {d}", .{@intFromEnum(cau_index)});
 
     assert(!zcu.analysis_in_progress.contains(anal_unit));
 
@@ -552,10 +549,12 @@ pub fn ensureCauAnalyzed(pt: Zcu.PerThread, cau_index: InternPool.Cau.Index) Zcu
                 // Since it does not, this must be a transitive failure.
                 try zcu.transitive_failed_analysis.put(gpa, anal_unit, {});
             }
-            // We treat errors as up-to-date, since those uses would just trigger a transitive error
+            // We treat errors as up-to-date, since those uses would just trigger a transitive error.
+            // The exception is types, since type declarations may require re-analysis if the type, e.g. its captures, changed.
+            const outdated = cau.owner.unwrap() == .type;
             break :res .{ .{
-                .invalidate_decl_val = false,
-                .invalidate_decl_ref = false,
+                .invalidate_decl_val = outdated,
+                .invalidate_decl_ref = outdated,
             }, true };
         },
         error.OutOfMemory => res: {
@@ -610,7 +609,7 @@ fn ensureCauAnalyzedInner(
     const ip = &zcu.intern_pool;
 
     const cau = ip.getCau(cau_index);
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .cau = cau_index });
+    const anal_unit = AnalUnit.wrap(.{ .cau = cau_index });
 
     const inst_info = cau.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
 
@@ -626,7 +625,6 @@ fn ensureCauAnalyzedInner(
     // * so, it uses the same `struct`
     // * but this doesn't stop it from updating the namespace!
     //   * we basically do `scanDecls`, updating the namespace as needed
-    //   * TODO: optimize this to make sure we only do it once a generation i guess?
     // * so everyone lived happily ever after
 
     if (zcu.fileByIndex(inst_info.file).status != .success_zir) {
@@ -646,17 +644,6 @@ fn ensureCauAnalyzedInner(
         _ = zcu.transitive_failed_analysis.swapRemove(anal_unit);
     }
 
-    if (inst_info.inst == .main_struct_inst) {
-        // Note that this is definitely a *recreation* due to outdated, because
-        // this instruction indicates that `cau.owner` is a `type`, which only
-        // reaches here if `cau_outdated`.
-        try pt.recreateFileRoot(inst_info.file);
-        return .{
-            .invalidate_decl_val = true,
-            .invalidate_decl_ref = true,
-        };
-    }
-
     const decl_prog_node = zcu.sema_prog_node.start(switch (cau.owner.unwrap()) {
         .nav => |nav| ip.getNav(nav).fqn.toSlice(ip),
         .type => |ty| Type.fromInterned(ty).containerTypeName(ip).toSlice(ip),
@@ -685,9 +672,9 @@ pub fn ensureFuncBodyAnalyzed(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
 
     const func = zcu.funcInfo(maybe_coerced_func_index);
 
-    //log.debug("ensureFuncBodyAnalyzed {d}", .{@intFromEnum(func_index)});
+    log.debug("ensureFuncBodyAnalyzed {d}", .{@intFromEnum(func_index)});
 
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .func = func_index });
+    const anal_unit = AnalUnit.wrap(.{ .func = func_index });
     const func_outdated = zcu.outdated.swapRemove(anal_unit) or
         zcu.potentially_outdated.swapRemove(anal_unit);
 
@@ -742,7 +729,7 @@ fn ensureFuncBodyAnalyzedInner(
     const ip = &zcu.intern_pool;
 
     const func = zcu.funcInfo(func_index);
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .func = func_index });
+    const anal_unit = AnalUnit.wrap(.{ .func = func_index });
 
     // Here's an interesting question: is this function actually valid?
     // Maybe the signature changed, so we'll end up creating a whole different `func`
@@ -766,7 +753,7 @@ fn ensureFuncBodyAnalyzedInner(
         if (func_outdated) {
             try zcu.markDependeeOutdated(.marked_po, .{ .interned = func_index }); // IES
         }
-        ip.removeDependenciesForDepender(gpa, InternPool.AnalUnit.wrap(.{ .func = func_index }));
+        ip.removeDependenciesForDepender(gpa, AnalUnit.wrap(.{ .func = func_index }));
         ip.remove(pt.tid, func_index);
         @panic("TODO: remove orphaned function from binary");
     }
@@ -901,7 +888,7 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: Ai
                     "unable to codegen: {s}",
                     .{@errorName(err)},
                 ));
-                try zcu.retryable_failures.append(zcu.gpa, InternPool.AnalUnit.wrap(.{ .func = func_index }));
+                try zcu.retryable_failures.append(zcu.gpa, AnalUnit.wrap(.{ .func = func_index }));
             },
         };
     } else if (zcu.llvm_object) |llvm_object| {
@@ -982,7 +969,7 @@ fn createFileRootStruct(
     if (zcu.comp.incremental) {
         try ip.addDependency(
             gpa,
-            InternPool.AnalUnit.wrap(.{ .cau = new_cau_index }),
+            AnalUnit.wrap(.{ .cau = new_cau_index }),
             .{ .src_hash = tracked_inst },
         );
     }
@@ -998,35 +985,6 @@ fn createFileRootStruct(
     return wip_ty.finish(ip, new_cau_index.toOptional(), namespace_index);
 }
 
-/// Recreate the root type of a file after it becomes outdated. A new struct type
-/// is constructed at a new InternPool index, reusing the namespace for efficiency.
-fn recreateFileRoot(pt: Zcu.PerThread, file_index: Zcu.File.Index) Zcu.SemaError!void {
-    const zcu = pt.zcu;
-    const ip = &zcu.intern_pool;
-    const file = zcu.fileByIndex(file_index);
-    const file_root_type = zcu.fileRootType(file_index);
-    const namespace_index = Type.fromInterned(file_root_type).getNamespaceIndex(zcu);
-
-    assert(file_root_type != .none);
-
-    log.debug("recreateFileRoot mod={s} sub_file_path={s}", .{
-        file.mod.fully_qualified_name,
-        file.sub_file_path,
-    });
-
-    if (file.status != .success_zir) {
-        return error.AnalysisFail;
-    }
-
-    // Invalidate the existing type, reusing its namespace.
-    const file_root_type_cau = ip.loadStructType(file_root_type).cau.unwrap().?;
-    ip.removeDependenciesForDepender(
-        zcu.gpa,
-        InternPool.AnalUnit.wrap(.{ .cau = file_root_type_cau }),
-    );
-    _ = try pt.createFileRootStruct(file_index, namespace_index, true);
-}
-
 /// Re-scan the namespace of a file's root struct type on an incremental update.
 /// The file must have successfully populated ZIR.
 /// If the file's root struct type is not populated (the file is unreferenced), nothing is done.
@@ -1060,6 +1018,7 @@ fn updateFileNamespace(pt: Zcu.PerThread, file_index: Zcu.File.Index) Allocator.
         break :decls file.zir.bodySlice(extra_index, decls_len);
     };
     try pt.scanNamespace(namespace_index, decls);
+    zcu.namespacePtr(namespace_index).generation = zcu.generation;
 }
 
 fn semaFile(pt: Zcu.PerThread, file_index: Zcu.File.Index) Zcu.SemaError!void {
@@ -1080,6 +1039,7 @@ fn semaFile(pt: Zcu.PerThread, file_index: Zcu.File.Index) Zcu.SemaError!void {
         .parent = .none,
         .owner_type = undefined, // set in `createFileRootStruct`
         .file_scope = file_index,
+        .generation = zcu.generation,
     });
     const struct_ty = try pt.createFileRootStruct(file_index, new_namespace_index, false);
     errdefer zcu.intern_pool.remove(pt.tid, struct_ty);
@@ -1131,7 +1091,7 @@ fn semaCau(pt: Zcu.PerThread, cau_index: InternPool.Cau.Index) !SemaCauResult {
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
 
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .cau = cau_index });
+    const anal_unit = AnalUnit.wrap(.{ .cau = cau_index });
 
     const cau = ip.getCau(cau_index);
     const inst_info = cau.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
@@ -1151,10 +1111,12 @@ fn semaCau(pt: Zcu.PerThread, cau_index: InternPool.Cau.Index) !SemaCauResult {
             // This declaration has no value so is definitely not a std.builtin type.
             break :ip_index .none;
         },
-        .type => {
+        .type => |ty| {
             // This is an incremental update, and this type is being re-analyzed because it is outdated.
-            // The type must be recreated at a new `InternPool.Index`.
-            // Mark it outdated so that creation sites are re-analyzed.
+            // Create a new type in its place, and mark the old one as outdated so that use sites will
+            // be re-analyzed and discover an up-to-date type.
+            const new_ty = try pt.ensureTypeUpToDate(ty, true);
+            assert(new_ty != ty);
             return .{
                 .invalidate_decl_val = true,
                 .invalidate_decl_ref = true,
@@ -2002,21 +1964,23 @@ const ScanDeclIter = struct {
 
                 try namespace.other_decls.append(gpa, cau);
 
-                // For a `comptime` declaration, whether to re-analyze is based solely on whether the
-                // `Cau` is outdated. So, add this one to `outdated` and `outdated_ready` if not already.
-                const unit = InternPool.AnalUnit.wrap(.{ .cau = cau });
-                if (zcu.potentially_outdated.fetchSwapRemove(unit)) |kv| {
-                    try zcu.outdated.ensureUnusedCapacity(gpa, 1);
-                    try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
-                    zcu.outdated.putAssumeCapacityNoClobber(unit, kv.value);
-                    if (kv.value == 0) { // no PO deps
+                if (existing_cau == null) {
+                    // For a `comptime` declaration, whether to analyze is based solely on whether the
+                    // `Cau` is outdated. So, add this one to `outdated` and `outdated_ready` if not already.
+                    const unit = AnalUnit.wrap(.{ .cau = cau });
+                    if (zcu.potentially_outdated.fetchSwapRemove(unit)) |kv| {
+                        try zcu.outdated.ensureUnusedCapacity(gpa, 1);
+                        try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
+                        zcu.outdated.putAssumeCapacityNoClobber(unit, kv.value);
+                        if (kv.value == 0) { // no PO deps
+                            zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
+                        }
+                    } else if (!zcu.outdated.contains(unit)) {
+                        try zcu.outdated.ensureUnusedCapacity(gpa, 1);
+                        try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
+                        zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
                         zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
                     }
-                } else if (!zcu.outdated.contains(unit)) {
-                    try zcu.outdated.ensureUnusedCapacity(gpa, 1);
-                    try zcu.outdated_ready.ensureUnusedCapacity(gpa, 1);
-                    zcu.outdated.putAssumeCapacityNoClobber(unit, 0);
-                    zcu.outdated_ready.putAssumeCapacityNoClobber(unit, {});
                 }
 
                 break :cau .{ cau, true };
@@ -2027,9 +1991,6 @@ const ScanDeclIter = struct {
                 const cau, const nav = if (existing_cau) |cau_index| cau_nav: {
                     const nav_index = ip.getCau(cau_index).owner.unwrap().nav;
                     const nav = ip.getNav(nav_index);
-                    if (nav.name != name) {
-                        std.debug.panic("'{}' vs '{}'", .{ nav.name.fmt(ip), name.fmt(ip) });
-                    }
                     assert(nav.name == name);
                     assert(nav.fqn == fqn);
                     break :cau_nav .{ cau_index, nav_index };
@@ -2078,7 +2039,7 @@ const ScanDeclIter = struct {
             },
         };
 
-        if (want_analysis or declaration.flags.is_export) {
+        if (existing_cau == null and (want_analysis or declaration.flags.is_export)) {
             log.debug(
                 "scanDecl queue analyze_cau file='{s}' cau_index={d}",
                 .{ namespace.fileScope(zcu).sub_file_path, cau },
@@ -2098,7 +2059,7 @@ fn analyzeFnBody(pt: Zcu.PerThread, func_index: InternPool.Index) Zcu.SemaError!
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
 
-    const anal_unit = InternPool.AnalUnit.wrap(.{ .func = func_index });
+    const anal_unit = AnalUnit.wrap(.{ .func = func_index });
     const func = zcu.funcInfo(func_index);
     const inst_info = func.zir_body_inst.resolveFull(ip) orelse return error.AnalysisFail;
     const file = zcu.fileByIndex(inst_info.file);
@@ -2484,7 +2445,7 @@ fn processExportsInner(
             const nav = ip.getNav(nav_index);
             if (zcu.failed_codegen.contains(nav_index)) break :failed true;
             if (nav.analysis_owner.unwrap()) |cau| {
-                const cau_unit = InternPool.AnalUnit.wrap(.{ .cau = cau });
+                const cau_unit = AnalUnit.wrap(.{ .cau = cau });
                 if (zcu.failed_analysis.contains(cau_unit)) break :failed true;
                 if (zcu.transitive_failed_analysis.contains(cau_unit)) break :failed true;
             }
@@ -2494,7 +2455,7 @@ fn processExportsInner(
             };
             // If the value is a function, we also need to check if that function succeeded analysis.
             if (val.typeOf(zcu).zigTypeTag(zcu) == .Fn) {
-                const func_unit = InternPool.AnalUnit.wrap(.{ .func = val.toIntern() });
+                const func_unit = AnalUnit.wrap(.{ .func = val.toIntern() });
                 if (zcu.failed_analysis.contains(func_unit)) break :failed true;
                 if (zcu.transitive_failed_analysis.contains(func_unit)) break :failed true;
             }
@@ -2669,7 +2630,7 @@ pub fn linkerUpdateNav(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) !void
                     .{@errorName(err)},
                 ));
                 if (nav.analysis_owner.unwrap()) |cau| {
-                    try zcu.retryable_failures.append(zcu.gpa, InternPool.AnalUnit.wrap(.{ .cau = cau }));
+                    try zcu.retryable_failures.append(zcu.gpa, AnalUnit.wrap(.{ .cau = cau }));
                 } else {
                     // TODO: we don't have a way to indicate that this failure is retryable!
                     // Since these are really rare, we could as a cop-out retry the whole build next update.
@@ -2782,7 +2743,7 @@ pub fn reportRetryableFileError(
     gop.value_ptr.* = err_msg;
 }
 
-/// Shortcut for calling `intern_pool.get`.
+///Shortcut for calling `intern_pool.get`.
 pub fn intern(pt: Zcu.PerThread, key: InternPool.Key) Allocator.Error!InternPool.Index {
     return pt.zcu.intern_pool.get(pt.zcu.gpa, pt.tid, key);
 }
@@ -3367,6 +3328,532 @@ pub fn navAlignment(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) InternPo
     return Value.fromInterned(r.val).typeOf(zcu).abiAlignment(pt);
 }
 
+/// Given a container type requiring resolution, ensures that it is up-to-date.
+/// If not, the type is recreated at a new `InternPool.Index`.
+/// The new index is returned. This is the same as the old index if the fields were up-to-date.
+/// If `already_updating` is set, assumes the type is already outdated and undergoing re-analysis rather than checking `zcu.outdated`.
+pub fn ensureTypeUpToDate(pt: Zcu.PerThread, ty: InternPool.Index, already_updating: bool) Zcu.SemaError!InternPool.Index {
+    const zcu = pt.zcu;
+    const ip = &zcu.intern_pool;
+    switch (ip.indexToKey(ty)) {
+        .struct_type => |key| {
+            const struct_obj = ip.loadStructType(ty);
+            const outdated = already_updating or o: {
+                const anal_unit = AnalUnit.wrap(.{ .cau = struct_obj.cau.unwrap().? });
+                const o = zcu.outdated.swapRemove(anal_unit) or
+                    zcu.potentially_outdated.swapRemove(anal_unit);
+                if (o) {
+                    _ = zcu.outdated_ready.swapRemove(anal_unit);
+                    try zcu.markDependeeOutdated(.marked_po, .{ .interned = ty });
+                }
+                break :o o;
+            };
+            if (!outdated) return ty;
+            return pt.recreateStructType(ty, key, struct_obj);
+        },
+        .union_type => |key| {
+            const union_obj = ip.loadUnionType(ty);
+            const outdated = already_updating or o: {
+                const anal_unit = AnalUnit.wrap(.{ .cau = union_obj.cau });
+                const o = zcu.outdated.swapRemove(anal_unit) or
+                    zcu.potentially_outdated.swapRemove(anal_unit);
+                if (o) {
+                    _ = zcu.outdated_ready.swapRemove(anal_unit);
+                    try zcu.markDependeeOutdated(.marked_po, .{ .interned = ty });
+                }
+                break :o o;
+            };
+            if (!outdated) return ty;
+            return pt.recreateUnionType(ty, key, union_obj);
+        },
+        .enum_type => |key| {
+            const enum_obj = ip.loadEnumType(ty);
+            const outdated = already_updating or o: {
+                const anal_unit = AnalUnit.wrap(.{ .cau = enum_obj.cau.unwrap().? });
+                const o = zcu.outdated.swapRemove(anal_unit) or
+                    zcu.potentially_outdated.swapRemove(anal_unit);
+                if (o) {
+                    _ = zcu.outdated_ready.swapRemove(anal_unit);
+                    try zcu.markDependeeOutdated(.marked_po, .{ .interned = ty });
+                }
+                break :o o;
+            };
+            if (!outdated) return ty;
+            return pt.recreateEnumType(ty, key, enum_obj);
+        },
+        .opaque_type => {
+            assert(!already_updating);
+            return ty;
+        },
+        else => unreachable,
+    }
+}
+
+fn recreateStructType(
+    pt: Zcu.PerThread,
+    ty: InternPool.Index,
+    full_key: InternPool.Key.NamespaceType,
+    struct_obj: InternPool.LoadedStructType,
+) Zcu.SemaError!InternPool.Index {
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const key = switch (full_key) {
+        .reified => unreachable, // never outdated
+        .empty_struct => unreachable, // never outdated
+        .generated_tag => unreachable, // not a struct
+        .declared => |d| d,
+    };
+
+    if (@intFromEnum(ty) <= InternPool.static_len) {
+        @panic("TODO: recreate resolved builtin type");
+    }
+
+    const inst_info = key.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
+    const file = zcu.fileByIndex(inst_info.file);
+    if (file.status != .success_zir) return error.AnalysisFail;
+    const zir = file.zir;
+
+    assert(zir.instructions.items(.tag)[@intFromEnum(inst_info.inst)] == .extended);
+    const extended = zir.instructions.items(.data)[@intFromEnum(inst_info.inst)].extended;
+    assert(extended.opcode == .struct_decl);
+    const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
+    const extra = zir.extraData(Zir.Inst.StructDecl, extended.operand);
+    var extra_index = extra.end;
+
+    const captures_len = if (small.has_captures_len) blk: {
+        const captures_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk captures_len;
+    } else 0;
+    const fields_len = if (small.has_fields_len) blk: {
+        const fields_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk fields_len;
+    } else 0;
+
+    if (captures_len != key.captures.owned.len) return error.AnalysisFail;
+    if (fields_len != struct_obj.field_types.len) return error.AnalysisFail;
+
+    // The old type will be unused, so drop its dependency information.
+    ip.removeDependenciesForDepender(gpa, AnalUnit.wrap(.{ .cau = struct_obj.cau.unwrap().? }));
+
+    const namespace_index = struct_obj.namespace.unwrap().?;
+
+    const wip_ty = switch (try ip.getStructType(gpa, pt.tid, .{
+        .layout = small.layout,
+        .fields_len = fields_len,
+        .known_non_opv = small.known_non_opv,
+        .requires_comptime = if (small.known_comptime_only) .yes else .unknown,
+        .is_tuple = small.is_tuple,
+        .any_comptime_fields = small.any_comptime_fields,
+        .any_default_inits = small.any_default_inits,
+        .inits_resolved = false,
+        .any_aligned_fields = small.any_aligned_fields,
+        .key = .{ .declared_owned_captures = .{
+            .zir_index = key.zir_index,
+            .captures = key.captures.owned,
+        } },
+    }, true)) {
+        .wip => |wip| wip,
+        .existing => unreachable, // we passed `replace_existing`
+    };
+    errdefer wip_ty.cancel(ip, pt.tid);
+
+    wip_ty.setName(ip, struct_obj.name);
+    const new_cau_index = try ip.createTypeCau(gpa, pt.tid, key.zir_index, namespace_index, wip_ty.index);
+    try ip.addDependency(
+        gpa,
+        AnalUnit.wrap(.{ .cau = new_cau_index }),
+        .{ .src_hash = key.zir_index },
+    );
+    zcu.namespacePtr(namespace_index).owner_type = wip_ty.index;
+    // No need to re-scan the namespace -- `zirStructDecl` will ultimately do that if the type is still alive.
+    try zcu.comp.queueJob(.{ .resolve_type_fully = wip_ty.index });
+
+    const new_ty = wip_ty.finish(ip, new_cau_index.toOptional(), namespace_index);
+    if (inst_info.inst == .main_struct_inst) {
+        // This is the root type of a file! Update the reference.
+        zcu.setFileRootType(inst_info.file, new_ty);
+    }
+    return new_ty;
+}
+
+fn recreateUnionType(
+    pt: Zcu.PerThread,
+    ty: InternPool.Index,
+    full_key: InternPool.Key.NamespaceType,
+    union_obj: InternPool.LoadedUnionType,
+) Zcu.SemaError!InternPool.Index {
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const key = switch (full_key) {
+        .reified => unreachable, // never outdated
+        .empty_struct => unreachable, // never outdated
+        .generated_tag => unreachable, // not a union
+        .declared => |d| d,
+    };
+
+    if (@intFromEnum(ty) <= InternPool.static_len) {
+        @panic("TODO: recreate resolved builtin type");
+    }
+
+    const inst_info = key.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
+    const file = zcu.fileByIndex(inst_info.file);
+    if (file.status != .success_zir) return error.AnalysisFail;
+    const zir = file.zir;
+
+    assert(zir.instructions.items(.tag)[@intFromEnum(inst_info.inst)] == .extended);
+    const extended = zir.instructions.items(.data)[@intFromEnum(inst_info.inst)].extended;
+    assert(extended.opcode == .union_decl);
+    const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
+    const extra = zir.extraData(Zir.Inst.UnionDecl, extended.operand);
+    var extra_index = extra.end;
+
+    extra_index += @intFromBool(small.has_tag_type);
+    const captures_len = if (small.has_captures_len) blk: {
+        const captures_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk captures_len;
+    } else 0;
+    extra_index += @intFromBool(small.has_body_len);
+    const fields_len = if (small.has_fields_len) blk: {
+        const fields_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk fields_len;
+    } else 0;
+
+    if (captures_len != key.captures.owned.len) return error.AnalysisFail;
+    if (fields_len != union_obj.field_types.len) return error.AnalysisFail;
+
+    // The old type will be unused, so drop its dependency information.
+    ip.removeDependenciesForDepender(gpa, AnalUnit.wrap(.{ .cau = union_obj.cau }));
+
+    const namespace_index = union_obj.namespace;
+
+    const wip_ty = switch (try ip.getUnionType(gpa, pt.tid, .{
+        .flags = .{
+            .layout = small.layout,
+            .status = .none,
+            .runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
+                .tagged
+            else if (small.layout != .auto)
+                .none
+            else switch (true) { // TODO
+                true => .safety,
+                false => .none,
+            },
+            .any_aligned_fields = small.any_aligned_fields,
+            .requires_comptime = .unknown,
+            .assumed_runtime_bits = false,
+            .assumed_pointer_aligned = false,
+            .alignment = .none,
+        },
+        .fields_len = fields_len,
+        .enum_tag_ty = .none, // set later
+        .field_types = &.{}, // set later
+        .field_aligns = &.{}, // set later
+        .key = .{ .declared_owned_captures = .{
+            .zir_index = key.zir_index,
+            .captures = key.captures.owned,
+        } },
+    }, true)) {
+        .wip => |wip| wip,
+        .existing => unreachable, // we passed `replace_existing`
+    };
+    errdefer wip_ty.cancel(ip, pt.tid);
+
+    wip_ty.setName(ip, union_obj.name);
+    const new_cau_index = try ip.createTypeCau(gpa, pt.tid, key.zir_index, namespace_index, wip_ty.index);
+    try ip.addDependency(
+        gpa,
+        AnalUnit.wrap(.{ .cau = new_cau_index }),
+        .{ .src_hash = key.zir_index },
+    );
+    zcu.namespacePtr(namespace_index).owner_type = wip_ty.index;
+    // No need to re-scan the namespace -- `zirUnionDecl` will ultimately do that if the type is still alive.
+    try zcu.comp.queueJob(.{ .resolve_type_fully = wip_ty.index });
+    return wip_ty.finish(ip, new_cau_index.toOptional(), namespace_index);
+}
+
+fn recreateEnumType(
+    pt: Zcu.PerThread,
+    ty: InternPool.Index,
+    full_key: InternPool.Key.NamespaceType,
+    enum_obj: InternPool.LoadedEnumType,
+) Zcu.SemaError!InternPool.Index {
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const key = switch (full_key) {
+        .reified => unreachable, // never outdated
+        .empty_struct => unreachable, // never outdated
+        .generated_tag => unreachable, // never outdated
+        .declared => |d| d,
+    };
+
+    if (@intFromEnum(ty) <= InternPool.static_len) {
+        @panic("TODO: recreate resolved builtin type");
+    }
+
+    const inst_info = key.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
+    const file = zcu.fileByIndex(inst_info.file);
+    if (file.status != .success_zir) return error.AnalysisFail;
+    const zir = file.zir;
+
+    assert(zir.instructions.items(.tag)[@intFromEnum(inst_info.inst)] == .extended);
+    const extended = zir.instructions.items(.data)[@intFromEnum(inst_info.inst)].extended;
+    assert(extended.opcode == .enum_decl);
+    const small: Zir.Inst.EnumDecl.Small = @bitCast(extended.small);
+    const extra = zir.extraData(Zir.Inst.EnumDecl, extended.operand);
+    var extra_index = extra.end;
+
+    const tag_type_ref = if (small.has_tag_type) blk: {
+        const tag_type_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
+        extra_index += 1;
+        break :blk tag_type_ref;
+    } else .none;
+
+    const captures_len = if (small.has_captures_len) blk: {
+        const captures_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk captures_len;
+    } else 0;
+
+    const body_len = if (small.has_body_len) blk: {
+        const body_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk body_len;
+    } else 0;
+
+    const fields_len = if (small.has_fields_len) blk: {
+        const fields_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk fields_len;
+    } else 0;
+
+    const decls_len = if (small.has_decls_len) blk: {
+        const decls_len = zir.extra[extra_index];
+        extra_index += 1;
+        break :blk decls_len;
+    } else 0;
+
+    if (captures_len != key.captures.owned.len) return error.AnalysisFail;
+    if (fields_len != enum_obj.names.len) return error.AnalysisFail;
+
+    extra_index += captures_len;
+    extra_index += decls_len;
+
+    const body = zir.bodySlice(extra_index, body_len);
+    extra_index += body.len;
+
+    const bit_bags_count = std.math.divCeil(usize, fields_len, 32) catch unreachable;
+    const body_end = extra_index;
+    extra_index += bit_bags_count;
+
+    const any_values = for (zir.extra[body_end..][0..bit_bags_count]) |bag| {
+        if (bag != 0) break true;
+    } else false;
+
+    // The old type will be unused, so drop its dependency information.
+    ip.removeDependenciesForDepender(gpa, AnalUnit.wrap(.{ .cau = enum_obj.cau.unwrap().? }));
+
+    const namespace_index = enum_obj.namespace;
+
+    const wip_ty = switch (try ip.getEnumType(gpa, pt.tid, .{
+        .has_values = any_values,
+        .tag_mode = if (small.nonexhaustive)
+            .nonexhaustive
+        else if (tag_type_ref == .none)
+            .auto
+        else
+            .explicit,
+        .fields_len = fields_len,
+        .key = .{ .declared_owned_captures = .{
+            .zir_index = key.zir_index,
+            .captures = key.captures.owned,
+        } },
+    }, true)) {
+        .wip => |wip| wip,
+        .existing => unreachable, // we passed `replace_existing`
+    };
+    var done = true;
+    errdefer if (!done) wip_ty.cancel(ip, pt.tid);
+
+    wip_ty.setName(ip, enum_obj.name);
+
+    const new_cau_index = try ip.createTypeCau(gpa, pt.tid, key.zir_index, namespace_index, wip_ty.index);
+
+    zcu.namespacePtr(namespace_index).owner_type = wip_ty.index;
+    // No need to re-scan the namespace -- `zirEnumDecl` will ultimately do that if the type is still alive.
+
+    wip_ty.prepare(ip, new_cau_index, namespace_index);
+    done = true;
+
+    Sema.resolveDeclaredEnum(
+        pt,
+        wip_ty,
+        inst_info.inst,
+        key.zir_index,
+        namespace_index,
+        enum_obj.name,
+        new_cau_index,
+        small,
+        body,
+        tag_type_ref,
+        any_values,
+        fields_len,
+        zir,
+        body_end,
+    ) catch |err| switch (err) {
+        error.GenericPoison => unreachable,
+        error.ComptimeBreak => unreachable,
+        error.ComptimeReturn => unreachable,
+        error.AnalysisFail, error.OutOfMemory => |e| return e,
+    };
+
+    return wip_ty.index;
+}
+
+/// Given a namespace, re-scan its declarations from the type definition if they have not
+/// yet been re-scanned on this update.
+/// If the type declaration instruction has been lost, returns `error.AnalysisFail`.
+/// This will effectively short-circuit the caller, which will be semantic analysis of a
+/// guaranteed-unreferenced `AnalUnit`, to trigger a transitive analysis error.
+pub fn ensureNamespaceUpToDate(pt: Zcu.PerThread, namespace_index: Zcu.Namespace.Index) Zcu.SemaError!void {
+    const zcu = pt.zcu;
+    const ip = &zcu.intern_pool;
+    const namespace = zcu.namespacePtr(namespace_index);
+
+    if (namespace.generation == zcu.generation) return;
+
+    const Container = enum { @"struct", @"union", @"enum", @"opaque" };
+    const container: Container, const full_key = switch (ip.indexToKey(namespace.owner_type)) {
+        .struct_type => |k| .{ .@"struct", k },
+        .union_type => |k| .{ .@"union", k },
+        .enum_type => |k| .{ .@"enum", k },
+        .opaque_type => |k| .{ .@"opaque", k },
+        else => unreachable, // namespaces are owned by a container type
+    };
+
+    const key = switch (full_key) {
+        .reified, .empty_struct, .generated_tag => {
+            // Namespace always empty, so up-to-date.
+            namespace.generation = zcu.generation;
+            return;
+        },
+        .declared => |d| d,
+    };
+
+    // Namespace outdated -- re-scan the type if necessary.
+
+    const inst_info = key.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
+    const file = zcu.fileByIndex(inst_info.file);
+    if (file.status != .success_zir) return error.AnalysisFail;
+    const zir = file.zir;
+
+    assert(zir.instructions.items(.tag)[@intFromEnum(inst_info.inst)] == .extended);
+    const extended = zir.instructions.items(.data)[@intFromEnum(inst_info.inst)].extended;
+
+    const decls = switch (container) {
+        .@"struct" => decls: {
+            assert(extended.opcode == .struct_decl);
+            const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
+            const extra = zir.extraData(Zir.Inst.StructDecl, extended.operand);
+            var extra_index = extra.end;
+            const captures_len = if (small.has_captures_len) blk: {
+                const captures_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk captures_len;
+            } else 0;
+            extra_index += @intFromBool(small.has_fields_len);
+            const decls_len = if (small.has_decls_len) blk: {
+                const decls_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk decls_len;
+            } else 0;
+            extra_index += captures_len;
+            if (small.has_backing_int) {
+                const backing_int_body_len = zir.extra[extra_index];
+                extra_index += 1; // backing_int_body_len
+                if (backing_int_body_len == 0) {
+                    extra_index += 1; // backing_int_ref
+                } else {
+                    extra_index += backing_int_body_len; // backing_int_body_inst
+                }
+            }
+            break :decls zir.bodySlice(extra_index, decls_len);
+        },
+        .@"union" => decls: {
+            assert(extended.opcode == .union_decl);
+            const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
+            const extra = zir.extraData(Zir.Inst.UnionDecl, extended.operand);
+            var extra_index = extra.end;
+            extra_index += @intFromBool(small.has_tag_type);
+            const captures_len = if (small.has_captures_len) blk: {
+                const captures_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk captures_len;
+            } else 0;
+            extra_index += @intFromBool(small.has_body_len);
+            extra_index += @intFromBool(small.has_fields_len);
+            const decls_len = if (small.has_decls_len) blk: {
+                const decls_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk decls_len;
+            } else 0;
+            extra_index += captures_len;
+            break :decls zir.bodySlice(extra_index, decls_len);
+        },
+        .@"enum" => decls: {
+            assert(extended.opcode == .enum_decl);
+            const small: Zir.Inst.EnumDecl.Small = @bitCast(extended.small);
+            const extra = zir.extraData(Zir.Inst.EnumDecl, extended.operand);
+            var extra_index = extra.end;
+            extra_index += @intFromBool(small.has_tag_type);
+            const captures_len = if (small.has_captures_len) blk: {
+                const captures_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk captures_len;
+            } else 0;
+            extra_index += @intFromBool(small.has_body_len);
+            extra_index += @intFromBool(small.has_fields_len);
+            const decls_len = if (small.has_decls_len) blk: {
+                const decls_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk decls_len;
+            } else 0;
+            extra_index += captures_len;
+            break :decls zir.bodySlice(extra_index, decls_len);
+        },
+        .@"opaque" => decls: {
+            assert(extended.opcode == .opaque_decl);
+            const small: Zir.Inst.OpaqueDecl.Small = @bitCast(extended.small);
+            const extra = zir.extraData(Zir.Inst.OpaqueDecl, extended.operand);
+            var extra_index = extra.end;
+            const captures_len = if (small.has_captures_len) blk: {
+                const captures_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk captures_len;
+            } else 0;
+            const decls_len = if (small.has_decls_len) blk: {
+                const decls_len = zir.extra[extra_index];
+                extra_index += 1;
+                break :blk decls_len;
+            } else 0;
+            extra_index += captures_len;
+            break :decls zir.bodySlice(extra_index, decls_len);
+        },
+    };
+
+    try pt.scanNamespace(namespace_index, decls);
+    namespace.generation = zcu.generation;
+}
+
 const Air = @import("../Air.zig");
 const Allocator = std.mem.Allocator;
 const assert = std.debug.assert;
@@ -3379,6 +3866,7 @@ const builtin = @import("builtin");
 const Cache = std.Build.Cache;
 const dev = @import("../dev.zig");
 const InternPool = @import("../InternPool.zig");
+const AnalUnit = InternPool.AnalUnit;
 const isUpDir = @import("../introspect.zig").isUpDir;
 const Liveness = @import("../Liveness.zig");
 const log = std.log.scoped(.zcu);
src/Compilation.zig
@@ -3569,6 +3569,8 @@ pub fn performAllTheWork(
         mod.sema_prog_node = std.Progress.Node.none;
         mod.codegen_prog_node.end();
         mod.codegen_prog_node = std.Progress.Node.none;
+
+        mod.generation += 1;
     };
     try comp.performAllTheWorkInner(main_progress_node);
     if (!InternPool.single_threaded) if (comp.codegen_work.job_error) |job_error| return job_error;
src/InternPool.zig
@@ -684,10 +684,6 @@ pub fn dependencyIterator(ip: *const InternPool, dependee: Dependee) DependencyI
         .ip = ip,
         .next_entry = .none,
     };
-    if (ip.dep_entries.items[@intFromEnum(first_entry)].depender == .none) return .{
-        .ip = ip,
-        .next_entry = .none,
-    };
     return .{
         .ip = ip,
         .next_entry = first_entry.toOptional(),
@@ -724,7 +720,6 @@ pub fn addDependency(ip: *InternPool, gpa: Allocator, depender: AnalUnit, depend
 
             if (gop.found_existing and ip.dep_entries.items[@intFromEnum(gop.value_ptr.*)].depender == .none) {
                 // Dummy entry, so we can reuse it rather than allocating a new one!
-                ip.dep_entries.items[@intFromEnum(gop.value_ptr.*)].next = .none;
                 break :new_index gop.value_ptr.*;
             }
 
@@ -732,7 +727,12 @@ pub fn addDependency(ip: *InternPool, gpa: Allocator, depender: AnalUnit, depend
             const new_index: DepEntry.Index, const ptr = if (ip.free_dep_entries.popOrNull()) |new_index| new: {
                 break :new .{ new_index, &ip.dep_entries.items[@intFromEnum(new_index)] };
             } else .{ @enumFromInt(ip.dep_entries.items.len), ip.dep_entries.addOneAssumeCapacity() };
-            ptr.next = if (gop.found_existing) gop.value_ptr.*.toOptional() else .none;
+            if (gop.found_existing) {
+                ptr.next = gop.value_ptr.*.toOptional();
+                ip.dep_entries.items[@intFromEnum(gop.value_ptr.*)].prev = new_index.toOptional();
+            } else {
+                ptr.next = .none;
+            }
             gop.value_ptr.* = new_index;
             break :new_index new_index;
         },
@@ -754,10 +754,9 @@ pub const NamespaceNameKey = struct {
 };
 
 pub const DepEntry = extern struct {
-    /// If null, this is a dummy entry - all other fields are `undefined`. It is
-    /// the first and only entry in one of `intern_pool.*_deps`, and does not
-    /// appear in any list by `first_dependency`, but is not in
-    /// `free_dep_entries` since `*_deps` stores a reference to it.
+    /// If null, this is a dummy entry. `next_dependee` is undefined. This is the first
+    /// entry in one of `*_deps`, and does not appear in any list by `first_dependency`,
+    /// but is not in `free_dep_entries` since `*_deps` stores a reference to it.
     depender: AnalUnit.Optional,
     /// Index into `dep_entries` forming a doubly linked list of all dependencies on this dependee.
     /// Used to iterate all dependers for a given dependee during an update.
@@ -2689,7 +2688,12 @@ pub const Key = union(enum) {
 
             .variable => |a_info| {
                 const b_info = b.variable;
-                return a_info.owner_nav == b_info.owner_nav;
+                return a_info.owner_nav == b_info.owner_nav and
+                    a_info.ty == b_info.ty and
+                    a_info.init == b_info.init and
+                    a_info.lib_name == b_info.lib_name and
+                    a_info.is_threadlocal == b_info.is_threadlocal and
+                    a_info.is_weak_linkage == b_info.is_weak_linkage;
             },
             .@"extern" => |a_info| {
                 const b_info = b.@"extern";
@@ -8016,6 +8020,10 @@ pub const UnionTypeInit = struct {
             zir_index: TrackedInst.Index,
             captures: []const CaptureValue,
         },
+        declared_owned_captures: struct {
+            zir_index: TrackedInst.Index,
+            captures: CaptureValue.Slice,
+        },
         reified: struct {
             zir_index: TrackedInst.Index,
             type_hash: u64,
@@ -8037,6 +8045,10 @@ pub fn getUnionType(
             .zir_index = d.zir_index,
             .captures = .{ .external = d.captures },
         } },
+        .declared_owned_captures => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .owned = d.captures },
+        } },
         .reified => |r| .{ .reified = .{
             .zir_index = r.zir_index,
             .type_hash = r.type_hash,
@@ -8060,7 +8072,7 @@ pub fn getUnionType(
         // TODO: fmt bug
         // zig fmt: off
         switch (ini.key) {
-            .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+            inline .declared, .declared_owned_captures => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
             .reified => 2, // type_hash: PackedU64
         } +
         // zig fmt: on
@@ -8069,7 +8081,10 @@ pub fn getUnionType(
 
     const extra_index = addExtraAssumeCapacity(extra, Tag.TypeUnion{
         .flags = .{
-            .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
+            .any_captures = switch (ini.key) {
+                inline .declared, .declared_owned_captures => |d| d.captures.len != 0,
+                .reified => false,
+            },
             .runtime_tag = ini.flags.runtime_tag,
             .any_aligned_fields = ini.flags.any_aligned_fields,
             .layout = ini.flags.layout,
@@ -8078,7 +8093,10 @@ pub fn getUnionType(
             .assumed_runtime_bits = ini.flags.assumed_runtime_bits,
             .assumed_pointer_aligned = ini.flags.assumed_pointer_aligned,
             .alignment = ini.flags.alignment,
-            .is_reified = ini.key == .reified,
+            .is_reified = switch (ini.key) {
+                .declared, .declared_owned_captures => false,
+                .reified => true,
+            },
         },
         .fields_len = ini.fields_len,
         .size = std.math.maxInt(u32),
@@ -8102,6 +8120,10 @@ pub fn getUnionType(
             extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
             extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures)});
         },
+        .declared_owned_captures => |d| if (d.captures.len != 0) {
+            extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
+            extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures.get(ip))});
+        },
         .reified => |r| _ = addExtraAssumeCapacity(extra, PackedU64.init(r.type_hash)),
     }
 
@@ -8199,6 +8221,10 @@ pub const StructTypeInit = struct {
             zir_index: TrackedInst.Index,
             captures: []const CaptureValue,
         },
+        declared_owned_captures: struct {
+            zir_index: TrackedInst.Index,
+            captures: CaptureValue.Slice,
+        },
         reified: struct {
             zir_index: TrackedInst.Index,
             type_hash: u64,
@@ -8220,6 +8246,10 @@ pub fn getStructType(
             .zir_index = d.zir_index,
             .captures = .{ .external = d.captures },
         } },
+        .declared_owned_captures => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .owned = d.captures },
+        } },
         .reified => |r| .{ .reified = .{
             .zir_index = r.zir_index,
             .type_hash = r.type_hash,
@@ -8251,7 +8281,7 @@ pub fn getStructType(
                 // TODO: fmt bug
                 // zig fmt: off
                 switch (ini.key) {
-                    .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+                    inline .declared, .declared_owned_captures => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
                     .reified => 2, // type_hash: PackedU64
                 } +
                 // zig fmt: on
@@ -8267,10 +8297,16 @@ pub fn getStructType(
                 .backing_int_ty = .none,
                 .names_map = names_map,
                 .flags = .{
-                    .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
+                    .any_captures = switch (ini.key) {
+                        inline .declared, .declared_owned_captures => |d| d.captures.len != 0,
+                        .reified => false,
+                    },
                     .field_inits_wip = false,
                     .inits_resolved = ini.inits_resolved,
-                    .is_reified = ini.key == .reified,
+                    .is_reified = switch (ini.key) {
+                        .declared, .declared_owned_captures => false,
+                        .reified => true,
+                    },
                 },
             });
             try items.append(.{
@@ -8282,6 +8318,10 @@ pub fn getStructType(
                     extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
                     extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures)});
                 },
+                .declared_owned_captures => |d| if (d.captures.len != 0) {
+                    extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
+                    extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures.get(ip))});
+                },
                 .reified => |r| {
                     _ = addExtraAssumeCapacity(extra, PackedU64.init(r.type_hash));
                 },
@@ -8309,7 +8349,7 @@ pub fn getStructType(
         // TODO: fmt bug
         // zig fmt: off
         switch (ini.key) {
-            .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+            inline .declared, .declared_owned_captures => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
             .reified => 2, // type_hash: PackedU64
         } +
         // zig fmt: on
@@ -8324,7 +8364,10 @@ pub fn getStructType(
         .fields_len = ini.fields_len,
         .size = std.math.maxInt(u32),
         .flags = .{
-            .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
+            .any_captures = switch (ini.key) {
+                inline .declared, .declared_owned_captures => |d| d.captures.len != 0,
+                .reified => false,
+            },
             .is_extern = is_extern,
             .known_non_opv = ini.known_non_opv,
             .requires_comptime = ini.requires_comptime,
@@ -8342,7 +8385,10 @@ pub fn getStructType(
             .field_inits_wip = false,
             .inits_resolved = ini.inits_resolved,
             .fully_resolved = false,
-            .is_reified = ini.key == .reified,
+            .is_reified = switch (ini.key) {
+                .declared, .declared_owned_captures => false,
+                .reified => true,
+            },
         },
     });
     try items.append(.{
@@ -8354,6 +8400,10 @@ pub fn getStructType(
             extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
             extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures)});
         },
+        .declared_owned_captures => |d| if (d.captures.len != 0) {
+            extra.appendAssumeCapacity(.{@intCast(d.captures.len)});
+            extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures.get(ip))});
+        },
         .reified => |r| {
             _ = addExtraAssumeCapacity(extra, PackedU64.init(r.type_hash));
         },
@@ -9157,6 +9207,10 @@ pub const EnumTypeInit = struct {
             zir_index: TrackedInst.Index,
             captures: []const CaptureValue,
         },
+        declared_owned_captures: struct {
+            zir_index: TrackedInst.Index,
+            captures: CaptureValue.Slice,
+        },
         reified: struct {
             zir_index: TrackedInst.Index,
             type_hash: u64,
@@ -9261,6 +9315,10 @@ pub fn getEnumType(
             .zir_index = d.zir_index,
             .captures = .{ .external = d.captures },
         } },
+        .declared_owned_captures => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .owned = d.captures },
+        } },
         .reified => |r| .{ .reified = .{
             .zir_index = r.zir_index,
             .type_hash = r.type_hash,
@@ -9288,7 +9346,7 @@ pub fn getEnumType(
                 // TODO: fmt bug
                 // zig fmt: off
                 switch (ini.key) {
-                    .declared => |d| d.captures.len,
+                    inline .declared, .declared_owned_captures => |d| d.captures.len,
                     .reified => 2, // type_hash: PackedU64
                 } +
                 // zig fmt: on
@@ -9298,7 +9356,7 @@ pub fn getEnumType(
             const extra_index = addExtraAssumeCapacity(extra, EnumAuto{
                 .name = undefined, // set by `prepare`
                 .captures_len = switch (ini.key) {
-                    .declared => |d| @intCast(d.captures.len),
+                    inline .declared, .declared_owned_captures => |d| @intCast(d.captures.len),
                     .reified => std.math.maxInt(u32),
                 },
                 .namespace = undefined, // set by `prepare`
@@ -9317,6 +9375,7 @@ pub fn getEnumType(
             extra.appendAssumeCapacity(undefined); // `cau` will be set by `finish`
             switch (ini.key) {
                 .declared => |d| extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures)}),
+                .declared_owned_captures => |d| extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures.get(ip))}),
                 .reified => |r| _ = addExtraAssumeCapacity(extra, PackedU64.init(r.type_hash)),
             }
             const names_start = extra.mutate.len;
@@ -9347,7 +9406,7 @@ pub fn getEnumType(
                 // TODO: fmt bug
                 // zig fmt: off
                 switch (ini.key) {
-                    .declared => |d| d.captures.len,
+                    inline .declared, .declared_owned_captures => |d| d.captures.len,
                     .reified => 2, // type_hash: PackedU64
                 } +
                 // zig fmt: on
@@ -9358,7 +9417,7 @@ pub fn getEnumType(
             const extra_index = addExtraAssumeCapacity(extra, EnumExplicit{
                 .name = undefined, // set by `prepare`
                 .captures_len = switch (ini.key) {
-                    .declared => |d| @intCast(d.captures.len),
+                    inline .declared, .declared_owned_captures => |d| @intCast(d.captures.len),
                     .reified => std.math.maxInt(u32),
                 },
                 .namespace = undefined, // set by `prepare`
@@ -9382,6 +9441,7 @@ pub fn getEnumType(
             extra.appendAssumeCapacity(undefined); // `cau` will be set by `finish`
             switch (ini.key) {
                 .declared => |d| extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures)}),
+                .declared_owned_captures => |d| extra.appendSliceAssumeCapacity(.{@ptrCast(d.captures.get(ip))}),
                 .reified => |r| _ = addExtraAssumeCapacity(extra, PackedU64.init(r.type_hash)),
             }
             const names_start = extra.mutate.len;
@@ -9445,10 +9505,12 @@ pub fn getGeneratedTagEnumType(
         .tid = tid,
         .index = items.mutate.len,
     }, ip);
+    const parent_namespace = ip.namespacePtr(ini.parent_namespace);
     const namespace = try ip.createNamespace(gpa, tid, .{
         .parent = ini.parent_namespace.toOptional(),
         .owner_type = enum_index,
-        .file_scope = ip.namespacePtr(ini.parent_namespace).file_scope,
+        .file_scope = parent_namespace.file_scope,
+        .generation = parent_namespace.generation,
     });
     errdefer ip.destroyNamespace(tid, namespace);
 
@@ -11044,6 +11106,7 @@ pub fn destroyNamespace(
         .parent = undefined,
         .file_scope = undefined,
         .owner_type = undefined,
+        .generation = undefined,
     };
     @field(namespace, Local.namespace_next_free_field) =
         @enumFromInt(local.mutate.namespaces.free_list);
src/Sema.zig
@@ -2723,32 +2723,6 @@ fn wrapWipTy(sema: *Sema, wip_ty: anytype) @TypeOf(wip_ty) {
     return new;
 }
 
-/// Given a type just looked up in the `InternPool`, check whether it is
-/// considered outdated on this update. If so, returns `true`, and the
-/// caller must replace the outdated type with a fresh one.
-fn checkOutdatedType(sema: *Sema, ty: InternPool.Index) !bool {
-    const pt = sema.pt;
-    const zcu = pt.zcu;
-    const ip = &zcu.intern_pool;
-
-    if (!zcu.comp.incremental) return false;
-
-    const cau_index = switch (ip.indexToKey(ty)) {
-        .struct_type => ip.loadStructType(ty).cau.unwrap().?,
-        .union_type => ip.loadUnionType(ty).cau,
-        .enum_type => ip.loadEnumType(ty).cau.unwrap().?,
-        else => unreachable,
-    };
-    const cau_unit = AnalUnit.wrap(.{ .cau = cau_index });
-    const was_outdated = zcu.outdated.swapRemove(cau_unit) or
-        zcu.potentially_outdated.swapRemove(cau_unit);
-    if (!was_outdated) return false;
-    _ = zcu.outdated_ready.swapRemove(cau_unit);
-    zcu.intern_pool.removeDependenciesForDepender(zcu.gpa, cau_unit);
-    try zcu.markDependeeOutdated(.marked_po, .{ .interned = ty });
-    return true;
-}
-
 fn zirStructDecl(
     sema: *Sema,
     block: *Block,
@@ -2815,13 +2789,16 @@ fn zirStructDecl(
         } },
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getStructType(gpa, pt.tid, struct_init, false)) {
-        .existing => |ty| wip: {
-            if (!try sema.checkOutdatedType(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, true)).wip;
+        .existing => |ty| {
+            const new_ty = try pt.ensureTypeUpToDate(ty, false);
+
+            // Make sure we update the namespace if the declaration is re-analyzed, to pick
+            // up on e.g. changed comptime decls.
+            try pt.ensureNamespaceUpToDate(Type.fromInterned(new_ty).getNamespaceIndex(mod));
+
+            try sema.declareDependency(.{ .interned = new_ty });
+            try sema.addTypeReferenceEntry(src, new_ty);
+            return Air.internedToRef(new_ty);
         },
         .wip => |wip| wip,
     });
@@ -2839,6 +2816,7 @@ fn zirStructDecl(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
     errdefer pt.destroyNamespace(new_namespace_index);
 
@@ -2977,7 +2955,6 @@ fn zirEnumDecl(
 
     const tracked_inst = try block.trackZir(inst);
     const src: LazySrcLoc = .{ .base_node_inst = tracked_inst, .offset = LazySrcLoc.Offset.nodeOffset(0) };
-    const tag_ty_src: LazySrcLoc = .{ .base_node_inst = tracked_inst, .offset = .{ .node_offset_container_tag = 0 } };
 
     const tag_type_ref = if (small.has_tag_type) blk: {
         const tag_type_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]);
@@ -3041,13 +3018,16 @@ fn zirEnumDecl(
         } },
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getEnumType(gpa, pt.tid, enum_init, false)) {
-        .existing => |ty| wip: {
-            if (!try sema.checkOutdatedType(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, true)).wip;
+        .existing => |ty| {
+            const new_ty = try pt.ensureTypeUpToDate(ty, false);
+
+            // Make sure we update the namespace if the declaration is re-analyzed, to pick
+            // up on e.g. changed comptime decls.
+            try pt.ensureNamespaceUpToDate(Type.fromInterned(new_ty).getNamespaceIndex(mod));
+
+            try sema.declareDependency(.{ .interned = new_ty });
+            try sema.addTypeReferenceEntry(src, new_ty);
+            return Air.internedToRef(new_ty);
         },
         .wip => |wip| wip,
     });
@@ -3071,19 +3051,12 @@ fn zirEnumDecl(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
     errdefer if (!done) pt.destroyNamespace(new_namespace_index);
 
     const new_cau_index = try ip.createTypeCau(gpa, pt.tid, tracked_inst, new_namespace_index, wip_ty.index);
 
-    if (pt.zcu.comp.incremental) {
-        try mod.intern_pool.addDependency(
-            gpa,
-            AnalUnit.wrap(.{ .cau = new_cau_index }),
-            .{ .src_hash = try block.trackZir(inst) },
-        );
-    }
-
     try pt.scanNamespace(new_namespace_index, decls);
 
     try sema.declareDependency(.{ .interned = wip_ty.index });
@@ -3094,144 +3067,22 @@ fn zirEnumDecl(
     wip_ty.prepare(ip, new_cau_index, new_namespace_index);
     done = true;
 
-    const int_tag_ty = ty: {
-        // We create a block for the field type instructions because they
-        // may need to reference Decls from inside the enum namespace.
-        // Within the field type, default value, and alignment expressions, the owner should be the enum's `Cau`.
-
-        const prev_owner = sema.owner;
-        sema.owner = AnalUnit.wrap(.{ .cau = new_cau_index });
-        defer sema.owner = prev_owner;
-
-        const prev_func_index = sema.func_index;
-        sema.func_index = .none;
-        defer sema.func_index = prev_func_index;
-
-        var enum_block: Block = .{
-            .parent = null,
-            .sema = sema,
-            .namespace = new_namespace_index,
-            .instructions = .{},
-            .inlining = null,
-            .is_comptime = true,
-            .src_base_inst = tracked_inst,
-            .type_name_ctx = type_name,
-        };
-        defer enum_block.instructions.deinit(sema.gpa);
-
-        if (body.len != 0) {
-            _ = try sema.analyzeInlineBody(&enum_block, body, inst);
-        }
-
-        if (tag_type_ref != .none) {
-            const ty = try sema.resolveType(&enum_block, tag_ty_src, tag_type_ref);
-            if (ty.zigTypeTag(mod) != .Int and ty.zigTypeTag(mod) != .ComptimeInt) {
-                return sema.fail(&enum_block, tag_ty_src, "expected integer tag type, found '{}'", .{ty.fmt(pt)});
-            }
-            break :ty ty;
-        } else if (fields_len == 0) {
-            break :ty try pt.intType(.unsigned, 0);
-        } else {
-            const bits = std.math.log2_int_ceil(usize, fields_len);
-            break :ty try pt.intType(.unsigned, bits);
-        }
-    };
-
-    wip_ty.setTagTy(ip, int_tag_ty.toIntern());
-
-    if (small.nonexhaustive and int_tag_ty.toIntern() != .comptime_int_type) {
-        if (fields_len > 1 and std.math.log2_int(u64, fields_len) == int_tag_ty.bitSize(pt)) {
-            return sema.fail(block, src, "non-exhaustive enum specifies every value", .{});
-        }
-    }
-
-    var bit_bag_index: usize = body_end;
-    var cur_bit_bag: u32 = undefined;
-    var field_i: u32 = 0;
-    var last_tag_val: ?Value = null;
-    while (field_i < fields_len) : (field_i += 1) {
-        if (field_i % 32 == 0) {
-            cur_bit_bag = sema.code.extra[bit_bag_index];
-            bit_bag_index += 1;
-        }
-        const has_tag_value = @as(u1, @truncate(cur_bit_bag)) != 0;
-        cur_bit_bag >>= 1;
-
-        const field_name_index: Zir.NullTerminatedString = @enumFromInt(sema.code.extra[extra_index]);
-        const field_name_zir = sema.code.nullTerminatedString(field_name_index);
-        extra_index += 2; // field name, doc comment
-
-        const field_name = try mod.intern_pool.getOrPutString(gpa, pt.tid, field_name_zir, .no_embedded_nulls);
-
-        const value_src: LazySrcLoc = .{
-            .base_node_inst = tracked_inst,
-            .offset = .{ .container_field_value = field_i },
-        };
-
-        const tag_overflow = if (has_tag_value) overflow: {
-            const tag_val_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]);
-            extra_index += 1;
-            const tag_inst = try sema.resolveInst(tag_val_ref);
-            last_tag_val = try sema.resolveConstDefinedValue(block, .{
-                .base_node_inst = tracked_inst,
-                .offset = .{ .container_field_name = field_i },
-            }, tag_inst, .{
-                .needed_comptime_reason = "enum tag value must be comptime-known",
-            });
-            if (!(try sema.intFitsInType(last_tag_val.?, int_tag_ty, null))) break :overflow true;
-            last_tag_val = try pt.getCoerced(last_tag_val.?, int_tag_ty);
-            if (wip_ty.nextField(&mod.intern_pool, field_name, last_tag_val.?.toIntern())) |conflict| {
-                assert(conflict.kind == .value); // AstGen validated names are unique
-                const other_field_src: LazySrcLoc = .{
-                    .base_node_inst = tracked_inst,
-                    .offset = .{ .container_field_value = conflict.prev_field_idx },
-                };
-                const msg = msg: {
-                    const msg = try sema.errMsg(value_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValueSema(pt, sema)});
-                    errdefer msg.destroy(gpa);
-                    try sema.errNote(other_field_src, msg, "other occurrence here", .{});
-                    break :msg msg;
-                };
-                return sema.failWithOwnedErrorMsg(block, msg);
-            }
-            break :overflow false;
-        } else if (any_values) overflow: {
-            var overflow: ?usize = null;
-            last_tag_val = if (last_tag_val) |val|
-                try sema.intAdd(val, try pt.intValue(int_tag_ty, 1), int_tag_ty, &overflow)
-            else
-                try pt.intValue(int_tag_ty, 0);
-            if (overflow != null) break :overflow true;
-            if (wip_ty.nextField(&mod.intern_pool, field_name, last_tag_val.?.toIntern())) |conflict| {
-                assert(conflict.kind == .value); // AstGen validated names are unique
-                const other_field_src: LazySrcLoc = .{
-                    .base_node_inst = tracked_inst,
-                    .offset = .{ .container_field_value = conflict.prev_field_idx },
-                };
-                const msg = msg: {
-                    const msg = try sema.errMsg(value_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValueSema(pt, sema)});
-                    errdefer msg.destroy(gpa);
-                    try sema.errNote(other_field_src, msg, "other occurrence here", .{});
-                    break :msg msg;
-                };
-                return sema.failWithOwnedErrorMsg(block, msg);
-            }
-            break :overflow false;
-        } else overflow: {
-            assert(wip_ty.nextField(&mod.intern_pool, field_name, .none) == null);
-            last_tag_val = try pt.intValue(Type.comptime_int, field_i);
-            if (!try sema.intFitsInType(last_tag_val.?, int_tag_ty, null)) break :overflow true;
-            last_tag_val = try pt.getCoerced(last_tag_val.?, int_tag_ty);
-            break :overflow false;
-        };
-
-        if (tag_overflow) {
-            const msg = try sema.errMsg(value_src, "enumeration value '{}' too large for type '{}'", .{
-                last_tag_val.?.fmtValueSema(pt, sema), int_tag_ty.fmt(pt),
-            });
-            return sema.failWithOwnedErrorMsg(block, msg);
-        }
-    }
+    try Sema.resolveDeclaredEnum(
+        pt,
+        wip_ty,
+        inst,
+        tracked_inst,
+        new_namespace_index,
+        type_name,
+        new_cau_index,
+        small,
+        body,
+        tag_type_ref,
+        any_values,
+        fields_len,
+        sema.code,
+        body_end,
+    );
 
     codegen_type: {
         if (mod.comp.config.use_llvm) break :codegen_type;
@@ -3311,13 +3162,16 @@ fn zirUnionDecl(
         } },
     };
     const wip_ty = sema.wrapWipTy(switch (try ip.getUnionType(gpa, pt.tid, union_init, false)) {
-        .existing => |ty| wip: {
-            if (!try sema.checkOutdatedType(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, true)).wip;
+        .existing => |ty| {
+            const new_ty = try pt.ensureTypeUpToDate(ty, false);
+
+            // Make sure we update the namespace if the declaration is re-analyzed, to pick
+            // up on e.g. changed comptime decls.
+            try pt.ensureNamespaceUpToDate(Type.fromInterned(new_ty).getNamespaceIndex(mod));
+
+            try sema.declareDependency(.{ .interned = new_ty });
+            try sema.addTypeReferenceEntry(src, new_ty);
+            return Air.internedToRef(new_ty);
         },
         .wip => |wip| wip,
     });
@@ -3335,6 +3189,7 @@ fn zirUnionDecl(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
     errdefer pt.destroyNamespace(new_namespace_index);
 
@@ -3344,7 +3199,7 @@ fn zirUnionDecl(
         try mod.intern_pool.addDependency(
             gpa,
             AnalUnit.wrap(.{ .cau = new_cau_index }),
-            .{ .src_hash = try block.trackZir(inst) },
+            .{ .src_hash = tracked_inst },
         );
     }
 
@@ -3406,8 +3261,12 @@ 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 `checkOutdatedType` as opaque types are never outdated.
         .existing => |ty| {
+            // Make sure we update the namespace if the declaration is re-analyzed, to pick
+            // up on e.g. changed comptime decls.
+            try pt.ensureNamespaceUpToDate(Type.fromInterned(ty).getNamespaceIndex(mod));
+
+            try sema.declareDependency(.{ .interned = ty });
             try sema.addTypeReferenceEntry(src, ty);
             return Air.internedToRef(ty);
         },
@@ -3427,6 +3286,7 @@ fn zirOpaqueDecl(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
     errdefer pt.destroyNamespace(new_namespace_index);
 
@@ -6072,6 +5932,7 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
     // trigger re-analysis later.
     try pt.ensureFileAnalyzed(result.file_index);
     const ty = zcu.fileRootType(result.file_index);
+    try sema.declareDependency(.{ .interned = ty });
     try sema.addTypeReferenceEntry(src, ty);
     return Air.internedToRef(ty);
 }
@@ -6821,6 +6682,8 @@ fn lookupInNamespace(
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
+    try pt.ensureNamespaceUpToDate(namespace_index);
+
     const namespace = zcu.namespacePtr(namespace_index);
 
     const adapter: Zcu.Namespace.NameAdapter = .{ .zcu = zcu };
@@ -14038,6 +13901,7 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
     // trigger re-analysis later.
     try pt.ensureFileAnalyzed(result.file_index);
     const ty = zcu.fileRootType(result.file_index);
+    try sema.declareDependency(.{ .interned = ty });
     try sema.addTypeReferenceEntry(operand_src, ty);
     return Air.internedToRef(ty);
 }
@@ -17703,7 +17567,13 @@ fn zirThis(
     _ = extended;
     const pt = sema.pt;
     const namespace = pt.zcu.namespacePtr(block.namespace);
-    return Air.internedToRef(namespace.owner_type);
+    const new_ty = try pt.ensureTypeUpToDate(namespace.owner_type, false);
+    switch (pt.zcu.intern_pool.indexToKey(new_ty)) {
+        .struct_type, .union_type, .enum_type => try sema.declareDependency(.{ .interned = new_ty }),
+        .opaque_type => {},
+        else => unreachable,
+    }
+    return Air.internedToRef(new_ty);
 }
 
 fn zirClosureGet(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref {
@@ -19005,6 +18875,7 @@ fn typeInfoNamespaceDecls(
     const ip = &zcu.intern_pool;
 
     const namespace_index = opt_namespace_index.unwrap() orelse return;
+    try pt.ensureNamespaceUpToDate(namespace_index);
     const namespace = zcu.namespacePtr(namespace_index);
 
     const gop = try seen_namespaces.getOrPut(namespace);
@@ -21871,6 +21742,7 @@ fn zirReify(
                 .parent = block.namespace.toOptional(),
                 .owner_type = wip_ty.index,
                 .file_scope = block.getFileScopeIndex(mod),
+                .generation = mod.generation,
             });
 
             try sema.addTypeReferenceEntry(src, wip_ty.index);
@@ -22080,6 +21952,7 @@ fn reifyEnum(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
 
     const new_cau_index = try ip.createTypeCau(gpa, pt.tid, tracked_inst, new_namespace_index, wip_ty.index);
@@ -22384,6 +22257,7 @@ fn reifyUnion(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
 
     const new_cau_index = try ip.createTypeCau(gpa, pt.tid, tracked_inst, new_namespace_index, wip_ty.index);
@@ -22667,6 +22541,7 @@ fn reifyStruct(
         .parent = block.namespace.toOptional(),
         .owner_type = wip_ty.index,
         .file_scope = block.getFileScopeIndex(mod),
+        .generation = mod.generation,
     });
 
     const new_cau_index = try ip.createTypeCau(gpa, pt.tid, tracked_inst, new_namespace_index, wip_ty.index);
@@ -35373,7 +35248,7 @@ pub fn resolveStructLayout(sema: *Sema, ty: Type) SemaError!void {
     if (struct_type.haveLayout(ip))
         return;
 
-    try ty.resolveFields(pt);
+    try sema.resolveTypeFieldsStruct(ty.toIntern(), struct_type);
 
     if (struct_type.layout == .@"packed") {
         semaBackingIntType(pt, struct_type) catch |err| switch (err) {
@@ -38499,6 +38374,187 @@ fn getOwnerFuncDeclInst(sema: *Sema) InternPool.TrackedInst.Index {
     return ip.getCau(cau).zir_index;
 }
 
+/// Called as soon as a `declared` enum type is created.
+/// Resolves the tag type and field inits.
+/// Marks the `src_inst` dependency on the enum's declaration, so call sites need not do this.
+pub fn resolveDeclaredEnum(
+    pt: Zcu.PerThread,
+    wip_ty: InternPool.WipEnumType,
+    inst: Zir.Inst.Index,
+    tracked_inst: InternPool.TrackedInst.Index,
+    namespace: InternPool.NamespaceIndex,
+    type_name: InternPool.NullTerminatedString,
+    enum_cau: InternPool.Cau.Index,
+    small: Zir.Inst.EnumDecl.Small,
+    body: []const Zir.Inst.Index,
+    tag_type_ref: Zir.Inst.Ref,
+    any_values: bool,
+    fields_len: u32,
+    zir: Zir,
+    body_end: usize,
+) Zcu.CompileError!void {
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const bit_bags_count = std.math.divCeil(usize, fields_len, 32) catch unreachable;
+
+    const src: LazySrcLoc = .{ .base_node_inst = tracked_inst, .offset = LazySrcLoc.Offset.nodeOffset(0) };
+    const tag_ty_src: LazySrcLoc = .{ .base_node_inst = tracked_inst, .offset = .{ .node_offset_container_tag = 0 } };
+
+    const anal_unit = AnalUnit.wrap(.{ .cau = enum_cau });
+
+    var arena = std.heap.ArenaAllocator.init(gpa);
+    defer arena.deinit();
+
+    var comptime_err_ret_trace = std.ArrayList(Zcu.LazySrcLoc).init(gpa);
+    defer comptime_err_ret_trace.deinit();
+
+    var sema: Sema = .{
+        .pt = pt,
+        .gpa = gpa,
+        .arena = arena.allocator(),
+        .code = zir,
+        .owner = anal_unit,
+        .func_index = .none,
+        .func_is_naked = false,
+        .fn_ret_ty = Type.void,
+        .fn_ret_ty_ies = null,
+        .comptime_err_ret_trace = &comptime_err_ret_trace,
+    };
+    defer sema.deinit();
+
+    try sema.declareDependency(.{ .src_hash = tracked_inst });
+
+    var block: Block = .{
+        .parent = null,
+        .sema = &sema,
+        .namespace = namespace,
+        .instructions = .{},
+        .inlining = null,
+        .is_comptime = true,
+        .src_base_inst = tracked_inst,
+        .type_name_ctx = type_name,
+    };
+    defer block.instructions.deinit(gpa);
+
+    const int_tag_ty = ty: {
+        if (body.len != 0) {
+            _ = try sema.analyzeInlineBody(&block, body, inst);
+        }
+
+        if (tag_type_ref != .none) {
+            const ty = try sema.resolveType(&block, tag_ty_src, tag_type_ref);
+            if (ty.zigTypeTag(zcu) != .Int and ty.zigTypeTag(zcu) != .ComptimeInt) {
+                return sema.fail(&block, tag_ty_src, "expected integer tag type, found '{}'", .{ty.fmt(pt)});
+            }
+            break :ty ty;
+        } else if (fields_len == 0) {
+            break :ty try pt.intType(.unsigned, 0);
+        } else {
+            const bits = std.math.log2_int_ceil(usize, fields_len);
+            break :ty try pt.intType(.unsigned, bits);
+        }
+    };
+
+    wip_ty.setTagTy(ip, int_tag_ty.toIntern());
+
+    if (small.nonexhaustive and int_tag_ty.toIntern() != .comptime_int_type) {
+        if (fields_len > 1 and std.math.log2_int(u64, fields_len) == int_tag_ty.bitSize(pt)) {
+            return sema.fail(&block, src, "non-exhaustive enum specifies every value", .{});
+        }
+    }
+
+    var extra_index = body_end + bit_bags_count;
+    var bit_bag_index: usize = body_end;
+    var cur_bit_bag: u32 = undefined;
+    var last_tag_val: ?Value = null;
+    for (0..fields_len) |field_i_usize| {
+        const field_i: u32 = @intCast(field_i_usize);
+        if (field_i % 32 == 0) {
+            cur_bit_bag = zir.extra[bit_bag_index];
+            bit_bag_index += 1;
+        }
+        const has_tag_value = @as(u1, @truncate(cur_bit_bag)) != 0;
+        cur_bit_bag >>= 1;
+
+        const field_name_index: Zir.NullTerminatedString = @enumFromInt(zir.extra[extra_index]);
+        const field_name_zir = zir.nullTerminatedString(field_name_index);
+        extra_index += 2; // field name, doc comment
+
+        const field_name = try ip.getOrPutString(gpa, pt.tid, field_name_zir, .no_embedded_nulls);
+
+        const value_src: LazySrcLoc = .{
+            .base_node_inst = tracked_inst,
+            .offset = .{ .container_field_value = field_i },
+        };
+
+        const tag_overflow = if (has_tag_value) overflow: {
+            const tag_val_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
+            extra_index += 1;
+            const tag_inst = try sema.resolveInst(tag_val_ref);
+            last_tag_val = try sema.resolveConstDefinedValue(&block, .{
+                .base_node_inst = tracked_inst,
+                .offset = .{ .container_field_name = field_i },
+            }, tag_inst, .{
+                .needed_comptime_reason = "enum tag value must be comptime-known",
+            });
+            if (!(try sema.intFitsInType(last_tag_val.?, int_tag_ty, null))) break :overflow true;
+            last_tag_val = try pt.getCoerced(last_tag_val.?, int_tag_ty);
+            if (wip_ty.nextField(ip, field_name, last_tag_val.?.toIntern())) |conflict| {
+                assert(conflict.kind == .value); // AstGen validated names are unique
+                const other_field_src: LazySrcLoc = .{
+                    .base_node_inst = tracked_inst,
+                    .offset = .{ .container_field_value = conflict.prev_field_idx },
+                };
+                const msg = msg: {
+                    const msg = try sema.errMsg(value_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValueSema(pt, &sema)});
+                    errdefer msg.destroy(gpa);
+                    try sema.errNote(other_field_src, msg, "other occurrence here", .{});
+                    break :msg msg;
+                };
+                return sema.failWithOwnedErrorMsg(&block, msg);
+            }
+            break :overflow false;
+        } else if (any_values) overflow: {
+            var overflow: ?usize = null;
+            last_tag_val = if (last_tag_val) |val|
+                try sema.intAdd(val, try pt.intValue(int_tag_ty, 1), int_tag_ty, &overflow)
+            else
+                try pt.intValue(int_tag_ty, 0);
+            if (overflow != null) break :overflow true;
+            if (wip_ty.nextField(ip, field_name, last_tag_val.?.toIntern())) |conflict| {
+                assert(conflict.kind == .value); // AstGen validated names are unique
+                const other_field_src: LazySrcLoc = .{
+                    .base_node_inst = tracked_inst,
+                    .offset = .{ .container_field_value = conflict.prev_field_idx },
+                };
+                const msg = msg: {
+                    const msg = try sema.errMsg(value_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValueSema(pt, &sema)});
+                    errdefer msg.destroy(gpa);
+                    try sema.errNote(other_field_src, msg, "other occurrence here", .{});
+                    break :msg msg;
+                };
+                return sema.failWithOwnedErrorMsg(&block, msg);
+            }
+            break :overflow false;
+        } else overflow: {
+            assert(wip_ty.nextField(ip, field_name, .none) == null);
+            last_tag_val = try pt.intValue(Type.comptime_int, field_i);
+            if (!try sema.intFitsInType(last_tag_val.?, int_tag_ty, null)) break :overflow true;
+            last_tag_val = try pt.getCoerced(last_tag_val.?, int_tag_ty);
+            break :overflow false;
+        };
+
+        if (tag_overflow) {
+            const msg = try sema.errMsg(value_src, "enumeration value '{}' too large for type '{}'", .{
+                last_tag_val.?.fmtValueSema(pt, &sema), int_tag_ty.fmt(pt),
+            });
+            return sema.failWithOwnedErrorMsg(&block, msg);
+        }
+    }
+}
+
 pub const bitCastVal = @import("Sema/bitcast.zig").bitCast;
 pub const bitCastSpliceVal = @import("Sema/bitcast.zig").bitCastSplice;
 
src/Zcu.zig
@@ -215,6 +215,8 @@ panic_messages: [PanicId.len]InternPool.Nav.Index.Optional = .{.none} ** PanicId
 panic_func_index: InternPool.Index = .none,
 null_stack_trace: InternPool.Index = .none,
 
+generation: u32 = 0,
+
 pub const PerThread = @import("Zcu/PerThread.zig");
 
 pub const PanicId = enum {
@@ -332,6 +334,7 @@ pub const TypeReference = struct {
 pub const Namespace = struct {
     parent: OptionalIndex,
     file_scope: File.Index,
+    generation: u32,
     /// Will be a struct, enum, union, or opaque.
     owner_type: InternPool.Index,
     /// Members of the namespace which are marked `pub`.
@@ -2295,7 +2298,7 @@ pub fn markDependeeOutdated(
     marked_po: enum { not_marked_po, marked_po },
     dependee: InternPool.Dependee,
 ) !void {
-    log.debug("outdated dependee: {}", .{fmtDependee(dependee, zcu)});
+    log.debug("outdated dependee: {}", .{zcu.fmtDependee(dependee)});
     var it = zcu.intern_pool.dependencyIterator(dependee);
     while (it.next()) |depender| {
         if (zcu.outdated.getPtr(depender)) |po_dep_count| {
@@ -2303,9 +2306,9 @@ pub fn markDependeeOutdated(
                 .not_marked_po => {},
                 .marked_po => {
                     po_dep_count.* -= 1;
-                    log.debug("po dep count: {} = {}", .{ fmtAnalUnit(depender, zcu), po_dep_count.* });
+                    log.debug("outdated {} => already outdated {} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), po_dep_count.* });
                     if (po_dep_count.* == 0) {
-                        log.debug("outdated ready: {}", .{fmtAnalUnit(depender, zcu)});
+                        log.debug("outdated ready: {}", .{zcu.fmtAnalUnit(depender)});
                         try zcu.outdated_ready.put(zcu.gpa, depender, {});
                     }
                 },
@@ -2316,20 +2319,19 @@ pub fn markDependeeOutdated(
         const new_po_dep_count = switch (marked_po) {
             .not_marked_po => if (opt_po_entry) |e| e.value else 0,
             .marked_po => if (opt_po_entry) |e| e.value - 1 else {
-                // This dependency has been registered during in-progress analysis, but the unit is
-                // not in `potentially_outdated` because analysis is in-progress. Nothing to do.
+                // This `AnalUnit` has already been re-analyzed this update, and registered a dependency
+                // on this thing, but already has sufficiently up-to-date information. Nothing to do.
                 continue;
             },
         };
-        log.debug("po dep count: {} = {}", .{ fmtAnalUnit(depender, zcu), new_po_dep_count });
         try zcu.outdated.putNoClobber(
             zcu.gpa,
             depender,
             new_po_dep_count,
         );
-        log.debug("outdated: {}", .{fmtAnalUnit(depender, zcu)});
+        log.debug("outdated {} => new outdated {} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), new_po_dep_count });
         if (new_po_dep_count == 0) {
-            log.debug("outdated ready: {}", .{fmtAnalUnit(depender, zcu)});
+            log.debug("outdated ready: {}", .{zcu.fmtAnalUnit(depender)});
             try zcu.outdated_ready.put(zcu.gpa, depender, {});
         }
         // If this is a Decl and was not previously PO, we must recursively
@@ -2342,16 +2344,16 @@ pub fn markDependeeOutdated(
 }
 
 pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
-    log.debug("up-to-date dependee: {}", .{fmtDependee(dependee, zcu)});
+    log.debug("up-to-date dependee: {}", .{zcu.fmtDependee(dependee)});
     var it = zcu.intern_pool.dependencyIterator(dependee);
     while (it.next()) |depender| {
         if (zcu.outdated.getPtr(depender)) |po_dep_count| {
             // This depender is already outdated, but it now has one
             // less PO dependency!
             po_dep_count.* -= 1;
-            log.debug("po dep count: {} = {}", .{ fmtAnalUnit(depender, zcu), po_dep_count.* });
+            log.debug("up-to-date {} => {} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), po_dep_count.* });
             if (po_dep_count.* == 0) {
-                log.debug("outdated ready: {}", .{fmtAnalUnit(depender, zcu)});
+                log.debug("outdated ready: {}", .{zcu.fmtAnalUnit(depender)});
                 try zcu.outdated_ready.put(zcu.gpa, depender, {});
             }
             continue;
@@ -2365,11 +2367,11 @@ pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
         };
         if (ptr.* > 1) {
             ptr.* -= 1;
-            log.debug("po dep count: {} = {}", .{ fmtAnalUnit(depender, zcu), ptr.* });
+            log.debug("up-to-date {} => {} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender), ptr.* });
             continue;
         }
 
-        log.debug("up-to-date (po deps = 0): {}", .{fmtAnalUnit(depender, zcu)});
+        log.debug("up-to-date {} => {} po_deps=0 (up-to-date)", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(depender) });
 
         // This dependency is no longer PO, i.e. is known to be up-to-date.
         assert(zcu.potentially_outdated.swapRemove(depender));
@@ -2398,7 +2400,7 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
         },
         .func => |func_index| .{ .interned = func_index }, // IES
     };
-    log.debug("marking dependee po: {}", .{fmtDependee(dependee, zcu)});
+    log.debug("potentially outdated dependee: {}", .{zcu.fmtDependee(dependee)});
     var it = ip.dependencyIterator(dependee);
     while (it.next()) |po| {
         if (zcu.outdated.getPtr(po)) |po_dep_count| {
@@ -2408,17 +2410,17 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
                 _ = zcu.outdated_ready.swapRemove(po);
             }
             po_dep_count.* += 1;
-            log.debug("po dep count: {} = {}", .{ fmtAnalUnit(po, zcu), po_dep_count.* });
+            log.debug("po {} => {} [outdated] po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po), po_dep_count.* });
             continue;
         }
         if (zcu.potentially_outdated.getPtr(po)) |n| {
             // There is now one more PO dependency.
             n.* += 1;
-            log.debug("po dep count: {} = {}", .{ fmtAnalUnit(po, zcu), n.* });
+            log.debug("po {} => {} po_deps={}", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po), n.* });
             continue;
         }
         try zcu.potentially_outdated.putNoClobber(zcu.gpa, po, 1);
-        log.debug("po dep count: {} = {}", .{ fmtAnalUnit(po, zcu), 1 });
+        log.debug("po {} => {} po_deps=1", .{ zcu.fmtDependee(dependee), zcu.fmtAnalUnit(po) });
         // This AnalUnit was not already PO, so we must recursively mark its dependers as also PO.
         try zcu.markTransitiveDependersPotentiallyOutdated(po);
     }
@@ -2443,7 +2445,7 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
 
     if (zcu.outdated_ready.count() > 0) {
         const unit = zcu.outdated_ready.keys()[0];
-        log.debug("findOutdatedToAnalyze: trivial {}", .{fmtAnalUnit(unit, zcu)});
+        log.debug("findOutdatedToAnalyze: trivial {}", .{zcu.fmtAnalUnit(unit)});
         return unit;
     }
 
@@ -2498,10 +2500,15 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
             const nav = zcu.funcInfo(func).owner_nav;
             std.io.getStdErr().writer().print("outdated: func {}, nav {}, name '{}', [p]o deps {}\n", .{ func, nav, ip.getNav(nav).fqn.fmt(ip), opod }) catch {};
         }
+        for (zcu.potentially_outdated.keys(), zcu.potentially_outdated.values()) |o, opod| {
+            const func = o.unwrap().func;
+            const nav = zcu.funcInfo(func).owner_nav;
+            std.io.getStdErr().writer().print("po: func {}, nav {}, name '{}', [p]o deps {}\n", .{ func, nav, ip.getNav(nav).fqn.fmt(ip), opod }) catch {};
+        }
     }
 
     log.debug("findOutdatedToAnalyze: heuristic returned '{}' ({d} dependers)", .{
-        fmtAnalUnit(AnalUnit.wrap(.{ .cau = chosen_cau.? }), zcu),
+        zcu.fmtAnalUnit(AnalUnit.wrap(.{ .cau = chosen_cau.? })),
         chosen_cau_dependers,
     });
 
@@ -2744,7 +2751,7 @@ pub fn deleteUnitReferences(zcu: *Zcu, anal_unit: AnalUnit) void {
     const gpa = zcu.gpa;
 
     unit_refs: {
-        const kv = zcu.reference_table.fetchSwapRemove(anal_unit) orelse return;
+        const kv = zcu.reference_table.fetchSwapRemove(anal_unit) orelse break :unit_refs;
         var idx = kv.value;
 
         while (idx != std.math.maxInt(u32)) {
@@ -2758,7 +2765,7 @@ pub fn deleteUnitReferences(zcu: *Zcu, anal_unit: AnalUnit) void {
     }
 
     type_refs: {
-        const kv = zcu.type_reference_table.fetchSwapRemove(anal_unit) orelse return;
+        const kv = zcu.type_reference_table.fetchSwapRemove(anal_unit) orelse break :type_refs;
         var idx = kv.value;
 
         while (idx != std.math.maxInt(u32)) {
@@ -3280,7 +3287,7 @@ pub fn resolveReferences(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolve
             const unit = kv.key;
             try result.putNoClobber(gpa, unit, kv.value);
 
-            log.debug("handle unit '{}'", .{fmtAnalUnit(unit, zcu)});
+            log.debug("handle unit '{}'", .{zcu.fmtAnalUnit(unit)});
 
             if (zcu.reference_table.get(unit)) |first_ref_idx| {
                 assert(first_ref_idx != std.math.maxInt(u32));
@@ -3289,8 +3296,8 @@ pub fn resolveReferences(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolve
                     const ref = zcu.all_references.items[ref_idx];
                     if (!result.contains(ref.referenced)) {
                         log.debug("unit '{}': ref unit '{}'", .{
-                            fmtAnalUnit(unit, zcu),
-                            fmtAnalUnit(ref.referenced, zcu),
+                            zcu.fmtAnalUnit(unit),
+                            zcu.fmtAnalUnit(ref.referenced),
                         });
                         try unit_queue.put(gpa, ref.referenced, .{
                             .referencer = unit,
@@ -3307,7 +3314,7 @@ pub fn resolveReferences(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolve
                     const ref = zcu.all_type_references.items[ref_idx];
                     if (!checked_types.contains(ref.referenced)) {
                         log.debug("unit '{}': ref type '{}'", .{
-                            fmtAnalUnit(unit, zcu),
+                            zcu.fmtAnalUnit(unit),
                             Type.fromInterned(ref.referenced).containerTypeName(ip).fmt(ip),
                         });
                         try type_queue.put(gpa, ref.referenced, .{
@@ -3389,10 +3396,10 @@ pub fn cauFileScope(zcu: *Zcu, cau: InternPool.Cau.Index) *File {
     return zcu.fileByIndex(file_index);
 }
 
-fn fmtAnalUnit(unit: AnalUnit, zcu: *Zcu) std.fmt.Formatter(formatAnalUnit) {
+pub fn fmtAnalUnit(zcu: *Zcu, unit: AnalUnit) std.fmt.Formatter(formatAnalUnit) {
     return .{ .data = .{ .unit = unit, .zcu = zcu } };
 }
-fn fmtDependee(d: InternPool.Dependee, zcu: *Zcu) std.fmt.Formatter(formatDependee) {
+pub fn fmtDependee(zcu: *Zcu, d: InternPool.Dependee) std.fmt.Formatter(formatDependee) {
     return .{ .data = .{ .dependee = d, .zcu = zcu } };
 }