kit

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

commit 390b95747b59e7a73202cd9a9b15a89bd985c270
parent 0711b27c9cd911ac8d8794d313ea5cf03a88bc6f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 13:56:15 -0700

opt: reuse a transient scratch base when collapsing an address with no free scratch

A computed-goto store under -O1 register pressure (the labels-as-values bytecode
interpreter gnu_labels_as_values_threaded) panicked "no scratch register for
native emission" on arm32: the store's address consumed both opt-scratch
registers (ip as the materialized base, lr as the reloaded index), leaving none
to materialize the stored value, and collapse_addr_to_reg then could not find a
third scratch distinct from base+index.

When both scratch registers are consumed by the address itself, the base is
necessarily a reserved scratch register — it holds only a transient (a reloaded
spilled base or a materialized frame/global address) that is dead after the
access. So fold the index into the base in place (load_addr reads the base
before adding the scaled index, so dst==base is correct), freeing the other
scratch for the caller. Arch-neutral and only reached when no free scratch
exists, so it is a no-op on register-rich backends. gnu_labels now runs 35 at
O0 and O1 on arm32.

Diffstat:
Msrc/opt/pass_native_emit.c | 43+++++++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 8 deletions(-)

diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -234,6 +234,18 @@ static int scratch_available(NativeEmitCtx* e, NativeAllocClass cls, Reg a, return 0; } +/* True iff `r` is one of this class's reserved scratch registers. Such a register + * is never assigned to an allocator-live value: when it appears as an address's + * base it holds only a transient (a reloaded spilled base or a materialized + * frame/global address) that is dead after the current access. */ +static int reg_is_scratch(NativeEmitCtx* e, NativeAllocClass cls, Reg r) { + u32 c = (u32)cls; + if (r == (Reg)REG_NONE || c >= OPT_REG_CLASSES) return 0; + for (u32 i = 0; i < e->f->opt_scratch_reg_count[c]; ++i) + if (e->f->opt_scratch_regs[c][i] == r) return 1; + return 0; +} + static NativeLoc scratch_loc(NativeEmitCtx* e, KitCgTypeId type, NativeAllocClass cls, Reg a, Reg b, SrcLoc loc) { return loc_reg(type, cls, scratch_reg(e, cls, a, b, loc)); @@ -396,14 +408,29 @@ static Reg addr_index_reg(const NativeAddr* addr) { static void collapse_addr_to_reg(NativeEmitCtx* e, NativeAddr* addr, SrcLoc loc) { - /* Materialize the full address into a reserved scratch register. We must not - * reuse the base register as the destination: the register allocator may keep - * that value live past this memory op (e.g. a pointer stored into several of - * its own fields and then returned), so an in-place `add base, base, #off` - * would corrupt it. Avoid both base and index so load_addr can still read - * them. */ - Reg r = scratch_reg(e, NATIVE_REG_INT, addr_base_reg(addr), - addr_index_reg(addr), loc); + /* Materialize the full address into a reserved scratch register. We normally + * must not reuse the base register as the destination: the register allocator + * may keep that value live past this memory op (e.g. a pointer stored into + * several of its own fields and then returned), so an in-place `add base, + * base, #off` would corrupt it. Avoid both base and index so load_addr can + * still read them. + * + * Exception (the only case where no third scratch exists): when both base and + * index are themselves reserved scratch registers, the address consumed the + * entire scratch budget and there is nothing left to avoid them with. A + * scratch base holds only a transient dead after this access, so folding the + * index into it in place is sound and frees the other scratch for the caller. + * load_addr reads the base before adding the (scaled) index, so dst==base is + * correct; dst==index would clobber the index before use — base only. */ + Reg base = addr_base_reg(addr); + Reg index = addr_index_reg(addr); + Reg r; + if (scratch_available(e, NATIVE_REG_INT, base, index)) + r = scratch_reg(e, NATIVE_REG_INT, base, index, loc); + else if (reg_is_scratch(e, NATIVE_REG_INT, base)) + r = base; + else + r = scratch_reg(e, NATIVE_REG_INT, base, index, loc); /* diagnoses cleanly */ NativeLoc dst = loc_reg(addr->base_type, NATIVE_REG_INT, r); e->target->load_addr(e->target, dst, *addr); memset(addr, 0, sizeof *addr);