Commit 4bc88dd116

Andrew Kelley <andrew@ziglang.org>
2023-10-27 05:32:16
link: support exporting constant values without a Decl
The main motivating change here is to prevent the creation of a fake Decl object by the frontend in order to `@export()` a value. Instead, `link.updateDeclExports` is renamed to `link.updateExports` and accepts a tagged union which can be either a Decl.Index or a InternPool.Index.
1 parent ba9e388
src/codegen/llvm.zig
@@ -1144,26 +1144,40 @@ pub const Object = struct {
 
         for (mod.decl_exports.keys(), mod.decl_exports.values()) |decl_index, export_list| {
             const global = object.decl_map.get(decl_index) orelse continue;
-            const global_base = global.toConst().getBase(&object.builder);
-            for (export_list.items) |exp| {
-                // Detect if the LLVM global has already been created as an extern. In such
-                // case, we need to replace all uses of it with this exported global.
-                const exp_name = object.builder.stringIfExists(mod.intern_pool.stringToSlice(exp.opts.name)) orelse continue;
-
-                const other_global = object.builder.getGlobal(exp_name) orelse continue;
-                if (other_global.toConst().getBase(&object.builder) == global_base) continue;
-
-                try global.takeName(other_global, &object.builder);
-                try other_global.replace(global, &object.builder);
-                // Problem: now we need to replace in the decl_map that
-                // the extern decl index points to this new global. However we don't
-                // know the decl index.
-                // Even if we did, a future incremental update to the extern would then
-                // treat the LLVM global as an extern rather than an export, so it would
-                // need a way to check that.
-                // This is a TODO that needs to be solved when making
-                // the LLVM backend support incremental compilation.
-            }
+            try resolveGlobalCollisions(object, global, export_list.items);
+        }
+
+        for (mod.value_exports.keys(), mod.value_exports.values()) |val, export_list| {
+            const global = object.anon_decl_map.get(val) orelse continue;
+            try resolveGlobalCollisions(object, global, export_list.items);
+        }
+    }
+
+    fn resolveGlobalCollisions(
+        object: *Object,
+        global: Builder.Global.Index,
+        export_list: []const *Module.Export,
+    ) !void {
+        const mod = object.module;
+        const global_base = global.toConst().getBase(&object.builder);
+        for (export_list) |exp| {
+            // Detect if the LLVM global has already been created as an extern. In such
+            // case, we need to replace all uses of it with this exported global.
+            const exp_name = object.builder.stringIfExists(mod.intern_pool.stringToSlice(exp.opts.name)) orelse continue;
+
+            const other_global = object.builder.getGlobal(exp_name) orelse continue;
+            if (other_global.toConst().getBase(&object.builder) == global_base) continue;
+
+            try global.takeName(other_global, &object.builder);
+            try other_global.replace(global, &object.builder);
+            // Problem: now we need to replace in the decl_map that
+            // the extern decl index points to this new global. However we don't
+            // know the decl index.
+            // Even if we did, a future incremental update to the extern would then
+            // treat the LLVM global as an extern rather than an export, so it would
+            // need a way to check that.
+            // This is a TODO that needs to be solved when making
+            // the LLVM backend support incremental compilation.
         }
     }
 
@@ -1642,7 +1656,7 @@ pub const Object = struct {
 
         try fg.wip.finish();
 
-        try o.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+        try o.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
     }
 
     pub fn updateDecl(self: *Object, module: *Module, decl_index: Module.Decl.Index) !void {
@@ -1662,18 +1676,22 @@ pub const Object = struct {
             },
             else => |e| return e,
         };
-        try self.updateDeclExports(module, decl_index, module.getDeclExports(decl_index));
+        try self.updateExports(module, .{ .decl_index = decl_index }, module.getDeclExports(decl_index));
     }
 
