kit

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

commit 0e4e19bd089f21e3ed275349fa873ff9eae95ae7
parent 45ba72817e4b4d3d2e4ee34765653935eb0ef6ba
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 11:28:11 -0700

arm32: 16-bit encoding density + shifted-register operand folding

Diffstat:
Msrc/arch/arm32/isa.h | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/arm32/native.c | 254++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtest/arch/arm32_decode_test.c | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 419 insertions(+), 34 deletions(-)

diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -136,6 +136,26 @@ static inline u32 arm_sub_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(13u, 0 static inline u32 arm_and_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(0u, 0u, rd, rn, rm); } static inline u32 arm_orr_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(2u, 0u, rd, rn, rm); } static inline u32 arm_eor_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(4u, 0u, rd, rn, rm); } +/* SUB.W/AND.W/ORR.W/EOR.W rd, rn, rm, LSL #sh (T2, no flags) — the shifted- + * register operand fold (mirror of arm_add_reg_lsl above; same imm3:imm2 = sh, + * type=00 LSL). The optimizer's L7 pass restricts sh to 1..4, so it always fits + * the 5-bit field. These are the only DP ops binop_takes_shifted_rhs admits. */ +static inline u32 arm_sub_reg_lsl(u32 rd, u32 rn, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xeba0u | rn, (imm3 << 12) | (rd << 8) | (imm2 << 6) | rm); +} +static inline u32 arm_and_reg_lsl(u32 rd, u32 rn, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xea00u | rn, (imm3 << 12) | (rd << 8) | (imm2 << 6) | rm); +} +static inline u32 arm_orr_reg_lsl(u32 rd, u32 rn, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xea40u | rn, (imm3 << 12) | (rd << 8) | (imm2 << 6) | rm); +} +static inline u32 arm_eor_reg_lsl(u32 rd, u32 rn, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xea80u | rn, (imm3 << 12) | (rd << 8) | (imm2 << 6) | rm); +} /* MOV.W rd, rm (ORR rd, 1111, rm). */ static inline u32 arm_mov_reg(u32 rd, u32 rm) { return arm_dp_reg(2u, 0u, rd, 0xfu, rm); } /* MVN.W rd, rm (ORN rd, 1111, rm). */ @@ -292,6 +312,70 @@ static inline u32 arm_nop32(void) { return arm_t32(0xf3afu, 0x8000u); } static inline u32 arm_push_w(u32 reglist) { return arm_t32(0xe92du, reglist & 0xdfffu); } static inline u32 arm_pop_w(u32 reglist) { return arm_t32(0xe8bdu, reglist & 0xffffu); } +/* --------- 16-bit narrow data-processing / move-immediate (Thumb-1) --------- + * EVERY 16-bit data-processing form below ALWAYS sets the condition flags (there + * is no S=0 16-bit DP encoding outside an IT block). The native backend may only + * emit these at FLAG-DEAD sites (kit keeps flags live solely across CMP→IT, and + * comparisons re-emit their CMP, so binop/move/shift results never carry a live + * flag dependency). Never narrow a MOV.W that sits between a CMP and its + * IT-predicated consumer. Low regs r0..r7 only (3-bit rd/rn/rm fields). */ +static inline u16 arm_movs_imm8(u32 rd, u32 imm8) { + return (u16)(0x2000u | ((rd & 7u) << 8) | (imm8 & 0xffu)); +} +static inline u16 arm_adds_imm8(u32 rdn, u32 imm8) { + return (u16)(0x3000u | ((rdn & 7u) << 8) | (imm8 & 0xffu)); +} +static inline u16 arm_subs_imm8(u32 rdn, u32 imm8) { + return (u16)(0x3800u | ((rdn & 7u) << 8) | (imm8 & 0xffu)); +} +static inline u16 arm_adds_imm3(u32 rd, u32 rn, u32 imm3) { + return (u16)(0x1c00u | ((imm3 & 7u) << 6) | ((rn & 7u) << 3) | (rd & 7u)); +} +static inline u16 arm_subs_imm3(u32 rd, u32 rn, u32 imm3) { + return (u16)(0x1e00u | ((imm3 & 7u) << 6) | ((rn & 7u) << 3) | (rd & 7u)); +} +static inline u16 arm_adds_reg16(u32 rd, u32 rn, u32 rm) { + return (u16)(0x1800u | ((rm & 7u) << 6) | ((rn & 7u) << 3) | (rd & 7u)); +} +static inline u16 arm_subs_reg16(u32 rd, u32 rn, u32 rm) { + return (u16)(0x1a00u | ((rm & 7u) << 6) | ((rn & 7u) << 3) | (rd & 7u)); +} +/* LSLS/LSRS/ASRS rd, rm, #imm5 (type LSL=0x0000, LSR=0x0800, ASR=0x1000). */ +static inline u16 arm_shift_imm16(u32 base, u32 rd, u32 rm, u32 imm5) { + return (u16)(base | ((imm5 & 0x1fu) << 6) | ((rm & 7u) << 3) | (rd & 7u)); +} +static inline u16 arm_lsls_imm16(u32 rd, u32 rm, u32 imm5) { return arm_shift_imm16(0x0000u, rd, rm, imm5); } +static inline u16 arm_lsrs_imm16(u32 rd, u32 rm, u32 imm5) { return arm_shift_imm16(0x0800u, rd, rm, imm5); } +static inline u16 arm_asrs_imm16(u32 rd, u32 rm, u32 imm5) { return arm_shift_imm16(0x1000u, rd, rm, imm5); } + +/* --------- 16-bit loads / stores (Thumb-1) --------- no flags --------- + * T1 [rn, #imm5*scale]: word ×4 (LDR 0x6800 / STR 0x6000), byte ×1 (LDRB 0x7800 + * / STRB 0x7000), half ×2 (LDRH 0x8800 / STRH 0x8000). rt/rn are low regs. + * T2 [sp, #imm8*4]: LDR 0x9800 / STR 0x9000, rt in [10:8]. */ +static inline u16 arm_ldst_i5_16(u32 base, u32 rt, u32 rn, u32 imm5) { + return (u16)(base | ((imm5 & 0x1fu) << 6) | ((rn & 7u) << 3) | (rt & 7u)); +} +static inline u16 arm_ldst_sp_16(u32 base, u32 rt, u32 imm8) { + return (u16)(base | ((rt & 7u) << 8) | (imm8 & 0xffu)); +} +/* ADD rd, sp, #imm8*4 (T1). */ +static inline u16 arm_add_sp_imm16(u32 rd, u32 imm8) { + return (u16)(0xa800u | ((rd & 7u) << 8) | (imm8 & 0xffu)); +} +/* ADD/SUB sp, sp, #imm7*4 (T2). No flags (special SP-adjust encoding). */ +static inline u16 arm_add_sp_sp_imm16(u32 imm7) { return (u16)(0xb000u | (imm7 & 0x7fu)); } +static inline u16 arm_sub_sp_sp_imm16(u32 imm7) { return (u16)(0xb080u | (imm7 & 0x7fu)); } + +/* 16-bit PUSH {list8, lr} / POP {list8, pc}. `list8` covers r0..r7; the high + * bit pulls in LR (push) or PC (pop). No flags. The common {r7,lr}/{r7,pc} + * frame pair fits exactly. */ +static inline u16 arm_push16(u32 list8, u32 with_lr) { + return (u16)(0xb400u | ((with_lr & 1u) << 8) | (list8 & 0xffu)); +} +static inline u16 arm_pop16(u32 list8, u32 with_pc) { + return (u16)(0xbc00u | ((with_pc & 1u) << 8) | (list8 & 0xffu)); +} + /* --------- bit-field extract / insert (Thumb-2 T1) --------- */ /* lsb is split into imm3:imm2 = (lsb>>2):(lsb&3). UBFX/SBFX encode width-1; * BFI/BFC encode msb = lsb + width - 1. */ diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -162,6 +162,26 @@ static void arm_patch_t32(Arm32NativeTarget* a, u32 pos, u32 instr) { obj_patch(a->base.obj, a->base.mc->section_id, pos, b, 4); } +/* Emit PUSH/POP, picking the 16-bit Thumb-1 form (STMDB sp!/LDMIA sp!) when the + * register list fits it (no flags). The 16-bit PUSH allows {r0..r7, lr}; POP + * allows {r0..r7, pc}; any other high register (r8..r12, or lr in a POP / pc in + * a PUSH) forces the 32-bit .W form. The common frame pairs {r7,lr}/{r7,pc} and + * a low-reg callee-save set narrow; mixed high callee-saves stay .W. */ +static void arm_emit_push(MCEmitter* mc, u32 reglist) { + u32 low = reglist & 0xffu, hi = reglist & ~0xffu; + if ((hi & ~(1u << 14u)) == 0u) /* only r0..r7 and optionally lr */ + arm_emit_t16(mc, arm_push16(low, (hi >> 14) & 1u)); + else + arm_emit_t32(mc, arm_push_w(reglist)); +} +static void arm_emit_pop(MCEmitter* mc, u32 reglist) { + u32 low = reglist & 0xffu, hi = reglist & ~0xffu; + if ((hi & ~(1u << 15u)) == 0u) /* only r0..r7 and optionally pc */ + arm_emit_t16(mc, arm_pop16(low, (hi >> 15) & 1u)); + else + arm_emit_t32(mc, arm_pop_w(reglist)); +} + /* ============================ register tables ============================ */ #define ARM_PHYS_ARG(r, idx) \ @@ -330,6 +350,44 @@ static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr, return addr->offset >= -255 && addr->offset <= 4095; } +/* Capability hook for the arch-neutral L7 shift-into-ALU fold + * (src/opt/pass_combine.c). arm32 has the 32-bit shifted-register forms wired up + * (arm_binop's register path emits ADD.W/SUB.W/AND.W/ORR.W/EOR.W rd,rn,rm,LSL #k + * via the arm_*_reg_lsl encoders), but the capability is left OFF here. + * + * WHY OFF: opting in surfaces a latent miscompile in the shared combine pass for + * the `base + (i << k)` address pattern when k exceeds the address index-scale + * range (k>=4, e.g. a 16-byte struct stride). The order is: addr-synth's index + * fold (pass_combine.c rule (b)) only absorbs a shift of 1..3 into the EA's + * log2_scale; for k=4 it declines, leaving the `i<<4` as an explicit IADD that + * computes the element address. L7 then stamps a shift rider on that IADD's rhs. + * On a later combine iteration, addr-synth rule (a) (reg+reg) folds that IADD + * into the indirect's index but copies only rhs.v.reg, DROPPING rhs.shift — so + * the `*16` silently becomes `*1` and every element aliases element 0 + * (test/parse/cases/opt_03_addr_index_alias_store regresses 42 -> 1 under qemu). + * aa64 avoids this because its addr_legal accepts the scaled-index EA, so + * addr-synth claims the address shift before L7 ever sees it; arm32's addr_legal + * rejects every indexed address (no scaled-index load/store in v1), so the + * shift stays in the ALU lane and the rider-drop bites. Re-enable once the + * shared pass preserves rhs.shift across the rule-(a) index fold; the encoders + + * arm_binop branch + decode round-trip test are already in place. */ +static int arm_can_fold_shift_into_alu(NativeTarget* t) { + (void)t; + return 0; +} + +/* ============================ 16-bit narrow helpers ============================ + * The 16-bit Thumb-1 data-processing forms used by the density pass always set + * the condition flags (there is no S=0 narrow DP form outside an IT block). kit + * keeps flags live only across CMP→IT (and comparisons always re-emit their + * CMP), so binop/move/shift/ldst results are FLAG-DEAD — narrowing them to a + * flag-setting 16-bit encoding is observationally identical. The MOV.W sites + * that sit between a CMP and an IT-predicated consumer (arm_cmp, arm_unop's + * UO_NOT, arm_emit_setcc) are NOT routed through here and stay 32-bit. */ +static inline int arm_low_reg(u32 r) { return r <= 7u; } +/* True when an unsigned immediate fits an n-bit field. */ +static inline int arm_uimm_fits(u32 v, u32 nbits) { return v < (1u << nbits); } + /* ============================ memory ============================ */ static void arm_emit_load_u32(Arm32NativeTarget* a, u32 rd, u32 v); @@ -342,6 +400,14 @@ static void arm_emit_add_const(Arm32NativeTarget* a, u32 rd, i64 k) { u32 enc, mag; if (k == 0) return; mag = (u32)(k < 0 ? -k : k); + /* 16-bit `ADD/SUB sp,sp,#imm7*4` (no flags) for the SP-adjust forms (e.g. the + * variadic GP-save reclaim). Requires rd==sp, a non-zero multiple of 4, and + * imm7 in range. */ + if (rd == 13u && (mag & 3u) == 0u && (mag >> 2) <= 0x7fu) { + arm_emit_t16(mc, k < 0 ? arm_sub_sp_sp_imm16(mag >> 2) + : arm_add_sp_sp_imm16(mag >> 2)); + return; + } if (mag <= 0xfffu) { arm_emit_t32(mc, k < 0 ? arm_sub_imm12(rd, rd, mag) : arm_add_imm12(rd, rd, mag)); @@ -527,6 +593,39 @@ static void arm_emit_mem_one(Arm32NativeTarget* a, int is_load, u32 rt, u32 base i32 off, u32 size) { MCEmitter* mc = a->base.mc; u32 t3hw1, t4hw1; + /* 16-bit narrow forms (no flags, biggest -O0 win since loads/stores dominate): + * T2 [sp, #imm8*4]: word only, rt low reg, base == sp (NOT r7 — the frame is + * r7-anchored, so this applies only to genuine sp-based slots). + * T1 [rn, #imm5*scale]: rt + rn both low regs (r7/ARM_FP qualifies), positive + * offset that is an exact multiple of the access size and fits 5 bits. + * Signed loads (LDRSB/LDRSH) have no 16-bit immediate-offset form, so the + * width dispatch below only narrows the unsigned word/byte/half loads + stores; + * the resolver never asks this function for a signed-narrowing load (that is a + * separate convert). */ + if (size == 4u && base == ARM_SP && arm_low_reg(rt) && off >= 0 && + (off & 3) == 0 && arm_uimm_fits((u32)off >> 2, 8u)) { + arm_emit_t16(mc, arm_ldst_sp_16(is_load ? 0x9800u : 0x9000u, rt, + (u32)off >> 2)); + return; + } + if (arm_low_reg(rt) && arm_low_reg(base) && off >= 0) { + u32 uoff = (u32)off; + if (size == 4u && (uoff & 3) == 0 && arm_uimm_fits(uoff >> 2, 5u)) { + arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x6800u : 0x6000u, rt, base, + uoff >> 2)); + return; + } + if (size == 2u && (uoff & 1) == 0 && arm_uimm_fits(uoff >> 1, 5u)) { + arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x8800u : 0x8000u, rt, base, + uoff >> 1)); + return; + } + if (size == 1u && arm_uimm_fits(uoff, 5u)) { + arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x7800u : 0x7000u, rt, base, + uoff)); + return; + } + } if (size == 1u) { t3hw1 = is_load ? 0xf890u : 0xf880u; t4hw1 = is_load ? 0xf810u : 0xf800u; @@ -576,6 +675,14 @@ static void arm_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) { u32 rd = loc_reg(dst); u32 v = (u32)imm; u32 enc; + /* MOVS rd,#imm8 (low reg, 0..255) — flag-dead 16-bit immediate move. This is + * the general NDT load_imm hook, never the CMP→IT flag-preserving MOV.W (those + * sites call arm_mov_imm directly), so setting flags here is observationally + * inert. */ + if (arm_low_reg(rd) && v <= 0xffu) { + arm_emit_t16(mc, arm_movs_imm8(rd, v)); + return; + } if (thumb_expand_imm_encode(v, &enc)) { arm_emit_t32(mc, arm_mov_imm(rd, enc)); return; @@ -685,23 +792,60 @@ static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc, int b_imm = b.kind == NATIVE_LOC_IMM; u32 rb = b_imm ? 0u : loc_reg(b); u32 enc; + /* L7 shifted-register fold (O1-only; the rider is set by pass_combine when the + * target advertised can_fold_shift_into_alu). `b` is a register pre-shifted + * left by `b.shift` (1..4): emit the one-instruction 32-bit shifted form + * `<op>.W rd,ra,rb,LSL #k` instead of a separate LSL + op. Only the five DP + * ops binop_takes_shifted_rhs admits ever carry a rider. The 16-bit forms have + * no shifted operand, so this forces a .W encoding — still a net -1 insn. + * NOTE: arm_can_fold_shift_into_alu currently returns 0 (see its comment for + * the shared-pass rider-drop miscompile), so b.shift is always 0 here today — + * this branch is the ready-to-go emit path for when the fold is re-enabled. */ + if (!b_imm && b.shift) { + u32 k = b.shift; + switch (op) { + case BO_IADD: arm_emit_t32(mc, arm_add_reg_lsl(rd, ra, rb, k)); return; + case BO_ISUB: arm_emit_t32(mc, arm_sub_reg_lsl(rd, ra, rb, k)); return; + case BO_AND: arm_emit_t32(mc, arm_and_reg_lsl(rd, ra, rb, k)); return; + case BO_OR: arm_emit_t32(mc, arm_orr_reg_lsl(rd, ra, rb, k)); return; + case BO_XOR: arm_emit_t32(mc, arm_eor_reg_lsl(rd, ra, rb, k)); return; + default: arm_panic(a, "shift rider on unsupported binop"); + } + } switch (op) { case BO_IADD: if (b_imm) { - if (thumb_expand_imm_encode((u32)b.v.imm, &enc)) + /* ADDS rd,rn,#imm3 (low regs) / ADDS rdn,#imm8 (in-place, low reg) — + * flag-dead 16-bit forms. The .W modified-imm/ADDW fallbacks below stay + * for high regs and wider immediates. */ + u32 v = (u32)b.v.imm; + if (arm_low_reg(rd) && arm_low_reg(ra) && arm_uimm_fits(v, 3u)) + arm_emit_t16(mc, arm_adds_imm3(rd, ra, v)); + else if (rd == ra && arm_low_reg(rd) && arm_uimm_fits(v, 8u)) + arm_emit_t16(mc, arm_adds_imm8(rd, v)); + else if (thumb_expand_imm_encode(v, &enc)) arm_emit_t32(mc, arm_dp_imm(8u, 0u, rd, ra, enc)); else - arm_emit_t32(mc, arm_add_imm12(rd, ra, (u32)b.v.imm & 0xfffu)); + arm_emit_t32(mc, arm_add_imm12(rd, ra, v & 0xfffu)); + } else if (arm_low_reg(rd) && arm_low_reg(ra) && arm_low_reg(rb)) { + arm_emit_t16(mc, arm_adds_reg16(rd, ra, rb)); /* flag-dead */ } else { arm_emit_t32(mc, arm_add_reg(rd, ra, rb)); } return; case BO_ISUB: if (b_imm) { - if (thumb_expand_imm_encode((u32)b.v.imm, &enc)) + u32 v = (u32)b.v.imm; + if (arm_low_reg(rd) && arm_low_reg(ra) && arm_uimm_fits(v, 3u)) + arm_emit_t16(mc, arm_subs_imm3(rd, ra, v)); + else if (rd == ra && arm_low_reg(rd) && arm_uimm_fits(v, 8u)) + arm_emit_t16(mc, arm_subs_imm8(rd, v)); + else if (thumb_expand_imm_encode(v, &enc)) arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, ra, enc)); else - arm_emit_t32(mc, arm_sub_imm12(rd, ra, (u32)b.v.imm & 0xfffu)); + arm_emit_t32(mc, arm_sub_imm12(rd, ra, v & 0xfffu)); + } else if (arm_low_reg(rd) && arm_low_reg(ra) && arm_low_reg(rb)) { + arm_emit_t16(mc, arm_subs_reg16(rd, ra, rb)); /* flag-dead */ } else { arm_emit_t32(mc, arm_sub_reg(rd, ra, rb)); } @@ -748,24 +892,40 @@ static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc, else arm_emit_t32(mc, arm_eor_reg(rd, ra, rb)); return; - case BO_SHL: - if (b_imm) - arm_emit_t32(mc, arm_shift_imm(0u, rd, ra, (u32)b.v.imm & 31u)); + case BO_SHL: { + /* LSLS rd,rm,#imm5 (low regs, flag-dead). LSL #0 is a plain move in both + * widths, so narrowing the whole 0..31 range is safe for LSL. */ + u32 sh = (u32)b.v.imm & 31u; + if (b_imm && arm_low_reg(rd) && arm_low_reg(ra)) + arm_emit_t16(mc, arm_lsls_imm16(rd, ra, sh)); + else if (b_imm) + arm_emit_t32(mc, arm_shift_imm(0u, rd, ra, sh)); else arm_emit_t32(mc, arm_shift_reg(0u, rd, ra, rb)); return; - case BO_SHR_U: - if (b_imm) - arm_emit_t32(mc, arm_shift_imm(1u, rd, ra, (u32)b.v.imm & 31u)); + } + case BO_SHR_U: { + /* LSRS: the 16-bit form encodes #32 as imm5=0, so only narrow #1..#31; + * #0 (no shift) keeps the .W form whose imm5=0 means #0 (a move). */ + u32 sh = (u32)b.v.imm & 31u; + if (b_imm && sh != 0u && arm_low_reg(rd) && arm_low_reg(ra)) + arm_emit_t16(mc, arm_lsrs_imm16(rd, ra, sh)); + else if (b_imm) + arm_emit_t32(mc, arm_shift_imm(1u, rd, ra, sh)); else arm_emit_t32(mc, arm_shift_reg(1u, rd, ra, rb)); return; - case BO_SHR_S: - if (b_imm) - arm_emit_t32(mc, arm_shift_imm(2u, rd, ra, (u32)b.v.imm & 31u)); + } + case BO_SHR_S: { + u32 sh = (u32)b.v.imm & 31u; /* ASRS: same #32-as-0 caveat as LSRS. */ + if (b_imm && sh != 0u && arm_low_reg(rd) && arm_low_reg(ra)) + arm_emit_t16(mc, arm_asrs_imm16(rd, ra, sh)); + else if (b_imm) + arm_emit_t32(mc, arm_shift_imm(2u, rd, ra, sh)); else arm_emit_t32(mc, arm_shift_reg(2u, rd, ra, rb)); return; + } default: arm_panic(a, "binop not lowered (FP / wide8 is Phase 2)"); } @@ -994,14 +1154,32 @@ static u32 arm_known_callee_saves(const NativeKnownFrameDesc* frame) { return mask; } +/* True (and sets *imm7) when `n` fits the 16-bit `SUB sp,sp,#imm7*4` form: + * a non-zero multiple of 4 with imm7 in 0..127 (n <= 508). The frame size is a + * multiple of 8, so the multiple-of-4 requirement is always met. */ +static int arm_sp_adjust_narrow(u32 n, u32* imm7) { + if (n == 0 || (n & 3u) != 0u || (n >> 2) > 0x7fu) return 0; + *imm7 = n >> 2; + return 1; +} + /* Build the `SUB sp, sp, #n` instruction sequence (n a multiple of 8) into - * words[], returning the count (<= ARM_NDT_SUB_WORDS). Small amounts use one - * inline SUBW; large amounts (n > 4095, big frames) stage n into IP via - * MOVW(+MOVT) and `SUB sp, sp, ip`. IP is the emit scratch, dead at the - * prologue. Shared by the inline known-frame emit and the single-pass patch. */ + * words[], returning the count (<= ARM_NDT_SUB_WORDS). A small frame fits the + * 16-bit `SUB sp,sp,#imm7*4`, packed (SUB16 in hw1, NOP16 in hw2) into one T32 + * slot so the single-pass deferred patch still overwrites exactly one reserved + * word; medium amounts use one SUBW/SUB.W modimm; large amounts (n > 4095, big + * frames) stage n into IP via MOVW(+MOVT) and `SUB sp, sp, ip`. IP is the emit + * scratch, dead at the prologue. Shared by the inline known-frame emit and the + * single-pass patch. */ static u32 arm_build_sub_sp(u32 n, u32* words) { - u32 enc, wi = 0; + u32 enc, imm7, wi = 0; if (n == 0) return 0; + if (arm_sp_adjust_narrow(n, &imm7)) { + /* hw1 = SUB sp (16-bit), hw2 = NOP (16-bit). arm_patch_t32 writes hw1 first + * in memory, so SUB executes then NOP. */ + words[wi++] = arm_t32(arm_sub_sp_sp_imm16(imm7), arm_nop16()); + return wi; + } if (n <= 0xfffu) { words[wi++] = arm_sub_imm12(13u, 13u, n); return wi; @@ -1016,10 +1194,17 @@ static u32 arm_build_sub_sp(u32 n, u32* words) { return wi; } -/* Emit `SUB sp, sp, #n` inline (known-frame prologue, where the size is final). */ +/* Emit `SUB sp, sp, #n` inline (known-frame prologue, where the size is final). + * The inline path is not slot-constrained, so a small frame emits a bare 16-bit + * SUB (no NOP padding); larger frames reuse arm_build_sub_sp's word list. */ static void arm_emit_sub_sp(Arm32NativeTarget* a, u32 n) { u32 words[ARM_NDT_SUB_WORDS]; - u32 nwords = arm_build_sub_sp(n, words), i; + u32 imm7, nwords, i; + if (arm_sp_adjust_narrow(n, &imm7)) { + arm_emit_t16(a->base.mc, arm_sub_sp_sp_imm16(imm7)); + return; + } + nwords = arm_build_sub_sp(n, words); for (i = 0; i < nwords; ++i) arm_emit_t32(a->base.mc, words[i]); } @@ -1109,8 +1294,8 @@ static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) { * just above the saved pair (so they are contiguous-below the named incoming * stack args at [r7 + 8 + 16]); va_start/va_arg walk forward across both. */ if (a->is_variadic) - arm_emit_t32(mc, arm_push_w(0xfu)); /* PUSH {r0-r3} */ - arm_emit_t32(mc, arm_push_w((1u << ARM_FP) | (1u << 14u))); /* PUSH {r7, lr} */ + arm_emit_push(mc, 0xfu); /* PUSH {r0-r3} (16-bit) */ + arm_emit_push(mc, (1u << ARM_FP) | (1u << 14u)); /* PUSH {r7, lr} (16-bit) */ arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ a->prologue_sub_pos = mc_pos(mc); for (u32 i = 0; i < ARM_NDT_SUB_WORDS; ++i) @@ -1225,9 +1410,9 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, * A variadic callee's r0..r3 save area sits just above the saved block (below * the named incoming stack args); va_start/va_arg walk forward across both, and * the teardown reclaims it (arm_emit_frame_restore). */ - if (a->is_variadic) arm_emit_t32(mc, arm_push_w(0xfu)); /* PUSH {r0-r3} */ - if (cs_mask) arm_emit_t32(mc, arm_push_w(cs_mask)); /* PUSH {callee-saves} */ - arm_emit_t32(mc, arm_push_w((1u << ARM_FP) | (1u << 14u))); /* PUSH {r7, lr} */ + if (a->is_variadic) arm_emit_push(mc, 0xfu); /* PUSH {r0-r3} */ + if (cs_mask) arm_emit_push(mc, cs_mask); /* PUSH {callee-saves} */ + arm_emit_push(mc, (1u << ARM_FP) | (1u << 14u)); /* PUSH {r7, lr} */ arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ frame = arm_frame_size(a); arm_emit_sub_sp(a, frame); @@ -1251,11 +1436,11 @@ static void arm_emit_frame_restore(Arm32NativeTarget* a, int to_pc) { u32 cs_mask = a->saved_reglist & ~((1u << ARM_FP) | (1u << 14u)); arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 (sp -> {r7,lr} pair) */ if (to_pc && va == 0u && cs_mask == 0u) { - arm_emit_t32(mc, arm_pop_w((1u << ARM_FP) | (1u << 15u))); /* POP {r7, pc} */ + arm_emit_pop(mc, (1u << ARM_FP) | (1u << 15u)); /* POP {r7, pc} (16-bit) */ return; } - arm_emit_t32(mc, arm_pop_w((1u << ARM_FP) | (1u << 14u))); /* POP {r7, lr} */ - if (cs_mask) arm_emit_t32(mc, arm_pop_w(cs_mask)); /* POP {callee-saves} */ + arm_emit_pop(mc, (1u << ARM_FP) | (1u << 14u)); /* POP {r7, lr} (16-bit) */ + if (cs_mask) arm_emit_pop(mc, cs_mask); /* POP {callee-saves} */ if (va) arm_emit_add_const(a, 13u, (i64)va); /* reclaim r0..r3 save */ if (to_pc) arm_emit_t16(mc, arm_bx(14u)); /* BX lr (return) */ } @@ -2109,7 +2294,7 @@ static u32 arm_copy_take_reg(Arm32NativeTarget* a, u32* used, u32* borrowed) { else { r = (*used & (1u << 4)) ? ((*used & (1u << 5)) ? 6u : 5u) : 4u; *borrowed = r; - arm_emit_t32(a->base.mc, arm_push_w(1u << r)); + arm_emit_push(a->base.mc, 1u << r); } *used |= 1u << r; return r; @@ -2171,7 +2356,7 @@ static void arm_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src, arm_emit_mem_one(a, 0, xfer, dbase, doff + (i32)off, sz); off += sz; } - if (borrowed != 0xffu) arm_emit_t32(a->base.mc, arm_pop_w(1u << borrowed)); + if (borrowed != 0xffu) arm_emit_pop(a->base.mc, 1u << borrowed); } /* Copy `size` bytes between two register-based addresses with the same 4|2|1 @@ -2416,7 +2601,7 @@ static void arm_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst, &pushmask); newv = temps[0]; status = temps[1]; - arm_emit_t32(mc, arm_push_w(pushmask)); + arm_emit_push(mc, pushmask); if (arm_order_release(order)) arm_emit_t32(mc, arm_dmb(0xfu)); /* Retry loop: rd = *base (LDREX); r4 = rd OP val; STREX r5,r4,[base]; retry * while r5 != 0. */ @@ -2453,7 +2638,7 @@ static void arm_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst, arm_emit_t32(mc, arm_b_cond_w(ARM_CC_NE)); /* BNE retry */ mc_emit_label_ref(mc, retry, R_ARM_THM_JUMP19, 4, 0); if (arm_order_acquire(order)) arm_emit_t32(mc, arm_dmb(0xfu)); - arm_emit_t32(mc, arm_pop_w(pushmask)); + arm_emit_pop(mc, pushmask); } static void arm_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok, @@ -2485,7 +2670,7 @@ static void arm_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok, (1u << base), 1u, statusv, &pushmask); status = statusv[0]; - arm_emit_t32(mc, arm_push_w(pushmask)); + arm_emit_push(mc, pushmask); if (arm_order_release(success)) arm_emit_t32(mc, arm_dmb(0xfu)); /* retry: prior = *base (LDREX); if prior != expected goto fail; STREX * r4,desired,[base]; retry on failure; ok = 1; goto done. fail: clear the @@ -2507,7 +2692,7 @@ static void arm_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok, arm_emit_t32(mc, arm_mov_imm(rok, enc0)); /* ok = 0 */ mc_label_place(mc, done); if (arm_order_acquire(success)) arm_emit_t32(mc, arm_dmb(0xfu)); - arm_emit_t32(mc, arm_pop_w(pushmask)); + arm_emit_pop(mc, pushmask); } static void arm_fence(NativeTarget* t, KitCgMemOrder order) { @@ -3316,6 +3501,7 @@ NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* obj, t->class_for_type = native_class_for_type_fp_le8; t->imm_legal = arm_imm_legal; t->addr_legal = arm_addr_legal; + t->can_fold_shift_into_alu = arm_can_fold_shift_into_alu; t->func_begin = arm_func_begin; t->func_begin_known_frame = arm_func_begin_known_frame; t->reserve_callee_saves = arm_reserve_callee_saves; diff --git a/test/arch/arm32_decode_test.c b/test/arch/arm32_decode_test.c @@ -410,6 +410,119 @@ static void decode_block_terminates(KitCompiler* pub) { EXPECT((insts[1].flags & KIT_DECODE_TERMINATOR) != 0, "b.w terminates"); } +/* The new 16-bit narrow encoders (density pass) round-trip against the existing + * decoder. Each asserts the rendered mnemonic + an operand needle; a couple also + * pin the raw 16-bit word so a bit-layout regression is caught at the encoder. */ +static void decode_narrow_16(KitCompiler* pub) { + unsigned char b[4]; + + /* PUSH {r4, r7, lr} = 0xb590 (list8=0x90, bit8=lr); POP {r7, pc} = 0xbd80. */ + EXPECT(arm_push16((1u << 4) | (1u << 7), 1u) == 0xb590u, "push16 bits = 0x%x", + (unsigned)arm_push16((1u << 4) | (1u << 7), 1u)); + put_t16(b, 0, arm_push16((1u << 7), 1u)); /* push {r7, lr} */ + expect_text(pub, b, 2, 0, 2, "push", "r7", "push16 r7"); + expect_text(pub, b, 2, 0, 2, "push", "lr", "push16 lr"); + EXPECT(arm_pop16((1u << 7), 1u) == 0xbd80u, "pop16 bits = 0x%x", + (unsigned)arm_pop16((1u << 7), 1u)); + put_t16(b, 0, arm_pop16((1u << 7), 1u)); /* pop {r7, pc} */ + expect_text(pub, b, 2, 0, 2, "pop", "pc", "pop16 pc"); + + /* LDR/STR [rn, #imm5*scale]: ldr r0,[r1,#4] = 0x6848 (imm5=1, ×4). */ + EXPECT(arm_ldst_i5_16(0x6800u, ARM_R0, ARM_R1, 1u) == 0x6848u, "ldr i5 bits"); + put_t16(b, 0, arm_ldst_i5_16(0x6800u, ARM_R0, ARM_R1, 1u)); + expect_text(pub, b, 2, 0, 2, "ldr", "[r1, #4]", "ldr i5"); + put_t16(b, 0, arm_ldst_i5_16(0x6000u, ARM_R2, ARM_R3, 2u)); /* str r2,[r3,#8] */ + expect_text(pub, b, 2, 0, 2, "str", "[r3, #8]", "str i5"); + put_t16(b, 0, arm_ldst_i5_16(0x7800u, ARM_R0, ARM_R1, 1u)); /* ldrb r0,[r1,#1] */ + expect_text(pub, b, 2, 0, 2, "ldrb", "[r1, #1]", "ldrb i5"); + put_t16(b, 0, arm_ldst_i5_16(0x8800u, ARM_R0, ARM_R1, 1u)); /* ldrh r0,[r1,#2] */ + expect_text(pub, b, 2, 0, 2, "ldrh", "[r1, #2]", "ldrh i5"); + + /* LDR/STR [sp, #imm8*4]: ldr r0,[sp,#4] = 0x9801. */ + EXPECT(arm_ldst_sp_16(0x9800u, ARM_R0, 1u) == 0x9801u, "ldr sp bits"); + put_t16(b, 0, arm_ldst_sp_16(0x9800u, ARM_R0, 1u)); + expect_text(pub, b, 2, 0, 2, "ldr", "[sp, #4]", "ldr sp"); + put_t16(b, 0, arm_ldst_sp_16(0x9000u, ARM_R2, 3u)); /* str r2,[sp,#12] */ + expect_text(pub, b, 2, 0, 2, "str", "[sp, #12]", "str sp"); + + /* ADD rd, sp, #imm8*4: add r0, sp, #16 = 0xa804. */ + put_t16(b, 0, arm_add_sp_imm16(ARM_R0, 4u)); + expect_text(pub, b, 2, 0, 2, "add", "sp", "add rd,sp"); + + /* ADD/SUB sp, sp, #imm7*4: sub sp,sp,#16 = 0xb084. */ + EXPECT(arm_sub_sp_sp_imm16(4u) == 0xb084u, "sub sp bits"); + put_t16(b, 0, arm_sub_sp_sp_imm16(4u)); + expect_text(pub, b, 2, 0, 2, "sub", "sp, sp", "sub sp,sp"); + put_t16(b, 0, arm_add_sp_sp_imm16(2u)); /* add sp,sp,#8 */ + expect_text(pub, b, 2, 0, 2, "add", "sp, sp", "add sp,sp"); + + /* MOVS/ADDS/SUBS rdn, #imm8. */ + put_t16(b, 0, arm_movs_imm8(ARM_R0, 5u)); + expect_text(pub, b, 2, 0, 2, "movs", "#5", "movs imm8"); + put_t16(b, 0, arm_adds_imm8(ARM_R1, 7u)); + expect_text(pub, b, 2, 0, 2, "adds", "#7", "adds imm8"); + put_t16(b, 0, arm_subs_imm8(ARM_R2, 9u)); + expect_text(pub, b, 2, 0, 2, "subs", "#9", "subs imm8"); + + /* ADDS/SUBS rd, rn, #imm3 and rd, rn, rm. */ + put_t16(b, 0, arm_adds_imm3(ARM_R0, ARM_R1, 3u)); + expect_text(pub, b, 2, 0, 2, "adds", "r0, r1, #3", "adds imm3"); + put_t16(b, 0, arm_subs_imm3(ARM_R0, ARM_R1, 2u)); + expect_text(pub, b, 2, 0, 2, "subs", "r0, r1, #2", "subs imm3"); + put_t16(b, 0, arm_adds_reg16(ARM_R0, ARM_R1, ARM_R2)); + expect_text(pub, b, 2, 0, 2, "adds", "r0, r1, r2", "adds reg16"); + put_t16(b, 0, arm_subs_reg16(ARM_R3, ARM_R4, ARM_R5)); + expect_text(pub, b, 2, 0, 2, "subs", "r3, r4, r5", "subs reg16"); + + /* LSLS/LSRS/ASRS rd, rm, #imm5. */ + put_t16(b, 0, arm_lsls_imm16(ARM_R0, ARM_R1, 3u)); + expect_text(pub, b, 2, 0, 2, "lsls", "r0, r1, #3", "lsls imm5"); + put_t16(b, 0, arm_lsrs_imm16(ARM_R0, ARM_R1, 4u)); + expect_text(pub, b, 2, 0, 2, "lsrs", "r0, r1, #4", "lsrs imm5"); + put_t16(b, 0, arm_asrs_imm16(ARM_R2, ARM_R3, 5u)); + expect_text(pub, b, 2, 0, 2, "asrs", "r2, r3, #5", "asrs imm5"); +} + +/* The 32-bit shifted-register operand encoders round-trip. The shift TEXT (lsl + * #k) rendering is owned by a concurrent disasm fix, so assert only the base + * mnemonic + rd/rn/rm and pin the raw encoding fields (hw1/hw2 + decoded shift) + * directly — those are this item's contract. */ +static void decode_shifted_reg(KitCompiler* pub) { + Compiler* c = (Compiler*)pub; + unsigned char b[4]; + KitDecodedInsn insn; + + /* add.w r0, r1, r2, lsl #3 -> hw1 0xeb01, hw2 imm3=0(0..2 of sh=3 -> imm2=3), + * encoded: arm_add_reg_lsl(r0,r1,r2,3) = 0xeb01 00c2. */ + EXPECT(arm_add_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 3u) == 0xeb0100c2u, + "add_reg_lsl bits = 0x%08x", + (unsigned)arm_add_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 3u)); + put_t32(b, 0, arm_add_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 3u)); + expect_text(pub, b, 4, 0, 4, "add.w", "r0, r1, r2", "add.w shifted reg"); + EXPECT(arm_sub_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 2u) == 0xeba10082u, + "sub_reg_lsl bits = 0x%08x", + (unsigned)arm_sub_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 2u)); + put_t32(b, 0, arm_sub_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 2u)); + expect_text(pub, b, 4, 0, 4, "sub.w", "r0, r1, r2", "sub.w shifted reg"); + put_t32(b, 0, arm_and_reg_lsl(ARM_R3, ARM_R4, ARM_R5, 1u)); + expect_text(pub, b, 4, 0, 4, "and.w", "r3, r4, r5", "and.w shifted reg"); + put_t32(b, 0, arm_orr_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 4u)); + expect_text(pub, b, 4, 0, 4, "orr.w", "r0, r1, r2", "orr.w shifted reg"); + put_t32(b, 0, arm_eor_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 2u)); + expect_text(pub, b, 4, 0, 4, "eor.w", "r0, r1, r2", "eor.w shifted reg"); + + /* The structured operand path recovers rd/rn/rm (the shift rider lives in the + * encoding fields, pinned above; the textual `lsl #k` render is owned by the + * concurrent disasm fix, so it is not asserted here). */ + put_t32(b, 0, arm_orr_reg_lsl(ARM_R0, ARM_R1, ARM_R2, 3u)); + memset(&insn, 0, sizeof insn); + EXPECT(arch_decode_one(c, b, 4, 0, &insn) == KIT_OK, "shifted reg decode"); + EXPECT(insn.noperands == 3, "shifted reg noperands = %u", + (unsigned)insn.noperands); + EXPECT(insn.operands[2].kind == KIT_DECOP_REG && insn.operands[2].reg == 2, + "shifted reg rm = r%u", (unsigned)insn.operands[2].reg); +} + /* Undecodable bytes fall back to .inst / .hword. */ static void decode_unknown_fallback(KitCompiler* pub) { unsigned char b[4]; @@ -442,6 +555,8 @@ int main(void) { decode_barriers(c); decode_ldrex_strex(c); decode_dsp_sat(c); + decode_narrow_16(c); + decode_shifted_reg(c); decode_it_block(c); decode_block_terminates(c); decode_unknown_fallback(c);