cross
This commit is contained in:
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
323
src/codegen.zig
323
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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
119
src/main.zig
119
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 <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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user