-    pub fn updateDeclExports(
+    pub fn updateExports(
         self: *Object,
         mod: *Module,
-        decl_index: Module.Decl.Index,
+        exported: Module.Exported,
         exports: []const *Module.Export,
-    ) !void {
+    ) link.File.UpdateExportsError!void {
+        const decl_index = switch (exported) {
+            .decl_index => |i| i,
+            .value => |val| return updateExportedValue(self, mod, val, exports),
+        };
         const gpa = mod.gpa;
         // If the module does not already have the function, we ignore this function call
-        // because we call `updateDeclExports` at the end of `updateFunc` and `updateDecl`.
+        // because we call `updateExports` at the end of `updateFunc` and `updateDecl`.
         const global_index = self.decl_map.get(decl_index) orelse return;
         const decl = mod.declPtr(decl_index);
         if (decl.isExtern(mod)) {
@@ -1733,8 +1751,7 @@ pub const Object = struct {
                 mod.intern_pool.stringToSlice(exports[0].opts.name),
             );
             try global_index.rename(main_exp_name, &self.builder);
-            global_index.setUnnamedAddr(.default, &self.builder);
-            if (mod.wantDllExports()) global_index.setDllStorageClass(.dllexport, &self.builder);
+
             if (self.di_map.get(decl)) |di_node| {
                 const main_exp_name_slice = main_exp_name.slice(&self.builder).?;
                 if (try decl.isFunction(mod)) {
@@ -1755,55 +1772,12 @@ pub const Object = struct {
                     di_global.replaceLinkageName(linkage_name);
                 }
             }
-            global_index.setLinkage(switch (exports[0].opts.linkage) {
-                .Internal => unreachable,
-                .Strong => .external,
-                .Weak => .weak_odr,
-                .LinkOnce => .linkonce_odr,
-            }, &self.builder);
-            global_index.setVisibility(switch (exports[0].opts.visibility) {
-                .default => .default,
-                .hidden => .hidden,
-                .protected => .protected,
-            }, &self.builder);
-            if (mod.intern_pool.stringToSliceUnwrap(exports[0].opts.section)) |section|
-                switch (global_index.ptrConst(&self.builder).kind) {
-                    inline .variable, .function => |impl_index| impl_index.setSection(
-                        try self.builder.string(section),
-                        &self.builder,
-                    ),
-                    .alias, .replaced => unreachable,
-                };
+
             if (decl.val.getVariable(mod)) |decl_var| if (decl_var.is_threadlocal)
                 global_index.ptrConst(&self.builder).kind
                     .variable.setThreadLocal(.generaldynamic, &self.builder);
 
-            // If a Decl is exported more than one time (which is rare),
-            // we add aliases for all but the first export.
-            // TODO LLVM C API does not support deleting aliases.
-            // The planned solution to this is https://github.com/ziglang/zig/issues/13265
-            // Until then we iterate over existing aliases and make them point
-            // to the correct decl, or otherwise add a new alias. Old aliases are leaked.
-            for (exports[1..]) |exp| {
-                const exp_name = try self.builder.string(mod.intern_pool.stringToSlice(exp.opts.name));
-                if (self.builder.getGlobal(exp_name)) |global| {
-                    switch (global.ptrConst(&self.builder).kind) {
-                        .alias => |alias| {
-                            alias.setAliasee(global_index.toConst(), &self.builder);
-                            continue;
-                        },
-                        .variable, .function => {},
-                        .replaced => unreachable,
-                    }
-                }
-                const alias_index = try self.builder.addAlias(
-                    .empty,
-                    global_index.typeOf(&self.builder),
-                    .default,
-                    global_index.toConst(),
-                );
-                try alias_index.rename(exp_name, &self.builder);
-            }
+            return updateExportedGlobal(self, mod, global_index, exports);
         } else {
             const fqn = try self.builder.string(
                 mod.intern_pool.stringToSlice(try decl.getFullyQualifiedName(mod)),
@@ -1824,6 +1798,100 @@ pub const Object = struct {
         }
     }
 
+    fn updateExportedValue(
+        o: *Object,
+        mod: *Module,
+        exported_value: InternPool.Index,
+        exports: []const *Module.Export,
+    ) link.File.UpdateExportsError!void {
+        const gpa = mod.gpa;
+        const main_exp_name = try o.builder.string(
+            mod.intern_pool.stringToSlice(exports[0].opts.name),
+        );
+        const global_index = i: {
+            const gop = try o.anon_decl_map.getOrPut(gpa, exported_value);
+            if (gop.found_existing) {
+                const global_index = gop.value_ptr.*;
+                try global_index.rename(main_exp_name, &o.builder);
+                break :i global_index;
+            }
+            const llvm_addr_space = toLlvmAddressSpace(.generic, o.target);
+            const variable_index = try o.builder.addVariable(
+                main_exp_name,
+                try o.lowerType(mod.intern_pool.typeOf(exported_value).toType()),
+                llvm_addr_space,
+            );
+            const global_index = variable_index.ptrConst(&o.builder).global;
+            gop.value_ptr.* = global_index;
+            // This line invalidates `gop`.
+            const init_val = o.lowerValue(exported_value) catch |err| switch (err) {
+                error.OutOfMemory => return error.OutOfMemory,
+                error.CodegenFail => return error.AnalysisFail,
+            };
+            try variable_index.setInitializer(init_val, &o.builder);
+            break :i global_index;
+        };
+        return updateExportedGlobal(o, mod, global_index, exports);
+    }
+
+    fn updateExportedGlobal(
+        o: *Object,
+        mod: *Module,
+        global_index: Builder.Global.Index,
+        exports: []const *Module.Export,
+    ) link.File.UpdateExportsError!void {
+        global_index.setUnnamedAddr(.default, &o.builder);
+        if (mod.wantDllExports()) global_index.setDllStorageClass(.dllexport, &o.builder);
+        global_index.setLinkage(switch (exports[0].opts.linkage) {
+            .Internal => unreachable,
+            .Strong => .external,
+            .Weak => .weak_odr,
+            .LinkOnce => .linkonce_odr,
+        }, &o.builder);
+        global_index.setVisibility(switch (exports[0].opts.visibility) {
+            .default => .default,
+            .hidden => .hidden,
+            .protected => .protected,
+        }, &o.builder);
+        if (mod.intern_pool.stringToSliceUnwrap(exports[0].opts.section)) |section|
+            switch (global_index.ptrConst(&o.builder).kind) {
+                .variable => |impl_index| impl_index.setSection(
+                    try o.builder.string(section),
+                    &o.builder,
+                ),
+                .function => unreachable,
+                .alias => unreachable,
+                .replaced => unreachable,
+            };
+
+        // If a Decl is exported more than one time (which is rare),
+        // we add aliases for all but the first export.
+        // TODO LLVM C API does not support deleting aliases.
+        // The planned solution to this is https://github.com/ziglang/zig/issues/13265
+        // Until then we iterate over existing aliases and make them point
+        // to the correct decl, or otherwise add a new alias. Old aliases are leaked.
+        for (exports[1..]) |exp| {
+            const exp_name = try o.builder.string(mod.intern_pool.stringToSlice(exp.opts.name));
+            if (o.builder.getGlobal(exp_name)) |global| {
+                switch (global.ptrConst(&o.builder).kind) {
+                    .alias => |alias| {
+                        alias.setAliasee(global_index.toConst(), &o.builder);
+                        continue;
+                    },
+                    .variable, .function => {},
+                    .replaced => unreachable,
+                }
+            }
+            const alias_index = try o.builder.addAlias(
+                .empty,
+                global_index.typeOf(&o.builder),
+                .default,
+                global_index.toConst(),
+            );
+            try alias_index.rename(exp_name, &o.builder);
+        }
+    }
+
     pub fn freeDecl(self: *Object, decl_index: Module.Decl.Index) void {
         const global = self.decl_map.get(decl_index) orelse return;
         global.delete(&self.builder);
src/link/C.zig
@@ -753,14 +753,14 @@ pub fn flushEmitH(module: *Module) !void {
     try file.pwritevAll(all_buffers.items, 0);
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *C,
     module: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
 ) !void {
     _ = exports;
-    _ = decl_index;
+    _ = exported;
     _ = module;
     _ = self;
 }
src/link/Coff.zig
@@ -1075,7 +1075,7 @@ pub fn updateFunc(self: *Coff, mod: *Module, func_index: InternPool.Index, air:
 
     // Since we updated the vaddr and the size, each corresponding export
     // symbol also needs to be updated.
-    return self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    return self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 pub fn lowerUnnamedConst(self: *Coff, tv: TypedValue, decl_index: Module.Decl.Index) !u32 {
@@ -1195,7 +1195,7 @@ pub fn updateDecl(
 
     // Since we updated the vaddr and the size, each corresponding export
     // symbol also needs to be updated.
-    return self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    return self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 fn updateLazySymbolAtom(
@@ -1409,12 +1409,12 @@ pub fn freeDecl(self: *Coff, decl_index: Module.Decl.Index) void {
     }
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *Coff,
     mod: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
-) link.File.UpdateDeclExportsError!void {
+) link.File.UpdateExportsError!void {
     if (build_options.skip_non_native and builtin.object_format != .coff) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
@@ -1425,7 +1425,11 @@ pub fn updateDeclExports(
         // Even in the case of LLVM, we need to notice certain exported symbols in order to
         // detect the default subsystem.
         for (exports) |exp| {
-            const exported_decl = mod.declPtr(exp.exported_decl);
+            const exported_decl_index = switch (exp.exported) {
+                .decl_index => |i| i,
+                .value => continue,
+            };
+            const exported_decl = mod.declPtr(exported_decl_index);
             if (exported_decl.getOwnedFunction(mod) == null) continue;
             const winapi_cc = switch (self.base.options.target.cpu.arch) {
                 .x86 => std.builtin.CallingConvention.Stdcall,
@@ -1452,12 +1456,19 @@ pub fn updateDeclExports(
         }
     }
 
-    if (self.llvm_object) |llvm_object| return llvm_object.updateDeclExports(mod, decl_index, exports);
+    if (self.llvm_object) |llvm_object| return llvm_object.updateExports(mod, exported, exports);
 
     if (self.base.options.emit == null) return;
 
     const gpa = self.base.allocator;
 
+    const decl_index = switch (exported) {
+        .decl_index => |i| i,
+        .value => |val| {
+            _ = val;
+            @panic("TODO: implement COFF linker code for exporting a constant value");
+        },
+    };
     const decl = mod.declPtr(decl_index);
     const atom_index = try self.getOrCreateAtomForDecl(decl_index);
     const atom = self.getAtom(atom_index);
src/link/Elf.zig
@@ -3306,7 +3306,7 @@ pub fn updateFunc(self: *Elf, mod: *Module, func_index: InternPool.Index, air: A
 
     // Since we updated the vaddr and the size, each corresponding export
     // symbol also needs to be updated.
-    return self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    return self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 pub fn updateDecl(
@@ -3388,7 +3388,7 @@ pub fn updateDecl(
 
     // Since we updated the vaddr and the size, each corresponding export
     // symbol also needs to be updated.
-    return self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    return self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 fn updateLazySymbol(self: *Elf, sym: link.File.LazySymbol, symbol_index: Symbol.Index) !void {
@@ -3555,16 +3555,16 @@ fn lowerConst(
     return .{ .ok = sym_index };
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *Elf,
     mod: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
-) link.File.UpdateDeclExportsError!void {
+) link.File.UpdateExportsError!void {
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (self.llvm_object) |llvm_object| return llvm_object.updateDeclExports(mod, decl_index, exports);
+    if (self.llvm_object) |llvm_object| return llvm_object.updateExports(mod, exported, exports);
 
     if (self.base.options.emit == null) return;
 
@@ -3573,6 +3573,13 @@ pub fn updateDeclExports(
 
     const gpa = self.base.allocator;
 
+    const decl_index = switch (exported) {
+        .decl_index => |i| i,
+        .value => |val| {
+            _ = val;
+            @panic("TODO: implement ELF linker code for exporting a constant value");
+        },
+    };
     const zig_module = self.file(self.zig_module_index.?).?.zig_module;
     const decl = mod.declPtr(decl_index);
     const decl_sym_index = try self.getOrCreateMetadataForDecl(decl_index);
src/link/MachO.zig
@@ -1670,7 +1670,7 @@ fn resolveGlobalSymbol(self: *MachO, current: SymbolWithLoc) !void {
     const global_is_weak = global_sym.sect() and (global_sym.weakDef() or global_sym.pext());
 
     if (sym_is_strong and global_is_strong) {
-        // TODO redo this logic with corresponding logic in updateDeclExports to avoid this
+        // TODO redo this logic with corresponding logic in updateExports to avoid this
         // ugly check.
         if (self.mode == .zld) {
             try self.reportSymbolCollision(global, current);
@@ -2180,7 +2180,7 @@ pub fn updateFunc(self: *MachO, mod: *Module, func_index: InternPool.Index, air:
 
     // Since we updated the vaddr and the size, each corresponding export symbol also
     // needs to be updated.
-    try self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    try self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 pub fn lowerUnnamedConst(self: *MachO, typed_value: TypedValue, decl_index: Module.Decl.Index) !u32 {
@@ -2340,7 +2340,7 @@ pub fn updateDecl(self: *MachO, mod: *Module, decl_index: Module.Decl.Index) !vo
 
     // Since we updated the vaddr and the size, each corresponding export symbol also
     // needs to be updated.
-    try self.updateDeclExports(mod, decl_index, mod.getDeclExports(decl_index));
+    try self.updateExports(mod, .{ .decl_index = decl_index }, mod.getDeclExports(decl_index));
 }
 
 fn updateLazySymbolAtom(
@@ -2529,7 +2529,7 @@ fn updateThreadlocalVariable(self: *MachO, module: *Module, decl_index: Module.D
         );
     }
 
-    try self.updateDeclExports(module, decl_index, module.getDeclExports(decl_index));
+    try self.updateExports(module, .{ .decl_index = decl_index }, module.getDeclExports(decl_index));
 
     // 2. Create a TLV descriptor.
     const init_atom_sym_loc = init_atom.getSymbolWithLoc();
@@ -2670,17 +2670,17 @@ pub fn updateDeclLineNumber(self: *MachO, module: *Module, decl_index: Module.De
     }
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *MachO,
     mod: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
-) File.UpdateDeclExportsError!void {
+) File.UpdateExportsError!void {
     if (build_options.skip_non_native and builtin.object_format != .macho) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
     if (self.llvm_object) |llvm_object|
-        return llvm_object.updateDeclExports(mod, decl_index, exports);
+        return llvm_object.updateExports(mod, exported, exports);
 
     if (self.base.options.emit == null) return;
 
@@ -2689,6 +2689,13 @@ pub fn updateDeclExports(
 
     const gpa = self.base.allocator;
 
+    const decl_index = switch (exported) {
+        .decl_index => |i| i,
+        .value => |val| {
+            _ = val;
+            @panic("TODO: implement MachO linker code for exporting a constant value");
+        },
+    };
     const decl = mod.declPtr(decl_index);
     const atom_index = try self.getOrCreateAtomForDecl(decl_index);
     const atom = self.getAtom(atom_index);
src/link/NvPtx.zig
@@ -74,16 +74,16 @@ pub fn updateDecl(self: *NvPtx, module: *Module, decl_index: Module.Decl.Index)
     return self.llvm_object.updateDecl(module, decl_index);
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *NvPtx,
     module: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
 ) !void {
     if (build_options.skip_non_native and builtin.object_format != .nvptx) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    return self.llvm_object.updateDeclExports(module, decl_index, exports);
+    return self.llvm_object.updateExports(module, exported, exports);
 }
 
 pub fn freeDecl(self: *NvPtx, decl_index: Module.Decl.Index) void {
src/link/Plan9.zig
@@ -1116,13 +1116,16 @@ pub fn seeDecl(self: *Plan9, decl_index: Module.Decl.Index) !Atom.Index {
     return atom_idx;
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *Plan9,
     module: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
 ) !void {
-    _ = try self.seeDecl(decl_index);
+    switch (exported) {
+        .value => @panic("TODO: plan9 updateExports handling values"),
+        .decl_index => |decl_index| _ = try self.seeDecl(decl_index),
+    }
     // we do all the things in flush
     _ = module;
     _ = exports;
src/link/SpirV.zig
@@ -120,12 +120,19 @@ pub fn updateDecl(self: *SpirV, module: *Module, decl_index: Module.Decl.Index)
     try self.object.updateDecl(module, decl_index);
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     self: *SpirV,
     mod: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
 ) !void {
+    const decl_index = switch (exported) {
+        .decl_index => |i| i,
+        .value => |val| {
+            _ = val;
+            @panic("TODO: implement SpirV linker code for exporting a constant value");
+        },
+    };
     const decl = mod.declPtr(decl_index);
     if (decl.val.isFuncBody(mod) and decl.ty.fnCallingConvention(mod) == .Kernel) {
         const spv_decl_index = try self.object.resolveDecl(mod, decl_index);
src/link/Wasm.zig
@@ -1786,19 +1786,26 @@ pub fn deleteDeclExport(wasm: *Wasm, decl_index: Module.Decl.Index) void {
     }
 }
 
-pub fn updateDeclExports(
+pub fn updateExports(
     wasm: *Wasm,
     mod: *Module,
-    decl_index: Module.Decl.Index,
+    exported: Module.Exported,
     exports: []const *Module.Export,
 ) !void {
     if (build_options.skip_non_native and builtin.object_format != .wasm) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
     }
-    if (wasm.llvm_object) |llvm_object| return llvm_object.updateDeclExports(mod, decl_index, exports);
+    if (wasm.llvm_object) |llvm_object| return llvm_object.updateExports(mod, exported, exports);
 
     if (wasm.base.options.emit == null) return;
 
+    const decl_index = switch (exported) {
+        .decl_index => |i| i,
+        .value => |val| {
+            _ = val;
+            @panic("TODO: implement Wasm linker code for exporting a constant value");
+        },
+    };
     const decl = mod.declPtr(decl_index);
     const atom_index = try wasm.getOrCreateAtomForDecl(decl_index);
     const atom = wasm.getAtom(atom_index);
@@ -1816,7 +1823,19 @@ pub fn updateDeclExports(
             continue;
         }
 
-        const exported_atom_index = try wasm.getOrCreateAtomForDecl(exp.exported_decl);
+        const exported_decl_index = switch (exp.exported) {
+            .value => {
+                try mod.failed_exports.putNoClobber(gpa, exp, try Module.ErrorMsg.create(
+                    gpa,
+                    decl.srcLoc(mod),
+                    "Unimplemented: exporting a named constant value",
+                    .{},
+                ));
+                continue;
+            },
+            .decl_index => |i| i,
+        };
+        const exported_atom_index = try wasm.getOrCreateAtomForDecl(exported_decl_index);
         const exported_atom = wasm.getAtom(exported_atom_index);
         const export_name = try wasm.string_table.put(wasm.base.allocator, mod.intern_pool.stringToSlice(exp.opts.name));
         const sym_loc = exported_atom.symbolLoc();
src/link.zig
@@ -587,7 +587,7 @@ pub const File = struct {
         }
     }
 
-    /// May be called before or after updateDeclExports for any given Decl.
+    /// May be called before or after updateExports for any given Decl.
     pub fn updateDecl(base: *File, module: *Module, decl_index: Module.Decl.Index) UpdateDeclError!void {
         const decl = module.declPtr(decl_index);
         assert(decl.has_tv);
@@ -609,7 +609,7 @@ pub const File = struct {
         }
     }
 
-    /// May be called before or after updateDeclExports for any given Decl.
+    /// May be called before or after updateExports for any given Decl.
     pub fn updateFunc(base: *File, module: *Module, func_index: InternPool.Index, air: Air, liveness: Liveness) UpdateDeclError!void {
         if (build_options.only_c) {
             assert(base.tag == .c);
@@ -882,33 +882,34 @@ pub const File = struct {
         }
     }
 
-    pub const UpdateDeclExportsError = error{
+    pub const UpdateExportsError = error{
         OutOfMemory,
         AnalysisFail,
     };
 
+    /// This is called for every exported thing. `exports` is almost always
+    /// a list of size 1, meaning that `exported` is exported once. However, it is possible
+    /// to export the same thing with multiple different symbol names (aliases).
     /// May be called before or after updateDecl for any given Decl.
-    pub fn updateDeclExports(
+    pub fn updateExports(
         base: *File,
         module: *Module,
-        decl_index: Module.Decl.Index,
+        exported: Module.Exported,
         exports: []const *Module.Export,
-    ) UpdateDeclExportsError!void {
-        const decl = module.declPtr(decl_index);
-        assert(decl.has_tv);
+    ) UpdateExportsError!void {
         if (build_options.only_c) {
             assert(base.tag == .c);
-            return @fieldParentPtr(C, "base", base).updateDeclExports(module, decl_index, exports);
+            return @fieldParentPtr(C, "base", base).updateExports(module, exported, exports);
         }
         switch (base.tag) {
-            .coff => return @fieldParentPtr(Coff, "base", base).updateDeclExports(module, decl_index, exports),
-            .elf => return @fieldParentPtr(Elf, "base", base).updateDeclExports(module, decl_index, exports),
-            .macho => return @fieldParentPtr(MachO, "base", base).updateDeclExports(module, decl_index, exports),
-            .c => return @fieldParentPtr(C, "base", base).updateDeclExports(module, decl_index, exports),
-            .wasm => return @fieldParentPtr(Wasm, "base", base).updateDeclExports(module, decl_index, exports),
-            .spirv => return @fieldParentPtr(SpirV, "base", base).updateDeclExports(module, decl_index, exports),
-            .plan9 => return @fieldParentPtr(Plan9, "base", base).updateDeclExports(module, decl_index, exports),
-            .nvptx => return @fieldParentPtr(NvPtx, "base", base).updateDeclExports(module, decl_index, exports),
+            .coff => return @fieldParentPtr(Coff, "base", base).updateExports(module, exported, exports),
+            .elf => return @fieldParentPtr(Elf, "base", base).updateExports(module, exported, exports),
+            .macho => return @fieldParentPtr(MachO, "base", base).updateExports(module, exported, exports),
+            .c => return @fieldParentPtr(C, "base", base).updateExports(module, exported, exports),
+            .wasm => return @fieldParentPtr(Wasm, "base", base).updateExports(module, exported, exports),
+            .spirv => return @fieldParentPtr(SpirV, "base", base).updateExports(module, exported, exports),
+            .plan9 => return @fieldParentPtr(Plan9, "base", base).updateExports(module, exported, exports),
+            .nvptx => return @fieldParentPtr(NvPtx, "base", base).updateExports(module, exported, exports),
         }
     }
 
@@ -968,6 +969,20 @@ pub const File = struct {
         }
     }
 
+    pub fn deleteDeclExport(base: *File, decl_index: Module.Decl.Index, name: InternPool.NullTerminatedString) !void {
+        if (build_options.only_c) unreachable;
+        switch (base.tag) {
+            .coff => return @fieldParentPtr(Coff, "base", base).deleteDeclExport(decl_index, name),
+            .elf => return @fieldParentPtr(Elf, "base", base).deleteDeclExport(decl_index, name),
+            .macho => return @fieldParentPtr(MachO, "base", base).deleteDeclExport(decl_index, name),
+            .plan9 => {},
+            .c => {},
+            .wasm => return @fieldParentPtr(Wasm, "base", base).deleteDeclExport(decl_index),
+            .spirv => {},
+            .nvptx => {},
+        }
+    }
+
     /// This function is called by the frontend before flush(). It communicates that
     /// `options.bin_file.emit` directory needs to be renamed from
     /// `[zig-cache]/tmp/[random]` to `[zig-cache]/o/[digest]`.
src/Module.zig
@@ -70,6 +70,8 @@ local_zir_cache: Compilation.Directory,
 /// The Export memory is owned by the `export_owners` table; the slice itself
 /// is owned by this table. The slice is guaranteed to not be empty.
 decl_exports: std.AutoArrayHashMapUnmanaged(Decl.Index, ArrayListUnmanaged(*Export)) = .{},
+/// Same as `decl_exports` but for exported constant values.
+value_exports: std.AutoArrayHashMapUnmanaged(InternPool.Index, ArrayListUnmanaged(*Export)) = .{},
 /// This models the Decls that perform exports, so that `decl_exports` can be updated when a Decl
 /// is modified. Note that the key of this table is not the Decl being exported, but the Decl that
 /// is performing the export of another Decl.
@@ -244,6 +246,13 @@ pub const GlobalEmitH = struct {
 
 pub const ErrorInt = u32;
 
+pub const Exported = union(enum) {
+    /// The Decl being exported. Note this is *not* the Decl performing the export.
+    decl_index: Decl.Index,
+    /// Constant value being exported.
+    value: InternPool.Index,
+};
+
 pub const Export = struct {
     opts: Options,
     src: LazySrcLoc,
@@ -252,8 +261,7 @@ pub const Export = struct {
     /// The Decl containing the export statement.  Inline function calls
     /// may cause this to be different from the owner_decl.
     src_decl: Decl.Index,
-    /// The Decl being exported. Note this is *not* the Decl performing the export.
-    exported_decl: Decl.Index,
+    exported: Exported,
     status: enum {
         in_progress,
         failed,
@@ -2575,6 +2583,11 @@ pub fn deinit(mod: *Module) void {
     }
     mod.decl_exports.deinit(gpa);
 
+    for (mod.value_exports.values()) |*export_list| {
+        export_list.deinit(gpa);
+    }
+    mod.value_exports.deinit(gpa);
+
     for (mod.export_owners.values()) |*value| {
         freeExportList(gpa, value);
     }
@@ -4620,36 +4633,49 @@ fn deleteDeclExports(mod: *Module, decl_index: Decl.Index) Allocator.Error!void
     var export_owners = (mod.export_owners.fetchSwapRemove(decl_index) orelse return).value;
 
     for (export_owners.items) |exp| {
-        if (mod.decl_exports.getPtr(exp.exported_decl)) |value_ptr| {
-            // Remove exports with owner_decl matching the regenerating decl.
-            const list = value_ptr.items;
-            var i: usize = 0;
-            var new_len = list.len;
-            while (i < new_len) {
-                if (list[i].owner_decl == decl_index) {
-                    mem.copyBackwards(*Export, list[i..], list[i + 1 .. new_len]);
-                    new_len -= 1;
-                } else {
-                    i += 1;
+        switch (exp.exported) {
+            .decl_index => |exported_decl_index| {
+                if (mod.decl_exports.getPtr(exported_decl_index)) |export_list| {
+                    // Remove exports with owner_decl matching the regenerating decl.
+                    const list = export_list.items;
+                    var i: usize = 0;
+                    var new_len = list.len;
+                    while (i < new_len) {
+                        if (list[i].owner_decl == decl_index) {
+                            mem.copyBackwards(*Export, list[i..], list[i + 1 .. new_len]);
+                            new_len -= 1;
+                        } else {
+                            i += 1;
+                        }
+                    }
+                    export_list.shrinkAndFree(mod.gpa, new_len);
+                    if (new_len == 0) {
+                        assert(mod.decl_exports.swapRemove(exported_decl_index));
+                    }
                 }
-            }
-            value_ptr.shrinkAndFree(mod.gpa, new_len);
-            if (new_len == 0) {
-                assert(mod.decl_exports.swapRemove(exp.exported_decl));
-            }
-        }
-        if (mod.comp.bin_file.cast(link.File.Elf)) |elf| {
-            elf.deleteDeclExport(decl_index, exp.opts.name);
-        }
-        if (mod.comp.bin_file.cast(link.File.MachO)) |macho| {
-            try macho.deleteDeclExport(decl_index, exp.opts.name);
-        }
-        if (mod.comp.bin_file.cast(link.File.Wasm)) |wasm| {
-            wasm.deleteDeclExport(decl_index);
-        }
-        if (mod.comp.bin_file.cast(link.File.Coff)) |coff| {
-            coff.deleteDeclExport(decl_index, exp.opts.name);
+            },
+            .value => |value| {
+                if (mod.value_exports.getPtr(value)) |export_list| {
+                    // Remove exports with owner_decl matching the regenerating decl.
+                    const list = export_list.items;
+                    var i: usize = 0;
+                    var new_len = list.len;
+                    while (i < new_len) {
+                        if (list[i].owner_decl == decl_index) {
+                            mem.copyBackwards(*Export, list[i..], list[i + 1 .. new_len]);
+                            new_len -= 1;
+                        } else {
+                            i += 1;
+                        }
+                    }
+                    export_list.shrinkAndFree(mod.gpa, new_len);
+                    if (new_len == 0) {
+                        assert(mod.value_exports.swapRemove(value));
+                    }
+                }
+            },
         }
+        try mod.comp.bin_file.deleteDeclExport(decl_index, exp.opts.name);
         if (mod.failed_exports.fetchSwapRemove(exp)) |failed_kv| {
             failed_kv.value.destroy(mod.gpa);
         }
@@ -5503,48 +5529,63 @@ pub fn processOutdatedAndDeletedDecls(mod: *Module) !void {
 /// reporting compile errors. In this function we emit exported symbol collision
 /// errors and communicate exported symbols to the linker backend.
 pub fn processExports(mod: *Module) !void {
-    const gpa = mod.gpa;
     // Map symbol names to `Export` for name collision detection.
-    var symbol_exports: std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, *Export) = .{};
-    defer symbol_exports.deinit(gpa);
-
-    var it = mod.decl_exports.iterator();
-    while (it.next()) |entry| {
-        const exported_decl = entry.key_ptr.*;
-        const exports = entry.value_ptr.items;
-        for (exports) |new_export| {
-            const gop = try symbol_exports.getOrPut(gpa, new_export.opts.name);
-            if (gop.found_existing) {
-                new_export.status = .failed_retryable;
-                try mod.failed_exports.ensureUnusedCapacity(gpa, 1);
-                const src_loc = new_export.getSrcLoc(mod);
-                const msg = try ErrorMsg.create(gpa, src_loc, "exported symbol collision: {}", .{
-                    new_export.opts.name.fmt(&mod.intern_pool),
-                });
-                errdefer msg.destroy(gpa);
-                const other_export = gop.value_ptr.*;
-                const other_src_loc = other_export.getSrcLoc(mod);
-                try mod.errNoteNonLazy(other_src_loc, msg, "other symbol here", .{});
-                mod.failed_exports.putAssumeCapacityNoClobber(new_export, msg);
-                new_export.status = .failed;
-            } else {
-                gop.value_ptr.* = new_export;
-            }
+    var symbol_exports: SymbolExports = .{};
+    defer symbol_exports.deinit(mod.gpa);
+
+    for (mod.decl_exports.keys(), mod.decl_exports.values()) |exported_decl, exports_list| {
+        const exported: Exported = .{ .decl_index = exported_decl };
+        try processExportsInner(mod, &symbol_exports, exported, exports_list.items);
+    }
+
+    for (mod.value_exports.keys(), mod.value_exports.values()) |exported_value, exports_list| {
+        const exported: Exported = .{ .value = exported_value };
+        try processExportsInner(mod, &symbol_exports, exported, exports_list.items);
+    }
+}
+
+const SymbolExports = std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, *Export);
+
+fn processExportsInner(
+    mod: *Module,
+    symbol_exports: *SymbolExports,
+    exported: Exported,
+    exports: []const *Export,
+) error{OutOfMemory}!void {
+    const gpa = mod.gpa;
+
+    for (exports) |new_export| {
+        const gop = try symbol_exports.getOrPut(gpa, new_export.opts.name);
+        if (gop.found_existing) {
+            new_export.status = .failed_retryable;
+            try mod.failed_exports.ensureUnusedCapacity(gpa, 1);
+            const src_loc = new_export.getSrcLoc(mod);
+            const msg = try ErrorMsg.create(gpa, src_loc, "exported symbol collision: {}", .{
+                new_export.opts.name.fmt(&mod.intern_pool),
+            });
+            errdefer msg.destroy(gpa);
+            const other_export = gop.value_ptr.*;
+            const other_src_loc = other_export.getSrcLoc(mod);
+            try mod.errNoteNonLazy(other_src_loc, msg, "other symbol here", .{});
+            mod.failed_exports.putAssumeCapacityNoClobber(new_export, msg);
+            new_export.status = .failed;
+        } else {
+            gop.value_ptr.* = new_export;
         }
-        mod.comp.bin_file.updateDeclExports(mod, exported_decl, exports) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            else => {
-                const new_export = exports[0];
-                new_export.status = .failed_retryable;
-                try mod.failed_exports.ensureUnusedCapacity(gpa, 1);
-                const src_loc = new_export.getSrcLoc(mod);
-                const msg = try ErrorMsg.create(gpa, src_loc, "unable to export: {s}", .{
-                    @errorName(err),
-                });
-                mod.failed_exports.putAssumeCapacityNoClobber(new_export, msg);
-            },
-        };
     }
+    mod.comp.bin_file.updateExports(mod, exported, exports) catch |err| switch (err) {
+        error.OutOfMemory => return error.OutOfMemory,
+        else => {
+            const new_export = exports[0];
+            new_export.status = .failed_retryable;
+            try mod.failed_exports.ensureUnusedCapacity(gpa, 1);
+            const src_loc = new_export.getSrcLoc(mod);
+            const msg = try ErrorMsg.create(gpa, src_loc, "unable to export: {s}", .{
+                @errorName(err),
+            });
+            mod.failed_exports.putAssumeCapacityNoClobber(new_export, msg);
+        },
+    };
 }
 
 pub fn populateTestFunctions(
src/Sema.zig
@@ -6026,6 +6026,7 @@ fn zirExportValue(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     const tracy = trace(@src());
     defer tracy.end();
 
+    const mod = sema.mod;
     const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
     const extra = sema.code.extraData(Zir.Inst.ExportValue, inst_data.payload_index).data;
     const src = inst_data.src();
@@ -6035,12 +6036,21 @@ fn zirExportValue(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         .needed_comptime_reason = "export target must be comptime-known",
     });
     const options = try sema.resolveExportOptions(block, options_src, extra.options);
-    const decl_index = if (operand.val.getFunction(sema.mod)) |function| function.owner_decl else blk: {
-        var anon_decl = try block.startAnonDecl(); // TODO: export value without Decl
-        defer anon_decl.deinit();
-        break :blk try anon_decl.finish(operand.ty, operand.val, .none);
-    };
-    try sema.analyzeExport(block, src, options, decl_index);
+    if (options.linkage == .Internal)
+        return;
+    if (operand.val.getFunction(mod)) |function| {
+        const decl_index = function.owner_decl;
+        return sema.analyzeExport(block, src, options, decl_index);
+    }
+
+    try addExport(mod, .{
+        .opts = options,
+        .src = src,
+        .owner_decl = sema.owner_decl_index,
+        .src_decl = block.src_decl,
+        .exported = .{ .value = operand.val.toIntern() },
+        .status = .in_progress,
+    });
 }
 
 pub fn analyzeExport(
@@ -6050,12 +6060,11 @@ pub fn analyzeExport(
     options: Module.Export.Options,
     exported_decl_index: Decl.Index,
 ) !void {
-    const Export = Module.Export;
+    const gpa = sema.gpa;
     const mod = sema.mod;
 
-    if (options.linkage == .Internal) {
+    if (options.linkage == .Internal)
         return;
-    }
 
     try mod.ensureDeclAnalyzed(exported_decl_index);
     const exported_decl = mod.declPtr(exported_decl_index);
@@ -6063,7 +6072,7 @@ pub fn analyzeExport(
     if (!try sema.validateExternType(exported_decl.ty, .other)) {
         const msg = msg: {
             const msg = try sema.errMsg(block, src, "unable to export type '{}'", .{exported_decl.ty.fmt(mod)});
-            errdefer msg.destroy(sema.gpa);
+            errdefer msg.destroy(gpa);
 
             const src_decl = mod.declPtr(block.src_decl);
             try sema.explainWhyTypeIsNotExtern(msg, src.toSrcLoc(src_decl, mod), exported_decl.ty, .other);
@@ -6083,38 +6092,45 @@ pub fn analyzeExport(
     try mod.markDeclAlive(exported_decl);
     try sema.maybeQueueFuncBodyAnalysis(exported_decl_index);
 
-    const gpa = sema.gpa;
+    try addExport(mod, .{
+        .opts = options,
+        .src = src,
+        .owner_decl = sema.owner_decl_index,
+        .src_decl = block.src_decl,
+        .exported = .{ .decl_index = exported_decl_index },
+        .status = .in_progress,
+    });
+}
+
+fn addExport(mod: *Module, export_init: Module.Export) error{OutOfMemory}!void {
+    const gpa = mod.gpa;
 
     try mod.decl_exports.ensureUnusedCapacity(gpa, 1);
+    try mod.value_exports.ensureUnusedCapacity(gpa, 1);
     try mod.export_owners.ensureUnusedCapacity(gpa, 1);
 
-    const new_export = try gpa.create(Export);
+    const new_export = try gpa.create(Module.Export);
     errdefer gpa.destroy(new_export);
 
-    new_export.* = .{
-        .opts = options,
-        .src = src,
-        .owner_decl = sema.owner_decl_index,
-        .src_decl = block.src_decl,
-        .exported_decl = exported_decl_index,
-        .status = .in_progress,
-    };
+    new_export.* = export_init;
 
-    // Add to export_owners table.
-    const eo_gop = mod.export_owners.getOrPutAssumeCapacity(sema.owner_decl_index);
-    if (!eo_gop.found_existing) {
-        eo_gop.value_ptr.* = .{};
-    }
+    const eo_gop = mod.export_owners.getOrPutAssumeCapacity(export_init.owner_decl);
+    if (!eo_gop.found_existing) eo_gop.value_ptr.* = .{};
     try eo_gop.value_ptr.append(gpa, new_export);
     errdefer _ = eo_gop.value_ptr.pop();
 
-    // Add to exported_decl table.
-    const de_gop = mod.decl_exports.getOrPutAssumeCapacity(exported_decl_index);
-    if (!de_gop.found_existing) {
-        de_gop.value_ptr.* = .{};
+    switch (export_init.exported) {
+        .decl_index => |decl_index| {
+            const de_gop = mod.decl_exports.getOrPutAssumeCapacity(decl_index);
+            if (!de_gop.found_existing) de_gop.value_ptr.* = .{};
+            try de_gop.value_ptr.append(gpa, new_export);
+        },
+        .value => |value| {
+            const ve_gop = mod.value_exports.getOrPutAssumeCapacity(value);
+            if (!ve_gop.found_existing) ve_gop.value_ptr.* = .{};
+            try ve_gop.value_ptr.append(gpa, new_export);
+        },
     }
-    try de_gop.value_ptr.append(gpa, new_export);
-    errdefer _ = de_gop.value_ptr.pop();
 }
 
 fn zirSetAlignStack(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!void {
test/behavior/export.zig
@@ -72,6 +72,8 @@ test "exporting using field access" {
 }
 
 test "exporting comptime-known value" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
+
     const x: u32 = 10;
     @export(x, .{ .name = "exporting_comptime_known_value_foo" });
     const S = struct {
@@ -81,6 +83,8 @@ test "exporting comptime-known value" {
 }
 
 test "exporting comptime var" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
+
     comptime var x: u32 = 5;
     @export(x, .{ .name = "exporting_comptime_var_foo" });
     x = 7; // modifying this now shouldn't change anything