ios + ir cleanup

- ios: --target ios/ios-sim shorthands, iOS SDK auto-discovery,
  #framework directive + BuildOptions.add_framework hook,
  .app bundle + Info.plist + codesign (ad-hoc and real),
  --codesign-identity/--provisioning-profile/--entitlements flags,
  modules/std/{objc,uikit}.sx, dynamic class registration,
  typed objc_msgSend cast pattern, UIApplicationMain handoff,
  UIWindow scene attach. Runs on iPhone hardware.
- ir: silent .s64 defaults → loud diagnostics,
  resolveReturnType infers from body, sub-byte int sizes match LLVM,
  tuple type interning includes names, compile errors exit 1
- issue-NNNN convention: resolved bugs rename to focused features
- 50 regression tests passing
This commit is contained in:
agra
2026-05-17 13:19:08 +03:00
parent 69934592d8
commit 1c32d54e01
57 changed files with 978 additions and 106 deletions

View File

@@ -50,6 +50,12 @@ pub fn main(init: std.process.Init) !void {
"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
raw;
target_config.triple = (try allocator.dupeZ(u8, expanded)).ptr;
@@ -77,6 +83,26 @@ pub fn main(init: std.process.Init) !void {
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, "--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")) {
@@ -117,13 +143,13 @@ pub fn main(init: std.process.Init) !void {
}
break :blk base;
};
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch return;
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "ir")) {
emitIR(allocator, io, path, target_config) catch return;
emitIR(allocator, io, path, target_config) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "ir-dump")) {
dumpSxIR(allocator, io, path) catch return;
dumpSxIR(allocator, io, path) catch std.process.exit(1);
} else if (std.mem.eql(u8, command, "asm")) {
emitAsm(allocator, io, path, target_config) catch return;
emitAsm(allocator, io, path, target_config) 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", .{});
@@ -135,18 +161,18 @@ pub fn main(init: std.process.Init) !void {
// Phase A: read + parse + resolveImports (for cache key)
timer.mark();
const source = readSource(allocator, io, path) catch return;
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);
defer comp.deinit();
timer.mark();
comp.parse() catch { comp.renderErrors(); return; };
comp.parse() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("parse");
timer.mark();
comp.resolveImports() catch { comp.renderErrors(); return; };
comp.resolveImports() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("imports");
// Cache check — use .o files (precompiled object, skip IR compilation in JIT)
@@ -154,7 +180,7 @@ pub fn main(init: std.process.Init) !void {
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 return;
const cache_obj = cachePath(allocator, key, "o") catch std.process.exit(1);
timer.mark();
const obj_buf: sx.llvm_api.c.LLVMMemoryBufferRef = blk: {
@@ -170,12 +196,12 @@ pub fn main(init: std.process.Init) !void {
}
// Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors)
comp.generateCode() catch { comp.renderErrors(); return; };
comp.generateCode() catch { comp.renderErrors(); std.process.exit(1); };
timer.record("codegen");
timer.mark();
comp.ir_emitter.?.verifyWithMessage() catch return;
const buf = comp.ir_emitter.?.emitObjectToMemory() catch return;
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)
@@ -188,12 +214,12 @@ pub fn main(init: std.process.Init) !void {
// Compile C sources natively and dlopen before JIT
timer.mark();
var c_handle = compileCForJIT(allocator, io, &comp) catch { comp.renderErrors(); return; };
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 return;
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);
@@ -213,7 +239,7 @@ pub fn main(init: std.process.Init) !void {
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) catch return;
runAOT(allocator, io, path, target_config, &timer, enable_cache) catch std.process.exit(1);
timer.printAll();
return;
};
@@ -269,7 +295,7 @@ fn printUsage() void {
\\ lsp Start language server (LSP)
\\
\\Options:
\\ --target <target> Target triple or shorthand: wasm, macos, linux, windows (default: host)
\\ --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
@@ -277,6 +303,11 @@ fn printUsage() void {
\\ --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
\\
@@ -371,7 +402,7 @@ fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !v
defer ir_module.deinit();
var aw = std.Io.Writer.Allocating.init(allocator);
sx.ir.printModule(&ir_module, &aw.writer) catch return;
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});
@@ -410,7 +441,6 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
timer.record("read");
var comp = sx.core.Compilation.init(allocator, io, input_path, source, target_config);
errdefer comp.deinit();
defer comp.deinit();
timer.mark();
@@ -421,9 +451,10 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
timer.record("imports");
// Extract library names from AST (needed for linking regardless of cache)
// 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";
@@ -483,6 +514,14 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// 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);
@@ -509,12 +548,20 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// Link (sx .o + C .o files)
timer.mark();
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, merged_config) catch {
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, fws, merged_config) 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) catch std.process.exit(1);
timer.record("bundle");
std.debug.print("bundled: {s}\n", .{bp});
}
// 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);
@@ -639,6 +686,33 @@ fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]c
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;