kit

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

commit cc7a68d9dd2cd09a8fbf02e115f49030fc99d5e3
parent 34b5294b427e90ed1a00dda03759deab3f203ddf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 13:56:34 -0700

arm32/asm: assemble general add/sub rd, rn, {#imm | rm}

The standalone/inline-asm assembler had no row for the 3-operand non-flag
add/sub: `add` resolved to the 2-operand hi-register form (plus the sp-adjust
rows) and `sub` to the sp-adjust row, so a third operand was silently dropped or
misread. `add r4, r4, #1` assembled as `add r4, r4` (= r4*2) and `sub r4, r4,
#1` as `sub sp, sp, #0` — corrupting any inline asm that used them (e.g. the
`+r` inout `add %0, %0, #1`).

Add a shared arm_asm_addsub_third encoder, reached from both the HIREG_16 (add)
and ADJSP_16 (sub) handlers once rd/rn are parsed. It picks the densest legal
Thumb-2 form: a register third operand → 32-bit add.w/sub.w; an immediate →
16-bit sp-adjust / add-from-sp, else 32-bit modified-immediate add.w/sub.w, else
12-bit addw/subw; a negative immediate flips the operation. The 2-operand hi-
register `add rdn, rm` and the `sub rd, #imm` shorthand are preserved.
Encodings byte-match llvm-mc (thumbv7m) across the immediate, 3-register, sp,
modimm, and imm12 forms.

Diffstat:
Msrc/arch/arm32/asm.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 58 insertions(+), 18 deletions(-)

diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c @@ -348,6 +348,42 @@ static const Arm32InsnDesc* resolve_mnemonic(AsmDriver* d, Slice mn, asm_driver_panic(d, "arm32 asm: unsupported instruction"); } +/* Encode the third operand of a general `add`/`sub rd, rn, <#imm | rm>` once rd + * and rn are parsed and the separating comma consumed. There is no 16-bit + * non-flag add/sub-immediate or 3-register form, so a register third operand + * takes the 32-bit add.w/sub.w, and an immediate takes the densest of: the + * 16-bit sp-adjust / add-from-sp forms, the 32-bit modified-immediate + * (add.w/sub.w), then the 12-bit addw/subw. A negative immediate flips the + * operation. The 16-bit hi-register `add rdn, rm` (2 operands) is handled at the + * call site, not here. */ +static void arm_asm_addsub_third(AsmDriver* d, int is_sub, u32 rd, u32 rn) { + i64 simm; + u32 mag, out12; + if (peek_is_reg(d)) { + u32 rm = parse_reg(d); + emit_t32(d, is_sub ? arm_sub_reg(rd, rn, rm) : arm_add_reg(rd, rn, rm)); + return; + } + simm = parse_imm(d); + if (simm < 0) { + is_sub = !is_sub; + simm = -simm; + } + mag = (u32)simm; + if (rd == 13u && rn == 13u && (mag & 3u) == 0u && (mag >> 2) <= 0x7fu) { + emit_t16(d, (u16)((is_sub ? 0xb080u : 0xb000u) | ((mag >> 2) & 0x7fu))); + } else if (!is_sub && rn == 13u && rd <= 7u && (mag & 3u) == 0u && + (mag >> 2) <= 0xffu) { + emit_t16(d, (u16)(0xa800u | ((rd & 7u) << 8) | ((mag >> 2) & 0xffu))); + } else if (thumb_expand_imm_encode(mag, &out12)) { + emit_t32(d, arm_dp_imm(is_sub ? 13u : 8u, 0u, rd, rn, out12)); + } else if (mag <= 0xfffu) { + emit_t32(d, is_sub ? arm_sub_imm12(rd, rn, mag) : arm_add_imm12(rd, rn, mag)); + } else { + asm_driver_panic(d, "arm32 asm: add/sub immediate not encodable"); + } +} + /* Encode + emit one instruction for the matched descriptor. */ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) { Slice mn = desc->mnemonic; @@ -881,18 +917,14 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) { return; } rm = parse_reg(d); - /* `add rd, sp, #imm` / `add sp, sp, #imm` (a third immediate operand - * through SP) use the SP-relative encodings: ADD rd,sp,#imm8*4 (T1, - * 0xA800, rd<=r7) or ADD sp,sp,#imm7*4 (T2, 0xB000). The hi-register form - * below has no immediate and would drop it. */ - if (slice_eq_cstr(mn, "add") && rm == 13u && asm_driver_eat_comma(d)) { - u32 imm = (u32)parse_imm(d); - if (rdn == 13u) - emit_t16(d, (u16)(0xb000u | ((imm / 4u) & 0x7fu))); - else if (rdn <= 7u) - emit_t16(d, (u16)(0xa800u | ((rdn & 7u) << 8) | ((imm / 4u) & 0xffu))); - else - asm_driver_panic(d, "arm32 asm: add sp immediate needs add.w"); + /* A third operand means the general `add rd, rn, <#imm | rm>` (rd=rdn, + * rn=rm): there is no 16-bit non-flag add-immediate or 3-register form, so + * route it through the shared encoder (which also covers the sp-relative + * forms). Without a third operand this is the 2-operand hi-register + * `add rdn, rm` below; the old SP-only special case dropped every other + * immediate. */ + if (slice_eq_cstr(mn, "add") && asm_driver_eat_comma(d)) { + arm_asm_addsub_third(d, 0, rdn, rm); return; } emit_t16(d, (u16)((desc->match & 0xff00u) | (((rdn >> 3) & 1u) << 7) | @@ -958,13 +990,21 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) { return; } case ARM_FMT_ADJSP_16: { - u32 imm7; - (void)parse_reg(d); /* sp */ - expect_comma(d); - (void)parse_reg(d); /* sp */ + /* Plain `sub` (and the unreachable plain-`add` row) land here. Parse the + * real registers and route through the shared add/sub encoder, which picks + * the 16-bit sp-adjust when rd==rn==sp. Accept the 2-operand shorthand + * `sub rd, #imm` (== `sub rd, rd, #imm`); the old code assumed sp/sp and + * silently mis-encoded `sub rN, rN, #imm` as `sub sp, sp, #...`. */ + int is_sub = slice_eq_cstr(mn, "sub"); + u32 rd = parse_reg(d), rn; expect_comma(d); - imm7 = ((u32)parse_imm(d) / 4u) & 0x7fu; - emit_t16(d, (u16)((desc->match & 0xff80u) | imm7)); + if (peek_is_reg(d)) { + rn = parse_reg(d); + expect_comma(d); + } else { + rn = rd; + } + arm_asm_addsub_third(d, is_sub, rd, rn); return; } case ARM_FMT_PUSHPOP_16: {