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