Commit 48af67c152

mlugg <mlugg@mlugg.co.uk>
2024-03-08 11:38:54
Zcu: rename implicitly-named decls to avoid overriding by explicit decls
1 parent 0096906
Changed files (2)
src/Module.zig
@@ -4083,6 +4083,9 @@ pub fn scanNamespace(
     const gpa = zcu.gpa;
     const namespace = zcu.namespacePtr(namespace_index);
 
+    var seen_decls: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{};
+    defer seen_decls.deinit(gpa);
+
     try zcu.comp.work_queue.ensureUnusedCapacity(decls.len);
     try namespace.decls.ensureTotalCapacity(gpa, decls.len);
 
@@ -4090,19 +4093,44 @@ pub fn scanNamespace(
         .zcu = zcu,
         .namespace_index = namespace_index,
         .parent_decl = parent_decl,
+        .seen_decls = &seen_decls,
+        .pass = .named,
     };
     for (decls) |decl_inst| {
         try scanDecl(&scan_decl_iter, decl_inst);
     }
+    scan_decl_iter.pass = .unnamed;
+    for (decls) |decl_inst| {
+        try scanDecl(&scan_decl_iter, decl_inst);
+    }
 }
 
 const ScanDeclIter = struct {
     zcu: *Zcu,
     namespace_index: Namespace.Index,
     parent_decl: *Decl,
+    seen_decls: *std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, void),
+    /// Decl scanning is run in two passes, so that we can detect when a generated
+    /// name would clash with an explicit name and use a different one.
+    pass: enum { named, unnamed },
     usingnamespace_index: usize = 0,
     comptime_index: usize = 0,
     unnamed_test_index: usize = 0,
+
+    fn avoidNameConflict(iter: *ScanDeclIter, comptime fmt: []const u8, args: anytype) !InternPool.NullTerminatedString {
+        const zcu = iter.zcu;
+        const gpa = zcu.gpa;
+        const ip = &zcu.intern_pool;
+        var name = try ip.getOrPutStringFmt(gpa, fmt, args);
+        var gop = try iter.seen_decls.getOrPut(gpa, name);
+        var next_suffix: u32 = 0;
+        while (gop.found_existing) {
+            name = try ip.getOrPutStringFmt(gpa, fmt ++ "_{d}", args ++ .{next_suffix});
+            gop = try iter.seen_decls.getOrPut(gpa, name);
+            next_suffix += 1;
+        }
+        return name;
+    }
 };
 
 fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void {
@@ -4126,54 +4154,63 @@ fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void
     // Every Decl needs a name.
     const decl_name: InternPool.NullTerminatedString, const kind: Decl.Kind, const is_named_test: bool = switch (declaration.name) {
         .@"comptime" => info: {
+            if (iter.pass != .unnamed) return;
             const i = iter.comptime_index;
             iter.comptime_index += 1;
-            // TODO: avoid collisions with named decls with this name
             break :info .{
-                try ip.getOrPutStringFmt(gpa, "comptime_{d}", .{i}),
+                try iter.avoidNameConflict("comptime_{d}", .{i}),
                 .@"comptime",
                 false,
             };
         },
         .@"usingnamespace" => info: {
+            if (iter.pass != .unnamed) return;
             const i = iter.usingnamespace_index;
             iter.usingnamespace_index += 1;
-            // TODO: avoid collisions with named decls with this name
             break :info .{
-                try ip.getOrPutStringFmt(gpa, "usingnamespace_{d}", .{i}),
+                try iter.avoidNameConflict("usingnamespace_{d}", .{i}),
                 .@"usingnamespace",
                 false,
             };
         },
         .unnamed_test => info: {
+            if (iter.pass != .unnamed) return;
             const i = iter.unnamed_test_index;
             iter.unnamed_test_index += 1;
-            // TODO: avoid collisions with named decls with this name
             break :info .{
-                try ip.getOrPutStringFmt(gpa, "test_{d}", .{i}),
+                try iter.avoidNameConflict("test_{d}", .{i}),
                 .@"test",
                 false,
             };
         },
         .decltest => info: {
+            // We consider these to be unnamed since the decl name can be adjusted to avoid conflicts if necessary.
+            if (iter.pass != .unnamed) return;
             assert(declaration.flags.has_doc_comment);
             const name = zir.nullTerminatedString(@enumFromInt(zir.extra[extra.end]));
-            // TODO: avoid collisions with named decls with this name
             break :info .{
-                try ip.getOrPutStringFmt(gpa, "decltest.{s}", .{name}),
+                try iter.avoidNameConflict("decltest.{s}", .{name}),
                 .@"test",
                 true,
             };
         },
-        _ => if (declaration.name.isNamedTest(zir)) .{
-            // TODO: avoid collisions with named decls with this name
-            try ip.getOrPutStringFmt(gpa, "test.{s}", .{zir.nullTerminatedString(declaration.name.toString(zir).?)}),
-            .@"test",
-            true,
-        } else .{
-            try ip.getOrPutString(gpa, zir.nullTerminatedString(declaration.name.toString(zir).?)),
-            .named,
-            false,
+        _ => if (declaration.name.isNamedTest(zir)) info: {
+            // We consider these to be unnamed since the decl name can be adjusted to avoid conflicts if necessary.
+            if (iter.pass != .unnamed) return;
+            break :info .{
+                try iter.avoidNameConflict("test.{s}", .{zir.nullTerminatedString(declaration.name.toString(zir).?)}),
+                .@"test",
+                true,
+            };
+        } else info: {
+            if (iter.pass != .named) return;
+            const name = try ip.getOrPutString(gpa, zir.nullTerminatedString(declaration.name.toString(zir).?));
+            try iter.seen_decls.putNoClobber(gpa, name, {});
+            break :info .{
+                name,
+                .named,
+                false,
+            };
         },
     };
 
test/cases/compile_errors/comptime_decl_name_conflict_resolved.zig
@@ -0,0 +1,8 @@
+comptime {
+    @compileError("should be reached");
+}
+const comptime_0 = {};
+
+// error
+//
+// :2:5: error: should be reached