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:
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(¶m, 0, sizeof param);
+ param.type = arg_ty;
+ memset(&sig, 0, sizeof sig);
+ sig.ret = ret_ty;
+ sig.params = ¶m;
+ 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