Skip to content

Commit 0ce40bf

Browse files
wthollidayclaude
andcommitted
Add --target flag for AOT: macos-arm64 and macos-x86_64 alongside ios-arm64
Each invocation emits a thin Mach-O for one architecture; combine the outputs with `lipo -create` to produce a fat universal object (the pattern build-xcframework.sh already uses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cfb96f2 commit 0ce40bf

4 files changed

Lines changed: 140 additions & 22 deletions

File tree

cli/src/main.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,21 @@ struct Args {
5656
#[clap(long)]
5757
entry: Option<String>,
5858

59-
/// Ahead-of-time compile to an arm64-apple-ios Mach-O object file at the
60-
/// given path. A companion `.h` is written next to it. Requires the
61-
/// `llvm` feature and implies `--no-recursion`.
59+
/// Ahead-of-time compile to a Mach-O object file at the given path. A
60+
/// companion `.h` is written next to it. Requires the `llvm` feature and
61+
/// implies `--no-recursion`. Combine outputs from multiple `--target`
62+
/// runs with `lipo -create` to produce a fat universal object.
6263
#[clap(long)]
6364
aot: Option<String>,
6465

6566
/// Symbol prefix for AOT outputs. Defaults to the stem of the --aot path.
6667
#[clap(long)]
6768
aot_prefix: Option<String>,
69+
70+
/// Target for `--aot`. One of: `ios-arm64` (default), `macos-arm64`,
71+
/// `macos-x86_64`.
72+
#[clap(long, default_value = "ios-arm64")]
73+
target: String,
6874
}
6975

7076
fn run(args: Args) -> i32 {
@@ -182,7 +188,12 @@ fn run(args: Args) -> i32 {
182188
#[cfg(feature = "llvm")]
183189
{
184190
if let Some(out) = args.aot.as_deref() {
185-
return run_aot(&mut compiler, out, args.aot_prefix.as_deref());
191+
return run_aot(
192+
&mut compiler,
193+
out,
194+
args.aot_prefix.as_deref(),
195+
&args.target,
196+
);
186197
}
187198
}
188199
#[cfg(not(feature = "llvm"))]
@@ -422,6 +433,7 @@ fn run_aot(
422433
compiler: &mut lyte::Compiler,
423434
output: &str,
424435
prefix_override: Option<&str>,
436+
target_str: &str,
425437
) -> i32 {
426438
if !compiler.no_recursion {
427439
eprintln!("--aot requires --no-recursion");
@@ -431,6 +443,13 @@ fn run_aot(
431443
eprintln!("--aot: no declarations to compile");
432444
return 1;
433445
}
446+
let target = match lyte::llvm_aot::AotTarget::parse(target_str) {
447+
Ok(t) => t,
448+
Err(e) => {
449+
eprintln!("{}", e);
450+
return 1;
451+
}
452+
};
434453
if let Err(e) = compiler.specialize() {
435454
eprintln!("{}", e);
436455
return 1;
@@ -448,7 +467,7 @@ fn run_aot(
448467
eprintln!("--aot: prefix is empty (use --aot-prefix to override)");
449468
return 1;
450469
}
451-
match compiler.compile_aot(&path, &prefix) {
470+
match compiler.compile_aot(&path, &prefix, target) {
452471
Ok(()) => 0,
453472
Err(e) => {
454473
eprintln!("{}", e);

cli/tests/cli.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,35 @@ fn contains_substr(haystack: &[u8], needle: &[u8]) -> bool {
171171
.windows(needle.len())
172172
.any(|w| w == needle)
173173
}
174+
175+
#[test]
176+
#[cfg(feature = "llvm")]
177+
fn aot_target_macos_x86_64_emits_x86_object() {
178+
use std::io::Write;
179+
let tmp = std::env::temp_dir().join("lyte_aot_x86");
180+
let _ = std::fs::remove_dir_all(&tmp);
181+
std::fs::create_dir_all(&tmp).unwrap();
182+
let src = tmp.join("sx.lyte");
183+
let mut f = std::fs::File::create(&src).unwrap();
184+
writeln!(f, "go(x: f32) -> f32 {{ return x * x }}").unwrap();
185+
drop(f);
186+
let out_o = tmp.join("sx.o");
187+
let status = std::process::Command::new(LYTE_BIN)
188+
.arg(&src)
189+
.arg("--no-recursion")
190+
.arg("--entry")
191+
.arg("go")
192+
.arg("--target")
193+
.arg("macos-x86_64")
194+
.arg("--aot")
195+
.arg(&out_o)
196+
.status()
197+
.expect("failed to invoke lyte");
198+
assert!(status.success(), "--aot exited with {}", status);
199+
// Mach-O 64-bit x86_64: same magic 0xfeedfacf, cputype 0x01000007.
200+
let bytes = std::fs::read(&out_o).expect("read .o");
201+
let magic = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
202+
let cputype = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
203+
assert_eq!(magic, 0xfeed_facf);
204+
assert_eq!(cputype, 0x0100_0007, "expected x86_64 cputype (got {:#x})", cputype);
205+
}

src/compiler.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -768,17 +768,20 @@ impl Compiler {
768768
}
769769
}
770770

771-
/// Ahead-of-time compile to an `arm64-apple-ios` Mach-O object file and
772-
/// write a companion C header. Output paths are derived from
773-
/// `output_path` (e.g. `path/biquad.o` → `path/biquad.h`).
771+
/// Ahead-of-time compile to a Mach-O object file for `target` and write
772+
/// a companion C header. Output paths are derived from `output_path`
773+
/// (e.g. `path/biquad.o` → `path/biquad.h`).
774774
///
775775
/// `prefix` is applied to every externally-visible symbol so multiple
776776
/// AOT objects can be linked into the same binary without collisions.
777+
/// For a fat universal object, run once per target and combine the
778+
/// resulting thin objects with `lipo -create`.
777779
#[cfg(feature = "llvm")]
778780
pub fn compile_aot(
779781
&self,
780782
output_path: &std::path::Path,
781783
prefix: &str,
784+
target: crate::llvm_aot::AotTarget,
782785
) -> Result<(), String> {
783786
if self.decls.decls.is_empty() {
784787
return Err("No declarations to compile".into());
@@ -795,6 +798,7 @@ impl Compiler {
795798
&entry_points,
796799
output_path,
797800
prefix,
801+
target,
798802
self.print_ir,
799803
)
800804
}

src/llvm_aot.rs

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,34 +78,86 @@ pub struct AotEntry {
7878
pub returns_via_ptr: bool,
7979
}
8080

81-
/// Compile entry points to a Mach-O arm64 object file and write a companion
82-
/// `.h` header. Output paths are derived from `output_path` (e.g. for
83-
/// `path/to/biquad.o` we also write `path/to/biquad.h`).
81+
/// Apple platform target for AOT compile. Each produces a thin Mach-O object
82+
/// for one architecture; combine multiple outputs with `lipo -create` if a fat
83+
/// universal object is needed (matches the pattern in build-xcframework.sh).
84+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85+
pub enum AotTarget {
86+
IosArm64,
87+
MacosArm64,
88+
MacosX86_64,
89+
}
90+
91+
impl AotTarget {
92+
/// Parse a CLI-style identifier. Accepts ios-arm64, macos-arm64,
93+
/// macos-x86_64 (and aliases macos-x64, macos-intel).
94+
pub fn parse(s: &str) -> Result<Self, String> {
95+
match s {
96+
"ios-arm64" => Ok(Self::IosArm64),
97+
"macos-arm64" => Ok(Self::MacosArm64),
98+
"macos-x86_64" | "macos-x64" | "macos-intel" => Ok(Self::MacosX86_64),
99+
other => Err(format!(
100+
"unknown --target '{}'. Expected one of: ios-arm64, macos-arm64, macos-x86_64",
101+
other
102+
)),
103+
}
104+
}
105+
106+
fn triple(self) -> &'static str {
107+
match self {
108+
// iOS deployment target matches build-xcframework.sh.
109+
Self::IosArm64 => "arm64-apple-ios16.0",
110+
// macOS deployment target matches build-xcframework.sh (15.0).
111+
Self::MacosArm64 => "arm64-apple-macosx15.0",
112+
Self::MacosX86_64 => "x86_64-apple-macosx15.0",
113+
}
114+
}
115+
116+
fn cpu(self) -> &'static str {
117+
match self {
118+
// apple-a12 covers every iPhone since the XS / Apple silicon Macs.
119+
Self::IosArm64 | Self::MacosArm64 => "apple-a12",
120+
// macOS requires x86-64-v2 (Haswell) at minimum.
121+
Self::MacosX86_64 => "x86-64",
122+
}
123+
}
124+
}
125+
126+
/// Compile entry points to a Mach-O object file for `target` and write a
127+
/// companion `.h` header. Output paths are derived from `output_path`
128+
/// (e.g. for `path/to/biquad.o` we also write `path/to/biquad.h`).
84129
pub fn compile_aot(
85130
decls: &DeclTable,
86131
entry_points: &[Name],
87132
output_path: &Path,
88133
prefix: &str,
134+
target: AotTarget,
89135
print_ir: bool,
90136
) -> Result<(), String> {
91-
// Set up the iOS target machine. Must initialize AArch64 specifically;
92-
// `Target::initialize_native` only sets up the host arch, which on x86_64
93-
// hosts wouldn't be enough.
94-
Target::initialize_aarch64(&InitializationConfig::default());
137+
// Initialize the LLVM backend for this target. We do this rather than
138+
// `initialize_native` because the host arch may not match the AOT target.
139+
match target {
140+
AotTarget::IosArm64 | AotTarget::MacosArm64 => {
141+
Target::initialize_aarch64(&InitializationConfig::default());
142+
}
143+
AotTarget::MacosX86_64 => {
144+
Target::initialize_x86(&InitializationConfig::default());
145+
}
146+
}
95147

96-
let triple = inkwell::targets::TargetTriple::create("arm64-apple-ios16.0");
97-
let target =
148+
let triple = inkwell::targets::TargetTriple::create(target.triple());
149+
let llvm_target =
98150
Target::from_triple(&triple).map_err(|e| format!("LLVM target from triple: {}", e))?;
99-
let machine = target
151+
let machine = llvm_target
100152
.create_target_machine(
101153
&triple,
102-
"apple-a12",
154+
target.cpu(),
103155
"",
104156
OptimizationLevel::Aggressive,
105157
RelocMode::PIC,
106158
CodeModel::Default,
107159
)
108-
.ok_or("failed to create AArch64 target machine")?;
160+
.ok_or_else(|| format!("failed to create target machine for {:?}", target))?;
109161

110162
let context = Context::create();
111163
let (mut state, _build_elapsed) = build_module(
@@ -171,7 +223,12 @@ pub fn compile_aot(
171223

172224
// Header lives next to the object file, with the same stem.
173225
let header_path = output_path.with_extension("h");
174-
let header = build_header(prefix, &entries, &collect_globals(decls, state.globals_size));
226+
let header = build_header(
227+
prefix,
228+
&entries,
229+
&collect_globals(decls, state.globals_size),
230+
target,
231+
);
175232
std::fs::write(&header_path, header)
176233
.map_err(|e| format!("write header {}: {}", header_path.display(), e))?;
177234

@@ -567,12 +624,18 @@ fn set_internal_linkage(state: &mut LLVMJITState<'_>, public_names: &HashSet<Str
567624

568625
// ─── Header generation ────────────────────────────────────────────────────────
569626

570-
fn build_header(prefix: &str, entries: &[AotEntry], globals: &AotGlobalsLayout) -> String {
627+
fn build_header(
628+
prefix: &str,
629+
entries: &[AotEntry],
630+
globals: &AotGlobalsLayout,
631+
target: AotTarget,
632+
) -> String {
571633
let upper = prefix.to_uppercase();
572634
let mut s = String::new();
573635
use std::fmt::Write;
574636

575637
let _ = writeln!(s, "// Generated by the lyte AOT backend. Do not edit.");
638+
let _ = writeln!(s, "// Target: {} ({})", target.triple(), target.cpu());
576639
let _ = writeln!(s, "//");
577640
let _ = writeln!(
578641
s,

0 commit comments

Comments
 (0)