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:
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();
}