kit

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

run.sh (30904B)


      1 #!/usr/bin/env bash
      2 # test/link/run.sh — linker and JIT test harness, on the shared corpus
      3 # harness (test/lib/kit_corpus.sh).
      4 #
      5 # Cases are DIRECTORIES: test/link/cases/<name>/ and bad/<name>/, each holding
      6 # source(s) + marker files. Three lanes per case (KIT_TEST_PATHS, default REJ):
      7 #
      8 #   R  test/link/cases/*/  clang-c → .o → kit-roundtrip → structural diff.
      9 #                          Validates obj reader + writer fidelity.
     10 #   E  clang-c → .o → link-exe-runner (kit_link_exe) → exe → qemu/podman
     11 #                          → check exit code (DEFERRED batched exec, except
     12 #                          kernel_image cases which run a SYNCHRONOUS
     13 #                          qemu-system-* via exec_kernel.sh inline).
     14 #                          Validates linker layout and reloc application.
     15 #   J  clang-c → .o → jit-runner (kit_link_jit) → call test_main()
     16 #                          → check return value (host-native arch only).
     17 #                          Validates JIT path.
     18 #
     19 # Negative tests live in test/link/bad/<name>/ — sources that compile
     20 # cleanly but should cause the linker (E) and JIT (J) to reject. Each bad/
     21 # case requires an `expect` file containing a substring that must appear in
     22 # stderr. The bad/ corpus is a SECOND kit_corpus_run with negative E/J lanes.
     23 #
     24 # Case markers (files in the case directory):
     25 #   expected       — expected exit/return value (default 0)
     26 #   jit_only       — skip R and E paths; run J only
     27 #   use_resolver   — pass --use-resolver to jit-runner (case 28)
     28 #   linker_flags   — one flag per line; passed to link-exe-runner and jit-runner
     29 #   cflags         — extra clang -c flags appended to every TU compile
     30 #   gc_absent      — one symbol per line; verified absent post-link
     31 #                    (jit_runner --check-absent for J; llvm-nm scan for E)
     32 #   gc_present     — one symbol per line; verified present post-link
     33 #                    (jit_runner --check-present for J; llvm-nm scan for E)
     34 #   archive_b      — package b.o into b.a; content "demand" or "whole"
     35 #   linker_script  — basename of an .lds file in the case dir; passed via
     36 #                    --linker-script to both runners. The harness first looks
     37 #                    for a per-arch variant (foo.<arch>.lds) before the literal.
     38 #   kernel_image   — empty marker; case is a freestanding kernel image. Skips
     39 #                    R and J; on E runs the linked exe via a per-arch
     40 #                    qemu-system-* invocation (semihosting on aa64; SIFIVE_TEST
     41 #                    MMIO exit on rv64) SYNCHRONOUSLY.
     42 #   targets        — whole-case applicability: listed <arch>-<obj> tuples (one
     43 #                    per line) run; others print SKIP-NA and don't count.
     44 #   j_targets      — per-lane applicability for J only. Listed tuples run J;
     45 #                    others SKIP-NA for J and continue running R/E.
     46 #   e_targets      — peer of j_targets for lane E.
     47 #
     48 # Per-arch source variants: for each candidate (entry.S, a.S, b.S, a.c, b.c,
     49 # c.c), prefer <name>.<TEST_ARCH>.<ext> if present, else the bare <name>.<ext>.
     50 # Same for any file referenced by linker_script.
     51 #
     52 # Filtering:
     53 #   ./run.sh [name_filter] [paths]
     54 #     name_filter   substring match against case name (e.g. "02", "rodata")
     55 #     paths         subset of "REJ" (default "REJ")
     56 #   Equivalent env vars: KIT_TEST_FILTER, KIT_TEST_PATHS.
     57 #
     58 # All lane hooks write only under KIT_WORK and record via kit_*, so the runner
     59 # is parallel-safe by construction; KIT_LINK_PARALLEL flips dispatch. The
     60 # default is serial (0) because of the cached start.o + synchronous kernel
     61 # path; flipping to 1 is a one-line change since every hook is KIT_WORK-confined.
     62 
     63 set -u
     64 
     65 ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
     66 export KIT_LIB_DIR="$ROOT/test/lib"
     67 . "$ROOT/test/lib/kit_corpus.sh"
     68 
     69 TEST_DIR="$ROOT/test/link"
     70 BUILD_DIR="$ROOT/build/test"
     71 LIB_AR="$ROOT/build/libkit.a"
     72 ROUNDTRIP_BIN_ELF="$ROOT/build/test/kit-roundtrip"
     73 ROUNDTRIP_BIN_MACHO="$ROOT/build/test/kit-roundtrip-macho"
     74 NORMALIZE_ELF="$ROOT/test/elf/normalize.py"
     75 NORMALIZE_MACHO="$ROOT/test/macho/normalize.py"
     76 
     77 LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner"
     78 JIT_RUNNER="$BUILD_DIR/jit-runner"
     79 
     80 # KIT_TEST_ARCH and KIT_TEST_OBJ select the cross-target. Defaults
     81 # aa64+elf preserve the pre-multiarch behavior. The C runners read the
     82 # same env vars via test/lib/kit_test_target.h.
     83 KIT_TEST_ARCH="${KIT_TEST_ARCH:-aa64}"
     84 KIT_TEST_OBJ="${KIT_TEST_OBJ:-elf}"
     85 case "$KIT_TEST_ARCH" in
     86     aa64|aarch64|arm64)   TEST_ARCH=aa64;   EXEC_ARCH=aarch64 ;;
     87     x64|x86_64|amd64)     TEST_ARCH=x64;    EXEC_ARCH=x64 ;;
     88     rv64|riscv64)         TEST_ARCH=rv64;   EXEC_ARCH=rv64 ;;
     89     *) printf 'unknown KIT_TEST_ARCH=%s\n' "$KIT_TEST_ARCH" >&2; exit 2 ;;
     90 esac
     91 case "$KIT_TEST_OBJ" in
     92     elf)
     93         EXEC_OS=linux
     94         case "$TEST_ARCH" in
     95             aa64) CLANG_TRIPLE=aarch64-linux-gnu ;;
     96             x64)  CLANG_TRIPLE=x86_64-linux-gnu ;;
     97             rv64) CLANG_TRIPLE=riscv64-linux-gnu ;;
     98         esac
     99         ROUNDTRIP_BIN="$ROUNDTRIP_BIN_ELF"
    100         NORMALIZE="$NORMALIZE_ELF"
    101         ;;
    102     macho)
    103         EXEC_OS=macos
    104         case "$TEST_ARCH" in
    105             aa64) CLANG_TRIPLE=arm64-apple-macos ;;
    106             x64)  CLANG_TRIPLE=x86_64-apple-macos ;;
    107             rv64) printf 'KIT_TEST_OBJ=macho has no rv64 target\n' >&2; exit 2 ;;
    108         esac
    109         ROUNDTRIP_BIN="$ROUNDTRIP_BIN_MACHO"
    110         NORMALIZE="$NORMALIZE_MACHO"
    111         ;;
    112     *) printf 'unknown KIT_TEST_OBJ=%s\n' "$KIT_TEST_OBJ" >&2; exit 2 ;;
    113 esac
    114 EXEC_TAG="${EXEC_ARCH}-${EXEC_OS}"
    115 export KIT_TEST_ARCH KIT_TEST_OBJ
    116 
    117 CLANG_TARGET="--target=$CLANG_TRIPLE"
    118 
    119 # Filters (env vars or positional args; args win):
    120 #   $1 / KIT_TEST_FILTER — substring match against case name (e.g. "02")
    121 #   $2 / KIT_TEST_PATHS  — subset of "REJ" (default "REJ"); selects paths
    122 FILTER="${1:-${KIT_TEST_FILTER:-}}"
    123 PATHS="${2:-${KIT_TEST_PATHS:-REJ}}"
    124 # kit_corpus_discover filters discovery by KIT_TEST_FILTER (substring vs base).
    125 export KIT_TEST_FILTER="$FILTER"
    126 case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac
    127 case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
    128 case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
    129 
    130 mkdir -p "$BUILD_DIR"
    131 mkdir -p "$BUILD_DIR/link"
    132 
    133 # ---- tool detection --------------------------------------------------------
    134 
    135 have_clang_cross=0
    136 have_readelf=0
    137 have_python3=0
    138 have_qemu=0
    139 have_podman=0
    140 have_runner=0
    141 have_ar=0
    142 have_roundtrip=0
    143 is_aarch64=0
    144 
    145 if clang $CLANG_TARGET -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
    146     have_clang_cross=1
    147 fi
    148 command -v llvm-readelf >/dev/null 2>&1 && have_readelf=1
    149 command -v readelf      >/dev/null 2>&1 && have_readelf=1
    150 command -v python3      >/dev/null 2>&1 && have_python3=1
    151 have_readobj=0
    152 command -v llvm-readobj >/dev/null 2>&1 && have_readobj=1
    153 command -v readobj      >/dev/null 2>&1 && have_readobj=1
    154 # Path R needs the right dump tool for the obj format. ELF wants
    155 # llvm-readelf; Mach-O wants llvm-readobj. The harness exposes a single
    156 # have_dump flag so per-case skip logic doesn't have to branch on the obj.
    157 have_dump=0
    158 case "$KIT_TEST_OBJ" in
    159     elf)   [ $have_readelf -eq 1 ] && have_dump=1 ;;
    160     macho) [ $have_readobj -eq 1 ] && have_dump=1 ;;
    161 esac
    162 # Prefer llvm-ar for archive creation: Apple's /usr/bin/ar requires Mach-O
    163 # members and silently drops ELF objects, breaking the cross-target archive
    164 # cases here.
    165 AR_BIN="$(command -v llvm-ar 2>/dev/null || command -v ar 2>/dev/null || true)"
    166 [ -n "$AR_BIN" ] && have_ar=1
    167 [ -f "$ROUNDTRIP_BIN" ] && have_roundtrip=1
    168 
    169 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
    170 [ -n "$QEMU_BIN" ] && have_qemu=1
    171 command -v podman >/dev/null 2>&1 && have_podman=1
    172 { [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; } && have_runner=1
    173 
    174 arch_raw="$(uname -m 2>/dev/null || true)"
    175 { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
    176 
    177 # is_native_target=1 when the cross-target arch matches the host arch.
    178 # Required for the jit-runner (path J).
    179 is_native_target=0
    180 case "$TEST_ARCH" in
    181     aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;;
    182     x64)  { [ "$arch_raw" = "x86_64" ] || [ "$arch_raw" = "amd64" ]; } && is_native_target=1 ;;
    183     rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;;
    184 esac
    185 
    186 READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
    187 # llvm-nm works on both ELF and Mach-O; format-agnostic tool for the
    188 # gc_present / gc_absent symbol-presence checks. Falls back to plain `nm`.
    189 NM_BIN="$(command -v llvm-nm 2>/dev/null || command -v nm 2>/dev/null || true)"
    190 have_nm=0
    191 [ -n "$NM_BIN" ] && have_nm=1
    192 
    193 # Shared per-arch exec helpers. Path E queues each linked.exe and the engine
    194 # drains all cases in one batched flush per arch (amortizes the ~150 ms
    195 # per-launch podman client overhead). Kernel images run synchronously via
    196 # exec_kernel.sh.
    197 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    198 export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
    199 # shellcheck source=../lib/exec_target.sh
    200 . "$ROOT/test/lib/exec_target.sh"
    201 # shellcheck source=../lib/exec_kernel.sh
    202 . "$ROOT/test/lib/exec_kernel.sh"
    203 
    204 # ---- locate harness binaries -----------------------------------------------
    205 # The Makefile's `test-link` target builds these as proper Make targets so
    206 # they pick up libkit.a changes. Running this script directly without
    207 # `make test-link` is supported but requires the binaries to already exist.
    208 
    209 printf 'Locating harness...\n'
    210 
    211 if [ ! -f "$LIB_AR" ]; then
    212     printf '  FATAL: %s not found — run "make lib" first\n' "$LIB_AR" >&2
    213     exit 1
    214 fi
    215 
    216 have_exe_runner=0
    217 have_jit_runner=0
    218 
    219 if [ -x "$LINK_EXE_RUNNER" ]; then
    220     have_exe_runner=1
    221     printf '  found link-exe-runner\n'
    222 else
    223     printf '  warn link-exe-runner missing (run "make %s")\n' "$LINK_EXE_RUNNER" >&2
    224 fi
    225 
    226 if [ $is_native_target -eq 1 ]; then
    227     if [ -x "$JIT_RUNNER" ]; then
    228         have_jit_runner=1
    229         printf '  found jit-runner\n'
    230     else
    231         printf '  warn jit-runner missing (run "make %s")\n' "$JIT_RUNNER" >&2
    232     fi
    233 fi
    234 
    235 # Cached start.o — every case used to recompile this from the same source
    236 # (~40 ms × N cases). Build it once for the whole harness run. Hooks only
    237 # READ it, so this stays parallel-safe.
    238 START_OBJ="$BUILD_DIR/link_start.$TEST_ARCH.$KIT_TEST_OBJ.o"
    239 have_start_obj=0
    240 if [ $have_clang_cross -eq 1 ]; then
    241     if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
    242             -fno-PIC -fno-pie \
    243             -c "$TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then
    244         have_start_obj=1
    245     fi
    246 fi
    247 
    248 # Mach-O Path E needs libSystem.tbd for the `exit` import in start.c (and any
    249 # libc calls user TUs make). Resolve it via xcrun on Darwin hosts. On non-Darwin
    250 # hosts there is no SDK to point at and Mach-O exec is SKIP anyway, so leaving
    251 # these empty is fine.
    252 MACHO_LIBSYSTEM=""
    253 MACHO_DSO_ARGS=()
    254 if [ "$KIT_TEST_OBJ" = "macho" ]; then
    255     if command -v xcrun >/dev/null 2>&1; then
    256         sdk="$(xcrun --show-sdk-path 2>/dev/null || true)"
    257         if [ -n "$sdk" ] && [ -f "$sdk/usr/lib/libSystem.tbd" ]; then
    258             MACHO_LIBSYSTEM="$sdk/usr/lib/libSystem.tbd"
    259             MACHO_DSO_ARGS=(--dso "$MACHO_LIBSYSTEM")
    260         fi
    261     fi
    262 fi
    263 
    264 CUR_TUPLE="${TEST_ARCH}-${KIT_TEST_OBJ}"
    265 
    266 # ---- per-case marker reading (KIT_READ_CASE) --------------------------------
    267 # Runs once per item, before lanes. Reads markers + per-arch source variants
    268 # into shell vars the lane hooks consume, then compiles the TUs (shared by all
    269 # lanes within the item) into $KIT_WORK. On compile failure, records one FAIL
    270 # for the whole case and sets KIT_SKIP_NA_CASE to suppress all lanes (mirrors
    271 # the original "note_fail; continue").
    272 link_read_case() {
    273     CASE_DIR="${KIT_SRC%/}"          # KIT_SRC is the glob match "cases/<name>/"
    274     NAME="$KIT_BASE"                 # case name (== dir basename)
    275 
    276     # Whole-case applicability via `targets` (literal marker name, no base/ext).
    277     if [ -f "$CASE_DIR/targets" ]; then
    278         applicable=0
    279         for tuple in $(cat "$CASE_DIR/targets"); do
    280             [ "$tuple" = "$CUR_TUPLE" ] && applicable=1
    281         done
    282         if [ $applicable -eq 0 ]; then
    283             # SKIP-NA whole case (uncounted) — target-specific feature with no
    284             # analogue on this tuple.
    285             printf '  SKIP-NA %s — N/A on %s\n' "$NAME" "$CUR_TUPLE"
    286             KIT_SKIP_NA_CASE=1
    287             return
    288         fi
    289     fi
    290 
    291     # Markers
    292     KIT_EXPECTED=0
    293     [ -f "$CASE_DIR/expected" ] && KIT_EXPECTED="$(tr -d '[:space:]' < "$CASE_DIR/expected")"
    294     JIT_ONLY=0;   [ -f "$CASE_DIR/jit_only" ]     && JIT_ONLY=1
    295     USE_RESOLVER=0; [ -f "$CASE_DIR/use_resolver" ] && USE_RESOLVER=1
    296 
    297     # Per-lane applicability — j_targets / e_targets list tuples on which the
    298     # J / E lane can run. R and the other lane still run; only the gated lane
    299     # prints SKIP-NA.
    300     J_APPLICABLE=1
    301     if [ -f "$CASE_DIR/j_targets" ]; then
    302         J_APPLICABLE=0
    303         for tuple in $(cat "$CASE_DIR/j_targets"); do
    304             [ "$tuple" = "$CUR_TUPLE" ] && J_APPLICABLE=1
    305         done
    306     fi
    307     E_APPLICABLE=1
    308     if [ -f "$CASE_DIR/e_targets" ]; then
    309         E_APPLICABLE=0
    310         for tuple in $(cat "$CASE_DIR/e_targets"); do
    311             [ "$tuple" = "$CUR_TUPLE" ] && E_APPLICABLE=1
    312         done
    313     fi
    314 
    315     ARCHIVE_MODE="none"
    316     [ -f "$CASE_DIR/archive_b" ] && ARCHIVE_MODE="$(tr -d '[:space:]' < "$CASE_DIR/archive_b")"
    317 
    318     # Extra linker flags (one per line).
    319     EXTRA_FLAGS=()
    320     if [ -f "$CASE_DIR/linker_flags" ]; then
    321         while IFS= read -r flag; do
    322             [ -n "$flag" ] && EXTRA_FLAGS+=("$flag")
    323         done < "$CASE_DIR/linker_flags"
    324     fi
    325 
    326     # GC-absent / GC-present symbols.
    327     GC_ABSENT_SYMS=()
    328     if [ -f "$CASE_DIR/gc_absent" ]; then
    329         while IFS= read -r sym; do
    330             [ -n "$sym" ] && GC_ABSENT_SYMS+=("$sym")
    331         done < "$CASE_DIR/gc_absent"
    332     fi
    333     GC_PRESENT_SYMS=()
    334     if [ -f "$CASE_DIR/gc_present" ]; then
    335         while IFS= read -r sym; do
    336             [ -n "$sym" ] && GC_PRESENT_SYMS+=("$sym")
    337         done < "$CASE_DIR/gc_present"
    338     fi
    339 
    340     # Per-case extra clang cflags (one token per whitespace).
    341     CASE_CFLAGS=()
    342     if [ -f "$CASE_DIR/cflags" ]; then
    343         while IFS= read -r line; do
    344             for tok in $line; do
    345                 [ -n "$tok" ] && CASE_CFLAGS+=("$tok")
    346             done
    347         done < "$CASE_DIR/cflags"
    348     fi
    349 
    350     KERNEL_IMAGE=0
    351     [ -f "$CASE_DIR/kernel_image" ] && KERNEL_IMAGE=1
    352 
    353     # Source files (.c and .S). For each candidate, prefer the per-arch variant
    354     # (entry.aa64.S beats entry.S when TEST_ARCH=aa64); the bare name is the
    355     # fallback.
    356     TU_SRCS=()
    357     for spec in entry:S a:S b:S a:c b:c c:c; do
    358         base="${spec%%:*}"; ext="${spec##*:}"
    359         if [ -f "$CASE_DIR/${base}.${TEST_ARCH}.${ext}" ]; then
    360             TU_SRCS+=("$CASE_DIR/${base}.${TEST_ARCH}.${ext}")
    361         elif [ -f "$CASE_DIR/${base}.${ext}" ]; then
    362             TU_SRCS+=("$CASE_DIR/${base}.${ext}")
    363         fi
    364     done
    365 
    366     # Linker script (marker content is a basename; derive per-arch variant
    367     # first, fall back to literal).
    368     LINKER_SCRIPT_FILE=""
    369     if [ -f "$CASE_DIR/linker_script" ]; then
    370         ls_base="$(tr -d '[:space:]' < "$CASE_DIR/linker_script")"
    371         ls_stem="${ls_base%.*}"
    372         ls_ext="${ls_base##*.}"
    373         if [ -f "$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}" ]; then
    374             LINKER_SCRIPT_FILE="$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}"
    375         elif [ -f "$CASE_DIR/${ls_base}" ]; then
    376             LINKER_SCRIPT_FILE="$CASE_DIR/${ls_base}"
    377         fi
    378     fi
    379 
    380     # kernel_image cases need an arch-specific entry stub. If the case ships no
    381     # entry.<arch>.S (and no bare entry.S), it's structurally inapplicable here.
    382     if [ $KERNEL_IMAGE -eq 1 ] && \
    383        [ ! -f "$CASE_DIR/entry.${TEST_ARCH}.S" ] && \
    384        [ ! -f "$CASE_DIR/entry.S" ]; then
    385         KIT_SKIP_CASE="kernel_image: no entry.${TEST_ARCH}.S in case"
    386         return
    387     fi
    388 
    389     # ---- compile with clang cross -----------------------------------------
    390     COMPILE_OK=1
    391     if [ $have_clang_cross -eq 0 ]; then
    392         # Original emits per-path SKIP lines (R, optionally E, J) and continues.
    393         kit_skip "$NAME/R" "no $TEST_ARCH clang"
    394         [ $JIT_ONLY -eq 0 ] && kit_skip "$NAME/E" "no $TEST_ARCH clang"
    395         kit_skip "$NAME/J" "no $TEST_ARCH clang"
    396         KIT_SKIP_NA_CASE=1
    397         return
    398     fi
    399 
    400     OBJ_FILES=()
    401     for src in "${TU_SRCS[@]}"; do
    402         base="$(basename "$src")"; base="${base%.c}"; base="${base%.S}"
    403         obj="$KIT_WORK/${base}.o"
    404         if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \
    405                 -fno-PIC -fno-pie -fcommon \
    406                 "${CASE_CFLAGS[@]}" \
    407                 -c "$src" -o "$obj" 2>"$KIT_WORK/compile_${base}.err"; then
    408             COMPILE_OK=0; break
    409         fi
    410         OBJ_FILES+=("$obj")
    411     done
    412 
    413     if [ $COMPILE_OK -eq 0 ]; then
    414         kit_fail "$NAME (compile failed)"
    415         KIT_SKIP_NA_CASE=1
    416         return
    417     fi
    418 
    419     # ---- build archive from b.o if requested ------------------------------
    420     # Roundtrip always uses raw .o files; exec/JIT use the archive.
    421     RT_OBJ_FILES=("${OBJ_FILES[@]}")   # for roundtrip: all .o files
    422     LINK_OBJ_FILES=()                  # for exec/JIT
    423     LINK_ARC_FLAGS=()
    424     for o in "${OBJ_FILES[@]}"; do
    425         base="$(basename "$o" .o)"
    426         if [ "$base" = "b" ] && [ "$ARCHIVE_MODE" != "none" ]; then
    427             if [ $have_ar -eq 1 ]; then
    428                 arc="$KIT_WORK/b.a"
    429                 "$AR_BIN" rcs "$arc" "$o" 2>/dev/null
    430                 if [ "$ARCHIVE_MODE" = "whole" ]; then
    431                     LINK_ARC_FLAGS+=(--whole-archive --archive "$arc")
    432                 else
    433                     LINK_ARC_FLAGS+=(--archive "$arc")
    434                 fi
    435             else
    436                 LINK_OBJ_FILES+=("$o")
    437             fi
    438         else
    439             LINK_OBJ_FILES+=("$o")
    440         fi
    441     done
    442 }
    443 
    444 # ---- cases-corpus lanes ----------------------------------------------------
    445 
    446 # R — roundtrip + golden structural diff. Skipped (no record) for jit_only and
    447 # kernel_image cases, matching the original guards.
    448 kit_lane_R() {
    449     [ $JIT_ONLY -eq 1 ] && return
    450     [ $KERNEL_IMAGE -eq 1 ] && return
    451     if [ $have_roundtrip -eq 1 ] && [ $have_dump -eq 1 ] && [ $have_python3 -eq 1 ]; then
    452         local t0; t0=$(kit_now_ms)
    453         local r_ok=1 obj base rt
    454         for obj in "${RT_OBJ_FILES[@]}"; do
    455             base="$(basename "$obj" .o)"
    456             rt="$KIT_WORK/${base}_rt.o"
    457             if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$KIT_WORK/rt_${base}.err"; then
    458                 r_ok=0; break
    459             fi
    460             if [ "$KIT_TEST_OBJ" = "macho" ]; then
    461                 python3 "$NORMALIZE" "$obj" >"$KIT_WORK/${base}_golden.norm" 2>/dev/null
    462                 python3 "$NORMALIZE" "$rt"  >"$KIT_WORK/${base}_rt.norm"     2>/dev/null
    463             else
    464                 "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \
    465                     >"$KIT_WORK/${base}_golden.norm" 2>/dev/null
    466                 "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \
    467                     >"$KIT_WORK/${base}_rt.norm" 2>/dev/null
    468             fi
    469             if ! diff -u "$KIT_WORK/${base}_golden.norm" "$KIT_WORK/${base}_rt.norm" \
    470                      >"$KIT_WORK/${base}_diff.txt" 2>&1; then
    471                 r_ok=0; break
    472             fi
    473         done
    474         kit_time R "$(( $(kit_now_ms) - t0 ))"
    475         if [ $r_ok -eq 1 ]; then kit_pass "$NAME/R"
    476         else kit_fail "$NAME/R"; fi
    477     else
    478         kit_skip "$NAME/R" "missing roundtrip/dump-tool/python3"
    479     fi
    480 }
    481 
    482 # E — link now; defer exec (batched) or run kernel synchronously. Branch
    483 # precedence mirrors the original if/elif/elif: the e_targets SKIP-NA check
    484 # (non-kernel, inapplicable tuple) fires first regardless of jit_only or runner
    485 # availability; then jit_only cases produce no E record; then the link path.
    486 kit_lane_E() {
    487     # e_targets SKIP-NA (uncounted) for non-kernel cases on inapplicable tuples.
    488     if [ $E_APPLICABLE -eq 0 ] && [ $KERNEL_IMAGE -eq 0 ]; then
    489         printf '  SKIP-NA %s/E — N/A on %s\n' "$NAME" "$CUR_TUPLE"
    490         kit_skip_na "$NAME/E"
    491         return
    492     fi
    493     # jit_only cases skip E entirely (no record).
    494     [ $JIT_ONLY -eq 1 ] && return
    495     if [ $have_exe_runner -eq 0 ]; then
    496         kit_skip "$NAME/E" "no link-exe-runner"
    497         return
    498     fi
    499 
    500     local t0; t0=$(kit_now_ms)
    501     local script_flags=()
    502     [ -n "$LINKER_SCRIPT_FILE" ] && script_flags=(--linker-script "$LINKER_SCRIPT_FILE")
    503 
    504     local exe="$KIT_WORK/linked.exe"
    505     local link_cmd
    506     if [ $KERNEL_IMAGE -eq 1 ]; then
    507         # Freestanding kernel image: no harness start.o; the case's own entry.S
    508         # is the program entry.
    509         link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \
    510                   "${script_flags[@]}" -o "$exe" \
    511                   "${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}")
    512     elif [ $have_start_obj -eq 1 ]; then
    513         link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \
    514                   "${script_flags[@]}" -o "$exe")
    515         [ ${#MACHO_DSO_ARGS[@]} -gt 0 ] && link_cmd+=("${MACHO_DSO_ARGS[@]}")
    516         link_cmd+=("${LINK_OBJ_FILES[@]}" "$START_OBJ" "${LINK_ARC_FLAGS[@]}")
    517     else
    518         kit_skip "$NAME/E" "no cached start.o"
    519         return
    520     fi
    521 
    522     if ! "${link_cmd[@]}" >"$KIT_WORK/exec_link.out" 2>"$KIT_WORK/exec_link.err"; then
    523         kit_time E "$(( $(kit_now_ms) - t0 ))"
    524         kit_fail "$NAME/E" "link failed"
    525     elif [ $KERNEL_IMAGE -eq 1 ]; then
    526         if ! exec_kernel_supported "$TEST_ARCH"; then
    527             kit_time E "$(( $(kit_now_ms) - t0 ))"
    528             kit_skip "$NAME/E" "no qemu-system-* for $TEST_ARCH"
    529         else
    530             exec_kernel_run "$TEST_ARCH" "$exe" "$KIT_WORK/exec.out" "$KIT_WORK/exec.err"
    531             kit_time E "$(( $(kit_now_ms) - t0 ))"
    532             if [ "$RUN_RC" -eq "$KIT_EXPECTED" ]; then kit_pass "$NAME/E"
    533             else kit_fail "$NAME/E" "expected $KIT_EXPECTED got $RUN_RC"; fi
    534         fi
    535     elif [ $have_runner -eq 1 ]; then
    536         kit_time E "$(( $(kit_now_ms) - t0 ))"
    537         # Defer to the batched flush. Payload carries the gc symbol-check sets,
    538         # space-joined per list and '|'-separated (absent|present), verified
    539         # post-flush by link_flush_verify.
    540         local gca="" gcp="" s
    541         for s in "${GC_ABSENT_SYMS[@]:-}";  do [ -n "$s" ] && gca="${gca:+$gca }$s"; done
    542         for s in "${GC_PRESENT_SYMS[@]:-}"; do [ -n "$s" ] && gcp="${gcp:+$gcp }$s"; done
    543         kit_queue_e "$NAME/E" "$exe" "$KIT_WORK/exec.out" "$KIT_WORK/exec.err" \
    544             "$KIT_WORK/exec.rc" "$KIT_EXPECTED" "$EXEC_TAG" "${gca}|${gcp}|$exe"
    545     else
    546         kit_skip "$NAME/E" "no runner (qemu/podman)"
    547     fi
    548 }
    549 
    550 # J — JIT runner + inline gc symbol checks. Skipped (no record) for kernel.
    551 kit_lane_J() {
    552     [ $KERNEL_IMAGE -eq 1 ] && return
    553     # j_targets SKIP-NA (uncounted) on inapplicable tuples.
    554     if [ $J_APPLICABLE -eq 0 ]; then
    555         printf '  SKIP-NA %s/J — N/A on %s\n' "$NAME" "$CUR_TUPLE"
    556         kit_skip_na "$NAME/J"
    557         return
    558     fi
    559     if [ $have_jit_runner -eq 0 ]; then
    560         kit_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
    561         return
    562     fi
    563 
    564     local t0; t0=$(kit_now_ms)
    565     local jit_cmd=("$JIT_RUNNER" "${EXTRA_FLAGS[@]}")
    566     [ $USE_RESOLVER -eq 1 ] && jit_cmd+=(--use-resolver)
    567     [ -n "$LINKER_SCRIPT_FILE" ] && jit_cmd+=(--linker-script "$LINKER_SCRIPT_FILE")
    568     jit_cmd+=("${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}")
    569 
    570     "${jit_cmd[@]}" >"$KIT_WORK/jit.out" 2>"$KIT_WORK/jit.err"
    571     local j_rc=$? sym
    572     # gc_absent / gc_present checks via the jit_runner's --check-* flags.
    573     for sym in "${GC_ABSENT_SYMS[@]:-}"; do
    574         [ -z "$sym" ] && continue
    575         if "${jit_cmd[@]}" --check-absent "$sym" \
    576            >"$KIT_WORK/jit_gc.out" 2>"$KIT_WORK/jit_gc.err"; then
    577             :
    578         else
    579             j_rc=$?
    580             break
    581         fi
    582     done
    583     if [ "$j_rc" -eq "$KIT_EXPECTED" ]; then
    584         for sym in "${GC_PRESENT_SYMS[@]:-}"; do
    585             [ -z "$sym" ] && continue
    586             if "${jit_cmd[@]}" --check-present "$sym" \
    587                >"$KIT_WORK/jit_gc.out" 2>"$KIT_WORK/jit_gc.err"; then
    588                 :
    589             else
    590                 j_rc=$?
    591                 break
    592             fi
    593         done
    594     fi
    595     kit_time J "$(( $(kit_now_ms) - t0 ))"
    596     if [ "$j_rc" -eq "$KIT_EXPECTED" ]; then kit_pass "$NAME/J"
    597     else kit_fail "$NAME/J" "expected $KIT_EXPECTED got $j_rc"; fi
    598 }
    599 
    600 # ---- deferred-E flush verification (gc symbol presence/absence) ------------
    601 # Called per queued-E item after the batched exec flush, ONLY when rc==expected
    602 # (the engine's rc check passed). Runs the gc symbol presence/absence checks via
    603 # llvm-nm (format-agnostic). Payload = "<absent syms>|<present syms>|<exe>".
    604 # Returns 0 to keep the engine's PASS, 1 to flip it to FAIL — we do NOT call
    605 # kit_fail ourselves (the engine records the single verdict).
    606 link_flush_verify() {
    607     local payload="$2"
    608     local gca="${payload%%|*}"; payload="${payload#*|}"
    609     local gcp="${payload%%|*}"; local exe="${payload#*|}"
    610     { [ -z "$gca" ] && [ -z "$gcp" ]; } && return 0
    611     [ $have_nm -eq 1 ] || return 0
    612 
    613     # Write the symbol dump next to the exe (its dir is the persistent per-item
    614     # KIT_WORK; parallel-safe).
    615     local syms="$exe.syms"
    616     "$NM_BIN" "$exe" >"$syms" 2>/dev/null
    617     [ -s "$syms" ] || return 0
    618 
    619     # llvm-nm format: "<addr> <type> <name>". `U` = undefined. On Mach-O the
    620     # asm form of source `name` is `_name`; accept either.
    621     local sym
    622     for sym in $gca; do
    623         if awk -v s="$sym" -v u="_$sym" \
    624             '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then
    625             return 1   # gc_absent symbol unexpectedly present
    626         fi
    627     done
    628     for sym in $gcp; do
    629         if ! awk -v s="$sym" -v u="_$sym" \
    630             '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then
    631             return 1   # gc_present symbol missing
    632         fi
    633     done
    634     return 0
    635 }
    636 
    637 # ---- bad/ negative lanes ---------------------------------------------------
    638 # Sources compile cleanly but the linker (E) and JIT (J) must reject: non-zero
    639 # exit with no signal AND stderr contains the substring from `expect`.
    640 
    641 bad_read_case() {
    642     CASE_DIR="${KIT_SRC%/}"
    643     NAME="bad/$KIT_BASE"
    644 
    645     # Whole-case applicability via `targets` (same mechanism as link_read_case):
    646     # a negative case may be format-specific (e.g. an ELF-only TLS scenario with
    647     # no Mach-O/COFF analogue).  Listed <arch>-<obj> tuples run; others SKIP-NA.
    648     if [ -f "$CASE_DIR/targets" ]; then
    649         applicable=0
    650         for tuple in $(cat "$CASE_DIR/targets"); do
    651             [ "$tuple" = "$CUR_TUPLE" ] && applicable=1
    652         done
    653         if [ $applicable -eq 0 ]; then
    654             printf '  SKIP-NA %s — N/A on %s\n' "$NAME" "$CUR_TUPLE"
    655             KIT_SKIP_NA_CASE=1
    656             return
    657         fi
    658     fi
    659 
    660     EXPECT_FILE="$CASE_DIR/expect"
    661     if [ ! -f "$EXPECT_FILE" ]; then
    662         kit_fail "$NAME (missing $EXPECT_FILE)"
    663         KIT_SKIP_NA_CASE=1
    664         return
    665     fi
    666     EXPECT="$(cat "$EXPECT_FILE")"
    667 
    668     if [ $have_clang_cross -eq 0 ]; then
    669         kit_skip "$NAME" "no $TEST_ARCH clang"
    670         KIT_SKIP_NA_CASE=1
    671         return
    672     fi
    673 
    674     TU_SRCS=()
    675     for f in "$CASE_DIR/a.c" "$CASE_DIR/b.c" "$CASE_DIR/c.c"; do
    676         [ -f "$f" ] && TU_SRCS+=("$f")
    677     done
    678 
    679     OBJ_FILES=(); COMPILE_OK=1
    680     for src in "${TU_SRCS[@]}"; do
    681         base="$(basename "$src" .c)"
    682         obj="$KIT_WORK/${base}.o"
    683         if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \
    684                 -fno-PIC -fno-pie -fcommon \
    685                 -c "$src" -o "$obj" 2>"$KIT_WORK/compile_${base}.err"; then
    686             COMPILE_OK=0; break
    687         fi
    688         OBJ_FILES+=("$obj")
    689     done
    690     if [ $COMPILE_OK -eq 0 ]; then
    691         kit_fail "$NAME (compile failed; sources should compile, only link is expected to fail)"
    692         KIT_SKIP_NA_CASE=1
    693         return
    694     fi
    695 }
    696 
    697 kit_lane_BE() {  # bad/ Path E (negative)
    698     if [ $have_exe_runner -eq 0 ]; then
    699         kit_skip "$NAME/E" "no link-exe-runner"
    700         return
    701     fi
    702     if [ $have_start_obj -eq 0 ]; then
    703         kit_skip "$NAME/E" "no cached start.o"
    704         return
    705     fi
    706     local exe="$KIT_WORK/linked.exe"
    707     if "$LINK_EXE_RUNNER" -o "$exe" "${OBJ_FILES[@]}" "$START_OBJ" \
    708             >"$KIT_WORK/link.out" 2>"$KIT_WORK/link.err"; then
    709         kit_fail "$NAME/E" "linker succeeded; expected non-zero exit"
    710     else
    711         local rc=$?
    712         if [ $rc -ge 128 ]; then
    713             kit_fail "$NAME/E" "linker died via signal $((rc-128))"
    714         elif ! grep -qF -- "$EXPECT" "$KIT_WORK/link.err"; then
    715             kit_fail "$NAME/E" "stderr did not contain: $EXPECT"
    716             sed 's/^/    | /' "$KIT_WORK/link.err"
    717         else
    718             kit_pass "$NAME/E"
    719         fi
    720     fi
    721 }
    722 
    723 kit_lane_BJ() {  # bad/ Path J (negative)
    724     if [ $have_jit_runner -eq 0 ]; then
    725         kit_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
    726         return
    727     fi
    728     if "$JIT_RUNNER" "${OBJ_FILES[@]}" >"$KIT_WORK/jit.out" 2>"$KIT_WORK/jit.err"; then
    729         kit_fail "$NAME/J" "jit-runner succeeded; expected non-zero exit"
    730     else
    731         local rc=$?
    732         if [ $rc -ge 128 ]; then
    733             kit_fail "$NAME/J" "jit-runner died via signal $((rc-128))"
    734         elif ! grep -qF -- "$EXPECT" "$KIT_WORK/jit.err"; then
    735             kit_fail "$NAME/J" "stderr did not contain: $EXPECT"
    736             sed 's/^/    | /' "$KIT_WORK/jit.err"
    737         else
    738             kit_pass "$NAME/J"
    739         fi
    740     fi
    741 }
    742 
    743 # ---- drive the corpora -----------------------------------------------------
    744 printf 'Running cases...\n'
    745 
    746 PAR="${KIT_LINK_PARALLEL:-0}"
    747 
    748 # Active lanes for the cases corpus, in REJ order.
    749 CASE_LANES=
    750 [ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R"
    751 [ "$RUN_E" -eq 1 ] && CASE_LANES="$CASE_LANES E"
    752 [ "$RUN_J" -eq 1 ] && CASE_LANES="$CASE_LANES J"
    753 
    754 KIT_LABEL=test-link KIT_BUILD_DIR="$BUILD_DIR/link" \
    755   KIT_CORPUS_GLOBS="$TEST_DIR/cases/*/" KIT_CORPUS_EXT="" KIT_SIDECAR_DIR="$TEST_DIR/cases" \
    756   KIT_LANES="$CASE_LANES" KIT_OPT_LEVELS="" KIT_TUPLES="$CUR_TUPLE" \
    757   KIT_TARGETS_EXT="" KIT_READ_CASE=link_read_case KIT_FLUSH_VERIFY=link_flush_verify \
    758   KIT_PARALLELIZABLE="$PAR" \
    759   kit_corpus_run
    760 
    761 # bad/ negative corpus — E and J only (no R).
    762 BAD_LANES=
    763 [ "$RUN_E" -eq 1 ] && BAD_LANES="$BAD_LANES BE"
    764 [ "$RUN_J" -eq 1 ] && BAD_LANES="$BAD_LANES BJ"
    765 
    766 if [ -n "$BAD_LANES" ]; then
    767     KIT_LABEL=test-link KIT_BUILD_DIR="$BUILD_DIR/link/bad" \
    768       KIT_CORPUS_GLOBS="$TEST_DIR/bad/*/" KIT_CORPUS_EXT="" KIT_SIDECAR_DIR="$TEST_DIR/bad" \
    769       KIT_LANES="$BAD_LANES" KIT_OPT_LEVELS="" KIT_TUPLES="$CUR_TUPLE" \
    770       KIT_TARGETS_EXT="" KIT_READ_CASE=bad_read_case \
    771       KIT_PARALLELIZABLE="$PAR" \
    772       kit_corpus_run
    773 fi
    774 
    775 kit_summary test-link
    776 kit_exit