diff --git a/.gitignore b/.gitignore index b6bb4f5..87557c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ zig-out .DS_Store .vscode/ .sx-cache +.sx-tmp current/ \ No newline at end of file diff --git a/examples/issue-0012.sx b/examples/52-match-optional-arms.sx similarity index 92% rename from examples/issue-0012.sx rename to examples/52-match-optional-arms.sx index f594a62..4b61c97 100644 --- a/examples/issue-0012.sx +++ b/examples/52-match-optional-arms.sx @@ -1,7 +1,5 @@ -// issue-0012: Pattern match expression with mixed null/concrete arms -// -// A match expression with both `null` arms and concrete struct value arms -// should produce an optional type (?T) and correctly wrap non-null values. +// Match expression with both `null` arms and concrete struct value arms +// produces an optional type (?T) and correctly wraps non-null values. #import "modules/std.sx"; diff --git a/examples/issue-0016.sx b/examples/53-callconv-c-callbacks.sx similarity index 54% rename from examples/issue-0016.sx rename to examples/53-callconv-c-callbacks.sx index 674ab03..48c9b6f 100644 --- a/examples/issue-0016.sx +++ b/examples/53-callconv-c-callbacks.sx @@ -1,7 +1,6 @@ -// issue-0016: C calling convention for function pointers passed to foreign callbacks. -// -// `callconv(.c)` ensures the function uses C ABI, so it can be safely -// passed as a callback to #foreign functions like SDL_AddEventWatch. +// `callconv(.c)` on function pointers passed to foreign callbacks — ensures +// the function uses C ABI so it can be safely invoked from `#foreign` +// functions like SDL_AddEventWatch. #import "modules/std.sx"; diff --git a/examples/issue-0017.sx b/examples/54-callconv-c-globals.sx similarity index 83% rename from examples/issue-0017.sx rename to examples/54-callconv-c-globals.sx index 9d59940..15b33ba 100644 --- a/examples/issue-0017.sx +++ b/examples/54-callconv-c-globals.sx @@ -1,5 +1,6 @@ -// issue-0017: Investigate data corruption in callconv(.c) callbacks -// when accessing struct methods on global pointers. +// `callconv(.c)` callbacks accessing struct methods on global pointers — +// regression coverage for prior data-corruption when the callback dispatches +// through a global pointer to a method on the pointed-to struct. #import "modules/std.sx"; diff --git a/examples/55-err-field-not-found.sx b/examples/55-err-field-not-found.sx new file mode 100644 index 0000000..54d501f --- /dev/null +++ b/examples/55-err-field-not-found.sx @@ -0,0 +1,9 @@ +// Accessing an unknown field on a struct produces a clear +// `error: field 'X' not found on type 'Y'` diagnostic and exit 1. + +Vec :: struct { x: f32; y: f32; } + +main :: () -> s32 { + v := Vec.{ x = 1.0, y = 2.0 }; + return xx v.bogus; +} diff --git a/examples/56-err-tuple-oob.sx b/examples/56-err-tuple-oob.sx new file mode 100644 index 0000000..834d01f --- /dev/null +++ b/examples/56-err-tuple-oob.sx @@ -0,0 +1,7 @@ +// Out-of-range tuple index produces a clear +// `error: field 'N' not found on type 'tuple'` diagnostic and exit 1. + +main :: () -> s32 { + t := (10, 20); + return xx t.42; +} diff --git a/examples/57-err-dot-shorthand.sx b/examples/57-err-dot-shorthand.sx new file mode 100644 index 0000000..ca43f29 --- /dev/null +++ b/examples/57-err-dot-shorthand.sx @@ -0,0 +1,7 @@ +// Dot-shorthand `.Variant(args)` without a tagged-union target type produces +// `error: cannot infer enum type for '.X'` instead of crashing. + +main :: () -> s32 { + x := .Foo(1, 2); + return 0; +} diff --git a/examples/58-infer-return-type.sx b/examples/58-infer-return-type.sx new file mode 100644 index 0000000..70bb404 --- /dev/null +++ b/examples/58-infer-return-type.sx @@ -0,0 +1,16 @@ +// Functions without an explicit return type infer the type from the first +// `return ;` statement in the body, so `foo :: () { return 42; }` is +// usable from a typed context. No explicit-value return → `.void` default. + +foo :: () { return 42; } +bar :: () { return foo() * 2; } +nested :: () { + if true { + return foo() + 10; + } + return 0; +} + +main :: () -> s32 { + return xx (foo() + bar() + nested()); +} diff --git a/examples/59-err-bad-variant.sx b/examples/59-err-bad-variant.sx new file mode 100644 index 0000000..3c96f70 --- /dev/null +++ b/examples/59-err-bad-variant.sx @@ -0,0 +1,22 @@ +// A match arm with a variant name that doesn't exist on the subject's +// enum/tagged-union produces `error: no variant 'X' on type 'Y'` instead of +// falling back to the arm index (which used to cause duplicate switch cases +// and LLVM verification failures). + +#import "modules/std.sx"; + +Shape :: enum { + circle: f32; + rect: struct { w, h: f32; }; + none; +} + +main :: () { + s :Shape = .circle(3.14); + if s == { + case .circle: (r) { print("r={}\n", r); } + case .Bogus: (x) { print("bogus={}\n", x); } + case .none: print("none\n"); + case .rect: print("rect\n"); + } +} diff --git a/examples/60-frameworks.sx b/examples/60-frameworks.sx new file mode 100644 index 0000000..c5d9a9b --- /dev/null +++ b/examples/60-frameworks.sx @@ -0,0 +1,12 @@ +// `#framework "Name"` top-level directive registers an Apple framework for +// linking; `#foreign` declarations can omit the library identifier (frameworks +// resolve symbols by global namespace at link time). + +#framework "CoreFoundation"; + +CFAbsoluteTimeGetCurrent :: () -> f64 #foreign; + +main :: () -> s32 { + t := CFAbsoluteTimeGetCurrent(); + return if t > 0.0 then 0 else 1; +} diff --git a/examples/61-objc-roundtrip.sx b/examples/61-objc-roundtrip.sx new file mode 100644 index 0000000..5959f7b --- /dev/null +++ b/examples/61-objc-roundtrip.sx @@ -0,0 +1,28 @@ +// Obj-C runtime FFI smoke test: round-trip a string through NSString. +// +// Demonstrates the typed-fn-pointer cast idiom for `objc_msgSend`. Each +// shape we invoke gets its own variable typed with the exact ABI: +// +// msg_3 : (*void, *void, [*]u8) -> *void = xx objc_msgSend; +// msg_2 : (*void, *void) -> [*]u8 = xx objc_msgSend; +// +// On ARM64 Apple, objc_msgSend doesn't take a varargs path — invoking it +// through a typed fn-pointer is the only correct way to land args in the +// right registers. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; + +main :: () -> s32 { + ns_class := objc_getClass("NSString".ptr); + sel_with_utf8 := sel_registerName("stringWithUTF8String:".ptr); + sel_utf8 := sel_registerName("UTF8String".ptr); + + msg_3 : (*void, *void, [*]u8) -> *void = xx objc_msgSend; + ns_str := msg_3(ns_class, sel_with_utf8, "hi".ptr); + + msg_2 : (*void, *void) -> [*]u8 = xx objc_msgSend; + back := msg_2(ns_str, sel_utf8); + + return xx (back[0] + back[1]); // 'h' + 'i' = 104 + 105 = 209 +} diff --git a/examples/62-objc-class.sx b/examples/62-objc-class.sx new file mode 100644 index 0000000..153cb77 --- /dev/null +++ b/examples/62-objc-class.sx @@ -0,0 +1,41 @@ +// Register a brand-new Obj-C class from sx and prove a sx-defined method +// actually runs when dispatched through `objc_msgSend`. +// +// The flow: +// 1. `objc_allocateClassPair(NSObject, "SxThing", 0)` returns an unregistered Class. +// 2. `class_addMethod(cls, sel_hello, IMP, "v@:")` attaches our sx function as +// the `hello` method (type encoding "v@:" = void method(id self, SEL _cmd)). +// 3. `objc_registerClassPair(cls)` finalizes it. +// 4. `class_createInstance(cls, 0)` returns an `id` for a fresh instance. +// 5. typed-cast `objc_msgSend` for `void (id, SEL)` and dispatch `hello`. +// If the IMP ran, the global `g_marker` is non-zero and we return it as exit. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; + +g_marker : s32 = 0; + +// IMP for `hello`. Must use C calling convention so `self` and `_cmd` land in +// x0 and x1 the way the Obj-C runtime expects. +hello_imp :: (self: *void, _cmd: *void) callconv(.c) { + g_marker = 42; +} + +main :: () -> s32 { + NSObject := objc_getClass("NSObject".ptr); + SxThing := objc_allocateClassPair(NSObject, "SxThing".ptr, 0); + sel_hello := sel_registerName("hello".ptr); + + ok := class_addMethod(SxThing, sel_hello, xx hello_imp, "v@:".ptr); + if !ok { return 1; } + objc_registerClassPair(SxThing); + + obj := class_createInstance(SxThing, 0); + if obj == xx 0 { return 2; } + + // [obj hello] + msg : (*void, *void) -> void = xx objc_msgSend; + msg(obj, sel_hello); + + return g_marker; // 42 if hello_imp ran +} diff --git a/examples/63-uikit-app.sx b/examples/63-uikit-app.sx new file mode 100644 index 0000000..d800ccf --- /dev/null +++ b/examples/63-uikit-app.sx @@ -0,0 +1,41 @@ +// Minimal iOS app entry point — pure sx, no .m files. +// +// 1. Register a class `SxAppDelegate : UIResponder ` +// dynamically, with one method: +// application:didFinishLaunchingWithOptions: returns YES (BOOL 1). +// 2. Call UIApplicationMain(0, null, null, @"SxAppDelegate") to hand off to +// UIKit's run loop. This blocks until the app exits. +// +// After install + launch, the simulator shows the default black screen +// (UIWindow not created — that's 5.8) and the AppDelegate callback fires +// once at startup. The process stays alive because UIApplicationMain +// drives the iOS run loop. + +#import "modules/std.sx"; +#import "modules/std/uikit.sx"; + +// IMP for application:didFinishLaunchingWithOptions: +// Obj-C: -(BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts +// Type encoding: "c@:@@" -- BOOL (signed char), self, _cmd, id, id +did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) { + NSLog(ns_string("[sx] application:didFinishLaunchingWithOptions: called\n".ptr)); + return 1; // YES +} + +main :: () -> s32 { + // SxAppDelegate : UIResponder. We deliberately don't try + // `class_addProtocol(UIApplicationDelegate)` — the linker dead-strips the + // protocol metadata from UIKit when nothing references it at compile + // time, and the C runtime can't look it up by name. UIApplicationMain + // duck-types on the method name, so this works without formal conformance. + UIResponder := objc_getClass("UIResponder".ptr); + SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0); + + sel := sel_registerName("application:didFinishLaunchingWithOptions:".ptr); + class_addMethod(SxAppDelegate, sel, xx did_finish_launching, "c@:@@".ptr); + + objc_registerClassPair(SxAppDelegate); + + // Hand off to the iOS run loop. Never returns under normal operation. + return UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); +} diff --git a/examples/64-uikit-window.sx b/examples/64-uikit-window.sx new file mode 100644 index 0000000..163db9f --- /dev/null +++ b/examples/64-uikit-window.sx @@ -0,0 +1,94 @@ +// Show something on screen — a UIWindow with a colored UIViewController root. +// +// Builds on 5.7's AppDelegate. Inside `didFinishLaunching`, we: +// 1. [[UIWindow alloc] initWithFrame:CGRect] +// 2. [[UIViewController alloc] init]; set view.backgroundColor +// 3. window.rootViewController = vc +// 4. [window makeKeyAndVisible] +// +// We hardcode the frame to 1024×1024 to avoid the struct-return ABI of +// `[UIScreen mainScreen].bounds` for now — any frame bigger than the device +// covers the screen, modulo safe-area insets. +// +// `g_window` is a module-level global so the window outlives the IMP scope +// (no ARC; UIKit retains it as the key window anyway). + +#import "modules/std.sx"; +#import "modules/std/uikit.sx"; + +g_window : *void = ---; + +// AppDelegate's `window` property. iOS queries this getter to discover the +// app's key window; without it, the legacy code path creates its own empty +// window and ignores anything we configure. +window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) { + return g_window; +} +window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) { + g_window = w; +} + +did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) { + UIWindow := objc_getClass("UIWindow".ptr); + UIViewController := objc_getClass("UIViewController".ptr); + UIColor := objc_getClass("UIColor".ptr); + + sel_alloc := sel_registerName("alloc".ptr); + sel_init := sel_registerName("init".ptr); + sel_init_with_scene := sel_registerName("initWithWindowScene:".ptr); + sel_view := sel_registerName("view".ptr); + sel_system_blue := sel_registerName("systemBlueColor".ptr); + sel_set_bg := sel_registerName("setBackgroundColor:".ptr); + sel_set_root_vc := sel_registerName("setRootViewController:".ptr); + sel_make_key_visible := sel_registerName("makeKeyAndVisible".ptr); + sel_connected_scenes := sel_registerName("connectedScenes".ptr); + sel_any_object := sel_registerName("anyObject".ptr); + + msg_o : (*void, *void) -> *void = xx objc_msgSend; + msg_v : (*void, *void) -> void = xx objc_msgSend; + msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; + msg_ooo : (*void, *void, *void) -> *void = xx objc_msgSend; + + // Modern iOS path: get the connected windowScene, then construct the + // window via initWithWindowScene: so UIKit auto-sizes it and attaches + // it to the display in one step. + scenes := msg_o(app, sel_connected_scenes); + scene := msg_o(scenes, sel_any_object); + if scene == xx 0 { NSLog(ns_string("[sx] scene NULL\n".ptr)); return 1; } + NSLog(ns_string("[sx] scene: ok\n".ptr)); + + win_raw := msg_o(UIWindow, sel_alloc); + g_window = msg_ooo(win_raw, sel_init_with_scene, scene); + NSLog(ns_string("[sx] window: ok\n".ptr)); + + vc_raw := msg_o(UIViewController, sel_alloc); + vc := msg_o(vc_raw, sel_init); + msg_oo(g_window, sel_set_root_vc, vc); + + blue := msg_o(UIColor, sel_system_blue); + view := msg_o(vc, sel_view); + msg_oo(view, sel_set_bg, blue); + msg_oo(g_window, sel_set_bg, blue); + + msg_v(g_window, sel_make_key_visible); + NSLog(ns_string("[sx] makeKeyAndVisible done\n".ptr)); + return 1; +} + +main :: () -> s32 { + UIResponder := objc_getClass("UIResponder".ptr); + SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0); + + class_addMethod(SxAppDelegate, + sel_registerName("application:didFinishLaunchingWithOptions:".ptr), + xx did_finish_launching, "c@:@@".ptr); + class_addMethod(SxAppDelegate, + sel_registerName("window".ptr), + xx window_getter, "@@:".ptr); + class_addMethod(SxAppDelegate, + sel_registerName("setWindow:".ptr), + xx window_setter, "v@:@".ptr); + + objc_registerClassPair(SxAppDelegate); + return UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); +} diff --git a/examples/65-add-framework.sx b/examples/65-add-framework.sx new file mode 100644 index 0000000..fcc8c5d --- /dev/null +++ b/examples/65-add-framework.sx @@ -0,0 +1,20 @@ +// BuildOptions.add_framework registers an Apple framework at comptime, +// equivalent to a top-level `#framework "Name"` directive. The advantage: +// you can gate it with `inline if OS == .ios { ... }` or similar logic, +// keeping the framework off non-Apple builds. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +configure_build :: () { + opts := build_options(); + opts.add_framework("CoreFoundation"); +} +#run configure_build(); + +CFAbsoluteTimeGetCurrent :: () -> f64 #foreign; + +main :: () -> s32 { + t := CFAbsoluteTimeGetCurrent(); + return if t > 0.0 then 0 else 1; +} diff --git a/examples/modules/compiler.sx b/examples/modules/compiler.sx index 5893ac9..7f2f606 100644 --- a/examples/modules/compiler.sx +++ b/examples/modules/compiler.sx @@ -1,4 +1,4 @@ -OperatingSystem :: enum { macos; linux; windows; wasm; unknown; } +OperatingSystem :: enum { macos; linux; windows; wasm; ios; unknown; } Architecture :: enum { aarch64; x86_64; wasm32; wasm64; unknown; } OS : OperatingSystem = .unknown; @@ -7,6 +7,7 @@ POINTER_SIZE : s64 = 8; BuildOptions :: struct { add_link_flag :: (self: BuildOptions, flag: [:0]u8) #compiler; + add_framework :: (self: BuildOptions, name: [:0]u8) #compiler; set_output_path :: (self: BuildOptions, path: [:0]u8) #compiler; set_wasm_shell :: (self: BuildOptions, path: [:0]u8) #compiler; } diff --git a/examples/modules/std/objc.sx b/examples/modules/std/objc.sx new file mode 100644 index 0000000..fcdb020 --- /dev/null +++ b/examples/modules/std/objc.sx @@ -0,0 +1,75 @@ +// Obj-C runtime FFI primitives. +// +// `*void` stands in for the Obj-C `id`/`Class`/`SEL` types. There's no +// sx-level type alias yet, so naming discipline at call sites is the only +// thing keeping them apart. +// +// objc_msgSend has the standard ARM64 calling convention (no varargs path). +// Each call site must invoke through a function pointer of the *exact* +// argument and return shape. The idiom: +// +// msg_fn : (recv: *void, sel: *void, arg: [*]u8) -> *void = xx objc_msgSend; +// result := msg_fn(receiver, selector, c_string); + +// On macOS libobjc is auto-loaded by libSystem; on iOS it isn't, so we +// link it explicitly. Foundation registers NSString etc. with the runtime, +// also auto-loaded on macOS and required as an explicit framework on iOS. +objc :: #library "objc"; +#framework "Foundation"; + +objc_getClass :: (name: [*]u8) -> *void #foreign objc; +objc_lookUpClass :: (name: [*]u8) -> *void #foreign objc; +sel_registerName :: (name: [*]u8) -> *void #foreign objc; +class_createInstance :: (cls: *void, extra: usize) -> *void #foreign objc; +object_getClass :: (obj: *void) -> *void #foreign objc; + +// Declared with the simplest non-variadic shape. Cast per call site. +objc_msgSend :: (recv: *void, sel: *void) -> *void #foreign objc; + +// ─── Dynamic class registration ───────────────────────────────────────── +// Define a new Obj-C class at runtime: allocate the pair, attach methods + +// protocols, then finalize with `objc_registerClassPair`. The class is then +// usable via `class_createInstance` and Obj-C dispatch. +// +// IMPs (method implementations) are function pointers with the implicit +// Obj-C method shape: `(self: *void, _cmd: *void, ...args) -> ret` with +// `callconv(.c)` so they land args in the standard C registers. +// +// Method type encoding strings follow Apple's runtime DSL: +// v = void c = char/BOOL i = int l = long f = float d = double +// @ = id (object) : = SEL # = Class +// Return type comes first, then receiver (`@`), then `_cmd` (`:`), then args. +// Examples: +// "v@:" -> void method(id, SEL) +// "c@:" -> BOOL method(id, SEL) +// "@@:@" -> id method(id, SEL, id) +// "B@:@@" -> BOOL method(id, SEL, id, id) +objc_allocateClassPair :: (super: *void, name: [*]u8, extra: usize) -> *void #foreign objc; +class_addMethod :: (cls: *void, sel: *void, imp: *void, types: [*]u8) -> bool #foreign objc; +class_addProtocol :: (cls: *void, proto: *void) -> bool #foreign objc; +objc_getProtocol :: (name: [*]u8) -> *void #foreign objc; +objc_registerClassPair :: (cls: *void) -> void #foreign objc; + +// Foundation C-API helpers (Foundation is already linked above via #framework). +// NSLog takes an NSString format; the variadic tail is not exposed here. +NSLog :: (fmt: *void) #foreign; + +// ─── Convenience helpers ──────────────────────────────────────────────── +// These hide the typed-fn-pointer cast for the most common shapes. They +// re-register selectors per call — if you're in a tight loop, cache the SEL. + +// Wrap a C string in an autoreleased NSString. +ns_string :: (s: [*]u8) -> *void { + cls := objc_getClass("NSString".ptr); + sel := sel_registerName("stringWithUTF8String:".ptr); + fn_ptr : (*void, *void, [*]u8) -> *void = xx objc_msgSend; + return fn_ptr(cls, sel, s); +} + +// View an NSString's bytes as a C string. The returned pointer's lifetime is +// tied to the NSString; don't free it. +c_string :: (ns: *void) -> [*]u8 { + sel := sel_registerName("UTF8String".ptr); + fn_ptr : (*void, *void) -> [*]u8 = xx objc_msgSend; + return fn_ptr(ns, sel); +} diff --git a/examples/modules/std/uikit.sx b/examples/modules/std/uikit.sx new file mode 100644 index 0000000..25a1041 --- /dev/null +++ b/examples/modules/std/uikit.sx @@ -0,0 +1,17 @@ +// UIKit framework bindings — iOS only. +// +// Consumers `#import "modules/std/uikit.sx";` and inherit the +// `#framework "UIKit"` link directive plus any C-API declarations exposed +// here. Obj-C class/method dispatch goes through `modules/std/objc.sx` +// which we re-import so users only need this one file. + +#import "objc.sx"; + +#framework "UIKit"; + +// int UIApplicationMain(int argc, char *_Nullable argv[_Nonnull], +// NSString *_Nullable principalClassName, +// NSString *_Nullable delegateClassName); +// +// Blocks. Drives the iOS run loop. Normally never returns. +UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign; diff --git a/src/ast.zig b/src/ast.zig index 2fdc4dc..596a61a 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -71,6 +71,7 @@ pub const Node = struct { compiler_expr: void, foreign_expr: ForeignExpr, library_decl: LibraryDecl, + framework_decl: FrameworkDecl, function_type_expr: FunctionTypeExpr, closure_type_expr: ClosureTypeExpr, tuple_type_expr: TupleTypeExpr, @@ -448,6 +449,10 @@ pub const LibraryDecl = struct { name: []const u8, // sx-side constant name }; +pub const FrameworkDecl = struct { + name: []const u8, // framework name, e.g. "Foundation" +}; + pub const FunctionTypeExpr = struct { param_types: []const *Node, param_names: ?[]const ?[]const u8 = null, // optional documentation names diff --git a/src/core.zig b/src/core.zig index 841a24d..7171430 100644 --- a/src/core.zig +++ b/src/core.zig @@ -121,6 +121,12 @@ pub const Compilation = struct { return &.{}; } + /// Get frameworks accumulated from #run build blocks (BuildOptions.add_framework). + pub fn getBuildFrameworks(self: *Compilation) []const []const u8 { + if (self.ir_emitter) |*e| return e.build_config.frameworks.items; + return &.{}; + } + /// Get output path set from #run build blocks, if any. pub fn getBuildOutputPath(self: *Compilation) ?[]const u8 { if (self.ir_emitter) |*e| return e.build_config.output_path; diff --git a/src/ir/compiler_hooks.zig b/src/ir/compiler_hooks.zig index eed66ff..4b63ff6 100644 --- a/src/ir/compiler_hooks.zig +++ b/src/ir/compiler_hooks.zig @@ -9,11 +9,13 @@ const Interpreter = interp_mod.Interpreter; pub const BuildConfig = struct { link_flags: std.ArrayList([]const u8) = .empty, + frameworks: std.ArrayList([]const u8) = .empty, output_path: ?[]const u8 = null, wasm_shell_path: ?[]const u8 = null, pub fn deinit(self: *BuildConfig, alloc: Allocator) void { self.link_flags.deinit(alloc); + self.frameworks.deinit(alloc); } }; @@ -52,6 +54,7 @@ pub const Registry = struct { pub fn registerDefaults(self: *Registry) void { self.hooks.put("build_options", &hookBuildOptions) catch {}; self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {}; + self.hooks.put("BuildOptions.add_framework", &hookAddFramework) catch {}; self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {}; self.hooks.put("BuildOptions.set_wasm_shell", &hookSetWasmShell) catch {}; } @@ -87,6 +90,21 @@ fn hookAddLinkFlag( return .void_val; } +fn hookAddFramework( + interp: *const Interpreter, + args: []const Value, + bc: *BuildConfig, + alloc: Allocator, +) HookError!Value { + // args: [self (BuildOptions value), framework_name] + if (args.len < 2) return .void_val; + const str_val = args[1]; + if (str_val.asString(interp)) |s| { + bc.frameworks.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime; + } + return .void_val; +} + fn hookSetOutputPath( interp: *const Interpreter, args: []const Value, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 86b9151..5405120 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -201,6 +201,8 @@ pub const Lowering = struct { self.findVariantIndex(os_info.@"enum".variants, "windows") else if (tc.isLinux()) self.findVariantIndex(os_info.@"enum".variants, "linux") + else if (tc.isIOS()) + self.findVariantIndex(os_info.@"enum".variants, "ios") else if (tc.isMacOS()) self.findVariantIndex(os_info.@"enum".variants, "macos") else @@ -1631,7 +1633,7 @@ pub const Lowering = struct { .break_expr => self.lowerBreak(), .continue_expr => self.lowerContinue(), .call => |c| self.lowerCall(&c), - .field_access => |fa| self.lowerFieldAccess(&fa), + .field_access => |fa| self.lowerFieldAccess(&fa, node.span), .struct_literal => |sl| self.lowerStructLiteral(&sl), .array_literal => |al| self.lowerArrayLiteral(&al), .index_expr => |ie| self.lowerIndexExpr(&ie), @@ -2486,6 +2488,10 @@ pub const Lowering = struct { break :blk @intCast(vi); } } + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(subject_ty); + diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); + } } else if (ty_info == .@"enum") { for (ty_info.@"enum".variants, 0..) |v, vi| { const vname = self.module.types.strings.get(v); @@ -2496,6 +2502,10 @@ pub const Lowering = struct { break :blk @intCast(vi); } } + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(subject_ty); + diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); + } } } break :blk @intCast(i); @@ -3017,7 +3027,7 @@ pub const Lowering = struct { return .s64; } - fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess) Ref { + fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { // Check for struct constant access: Struct.CONST if (fa.object.data == .identifier) { const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; @@ -3061,10 +3071,10 @@ pub const Lowering = struct { // Optional chaining: p?.field if (fa.is_optional) { - return self.lowerOptionalChain(obj, fa); + return self.lowerOptionalChain(obj, fa, span); } - return self.lowerFieldAccessOnType(obj, obj_ty, fa.field); + return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); } /// Lower a struct-level constant value (e.g., Phys.GRAVITY). @@ -3082,7 +3092,7 @@ pub const Lowering = struct { /// Lower optional chaining: `p?.field` where p is ?T /// Produces ?FieldType: some(unwrap(p).field) if p has value, else null /// If FieldType is already optional (?U), flattens to ?U (no double wrapping) - fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess) Ref { + fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { const obj_ty = self.inferExprType(fa.object); // Get the inner (non-optional) type const inner_ty = if (!obj_ty.isBuiltin()) blk: { @@ -3109,7 +3119,7 @@ pub const Lowering = struct { // Some: unwrap, access field (already ?FieldType if flattened, else wrap) self.builder.switchToBlock(some_bb); const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); - const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field); + const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); self.builder.br(merge_bb, &.{some_result}); @@ -3124,7 +3134,7 @@ pub const Lowering = struct { } /// Field access on a known type (shared by regular field access and optional chaining) - fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8) Ref { + fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { const field_name_id = self.module.types.internString(field); // Check if it's a union type @@ -3221,28 +3231,24 @@ pub const Lowering = struct { } // Try numeric index (e.g., "0", "1") const idx = std.fmt.parseInt(u32, field, 10) catch { - return self.builder.structGet(obj, 0, if (tuple.fields.len > 0) tuple.fields[0] else .s64); + return self.emitFieldError(obj_ty, field, span); }; if (idx < tuple.fields.len) { return self.builder.structGet(obj, idx, tuple.fields[idx]); } - return self.builder.structGet(obj, 0, if (tuple.fields.len > 0) tuple.fields[0] else .s64); + return self.emitFieldError(obj_ty, field, span); } } // Resolve struct field index and type const struct_fields = self.getStructFields(obj_ty); - var field_idx: u32 = 0; - var field_ty: TypeId = .s64; for (struct_fields, 0..) |f, i| { if (f.name == field_name_id) { - field_idx = @intCast(i); - field_ty = f.ty; - break; + return self.builder.structGet(obj, @intCast(i), f.ty); } } - return self.builder.structGet(obj, field_idx, field_ty); + return self.emitFieldError(obj_ty, field, span); } fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { @@ -4210,34 +4216,44 @@ pub const Lowering = struct { return self.emitError(fa.field, c.callee.span); }, .enum_literal => |el| { - const target = self.target_type orelse .s64; + const target_opt: ?TypeId = self.target_type; - // Check if target type is a struct — dispatch as static method call - if (!target.isBuiltin()) { - const target_info = self.module.types.get(target); - if (target_info == .@"struct") { - // Try to resolve StructName.method - const struct_name = self.module.types.typeName(target); - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name; - if (self.fn_ast_map.get(qualified)) |fd| { - if (fd.type_params.len > 0) { - return self.lowerGenericCall(fd, qualified, c, args.items); + // Try struct-method dispatch first: .{...}.method() where target is a struct + if (target_opt) |tgt| { + if (!tgt.isBuiltin()) { + const target_info = self.module.types.get(tgt); + if (target_info == .@"struct") { + const struct_name = self.module.types.typeName(tgt); + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name; + if (self.fn_ast_map.get(qualified)) |fd| { + if (fd.type_params.len > 0) { + return self.lowerGenericCall(fd, qualified, c, args.items); + } + if (!self.lowered_functions.contains(qualified)) { + self.lazyLowerFunction(qualified); + } } - if (!self.lowered_functions.contains(qualified)) { - self.lazyLowerFunction(qualified); + if (self.resolveFuncByName(qualified)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + self.coerceCallArgs(args.items, params); + return self.builder.call(fid, args.items, ret_ty); } } - if (self.resolveFuncByName(qualified)) |fid| { - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - self.coerceCallArgs(args.items, params); - return self.builder.call(fid, args.items, ret_ty); - } } } - // .Variant(payload) — tagged enum construction + // .Variant(payload) — tagged enum construction. Requires target to be a tagged union. + const target = blk: { + if (target_opt) |tgt| { + if (!tgt.isBuiltin() and self.module.types.get(tgt) == .tagged_union) break :blk tgt; + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, c.callee.span, "cannot infer enum type for '.{s}' \u{2014} use an explicit type or assign to a typed variable", .{el.name}); + } + return self.emitPlaceholder(el.name); + }; const tag = self.resolveVariantIndex(target, el.name); var payload = if (args.items.len > 0) args.items[0] else Ref.none; // Coerce payload to match the field type @@ -6723,9 +6739,44 @@ pub const Lowering = struct { if (fd.is_arrow) { return self.inferExprType(fd.body); } + // No annotation, not arrow: an explicit `return ` statement + // wins. Otherwise default to void — the body's tail expression is + // a side-effect statement, not an implicit return. + if (self.findReturnValueType(fd.body)) |ty| return ty; return .void; } + /// Walk a function body and return the type of the first `return ;` + /// statement encountered. Does not descend into nested function or lambda + /// declarations (those have their own return types). + fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId { + return switch (node.data) { + .return_stmt => |rs| if (rs.value) |v| self.inferExprType(v) else null, + .block => |blk| blk: { + for (blk.stmts) |s| { + if (self.findReturnValueType(s)) |t| break :blk t; + } + break :blk null; + }, + .if_expr => |ie| blk: { + if (self.findReturnValueType(ie.then_branch)) |t| break :blk t; + if (ie.else_branch) |eb| { + if (self.findReturnValueType(eb)) |t| break :blk t; + } + break :blk null; + }, + .while_expr => |we| self.findReturnValueType(we.body), + .for_expr => |fe| self.findReturnValueType(fe.body), + .match_expr => |me| blk: { + for (me.arms) |arm| { + if (self.findReturnValueType(arm.body)) |t| break :blk t; + } + break :blk null; + }, + else => null, + }; + } + fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { const elem_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { @@ -8420,6 +8471,9 @@ pub const Lowering = struct { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); return switch (info) { + // Arbitrary-width integer types (u1, u2, s4, ...) interned as + // `.signed`/`.unsigned` variants — fall through `isBuiltin()`. + .signed, .unsigned => self.builder.constInt(0, ty), .pointer, .tuple, .optional => self.builder.constNull(ty), .@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty), else => self.builder.constUndef(ty), @@ -8523,6 +8577,14 @@ pub const Lowering = struct { return self.emitPlaceholder(name); } + fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(obj_ty); + diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name }); + } + return self.emitPlaceholder(field); + } + /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { diff --git a/src/ir/types.zig b/src/ir/types.zig index 586827f..8b64005 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -407,6 +407,15 @@ pub const TypeTable = struct { /// Compute the ABI size in bytes for a type, matching LLVM's struct layout rules. /// This is the authoritative size computation used for closure env sizing and /// verified against LLVMABISizeOfType. + fn intAbiBytes(w: u16) usize { + // LLVM ABI size for iN: round w up to the next power of 2, then /8. + // Sub-byte widths (i1, i2, ..., i7) are 1 byte. + if (w <= 8) return 1; + if (w <= 16) return 2; + if (w <= 32) return 4; + return 8; + } + pub fn typeSizeBytes(self: *const TypeTable, ty: TypeId) usize { const ptr_size: usize = self.pointer_size; if (ty == .void) return 0; @@ -494,6 +503,10 @@ pub const TypeTable = struct { if (e.backing_type) |bt| return self.typeSizeBytes(bt); return 8; }, + // LLVM rounds arbitrary-width integers up to the next power-of-2 + // width before computing ABI size (i12 → 2 bytes, i24 → 4 bytes). + .signed => |w| intAbiBytes(w), + .unsigned => |w| intAbiBytes(w), else => 8, }; } @@ -544,6 +557,8 @@ pub const TypeTable = struct { } break :blk max_a; }, + .signed => |w| intAbiBytes(w), + .unsigned => |w| intAbiBytes(w), else => 8, }; } @@ -650,6 +665,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void { .protocol => |p| h.update(std.mem.asBytes(&p.name)), .tuple => |t| { for (t.fields) |f| h.update(std.mem.asBytes(&f)); + if (t.names) |ns| for (ns) |n| h.update(std.mem.asBytes(&n)); }, } } @@ -697,6 +713,12 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool { for (t.fields, u.fields) |tf, uf| { if (tf != uf) return false; } + if ((t.names == null) != (u.names == null)) return false; + if (t.names) |tn| { + const un = u.names.?; + if (tn.len != un.len) return false; + for (tn, un) |tna, una| if (tna != una) return false; + } return true; }, }; diff --git a/src/lexer.zig b/src/lexer.zig index c2133c8..c1d178c 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -72,6 +72,7 @@ pub const Lexer = struct { .{ "#compiler", Tag.hash_compiler }, .{ "#foreign", Tag.hash_foreign }, .{ "#library", Tag.hash_library }, + .{ "#framework", Tag.hash_framework }, .{ "#using", Tag.hash_using }, .{ "#include", Tag.hash_include }, .{ "#source", Tag.hash_source }, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index e45c78c..fef5178 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1486,6 +1486,7 @@ pub const Server = struct { .hash_compiler, .hash_foreign, .hash_library, + .hash_framework, .hash_using, .hash_include, .hash_source, diff --git a/src/main.zig b/src/main.zig index 6fed0a6..40e7be5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 triple or shorthand: wasm, macos, linux, windows (default: host) + \\ --target Target triple or shorthand: wasm, macos, linux, windows, ios, ios-sim (default: host) \\ --cpu CPU name (default: generic) \\ --opt Optimization: none/0, less/1, default/2, aggressive/3 \\ -o Output path @@ -277,6 +303,11 @@ fn printUsage() void { \\ --linker Linker command (default: cc) \\ --sysroot Sysroot for cross-compilation \\ --lflags Extra linker flag (repeatable, e.g. --lflags -sUSE_SDL=2) + \\ --bundle Wrap the binary in an iOS/macOS .app bundle (after linking) + \\ --bundle-id CFBundleIdentifier (required with --bundle) + \\ --codesign-identity Codesigning identity (e.g. "Apple Development: ...") + \\ --provisioning-profile .mobileprovision to embed (required for device) + \\ --entitlements 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; diff --git a/src/parser.zig b/src/parser.zig index 654a4fe..a52440c 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -77,6 +77,19 @@ pub const Parser = struct { return try self.createNode(start, .{ .comptime_expr = .{ .expr = expr } }); } + // Top-level #framework directive: link against an Apple framework. + if (self.current.tag == .hash_framework) { + self.advance(); + if (self.current.tag != .string_literal) { + return self.fail("expected string after '#framework'"); + } + const raw = self.tokenSlice(self.current); + const fw_name = raw[1 .. raw.len - 1]; + self.advance(); + try self.expect(.semicolon); + return try self.createNode(start, .{ .framework_decl = .{ .name = fw_name } }); + } + // impl Protocol for Type { methods } if (self.current.tag == .kw_impl) { return self.parseImplBlock(start); @@ -240,15 +253,17 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = bi } }); } - // name :: type_expr #foreign lib ["c_name"]; — foreign with type annotation + // name :: type_expr #foreign [lib] ["c_name"]; — foreign with type annotation if (self.current.tag == .hash_foreign) { const fi_start = self.current.loc.start; self.advance(); - // Required: library reference (identifier) - if (self.current.tag != .identifier) - return self.fail("expected library name after '#foreign'"); - const lib_ref = self.tokenSlice(self.current); - self.advance(); + // Optional: library reference (identifier). Omitted when the symbol + // resolves at link time from a framework or auto-detected library. + var lib_ref: ?[]const u8 = null; + if (self.current.tag == .identifier) { + lib_ref = self.tokenSlice(self.current); + self.advance(); + } // Optional: C symbol name (string literal) var c_name: ?[]const u8 = null; if (self.current.tag == .string_literal) { @@ -1254,10 +1269,12 @@ pub const Parser = struct { const fi_start = self.current.loc.start; self.advance(); // Required: library reference (identifier) - if (self.current.tag != .identifier) - return self.fail("expected library name after '#foreign'"); - const lib_ref = self.tokenSlice(self.current); - self.advance(); + // Optional: library reference (identifier). + var lib_ref: ?[]const u8 = null; + if (self.current.tag == .identifier) { + lib_ref = self.tokenSlice(self.current); + self.advance(); + } // Optional: C symbol name (string literal) var c_name: ?[]const u8 = null; if (self.current.tag == .string_literal) { diff --git a/src/sema.zig b/src/sema.zig index 310cfd5..6182eb9 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -868,6 +868,7 @@ pub const Analyzer = struct { .compiler_expr, .foreign_expr, .library_decl, + .framework_decl, .function_type_expr, .closure_type_expr, .import_decl, @@ -1274,6 +1275,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .compiler_expr, .foreign_expr, .library_decl, + .framework_decl, .function_type_expr, .enum_decl, .union_decl, diff --git a/src/target.zig b/src/target.zig index 253386c..9775afd 100644 --- a/src/target.zig +++ b/src/target.zig @@ -23,6 +23,23 @@ pub const TargetConfig = struct { extra_link_flags: []const []const u8 = &.{}, /// Custom WASM shell template path (overrides the built-in template). wasm_shell_path: ?[]const u8 = null, + /// Path to a `.app` bundle directory to produce (iOS/macOS). When set, the + /// linker output is moved into the bundle alongside a generated Info.plist + /// and ad-hoc signed for simulator runs. + bundle_path: ?[]const u8 = null, + /// CFBundleIdentifier for the bundle (e.g. "co.swipelab.sxhello"). + /// Required when `bundle_path` is set. + bundle_id: ?[]const u8 = null, + /// Codesigning identity (e.g. `"Apple Development: Alex (TEAMID)"` or a + /// SHA-1 fingerprint from `security find-identity -p codesigning`). + /// When null, ad-hoc signs with `-` (sufficient for simulator, not device). + codesign_identity: ?[]const u8 = null, + /// Path to a `.mobileprovision` to embed as `embedded.mobileprovision`. + /// Required for real-device builds. + provisioning_profile: ?[]const u8 = null, + /// Path to an entitlements plist. When null and `provisioning_profile` + /// is set, the entitlements are auto-extracted from the profile. + entitlements_path: ?[]const u8 = null, pub const OptLevel = enum { none, @@ -70,11 +87,27 @@ pub const TargetConfig = struct { return self.tripleHasPrefix("wasm64", "wasm64"); } - /// Check if target triple indicates macOS/Darwin. + /// Check if target triple indicates macOS/Darwin (does not match iOS). pub fn isMacOS(self: TargetConfig) bool { + if (self.isIOS()) return false; return self.tripleContains("darwin") or self.tripleContains("macos"); } + /// Check if target triple indicates iOS (device or simulator). + pub fn isIOS(self: TargetConfig) bool { + return self.tripleContains("-apple-ios"); + } + + /// Check if target triple indicates the iOS Simulator. + pub fn isIOSSimulator(self: TargetConfig) bool { + return self.isIOS() and self.tripleContains("simulator"); + } + + /// Check if target triple indicates a real iOS device (not Simulator). + pub fn isIOSDevice(self: TargetConfig) bool { + return self.isIOS() and !self.tripleContains("simulator"); + } + /// Check if target triple indicates Linux. pub fn isLinux(self: TargetConfig) bool { return self.tripleContains("linux"); @@ -170,10 +203,70 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 { return if (result >= 0 and result <= 255) @intCast(result) else 1; } -pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void { +/// Run `xcrun --sdk --show-sdk-path` and return the trimmed path. +/// Caller owns the returned slice. +pub fn discoverAppleSdk(allocator: std.mem.Allocator, io: std.Io, sdk_name: []const u8) ![]const u8 { + const r = std.process.run(allocator, io, .{ + .argv = &.{ "xcrun", "--sdk", sdk_name, "--show-sdk-path" }, + }) catch |e| { + std.debug.print("error: failed to run xcrun: {} \u{2014} install Xcode Command Line Tools (xcode-select --install)\n", .{e}); + return error.SdkNotFound; + }; + defer allocator.free(r.stderr); + errdefer allocator.free(r.stdout); + if (r.term != .exited or r.term.exited != 0) { + std.debug.print("error: xcrun --sdk {s} --show-sdk-path failed\n", .{sdk_name}); + allocator.free(r.stdout); + return error.SdkNotFound; + } + const trimmed = std.mem.trimEnd(u8, r.stdout, " \t\r\n"); + const out = try allocator.dupe(u8, trimmed); + allocator.free(r.stdout); + return out; +} + +pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, frameworks: []const []const u8, target_config: TargetConfig) !void { var argv = std.ArrayList([]const u8).empty; - if (target_config.isEmscripten()) { + if (target_config.isIOS()) { + // iOS: clang driver with -isysroot pointing at the iOS SDK. + // -l libraries are generally wrong for iOS (Apple ships system code + // as frameworks); user-declared #library still pass through. + const linker = target_config.linker orelse "clang"; + try argv.append(allocator, linker); + if (target_config.triple) |t| { + try argv.append(allocator, "-target"); + try argv.append(allocator, std.mem.span(t)); + } + const sdk_path = if (target_config.sysroot) |sr| + try allocator.dupe(u8, sr) + else blk: { + const sdk_name: []const u8 = if (target_config.isIOSSimulator()) "iphonesimulator" else "iphoneos"; + break :blk try discoverAppleSdk(allocator, io, sdk_name); + }; + try argv.append(allocator, "-isysroot"); + try argv.append(allocator, sdk_path); + const min_flag: []const u8 = if (target_config.isIOSSimulator()) "-mios-simulator-version-min=14.0" else "-mios-version-min=14.0"; + try argv.append(allocator, min_flag); + try argv.append(allocator, output_obj); + try argv.append(allocator, "-o"); + try argv.append(allocator, output_bin); + for (extra_objects) |eo| try argv.append(allocator, eo); + for (target_config.lib_paths) |lp| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp})); + } + for (libraries) |lib| { + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); + } + for (frameworks) |fw| { + try argv.append(allocator, "-framework"); + try argv.append(allocator, fw); + } + for (target_config.extra_link_flags) |flag| { + var it = std.mem.tokenizeScalar(u8, flag, ' '); + while (it.next()) |part| try argv.append(allocator, part); + } + } else if (target_config.isEmscripten()) { // Emscripten: use emcc as the linker/driver const linker = target_config.linker orelse "emcc"; try argv.appendSlice(allocator, &.{ linker, output_obj, "-o", output_bin }); @@ -252,6 +345,14 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib})); } + // Frameworks: only meaningful on Apple targets; silently ignored elsewhere. + if (target_config.isMacOS()) { + for (frameworks) |fw| { + try argv.append(allocator, "-framework"); + try argv.append(allocator, fw); + } + } + // Extra linker flags — split space-separated flags into individual argv entries. for (target_config.extra_link_flags) |flag| { var it = std.mem.tokenizeScalar(u8, flag, ' '); @@ -270,6 +371,184 @@ pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, ex if (result.exited != 0) return error.LinkError; } +/// Move `binary_path` into a freshly-created `` directory, +/// write a minimal Info.plist, and ad-hoc codesign for simulator runs. +/// The executable inside the bundle is named after the basename of +/// `binary_path` (also used as CFBundleExecutable). +pub fn createBundle(allocator: std.mem.Allocator, io: std.Io, binary_path: []const u8, target_config: TargetConfig) !void { + const bundle_path = target_config.bundle_path orelse return error.NoBundlePath; + const bundle_id = target_config.bundle_id orelse { + std.debug.print("error: --bundle requires --bundle-id (e.g. co.swipelab.app)\n", .{}); + return error.MissingBundleId; + }; + + // Device builds without a real identity will be rejected by the device, + // so fail fast with a clear hint. + if (target_config.isIOSDevice() and target_config.codesign_identity == null) { + std.debug.print("error: --target ios requires --codesign-identity (e.g. \"Apple Development: ...\") and --provisioning-profile \n", .{}); + return error.MissingCodesignIdentity; + } + + const cwd = std.Io.Dir.cwd(); + cwd.deleteTree(io, bundle_path) catch {}; + try cwd.createDirPath(io, bundle_path); + + const exe_name = std.fs.path.basename(binary_path); + const exe_dest = try std.fs.path.join(allocator, &.{ bundle_path, exe_name }); + cwd.rename(binary_path, cwd, exe_dest, io) catch return error.BundleMoveFailed; + + // Info.plist + const plist = try buildInfoPlist(allocator, exe_name, bundle_id, target_config); + const plist_path = try std.fs.path.join(allocator, &.{ bundle_path, "Info.plist" }); + try cwd.writeFile(io, .{ .sub_path = plist_path, .data = plist }); + + // Embed provisioning profile if supplied. Required for device installs. + if (target_config.provisioning_profile) |pp| { + const profile_data = std.Io.Dir.readFileAlloc(.cwd(), io, pp, allocator, .limited(1 * 1024 * 1024)) catch { + std.debug.print("error: cannot read provisioning profile: {s}\n", .{pp}); + return error.ProvisioningProfileNotFound; + }; + const embedded_path = try std.fs.path.join(allocator, &.{ bundle_path, "embedded.mobileprovision" }); + try cwd.writeFile(io, .{ .sub_path = embedded_path, .data = profile_data }); + } + + // Codesign: real identity for device, ad-hoc otherwise. + const identity: []const u8 = target_config.codesign_identity orelse "-"; + const ent_path: ?[]const u8 = if (target_config.entitlements_path) |e| e else blk: { + if (target_config.provisioning_profile) |pp| { + break :blk try extractEntitlements(allocator, io, pp, bundle_id); + } + break :blk null; + }; + try codesign(allocator, io, bundle_path, identity, ent_path); +} + +/// Extract entitlements XML from a `.mobileprovision` and resolve the +/// `application-identifier` wildcard (`.*`) to the concrete bundle ID +/// (`.`). Without this substitution the device installer +/// rejects the app with `MIInstallerErrorDomain error 13` / +/// `0xe8008015 (A valid provisioning profile ... was not found)`. +/// Writes the resolved entitlements to `.sx-tmp/entitlements.plist`. +fn extractEntitlements(allocator: std.mem.Allocator, io: std.Io, profile_path: []const u8, bundle_id: []const u8) ![]const u8 { + const cwd = std.Io.Dir.cwd(); + cwd.createDirPath(io, ".sx-tmp") catch {}; + + const profile_plist_path = ".sx-tmp/profile.plist"; + const ent_path = ".sx-tmp/entitlements.plist"; + + // 1. security cms -D -i -o profile.plist (decode CMS to plist) + const r1 = std.process.run(allocator, io, .{ + .argv = &.{ "security", "cms", "-D", "-i", profile_path, "-o", profile_plist_path }, + }) catch return error.SecurityCommandFailed; + defer allocator.free(r1.stdout); + defer allocator.free(r1.stderr); + if (r1.term != .exited or r1.term.exited != 0) { + std.debug.print("error: failed to decode provisioning profile: {s}\n", .{r1.stderr}); + return error.SecurityCommandFailed; + } + + // 2. plutil -extract Entitlements xml1 -o entitlements.plist profile.plist + const r2 = std.process.run(allocator, io, .{ + .argv = &.{ "plutil", "-extract", "Entitlements", "xml1", "-o", ent_path, profile_plist_path }, + }) catch return error.PlutilCommandFailed; + defer allocator.free(r2.stdout); + defer allocator.free(r2.stderr); + if (r2.term != .exited or r2.term.exited != 0) { + std.debug.print("error: failed to extract entitlements: {s}\n", .{r2.stderr}); + return error.PlutilCommandFailed; + } + + // 3. Read the team identifier so we can resolve the wildcard. The profile + // stores it as `ApplicationIdentifierPrefix.0` (an array). We use that + // path because `com.apple.developer.team-identifier` would confuse + // plutil — dots in plutil paths are interpreted as path separators. + const r3 = std.process.run(allocator, io, .{ + .argv = &.{ "plutil", "-extract", "ApplicationIdentifierPrefix.0", "raw", "-o", "-", profile_plist_path }, + }) catch return error.PlutilCommandFailed; + defer allocator.free(r3.stdout); + defer allocator.free(r3.stderr); + if (r3.term != .exited or r3.term.exited != 0) { + std.debug.print("error: profile missing ApplicationIdentifierPrefix: {s}\n", .{r3.stderr}); + return error.PlutilCommandFailed; + } + const team = std.mem.trimEnd(u8, r3.stdout, " \t\r\n"); + const resolved_app_id = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ team, bundle_id }); + defer allocator.free(resolved_app_id); + + // 4. plutil -replace application-identifier -string "." entitlements.plist + const r4 = std.process.run(allocator, io, .{ + .argv = &.{ "plutil", "-replace", "application-identifier", "-string", resolved_app_id, ent_path }, + }) catch return error.PlutilCommandFailed; + defer allocator.free(r4.stdout); + defer allocator.free(r4.stderr); + if (r4.term != .exited or r4.term.exited != 0) { + std.debug.print("error: failed to resolve application-identifier: {s}\n", .{r4.stderr}); + return error.PlutilCommandFailed; + } + + return try allocator.dupe(u8, ent_path); +} + +fn buildInfoPlist(allocator: std.mem.Allocator, exe_name: []const u8, bundle_id: []const u8, target_config: TargetConfig) ![]const u8 { + const min_os: []const u8 = "14.0"; + const is_sim = target_config.isIOSSimulator(); + const platform_key: []const u8 = if (is_sim) "iPhoneSimulator" else "iPhoneOS"; + return std.fmt.allocPrint(allocator, + \\ + \\ + \\ + \\ + \\ CFBundleIdentifier + \\ {s} + \\ CFBundleName + \\ {s} + \\ CFBundleExecutable + \\ {s} + \\ CFBundlePackageType + \\ APPL + \\ CFBundleVersion + \\ 1 + \\ CFBundleShortVersionString + \\ 0.1 + \\ MinimumOSVersion + \\ {s} + \\ UIDeviceFamily + \\ + \\ 1 + \\ + \\ LSRequiresIPhoneOS + \\ + \\ UILaunchScreen + \\ + \\ DTPlatformName + \\ {s} + \\ + \\ + \\ + , .{ bundle_id, exe_name, exe_name, min_os, platform_key }); +} + +fn codesign(allocator: std.mem.Allocator, io: std.Io, bundle_path: []const u8, identity: []const u8, entitlements: ?[]const u8) !void { + var argv = std.ArrayList([]const u8).empty; + defer argv.deinit(allocator); + try argv.appendSlice(allocator, &.{ "codesign", "--force", "--sign", identity, "--timestamp=none" }); + if (entitlements) |ep| { + try argv.appendSlice(allocator, &.{ "--entitlements", ep }); + } + try argv.append(allocator, bundle_path); + + const r = std.process.run(allocator, io, .{ .argv = argv.items }) catch |e| { + std.debug.print("error: failed to run codesign: {}\n", .{e}); + return error.CodesignFailed; + }; + defer allocator.free(r.stdout); + defer allocator.free(r.stderr); + if (r.term != .exited or r.term.exited != 0) { + std.debug.print("codesign failed: {s}\n", .{r.stderr}); + return error.CodesignFailed; + } +} + /// After emcc produces HTML output, inject cache-busting hashes into the /// generated