kit

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

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:
Msrc/arch/arm32/isa.h | 11++++++++++-
Msrc/arch/arm32/native.c | 53+++++++++++++++++++++++++++++++++++++++++++++--------
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;