kit

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

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:
Minclude/kit/compile.h | 10++++++++++
Mlang/cpp/pp/pp.c | 11+++++++++++
Msrc/api/compile.c | 11+++++++++++
Msrc/arch/arch.h | 24++++++++++++++++++++++++
Msrc/arch/riscv/arch.c | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtest/api/abi_classify_test.c | 6++++--
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,