commit 5b6da982b55a6146cf7742d6254c6016fa8d2122
parent dc7830d966d3836fc25be94cd5cc03ac7e0a1427
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 18:06:46 -0700
feat(macho): x86_64-macos compile+link+execute via Rosetta
Bring up x86_64-apple-darwin as a working compile->link->execute target on
an Apple-silicon host (binaries run under Rosetta 2). Most of the stack
already existed (x64 backend, Apple x64 ABI, Mach-O read/write, x86_64
relocs, triple parsing); two gaps remained:
- Linker: the Mach-O __stubs hook had no x86_64 entry (stub_size=0,
emit_stub=NULL), so link_emit_macho panicked for any x86_64 target --
even a zero-import program. Add x64_emit_macho_stub: a 6-byte
`jmpq *disp32(%rip)` through the __got slot (the COFF IAT stub shape;
x86_64 reaches any +/-2GiB slot directly, unlike aa64's 12-byte
adrp/ldr/br). Wire it into the registry.
- Object emit: x86_64 .o files carried a bogus ARM64_RELOC_ADDEND (reloc
type 10, invalid on x86_64) for non-zero non-UNSIGNED addends, which
ld64 rejects. x86_64 Mach-O stores the addend inline in the relocated
field, so bake symrel = addend + field_width into a writable copy of
the section bytes and drop the ADDEND reloc (arm64 path unchanged).
kit .o now links under the system ld64/lld too.
Testing:
- test/lib/exec_target.sh: x64-on-arm64-Darwin runs natively via Rosetta
(cached probe).
- test/smoke/x64_macos.sh + `make test-smoke-x64-macos`: kit-built
x86_64-macos hello, run via Rosetta (stdout + exit code).
- scripts/hosted.sh: macos-x64 in the support set; cross `cc`; selfhost
gated to host-arch so test-selfhost/test-port skip the unvalidated
x86_64-macos self-host. `make test-cross TARGET=macos-x64`.
- test/toy/vm.sh: macos/Rosetta backend (cross-compile the .toy corpus,
exec via the x64-macos tag), wired into cross_test.sh's full lane.
The full toy-corpus lane is committed RED: 324 pass, 6 fail (3 cases x
{O0,O1}), documenting two known x86_64-macos linker gaps still to fix --
chained-fixups rebase (15_cg_api_types_bytes_globals, SIGBUS) and TLV
thread-locals (141/142_threadlocal_*, SIGSEGV). Both pass on native
arm64-macos.
Diffstat:
9 files changed, 351 insertions(+), 54 deletions(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -122,6 +122,7 @@ TEST_TARGETS = \
test-smoke-rv64 \
test-smoke-rv32 \
test-smoke-x64 \
+ test-smoke-x64-macos \
test-toy \
test-wasm \
test-wasm-c \
@@ -1032,6 +1033,15 @@ test-wasm-target: bin
test-smoke-x64:
bash test/smoke/x64.sh
+# test-smoke-x64-macos: end-to-end check of kit's OWN x86_64 Mach-O
+# compile→link→execute path. kit compiles + links a hello-world to an
+# x86_64-macos Mach-O executable (driving the __stubs/__got import path) and
+# runs it via Rosetta 2 on an Apple-silicon host, asserting stdout + exit
+# code. Excluded from the default `test` target because it needs a Darwin
+# host with Rosetta + a macOS SDK; opt-in via `make test-smoke-x64-macos`.
+test-smoke-x64-macos:
+ bash test/smoke/x64_macos.sh
+
# test-smoke-rv64: phase-2 counterpart of test-smoke-x64. Builds a
# tiny freestanding riscv64 ELF with clang --target=riscv64-linux-gnu
# and runs it through test/lib/exec_target.sh, proving the rv64 lane
diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh
@@ -167,8 +167,18 @@ full_token() {
_full_lane "$t:libc" env KIT="$KIT" KIT_LIBC_ARCHES="$arch" bash "$ROOT/test/libc/$libc/run.sh"
;;
macos)
- _full_lane "$t:toy" env KIT="$KIT" bash "$ROOT/test/toy/run.sh"
- _full_lane "$t:parse" env KIT="$KIT" bash "$ROOT/test/parse/run.sh"
+ local ha; ha="$(uname -m 2>/dev/null)"
+ case "$arch:$ha" in
+ aa64:arm64|aa64:aarch64|x64:x86_64|x64:amd64)
+ # Native arch: run the corpus in-process.
+ _full_lane "$t:toy" env KIT="$KIT" bash "$ROOT/test/toy/run.sh"
+ _full_lane "$t:parse" env KIT="$KIT" bash "$ROOT/test/parse/run.sh" ;;
+ *)
+ # Cross arch (e.g. x86_64 on Apple silicon): cross-compile the toy
+ # corpus and run it via Rosetta 2 through the shared exec_target seam.
+ _full_lane "$t:toy-rosetta" \
+ env KIT="$KIT" KIT_TOY_MACOS_ARCHES="$arch" bash "$ROOT/test/toy/vm.sh" macos ;;
+ esac
;;
freebsd)
_full_lane "$t:toy-vm" env KIT="$KIT" bash "$ROOT/test/toy/vm.sh" freebsd
diff --git a/scripts/hosted.sh b/scripts/hosted.sh
@@ -43,7 +43,7 @@ linux-glibc-aa64 linux-glibc-x64 linux-glibc-rv64
linux-musl-aa64 linux-musl-x64 linux-musl-rv64
freebsd-aa64 freebsd-x64 freebsd-rv64
windows-aa64 windows-x64
-macos-aa64
+macos-aa64 macos-x64
freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32
"
@@ -81,7 +81,8 @@ parse_target() {
macos)
case "$rest" in
aarch64|arm64|aa64) T_ARCH=aarch64 ;;
- *) die "bad macos arch '$rest' in '$t' (arm64 only)" ;;
+ x64|x86_64|amd64) T_ARCH=x64 ;;
+ *) die "bad macos arch '$rest' in '$t' (want aa64|x64)" ;;
esac ;;
freestanding)
case "$rest" in
@@ -116,7 +117,11 @@ triple_of() {
x64) echo x86_64-windows ;;
aarch64) echo aarch64-windows ;;
esac ;;
- macos) echo aarch64-apple-darwin ;;
+ macos)
+ case "$T_ARCH" in
+ aarch64) echo aarch64-apple-darwin ;;
+ x64) echo x86_64-apple-darwin ;;
+ esac ;;
freestanding)
case "$T_ARCH" in
aarch64) echo aarch64-none-elf ;;
@@ -160,7 +165,19 @@ canon_token() {
# compiler cannot run on it: cross only.
modes_of() {
parse_target "$1"
- case "$T_OS" in freestanding) echo cross ;; *) echo "cross selfhost" ;; esac
+ case "$T_OS" in
+ freestanding) echo cross ;;
+ macos)
+ # Self-host runs the freshly built kit on the host. A non-host macOS arch
+ # (e.g. an x86_64 kit on Apple silicon) would run under Rosetta, but that
+ # path isn't validated yet — so only the host-arch macOS target self-hosts.
+ local ha; ha="$(uname -m 2>/dev/null)"
+ case "$T_ARCH:$ha" in
+ aarch64:arm64|aarch64:aarch64|x64:x86_64|x64:amd64) echo "cross selfhost" ;;
+ *) echo cross ;;
+ esac ;;
+ *) echo "cross selfhost" ;;
+ esac
}
_linux_sysroot_dir() { # libc arch
@@ -207,9 +224,15 @@ cc_target() {
local triple sysroot; triple="$(triple_of "$target")"; sysroot="$(path_of "$target")"
local args=()
if [ "$T_OS" = macos ]; then
- # Native host compile against the Xcode SDK.
+ # Compile against the Xcode/CLT SDK. A native-arch build omits -target so
+ # the host default applies; a cross to the other macOS arch (e.g. x86_64 on
+ # Apple silicon, run via Rosetta) needs the explicit target.
local sdk; sdk="$(xcrun --show-sdk-path 2>/dev/null)"
[ -n "$sdk" ] && args+=(-isysroot "$sdk")
+ local ha; ha="$(uname -m 2>/dev/null)"
+ case "$T_ARCH:$ha" in
+ x64:arm64|x64:aarch64|aarch64:x86_64|aarch64:amd64) args+=(-target "$triple") ;;
+ esac
else
args+=(-target "$triple")
[ -n "$sysroot" ] && args+=(--sysroot "$sysroot")
@@ -277,8 +300,13 @@ cmd_expand() {
for a in "$@"; do case "$a" in --mode=*) mode="${a#--mode=}" ;; *) sel="$a" ;; esac; done
toks="$(_expand_raw "$sel")" || exit 1
for t in $toks; do
- case "$t" in freestanding-*) [ "$mode" = selfhost ] && continue ;; esac
case "$t" in freebsd-*|windows-*) [ "${KIT_VM:-1}" = 0 ] && continue ;; esac
+ # selfhost mode keeps only targets whose modes_of advertises selfhost. This
+ # drops freestanding (cross-only) and a non-host-arch macOS target, whose
+ # self-host runs the built kit under emulation and isn't validated yet.
+ if [ "$mode" = selfhost ]; then
+ case " $(modes_of "$t") " in *" selfhost "*) ;; *) continue ;; esac
+ fi
printf '%s\n' "$t"
done
}
diff --git a/src/arch/x64/link.c b/src/arch/x64/link.c
@@ -78,6 +78,22 @@ void x64_emit_coff_iat_stub(u8* dst, u64 stub_vaddr, u64 iat_slot_vaddr) {
x64_write_jmp_riprel(dst, disp32);
}
+/* Mach-O __stubs entry for x86_64 (6 B):
+ *
+ * ff 25 disp32 ; jmpq *[rip + disp_to_got_slot]
+ *
+ * disp32 is the signed offset from the END of the JMP (stub_vaddr + 6) to the
+ * __got slot dyld binds. Byte-identical to the COFF IAT stub — both are one
+ * RIP-relative indirect jump through a pointer slot — but kept as its own
+ * symbol so the Mach-O linker's per-arch stub hook stays decoupled from COFF.
+ * Unlike AArch64 (a 12-byte adrp/ldr/br trampoline), x86_64 reaches any slot in
+ * the ±2GiB image directly, so no scratch register and no page split. */
+void x64_emit_macho_stub(u8* dst, u64 stub_vaddr, u64 got_slot_vaddr) {
+ i64 disp = (i64)got_slot_vaddr - (i64)(stub_vaddr + X64_JMP_RIPREL_SIZE);
+ i32 disp32 = (i32)(u32)((u64)disp & 0xffffffffu);
+ x64_write_jmp_riprel(dst, disp32);
+}
+
const LinkArchDesc link_arch_x64 = {
.plt0_size = 32u,
.plt_entry_size = 16u,
diff --git a/src/obj/macho/emit.c b/src/obj/macho/emit.c
@@ -149,6 +149,8 @@ typedef struct MSec {
u32 obj_sec; /* originating ObjSecId */
int is_zerofill;
const Buf* obj_bytes; /* NULL when zerofill */
+ u8* patch_bytes; /* writable flattened copy with addends baked into
+ * reloc fields (x86_64); NULL → write obj_bytes verbatim */
u8* relocs; /* arena-allocated; nreloc * 8 bytes */
} MSec;
@@ -158,6 +160,27 @@ static u32 log2_align(u32 a) {
return r;
}
+/* Lazily flatten m's section bytes into a writable arena buffer so x86_64
+ * addends can be baked into the relocated fields. Returns NULL for zerofill. */
+static u8* macho_sec_writable(Compiler* c, MSec* m) {
+ if (m->patch_bytes || !m->obj_bytes) return m->patch_bytes;
+ u32 sz = m->obj_bytes->total;
+ u8* p = (u8*)arena_alloc(c->scratch, sz ? sz : 1, _Alignof(u64));
+ if (sz) buf_flatten(m->obj_bytes, p);
+ m->patch_bytes = p;
+ return p;
+}
+
+/* Write a `length`-encoded little-endian field (length 0/1/2/3 → 1/2/4/8 B). */
+static void macho_write_field(u8* at, u32 length, i64 v) {
+ switch (length) {
+ case 0: *at = (u8)v; break;
+ case 1: wr_u16_le(at, (u16)v); break;
+ case 2: wr_u32_le(at, (u32)v); break;
+ default: wr_u64_le(at, (u64)v); break;
+ }
+}
+
static u32 section_flags_for(u16 sec_kind, u16 sec_flags, const char* sectname,
u32 sect_len) {
u32 f = 0;
@@ -604,20 +627,34 @@ void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) {
}
u32 r_symbolnum = mach_sym_idx - 1; /* Mach-O uses 0-based. */
- /* Non-zero addend: emit a leading ARM64_RELOC_ADDEND pair (only
- * meaningful for non-UNSIGNED types — UNSIGNED carries the addend
- * inline in the patched bytes). */
+ /* Non-zero addend (UNSIGNED carries its addend inline on both arches,
+ * so its enum value 0 — shared by ARM64_RELOC_UNSIGNED and
+ * X86_64_RELOC_UNSIGNED — is excluded here). The two arches diverge:
+ * AArch64 has no inline addend field for the PC-relative/GOT kinds,
+ * so a leading ARM64_RELOC_ADDEND pseudo-reloc carries it.
+ * x86_64 has no X86_64_RELOC_ADDEND; the relocated field holds the
+ * symbol-relative addend inline and the linker applies the PC bias.
+ * kit's addend is ELF-style (symrel − field_width for these
+ * PC-relative kinds), so bake symrel = addend + width back in. */
if (r->addend != 0 && mtype != ARM64_RELOC_UNSIGNED) {
- u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
- wr_u32_le(slot + 0, (u32)r->offset);
- /* ARM64_RELOC_ADDEND stores a signed 24-bit immediate in
- * r_symbolnum. It is not a symbol-table reference; setting
- * r_extern would make readers interpret the addend as a symbol
- * index. */
- u32 packed = ((u32)(i64)r->addend & 0x00ffffffu) | (0u << 24) |
- (length << 25) | (ARM64_RELOC_ADDEND << 28);
- wr_u32_le(slot + 4, packed);
- ++j;
+ if (c->target.arch == KIT_ARCH_ARM_64) {
+ u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
+ wr_u32_le(slot + 0, (u32)r->offset);
+ /* ARM64_RELOC_ADDEND stores a signed 24-bit immediate in
+ * r_symbolnum. It is not a symbol-table reference; setting
+ * r_extern would make readers interpret the addend as a symbol
+ * index. */
+ u32 packed = ((u32)(i64)r->addend & 0x00ffffffu) | (0u << 24) |
+ (length << 25) | (ARM64_RELOC_ADDEND << 28);
+ wr_u32_le(slot + 4, packed);
+ ++j;
+ } else {
+ u8* data = macho_sec_writable(c, m);
+ if (data) {
+ i64 symrel = r->addend + (pcrel ? (i64)(1u << length) : 0);
+ macho_write_field(data + r->offset, length, symrel);
+ }
+ }
}
u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
@@ -780,10 +817,14 @@ void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) {
kit_writer_seek(w, m->fileoff);
if (m->obj_bytes) {
u32 sz = m->obj_bytes->total;
- u8* tmp = (u8*)h->alloc(h, sz ? sz : 1, 1);
- if (sz) buf_flatten(m->obj_bytes, tmp);
- kit_writer_write(w, tmp, sz);
- h->free(h, tmp, sz ? sz : 1);
+ if (m->patch_bytes) {
+ kit_writer_write(w, m->patch_bytes, sz);
+ } else {
+ u8* tmp = (u8*)h->alloc(h, sz ? sz : 1, 1);
+ if (sz) buf_flatten(m->obj_bytes, tmp);
+ kit_writer_write(w, tmp, sz);
+ h->free(h, tmp, sz ? sz : 1);
+ }
}
}
diff --git a/src/obj/registry.c b/src/obj/registry.c
@@ -53,6 +53,7 @@ ObjFormatArchiveAction obj_format_archive_member_disabled(
#if KIT_LINK_ENABLED
void aa64_emit_macho_stub(u8* dst, u64 stub_vaddr, u64 got_slot_vaddr);
void aa64_emit_coff_iat_stub(u8* dst, u64 stub_vaddr, u64 iat_slot_vaddr);
+void x64_emit_macho_stub(u8* dst, u64 stub_vaddr, u64 got_slot_vaddr);
void x64_emit_coff_iat_stub(u8* dst, u64 stub_vaddr, u64 iat_slot_vaddr);
/* COFF synthetic-input hook body. Needs Linker internals (LinkInput append,
* link_arch_desc_for for the __chkstk stub), so it lives in src/link; wired
@@ -65,6 +66,7 @@ void obj_format_macho_stub_disabled(u8* dst, u64 stub_vaddr,
void obj_format_coff_stub_disabled(u8* dst, u64 stub_vaddr, u64 iat_slot_vaddr);
#define aa64_emit_macho_stub obj_format_macho_stub_disabled
#define aa64_emit_coff_iat_stub obj_format_coff_stub_disabled
+#define x64_emit_macho_stub obj_format_macho_stub_disabled
#define x64_emit_coff_iat_stub obj_format_coff_stub_disabled
#define OBJ_COFF_SYNTH_INPUTS NULL
#endif
@@ -223,8 +225,8 @@ static const ObjMachoArchOps obj_macho_arch_ops[] = {
.arch = KIT_ARCH_X86_64,
.cputype = CPU_TYPE_X86_64,
.cpusubtype = CPU_SUBTYPE_X86_64_ALL,
- .stub_size = 0u,
- .emit_stub = NULL,
+ .stub_size = 6u,
+ .emit_stub = x64_emit_macho_stub,
.reloc_to = macho_x86_64_reloc_to,
.reloc_pcrel = macho_x86_64_reloc_pcrel,
.reloc_length = macho_x86_64_reloc_length,
diff --git a/test/lib/exec_target.sh b/test/lib/exec_target.sh
@@ -167,7 +167,16 @@ _exec_target_native() {
;;
macos)
[ "$host_kernel" = "Darwin" ] || return 1
- _exec_target_arch_matches_host "$arch" "$host_arch"
+ _exec_target_arch_matches_host "$arch" "$host_arch" && return 0
+ # Rosetta 2: an Apple-silicon Darwin host transparently translates
+ # and runs x86_64 Mach-O binaries — executing the file directly is
+ # enough (no `arch -x86_64` wrapper). Treat x64-on-arm64 as native
+ # when Rosetta is installed.
+ if [ "$arch" = "x64" ] && \
+ { [ "$host_arch" = "arm64" ] || [ "$host_arch" = "aarch64" ]; }; then
+ _exec_target_rosetta_available && return 0
+ fi
+ return 1
;;
*) return 1 ;;
esac
@@ -183,6 +192,19 @@ _exec_target_arch_matches_host() {
esac
}
+# True when Rosetta 2 is available to run x86_64 binaries on this host. Probe
+# once and cache — exec paths call this per binary.
+_exec_target_rosetta_available() {
+ case "${_EXEC_TARGET_ROSETTA:-}" in
+ yes) return 0 ;;
+ no) return 1 ;;
+ esac
+ if arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
+ _EXEC_TARGET_ROSETTA=yes; return 0
+ fi
+ _EXEC_TARGET_ROSETTA=no; return 1
+}
+
# True when podman can run this linux target without emulation. The podman
# machine on Darwin/arm64 already runs linux/arm64, so passing `--platform
# linux/arm64` there is redundant — and worse, triggers a registry manifest
diff --git a/test/smoke/x64_macos.sh b/test/smoke/x64_macos.sh
@@ -0,0 +1,130 @@
+#!/usr/bin/env bash
+# test/smoke/x64_macos.sh — end-to-end smoke test for kit's x86_64-macos
+# (Mach-O) compile→link→execute path, on the shared Type-K mode-P kit
+# (test/lib/kit_sh_kit.sh).
+#
+# Unlike test/smoke/x64.sh (which proves the harness can run an x86_64-LINUX
+# ELF built by clang), this exercises kit's OWN x86_64 Mach-O codegen and
+# linker: kit compiles a hello-world to a CPU_TYPE_X86_64 Mach-O object, kit
+# links it into a Mach-O executable (driving the __stubs/__got import path —
+# x64_emit_macho_stub), and the binary runs via Rosetta 2 on an Apple-silicon
+# host. It asserts both stdout content and exit code, on the synchronous
+# (exec_target_run) and batched (exec_target_queue+flush) paths.
+#
+# Skipped unless: host is Darwin, kit is built, Rosetta 2 is installed (so an
+# arm64 host can run x86_64), and a macOS SDK is discoverable via xcrun (kit's
+# Darwin hosted profile needs a sysroot). Skip is a failure unless
+# KIT_TEST_ALLOW_SKIP=1 (the shared kit_exit honors it).
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+KIT="${KIT:-$ROOT/build/kit}"
+BUILD_DIR="$ROOT/build/test/smoke-x64-macos"
+mkdir -p "$BUILD_DIR"
+
+KIT_KIT_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/kit_sh_kit.sh
+. "$ROOT/test/lib/kit_sh_kit.sh"
+kit_report_init
+
+# A SKIP is a failure unless KIT_TEST_ALLOW_SKIP=1, so CI catches a degraded run.
+KIT_SKIP_IS_FAILURE=1
+
+work="$BUILD_DIR"
+
+# ---- detect prerequisites --------------------------------------------------
+
+# Variables consumed by exec_target.sh; none of qemu/podman apply to a native
+# Mach-O run, but the helper reads them unconditionally.
+have_qemu=0
+QEMU_BIN=""
+have_podman=0
+arch_raw="$(uname -m 2>/dev/null || true)"
+is_aarch64=0
+{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
+export have_qemu QEMU_BIN have_podman is_aarch64
+EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export EXEC_TARGET_MOUNT_ROOT
+# shellcheck source=../lib/exec_target.sh
+. "$ROOT/test/lib/exec_target.sh"
+
+if [ "$(uname -s 2>/dev/null)" != "Darwin" ]; then
+ skip_test "exec" "x86_64-macos exec requires a Darwin host"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+if [ ! -x "$KIT" ]; then
+ skip_test "build" "$KIT not built (run 'make bin')"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+# exec_target_supported x64-macos is true on an x86_64 host, or on an arm64
+# host with Rosetta 2 (the case this lane is built for).
+if ! exec_target_supported x64-macos; then
+ skip_test "exec" "no x86_64-macos runner (need x86_64 host or Rosetta 2)"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
+if [ -z "$SDK" ]; then
+ skip_test "build" "no macOS SDK (xcrun --show-sdk-path empty)"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+
+# ---- build: kit -> x86_64 Mach-O executable --------------------------------
+
+# Exercises the import path (printf → __stubs → __got → dyld bind on libSystem)
+# plus a non-trivial exit code. Returning 42 mirrors the other smoke lanes.
+SRC="$BUILD_DIR/smoke.c"
+cat >"$SRC" <<'EOF'
+#include <stdio.h>
+int main(void) {
+ printf("kit-x64-macos-ok\n");
+ return 42;
+}
+EOF
+
+EXE="$BUILD_DIR/smoke"
+if ! "$KIT" cc -target x86_64-macos --sysroot "$SDK" -lc \
+ "$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then
+ not_ok "build" "$BUILD_DIR/build.err"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+# Confirm we actually produced an x86_64 Mach-O, not a host-arch binary.
+if ! file "$EXE" 2>/dev/null | grep -q "Mach-O.*x86_64"; then
+ file "$EXE" > "$BUILD_DIR/file.diag" 2>&1
+ not_ok "build (x86_64 Mach-O)" "$BUILD_DIR/file.diag"
+ kit_summary test-smoke-x64-macos; kit_exit
+fi
+ok "build (x86_64 Mach-O executable)"
+
+# ---- exec_target_run --------------------------------------------------------
+
+exec_target_run x64-macos "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err"
+if [ "$RUN_RC" -eq 42 ] && grep -q "kit-x64-macos-ok" "$BUILD_DIR/run.out"; then
+ ok "exec_target_run x64-macos (rc=42, stdout)"
+else
+ echo "expected rc=42 + stdout marker; got rc=$RUN_RC out=[$(cat "$BUILD_DIR/run.out" 2>/dev/null)] err=$BUILD_DIR/run.err" \
+ > "$BUILD_DIR/run.diag"
+ not_ok "exec_target_run x64-macos" "$BUILD_DIR/run.diag"
+fi
+
+# ---- exec_target_queue + flush ----------------------------------------------
+
+exec_target_queue x64-macos smoke "$EXE" \
+ "$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc"
+exec_target_flush
+if [ ! -f "$BUILD_DIR/q.rc" ]; then
+ echo "no rc file produced" > "$BUILD_DIR/q.diag"
+ not_ok "exec_target_queue+flush x64-macos" "$BUILD_DIR/q.diag"
+else
+ Q_RC="$(cat "$BUILD_DIR/q.rc")"
+ if [ "$Q_RC" -eq 42 ] && grep -q "kit-x64-macos-ok" "$BUILD_DIR/q.out"; then
+ ok "exec_target_queue+flush x64-macos (rc=42, stdout)"
+ else
+ echo "expected rc=42 + stdout marker; got rc=$Q_RC; see $BUILD_DIR/q.err" \
+ > "$BUILD_DIR/q.diag"
+ not_ok "exec_target_queue+flush x64-macos" "$BUILD_DIR/q.diag"
+ fi
+fi
+
+kit_summary test-smoke-x64-macos
+kit_exit
diff --git a/test/toy/vm.sh b/test/toy/vm.sh
@@ -16,7 +16,11 @@
# regardless of VM availability (so a codegen/link bug is caught even with no VM);
# only execution is gated on a runner.
#
-# usage: test/toy/vm.sh <os> [name_filter] os: freebsd | windows
+# usage: test/toy/vm.sh <os> [name_filter] os: freebsd | windows | macos
+#
+# macos is the Rosetta variant: on an Apple-silicon host it cross-compiles each
+# case to x86_64-apple-darwin and runs it via Rosetta 2 (exec_target's x64-macos
+# tag — no VM). Same staging/skip/flush machinery as the VM backends.
#
# env: KIT, KIT_TOY_FREEBSD_ARCHES (amd64 aarch64 riscv64),
# KIT_TOY_WINDOWS_ARCHES (x64 aarch64), KIT_OPT_LEVELS (0 1),
@@ -47,13 +51,19 @@ trap exec_target_teardown_all EXIT
OS="${1:-}"
FILTER="${2:-${KIT_TEST_FILTER:-}}"
case "$OS" in
- freebsd|windows) ;;
- *) echo "usage: $0 <freebsd|windows> [name_filter]" >&2; exit 2 ;;
+ freebsd|windows|macos) ;;
+ *) echo "usage: $0 <freebsd|windows|macos> [name_filter]" >&2; exit 2 ;;
esac
OPT_LEVELS="${KIT_OPT_LEVELS:-0 1}"
FREEBSD_ARCHES="${KIT_TOY_FREEBSD_ARCHES:-amd64 aarch64 riscv64}"
WINDOWS_ARCHES="${KIT_TOY_WINDOWS_ARCHES:-x64 aarch64}"
+# macOS runs a *cross*-arch (non-host) toy binary via Rosetta 2 on an
+# Apple-silicon host — defaults to x64 (the x86_64-on-arm64 case). The
+# native-arch macOS corpus is run in-process by test/toy/run.sh, not here.
+MACOS_ARCHES="${KIT_TOY_MACOS_ARCHES:-x64}"
+MACOS_SDK="" # resolved (xcrun) in the macos drive branch; needed only by
+ # <name>.link.hosted cases (e.g. thread-locals → _tlv_bootstrap)
LINK="${KIT_FREEBSD_LINK:-both}"
if [ ! -x "$KIT" ]; then
@@ -71,6 +81,8 @@ triple_for() {
freebsd/riscv64) echo riscv64-freebsd ;;
windows/x64) echo x86_64-windows ;;
windows/aarch64) echo aarch64-windows ;;
+ macos/x64) echo x86_64-apple-darwin ;;
+ macos/aarch64) echo arm64-apple-darwin ;;
*) echo "" ;;
esac
}
@@ -79,6 +91,7 @@ sysroot_for() {
case "$1" in
freebsd) "$ROOT/scripts/freebsd_sysroot.sh" path "$2" 2>/dev/null ;;
windows) "$ROOT/scripts/llvm_mingw_sysroot.sh" path "$2" 2>/dev/null ;;
+ macos) echo "" ;; # .toy cases are freestanding (Mach-O LC_MAIN); no sysroot
esac
}
@@ -86,7 +99,7 @@ sysroot_for() {
# *_aa64 / *_rv64 use intrinsics that only lower on that arch (cf. run.sh).
arch_suffix_for() {
case "$1/$2" in
- freebsd/amd64|windows/x64) echo _x64 ;;
+ freebsd/amd64|windows/x64|macos/x64) echo _x64 ;;
*/aarch64) echo _aa64 ;;
freebsd/riscv64) echo _rv64 ;;
*) echo "" ;;
@@ -111,6 +124,7 @@ case_skip_reason() {
modes_for() {
if [ "$1" = windows ]; then echo ucrt; return; fi
+ if [ "$1" = macos ]; then echo default; return; fi # freestanding, no link split
case "$LINK" in
static) echo static ;;
dynamic) echo dynamic ;;
@@ -133,8 +147,10 @@ TF_LABEL=(); TF_EXP=(); TF_EXE=(); TF_OUT=(); TF_ERR=(); TF_RC=(); TF_TAG=()
STAGED_N=0
stage_arch() {
local os="$1" arch="$2" triple="$3" sysroot="$4" stage="$5"
- local ext="" id=0 name base reason opt mode label out cc_err expected
+ local ext="" id=0 name base reason opt mode label out cc_err expected link_flag
[ "$os" = windows ] && ext=".exe"
+ local sysroot_flag=()
+ [ -n "$sysroot" ] && sysroot_flag=(--sysroot "$sysroot")
rm -rf "$stage"; mkdir -p "$stage"
for src in "$TEST_DIR"/cases/*.toy; do
@@ -144,12 +160,22 @@ stage_arch() {
if [ -n "$reason" ]; then kit_skip "$base/$os-$arch" "$reason"; continue; fi
expected=0
[ -f "${src%.toy}.expected" ] && expected="$(cat "${src%.toy}.expected")"
+ # macOS .toy cases link freestanding (Mach-O LC_MAIN) by default; a
+ # <name>.link.hosted sidecar forces a hosted link (kit cc -lc against the
+ # SDK) for cases needing the libSystem runtime (e.g. _tlv_bootstrap for
+ # thread-locals), mirroring test/toy/run.sh's path L.
+ link_flag=()
+ if [ "$os" = macos ] && [ -e "${src%.toy}.link.hosted" ]; then
+ if [ -z "$MACOS_SDK" ]; then
+ kit_skip "$base/$os-$arch" "needs macOS SDK for hosted link (xcrun --show-sdk-path)"; continue; fi
+ link_flag=(--sysroot "$MACOS_SDK" -lc)
+ fi
for opt in $OPT_LEVELS; do
for mode in $(modes_for "$os"); do
label="$base/$os-$arch-$mode-O$opt"
out="$stage/$id$ext"; cc_err="$stage/$id.cc.err"
# shellcheck disable=SC2046
- if ! "$KIT" cc "-O$opt" -target "$triple" --sysroot "$sysroot" \
+ if ! "$KIT" cc "-O$opt" -target "$triple" "${sysroot_flag[@]}" "${link_flag[@]}" \
$(cc_extra_flags "$os" "$mode") "$src" -o "$out" \
> "$stage/$id.cc.out" 2> "$cc_err"; then
kit_fail "$label" "kit cc -target $triple failed"
@@ -174,31 +200,43 @@ mkdir -p "$BUILD_DIR/$OS"
# Phase 1: compile every applicable case for every arch (catches codegen/link
# bugs even with no VM).
-if [ "$OS" = freebsd ]; then
- printf 'toy-vm freebsd: arches="%s" opts="%s" link=%s\n' \
- "$FREEBSD_ARCHES" "$OPT_LEVELS" "$LINK"
- ARCHES="$FREEBSD_ARCHES"
-else
- printf 'toy-vm windows: arches="%s" opts="%s"\n' "$WINDOWS_ARCHES" "$OPT_LEVELS"
- ARCHES="$WINDOWS_ARCHES"
-fi
+case "$OS" in
+ freebsd)
+ printf 'toy-vm freebsd: arches="%s" opts="%s" link=%s\n' \
+ "$FREEBSD_ARCHES" "$OPT_LEVELS" "$LINK"
+ ARCHES="$FREEBSD_ARCHES" ;;
+ windows)
+ printf 'toy-vm windows: arches="%s" opts="%s"\n' "$WINDOWS_ARCHES" "$OPT_LEVELS"
+ ARCHES="$WINDOWS_ARCHES" ;;
+ macos)
+ MACOS_SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
+ printf 'toy-vm macos(rosetta): arches="%s" opts="%s" sdk=%s\n' \
+ "$MACOS_ARCHES" "$OPT_LEVELS" "${MACOS_SDK:-<none>}"
+ ARCHES="$MACOS_ARCHES" ;;
+esac
for arch in $ARCHES; do
triple="$(triple_for "$OS" "$arch")"
if [ -z "$triple" ]; then kit_skip_na "toy/$OS-$arch" "unknown arch"; continue; fi
sysroot="$(sysroot_for "$OS" "$arch")"
- if [ "$OS" = freebsd ]; then
- if [ -z "$sysroot" ] || [ ! -d "$sysroot/usr/include" ]; then
- kit_skip "toy/$OS-$arch" "missing sysroot (scripts/freebsd_sysroot.sh $arch)"; continue; fi
- case "$LINK" in static|both) [ -f "$sysroot/usr/lib/libc.a" ] || {
- kit_skip "toy/$OS-$arch" "missing $sysroot/usr/lib/libc.a"; continue; } ;; esac
- case "$LINK" in dynamic|both) [ -f "$sysroot/lib/libc.so.7" ] || {
- kit_skip "toy/$OS-$arch" "missing $sysroot/lib/libc.so.7"; continue; } ;; esac
- else
- if [ -z "$sysroot" ] || [ ! -r "$sysroot/include/windows.h" ] ||
- [ ! -r "$sysroot/lib/libucrt.a" ]; then
- kit_skip "toy/$OS-$arch" "missing UCRT sysroot (scripts/llvm_mingw_sysroot.sh prepare $arch)"; continue; fi
- fi
+ case "$OS" in
+ freebsd)
+ if [ -z "$sysroot" ] || [ ! -d "$sysroot/usr/include" ]; then
+ kit_skip "toy/$OS-$arch" "missing sysroot (scripts/freebsd_sysroot.sh $arch)"; continue; fi
+ case "$LINK" in static|both) [ -f "$sysroot/usr/lib/libc.a" ] || {
+ kit_skip "toy/$OS-$arch" "missing $sysroot/usr/lib/libc.a"; continue; } ;; esac
+ case "$LINK" in dynamic|both) [ -f "$sysroot/lib/libc.so.7" ] || {
+ kit_skip "toy/$OS-$arch" "missing $sysroot/lib/libc.so.7"; continue; } ;; esac
+ ;;
+ windows)
+ if [ -z "$sysroot" ] || [ ! -r "$sysroot/include/windows.h" ] ||
+ [ ! -r "$sysroot/lib/libucrt.a" ]; then
+ kit_skip "toy/$OS-$arch" "missing UCRT sysroot (scripts/llvm_mingw_sysroot.sh prepare $arch)"; continue; fi
+ ;;
+ macos)
+ : # freestanding .toy cases — no sysroot prerequisite; exec gated on Rosetta
+ ;;
+ esac
stage_arch "$OS" "$arch" "$triple" "$sysroot" "$BUILD_DIR/$OS/$arch"
done