242 lines
9.3 KiB
Zig
242 lines
9.3 KiB
Zig
const std = @import("std");
|
|
const sx = @import("sx");
|
|
|
|
pub fn main(init: std.process.Init) !void {
|
|
const allocator = init.arena.allocator();
|
|
const io = init.io;
|
|
const args = try init.minimal.args.toSlice(allocator);
|
|
|
|
if (args.len < 2) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
const command = args[1];
|
|
|
|
// LSP subcommand doesn't need a file argument
|
|
if (std.mem.eql(u8, command, "lsp")) {
|
|
runLsp(allocator, io);
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 = 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, 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", .{});
|
|
return;
|
|
};
|
|
_ = child.wait(io) catch {
|
|
std.debug.print("error: program execution failed\n", .{});
|
|
return;
|
|
};
|
|
} else {
|
|
printUsage();
|
|
}
|
|
}
|
|
|
|
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> [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
|
|
\\
|
|
, .{});
|
|
}
|
|
|
|
fn runLsp(allocator: std.mem.Allocator, io: std.Io) void {
|
|
const Transport = sx.lsp.transport.Transport;
|
|
const Server = sx.lsp.server.Server;
|
|
|
|
const stdin_file = std.Io.File.stdin();
|
|
const stdout_file = std.Io.File.stdout();
|
|
|
|
var read_buf: [4096]u8 = undefined;
|
|
var stdin_reader = stdin_file.readerStreaming(io, &read_buf);
|
|
|
|
var transport = Transport.init(allocator, io, &stdin_reader.interface, stdout_file);
|
|
var server = Server.init(allocator, &transport, io);
|
|
|
|
while (true) {
|
|
const msg = transport.readMessage() catch |err| {
|
|
if (err == error.EndOfStream) break;
|
|
std.debug.print("lsp: read error: {}\n", .{err});
|
|
break;
|
|
};
|
|
|
|
const keep_going = server.handleMessage(msg);
|
|
|
|
if (!keep_going) break;
|
|
}
|
|
}
|
|
|
|
fn deriveOutputName(input_path: []const u8) []const u8 {
|
|
// Get basename (strip directory)
|
|
var start: usize = 0;
|
|
for (input_path, 0..) |ch, i| {
|
|
if (ch == '/' or ch == '\\') start = i + 1;
|
|
}
|
|
const basename = input_path[start..];
|
|
// Strip .sx extension
|
|
if (std.mem.endsWith(u8, basename, ".sx")) {
|
|
return basename[0 .. basename.len - 3];
|
|
}
|
|
return basename;
|
|
}
|
|
|
|
|
|
fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {
|
|
const source_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, input_path, allocator, .limited(10 * 1024 * 1024)) catch |err| {
|
|
std.debug.print("error: cannot read '{s}': {}\n", .{ input_path, err });
|
|
return error.CompileError;
|
|
};
|
|
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 {
|
|
const source = try readSource(allocator, io, input_path);
|
|
|
|
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config);
|
|
errdefer comp.deinit();
|
|
|
|
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
|
|
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
|
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
|
|
|
var cg = &comp.cg.?;
|
|
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
|
|
|
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);
|
|
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);
|
|
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.?;
|
|
|
|
// 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; };
|
|
|
|
// Link
|
|
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;
|
|
};
|
|
|
|
// Clean up object file
|
|
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
|
|
}
|