From 9a2164ac2efa40148bb6a813bfb2c50ba18274d2 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:05:14 -0700 Subject: [PATCH 01/32] abi test harness --- .github/workflows/ci.yml | 9 + .gitignore | 1 + tests/abi/cross.sh | 123 +++++++ tests/abi/gen.py | 697 +++++++++++++++++++++++++++++++++++++++ tests/abi/run.bat | 22 ++ tests/abi/run.sh | 49 +++ 6 files changed, 901 insertions(+) create mode 100755 tests/abi/cross.sh create mode 100644 tests/abi/gen.py create mode 100644 tests/abi/run.bat create mode 100755 tests/abi/run.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3874a65ab..3a59fa456 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ 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) ./odin check tests/benchmark -vet -strict-style -no-entry-point build_freebsd: @@ -73,6 +74,7 @@ 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) ./odin check tests/benchmark -vet -strict-style -no-entry-point ci: strategy: @@ -171,6 +173,11 @@ jobs: cd tests/issues ./run.sh + - name: ABI comparator + run: | + cd tests/abi + ./run.sh + - 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 @@ -283,6 +290,8 @@ 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 - name: Check benchmarks shell: cmd run: | diff --git a/.gitignore b/.gitignore index 56a75c788..776c1fe12 100644 --- a/.gitignore +++ b/.gitignore @@ -307,6 +307,7 @@ build.sh *.raddbg *.rdi tests/issues/build/* +tests/abi/build/* misc/featuregen/featuregen # Clangd stuff diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh new file mode 100755 index 000000000..8d618e425 --- /dev/null +++ b/tests/abi/cross.sh @@ -0,0 +1,123 @@ +#!/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 +# ./cross.sh linux_arm32 arm-linux-gnueabihf qemu-arm +# ./cross.sh linux_riscv64 riscv64-linux-gnu qemu-riscv64 + +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} +: "${ODIN:=../../odin}" +: "${CLANG:=clang}" + +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' ;; +*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 + +# Ask the C compiler which tiers it actually has, the Odin side must have the same answer or +# it references symbols the C side never emitted ( eg `__int128` does not exist on i386). +macros=$($CLANG --target="$TRIPLE" -dM -E -x c /dev/null 2>/dev/null) +tier() { case "$macros" in *"$1"*) echo true ;; *) echo false ;; esac; } +TIERS="-define:ABI_TIER_GNU=$(tier __GNUC__) -define:ABI_TIER_F16=$(tier __FLT16_MANT_DIG__) -define:ABI_TIER_I128=$(tier __SIZEOF_INT128__)" + +rm -rf build-cross +mkdir -p build-cross/p +python3 gen.py build-cross +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< 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 +rm -rf build-cross +exit $rc diff --git a/tests/abi/gen.py b/tests/abi/gen.py new file mode 100644 index 000000000..ee315fa4f --- /dev/null +++ b/tests/abi/gen.py @@ -0,0 +1,697 @@ +#!/usr/bin/env python3 +"""Generates abi_corpus.odin and abi_corpus.c from one description. + +The two files are checked in; this only needs running when the corpus changes. +Writing them by hand is what the generator exists to avoid: the whole test is +the claim that the Odin and C declarations describe the SAME type, and two +hand-maintained files drift. + +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. +""" + +import io + +# ---------------------------------------------------------------- scalars + +# tag -> (odin, c, is_float) +SCALARS = { + "i8": ("i8", "int8_t", False), + "i16": ("i16", "int16_t", False), + "i32": ("i32", "int32_t", False), + "i64": ("i64", "int64_t", False), + "u8": ("u8", "uint8_t", False), + "u16": ("u16", "uint16_t", False), + "u32": ("u32", "uint32_t", False), + "u64": ("u64", "uint64_t", False), + "bool":("bool","_Bool", False), + "f16": ("f16", "_Float16", True), + "i128":("i128","__int128", False), + "enum":("E32", "enum E32", False), + "c64": ("complex64", "float _Complex", False), + "c128":("complex128", "double _Complex", False), + "bset":("BS", "unsigned", False), + "f32": ("f32", "float", True), + "f64": ("f64", "double", True), + "ptr": ("rawptr", "void *", False), +} + +# 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 = {"f16": TIER_F16, "i128": TIER_I128} + + +def tier_of(*tags, base=TIER_CORE): + for t in tags: + if t in SCALAR_TIER: + return SCALAR_TIER[t] + return base + + +class Ty: + def __init__(self, name, odin, c, fields, tier=TIER_CORE, odin_set=None, odin_get=None): + self.name, self.odin, self.c, self.fields, self.tier = name, odin, c, fields, tier + # 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). + self.odin_set, self.odin_get = odin_set, odin_get + + +def val(i, tag): + """A distinct value per field position, so a shifted read is detectable.""" + if SCALARS[tag][2]: + return f"{i * 7 + 3}.5" + return str(i * 7 + 3) + + +def c_val(tag, v): + if tag == "ptr": return f"(void *)(intptr_t)({v})" + if tag == "bool": return "1" + if tag == "enum": return f"(enum E32)({v})" + if tag == "c64": return f"({v}.0f + {v}.0if)" + if tag == "c128": return f"({v}.0 + {v}.0i)" + if tag == "bset": return f"({(1 << (int(v) % 31)) | 1}u)" + return v + + +def odin_val(tag, v): + if tag == "ptr": return f"rawptr(uintptr({v}))" + if tag == "bool": return "true" + if tag == "enum": return f"E32({v})" + if tag == "c64": return f"complex64(complex({v}, {v}))" + if tag == "c128": return f"complex128(complex({v}, {v}))" + if tag == "bset": return "(BS{0, " + str(int(v) % 31) + "})" + return v + + +def c_ref(cp, var): + """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.""" + return cp.format(var) if "{}" in cp else f"{var}.{cp}" + + +def c_conds(t, var): + parts = [f"{c_ref(cp, var)} == ({c_val(k, v)})" for _op, cp, k, v in t.fields] + return " && ".join(parts) or "1" + + +def odin_setters(t, var): + if t.odin_set is not None: + return [x.replace("{}", var) for x in t.odin_set] + return [f"{var}.{op} = {odin_val(tag, v)}" for op, _cp, tag, v in t.fields] + + +def odin_getters(t, var): + if t.odin_get is not None: + return [(e.replace("{}", var), ev) for e, ev in t.odin_get] + out = [] + for op, _cp, tag, v in t.fields: + ot = SCALARS[tag][0] if tag in SCALARS else "f16" + ev = odin_val(tag, v) if tag in ("ptr", "bool", "enum", "c64", "c128", "bset") else f"{ot}({v})" + out.append((f"{var}.{op}", ev)) + return out + + +def mutated(tag, v): + """A value the checks MUST reject, for the mutation control.""" + if tag == "bool": return "false" + if tag == "ptr": return "rawptr(uintptr(999))" + if tag == "enum": return f"E32({int(v) + 1})" + if tag == "c64": return f"complex64(complex({int(v) + 1}, {v}))" + if tag == "c128": return f"complex128(complex({int(v) + 1}, {v}))" + if tag == "bset": return "(BS{2})" + return f"{float(v) + 1}" if "." in str(v) else f"{int(v) + 1}" + + +def leaf(path, tag, i): + return (path, path, tag, val(i, tag)) + + +def leaf2(odin_path, c_path, tag, v): + """A member spelled differently in the two languages -- matrix indexing, + or complex, where there is no common accessor.""" + return (odin_path, c_path, tag, v) + + +# ---------------------------------------------------------------- corpus + +def build(): + out = [] + + def add(*a, **k): + out.append(Ty(*a, **k)) + + # --- scalar arity 1..4, the merge and by-value/memory boundaries + combos = [ + ("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"), + ] + for tags in combos: + n = "s_" + "_".join(tags) + od = "struct { " + ", ".join(f"f{i}: {SCALARS[t][0]}" for i, t in enumerate(tags)) + " }" + cd = "struct { " + " ".join(f"{SCALARS[t][1]} f{i};" for i, t in enumerate(tags)) + " }" + add(n, od, cd, [leaf(f"f{i}", t, i) for i, t in enumerate(tags)], tier=tier_of(*tags)) + + # --- arrays: the same eightbytes from one declaration + for tag in ("f32", "f64", "i32", "i64", "i8", "f16", "enum", "i128"): + for cnt in (1, 2, 3, 4, 5): + n = f"a{cnt}_{tag}" + od = f"struct {{ a: [{cnt}]{SCALARS[tag][0]} }}" + cd = f"struct {{ {SCALARS[tag][1]} a[{cnt}]; }}" + add(n, od, cd, [leaf(f"a[{i}]", tag, i) for i in range(cnt)], tier=tier_of(tag)) + + # --- nesting: same leaves reached through another level + for a, b in (("f32", "f32"), ("f64", "f64"), ("i32", "f32"), ("f32", "i64"), + ("f16", "f16"), ("f16", "i32")): + add(f"n_{a}_{b}", + f"struct {{ i: struct {{ x: {SCALARS[a][0]}, y: {SCALARS[b][0]} }} }}", + f"struct {{ struct {{ {SCALARS[a][1]} x; {SCALARS[b][1]} y; }} i; }}", + [leaf("i.x", a, 0), leaf("i.y", b, 1)], tier=tier_of(a, b)) + add(f"n2_{a}_{b}", + f"struct {{ i: struct {{ x: {SCALARS[a][0]} }}, y: {SCALARS[b][0]} }}", + f"struct {{ struct {{ {SCALARS[a][1]} x; }} i; {SCALARS[b][1]} y; }}", + [leaf("i.x", a, 0), leaf("y", b, 1)], tier=tier_of(a, b)) + + # --- unions, and a union below the top level + for a, b in (("f32", "i32"), ("f64", "i64"), ("f32", "f32"), ("f64", "f32"), + ("f16", "i16"), ("f16", "f32")): + add(f"u_{a}_{b}", + f"struct #raw_union {{ x: {SCALARS[a][0]}, y: {SCALARS[b][0]} }}", + f"union {{ {SCALARS[a][1]} x; {SCALARS[b][1]} y; }}", + [leaf("x", a, 0)], tier=tier_of(a, b)) + add(f"su_{a}_{b}", + f"struct {{ u: struct #raw_union {{ x: {SCALARS[a][0]} }}, y: {SCALARS[b][0]} }}", + f"struct {{ union {{ {SCALARS[a][1]} x; }} u; {SCALARS[b][1]} y; }}", + [leaf("u.x", a, 0), leaf("y", b, 1)], tier=tier_of(a, b)) + + # --- homogeneous float aggregates and the shapes that disqualify them + for tag in ("f32", "f64", "f16"): + w = SCALARS[tag][0] + cw = SCALARS[tag][1] + add(f"hfa4_{tag}", + f"struct {{ a, b, c, d: {w} }}", + f"struct {{ {cw} a, b, c, d; }}", + [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], + tier=tier_of(tag)) + add(f"hfa5_{tag}", + f"struct {{ a, b, c, d, e: {w} }}", + f"struct {{ {cw} a, b, c, d, e; }}", + [leaf(x, tag, i) for i, x in enumerate("abcde")], tier=tier_of(tag)) + # zero-length array member -- disqualifies the HFA + add(f"zla_{tag}", + f"struct {{ z: [0]f32, a, b, c, d: {w} }}", + f"struct {{ float z[0]; {cw} a, b, c, d; }}", + [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], + tier=TIER_GNU) + add(f"zlat_{tag}", + f"struct {{ a, b, c, d: {w}, z: [0]f32 }}", + f"struct {{ {cw} a, b, c, d; float z[0]; }}", + [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], + tier=TIER_GNU) + # empty struct member -- does NOT disqualify it + add(f"esm_{tag}", + f"struct {{ e: struct {{}}, a, b, c, d: {w} }}", + f"struct {{ struct {{}} e; {cw} a, b, c, d; }}", + [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], + tier=TIER_GNU) + + # --- alignment: changes size and placement without changing any field type + for al in (16, 32): + add(f"al{al}", + f"struct #align({al}) {{ a, b, c: f64 }}", + f"struct __attribute__((aligned({al}))) {{ double a, b, c; }}", + [leaf("a", "f64", 0), leaf("b", "f64", 1), leaf("c", "f64", 2)], + 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; }", + [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; }", + [leaf("a", "i64", 0), leaf("b", "f32", 1)]) + add("pad_f32_f64", "struct { a: f32, b: f64 }", + "struct { float a; double b; }", + [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 = [("f32", 4, 16), ("f32", 2, 8), ("f64", 2, 16), ("i32", 4, 16), ("i8", 16, 16)] + for tag, n, _sz in VEC: + ct, cc = SCALARS[tag][0], SCALARS[tag][1] + add(f"v{n}_{tag}", + f"struct {{ v: #simd[{n}]{ct} }}", + f"struct {{ {cc} v __attribute__((vector_size({n} * sizeof({cc})))); }}", + [leaf(f"v[{i}]", tag, i) for i in range(n)], tier=TIER_GNU, + odin_set=["{}.v = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], + odin_get=[(f"simd.extract({{}}.v, {i})", f"{ct}({val(i, tag)})") for i in range(n)]) + # two vectors: an HVA on AAPCS64, memory on x86-64 + add("v4f32x2", + "struct { a, b: #simd[4]f32 }", + "struct { float a __attribute__((vector_size(16))), b __attribute__((vector_size(16))); }", + [leaf(f"a[{i}]", "f32", i) for i in range(4)] + + [leaf(f"b[{i}]", "f32", i + 4) for i in range(4)], tier=TIER_GNU, + odin_set=["{}.a = " + "{" + ", ".join(val(i, "f32") for i in range(4)) + "}", + "{}.b = " + "{" + ", ".join(val(i + 4, "f32") for i in range(4)) + "}"], + odin_get=[(f"simd.extract({{}}.a, {i})", f"f32({val(i, 'f32')})") for i in range(4)] + + [(f"simd.extract({{}}.b, {i})", f"f32({val(i + 4, 'f32')})") for i in range(4)]) + # a vector beside a scalar: homogeneous no longer + add("v4f32_i64", + "struct { a: #simd[4]f32, b: i64 }", + "struct { float a __attribute__((vector_size(16))); int64_t b; }", + [leaf(f"a[{i}]", "f32", i) for i in range(4)] + [leaf("b", "i64", 4)], + tier=TIER_GNU, + odin_set=["{}.a = " + "{" + ", ".join(val(i, "f32") for i in range(4)) + "}", + f"{{}}.b = {val(4, 'i64')}"], + odin_get=[(f"simd.extract({{}}.a, {i})", f"f32({val(i, 'f32')})") for i in range(4)] + + [("{}.b", f"i64({val(4, 'i64')})")]) + + # --- 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 w1, w2 in ((3, 5), (1, 31), (17, 15)): + # the value has to fit the declared width, so it is derived from it + # the value must fit the width AND leave room for the mutation control + va, vb = str(min(5, (1 << w1) - 1) if w1 > 1 else 0), str(min(9, (1 << w2) - 1)) + add(f"bf_{w1}_{w2}", + f"bit_field u32 {{ a: u32 | {w1}, b: u32 | {w2} }}", + f"struct {{ unsigned a : {w1}; unsigned b : {w2}; }}", + [leaf2("a", "a", "u32", va), leaf2("b", "b", "u32", vb)]) + add("bff_f32", + "struct { f: f32, b: bit_field u32 { a: u32 | 3 } }", + "struct { float f; struct { unsigned a : 3; } b; }", + [leaf("f", "f32", 0), leaf2("b.a", "b.a", "u32", "5")]) + + # --- matrix, which lowers to an array with its own alignment + add("m22_f32", "struct { m: matrix[2,2]f32 }", + "struct { float m[4] __attribute__((aligned(16))); }", + [leaf2(f"m[{i % 2}, {i // 2}]", f"m[{i}]", "f32", val(i, "f32")) for i in range(4)], + tier=TIER_GNU) + + # NOTE: `complex64`/`complex128` are deliberately absent. Their members have + # no common accessor -- Odin spells it `real(x)`, C spells it `__real__ x`, a + # prefix operator rather than a member -- so a per-field check cannot be + # generated from one path. Measured separately as agreeing with clang on + # x86-64, aarch64 and riscv64; add them if the accessor problem is solved. + + # --- 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]; }", + [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]; }", + [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))); }", + [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; }", + [leaf("a.x", "f32", 0), leaf("a.y", "f32", 1)]) + add("ua_arr", + "struct #raw_union { a: [4]f32, b: [2]f64 }", + "union { float a[4]; double b[2]; }", + [leaf(f"a[{i}]", "f32", i) for i in range(4)]) + + # --- 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; }", + [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; }", + [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; }", [], tier=TIER_GNU) + add("zarr", "struct { z: [0]f32 }", "struct { float z[0]; }", [], tier=TIER_GNU) + + # --- an array OF vectors, and a vector wider than one register + add("av2_f32", + "struct { a: [2]#simd[4]f32 }", + "struct { rx_v4f a[2]; }", + [], tier=TIER_GNU, + odin_set=["{}.a[0] = {1.5, 2.5, 3.5, 4.5}", "{}.a[1] = {5.5, 6.5, 7.5, 8.5}"], + odin_get=[("simd.extract({}.a[0], 0)", "f32(1.5)"), + ("simd.extract({}.a[1], 3)", "f32(8.5)")]) + add("v8_f32", + "struct { v: #simd[8]f32 }", + "struct { float v __attribute__((vector_size(32))); }", + [leaf(f"v[{i}]", "f32", i) for i in range(8)], tier=TIER_GNU, + odin_set=["{}.v = " + "{" + ", ".join(val(i, "f32") for i in range(8)) + "}"], + odin_get=[(f"simd.extract({{}}.v, {i})", f"f32({val(i, 'f32')})") for i in range(8)]) + + # --- large, past every by-value threshold + add("big", "struct { a: [8]i64 }", "struct { int64_t a[8]; }", + [leaf(f"a[{i}]", "i64", i) for i in range(8)]) + + return out + + +# ---------------------------------------------------------------- emit + +GUARD = {TIER_CORE: None, TIER_GNU: "ABI_TIER_GNU", TIER_F16: "ABI_TIER_F16", + TIER_I128: "ABI_TIER_I128"} + +C_HEAD = """\ +/* GENERATED by tests/abi/gen.py -- 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. */ +#if defined(__GNUC__) +#define ABI_TIER_GNU 1 +#endif +#if defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER) +#define ABI_TIER_F16 1 +#endif +#if defined(__SIZEOF_INT128__) +#define ABI_TIER_I128 1 +#endif + +/* 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))); +#endif +""" + +ODIN_HEAD = """\ +// GENERATED by tests/abi/gen.py -- 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. +// +// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the +// raw aggregate where clang coerces per the psABI -- so 111 of the types here +// fail. That is one defect, not 111, and leaving it on would drown every other +// signal. 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" +""" + + +def emit_c(types): + o = io.StringIO() + o.write(C_HEAD) + for t in types: + g = GUARD[t.tier] + if g: + o.write(f"\n#ifdef {g}\n") + o.write(f"\ntypedef {t.c} {t.name};\n") + # _arg: return the argument that FOLLOWS the aggregate + o.write(f"double {t.name}_arg({t.name} s, double next) {{ (void)s; return next; }}\n") + # _chk: every field, so a wrong offset is caught as well as a wrong register + o.write(f"int {t.name}_chk({t.name} s) {{ return ({c_conds(t, 's')}) ? 0 : 1; }}\n") + # _ret: return position + o.write(f"{t.name} {t.name}_ret(void) {{ {t.name} s; ") + o.write("".join(f"{c_ref(cp, 's')} = ({c_val(k, v)}); " for _op, cp, k, v in t.fields)) + o.write("return s; }\n") + # _ex: the aggregate after the argument registers are gone + o.write(f"double {t.name}_ex(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e," + f" int64_t f, int64_t o, double g, double h, double i, double j, double k," + f" double l, double m, double n, {t.name} s, double next) {{\n") + o.write("\t(void)a;(void)b;(void)c;(void)d;(void)e;(void)f;(void)o;(void)g;(void)h;\n") + o.write("\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. + ints = ", ".join(f"int64_t q{i}" for i in range(9)) + dbls = ", ".join(f"double w{i}" for i in range(9)) + o.write(f"double {t.name}_ex2({ints}, {dbls}, {t.name} s, double next) {{\n\t") + o.write("".join(f"(void)q{i};" for i in range(9))) + o.write("".join(f"(void)w{i};" for i in range(9))) + o.write("(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 + o.write(f"double {t.name}_two({t.name} s1, {t.name} s2, double next) {{\n") + o.write(f"\tif (!({c_conds(t, 's1')})) return -1;\n") + o.write(f"\tif (!({c_conds(t, 's2')})) return -2;\n\treturn next;\n}}\n") + # _back: the other direction -- C calls an exported Odin callee, which is + # what a callback does and what nothing else here covers + o.write(f"extern double o_{t.name}_take({t.name} s, double next);\n") + o.write(f"extern {t.name} o_{t.name}_make(void);\n") + o.write(f"int {t.name}_back(void) {{\n\t{t.name} s; ") + o.write("".join(f"{c_ref(cp, 's')} = ({c_val(k, v)}); " for _op, cp, k, v in t.fields)) + o.write(f"\n\tif (o_{t.name}_take(s, 7) != 7) return 1;\n") + o.write(f"\t{t.name} r = o_{t.name}_make();\n") + o.write(f"\tif (!({c_conds(t, 'r')})) return 2;\n\treturn 0;\n}}\n") + # _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 t.fields: + o.write(f"double {t.name}_va(int n, ...) {{\n\tva_list ap; va_start(ap, n);\n") + o.write(f"\t{t.name} s = va_arg(ap, {t.name});\n") + o.write("\tdouble next = va_arg(ap, double);\n\tva_end(ap);\n") + o.write(f"\treturn ({c_conds(t, 's')}) ? next : -1;\n}}\n") + if g: + o.write(f"\n#endif /* {g} */\n") + return o.getvalue() + + +def emit_odin(types): + o = io.StringIO() + o.write(ODIN_HEAD) + for t in types: + g = GUARD[t.tier] + w = f"when {g} {{\n" if g else "" + ind = "\t" if g else "" + o.write("\n" + w) + o.write(f"{ind}{t.name} :: {t.odin}\n") + o.write(f'{ind}@(default_calling_convention="c")\n{ind}foreign lib {{\n') + o.write(f"{ind}\t{t.name}_arg :: proc(s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_chk :: proc(s: {t.name}) -> i32 ---\n") + o.write(f"{ind}\t{t.name}_ret :: proc() -> {t.name} ---\n") + o.write(f"{ind}\t{t.name}_ex :: proc(a, b, c, d, e, f, o: i64, g, h, i, j, k, l, m, n: f64," + f" s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_ex2 :: proc(q0, q1, q2, q3, q4, q5, q6, q7, q8: i64," + f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") + if t.fields: + o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") + o.write(f"{ind}}}\n") + # the callees C calls back into: the direction a callback uses + o.write(f"{ind}@(export) o_{t.name}_take :: proc \"c\" (s: {t.name}, next: f64) -> f64 {{\n") + for expr, ev in odin_getters(t, "s"): + o.write(f"{ind}\tif {expr} != {ev} {{ return -1 }}\n") + o.write(f"{ind}\treturn next\n{ind}}}\n") + o.write(f"{ind}@(export) o_{t.name}_make :: proc \"c\" () -> {t.name} {{\n{ind}\ts: {t.name}\n") + for st in odin_setters(t, "s"): + o.write(f"{ind}\t{st}\n") + o.write(f"{ind}\treturn s\n{ind}}}\n") + o.write(f"{ind}@(test)\n{ind}test_{t.name} :: proc(t: ^testing.T) {{\n") + o.write(f"{ind}\ts: {t.name}\n") + for st in odin_setters(t, "s"): + o.write(f"{ind}\t{st}\n") + # 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 t.fields and t.odin_set is None: + op, _cp, tag, v = t.fields[0] + o.write(f"{ind}\twhen ABI_MUTATE {{ s.{op} = {mutated(tag, v)} }}\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_arg(s, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_chk(s), i32(0))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex2(1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9, s, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_two(s, s, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_back(), i32(0))\n") + if t.fields: + o.write(f"{ind}\twhen ABI_VARARGS {{\n") + o.write(f"{ind}\t\ttesting.expect_value(t, {t.name}_va(1, s, f64(7)), f64(7))\n") + o.write(f"{ind}\t}}\n") + o.write(f"{ind}\tr := {t.name}_ret()\n") + if not odin_getters(t, "r"): + o.write(f"{ind}\t_ = r\n") + for expr, ev in odin_getters(t, "r"): + o.write(f"{ind}\ttesting.expect_value(t, {expr}, {ev})\n") + o.write(f"{ind}}}\n") + if g: + o.write("}\n") + return o.getvalue() + + +MAIN_HEAD = """\ +// GENERATED by tests/abi/gen.py -- 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. +// +// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the +// raw aggregate where clang coerces per the psABI -- so 111 of the types here +// fail. That is one defect, not 111, and leaving it on would drown every other +// signal. Turn it on with `-define:ABI_VARARGS=true` to measure it. +ABI_VARARGS :: #config(ABI_VARARGS, false) + + +foreign import lib "../abi_corpus_c.o" +""" + + +def emit_main(types): + o = io.StringIO() + o.write(MAIN_HEAD) + body = io.StringIO() + seen = [] + for t in types: + g = GUARD[t.tier] + w = f"when {g} {{\n" if g else "" + ind = "\t" if g else "" + o.write("\n" + w) + o.write(f"{ind}{t.name} :: {t.odin}\n") + o.write(f'{ind}@(default_calling_convention="c")\n{ind}foreign lib {{\n') + o.write(f"{ind}\t{t.name}_arg :: proc(s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_chk :: proc(s: {t.name}) -> i32 ---\n") + o.write(f"{ind}\t{t.name}_ret :: proc() -> {t.name} ---\n") + o.write(f"{ind}\t{t.name}_ex :: proc(a, b, c, d, e, f, o: i64, g, h, i, j, k, l, m, n: f64," + f" s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_ex2 :: proc(q0, q1, q2, q3, q4, q5, q6, q7, q8: i64," + f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") + if t.fields: + o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") + o.write(f"{ind}}}\n") + # the callees C calls back into: the direction a callback uses + o.write(f"{ind}@(export) o_{t.name}_take :: proc \"c\" (s: {t.name}, next: f64) -> f64 {{\n") + for expr, ev in odin_getters(t, "s"): + o.write(f"{ind}\tif {expr} != {ev} {{ return -1 }}\n") + o.write(f"{ind}\treturn next\n{ind}}}\n") + o.write(f"{ind}@(export) o_{t.name}_make :: proc \"c\" () -> {t.name} {{\n{ind}\ts: {t.name}\n") + for st in odin_setters(t, "s"): + o.write(f"{ind}\t{st}\n") + o.write(f"{ind}\treturn s\n{ind}}}\n") + o.write(f"{ind}check_{t.name} :: proc \"contextless\" () -> i32 {{\n") + o.write(f"{ind}\ts: {t.name}\n") + for st in odin_setters(t, "s"): + o.write(f"{ind}\t{st}\n") + o.write(f"{ind}\tif {t.name}_arg(s, 7) != 7 {{ return 1 }}\n") + o.write(f"{ind}\tif {t.name}_chk(s) != 0 {{ return 1 }}\n") + o.write(f"{ind}\tif {t.name}_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7) != 7 {{ return 1 }}\n") + o.write(f"{ind}\tr := {t.name}_ret()\n") + if not odin_getters(t, "r"): + o.write(f"{ind}\t_ = r\n") + for expr, ev in odin_getters(t, "r"): + o.write(f"{ind}\tif {expr} != {ev} {{ return 1 }}\n") + o.write(f"{ind}\treturn 0\n{ind}}}\n") + if g: + o.write("}\n") + idx = len(seen) + 1 + seen.append(t.name) + chk = f"if {idx} > ABI_SKIP && check_{t.name}() != 0 {{ return {idx} }}" + body.write(f"\t{'when ' + g + ' { ' if g else ''}{chk}{' }' if g else ''}\n") + o.write("\n@(export)\nprobe_main :: proc \"c\" () -> i32 {\n") + o.write(body.getvalue()) + o.write("\treturn 0\n}\n") + o.write("\n// index -> name\n") + for i, n in enumerate(seen): + o.write(f"// {i+1}\t{n}\n") + return o.getvalue() + + +if __name__ == "__main__": + import os, sys + # Written into the caller's build directory, not the source tree: nothing + # generated is checked in, so the two languages cannot drift apart. + here = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + ts = build() + open(os.path.join(here, "abi_corpus.c"), "w").write(emit_c(ts)) + open(os.path.join(here, "abi_corpus.odin"), "w").write(emit_odin(ts)) + open(os.path.join(here, "abi_main.odin"), "w").write(emit_main(ts)) + print(f"{len(ts)} types, {sum(7 + (1 if t.fields else 0) for t in ts)} C functions, {len(ts) * 2} Odin callees") diff --git a/tests/abi/run.bat b/tests/abi/run.bat new file mode 100644 index 000000000..ac93c9748 --- /dev/null +++ b/tests/abi/run.bat @@ -0,0 +1,22 @@ +@echo off + +REM The ABI comparator. Every check is "Odin agrees with the platform C compiler" + +if not exist "build\" mkdir build +pushd build + +set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused-defineables + +@echo on + +python3 ..\gen.py . || exit /b + +REM -w because the corpus deliberately uses zero-length arrays and empty +REM structs; both are the extensions under test. +clang -c abi_corpus.c -o abi_corpus_c.o -w || exit /b +..\..\..\odin test abi_corpus.odin %COMMON% || exit /b + +@echo off + +popd +rmdir /S /Q build diff --git a/tests/abi/run.sh b/tests/abi/run.sh new file mode 100755 index 000000000..4c92b4634 --- /dev/null +++ b/tests/abi/run.sh @@ -0,0 +1,49 @@ +#!/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}" +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" + +# Ask the C compiler which tiers it actually has, the Odin side must have the same answer or +# it references symbols the C side never emitted ( eg `__int128` does not exist on i386). +macros=$($CLANG $CC_TARGET -dM -E -x c /dev/null 2>/dev/null) +tier() { case "$macros" in *"$1"*) echo true ;; *) echo false ;; esac; } +TIERS="-define:ABI_TIER_GNU=$(tier __GNUC__) -define:ABI_TIER_F16=$(tier __FLT16_MANT_DIG__) -define:ABI_TIER_I128=$(tier __SIZEOF_INT128__)" + +rm -rf "$here/build" +mkdir -p "$here/build" +trap 'rm -rf "$here/build"' EXIT # also on failure, where `set -e` would skip it +pushd "$here/build" > /dev/null + +set -x + +python3 ../gen.py . + +# `-w` because the corpus deliberately uses zero-length arrays and empty +# structs; both are the extensions under test. +$CLANG $CC_TARGET -c abi_corpus.c -o abi_corpus_c.o -w +$ODIN test abi_corpus.odin $COMMON $ODIN_TARGET $TIERS "$@" + +set +x + +popd > /dev/null From 8470c6eda2cebcba80ee2f4ce289d86618c605ae Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:08:26 -0700 Subject: [PATCH 02/32] i386 64-bit scalar alignment --- src/types.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/types.cpp b/src/types.cpp index 13d335348..942917e30 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -4539,7 +4539,13 @@ 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); + i64 max_align = build_context.max_align; + if (build_context.metrics.arch == TargetArch_i386 && + build_context.metrics.os != TargetOs_windows) { + // the i386 System V psABI aligns every scalar to at most 4, unlike Windows + max_align = gb_min(max_align, 4); + } + return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, 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) { From c2389067d24c48979785d8bd9627ee7fbef9722d Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:11:39 -0700 Subject: [PATCH 03/32] arm64 HFA eligibility; byval stack slot align on i386, amd64 + callsite; sse eightbyte merge --- src/llvm_abi.cpp | 85 ++++++++++++++++++++++++++++----------- src/llvm_backend_proc.cpp | 5 +++ 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 6a60b64c0..326e9c50f 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -33,9 +33,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); @@ -785,7 +791,7 @@ namespace lbAbiAmd64SysV { 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); @@ -796,7 +802,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); } @@ -980,6 +986,23 @@ namespace lbAbiAmd64SysV { return reg_classes; } + // An SSE class that fills a whole eightbyte from offset 0, so nothing narrower + // starting at offset 0 can add to it. + gb_internal bool sse_class_covers_eightbyte(RegClass c) { + return c == RegClass_SSEDs || c == RegClass_SSEInt64; + } + // An SSE class positioned at offset 0 of its eightbyte. The `v` classes sit at + // offset 4 and are therefore DISJOINT from a 4-byte class at offset 0. + gb_internal bool sse_class_at_offset_zero(RegClass c) { + switch (c) { + case RegClass_SSEHs: case RegClass_SSEFs: case RegClass_SSEDs: + case RegClass_SSEInt8: case RegClass_SSEInt16: + case RegClass_SSEInt32: case RegClass_SSEInt64: + return true; + } + return false; + } + gb_internal void unify(Array *cls, i64 i, RegClass const newv) { RegClass const oldv = (*cls)[cast(isize)i]; if (oldv == newv) { @@ -1013,6 +1036,13 @@ namespace lbAbiAmd64SysV { case RegClass_SSEInt64: return; } + } else if (sse_class_covers_eightbyte(oldv) && sse_class_at_offset_zero(newv)) { + // The members OVERLAP -- a union. Last-writer-wins would pass + // `union{f64, f32}` as a 4-byte float and lose the top half. Restricted + // to a full-eightbyte old class against an offset-zero new one, because + // `struct{f32, f16}` is Fs then Hv at offset 4, which is disjoint and + // must still combine rather than pick. + return; } (*cls)[cast(isize)i] = to_write; @@ -1419,6 +1449,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; } @@ -1451,6 +1485,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; @@ -1488,6 +1523,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; @@ -1499,6 +1538,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)) { @@ -1515,6 +1558,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)) { @@ -1556,20 +1604,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(); @@ -1616,12 +1656,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); diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index be1b3724f..d3e0e01ea 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -1033,6 +1033,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; } From 1638bd7e9ce0a50d682c3e513e807fb58793efe3 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:16:51 -0700 Subject: [PATCH 04/32] fix windows ci --- tests/abi/cross.sh | 10 +++++----- tests/abi/gen.py | 33 +++++++++++++++++++++++---------- tests/abi/run.bat | 17 ++++++++++++++++- tests/abi/run.sh | 10 +++++----- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 8d618e425..399392ff3 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -51,15 +51,15 @@ _start: *) echo "no start stub for $TARGET" >&2; exit 2 ;; esac -# Ask the C compiler which tiers it actually has, the Odin side must have the same answer or -# it references symbols the C side never emitted ( eg `__int128` does not exist on i386). -macros=$($CLANG --target="$TRIPLE" -dM -E -x c /dev/null 2>/dev/null) -tier() { case "$macros" in *"$1"*) echo true ;; *) echo false ;; esac; } -TIERS="-define:ABI_TIER_GNU=$(tier __GNUC__) -define:ABI_TIER_F16=$(tier __FLT16_MANT_DIG__) -define:ABI_TIER_I128=$(tier __SIZEOF_INT128__)" rm -rf build-cross mkdir -p build-cross/p python3 gen.py 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 diff --git a/tests/abi/gen.py b/tests/abi/gen.py index ee315fa4f..bf2d8b50b 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -398,6 +398,25 @@ def build(): GUARD = {TIER_CORE: None, TIER_GNU: "ABI_TIER_GNU", TIER_F16: "ABI_TIER_F16", TIER_I128: "ABI_TIER_I128"} +# The tier conditions live here. The corpus is guarded by them, `tiers.c` reports them. +TIER_COND = { + TIER_GNU: "defined(__GNUC__)", + TIER_F16: "defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER)", + TIER_I128: "defined(__SIZEOF_INT128__)", +} + + +def emit_tiers_c(): + o = io.StringIO() + o.write("/* GENERATED by tests/abi/gen.py -- do not edit.\n" + " Preprocess this and grep the markers: it answers which tiers the C\n" + " compiler actually has, so the Odin side can be gated by the same\n" + " answer rather than by a restatement of the condition. */\n") + for tier, cond in TIER_COND.items(): + o.write(f"#if {cond}\nABI_YES_{GUARD[tier].replace('ABI_TIER_', '')}\n#endif\n") + return o.getvalue() + + C_HEAD = """\ /* GENERATED by tests/abi/gen.py -- do not edit. */ #include @@ -405,15 +424,7 @@ C_HEAD = """\ /* Tier guards. A target whose C compiler lacks an extension still runs the core corpus; the Odin side is gated by the matching -define. */ -#if defined(__GNUC__) -#define ABI_TIER_GNU 1 -#endif -#if defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER) -#define ABI_TIER_F16 1 -#endif -#if defined(__SIZEOF_INT128__) -#define ABI_TIER_I128 1 -#endif +@TIER_DEFINES@ /* An enum with an explicit wide enumerator, so it is int-sized rather than whatever the compiler picks for a small one. */ @@ -472,7 +483,8 @@ foreign import lib "abi_corpus_c.o" def emit_c(types): o = io.StringIO() - o.write(C_HEAD) + defines = "".join(f"#if {c}\n#define {GUARD[t]} 1\n#endif\n" for t, c in TIER_COND.items()) + o.write(C_HEAD.replace("@TIER_DEFINES@", defines.rstrip())) for t in types: g = GUARD[t.tier] if g: @@ -694,4 +706,5 @@ if __name__ == "__main__": open(os.path.join(here, "abi_corpus.c"), "w").write(emit_c(ts)) open(os.path.join(here, "abi_corpus.odin"), "w").write(emit_odin(ts)) open(os.path.join(here, "abi_main.odin"), "w").write(emit_main(ts)) + open(os.path.join(here, "tiers.c"), "w").write(emit_tiers_c()) print(f"{len(ts)} types, {sum(7 + (1 if t.fields else 0) for t in ts)} C functions, {len(ts) * 2} Odin callees") diff --git a/tests/abi/run.bat b/tests/abi/run.bat index ac93c9748..97b9fff53 100644 --- a/tests/abi/run.bat +++ b/tests/abi/run.bat @@ -11,10 +11,25 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused python3 ..\gen.py . || 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 -c abi_corpus.c -o abi_corpus_c.o -w || exit /b -..\..\..\odin test abi_corpus.odin %COMMON% || exit /b +..\..\..\odin test abi_corpus.odin %COMMON% %TIERS% || exit /b @echo off diff --git a/tests/abi/run.sh b/tests/abi/run.sh index 4c92b4634..f2b9cac42 100755 --- a/tests/abi/run.sh +++ b/tests/abi/run.sh @@ -24,11 +24,6 @@ COMMON="-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused-de CC_TARGET=""; [ -n "$TRIPLE" ] && CC_TARGET="--target=$TRIPLE" ODIN_TARGET=""; [ -n "$TARGET" ] && ODIN_TARGET="-target:$TARGET" -# Ask the C compiler which tiers it actually has, the Odin side must have the same answer or -# it references symbols the C side never emitted ( eg `__int128` does not exist on i386). -macros=$($CLANG $CC_TARGET -dM -E -x c /dev/null 2>/dev/null) -tier() { case "$macros" in *"$1"*) echo true ;; *) echo false ;; esac; } -TIERS="-define:ABI_TIER_GNU=$(tier __GNUC__) -define:ABI_TIER_F16=$(tier __FLT16_MANT_DIG__) -define:ABI_TIER_I128=$(tier __SIZEOF_INT128__)" rm -rf "$here/build" mkdir -p "$here/build" @@ -39,6 +34,11 @@ set -x python3 ../gen.py . +# 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 -c abi_corpus.c -o abi_corpus_c.o -w From b92e8af434e852a2e0513ef21638b5518b804411 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:29:48 -0700 Subject: [PATCH 05/32] i128 unwrap aggregate single member structs --- src/llvm_abi.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 326e9c50f..da72ecd67 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -724,16 +724,22 @@ 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, where 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); + 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; } From f5ab81cfa1e7b4ef6078d9072bafd4c2ea10b2ec Mon Sep 17 00:00:00 2001 From: kalsprite Date: Wed, 12 Aug 2026 23:47:27 -0700 Subject: [PATCH 06/32] simd fix; generator fix --- src/llvm_abi.cpp | 10 ++++++++-- tests/abi/gen.py | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index da72ecd67..37df13b2c 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -726,13 +726,19 @@ 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, where a bare `i128` does not -- clang - // emits `byval align 16` for the struct and a plain `i128` for the scalar. + // 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) { 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; diff --git a/tests/abi/gen.py b/tests/abi/gen.py index bf2d8b50b..4a9c86163 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -372,10 +372,13 @@ def build(): add("zarr", "struct { z: [0]f32 }", "struct { float z[0]; }", [], 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. add("av2_f32", "struct { a: [2]#simd[4]f32 }", "struct { rx_v4f a[2]; }", - [], tier=TIER_GNU, + [leaf2("", f"a[{i // 4}][{i % 4}]", "f32", f"{i + 1}.5") for i in range(8)], + tier=TIER_GNU, odin_set=["{}.a[0] = {1.5, 2.5, 3.5, 4.5}", "{}.a[1] = {5.5, 6.5, 7.5, 8.5}"], odin_get=[("simd.extract({}.a[0], 0)", "f32(1.5)"), ("simd.extract({}.a[1], 3)", "f32(8.5)")]) From e621a924f5da07064eade2f7c1497b4f8b8bda96 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 00:04:19 -0700 Subject: [PATCH 07/32] darwin align(16) max --- src/llvm_abi.cpp | 6 +++++- src/types.cpp | 7 ++++++- tests/abi/gen.py | 14 +++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 37df13b2c..07a86e05d 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -335,7 +335,11 @@ gb_internal i64 lb_alignof(LLVMTypeRef type) { i64 elem_size = lb_sizeof(elem); i64 count = LLVMGetVectorSize(type); i64 size = count * elem_size; - return gb_clamp(next_pow2(size), 1, build_context.max_simd_align); + i64 max_align = build_context.max_simd_align; + if (build_context.metrics.os == TargetOs_darwin && build_context.metrics.arch == TargetArch_amd64) { + max_align = gb_min(max_align, 16); // see type_align_of_internal + } + return gb_clamp(next_pow2(size), 1, max_align); } } diff --git a/src/types.cpp b/src/types.cpp index 942917e30..dda2ea2a1 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -4527,7 +4527,12 @@ 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); + i64 max_align = build_context.max_simd_align*2; + if (build_context.metrics.os == TargetOs_darwin && build_context.metrics.arch == TargetArch_amd64) { + // Darwin guarantees only 16-byte stack alignment + max_align = gb_min(max_align, 16); + } + return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, max_align); } case Type_Matrix: diff --git a/tests/abi/gen.py b/tests/abi/gen.py index 4a9c86163..f372e5add 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -190,7 +190,10 @@ def build(): # --- nesting: same leaves reached through another level for a, b in (("f32", "f32"), ("f64", "f64"), ("i32", "f32"), ("f32", "i64"), - ("f16", "f16"), ("f16", "i32")): + ("f16", "f16"), ("f16", "i32"), + # a lone f32 in eightbyte 0 reached through a level, then an f64: + # the shape #7292 was about + ("f32", "f64")): add(f"n_{a}_{b}", f"struct {{ i: struct {{ x: {SCALARS[a][0]}, y: {SCALARS[b][0]} }} }}", f"struct {{ struct {{ {SCALARS[a][1]} x; {SCALARS[b][1]} y; }} i; }}", @@ -211,6 +214,13 @@ def build(): f"struct {{ u: struct #raw_union {{ x: {SCALARS[a][0]} }}, y: {SCALARS[b][0]} }}", f"struct {{ union {{ {SCALARS[a][1]} x; }} u; {SCALARS[b][1]} y; }}", [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(f"su2_{a}_{b}", + f"struct {{ u: struct #raw_union {{ x, y: {SCALARS[a][0]} }}, z: {SCALARS[b][0]} }}", + f"struct {{ union {{ {SCALARS[a][1]} x, y; }} u; {SCALARS[b][1]} z; }}", + [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 ("f32", "f64", "f16"): @@ -477,6 +487,7 @@ ABI_MUTATE :: #config(ABI_MUTATE, false) ABI_VARARGS :: #config(ABI_VARARGS, false) + E32 :: enum i32 { LO = 0, HI = 0x7fffffff } BS :: bit_set[0..<31; u32] @@ -635,6 +646,7 @@ ABI_SKIP :: #config(ABI_SKIP, 0) ABI_VARARGS :: #config(ABI_VARARGS, false) + foreign import lib "../abi_corpus_c.o" """ From 039ee3b63ebea1a8af7835921fdc4a92112e5b52 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 00:15:43 -0700 Subject: [PATCH 08/32] add alignment canaries --- tests/abi/gen.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/abi/gen.py b/tests/abi/gen.py index f372e5add..4c00f318b 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -254,12 +254,24 @@ def build(): tier=TIER_GNU) # --- alignment: changes size and placement without changing any field type + for al in (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(f"aln{al}", + f"struct #align({al}) {{ a: i8, b: i32 }}", + f"struct __attribute__((aligned({al}))) {{ int8_t a; int32_t b; }}", + [leaf("a", "i8", 0), leaf("b", "i32", 1)], tier=TIER_GNU) for al in (16, 32): add(f"al{al}", f"struct #align({al}) {{ a, b, c: f64 }}", f"struct __attribute__((aligned({al}))) {{ double a, b, c; }}", [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))); }", + [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; }", [leaf("a", "i8", 0), leaf("b", "i32", 1), leaf("c", "i64", 2)], tier=TIER_GNU) @@ -392,6 +404,18 @@ def build(): odin_set=["{}.a[0] = {1.5, 2.5, 3.5, 4.5}", "{}.a[1] = {5.5, 6.5, 7.5, 8.5}"], odin_get=[("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. + add("v8_off", + "struct { a: i8, v: #simd[8]f32 }", + "struct { int8_t a; float v __attribute__((vector_size(32))); }", + [leaf("a", "i8", 0)] + [leaf(f"v[{i}]", "f32", i) for i in range(8)], + tier=TIER_GNU, + odin_set=["{}.a = 3", "{}.v = " + "{" + ", ".join(val(i, "f32") for i in range(8)) + "}"], + odin_get=[("{}.a", "i8(3)")] + + [(f"simd.extract({{}}.v, {i})", f"f32({val(i, 'f32')})") for i in range(8)]) add("v8_f32", "struct { v: #simd[8]f32 }", "struct { float v __attribute__((vector_size(32))); }", @@ -540,6 +564,31 @@ def emit_c(types): o.write(f"\n\tif (o_{t.name}_take(s, 7) != 7) return 1;\n") o.write(f"\t{t.name} r = o_{t.name}_make();\n") o.write(f"\tif (!({c_conds(t, 'r')})) return 2;\n\treturn 0;\n}}\n") + # _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. + o.write(f"double {t.name}_can(int64_t q0, int64_t q1, int64_t q2, int64_t q3," + f" int64_t q4, int64_t q5, int64_t q6, double w0, double w1, double w2," + f" double w3, double w4, double w5, double w6, double w7," + f" int64_t before, {t.name} s, int64_t after, double last) {{\n") + o.write("\t(void)q0;(void)q1;(void)q2;(void)q3;(void)q4;(void)q5;(void)q6;\n") + o.write("\t(void)w0;(void)w1;(void)w2;(void)w3;(void)w4;(void)w5;(void)w6;(void)w7;\n") + o.write("\tif (before != 0x1111111111111111LL) return -1;\n") + o.write("\tif (after != 0x2222222222222222LL) return -2;\n") + o.write(f"\tif (!({c_conds(t, 's')})) return -3;\n\treturn last;\n}}\n") + # _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. + ints2 = ", ".join(f"int64_t p{i}" for i in range(11)) + o.write(f"double {t.name}_can2({ints2}, int64_t before, {t.name} s," + f" int64_t after, double last) {{\n\t") + o.write("".join(f"(void)p{i};" for i in range(11))) + o.write("\n\tif (before != 0x1111111111111111LL) return -1;\n") + o.write("\tif (after != 0x2222222222222222LL) return -2;\n") + o.write(f"\tif (!({c_conds(t, 's')})) return -3;\n\treturn last;\n}}\n") # _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 @@ -573,6 +622,11 @@ def emit_odin(types): f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") + o.write(f"{ind}\t{t.name}_can2 :: proc(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10: i64," + f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_can :: proc(q0, q1, q2, q3, q4, q5, q6: i64," + f" w0, w1, w2, w3, w4, w5, w6, w7: f64," + f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") if t.fields: o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") o.write(f"{ind}}}\n") @@ -599,6 +653,8 @@ def emit_odin(types): o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7), f64(7))\n") o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex2(1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9, s, 7), f64(7))\n") o.write(f"{ind}\ttesting.expect_value(t, {t.name}_two(s, s, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_can(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n") + o.write(f"{ind}\ttesting.expect_value(t, {t.name}_can2(1,2,3,4,5,6,7,8,9,10,11, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n") o.write(f"{ind}\ttesting.expect_value(t, {t.name}_back(), i32(0))\n") if t.fields: o.write(f"{ind}\twhen ABI_VARARGS {{\n") @@ -672,6 +728,11 @@ def emit_main(types): f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") + o.write(f"{ind}\t{t.name}_can2 :: proc(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10: i64," + f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") + o.write(f"{ind}\t{t.name}_can :: proc(q0, q1, q2, q3, q4, q5, q6: i64," + f" w0, w1, w2, w3, w4, w5, w6, w7: f64," + f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") if t.fields: o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") o.write(f"{ind}}}\n") From b25497ecd5506f4e44507e9fa5a02ea4a7adfbab Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 20:20:00 -0700 Subject: [PATCH 09/32] matrix, vector align --- src/types.cpp | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/types.cpp b/src/types.cpp index dda2ea2a1..b51e44a45 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1605,16 +1605,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); } @@ -4527,12 +4519,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 - i64 max_align = build_context.max_simd_align*2; - if (build_context.metrics.os == TargetOs_darwin && build_context.metrics.arch == TargetArch_amd64) { - // Darwin guarantees only 16-byte stack alignment - max_align = gb_min(max_align, 16); - } - return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, max_align); + return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, build_context.max_simd_align); } case Type_Matrix: From 5c5bb381150651dfb9725fb2170dd7408b80f44f Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 20:23:05 -0700 Subject: [PATCH 10/32] enhance abi harness; dont delete build dir --- .gitignore | 1 + tests/abi/cross.sh | 2 +- tests/abi/gen.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- tests/abi/run.bat | 5 +++-- tests/abi/run.sh | 3 ++- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 776c1fe12..b867398c4 100644 --- a/.gitignore +++ b/.gitignore @@ -308,6 +308,7 @@ build.sh *.rdi tests/issues/build/* tests/abi/build/* +tests/abi/build-cross/* misc/featuregen/featuregen # Clangd stuff diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 399392ff3..181fbf470 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -52,6 +52,7 @@ _start: esac +# cleaned BEFORE, not after -- the driver is left in place to inspect rm -rf build-cross mkdir -p build-cross/p python3 gen.py build-cross @@ -119,5 +120,4 @@ else 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 -rm -rf build-cross exit $rc diff --git a/tests/abi/gen.py b/tests/abi/gen.py index 4c00f318b..d0b155c8f 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -318,6 +318,37 @@ def build(): odin_get=[(f"simd.extract({{}}.a, {i})", f"f32({val(i, 'f32')})") for i in range(4)] + [("{}.b", f"i64({val(4, 'i64')})")]) + # --- 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. + BARE = [("i8", 4, "rx_i8x4", TIER_GNU), ("i8", 8, "rx_i8x8", TIER_GNU), + ("i16", 2, "rx_i16x2", TIER_GNU), ("i32", 8, "rx_i32x8", TIER_GNU), + ("i64", 4, "rx_i64x4", TIER_GNU), ("f32", 8, "rx_f32x8", TIER_GNU), + ("f32", 16, "rx_f32x16", TIER_GNU), ("f16", 2, "rx_f16x2", TIER_F16)] + for tag, n, cname, tier in BARE: + ot = SCALARS[tag][0] + lanes = min(n, 4) + add(f"bv{n}_{tag}", f"#simd[{n}]{ot}", cname, + [leaf2("", "{}" + f"[{i}]", tag, val(i, tag)) for i in range(lanes)], + tier=tier, + odin_set=["{} = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], + odin_get=[(f"simd.extract({{}}, {i})", f"{ot}({val(i, tag)})") for i in range(lanes)]) + # the same widths WRAPPED, so the pair is directly comparable + for tag, n, cname, tier in BARE[:3] + [BARE[7]]: + ot = SCALARS[tag][0] + lanes = min(n, 4) + add(f"wv{n}_{tag}", f"struct {{ v: #simd[{n}]{ot} }}", + f"struct {{ {cname} v; }}", + [leaf2("", f"v[{i}]", tag, val(i, tag)) for i in range(lanes)], + tier=tier, + odin_set=["{}.v = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], + odin_get=[(f"simd.extract({{}}.v, {i})", f"{ot}({val(i, tag)})") for i in range(lanes)]) + # --- 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 @@ -336,8 +367,9 @@ def build(): [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 add("m22_f32", "struct { m: matrix[2,2]f32 }", - "struct { float m[4] __attribute__((aligned(16))); }", + "struct { float m[4]; }", [leaf2(f"m[{i % 2}, {i // 2}]", f"m[{i}]", "f32", val(i, "f32")) for i in range(4)], tier=TIER_GNU) @@ -470,6 +502,17 @@ 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 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 """ diff --git a/tests/abi/run.bat b/tests/abi/run.bat index 97b9fff53..8a07c8bb7 100644 --- a/tests/abi/run.bat +++ b/tests/abi/run.bat @@ -2,7 +2,9 @@ REM The ABI comparator. Every check is "Odin agrees with the platform C compiler" -if not exist "build\" mkdir build +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 @@ -34,4 +36,3 @@ clang -c abi_corpus.c -o abi_corpus_c.o -w || exit /b @echo off popd -rmdir /S /Q build diff --git a/tests/abi/run.sh b/tests/abi/run.sh index f2b9cac42..799497624 100755 --- a/tests/abi/run.sh +++ b/tests/abi/run.sh @@ -25,9 +25,10 @@ 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" -trap 'rm -rf "$here/build"' EXIT # also on failure, where `set -e` would skip it pushd "$here/build" > /dev/null set -x From f19e33fbdd5139bf18515e0d1923b9a02e4708fd Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 20:47:16 -0700 Subject: [PATCH 11/32] adjust sizes to match audit --- src/build_settings.cpp | 56 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) 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"), }; From 88c194379ce3d4ad4cd4ae491f17df30af95c968 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 20:48:50 -0700 Subject: [PATCH 12/32] fix SysV vector classification: SSEUp scan, wide-SSE args, short-vector lanes and sub-eightbyte class --- src/llvm_abi.cpp | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 07a86e05d..4288a0ccc 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -335,11 +335,7 @@ gb_internal i64 lb_alignof(LLVMTypeRef type) { i64 elem_size = lb_sizeof(elem); i64 count = LLVMGetVectorSize(type); i64 size = count * elem_size; - i64 max_align = build_context.max_simd_align; - if (build_context.metrics.os == TargetOs_darwin && build_context.metrics.arch == TargetArch_amd64) { - max_align = gb_min(max_align, 16); // see type_align_of_internal - } - return gb_clamp(next_pow2(size), 1, max_align); + return gb_clamp(next_pow2(size), 1, build_context.max_simd_align); } } @@ -799,6 +795,13 @@ namespace lbAbiAmd64SysV { } } + if (is_arg && cls.count > 2 && is_sse(cls[0])) { + // An SSE run wider than two eightbytes has no register to land in at + // the baseline ISA, so as an argument it goes to memory, bare vector + // or struct-wrapped. The return does not: clang returns it by value + // and lets LLVM split it across xmm0:xmm1. + return lb_arg_type_indirect_byval(c, type, source_type); + } if (is_register(type)) { LLVMAttributeRef attribute = nullptr; if (type == LLVMInt1TypeInContext(c)) { @@ -1073,7 +1076,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; } @@ -1205,7 +1209,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; @@ -1305,6 +1318,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: { From 1cf1c4103e63394685b11b68d00f6694e9e3cc70 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:30:36 -0700 Subject: [PATCH 13/32] fix SysV vector ABI: 64-bit slot width, wide-vector return, wrapped-vector return --- src/llvm_abi.cpp | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 4288a0ccc..cb3767d39 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -795,12 +795,18 @@ namespace lbAbiAmd64SysV { } } - if (is_arg && cls.count > 2 && is_sse(cls[0])) { + 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, so as an argument it goes to memory, bare vector - // or struct-wrapped. The return does not: clang returns it by value - // and lets LLVM split it across xmm0:xmm1. - return lb_arg_type_indirect_byval(c, type, source_type); + // 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 = nullptr; @@ -978,7 +984,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) && @@ -1143,6 +1153,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); From 416619b71bf8557632538b25b0fdd9c329daf087 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:30:56 -0700 Subject: [PATCH 14/32] cover every 8-byte bare vector type --- tests/abi/gen.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/abi/gen.py b/tests/abi/gen.py index d0b155c8f..e885b3c6b 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -325,9 +325,13 @@ def build(): # `#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. + # 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 = [("i8", 4, "rx_i8x4", TIER_GNU), ("i8", 8, "rx_i8x8", TIER_GNU), - ("i16", 2, "rx_i16x2", TIER_GNU), ("i32", 8, "rx_i32x8", TIER_GNU), + ("i16", 2, "rx_i16x2", TIER_GNU), ("i16", 4, "rx_i16x4", TIER_GNU), + ("i32", 2, "rx_i32x2", TIER_GNU), ("f32", 2, "rx_f32x2", TIER_GNU), + ("i32", 8, "rx_i32x8", TIER_GNU), ("i64", 4, "rx_i64x4", TIER_GNU), ("f32", 8, "rx_f32x8", TIER_GNU), ("f32", 16, "rx_f32x16", TIER_GNU), ("f16", 2, "rx_f16x2", TIER_F16)] for tag, n, cname, tier in BARE: @@ -506,6 +510,9 @@ typedef float rx_v4f __attribute__((vector_size(16))); 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))); From 1ffcc43b39d87098de19fb5a4cc7ca0c47e33a2c Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:34:17 -0700 Subject: [PATCH 15/32] generator typo --- tests/abi/gen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/abi/gen.py b/tests/abi/gen.py index e885b3c6b..0ed67de77 100644 --- a/tests/abi/gen.py +++ b/tests/abi/gen.py @@ -342,8 +342,10 @@ def build(): tier=tier, odin_set=["{} = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], odin_get=[(f"simd.extract({{}}, {i})", f"{ot}({val(i, tag)})") for i in range(lanes)]) - # the same widths WRAPPED, so the pair is directly comparable - for tag, n, cname, tier in BARE[:3] + [BARE[7]]: + # the same widths WRAPPED, so the pair is directly comparable. Selected by + # NAME, not by position: indices move whenever a row is added. + WRAP = {("i8", 4), ("i8", 8), ("i16", 2), ("f16", 2)} + for tag, n, cname, tier in [b for b in BARE if (b[0], b[1]) in WRAP]: ot = SCALARS[tag][0] lanes = min(n, 4) add(f"wv{n}_{tag}", f"struct {{ v: #simd[{n}]{ot} }}", From a50c9e0a1dbef555b79874e5f278e2f3805bb39e Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:51:53 -0700 Subject: [PATCH 16/32] switch test harness to odin --- tests/abi/cross.sh | 2 +- tests/abi/gen.odin | 1303 ++++++++++++++++++++++++++++++++++++++++++++ tests/abi/gen.py | 838 ---------------------------- tests/abi/run.bat | 2 +- tests/abi/run.sh | 2 +- 5 files changed, 1306 insertions(+), 841 deletions(-) create mode 100644 tests/abi/gen.odin delete mode 100644 tests/abi/gen.py diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 181fbf470..137087b5b 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -55,7 +55,7 @@ esac # cleaned BEFORE, not after -- the driver is left in place to inspect rm -rf build-cross mkdir -p build-cross/p -python3 gen.py build-cross +$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. diff --git a/tests/abi/gen.odin b/tests/abi/gen.odin new file mode 100644 index 000000000..b8ca085ad --- /dev/null +++ b/tests/abi/gen.odin @@ -0,0 +1,1303 @@ +// 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} + } + 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 "bool": return "1" + case "enum": return tp("(enum E32)(%s)", v) + case "c64": return tp("(%s.0f + %s.0if)", v, v) + case "c128": return tp("(%s.0 + %s.0i)", v, v) + 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, %s))", v, v) + case "c128": return tp("complex128(complex(%s, %s))", v, v) + 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, %s))", as_int(v) + 1, v) + case "c128": return tp("complex128(complex(%d, %s))", as_int(v) + 1, v) + 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"}, + } + 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), + ) + } + + // --- 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, + ) + + // --- 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, + ) + } + + // NOTE: `complex64`/`complex128` are deliberately absent. Their members have + // no common accessor -- Odin spells it `real(x)`, C spells it `__real__ x`, a + // prefix operator rather than a member -- so a per-field check cannot be + // generated from one path. Measured separately as agreeing with clang on + // x86-64, aarch64 and riscv64; add them if the accessor problem is solved. + + // --- 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, + ) + } + + // --- 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. +// +// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the +// raw aggregate where clang coerces per the psABI -- so 111 of the types here +// fail. That is one defect, not 111, and leaving it on would drown every other +// signal. 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. +// +// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the +// raw aggregate where clang coerces per the psABI -- so 111 of the types here +// fail. That is one defect, not 111, and leaving it on would drown every other +// signal. 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/gen.py b/tests/abi/gen.py deleted file mode 100644 index 0ed67de77..000000000 --- a/tests/abi/gen.py +++ /dev/null @@ -1,838 +0,0 @@ -#!/usr/bin/env python3 -"""Generates abi_corpus.odin and abi_corpus.c from one description. - -The two files are checked in; this only needs running when the corpus changes. -Writing them by hand is what the generator exists to avoid: the whole test is -the claim that the Odin and C declarations describe the SAME type, and two -hand-maintained files drift. - -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. -""" - -import io - -# ---------------------------------------------------------------- scalars - -# tag -> (odin, c, is_float) -SCALARS = { - "i8": ("i8", "int8_t", False), - "i16": ("i16", "int16_t", False), - "i32": ("i32", "int32_t", False), - "i64": ("i64", "int64_t", False), - "u8": ("u8", "uint8_t", False), - "u16": ("u16", "uint16_t", False), - "u32": ("u32", "uint32_t", False), - "u64": ("u64", "uint64_t", False), - "bool":("bool","_Bool", False), - "f16": ("f16", "_Float16", True), - "i128":("i128","__int128", False), - "enum":("E32", "enum E32", False), - "c64": ("complex64", "float _Complex", False), - "c128":("complex128", "double _Complex", False), - "bset":("BS", "unsigned", False), - "f32": ("f32", "float", True), - "f64": ("f64", "double", True), - "ptr": ("rawptr", "void *", False), -} - -# 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 = {"f16": TIER_F16, "i128": TIER_I128} - - -def tier_of(*tags, base=TIER_CORE): - for t in tags: - if t in SCALAR_TIER: - return SCALAR_TIER[t] - return base - - -class Ty: - def __init__(self, name, odin, c, fields, tier=TIER_CORE, odin_set=None, odin_get=None): - self.name, self.odin, self.c, self.fields, self.tier = name, odin, c, fields, tier - # 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). - self.odin_set, self.odin_get = odin_set, odin_get - - -def val(i, tag): - """A distinct value per field position, so a shifted read is detectable.""" - if SCALARS[tag][2]: - return f"{i * 7 + 3}.5" - return str(i * 7 + 3) - - -def c_val(tag, v): - if tag == "ptr": return f"(void *)(intptr_t)({v})" - if tag == "bool": return "1" - if tag == "enum": return f"(enum E32)({v})" - if tag == "c64": return f"({v}.0f + {v}.0if)" - if tag == "c128": return f"({v}.0 + {v}.0i)" - if tag == "bset": return f"({(1 << (int(v) % 31)) | 1}u)" - return v - - -def odin_val(tag, v): - if tag == "ptr": return f"rawptr(uintptr({v}))" - if tag == "bool": return "true" - if tag == "enum": return f"E32({v})" - if tag == "c64": return f"complex64(complex({v}, {v}))" - if tag == "c128": return f"complex128(complex({v}, {v}))" - if tag == "bset": return "(BS{0, " + str(int(v) % 31) + "})" - return v - - -def c_ref(cp, var): - """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.""" - return cp.format(var) if "{}" in cp else f"{var}.{cp}" - - -def c_conds(t, var): - parts = [f"{c_ref(cp, var)} == ({c_val(k, v)})" for _op, cp, k, v in t.fields] - return " && ".join(parts) or "1" - - -def odin_setters(t, var): - if t.odin_set is not None: - return [x.replace("{}", var) for x in t.odin_set] - return [f"{var}.{op} = {odin_val(tag, v)}" for op, _cp, tag, v in t.fields] - - -def odin_getters(t, var): - if t.odin_get is not None: - return [(e.replace("{}", var), ev) for e, ev in t.odin_get] - out = [] - for op, _cp, tag, v in t.fields: - ot = SCALARS[tag][0] if tag in SCALARS else "f16" - ev = odin_val(tag, v) if tag in ("ptr", "bool", "enum", "c64", "c128", "bset") else f"{ot}({v})" - out.append((f"{var}.{op}", ev)) - return out - - -def mutated(tag, v): - """A value the checks MUST reject, for the mutation control.""" - if tag == "bool": return "false" - if tag == "ptr": return "rawptr(uintptr(999))" - if tag == "enum": return f"E32({int(v) + 1})" - if tag == "c64": return f"complex64(complex({int(v) + 1}, {v}))" - if tag == "c128": return f"complex128(complex({int(v) + 1}, {v}))" - if tag == "bset": return "(BS{2})" - return f"{float(v) + 1}" if "." in str(v) else f"{int(v) + 1}" - - -def leaf(path, tag, i): - return (path, path, tag, val(i, tag)) - - -def leaf2(odin_path, c_path, tag, v): - """A member spelled differently in the two languages -- matrix indexing, - or complex, where there is no common accessor.""" - return (odin_path, c_path, tag, v) - - -# ---------------------------------------------------------------- corpus - -def build(): - out = [] - - def add(*a, **k): - out.append(Ty(*a, **k)) - - # --- scalar arity 1..4, the merge and by-value/memory boundaries - combos = [ - ("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"), - ] - for tags in combos: - n = "s_" + "_".join(tags) - od = "struct { " + ", ".join(f"f{i}: {SCALARS[t][0]}" for i, t in enumerate(tags)) + " }" - cd = "struct { " + " ".join(f"{SCALARS[t][1]} f{i};" for i, t in enumerate(tags)) + " }" - add(n, od, cd, [leaf(f"f{i}", t, i) for i, t in enumerate(tags)], tier=tier_of(*tags)) - - # --- arrays: the same eightbytes from one declaration - for tag in ("f32", "f64", "i32", "i64", "i8", "f16", "enum", "i128"): - for cnt in (1, 2, 3, 4, 5): - n = f"a{cnt}_{tag}" - od = f"struct {{ a: [{cnt}]{SCALARS[tag][0]} }}" - cd = f"struct {{ {SCALARS[tag][1]} a[{cnt}]; }}" - add(n, od, cd, [leaf(f"a[{i}]", tag, i) for i in range(cnt)], tier=tier_of(tag)) - - # --- nesting: same leaves reached through another level - for a, b in (("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")): - add(f"n_{a}_{b}", - f"struct {{ i: struct {{ x: {SCALARS[a][0]}, y: {SCALARS[b][0]} }} }}", - f"struct {{ struct {{ {SCALARS[a][1]} x; {SCALARS[b][1]} y; }} i; }}", - [leaf("i.x", a, 0), leaf("i.y", b, 1)], tier=tier_of(a, b)) - add(f"n2_{a}_{b}", - f"struct {{ i: struct {{ x: {SCALARS[a][0]} }}, y: {SCALARS[b][0]} }}", - f"struct {{ struct {{ {SCALARS[a][1]} x; }} i; {SCALARS[b][1]} y; }}", - [leaf("i.x", a, 0), leaf("y", b, 1)], tier=tier_of(a, b)) - - # --- unions, and a union below the top level - for a, b in (("f32", "i32"), ("f64", "i64"), ("f32", "f32"), ("f64", "f32"), - ("f16", "i16"), ("f16", "f32")): - add(f"u_{a}_{b}", - f"struct #raw_union {{ x: {SCALARS[a][0]}, y: {SCALARS[b][0]} }}", - f"union {{ {SCALARS[a][1]} x; {SCALARS[b][1]} y; }}", - [leaf("x", a, 0)], tier=tier_of(a, b)) - add(f"su_{a}_{b}", - f"struct {{ u: struct #raw_union {{ x: {SCALARS[a][0]} }}, y: {SCALARS[b][0]} }}", - f"struct {{ union {{ {SCALARS[a][1]} x; }} u; {SCALARS[b][1]} y; }}", - [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(f"su2_{a}_{b}", - f"struct {{ u: struct #raw_union {{ x, y: {SCALARS[a][0]} }}, z: {SCALARS[b][0]} }}", - f"struct {{ union {{ {SCALARS[a][1]} x, y; }} u; {SCALARS[b][1]} z; }}", - [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 ("f32", "f64", "f16"): - w = SCALARS[tag][0] - cw = SCALARS[tag][1] - add(f"hfa4_{tag}", - f"struct {{ a, b, c, d: {w} }}", - f"struct {{ {cw} a, b, c, d; }}", - [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], - tier=tier_of(tag)) - add(f"hfa5_{tag}", - f"struct {{ a, b, c, d, e: {w} }}", - f"struct {{ {cw} a, b, c, d, e; }}", - [leaf(x, tag, i) for i, x in enumerate("abcde")], tier=tier_of(tag)) - # zero-length array member -- disqualifies the HFA - add(f"zla_{tag}", - f"struct {{ z: [0]f32, a, b, c, d: {w} }}", - f"struct {{ float z[0]; {cw} a, b, c, d; }}", - [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], - tier=TIER_GNU) - add(f"zlat_{tag}", - f"struct {{ a, b, c, d: {w}, z: [0]f32 }}", - f"struct {{ {cw} a, b, c, d; float z[0]; }}", - [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], - tier=TIER_GNU) - # empty struct member -- does NOT disqualify it - add(f"esm_{tag}", - f"struct {{ e: struct {{}}, a, b, c, d: {w} }}", - f"struct {{ struct {{}} e; {cw} a, b, c, d; }}", - [leaf("a", tag, 0), leaf("b", tag, 1), leaf("c", tag, 2), leaf("d", tag, 3)], - tier=TIER_GNU) - - # --- alignment: changes size and placement without changing any field type - for al in (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(f"aln{al}", - f"struct #align({al}) {{ a: i8, b: i32 }}", - f"struct __attribute__((aligned({al}))) {{ int8_t a; int32_t b; }}", - [leaf("a", "i8", 0), leaf("b", "i32", 1)], tier=TIER_GNU) - for al in (16, 32): - add(f"al{al}", - f"struct #align({al}) {{ a, b, c: f64 }}", - f"struct __attribute__((aligned({al}))) {{ double a, b, c; }}", - [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))); }", - [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; }", - [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; }", - [leaf("a", "i64", 0), leaf("b", "f32", 1)]) - add("pad_f32_f64", "struct { a: f32, b: f64 }", - "struct { float a; double b; }", - [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 = [("f32", 4, 16), ("f32", 2, 8), ("f64", 2, 16), ("i32", 4, 16), ("i8", 16, 16)] - for tag, n, _sz in VEC: - ct, cc = SCALARS[tag][0], SCALARS[tag][1] - add(f"v{n}_{tag}", - f"struct {{ v: #simd[{n}]{ct} }}", - f"struct {{ {cc} v __attribute__((vector_size({n} * sizeof({cc})))); }}", - [leaf(f"v[{i}]", tag, i) for i in range(n)], tier=TIER_GNU, - odin_set=["{}.v = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], - odin_get=[(f"simd.extract({{}}.v, {i})", f"{ct}({val(i, tag)})") for i in range(n)]) - # two vectors: an HVA on AAPCS64, memory on x86-64 - add("v4f32x2", - "struct { a, b: #simd[4]f32 }", - "struct { float a __attribute__((vector_size(16))), b __attribute__((vector_size(16))); }", - [leaf(f"a[{i}]", "f32", i) for i in range(4)] + - [leaf(f"b[{i}]", "f32", i + 4) for i in range(4)], tier=TIER_GNU, - odin_set=["{}.a = " + "{" + ", ".join(val(i, "f32") for i in range(4)) + "}", - "{}.b = " + "{" + ", ".join(val(i + 4, "f32") for i in range(4)) + "}"], - odin_get=[(f"simd.extract({{}}.a, {i})", f"f32({val(i, 'f32')})") for i in range(4)] + - [(f"simd.extract({{}}.b, {i})", f"f32({val(i + 4, 'f32')})") for i in range(4)]) - # a vector beside a scalar: homogeneous no longer - add("v4f32_i64", - "struct { a: #simd[4]f32, b: i64 }", - "struct { float a __attribute__((vector_size(16))); int64_t b; }", - [leaf(f"a[{i}]", "f32", i) for i in range(4)] + [leaf("b", "i64", 4)], - tier=TIER_GNU, - odin_set=["{}.a = " + "{" + ", ".join(val(i, "f32") for i in range(4)) + "}", - f"{{}}.b = {val(4, 'i64')}"], - odin_get=[(f"simd.extract({{}}.a, {i})", f"f32({val(i, 'f32')})") for i in range(4)] + - [("{}.b", f"i64({val(4, 'i64')})")]) - - # --- 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 = [("i8", 4, "rx_i8x4", TIER_GNU), ("i8", 8, "rx_i8x8", TIER_GNU), - ("i16", 2, "rx_i16x2", TIER_GNU), ("i16", 4, "rx_i16x4", TIER_GNU), - ("i32", 2, "rx_i32x2", TIER_GNU), ("f32", 2, "rx_f32x2", TIER_GNU), - ("i32", 8, "rx_i32x8", TIER_GNU), - ("i64", 4, "rx_i64x4", TIER_GNU), ("f32", 8, "rx_f32x8", TIER_GNU), - ("f32", 16, "rx_f32x16", TIER_GNU), ("f16", 2, "rx_f16x2", TIER_F16)] - for tag, n, cname, tier in BARE: - ot = SCALARS[tag][0] - lanes = min(n, 4) - add(f"bv{n}_{tag}", f"#simd[{n}]{ot}", cname, - [leaf2("", "{}" + f"[{i}]", tag, val(i, tag)) for i in range(lanes)], - tier=tier, - odin_set=["{} = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], - odin_get=[(f"simd.extract({{}}, {i})", f"{ot}({val(i, tag)})") for i in range(lanes)]) - # the same widths WRAPPED, so the pair is directly comparable. Selected by - # NAME, not by position: indices move whenever a row is added. - WRAP = {("i8", 4), ("i8", 8), ("i16", 2), ("f16", 2)} - for tag, n, cname, tier in [b for b in BARE if (b[0], b[1]) in WRAP]: - ot = SCALARS[tag][0] - lanes = min(n, 4) - add(f"wv{n}_{tag}", f"struct {{ v: #simd[{n}]{ot} }}", - f"struct {{ {cname} v; }}", - [leaf2("", f"v[{i}]", tag, val(i, tag)) for i in range(lanes)], - tier=tier, - odin_set=["{}.v = " + "{" + ", ".join(val(i, tag) for i in range(n)) + "}"], - odin_get=[(f"simd.extract({{}}.v, {i})", f"{ot}({val(i, tag)})") for i in range(lanes)]) - - # --- 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 w1, w2 in ((3, 5), (1, 31), (17, 15)): - # the value has to fit the declared width, so it is derived from it - # the value must fit the width AND leave room for the mutation control - va, vb = str(min(5, (1 << w1) - 1) if w1 > 1 else 0), str(min(9, (1 << w2) - 1)) - add(f"bf_{w1}_{w2}", - f"bit_field u32 {{ a: u32 | {w1}, b: u32 | {w2} }}", - f"struct {{ unsigned a : {w1}; unsigned b : {w2}; }}", - [leaf2("a", "a", "u32", va), leaf2("b", "b", "u32", vb)]) - add("bff_f32", - "struct { f: f32, b: bit_field u32 { a: u32 | 3 } }", - "struct { float f; struct { unsigned a : 3; } b; }", - [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 - add("m22_f32", "struct { m: matrix[2,2]f32 }", - "struct { float m[4]; }", - [leaf2(f"m[{i % 2}, {i // 2}]", f"m[{i}]", "f32", val(i, "f32")) for i in range(4)], - tier=TIER_GNU) - - # NOTE: `complex64`/`complex128` are deliberately absent. Their members have - # no common accessor -- Odin spells it `real(x)`, C spells it `__real__ x`, a - # prefix operator rather than a member -- so a per-field check cannot be - # generated from one path. Measured separately as agreeing with clang on - # x86-64, aarch64 and riscv64; add them if the accessor problem is solved. - - # --- 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]; }", - [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]; }", - [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))); }", - [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; }", - [leaf("a.x", "f32", 0), leaf("a.y", "f32", 1)]) - add("ua_arr", - "struct #raw_union { a: [4]f32, b: [2]f64 }", - "union { float a[4]; double b[2]; }", - [leaf(f"a[{i}]", "f32", i) for i in range(4)]) - - # --- 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; }", - [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; }", - [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; }", [], tier=TIER_GNU) - add("zarr", "struct { z: [0]f32 }", "struct { float z[0]; }", [], 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. - add("av2_f32", - "struct { a: [2]#simd[4]f32 }", - "struct { rx_v4f a[2]; }", - [leaf2("", f"a[{i // 4}][{i % 4}]", "f32", f"{i + 1}.5") for i in range(8)], - tier=TIER_GNU, - odin_set=["{}.a[0] = {1.5, 2.5, 3.5, 4.5}", "{}.a[1] = {5.5, 6.5, 7.5, 8.5}"], - odin_get=[("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. - add("v8_off", - "struct { a: i8, v: #simd[8]f32 }", - "struct { int8_t a; float v __attribute__((vector_size(32))); }", - [leaf("a", "i8", 0)] + [leaf(f"v[{i}]", "f32", i) for i in range(8)], - tier=TIER_GNU, - odin_set=["{}.a = 3", "{}.v = " + "{" + ", ".join(val(i, "f32") for i in range(8)) + "}"], - odin_get=[("{}.a", "i8(3)")] + - [(f"simd.extract({{}}.v, {i})", f"f32({val(i, 'f32')})") for i in range(8)]) - add("v8_f32", - "struct { v: #simd[8]f32 }", - "struct { float v __attribute__((vector_size(32))); }", - [leaf(f"v[{i}]", "f32", i) for i in range(8)], tier=TIER_GNU, - odin_set=["{}.v = " + "{" + ", ".join(val(i, "f32") for i in range(8)) + "}"], - odin_get=[(f"simd.extract({{}}.v, {i})", f"f32({val(i, 'f32')})") for i in range(8)]) - - # --- large, past every by-value threshold - add("big", "struct { a: [8]i64 }", "struct { int64_t a[8]; }", - [leaf(f"a[{i}]", "i64", i) for i in range(8)]) - - return out - - -# ---------------------------------------------------------------- emit - -GUARD = {TIER_CORE: None, TIER_GNU: "ABI_TIER_GNU", TIER_F16: "ABI_TIER_F16", - TIER_I128: "ABI_TIER_I128"} - -# The tier conditions live here. The corpus is guarded by them, `tiers.c` reports them. -TIER_COND = { - TIER_GNU: "defined(__GNUC__)", - TIER_F16: "defined(__FLT16_MANT_DIG__) && !defined(_MSC_VER)", - TIER_I128: "defined(__SIZEOF_INT128__)", -} - - -def emit_tiers_c(): - o = io.StringIO() - o.write("/* GENERATED by tests/abi/gen.py -- do not edit.\n" - " Preprocess this and grep the markers: it answers which tiers the C\n" - " compiler actually has, so the Odin side can be gated by the same\n" - " answer rather than by a restatement of the condition. */\n") - for tier, cond in TIER_COND.items(): - o.write(f"#if {cond}\nABI_YES_{GUARD[tier].replace('ABI_TIER_', '')}\n#endif\n") - return o.getvalue() - - -C_HEAD = """\ -/* GENERATED by tests/abi/gen.py -- 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 -""" - -ODIN_HEAD = """\ -// GENERATED by tests/abi/gen.py -- 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. -// -// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the -// raw aggregate where clang coerces per the psABI -- so 111 of the types here -// fail. That is one defect, not 111, and leaving it on would drown every other -// signal. 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" -""" - - -def emit_c(types): - o = io.StringIO() - defines = "".join(f"#if {c}\n#define {GUARD[t]} 1\n#endif\n" for t, c in TIER_COND.items()) - o.write(C_HEAD.replace("@TIER_DEFINES@", defines.rstrip())) - for t in types: - g = GUARD[t.tier] - if g: - o.write(f"\n#ifdef {g}\n") - o.write(f"\ntypedef {t.c} {t.name};\n") - # _arg: return the argument that FOLLOWS the aggregate - o.write(f"double {t.name}_arg({t.name} s, double next) {{ (void)s; return next; }}\n") - # _chk: every field, so a wrong offset is caught as well as a wrong register - o.write(f"int {t.name}_chk({t.name} s) {{ return ({c_conds(t, 's')}) ? 0 : 1; }}\n") - # _ret: return position - o.write(f"{t.name} {t.name}_ret(void) {{ {t.name} s; ") - o.write("".join(f"{c_ref(cp, 's')} = ({c_val(k, v)}); " for _op, cp, k, v in t.fields)) - o.write("return s; }\n") - # _ex: the aggregate after the argument registers are gone - o.write(f"double {t.name}_ex(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e," - f" int64_t f, int64_t o, double g, double h, double i, double j, double k," - f" double l, double m, double n, {t.name} s, double next) {{\n") - o.write("\t(void)a;(void)b;(void)c;(void)d;(void)e;(void)f;(void)o;(void)g;(void)h;\n") - o.write("\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. - ints = ", ".join(f"int64_t q{i}" for i in range(9)) - dbls = ", ".join(f"double w{i}" for i in range(9)) - o.write(f"double {t.name}_ex2({ints}, {dbls}, {t.name} s, double next) {{\n\t") - o.write("".join(f"(void)q{i};" for i in range(9))) - o.write("".join(f"(void)w{i};" for i in range(9))) - o.write("(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 - o.write(f"double {t.name}_two({t.name} s1, {t.name} s2, double next) {{\n") - o.write(f"\tif (!({c_conds(t, 's1')})) return -1;\n") - o.write(f"\tif (!({c_conds(t, 's2')})) return -2;\n\treturn next;\n}}\n") - # _back: the other direction -- C calls an exported Odin callee, which is - # what a callback does and what nothing else here covers - o.write(f"extern double o_{t.name}_take({t.name} s, double next);\n") - o.write(f"extern {t.name} o_{t.name}_make(void);\n") - o.write(f"int {t.name}_back(void) {{\n\t{t.name} s; ") - o.write("".join(f"{c_ref(cp, 's')} = ({c_val(k, v)}); " for _op, cp, k, v in t.fields)) - o.write(f"\n\tif (o_{t.name}_take(s, 7) != 7) return 1;\n") - o.write(f"\t{t.name} r = o_{t.name}_make();\n") - o.write(f"\tif (!({c_conds(t, 'r')})) return 2;\n\treturn 0;\n}}\n") - # _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. - o.write(f"double {t.name}_can(int64_t q0, int64_t q1, int64_t q2, int64_t q3," - f" int64_t q4, int64_t q5, int64_t q6, double w0, double w1, double w2," - f" double w3, double w4, double w5, double w6, double w7," - f" int64_t before, {t.name} s, int64_t after, double last) {{\n") - o.write("\t(void)q0;(void)q1;(void)q2;(void)q3;(void)q4;(void)q5;(void)q6;\n") - o.write("\t(void)w0;(void)w1;(void)w2;(void)w3;(void)w4;(void)w5;(void)w6;(void)w7;\n") - o.write("\tif (before != 0x1111111111111111LL) return -1;\n") - o.write("\tif (after != 0x2222222222222222LL) return -2;\n") - o.write(f"\tif (!({c_conds(t, 's')})) return -3;\n\treturn last;\n}}\n") - # _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. - ints2 = ", ".join(f"int64_t p{i}" for i in range(11)) - o.write(f"double {t.name}_can2({ints2}, int64_t before, {t.name} s," - f" int64_t after, double last) {{\n\t") - o.write("".join(f"(void)p{i};" for i in range(11))) - o.write("\n\tif (before != 0x1111111111111111LL) return -1;\n") - o.write("\tif (after != 0x2222222222222222LL) return -2;\n") - o.write(f"\tif (!({c_conds(t, 's')})) return -3;\n\treturn last;\n}}\n") - # _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 t.fields: - o.write(f"double {t.name}_va(int n, ...) {{\n\tva_list ap; va_start(ap, n);\n") - o.write(f"\t{t.name} s = va_arg(ap, {t.name});\n") - o.write("\tdouble next = va_arg(ap, double);\n\tva_end(ap);\n") - o.write(f"\treturn ({c_conds(t, 's')}) ? next : -1;\n}}\n") - if g: - o.write(f"\n#endif /* {g} */\n") - return o.getvalue() - - -def emit_odin(types): - o = io.StringIO() - o.write(ODIN_HEAD) - for t in types: - g = GUARD[t.tier] - w = f"when {g} {{\n" if g else "" - ind = "\t" if g else "" - o.write("\n" + w) - o.write(f"{ind}{t.name} :: {t.odin}\n") - o.write(f'{ind}@(default_calling_convention="c")\n{ind}foreign lib {{\n') - o.write(f"{ind}\t{t.name}_arg :: proc(s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_chk :: proc(s: {t.name}) -> i32 ---\n") - o.write(f"{ind}\t{t.name}_ret :: proc() -> {t.name} ---\n") - o.write(f"{ind}\t{t.name}_ex :: proc(a, b, c, d, e, f, o: i64, g, h, i, j, k, l, m, n: f64," - f" s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_ex2 :: proc(q0, q1, q2, q3, q4, q5, q6, q7, q8: i64," - f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") - o.write(f"{ind}\t{t.name}_can2 :: proc(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10: i64," - f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_can :: proc(q0, q1, q2, q3, q4, q5, q6: i64," - f" w0, w1, w2, w3, w4, w5, w6, w7: f64," - f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") - if t.fields: - o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") - o.write(f"{ind}}}\n") - # the callees C calls back into: the direction a callback uses - o.write(f"{ind}@(export) o_{t.name}_take :: proc \"c\" (s: {t.name}, next: f64) -> f64 {{\n") - for expr, ev in odin_getters(t, "s"): - o.write(f"{ind}\tif {expr} != {ev} {{ return -1 }}\n") - o.write(f"{ind}\treturn next\n{ind}}}\n") - o.write(f"{ind}@(export) o_{t.name}_make :: proc \"c\" () -> {t.name} {{\n{ind}\ts: {t.name}\n") - for st in odin_setters(t, "s"): - o.write(f"{ind}\t{st}\n") - o.write(f"{ind}\treturn s\n{ind}}}\n") - o.write(f"{ind}@(test)\n{ind}test_{t.name} :: proc(t: ^testing.T) {{\n") - o.write(f"{ind}\ts: {t.name}\n") - for st in odin_setters(t, "s"): - o.write(f"{ind}\t{st}\n") - # 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 t.fields and t.odin_set is None: - op, _cp, tag, v = t.fields[0] - o.write(f"{ind}\twhen ABI_MUTATE {{ s.{op} = {mutated(tag, v)} }}\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_arg(s, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_chk(s), i32(0))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_ex2(1,2,3,4,5,6,7,8,9, 1,2,3,4,5,6,7,8,9, s, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_two(s, s, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_can(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_can2(1,2,3,4,5,6,7,8,9,10,11, 0x1111111111111111, s, 0x2222222222222222, 7), f64(7))\n") - o.write(f"{ind}\ttesting.expect_value(t, {t.name}_back(), i32(0))\n") - if t.fields: - o.write(f"{ind}\twhen ABI_VARARGS {{\n") - o.write(f"{ind}\t\ttesting.expect_value(t, {t.name}_va(1, s, f64(7)), f64(7))\n") - o.write(f"{ind}\t}}\n") - o.write(f"{ind}\tr := {t.name}_ret()\n") - if not odin_getters(t, "r"): - o.write(f"{ind}\t_ = r\n") - for expr, ev in odin_getters(t, "r"): - o.write(f"{ind}\ttesting.expect_value(t, {expr}, {ev})\n") - o.write(f"{ind}}}\n") - if g: - o.write("}\n") - return o.getvalue() - - -MAIN_HEAD = """\ -// GENERATED by tests/abi/gen.py -- 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. -// -// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the -// raw aggregate where clang coerces per the psABI -- so 111 of the types here -// fail. That is one defect, not 111, and leaving it on would drown every other -// signal. Turn it on with `-define:ABI_VARARGS=true` to measure it. -ABI_VARARGS :: #config(ABI_VARARGS, false) - - - -foreign import lib "../abi_corpus_c.o" -""" - - -def emit_main(types): - o = io.StringIO() - o.write(MAIN_HEAD) - body = io.StringIO() - seen = [] - for t in types: - g = GUARD[t.tier] - w = f"when {g} {{\n" if g else "" - ind = "\t" if g else "" - o.write("\n" + w) - o.write(f"{ind}{t.name} :: {t.odin}\n") - o.write(f'{ind}@(default_calling_convention="c")\n{ind}foreign lib {{\n') - o.write(f"{ind}\t{t.name}_arg :: proc(s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_chk :: proc(s: {t.name}) -> i32 ---\n") - o.write(f"{ind}\t{t.name}_ret :: proc() -> {t.name} ---\n") - o.write(f"{ind}\t{t.name}_ex :: proc(a, b, c, d, e, f, o: i64, g, h, i, j, k, l, m, n: f64," - f" s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_ex2 :: proc(q0, q1, q2, q3, q4, q5, q6, q7, q8: i64," - f" w0, w1, w2, w3, w4, w5, w6, w7, w8: f64, s: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_two :: proc(s1, s2: {t.name}, next: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_back :: proc() -> i32 ---\n") - o.write(f"{ind}\t{t.name}_can2 :: proc(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10: i64," - f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") - o.write(f"{ind}\t{t.name}_can :: proc(q0, q1, q2, q3, q4, q5, q6: i64," - f" w0, w1, w2, w3, w4, w5, w6, w7: f64," - f" before: i64, s: {t.name}, after: i64, last: f64) -> f64 ---\n") - if t.fields: - o.write(f"{ind}\t{t.name}_va :: proc(n: i32, #c_vararg args: ..any) -> f64 ---\n") - o.write(f"{ind}}}\n") - # the callees C calls back into: the direction a callback uses - o.write(f"{ind}@(export) o_{t.name}_take :: proc \"c\" (s: {t.name}, next: f64) -> f64 {{\n") - for expr, ev in odin_getters(t, "s"): - o.write(f"{ind}\tif {expr} != {ev} {{ return -1 }}\n") - o.write(f"{ind}\treturn next\n{ind}}}\n") - o.write(f"{ind}@(export) o_{t.name}_make :: proc \"c\" () -> {t.name} {{\n{ind}\ts: {t.name}\n") - for st in odin_setters(t, "s"): - o.write(f"{ind}\t{st}\n") - o.write(f"{ind}\treturn s\n{ind}}}\n") - o.write(f"{ind}check_{t.name} :: proc \"contextless\" () -> i32 {{\n") - o.write(f"{ind}\ts: {t.name}\n") - for st in odin_setters(t, "s"): - o.write(f"{ind}\t{st}\n") - o.write(f"{ind}\tif {t.name}_arg(s, 7) != 7 {{ return 1 }}\n") - o.write(f"{ind}\tif {t.name}_chk(s) != 0 {{ return 1 }}\n") - o.write(f"{ind}\tif {t.name}_ex(1,2,3,4,5,6,7, 1,2,3,4,5,6,7,8, s, 7) != 7 {{ return 1 }}\n") - o.write(f"{ind}\tr := {t.name}_ret()\n") - if not odin_getters(t, "r"): - o.write(f"{ind}\t_ = r\n") - for expr, ev in odin_getters(t, "r"): - o.write(f"{ind}\tif {expr} != {ev} {{ return 1 }}\n") - o.write(f"{ind}\treturn 0\n{ind}}}\n") - if g: - o.write("}\n") - idx = len(seen) + 1 - seen.append(t.name) - chk = f"if {idx} > ABI_SKIP && check_{t.name}() != 0 {{ return {idx} }}" - body.write(f"\t{'when ' + g + ' { ' if g else ''}{chk}{' }' if g else ''}\n") - o.write("\n@(export)\nprobe_main :: proc \"c\" () -> i32 {\n") - o.write(body.getvalue()) - o.write("\treturn 0\n}\n") - o.write("\n// index -> name\n") - for i, n in enumerate(seen): - o.write(f"// {i+1}\t{n}\n") - return o.getvalue() - - -if __name__ == "__main__": - import os, sys - # Written into the caller's build directory, not the source tree: nothing - # generated is checked in, so the two languages cannot drift apart. - here = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) - ts = build() - open(os.path.join(here, "abi_corpus.c"), "w").write(emit_c(ts)) - open(os.path.join(here, "abi_corpus.odin"), "w").write(emit_odin(ts)) - open(os.path.join(here, "abi_main.odin"), "w").write(emit_main(ts)) - open(os.path.join(here, "tiers.c"), "w").write(emit_tiers_c()) - print(f"{len(ts)} types, {sum(7 + (1 if t.fields else 0) for t in ts)} C functions, {len(ts) * 2} Odin callees") diff --git a/tests/abi/run.bat b/tests/abi/run.bat index 8a07c8bb7..e89e989d6 100644 --- a/tests/abi/run.bat +++ b/tests/abi/run.bat @@ -11,7 +11,7 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused @echo on -python3 ..\gen.py . || exit /b +..\..\..\odin run ..\gen.odin -file -- . || exit /b @echo off REM Ask the C compiler which tiers it has, by preprocessing the generated diff --git a/tests/abi/run.sh b/tests/abi/run.sh index 799497624..bba46b2a1 100755 --- a/tests/abi/run.sh +++ b/tests/abi/run.sh @@ -33,7 +33,7 @@ pushd "$here/build" > /dev/null set -x -python3 ../gen.py . +$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. From 98e580404a19be6b848fd031307b5e6231563536 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:52:34 -0700 Subject: [PATCH 17/32] pack structs whose member alignment LLVM would raise above the target cap arm64: return a narrow bare vector as itself, not coerced to an integer --- src/llvm_abi.cpp | 33 +++++++++++++++++++++++++++++++++ src/llvm_backend_general.cpp | 12 +++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index cb3767d39..3697aaf24 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -347,6 +347,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); @@ -1674,6 +1700,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); diff --git a/src/llvm_backend_general.cpp b/src/llvm_backend_general.cpp index 9e9c09051..982da14ef 100644 --- a/src/llvm_backend_general.cpp +++ b/src/llvm_backend_general.cpp @@ -2655,7 +2655,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); } From 32f90a04dbd3fdde6ff8a4c98c81d1f883a4397e Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 22:11:53 -0700 Subject: [PATCH 18/32] add optimization on to harness --- .github/workflows/ci.yml | 5 +++++ tests/abi/cross.sh | 8 ++++++-- tests/abi/run.bat | 8 ++++++-- tests/abi/run.sh | 5 ++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a59fa456..e18998504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: ./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: @@ -75,6 +76,7 @@ jobs: ./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: @@ -177,6 +179,7 @@ jobs: run: | cd tests/abi ./run.sh + ABI_CFLAGS=-O2 ./run.sh "" "" -o:speed - name: Run demo on WASI WASM32 run: | @@ -292,6 +295,8 @@ jobs: call run.bat cd ../abi call run.bat + set ABI_CFLAGS=-O2 + call run.bat -o:speed - name: Check benchmarks shell: cmd run: | diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 137087b5b..5113336e5 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -18,8 +18,12 @@ set -eu 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 @@ -98,8 +102,8 @@ EOF set -x $ODIN build build-cross/p -target:"$TARGET" -build-mode:obj -no-entry-point \ - -no-thread-local -reloc-mode:static $TIERS -out:build-cross/o -$CLANG --target="$TRIPLE" -c build-cross/abi_corpus.c -o build-cross/abi_corpus_c.o -w -fno-stack-protector + -no-thread-local -reloc-mode:static $TIERS -out:build-cross/o "$@" +$CLANG --target="$TRIPLE" $ABI_CFLAGS -c build-cross/abi_corpus.c -o build-cross/abi_corpus_c.o -w -fno-stack-protector $CLANG --target="$TRIPLE" -c build-cross/start.s -o build-cross/start.o $CLANG --target="$TRIPLE" -ffreestanding -fno-builtin -O1 -w -c build-cross/shim.c -o build-cross/shim.o $CLANG --target="$TRIPLE" -nostdlib -static -fuse-ld=lld \ diff --git a/tests/abi/run.bat b/tests/abi/run.bat index e89e989d6..db03eda3b 100644 --- a/tests/abi/run.bat +++ b/tests/abi/run.bat @@ -2,6 +2,10 @@ 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 @@ -30,8 +34,8 @@ echo tiers: %TIERS% REM -w because the corpus deliberately uses zero-length arrays and empty REM structs; both are the extensions under test. -clang -c abi_corpus.c -o abi_corpus_c.o -w || exit /b -..\..\..\odin test abi_corpus.odin %COMMON% %TIERS% || exit /b +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 diff --git a/tests/abi/run.sh b/tests/abi/run.sh index bba46b2a1..0fea5f67b 100755 --- a/tests/abi/run.sh +++ b/tests/abi/run.sh @@ -19,6 +19,9 @@ if [ $# -gt 2 ]; then shift 2; else shift $#; fi # anything else goes to `odin 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" @@ -42,7 +45,7 @@ TIERS="-define:ABI_TIER_GNU=$(have GNU) -define:ABI_TIER_F16=$(have F16) -define # `-w` because the corpus deliberately uses zero-length arrays and empty # structs; both are the extensions under test. -$CLANG $CC_TARGET -c abi_corpus.c -o abi_corpus_c.o -w +$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 From 2c947bb173110a532d9be670552709ae1ad8169d Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 22:49:55 -0700 Subject: [PATCH 19/32] add riscv abi to ci; uneven unions, bare scalars --- .github/workflows/ci.yml | 6 +++++ tests/abi/gen.odin | 58 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e18998504..1d693370d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -372,3 +372,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/tests/abi/gen.odin b/tests/abi/gen.odin index b8ca085ad..3317c16b9 100644 --- a/tests/abi/gen.odin +++ b/tests/abi/gen.odin @@ -298,6 +298,27 @@ build :: proc() { ) } + // --- 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", "f32"}) { + s := scalar(tag) + v := val(0, tag) + expected := tag == "bool" ? odin_val(tag, v) : 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}), + ) + } + // --- arrays: the same eightbytes from one declaration for tag in ([]string{"f32", "f64", "i32", "i64", "i8", "f16", "enum", "i128"}) { for cnt in 1 ..= 5 { @@ -457,6 +478,21 @@ build :: proc() { 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( @@ -731,7 +767,29 @@ build :: proc() { "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 From a2f92401d3b45c41c5eead9227a49be7893aa9b6 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 22:51:07 -0700 Subject: [PATCH 20/32] sysv: fix a union passed as its last member when that member is narrower --- src/llvm_abi.cpp | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 3697aaf24..9c04df402 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -1041,21 +1041,15 @@ namespace lbAbiAmd64SysV { return reg_classes; } - // An SSE class that fills a whole eightbyte from offset 0, so nothing narrower - // starting at offset 0 can add to it. - gb_internal bool sse_class_covers_eightbyte(RegClass c) { - return c == RegClass_SSEDs || c == RegClass_SSEInt64; - } - // An SSE class positioned at offset 0 of its eightbyte. The `v` classes sit at - // offset 4 and are therefore DISJOINT from a 4-byte class at offset 0. - gb_internal bool sse_class_at_offset_zero(RegClass c) { + // 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: case RegClass_SSEFs: case RegClass_SSEDs: - case RegClass_SSEInt8: case RegClass_SSEInt16: - case RegClass_SSEInt32: case RegClass_SSEInt64: - return true; + case RegClass_SSEHs: return 2; + case RegClass_SSEFs: return 4; } - return false; + return 8; } gb_internal void unify(Array *cls, i64 i, RegClass const newv) { @@ -1091,12 +1085,13 @@ namespace lbAbiAmd64SysV { case RegClass_SSEInt64: return; } - } else if (sse_class_covers_eightbyte(oldv) && sse_class_at_offset_zero(newv)) { - // The members OVERLAP -- a union. Last-writer-wins would pass - // `union{f64, f32}` as a 4-byte float and lose the top half. Restricted - // to a full-eightbyte old class against an offset-zero new one, because - // `struct{f32, f16}` is Fs then Hv at offset 4, which is disjoint and - // must still combine rather than pick. + } 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; } From 81a4bf16fe7e27a0145c6b2fb01e8011e599a13a Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 23:11:02 -0700 Subject: [PATCH 21/32] riscv64: exclude aggregates containing a union from the FP calling convention checker: keep #raw_union marked when it has a single field, #raw_union must mark on single field --- src/check_type.cpp | 4 ++- src/llvm_abi.cpp | 78 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/check_type.cpp b/src/check_type.cpp index 14da12340..f1af16b7c 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 9c04df402..a8fb1cb8a 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -2157,7 +2157,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. @@ -2204,7 +2242,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; @@ -2214,7 +2254,7 @@ namespace lbAbiRiscv64 { return non_struct(c, orig_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); @@ -2267,9 +2307,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; @@ -2282,10 +2337,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. From 90d658f7d9d338b15bd06b4dac447d74f111ec50 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 23:31:51 -0700 Subject: [PATCH 22/32] abi: sign/zero-extend sub-32-bit scalar arguments on every psABI that requires it --- src/llvm_abi.cpp | 123 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index a8fb1cb8a..cd5a5d515 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -417,21 +417,73 @@ 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); 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) { if (!is_return && lb_sizeof(type) > 8) { return lb_arg_type_indirect(type, nullptr); } @@ -450,16 +502,13 @@ 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]; @@ -474,7 +523,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; @@ -506,25 +555,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++) { @@ -544,7 +593,7 @@ namespace lbAbiAmd64Win64 { break; } } else { - args[i] = lbAbi386::non_struct(c, t, false); + args[i] = lbAbi386::non_struct(c, t, false, nullptr); } } return args; @@ -567,7 +616,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); } }; @@ -835,10 +884,7 @@ namespace lbAbiAmd64SysV { } } if (is_register(type)) { - LLVMAttributeRef attribute = nullptr; - if (type == LLVMInt1TypeInContext(c)) { - attribute = lb_create_enum_attribute(c, "zeroext"); - } + 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) { @@ -868,7 +914,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); } } @@ -1985,14 +2035,14 @@ namespace lbAbiWasm { } namespace lbAbiArm32 { - gb_internal Array compute_arg_types(LLVMContextRef c, LLVMTypeRef *arg_types, unsigned arg_count, 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); 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->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); ft->calling_convention = calling_convention; return ft; @@ -2017,22 +2067,19 @@ 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) { + 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) { + 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); @@ -2069,7 +2116,7 @@ namespace lbAbiArm32 { 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); } }; @@ -2100,12 +2147,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); } @@ -2251,7 +2294,7 @@ namespace lbAbiRiscv64 { // handing back the original sends an over-aligned one to integer registers. 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 (!integer_only && fp_kind == LLVMStructTypeKind && fp_size <= 2*flen) { @@ -2288,7 +2331,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); } From 1e2d91c38bbb8da4c01b09cfa33eeafccac44f02 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 17:38:21 -0700 Subject: [PATCH 23/32] i386: pass and return bare vectors directly, coercing only 8-byte integer vectors, i386: treat a bit_field as the aggregate it is in C, fixing a segfault on return, abi test: document the i386 microarch baseline; do not read a signal exit as a type index --- src/llvm_abi.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++++- tests/abi/cross.sh | 22 +++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index cd5a5d515..08d6adf69 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -479,11 +479,50 @@ namespace lbAbi386 { ft->ctx = c; ft->args = compute_arg_types(c, arg_types, arg_count, original_type); ft->ret = compute_return_type(ft, c, return_type, return_is_defined, return_is_tuple); + + // `bit_field` is treated as a struct. + Type *return_source = lb_abi_single_result_type(original_type); + if (return_is_defined && !return_is_tuple && + return_source != nullptr && is_type_bit_field(return_source) && + !lb_is_type_kind(return_type, LLVMStructTypeKind) && + !lb_is_type_kind(return_type, LLVMArrayTypeKind)) { + // Windows and the BSDs return a small struct in registers, same as the scalar + // path so only the psABI targets need moving to the hidden pointer. + bool small_in_registers = build_context.metrics.os == TargetOs_windows || + build_context.metrics.os == TargetOs_freebsd || + build_context.metrics.os == TargetOs_openbsd; + i64 sz = lb_sizeof(return_type); + bool returned_in_registers = small_in_registers && (sz == 1 || sz == 2 || sz == 4 || sz == 8); + if (!returned_in_registers) { + LLVMAttributeRef attr = lb_create_enum_attribute_with_type(c, "sret", return_type); + ft->ret = lb_arg_type_indirect(return_type, attr); + } + } + ft->calling_convention = calling_convention; return ft; } gb_internal lbArgType non_struct(LLVMContextRef c, LLVMTypeRef type, bool is_return, Type *source_type) { + // A bare vector is passed and returned as itself; only aggregates + // take the indirect path below, ergo the vector check has to be first. + // + // Exception is an 8-byte vector Arg whose element is an integer: its an MMX type; + // clang coerces it to `i64` to keep it out of the MMX registers. An 8-byte + // vector of floats is an SSE type and stays itself, so the rule turns on the element and + // not on the width alone: + // + // <8 x i8> <4 x i16> <2 x i32> -> i64 + // <2 x float> <4 x half> -> unchanged + // + // The RETURN is never coerced -- `<8 x i8>` comes back as itself. + if (LLVMGetTypeKind(type) == LLVMVectorTypeKind) { + if (!is_return && lb_sizeof(type) == 8 && + LLVMGetTypeKind(LLVMGetElementType(type)) == LLVMIntegerTypeKind) { + return lb_arg_type_direct(type, LLVMIntTypeInContext(c, 64), nullptr, nullptr); + } + return lb_arg_type_direct(type, nullptr, nullptr, nullptr); + } if (!is_return && lb_sizeof(type) > 8) { return lb_arg_type_indirect(type, nullptr); } @@ -514,7 +553,11 @@ namespace lbAbi386 { LLVMTypeRef t = arg_types[i]; LLVMTypeKind kind = LLVMGetTypeKind(t); i64 sz = lb_sizeof(t); - if (kind == LLVMStructTypeKind || kind == LLVMArrayTypeKind) { + // `bit_field` lowers to a bare integer; C represents it as a struct with bit-field + // members, and i386 passes every struct by value on the stack. Use Src Type to match + bool is_aggregate = kind == LLVMStructTypeKind || kind == LLVMArrayTypeKind || + (srcs[i] != nullptr && is_type_bit_field(srcs[i])); + if (is_aggregate) { if (sz == 0) { args[i] = lb_arg_type_ignore(t); } else { diff --git a/tests/abi/cross.sh b/tests/abi/cross.sh index 5113336e5..1a700114c 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -11,9 +11,19 @@ set -eu # clang (which targets everything) and qemu-user. # # ./cross.sh linux_arm64 aarch64-linux-gnu qemu-aarch64 -# ./cross.sh linux_i386 i386-linux-gnu qemu-i386 +# ./cross.sh linux_i386 i386-linux-gnu qemu-i386 -microarch:pentium4 # ./cross.sh linux_arm32 arm-linux-gnueabihf qemu-arm # ./cross.sh linux_riscv64 riscv64-linux-gnu qemu-riscv64 +# +# i386 needs a microarch: below SSE2 the x86 backend cannot legalise a +# sub-16-byte `f16` vector, and merely declaring a `proc "c"` that takes a +# `#simd[2]f16` aborts the compiler with "LLVM ERROR: Do not know how to split +# the result of this operator!". +# +# `pentium4` is what clang's own default for `i386-linux-gnu` is, `haswell` +# additionally has F16C, which isolates one non-ABI failure: `hfa4_f16` +# diverges at pentium4 and agrees at haswell. It is `_Float16` conversion +# codegen rather than a calling convention TARGET=${1:?odin target, e.g. linux_arm64} TRIPLE=${2:?clang triple, e.g. aarch64-linux-gnu} @@ -115,6 +125,16 @@ set +e rc=$? set -e +# A driver that DIES reports 128+signal, and that collides with the type indices: 139 is both +# SIGSEGV and a perfectly good index, so reading it as an index names an innocent type. There is no +# cheap way to tell them apart here. ABI_SKIP is a compile-time `-define` +if [ "$rc" -gt 128 ] && [ "$rc" -lt 165 ]; then + echo "$TARGET: exit $rc is AMBIGUOUS." >&2 + echo " Either type index $rc, or the driver died of signal $((rc-128)) (11 = SIGSEGV)." >&2 + echo " Re-run with -define:ABI_SKIP=$((rc+1)): if the result moves it was the type," >&2 + echo " and if it does not, a wrong-ABI call is corrupting the process." >&2 +fi + if [ "$rc" -eq 0 ]; then echo "$TARGET: every type agrees with clang" else From 9047c60ec137f4a0c77ab2ea0cc57c08fe28c0dd Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 18:08:11 -0700 Subject: [PATCH 24/32] arm32: default to arm1176jzf-s so the gnueabihf triple actually has an FPU, arm32: implement AAPCS32 homogeneous float aggregates incl unions, arm32: fix reversed arguments in the small-struct return coercion,abi test: __aeabi_uldivmod so the arm32 corpus links --- src/llvm_abi.cpp | 62 +++++++++++++++++++++++++++++++++++++++----- src/llvm_backend.cpp | 7 +++++ tests/abi/cross.sh | 28 ++++++++++++++++---- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 08d6adf69..6d48272e9 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -2079,14 +2079,14 @@ namespace lbAbiWasm { namespace lbAbiArm32 { 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); + 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, original_type); - ft->ret = compute_return_type(c, return_type, return_is_defined, calling_convention); + 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; } @@ -2115,6 +2115,47 @@ namespace lbAbiArm32 { return lb_arg_type_direct(type, nullptr, nullptr, attr); } + // 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; + 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); @@ -2129,6 +2170,8 @@ namespace lbAbiArm32 { // 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 (LLVMTypeRef hfa_coerce = nullptr; 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); @@ -2144,17 +2187,24 @@ 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 (!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); diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index 95df563fd..1f71257f8 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/tests/abi/cross.sh b/tests/abi/cross.sh index 1a700114c..64ad01526 100755 --- a/tests/abi/cross.sh +++ b/tests/abi/cross.sh @@ -20,10 +20,12 @@ set -eu # `#simd[2]f16` aborts the compiler with "LLVM ERROR: Do not know how to split # the result of this operator!". # -# `pentium4` is what clang's own default for `i386-linux-gnu` is, `haswell` -# additionally has F16C, which isolates one non-ABI failure: `hfa4_f16` -# diverges at pentium4 and agrees at haswell. It is `_Float16` conversion -# codegen rather than a calling convention +# `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} @@ -54,7 +56,22 @@ _start: _start: bl probe_main mov r7, #1 - svc #0' ;; + 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: @@ -102,6 +119,7 @@ static u64 udivmod(u64 a, u64 b, u64 *rem){ u64 q=0,r=0; if(b==0){ if(rem)*rem=0; return 0; } for(int i=63;i>=0;i--){ r=(r<<1)|((a>>i)&1); if(r>=b){ r-=b; q|=(u64)1< Date: Fri, 14 Aug 2026 18:25:12 -0700 Subject: [PATCH 25/32] arm32 float linkage --- base/runtime/internal.odin | 68 ++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 18 deletions(-) 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 { From eb72d8ffbef5e325b0aaf0aeb58e22b2bca0a247 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 18:42:23 -0700 Subject: [PATCH 26/32] arm32: pass homogeneous vector aggregates in the VFP registers arm32: coerce narrow bare vectors, and return wide ones through a hidden pointer --- src/llvm_abi.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index 6d48272e9..eaafdae24 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -2111,6 +2111,20 @@ namespace lbAbiArm32 { } 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); } @@ -2147,6 +2161,17 @@ namespace lbAbiArm32 { 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; } @@ -2190,6 +2215,13 @@ namespace lbAbiArm32 { 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); From f15d9ea4c7a190a6d5c3b0922156ec760a893c85 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 18:53:31 -0700 Subject: [PATCH 27/32] abi test: cover cstring and uint family --- tests/abi/gen.odin | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/abi/gen.odin b/tests/abi/gen.odin index 3317c16b9..47eb05ee3 100644 --- a/tests/abi/gen.odin +++ b/tests/abi/gen.odin @@ -43,6 +43,7 @@ scalar :: proc(tag: string) -> Scalar { 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) } @@ -117,7 +118,8 @@ val :: proc(i: int, tag: string) -> string { c_val :: proc(tag, v: string) -> string { switch tag { - case "ptr": return tp("(void *)(intptr_t)(%s)", v) + 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 + %s.0if)", v, v) @@ -279,6 +281,10 @@ build :: proc() { {"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"}, } for tags in combos { odin_members := make([]string, len(tags), context.temp_allocator) @@ -304,7 +310,7 @@ build :: proc() { // 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", "f32"}) { + for tag in ([]string{"i8", "u8", "i16", "u16", "bool", "i32", "u32", "u64", "f32"}) { s := scalar(tag) v := val(0, tag) expected := tag == "bool" ? odin_val(tag, v) : tp("%s(%s)", s.odin, v) @@ -319,6 +325,25 @@ build :: proc() { ) } + // --- 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)}), + ) + } + // --- arrays: the same eightbytes from one declaration for tag in ([]string{"f32", "f64", "i32", "i64", "i8", "f16", "enum", "i128"}) { for cnt in 1 ..= 5 { From 1b57f6e0926ebd4903336d0dc1abc8fdd14d556a Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 19:34:49 -0700 Subject: [PATCH 28/32] remove cpp17 feature --- src/llvm_abi.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index eaafdae24..b4c866855 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -2192,10 +2192,12 @@ namespace lbAbiArm32 { } 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 (LLVMTypeRef hfa_coerce = nullptr; is_hfa(c, t, srcs[i], calling_convention, &hfa_coerce)) { + } 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 From 0fb1721e45463a6b43e53549c5c52c2be07ac82e Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 21:09:26 -0700 Subject: [PATCH 29/32] i386: return a complex of eight bytes or fewer in EAX:EDX, not through a hidden pointer abi test: cover bare f16, complex, rawptr, enum and bit_set --- src/llvm_abi.cpp | 12 ++++++++++++ tests/abi/gen.odin | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/llvm_abi.cpp b/src/llvm_abi.cpp index b4c866855..4ecfaad06 100644 --- a/src/llvm_abi.cpp +++ b/src/llvm_abi.cpp @@ -499,6 +499,18 @@ namespace lbAbi386 { } } + // 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; } diff --git a/tests/abi/gen.odin b/tests/abi/gen.odin index 47eb05ee3..685134f89 100644 --- a/tests/abi/gen.odin +++ b/tests/abi/gen.odin @@ -310,10 +310,22 @@ build :: proc() { // 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"}) { + 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 := tag == "bool" ? odin_val(tag, v) : tp("%s(%s)", s.odin, v) + 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, From 2a48374df360cd366920b59fa660fb712bbdd305 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 22:51:42 -0700 Subject: [PATCH 30/32] abi harness note on vararg exclusion; add quaternions & complex --- tests/abi/gen.odin | 113 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/tests/abi/gen.odin b/tests/abi/gen.odin index 685134f89..611b5343d 100644 --- a/tests/abi/gen.odin +++ b/tests/abi/gen.odin @@ -122,8 +122,8 @@ c_val :: proc(tag, v: string) -> string { 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 + %s.0if)", v, v) - case "c128": return tp("(%s.0 + %s.0i)", v, 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 @@ -134,8 +134,8 @@ odin_val :: proc(tag, v: string) -> string { 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, %s))", v, v) - case "c128": return tp("complex128(complex(%s, %s))", v, 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 @@ -147,8 +147,8 @@ mutated :: proc(tag, v: string) -> string { 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, %s))", as_int(v) + 1, v) - case "c128": return tp("complex128(complex(%d, %s))", as_int(v) + 1, v) + 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 @@ -356,6 +356,53 @@ build :: proc() { ) } + // --- 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), + } + } + 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 { @@ -748,11 +795,14 @@ build :: proc() { ) } - // NOTE: `complex64`/`complex128` are deliberately absent. Their members have - // no common accessor -- Odin spells it `real(x)`, C spells it `__real__ x`, a - // prefix operator rather than a member -- so a per-field check cannot be - // generated from one path. Measured separately as agreeing with clang on - // x86-64, aarch64 and riscv64; add them if the accessor problem is solved. + // 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 @@ -1181,11 +1231,22 @@ ABI_MUTATE :: #config(ABI_MUTATE, false) // Variadic coverage, OFF by default. // -// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the -// raw aggregate where clang coerces per the psABI -- so 111 of the types here -// fail. That is one defect, not 111, and leaving it on would drown every other -// signal. Turn it on with ` + "`-define:ABI_VARARGS=true`" + ` to measure it. -ABI_VARARGS :: #config(ABI_VARARGS, false) +// 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. @@ -1305,10 +1366,22 @@ ABI_SKIP :: #config(ABI_SKIP, 0) // Variadic coverage, OFF by default. // -// Odin does not ABI-classify a variadic argument at all -- it hands LLVM the -// raw aggregate where clang coerces per the psABI -- so 111 of the types here -// fail. That is one defect, not 111, and leaving it on would drown every other -// signal. Turn it on with ` + "`-define:ABI_VARARGS=true`" + ` to measure it. +// 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) From 38e053b96c010552b36d46101e2f46b646f76200 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Fri, 14 Aug 2026 23:02:04 -0700 Subject: [PATCH 31/32] i386: cap cmplx / quaternion alignment; abi test: cover a narrow member in front of complex --- src/types.cpp | 25 ++++++++++++++++--------- tests/abi/gen.odin | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/types.cpp b/src/types.cpp index b51e44a45..8e7fea777 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -4353,6 +4353,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) { @@ -4377,10 +4389,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; @@ -4531,13 +4544,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)? - i64 max_align = build_context.max_align; - if (build_context.metrics.arch == TargetArch_i386 && - build_context.metrics.os != TargetOs_windows) { - // the i386 System V psABI aligns every scalar to at most 4, unlike Windows - max_align = gb_min(max_align, 4); - } - return gb_clamp(next_pow2(type_size_of_internal(t, path)), 1, 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/gen.odin b/tests/abi/gen.odin index 611b5343d..a793faf30 100644 --- a/tests/abi/gen.odin +++ b/tests/abi/gen.odin @@ -285,6 +285,9 @@ build :: proc() { // 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) @@ -390,6 +393,33 @@ build :: proc() { 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, @@ -1247,6 +1277,7 @@ ABI_MUTATE :: #config(ABI_MUTATE, false) // // 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) From db8aa97db78142db781e55ec4ee7a12b1b29980e Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 19:56:26 -0700 Subject: [PATCH 32/32] matrix: take the input-vector load alignment from the vector --- src/llvm_backend_expr.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/llvm_backend_expr.cpp b/src/llvm_backend_expr.cpp index 128b688ce..7f334773b 100644 --- a/src/llvm_backend_expr.cpp +++ b/src/llvm_backend_expr.cpp @@ -1223,7 +1223,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);