commit c404424a4ab14b13cca7869d5c7b7c22a73c0c00
parent 09c0cfd167b6640c0da40184f4a2576da51b6ee5
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 21:49:50 -0700
arm32 -O1: give the optimizer native-emit a 2nd scratch register
The -O1 pass_native_emit ran out of scratch ('no scratch register for native
emission') whenever it needed two at once (e.g. storing a spilled value through a
spilled-pointer address) — arm32 declared only IP. Fix:
- Add an optional NativeAllocClassInfo.opt_scratch list (optimizer-only native
emit scratch); when unset the optimizer reuses .scratch (no change for other
backends). arm32 exposes {IP, LR}: LR is already prologue-saved, so it is a
free transient scratch, but it must stay OUT of the -O0 NDT scratch (the NDT
homes operands there, and arm_emit_global_addr/copy_bytes use LR directly).
- Make SREM/UREM not depend on a hard LR temp: use rd as the quotient temp when
rd aliases neither operand (the common case, no extra reg); otherwise pick a
reserved scratch — sound because when rd aliases an operand, rd is an allocated
register (never IP/LR), so at most one of {IP,LR} is an operand and the other
is free. This lets LR safely double as an optimizer scratch.
Fixes 07_break_continue, 63_memory_flags, 131_memcpy_uses_bulk,
121_dynamic_memory_builtin at -O1. arm32 corpus 285 pass (was 281); host aa64
toy 1392/0 (opt_scratch is a no-op for other backends); rem correct O0+O1.
(Pre-existing, unrelated: test-opt-whole-program-inline's metric check —
inliner subsystem, not touched here.)
Diffstat:
3 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c
@@ -182,6 +182,12 @@ static void arm_patch_t32(Arm32NativeTarget* a, u32 pos, u32 instr) {
* callee-save spills. */
static const Reg arm_int_allocable[] = {0u, 1u, 2u, 3u};
static const Reg arm_int_scratch[] = {ARM_SCRATCH};
+/* The -O1 optimizer's native-emit pass needs two scratch registers (e.g. to
+ * store a spilled value through a spilled-pointer address). The -O0 NDT must NOT
+ * take LR as an operand scratch (LR is the backend's dedicated div/staging temp,
+ * and arm_emit_global_addr/copy_bytes use it), so LR is exposed only here, on the
+ * optimizer path, where operands are allocated registers (never LR). */
+static const Reg arm_int_opt_scratch[] = {ARM_SCRATCH, ARM_TMP};
static const NativePhysRegInfo arm_int_phys[] = {
ARM_PHYS_ARG(0u, 0u), ARM_PHYS_ARG(1u, 1u),
@@ -242,6 +248,8 @@ static const NativeAllocClassInfo arm_classes[] = {
.nallocable = sizeof arm_int_allocable / sizeof arm_int_allocable[0],
.scratch = arm_int_scratch,
.nscratch = sizeof arm_int_scratch / sizeof arm_int_scratch[0],
+ .opt_scratch = arm_int_opt_scratch,
+ .opt_nscratch = sizeof arm_int_opt_scratch / sizeof arm_int_opt_scratch[0],
.phys = arm_int_phys,
.nphys = sizeof arm_int_phys / sizeof arm_int_phys[0],
/* r0..r3 + r12 (lr/ip caller-saved; lr reserved) */
@@ -663,13 +671,20 @@ static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc,
arm_emit_t32(mc, arm_udiv(rd, ra, rb));
return;
case BO_SREM:
- arm_emit_t32(mc, arm_sdiv(ARM_TMP, ra, rb));
- arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra)); /* rd = ra - (ra/rb)*rb */
- return;
- case BO_UREM:
- arm_emit_t32(mc, arm_udiv(ARM_TMP, ra, rb));
- arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra));
+ case BO_UREM: {
+ /* rd = ra - (ra/rb)*rb via SDIV/UDIV into a quotient temp, then MLS. The
+ * temp must survive ra/rb (read by MLS), so it must differ from ra and rb;
+ * it may be rd when rd aliases neither operand (the common case — no extra
+ * register). Otherwise pick a reserved scratch (LR, else IP): when rd does
+ * alias an operand, rd is an allocated register (≠ IP/LR), so at most one of
+ * {IP,LR} can be an operand and the other is always free. */
+ u32 q = (rd != ra && rd != rb)
+ ? rd
+ : ((ra != ARM_TMP && rb != ARM_TMP) ? ARM_TMP : ARM_SCRATCH);
+ arm_emit_t32(mc, op == BO_SREM ? arm_sdiv(q, ra, rb) : arm_udiv(q, ra, rb));
+ arm_emit_t32(mc, arm_mls(rd, q, rb, ra));
return;
+ }
case BO_AND:
if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
arm_emit_t32(mc, arm_dp_imm(0u, 0u, rd, ra, enc));
diff --git a/src/arch/native_target.h b/src/arch/native_target.h
@@ -153,6 +153,14 @@ typedef struct NativeAllocClassInfo {
const Reg* scratch;
u32 nscratch;
+ /* Optional native-emit scratch for the -O1 optimizer (pass_native_emit) when
+ * it needs a different set than the -O0 NDT `scratch`. opt_nscratch == 0 means
+ * the optimizer reuses `scratch`. Register-starved backends (arm32) expose a
+ * second reserved register here (one the NDT must not take as an operand
+ * scratch, e.g. a dedicated backend div/staging temp). */
+ const Reg* opt_scratch;
+ u32 opt_nscratch;
+
const NativePhysRegInfo* phys;
u32 nphys;
diff --git a/src/opt/pass_machinize.c b/src/opt/pass_machinize.c
@@ -156,8 +156,12 @@ static void collect_class(Func* f, NativeTarget* target,
f->opt_hard_reg_count[cls] < OPT_MAX_HARD_REGS)
f->opt_hard_regs[cls][f->opt_hard_reg_count[cls]++] = src->reg;
}
- for (u32 i = 0; i < ci->nscratch && i < OPT_MAX_SCRATCH_REGS; ++i)
- f->opt_scratch_regs[cls][f->opt_scratch_reg_count[cls]++] = ci->scratch[i];
+ /* The optimizer's native-emit scratch: opt_scratch overrides scratch when a
+ * backend needs a different (usually larger) set at -O1 than the -O0 NDT. */
+ const Reg* scr = ci->opt_nscratch ? ci->opt_scratch : ci->scratch;
+ u32 nscr = ci->opt_nscratch ? ci->opt_nscratch : ci->nscratch;
+ for (u32 i = 0; i < nscr && i < OPT_MAX_SCRATCH_REGS; ++i)
+ f->opt_scratch_regs[cls][f->opt_scratch_reg_count[cls]++] = scr[i];
}
static void machinize_collect_regs(Func* f, NativeTarget* target) {