commit 79ae72f38b6d035e974765489d3e851a89196050
parent 9351e593c35aafabb76580dee163dc5e95ba5c1f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 9 May 2026 17:43:32 -0700
test: batch path-E podman runs and cache start.o
Per-`podman run` cost on macOS is ~150 ms regardless of payload, dominated
by the client→server round-trip. test-link issued ~40 such runs and
test-cg ~190, so path E was burning ~46 s of wall-clock per full pass.
Add test/lib/exec_aarch64.sh with queue/flush/run API. Path E now links
each case inline (~25 ms) and queues the linked.exe; after the case loop,
a single `podman run -i` reads a tab-separated manifest from stdin and
loops over every queued exe inside the container, writing per-case
.rc/.out/.err into the bind-mounted build/test/ tree. Qemu hosts (and
no-runner hosts) fall back to a serial host-side loop with the same API.
Also cache start.o per harness invocation rather than recompiling it from
the same source for every case.
Net per-pass:
test-link E: 7884 ms → 1574 ms (5.0×)
test-cg E: 37718 ms → 5929 ms (6.4×)
Output ordering: R and J still print inline per case; E PASS/FAIL lines
print together after the batch flush, prefixed by a "Running path E (N
cases batched)..." line. Pass/fail set unchanged.
Diffstat:
| M | test/cg/run.sh | | | 109 | ++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- |
| A | test/lib/exec_aarch64.sh | | | 132 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | test/link/run.sh | | | 290 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ |
| M | test/parse/run.sh | | | 98 | +++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------- |
4 files changed, 469 insertions(+), 160 deletions(-)
diff --git a/test/cg/run.sh b/test/cg/run.sh
@@ -103,26 +103,14 @@ arch_raw="$(uname -m 2>/dev/null || true)"
{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
-RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}"
-run_aarch64() {
- local exe="$1" out="$2" err="$3"
- if [ $have_qemu -eq 1 ]; then
- "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return
- fi
- if [ $have_podman -eq 1 ]; then
- local dir base platform_flag=()
- dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")"
- # `--platform linux/arm64` triggers a registry manifest lookup (~30s)
- # even when the local image is already arm64. Only pass it on hosts
- # where the podman machine isn't natively arm64.
- [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64)
- podman run --rm "${platform_flag[@]}" --net=none \
- -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err"
- RUN_RC=$?; return
- fi
- RUN_RC=127
-}
+# Shared aarch64 exec helper — see test/lib/exec_aarch64.sh. Path E queues
+# each linked.exe and we drain the queue in a single batched podman run
+# after the case loop, amortizing the per-launch podman overhead across
+# all ~200 cg cases.
+EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR"
+# shellcheck source=../lib/exec_aarch64.sh
+source "$ROOT/test/lib/exec_aarch64.sh"
# ---- build harness binaries ------------------------------------------------
@@ -221,12 +209,31 @@ else
"$(color_yel warn)" "$BUILD_DIR/cg-check-dwarf.err" >&2
fi
+# Cached start.o — every case used to recompile this from the same source
+# (~40 ms × N cases). Build it once for the whole harness run.
+START_OBJ="$BUILD_DIR/cg_start.o"
+have_start_obj=0
+if [ $have_clang_cross -eq 1 ]; then
+ if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
+ -fno-PIC -fno-pie \
+ -c "$LINK_TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then
+ have_start_obj=1
+ fi
+fi
+
printf 'Running cases...\n'
# ---- per-case loop ---------------------------------------------------------
CASES="$($CG_RUNNER --list)"
+# Path E result bookkeeping. We queue exes during the main loop and verify
+# after a single batched podman flush.
+E_NAMES=()
+E_WORK=()
+E_LINK_MS=()
+E_EXPECTED=()
+
for name in $CASES; do
[ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
work="$BUILD_DIR/cg/$name"
@@ -286,33 +293,30 @@ for name in $CASES; do
fi
fi
- # ---- Path E: link + qemu ----------------------------------------------
+ # ---- Path E: link + (batched) qemu/podman ------------------------------
+ # Link now (per case); the run is queued for the post-loop flush.
if [ $RUN_E -eq 1 ]; then
- if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then
+ if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \
+ && [ $have_start_obj -eq 1 ]; then
t0=$(now_ms)
- start_obj="$work/start.o"
- clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
- -fno-PIC -fno-pie \
- -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null
-
exe="$work/linked.exe"
- if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \
+ if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \
>"$work/exec_link.out" 2>"$work/exec_link.err"; then
dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
note_fail "$name/E (link failed, ${dt}ms)"
elif [ $have_runner -eq 1 ]; then
- run_aarch64 "$exe" "$work/exec.out" "$work/exec.err"
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- if [ "$RUN_RC" -eq "$expected_byte" ]; then
- note_pass "$name/E (${dt}ms)"
- else
- note_fail "$name/E (expected $expected_byte got $RUN_RC, ${dt}ms)"
- fi
+ link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt ))
+ E_NAMES+=("$name")
+ E_WORK+=("$work")
+ E_LINK_MS+=("$link_dt")
+ E_EXPECTED+=("$expected_byte")
+ exec_aarch64_queue "$name" "$exe" \
+ "$work/exec.out" "$work/exec.err" "$work/exec.rc"
else
note_skip "$name/E" "no qemu/podman"
fi
else
- note_skip "$name/E" "no link-exe-runner or aarch64 clang"
+ note_skip "$name/E" "no link-exe-runner, aarch64 clang, or start.o"
fi
fi
@@ -359,11 +363,42 @@ for name in $CASES; do
fi
done
+# ---- batched path-E flush + verification -----------------------------------
+# Run every queued case in a single podman invocation, then iterate the
+# queue to read each exit code and emit PASS/FAIL.
+
+T_E_BATCH=0
+if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then
+ printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)"
+ t0=$(now_ms)
+ exec_aarch64_flush
+ T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
+
+ i=0
+ while [ $i -lt ${#E_NAMES[@]} ]; do
+ name="${E_NAMES[$i]}"
+ work="${E_WORK[$i]}"
+ link_dt="${E_LINK_MS[$i]}"
+ expected_byte="${E_EXPECTED[$i]}"
+ if [ ! -f "$work/exec.rc" ]; then
+ note_fail "$name/E (no rc; podman batch did not produce results)"
+ else
+ RUN_RC="$(cat "$work/exec.rc")"
+ if [ "$RUN_RC" -eq "$expected_byte" ]; then
+ note_pass "$name/E (link ${link_dt}ms)"
+ else
+ note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)"
+ fi
+ fi
+ i=$((i+1))
+ done
+fi
+
# ---- summary ---------------------------------------------------------------
printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: D=%dms R=%dms E=%dms J=%dms W=%dms\n' \
- "$T_D" "$T_R" "$T_E" "$T_J" "$T_W"
+printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms W=%dms\n' \
+ "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J" "$T_W"
if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
printf 'Failed:\n'
diff --git a/test/lib/exec_aarch64.sh b/test/lib/exec_aarch64.sh
@@ -0,0 +1,132 @@
+# test/lib/exec_aarch64.sh — shared aarch64 exec helper for test harnesses.
+#
+# Sourced by test/{link,cg,parse}/run.sh. Provides two execution modes:
+#
+# exec_aarch64_run — synchronous one-shot. Used for kernel images and
+# negative-test bad/ cases that need an immediate rc.
+# exec_aarch64_queue — append to internal queue.
+# exec_aarch64_flush — drain the queue. On podman hosts, single
+# `podman run` loops over all queued exes inside
+# the container, amortizing the ~150 ms per-launch
+# podman client round-trip across the whole suite.
+# On qemu hosts (or when no runner is available),
+# falls back to a serial loop on the host.
+#
+# Caller contract:
+# - Sets the following before sourcing or calling: have_qemu, have_podman,
+# is_aarch64, QEMU_BIN. (Already detected by every harness.)
+# - For batched podman, sets EXEC_AARCH64_MOUNT_ROOT to a host directory
+# that contains *every* exe / out / err / rc path that will be queued.
+# The same path is bind-mounted at the same path inside the container,
+# so no path translation is needed in the manifest.
+# - Optional: RUN_AARCH64_IMAGE overrides the container image
+# (default alpine:latest, matching the prior inline implementation).
+
+# Internal queue arrays. Bash arrays are local to the sourcing shell.
+EXEC_AARCH64_NAMES=()
+EXEC_AARCH64_EXES=()
+EXEC_AARCH64_OUTS=()
+EXEC_AARCH64_ERRS=()
+EXEC_AARCH64_RCS=()
+
+exec_aarch64_supported() {
+ [ "${have_qemu:-0}" -eq 1 ] || [ "${have_podman:-0}" -eq 1 ]
+}
+
+# Synchronous run; sets RUN_RC. Mirrors the prior run_aarch64() in each harness.
+exec_aarch64_run() {
+ local exe="$1" out="$2" err="$3"
+ if [ "${have_qemu:-0}" -eq 1 ]; then
+ "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return
+ fi
+ if [ "${have_podman:-0}" -eq 1 ]; then
+ local dir base platform_flag=()
+ dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")"
+ # `--platform linux/arm64` triggers a registry manifest lookup
+ # (~30 s) even when the local image is already arm64. Only pass it
+ # on hosts where the podman machine isn't natively arm64.
+ [ "${is_aarch64:-0}" -eq 0 ] && platform_flag=(--platform linux/arm64)
+ podman run --rm "${platform_flag[@]}" --net=none \
+ -v "$dir":/work:Z -w /work \
+ "${RUN_AARCH64_IMAGE:-alpine:latest}" "./$base" \
+ >"$out" 2>"$err"
+ RUN_RC=$?; return
+ fi
+ RUN_RC=127
+}
+
+# Queue an exe to run later. Stored verbatim; flush writes <rc_file> with
+# the integer exit code, and routes stdout/stderr to <out_file>/<err_file>.
+exec_aarch64_queue() {
+ EXEC_AARCH64_NAMES+=("$1")
+ EXEC_AARCH64_EXES+=("$2")
+ EXEC_AARCH64_OUTS+=("$3")
+ EXEC_AARCH64_ERRS+=("$4")
+ EXEC_AARCH64_RCS+=("$5")
+}
+
+exec_aarch64_queue_size() { echo "${#EXEC_AARCH64_EXES[@]}"; }
+
+# Drain the queue. Reads back via the .rc files written into the bind-mounted
+# tree; callers iterate their own bookkeeping arrays after this returns.
+exec_aarch64_flush() {
+ local n="${#EXEC_AARCH64_EXES[@]}"
+ [ "$n" -eq 0 ] && return 0
+
+ if [ "${have_qemu:-0}" -eq 1 ]; then
+ local i=0
+ while [ $i -lt "$n" ]; do
+ "$QEMU_BIN" "${EXEC_AARCH64_EXES[$i]}" \
+ >"${EXEC_AARCH64_OUTS[$i]}" 2>"${EXEC_AARCH64_ERRS[$i]}"
+ echo $? >"${EXEC_AARCH64_RCS[$i]}"
+ i=$((i+1))
+ done
+ elif [ "${have_podman:-0}" -eq 1 ]; then
+ if [ -z "${EXEC_AARCH64_MOUNT_ROOT:-}" ]; then
+ echo "exec_aarch64_flush: EXEC_AARCH64_MOUNT_ROOT must be set" >&2
+ return 2
+ fi
+ local platform_flag=()
+ [ "${is_aarch64:-0}" -eq 0 ] && platform_flag=(--platform linux/arm64)
+ local i=0
+ # Manifest is fed via stdin; one tab-separated line per case.
+ # The in-container shell loop runs each exe and writes its rc to
+ # the bind-mounted .rc file, so the host can poll results after
+ # `podman run` returns. stdout/stderr from individual exes go to
+ # their .out/.err files inside the same mount.
+ {
+ while [ $i -lt "$n" ]; do
+ printf '%s\t%s\t%s\t%s\n' \
+ "${EXEC_AARCH64_EXES[$i]}" \
+ "${EXEC_AARCH64_OUTS[$i]}" \
+ "${EXEC_AARCH64_ERRS[$i]}" \
+ "${EXEC_AARCH64_RCS[$i]}"
+ i=$((i+1))
+ done
+ } | podman run -i --rm "${platform_flag[@]}" --net=none \
+ -v "$EXEC_AARCH64_MOUNT_ROOT":"$EXEC_AARCH64_MOUNT_ROOT":Z \
+ "${RUN_AARCH64_IMAGE:-alpine:latest}" \
+ /bin/sh -c '
+set -u
+while IFS=" " read -r exe out err rc; do
+ "$exe" >"$out" 2>"$err"
+ echo $? >"$rc"
+done
+'
+ else
+ # No runner: mark each as 127, matching the prior fallback.
+ local i=0
+ while [ $i -lt "$n" ]; do
+ : >"${EXEC_AARCH64_OUTS[$i]}"
+ : >"${EXEC_AARCH64_ERRS[$i]}"
+ echo 127 >"${EXEC_AARCH64_RCS[$i]}"
+ i=$((i+1))
+ done
+ fi
+
+ EXEC_AARCH64_NAMES=()
+ EXEC_AARCH64_EXES=()
+ EXEC_AARCH64_OUTS=()
+ EXEC_AARCH64_ERRS=()
+ EXEC_AARCH64_RCS=()
+}
diff --git a/test/link/run.sh b/test/link/run.sh
@@ -29,6 +29,11 @@
# gc_present — one symbol per line; verified present post-link
# (jit_runner --check-present for J; readelf -s for E)
# archive_b — package b.o into b.a; content "demand" or "whole"
+# linker_script — basename of an .lds file in the case dir; passed via
+# --linker-script to both runners
+# kernel_image — empty marker; case is a freestanding kernel image.
+# Skips paths R and J; on E, runs the linked exe via
+# qemu-system-aarch64 -kernel … with semihosting.
#
# Filtering:
# ./run.sh [name_filter] [paths]
@@ -112,26 +117,13 @@ arch_raw="$(uname -m 2>/dev/null || true)"
{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
-RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}"
-run_aarch64() {
- local exe="$1" out="$2" err="$3"
- if [ $have_qemu -eq 1 ]; then
- "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return
- fi
- if [ $have_podman -eq 1 ]; then
- local dir base platform_flag=()
- dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")"
- # `--platform linux/arm64` triggers a registry manifest lookup (~30s)
- # even when the local image is already arm64. Only pass it on hosts
- # where the podman machine isn't natively arm64.
- [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64)
- podman run --rm "${platform_flag[@]}" --net=none \
- -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err"
- RUN_RC=$?; return
- fi
- RUN_RC=127
-}
+# Shared aarch64 exec helper. Path E queues each linked.exe and we drain
+# all cases in a single `podman run` after the main loop — amortizes the
+# ~150 ms per-launch podman client overhead across the whole suite.
+EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR"
+# shellcheck source=../lib/exec_aarch64.sh
+source "$ROOT/test/lib/exec_aarch64.sh"
# ---- locate harness binaries ------------------------------------------------
# The Makefile's `test-link` target builds these as proper Make targets so
@@ -166,8 +158,37 @@ if [ $is_aarch64 -eq 1 ]; then
fi
fi
+# Cached start.o — every case used to recompile this from the same source
+# (~40 ms × N cases). Build it once for the whole harness run.
+START_OBJ="$BUILD_DIR/link_start.o"
+have_start_obj=0
+if [ $have_clang_cross -eq 1 ]; then
+ if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
+ -fno-PIC -fno-pie \
+ -c "$TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then
+ have_start_obj=1
+ fi
+fi
+
printf 'Running cases...\n'
+# Path E result bookkeeping. We queue each linked.exe during the main loop
+# and drain them all in one podman invocation after the loop, then verify
+# the recorded exit codes / symbol checks here.
+#
+# bad/ cases are NOT queued: their linker is expected to fail, so the
+# podman runner never gets invoked.
+#
+# The kernel_image case is also not queued: it uses qemu-system-aarch64
+# rather than the user-mode runner and runs synchronously.
+E_NAMES=()
+E_WORK=()
+E_EXE=()
+E_LINK_MS=()
+E_EXPECTED=()
+E_GC_ABSENT_LIST=() # newline-joined per case (empty if none)
+E_GC_PRESENT_LIST=()
+
# ---- per-case loop ---------------------------------------------------------
for case_dir in "$TEST_DIR/cases"/*/; do
@@ -219,12 +240,21 @@ for case_dir in "$TEST_DIR/cases"/*/; do
done < "$case_dir/cflags"
fi
- # Collect source files
+ # Collect source files (.c and .S; clang -c accepts both)
tu_srcs=()
- for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do
+ for f in "$case_dir/entry.S" "$case_dir/a.S" "$case_dir/b.S" \
+ "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do
[ -f "$f" ] && tu_srcs+=("$f")
done
+ # Linker script + kernel-image markers
+ linker_script_file=""
+ if [ -f "$case_dir/linker_script" ]; then
+ linker_script_file="$case_dir/$(cat "$case_dir/linker_script" | tr -d '[:space:]')"
+ fi
+ kernel_image=0
+ [ -f "$case_dir/kernel_image" ] && kernel_image=1
+
# ---- compile with clang cross ------------------------------------------
if [ $have_clang_cross -eq 0 ]; then
note_skip "$name/R" "no aarch64 clang"
@@ -235,7 +265,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do
obj_files=(); compile_ok=1
for src in "${tu_srcs[@]}"; do
- base="$(basename "$src" .c)"
+ base="$(basename "$src")"; base="${base%.c}"; base="${base%.S}"
obj="$work/${base}.o"
if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \
-fno-PIC -fno-pie -fcommon \
@@ -278,7 +308,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do
done
# ---- Path R: roundtrip --------------------------------------------------
- if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ]; then
+ if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ] && [ $kernel_image -eq 0 ]; then
if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then
t0=$(now_ms)
r_ok=1
@@ -307,63 +337,73 @@ for case_dir in "$TEST_DIR/cases"/*/; do
fi
# ---- Path E: exec -------------------------------------------------------
+ # Two stages: link now (per case), then run later in a single batched
+ # podman invocation. Kernel-image cases are an exception — they use
+ # qemu-system-aarch64 and run inline.
if [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then
t0=$(now_ms)
- # Compile start.o
- start_obj="$work/start.o"
- clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
- -fno-PIC -fno-pie \
- -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null
+ script_flags=()
+ if [ -n "$linker_script_file" ]; then
+ script_flags=(--linker-script "$linker_script_file")
+ fi
exe="$work/linked.exe"
- link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" -o "$exe" \
- "${link_obj_files[@]}" "$start_obj" "${link_arc_flags[@]}")
+ if [ $kernel_image -eq 1 ]; then
+ # Freestanding kernel image: no harness start.o, no startup
+ # crt; the case's own entry.S is the program entry.
+ link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \
+ "${script_flags[@]}" -o "$exe" \
+ "${link_obj_files[@]}" "${link_arc_flags[@]}")
+ elif [ $have_start_obj -eq 1 ]; then
+ link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \
+ "${script_flags[@]}" -o "$exe" \
+ "${link_obj_files[@]}" "$START_OBJ" "${link_arc_flags[@]}")
+ else
+ note_skip "$name/E" "no cached start.o"
+ continue
+ fi
if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then
dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
note_fail "$name/E (link failed, ${dt}ms)"
- elif [ $have_runner -eq 1 ]; then
- run_aarch64 "$exe" "$work/exec.out" "$work/exec.err"
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
-
- # Symbol presence/absence checks via readelf -s. The cfree
- # exe writer emits PHDRs only — no .symtab — so on the E path
- # this check is skipped today. (The J path validates presence
- # via cfree_jit_lookup, which is the authoritative check.) We
- # keep the wiring here so the day cfree-link-exe gains a symtab
- # the verification activates without further harness changes.
- e_ok=1
- if [ "$RUN_RC" -ne "$expected" ]; then e_ok=0; fi
- if [ $e_ok -eq 1 ] && [ $have_readelf -eq 1 ] && \
- { [ ${#gc_absent_syms[@]} -gt 0 ] || \
- [ ${#gc_present_syms[@]} -gt 0 ]; }; then
- "$READELF_BIN" -sW "$exe" >"$work/exec_syms.txt" 2>/dev/null
- if [ -s "$work/exec_syms.txt" ]; then
- for sym in "${gc_absent_syms[@]}"; do
- if awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \
- "$work/exec_syms.txt"; then
- e_ok=0
- note_fail "$name/E gc_absent: '$sym' present"
- break
- fi
- done
- if [ $e_ok -eq 1 ]; then
- for sym in "${gc_present_syms[@]}"; do
- if ! awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \
- "$work/exec_syms.txt"; then
- e_ok=0
- note_fail "$name/E gc_present: '$sym' missing"
- break
- fi
- done
- fi
+ elif [ $kernel_image -eq 1 ]; then
+ QEMU_KERNEL_BIN="$(command -v qemu-system-aarch64 2>/dev/null || true)"
+ if [ -z "$QEMU_KERNEL_BIN" ]; then
+ dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
+ note_skip "$name/E" "no qemu-system-aarch64"
+ else
+ "$QEMU_KERNEL_BIN" -machine virt -cpu cortex-a72 \
+ -kernel "$exe" -nographic \
+ -semihosting-config enable=on,target=native \
+ -no-reboot >"$work/exec.out" 2>"$work/exec.err"
+ RUN_RC=$?
+ dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
+ if [ "$RUN_RC" -eq "$expected" ]; then
+ note_pass "$name/E (${dt}ms)"
+ else
+ note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"
fi
fi
-
- if [ $e_ok -eq 1 ]; then
- if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (${dt}ms)"
- else note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"; fi
- fi
+ elif [ $have_runner -eq 1 ]; then
+ # Queue for the post-loop batched flush. Per-case wall-clock
+ # for the run portion is amortized; we record link-time only.
+ link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt ))
+ E_NAMES+=("$name")
+ E_WORK+=("$work")
+ E_EXE+=("$exe")
+ E_LINK_MS+=("$link_dt")
+ E_EXPECTED+=("$expected")
+ # Newline-join the gc symbol lists so we can stash multi-element
+ # data inside scalar array slots (bash arrays-of-arrays don't
+ # exist). Empty string = no checks.
+ gca=""
+ for s in "${gc_absent_syms[@]:-}"; do [ -n "$s" ] && gca="${gca}${s}"$'\n'; done
+ gcp=""
+ for s in "${gc_present_syms[@]:-}"; do [ -n "$s" ] && gcp="${gcp}${s}"$'\n'; done
+ E_GC_ABSENT_LIST+=("$gca")
+ E_GC_PRESENT_LIST+=("$gcp")
+ exec_aarch64_queue "$name" "$exe" \
+ "$work/exec.out" "$work/exec.err" "$work/exec.rc"
else
note_skip "$name/E" "no runner (qemu/podman)"
fi
@@ -374,10 +414,11 @@ for case_dir in "$TEST_DIR/cases"/*/; do
fi
# ---- Path J: JIT --------------------------------------------------------
- if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ]; then
+ if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ] && [ $kernel_image -eq 0 ]; then
t0=$(now_ms)
jit_cmd=("$JIT_RUNNER" "${extra_flags[@]}")
[ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver)
+ [ -n "$linker_script_file" ] && jit_cmd+=(--linker-script "$linker_script_file")
jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}")
"${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err"
@@ -410,7 +451,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do
dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt ))
if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)"
else note_fail "$name/J (expected $expected, got $j_rc, ${dt}ms)"; fi
- elif [ $RUN_J -eq 1 ]; then
+ elif [ $RUN_J -eq 1 ] && [ $kernel_image -eq 0 ]; then
note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)"
fi
@@ -458,25 +499,26 @@ for case_dir in "$TEST_DIR/bad"/*/; do
continue
fi
- # Path E
+ # Path E (negative). Linker is expected to fail; no exe is run, so
+ # this case never enters the podman queue.
if [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then
- start_obj="$work/start.o"
- clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
- -fno-PIC -fno-pie \
- -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null
- exe="$work/linked.exe"
- if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$start_obj" \
- >"$work/link.out" 2>"$work/link.err"; then
- note_fail "$name/E (linker succeeded; expected non-zero exit)"
+ if [ $have_start_obj -eq 0 ]; then
+ note_skip "$name/E" "no cached start.o"
else
- rc=$?
- if [ $rc -ge 128 ]; then
- note_fail "$name/E (linker died via signal $((rc-128)))"
- elif ! grep -qF -- "$expect" "$work/link.err"; then
- note_fail "$name/E (stderr did not contain: $expect)"
- sed 's/^/ | /' "$work/link.err"
+ exe="$work/linked.exe"
+ if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$START_OBJ" \
+ >"$work/link.out" 2>"$work/link.err"; then
+ note_fail "$name/E (linker succeeded; expected non-zero exit)"
else
- note_pass "$name/E"
+ rc=$?
+ if [ $rc -ge 128 ]; then
+ note_fail "$name/E (linker died via signal $((rc-128)))"
+ elif ! grep -qF -- "$expect" "$work/link.err"; then
+ note_fail "$name/E (stderr did not contain: $expect)"
+ sed 's/^/ | /' "$work/link.err"
+ else
+ note_pass "$name/E"
+ fi
fi
fi
elif [ $RUN_E -eq 1 ]; then
@@ -503,11 +545,81 @@ for case_dir in "$TEST_DIR/bad"/*/; do
fi
done
+# ---- batched path-E flush + verification -----------------------------------
+# Run every queued user case in a single podman invocation, then iterate
+# the queue to read each exit code and emit PASS/FAIL. R and J results
+# already printed inline above; E results print together here.
+
+T_E_BATCH=0
+if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then
+ printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)"
+ t0=$(now_ms)
+ exec_aarch64_flush
+ T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
+
+ i=0
+ while [ $i -lt ${#E_NAMES[@]} ]; do
+ name="${E_NAMES[$i]}"
+ work="${E_WORK[$i]}"
+ exe="${E_EXE[$i]}"
+ link_dt="${E_LINK_MS[$i]}"
+ expected="${E_EXPECTED[$i]}"
+
+ if [ ! -f "$work/exec.rc" ]; then
+ note_fail "$name/E (no rc; podman batch did not produce results)"
+ i=$((i+1)); continue
+ fi
+ RUN_RC="$(cat "$work/exec.rc")"
+
+ e_ok=1
+ if [ "$RUN_RC" -ne "$expected" ]; then e_ok=0; fi
+
+ # Symbol presence/absence checks via readelf -s. The cfree exe
+ # writer emits PHDRs only — no .symtab — so today this check is a
+ # no-op for path E (J is the authoritative validator). Wired here
+ # so it activates as soon as cfree-link-exe gains a symtab.
+ if [ $e_ok -eq 1 ] && [ $have_readelf -eq 1 ] && \
+ { [ -n "${E_GC_ABSENT_LIST[$i]}" ] || \
+ [ -n "${E_GC_PRESENT_LIST[$i]}" ]; }; then
+ "$READELF_BIN" -sW "$exe" >"$work/exec_syms.txt" 2>/dev/null
+ if [ -s "$work/exec_syms.txt" ]; then
+ while IFS= read -r sym; do
+ [ -z "$sym" ] && continue
+ if awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \
+ "$work/exec_syms.txt"; then
+ e_ok=0
+ note_fail "$name/E gc_absent: '$sym' present"
+ break
+ fi
+ done <<< "${E_GC_ABSENT_LIST[$i]}"
+ if [ $e_ok -eq 1 ]; then
+ while IFS= read -r sym; do
+ [ -z "$sym" ] && continue
+ if ! awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \
+ "$work/exec_syms.txt"; then
+ e_ok=0
+ note_fail "$name/E gc_present: '$sym' missing"
+ break
+ fi
+ done <<< "${E_GC_PRESENT_LIST[$i]}"
+ fi
+ fi
+ fi
+
+ if [ $e_ok -eq 1 ]; then
+ if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (link ${link_dt}ms)"
+ else note_fail "$name/E (expected $expected, got $RUN_RC, link ${link_dt}ms)"; fi
+ fi
+ i=$((i+1))
+ done
+fi
+
# ---- summary ---------------------------------------------------------------
printf '\n'
printf 'Results: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: R=%dms E=%dms J=%dms\n' "$T_R" "$T_E" "$T_J"
+printf 'Time: R=%dms E=%dms (batch %dms) J=%dms\n' \
+ "$T_R" "$T_E" "$T_E_BATCH" "$T_J"
if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
printf 'Failed:\n'
diff --git a/test/parse/run.sh b/test/parse/run.sh
@@ -97,23 +97,11 @@ arch_raw="$(uname -m 2>/dev/null || true)"
{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
-RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}"
-run_aarch64() {
- local exe="$1" out="$2" err="$3"
- if [ $have_qemu -eq 1 ]; then
- "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return
- fi
- if [ $have_podman -eq 1 ]; then
- local dir base platform_flag=()
- dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")"
- [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64)
- podman run --rm "${platform_flag[@]}" --net=none \
- -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err"
- RUN_RC=$?; return
- fi
- RUN_RC=127
-}
+# Shared aarch64 exec helper — see test/lib/exec_aarch64.sh.
+EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR"
+# shellcheck source=../lib/exec_aarch64.sh
+source "$ROOT/test/lib/exec_aarch64.sh"
# ---- build harness binaries ------------------------------------------------
@@ -179,6 +167,17 @@ if [ $is_aarch64 -eq 1 ]; then
fi
fi
+# Cached start.o — build once for the harness run rather than per case.
+START_OBJ="$BUILD_DIR/parse_start.o"
+have_start_obj=0
+if [ $have_clang_cross -eq 1 ]; then
+ if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
+ -fno-PIC -fno-pie \
+ -c "$LINK_TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then
+ have_start_obj=1
+ fi
+fi
+
printf 'Running cases...\n'
# ---- per-case loop ---------------------------------------------------------
@@ -189,6 +188,12 @@ for src in "$TEST_DIR"/cases/*.c; do
CASES+=("$src")
done
+# Path E result bookkeeping — same shape as test/cg.
+E_NAMES=()
+E_WORK=()
+E_LINK_MS=()
+E_EXPECTED=()
+
for src in "${CASES[@]}"; do
name="$(basename "$src" .c)"
[ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
@@ -256,33 +261,29 @@ for src in "${CASES[@]}"; do
fi
fi
- # ---- Path E: link + qemu/podman --------------------------------------
+ # ---- Path E: link + (batched) qemu/podman ----------------------------
if [ $RUN_E -eq 1 ]; then
- if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then
+ if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \
+ && [ $have_start_obj -eq 1 ]; then
t0=$(now_ms)
- start_obj="$work/start.o"
- clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
- -fno-PIC -fno-pie \
- -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null
-
exe="$work/linked.exe"
- if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \
+ if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \
>"$work/exec_link.out" 2>"$work/exec_link.err"; then
dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
note_fail "$name/E (link failed, ${dt}ms)"
elif [ $have_runner -eq 1 ]; then
- run_aarch64 "$exe" "$work/exec.out" "$work/exec.err"
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- if [ "$RUN_RC" -eq "$expected_byte" ]; then
- note_pass "$name/E (${dt}ms)"
- else
- note_fail "$name/E (expected $expected_byte got $RUN_RC, ${dt}ms)"
- fi
+ link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt ))
+ E_NAMES+=("$name")
+ E_WORK+=("$work")
+ E_LINK_MS+=("$link_dt")
+ E_EXPECTED+=("$expected_byte")
+ exec_aarch64_queue "$name" "$exe" \
+ "$work/exec.out" "$work/exec.err" "$work/exec.rc"
else
note_skip "$name/E" "no qemu/podman"
fi
else
- note_skip "$name/E" "no link-exe-runner or aarch64 clang"
+ note_skip "$name/E" "no link-exe-runner, aarch64 clang, or start.o"
fi
fi
@@ -304,11 +305,40 @@ for src in "${CASES[@]}"; do
fi
done
+# ---- batched path-E flush + verification -----------------------------------
+
+T_E_BATCH=0
+if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then
+ printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)"
+ t0=$(now_ms)
+ exec_aarch64_flush
+ T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
+
+ i=0
+ while [ $i -lt ${#E_NAMES[@]} ]; do
+ name="${E_NAMES[$i]}"
+ work="${E_WORK[$i]}"
+ link_dt="${E_LINK_MS[$i]}"
+ expected_byte="${E_EXPECTED[$i]}"
+ if [ ! -f "$work/exec.rc" ]; then
+ note_fail "$name/E (no rc; podman batch did not produce results)"
+ else
+ RUN_RC="$(cat "$work/exec.rc")"
+ if [ "$RUN_RC" -eq "$expected_byte" ]; then
+ note_pass "$name/E (link ${link_dt}ms)"
+ else
+ note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)"
+ fi
+ fi
+ i=$((i+1))
+ done
+fi
+
# ---- summary ---------------------------------------------------------------
printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: D=%dms R=%dms E=%dms J=%dms\n' \
- "$T_D" "$T_R" "$T_E" "$T_J"
+printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms\n' \
+ "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J"
if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
printf 'Failed:\n'