From 06ea028cb40d9b1133cfe7faa1f4b78987ca9df9 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sat, 10 Oct 2026 10:37:40 +0000 Subject: [PATCH 1/2] abi-diff --asm: placement after instruction selection decides IR differences; triage of every target; findings 40-43 Each generated function stores its parameters to extern volatile globals and returns a volatile load; both sides are compiled to MIR after instruction selection by their own backend (rustc -Cllvm-args=-stop-after=finalize-isel, clang -mllvm -stop-after=finalize-isel, clang given rustc's FPU, soft-float ABI and relocation model) and the incoming registers, stack slots and return registers are compared. Validated on the main targets (finding 20 differs, findings 19 and #163911 keep their placement, nothing else differs; the ppc64 inreg float is equivalent). --all, seeds 1-3: thousands of IR differences are equivalent; what remains is classified in docs/checks.md. Findings: 40 mips64 narrow integer arguments lose signext/zeroext (regression from #163653), 41 thumbv7a Windows homogeneous float aggregates not in VFP registers, 42 32-bit PowerPC BSDs return small structs through memory, 43 m68k repr(C) alignment of i64/f64. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXiEXbESemwqMLYKaWLDbT --- crates/mirth-lab/src/tools/abi_diff.rs | 419 ++++++++++++++++++++-- docs/checks.md | 56 ++- docs/hunt.md | 4 + docs/hunt/m68k-repr-c-alignment.md | 32 ++ docs/hunt/mips64-narrow-int-extension.md | 64 ++++ docs/hunt/powerpc-bsd-struct-return.md | 47 +++ docs/hunt/windows-arm32-vfp-aggregates.md | 53 +++ 7 files changed, 646 insertions(+), 29 deletions(-) create mode 100644 docs/hunt/m68k-repr-c-alignment.md create mode 100644 docs/hunt/mips64-narrow-int-extension.md create mode 100644 docs/hunt/powerpc-bsd-struct-return.md create mode 100644 docs/hunt/windows-arm32-vfp-aggregates.md diff --git a/crates/mirth-lab/src/tools/abi_diff.rs b/crates/mirth-lab/src/tools/abi_diff.rs index 1100ce8..c3c8f0a 100644 --- a/crates/mirth-lab/src/tools/abi_diff.rs +++ b/crates/mirth-lab/src/tools/abi_diff.rs @@ -8,8 +8,18 @@ //! compares the two LLVM IR signatures parameter by parameter after first-class aggregates are //! flattened. A difference in register class, extension, inreg/byval/sret, byval alignment, //! parameter count or calling convention is a finding; representation-only differences are -//! notes. Verified equivalences per architecture, known bugs (#163911, findings 19 and 20) and -//! two differences this host cannot decide are labelled. +//! notes. Verified equivalences per architecture, known bugs (#163911, findings 19, 20 and 40) +//! and two differences this host cannot decide are labelled. +//! +//! `--asm` also compares where each argument and the return value end up. Each function then +//! stores every parameter to an extern volatile global and returns a volatile load, both sides +//! are compiled to MIR after instruction selection by their own backend setup (`rustc +//! -Cllvm-args=-stop-after=finalize-isel`, `clang -mllvm -stop-after=finalize-isel`, with clang +//! given rustc's FPU, soft-float ABI and relocation model), and the incoming physical registers +//! in argument order, the incoming stack slots read and the return registers are compared. An +//! IR difference with the same placement is equivalent; an extension attribute on an integer +//! narrower than a register still counts, since placement cannot show it. docs/checks.md has +//! the per-target triage of what remains. use std::collections::BTreeMap; use std::path::PathBuf; @@ -43,6 +53,10 @@ pub struct Args { jobs: usize, #[arg(long, default_value = "clang")] clang: String, + /// Also compare where each argument and the return value end up after instruction selection + /// (each side's own backend): an IR difference with the same placement is equivalent. + #[arg(long)] + asm: bool, } const MAIN: &[&str] = &[ @@ -147,7 +161,7 @@ impl Gen { } } -fn program(wide: bool, seed: u64, count: usize) -> (String, String, Vec) { +fn program(wide: bool, seed: u64, count: usize, asm: bool) -> (String, String, Vec) { let mut g = Gen { rng: Rng(seed), wide, aggregates: Vec::new(), names: 0 }; let mut fns = Vec::new(); for k in 0..count { @@ -183,13 +197,41 @@ fn program(wide: bool, seed: u64, count: usize) -> (String, String, Vec) // Union fields must be Copy; minicore has no derive. rs.push(format!("impl Copy for {} {{}}", a.name)); } + if asm { + // Each parameter is stored to an extern volatile global and the return value loaded from + // one, so the lowering of every argument and of the return value is observable. + rs[0] = "#![feature(no_core, intrinsics, rustc_attrs)]".into(); + rs[3] = "#![allow(improper_ctypes_definitions, improper_ctypes, internal_features, unused, non_snake_case, non_upper_case_globals)]".into(); + rs.push("#[rustc_intrinsic] unsafe fn volatile_store(dst: *mut T, val: T);".into()); + rs.push("#[rustc_intrinsic] unsafe fn volatile_load(src: *const T) -> T;".into()); + } let mut names = Vec::new(); for (name, params, ret) in &fns { let cp = if params.is_empty() { "void".into() } else { params.iter().enumerate().map(|(i, t)| format!("{} a{i}", t.0)).collect::>().join(", ") }; - c.push(format!("{} {name}({cp}) {{ for (;;); }}", ret.as_ref().map_or("void", |r| r.0.as_str()))); let rp = params.iter().enumerate().map(|(i, t)| format!("a{i}: {}", t.1)).collect::>().join(", "); let rr = ret.as_ref().map(|r| format!(" -> {}", r.1)).unwrap_or_default(); - rs.push(format!("#[no_mangle] pub extern \"C\" fn {name}({rp}){rr} {{ loop {{}} }}")); + let cret = ret.as_ref().map_or("void", |r| r.0.as_str()); + if asm { + let mut decls: Vec = params.iter().enumerate().map(|(i, t)| format!("{} volatile g_{name}_{i};", t.0)).collect(); + let mut rdecls: Vec = params.iter().enumerate().map(|(i, t)| format!("static mut g_{name}_{i}: {};", t.1)).collect(); + if let Some(r) = ret { + decls.push(format!("{} volatile r_{name};", r.0)); + rdecls.push(format!("static mut r_{name}: {};", r.1)); + } + for d in decls { + c.push(format!("extern {d}")); + } + let stores: String = (0..params.len()).map(|i| format!("g_{name}_{i} = a{i}; ")).collect(); + let cbody = if ret.is_some() { format!("{stores}return r_{name};") } else { stores }; + c.push(format!("{cret} {name}({cp}) {{ {cbody} }}")); + rs.push(format!("unsafe extern \"C\" {{ {} }}", rdecls.join(" "))); + let stores: String = (0..params.len()).map(|i| format!("volatile_store(&raw mut g_{name}_{i}, a{i}); ")).collect(); + let load = if ret.is_some() { format!("volatile_load(&raw const r_{name})") } else { String::new() }; + rs.push(format!("#[no_mangle] pub extern \"C\" fn {name}({rp}){rr} {{ unsafe {{ {stores}{load} }} }}")); + } else { + c.push(format!("{cret} {name}({cp}) {{ for (;;); }}")); + rs.push(format!("#[no_mangle] pub extern \"C\" fn {name}({rp}){rr} {{ loop {{}} }}")); + } names.push(name.clone()); } (c.join("\n") + "\n", rs.join("\n") + "\n", names) @@ -462,11 +504,19 @@ fn label(arch: &str, target: &str, d: &Diff) -> Option<&'static str> { match d { Diff::ReturnAttributes { clang, .. } if arch == "x86_64" && clang == &["zeroext"] => Some("rust-lang/rust#163911"), Diff::ParameterAttributes { clang, rustc, .. } - if matches!(arch, "riscv64" | "riscv32" | "loongarch64") && rustc.is_empty() && (clang == &["signext"] || clang == &["zeroext"]) => + if matches!(arch, "riscv64" | "riscv32" | "loongarch64" | "loongarch32") && rustc.is_empty() && (clang == &["signext"] || clang == &["zeroext"]) => { Some("finding 19 (docs/hunt.md)") } - Diff::Parameters { clang, rustc } | Diff::Return { clang, rustc } if matches!(arch, "riscv64" | "riscv32" | "loongarch64") && fp(rustc) > fp(clang) => { + // Every narrow integer argument, in registers too (since #163653). + Diff::ParameterAttributes { clang, rustc, .. } + if matches!(arch, "mips64" | "mips64r6") && rustc.is_empty() && (clang == &["signext"] || clang == &["zeroext"]) => + { + Some("finding 40 (docs/hunt.md)") + } + Diff::Parameters { clang, rustc } | Diff::Return { clang, rustc } + if matches!(arch, "riscv64" | "riscv32" | "loongarch64" | "loongarch32") && fp(rustc) > fp(clang) => + { Some("finding 20 (docs/hunt.md)") } // clang returns an 8-byte struct with a 3-byte array field indirectly; rustc and MSVC's @@ -485,6 +535,198 @@ fn label(arch: &str, target: &str, d: &Diff) -> Option<&'static str> { } } +// ---- placement after instruction selection ---- + +/// Where a function's arguments and return value are after instruction selection: the physical +/// registers it receives (in argument order), the incoming stack slots it reads, and the +/// registers its return reads (WebAssembly: the parameter and result types). +#[derive(Debug, Clone, Default, PartialEq, Serialize)] +struct Placement { + ins: Vec, + stack: Vec, + ret: Vec, +} + +static MIR_LIVEIN: LazyLock = LazyLock::new(|| Regex::new(r"^\s+- \{ reg: '\$(\w+)'").unwrap()); +static MIR_FIXED: LazyLock = + LazyLock::new(|| Regex::new(r"- \{ id: \d+, type: [\w-]+, offset: (-?\d+), size: (\d+)").unwrap()); +static MIR_PHYS: LazyLock = LazyLock::new(|| Regex::new(r"\$(\w+)").unwrap()); +static MIR_WASM: LazyLock = LazyLock::new(|| Regex::new(r"^\s+(params|results):\s*\[(.*)\]").unwrap()); +static REG_NUM: LazyLock = LazyLock::new(|| Regex::new(r"^([a-z]+)(\d+)(.*)$").unwrap()); + +/// One name per register-file location: the 32-bit, 16-bit and 8-bit names of a register, and a +/// floating-point register's single and double names, are the same place. +fn normalize(arch: &str, reg: &str) -> String { + let r = reg.to_lowercase(); + let x86 = |r: &str| -> String { + let base = match r { + "eax" | "ax" | "al" | "ah" => "rax", + "ebx" | "bx" | "bl" | "bh" => "rbx", + "ecx" | "cx" | "cl" | "ch" => "rcx", + "edx" | "dx" | "dl" | "dh" => "rdx", + "esi" | "si" | "sil" => "rsi", + "edi" | "di" | "dil" => "rdi", + "ebp" | "bp" | "bpl" => "rbp", + _ => "", + }; + if !base.is_empty() { + return base.into(); + } + match REG_NUM.captures(r) { + Some(c) if &c[1] == "r" && matches!(&c[3], "d" | "w" | "b") => format!("r{}", &c[2]), + _ => r.into(), + } + }; + let Some(c) = REG_NUM.captures(&r) else { return if arch.starts_with("x86") { x86(&r) } else { r } }; + let (class, n, rest) = (&c[1], &c[2], &c[3]); + match arch { + "x86_64" | "x86" => x86(&r), + "aarch64" | "arm64ec" => match class { + "w" => format!("x{n}"), + "b" | "h" | "s" | "d" | "q" => format!("v{n}"), + _ => r, + }, + "riscv32" | "riscv64" => format!("{class}{n}"), + "loongarch32" | "loongarch64" => format!("{class}{n}"), + "mips" | "mips32r6" | "mips64" | "mips64r6" => { + let n: u32 = n.parse().unwrap_or(0); + match (class, rest) { + ("d", "_64") => format!("f{n}"), + // O32 (32-bit FPU registers): $dN is the pair $f2N:$f2N+1. + ("d", "") => format!("f{}", 2 * n), + _ => format!("{class}{n}"), + } + } + "powerpc" | "powerpc64" => match class { + "x" => format!("r{n}"), + _ => r, + }, + "s390x" => match class { + "r" | "f" => format!("{class}{n}"), + _ => r, + }, + "bpf" => match class { + "w" => format!("r{n}"), + _ => r, + }, + _ => r, + } +} + +/// Bytes at the bottom of the incoming argument area that mirror argument registers (PowerPC64 +/// ELF's parameter save area, MIPS O32's home slots): a byval aggregate passed partly in +/// registers covers them, an aggregate passed as an array does not, and both receive the +/// registers' part in registers. +fn home_area(arch: &str, llvm_target: &str) -> u64 { + match arch { + "powerpc64" if llvm_target.starts_with("powerpc64le") => 96, + "powerpc64" => 112, + "mips" | "mips32r6" => 16, + _ => 0, + } +} + +fn placements(mir: &str, arch: &str, slot: u64, home: u64) -> BTreeMap { + let mut out = BTreeMap::new(); + for doc in mir.split("\n---").skip(1) { + let Some(name) = doc.lines().find_map(|l| l.strip_prefix("name:")).map(|n| n.trim().to_owned()) else { continue }; + let mut p = Placement::default(); + let mut section = ""; + let mut slots = std::collections::BTreeSet::new(); + for line in doc.lines() { + if !line.starts_with(' ') && line.contains(':') { + section = line.split(':').next().unwrap_or(""); + } + match section { + "liveins" => { + if let Some(c) = MIR_LIVEIN.captures(line) { + p.ins.push(normalize(arch, &c[1])); + } + } + "fixedStack" => { + if let Some(c) = MIR_FIXED.captures(line) { + // Clipped to the incoming argument area: ARM keeps the register part of a + // split byval aggregate below it (negative offsets). + let (off, size): (i64, i64) = (c[1].parse().unwrap_or(0), c[2].parse().unwrap_or(0)); + let (start, end) = (off.max(home as i64), off + size); + if end > start { + slots.extend(start as u64 / slot..=(end as u64 - 1) / slot); + } + } + } + "machineFunctionInfo" => { + if let Some(c) = MIR_WASM.captures(line) { + let v: Vec = c[2].split(',').map(|t| t.trim().to_owned()).filter(|t| !t.is_empty()).collect(); + if &c[1] == "params" { p.ins = v } else { p.ret = v } + } + } + "body" => { + let insn = line.trim(); + let op = insn.split_whitespace().next().unwrap_or(""); + let up = op.to_uppercase(); + let is_ret = !insn.contains(" = ") + && (up.contains("RET") || up.starts_with("BLR") || up.starts_with("RTS") || up.starts_with("RETURN")) + && !arch.starts_with("wasm"); + if is_ret && p.ret.is_empty() { + // Return registers are explicit operands on some targets (x86 `RET 0, $al`), + // implicit uses on others (`PseudoRET implicit $x10`). + p.ret = MIR_PHYS.captures_iter(insn).map(|c| c[1].to_owned()).filter(|r| r != "noreg").map(|r| normalize(arch, &r)).collect(); + } + } + _ => {} + } + } + p.stack = slots.into_iter().collect(); + out.insert(name, p); + } + out +} + +/// The integer width of an IR type (`i8` → 8), for deciding whether an extension can matter. +fn int_width(ty: &str) -> Option { + ty.strip_prefix('i').and_then(|n| n.parse().ok()) +} + +/// After placement agrees, whether an IR difference can still matter: a zero or sign extension +/// of an integer narrower than a register is a contract between caller and callee that +/// placement does not show. +fn extension_matters(d: &Diff, width: usize) -> bool { + let ext = |a: &[String], b: &[String]| { + let e = |v: &[String]| v.iter().filter(|x| *x == "zeroext" || *x == "signext").cloned().collect::>(); + e(a) != e(b) + }; + match d { + Diff::ParameterAttributes { clang, rustc, ty, .. } => ext(clang, rustc) && int_width(ty).is_some_and(|w| w < width), + Diff::ReturnAttributes { clang, rustc, ret } => { + ext(clang, rustc) && ret.first().and_then(|t| int_width(t)).is_some_and(|w| w < width) + } + _ => false, + } +} + +#[derive(Debug, Clone, Serialize)] +struct PlacementDiff { + clang: Placement, + rustc: Placement, +} + +/// The `-mfpu` that gives clang the FPU rustc's ARM features name, strongest first. +fn arm_fpu(features: &str) -> Option<&'static str> { + let has = |f: &str| features.split(',').any(|x| x == f); + [ + ("+fp-armv8", "fp-armv8"), + ("+neon", "neon"), + ("+vfp4", "vfpv4"), + ("+vfp4d16", "vfpv4-d16"), + ("+vfp3", "vfpv3"), + ("+vfp3d16", "vfpv3-d16"), + ("+vfp2", "vfpv2"), + ] + .into_iter() + .find(|(f, _)| has(f)) + .map(|(_, fpu)| fpu) +} + // ---- running ---- #[derive(Deserialize, Default)] @@ -506,6 +748,8 @@ struct Spec { llvm_abiname: String, #[serde(default)] llvm_floatabi: String, + #[serde(default)] + rustc_abi: String, } fn num(v: &serde_json::Value, default: usize) -> usize { @@ -520,6 +764,14 @@ struct TargetResult { findings: BTreeMap>, labelled: BTreeMap, notes: usize, + /// --asm: functions whose arguments or return value end up in different places. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + placement: BTreeMap, + /// --asm: IR differences whose placement is the same, by kind. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + equivalent: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + asm_skip: Option, } fn run_cmd(cmd: &mut Command) -> Result<(), String> { @@ -541,7 +793,7 @@ fn one_target(args: &Args, target: &str) -> TargetResult { } let d = args.work.join(target); let _ = std::fs::create_dir_all(&d); - let (csrc, rsrc, fns) = program(width == 64 && spec.arch != "sparc64", args.seed, args.count); + let (csrc, rsrc, fns) = program(width == 64 && spec.arch != "sparc64", args.seed, args.count, args.asm); let _ = std::fs::write(d.join("a.c"), &csrc); let _ = std::fs::write(d.join("a.rs"), &rsrc); let base = |c: &mut Command| { @@ -555,40 +807,98 @@ fn one_target(args: &Args, target: &str) -> TargetResult { } let mut rc = Command::new(&args.rustc); base(&mut rc); - rc.args(["--emit=llvm-ir", "-Copt-level=0", "--extern"]).arg(format!("minicore={}", d.join("libminicore.rlib").display())).arg("-o").arg(d.join("r.ll")).arg(d.join("a.rs")); + rc.args(["-Copt-level=0", "--extern"]).arg(format!("minicore={}", d.join("libminicore.rlib").display())); + if args.asm { + // The asm output after instruction selection is MIR, from rustc's own backend setup. + rc.arg(format!("--emit=llvm-ir={},asm={}", d.join("r.ll").display(), d.join("r.mir").display())); + rc.arg("-Cllvm-args=-stop-after=finalize-isel"); + } else { + rc.arg("--emit=llvm-ir").arg("-o").arg(d.join("r.ll")); + } + rc.arg(d.join("a.rs")); if let Err(e) = run_cmd(&mut rc) { return skip(format!("rust side does not build: {e}")); } - let mut cc = Command::new(&args.clang); - cc.arg(format!("--target={}", spec.llvm_target)).args(["-ffreestanding", "-S", "-emit-llvm", "-O0", "-Wno-everything"]); - // The target's CPU and features decide parts of the ABI in clang too (soft-float, SSE). - if !spec.cpu.is_empty() && spec.cpu != "generic" { - cc.args(["-Xclang", "-target-cpu", "-Xclang", &spec.cpu]); - } - for f in spec.features.split(',').filter(|f| !f.is_empty()) { - cc.args(["-Xclang", "-target-feature", "-Xclang", f]); - } - if !spec.llvm_abiname.is_empty() { - cc.arg(format!("-mabi={}", spec.llvm_abiname)); - } - if spec.llvm_floatabi == "hard" { - cc.arg("-mfloat-abi=hard"); - } - cc.arg("-o").arg(d.join("c.ll")).arg(d.join("a.c")); - if let Err(e) = run_cmd(&mut cc) { + let pic = std::fs::read_to_string(d.join("r.ll")).unwrap_or_default().contains("\"PIC Level\""); + let clang = |emit: &[&str], out: &str| { + let mut cc = Command::new(&args.clang); + cc.arg(format!("--target={}", spec.llvm_target)).args(["-ffreestanding", "-S", "-O0", "-Wno-everything"]).args(emit); + // The target's CPU and features decide parts of the ABI in clang too (soft-float, SSE). + if !spec.cpu.is_empty() && spec.cpu != "generic" { + cc.args(["-Xclang", "-target-cpu", "-Xclang", &spec.cpu]); + } + for f in spec.features.split(',').filter(|f| !f.is_empty()) { + cc.args(["-Xclang", "-target-feature", "-Xclang", f]); + } + if !spec.llvm_abiname.is_empty() { + cc.arg(format!("-mabi={}", spec.llvm_abiname)); + } + if spec.llvm_floatabi == "hard" { + cc.arg("-mfloat-abi=hard"); + } + // clang's default CPU for a triple can lack the FPU rustc's features name (armv7r: + // cortex-r4), and clang then disables the FP registers whatever -target-feature says. + if spec.arch == "arm" + && let Some(fpu) = arm_fpu(&spec.features) + { + cc.arg(format!("-mfpu={fpu}")); + } + // rustc's soft-float AArch64 targets pass floats in integer registers: clang's aapcs-soft + // (which requires the FPU off; the FPU changes code, not the calling convention). + if spec.arch == "aarch64" && spec.rustc_abi == "softfloat" { + cc.args(["-mabi=aapcs-soft", "-Xclang", "-target-feature", "-Xclang", "-fp-armv8"]); + } + // The same relocation model as rustc (MIPS PIC code receives its address in $t9); clang + // refuses the option for COFF targets, where it does not apply. + if !spec.llvm_target.contains("windows") && !spec.llvm_target.contains("uefi") { + cc.arg(if pic { "-fPIC" } else { "-fno-pic" }); + } + cc.arg("-o").arg(d.join(out)).arg(d.join("a.c")); + run_cmd(&mut cc) + }; + if let Err(e) = clang(&["-emit-llvm"], "c.ll") { return skip(format!("clang does not build: {e}")); } + let mut asm_skip = None; + let (mut cplace, mut rplace) = (BTreeMap::new(), BTreeMap::new()); + if args.asm { + match clang(&["-mllvm", "-stop-after=finalize-isel"], "c.mir") { + Ok(()) => { + let slot = (width / 8) as u64; + let home = home_area(&spec.arch, &spec.llvm_target); + cplace = placements(&std::fs::read_to_string(d.join("c.mir")).unwrap_or_default(), &spec.arch, slot, home); + rplace = placements(&std::fs::read_to_string(d.join("r.mir")).unwrap_or_default(), &spec.arch, slot, home); + } + Err(e) => asm_skip = Some(format!("clang backend: {}", e.lines().find(|l| l.contains("error")).unwrap_or(&e))), + } + } let cs = signatures(&std::fs::read_to_string(d.join("c.ll")).unwrap_or_default()); let rsig = signatures(&std::fs::read_to_string(d.join("r.ll")).unwrap_or_default()); - let mut res = TargetResult { compared: fns.len(), ..Default::default() }; + let mut res = TargetResult { compared: fns.len(), asm_skip, ..Default::default() }; for name in &fns { let (Some(c), Some(r)) = (cs.get(name), rsig.get(name)) else { continue }; let (f, n) = compare(c, r, &spec.arch, width / 8); res.notes += n.len(); + // With placements: None when they could not be compared, else whether they agree. + let same = match (cplace.get(name), rplace.get(name)) { + (Some(a), Some(b)) => Some(a == b), + _ => None, + }; + if same == Some(false) { + res.placement.insert(name.clone(), PlacementDiff { clang: cplace[name].clone(), rustc: rplace[name].clone() }); + } let mut unlabelled = Vec::new(); for diff in f { + let verdict = match same { + Some(true) => " [same placement]", + Some(false) => " [placement differs]", + None => "", + }; match label(&spec.arch, target, &diff) { - Some(l) => *res.labelled.entry(l.to_owned()).or_default() += 1, + Some(l) => *res.labelled.entry(format!("{l}{verdict}")).or_default() += 1, + None if same == Some(true) && !extension_matters(&diff, width) => { + *res.equivalent.entry(diff.label().to_owned()).or_default() += 1; + } None => unlabelled.push(diff), } } @@ -631,6 +941,27 @@ pub fn run(args: Args) -> anyhow::Result { } } println!("{compared} targets compared; skipped: {skipped:?}"); + if args.asm { + let mut per: Vec<(&String, usize)> = results.iter().filter(|(_, r)| !r.placement.is_empty()).map(|(t, r)| (t, r.placement.len())).collect(); + per.sort_by(|a, b| b.1.cmp(&a.1)); + println!( + "placement differs: {} functions in {} targets: {}", + per.iter().map(|x| x.1).sum::(), + per.len(), + per.iter().take(12).map(|(t, n)| format!("{t} {n}")).collect::>().join(", ") + ); + let mut eq: BTreeMap<&str, usize> = BTreeMap::new(); + for r in results.values() { + for (k, n) in &r.equivalent { + *eq.entry(k.as_str()).or_default() += n; + } + } + println!("IR differences with the same placement (equivalent): {eq:?}"); + let asm_skipped: Vec<&String> = results.iter().filter(|(_, r)| r.asm_skip.is_some()).map(|(t, _)| t).collect(); + if !asm_skipped.is_empty() { + println!("placement not compared ({}): {}", asm_skipped.len(), asm_skipped.iter().map(|s| s.as_str()).collect::>().join(", ")); + } + } if !labelled.is_empty() { println!("known: {labelled:?}"); } @@ -642,3 +973,35 @@ pub fn run(args: Args) -> anyhow::Result { } Ok(ExitCode::SUCCESS) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn register_names_are_normalized() { + assert_eq!(normalize("aarch64", "w3"), "x3"); + assert_eq!(normalize("aarch64", "s0"), "v0"); + assert_eq!(normalize("x86_64", "edi"), "rdi"); + assert_eq!(normalize("x86_64", "r8d"), "r8"); + assert_eq!(normalize("x86", "al"), "rax"); + assert_eq!(normalize("riscv64", "f10_d"), "f10"); + assert_eq!(normalize("mips", "d6"), "f12"); + assert_eq!(normalize("mips64", "d12_64"), "f12"); + assert_eq!(normalize("mips64", "a0_64"), "a0"); + assert_eq!(normalize("powerpc64", "x3"), "r3"); + assert_eq!(normalize("s390x", "r2d"), "r2"); + } + + #[test] + fn placement_from_mir() { + let mir = "--- |\n ; ModuleID = 'x'\n...\n---\nname: f0\nliveins:\n - { reg: '$r0', virtual-reg: '%0' }\n - { reg: '$r1', virtual-reg: '%1' }\nfixedStack:\n - { id: 0, type: default, offset: -8, size: 16, alignment: 8, stack-id: default,\n isImmutable: false }\nstack: []\nbody: |\n bb.0:\n liveins: $r0, $r1\n BX_RET 14 /* CC::al */, $noreg, implicit $r0\n...\n"; + let p = &placements(mir, "arm", 4, 0)["f0"]; + assert_eq!(p.ins, ["r0", "r1"]); + // Clipped to the incoming area: bytes 0..8 are slots 0 and 1. + assert_eq!(p.stack, [0, 1]); + assert_eq!(p.ret, ["r0"]); + let x86 = "--- |\n...\n---\nname: g\nliveins: []\nbody: |\n bb.0:\n RET 0, $al\n...\n"; + assert_eq!(placements(x86, "x86_64", 8, 0)["g"].ret, ["rax"]); + } +} diff --git a/docs/checks.md b/docs/checks.md index 1e149f5..b5ddeab 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -272,6 +272,60 @@ 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 | | + +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 @@ -355,7 +409,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 | diff --git a/docs/hunt.md b/docs/hunt.md index e52fcc5..43116f5 100644 --- a/docs/hunt.md +++ b/docs/hunt.md @@ -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<…>: 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 diff --git a/docs/hunt/m68k-repr-c-alignment.md b/docs/hunt/m68k-repr-c-alignment.md new file mode 100644 index 0000000..c3714d3 --- /dev/null +++ b/docs/hunt/m68k-repr-c-alignment.md @@ -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. diff --git a/docs/hunt/mips64-narrow-int-extension.md b/docs/hunt/mips64-narrow-int-extension.md new file mode 100644 index 0000000..570fc97 --- /dev/null +++ b/docs/hunt/mips64-narrow-int-extension.md @@ -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. diff --git a/docs/hunt/powerpc-bsd-struct-return.md b/docs/hunt/powerpc-bsd-struct-return.md new file mode 100644 index 0000000..d544fdd --- /dev/null +++ b/docs/hunt/powerpc-bsd-struct-return.md @@ -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(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"). diff --git a/docs/hunt/windows-arm32-vfp-aggregates.md b/docs/hunt/windows-arm32-vfp-aggregates.md new file mode 100644 index 0000000..656c5fc --- /dev/null +++ b/docs/hunt/windows-arm32-vfp-aggregates.md @@ -0,0 +1,53 @@ +# thumbv7a Windows: homogeneous float aggregates are not passed in VFP registers + +Facts for finding 41. Found by the ABI differential's assembly-level mode ([`checks.md`](../checks.md), +check 14). + +## What happens + +On thumbv7a-pc-windows-msvc and thumbv7a-uwp-windows-msvc, scalar `float`/`double` arguments go in +VFP registers on both sides (the target's LLVM float ABI is hard). A struct of one to four floats +or doubles, or a union of one float type, is passed and returned by clang in VFP registers (the +AAPCS VFP rules for homogeneous aggregates; clang marks these functions `arm_aapcs_vfpcc`). rustc +passes it in core registers and returns it through memory. + +```c +struct Pair { float a, b; }; +void take_pair(struct Pair p); +struct Pair make_pair(float a, float b); +``` + +Placement after instruction selection (`-O1`): + +| | clang `--target=thumbv7a-pc-windows-msvc` | rustc (pinned nightly) | +|---|---|---| +| `make_pair` arguments / return | s0, s1 / s0, s1 | sret pointer in r0, then s0, s1 / memory | +| `pass_pair(struct Pair)` argument | s0, s1 | r0, r1 | + +rustc's IR: `define void @make_pair(ptr sret([8 x i8]) align 4, float, float)` and +`define void @pass_pair([2 x i32])`, the same with 1.80.0, 1.90.0, 1.98.0, nightly-2026-07-18 and +nightly-2026-10-06. + +A second difference on the same targets, for aggregates with an `aligned(N)` attribute above 8: +rustc chooses 4- or 8-byte units from the natural alignment (`unadjusted_abi_align`), clang from +the declared alignment (on Linux targets clang uses the natural one, like rustc). Which one MSVC +uses was not checked; no MSVC here. + +## Where + +`compiler/rustc_target/src/callconv/arm.rs`, `compute_abi_info`: the VFP rules for homogeneous +aggregates apply when `abi_kind` is `AapcsVfp`, which is chosen only when +`cx.target_spec().cfg_abi == CfgAbi::EabiHf`. These targets have `llvm-floatabi: hard` and no +`eabihf` ABI, so they get the base AAPCS rules for aggregates while LLVM uses the VFP convention +for scalars. + +## Expected + +Microsoft's ARM32 ABI overview says non-variadic functions follow the ARM parameter-passing rules +including the VFP extensions (from the documentation; not checked against MSVC here). clang +implements that. + +## Scope + +Tier 3 targets; any `extern "C"` function, either direction, with a homogeneous float aggregate +argument or return. No rust-lang/rust issue found ("thumbv7a windows"). From c8b4aa6605bed3fa8c94deb2842ce9bf637a7c32 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sat, 10 Oct 2026 10:38:46 +0000 Subject: [PATCH 2/2] checks.md: nvptx64's skipped Rust side is the known ICE #163497 (also from a plain extern "C" return of an align(32) struct, since 1.90.0) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXiEXbESemwqMLYKaWLDbT --- docs/checks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/checks.md b/docs/checks.md index b5ddeab..a01cf5d 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -321,6 +321,7 @@ Run with `--all`, seeds 1–3, 300 functions each, 312 targets. | 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