This commit is contained in:
agra
2026-02-15 23:46:43 +02:00
parent 7da3ecfa7c
commit 3e1e764753
5 changed files with 961 additions and 91 deletions

View File

@@ -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 });
}
};