kit

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

commit 6aa07f2e4635fb0b1af0c0afc2ab2360dbda43c5
parent f4828d2f04a2c393f74e136359bd15952323ac81
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 30 May 2026 06:46:18 -0700

test+asm: Toy-corpus L2 round-trip lane; fix .inst-dropped miscompile

Reuse the ~150-case Toy corpus (full CG op set + exit-code oracle) for free
round-trip coverage far beyond the hand-written set: per case, native
`cfree run case.toy` vs `cfree cc -S | cfree as | cfree run`, exit codes must
match (test/asm/roundtrip_toy.sh, test-asm-roundtrip-toy). 292/0/10.

It immediately found a silent miscompile the hand corpus never reached: a
multiply-high (smulh/umulh) the disassembler can't decode is emitted by cc -S
as `.inst 0x<word>`, and `as` *dropped* `.inst` (emitting no bytes) — deleting
the instruction and shifting every following branch. `.inst` now emits the
4-byte word(s) like GNU as / llvm-mc, so it round-trips correctly even before
the decode lands (132_intrinsic_bit_and_overflow: was exit 6, now 42).

jit-runner gains --entry <sym> (was hard-wired to test_main) so non-C
frontends (toy main) can be driven.

The 10 lane skips are documented backlog: cc -S symbolizer gaps (tentative/
common, merged strings, computed-goto label-address tables) and a Mach-O
object-file jump-table reloc crash (codegen's macho .o and the assemble-inline
path both work; an as-macho-obj axis, not a -S|as issue). Regressions green:
asm, symmetry (baseline unchanged), roundtrip 572/0, ISA unit.

Diffstat:
Mdoc/ASM_ROUNDTRIP_TESTING.md | 31+++++++++++++++++++++++++++++++
Msrc/asm/asm.c | 15+++++++++++++++
Atest/asm/roundtrip_toy.sh | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/link/harness/jit_runner.c | 7+++++--
Mtest/test.mk | 9+++++++++
5 files changed, 153 insertions(+), 2 deletions(-)

