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:
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);