commit 7ef74a962f314f7d01416856d4484aff2ed07079
parent 22b8a80d7271c883acfddcfc5948b058c2c8e716
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 29 May 2026 22:57:37 -0700
test+aa64: asm<->disasm self-symmetry sweep (decode<->encode)
A new sweep checks the assembler and disassembler agree on the instruction
set, independent of codegen (the codegen round-trip only exercises the
disassembler on what the compiler emits):
decode-side: aa64_sweep_gen emits a representative encoding per disasm-table
row; the harness decodes -> re-assembles -> decodes and requires a text
fixed point. Catches decode-only / disagreeing forms.
encode-side: assemble every aa64 encode/*.s and disassemble; any .inst is a
form the assembler emits that the disassembler can't decode.
Known asymmetries are snapshotted in test/asm/symmetry.baseline; the sweep
gates against *new* asymmetry while the baseline documents the disasm-
completeness backlog (test-asm-symmetry, opt-in, ~2s).
Closed in passing (found by the sweep): fmax/fmin/fnmul (decoded but the
assembler lacked them) and the non-acquire byte/half exclusives
ldxrb/ldxrh/stxrb/stxrh (encodable but undecoded). Baseline starts at 69
encode-only forms (LSE/CAS/writeback/signed-regoff/logical-imm/q/...), all
instructions codegen never emits.
Diffstat:
8 files changed, 306 insertions(+), 0 deletions(-)
diff --git a/doc/ASM_ROUNDTRIP_TESTING.md b/doc/ASM_ROUNDTRIP_TESTING.md
@@ -45,6 +45,31 @@ follow-up below). L0+L1 are wired into the default `make test` via
(atomics were the one core-op family the corpus fan-out missed); now
`roundtrip/atomic_{rmw,cas,ops}`.
+### asm⊗disasm self-symmetry sweep (`test-asm-symmetry`)
+
+The codegen round-trip only exercises the disassembler on instructions the
+compiler emits. A complementary sweep checks the *tools' own* instruction set
+for asm⊗disasm symmetry, independent of codegen (`test/asm/symmetry.sh`):
+
+- **decode-side** (`test/arch/aa64_sweep_gen.c`): synthesize one representative
+ encoding per row of `aa64_insn_table`, decode → re-assemble → decode, and
+ require the disassembly text to be a fixed point. Catches a form the
+ disassembler decodes but the assembler can't re-encode, or where they
+ disagree. Now clean (closed `fmax`/`fmin`/`fnmul`, missing from `as`).
+- **encode-side**: assemble every aa64 `test/asm/encode/*.s` and disassemble;
+ any `.inst` is a form the assembler encodes but the disassembler can't decode.
+
+Known asymmetries live in a checked-in snapshot, `test/asm/symmetry.baseline`;
+the sweep passes iff the current set equals it, so it **gates against new
+asymmetry** (a regression) while the baseline is the disasm-completeness
+backlog. The current 69 entries are all encode-only (the assembler accepts
+these for completeness but codegen never emits them, so the disassembler never
+had to decode them): LSE atomics (`ldadd`/`swp`/…), CAS, single-register
+writeback ld/st (`ldr x,[x,#imm]!`), signed register-offset ld/st, logical-
+immediate (`mov #bitmask`/`orr #imm`), the `bfm` bitfield + aliases, 128-bit
+`q` ld/st, and a couple of `ldp`/`stp` variants. Closing any of these shrinks
+the baseline (`bash test/asm/symmetry.sh --update`).
+
### Earlier vertical-slice notes (aa64)
- **L0 decode-completeness** — `cc -S` already emits the distinct, re-assemblable
diff --git a/src/arch/aa64/asm.c b/src/arch/aa64/asm.c
@@ -1712,6 +1712,9 @@ static void p_fadd(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FADD); }
static void p_fsub(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FSUB); }
static void p_fmul(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FMUL); }
static void p_fdiv(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FDIV); }
+static void p_fmax(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FMAX); }
+static void p_fmin(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FMIN); }
+static void p_fnmul(AsmDriver* d) { p_fp_dp2(d, AA64_FP_DP2_FNMUL); }
static void p_fneg(AsmDriver* d) { p_fp_dp1(d, AA64_FP_DP1_FNEG); }
static void p_fabs(AsmDriver* d) { p_fp_dp1(d, AA64_FP_DP1_FABS); }
static void p_fsqrt(AsmDriver* d) { p_fp_dp1(d, AA64_FP_DP1_FSQRT); }
@@ -2010,6 +2013,9 @@ static const AA64Mn kTable[] = {
{"fsub", p_fsub, 0},
{"fmul", p_fmul, 0},
{"fdiv", p_fdiv, 0},
+ {"fmax", p_fmax, 0},
+ {"fmin", p_fmin, 0},
+ {"fnmul", p_fnmul, 0},
{"fneg", p_fneg, 0},
{"fabs", p_fabs, 0},
{"fsqrt", p_fsqrt, 0},
diff --git a/src/arch/aa64/isa.c b/src/arch/aa64/isa.c
@@ -322,6 +322,10 @@ const AA64InsnDesc aa64_insn_table[] = {
{MN("stxr"), 0x88000000u, 0xBFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
{MN("stlxr"), 0x88008000u, 0xBFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
{MN("stlr"), 0x88808000u, 0xBFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
+ {MN("ldxrb"), 0x08400000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
+ {MN("ldxrh"), 0x48400000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
+ {MN("stxrb"), 0x08000000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
+ {MN("stxrh"), 0x48000000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
{MN("ldaxrb"), 0x08408000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
{MN("ldaxrh"), 0x48408000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
{MN("ldarb"), 0x08C08000u, 0xFFE08000u, AA64_FMT_LDST_EXCL, 0, {0, 0}},
diff --git a/src/arch/aa64/isa.h b/src/arch/aa64/isa.h
@@ -801,6 +801,9 @@ static inline u32 aa64_str64_uimm12(u32 Rt, u32 Rn, u32 imm12_scaled) {
#define AA64_FP_DP2_FDIV 0x1800u
#define AA64_FP_DP2_FADD 0x2800u
#define AA64_FP_DP2_FSUB 0x3800u
+#define AA64_FP_DP2_FMAX 0x4800u
+#define AA64_FP_DP2_FMIN 0x5800u
+#define AA64_FP_DP2_FNMUL 0x8800u
#define AA64_FP_DP1_FMOV 0x4000u
#define AA64_FP_DP1_FABS 0xC000u
#define AA64_FP_DP1_FNEG 0x14000u
diff --git a/test/arch/aa64_sweep_gen.c b/test/arch/aa64_sweep_gen.c
@@ -0,0 +1,62 @@
+/* aa64_sweep_gen — emit one representative encoding per row of aa64_insn_table
+ * (as little-endian hex words), for the asm<->disasm self-symmetry round-trip
+ * (test/asm/symmetry.sh).
+ *
+ * The harness decodes each word, re-assembles the disassembly, and decodes
+ * again, asserting the text is a fixed point. That catches the two decode-side
+ * asymmetries the codegen round-trip can't reach systematically: a form the
+ * disassembler decodes but the assembler can't re-encode (assemble error), and
+ * a form where decode and encode disagree on the bytes (text changes across the
+ * round-trip). All decode/encode goes through asm-runner, so the comparison is
+ * immune to any formatting difference between rendering paths.
+ *
+ * Variable (~mask) bits are filled with a fixed representative pattern that
+ * gives distinct, non-ZR/SP registers in the standard field positions; the
+ * text-fixed-point criterion tolerates the resulting don't-care bits (e.g. the
+ * RES Rt2 field of an exclusive load). Formats whose only operand is a
+ * PC-relative target or a relocation — branches and adr/adrp — are skipped:
+ * they aren't assemblable standalone without a label/symbol, and L1/L2 of the
+ * codegen round-trip already cover them.
+ *
+ * Builds against the internal arch/aa64/isa.h surface (test.mk passes -Isrc). */
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include "arch/aa64/isa.h"
+
+static int needs_context(uint8_t fmt) {
+ switch (fmt) {
+ case AA64_FMT_BR_IMM: /* b/bl: numeric target needs a label */
+ case AA64_FMT_BR_COND: /* b.cc */
+ case AA64_FMT_CB: /* cbz/cbnz */
+ case AA64_FMT_PCREL_ADR: /* adr/adrp: needs a symbol operand */
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+int main(void) {
+ /* Rt/Rd[4:0]=1, Rn[9:5]=2, Rt2/Ra[14:10]=3, Rm/Rs[20:16]=4 — distinct,
+ * non-ZR/SP so ZR-keyed aliases don't fire; small immediates elsewhere. */
+ const uint32_t PATTERN =
+ (1u << 0) | (2u << 5) | (3u << 10) | (4u << 16);
+ for (uint32_t i = 0; i < aa64_insn_table_n; ++i) {
+ const AA64InsnDesc* d = &aa64_insn_table[i];
+ uint32_t word;
+ if (needs_context(d->fmt)) continue;
+ word = (d->match & d->mask) | (PATTERN & ~d->mask);
+ /* The register-offset form's option field [15:13] must name a valid
+ * extend; the bare fill pattern yields the invalid UXTB(000). Force
+ * LSL/UXTX(011) so the synthesized word is a real instruction. */
+ if (d->fmt == AA64_FMT_LDST_REGOFF)
+ word = (word & ~(7u << 13)) | (3u << 13);
+ /* Only sweep words that decode cleanly to this kind of instruction. */
+ if (!aa64_disasm_find(word)) continue;
+ printf("%02x%02x%02x%02x\n", (unsigned)(word & 0xff),
+ (unsigned)((word >> 8) & 0xff), (unsigned)((word >> 16) & 0xff),
+ (unsigned)((word >> 24) & 0xff));
+ }
+ return 0;
+}
diff --git a/test/asm/symmetry.baseline b/test/asm/symmetry.baseline
@@ -0,0 +1,69 @@
+encode-only: aa64_bitfield_dp1 0xb34840a4
+encode-only: aa64_compare_and_swap 0x48a07c41
+encode-only: aa64_compare_and_swap 0x48a0fc41
+encode-only: aa64_compare_and_swap 0x48e07c41
+encode-only: aa64_compare_and_swap 0x48e0fc41
+encode-only: aa64_compare_and_swap 0x88a07c41
+encode-only: aa64_compare_and_swap 0x88a0fc41
+encode-only: aa64_compare_and_swap 0x88e07c41
+encode-only: aa64_compare_and_swap 0x88e0fc41
+encode-only: aa64_compare_and_swap 0x8a07c41
+encode-only: aa64_compare_and_swap 0x8a0fc41
+encode-only: aa64_compare_and_swap 0x8e07c41
+encode-only: aa64_compare_and_swap 0x8e0fc41
+encode-only: aa64_compare_and_swap 0xc8a07c41
+encode-only: aa64_compare_and_swap 0xc8e07c41
+encode-only: aa64_compare_and_swap 0xc8e0fc41
+encode-only: aa64_fp_ldst 0x3d800540
+encode-only: aa64_fp_ldst 0x3dc00141
+encode-only: aa64_ldp_stp_index 0x28c153f3
+encode-only: aa64_ldp_stp_index 0x29bf53f3
+encode-only: aa64_ldst_pre_post_index 0x381ffc20
+encode-only: aa64_ldst_pre_post_index 0x38401420
+encode-only: aa64_ldst_pre_post_index 0x38401c20
+encode-only: aa64_ldst_pre_post_index 0x38801c20
+encode-only: aa64_ldst_pre_post_index 0x38c01420
+encode-only: aa64_ldst_pre_post_index 0x78002420
+encode-only: aa64_ldst_pre_post_index 0x78402c20
+encode-only: aa64_ldst_pre_post_index 0x78802c20
+encode-only: aa64_ldst_pre_post_index 0x78c02420
+encode-only: aa64_ldst_pre_post_index 0xb8004c20
+encode-only: aa64_ldst_pre_post_index 0xb8404c20
+encode-only: aa64_ldst_pre_post_index 0xb85fc420
+encode-only: aa64_ldst_pre_post_index 0xb8804420
+encode-only: aa64_ldst_pre_post_index 0xb8804c20
+encode-only: aa64_ldst_pre_post_index 0xf80ff420
+encode-only: aa64_ldst_pre_post_index 0xf8100c20
+encode-only: aa64_ldst_pre_post_index 0xf8408420
+encode-only: aa64_ldst_pre_post_index 0xf8408c20
+encode-only: aa64_ldst_regoff 0x38a26820
+encode-only: aa64_ldst_regoff 0x38e26820
+encode-only: aa64_ldst_regoff 0x78a27820
+encode-only: aa64_ldst_regoff 0x78e27820
+encode-only: aa64_ldst_regoff 0xb8a27820
+encode-only: aa64_ldst_regoff 0xb8a2d820
+encode-only: aa64_lse_atomics 0x38200041
+encode-only: aa64_lse_atomics 0x38203041
+encode-only: aa64_lse_atomics 0x38208041
+encode-only: aa64_lse_atomics 0x78200041
+encode-only: aa64_lse_atomics 0x78201041
+encode-only: aa64_lse_atomics 0x78208041
+encode-only: aa64_lse_atomics 0xb8200041
+encode-only: aa64_lse_atomics 0xb8201041
+encode-only: aa64_lse_atomics 0xb8202041
+encode-only: aa64_lse_atomics 0xb8203041
+encode-only: aa64_lse_atomics 0xb8208041
+encode-only: aa64_lse_atomics 0xb8600041
+encode-only: aa64_lse_atomics 0xb8601041
+encode-only: aa64_lse_atomics 0xb8608041
+encode-only: aa64_lse_atomics 0xb8a00041
+encode-only: aa64_lse_atomics 0xb8a08041
+encode-only: aa64_lse_atomics 0xb8e00041
+encode-only: aa64_lse_atomics 0xb8e02041
+encode-only: aa64_lse_atomics 0xb8e08041
+encode-only: aa64_lse_atomics 0xf8200041
+encode-only: aa64_lse_atomics 0xf8208041
+encode-only: aa64_lse_atomics 0xf8a03041
+encode-only: aa64_mov_orr_bitmask 0x3204cfe4
+encode-only: aa64_mov_orr_bitmask 0xb201f3e3
+encode-only: aa64_mov_orr_bitmask 0xb204c3e1
diff --git a/test/asm/symmetry.sh b/test/asm/symmetry.sh
@@ -0,0 +1,120 @@
+#!/usr/bin/env bash
+# test/asm/symmetry.sh — asm<->disasm self-symmetry sweep (aa64).
+#
+# Systematically checks that the assembler and disassembler agree on the
+# instruction set, in both directions:
+#
+# decode-side (table sweep): aa64_sweep_gen emits one representative encoding
+# per row of the disassembler's instruction table. Each is decoded, the
+# disassembly re-assembled, and decoded again; the text must be a FIXED
+# POINT. A form the assembler can't re-encode is "decode-only"; one that
+# re-encodes to a different instruction is a "disagree".
+#
+# encode-side (corpus sweep): every test/asm/encode/*.s that applies to aa64
+# is assembled and disassembled; any `.inst` means the assembler emitted a
+# byte the disassembler can't decode ("encode-only").
+#
+# The two tools cover slightly different ISA subsets (forms the assembler
+# accepts for completeness but codegen never emits, so the disassembler never
+# had to decode them, and vice-versa). The known asymmetries live in a checked-
+# in snapshot, test/asm/symmetry.baseline; the sweep PASSES iff the current set
+# equals the baseline, so it gates against *new* asymmetry (a regression) while
+# the baseline documents the disasm-completeness backlog. Closing a gap shrinks
+# the baseline (regenerate with --update). See doc/ASM_ROUNDTRIP_TESTING.md.
+#
+# Opt-in; host-independent (no execution). Decode-side assembles line-by-line,
+# so it is a few seconds.
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+AR="$ROOT/build/test/asm-runner"
+GEN="$ROOT/build/test/aa64_sweep_gen"
+ENCODE_DIR="$ROOT/test/asm/encode"
+WORK="$ROOT/build/test/asm/symmetry"
+BASELINE="$ROOT/test/asm/symmetry.baseline"
+UPDATE=0
+[ "${1:-}" = "--update" ] && UPDATE=1
+
+color_red() { printf '\033[31m%s\033[0m' "$1"; }
+color_grn() { printf '\033[32m%s\033[0m' "$1"; }
+
+if [ ! -x "$AR" ] || [ ! -x "$GEN" ]; then
+ printf '%s asm-runner / aa64_sweep_gen missing — run the test target\n' \
+ "$(color_red FATAL)" >&2
+ exit 2
+fi
+
+export CFREE_TEST_ARCH=aa64
+mkdir -p "$WORK"
+report="$WORK/report"
+: > "$report"
+
+strip_off() { sed -E 's/^[0-9a-f]+:\t//'; }
+
+# ---- decode-side: table sweep ---------------------------------------------
+# Fast path: re-assemble the whole disassembly at once; if it assembles, the
+# round-trip is decode->encode->decode and we compare the text line-for-line.
+# Slow path (only when whole-file assembly fails, i.e. some form is decode-
+# only): re-assemble line-by-line to name each offending mnemonic.
+"$GEN" > "$WORK/words.hex"
+"$AR" --decode "$WORK/words.hex" "$WORK/t1.txt" 2>/dev/null
+strip_off < "$WORK/t1.txt" > "$WORK/t1.norm"
+{ printf '\t.text\n'; sed -E 's/^[0-9a-f]+:\t/\t/' "$WORK/t1.txt"; } > "$WORK/t1.s"
+if "$AR" --encode "$WORK/t1.s" "$WORK/hex2.hex" 2>/dev/null; then
+ "$AR" --decode "$WORK/hex2.hex" "$WORK/t2.txt" 2>/dev/null
+ strip_off < "$WORK/t2.txt" > "$WORK/t2.norm"
+ awk 'NR==FNR { a[FNR] = $0; next }
+ a[FNR] != $0 { printf "disagree: %s => %s\n", a[FNR], $0 }' \
+ "$WORK/t1.norm" "$WORK/t2.norm" >> "$report"
+else
+ while IFS= read -r line; do
+ [ -z "$line" ] && continue
+ mnem=$(printf '%s' "$line" | sed -E 's/^\t?([a-z0-9.]+).*/\1/')
+ printf '\t.text\n%s\n' "$line" > "$WORK/one.s"
+ if ! "$AR" --encode "$WORK/one.s" "$WORK/one.hex" 2>/dev/null; then
+ printf 'decode-only: %s\n' "$mnem" >> "$report"
+ continue
+ fi
+ "$AR" --decode "$WORK/one.hex" "$WORK/one.t2" 2>/dev/null
+ t2=$(strip_off < "$WORK/one.t2")
+ norm_line=$(printf '%s' "$line" | sed -E 's/^\t//')
+ [ "$norm_line" != "$t2" ] && \
+ printf 'disagree: %s => %s\n' "$norm_line" "$t2" >> "$report"
+ done < "$WORK/t1.norm"
+fi
+
+# ---- encode-side: corpus sweep --------------------------------------------
+shopt -s nullglob
+for s in "$ENCODE_DIR"/*.s; do
+ name="$(basename "$s" .s)"
+ tg="$ENCODE_DIR/$name.targets"
+ [ -f "$tg" ] && ! grep -qE 'aa64|aarch64|arm64' "$tg" && continue
+ # Skip cases that deliberately place data in .text (not instructions).
+ grep -qE '^\s*\.(uleb128|sleb128|byte|hword|short|word|long|quad|ascii|asciz|string|zero|fill|space|inst)\b' "$s" && continue
+ w="$WORK/$name"
+ "$AR" --encode "$s" "$w.hex" 2>/dev/null || continue
+ "$AR" --decode "$w.hex" "$w.dec" 2>/dev/null
+ awk -v n="$name" '/[[:space:]]\.inst[[:space:]]/{print "encode-only: " n " " $NF}' "$w.dec" >> "$report"
+done
+shopt -u nullglob
+
+sort -u "$report" -o "$report"
+
+# ---- compare against the baseline snapshot --------------------------------
+if [ $UPDATE -eq 1 ]; then
+ cp "$report" "$BASELINE"
+ printf 'symmetry: baseline updated (%d known asymmetries)\n' "$(wc -l < "$report" | tr -d ' ')"
+ exit 0
+fi
+[ -f "$BASELINE" ] || : > "$BASELINE"
+if diff -u "$BASELINE" "$report" > "$WORK/delta.diff" 2>&1; then
+ printf 'symmetry: %s (%d known asymmetries in baseline)\n' \
+ "$(color_grn 'no new asm<->disasm asymmetry')" \
+ "$(wc -l < "$BASELINE" | tr -d ' ')"
+ exit 0
+fi
+printf 'symmetry: %s\n' "$(color_red 'asymmetry set changed vs baseline')"
+printf ' (+ = new asymmetry / regression, - = fixed; regenerate with --update)\n'
+grep -E '^[-+][^-+]' "$WORK/delta.diff" | head -30 | sed 's/^/ /'
+exit 1
diff --git a/test/test.mk b/test/test.mk
@@ -40,6 +40,7 @@ TEST_TARGETS = \
test-disasm-complete \
test-asm-roundtrip \
test-asm-roundtrip-exec \
+ test-asm-symmetry \
test-bounce \
test-cbackend \
test-cg-api \
@@ -270,6 +271,14 @@ $(RV64_DECODE_TEST_BIN): test/arch/rv64_decode_test.c $(LIB_OBJS)
@mkdir -p $(dir $@)
$(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/rv64_decode_test.c $(LIB_OBJS) -o $@
+# aa64_sweep_gen: emits one representative encoding per disasm-table row for the
+# asm<->disasm self-symmetry sweep (test/asm/symmetry.sh). Needs the internal
+# arch/aa64/isa.h surface, so -Isrc + LIB_OBJS like the ISA unit test.
+AA64_SWEEP_GEN = build/test/aa64_sweep_gen
+$(AA64_SWEEP_GEN): test/arch/aa64_sweep_gen.c $(LIB_OBJS)
+ @mkdir -p $(dir $@)
+ $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_sweep_gen.c $(LIB_OBJS) -o $@
+
# test-emu: emulator unit tests. The rv64 lane builds a tiny in-memory rv64
# ELF and asserts the lifted/JIT path exits through the syscall handler with
# the expected code. Internal arch/emu surface — needs -Isrc.
@@ -646,6 +655,14 @@ test-asm-roundtrip-exec: bin $(JIT_RUNNER)
@CFREE_TEST_ARCH=aa64 CFREE_TEST_OPTS="O0 O1" CFREE_TEST_PATHS=012 \
bash test/asm/roundtrip.sh
+# test-asm-symmetry: asm<->disasm self-symmetry sweep (aa64). Decode-side sweeps
+# every disasm-table form (decode->encode->decode fixed point); encode-side
+# asserts every byte the assembler emits over the encode corpus is decodable.
+# Catches encode/decode asymmetries the codegen round-trip can't reach (e.g. a
+# form one tool handles and the other doesn't). Host-independent, no exec.
+test-asm-symmetry: $(ASM_RUNNER) $(AA64_SWEEP_GEN)
+ @bash test/asm/symmetry.sh
+
test-wasm: test-wasm-front test-wasm-target test-wasm-toy
test-wasm-front: bin $(WASM_TOOL) $(LINK_EXE_RUNNER) $(JIT_RUNNER)