Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 391 additions & 28 deletions crates/mirth-lab/src/tools/abi_diff.rs

Large diffs are not rendered by default.

57 changes: 56 additions & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,61 @@ the C side with clang and the Rust side with rustc for each target, and compare
bits. **The ABI generator already compiles signatures for every target; this adds clang and the
comparison.**

#### Assembly-level triage (`abi-diff --asm`, 2026-10-10)

The IR comparison flags representation differences that the backend may lower identically, and
on the non-main targets it reported thousands of them. `--asm` settles each one by where the
arguments and the return value actually go. Each generated function stores every parameter to
an extern volatile global and returns a volatile load. Each side is compiled to MIR after
instruction selection by its own backend: rustc with `-Cllvm-args=-stop-after=finalize-isel`,
clang with `-mllvm -stop-after=finalize-isel`. clang is given rustc's FPU, soft-float ABI and
relocation model. The comparison covers the incoming physical registers in argument order (one
name per register-file location), the incoming stack slots read (outside the register home
area), and the registers the return reads.

Validated on the 21 main targets first:
- finding 20 shows a placement difference;
- finding 19 and #163911 keep the same placement (they are extension contracts, which placement
cannot show, so they stay findings);
- nothing else differs;
- the PowerPC64 `inreg` float (labelled "needs a run") has the same placement in all 40 cases,
so it is equivalent.

Run with `--all`, seeds 1–3, 300 functions each, 312 targets.
- **Equivalent (same placement):** about 4,100–5,000 parameter-attribute and parameter-type
differences per seed, 560 calling-convention differences and 120–220 return differences. These
are the IR noise.
- **Harness artifacts, fixed in the harness:**
- clang's default armv7r CPU (cortex-r4) has no FPU, so clang disabled the FP registers. It is
now given `-mfpu` from rustc's features.
- rustc's soft-float AArch64 targets need clang's `-mabi=aapcs-soft`.
- clang built non-PIC code where rustc builds PIC. MIPS PIC code receives its address in `$t9`,
so clang now gets `-fPIC`/`-fno-pic` matching rustc.
- **What remains**, every placement difference classified:

