kit

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

run.sh (33548B)


      1 #!/usr/bin/env bash
      2 # test/toy/run.sh — .toy frontend end-to-end tests, on the shared corpus
      3 # harness (test/lib/kit_corpus.sh). Lanes (KIT_TEST_PATHS, default RLCWI):
      4 #   R  kit run -O{level} case.toy                          (JIT native)
      5 #   I  kit run --no-jit -O{level} case.toy                 (IR interpreter)
      6 #      Ops the interpreter does not yet implement SKIP (greppable
      7 #      "interp: <feature> not supported"), like paths C/W.
      8 #   L  kit cc -O{level} -c -> kit ld -> native exec. Has a <name>.objdump
      9 #      sidecar substring check (separate :objdump verdict) and a
     10 #      <name>.link.skip sidecar.
     11 #   X  cross-arch: kit cc -O{level} -target -> kit ld -> exec_target for the
     12 #      aa64/x64/rv64 Linux targets. The freestanding _start/TLS stub is also
     13 #      built by kit cc -target (no host toolchain), so the whole path — case
     14 #      object, startup stub, and link — is kit end to end. Exec is deferred to
     15 #      the engine's batched exec_target flush (kit_queue_e). Opt-in (not in the
     16 #      default paths).
     17 #   C  kit cc --emit=c case.toy -> host cc -> native exec. Exercises the
     18 #      --emit=c C-source backend driven by a non-C frontend (validates that the
     19 #      CGTarget seam is frontend-agnostic). Phased-rollout panics from the C
     20 #      target report as SKIP. Host cc runs under -Wall -Wextra -Werror; fixtures
     21 #      wrap their i64 main in a small i32 thunk so the emitted
     22 #      `int32_t main(void)` satisfies the standard. opt-0 only.
     23 #   W  kit cc -target wasm32-none -c case.toy -> .wasm; then kit run on the
     24 #      .wasm (routes through the lang/wasm frontend back to native CG, JITs, and
     25 #      invokes main). Exercises the Wasm backend (Toy -> wasm). opt-0 only.
     26 #      Most cases hit unimplemented Wasm lowerings -> phased-rollout SKIP.
     27 #
     28 # Sidecars:
     29 #   <name>.expected         expected process exit code, default 0
     30 #   <name>.objdump          fixed substrings expected in `kit objdump -h -t`
     31 #                           after the linked-object compile path (lane L)
     32 #   <name>.cbackend.skip    opts the case out of path C (with reason)
     33 #   <name>.wasm.skip        opts the case out of path W (with reason)
     34 #   <name>.link.skip        opts the case out of path L (with reason)
     35 #   <name>.link.hosted      forces path L to link hosted (kit cc -lc) for this
     36 #                           case regardless of platform default (with reason)
     37 #   err/<name>.expected     expected diagnostic substring for compile-fail cases
     38 #   err/<name>.ccargs       extra `kit cc -c` flags for that err case (e.g. a
     39 #                           -target triple for a target-specific diagnostic)
     40 #
     41 # Filtering:
     42 #   ./run.sh [name_filter] [paths]
     43 #   KIT_TEST_FILTER / KIT_TEST_PATHS, where paths is a subset of "RLXCWI".
     44 #   X is opt-in cross-arch cc+ld+exec for aa64, x64, and rv64.
     45 #   C and W run only at O0 even when included with other opt levels.
     46 #   Default paths are "RLCWI"; override with KIT_TEST_PATHS.
     47 #   KIT_OPT_LEVELS selects optimization levels.
     48 #
     49 # Every lane hook writes only under KIT_WORK and records via kit_*, so the runner
     50 # is parallel-safe; KIT_TOY_PARALLEL flips dispatch.
     51 
     52 set -u
     53 
     54 ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
     55 export KIT_LIB_DIR="$ROOT/test/lib"
     56 . "$ROOT/test/lib/kit_corpus.sh"
     57 
     58 TEST_DIR="$ROOT/test/toy"
     59 BUILD_DIR="$ROOT/build/test/toy"
     60 KIT="${KIT:-$ROOT/build/kit}"
     61 
     62 FILTER="${1:-${KIT_TEST_FILTER:-}}"
     63 PATHS="${2:-${KIT_TEST_PATHS:-RLCWI}}"
     64 case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac
     65 case "$PATHS" in *L*) RUN_L=1;; *) RUN_L=0;; esac
     66 case "$PATHS" in *X*) RUN_X=1;; *) RUN_X=0;; esac
     67 case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac
     68 case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac
     69 case "$PATHS" in *I*) RUN_I=1;; *) RUN_I=0;; esac
     70 TOY_CROSS_ARCHS="${KIT_TOY_CROSS_ARCHS:-aa64 x64 rv64 rv32}"
     71 TOY_OPT_LEVELS="${KIT_OPT_LEVELS:-0 1}"
     72 HOST_CC="${CC:-cc}"
     73 PAR="${KIT_TOY_PARALLEL:-1}"
     74 
     75 # Is the host C compiler gcc (vs clang)? GCC's tail-call checker rejects the
     76 # c_target's `__attribute__((musttail)) return <sret-call>` ("cannot tail-call:
     77 # return value used after call") where clang honors it, so musttail+sret cases
     78 # carry a `.cbackend.gcc.skip` sidecar that the C lane respects only on gcc.
     79 case "$($HOST_CC --version 2>&1 | head -n1)" in
     80     *clang*) TOY_HOST_CC_IS_GCC=0 ;;
     81     *)       TOY_HOST_CC_IS_GCC=1 ;;
     82 esac
     83 
     84 # Lane L links the kit object and runs the result natively. A freestanding
     85 # `kit ld` (no crt) is runnable where the format drives the entry to `main`
     86 # directly (Mach-O's LC_MAIN), but a native ELF executable needs a crt-provided
     87 # `_start`, so on ELF hosts we drive the link through `kit cc -lc` (kit's hosted
     88 # profile pulls the crt that calls main). kit ld is still exercised -- cc just
     89 # invokes it with the crt/libc inputs. Override with KIT_TOY_L_HOSTED=0|1.
     90 case "$(uname -s)" in
     91     Darwin) TOY_L_HOSTED_DEFAULT=0 ;;
     92     *)      TOY_L_HOSTED_DEFAULT=1 ;;
     93 esac
     94 TOY_L_HOSTED="${KIT_TOY_L_HOSTED:-$TOY_L_HOSTED_DEFAULT}"
     95 
     96 # The engine's KIT_TEST_FILTER drives discovery; honor the positional filter.
     97 export KIT_TEST_FILTER="$FILTER"
     98 
     99 shopt -s nullglob
    100 mkdir -p "$BUILD_DIR"
    101 
    102 if [ ! -x "$KIT" ]; then
    103     printf 'missing kit binary: %s\n' "$KIT" >&2
    104     exit 2
    105 fi
    106 
    107 # ---- shared rc oracle (KIT_WORK-confined -> parallel-safe) -------------------
    108 # Toy's pass rule is stricter than wasm's: rc must match AND stderr must be
    109 # empty. Diagnostics (expected/got + the stderr body) are surfaced on FAIL.
    110 tf_check_rc() {          # LABEL GOT EXPECTED STDERR_FILE
    111     local label="$1" got="$2" expected="$3" stderr_file="$4"
    112     expected=$((expected & 255))
    113     if [ "$got" -eq "$expected" ] && [ ! -s "$stderr_file" ]; then
    114         kit_pass "$label"
    115     else
    116         kit_fail "$label" "expected rc $expected, got $got"
    117         if [ -s "$stderr_file" ]; then
    118             sed 's/^/    | /' "$stderr_file"
    119         fi
    120     fi
    121 }
    122 
    123 # ---- lanes (cases corpus) --------------------------------------------------
    124 kit_lane_R() {
    125     local label="$KIT_BASE/R-O$KIT_OPT" rc
    126     "$KIT" run "-O$KIT_OPT" "$KIT_SRC" > "$KIT_WORK/run.out" 2> "$KIT_WORK/run.err"
    127     rc=$?
    128     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$KIT_WORK/run.err"
    129 }
    130 
    131 # Path I: kit run --no-jit — execute through the IR interpreter instead of
    132 # JIT-compiled native code, and assert the same exit code as the golden. Ops the
    133 # interpreter does not yet implement emit a greppable
    134 # "interp: <feature> not supported" diagnostic and SKIP rather than FAIL.
    135 kit_lane_I() {
    136     local label="$KIT_BASE/I-O$KIT_OPT" rc missing
    137     "$KIT" run --no-jit "-O$KIT_OPT" "$KIT_SRC" \
    138         > "$KIT_WORK/interp.out" 2> "$KIT_WORK/interp.err"
    139     rc=$?
    140     missing=$(grep -oE 'interp: .*not supported' "$KIT_WORK/interp.err" 2>/dev/null | head -n1 || true)
    141     if [ -n "$missing" ]; then
    142         kit_skip "$label" "$missing"
    143         return
    144     fi
    145     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$KIT_WORK/interp.err"
    146 }
    147 
    148 kit_lane_L() {
    149     local label="$KIT_BASE/L-O$KIT_OPT" rc
    150     local obj="$KIT_WORK/$KIT_BASE.o" exe="$KIT_WORK/$KIT_BASE.exe"
    151     local cc_err="$KIT_WORK/cc.err" ld_err="$KIT_WORK/ld.err"
    152     local dump="$KIT_WORK/objdump.out" dump_err="$KIT_WORK/objdump.err"
    153     local dump_exp="${KIT_SRC%.toy}.objdump"
    154     # The .objdump goldens encode section/symbol spellings, which differ by
    155     # object format (Mach-O `__TEXT,__const` vs ELF `.rodata`...). On non-Darwin
    156     # (ELF) hosts, prefer a `.objdump.elf` sidecar when present; fall back to the
    157     # Mach-O golden otherwise so cases without an ELF variant still run.
    158     if [ "$(uname -s)" != "Darwin" ] && [ -f "${KIT_SRC%.toy}.objdump.elf" ]; then
    159         dump_exp="${KIT_SRC%.toy}.objdump.elf"
    160     fi
    161     local link_skip="${KIT_SRC%.toy}.link.skip"
    162     local link_hosted="${KIT_SRC%.toy}.link.hosted"
    163     local pattern missing
    164 
    165     if [ -e "$link_skip" ]; then
    166         kit_skip "$label" "$(head -n1 "$link_skip")"
    167         return
    168     fi
    169 
    170     # A <name>.link.hosted sidecar forces this case through the hosted link
    171     # (kit cc -lc) regardless of the platform default. Used for cases that need
    172     # the libc/dyld runtime to link at all (e.g. thread-locals, whose Mach-O TLV
    173     # / ELF static-TLS setup is supplied by the hosted crt, not bare `kit ld`).
    174     local case_hosted="$TOY_L_HOSTED"
    175     if [ -e "$link_hosted" ]; then
    176         case_hosted=1
    177     fi
    178 
    179     if ! "$KIT" cc "-O$KIT_OPT" -c "$KIT_SRC" -o "$obj" \
    180             > "$KIT_WORK/cc.out" 2> "$cc_err"; then
    181         kit_fail "$label" "kit cc -O$KIT_OPT -c failed"
    182         sed 's/^/    | /' "$cc_err"
    183         return
    184     fi
    185     if [ -s "$cc_err" ]; then
    186         kit_fail "$label" "kit cc -O$KIT_OPT -c wrote stderr"
    187         sed 's/^/    | /' "$cc_err"
    188         return
    189     fi
    190 
    191     if [ -f "$dump_exp" ]; then
    192         if ! "$KIT" objdump -h -t "$obj" > "$dump" 2> "$dump_err"; then
    193             kit_fail "$label:objdump" "kit objdump failed"
    194             sed 's/^/    | /' "$dump_err"
    195             return
    196         fi
    197         missing=0
    198         while IFS= read -r pattern || [ -n "$pattern" ]; do
    199             [ -z "$pattern" ] && continue
    200             if ! grep -F -q -- "$pattern" "$dump"; then
    201                 missing=1
    202                 printf '%s\n' "$pattern" >> "$KIT_WORK/objdump.missing"
    203             fi
    204         done < "$dump_exp"
    205         if [ "$missing" -eq 0 ]; then
    206             kit_pass "$label:objdump"
    207         else
    208             kit_fail "$label:objdump" "missing objdump substring(s)"
    209             printf '    missing objdump substring(s):\n'
    210             sed 's/^/    > /' "$KIT_WORK/objdump.missing"
    211             printf '    actual objdump:\n'
    212             sed 's/^/    | /' "$dump"
    213         fi
    214     fi
    215 
    216     if [ "$case_hosted" = "1" ]; then
    217         if ! "$KIT" cc "$obj" -lc -o "$exe" > "$KIT_WORK/ld.out" 2> "$ld_err"; then
    218             kit_fail "$label" "kit cc -lc link failed"
    219             sed 's/^/    | /' "$ld_err"
    220             return
    221         fi
    222     else
    223         if ! "$KIT" ld "$obj" -o "$exe" > "$KIT_WORK/ld.out" 2> "$ld_err"; then
    224             kit_fail "$label" "kit ld failed"
    225             sed 's/^/    | /' "$ld_err"
    226             return
    227         fi
    228     fi
    229     if [ -s "$ld_err" ]; then
    230         kit_fail "$label" "kit link wrote stderr"
    231         sed 's/^/    | /' "$ld_err"
    232         return
    233     fi
    234 
    235     chmod +x "$exe" 2>/dev/null || true
    236     "$exe" > "$KIT_WORK/exe.out" 2> "$KIT_WORK/exe.err"
    237     rc=$?
    238     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$KIT_WORK/exe.err"
    239 }
    240 
    241 # ---- cross-arch (path X) helpers -------------------------------------------
    242 cross_triple_for() {
    243     case "$1" in
    244         aa64|aarch64) printf 'aarch64-linux-gnu' ;;
    245         x64|x86_64)   printf 'x86_64-linux-gnu' ;;
    246         rv64|riscv64) printf 'riscv64-linux-gnu' ;;
    247         *)            return 1 ;;
    248     esac
    249 }
    250 
    251 cross_tag_for() {
    252     case "$1" in
    253         aa64|aarch64) printf 'aarch64-linux' ;;
    254         x64|x86_64)   printf 'x64-linux' ;;
    255         rv64|riscv64) printf 'rv64-linux' ;;
    256         *)            return 1 ;;
    257     esac
    258 }
    259 
    260 cross_start_triple_for() {
    261     local arch="$1" triple="$2"
    262     case "$arch" in
    263         rv64|riscv64)
    264             # The X lane links a no-libc static image with this custom _start.
    265             # Compile the rv64 startup object as freestanding so kit ld keeps
    266             # the same +16 TCB-biased TLS convention that the stub installs,
    267             # while the Toy object itself still exercises the Linux target.
    268             printf 'riscv64-none-elf' ;;
    269         *)
    270             printf '%s' "$triple" ;;
    271     esac
    272 }
    273 
    274 cross_make_start_obj() {
    275     local arch="$1" triple="$2" work="$3" start_triple
    276     local start_c="$work/$arch.start.c" start_o="$work/$arch.start.o"
    277     start_triple="$(cross_start_triple_for "$arch" "$triple")"
    278     cat > "$start_c" <<'EOF_START'
    279 extern int main(void);
    280 
    281 /* kit-ld synthesizes the .tdata template boundary symbols for a static
    282  * image. The per-thread TLS image size is their difference, computed as a
    283  * pointer subtraction so each symbol's PC-relative bias cancels — a lone
    284  * kit boundary symbol doesn't resolve cleanly under -fno-pic. The toy
    285  * corpus has no uninitialized (.tbss) thread-locals, so the image is
    286  * exactly .tdata; any .tbss tail is covered by g_tls_block's zero-fill. */
    287 extern char __tdata_start[];
    288 extern char __tdata_end[];
    289 
    290 /* Per-thread static-TLS image. kit resolves a local-exec var at .tdata
    291  * image offset `o` to a thread-pointer-relative `o + 16` on aarch64/riscv64
    292  * (a 16-byte TCB sits ahead of .tdata; see src/obj/elf/link.c) and to a
    293  * negative offset on x86_64 (variant II, TCB after the image). The
    294  * freestanding stub must seed this block and set the thread pointer ITSELF:
    295  * the cross runtimes don't do it for a no-libc static binary — notably
    296  * qemu-riscv64 user-mode leaves tp pointing at uninitialized memory. The
    297  * block is zero-initialized (.bss); single-threaded, so file scope is fine. */
    298 static char g_tls_block[4096] __attribute__((aligned(16)));
    299 
    300 static void tls_init(void) {
    301   unsigned long td_n = (unsigned long)(__tdata_end - __tdata_start);
    302   unsigned long i;
    303 #if defined(__aarch64__)
    304   /* Variant I: tp -> [TCB(16) | tdata]; var at tp + (off + 16). */
    305   char* dst = g_tls_block + 16;
    306   for (i = 0; i < td_n; ++i) dst[i] = __tdata_start[i];
    307   __asm__ volatile("msr tpidr_el0, %0" ::"r"(g_tls_block) : "memory");
    308 #elif defined(__riscv) && __riscv_xlen == 64
    309   /* Variant I: tp -> [TCB(16) | tdata]; var at tp + (off + 16). */
    310   char* dst = g_tls_block + 16;
    311   for (i = 0; i < td_n; ++i) dst[i] = __tdata_start[i];
    312   __asm__ volatile("mv tp, %0" ::"r"(g_tls_block) : "memory");
    313 #elif defined(__x86_64__)
    314   /* Variant II: TLS bytes at negative offsets from %fs, which points at the
    315    * TCB whose first word is a self-pointer (kit reads it via %fs:0). Lay out
    316    * [tdata | TCB] and arch_prctl(ARCH_SET_FS, &TCB) so a var at offset `o`
    317    * lands at fs + (o - td_n) = &tdata[o]. */
    318   char* tcb = g_tls_block + td_n;
    319   for (i = 0; i < td_n; ++i) g_tls_block[i] = __tdata_start[i];
    320   *(void**)tcb = tcb;
    321   register long rax __asm__("rax") = 158;    /* arch_prctl */
    322   register long rdi __asm__("rdi") = 0x1002; /* ARCH_SET_FS */
    323   register long rsi __asm__("rsi") = (long)tcb;
    324   __asm__ volatile("syscall" : "+r"(rax) : "r"(rdi), "r"(rsi)
    325                    : "rcx", "r11", "memory");
    326 #else
    327 #error unsupported target
    328 #endif
    329 }
    330 
    331 __attribute__((noreturn)) static void do_exit(int code) {
    332 #if defined(__aarch64__)
    333   register long x8 __asm__("x8") = 94;
    334   register long x0 __asm__("x0") = code;
    335   __asm__ volatile("svc #0" ::"r"(x8), "r"(x0) : "memory");
    336 #elif defined(__x86_64__)
    337   register long rax __asm__("rax") = 231;
    338   register long rdi __asm__("rdi") = code;
    339   __asm__ volatile("syscall" ::"r"(rax), "r"(rdi) : "memory");
    340 #elif defined(__riscv) && __riscv_xlen == 64
    341   register long a7 __asm__("a7") = 94;
    342   register long a0 __asm__("a0") = code;
    343   __asm__ volatile("ecall" ::"r"(a7), "r"(a0) : "memory");
    344 #else
    345 #error unsupported target
    346 #endif
    347   __builtin_unreachable();
    348 }
    349 
    350 #if defined(__x86_64__)
    351 __attribute__((force_align_arg_pointer))
    352 #endif
    353 void _start(void) {
    354   tls_init();
    355   do_exit(main());
    356 }
    357 EOF_START
    358     # Compile the startup stub with kit cc (the tool under test), not a host
    359     # toolchain: the X path exercises kit cc + kit ld end to end, stub
    360     # included. -fno-PIC/-fno-pie give absolute (non-GOT) relocations for the
    361     # kit-ld-synthesized __tdata_{start,end} boundary symbols, matching the
    362     # static ET_EXEC the link produces. (kit cc has no -fno-stack-protector.)
    363     if ! "$KIT" cc -O1 -target "$start_triple" -ffreestanding \
    364         -fno-PIC -fno-pie -c "$start_c" -o "$start_o" \
    365         > "$work/$arch.start.out" 2> "$work/$arch.start.err"; then
    366         return 1
    367     fi
    368     printf '%s' "$start_o"
    369 }
    370 
    371 # rv32 cross arch: freestanding (`-none-elf`), so it cannot use the Linux
    372 # qemu-user / exec_target path the other cross arches share. Compile for
    373 # riscv32-none-elf and run the bare-metal image under qemu-system-riscv32 via the
    374 # shared helper. Runs inline (one qemu-system boot per image) rather than through
    375 # the deferred exec_target queue.
    376 #
    377 # A `<name>.rv32.skip` sidecar opts a single case out (the standard per-lane
    378 # mechanism, like `.link.skip` / `.wasm.skip`) — but NONE are committed: the real
    379 # rv32 gaps surfaced by this lane (i64 atomics, the 64-bit overflow intrinsic,
    380 # i64 varargs, thread-local storage, a toy soft-float compare lowering) are left
    381 # RED on purpose, so they are not silently hidden. The sidecar exists only for
    382 # cases that are genuinely inapplicable to rv32 (and may be added later). The
    383 # shared asmnop skip (an aa64-only construct, already skipped for every non-aa64
    384 # cross arch) and the env-unavailable skip are the only built-in non-run verdicts.
    385 cross_one_rv32() {
    386     local label="$KIT_BASE/X-O$KIT_OPT:rv32" rc reason exp obj cc_err
    387     local skip="${KIT_SRC%.toy}.rv32.skip"
    388     if [ -e "$skip" ]; then kit_skip "$label" "$(head -n1 "$skip")"; return; fi
    389     if [ "${RV32_BARE_OK:-0}" -ne 1 ]; then
    390         kit_skip "$label" "no rv32 runner (qemu-system-riscv32)"; return; fi
    391     if grep -q 'asmnop' "$KIT_SRC" 2>/dev/null; then
    392         kit_skip "$label" "asmnop is target-specific before toy asm selectors"; return; fi
    393     obj="$KIT_WORK/$KIT_BASE.O$KIT_OPT.rv32.o"; cc_err="$KIT_WORK/rv32.cc.err"
    394     if ! "$KIT" cc "-O$KIT_OPT" -target riscv32-none-elf \
    395             -march=rv32imafc_zicsr_zifencei -mabi=ilp32f -ffreestanding \
    396             -c "$KIT_SRC" -o "$obj" > "$KIT_WORK/rv32.cc.out" 2> "$cc_err"; then
    397         kit_fail "$label" "kit cc -target riscv32-none-elf failed"
    398         sed 's/^/    | /' "$cc_err"; return
    399     fi
    400     if [ -s "$cc_err" ]; then
    401         kit_fail "$label" "kit cc rv32 wrote stderr"; sed 's/^/    | /' "$cc_err"; return; fi
    402     reason="$(exec_bare_run rv32 "$obj" "$KIT_WORK" "$KIT_WORK/rv32.rc")"
    403     if [ $? -eq 2 ]; then kit_skip "$label" "$reason"; return; fi
    404     rc="$(cat "$KIT_WORK/rv32.rc" 2>/dev/null || echo 99)"; exp=$(( KIT_EXPECTED & 255 ))
    405     # Bare-metal has no stderr channel; the finisher maps main()'s return onto the
    406     # qemu exit code, so compare it directly to the expected exit code.
    407     if [ "$rc" -eq "$exp" ]; then kit_pass "$label"
    408     else kit_fail "$label" "expected rc $exp, got $rc (qemu-system-riscv32)"; fi
    409 }
    410 
    411 # arm32 cross arch: freestanding (arm-none-eabi, Cortex-M), bare-metal under
    412 # qemu-system-arm via exec_bare — same model as rv32. Real arm32 gaps (i64
    413 # atomics, TLS needing a thread pointer, __int128) are left RED on purpose, not
    414 # skipped, so they are not silently hidden.
    415 cross_one_arm32() {
    416     local label="$KIT_BASE/X-O$KIT_OPT:arm32" rc reason exp obj cc_err
    417     local skip="${KIT_SRC%.toy}.arm32.skip"
    418     if [ -e "$skip" ]; then kit_skip "$label" "$(head -n1 "$skip")"; return; fi
    419     if [ "${ARM32_BARE_OK:-0}" -ne 1 ]; then
    420         kit_skip "$label" "no arm32 runner (qemu-system-arm)"; return; fi
    421     if grep -q 'asmnop' "$KIT_SRC" 2>/dev/null; then
    422         kit_skip "$label" "asmnop is target-specific before toy asm selectors"; return; fi
    423     obj="$KIT_WORK/$KIT_BASE.O$KIT_OPT.arm32.o"; cc_err="$KIT_WORK/arm32.cc.err"
    424     if ! "$KIT" cc "-O$KIT_OPT" -target arm-none-eabi -mcpu=cortex-m3 \
    425             -mfloat-abi=soft -ffreestanding \
    426             -c "$KIT_SRC" -o "$obj" > "$KIT_WORK/arm32.cc.out" 2> "$cc_err"; then
    427         kit_fail "$label" "kit cc -target arm-none-eabi failed"
    428         sed 's/^/    | /' "$cc_err"; return
    429     fi
    430     if [ -s "$cc_err" ]; then
    431         kit_fail "$label" "kit cc arm32 wrote stderr"; sed 's/^/    | /' "$cc_err"; return; fi
    432     reason="$(exec_bare_run arm32 "$obj" "$KIT_WORK" "$KIT_WORK/arm32.rc")"
    433     if [ $? -eq 2 ]; then kit_skip "$label" "$reason"; return; fi
    434     rc="$(cat "$KIT_WORK/arm32.rc" 2>/dev/null || echo 99)"; exp=$(( KIT_EXPECTED & 255 ))
    435     if [ "$rc" -eq "$exp" ]; then kit_pass "$label"
    436     else kit_fail "$label" "expected rc $exp, got $rc (qemu-system-arm)"; fi
    437 }
    438 
    439 cross_one() {
    440     local arch="$1"
    441     local triple tag obj exe start_obj cc_err ld_err out err label
    442     if [ "$arch" = "rv32" ]; then cross_one_rv32; return; fi
    443     if [ "$arch" = "arm32" ]; then cross_one_arm32; return; fi
    444     triple="$(cross_triple_for "$arch")" || {
    445         kit_skip "$KIT_BASE/X-O$KIT_OPT:$arch" "unknown cross arch"
    446         return
    447     }
    448     tag="$(cross_tag_for "$arch")" || {
    449         kit_skip "$KIT_BASE/X-O$KIT_OPT:$arch" "unknown cross arch"
    450         return
    451     }
    452     label="$KIT_BASE/X-O$KIT_OPT:$arch"
    453     if [ "$arch" != "aa64" ] && [ "$arch" != "aarch64" ] &&
    454         grep -q 'asmnop' "$KIT_SRC"; then
    455         kit_skip "$label" "asmnop is target-specific before toy asm selectors"
    456         return
    457     fi
    458     # Arch-specific cases name themselves with an _aa64/_x64/_rv64 basename
    459     # suffix and use intrinsics that only lower on that arch (e.g. the aa64
    460     # privileged wfi/wfe/DAIF pair has no x64/rv64 lowering). Cross-compiling
    461     # them onto a sibling arch is expected to fail, so skip the mismatches.
    462     case "$KIT_BASE" in
    463         *_aa64) case "$arch" in aa64|aarch64) ;; *)
    464             kit_skip "$label" "aa64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    465         *_x64)  case "$arch" in x64|x86_64) ;; *)
    466             kit_skip "$label" "x64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    467         *_rv64) case "$arch" in rv64|riscv64) ;; *)
    468             kit_skip "$label" "rv64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    469     esac
    470     if ! exec_target_supported "$tag"; then
    471         kit_skip "$label" "no runner for $tag"
    472         return
    473     fi
    474 
    475     obj="$KIT_WORK/$KIT_BASE.O$KIT_OPT.$arch.o"
    476     exe="$KIT_WORK/$KIT_BASE.O$KIT_OPT.$arch.exe"
    477     cc_err="$KIT_WORK/$arch.cc.err"
    478     ld_err="$KIT_WORK/$arch.ld.err"
    479     out="$KIT_WORK/$arch.out"
    480     err="$KIT_WORK/$arch.err"
    481 
    482     if ! "$KIT" cc "-O$KIT_OPT" -target "$triple" -c "$KIT_SRC" -o "$obj" \
    483         > "$KIT_WORK/$arch.cc.out" 2> "$cc_err"; then
    484         kit_fail "$label" "kit cc -O$KIT_OPT -target $triple -c failed"
    485         sed 's/^/    | /' "$cc_err"
    486         return
    487     fi
    488     if [ -s "$cc_err" ]; then
    489         kit_fail "$label" "kit cc -O$KIT_OPT -target $triple -c wrote stderr"
    490         sed 's/^/    | /' "$cc_err"
    491         return
    492     fi
    493 
    494     # The stub is compiled by kit cc (the tool under test), so a failure here
    495     # is a real codegen bug, not a missing host toolchain — surface it as FAIL.
    496     start_obj="$(cross_make_start_obj "$arch" "$triple" "$KIT_WORK")" || {
    497         kit_fail "$label" "kit cc failed on startup stub"
    498         sed 's/^/    | /' "$KIT_WORK/$arch.start.err"
    499         return
    500     }
    501 
    502     # kit ld links a non-PIE static ET_EXEC by default (no -pie here), so the
    503     # freestanding image lands at IMAGE_BASE_STATIC (0x400000): a PIE/ET_DYN
    504     # would be based at vaddr 0, putting its writable segments below the
    505     # loader's mmap_min_addr where qemu-user maps them non-writable and the
    506     # stub's TLS-block store faults.
    507     if ! "$KIT" ld "$obj" "$start_obj" -o "$exe" \
    508         > "$KIT_WORK/$arch.ld.out" 2> "$ld_err"; then
    509         kit_fail "$label" "kit ld failed"
    510         sed 's/^/    | /' "$ld_err"
    511         return
    512     fi
    513     if [ -s "$ld_err" ]; then
    514         kit_fail "$label" "kit ld wrote stderr"
    515         sed 's/^/    | /' "$ld_err"
    516         return
    517     fi
    518 
    519     chmod +x "$exe" 2>/dev/null || true
    520     # Defer execution: the engine batches one container run per arch and checks
    521     # rc == expected after exec_target_flush; tf_flush_verify (KIT_FLUSH_VERIFY)
    522     # adds the empty-stderr check after filtering the platform-mismatch warning.
    523     kit_queue_e "$label" "$exe" "$out" "$err" "$KIT_WORK/$arch.rc" "$KIT_EXPECTED" "$tag" "$err"
    524 }
    525 
    526 kit_lane_X() {
    527     local arch
    528     for arch in $TOY_CROSS_ARCHS; do
    529         cross_one "$arch"
    530     done
    531 }
    532 
    533 # KIT_FLUSH_VERIFY: called per queued-X case after flush with (label, payload,
    534 # rc). The engine already verified rc == expected; here we replicate the
    535 # original's "stderr must be empty (after filtering the image-platform warning)"
    536 # rule. payload is the err file path. Returns 0 to keep pass, 1 to fail.
    537 tf_flush_verify() {
    538     local errf="$2"
    539     [ -n "$errf" ] || return 0
    540     if [ -s "$errf" ]; then
    541         grep -v '^WARNING: image platform .* does not match the expected platform' \
    542             "$errf" > "$errf.clean" 2>/dev/null || true
    543         mv "$errf.clean" "$errf" 2>/dev/null || true
    544     fi
    545     [ ! -s "$errf" ]
    546 }
    547 
    548 # ---- C-source backend (path C, opt-0 only) ---------------------------------
    549 kit_lane_C() {
    550     local label="$KIT_BASE/C-O$KIT_OPT" rc missing
    551     local cbackend_skip="${KIT_SRC%.toy}.cbackend.skip"
    552     if [ -e "$cbackend_skip" ]; then
    553         kit_skip "$label" "$(head -n1 "$cbackend_skip")"
    554         return
    555     fi
    556     local cbackend_gcc_skip="${KIT_SRC%.toy}.cbackend.gcc.skip"
    557     if [ "$TOY_HOST_CC_IS_GCC" = 1 ] && [ -e "$cbackend_gcc_skip" ]; then
    558         kit_skip "$label" "$(head -n1 "$cbackend_gcc_skip")"
    559         return
    560     fi
    561     local out_c="$KIT_WORK/$KIT_BASE.kit.c"
    562     local out_bin="$KIT_WORK/$KIT_BASE.cbackend.bin"
    563     local emit_err="$KIT_WORK/c.emit.err"
    564     local cc_err="$KIT_WORK/c.cc.err"
    565     local run_err="$KIT_WORK/c.run.err"
    566 
    567     # --emit=c forces opt_level=0 internally; pass -O$KIT_OPT anyway so the
    568     # driver flag parsing stays exercised.
    569     if ! "$KIT" cc "-O$KIT_OPT" --emit=c "$KIT_SRC" -o "$out_c" \
    570             > "$KIT_WORK/c.emit.out" 2> "$emit_err"; then
    571         # Phased-rollout panic from the C target -> SKIP, not FAIL.
    572         missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
    573                   "$emit_err" 2>/dev/null | head -n1 || true)
    574         if [ -n "$missing" ]; then
    575             kit_skip "$label" "$missing"
    576             return
    577         fi
    578         kit_fail "$label" "kit cc --emit=c failed"
    579         sed 's/^/    | /' "$emit_err"
    580         return
    581     fi
    582     # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the emitted
    583     # `int32_t main(void)` satisfies the host C compiler's main-return-type check.
    584     # -Wno-tentative-definition-incomplete-type: the c_target forward-declares a
    585     # data global as `static const struct __kit_data_g g;` before defining that
    586     # per-global struct later in the same TU. It is valid C (g is completed by
    587     # end of TU; clang>=15 and gcc accept it), but clang 14's pedantic warning
    588     # rejects the forward decl under -Werror. clang has the flag; gcc ignores the
    589     # unknown -Wno- silently, so this is safe across host toolchains.
    590     if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror \
    591             -Wno-tentative-definition-incomplete-type "$out_c" -o "$out_bin" \
    592             > "$KIT_WORK/c.cc.out" 2> "$cc_err"; then
    593         kit_fail "$label" "host cc rejected emitted source"
    594         sed 's/^/    | /' "$cc_err"
    595         return
    596     fi
    597     "$out_bin" > "$KIT_WORK/c.run.out" 2> "$run_err"
    598     rc=$?
    599     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$run_err"
    600 }
    601 
    602 # ---- Wasm roundtrip (path W, opt-0 only) -----------------------------------
    603 kit_lane_W() {
    604     local label="$KIT_BASE/W-O$KIT_OPT" rc missing
    605     local wasm_skip="${KIT_SRC%.toy}.wasm.skip"
    606     if [ -e "$wasm_skip" ]; then
    607         kit_skip "$label" "$(head -n1 "$wasm_skip")"
    608         return
    609     fi
    610     local wasm="$KIT_WORK/$KIT_BASE.wasm"
    611     local cc_err="$KIT_WORK/wasm.cc.err"
    612     local run_err="$KIT_WORK/wasm.run.err"
    613     if ! "$KIT" cc "-O$KIT_OPT" -target wasm32-none -c "$KIT_SRC" -o "$wasm" \
    614             > "$KIT_WORK/wasm.cc.out" 2> "$cc_err"; then
    615         # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. The Wasm
    616         # target emits "wasm: <feature> not yet implemented" plus a handful of
    617         # "unsupported X" / "too many X (max N supported in v1)" /
    618         # "max N supported in v1" variants — all meaning this case needs more
    619         # wasm CGTarget work.
    620         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)' \
    621                   "$cc_err" 2>/dev/null | head -n1 || true)
    622         if [ -n "$missing" ]; then
    623             kit_skip "$label" "$missing"
    624             return
    625         fi
    626         kit_fail "$label" "kit cc -target wasm32-none failed"
    627         sed 's/^/    | /' "$cc_err"
    628         return
    629     fi
    630     if [ -s "$cc_err" ]; then
    631         kit_fail "$label" "kit cc -target wasm32-none wrote stderr"
    632         sed 's/^/    | /' "$cc_err"
    633         return
    634     fi
    635     # kit run on the .wasm routes through the lang/wasm frontend, lowers to
    636     # native CG, links via the JIT, and invokes main.
    637     "$KIT" run "$wasm" > "$KIT_WORK/wasm.run.out" 2> "$run_err"
    638     rc=$?
    639     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$run_err"
    640 }
    641 
    642 # ---- err corpus (compile-fail cases) ---------------------------------------
    643 # cc MUST fail; the .expected file holds diagnostic substring(s) that must
    644 # appear in stderr (grep -F -f). cc is invoked native (no -target) by default,
    645 # matching the original which used `kit cc -c` on the host arch. A case that
    646 # needs a non-host target (e.g. a 32-bit-only diagnostic) supplies the extra cc
    647 # flags in an err/<name>.ccargs sidecar; its contents are word-split onto the
    648 # `kit cc -c` command line.
    649 kit_lane_ERR() {
    650     local label="$KIT_BASE/E"
    651     local obj="$KIT_WORK/$KIT_BASE.o"
    652     local err="$KIT_WORK/cc.err"
    653     local exp="${KIT_SRC%.toy}.expected"
    654     local ccargs_file="${KIT_SRC%.toy}.ccargs"
    655     local ccargs=""
    656     [ -f "$ccargs_file" ] && ccargs="$(cat "$ccargs_file")"
    657     if "$KIT" cc -c $ccargs "$KIT_SRC" -o "$obj" > "$KIT_WORK/cc.out" 2> "$err"; then
    658         kit_fail "$label" "expected compile failure, got success"
    659         return
    660     fi
    661     if [ ! -f "$exp" ]; then
    662         kit_fail "$label" "missing expected diagnostic: $exp"
    663         return
    664     fi
    665     if grep -F -q -f "$exp" "$err"; then
    666         kit_pass "$label"
    667     else
    668         kit_fail "$label" "expected diagnostic substring not found"
    669         printf '    expected diagnostic substring:\n'
    670         sed 's/^/    > /' "$exp"
    671         printf '    actual stderr:\n'
    672         sed 's/^/    | /' "$err"
    673     fi
    674 }
    675 
    676 # ---- exec_target wiring (path X uses the engine's deferred-exec flush) ------
    677 if [ "$RUN_X" -eq 1 ]; then
    678     have_podman=0
    679     command -v podman >/dev/null 2>&1 && have_podman=1
    680     QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
    681     have_qemu=0
    682     [ -n "$QEMU_BIN" ] && have_qemu=1
    683     case "$(uname -m 2>/dev/null)" in
    684         aarch64|arm64) is_aarch64=1 ;;
    685         *)             is_aarch64=0 ;;
    686     esac
    687     export have_qemu QEMU_BIN have_podman is_aarch64
    688     # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it once
    689     # so a single batched container drains the whole path-X queue.
    690     EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    691     export EXEC_TARGET_MOUNT_ROOT
    692     # shellcheck source=../lib/exec_target.sh
    693     . "$ROOT/test/lib/exec_target.sh"
    694     # rv32 is a cross arch in path X, but freestanding (qemu-system bare-metal),
    695     # so it uses the exec_bare backend (sourced by exec_target.sh) rather than
    696     # exec_target's qemu-user path: exec_bare links each object against a
    697     # bare-metal stub + rt and boots it under qemu-system-riscv32.
    698     case " $TOY_CROSS_ARCHS " in
    699         *" rv32 "*)
    700             if exec_bare_setup rv32 "$BUILD_DIR/rv32"; then RV32_BARE_OK=1; else RV32_BARE_OK=0; fi ;;
    701     esac
    702     case " $TOY_CROSS_ARCHS " in
    703         *" arm32 "*)
    704             if exec_bare_setup arm32 "$BUILD_DIR/arm32"; then ARM32_BARE_OK=1; else ARM32_BARE_OK=0; fi ;;
    705     esac
    706 fi
    707 
    708 # ---- drive the corpora -----------------------------------------------------
    709 printf 'toy: KIT=%s\n' "$KIT"
    710 printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS"
    711 if [ "$RUN_X" -eq 1 ]; then
    712     printf 'toy: cross archs=%s (path X)\n' "$TOY_CROSS_ARCHS"
    713 fi
    714 
    715 # cases corpus — active lanes in PATHS canonical order R I L X C W.
    716 CASE_LANES=
    717 [ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R"
    718 [ "$RUN_I" -eq 1 ] && CASE_LANES="$CASE_LANES I"
    719 [ "$RUN_L" -eq 1 ] && CASE_LANES="$CASE_LANES L"
    720 [ "$RUN_X" -eq 1 ] && CASE_LANES="$CASE_LANES X"
    721 [ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C"
    722 [ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W"
    723 
    724 KIT_LABEL=toy KIT_BUILD_DIR="$BUILD_DIR" \
    725   KIT_CORPUS_GLOBS="$TEST_DIR/cases/*.toy" KIT_CORPUS_EXT=toy \
    726   KIT_SIDECAR_DIR="$TEST_DIR/cases" \
    727   KIT_LANES="$CASE_LANES" KIT_OPT_LEVELS="$TOY_OPT_LEVELS" \
    728   KIT_TUPLES="host-host" KIT_TARGETS_EXT="" KIT_OPT0ONLY="C W" \
    729   KIT_PARALLELIZABLE="$PAR" KIT_FLUSH_VERIFY=tf_flush_verify \
    730   kit_corpus_run
    731 
    732 # err cases exercise compile-failure paths; they aren't relevant to path C/W
    733 # (which go through the same cc invocation). Only run them when at least one of
    734 # the native compile paths (R/L/X) is enabled.
    735 if [ "$RUN_R" -eq 1 ] || [ "$RUN_L" -eq 1 ] || [ "$RUN_X" -eq 1 ]; then
    736     err_cases=("$TEST_DIR"/err/*.toy)
    737     err_have_match=1
    738     if [ -n "${KIT_TEST_FILTER:-}" ]; then
    739         err_have_match=0
    740         for src in "${err_cases[@]}"; do
    741             base="$(basename "${src%.toy}")"
    742             case "$base" in *"$KIT_TEST_FILTER"*) err_have_match=1; break ;; esac
    743         done
    744     fi
    745     if [ "${#err_cases[@]}" -gt 0 ] && [ "$err_have_match" -eq 1 ]; then
    746         KIT_LABEL=toy KIT_BUILD_DIR="$BUILD_DIR/err" \
    747           KIT_CORPUS_GLOBS="$TEST_DIR/err/*.toy" KIT_CORPUS_EXT=toy \
    748           KIT_SIDECAR_DIR="$TEST_DIR/err" \
    749           KIT_LANES="ERR" KIT_OPT_LEVELS="" KIT_TUPLES="host-host" \
    750           KIT_TARGETS_EXT="" KIT_PARALLELIZABLE="$PAR" \
    751           kit_corpus_run
    752     fi
    753 fi
    754 
    755 # toy treats skips (interp-unsupported ops, no cross-arch runner, the
    756 # intentionally-red musttail cases, ...) as non-fatal — matching the original
    757 # runner, which only ever exited nonzero on a real FAIL.
    758 KIT_SKIP_IS_FAILURE=0
    759 kit_summary toy
    760 kit_exit