perf
This commit is contained in:
197
src/codegen.zig
197
src/codegen.zig
@@ -104,6 +104,12 @@ pub const CodeGen = struct {
|
||||
module: c.LLVMModuleRef,
|
||||
builder: c.LLVMBuilderRef,
|
||||
allocator: std.mem.Allocator,
|
||||
// ORC ThreadSafeContext — wraps the LLVMContext for JIT compatibility
|
||||
ts_context: c.LLVMOrcThreadSafeContextRef = null,
|
||||
// Whether we still own the module (false after JIT takes ownership)
|
||||
module_owned: bool = true,
|
||||
// Cached target machine (created in init, reused by emitToFile)
|
||||
target_machine: c.LLVMTargetMachineRef = null,
|
||||
|
||||
// Symbol table: maps variable names to their alloca pointers
|
||||
named_values: std.StringHashMap(NamedValue),
|
||||
@@ -188,6 +194,16 @@ pub const CodeGen = struct {
|
||||
function_return_types: std.StringHashMap(Type),
|
||||
// Target configuration (triple, cpu, opt level, lib paths, linker)
|
||||
target_config: TargetConfig = .{},
|
||||
// Cached primitive LLVM types (initialized once in init(), avoids repeated FFI calls)
|
||||
cached_i1: c.LLVMTypeRef = null,
|
||||
cached_i8: c.LLVMTypeRef = null,
|
||||
cached_i16: c.LLVMTypeRef = null,
|
||||
cached_i32: c.LLVMTypeRef = null,
|
||||
cached_i64: c.LLVMTypeRef = null,
|
||||
cached_f32: c.LLVMTypeRef = null,
|
||||
cached_f64: c.LLVMTypeRef = null,
|
||||
cached_ptr: c.LLVMTypeRef = null,
|
||||
cached_void: c.LLVMTypeRef = null,
|
||||
|
||||
const DeferredFn = struct {
|
||||
fd: ast.FnDecl,
|
||||
@@ -311,12 +327,18 @@ pub const CodeGen = struct {
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, module_name: [*:0]const u8, target_config: TargetConfig) CodeGen {
|
||||
const ctx = c.LLVMContextCreate();
|
||||
// Create context via ORC ThreadSafeContext for JIT compatibility
|
||||
const ts_ctx = c.LLVMOrcCreateNewThreadSafeContext();
|
||||
const ctx = c.LLVMOrcThreadSafeContextGetContext(ts_ctx);
|
||||
const module = c.LLVMModuleCreateWithNameInContext(module_name, ctx);
|
||||
const builder = c.LLVMCreateBuilderInContext(ctx);
|
||||
|
||||
// Initialize LLVM targets and set data layout early so alignment queries work
|
||||
llvm.initAllTargets();
|
||||
// Initialize LLVM targets — native-only when targeting host, all for cross-compilation
|
||||
if (target_config.triple == null) {
|
||||
llvm.initNativeTarget();
|
||||
} else {
|
||||
llvm.initAllTargets();
|
||||
}
|
||||
|
||||
const triple_owned = target_config.triple == null;
|
||||
const triple = target_config.triple orelse c.LLVMGetDefaultTargetTriple();
|
||||
@@ -326,8 +348,9 @@ pub const CodeGen = struct {
|
||||
|
||||
var target: c.LLVMTargetRef = null;
|
||||
var err_msg: [*c]u8 = null;
|
||||
var tm: c.LLVMTargetMachineRef = null;
|
||||
if (c.LLVMGetTargetFromTriple(triple, &target, &err_msg) == 0) {
|
||||
const tm = c.LLVMCreateTargetMachine(
|
||||
tm = c.LLVMCreateTargetMachine(
|
||||
target,
|
||||
triple,
|
||||
target_config.getCpu(),
|
||||
@@ -339,7 +362,6 @@ pub const CodeGen = struct {
|
||||
const dl = c.LLVMCreateTargetDataLayout(tm);
|
||||
c.LLVMSetModuleDataLayout(module, dl);
|
||||
c.LLVMDisposeTargetData(dl);
|
||||
c.LLVMDisposeTargetMachine(tm);
|
||||
} else {
|
||||
if (err_msg != null) c.LLVMDisposeMessage(err_msg);
|
||||
}
|
||||
@@ -348,6 +370,8 @@ pub const CodeGen = struct {
|
||||
.module = module,
|
||||
.builder = builder,
|
||||
.allocator = allocator,
|
||||
.ts_context = ts_ctx,
|
||||
.target_machine = tm,
|
||||
.named_values = std.StringHashMap(NamedValue).init(allocator),
|
||||
.type_registry = std.StringHashMap(TypeRegistryEntry).init(allocator),
|
||||
.flags_enum_types = std.StringHashMap(void).init(allocator),
|
||||
@@ -375,6 +399,15 @@ pub const CodeGen = struct {
|
||||
.global_mutable_vars = std.StringHashMap(NamedValue).init(allocator),
|
||||
.function_return_types = std.StringHashMap(Type).init(allocator),
|
||||
.target_config = target_config,
|
||||
.cached_i1 = c.LLVMInt1TypeInContext(ctx),
|
||||
.cached_i8 = c.LLVMInt8TypeInContext(ctx),
|
||||
.cached_i16 = c.LLVMInt16TypeInContext(ctx),
|
||||
.cached_i32 = c.LLVMInt32TypeInContext(ctx),
|
||||
.cached_i64 = c.LLVMInt64TypeInContext(ctx),
|
||||
.cached_f32 = c.LLVMFloatTypeInContext(ctx),
|
||||
.cached_f64 = c.LLVMDoubleTypeInContext(ctx),
|
||||
.cached_ptr = c.LLVMPointerTypeInContext(ctx, 0),
|
||||
.cached_void = c.LLVMVoidTypeInContext(ctx),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -396,8 +429,15 @@ pub const CodeGen = struct {
|
||||
self.foreign_libraries.deinit(self.allocator);
|
||||
self.foreign_fns.deinit();
|
||||
c.LLVMDisposeBuilder(self.builder);
|
||||
c.LLVMDisposeModule(self.module);
|
||||
c.LLVMContextDispose(self.context);
|
||||
if (self.target_machine) |tm| c.LLVMDisposeTargetMachine(tm);
|
||||
if (self.module_owned) {
|
||||
c.LLVMDisposeModule(self.module);
|
||||
}
|
||||
if (self.ts_context) |ts_ctx| {
|
||||
c.LLVMOrcDisposeThreadSafeContext(ts_ctx);
|
||||
} else {
|
||||
c.LLVMContextDispose(self.context);
|
||||
}
|
||||
}
|
||||
|
||||
fn getStructInfo(self: *CodeGen, name: []const u8) !StructInfo {
|
||||
@@ -510,8 +550,14 @@ pub const CodeGen = struct {
|
||||
|
||||
pub fn typeToLLVM(self: *CodeGen, ty: Type) c.LLVMTypeRef {
|
||||
return switch (ty) {
|
||||
.signed => |w| c.LLVMIntTypeInContext(self.context, w),
|
||||
.unsigned => |w| c.LLVMIntTypeInContext(self.context, w),
|
||||
.signed, .unsigned => |w| switch (w) {
|
||||
1 => self.cached_i1.?,
|
||||
8 => self.cached_i8.?,
|
||||
16 => self.cached_i16.?,
|
||||
32 => self.cached_i32.?,
|
||||
64 => self.cached_i64.?,
|
||||
else => c.LLVMIntTypeInContext(self.context, w),
|
||||
},
|
||||
.f32 => self.f32Type(),
|
||||
.f64 => self.f64Type(),
|
||||
.void_type => self.voidType(),
|
||||
@@ -736,15 +782,15 @@ pub const CodeGen = struct {
|
||||
return self.buildFatPointer(self.getStringStructType(), ptr, len_val);
|
||||
}
|
||||
|
||||
// LLVM type shortcuts
|
||||
fn i1Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMInt1TypeInContext(self.context); }
|
||||
fn i8Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMInt8TypeInContext(self.context); }
|
||||
fn i32Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMInt32TypeInContext(self.context); }
|
||||
fn i64Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMInt64TypeInContext(self.context); }
|
||||
fn f32Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMFloatTypeInContext(self.context); }
|
||||
fn f64Type(self: *CodeGen) c.LLVMTypeRef { return c.LLVMDoubleTypeInContext(self.context); }
|
||||
fn ptrType(self: *CodeGen) c.LLVMTypeRef { return c.LLVMPointerTypeInContext(self.context, 0); }
|
||||
fn voidType(self: *CodeGen) c.LLVMTypeRef { return c.LLVMVoidTypeInContext(self.context); }
|
||||
// LLVM type shortcuts (cached — no FFI call)
|
||||
fn i1Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_i1.?; }
|
||||
fn i8Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_i8.?; }
|
||||
fn i32Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_i32.?; }
|
||||
fn i64Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_i64.?; }
|
||||
fn f32Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_f32.?; }
|
||||
fn f64Type(self: *CodeGen) c.LLVMTypeRef { return self.cached_f64.?; }
|
||||
fn ptrType(self: *CodeGen) c.LLVMTypeRef { return self.cached_ptr.?; }
|
||||
fn voidType(self: *CodeGen) c.LLVMTypeRef { return self.cached_void.?; }
|
||||
|
||||
fn gepArrayElement(self: *CodeGen, arr_ty: c.LLVMTypeRef, arr_ptr: c.LLVMValueRef, idx: c.LLVMValueRef, name: [*c]const u8) c.LLVMValueRef {
|
||||
var indices = [_]c.LLVMValueRef{ self.constInt32(0), idx };
|
||||
@@ -6206,13 +6252,16 @@ pub const CodeGen = struct {
|
||||
// Merge block
|
||||
self.positionAt(merge_bb);
|
||||
|
||||
// PHI node if both branches produced values
|
||||
// PHI node if both branches produced values (skip for void type)
|
||||
if (then_val != null and else_val != null) {
|
||||
const phi = c.LLVMBuildPhi(self.builder, c.LLVMTypeOf(then_val), "iftmp");
|
||||
var vals = [2]c.LLVMValueRef{ then_val, else_val };
|
||||
var blocks = [2]c.LLVMBasicBlockRef{ then_bb, else_bb };
|
||||
c.LLVMAddIncoming(phi, &vals, &blocks, 2);
|
||||
return phi;
|
||||
const ty = c.LLVMTypeOf(then_val);
|
||||
if (c.LLVMGetTypeKind(ty) != c.LLVMVoidTypeKind) {
|
||||
const phi = c.LLVMBuildPhi(self.builder, ty, "iftmp");
|
||||
var vals = [2]c.LLVMValueRef{ then_val, else_val };
|
||||
var blocks = [2]c.LLVMBasicBlockRef{ then_bb, else_bb };
|
||||
c.LLVMAddIncoming(phi, &vals, &blocks, 2);
|
||||
return phi;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -7176,39 +7225,12 @@ pub const CodeGen = struct {
|
||||
}
|
||||
|
||||
fn emitToFile(self: *CodeGen, output_path: [*:0]const u8, file_type: c.LLVMCodeGenFileType) !void {
|
||||
llvm.initAllTargets();
|
||||
const tm = self.target_machine orelse return self.emitError("no target machine available");
|
||||
|
||||
const cfg = self.target_config;
|
||||
const triple_owned = cfg.triple == null;
|
||||
const triple = cfg.triple orelse c.LLVMGetDefaultTargetTriple();
|
||||
defer if (triple_owned) c.LLVMDisposeMessage(@constCast(triple));
|
||||
|
||||
var target: c.LLVMTargetRef = null;
|
||||
var err_msg: [*c]u8 = null;
|
||||
|
||||
if (c.LLVMGetTargetFromTriple(triple, &target, &err_msg) != 0) {
|
||||
if (c.LLVMTargetMachineEmitToFile(tm, self.module, output_path, file_type, &err_msg) != 0) {
|
||||
defer c.LLVMDisposeMessage(err_msg);
|
||||
const msg = std.mem.span(err_msg);
|
||||
return self.emitErrorFmt("failed to get target: {s}", .{msg});
|
||||
}
|
||||
|
||||
const tm = c.LLVMCreateTargetMachine(
|
||||
target,
|
||||
triple,
|
||||
cfg.getCpu(),
|
||||
cfg.getFeatures(),
|
||||
cfg.opt_level.toLLVM(),
|
||||
c.LLVMRelocPIC,
|
||||
c.LLVMCodeModelDefault,
|
||||
);
|
||||
defer c.LLVMDisposeTargetMachine(tm);
|
||||
|
||||
c.LLVMSetTarget(self.module, triple);
|
||||
|
||||
var err_msg2: [*c]u8 = null;
|
||||
if (c.LLVMTargetMachineEmitToFile(tm, self.module, output_path, file_type, &err_msg2) != 0) {
|
||||
defer c.LLVMDisposeMessage(err_msg2);
|
||||
const msg = std.mem.span(err_msg2);
|
||||
return self.emitErrorFmt("failed to emit file: {s}", .{msg});
|
||||
}
|
||||
}
|
||||
@@ -7221,6 +7243,75 @@ pub const CodeGen = struct {
|
||||
return self.emitToFile(output_path, c.LLVMAssemblyFile);
|
||||
}
|
||||
|
||||
/// Emit the module as an object file to a memory buffer.
|
||||
/// Caller owns the returned buffer and must dispose or pass to JIT.
|
||||
pub fn emitObjectToMemory(self: *CodeGen) !c.LLVMMemoryBufferRef {
|
||||
const tm = self.target_machine orelse return self.emitError("no target machine available");
|
||||
var err_msg: [*c]u8 = null;
|
||||
var buf: c.LLVMMemoryBufferRef = null;
|
||||
if (c.LLVMTargetMachineEmitToMemoryBuffer(tm, self.module, c.LLVMObjectFile, &err_msg, &buf) != 0) {
|
||||
if (err_msg != null) {
|
||||
defer c.LLVMDisposeMessage(err_msg);
|
||||
const msg = std.mem.span(err_msg);
|
||||
return self.emitErrorFmt("failed to emit object to memory: {s}", .{msg});
|
||||
}
|
||||
return error.CompileError;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Execute a precompiled object file in-process using LLVM's ORC JIT.
|
||||
/// Takes ownership of obj_buf. Returns the exit code from main().
|
||||
pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
|
||||
// Create LLJIT with default builder (no custom TM needed — .o is precompiled)
|
||||
var jit: c.LLVMOrcLLJITRef = null;
|
||||
var err = c.LLVMOrcCreateLLJIT(&jit, null);
|
||||
if (err != null) {
|
||||
const msg = c.LLVMGetErrorMessage(err);
|
||||
defer c.LLVMDisposeErrorMessage(msg);
|
||||
std.debug.print("JIT error: {s}\n", .{std.mem.span(msg)});
|
||||
return error.CompileError;
|
||||
}
|
||||
defer _ = c.LLVMOrcDisposeLLJIT(jit);
|
||||
|
||||
// Add process symbols so JIT can find libc (printf, write, etc.)
|
||||
const jd = c.LLVMOrcLLJITGetMainJITDylib(jit);
|
||||
const prefix = c.LLVMOrcLLJITGetGlobalPrefix(jit);
|
||||
var gen: c.LLVMOrcDefinitionGeneratorRef = null;
|
||||
err = c.LLVMOrcCreateDynamicLibrarySearchGeneratorForProcess(&gen, prefix, null, null);
|
||||
if (err != null) {
|
||||
const msg = c.LLVMGetErrorMessage(err);
|
||||
defer c.LLVMDisposeErrorMessage(msg);
|
||||
std.debug.print("JIT symbol gen error: {s}\n", .{std.mem.span(msg)});
|
||||
return error.CompileError;
|
||||
}
|
||||
c.LLVMOrcJITDylibAddGenerator(jd, gen);
|
||||
|
||||
// Add precompiled object file (transfers ownership of obj_buf)
|
||||
err = c.LLVMOrcLLJITAddObjectFile(jit, jd, obj_buf);
|
||||
if (err != null) {
|
||||
const msg = c.LLVMGetErrorMessage(err);
|
||||
defer c.LLVMDisposeErrorMessage(msg);
|
||||
std.debug.print("JIT add object error: {s}\n", .{std.mem.span(msg)});
|
||||
return error.CompileError;
|
||||
}
|
||||
|
||||
// Look up the "main" function
|
||||
var main_addr: c.LLVMOrcExecutorAddress = 0;
|
||||
err = c.LLVMOrcLLJITLookup(jit, &main_addr, "main");
|
||||
if (err != null) {
|
||||
const msg = c.LLVMGetErrorMessage(err);
|
||||
defer c.LLVMDisposeErrorMessage(msg);
|
||||
std.debug.print("JIT lookup error: {s}\n", .{std.mem.span(msg)});
|
||||
return error.CompileError;
|
||||
}
|
||||
|
||||
// Cast to function pointer and call
|
||||
const main_fn: *const fn () callconv(.c) i32 = @ptrFromInt(main_addr);
|
||||
const result = main_fn();
|
||||
return if (result >= 0 and result <= 255) @intCast(result) else 1;
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void {
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub const c = @cImport({
|
||||
@cInclude("llvm-c/Core.h");
|
||||
@cInclude("llvm-c/Analysis.h");
|
||||
@cInclude("llvm-c/BitWriter.h");
|
||||
|
||||
@cInclude("llvm-c/Target.h");
|
||||
@cInclude("llvm-c/TargetMachine.h");
|
||||
@cInclude("llvm-c/LLJIT.h");
|
||||
|
||||
349
src/main.zig
349
src/main.zig
@@ -23,6 +23,9 @@ pub fn main(init: std.process.Init) !void {
|
||||
var input_path: ?[]const u8 = null;
|
||||
var target_config = sx.codegen.TargetConfig{};
|
||||
var lib_paths = std.ArrayList([]const u8).empty;
|
||||
var show_timing: bool = false;
|
||||
var explicit_opt: bool = false;
|
||||
var no_cache: bool = false;
|
||||
|
||||
var i: usize = 2;
|
||||
while (i < args.len) : (i += 1) {
|
||||
@@ -42,6 +45,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
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; }
|
||||
@@ -54,6 +58,10 @@ pub fn main(init: std.process.Init) !void {
|
||||
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, "--no-cache")) {
|
||||
no_cache = true;
|
||||
} else if (std.mem.startsWith(u8, arg, "-L")) {
|
||||
if (arg.len > 2) {
|
||||
try lib_paths.append(allocator, arg[2..]);
|
||||
@@ -79,33 +87,84 @@ pub fn main(init: std.process.Init) !void {
|
||||
|
||||
if (std.mem.eql(u8, command, "build")) {
|
||||
const output_name = target_config.output_path orelse deriveOutputName(path);
|
||||
compile(allocator, io, path, output_name, target_config) catch return;
|
||||
compile(allocator, io, path, output_name, target_config, show_timing, no_cache) catch return;
|
||||
std.debug.print("compiled: {s}\n", .{output_name});
|
||||
} else if (std.mem.eql(u8, command, "ir")) {
|
||||
emitIR(allocator, io, path, target_config) 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")) {
|
||||
const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp";
|
||||
compile(allocator, io, path, tmp_bin, target_config) catch return;
|
||||
defer {
|
||||
std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {};
|
||||
}
|
||||
var child = std.process.spawn(io, .{
|
||||
.argv = &.{tmp_bin},
|
||||
}) catch {
|
||||
std.debug.print("error: failed to run program\n", .{});
|
||||
// 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 = !no_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();
|
||||
var cg = &comp.cg.?;
|
||||
const buf = cg.emitObjectToMemory() catch { comp.renderErrors(); 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;
|
||||
};
|
||||
|
||||
// JIT from precompiled object (relocation only, no IR compilation)
|
||||
sx.llvm_api.initNativeTarget();
|
||||
timer.mark();
|
||||
const exit_code = sx.codegen.CodeGen.runJITFromObject(obj_buf) catch {
|
||||
// JIT failed — fall back to AOT
|
||||
timer.record("jit-fail");
|
||||
runAOT(allocator, io, path, target_config, &timer, no_cache) catch return;
|
||||
timer.printAll();
|
||||
return;
|
||||
};
|
||||
const term = child.wait(io) catch {
|
||||
std.debug.print("error: program execution failed\n", .{});
|
||||
return;
|
||||
};
|
||||
switch (term) {
|
||||
.exited => |code| if (code != 0) std.process.exit(code),
|
||||
.signal => std.process.exit(1),
|
||||
.stopped, .unknown => std.process.exit(1),
|
||||
}
|
||||
timer.record("jit");
|
||||
timer.printAll();
|
||||
|
||||
if (exit_code != 0) std.process.exit(exit_code);
|
||||
} else {
|
||||
printUsage();
|
||||
}
|
||||
@@ -138,6 +197,8 @@ fn printUsage() void {
|
||||
\\ -L <path> Library search path (repeatable)
|
||||
\\ --linker <cmd> Linker command (default: cc)
|
||||
\\ --sysroot <path> Sysroot for cross-compilation
|
||||
\\ --no-cache Disable build caching
|
||||
\\ --time Show compilation timing breakdown
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
@@ -191,30 +252,44 @@ fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8)
|
||||
return try allocator.dupeZ(u8, source_bytes);
|
||||
}
|
||||
|
||||
fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !sx.core.Compilation {
|
||||
fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.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; };
|
||||
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
||||
comp.generateCode() 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();
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("verify");
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !void {
|
||||
var comp = try compilePipeline(allocator, io, input_path, target_config);
|
||||
var timer = Timing.init(false);
|
||||
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer);
|
||||
defer comp.deinit();
|
||||
comp.cg.?.printIR();
|
||||
}
|
||||
|
||||
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !void {
|
||||
var comp = try compilePipeline(allocator, io, input_path, target_config);
|
||||
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);
|
||||
@@ -225,22 +300,236 @@ fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, tar
|
||||
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.codegen.TargetConfig) !void {
|
||||
var comp = try compilePipeline(allocator, io, input_path, target_config);
|
||||
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, show_timing: bool, no_cache: bool) !void {
|
||||
var timer = Timing.init(show_timing);
|
||||
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, no_cache);
|
||||
timer.printAll();
|
||||
}
|
||||
|
||||
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, no_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();
|
||||
|
||||
var cg = &comp.cg.?;
|
||||
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);
|
||||
|
||||
// Emit object file
|
||||
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}.o", .{output_path}, 0);
|
||||
cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
|
||||
// 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 (!no_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 (no_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();
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("verify");
|
||||
|
||||
timer.mark();
|
||||
cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("emit");
|
||||
|
||||
// Save .o to cache
|
||||
if (!no_cache) {
|
||||
std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// Link
|
||||
sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, cg.foreign_libraries.items, target_config) catch {
|
||||
timer.mark();
|
||||
sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, libs, target_config) catch {
|
||||
std.debug.print("error: linking failed\n", .{});
|
||||
return error.CompileError;
|
||||
};
|
||||
timer.record("link");
|
||||
|
||||
// Save linked binary to cache
|
||||
if (!no_cache) {
|
||||
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
|
||||
}
|
||||
|
||||
// Clean up object file
|
||||
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
|
||||
}
|
||||
|
||||
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, no_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, no_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.codegen.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;
|
||||
for (root.data.root.decls) |decl| {
|
||||
switch (decl.data) {
|
||||
.library_decl => |ld| try libs.append(allocator, ld.lib_name),
|
||||
.namespace_decl => |ns| {
|
||||
for (ns.decls) |nd| {
|
||||
switch (nd.data) {
|
||||
.library_decl => |ld| try libs.append(allocator, ld.lib_name),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return try libs.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user