From 1e2d91c38bbb8da4c01b09cfa33eeafccac44f02 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 17:38:21 -0700 Subject: [PATCH] i386: pass and return bare vectors directly, coercing only 8-byte integer vectors, i386: treat a bit_field as the aggregate it is in C, fixing a segfault on return, abi test: document the i386 microarch baseline; do not read a signal exit as a type index --- src/llvm_abi.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++++- tests/abi/cross.sh | 22 +++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index cd5a5d515..08d6adf69 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -479,11 +479,50 @@ namespace lbAbi386 { ft->ctx = c; ft->args = compute_arg_types(c, arg_types, arg_count, original_type); ft->ret = compute_return_type(ft, c, return_type, return_is_defined, return_is_tuple); + + // `bit_field` is treated as a struct. + Type *return_source = lb_abi_single_result_type(original_type); + if (return_is_defined && !return_is_tuple && + return_source != nullptr && is_type_bit_field(return_source) && + !lb_is_type_kind(return_type, LLVMStructTypeKind) && + !lb_is_type_kind(return_type, LLVMArrayTypeKind)) { + // Windows and the BSDs return a small struct in registers, same as the scalar + // path so only the psABI targets need moving to the hidden pointer. + bool small_in_registers = build_context.metrics.os == TargetOs_windows || + build_context.metrics.os == TargetOs_freebsd || + build_context.metrics.os == TargetOs_openbsd; + i64 sz = lb_sizeof(return_type); + bool returned_in_registers = small_in_registers && (sz == 1 || sz == 2 || sz == 4 || sz == 8); + if (!returned_in_registers) { + LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); + ft->ret = lb_arg_type_indirect(return_type, attr); + } + } + ft->calling_convention = calling_convention; return ft; } gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, bool is_return, Type *source_type) { + // A bare vector is passed and returned as itself; only aggregates + // take the indirect path below, ergo the vector check has to be first. + // + // Exception is an 8-byte vector Arg whose element is an integer: its an MMX type; + // clang coerces it to `i64` to keep it out of the MMX registers. An 8-byte + // vector of floats is an SSE type and stays itself, so the rule turns on the element and + // not on the width alone: + // + // <8 x i8> <4 x i16> <2 x i32> -> i64 + // <2 x float> <4 x half> -> unchanged + // + // The RETURN is never coerced -- `<8 x i8>` comes back as itself. + if (LLVMGetTypeKind(type) == LLVMVectorTypeKind) { + if (!is_return && lb_sizeof(type) == 8 && + LLVMGetTypeKind(LLVMGetElementType(type)) == LLVMIntegerTypeKind) { + return lb_arg_type_direct(type, LLVMIntTypeInContext(c, 64), nullptr, nullptr); + } + return lb_arg_type_direct(type, nullptr, nullptr, nullptr); + } if (!is_return && lb_sizeof(type) > 8) { return lb_arg_type_indirect(type, nullptr); } @@ -514,7 +553,11 @@ namespace lbAbi386 { LLVMTypeRef t = arg_types[i]; LLVMTypeKind kind = LLVMGetTypeKind(t); i64 sz = lb_sizeof(t); - if (kind == LLVMStructTypeKind || kind == LLVMArrayTypeKind) { + // `bit_field` lowers to a bare integer; C represents it as a struct with bit-field + // members, and i386 passes every struct by value on the stack. Use Src Type to match + bool is_aggregate = kind == LLVMStructTypeKind || kind == LLVMArrayTypeKind || + (srcs[i] != nullptr && is_type_bit_field(srcs[i])); + if (is_aggregate) { if (sz == 0) { args[i] = lb_arg_type_ignore(t); } else { diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 5113336e5..1a700114c 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -11,9 +11,19 @@ set -eu # clang (which targets everything) and qemu-user. # # ./cross.sh linux_arm64 aarch64-linux-gnu qemu-aarch64 -# ./cross.sh linux_i386 i386-linux-gnu qemu-i386 +# ./cross.sh linux_i386 i386-linux-gnu qemu-i386 -microarch:pentium4 # ./cross.sh linux_arm32 arm-linux-gnueabihf qemu-arm # ./cross.sh linux_riscv64 riscv64-linux-gnu qemu-riscv64 +# +# i386 needs a microarch: below SSE2 the x86 backend cannot legalise a +# sub-16-byte `f16` vector, and merely declaring a `proc "c"` that takes a +# `#simd[2]f16` aborts the compiler with "LLVM ERROR: Do not know how to split +# the result of this operator!". +# +# `pentium4` is what clang's own default for `i386-linux-gnu` is, `haswell` +# additionally has F16C, which isolates one non-ABI failure: `hfa4_f16` +# diverges at pentium4 and agrees at haswell. It is `_Float16` conversion +# codegen rather than a calling convention TARGET=${1:?odin target, e.g. linux_arm64} TRIPLE=${2:?clang triple, e.g. aarch64-linux-gnu} @@ -115,6 +125,16 @@ set +e rc=$? set -e +# A driver that DIES reports 128+signal, and that collides with the type indices: 139 is both +# SIGSEGV and a perfectly good index, so reading it as an index names an innocent type. There is no +# cheap way to tell them apart here. ABI_SKIP is a compile-time `-define` +if [ "$rc" -gt 128 ] && [ "$rc" -lt 165 ]; then + echo "$TARGET: exit $rc is AMBIGUOUS." >&2 + echo " Either type index $rc, or the driver died of signal $((rc-128)) (11 = SIGSEGV)." >&2 + echo " Re-run with -define:ABI_SKIP=$((rc+1)): if the result moves it was the type," >&2 + echo " and if it does not, a wrong-ABI call is corrupting the process." >&2 +fi + if [ "$rc" -eq 0 ]; then echo "$TARGET: every type agrees with clang" else