From a50c9e0a1dbef555b79874e5f278e2f3805bb39e Mon Sep 17 00:00:00 2001 From: kalsprite Date: Thu, 13 Aug 2026 21:51:53 -0700 Subject: [PATCH] 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.