| class | targets (tier) | functions, seeds 1–3 | verdict | evidence |
|---|---|---:|---|---|
| narrow integer arguments lose `signext`/`zeroext` (same placement, extension contract) | mips64, mips64el, mipsisa64r6(el) `-linux-gnuabi64`/`-muslabi64`, mips64-openwrt (3) | 749 attribute differences | **finding 40**, a regression from #163653 | rustc's caller no longer extends (`sll`/`seb` gone, nightly-2026-07-18 vs 2026-10-06) |
| small aggregate (≤ 8 bytes) returned through sret; clang returns it in r3/r4 | powerpc-unknown-{freebsd,netbsd,openbsd,helenos} (3) | 300 | **finding 42** | clang returns in registers for non-Linux ELF PowerPC32 and agrees with rustc on Linux; FreeBSD's system compiler on powerpc is clang |
| homogeneous float aggregate (incl. a union of one float type) not in VFP registers | thumbv7a-{pc,uwp}-windows-msvc (3) | 74 | **finding 41** | rustc's VFP aggregate rules depend on `cfg_abi == EabiHf`; this target has `llvm-floatabi: hard` but no `eabihf` |
| over-aligned aggregate: natural vs declared alignment | thumbv7a-{pc,uwp}-windows-msvc (3) | 38 | part of finding 41 (undecided which is MSVC's) | rustc's ARM code uses `unadjusted_abi_align`; clang uses the declared alignment on Windows, the natural one on Linux |
| `repr(C)` layout: `i64`/`f64` alignment | m68k-unknown-linux-gnu, m68k-unknown-none-elf (3) | 480 | **finding 43** | rustc 4/8, clang 8/8, GCC's documented default 2/2 |
| scalar `__int128` padded to an even slot by rustc, not by clang | the seven mips64 targets (3) | 728 | **clang differs, rustc matches GCC** | the padding is #163653, fixing #161679 to match GCC; clang 21 does not align `__int128` arguments at all |
| union holding a float/double passed in integer registers by rustc, FP registers by clang | sparc64-*, sparcv9-sun-solaris (2/3) | 185 | clang differs (GCC passes unions in integer registers) | GCC `function_arg_union_value`, from source, not run here |
| 16-byte-aligned aggregate: even-slot alignment | sparc64-*, sparcv9-sun-solaris (2/3) | 30 | clang inconsistent (sometimes no alignment, sometimes an extra slot); rustc aligns to an even slot like GCC | GCC `function_arg_slotno`, from source, not run here |
| 16-byte-aligned small aggregate: declared vs natural alignment | aarch64-unknown-none-softfloat, aarch64_be-, aarch64v8r-, aarch64-unknown-linux-pauthtest (2/3) | 26 | clang inconsistent: on aarch64-linux it uses the natural alignment like rustc; under `aapcs-soft` and `pauthtest` the declared one | the same signature on aarch64-unknown-linux-gnu agrees |
| over-aligned aggregate passed by reference (MSVC rule) | i686-unknown-uefi (2) | 293 | expected: rustc applies MSVC's x86 rules on this target (`is_like_msvc`) while its LLVM triple is `windows-gnu` | rustc's lowering equals clang `--target=i686-pc-windows-msvc` in all 300 functions of a seed |
| register-size aggregate with a non-register-size member (e.g. an 8-byte union with a `short[3]`) returned in edx:eax by rustc, sret by clang | 12 i386 targets with register struct return (Windows, Darwin, BSDs) | 12 | undecided: needs GCC or MSVC (the main-target label "i686 msvc small-struct return") | clang's rule recurses into fields; rustc uses the size |
| float or double arguments without SSE | x86_64-unknown-none (2) | 14 | no reference ABI (soft-float x86-64 has no psABI) | clang classifies float pairs as SSE and its backend then splits them over GPRs |
| packed aggregate | hexagon-* (3) | 3 | undecided | one signature shape |
| 16-byte-aligned aggregate in the parameter save area | powerpc64-ibm-aix (3) | 1 | undecided | one signature |
| BPF | bpfel, bpfeb (3) | not compared | no reference: clang's BPF backend rejects stack arguments and large returns | |
| the Rust side does not compile: ICE `unreachable!("Align is given as power of 2 no larger than 16 bytes")` in `callconv/nvptx64.rs` | nvptx64-nvidia-cuda (2) | whole target | **known, rust-lang/rust#163497** (open; its reproducer is `ptx-kernel` parameters). Here a plain `extern "C" fn g() -> A` with `#[repr(C, align(32))] struct A` ICEs on 1.90.0, 1.98.0 and nightly-2026-10-06; 1.80.0 compiled it (sret, align 32) | |

Also: finding 20's float-and-pointer struct appears on the 32-bit RISC-V and LoongArch targets
(50 functions), and finding 19's missing extension on stack arguments on loongarch32. Both
labels now include those targets.

### 15. Determinism (15)

Mostly parallel-frontend reproducibility (#163878, #162202, #162203; mirth found #162202), plus
Expand Down Expand Up @@ -355,7 +410,7 @@ over the standalone UI tests at the pin (and real crates for release-to-release)
| solver differential | `mirth-lab solver-diff` | 17,634 tests × old/new solver × NLL/Polonius | the 26 rejections and 3 crashes of [`solver.md`](solver.md); Polonius agrees with NLL everywhere |
| Miri differential | `mirth-lab miri-diff` | 3,094 runnable tests at MIR opt levels 0, 2, 4 and natively | nothing; tests asserting unspecified behavior (function pointer equality, ZST addresses) listed |
| equivalent rewrites | `mirth-rewrite` + `mirth-lab rewrite-diff` | 18,624 tests × generic-wrap, alias, reorder, unused | findings 25 (generic-wrap) and 28 (reorder) |
| ABI vs clang | `mirth-lab abi-diff` | 21 main targets × 10 seeds × 300 random signatures | findings 19 and 20; #163911 reproduced; i686 MSVC small-struct returns and a PowerPC64 `inreg` float undecided |
| ABI vs clang | `mirth-lab abi-diff` | 21 main targets × 10 seeds × 300 random signatures; `--asm --all`: 312 targets × 3 seeds | findings 19 and 20; #163911 reproduced; i686 MSVC small-struct returns undecided; the PowerPC64 `inreg` float equivalent (same placement); findings 40–43 on non-main targets (assembly-level triage under check 14) |
| internal checks on | `mirth-lab crash-diff` + a debug-assertions compiler | 18,624 tests with `-Zvalidate-mir` | findings 21–24 (17 tests) |
| release-to-release | `mirth-lab release-diff` | 87 real repositories, nightly-2026-07-18 → 10-06 | findings 26 and 27; `allocative` (unstable features) noted |

Expand Down
4 changes: 4 additions & 0 deletions docs/hunt.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks.
| 30 | compiler-internal debug output in user-facing diagnostics: under the default (new) solver, E0308 help suggests `as fn(?0t) -> ?0t`; an E0391 cycle note prints `Binder { value: ConstEvaluatable(AliasConst(… DefId(0:7 ~ …` (blessed in `offset-of/inside-array-length.stderr`) | low, diagnostics; found by the diagnostic-invariants check over 18,374 UI tests (excluding tests that ask for verbose output); the first not in CI because of the solver pin ([`solver-triage.md`](solver-triage.md) item I) |
| 31 | `#[rustc_main]` on a struct, impl, trait or module, on stable: after the expected E0658 and "cannot be used on structs", rustc ICEs ("unexpected sort of node in fn_sig()", `collect.rs`): the item is still taken as the entry point | **looks new**, low (error recovery, internal attribute); regression between 1.91.0 and 1.93.0; found by the feature-gate check; [repro](hunt/tests/rustc-main-on-struct.rs) |
| 32 | new-solver compile-time regression: a chain of N `.map()` calls type-checks in 4.5 s / 520 MB at N=200 on nightly-2026-08-03 and 37–58 s / 2.0–2.9 GB from nightly-2026-08-04, with a new "overflow evaluating the requirement `Map<…<Map<_, …>>: Iterator`" future-compat warning; nightly's default solver is the new one, so default builds regressed from 2.2 s (old solver, July) to 53 s | **looks new**, medium (compile time, realistic code shape); bisected over nightlies to #160254 (the only solver PR in the range); found by the scaling check; [facts](hunt/iter-chain-solver-regression.md) |
| 40 | mips64 (n64): narrow integer `extern "C"` arguments lose `signext`/`zeroext`, in registers too; a Rust caller passes `x as i32` / `x as i8` without `sll`/`seb`, where the convention and clang/GCC callees expect sign extension | **looks new**, high for the targets (silent wrong values in C callees), tier 3; regression from #163653 (merged 2026-10-04): present in nightly-2026-10-06, absent in nightly-2026-07-18 and 1.98.0; found by `abi-diff --asm`; [facts](hunt/mips64-narrow-int-extension.md) |
| 41 | thumbv7a-{pc,uwp}-windows-msvc: homogeneous float aggregates (e.g. `struct { float a, b; }`) are passed in core registers and returned through memory, where clang uses s0/s1 (AAPCS VFP rules); rustc applies the VFP aggregate rules only to `eabihf` targets | **looks new**, tier 3; since at least 1.80.0; found by `abi-diff --asm`; [facts](hunt/windows-arm32-vfp-aggregates.md) |
| 42 | powerpc-unknown-{freebsd,netbsd,openbsd,helenos}: aggregates of up to 8 bytes are returned through memory, where clang returns them in r3/r4 (on Linux both use memory) | **looks new**, tier 3; since at least 1.80.0; FreeBSD's powerpc system compiler is clang; found by `abi-diff --asm`; [facts](hunt/powerpc-bsd-struct-return.md) |
| 43 | m68k: `repr(C)` alignment of `i64` is 4 and of `f64` 8, where GCC's documented m68k default is 2 (clang uses 8 for both), so structs containing them are laid out differently | tier 3; related to open #117252 (pointer alignment); found by `abi-diff --asm`; [facts](hunt/m68k-repr-c-alignment.md) |

Findings 1 and 2 are single-threaded: an ordinary `cargo build`, an edit, another
`cargo build`, and the metadata differs from a clean build of the edited source. Both come
Expand Down
32 changes: 32 additions & 0 deletions docs/hunt/m68k-repr-c-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# m68k: `repr(C)` structs with `i64` or `f64` are laid out unlike GCC's

Facts for finding 43. Found by the ABI differential's assembly-level mode ([`checks.md`](../checks.md),
check 14): every placement difference on m68k (480 functions over 3 seeds) involves a struct
containing `long long` or `double`.

## What happens

| alignment | `int`/`i32` | `long long`/`i64` | `double`/`f64` | `struct { _Bool; long long; unsigned long long; }` size |
|---|---:|---:|---:|---:|
| rustc (pinned nightly), `m68k-unknown-linux-gnu` | 2 | 4 | 8 | 20 |
| clang 21, `--target=m68k-unknown-linux-gnu` | 2 | 8 | 8 | 24 |
| GCC's documented default (`-mno-align-int`) | 2 | 2 | 2 | 18 |

rustc's values come from its data layout `E-m:e-p:32:16:32-i8:8:8-i16:16:16-i32:16:32-n8:16:32-a:0:16-S16`,
which does not mention `i64` or `f64`, so LLVM's defaults apply (`i64:32:64`, `f64:64:64`).
clang's data layout string is the same; its `long long` and `double` alignments come from clang's
target description.

GCC's m68k options documentation: `-malign-int` aligns int, long, long long, float, double and long
double on a 32-bit boundary and `-mno-align-int` on a 16-bit boundary, and with `-malign-int` "GCC
aligns structures containing the above types differently than most published application binary
interface specifications for the m68k". The table's GCC row is from that documentation; no m68k
GCC here.

## Scope

Tier 3 targets m68k-unknown-linux-gnu and m68k-unknown-none-elf: any `repr(C)` type shared with C
that contains an `i64`, `u64` or `f64` (layout and size differ, so passing it by value or through
a pointer disagrees). Related: rust-lang/rust#117252 (open, "Wrong alignment for m68k pointer
types"), which is about pointers and `usize` (rustc 2, the m680x0 ABI document 4); its thread
mentions plans to change the default alignment on Debian and Gentoo m68k.
64 changes: 64 additions & 0 deletions docs/hunt/mips64-narrow-int-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# mips64: narrow integer arguments are no longer sign- or zero-extended

Facts for finding 40. Found by the ABI differential's assembly-level mode ([`checks.md`](../checks.md),
check 14: `mirth-lab abi-diff --asm --all`).

