`target.link` now takes a `has_jni_main: bool` parameter (passed by
main.zig from `comp.getJniMainEmissions().len > 0`). When set:
- native_app_glue.c is not compiled — no `.glue.o` produced.
- `-u ANativeActivity_onCreate` is not added to the link argv.
- The Java-driven Activity is the entry; the .so just provides JNI
impls, bound at load time via the `JNI_OnLoad` slice R.3 will
synthesize.
Legacy NativeActivity builds (no `#jni_main` decl) are unchanged: glue
is still compiled and `ANativeActivity_onCreate` still retained.
Verified end-to-end:
- #jni_main .so: `llvm-nm -D` shows neither `ANativeActivity_onCreate`
nor `android_main` (correct — Java side drives entry).
- Legacy .so (99-android-egl-clear): both symbols still exported.
131 host / 4 cross / zig build test all green.
857 lines
36 KiB
Zig
857 lines
36 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);
|
|
|
|
// Stdlib discovered from binary location (or $SX_STDLIB_PATH override).
|
|
// Empty slice on hosts where discovery fails — imports fall back to CWD.
|
|
const stdlib_paths = sx.imports.discoverStdlibPaths(allocator) catch &[_][]const u8{};
|
|
|
|
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, stdlib_paths);
|
|
return;
|
|
}
|
|
|
|
// Parse flags and positional arguments
|
|
var input_path: ?[]const u8 = null;
|
|
var target_config = sx.target.TargetConfig{};
|
|
var lib_paths = std.ArrayList([]const u8).empty;
|
|
var framework_paths = std.ArrayList([]const u8).empty;
|
|
var link_flags = std.ArrayList([]const u8).empty;
|
|
var show_timing: bool = false;
|
|
var explicit_opt: bool = false;
|
|
var enable_cache: bool = false;
|
|
|
|
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; }
|
|
const raw = args[i];
|
|
// Shorthand aliases for common targets
|
|
const expanded = if (std.mem.eql(u8, raw, "wasm") or std.mem.eql(u8, raw, "wasm32") or std.mem.eql(u8, raw, "emscripten"))
|
|
"wasm32-unknown-emscripten"
|
|
else if (std.mem.eql(u8, raw, "wasm64"))
|
|
"wasm64-unknown-emscripten"
|
|
else if (std.mem.eql(u8, raw, "macos") or std.mem.eql(u8, raw, "macos-arm"))
|
|
"aarch64-apple-macos"
|
|
else if (std.mem.eql(u8, raw, "macos-x86"))
|
|
"x86_64-apple-macos"
|
|
else if (std.mem.eql(u8, raw, "linux") or std.mem.eql(u8, raw, "linux-x86"))
|
|
"x86_64-unknown-linux-gnu"
|
|
else if (std.mem.eql(u8, raw, "linux-arm"))
|
|
"aarch64-unknown-linux-gnu"
|
|
else if (std.mem.eql(u8, raw, "windows"))
|
|
"x86_64-windows-msvc"
|
|
else if (std.mem.eql(u8, raw, "ios") or std.mem.eql(u8, raw, "ios-arm"))
|
|
"arm64-apple-ios14.0"
|
|
else if (std.mem.eql(u8, raw, "ios-sim") or std.mem.eql(u8, raw, "ios-sim-arm"))
|
|
"arm64-apple-ios14.0-simulator"
|
|
else if (std.mem.eql(u8, raw, "ios-sim-x86"))
|
|
"x86_64-apple-ios14.0-simulator"
|
|
else if (std.mem.eql(u8, raw, "android") or std.mem.eql(u8, raw, "android-arm64"))
|
|
"aarch64-linux-android21"
|
|
else if (std.mem.eql(u8, raw, "android-x86_64"))
|
|
"x86_64-linux-android21"
|
|
else
|
|
raw;
|
|
target_config.triple = (try allocator.dupeZ(u8, expanded)).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;
|
|
};
|
|
explicit_opt = true;
|
|
} else if (std.mem.eql(u8, arg, "-o")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: -o requires a value\n", .{}); return; }
|
|
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.eql(u8, arg, "--bundle")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --bundle requires a path (e.g. MyApp.app)\n", .{}); return; }
|
|
target_config.bundle_path = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--apk")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --apk requires a path (e.g. out.apk)\n", .{}); return; }
|
|
target_config.apk_path = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--manifest")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --manifest requires a path\n", .{}); return; }
|
|
target_config.manifest_path = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--keystore")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --keystore requires a path\n", .{}); return; }
|
|
target_config.keystore_path = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--bundle-id")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --bundle-id requires a value (e.g. co.swipelab.myapp)\n", .{}); return; }
|
|
target_config.bundle_id = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--codesign-identity")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --codesign-identity requires a value\n", .{}); return; }
|
|
target_config.codesign_identity = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--provisioning-profile")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --provisioning-profile requires a path\n", .{}); return; }
|
|
target_config.provisioning_profile = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--entitlements")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --entitlements requires a path\n", .{}); return; }
|
|
target_config.entitlements_path = args[i];
|
|
} else if (std.mem.eql(u8, arg, "--time")) {
|
|
show_timing = true;
|
|
} else if (std.mem.eql(u8, arg, "--cache")) {
|
|
enable_cache = true;
|
|
} 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, "-F")) {
|
|
if (arg.len > 2) {
|
|
try framework_paths.append(allocator, arg[2..]);
|
|
} else {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: -F requires a value\n", .{}); return; }
|
|
try framework_paths.append(allocator, args[i]);
|
|
}
|
|
} else if (std.mem.eql(u8, arg, "--lflags")) {
|
|
i += 1;
|
|
if (i >= args.len) { std.debug.print("error: --lflags requires a value\n", .{}); return; }
|
|
try link_flags.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);
|
|
target_config.framework_paths = try framework_paths.toOwnedSlice(allocator);
|
|
target_config.extra_link_flags = try link_flags.toOwnedSlice(allocator);
|
|
|
|
// Auto-discover iOS SDK once so both the C compile path and the link
|
|
// path see the same sysroot. Honors any explicit --sysroot.
|
|
if (target_config.isIOS() and target_config.sysroot == null) {
|
|
const sdk_name: []const u8 = if (target_config.isIOSSimulator()) "iphonesimulator" else "iphoneos";
|
|
target_config.sysroot = sx.target.discoverAppleSdk(allocator, io, sdk_name) catch null;
|
|
}
|
|
|
|
// Same idea for Android — the NDK root must be visible to BOTH the
|
|
// C-import compile path (so `--sysroot ndk/.../sysroot` finds bionic
|
|
// headers) and the link path. By convention, target_config.sysroot
|
|
// holds the NDK root on Android (target.zig's link branch + c_import.zig
|
|
// both read it). Honors any explicit --sysroot.
|
|
if (target_config.isAndroid() and target_config.sysroot == null) {
|
|
target_config.sysroot = sx.target.discoverAndroidNdk(allocator, io) catch null;
|
|
}
|
|
|
|
const path = input_path orelse {
|
|
printUsage();
|
|
return;
|
|
};
|
|
|
|
if (std.mem.eql(u8, command, "build")) {
|
|
const output_name = target_config.output_path orelse blk: {
|
|
const base = deriveOutputName(path);
|
|
if (target_config.isEmscripten()) {
|
|
break :blk try std.fmt.allocPrint(allocator, "{s}.html", .{base});
|
|
}
|
|
break :blk base;
|
|
};
|
|
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache, stdlib_paths) catch std.process.exit(1);
|
|
} else if (std.mem.eql(u8, command, "ir")) {
|
|
emitIR(allocator, io, path, target_config, stdlib_paths) catch std.process.exit(1);
|
|
} else if (std.mem.eql(u8, command, "ir-dump")) {
|
|
dumpSxIR(allocator, io, path, stdlib_paths) catch std.process.exit(1);
|
|
} else if (std.mem.eql(u8, command, "asm")) {
|
|
emitAsm(allocator, io, path, target_config, stdlib_paths) catch std.process.exit(1);
|
|
} else if (std.mem.eql(u8, command, "run")) {
|
|
if (target_config.isWasm()) {
|
|
std.debug.print("error: 'run' is not supported for wasm targets. Use 'build' instead.\n", .{});
|
|
return;
|
|
}
|
|
// Default to -O0 for run (faster compile) unless user explicitly set --opt
|
|
if (!explicit_opt) target_config.opt_level = .none;
|
|
var timer = Timing.init(io, show_timing);
|
|
|
|
// Phase A: read + parse + resolveImports (for cache key)
|
|
timer.mark();
|
|
const source = readSource(allocator, io, path) catch std.process.exit(1);
|
|
timer.record("read");
|
|
|
|
var comp = sx.core.Compilation.init(allocator, io, path, source, target_config, stdlib_paths);
|
|
defer comp.deinit();
|
|
|
|
timer.mark();
|
|
comp.parse() catch { comp.renderErrors(); std.process.exit(1); };
|
|
timer.record("parse");
|
|
|
|
timer.mark();
|
|
comp.resolveImports() catch { comp.renderErrors(); std.process.exit(1); };
|
|
timer.record("imports");
|
|
|
|
// Cache check — use .o files (precompiled object, skip IR compilation in JIT)
|
|
// Disable caching for files with top-level #run (side effects lost on cache hit)
|
|
const root = comp.resolved_root orelse comp.root orelse return;
|
|
const use_cache = enable_cache and !hasTopLevelRun(root);
|
|
const key = computeCacheKey(source, &comp.import_sources, target_config);
|
|
const cache_obj = cachePath(allocator, key, "o") catch std.process.exit(1);
|
|
|
|
timer.mark();
|
|
const obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef = blk: {
|
|
if (use_cache) {
|
|
// Try loading cached .o from disk
|
|
var buf: sx.llvm_api.c.LLVMMemoryBufferRef = null;
|
|
var err_msg: [*c]u8 = null;
|
|
if (sx.llvm_api.c.LLVMCreateMemoryBufferWithContentsOfFile(cache_obj.ptr, &buf, &err_msg) == 0) {
|
|
timer.record("cache");
|
|
break :blk buf;
|
|
}
|
|
if (err_msg != null) sx.llvm_api.c.LLVMDisposeMessage(err_msg);
|
|
}
|
|
|
|
// Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors)
|
|
comp.generateCode() catch { comp.renderErrors(); std.process.exit(1); };
|
|
timer.record("codegen");
|
|
|
|
timer.mark();
|
|
comp.ir_emitter.?.verifyWithMessage() catch std.process.exit(1);
|
|
const buf = comp.ir_emitter.?.emitObjectToMemory() catch std.process.exit(1);
|
|
timer.record("emit");
|
|
|
|
// Save .o to cache (extract data before JIT takes ownership)
|
|
if (use_cache) {
|
|
saveObjectToCache(buf, io, cache_obj);
|
|
}
|
|
|
|
break :blk buf;
|
|
};
|
|
|
|
// Compile C sources natively and dlopen before JIT
|
|
timer.mark();
|
|
var c_handle = compileCForJIT(allocator, io, &comp) catch { comp.renderErrors(); std.process.exit(1); };
|
|
defer c_handle.unload(io);
|
|
timer.record("c-import");
|
|
|
|
// dlopen #library dependencies so JIT can resolve foreign symbols
|
|
const libs = extractLibraries(allocator, root) catch std.process.exit(1);
|
|
var lib_handles = std.ArrayList(*anyopaque).empty;
|
|
defer {
|
|
for (lib_handles.items) |h| _ = std.c.dlclose(h);
|
|
}
|
|
for (libs) |lib_name| {
|
|
if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |handle| {
|
|
lib_handles.append(allocator, handle) catch {};
|
|
} else {
|
|
const e = std.c.dlerror();
|
|
if (e) |msg| std.debug.print("warning: could not load library '{s}': {s}\n", .{ lib_name, std.mem.span(msg) });
|
|
}
|
|
}
|
|
|
|
// JIT from precompiled object (relocation only, no IR compilation)
|
|
sx.llvm_api.initNativeTarget();
|
|
timer.mark();
|
|
const exit_code = sx.target.runJITFromObject(obj_buf) catch {
|
|
// JIT failed — fall back to AOT
|
|
timer.record("jit-fail");
|
|
runAOT(allocator, io, path, target_config, &timer, enable_cache, stdlib_paths) catch std.process.exit(1);
|
|
timer.printAll();
|
|
return;
|
|
};
|
|
timer.record("jit");
|
|
timer.printAll();
|
|
|
|
if (exit_code != 0) std.process.exit(exit_code);
|
|
} else {
|
|
printUsage();
|
|
}
|
|
}
|
|
|
|
/// Compile C sources from #import c blocks and dlopen them for JIT.
|
|
fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) !sx.c_import.CImportHandle {
|
|
const c_infos = try comp.collectCImportSources();
|
|
if (c_infos.len == 0) return .{ .allocator = allocator };
|
|
|
|
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos, comp.target_config);
|
|
return try sx.c_import.loadCObjectsForJIT(allocator, io, obj_bufs);
|
|
}
|
|
|
|
/// Compile C sources from #import c blocks to .o files for linking.
|
|
fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation, tmp_dir: []const u8) ![]const []const u8 {
|
|
const c_infos = try comp.collectCImportSources();
|
|
if (c_infos.len == 0) return &.{};
|
|
|
|
// For Emscripten targets, use emcc to cross-compile C sources
|
|
if (comp.target_config.isEmscripten()) {
|
|
return try sx.c_import.compileCWithEmcc(allocator, io, c_infos, comp.target_config, tmp_dir);
|
|
}
|
|
|
|
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos, comp.target_config);
|
|
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs, tmp_dir);
|
|
}
|
|
|
|
fn parseOptLevel(s: []const u8) ?sx.target.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 <target> Target triple or shorthand: wasm, macos, linux, windows, ios, ios-sim (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
|
|
\\ --lflags <flag> Extra linker flag (repeatable, e.g. --lflags -sUSE_SDL=2)
|
|
\\ --bundle <Name.app> Wrap the binary in an iOS/macOS .app bundle (after linking)
|
|
\\ --bundle-id <id> CFBundleIdentifier (required with --bundle)
|
|
\\ --codesign-identity <name> Codesigning identity (e.g. "Apple Development: ...")
|
|
\\ --provisioning-profile <path> .mobileprovision to embed (required for device)
|
|
\\ --entitlements <path> Entitlements plist (auto-extracted from profile if omitted)
|
|
\\ --cache Enable build caching
|
|
\\ --time Show compilation timing breakdown
|
|
\\
|
|
, .{});
|
|
}
|
|
|
|
fn runLsp(allocator: std.mem.Allocator, io: std.Io, stdlib_paths: []const []const u8) 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, stdlib_paths);
|
|
|
|
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, idx| {
|
|
if (ch == '/' or ch == '\\') start = idx + 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.target.TargetConfig, timer: *Timing, stdlib_paths: []const []const u8) !sx.core.Compilation {
|
|
timer.mark();
|
|
const source = try readSource(allocator, io, input_path);
|
|
timer.record("read");
|
|
|
|
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config, stdlib_paths);
|
|
errdefer comp.deinit();
|
|
|
|
timer.mark();
|
|
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("parse");
|
|
|
|
timer.mark();
|
|
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("imports");
|
|
|
|
timer.mark();
|
|
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("codegen");
|
|
|
|
timer.mark();
|
|
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
|
|
timer.record("verify");
|
|
|
|
return comp;
|
|
}
|
|
|
|
fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, stdlib_paths: []const []const u8) !void {
|
|
const source = try readSource(allocator, io, input_path);
|
|
var comp = sx.core.Compilation.init(allocator, io, input_path, source, .{}, stdlib_paths);
|
|
defer comp.deinit();
|
|
|
|
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
|
|
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
|
|
|
var ir_module = comp.lowerToIR() catch { comp.renderErrors(); return error.CompileError; };
|
|
defer ir_module.deinit();
|
|
|
|
var aw = std.Io.Writer.Allocating.init(allocator);
|
|
sx.ir.printModule(&ir_module, &aw.writer) catch return error.CompileError;
|
|
var result = aw.writer.toArrayList();
|
|
defer result.deinit(allocator);
|
|
std.debug.print("{s}", .{result.items});
|
|
}
|
|
|
|
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, stdlib_paths: []const []const u8) !void {
|
|
var timer = Timing.init(io, false);
|
|
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer, stdlib_paths);
|
|
defer comp.deinit();
|
|
comp.ir_emitter.?.printIR();
|
|
}
|
|
|
|
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, stdlib_paths: []const []const u8) !void {
|
|
var timer = Timing.init(io, false);
|
|
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer, stdlib_paths);
|
|
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.ir_emitter.?.emitAssembly(asm_path_z.ptr) catch 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.target.TargetConfig, show_timing: bool, enable_cache: bool, stdlib_paths: []const []const u8) !void {
|
|
var timer = Timing.init(io, show_timing);
|
|
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, enable_cache, stdlib_paths);
|
|
timer.printAll();
|
|
}
|
|
|
|
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {
|
|
// Phase A: read + parse + resolveImports (fast: ~0.5ms)
|
|
timer.mark();
|
|
const source = try readSource(allocator, io, input_path);
|
|
timer.record("read");
|
|
|
|
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config, stdlib_paths);
|
|
defer comp.deinit();
|
|
|
|
timer.mark();
|
|
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("parse");
|
|
|
|
timer.mark();
|
|
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("imports");
|
|
|
|
// Extract library + framework names from AST (needed for linking regardless of cache)
|
|
const root = comp.resolved_root orelse comp.root orelse return error.CompileError;
|
|
const libs = try extractLibraries(allocator, root);
|
|
var fws = try extractFrameworks(allocator, root);
|
|
|
|
// Create temp directory for build artifacts
|
|
const tmp_dir: []const u8 = ".sx-tmp";
|
|
std.Io.Dir.createDirPath(.cwd(), io, tmp_dir) catch {};
|
|
|
|
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}/main.o", .{tmp_dir}, 0);
|
|
|
|
// Cache: compute key and check for cached binary/.o
|
|
const key = computeCacheKey(source, &comp.import_sources, target_config);
|
|
const cache_obj = try cachePath(allocator, key, "o");
|
|
const cache_bin = try cachePath(allocator, key, "bin");
|
|
|
|
// Level 1: Try cached binary (skip everything — no codegen, no link)
|
|
if (enable_cache) bin_cache: {
|
|
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
|
|
timer.record("cache");
|
|
return;
|
|
}
|
|
|
|
// Level 2: Try cached .o (skip codegen+emit, still need link)
|
|
const used_obj_cache = blk: {
|
|
if (!enable_cache) break :blk false;
|
|
std.Io.Dir.copyFile(.cwd(), cache_obj, .cwd(), obj_path, io, .{}) catch break :blk false;
|
|
break :blk true;
|
|
};
|
|
|
|
if (used_obj_cache) {
|
|
timer.record("cache");
|
|
} else {
|
|
// Cache MISS — full codegen + emit
|
|
timer.mark();
|
|
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
|
timer.record("codegen");
|
|
|
|
timer.mark();
|
|
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
|
|
timer.record("verify");
|
|
|
|
timer.mark();
|
|
comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError;
|
|
timer.record("emit");
|
|
|
|
// Save .o to cache
|
|
if (enable_cache) {
|
|
std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {};
|
|
}
|
|
}
|
|
|
|
// Compile C sources from #import c blocks to .o files
|
|
timer.mark();
|
|
const c_obj_paths = compileCForBuild(allocator, io, &comp, tmp_dir) catch {
|
|
std.debug.print("error: C import compilation failed\n", .{});
|
|
return error.CompileError;
|
|
};
|
|
timer.record("c-import");
|
|
|
|
// Merge build config (from #run blocks) with CLI config
|
|
var merged_config = target_config;
|
|
const build_flags = comp.getBuildLinkFlags();
|
|
const build_fws = comp.getBuildFrameworks();
|
|
if (build_fws.len > 0) {
|
|
var merged_fws: std.ArrayList([]const u8) = .empty;
|
|
for (fws) |f| try merged_fws.append(allocator, f);
|
|
for (build_fws) |f| try merged_fws.append(allocator, f);
|
|
// Shadow the outer `fws` for the rest of the function by reassignment.
|
|
fws = try merged_fws.toOwnedSlice(allocator);
|
|
}
|
|
|
|
if (build_flags.len > 0) {
|
|
var all_flags: std.ArrayList([]const u8) = .empty;
|
|
for (target_config.extra_link_flags) |f| try all_flags.append(allocator, f);
|
|
for (build_flags) |f| try all_flags.append(allocator, f);
|
|
merged_config.extra_link_flags = try all_flags.toOwnedSlice(allocator);
|
|
}
|
|
// Override output path from #run if set (and no explicit -o was given on CLI)
|
|
const final_output = if (target_config.output_path == null)
|
|
(comp.getBuildOutputPath() orelse output_path)
|
|
else
|
|
output_path;
|
|
|
|
// Override WASM shell template from #run if set
|
|
if (comp.getBuildWasmShell()) |shell| {
|
|
merged_config.wasm_shell_path = shell;
|
|
}
|
|
|
|
// Ensure output directory exists
|
|
if (std.mem.lastIndexOfScalar(u8, final_output, '/')) |sep| {
|
|
if (sep > 0) {
|
|
std.Io.Dir.createDirPath(.cwd(), io, final_output[0..sep]) catch {};
|
|
}
|
|
}
|
|
|
|
// Link (sx .o + C .o files)
|
|
timer.mark();
|
|
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, fws, merged_config, comp.getJniMainEmissions().len > 0) catch {
|
|
std.debug.print("error: linking failed\n", .{});
|
|
return error.CompileError;
|
|
};
|
|
timer.record("link");
|
|
|
|
// Wrap into a .app bundle if requested (iOS/macOS).
|
|
if (merged_config.bundle_path) |bp| {
|
|
timer.mark();
|
|
sx.target.createBundle(allocator, io, final_output, merged_config, fws) catch std.process.exit(1);
|
|
timer.record("bundle");
|
|
std.debug.print("bundled: {s}\n", .{bp});
|
|
}
|
|
|
|
// Wrap into an .apk if requested (Android).
|
|
if (merged_config.apk_path) |ap| {
|
|
timer.mark();
|
|
sx.target.createApk(allocator, io, final_output, merged_config, comp.getJniMainEmissions()) catch std.process.exit(1);
|
|
timer.record("apk");
|
|
std.debug.print("apk: {s}\n", .{ap});
|
|
}
|
|
|
|
// Post-process wasm HTML: inject content hash for cache busting
|
|
if (merged_config.isEmscripten() and std.mem.endsWith(u8, final_output, ".html")) {
|
|
sx.target.postProcessWasmHtml(allocator, io, final_output);
|
|
}
|
|
|
|
// Save linked binary to cache
|
|
if (enable_cache) {
|
|
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
|
|
}
|
|
|
|
std.debug.print("compiled: {s}\n", .{final_output});
|
|
|
|
// Clean up temp directory and all build artifacts
|
|
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
|
|
const shell_tmp = std.fmt.allocPrint(allocator, "{s}.shell.html", .{obj_path}) catch null;
|
|
if (shell_tmp) |sp| std.Io.Dir.deleteFile(.cwd(), io, sp) catch {};
|
|
for (c_obj_paths) |cop| {
|
|
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
|
|
}
|
|
std.Io.Dir.deleteDir(.cwd(), io, tmp_dir) catch {};
|
|
}
|
|
|
|
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {
|
|
const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp";
|
|
try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, enable_cache, stdlib_paths);
|
|
defer {
|
|
std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {};
|
|
}
|
|
|
|
timer.mark();
|
|
var child = std.process.spawn(io, .{
|
|
.argv = &.{tmp_bin},
|
|
}) catch {
|
|
std.debug.print("error: failed to run program\n", .{});
|
|
return error.CompileError;
|
|
};
|
|
const term = child.wait(io) catch {
|
|
std.debug.print("error: program execution failed\n", .{});
|
|
return error.CompileError;
|
|
};
|
|
timer.record("exec");
|
|
|
|
switch (term) {
|
|
.exited => |code| if (code != 0) std.process.exit(code),
|
|
.signal => std.process.exit(1),
|
|
.stopped, .unknown => std.process.exit(1),
|
|
}
|
|
}
|
|
|
|
// --- Cache helpers ---
|
|
|
|
fn computeCacheKey(source: [:0]const u8, import_sources: *const std.StringHashMap([:0]const u8), target_config: sx.target.TargetConfig) u64 {
|
|
const Wyhash = std.hash.Wyhash;
|
|
var key = Wyhash.hash(0, source);
|
|
|
|
// XOR import hashes for order independence (HashMap iteration is non-deterministic)
|
|
var import_hash: u64 = 0;
|
|
var it = import_sources.iterator();
|
|
while (it.next()) |entry| {
|
|
var h = Wyhash.hash(0, entry.key_ptr.*);
|
|
h = Wyhash.hash(h, entry.value_ptr.*);
|
|
import_hash ^= h;
|
|
}
|
|
key = Wyhash.hash(key, std.mem.asBytes(&import_hash));
|
|
|
|
// Hash target config fields that affect codegen
|
|
if (target_config.triple) |t| key = Wyhash.hash(key, std.mem.span(t));
|
|
if (target_config.cpu) |cp| key = Wyhash.hash(key, std.mem.span(cp));
|
|
if (target_config.features) |f| key = Wyhash.hash(key, std.mem.span(f));
|
|
key = Wyhash.hash(key, std.mem.asBytes(&target_config.opt_level));
|
|
|
|
return key;
|
|
}
|
|
|
|
fn cachePath(allocator: std.mem.Allocator, key: u64, ext: []const u8) ![:0]const u8 {
|
|
return try std.fmt.allocPrintSentinel(allocator, ".sx-cache/{x:0>16}.{s}", .{ key, ext }, 0);
|
|
}
|
|
|
|
fn saveObjectToCache(obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef, io: std.Io, cache_path: [:0]const u8) void {
|
|
const c_api = sx.llvm_api.c;
|
|
const start = c_api.LLVMGetBufferStart(obj_buf);
|
|
const size = c_api.LLVMGetBufferSize(obj_buf);
|
|
if (start == null or size == 0) return;
|
|
const data = @as([*]const u8, @ptrCast(start))[0..size];
|
|
// Write to temp file, then copy to cache (make_path creates .sx-cache/ if needed)
|
|
std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = ".sx-cache-tmp", .data = data }) catch return;
|
|
std.Io.Dir.copyFile(.cwd(), ".sx-cache-tmp", .cwd(), cache_path, io, .{ .make_path = true }) catch {};
|
|
std.Io.Dir.deleteFile(.cwd(), io, ".sx-cache-tmp") catch {};
|
|
}
|
|
|
|
fn hasTopLevelRun(root: *const sx.ast.Node) bool {
|
|
for (root.data.root.decls) |decl| {
|
|
if (decl.data == .comptime_expr) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
|
|
var libs = std.ArrayList([]const u8).empty;
|
|
var seen = std.StringHashMap(void).init(allocator);
|
|
const addLib = struct {
|
|
fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void {
|
|
if (s.contains(name)) return;
|
|
try s.put(name, {});
|
|
try l.append(a, name);
|
|
}
|
|
}.f;
|
|
for (root.data.root.decls) |decl| {
|
|
switch (decl.data) {
|
|
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
|
|
.namespace_decl => |ns| {
|
|
for (ns.decls) |nd| {
|
|
switch (nd.data) {
|
|
.library_decl => |ld| try addLib(&libs, &seen, allocator, ld.lib_name),
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return try libs.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn extractFrameworks(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]const []const u8 {
|
|
var fws = std.ArrayList([]const u8).empty;
|
|
var seen = std.StringHashMap(void).init(allocator);
|
|
const addFw = struct {
|
|
fn f(l: *std.ArrayList([]const u8), s: *std.StringHashMap(void), a: std.mem.Allocator, name: []const u8) !void {
|
|
if (s.contains(name)) return;
|
|
try s.put(name, {});
|
|
try l.append(a, name);
|
|
}
|
|
}.f;
|
|
for (root.data.root.decls) |decl| {
|
|
switch (decl.data) {
|
|
.framework_decl => |fd| try addFw(&fws, &seen, allocator, fd.name),
|
|
.namespace_decl => |ns| {
|
|
for (ns.decls) |nd| {
|
|
switch (nd.data) {
|
|
.framework_decl => |fd| try addFw(&fws, &seen, allocator, fd.name),
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return try fws.toOwnedSlice(allocator);
|
|
}
|
|
|
|
/// Try to dlopen a library by name, searching user paths, host paths, and common naming conventions.
|
|
fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?*anyopaque {
|
|
const is_macos = comptime @import("builtin").os.tag == .macos;
|
|
const suffixes: []const []const u8 = if (is_macos) &.{ ".dylib", ".so" } else &.{ ".so", ".dylib" };
|
|
|
|
// Search paths: user-supplied first, then host defaults
|
|
const search_paths = comptime blk: {
|
|
var paths: []const []const u8 = &.{};
|
|
for (sx.target.host_lib_paths) |p| {
|
|
paths = paths ++ .{p};
|
|
}
|
|
break :blk paths;
|
|
};
|
|
|
|
// Try each path with each suffix
|
|
const all_paths = [_][]const []const u8{ user_lib_paths, search_paths };
|
|
for (&all_paths) |paths| {
|
|
for (paths) |dir| {
|
|
for (suffixes) |sfx| {
|
|
const full = std.fmt.allocPrintSentinel(allocator, "{s}/lib{s}{s}", .{ dir, lib_name, sfx }, 0) catch continue;
|
|
if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return h;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: bare name (let dlopen search its default paths)
|
|
for (suffixes) |sfx| {
|
|
const bare = std.fmt.allocPrintSentinel(allocator, "lib{s}{s}", .{ lib_name, sfx }, 0) catch continue;
|
|
if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return h;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Simple timing helper — records stage durations and prints a summary table.
|
|
const Timing = struct {
|
|
const max_entries = 16;
|
|
|
|
enabled: bool,
|
|
io: std.Io,
|
|
names: [max_entries][]const u8,
|
|
durations_ns: [max_entries]u64,
|
|
count: usize,
|
|
last: ?std.Io.Timestamp,
|
|
|
|
fn init(io: std.Io, enabled: bool) Timing {
|
|
return .{
|
|
.enabled = enabled,
|
|
.io = io,
|
|
.names = undefined,
|
|
.durations_ns = undefined,
|
|
.count = 0,
|
|
.last = if (enabled) std.Io.Timestamp.now(io, .awake) else null,
|
|
};
|
|
}
|
|
|
|
fn mark(self: *Timing) void {
|
|
if (self.enabled) self.last = std.Io.Timestamp.now(self.io, .awake);
|
|
}
|
|
|
|
fn record(self: *Timing, name: []const u8) void {
|
|
if (!self.enabled) return;
|
|
const now = std.Io.Timestamp.now(self.io, .awake);
|
|
const elapsed_ns: u64 = if (self.last) |prev| @intCast(prev.durationTo(now).nanoseconds) else 0;
|
|
if (self.count < max_entries) {
|
|
self.names[self.count] = name;
|
|
self.durations_ns[self.count] = elapsed_ns;
|
|
self.count += 1;
|
|
}
|
|
self.last = now;
|
|
}
|
|
|
|
fn printAll(self: *const Timing) void {
|
|
if (!self.enabled or self.count == 0) return;
|
|
var total_ns: u64 = 0;
|
|
for (self.durations_ns[0..self.count]) |d| total_ns += d;
|
|
|
|
std.debug.print("\n--- timing ---\n", .{});
|
|
for (0..self.count) |idx| {
|
|
const ms = @as(f64, @floatFromInt(self.durations_ns[idx])) / 1_000_000.0;
|
|
std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ self.names[idx], ms });
|
|
}
|
|
const total_ms = @as(f64, @floatFromInt(total_ns)) / 1_000_000.0;
|
|
std.debug.print(" {s:<10} {d:>7.1} ms\n", .{ "total", total_ms });
|
|
}
|
|
};
|