kit

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

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:
Msrc/arch/arm32/native.c | 27+++++++++++++++++++++------
Msrc/arch/native_target.h | 8++++++++
Msrc/opt/pass_machinize.c | 8++++++--
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) {