kit

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

run.sh (31579B)


      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="$(rv32_bare_run "$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 cross_one() {
    412     local arch="$1"
    413     local triple tag obj exe start_obj cc_err ld_err out err label
    414     if [ "$arch" = "rv32" ]; then cross_one_rv32; return; fi
    415     triple="$(cross_triple_for "$arch")" || {
    416         kit_skip "$KIT_BASE/X-O$KIT_OPT:$arch" "unknown cross arch"
    417         return
    418     }
    419     tag="$(cross_tag_for "$arch")" || {
    420         kit_skip "$KIT_BASE/X-O$KIT_OPT:$arch" "unknown cross arch"
    421         return
    422     }
    423     label="$KIT_BASE/X-O$KIT_OPT:$arch"
    424     if [ "$arch" != "aa64" ] && [ "$arch" != "aarch64" ] &&
    425         grep -q 'asmnop' "$KIT_SRC"; then
    426         kit_skip "$label" "asmnop is target-specific before toy asm selectors"
    427         return
    428     fi
    429     # Arch-specific cases name themselves with an _aa64/_x64/_rv64 basename
    430     # suffix and use intrinsics that only lower on that arch (e.g. the aa64
    431     # privileged wfi/wfe/DAIF pair has no x64/rv64 lowering). Cross-compiling
    432     # them onto a sibling arch is expected to fail, so skip the mismatches.
    433     case "$KIT_BASE" in
    434         *_aa64) case "$arch" in aa64|aarch64) ;; *)
    435             kit_skip "$label" "aa64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    436         *_x64)  case "$arch" in x64|x86_64) ;; *)
    437             kit_skip "$label" "x64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    438         *_rv64) case "$arch" in rv64|riscv64) ;; *)
    439             kit_skip "$label" "rv64-only case (intrinsics have no $arch lowering)"; return ;; esac ;;
    440     esac
    441     if ! exec_target_supported "$tag"; then
    442         kit_skip "$label" "no runner for $tag"
    443         return
    444     fi
    445 
    446     obj="$KIT_WORK/$KIT_BASE.O$KIT_OPT.$arch.o"
    447     exe="$KIT_WORK/$KIT_BASE.O$KIT_OPT.$arch.exe"
    448     cc_err="$KIT_WORK/$arch.cc.err"
    449     ld_err="$KIT_WORK/$arch.ld.err"
    450     out="$KIT_WORK/$arch.out"
    451     err="$KIT_WORK/$arch.err"
    452 
    453     if ! "$KIT" cc "-O$KIT_OPT" -target "$triple" -c "$KIT_SRC" -o "$obj" \
    454         > "$KIT_WORK/$arch.cc.out" 2> "$cc_err"; then
    455         kit_fail "$label" "kit cc -O$KIT_OPT -target $triple -c failed"
    456         sed 's/^/    | /' "$cc_err"
    457         return
    458     fi
    459     if [ -s "$cc_err" ]; then
    460         kit_fail "$label" "kit cc -O$KIT_OPT -target $triple -c wrote stderr"
    461         sed 's/^/    | /' "$cc_err"
    462         return
    463     fi
    464 
    465     # The stub is compiled by kit cc (the tool under test), so a failure here
    466     # is a real codegen bug, not a missing host toolchain — surface it as FAIL.
    467     start_obj="$(cross_make_start_obj "$arch" "$triple" "$KIT_WORK")" || {
    468         kit_fail "$label" "kit cc failed on startup stub"
    469         sed 's/^/    | /' "$KIT_WORK/$arch.start.err"
    470         return
    471     }
    472 
    473     # kit ld links a non-PIE static ET_EXEC by default (no -pie here), so the
    474     # freestanding image lands at IMAGE_BASE_STATIC (0x400000): a PIE/ET_DYN
    475     # would be based at vaddr 0, putting its writable segments below the
    476     # loader's mmap_min_addr where qemu-user maps them non-writable and the
    477     # stub's TLS-block store faults.
    478     if ! "$KIT" ld "$obj" "$start_obj" -o "$exe" \
    479         > "$KIT_WORK/$arch.ld.out" 2> "$ld_err"; then
    480         kit_fail "$label" "kit ld failed"
    481         sed 's/^/    | /' "$ld_err"
    482         return
    483     fi
    484     if [ -s "$ld_err" ]; then
    485         kit_fail "$label" "kit ld wrote stderr"
    486         sed 's/^/    | /' "$ld_err"
    487         return
    488     fi
    489 
    490     chmod +x "$exe" 2>/dev/null || true
    491     # Defer execution: the engine batches one container run per arch and checks
    492     # rc == expected after exec_target_flush; tf_flush_verify (KIT_FLUSH_VERIFY)
    493     # adds the empty-stderr check after filtering the platform-mismatch warning.
    494     kit_queue_e "$label" "$exe" "$out" "$err" "$KIT_WORK/$arch.rc" "$KIT_EXPECTED" "$tag" "$err"
    495 }
    496 
    497 kit_lane_X() {
    498     local arch
    499     for arch in $TOY_CROSS_ARCHS; do
    500         cross_one "$arch"
    501     done
    502 }
    503 
    504 # KIT_FLUSH_VERIFY: called per queued-X case after flush with (label, payload,
    505 # rc). The engine already verified rc == expected; here we replicate the
    506 # original's "stderr must be empty (after filtering the image-platform warning)"
    507 # rule. payload is the err file path. Returns 0 to keep pass, 1 to fail.
    508 tf_flush_verify() {
    509     local errf="$2"
    510     [ -n "$errf" ] || return 0
    511     if [ -s "$errf" ]; then
    512         grep -v '^WARNING: image platform .* does not match the expected platform' \
    513             "$errf" > "$errf.clean" 2>/dev/null || true
    514         mv "$errf.clean" "$errf" 2>/dev/null || true
    515     fi
    516     [ ! -s "$errf" ]
    517 }
    518 
    519 # ---- C-source backend (path C, opt-0 only) ---------------------------------
    520 kit_lane_C() {
    521     local label="$KIT_BASE/C-O$KIT_OPT" rc missing
    522     local cbackend_skip="${KIT_SRC%.toy}.cbackend.skip"
    523     if [ -e "$cbackend_skip" ]; then
    524         kit_skip "$label" "$(head -n1 "$cbackend_skip")"
    525         return
    526     fi
    527     local cbackend_gcc_skip="${KIT_SRC%.toy}.cbackend.gcc.skip"
    528     if [ "$TOY_HOST_CC_IS_GCC" = 1 ] && [ -e "$cbackend_gcc_skip" ]; then
    529         kit_skip "$label" "$(head -n1 "$cbackend_gcc_skip")"
    530         return
    531     fi
    532     local out_c="$KIT_WORK/$KIT_BASE.kit.c"
    533     local out_bin="$KIT_WORK/$KIT_BASE.cbackend.bin"
    534     local emit_err="$KIT_WORK/c.emit.err"
    535     local cc_err="$KIT_WORK/c.cc.err"
    536     local run_err="$KIT_WORK/c.run.err"
    537 
    538     # --emit=c forces opt_level=0 internally; pass -O$KIT_OPT anyway so the
    539     # driver flag parsing stays exercised.
    540     if ! "$KIT" cc "-O$KIT_OPT" --emit=c "$KIT_SRC" -o "$out_c" \
    541             > "$KIT_WORK/c.emit.out" 2> "$emit_err"; then
    542         # Phased-rollout panic from the C target -> SKIP, not FAIL.
    543         missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
    544                   "$emit_err" 2>/dev/null | head -n1 || true)
    545         if [ -n "$missing" ]; then
    546             kit_skip "$label" "$missing"
    547             return
    548         fi
    549         kit_fail "$label" "kit cc --emit=c failed"
    550         sed 's/^/    | /' "$emit_err"
    551         return
    552     fi
    553     # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the emitted
    554     # `int32_t main(void)` satisfies the host C compiler's main-return-type check.
    555     # -Wno-tentative-definition-incomplete-type: the c_target forward-declares a
    556     # data global as `static const struct __kit_data_g g;` before defining that
    557     # per-global struct later in the same TU. It is valid C (g is completed by
    558     # end of TU; clang>=15 and gcc accept it), but clang 14's pedantic warning
    559     # rejects the forward decl under -Werror. clang has the flag; gcc ignores the
    560     # unknown -Wno- silently, so this is safe across host toolchains.
    561     if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror \
    562             -Wno-tentative-definition-incomplete-type "$out_c" -o "$out_bin" \
    563             > "$KIT_WORK/c.cc.out" 2> "$cc_err"; then
    564         kit_fail "$label" "host cc rejected emitted source"
    565         sed 's/^/    | /' "$cc_err"
    566         return
    567     fi
    568     "$out_bin" > "$KIT_WORK/c.run.out" 2> "$run_err"
    569     rc=$?
    570     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$run_err"
    571 }
    572 
    573 # ---- Wasm roundtrip (path W, opt-0 only) -----------------------------------
    574 kit_lane_W() {
    575     local label="$KIT_BASE/W-O$KIT_OPT" rc missing
    576     local wasm_skip="${KIT_SRC%.toy}.wasm.skip"
    577     if [ -e "$wasm_skip" ]; then
    578         kit_skip "$label" "$(head -n1 "$wasm_skip")"
    579         return
    580     fi
    581     local wasm="$KIT_WORK/$KIT_BASE.wasm"
    582     local cc_err="$KIT_WORK/wasm.cc.err"
    583     local run_err="$KIT_WORK/wasm.run.err"
    584     if ! "$KIT" cc "-O$KIT_OPT" -target wasm32-none -c "$KIT_SRC" -o "$wasm" \
    585             > "$KIT_WORK/wasm.cc.out" 2> "$cc_err"; then
    586         # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. The Wasm
    587         # target emits "wasm: <feature> not yet implemented" plus a handful of
    588         # "unsupported X" / "too many X (max N supported in v1)" /
    589         # "max N supported in v1" variants — all meaning this case needs more
    590         # wasm CGTarget work.
    591         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)' \
    592                   "$cc_err" 2>/dev/null | head -n1 || true)
    593         if [ -n "$missing" ]; then
    594             kit_skip "$label" "$missing"
    595             return
    596         fi
    597         kit_fail "$label" "kit cc -target wasm32-none failed"
    598         sed 's/^/    | /' "$cc_err"
    599         return
    600     fi
    601     if [ -s "$cc_err" ]; then
    602         kit_fail "$label" "kit cc -target wasm32-none wrote stderr"
    603         sed 's/^/    | /' "$cc_err"
    604         return
    605     fi
    606     # kit run on the .wasm routes through the lang/wasm frontend, lowers to
    607     # native CG, links via the JIT, and invokes main.
    608     "$KIT" run "$wasm" > "$KIT_WORK/wasm.run.out" 2> "$run_err"
    609     rc=$?
    610     tf_check_rc "$label" "$rc" "$KIT_EXPECTED" "$run_err"
    611 }
    612 
    613 # ---- err corpus (compile-fail cases) ---------------------------------------
    614 # cc MUST fail; the .expected file holds diagnostic substring(s) that must
    615 # appear in stderr (grep -F -f). cc is invoked native (no -target) by default,
    616 # matching the original which used `kit cc -c` on the host arch. A case that
    617 # needs a non-host target (e.g. a 32-bit-only diagnostic) supplies the extra cc
    618 # flags in an err/<name>.ccargs sidecar; its contents are word-split onto the
    619 # `kit cc -c` command line.
    620 kit_lane_ERR() {
    621     local label="$KIT_BASE/E"
    622     local obj="$KIT_WORK/$KIT_BASE.o"
    623     local err="$KIT_WORK/cc.err"
    624     local exp="${KIT_SRC%.toy}.expected"
    625     local ccargs_file="${KIT_SRC%.toy}.ccargs"
    626     local ccargs=""
    627     [ -f "$ccargs_file" ] && ccargs="$(cat "$ccargs_file")"
    628     if "$KIT" cc -c $ccargs "$KIT_SRC" -o "$obj" > "$KIT_WORK/cc.out" 2> "$err"; then
    629         kit_fail "$label" "expected compile failure, got success"
    630         return
    631     fi
    632     if [ ! -f "$exp" ]; then
    633         kit_fail "$label" "missing expected diagnostic: $exp"
    634         return
    635     fi
    636     if grep -F -q -f "$exp" "$err"; then
    637         kit_pass "$label"
    638     else
    639         kit_fail "$label" "expected diagnostic substring not found"
    640         printf '    expected diagnostic substring:\n'
    641         sed 's/^/    > /' "$exp"
    642         printf '    actual stderr:\n'
    643         sed 's/^/    | /' "$err"
    644     fi
    645 }
    646 
    647 # ---- exec_target wiring (path X uses the engine's deferred-exec flush) ------
    648 if [ "$RUN_X" -eq 1 ]; then
    649     have_podman=0
    650     command -v podman >/dev/null 2>&1 && have_podman=1
    651     QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
    652     have_qemu=0
    653     [ -n "$QEMU_BIN" ] && have_qemu=1
    654     case "$(uname -m 2>/dev/null)" in
    655         aarch64|arm64) is_aarch64=1 ;;
    656         *)             is_aarch64=0 ;;
    657     esac
    658     export have_qemu QEMU_BIN have_podman is_aarch64
    659     # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it once
    660     # so a single batched container drains the whole path-X queue.
    661     EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
    662     export EXEC_TARGET_MOUNT_ROOT
    663     # shellcheck source=../lib/exec_target.sh
    664     . "$ROOT/test/lib/exec_target.sh"
    665     # rv32 is a cross arch in path X, but freestanding (qemu-system bare-metal),
    666     # so it uses its own exec helper rather than exec_target's qemu-user path.
    667     case " $TOY_CROSS_ARCHS " in
    668         *" rv32 "*)
    669             # shellcheck source=../lib/exec_rv32_bare.sh
    670             . "$ROOT/test/lib/exec_rv32_bare.sh"
    671             rv32_bare_setup "$BUILD_DIR/rv32" ;;
    672     esac
    673 fi
    674 
    675 # ---- drive the corpora -----------------------------------------------------
    676 printf 'toy: KIT=%s\n' "$KIT"
    677 printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS"
    678 if [ "$RUN_X" -eq 1 ]; then
    679     printf 'toy: cross archs=%s (path X)\n' "$TOY_CROSS_ARCHS"
    680 fi
    681 
    682 # cases corpus — active lanes in PATHS canonical order R I L X C W.
    683 CASE_LANES=
    684 [ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R"
    685 [ "$RUN_I" -eq 1 ] && CASE_LANES="$CASE_LANES I"
    686 [ "$RUN_L" -eq 1 ] && CASE_LANES="$CASE_LANES L"
    687 [ "$RUN_X" -eq 1 ] && CASE_LANES="$CASE_LANES X"
    688 [ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C"
    689 [ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W"
    690 
    691 KIT_LABEL=toy KIT_BUILD_DIR="$BUILD_DIR" \
    692   KIT_CORPUS_GLOBS="$TEST_DIR/cases/*.toy" KIT_CORPUS_EXT=toy \
    693   KIT_SIDECAR_DIR="$TEST_DIR/cases" \
    694   KIT_LANES="$CASE_LANES" KIT_OPT_LEVELS="$TOY_OPT_LEVELS" \
    695   KIT_TUPLES="host-host" KIT_TARGETS_EXT="" KIT_OPT0ONLY="C W" \
    696   KIT_PARALLELIZABLE="$PAR" KIT_FLUSH_VERIFY=tf_flush_verify \
    697   kit_corpus_run
    698 
    699 # err cases exercise compile-failure paths; they aren't relevant to path C/W
    700 # (which go through the same cc invocation). Only run them when at least one of
    701 # the native compile paths (R/L/X) is enabled.
    702 if [ "$RUN_R" -eq 1 ] || [ "$RUN_L" -eq 1 ] || [ "$RUN_X" -eq 1 ]; then
    703     err_cases=("$TEST_DIR"/err/*.toy)
    704     err_have_match=1
    705     if [ -n "${KIT_TEST_FILTER:-}" ]; then
    706         err_have_match=0
    707         for src in "${err_cases[@]}"; do
    708             base="$(basename "${src%.toy}")"
    709             case "$base" in *"$KIT_TEST_FILTER"*) err_have_match=1; break ;; esac
    710         done
    711     fi
    712     if [ "${#err_cases[@]}" -gt 0 ] && [ "$err_have_match" -eq 1 ]; then
    713         KIT_LABEL=toy KIT_BUILD_DIR="$BUILD_DIR/err" \
    714           KIT_CORPUS_GLOBS="$TEST_DIR/err/*.toy" KIT_CORPUS_EXT=toy \
    715           KIT_SIDECAR_DIR="$TEST_DIR/err" \
    716           KIT_LANES="ERR" KIT_OPT_LEVELS="" KIT_TUPLES="host-host" \
    717           KIT_TARGETS_EXT="" KIT_PARALLELIZABLE="$PAR" \
    718           kit_corpus_run
    719     fi
    720 fi
    721 
    722 # toy treats skips (interp-unsupported ops, no cross-arch runner, the
    723 # intentionally-red musttail cases, ...) as non-fatal — matching the original
    724 # runner, which only ever exited nonzero on a real FAIL.
    725 KIT_SKIP_IS_FAILURE=0
    726 kit_summary toy
    727 kit_exit