kit

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

commit feaa52041dcd6ed0dfcb6bf1853df0da25d76d23
parent 94ddbef56c292a076d693d710eb4bdd618c96c3d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 30 May 2026 16:54:00 -0700

test+asm: cross-compile + cross-exec host-assembler lane (test-hostas-cross)

Extend the host-assembler-by-execution idea (test-hostas-toy) from the native
target to ELF Linux cross targets. test/asm/hostas_cross.sh emits one
`cc -S -target <triple>` per toy case, assembles it with BOTH cfree-as and clang,
links each into a static non-PIE ELF with `cfree ld -static` + the freestanding
start.c crt (compiled -Dtest_main=main so _start calls the toy's main and exits
with its return), and runs under podman/qemu via test/lib/exec_target.sh (one
batched container per target). Exit must match the toy oracle; the assembler is
the only variable between lanes.

Each target self-skips (never fails) unless the host has a clang cross target, a
runner, a working `cc -S | cfree as` for that arch, and a passing *bounded* exec
smoke (so a wedged emulator downgrades to SKIP rather than hanging). Status:
- aarch64-linux: green end-to-end (cfree-as 312/0, clang-as 312/0) — podman runs
  arm64 natively in its VM. This is the primary verified target.
- x86_64-linux: SKIPS on the pre-existing x64 `cc -S` symbolizer gap (numeric
  branch targets the x64 `as` can't reassemble; branch-label synthesis +
  reloc-operand syntax are aarch64-only).
- riscv64-linux: `cc -S | as | ld` works; SKIPS where rv64 user-mode emulation
  is unavailable/too slow to pass the exec smoke.

Overridable via CFREE_HOSTAS_CROSS_TARGETS / CFREE_HOSTAS_EXEC_TIMEOUT /
RUN_{AARCH64,X64,RV64}_IMAGE. Opt-in `make test-hostas-cross`; added to
TEST_TARGETS. Also refreshed the now-stale test-hostas-toy comment (clang lane
gates by default since the format-aware cc -S landed) and documented the cross
lane in doc/ASM_ROUNDTRIP_TESTING.md.

Diffstat:
Mdoc/ASM_ROUNDTRIP_TESTING.md | 43+++++++++++++++++++++++++++++++++++++++----
Atest/asm/hostas_cross.sh | 288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 20++++++++++++++++----
3 files changed, 343 insertions(+), 8 deletions(-)

