diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9810b17de..3968b4651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,8 @@ jobs: ./odin test tests/core/speed.odin -file -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -o:speed -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -microarch:native ./odin test tests/vendor -all-packages -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -microarch:native (cd tests/issues; ./run.sh) + (cd tests/abi; ./run.sh) + (cd tests/abi; ABI_CFLAGS=-O2 ./run.sh "" "" -o:speed) ./odin check tests/benchmark -vet -strict-style -no-entry-point build_freebsd: @@ -73,6 +75,8 @@ jobs: ./odin test tests/core/speed.odin -file -all-packages -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -o:speed -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true ./odin test tests/vendor -all-packages -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true (cd tests/issues; ./run.sh) + (cd tests/abi; ./run.sh) + (cd tests/abi; ABI_CFLAGS=-O2 ./run.sh "" "" -o:speed) ./odin check tests/benchmark -vet -strict-style -no-entry-point ci: strategy: @@ -171,6 +175,12 @@ jobs: cd tests/issues ./run.sh + - name: ABI comparator + run: | + cd tests/abi + ./run.sh + ABI_CFLAGS=-O2 ./run.sh "" "" -o:speed + - name: Run demo on WASI WASM32 run: | ./odin build examples/demo -target:wasi_wasm32 -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -out:demo @@ -284,6 +294,10 @@ jobs: call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" cd tests/issues call run.bat + cd ../abi + call run.bat + set ABI_CFLAGS=-O2 + call run.bat -o:speed - name: Check benchmarks shell: cmd run: | @@ -359,3 +373,9 @@ jobs: - name: Internals tests run: ./odin test tests/internal -all-packages -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -target:linux_riscv64 -extra-linker-flags:"-fuse-ld=/usr/bin/riscv64-linux-gnu-gcc-12 -static -Wl,-static" -no-rpath + + - name: ABI comparator + run: | + cd tests/abi + ./run.sh linux_riscv64 riscv64-linux-gnu "-extra-linker-flags:-fuse-ld=/usr/bin/riscv64-linux-gnu-gcc-12 -static -Wl,-static" -no-rpath + ABI_CFLAGS=-O2 ./run.sh linux_riscv64 riscv64-linux-gnu "-extra-linker-flags:-fuse-ld=/usr/bin/riscv64-linux-gnu-gcc-12 -static -Wl,-static" -no-rpath -o:speed diff --git a/.gitignore b/.gitignore index 56a75c788..b867398c4 100644 --- a/.gitignore +++ b/.gitignore @@ -307,6 +307,8 @@ build.sh *.raddbg *.rdi tests/issues/build/* +tests/abi/build/* +tests/abi/build-cross/* misc/featuregen/featuregen # Clangd stuff diff --git a/base/runtime/internal.odin b/base/runtime/internal.odin index a682f7e76..09fe64b60 100644 --- a/base/runtime/internal.odin +++ b/base/runtime/internal.odin @@ -1067,8 +1067,8 @@ quo_quaternion256 :: proc "contextless" (q, r: quaternion256) -> quaternion256 { return quaternion(w=t0, x=t1, y=t2, z=t3) } -@(link_name="__truncsfhf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -truncsfhf2 :: proc "c" (value: f32) -> __float16 { +@(private="file") +f32_to_f16 :: proc "contextless" (value: f32) -> __float16 { v: struct #raw_union { i: u32, f: f32 } i, s, e, m: i32 @@ -1124,18 +1124,8 @@ truncsfhf2 :: proc "c" (value: f32) -> __float16 { } } -@(link_name="__aeabi_d2h", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -aeabi_d2h :: proc "c" (value: f64) -> __float16 { - return truncsfhf2(f32(value)) -} - -@(link_name="__truncdfhf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -truncdfhf2 :: proc "c" (value: f64) -> __float16 { - return truncsfhf2(f32(value)) -} - -@(link_name="__gnu_h2f_ieee", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -gnu_h2f_ieee :: proc "c" (value_: __float16) -> f32 { +@(private="file") +f16_to_f32 :: proc "contextless" (value_: __float16) -> f32 { fp32 :: struct #raw_union { u: u32, f: f32 } value := transmute(u16)value_ @@ -1154,14 +1144,56 @@ gnu_h2f_ieee :: proc "c" (value_: __float16) -> f32 { } +// The conversion helpers below are libgcc / compiler-rt entry points, so their calling convention +// is compiler-rt's and not the target's ordinary one. On ARM they take and return their values in +// the core registers even though the target is AAPCS-VFP, where an ordinary `proc "c"` float +// travels in `s0`. clang's call sites move the value out of the VFP register and back around the +// call: +// +// vmov r0, s0 ; bl __gnu_h2f_ieee ; vmov s0, r0 +// +// Typing the boundary as integers is what puts them in the same registers. Declared as floats they +// land in `s0` at both ends and every `_Float16` conversion in C code linked against this runtime +// reads whatever the other register happened to hold. Everywhere else the helpers really do take +// and return floats, so only arm32 changes shape. +when ODIN_ARCH == .arm32 { + __f16_abi :: u16 + __f32_abi :: u32 + __f64_abi :: u64 +} else { + __f16_abi :: __float16 + __f32_abi :: f32 + __f64_abi :: f64 +} + +@(link_name="__truncsfhf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) +truncsfhf2 :: proc "c" (value: __f32_abi) -> __f16_abi { + return transmute(__f16_abi)f32_to_f16(transmute(f32)value) +} + @(link_name="__gnu_f2h_ieee", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -gnu_f2h_ieee :: proc "c" (value: f32) -> __float16 { - return truncsfhf2(value) +gnu_f2h_ieee :: proc "c" (value: __f32_abi) -> __f16_abi { + return transmute(__f16_abi)f32_to_f16(transmute(f32)value) +} + +@(link_name="__aeabi_d2h", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) +aeabi_d2h :: proc "c" (value: __f64_abi) -> __f16_abi { + return transmute(__f16_abi)f32_to_f16(f32(transmute(f64)value)) +} + +@(link_name="__truncdfhf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) +truncdfhf2 :: proc "c" (value: __f64_abi) -> __f16_abi { + return transmute(__f16_abi)f32_to_f16(f32(transmute(f64)value)) +} + +@(link_name="__gnu_h2f_ieee", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) +gnu_h2f_ieee :: proc "c" (value: __f16_abi) -> __f32_abi { + return transmute(__f32_abi)f16_to_f32(transmute(__float16)value) } @(link_name="__extendhfsf2", linkage=RUNTIME_LINKAGE, require=RUNTIME_REQUIRE) -extendhfsf2 :: proc "c" (value: __float16) -> f32 { - return gnu_h2f_ieee(value) +extendhfsf2 :: proc "c" (value: __f16_abi) -> __f32_abi { + return transmute(__f32_abi)f16_to_f32(transmute(__float16)value) } when .Address in ODIN_SANITIZER_FLAGS { diff --git a/src/build_settings.cpp b/src/build_settings.cpp index cefe2f4c7..7932bd635 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -694,100 +694,100 @@ gb_internal isize MAX_ERROR_COLLECTOR_COUNT(void) { gb_global TargetMetrics target_windows_i386 = { TargetOs_windows, TargetArch_i386, - 4, 4, I386_MAX_ALIGNMENT, 16, + 4, 4, I386_MAX_ALIGNMENT, 512, str_lit("i386-pc-windows-msvc"), }; gb_global TargetMetrics target_windows_amd64 = { TargetOs_windows, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-pc-windows-msvc"), }; gb_global TargetMetrics target_linux_i386 = { TargetOs_linux, TargetArch_i386, - 4, 4, I386_MAX_ALIGNMENT, 16, + 4, 4, I386_MAX_ALIGNMENT, 512, str_lit("i386-pc-linux-gnu"), }; gb_global TargetMetrics target_linux_amd64 = { TargetOs_linux, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-pc-linux-gnu"), }; gb_global TargetMetrics target_linux_arm64 = { TargetOs_linux, TargetArch_arm64, - 8, 8, 16, 32, + 8, 8, 16, 16, str_lit("aarch64-linux-elf"), }; gb_global TargetMetrics target_linux_arm32 = { TargetOs_linux, TargetArch_arm32, - 4, 4, 8, 16, + 4, 4, 8, 8, str_lit("arm-unknown-linux-gnueabihf"), }; gb_global TargetMetrics target_linux_riscv64 = { TargetOs_linux, TargetArch_riscv64, - 8, 8, 16, 32, + 8, 8, 16, 512, str_lit("riscv64-linux-gnu"), }; gb_global TargetMetrics target_darwin_amd64 = { TargetOs_darwin, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 16, str_lit("x86_64-apple-macosx"), // NOTE: Changes during initialization based on build flags. }; gb_global TargetMetrics target_darwin_arm64 = { TargetOs_darwin, TargetArch_arm64, - 8, 8, 16, 32, + 8, 8, 16, 16, str_lit("arm64-apple-macosx"), // NOTE: Changes during initialization based on build flags. }; gb_global TargetMetrics target_freebsd_i386 = { TargetOs_freebsd, TargetArch_i386, - 4, 4, I386_MAX_ALIGNMENT, 16, + 4, 4, I386_MAX_ALIGNMENT, 512, str_lit("i386-unknown-freebsd-elf"), }; gb_global TargetMetrics target_freebsd_amd64 = { TargetOs_freebsd, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-unknown-freebsd-elf"), }; gb_global TargetMetrics target_freebsd_arm64 = { TargetOs_freebsd, TargetArch_arm64, - 8, 8, 16, 32, + 8, 8, 16, 16, str_lit("aarch64-unknown-freebsd-elf"), }; gb_global TargetMetrics target_openbsd_amd64 = { TargetOs_openbsd, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-unknown-openbsd-elf"), }; gb_global TargetMetrics target_netbsd_amd64 = { TargetOs_netbsd, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-unknown-netbsd-elf"), }; gb_global TargetMetrics target_netbsd_arm64 = { TargetOs_netbsd, TargetArch_arm64, - 8, 8, 16, 32, + 8, 8, 16, 16, str_lit("aarch64-unknown-netbsd-elf"), }; @@ -795,21 +795,21 @@ gb_global TargetMetrics target_netbsd_arm64 = { gb_global TargetMetrics target_freestanding_wasm32 = { TargetOs_freestanding, TargetArch_wasm32, - 4, 4, 8, 16, + 4, 4, 8, 512, str_lit("wasm32-freestanding-js"), }; gb_global TargetMetrics target_js_wasm32 = { TargetOs_js, TargetArch_wasm32, - 4, 4, 8, 16, + 4, 4, 8, 512, str_lit("wasm32-js-js"), }; gb_global TargetMetrics target_wasi_wasm32 = { TargetOs_wasi, TargetArch_wasm32, - 4, 4, 8, 16, + 4, 4, 8, 512, str_lit("wasm32-wasi-js"), }; @@ -817,7 +817,7 @@ gb_global TargetMetrics target_wasi_wasm32 = { gb_global TargetMetrics target_orca_wasm32 = { TargetOs_orca, TargetArch_wasm32, - 4, 4, 8, 16, + 4, 4, 8, 512, str_lit("wasm32-wasi-js"), }; @@ -825,21 +825,21 @@ gb_global TargetMetrics target_orca_wasm32 = { gb_global TargetMetrics target_freestanding_wasm64p32 = { TargetOs_freestanding, TargetArch_wasm64p32, - 4, 8, 8, 16, + 4, 8, 8, 512, str_lit("wasm32-freestanding-js"), }; gb_global TargetMetrics target_js_wasm64p32 = { TargetOs_js, TargetArch_wasm64p32, - 4, 8, 8, 16, + 4, 8, 8, 512, str_lit("wasm32-js-js"), }; gb_global TargetMetrics target_wasi_wasm64p32 = { TargetOs_wasi, TargetArch_wasm32, - 4, 8, 8, 16, + 4, 8, 8, 512, str_lit("wasm32-wasi-js"), }; @@ -848,7 +848,7 @@ gb_global TargetMetrics target_wasi_wasm64p32 = { gb_global TargetMetrics target_freestanding_amd64_sysv = { TargetOs_freestanding, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-pc-none-gnu"), TargetABI_SysV, }; @@ -856,7 +856,7 @@ gb_global TargetMetrics target_freestanding_amd64_sysv = { gb_global TargetMetrics target_freestanding_amd64_win64 = { TargetOs_freestanding, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-pc-windows-msvc"), TargetABI_Win64, }; @@ -864,7 +864,7 @@ gb_global TargetMetrics target_freestanding_amd64_win64 = { gb_global TargetMetrics target_freestanding_amd64_mingw = { TargetOs_freestanding, TargetArch_amd64, - 8, 8, AMD64_MAX_ALIGNMENT, 32, + 8, 8, AMD64_MAX_ALIGNMENT, 512, str_lit("x86_64-pc-windows-gnu"), TargetABI_Win64, }; @@ -873,20 +873,20 @@ gb_global TargetMetrics target_freestanding_amd64_mingw = { gb_global TargetMetrics target_freestanding_arm64 = { TargetOs_freestanding, TargetArch_arm64, - 8, 8, 16, 32, + 8, 8, 16, 16, str_lit("aarch64-none-elf"), }; gb_global TargetMetrics target_freestanding_arm32 = { TargetOs_freestanding, TargetArch_arm32, - 4, 4, 8, 16, + 4, 4, 8, 8, str_lit("arm-none-eabihf"), }; gb_global TargetMetrics target_freestanding_riscv64 = { TargetOs_freestanding, TargetArch_riscv64, - 8, 8, 16, 32, + 8, 8, 16, 512, str_lit("riscv64-unknown-gnu"), }; diff --git a/src/check_type.cpp b/src/check_type.cpp index 68c332b3d..d603a8f8e 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -677,7 +677,9 @@ gb_internal void check_struct_type(CheckerContext *ctx, Type *struct_type, Ast * scope_reserve(ctx->scope, min_field_count); - if (st->is_raw_union && min_field_count > 1) { + // Even a one-field `#raw_union` must be marked. RISC-V psABI excludes unions from the hardware + // floating-point convention. `struct{union{f32}}` goes in `a0` where `struct{f32}` goes in `fa0`. + if (st->is_raw_union) { struct_type->Struct.is_raw_union = true; context = str_lit("struct #raw_union"); } diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index a760c47b8..cde9ed0b2 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -49,9 +49,15 @@ gb_internal lbArgType lb_arg_type_indirect(LLVMTypeRef type, LLVMAttributeRef at return lbArgType{lbArg_Indirect, type, nullptr, nullptr, attr, nullptr, 0, false}; } -gb_internal lbArgType lb_arg_type_indirect_byval(LLVMContextRef c, LLVMTypeRef type) { - i64 alignment = lb_alignof(type); - alignment = gb_max(alignment, 8); +gb_internal lbArgType lb_arg_type_indirect_byval(LLVMContextRef c, LLVMTypeRef type, Type *source_type = nullptr) { + // the outgoing stack slot, which i386 never over-aligns, not even for an over-aligned struct + i64 alignment = build_context.ptr_size; + if (build_context.metrics.arch != TargetArch_i386) { + // `#align` and `#min_field_align` do not survive lowering, so ask the source + // type where there is one + i64 a = source_type != nullptr ? type_align_of(source_type) : lb_alignof(type); + alignment = gb_max(alignment, a); + } LLVMAttributeRef byval_attr = lb_create_enum_attribute_with_type(c, "byval", type); LLVMAttributeRef align_attr = lb_create_enum_attribute(c, "align", alignment); @@ -357,6 +363,32 @@ gb_internal i64 lb_alignof(LLVMTypeRef type) { return 1; } +// The alignment LLVM itself will give the lowered type, which is not `lb_alignof`: +// that applies `max_simd_align`, and LLVM knows nothing about it. A 32-byte vector +// is 16-aligned on arm64 and Darwin and 32-aligned to LLVM, and a struct holding +// one has to be packed or LLVM re-inserts padding and moves the member. +gb_internal i64 lb_llvm_natural_alignof(LLVMTypeRef type) { + switch (LLVMGetTypeKind(type)) { + case LLVMStructTypeKind: + { + if (LLVMIsPackedStruct(type)) { + return 1; + } + unsigned field_count = LLVMCountStructElementTypes(type); + i64 max_align = 1; + for (unsigned i = 0; i < field_count; i++) { + max_align = gb_max(max_align, lb_llvm_natural_alignof(LLVMStructGetTypeAtIndex(type, i))); + } + return max_align; + } + case LLVMArrayTypeKind: + return lb_llvm_natural_alignof(OdinLLVMGetArrayElementType(type)); + case LLVMVectorTypeKind: + return gb_max(next_pow2(lb_sizeof(type)), 1); + } + return lb_alignof(type); +} + #define LB_ABI_INFO(name) lbFunctionType *name(lbModule *m, LLVMTypeRef *arg_types, unsigned arg_count, LLVMTypeRef return_type, bool return_is_defined, bool return_is_tuple, ProcCallingConvention calling_convention, Type *original_type) typedef LB_ABI_INFO(lbAbiInfoType); @@ -401,21 +433,124 @@ gb_internal lbArgType lb_abi_modify_return_is_tuple(lbFunctionType *ft, LLVMCont } while (0) // NOTE(bill): I hate `namespace` in C++ but this is just because I don't want to prefix everything + +// Every psABI except AAPCS64 and Win64 makes the caller widen a sub-word integer to 32 bits, in +// both argument and return position, and clang records that as `signext`/`zeroext`. A callee +// compiled against the attribute reads the whole 32-bit register rather than the byte, so omitting +// it hands the callee whatever the high bits happened to hold. +gb_internal LLVMAttributeRef lb_integer_extension_attribute(LLVMContextRef c, LLVMTypeRef type, Type *source_type) { + if (source_type == nullptr) { + // Knowable without the source: an `i1` is always zero-extended. + return type == LLVMInt1TypeInContext(c) ? lb_create_enum_attribute(c, "zeroext") : nullptr; + } + if (lb_sizeof(type) >= 4) { + return nullptr; + } + if (!is_type_integer_like(source_type) && !is_type_enum(source_type)) { + return nullptr; + } + if (is_type_unsigned(source_type) || is_type_boolean(source_type)) { + return lb_create_enum_attribute(c, "zeroext"); + } + return lb_create_enum_attribute(c, "signext"); +} + +// The source type of each parameter, where one exists. `arg_types` can carry entries with no +// counterpart, so the tuple is walked rather than indexed. +gb_internal Array lb_abi_param_source_types(Type *proc_type, unsigned arg_count) { + auto out = array_make(temporary_allocator(), cast(isize)arg_count); + Entity **params = nullptr; + isize param_count = 0; + if (proc_type != nullptr && proc_type->kind == Type_Proc && proc_type->Proc.params != nullptr) { + params = proc_type->Proc.params->Tuple.variables.data; + param_count = proc_type->Proc.params->Tuple.variables.count; + } + for (unsigned i = 0, j = 0; i < arg_count; i++, j++) { + while (cast(isize)j < param_count && params[j]->kind != Entity_Variable) { + j++; + } + out[i] = cast(isize)j < param_count ? params[j]->type : nullptr; + } + return out; +} + +// A single result can be classified from its source type. A tuple cannot: it is split into +// out-pointers, and C has no such return shape anyway. +gb_internal Type *lb_abi_single_result_type(Type *proc_type) { + if (proc_type != nullptr && proc_type->kind == Type_Proc && + proc_type->Proc.results != nullptr && + proc_type->Proc.results->Tuple.variables.count == 1) { + return proc_type->Proc.results->Tuple.variables[0]->type; + } + return nullptr; +} + namespace lbAbi386 { - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count); + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, Type *original_type); gb_internal LB_ABI_COMPUTE_RETURN_TYPE(compute_return_type); gb_internal LB_ABI_INFO(abi_info) { LLVMContextRef c = m->ctx; lbFunctionType *ft = permanent_alloc_item(); ft->ctx = c; - ft->args = compute_arg_types(c, arg_types, arg_count); + 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); + } + } + + // A complex lowers to a struct of two floats. The struct rule sends every struct through + // a hidden pointer. The psABI gives complex its own rule: one of eight bytes or fewer + // comes back in EAX:EDX, and only the wider ones go through memory. `complex64` is + // returned coerced to `i64` and `complex128` keeps the hidden pointer. + if (return_is_defined && !return_is_tuple && + return_source != nullptr && is_type_complex(return_source)) { + i64 sz = lb_sizeof(return_type); + if (sz > 0 && sz <= 8) { + ft->ret = lb_arg_type_direct(return_type, LLVMIntTypeInContext(c, cast(unsigned)(sz*8)), nullptr, nullptr); + } + } + ft->calling_convention = calling_convention; return ft; } - gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, bool is_return) { + 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); } @@ -434,22 +569,23 @@ namespace lbAbi386 { return lb_arg_type_direct(type, cast_type, nullptr, nullptr); } - LLVMAttributeRef attr = nullptr; - LLVMTypeRef i1 = LLVMInt1TypeInContext(c); - if (type == i1) { - attr = lb_create_enum_attribute(c, "zeroext"); - } + LLVMAttributeRef attr = lb_integer_extension_attribute(c, type, source_type); return lb_arg_type_direct(type, nullptr, nullptr, attr); } - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count) { + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, Type *original_type) { auto args = array_make(lb_function_type_args_allocator(), arg_count); + auto srcs = lb_abi_param_source_types(original_type, arg_count); for (unsigned i = 0; i < arg_count; i++) { 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 { @@ -458,7 +594,7 @@ namespace lbAbi386 { args[i] = lb_arg_type_indirect_byval(c, t); } } else { - args[i] = non_struct(c, t, false); + args[i] = non_struct(c, t, false, srcs[i]); } } return args; @@ -490,25 +626,25 @@ namespace lbAbi386 { LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); return lb_arg_type_indirect(return_type, attr); } - return non_struct(c, return_type, true); + return non_struct(c, return_type, true, nullptr); } }; namespace lbAbiAmd64Win64 { - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count); + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, Type *original_type); gb_internal LB_ABI_COMPUTE_RETURN_TYPE(compute_return_type); gb_internal LB_ABI_INFO(abi_info) { LLVMContextRef c = m->ctx; lbFunctionType *ft = permanent_alloc_item(); ft->ctx = c; - ft->args = compute_arg_types(c, arg_types, arg_count); + 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); ft->calling_convention = calling_convention; return ft; } - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count) { + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, Type *original_type) { auto args = array_make(lb_function_type_args_allocator(), arg_count); for (unsigned i = 0; i < arg_count; i++) { @@ -528,7 +664,7 @@ namespace lbAbiAmd64Win64 { break; } } else { - args[i] = lbAbi386::non_struct(c, t, false); + args[i] = lbAbi386::non_struct(c, t, false, nullptr); } } return args; @@ -551,7 +687,7 @@ namespace lbAbiAmd64Win64 { LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); return lb_arg_type_indirect(return_type, attr); } - return lbAbi386::non_struct(c, return_type, true); + return lbAbi386::non_struct(c, return_type, true, nullptr); } }; @@ -734,16 +870,28 @@ namespace lbAbiAmd64SysV { } gb_internal bool is_aggregate(LLVMTypeRef type) { + // A single-member wrapper is passed like its member, but only while that + // member still fits one eightbyte. `struct{i128}` needs two registers and + // goes to memory when they are gone. A bare `i128` does not; clang emits + // `byval align 16` for the struct and a plain `i128` for the scalar. LLVMTypeKind kind = LLVMGetTypeKind(type); switch (kind) { case LLVMStructTypeKind: if (LLVMCountStructElementTypes(type) == 1) { - return is_aggregate(LLVMStructGetTypeAtIndex(type, 0)); + LLVMTypeRef elem = LLVMStructGetTypeAtIndex(type, 0); + // A VECTOR member keeps the wrapper an aggregate whatever its size: + // LLVM gives a vector its own oversized stack slot, so an unwrapped + // `struct{#simd[2]f32}` takes 16 bytes where the ABI wants 8. + if (LLVMGetTypeKind(elem) == LLVMVectorTypeKind) { + return true; + } + return lb_sizeof(elem) > 8 || is_aggregate(elem); } return true; case LLVMArrayTypeKind: if (LLVMGetArrayLength(type) == 1) { - return is_aggregate(LLVMGetElementType(type)); + LLVMTypeRef elem = OdinLLVMGetArrayElementType(type); + return lb_sizeof(elem) > 8 || is_aggregate(elem); } return true; } @@ -793,15 +941,25 @@ namespace lbAbiAmd64SysV { } } - if (is_register(type)) { - LLVMAttributeRef attribute = nullptr; - if (type == LLVMInt1TypeInContext(c)) { - attribute = lb_create_enum_attribute(c, "zeroext"); + if (cls.count > 2 && is_sse(cls[0])) { + // An SSE run wider than two eightbytes has no register to land in at + // the baseline ISA. It goes to memory, bare vector or struct-wrapped. + // A bare vector RETURN is the one exception: it is not an aggregate. + // Clang gives it no hidden pointer and lets LLVM split it across + // xmm0:xmm1, but the struct that wraps it still gets one. + if (is_arg) { + return lb_arg_type_indirect_byval(c, type, source_type); } + if (LLVMGetTypeKind(type) != LLVMVectorTypeKind) { + all_mem(&cls); + } + } + if (is_register(type)) { + LLVMAttributeRef attribute = lb_integer_extension_attribute(c, type, source_type); return lb_arg_type_direct(type, nullptr, nullptr, attribute); } else if (ran_out_of_regs) { if (is_arg) { - return lb_arg_type_indirect_byval(c, type); + return lb_arg_type_indirect_byval(c, type, source_type); } else { LLVMAttributeRef attribute = lb_create_enum_attribute_with_type(c, "sret", type); return lb_arg_type_indirect(type, attribute); @@ -812,7 +970,7 @@ namespace lbAbiAmd64SysV { if (is_calling_convention_odin(calling_convention)) { return lb_arg_type_indirect(type, attribute); } - return lb_arg_type_indirect_byval(c, type); + return lb_arg_type_indirect_byval(c, type, source_type); } else if (attribute_kind == Amd64TypeAttribute_StructRect) { attribute = lb_create_enum_attribute_with_type(c, "sret", type); } @@ -827,7 +985,11 @@ namespace lbAbiAmd64SysV { } else { reg_type = llreg(c, cls, type); } - return lb_arg_type_direct(type, reg_type, nullptr, nullptr); + // `is_register` above answers false for every integer narrower than 16 bytes, so a + // sub-word scalar lands HERE rather than in the direct arm, and this is where its + // extension attribute has to go. + LLVMAttributeRef attribute = lb_integer_extension_attribute(c, type, source_type); + return lb_arg_type_direct(type, reg_type, nullptr, attribute); } } @@ -969,7 +1131,11 @@ namespace lbAbiAmd64SysV { i64 sz = lb_sizeof(t); i64 words = (sz + 7)/8; auto reg_classes = array_make(heap_allocator(), cast(isize)words); - if (words > 4) { + if (words > 4 && LLVMGetTypeKind(t) != LLVMVectorTypeKind) { + // A BARE vector is exempt: it is not an aggregate. Clang never gives it + // a hidden pointer however wide it is. `<16 x float>` is returned directly + // and split across xmm0-xmm3, and the SSE run below still sends it to + // memory as an ARGUMENT. all_mem(®_classes); } else { bool from_source = source_type != nullptr && source_is_classifiable(source_type) && @@ -996,6 +1162,17 @@ namespace lbAbiAmd64SysV { return reg_classes; } + // How much of its eightbyte an SSE class occupies, which is what `llreg` turns + // it back into: the scalar classes are as wide as their element, and every `v` + // class becomes a vector spanning the whole eightbyte. + gb_internal i64 sse_class_width(RegClass c) { + switch (c) { + case RegClass_SSEHs: return 2; + case RegClass_SSEFs: return 4; + } + return 8; + } + gb_internal void unify(Array *cls, i64 i, RegClass const newv) { RegClass const oldv = (*cls)[cast(isize)i]; if (oldv == newv) { @@ -1029,6 +1206,14 @@ namespace lbAbiAmd64SysV { case RegClass_SSEInt64: return; } + } else if (is_sse(oldv) && is_sse(newv) && sse_class_width(oldv) > sse_class_width(newv)) { + // The members OVERLAP, a union. Last-writer-wins would pass + // `union{f64, f32}` as a 4-byte float, and `union{[2]f32, f32}` as one + // lane of two, losing the top half of the eightbyte either way. Keeping + // the WIDER class is what leaves `struct{f32, f16}` alone: that is Fs + // then Hv at offset 4, and Hv spans the eightbyte, so it still widens + // rather than gets picked over. + return; } (*cls)[cast(isize)i] = to_write; @@ -1043,7 +1228,8 @@ namespace lbAbiAmd64SysV { RegClass &oldv = (*cls)[cast(isize)i]; if (is_sse(oldv)) { for (i++; i < e; i++) { - if (oldv != RegClass_SSEUp) { + // NOTE: the current eightbyte, not `oldv`, is bound to cls[0], which is never SSEUp + if ((*cls)[cast(isize)i] != RegClass_SSEUp) { all_mem(cls); return; } @@ -1109,6 +1295,15 @@ namespace lbAbiAmd64SysV { } i64 sz = lb_sizeof(type); + if (LLVMGetTypeKind(type) == LLVMVectorTypeKind && sz == 8 && + reg_classes.count == 1 && is_sse(reg_classes[0])) { + // LLVM rounds a bare vector's stack slot up to the legal vector width. + // An 8-byte one takes 16 bytes where the ABI wants 8. clang coerces every + // 64-bit vector to `double`, which lands in the same half of the same xmm + // and takes one eightbyte on the stack. + array_free(&types); + return LLVMDoubleTypeInContext(c); + } if (all_ints) { for_array(i, reg_classes) { GB_ASSERT(sz > 0); @@ -1175,7 +1370,16 @@ namespace lbAbiAmd64SysV { } unsigned vec_len = llvec_len(reg_classes, i+1); - LLVMTypeRef vec_type = LLVMVectorType(elem_type, vec_len * elems_per_word); + unsigned lanes = vec_len * elems_per_word; + // Never widen past what is actually left: a 4-byte vector + // occupies half an eightbyte, and padding it to a whole one + // makes the parameter 8 bytes where clang coerces to i32. + i64 elem_bytes = lb_sizeof(elem_type); + if (elem_bytes > 0 && sz > 0 && cast(i64)lanes * elem_bytes > sz) { + lanes = cast(unsigned)(sz / elem_bytes); + } + if (lanes == 0) { lanes = 1; } + LLVMTypeRef vec_type = LLVMVectorType(elem_type, lanes); array_add(&types, vec_type); sz -= lb_sizeof(vec_type); i += vec_len; @@ -1275,6 +1479,13 @@ namespace lbAbiAmd64SysV { LLVMTypeRef elem = OdinLLVMGetVectorElementType(t); i64 elem_sz = lb_sizeof(elem); LLVMTypeKind elem_kind = LLVMGetTypeKind(elem); + if (t_size < 8) { + // A vector narrower than an eightbyte is INTEGER, not SSE: + // clang coerces `<4 x i8>` to `i32` and passes it in an integer + // register. + unify(cls, ix + off/8, RegClass_Int); + break; + } RegClass reg = RegClass_NoClass; switch (elem_kind) { case LLVMIntegerTypeKind: { @@ -1435,6 +1646,10 @@ namespace lbAbiArm64 { unsigned field_member_count = 0; LLVMTypeRef elem = LLVMStructGetTypeAtIndex(type, i); + if (lb_is_type_kind(elem, LLVMStructTypeKind) && lb_sizeof(elem) == 0) { + // an empty struct occupies nothing and is ignored + continue; + } if (!is_homogenous_aggregate(c, elem, &field_type, &field_member_count)) { return false; } @@ -1467,6 +1682,7 @@ namespace lbAbiArm64 { gb_internal bool is_homogenous_aggregate(LLVMContextRef c, LLVMTypeRef type, LLVMTypeRef *base_type_, unsigned *member_count_) { LLVMTypeKind kind = LLVMGetTypeKind(type); switch (kind) { + case LLVMHalfTypeKind: case LLVMFloatTypeKind: case LLVMDoubleTypeKind: if (base_type_) *base_type_ = type; @@ -1504,6 +1720,10 @@ namespace lbAbiArm64 { switch (bt->kind) { case Type_Basic: switch (bt->Basic.kind) { + case Basic_f16: + if (base_type_) *base_type_ = LLVMHalfTypeInContext(c); + if (member_count_) *member_count_ = 1; + return true; case Basic_f32: if (base_type_) *base_type_ = LLVMFloatTypeInContext(c); if (member_count_) *member_count_ = 1; @@ -1515,6 +1735,10 @@ namespace lbAbiArm64 { } return false; case Type_Array: { + if (bt->Array.count == 0) { + // a zero-length member disqualifies the aggregate, unlike an empty struct + return false; + } LLVMTypeRef elem_base = nullptr; unsigned elem_count = 0; if (!is_homogenous_aggregate_source(c, bt->Array.elem, &elem_base, &elem_count)) { @@ -1531,6 +1755,11 @@ namespace lbAbiArm64 { LLVMTypeRef found_base = nullptr; unsigned total = 0; for (Entity *f : bt->Struct.fields) { + Type *fbt = base_type(f->type); + if (fbt != nullptr && fbt->kind == Type_Struct && type_size_of(f->type) == 0) { + // an empty struct occupies nothing and is ignored + continue; + } LLVMTypeRef field_base = nullptr; unsigned field_count = 0; if (!is_homogenous_aggregate_source(c, f->type, &field_base, &field_count)) { @@ -1572,20 +1801,12 @@ namespace lbAbiArm64 { return lb_arg_type_direct(LLVMVoidTypeInContext(c)); } else if (is_register(return_type)) { return non_struct(c, return_type, nullptr); - } else if (is_homogenous_aggregate(c, return_type, &homo_base_type, &homo_member_count)) { - if (is_homogenous_aggregate_small_enough(homo_base_type, homo_member_count)) { - return lb_arg_type_direct(return_type, llvm_array_type(homo_base_type, homo_member_count), nullptr, nullptr); - } else { - //TODO(Platin): do i need to create stuff that can handle the diffrent return type? - // else this needs a fix in llvm_backend_proc as we would need to cast it to the correct array type - - LB_ABI_MODIFY_RETURN_IF_TUPLE_MACRO(); - - //LLVMTypeRef array_type = llvm_array_type(homo_base_type, homo_member_count); - LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); - return lb_arg_type_indirect(return_type, attr); - } + } else if (is_homogenous_aggregate(c, return_type, &homo_base_type, &homo_member_count) && + is_homogenous_aggregate_small_enough(homo_base_type, homo_member_count)) { + return lb_arg_type_direct(return_type, llvm_array_type(homo_base_type, homo_member_count), nullptr, nullptr); } else { + // too many members to be an HFA falls through to the size rule, it does not + // become indirect on its own: `struct{[5]f16}` is 10 bytes and goes in x0:x1 i64 size = lb_sizeof(return_type); if (size > 16) { LB_ABI_MODIFY_RETURN_IF_TUPLE_MACRO(); @@ -1595,6 +1816,13 @@ namespace lbAbiArm64 { } GB_ASSERT(size <= 16); + if (LLVMGetTypeKind(return_type) == LLVMVectorTypeKind) { + // A vector too narrow to be a short vector is still RETURNED as + // itself. clang coerces a 4-byte vector argument to `i32` and puts + // it in w0, but returns `<4 x i8>` in v0; coercing the return too + // picks the wrong register file. + return lb_arg_type_direct(return_type, nullptr, nullptr, nullptr); + } LLVMTypeRef cast_type = nullptr; if (size == 0) { cast_type = LLVMStructTypeInContext(c, nullptr, 0, false); @@ -1632,12 +1860,9 @@ namespace lbAbiArm64 { if (is_register(type)) { args[i] = non_struct(c, type, ptype); - } else if (is_homogenous_aggregate(c, type, &homo_base_type, &homo_member_count)) { - if (is_homogenous_aggregate_small_enough(homo_base_type, homo_member_count)) { - args[i] = lb_arg_type_direct(type, llvm_array_type(homo_base_type, homo_member_count), nullptr, nullptr); - } else { - args[i] = lb_arg_type_indirect(type, nullptr);; - } + } else if (is_homogenous_aggregate(c, type, &homo_base_type, &homo_member_count) && + is_homogenous_aggregate_small_enough(homo_base_type, homo_member_count)) { + args[i] = lb_arg_type_direct(type, llvm_array_type(homo_base_type, homo_member_count), nullptr, nullptr); } else if (is_homogenous_aggregate_source(c, ptype, &src_base_type, &src_member_count) && is_homogenous_aggregate_small_enough(src_base_type, src_member_count)) { args[i] = lb_arg_type_direct(type, llvm_array_type(src_base_type, src_member_count), nullptr, nullptr); @@ -1881,15 +2106,15 @@ namespace lbAbiWasm { } namespace lbAbiArm32 { - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, ProcCallingConvention calling_convention); - gb_internal lbArgType compute_return_type(LLVMContextRef c, LLVMTypeRef return_type, bool return_is_defined, ProcCallingConvention calling_convention); + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, ProcCallingConvention calling_convention, Type *original_type); + gb_internal lbArgType compute_return_type(LLVMContextRef c, LLVMTypeRef return_type, bool return_is_defined, ProcCallingConvention calling_convention, Type *return_source); gb_internal LB_ABI_INFO(abi_info) { LLVMContextRef c = m->ctx; lbFunctionType *ft = permanent_alloc_item(); ft->ctx = c; - ft->args = compute_arg_types(c, arg_types, arg_count, calling_convention); - ft->ret = compute_return_type(c, return_type, return_is_defined, calling_convention); + ft->args = compute_arg_types(c, arg_types, arg_count, calling_convention, original_type); + ft->ret = compute_return_type(c, return_type, return_is_defined, calling_convention, lb_abi_single_result_type(original_type)); ft->calling_convention = calling_convention; return ft; } @@ -1913,28 +2138,95 @@ namespace lbAbiArm32 { return false; } - gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, bool is_return) { - LLVMAttributeRef attr = nullptr; - LLVMTypeRef i1 = LLVMInt1TypeInContext(c); - if (type == i1) { - attr = lb_create_enum_attribute(c, "zeroext"); + gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, bool is_return, Type *source_type) { + // A bare vector narrower than a word has no register of its own to sit in, clang coerces + // it to `i32` as an argument whatever its element is. The return keeps the vector + // type, same as x86, except: a half vector is not a legal type at this microarchitecture + // (`arm1176jzf-s` has VFP2 but no fp16), so clang coerces that one in both directions. + // + // <4 x i8> <2 x i16> <2 x half> as an argument -> i32 + // <4 x i8> <2 x i16> as a return -> unchanged + // <2 x half> as a return -> i32 + if (LLVMGetTypeKind(type) == LLVMVectorTypeKind && lb_sizeof(type) == 4) { + bool is_half = LLVMGetTypeKind(LLVMGetElementType(type)) == LLVMHalfTypeKind; + if (!is_return || is_half) { + return lb_arg_type_direct(type, LLVMIntTypeInContext(c, 32), nullptr, nullptr); + } } + LLVMAttributeRef attr = lb_integer_extension_attribute(c, type, source_type); return lb_arg_type_direct(type, nullptr, nullptr, attr); } - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, ProcCallingConvention calling_convention) { + // AAPCS32 ยง5.5, the VFP variant that the `gnueabihf` triple selects: an aggregate of at most + // four members that are all the same fp type is a Homogeneous fp Aggregate, and travels + // in s0-s3 / d0-d3 rather than in the core registers. Everything below coerces aggregates to + // `[N x i32]`, which puts an HFA in r0-r3 where the C side reads s0-s3. + // + // The detector is arm64's: AAPCS64 states the same rule over the same shapes. + // `coerce_` is set when the lowered type cannot express the HFA and LLVM has to be handed an + // `[N x base]` instead of the type itself. That happens for a `#raw_union`, which has become + // an opaque integer by now and AAPCS32 DOES count a union of floats as homogeneous + gb_internal bool is_hfa(LLVMContextRef c, LLVMTypeRef type, Type *source_type, + ProcCallingConvention calling_convention, LLVMTypeRef *coerce_) { + if (is_calling_convention_odin(calling_convention)) { + // Both sides are Odin, so the existing lowering is self-consistent; leave it alone. + return false; + } + LLVMTypeRef base_type = nullptr; + unsigned member_count = 0; + bool needs_coerce = false; + if (!lbAbiArm64::is_homogenous_aggregate(c, type, &base_type, &member_count)) { + if (source_type == nullptr || + !lbAbiArm64::is_homogenous_aggregate_source(c, source_type, &base_type, &member_count)) { + return false; + } + needs_coerce = true; + } + if (member_count == 0 || member_count > 4) { + return false; + } + switch (LLVMGetTypeKind(base_type)) { + case LLVMFloatTypeKind: + case LLVMDoubleTypeKind: + break; + case LLVMVectorTypeKind: + // AAPCS32's short vectors are the 64-bit and 128-bit ones. An aggregate of up to + // four of them is a Homogeneous Vector Aggregate, which rides in the VFP registers + // exactly as an HFA does. Any other width is not a short vector and does not qualify. + { + i64 vec_size = lb_sizeof(base_type); + if (vec_size != 8 && vec_size != 16) { + return false; + } + } + break; + default: + return false; + } + if (coerce_) { + *coerce_ = needs_coerce ? llvm_array_type(base_type, member_count) : nullptr; + } + return true; + } + + gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, ProcCallingConvention calling_convention, Type *original_type) { auto args = array_make(lb_function_type_args_allocator(), arg_count); + auto srcs = lb_abi_param_source_types(original_type, arg_count); for (unsigned i = 0; i < arg_count; i++) { LLVMTypeRef t = arg_types[i]; if (is_register(t, false)) { - args[i] = non_struct(c, t, false); + args[i] = non_struct(c, t, false, srcs[i]); } else { i64 sz = lb_sizeof(t); i64 a = lb_alignof(t); + + LLVMTypeRef hfa_coerce = nullptr; // Added to support hard floats included in the playdates cortex-m7. if (calling_convention == ProcCC_CDecl && selected_subtarget == Subtarget_Playdate) { args[i] = lb_arg_type_direct(t); + } else if (is_hfa(c, t, srcs[i], calling_convention, &hfa_coerce)) { + args[i] = lb_arg_type_direct(t, hfa_coerce, nullptr, nullptr); } else if (is_calling_convention_odin(calling_convention) && sz > 8) { // Minor change to improve performance using the Odin calling conventions args[i] = lb_arg_type_indirect(t, nullptr); @@ -1950,22 +2242,36 @@ namespace lbAbiArm32 { return args; } - gb_internal lbArgType compute_return_type(LLVMContextRef c, LLVMTypeRef return_type, bool return_is_defined, ProcCallingConvention calling_convention) { + gb_internal lbArgType compute_return_type(LLVMContextRef c, LLVMTypeRef return_type, bool return_is_defined, ProcCallingConvention calling_convention, Type *return_source) { if (!return_is_defined) { return lb_arg_type_direct(LLVMVoidTypeInContext(c)); + } else if (LLVMGetTypeKind(return_type) == LLVMVectorTypeKind && lb_sizeof(return_type) > 16) { + // A bare vector wider than a short vector has no register file to come back in. It + // is returned through a hidden pointer. `is_register` answers true for every vector, + // without this the caller returns it directly while the C callee stores + // through an `sret` pointer that was never passed (segfault) + LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); + return lb_arg_type_indirect(return_type, attr); } else if (!is_register(return_type, true)) { if (calling_convention == ProcCC_CDecl && selected_subtarget == Subtarget_Playdate) { return lb_arg_type_direct(return_type); } + // An HFA is returned in s0-s3 / d0-d3 too. It must not fall through to the + // integer coercions or to `sret`. + LLVMTypeRef hfa_coerce = nullptr; + if (is_hfa(c, return_type, return_source, calling_convention, &hfa_coerce)) { + return lb_arg_type_direct(return_type, hfa_coerce, nullptr, nullptr); + } + // `lb_arg_type_direct` takes (type, cast_type), and the cast type is what the function actually returns. switch (lb_sizeof(return_type)) { - case 1: return lb_arg_type_direct(LLVMIntTypeInContext(c, 8), return_type, nullptr, nullptr); - case 2: return lb_arg_type_direct(LLVMIntTypeInContext(c, 16), return_type, nullptr, nullptr); - case 3: case 4: return lb_arg_type_direct(LLVMIntTypeInContext(c, 32), return_type, nullptr, nullptr); + case 1: return lb_arg_type_direct(return_type, LLVMIntTypeInContext(c, 8), nullptr, nullptr); + case 2: return lb_arg_type_direct(return_type, LLVMIntTypeInContext(c, 16), nullptr, nullptr); + case 3: case 4: return lb_arg_type_direct(return_type, LLVMIntTypeInContext(c, 32), nullptr, nullptr); } LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); return lb_arg_type_indirect(return_type, attr); } - return non_struct(c, return_type, true); + return non_struct(c, return_type, true, nullptr); } }; @@ -1996,12 +2302,8 @@ namespace lbAbiRiscv64 { } } - gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type) { - LLVMAttributeRef attr = nullptr; - LLVMTypeRef i1 = LLVMInt1TypeInContext(c); - if (type == i1) { - attr = lb_create_enum_attribute(c, "zeroext"); - } + gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, Type *source_type) { + LLVMAttributeRef attr = lb_integer_extension_attribute(c, type, source_type); return lb_arg_type_direct(type, nullptr, nullptr, attr); } @@ -2125,7 +2427,45 @@ namespace lbAbiRiscv64 { return LLVMGetTypeKind(type) == LLVMIntegerTypeKind && lb_sizeof(type) > 0; } - gb_internal lbArgType compute_arg_type(lbModule *m, LLVMTypeRef type, int *gprs_left, int *fprs_left, Type *odin_type) { + // The psABI applies the hardware floating-point convention to a struct's MEMBERS. A union is + // never flattened, so an aggregate holding one ANYWHERE, at any depth, and through an array, + // takes the integer convention instead, whatever the union itself contains. + // + // The lowered type cannot answer this. A `#raw_union{f32}` comes out as a bare `float`, and a + // two-member one comes out as the integer its padding filler is, which is indistinguishable + // from a real integer member. Both have to be read off the source type. + gb_internal bool contains_union(Type *t) { + if (t == nullptr) { + return false; + } + Type *bt = base_type(t); + if (bt == nullptr) { + return false; + } + switch (bt->kind) { + case Type_Union: + return true; + case Type_Struct: + if (bt->Struct.is_raw_union) { + return true; + } + for (Entity *f : bt->Struct.fields) { + if (contains_union(f->type)) { + return true; + } + } + return false; + case Type_Array: + return contains_union(bt->Array.elem); + case Type_EnumeratedArray: + return contains_union(bt->EnumeratedArray.elem); + case Type_Matrix: + return contains_union(bt->Matrix.elem); + } + return false; + } + + gb_internal lbArgType compute_arg_type(lbModule *m, LLVMTypeRef type, int *gprs_left, int *fprs_left, Type *source_type) { LLVMContextRef c = m->ctx; int xlen = 8; // 8 byte int register size for riscv64. @@ -2184,7 +2524,9 @@ namespace lbAbiRiscv64 { fp_size = lb_sizeof(fp_type); } - if (is_float(fp_type) && fp_size <= flen && *fprs_left >= 1) { + bool integer_only = contains_union(source_type); + + if (!integer_only && is_float(fp_type) && fp_size <= flen && *fprs_left >= 1) { *fprs_left -= 1; if (fp_type != orig_type) { // A struct that flattened to a single float has to be coerced to that float; @@ -2194,10 +2536,10 @@ namespace lbAbiRiscv64 { } return lb_arg_type_direct(orig_type, fp_type, nullptr, nullptr); } - return non_struct(c, orig_type); + return non_struct(c, orig_type, source_type); } - if (fp_kind == LLVMStructTypeKind && fp_size <= 2*flen) { + if (!integer_only && fp_kind == LLVMStructTypeKind && fp_size <= 2*flen) { unsigned elem_count = LLVMCountStructElementTypes(fp_type); if (elem_count == 2) { LLVMTypeRef ty1 = LLVMStructGetTypeAtIndex(fp_type, 0); @@ -2240,7 +2582,7 @@ namespace lbAbiRiscv64 { if (size <= xlen) { *gprs_left -= 1; if (is_register(type)) { - return non_struct(c, orig_type); + return non_struct(c, orig_type, source_type); } else { return lb_arg_type_direct(orig_type, LLVMIntTypeInContext(c, cast(unsigned)(size*8)), nullptr, nullptr); } @@ -2259,9 +2601,24 @@ namespace lbAbiRiscv64 { gb_internal Array compute_arg_types(lbModule *m, LLVMTypeRef *arg_types, unsigned arg_count, ProcCallingConvention calling_convention, Type *odin_type, int *gprs, int *fprs) { auto args = array_make(lb_function_type_args_allocator(), arg_count); - for (unsigned i = 0; i < arg_count; i++) { + // The source type of each parameter, where one exists. `arg_types` can carry entries with + // no counterpart, so this walks the tuple the way lbAbiAmd64SysV does and hands back + // nullptr once it runs out. + Entity **params = nullptr; + isize param_count = 0; + if (odin_type != nullptr && odin_type->kind == Type_Proc && odin_type->Proc.params != nullptr) { + params = odin_type->Proc.params->Tuple.variables.data; + param_count = odin_type->Proc.params->Tuple.variables.count; + } + + for (unsigned i = 0, j = 0; i < arg_count; i++, j++) { + while (cast(isize)j < param_count && params[j]->kind != Entity_Variable) { + j++; + } + Type *source_type = cast(isize)j < param_count ? params[j]->type : nullptr; + LLVMTypeRef type = arg_types[i]; - args[i] = compute_arg_type(m, type, gprs, fprs, odin_type); + args[i] = compute_arg_type(m, type, gprs, fprs, source_type); } return args; @@ -2274,10 +2631,21 @@ namespace lbAbiRiscv64 { return lb_arg_type_direct(LLVMVoidTypeInContext(c)); } + // A single result is classified from its source type. The union rule reaches the return + // as well. A tuple keeps nullptr: it is split into out-pointers below. The recursive call + // for the last tuple field lands here with a result count above one, so it takes the same path. + Type *return_source = nullptr; + if (!return_is_tuple && + odin_type != nullptr && odin_type->kind == Type_Proc && + odin_type->Proc.results != nullptr && + odin_type->Proc.results->Tuple.variables.count == 1) { + return_source = odin_type->Proc.results->Tuple.variables[0]->type; + } + // There are two registers for return types. int gprs = 2; int fprs = 2; - lbArgType ret = compute_arg_type(m, return_type, &gprs, &fprs, odin_type); + lbArgType ret = compute_arg_type(m, return_type, &gprs, &fprs, return_source); // Return didn't fit into the return registers, so caller allocates and it is returned via // an out-pointer. diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index 792cb6253..3aa78731f 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -46,6 +46,13 @@ gb_internal String get_default_microarchitecture() { } } else if (build_context.metrics.arch == TargetArch_riscv64) { default_march = str_lit("generic-rv64"); + } else if (build_context.metrics.arch == TargetArch_arm32) { + // The arm32 triple is `gnueabihf`, and the hard-float ABI passes floating point in the + // VFP registers. `generic` has no FPU at all. LLVM cannot honor the ABI its own + // triple asks for and quietly falls back to the soft-float convention. + // + // `arm1176jzf-s` is what clang picks by default for this same triple. + default_march = str_lit("arm1176jzf-s"); } return default_march; diff --git a/src/llvm_backend_expr.cpp b/src/llvm_backend_expr.cpp index c957f3278..abc4cfe47 100644 --- a/src/llvm_backend_expr.cpp +++ b/src/llvm_backend_expr.cpp @@ -1287,7 +1287,9 @@ gb_internal lbValue lb_emit_matrix_mul_vector(lbProcedure *p, lbValue lhs, lbVal LLVMValueRef rhs_ptr = LLVMGetOperand(rhs.value, 0); LLVMTypeRef vector_type = LLVMVectorType(lb_type(p->module, elem), cast(unsigned)vector_count); LLVMValueRef rhs_vector = LLVMBuildLoad2(p->builder, vector_type, rhs_ptr, ""); - LLVMSetAlignment(rhs_vector, cast(unsigned)type_align_of(type)); + // The alignment of what is being loaded, which is the right-hand vector. `type` is the + // result, and asking it cannot be right except by coincidence. + LLVMSetAlignment(rhs_vector, cast(unsigned)type_align_of(vt)); for (unsigned i = 0; i < column_count; i++) { LLVMValueRef mask = llvm_mask_same(p->module, i, row_count); diff --git a/src/llvm_backend_general.cpp b/src/llvm_backend_general.cpp index e4bd916f1..f798dd34d 100644 --- a/src/llvm_backend_general.cpp +++ b/src/llvm_backend_general.cpp @@ -2671,7 +2671,17 @@ gb_internal LLVMTypeRef lb_type_internal(lbModule *m, Type *type) { // so check the alignment of all fields to see if packing is required. requires_packing = requires_packing || ((offset % type_align_of(field_type)) != 0); - array_add(&fields, lb_type(m, field_type)); + LLVMTypeRef field_llvm_type = lb_type(m, field_type); + + // `max_simd_align` can cap a member below what LLVM gives the lowered + // type. Unpacked, LLVM lays the struct out by its own alignment and the + // member moves: `struct{i8, #simd[8]f32}` is 48 bytes here and 64 to + // LLVM on every target that caps the vector at 16. + i64 natural_align = lb_llvm_natural_alignof(field_llvm_type); + requires_packing = requires_packing || ((offset % natural_align) != 0) || + natural_align > full_type_align; + + array_add(&fields, field_llvm_type); prev_offset = offset + type_size_of(field->type); } diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index 1fe11a5c2..b91a9dda0 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -1092,6 +1092,11 @@ gb_internal lbValue lb_emit_call_internal(lbProcedure *p, lbValue value, lbValue if (attribute != nullptr) { LLVMAddCallSiteAttribute(ret, param_offset, attribute); } + // `byval`'s alignment decides the outgoing stack slot, and LLVM reads it + // from the CALL, not the declaration + if (ft->args[i].align_attribute != nullptr) { + LLVMAddCallSiteAttribute(ret, param_offset, ft->args[i].align_attribute); + } param_offset += 1; } diff --git a/src/types.cpp b/src/types.cpp index e9ac4260c..090179c68 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1607,16 +1607,8 @@ gb_internal i64 matrix_align_of(Type *t, struct TypePath *tp) { // could be maximally aligned but as a compromise, having no padding will be // beneficial to third libraries that assume no padding - i64 total_expected_size = row_count*column_count*elem_size; - // i64 min_alignment = prev_pow2(elem_align * row_count); - i64 min_alignment = prev_pow2(total_expected_size); - while (total_expected_size != 0 && (total_expected_size % min_alignment) != 0) { - min_alignment >>= 1; - } - min_alignment = gb_max(min_alignment, elem_align); - - i64 align = gb_min(min_alignment, build_context.max_simd_align); - return align; + gb_unused(row_count); gb_unused(column_count); gb_unused(elem_size); + return gb_clamp(elem_align, 1, build_context.max_simd_align); } @@ -4363,6 +4355,18 @@ gb_internal i64 type_align_of(Type *t) { } +// The largest alignment the target permits. The i386 System V psABI caps every scalar at 4, unlike +// Windows. Anything that derives its alignment from a COMPONENT rather than from its own size has +// to be capped here too. +gb_internal i64 type_target_max_align(void) { + i64 max_align = build_context.max_align; + if (build_context.metrics.arch == TargetArch_i386 && + build_context.metrics.os != TargetOs_windows) { + max_align = gb_min(max_align, 4); + } + return max_align; +} + gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { GB_ASSERT(path != nullptr); if (t->failure) { @@ -4387,10 +4391,11 @@ gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { case Basic_uintptr: case Basic_rawptr: return build_context.ptr_size; + // A complex aligns to one component and a quaternion to one of its four. case Basic_complex32: case Basic_complex64: case Basic_complex128: - return type_size_of_internal(t, path) / 2; + return gb_min(type_size_of_internal(t, path) / 2, type_target_max_align()); case Basic_quaternion64: case Basic_quaternion128: case Basic_quaternion256: - return type_size_of_internal(t, path) / 4; + return gb_min(type_size_of_internal(t, path) / 4, type_target_max_align()); } } break; @@ -4529,7 +4534,7 @@ gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { case Type_SimdVector: { // IMPORTANT TODO(bill): Figure out the alignment of vector types - return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, build_context.max_simd_align*2); + return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, build_context.max_simd_align); } case Type_Matrix: @@ -4541,7 +4546,7 @@ gb_internal i64 type_align_of_internal(Type *t, TypePath *path) { // NOTE(bill): Things that are bigger than build_context.ptr_size, are actually comprised of smaller types // TODO(bill): Is this correct for 128-bit types (integers)? - return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, build_context.max_align); + return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, type_target_max_align()); } gb_internal i64 *type_set_offsets_of(Slice const &fields, bool is_packed, bool is_raw_union, i64 min_field_align, i64 max_field_align) { diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh new file mode 100755 index 000000000..64ad01526 --- /dev/null +++ b/tests/abi/cross.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -eu + +# LOCAL ONLY: not wired into CI. +# +# This covers what is not in CI: the targets with no CI job and no cross libc, +# i386 and arm32, for checking manually. +# +# `abi_main.odin` is the corpus without core:testing, exiting with the number of +# failing types, so this needs no threads, no libc and no cross sysroot, only +# 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 -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, so it is the +# baseline to compare against. BOTH SIDES must agree on it: the C side follows +# clang's default unless ABI_CFLAGS says otherwise, so `-microarch:haswell` +# alone makes the two disagree about where a 32-byte vector lives and reports +# phantom vector failures. Match them (`ABI_CFLAGS=-march=haswell`) or use +# pentium4 on both. + +TARGET=${1:?odin target, e.g. linux_arm64} +TRIPLE=${2:?clang triple, e.g. aarch64-linux-gnu} +QEMU=${3:?qemu binary, e.g. qemu-aarch64} +shift 3 # anything else goes to `odin build` +: "${ODIN:=../../odin}" +: "${CLANG:=clang}" +# The C side's optimisation level. An ABI is a link-time contract, so the two +# sides are built independently and either may be optimised: `ABI_CFLAGS=-O2`. +: "${ABI_CFLAGS:=}" + +case "$TARGET" in +*i386*) START='.text + .globl _start +_start: + call probe_main + movl %eax, %ebx + movl $1, %eax + int $0x80' ;; +*arm64*) START='.text + .globl _start +_start: + bl probe_main + mov x8, #93 + svc #0' ;; +*arm32*) START='.text + .globl _start +_start: + bl probe_main + mov r7, #1 + svc #0 + +@ The runtime does 64-bit division. On Arm, the compiler emits `__aeabi_uldivmod` +@ instead of `__udivdi3`. It returns the quotient in r0:r1 and the remainder in +@ r2:r3, which C cannot express. It is written here and forwards to the shim. + .globl __aeabi_uldivmod +__aeabi_uldivmod: + push {lr} + sub sp, sp, #12 + add r12, sp, #4 + str r12, [sp] + bl shim_udivmod + ldr r2, [sp, #4] + ldr r3, [sp, #8] + add sp, sp, #12 + pop {pc}' ;; +*riscv64*) START='.text + .globl _start +_start: + call probe_main + mv a0, a0 + li a7, 93 + ecall' ;; +*) echo "no start stub for $TARGET" >&2; exit 2 ;; +esac + + +# cleaned BEFORE, not after -- the driver is left in place to inspect +rm -rf build-cross +mkdir -p build-cross/p +$ODIN run gen.odin -file -- build-cross + +# Ask the C compiler which tiers it has, by preprocessing the generated `build-cross/tiers.c`. +# The Odin side must use the same tiers or it references symbols C never emitted. +have() { $CLANG --target="$TRIPLE" -E build-cross/tiers.c 2>/dev/null | grep -q "ABI_YES_$1" && echo true || echo false; } +TIERS="-define:ABI_TIER_GNU=$(have GNU) -define:ABI_TIER_F16=$(have F16) -define:ABI_TIER_I128=$(have I128)" +mv build-cross/abi_main.odin build-cross/p/ +printf '%s\n' "$START" > build-cross/start.s + +# A freestanding shim: the runtime reaches for a few libc symbols even with +# -no-crt, and 64-bit division on a 32-bit target is a compiler-rt call. +cat > build-cross/shim.c <<'EOF' +typedef unsigned long usz; +static char heap[1<<20]; +static usz hoff; +void *malloc(usz n){ usz a=(hoff+15)&~(usz)15; if(a+n>sizeof heap) return 0; hoff=a+n; return heap+a; } +void free(void *p){ (void)p; } +void *calloc(usz n, usz m){ char*p=malloc(n*m); if(p) for(usz i=0;i0;i--)a[i-1]=b[i-1];} return d; } +void *memset(void *d, int c, usz n){ char*a=d; for(usz i=0;i=0;i--){ r=(r<<1)|((a>>i)&1); if(r>=b){ r-=b; q|=(u64)1<&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 + # the driver returns the INDEX of the first disagreement, and the generator + # emits the index -> name map, so the failure names a type rather than a count + name=$(grep -m1 "^// $rc " build-cross/p/abi_main.odin | cut -f2) + echo "$TARGET: DISAGREES with clang, first at type '${name:-#$rc}'" >&2 + echo " re-run with -define:ABI_SKIP=$rc to find the next one" >&2 +fi +exit $rc diff --git a/tests/abi/gen.odin b/tests/abi/gen.odin new file mode 100644 index 000000000..a793faf30 --- /dev/null +++ b/tests/abi/gen.odin @@ -0,0 +1,1502 @@ +// Generates abi_corpus.odin, abi_corpus.c, abi_main.odin and tiers.c from one +// description. +// +// The runners regenerate into their build directory +// +// The corpus encodes NO ABI. Every check is "Odin and the platform C compiler +// agree", so one corpus is valid on every target without knowing whether it is +// SysV, AAPCS64 or Win64. +// +// odin run gen.odin -file -- +package abi_gen + +import "core:fmt" +import "core:os" +import "core:strconv" +import "core:strings" + +// ---------------------------------------------------------------- scalars + +Scalar :: struct { + odin: string, + c: string, + is_float: bool, +} + +scalar :: proc(tag: string) -> Scalar { + switch tag { + case "i8": return {"i8", "int8_t", false} + case "i16": return {"i16", "int16_t", false} + case "i32": return {"i32", "int32_t", false} + case "i64": return {"i64", "int64_t", false} + case "u8": return {"u8", "uint8_t", false} + case "u16": return {"u16", "uint16_t", false} + case "u32": return {"u32", "uint32_t", false} + case "u64": return {"u64", "uint64_t", false} + case "bool": return {"bool", "_Bool", false} + case "f16": return {"f16", "_Float16", true} + case "i128": return {"i128", "__int128", false} + case "enum": return {"E32", "enum E32", false} + case "c64": return {"complex64", "float _Complex", false} + case "c128": return {"complex128", "double _Complex", false} + case "bset": return {"BS", "unsigned", false} + case "f32": return {"f32", "float", true} + case "f64": return {"f64", "double", true} + case "ptr": return {"rawptr", "void *", false} + case "cstring": return {"cstring", "char *", false} + } + fmt.panicf("unknown scalar tag %q", tag) +} + +// Tiers keep a target that lacks an extension from losing the whole corpus. +TIER_CORE :: "core" +TIER_GNU :: "gnu" // zero-length arrays, empty structs __GNUC__ +TIER_F16 :: "f16" // _Float16 +TIER_I128 :: "i128" // __int128, 64-bit targets only + +// A scalar can carry a tier, and any type built from it inherits it: `_Float16` +// is not available everywhere, and a family is only as portable as its members. +scalar_tier :: proc(tag: string) -> (string, bool) { + switch tag { + case "f16": return TIER_F16, true + case "i128": return TIER_I128, true + } + return TIER_CORE, false +} + +tier_of :: proc(tags: ..string) -> string { + for t in tags { + if tier, ok := scalar_tier(t); ok { + return tier + } + } + return TIER_CORE +} + +// A member spelled differently in the two languages -- matrix indexing, or a +// bare vector, where there is no common accessor. +Leaf :: struct { + odin_path: string, + c_path: string, + tag: string, + val: string, +} + +Ty :: struct { + name: string, + odin: string, + c: string, + fields: []Leaf, + tier: string, + // Escape hatch for members with no lvalue path on the Odin side. A #simd + // lane is read with `simd.extract` and written only as a whole vector, so + // the C side still checks every lane while Odin uses these. + // odin_set: statements, `{}` is the variable. odin_get: (expr, expected). + odin_set: []string, + odin_get: [][2]string, +} + +types: [dynamic]Ty + +add :: proc( + name, odin, c: string, + fields: []Leaf, + tier: string = TIER_CORE, + odin_set: []string = nil, + odin_get: [][2]string = nil, +) { + append(&types, Ty{name, odin, c, fields, tier, odin_set, odin_get}) +} + +// A distinct value per field position, so a shifted read is detectable. +val :: proc(i: int, tag: string) -> string { + if scalar(tag).is_float { + return tp("%d.5", i * 7 + 3) + } + return tp("%d", i * 7 + 3) +} + +c_val :: proc(tag, v: string) -> string { + switch tag { + case "ptr": return tp("(void *)(intptr_t)(%s)", v) + case "cstring": return tp("(char *)(intptr_t)(%s)", v) + case "bool": return "1" + case "enum": return tp("(enum E32)(%s)", v) + case "c64": return tp("(%s.0f + %d.0if)", v, as_int(v) + 1) + case "c128": return tp("(%s.0 + %d.0i)", v, as_int(v) + 1) + case "bset": return tp("(%du)", (1 << u32(as_int(v) % 31)) | 1) + } + return v +} + +odin_val :: proc(tag, v: string) -> string { + switch tag { + case "ptr": return tp("rawptr(uintptr(%s))", v) + case "bool": return "true" + case "enum": return tp("E32(%s)", v) + case "c64": return tp("complex64(complex(%s, %d))", v, as_int(v) + 1) + case "c128": return tp("complex128(complex(%s, %d))", v, as_int(v) + 1) + case "bset": return tp("(BS{0, %d})", as_int(v) % 31) + } + return v +} + +// A value the checks MUST reject, for the mutation control. +mutated :: proc(tag, v: string) -> string { + switch tag { + case "bool": return "false" + case "ptr": return "rawptr(uintptr(999))" + case "enum": return tp("E32(%d)", as_int(v) + 1) + case "c64": return tp("complex64(complex(%d, %d))", as_int(v) + 2, as_int(v) + 1) + case "c128": return tp("complex128(complex(%d, %d))", as_int(v) + 2, as_int(v) + 1) + case "bset": return "(BS{2})" + } + // every generated float value ends in `.5`, so adding one keeps the form + if dot := strings.index_byte(v, '.'); dot >= 0 { + return tp("%d%s", as_int(v[:dot]) + 1, v[dot:]) + } + return tp("%d", as_int(v) + 1) +} + +// core:fmt reads `{` as the start of a format verb and the corpus is mostly +// braces, so they are escaped here rather than at every call site. Every format +// string below can then be written exactly as it should come out. +tp :: proc(format: string, args: ..any) -> string { + return fmt.tprintf(brace_escape(format), ..args) +} + +w :: proc(sb: ^strings.Builder, format: string, args: ..any) { + fmt.sbprintf(sb, brace_escape(format), ..args) +} + +brace_escape :: proc(format: string) -> string { + if !strings.contains_any(format, "{}") { + return format + } + out, _ := strings.replace_all(format, "{", "{{", context.temp_allocator) + out, _ = strings.replace_all(out, "}", "}}", context.temp_allocator) + return out +} + +as_int :: proc(s: string) -> int { + n, ok := strconv.parse_int(s) + fmt.assertf(ok, "not an integer: %q", s) + return n +} + +leaf :: proc(path, tag: string, i: int) -> Leaf { + return {path, path, tag, val(i, tag)} +} + +leaf2 :: proc(odin_path, c_path, tag, v: string) -> Leaf { + return {odin_path, c_path, tag, v} +} + +// A C member reference. `{}` lets a member be an EXPRESSION rather than a path, +// which is what `__real__ x` needs -- it is a prefix operator. +c_ref :: proc(cp, v: string) -> string { + if strings.contains(cp, "{}") { + return sub(cp, v) + } + return tp("%s.%s", v, cp) +} + +// `{}` -> the variable name. +sub :: proc(s, v: string) -> string { + out, _ := strings.replace_all(s, "{}", v, context.temp_allocator) + return out +} + +c_conds :: proc(t: Ty, v: string) -> string { + if len(t.fields) == 0 { + return "1" + } + parts := make([]string, len(t.fields), context.temp_allocator) + for f, i in t.fields { + parts[i] = tp("%s == (%s)", c_ref(f.c_path, v), c_val(f.tag, f.val)) + } + return strings.join(parts, " && ", context.temp_allocator) +} + +odin_setters :: proc(t: Ty, v: string) -> []string { + if len(t.odin_set) > 0 { + out := make([]string, len(t.odin_set), context.temp_allocator) + for s, i in t.odin_set { + out[i] = sub(s, v) + } + return out + } + out := make([]string, len(t.fields), context.temp_allocator) + for f, i in t.fields { + out[i] = tp("%s.%s = %s", v, f.odin_path, odin_val(f.tag, f.val)) + } + return out +} + +odin_getters :: proc(t: Ty, v: string) -> [][2]string { + if len(t.odin_get) > 0 { + out := make([][2]string, len(t.odin_get), context.temp_allocator) + for g, i in t.odin_get { + out[i] = {sub(g[0], v), g[1]} + } + return out + } + out := make([][2]string, len(t.fields), context.temp_allocator) + for f, i in t.fields { + expected: string + switch f.tag { + case "ptr", "bool", "enum", "c64", "c128", "bset": + expected = odin_val(f.tag, f.val) + case: + expected = tp("%s(%s)", scalar(f.tag).odin, f.val) + } + out[i] = {tp("%s.%s", v, f.odin_path), expected} + } + return out +} + +// ---------------------------------------------------------------- corpus + +build :: proc() { + // --- scalar arity 1..4, the merge and by-value/memory boundaries + combos := [][]string{ + {"i32"}, {"i64"}, {"f32"}, {"f64"}, {"i8"}, {"ptr"}, + {"i32", "i32"}, {"f32", "f32"}, {"f64", "f64"}, {"i64", "f64"}, + {"f64", "i64"}, {"i32", "f32"}, {"f32", "i32"}, {"i8", "i64"}, + {"f32", "f32", "f32"}, {"i32", "i32", "i32"}, {"f64", "f64", "f64"}, + {"i64", "i64", "i64"}, {"f32", "i32", "f32"}, {"i8", "f64", "i8"}, + {"f32", "f32", "f32", "f32"}, {"f64", "f64", "f64", "f64"}, + {"i32", "i32", "i32", "i32"}, {"i64", "i64", "i64", "i64"}, + {"f32", "f32", "f32", "i32"}, + // half, at each arity and mixed: the merge rules turn on the WIDTH of a + // float member, not just on its being one + {"f16"}, {"f16", "f16"}, {"f16", "i16"}, {"f16", "f32"}, + {"f16", "f16", "f16"}, {"f16", "f16", "f16", "f16"}, + {"f32", "f16"}, {"f64", "f16"}, + // an enum is only under test if it is explicitly backed: Odin's default + // is `int`, which is register-sized against C's 4 + {"enum"}, {"enum", "enum"}, {"enum", "f32"}, {"i8", "enum"}, + // the only scalar that spans two eightbytes, and the one that reaches + // AAPCS64's even-register-pair rule + {"i128"}, {"i128", "i64"}, {"i8", "i128"}, {"i128", "f64"}, + {"c64"}, {"c128"}, {"c64", "c64"}, {"c64", "f32"}, {"c128", "i64"}, + {"bset"}, {"bset", "bset"}, {"bset", "f32"}, + // the unsigned widths: same size and class as their signed twins, so they are the + // control on anything that reads signedness where it should not + {"u8"}, {"u16"}, {"u32"}, {"u64"}, + {"u8", "u32"}, {"u16", "u64"}, {"u32", "f32"}, {"u64", "f64"}, + // a narrow member in FRONT of a complex: the complex aligns to its component, so a wrong + // component alignment moves it and changes the struct's size. Nothing else here reaches that. + {"i8", "c64"}, {"i8", "c128"}, {"i16", "c128"}, + } + for tags in combos { + odin_members := make([]string, len(tags), context.temp_allocator) + c_members := make([]string, len(tags), context.temp_allocator) + fields := make([]Leaf, len(tags)) + for tag, i in tags { + odin_members[i] = tp("f%d: %s", i, scalar(tag).odin) + c_members[i] = tp("%s f%d;", scalar(tag).c, i) + fields[i] = leaf(tp("f%d", i), tag, i) + } + add( + tp("s_%s", strings.join(tags, "_", context.temp_allocator)), + tp("struct { %s }", strings.join(odin_members, ", ", context.temp_allocator)), + tp("struct { %s }", strings.join(c_members, " ", context.temp_allocator)), + fields, + tier = tier_of(..tags), + ) + } + + // --- BARE scalars. Every scalar above is a struct MEMBER, and a member never + // carries a parameter extension attribute: `signext`/`zeroext` exist only on a + // scalar passed in its own right, and say the CALLER has already widened it to + // 32 bits. A callee compiled to rely on that reads the untouched high bits. + // The sub-32-bit widths are the ones that have it; i32 and f32 are the controls + // that must not. + for tag in ([]string{ + "i8", "u8", "i16", "u16", "bool", "i32", "u32", "u64", "f32", + // the kinds whose BARE form asks a different question from their wrapped one: a complex is + // two floats to the type system and a rule of its own to a psABI, and a bare `f16` is the + // narrowest float there is + "f16", "c64", "c128", "ptr", "enum", "bset", + }) { + s := scalar(tag) + v := val(0, tag) + expected: string + switch tag { + case "ptr", "bool", "enum", "c64", "c128", "bset": + expected = odin_val(tag, v) + case: + expected = tp("%s(%s)", s.odin, v) + } + add( + tp("bs_%s", tag), + s.odin, + s.c, + leaves(leaf2("", "{}", tag, v)), + tier = tier_of(tag), + odin_set = strs(tp("{} = %s", odin_val(tag, v))), + odin_get = pairs([2]string{"{}", expected}), + ) + } + + // --- cstring, the shape every C binding is made of. It is a pointer, but `==` on a cstring + // has STRING semantics in Odin and would dereference the fabricated address, so the Odin side + // compares the pointer VALUE while the C side compares the pointer directly. + { + v := val(0, "cstring") + add( + "bs_cstring", "cstring", "char *", + leaves(leaf2("", "{}", "cstring", v)), + odin_set = strs(tp("{} = transmute(cstring)uintptr(%s)", v)), + odin_get = pairs([2]string{"transmute(uintptr)({})", tp("uintptr(%s)", v)}), + ) + add( + "s_cstring", "struct { a: cstring }", "struct { char *a; }", + leaves(leaf2("a", "a", "cstring", v)), + odin_set = strs(tp("{}.a = transmute(cstring)uintptr(%s)", v)), + odin_get = pairs([2]string{"transmute(uintptr)({}.a)", tp("uintptr(%s)", v)}), + ) + } + + // --- quaternions. There is no C quaternion, but Odin's lowers to four floats in memory: + // `[x, y, z, w]`. The counterpart is the struct a C binding would actually declare, + // and the question this asks is exactly the one interop depends on: does a quaternion + // travel the way the equivalent four-float struct does? + // + // The accessors differ on the two sides (`imag/jmag/kmag/real` against `.x/.y/.z/.w`), which is + // what `c_path`'s `{}` form and the `odin_get` hatch are for. + { + Quat :: struct { + name: string, + odin: string, + c_elem: string, + tag: string, + tier: string, + } + quats := []Quat{ + {"q128", "quaternion128", "float", "f32", TIER_CORE}, + {"q256", "quaternion256", "double", "f64", TIER_CORE}, + {"q64", "quaternion64", "_Float16", "f16", TIER_F16}, + } + lanes := [4]string{"x", "y", "z", "w"} + // memory order is x,y,z,w; the accessors for those are imag,jmag,kmag,real + accessors := [4]string{"imag", "jmag", "kmag", "real"} + for q in quats { + fields := make([]Leaf, 4) + getters := make([][2]string, 4) + for i in 0 ..< 4 { + v := val(i, q.tag) + fields[i] = leaf2("", tp("{}.%s", lanes[i]), q.tag, v) + getters[i] = { + tp("%s({})", accessors[i]), + tp("%s(%s)", scalar(q.tag).odin, v), + } + } + // the same aggregate with a narrow member in front, which is what catches a + // wrong component alignment: it moves the quaternion and resizes the struct + off_fields := make([]Leaf, 5) + off_getters := make([][2]string, 5) + off_fields[0] = leaf("a", "i8", 0) + off_getters[0] = {"{}.a", tp("i8(%s)", val(0, "i8"))} + for i in 0 ..< 4 { + v := val(i, q.tag) + off_fields[i + 1] = leaf2("", tp("{}.q.%s", lanes[i]), q.tag, v) + off_getters[i + 1] = { + tp("%s({}.q)", accessors[i]), + tp("%s(%s)", scalar(q.tag).odin, v), + } + } + add( + tp("off_%s", q.name), + tp("struct { a: i8, q: %s }", q.odin), + tp("struct { int8_t a; struct { %s x, y, z, w; } q; }", q.c_elem), + off_fields, + tier = q.tier, + odin_set = strs( + tp("{}.a = %s", val(0, "i8")), + tp("{}.q = quaternion(x=%s, y=%s, z=%s, w=%s)", + val(0, q.tag), val(1, q.tag), val(2, q.tag), val(3, q.tag)), + ), + odin_get = off_getters, + ) + add( + tp("bs_%s", q.name), + q.odin, + tp("struct { %s x, y, z, w; }", q.c_elem), + fields, + tier = q.tier, + odin_set = strs(tp("{} = quaternion(x=%s, y=%s, z=%s, w=%s)", + val(0, q.tag), val(1, q.tag), val(2, q.tag), val(3, q.tag))), + odin_get = getters, + ) + } + } + + // --- arrays: the same eightbytes from one declaration + for tag in ([]string{"f32", "f64", "i32", "i64", "i8", "f16", "enum", "i128"}) { + for cnt in 1 ..= 5 { + fields := make([]Leaf, cnt) + for i in 0 ..< cnt { + fields[i] = leaf(tp("a[%d]", i), tag, i) + } + add( + tp("a%d_%s", cnt, tag), + tp("struct { a: [%d]%s }", cnt, scalar(tag).odin), + tp("struct { %s a[%d]; }", scalar(tag).c, cnt), + fields, + tier = tier_of(tag), + ) + } + } + + // --- nesting: same leaves reached through another level + nests := [][2]string{ + {"f32", "f32"}, {"f64", "f64"}, {"i32", "f32"}, {"f32", "i64"}, + {"f16", "f16"}, {"f16", "i32"}, + // a lone f32 in eightbyte 0 reached through a level, then an f64: the + // shape #7292 was about + {"f32", "f64"}, + } + for pair in nests { + a, b := pair[0], pair[1] + add( + tp("n_%s_%s", a, b), + tp("struct { i: struct { x: %s, y: %s } }", scalar(a).odin, scalar(b).odin), + tp("struct { struct { %s x; %s y; } i; }", scalar(a).c, scalar(b).c), + leaves(leaf("i.x", a, 0), leaf("i.y", b, 1)), + tier = tier_of(a, b), + ) + add( + tp("n2_%s_%s", a, b), + tp("struct { i: struct { x: %s }, y: %s }", scalar(a).odin, scalar(b).odin), + tp("struct { struct { %s x; } i; %s y; }", scalar(a).c, scalar(b).c), + leaves(leaf("i.x", a, 0), leaf("y", b, 1)), + tier = tier_of(a, b), + ) + } + + // --- unions, and a union below the top level + unions := [][2]string{ + {"f32", "i32"}, {"f64", "i64"}, {"f32", "f32"}, {"f64", "f32"}, + {"f16", "i16"}, {"f16", "f32"}, + } + for pair in unions { + a, b := pair[0], pair[1] + add( + tp("u_%s_%s", a, b), + tp("struct #raw_union { x: %s, y: %s }", scalar(a).odin, scalar(b).odin), + tp("union { %s x; %s y; }", scalar(a).c, scalar(b).c), + leaves(leaf("x", a, 0)), + tier = tier_of(a, b), + ) + add( + tp("su_%s_%s", a, b), + tp("struct { u: struct #raw_union { x: %s }, y: %s }", scalar(a).odin, scalar(b).odin), + tp("struct { union { %s x; } u; %s y; }", scalar(a).c, scalar(b).c), + leaves(leaf("u.x", a, 0), leaf("y", b, 1)), + tier = tier_of(a, b), + ) + // TWO members in the nested union. The one-member form above is the case + // overlap alone cannot detect; this is its control, and it is the shape + // `test_issue_sysv_abi` pins. + add( + tp("su2_%s_%s", a, b), + tp("struct { u: struct #raw_union { x, y: %s }, z: %s }", scalar(a).odin, scalar(b).odin), + tp("struct { union { %s x, y; } u; %s z; }", scalar(a).c, scalar(b).c), + leaves(leaf("u.x", a, 0), leaf("z", b, 1)), + tier = tier_of(a, b), + ) + } + + // --- homogeneous float aggregates and the shapes that disqualify them + for tag in ([]string{"f32", "f64", "f16"}) { + w, cw := scalar(tag).odin, scalar(tag).c + abcd := leaves(leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)) + add( + tp("hfa4_%s", tag), + tp("struct { a, b, c, d: %s }", w), + tp("struct { %s a, b, c, d; }", cw), + abcd, + tier = tier_of(tag), + ) + add( + tp("hfa5_%s", tag), + tp("struct { a, b, c, d, e: %s }", w), + tp("struct { %s a, b, c, d, e; }", cw), + leaves( + leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), + leaf("d", tag, 3), leaf("e", tag, 4), + ), + tier = tier_of(tag), + ) + // zero-length array member -- disqualifies the HFA + add( + tp("zla_%s", tag), + tp("struct { z: [0]f32, a, b, c, d: %s }", w), + tp("struct { float z[0]; %s a, b, c, d; }", cw), + abcd, + tier = TIER_GNU, + ) + add( + tp("zlat_%s", tag), + tp("struct { a, b, c, d: %s, z: [0]f32 }", w), + tp("struct { %s a, b, c, d; float z[0]; }", cw), + abcd, + tier = TIER_GNU, + ) + // empty struct member -- does NOT disqualify it + add( + tp("esm_%s", tag), + tp("struct { e: struct {}, a, b, c, d: %s }", w), + tp("struct { struct {} e; %s a, b, c, d; }", cw), + abcd, + tier = TIER_GNU, + ) + } + + // --- alignment: changes size and placement without changing any field type + for al in ([]int{2, 4, 8, 16, 32, 64}) { + // a small struct whose ALIGNMENT is the only thing that varies: same + // fields, same field offsets, different slot + add( + tp("aln%d", al), + tp("struct #align(%d) { a: i8, b: i32 }", al), + tp("struct __attribute__((aligned(%d))) { int8_t a; int32_t b; }", al), + leaves(leaf("a", "i8", 0), leaf("b", "i32", 1)), + tier = TIER_GNU, + ) + } + for al in ([]int{16, 32}) { + add( + tp("al%d", al), + tp("struct #align(%d) { a, b, c: f64 }", al), + tp("struct __attribute__((aligned(%d))) { double a, b, c; }", al), + leaves(leaf("a", "f64", 0), leaf("b", "f64", 1), leaf("c", "f64", 2)), + tier = TIER_GNU, + ) + } + // an over-aligned member in TRAILING position, which adds interior padding + // before it rather than after + add( + "oamt", + "struct #min_field_align(16) { a: f32, b: i8 }", + "struct { float a; int8_t b __attribute__((aligned(16))); }", + leaves(leaf("a", "f32", 0), leaf("b", "i8", 1)), + tier = TIER_GNU, + ) + add( + "pk", + "struct #packed { a: i8, b: i32, c: i64 }", + "struct __attribute__((packed)) { int8_t a; int32_t b; int64_t c; }", + leaves(leaf("a", "i8", 0), leaf("b", "i32", 1), leaf("c", "i64", 2)), + tier = TIER_GNU, + ) + // `#max_field_align` CAPS a member's alignment where `#packed` removes it + // entirely, so the struct keeps interior padding but less of it, and its size + // is not a multiple of the widest member. C spells it `#pragma pack(n)`. + // `#pragma pack(n)` has no expression form, so the C side caps each member + // with `packed, aligned(n)`, which is the same rule applied per member. + for al in ([]int{2, 4}) { + add( + tp("mfa%d", al), + tp("struct #max_field_align(%d) { a: i8, b: i32, c: i64 }", al), + tp("struct { int8_t a; int32_t b __attribute__((packed, aligned(%d)));" + + " int64_t c __attribute__((packed, aligned(%d))); }", al, al), + leaves(leaf("a", "i8", 0), leaf("b", "i32", 1), leaf("c", "i64", 2)), + tier = TIER_GNU, + ) + } + + // --- explicit padding, the shape that started this file + add( + "pad_i64_f32", + "struct { a: i64, b: f32 }", + "struct { int64_t a; float b; }", + leaves(leaf("a", "i64", 0), leaf("b", "f32", 1)), + ) + add( + "pad_f32_f64", + "struct { a: f32, b: f64 }", + "struct { float a; double b; }", + leaves(leaf("a", "f32", 0), leaf("b", "f64", 1)), + ) + + // --- #simd vectors. Three ABIs disagree completely: x86-64 puts a 16-byte + // one in a single xmm (SSE then SSEUP), AAPCS64 gives it a Q register and + // lets several form a homogeneous VECTOR aggregate, Win64 passes every + // vector by reference, and i386 has a separate xmm argument file. + Vec :: struct { + tag: string, + lanes: int, + } + for v in ([]Vec{{"f32", 4}, {"f32", 2}, {"f64", 2}, {"i32", 4}, {"i8", 16}}) { + ct, cc := scalar(v.tag).odin, scalar(v.tag).c + fields := make([]Leaf, v.lanes) + getters := make([][2]string, v.lanes) + for i in 0 ..< v.lanes { + fields[i] = leaf(tp("v[%d]", i), v.tag, i) + getters[i] = { + tp("simd.extract({}.v, %d)", i), + tp("%s(%s)", ct, val(i, v.tag)), + } + } + add( + tp("v%d_%s", v.lanes, v.tag), + tp("struct { v: #simd[%d]%s }", v.lanes, ct), + tp("struct { %s v __attribute__((vector_size(%d * sizeof(%s)))); }", cc, v.lanes, cc), + fields, + tier = TIER_GNU, + odin_set = strs(tp("{}.v = {%s}", vals(0, v.lanes, v.tag))), + odin_get = getters, + ) + } + // two vectors: an HVA on AAPCS64, memory on x86-64 + { + fields := make([]Leaf, 8) + getters := make([][2]string, 8) + for i in 0 ..< 4 { + fields[i] = leaf(tp("a[%d]", i), "f32", i) + fields[i + 4] = leaf(tp("b[%d]", i), "f32", i + 4) + getters[i] = {tp("simd.extract({}.a, %d)", i), tp("f32(%s)", val(i, "f32"))} + getters[i + 4] = {tp("simd.extract({}.b, %d)", i), tp("f32(%s)", val(i + 4, "f32"))} + } + add( + "v4f32x2", + "struct { a, b: #simd[4]f32 }", + "struct { float a __attribute__((vector_size(16))), b __attribute__((vector_size(16))); }", + fields, + tier = TIER_GNU, + odin_set = strs( + tp("{}.a = {%s}", vals(0, 4, "f32")), + tp("{}.b = {%s}", vals(4, 8, "f32")), + ), + odin_get = getters, + ) + } + // a vector beside a scalar: homogeneous no longer + { + fields := make([]Leaf, 5) + getters := make([][2]string, 5) + for i in 0 ..< 4 { + fields[i] = leaf(tp("a[%d]", i), "f32", i) + getters[i] = {tp("simd.extract({}.a, %d)", i), tp("f32(%s)", val(i, "f32"))} + } + fields[4] = leaf("b", "i64", 4) + getters[4] = {"{}.b", tp("i64(%s)", val(4, "i64"))} + add( + "v4f32_i64", + "struct { a: #simd[4]f32, b: i64 }", + "struct { float a __attribute__((vector_size(16))); int64_t b; }", + fields, + tier = TIER_GNU, + odin_set = strs( + tp("{}.a = {%s}", vals(0, 4, "f32")), + tp("{}.b = %s", val(4, "i64")), + ), + odin_get = getters, + ) + } + + // --- BARE vectors, and 4-byte widths. + // + // Every vector row above wraps the vector in a struct, and the two are not + // the same question: `struct{v8f}` returns correctly where a bare + // `#simd[8]f32` does not. 4-byte widths were absent entirely. Measured + // against clang on x86-64, the divergence is purely SIZE-driven and + // independent of the element: 4-byte and >=32-byte diverge, 8- and 16-byte + // agree. Every 8-byte element type is here for that reason -- LLVM rounds a + // bare vector's stack slot up to the legal vector width whatever it holds, + // so one 8-byte row would only have caught the defect for its own element. + Bare :: struct { + tag: string, + lanes: int, + cname: string, + tier: string, + // the same width WRAPPED, so the pair is directly comparable + wrap: bool, + } + bares := []Bare{ + {"i8", 4, "rx_i8x4", TIER_GNU, true}, + {"i8", 8, "rx_i8x8", TIER_GNU, true}, + {"i16", 2, "rx_i16x2", TIER_GNU, true}, + {"i16", 4, "rx_i16x4", TIER_GNU, false}, + {"i32", 2, "rx_i32x2", TIER_GNU, false}, + {"f32", 2, "rx_f32x2", TIER_GNU, false}, + {"i32", 8, "rx_i32x8", TIER_GNU, false}, + {"i64", 4, "rx_i64x4", TIER_GNU, false}, + {"f32", 8, "rx_f32x8", TIER_GNU, false}, + {"f32", 16, "rx_f32x16", TIER_GNU, false}, + {"f16", 2, "rx_f16x2", TIER_F16, true}, + } + for b in bares { + ot := scalar(b.tag).odin + // only the first four lanes are checked; a shifted read moves all of them + checked := min(b.lanes, 4) + fields := make([]Leaf, checked) + getters := make([][2]string, checked) + for i in 0 ..< checked { + fields[i] = leaf2("", tp("{}[%d]", i), b.tag, val(i, b.tag)) + getters[i] = { + tp("simd.extract({}, %d)", i), + tp("%s(%s)", ot, val(i, b.tag)), + } + } + add( + tp("bv%d_%s", b.lanes, b.tag), + tp("#simd[%d]%s", b.lanes, ot), + b.cname, + fields, + tier = b.tier, + odin_set = strs(tp("{} = {%s}", vals(0, b.lanes, b.tag))), + odin_get = getters, + ) + } + for b in bares { + if !b.wrap { + continue + } + ot := scalar(b.tag).odin + checked := min(b.lanes, 4) + fields := make([]Leaf, checked) + getters := make([][2]string, checked) + for i in 0 ..< checked { + fields[i] = leaf2("", tp("v[%d]", i), b.tag, val(i, b.tag)) + getters[i] = { + tp("simd.extract({}.v, %d)", i), + tp("%s(%s)", ot, val(i, b.tag)), + } + } + add( + tp("wv%d_%s", b.lanes, b.tag), + tp("struct { v: #simd[%d]%s }", b.lanes, ot), + tp("struct { %s v; }", b.cname), + fields, + tier = b.tier, + odin_set = strs(tp("{}.v = {%s}", vals(0, b.lanes, b.tag))), + odin_get = getters, + ) + } + + // --- bit-fields. A member measured in BITS is neither an integer nor + // padding: x86-64 merges its eightbyte to INTEGER, and RISC-V's hardware + // float rule names it explicitly. The BACKING must match C's allocation + // unit -- `bit_field u8` against `unsigned a:3` is a different type. + for w in ([][2]int{{3, 5}, {1, 31}, {17, 15}}) { + w1, w2 := w[0], w[1] + // the value must fit the width AND leave room for the mutation control + va := w1 > 1 ? min(5, (1 << uint(w1)) - 1) : 0 + vb := min(9, (1 << uint(w2)) - 1) + add( + tp("bf_%d_%d", w1, w2), + tp("bit_field u32 { a: u32 | %d, b: u32 | %d }", w1, w2), + tp("struct { unsigned a : %d; unsigned b : %d; }", w1, w2), + leaves( + leaf2("a", "a", "u32", tp("%d", va)), + leaf2("b", "b", "u32", tp("%d", vb)), + ), + ) + } + add( + "bff_f32", + "struct { f: f32, b: bit_field u32 { a: u32 | 3 } }", + "struct { float f; struct { unsigned a : 3; } b; }", + leaves(leaf("f", "f32", 0), leaf2("b.a", "b.a", "u32", "5")), + ) + + // --- matrix, which lowers to an array with its own alignment + // a matrix aligns to its element, so the counterpart is a plain array + { + fields := make([]Leaf, 4) + for i in 0 ..< 4 { + fields[i] = leaf2( + tp("m[%d, %d]", i % 2, i / 2), + tp("m[%d]", i), + "f32", + val(i, "f32"), + ) + } + add( + "m22_f32", + "struct { m: matrix[2,2]f32 }", + "struct { float m[4]; }", + fields, + tier = TIER_GNU, + ) + } + + // Complex is covered by the scalar families above, compared as a WHOLE value rather than + // per lane -- `==` on a complex compares both halves, so a dropped or corrupted half is + // caught without needing an accessor. The lanes carry DIFFERENT values (`v` and `v+1`) + // because equal ones would make a swapped real/imag undetectable. + // + // Per-lane checks are expressible if a failure ever needs to name which half: `c_ref`'s `{}` + // form takes an expression rather than a path, so C's prefix `__real__ {}` works, and the + // Odin side uses the `odin_get` hatch with `real({})`. That was the accessor problem. + + // --- array OF struct: the array rule and the struct rule compose, and a + // stride bug lives in the composition + add( + "aos", + "struct { a: [2]struct{ x, y: f32 } }", + "struct { struct { float x, y; } a[2]; }", + leaves( + leaf("a[0].x", "f32", 0), leaf("a[0].y", "f32", 1), + leaf("a[1].x", "f32", 2), leaf("a[1].y", "f32", 3), + ), + ) + add( + "aos2", + "struct { a: [2][2]f32 }", + "struct { float a[2][2]; }", + leaves( + leaf("a[0][0]", "f32", 0), leaf("a[0][1]", "f32", 1), + leaf("a[1][0]", "f32", 2), leaf("a[1][1]", "f32", 3), + ), + ) + + // --- an over-aligned MEMBER, which leaves an interior gap. A layout walk + // that sums field sizes gets this wrong and a per-field check catches it. + add( + "oam", + "struct #min_field_align(16) { a: i8, b: f32 }", + "struct { int8_t a; float b __attribute__((aligned(16))); }", + leaves(leaf("a", "i8", 0), leaf("b", "f32", 1)), + tier = TIER_GNU, + ) + + // --- a union whose MEMBERS are aggregates: the merge has two composite + // candidates for one byte, not two scalars + add( + "ua_s2_f64", + "struct #raw_union { a: struct{ x, y: f32 }, b: f64 }", + "union { struct { float x, y; } a; double b; }", + leaves(leaf("a.x", "f32", 0), leaf("a.y", "f32", 1)), + ) + { + fields := make([]Leaf, 4) + for i in 0 ..< 4 { + fields[i] = leaf(tp("a[%d]", i), "f32", i) + } + add( + "ua_arr", + "struct #raw_union { a: [4]f32, b: [2]f64 }", + "union { float a[4]; double b[2]; }", + fields, + ) + add( + "ua_arr_n4", + "struct #raw_union { a: [4]f32, b: f32 }", + "union { float a[4]; float b; }", + fields, + ) + } + // The rows above are equal-width, so "the last member's type" happens to give + // the right answer and cannot detect a classifier that uses it. These two are + // the pair that can: an aggregate member followed by a NARROWER one, and the + // same two members the other way round as the control. + add( + "ua_arr_n", + "struct #raw_union { a: [2]f32, b: f32 }", + "union { float a[2]; float b; }", + leaves(leaf("a[0]", "f32", 0), leaf("a[1]", "f32", 1)), + ) + add( + "ua_arr_w", + "struct #raw_union { b: f32, a: [2]f32 }", + "union { float b; float a[2]; }", + leaves(leaf("a[0]", "f32", 0), leaf("a[1]", "f32", 1)), + ) + + // --- three levels of nesting: SysV flattens, and anything that classifies + // per top-level member stops early + add( + "n3_deep", + "struct { a: struct{ b: struct{ c: f32, d: f32 } } }", + "struct { struct { struct { float c, d; } b; } a; }", + leaves(leaf("a.b.c", "f32", 0), leaf("a.b.d", "f32", 1)), + ) + add( + "n3_mix", + "struct { a: struct{ b: struct{ c: i64 }, d: f32 }, e: f64 }", + "struct { struct { struct { int64_t c; } b; float d; } a; double e; }", + leaves(leaf("a.b.c", "i64", 0), leaf("a.d", "f32", 1), leaf("e", "f64", 2)), + ) + + // NOTE: `#packed` with `#align(N)` is rejected by Odin ("'#align' cannot be + // applied with '#packed'") though C accepts the combination, so there is no + // shape to compare. + + // --- zero-sized on its own, in argument and return position + add("empty", "struct { e: struct{} }", "struct { struct {} e; }", nil, tier = TIER_GNU) + add("zarr", "struct { z: [0]f32 }", "struct { float z[0]; }", nil, tier = TIER_GNU) + + // --- an array OF vectors, and a vector wider than one register + // The C paths index the vector array directly; only the ODIN side needs the + // hatch. + { + fields := make([]Leaf, 8) + for i in 0 ..< 8 { + fields[i] = leaf2("", tp("a[%d][%d]", i / 4, i % 4), "f32", tp("%d.5", i + 1)) + } + add( + "av2_f32", + "struct { a: [2]#simd[4]f32 }", + "struct { rx_v4f a[2]; }", + fields, + tier = TIER_GNU, + odin_set = strs("{}.a[0] = {1.5, 2.5, 3.5, 4.5}", "{}.a[1] = {5.5, 6.5, 7.5, 8.5}"), + odin_get = pairs({"simd.extract({}.a[0], 0)", "f32(1.5)"}, {"simd.extract({}.a[1], 3)", "f32(8.5)"}), + ) + } + // A wide vector NOT at offset 0. Its alignment decides where it starts, so a + // wrong alignment moves the member and changes `size_of` -- which is the only + // way the difference is observable on a target that passes a >16-byte + // aggregate by POINTER (AAPCS64), where the slot alignment never shows. + { + fields := make([]Leaf, 9) + getters := make([][2]string, 9) + fields[0] = leaf("a", "i8", 0) + getters[0] = {"{}.a", "i8(3)"} + for i in 0 ..< 8 { + fields[i + 1] = leaf(tp("v[%d]", i), "f32", i) + getters[i + 1] = { + tp("simd.extract({}.v, %d)", i), + tp("f32(%s)", val(i, "f32")), + } + } + add( + "v8_off", + "struct { a: i8, v: #simd[8]f32 }", + "struct { int8_t a; float v __attribute__((vector_size(32))); }", + fields, + tier = TIER_GNU, + odin_set = strs("{}.a = 3", tp("{}.v = {%s}", vals(0, 8, "f32"))), + odin_get = getters, + ) + } + { + fields := make([]Leaf, 8) + getters := make([][2]string, 8) + for i in 0 ..< 8 { + fields[i] = leaf(tp("v[%d]", i), "f32", i) + getters[i] = { + tp("simd.extract({}.v, %d)", i), + tp("f32(%s)", val(i, "f32")), + } + } + add( + "v8_f32", + "struct { v: #simd[8]f32 }", + "struct { float v __attribute__((vector_size(32))); }", + fields, + tier = TIER_GNU, + odin_set = strs(tp("{}.v = {%s}", vals(0, 8, "f32"))), + odin_get = getters, + ) + } + + // --- large, past every by-value threshold + { + fields := make([]Leaf, 8) + for i in 0 ..< 8 { + fields[i] = leaf(tp("a[%d]", i), "i64", i) + } + add("big", "struct { a: [8]i64 }", "struct { int64_t a[8]; }", fields) + } +} + +// `val(lo.. string { + out := make([]string, hi - lo, context.temp_allocator) + for i in lo ..< hi { + out[i - lo] = val(i, tag) + } + return strings.join(out, ", ", context.temp_allocator) +} + +leaves :: proc(items: ..Leaf) -> []Leaf { + out := make([]Leaf, len(items)) + copy(out, items) + return out +} + +strs :: proc(items: ..string) -> []string { + out := make([]string, len(items)) + copy(out, items) + return out +} + +pairs :: proc(items: ..[2]string) -> [][2]string { + out := make([][2]string, len(items)) + copy(out, items) + return out +} + +// ---------------------------------------------------------------- emit + +guard_of :: proc(tier: string) -> string { + switch tier { + case TIER_GNU: return "ABI_TIER_GNU" + case TIER_F16: return "ABI_TIER_F16" + case TIER_I128: return "ABI_TIER_I128" + } + return "" +} + +// The tier conditions live here. The corpus is guarded by them, `tiers.c` reports them. +TIER_CONDS := [][2]string{ + {TIER_GNU, "defined(__GNUC__)"}, + {TIER_F16, "defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER)"}, + {TIER_I128, "defined(__SIZEOF_INT128__)"}, +} + +emit_tiers_c :: proc() -> string { + sb := strings.builder_make() + strings.write_string( + &sb, + `/* GENERATED by tests/abi/gen.odin -- do not edit. + Preprocess this and grep the markers: it answers which tiers the C + compiler actually has, so the Odin side can be gated by the same + answer rather than by a restatement of the condition. */ +`, + ) + for tc in TIER_CONDS { + name, _ := strings.replace_all(guard_of(tc[0]), "ABI_TIER_", "", context.temp_allocator) + w(&sb, "#if %s\nABI_YES_%s\n#endif\n", tc[1], name) + } + return strings.to_string(sb) +} + +C_HEAD :: `/* GENERATED by tests/abi/gen.odin -- do not edit. */ +#include +#include + +/* Tier guards. A target whose C compiler lacks an extension still runs the + core corpus; the Odin side is gated by the matching -define. */ +@TIER_DEFINES@ + +/* An enum with an explicit wide enumerator, so it is int-sized rather than + whatever the compiler picks for a small one. */ +enum E32 { E32_LO = 0, E32_HI = 0x7fffffff }; + +/* ` + "`vector_size`" + ` attaches to the ELEMENT, so an array of vectors needs a name. */ +#if defined(__GNUC__) +typedef float rx_v4f __attribute__((vector_size(16))); +/* Named vectors, so a BARE vector row can be ` + "`typedef rx_ ;`" + `. */ +typedef signed char rx_i8x4 __attribute__((vector_size(4))); +typedef signed char rx_i8x8 __attribute__((vector_size(8))); +typedef short rx_i16x2 __attribute__((vector_size(4))); +typedef short rx_i16x4 __attribute__((vector_size(8))); +typedef int rx_i32x2 __attribute__((vector_size(8))); +typedef float rx_f32x2 __attribute__((vector_size(8))); +typedef int rx_i32x8 __attribute__((vector_size(32))); +typedef long long rx_i64x4 __attribute__((vector_size(32))); +typedef float rx_f32x8 __attribute__((vector_size(32))); +typedef float rx_f32x16 __attribute__((vector_size(64))); +#endif +#if defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER) +typedef _Float16 rx_f16x2 __attribute__((vector_size(4))); +#endif +` + +emit_c :: proc() -> string { + sb := strings.builder_make() + + defines := strings.builder_make(context.temp_allocator) + for tc in TIER_CONDS { + w(&defines, "#if %s\n#define %s 1\n#endif\n", tc[1], guard_of(tc[0])) + } + head, _ := strings.replace_all( + C_HEAD, + "@TIER_DEFINES@", + strings.trim_right_space(strings.to_string(defines)), + context.temp_allocator, + ) + strings.write_string(&sb, head) + + for t in types { + g := guard_of(t.tier) + if g != "" { + w(&sb, "\n#ifdef %s\n", g) + } + w(&sb, "\ntypedef %s %s;\n", t.c, t.name) + // _arg: return the argument that FOLLOWS the aggregate + w(&sb, "double %s_arg(%s s, double next) { (void)s; return next; }\n", t.name, t.name) + // _chk: every field, so a wrong offset is caught as well as a wrong register + w(&sb, "int %s_chk(%s s) { return (%s) ? 0 : 1; }\n", t.name, t.name, c_conds(t, "s")) + // _ret: return position + w(&sb, "%s %s_ret(void) { %s s; ", t.name, t.name, t.name) + for f in t.fields { + w(&sb, "%s = (%s); ", c_ref(f.c_path, "s"), c_val(f.tag, f.val)) + } + strings.write_string(&sb, "return s; }\n") + // _ex: the aggregate after the argument registers are gone + w( + &sb, + "double %s_ex(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e," + + " int64_t f, int64_t o, double g, double h, double i, double j, double k," + + " double l, double m, double n, %s s, double next) {\n", + t.name, + t.name, + ) + strings.write_string(&sb, "\t(void)a;(void)b;(void)c;(void)d;(void)e;(void)f;(void)o;(void)g;(void)h;\n") + strings.write_string(&sb, "\t(void)i;(void)j;(void)k;(void)l;(void)m;(void)n;(void)s;\n\treturn next;\n}\n") + // _ex2: SysV has six integer registers but AAPCS64 and RISC-V have eight, + // so `_ex` only partially fills those. Nine of each exhausts all three. + w(&sb, "double %s_ex2(%s, %s, %s s, double next) {\n\t", + t.name, numbered("int64_t q%d", 9), numbered("double w%d", 9), t.name) + for i in 0 ..< 9 { + w(&sb, "(void)q%d;", i) + } + for i in 0 ..< 9 { + w(&sb, "(void)w%d;", i) + } + strings.write_string(&sb, "(void)s;\n\treturn next;\n}\n") + // _two: the FIRST aggregate's register consumption decides the second's + // placement, which nothing with a single aggregate can observe + w(&sb, "double %s_two(%s s1, %s s2, double next) {\n", t.name, t.name, t.name) + w(&sb, "\tif (!(%s)) return -1;\n", c_conds(t, "s1")) + w(&sb, "\tif (!(%s)) return -2;\n\treturn next;\n}\n", c_conds(t, "s2")) + // _back: the other direction -- C calls an exported Odin callee, which is + // what a callback does and what nothing else here covers + w(&sb, "extern double o_%s_take(%s s, double next);\n", t.name, t.name) + w(&sb, "extern %s o_%s_make(void);\n", t.name, t.name) + w(&sb, "int %s_back(void) {\n\t%s s; ", t.name, t.name) + for f in t.fields { + w(&sb, "%s = (%s); ", c_ref(f.c_path, "s"), c_val(f.tag, f.val)) + } + w(&sb, "\n\tif (o_%s_take(s, 7) != 7) return 1;\n", t.name) + w(&sb, "\t%s r = o_%s_make();\n", t.name, t.name) + w(&sb, "\tif (!(%s)) return 2;\n\treturn 0;\n}\n", c_conds(t, "r")) + // _can: the aggregate wedged between two stack neighbours, after the + // registers are gone. `_ex` only checks what follows; a wrongly sized or + // wrongly aligned slot can equally eat what precedes it, and an + // over-aligned slot slides the aggregate onto its own neighbour. + w( + &sb, + "double %s_can(int64_t q0, int64_t q1, int64_t q2, int64_t q3," + + " int64_t q4, int64_t q5, int64_t q6, double w0, double w1, double w2," + + " double w3, double w4, double w5, double w6, double w7," + + " int64_t before, %s s, int64_t after, double last) {\n", + t.name, + t.name, + ) + strings.write_string(&sb, "\t(void)q0;(void)q1;(void)q2;(void)q3;(void)q4;(void)q5;(void)q6;\n") + strings.write_string(&sb, "\t(void)w0;(void)w1;(void)w2;(void)w3;(void)w4;(void)w5;(void)w6;(void)w7;\n") + strings.write_string(&sb, "\tif (before != 0x1111111111111111LL) return -1;\n") + strings.write_string(&sb, "\tif (after != 0x2222222222222222LL) return -2;\n") + w(&sb, "\tif (!(%s)) return -3;\n\treturn last;\n}\n", c_conds(t, "s")) + // _can2: same idea as `_can`, but with enough integer fillers to push the + // aggregate to an outgoing offset that is 16-aligned and NOT 32-aligned. + // At offset 0 a 16- and a 32-aligned slot coincide, so an over-aligned + // aggregate is invisible there -- which is why `_can` alone passes on + // AArch64 while its vector alignment disagrees with clang. + w( + &sb, + "double %s_can2(%s, int64_t before, %s s, int64_t after, double last) {\n\t", + t.name, + numbered("int64_t p%d", 11), + t.name, + ) + for i in 0 ..< 11 { + w(&sb, "(void)p%d;", i) + } + strings.write_string(&sb, "\n\tif (before != 0x1111111111111111LL) return -1;\n") + strings.write_string(&sb, "\tif (after != 0x2222222222222222LL) return -2;\n") + w(&sb, "\tif (!(%s)) return -3;\n\treturn last;\n}\n", c_conds(t, "s")) + // _va: the variadic path, which is a separate set of rules -- SysV's AL + // register count, Win64 duplicating a float into the matching GPR, + // Darwin-arm64 stacking every variadic argument. A zero-sized type has + // no meaningful `va_arg`, so it is skipped. + if len(t.fields) > 0 { + w(&sb, "double %s_va(int n, ...) {\n\tva_list ap; va_start(ap, n);\n", t.name) + w(&sb, "\t%s s = va_arg(ap, %s);\n", t.name, t.name) + strings.write_string(&sb, "\tdouble next = va_arg(ap, double);\n\tva_end(ap);\n") + w(&sb, "\treturn (%s) ? next : -1;\n}\n", c_conds(t, "s")) + } + if g != "" { + w(&sb, "\n#endif /* %s */\n", g) + } + } + return strings.to_string(sb) +} + +// `p0, p1, ... p`, from a format holding one %d. +numbered :: proc(format: string, n: int) -> string { + out := make([]string, n, context.temp_allocator) + for i in 0 ..< n { + out[i] = tp(format, i) + } + return strings.join(out, ", ", context.temp_allocator) +} + +ODIN_HEAD :: `// GENERATED by tests/abi/gen.odin -- do not edit. +// +// Every procedure below asks one question: does Odin place this value where the +// platform C compiler expects it? No ABI is encoded here, so the same corpus is +// valid on SysV, AAPCS64 and Win64 without changing a line. +// +// Three checks per type, because they fail for different reasons: +// _arg the value AFTER the aggregate comes back -- catches a wrong size or +// a wrong number of consumed registers, deterministically rather than +// by scratch-register luck +// _chk every field of the aggregate itself -- catches a wrong offset +// _ret the aggregate in return position -- a separate classifier path +package test_abi + +import "core:simd" +import "core:testing" +_ :: simd + +ABI_TIER_GNU :: #config(ABI_TIER_GNU, true) +ABI_TIER_F16 :: #config(ABI_TIER_F16, true) +ABI_TIER_I128 :: #config(ABI_TIER_I128, true) + +// The mutation control. With ` + "`-define:ABI_MUTATE=true`" + ` every type feeds a value +// the C side must reject, so the suite MUST go red. A suite that cannot fail is +// not evidence, and once the defects it currently catches are fixed this is the +// only thing left proving the checks still bite. +ABI_MUTATE :: #config(ABI_MUTATE, false) + +// Variadic coverage, OFF by default. +// +// This measures one direction: Odin calling a C variadic. Receiving one is a +// separate mechanism -- a #c_vararg parameter cannot be read directly, and the +// compiler points you at c_va_start / c_va_list; nothing here covers it. +// +// The scalar half of the calling side is correct: the C default-argument promotions +// are implemented, so i8, u16, f32, bool and every pointer come out matching clang +// arg for arg. +// +// What is missing is the step after promotion. An aggregate is handed to LLVM raw +// where clang coerces it per the psABI: +// +// odin send(i32 1, { float, float } %h, double 7.0) +// clang send(i32 1, <2 x float> %h, double 7.0) +// +// It is a single defect, but very noisy in the test results as ~75% trip it. +// Turn it on with ` + "`-define:ABI_VARARGS=true`" + ` to measure it. +ABI_VARARGS :: #config(ABI_VARARGS, false) + + + +E32 :: enum i32 { LO = 0, HI = 0x7fffffff } +BS :: bit_set[0..<31; u32] + +foreign import lib "abi_corpus_c.o" +` + +// The corpus declarations, identical in the test and the freestanding driver: +// the type, the C functions it calls, and the two callees C calls back into. +emit_odin_decls :: proc(sb: ^strings.Builder, t: Ty, ind: string) { + w(sb, "%s%s :: %s\n", ind, t.name, t.odin) + w(sb, "%s@(default_calling_convention=\"c\")\n%sforeign lib {\n", ind, ind) + w(sb, "%s\t%s_arg :: proc(s: %s, next: f64) -> f64 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_chk :: proc(s: %s) -> i32 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_ret :: proc() -> %s ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_ex :: proc(a, b, c, d, e, f, o: i64, g, h, i, j, k, l, m, n: f64," + + " s: %s, next: f64) -> f64 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_ex2 :: proc(q0, q1, q2, q3, q4, q5, q6, q7, q8: i64," + + " w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: %s, next: f64) -> f64 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_two :: proc(s1, s2: %s, next: f64) -> f64 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_back :: proc() -> i32 ---\n", ind, t.name) + w(sb, "%s\t%s_can2 :: proc(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10: i64," + + " before: i64, s: %s, after: i64, last: f64) -> f64 ---\n", ind, t.name, t.name) + w(sb, "%s\t%s_can :: proc(q0, q1, q2, q3, q4, q5, q6: i64," + + " w0, w1, w2, w3, w4, w5, w6, w7: f64," + + " before: i64, s: %s, after: i64, last: f64) -> f64 ---\n", ind, t.name, t.name) + if len(t.fields) > 0 { + w(sb, "%s\t%s_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n", ind, t.name) + } + w(sb, "%s}\n", ind) + // the callees C calls back into: the direction a callback uses + w(sb, "%s@(export) o_%s_take :: proc \"c\" (s: %s, next: f64) -> f64 {\n", ind, t.name, t.name) + for g in odin_getters(t, "s") { + w(sb, "%s\tif %s != %s { return -1 }\n", ind, g[0], g[1]) + } + w(sb, "%s\treturn next\n%s}\n", ind, ind) + w(sb, "%s@(export) o_%s_make :: proc \"c\" () -> %s {\n%s\ts: %s\n", ind, t.name, t.name, ind, t.name) + for st in odin_setters(t, "s") { + w(sb, "%s\t%s\n", ind, st) + } + w(sb, "%s\treturn s\n%s}\n", ind, ind) +} + +emit_odin :: proc() -> string { + sb := strings.builder_make() + strings.write_string(&sb, ODIN_HEAD) + for t in types { + g := guard_of(t.tier) + ind := g != "" ? "\t" : "" + strings.write_string(&sb, "\n") + if g != "" { + w(&sb, "when %s {\n", g) + } + emit_odin_decls(&sb, t, ind) + w(&sb, "%s@(test)\n%stest_%s :: proc(t: ^testing.T) {\n", ind, ind, t.name) + w(&sb, "%s\ts: %s\n", ind, t.name) + for st in odin_setters(t, "s") { + w(&sb, "%s\t%s\n", ind, st) + } + // types whose members have no lvalue path (#simd) are set as a whole and + // cannot be perturbed field-wise, so the control skips them + if len(t.fields) > 0 && len(t.odin_set) == 0 { + f := t.fields[0] + w(&sb, "%s\twhen ABI_MUTATE { s.%s = %s }\n", ind, f.odin_path, mutated(f.tag, f.val)) + } + w(&sb, "%s\ttesting.expect_value(t, %s_arg(s, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_chk(s), i32(0))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_ex2(1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9, s, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_two(s, s, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_can(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_can2(1,2,3,4,5,6,7,8,9,10,11, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n", ind, t.name) + w(&sb, "%s\ttesting.expect_value(t, %s_back(), i32(0))\n", ind, t.name) + if len(t.fields) > 0 { + w(&sb, "%s\twhen ABI_VARARGS {\n", ind) + w(&sb, "%s\t\ttesting.expect_value(t, %s_va(1, s, f64(7)), f64(7))\n", ind, t.name) + w(&sb, "%s\t}\n", ind) + } + w(&sb, "%s\tr := %s_ret()\n", ind, t.name) + getters := odin_getters(t, "r") + if len(getters) == 0 { + w(&sb, "%s\t_ = r\n", ind) + } + for g in getters { + w(&sb, "%s\ttesting.expect_value(t, %s, %s)\n", ind, g[0], g[1]) + } + w(&sb, "%s}\n", ind) + if g != "" { + strings.write_string(&sb, "}\n") + } + } + return strings.to_string(sb) +} + +MAIN_HEAD :: `// GENERATED by tests/abi/gen.odin -- do not edit. +// +// The same corpus as a freestanding driver, for a target with no test runner. +// Exits with the number of failing types, so a cross target can be checked +// under an emulator in CI without core:testing or a thread. +package abi_main + +import "core:simd" +_ :: simd + +ABI_TIER_GNU :: #config(ABI_TIER_GNU, true) +ABI_TIER_F16 :: #config(ABI_TIER_F16, true) +ABI_TIER_I128 :: #config(ABI_TIER_I128, true) + +E32 :: enum i32 { LO = 0, HI = 0x7fffffff } +BS :: bit_set[0..<31; u32] + +// Types at or below this index are skipped, so a runner can enumerate every +// failure by re-running from the last one rather than only seeing a count. +ABI_SKIP :: #config(ABI_SKIP, 0) + +// Variadic coverage, OFF by default. +// +// This measures one direction: Odin calling a C variadic. Receiving one is a +// separate mechanism -- a #c_vararg parameter cannot be read directly, and the +// compiler points you at c_va_start / c_va_list; nothing here covers it. +// +// The scalar half of the calling side is correct: the C default-argument promotions +// are implemented, so i8, u16, f32, bool and every pointer come out matching clang +// arg for arg. +// +// What is missing is the step after promotion. An aggregate is handed to LLVM raw +// where clang coerces it per the psABI: +// +// odin send(i32 1, { float, float } %h, double 7.0) +// clang send(i32 1, <2 x float> %h, double 7.0) +// +// It is a single defect, but very noisy in the test results as ~75% trip it. +// Turn it on with ` + "`-define:ABI_VARARGS=true`" + ` to measure it. +ABI_VARARGS :: #config(ABI_VARARGS, false) + + + +foreign import lib "../abi_corpus_c.o" +` + +emit_main :: proc() -> string { + sb := strings.builder_make() + strings.write_string(&sb, MAIN_HEAD) + body := strings.builder_make(context.temp_allocator) + + for t, idx in types { + g := guard_of(t.tier) + ind := g != "" ? "\t" : "" + strings.write_string(&sb, "\n") + if g != "" { + w(&sb, "when %s {\n", g) + } + emit_odin_decls(&sb, t, ind) + w(&sb, "%scheck_%s :: proc \"contextless\" () -> i32 {\n", ind, t.name) + w(&sb, "%s\ts: %s\n", ind, t.name) + for st in odin_setters(t, "s") { + w(&sb, "%s\t%s\n", ind, st) + } + w(&sb, "%s\tif %s_arg(s, 7) != 7 { return 1 }\n", ind, t.name) + w(&sb, "%s\tif %s_chk(s) != 0 { return 1 }\n", ind, t.name) + w(&sb, "%s\tif %s_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7) != 7 { return 1 }\n", ind, t.name) + w(&sb, "%s\tr := %s_ret()\n", ind, t.name) + getters := odin_getters(t, "r") + if len(getters) == 0 { + w(&sb, "%s\t_ = r\n", ind) + } + for gt in getters { + w(&sb, "%s\tif %s != %s { return 1 }\n", ind, gt[0], gt[1]) + } + w(&sb, "%s\treturn 0\n%s}\n", ind, ind) + if g != "" { + strings.write_string(&sb, "}\n") + } + + n := idx + 1 + chk := tp("if %d > ABI_SKIP && check_%s() != 0 { return %d }", n, t.name, n) + if g != "" { + w(&body, "\twhen %s { %s }\n", g, chk) + } else { + w(&body, "\t%s\n", chk) + } + } + + strings.write_string(&sb, "\n@(export)\nprobe_main :: proc \"c\" () -> i32 {\n") + strings.write_string(&sb, strings.to_string(body)) + strings.write_string(&sb, "\treturn 0\n}\n") + strings.write_string(&sb, "\n// index -> name\n") + for t, idx in types { + w(&sb, "// %d\t%s\n", idx + 1, t.name) + } + return strings.to_string(sb) +} + +// ---------------------------------------------------------------- main + +main :: proc() { + // Written into the caller's build directory, not the source tree: nothing + // generated is checked in, so the two languages cannot drift apart. + dir := len(os.args) > 1 ? os.args[1] : "." + + build() + + write :: proc(dir, name, content: string) { + path := tp("%s/%s", dir, name) + if err := os.write_entire_file(path, content); err != nil { + fmt.eprintfln("could not write %s: %v", path, err) + os.exit(1) + } + } + write(dir, "abi_corpus.c", emit_c()) + write(dir, "abi_corpus.odin", emit_odin()) + write(dir, "abi_main.odin", emit_main()) + write(dir, "tiers.c", emit_tiers_c()) + + c_funcs := 0 + for t in types { + c_funcs += len(t.fields) > 0 ? 8 : 7 + } + fmt.printfln("%d types, %d C functions, %d Odin callees", len(types), c_funcs, len(types) * 2) +} diff --git a/tests/abi/run.bat b/tests/abi/run.bat new file mode 100644 index 000000000..db03eda3b --- /dev/null +++ b/tests/abi/run.bat @@ -0,0 +1,42 @@ +@echo off + +REM The ABI comparator. Every check is "Odin agrees with the platform C compiler" + +REM An ABI is a link-time contract, so the two sides are built independently and +REM either may be optimised: `set ABI_CFLAGS=-O2` for the C side, and any +REM argument here goes to `odin test`, e.g. `run.bat -o:speed`. + +REM cleaned BEFORE, not after: the generated corpus is left to inspect +if exist "build\" rmdir /S /Q build +mkdir build +pushd build + +set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused-defineables + +@echo on + +..\..\..\odin run ..\gen.odin -file -- . || exit /b + +@echo off +REM Ask the C compiler which tiers it has, by preprocessing the generated +REM `tiers.c`. Clang targeting MSVC doesnt define `__GNUC__` or `_Float16`, +REM Odin side needs to match +set TIER_GNU=false +set TIER_F16=false +set TIER_I128=false +clang -E tiers.c 2>nul | findstr /C:"ABI_YES_GNU" >nul && set TIER_GNU=true +clang -E tiers.c 2>nul | findstr /C:"ABI_YES_F16" >nul && set TIER_F16=true +clang -E tiers.c 2>nul | findstr /C:"ABI_YES_I128" >nul && set TIER_I128=true +set TIERS=-define:ABI_TIER_GNU=%TIER_GNU% -define:ABI_TIER_F16=%TIER_F16% -define:ABI_TIER_I128=%TIER_I128% +echo tiers: %TIERS% + +@echo on + +REM -w because the corpus deliberately uses zero-length arrays and empty +REM structs; both are the extensions under test. +clang %ABI_CFLAGS% -c abi_corpus.c -o abi_corpus_c.o -w || exit /b +..\..\..\odin test abi_corpus.odin %COMMON% %TIERS% %* || exit /b + +@echo off + +popd diff --git a/tests/abi/run.sh b/tests/abi/run.sh new file mode 100755 index 000000000..0fea5f67b --- /dev/null +++ b/tests/abi/run.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -eu + +# The ABI comparator. +# +# Every check is "Odin agrees with the platform C compiler" +# +# ./run.sh +# ./run.sh linux_riscv64 riscv64-linux-gnu \ +# "-extra-linker-flags:-fuse-ld=/usr/bin/riscv64-linux-gnu-gcc-12 -static -Wl,-static" -no-rpath +# +# For a target with no cross libc -- i386, arm32 -- see `cross.sh`, which builds +# the same corpus freestanding. + +TARGET=${1:-} +TRIPLE=${2:-} +if [ $# -gt 2 ]; then shift 2; else shift $#; fi # anything else goes to `odin test` + +here=$(cd "$(dirname "$0")" && pwd) +: "${ODIN:=$here/../../odin}" +: "${CLANG:=clang}" +# The C side's optimisation level. An ABI is a link-time contract, so the two +# sides are built independently and either may be optimised: `ABI_CFLAGS=-O2`. +: "${ABI_CFLAGS:=}" +COMMON="-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused-defineables" + +CC_TARGET=""; [ -n "$TRIPLE" ] && CC_TARGET="--target=$TRIPLE" +ODIN_TARGET=""; [ -n "$TARGET" ] && ODIN_TARGET="-target:$TARGET" + + +# Cleaned BEFORE, not after: the generated corpus is left in place so it can be +# read after a failure. CI throws the tree away anyway. +rm -rf "$here/build" +mkdir -p "$here/build" +pushd "$here/build" > /dev/null + +set -x + +$ODIN run ../gen.odin -file -- . + +# Ask the C compiler which tiers it has, by preprocessing the generated `build-cross/tiers.c`. +# The Odin side must use the same tiers or it references symbols C never emitted. +have() { $CLANG $CC_TARGET -E tiers.c 2>/dev/null | grep -q "ABI_YES_$1" && echo true || echo false; } +TIERS="-define:ABI_TIER_GNU=$(have GNU) -define:ABI_TIER_F16=$(have F16) -define:ABI_TIER_I128=$(have I128)" + +# `-w` because the corpus deliberately uses zero-length arrays and empty +# structs; both are the extensions under test. +$CLANG $CC_TARGET $ABI_CFLAGS -c abi_corpus.c -o abi_corpus_c.o -w +$ODIN test abi_corpus.odin $COMMON $ODIN_TARGET $TIERS "$@" + +set +x + +popd > /dev/null