kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 167d1a17f58b9fcca5a88d77d253903e0babc931
parent 889ad29eec3c9e6ab03bc7592c419335285b2463
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 19 May 2026 15:32:03 -0700

abi: classify wide16 scalars (i128, long double) in SysV-x64

Today abi_sysv_x64 emits a single 16B INT part for any 16-byte scalar.
That is malformed: no GPR can hold a 16B value, and the CG-layer
api_is_wide16_scalar_type shortcut quietly papered over it by forcing
the value through a memory image regardless of the ABI's output.

Split the wide16 cases out so the classifier itself produces the right
shape:

- __int128 / __uint128: DIRECT with two 8B INT parts at offsets 0/8,
  matching what RV64 and AAPCS64 already do (psABI: two INTEGER
  eightbytes, rdi+rsi for args, rax+rdx for return).
- long double: x87 80-bit padded to 16B with 16B alignment. cfree has
  no x87 backend, so route through memory (INDIRECT + SRET/BYVAL),
  which is what the wide16 CG shortcut was forcing anyway.

This is Phase 2a of doc/CBACKEND.md and is a prerequisite for deleting
the CG-layer wide16 shortcut.

Adds test/api/abi_classify_test.c — a focused ABI-vtable regression
suite covering i128 and f128 across SysV-x64, AAPCS64, Apple-ARM64,
and RV64.

Diffstat:
Msrc/abi/abi_sysv_x64.c | 37+++++++++++++++++++++++++++++++++++--
Atest/api/abi_classify_test.c | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 8+++++++-
3 files changed, 276 insertions(+), 3 deletions(-)