diff --git a/doc/ASM_ROUNDTRIP_TESTING.md b/doc/ASM_ROUNDTRIP_TESTING.md @@ -228,10 +228,45 @@ ELF-triple `roundtrip`/`diff-llvm` lanes are unaffected. The same yet — x64/rv64 add their own `ArchAsmOps.reloc_operand`, COFF adds an `AsmSyntax` impl, then this lane extends to them). -Opt-in (`make test-hostas-toy`); skips cleanly when `clang` is absent. ELF -cross-targets (`aarch64/x86_64/riscv64-linux-gnu`) already assemble cleanly with -clang/llvm-mc and can extend this lane to podman/qemu cross-execution (à la -`test/toy/run.sh` path X). +Opt-in (`make test-hostas-toy`); skips cleanly when `clang` is absent. + +### Cross-compile + cross-exec lane (`test-hostas-cross`) + +`test/asm/hostas_cross.sh` is the cross extension of the host-assembler lane: +the same two-assembler-by-execution test, but for ELF **Linux** targets +(`aarch64`/`x86_64`/`riscv64-linux`) emitted with `cc -S -target <triple>`, +assembled by BOTH cfree-as and clang, linked into a **static, non-PIE** ELF with +`cfree ld -static`, and run under **podman/qemu** via the shared +`test/lib/exec_target.sh` helper (one batched container per target). The +executable is made runnable without a libc/loader by linking the freestanding +crt `test/link/harness/start.c` compiled `-Dtest_main=main`: its `_start` runs +ctors then calls the toy's `main` and exits with its return (the oracle) via a +raw `exit_group` syscall. + +Each target **self-skips** (never fails) unless the host has (1) a clang cross +target, (2) a runner (podman/qemu), (3) a working `cc -S | cfree as` round-trip +for that arch, and (4) a passing **bounded** exec smoke (so a wedged emulator +downgrades to SKIP instead of hanging). Status: + +- **aarch64-linux**: green end-to-end (cfree-as 312/0, clang-as 312/0) — podman + runs arm64 natively in its VM, so it's fast and the primary verified target. +- **x86_64-linux**: SKIPS on the x64 `cc -S` symbolizer gap — x64 emits numeric + branch targets (`jmp 0x77`) the x64 `as` can't reassemble. The aarch64 + symbolizer (intra-section branch-target label synthesis in + `src/api/asm_emit.c`, and the relocation-operand syntax via + `ArchAsmOps.reloc_operand`) needs x64 implementations — `is_local_branch`-style + recognition for `jmp`/`jcc`, plus an x64 `reloc_operand` table + (`sym(%rip)`/`@PLT`/`@GOTPCREL`). Tracked. +- **riscv64-linux**: `cc -S | cfree as | cfree ld -static` works; SKIPS where + riscv64 user-mode emulation is unavailable or too slow/wedged to pass the exec + smoke (e.g. the macOS/arm64 dev host's podman riscv64 path hangs on the + cfree-built static ELF even though it runs a clang-built one — likely a + cfree-rv64-ELF-under-qemu-user issue to chase separately). + +Override the matrix with `CFREE_HOSTAS_CROSS_TARGETS="tag:triple ..."`, the +exec-smoke cap with `CFREE_HOSTAS_EXEC_TIMEOUT=<secs>`, and per-arch images with +`RUN_{AARCH64,X64,RV64}_IMAGE`. Opt-in (`make test-hostas-cross`); skips cleanly +without clang/podman. ## Background — what cfree can do today (verified) diff --git a/test/asm/hostas_cross.sh b/test/asm/hostas_cross.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# test/asm/hostas_cross.sh — cross-compile + cross-exec extension of the +# host-assembler lane (test/asm/hostas_toy.sh) to ELF Linux targets. +# +# Where hostas_toy.sh proves `cc -S` on the *native* target, this proves it +# CROSS: for each ELF target (aarch64/x86_64/riscv64-linux) it emits ONE +# `cc -S`, feeds it to BOTH cfree-as and a host assembler (clang), links each +# into a static ELF with `cfree ld`, and runs it under podman/qemu — exit must +# match the toy oracle. The assembler is the only variable between the two +# lanes, judged by EXECUTION (cfree and clang emit different code, so a +# byte/text match would be meaningless), exactly like hostas_toy.sh. +# +# Per toy case (each target; both O0 and O1): +# cfree cc -S -target <triple> -> s.s (shared by both) +# A /cfree-as: cfree as -target | cfree ld -static -> run exit == oracle +# B /clang-as: clang --target -c | cfree ld -static -> run exit == oracle +# +# Executable shape: a STATIC, non-PIE ELF (`cfree ld -static`) linked with the +# freestanding crt test/link/harness/start.c compiled `-Dtest_main=main`, so +# `_start` runs ctors then calls the toy's `main` and exits with its return +# (the oracle) via a raw syscall — no libc/loader needed, so any same-arch +# Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper +# (one batched `podman run` per target) — the same path test/{link,smoke} +# already use. +# +# Self-probing: each target is SKIPPED (not failed) unless the host has (1) a +# clang cross-compiler for it, (2) a runner (podman/qemu) per exec_target, (3) a +# working `cfree cc -S | cfree as` round-trip for that arch, and (4) a bounded +# exec smoke that returns the oracle. So the harness runs green on whatever the +# host supports and self-extends as gaps close. Status at time of writing: +# - aarch64-linux: works end-to-end (podman runs arm64 natively in its VM). +# - x86_64-linux: SKIPS on the x64 `cc -S` symbolizer gap — x64 emits numeric +# branch targets (`jmp 0x77`) the x64 `as` can't reassemble +# (branch-target label synthesis + reloc-operand symbolization +# are aarch64-only today; see doc/ASM_ROUNDTRIP_TESTING.md). +# - riscv64-linux: `cc -S | as | ld` works; SKIPS where riscv64 user-mode +# emulation is unavailable/too slow to pass the exec smoke. +# +# Override the matrix with CFREE_HOSTAS_CROSS_TARGETS="tag:triple ..." and the +# clang-as gate with CFREE_HOSTAS_ENFORCE_CLANG=0 (demote lane B to XFAIL). + +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CFREE="$ROOT/build/cfree" +CASES="$ROOT/test/toy/cases" +WORK="$ROOT/build/test/asm/hostas_cross" +START_SRC="$ROOT/test/link/harness/start.c" +OPTS="${CFREE_TEST_OPTS:-O0 O1}" +FILTER="${1:-}" +ENFORCE_CLANG="${CFREE_HOSTAS_ENFORCE_CLANG:-1}" +EXEC_SMOKE_TIMEOUT="${CFREE_HOSTAS_EXEC_TIMEOUT:-45}" + +# "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. +TARGETS="${CFREE_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}" + +# Same TLS-symbolization skip as the sibling lanes. +SKIP="141_threadlocal_mutate" + +CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}" + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +if [ ! -x "$CFREE" ]; then + printf 'hostas-cross: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2 + exit 1 +fi +if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then + printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)" + exit 0 +fi +mkdir -p "$WORK" + +# ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------ +have_qemu=0 +QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" +[ -n "$QEMU_BIN" ] && have_qemu=1 +QEMU_RV64_BIN="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)" +have_podman=0 +command -v podman >/dev/null 2>&1 && have_podman=1 +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 QEMU_RV64_BIN have_podman is_aarch64 +# Pin arch-explicit images so podman doesn't resolve an ambiguous multi-arch +# tag to the wrong variant (the bare `alpine:latest` in local storage can map +# to a non-host arch). Overridable; the binaries are static/freestanding so any +# same-arch Linux image with /bin/sh works. Absent images fail --pull=never, +# which the per-target exec smoke turns into a clean SKIP. +: "${RUN_AARCH64_IMAGE:=docker.io/arm64v8/alpine:latest}" +: "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}" +: "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}" +export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE +EXEC_TARGET_MOUNT_ROOT="$WORK" +# shellcheck source=../lib/exec_target.sh +source "$ROOT/test/lib/exec_target.sh" + +is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; } + +oracle() { + local name="$1" exp=0 + [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected") + echo $((exp & 255)) +} + +# First real `error:` line from an assembler's stderr (clang prints a harmless +# -Wmissing-sysroot warning first — we never link with clang, so the SDK is +# irrelevant). +err_reason() { + local f="$1" line="" + line=$(grep -m1 -E 'error:|fatal:' "$f" 2>/dev/null | sed 's|.*\(error\|fatal\): *||') + [ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||') + printf '%s' "$line" +} + +# Bounded exec: run exe via exec_target under a wall-clock cap so a wedged +# emulator (e.g. a riscv64 qemu-user that never returns) downgrades a target to +# SKIP instead of hanging the whole harness. Sets SMOKE_RC (124 == timed out). +bounded_exec() { + local to="$1" tag="$2" exe="$3" out="$4" err="$5" + local rc0="$exe.rc0" + rm -f "$rc0" + ( exec_target_run "$tag" "$exe" "$out" "$err"; echo "$RUN_RC" >"$rc0" ) & + local pid=$! waited=0 + while kill -0 "$pid" 2>/dev/null; do + sleep 1; waited=$((waited+1)) + if [ "$waited" -ge "$to" ]; then + kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null + SMOKE_RC=124; return + fi + done + wait "$pid" 2>/dev/null + SMOKE_RC=124 + [ -f "$rc0" ] && SMOKE_RC="$(cat "$rc0")" +} + +# Link a relocatable object + the target crt into a static ELF. Echoes nothing; +# returns nonzero (and leaves stderr in $3) on failure or a non-empty linker +# diagnostic (warnings are treated as failures, as in hostas_toy.sh). +cfree_ld_static() { + local obj="$1" out="$2" lderr="$3" crt="$4" + "$CFREE" ld -static "$obj" "$crt" -o "$out" 2>"$lderr" || return 1 + [ -s "$lderr" ] && return 1 + return 0 +} + +printf 'hostas-cross: cfree=%s\n' "$CFREE" +printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \ + "$CLANG" "$OPTS" "$ENFORCE_CLANG" "$have_podman" +printf 'hostas-cross: targets="%s"\n' "$TARGETS" + +a_pass=0; a_fail=0 +b_pass=0; b_xfail=0; b_xpass=0; b_efail=0 +skip_cases=0 +tgt_run=0; tgt_skip=0 +a_failnames=() + +shopt -s nullglob +for entry in $TARGETS; do + tag="${entry%%:*}"; triple="${entry##*:}" + tdir="$WORK/$tag"; rm -rf "$tdir"; mkdir -p "$tdir" + + # --- per-target capability gates (SKIP, never FAIL) --- + if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; continue + fi + if ! exec_target_supported "$tag"; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; continue + fi + crt="$tdir/start.o" + if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$crt" 2>"$tdir/crt.err"; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; continue + fi + + # Pick a representative non-skip case for the smokes. + smoke=""; for s in "$CASES"/*.toy; do n="$(basename "$s" .toy)"; is_skip "$n" && continue; smoke="$n"; break; done + sd="$tdir/_smoke"; mkdir -p "$sd" + if ! "$CFREE" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; continue + fi + if ! "$CFREE" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; continue + fi + if ! cfree_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$crt"; then + tgt_skip=$((tgt_skip+1)); printf ' %s %s — cfree ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; continue + fi + bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err" + sexp=$(oracle "$smoke") + if [ "$SMOKE_RC" != "$sexp" ]; then + reason="exit $SMOKE_RC != $sexp"; [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)" + tgt_skip=$((tgt_skip+1)); printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; continue + fi + + # --- full corpus for this target: build everything, then one batched run --- + tgt_run=$((tgt_run+1)) + printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$tag" "$triple" + EXEC_TARGET_TAGS=(); EXEC_TARGET_NAMES=(); EXEC_TARGET_EXES=() + EXEC_TARGET_OUTS=(); EXEC_TARGET_ERRS=(); EXEC_TARGET_RCS=() + # exec_target_flush() clears its arrays on return, so keep our own + # reconciliation bookkeeping (lane / case / oracle / rc-file path). + Q_LANE=(); Q_NAME=(); Q_EXP=(); Q_RCF=() + + for src in "$CASES"/*.toy; do + name="$(basename "$src" .toy)" + [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue + if is_skip "$name"; then skip_cases=$((skip_cases+1)); continue; fi + exp=$(oracle "$name") + for opt in $OPTS; do + w="$tdir/$name/$opt"; rm -rf "$w"; mkdir -p "$w" + if ! "$CFREE" cc -S "-$opt" -target "$triple" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then + a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt] cc-S: $(err_reason "$w/ccs.err")") + printf ' %s %s/%s[-%s] cc -S: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/ccs.err")" + continue + fi + # Lane A: cfree as -> cfree ld -static. + if ! "$CFREE" as -target "$triple" "$w/s.s" -o "$w/a.o" 2>"$w/a.as.err"; then + a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as: $(err_reason "$w/a.as.err")") + printf ' %s %s/%s[-%s]/cfree-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.as.err")" + elif ! cfree_ld_static "$w/a.o" "$w/a.out" "$w/a.ld.err" "$crt"; then + a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as ld: $(err_reason "$w/a.ld.err")") + printf ' %s %s/%s[-%s]/cfree-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.ld.err")" + else + exec_target_queue "$tag" "A:$name[-$opt]" "$w/a.out" "$w/a.run.out" "$w/a.run.err" "$w/a.rc" + Q_LANE+=("A"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/a.rc") + fi + # Lane B: clang -c (third-party assembler) -> cfree ld -static. + if ! "$CLANG" --target="$triple" -c "$w/s.s" -o "$w/b.o" 2>"$w/b.as.err"; then + if [ "$ENFORCE_CLANG" = "1" ]; then + b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")" + else + b_xfail=$((b_xfail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_yel XFAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")" + fi + elif ! cfree_ld_static "$w/b.o" "$w/b.out" "$w/b.ld.err" "$crt"; then + if [ "$ENFORCE_CLANG" = "1" ]; then + b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.ld.err")" + else + b_xfail=$((b_xfail+1)) + fi + else + exec_target_queue "$tag" "B:$name[-$opt]" "$w/b.out" "$w/b.run.out" "$w/b.run.err" "$w/b.rc" + Q_LANE+=("B"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/b.rc") + fi + done + done + + # Drain this target's queue in one batched container run, then reconcile. + exec_target_flush + qn="${#Q_LANE[@]}"; qi=0 + while [ "$qi" -lt "$qn" ]; do + lane="${Q_LANE[$qi]}"; nm="${Q_NAME[$qi]}"; exp="${Q_EXP[$qi]}" + rcf="${Q_RCF[$qi]}" + rc=127; [ -f "$rcf" ] && rc="$(cat "$rcf")" + if [ "$lane" = "A" ]; then + if [ "$rc" = "$exp" ]; then a_pass=$((a_pass+1)); else + a_fail=$((a_fail+1)); a_failnames+=("$tag/$nm/cfree-as exit $rc != $exp") + printf ' %s %s/%s/cfree-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp" + fi + else + if [ "$rc" = "$exp" ]; then + if [ "$ENFORCE_CLANG" = "1" ]; then b_pass=$((b_pass+1)); else b_xpass=$((b_xpass+1)); fi + else + if [ "$ENFORCE_CLANG" = "1" ]; then + b_efail=$((b_efail+1)); printf ' %s %s/%s/clang-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp" + else + b_xfail=$((b_xfail+1)) + fi + fi + fi + qi=$((qi+1)) + done +done +shopt -u nullglob + +printf '\n' +[ "${#a_failnames[@]}" -gt 0 ] && { printf 'cfree-as failures:\n'; for f in "${a_failnames[@]}"; do printf ' %s\n' "$f"; done; } +printf 'hostas-cross: targets %d run, %d skip | cfree-as %d pass, %d fail | clang-as %d pass, %d xfail, %d xpass, %d efail | %d case-skip\n' \ + "$tgt_run" "$tgt_skip" "$a_pass" "$a_fail" "$b_pass" "$b_xfail" "$b_xpass" "$b_efail" "$skip_cases" +if [ "$tgt_run" -eq 0 ]; then + printf 'hostas-cross: no target ran (all SKIP-TGT) — needs a clang cross target + podman/qemu + a working cc -S|as for some ELF arch.\n' +fi + +rc=0 +[ "$a_fail" -gt 0 ] && rc=1 +[ "$ENFORCE_CLANG" = "1" ] && [ "$b_efail" -gt 0 ] && rc=1 +exit $rc diff --git a/test/test.mk b/test/test.mk @@ -43,6 +43,7 @@ TEST_TARGETS = \ test-asm-symmetry \ test-asm-roundtrip-toy \ test-hostas-toy \ + test-hostas-cross \ test-diff-llvm \ test-bounce \ test-cbackend \ @@ -739,13 +740,24 @@ test-asm-roundtrip-toy: bin # oracle. Only the assembler differs between the two lanes, so the clang lane is # the real test: a standard assembler can't paper over a private-dialect quirk # the way cfree's own `as` can (cf. test/asm/diff_llvm.sh, but by execution). -# The clang lane is currently XFAIL (native Mach-O cc -S emits ELF-only -# .type/.size and an @progbits token inside Mach-O .sections, which clang -# rejects) and does not gate by default; run with CFREE_HOSTAS_ENFORCE_CLANG=1 -# to gate it while fixing. Opt-in; skips cleanly if clang is absent. +# cc -S is now object-format-aware, so the native Mach-O clang lane GATES by +# default (both lanes 312/0); CFREE_HOSTAS_ENFORCE_CLANG=0 demotes it to XFAIL. +# Opt-in; skips cleanly if clang is absent. test-hostas-toy: bin @bash test/asm/hostas_toy.sh +# test-hostas-cross: the same two-assembler-by-execution idea as test-hostas-toy, +# but CROSS — `cc -S -target <triple>` for ELF Linux arches (aarch64/x86_64/ +# riscv64), assembled by cfree-as AND clang, linked static (+ the start.c crt) +# with cfree ld, and run under podman/qemu via test/lib/exec_target.sh. Each +# target self-skips unless the host has a clang cross target, a runner, a +# working `cc -S | cfree as` for that arch, and a passing bounded exec smoke — +# so it runs green on whatever the host supports (aarch64-linux today; x86_64 +# pends the x64 cc -S symbolizer, riscv64 pends a working rv64 user-mode +# emulator). Opt-in; skips cleanly if clang/podman are absent. +test-hostas-cross: bin + @bash test/asm/hostas_cross.sh + test-wasm: test-wasm-front test-wasm-target test-wasm-toy test-wasm-front: bin $(WASM_TOOL) $(LINK_EXE_RUNNER) $(JIT_RUNNER)