commit 0abbed1b027074bf9c596adb91d0fab896d8a1b1
parent 21d150509f42a4aba98cb8211e89220be143b242
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 22:11:22 -0700
arm32: intrinsics — IMM args + bswap high-register encoding
Two intrinsic correctness bugs:
- bswap used the 16-bit REV/REV16 (T1) encodings, which only address r0..r7.
The bswap dst is the IP (r12) codegen scratch at -O0 (and may be r8..r11 at
-O1), so the register silently truncated mod 8 (rev r4 instead of rev ip):
the result landed in the wrong register and the caller read garbage. Add the
32-bit REV.W/REV16.W (T2) encoders (arm_rev_w/arm_rev16_w) and use them.
- nd_intrinsic forwards an immediate operand as NATIVE_LOC_IMM, not a register;
clz/ctz/bswap/popcount/expect/*_overflow read it via loc_reg() and got the
immediate misread as a register number. Materialize an IMM arg into an output
register first (arm_intrin_arg, the riscv pattern). @expect also now moves both
lanes of an 8-byte value (a single move dropped the high lane).
Fixes 87_expect, 88_scalar_intrinsic, 132_intrinsic_bit_and_overflow (O0+O1).
Diffstat:
2 files changed, 55 insertions(+), 9 deletions(-)
diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h
@@ -320,10 +320,19 @@ static inline u32 arm_clz(u32 rd, u32 rm) {
static inline u32 arm_rbit(u32 rd, u32 rm) {
return arm_t32(0xfa90u | (rm & 0xfu), 0xf0a0u | (rd << 8) | (rm & 0xfu));
}
-/* 16-bit byte-reverse (T1): REV / REV16 / REVSH. */
+/* 16-bit byte-reverse (T1): REV / REV16 / REVSH. r0..r7 ONLY (3-bit fields) —
+ * a caller with a high register (e.g. the IP/r12 codegen scratch) must use the
+ * 32-bit .W forms below, else the register silently truncates mod 8. */
static inline u16 arm_rev(u32 rd, u32 rm) { return (u16)(0xba00u | ((rm & 7u) << 3) | (rd & 7u)); }
static inline u16 arm_rev16(u32 rd, u32 rm) { return (u16)(0xba40u | ((rm & 7u) << 3) | (rd & 7u)); }
static inline u16 arm_revsh(u32 rd, u32 rm) { return (u16)(0xbac0u | ((rm & 7u) << 3) | (rd & 7u)); }
+/* 32-bit byte-reverse (T2): REV.W / REV16.W — any register r0..r14. */
+static inline u32 arm_rev_w(u32 rd, u32 rm) {
+ return arm_t32(0xfa90u | (rm & 0xfu), 0xf080u | ((rd & 0xfu) << 8) | (rm & 0xfu));
+}
+static inline u32 arm_rev16_w(u32 rd, u32 rm) {
+ return arm_t32(0xfa90u | (rm & 0xfu), 0xf090u | ((rd & 0xfu) << 8) | (rm & 0xfu));
+}
/* --------- barriers (32-bit T1, hw2 = 0x8f00 | op<<4 | option) --------- */
/* option = 0xf for the "sy" full-system barrier (the only form kit emits). */
diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c
@@ -2469,6 +2469,19 @@ static void arm_emit_setcc(MCEmitter* mc, u32 rd, u32 cc) {
* backend only sees the 32-bit forms. lr/ip are the transient scratch (lr is
* reserved; ip is the materialization scratch, free here since the operands are
* already in their NDT registers). */
+/* nd_intrinsic forwards an immediate argument as NATIVE_LOC_IMM (not in a
+ * register). A value-consuming intrinsic needs it in a register: materialize it
+ * into `into` (an output register, free to reuse as an input) and return the reg
+ * loc; pass a register arg through unchanged. Mirrors riscv's intrinsic path. */
+static NativeLoc arm_intrin_arg(Arm32NativeTarget* a, NativeLoc arg, u32 into) {
+ NativeLoc r;
+ if (arg.kind != NATIVE_LOC_IMM) return arg;
+ r = native_loc_reg(arg.type ? arg.type : builtin_id(KIT_CG_BUILTIN_I32),
+ NATIVE_REG_INT, into);
+ arm_load_imm(&a->base, r, arg.v.imm);
+ return r;
+}
+
static void arm_overflow(Arm32NativeTarget* a, IntrinKind kind,
const NativeLoc* dsts, const NativeLoc* args) {
MCEmitter* mc = a->base.mc;
@@ -2537,7 +2550,8 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
return;
case INTRIN_CLZ:
if (ndst == 1u && narg == 1u) {
- arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(args[0])));
+ NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
+ arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(a0)));
return;
}
break;
@@ -2545,7 +2559,8 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
/* RBIT reverses the bit order, so the trailing-zero count becomes a
* leading-zero count of the reversed word. */
if (ndst == 1u && narg == 1u) {
- arm_emit_t32(mc, arm_rbit(loc_reg(dsts[0]), loc_reg(args[0])));
+ NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
+ arm_emit_t32(mc, arm_rbit(loc_reg(dsts[0]), loc_reg(a0)));
arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(dsts[0])));
return;
}
@@ -2553,14 +2568,18 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
case INTRIN_BSWAP:
if (ndst == 1u && narg == 1u) {
u32 width = native_type_size(t, dsts[0].type);
+ NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
+ /* Use the 32-bit REV.W/REV16.W forms: the dst may be a high register
+ * (the IP codegen scratch at -O0, r8..r11 at -O1) that the 16-bit T1
+ * encoding cannot represent. */
if (width == 4u) {
- arm_emit_t16(mc, arm_rev(loc_reg(dsts[0]), loc_reg(args[0])));
+ arm_emit_t32(mc, arm_rev_w(loc_reg(dsts[0]), loc_reg(a0)));
return;
}
if (width == 2u) {
/* REV16 byte-swaps within each halfword: the low halfword of the
* input becomes the swapped 16-bit result in the low halfword. */
- arm_emit_t16(mc, arm_rev16(loc_reg(dsts[0]), loc_reg(args[0])));
+ arm_emit_t32(mc, arm_rev16_w(loc_reg(dsts[0]), loc_reg(a0)));
return;
}
}
@@ -2570,7 +2589,8 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
* ThumbExpandImm-encodable, so no MOVW/MOVT is needed. lr/ip are the
* scratch. 64-bit popcount stays in the cg lane path / a follow-on. */
if (ndst == 1u && narg == 1u && native_type_size(t, dsts[0].type) <= 4u) {
- u32 rs = loc_reg(args[0]), rd = loc_reg(dsts[0]);
+ u32 rd = loc_reg(dsts[0]);
+ u32 rs = loc_reg(arm_intrin_arg(a, args[0], rd));
u32 e55, e33, e0f, e01;
thumb_expand_imm_encode(0x55555555u, &e55);
thumb_expand_imm_encode(0x33333333u, &e33);
@@ -2594,9 +2614,21 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
break;
case INTRIN_EXPECT:
case INTRIN_ASSUME_ALIGNED:
- /* Branch/alignment hints: the result is the first argument, unchanged. */
+ /* Branch/alignment hints: the result is the first argument, unchanged. An
+ * immediate value materializes straight into dst; an 8-byte value carries
+ * two lanes, so move each (a single arm_move would drop the high lane). */
if (ndst == 1u && narg >= 1u) {
- arm_move(t, dsts[0], args[0]);
+ if (args[0].kind == NATIVE_LOC_IMM) {
+ arm_load_imm(t, dsts[0], args[0].v.imm);
+ } else if (native_type_size(t, dsts[0].type) > 4u &&
+ dsts[0].kind == NATIVE_LOC_REG &&
+ args[0].kind == NATIVE_LOC_REG) {
+ u32 dlo = loc_reg(dsts[0]), slo = loc_reg(args[0]);
+ if (dlo != slo) arm_emit_t16(mc, arm_mov_hi(dlo, slo));
+ arm_emit_t16(mc, arm_mov_hi((dlo + 1u) & 0xfu, (slo + 1u) & 0xfu));
+ } else {
+ arm_move(t, dsts[0], args[0]);
+ }
return;
}
break;
@@ -2633,7 +2665,12 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
case INTRIN_SMUL_OVERFLOW:
case INTRIN_UMUL_OVERFLOW:
if (ndst == 2u && narg == 2u) {
- arm_overflow(a, kind, dsts, args);
+ /* Materialize immediate operands into the output registers (free to use
+ * as inputs: arm_overflow reads both operands before writing dsts). */
+ NativeLoc av[2];
+ av[0] = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
+ av[1] = arm_intrin_arg(a, args[1], loc_reg(dsts[1]));
+ arm_overflow(a, kind, dsts, av);
return;
}
break;