kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mmk/test.mk | 10++++++++++
Mscripts/cross_test.sh | 14++++++++++++--
Mscripts/hosted.sh | 40++++++++++++++++++++++++++++++++++------
Msrc/arch/x64/link.c | 16++++++++++++++++
Msrc/obj/macho/emit.c | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/obj/registry.c | 6++++--
Mtest/lib/exec_target.sh | 24+++++++++++++++++++++++-
Atest/smoke/x64_macos.sh | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/toy/vm.sh | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
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