This commit is contained in:
agra
2026-02-11 21:22:03 +02:00
parent 9d96f05d3b
commit 9a2501f662
5 changed files with 449 additions and 56 deletions

View File

@@ -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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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 <command> [file.sx]
\\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 <triple> Target triple (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
\\
, .{});
}
@@ -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;
};