## What happens

On the n64 targets (mips64, mips64el, mipsisa64r6, mipsisa64r6el `-unknown-linux-gnuabi64`/`-muslabi64`,
mips64-openwrt-linux-musl), rustc declares `extern "C"` integer parameters narrower than 64 bits
without `signext`/`zeroext`, in registers and on the stack alike, and a Rust caller passes them with
whatever is in the upper bits. The n64 convention extends them: 32-bit integers are sign-extended
to 64 bits whatever their signedness, narrower ones by their sign. clang declares
`signext`/`zeroext` on every such parameter, and a C callee compiled by clang or GCC may rely on
it.

## Reproduction

```rust
// any no_core crate for --target mips64-unknown-linux-gnuabi64
extern "C" { fn callee32(x: i32); fn callee8(x: i8); }
#[no_mangle] pub unsafe extern "C" fn caller32(x: i64) { callee32(x as i32) }
#[no_mangle] pub unsafe extern "C" fn caller8(x: i64) { callee8(x as i8) }
```

`rustc -Copt-level=2 -Crelocation-model=static --emit=asm`:

| | nightly-2026-07-18 | nightly-2026-10-06 |
|---|---|---|
| `declare void @callee32` | `(i32 signext)` | `(i32)` |
| `declare void @callee8` | `(i8 signext)` | `(i8)` |
| `caller32`, delay slot of `jal callee32` | `sll $4, $4, 0` | `nop` |
| `caller8`, delay slot of `jal callee8` | `seb $4, $4` | `nop` |

