Commit 06d8bb32e3

mlugg <mlugg@mlugg.co.uk>
2024-01-07 01:42:30
InternPool: introduce TrackedInst
It is problematic for the cached `InternPool` state to directly reference ZIR instruction indices, as these are not stable across incremental updates. The existing ZIR mapping logic attempts to handle this by iterating the existing Decl graph for a file after `AstGen` and update ZIR indices on `Decl`s, struct types, etc. However, this is unreliable due to generic instantiations, and relies on specialized logic for everything which may refer to a ZIR instruction (e.g. a struct's owner decl). I therefore determined that a prerequisite change for incremental compilation would be to rework how we store these indices. This commit introduces a `TrackedInst` type which provides a stable index (`TrackedInst.Index`) for a single ZIR instruction in the compilation. The `InternPool` now stores these values in place of ZIR instruction indices. This makes the ZIR mapping logic relatively trivial: after `AstGen` completes, we simply iterate all `TrackedInst` values and update those indices which have changed. In future, if the corresponding ZIR instruction has been removed, we must also invalidate any dependencies on this instruction to trigger any required re-analysis, however the dependency system does not yet exist.
1 parent ae845a3
src/Compilation.zig
@@ -2795,6 +2795,7 @@ const Header = extern struct {
         extra_len: u32,
         limbs_len: u32,
         string_bytes_len: u32,
+        tracked_insts_len: u32,
     },
 };
 
@@ -2802,7 +2803,7 @@ const Header = extern struct {
 /// saved, such as the target and most CLI flags. A cache hit will only occur
 /// when subsequent compiler invocations use the same set of flags.
 pub fn saveState(comp: *Compilation) !void {
-    var bufs_list: [6]std.os.iovec_const = undefined;
+    var bufs_list: [7]std.os.iovec_const = undefined;
     var bufs_len: usize = 0;
 
     const lf = comp.bin_file orelse return;
@@ -2815,6 +2816,7 @@ pub fn saveState(comp: *Compilation) !void {
                 .extra_len = @intCast(ip.extra.items.len),
                 .limbs_len = @intCast(ip.limbs.items.len),
                 .string_bytes_len = @intCast(ip.string_bytes.items.len),
+                .tracked_insts_len = @intCast(ip.tracked_insts.count()),
             },
         };
         addBuf(&bufs_list, &bufs_len, mem.asBytes(&header));
@@ -2823,6 +2825,7 @@ pub fn saveState(comp: *Compilation) !void {
         addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.items.items(.data)));
         addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.items.items(.tag)));
         addBuf(&bufs_list, &bufs_len, ip.string_bytes.items);
+        addBuf(&bufs_list, &bufs_len, mem.sliceAsBytes(ip.tracked_insts.keys()));
 
         // TODO: compilation errors
         // TODO: files
src/InternPool.zig
@@ -54,6 +54,34 @@ string_table: std.HashMapUnmanaged(
     std.hash_map.default_max_load_percentage,
 ) = .{},
 
+/// An index into `tracked_insts` gives a reference to a single ZIR instruction which
+/// persists across incremental updates.
+tracked_insts: std.AutoArrayHashMapUnmanaged(TrackedInst, void) = .{},
+
+pub const TrackedInst = extern struct {
+    path_digest: Cache.BinDigest,
+    inst: Zir.Inst.Index,
+    comptime {
+        // The fields should be tightly packed. See also serialiation logic in `Compilation.saveState`.
+        assert(@sizeOf(@This()) == Cache.bin_digest_len + @sizeOf(Zir.Inst.Index));
+    }
+    pub const Index = enum(u32) {
+        _,
+        pub fn resolve(i: TrackedInst.Index, ip: *const InternPool) Zir.Inst.Index {
+            return ip.tracked_insts.keys()[@intFromEnum(i)].inst;
+        }
+    };
+};
+
+pub fn trackZir(ip: *InternPool, gpa: Allocator, file: *Module.File, inst: Zir.Inst.Index) Allocator.Error!TrackedInst.Index {
+    const key: TrackedInst = .{
+        .path_digest = file.path_digest,
+        .inst = inst,
+    };
+    const gop = try ip.tracked_insts.getOrPut(gpa, key);
+    return @enumFromInt(gop.index);
+}
+
 const FieldMap = std.ArrayHashMapUnmanaged(void, void, std.array_hash_map.AutoContext(void), false);
 
 const builtin = @import("builtin");
