Commit 06ee383da9

Mason Remaley <mason@anthropicstudios.com>
2025-02-16 00:42:59
compiler: allow `@import` of ZON without a result type
In particular, this allows importing `build.zig.zon` at comptime.
1 parent 1b62a22
src/Sema/LowerZon.zig
@@ -33,7 +33,7 @@ pub fn run(
     sema: *Sema,
     file: *File,
     file_index: Zcu.File.Index,
-    res_ty: Type,
+    res_ty_interned: InternPool.Index,
     import_loc: LazySrcLoc,
     block: *Sema.Block,
 ) CompileError!InternPool.Index {
@@ -53,13 +53,167 @@ pub fn run(
         .base_node_inst = tracked_inst,
     };
 
-    try lower_zon.checkType(res_ty);
+    if (res_ty_interned == .none) {
+        return lower_zon.lowerExprAnonResTy(.root);
+    } else {
+        const res_ty: Type = .fromInterned(res_ty_interned);
+        try lower_zon.checkType(res_ty);
+        return lower_zon.lowerExprKnownResTy(.root, res_ty);
+    }
+}
 
-    return lower_zon.lowerExpr(.root, res_ty);
+fn lowerExprAnonResTy(self: *LowerZon, node: Zoir.Node.Index) CompileError!InternPool.Index {
+    const gpa = self.sema.gpa;
+    const pt = self.sema.pt;
+    const ip = &pt.zcu.intern_pool;
+    switch (node.get(self.file.zoir.?)) {
+        .true => return .bool_true,
+        .false => return .bool_false,
+        .null => return .null_value,
+        .pos_inf => return self.fail(node, "infinity requires a known result type", .{}),
+        .neg_inf => return self.fail(node, "negative infinity requires a known result type", .{}),
+        .nan => return self.fail(node, "NaN requires a known result type", .{}),
+        .int_literal => |int| switch (int) {
+            .small => |val| return pt.intern(.{ .int = .{
+                .ty = .comptime_int_type,
+                .storage = .{ .i64 = val },
+            } }),
+            .big => |val| return pt.intern(.{ .int = .{
+                .ty = .comptime_int_type,
+                .storage = .{ .big_int = val },
+            } }),
+        },
+        .float_literal => |val| {
+            const result = try pt.floatValue(.comptime_float, val);
+            return result.toIntern();
+        },
+        .char_literal => |val| return pt.intern(.{ .int = .{
+            .ty = .comptime_int_type,
+            .storage = .{ .i64 = val },
+        } }),
+        .enum_literal => |val| return pt.intern(.{
+            .enum_literal = try ip.getOrPutString(
+                gpa,
+                pt.tid,
+                val.get(self.file.zoir.?),
+                .no_embedded_nulls,
+            ),
+        }),
+        .string_literal => |val| {
+            const ip_str = try ip.getOrPutString(gpa, pt.tid, val, .maybe_embedded_nulls);
+            const result = try self.sema.addStrLit(ip_str, val.len);
+            return result.toInterned().?;
+        },
+        .empty_literal => return .empty_tuple,
+        .array_literal => |nodes| {
+            const types = try self.sema.arena.alloc(InternPool.Index, nodes.len);
+            const values = try self.sema.arena.alloc(InternPool.Index, nodes.len);
+            for (0..nodes.len) |i| {
+                values[i] = try self.lowerExprAnonResTy(nodes.at(@intCast(i)));
+                types[i] = Value.fromInterned(values[i]).typeOf(pt.zcu).toIntern();
+            }
+            const ty = try ip.getTupleType(
+                gpa,
+                pt.tid,
+                .{
+                    .types = types,
+                    .values = values,
+                },
+            );
+            return pt.intern(.{ .aggregate = .{
+                .ty = ty,
+                .storage = .{ .elems = values },
+            } });
+        },
+        .struct_literal => |init| {
+            const elems = try self.sema.arena.alloc(InternPool.Index, init.names.len);
+            for (0..init.names.len) |i| {
+                elems[i] = try self.lowerExprAnonResTy(init.vals.at(@intCast(i)));
+            }
+            const struct_ty = switch (try ip.getStructType(
+                gpa,
+                pt.tid,
+                .{
+                    .layout = .auto,
+                    .fields_len = @intCast(init.names.len),
+                    .known_non_opv = false,
+                    .requires_comptime = .no,
+                    .any_comptime_fields = true,
+                    .any_default_inits = true,
+                    .inits_resolved = true,
+                    .any_aligned_fields = false,
+                    .key = .{ .reified = .{
+                        .zir_index = self.base_node_inst,
+                        .type_hash = hash: {
+                            var hasher: std.hash.Wyhash = .init(0);
+                            hasher.update(std.mem.asBytes(&node));
+                            hasher.update(std.mem.sliceAsBytes(elems));
+                            hasher.update(std.mem.sliceAsBytes(init.names));
+                            break :hash hasher.final();
+                        },
+                    } },
+                },
+                false,
+            )) {
+                .wip => |wip| ty: {
+                    errdefer wip.cancel(ip, pt.tid);
+                    wip.setName(ip, try self.sema.createTypeName(
+                        self.block,
+                        .anon,
+                        "struct",
+                        self.base_node_inst.resolve(ip),
+                        wip.index,
+                    ));
+
+                    const struct_type = ip.loadStructType(wip.index);
+
+                    for (init.names, 0..) |name, field_idx| {
+                        const name_interned = try ip.getOrPutString(
+                            gpa,
+                            pt.tid,
+                            name.get(self.file.zoir.?),
+                            .no_embedded_nulls,
+                        );
+                        assert(struct_type.addFieldName(ip, name_interned) == null);
+                        struct_type.setFieldComptime(ip, field_idx);
+                    }
+
+                    @memcpy(struct_type.field_inits.get(ip), elems);
+                    const types = struct_type.field_types.get(ip);
+                    for (0..init.names.len) |i| {
+                        types[i] = Value.fromInterned(elems[i]).typeOf(pt.zcu).toIntern();
+                    }
+
+                    const new_namespace_index = try pt.createNamespace(.{
+                        .parent = self.block.namespace.toOptional(),
+                        .owner_type = wip.index,
+                        .file_scope = self.block.getFileScopeIndex(pt.zcu),
+                        .generation = pt.zcu.generation,
+                    });
+                    try pt.zcu.comp.queueJob(.{ .resolve_type_fully = wip.index });
+                    codegen_type: {
+                        if (pt.zcu.comp.config.use_llvm) break :codegen_type;
+                        if (self.block.ownerModule().strip) break :codegen_type;
+                        try pt.zcu.comp.queueJob(.{ .codegen_type = wip.index });
+                    }
+                    break :ty wip.finish(ip, new_namespace_index);
+                },
+                .existing => |ty| ty,
+            };
+            try self.sema.declareDependency(.{ .interned = struct_ty });
+            try self.sema.addTypeReferenceEntry(self.nodeSrc(node), struct_ty);
+
+            return try pt.intern(.{ .aggregate = .{
+                .ty = struct_ty,
+                .storage = .{ .elems = elems },
+            } });
+        },
+    }
 }
 