Rust callees are unaffected: without the attribute they re-extend (`take32: sll $2, $4, 0`).
Returns keep their extension (the return path was not changed).

## Where

PR #163653 ("callconv: mips64: Match GCC for alignment of 16-byte scalars", merged 2026-10-04,
fixing #161679) added to `classify_arg` in `compiler/rustc_target/src/callconv/mips64.rs`, after
`extend_integer_width_mips(arg, 64)`:

```rust
if let BackendRepr::Scalar(scalar) = arg.layout.backend_repr {
...
arg.cast_to_and_pad_i32(CastTarget::from(Reg { kind, size }), pad_i32);
}
```

Every scalar now becomes `PassMode::Cast`, also when `pad_i32` is 0, and the cast carries its own
(empty) attributes, so the extension set just before is dropped.

## Versions

`signext` present with 1.80.0, 1.90.0, 1.98.0, nightly-2025-04-11, nightly-2025-12-20 and
nightly-2026-07-18; absent with nightly-2026-10-06 (the first nightly after the merge was not
installed here). The commit range between nightly-2026-07-18 and nightly-2026-10-06 has one change
to `mips64.rs` that alters scalar lowering, 22067c76adf (#163653). Not confirmed by reverting it.

## Scope

Rust calling C (or any non-Rust callee) on the tier 3 n64 targets, with an `i32`, `u32`, `i16`,
`u16`, `i8`, `u8` or `bool` argument. Searching rust-lang/rust for "mips64 signext" and the PR
number found nothing.
47 changes: 47 additions & 0 deletions docs/hunt/powerpc-bsd-struct-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 32-bit PowerPC BSDs: small structs are returned through memory, clang returns them in registers

Facts for finding 42. Found by the ABI differential's assembly-level mode ([`checks.md`](../checks.md),
check 14).

