diff --git a/src/c_import.test.zig b/src/c_import.test.zig new file mode 100644 index 0000000..e889b89 --- /dev/null +++ b/src/c_import.test.zig @@ -0,0 +1,68 @@ +const std = @import("std"); +const c_import = @import("c_import.zig"); + +const SRC = "int f(void) { return 1; }"; +const HDR = "int f(void);"; +const VER = "19.1.7"; + +const none: []const []const u8 = &.{}; + +fn baseKey() u64 { + return c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); +} + +test "cSourceCacheKey: stable when nothing changes" { + try std.testing.expectEqual(baseKey(), baseKey()); +} + +test "cSourceCacheKey: source bytes vary the key" { + const other = c_import.cSourceCacheKey("int f(void) { return 2; }", &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other); +} + +test "cSourceCacheKey: declared header content varies the key" { + const other = c_import.cSourceCacheKey(SRC, &.{"int f(void); int g(void);"}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other); +} + +test "cSourceCacheKey: defines vary the key (value and order)" { + const v2 = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=2"}, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != v2); + + const ab = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{ "A=1", "B=1" }, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + const ba = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{ "B=1", "A=1" }, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(ab != ba); +} + +test "cSourceCacheKey: flags vary the key" { + const other = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O3"}, &.{"inc"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other); +} + +test "cSourceCacheKey: a define is not a flag (same string, different role)" { + const as_define = c_import.cSourceCacheKey(SRC, none, &.{"X"}, none, none, VER, null, null); + const as_flag = c_import.cSourceCacheKey(SRC, none, none, &.{"X"}, none, VER, null, null); + try std.testing.expect(as_define != as_flag); +} + +test "cSourceCacheKey: include dirs vary the key" { + const other = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"other"}, VER, "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other); +} + +test "cSourceCacheKey: llvm version varies the key" { + const other = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, "20.0.0", "arm64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other); +} + +test "cSourceCacheKey: triple and sysroot vary the key; absent is not empty" { + const other_triple = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, VER, "x86_64-apple-darwin", "/sdk"); + try std.testing.expect(baseKey() != other_triple); + + const other_sysroot = c_import.cSourceCacheKey(SRC, &.{HDR}, &.{"A=1"}, &.{"-O2"}, &.{"inc"}, VER, "arm64-apple-darwin", "/ndk"); + try std.testing.expect(baseKey() != other_sysroot); + + const absent = c_import.cSourceCacheKey(SRC, none, none, none, none, VER, null, null); + const empty = c_import.cSourceCacheKey(SRC, none, none, none, none, VER, "", ""); + try std.testing.expect(absent != empty); +} diff --git a/src/c_import.zig b/src/c_import.zig index 82cf1d7..79319f1 100644 --- a/src/c_import.zig +++ b/src/c_import.zig @@ -33,6 +33,50 @@ pub const CImportInfo = struct { flags: []const []const u8, }; +/// Cache key for one compiled `#source` member. Everything that can +/// change the produced object participates: the source bytes, the +/// unit's declared `#include` headers BY CONTENT (editing a declared +/// header invalidates), defines / flags / include dirs in declaration +/// order, the LLVM version, and the cross-target (triple + sysroot). +/// Section tags keep equal strings in different roles distinct (a +/// define never aliases a flag, an absent triple never aliases an +/// empty one). Transitive includes of the .c itself do NOT +/// participate — the block's declared surface is the invalidation +/// boundary. +pub fn cSourceCacheKey( + source_bytes: []const u8, + header_bytes: []const []const u8, + defines: []const []const u8, + flags: []const []const u8, + include_dirs: []const []const u8, + llvm_version: []const u8, + triple: ?[]const u8, + sysroot: ?[]const u8, +) u64 { + const Wyhash = std.hash.Wyhash; + var key = Wyhash.hash(0, "sx-c-import-v1"); + key = Wyhash.hash(key, source_bytes); + key = Wyhash.hash(key, "\x01headers"); + for (header_bytes) |hb| key = Wyhash.hash(key, hb); + key = Wyhash.hash(key, "\x01defines"); + for (defines) |d| key = Wyhash.hash(key, d); + key = Wyhash.hash(key, "\x01flags"); + for (flags) |f| key = Wyhash.hash(key, f); + key = Wyhash.hash(key, "\x01incdirs"); + for (include_dirs) |inc| key = Wyhash.hash(key, inc); + key = Wyhash.hash(key, "\x01llvm"); + key = Wyhash.hash(key, llvm_version); + if (triple) |t| { + key = Wyhash.hash(key, "\x01triple"); + key = Wyhash.hash(key, t); + } + if (sysroot) |sr| { + key = Wyhash.hash(key, "\x01sysroot"); + key = Wyhash.hash(key, sr); + } + return key; +} + /// Handle returned from loadCObjectsForJIT — caller must call unload() after JIT. pub const CImportHandle = struct { dylib_handle: ?*anyopaque = null, diff --git a/src/root.zig b/src/root.zig index 767d19f..a1f6774 100644 --- a/src/root.zig +++ b/src/root.zig @@ -16,6 +16,7 @@ pub const imports = @import("imports.zig"); pub const imports_tests = @import("imports.test.zig"); pub const core = @import("core.zig"); pub const c_import = @import("c_import.zig"); +pub const c_import_tests = @import("c_import.test.zig"); pub const ir = @import("ir/ir.zig"); pub const lsp = struct {