commit b436d6ef93fea4096f6179be8fc1ac086d0f1740
parent a3a93d53b51655c1a9a307c1f658d018ef4dfd28
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 06:26:01 -0700
rv32: default to soft-float (ilp32), not hard-single (ilp32f)
The rv32 default profile was rv32imafc/ilp32f, so a forgotten -march/-mabi
silently emitted FLW/FSW that fault on the FPU-less parts that dominate the
32-bit RISC-V MCU market (ESP32-C3 = rv32imc). Drop F from the rv32 default
(rv64 keeps lp64d); the FPU is now opt-in via -march=rv32imafc -mabi=ilp32f,
which still emits fadd.s (the explicit flag overrides defaults, unchanged).
The float-ABI predefined macros were a static per-arch table that could encode
only one profile, so rt/lib/coro/riscv32.c (which keys on __riscv_flen) was
built with flen=32 even for soft targets (FP save/restore in soft code), and
rv64 lp64 soft builds got __riscv_float_abi_double. Make the float macros
Target-aware via a new ArchImpl.float_predefines hook
(kit_compiler_arch_float_predefines), emitting __riscv_float_abi_*/__riscv_flen/
__riscv_fdiv/__riscv_fsqrt per resolved float_abi so the preprocessor always
agrees with the codegen ABI.
Tested: test-smoke-rv32 (ilp32 + ilp32f, -O0/-O1, qemu) 7/0, test-cg-api,
test-abi-classify 384/0, test-smoke-rv64 3/0.
Diffstat:
6 files changed, 125 insertions(+), 25 deletions(-)
diff --git a/include/kit/compile.h b/include/kit/compile.h
@@ -313,6 +313,16 @@ KIT_API void kit_frontend_free_options(KitCompiler*, KitLanguage, void* opts);
KIT_API uint32_t kit_compiler_arch_predefines(KitCompiler*,
const KitPredefinedMacro** out);
+/* Float-ABI-dependent predefined macros for the compiler's resolved target,
+ * separate from kit_compiler_arch_predefines because a single static table
+ * cannot encode both the soft and the hard-float profiles while the
+ * preprocessor must agree with the codegen ABI (e.g. RISC-V's __riscv_flen,
+ * which rt/lib/coro keys on, and __riscv_float_abi_*). Sets *out to a borrowed
+ * static-lifetime array and returns its length; 0 when the target's arch has
+ * no float-ABI axis. */
+KIT_API uint32_t kit_compiler_arch_float_predefines(
+ KitCompiler*, const KitPredefinedMacro** out);
+
typedef struct KitCompileSessionOptions {
KitLanguage lang;
KitFrontendCompileOptions compile;
diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c
@@ -856,6 +856,17 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, arch_defs[i].name.s, arch_defs[i].body.s);
}
+ /* Float-ABI-dependent arch macros (e.g. RISC-V __riscv_float_abi_* /
+ * __riscv_flen) come from a separate getter because the static table above
+ * can encode only one float profile; these track the resolved float ABI. */
+ {
+ const KitPredefinedMacro* fp_defs = NULL;
+ uint32_t nfp_defs = kit_compiler_arch_float_predefines(pp->c, &fp_defs);
+ for (i = 0; i < nfp_defs; ++i) {
+ pp_define(pp, fp_defs[i].name.s, fp_defs[i].body.s);
+ }
+ }
+
/* __USER_LABEL_PREFIX__ is the C source-symbol prefix the object format
* prepends ("_" for Mach-O, "" else); read it from the CG target rather
* than re-deriving from the object-format identity. */
diff --git a/src/api/compile.c b/src/api/compile.c
@@ -494,6 +494,17 @@ uint32_t kit_compiler_arch_predefines(KitCompiler* c,
return arch->npredefined_macros;
}
+uint32_t kit_compiler_arch_float_predefines(KitCompiler* c,
+ const KitPredefinedMacro** out) {
+ const ArchImpl* arch;
+ KitTargetSpec spec;
+ if (out) *out = NULL;
+ if (!c) return 0;
+ arch = arch_for_compiler((Compiler*)c);
+ spec = kit_compiler_target_spec(c);
+ return arch_float_predefines(arch, &spec, out);
+}
+
static void validate_bytes(Compiler* c, const KitSourceInput* in) {
if (!in->name.s) panic_bad_options(c, "input name is NULL");
if (!in->bytes.data && in->bytes.len != 0) {
diff --git a/src/arch/arch.h b/src/arch/arch.h
@@ -350,6 +350,19 @@ typedef struct ArchImpl {
KitTargetSpec* spec, const u64* feature_words,
u32 nfeature_words, KitSlice abi, char* err,
size_t errcap);
+
+ /* Float-ABI-dependent predefined macros for the resolved target. A single
+ * static `predefined_macros` table cannot encode both the soft and the
+ * hard-float profiles, yet the preprocessor must agree with the codegen ABI:
+ * rt/lib/coro keys on __riscv_flen to decide whether a context switch saves
+ * the FP registers, and user code branches on __riscv_float_abi_*. RISC-V
+ * sets this hook to emit __riscv_float_abi_{soft,single,double} (plus
+ * __riscv_flen / __riscv_fdiv / __riscv_fsqrt for the hard cases) keyed on
+ * spec->float_abi, so those macros stay out of the static table. NULL means
+ * the arch has no float-ABI axis and its static table is complete. The
+ * returned entries are static-lifetime. Read via arch_float_predefines. */
+ u32 (*float_predefines)(const struct ArchImpl* impl, const KitTargetSpec* spec,
+ const KitPredefinedMacro** out);
} ArchImpl;
const ArchImpl* arch_lookup(KitArchKind);
@@ -380,6 +393,17 @@ static inline KitStatus arch_resolve_float_abi(const ArchImpl* impl,
err, errcap);
}
+/* Float-ABI-dependent predefined macros for `spec` (see ArchImpl.float_predefines).
+ * Sets *out to a borrowed static-lifetime array and returns its length; 0 (with
+ * *out=NULL) when the arch sets no hook. */
+static inline u32 arch_float_predefines(const ArchImpl* impl,
+ const KitTargetSpec* spec,
+ const KitPredefinedMacro** out) {
+ if (out) *out = NULL;
+ if (!impl || !impl->float_predefines) return 0;
+ return impl->float_predefines(impl, spec, out);
+}
+
/* Spelling for a relocated operand in `cc -S` text, for the compiler's target
* arch+format. Returns 1 and fills *out when symbolizable, 0 to keep numeric
* (also when the arch provides no asm_ops). Thin dispatch over ArchAsmOps. */
diff --git a/src/arch/riscv/arch.c b/src/arch/riscv/arch.c
@@ -113,19 +113,17 @@ static int rv64_apply_label_fixup(Compiler* c, const ArchLabelFixup* fx) {
/* Mirrors `clang --target=riscv64-linux-gnu -E -dM` for the in-scope
* RV64GC profile: I/M/F/D/A/C + Zicsr-minimal. Macros that depend on
* extensions outside scope (V, B, Zve*, Zfh, …) are deliberately
- * absent. ABI variant is lp64d. */
+ * absent. The float-ABI-dependent macros (__riscv_float_abi_*, __riscv_flen,
+ * __riscv_fdiv, __riscv_fsqrt) are emitted per resolved float_abi by
+ * rv64_float_predefines, not baked here — see that hook. Default ABI is lp64d. */
static const KitPredefinedMacro rv64_predefined_macros[] = {
{KIT_SLICE_LIT("__riscv"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_xlen"), KIT_SLICE_LIT("64")},
- {KIT_SLICE_LIT("__riscv_float_abi_double"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_atomic"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_mul"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_div"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_muldiv"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_compressed"), KIT_SLICE_LIT("1")},
- {KIT_SLICE_LIT("__riscv_flen"), KIT_SLICE_LIT("64")},
- {KIT_SLICE_LIT("__riscv_fdiv"), KIT_SLICE_LIT("1")},
- {KIT_SLICE_LIT("__riscv_fsqrt"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_zicsr"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_zifencei"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_arch_test"), KIT_SLICE_LIT("1")},
@@ -137,22 +135,20 @@ static const KitPredefinedMacro rv64_predefined_macros[] = {
{KIT_SLICE_LIT("__LITTLE_ENDIAN__"), KIT_SLICE_LIT("1")},
};
-/* Mirrors `clang --target=riscv32-linux-gnu -march=rv32imafc_zicsr_zifencei
- * -mabi=ilp32f -E -dM` for the DEFAULT ilp32f hard-single profile:
- * I/M/F/A/C + Zicsr-minimal, single-precision float ABI (double is soft).
- * __riscv_flen=32 (no D). __ILP32__/_ILP32 replace __LP64__/_LP64.
+/* Mirrors `clang --target=riscv32-linux-gnu -march=rv32imac_zicsr_zifencei
+ * -mabi=ilp32 -E -dM` for the DEFAULT ilp32 SOFT-float MCU profile:
+ * I/M/A/C + Zicsr-minimal, no hardware float. __ILP32__/_ILP32 replace
+ * __LP64__/_LP64.
*
- * Known v1 limitation: predefined macros are a static (ptr,count) table
- * consumed without a Target (src/api/compile.c), so this fixed table reflects
- * the default ilp32f profile only. Soft-float ilp32 codegen correctness is
- * driven by c->target.float_abi, not these macros. */
+ * The float-ABI-dependent macros (__riscv_float_abi_*, __riscv_flen,
+ * __riscv_fdiv, __riscv_fsqrt) are NOT baked here: a static table can encode
+ * only one float profile, but rv32 supports soft (ilp32, the default), single
+ * (ilp32f) and a soft-double layout, and the preprocessor must agree with the
+ * codegen ABI (rt/lib/coro keys on __riscv_flen). rv64_float_predefines emits
+ * them per resolved c->target.float_abi instead — shared by rv32 and rv64. */
static const KitPredefinedMacro rv32_predefined_macros[] = {
{KIT_SLICE_LIT("__riscv"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_xlen"), KIT_SLICE_LIT("32")},
- {KIT_SLICE_LIT("__riscv_float_abi_single"), KIT_SLICE_LIT("1")},
- {KIT_SLICE_LIT("__riscv_flen"), KIT_SLICE_LIT("32")},
- {KIT_SLICE_LIT("__riscv_fdiv"), KIT_SLICE_LIT("1")},
- {KIT_SLICE_LIT("__riscv_fsqrt"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_atomic"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_mul"), KIT_SLICE_LIT("1")},
{KIT_SLICE_LIT("__riscv_div"), KIT_SLICE_LIT("1")},
@@ -303,11 +299,15 @@ static void rv64_target_feature_defaults(const Target* target, u64* words,
rv64_feature_set(words, nwords, RV64_FEAT_I);
rv64_feature_set(words, nwords, RV64_FEAT_M);
rv64_feature_set(words, nwords, RV64_FEAT_A);
- rv64_feature_set(words, nwords, RV64_FEAT_F);
- /* rv32 default profile is rv32imafc_zicsr_zifencei (ilp32f hard-single) —
- * no D. rv64 keeps the full G+C (lp64d) profile including D. */
- if (target->arch != KIT_ARCH_RV32)
+ /* rv32 default profile is rv32imac_zicsr_zifencei (ilp32 SOFT float): most
+ * 32-bit RISC-V microcontrollers (e.g. ESP32-C3 = rv32imc) ship no F/D
+ * hardware, and a hard-float default silently emits FLW/FSW that fault on the
+ * chip. Opt into the FPU explicitly with -march=rv32imafc -mabi=ilp32f. rv64
+ * keeps the full G+C (lp64d) profile including F+D. */
+ if (target->arch != KIT_ARCH_RV32) {
+ rv64_feature_set(words, nwords, RV64_FEAT_F);
rv64_feature_set(words, nwords, RV64_FEAT_D);
+ }
rv64_feature_set(words, nwords, RV64_FEAT_C);
rv64_feature_set(words, nwords, RV64_FEAT_ZICSR);
rv64_feature_set(words, nwords, RV64_FEAT_ZIFENCEI);
@@ -478,6 +478,46 @@ static KitStatus rv64_resolve_float_abi(const ArchImpl* impl,
return KIT_OK;
}
+/* Float-ABI-dependent predefined macros, keyed on the resolved float ABI (see
+ * ArchImpl.float_predefines). kit's RISC-V codegen only touches FP registers
+ * under a hardware-float ABI, so __riscv_flen (and the coro FP-save it gates)
+ * tracks float_abi rather than the raw F/D ISA bits. Shared by rv32 + rv64. */
+static const KitPredefinedMacro rv_float_macros_soft[] = {
+ {KIT_SLICE_LIT("__riscv_float_abi_soft"), KIT_SLICE_LIT("1")},
+};
+static const KitPredefinedMacro rv_float_macros_single[] = {
+ {KIT_SLICE_LIT("__riscv_float_abi_single"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__riscv_flen"), KIT_SLICE_LIT("32")},
+ {KIT_SLICE_LIT("__riscv_fdiv"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__riscv_fsqrt"), KIT_SLICE_LIT("1")},
+};
+static const KitPredefinedMacro rv_float_macros_double[] = {
+ {KIT_SLICE_LIT("__riscv_float_abi_double"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__riscv_flen"), KIT_SLICE_LIT("64")},
+ {KIT_SLICE_LIT("__riscv_fdiv"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__riscv_fsqrt"), KIT_SLICE_LIT("1")},
+};
+
+static u32 rv64_float_predefines(const ArchImpl* impl,
+ const KitTargetSpec* spec,
+ const KitPredefinedMacro** out) {
+ (void)impl;
+ switch ((KitFloatAbi)spec->float_abi) {
+ case KIT_FLOAT_ABI_SINGLE:
+ *out = rv_float_macros_single;
+ return (u32)(sizeof rv_float_macros_single / sizeof rv_float_macros_single[0]);
+ case KIT_FLOAT_ABI_DOUBLE:
+ *out = rv_float_macros_double;
+ return (u32)(sizeof rv_float_macros_double / sizeof rv_float_macros_double[0]);
+ case KIT_FLOAT_ABI_SOFT:
+ case KIT_FLOAT_ABI_DEFAULT:
+ break;
+ }
+ /* SOFT (and the unreached DEFAULT — RISC-V always resolves a concrete ABI). */
+ *out = rv_float_macros_soft;
+ return (u32)(sizeof rv_float_macros_soft / sizeof rv_float_macros_soft[0]);
+}
+
const ArchImpl arch_impl_rv64 = {
.backend = {.name = "rv64", .make = rv64_backend_make},
.kind = KIT_ARCH_RV64,
@@ -517,13 +557,14 @@ const ArchImpl arch_impl_rv64 = {
.supports_call_conv = rv64_supports_call_conv,
.supports_intrinsic = rv64_supports_intrinsic,
.resolve_float_abi = rv64_resolve_float_abi,
+ .float_predefines = rv64_float_predefines,
};
/* RV32 shares nearly all of the RISC-V backend with rv64 — the per-XLEN
* differences are threaded through RiscvVariant inside the shared functions.
* Differs only in: backend/arch names + kind, the link descriptor + dbg ops
- * (rv_lw / 2-byte min insn), the ilp32f predefined-macro table, and the CFI
- * data alignment factor (-4 word stride vs rv64's -8 doubleword). */
+ * (rv_lw / 2-byte min insn), the ilp32 (soft-float) predefined-macro table, and
+ * the CFI data alignment factor (-4 word stride vs rv64's -8 doubleword). */
const ArchImpl arch_impl_rv32 = {
.backend = {.name = "rv32", .make = rv64_backend_make},
.kind = KIT_ARCH_RV32,
@@ -564,4 +605,5 @@ const ArchImpl arch_impl_rv32 = {
.supports_call_conv = rv64_supports_call_conv,
.supports_intrinsic = rv64_supports_intrinsic,
.resolve_float_abi = rv64_resolve_float_abi,
+ .float_predefines = rv64_float_predefines,
};
diff --git a/test/api/abi_classify_test.c b/test/api/abi_classify_test.c
@@ -612,8 +612,10 @@ static void check_scalar_split_lane_target(KitArchKind arch, KitOSKind os,
}
static void test_scalar_split_lane_size(void) {
- /* RV32's default profile is ilp32f: 64-bit ints and soft doubles are
- * represented as two 4-byte integer lanes. */
+ /* RV32's default profile is ilp32 (soft float): 64-bit ints and (always-soft)
+ * doubles are represented as two 4-byte integer lanes. The split-lane sizes
+ * are the same under ilp32f, since rv32 has no D and passes doubles in
+ * integer-register pairs regardless of the float ABI. */
check_scalar_split_lane_target(KIT_ARCH_RV32, KIT_OS_LINUX, KIT_OBJ_ELF,
"rv32", 4, 4);
check_scalar_split_lane_target(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF,