kit

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

commit 10d51e050c4a32261df88a66fdf4b45df33c3740
parent c7e3f5fb08e677e1bab9fb3734f4c2b42a396eac
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 14:51:14 -0700

fix(rv64): give the -O1 native emitter a 3rd scratch reg (t6)

rv64 -O1 codegen crashed with 'opt native emit: no scratch register' on a
scaled struct-array index (e.g. src/api/wasm_host.c:798 `w->fds[fd].used`):
the optimizer native emitter (src/opt/pass_native_emit.c) draws scratch regs
from rv64's pool of two (t4,t5), but the worst case needs three live at once —
a 3-operand binop, or a load/store whose scaled-index address must be
materialized into a scratch (collapse_addr_to_reg; rv64/aarch64 can't fold a
scaled index, x64 can). aarch64 reserves three scratches for exactly this; x64
gets away with two. Promote t6/x31 (rv64's lone caller-saved allocable int reg)
to the third native-emit scratch: drop it from rv_int_allocable, add to
rv_int_scratch, mark its phys entry RESERVED. rv64 -O1 now keeps no
caller-saved allocable int reg (call-crossing temps use callee-saved s1..s11).
Swept all 347 libkit/lang/driver sources at rv64 -O1: zero native-emit panics.

Also harden the panic path: compiler_panicv fell through to longjmp(c->panic)
when no panic frame was active, but that bare jmp_buf is never armed by anyone,
so it jumped into garbage (a wild SEGV masking the real fault — that is what
turned the scratch panic into a core dump). Trap deterministically instead; the
FATAL diagnostic is already emitted first.

Fixes rv64 -O1 self-host (release bootstrap); rv64 -O0 was already byte-identical.

Diffstat:
Msrc/arch/riscv/native.c | 26+++++++++++++++-----------
Msrc/core/core.c | 11++++++++++-
2 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -378,14 +378,18 @@ static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0x1fu; } .copy_cost = 0u} /* t0..t3 (x5,x6,x7,x28) are emit-internal scratch (RV_TMP0..RV_TMP3), reserved - * and never handed to the allocator or driver. t4/t5 are the driver scratch - * pool (disjoint from the emit temps so a hook can never clobber an operand the - * driver parked there). t6 is the lone caller-saved allocable (the -O0 cache's - * only caller-saved home); s1..s11 are appended callee-saved, chosen under - * pressure (and saved by the optimizer prologue at -O1). */ -static const Reg rv_int_allocable[] = {31u, 9u, 18u, 19u, 20u, 21u, - 22u, 23u, 24u, 25u, 26u, 27u}; -static const Reg rv_int_scratch[] = {29u, 30u}; /* t4, t5 */ + * and never handed to the allocator or driver. t4/t5/t6 are the native-emit + * scratch pool (disjoint from the emit temps so a hook can never clobber an + * operand parked there). Three are needed, not two: a load/store arch must + * materialize a scaled-index address into a scratch (collapse_addr_to_reg, e.g. + * `s->arr[i].field`), and the worst case — a 3-operand binop, or such an access + * — has three native-emit scratches live at once. aarch64 reserves three for + * the same reason; x64 needs only two because it folds the index into its + * addressing mode. s1..s11 (callee-saved) are the allocable set, saved by the + * optimizer prologue at -O1; rv64 keeps no caller-saved allocable int reg. */ +static const Reg rv_int_allocable[] = {9u, 18u, 19u, 20u, 21u, 22u, + 23u, 24u, 25u, 26u, 27u}; +static const Reg rv_int_scratch[] = {29u, 30u, 31u}; /* t4, t5, t6 */ static const NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_RESERVED(0u), /* zero */ @@ -408,9 +412,9 @@ static const NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_CALLEE(24u), RV_PHYS_INT_CALLEE(25u), RV_PHYS_INT_CALLEE(26u), RV_PHYS_INT_CALLEE(27u), RV_PHYS_INT_RESERVED(28u), /* t3 = TMP3 (emit) */ - RV_PHYS_INT_RESERVED(29u), /* t4 = driver scratch */ - RV_PHYS_INT_RESERVED(30u), /* t5 = driver scratch */ - RV_PHYS_INT_CALLER(31u), /* t6 = caller-saved allocable */ + RV_PHYS_INT_RESERVED(29u), /* t4 = native-emit scratch */ + RV_PHYS_INT_RESERVED(30u), /* t5 = native-emit scratch */ + RV_PHYS_INT_RESERVED(31u), /* t6 = native-emit scratch (3rd) */ }; #define RV_PHYS_FP_ARG(r, idx) \ diff --git a/src/core/core.c b/src/core/core.c @@ -225,5 +225,14 @@ void compiler_panicv(Compiler* c, SrcLoc loc, const char* fmt, va_list ap) { diag_emitv(c->ctx->diag, DIAG_FATAL, loc, fmt, ap); } if (c->panic_frame) longjmp(c->panic_frame->env, 1); - longjmp(c->panic, 1); + /* No active panic handler. Every compile/codegen entry point pushes a panic + * frame (compiler_panic_push + setjmp) before reaching code that can panic, so + * reaching here is an internal-consistency failure, not a recoverable error. + * The legacy bare `c->panic` jmp_buf is never armed by anyone, so longjmp'ing + * it would jump into garbage — a wild SEGV that hides the real fault. Trap + * deterministically instead; the FATAL diagnostic above has already been + * emitted, so the failing location is reported before we abort. (libkit is + * freestanding — __builtin_trap is the same unrecoverable primitive + * __kit_assert_fail uses; there is no exit() to call here.) */ + __builtin_trap(); }