commit 34b5294b427e90ed1a00dda03759deab3f203ddf
parent 390b95747b59e7a73202cd9a9b15a89bd985c270
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 13:56:25 -0700
arm32: stage frame-resident "r" inline-asm inputs at -O1
arm_asm_block_native panicked "optimizer asm input not in a register" when a
register-constrained ("r") input's value is not in a register but in a frame
slot — the optimizer cannot keep an address-stable / frame-resident local (e.g.
a value that is both `+r` inout and returned) live in a register across an asm
block, so it forwards the frame home, not a register. aa64 already handles this
(native.c) by loading the value into a reserved scratch before binding.
Mirror that on arm32: a plain "r" input arriving as a frame/stack/addr loc (or a
folded immediate) is staged into a reserved scratch (IP, then LR) and bound as
that register. The "m" memory path now shares the same staging counter via the
new arm_asm_in_loc_addr / arm_asm_stage_reg helpers, so two staged operands no
longer alias IP. The restricted "l" class and hard-register pins still require
an already-allocated register (IP/LR are high regs), so they keep the
diagnostic.
Diffstat:
1 file changed, 64 insertions(+), 24 deletions(-)
diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c
@@ -3468,6 +3468,44 @@ static void arm_direct_asm_block(NativeDirectTarget* d, const char* tmpl,
&hooks);
}
+/* Convert an inline-asm input location — a computed address, or a frame/stack
+ * home — to a NativeAddr. Shared by the "m" (bind the address) and the staged
+ * register (load the value) input paths. */
+static NativeAddr arm_asm_in_loc_addr(NativeTarget* t, NativeLoc loc,
+ KitCgTypeId type) {
+ NativeAddr ma;
+ memset(&ma, 0, sizeof ma);
+ if (loc.kind == NATIVE_LOC_ADDR) {
+ ma = loc.v.addr;
+ } else if (loc.kind == NATIVE_LOC_FRAME || loc.kind == NATIVE_LOC_STACK) {
+ ma.base_kind = NATIVE_ADDR_BASE_FRAME;
+ ma.base.frame =
+ loc.kind == NATIVE_LOC_FRAME ? loc.v.frame : loc.v.stack.slot;
+ ma.offset = loc.kind == NATIVE_LOC_STACK ? loc.v.stack.offset : 0;
+ ma.base_type = type;
+ } else {
+ arm_asm_panic_at(t->c, arm_of(t)->loc, "unsupported memory asm input");
+ }
+ return ma;
+}
+
+/* Reserve the next scratch register to stage an inline-asm operand into: IP (the
+ * operand scratch) first, then LR. The native-emit asm path runs with both free
+ * (the optimizer allocates asm operands to r0..r11 and never to IP/LR), so two
+ * staged operands are available — enough for the operand shapes the corpus
+ * exercises. A third staged operand is a clean diagnostic rather than a clobber. */
+static Reg arm_asm_stage_reg(NativeTarget* t, u32* nstage) {
+ Reg r;
+ if (*nstage == 0u)
+ r = (Reg)ARM_SCRATCH;
+ else if (*nstage == 1u)
+ r = (Reg)ARM_TMP;
+ else
+ arm_asm_panic_at(t->c, arm_of(t)->loc, "too many staged inline-asm operands");
+ (*nstage)++;
+ return r;
+}
+
/* NativeTarget (optimizer / -O1) inline-asm path: bind operands to registers
* (no self-allocation — the optimizer pre-allocated and forwarded clobbers via
* plan_frame), then run the template. The minimal runner needs only the bound
@@ -3481,6 +3519,7 @@ static void arm_asm_block_native(NativeTarget* t, const char* tmpl,
Operand* bound_outs = nout ? arena_zarray(c->tu, Operand, nout) : NULL;
Operand* bound_ins = nin ? arena_zarray(c->tu, Operand, nin) : NULL;
u32 i;
+ u32 nstage = 0u; /* reserved scratch (IP, then LR) consumed for staged inputs */
(void)clobbers;
(void)nclob;
for (i = 0; i < nout; ++i) {
@@ -3514,30 +3553,31 @@ static void arm_asm_block_native(NativeTarget* t, const char* tmpl,
} else if (body[0] == 'm') {
/* Memory ("m") constraint: bind a base register holding the operand's
* address. The optimizer hands a computed address (NATIVE_LOC_ADDR) or a
- * frame/stack home; either way materialize the address into the operand
- * scratch (ip) and bind it as the memory base. */
- NativeAddr ma;
- memset(&ma, 0, sizeof ma);
- if (in_locs[i].kind == NATIVE_LOC_ADDR) {
- ma = in_locs[i].v.addr;
- } else if (in_locs[i].kind == NATIVE_LOC_FRAME ||
- in_locs[i].kind == NATIVE_LOC_STACK) {
- ma.base_kind = NATIVE_ADDR_BASE_FRAME;
- ma.base.frame = in_locs[i].kind == NATIVE_LOC_FRAME
- ? in_locs[i].v.frame
- : in_locs[i].v.stack.slot;
- ma.offset = in_locs[i].kind == NATIVE_LOC_STACK
- ? in_locs[i].v.stack.offset
- : 0;
- ma.base_type = type;
- } else {
- arm_asm_panic_at(c, arm_of(t)->loc, "unsupported memory asm input");
- }
- arm_load_addr(t,
- native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32),
- NATIVE_REG_INT, ARM_SCRATCH),
- ma);
- arm_asm_bound_mem(&bound_ins[i], type, (Reg)ARM_SCRATCH);
+ * frame/stack home; either way materialize the address into a reserved
+ * staging scratch and bind it as the memory base. */
+ Reg base = arm_asm_stage_reg(t, &nstage);
+ NativeAddr ma = arm_asm_in_loc_addr(t, in_locs[i], type);
+ arm_load_addr(
+ t, native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT, base),
+ ma);
+ arm_asm_bound_mem(&bound_ins[i], type, base);
+ } else if (body[0] == 'r' && body[1] == '\0' && !ins[i].reg) {
+ /* Plain register ("r") input whose value is NOT already in a register: an
+ * address-stable / frame-resident local (the optimizer cannot keep an
+ * address-taken local live in a register across the asm block), or a
+ * folded immediate. Stage the VALUE into a reserved scratch and bind that
+ * register, mirroring the aa64 native asm path. Only the unrestricted "r"
+ * class can use IP/LR; the low-reg "l" class (and hard-register pins) must
+ * already arrive in an allowed register, so they fall through to the
+ * diagnostic below. */
+ Reg sr = arm_asm_stage_reg(t, &nstage);
+ NativeLoc sd = native_loc_reg(type, NATIVE_REG_INT, sr);
+ if (in_locs[i].kind == NATIVE_LOC_IMM)
+ arm_load_imm(t, sd, in_locs[i].v.imm);
+ else
+ arm_load(t, sd, arm_asm_in_loc_addr(t, in_locs[i], type),
+ native_mem_for_type(t, type, native_type_size(t, type)));
+ arm_asm_bound_reg(&bound_ins[i], type, NATIVE_REG_INT, sr);
} else {
arm_asm_panic_at(c, arm_of(t)->loc,
"optimizer asm input not in a register");