diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44716ce..b65c130 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,35 +8,68 @@ on: branches: [master] jobs: - build-windows: - runs-on: windows-latest + build-linux: + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Install Zig uses: mlugg/setup-zig@v2 with: - version: master # pin to a specific version once stable + version: master + + - name: Install LLVM 18 + run: sudo apt-get update && sudo apt-get install -y llvm-18-dev + + - name: Build + run: zig build -Dstatic-llvm -Dllvm-prefix=/usr/lib/llvm-18 + + - name: Test + run: zig build test -Dstatic-llvm -Dllvm-prefix=/usr/lib/llvm-18 --summary all + + - name: Package + run: tar czf sx-linux-x86_64.tar.gz -C zig-out/bin . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: sx-linux-x86_64 + path: zig-out/bin/ + + - name: Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: sx-linux-x86_64.tar.gz + + build-windows: + runs-on: windows-latest + env: + LLVM_PREFIX: C:\LLVM\llvm-18.1.8-windows-amd64-msvc17-msvcrt + steps: + - uses: actions/checkout@v4 + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: master - name: Download LLVM 18 run: | Invoke-WebRequest -Uri "https://github.com/vovkos/llvm-package-windows/releases/download/llvm-18.1.8/llvm-18.1.8-windows-amd64-msvc17-msvcrt.7z" -OutFile "$env:TEMP\llvm.7z" 7z x "$env:TEMP\llvm.7z" -oC:\LLVM - - name: Verify LLVM - run: | - Get-ChildItem C:\LLVM -Recurse -Name "*.lib" | Select-Object -First 5 - Get-ChildItem C:\LLVM -Directory - - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 - name: Build - run: zig build -Dstatic-llvm -Dllvm-prefix="C:\LLVM\llvm-18.1.8-windows-amd64-msvc17-msvcrt" -Dtarget=x86_64-windows-msvc + shell: cmd + run: zig build -Dstatic-llvm -Dllvm-prefix=%LLVM_PREFIX% -Dtarget=x86_64-windows-msvc - name: Test + shell: cmd continue-on-error: true - run: zig build test -Dstatic-llvm -Dllvm-prefix="C:\LLVM\llvm-18.1.8-windows-amd64-msvc17-msvcrt" -Dtarget=x86_64-windows-msvc --summary all + run: zig build test -Dstatic-llvm -Dllvm-prefix=%LLVM_PREFIX% -Dtarget=x86_64-windows-msvc --summary all - name: Package run: Compress-Archive -Path zig-out\bin\* -DestinationPath sx-windows-x86_64.zip diff --git a/src/codegen.zig b/src/codegen.zig index 40cda46..7a55985 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -12,6 +12,88 @@ const errors = @import("errors.zig"); const sema = @import("sema.zig"); const comptime_mod = @import("comptime.zig"); +pub const TargetConfig = struct { + /// Target triple (e.g. "aarch64-apple-darwin"). Null = host default. + triple: ?[*:0]const u8 = null, + /// CPU name (e.g. "generic", "apple-m1"). Null = "generic". + cpu: ?[*:0]const u8 = null, + /// CPU features string (e.g. "+avx2"). Null = "". + features: ?[*:0]const u8 = null, + /// Optimization level. + opt_level: OptLevel = .default, + /// Library search paths (-L flags). + lib_paths: []const []const u8 = &.{}, + /// Output path override. + output_path: ?[]const u8 = null, + /// Linker command (null = "cc" on Unix, "link.exe" on Windows). + linker: ?[]const u8 = null, + /// Sysroot for cross-compilation (passed as --sysroot to linker). + sysroot: ?[]const u8 = null, + + pub const OptLevel = enum { + none, + less, + default, + aggressive, + + pub fn toLLVM(self: OptLevel) c.LLVMCodeGenOptLevel { + return switch (self) { + .none => c.LLVMCodeGenLevelNone, + .less => c.LLVMCodeGenLevelLess, + .default => c.LLVMCodeGenLevelDefault, + .aggressive => c.LLVMCodeGenLevelAggressive, + }; + } + }; + + /// Check if target triple indicates aarch64/arm64 (runtime check, not comptime). + pub fn isAarch64(self: TargetConfig) bool { + return self.tripleHasPrefix("aarch64", "arm64"); + } + + /// Check if target triple indicates x86_64/x86-64. + pub fn isX86_64(self: TargetConfig) bool { + return self.tripleHasPrefix("x86_64", "x86-64"); + } + + /// Check if target triple indicates Windows (contains "windows" or "win32"). + pub fn isWindows(self: TargetConfig) bool { + return self.tripleContains("windows") or self.tripleContains("win32"); + } + + fn tripleHasPrefix(self: TargetConfig, prefix1: []const u8, prefix2: []const u8) bool { + if (self.triple) |t| { + const span = std.mem.span(t); + return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2); + } + const dt = c.LLVMGetDefaultTargetTriple(); + defer c.LLVMDisposeMessage(dt); + const span = std.mem.span(dt); + return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2); + } + + fn tripleContains(self: TargetConfig, needle: []const u8) bool { + if (self.triple) |t| { + return std.mem.indexOf(u8, std.mem.span(t), needle) != null; + } + const dt = c.LLVMGetDefaultTargetTriple(); + defer c.LLVMDisposeMessage(dt); + return std.mem.indexOf(u8, std.mem.span(dt), needle) != null; + } + + pub fn getCpu(self: TargetConfig) [*:0]const u8 { + return self.cpu orelse "generic"; + } + + pub fn getFeatures(self: TargetConfig) [*:0]const u8 { + return self.features orelse ""; + } + + pub fn getLinker(self: TargetConfig) []const u8 { + return self.linker orelse "cc"; + } +}; + pub const CodeGen = struct { context: c.LLVMContextRef, module: c.LLVMModuleRef, @@ -95,6 +177,8 @@ pub const CodeGen = struct { foreign_libraries: std.ArrayList([]const u8), // Set of foreign function names (for ABI lowering at call sites) foreign_fns: std.StringHashMap(void), + // Target configuration (triple, cpu, opt level, lib paths, linker) + target_config: TargetConfig = .{}, const DeferredFn = struct { fd: ast.FnDecl, @@ -167,10 +251,19 @@ pub const CodeGen = struct { ty: Type, // sx type }; - pub fn init(allocator: std.mem.Allocator, module_name: [*:0]const u8) CodeGen { + pub fn init(allocator: std.mem.Allocator, module_name: [*:0]const u8, target_config: TargetConfig) CodeGen { const ctx = c.LLVMContextCreate(); const module = c.LLVMModuleCreateWithNameInContext(module_name, ctx); const builder = c.LLVMCreateBuilderInContext(ctx); + + // Set target triple on module so it appears in IR output + if (target_config.triple) |t| { + c.LLVMSetTarget(module, t); + } else { + const default_triple = c.LLVMGetDefaultTargetTriple(); + c.LLVMSetTarget(module, default_triple); + c.LLVMDisposeMessage(default_triple); + } return .{ .context = ctx, .module = module, @@ -200,6 +293,7 @@ pub const CodeGen = struct { .deferred_fn_bodies = std.ArrayList(DeferredFn).empty, .foreign_libraries = std.ArrayList([]const u8).empty, .foreign_fns = std.StringHashMap(void).init(allocator), + .target_config = target_config, }; } @@ -1143,6 +1237,8 @@ pub const CodeGen = struct { const is_main = std.mem.eql(u8, name, "main"); const ret_llvm_type = if (is_main) c.LLVMInt32TypeInContext(self.context) + else if (is_foreign and ret_sx_type.isStruct()) + self.getForeignReturnABIType(ret_sx_type) else self.typeToLLVM(ret_sx_type); @@ -1175,19 +1271,33 @@ pub const CodeGen = struct { ); } - /// For foreign (C ABI) functions on ARM64, struct parameters must be lowered - /// to their ABI-equivalent types. LLVM does NOT do this automatically. - /// - HFA (1-4 same float/double fields): [N x float/double] - /// - Non-HFA ≤ 8 bytes: i64 - /// - Non-HFA 9-16 bytes: [2 x i64] + /// For foreign (C ABI) functions, struct parameters must be lowered to their + /// ABI-equivalent types. LLVM does NOT do this automatically on all targets. + /// Dispatches to architecture-specific lowering based on target config. fn getForeignParamABIType(self: *CodeGen, sx_ty: Type) c.LLVMTypeRef { - const is_aarch64 = comptime @import("builtin").cpu.arch == .aarch64; - if (!is_aarch64) return self.typeToLLVM(sx_ty); if (!sx_ty.isStruct()) return self.typeToLLVM(sx_ty); const sname = self.type_aliases.get(sx_ty.struct_type) orelse sx_ty.struct_type; const info = self.struct_types.get(sname) orelse return self.typeToLLVM(sx_ty); + if (self.target_config.isAarch64()) { + return self.aarch64ParamABI(info); + } else if (self.target_config.isX86_64()) { + if (self.target_config.isWindows()) { + return self.win64ParamABI(info); + } + return self.x86_64SysVParamABI(info); + } + // Unknown architecture: pass struct type as-is (let LLVM backend handle it) + return info.llvm_type; + } + + /// AArch64 ABI: struct parameter lowering. + /// - HFA (1-4 same float/double fields): [N x float/double] + /// - Non-HFA ≤ 8 bytes: i64 + /// - Non-HFA 9-16 bytes: [2 x i64] + /// - > 16 bytes: pass as-is (indirect, not yet fully handled) + fn aarch64ParamABI(self: *CodeGen, info: StructInfo) c.LLVMTypeRef { // Check HFA: 1-4 fields all of the same float type const field_types = info.field_types; if (field_types.len >= 1 and field_types.len <= 4) { @@ -1215,10 +1325,119 @@ pub const CodeGen = struct { const size = c.LLVMStoreSizeOfType(data_layout, info.llvm_type); if (size <= 8) return c.LLVMInt64TypeInContext(self.context); if (size <= 16) return c.LLVMArrayType2(c.LLVMInt64TypeInContext(self.context), 2); - // > 16 bytes: pass by pointer (indirect) — not yet handled, fall back to struct type return info.llvm_type; } + /// x86-64 SysV ABI: struct parameter lowering. + /// Each 8-byte "eightbyte" is classified as INTEGER or SSE: + /// - If all fields in the eightbyte are float/double: SSE (passed in XMM register) + /// - If any field is integer/pointer: INTEGER (passed in GPR) + /// - Structs > 16 bytes: passed in memory (by pointer) + fn x86_64SysVParamABI(self: *CodeGen, info: StructInfo) c.LLVMTypeRef { + const data_layout = c.LLVMGetModuleDataLayout(self.module); + const size = c.LLVMStoreSizeOfType(data_layout, info.llvm_type); + + // > 16 bytes: MEMORY class (passed by pointer, handled by LLVM backend) + if (size > 16) return info.llvm_type; + + // Single eightbyte (≤ 8 bytes) + if (size <= 8) { + return self.classifyEightbyte(info.field_types, size); + } + + // Two eightbytes (9-16 bytes): classify each half independently + // Split fields into first eightbyte (offset < 8) and second eightbyte (offset >= 8) + var first_eb_types = std.ArrayList(Type).empty; + var second_eb_types = std.ArrayList(Type).empty; + var second_eb_size: u64 = 0; + + const struct_ty = info.llvm_type; + for (info.field_types, 0..) |ft, idx| { + const offset = c.LLVMOffsetOfElement(data_layout, struct_ty, @intCast(idx)); + if (offset < 8) { + first_eb_types.append(self.allocator, ft) catch return info.llvm_type; + } else { + second_eb_types.append(self.allocator, ft) catch return info.llvm_type; + const field_llvm = self.typeToLLVM(ft); + second_eb_size += c.LLVMStoreSizeOfType(data_layout, field_llvm); + } + } + + const eb1 = self.classifyEightbyte(first_eb_types.items, 8); + const eb2 = self.classifyEightbyte(second_eb_types.items, if (second_eb_size > 0) second_eb_size else size - 8); + + // Compose the two eightbytes into a struct type + var members: [2]c.LLVMTypeRef = .{ eb1, eb2 }; + return c.LLVMStructTypeInContext(self.context, &members, 2, 0); + } + + /// Classify a single x86-64 eightbyte: if all fields are float, return SSE type; + /// otherwise return an integer type matching the byte size. + fn classifyEightbyte(self: *CodeGen, field_types_in_eb: []const Type, byte_size: u64) c.LLVMTypeRef { + if (field_types_in_eb.len == 0) { + // No fields in this chunk — use integer padding + return c.LLVMIntTypeInContext(self.context, @intCast(byte_size * 8)); + } + + // Check if all fields are SSE (float/double) + var all_sse = true; + var float_count: u32 = 0; + var double_count: u32 = 0; + for (field_types_in_eb) |ft| { + if (ft == .f32) { + float_count += 1; + } else if (ft == .f64) { + double_count += 1; + } else { + all_sse = false; + break; + } + } + + if (all_sse) { + // SSE class: return appropriate float type + if (double_count > 0 and float_count == 0) { + if (double_count == 1) return c.LLVMDoubleTypeInContext(self.context); + // Multiple doubles shouldn't fit in one eightbyte (double = 8 bytes) + return c.LLVMDoubleTypeInContext(self.context); + } + if (float_count > 0 and double_count == 0) { + if (float_count == 1) return c.LLVMFloatTypeInContext(self.context); + // 2 floats = 8 bytes, fits in one eightbyte + return c.LLVMArrayType2(c.LLVMFloatTypeInContext(self.context), @intCast(float_count)); + } + // Mixed float/double in one eightbyte shouldn't happen (float=4, double=8) + // but fall through to integer just in case + } + + // INTEGER class: coerce to integer matching the byte size + return c.LLVMIntTypeInContext(self.context, @intCast(byte_size * 8)); + } + + /// Windows x64 ABI: struct parameter lowering. + /// Only structs of exactly 1, 2, 4, or 8 bytes are passed in a register. + /// Everything else is passed by pointer (handled by LLVM backend). + fn win64ParamABI(self: *CodeGen, info: StructInfo) c.LLVMTypeRef { + const data_layout = c.LLVMGetModuleDataLayout(self.module); + const size = c.LLVMStoreSizeOfType(data_layout, info.llvm_type); + + // Windows x64: only power-of-2 sizes ≤ 8 passed in register + if (size == 1 or size == 2 or size == 4 or size == 8) { + return c.LLVMIntTypeInContext(self.context, @intCast(size * 8)); + } + // All other sizes: passed by pointer (LLVM handles byval) + return info.llvm_type; + } + + /// For foreign functions returning structs, apply the same ABI lowering as parameters. + /// The rules for return values match parameter rules on both AArch64 and x86-64 SysV + /// for small structs (≤ 16 bytes). Larger structs use sret (handled by LLVM). + fn getForeignReturnABIType(self: *CodeGen, sx_ty: Type) c.LLVMTypeRef { + // Reuse the same classification as parameters — the rules are identical + // for small struct returns on both AArch64 and x86-64 SysV. + return self.getForeignParamABIType(sx_ty); + } + /// Convert a struct value to its C ABI representation for a foreign call. /// Stores the struct to memory, then loads as the ABI type. fn convertStructToABI(self: *CodeGen, struct_val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef, abi_ty: c.LLVMTypeRef) c.LLVMValueRef { @@ -5894,11 +6113,13 @@ pub const CodeGen = struct { std.debug.print("{s}\n", .{ir[0..len]}); } - pub fn emitObject(self: *CodeGen, output_path: [*:0]const u8) !void { + fn emitToFile(self: *CodeGen, output_path: [*:0]const u8, file_type: c.LLVMCodeGenFileType) !void { llvm.initAllTargets(); - const triple = c.LLVMGetDefaultTargetTriple(); - defer c.LLVMDisposeMessage(triple); + 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; @@ -5912,9 +6133,9 @@ pub const CodeGen = struct { const tm = c.LLVMCreateTargetMachine( target, triple, - "generic", - "", - c.LLVMCodeGenLevelDefault, + cfg.getCpu(), + cfg.getFeatures(), + cfg.opt_level.toLLVM(), c.LLVMRelocPIC, c.LLVMCodeModelDefault, ); @@ -5923,24 +6144,58 @@ pub const CodeGen = struct { c.LLVMSetTarget(self.module, triple); var err_msg2: [*c]u8 = null; - if (c.LLVMTargetMachineEmitToFile(tm, self.module, output_path, c.LLVMObjectFile, &err_msg2) != 0) { + 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 object file: {s}", .{msg}); + return self.emitErrorFmt("failed to emit file: {s}", .{msg}); } } - pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, output_bin: []const u8, libraries: []const []const u8) !void { - var argv = std.ArrayList([]const u8).empty; - try argv.appendSlice(allocator, &.{ "cc", output_obj, "-o", output_bin }); + pub fn emitObject(self: *CodeGen, output_path: [*:0]const u8) !void { + return self.emitToFile(output_path, c.LLVMObjectFile); + } - if (libraries.len > 0) { - // Add Homebrew library path on macOS - try argv.append(allocator, "-L/opt/homebrew/lib"); + pub fn emitAssembly(self: *CodeGen, output_path: [*:0]const u8) !void { + return self.emitToFile(output_path, c.LLVMAssemblyFile); + } + + 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; + + if (target_config.isWindows()) { + // Windows: MSVC-style linker flags + const linker = target_config.linker orelse "link.exe"; + try argv.appendSlice(allocator, &.{ linker, output_obj }); + try argv.append(allocator, try std.fmt.allocPrint(allocator, "/OUT:{s}", .{output_bin})); + + for (target_config.lib_paths) |lp| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "/LIBPATH:{s}", .{lp})); + } + for (libraries) |lib| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "{s}.lib", .{lib})); + } + } else { + // Unix: cc-style linker flags + try argv.appendSlice(allocator, &.{ target_config.getLinker(), output_obj, "-o", output_bin }); + + if (target_config.sysroot) |sr| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr})); + } + + // User-supplied library paths first + for (target_config.lib_paths) |lp| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp})); + } + + // Auto-detect host OS library paths when linking foreign libraries + if (libraries.len > 0 and target_config.triple == null) { + for (host_lib_paths) |path| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{path})); + } + } for (libraries) |lib| { - const flag = try std.fmt.allocPrint(allocator, "-l{s}", .{lib}); - try argv.append(allocator, flag); + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); } } @@ -5952,4 +6207,22 @@ pub const CodeGen = struct { if (result != .exited) return error.LinkError; if (result.exited != 0) return error.LinkError; } + + /// Common library paths for the host OS, computed at comptime. + const host_lib_paths = blk: { + const builtin = @import("builtin"); + var paths: []const []const u8 = &.{}; + if (builtin.os.tag == .macos) { + if (builtin.cpu.arch == .aarch64) { + // Apple Silicon Homebrew + paths = &.{ "/opt/homebrew/lib", "/usr/local/lib" }; + } else { + // Intel Mac Homebrew + paths = &.{"/usr/local/lib"}; + } + } else if (builtin.os.tag == .linux) { + paths = &.{ "/usr/local/lib", "/usr/lib" }; + } + break :blk paths; + }; }; diff --git a/src/core.zig b/src/core.zig index 0bbb955..c0d2c78 100644 --- a/src/core.zig +++ b/src/core.zig @@ -7,12 +7,15 @@ const codegen = @import("codegen.zig"); const errors = @import("errors.zig"); const Node = ast.Node; +pub const TargetConfig = codegen.TargetConfig; + pub const Compilation = struct { allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, diagnostics: errors.DiagnosticList, + target_config: TargetConfig, // Pipeline results root: ?*Node = null, @@ -21,7 +24,7 @@ pub const Compilation = struct { sema_result: ?sema.SemaResult = null, cg: ?codegen.CodeGen = null, - pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8) Compilation { + pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, target_config: TargetConfig) Compilation { return .{ .allocator = allocator, .io = io, @@ -29,6 +32,7 @@ pub const Compilation = struct { .source = source, .diagnostics = errors.DiagnosticList.init(allocator, source, file_path), .import_sources = std.StringHashMap([:0]const u8).init(allocator), + .target_config = target_config, }; } @@ -83,7 +87,7 @@ pub const Compilation = struct { pub fn generateCode(self: *Compilation) !void { const root = self.resolved_root orelse self.root orelse return error.CompileError; - var cg = codegen.CodeGen.init(self.allocator, "sx_module"); + var cg = codegen.CodeGen.init(self.allocator, "sx_module", self.target_config); cg.diagnostics = &self.diagnostics; if (self.sema_result) |*sr| { cg.sema_result = sr; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 12029ad..82e4ca3 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -893,7 +893,7 @@ pub const Server = struct { const source = try self.allocator.dupeZ(u8, text); const file_path = uriToFilePath(uri) orelse ""; - var comp = sx.core.Compilation.init(self.allocator, self.io, file_path, source); + var comp = sx.core.Compilation.init(self.allocator, self.io, file_path, source, .{}); defer comp.deinit(); comp.parse() catch { diff --git a/src/main.zig b/src/main.zig index dbe6362..96788b6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,22 +19,75 @@ pub fn main(init: std.process.Init) !void { return; } - if (args.len < 3) { - printUsage(); - return; + // Parse flags and positional arguments + var input_path: ?[]const u8 = null; + var target_config = sx.codegen.TargetConfig{}; + var lib_paths = std.ArrayList([]const u8).empty; + + 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; } + target_config.triple = (try allocator.dupeZ(u8, args[i])).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; + }; + } 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.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.startsWith(u8, arg, "-")) { + input_path = arg; + } else { + std.debug.print("error: unknown flag '{s}'\n", .{arg}); + return; + } } - const input_path = args[2]; + target_config.lib_paths = try lib_paths.toOwnedSlice(allocator); + + const path = input_path orelse { + printUsage(); + return; + }; if (std.mem.eql(u8, command, "build")) { - const output_name = deriveOutputName(input_path); - compile(allocator, io, input_path, output_name) catch return; + const output_name = target_config.output_path orelse deriveOutputName(path); + compile(allocator, io, path, output_name, target_config) catch return; std.debug.print("compiled: {s}\n", .{output_name}); } else if (std.mem.eql(u8, command, "ir")) { - emitIR(allocator, io, input_path) catch return; + 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 = "/tmp/sx_run_tmp"; - compile(allocator, io, input_path, tmp_bin) catch return; + 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 {}; } @@ -53,16 +106,34 @@ pub fn main(init: std.process.Init) !void { } } +fn parseOptLevel(s: []const u8) ?sx.codegen.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 [file.sx] + \\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 (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 + \\ , .{}); } @@ -96,7 +167,7 @@ fn deriveOutputName(input_path: []const u8) []const u8 { // Get basename (strip directory) var start: usize = 0; for (input_path, 0..) |ch, i| { - if (ch == '/') start = i + 1; + if (ch == '/' or ch == '\\') start = i + 1; } const basename = input_path[start..]; // Strip .sx extension @@ -115,10 +186,10 @@ 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) !sx.core.Compilation { +fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !sx.core.Compilation { const source = try readSource(allocator, io, input_path); - var comp = sx.core.Compilation.init(allocator, io, input_path, source); + var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config); errdefer comp.deinit(); comp.parse() catch { comp.renderErrors(); return error.CompileError; }; @@ -131,14 +202,26 @@ fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const return comp; } -fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !void { - var comp = try compilePipeline(allocator, io, input_path); +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); defer comp.deinit(); comp.cg.?.printIR(); } -fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8) !void { - var comp = try compilePipeline(allocator, io, input_path); +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); + 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.cg.?.emitAssembly(asm_path_z.ptr) catch { comp.renderErrors(); 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.codegen.TargetConfig) !void { + var comp = try compilePipeline(allocator, io, input_path, target_config); defer comp.deinit(); var cg = &comp.cg.?; @@ -148,7 +231,7 @@ fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, out cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; }; // Link - sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, cg.foreign_libraries.items) catch { + sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, cg.foreign_libraries.items, target_config) catch { std.debug.print("error: linking failed\n", .{}); return error.CompileError; };