const std = @import("std"); const ast = @import("ast.zig"); const llvm = @import("llvm_api.zig"); const Node = ast.Node; const c = llvm.c; const builtin = @import("builtin"); pub const CSourceLocation = struct { file: []const u8, line: u32, }; /// Derive the NDK sysroot path from the NDK root (which by convention /// lives in `target_config.sysroot` on Android — see target.zig's /// Android link branch + main.zig's auto-discovery). Returns a NUL- /// terminated path suitable for clang's `--sysroot ` argv. fn androidSysrootFromNdkRoot(allocator: std.mem.Allocator, ndk_root: []const u8) ![:0]u8 { const host_tag: []const u8 = if (builtin.os.tag == .macos) "darwin-x86_64" else "linux-x86_64"; return try std.fmt.allocPrintSentinel(allocator, "{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ ndk_root, host_tag }, 0); } pub const CImportResult = struct { fn_decls: []const *Node, /// Source locations for each fn_decl (parallel array, same indices). locations: []const CSourceLocation, }; /// Info collected from c_import_decl AST nodes for native compilation. pub const CImportInfo = struct { sources: []const []const u8, includes: []const []const u8, defines: []const []const u8, 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, the source's /// TRANSITIVE quoted includes BY CONTENT (`dep_bytes` — editing any /// header the compile actually reads invalidates), defines / flags / /// include dirs in declaration order, the toolchain 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). pub fn cSourceCacheKey( source_bytes: []const u8, header_bytes: []const []const u8, dep_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, "\x01deps"); for (dep_bytes) |db| key = Wyhash.hash(key, db); 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; } /// Quoted `#include "x"` targets in `source`, appended to `out` in /// order of appearance. Angle includes (<...>) are system headers and /// never participate in invalidation. Over-collection (an include /// inside an inactive `#if` branch) is harmless: an extra existing /// file gets hashed, a missing one is skipped at resolution. pub fn scanQuotedIncludes(allocator: std.mem.Allocator, source: []const u8, out: *std.ArrayList([]const u8)) !void { var it = std.mem.splitScalar(u8, source, '\n'); while (it.next()) |line| { var s = std.mem.trimStart(u8, line, " \t"); if (s.len == 0 or s[0] != '#') continue; s = std.mem.trimStart(u8, s[1..], " \t"); if (!std.mem.startsWith(u8, s, "include")) continue; s = std.mem.trimStart(u8, s["include".len..], " \t"); if (s.len < 2 or s[0] != '"') continue; const rest = s[1..]; const end = std.mem.indexOfScalar(u8, rest, '"') orelse continue; if (end == 0) continue; try out.append(allocator, rest[0..end]); } } /// The transitive closure of quoted includes reachable from /// `root_path`/`root_bytes`, each include resolved against its /// includer's directory first and the unit's include dirs second; /// unresolvable names (system or conditionally-absent includes) are /// skipped. Returns the file CONTENTS of every dependency for /// cache-key participation — editing any header the compile actually /// reads must change the key. fn collectIncludeDepBytes( allocator: std.mem.Allocator, io: std.Io, root_path: []const u8, root_bytes: []const u8, include_dirs: []const []const u8, ) ![]const []const u8 { const Pending = struct { path: []const u8, bytes: []const u8 }; var dep_bytes = std.ArrayList([]const u8).empty; var visited = std.StringHashMap(void).init(allocator); defer visited.deinit(); var queue = std.ArrayList(Pending).empty; try queue.append(allocator, .{ .path = root_path, .bytes = root_bytes }); var idx: usize = 0; while (idx < queue.items.len) : (idx += 1) { const item = queue.items[idx]; var incs = std.ArrayList([]const u8).empty; try scanQuotedIncludes(allocator, item.bytes, &incs); const base = dirName(item.path); for (incs.items) |inc| { var candidates = std.ArrayList([]const u8).empty; try candidates.append(allocator, try std.fs.path.join(allocator, &.{ base, inc })); for (include_dirs) |dir| { try candidates.append(allocator, try std.fs.path.join(allocator, &.{ dir, inc })); } for (candidates.items) |cand| { const norm = std.fs.path.resolve(allocator, &.{cand}) catch cand; if (visited.contains(norm)) break; const bytes = std.Io.Dir.readFileAlloc(.cwd(), io, cand, allocator, .limited(64 * 1024 * 1024)) catch continue; try visited.put(norm, {}); try dep_bytes.append(allocator, bytes); try queue.append(allocator, .{ .path = cand, .bytes = bytes }); break; } } } return try dep_bytes.toOwnedSlice(allocator); } /// Handle returned from loadCObjectsForJIT — caller must call unload() after JIT. pub const CImportHandle = struct { dylib_handle: ?*anyopaque = null, /// Where the unit's linked dylib lives for THIS run; the JIT adds it /// as a priority symbol-search target ahead of the process images. dylib_path: ?[:0]const u8 = null, temp_paths: []const []const u8 = &.{}, allocator: std.mem.Allocator, pub fn unload(self: *CImportHandle, io: std.Io) void { // dlclose if (self.dylib_handle) |h| { _ = std.c.dlclose(h); } // Clean up temp files for (self.temp_paths) |path| { std.Io.Dir.deleteFile(.cwd(), io, path) catch {}; } } }; /// Parse C headers to extract function declarations as synthetic AST nodes. /// Called during import resolution (no LLVM context needed). pub fn processCImport( allocator: std.mem.Allocator, includes: []const []const u8, defines: []const []const u8, flags: []const []const u8, ) !CImportResult { // Build clang args: -I dirs, -D defines, raw flags var args_list = std.ArrayList([*c]const u8).empty; for (includes) |inc| { const dir = dirName(inc); const arg = try allocPrintZ(allocator, "-I{s}", .{dir}); try args_list.append(allocator, arg.ptr); } for (defines) |def| { const arg = try allocPrintZ(allocator, "-D{s}", .{def}); try args_list.append(allocator, arg.ptr); } for (flags) |flag| { const arg = try allocator.dupeZ(u8, flag); try args_list.append(allocator, arg.ptr); } var all_decls = std.ArrayList(*Node).empty; var all_locs = std.ArrayList(CSourceLocation).empty; for (includes) |header| { const header_z = try allocator.dupeZ(u8, header); var err_msg: [*c]u8 = null; const args_ptr: [*c][*c]const u8 = if (args_list.items.len > 0) @ptrCast(args_list.items.ptr) else null; const info = c.sx_clang_parse_header( header_z.ptr, args_ptr, @intCast(args_list.items.len), &err_msg, ); if (info == null) { if (err_msg) |e| { std.debug.print("clang parse error for '{s}': {s}\n", .{ header, std.mem.span(e) }); } return error.CompileError; } defer c.sx_clang_free_header_info(info); const funcs = info.*.functions; const num: usize = @intCast(info.*.num_functions); for (0..num) |i| { const fi = funcs[i]; const name = try allocator.dupe(u8, std.mem.span(fi.name)); // Build params var params = std.ArrayList(ast.Param).empty; const np: usize = @intCast(fi.num_params); for (0..np) |j| { const pi = fi.params[j]; const pname_raw = std.mem.span(pi.name); const pname = if (pname_raw.len > 0) try allocator.dupe(u8, pname_raw) else try std.fmt.allocPrint(allocator, "p{d}", .{j}); const ptype_str = std.mem.span(pi.type_spelling); const ptype_node = try mapCTypeToSxNode(allocator, ptype_str); try params.append(allocator, .{ .name = pname, .name_span = .{ .start = 0, .end = 0 }, .type_expr = ptype_node, // Foreign C param names (`i1`, `i2`, …) are RAW — exempt from // the reserved-type-name binding check; generated bindings // must import without hand-edits. .is_raw = true, }); } // Return type const ret_str = std.mem.span(fi.return_type); const ret_node = if (std.mem.eql(u8, ret_str, "void")) null else try mapCTypeToSxNode(allocator, ret_str); // Create foreign_expr body (no library_ref — symbols resolved at runtime) const foreign_body = try allocator.create(Node); foreign_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .foreign_expr = .{ .library_ref = null, .c_name = null } }, }; const fn_node = try allocator.create(Node); fn_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .fn_decl = .{ .name = name, .params = try params.toOwnedSlice(allocator), .return_type = ret_node, .body = foreign_body, // A foreign C function whose own NAME collides with a reserved // type spelling (`int i2(int);`) is RAW — exempt from the // reserved-type-name decl check so generated bindings import // without hand-edits. .is_raw = true, } }, }; try all_decls.append(allocator, fn_node); // Collect source location const src_file = if (fi.source_file) |sf| try allocator.dupe(u8, std.mem.span(sf)) else header; try all_locs.append(allocator, .{ .file = src_file, .line = @intCast(fi.source_line), }); } } return .{ .fn_decls = try all_decls.toOwnedSlice(allocator), .locations = try all_locs.toOwnedSlice(allocator), }; } // --------------------------------------------------------------------------- // Native C compilation (compile to .o, not LLVM module) // --------------------------------------------------------------------------- // ── #foreign ref validation ─────────────────────────────────────────── fn collectForeignRefTargets(valid: *std.StringHashMap(void), decls: []const *Node) !void { for (decls) |d| { switch (d.data) { .library_decl => |ld| try valid.put(ld.name, {}), .namespace_decl => |ns| { // A NAMED `#import c` unit lowers to a namespace wrapping // its c_import_decl — the namespace name is the unit ref. for (ns.decls) |nd| { if (nd.data == .c_import_decl) { try valid.put(ns.name, {}); break; } } try collectForeignRefTargets(valid, ns.decls); }, else => {}, } } } fn checkForeignRefs(valid: *const std.StringHashMap(void), decls: []const *Node, diags: *@import("errors.zig").DiagnosticList) void { for (decls) |d| { switch (d.data) { .fn_decl => |fd| { if (fd.body.data != .foreign_expr) continue; const ref = fd.body.data.foreign_expr.library_ref orelse continue; if (!valid.contains(ref)) { diags.addFmt(.err, d.span, "#foreign library '{s}' is not declared; expected a #library constant or a named '#import c' unit", .{ref}); } }, .namespace_decl => |ns| checkForeignRefs(valid, ns.decls, diags), else => {}, } } } /// Validate every `#foreign ` in the merged program: the ref must /// name a `#library` constant or a NAMED `#import c` unit. Refs are /// author-side module-local names, but modules merge flat or /// namespaced into one tree, so existence is checked program-wide. /// Decls synthesized from `#include` headers carry no ref and are /// exempt. pub fn validateForeignRefs(allocator: std.mem.Allocator, root: *const Node, diags: *@import("errors.zig").DiagnosticList) !void { if (root.data != .root) return; var valid = std.StringHashMap(void).init(allocator); defer valid.deinit(); try collectForeignRefTargets(&valid, root.data.root.decls); checkForeignRefs(&valid, root.data.root.decls, diags); } /// A cached entry must at least LOOK like an object file (Mach-O or /// ELF magic) — a truncated or garbage entry falls back to a fresh /// compile instead of poisoning the link with an opaque error. pub fn objectMagicOk(data: []const u8) bool { if (data.len < 4) return false; if (data[0] == 0x7f and data[1] == 'E' and data[2] == 'L' and data[3] == 'F') return true; if (data[0] == 0xcf and data[1] == 0xfa and data[2] == 0xed and data[3] == 0xfe) return true; // Mach-O 64 if (data[0] == 0xce and data[1] == 0xfa and data[2] == 0xed and data[3] == 0xfe) return true; // Mach-O 32 return false; } fn loadCachedObject(path: [:0]const u8) ?c.LLVMMemoryBufferRef { var buf: c.LLVMMemoryBufferRef = null; var err_msg: [*c]u8 = null; if (c.LLVMCreateMemoryBufferWithContentsOfFile(path.ptr, &buf, &err_msg) != 0) { if (err_msg != null) c.LLVMDisposeMessage(err_msg); return null; } const start = c.LLVMGetBufferStart(buf); const size = c.LLVMGetBufferSize(buf); if (start == null or size < 4) { c.LLVMDisposeMemoryBuffer(buf); return null; } const data = @as([*]const u8, @ptrCast(start))[0..size]; if (!objectMagicOk(data)) { c.LLVMDisposeMemoryBuffer(buf); return null; } return buf; } // Best-effort, never fails the build: write to a per-pid temp at the // repo root, then copy into place (copyFile's make_path creates // .sx-cache/ if needed) — same pattern as main.zig's object cache. fn saveCachedObject(allocator: std.mem.Allocator, obj_buf: c.LLVMMemoryBufferRef, io: std.Io, cache_path: [:0]const u8) void { const start = c.LLVMGetBufferStart(obj_buf); const size = c.LLVMGetBufferSize(obj_buf); if (start == null or size == 0) return; const data = @as([*]const u8, @ptrCast(start))[0..size]; const tmp = std.fmt.allocPrint(allocator, ".sx-c-cache-tmp-{d}", .{std.c.getpid()}) catch return; std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = tmp, .data = data }) catch return; std.Io.Dir.copyFile(.cwd(), tmp, .cwd(), cache_path, io, .{ .make_path = true }) catch {}; std.Io.Dir.deleteFile(.cwd(), io, tmp) catch {}; } /// Compile C sources to native object files (in memory), through a /// persistent content-addressed cache (`.sx-cache/c-.o`, default /// on). A `#source` unit recompiles only when its cache key changes — /// see `cSourceCacheKey` for what participates. Any cache-machinery /// failure (unreadable input for hashing, corrupt entry) falls back to /// a fresh compile; the cache can never fail a build. /// Returns list of LLVMMemoryBufferRef (each containing a .o file). pub fn compileCToObjects( allocator: std.mem.Allocator, io: std.Io, infos: []const CImportInfo, target_config: @import("target.zig").TargetConfig, ) ![]c.LLVMMemoryBufferRef { var obj_bufs = std.ArrayList(c.LLVMMemoryBufferRef).empty; var ver_maj: c_uint = 0; var ver_min: c_uint = 0; var ver_pat: c_uint = 0; c.LLVMGetVersion(&ver_maj, &ver_min, &ver_pat); const llvm_version = try std.fmt.allocPrint(allocator, "{d}.{d}.{d}", .{ ver_maj, ver_min, ver_pat }); const triple_slice: ?[]const u8 = if (target_config.triple) |t| std.mem.span(t) else null; for (infos) |info| { if (info.sources.len == 0) continue; // Build clang args: -I dirs, -D defines, raw flags var args_list = std.ArrayList([*c]const u8).empty; // Cross-compile target: forward -target / -isysroot when set. if (target_config.triple) |t| { try args_list.append(allocator, "-target"); try args_list.append(allocator, t); } if (target_config.sysroot) |sr| { try args_list.append(allocator, "-isysroot"); try args_list.append(allocator, (try allocator.dupeZ(u8, sr)).ptr); } // Android: route through the NDK sysroot so bionic headers resolve. // The embedded clang library doesn't know how to be an Android cross- // compiler on its own. `target_config.sysroot` holds the NDK root // by convention (main.zig auto-fills it for --target android), so // derive the headers/libs sysroot inside it. if (target_config.isAndroid()) { if (target_config.sysroot) |ndk_root| { const sysroot = try androidSysrootFromNdkRoot(allocator, ndk_root); try args_list.append(allocator, "--sysroot"); try args_list.append(allocator, sysroot.ptr); } } var inc_dirs = std.ArrayList([]const u8).empty; for (info.includes) |inc| { const dir = dirName(inc); try inc_dirs.append(allocator, dir); try args_list.append(allocator, (try allocPrintZ(allocator, "-I{s}", .{dir})).ptr); } for (info.defines) |def| { try args_list.append(allocator, (try allocPrintZ(allocator, "-D{s}", .{def})).ptr); } for (info.flags) |flag| { try args_list.append(allocator, (try allocator.dupeZ(u8, flag)).ptr); } const args_ptr: [*c][*c]const u8 = if (args_list.items.len > 0) @ptrCast(args_list.items.ptr) else null; const args_len: c_int = @intCast(args_list.items.len); // Declared headers participate in the cache key BY CONTENT; an // unreadable one disables caching for this unit, never the build. var header_bytes = std.ArrayList([]const u8).empty; var cache_ok = true; for (info.includes) |inc| { const hb = std.Io.Dir.readFileAlloc(.cwd(), io, inc, allocator, .limited(64 * 1024 * 1024)) catch { cache_ok = false; break; }; try header_bytes.append(allocator, hb); } for (info.sources) |src| { var cache_path: ?[:0]const u8 = null; if (cache_ok) { if (std.Io.Dir.readFileAlloc(.cwd(), io, src, allocator, .limited(64 * 1024 * 1024))) |src_bytes| { const dep_bytes = collectIncludeDepBytes(allocator, io, src, src_bytes, inc_dirs.items) catch &.{}; const key = cSourceCacheKey( src_bytes, header_bytes.items, dep_bytes, info.defines, info.flags, inc_dirs.items, llvm_version, triple_slice, target_config.sysroot, ); cache_path = try std.fmt.allocPrintSentinel(allocator, ".sx-cache/c-{x:0>16}.o", .{key}, 0); if (loadCachedObject(cache_path.?)) |cached| { try obj_bufs.append(allocator, cached); continue; } } else |_| {} } const src_z = try allocator.dupeZ(u8, src); var err_msg: [*c]u8 = null; const obj_buf = c.sx_clang_compile_to_object( src_z.ptr, args_ptr, args_len, &err_msg, ); if (obj_buf == null) { if (err_msg) |e| { std.debug.print("clang compile error for '{s}': {s}\n", .{ src, std.mem.span(e) }); } return error.CompileError; } if (cache_path) |cp| saveCachedObject(allocator, obj_buf, io, cp); try obj_bufs.append(allocator, obj_buf); } } return try obj_bufs.toOwnedSlice(allocator); } /// For JIT mode: write .o files to temp, link into a shared library, dlopen it. /// Returns a handle that must be unloaded after JIT execution. pub fn loadCObjectsForJIT( allocator: std.mem.Allocator, io: std.Io, obj_bufs: []c.LLVMMemoryBufferRef, ) !CImportHandle { if (obj_bufs.len == 0) return .{ .allocator = allocator }; var temp_paths = std.ArrayList([]const u8).empty; // Write each .o buffer to a temp file (per-pid names: concurrent // `sx run` processes must not clobber each other's link inputs) const pid = std.c.getpid(); var obj_paths = std.ArrayList([]const u8).empty; for (obj_bufs, 0..) |buf, i| { const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}_{d}.o", .{ pid, i }); const start = c.LLVMGetBufferStart(buf); const size = c.LLVMGetBufferSize(buf); const data = @as([*]const u8, @ptrCast(start))[0..size]; std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = path, .data = data }) catch { std.debug.print("failed to write temp object: {s}\n", .{path}); return error.CompileError; }; try obj_paths.append(allocator, path); try temp_paths.append(allocator, path); c.LLVMDisposeMemoryBuffer(buf); } // Link into a shared library const dylib_path = try std.fmt.allocPrintSentinel(allocator, "/tmp/sx_c_import_{d}.dylib", .{pid}, 0); try temp_paths.append(allocator, dylib_path); var argv = std.ArrayList([]const u8).empty; try argv.append(allocator, "cc"); if (comptime builtin.os.tag == .macos) { try argv.append(allocator, "-dynamiclib"); } else { try argv.append(allocator, "-shared"); } try argv.append(allocator, "-o"); try argv.append(allocator, dylib_path); for (obj_paths.items) |op| { try argv.append(allocator, op); } const argv_slice = try argv.toOwnedSlice(allocator); var child = std.process.spawn(io, .{ .argv = argv_slice, }) catch { std.debug.print("failed to spawn linker for C import shared library\n", .{}); return error.CompileError; }; const result = child.wait(io) catch { std.debug.print("linker wait failed for C import shared library\n", .{}); return error.CompileError; }; if (result != .exited or result.exited != 0) { std.debug.print("linker failed for C import shared library (exit={})\n", .{result.exited}); return error.CompileError; } // dlopen the shared library const dylib_z = try allocator.dupeZ(u8, dylib_path); const handle = std.c.dlopen(dylib_z.ptr, .{ .NOW = true }); if (handle == null) { const err = std.c.dlerror(); if (err) |e| { std.debug.print("dlopen failed: {s}\n", .{std.mem.span(e)}); } return error.CompileError; } return .{ .dylib_handle = handle, .dylib_path = dylib_path, .temp_paths = try temp_paths.toOwnedSlice(allocator), .allocator = allocator, }; } /// Compile C sources using emcc for Emscripten/WASM targets. /// Shells out to `emcc -c` for each source file, returns temp object file paths. pub fn compileCWithEmcc( allocator: std.mem.Allocator, io: std.Io, infos: []const CImportInfo, target_config: @import("target.zig").TargetConfig, tmp_dir: []const u8, ) ![]const []const u8 { var paths = std.ArrayList([]const u8).empty; var obj_idx: usize = 0; for (infos) |info| { if (info.sources.len == 0) continue; for (info.sources) |src| { const out_path = try std.fmt.allocPrint(allocator, "{s}/sx_emcc_{d}.o", .{ tmp_dir, obj_idx }); obj_idx += 1; var argv = std.ArrayList([]const u8).empty; try argv.appendSlice(allocator, &.{ "emcc", "-c", "-O2", src, "-o", out_path }); // wasm64: compile C sources with memory64 support if (target_config.isWasm64()) { try argv.append(allocator, "-sMEMORY64"); } // Add include paths for (info.includes) |inc| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-I{s}", .{dirName(inc)})); } for (info.defines) |def| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-D{s}", .{def})); } for (info.flags) |flag| { try argv.append(allocator, flag); } const argv_slice = try argv.toOwnedSlice(allocator); var child = std.process.spawn(io, .{ .argv = argv_slice }) catch { std.debug.print("error: failed to spawn emcc for '{s}'\n", .{src}); return error.CompileError; }; const result = child.wait(io) catch return error.CompileError; if (result != .exited or result.exited != 0) { std.debug.print("error: emcc failed for '{s}'\n", .{src}); return error.CompileError; } try paths.append(allocator, out_path); } } return try paths.toOwnedSlice(allocator); } /// For build mode: write .o buffers to temp files, return paths for the linker. pub fn writeCObjectFiles( allocator: std.mem.Allocator, io: std.Io, obj_bufs: []c.LLVMMemoryBufferRef, tmp_dir: []const u8, ) ![]const []const u8 { var paths = std.ArrayList([]const u8).empty; for (obj_bufs, 0..) |buf, i| { const path = try std.fmt.allocPrint(allocator, "{s}/sx_c_{d}.o", .{ tmp_dir, i }); const start = c.LLVMGetBufferStart(buf); const size = c.LLVMGetBufferSize(buf); const data = @as([*]const u8, @ptrCast(start))[0..size]; std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = path, .data = data }) catch { std.debug.print("failed to write temp object: {s}\n", .{path}); return error.CompileError; }; try paths.append(allocator, path); c.LLVMDisposeMemoryBuffer(buf); } return try paths.toOwnedSlice(allocator); } /// Walk the resolved AST and collect CImportInfo from all c_import_decl nodes. /// Deduplicates by source pointer identity (shared nodes from import propagation). pub fn collectCImportSources(allocator: std.mem.Allocator, root: *const Node) ![]CImportInfo { if (root.data != .root) return &.{}; var infos = std.ArrayList(CImportInfo).empty; var seen = std.StringHashMap(void).init(allocator); defer seen.deinit(); // Aliased imports lower to namespace_decl nodes and NEST when a // namespaced module declares its own named unit — recurse, or a // 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.StringHashMap(void), alloc: std.mem.Allocator, decls: []const *Node, ) !void { for (decls) |d| { switch (d.data) { .c_import_decl => |ci| { if (ci.sources.len > 0) { const key = try contentKey(alloc, ci); if (!seen_.contains(key)) { try seen_.put(key, {}); try infos_.append(alloc, .{ .sources = ci.sources, .includes = ci.includes, .defines = ci.defines, .flags = ci.flags, }); } } }, .namespace_decl => |ns| try walk(infos_, seen_, alloc, ns.decls), else => {}, } } } }; try walker.walk(&infos, &seen, allocator, root.data.root.decls); return try infos.toOwnedSlice(allocator); } // --------------------------------------------------------------------------- // C type → sx type mapping // --------------------------------------------------------------------------- fn mapCTypeToSxNode( allocator: std.mem.Allocator, c_type: []const u8, ) !*Node { const trimmed = std.mem.trim(u8, c_type, " "); // Pointer types (trailing *) if (std.mem.endsWith(u8, trimmed, "*")) { const base = std.mem.trim(u8, trimmed[0 .. trimmed.len - 1], " "); // const char * → [*]u8 (raw pointer, matches C ABI) if (std.mem.eql(u8, base, "const char") or std.mem.eql(u8, base, "char const")) { return makeManyPointerTypeNode(allocator, "u8"); } // char * → [*]u8 if (std.mem.eql(u8, base, "char")) { return makeManyPointerTypeNode(allocator, "u8"); } // unsigned char * / const unsigned char * → [*]u8 if (std.mem.eql(u8, base, "unsigned char") or std.mem.eql(u8, base, "const unsigned char") or std.mem.eql(u8, base, "unsigned char const")) { return makeManyPointerTypeNode(allocator, "u8"); } // void * / const void * → *void if (std.mem.eql(u8, base, "void") or std.mem.eql(u8, base, "const void")) { return makePointerTypeNode(allocator, "void"); } // int * → *i32 if (std.mem.eql(u8, base, "int") or std.mem.eql(u8, base, "const int")) { return makePointerTypeNode(allocator, "i32"); } // unsigned int * / unsigned * → *u32 if (std.mem.eql(u8, base, "unsigned int") or std.mem.eql(u8, base, "unsigned") or std.mem.eql(u8, base, "const unsigned int")) { return makePointerTypeNode(allocator, "u32"); } // float * → *f32 if (std.mem.eql(u8, base, "float") or std.mem.eql(u8, base, "const float")) { return makePointerTypeNode(allocator, "f32"); } // double * → *f64 if (std.mem.eql(u8, base, "double") or std.mem.eql(u8, base, "const double")) { return makePointerTypeNode(allocator, "f64"); } // short * → *i16 if (std.mem.eql(u8, base, "short") or std.mem.eql(u8, base, "const short")) { return makePointerTypeNode(allocator, "i16"); } // Pointer to pointer → *void if (std.mem.endsWith(u8, base, "*")) { return makePointerTypeNode(allocator, "void"); } // Remove const qualifier and retry if (std.mem.startsWith(u8, base, "const ")) { const without_const = try std.fmt.allocPrint(allocator, "{s} *", .{base[6..]}); return mapCTypeToSxNode(allocator, without_const); } // Default: struct/opaque pointer → *void return makePointerTypeNode(allocator, "void"); } // Direct types if (std.mem.eql(u8, trimmed, "int") or std.mem.eql(u8, trimmed, "signed int")) return makeTypeExprNode(allocator, "i32"); if (std.mem.eql(u8, trimmed, "unsigned int") or std.mem.eql(u8, trimmed, "unsigned")) return makeTypeExprNode(allocator, "u32"); if (std.mem.eql(u8, trimmed, "long") or std.mem.eql(u8, trimmed, "long int") or std.mem.eql(u8, trimmed, "signed long")) return makeTypeExprNode(allocator, "i64"); if (std.mem.eql(u8, trimmed, "unsigned long") or std.mem.eql(u8, trimmed, "unsigned long int")) return makeTypeExprNode(allocator, "u64"); if (std.mem.eql(u8, trimmed, "long long") or std.mem.eql(u8, trimmed, "long long int")) return makeTypeExprNode(allocator, "i64"); if (std.mem.eql(u8, trimmed, "unsigned long long") or std.mem.eql(u8, trimmed, "unsigned long long int")) return makeTypeExprNode(allocator, "u64"); if (std.mem.eql(u8, trimmed, "short") or std.mem.eql(u8, trimmed, "short int") or std.mem.eql(u8, trimmed, "signed short")) return makeTypeExprNode(allocator, "i16"); if (std.mem.eql(u8, trimmed, "unsigned short") or std.mem.eql(u8, trimmed, "unsigned short int")) return makeTypeExprNode(allocator, "u16"); if (std.mem.eql(u8, trimmed, "char") or std.mem.eql(u8, trimmed, "signed char")) return makeTypeExprNode(allocator, "u8"); if (std.mem.eql(u8, trimmed, "unsigned char")) return makeTypeExprNode(allocator, "u8"); if (std.mem.eql(u8, trimmed, "float")) return makeTypeExprNode(allocator, "f32"); if (std.mem.eql(u8, trimmed, "double")) return makeTypeExprNode(allocator, "f64"); if (std.mem.eql(u8, trimmed, "size_t")) return makeTypeExprNode(allocator, "u64"); if (std.mem.eql(u8, trimmed, "_Bool") or std.mem.eql(u8, trimmed, "bool")) return makeTypeExprNode(allocator, "u8"); // Default: unknown type → i64 (treat as opaque integer-sized value) return makeTypeExprNode(allocator, "i64"); } // --------------------------------------------------------------------------- // AST node construction helpers // --------------------------------------------------------------------------- fn makeTypeExprNode(allocator: std.mem.Allocator, name: []const u8) !*Node { const node = try allocator.create(Node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name } }, }; return node; } fn makePointerTypeNode(allocator: std.mem.Allocator, pointee: []const u8) !*Node { const inner = try makeTypeExprNode(allocator, pointee); const node = try allocator.create(Node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = inner } }, }; return node; } fn makeManyPointerTypeNode(allocator: std.mem.Allocator, element: []const u8) !*Node { const inner = try makeTypeExprNode(allocator, element); const node = try allocator.create(Node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .many_pointer_type_expr = .{ .element_type = inner } }, }; return node; } fn makeSliceTypeNode(allocator: std.mem.Allocator, element: []const u8) !*Node { const inner = try makeTypeExprNode(allocator, element); const node = try allocator.create(Node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .slice_type_expr = .{ .element_type = inner } }, }; return node; } // --------------------------------------------------------------------------- // Utility // --------------------------------------------------------------------------- fn allocPrintZ(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) ![:0]u8 { return allocator.dupeZ(u8, try std.fmt.allocPrint(allocator, fmt, args)); } fn dirName(path: []const u8) []const u8 { var last_sep: usize = 0; var found = false; for (path, 0..) |ch, i| { if (ch == '/') { last_sep = i; found = true; } } return if (found) path[0..last_sep] else "."; }