## What happens

On powerpc-unknown-freebsd, -netbsd, -openbsd and -helenos, clang returns an aggregate of up to 8
bytes in r3 (and r4). rustc returns every aggregate through a hidden pointer, as on Linux.

```c
struct Small { short a; };
struct Small make_small(short a);
```

| | registers in | return |
|---|---|---|
| clang `--target=powerpc-unknown-freebsd13.0` | r3 (`a`) | r3 |
| clang `--target=powerpc-unknown-linux-gnu` | r3 (sret), r4 (`a`) | memory |
| rustc `--target powerpc-unknown-freebsd` (1.80.0 … nightly-2026-10-06) | r3 (sret), r4 (`a`) | memory |

In the random signatures (3 seeds × 300) all 300 placement differences on these four targets are
this case, and clang and rustc agree on powerpc-unknown-linux-gnu.

## Where

`compiler/rustc_target/src/callconv/powerpc.rs`:

```rust
fn classify_ret<Ty>(ret: &mut ArgAbi<'_, Ty>) {
if ret.layout.is_aggregate() {
ret.make_indirect();
```

with no per-OS distinction. clang returns small aggregates in registers for 32-bit PowerPC ELF
targets other than Linux (its default when neither `-msvr4-struct-return` nor
`-maix-struct-return` is given); observed above, the source was not read here.

## Expected

The C compiler of the platform decides. FreeBSD's system compiler on powerpc is clang (since
FreeBSD 13). GCC's defaults on NetBSD and OpenBSD were not checked; no PowerPC GCC here.

## Scope

Tier 3 targets; Rust calling C or C calling Rust with an aggregate return of at most 8 bytes. No
rust-lang/rust issue found ("powerpc freebsd struct return").
Loading
Loading