Commit 3afda4322c

mlugg <mlugg@mlugg.co.uk>
2024-12-23 21:39:19
compiler: analyze type and value of global declaration separately
This commit separates semantic analysis of the annotated type vs value of a global declaration, therefore allowing recursive and mutually recursive values to be declared. Every `Nav` which undergoes analysis now has *two* corresponding `AnalUnit`s: `.{ .nav_val = n }` and `.{ .nav_ty = n }`. The `nav_val` unit is responsible for *fully resolving* the `Nav`: determining its value, linksection, addrspace, etc. The `nav_ty` unit, on the other hand, resolves only the information necessary to construct a *pointer* to the `Nav`: its type, addrspace, etc. (It does also analyze its linksection, but that could be moved to `nav_val` I think; it doesn't make any difference). Analyzing a `nav_ty` for a declaration with no type annotation will just mark a dependency on the `nav_val`, analyze it, and finish. Conversely, analyzing a `nav_val` for a declaration *with* a type annotation will first mark a dependency on the `nav_ty` and analyze it, using this as the result type when evaluating the value body. The `nav_val` and `nav_ty` units always have references to one another: so, if a `Nav`'s type is referenced, its value implicitly is too, and vice versa. However, these dependencies are trivial, so, to save memory, are only known implicitly by logic in `resolveReferences`. In general, analyzing ZIR `decl_val` will only analyze `nav_ty` of the corresponding `Nav`. There are two exceptions to this. If the declaration is an `extern` declaration, then we immediately ensure the `Nav` value is resolved (which doesn't actually require any more analysis, since such a declaration has no value body anyway). Additionally, if the resolved type has type tag `.@"fn"`, we again immediately resolve the `Nav` value. The latter restriction is in place for two reasons: * Functions are special, in that their externs are allowed to trivially alias; i.e. with a declaration `extern fn foo(...)`, you can write `const bar = foo;`. This is not allowed for non-function externs, and it means that function types are the only place where it is possible for a declaration `Nav` to have a `.@"extern"` value without actually being declared `extern`. We need to identify this situation immediately so that the `decl_ref` can create a pointer to the *real* extern `Nav`, not this alias. * In certain situations, such as taking a pointer to a `Nav`, Sema needs to queue analysis of a runtime function if the value is a function. To do this, the function value needs to be known, so we need to resolve the value immediately upon `&foo` where `foo` is a function. This restriction is simple to codify into the eventual language specification, and doesn't limit the utility of this feature in practice. A consequence of this commit is that codegen and linking logic needs to be more careful when looking at `Nav`s. In general: * When `updateNav` or `updateFunc` is called, it is safe to assume that the `Nav` being updated (the owner `Nav` for `updateFunc`) is fully resolved. * Any `Nav` whose value is/will be an `@"extern"` or a function is fully resolved; see `Nav.getExtern` for a helper for a common case here. * Any other `Nav` may only have its type resolved. This didn't seem to be too tricky to satisfy in any of the existing codegen/linker backends. Resolves: #131
1 parent 40aafcd
src/arch/wasm/CodeGen.zig
@@ -3218,15 +3218,7 @@ fn lowerNavRef(func: *CodeGen, nav_index: InternPool.Nav.Index, offset: u32) Inn
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
-    // check if decl is an alias to a function, in which case we
-    // want to lower the actual decl, rather than the alias itself.
-    const owner_nav = switch (ip.indexToKey(zcu.navValue(nav_index).toIntern())) {
-        .func => |function| function.owner_nav,
-        .variable => |variable| variable.owner_nav,
-        .@"extern" => |@"extern"| @"extern".owner_nav,
-        else => nav_index,
-    };
-    const nav_ty = ip.getNav(owner_nav).typeOf(ip);
+    const nav_ty = ip.getNav(nav_index).typeOf(ip);
     if (!ip.isFunctionType(nav_ty) and !Type.fromInterned(nav_ty).hasRuntimeBitsIgnoreComptime(zcu)) {
         return .{ .imm32 = 0xaaaaaaaa };
     }
src/codegen/c.zig
@@ -770,11 +770,14 @@ pub const DeclGen = struct {
         const ctype_pool = &dg.ctype_pool;
 
         // Chase function values in order to be able to reference the original function.
-        const owner_nav = switch (ip.indexToKey(zcu.navValue(nav_index).toIntern())) {
-            .variable => |variable| variable.owner_nav,
-            .func => |func| func.owner_nav,
-            .@"extern" => |@"extern"| @"extern".owner_nav,
-            else => nav_index,
+        const owner_nav = switch (ip.getNav(nav_index).status) {
+            .unresolved => unreachable,
+            .type_resolved => nav_index, // this can't be an extern or a function
+            .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
+                .func => |f| f.owner_nav,
+                .@"extern" => |e| e.owner_nav,
+                else => nav_index,
+            },
         };
 
         // Render an undefined pointer if we have a pointer to a zero-bit or comptime type.
@@ -2237,7 +2240,7 @@ pub const DeclGen = struct {
             Type.fromInterned(nav.typeOf(ip)),
             .{ .nav = nav_index },
             CQualifiers.init(.{ .@"const" = flags.is_const }),
-            nav.status.resolved.alignment,
+            nav.getAlignment(),
             .complete,
         );
         try fwd.writeAll(";\n");
@@ -2246,19 +2249,19 @@ pub const DeclGen = struct {
     fn renderNavName(dg: *DeclGen, writer: anytype, nav_index: InternPool.Nav.Index) !void {
         const zcu = dg.pt.zcu;
         const ip = &zcu.intern_pool;
-        switch (ip.indexToKey(zcu.navValue(nav_index).toIntern())) {
-            .@"extern" => |@"extern"| try writer.print("{ }", .{
+        const nav = ip.getNav(nav_index);
+        if (nav.getExtern(ip)) |@"extern"| {
+            try writer.print("{ }", .{
                 fmtIdent(ip.getNav(@"extern".owner_nav).name.toSlice(ip)),
-            }),
-            else => {
-                // MSVC has a limit of 4095 character token length limit, and fmtIdent can (worst case),
-                // expand to 3x the length of its input, but let's cut it off at a much shorter limit.
-                const fqn_slice = ip.getNav(nav_index).fqn.toSlice(ip);
-                try writer.print("{}__{d}", .{
-                    fmtIdent(fqn_slice[0..@min(fqn_slice.len, 100)]),
-                    @intFromEnum(nav_index),
-                });
-            },
+            });
+        } else {
+            // MSVC has a limit of 4095 character token length limit, and fmtIdent can (worst case),
+            // expand to 3x the length of its input, but let's cut it off at a much shorter limit.
+            const fqn_slice = ip.getNav(nav_index).fqn.toSlice(ip);
+            try writer.print("{}__{d}", .{
+                fmtIdent(fqn_slice[0..@min(fqn_slice.len, 100)]),
+                @intFromEnum(nav_index),
+            });
         }
     }
 
@@ -2826,7 +2829,7 @@ pub fn genLazyFn(o: *Object, lazy_ctype_pool: *const CType.Pool, lazy_fn: LazyFn
 
             const fwd = o.dg.fwdDeclWriter();
             try fwd.print("static zig_{s} ", .{@tagName(key)});
-            try o.dg.renderFunctionSignature(fwd, fn_val, ip.getNav(fn_nav_index).status.resolved.alignment, .forward, .{
+            try o.dg.renderFunctionSignature(fwd, fn_val, ip.getNav(fn_nav_index).getAlignment(), .forward, .{
                 .fmt_ctype_pool_string = fn_name,
             });
             try fwd.writeAll(";\n");
@@ -2867,13 +2870,13 @@ pub fn genFunc(f: *Function) !void {
     try o.dg.renderFunctionSignature(
         fwd,
         nav_val,
-        nav.status.resolved.alignment,
+        nav.status.fully_resolved.alignment,
         .forward,
         .{ .nav = nav_index },
     );
     try fwd.writeAll(";\n");
 
-    if (nav.status.resolved.@"linksection".toSlice(ip)) |s|
+    if (nav.status.fully_resolved.@"linksection".toSlice(ip)) |s|
         try o.writer().print("zig_linksection_fn({s}) ", .{fmtStringLiteral(s, null)});
     try o.dg.renderFunctionSignature(
         o.writer(),
@@ -2952,7 +2955,7 @@ pub fn genDecl(o: *Object) !void {
     const nav_ty = Type.fromInterned(nav.typeOf(ip));
 
     if (!nav_ty.isFnOrHasRuntimeBitsIgnoreComptime(zcu)) return;
-    switch (ip.indexToKey(nav.status.resolved.val)) {
+    switch (ip.indexToKey(nav.status.fully_resolved.val)) {
         .@"extern" => |@"extern"| {
             if (!ip.isFunctionType(nav_ty.toIntern())) return o.dg.renderFwdDecl(o.dg.pass.nav, .{
                 .is_extern = true,
@@ -2965,8 +2968,8 @@ pub fn genDecl(o: *Object) !void {
             try fwd.writeAll("zig_extern ");
             try o.dg.renderFunctionSignature(
                 fwd,
-                Value.fromInterned(nav.status.resolved.val),
-                nav.status.resolved.alignment,
+                Value.fromInterned(nav.status.fully_resolved.val),
+                nav.status.fully_resolved.alignment,
                 .forward,
                 .{ .@"export" = .{
                     .main_name = nav.name,
@@ -2985,14 +2988,14 @@ pub fn genDecl(o: *Object) !void {
             const w = o.writer();
             if (variable.is_weak_linkage) try w.writeAll("zig_weak_linkage ");
             if (variable.is_threadlocal and !o.dg.mod.single_threaded) try w.writeAll("zig_threadlocal ");
-            if (nav.status.resolved.@"linksection".toSlice(&zcu.intern_pool)) |s|
+            if (nav.status.fully_resolved.@"linksection".toSlice(&zcu.intern_pool)) |s|
                 try w.print("zig_linksection({s}) ", .{fmtStringLiteral(s, null)});
             try o.dg.renderTypeAndName(
                 w,
                 nav_ty,
                 .{ .nav = o.dg.pass.nav },
                 .{},
-                nav.status.resolved.alignment,
+                nav.status.fully_resolved.alignment,
                 .complete,
             );
             try w.writeAll(" = ");
@@ -3002,10 +3005,10 @@ pub fn genDecl(o: *Object) !void {
         },
         else => try genDeclValue(
             o,
-            Value.fromInterned(nav.status.resolved.val),
+            Value.fromInterned(nav.status.fully_resolved.val),
             .{ .nav = o.dg.pass.nav },
-            nav.status.resolved.alignment,
-            nav.status.resolved.@"linksection",
+            nav.status.fully_resolved.alignment,
+            nav.status.fully_resolved.@"linksection",
         ),
     }
 }
src/codegen/llvm.zig
@@ -1476,7 +1476,7 @@ pub const Object = struct {
             } }, &o.builder);
         }
 
-        if (nav.status.resolved.@"linksection".toSlice(ip)) |section|
+        if (nav.status.fully_resolved.@"linksection".toSlice(ip)) |section|
             function_index.setSection(try o.builder.string(section), &o.builder);
 
         var deinit_wip = true;
@@ -1684,7 +1684,7 @@ pub const Object = struct {
             const file = try o.getDebugFile(file_scope);
 
             const line_number = zcu.navSrcLine(func.owner_nav) + 1;
-            const is_internal_linkage = ip.indexToKey(nav.status.resolved.val) != .@"extern";
+            const is_internal_linkage = ip.indexToKey(nav.status.fully_resolved.val) != .@"extern";
             const debug_decl_type = try o.lowerDebugType(fn_ty);
 
             const subprogram = try o.builder.debugSubprogram(
@@ -2928,9 +2928,7 @@ pub const Object = struct {
         const gpa = o.gpa;
         const nav = ip.getNav(nav_index);
         const owner_mod = zcu.navFileScope(nav_index).mod;
-        const resolved = nav.status.resolved;
-        const val = Value.fromInterned(resolved.val);
-        const ty = val.typeOf(zcu);
+        const ty: Type = .fromInterned(nav.typeOf(ip));
         const gop = try o.nav_map.getOrPut(gpa, nav_index);
         if (gop.found_existing) return gop.value_ptr.ptr(&o.builder).kind.function;
 
@@ -2938,14 +2936,14 @@ pub const Object = struct {
         const target = owner_mod.resolved_target.result;
         const sret = firstParamSRet(fn_info, zcu, target);
 
-        const is_extern, const lib_name = switch (ip.indexToKey(val.toIntern())) {
-            .@"extern" => |@"extern"| .{ true, @"extern".lib_name },
-            else => .{ false, .none },
-        };
+        const is_extern, const lib_name = if (nav.getExtern(ip)) |@"extern"|
+            .{ true, @"extern".lib_name }
+        else
+            .{ false, .none };
         const function_index = try o.builder.addFunction(
             try o.lowerType(ty),
             try o.builder.strtabString((if (is_extern) nav.name else nav.fqn).toSlice(ip)),
-            toLlvmAddressSpace(resolved.@"addrspace", target),
+            toLlvmAddressSpace(nav.getAddrspace(), target),
         );
         gop.value_ptr.* = function_index.ptrConst(&o.builder).global;
 
@@ -3063,8 +3061,8 @@ pub const Object = struct {
             }
         }
 
-        if (resolved.alignment != .none)
-            function_index.setAlignment(resolved.alignment.toLlvm(), &o.builder);
+        if (nav.getAlignment() != .none)
+            function_index.setAlignment(nav.getAlignment().toLlvm(), &o.builder);
 
         // Function attributes that are independent of analysis results of the function body.
         try o.addCommonFnAttributes(
@@ -3249,17 +3247,21 @@ pub const Object = struct {
         const zcu = pt.zcu;
         const ip = &zcu.intern_pool;
         const nav = ip.getNav(nav_index);
-        const resolved = nav.status.resolved;
-        const is_extern, const is_threadlocal, const is_weak_linkage, const is_dll_import = switch (ip.indexToKey(resolved.val)) {
-            .variable => |variable| .{ false, variable.is_threadlocal, variable.is_weak_linkage, false },
-            .@"extern" => |@"extern"| .{ true, @"extern".is_threadlocal, @"extern".is_weak_linkage, @"extern".is_dll_import },
-            else => .{ false, false, false, false },
+        const is_extern, const is_threadlocal, const is_weak_linkage, const is_dll_import = switch (nav.status) {
+            .unresolved => unreachable,
+            .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
+                .variable => |variable| .{ false, variable.is_threadlocal, variable.is_weak_linkage, false },
+                .@"extern" => |@"extern"| .{ true, @"extern".is_threadlocal, @"extern".is_weak_linkage, @"extern".is_dll_import },
+                else => .{ false, false, false, false },
+            },
+            // This means it's a source declaration which is not `extern`!
+            .type_resolved => |r| .{ false, r.is_threadlocal, false, false },
         };
 
         const variable_index = try o.builder.addVariable(
             try o.builder.strtabString((if (is_extern) nav.name else nav.fqn).toSlice(ip)),
             try o.lowerType(Type.fromInterned(nav.typeOf(ip))),
-            toLlvmGlobalAddressSpace(resolved.@"addrspace", zcu.getTarget()),
+            toLlvmGlobalAddressSpace(nav.getAddrspace(), zcu.getTarget()),
         );
         gop.value_ptr.* = variable_index.ptrConst(&o.builder).global;
 
@@ -4528,20 +4530,10 @@ pub const Object = struct {
         const zcu = pt.zcu;
         const ip = &zcu.intern_pool;
 
-        // In the case of something like:
-        // fn foo() void {}
-        // const bar = foo;
-        // ... &bar;
-        // `bar` is just an alias and we actually want to lower a reference to `foo`.
-        const owner_nav_index = switch (ip.indexToKey(zcu.navValue(nav_index).toIntern())) {
-            .func => |func| func.owner_nav,
-            .@"extern" => |@"extern"| @"extern".owner_nav,
-            else => nav_index,
-        };
-        const owner_nav = ip.getNav(owner_nav_index);
+        const nav = ip.getNav(nav_index);
 
-        const nav_ty = Type.fromInterned(owner_nav.typeOf(ip));
-        const ptr_ty = try pt.navPtrType(owner_nav_index);
+        const nav_ty = Type.fromInterned(nav.typeOf(ip));
+        const ptr_ty = try pt.navPtrType(nav_index);
 
         const is_fn_body = nav_ty.zigTypeTag(zcu) == .@"fn";
         if ((!is_fn_body and !nav_ty.hasRuntimeBits(zcu)) or
@@ -4551,13 +4543,13 @@ pub const Object = struct {
         }
 
         const llvm_global = if (is_fn_body)
-            (try o.resolveLlvmFunction(owner_nav_index)).ptrConst(&o.builder).global
+            (try o.resolveLlvmFunction(nav_index)).ptrConst(&o.builder).global
         else
-            (try o.resolveGlobalNav(owner_nav_index)).ptrConst(&o.builder).global;
+            (try o.resolveGlobalNav(nav_index)).ptrConst(&o.builder).global;
 
         const llvm_val = try o.builder.convConst(
             llvm_global.toConst(),
-            try o.builder.ptrType(toLlvmAddressSpace(owner_nav.status.resolved.@"addrspace", zcu.getTarget())),
+            try o.builder.ptrType(toLlvmAddressSpace(nav.getAddrspace(), zcu.getTarget())),
         );
 
         return o.builder.convConst(llvm_val, try o.lowerType(ptr_ty));
@@ -4799,7 +4791,7 @@ pub const NavGen = struct {
         const ip = &zcu.intern_pool;
         const nav_index = ng.nav_index;
         const nav = ip.getNav(nav_index);
-        const resolved = nav.status.resolved;
+        const resolved = nav.status.fully_resolved;
 
         const is_extern, const lib_name, const is_threadlocal, const is_weak_linkage, const is_dll_import, const is_const, const init_val, const owner_nav = switch (ip.indexToKey(resolved.val)) {
             .variable => |variable| .{ false, .none, variable.is_threadlocal, variable.is_weak_linkage, false, false, variable.init, variable.owner_nav },
@@ -5765,7 +5757,7 @@ pub const FuncGen = struct {
         const msg_nav_index = zcu.panic_messages[@intFromEnum(panic_id)].unwrap().?;
         const msg_nav = ip.getNav(msg_nav_index);
         const msg_len = Type.fromInterned(msg_nav.typeOf(ip)).childType(zcu).arrayLen(zcu);
-        const msg_ptr = try o.lowerValue(msg_nav.status.resolved.val);
+        const msg_ptr = try o.lowerValue(msg_nav.status.fully_resolved.val);
         const null_opt_addr_global = try fg.resolveNullOptUsize();
         const target = zcu.getTarget();
         const llvm_usize = try o.lowerType(Type.usize);
src/codegen/spirv.zig
@@ -268,7 +268,7 @@ pub const Object = struct {
             // TODO: Extern fn?
             const kind: SpvModule.Decl.Kind = if (ip.isFunctionType(nav.typeOf(ip)))
                 .func
-            else switch (nav.status.resolved.@"addrspace") {
+            else switch (nav.getAddrspace()) {
                 .generic => .invocation_global,
                 else => .global,
             };
@@ -1279,17 +1279,20 @@ const NavGen = struct {
         const ip = &zcu.intern_pool;
         const ty_id = try self.resolveType(ty, .direct);
         const nav = ip.getNav(nav_index);
-        const nav_val = zcu.navValue(nav_index);
-        const nav_ty = nav_val.typeOf(zcu);
-
-        switch (ip.indexToKey(nav_val.toIntern())) {
-            .func => {
-                // TODO: Properly lower function pointers. For now we are going to hack around it and
-                // just generate an empty pointer. Function pointers are represented by a pointer to usize.
-                return try self.spv.constUndef(ty_id);
+        const nav_ty: Type = .fromInterned(nav.typeOf(ip));
+
+        switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => {}, // this is not a function or extern
+            .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
+                .func => {
+                    // TODO: Properly lower function pointers. For now we are going to hack around it and
+                    // just generate an empty pointer. Function pointers are represented by a pointer to usize.
+                    return try self.spv.constUndef(ty_id);
+                },
+                .@"extern" => if (ip.isFunctionType(nav_ty.toIntern())) @panic("TODO"),
+                else => {},
             },
-            .@"extern" => assert(!ip.isFunctionType(nav_ty.toIntern())), // TODO
-            else => {},
         }
 
         if (!nav_ty.isFnOrHasRuntimeBitsIgnoreComptime(zcu)) {
@@ -1305,7 +1308,7 @@ const NavGen = struct {
             .global, .invocation_global => spv_decl.result_id,
         };
 
-        const storage_class = self.spvStorageClass(nav.status.resolved.@"addrspace");
+        const storage_class = self.spvStorageClass(nav.getAddrspace());
         try self.addFunctionDep(spv_decl_index, storage_class);
 
         const decl_ptr_ty_id = try self.ptrType(nav_ty, storage_class);
@@ -3182,7 +3185,7 @@ const NavGen = struct {
                 };
                 assert(maybe_init_val == null); // TODO
 
-                const storage_class = self.spvStorageClass(nav.status.resolved.@"addrspace");
+                const storage_class = self.spvStorageClass(nav.getAddrspace());
                 assert(storage_class != .Generic); // These should be instance globals
 
                 const ptr_ty_id = try self.ptrType(ty, storage_class);
src/link/Elf/ZigObject.zig
@@ -925,14 +925,11 @@ pub fn getNavVAddr(
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
     log.debug("getNavVAddr {}({d})", .{ nav.fqn.fmt(ip), nav_index });
-    const this_sym_index = switch (ip.indexToKey(nav.status.resolved.val)) {
-        .@"extern" => |@"extern"| try self.getGlobalSymbol(
-            elf_file,
-            nav.name.toSlice(ip),
-            @"extern".lib_name.toSlice(ip),
-        ),
-        else => try self.getOrCreateMetadataForNav(zcu, nav_index),
-    };
+    const this_sym_index = if (nav.getExtern(ip)) |@"extern"| try self.getGlobalSymbol(
+        elf_file,
+        nav.name.toSlice(ip),
+        @"extern".lib_name.toSlice(ip),
+    ) else try self.getOrCreateMetadataForNav(zcu, nav_index);
     const this_sym = self.symbol(this_sym_index);
     const vaddr = this_sym.address(.{}, elf_file);
     switch (reloc_info.parent) {
@@ -1107,15 +1104,13 @@ pub fn freeNav(self: *ZigObject, elf_file: *Elf, nav_index: InternPool.Nav.Index
 
 pub fn getOrCreateMetadataForNav(self: *ZigObject, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Symbol.Index {
     const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
     const gop = try self.navs.getOrPut(gpa, nav_index);
     if (!gop.found_existing) {
         const symbol_index = try self.newSymbolWithAtom(gpa, 0);
-        const nav_val = Value.fromInterned(zcu.intern_pool.getNav(nav_index).status.resolved.val);
         const sym = self.symbol(symbol_index);
-        if (nav_val.getVariable(zcu)) |variable| {
-            if (variable.is_threadlocal and zcu.comp.config.any_non_single_threaded) {
-                sym.flags.is_tls = true;
-            }
+        if (ip.getNav(nav_index).isThreadlocal(ip) and zcu.comp.config.any_non_single_threaded) {
+            sym.flags.is_tls = true;
         }
         gop.value_ptr.* = .{ .symbol_index = symbol_index };
     }
@@ -1547,7 +1542,7 @@ pub fn updateNav(
 
     log.debug("updateNav {}({d})", .{ nav.fqn.fmt(ip), nav_index });
 
-    const nav_init = switch (ip.indexToKey(nav.status.resolved.val)) {
+    const nav_init = switch (ip.indexToKey(nav.status.fully_resolved.val)) {
         .func => .none,
         .variable => |variable| variable.init,
         .@"extern" => |@"extern"| {
@@ -1560,7 +1555,7 @@ pub fn updateNav(
             self.symbol(sym_index).flags.is_extern_ptr = true;
             return;
         },
-        else => nav.status.resolved.val,
+        else => nav.status.fully_resolved.val,
     };
 
     if (nav_init != .none and Value.fromInterned(nav_init).typeOf(zcu).hasRuntimeBits(zcu)) {
src/link/MachO/ZigObject.zig
@@ -608,14 +608,11 @@ pub fn getNavVAddr(
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
     log.debug("getNavVAddr {}({d})", .{ nav.fqn.fmt(ip), nav_index });
-    const sym_index = switch (ip.indexToKey(nav.status.resolved.val)) {
-        .@"extern" => |@"extern"| try self.getGlobalSymbol(
-            macho_file,
-            nav.name.toSlice(ip),
-            @"extern".lib_name.toSlice(ip),
-        ),
-        else => try self.getOrCreateMetadataForNav(macho_file, nav_index),
-    };
+    const sym_index = if (nav.getExtern(ip)) |@"extern"| try self.getGlobalSymbol(
+        macho_file,
+        nav.name.toSlice(ip),
+        @"extern".lib_name.toSlice(ip),
+    ) else try self.getOrCreateMetadataForNav(macho_file, nav_index);
     const sym = self.symbols.items[sym_index];
     const vaddr = sym.getAddress(.{}, macho_file);
     switch (reloc_info.parent) {
@@ -882,7 +879,7 @@ pub fn updateNav(
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
 
-    const nav_init = switch (ip.indexToKey(nav.status.resolved.val)) {
+    const nav_init = switch (ip.indexToKey(nav.status.fully_resolved.val)) {
         .func => .none,
         .variable => |variable| variable.init,
         .@"extern" => |@"extern"| {
@@ -895,7 +892,7 @@ pub fn updateNav(
             sym.flags.is_extern_ptr = true;
             return;
         },
-        else => nav.status.resolved.val,
+        else => nav.status.fully_resolved.val,
     };
 
     if (nav_init != .none and Value.fromInterned(nav_init).typeOf(zcu).hasRuntimeBits(zcu)) {
@@ -1561,11 +1558,7 @@ fn isThreadlocal(macho_file: *MachO, nav_index: InternPool.Nav.Index) bool {
     if (!macho_file.base.comp.config.any_non_single_threaded)
         return false;
     const ip = &macho_file.base.comp.zcu.?.intern_pool;
-    return switch (ip.indexToKey(ip.getNav(nav_index).status.resolved.val)) {
-        .variable => |variable| variable.is_threadlocal,
-        .@"extern" => |@"extern"| @"extern".is_threadlocal,
-        else => false,
-    };
+    return ip.getNav(nav_index).isThreadlocal(ip);
 }
 
 fn addAtom(self: *ZigObject, allocator: Allocator) !Atom.Index {
src/link/Wasm/ZigObject.zig
@@ -734,15 +734,14 @@ pub fn getNavVAddr(
     const target_atom_index = try zig_object.getOrCreateAtomForNav(wasm, pt, nav_index);
     const target_atom = wasm.getAtom(target_atom_index);
     const target_symbol_index = @intFromEnum(target_atom.sym_index);
-    switch (ip.indexToKey(nav.status.resolved.val)) {
-        .@"extern" => |@"extern"| try zig_object.addOrUpdateImport(
+    if (nav.getExtern(ip)) |@"extern"| {
+        try zig_object.addOrUpdateImport(
             wasm,
             nav.name.toSlice(ip),
             target_atom.sym_index,
             @"extern".lib_name.toSlice(ip),
             null,
-        ),
-        else => {},
+        );
     }
 
     std.debug.assert(reloc_info.parent.atom_index != 0);
@@ -945,8 +944,8 @@ pub fn freeNav(zig_object: *ZigObject, wasm: *Wasm, nav_index: InternPool.Nav.In
         segment.name = &.{}; // Ensure no accidental double free
     }
 
-    const nav_val = zcu.navValue(nav_index).toIntern();
-    if (ip.indexToKey(nav_val) == .@"extern") {
+    const nav = ip.getNav(nav_index);
+    if (nav.getExtern(ip) != null) {
         std.debug.assert(zig_object.imports.remove(atom.sym_index));
     }
     std.debug.assert(wasm.symbol_atom.remove(atom.symbolLoc()));
@@ -960,7 +959,7 @@ pub fn freeNav(zig_object: *ZigObject, wasm: *Wasm, nav_index: InternPool.Nav.In
     if (sym.isGlobal()) {
         std.debug.assert(zig_object.global_syms.remove(atom.sym_index));
     }
-    if (ip.isFunctionType(ip.typeOf(nav_val))) {
+    if (ip.isFunctionType(nav.typeOf(ip))) {
         zig_object.functions_free_list.append(gpa, sym.index) catch {};
         std.debug.assert(zig_object.atom_types.remove(atom_index));
     } else {
src/link/C.zig
@@ -217,7 +217,7 @@ pub fn updateFunc(
                 .mod = zcu.navFileScope(func.owner_nav).mod,
                 .error_msg = null,
                 .pass = .{ .nav = func.owner_nav },
-                .is_naked_fn = zcu.navValue(func.owner_nav).typeOf(zcu).fnCallingConvention(zcu) == .naked,
+                .is_naked_fn = Type.fromInterned(func.ty).fnCallingConvention(zcu) == .naked,
                 .fwd_decl = fwd_decl.toManaged(gpa),
                 .ctype_pool = ctype_pool.*,
                 .scratch = .{},
@@ -320,11 +320,11 @@ pub fn updateNav(self: *C, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) !
     const ip = &zcu.intern_pool;
 
     const nav = ip.getNav(nav_index);
-    const nav_init = switch (ip.indexToKey(nav.status.resolved.val)) {
+    const nav_init = switch (ip.indexToKey(nav.status.fully_resolved.val)) {
         .func => return,
         .@"extern" => .none,
         .variable => |variable| variable.init,
-        else => nav.status.resolved.val,
+        else => nav.status.fully_resolved.val,
     };
     if (nav_init != .none and !Value.fromInterned(nav_init).typeOf(zcu).hasRuntimeBits(zcu)) return;
 
@@ -499,7 +499,7 @@ pub fn flushModule(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
             av_block,
             self.exported_navs.getPtr(nav),
             export_names,
-            if (ip.indexToKey(zcu.navValue(nav).toIntern()) == .@"extern")
+            if (ip.getNav(nav).getExtern(ip) != null)
                 ip.getNav(nav).name.toOptional()
             else
                 .none,
@@ -544,13 +544,11 @@ pub fn flushModule(self: *C, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
         },
         self.getString(av_block.code),
     );
-    for (self.navs.keys(), self.navs.values()) |nav, av_block| f.appendCodeAssumeCapacity(
-        if (self.exported_navs.contains(nav)) .default else switch (ip.indexToKey(zcu.navValue(nav).toIntern())) {
-            .@"extern" => .zig_extern,
-            else => .static,
-        },
-        self.getString(av_block.code),
-    );
+    for (self.navs.keys(), self.navs.values()) |nav, av_block| f.appendCodeAssumeCapacity(storage: {
+        if (self.exported_navs.contains(nav)) break :storage .default;
+        if (ip.getNav(nav).getExtern(ip) != null) break :storage .zig_extern;
+        break :storage .static;
+    }, self.getString(av_block.code));
 
     const file = self.base.file.?;
     try file.setEndPos(f.file_size);
src/link/Coff.zig
@@ -1110,6 +1110,8 @@ pub fn updateFunc(coff: *Coff, pt: Zcu.PerThread, func_index: InternPool.Index,
     const atom_index = try coff.getOrCreateAtomForNav(func.owner_nav);
     coff.freeRelocations(atom_index);
 
+    coff.navs.getPtr(func.owner_nav).?.section = coff.text_section_index.?;
+
     var code_buffer = std.ArrayList(u8).init(gpa);
     defer code_buffer.deinit();
 
@@ -1223,6 +1225,8 @@ pub fn updateNav(
         coff.freeRelocations(atom_index);
         const atom = coff.getAtom(atom_index);
 
+        coff.navs.getPtr(nav_index).?.section = coff.getNavOutputSection(nav_index);
+
         var code_buffer = std.ArrayList(u8).init(gpa);
         defer code_buffer.deinit();
 
@@ -1342,7 +1346,8 @@ pub fn getOrCreateAtomForNav(coff: *Coff, nav_index: InternPool.Nav.Index) !Atom
     if (!gop.found_existing) {
         gop.value_ptr.* = .{
             .atom = try coff.createAtom(),
-            .section = coff.getNavOutputSection(nav_index),
+            // If necessary, this will be modified by `updateNav` or `updateFunc`.
+            .section = coff.rdata_section_index.?,
             .exports = .{},
         };
     }
@@ -1355,7 +1360,7 @@ fn getNavOutputSection(coff: *Coff, nav_index: InternPool.Nav.Index) u16 {
     const nav = ip.getNav(nav_index);
     const ty = Type.fromInterned(nav.typeOf(ip));
     const zig_ty = ty.zigTypeTag(zcu);
-    const val = Value.fromInterned(nav.status.resolved.val);
+    const val = Value.fromInterned(nav.status.fully_resolved.val);
     const index: u16 = blk: {
         if (val.isUndefDeep(zcu)) {
             // TODO in release-fast and release-small, we should put undef in .bss
@@ -2348,10 +2353,10 @@ pub fn getNavVAddr(
     const ip = &zcu.intern_pool;
     const nav = ip.getNav(nav_index);
     log.debug("getNavVAddr {}({d})", .{ nav.fqn.fmt(ip), nav_index });
-    const sym_index = switch (ip.indexToKey(nav.status.resolved.val)) {
-        .@"extern" => |@"extern"| try coff.getGlobalSymbol(nav.name.toSlice(ip), @"extern".lib_name.toSlice(ip)),
-        else => coff.getAtom(try coff.getOrCreateAtomForNav(nav_index)).getSymbolIndex().?,
-    };
+    const sym_index = if (nav.getExtern(ip)) |e|
+        try coff.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip))
+    else
+        coff.getAtom(try coff.getOrCreateAtomForNav(nav_index)).getSymbolIndex().?;
     const atom_index = coff.getAtomIndexForSymbol(.{
         .sym_index = reloc_info.parent.atom_index,
         .file = null,
src/link/Dwarf.zig
@@ -2281,7 +2281,7 @@ pub fn initWipNav(dwarf: *Dwarf, pt: Zcu.PerThread, nav_index: InternPool.Nav.In
             const nav_ty = nav_val.typeOf(zcu);
             const nav_ty_reloc_index = try wip_nav.refForward();
             try wip_nav.infoExprloc(.{ .addr = .{ .sym = sym_index } });
-            try uleb128(diw, nav.status.resolved.alignment.toByteUnits() orelse
+            try uleb128(diw, nav.status.fully_resolved.alignment.toByteUnits() orelse
                 nav_ty.abiAlignment(zcu).toByteUnits().?);
             try diw.writeByte(@intFromBool(false));
             wip_nav.finishForward(nav_ty_reloc_index);
@@ -2313,7 +2313,7 @@ pub fn initWipNav(dwarf: *Dwarf, pt: Zcu.PerThread, nav_index: InternPool.Nav.In
             try wip_nav.refType(ty);
             const addr: Loc = .{ .addr = .{ .sym = sym_index } };
             try wip_nav.infoExprloc(if (variable.is_threadlocal) .{ .form_tls_address = &addr } else addr);
-            try uleb128(diw, nav.status.resolved.alignment.toByteUnits() orelse
+            try uleb128(diw, nav.status.fully_resolved.alignment.toByteUnits() orelse
                 ty.abiAlignment(zcu).toByteUnits().?);
             try diw.writeByte(@intFromBool(false));
         },
@@ -2388,7 +2388,7 @@ pub fn initWipNav(dwarf: *Dwarf, pt: Zcu.PerThread, nav_index: InternPool.Nav.In
             wip_nav.func_high_pc = @intCast(wip_nav.debug_info.items.len);
             try diw.writeInt(u32, 0, dwarf.endian);
             const target = file.mod.resolved_target.result;
-            try uleb128(diw, switch (nav.status.resolved.alignment) {
+            try uleb128(diw, switch (nav.status.fully_resolved.alignment) {
                 .none => target_info.defaultFunctionAlignment(target),
                 else => |a| a.maxStrict(target_info.minFunctionAlignment(target)),
             }.toByteUnits().?);
@@ -2952,7 +2952,7 @@ pub fn updateComptimeNav(dwarf: *Dwarf, pt: Zcu.PerThread, nav_index: InternPool
             const nav_ty = nav_val.typeOf(zcu);
             try wip_nav.refType(nav_ty);
             try wip_nav.blockValue(nav_src_loc, nav_val);
-            try uleb128(diw, nav.status.resolved.alignment.toByteUnits() orelse
+            try uleb128(diw, nav.status.fully_resolved.alignment.toByteUnits() orelse
                 nav_ty.abiAlignment(zcu).toByteUnits().?);
             try diw.writeByte(@intFromBool(false));
         },
@@ -2977,7 +2977,7 @@ pub fn updateComptimeNav(dwarf: *Dwarf, pt: Zcu.PerThread, nav_index: InternPool
             try wip_nav.strp(nav.name.toSlice(ip));
             try wip_nav.strp(nav.fqn.toSlice(ip));
             const nav_ty_reloc_index = try wip_nav.refForward();
-            try uleb128(diw, nav.status.resolved.alignment.toByteUnits() orelse
+            try uleb128(diw, nav.status.fully_resolved.alignment.toByteUnits() orelse
                 nav_ty.abiAlignment(zcu).toByteUnits().?);
             try diw.writeByte(@intFromBool(false));
             if (has_runtime_bits) try wip_nav.blockValue(nav_src_loc, nav_val);
src/link/Plan9.zig
@@ -1021,7 +1021,7 @@ pub fn seeNav(self: *Plan9, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index)
     const atom_idx = gop.value_ptr.index;
     // handle externs here because they might not get updateDecl called on them
     const nav = ip.getNav(nav_index);
-    if (ip.indexToKey(nav.status.resolved.val) == .@"extern") {
+    if (nav.getExtern(ip) != null) {
         // this is a "phantom atom" - it is never actually written to disk, just convenient for us to store stuff about externs
         if (nav.name.eqlSlice("etext", ip)) {
             self.etext_edata_end_atom_indices[0] = atom_idx;
@@ -1370,7 +1370,7 @@ pub fn getNavVAddr(
     const ip = &pt.zcu.intern_pool;
     const nav = ip.getNav(nav_index);
     log.debug("getDeclVAddr for {}", .{nav.name.fmt(ip)});
-    if (ip.indexToKey(nav.status.resolved.val) == .@"extern") {
+    if (nav.getExtern(ip) != null) {
         if (nav.name.eqlSlice("etext", ip)) {
             try self.addReloc(reloc_info.parent.atom_index, .{
                 .target = undefined,
src/Sema/comptime_ptr_access.zig
@@ -219,9 +219,8 @@ fn loadComptimePtrInner(
 
     const base_val: MutableValue = switch (ptr.base_addr) {
         .nav => |nav| val: {
-            try sema.declareDependency(.{ .nav_val = nav });
-            try sema.ensureNavResolved(src, nav);
-            const val = ip.getNav(nav).status.resolved.val;
+            try sema.ensureNavResolved(src, nav, .fully);
+            const val = ip.getNav(nav).status.fully_resolved.val;
             switch (ip.indexToKey(val)) {
                 .variable => return .runtime_load,
                 // We let `.@"extern"` through here if it's a function.
src/Zcu/PerThread.zig
@@ -731,10 +731,12 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
 
+    _ = zcu.nav_val_analysis_queued.swapRemove(nav_id);
+
     const anal_unit: AnalUnit = .wrap(.{ .nav_val = nav_id });
     const nav = ip.getNav(nav_id);
 
-    log.debug("ensureNavUpToDate {}", .{zcu.fmtAnalUnit(anal_unit)});
+    log.debug("ensureNavValUpToDate {}", .{zcu.fmtAnalUnit(anal_unit)});
 
     // Determine whether or not this `Nav`'s value is outdated. This also includes checking if the
     // status is `.unresolved`, which indicates that the value is outdated because it has *never*
@@ -763,19 +765,19 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
     } else {
         // We can trust the current information about this unit.
         if (prev_failed) return error.AnalysisFail;
-        if (nav.status == .resolved) return;
+        switch (nav.status) {
+            .unresolved, .type_resolved => {},
+            .fully_resolved => return,
+        }
     }
 
     const unit_prog_node = zcu.sema_prog_node.start(nav.fqn.toSlice(ip), 0);
     defer unit_prog_node.end();
 
-    const sema_result: SemaNavResult, const new_failed: bool = if (pt.analyzeNavVal(nav_id)) |result| res: {
+    const invalidate_value: bool, const new_failed: bool = if (pt.analyzeNavVal(nav_id)) |result| res: {
         break :res .{
-            .{
-                // If the unit has gone from failed to success, we still need to invalidate the dependencies.
-                .invalidate_nav_val = result.invalidate_nav_val or prev_failed,
-                .invalidate_nav_ref = result.invalidate_nav_ref or prev_failed,
-            },
+            // If the unit has gone from failed to success, we still need to invalidate the dependencies.
+            result.val_changed or prev_failed,
             false,
         };
     } else |err| switch (err) {
@@ -786,10 +788,7 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
                 try zcu.transitive_failed_analysis.put(gpa, anal_unit, {});
                 log.debug("mark transitive analysis failure for {}", .{zcu.fmtAnalUnit(anal_unit)});
             }
-            break :res .{ .{
-                .invalidate_nav_val = !prev_failed,
-                .invalidate_nav_ref = !prev_failed,
-            }, true };
+            break :res .{ !prev_failed, true };
         },
         error.OutOfMemory => {
             // TODO: it's unclear how to gracefully handle this.
@@ -806,10 +805,8 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
     };
 
     if (was_outdated) {
-        // TODO: we do not yet have separate dependencies for Nav values vs types.
-        const invalidate = sema_result.invalidate_nav_val or sema_result.invalidate_nav_ref;
         const dependee: InternPool.Dependee = .{ .nav_val = nav_id };
-        if (invalidate) {
+        if (invalidate_value) {
             // This dependency was marked as PO, meaning dependees were waiting
             // on its analysis result, and it has turned out to be outdated.
             // Update dependees accordingly.
@@ -824,14 +821,7 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
     if (new_failed) return error.AnalysisFail;
 }
 
-const SemaNavResult = packed struct {
-    /// Whether the value of a `decl_val` of the corresponding Nav changed.
-    invalidate_nav_val: bool,
-    /// Whether the type of a `decl_ref` of the corresponding Nav changed.
-    invalidate_nav_ref: bool,
-};
-
-fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileError!SemaNavResult {
+fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileError!struct { val_changed: bool } {
     const zcu = pt.zcu;
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
@@ -875,9 +865,13 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     };
     defer sema.deinit();
 
-    // The comptime unit declares on the source of the corresponding declaration.
+    // Every `Nav` declares a dependency on the source of the corresponding declaration.
     try sema.declareDependency(.{ .src_hash = old_nav.analysis.?.zir_index });
 
+    // In theory, we would also add a reference to the corresponding `nav_val` unit here: there are
+    // always references in both directions between a `nav_val` and `nav_ty`. However, to save memory,
+    // these references are known implicitly. See logic in `Zcu.resolveReferences`.
+
     var block: Sema.Block = .{
         .parent = null,
         .sema = &sema,
@@ -891,31 +885,44 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     defer block.instructions.deinit(gpa);
 
     const zir_decl = zir.getDeclaration(inst_resolved.inst);
-
     assert(old_nav.is_usingnamespace == (zir_decl.kind == .@"usingnamespace"));
 
+    const ty_src = block.src(.{ .node_offset_var_decl_ty = 0 });
+    const init_src = block.src(.{ .node_offset_var_decl_init = 0 });
     const align_src = block.src(.{ .node_offset_var_decl_align = 0 });
     const section_src = block.src(.{ .node_offset_var_decl_section = 0 });
     const addrspace_src = block.src(.{ .node_offset_var_decl_addrspace = 0 });
-    const ty_src = block.src(.{ .node_offset_var_decl_ty = 0 });
-    const init_src = block.src(.{ .node_offset_var_decl_init = 0 });
+
+    const maybe_ty: ?Type = if (zir_decl.type_body != null) ty: {
+        // Since we have a type body, the type is resolved separately!
+        // Of course, we need to make sure we depend on it properly.
+        try sema.declareDependency(.{ .nav_ty = nav_id });
+        try pt.ensureNavTypeUpToDate(nav_id);
+        break :ty .fromInterned(ip.getNav(nav_id).status.type_resolved.type);
+    } else null;
+
+    const final_val: ?Value = if (zir_decl.value_body) |value_body| val: {
+        if (maybe_ty) |ty| {
+            // Put the resolved type into `inst_map` to be used as the result type of the init.
+            try sema.inst_map.ensureSpaceForInstructions(gpa, &.{inst_resolved.inst});
+            sema.inst_map.putAssumeCapacity(inst_resolved.inst, Air.internedToRef(ty.toIntern()));
+            const uncoerced_result_ref = try sema.resolveInlineBody(&block, value_body, inst_resolved.inst);
+            assert(sema.inst_map.remove(inst_resolved.inst));
+
+            const result_ref = try sema.coerce(&block, ty, uncoerced_result_ref, init_src);
+            break :val try sema.resolveFinalDeclValue(&block, init_src, result_ref);
+        } else {
+            // Just analyze the value; we have no type to offer.
+            const result_ref = try sema.resolveInlineBody(&block, value_body, inst_resolved.inst);
+            break :val try sema.resolveFinalDeclValue(&block, init_src, result_ref);
+        }
+    } else null;
+
+    const nav_ty: Type = maybe_ty orelse final_val.?.typeOf(zcu);
 
     // First, we must resolve the declaration's type. To do this, we analyze the type body if available,
     // or otherwise, we analyze the value body, populating `early_val` in the process.
 
-    const nav_ty: Type, const early_val: ?Value = if (zir_decl.type_body) |type_body| ty: {
-        // We evaluate only the type now; no need for the value yet.
-        const uncoerced_type_ref = try sema.resolveInlineBody(&block, type_body, inst_resolved.inst);
-        const type_ref = try sema.coerce(&block, .type, uncoerced_type_ref, ty_src);
-        break :ty .{ .fromInterned(type_ref.toInterned().?), null };
-    } else ty: {
-        // We don't have a type body, so we need to evaluate the value immediately.
-        const value_body = zir_decl.value_body.?;
-        const result_ref = try sema.resolveInlineBody(&block, value_body, inst_resolved.inst);
-        const val = try sema.resolveFinalDeclValue(&block, init_src, result_ref);
-        break :ty .{ val.typeOf(zcu), val };
-    };
-
     switch (zir_decl.kind) {
         .@"comptime" => unreachable, // this is not a Nav
         .unnamed_test, .@"test", .decltest => assert(nav_ty.zigTypeTag(zcu) == .@"fn"),
@@ -932,58 +939,24 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     // Now that we know the type, we can evaluate the alignment, linksection, and addrspace, to determine
     // the full pointer type of this declaration.
 
-    const alignment: InternPool.Alignment = a: {
-        const align_body = zir_decl.align_body orelse break :a .none;
-        const align_ref = try sema.resolveInlineBody(&block, align_body, inst_resolved.inst);
-        break :a try sema.analyzeAsAlign(&block, align_src, align_ref);
-    };
-
-    const @"linksection": InternPool.OptionalNullTerminatedString = ls: {
-        const linksection_body = zir_decl.linksection_body orelse break :ls .none;
-        const linksection_ref = try sema.resolveInlineBody(&block, linksection_body, inst_resolved.inst);
-        const bytes = try sema.toConstString(&block, section_src, linksection_ref, .{
-            .needed_comptime_reason = "linksection must be comptime-known",
-        });
-        if (std.mem.indexOfScalar(u8, bytes, 0) != null) {
-            return sema.fail(&block, section_src, "linksection cannot contain null bytes", .{});
-        } else if (bytes.len == 0) {
-            return sema.fail(&block, section_src, "linksection cannot be empty", .{});
-        }
-        break :ls try ip.getOrPutStringOpt(gpa, pt.tid, bytes, .no_embedded_nulls);
-    };
-
-    const @"addrspace": std.builtin.AddressSpace = as: {
-        const addrspace_ctx: Sema.AddressSpaceContext = switch (zir_decl.kind) {
-            .@"var" => .variable,
-            else => switch (nav_ty.zigTypeTag(zcu)) {
-                .@"fn" => .function,
-                else => .constant,
+    const modifiers: Sema.NavPtrModifiers = if (zir_decl.type_body != null) m: {
+        // `analyzeNavType` (from the `ensureNavTypeUpToDate` call above) has already populated this data into
+        // the `Nav`. Load the new one, and pull the modifiers out.
+        switch (ip.getNav(nav_id).status) {
+            .unresolved => unreachable, // `analyzeNavType` will never leave us in this state
+            inline .type_resolved, .fully_resolved => |r| break :m .{
+                .alignment = r.alignment,
+                .@"linksection" = r.@"linksection",
+                .@"addrspace" = r.@"addrspace",
             },
-        };
-        const target = zcu.getTarget();
-        const addrspace_body = zir_decl.addrspace_body orelse break :as switch (addrspace_ctx) {
-            .function => target_util.defaultAddressSpace(target, .function),
-            .variable => target_util.defaultAddressSpace(target, .global_mutable),
-            .constant => target_util.defaultAddressSpace(target, .global_constant),
-            else => unreachable,
-        };
-        const addrspace_ref = try sema.resolveInlineBody(&block, addrspace_body, inst_resolved.inst);
-        break :as try sema.analyzeAsAddressSpace(&block, addrspace_src, addrspace_ref, addrspace_ctx);
+        }
+    } else m: {
+        // `analyzeNavType` is essentially a stub which calls us. We are responsible for resolving this data.
+        break :m try sema.resolveNavPtrModifiers(&block, zir_decl, inst_resolved.inst, nav_ty);
     };
 
-    // Lastly, we must evaluate the value if we have not already done so. Note, however, that extern declarations
-    // don't have an associated value body.
-
-    const final_val: ?Value = early_val orelse if (zir_decl.value_body) |value_body| val: {
-        // Put the resolved type into `inst_map` to be used as the result type of the init.
-        try sema.inst_map.ensureSpaceForInstructions(gpa, &.{inst_resolved.inst});
-        sema.inst_map.putAssumeCapacity(inst_resolved.inst, Air.internedToRef(nav_ty.toIntern()));
-        const uncoerced_result_ref = try sema.resolveInlineBody(&block, value_body, inst_resolved.inst);
-        assert(sema.inst_map.remove(inst_resolved.inst));
-
-        const result_ref = try sema.coerce(&block, nav_ty, uncoerced_result_ref, init_src);
-        break :val try sema.resolveFinalDeclValue(&block, init_src, result_ref);
-    } else null;
+    // Lastly, we must figure out the actual interned value to store to the `Nav`.
+    // This isn't necessarily the same as `final_val`!
 
     const nav_val: Value = switch (zir_decl.linkage) {
         .normal, .@"export" => switch (zir_decl.kind) {
@@ -1013,8 +986,8 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
                 .is_threadlocal = zir_decl.is_threadlocal,
                 .is_weak_linkage = false,
                 .is_dll_import = false,
-                .alignment = alignment,
-                .@"addrspace" = @"addrspace",
+                .alignment = modifiers.alignment,
+                .@"addrspace" = modifiers.@"addrspace",
                 .zir_index = old_nav.analysis.?.zir_index, // `declaration` instruction
                 .owner_nav = undefined, // ignored by `getExtern`
             }));
@@ -1047,10 +1020,7 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
         });
         // TODO: usingnamespace cannot participate in incremental compilation
         assert(zcu.analysis_in_progress.swapRemove(anal_unit));
-        return .{
-            .invalidate_nav_val = true,
-            .invalidate_nav_ref = true,
-        };
+        return .{ .val_changed = true };
     }
 
     const queue_linker_work, const is_owned_fn = switch (ip.indexToKey(nav_val.toIntern())) {
@@ -1087,14 +1057,22 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
 
     ip.resolveNavValue(nav_id, .{
         .val = nav_val.toIntern(),
-        .alignment = alignment,
-        .@"linksection" = @"linksection",
-        .@"addrspace" = @"addrspace",
+        .alignment = modifiers.alignment,
+        .@"linksection" = modifiers.@"linksection",
+        .@"addrspace" = modifiers.@"addrspace",
     });
 
     // Mark the unit as completed before evaluating the export!
     assert(zcu.analysis_in_progress.swapRemove(anal_unit));
 
+    if (zir_decl.type_body == null) {
+        // In this situation, it's possible that we were triggered by `analyzeNavType` up the stack. In that
+        // case, we must also signal that the *type* is now populated to make this export behave correctly.
+        // An alternative strategy would be to just put something on the job queue to perform the export, but
+        // this is a little more straightforward, if perhaps less elegant.
+        _ = zcu.analysis_in_progress.swapRemove(.wrap(.{ .nav_ty = nav_id }));
+    }
+
     if (zir_decl.linkage == .@"export") {
         const export_src = block.src(.{ .token_offset = @intFromBool(zir_decl.is_pub) });
         const name_slice = zir.nullTerminatedString(zir_decl.name);
@@ -1117,21 +1095,246 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     }
 
     switch (old_nav.status) {
-        .unresolved => return .{
-            .invalidate_nav_val = true,
-            .invalidate_nav_ref = true,
+        .unresolved, .type_resolved => return .{ .val_changed = true },
+        .fully_resolved => |old| return .{ .val_changed = old.val != nav_val.toIntern() },
+    }
+}
+
+pub fn ensureNavTypeUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.SemaError!void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const anal_unit: AnalUnit = .wrap(.{ .nav_ty = nav_id });
+    const nav = ip.getNav(nav_id);
+
+    log.debug("ensureNavTypeUpToDate {}", .{zcu.fmtAnalUnit(anal_unit)});
+
+    // Determine whether or not this `Nav`'s type is outdated. This also includes checking if the
+    // status is `.unresolved`, which indicates that the value is outdated because it has *never*
+    // been analyzed so far.
+    //
+    // Note that if the unit is PO, we pessimistically assume that it *does* require re-analysis, to
+    // ensure that the unit is definitely up-to-date when this function returns. This mechanism could
+    // result in over-analysis if analysis occurs in a poor order; we do our best to avoid this by
+    // carefully choosing which units to re-analyze. See `Zcu.findOutdatedToAnalyze`.
+
+    const was_outdated = zcu.outdated.swapRemove(anal_unit) or
+        zcu.potentially_outdated.swapRemove(anal_unit);
+
+    const prev_failed = zcu.failed_analysis.contains(anal_unit) or
+        zcu.transitive_failed_analysis.contains(anal_unit);
+
+    if (was_outdated) {
+        dev.check(.incremental);
+        _ = zcu.outdated_ready.swapRemove(anal_unit);
+        zcu.deleteUnitExports(anal_unit);
+        zcu.deleteUnitReferences(anal_unit);
+        if (zcu.failed_analysis.fetchSwapRemove(anal_unit)) |kv| {
+            kv.value.destroy(gpa);
+        }
+        _ = zcu.transitive_failed_analysis.swapRemove(anal_unit);
+    } else {
+        // We can trust the current information about this unit.
+        if (prev_failed) return error.AnalysisFail;
+        switch (nav.status) {
+            .unresolved => {},
+            .type_resolved, .fully_resolved => return,
+        }
+    }
+
+    const unit_prog_node = zcu.sema_prog_node.start(nav.fqn.toSlice(ip), 0);
+    defer unit_prog_node.end();
+
+    const invalidate_type: bool, const new_failed: bool = if (pt.analyzeNavType(nav_id)) |result| res: {
+        break :res .{
+            // If the unit has gone from failed to success, we still need to invalidate the dependencies.
+            result.type_changed or prev_failed,
+            false,
+        };
+    } else |err| switch (err) {
+        error.AnalysisFail => res: {
+            if (!zcu.failed_analysis.contains(anal_unit)) {
+                // If this unit caused the error, it would have an entry in `failed_analysis`.
+                // Since it does not, this must be a transitive failure.
+                try zcu.transitive_failed_analysis.put(gpa, anal_unit, {});
+                log.debug("mark transitive analysis failure for {}", .{zcu.fmtAnalUnit(anal_unit)});
+            }
+            break :res .{ !prev_failed, true };
         },
-        .resolved => |old| {
-            const new = ip.getNav(nav_id).status.resolved;
-            return .{
-                .invalidate_nav_val = new.val != old.val,
-                .invalidate_nav_ref = ip.typeOf(new.val) != ip.typeOf(old.val) or
-                    new.alignment != old.alignment or
-                    new.@"linksection" != old.@"linksection" or
-                    new.@"addrspace" != old.@"addrspace",
-            };
+        error.OutOfMemory => {
+            // TODO: it's unclear how to gracefully handle this.
+            // To report the error cleanly, we need to add a message to `failed_analysis` and a
+            // corresponding entry to `retryable_failures`; but either of these things is quite
+            // likely to OOM at this point.
+            // If that happens, what do we do? Perhaps we could have a special field on `Zcu`
+            // for reporting OOM errors without allocating.
+            return error.OutOfMemory;
         },
+        error.GenericPoison => unreachable,
+        error.ComptimeReturn => unreachable,
+        error.ComptimeBreak => unreachable,
+    };
+
+    if (was_outdated) {
+        const dependee: InternPool.Dependee = .{ .nav_ty = nav_id };
+        if (invalidate_type) {
+            // This dependency was marked as PO, meaning dependees were waiting
+            // on its analysis result, and it has turned out to be outdated.
+            // Update dependees accordingly.
+            try zcu.markDependeeOutdated(.marked_po, dependee);
+        } else {
+            // This dependency was previously PO, but turned out to be up-to-date.
+            // We do not need to queue successive analysis.
+            try zcu.markPoDependeeUpToDate(dependee);
+        }
     }
+
+    if (new_failed) return error.AnalysisFail;
+}
+
+fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileError!struct { type_changed: bool } {
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const anal_unit: AnalUnit = .wrap(.{ .nav_ty = nav_id });
+    const old_nav = ip.getNav(nav_id);
+
+    log.debug("analyzeNavType {}", .{zcu.fmtAnalUnit(anal_unit)});
+
+    const inst_resolved = old_nav.analysis.?.zir_index.resolveFull(ip) orelse return error.AnalysisFail;
+    const file = zcu.fileByIndex(inst_resolved.file);
+    // TODO: stop the compiler ever reaching Sema if there are failed files. That way, this check is
+    // unnecessary, and we can move the below `removeDependenciesForDepender` call up with its friends
+    // in `ensureComptimeUnitUpToDate`.
+    if (file.status != .success_zir) return error.AnalysisFail;
+    const zir = file.zir;
+
+    // We are about to re-analyze this unit; drop its depenndencies.
+    zcu.intern_pool.removeDependenciesForDepender(gpa, anal_unit);
+
+    try zcu.analysis_in_progress.put(gpa, anal_unit, {});
+    defer _ = zcu.analysis_in_progress.swapRemove(anal_unit);
+
+    var analysis_arena: std.heap.ArenaAllocator = .init(gpa);
+    defer analysis_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 = analysis_arena.allocator(),
+        .code = zir,
+        .owner = anal_unit,
+        .func_index = .none,
+        .func_is_naked = false,
+        .fn_ret_ty = .void,
+        .fn_ret_ty_ies = null,
+        .comptime_err_ret_trace = &comptime_err_ret_trace,
+    };
+    defer sema.deinit();
+
+    // Every `Nav` declares a dependency on the source of the corresponding declaration.
+    try sema.declareDependency(.{ .src_hash = old_nav.analysis.?.zir_index });
+
+    // In theory, we would also add a reference to the corresponding `nav_val` unit here: there are
+    // always references in both directions between a `nav_val` and `nav_ty`. However, to save memory,
+    // these references are known implicitly. See logic in `Zcu.resolveReferences`.
+
+    var block: Sema.Block = .{
+        .parent = null,
+        .sema = &sema,
+        .namespace = old_nav.analysis.?.namespace,
+        .instructions = .{},
+        .inlining = null,
+        .is_comptime = true,
+        .src_base_inst = old_nav.analysis.?.zir_index,
+        .type_name_ctx = old_nav.fqn,
+    };
+    defer block.instructions.deinit(gpa);
+
+    const zir_decl = zir.getDeclaration(inst_resolved.inst);
+    assert(old_nav.is_usingnamespace == (zir_decl.kind == .@"usingnamespace"));
+
+    const type_body = zir_decl.type_body orelse {
+        // The type of this `Nav` is inferred from the value.
+        // In other words, this `nav_ty` depends on the corresponding `nav_val`.
+        try sema.declareDependency(.{ .nav_val = nav_id });
+        try pt.ensureNavValUpToDate(nav_id);
+        // Note that the above call, if it did any work, has removed our `analysis_in_progress` entry for us.
+        // (Our `defer` will run anyway, but it does nothing in this case.)
+
+        // There's not a great way for us to know whether the type actually changed.
+        // For instance, perhaps the `nav_val` was already up-to-date, but this `nav_ty` is being
+        // analyzed because this declaration had a type annotation on the *previous* update.
+        // However, such cases are rare, and it's not unreasonable to re-analyze in them; and in
+        // other cases where we get here, it's because the `nav_val` was already re-analyzed and
+        // is outdated.
+        return .{ .type_changed = true };
+    };
+
+    const ty_src = block.src(.{ .node_offset_var_decl_ty = 0 });
+
+    const resolved_ty: Type = ty: {
+        const uncoerced_type_ref = try sema.resolveInlineBody(&block, type_body, inst_resolved.inst);
+        const type_ref = try sema.coerce(&block, .type, uncoerced_type_ref, ty_src);
+        break :ty .fromInterned(type_ref.toInterned().?);
+    };
+
+    // In the case where the type is specified, this function is also responsible for resolving
+    // the pointer modifiers, i.e. alignment, linksection, addrspace.
+    const modifiers = try sema.resolveNavPtrModifiers(&block, zir_decl, inst_resolved.inst, resolved_ty);
+
+    // Usually, we can infer this information from the resolved `Nav` value; see `Zcu.navValIsConst`.
+    // However, since we don't have one, we need to quickly check the ZIR to figure this out.
+    const is_const = switch (zir_decl.kind) {
+        .@"comptime" => unreachable,
+        .unnamed_test, .@"test", .decltest, .@"usingnamespace", .@"const" => true,
+        .@"var" => false,
+    };
+
+    const is_extern_decl = zir_decl.linkage == .@"extern";
+
+    // Now for the question of the day: are the type and modifiers the same as before?
+    // If they are, then we should actually keep the `Nav` as `fully_resolved` if it currently is.
+    // That's because `analyzeNavVal` will later want to look at the resolved value to figure out
+    // whether it's changed: if we threw that data away now, it would have to assume that the value
+    // had changed, potentially spinning off loads of unnecessary re-analysis!
+    const changed = switch (old_nav.status) {
+        .unresolved => true,
+        .type_resolved => |r| r.type != resolved_ty.toIntern() or
+            r.alignment != modifiers.alignment or
+            r.@"linksection" != modifiers.@"linksection" or
+            r.@"addrspace" != modifiers.@"addrspace" or
+            r.is_const != is_const or
+            r.is_extern_decl != is_extern_decl,
+        .fully_resolved => |r| ip.typeOf(r.val) != resolved_ty.toIntern() or
+            r.alignment != modifiers.alignment or
+            r.@"linksection" != modifiers.@"linksection" or
+            r.@"addrspace" != modifiers.@"addrspace" or
+            zcu.navValIsConst(r.val) != is_const or
+            (old_nav.getExtern(ip) != null) != is_extern_decl,
+    };
+
+    if (!changed) return .{ .type_changed = false };
+
+    ip.resolveNavType(nav_id, .{
+        .type = resolved_ty.toIntern(),
+        .alignment = modifiers.alignment,
+        .@"linksection" = modifiers.@"linksection",
+        .@"addrspace" = modifiers.@"addrspace",
+        .is_const = is_const,
+        .is_threadlocal = zir_decl.is_threadlocal,
+        .is_extern_decl = is_extern_decl,
+    });
+
+    return .{ .type_changed = true };
 }
 
 pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: InternPool.Index) Zcu.SemaError!void {
@@ -1144,6 +1347,8 @@ pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
     const gpa = zcu.gpa;
     const ip = &zcu.intern_pool;
 
+    _ = zcu.func_body_analysis_queued.swapRemove(maybe_coerced_func_index);
+
     // We only care about the uncoerced function.
     const func_index = ip.unwrapCoercedFunc(maybe_coerced_func_index);
     const anal_unit: AnalUnit = .wrap(.{ .func = func_index });
@@ -1171,11 +1376,7 @@ pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
         if (prev_failed) {
             return error.AnalysisFail;
         }
-        switch (func.analysisUnordered(ip).state) {
-            .unreferenced => {}, // this is the first reference
-            .queued => {}, // we're waiting on first-time analysis
-            .analyzed => return, // up-to-date
-        }
+        if (func.analysisUnordered(ip).is_analyzed) return;
     }
 
     const func_prog_node = zcu.sema_prog_node.start(ip.getNav(func.owner_nav).fqn.toSlice(ip), 0);
@@ -1236,7 +1437,7 @@ fn analyzeFuncBody(
     if (func.generic_owner == .none) {
         // Among another things, this ensures that the function's `zir_body_inst` is correct.
         try pt.ensureNavValUpToDate(func.owner_nav);
-        if (ip.getNav(func.owner_nav).status.resolved.val != func_index) {
+        if (ip.getNav(func.owner_nav).status.fully_resolved.val != func_index) {
             // This function is no longer referenced! There's no point in re-analyzing it.
             // Just mark a transitive failure and move on.
             return error.AnalysisFail;
@@ -1245,7 +1446,7 @@ fn analyzeFuncBody(
         const go_nav = zcu.funcInfo(func.generic_owner).owner_nav;
         // Among another things, this ensures that the function's `zir_body_inst` is correct.
         try pt.ensureNavValUpToDate(go_nav);
-        if (ip.getNav(go_nav).status.resolved.val != func.generic_owner) {
+        if (ip.getNav(go_nav).status.fully_resolved.val != func.generic_owner) {
             // The generic owner is no longer referenced, so this function is also unreferenced.
             // There's no point in re-analyzing it. Just mark a transitive failure and move on.
             return error.AnalysisFail;
@@ -2172,7 +2373,7 @@ fn analyzeFnBodyInner(pt: Zcu.PerThread, func_index: InternPool.Index) Zcu.SemaE
     try zcu.analysis_in_progress.put(gpa, anal_unit, {});
     errdefer _ = zcu.analysis_in_progress.swapRemove(anal_unit);
 
-    func.setAnalysisState(ip, .analyzed);
+    func.setAnalyzed(ip);
     if (func.analysisUnordered(ip).inferred_error_set) {
         func.setResolvedErrorSet(ip, .none);
     }
@@ -2550,8 +2751,8 @@ fn processExportsInner(
                 if (zcu.transitive_failed_analysis.contains(unit)) break :failed true;
             }
             const val = switch (nav.status) {
-                .unresolved => break :failed true,
-                .resolved => |r| Value.fromInterned(r.val),
+                .unresolved, .type_resolved => break :failed true,
+                .fully_resolved => |r| Value.fromInterned(r.val),
             };
             // If the value is a function, we also need to check if that function succeeded analysis.
             if (val.typeOf(zcu).zigTypeTag(zcu) == .@"fn") {
@@ -3256,30 +3457,29 @@ pub fn getBuiltinNav(pt: Zcu.PerThread, name: []const u8) Allocator.Error!Intern
     const builtin_nav = std_namespace.pub_decls.getKeyAdapted(builtin_str, Zcu.Namespace.NameAdapter{ .zcu = zcu }) orelse
         @panic("lib/std.zig is corrupt and missing 'builtin'");
     pt.ensureNavValUpToDate(builtin_nav) catch @panic("std.builtin is corrupt");
-    const builtin_type = Type.fromInterned(ip.getNav(builtin_nav).status.resolved.val);
+    const builtin_type = Type.fromInterned(ip.getNav(builtin_nav).status.fully_resolved.val);
     const builtin_namespace = zcu.namespacePtr(builtin_type.getNamespace(zcu).unwrap() orelse @panic("std.builtin is corrupt"));
     const name_str = try ip.getOrPutString(gpa, pt.tid, name, .no_embedded_nulls);
     return builtin_namespace.pub_decls.getKeyAdapted(name_str, Zcu.Namespace.NameAdapter{ .zcu = zcu }) orelse @panic("lib/std/builtin.zig is corrupt");
 }
 
-pub fn navPtrType(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) Allocator.Error!Type {
+pub fn navPtrType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Allocator.Error!Type {
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
-    const r = ip.getNav(nav_index).status.resolved;
-    const ty = Value.fromInterned(r.val).typeOf(zcu);
+    const ty, const alignment, const @"addrspace", const is_const = switch (ip.getNav(nav_id).status) {
+        .unresolved => unreachable,
+        .type_resolved => |r| .{ r.type, r.alignment, r.@"addrspace", r.is_const },
+        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", zcu.navValIsConst(r.val) },
+    };
     return pt.ptrType(.{
-        .child = ty.toIntern(),
+        .child = ty,
         .flags = .{
-            .alignment = if (r.alignment == ty.abiAlignment(zcu))
+            .alignment = if (alignment == Type.fromInterned(ty).abiAlignment(zcu))
                 .none
             else
-                r.alignment,
-            .address_space = r.@"addrspace",
-            .is_const = switch (ip.indexToKey(r.val)) {
-                .variable => false,
-                .@"extern" => |e| e.is_const,
-                else => true,
-            },
+                alignment,
+            .address_space = @"addrspace",
+            .is_const = is_const,
         },
     });
 }
@@ -3299,9 +3499,13 @@ pub fn getExtern(pt: Zcu.PerThread, key: InternPool.Key.Extern) Allocator.Error!
 // TODO: this shouldn't need a `PerThread`! Fix the signature of `Type.abiAlignment`.
 pub fn navAlignment(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) InternPool.Alignment {
     const zcu = pt.zcu;
-    const r = zcu.intern_pool.getNav(nav_index).status.resolved;
-    if (r.alignment != .none) return r.alignment;
-    return Value.fromInterned(r.val).typeOf(zcu).abiAlignment(zcu);
+    const ty: Type, const alignment = switch (zcu.intern_pool.getNav(nav_index).status) {
+        .unresolved => unreachable,
+        .type_resolved => |r| .{ .fromInterned(r.type), r.alignment },
+        .fully_resolved => |r| .{ Value.fromInterned(r.val).typeOf(zcu), r.alignment },
+    };
+    if (alignment != .none) return alignment;
+    return ty.abiAlignment(zcu);
 }
 
 /// Given a container type requiring resolution, ensures that it is up-to-date.
src/codegen.zig
@@ -817,7 +817,7 @@ fn genNavRef(
     pt: Zcu.PerThread,
     src_loc: Zcu.LazySrcLoc,
     val: Value,
-    ref_nav_index: InternPool.Nav.Index,
+    nav_index: InternPool.Nav.Index,
     target: std.Target,
 ) CodeGenError!GenResult {
     const zcu = pt.zcu;
@@ -851,14 +851,15 @@ fn genNavRef(
         }
     }
 
-    const nav_index, const is_extern, const lib_name, const is_threadlocal = switch (ip.indexToKey(zcu.navValue(ref_nav_index).toIntern())) {
-        .func => |func| .{ func.owner_nav, false, .none, false },
-        .variable => |variable| .{ variable.owner_nav, false, .none, variable.is_threadlocal },
-        .@"extern" => |@"extern"| .{ @"extern".owner_nav, true, @"extern".lib_name, @"extern".is_threadlocal },
-        else => .{ ref_nav_index, false, .none, false },
-    };
+    const nav = ip.getNav(nav_index);
+
+    const is_extern, const lib_name, const is_threadlocal = if (nav.getExtern(ip)) |e|
+        .{ true, e.lib_name, e.is_threadlocal }
+    else
+        .{ false, .none, nav.isThreadlocal(ip) };
+
     const single_threaded = zcu.navFileScope(nav_index).mod.single_threaded;
-    const name = ip.getNav(nav_index).name;
+    const name = nav.name;
     if (lf.cast(.elf)) |elf_file| {
         const zo = elf_file.zigObjectPtr().?;
         if (is_extern) {
src/Compilation.zig
@@ -2906,6 +2906,7 @@ const Header = extern struct {
         file_deps_len: u32,
         src_hash_deps_len: u32,
         nav_val_deps_len: u32,
+        nav_ty_deps_len: u32,
         namespace_deps_len: u32,
         namespace_name_deps_len: u32,
         first_dependency_len: u32,
@@ -2949,6 +2950,7 @@ pub fn saveState(comp: *Compilation) !void {
                 .file_deps_len = @intCast(ip.file_deps.count()),
                 .src_hash_deps_len = @intCast(ip.src_hash_deps.count()),
                 .nav_val_deps_len = @intCast(ip.nav_val_deps.count()),
+                .nav_ty_deps_len = @intCast(ip.nav_ty_deps.count()),
                 .namespace_deps_len = @intCast(ip.namespace_deps.count()),
                 .namespace_name_deps_len = @intCast(ip.namespace_name_deps.count()),
                 .first_dependency_len = @intCast(ip.first_dependency.count()),
@@ -2979,6 +2981,8 @@ pub fn saveState(comp: *Compilation) !void {
         addBuf(&bufs, mem.sliceAsBytes(ip.src_hash_deps.values()));
         addBuf(&bufs, mem.sliceAsBytes(ip.nav_val_deps.keys()));
         addBuf(&bufs, mem.sliceAsBytes(ip.nav_val_deps.values()));
+        addBuf(&bufs, mem.sliceAsBytes(ip.nav_ty_deps.keys()));
+        addBuf(&bufs, mem.sliceAsBytes(ip.nav_ty_deps.values()));
         addBuf(&bufs, mem.sliceAsBytes(ip.namespace_deps.keys()));
         addBuf(&bufs, mem.sliceAsBytes(ip.namespace_deps.values()));
         addBuf(&bufs, mem.sliceAsBytes(ip.namespace_name_deps.keys()));
@@ -3145,7 +3149,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
 
             const file_index = switch (anal_unit.unwrap()) {
                 .@"comptime" => |cu| ip.getComptimeUnit(cu).zir_index.resolveFile(ip),
-                .nav_val => |nav| ip.getNav(nav).analysis.?.zir_index.resolveFile(ip),
+                .nav_val, .nav_ty => |nav| ip.getNav(nav).analysis.?.zir_index.resolveFile(ip),
                 .type => |ty| Type.fromInterned(ty).typeDeclInst(zcu).?.resolveFile(ip),
                 .func => |ip_index| zcu.funcInfo(ip_index).zir_body_inst.resolveFile(ip),
             };
@@ -3380,7 +3384,7 @@ pub fn addModuleErrorMsg(
                 defer gpa.free(rt_file_path);
                 const name = switch (ref.referencer.unwrap()) {
                     .@"comptime" => "comptime",
-                    .nav_val => |nav| ip.getNav(nav).name.toSlice(ip),
+                    .nav_val, .nav_ty => |nav| ip.getNav(nav).name.toSlice(ip),
                     .type => |ty| Type.fromInterned(ty).containerTypeName(ip).toSlice(ip),
                     .func => |f| ip.getNav(zcu.funcInfo(f).owner_nav).name.toSlice(ip),
                 };
@@ -3647,6 +3651,7 @@ fn performAllTheWorkInner(
                 try comp.queueJob(switch (outdated.unwrap()) {
                     .func => |f| .{ .analyze_func = f },
                     .@"comptime",
+                    .nav_ty,
                     .nav_val,
                     .type,
                     => .{ .analyze_comptime_unit = outdated },
@@ -3679,7 +3684,7 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job, prog_node: std.Progre
                     return;
                 }
             }
-            assert(nav.status == .resolved);
+            assert(nav.status == .fully_resolved);
             comp.dispatchCodegenTask(tid, .{ .codegen_nav = nav_index });
         },
         .codegen_func => |func| {
@@ -3709,6 +3714,7 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job, prog_node: std.Progre
 
             const maybe_err: Zcu.SemaError!void = switch (unit.unwrap()) {
                 .@"comptime" => |cu| pt.ensureComptimeUnitUpToDate(cu),
+                .nav_ty => |nav| pt.ensureNavTypeUpToDate(nav),
                 .nav_val => |nav| pt.ensureNavValUpToDate(nav),
                 .type => |ty| if (pt.ensureTypeUpToDate(ty)) |_| {} else |err| err,
                 .func => unreachable,
@@ -3734,7 +3740,7 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job, prog_node: std.Progre
                 // Tests are always emitted in test binaries. The decl_refs are created by
                 // Zcu.populateTestFunctions, but this will not queue body analysis, so do
                 // that now.
-                try pt.zcu.ensureFuncBodyAnalysisQueued(ip.getNav(nav).status.resolved.val);
+                try pt.zcu.ensureFuncBodyAnalysisQueued(ip.getNav(nav).status.fully_resolved.val);
             }
         },
         .resolve_type_fully => |ty| {
src/InternPool.zig
@@ -34,6 +34,9 @@ src_hash_deps: std.AutoArrayHashMapUnmanaged(TrackedInst.Index, DepEntry.Index),
 /// Dependencies on the value of a Nav.
 /// Value is index into `dep_entries` of the first dependency on this Nav value.
 nav_val_deps: std.AutoArrayHashMapUnmanaged(Nav.Index, DepEntry.Index),
+/// Dependencies on the type of a Nav.
+/// Value is index into `dep_entries` of the first dependency on this Nav value.
+nav_ty_deps: std.AutoArrayHashMapUnmanaged(Nav.Index, DepEntry.Index),
 /// Dependencies on an interned value, either:
 /// * a runtime function (invalidated when its IES changes)
 /// * a container type requiring resolution (invalidated when the type must be recreated at a new index)
@@ -80,6 +83,7 @@ pub const empty: InternPool = .{
     .file_deps = .empty,
     .src_hash_deps = .empty,
     .nav_val_deps = .empty,
+    .nav_ty_deps = .empty,
     .interned_deps = .empty,
     .namespace_deps = .empty,
     .namespace_name_deps = .empty,
@@ -371,6 +375,7 @@ pub const AnalUnit = packed struct(u64) {
     pub const Kind = enum(u32) {
         @"comptime",
         nav_val,
+        nav_ty,
         type,
         func,
     };
@@ -380,6 +385,8 @@ pub const AnalUnit = packed struct(u64) {
         @"comptime": ComptimeUnit.Id,
         /// This `AnalUnit` resolves the value of the given `Nav`.
         nav_val: Nav.Index,
+        /// This `AnalUnit` resolves the type of the given `Nav`.
+        nav_ty: Nav.Index,
         /// This `AnalUnit` resolves the given `struct`/`union`/`enum` type.
         /// Generated tag enums are never used here (they do not undergo type resolution).
         type: InternPool.Index,
@@ -483,8 +490,20 @@ pub const Nav = struct {
     status: union(enum) {
         /// This `Nav` is pending semantic analysis.
         unresolved,
+        /// The type of this `Nav` is resolved; the value is queued for resolution.
+        type_resolved: struct {
+            type: InternPool.Index,
+            alignment: Alignment,
+            @"linksection": OptionalNullTerminatedString,
+            @"addrspace": std.builtin.AddressSpace,
+            is_const: bool,
+            is_threadlocal: bool,
+            /// This field is whether this `Nav` is a literal `extern` definition.
+            /// It does *not* tell you whether this might alias an extern fn (see #21027).
+            is_extern_decl: bool,
+        },
         /// The value of this `Nav` is resolved.
-        resolved: struct {
+        fully_resolved: struct {
             val: InternPool.Index,
             alignment: Alignment,
             @"linksection": OptionalNullTerminatedString,
@@ -492,14 +511,81 @@ pub const Nav = struct {
         },
     },
 
-    /// Asserts that `status == .resolved`.
+    /// Asserts that `status != .unresolved`.
     pub fn typeOf(nav: Nav, ip: *const InternPool) InternPool.Index {
-        return ip.typeOf(nav.status.resolved.val);
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => |r| r.type,
+            .fully_resolved => |r| ip.typeOf(r.val),
+        };
     }
 
-    /// Asserts that `status == .resolved`.
-    pub fn isExtern(nav: Nav, ip: *const InternPool) bool {
-        return ip.indexToKey(nav.status.resolved.val) == .@"extern";
+    /// Always returns `null` for `status == .type_resolved`. This function is inteded
+    /// to be used by code generation, since semantic analysis will ensure that any `Nav`
+    /// which is potentially `extern` is fully resolved.
+    /// Asserts that `status != .unresolved`.
+    pub fn getExtern(nav: Nav, ip: *const InternPool) ?Key.Extern {
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => null,
+            .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
+                .@"extern" => |e| e,
+                else => null,
+            },
+        };
+    }
+
+    /// Asserts that `status != .unresolved`.
+    pub fn getAddrspace(nav: Nav) std.builtin.AddressSpace {
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => |r| r.@"addrspace",
+            .fully_resolved => |r| r.@"addrspace",
+        };
+    }
+
+    /// Asserts that `status != .unresolved`.
+    pub fn getAlignment(nav: Nav) Alignment {
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => |r| r.alignment,
+            .fully_resolved => |r| r.alignment,
+        };
+    }
+
+    /// Asserts that `status != .unresolved`.
+    pub fn isThreadlocal(nav: Nav, ip: *const InternPool) bool {
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => |r| r.is_threadlocal,
+            .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
+                .@"extern" => |e| e.is_threadlocal,
+                .variable => |v| v.is_threadlocal,
+                else => false,
+            },
+        };
+    }
+
+    /// If this returns `true`, then a pointer to this `Nav` might actually be encoded as a pointer
+    /// to some other `Nav` due to an extern definition or extern alias (see #21027).
+    /// This query is valid on `Nav`s for whom only the type is resolved.
+    /// Asserts that `status != .unresolved`.
+    pub fn isExternOrFn(nav: Nav, ip: *const InternPool) bool {
+        return switch (nav.status) {
+            .unresolved => unreachable,
+            .type_resolved => |r| {
+                if (r.is_extern_decl) return true;
+                const tag = ip.zigTypeTagOrPoison(r.type) catch unreachable;
+                if (tag == .@"fn") return true;
+                return false;
+            },
+            .fully_resolved => |r| {
+                if (ip.indexToKey(r.val) == .@"extern") return true;
+                const tag = ip.zigTypeTagOrPoison(ip.typeOf(r.val)) catch unreachable;
+                if (tag == .@"fn") return true;
+                return false;
+            },
+        };
     }
 
     /// Get the ZIR instruction corresponding to this `Nav`, used to resolve source locations.
@@ -509,7 +595,7 @@ pub const Nav = struct {
             return a.zir_index;
         }
         // A `Nav` which does not undergo analysis always has a resolved value.
-        return switch (ip.indexToKey(nav.status.resolved.val)) {
+        return switch (ip.indexToKey(nav.status.fully_resolved.val)) {
             .func => |func| {
                 // Since `analysis` was not populated, this must be an instantiation.
                 // Go up to the generic owner and consult *its* `analysis` field.
@@ -567,19 +653,22 @@ pub const Nav = struct {
         // The following 1 fields are either both populated, or both `.none`.
         analysis_namespace: OptionalNamespaceIndex,
         analysis_zir_index: TrackedInst.Index.Optional,
-        /// Populated only if `bits.status == .resolved`.
-        val: InternPool.Index,
-        /// Populated only if `bits.status == .resolved`.
+        /// Populated only if `bits.status != .unresolved`.
+        type_or_val: InternPool.Index,
+        /// Populated only if `bits.status != .unresolved`.
         @"linksection": OptionalNullTerminatedString,
         bits: Bits,
 
         const Bits = packed struct(u16) {
-            status: enum(u1) { unresolved, resolved },
-            /// Populated only if `bits.status == .resolved`.
+            status: enum(u2) { unresolved, type_resolved, fully_resolved, type_resolved_extern_decl },
+            /// Populated only if `bits.status != .unresolved`.
             alignment: Alignment,
-            /// Populated only if `bits.status == .resolved`.
+            /// Populated only if `bits.status != .unresolved`.
             @"addrspace": std.builtin.AddressSpace,
-            _: u3 = 0,
+            /// Populated only if `bits.status == .type_resolved`.
+            is_const: bool,
+            /// Populated only if `bits.status == .type_resolved`.
+            is_threadlocal: bool,
             is_usingnamespace: bool,
         };
 
@@ -597,8 +686,17 @@ pub const Nav = struct {
                 .is_usingnamespace = repr.bits.is_usingnamespace,
                 .status = switch (repr.bits.status) {
                     .unresolved => .unresolved,
-                    .resolved => .{ .resolved = .{
-                        .val = repr.val,
+                    .type_resolved, .type_resolved_extern_decl => .{ .type_resolved = .{
+                        .type = repr.type_or_val,
+                        .alignment = repr.bits.alignment,
+                        .@"linksection" = repr.@"linksection",
+                        .@"addrspace" = repr.bits.@"addrspace",
+                        .is_const = repr.bits.is_const,
+                        .is_threadlocal = repr.bits.is_threadlocal,
+                        .is_extern_decl = repr.bits.status == .type_resolved_extern_decl,
+                    } },
+                    .fully_resolved => .{ .fully_resolved = .{
+                        .val = repr.type_or_val,
                         .alignment = repr.bits.alignment,
                         .@"linksection" = repr.@"linksection",
                         .@"addrspace" = repr.bits.@"addrspace",
@@ -616,13 +714,15 @@ pub const Nav = struct {
             .fqn = nav.fqn,
             .analysis_namespace = if (nav.analysis) |a| a.namespace.toOptional() else .none,
             .analysis_zir_index = if (nav.analysis) |a| a.zir_index.toOptional() else .none,
-            .val = switch (nav.status) {
+            .type_or_val = switch (nav.status) {
                 .unresolved => .none,
-                .resolved => |r| r.val,
+                .type_resolved => |r| r.type,
+                .fully_resolved => |r| r.val,
             },
             .@"linksection" = switch (nav.status) {
                 .unresolved => .none,
-                .resolved => |r| r.@"linksection",
+                .type_resolved => |r| r.@"linksection",
+                .fully_resolved => |r| r.@"linksection",
             },
             .bits = switch (nav.status) {
                 .unresolved => .{
@@ -630,12 +730,24 @@ pub const Nav = struct {
                     .alignment = .none,
                     .@"addrspace" = .generic,
                     .is_usingnamespace = nav.is_usingnamespace,
+                    .is_const = false,
+                    .is_threadlocal = false,
+                },
+                .type_resolved => |r| .{
+                    .status = if (r.is_extern_decl) .type_resolved_extern_decl else .type_resolved,
+                    .alignment = r.alignment,
+                    .@"addrspace" = r.@"addrspace",
+                    .is_usingnamespace = nav.is_usingnamespace,
+                    .is_const = r.is_const,
+                    .is_threadlocal = r.is_threadlocal,
                 },
-                .resolved => |r| .{
-                    .status = .resolved,
+                .fully_resolved => |r| .{
+                    .status = .fully_resolved,
                     .alignment = r.alignment,
                     .@"addrspace" = r.@"addrspace",
                     .is_usingnamespace = nav.is_usingnamespace,
+                    .is_const = false,
+                    .is_threadlocal = false,
                 },
             },
         };
@@ -646,6 +758,7 @@ pub const Dependee = union(enum) {
     file: FileIndex,
     src_hash: TrackedInst.Index,
     nav_val: Nav.Index,
+    nav_ty: Nav.Index,
     interned: Index,
     namespace: TrackedInst.Index,
     namespace_name: NamespaceNameKey,
@@ -695,6 +808,7 @@ pub fn dependencyIterator(ip: *const InternPool, dependee: Dependee) DependencyI
         .file => |x| ip.file_deps.get(x),
         .src_hash => |x| ip.src_hash_deps.get(x),
         .nav_val => |x| ip.nav_val_deps.get(x),
+        .nav_ty => |x| ip.nav_ty_deps.get(x),
         .interned => |x| ip.interned_deps.get(x),
         .namespace => |x| ip.namespace_deps.get(x),
         .namespace_name => |x| ip.namespace_name_deps.get(x),
@@ -732,6 +846,7 @@ pub fn addDependency(ip: *InternPool, gpa: Allocator, depender: AnalUnit, depend
                 .file => ip.file_deps,
                 .src_hash => ip.src_hash_deps,
                 .nav_val => ip.nav_val_deps,
+                .nav_ty => ip.nav_ty_deps,
                 .interned => ip.interned_deps,
                 .namespace => ip.namespace_deps,
                 .namespace_name => ip.namespace_name_deps,
@@ -2079,36 +2194,36 @@ pub const Key = union(enum) {
             return @atomicLoad(FuncAnalysis, func.analysisPtr(ip), .unordered);
         }
 
-        pub fn setAnalysisState(func: Func, ip: *InternPool, state: FuncAnalysis.State) void {
+        pub fn setCallsOrAwaitsErrorableFn(func: Func, ip: *InternPool, value: bool) void {
             const extra_mutex = &ip.getLocal(func.tid).mutate.extra.mutex;
             extra_mutex.lock();
             defer extra_mutex.unlock();
 
             const analysis_ptr = func.analysisPtr(ip);
             var analysis = analysis_ptr.*;
-            analysis.state = state;
+            analysis.calls_or_awaits_errorable_fn = value;
             @atomicStore(FuncAnalysis, analysis_ptr, analysis, .release);
         }
 
-        pub fn setCallsOrAwaitsErrorableFn(func: Func, ip: *InternPool, value: bool) void {
+        pub fn setBranchHint(func: Func, ip: *InternPool, hint: std.builtin.BranchHint) void {
             const extra_mutex = &ip.getLocal(func.tid).mutate.extra.mutex;
             extra_mutex.lock();
             defer extra_mutex.unlock();
 
             const analysis_ptr = func.analysisPtr(ip);
             var analysis = analysis_ptr.*;
-            analysis.calls_or_awaits_errorable_fn = value;
+            analysis.branch_hint = hint;
             @atomicStore(FuncAnalysis, analysis_ptr, analysis, .release);
         }
 
-        pub fn setBranchHint(func: Func, ip: *InternPool, hint: std.builtin.BranchHint) void {
+        pub fn setAnalyzed(func: Func, ip: *InternPool) void {
             const extra_mutex = &ip.getLocal(func.tid).mutate.extra.mutex;
             extra_mutex.lock();
             defer extra_mutex.unlock();
 
             const analysis_ptr = func.analysisPtr(ip);
             var analysis = analysis_ptr.*;
-            analysis.branch_hint = hint;
+            analysis.is_analyzed = true;
             @atomicStore(FuncAnalysis, analysis_ptr, analysis, .release);
         }
 
@@ -5755,7 +5870,7 @@ pub const Tag = enum(u8) {
 /// equality or hashing, except for `inferred_error_set` which is considered
 /// to be part of the type of the function.
 pub const FuncAnalysis = packed struct(u32) {
-    state: State,
+    is_analyzed: bool,
     branch_hint: std.builtin.BranchHint,
     is_noinline: bool,
     calls_or_awaits_errorable_fn: bool,
@@ -5763,20 +5878,7 @@ pub const FuncAnalysis = packed struct(u32) {
     inferred_error_set: bool,
     disable_instrumentation: bool,
 
-    _: u23 = 0,
-
-    pub const State = enum(u2) {
-        /// The runtime function has never been referenced.
-        /// As such, it has never been analyzed, nor is it queued for analysis.
-        unreferenced,
-        /// The runtime function has been referenced, but has not yet been analyzed.
-        /// Its semantic analysis is queued.
-        queued,
-        /// The runtime function has been (or is currently being) semantically analyzed.
-        /// To know if analysis succeeded, consult `zcu.[transitive_]failed_analysis`.
-        /// To know if analysis is up-to-date, consult `zcu.[potentially_]outdated`.
-        analyzed,
-    };
+    _: u24 = 0,
 };
 
 pub const Bytes = struct {
@@ -6419,6 +6521,7 @@ pub fn deinit(ip: *InternPool, gpa: Allocator) void {
     ip.file_deps.deinit(gpa);
     ip.src_hash_deps.deinit(gpa);
     ip.nav_val_deps.deinit(gpa);
+    ip.nav_ty_deps.deinit(gpa);
     ip.interned_deps.deinit(gpa);
     ip.namespace_deps.deinit(gpa);
     ip.namespace_name_deps.deinit(gpa);
@@ -6875,8 +6978,8 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
                 .is_threadlocal = extra.flags.is_threadlocal,
                 .is_weak_linkage = extra.flags.is_weak_linkage,
                 .is_dll_import = extra.flags.is_dll_import,
-                .alignment = nav.status.resolved.alignment,
-                .@"addrspace" = nav.status.resolved.@"addrspace",
+                .alignment = nav.status.fully_resolved.alignment,
+                .@"addrspace" = nav.status.fully_resolved.@"addrspace",
                 .zir_index = extra.zir_index,
                 .owner_nav = extra.owner_nav,
             } };
@@ -8794,7 +8897,7 @@ pub fn getFuncDecl(
 
     const func_decl_extra_index = addExtraAssumeCapacity(extra, Tag.FuncDecl{
         .analysis = .{
-            .state = .unreferenced,
+            .is_analyzed = false,
             .branch_hint = .none,
             .is_noinline = key.is_noinline,
             .calls_or_awaits_errorable_fn = false,
@@ -8903,7 +9006,7 @@ pub fn getFuncDeclIes(
 
     const func_decl_extra_index = addExtraAssumeCapacity(extra, Tag.FuncDecl{
         .analysis = .{
-            .state = .unreferenced,
+            .is_analyzed = false,
             .branch_hint = .none,
             .is_noinline = key.is_noinline,
             .calls_or_awaits_errorable_fn = false,
@@ -9099,7 +9202,7 @@ pub fn getFuncInstance(
 
     const func_extra_index = addExtraAssumeCapacity(extra, Tag.FuncInstance{
         .analysis = .{
-            .state = .unreferenced,
+            .is_analyzed = false,
             .branch_hint = .none,
             .is_noinline = arg.is_noinline,
             .calls_or_awaits_errorable_fn = false,
@@ -9197,7 +9300,7 @@ pub fn getFuncInstanceIes(
 
     const func_extra_index = addExtraAssumeCapacity(extra, Tag.FuncInstance{
         .analysis = .{
-            .state = .unreferenced,
+            .is_analyzed = false,
             .branch_hint = .none,
             .is_noinline = arg.is_noinline,
             .calls_or_awaits_errorable_fn = false,
@@ -9316,9 +9419,9 @@ fn finishFuncInstance(
         .name = nav_name,
         .fqn = try ip.namespacePtr(fn_namespace).internFullyQualifiedName(ip, gpa, tid, nav_name),
         .val = func_index,
-        .alignment = fn_owner_nav.status.resolved.alignment,
-        .@"linksection" = fn_owner_nav.status.resolved.@"linksection",
-        .@"addrspace" = fn_owner_nav.status.resolved.@"addrspace",
+        .alignment = fn_owner_nav.status.fully_resolved.alignment,
+        .@"linksection" = fn_owner_nav.status.fully_resolved.@"linksection",
+        .@"addrspace" = fn_owner_nav.status.fully_resolved.@"addrspace",
     });
 
     // Populate the owner_nav field which was left undefined until now.
@@ -11030,7 +11133,7 @@ pub fn createNav(
         .name = opts.name,
         .fqn = opts.fqn,
         .analysis = null,
-        .status = .{ .resolved = .{
+        .status = .{ .fully_resolved = .{
             .val = opts.val,
             .alignment = opts.alignment,
             .@"linksection" = opts.@"linksection",
@@ -11077,6 +11180,50 @@ pub fn createDeclNav(
     return nav;
 }
 
+/// Resolve the type of a `Nav` with an analysis owner.
+/// If its status is already `resolved`, the old value is discarded.
+pub fn resolveNavType(
+    ip: *InternPool,
+    nav: Nav.Index,
+    resolved: struct {
+        type: InternPool.Index,
+        alignment: Alignment,
+        @"linksection": OptionalNullTerminatedString,
+        @"addrspace": std.builtin.AddressSpace,
+        is_const: bool,
+        is_threadlocal: bool,
+        is_extern_decl: bool,
+    },
+) void {
+    const unwrapped = nav.unwrap(ip);
+
+    const local = ip.getLocal(unwrapped.tid);
+    local.mutate.extra.mutex.lock();
+    defer local.mutate.extra.mutex.unlock();
+
+    const navs = local.shared.navs.view();
+
+    const nav_analysis_namespace = navs.items(.analysis_namespace);
+    const nav_analysis_zir_index = navs.items(.analysis_zir_index);
+    const nav_types = navs.items(.type_or_val);
+    const nav_linksections = navs.items(.@"linksection");
+    const nav_bits = navs.items(.bits);
+
+    assert(nav_analysis_namespace[unwrapped.index] != .none);
+    assert(nav_analysis_zir_index[unwrapped.index] != .none);
+
+    @atomicStore(InternPool.Index, &nav_types[unwrapped.index], resolved.type, .release);
+    @atomicStore(OptionalNullTerminatedString, &nav_linksections[unwrapped.index], resolved.@"linksection", .release);
+
+    var bits = nav_bits[unwrapped.index];
+    bits.status = if (resolved.is_extern_decl) .type_resolved_extern_decl else .type_resolved;
+    bits.alignment = resolved.alignment;
+    bits.@"addrspace" = resolved.@"addrspace";
+    bits.is_const = resolved.is_const;
+    bits.is_threadlocal = resolved.is_threadlocal;
+    @atomicStore(Nav.Repr.Bits, &nav_bits[unwrapped.index], bits, .release);
+}
+
 /// Resolve the value of a `Nav` with an analysis owner.
 /// If its status is already `resolved`, the old value is discarded.
 pub fn resolveNavValue(
@@ -11099,7 +11246,7 @@ pub fn resolveNavValue(
 
     const nav_analysis_namespace = navs.items(.analysis_namespace);
     const nav_analysis_zir_index = navs.items(.analysis_zir_index);
-    const nav_vals = navs.items(.val);
+    const nav_vals = navs.items(.type_or_val);
     const nav_linksections = navs.items(.@"linksection");
     const nav_bits = navs.items(.bits);
 
@@ -11110,7 +11257,7 @@ pub fn resolveNavValue(
     @atomicStore(OptionalNullTerminatedString, &nav_linksections[unwrapped.index], resolved.@"linksection", .release);
 
     var bits = nav_bits[unwrapped.index];
-    bits.status = .resolved;
+    bits.status = .fully_resolved;
     bits.alignment = resolved.alignment;
     bits.@"addrspace" = resolved.@"addrspace";
     @atomicStore(Nav.Repr.Bits, &nav_bits[unwrapped.index], bits, .release);
src/link.zig
@@ -692,7 +692,7 @@ pub const File = struct {
     /// May be called before or after updateExports for any given Nav.
     pub fn updateNav(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) UpdateNavError!void {
         const nav = pt.zcu.intern_pool.getNav(nav_index);
-        assert(nav.status == .resolved);
+        assert(nav.status == .fully_resolved);
         switch (base.tag) {
             inline else => |tag| {
                 dev.check(tag.devFeature());
src/Sema.zig
@@ -6495,9 +6495,9 @@ pub fn analyzeExport(
     if (options.linkage == .internal)
         return;
 
-    try sema.ensureNavResolved(src, orig_nav_index);
+    try sema.ensureNavResolved(src, orig_nav_index, .fully);
 
-    const exported_nav_index = switch (ip.indexToKey(ip.getNav(orig_nav_index).status.resolved.val)) {
+    const exported_nav_index = switch (ip.indexToKey(ip.getNav(orig_nav_index).status.fully_resolved.val)) {
         .variable => |v| v.owner_nav,
         .@"extern" => |e| e.owner_nav,
         .func => |f| f.owner_nav,
@@ -6520,7 +6520,7 @@ pub fn analyzeExport(
     }
 
     // TODO: some backends might support re-exporting extern decls
-    if (exported_nav.isExtern(ip)) {
+    if (exported_nav.getExtern(ip) != null) {
         return sema.fail(block, src, "export target cannot be extern", .{});
     }
 
@@ -6542,6 +6542,7 @@ fn zirDisableInstrumentation(sema: *Sema) CompileError!void {
         .func => |func| func,
         .@"comptime",
         .nav_val,
+        .nav_ty,
         .type,
         => return, // does nothing outside a function
     };
@@ -6854,8 +6855,8 @@ fn lookupInNamespace(
                 }
 
                 for (usingnamespaces.items) |sub_ns_nav| {
-                    try sema.ensureNavResolved(src, sub_ns_nav);
-                    const sub_ns_ty = Type.fromInterned(ip.getNav(sub_ns_nav).status.resolved.val);
+                    try sema.ensureNavResolved(src, sub_ns_nav, .fully);
+                    const sub_ns_ty = Type.fromInterned(ip.getNav(sub_ns_nav).status.fully_resolved.val);
                     const sub_ns = zcu.namespacePtr(sub_ns_ty.getNamespaceIndex(zcu));
                     try checked_namespaces.put(gpa, sub_ns, {});
                 }
@@ -6865,7 +6866,7 @@ fn lookupInNamespace(
         ignore_self: {
             const skip_nav = switch (sema.owner.unwrap()) {
                 .@"comptime", .type, .func => break :ignore_self,
-                .nav_val => |nav| nav,
+                .nav_ty, .nav_val => |nav| nav,
             };
             var i: usize = 0;
             while (i < candidates.items.len) {
@@ -7125,7 +7126,7 @@ fn zirCall(
     const call_inst = try sema.analyzeCall(block, func, func_ty, callee_src, call_src, modifier, ensure_result_used, args_info, call_dbg_node, .call);
 
     switch (sema.owner.unwrap()) {
-        .@"comptime", .type, .nav_val => input_is_error = false,
+        .@"comptime", .type, .nav_ty, .nav_val => input_is_error = false,
         .func => |owner_func| if (!zcu.intern_pool.funcAnalysisUnordered(owner_func).calls_or_awaits_errorable_fn) {
             // No errorable fn actually called; we have no error return trace
             input_is_error = false;
@@ -7686,12 +7687,13 @@ fn analyzeCall(
             .ptr => |ptr| blk: {
                 switch (ptr.base_addr) {
                     .nav => |nav_index| if (ptr.byte_offset == 0) {
+                        try sema.ensureNavResolved(call_src, nav_index, .fully);
                         const nav = ip.getNav(nav_index);
-                        if (nav.isExtern(ip))
+                        if (nav.getExtern(ip) != null)
                             return sema.fail(block, call_src, "{s} call of extern function pointer", .{
                                 if (is_comptime_call) "comptime" else "inline",
                             });
-                        break :blk nav.status.resolved.val;
+                        break :blk nav.status.fully_resolved.val;
                     },
                     else => {},
                 }
@@ -8007,7 +8009,7 @@ fn analyzeCall(
         if (call_dbg_node) |some| try sema.zirDbgStmt(block, some);
 
         switch (sema.owner.unwrap()) {
-            .@"comptime", .nav_val, .type => {},
+            .@"comptime", .nav_ty, .nav_val, .type => {},
             .func => |owner_func| if (Type.fromInterned(func_ty_info.return_type).isError(zcu)) {
                 ip.funcSetCallsOrAwaitsErrorableFn(owner_func);
             },
@@ -8046,7 +8048,10 @@ fn analyzeCall(
                 switch (zcu.intern_pool.indexToKey(func_val.toIntern())) {
                     .func => break :skip_safety,
                     .ptr => |ptr| if (ptr.byte_offset == 0) switch (ptr.base_addr) {
-                        .nav => |nav| if (!ip.getNav(nav).isExtern(ip)) break :skip_safety,
+                        .nav => |nav| {
+                            try sema.ensureNavResolved(call_src, nav, .fully);
+                            if (ip.getNav(nav).getExtern(ip) == null) break :skip_safety;
+                        },
                         else => {},
                     },
                     else => {},
@@ -8243,7 +8248,7 @@ fn instantiateGenericCall(
     });
     const generic_owner = switch (zcu.intern_pool.indexToKey(func_val.toIntern())) {
         .func => func_val.toIntern(),
-        .ptr => |ptr| ip.getNav(ptr.base_addr.nav).status.resolved.val,
+        .ptr => |ptr| ip.getNav(ptr.base_addr.nav).status.fully_resolved.val,
         else => unreachable,
     };
     const generic_owner_func = zcu.intern_pool.indexToKey(generic_owner).func;
@@ -8471,7 +8476,7 @@ fn instantiateGenericCall(
     if (call_dbg_node) |some| try sema.zirDbgStmt(block, some);
 
     switch (sema.owner.unwrap()) {
-        .@"comptime", .nav_val, .type => {},
+        .@"comptime", .nav_ty, .nav_val, .type => {},
         .func => |owner_func| if (Type.fromInterned(func_ty_info.return_type).isError(zcu)) {
             ip.funcSetCallsOrAwaitsErrorableFn(owner_func);
         },
@@ -19311,8 +19316,8 @@ fn typeInfoNamespaceDecls(
         if (zcu.analysis_in_progress.contains(.wrap(.{ .nav_val = nav }))) {
             continue;
         }
-        try sema.ensureNavResolved(src, nav);
-        const namespace_ty = Type.fromInterned(ip.getNav(nav).status.resolved.val);
+        try sema.ensureNavResolved(src, nav, .fully);
+        const namespace_ty = Type.fromInterned(ip.getNav(nav).status.fully_resolved.val);
         try sema.typeInfoNamespaceDecls(block, src, namespace_ty.getNamespaceIndex(zcu).toOptional(), declaration_ty, decl_vals, seen_namespaces);
     }
 }
@@ -21602,7 +21607,7 @@ fn getErrorReturnTrace(sema: *Sema, block: *Block) CompileError!Air.Inst.Ref {
         .func => |func| if (ip.funcAnalysisUnordered(func).calls_or_awaits_errorable_fn and block.ownerModule().error_tracing) {
             return block.addTy(.err_return_trace, opt_ptr_stack_trace_ty);
         },
-        .@"comptime", .nav_val, .type => {},
+        .@"comptime", .nav_ty, .nav_val, .type => {},
     }
     return Air.internedToRef(try pt.intern(.{ .opt = .{
         .ty = opt_ptr_stack_trace_ty.toIntern(),
@@ -27086,7 +27091,7 @@ fn zirBuiltinExtern(
         .zir_index = switch (sema.owner.unwrap()) {
             .@"comptime" => |cu| ip.getComptimeUnit(cu).zir_index,
             .type => |owner_ty| Type.fromInterned(owner_ty).typeDeclInst(zcu).?,
-            .nav_val => |nav| ip.getNav(nav).analysis.?.zir_index,
+            .nav_ty, .nav_val => |nav| ip.getNav(nav).analysis.?.zir_index,
             .func => |func| zir_index: {
                 const func_info = zcu.funcInfo(func);
                 const owner_func_info = if (func_info.generic_owner != .none) owner: {
@@ -27741,7 +27746,7 @@ fn preparePanicId(sema: *Sema, block: *Block, src: LazySrcLoc, panic_id: Zcu.Pan
         error.GenericPoison, error.ComptimeReturn, error.ComptimeBreak => unreachable,
         error.OutOfMemory => |e| return e,
     }).?;
-    try sema.ensureNavResolved(src, msg_nav_index);
+    try sema.ensureNavResolved(src, msg_nav_index, .fully);
     zcu.panic_messages[@intFromEnum(panic_id)] = msg_nav_index.toOptional();
     return msg_nav_index;
 }
@@ -32648,21 +32653,29 @@ fn addTypeReferenceEntry(
     try zcu.addTypeReference(sema.owner, referenced_type, src);
 }
 
-pub fn ensureNavResolved(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav.Index) CompileError!void {
+pub fn ensureNavResolved(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav.Index, kind: enum { type, fully }) CompileError!void {
     const pt = sema.pt;
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
     const nav = ip.getNav(nav_index);
     if (nav.analysis == null) {
-        assert(nav.status == .resolved);
+        assert(nav.status == .fully_resolved);
         return;
     }
 
+    try sema.declareDependency(switch (kind) {
+        .type => .{ .nav_ty = nav_index },
+        .fully => .{ .nav_val = nav_index },
+    });
+
     // Note that even if `nav.status == .resolved`, we must still trigger `ensureNavValUpToDate`
     // to make sure the value is up-to-date on incremental updates.
 
-    const anal_unit: AnalUnit = .wrap(.{ .nav_val = nav_index });
+    const anal_unit: AnalUnit = .wrap(switch (kind) {
+        .type => .{ .nav_ty = nav_index },
+        .fully => .{ .nav_val = nav_index },
+    });
     try sema.addReferenceEntry(src, anal_unit);
 
     if (zcu.analysis_in_progress.contains(anal_unit)) {
@@ -32672,7 +32685,13 @@ pub fn ensureNavResolved(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav
         }, "dependency loop detected", .{}));
     }
 
-    return pt.ensureNavValUpToDate(nav_index);
+    switch (kind) {
+        .type => {
+            try zcu.ensureNavValAnalysisQueued(nav_index);
+            return pt.ensureNavTypeUpToDate(nav_index);
+        },
+        .fully => return pt.ensureNavValUpToDate(nav_index),
+    }
 }
 
 fn optRefValue(sema: *Sema, opt_val: ?Value) !Value {
@@ -32691,36 +32710,44 @@ fn analyzeNavRef(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav.Index)
     return sema.analyzeNavRefInner(src, nav_index, true);
 }
 
-/// Analyze a reference to the `Nav` at the given index. Ensures the underlying `Nav` is analyzed, but
-/// only triggers analysis for function bodies if `analyze_fn_body` is true. If it's possible for a
-/// decl_ref to end up in runtime code, the function body must be analyzed: `analyzeNavRef` wraps
-/// this function with `analyze_fn_body` set to true.
-fn analyzeNavRefInner(sema: *Sema, src: LazySrcLoc, orig_nav_index: InternPool.Nav.Index, analyze_fn_body: bool) CompileError!Air.Inst.Ref {
+/// Analyze a reference to the `Nav` at the given index. Ensures the underlying `Nav` is analyzed.
+/// If this pointer will be used directly, `is_ref` must be `true`.
+/// If this pointer will be immediately loaded (i.e. a `decl_val` instruction), `is_ref` must be `false`.
+fn analyzeNavRefInner(sema: *Sema, src: LazySrcLoc, orig_nav_index: InternPool.Nav.Index, is_ref: bool) CompileError!Air.Inst.Ref {
     const pt = sema.pt;
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
 
-    // TODO: if this is a `decl_ref` of a non-variable Nav, only depend on Nav type
-    try sema.declareDependency(.{ .nav_val = orig_nav_index });
-    try sema.ensureNavResolved(src, orig_nav_index);
+    try sema.ensureNavResolved(src, orig_nav_index, if (is_ref) .type else .fully);
 
-    const nav_val = zcu.navValue(orig_nav_index);
-    const nav_index, const is_const = switch (ip.indexToKey(nav_val.toIntern())) {
-        .variable => |v| .{ v.owner_nav, false },
-        .func => |f| .{ f.owner_nav, true },
-        .@"extern" => |e| .{ e.owner_nav, e.is_const },
-        else => .{ orig_nav_index, true },
+    const nav_index = nav: {
+        if (ip.getNav(orig_nav_index).isExternOrFn(ip)) {
+            // Getting a pointer to this `Nav` might mean we actually get a pointer to something else!
+            // We need to resolve the value to know for sure.
+            if (is_ref) try sema.ensureNavResolved(src, orig_nav_index, .fully);
+            switch (ip.indexToKey(ip.getNav(orig_nav_index).status.fully_resolved.val)) {
+                .func => |f| break :nav f.owner_nav,
+                .@"extern" => |e| break :nav e.owner_nav,
+                else => {},
+            }
+        }
+        break :nav orig_nav_index;
+    };
+
+    const ty, const alignment, const @"addrspace", const is_const = switch (ip.getNav(nav_index).status) {
+        .unresolved => unreachable,
+        .type_resolved => |r| .{ r.type, r.alignment, r.@"addrspace", r.is_const },
+        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", zcu.navValIsConst(r.val) },
     };
-    const nav_info = ip.getNav(nav_index).status.resolved;
     const ptr_ty = try pt.ptrTypeSema(.{
-        .child = nav_val.typeOf(zcu).toIntern(),
+        .child = ty,
         .flags = .{
-            .alignment = nav_info.alignment,
+            .alignment = alignment,
             .is_const = is_const,
-            .address_space = nav_info.@"addrspace",
+            .address_space = @"addrspace",
         },
     });
-    if (analyze_fn_body) {
+    if (is_ref) {
         try sema.maybeQueueFuncBodyAnalysis(src, nav_index);
     }
     return Air.internedToRef((try pt.intern(.{ .ptr = .{
@@ -32731,11 +32758,22 @@ fn analyzeNavRefInner(sema: *Sema, src: LazySrcLoc, orig_nav_index: InternPool.N
 }
 
 fn maybeQueueFuncBodyAnalysis(sema: *Sema, src: LazySrcLoc, nav_index: InternPool.Nav.Index) !void {
-    const zcu = sema.pt.zcu;
+    const pt = sema.pt;
+    const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
+
+    // To avoid forcing too much resolution, let's first resolve the type, and check if it's a function.
+    // If it is, we can resolve the *value*, and queue analysis as needed.
+
+    try sema.ensureNavResolved(src, nav_index, .type);
+    const nav_ty: Type = .fromInterned(ip.getNav(nav_index).typeOf(ip));
+    if (nav_ty.zigTypeTag(zcu) != .@"fn") return;
+    if (!try nav_ty.fnHasRuntimeBitsSema(pt)) return;
+
+    try sema.ensureNavResolved(src, nav_index, .fully);
     const nav_val = zcu.navValue(nav_index);
     if (!ip.isFuncBody(nav_val.toIntern())) return;
-    if (!try nav_val.typeOf(zcu).fnHasRuntimeBitsSema(sema.pt)) return;
+
     try sema.addReferenceEntry(src, AnalUnit.wrap(.{ .func = nav_val.toIntern() }));
     try zcu.ensureFuncBodyAnalysisQueued(nav_val.toIntern());
 }
@@ -38450,11 +38488,16 @@ pub fn declareDependency(sema: *Sema, dependee: InternPool.Dependee) !void {
     // of a type and they use `@This()`. This dependency would be unnecessary, and in fact would
     // just result in over-analysis since `Zcu.findOutdatedToAnalyze` would never be able to resolve
     // the loop.
+    // Note that this also disallows a `nav_val`
     switch (sema.owner.unwrap()) {
         .nav_val => |this_nav| switch (dependee) {
             .nav_val => |other_nav| if (this_nav == other_nav) return,
             else => {},
         },
+        .nav_ty => |this_nav| switch (dependee) {
+            .nav_ty => |other_nav| if (this_nav == other_nav) return,
+            else => {},
+        },
         else => {},
     }
 
@@ -38873,8 +38916,8 @@ fn getBuiltinInnerType(
     const nav = opt_nav orelse return sema.fail(block, src, "std.builtin.{s} missing {s}", .{
         compile_error_parent_name, inner_name,
     });
-    try sema.ensureNavResolved(src, nav);
-    const val = Value.fromInterned(ip.getNav(nav).status.resolved.val);
+    try sema.ensureNavResolved(src, nav, .fully);
+    const val = Value.fromInterned(ip.getNav(nav).status.fully_resolved.val);
     const ty = val.toType();
     try ty.resolveFully(pt);
     return ty;
@@ -38886,5 +38929,73 @@ fn getBuiltin(sema: *Sema, name: []const u8) SemaError!Air.Inst.Ref {
     const ip = &zcu.intern_pool;
     const nav = try pt.getBuiltinNav(name);
     try pt.ensureNavValUpToDate(nav);
-    return Air.internedToRef(ip.getNav(nav).status.resolved.val);
+    return Air.internedToRef(ip.getNav(nav).status.fully_resolved.val);
+}
+
+pub const NavPtrModifiers = struct {
+    alignment: Alignment,
+    @"linksection": InternPool.OptionalNullTerminatedString,
+    @"addrspace": std.builtin.AddressSpace,
+};
+
+pub fn resolveNavPtrModifiers(
+    sema: *Sema,
+    block: *Block,
+    zir_decl: Zir.Inst.Declaration.Unwrapped,
+    decl_inst: Zir.Inst.Index,
+    nav_ty: Type,
+) CompileError!NavPtrModifiers {
+    const pt = sema.pt;
+    const zcu = pt.zcu;
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+
+    const align_src = block.src(.{ .node_offset_var_decl_align = 0 });
+    const section_src = block.src(.{ .node_offset_var_decl_section = 0 });
+    const addrspace_src = block.src(.{ .node_offset_var_decl_addrspace = 0 });
+
+    const alignment: InternPool.Alignment = a: {
+        const align_body = zir_decl.align_body orelse break :a .none;
+        const align_ref = try sema.resolveInlineBody(block, align_body, decl_inst);
+        break :a try sema.analyzeAsAlign(block, align_src, align_ref);
+    };
+
+    const @"linksection": InternPool.OptionalNullTerminatedString = ls: {
+        const linksection_body = zir_decl.linksection_body orelse break :ls .none;
+        const linksection_ref = try sema.resolveInlineBody(block, linksection_body, decl_inst);
+        const bytes = try sema.toConstString(block, section_src, linksection_ref, .{
+            .needed_comptime_reason = "linksection must be comptime-known",
+        });
+        if (std.mem.indexOfScalar(u8, bytes, 0) != null) {
+            return sema.fail(block, section_src, "linksection cannot contain null bytes", .{});
+        } else if (bytes.len == 0) {
+            return sema.fail(block, section_src, "linksection cannot be empty", .{});
+        }
+        break :ls try ip.getOrPutStringOpt(gpa, pt.tid, bytes, .no_embedded_nulls);
+    };
+
+    const @"addrspace": std.builtin.AddressSpace = as: {
+        const addrspace_ctx: Sema.AddressSpaceContext = switch (zir_decl.kind) {
+            .@"var" => .variable,
+            else => switch (nav_ty.zigTypeTag(zcu)) {
+                .@"fn" => .function,
+                else => .constant,
+            },
+        };
+        const target = zcu.getTarget();
+        const addrspace_body = zir_decl.addrspace_body orelse break :as switch (addrspace_ctx) {
+            .function => target_util.defaultAddressSpace(target, .function),
+            .variable => target_util.defaultAddressSpace(target, .global_mutable),
+            .constant => target_util.defaultAddressSpace(target, .global_constant),
+            else => unreachable,
+        };
+        const addrspace_ref = try sema.resolveInlineBody(block, addrspace_body, decl_inst);
+        break :as try sema.analyzeAsAddressSpace(block, addrspace_src, addrspace_ref, addrspace_ctx);
+    };
+
+    return .{
+        .alignment = alignment,
+        .@"linksection" = @"linksection",
+        .@"addrspace" = @"addrspace",
+    };
 }
src/Value.zig
@@ -1343,7 +1343,12 @@ pub fn isLazySize(val: Value, zcu: *Zcu) bool {
 pub fn isPtrRuntimeValue(val: Value, zcu: *Zcu) bool {
     const ip = &zcu.intern_pool;
     const nav = ip.getBackingNav(val.toIntern()).unwrap() orelse return false;
-    return switch (ip.indexToKey(ip.getNav(nav).status.resolved.val)) {
+    const nav_val = switch (ip.getNav(nav).status) {
+        .unresolved => unreachable,
+        .type_resolved => |r| return r.is_threadlocal,
+        .fully_resolved => |r| r.val,
+    };
+    return switch (ip.indexToKey(nav_val)) {
         .@"extern" => |e| e.is_threadlocal or e.is_dll_import,
         .variable => |v| v.is_threadlocal,
         else => false,
src/Zcu.zig
@@ -170,6 +170,9 @@ outdated_ready: std.AutoArrayHashMapUnmanaged(AnalUnit, void) = .empty,
 /// it as outdated.
 retryable_failures: std.ArrayListUnmanaged(AnalUnit) = .empty,
 
+func_body_analysis_queued: std.AutoArrayHashMapUnmanaged(InternPool.Index, void) = .empty,
+nav_val_analysis_queued: std.AutoArrayHashMapUnmanaged(InternPool.Nav.Index, void) = .empty,
+
 /// These are the modules which we initially queue for analysis in `Compilation.update`.
 /// `resolveReferences` will use these as the root of its reachability traversal.
 analysis_roots: std.BoundedArray(*Package.Module, 3) = .{},
@@ -282,7 +285,11 @@ pub const Exported = union(enum) {
 
     pub fn getAlign(exported: Exported, zcu: *Zcu) Alignment {
         return switch (exported) {
-            .nav => |nav| zcu.intern_pool.getNav(nav).status.resolved.alignment,
+            .nav => |nav| switch (zcu.intern_pool.getNav(nav).status) {
+                .unresolved => unreachable,
+                .type_resolved => |r| r.alignment,
+                .fully_resolved => |r| r.alignment,
+            },
             .uav => .none,
         };
     }
@@ -2241,6 +2248,9 @@ pub fn deinit(zcu: *Zcu) void {
         zcu.outdated_ready.deinit(gpa);
         zcu.retryable_failures.deinit(gpa);
 
+        zcu.func_body_analysis_queued.deinit(gpa);
+        zcu.nav_val_analysis_queued.deinit(gpa);
+
         zcu.test_functions.deinit(gpa);
 
         for (zcu.global_assembly.values()) |s| {
@@ -2441,6 +2451,7 @@ pub fn markPoDependeeUpToDate(zcu: *Zcu, dependee: InternPool.Dependee) !void {
         switch (depender.unwrap()) {
             .@"comptime" => {},
             .nav_val => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_val = nav }),
+            .nav_ty => |nav| try zcu.markPoDependeeUpToDate(.{ .nav_ty = nav }),
             .type => |ty| try zcu.markPoDependeeUpToDate(.{ .interned = ty }),
             .func => |func| try zcu.markPoDependeeUpToDate(.{ .interned = func }),
         }
@@ -2453,7 +2464,8 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni
     const ip = &zcu.intern_pool;
     const dependee: InternPool.Dependee = switch (maybe_outdated.unwrap()) {
         .@"comptime" => return, // analysis of a comptime decl can't outdate any dependencies
-        .nav_val => |nav| .{ .nav_val = nav }, // TODO: also `nav_ref` deps when introduced
+        .nav_val => |nav| .{ .nav_val = nav },
+        .nav_ty => |nav| .{ .nav_ty = nav },
         .type => |ty| .{ .interned = ty },
         .func => |func_index| .{ .interned = func_index }, // IES
     };
@@ -2540,6 +2552,7 @@ pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit {
                 .@"comptime" => continue, // a `comptime` block can't even be depended on so it is a terrible choice
                 .type => |ty| .{ .interned = ty },
                 .nav_val => |nav| .{ .nav_val = nav },
+                .nav_ty => |nav| .{ .nav_ty = nav },
             });
             while (it.next()) |_| n += 1;
 
@@ -2780,14 +2793,39 @@ pub fn ensureFuncBodyAnalysisQueued(zcu: *Zcu, func_index: InternPool.Index) !vo
     const ip = &zcu.intern_pool;
     const func = zcu.funcInfo(func_index);
 
-    switch (func.analysisUnordered(ip).state) {
-        .unreferenced => {}, // We're the first reference!
-        .queued => return, // Analysis is already queued.
-        .analyzed => return, // Analysis is complete; if it's out-of-date, it'll be re-analyzed later this update.
+    if (zcu.func_body_analysis_queued.contains(func_index)) return;
+
+    if (func.analysisUnordered(ip).is_analyzed) {
+        if (!zcu.outdated.contains(.wrap(.{ .func = func_index })) and
+            !zcu.potentially_outdated.contains(.wrap(.{ .func = func_index })))
+        {
+            // This function has been analyzed before and is definitely up-to-date.
+            return;
+        }
     }
 
+    try zcu.func_body_analysis_queued.ensureUnusedCapacity(zcu.gpa, 1);
     try zcu.comp.queueJob(.{ .analyze_func = func_index });
-    func.setAnalysisState(ip, .queued);
+    zcu.func_body_analysis_queued.putAssumeCapacityNoClobber(func_index, {});
+}
+
+pub fn ensureNavValAnalysisQueued(zcu: *Zcu, nav_id: InternPool.Nav.Index) !void {
+    const ip = &zcu.intern_pool;
+
+    if (zcu.nav_val_analysis_queued.contains(nav_id)) return;
+
+    if (ip.getNav(nav_id).status == .fully_resolved) {
+        if (!zcu.outdated.contains(.wrap(.{ .nav_val = nav_id })) and
+            !zcu.potentially_outdated.contains(.wrap(.{ .nav_val = nav_id })))
+        {
+            // This `Nav` has been analyzed before and is definitely up-to-date.
+            return;
+        }
+    }
+
+    try zcu.nav_val_analysis_queued.ensureUnusedCapacity(zcu.gpa, 1);
+    try zcu.comp.queueJob(.{ .analyze_comptime_unit = .wrap(.{ .nav_val = nav_id }) });
+    zcu.nav_val_analysis_queued.putAssumeCapacityNoClobber(nav_id, {});
 }
 
 pub const ImportFileResult = struct {
@@ -3424,6 +3462,17 @@ fn resolveReferencesInner(zcu: *Zcu) !std.AutoHashMapUnmanaged(AnalUnit, ?Resolv
             const unit = kv.key;
             try result.putNoClobber(gpa, unit, kv.value);
 
+            // `nav_val` and `nav_ty` reference each other *implicitly* to save memory.
+            queue_paired: {
+                const other: AnalUnit = .wrap(switch (unit.unwrap()) {
+                    .nav_val => |n| .{ .nav_ty = n },
+                    .nav_ty => |n| .{ .nav_val = n },
+                    .@"comptime", .type, .func => break :queue_paired,
+                });
+                if (result.contains(other)) break :queue_paired;
+                try unit_queue.put(gpa, other, kv.value); // same reference location
+            }
+
             log.debug("handle unit '{}'", .{zcu.fmtAnalUnit(unit)});
 
             if (zcu.reference_table.get(unit)) |first_ref_idx| {
@@ -3513,7 +3562,7 @@ pub fn navSrcLine(zcu: *Zcu, nav_index: InternPool.Nav.Index) u32 {
 }
 
 pub fn navValue(zcu: *const Zcu, nav_index: InternPool.Nav.Index) Value {
-    return Value.fromInterned(zcu.intern_pool.getNav(nav_index).status.resolved.val);
+    return Value.fromInterned(zcu.intern_pool.getNav(nav_index).status.fully_resolved.val);
 }
 
 pub fn navFileScopeIndex(zcu: *Zcu, nav: InternPool.Nav.Index) File.Index {
@@ -3547,6 +3596,7 @@ fn formatAnalUnit(data: struct { unit: AnalUnit, zcu: *Zcu }, comptime fmt: []co
             }
         },
         .nav_val => |nav| return writer.print("nav_val('{}')", .{ip.getNav(nav).fqn.fmt(ip)}),
+        .nav_ty => |nav| return writer.print("nav_ty('{}')", .{ip.getNav(nav).fqn.fmt(ip)}),
         .type => |ty| return writer.print("ty('{}')", .{Type.fromInterned(ty).containerTypeName(ip).fmt(ip)}),
         .func => |func| {
             const nav = zcu.funcInfo(func).owner_nav;
@@ -3572,7 +3622,11 @@ fn formatDependee(data: struct { dependee: InternPool.Dependee, zcu: *Zcu }, com
         },
         .nav_val => |nav| {
             const fqn = ip.getNav(nav).fqn;
-            return writer.print("nav('{}')", .{fqn.fmt(ip)});
+            return writer.print("nav_val('{}')", .{fqn.fmt(ip)});
+        },
+        .nav_ty => |nav| {
+            const fqn = ip.getNav(nav).fqn;
+            return writer.print("nav_ty('{}')", .{fqn.fmt(ip)});
         },
         .interned => |ip_index| switch (ip.indexToKey(ip_index)) {
             .struct_type, .union_type, .enum_type => return writer.print("type('{}')", .{Type.fromInterned(ip_index).containerTypeName(ip).fmt(ip)}),
@@ -3749,3 +3803,12 @@ pub fn callconvSupported(zcu: *Zcu, cc: std.builtin.CallingConvention) union(enu
     if (!backend_ok) return .{ .bad_backend = backend };
     return .ok;
 }
+
+/// Given that a `Nav` has value `val`, determine if a ref of that `Nav` gives a `const` pointer.
+pub fn navValIsConst(zcu: *const Zcu, val: InternPool.Index) bool {
+    return switch (zcu.intern_pool.indexToKey(val)) {
+        .variable => false,
+        .@"extern" => |e| e.is_const,
+        else => true,
+    };
+}
test/behavior/globals.zig
@@ -66,3 +66,99 @@ test "global loads can affect liveness" {
     S.f();
     try std.testing.expect(y.a == 1);
 }
+
+test "global const can be self-referential" {
+    const S = struct {
+        self: *const @This(),
+        x: u32,
+
+        const foo: @This() = .{ .self = &foo, .x = 123 };
+    };
+
+    try std.testing.expect(S.foo.x == 123);
+    try std.testing.expect(S.foo.self.x == 123);
+    try std.testing.expect(S.foo.self.self.x == 123);
+    try std.testing.expect(S.foo.self == &S.foo);
+    try std.testing.expect(S.foo.self.self == &S.foo);
+}
+
+test "global var can be self-referential" {
+    const S = struct {
+        self: *@This(),
+        x: u32,
+
+        var foo: @This() = .{ .self = &foo, .x = undefined };
+    };
+
+    S.foo.x = 123;
+
+    try std.testing.expect(S.foo.x == 123);
+    try std.testing.expect(S.foo.self.x == 123);
+    try std.testing.expect(S.foo.self == &S.foo);
+
+    S.foo.self.x = 456;
+
+    try std.testing.expect(S.foo.x == 456);
+    try std.testing.expect(S.foo.self.x == 456);
+    try std.testing.expect(S.foo.self == &S.foo);
+
+    S.foo.self.self.x = 789;
+
+    try std.testing.expect(S.foo.x == 789);
+    try std.testing.expect(S.foo.self.x == 789);
+    try std.testing.expect(S.foo.self == &S.foo);
+}
+
+test "global const can be indirectly self-referential" {
+    const S = struct {
+        other: *const @This(),
+        x: u32,
+
+        const foo: @This() = .{ .other = &bar, .x = 123 };
+        const bar: @This() = .{ .other = &foo, .x = 456 };
+    };
+
+    try std.testing.expect(S.foo.x == 123);
+    try std.testing.expect(S.foo.other.x == 456);
+    try std.testing.expect(S.foo.other.other.x == 123);
+    try std.testing.expect(S.foo.other.other.other.x == 456);
+    try std.testing.expect(S.foo.other == &S.bar);
+    try std.testing.expect(S.foo.other.other == &S.foo);
+
+    try std.testing.expect(S.bar.x == 456);
+    try std.testing.expect(S.bar.other.x == 123);
+    try std.testing.expect(S.bar.other.other.x == 456);
+    try std.testing.expect(S.bar.other.other.other.x == 123);
+    try std.testing.expect(S.bar.other == &S.foo);
+    try std.testing.expect(S.bar.other.other == &S.bar);
+}
+
+test "global var can be indirectly self-referential" {
+    const S = struct {
+        other: *@This(),
+        x: u32,
+
+        var foo: @This() = .{ .other = &bar, .x = undefined };
+        var bar: @This() = .{ .other = &foo, .x = undefined };
+    };
+
+    S.foo.other.x = 123; // bar.x
+    S.foo.other.other.x = 456; // foo.x
+
+    try std.testing.expect(S.foo.x == 456);
+    try std.testing.expect(S.foo.other.x == 123);
+    try std.testing.expect(S.foo.other.other.x == 456);
+    try std.testing.expect(S.foo.other.other.other.x == 123);
+    try std.testing.expect(S.foo.other == &S.bar);
+    try std.testing.expect(S.foo.other.other == &S.foo);
+
+    S.bar.other.x = 111; // foo.x
+    S.bar.other.other.x = 222; // bar.x
+
+    try std.testing.expect(S.bar.x == 222);
+    try std.testing.expect(S.bar.other.x == 111);
+    try std.testing.expect(S.bar.other.other.x == 222);
+    try std.testing.expect(S.bar.other.other.other.x == 111);
+    try std.testing.expect(S.bar.other == &S.foo);
+    try std.testing.expect(S.bar.other.other == &S.bar);
+}
test/cases/compile_errors/self_reference_missing_const.zig
@@ -0,0 +1,11 @@
+const S = struct { self: *S, x: u32 };
+const s: S = .{ .self = &s, .x = 123 };
+
+comptime {
+    _ = s;
+}
+
+// error
+//
+// :2:18: error: expected type '*tmp.S', found '*const tmp.S'
+// :2:18: note: cast discards const qualifier