-/// Validate that `ty` is a valid ZON type. If not, emit a compile error.
-/// i.e. no nested optionals, no error sets, etc.
+/// Validate that `ty` is a valid ZON type, or emit a compile error.
+///
+/// Rules out nested optionals, error sets, etc.
 fn checkType(self: *LowerZon, ty: Type) !void {
     var visited: std.AutoHashMapUnmanaged(InternPool.Index, void) = .empty;
     try self.checkTypeInner(ty, null, &visited);
@@ -201,9 +355,9 @@ fn fail(
     return self.sema.failWithOwnedErrorMsg(self.block, err_msg);
 }
 
-fn lowerExpr(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) CompileError!InternPool.Index {
+fn lowerExprKnownResTy(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) CompileError!InternPool.Index {
     const pt = self.sema.pt;
-    return self.lowerExprInner(node, res_ty) catch |err| switch (err) {
+    return self.lowerExprKnownResTyInner(node, res_ty) catch |err| switch (err) {
         error.WrongType => return self.fail(
             node,
             "expected type '{}'",
@@ -213,7 +367,7 @@ fn lowerExpr(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) CompileError!
     };
 }
 
-fn lowerExprInner(
+fn lowerExprKnownResTyInner(
     self: *LowerZon,
     node: Zoir.Node.Index,
     res_ty: Type,
@@ -227,7 +381,7 @@ fn lowerExprInner(
                     break :b .none;
                 } else b: {
                     const child_type = res_ty.optionalChild(pt.zcu);
-                    break :b try self.lowerExprInner(node, child_type);
+                    break :b try self.lowerExprKnownResTyInner(node, child_type);
                 },
             },
         }),
@@ -239,7 +393,7 @@ fn lowerExprInner(
                     .base_addr = .{
                         .uav = .{
                             .orig_ty = res_ty.toIntern(),
-                            .val = try self.lowerExprInner(node, .fromInterned(ptr_info.child)),
+                            .val = try self.lowerExprKnownResTyInner(node, .fromInterned(ptr_info.child)),
                         },
                     },
                     .byte_offset = 0,
@@ -486,7 +640,7 @@ fn lowerArray(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool.
     );
 
     for (0..nodes.len) |i| {
-        elems[i] = try self.lowerExpr(nodes.at(@intCast(i)), array_info.elem_type);
+        elems[i] = try self.lowerExprKnownResTy(nodes.at(@intCast(i)), array_info.elem_type);
     }
 
     if (array_info.sentinel) |sentinel| {
@@ -587,7 +741,7 @@ fn lowerTuple(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool.
             );
         }
 
-        const val = try self.lowerExpr(elem_nodes.at(@intCast(i)), .fromInterned(field_types[i]));
+        const val = try self.lowerExprKnownResTy(elem_nodes.at(@intCast(i)), .fromInterned(field_types[i]));
 
         if (elems[i] != .none and val != elems[i]) {
             const elem_node = elem_nodes.at(@intCast(i));
@@ -650,7 +804,7 @@ fn lowerStruct(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool
         };
 
         const field_type: Type = .fromInterned(struct_info.field_types.get(ip)[name_index]);
-        field_values[name_index] = try self.lowerExpr(field_node, field_type);
+        field_values[name_index] = try self.lowerExprKnownResTy(field_node, field_type);
 
         if (struct_info.comptime_bits.getBit(ip, name_index)) {
             const val = ip.indexToKey(field_values[name_index]);
@@ -715,7 +869,7 @@ fn lowerSlice(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool.
     const elems = try self.sema.arena.alloc(InternPool.Index, elem_nodes.len + @intFromBool(ptr_info.sentinel != .none));
 
     for (elems, 0..) |*elem, i| {
-        elem.* = try self.lowerExpr(elem_nodes.at(@intCast(i)), .fromInterned(ptr_info.child));
+        elem.* = try self.lowerExprKnownResTy(elem_nodes.at(@intCast(i)), .fromInterned(ptr_info.child));
     }
 
     if (ptr_info.sentinel != .none) {
@@ -810,7 +964,7 @@ fn lowerUnion(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool.
         if (field_type.toIntern() == .void_type) {
             return self.fail(field_node, "expected type 'void'", .{});
         }
-        break :b try self.lowerExpr(field_node, field_type);
+        break :b try self.lowerExprKnownResTy(field_node, field_type);
     } else b: {
         if (field_type.toIntern() != .void_type) {
             return error.WrongType;
@@ -846,7 +1000,7 @@ fn lowerVector(self: *LowerZon, node: Zoir.Node.Index, res_ty: Type) !InternPool
     }
 
     for (elems, 0..) |*elem, i| {
-        elem.* = try self.lowerExpr(elem_nodes.at(@intCast(i)), .fromInterned(vector_info.child));
+        elem.* = try self.lowerExprKnownResTy(elem_nodes.at(@intCast(i)), .fromInterned(vector_info.child));
     }
 
     return self.sema.pt.intern(.{ .aggregate = .{
src/InternPool.zig
@@ -2136,6 +2136,7 @@ pub const Key = union(enum) {
         /// To avoid making this key overly complex, the type-specific data is hashed by Sema.
         reified: struct {
             /// A `reify`, `struct_init`, `struct_init_ref`, or `struct_init_anon` instruction.
+            /// Alternatively, this is `main_struct_inst` of a ZON file.
             zir_index: TrackedInst.Index,
             /// A hash of this type's attributes, fields, etc, generated by Sema.
             type_hash: u64,
src/Sema.zig
@@ -2998,7 +2998,7 @@ fn zirStructDecl(
     return Air.internedToRef(wip_ty.finish(ip, new_namespace_index));
 }
 
-fn createTypeName(
+pub fn createTypeName(
     sema: *Sema,
     block: *Block,
     name_strategy: Zir.Inst.NameStrategy,
@@ -14065,14 +14065,13 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
             return Air.internedToRef(ty);
         },
         .zon => {
-            if (extra.res_ty == .none) {
-                return sema.fail(block, operand_src, "'@import' of ZON must have a known result type", .{});
-            }
-            const res_ty_inst = try sema.resolveInst(extra.res_ty);
-            const res_ty = try sema.analyzeAsType(block, operand_src, res_ty_inst);
-            if (res_ty.isGenericPoison()) {
-                return sema.fail(block, operand_src, "'@import' of ZON must have a known result type", .{});
-            }
+            const res_ty: InternPool.Index = b: {
+                if (extra.res_ty == .none) break :b .none;
+                const res_ty_inst = try sema.resolveInst(extra.res_ty);
+                const res_ty = try sema.analyzeAsType(block, operand_src, res_ty_inst);
+                if (res_ty.isGenericPoison()) break :b .none;
+                break :b res_ty.toIntern();
+            };
 
             try sema.declareDependency(.{ .zon_file = result.file_index });
             const interned = try LowerZon.run(
@@ -31699,7 +31698,7 @@ fn addReferenceEntry(
     try zcu.addUnitReference(sema.owner, referenced_unit, src);
 }
 
-fn addTypeReferenceEntry(
+pub fn addTypeReferenceEntry(
     sema: *Sema,
     src: LazySrcLoc,
     referenced_type: InternPool.Index,
src/Type.zig
@@ -3589,7 +3589,10 @@ pub fn typeDeclSrcLine(ty: Type, zcu: *Zcu) ?u32 {
     };
     const info = tracked.resolveFull(&zcu.intern_pool) orelse return null;
     const file = zcu.fileByIndex(info.file);
-    const zir = file.zir.?;
+    const zir = switch (file.getMode()) {
+        .zig => file.zir.?,
+        .zon => return 0,
+    };
     const inst = zir.instructions.get(@intFromEnum(info.inst));
     return switch (inst.tag) {
         .struct_init, .struct_init_ref => zir.extraData(Zir.Inst.StructInit, inst.data.pl_node.payload_index).data.abs_line,
test/behavior/zon/anon.zon
@@ -0,0 +1,16 @@
+.{
+    .{
+        .bool_true = true,
+        .bool_false = false,
+        .string = "foo",
+    },
+    .{
+        null,
+        10,
+        36893488147419103232,
+        1.234,
+        'z',
+        .bar,
+        .{},
+    },
+}
test/behavior/zon/build.zig.zon
@@ -0,0 +1,20 @@
+.{
+    // Comment
+    .name = "temp",
+    .version = "0.0.0",
+    .dependencies = .{
+        .example_0 = .{
+            .url = "https://example.com/foo.tar.gz",
+            .hash = "...",
+        },
+        .example_1 = .{
+            .path = "../foo",
+            .lazy = false,
+        },
+    },
+    .paths = .{
+        "build.zig",
+        "build.zig.zon",
+        "src",
+    },
+}
test/behavior/zon.zig
@@ -517,3 +517,57 @@ test "recursive" {
     const expected: Recursive = .{ .foo = &.{ .foo = null } };
     try expectEqualDeep(expected, @as(Recursive, @import("zon/recursive.zon")));
 }
+
+test "anon" {
+    const expected = .{
+        .{
+            .bool_true = true,
+            .bool_false = false,
+            .string = "foo",
+        },
+        .{
+            null,
+            10,
+            36893488147419103232,
+            1.234,
+            'z',
+            .bar,
+            .{},
+        },
+    };
+
+    const actual = @import("zon/anon.zon");
+    try expectEqual(expected.len, actual.len);
+    try expectEqual(expected[1], actual[1]);
+    const expected_struct = expected[0];
+    const actual_struct = actual[0];
+    const expected_fields = @typeInfo(@TypeOf(expected_struct)).@"struct".fields;
+    const actual_fields = @typeInfo(@TypeOf(actual_struct)).@"struct".fields;
+    try expectEqual(expected_fields.len, actual_fields.len);
+    inline for (expected_fields) |field| {
+        try expectEqual(@field(expected_struct, field.name), @field(actual_struct, field.name));
+    }
+}
+
+test "build.zig.zon" {
+    const build = @import("zon/build.zig.zon");
+
+    try expectEqual(4, @typeInfo(@TypeOf(build)).@"struct".fields.len);
+    try expectEqualStrings("temp", build.name);
+    try expectEqualStrings("0.0.0", build.version);
+
+    const dependencies = build.dependencies;
+    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);
+
+    const example_0 = dependencies.example_0;
+    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);
+    try expectEqualStrings("https://example.com/foo.tar.gz", example_0.url);
+    try expectEqualStrings("...", example_0.hash);
+
+    const example_1 = dependencies.example_1;
+    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);
+    try expectEqualStrings("../foo", example_1.path);
+    try expectEqual(false, example_1.lazy);
+
+    try expectEqual(.{ "build.zig", "build.zig.zon", "src" }, build.paths);
+}
test/cases/compile_errors/@import_zon_anon_inf.zig
@@ -0,0 +1,9 @@
+export fn entry() void {
+    _ = @import("zon/inf.zon");
+}
+
+// error
+// imports=zon/inf.zon
+//
+// inf.zon:1:1: error: infinity requires a known result type
+// tmp.zig:2:17: note: imported here
test/cases/compile_errors/@import_zon_anon_nan.zig
@@ -0,0 +1,9 @@
+export fn entry() void {
+    _ = @import("zon/nan.zon");
+}
+
+// error
+// imports=zon/nan.zon
+//
+// nan.zon:1:1: error: NaN requires a known result type
+// tmp.zig:2:17: note: imported here
test/cases/compile_errors/@import_zon_anon_neg_inf.zig
@@ -0,0 +1,9 @@
+export fn entry() void {
+    _ = @import("zon/neg_inf.zon");
+}
+
+// error
+// imports=zon/neg_inf.zon
+//
+// neg_inf.zon:1:1: error: negative infinity requires a known result type
+// tmp.zig:2:17: note: imported here
test/cases/compile_errors/@import_zon_no_rt.zig
@@ -1,9 +0,0 @@
-export fn entry() void {
-    const f = @import("zon/simple_union.zon");
-    _ = f;
-}
-
-// error
-// imports=zon/simple_union.zon
-//
-// tmp.zig:2:23: error: '@import' of ZON must have a known result type