diff --git a/doc/ASM_ROUNDTRIP_TESTING.md b/doc/ASM_ROUNDTRIP_TESTING.md @@ -42,6 +42,13 @@ round-trip passes all three lanes at `-O0` and `-O1`: **858 lane-checks pass, the `bss_size` cursor for a NOBITS section, so `.zero`/labels reserve space rather than writing a byte buffer the emitters drop. (Codegen is unaffected — it never `obj_write`s/`obj_pos`es a BSS section.) Closed the last corpus skip. +- **`.inst` emits the word (DONE).** `as` silently dropped the `.inst` directive + (emitting no bytes), so an instruction the disassembler can't decode yet + (`.inst 0x<word>` in `cc -S`) was *deleted* on re-assembly — a silent + miscompile, and every following branch offset shifted. `.inst` now emits the + 4-byte word(s) like GNU as / llvm-mc. Found by the Toy round-trip lane below + (a multiply-high the disassembler doesn't decode); it now round-trips + correctly even before the decode lands. - **Exclusive / acquire-release atomic decode (DONE).** The assembler already encoded `ldxr`/`ldaxr`/`stxr`/`stlxr`/`ldar`/`stlr` (+ b/h), but the disassembler rendered them `.inst`, so the atomic RMW sequence codegen emits @@ -119,6 +126,30 @@ the baseline (`bash test/asm/symmetry.sh --update`). RelocKind→syntax tables below. The self-symmetry sweep and llvm differential are aa64-only too. +### Toy-corpus L2 round-trip (`test-asm-roundtrip-toy`) + +Reuses the ~150-case Toy corpus (`test/toy/cases/`, which exercises the full CG +op set and carries an exit-code oracle) for free L2 coverage far beyond the +hand-written `roundtrip/` set (`test/asm/roundtrip_toy.sh`). For each case, +native: `cfree run case.toy` (direct) vs `cfree cc -S | cfree as | cfree run` +(round-trip); the exit codes must match. `cfree run` propagates `main()`'s +return on the native arch (aarch64 macOS), so the oracle is the exit code. + +292 pass / 0 fail / 10 skip. It immediately found a real miscompile — see the +`.inst` fix above. The 10 skips are two backlog groups: +- **`cc -S` symbolizer gaps** — `as` hits an undefined reference / "symbol + required" because the disassembly omits a symbol kind: tentative/common + (`.comm`) definitions, merged-string symbols, and computed-`goto` + label-address tables (`62_decl_data_attrs`, `118_decl_extra_attrs`, + `51_labeladdr_goto`, `123_spec_demo`). +- **Mach-O object-file jump table** — an `as`-produced macho `.o` with a switch + jump table crashes when run, while codegen's macho `.o` and the assemble- + inline path (`cfree run x.s`) both work — a macho obj write/read axis, not a + `-S`|`as` round-trip issue (the ELF round-trip of jump tables is green). +- Also tracked: `smulh`/`umulh` (and `smaddl`/`umaddl`/`smsubl`/`umsubl`) DP3 + long/high-multiply *decode* (the `.inst` the lane found; correctness is + already restored by the `.inst` fix, but `-S` still shows `.inst` for them). + ### llvm differential (`test-diff-llvm`) A second-oracle cross-check against llvm (`test/asm/diff_llvm.sh`), byte-level diff --git a/src/asm/asm.c b/src/asm/asm.c @@ -830,6 +830,21 @@ static void do_directive(AsmDriver* d, Sym name) { d_skip_to_eol(d); return; } + /* .inst WORD[, WORD...] — emit raw 32-bit instruction word(s), little-endian + * (AArch64/RISC-V are fixed 4-byte). This is how `cc -S` round-trips an + * instruction the disassembler can't decode yet (`.inst 0x<word>`); silently + * dropping it — the old behavior — deletes the instruction and miscompiles. + * Matches GNU as / llvm-mc, which emit the word. */ + if (sym_eq(d, name, "inst")) { + (void)asm_driver_cur_section(d); + for (;;) { + i64 v = asm_driver_parse_const(d); + emit_le(d, (u64)v, 4); + if (!asm_driver_eat_comma(d)) break; + } + d_skip_to_eol(d); + return; + } if (sym_eq(d, name, "ascii") || sym_eq(d, name, "asciz") || sym_eq(d, name, "string")) { int term = !sym_eq(d, name, "ascii"); diff --git a/test/asm/roundtrip_toy.sh b/test/asm/roundtrip_toy.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# test/asm/roundtrip_toy.sh — L2 exec round-trip over the Toy corpus (native). +# +# The Toy frontend exercises the full CG op set, and each case carries an +# exit-code oracle (test/toy/cases/<name>.expected). This lane reuses that +# corpus for free round-trip coverage far beyond the hand-written +# test/asm/roundtrip/ set: for every case, compare the DIRECT compile to the +# round-tripped one and require the same exit code — +# +# direct: cfree run case.toy +# round-trip: cfree cc -S case.toy | cfree as | cfree run <obj> +# +# Native target (aarch64 macOS here): cfree run propagates main()'s return as +# the process exit, so the oracle is the exit code. This found a real miscompile +# (a multiply-high the disassembler couldn't decode, dropped by `as` until the +# `.inst` fix) that the hand corpus never reached. +# +# Cases that hit a known `cc -S` symbolizer gap (tentative/common defs, merged +# strings, computed-goto label-address tables) are listed in SKIP below until +# those land; the lane stays green and gates regressions. Opt-in. + +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CFREE="$ROOT/build/cfree" +CASES="$ROOT/test/toy/cases" +WORK="$ROOT/build/test/asm/roundtrip_toy" +OPTS="${CFREE_TEST_OPTS:-O0 O1}" +FILTER="${1:-}" + +# Known gaps the toy corpus surfaced (see doc/ASM_ROUNDTRIP_TESTING.md): +# (a) cc -S symbolizer gaps — `as` fails with an undefined reference / +# "symbol required" because the disassembly omits a symbol kind it can't +# resolve: tentative/common (.comm) defs, merged strings, and computed-goto +# label-address tables. +# (b) Mach-O object-file jump-table relocation — an `as`-produced .o with a +# switch jump table crashes when run, while codegen's macho .o and the +# assemble-inline path (`cfree run x.s`) both work. A macho obj write/read +# axis, not a -S|as round-trip issue. +SKIP="62_decl_data_attrs 118_decl_extra_attrs 51_labeladdr_goto 123_spec_demo \ + 118_many_enum_switch_values 119_static_labeladdr_data 119_switch_strategy_hints \ + 125_switch_dense_boundaries 127_switch_forced_jump_table 47_target_arch_switch" + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +if [ ! -x "$CFREE" ]; then + printf 'roundtrip-toy: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2 + exit 1 +fi +mkdir -p "$WORK" + +pass=0; fail=0; skip=0; failnames=() +is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; } + +shopt -s nullglob +for src in "$CASES"/*.toy; do + name="$(basename "$src" .toy)" + [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue + if is_skip "$name"; then + skip=$((skip+1)); printf ' %s %s — known cc -S symbolizer gap\n' "$(color_yel SKIP)" "$name"; continue + fi + exp=0; [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected") + exp=$((exp & 255)) + for opt in $OPTS; do + w="$WORK/$name/$opt"; mkdir -p "$w" + if ! "$CFREE" cc -S "-$opt" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then + fail=$((fail+1)); failnames+=("$name[-$opt] cc-S"); printf ' %s %s[-%s] cc -S failed\n' "$(color_red FAIL)" "$name" "$opt"; continue + fi + if ! "$CFREE" as "$w/s.s" -o "$w/rt.o" 2>"$w/as.err"; then + fail=$((fail+1)); failnames+=("$name[-$opt] as: $(head -1 "$w/as.err"|sed 's|.*: ||')") + printf ' %s %s[-%s] as failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/as.err"|sed 's|.*: ||')"; continue + fi + "$CFREE" run "$w/rt.o" >"$w/out" 2>"$w/run.err"; rc=$? + if [ -s "$w/run.err" ]; then + fail=$((fail+1)); failnames+=("$name[-$opt] run: $(head -1 "$w/run.err"|sed 's|.*: ||')") + printf ' %s %s[-%s] run failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/run.err"|sed 's|.*: ||')"; continue + fi + if [ "$rc" -eq "$exp" ]; then + pass=$((pass+1)) + else + fail=$((fail+1)); failnames+=("$name[-$opt] exit $rc != $exp") + printf ' %s %s[-%s] exit %d != expected %d\n' "$(color_red FAIL)" "$name" "$opt" "$rc" "$exp" + fi + done +done +shopt -u nullglob + +printf '\n' +[ "${#failnames[@]}" -gt 0 ] && { printf 'Failed:\n'; for f in "${failnames[@]}"; do printf ' %s\n' "$f"; done; } +printf 'roundtrip-toy: %d pass, %d fail, %d skip\n' "$pass" "$fail" "$skip" +[ "$fail" -eq 0 ] diff --git a/test/link/harness/jit_runner.c b/test/link/harness/jit_runner.c @@ -435,6 +435,7 @@ int main(int argc, char** argv) { * --check-present SYM: after link, verify symbol IS in image. */ const char* check_absent = NULL; const char* check_present = NULL; + const char* entry_sym = "test_main"; const char* script_path = NULL; CfreeSlice objs[64]; @@ -460,6 +461,8 @@ int main(int argc, char** argv) { check_present = argv[++i]; } else if (!strcmp(argv[i], "--linker-script") && i + 1 < argc) { script_path = argv[++i]; + } else if (!strcmp(argv[i], "--entry") && i + 1 < argc) { + entry_sym = argv[++i]; } else { uint8_t* data; size_t len; @@ -515,7 +518,7 @@ int main(int argc, char** argv) { CfreeLinkScript* script = NULL; memset(&opts, 0, sizeof(opts)); opts.output_kind = CFREE_LINK_OUTPUT_JIT; - opts.entry = CFREE_SLICE_LIT("test_main"); + opts.entry = cfree_slice_cstr(entry_sym); opts.gc_sections = gc_sections; opts.jit_host = &jhost; if (use_resolver) { @@ -598,7 +601,7 @@ int main(int argc, char** argv) { } void* wasm_init = cfree_jit_lookup(jit, CFREE_SLICE_LIT("__cfree_wasm_init")); - void* entry = cfree_jit_lookup(jit, CFREE_SLICE_LIT("test_main")); + void* entry = cfree_jit_lookup(jit, cfree_slice_cstr(entry_sym)); int (*fn)(void) = entry; /* TLS local-exec setup. Build the static TLS block and install the diff --git a/test/test.mk b/test/test.mk @@ -41,6 +41,7 @@ TEST_TARGETS = \ test-asm-roundtrip \ test-asm-roundtrip-exec \ test-asm-symmetry \ + test-asm-roundtrip-toy \ test-diff-llvm \ test-bounce \ test-cbackend \ @@ -672,6 +673,14 @@ test-asm-symmetry: $(ASM_RUNNER) $(AA64_SWEEP_GEN) test-diff-llvm: bin @CFREE_TEST_OPTS="O0 O1" bash test/asm/diff_llvm.sh +# test-asm-roundtrip-toy: L2 exec round-trip over the Toy corpus (native arch). +# Reuses the ~150 toy cases (full CG op set, exit-code oracle) for free +# round-trip coverage: cfree cc -S | cfree as | cfree run, exit must match. +# Opt-in; native target. Found a real miscompile (dropped .inst) the hand +# corpus never reached. +test-asm-roundtrip-toy: bin + @bash test/asm/roundtrip_toy.sh + test-wasm: test-wasm-front test-wasm-target test-wasm-toy test-wasm-front: bin $(WASM_TOOL) $(LINK_EXE_RUNNER) $(JIT_RUNNER)