fix(C4): dedup c-import units by normalized content, not node identity

One module imported through several aliased chains materializes one
c_import_decl copy per chain, each carrying a differently-spelled
relative path to the same file (src/app/../repo/../db/../../vendor/x.c
vs src/db/../../vendor/x.c). Dedup now keys on lexically-normalized
sources/includes + defines + flags, so the unit compiles and links
exactly once — pointer-identity dedup linked it once per chain and
died with duplicate symbols at AOT link.
This commit is contained in:
agra
2026-06-12 17:27:06 +03:00
parent e9b500a232
commit 8dedacb23f

View File

@@ -624,7 +624,7 @@ pub fn collectCImportSources(allocator: std.mem.Allocator, root: *const Node) ![
if (root.data != .root) return &.{};
var infos = std.ArrayList(CImportInfo).empty;
var seen = std.AutoHashMap([*]const []const u8, void).init(allocator);
var seen = std.StringHashMap(void).init(allocator);
defer seen.deinit();
// Aliased imports lower to namespace_decl nodes and NEST when a
@@ -632,10 +632,47 @@ pub fn collectCImportSources(allocator: std.mem.Allocator, root: *const Node) ![
// unit two aliases deep is silently never compiled and its symbols
// resolve from whatever process image carries the same names (the
// extractLibraries depth bug, issue 0130, in c_import form).
//
// Dedup is by CONTENT (sources + includes + defines + flags), not
// node identity: one module imported through several aliased paths
// materializes several copies of its c_import_decl, and collecting
// each copy would compile (and link!) the same unit repeatedly —
// duplicate-symbol death at AOT link time.
const walker = struct {
// Lexically normalized: the SAME file reached through different
// import chains spells differently ("src/app/../repo/../db/../..
// /vendor/x.c" vs "src/db/../../vendor/x.c") and must dedup.
fn appendNormalized(key: *std.ArrayList(u8), alloc: std.mem.Allocator, path: []const u8) !void {
const norm = std.fs.path.resolve(alloc, &.{path}) catch path;
try key.appendSlice(alloc, norm);
try key.append(alloc, 0);
}
fn contentKey(alloc: std.mem.Allocator, ci: anytype) ![]const u8 {
var key = std.ArrayList(u8).empty;
for (ci.sources) |s| {
try appendNormalized(&key, alloc, s);
}
try key.append(alloc, 1);
for (ci.includes) |s| {
try appendNormalized(&key, alloc, s);
}
try key.append(alloc, 1);
for (ci.defines) |s| {
try key.appendSlice(alloc, s);
try key.append(alloc, 0);
}
try key.append(alloc, 1);
for (ci.flags) |s| {
try key.appendSlice(alloc, s);
try key.append(alloc, 0);
}
return try key.toOwnedSlice(alloc);
}
fn walk(
infos_: *std.ArrayList(CImportInfo),
seen_: *std.AutoHashMap([*]const []const u8, void),
seen_: *std.StringHashMap(void),
alloc: std.mem.Allocator,
decls: []const *Node,
) !void {
@@ -643,7 +680,7 @@ pub fn collectCImportSources(allocator: std.mem.Allocator, root: *const Node) ![
switch (d.data) {
.c_import_decl => |ci| {
if (ci.sources.len > 0) {
const key = ci.sources.ptr;
const key = try contentKey(alloc, ci);
if (!seen_.contains(key)) {
try seen_.put(key, {});
try infos_.append(alloc, .{