kit

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

run.sh (26235B)


      1 #!/usr/bin/env bash
      2 # test/parse/run.sh — file-driven C-parser test harness, on the shared corpus
      3 # harness (test/lib/kit_corpus.sh).
      4 #
      5 # For each test/parse/cases/*.c, runs up to six lanes (KIT_TEST_PATHS,
      6 # default DREJ — C and W are opt-in):
      7 #
      8 #   D  in-process JIT — parse-runner --jit FILE.c → exit code matches
      9 #                       expected. No file I/O. Host arch must match cross
     10 #                       target.
     11 #   R  ELF roundtrip  — parse-runner --emit + kit-roundtrip + readelf
     12 #                       normalize diff. Validates emitter+reader fidelity.
     13 #   E  exec via qemu  — parse-runner --emit + start.o → link-exe-runner →
     14 #                       qemu/podman → exit code. Cross-host friendly. Deferred
     15 #                       to a batched exec_target flush (kit_queue_e).
     16 #   J  jit-via-file   — parse-runner --emit + jit-runner. Host arch must match
     17 #                       cross target.
     18 #   C  emit-c host    — parse-runner --emit-c + host cc + test_main wrapper,
     19 #                       run native. Validates the --emit=c C-source backend.
     20 #                       Host arch must match cross target. opt=0 only. Cases
     21 #                       that hit an unimplemented C-target method are reported
     22 #                       as SKIP (not FAIL) so phased backend rollout is
     23 #                       tolerated.
     24 #   W  wasm roundtrip — kit cc -target wasm32-none -c case.c -> .wasm, then
     25 #                       kit run -e test_main on it (the lang/wasm frontend
     26 #                       re-lowers to native CG, JITs, calls test_main).
     27 #                       Exercises the Wasm CGTarget (C -> wasm). Opt-in and
     28 #                       opt=0 only; host arch must match cross target (the
     29 #                       re-lowering JITs for the host). Phased-rollout panics
     30 #                       ("wasm: ... not yet implemented") report SKIP.
     31 #
     32 # Reuses the test/link harness binaries (kit-roundtrip, link-exe-runner,
     33 # jit-runner) and test/link/harness/start.c verbatim.
     34 #
     35 # Sidecar conventions (each missing file uses the documented default):
     36 #   <name>.expected   — integer; default 0. Compared mod 256 to test_main.
     37 #   <name>.skip       — single-line reason. Whole-case skip on every arch.
     38 #   <name>.<arch>.skip— single-line reason; whole-case skip on that arch only
     39 #                       (e.g. asm_01_grammar.rv64.skip).
     40 #   <name>.cbackend.skip — single-line reason; opts the case out of lane C only.
     41 #   <name>.wasm.skip  — single-line reason; opts the case out of lane W only.
     42 # Skips are treated as failure unless KIT_TEST_ALLOW_SKIP=1 (matching the
     43 # rest of the test suite).
     44 #
     45 # Filtering:
     46 #   ./run.sh [name_filter] [paths]
     47 #     name_filter   substring match against case basename
     48 #     paths         subset of "DREJCW" (default "DREJ" — C and W opt-in)
     49 #   Equivalent env vars: KIT_TEST_FILTER, KIT_TEST_PATHS.
     50 #
     51 # Optimization levels:
     52 #   KIT_OPT_LEVELS="0 1"  whitespace-separated levels to test.
     53 #   KIT_OPT_LEVEL=1       compatibility shorthand for one level.
     54 #   Default is "0 1".
     55 #
     56 # Parallelism:
     57 #   default                 run in parallel with a capped CPU-count default.
     58 #   KIT_TEST_JOBS=N       run up to N cases concurrently.
     59 #   KIT_PARSE_PARALLEL=0  force serial dispatch.
     60 #   KIT_CORPUS_TRACE=1    force serial dispatch and print each item/lane before
     61 #                         running it.
     62 # All lane hooks write only under KIT_WORK and record via kit_*, so the runner is
     63 # parallel-safe by construction.
     64 
     65 set -u
     66 
     67 ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
     68 export KIT_LIB_DIR="$ROOT/test/lib"
     69 . "$ROOT/test/lib/kit_corpus.sh"
     70 
     71 TEST_DIR="$ROOT/test/parse"
     72 LINK_TEST_DIR="$ROOT/test/link"
     73 BUILD_DIR="$ROOT/build/test"
     74 LIB_AR="$ROOT/build/libkit.a"
     75 
     76 KIT="${KIT:-$ROOT/build/kit}"
     77 PARSE_RUNNER="$BUILD_DIR/parse-runner"
     78 ROUNDTRIP_BIN="$BUILD_DIR/kit-roundtrip"
     79 LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner"
     80 JIT_RUNNER="$BUILD_DIR/jit-runner"
     81 NORMALIZE="$ROOT/test/elf/normalize.py"
     82 
     83 # KIT_TEST_ARCH selects the cross-target. Default aa64 preserves the
     84 # pre-multiarch behavior. The C runners read the same env via
     85 # test/lib/kit_test_target.h.
     86 KIT_TEST_ARCH="${KIT_TEST_ARCH:-aa64}"
     87 case "$KIT_TEST_ARCH" in
     88     aa64|aarch64|arm64)   TEST_ARCH=aa64;   CLANG_TRIPLE=aarch64-linux-gnu;  EXEC_ARCH=aarch64 ;;
     89     x64|x86_64|amd64)     TEST_ARCH=x64;    CLANG_TRIPLE=x86_64-linux-gnu;   EXEC_ARCH=x64 ;;
     90     rv64|riscv64)         TEST_ARCH=rv64;   CLANG_TRIPLE=riscv64-linux-gnu;  EXEC_ARCH=rv64 ;;
     91     # rv32 is freestanding: the E lane runs bare-metal under qemu-system-riscv32
     92     # via exec_rv32_bare.sh, not exec_target's qemu-user path. CLANG_TRIPLE is
     93     # only for clang probes; the kit target comes from KIT_TEST_ARCH.
     94     rv32|riscv32)         TEST_ARCH=rv32;   CLANG_TRIPLE=riscv32-unknown-elf; EXEC_ARCH=rv32 ;;
     95     *) printf 'unknown KIT_TEST_ARCH=%s\n' "$KIT_TEST_ARCH" >&2; exit 2 ;;
     96 esac
     97 export KIT_TEST_ARCH
     98 
     99 case "$TEST_ARCH" in
    100     aa64) RT_AR="$ROOT/build/rt/aarch64-linux/libkit_rt.a" ;;
    101     x64)  RT_AR="$ROOT/build/rt/x86_64-linux/libkit_rt.a" ;;
    102     rv64) RT_AR="$ROOT/build/rt/riscv64-linux/libkit_rt.a" ;;
    103     rv32) RT_AR="$ROOT/build/rt/riscv32-elf-hardfloat/libkit_rt.a" ;;  # used by exec_rv32_bare
    104 esac
    105 RT_LINK_ARGS=()
    106 if [ -f "$RT_AR" ]; then
    107     RT_LINK_ARGS=(--archive "$RT_AR")
    108 fi
    109 
    110 CLANG_TARGET="--target=$CLANG_TRIPLE"
    111 CC="${CC:-cc}"
    112 
    113 FILTER="${1:-${KIT_TEST_FILTER:-}}"
    114 PATHS="${2:-${KIT_TEST_PATHS:-DREJ}}"
    115 export KIT_TEST_FILTER="$FILTER"
    116 if [ -n "${KIT_OPT_LEVELS:-}" ]; then
    117     OPT_LEVELS="$KIT_OPT_LEVELS"
    118 elif [ -n "${KIT_OPT_LEVEL:-}" ]; then
    119     OPT_LEVELS="$KIT_OPT_LEVEL"
    120 else
    121     OPT_LEVELS="0 1"
    122 fi
    123 for opt in $OPT_LEVELS; do
    124     case "$opt" in
    125         0|1|2) ;;
    126         *) printf 'parse: invalid opt level %s in KIT_OPT_LEVELS\n' "$opt" >&2; exit 2 ;;
    127     esac
    128 done
    129 case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac
    130 case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac
    131 case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
    132 case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
    133 case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac
    134 case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac
    135 
    136 mkdir -p "$BUILD_DIR" "$BUILD_DIR/parse"
    137 
    138 # ---- tool detection (mirrors test/cg/run.sh) -------------------------------
    139 
    140 have_clang_cross=0
    141 have_readelf=0
    142 have_python3=0
    143 have_qemu=0
    144 have_podman=0
    145 have_roundtrip=0
    146 have_exe_runner=0
    147 have_jit_runner=0
    148 is_aarch64=0
    149 
    150 if clang $CLANG_TARGET -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
    151     have_clang_cross=1
    152 fi
    153 command -v llvm-readelf >/dev/null 2>&1 && have_readelf=1
    154 command -v readelf      >/dev/null 2>&1 && have_readelf=1
    155 command -v python3      >/dev/null 2>&1 && have_python3=1
    156 
    157 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
    158 [ -n "$QEMU_BIN" ] && have_qemu=1
    159 command -v podman >/dev/null 2>&1 && have_podman=1
    160 
    161 arch_raw="$(uname -m 2>/dev/null || true)"
    162 { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
    163 
    164 # Host object format for path C: the emitted C is target-locked, so the
    165 # C-target must use the host's obj format (controls ELF-vs-Mach-O choices
    166 # like `__attribute__((alias))` vs a thunk fallback).
    167 case "$(uname -s 2>/dev/null)" in
    168     Darwin) HOST_OBJ_FMT=macho ;;
    169     *)      HOST_OBJ_FMT=elf   ;;
    170 esac
    171 
    172 # is_native_target=1 when the cross-target arch matches the host arch.
    173 # Required for in-process JIT (path D) and the jit-runner (path J).
    174 is_native_target=0
    175 case "$TEST_ARCH" in
    176     aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;;
    177     x64)  { [ "$arch_raw" = "x86_64" ] || [ "$arch_raw" = "amd64" ]; } && is_native_target=1 ;;
    178     rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;;
    179 esac
    180 
    181 READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
    182 
    183 # Shared per-arch exec helper — see test/lib/exec_target.sh.
    184 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    185 export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
    186 # shellcheck source=../lib/exec_target.sh
    187 source "$ROOT/test/lib/exec_target.sh"
    188 
    189 # rv32 is freestanding: the E lane runs bare-metal under qemu-system-riscv32.
    190 if [ "$TEST_ARCH" = "rv32" ]; then
    191     # The parse corpus's entry is test_main() (path C bridges main->test_main).
    192     RV32_BARE_ENTRY=test_main
    193     export RV32_BARE_ENTRY
    194     # shellcheck source=../lib/exec_rv32_bare.sh
    195     . "$ROOT/test/lib/exec_rv32_bare.sh"
    196     rv32_bare_setup "$BUILD_DIR/rv32"
    197 fi
    198 
    199 # ---- harness binaries ------------------------------------------------------
    200 
    201 printf 'Checking harness...\n'
    202 
    203 if [ ! -f "$LIB_AR" ]; then
    204     printf '  FATAL: %s not found — run "make lib" first\n' "$LIB_AR" >&2
    205     exit 1
    206 fi
    207 
    208 # parse-runner
    209 if [ -x "$PARSE_RUNNER" ]; then
    210     printf '  found parse-runner\n'
    211 else
    212     printf '  FATAL parse-runner missing — run "make build/test/parse-runner"\n' >&2
    213     exit 1
    214 fi
    215 
    216 # kit-roundtrip — for path R.
    217 if [ -x "$ROUNDTRIP_BIN" ]; then
    218     have_roundtrip=1
    219     printf '  found kit-roundtrip\n'
    220 else
    221     printf '  warn kit-roundtrip missing — path R will skip\n' >&2
    222 fi
    223 
    224 # link-exe-runner — for path E.
    225 if [ -x "$LINK_EXE_RUNNER" ]; then
    226     have_exe_runner=1
    227     printf '  found link-exe-runner\n'
    228 else
    229     printf '  warn link-exe-runner missing — path E will skip\n' >&2
    230 fi
    231 
    232 # jit-runner — for path J. Only when host arch matches the cross-target.
    233 if [ $is_native_target -eq 1 ]; then
    234     if [ -x "$JIT_RUNNER" ]; then
    235         have_jit_runner=1
    236         printf '  found jit-runner\n'
    237     else
    238         printf '  warn jit-runner missing — path J will skip\n' >&2
    239     fi
    240 fi
    241 
    242 # Cached start.o — build once for the harness run rather than per case.
    243 START_OBJ="$BUILD_DIR/parse_start.o"
    244 have_start_obj=0
    245 if [ $have_clang_cross -eq 1 ]; then
    246     if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \
    247             -fno-PIC -fno-pie \
    248             -c "$LINK_TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then
    249         have_start_obj=1
    250     fi
    251 fi
    252 
    253 # Cached test_main main-wrapper.o — used by path C to link the emitted C
    254 # source against a host-cc-compiled main() that returns test_main()'s value.
    255 # Phase 1 C target only supports native host arch (no cross-emit), so this
    256 # wrapper is built with the host CC, not the cross clang.
    257 C_WRAPPER_SRC="$BUILD_DIR/parse_c_wrapper.c"
    258 C_WRAPPER_OBJ="$BUILD_DIR/parse_c_wrapper.o"
    259 have_c_wrapper=0
    260 if [ ! -f "$C_WRAPPER_SRC" ] || [ ! -s "$C_WRAPPER_SRC" ]; then
    261     cat > "$C_WRAPPER_SRC" <<'EOF'
    262 /* Generated by test/parse/run.sh — bridges main() to test_main() for path C. */
    263 extern int test_main(void);
    264 int main(void) { return test_main(); }
    265 EOF
    266 fi
    267 if $CC -std=c11 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" 2>/dev/null; then
    268     have_c_wrapper=1
    269     printf '  built c-wrapper\n'
    270 else
    271     printf '  warn c-wrapper (host CC failed)\n' >&2
    272 fi
    273 
    274 # Probe whether the host C compiler treats `long double` as 128-bit
    275 # (IEEE binary128). The ldbl128_* fixtures early-return 0 unless
    276 # __LDBL_MANT_DIG__ == 113, and the kit target the C path uses
    277 # (host arch + host OS) matches the host compiler, so a mismatch means
    278 # the test cannot exercise its 128-bit code path and silently turns
    279 # into a return-0 — which then fails the non-zero expected. Skip path C
    280 # for these tests when the host doesn't provide 128-bit ldbl.
    281 HOST_LDBL128=0
    282 LDBL_PROBE_SRC="$BUILD_DIR/parse_ldbl_probe.c"
    283 LDBL_PROBE_BIN="$BUILD_DIR/parse_ldbl_probe"
    284 cat > "$LDBL_PROBE_SRC" <<'EOF'
    285 int main(void) { return __LDBL_MANT_DIG__ == 113 ? 0 : 1; }
    286 EOF
    287 if $CC -std=c11 "$LDBL_PROBE_SRC" -o "$LDBL_PROBE_BIN" 2>/dev/null \
    288         && "$LDBL_PROBE_BIN" 2>/dev/null; then
    289     HOST_LDBL128=1
    290 fi
    291 
    292 # ---- per-lane oracle hooks (KIT_WORK-confined -> parallel-safe) -------------
    293 # Each hook records via kit_pass/kit_fail/kit_skip (or kit_queue_e for E). The
    294 # expected exit code is KIT_EXPECTED (the engine read <name>.expected); compared
    295 # mod 256 to the program's exit code.
    296 
    297 # Build the .o the R/E/J lanes share, once per (case,opt). Sets PARSE_OBJ.
    298 # Returns 0 on success, 1 on failure.
    299 #
    300 # The original harness emitted the .o ONCE up front (before R/E/J) and, on a
    301 # failure, recorded a single FAIL "<name>/emit" and skipped R/E/J entirely. To
    302 # preserve that exact verdict (one FAIL, not one per lane), the first lane that
    303 # needs the object reports "<name>/emit"; a failure marker makes every later
    304 # lane in the same item return silently without re-reporting.
    305 _parse_emit_obj() {
    306     PARSE_OBJ="$KIT_WORK/$KIT_BASE.o"
    307     [ -f "$PARSE_OBJ" ] && return 0
    308     [ -f "$KIT_WORK/.emit.failed" ] && return 1
    309     if ! KIT_OPT_LEVEL="$KIT_OPT" "$PARSE_RUNNER" --emit "$KIT_SRC" "$PARSE_OBJ" \
    310             2>"$KIT_WORK/emit.err"; then
    311         : > "$KIT_WORK/.emit.failed"
    312         kit_fail "$KIT_NAME/emit" "parse-runner --emit failed; see $KIT_WORK/emit.err"
    313         return 1
    314     fi
    315     return 0
    316 }
    317 
    318 kit_lane_D() {
    319     if [ $is_native_target -eq 0 ]; then
    320         kit_skip "$KIT_NAME/D" "host arch != $TEST_ARCH (no native JIT)"
    321         return
    322     fi
    323     local exp_byte t0 dt d_rc
    324     exp_byte=$(( KIT_EXPECTED & 0xff ))
    325     t0=$(kit_now_ms)
    326     KIT_OPT_LEVEL="$KIT_OPT" "$PARSE_RUNNER" --jit "$KIT_SRC" \
    327         >"$KIT_WORK/d.out" 2>"$KIT_WORK/d.err"
    328     d_rc=$?
    329     dt=$(( $(kit_now_ms) - t0 ))
    330     kit_time D "$dt"
    331     if [ "$d_rc" -eq "$exp_byte" ]; then
    332         kit_pass "$KIT_NAME/D (${dt}ms)"
    333     else
    334         kit_fail "$KIT_NAME/D" "expected $exp_byte got $d_rc, ${dt}ms"
    335     fi
    336 }
    337 
    338 kit_lane_R() {
    339     if [ $have_roundtrip -ne 1 ] || [ $have_readelf -ne 1 ] || [ $have_python3 -ne 1 ]; then
    340         kit_skip "$KIT_NAME/R" "missing roundtrip/readelf/python3"
    341         return
    342     fi
    343     _parse_emit_obj || return
    344     local t0 dt rt r_ok r_msg
    345     t0=$(kit_now_ms)
    346     rt="$KIT_WORK/$KIT_BASE.rt.o"
    347     r_ok=1; r_msg=""
    348     if ! "$ROUNDTRIP_BIN" "$PARSE_OBJ" "$rt" 2>"$KIT_WORK/rt.err"; then
    349         r_ok=0; r_msg="roundtrip failed"
    350     else
    351         "$READELF_BIN" -aW "$PARSE_OBJ" | python3 "$NORMALIZE" >"$KIT_WORK/golden.norm" 2>/dev/null
    352         "$READELF_BIN" -aW "$rt"        | python3 "$NORMALIZE" >"$KIT_WORK/rt.norm"     2>/dev/null
    353         diff -u "$KIT_WORK/golden.norm" "$KIT_WORK/rt.norm" >"$KIT_WORK/r.diff" 2>&1 || r_ok=0
    354     fi
    355     dt=$(( $(kit_now_ms) - t0 ))
    356     kit_time R "$dt"
    357     if [ $r_ok -eq 1 ]; then kit_pass "$KIT_NAME/R (${dt}ms)"
    358     else kit_fail "$KIT_NAME/R" "${r_msg} ${dt}ms"; fi
    359 }
    360 
    361 kit_lane_E() {
    362     # rv32: freestanding bare-metal. parse-runner --emit -> kit ld with a startup
    363     # that calls main() and reports its return via a SiFive finisher -> run under
    364     # qemu-system-riscv32 (test/lib/exec_rv32_bare.sh). The qemu exit equals
    365     # main()'s return, so the corpus rc==expected oracle applies. Gaps stay RED.
    366     if [ "$TEST_ARCH" = "rv32" ]; then
    367         local exp_byte rc reason t0 dt run_rc
    368         if [ "${RV32_BARE_OK:-0}" -ne 1 ]; then
    369             kit_skip "$KIT_NAME/E" "no rv32 runner (qemu-system-riscv32)"
    370             return
    371         fi
    372         _parse_emit_obj || return
    373         exp_byte=$(( KIT_EXPECTED & 0xff ))
    374         t0=$(kit_now_ms)
    375         reason="$(rv32_bare_run "$PARSE_OBJ" "$KIT_WORK" "$KIT_WORK/exec.rc")"
    376         run_rc=$?
    377         dt=$(( $(kit_now_ms) - t0 ))
    378         kit_time E "$dt"
    379         if [ "$run_rc" -eq 2 ]; then kit_fail "$KIT_NAME/E" "$reason, ${dt}ms"; return; fi
    380         rc="$(cat "$KIT_WORK/exec.rc" 2>/dev/null || echo 99)"
    381         if [ "$rc" -eq "$exp_byte" ]; then kit_pass "$KIT_NAME/E (${dt}ms)"
    382         else kit_fail "$KIT_NAME/E" "expected $exp_byte got $rc (qemu-system-riscv32), ${dt}ms"; fi
    383         return
    384     fi
    385     if [ $have_exe_runner -ne 1 ] || [ $have_clang_cross -ne 1 ] || [ $have_start_obj -ne 1 ]; then
    386         kit_skip "$KIT_NAME/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"
    387         return
    388     fi
    389     _parse_emit_obj || return
    390     local t0 dt exe exp_byte
    391     exp_byte=$(( KIT_EXPECTED & 0xff ))
    392     t0=$(kit_now_ms)
    393     exe="$KIT_WORK/linked.exe"
    394     if ! "$LINK_EXE_RUNNER" -o "$exe" "$PARSE_OBJ" "$START_OBJ" "${RT_LINK_ARGS[@]}" \
    395             >"$KIT_WORK/exec_link.out" 2>"$KIT_WORK/exec_link.err"; then
    396         dt=$(( $(kit_now_ms) - t0 ))
    397         kit_time E "$dt"
    398         kit_fail "$KIT_NAME/E" "link failed, ${dt}ms"
    399     elif exec_target_supported "$EXEC_ARCH"; then
    400         dt=$(( $(kit_now_ms) - t0 ))
    401         kit_time E "$dt"
    402         # Deferred batched exec: the engine flush runs the exe and verifies
    403         # rc == exp_byte (both masked & 255) at the end.
    404         kit_queue_e "$KIT_NAME/E (link ${dt}ms)" "$exe" \
    405             "$KIT_WORK/exec.out" "$KIT_WORK/exec.err" "$KIT_WORK/exec.rc" \
    406             "$exp_byte" "$EXEC_ARCH"
    407     else
    408         dt=$(( $(kit_now_ms) - t0 ))
    409         kit_time E "$dt"
    410         kit_skip "$KIT_NAME/E" "no runner for $EXEC_ARCH"
    411     fi
    412 }
    413 
    414 kit_lane_J() {
    415     if [ $have_jit_runner -ne 1 ]; then
    416         kit_skip "$KIT_NAME/J" "no jit-runner (host arch != $TEST_ARCH)"
    417         return
    418     fi
    419     _parse_emit_obj || return
    420     local t0 dt j_rc exp_byte
    421     exp_byte=$(( KIT_EXPECTED & 0xff ))
    422     t0=$(kit_now_ms)
    423     "$JIT_RUNNER" "$PARSE_OBJ" "${RT_LINK_ARGS[@]}" >"$KIT_WORK/jit.out" 2>"$KIT_WORK/jit.err"
    424     j_rc=$?
    425     dt=$(( $(kit_now_ms) - t0 ))
    426     kit_time J "$dt"
    427     if [ "$j_rc" -eq "$exp_byte" ]; then
    428         kit_pass "$KIT_NAME/J (${dt}ms)"
    429     else
    430         kit_fail "$KIT_NAME/J" "expected $exp_byte got $j_rc, ${dt}ms"
    431     fi
    432 }
    433 
    434 # Path C: --emit=c + host cc + run. Phase 1 of the C-source backend only
    435 # handles a slice of the CGTarget vtable (doc/CBACKEND.md). Cases that hit an
    436 # unimplemented method panic, surfaced here as SKIP so the pass/fail signal
    437 # reflects the implemented surface. The .cbackend.skip sidecar (handled by the
    438 # engine via the per-lane sidecar with LANE=cbackend) opts a case out of C only.
    439 kit_lane_C() {
    440     # Per-case opt-out for path C: <name>.cbackend.skip. The engine's per-lane
    441     # sidecar check keys on the lane id ("C"), so the cbackend-named sidecar is
    442     # handled here instead.
    443     local reason
    444     if reason=$(kit_skip_sidecar "$KIT_SIDECAR_DIR" "$KIT_BASE" "" cbackend); then
    445         kit_skip "$KIT_NAME/C" "$reason"
    446         return
    447     fi
    448     # ldbl128_* tests assert 128-bit long-double semantics. Skip them on
    449     # path C when the host C compiler doesn't provide 128-bit ldbl (the
    450     # test's `if (__LDBL_MANT_DIG__ != 113) return 0;` early-out can't be
    451     # reconciled with a non-zero expected). ldbl128_01_* are exempt.
    452     if [ $HOST_LDBL128 -eq 0 ] && \
    453             [[ "$KIT_BASE" == ldbl128_* ]] && \
    454             [[ "$KIT_BASE" != ldbl128_01_* ]]; then
    455         kit_skip "$KIT_NAME/C" "host long double is not 128-bit"
    456         return
    457     fi
    458     # Mach-O's static linker rejects unresolved weak undef refs that aren't
    459     # backed by a dylib. ELF lets them resolve to 0 at link time, which is
    460     # what the test expects.
    461     if [ "$HOST_OBJ_FMT" = "macho" ] && [[ "$KIT_BASE" == attr_p2_08_weak_undef ]]; then
    462         kit_skip "$KIT_NAME/C" "Mach-O static link rejects weak undef ref without dylib"
    463         return
    464     fi
    465     # File-scope asm that defines C-visible symbols re-emits verbatim, so it
    466     # defines the bare name (global_x). On Mach-O the C reference picks up the
    467     # leading-underscore (_global_x), so the link can't resolve — a name-mangling
    468     # mismatch the C backend can't bridge without parsing the opaque asm. ELF has
    469     # no such prefix, so the emitted C links and runs there.
    470     # asm_04_register_callee_saved hits the same wall: its file-scope asm defines
    471     # write_saved_reg/read_saved_reg as bare names, but the C calls reference the
    472     # underscored _write_saved_reg/_read_saved_reg on Mach-O, so the link fails.
    473     # Verified otherwise-correct: underscoring the asm labels links clean under
    474     # -Wall -Wextra -Werror and returns the expected 77.
    475     if [ "$HOST_OBJ_FMT" = "macho" ] && \
    476             { [[ "$KIT_BASE" == asm_02_file_scope ]] || \
    477               [[ "$KIT_BASE" == asm_04_register_callee_saved ]]; }; then
    478         kit_skip "$KIT_NAME/C" "Mach-O underscores C symbol refs; verbatim file-scope asm defines the bare name"
    479         return
    480     fi
    481     if [ $have_c_wrapper -eq 0 ]; then
    482         kit_skip "$KIT_NAME/C" "no c-wrapper (host CC failed)"
    483         return
    484     fi
    485     if [ $is_native_target -eq 0 ]; then
    486         kit_skip "$KIT_NAME/C" "host arch != $TEST_ARCH (C target is target-locked)"
    487         return
    488     fi
    489     local t0 dt c_src c_bin c_rc missing exp_byte
    490     exp_byte=$(( KIT_EXPECTED & 0xff ))
    491     t0=$(kit_now_ms)
    492     c_src="$KIT_WORK/$KIT_BASE.kit.c"
    493     c_bin="$KIT_WORK/$KIT_BASE.cbackend.bin"
    494     # Emitted C is target-locked, so we override KIT_TEST_OBJ to the host's
    495     # object format for the --emit-c invocation — otherwise ELF-only constructs
    496     # like __attribute__((alias("x"))) leak into source compiled by a
    497     # Mach-O-targeting host cc and fail at compile time.
    498     if ! KIT_TEST_OBJ="$HOST_OBJ_FMT" "$PARSE_RUNNER" \
    499          --emit-c "$KIT_SRC" "$c_src" \
    500             >"$KIT_WORK/c.emit.out" 2>"$KIT_WORK/c.emit.err"; then
    501         dt=$(( $(kit_now_ms) - t0 ))
    502         kit_time C "$dt"
    503         # Recognize "C target: ... not implemented" and "... not yet supported"
    504         # as phased-rollout skips. Anything else is a real failure.
    505         missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
    506                   "$KIT_WORK/c.emit.err" 2>/dev/null | head -n1 || true)
    507         if [ -n "$missing" ]; then
    508             kit_skip "$KIT_NAME/C" "$missing"
    509         else
    510             kit_fail "$KIT_NAME/C" "parse-runner --emit-c failed; see $KIT_WORK/c.emit.err"
    511         fi
    512     elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
    513             >"$KIT_WORK/c.cc.out" 2>"$KIT_WORK/c.cc.err"; then
    514         dt=$(( $(kit_now_ms) - t0 ))
    515         kit_time C "$dt"
    516         kit_fail "$KIT_NAME/C" "host cc rejected emitted source; see $KIT_WORK/c.cc.err"
    517     else
    518         "$c_bin" >"$KIT_WORK/c.run.out" 2>"$KIT_WORK/c.run.err"
    519         c_rc=$?
    520         dt=$(( $(kit_now_ms) - t0 ))
    521         kit_time C "$dt"
    522         if [ "$c_rc" -eq "$exp_byte" ]; then
    523             kit_pass "$KIT_NAME/C (${dt}ms)"
    524         else
    525             kit_fail "$KIT_NAME/C" "expected $exp_byte got $c_rc, ${dt}ms"
    526         fi
    527     fi
    528 }
    529 
    530 # Path W: cc -target wasm32-none + kit run. Compile the case straight to a
    531 # .wasm via the Wasm CGTarget, then run it with `kit run -e test_main` (the
    532 # lang/wasm frontend re-lowers to native CG, JITs it, and calls test_main).
    533 # Target-agnostic like C, but the re-lowering JITs for the host, so it only
    534 # runs when the host arch matches the cross target. opt=0 only. Phased-rollout
    535 # panics surface as SKIP. The .wasm.skip sidecar (engine per-lane, LANE=wasm)
    536 # opts a case out of W only.
    537 kit_lane_W() {
    538     # Per-case opt-out for path W: <name>.wasm.skip. The engine's per-lane
    539     # sidecar check keys on the lane id ("W"), so the wasm-named sidecar is
    540     # handled here instead.
    541     local reason
    542     if reason=$(kit_skip_sidecar "$KIT_SIDECAR_DIR" "$KIT_BASE" "" wasm); then
    543         kit_skip "$KIT_NAME/W" "$reason"
    544         return
    545     fi
    546     if [ $is_native_target -eq 0 ]; then
    547         kit_skip "$KIT_NAME/W" "host arch != $TEST_ARCH (no native JIT for re-lowering)"
    548         return
    549     fi
    550     local t0 dt wasm w_cc_err w_run_err w_rc w_missing exp_byte
    551     exp_byte=$(( KIT_EXPECTED & 0xff ))
    552     wasm="$KIT_WORK/$KIT_BASE.wasm"
    553     w_cc_err="$KIT_WORK/w.cc.err"
    554     w_run_err="$KIT_WORK/w.run.err"
    555     t0=$(kit_now_ms)
    556     if ! "$KIT" cc -O0 -target wasm32-none -c "$KIT_SRC" -o "$wasm" \
    557             >"$KIT_WORK/w.cc.out" 2>"$w_cc_err"; then
    558         dt=$(( $(kit_now_ms) - t0 )); kit_time W "$dt"
    559         w_missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \
    560                     "$w_cc_err" 2>/dev/null | head -n1 || true)
    561         if [ -n "$w_missing" ]; then
    562             kit_skip "$KIT_NAME/W" "$w_missing"
    563         else
    564             kit_fail "$KIT_NAME/W" "cc -target wasm32-none failed; see $w_cc_err"
    565         fi
    566     else
    567         # Validate by exit code only (like D/J/C). cc stderr is not a failure
    568         # on its own: legitimate non-fatal diagnostics such as `#warning` print
    569         # there while compilation succeeds.
    570         "$KIT" run -e test_main "$wasm" >"$KIT_WORK/w.run.out" 2>"$w_run_err"
    571         w_rc=$?
    572         dt=$(( $(kit_now_ms) - t0 )); kit_time W "$dt"
    573         if [ "$w_rc" -eq "$exp_byte" ]; then
    574             kit_pass "$KIT_NAME/W (${dt}ms)"
    575         else
    576             kit_fail "$KIT_NAME/W" "expected $exp_byte got $w_rc, ${dt}ms"
    577         fi
    578     fi
    579 }
    580 
    581 # ---- drive the corpus ------------------------------------------------------
    582 
    583 # Active lanes in DREJCW order.
    584 LANES=
    585 [ $RUN_D -eq 1 ] && LANES="$LANES D"
    586 [ $RUN_R -eq 1 ] && LANES="$LANES R"
    587 [ $RUN_E -eq 1 ] && LANES="$LANES E"
    588 [ $RUN_J -eq 1 ] && LANES="$LANES J"
    589 [ $RUN_C -eq 1 ] && LANES="$LANES C"
    590 [ $RUN_W -eq 1 ] && LANES="$LANES W"
    591 
    592 # Paths C/W force opt_level=0 internally; running them at every requested opt
    593 # level would duplicate identical work. When C and/or W are the ONLY enabled
    594 # lanes, collapse the opt axis to "0" so the matrix isn't padded with items
    595 # that produce no records. (The engine already gates C/W to opt 0 via
    596 # KIT_OPT0ONLY; this just trims the redundant opt=1 expansion.)
    597 CASE_OPT_LEVELS="$OPT_LEVELS"
    598 if [ $RUN_D -eq 0 ] && [ $RUN_R -eq 0 ] && [ $RUN_E -eq 0 ] && [ $RUN_J -eq 0 ] \
    599         && { [ $RUN_C -eq 1 ] || [ $RUN_W -eq 1 ]; }; then
    600     CASE_OPT_LEVELS="0"
    601 fi
    602 
    603 PAR="${KIT_PARSE_PARALLEL:-1}"
    604 
    605 printf 'test-parse-ok target=%s obj=%s arch=%s\n' "$CLANG_TRIPLE" "$HOST_OBJ_FMT" "$TEST_ARCH"
    606 
    607 KIT_LABEL=test-parse-ok KIT_BUILD_DIR="$BUILD_DIR/parse" \
    608     KIT_CORPUS_GLOBS="$TEST_DIR/cases/*.c" KIT_CORPUS_EXT=c KIT_SIDECAR_DIR="$TEST_DIR/cases" \
    609     KIT_LANES="$LANES" KIT_OPT_LEVELS="$CASE_OPT_LEVELS" KIT_TUPLES="$TEST_ARCH-noobj" \
    610     KIT_OPT0ONLY="C W" KIT_TARGETS_EXT="" KIT_PARALLELIZABLE="$PAR" \
    611     kit_corpus_run
    612 
    613 # Treat skips (cross-target without a runner, the data-model-bound .skip
    614 # sidecars — i128_*/ldbl128_* under ILP32, long-double-aliased targets, ...) as
    615 # non-fatal, matching the toy runner: only a real FAIL (wrong exit code) or an
    616 # XPASS (stale skip) gates the exit. Per-lane opt-ins like KIT_TEST_ALLOW_SKIP=1
    617 # are now redundant but harmless.
    618 KIT_SKIP_IS_FAILURE=0
    619 kit_summary test-parse-ok
    620 kit_exit