Files
sx/src/main.zig
agra bbb5426777 sm
2026-03-02 21:00:55 +02:00

716 lines
28 KiB
Zig

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 <command> [options] <file.sx>
\\
\\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> Target triple or shorthand: wasm, macos, linux, windows (default: host)
\\ --cpu <name> CPU name (default: generic)
\\ --opt <level> Optimization: none/0, less/1, default/2, aggressive/3
\\ -o <path> Output path
\\ -L <path> Library search path (repeatable)
\\ --linker <cmd> Linker command (default: cc)
\\ --sysroot <path> Sysroot for cross-compilation
\\ --lflags <flag> 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 });
}
};