diff --git a/src/abi/abi_sysv_x64.c b/src/abi/abi_sysv_x64.c @@ -25,8 +25,41 @@ static void classify_void(ABIArgInfo* out) { out->kind = ABI_ARG_IGNORE; } -static void classify_scalar(TargetABI* a, CfreeCgTypeId t, ABIArgInfo* out) { +static void classify_scalar(TargetABI* a, CfreeCgTypeId t, ABIArgInfo* out, + int is_return) { ABITypeInfo ti = abi_internal_type_info(a, t); + /* __int128 / __uint128: SysV psABI classifies as two INTEGER eightbytes + * (rdi+rsi etc. for args; rax+rdx for return). */ + if (ti.scalar_kind == ABI_SC_INT && ti.size == 16) { + ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, 2); + memset(parts, 0, sizeof(ABIArgPart) * 2); + for (u32 i = 0; i < 2; ++i) { + parts[i].cls = ABI_CLASS_INT; + parts[i].loc = ABI_LOC_REG; + parts[i].size = 8; + parts[i].align = 8; + parts[i].src_offset = i * 8; + } + out->kind = ABI_ARG_DIRECT; + out->flags = ABI_AF_NONE; + out->parts = parts; + out->nparts = 2; + out->indirect_align = 0; + return; + } + /* long double: 80-bit x87 (padded to 16B with 16B alignment). SysV class + * is X87/X87UP which always routes through memory. cfree has no x87 + * backend, so route through a stack image — sret for return, byval for + * args — consistent with the rest of the in-memory aggregate path. */ + if (ti.scalar_kind == ABI_SC_FLOAT && ti.size == 16) { + out->kind = ABI_ARG_INDIRECT; + out->flags = is_return ? ABI_AF_SRET : ABI_AF_BYVAL; + out->indirect_align = ti.align ? ti.align : 16; + out->parts = NULL; + out->nparts = 0; + return; + } + out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; out->indirect_align = 0; @@ -93,7 +126,7 @@ static void classify_one(TargetABI* a, CfreeCgTypeId t, ABIArgInfo* out, classify_one(a, ty->alias.base, out, is_return); return; default: - classify_scalar(a, t, out); + classify_scalar(a, t, out, is_return); return; } } diff --git a/test/api/abi_classify_test.c b/test/api/abi_classify_test.c @@ -0,0 +1,234 @@ +/* ABI classification regression tests for wide16 scalars (i128, long double). + * + * Locks in the behaviour described in doc/CBACKEND.md "Wide16 classification + * is incomplete in some native ABIs". Each (target, type) case asserts the + * shape the ABI vtable should produce for an argument and a return — i.e. + * what the C frontend / CG layer would see if the wide16 CG-layer shortcut + * were removed. */ + +#include <cfree/cg.h> +#include <cfree/core.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "abi/abi.h" +#include "core/core.h" + +static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { + (void)h; + (void)a; + return n ? malloc(n) : NULL; +} +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { + (void)h; + (void)o; + (void)a; + return realloc(p, n); +} +static void h_free(CfreeHeap* h, void* p, size_t n) { + (void)h; + (void)n; + free(p); +} +static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; + +static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) { + (void)s; + (void)loc; + fprintf(stderr, "diag %d: ", (int)k); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); +} +static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; + +static int g_fail; + +#define EXPECT(cond, ...) \ + do { \ + if (!(cond)) { \ + ++g_fail; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +/* Storage outlives every Compiler; cfree_compiler_new just stores `ctx`. */ +static CfreeContext g_ctx; + +static CfreeCompiler* new_compiler(CfreeArchKind arch, CfreeOSKind os, + CfreeObjFmt obj) { + CfreeTarget t; + CfreeCompiler* c = NULL; + memset(&t, 0, sizeof t); + t.arch = arch; + t.os = os; + t.obj = obj; + t.ptr_size = 8; + t.ptr_align = 8; + memset(&g_ctx, 0, sizeof g_ctx); + g_ctx.heap = &g_heap; + g_ctx.diag = &g_diag; + if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + fprintf(stderr, "compiler_new failed for arch=%d os=%d\n", (int)arch, + (int)os); + exit(2); + } + return c; +} + +/* Build a function type `ret_ty fn(arg_ty)` and return its ABIFuncInfo. */ +static const ABIFuncInfo* classify_fn(CfreeCompiler* c, CfreeCgTypeId ret_ty, + CfreeCgTypeId arg_ty) { + CfreeCgFuncParam param; + CfreeCgFuncSig sig; + CfreeCgTypeId fn; + memset(&param, 0, sizeof param); + param.type = arg_ty; + memset(&sig, 0, sizeof sig); + sig.ret = ret_ty; + sig.params = &param; + sig.nparams = 1; + fn = cfree_cg_type_func(c, sig); + return abi_cg_func_info(((Compiler*)c)->abi, fn); +} + +static const char* arch_name(CfreeArchKind a) { + switch (a) { + case CFREE_ARCH_X86_64: return "sysv-x64"; + case CFREE_ARCH_ARM_64: return "aarch64"; + case CFREE_ARCH_RV64: return "rv64"; + default: return "?"; + } +} +static const char* os_name(CfreeOSKind o) { + switch (o) { + case CFREE_OS_LINUX: return "linux"; + case CFREE_OS_MACOS: return "macos"; + default: return "?"; + } +} + +/* Assert: arg/ret classify as DIRECT with two 8-byte INT parts at offsets 0/8. + * This is the shape every native ABI uses for i128 (and for RV64, also f128). + * Used as both the green case (RV64) and the post-fix expectation (SysV-x64). */ +static void expect_direct_2x_int8(const char* tag, const ABIArgInfo* ai) { + EXPECT(ai->kind == ABI_ARG_DIRECT, "%s: kind=%d want DIRECT", tag, + (int)ai->kind); + EXPECT(ai->nparts == 2, "%s: nparts=%u want 2", tag, (unsigned)ai->nparts); + if (ai->nparts != 2 || !ai->parts) return; + for (u32 i = 0; i < 2; ++i) { + EXPECT(ai->parts[i].cls == ABI_CLASS_INT, "%s: parts[%u].cls=%d want INT", + tag, i, (int)ai->parts[i].cls); + EXPECT(ai->parts[i].size == 8, "%s: parts[%u].size=%u want 8", tag, i, + (unsigned)ai->parts[i].size); + EXPECT(ai->parts[i].src_offset == i * 8u, + "%s: parts[%u].src_offset=%u want %u", tag, i, + (unsigned)ai->parts[i].src_offset, (unsigned)(i * 8u)); + } +} + +/* Assert: classifies as INDIRECT (memory image). */ +static void expect_indirect(const char* tag, const ABIArgInfo* ai, + int is_return) { + EXPECT(ai->kind == ABI_ARG_INDIRECT, "%s: kind=%d want INDIRECT", tag, + (int)ai->kind); + EXPECT(ai->nparts == 0, "%s: nparts=%u want 0", tag, (unsigned)ai->nparts); + EXPECT(ai->indirect_align >= 8, "%s: indirect_align=%u want >=8", tag, + (unsigned)ai->indirect_align); + u32 expected_flag = is_return ? ABI_AF_SRET : ABI_AF_BYVAL; + EXPECT((ai->flags & expected_flag) != 0, + "%s: flags=0x%x missing %s", tag, (unsigned)ai->flags, + is_return ? "SRET" : "BYVAL"); +} + +/* Assert: DIRECT with a single FP part covering the full type. */ +static void expect_direct_1x_fp(const char* tag, const ABIArgInfo* ai, + u32 want_size) { + EXPECT(ai->kind == ABI_ARG_DIRECT, "%s: kind=%d want DIRECT", tag, + (int)ai->kind); + EXPECT(ai->nparts == 1, "%s: nparts=%u want 1", tag, (unsigned)ai->nparts); + if (ai->nparts != 1 || !ai->parts) return; + EXPECT(ai->parts[0].cls == ABI_CLASS_FP, "%s: parts[0].cls=%d want FP", tag, + (int)ai->parts[0].cls); + EXPECT(ai->parts[0].size == want_size, "%s: parts[0].size=%u want %u", tag, + (unsigned)ai->parts[0].size, want_size); + EXPECT(ai->parts[0].src_offset == 0, "%s: parts[0].src_offset=%u want 0", tag, + (unsigned)ai->parts[0].src_offset); +} + +static void check_target(CfreeArchKind arch, CfreeOSKind os, CfreeObjFmt obj) { + CfreeCompiler* c = new_compiler(arch, os, obj); + CfreeCgBuiltinTypes bi = cfree_cg_builtin_types(c); + CfreeCgTypeId i128_ty = bi.id[CFREE_CG_BUILTIN_I128]; + CfreeCgTypeId f128_ty = bi.id[CFREE_CG_BUILTIN_F128]; + EXPECT(i128_ty != CFREE_CG_TYPE_NONE, "%s/%s: missing i128 builtin", + arch_name(arch), os_name(os)); + EXPECT(f128_ty != CFREE_CG_TYPE_NONE, "%s/%s: missing f128 builtin", + arch_name(arch), os_name(os)); + + char tag[64]; + + /* i128 — every native ABI: DIRECT/2 INT parts of 8B. */ + { + const ABIFuncInfo* fi = classify_fn(c, i128_ty, i128_ty); + snprintf(tag, sizeof tag, "%s/%s i128 arg", arch_name(arch), os_name(os)); + expect_direct_2x_int8(tag, &fi->params[0]); + snprintf(tag, sizeof tag, "%s/%s i128 ret", arch_name(arch), os_name(os)); + expect_direct_2x_int8(tag, &fi->ret); + EXPECT(fi->has_sret == 0, "%s/%s: i128 should not set has_sret", + arch_name(arch), os_name(os)); + } + + /* f128 (long double / __float128). Per-target expectations differ. */ + { + const ABIFuncInfo* fi = classify_fn(c, f128_ty, f128_ty); + snprintf(tag, sizeof tag, "%s/%s f128 arg", arch_name(arch), os_name(os)); + if (arch == CFREE_ARCH_X86_64) { + /* SysV-x64: long double is x87 (80-bit padded to 16B). cfree lacks + * x87 support; classify as INDIRECT (memory) so it routes through + * a stack image consistent with the wide16 CG-layer shortcut. */ + expect_indirect(tag, &fi->params[0], /*is_return=*/0); + snprintf(tag, sizeof tag, "%s/%s f128 ret", arch_name(arch), + os_name(os)); + expect_indirect(tag, &fi->ret, /*is_return=*/1); + EXPECT(fi->has_sret == 1, "%s/%s: f128 ret should set has_sret", + arch_name(arch), os_name(os)); + } else if (arch == CFREE_ARCH_ARM_64) { + /* AAPCS64 / Apple ARM64: 128-bit FP scalar passes in a single Q + * register — DIRECT/1 FP part of 16B. */ + expect_direct_1x_fp(tag, &fi->params[0], 16); + snprintf(tag, sizeof tag, "%s/%s f128 ret", arch_name(arch), + os_name(os)); + expect_direct_1x_fp(tag, &fi->ret, 16); + EXPECT(fi->has_sret == 0, "%s/%s: f128 should not set has_sret", + arch_name(arch), os_name(os)); + } else if (arch == CFREE_ARCH_RV64) { + /* RV64 LP64D: long double passes like a 2*XLEN scalar — 2 INT parts. */ + expect_direct_2x_int8(tag, &fi->params[0]); + snprintf(tag, sizeof tag, "%s/%s f128 ret", arch_name(arch), + os_name(os)); + expect_direct_2x_int8(tag, &fi->ret); + EXPECT(fi->has_sret == 0, "%s/%s: f128 should not set has_sret", + arch_name(arch), os_name(os)); + } + } + + cfree_compiler_free(c); +} + +int main(void) { + check_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); + check_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); + check_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO); + check_target(CFREE_ARCH_RV64, CFREE_OS_LINUX, CFREE_OBJ_ELF); + if (g_fail) { + fprintf(stderr, "%d failures\n", g_fail); + return 1; + } + fprintf(stderr, "abi_classify_test: OK\n"); + return 0; +} diff --git a/test/test.mk b/test/test.mk @@ -104,14 +104,20 @@ $(AA64_ISA_TEST_BIN): test/arch/aa64_isa_test.c $(LIB_AR) $(CC) $(DRIVER_CFLAGS) -Isrc test/arch/aa64_isa_test.c $(LIB_AR) -o $@ CG_API_TEST_BIN = build/test/cg_api_test +ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test -test-cg-api: $(CG_API_TEST_BIN) +test-cg-api: $(CG_API_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) $(CG_API_TEST_BIN) + $(ABI_CLASSIFY_TEST_BIN) $(CG_API_TEST_BIN): test/api/cg_type_test.c $(LIB_AR) @mkdir -p $(dir $@) $(CC) $(DRIVER_CFLAGS) -Isrc test/api/cg_type_test.c $(LIB_AR) -o $@ +$(ABI_CLASSIFY_TEST_BIN): test/api/abi_classify_test.c $(LIB_AR) + @mkdir -p $(dir $@) + $(CC) $(DRIVER_CFLAGS) -Isrc test/api/abi_classify_test.c $(LIB_AR) -o $@ + test-toy: bin @CFREE=$(abspath $(BIN)) test/toy/run.sh