@@ -62,11 +90,13 @@ const Allocator = std.mem.Allocator;
 const assert = std.debug.assert;
 const BigIntConst = std.math.big.int.Const;
 const BigIntMutable = std.math.big.int.Mutable;
+const Cache = std.Build.Cache;
 const Limb = std.math.big.Limb;
 const Hash = std.hash.Wyhash;
 
 const InternPool = @This();
 const Module = @import("Module.zig");
+const Zcu = Module;
 const Zir = @import("Zir.zig");
 
 const KeyAdapter = struct {
@@ -409,7 +439,7 @@ pub const Key = union(enum) {
         /// `none` when the struct has no declarations.
         namespace: OptionalNamespaceIndex,
         /// Index of the struct_decl ZIR instruction.
-        zir_index: Zir.Inst.Index,
+        zir_index: TrackedInst.Index,
         layout: std.builtin.Type.ContainerLayout,
         field_names: NullTerminatedString.Slice,
         field_types: Index.Slice,
@@ -653,7 +683,7 @@ pub const Key = union(enum) {
         }
 
         /// Asserts the struct is not packed.
-        pub fn setZirIndex(s: @This(), ip: *InternPool, new_zir_index: Zir.Inst.Index) void {
+        pub fn setZirIndex(s: @This(), ip: *InternPool, new_zir_index: TrackedInst.Index) void {
             assert(s.layout != .Packed);
             const field_index = std.meta.fieldIndex(Tag.TypeStruct, "zir_index").?;
             ip.extra.items[s.extra_index + field_index] = @intFromEnum(new_zir_index);
@@ -769,7 +799,7 @@ pub const Key = union(enum) {
         flags: Tag.TypeUnion.Flags,
         /// The enum that provides the list of field names and values.
         enum_tag_ty: Index,
-        zir_index: Zir.Inst.Index,
+        zir_index: TrackedInst.Index,
 
         /// The returned pointer expires with any addition to the `InternPool`.
         pub fn flagsPtr(self: @This(), ip: *const InternPool) *Tag.TypeUnion.Flags {
@@ -1056,7 +1086,7 @@ pub const Key = union(enum) {
         /// the body. We store this rather than the body directly so that when ZIR
         /// is regenerated on update(), we can map this to the new corresponding
         /// ZIR instruction.
-        zir_body_inst: Zir.Inst.Index,
+        zir_body_inst: TrackedInst.Index,
         /// Relative to owner Decl.
         lbrace_line: u32,
         /// Relative to owner Decl.
@@ -1082,7 +1112,7 @@ pub const Key = union(enum) {
         }
 
         /// Returns a pointer that becomes invalid after any additions to the `InternPool`.
-        pub fn zirBodyInst(func: *const Func, ip: *const InternPool) *Zir.Inst.Index {
+        pub fn zirBodyInst(func: *const Func, ip: *const InternPool) *TrackedInst.Index {
             return @ptrCast(&ip.extra.items[func.zir_body_inst_extra_index]);
         }
 
@@ -1860,7 +1890,7 @@ pub const UnionType = struct {
     /// If this slice has length 0 it means all elements are `none`.
     field_aligns: Alignment.Slice,
     /// Index of the union_decl ZIR instruction.
-    zir_index: Zir.Inst.Index,
+    zir_index: TrackedInst.Index,
     /// Index into extra array of the `flags` field.
     flags_index: u32,
     /// Copied from `enum_tag_ty`.
@@ -1954,10 +1984,10 @@ pub const UnionType = struct {
     }
 
     /// This does not mutate the field of UnionType.
-    pub fn setZirIndex(self: @This(), ip: *InternPool, new_zir_index: Zir.Inst.Index) void {
+    pub fn setZirIndex(self: @This(), ip: *InternPool, new_zir_index: TrackedInst.Index) void {
         const flags_field_index = std.meta.fieldIndex(Tag.TypeUnion, "flags").?;
         const zir_index_field_index = std.meta.fieldIndex(Tag.TypeUnion, "zir_index").?;
-        const ptr: *Zir.Inst.Index =
+        const ptr: *TrackedInst.Index =
             @ptrCast(&ip.extra.items[self.flags_index - flags_field_index + zir_index_field_index]);
         ptr.* = new_zir_index;
     }
@@ -2976,7 +3006,7 @@ pub const Tag = enum(u8) {
         analysis: FuncAnalysis,
         owner_decl: DeclIndex,
         ty: Index,
-        zir_body_inst: Zir.Inst.Index,
+        zir_body_inst: TrackedInst.Index,
         lbrace_line: u32,
         rbrace_line: u32,
         lbrace_column: u32,
@@ -3050,7 +3080,7 @@ pub const Tag = enum(u8) {
         namespace: NamespaceIndex,
         /// The enum that provides the list of field names and values.
         tag_ty: Index,
-        zir_index: Zir.Inst.Index,
+        zir_index: TrackedInst.Index,
 
         pub const Flags = packed struct(u32) {
             runtime_tag: UnionType.RuntimeTag,
@@ -3072,7 +3102,7 @@ pub const Tag = enum(u8) {
     /// 2. init: Index for each fields_len // if tag is type_struct_packed_inits
     pub const TypeStructPacked = struct {
         decl: DeclIndex,
-        zir_index: Zir.Inst.Index,
+        zir_index: TrackedInst.Index,
         fields_len: u32,
         namespace: OptionalNamespaceIndex,
         backing_int_ty: Index,
@@ -3119,7 +3149,7 @@ pub const Tag = enum(u8) {
     /// 7. field_offset: u32 // for each field in declared order, undef until layout_resolved
     pub const TypeStruct = struct {
         decl: DeclIndex,
-        zir_index: Zir.Inst.Index,
+        zir_index: TrackedInst.Index,
         fields_len: u32,
         flags: Flags,
         size: u32,
@@ -3708,6 +3738,8 @@ pub fn deinit(ip: *InternPool, gpa: Allocator) void {
 
     ip.string_table.deinit(gpa);
 
+    ip.tracked_insts.deinit(gpa);
+
     ip.* = undefined;
 }
 
@@ -5358,7 +5390,7 @@ pub const UnionTypeInit = struct {
     flags: Tag.TypeUnion.Flags,
     decl: DeclIndex,
     namespace: NamespaceIndex,
-    zir_index: Zir.Inst.Index,
+    zir_index: TrackedInst.Index,
     fields_len: u32,
     enum_tag_ty: Index,
     /// May have length 0 which leaves the values unset until later.
@@ -5430,7 +5462,7 @@ pub const StructTypeInit = struct {
     decl: DeclIndex,
     namespace: OptionalNamespaceIndex,
     layout: std.builtin.Type.ContainerLayout,
-    zir_index: Zir.Inst.Index,
+    zir_index: TrackedInst.Index,
     fields_len: u32,
     known_non_opv: bool,
     requires_comptime: RequiresComptime,
@@ -5704,7 +5736,7 @@ pub fn getExternFunc(ip: *InternPool, gpa: Allocator, key: Key.ExternFunc) Alloc
 pub const GetFuncDeclKey = struct {
     owner_decl: DeclIndex,
     ty: Index,
-    zir_body_inst: Zir.Inst.Index,
+    zir_body_inst: TrackedInst.Index,
     lbrace_line: u32,
     rbrace_line: u32,
     lbrace_column: u32,
@@ -5773,7 +5805,7 @@ pub const GetFuncDeclIesKey = struct {
     is_var_args: bool,
     is_generic: bool,
     is_noinline: bool,
-    zir_body_inst: Zir.Inst.Index,
+    zir_body_inst: TrackedInst.Index,
     lbrace_line: u32,
     rbrace_line: u32,
     lbrace_column: u32,
@@ -6535,7 +6567,7 @@ fn addExtraAssumeCapacity(ip: *InternPool, extra: anytype) u32 {
             NullTerminatedString,
             OptionalNullTerminatedString,
             Tag.TypePointer.VectorIndex,
-            Zir.Inst.Index,
+            TrackedInst.Index,
             => @intFromEnum(@field(extra, field.name)),
 
             u32,
@@ -6611,7 +6643,7 @@ fn extraDataTrail(ip: *const InternPool, comptime T: type, index: usize) struct
             NullTerminatedString,
             OptionalNullTerminatedString,
             Tag.TypePointer.VectorIndex,
-            Zir.Inst.Index,
+            TrackedInst.Index,
             => @enumFromInt(int32),
 
             u32,
@@ -8317,7 +8349,7 @@ pub fn funcHasInferredErrorSet(ip: *const InternPool, i: Index) bool {
     return funcAnalysis(ip, i).inferred_error_set;
 }
 
-pub fn funcZirBodyInst(ip: *const InternPool, i: Index) Zir.Inst.Index {
+pub fn funcZirBodyInst(ip: *const InternPool, i: Index) TrackedInst.Index {
     assert(i != .none);
     const item = ip.items.get(@intFromEnum(i));
     const zir_body_inst_field_index = std.meta.fieldIndex(Tag.FuncDecl, "zir_body_inst").?;
src/Module.zig
@@ -834,6 +834,9 @@ pub const File = struct {
     multi_pkg: bool = false,
     /// List of references to this file, used for multi-package errors.
     references: std.ArrayListUnmanaged(Reference) = .{},
+    /// The hash of the path to this file, used to store `InternPool.TrackedInst`.
+    /// undefined until `zir_loaded == true`.
+    path_digest: Cache.BinDigest = undefined,
 
     /// Used by change detection algorithm, after astgen, contains the
     /// set of decls that existed in the previous ZIR but not in the new one.
@@ -2594,7 +2597,7 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
     const stat = try source_file.stat();
 
     const want_local_cache = file.mod == mod.main_mod;
-    const digest = hash: {
+    const bin_digest = hash: {
         var path_hash: Cache.HashHelper = .{};
         path_hash.addBytes(build_options.version);
         path_hash.add(builtin.zig_backend);
@@ -2603,7 +2606,19 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
             path_hash.addBytes(file.mod.root.sub_path);
         }
         path_hash.addBytes(file.sub_file_path);
-        break :hash path_hash.final();
+        var bin: Cache.BinDigest = undefined;
+        path_hash.hasher.final(&bin);
+        break :hash bin;
+    };
+    file.path_digest = bin_digest;
+    const hex_digest = hex: {
+        var hex: Cache.HexDigest = undefined;
+        _ = std.fmt.bufPrint(
+            &hex,
+            "{s}",
+            .{std.fmt.fmtSliceHexLower(&bin_digest)},
+        ) catch unreachable;
+        break :hex hex;
     };
     const cache_directory = if (want_local_cache) mod.local_zir_cache else mod.global_zir_cache;
     const zir_dir = cache_directory.handle;
@@ -2613,7 +2628,7 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
         .never_loaded, .retryable_failure => lock: {
             // First, load the cached ZIR code, if any.
             log.debug("AstGen checking cache: {s} (local={}, digest={s})", .{
-                file.sub_file_path, want_local_cache, &digest,
+                file.sub_file_path, want_local_cache, &hex_digest,
             });
 
             break :lock .shared;
@@ -2640,7 +2655,7 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
     // version. Likewise if we're working on AstGen and another process asks for
     // the cached file, they'll get it.
     const cache_file = while (true) {
-        break zir_dir.createFile(&digest, .{
+        break zir_dir.createFile(&hex_digest, .{
             .read = true,
             .truncate = false,
             .lock = lock,
@@ -2826,7 +2841,7 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
     };
     cache_file.writevAll(&iovecs) catch |err| {
         log.warn("unable to write cached ZIR code for {}{s} to {}{s}: {s}", .{
-            file.mod.root, file.sub_file_path, cache_directory, &digest, @errorName(err),
+            file.mod.root, file.sub_file_path, cache_directory, &hex_digest, @errorName(err),
         });
     };
 
@@ -2935,89 +2950,22 @@ fn loadZirCacheBody(gpa: Allocator, header: Zir.Header, cache_file: std.fs.File)
     return zir;
 }
 
-/// Patch ups:
-/// * Struct.zir_index
-/// * Decl.zir_index
-/// * Fn.zir_body_inst
-/// * Decl.zir_decl_index
-fn updateZirRefs(mod: *Module, file: *File, old_zir: Zir) !void {
-    const gpa = mod.gpa;
-    const new_zir = file.zir;
-
-    // The root decl will be null if the previous ZIR had AST errors.
-    const root_decl = file.root_decl.unwrap() orelse return;
+fn updateZirRefs(zcu: *Module, file: *File, old_zir: Zir) !void {
+    const gpa = zcu.gpa;
 
-    // Maps from old ZIR to new ZIR, declaration, struct_decl, enum_decl, etc. Any instruction which
-    // creates a namespace, and any `declaration` instruction, gets mapped from old to new here.
     var inst_map: std.AutoHashMapUnmanaged(Zir.Inst.Index, Zir.Inst.Index) = .{};
     defer inst_map.deinit(gpa);
 
-    try mapOldZirToNew(gpa, old_zir, new_zir, &inst_map);
-
-    // Walk the Decl graph, updating ZIR indexes, strings, and populating
-    // the deleted and outdated lists.
-
-    var decl_stack: ArrayListUnmanaged(Decl.Index) = .{};
-    defer decl_stack.deinit(gpa);
-
-    try decl_stack.append(gpa, root_decl);
-
-    file.deleted_decls.clearRetainingCapacity();
-    file.outdated_decls.clearRetainingCapacity();
-
-    // The root decl is always outdated; otherwise we would not have had
-    // to re-generate ZIR for the File.
-    try file.outdated_decls.append(gpa, root_decl);
-
-    const ip = &mod.intern_pool;
-
-    while (decl_stack.popOrNull()) |decl_index| {
-        const decl = mod.declPtr(decl_index);
-        // Anonymous decls and the root decl have this set to 0. We still need
-        // to walk them but we do not need to modify this value.
-        // Anonymous decls should not be marked outdated. They will be re-generated
-        // if their owner decl is marked outdated.
-        if (decl.zir_decl_index.unwrap()) |old_zir_decl_index| {
-            const new_zir_decl_index = inst_map.get(old_zir_decl_index) orelse {
-                try file.deleted_decls.append(gpa, decl_index);
-                continue;
-            };
-            const old_hash = decl.contentsHashZir(old_zir);
-            decl.zir_decl_index = new_zir_decl_index.toOptional();
-            const new_hash = decl.contentsHashZir(new_zir);
-            if (!std.zig.srcHashEql(old_hash, new_hash)) {
-                try file.outdated_decls.append(gpa, decl_index);
-            }
-        }
+    try mapOldZirToNew(gpa, old_zir, file.zir, &inst_map);
 
-        if (!decl.owns_tv) continue;
-
-        if (decl.getOwnedStruct(mod)) |struct_type| {
-            struct_type.setZirIndex(ip, inst_map.get(struct_type.zir_index) orelse {
-                try file.deleted_decls.append(gpa, decl_index);
-                continue;
-            });
-        }
-
-        if (decl.getOwnedUnion(mod)) |union_type| {
-            union_type.setZirIndex(ip, inst_map.get(union_type.zir_index) orelse {
-                try file.deleted_decls.append(gpa, decl_index);
-                continue;
-            });
-        }
-
-        if (decl.getOwnedFunction(mod)) |func| {
-            func.zirBodyInst(ip).* = inst_map.get(func.zir_body_inst) orelse {
-                try file.deleted_decls.append(gpa, decl_index);
-                continue;
-            };
-        }
-
-        if (decl.getOwnedInnerNamespace(mod)) |namespace| {
-            for (namespace.decls.keys()) |sub_decl| {
-                try decl_stack.append(gpa, sub_decl);
-            }
-        }
+    // TODO: this should be done after all AstGen workers complete, to avoid
+    // iterating over this full set for every updated file.
+    for (zcu.intern_pool.tracked_insts.keys()) |*ti| {
+        if (!std.mem.eql(u8, &ti.path_digest, &file.path_digest)) continue;
+        ti.inst = inst_map.get(ti.inst) orelse {
+            // TODO: invalidate this `TrackedInst` via the dependency mechanism
+            continue;
+        };
     }
 }
 
@@ -3494,7 +3442,7 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
     const struct_ty = sema.getStructType(
         new_decl_index,
         new_namespace_index,
-        .main_struct_inst,
+        try mod.intern_pool.trackZir(gpa, file, .main_struct_inst),
     ) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
     };
@@ -4472,7 +4420,7 @@ pub fn analyzeFnBody(mod: *Module, func_index: InternPool.Index, arena: Allocato
     };
     defer inner_block.instructions.deinit(gpa);
 
-    const fn_info = sema.code.getFnInfo(func.zirBodyInst(ip).*);
+    const fn_info = sema.code.getFnInfo(func.zirBodyInst(ip).resolve(ip));
 
     // Here we are performing "runtime semantic analysis" for a function body, which means
     // we must map the parameter ZIR instructions to `arg` AIR instructions.
@@ -6125,7 +6073,7 @@ pub fn getParamName(mod: *Module, func_index: InternPool.Index, index: u32) [:0]
     const tags = file.zir.instructions.items(.tag);
     const data = file.zir.instructions.items(.data);
 
-    const param_body = file.zir.getParamBody(func.zir_body_inst);
+    const param_body = file.zir.getParamBody(func.zir_body_inst.resolve(&mod.intern_pool));
     const param = param_body[index];
 
     return switch (tags[@intFromEnum(param)]) {
src/Sema.zig
@@ -2708,11 +2708,12 @@ pub fn getStructType(
     sema: *Sema,
     decl: InternPool.DeclIndex,
     namespace: InternPool.NamespaceIndex,
-    zir_index: Zir.Inst.Index,
+    tracked_inst: InternPool.TrackedInst.Index,
 ) !InternPool.Index {
     const mod = sema.mod;
     const gpa = sema.gpa;
     const ip = &mod.intern_pool;
+    const zir_index = tracked_inst.resolve(ip);
     const extended = sema.code.instructions.items(.data)[@intFromEnum(zir_index)].extended;
     assert(extended.opcode == .struct_decl);
     const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
@@ -2747,7 +2748,7 @@ pub fn getStructType(
     const ty = try ip.getStructType(gpa, .{
         .decl = decl,
         .namespace = namespace.toOptional(),
-        .zir_index = zir_index,
+        .zir_index = tracked_inst,
         .layout = small.layout,
         .known_non_opv = small.known_non_opv,
         .is_tuple = small.is_tuple,
@@ -2797,7 +2798,8 @@ fn zirStructDecl(
     errdefer mod.destroyNamespace(new_namespace_index);
 
     const struct_ty = ty: {
-        const ty = try sema.getStructType(new_decl_index, new_namespace_index, inst);
+        const tracked_inst = try ip.trackZir(mod.gpa, block.getFileScope(mod), inst);
+        const ty = try sema.getStructType(new_decl_index, new_namespace_index, tracked_inst);
         if (sema.builtin_type_target_index != .none) {
             ip.resolveBuiltinType(sema.builtin_type_target_index, ty);
             break :ty sema.builtin_type_target_index;
@@ -2856,7 +2858,7 @@ fn createAnonymousDeclTypeNamed(
             return new_decl_index;
         },
         .func => {
-            const fn_info = sema.code.getFnInfo(ip.funcZirBodyInst(sema.func_index));
+            const fn_info = sema.code.getFnInfo(ip.funcZirBodyInst(sema.func_index).resolve(ip));
             const zir_tags = sema.code.instructions.items(.tag);
 
             var buf = std.ArrayList(u8).init(gpa);
@@ -3252,7 +3254,7 @@ fn zirUnionDecl(
             },
             .decl = new_decl_index,
             .namespace = new_namespace_index,
-            .zir_index = inst,
+            .zir_index = try mod.intern_pool.trackZir(gpa, block.getFileScope(mod), inst),
             .fields_len = fields_len,
             .enum_tag_ty = .none,
             .field_types = &.{},
@@ -7446,7 +7448,7 @@ fn analyzeCall(
         // the AIR instructions of the callsite. The callee could be a generic function
         // which means its parameter type expressions must be resolved in order and used
         // to successively coerce the arguments.
-        const fn_info = ics.callee().code.getFnInfo(module_fn.zir_body_inst);
+        const fn_info = ics.callee().code.getFnInfo(module_fn.zir_body_inst.resolve(ip));
         try ics.callee().inst_map.ensureSpaceForInstructions(gpa, fn_info.param_body);
 
         var arg_i: u32 = 0;
@@ -7494,7 +7496,7 @@ fn analyzeCall(
         // each of the parameters, resolving the return type and providing it to the child
         // `Sema` so that it can be used for the `ret_ptr` instruction.
         const ret_ty_inst = if (fn_info.ret_ty_body.len != 0)
-            try sema.resolveBody(&child_block, fn_info.ret_ty_body, module_fn.zir_body_inst)
+            try sema.resolveBody(&child_block, fn_info.ret_ty_body, module_fn.zir_body_inst.resolve(ip))
         else
             try sema.resolveInst(fn_info.ret_ty_ref);
         const ret_ty_src: LazySrcLoc = .{ .node_offset_fn_type_ret_ty = 0 };
@@ -7885,7 +7887,7 @@ fn instantiateGenericCall(
     const namespace_index = fn_owner_decl.src_namespace;
     const namespace = mod.namespacePtr(namespace_index);
     const fn_zir = namespace.file_scope.zir;
-    const fn_info = fn_zir.getFnInfo(generic_owner_func.zir_body_inst);
+    const fn_info = fn_zir.getFnInfo(generic_owner_func.zir_body_inst.resolve(ip));
 
     const comptime_args = try sema.arena.alloc(InternPool.Index, args_info.count());
     @memset(comptime_args, .none);
@@ -9467,7 +9469,7 @@ fn funcCommon(
             .is_generic = final_is_generic,
             .is_noinline = is_noinline,
 
-            .zir_body_inst = func_inst,
+            .zir_body_inst = try ip.trackZir(gpa, block.getFileScope(mod), func_inst),
             .lbrace_line = src_locs.lbrace_line,
             .rbrace_line = src_locs.rbrace_line,
             .lbrace_column = @as(u16, @truncate(src_locs.columns)),
@@ -9545,7 +9547,7 @@ fn funcCommon(
             .ty = func_ty,
             .cc = cc,
             .is_noinline = is_noinline,
-            .zir_body_inst = func_inst,
+            .zir_body_inst = try ip.trackZir(gpa, block.getFileScope(mod), func_inst),
             .lbrace_line = src_locs.lbrace_line,
             .rbrace_line = src_locs.rbrace_line,
             .lbrace_column = @as(u16, @truncate(src_locs.columns)),
@@ -21553,7 +21555,7 @@ fn zirReify(
                 .namespace = new_namespace_index,
                 .enum_tag_ty = enum_tag_ty,
                 .fields_len = fields_len,
-                .zir_index = inst,
+                .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst), // TODO: should reified types be handled differently?
                 .flags = .{
                     .layout = layout,
                     .status = .have_field_types,
@@ -21721,7 +21723,7 @@ fn reifyStruct(
     const ty = try ip.getStructType(gpa, .{
         .decl = new_decl_index,
         .namespace = .none,
-        .zir_index = inst,
+        .zir_index = try mod.intern_pool.trackZir(gpa, block.getFileScope(mod), inst), // TODO: should reified types be handled differently?
         .layout = layout,
         .known_non_opv = false,
         .fields_len = fields_len,
@@ -35593,7 +35595,8 @@ fn semaBackingIntType(mod: *Module, struct_type: InternPool.Key.StructType) Comp
         break :blk accumulator;
     };
 
-    const extended = zir.instructions.items(.data)[@intFromEnum(struct_type.zir_index)].extended;
+    const zir_index = struct_type.zir_index.resolve(ip);
+    const extended = zir.instructions.items(.data)[@intFromEnum(zir_index)].extended;
     assert(extended.opcode == .struct_decl);
     const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
 
@@ -35613,7 +35616,7 @@ fn semaBackingIntType(mod: *Module, struct_type: InternPool.Key.StructType) Comp
                 break :blk try sema.resolveType(&block, backing_int_src, backing_int_ref);
             } else {
                 const body = zir.bodySlice(extra_index, backing_int_body_len);
-                const ty_ref = try sema.resolveBody(&block, body, struct_type.zir_index);
+                const ty_ref = try sema.resolveBody(&block, body, zir_index);
                 break :blk try sema.analyzeAsType(&block, backing_int_src, ty_ref);
             }
         };
@@ -36357,7 +36360,7 @@ fn semaStructFields(
     const decl = mod.declPtr(decl_index);
     const namespace_index = struct_type.namespace.unwrap() orelse decl.src_namespace;
     const zir = mod.namespacePtr(namespace_index).file_scope.zir;
-    const zir_index = struct_type.zir_index;
+    const zir_index = struct_type.zir_index.resolve(ip);
 
     const fields_len, const small, var extra_index = structZirInfo(zir, zir_index);
 
@@ -36628,7 +36631,7 @@ fn semaStructFieldInits(
     const decl = mod.declPtr(decl_index);
     const namespace_index = struct_type.namespace.unwrap() orelse decl.src_namespace;
     const zir = mod.namespacePtr(namespace_index).file_scope.zir;
-    const zir_index = struct_type.zir_index;
+    const zir_index = struct_type.zir_index.resolve(ip);
     const fields_len, const small, var extra_index = structZirInfo(zir, zir_index);
 
     var comptime_mutable_decls = std.ArrayList(InternPool.DeclIndex).init(gpa);
@@ -36777,7 +36780,8 @@ fn semaUnionFields(mod: *Module, arena: Allocator, union_type: InternPool.Key.Un
     const ip = &mod.intern_pool;
     const decl_index = union_type.decl;
     const zir = mod.namespacePtr(union_type.namespace).file_scope.zir;
-    const extended = zir.instructions.items(.data)[@intFromEnum(union_type.zir_index)].extended;
+    const zir_index = union_type.zir_index.resolve(ip);
+    const extended = zir.instructions.items(.data)[@intFromEnum(zir_index)].extended;
     assert(extended.opcode == .union_decl);
     const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
     var extra_index: usize = extended.operand;