const std = @import("std"); const sx = @import("sx"); pub fn main(init: std.process.Init) !void { const allocator = init.arena.allocator(); const io = init.io; const args = try init.minimal.args.toSlice(allocator); if (args.len < 2) { printUsage(); return; } const command = args[1]; // LSP subcommand doesn't need a file argument if (std.mem.eql(u8, command, "lsp")) { runLsp(allocator, io); return; } // Parse flags and positional arguments var input_path: ?[]const u8 = null; var target_config = sx.target.TargetConfig{}; var lib_paths = std.ArrayList([]const u8).empty; var link_flags = std.ArrayList([]const u8).empty; var show_timing: bool = false; var explicit_opt: bool = false; var enable_cache: bool = false; var i: usize = 2; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, arg, "--target")) { i += 1; if (i >= args.len) { std.debug.print("error: --target requires a value\n", .{}); return; } const raw = args[i]; // Shorthand aliases for common targets const expanded = if (std.mem.eql(u8, raw, "wasm") or std.mem.eql(u8, raw, "wasm32") or std.mem.eql(u8, raw, "emscripten")) "wasm32-unknown-emscripten" else if (std.mem.eql(u8, raw, "wasm64")) "wasm64-unknown-emscripten" else if (std.mem.eql(u8, raw, "macos") or std.mem.eql(u8, raw, "macos-arm")) "aarch64-apple-macos" else if (std.mem.eql(u8, raw, "macos-x86")) "x86_64-apple-macos" else if (std.mem.eql(u8, raw, "linux") or std.mem.eql(u8, raw, "linux-x86")) "x86_64-unknown-linux-gnu" else if (std.mem.eql(u8, raw, "linux-arm")) "aarch64-unknown-linux-gnu" else if (std.mem.eql(u8, raw, "windows")) "x86_64-windows-msvc" else raw; target_config.triple = (try allocator.dupeZ(u8, expanded)).ptr; } else if (std.mem.eql(u8, arg, "--cpu")) { i += 1; if (i >= args.len) { std.debug.print("error: --cpu requires a value\n", .{}); return; } target_config.cpu = (try allocator.dupeZ(u8, args[i])).ptr; } else if (std.mem.eql(u8, arg, "--opt")) { i += 1; if (i >= args.len) { std.debug.print("error: --opt requires a value\n", .{}); return; } target_config.opt_level = parseOptLevel(args[i]) orelse { std.debug.print("error: invalid --opt value '{s}' (expected: none/0, less/1, default/2, aggressive/3)\n", .{args[i]}); return; }; explicit_opt = true; } else if (std.mem.eql(u8, arg, "-o")) { i += 1; if (i >= args.len) { std.debug.print("error: -o requires a value\n", .{}); return; } target_config.output_path = args[i]; } else if (std.mem.eql(u8, arg, "--linker")) { i += 1; if (i >= args.len) { std.debug.print("error: --linker requires a value\n", .{}); return; } target_config.linker = args[i]; } else if (std.mem.eql(u8, arg, "--sysroot")) { i += 1; if (i >= args.len) { std.debug.print("error: --sysroot requires a value\n", .{}); return; } target_config.sysroot = args[i]; } else if (std.mem.eql(u8, arg, "--time")) { show_timing = true; } else if (std.mem.eql(u8, arg, "--cache")) { enable_cache = true; } else if (std.mem.startsWith(u8, arg, "-L")) { if (arg.len > 2) { try lib_paths.append(allocator, arg[2..]); } else { i += 1; if (i >= args.len) { std.debug.print("error: -L requires a value\n", .{}); return; } try lib_paths.append(allocator, args[i]); } } else if (std.mem.eql(u8, arg, "--lflags")) { i += 1; if (i >= args.len) { std.debug.print("error: --lflags requires a value\n", .{}); return; } try link_flags.append(allocator, args[i]); } else if (!std.mem.startsWith(u8, arg, "-")) { input_path = arg; } else { std.debug.print("error: unknown flag '{s}'\n", .{arg}); return; } } target_config.lib_paths = try lib_paths.toOwnedSlice(allocator); target_config.extra_link_flags = try link_flags.toOwnedSlice(allocator); const path = input_path orelse { printUsage(); return; }; if (std.mem.eql(u8, command, "build")) { const output_name = target_config.output_path orelse blk: { const base = deriveOutputName(path); if (target_config.isEmscripten()) { break :blk try std.fmt.allocPrint(allocator, "{s}.html", .{base}); } break :blk base; }; compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch return; } else if (std.mem.eql(u8, command, "ir")) { emitIR(allocator, io, path, target_config) catch return; } else if (std.mem.eql(u8, command, "ir-dump")) { dumpSxIR(allocator, io, path) catch return; } else if (std.mem.eql(u8, command, "asm")) { emitAsm(allocator, io, path, target_config) catch return; } else if (std.mem.eql(u8, command, "run")) { if (target_config.isWasm()) { std.debug.print("error: 'run' is not supported for wasm targets. Use 'build' instead.\n", .{}); return; } // Default to -O0 for run (faster compile) unless user explicitly set --opt if (!explicit_opt) target_config.opt_level = .none; var timer = Timing.init(show_timing); // Phase A: read + parse + resolveImports (for cache key) timer.mark(); const source = readSource(allocator, io, path) catch return; timer.record("read"); var comp = sx.core.Compilation.init(allocator, io, path, source, target_config); defer comp.deinit(); timer.mark(); comp.parse() catch { comp.renderErrors(); return; }; timer.record("parse"); timer.mark(); comp.resolveImports() catch { comp.renderErrors(); return; }; timer.record("imports"); // Cache check — use .o files (precompiled object, skip IR compilation in JIT) // Disable caching for files with top-level #run (side effects lost on cache hit) const root = comp.resolved_root orelse comp.root orelse return; const use_cache = enable_cache and !hasTopLevelRun(root); const key = computeCacheKey(source, &comp.import_sources, target_config); const cache_obj = cachePath(allocator, key, "o") catch return; timer.mark(); const obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef = blk: { if (use_cache) { // Try loading cached .o from disk var buf: sx.llvm_api.c.LLVMMemoryBufferRef = null; var err_msg: [*c]u8 = null; if (sx.llvm_api.c.LLVMCreateMemoryBufferWithContentsOfFile(cache_obj.ptr, &buf, &err_msg) == 0) { timer.record("cache"); break :blk buf; } if (err_msg != null) sx.llvm_api.c.LLVMDisposeMessage(err_msg); } // Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors) comp.generateCode() catch { comp.renderErrors(); return; }; timer.record("codegen"); timer.mark(); comp.ir_emitter.?.verifyWithMessage() catch return; const buf = comp.ir_emitter.?.emitObjectToMemory() catch return; timer.record("emit"); // Save .o to cache (extract data before JIT takes ownership) if (use_cache) { saveObjectToCache(buf, io, cache_obj); } break :blk buf; }; // Compile C sources natively and dlopen before JIT timer.mark(); var c_handle = compileCForJIT(allocator, io, &comp) catch { comp.renderErrors(); return; }; defer c_handle.unload(io); timer.record("c-import"); // dlopen #library dependencies so JIT can resolve foreign symbols const libs = extractLibraries(allocator, root) catch return; var lib_handles = std.ArrayList(*anyopaque).empty; defer { for (lib_handles.items) |h| _ = std.c.dlclose(h); } for (libs) |lib_name| { if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |handle| { lib_handles.append(allocator, handle) catch {}; } else { const e = std.c.dlerror(); if (e) |msg| std.debug.print("warning: could not load library '{s}': {s}\n", .{ lib_name, std.mem.span(msg) }); } } // JIT from precompiled object (relocation only, no IR compilation) sx.llvm_api.initNativeTarget(); timer.mark(); const exit_code = sx.target.runJITFromObject(obj_buf) catch { // JIT failed — fall back to AOT timer.record("jit-fail"); runAOT(allocator, io, path, target_config, &timer, enable_cache) catch return; timer.printAll(); return; }; timer.record("jit"); timer.printAll(); if (exit_code != 0) std.process.exit(exit_code); } else { printUsage(); } } /// Compile C sources from #import c blocks and dlopen them for JIT. fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) !sx.c_import.CImportHandle { const c_infos = try comp.collectCImportSources(); if (c_infos.len == 0) return .{ .allocator = allocator }; const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos); return try sx.c_import.loadCObjectsForJIT(allocator, io, obj_bufs); } /// Compile C sources from #import c blocks to .o files for linking. fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation, tmp_dir: []const u8) ![]const []const u8 { const c_infos = try comp.collectCImportSources(); if (c_infos.len == 0) return &.{}; // For Emscripten targets, use emcc to cross-compile C sources if (comp.target_config.isEmscripten()) { return try sx.c_import.compileCWithEmcc(allocator, io, c_infos, comp.target_config, tmp_dir); } const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos); return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs, tmp_dir); } fn parseOptLevel(s: []const u8) ?sx.target.TargetConfig.OptLevel { if (std.mem.eql(u8, s, "none") or std.mem.eql(u8, s, "0")) return .none; if (std.mem.eql(u8, s, "less") or std.mem.eql(u8, s, "1")) return .less; if (std.mem.eql(u8, s, "default") or std.mem.eql(u8, s, "2")) return .default; if (std.mem.eql(u8, s, "aggressive") or std.mem.eql(u8, s, "3")) return .aggressive; return null; } fn printUsage() void { std.debug.print( \\Usage: sx [options] \\ \\Commands: \\ run Build and run immediately \\ build Build binary in current directory \\ ir Print LLVM IR to stdout \\ asm Emit assembly (.s) file \\ lsp Start language server (LSP) \\ \\Options: \\ --target Target triple or shorthand: wasm, macos, linux, windows (default: host) \\ --cpu CPU name (default: generic) \\ --opt Optimization: none/0, less/1, default/2, aggressive/3 \\ -o Output path \\ -L Library search path (repeatable) \\ --linker Linker command (default: cc) \\ --sysroot Sysroot for cross-compilation \\ --lflags Extra linker flag (repeatable, e.g. --lflags -sUSE_SDL=2) \\ --cache Enable build caching \\ --time Show compilation timing breakdown \\ , .{}); } fn runLsp(allocator: std.mem.Allocator, io: std.Io) void { const Transport = sx.lsp.transport.Transport; const Server = sx.lsp.server.Server; const stdin_file = std.Io.File.stdin(); const stdout_file = std.Io.File.stdout(); var read_buf: [4096]u8 = undefined; var stdin_reader = stdin_file.readerStreaming(io, &read_buf); var transport = Transport.init(allocator, io, &stdin_reader.interface, stdout_file); var server = Server.init(allocator, &transport, io); while (true) { const msg = transport.readMessage() catch |err| { if (err == error.EndOfStream) break; std.debug.print("lsp: read error: {}\n", .{err}); break; }; const keep_going = server.handleMessage(msg); if (!keep_going) break; } } fn deriveOutputName(input_path: []const u8) []const u8 { // Get basename (strip directory) var start: usize = 0; for (input_path, 0..) |ch, idx| { if (ch == '/' or ch == '\\') start = idx + 1; } const basename = input_path[start..]; // Strip .sx extension if (std.mem.endsWith(u8, basename, ".sx")) { return basename[0 .. basename.len - 3]; } return basename; } fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 { const source_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, input_path, allocator, .limited(10 * 1024 * 1024)) catch |err| { std.debug.print("error: cannot read '{s}': {}\n", .{ input_path, err }); return error.CompileError; }; return try allocator.dupeZ(u8, source_bytes); } fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing) !sx.core.Compilation { timer.mark(); const source = try readSource(allocator, io, input_path); timer.record("read"); var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config); errdefer comp.deinit(); timer.mark(); comp.parse() catch { comp.renderErrors(); return error.CompileError; }; timer.record("parse"); timer.mark(); comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; }; timer.record("imports"); timer.mark(); comp.generateCode() catch { comp.renderErrors(); return error.CompileError; }; timer.record("codegen"); timer.mark(); comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError; timer.record("verify"); return comp; } fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !void { const source = try readSource(allocator, io, input_path); var comp = sx.core.Compilation.init(allocator, io, input_path, source, .{}); defer comp.deinit(); comp.parse() catch { comp.renderErrors(); return error.CompileError; }; comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; }; var ir_module = comp.lowerToIR(); defer ir_module.deinit(); var aw = std.Io.Writer.Allocating.init(allocator); sx.ir.printModule(&ir_module, &aw.writer) catch return; var result = aw.writer.toArrayList(); defer result.deinit(allocator); std.debug.print("{s}", .{result.items}); } fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig) !void { var timer = Timing.init(false); var comp = try compilePipeline(allocator, io, input_path, target_config, &timer); defer comp.deinit(); comp.ir_emitter.?.printIR(); } fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig) !void { var timer = Timing.init(false); var comp = try compilePipeline(allocator, io, input_path, target_config, &timer); defer comp.deinit(); const asm_path = target_config.output_path orelse blk: { const name = deriveOutputName(input_path); break :blk try std.fmt.allocPrint(allocator, "{s}.s", .{name}); }; const asm_path_z = try allocator.dupeZ(u8, asm_path); comp.ir_emitter.?.emitAssembly(asm_path_z.ptr) catch return error.CompileError; std.debug.print("emitted: {s}\n", .{asm_path}); } fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, show_timing: bool, enable_cache: bool) !void { var timer = Timing.init(show_timing); try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, enable_cache); timer.printAll(); } fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool) !void { // Phase A: read + parse + resolveImports (fast: ~0.5ms) timer.mark(); const source = try readSource(allocator, io, input_path); timer.record("read"); var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config); errdefer comp.deinit(); defer comp.deinit(); timer.mark(); comp.parse() catch { comp.renderErrors(); return error.CompileError; }; timer.record("parse"); timer.mark(); comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; }; timer.record("imports"); // Extract library names from AST (needed for linking regardless of cache) const root = comp.resolved_root orelse comp.root orelse return error.CompileError; const libs = try extractLibraries(allocator, root); // Create temp directory for build artifacts const tmp_dir: []const u8 = ".sx-tmp"; std.Io.Dir.createDirPath(.cwd(), io, tmp_dir) catch {}; const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}/main.o", .{tmp_dir}, 0); // Cache: compute key and check for cached binary/.o const key = computeCacheKey(source, &comp.import_sources, target_config); const cache_obj = try cachePath(allocator, key, "o"); const cache_bin = try cachePath(allocator, key, "bin"); // Level 1: Try cached binary (skip everything — no codegen, no link) if (enable_cache) bin_cache: { std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache; timer.record("cache"); return; } // Level 2: Try cached .o (skip codegen+emit, still need link) const used_obj_cache = blk: { if (!enable_cache) break :blk false; std.Io.Dir.copyFile(.cwd(), cache_obj, .cwd(), obj_path, io, .{}) catch break :blk false; break :blk true; }; if (used_obj_cache) { timer.record("cache"); } else { // Cache MISS — full codegen + emit timer.mark(); comp.generateCode() catch { comp.renderErrors(); return error.CompileError; }; timer.record("codegen"); timer.mark(); comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError; timer.record("verify"); timer.mark(); comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError; timer.record("emit"); // Save .o to cache if (enable_cache) { std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {}; } } // Compile C sources from #import c blocks to .o files timer.mark(); const c_obj_paths = compileCForBuild(allocator, io, &comp, tmp_dir) catch { std.debug.print("error: C import compilation failed\n", .{}); return error.CompileError; }; timer.record("c-import"); // Merge build config (from #run blocks) with CLI config var merged_config = target_config; const build_flags = comp.getBuildLinkFlags(); if (build_flags.len > 0) { var all_flags: std.ArrayList([]const u8) = .empty; for (target_config.extra_link_flags) |f| try all_flags.append(allocator, f); for (build_flags) |f| try all_flags.append(allocator, f); merged_config.extra_link_flags = try all_flags.toOwnedSlice(allocator); } // Override output path from #run if set (and no explicit -o was given on CLI) const final_output = if (target_config.output_path == null) (comp.getBuildOutputPath() orelse output_path) else output_path; // Ensure output directory exists if (std.mem.lastIndexOfScalar(u8, final_output, '/')) |sep| { if (sep > 0) { std.Io.Dir.createDirPath(.cwd(), io, final_output[0..sep]) catch {}; } } // Link (sx .o + C .o files) timer.mark(); sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, merged_config) catch { std.debug.print("error: linking failed\n", .{}); return error.CompileError; }; timer.record("link"); // Save linked binary to cache if (enable_cache) { std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {}; } std.debug.print("compiled: {s}\n", .{final_output}); // Clean up temp directory and all build artifacts std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {}; const shell_tmp = std.fmt.allocPrint(allocator, "{s}.shell.html", .{obj_path}) catch null; if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {}; for (c_obj_paths) |cop| { std.Io.Dir.deleteFile(.cwd(), io, cop) catch {}; } std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {}; } fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool) !void { const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp"; try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, enable_cache); defer { std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {}; } timer.mark(); var child = std.process.spawn(io, .{ .argv = &.{tmp_bin}, }) catch { std.debug.print("error: failed to run program\n", .{}); return error.CompileError; }; const term = child.wait(io) catch { std.debug.print("error: program execution failed\n", .{}); return error.CompileError; }; timer.record("exec"); switch (term) { .exited => |code| if (code != 0) std.process.exit(code), .signal => std.process.exit(1), .stopped, .unknown => std.process.exit(1), } } // --- Cache helpers --- fn computeCacheKey(source: [:0]const u8, import_sources: *const std.StringHashMap([:0]const u8), target_config: sx.target.TargetConfig) u64 { const Wyhash = std.hash.Wyhash; var key = Wyhash.hash(0, source); // XOR import hashes for order independence (HashMap iteration is non-deterministic) var import_hash: u64 = 0; var it = import_sources.iterator(); while (it.next()) |entry| { var h = Wyhash.hash(0, entry.key_ptr.*); h = Wyhash.hash(h, entry.value_ptr.*); import_hash ^= h; } key = Wyhash.hash(key, std.mem.asBytes(&import_hash)); // Hash target config fields that affect codegen if (target_config.triple) |t| key = Wyhash.hash(key, std.mem.span(t)); if (target_config.cpu) |cp| key = Wyhash.hash(key, std.mem.span(cp)); if (target_config.features) |f| key = Wyhash.hash(key, std.mem.span(f)); key = Wyhash.hash(key, std.mem.asBytes(&target_config.opt_level)); return key; } fn cachePath(allocator: std.mem.Allocator, key: u64, ext: []const u8) ![:0]const u8 { return try std.fmt.allocPrintSentinel(allocator, ".sx-cache/{x:0>16}.{s}", .{ key, ext }, 0); } fn saveObjectToCache(obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef, io: std.Io, cache_path: [:0]const u8) void { const c_api = sx.llvm_api.c; const start = c_api.LLVMGetBufferStart(obj_buf); const size = c_api.LLVMGetBufferSize(obj_buf); if (start == null or size == 0) return; const data = @as([*]const u8, @ptrCast(start))[0..size]; // Write to temp file, then copy to cache (make_path creates .sx-cache/ if needed) std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = ".sx-cache-tmp", .data = data }) catch return; std.Io.Dir.copyFile(.cwd(), ".sx-cache-tmp", .cwd(), cache_path, io, .{ .make_path = true }) catch {}; std.Io.Dir.deleteFile(.cwd(), io, ".sx-cache-tmp") catch {}; } fn hasTopLevelRun(root: *const sx.ast.Node) bool { for (root.data.root.decls) |decl| { if (decl.data == .comptime_expr) return true; } return false; } fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 { var libs = std.ArrayList([]const u8).empty; var seen = std.StringHashMap(void).init(allocator); const addLib = struct { fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void { if (s.contains(name)) return; try s.put(name, {}); try l.append(a, name); } }.f; for (root.data.root.decls) |decl| { switch (decl.data) { .library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name), .namespace_decl => |ns| { for (ns.decls) |nd| { switch (nd.data) { .library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name), else => {}, } } }, else => {}, } } return try libs.toOwnedSlice(allocator); } /// Try to dlopen a library by name, searching user paths, host paths, and common naming conventions. fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?*anyopaque { const is_macos = comptime @import("builtin").os.tag == .macos; const suffixes: []const []const u8 = if (is_macos) &.{ ".dylib", ".so" } else &.{ ".so", ".dylib" }; // Search paths: user-supplied first, then host defaults const search_paths = comptime blk: { var paths: []const []const u8 = &.{}; for (sx.target.host_lib_paths) |p| { paths = paths ++ .{p}; } break :blk paths; }; // Try each path with each suffix const all_paths = [_][]const []const u8{ user_lib_paths, search_paths }; for (&all_paths) |paths| { for (paths) |dir| { for (suffixes) |sfx| { const full = std.fmt.allocPrintSentinel(allocator, "{s}/lib{s}{s}", .{ dir, lib_name, sfx }, 0) catch continue; if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return h; } } } // Fallback: bare name (let dlopen search its default paths) for (suffixes) |sfx| { const bare = std.fmt.allocPrintSentinel(allocator, "lib{s}{s}", .{ lib_name, sfx }, 0) catch continue; if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return h; } return null; } // Simple timing helper — records stage durations and prints a summary table. const Timing = struct { const max_entries = 16; enabled: bool, names: [max_entries][]const u8, durations_ns: [max_entries]u64, count: usize, last: ?std.time.Instant, fn init(enabled: bool) Timing { return .{ .enabled = enabled, .names = undefined, .durations_ns = undefined, .count = 0, .last = if (enabled) (std.time.Instant.now() catch null) else null, }; } fn mark(self: *Timing) void { if (self.enabled) self.last = std.time.Instant.now() catch null; } fn record(self: *Timing, name: []const u8) void { if (!self.enabled) return; const now = std.time.Instant.now() catch null; const elapsed_ns: u64 = if (self.last != null and now != null) now.?.since(self.last.?) else 0; if (self.count < max_entries) { self.names[self.count] = name; self.durations_ns[self.count] = elapsed_ns; self.count += 1; } self.last = now; } fn printAll(self: *const Timing) void { if (!self.enabled or self.count == 0) return; var total_ns: u64 = 0; for (self.durations_ns[0..self.count]) |d| total_ns += d; std.debug.print("\n--- timing ---\n", .{}); for (0..self.count) |idx| { const ms = @as(f64, @floatFromInt(self.durations_ns[idx])) / 1_000_000.0; std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ self.names[idx], ms }); } const total_ms = @as(f64, @floatFromInt(total_ns)) / 1_000_000.0; std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ "total", total_ms }); } };