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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,5 @@ zig-out
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.sx-cache
|
||||
.sx-tmp
|
||||
current/
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
9
examples/55-err-field-not-found.sx
Normal file
9
examples/55-err-field-not-found.sx
Normal file
@@ -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;
|
||||
}
|
||||
7
examples/56-err-tuple-oob.sx
Normal file
7
examples/56-err-tuple-oob.sx
Normal file
@@ -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;
|
||||
}
|
||||
7
examples/57-err-dot-shorthand.sx
Normal file
7
examples/57-err-dot-shorthand.sx
Normal file
@@ -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;
|
||||
}
|
||||
16
examples/58-infer-return-type.sx
Normal file
16
examples/58-infer-return-type.sx
Normal file
@@ -0,0 +1,16 @@
|
||||
// Functions without an explicit return type infer the type from the first
|
||||
// `return <value>;` 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());
|
||||
}
|
||||
22
examples/59-err-bad-variant.sx
Normal file
22
examples/59-err-bad-variant.sx
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
12
examples/60-frameworks.sx
Normal file
12
examples/60-frameworks.sx
Normal file
@@ -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;
|
||||
}
|
||||
28
examples/61-objc-roundtrip.sx
Normal file
28
examples/61-objc-roundtrip.sx
Normal file
@@ -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
|
||||
}
|
||||
41
examples/62-objc-class.sx
Normal file
41
examples/62-objc-class.sx
Normal file
@@ -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
|
||||
}
|
||||
41
examples/63-uikit-app.sx
Normal file
41
examples/63-uikit-app.sx
Normal file
@@ -0,0 +1,41 @@
|
||||
// Minimal iOS app entry point — pure sx, no .m files.
|
||||
//
|
||||
// 1. Register a class `SxAppDelegate : UIResponder <UIApplicationDelegate>`
|
||||
// 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));
|
||||
}
|
||||
94
examples/64-uikit-window.sx
Normal file
94
examples/64-uikit-window.sx
Normal file
@@ -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));
|
||||
}
|
||||
20
examples/65-add-framework.sx
Normal file
20
examples/65-add-framework.sx
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
75
examples/modules/std/objc.sx
Normal file
75
examples/modules/std/objc.sx
Normal file
@@ -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);
|
||||
}
|
||||
17
examples/modules/std/uikit.sx
Normal file
17
examples/modules/std/uikit.sx
Normal file
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
134
src/ir/lower.zig
134
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 <value>` 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 <value>;`
|
||||
/// 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 {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -1486,6 +1486,7 @@ pub const Server = struct {
|
||||
.hash_compiler,
|
||||
.hash_foreign,
|
||||
.hash_library,
|
||||
.hash_framework,
|
||||
.hash_using,
|
||||
.hash_include,
|
||||
.hash_source,
|
||||
|
||||
112
src/main.zig
112
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> 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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
285
src/target.zig
285
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 <sdk_name> --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 `<bundle_path>` 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 <path>\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 (`<TEAM>.*`) to the concrete bundle ID
|
||||
/// (`<TEAM>.<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 <profile> -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 "<team>.<bundle_id>" 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,
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
\\<plist version="1.0">
|
||||
\\<dict>
|
||||
\\ <key>CFBundleIdentifier</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundleName</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundleExecutable</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>CFBundlePackageType</key>
|
||||
\\ <string>APPL</string>
|
||||
\\ <key>CFBundleVersion</key>
|
||||
\\ <string>1</string>
|
||||
\\ <key>CFBundleShortVersionString</key>
|
||||
\\ <string>0.1</string>
|
||||
\\ <key>MinimumOSVersion</key>
|
||||
\\ <string>{s}</string>
|
||||
\\ <key>UIDeviceFamily</key>
|
||||
\\ <array>
|
||||
\\ <integer>1</integer>
|
||||
\\ </array>
|
||||
\\ <key>LSRequiresIPhoneOS</key>
|
||||
\\ <true/>
|
||||
\\ <key>UILaunchScreen</key>
|
||||
\\ <dict/>
|
||||
\\ <key>DTPlatformName</key>
|
||||
\\ <string>{s}</string>
|
||||
\\</dict>
|
||||
\\</plist>
|
||||
\\
|
||||
, .{ 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 <script> tag and add Module.locateFile for .wasm/.data files.
|
||||
pub fn postProcessWasmHtml(allocator: std.mem.Allocator, io: std.Io, html_path: []const u8) void {
|
||||
|
||||
@@ -105,6 +105,7 @@ pub const Tag = enum {
|
||||
hash_compiler, // #compiler
|
||||
hash_foreign, // #foreign
|
||||
hash_library, // #library
|
||||
hash_framework, // #framework
|
||||
hash_using, // #using
|
||||
hash_include, // #include (inside #import c { ... })
|
||||
hash_source, // #source (inside #import c { ... })
|
||||
|
||||
4
tests/expected/08-types.txt
Normal file
4
tests/expected/08-types.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
a 0 : Foo{a: 0, b: 42, c: 124, d: 17}
|
||||
a 1 : Foo{a: 1, b: 42, c: 8, d: 17}
|
||||
b: Foo{a: 1, b: 1, c: 101, d: 1}
|
||||
Pack{a: 1, b: 0, c: 3, d: 5, f: 9, v: 100, x: 3.500000}
|
||||
@@ -1,27 +0,0 @@
|
||||
P1.1: 3
|
||||
P1.2: 30
|
||||
P2.1: 42
|
||||
P2.2: 150
|
||||
P2.3: 5 10
|
||||
P2.6: 5 10
|
||||
P2.7: 15
|
||||
P3.1: 5
|
||||
P3.2: 12
|
||||
P3.3: 102
|
||||
hi hi
|
||||
P4.1: 2
|
||||
yo yo
|
||||
P4.2: 2
|
||||
P4.3: 6 2
|
||||
P5.1: true false
|
||||
P5.2: 10 20
|
||||
P5.3: true false
|
||||
P5.5: true false
|
||||
P6.1: true false
|
||||
P6.2: true false
|
||||
P6.3: true false
|
||||
P6.4: 40
|
||||
P6.5: 20
|
||||
P7.1: 30
|
||||
P7.2: 10 300
|
||||
=== DONE ===
|
||||
1
tests/expected/55-err-field-not-found.exit
Normal file
1
tests/expected/55-err-field-not-found.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
tests/expected/55-err-field-not-found.txt
Normal file
1
tests/expected/55-err-field-not-found.txt
Normal file
@@ -0,0 +1 @@
|
||||
/Users/agra/projects/sx/examples/55-err-field-not-found.sx:8:15: error: field 'bogus' not found on type 'Vec'
|
||||
1
tests/expected/56-err-tuple-oob.exit
Normal file
1
tests/expected/56-err-tuple-oob.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
tests/expected/56-err-tuple-oob.txt
Normal file
1
tests/expected/56-err-tuple-oob.txt
Normal file
@@ -0,0 +1 @@
|
||||
/Users/agra/projects/sx/examples/56-err-tuple-oob.sx:6:15: error: field '42' not found on type 'tuple'
|
||||
1
tests/expected/57-err-dot-shorthand.exit
Normal file
1
tests/expected/57-err-dot-shorthand.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
tests/expected/57-err-dot-shorthand.txt
Normal file
1
tests/expected/57-err-dot-shorthand.txt
Normal file
@@ -0,0 +1 @@
|
||||
/Users/agra/projects/sx/examples/57-err-dot-shorthand.sx:5:10: error: cannot infer enum type for '.Foo' — use an explicit type or assign to a typed variable
|
||||
1
tests/expected/58-infer-return-type.exit
Normal file
1
tests/expected/58-infer-return-type.exit
Normal file
@@ -0,0 +1 @@
|
||||
178
|
||||
1
tests/expected/58-infer-return-type.txt
Normal file
1
tests/expected/58-infer-return-type.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
tests/expected/59-err-bad-variant.exit
Normal file
1
tests/expected/59-err-bad-variant.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
tests/expected/59-err-bad-variant.txt
Normal file
1
tests/expected/59-err-bad-variant.txt
Normal file
@@ -0,0 +1 @@
|
||||
/Users/agra/projects/sx/examples/59-err-bad-variant.sx:18:14: error: no variant 'Bogus' on type 'Shape'
|
||||
1
tests/expected/60-frameworks.exit
Normal file
1
tests/expected/60-frameworks.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/60-frameworks.txt
Normal file
1
tests/expected/60-frameworks.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
tests/expected/61-objc-roundtrip.exit
Normal file
1
tests/expected/61-objc-roundtrip.exit
Normal file
@@ -0,0 +1 @@
|
||||
209
|
||||
1
tests/expected/61-objc-roundtrip.txt
Normal file
1
tests/expected/61-objc-roundtrip.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
tests/expected/62-objc-class.exit
Normal file
1
tests/expected/62-objc-class.exit
Normal file
@@ -0,0 +1 @@
|
||||
42
|
||||
1
tests/expected/62-objc-class.txt
Normal file
1
tests/expected/62-objc-class.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
tests/expected/65-add-framework.exit
Normal file
1
tests/expected/65-add-framework.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/65-add-framework.txt
Normal file
1
tests/expected/65-add-framework.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user