kit

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

commit 45ba72817e4b4d3d2e4ee34765653935eb0ef6ba
parent 0af956d9bd290eca6860723a0590e3e6296bd34e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 10:54:24 -0700

arm32: SSAT/USAT/QADD/QSUB/QDADD/QDSUB encoders + asm/disasm support

Diffstat:
Msrc/arch/arm32/asm.c | 25+++++++++++++++++++++++++
Msrc/arch/arm32/disasm.c | 20++++++++++++++++++++
Msrc/arch/arm32/isa.c | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/arm32/isa.h | 28++++++++++++++++++++++++++++
Mtest/arch/arm32_decode_test.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/arch/arm32_inline_test.c | 13+++++++++++++
6 files changed, 202 insertions(+), 0 deletions(-)

diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c @@ -571,6 +571,31 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) { emit_t32(d, arm_bfc(rd, lsb, width)); return; } + case ARM_FMT_SAT: { + /* SSAT/USAT rd, #sat, rm (no-shift first cut; mnemonic picks signed vs + * unsigned). */ + u32 rd = parse_reg(d), sat, rm; + expect_comma(d); + sat = (u32)parse_imm(d); + expect_comma(d); + rm = parse_reg(d); + emit_t32(d, slice_eq_cstr(mn, "usat") ? arm_usat(rd, sat, rm) + : arm_ssat(rd, sat, rm)); + return; + } + case ARM_FMT_QADD: { + /* QADD/QSUB/QDADD/QDSUB rd, rm, rn (ARM operand order). */ + u32 rd = parse_reg(d), rm, rn; + expect_comma(d); + rm = parse_reg(d); + expect_comma(d); + rn = parse_reg(d); + emit_t32(d, slice_eq_cstr(mn, "qsub") ? arm_qsub(rd, rm, rn) + : slice_eq_cstr(mn, "qdadd") ? arm_qdadd(rd, rm, rn) + : slice_eq_cstr(mn, "qdsub") ? arm_qdsub(rd, rm, rn) + : arm_qadd(rd, rm, rn)); + return; + } case ARM_FMT_LDST_T3: { u32 rt = parse_reg(d), base; i64 disp; diff --git a/src/arch/arm32/disasm.c b/src/arch/arm32/disasm.c @@ -198,6 +198,26 @@ static void arm32_decode_operands(const Arm32InsnDesc* d, u32 w, u64 pc, arm_decop_pcrel(&out->operands[0], pc, 0); break; } + case ARM_FMT_SAT: { + /* SSAT/USAT rd, #sat, rm: USAT (word[23]=1) encodes sat directly, SSAT + * encodes sat-1. Rm is in hw1[3:0]. */ + u32 sat_imm = w & 0x1fu; + u32 is_usat = (w >> 23) & 1u; + out->noperands = 3; + arm_decop_reg(&out->operands[0], (w >> 8) & 0xfu); + arm_decop_imm(&out->operands[1], + (i64)(is_usat ? sat_imm : sat_imm + 1u)); + arm_decop_reg(&out->operands[2], (w >> 16) & 0xfu); + break; + } + case ARM_FMT_QADD: { + /* Q{D}ADD/Q{D}SUB rd, rm, rn: Rd=hw2[11:8], Rm=hw2[3:0], Rn=hw1[3:0]. */ + out->noperands = 3; + arm_decop_reg(&out->operands[0], (w >> 8) & 0xfu); + arm_decop_reg(&out->operands[1], w & 0xfu); + arm_decop_reg(&out->operands[2], (w >> 16) & 0xfu); + break; + } default: break; } diff --git a/src/arch/arm32/isa.c b/src/arch/arm32/isa.c @@ -183,6 +183,25 @@ const Arm32InsnDesc arm32_insn_table[] = { {MN("ubfx"), (0xf3c00000u), 0xfff08020u, ARM_FMT_BFX, 0, {0}}, /* ================================================================= + * ARMv7E-M DSP saturating. SSAT/USAT share the 0xF3xx plain-binary-imm + * group with the bitfield/movw rows above; the mask pins the LSL/no-shift + * forms (word[21]=0, since the ASR and SSAT16/USAT16 forms have word[21]=1) + * plus hw2[15]=0 and hw2[5:4]=00, so SSAT16/USAT16/SBFX/UBFX/BFI/MOVW never + * collide. The ASR-shifted SSAT/USAT (word[21]=1) is intentionally not + * decoded — it falls through to .inst rather than risk misdecoding the + * encoding-overlapping SSAT16. QADD/QSUB/QDADD/QDSUB pin hw1[15:4]=0xFA8, + * hw2[15:12]=1111, hw2[7:4]=op (collision-free vs CLZ 0xFAB0 / REV/RBIT + * 0xFA9x). Verified by round-tripping each match word through llvm-mc + * (`--triple=thumbv7em-none-eabi --mattr=+dsp -disassemble`). + * ================================================================= */ + {MN("ssat"), (0xf3000000u), 0xffe08030u, ARM_FMT_SAT, 0, {0}}, + {MN("usat"), (0xf3800000u), 0xffe08030u, ARM_FMT_SAT, 0, {0}}, + {MN("qadd"), (0xfa80f080u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}}, + {MN("qdadd"), (0xfa80f090u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}}, + {MN("qsub"), (0xfa80f0a0u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}}, + {MN("qdsub"), (0xfa80f0b0u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}}, + + /* ================================================================= * Loads / stores — T3 (positive imm12). hw1 selects op+width: * LDR 0xF8D0 STR 0xF8C0 LDRB 0xF890 STRB 0xF880 * LDRH 0xF8B0 STRH 0xF8A0 LDRSB 0xF990 LDRSH 0xF9B0. @@ -701,6 +720,37 @@ static void print_bfc(StrBuf* sb, u32 w) { p_imm(sb, (i64)(msb - lsb + 1u)); } +/* SSAT/USAT rd, #sat, rm. The U bit (word[23] = hw1[7]) selects USAT, which + * encodes the saturation position directly; SSAT encodes sat-1. Rm sits in + * hw1[3:0] (f_rn). The (LSL) shift amount lives in imm3:imm2 (hw2[14:12]:[7:6]); + * it is appended only when nonzero. */ +static void print_sat(StrBuf* sb, u32 w) { + u32 is_usat = (f_hw1(w) >> 7) & 1u; + u32 sat_imm = f_hw2(w) & 0x1fu; + u32 sat = is_usat ? sat_imm : (sat_imm + 1u); + u32 imm3 = (f_hw2(w) >> 12) & 7u, imm2 = (f_hw2(w) >> 6) & 3u; + u32 sh = (imm3 << 2) | imm2; + p_reg(sb, f_rd(w)); + p_sep(sb); + p_imm(sb, (i64)sat); + p_sep(sb); + p_reg(sb, f_rn(w)); /* Rm */ + if (sh) { + strbuf_puts(sb, ", lsl "); + p_imm(sb, (i64)sh); + } +} + +/* QADD/QSUB/QDADD/QDSUB rd, rm, rn. Rd = hw2[11:8], Rm = hw2[3:0], Rn lands in + * hw1[3:0] (the ARM operand order is Rd, Rm, Rn). */ +static void print_qadd(StrBuf* sb, u32 w) { + p_reg(sb, f_rd(w)); + p_sep(sb); + p_reg(sb, f_rm(w)); + p_sep(sb); + p_reg(sb, f_rn(w)); +} + static void print_ldst_t3(StrBuf* sb, u32 w) { u32 rt = (f_hw2(w) >> 12) & 0xfu; u32 imm12 = f_hw2(w) & 0xfffu; @@ -1067,6 +1117,12 @@ void arm32_print_operands(StrBuf* sb, const Arm32InsnDesc* desc, u32 word, case ARM_FMT_BFC: print_bfc(sb, word); break; + case ARM_FMT_SAT: + print_sat(sb, word); + break; + case ARM_FMT_QADD: + print_qadd(sb, word); + break; case ARM_FMT_LDST_T3: print_ldst_t3(sb, word); break; diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -350,6 +350,32 @@ static inline u32 arm_smull(u32 rdlo, u32 rdhi, u32 rn, u32 rm) { return arm_t32(0xfb80u | rn, (rdlo << 12) | (rdhi << 8) | rm); } +/* --------- ARMv7E-M DSP saturating (T1) --------- + * SSAT/USAT saturate Rm to a signed/unsigned `sat`-bit value (no-shift form): + * SSAT Rd, #sat, Rm : hw1 = 0xF300|Rn(=Rm), hw2 = (Rd<<8) | ((sat-1)&0x1f) + * USAT Rd, #sat, Rm : hw1 = 0xF380|Rm, hw2 = (Rd<<8) | (sat&0x1f) + * (SSAT encodes sat_imm = sat-1; USAT encodes sat directly. The LSL/ASR shift + * operand is omitted in this cut — see isa.c for the decode-mask rationale.) + * + * QADD/QSUB/QDADD/QDSUB are register saturating add/sub (ARM operand order + * Rd, Rm, Rn): hw1 = 0xFA80|Rn, hw2 = 0xF000 | (Rd<<8) | (op<<4) | Rm, with + * op QADD=0x8, QDADD=0x9, QSUB=0xA, QDSUB=0xB. Note Rn lands in hw1[3:0] and + * Rm in hw2[3:0]. */ +static inline u32 arm_ssat(u32 rd, u32 sat, u32 rm) { + return arm_t32(0xf300u | (rm & 0xfu), (rd << 8) | ((sat - 1u) & 0x1fu)); +} +static inline u32 arm_usat(u32 rd, u32 sat, u32 rm) { + return arm_t32(0xf380u | (rm & 0xfu), (rd << 8) | (sat & 0x1fu)); +} +static inline u32 arm_qop(u32 op, u32 rd, u32 rm, u32 rn) { + return arm_t32(0xfa80u | (rn & 0xfu), + 0xf000u | (rd << 8) | ((op & 0xfu) << 4) | (rm & 0xfu)); +} +static inline u32 arm_qadd(u32 rd, u32 rm, u32 rn) { return arm_qop(0x8u, rd, rm, rn); } +static inline u32 arm_qdadd(u32 rd, u32 rm, u32 rn) { return arm_qop(0x9u, rd, rm, rn); } +static inline u32 arm_qsub(u32 rd, u32 rm, u32 rn) { return arm_qop(0xau, rd, rm, rn); } +static inline u32 arm_qdsub(u32 rd, u32 rm, u32 rn) { return arm_qop(0xbu, rd, rm, rn); } + static inline u32 arm_dmb(u32 option) { return arm_t32(0xf3bfu, 0x8f50u | (option & 0xfu)); } static inline u32 arm_dsb(u32 option) { return arm_t32(0xf3bfu, 0x8f40u | (option & 0xfu)); } static inline u32 arm_isb(u32 option) { return arm_t32(0xf3bfu, 0x8f60u | (option & 0xfu)); } @@ -412,6 +438,8 @@ typedef enum Arm32Format { ARM_FMT_BFX, /* SBFX/UBFX rd, rn, #lsb, #width */ ARM_FMT_BFI, /* BFI rd, rn, #lsb, #width */ ARM_FMT_BFC, /* BFC rd, #lsb, #width */ + ARM_FMT_SAT, /* SSAT/USAT rd, #sat, rm */ + ARM_FMT_QADD, /* QADD/QSUB/QDADD/QDSUB rd, rm, rn */ ARM_FMT_LDST_T3, /* LDR/STR{B,H,SB,SH} rt, [rn, #imm12] */ ARM_FMT_LDST_T4, /* LDR/STR{B,H,SB,SH} rt, [rn, #+/-imm8] */ ARM_FMT_LDREX, /* LDREX rt, [rn{, #imm}] */ diff --git a/test/arch/arm32_decode_test.c b/test/arch/arm32_decode_test.c @@ -287,6 +287,65 @@ static void decode_barriers(KitCompiler* pub) { expect_text(pub, b, 4, 0, 4, "isb", "sy", "isb sy"); } +/* ARMv7E-M DSP saturating ops: SSAT/USAT (Rd, #sat, Rm) + the QADD family + * (Rd, Rm, Rn — note Rn lands in hw1[3:0], Rm in hw2[3:0]). Golden words pinned + * to llvm-mc (`--triple=thumbv7em-none-eabi --mattr=+dsp -show-encoding`). */ +static void decode_dsp_sat(KitCompiler* pub) { + unsigned char b[4]; + + /* Direct encoder -> word equality, pinned to the oracle bytes. */ + EXPECT(arm_ssat(ARM_R0, 16, ARM_R1) == 0xf301000fu, "ssat encode = %#x", + arm_ssat(ARM_R0, 16, ARM_R1)); + /* llvm-mc bytes [0x06,0xf3,0x1f,0x05] -> hw1=0xf306, hw2=0x051f. (The task + * spec quoted 0xf3061f05 with hw2's bytes transposed; the real word is + * 0xf306051f, hw2 = (rd<<8)|(sat-1) = (5<<8)|31.) */ + EXPECT(arm_ssat(ARM_R5, 32, ARM_R6) == 0xf306051fu, "ssat32 encode = %#x", + arm_ssat(ARM_R5, 32, ARM_R6)); + EXPECT(arm_usat(ARM_R0, 15, ARM_R1) == 0xf381000fu, "usat encode = %#x", + arm_usat(ARM_R0, 15, ARM_R1)); + EXPECT(arm_qadd(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f381u, "qadd encode = %#x", + arm_qadd(ARM_R3, ARM_R1, ARM_R2)); + EXPECT(arm_qadd(ARM_R0, ARM_R2, ARM_R1) == 0xfa81f082u, "qadd2 encode = %#x", + arm_qadd(ARM_R0, ARM_R2, ARM_R1)); + EXPECT(arm_qsub(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f3a1u, "qsub encode = %#x", + arm_qsub(ARM_R3, ARM_R1, ARM_R2)); + EXPECT(arm_qdadd(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f391u, "qdadd encode = %#x", + arm_qdadd(ARM_R3, ARM_R1, ARM_R2)); + EXPECT(arm_qdsub(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f3b1u, "qdsub encode = %#x", + arm_qdsub(ARM_R3, ARM_R1, ARM_R2)); + + /* Disassembly round-trips. */ + put_t32(b, 0, arm_ssat(ARM_R0, 16, ARM_R1)); + expect_text(pub, b, 4, 0, 4, "ssat", "r0, #16, r1", "ssat"); + put_t32(b, 0, arm_usat(ARM_R0, 15, ARM_R1)); + expect_text(pub, b, 4, 0, 4, "usat", "r0, #15, r1", "usat"); + put_t32(b, 0, arm_qadd(ARM_R3, ARM_R1, ARM_R2)); + expect_text(pub, b, 4, 0, 4, "qadd", "r3, r1, r2", "qadd"); + put_t32(b, 0, arm_qsub(ARM_R3, ARM_R1, ARM_R2)); + expect_text(pub, b, 4, 0, 4, "qsub", "r3, r1, r2", "qsub"); + put_t32(b, 0, arm_qdadd(ARM_R3, ARM_R1, ARM_R2)); + expect_text(pub, b, 4, 0, 4, "qdadd", "r3, r1, r2", "qdadd"); + put_t32(b, 0, arm_qdsub(ARM_R3, ARM_R1, ARM_R2)); + expect_text(pub, b, 4, 0, 4, "qdsub", "r3, r1, r2", "qdsub"); + + /* Structured-operand check: SSAT decodes to (reg, imm=16, reg). */ + { + Compiler* c = (Compiler*)pub; + KitDecodedInsn insn; + put_t32(b, 0, arm_ssat(ARM_R0, 16, ARM_R1)); + memset(&insn, 0, sizeof insn); + EXPECT(arch_decode_one(c, b, 4, 0, &insn) == KIT_OK, "ssat decode"); + EXPECT(insn.noperands == 3, "ssat noperands = %u", + (unsigned)insn.noperands); + EXPECT(insn.operands[0].kind == KIT_DECOP_REG && insn.operands[0].reg == 0, + "ssat rd"); + EXPECT(insn.operands[1].kind == KIT_DECOP_IMM && insn.operands[1].imm == 16, + "ssat sat = %lld", (long long)insn.operands[1].imm); + EXPECT(insn.operands[2].kind == KIT_DECOP_REG && insn.operands[2].reg == 1, + "ssat rm"); + } +} + static void decode_ldrex_strex(KitCompiler* pub) { unsigned char b[4]; put_t32(b, 0, arm_ldrex(ARM_R0, ARM_R1, 0)); @@ -382,6 +441,7 @@ int main(void) { decode_misc16(c); decode_barriers(c); decode_ldrex_strex(c); + decode_dsp_sat(c); decode_it_block(c); decode_block_terminates(c); decode_unknown_fallback(c); diff --git a/test/arch/arm32_inline_test.c b/test/arch/arm32_inline_test.c @@ -12,6 +12,9 @@ static void arm32_body(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { it_inline_asm(c, cg, "add.w r0, r0, #7", NULL, 0, NULL, 0, NULL, 0); it_inline_asm(c, cg, "movw r1, #0x1234", NULL, 0, NULL, 0, NULL, 0); it_inline_asm(c, cg, "bkpt #0xab", NULL, 0, NULL, 0, NULL, 0); + /* ARMv7E-M DSP saturating (no dsp-feature gate; assembler is permissive). */ + it_inline_asm(c, cg, "ssat r0, #16, r1", NULL, 0, NULL, 0, NULL, 0); + it_inline_asm(c, cg, "qadd r3, r1, r2", NULL, 0, NULL, 0, NULL, 0); } static void arm32_bad_operand(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { @@ -156,6 +159,8 @@ int main(void) { static const uint8_t add_w_r0_7[] = {0x00u, 0xf1u, 0x07u, 0x00u}; static const uint8_t movw_r1_1234[] = {0x41u, 0xf2u, 0x34u, 0x21u}; static const uint8_t bkpt_ab[] = {0xabu, 0xbeu}; + static const uint8_t ssat_r0_16_r1[] = {0x01u, 0xf3u, 0x0fu, 0x00u}; + static const uint8_t qadd_r3_r1_r2[] = {0x82u, 0xfau, 0x81u, 0xf3u}; InlineTestEnv env; InlineText text; @@ -176,6 +181,14 @@ int main(void) { "missing movw r1, #0x1234 encoding"); IT_EXPECT(&env, it_contains(text.data, text.len, bkpt_ab, sizeof bkpt_ab), "missing bkpt #0xab encoding"); + IT_EXPECT(&env, + it_contains(text.data, text.len, ssat_r0_16_r1, + sizeof ssat_r0_16_r1), + "missing ssat r0, #16, r1 encoding"); + IT_EXPECT(&env, + it_contains(text.data, text.len, qadd_r3_r1_r2, + sizeof qadd_r3_r1_r2), + "missing qadd r3, r1, r2 encoding"); } it_text_close(&text);